diff --git a/src/cliproxy/codex-plan-compatibility.ts b/src/cliproxy/codex-plan-compatibility.ts index 5276f970..73cd82c8 100644 --- a/src/cliproxy/codex-plan-compatibility.ts +++ b/src/cliproxy/codex-plan-compatibility.ts @@ -18,6 +18,7 @@ const KNOWN_CODEX_MODELS = new Set( ); const FREE_PLAN_FALLBACKS = new Map([ + ['gpt-5.5', FREE_SAFE_DEFAULT_MODEL], ['gpt-5.3-codex', FREE_SAFE_DEFAULT_MODEL], ['gpt-5.3-codex-spark', FREE_SAFE_FAST_MODEL], ]); diff --git a/src/cliproxy/model-catalog.ts b/src/cliproxy/model-catalog.ts index ee1032f5..1f37537b 100644 --- a/src/cliproxy/model-catalog.ts +++ b/src/cliproxy/model-catalog.ts @@ -188,6 +188,19 @@ export const MODEL_CATALOG: Partial> = displayName: 'Copilot Codex', defaultModel: 'gpt-5.4', models: [ + { + id: 'gpt-5.5', + name: 'GPT-5.5', + tier: 'pro', + description: + 'Newest Codex-released GPT-5 family model; falls back to GPT-5.4 on free plans', + thinking: { + type: 'levels', + levels: ['low', 'medium', 'high', 'xhigh'], + maxLevel: 'xhigh', + dynamicAllowed: false, + }, + }, { id: 'gpt-5.4', name: 'GPT-5.4', diff --git a/tests/unit/cliproxy/codex-plan-compatibility-reconcile.test.ts b/tests/unit/cliproxy/codex-plan-compatibility-reconcile.test.ts index 1f004389..97ae8940 100644 --- a/tests/unit/cliproxy/codex-plan-compatibility-reconcile.test.ts +++ b/tests/unit/cliproxy/codex-plan-compatibility-reconcile.test.ts @@ -45,6 +45,48 @@ async function importCompatibilityModule(cacheTag: string) { const identity = (message: string) => message; describe('codex plan compatibility reconcile', () => { + it('warns about the GPT-5.5 free-plan fallback without rewriting saved settings', async () => { + const { tmpDir, settingsPath } = createCodexSettingsFixture(); + const errorSpy = spyOn(console, 'error').mockImplementation(() => {}); + + try { + const { reconcileCodexModelForActivePlan } = await importCompatibilityModule('gpt-5-5-free'); + + await reconcileCodexModelForActivePlan( + { + currentModel: 'gpt-5.5', + verbose: false, + }, + { + getDefaultAccount: () => ({ id: 'free@example.com' }) as never, + fetchCodexQuota: async () => ({ + success: true, + windows: [], + coreUsage: { fiveHour: null, weekly: null }, + planType: 'free', + lastUpdated: Date.now(), + accountId: 'free@example.com', + }), + formatInfo: identity, + formatWarn: identity, + } + ); + + const repaired = JSON.parse(fs.readFileSync(settingsPath, 'utf-8')) as { + env: Record; + }; + expect(repaired.env.ANTHROPIC_MODEL).toBe('gpt-5.3-codex'); + expect(repaired.env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('gpt-5.3-codex'); + expect(repaired.env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('gpt-5.3-codex'); + expect(repaired.env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('gpt-5.4-mini'); + expect(errorSpy).toHaveBeenCalledWith( + 'Codex free plan detected. Keeping saved model "gpt-5.5" in settings; runtime requests will fall back to "gpt-5.4" when needed.' + ); + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); + it('keeps saved Codex settings intact and warns about runtime fallback for free-plan accounts', async () => { const { tmpDir, settingsPath } = createCodexSettingsFixture('gpt-5.3-codex-spark'); const errorSpy = spyOn(console, 'error').mockImplementation(() => {}); diff --git a/tests/unit/cliproxy/codex-plan-compatibility.test.ts b/tests/unit/cliproxy/codex-plan-compatibility.test.ts index 2fc1266a..41f52c68 100644 --- a/tests/unit/cliproxy/codex-plan-compatibility.test.ts +++ b/tests/unit/cliproxy/codex-plan-compatibility.test.ts @@ -14,6 +14,8 @@ describe('codex plan compatibility', () => { }); it('maps paid-only free-plan models to safe fallbacks', () => { + expect(getFreePlanFallbackCodexModel('gpt-5.5')).toBe('gpt-5.4'); + expect(getFreePlanFallbackCodexModel('gpt-5.5-xhigh')).toBe('gpt-5.4'); expect(getFreePlanFallbackCodexModel('gpt-5.3-codex')).toBe('gpt-5.4'); expect(getFreePlanFallbackCodexModel('gpt-5.3-codex-xhigh')).toBe('gpt-5.4'); expect(getFreePlanFallbackCodexModel('gpt-5.3-codex(high)')).toBe('gpt-5.4'); @@ -71,6 +73,7 @@ describe('codex plan compatibility', () => { }); it('tracks Codex thinking caps for current safe defaults, paid models, and legacy aliases', () => { + expect(getModelMaxLevel('codex', 'gpt-5.5')).toBe('xhigh'); expect(getModelMaxLevel('codex', 'gpt-5.4')).toBe('xhigh'); expect(getModelMaxLevel('codex', 'gpt-5.4-mini')).toBe('high'); expect(getModelMaxLevel('codex', 'gpt-5-codex')).toBe('xhigh'); diff --git a/tests/unit/cliproxy/model-catalog.test.js b/tests/unit/cliproxy/model-catalog.test.js index 704c5764..bc11ce90 100644 --- a/tests/unit/cliproxy/model-catalog.test.js +++ b/tests/unit/cliproxy/model-catalog.test.js @@ -209,6 +209,7 @@ describe('Model Catalog', () => { const { MODEL_CATALOG } = modelCatalog; const ids = MODEL_CATALOG.codex.models.map((m) => m.id); assert.deepStrictEqual(ids, [ + 'gpt-5.5', 'gpt-5.4', 'gpt-5.4-mini', 'gpt-5.3-codex', diff --git a/ui/src/components/compatible-cli/codex-top-level-controls-card.tsx b/ui/src/components/compatible-cli/codex-top-level-controls-card.tsx index 54a88bdb..87b6c3a0 100644 --- a/ui/src/components/compatible-cli/codex-top-level-controls-card.tsx +++ b/ui/src/components/compatible-cli/codex-top-level-controls-card.tsx @@ -130,7 +130,7 @@ function TopLevelControlsForm({ onChange={(event) => setDraft((current) => ({ ...current, model: event.target.value || null })) } - placeholder="gpt-5.4" + placeholder="gpt-5.5" disabled={disabled} /> @@ -312,7 +312,9 @@ function TopLevelControlsForm({ className="text-[10px] uppercase tracking-[0.16em] text-muted-foreground" > {/* TODO i18n: missing keys codex.gpt54Selected / codex.gpt54Reference */} - {isGpt54Selected ? 'GPT-5.4 selected' : 'GPT-5.4 reference'} + {isGpt54Selected + ? 'GPT-5.4 long-context selected' + : 'GPT-5.4 long-context reference'}

@@ -500,16 +502,16 @@ function TopLevelControlsForm({ {/* TODO i18n: missing key codex.docs */} Docs - {/* TODO i18n: missing key codex.gpt54ModelPage */} - GPT-5.4 model page + {/* TODO i18n: missing key codex.gptModelGuide */} + GPT-5 model guide = { displayName: 'Codex', defaultModel: 'gpt-5-codex', models: [ + { + id: 'gpt-5.5', + name: 'GPT-5.5', + tier: 'paid', + description: 'Newest Codex-only GPT-5 family model', + codexMaxEffort: 'xhigh', + presetMapping: { + default: 'gpt-5.5', + opus: 'gpt-5.5', + sonnet: 'gpt-5.5', + haiku: 'gpt-5-codex-mini', + }, + }, { id: 'gpt-5-codex', name: 'GPT-5 Codex', diff --git a/ui/tests/unit/components/compatible-cli/codex-top-level-controls-card.test.tsx b/ui/tests/unit/components/compatible-cli/codex-top-level-controls-card.test.tsx index 803d48e3..fab41a28 100644 --- a/ui/tests/unit/components/compatible-cli/codex-top-level-controls-card.test.tsx +++ b/ui/tests/unit/components/compatible-cli/codex-top-level-controls-card.test.tsx @@ -28,7 +28,7 @@ describe('CodexTopLevelControlsCard', () => { const saveButton = screen.getByRole('button', { name: 'Save top-level settings' }); expect(saveButton).toBeDisabled(); - await userEvent.type(screen.getByPlaceholderText('gpt-5.4'), 'gpt-5.4-mini'); + await userEvent.type(screen.getByPlaceholderText('gpt-5.5'), 'gpt-5.4-mini'); expect(saveButton).toBeEnabled(); await userEvent.click(saveButton); diff --git a/ui/tests/unit/ui/lib/model-catalogs-codex.test.ts b/ui/tests/unit/ui/lib/model-catalogs-codex.test.ts index b8ceaf9a..4e3f5f94 100644 --- a/ui/tests/unit/ui/lib/model-catalogs-codex.test.ts +++ b/ui/tests/unit/ui/lib/model-catalogs-codex.test.ts @@ -4,10 +4,13 @@ import { MODEL_CATALOGS } from '@/lib/model-catalogs'; describe('codex model catalog defaults', () => { it('uses gpt-5-codex-mini as the haiku mapping for cross-plan codex presets', () => { const codexCatalog = MODEL_CATALOGS.codex; + const codex55 = codexCatalog.models.find((model) => model.id === 'gpt-5.5'); const codex53 = codexCatalog.models.find((model) => model.id === 'gpt-5.3-codex'); const codex52 = codexCatalog.models.find((model) => model.id === 'gpt-5.2-codex'); const codexMini = codexCatalog.models.find((model) => model.id === 'gpt-5-codex-mini'); + expect(codex55?.presetMapping?.haiku).toBe('gpt-5-codex-mini'); + expect(codex55?.codexMaxEffort).toBe('xhigh'); expect(codex53?.presetMapping?.haiku).toBe('gpt-5-codex-mini'); expect(codex52?.presetMapping?.haiku).toBe('gpt-5-codex-mini'); expect(codex53?.codexMaxEffort).toBe('xhigh');