From 24eae8ae8f0dec38e0e12b8bb1147ebc1e8a85ed Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Fri, 10 Jul 2026 08:54:48 -0400 Subject: [PATCH] feat(codex): add GPT-5.6 model catalog Closes #1633 --- src/cliproxy/__tests__/model-catalog.test.js | 3 ++ .../codex-plan-compatibility.test.ts | 6 +++ src/cliproxy/model-catalog.ts | 36 ++++++++++++++++ .../cliproxy/provider-model-selector.tsx | 8 +++- ui/src/lib/codex-effort.ts | 7 ++-- ui/src/lib/model-catalogs.ts | 42 +++++++++++++++++++ .../cliproxy/provider-model-selector.test.tsx | 4 ++ .../unit/ui/lib/model-catalogs-codex.test.ts | 9 ++++ 8 files changed, 111 insertions(+), 4 deletions(-) diff --git a/src/cliproxy/__tests__/model-catalog.test.js b/src/cliproxy/__tests__/model-catalog.test.js index 32f4cecd..34d0a133 100644 --- a/src/cliproxy/__tests__/model-catalog.test.js +++ b/src/cliproxy/__tests__/model-catalog.test.js @@ -247,6 +247,9 @@ describe('Model Catalog', () => { const { MODEL_CATALOG } = modelCatalog; const ids = MODEL_CATALOG.codex.models.map((m) => m.id); assert.deepStrictEqual(ids, [ + 'gpt-5.6-sol', + 'gpt-5.6-terra', + 'gpt-5.6-luna', 'gpt-5.5', 'gpt-5.4', 'gpt-5.4-mini', diff --git a/src/cliproxy/ai-providers/__tests__/codex-plan-compatibility.test.ts b/src/cliproxy/ai-providers/__tests__/codex-plan-compatibility.test.ts index 45548c1d..932feeee 100644 --- a/src/cliproxy/ai-providers/__tests__/codex-plan-compatibility.test.ts +++ b/src/cliproxy/ai-providers/__tests__/codex-plan-compatibility.test.ts @@ -29,6 +29,9 @@ describe('codex plan compatibility', () => { }); it('does not rewrite cross-plan or already-safe Codex models', () => { + expect(getFreePlanFallbackCodexModel('gpt-5.6-sol')).toBeNull(); + expect(getFreePlanFallbackCodexModel('gpt-5.6-terra-high')).toBeNull(); + expect(getFreePlanFallbackCodexModel('gpt-5.6-luna-fast')).toBeNull(); expect(getFreePlanFallbackCodexModel('gpt-5.4')).toBeNull(); expect(getFreePlanFallbackCodexModel('gpt-5.4-mini')).toBeNull(); expect(getFreePlanFallbackCodexModel('gpt-5.2')).toBeNull(); @@ -94,6 +97,9 @@ describe('codex plan compatibility', () => { }); it('tracks Codex thinking caps for current safe defaults, paid models, and legacy aliases', () => { + expect(getModelMaxLevel('codex', 'gpt-5.6-sol')).toBe('xhigh'); + expect(getModelMaxLevel('codex', 'gpt-5.6-terra')).toBe('xhigh'); + expect(getModelMaxLevel('codex', 'gpt-5.6-luna')).toBe('xhigh'); 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'); diff --git a/src/cliproxy/model-catalog.ts b/src/cliproxy/model-catalog.ts index 8ba19731..94cf22b6 100644 --- a/src/cliproxy/model-catalog.ts +++ b/src/cliproxy/model-catalog.ts @@ -190,6 +190,42 @@ export const MODEL_CATALOG: Partial> = displayName: 'Copilot Codex', defaultModel: 'gpt-5.4', models: [ + { + id: 'gpt-5.6-sol', + name: 'GPT-5.6 Sol', + description: 'Latest frontier agentic coding model.', + thinking: { + type: 'levels', + levels: ['low', 'medium', 'high', 'xhigh'], + maxLevel: 'xhigh', + dynamicAllowed: false, + }, + codexServiceTiers: ['fast'], + }, + { + id: 'gpt-5.6-terra', + name: 'GPT-5.6 Terra', + description: 'Balanced agentic coding model for everyday work.', + thinking: { + type: 'levels', + levels: ['low', 'medium', 'high', 'xhigh'], + maxLevel: 'xhigh', + dynamicAllowed: false, + }, + codexServiceTiers: ['fast'], + }, + { + id: 'gpt-5.6-luna', + name: 'GPT-5.6 Luna', + description: 'Fast and affordable agentic coding model.', + thinking: { + type: 'levels', + levels: ['low', 'medium', 'high', 'xhigh'], + maxLevel: 'xhigh', + dynamicAllowed: false, + }, + codexServiceTiers: ['fast'], + }, { id: 'gpt-5.5', name: 'GPT-5.5', diff --git a/ui/src/components/cliproxy/provider-model-selector.tsx b/ui/src/components/cliproxy/provider-model-selector.tsx index aa644703..a9c47779 100644 --- a/ui/src/components/cliproxy/provider-model-selector.tsx +++ b/ui/src/components/cliproxy/provider-model-selector.tsx @@ -43,6 +43,8 @@ export interface ModelEntry { }; /** Highest codex reasoning-effort suffix this model supports in the dashboard UI. */ codexMaxEffort?: CodexEffort; + /** Exact Codex reasoning-effort suffixes supported by this model in the dashboard UI. */ + codexEfforts?: CodexEffort[]; /** Additional Codex service-tier suffixes supported by this model in the dashboard UI. */ codexServiceTiers?: CodexServiceTier[]; } @@ -347,12 +349,13 @@ function getPreferredOptionValue( function getModelOptionValues( codexMaxEffort: CodexEffort | undefined, + codexEfforts: readonly CodexEffort[] | undefined, codexServiceTiers: readonly CodexServiceTier[] | undefined, optionValue: string, isCodexProvider: boolean ): string[] { return isCodexProvider - ? getCodexEffortVariants(optionValue, codexMaxEffort, codexServiceTiers) + ? getCodexEffortVariants(optionValue, codexMaxEffort, codexServiceTiers, codexEfforts) : [optionValue]; } @@ -395,6 +398,7 @@ export function FlexibleModelSelector({ resolvedCatalogModels.flatMap((model) => getModelOptionValues( model.codexMaxEffort, + model.codexEfforts, model.codexServiceTiers, getPreferredOptionValue(model.id, routingHints.get(model.id.toLowerCase())), isCodexProvider @@ -431,6 +435,7 @@ export function FlexibleModelSelector({ const routingHint = routingHints.get(model.id.toLowerCase()); const optionValues = getModelOptionValues( model.codexMaxEffort, + model.codexEfforts, model.codexServiceTiers, getPreferredOptionValue(model.id, routingHint), isCodexProvider @@ -485,6 +490,7 @@ export function FlexibleModelSelector({ .flatMap((model) => { const routingHint = routingHints.get(model.id.toLowerCase()); const optionValues = getModelOptionValues( + undefined, undefined, undefined, getPreferredOptionValue(model.id, routingHint), diff --git a/ui/src/lib/codex-effort.ts b/ui/src/lib/codex-effort.ts index 7da7355b..ef884b0d 100644 --- a/ui/src/lib/codex-effort.ts +++ b/ui/src/lib/codex-effort.ts @@ -69,7 +69,8 @@ export function applyCodexEffortSuffix( export function getCodexEffortVariants( modelId: string, maxEffort: CodexEffort | undefined, - serviceTiers: readonly CodexServiceTier[] = [] + serviceTiers: readonly CodexServiceTier[] = [], + efforts?: readonly CodexEffort[] ): string[] { if (!maxEffort) { const explicitEffort = parseCodexEffort(modelId); @@ -91,9 +92,9 @@ export function getCodexEffortVariants( appendVariantsForEffort(undefined); - for (const effort of CODEX_EFFORTS_IN_ORDER) { + for (const effort of efforts ?? CODEX_EFFORTS_IN_ORDER) { appendVariantsForEffort(effort); - if (effort === maxEffort) { + if (!efforts && effort === maxEffort) { break; } } diff --git a/ui/src/lib/model-catalogs.ts b/ui/src/lib/model-catalogs.ts index 1d358ee7..2a6475ee 100644 --- a/ui/src/lib/model-catalogs.ts +++ b/ui/src/lib/model-catalogs.ts @@ -259,6 +259,48 @@ export const MODEL_CATALOGS: Record = { displayName: 'Codex', defaultModel: 'gpt-5.4', models: [ + { + id: 'gpt-5.6-sol', + name: 'GPT-5.6 Sol', + description: 'Latest frontier agentic coding model.', + codexMaxEffort: 'xhigh', + codexEfforts: ['low', 'medium', 'high', 'xhigh'], + codexServiceTiers: ['fast'], + presetMapping: { + default: 'gpt-5.6-sol', + opus: 'gpt-5.6-sol', + sonnet: 'gpt-5.6-sol', + haiku: 'gpt-5.4-mini', + }, + }, + { + id: 'gpt-5.6-terra', + name: 'GPT-5.6 Terra', + description: 'Balanced agentic coding model for everyday work.', + codexMaxEffort: 'xhigh', + codexEfforts: ['low', 'medium', 'high', 'xhigh'], + codexServiceTiers: ['fast'], + presetMapping: { + default: 'gpt-5.6-terra', + opus: 'gpt-5.6-terra', + sonnet: 'gpt-5.6-terra', + haiku: 'gpt-5.4-mini', + }, + }, + { + id: 'gpt-5.6-luna', + name: 'GPT-5.6 Luna', + description: 'Fast and affordable agentic coding model.', + codexMaxEffort: 'xhigh', + codexEfforts: ['low', 'medium', 'high', 'xhigh'], + codexServiceTiers: ['fast'], + presetMapping: { + default: 'gpt-5.6-luna', + opus: 'gpt-5.6-luna', + sonnet: 'gpt-5.6-luna', + haiku: 'gpt-5.4-mini', + }, + }, { id: 'gpt-5.5', name: 'GPT-5.5', diff --git a/ui/tests/unit/components/cliproxy/provider-model-selector.test.tsx b/ui/tests/unit/components/cliproxy/provider-model-selector.test.tsx index 4c71ee7b..c1c29da3 100644 --- a/ui/tests/unit/components/cliproxy/provider-model-selector.test.tsx +++ b/ui/tests/unit/components/cliproxy/provider-model-selector.test.tsx @@ -191,6 +191,10 @@ describe('FlexibleModelSelector', () => { expect(screen.getByText('gpt-5.3-codex-high')).toBeInTheDocument(); expect(screen.getByText('gpt-5.3-codex-xhigh')).toBeInTheDocument(); expect(screen.getByText('gpt-5.4-high-fast')).toBeInTheDocument(); + expect(screen.getByText('gpt-5.6-sol-low')).toBeInTheDocument(); + expect(screen.getByText('gpt-5.6-terra-xhigh')).toBeInTheDocument(); + expect(screen.getByText('gpt-5.6-luna-fast')).toBeInTheDocument(); + expect(screen.queryByText('gpt-5.6-sol-minimal')).not.toBeInTheDocument(); await userEvent.click(screen.getByText('gpt-5.3-codex-high')); expect(onChange).toHaveBeenCalledWith('gpt-5.3-codex-high'); 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 150de1c4..6c37a5ae 100644 --- a/ui/tests/unit/ui/lib/model-catalogs-codex.test.ts +++ b/ui/tests/unit/ui/lib/model-catalogs-codex.test.ts @@ -4,6 +4,9 @@ import { MODEL_CATALOGS } from '@/lib/model-catalogs'; describe('codex model catalog defaults', () => { it('mirrors the current Codex runtime catalog and free-safe defaults', () => { const codexCatalog = MODEL_CATALOGS.codex; + const codex56Sol = codexCatalog.models.find((model) => model.id === 'gpt-5.6-sol'); + const codex56Terra = codexCatalog.models.find((model) => model.id === 'gpt-5.6-terra'); + const codex56Luna = codexCatalog.models.find((model) => model.id === 'gpt-5.6-luna'); 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'); @@ -11,6 +14,12 @@ describe('codex model catalog defaults', () => { const codexMini = codexCatalog.models.find((model) => model.id === 'gpt-5.4-mini'); expect(codexCatalog.defaultModel).toBe('gpt-5.4'); + for (const model of [codex56Sol, codex56Terra, codex56Luna]) { + expect(model?.tier).toBeUndefined(); + expect(model?.codexEfforts).toEqual(['low', 'medium', 'high', 'xhigh']); + expect(model?.codexServiceTiers).toEqual(['fast']); + expect(model?.presetMapping?.haiku).toBe('gpt-5.4-mini'); + } expect(codex55?.tier).toBe('paid'); expect(codex55?.presetMapping?.haiku).toBe('gpt-5.4-mini'); expect(codex55?.codexMaxEffort).toBe('xhigh');