Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 5 additions & 7 deletions src/services/api/claude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down
33 changes: 33 additions & 0 deletions src/services/api/openaiShim.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown> | 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'
Expand Down
20 changes: 17 additions & 3 deletions src/utils/thinking.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`)
}
Expand Down Expand Up @@ -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)
})
})
16 changes: 16 additions & 0 deletions src/utils/thinking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' }
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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)
)
}