From aed12ac60fed3174c2ff46280f9e9af0d0bc8cf4 Mon Sep 17 00:00:00 2001 From: yetone Date: Mon, 28 Sep 2026 19:06:32 +0800 Subject: [PATCH] =?UTF-8?q?codex:=20the=20ChatGPT=20account's=20own=20mode?= =?UTF-8?q?ls=20Codex=20lists=20follow=20the=20user's=20picks=20too=20(?= =?UTF-8?q?=E5=9C=A8magpie=E9=87=8C=E6=8A=8AChatGPT=E6=8F=90=E4=BE=9B?= =?UTF-8?q?=E5=95=86=E5=8F=AA=E5=8B=BE=E9=80=89GPT-6-Sol=EF=BC=8Ccodex?= =?UTF-8?q?=E9=87=8C=E8=BF=98=E6=98=AF=E6=98=BE=E7=A4=BA=E6=89=80=E6=9C=89?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=20=E2=80=94=20@vemsux)=20=E2=80=94=20signed?= =?UTF-8?q?=20into=20ChatGPT=20with=20magpie=20as=20the=20base=20URL,=20Co?= =?UTF-8?q?dex=20asked=20the=20backend=20for=20its=20model=20list=20and=20?= =?UTF-8?q?magpie=20passed=20every=20model=20the=20account=20can=20reach?= =?UTF-8?q?=20straight=20through,=20so=20picking=20among=20them=20on=20the?= =?UTF-8?q?=20codex=20provider=20only=20ever=20narrowed=20magpie's=20own?= =?UTF-8?q?=20added=20models,=20never=20ChatGPT's.=20Now=20the=20gateway?= =?UTF-8?q?=20keeps=20that=20/models=20answer=20to=20the=20slugs=20the=20u?= =?UTF-8?q?ser=20kept=20(provider.CodexNativePicked,=20from=20the=20codex?= =?UTF-8?q?=20provider's=20picks);=20untouched=20picks=20leave=20the=20lis?= =?UTF-8?q?t=20whole,=20as=20before.=20The=20dedup=20that=20skips=20a=20Ch?= =?UTF-8?q?atGPT=20account's=20models=20in=20CodexListed=20is=20unchanged?= =?UTF-8?q?=20=E2=80=94=20the=20backend=20still=20lists=20them,=20now=20on?= =?UTF-8?q?ly=20the=20kept=20ones?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/gateway/codex_backend.go | 14 ++++++++++ internal/gateway/codex_backend_test.go | 38 ++++++++++++++++++++++++++ internal/provider/codex_models.go | 17 ++++++++++++ 3 files changed, 69 insertions(+) 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).