diff --git a/internal/provider/chatgpt_api.go b/internal/provider/chatgpt_api.go index b36f5811..ed7119f7 100644 --- a/internal/provider/chatgpt_api.go +++ b/internal/provider/chatgpt_api.go @@ -492,14 +492,39 @@ func siwcFetchModels(ctx context.Context, user string) ([]catalog.Model, error) } return nil, errors.New(msg) } - ms := parseSIWCModels(b) + ms, hidden := parseSIWCModels(b) + ms = siwcWithCodex(ms, hidden) if len(ms) == 0 { return nil, errors.New("ChatGPT lists no models for this account") } return ms, nil } -func parseSIWCModels(b []byte) []catalog.Model { +// siwcWithCodex adds to the account's catalog the models Codex lists for a +// ChatGPT plan: the token is served as Codex on the plan (a model it can't +// run is "not supported when using Codex with a ChatGPT account"), and runs +// Codex's newest models the catalog leaves out (#933: gpt-6-luna, +// gpt-6-sol and gpt-6.1-sol ran, listed by neither). Codex's list is the +// one magpie's Codex subscription last fetched, else Codex CLI's cache; a +// model the catalog hides stays out. +func siwcWithCodex(ms []catalog.Model, hidden []string) []catalog.Model { + codex, _, ok := catalog.Live("codex") + if !ok { + codex = catalog.Codex() + } + for _, m := range codex { + if m.ID == "" || strings.Contains(m.ID, "/") || slices.Contains(hidden, m.ID) || slices.ContainsFunc(ms, func(o catalog.Model) bool { return o.ID == m.ID }) { + continue + } + m.Provider, m.APIs = "openai", []string{string(Responses)} + ms = append(ms, m) + } + return ms +} + +// parseSIWCModels gives the catalog's models to list, and the slugs of +// those it hides. +func parseSIWCModels(b []byte) (out []catalog.Model, hidden []string) { var list struct { Models []struct { Slug string `json:"slug"` @@ -514,11 +539,14 @@ func parseSIWCModels(b []byte) []catalog.Model { } `json:"models"` } if json.Unmarshal(b, &list) != nil { - return nil + return nil, nil } - var out []catalog.Model for _, m := range list.Models { - if m.Slug == "" || m.Visibility != "" && m.Visibility != "list" { + if m.Slug == "" { + continue + } + if m.Visibility != "" && m.Visibility != "list" { + hidden = append(hidden, m.Slug) continue } mm := catalog.Model{ID: m.Slug, Name: m.DisplayName, Provider: "openai", Context: m.Context, APIs: []string{string(Responses)}} @@ -534,7 +562,7 @@ func parseSIWCModels(b []byte) []catalog.Model { } out = append(out, mm) } - return out + return out, hidden } // siwcDropped are the Responses fields Sign in with ChatGPT refuses: the diff --git a/internal/provider/chatgpt_api_models_test.go b/internal/provider/chatgpt_api_models_test.go new file mode 100644 index 00000000..7a591d4b --- /dev/null +++ b/internal/provider/chatgpt_api_models_test.go @@ -0,0 +1,47 @@ +package provider + +import ( + "context" + "os" + "path/filepath" + "testing" +) + +// #933: a ChatGPT API account lists, beside the models its catalog lists, +// those Codex lists for the plan, which the token runs though the catalog +// leaves them out (gpt-6-luna, added by hand, ran); one the catalog hides, +// and magpie's own in Codex's list, stay out. +func TestChatGPTAPIListsCodexModels(t *testing.T) { + home := claudeHome(t) + codex := filepath.Join(home, ".codex") + os.MkdirAll(codex, 0o755) + t.Setenv("CODEX_HOME", codex) + os.WriteFile(filepath.Join(codex, "models_cache.json"), []byte(`{"models":[ + {"slug":"gpt-6-luna","display_name":"GPT-6-Luna","priority":1,"context_window":272000,"supported_reasoning_levels":[{"effort":"low"},{"effort":"xhigh"}]}, + {"slug":"gpt-5.5","display_name":"GPT-5.5 (Codex)","priority":2}, + {"slug":"gpt-hidden","display_name":"Hidden","priority":3}, + {"slug":"deepseek/deepseek-v4","display_name":"DeepSeek V4","description":"DeepSeek V4 via magpie","priority":4}]}`), 0o644) + f := newFakeOpenAI(t) + if st := signInVia(t, f, "oaiapp_1", same); st.State != "done" { + t.Fatalf("sign-in %+v", st) + } + p, ok := siwcAccount() + if !ok { + t.Fatal("no account") + } + ms, err := p.Fetch(context.Background()) + if err != nil { + t.Fatal(err) + } + var ids []string + for _, m := range ms { + ids = append(ids, m.ID) + } + if len(ms) != 3 || ids[0] != "gpt-5.5" || ids[1] != "gpt-5.4-mini" || ids[2] != "gpt-6-luna" { + t.Fatalf("models %v", ids) + } + if ms[0].Name != "GPT-5.5" || ms[2].Provider != "openai" || len(ms[2].APIs) != 1 || ms[2].APIs[0] != string(Responses) || + len(ms[2].Efforts) != 2 { + t.Errorf("models %+v", ms) + } +}