This commit is contained in:
lxowalle 2026-04-22 10:39:11 +08:00
parent 80e0ff8393
commit da7b5d499c
4 changed files with 66 additions and 134 deletions

View file

@ -99,6 +99,7 @@ func createCodexAuthProvider() (LLMProvider, error) {
// - Model "openai/gpt-4o" -> ("openai", "gpt-4o")
// - Model "nvidia/z-ai/glm-5.1" -> ("nvidia", "z-ai/glm-5.1")
// - Provider "nvidia", Model "z-ai/glm-5.1" -> ("nvidia", "z-ai/glm-5.1")
// - Provider "openai", Model "openai/gpt-4o" -> ("openai", "openai/gpt-4o")
// - Model "gpt-4o" -> ("openai", "gpt-4o")
func ExtractProtocol(cfg *config.ModelConfig) (protocol, modelID string) {
if cfg == nil {
@ -107,13 +108,7 @@ func ExtractProtocol(cfg *config.ModelConfig) (protocol, modelID string) {
model := strings.TrimSpace(cfg.Model)
if provider := strings.TrimSpace(cfg.Provider); provider != "" {
normalized := NormalizeProvider(provider)
if prefix, rest, found := strings.Cut(model, "/"); found {
if NormalizeProvider(prefix) == normalized && strings.TrimSpace(rest) != "" {
return normalized, strings.TrimSpace(rest)
}
}
return normalized, model
return NormalizeProvider(provider), model
}
if model == "" {
return "", ""

View file

@ -84,16 +84,16 @@ func TestExtractProtocol(t *testing.T) {
wantModelID: "z-ai/glm-5.1",
},
{
name: "explicit provider strips redundant matching prefix",
name: "explicit provider preserves matching prefix",
config: &config.ModelConfig{Provider: "openai", Model: "openai/gpt-4o"},
wantProtocol: "openai",
wantModelID: "gpt-4o",
wantModelID: "openai/gpt-4o",
},
{
name: "explicit provider strips redundant aliased prefix",
name: "explicit provider preserves aliased prefix",
config: &config.ModelConfig{Provider: "qwen", Model: "qwen/qwen-plus"},
wantProtocol: "qwen-portal",
wantModelID: "qwen-plus",
wantModelID: "qwen/qwen-plus",
},
{
name: "empty provider segment",
@ -164,7 +164,7 @@ func TestCreateProviderFromConfig_UsesExplicitProvider(t *testing.T) {
}
}
func TestCreateProviderFromConfig_StripsRedundantExplicitProviderPrefix(t *testing.T) {
func TestCreateProviderFromConfig_PreservesExplicitProviderPrefixedModel(t *testing.T) {
cfg := &config.ModelConfig{
ModelName: "test-openai",
Provider: "openai",
@ -180,8 +180,8 @@ func TestCreateProviderFromConfig_StripsRedundantExplicitProviderPrefix(t *testi
if provider == nil {
t.Fatal("CreateProviderFromConfig() returned nil provider")
}
if modelID != "gpt-4o" {
t.Fatalf("modelID = %q, want %q", modelID, "gpt-4o")
if modelID != "openai/gpt-4o" {
t.Fatalf("modelID = %q, want %q", modelID, "openai/gpt-4o")
}
}

View file

@ -51,27 +51,6 @@ type modelResponse struct {
IsVirtual bool `json:"is_virtual"`
}
func normalizeExplicitProviderModel(mc *config.ModelConfig) {
if mc == nil {
return
}
provider := strings.TrimSpace(mc.Provider)
model := strings.TrimSpace(mc.Model)
if provider == "" || model == "" {
return
}
prefix, rest, found := strings.Cut(model, "/")
if !found || strings.TrimSpace(rest) == "" {
return
}
if providers.NormalizeProvider(prefix) == providers.NormalizeProvider(provider) {
mc.Model = strings.TrimSpace(rest)
}
}
// handleListModels returns all model_list entries with masked API keys.
//
// GET /api/models
@ -161,7 +140,6 @@ func (h *Handler) handleAddModel(w http.ResponseWriter, r *http.Request) {
if mc.APIKey != "" {
mc.ModelConfig.SetAPIKey(mc.APIKey)
}
normalizeExplicitProviderModel(&mc.ModelConfig)
cfg, err := config.LoadConfig(h.configPath)
if err != nil {
@ -288,7 +266,6 @@ func (h *Handler) handleUpdateModel(w http.ResponseWriter, r *http.Request) {
}
}
}
normalizeExplicitProviderModel(&mc.ModelConfig)
cfg.ModelList[idx] = &mc.ModelConfig

View file

@ -508,7 +508,7 @@ func TestHandleAddModel_PersistsProvider(t *testing.T) {
}
}
func TestHandleAddModel_NormalizesRedundantExplicitProviderPrefix(t *testing.T) {
func TestHandleAddModel_PreservesExplicitProviderPrefixedModel(t *testing.T) {
configPath, cleanup := setupOAuthTestEnv(t)
defer cleanup()
@ -538,8 +538,8 @@ func TestHandleAddModel_NormalizesRedundantExplicitProviderPrefix(t *testing.T)
if got := added.Provider; got != "openai" {
t.Fatalf("provider = %q, want %q", got, "openai")
}
if got := added.Model; got != "gpt-4o-mini" {
t.Fatalf("model = %q, want %q", got, "gpt-4o-mini")
if got := added.Model; got != "openai/gpt-4o-mini" {
t.Fatalf("model = %q, want %q", got, "openai/gpt-4o-mini")
}
}
@ -662,7 +662,7 @@ func TestHandleUpdateModel_PersistsProvider(t *testing.T) {
Model: "gpt-4o",
Provider: "openai",
}}
if err := config.SaveConfig(configPath, cfg); err != nil {
if err = config.SaveConfig(configPath, cfg); err != nil {
t.Fatalf("SaveConfig() error = %v", err)
}
@ -692,49 +692,7 @@ func TestHandleUpdateModel_PersistsProvider(t *testing.T) {
}
}
func TestHandleUpdateModel_PreservesProviderWhenOmitted(t *testing.T) {
configPath, cleanup := setupOAuthTestEnv(t)
defer cleanup()
cfg, err := config.LoadConfig(configPath)
if err != nil {
t.Fatalf("LoadConfig() error = %v", err)
}
cfg.ModelList = []*config.ModelConfig{{
ModelName: "editable",
Model: "z-ai/glm-5.1",
Provider: "nvidia",
}}
if err := config.SaveConfig(configPath, cfg); err != nil {
t.Fatalf("SaveConfig() error = %v", err)
}
h := NewHandler(configPath)
mux := http.NewServeMux()
h.RegisterRoutes(mux)
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPut, "/api/models/0", bytes.NewBufferString(`{
"model_name":"editable",
"model":"z-ai/glm-5.1"
}`))
req.Header.Set("Content-Type", "application/json")
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusOK, rec.Body.String())
}
updated, err := config.LoadConfig(configPath)
if err != nil {
t.Fatalf("LoadConfig() error = %v", err)
}
if got := updated.ModelList[0].Provider; got != "nvidia" {
t.Fatalf("provider = %q, want %q", got, "nvidia")
}
}
func TestHandleUpdateModel_NormalizesRedundantExplicitProviderPrefix(t *testing.T) {
func TestHandleUpdateModel_PreservesExplicitProviderPrefixedModel(t *testing.T) {
configPath, cleanup := setupOAuthTestEnv(t)
defer cleanup()
@ -747,7 +705,7 @@ func TestHandleUpdateModel_NormalizesRedundantExplicitProviderPrefix(t *testing.
Model: "gpt-4o",
Provider: "openai",
}}
if err := config.SaveConfig(configPath, cfg); err != nil {
if err = config.SaveConfig(configPath, cfg); err != nil {
t.Fatalf("SaveConfig() error = %v", err)
}
@ -775,8 +733,54 @@ func TestHandleUpdateModel_NormalizesRedundantExplicitProviderPrefix(t *testing.
if got := updated.ModelList[0].Provider; got != "openai" {
t.Fatalf("provider = %q, want %q", got, "openai")
}
if got := updated.ModelList[0].Model; got != "gpt-5.4" {
t.Fatalf("model = %q, want %q", got, "gpt-5.4")
if got := updated.ModelList[0].Model; got != "openai/gpt-5.4" {
t.Fatalf("model = %q, want %q", got, "openai/gpt-5.4")
}
}
func TestHandleListModels_PreservesExplicitProviderPrefixedModel(t *testing.T) {
configPath, cleanup := setupOAuthTestEnv(t)
defer cleanup()
cfg, err := config.LoadConfig(configPath)
if err != nil {
t.Fatalf("LoadConfig() error = %v", err)
}
cfg.ModelList = []*config.ModelConfig{{
ModelName: "openrouter-auto-explicit",
Provider: "openrouter",
Model: "openrouter/auto",
}}
if err := config.SaveConfig(configPath, cfg); err != nil {
t.Fatalf("SaveConfig() error = %v", err)
}
h := NewHandler(configPath)
mux := http.NewServeMux()
h.RegisterRoutes(mux)
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/models", nil)
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusOK, rec.Body.String())
}
var resp struct {
Models []modelResponse `json:"models"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("Unmarshal() error = %v", err)
}
if len(resp.Models) != 1 {
t.Fatalf("len(models) = %d, want 1", len(resp.Models))
}
if got := resp.Models[0].Provider; got != "openrouter" {
t.Fatalf("provider = %q, want %q", got, "openrouter")
}
if got := resp.Models[0].Model; got != "openrouter/auto" {
t.Fatalf("model = %q, want %q", got, "openrouter/auto")
}
}
@ -792,7 +796,7 @@ func TestHandleUpdateModel_PreservesLegacyModelPrefixWhenProviderOmitted(t *test
ModelName: "legacy-openrouter",
Model: "openrouter/openai/gpt-5.4",
}}
if err := config.SaveConfig(configPath, cfg); err != nil {
if err = config.SaveConfig(configPath, cfg); err != nil {
t.Fatalf("SaveConfig() error = %v", err)
}
@ -813,7 +817,7 @@ func TestHandleUpdateModel_PreservesLegacyModelPrefixWhenProviderOmitted(t *test
var listResp struct {
Models []modelResponse `json:"models"`
}
if err := json.Unmarshal(recList.Body.Bytes(), &listResp); err != nil {
if err = json.Unmarshal(recList.Body.Bytes(), &listResp); err != nil {
t.Fatalf("Unmarshal() error = %v", err)
}
if len(listResp.Models) != 1 {
@ -862,7 +866,7 @@ func TestHandleUpdateModel_PreservesLegacyModelPrefixWhenProviderOmittedAndModel
ModelName: "legacy-openrouter",
Model: "openrouter/openai/gpt-5.4",
}}
if err := config.SaveConfig(configPath, cfg); err != nil {
if err = config.SaveConfig(configPath, cfg); err != nil {
t.Fatalf("SaveConfig() error = %v", err)
}
@ -894,50 +898,6 @@ func TestHandleUpdateModel_PreservesLegacyModelPrefixWhenProviderOmittedAndModel
}
}
func TestHandleUpdateModel_OmittedProviderAllowsLegacyProviderChangeForSimpleVisibleModel(t *testing.T) {
configPath, cleanup := setupOAuthTestEnv(t)
defer cleanup()
cfg, err := config.LoadConfig(configPath)
if err != nil {
t.Fatalf("LoadConfig() error = %v", err)
}
cfg.ModelList = []*config.ModelConfig{{
ModelName: "legacy-openai",
Model: "openai/gpt-5.4",
}}
if err := config.SaveConfig(configPath, cfg); err != nil {
t.Fatalf("SaveConfig() error = %v", err)
}
h := NewHandler(configPath)
mux := http.NewServeMux()
h.RegisterRoutes(mux)
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPut, "/api/models/0", bytes.NewBufferString(`{
"model_name":"legacy-openai",
"model":"anthropic/claude-sonnet-4.6"
}`))
req.Header.Set("Content-Type", "application/json")
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusOK, rec.Body.String())
}
updated, err := config.LoadConfig(configPath)
if err != nil {
t.Fatalf("LoadConfig() error = %v", err)
}
if got := updated.ModelList[0].Provider; got != "" {
t.Fatalf("provider = %q, want empty", got)
}
if got := updated.ModelList[0].Model; got != "anthropic/claude-sonnet-4.6" {
t.Fatalf("model = %q, want %q", got, "anthropic/claude-sonnet-4.6")
}
}
func TestHandleListModels_ReturnsProviderField(t *testing.T) {
configPath, cleanup := setupOAuthTestEnv(t)
defer cleanup()