gateway: a ChatGPT account's Automatic image model is gpt-image-2, the one Codex CLI asks for, and chatgpt.com's 403 says what it likely means (#545) — Automatic picked gpt-image-2.5 as the newest, but Codex CLI only ever asks the images API for gpt-image-2, and a plan may be refused other models: azir12345's images with no model set came back "chatgpt.com: 403 Forbidden: {"detail":"Forbidden"}". Image requests also send Codex's x-codex-image-turn-id (codex-rs ext/image-generation). gpt-image-2.5 stays pickable in Settings → Images. TestCodexAccountDraws (Automatic) and the new TestCodexDrawRefused fail on the old code and pass. go vet, builds (darwin, GOOS=linux, GOOS=windows) pass; go test -tags nogui ./... under a temp HOME passes but TestProvidersAnswerWhileListsComeIn, TestUserPath and TestClaudeSignedOut, timing tests that pass alone. Not tried against a real ChatGPT account, so whether 2.5 is what was refused is unconfirmed.

This commit is contained in:
yetone committed 2026-10-03 14:28:55 +08:00
1 parent 243c5a0429
commit 18e6cb8140
2 files changed
+44 -3

No files matched your search

+15 -1
View File
@@ -68,6 +68,10 @@ var codexDrawers = []catalog.Model{
{ID: "gpt-image-2.5", Name: "GPT Image 2.5", Released: "2026-06-01"},
}
// codexDrawer is the image model Codex CLI asks a ChatGPT account for, the
// one Automatic picks there.
const codexDrawer = "gpt-image-2"
// grokDrawers are the image models a Grok subscription (SuperGrok, X Premium+)
// draws with, at the Imagine API of the backend Grok Build's image_gen tool
// calls: cli-chat-proxy.grok.com/v1/images/generations.
@@ -146,6 +150,11 @@ func AutoDrawer() string {
continue
}
for _, m := range Drawers(p) {
if drawsCodex(p) && m.ID != codexDrawer {
// Codex CLI draws with gpt-image-2 only; a plan may be
// refused the others (chatgpt.com: 403, #545)
continue
}
cost := 1e9
switch {
case drawsCodex(p), drawsGrok(p):
@@ -553,6 +562,7 @@ func (s *Server) sendWith(ctx context.Context, p provider.Provider, method, url,
// signing asks of its Responses; the id is Codex CLI's own
req.Header.Set("Accept", "application/json")
req.Header.Set("x-codex-imagegen-request-id", newUUID())
req.Header.Set("x-codex-image-turn-id", newUUID())
}
} else {
// Google's API keys go in their own header
@@ -577,7 +587,11 @@ func (s *Server) sendWith(ctx context.Context, p provider.Provider, method, url,
return nil, 502, err
}
if res.StatusCode >= 300 {
return b, res.StatusCode, fmt.Errorf("%s: %d %s%s", provider.HostOf(url), res.StatusCode, http.StatusText(res.StatusCode), vendorSaid(vendorMessage(b)))
hint := ""
if drawsCodex(p) && res.StatusCode == http.StatusForbidden {
hint = fmt.Sprintf(" — ChatGPT turned %s's images away: its plan or workspace may not draw, or not with this model (Codex CLI draws with %s)", p.Name, codexDrawer)
}
return b, res.StatusCode, fmt.Errorf("%s: %d %s%s%s", provider.HostOf(url), res.StatusCode, http.StatusText(res.StatusCode), vendorSaid(vendorMessage(b)), hint)
}
return b, res.StatusCode, nil
}
+29 -2
View File
@@ -298,7 +298,8 @@ func TestCodexAccountDraws(t *testing.T) {
if ds := Drawers(*p); len(ds) != 2 || ds[0].ID != "gpt-image-2" || ds[1].ID != "gpt-image-2.5" {
t.Fatalf("drawers %v", ds)
}
if m, ok := drawer(); !ok || m != "codex/gpt-image-2.5" {
// Automatic asks for what Codex CLI asks for: a plan may be refused 2.5
if m, ok := drawer(); !ok || m != "codex/gpt-image-2" {
t.Fatalf("drawer = %q %v", m, ok)
}
s := New()
@@ -311,7 +312,7 @@ func TestCodexAccountDraws(t *testing.T) {
if paths[0] != "/backend-api/codex/images/generations" || !strings.Contains(bodies[0], `"model":"gpt-image-2"`) {
t.Fatalf("asked %v %v", paths, bodies)
}
if head.Get("chatgpt-account-id") != "acct-1" || head.Get("Accept") != "application/json" || head.Get("originator") != "codex_cli_rs" || head.Get("x-codex-imagegen-request-id") == "" {
if head.Get("chatgpt-account-id") != "acct-1" || head.Get("Accept") != "application/json" || head.Get("originator") != "codex_cli_rs" || head.Get("x-codex-imagegen-request-id") == "" || head.Get("x-codex-image-turn-id") == "" {
t.Fatalf("headers %v", head)
}
// an edit goes as JSON, the image a data URL: the backend turns multipart away
@@ -324,6 +325,32 @@ func TestCodexAccountDraws(t *testing.T) {
}
}
// chatgpt.com's 403 for a ChatGPT account's images says what it likely
// means, and Automatic asks for the model Codex CLI does (#545).
func TestCodexDrawRefused(t *testing.T) {
codexSignedIn(t)
var models []string
up := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var req struct{ Model string }
json.NewDecoder(r.Body).Decode(&req)
models = append(models, req.Model)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusForbidden)
io.WriteString(w, `{"detail":"Forbidden"}`)
}))
defer up.Close()
was := provider.CodexBase
provider.CodexBase = up.URL + "/backend-api/codex"
defer func() { provider.CodexBase = was }()
code, _, raw := postImages(t, New(), "/v1/images/generations", "application/json", `{"prompt":"a magpie"}`)
if code != 403 || !strings.Contains(raw, "Forbidden") || !strings.Contains(raw, "plan or workspace may not draw") {
t.Fatalf("%d %s", code, raw)
}
if len(models) != 1 || models[0] != "gpt-image-2" {
t.Fatalf("asked for %v", models)
}
}
// grokSignedIn gives the test's HOME the Grok CLI's sign-in, and a CLI to find.
func grokSignedIn(t *testing.T) {
t.Helper()