Skip to content

Commit 5c138aa

Browse files
committed
fix: handle normalized codex fallback
1 parent e6f458c commit 5c138aa

7 files changed

Lines changed: 365 additions & 47 deletions

File tree

config/README.md

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,17 +75,21 @@ A barebones debug template is available at [`minimal-opencode.json`](./minimal-o
7575

7676
## Unsupported-model behavior
7777

78-
Current defaults are strict entitlement handling:
79-
- `unsupportedCodexPolicy: "strict"` returns entitlement errors directly
80-
- set `unsupportedCodexPolicy: "fallback"` (or `CODEX_AUTH_UNSUPPORTED_MODEL_POLICY=fallback`) to enable automatic fallback retries
78+
Current defaults are strict entitlement handling except for default public selectors that are commonly entitlement-gated:
79+
- `gpt-5.5` and canonical `gpt-5-codex` can auto-fallback through `gpt-5.4`, `gpt-5.4-mini`, then `gpt-5.4-nano` when the backend reports the selected model is not supported for the active account/workspace
80+
- `unsupportedCodexPolicy: "strict"` returns other entitlement errors directly
81+
- set `unsupportedCodexPolicy: "fallback"` (or `CODEX_AUTH_UNSUPPORTED_MODEL_POLICY=fallback`) to enable the full fallback chain for manual/legacy selectors
8182
- `fallbackToGpt52OnUnsupportedGpt53: true` keeps the legacy `gpt-5.3-codex -> gpt-5.2-codex` edge inside fallback mode
82-
- `gpt-5.5 -> gpt-5.4` is included by default for accounts/workspaces that do not yet expose GPT-5.5
8383
- user-typed `gpt-5.5-pro*` is canonicalized to `gpt-5.5` before fallback because GPT-5.5 Pro is ChatGPT-only, not a Codex-routable model
84+
- legacy Codex selectors such as `gpt-5.2-codex`, `gpt-5.3-codex`, and `gpt-5.3-codex-spark` normalize to canonical `gpt-5-codex`; if that canonical Codex model is gated, the default auto-fallback can retry through the GPT-5.4 family
85+
- set `CODEX_AUTH_DISABLE_GPT55_AUTO_FALLBACK=1` to disable GPT-5.5 auto-fallback
86+
- set `CODEX_AUTH_DISABLE_CODEX_AUTO_FALLBACK=1` to disable canonical Codex/GPT-5.4-family auto-fallback
8487
- `gpt-5.4-pro -> gpt-5.4` remains available for older manual configs
8588
- `unsupportedCodexFallbackChain` lets you override fallback order per model
8689

87-
Default fallback chain (when policy is `fallback`):
88-
- `gpt-5.5 -> gpt-5.4`
90+
Default fallback chain (auto-fallback for `gpt-5.5`/`gpt-5-codex` through the GPT-5.4 family; full chain when policy is `fallback`):
91+
- `gpt-5.5 -> gpt-5.4 -> gpt-5.4-mini -> gpt-5.4-nano`
92+
- `gpt-5-codex -> gpt-5.4 -> gpt-5.4-mini -> gpt-5.4-nano`
8993
- `gpt-5.4-pro -> gpt-5.4` (if you manually select `gpt-5.4-pro`)
9094
- `gpt-5.3-codex -> gpt-5-codex -> gpt-5.2-codex`
9195
- `gpt-5.3-codex-spark -> gpt-5-codex -> gpt-5.3-codex -> gpt-5.2-codex` (only relevant if Spark IDs are added manually)

docs/configuration.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ The sample above intentionally sets `"retryAllAccountsMaxRetries": 3` as a bound
172172
| `unsupportedCodexPolicy` | `strict` | unsupported-model behavior: `strict` (return entitlement error) or `fallback` (retry with configured fallback chain) |
173173
| `fallbackOnUnsupportedCodexModel` | `false` | legacy fallback toggle mapped to `unsupportedCodexPolicy` (prefer using `unsupportedCodexPolicy`) |
174174
| `fallbackToGpt52OnUnsupportedGpt53` | `true` | legacy compatibility toggle for the `gpt-5.3-codex -> gpt-5.2-codex` edge when generic fallback is enabled |
175-
| `unsupportedCodexFallbackChain` | `{}` | optional per-model fallback-chain override (map of `model -> [fallback1, fallback2, ...]`; default includes `gpt-5.5 -> gpt-5.4`). `gpt-5.5` auto-fallback is on by default during the rollout; set `CODEX_AUTH_DISABLE_GPT55_AUTO_FALLBACK=1` to opt out. GPT-5.5 Pro is not mapped: it is ChatGPT-only per OpenAI's 2026-04-23 launch. |
175+
| `unsupportedCodexFallbackChain` | `{}` | optional per-model fallback-chain override (map of `model -> [fallback1, fallback2, ...]`; default includes `gpt-5.5` and `gpt-5-codex` through the GPT-5.4 family). `gpt-5.5` and canonical Codex auto-fallbacks are on by default for common entitlement gates; set `CODEX_AUTH_DISABLE_GPT55_AUTO_FALLBACK=1` or `CODEX_AUTH_DISABLE_CODEX_AUTO_FALLBACK=1` to opt out. GPT-5.5 Pro is not mapped: it is ChatGPT-only per OpenAI's 2026-04-23 launch. |
176176
| `sessionRecovery` | `true` | auto-recover from common api errors |
177177
| `autoResume` | `true` | auto-resume after thinking block recovery |
178178
| `tokenRefreshSkewMs` | `60000` | refresh tokens this many ms before expiry |
@@ -191,12 +191,13 @@ this mode is intended for beginners who prefer quick failures + clearer recovery
191191

192192
### Unsupported-Model Behavior and Fallback Chain
193193

194-
by default the plugin is strict (`unsupportedCodexPolicy: "strict"`). it returns entitlement errors directly for unsupported models.
194+
by default the plugin is strict (`unsupportedCodexPolicy: "strict"`) except for common default-selector entitlement gates. it returns other entitlement errors directly for unsupported models.
195195

196196
set `unsupportedCodexPolicy: "fallback"` to enable model fallback after account/workspace attempts are exhausted.
197197

198198
defaults when fallback policy is enabled and `unsupportedCodexFallbackChain` is empty:
199-
- `gpt-5.5 -> gpt-5.4`
199+
- `gpt-5.5 -> gpt-5.4 -> gpt-5.4-mini -> gpt-5.4-nano`
200+
- `gpt-5-codex -> gpt-5.4 -> gpt-5.4-mini -> gpt-5.4-nano`
200201
- `gpt-5.4-pro -> gpt-5.4` (if `gpt-5.4-pro` is selected manually)
201202
- `gpt-5.3-codex -> gpt-5-codex -> gpt-5.2-codex`
202203
- `gpt-5.3-codex-spark -> gpt-5-codex -> gpt-5.3-codex -> gpt-5.2-codex` (applies if you manually select Spark model IDs)
@@ -211,9 +212,10 @@ custom chain example:
211212
"unsupportedCodexPolicy": "fallback",
212213
"fallbackOnUnsupportedCodexModel": true,
213214
"unsupportedCodexFallbackChain": {
214-
"gpt-5.5": ["gpt-5.4"],
215+
"gpt-5.5": ["gpt-5.4", "gpt-5.4-mini", "gpt-5.4-nano"],
216+
"gpt-5.4": ["gpt-5.4-mini", "gpt-5.4-nano"],
215217
"gpt-5.4-pro": ["gpt-5.4"],
216-
"gpt-5-codex": ["gpt-5.2-codex"],
218+
"gpt-5-codex": ["gpt-5.4", "gpt-5.4-mini", "gpt-5.4-nano"],
217219
"gpt-5.3-codex": ["gpt-5-codex", "gpt-5.2-codex"],
218220
"gpt-5.3-codex-spark": ["gpt-5-codex", "gpt-5.3-codex", "gpt-5.2-codex"]
219221
}
@@ -255,6 +257,7 @@ override any config with env vars:
255257
| `CODEX_AUTH_FALLBACK_UNSUPPORTED_MODEL=1` | legacy fallback toggle (prefer policy variable above) |
256258
| `CODEX_AUTH_FALLBACK_GPT53_TO_GPT52=0` | disable only the legacy `gpt-5.3-codex -> gpt-5.2-codex` edge |
257259
| `CODEX_AUTH_DISABLE_GPT55_AUTO_FALLBACK=1` | disable automatic `gpt-5.5 -> gpt-5.4` fallback during rollout |
260+
| `CODEX_AUTH_DISABLE_CODEX_AUTO_FALLBACK=1` | disable automatic canonical Codex/GPT-5.4-family fallback |
258261
| `CODEX_AUTH_ACCOUNT_ID=acc_xxx` | force specific workspace id |
259262
| `CODEX_AUTH_FETCH_TIMEOUT_MS=120000` | override fetch timeout |
260263
| `CODEX_AUTH_STREAM_STALL_TIMEOUT_MS=60000` | override SSE stall timeout |

docs/troubleshooting.md

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -362,44 +362,53 @@ resolvedConfig: { reasoningEffort: 'low', ... } ← Should show your options
362362
opencode auth login
363363
```
364364
2. Add another entitled account/workspace. The plugin tries remaining accounts/workspaces before model fallback.
365-
3. Enable fallback policy only if you want automatic model downgrades:
365+
3. Default public selectors that are commonly entitlement-gated (`gpt-5.5` and canonical `gpt-5-codex`) can auto-fallback through `gpt-5.4`, `gpt-5.4-mini`, then `gpt-5.4-nano`.
366+
4. Enable fallback policy if you also want automatic downgrades for manual/legacy selectors:
366367
```bash
367368
CODEX_AUTH_UNSUPPORTED_MODEL_POLICY=fallback opencode
368369
```
369-
4. Default fallback chain (when policy is `fallback` and not overridden):
370-
- `gpt-5.5 -> gpt-5.4`
370+
5. Default fallback chain (auto-fallback for `gpt-5.5`/`gpt-5-codex` through the GPT-5.4 family; full chain when policy is `fallback` and not overridden):
371+
- `gpt-5.5 -> gpt-5.4 -> gpt-5.4-mini -> gpt-5.4-nano`
372+
- `gpt-5-codex -> gpt-5.4 -> gpt-5.4-mini -> gpt-5.4-nano`
371373
- `gpt-5.4-pro -> gpt-5.4` (if `gpt-5.4-pro` is selected manually)
372374
- `gpt-5.3-codex -> gpt-5-codex -> gpt-5.2-codex`
373375
- `gpt-5.3-codex-spark -> gpt-5-codex -> gpt-5.3-codex -> gpt-5.2-codex` (if Spark IDs are selected manually)
374376
- `gpt-5.2-codex -> gpt-5-codex`
375377
- `gpt-5.1-codex -> gpt-5-codex`
376-
5. Configure a custom fallback chain in `~/.opencode/openai-codex-auth-config.json`:
378+
6. Configure a custom fallback chain in `~/.opencode/openai-codex-auth-config.json`:
377379
```json
378380
{
379381
"unsupportedCodexPolicy": "fallback",
380382
"fallbackOnUnsupportedCodexModel": true,
381383
"unsupportedCodexFallbackChain": {
382-
"gpt-5.5": ["gpt-5.4"],
384+
"gpt-5.5": ["gpt-5.4", "gpt-5.4-mini", "gpt-5.4-nano"],
385+
"gpt-5.4": ["gpt-5.4-mini", "gpt-5.4-nano"],
383386
"gpt-5.4-pro": ["gpt-5.4"],
384-
"gpt-5-codex": ["gpt-5.2-codex"],
387+
"gpt-5-codex": ["gpt-5.4", "gpt-5.4-mini", "gpt-5.4-nano"],
385388
"gpt-5.3-codex": ["gpt-5-codex", "gpt-5.2-codex"],
386389
"gpt-5.3-codex-spark": ["gpt-5-codex", "gpt-5.3-codex", "gpt-5.2-codex"]
387390
}
388391
}
389392
```
390-
6. Use strict mode (no model fallback) for explicit entitlement failures:
393+
7. Use strict mode for explicit entitlement failures outside the default public selector auto-fallbacks:
391394
```bash
392395
CODEX_AUTH_UNSUPPORTED_MODEL_POLICY=strict opencode
393396
```
394-
7. Legacy compatibility toggle (only controls `gpt-5.3-codex -> gpt-5.2-codex`):
397+
8. Disable default-selector auto-fallbacks when you need strict entitlement failures for those selectors:
398+
```bash
399+
CODEX_AUTH_DISABLE_GPT55_AUTO_FALLBACK=1 opencode
400+
CODEX_AUTH_DISABLE_CODEX_AUTO_FALLBACK=1 opencode
401+
```
402+
`CODEX_AUTH_DISABLE_CODEX_AUTO_FALLBACK=1` only disables the automatic canonical Codex/GPT-5.4-family fallback path; explicit `unsupportedCodexPolicy: "fallback"` chains still apply.
403+
9. Legacy compatibility toggle (only controls `gpt-5.3-codex -> gpt-5.2-codex`):
395404
```bash
396405
CODEX_AUTH_FALLBACK_GPT53_TO_GPT52=0 opencode
397406
```
398-
8. Legacy generic fallback toggle compatibility:
407+
10. Legacy generic fallback toggle compatibility:
399408
```bash
400409
CODEX_AUTH_FALLBACK_UNSUPPORTED_MODEL=1 opencode
401410
```
402-
9. Verify effective upstream model when debugging Spark/fallback behavior:
411+
11. Verify effective upstream model when debugging Spark/fallback behavior:
403412
```bash
404413
ENABLE_PLUGIN_REQUEST_LOGGING=1 CODEX_PLUGIN_LOG_BODIES=1 opencode run "ping" --model=openai/gpt-5.3-codex-spark
405414
```

index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2631,7 +2631,7 @@ while (attempted.size < Math.max(1, accountCount)) {
26312631
: waitMs > 0
26322632
? `All ${count} account(s) are rate-limited. Try again in ${waitLabel} or add another account with \`opencode auth login\`.`
26332633
: wasEntitlementExhaustion
2634-
? `All ${count} account(s) returned 'model not supported' for the requested model.${entitlementDetail} If this is a GPT-5.5 request during the rollout period, set \`unsupportedCodexPolicy: "fallback"\` (or \`CODEX_AUTH_UNSUPPORTED_MODEL_POLICY=fallback\`) to auto-fallback to gpt-5.4. See \`codex-health\` for per-account details.`
2634+
? `All ${count} account(s) returned 'model not supported' for the requested model.${entitlementDetail} Codex model access is account/workspace gated; default gpt-5.5/gpt-5-codex selectors auto-fallback through the GPT-5.4 family when possible. Set \`unsupportedCodexPolicy: "fallback"\` for the full manual fallback chain, or see \`codex-health\` for per-account details.`
26352635
: `All ${count} account(s) failed (server errors or auth issues). Check account health with \`codex-health\`.`;
26362636
runtimeMetrics.failedRequests++;
26372637
runtimeMetrics.lastError = message;

lib/request/fetch-helpers.ts

Lines changed: 58 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,49 @@ const CHATGPT_CODEX_UNSUPPORTED_MODEL_PATTERN =
5454
const NORMALIZED_UNSUPPORTED_MODEL_PATTERN =
5555
/the model ['"]([^'"]+)['"] is not currently available for this chatgpt account/i;
5656
export const DEFAULT_UNSUPPORTED_CODEX_FALLBACK_CHAIN: Record<string, string[]> = {
57-
[GPT_55_MODEL_ID]: ["gpt-5.4"],
57+
[GPT_55_MODEL_ID]: ["gpt-5.4", "gpt-5.4-mini", "gpt-5.4-nano"],
58+
"gpt-5.4": ["gpt-5.4-mini", "gpt-5.4-nano"],
59+
"gpt-5.4-mini": ["gpt-5.4-nano"],
5860
"gpt-5.4-pro": ["gpt-5.4"],
61+
"gpt-5-codex": ["gpt-5.4", "gpt-5.4-mini", "gpt-5.4-nano"],
62+
// Legacy selectors normalize to `gpt-5-codex` before lookup during the
63+
// request path. Keep these historical entries for direct helper callers and
64+
// custom-chain documentation; the canonical `gpt-5-codex` edge above is the
65+
// runtime path for default OpenCode selectors.
5966
"gpt-5.3-codex-spark": ["gpt-5-codex", "gpt-5.3-codex", "gpt-5.2-codex"],
6067
"gpt-5.3-codex": ["gpt-5-codex", "gpt-5.2-codex"],
6168
"gpt-5.2-codex": ["gpt-5-codex"],
6269
"gpt-5.1-codex": ["gpt-5-codex"],
6370
};
6471

72+
const DEFAULT_AUTO_FALLBACK_ENTRY_OPT_OUT_ENV: Record<string, string> = {
73+
[GPT_55_MODEL_ID]: "CODEX_AUTH_DISABLE_GPT55_AUTO_FALLBACK",
74+
"gpt-5-codex": "CODEX_AUTH_DISABLE_CODEX_AUTO_FALLBACK",
75+
};
76+
77+
const DEFAULT_AUTO_FALLBACK_CONTINUATION_MODELS = new Set([
78+
"gpt-5.4",
79+
"gpt-5.4-mini",
80+
]);
81+
82+
function resolveAutoFallbackEntryModel(
83+
currentModel: string,
84+
attempted: Set<string>,
85+
): string | undefined {
86+
if (DEFAULT_AUTO_FALLBACK_ENTRY_OPT_OUT_ENV[currentModel] !== undefined) {
87+
return currentModel;
88+
}
89+
if (!DEFAULT_AUTO_FALLBACK_CONTINUATION_MODELS.has(currentModel)) {
90+
return undefined;
91+
}
92+
for (const model of attempted) {
93+
if (DEFAULT_AUTO_FALLBACK_ENTRY_OPT_OUT_ENV[model] !== undefined) {
94+
return model;
95+
}
96+
}
97+
return undefined;
98+
}
99+
65100
export interface UnsupportedCodexModelInfo {
66101
isUnsupported: boolean;
67102
code?: string;
@@ -210,27 +245,34 @@ export function resolveUnsupportedCodexFallbackModel(
210245
const currentModel = requestedModel ?? unsupported.unsupportedModel;
211246
if (!currentModel) return undefined;
212247

213-
// During the GPT-5.5 rollout period, the backend gates `gpt-5.5` per-account
214-
// even though the plugin maps every GPT-5.5 alias to it. Without a scoped
215-
// auto-fallback, every one of the pooled accounts returns
216-
// `model_not_supported_with_chatgpt_account` and rotation exhausts the pool
217-
// with a misleading "server errors or auth issues" message. Gate this to
218-
// gpt-5.5 only so legacy families keep their opt-in behavior.
219-
const isGpt55AutoFallbackOptOut =
220-
process.env.CODEX_AUTH_DISABLE_GPT55_AUTO_FALLBACK === "1";
221-
const shouldAutoFallbackForGpt55 =
222-
!isGpt55AutoFallbackOptOut && currentModel === GPT_55_MODEL_ID;
223-
224-
if (!options.fallbackOnUnsupportedCodexModel && !shouldAutoFallbackForGpt55) {
225-
return undefined;
226-
}
227-
228248
const attempted = new Set<string>();
229249
for (const model of options.attemptedModels ?? []) {
230250
const normalized = canonicalizeModelName(model);
231251
if (normalized) attempted.add(normalized);
232252
}
233253

254+
// The backend gates some public selector ids per account/workspace. When the
255+
// selected id is a default UI selector, auto-fallback prevents exhausting every
256+
// pooled account with the same unsupported-model response. Continuation models
257+
// such as `gpt-5.4` only auto-fallback when the attempted set proves the chain
258+
// started from a default entry point; direct user selection remains strict.
259+
const autoFallbackEntryModel = resolveAutoFallbackEntryModel(
260+
currentModel,
261+
attempted,
262+
);
263+
const autoFallbackOptOutEnv = autoFallbackEntryModel
264+
? DEFAULT_AUTO_FALLBACK_ENTRY_OPT_OUT_ENV[autoFallbackEntryModel]
265+
: undefined;
266+
const shouldAutoFallbackForDefaultSelector =
267+
!!autoFallbackOptOutEnv && process.env[autoFallbackOptOutEnv] !== "1";
268+
269+
if (
270+
!options.fallbackOnUnsupportedCodexModel &&
271+
!shouldAutoFallbackForDefaultSelector
272+
) {
273+
return undefined;
274+
}
275+
234276
const chain = normalizeFallbackChain(options.customChain);
235277
const targets = chain[currentModel] ?? [];
236278
if (targets.length === 0) return undefined;

0 commit comments

Comments
 (0)