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:
1 parent
243c5a0429
commit
18e6cb8140
2 files changed
+44
-3
No files matched your search
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in new issue
Block a user