From 2b5ae791f8d3c2d8d1faade80cae0fc5fa53de5e Mon Sep 17 00:00:00 2001 From: jatmn Date: Sat, 1 Aug 2026 10:46:30 -0700 Subject: [PATCH] fix codexplan gateway defaults after model resolution --- src/cli/print.ts | 3 +- src/main.tsx | 2 +- src/query.ts | 4 + src/screens/replActiveAgentModel.test.ts | 14 +++ src/screens/replActiveAgentModel.ts | 5 +- src/services/api/agentRouting.test.ts | 6 +- src/services/api/agentRouting.ts | 8 +- src/services/api/claude.lifecycle.test.ts | 63 +++++++++++++ src/services/api/claude.ts | 15 +-- src/tools/AgentTool/runAgent.routing.test.ts | 94 +++++++++++++++++++ src/tools/AgentTool/runAgent.ts | 37 +++++++- src/utils/model/model.ts | 14 +++ .../parseUserSpecifiedModel.codexTag.test.ts | 18 +++- src/utils/sessionRestore.test.ts | 23 +++++ src/utils/sessionRestore.ts | 3 +- 15 files changed, 286 insertions(+), 23 deletions(-) diff --git a/src/cli/print.ts b/src/cli/print.ts index a049ef0f3..b637e33e3 100644 --- a/src/cli/print.ts +++ b/src/cli/print.ts @@ -4610,8 +4610,7 @@ async function handleInitializeRequest( mainThreadAgent.model && mainThreadAgent.model !== 'inherit' ) { - const agentModel = parseUserSpecifiedModel(mainThreadAgent.model) - setMainLoopModelOverride(agentModel) + setMainLoopModelOverride(mainThreadAgent.model) } // SDK-defined agents arrive via init, so main.tsx's lookup missed them. diff --git a/src/main.tsx b/src/main.tsx index a7832be9b..b7511a41e 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -2070,7 +2070,7 @@ async function run(): Promise { // If user didn't specify a model but agent has one, use the agent's model let effectiveModel = userSpecifiedModel; if (!effectiveModel && mainThreadAgentDefinition?.model && mainThreadAgentDefinition.model !== 'inherit') { - effectiveModel = parseUserSpecifiedModel(mainThreadAgentDefinition.model); + effectiveModel = mainThreadAgentDefinition.model; } setMainLoopModelOverride(effectiveModel); diff --git a/src/query.ts b/src/query.ts index 1e4581765..2747550c1 100644 --- a/src/query.ts +++ b/src/query.ts @@ -97,6 +97,7 @@ import { notifyCommandLifecycle } from './utils/commandLifecycle.js' import { headlessProfilerCheckpoint } from './utils/headlessProfiler.js' import { getDefaultMainLoopModelSetting, + getProviderRequestModel, getRuntimeMainLoopModel, parseUserSpecifiedModel, renderModelName, @@ -1389,6 +1390,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/screens/replActiveAgentModel.test.ts b/src/screens/replActiveAgentModel.test.ts index ef33c5d2d..4007868ac 100644 --- a/src/screens/replActiveAgentModel.test.ts +++ b/src/screens/replActiveAgentModel.test.ts @@ -26,6 +26,20 @@ describe('getActiveSessionAgentModelSelection', () => { expect(selection.mainLoopModelForSession).toBe('agent-specific-model') }) + test('retains codexplan for provider-boundary routing', () => { + const selection = getActiveSessionAgentModelSelection({ + agent: createAgent('codexplan'), + baseMainLoopModel: 'sonnet', + hasExplicitModelOverride: false, + hasAgentManagedModel: false, + }) + + expect(selection).toEqual({ + shouldUpdateModel: true, + mainLoopModelForSession: 'codexplan', + }) + }) + test('preserves an explicit model override when selecting an agent with a model', () => { const selection = getActiveSessionAgentModelSelection({ agent: createAgent('agent-specific-model'), diff --git a/src/screens/replActiveAgentModel.ts b/src/screens/replActiveAgentModel.ts index 6a5890cd4..6a17cb750 100644 --- a/src/screens/replActiveAgentModel.ts +++ b/src/screens/replActiveAgentModel.ts @@ -2,7 +2,6 @@ import type { AgentDefinition } from '../tools/AgentTool/loadAgentsDir.js' import { getDefaultMainLoopModelSetting, type ModelSetting, - parseUserSpecifiedModel, } from '../utils/model/model.js' type ActiveSessionAgentModelSelection = @@ -33,7 +32,9 @@ export function getActiveSessionAgentModelSelection({ if (agent.model && agent.model !== 'inherit') { return { shouldUpdateModel: true, - mainLoopModelForSession: parseUserSpecifiedModel(agent.model), + // Query resolves this to the concrete runtime model, while retaining the + // selection at the provider boundary for custom-gateway defaults. + mainLoopModelForSession: agent.model, } } diff --git a/src/services/api/agentRouting.test.ts b/src/services/api/agentRouting.test.ts index 736ba4e52..cd883245b 100644 --- a/src/services/api/agentRouting.test.ts +++ b/src/services/api/agentRouting.test.ts @@ -232,7 +232,7 @@ describe('model-only routes', () => { subagentType: 'verification', settings: modelOnlySettings, }) - expect(result).toEqual({ mainLoopModel: 'gpt-5-mini' }) + expect(result).toEqual({ mainLoopModel: 'gpt-5-mini', routed: true }) expect('providerOverride' in result).toBe(false) }) @@ -316,6 +316,7 @@ describe('resolveAgentRunModelRouting', () => { expect(result).toEqual({ mainLoopModel: 'deepseek-chat', + routed: true, providerOverride: { model: 'deepseek-chat', baseURL: 'https://api.deepseek.com/v1', @@ -440,7 +441,7 @@ describe('resolveAgentRunModelRouting', () => { subagentType: 'verification', settings, }) - expect(result).toEqual({ mainLoopModel: 'gpt-5-mini' }) + expect(result).toEqual({ mainLoopModel: 'gpt-5-mini', routed: true }) }) test('permissionMode is threaded into alias resolution, not dropped', () => { @@ -783,6 +784,7 @@ describe('resolveAgentRunModelRouting: in-process teammate route identity', () = }) expect(result).toEqual({ mainLoopModel: 'deepseek-chat', + routed: true, providerOverride: { model: 'deepseek-chat', baseURL: 'https://api.deepseek.com/v1', diff --git a/src/services/api/agentRouting.ts b/src/services/api/agentRouting.ts index 32b358e36..020869293 100644 --- a/src/services/api/agentRouting.ts +++ b/src/services/api/agentRouting.ts @@ -30,6 +30,8 @@ export function isProviderOverride(route: AgentRoute): route is ProviderOverride export interface AgentRunModelRouting { mainLoopModel: string providerOverride?: ProviderOverride + /** True when an explicit agent route selected this model. */ + routed?: boolean } type AgentModelConfig = NonNullable[string] @@ -199,10 +201,11 @@ export function resolveAgentRunModelRouting({ const route = resolveAgentModelProvider(toolRequestedModel, settings) if (!route) return { mainLoopModel: resolvedAgentModel } if (isProviderOverride(route)) { - return { mainLoopModel: route.model, providerOverride: route } + return { mainLoopModel: route.model, providerOverride: route, routed: true } } return { mainLoopModel: resolveModelOnlyModel(route.model, parentModel, permissionMode), + routed: true, } } @@ -211,10 +214,11 @@ export function resolveAgentRunModelRouting({ resolveAgentModelProvider(agentDefinitionModel, settings) if (!route) return { mainLoopModel: resolvedAgentModel } if (isProviderOverride(route)) { - return { mainLoopModel: route.model, providerOverride: route } + return { mainLoopModel: route.model, providerOverride: route, routed: true } } return { mainLoopModel: resolveModelOnlyModel(route.model, parentModel, permissionMode), + routed: true, } } diff --git a/src/services/api/claude.lifecycle.test.ts b/src/services/api/claude.lifecycle.test.ts index 8f63f50d7..b231ba38e 100644 --- a/src/services/api/claude.lifecycle.test.ts +++ b/src/services/api/claude.lifecycle.test.ts @@ -141,6 +141,27 @@ 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 +358,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-000000000007', + 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) { + // Draining the stream makes the request complete before assertions. + } + + expect(requestBody?.model).toBe('gpt-5.6-sol') + expect(requestBody?.reasoning_effort).toBe('high') + }) + test('ends a failed streaming dispatch before retry backoff is reported', async () => { setClientTestEnv() process.env.OPENCLAUDE_MAX_RETRIES = '1' diff --git a/src/services/api/claude.ts b/src/services/api/claude.ts index 3a778cb65..d07b24f78 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[] @@ -1147,6 +1149,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. @@ -1873,7 +1876,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 @@ -1964,7 +1967,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, @@ -2858,9 +2861,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 }), @@ -2975,13 +2978,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/tools/AgentTool/runAgent.routing.test.ts b/src/tools/AgentTool/runAgent.routing.test.ts index 9335b5361..75238d7b8 100644 --- a/src/tools/AgentTool/runAgent.routing.test.ts +++ b/src/tools/AgentTool/runAgent.routing.test.ts @@ -114,6 +114,100 @@ describe('runAgent provider routing', () => { expect(parentContext.getAppState().mainLoopModel).toBe('parent-model') }) + test('retains an explicit codexplan selection in child state', async () => { + const parentContext = createToolUseContext('parent-model') + const stop = new Error('stop after cache-safe params') + let capturedContext: ToolUseContext | undefined + const runAgent = await importRunAgent() + + const generator = runAgent({ + agentDefinition: { ...createAgentDefinition(), model: 'codexplan' }, + promptMessages: [createUserMessage({ content: 'inspect this' })], + toolUseContext: parentContext, + canUseTool: async () => ({ behavior: 'allow' }), + isAsync: false, + querySource: 'agent:builtin:general-purpose', + availableTools: [], + onCacheSafeParams: params => { + capturedContext = params.toolUseContext + throw stop + }, + }) + + await expect(generator.next()).rejects.toBe(stop) + + expect(capturedContext?.options.mainLoopModel).toBe('gpt-5.6-sol') + expect(capturedContext?.getAppState().mainLoopModel).toBe('codexplan') + expect(capturedContext?.getAppState().mainLoopModelForSession).toBe( + 'codexplan', + ) + }) + + test('does not retain codexplan when an explicit agent route selects Sol', async () => { + settingsForTest = { + agentModels: { sol: { model: 'gpt-5.6-sol' } }, + agentRouting: { 'general-purpose': 'sol' }, + } + const stop = new Error('stop after cache-safe params') + let capturedContext: ToolUseContext | undefined + const runAgent = await importRunAgent() + + const generator = runAgent({ + agentDefinition: { ...createAgentDefinition(), model: 'codexplan' }, + promptMessages: [createUserMessage({ content: 'inspect this' })], + toolUseContext: createToolUseContext('parent-model'), + canUseTool: async () => ({ behavior: 'allow' }), + isAsync: false, + querySource: 'agent:builtin:general-purpose', + availableTools: [], + onCacheSafeParams: params => { + capturedContext = params.toolUseContext + throw stop + }, + }) + + await expect(generator.next()).rejects.toBe(stop) + + expect(capturedContext?.options.mainLoopModel).toBe('gpt-5.6-sol') + expect(capturedContext?.getAppState().mainLoopModel).toBe('gpt-5.6-sol') + }) + + test('an explicit agent route wins over CLAUDE_CODE_SUBAGENT_MODEL', async () => { + settingsForTest = { + agentModels: { sol: { model: 'gpt-5.6-sol' } }, + agentRouting: { 'general-purpose': 'sol' }, + } + const previous = process.env.CLAUDE_CODE_SUBAGENT_MODEL + process.env.CLAUDE_CODE_SUBAGENT_MODEL = 'codexplan' + try { + const stop = new Error('stop after cache-safe params') + let capturedContext: ToolUseContext | undefined + const runAgent = await importRunAgent() + const generator = runAgent({ + agentDefinition: createAgentDefinition(), + promptMessages: [createUserMessage({ content: 'inspect this' })], + toolUseContext: createToolUseContext('parent-model'), + canUseTool: async () => ({ behavior: 'allow' }), + isAsync: false, + querySource: 'agent:builtin:general-purpose', + availableTools: [], + onCacheSafeParams: params => { + capturedContext = params.toolUseContext + throw stop + }, + }) + + await expect(generator.next()).rejects.toBe(stop) + expect(capturedContext?.getAppState().mainLoopModel).toBe('gpt-5.6-sol') + } finally { + if (previous === undefined) { + delete process.env.CLAUDE_CODE_SUBAGENT_MODEL + } else { + process.env.CLAUDE_CODE_SUBAGENT_MODEL = previous + } + } + }) + test('rejects disallowed routed models before building child context', async () => { settingsForTest = { ...routedSettings, diff --git a/src/tools/AgentTool/runAgent.ts b/src/tools/AgentTool/runAgent.ts index 341840cb9..324621e0b 100644 --- a/src/tools/AgentTool/runAgent.ts +++ b/src/tools/AgentTool/runAgent.ts @@ -58,6 +58,7 @@ import { clearSessionHooks } from '../../utils/hooks/sessionHooks.js' import { executeSubagentStartHooks } from '../../utils/hooks.js' import { createUserMessage } from '../../utils/messages.js' import { getAgentModel } from '../../utils/model/agent.js' +import { getProviderRequestModel } from '../../utils/model/model.js' import { isModelAllowed } from '../../utils/model/modelAllowlist.js' import { resolveAgentRunModelRouting, shouldEnforceModelAllowlist } from '../../services/api/agentRouting.js' import { getInitialSettings } from '../../utils/settings/settings.js' @@ -356,7 +357,11 @@ export async function* runAgent({ // Resolve per-agent provider routing from settings const settings = getInitialSettings() - const { mainLoopModel: effectiveModel, providerOverride } = + const { + mainLoopModel: effectiveModel, + providerOverride, + routed: isExplicitlyRouted, + } = resolveAgentRunModelRouting({ resolvedAgentModel, parentModel: toolUseContext.options.mainLoopModel, @@ -368,6 +373,28 @@ export async function* runAgent({ permissionMode, }) + // Keep the concrete model in the tool context, but preserve a codexplan + // selection in agent state when it still resolves to that concrete model. + // Query uses the state value only at the provider boundary, where custom + // gateways distinguish the legacy alias from an explicit GPT-5.6 Sol model. + const requestedToolModel = model?.trim() + const requestedSubagentModel = process.env.CLAUDE_CODE_SUBAGENT_MODEL?.trim() + const rawAgentSelection = + isExplicitlyRouted + ? effectiveModel + : requestedSubagentModel || + (requestedToolModel && requestedToolModel.toLowerCase() !== 'inherit' + ? requestedToolModel + : agentDefinition.model && agentDefinition.model !== 'inherit' + ? agentDefinition.model + : appState.mainLoopModelForSession ?? + appState.mainLoopModel ?? + effectiveModel) + const agentProviderRequestModel = getProviderRequestModel( + rawAgentSelection, + effectiveModel, + ) + if ( shouldEnforceModelAllowlist( resolvedAgentModel, @@ -518,8 +545,8 @@ export async function* runAgent({ : state.effortValue const modelStateChanged = - state.mainLoopModel !== effectiveModel || - state.mainLoopModelForSession !== effectiveModel + state.mainLoopModel !== agentProviderRequestModel || + state.mainLoopModelForSession !== agentProviderRequestModel if ( toolPermissionContext === state.toolPermissionContext && @@ -530,8 +557,8 @@ export async function* runAgent({ } return { ...state, - mainLoopModel: effectiveModel, - mainLoopModelForSession: effectiveModel, + mainLoopModel: agentProviderRequestModel, + mainLoopModelForSession: agentProviderRequestModel, toolPermissionContext, effortValue, } diff --git a/src/utils/model/model.ts b/src/utils/model/model.ts index b09710053..811fb65c8 100644 --- a/src/utils/model/model.ts +++ b/src/utils/model/model.ts @@ -896,6 +896,20 @@ export function parseUserSpecifiedModel( return modelInputTrimmed } +// Keep the concrete runtime model for context, display, and capability lookup, +// but retain codexplan at the provider boundary: custom gateways deliberately +// give that legacy alias a different default effort than explicit GPT-5.6 Sol. +export function getProviderRequestModel( + selectedModel: string, + runtimeModel: string, +): string { + const selected = selectedModel.trim() + const base = selected.replace(/\[1m]$/i, '').split('?', 1)[0]?.toLowerCase() + return base === 'codexplan' && parseUserSpecifiedModel(selected) === runtimeModel + ? 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 e3b812c26..ad3fff508 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, renderModelSetting } 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 @@ -48,6 +52,18 @@ describe('parseUserSpecifiedModel — codex alias 1M tag', () => { 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', + ) + }) }) // Bare gpt-5.6 resolves to the flagship tier (Sol) at parse time — not just in diff --git a/src/utils/sessionRestore.test.ts b/src/utils/sessionRestore.test.ts index c2ed950a4..370a17831 100644 --- a/src/utils/sessionRestore.test.ts +++ b/src/utils/sessionRestore.test.ts @@ -5,9 +5,11 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { + getMainLoopModelOverride, getSessionId, getSessionProjectDir, isSessionPersistenceDisabled, + setMainLoopModelOverride, setSessionPersistenceDisabled, switchSession, } from '../bootstrap/state.js' @@ -29,6 +31,7 @@ import { } from './sessionStorage.ts' import { processResumedConversation, + restoreAgentFromSession, type ProcessedResume, } from './sessionRestore.js' import type { ContentReplacementRecord } from './toolResultStorage.js' @@ -45,6 +48,7 @@ let originalSkipPromptHistory: string | undefined let originalSessionId: string let originalSessionProjectDir: string | null let originalPersistenceDisabled: boolean +let originalMainLoopModelOverride: ReturnType function id(n: number): UUID { return `00000000-0000-4000-8000-${String(n).padStart(12, '0')}` as UUID @@ -251,6 +255,7 @@ beforeEach(async () => { originalSessionId = getSessionId() originalSessionProjectDir = getSessionProjectDir() originalPersistenceDisabled = isSessionPersistenceDisabled() + originalMainLoopModelOverride = getMainLoopModelOverride() process.env.NODE_ENV = 'test' process.env.TEST_ENABLE_SESSION_PERSISTENCE = 'true' @@ -289,6 +294,7 @@ afterEach(async () => { originalSkipPromptHistory } setSessionPersistenceDisabled(originalPersistenceDisabled) + setMainLoopModelOverride(originalMainLoopModelOverride) switchSession(originalSessionId as never, originalSessionProjectDir) resetProjectForTesting() releaseSharedMutationLock() @@ -422,3 +428,20 @@ describe('forked session resume', () => { expect(forkInfoMessage(result.messages)).toBeUndefined() }) }) + +describe('restoreAgentFromSession', () => { + test('retains codexplan as the provider-boundary selection', () => { + setMainLoopModelOverride(undefined) + const agent = { + agentType: 'planner', + model: 'codexplan', + } as unknown as import('../tools/AgentTool/loadAgentsDir.js').AgentDefinition + + restoreAgentFromSession('planner', undefined, { + activeAgents: [agent], + allAgents: [agent], + }) + + expect(getMainLoopModelOverride()).toBe('codexplan') + }) +}) diff --git a/src/utils/sessionRestore.ts b/src/utils/sessionRestore.ts index 6bfcd9d43..8afc131bd 100644 --- a/src/utils/sessionRestore.ts +++ b/src/utils/sessionRestore.ts @@ -41,7 +41,6 @@ import { logForDebugging } from './debug.js' import type { FileHistorySnapshot } from './fileHistory.js' import { fileHistoryRestoreStateFromLog } from './fileHistory.js' import { createSystemMessage } from './messages.js' -import { parseUserSpecifiedModel } from './model/model.js' import { getPlansDirectory } from './plans.js' import { setCwd } from './Shell.js' import { @@ -244,7 +243,7 @@ export function restoreAgentFromSession( resumedAgent.model && resumedAgent.model !== 'inherit' ) { - setMainLoopModelOverride(parseUserSpecifiedModel(resumedAgent.model)) + setMainLoopModelOverride(resumedAgent.model) } return { agentDefinition: resumedAgent, agentType: resumedAgent.agentType }