diff --git a/src/services/api/claude.ts b/src/services/api/claude.ts index cd887963e..72f664244 100644 --- a/src/services/api/claude.ts +++ b/src/services/api/claude.ts @@ -181,7 +181,7 @@ import { calculateUSDCost } from 'src/utils/modelCost.js' import { endQueryProfile, queryCheckpoint } from 'src/utils/queryProfiler.js' import { modelSupportsAdaptiveThinking, - modelSupportsThinking, + shouldUseThinkingForModel, type ThinkingConfig, } from 'src/utils/thinking.js' import { @@ -1604,18 +1604,16 @@ async function* queryModel( options.maxOutputTokensOverride || getMaxOutputTokensForModel(options.model) - const hasThinking = - thinkingConfig.type !== 'disabled' && - !isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_THINKING) + const hasThinking = shouldUseThinkingForModel(retryContext.model, thinkingConfig) let thinking: BetaMessageStreamParams['thinking'] | undefined = undefined // IMPORTANT: Do not change the adaptive-vs-budget thinking selection below // without notifying the model launch DRI and research. This is a sensitive // setting that can greatly affect model quality and bashing. - if (hasThinking && modelSupportsThinking(options.model)) { + if (hasThinking) { if ( !isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING) && - modelSupportsAdaptiveThinking(options.model) + modelSupportsAdaptiveThinking(retryContext.model) ) { // For models that support adaptive thinking, always use adaptive // thinking without a budget. @@ -1625,7 +1623,7 @@ async function* queryModel( } else { // For models that do not support adaptive thinking, use the default // thinking budget unless explicitly specified. - let thinkingBudget = getMaxThinkingTokensForModel(options.model) + let thinkingBudget = getMaxThinkingTokensForModel(retryContext.model) if ( thinkingConfig.type === 'enabled' && thinkingConfig.budgetTokens !== undefined diff --git a/src/utils/thinking.test.ts b/src/utils/thinking.test.ts index c8575550e..12a29d0db 100644 --- a/src/utils/thinking.test.ts +++ b/src/utils/thinking.test.ts @@ -25,6 +25,7 @@ const ENV_KEYS = [ 'ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES', 'ANTHROPIC_DEFAULT_HAIKU_MODEL', 'ANTHROPIC_DEFAULT_HAIKU_MODEL_SUPPORTED_CAPABILITIES', + 'CLAUDE_CODE_DISABLE_THINKING', 'USER_TYPE', ] @@ -57,9 +58,13 @@ afterEach(() => { async function importFreshThinkingModule() { mock.restore() - mock.module('./model/providers.js', () => ({ - getAPIProvider: () => 'openai', - })) + const originalProviders = await import('./model/providers.js') + mock.module('./model/providers.js', () => { + return { + ...originalProviders, + getAPIProvider: () => 'openai', + } + }) const nonce = `${Date.now()}-${Math.random()}` return import(`./thinking.js?ts=${nonce}`) } @@ -109,3 +114,16 @@ describe('modelSupportsThinking — Z.AI GLM', () => { expect(modelSupportsThinking('GLM-5.1')).toBe(true) }) }) + +describe('shouldUseThinkingForModel — Ollama', () => { + test('does not use thinking for Ollama models when app-level thinking is enabled', async () => { + process.env.CLAUDE_CODE_USE_OPENAI = '1' + process.env.OPENAI_BASE_URL = 'http://localhost:11434/v1' + const { shouldUseThinkingForModel } = await importFreshThinkingModule() + const enabledThinking = { type: 'enabled' as const, budgetTokens: 1024 } + + expect(shouldUseThinkingForModel('llama3.1:8b', enabledThinking)).toBe(false) + // Covers catalog-missing local names that would otherwise match Claude 4 heuristics. + expect(shouldUseThinkingForModel('claude-sonnet-4-local', enabledThinking)).toBe(false) + }) +}) diff --git a/src/utils/thinking.ts b/src/utils/thinking.ts index a4c2aa746..73317007f 100644 --- a/src/utils/thinking.ts +++ b/src/utils/thinking.ts @@ -12,6 +12,7 @@ import { resolveAntModel } from './model/antModels.js' import { get3PModelCapabilityOverride } from './model/modelSupportOverrides.js' import { getAPIProvider } from './model/providers.js' import { getSettingsWithErrors } from './settings/settings.js' +import { isEnvTruthy } from './envUtils.js' export type ThinkingConfig = | { type: 'adaptive' } @@ -143,6 +144,10 @@ export function modelSupportsThinking(model: string): boolean { if (descriptorSupportsThinking !== undefined) { return descriptorSupportsThinking } + const routeId = resolveActiveRouteIdFromEnv(process.env) + if (routeId === 'ollama') { + return false + } } // 3P (Bedrock/Vertex): only Opus 4+ and Sonnet 4+ return canonical.includes('sonnet-4') || canonical.includes('opus-4') @@ -199,3 +204,14 @@ export function shouldEnableThinkingByDefault(): boolean { // Enable thinking by default unless explicitly disabled. return true } + +export function shouldUseThinkingForModel( + model: string, + thinkingConfig: ThinkingConfig, +): boolean { + return ( + thinkingConfig.type !== 'disabled' && + !isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_THINKING) && + modelSupportsThinking(model) + ) +}