feat: enhance model listing to include aliases (#400)
introduce includeAliasesInList as a new configuration setting (default false) that includes aliases in v1/models Fixes #399
This commit is contained in:
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -376,8 +376,9 @@ func (pm *ProxyManager) listModelsHandler(c *gin.Context) {
|
||||
continue
|
||||
}
|
||||
|
||||
newRecord := func(modelId string) gin.H {
|
||||
record := gin.H{
|
||||
"id": id,
|
||||
"id": modelId,
|
||||
"object": "model",
|
||||
"created": createdTime,
|
||||
"owned_by": "llama-swap",
|
||||
@@ -396,8 +397,19 @@ func (pm *ProxyManager) listModelsHandler(c *gin.Context) {
|
||||
"llamaswap": modelConfig.Metadata,
|
||||
}
|
||||
}
|
||||
return record
|
||||
}
|
||||
|
||||
data = append(data, record)
|
||||
data = append(data, newRecord(id))
|
||||
|
||||
// Include aliases
|
||||
if pm.config.IncludeAliasesInList {
|
||||
for _, alias := range modelConfig.Aliases {
|
||||
if alias := strings.TrimSpace(alias); alias != "" {
|
||||
data = append(data, newRecord(alias))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by the "id" key
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user