Use Codex CLI request shape for all ChatGPT subscription models
The ChatGPT Codex backend load-sheds requests from unrecognized client identities with "Our servers are currently overloaded" stream errors even when capacity is fine. Extend the existing Codex CLI routing metadata (originator, user-agent, session-id, prompt_cache_key) from preview models to every model on the subscription backend. Public Responses API paths are unchanged.
- commit
- b58450d9a0577d71baa0007af889d6d6a5f6a8a8
- author
- patriceckhart · · 2026-07-30 10:05 UTC
- parent
- ad8d624
- signature
- unsigned
- refs
- tag: v0.3.28
2 files · +29 -11
M packages/provider/openai_codex.go +14 -5
| @@ -45,6 +45,7 @@ type codexClient struct { | |||
| 45 | 45 | providerName string | |
| 46 | 46 | modelName func(string) string | |
| 47 | 47 | disableCLIRouting bool | |
| 48 | + | cliRoutingAll bool | |
| 48 | 49 | http *http.Client | |
| 49 | 50 | } | |
| 50 | 51 | ||
| @@ -61,7 +62,14 @@ func NewOpenAICodex(token, accountID, baseURL string) Client { | |||
| 61 | 62 | baseURL: strings.TrimRight(baseURL, "/"), | |
| 62 | 63 | errorLabel: "codex", | |
| 63 | 64 | providerName: "openai-codex", | |
| 64 | - | http: &http.Client{Timeout: 0}, | |
| 65 | + | // The ChatGPT Codex backend load-sheds requests from client | |
| 66 | + | // identities it does not recognize: unknown originator/user-agent | |
| 67 | + | // pairs receive "Our servers are currently overloaded" stream | |
| 68 | + | // errors near-deterministically while Codex CLI requests succeed. | |
| 69 | + | // Send the Codex CLI request shape for every model on this | |
| 70 | + | // backend. | |
| 71 | + | cliRoutingAll: true, | |
| 72 | + | http: &http.Client{Timeout: 0}, | |
| 65 | 73 | } | |
| 66 | 74 | } | |
| 67 | 75 | ||
| @@ -363,7 +371,7 @@ func (c *codexClient) Stream(ctx context.Context, req Request) (<-chan Event, er | |||
| 363 | 371 | wire.Model = c.modelName(wire.Model) | |
| 364 | 372 | } | |
| 365 | 373 | var codexCLISessionID string | |
| 366 | - | if !c.disableCLIRouting && usesCodexCLIRouting(wire.Model) { | |
| 374 | + | if !c.disableCLIRouting && (c.cliRoutingAll || usesCodexCLIRouting(wire.Model)) { | |
| 367 | 375 | codexCLISessionID = newCodexSessionID() | |
| 368 | 376 | wire.PromptCacheKey = codexCLISessionID | |
| 369 | 377 | } | |
| @@ -383,9 +391,10 @@ func (c *codexClient) Stream(ctx context.Context, req Request) (<-chan Event, er | |||
| 383 | 391 | httpReq.Header.Set("chatgpt-account-id", c.accountID) | |
| 384 | 392 | httpReq.Header.Set("openai-beta", "responses=experimental") | |
| 385 | 393 | if codexCLISessionID != "" { | |
| 386 | - | // Some preview models are only admitted or reliably served by the | |
| 387 | - | // ChatGPT Codex backend when the request follows Codex CLI routing | |
| 388 | - | // metadata. Keep this narrow so Sol retains zot's proven shape. | |
| 394 | + | // The ChatGPT Codex backend only admits (and reliably serves) | |
| 395 | + | // requests that follow Codex CLI routing metadata; other client | |
| 396 | + | // identities are load-shed with "servers are currently | |
| 397 | + | // overloaded" errors even when capacity is fine. | |
| 389 | 398 | httpReq.Header.Set("originator", "codex_cli_rs") | |
| 390 | 399 | httpReq.Header.Set("session-id", codexCLISessionID) | |
| 391 | 400 | httpReq.Header.Set("user-agent", "codex_cli_rs/0.0.0") | |
| @@ -45,6 +45,7 @@ type codexClient struct { | |||||
| 45 | providerName string | 45 | providerName string | ||
| 46 | modelName func(string) string | 46 | modelName func(string) string | ||
| 47 | disableCLIRouting bool | 47 | disableCLIRouting bool | ||
| 48 | + | cliRoutingAll bool | |||
| 48 | http *http.Client | 49 | http *http.Client | ||
| 49 | } | 50 | } | ||
| 50 | 51 | ||||
| @@ -61,7 +62,14 @@ func NewOpenAICodex(token, accountID, baseURL string) Client { | |||||
| 61 | baseURL: strings.TrimRight(baseURL, "/"), | 62 | baseURL: strings.TrimRight(baseURL, "/"), | ||
| 62 | errorLabel: "codex", | 63 | errorLabel: "codex", | ||
| 63 | providerName: "openai-codex", | 64 | providerName: "openai-codex", | ||
| 64 | - | http: &http.Client{Timeout: 0}, | 65 | + | // The ChatGPT Codex backend load-sheds requests from client |
| 66 | + | // identities it does not recognize: unknown originator/user-agent | |||
| 67 | + | // pairs receive "Our servers are currently overloaded" stream | |||
| 68 | + | // errors near-deterministically while Codex CLI requests succeed. | |||
| 69 | + | // Send the Codex CLI request shape for every model on this | |||
| 70 | + | // backend. | |||
| 71 | + | cliRoutingAll: true, | |||
| 72 | + | http: &http.Client{Timeout: 0}, | |||
| 65 | } | 73 | } | ||
| 66 | } | 74 | } | ||
| 67 | 75 | ||||
| @@ -363,7 +371,7 @@ func (c *codexClient) Stream(ctx context.Context, req Request) (<-chan Event, er | |||||
| 363 | wire.Model = c.modelName(wire.Model) | 371 | wire.Model = c.modelName(wire.Model) | ||
| 364 | } | 372 | } | ||
| 365 | var codexCLISessionID string | 373 | var codexCLISessionID string | ||
| 366 | - | if !c.disableCLIRouting && usesCodexCLIRouting(wire.Model) { | 374 | + | if !c.disableCLIRouting && (c.cliRoutingAll || usesCodexCLIRouting(wire.Model)) { |
| 367 | codexCLISessionID = newCodexSessionID() | 375 | codexCLISessionID = newCodexSessionID() | ||
| 368 | wire.PromptCacheKey = codexCLISessionID | 376 | wire.PromptCacheKey = codexCLISessionID | ||
| 369 | } | 377 | } | ||
| @@ -383,9 +391,10 @@ func (c *codexClient) Stream(ctx context.Context, req Request) (<-chan Event, er | |||||
| 383 | httpReq.Header.Set("chatgpt-account-id", c.accountID) | 391 | httpReq.Header.Set("chatgpt-account-id", c.accountID) | ||
| 384 | httpReq.Header.Set("openai-beta", "responses=experimental") | 392 | httpReq.Header.Set("openai-beta", "responses=experimental") | ||
| 385 | if codexCLISessionID != "" { | 393 | if codexCLISessionID != "" { | ||
| 386 | - | // Some preview models are only admitted or reliably served by the | 394 | + | // The ChatGPT Codex backend only admits (and reliably serves) |
| 387 | - | // ChatGPT Codex backend when the request follows Codex CLI routing | 395 | + | // requests that follow Codex CLI routing metadata; other client |
| 388 | - | // metadata. Keep this narrow so Sol retains zot's proven shape. | 396 | + | // identities are load-shed with "servers are currently |
| 397 | + | // overloaded" errors even when capacity is fine. | |||
| 389 | httpReq.Header.Set("originator", "codex_cli_rs") | 398 | httpReq.Header.Set("originator", "codex_cli_rs") | ||
| 390 | httpReq.Header.Set("session-id", codexCLISessionID) | 399 | httpReq.Header.Set("session-id", codexCLISessionID) | ||
| 391 | httpReq.Header.Set("user-agent", "codex_cli_rs/0.0.0") | 400 | httpReq.Header.Set("user-agent", "codex_cli_rs/0.0.0") | ||
M packages/provider/openai_codex_test.go +15 -6
| @@ -196,7 +196,13 @@ func TestCodexNestedStreamError(t *testing.T) { | |||
| 196 | 196 | } | |
| 197 | 197 | } | |
| 198 | 198 | ||
| 199 | - | func TestCodexSolKeepsZotShape(t *testing.T) { | |
| 199 | + | // TestCodexSubscriptionAlwaysUsesCodexCLIShape pins the ChatGPT | |
| 200 | + | // subscription backend to the Codex CLI request identity for every | |
| 201 | + | // model. The backend load-sheds unrecognized originator/user-agent | |
| 202 | + | // pairs with "Our servers are currently overloaded" stream errors | |
| 203 | + | // even when capacity is fine, so the CLI shape is required for | |
| 204 | + | // reliable service, not just for preview-model admission. | |
| 205 | + | func TestCodexSubscriptionAlwaysUsesCodexCLIShape(t *testing.T) { | |
| 200 | 206 | c := NewOpenAICodex("token", "acct", "https://example.test/backend-api/codex/responses").(*codexClient) | |
| 201 | 207 | var gotReq *http.Request | |
| 202 | 208 | var body bytes.Buffer | |
| @@ -223,13 +229,16 @@ func TestCodexSolKeepsZotShape(t *testing.T) { | |||
| 223 | 229 | if gotReq == nil { | |
| 224 | 230 | t.Fatal("request was not sent") | |
| 225 | 231 | } | |
| 226 | - | if gotReq.Header.Get("originator") != "zot" { | |
| 232 | + | if gotReq.Header.Get("originator") != "codex_cli_rs" { | |
| 227 | 233 | t.Fatalf("originator = %q", gotReq.Header.Get("originator")) | |
| 228 | 234 | } | |
| 229 | - | if gotReq.Header.Get("session-id") != "" { | |
| 230 | - | t.Fatalf("session-id = %q", gotReq.Header.Get("session-id")) | |
| 235 | + | if gotReq.Header.Get("user-agent") != "codex_cli_rs/0.0.0" { | |
| 236 | + | t.Fatalf("user-agent = %q", gotReq.Header.Get("user-agent")) | |
| 237 | + | } | |
| 238 | + | if gotReq.Header.Get("session-id") == "" { | |
| 239 | + | t.Fatal("session-id header missing") | |
| 231 | 240 | } | |
| 232 | - | if strings.Contains(body.String(), "prompt_cache_key") { | |
| 233 | - | t.Fatalf("Sol request unexpectedly included prompt_cache_key: %s", body.String()) | |
| 241 | + | if !strings.Contains(body.String(), "prompt_cache_key") { | |
| 242 | + | t.Fatalf("request missing prompt_cache_key: %s", body.String()) | |
| 234 | 243 | } | |
| 235 | 244 | } | |
| @@ -196,7 +196,13 @@ func TestCodexNestedStreamError(t *testing.T) { | |||||
| 196 | } | 196 | } | ||
| 197 | } | 197 | } | ||
| 198 | 198 | ||||
| 199 | - | func TestCodexSolKeepsZotShape(t *testing.T) { | 199 | + | // TestCodexSubscriptionAlwaysUsesCodexCLIShape pins the ChatGPT |
| 200 | + | // subscription backend to the Codex CLI request identity for every | |||
| 201 | + | // model. The backend load-sheds unrecognized originator/user-agent | |||
| 202 | + | // pairs with "Our servers are currently overloaded" stream errors | |||
| 203 | + | // even when capacity is fine, so the CLI shape is required for | |||
| 204 | + | // reliable service, not just for preview-model admission. | |||
| 205 | + | func TestCodexSubscriptionAlwaysUsesCodexCLIShape(t *testing.T) { | |||
| 200 | c := NewOpenAICodex("token", "acct", "https://example.test/backend-api/codex/responses").(*codexClient) | 206 | c := NewOpenAICodex("token", "acct", "https://example.test/backend-api/codex/responses").(*codexClient) | ||
| 201 | var gotReq *http.Request | 207 | var gotReq *http.Request | ||
| 202 | var body bytes.Buffer | 208 | var body bytes.Buffer | ||
| @@ -223,13 +229,16 @@ func TestCodexSolKeepsZotShape(t *testing.T) { | |||||
| 223 | if gotReq == nil { | 229 | if gotReq == nil { | ||
| 224 | t.Fatal("request was not sent") | 230 | t.Fatal("request was not sent") | ||
| 225 | } | 231 | } | ||
| 226 | - | if gotReq.Header.Get("originator") != "zot" { | 232 | + | if gotReq.Header.Get("originator") != "codex_cli_rs" { |
| 227 | t.Fatalf("originator = %q", gotReq.Header.Get("originator")) | 233 | t.Fatalf("originator = %q", gotReq.Header.Get("originator")) | ||
| 228 | } | 234 | } | ||
| 229 | - | if gotReq.Header.Get("session-id") != "" { | 235 | + | if gotReq.Header.Get("user-agent") != "codex_cli_rs/0.0.0" { |
| 230 | - | t.Fatalf("session-id = %q", gotReq.Header.Get("session-id")) | 236 | + | t.Fatalf("user-agent = %q", gotReq.Header.Get("user-agent")) |
| 237 | + | } | |||
| 238 | + | if gotReq.Header.Get("session-id") == "" { | |||
| 239 | + | t.Fatal("session-id header missing") | |||
| 231 | } | 240 | } | ||
| 232 | - | if strings.Contains(body.String(), "prompt_cache_key") { | 241 | + | if !strings.Contains(body.String(), "prompt_cache_key") { |
| 233 | - | t.Fatalf("Sol request unexpectedly included prompt_cache_key: %s", body.String()) | 242 | + | t.Fatalf("request missing prompt_cache_key: %s", body.String()) |
| 234 | } | 243 | } | ||
| 235 | } | 244 | } | ||