diff --git a/pkg/providers/factory_provider.go b/pkg/providers/factory_provider.go index 140d140e3..86d009811 100644 --- a/pkg/providers/factory_provider.go +++ b/pkg/providers/factory_provider.go @@ -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 "", "" diff --git a/pkg/providers/factory_provider_test.go b/pkg/providers/factory_provider_test.go index d691d2398..3dd1eefb3 100644 --- a/pkg/providers/factory_provider_test.go +++ b/pkg/providers/factory_provider_test.go @@ -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") } } diff --git a/web/backend/api/models.go b/web/backend/api/models.go index 459bd7655..cf903ce4c 100644 --- a/web/backend/api/models.go +++ b/web/backend/api/models.go @@ -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 diff --git a/web/backend/api/models_test.go b/web/backend/api/models_test.go index b43b569c6..f374ac15b 100644 --- a/web/backend/api/models_test.go +++ b/web/backend/api/models_test.go @@ -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()