From 606cb66cfd104205b06ea48534b1b5461d952630 Mon Sep 17 00:00:00 2001 From: chioarub Date: Tue, 26 May 2026 17:39:37 +0300 Subject: [PATCH 1/2] fix(thinking): disable thinking for unsupported Ollama models Fixes #1371 - Adds central `shouldUseThinkingForModel` gate that checks the actual route and model descriptor. - Disables thinking parameters for the Ollama route when the model is unknown or unsupported. - Updates API requests to evaluate the actual retry model against the capability gate instead of the initial request model. - Adds targeted tests for Ollama logic and shim payloads. --- src/services/api/claude.ts | 12 +++++------ src/services/api/openaiShim.test.ts | 33 +++++++++++++++++++++++++++++ src/utils/thinking.test.ts | 20 ++++++++++++++--- src/utils/thinking.ts | 16 ++++++++++++++ 4 files changed, 71 insertions(+), 10 deletions(-) diff --git a/src/services/api/claude.ts b/src/services/api/claude.ts index cd887963e9..72f6642449 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/services/api/openaiShim.test.ts b/src/services/api/openaiShim.test.ts index 19e4a96d73..6e4f4032fc 100644 --- a/src/services/api/openaiShim.test.ts +++ b/src/services/api/openaiShim.test.ts @@ -4952,6 +4952,39 @@ test('Local provider (vLLM/Ollama/etc.): strips unsupported store on chat_comple expect(requestBody?.store).toBeUndefined() }) +test('Ollama: does not send thinking when model does not support it', async () => { + process.env.OPENAI_BASE_URL = 'http://localhost:11434/v1' + process.env.OPENAI_API_KEY = 'ollama' + + let requestBody: Record | undefined + globalThis.fetch = (async (_input, init) => { + requestBody = JSON.parse(String(init?.body)) + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + model: 'llama3.1:8b', + choices: [ + { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, + ], + usage: { prompt_tokens: 3, completion_tokens: 1, total_tokens: 4 }, + }), + { headers: { 'Content-Type': 'application/json' } }, + ) + }) as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + await client.beta.messages.create({ + model: 'llama3.1:8b', + system: 'you are local', + messages: [{ role: 'user', content: 'hi' }], + max_tokens: 64, + thinking: { type: 'enabled', budget_tokens: 1024 }, + stream: false, + }) + + expect(requestBody?.thinking).toBeUndefined() +}) + test('Groq: keeps max_completion_tokens and strips unsupported store', async () => { process.env.OPENAI_BASE_URL = 'https://api.groq.com/openai/v1' process.env.OPENAI_API_KEY = 'gsk-test' diff --git a/src/utils/thinking.test.ts b/src/utils/thinking.test.ts index c8575550e8..29cab4f6bc 100644 --- a/src/utils/thinking.test.ts +++ b/src/utils/thinking.test.ts @@ -57,9 +57,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 +113,13 @@ describe('modelSupportsThinking — Z.AI GLM', () => { expect(modelSupportsThinking('GLM-5.1')).toBe(true) }) }) + +describe('modelSupportsThinking — Ollama', () => { + test('does not enable thinking for unknown Ollama models', async () => { + process.env.CLAUDE_CODE_USE_OPENAI = '1' + process.env.OPENAI_BASE_URL = 'http://localhost:11434/v1' + const { modelSupportsThinking } = await importFreshThinkingModule() + + expect(modelSupportsThinking('llama3.1:8b')).toBe(false) + }) +}) diff --git a/src/utils/thinking.ts b/src/utils/thinking.ts index a4c2aa746d..73317007fe 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) + ) +} From 0732cc588af6b4e6146a91c6ea448a507fba901e Mon Sep 17 00:00:00 2001 From: chioarub Date: Tue, 26 May 2026 20:23:28 +0300 Subject: [PATCH 2/2] test(thinking): cover Ollama thinking gate --- src/services/api/openaiShim.test.ts | 33 ----------------------------- src/utils/thinking.test.ts | 12 +++++++---- 2 files changed, 8 insertions(+), 37 deletions(-) diff --git a/src/services/api/openaiShim.test.ts b/src/services/api/openaiShim.test.ts index 6e4f4032fc..19e4a96d73 100644 --- a/src/services/api/openaiShim.test.ts +++ b/src/services/api/openaiShim.test.ts @@ -4952,39 +4952,6 @@ test('Local provider (vLLM/Ollama/etc.): strips unsupported store on chat_comple expect(requestBody?.store).toBeUndefined() }) -test('Ollama: does not send thinking when model does not support it', async () => { - process.env.OPENAI_BASE_URL = 'http://localhost:11434/v1' - process.env.OPENAI_API_KEY = 'ollama' - - let requestBody: Record | undefined - globalThis.fetch = (async (_input, init) => { - requestBody = JSON.parse(String(init?.body)) - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - model: 'llama3.1:8b', - choices: [ - { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, - ], - usage: { prompt_tokens: 3, completion_tokens: 1, total_tokens: 4 }, - }), - { headers: { 'Content-Type': 'application/json' } }, - ) - }) as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - await client.beta.messages.create({ - model: 'llama3.1:8b', - system: 'you are local', - messages: [{ role: 'user', content: 'hi' }], - max_tokens: 64, - thinking: { type: 'enabled', budget_tokens: 1024 }, - stream: false, - }) - - expect(requestBody?.thinking).toBeUndefined() -}) - test('Groq: keeps max_completion_tokens and strips unsupported store', async () => { process.env.OPENAI_BASE_URL = 'https://api.groq.com/openai/v1' process.env.OPENAI_API_KEY = 'gsk-test' diff --git a/src/utils/thinking.test.ts b/src/utils/thinking.test.ts index 29cab4f6bc..12a29d0db3 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', ] @@ -114,12 +115,15 @@ describe('modelSupportsThinking — Z.AI GLM', () => { }) }) -describe('modelSupportsThinking — Ollama', () => { - test('does not enable thinking for unknown Ollama models', async () => { +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 { modelSupportsThinking } = await importFreshThinkingModule() + const { shouldUseThinkingForModel } = await importFreshThinkingModule() + const enabledThinking = { type: 'enabled' as const, budgetTokens: 1024 } - expect(modelSupportsThinking('llama3.1:8b')).toBe(false) + 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) }) })