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 2b5ae791f8.

* fix codexplan custom gateway reasoning default

* fix codexplan reasoning query request routing

* fix(test): unmount CodexCredentialStep render instance before cleanup

---------

Co-authored-by: jatmn <the@jat.mn>
This commit is contained in:
Lucas Molinari
2026-08-10 10:09:48 +08:00
committed by GitHub
co-authored by jatmn
parent eb1de5b576
commit 95409464f3
18 changed files with 281 additions and 58 deletions
+1 -1
View File
@@ -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.
+36 -5
View File
@@ -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<string> {
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<string> {
instance.waitUntilExit(),
new Promise<void>(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(<StepChangeHarness />)
const { output } = await renderFinalFrame(<StepChangeHarness />)
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(
<CodexCredentialStep
onSave={() => {}}
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'
+2 -2
View File
@@ -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',
+4
View File
@@ -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,
}),
+61
View File
@@ -141,6 +141,25 @@ function makeOpenAIStreamChunk(
})}\n\n`
}
function makeOpenAIStreamingResponse(): Response {
const encoder = new TextEncoder()
return new Response(
new ReadableStream<Uint8Array>({
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<string, unknown> | 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'
+9 -6
View File
@@ -706,6 +706,8 @@ export function assistantMessageToMessageParam(
export type Options = {
getToolPermissionContext: () => Promise<ToolPermissionContext>
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 }),
+4 -3
View File
@@ -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 () => {
@@ -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',
})
})
@@ -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')
})
})
+22 -4
View File
@@ -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',
+5 -6
View File
@@ -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 =
+18 -13
View File
@@ -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<LegacyAPIProvider, ModelName>
// 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',
@@ -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'
+31 -8
View File
@@ -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.
@@ -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')
+2 -2
View File
@@ -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,
+3 -1
View File
@@ -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 () => {
+7
View File
@@ -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')
})