From 206e2cf09bdb4c2a96e413e5be39746d29923e39 Mon Sep 17 00:00:00 2001 From: Reidho Satria Date: Mon, 17 Aug 2026 21:17:24 +0700 Subject: [PATCH] fix(providers): show legacy custom provider card --- internal/server/handlers_config.go | 29 ++++++-- internal/server/handlers_config_test.go | 92 +++++++++++++++++++++++++ 2 files changed, 115 insertions(+), 6 deletions(-) create mode 100644 internal/server/handlers_config_test.go diff --git a/internal/server/handlers_config.go b/internal/server/handlers_config.go index 47338db..6fb6ffe 100644 --- a/internal/server/handlers_config.go +++ b/internal/server/handlers_config.go @@ -173,19 +173,23 @@ func (s *Server) handleModelOptions(w http.ResponseWriter, r *http.Request) { // Every provider from the catalogue (configured or not), so the new kinds // can be set up from here — then any custom providers only in the config. - // The catalogue's "custom" entry belongs to the first-run wizard only: it - // never renders here, so the providers page shows solely built-ins and - // providers the user created (named, manageable, deletable). + // The catalogue's "custom" entry belongs to the first-run wizard, but older + // installations may still use that id for their real custom provider. seen := map[string]bool{} providerList := make([]providerInfo, 0) for _, sp := range setupProviderCatalogue(cfg) { - if sp.Custom { + p := cfg.Providers[sp.ID] + if sp.Custom && !legacyCustomProviderInUse(cfg, p) { seen[sp.ID] = true continue } - p := cfg.Providers[sp.ID] + label, kind := sp.Label, sp.Kind + if sp.Custom { + label = firstNonEmpty(p.Label, sp.Label) + kind = firstNonEmpty(p.Kind, sp.Kind) + } providerList = append(providerList, providerInfo{ - ID: sp.ID, Label: sp.Label, Kind: sp.Kind, + ID: sp.ID, Label: label, Kind: kind, Enabled: p.Enabled, HasKey: p.APIKey != "", Local: sp.Local, BaseURL: firstNonEmpty(p.BaseURL, sp.BaseURL), Active: sp.ID == cfg.Model.Provider, Hint: sp.Hint, KeyHint: sp.KeyHint, KeyURL: sp.KeyURL, KeyLabel: sp.KeyLabel, @@ -217,6 +221,19 @@ func (s *Server) handleModelOptions(w http.ResponseWriter, r *http.Request) { }) } +func legacyCustomProviderInUse(cfg *config.Config, p config.Provider) bool { + return cfg.Model.Provider == "custom" || + p.Enabled || + strings.TrimSpace(p.BaseURL) != "" || + strings.TrimSpace(p.APIKey) != "" || + strings.TrimSpace(p.APIKeyEnv) != "" || + strings.TrimSpace(p.APIVersion) != "" || + strings.TrimSpace(p.Region) != "" || + len(p.Headers) > 0 || + len(p.Models) > 0 || + len(p.ModelMeta) > 0 +} + func (s *Server) handleModelList(w http.ResponseWriter, r *http.Request) { provider := r.URL.Query().Get("provider") cfg := s.config() diff --git a/internal/server/handlers_config_test.go b/internal/server/handlers_config_test.go new file mode 100644 index 0000000..5d3db93 --- /dev/null +++ b/internal/server/handlers_config_test.go @@ -0,0 +1,92 @@ +package server + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/enowdev/antares/internal/config" +) + +type modelOptionsProvider struct { + ID string `json:"id"` + Label string `json:"label"` + Kind string `json:"kind"` + Enabled bool `json:"enabled"` + HasKey bool `json:"has_key"` + BaseURL string `json:"base_url"` + Active bool `json:"active"` + Custom bool `json:"custom"` + NeedsBaseURL bool `json:"needs_base_url"` +} + +type modelOptionsResponse struct { + Providers []modelOptionsProvider `json:"providers"` +} + +func modelOptions(t *testing.T, cfg *config.Config) modelOptionsResponse { + t.Helper() + s := &Server{cfg: cfg} + rr := httptest.NewRecorder() + s.handleModelOptions(rr, httptest.NewRequest(http.MethodGet, "/api/model/options", nil)) + if rr.Code != http.StatusOK { + t.Fatalf("model options: status = %d (body=%s)", rr.Code, rr.Body.String()) + } + var response modelOptionsResponse + if err := json.Unmarshal(rr.Body.Bytes(), &response); err != nil { + t.Fatalf("decode model options: %v", err) + } + return response +} + +func customProviderCard(response modelOptionsResponse) (modelOptionsProvider, int) { + var match modelOptionsProvider + count := 0 + for _, provider := range response.Providers { + if provider.ID != "custom" { + continue + } + match = provider + count++ + } + return match, count +} + +func TestModelOptionsShowsConfiguredLegacyCustomProvider(t *testing.T) { + cfg := config.Default() + cfg.Providers["custom"] = config.Provider{ + Kind: "openai-compatible", BaseURL: "https://legacy.example/v1", + APIKey: "legacy-key", Enabled: true, Label: "Something else", + } + cfg.Model.Provider = "custom" + + got, count := customProviderCard(modelOptions(t, cfg)) + if count != 1 { + t.Fatalf("legacy custom provider cards = %d, want 1", count) + } + if got.Label != "Something else" { + t.Errorf("label = %q, want the stored legacy label", got.Label) + } + if got.Kind != "openai-compatible" { + t.Errorf("kind = %q, want the stored legacy kind", got.Kind) + } + if got.BaseURL != "https://legacy.example/v1" { + t.Errorf("base_url = %q, want the stored legacy endpoint", got.BaseURL) + } + if !got.Enabled || !got.HasKey || !got.Active || !got.Custom || !got.NeedsBaseURL { + t.Errorf("legacy card flags = %+v, want enabled, keyed, active, custom, and needs_base_url", got) + } +} + +func TestModelOptionsHidesUnusedLegacyCustomPlaceholder(t *testing.T) { + cfg := config.Default() + cfg.Providers["custom"] = config.Provider{ + Kind: "custom", Label: "Custom endpoint", TimeoutSecs: 300, + } + + _, count := customProviderCard(modelOptions(t, cfg)) + if count != 0 { + t.Fatalf("unused legacy custom provider cards = %d, want 0", count) + } +}