diff --git a/internal/gateway/codex_backend.go b/internal/gateway/codex_backend.go index ada50c16..d33a78ea 100644 --- a/internal/gateway/codex_backend.go +++ b/internal/gateway/codex_backend.go @@ -417,6 +417,20 @@ func (s *Server) codexModels(w http.ResponseWriter, r *http.Request) { own = append(own, e) } } + // The backend lists every model the ChatGPT account can reach. When the + // user picked among them on the codex provider, keep the list to those: + // their pick governs Codex's own models, not just magpie's added ones. + if keep, narrowed := provider.CodexNativePicked(); narrowed { + kept := own[:0] + for _, m := range own { + o, _ := m.(map[string]any) + if slug, _ := o["slug"].(string); slug != "" && !keep[slug] { + continue + } + kept = append(kept, m) + } + own = kept + } ms := provider.CodexListed() // the list is the backend's and magpie's, and so is its ETag w.Header().Set("ETag", codexcat.WithTag(etag, codexcat.Tag(ms))) diff --git a/internal/gateway/codex_backend_test.go b/internal/gateway/codex_backend_test.go index 6f0187d7..0f4cc777 100644 --- a/internal/gateway/codex_backend_test.go +++ b/internal/gateway/codex_backend_test.go @@ -357,6 +357,44 @@ func TestCodexModelList(t *testing.T) { } } +// The user's model picks on the ChatGPT account narrow the backend's own +// list too: Codex is shown only the native models kept, not every one the +// account can reach. +func TestCodexModelListNarrowedByPicks(t *testing.T) { + codexSignedIn(t) + if err := provider.Save(provider.Provider{ID: "codex", Models: []string{"gpt-6-sol"}}); err != nil { + t.Fatal(err) + } + up := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + io.WriteString(w, `{"models":[{"slug":"gpt-6-sol","priority":1},{"slug":"gpt-5.5","priority":2},{"slug":"gpt-5-codex","priority":3}]}`) + })) + defer up.Close() + was := provider.CodexBase + provider.CodexBase = up.URL + "/backend-api/codex" + defer func() { provider.CodexBase = was }() + + rec := httptest.NewRecorder() + req := httptest.NewRequest("GET", CodexPath+"/models", nil) + req.Header.Set("Authorization", "Bearer chatgpt-token") + New().Handler().ServeHTTP(rec, req) + var list struct { + Models []map[string]any `json:"models"` + } + json.Unmarshal(rec.Body.Bytes(), &list) + var native []string + for _, m := range list.Models { + slug, _ := m["slug"].(string) + // the backend's own models are the gpt-* ones; magpie's added + // entries carry their own slugs and stay + if strings.HasPrefix(slug, "gpt-") { + native = append(native, slug) + } + } + if rec.Code != 200 || len(native) != 1 || native[0] != "gpt-6-sol" { + t.Errorf("native models after picks = %v (want just gpt-6-sol); code %d", native, rec.Code) + } +} + // A reply's X-Models-Etag carries magpie's list too: Codex refetches its // model list on a new one, and only on the backend's it never would when a // provider was added. diff --git a/internal/provider/codex_models.go b/internal/provider/codex_models.go index e694efdf..a2941b93 100644 --- a/internal/provider/codex_models.go +++ b/internal/provider/codex_models.go @@ -202,6 +202,23 @@ func CodexListed() []catalog.Model { }) } +// CodexNativePicked is the set of the ChatGPT account's own model slugs the +// user kept, and whether they narrowed that list at all. The backend lists +// every model the account can reach; when the user has picked among them on +// the codex provider, the gateway keeps its /models answer to those (see +// codexModels). Not narrowed — the account's list is left whole. +func CodexNativePicked() (map[string]bool, bool) { + p, ok := find(All(), "codex") + if !ok || len(p.Models) == 0 { + return nil, false + } + keep := make(map[string]bool, len(p.Models)) + for _, id := range p.Models { + keep[id] = true + } + return keep, true +} + // codexListed marks a group Fast when a ChatGPT account's GPT model is in // it, so Codex offers /fast there too; the tier goes out only to that // account (buildResponses).