From 95409464f3685f49e14060ec314997c3a0428901 Mon Sep 17 00:00:00 2001 From: Lucas Molinari <101225122+lucasmolinari@users.noreply.github.com> Date: Sun, 9 Aug 2026 23:09:48 -0300 Subject: [PATCH] feat(codex): move codexplan default to GPT-5.6 Sol (#2051) * feat(codex): default codexplan to GPT-5.6 Sol Preserve existing reasoning and routing behavior while updating the default model, labels, documentation, and focused coverage. * test(codex): lock fallback and routing behavior * make credential-step test self-contained * fix(codex): default unset teammate fallback to GPT-5.6 Sol The changes to the inert codex config keys are known to have no functional effect, but we updated them to ensure the defaults are correct and consistent across the table. * fix codexplan gateway defaults after model resolution * Revert "fix codexplan gateway defaults after model resolution" This reverts commit 2b5ae791f8d3c2d8d1faade80cae0fc5fa53de5e. * fix codexplan custom gateway reasoning default * fix codexplan reasoning query request routing * fix(test): unmount CodexCredentialStep render instance before cleanup --------- Co-authored-by: jatmn --- docs/advanced-setup.md | 2 +- src/commands/provider/provider.test.tsx | 41 +++++++++++-- src/commands/provider/provider.tsx | 4 +- src/query.ts | 4 ++ src/services/api/claude.lifecycle.test.ts | 61 +++++++++++++++++++ src/services/api/claude.ts | 15 +++-- src/services/api/codexShim.test.ts | 7 ++- src/services/api/providerConfig.local.test.ts | 2 +- .../api/providerConfig.protoAlias.test.ts | 12 +++- src/services/api/providerConfig.test.ts | 26 ++++++-- src/services/api/providerConfig.ts | 11 ++-- src/utils/model/configs.ts | 31 ++++++---- .../model/model.openai-shim-providers.test.ts | 27 ++++++++ src/utils/model/model.ts | 39 +++++++++--- .../parseUserSpecifiedModel.codexTag.test.ts | 42 ++++++++++++- src/utils/providerProfiles.test.ts | 4 +- src/utils/status.test.ts | 4 +- src/utils/swarm/teammateModel.test.ts | 7 +++ 18 files changed, 281 insertions(+), 58 deletions(-) diff --git a/docs/advanced-setup.md b/docs/advanced-setup.md index 8f096f55c..6e32cfd5e 100644 --- a/docs/advanced-setup.md +++ b/docs/advanced-setup.md @@ -48,7 +48,7 @@ export OPENAI_MODEL=gpt-4o ### Codex via ChatGPT auth -`codexplan` maps to GPT-5.5 on the Codex backend with high reasoning. +`codexplan` maps to GPT-5.6 Sol on the Codex backend with high reasoning. `codexspark` maps to GPT-5.3 Codex Spark for faster loops. If you use the in-app provider wizard, choose `Codex OAuth` to open ChatGPT sign-in in your browser and let OpenClaude store Codex credentials securely. diff --git a/src/commands/provider/provider.test.tsx b/src/commands/provider/provider.test.tsx index 5549d68f1..d3d3d3183 100644 --- a/src/commands/provider/provider.test.tsx +++ b/src/commands/provider/provider.test.tsx @@ -4,7 +4,7 @@ import { afterEach, beforeEach, expect, mock, test } from 'bun:test' import React from 'react' import { stripVTControlCharacters as stripAnsi } from 'node:util' -import { createRoot, render, useApp } from '../../ink.js' +import { type Instance, createRoot, render, useApp } from '../../ink.js' import { AppStateProvider } from '../../state/AppState.js' import { applySavedProfileToCurrentSession, @@ -12,6 +12,7 @@ import { buildCurrentProviderSummary, buildProfileSaveMessage, buildProviderManagerCompletion, + CodexCredentialStep, getProviderWizardDefaults, ProviderWizard, TextEntryDialog, @@ -63,8 +64,9 @@ function extractLastFrame(output: string): string { return lastFrame ?? output } -async function renderFinalFrame(node: React.ReactNode): Promise { - let output = '' +async function renderFinalFrame( + node: React.ReactNode, +): Promise<{ output: string; instance: Instance }> { const { stdout, stdin, getOutput } = createTestStreams() const instance = await render(node, { @@ -78,7 +80,7 @@ async function renderFinalFrame(node: React.ReactNode): Promise { instance.waitUntilExit(), new Promise(resolve => setTimeout(resolve, 3000)), ]) - return stripAnsi(extractLastFrame(getOutput())) + return { output: stripAnsi(extractLastFrame(getOutput())), instance } } async function waitForOutput( @@ -226,7 +228,7 @@ function StepChangeHarness(): React.ReactNode { } test('TextEntryDialog resets its input state when initialValue changes', async () => { - const output = await renderFinalFrame() + const { output } = await renderFinalFrame() expect(output).toContain('Model step') expect(output).toContain('fresh-model-name') @@ -770,6 +772,35 @@ test('getProviderWizardDefaults ignores poisoned current provider values', () => expect(defaults.geminiModel).toBe('gemini-3-flash-preview') }) +test('CodexCredentialStep renders the codexplan Sol description', async () => { + const previousApiKey = process.env.CODEX_API_KEY + const previousAccountId = process.env.CHATGPT_ACCOUNT_ID + let instance: Instance | undefined + try { + process.env.CODEX_API_KEY = 'codex-test-key' + process.env.CHATGPT_ACCOUNT_ID = 'acct_test' + const result = await renderFinalFrame( + {}} + onBack={() => {}} + onCancel={() => {}} + />, + ) + instance = result.instance + + expect(result.output).toContain( + 'GPT-5.6 Sol with higher reasoning on the Codex backend', + ) + } finally { + instance?.unmount() + if (previousApiKey === undefined) delete process.env.CODEX_API_KEY + else process.env.CODEX_API_KEY = previousApiKey + + if (previousAccountId === undefined) delete process.env.CHATGPT_ACCOUNT_ID + else process.env.CHATGPT_ACCOUNT_ID = previousAccountId + } +}) + test('ProviderWizard hides Codex OAuth while running in bare mode', async () => { process.env.CLAUDE_CODE_SIMPLE = '1' diff --git a/src/commands/provider/provider.tsx b/src/commands/provider/provider.tsx index c79b05222..4f0269f37 100644 --- a/src/commands/provider/provider.tsx +++ b/src/commands/provider/provider.tsx @@ -1181,7 +1181,7 @@ function CodexOAuthStep({ ) } -function CodexCredentialStep({ +export function CodexCredentialStep({ onSave, onBack, onCancel, @@ -1216,7 +1216,7 @@ function CodexCredentialStep({ { label: 'codexplan', value: 'codexplan', - description: 'GPT-5.4 with higher reasoning on the Codex backend', + description: 'GPT-5.6 Sol with higher reasoning on the Codex backend', }, { label: 'codexspark', diff --git a/src/query.ts b/src/query.ts index 8dc317f78..1a99f9da4 100644 --- a/src/query.ts +++ b/src/query.ts @@ -98,6 +98,7 @@ import { notifyCommandLifecycle } from './utils/commandLifecycle.js' import { headlessProfilerCheckpoint } from './utils/headlessProfiler.js' import { getDefaultMainLoopModelSetting, + getProviderRequestModel, getRuntimeMainLoopModel, parseUserSpecifiedModel, renderModelName, @@ -1518,6 +1519,9 @@ async function* queryLoop( return appState.toolPermissionContext }, model: currentModel, + requestModel: pinnedTurnRoute?.routed + ? currentModel + : getProviderRequestModel(appStateMainLoopModel, currentModel), ...(config.gates.fastModeEnabled && { fastMode: appState.fastMode, }), diff --git a/src/services/api/claude.lifecycle.test.ts b/src/services/api/claude.lifecycle.test.ts index a975dd445..783296195 100644 --- a/src/services/api/claude.lifecycle.test.ts +++ b/src/services/api/claude.lifecycle.test.ts @@ -141,6 +141,25 @@ function makeOpenAIStreamChunk( })}\n\n` } +function makeOpenAIStreamingResponse(): Response { + const encoder = new TextEncoder() + return new Response( + new ReadableStream({ + start(controller) { + controller.enqueue( + encoder.encode( + makeOpenAIStreamChunk({ role: 'assistant', content: 'ok' }), + ), + ) + controller.enqueue(encoder.encode(makeOpenAIStreamChunk({}, 'stop'))) + controller.enqueue(encoder.encode('data: [DONE]\n\n')) + controller.close() + }, + }), + { headers: { 'content-type': 'text/event-stream' } }, + ) +} + function makeStallingOpenAIStreamResponse( onCancel?: (reason: unknown) => void, ): Response { @@ -337,6 +356,48 @@ afterEach(() => { }) describe('Claude API lifecycle tracking', () => { + test('uses the original codexplan selection for custom-gateway defaults', async () => { + setClientTestEnv() + process.env.CLAUDE_CODE_USE_OPENAI = '1' + process.env.OPENAI_BASE_URL = 'https://gateway.example/v1' + process.env.OPENAI_API_KEY = 'test-key' + process.env.OPENCLAUDE_MAX_RETRIES = '0' + const queryLifecycle = new QueryLifecycleOperationTracker() + let requestBody: Record | undefined + + globalThis.fetch = (async (_input, init) => { + requestBody = parseRequestBody(init) + return makeOpenAIStreamingResponse() + }) as typeof fetch + + const generator = queryModelWithStreaming({ + messages: [ + { + type: 'user', + uuid: '00000000-0000-0000-0000-000000000001', + timestamp: '2026-06-17T00:00:00.000Z', + message: { role: 'user', content: 'hello' }, + } as Message, + ], + systemPrompt: asSystemPrompt([]), + thinkingConfig: { type: 'disabled' }, + tools: [], + signal: new AbortController().signal, + options: { + ...makeOptions(queryLifecycle), + model: 'gpt-5.6-sol', + requestModel: 'codexplan', + }, + }) + + for await (const _message of generator) { + // Drain the stream before asserting its request. + } + + expect(requestBody?.model).toBe('gpt-5.6-sol') + expect(requestBody?.reasoning_effort).toBe('high') + }) + test('checks provider-request ownership immediately before dispatch', async () => { setClientTestEnv() process.env.OPENCLAUDE_MAX_RETRIES = '0' diff --git a/src/services/api/claude.ts b/src/services/api/claude.ts index 91d2dd764..909f52691 100644 --- a/src/services/api/claude.ts +++ b/src/services/api/claude.ts @@ -706,6 +706,8 @@ export function assistantMessageToMessageParam( export type Options = { getToolPermissionContext: () => Promise model: string + /** Original selection used only for provider-side alias routing. */ + requestModel?: string toolChoice?: BetaToolChoiceTool | BetaToolChoiceAuto | undefined isNonInteractiveSession: boolean extraToolSchemas?: BetaToolUnion[] @@ -1154,6 +1156,7 @@ async function* queryModel( StreamEvent | AssistantMessage | SystemAPIErrorMessage, void > { + const providerRequestModel = options.requestModel ?? options.model // Check cheap conditions first — the off-switch await blocks on GrowthBook // init (~10ms). For non-Opus models (haiku, sonnet) this skips the await // entirely. Subscribers don't hit this path at all. @@ -1880,7 +1883,7 @@ async function* queryModel( lastRequestBetas = betasParams return { - model: normalizeModelStringForAPI(options.model), + model: normalizeModelStringForAPI(providerRequestModel), // IMPORTANT: `system` must appear before `messages` in the object literal. // JSON.stringify preserves insertion order. The native Bun attestation // (Attestation.zig) overwrites the FIRST `cch=00000` sentinel in the @@ -1942,7 +1945,7 @@ async function* queryModel( () => getAnthropicClient({ maxRetries: 0, // Disabled auto-retry in favor of manual implementation - model: options.model, + model: providerRequestModel, fetchOverride: options.fetchOverride, source: options.querySource, providerOverride: options.providerOverride, @@ -2870,9 +2873,9 @@ async function* queryModel( }) endActiveApiCall() const result = yield* executeNonStreamingRequest( - { model: options.model, source: options.querySource, providerOverride: options.providerOverride, effortValue: effort }, + { model: providerRequestModel, source: options.querySource, providerOverride: options.providerOverride, effortValue: effort }, { - model: options.model, + model: providerRequestModel, fallbackModel: options.fallbackModel, thinkingConfig, ...(isFastModeEnabled() && { fastMode: isFastMode }), @@ -2990,13 +2993,13 @@ async function* queryModel( endActiveApiCall() const result = yield* executeNonStreamingRequest( { - model: options.model, + model: providerRequestModel, source: options.querySource, providerOverride: options.providerOverride, effortValue: effort, }, { - model: options.model, + model: providerRequestModel, fallbackModel: options.fallbackModel, thinkingConfig, ...(isFastModeEnabled() && { fastMode: isFastMode }), diff --git a/src/services/api/codexShim.test.ts b/src/services/api/codexShim.test.ts index ee7422f53..6eaaf9f54 100644 --- a/src/services/api/codexShim.test.ts +++ b/src/services/api/codexShim.test.ts @@ -159,7 +159,7 @@ describe('Codex provider config', () => { const resolved = resolveProviderRequest({ model: 'codexplan' }) expect(resolved.transport).toBe('codex_responses') - expect(resolved.resolvedModel).toBe('gpt-5.5') + expect(resolved.resolvedModel).toBe('gpt-5.6-sol') expect(resolved.reasoning).toEqual({ effort: 'high' }) expect(resolved.baseUrl).toBe('https://chatgpt.com/backend-api/codex') }) @@ -185,7 +185,8 @@ describe('Codex provider config', () => { expect(resolved.transport).toBe('chat_completions') expect(resolved.baseUrl).toBe('http://127.0.0.1:8080/v1') - expect(resolved.resolvedModel).toBe('gpt-5.5') + expect(resolved.resolvedModel).toBe('gpt-5.6-sol') + expect(resolved.reasoning).toEqual({ effort: 'high' }) }) test('resolves codexplan to Codex transport even when OPENAI_BASE_URL is the string "undefined"', async () => { @@ -232,7 +233,7 @@ describe('Codex provider config', () => { const resolved = resolveProviderRequest() expect(resolved.transport).toBe('codex_responses') expect(resolved.baseUrl).toBe('https://chatgpt.com/backend-api/codex') - expect(resolved.resolvedModel).toBe('gpt-5.5') + expect(resolved.resolvedModel).toBe('gpt-5.6-sol') }) test('does not override custom base URL for codexplan (e.g., local provider)', async () => { diff --git a/src/services/api/providerConfig.local.test.ts b/src/services/api/providerConfig.local.test.ts index 2849dcfa9..fd644f84c 100644 --- a/src/services/api/providerConfig.local.test.ts +++ b/src/services/api/providerConfig.local.test.ts @@ -320,7 +320,7 @@ test('keeps Codex backend on Codex responses transport even when API format is s expect(resolveProviderRequest()).toMatchObject({ transport: 'codex_responses', requestedModel: 'codexplan', - resolvedModel: 'gpt-5.5', + resolvedModel: 'gpt-5.6-sol', baseUrl: 'https://chatgpt.com/backend-api/codex', }) }) diff --git a/src/services/api/providerConfig.protoAlias.test.ts b/src/services/api/providerConfig.protoAlias.test.ts index bb05175e7..db85d4f2b 100644 --- a/src/services/api/providerConfig.protoAlias.test.ts +++ b/src/services/api/providerConfig.protoAlias.test.ts @@ -111,11 +111,17 @@ describe('providerConfig — Codex alias lookup is prototype-safe', () => { }) test('resolveProviderRequest query branch still resolves a genuine Codex alias', () => { - const { resolvedModel } = resolveProviderRequest({ + const request = resolveProviderRequest({ model: 'codexplan?reasoning=medium', processEnv: {}, }) - expect(resolvedModel).toBe('gpt-5.5') + expect(request).toMatchObject({ + requestedModel: 'codexplan?reasoning=medium', + resolvedModel: 'gpt-5.6-sol', + transport: 'codex_responses', + baseUrl: 'https://chatgpt.com/backend-api/codex', + reasoning: { effort: 'medium' }, + }) }) test('resolveProviderRequest still resolves a genuine Codex alias', () => { @@ -123,6 +129,6 @@ describe('providerConfig — Codex alias lookup is prototype-safe', () => { model: 'codexplan', processEnv: {}, }) - expect(resolvedModel).toBe('gpt-5.5') + expect(resolvedModel).toBe('gpt-5.6-sol') }) }) diff --git a/src/services/api/providerConfig.test.ts b/src/services/api/providerConfig.test.ts index 369b87e16..558927780 100644 --- a/src/services/api/providerConfig.test.ts +++ b/src/services/api/providerConfig.test.ts @@ -334,6 +334,16 @@ test('resolveProviderRequest resolves the GPT-5.6 family Codex aliases', () => { expect(bare.reasoning).toEqual({ effort: 'high' }) }) +test('resolveProviderRequest keeps the implicit Codex fallback on Sol with high reasoning', () => { + expect(resolveProviderRequest({ processEnv: {} })).toMatchObject({ + requestedModel: 'codexplan', + resolvedModel: 'gpt-5.6-sol', + transport: 'codex_responses', + baseUrl: 'https://chatgpt.com/backend-api/codex', + reasoning: { effort: 'high' }, + }) +}) + test('resolveProviderRequest honors reasoning query overrides on GPT-5.6 aliases', () => { const request = resolveProviderRequest({ model: 'gpt-5.6-sol?reasoning=medium', @@ -353,10 +363,18 @@ test('resolveProviderRequest scopes GPT-5.6 alias effort defaults to the Codex t OPENAI_API_KEY: 'test-key', } - const plain = resolveProviderRequest({ model: 'gpt-5.6-sol', processEnv }) - expect(plain.resolvedModel).toBe('gpt-5.6-sol') - expect(plain.transport).not.toBe('codex_responses') - expect(plain.reasoning).toBeUndefined() + for (const model of ['gpt-5.6', 'gpt-5.6-sol']) { + const plain = resolveProviderRequest({ model, processEnv }) + expect(plain.resolvedModel).toBe('gpt-5.6-sol') + expect(plain.transport).not.toBe('codex_responses') + expect(plain.reasoning).toBeUndefined() + } + + const codexplan = resolveProviderRequest({ model: 'codexplan', processEnv }) + expect(codexplan.resolvedModel).toBe('gpt-5.6-sol') + expect(codexplan.baseUrl).toBe('https://gateway.example/v1') + expect(codexplan.transport).not.toBe('codex_responses') + expect(codexplan.reasoning).toEqual({ effort: 'high' }) const explicit = resolveProviderRequest({ model: 'gpt-5.6-sol?reasoning=medium', diff --git a/src/services/api/providerConfig.ts b/src/services/api/providerConfig.ts index dc023a3d4..0ccd2e28c 100644 --- a/src/services/api/providerConfig.ts +++ b/src/services/api/providerConfig.ts @@ -79,7 +79,7 @@ const CODEX_ALIAS_MODELS: Record< } > = { codexplan: { - model: 'gpt-5.5', + model: 'gpt-5.6-sol', reasoningEffort: 'high', }, // GPT-5.6 family (July 2026). `gpt-5.6` follows the Codex CLI convention of @@ -1132,18 +1132,17 @@ export function resolveProviderRequest(options?: { ? requestedApiFormat : 'chat_completions' - // The gpt-5.6 alias defaults are Codex-transport-only: off the Codex + // Explicit gpt-5.6 alias defaults are Codex-transport-only: off the Codex // transport the 5.6 family's effort metadata is owned by the route catalog // (#1961), and an OPENAI_API_BASE gateway must not inherit the first-party // default. Explicit picks (the /effort override or a ?reasoning= query) - // still flow on every transport, and the older aliases (gpt-5.4/5.5, - // codexplan) keep the pre-5.6 legacy behavior of carrying their default - // effort everywhere. + // still flow on every transport, and codexplan keeps its existing behavior + // of carrying its high default effort everywhere. const requestedReasoning = options?.reasoningEffortOverride ? { effort: options.reasoningEffortOverride } : descriptor.reasoningFromAlias && transport !== 'codex_responses' && - /^gpt-5\.6/.test(descriptor.baseModel) + /^gpt-5\.6(?:-|$|[?[])/i.test(requestedModel.trim()) ? undefined : descriptor.reasoning const catalogReasoningLevels = diff --git a/src/utils/model/configs.ts b/src/utils/model/configs.ts index 0fb20a284..530d127a8 100644 --- a/src/utils/model/configs.ts +++ b/src/utils/model/configs.ts @@ -5,6 +5,11 @@ import type { LegacyAPIProvider } from './providers.js' // returned from getAPIProvider(). Descriptor-native callers should prefer // route/model metadata directly; this table exists for older provider-keyed // consumers that have not been retired yet. +// +// Note: the `codex` keys are inert legacy compatibility entries. modelStrings.ts +// routes codex to the `openai` key, and the only real reader is +// getHardcodedTeammateModelFallback() (via CLAUDE_OPUS_4_8_CONFIG). We keep +// them in sync with the Codex runtime default (gpt-5.6-sol) for consistency. export type LegacyProviderModelConfig = Record // Backward-compatible alias for existing imports. @@ -44,7 +49,7 @@ export const CLAUDE_3_7_SONNET_CONFIG = { gemini: 'gemini-2.0-flash', mistral: 'mistral-medium-latest', github: 'github:copilot', - codex: 'gpt-5.5', + codex: 'gpt-5.6-sol', 'nvidia-nim': 'nvidia/llama-3.1-nemotron-70b-instruct', minimax: 'MiniMax-M2.5', 'xiaomi-mimo': 'mimo-v2.5-pro', @@ -60,7 +65,7 @@ export const CLAUDE_3_5_V2_SONNET_CONFIG = { gemini: 'gemini-2.0-flash', mistral: 'mistral-medium-latest', github: 'github:copilot', - codex: 'gpt-5.5', + codex: 'gpt-5.6-sol', 'nvidia-nim': 'nvidia/llama-3.1-nemotron-70b-instruct', minimax: 'MiniMax-M2.5', 'xiaomi-mimo': 'mimo-v2.5-pro', @@ -76,7 +81,7 @@ export const CLAUDE_3_5_HAIKU_CONFIG = { gemini: 'gemini-2.0-flash-lite', mistral: 'ministral-3b-latest', github: 'github:copilot', - codex: 'gpt-5.5', + codex: 'gpt-5.6-sol', 'nvidia-nim': 'nvidia/llama-3.1-nemotron-70b-instruct', minimax: 'MiniMax-M2.5', 'xiaomi-mimo': 'mimo-v2.5-pro', @@ -92,7 +97,7 @@ export const CLAUDE_HAIKU_4_5_CONFIG = { gemini: 'gemini-2.0-flash-lite', mistral: 'ministral-3b-latest', github: 'github:copilot', - codex: 'gpt-5.5', + codex: 'gpt-5.6-sol', 'nvidia-nim': 'nvidia/llama-3.1-nemotron-70b-instruct', minimax: 'MiniMax-M2.5', 'xiaomi-mimo': 'mimo-v2.5-pro', @@ -108,7 +113,7 @@ export const CLAUDE_SONNET_4_CONFIG = { gemini: 'gemini-2.0-flash', mistral: 'mistral-medium-latest', github: 'github:copilot', - codex: 'gpt-5.5', + codex: 'gpt-5.6-sol', 'nvidia-nim': 'nvidia/llama-3.1-nemotron-70b-instruct', minimax: 'MiniMax-M2.5', 'xiaomi-mimo': 'mimo-v2.5-pro', @@ -124,7 +129,7 @@ export const CLAUDE_SONNET_4_5_CONFIG = { gemini: 'gemini-2.0-flash', mistral: 'mistral-medium-latest', github: 'github:copilot', - codex: 'gpt-5.5', + codex: 'gpt-5.6-sol', 'nvidia-nim': 'nvidia/llama-3.1-nemotron-70b-instruct', minimax: 'MiniMax-M2.5', 'xiaomi-mimo': 'mimo-v2.5-pro', @@ -140,7 +145,7 @@ export const CLAUDE_OPUS_4_CONFIG = { gemini: 'gemini-2.5-pro', mistral: 'devstral-latest', github: 'github:copilot', - codex: 'gpt-5.5', + codex: 'gpt-5.6-sol', 'nvidia-nim': 'nvidia/llama-3.1-nemotron-70b-instruct', minimax: 'MiniMax-M2.5', 'xiaomi-mimo': 'mimo-v2.5-pro', @@ -156,7 +161,7 @@ export const CLAUDE_OPUS_4_1_CONFIG = { gemini: 'gemini-2.5-pro', mistral: 'devstral-latest', github: 'github:copilot', - codex: 'gpt-5.5', + codex: 'gpt-5.6-sol', 'nvidia-nim': 'nvidia/llama-3.1-nemotron-70b-instruct', minimax: 'MiniMax-M2.5', 'xiaomi-mimo': 'mimo-v2.5-pro', @@ -172,7 +177,7 @@ export const CLAUDE_OPUS_4_5_CONFIG = { gemini: 'gemini-2.5-pro', mistral: 'devstral-latest', github: 'github:copilot', - codex: 'gpt-5.5', + codex: 'gpt-5.6-sol', 'nvidia-nim': 'nvidia/llama-3.1-nemotron-70b-instruct', minimax: 'MiniMax-M2.5', 'xiaomi-mimo': 'mimo-v2.5-pro', @@ -188,7 +193,7 @@ export const CLAUDE_OPUS_4_6_CONFIG = { gemini: 'gemini-2.5-pro', mistral: 'devstral-latest', github: 'github:copilot', - codex: 'gpt-5.5', + codex: 'gpt-5.6-sol', 'nvidia-nim': 'nvidia/llama-3.1-nemotron-70b-instruct', minimax: 'MiniMax-M2.5', 'xiaomi-mimo': 'mimo-v2.5-pro', @@ -204,7 +209,7 @@ export const CLAUDE_OPUS_4_7_CONFIG = { gemini: 'gemini-2.5-pro', mistral: 'devstral-latest', github: 'github:copilot', - codex: 'gpt-5.5', + codex: 'gpt-5.6-sol', 'nvidia-nim': 'nvidia/llama-3.1-nemotron-70b-instruct', minimax: 'MiniMax-M2.5', 'xiaomi-mimo': 'mimo-v2.5-pro', @@ -220,7 +225,7 @@ export const CLAUDE_OPUS_4_8_CONFIG = { gemini: 'gemini-2.5-pro', mistral: 'devstral-latest', github: 'github:copilot', - codex: 'gpt-5.5', + codex: 'gpt-5.6-sol', 'nvidia-nim': 'nvidia/llama-3.1-nemotron-70b-instruct', minimax: 'MiniMax-M2.5', 'xiaomi-mimo': 'mimo-v2.5-pro', @@ -236,7 +241,7 @@ export const CLAUDE_SONNET_4_6_CONFIG = { gemini: 'gemini-2.0-flash', mistral: 'mistral-medium-latest', github: 'github:copilot', - codex: 'gpt-5.5', + codex: 'gpt-5.6-sol', 'nvidia-nim': 'nvidia/llama-3.1-nemotron-70b-instruct', minimax: 'MiniMax-M2.5', 'xiaomi-mimo': 'mimo-v2.5-pro', diff --git a/src/utils/model/model.openai-shim-providers.test.ts b/src/utils/model/model.openai-shim-providers.test.ts index 3af48a4f2..17c50fe60 100644 --- a/src/utils/model/model.openai-shim-providers.test.ts +++ b/src/utils/model/model.openai-shim-providers.test.ts @@ -195,6 +195,33 @@ test('codex provider reads OPENAI_MODEL, not stale settings.model', async () => expect(model).toBe('codexplan') }) +test('Codex runtime fallbacks use Sol and honor OPENAI_MODEL', async () => { + process.env.CLAUDE_CODE_USE_OPENAI = '1' + process.env.OPENAI_BASE_URL = 'https://chatgpt.com/backend-api/codex' + + const { + getDefaultOpusModel, + getDefaultSonnetModel, + getDefaultHaikuModel, + getDefaultMainLoopModel, + } = await importFreshModelModule() + const helpers = [ + getDefaultOpusModel, + getDefaultSonnetModel, + getDefaultHaikuModel, + getDefaultMainLoopModel, + ] + + for (const helper of helpers) { + expect(helper()).toBe('gpt-5.6-sol') + } + + process.env.OPENAI_MODEL = 'gpt-5.6-terra' + for (const helper of helpers) { + expect(helper()).toBe('gpt-5.6-terra') + } +}) + test('nvidia-nim provider reads OPENAI_MODEL, not stale settings.model', async () => { saveGlobalConfig(current => ({ ...current, model: 'kimi-k2.6' })) process.env.NVIDIA_NIM = '1' diff --git a/src/utils/model/model.ts b/src/utils/model/model.ts index 3b8c8032d..ab9bbcae9 100644 --- a/src/utils/model/model.ts +++ b/src/utils/model/model.ts @@ -207,9 +207,9 @@ export function getDefaultOpusModel(): ModelName { if (getAPIProvider() === 'openai') { return process.env.OPENAI_MODEL || 'gpt-4o' } - // Codex provider: use user-specified model or default to gpt-5.5 + // Codex provider: use user-specified model or default if (getAPIProvider() === 'codex') { - return process.env.OPENAI_MODEL || 'gpt-5.5' + return process.env.OPENAI_MODEL || 'gpt-5.6-sol' } // GitHub Copilot provider if (getAPIProvider() === 'github') { @@ -259,7 +259,7 @@ export function getDefaultSonnetModel(): ModelName { } // Codex provider if (getAPIProvider() === 'codex') { - return process.env.OPENAI_MODEL || 'gpt-5.5' + return process.env.OPENAI_MODEL || 'gpt-5.6-sol' } // GitHub Copilot provider if (getAPIProvider() === 'github') { @@ -303,7 +303,7 @@ export function getDefaultHaikuModel(): ModelName { } // Codex provider if (getAPIProvider() === 'codex') { - return process.env.OPENAI_MODEL || 'gpt-5.5' + return process.env.OPENAI_MODEL || 'gpt-5.6-sol' } // GitHub Copilot provider if (getAPIProvider() === 'github') { @@ -399,9 +399,9 @@ export function getDefaultMainLoopModelSetting(): ModelName | ModelAlias { if (getAPIProvider() === 'openai') { return process.env.OPENAI_MODEL || 'gpt-4o' } - // Codex provider: always use the configured Codex model (default gpt-5.5) + // Codex provider: always use the configured Codex model if (getAPIProvider() === 'codex') { - return process.env.OPENAI_MODEL || 'gpt-5.5' + return process.env.OPENAI_MODEL || 'gpt-5.6-sol' } // NVIDIA NIM uses OpenAI-compatible model ids. Keep this fallback aligned // with the route descriptor so headless sessions never send a Claude model. @@ -595,7 +595,7 @@ export function renderModelSetting(setting: ModelName | ModelAlias): string { } // Handle Codex models - show actual model name + resolved model if (setting === 'codexplan') { - return 'codexplan (gpt-5.5)' + return 'codexplan (gpt-5.6-sol)' } if (setting === 'codexspark') { return 'codexspark (gpt-5.3-codex-spark)' @@ -835,7 +835,7 @@ export function parseUserSpecifiedModel( // silently shrink a `codexplan[1m]`/`codexspark[1m]` session back to the // model default. if (modelString === 'codexplan') { - return 'gpt-5.5' + (has1mTag ? '[1m]' : '') + return 'gpt-5.6-sol' + (has1mTag ? '[1m]' : '') } if (modelString === 'codexspark') { return 'gpt-5.3-codex-spark' + (has1mTag ? '[1m]' : '') @@ -896,6 +896,29 @@ export function parseUserSpecifiedModel( return modelInputTrimmed } +// Runtime code needs the concrete model for capabilities and routing, but a +// custom gateway still distinguishes the legacy codexplan selection from an +// explicit GPT-5.6 Sol request when applying its default reasoning effort. +export function getProviderRequestModel( + selectedModel: string, + runtimeModel: string, +): string { + const selected = selectedModel.trim() + const selectedBase = selected + .replace(/\[1m]$/i, '') + .split('?', 1)[0] + ?.toLowerCase() + const runtimeBase = runtimeModel + .trim() + .replace(/\[1m]$/i, '') + .split('?', 1)[0] + ?.toLowerCase() + return selectedBase === 'codexplan' && + runtimeBase === parseUserSpecifiedModel('codexplan') + ? selected + : runtimeModel +} + /** * Resolves a skill's `model:` frontmatter against the current model, carrying * the `[1m]` suffix over when the target family supports it. diff --git a/src/utils/model/parseUserSpecifiedModel.codexTag.test.ts b/src/utils/model/parseUserSpecifiedModel.codexTag.test.ts index 11870265f..152b631ae 100644 --- a/src/utils/model/parseUserSpecifiedModel.codexTag.test.ts +++ b/src/utils/model/parseUserSpecifiedModel.codexTag.test.ts @@ -1,6 +1,10 @@ import { describe, expect, test } from 'bun:test' import { has1mContext } from '../context.js' -import { parseUserSpecifiedModel } from './model.js' +import { + getProviderRequestModel, + parseUserSpecifiedModel, + renderModelSetting, +} from './model.js' // Regression: the Codex aliases (codexplan/codexspark) dropped the `[1m]` // (1M-context) tag while every Claude alias (opus/sonnet/haiku/best) preserved @@ -26,18 +30,49 @@ describe('parseUserSpecifiedModel — codex alias 1M tag', () => { expect(has1mContext(tagged)).toBe(true) }) - test('the bare codex aliases are unchanged and carry no 1M tag', () => { - expect(parseUserSpecifiedModel('codexplan')).toBe('gpt-5.5') + test('the bare codex aliases resolve and carry no 1M tag', () => { + expect(parseUserSpecifiedModel('codexplan')).toBe('gpt-5.6-sol') expect(parseUserSpecifiedModel('codexspark')).toBe('gpt-5.3-codex-spark') expect(has1mContext(parseUserSpecifiedModel('codexplan'))).toBe(false) expect(has1mContext(parseUserSpecifiedModel('codexspark'))).toBe(false) }) + test('codexplan reasoning queries stay symbolic for Codex routing', () => { + expect(parseUserSpecifiedModel('codexplan?reasoning=medium')).toBe( + 'codexplan?reasoning=medium', + ) + }) + test('the tag is case-insensitive and not duplicated', () => { const tagged = parseUserSpecifiedModel('codexplan[1m]') expect(parseUserSpecifiedModel('CODEXPLAN[1M]')).toBe(tagged) expect(tagged.match(/\[1m]/gi)?.length).toBe(1) }) + + test('codexplan display identifies the Sol model', () => { + expect(renderModelSetting('codexplan')).toBe('codexplan (gpt-5.6-sol)') + }) + + test('keeps codexplan as the provider request selection after runtime resolution', () => { + expect(getProviderRequestModel('codexplan', 'gpt-5.6-sol')).toBe( + 'codexplan', + ) + expect(getProviderRequestModel('gpt-5.6-sol', 'gpt-5.6-sol')).toBe( + 'gpt-5.6-sol', + ) + expect(getProviderRequestModel('codexplan', 'gpt-5.6-terra')).toBe( + 'gpt-5.6-terra', + ) + expect( + getProviderRequestModel('codexplan?reasoning=medium', 'gpt-5.6-sol'), + ).toBe('codexplan?reasoning=medium') + expect( + getProviderRequestModel( + 'codexplan?reasoning=medium', + 'gpt-5.6-sol?reasoning=medium', + ), + ).toBe('codexplan?reasoning=medium') + }) }) // Bare gpt-5.6 resolves to the flagship tier (Sol) at parse time — not just in @@ -50,6 +85,7 @@ describe('parseUserSpecifiedModel — bare gpt-5.6 resolves to Sol', () => { }) test('explicit tier ids pass through unchanged', () => { + expect(parseUserSpecifiedModel('gpt-5.5')).toBe('gpt-5.5') expect(parseUserSpecifiedModel('gpt-5.6-sol')).toBe('gpt-5.6-sol') expect(parseUserSpecifiedModel('gpt-5.6-terra')).toBe('gpt-5.6-terra') expect(parseUserSpecifiedModel('gpt-5.6-luna')).toBe('gpt-5.6-luna') diff --git a/src/utils/providerProfiles.test.ts b/src/utils/providerProfiles.test.ts index 77c69959c..d08f6e918 100644 --- a/src/utils/providerProfiles.test.ts +++ b/src/utils/providerProfiles.test.ts @@ -2105,8 +2105,8 @@ describe('applyActiveProviderProfileFromConfig', () => { // Regression: the Codex OAuth profile is created with a single // `codexplan` model entry, so profileSupportsModel rejected any other // Codex model saved via /model (e.g. gpt-5.6-terra) and the next startup - // silently reverted to codexplan → gpt-5.5. Codex-backend profiles must - // accept every Codex alias model and gpt-5.x free-text picks. + // silently reverted to the model behind codexplan. Codex-backend profiles + // must accept every Codex alias model and gpt-5.x free-text picks. const { _setSavedModelOverrideForTesting, applyActiveProviderProfileFromConfig, diff --git a/src/utils/status.test.ts b/src/utils/status.test.ts index 59d26f7e7..234077935 100644 --- a/src/utils/status.test.ts +++ b/src/utils/status.test.ts @@ -125,7 +125,9 @@ test('buildAPIProviderProperties keeps Codex-specific labels on the shared OpenA expect(await readPropertyValue('Codex base URL', 'codex')).toBe( DEFAULT_CODEX_BASE_URL, ) - expect(await readPropertyValue('Model', 'codex')).toBe('gpt-5.5 (high)') + expect(await readPropertyValue('Model', 'codex')).toBe( + 'gpt-5.6-sol (high)', + ) }) test('buildAPIProviderProperties redacts credentials in OpenAI-compatible base URLs', async () => { diff --git a/src/utils/swarm/teammateModel.test.ts b/src/utils/swarm/teammateModel.test.ts index 759e54275..a7492fbb2 100644 --- a/src/utils/swarm/teammateModel.test.ts +++ b/src/utils/swarm/teammateModel.test.ts @@ -48,3 +48,10 @@ test('getHardcodedTeammateModelFallback is provider-aware (Bedrock gets the Opus 'us.anthropic.claude-opus-4-8-v1', ) }) + +test('getHardcodedTeammateModelFallback returns the Codex default (GPT-5.6 Sol) for codex', async () => { + const { getHardcodedTeammateModelFallback } = + await importFreshTeammateModelModule('codex') + + expect(getHardcodedTeammateModelFallback()).toBe('gpt-5.6-sol') +})