diff --git a/config/base-codex.settings.json b/config/base-codex.settings.json index 5dae2d86..d3a5ecd4 100644 --- a/config/base-codex.settings.json +++ b/config/base-codex.settings.json @@ -2,9 +2,9 @@ "env": { "ANTHROPIC_BASE_URL": "http://127.0.0.1:8317/api/provider/codex", "ANTHROPIC_AUTH_TOKEN": "ccs-internal-managed", - "ANTHROPIC_MODEL": "gpt-5-codex", - "ANTHROPIC_DEFAULT_OPUS_MODEL": "gpt-5-codex", - "ANTHROPIC_DEFAULT_SONNET_MODEL": "gpt-5-codex", - "ANTHROPIC_DEFAULT_HAIKU_MODEL": "gpt-5-codex-mini" + "ANTHROPIC_MODEL": "gpt-5.4", + "ANTHROPIC_DEFAULT_OPUS_MODEL": "gpt-5.4", + "ANTHROPIC_DEFAULT_SONNET_MODEL": "gpt-5.4", + "ANTHROPIC_DEFAULT_HAIKU_MODEL": "gpt-5.4-mini" } } diff --git a/src/cliproxy/codex-plan-compatibility.ts b/src/cliproxy/codex-plan-compatibility.ts index 95506089..5276f970 100644 --- a/src/cliproxy/codex-plan-compatibility.ts +++ b/src/cliproxy/codex-plan-compatibility.ts @@ -1,5 +1,6 @@ import { getDefaultAccount } from './account-manager'; import { getProviderCatalog } from './model-catalog'; +import { normalizeModelIdForProvider } from './model-id-normalizer'; import { fetchCodexQuota } from './quota-fetcher-codex'; import { getCachedQuota, setCachedQuota } from './quota-response-cache'; import type { CodexQuotaResult } from './quota-types'; @@ -7,8 +8,8 @@ import { info, warn } from '../utils/ui'; export type CodexPlanType = CodexQuotaResult['planType']; -const FREE_SAFE_DEFAULT_MODEL = 'gpt-5-codex'; -const FREE_SAFE_FAST_MODEL = 'gpt-5-codex-mini'; +const FREE_SAFE_DEFAULT_MODEL = 'gpt-5.4'; +const FREE_SAFE_FAST_MODEL = 'gpt-5.4-mini'; const CODEX_EFFORT_SUFFIX_REGEX = /-(xhigh|high|medium)$/i; const CODEX_PAREN_SUFFIX_REGEX = /\((xhigh|high|medium)\)$/i; const EXTENDED_CONTEXT_SUFFIX_REGEX = /\[1m\]$/i; @@ -19,7 +20,6 @@ const KNOWN_CODEX_MODELS = new Set( const FREE_PLAN_FALLBACKS = new Map([ ['gpt-5.3-codex', FREE_SAFE_DEFAULT_MODEL], ['gpt-5.3-codex-spark', FREE_SAFE_FAST_MODEL], - ['gpt-5.4', FREE_SAFE_DEFAULT_MODEL], ]); export interface CodexRuntimeFallbackModelMap { @@ -52,13 +52,13 @@ function isKnownCodexModel(model: string): boolean { } export function normalizeCodexModelId(model: string): string { - return model + const stripped = model .trim() .replace(EXTENDED_CONTEXT_SUFFIX_REGEX, '') .replace(CODEX_PAREN_SUFFIX_REGEX, '') .replace(CODEX_EFFORT_SUFFIX_REGEX, '') - .trim() - .toLowerCase(); + .trim(); + return normalizeModelIdForProvider(stripped, 'codex').trim().toLowerCase(); } export function getDefaultCodexModel(): string { diff --git a/src/cliproxy/model-catalog.ts b/src/cliproxy/model-catalog.ts index 8a4e4564..f4187873 100644 --- a/src/cliproxy/model-catalog.ts +++ b/src/cliproxy/model-catalog.ts @@ -186,56 +186,12 @@ export const MODEL_CATALOG: Partial> = codex: { provider: 'codex', displayName: 'Copilot Codex', - defaultModel: 'gpt-5-codex', + defaultModel: 'gpt-5.4', models: [ { - id: 'gpt-5-codex', - name: 'GPT-5 Codex', - description: 'Cross-plan safe Codex default', - thinking: { - type: 'levels', - levels: ['low', 'medium', 'high'], - maxLevel: 'high', - dynamicAllowed: false, - }, - }, - { - id: 'gpt-5-codex-mini', - name: 'GPT-5 Codex Mini', - description: 'Faster and cheaper Codex option', - thinking: { - type: 'levels', - levels: ['low', 'medium', 'high'], - maxLevel: 'high', - dynamicAllowed: false, - }, - }, - { - id: 'gpt-5-mini', - name: 'GPT-5 Mini', - description: 'Legacy mini model ID kept for backwards compatibility', - thinking: { - type: 'levels', - levels: ['low', 'medium', 'high'], - maxLevel: 'high', - dynamicAllowed: false, - }, - }, - { - id: 'gpt-5.1-codex-mini', - name: 'GPT-5.1 Codex Mini', - description: 'Legacy fast Codex mini model', - thinking: { - type: 'levels', - levels: ['low', 'medium', 'high'], - maxLevel: 'high', - dynamicAllowed: false, - }, - }, - { - id: 'gpt-5.1-codex-max', - name: 'GPT-5.1 Codex Max', - description: 'Higher-effort Codex model with xhigh support', + id: 'gpt-5.4', + name: 'GPT-5.4', + description: 'Recommended Codex default for most coding and agentic tasks', thinking: { type: 'levels', levels: ['low', 'medium', 'high', 'xhigh'], @@ -244,13 +200,13 @@ export const MODEL_CATALOG: Partial> = }, }, { - id: 'gpt-5.2-codex', - name: 'GPT-5.2 Codex', - description: 'Cross-plan Codex model with xhigh support', + id: 'gpt-5.4-mini', + name: 'GPT-5.4 Mini', + description: 'Fast, lower-cost Codex option for lighter tasks and haiku-tier routing', thinking: { type: 'levels', - levels: ['low', 'medium', 'high', 'xhigh'], - maxLevel: 'xhigh', + levels: ['low', 'medium', 'high'], + maxLevel: 'high', dynamicAllowed: false, }, }, @@ -258,7 +214,7 @@ export const MODEL_CATALOG: Partial> = id: 'gpt-5.3-codex', name: 'GPT-5.3 Codex', tier: 'pro', - description: 'Paid Codex plans only', + description: 'Previous flagship coding model whose capabilities now power GPT-5.4', thinking: { type: 'levels', levels: ['low', 'medium', 'high', 'xhigh'], @@ -270,7 +226,8 @@ export const MODEL_CATALOG: Partial> = id: 'gpt-5.3-codex-spark', name: 'GPT-5.3 Codex Spark', tier: 'pro', - description: 'Paid Codex plans only, ultra-fast coding model', + description: + 'Research preview model for ChatGPT Pro subscribers, optimized for near-instant coding iteration', thinking: { type: 'levels', levels: ['low', 'medium', 'high', 'xhigh'], @@ -279,10 +236,9 @@ export const MODEL_CATALOG: Partial> = }, }, { - id: 'gpt-5.4', - name: 'GPT-5.4', - tier: 'pro', - description: 'Paid Codex plans only, latest GPT-5 family model', + id: 'gpt-5.2', + name: 'GPT-5.2', + description: 'Previous general-purpose Codex model', thinking: { type: 'levels', levels: ['low', 'medium', 'high', 'xhigh'], diff --git a/src/cliproxy/model-id-normalizer.ts b/src/cliproxy/model-id-normalizer.ts index d2719d2c..192a369f 100644 --- a/src/cliproxy/model-id-normalizer.ts +++ b/src/cliproxy/model-id-normalizer.ts @@ -27,6 +27,16 @@ const DENIED_ANTIGRAVITY_SONNET_45_REGEX = /claude-sonnet-4(?:[.-])5(?:-thinking)?(?=(?:$|[^a-z0-9]))/gi; const CANONICAL_ANTIGRAVITY_OPUS_46_MODEL = 'claude-opus-4-6-thinking'; const CODEX_EFFORT_SUFFIX_REGEX = /-(xhigh|high|medium)$/i; +const CODEX_LEGACY_MODEL_ALIASES: Readonly> = Object.freeze({ + 'gpt-5-codex': 'gpt-5.4', + 'gpt-5-codex-mini': 'gpt-5.4-mini', + 'gpt-5-mini': 'gpt-5.4-mini', + 'gpt-5.1-codex': 'gpt-5.2', + 'gpt-5.1-codex-mini': 'gpt-5.4-mini', + 'gpt-5.1-codex-max': 'gpt-5.4', + 'gpt-5.2-codex': 'gpt-5.2', + 'gpt-5.2-codex-mini': 'gpt-5.4-mini', +}); const IFLOW_LEGACY_MODEL_ALIASES: Readonly> = Object.freeze({ 'iflow-default': 'qwen3-coder-plus', 'kimi-k2.5': 'kimi-k2', @@ -92,6 +102,17 @@ export function stripCodexEffortSuffix(model: string): string { return model.replace(CODEX_EFFORT_SUFFIX_REGEX, ''); } +/** Normalize legacy Codex aliases to the current public Codex model IDs. */ +export function normalizeCodexLegacyModelAliases(model: string): string { + const trimmed = trimModelId(model); + const { baseModel, suffix } = splitBaseModelAndSuffix(trimmed); + const replacement = CODEX_LEGACY_MODEL_ALIASES[baseModel.trim().toLowerCase()]; + if (!replacement) { + return trimmed; + } + return `${replacement}${suffix}`; +} + /** * Normalize known legacy iFlow model aliases to current upstream model IDs. * Preserves suffixes such as (budget) and [1m]. @@ -186,6 +207,9 @@ export function normalizeModelIdForProvider(model: string, provider: ProviderLik if (isIFlowProvider(provider)) { return normalizeIFlowLegacyModelAliases(trimmedModel); } + if (isCodexProvider(provider)) { + return normalizeCodexLegacyModelAliases(trimmedModel); + } if (!isAntigravityProvider(provider)) return trimmedModel; const normalizedDottedVersion = normalizeClaudeDottedMajorMinor(trimmedModel); return normalizeDeprecatedAntigravityModelAliases(normalizedDottedVersion); diff --git a/tests/unit/cliproxy/codex-plan-compatibility-reconcile.test.ts b/tests/unit/cliproxy/codex-plan-compatibility-reconcile.test.ts index 4cdca89f..1f004389 100644 --- a/tests/unit/cliproxy/codex-plan-compatibility-reconcile.test.ts +++ b/tests/unit/cliproxy/codex-plan-compatibility-reconcile.test.ts @@ -9,7 +9,7 @@ afterEach(() => { mock.restore(); }); -function createCodexSettingsFixture(haikuModel: string = 'gpt-5-codex-mini'): { +function createCodexSettingsFixture(haikuModel: string = 'gpt-5.4-mini'): { tmpDir: string; settingsPath: string; } { @@ -80,7 +80,7 @@ describe('codex plan compatibility reconcile', () => { expect(repaired.env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('gpt-5.3-codex'); expect(repaired.env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('gpt-5.3-codex-spark'); expect(errorSpy).toHaveBeenCalledWith( - 'Codex free plan detected. Keeping saved model "gpt-5.3-codex" in settings; runtime requests will fall back to "gpt-5-codex" when needed.' + 'Codex free plan detected. Keeping saved model "gpt-5.3-codex" in settings; runtime requests will fall back to "gpt-5.4" when needed.' ); } finally { fs.rmSync(tmpDir, { recursive: true, force: true }); @@ -115,7 +115,7 @@ describe('codex plan compatibility reconcile', () => { }; expect(settings.env.ANTHROPIC_MODEL).toBe('gpt-5.3-codex'); expect(errorSpy).toHaveBeenCalledWith( - 'Configured Codex model "gpt-5.3-codex" may require a paid Codex plan. If startup fails, switch to "gpt-5-codex" with "ccs codex --config".' + 'Configured Codex model "gpt-5.3-codex" may require a paid Codex plan. If startup fails, switch to "gpt-5.4" with "ccs codex --config".' ); } finally { fs.rmSync(tmpDir, { recursive: true, force: true }); @@ -195,7 +195,7 @@ describe('codex plan compatibility reconcile', () => { }; expect(settings.env.ANTHROPIC_MODEL).toBe('gpt-5.3-codex'); expect(errorSpy).toHaveBeenCalledWith( - 'Could not verify Codex plan for model "gpt-5.3-codex". If startup fails with model_not_supported, switch to "gpt-5-codex" via "ccs codex --config".' + 'Could not verify Codex plan for model "gpt-5.3-codex". If startup fails with model_not_supported, switch to "gpt-5.4" via "ccs codex --config".' ); } finally { fs.rmSync(tmpDir, { recursive: true, force: true }); @@ -234,9 +234,9 @@ describe('codex plan compatibility reconcile', () => { env: Record; }; expect(settings.env.ANTHROPIC_MODEL).toBe('gpt-5.3-codex'); - expect(settings.env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('gpt-5-codex-mini'); + expect(settings.env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('gpt-5.4-mini'); expect(errorSpy).toHaveBeenCalledWith( - 'Could not verify Codex plan for model "gpt-5.3-codex". If startup fails with model_not_supported, switch to "gpt-5-codex" via "ccs codex --config".' + 'Could not verify Codex plan for model "gpt-5.3-codex". If startup fails with model_not_supported, switch to "gpt-5.4" via "ccs codex --config".' ); } finally { fs.rmSync(tmpDir, { recursive: true, force: true }); diff --git a/tests/unit/cliproxy/codex-plan-compatibility.test.ts b/tests/unit/cliproxy/codex-plan-compatibility.test.ts index 23bb7bc9..2fc1266a 100644 --- a/tests/unit/cliproxy/codex-plan-compatibility.test.ts +++ b/tests/unit/cliproxy/codex-plan-compatibility.test.ts @@ -9,22 +9,21 @@ import { describe('codex plan compatibility', () => { it('uses a cross-plan safe Codex default', () => { - expect(getDefaultCodexModel()).toBe('gpt-5-codex'); - expect(getProviderCatalog('codex')?.defaultModel).toBe('gpt-5-codex'); + expect(getDefaultCodexModel()).toBe('gpt-5.4'); + expect(getProviderCatalog('codex')?.defaultModel).toBe('gpt-5.4'); }); it('maps paid-only free-plan models to safe fallbacks', () => { - expect(getFreePlanFallbackCodexModel('gpt-5.3-codex')).toBe('gpt-5-codex'); - expect(getFreePlanFallbackCodexModel('gpt-5.3-codex-xhigh')).toBe('gpt-5-codex'); - expect(getFreePlanFallbackCodexModel('gpt-5.3-codex(high)')).toBe('gpt-5-codex'); - expect(getFreePlanFallbackCodexModel('gpt-5.4')).toBe('gpt-5-codex'); - expect(getFreePlanFallbackCodexModel('gpt-5.3-codex-spark')).toBe('gpt-5-codex-mini'); + 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'); + expect(getFreePlanFallbackCodexModel('gpt-5.3-codex-spark')).toBe('gpt-5.4-mini'); }); it('does not rewrite cross-plan or already-safe Codex models', () => { - expect(getFreePlanFallbackCodexModel('gpt-5-codex')).toBeNull(); - expect(getFreePlanFallbackCodexModel('gpt-5.2-codex')).toBeNull(); - expect(getFreePlanFallbackCodexModel('gpt-5.1-codex-mini')).toBeNull(); + expect(getFreePlanFallbackCodexModel('gpt-5.4')).toBeNull(); + expect(getFreePlanFallbackCodexModel('gpt-5.4-mini')).toBeNull(); + expect(getFreePlanFallbackCodexModel('gpt-5.2')).toBeNull(); }); it('detects upstream Codex model_not_supported responses', () => { @@ -54,25 +53,27 @@ describe('codex plan compatibility', () => { it('resolves runtime fallbacks without retrying the rejected model again', () => { expect( resolveRuntimeCodexFallbackModel({ - requestedModel: 'gpt-5.4', - modelMap: { defaultModel: 'gpt-5-codex' }, + requestedModel: 'gpt-5.3-codex', + modelMap: { defaultModel: 'gpt-5.4' }, }) - ).toBe('gpt-5-codex'); + ).toBe('gpt-5.4'); expect( resolveRuntimeCodexFallbackModel({ - requestedModel: 'gpt-5.4', + requestedModel: 'gpt-5.3-codex', modelMap: { defaultModel: 'gpt-5.4', - haikuModel: 'gpt-5-codex-mini', + haikuModel: 'gpt-5.4-mini', }, - excludeModels: ['gpt-5-codex'], + excludeModels: ['gpt-5.4'], }) - ).toBe('gpt-5-codex-mini'); + ).toBe('gpt-5.4-mini'); }); - it('tracks Codex thinking caps for current safe defaults and paid models', () => { - expect(getModelMaxLevel('codex', 'gpt-5-codex')).toBe('high'); + it('tracks Codex thinking caps for current safe defaults, paid models, and legacy aliases', () => { + 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'); expect(getModelMaxLevel('codex', 'gpt-5-codex-mini')).toBe('high'); expect(getModelMaxLevel('codex', 'gpt-5.2-codex')).toBe('xhigh'); expect(getModelMaxLevel('codex', 'gpt-5.3-codex')).toBe('xhigh'); diff --git a/tests/unit/cliproxy/codex-reasoning-proxy-extended-context.test.ts b/tests/unit/cliproxy/codex-reasoning-proxy-extended-context.test.ts index 5a6dc3a2..569b9dc3 100644 --- a/tests/unit/cliproxy/codex-reasoning-proxy-extended-context.test.ts +++ b/tests/unit/cliproxy/codex-reasoning-proxy-extended-context.test.ts @@ -306,11 +306,11 @@ describe('CodexReasoningProxy extended-context compatibility', () => { expect(firstResponse.statusCode).toBe(200); expect(secondResponse.statusCode).toBe(200); - expect(firstResponse.body.model).toBe('gpt-5-codex'); + expect(firstResponse.body.model).toBe('gpt-5.4-mini'); expect(firstResponse.body.effort).toBe('high'); - expect(secondResponse.body.model).toBe('gpt-5-codex'); + expect(secondResponse.body.model).toBe('gpt-5.4-mini'); expect(secondResponse.body.effort).toBe('high'); - expect(capturedModels).toEqual(['gpt-5.4', 'gpt-5-codex', 'gpt-5-codex']); + expect(capturedModels).toEqual(['gpt-5.4', 'gpt-5.4-mini', 'gpt-5.4-mini']); expect(capturedEfforts).toEqual(['xhigh', 'high', 'high']); }); diff --git a/tests/unit/cliproxy/composite-env-routing.test.ts b/tests/unit/cliproxy/composite-env-routing.test.ts index 0ef51fc7..3bd86e6c 100644 --- a/tests/unit/cliproxy/composite-env-routing.test.ts +++ b/tests/unit/cliproxy/composite-env-routing.test.ts @@ -8,7 +8,7 @@ import { buildClaudeEnvironment } from '../../../src/cliproxy/executor/env-resol const tiers = { opus: { provider: 'agy' as const, model: 'claude-opus-4-6-thinking' }, sonnet: { provider: 'gemini' as const, model: 'gemini-2.5-pro' }, - haiku: { provider: 'codex' as const, model: 'gpt-5.1-codex-mini' }, + haiku: { provider: 'codex' as const, model: 'gpt-5.4-mini' }, }; describe('buildClaudeEnvironment - composite remote routing', () => { @@ -82,7 +82,7 @@ describe('buildClaudeEnvironment - composite remote routing', () => { compositeTiers: { opus: { provider: 'agy', model: 'claude-opus-4.6-thinking' }, sonnet: { provider: 'gemini', model: 'gemini-2.5-pro' }, - haiku: { provider: 'codex', model: 'gpt-5.1-codex-mini' }, + haiku: { provider: 'codex', model: 'gpt-5.4-mini' }, }, compositeDefaultTier: 'opus', }); @@ -90,6 +90,6 @@ describe('buildClaudeEnvironment - composite remote routing', () => { expect(env.ANTHROPIC_MODEL).toMatch(/^claude-opus-4-6-thinking(\([^)]+\))?$/); expect(env.ANTHROPIC_DEFAULT_OPUS_MODEL).toMatch(/^claude-opus-4-6-thinking(\([^)]+\))?$/); expect(env.ANTHROPIC_DEFAULT_SONNET_MODEL).toMatch(/^gemini-2.5-pro(\([^)]+\))?$/); - expect(env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toMatch(/^gpt-5.1-codex-mini(?:-medium)?$/); + expect(env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toMatch(/^gpt-5.4-mini(?:-medium)?$/); }); }); diff --git a/tests/unit/cliproxy/env-builder-provider-url.test.ts b/tests/unit/cliproxy/env-builder-provider-url.test.ts index bc7b71e1..7f96cde9 100644 --- a/tests/unit/cliproxy/env-builder-provider-url.test.ts +++ b/tests/unit/cliproxy/env-builder-provider-url.test.ts @@ -47,7 +47,7 @@ describe('getEffectiveEnvVars local provider URL normalization', () => { ANTHROPIC_MODEL: 'gpt-5.3-codex-xhigh', ANTHROPIC_DEFAULT_OPUS_MODEL: 'gpt-5.3-codex-xhigh', ANTHROPIC_DEFAULT_SONNET_MODEL: 'gpt-5.3-codex-high', - ANTHROPIC_DEFAULT_HAIKU_MODEL: 'gpt-5-mini-medium', + ANTHROPIC_DEFAULT_HAIKU_MODEL: 'gpt-5.4-mini-medium', }); const env = getEffectiveEnvVars('codex', 8317, settingsPath); @@ -55,7 +55,7 @@ describe('getEffectiveEnvVars local provider URL normalization', () => { expect(env.ANTHROPIC_MODEL).toBe('gpt-5.3-codex'); expect(env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('gpt-5.3-codex'); expect(env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('gpt-5.3-codex'); - expect(env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('gpt-5-mini'); + expect(env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('gpt-5.4-mini'); const persisted = JSON.parse(fs.readFileSync(settingsPath, 'utf-8')) as { env: Record; @@ -63,7 +63,7 @@ describe('getEffectiveEnvVars local provider URL normalization', () => { expect(persisted.env.ANTHROPIC_MODEL).toBe('gpt-5.3-codex'); expect(persisted.env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('gpt-5.3-codex'); expect(persisted.env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('gpt-5.3-codex'); - expect(persisted.env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('gpt-5-mini'); + expect(persisted.env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('gpt-5.4-mini'); }); it('rewrites wrong local provider path to the requested provider', () => { @@ -73,7 +73,7 @@ describe('getEffectiveEnvVars local provider URL normalization', () => { ANTHROPIC_MODEL: 'gpt-5.3-codex-xhigh', ANTHROPIC_DEFAULT_OPUS_MODEL: 'gpt-5.3-codex-xhigh', ANTHROPIC_DEFAULT_SONNET_MODEL: 'gpt-5.3-codex-high', - ANTHROPIC_DEFAULT_HAIKU_MODEL: 'gpt-5-mini-medium', + ANTHROPIC_DEFAULT_HAIKU_MODEL: 'gpt-5.4-mini-medium', }); const env = getEffectiveEnvVars('codex', 8317, settingsPath); @@ -438,7 +438,7 @@ describe('getEffectiveEnvVars local provider URL normalization', () => { ANTHROPIC_MODEL: ' gpt-5.3-codex-xhigh ', ANTHROPIC_DEFAULT_OPUS_MODEL: 'gpt-5.3-codex-xhigh', ANTHROPIC_DEFAULT_SONNET_MODEL: 'gpt-5.3-codex-high', - ANTHROPIC_DEFAULT_HAIKU_MODEL: 'gpt-5-mini-medium', + ANTHROPIC_DEFAULT_HAIKU_MODEL: 'gpt-5.4-mini-medium', }, presets: [ { @@ -446,7 +446,7 @@ describe('getEffectiveEnvVars local provider URL normalization', () => { default: 'gpt-5.3-codex-xhigh', opus: 'gpt-5.3-codex-xhigh', sonnet: 'gpt-5.3-codex-high', - haiku: 'gpt-5-mini-medium', + haiku: 'gpt-5.4-mini-medium', }, ], }, @@ -464,11 +464,11 @@ describe('getEffectiveEnvVars local provider URL normalization', () => { 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-mini'); + expect(repaired.env?.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('gpt-5.4-mini'); expect(repaired.presets?.[0]?.default).toBe('gpt-5.3-codex'); expect(repaired.presets?.[0]?.opus).toBe('gpt-5.3-codex'); expect(repaired.presets?.[0]?.sonnet).toBe('gpt-5.3-codex'); - expect(repaired.presets?.[0]?.haiku).toBe('gpt-5-mini'); + expect(repaired.presets?.[0]?.haiku).toBe('gpt-5.4-mini'); }); it('recovers malformed provider settings files by writing defaults and backup copy', () => { diff --git a/tests/unit/cliproxy/env-resolver-codex-fallback.test.ts b/tests/unit/cliproxy/env-resolver-codex-fallback.test.ts index 36036cfd..39ee4e6d 100644 --- a/tests/unit/cliproxy/env-resolver-codex-fallback.test.ts +++ b/tests/unit/cliproxy/env-resolver-codex-fallback.test.ts @@ -87,7 +87,7 @@ describe('buildClaudeEnvironment codex fallback normalization', () => { defaultModel: 'gpt-5.3-codex-high', opusModel: 'gpt-5.3-codex-xhigh', sonnetModel: 'gpt-5.3-codex-high', - haikuModel: 'gpt-5-mini-medium', + haikuModel: 'gpt-5.4-mini-medium', }); const env = buildClaudeEnvironment({ @@ -101,7 +101,7 @@ describe('buildClaudeEnvironment codex fallback normalization', () => { expect(env.ANTHROPIC_MODEL).toBe('gpt-5.3-codex(high)'); expect(env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('gpt-5.3-codex(xhigh)'); expect(env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('gpt-5.3-codex(high)'); - expect(env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('gpt-5-mini(medium)'); + expect(env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('gpt-5.4-mini(medium)'); }); it('keeps codex effort aliases when reasoning proxy is active', () => { @@ -109,7 +109,7 @@ describe('buildClaudeEnvironment codex fallback normalization', () => { defaultModel: 'gpt-5.3-codex-high', opusModel: 'gpt-5.3-codex-xhigh', sonnetModel: 'gpt-5.3-codex-high', - haikuModel: 'gpt-5-mini-medium', + haikuModel: 'gpt-5.4-mini-medium', }); const env = buildClaudeEnvironment({ @@ -124,7 +124,7 @@ describe('buildClaudeEnvironment codex fallback normalization', () => { expect(env.ANTHROPIC_MODEL).toBe('gpt-5.3-codex-high'); expect(env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('gpt-5.3-codex-xhigh'); expect(env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('gpt-5.3-codex-high'); - expect(env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('gpt-5-mini-medium'); + expect(env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('gpt-5.4-mini-medium'); expect(env.ANTHROPIC_BASE_URL).toBe('http://127.0.0.1:9444/api/provider/codex'); }); diff --git a/tests/unit/cliproxy/model-catalog.test.js b/tests/unit/cliproxy/model-catalog.test.js index 17068475..3e8f3751 100644 --- a/tests/unit/cliproxy/model-catalog.test.js +++ b/tests/unit/cliproxy/model-catalog.test.js @@ -186,6 +186,25 @@ describe('Model Catalog', () => { }); }); + describe('Codex models', () => { + it('has correct default model', () => { + const { MODEL_CATALOG } = modelCatalog; + assert.strictEqual(MODEL_CATALOG.codex.defaultModel, 'gpt-5.4'); + }); + + it('advertises the current official Codex model set', () => { + const { MODEL_CATALOG } = modelCatalog; + const ids = MODEL_CATALOG.codex.models.map((m) => m.id); + assert.deepStrictEqual(ids, [ + 'gpt-5.4', + 'gpt-5.4-mini', + 'gpt-5.3-codex', + 'gpt-5.3-codex-spark', + 'gpt-5.2', + ]); + }); + }); + describe('supportsModelConfig', () => { it('returns true for agy', () => { const { supportsModelConfig } = modelCatalog; diff --git a/tests/unit/cliproxy/model-id-normalizer.test.ts b/tests/unit/cliproxy/model-id-normalizer.test.ts index 6d4cc361..d7e969cf 100644 --- a/tests/unit/cliproxy/model-id-normalizer.test.ts +++ b/tests/unit/cliproxy/model-id-normalizer.test.ts @@ -8,6 +8,7 @@ import { migrateDeniedAntigravityModelAliases, normalizeClaudeDottedMajorMinor, normalizeClaudeDottedThinkingMajorMinor, + normalizeCodexLegacyModelAliases, normalizeModelIdForProvider, normalizeModelIdForRouting, normalizeModelEnvVarsForProvider, @@ -115,6 +116,14 @@ describe('model-id-normalizer', () => { expect(canonicalizeModelIdForProvider('minimax-m2.5', 'iflow')).toBe('qwen3-coder-plus'); expect(canonicalizeModelIdForProvider('kimi-k2.5', 'gemini')).toBe('kimi-k2.5'); }); + + it('normalizes legacy codex aliases to the current supported model IDs', () => { + expect(normalizeCodexLegacyModelAliases('gpt-5-codex')).toBe('gpt-5.4'); + expect(normalizeCodexLegacyModelAliases('gpt-5-codex-mini[1m]')).toBe('gpt-5.4-mini[1m]'); + expect(normalizeModelIdForProvider('gpt-5.2-codex', 'codex')).toBe('gpt-5.2'); + expect(normalizeModelIdForProvider('gpt-5.1-codex-mini', 'codex')).toBe('gpt-5.4-mini'); + expect(canonicalizeModelIdForProvider('gpt-5-codex-high', 'codex')).toBe('gpt-5.4'); + }); }); describe('env normalization', () => { diff --git a/tests/unit/cliproxy/variant-update-service.test.ts b/tests/unit/cliproxy/variant-update-service.test.ts index fe63fb7d..7e98a4ff 100644 --- a/tests/unit/cliproxy/variant-update-service.test.ts +++ b/tests/unit/cliproxy/variant-update-service.test.ts @@ -100,7 +100,7 @@ cliproxy: expect(result.success).toBe(true); expect(result.variant?.provider).toBe('codex'); - expect(result.variant?.model).toBe('gpt-5.1-codex-mini'); + expect(result.variant?.model).toBe('gpt-5.4-mini'); const settingsPath = path.join(tmpDir, 'gemini-demo.settings.json'); const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8')) as { @@ -109,10 +109,10 @@ cliproxy: }; expect(settings.env.ANTHROPIC_BASE_URL).toContain('/api/provider/codex'); - expect(settings.env.ANTHROPIC_MODEL).toBe('gpt-5.1-codex-mini'); - expect(settings.env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('gpt-5.1-codex-mini'); - expect(settings.env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('gpt-5.1-codex-mini'); - expect(settings.env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('gpt-5-codex-mini'); + expect(settings.env.ANTHROPIC_MODEL).toBe('gpt-5.4-mini'); + expect(settings.env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('gpt-5.4-mini'); + expect(settings.env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('gpt-5.4-mini'); + expect(settings.env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('gpt-5.4-mini'); expect(settings.env.CUSTOM_FLAG).toBe('keep-me'); expect(settings.hooks.PreToolUse.length).toBe(1); @@ -134,7 +134,7 @@ cliproxy: expect(settings.env.ANTHROPIC_MODEL).toBe('gpt-5.3-codex'); expect(settings.env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('gpt-5.3-codex'); expect(settings.env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('gpt-5.3-codex'); - expect(settings.env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('gpt-5-codex-mini'); + expect(settings.env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('gpt-5.4-mini'); const modelOnly = updateVariant('demo', { model: 'gpt-5.3-codex' }); expect(modelOnly.success).toBe(true); @@ -145,6 +145,6 @@ cliproxy: expect(settings.env.ANTHROPIC_MODEL).toBe('gpt-5.3-codex'); expect(settings.env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('gpt-5.3-codex'); expect(settings.env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('gpt-5.3-codex'); - expect(settings.env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('gpt-5-codex-mini'); + expect(settings.env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('gpt-5.4-mini'); }); }); diff --git a/tests/unit/web-server/cliproxy-stats-routes-model-update.test.ts b/tests/unit/web-server/cliproxy-stats-routes-model-update.test.ts index 84cba3c9..538152a4 100644 --- a/tests/unit/web-server/cliproxy-stats-routes-model-update.test.ts +++ b/tests/unit/web-server/cliproxy-stats-routes-model-update.test.ts @@ -137,7 +137,7 @@ describe('cliproxy-stats-routes model update canonicalization', () => { ANTHROPIC_MODEL: 'gpt-5.3-codex', ANTHROPIC_DEFAULT_OPUS_MODEL: 'gpt-5.3-codex', ANTHROPIC_DEFAULT_SONNET_MODEL: 'gpt-5.3-codex', - ANTHROPIC_DEFAULT_HAIKU_MODEL: 'gpt-5-mini', + ANTHROPIC_DEFAULT_HAIKU_MODEL: 'gpt-5.4-mini', }); const response = await fetch(`${baseUrl}/api/cliproxy/models/codex`, { @@ -156,7 +156,7 @@ describe('cliproxy-stats-routes model update canonicalization', () => { expect(persisted.env.ANTHROPIC_MODEL).toBe('gpt-5.3-codex'); expect(persisted.env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('gpt-5.3-codex'); expect(persisted.env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('gpt-5.3-codex'); - expect(persisted.env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('gpt-5-mini'); + expect(persisted.env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('gpt-5.4-mini'); }); it('canonicalizes legacy iflow model IDs to supported upstream IDs', async () => {