diff --git a/config-schema.json b/config-schema.json index f44c7034..2f539a94 100644 --- a/config-schema.json +++ b/config-schema.json @@ -74,6 +74,11 @@ "default": false, "description": "Inject loading status updates into the reasoning field. When true, a stream of loading messages will be sent to the client." }, + "includeAliasesInList": { + "type": "boolean", + "default": false, + "description": "Present aliases within the /v1/models OpenAI API listing. when true, model aliases will be output to the API model listing duplicating all fields except for Id so chat UIs can use the alias equivalent to the original." + }, "macros": { "$ref": "#/definitions/macros" }, @@ -247,4 +252,4 @@ "description": "A dictionary of event triggers and actions. Only supported hook is on_startup." } } -} \ No newline at end of file +} diff --git a/config.example.yaml b/config.example.yaml index 6af6268f..b8437bb3 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -46,6 +46,12 @@ startPort: 10001 # - see #366 for more details sendLoadingState: true +# includeAliasesInList: present aliases within the /v1/models OpenAI API listing +# - optional, default: false +# - when true, model aliases will be output to the API model listing duplicating +# all fields except for Id so chat UIs can use the alias equivalent to the original. +includeAliasesInList: false + # macros: a dictionary of string substitutions # - optional, default: empty dictionary # - macros are reusable snippets diff --git a/proxy/config/config.go b/proxy/config/config.go index d957cd41..e5497582 100644 --- a/proxy/config/config.go +++ b/proxy/config/config.go @@ -132,6 +132,9 @@ type Config struct { // send loading state in reasoning SendLoadingState bool `yaml:"sendLoadingState"` + + // present aliases to /v1/models OpenAI API listing + IncludeAliasesInList bool `yaml:"includeAliasesInList"` } func (c *Config) RealModelName(search string) (string, bool) { diff --git a/proxy/proxymanager.go b/proxy/proxymanager.go index 1589341a..d83f50ed 100644 --- a/proxy/proxymanager.go +++ b/proxy/proxymanager.go @@ -376,28 +376,40 @@ func (pm *ProxyManager) listModelsHandler(c *gin.Context) { continue } - record := gin.H{ - "id": id, - "object": "model", - "created": createdTime, - "owned_by": "llama-swap", + newRecord := func(modelId string) gin.H { + record := gin.H{ + "id": modelId, + "object": "model", + "created": createdTime, + "owned_by": "llama-swap", + } + + if name := strings.TrimSpace(modelConfig.Name); name != "" { + record["name"] = name + } + if desc := strings.TrimSpace(modelConfig.Description); desc != "" { + record["description"] = desc + } + + // Add metadata if present + if len(modelConfig.Metadata) > 0 { + record["meta"] = gin.H{ + "llamaswap": modelConfig.Metadata, + } + } + return record } - if name := strings.TrimSpace(modelConfig.Name); name != "" { - record["name"] = name - } - if desc := strings.TrimSpace(modelConfig.Description); desc != "" { - record["description"] = desc - } + data = append(data, newRecord(id)) - // Add metadata if present - if len(modelConfig.Metadata) > 0 { - record["meta"] = gin.H{ - "llamaswap": modelConfig.Metadata, + // Include aliases + if pm.config.IncludeAliasesInList { + for _, alias := range modelConfig.Aliases { + if alias := strings.TrimSpace(alias); alias != "" { + data = append(data, newRecord(alias)) + } } } - - data = append(data, record) } // Sort by the "id" key diff --git a/proxy/proxymanager_test.go b/proxy/proxymanager_test.go index 1bc5a128..bbe5e936 100644 --- a/proxy/proxymanager_test.go +++ b/proxy/proxymanager_test.go @@ -437,6 +437,70 @@ func TestProxyManager_ListModelsHandler_SortedByID(t *testing.T) { } } +func TestProxyManager_ListModelsHandler_IncludeAliasesInList(t *testing.T) { + // Configure alias + config := config.Config{ + HealthCheckTimeout: 15, + IncludeAliasesInList: true, + Models: map[string]config.ModelConfig{ + "model1": func() config.ModelConfig { + mc := getTestSimpleResponderConfig("model1") + mc.Name = "Model 1" + mc.Aliases = []string{"alias1"} + return mc + }(), + }, + LogLevel: "error", + } + + proxy := New(config) + + // Request models list + req := httptest.NewRequest("GET", "/v1/models", nil) + w := CreateTestResponseRecorder() + proxy.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + + var response struct { + Data []map[string]interface{} `json:"data"` + } + if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil { + t.Fatalf("Failed to parse JSON response: %v", err) + } + + // We expect both base id and alias + var model1Data, alias1Data map[string]any + for _, model := range response.Data { + if model["id"] == "model1" { + model1Data = model + } else if model["id"] == "alias1" { + alias1Data = model + } + } + + // Verify model1 has name + assert.NotNil(t, model1Data) + _, exists := model1Data["name"] + if !assert.True(t, exists, "model1 should have name key") { + t.FailNow() + } + name1, ok := model1Data["name"].(string) + assert.True(t, ok, "name1 should be a string") + + // Verify alias1 has name + assert.NotNil(t, alias1Data) + _, exists = alias1Data["name"] + if !assert.True(t, exists, "alias1 should have name key") { + t.FailNow() + } + name2, ok := alias1Data["name"].(string) + assert.True(t, ok, "name2 should be a string") + + // Name keys should match + assert.Equal(t, name1, name2) +} + func TestProxyManager_Shutdown(t *testing.T) { // make broken model configurations model1Config := getTestSimpleResponderConfigPort("model1", 9991)