subscriptions: a ChatGPT account lists the Codex models of its own plan, asked from ChatGPT with its own sign-in when models are refreshed — it was Codex CLI's cache, which is of whichever account Codex CLI last asked with, or nothing if it never ran, so a Free account was offered models its plan answers with a 400
This commit is contained in:
3 files changed
+144
-1
No files matched your search
@@ -640,7 +640,13 @@ func codexAccount(home string) (Provider, bool) {
|
||||
acct.sign = codexSign(func(ctx context.Context) (string, string, error) { return codexToken(ctx, path) })
|
||||
acct.body = codexBody
|
||||
acct.models = catalog.Codex
|
||||
acct.fetch = func(context.Context) ([]catalog.Model, error) { return catalog.Codex(), nil }
|
||||
acct.fetch = func(ctx context.Context) ([]catalog.Model, error) {
|
||||
ms, err := codexModels(ctx, acct.sign)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ms, catalog.SaveLive("codex", CodexBase, ms)
|
||||
}
|
||||
return Provider{ID: "codex", Name: "Codex", Icon: "codex-color", Responses: CodexBase, Website: "https://chatgpt.com/codex", Account: acct}, true
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
|
||||
"github.com/yetone/magpie/internal/catalog"
|
||||
)
|
||||
|
||||
// codexClientVersion is the Codex CLI version the models list is asked for
|
||||
// when Codex CLI has not asked itself yet: the list leaves out models newer
|
||||
// than the client asking.
|
||||
const codexClientVersion = "0.154.0"
|
||||
|
||||
// codexModels asks the ChatGPT backend which Codex models the account's own
|
||||
// plan has — a Free account lists fewer than a Plus or Pro one, and one it
|
||||
// doesn't have fails with a 400. It is the list Codex CLI keeps in
|
||||
// models_cache.json, but that one is of whichever account Codex CLI last
|
||||
// asked with, if it ran at all.
|
||||
func codexModels(ctx context.Context, sign func(context.Context, *http.Request, []byte) error) ([]catalog.Model, error) {
|
||||
u := CodexBase + "/models?client_version=" + url.QueryEscape(codexCacheVersion())
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := sign(ctx, req, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
b, _ := io.ReadAll(io.LimitReader(resp.Body, 4<<20))
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("ChatGPT models: %s", resp.Status)
|
||||
}
|
||||
ms := parseCodexModels(b)
|
||||
if len(ms) == 0 {
|
||||
return nil, errors.New("ChatGPT listed no Codex models")
|
||||
}
|
||||
return ms, nil
|
||||
}
|
||||
|
||||
// codexCacheVersion is the client version Codex CLI last asked with.
|
||||
func codexCacheVersion() string {
|
||||
home, _ := os.UserHomeDir()
|
||||
var c struct {
|
||||
ClientVersion string `json:"client_version"`
|
||||
}
|
||||
if b, err := os.ReadFile(filepath.Join(home, ".codex", "models_cache.json")); err == nil && json.Unmarshal(b, &c) == nil && c.ClientVersion != "" {
|
||||
return c.ClientVersion
|
||||
}
|
||||
return codexClientVersion
|
||||
}
|
||||
|
||||
// parseCodexModels reads the backend's list, the listed ones in its order.
|
||||
func parseCodexModels(b []byte) []catalog.Model {
|
||||
var list struct {
|
||||
Models []struct {
|
||||
Slug string `json:"slug"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Visibility string `json:"visibility"`
|
||||
Priority int `json:"priority"`
|
||||
Levels []struct {
|
||||
Effort string `json:"effort"`
|
||||
} `json:"supported_reasoning_levels"`
|
||||
} `json:"models"`
|
||||
}
|
||||
if json.Unmarshal(b, &list) != nil {
|
||||
return nil
|
||||
}
|
||||
sort.SliceStable(list.Models, func(i, j int) bool { return list.Models[i].Priority < list.Models[j].Priority })
|
||||
var out []catalog.Model
|
||||
for _, m := range list.Models {
|
||||
if m.Slug == "" || m.Visibility == "hide" {
|
||||
continue
|
||||
}
|
||||
mm := catalog.Model{ID: m.Slug, Name: m.DisplayName, Provider: "openai"}
|
||||
for _, l := range m.Levels {
|
||||
mm.Efforts = append(mm.Efforts, l.Effort)
|
||||
}
|
||||
out = append(out, mm)
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// A ChatGPT account lists the models of its own plan, asked with its own
|
||||
// token; the cache Codex CLI keeps may be another account's.
|
||||
func TestCodexModelsOfTheAccount(t *testing.T) {
|
||||
signIn(t)
|
||||
fake := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/backend-api/codex/models" || r.Header.Get("chatgpt-account-id") != "acct-1" || r.URL.Query().Get("client_version") == "" {
|
||||
w.WriteHeader(400)
|
||||
return
|
||||
}
|
||||
w.Write([]byte(`{"models": [
|
||||
{"slug": "gpt-5.6-luna", "display_name": "GPT-5.6 Luna", "visibility": "list", "priority": 2,
|
||||
"supported_reasoning_levels": [{"effort": "low"}, {"effort": "medium"}]},
|
||||
{"slug": "codex-auto-review", "visibility": "hide", "priority": 1}]}`))
|
||||
}))
|
||||
defer fake.Close()
|
||||
old := CodexBase
|
||||
CodexBase = fake.URL + "/backend-api/codex"
|
||||
t.Cleanup(func() { CodexBase = old })
|
||||
|
||||
p, ok := find(All(), "codex")
|
||||
if !ok {
|
||||
t.Fatal("no codex account")
|
||||
}
|
||||
if ms := p.Available(); len(ms) != 1 || ms[0].ID != "gpt-5.5" {
|
||||
t.Fatalf("before fetching, Codex CLI's cache: %+v", ms)
|
||||
}
|
||||
if _, err := p.Fetch(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ms := p.Available()
|
||||
if len(ms) != 1 || ms[0].ID != "gpt-5.6-luna" || len(ms[0].Efforts) != 2 {
|
||||
t.Fatalf("after fetching: %+v", ms)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user