diff --git a/src/commands/effort/effort.tsx b/src/commands/effort/effort.tsx
index ebc2373510..57c0d17e23 100644
--- a/src/commands/effort/effort.tsx
+++ b/src/commands/effort/effort.tsx
@@ -13,6 +13,7 @@ import {
getEffortEnvOverride,
getEffortValueDescription,
isEffortLevel,
+ modelSupportsEffort,
toPersistableEffort,
} from '../../utils/effort.js';
import { updateSettingsForSource } from '../../utils/settings/settings.js';
@@ -130,14 +131,12 @@ function ShowCurrentEffort({ onDone }: { onDone: (result: string) => void }): Re
return null;
}
-function ApplyEffortAndClose({
- result,
- onDone,
-}: {
- result: EffortCommandResult;
- onDone: (result: string) => void;
-}): React.ReactNode {
+function ApplyEffortAndClose({ args, onDone }: { args: string; onDone: (result: string) => void }): React.ReactNode {
const setAppState = useSetAppState();
+ const model = useMainLoopModel();
+ const result = modelSupportsEffort(model)
+ ? executeEffort(args)
+ : { message: 'Effort is disabled because thinking is disabled for the current model.' };
const { effortUpdate, message } = result;
React.useEffect(() => {
if (effortUpdate) {
@@ -169,11 +168,20 @@ export async function call(onDone: LocalJSXCommandOnDone, _context: unknown, arg
return ;
}
- const result = executeEffort(args);
- return ;
+ return ;
}
function EffortPanelWrapper({ onDone }: { onDone: (result: string) => void }): React.ReactNode {
const effortValue = useAppState(s => s.effortValue);
+ const model = useMainLoopModel();
+ const supported = modelSupportsEffort(model);
+ React.useEffect(() => {
+ if (!supported) {
+ onDone('Effort is disabled because thinking is disabled for the current model.');
+ }
+ }, [supported, onDone]);
+ if (!supported) {
+ return null;
+ }
return ;
}
diff --git a/src/components/ConsoleOAuthFlow.tsx b/src/components/ConsoleOAuthFlow.tsx
index 8f4219ca30..a34ae990fe 100644
--- a/src/components/ConsoleOAuthFlow.tsx
+++ b/src/components/ConsoleOAuthFlow.tsx
@@ -1,4 +1,4 @@
-import React, { useCallback, useEffect, useRef, useState } from 'react';
+import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
logEvent,
@@ -52,7 +52,8 @@ type OAuthStatus =
haikuModel: string;
sonnetModel: string;
opusModel: string;
- activeField: 'base_url' | 'api_key' | 'haiku_model' | 'sonnet_model' | 'opus_model';
+ thinkingEnabled: boolean;
+ activeField: 'base_url' | 'api_key' | 'haiku_model' | 'sonnet_model' | 'opus_model' | 'thinking_enabled';
} // OpenAI Chat Completions API platform
| {
state: 'chatgpt_subscription';
@@ -84,6 +85,10 @@ type OAuthStatus =
};
const PASTE_HERE_MSG = 'Paste code here if prompted > ';
+
+export function getOpenAIThinkingEnabled(value: string | undefined): boolean {
+ return !['0', 'false', 'no', 'off'].includes(value?.toLowerCase().trim() ?? '');
+}
export function ConsoleOAuthFlow({
onDone,
startingMessage,
@@ -550,6 +555,7 @@ function OAuthStatusMessage({
haikuModel: process.env.OPENAI_DEFAULT_HAIKU_MODEL ?? '',
sonnetModel: process.env.OPENAI_DEFAULT_SONNET_MODEL ?? '',
opusModel: process.env.OPENAI_DEFAULT_OPUS_MODEL ?? '',
+ thinkingEnabled: getOpenAIThinkingEnabled(process.env.OPENAI_ENABLE_THINKING),
activeField: 'base_url',
});
} else if (value === 'china_providers') {
@@ -799,8 +805,15 @@ function OAuthStatusMessage({
}
case 'openai_chat_api': {
- type OpenAIField = 'base_url' | 'api_key' | 'haiku_model' | 'sonnet_model' | 'opus_model';
- const OPENAI_FIELDS: OpenAIField[] = ['base_url', 'api_key', 'haiku_model', 'sonnet_model', 'opus_model'];
+ type OpenAIField = 'base_url' | 'api_key' | 'haiku_model' | 'sonnet_model' | 'opus_model' | 'thinking_enabled';
+ const OPENAI_FIELDS: OpenAIField[] = [
+ 'base_url',
+ 'api_key',
+ 'haiku_model',
+ 'sonnet_model',
+ 'opus_model',
+ 'thinking_enabled',
+ ];
const op = oauthStatus as {
state: 'openai_chat_api';
activeField: OpenAIField;
@@ -809,19 +822,25 @@ function OAuthStatusMessage({
haikuModel: string;
sonnetModel: string;
opusModel: string;
+ thinkingEnabled: boolean;
};
- const { activeField, baseUrl, apiKey, haikuModel, sonnetModel, opusModel } = op;
- const openaiDisplayValues: Record = {
- base_url: baseUrl,
- api_key: apiKey,
- haiku_model: haikuModel,
- sonnet_model: sonnetModel,
- opus_model: opusModel,
- };
+ const { activeField, baseUrl, apiKey, haikuModel, sonnetModel, opusModel, thinkingEnabled } = op;
+ const openaiDisplayValues = useMemo, string>>(
+ () => ({
+ base_url: baseUrl,
+ api_key: apiKey,
+ haiku_model: haikuModel,
+ sonnet_model: sonnetModel,
+ opus_model: opusModel,
+ }),
+ [baseUrl, apiKey, haikuModel, sonnetModel, opusModel],
+ );
+ const getOpenAIFieldValue = (field: OpenAIField): string =>
+ field === 'thinking_enabled' ? '' : openaiDisplayValues[field];
- const [openaiInputValue, setOpenaiInputValue] = useState(() => openaiDisplayValues[activeField]);
+ const [openaiInputValue, setOpenaiInputValue] = useState(() => getOpenAIFieldValue(activeField));
const [openaiInputCursorOffset, setOpenaiInputCursorOffset] = useState(
- () => openaiDisplayValues[activeField].length,
+ () => getOpenAIFieldValue(activeField).length,
);
const buildOpenAIState = useCallback(
@@ -834,6 +853,7 @@ function OAuthStatusMessage({
haikuModel,
sonnetModel,
opusModel,
+ thinkingEnabled,
};
switch (field) {
case 'base_url':
@@ -846,15 +866,21 @@ function OAuthStatusMessage({
return { ...s, sonnetModel: value };
case 'opus_model':
return { ...s, opusModel: value };
+ case 'thinking_enabled':
+ return s;
}
},
- [activeField, baseUrl, apiKey, haikuModel, sonnetModel, opusModel],
+ [activeField, baseUrl, apiKey, haikuModel, sonnetModel, opusModel, thinkingEnabled],
);
const doOpenAISave = useCallback(() => {
- const finalVals = { ...openaiDisplayValues, [activeField]: openaiInputValue };
+ const finalVals =
+ activeField === 'thinking_enabled'
+ ? openaiDisplayValues
+ : { ...openaiDisplayValues, [activeField]: openaiInputValue };
const env: Record = {
OPENAI_AUTH_MODE: undefined,
+ OPENAI_ENABLE_THINKING: String(thinkingEnabled),
};
// Validate base_url if provided
@@ -872,6 +898,7 @@ function OAuthStatusMessage({
haikuModel: '',
sonnetModel: '',
opusModel: '',
+ thinkingEnabled,
activeField: 'base_url',
},
});
@@ -900,6 +927,7 @@ function OAuthStatusMessage({
haikuModel: finalVals.haiku_model ?? '',
sonnetModel: finalVals.sonnet_model ?? '',
opusModel: finalVals.opus_model ?? '',
+ thinkingEnabled,
activeField: 'base_url',
},
});
@@ -919,7 +947,7 @@ function OAuthStatusMessage({
setOAuthStatus({ state: 'success' });
void onDone();
}
- }, [activeField, openaiInputValue, openaiDisplayValues, setOAuthStatus, onDone]);
+ }, [activeField, openaiInputValue, openaiDisplayValues, thinkingEnabled, setOAuthStatus, onDone]);
const handleOpenAIEnter = useCallback(() => {
const idx = OPENAI_FIELDS.indexOf(activeField);
@@ -929,8 +957,8 @@ function OAuthStatusMessage({
} else {
const next = OPENAI_FIELDS[idx + 1]!;
setOAuthStatus(buildOpenAIState(activeField, openaiInputValue, next));
- setOpenaiInputValue(openaiDisplayValues[next] ?? '');
- setOpenaiInputCursorOffset((openaiDisplayValues[next] ?? '').length);
+ setOpenaiInputValue(getOpenAIFieldValue(next));
+ setOpenaiInputCursorOffset(getOpenAIFieldValue(next).length);
}
}, [activeField, openaiInputValue, buildOpenAIState, doOpenAISave, openaiDisplayValues, setOAuthStatus]);
@@ -939,9 +967,10 @@ function OAuthStatusMessage({
() => {
const idx = OPENAI_FIELDS.indexOf(activeField);
if (idx < OPENAI_FIELDS.length - 1) {
- setOAuthStatus(buildOpenAIState(activeField, openaiInputValue, OPENAI_FIELDS[idx + 1]));
- setOpenaiInputValue(openaiDisplayValues[OPENAI_FIELDS[idx + 1]!] ?? '');
- setOpenaiInputCursorOffset((openaiDisplayValues[OPENAI_FIELDS[idx + 1]!] ?? '').length);
+ const next = OPENAI_FIELDS[idx + 1]!;
+ setOAuthStatus(buildOpenAIState(activeField, openaiInputValue, next));
+ setOpenaiInputValue(getOpenAIFieldValue(next));
+ setOpenaiInputCursorOffset(getOpenAIFieldValue(next).length);
}
},
{ context: 'FormField' },
@@ -952,12 +981,24 @@ function OAuthStatusMessage({
const idx = OPENAI_FIELDS.indexOf(activeField);
if (idx > 0) {
setOAuthStatus(buildOpenAIState(activeField, openaiInputValue, OPENAI_FIELDS[idx - 1]));
- setOpenaiInputValue(openaiDisplayValues[OPENAI_FIELDS[idx - 1]!] ?? '');
- setOpenaiInputCursorOffset((openaiDisplayValues[OPENAI_FIELDS[idx - 1]!] ?? '').length);
+ const previous = OPENAI_FIELDS[idx - 1]!;
+ setOpenaiInputValue(getOpenAIFieldValue(previous));
+ setOpenaiInputCursorOffset(getOpenAIFieldValue(previous).length);
}
},
{ context: 'FormField' },
);
+ useKeybinding(
+ 'confirm:toggle',
+ () => {
+ setOAuthStatus({ ...op, thinkingEnabled: !thinkingEnabled });
+ },
+ { context: 'Confirmation', isActive: activeField === 'thinking_enabled' },
+ );
+ useKeybinding('confirm:yes', doOpenAISave, {
+ context: 'Confirmation',
+ isActive: activeField === 'thinking_enabled',
+ });
useKeybinding(
'confirm:no',
() => {
@@ -968,7 +1009,11 @@ function OAuthStatusMessage({
const openaiColumns = useTerminalSize().columns - 20;
- const renderOpenAIRow = (field: OpenAIField, label: string, opts?: { mask?: boolean }) => {
+ const renderOpenAIRow = (
+ field: Exclude,
+ label: string,
+ opts?: { mask?: boolean },
+ ) => {
const active = activeField === field;
const val = openaiDisplayValues[field];
return (
@@ -1007,8 +1052,18 @@ function OAuthStatusMessage({
{renderOpenAIRow('haiku_model', 'Haiku ')}
{renderOpenAIRow('sonnet_model', 'Sonnet ')}
{renderOpenAIRow('opus_model', 'Opus ')}
+
+
+ {' Thinking/effort '}
+
+
+ {String(thinkingEnabled)}
+
- ↑↓/Tab to switch · Enter on last field to save · Esc to go back
+ ↑↓/Tab to switch · Space to toggle · Enter to save · Esc to go back
);
}
diff --git a/src/services/api/openai/__tests__/thinking.test.ts b/src/services/api/openai/__tests__/thinking.test.ts
index 9ff9d92e38..97615b6e56 100644
--- a/src/services/api/openai/__tests__/thinking.test.ts
+++ b/src/services/api/openai/__tests__/thinking.test.ts
@@ -43,29 +43,11 @@ describe('isOpenAIThinkingEnabled', () => {
})
describe('OPENAI_ENABLE_THINKING env var', () => {
- test('returns true when OPENAI_ENABLE_THINKING=1', () => {
- process.env.OPENAI_ENABLE_THINKING = '1'
- expect(isOpenAIThinkingEnabled('gpt-4o')).toBe(true)
- })
-
- test('returns true when OPENAI_ENABLE_THINKING=true', () => {
- process.env.OPENAI_ENABLE_THINKING = 'true'
- expect(isOpenAIThinkingEnabled('gpt-4o')).toBe(true)
- })
-
- test('returns true when OPENAI_ENABLE_THINKING=yes', () => {
- process.env.OPENAI_ENABLE_THINKING = 'yes'
- expect(isOpenAIThinkingEnabled('gpt-4o')).toBe(true)
- })
-
- test('returns true when OPENAI_ENABLE_THINKING=on', () => {
- process.env.OPENAI_ENABLE_THINKING = 'on'
- expect(isOpenAIThinkingEnabled('gpt-4o')).toBe(true)
- })
-
- test('returns true when OPENAI_ENABLE_THINKING=TRUE (case insensitive)', () => {
- process.env.OPENAI_ENABLE_THINKING = 'TRUE'
- expect(isOpenAIThinkingEnabled('gpt-4o')).toBe(true)
+ test('does not send provider-specific thinking fields to generic models', () => {
+ for (const value of ['1', 'true', 'yes', 'on', 'TRUE']) {
+ process.env.OPENAI_ENABLE_THINKING = value
+ expect(isOpenAIThinkingEnabled('gpt-4o')).toBe(false)
+ }
})
test('returns false when OPENAI_ENABLE_THINKING=0', () => {
@@ -173,11 +155,11 @@ describe('isOpenAIThinkingEnabled', () => {
})
describe('priority and combined detection', () => {
- test('OPENAI_ENABLE_THINKING=1 enables thinking for any model', () => {
+ test('OPENAI_ENABLE_THINKING=1 only enables provider-specific fields for detected models', () => {
process.env.OPENAI_ENABLE_THINKING = '1'
- expect(isOpenAIThinkingEnabled('gpt-4o')).toBe(true)
+ expect(isOpenAIThinkingEnabled('gpt-4o')).toBe(false)
expect(isOpenAIThinkingEnabled('deepseek-v3')).toBe(true)
- expect(isOpenAIThinkingEnabled('qwen-3')).toBe(true)
+ expect(isOpenAIThinkingEnabled('qwen-3')).toBe(false)
})
test('OPENAI_ENABLE_THINKING=false disables thinking even for deepseek-reasoner', () => {
@@ -221,8 +203,14 @@ describe('buildOpenAIRequestBody — thinking params', () => {
expect(body.prompt_cache_key).toBe('ccb:test-session')
})
- test('includes vLLM/self-hosted thinking format when enabled', () => {
- const body = buildOpenAIRequestBody({ ...baseParams, enableThinking: true })
+ test('sends reasoning effort with the DeepSeek thinking body', () => {
+ const body = buildOpenAIRequestBody({
+ ...baseParams,
+ enableThinking: true,
+ reasoningEffort: 'high',
+ }) as Record
+ expect(body.reasoning_effort).toBe('high')
+ expect(body.thinking).toEqual({ type: 'enabled' })
expect(body.enable_thinking).toBe(true)
expect(body.chat_template_kwargs).toEqual({
thinking: true,
@@ -230,21 +218,14 @@ describe('buildOpenAIRequestBody — thinking params', () => {
})
})
- test('includes both formats simultaneously when enabled', () => {
- const body = buildOpenAIRequestBody({ ...baseParams, enableThinking: true })
- expect(body.thinking).toEqual({ type: 'enabled' })
- expect(body.enable_thinking).toBe(true)
- expect(body.chat_template_kwargs!.thinking).toBe(true)
- })
-
test('does NOT include thinking params when disabled', () => {
const body = buildOpenAIRequestBody({
...baseParams,
enableThinking: false,
- })
+ }) as Record
expect(body.thinking).toBeUndefined()
- expect(body.enable_thinking).toBeUndefined()
- expect(body.chat_template_kwargs).toBeUndefined()
+ expect('enable_thinking' in body).toBe(false)
+ expect('chat_template_kwargs' in body).toBe(false)
})
test('always includes stream and stream_options', () => {
diff --git a/src/services/api/openai/index.ts b/src/services/api/openai/index.ts
index c201f12992..035fe001ab 100644
--- a/src/services/api/openai/index.ts
+++ b/src/services/api/openai/index.ts
@@ -56,6 +56,13 @@ export {
}
import { getModelMaxOutputTokens } from '../../../utils/context.js'
import type { Options } from '../claude.js'
+import {
+ convertEffortValueToLevel,
+ getEffortEnvOverride,
+ modelSupportsEffort,
+ modelSupportsMaxEffort,
+ modelSupportsXhighEffort,
+} from '../../../utils/effort.js'
import { randomUUID } from 'crypto'
import {
createAssistantAPIErrorMessage,
@@ -97,6 +104,25 @@ function getChatGPTResponsesReasoningEffort(
)
}
+function getChatCompletionsReasoningEffort(
+ model: string,
+ effortValue: Options['effortValue'],
+): ResponsesReasoningEffort | undefined {
+ if (!modelSupportsEffort(model)) return undefined
+ const envOverride = getEffortEnvOverride()
+ if (envOverride === null) return undefined
+ const applied = envOverride ?? effortValue
+ if (applied === undefined) return undefined
+ const level = convertEffortValueToLevel(applied)
+ if (level === 'max' && !modelSupportsMaxEffort(model)) {
+ return modelSupportsXhighEffort(model) ? 'xhigh' : 'high'
+ }
+ if (level === 'xhigh' && !modelSupportsXhighEffort(model)) {
+ return 'high'
+ }
+ return level
+}
+
/**
* Mirrors the Anthropic request path's deferred-tool announcement for OpenAI.
*
@@ -288,7 +314,8 @@ export async function* queryModelOpenAI(
)
// 8. Convert messages and tools to OpenAI format
- const enableThinking = isOpenAIThinkingEnabled(openaiModel)
+ const enableThinking =
+ modelSupportsEffort(openaiModel) && isOpenAIThinkingEnabled(openaiModel)
const openAIConvertibleMessages = messagesForAPI.filter(
isOpenAIConvertibleMessage,
)
@@ -305,7 +332,11 @@ export async function* queryModelOpenAI(
)
const openaiTools = anthropicToolsToOpenAI(standardTools)
const openaiToolChoice = anthropicToolChoiceToOpenAI(options.toolChoice)
- const reasoningEffort = getChatGPTResponsesReasoningEffort(
+ const responsesReasoningEffort = getChatGPTResponsesReasoningEffort(
+ options.effortValue,
+ )
+ const chatCompletionsReasoningEffort = getChatCompletionsReasoningEffort(
+ openaiModel,
options.effortValue,
)
@@ -366,7 +397,7 @@ export async function* queryModelOpenAI(
messages: openaiMessages,
tools: openaiTools,
toolChoice: openaiToolChoice,
- reasoningEffort,
+ reasoningEffort: responsesReasoningEffort,
promptCacheKey,
}),
signal,
@@ -388,8 +419,9 @@ export async function* queryModelOpenAI(
enableThinking,
maxTokens,
temperatureOverride: options.temperatureOverride,
+ reasoningEffort: chatCompletionsReasoningEffort,
promptCacheKey,
- }),
+ }) as unknown as import('openai/resources/chat/completions/completions.mjs').ChatCompletionCreateParamsStreaming,
{ signal },
),
openaiModel,
diff --git a/src/services/api/openai/requestBody.ts b/src/services/api/openai/requestBody.ts
index a31e377667..c60220bafe 100644
--- a/src/services/api/openai/requestBody.ts
+++ b/src/services/api/openai/requestBody.ts
@@ -4,26 +4,24 @@
* triggering heavy module side-effects (OpenAI client, stream adapter, etc.).
*/
import type { ChatCompletionCreateParamsStreaming } from 'openai/resources/chat/completions/completions.mjs'
-import { isEnvTruthy, isEnvDefinedFalsy } from '../../../utils/envUtils.js'
+import type { EffortLevel } from '../../../entrypoints/sdk/runtimeTypes.js'
+import { isEnvDefinedFalsy } from '../../../utils/envUtils.js'
import { getOpenAIPromptCacheKey } from './openaiShared.js'
/**
* Detect whether thinking mode should be enabled for this model.
*
- * Enabled when:
- * 1. OPENAI_ENABLE_THINKING=1 is set (explicit enable), OR
- * 2. Model name contains "deepseek" or "mimo" (auto-detect, case-insensitive)
- *
- * Disabled when:
- * - OPENAI_ENABLE_THINKING=0/false/no/off is explicitly set (overrides model detection)
+ * Enabled for DeepSeek and MiMo models unless OPENAI_ENABLE_THINKING is
+ * explicitly disabled. Other models use standard reasoning_effort without
+ * receiving provider-specific thinking fields.
*
* @param model - The resolved OpenAI model name
*/
export function isOpenAIThinkingEnabled(model: string): boolean {
// Explicit disable takes priority (overrides model auto-detect)
if (isEnvDefinedFalsy(process.env.OPENAI_ENABLE_THINKING)) return false
- // Explicit enable
- if (isEnvTruthy(process.env.OPENAI_ENABLE_THINKING)) return true
+ // DeepSeek and MiMo use the provider-specific thinking body below. For other
+ // models OPENAI_ENABLE_THINKING only gates standard reasoning_effort.
// Auto-detect from model name (DeepSeek and MiMo models support thinking mode).
// Grok is intentionally excluded — Grok reasoning models reason automatically
// and do NOT require thinking/enable_thinking request body parameters.
@@ -59,15 +57,18 @@ export function resolveOpenAIMaxTokens(
/**
* Build the request body for OpenAI chat.completions.create().
- * Extracted for testability — the thinking mode params are injected here.
- *
- * Three thinking-mode formats are sent simultaneously; each endpoint uses the
- * format it recognizes and ignores the others:
- * - Official DeepSeek API: `thinking: { type: 'enabled' }`
- * - Self-hosted DeepSeek: `enable_thinking: true` + `chat_template_kwargs: { thinking: true }`
- * - MiMo (Xiaomi): `chat_template_kwargs: { enable_thinking: true }`
- * OpenAI SDK passes unknown keys through to the HTTP body.
+ * Reasoning effort uses the standard top-level `reasoning_effort` field.
+ * DeepSeek and MiMo additionally receive `thinking: { type: 'enabled' }`.
*/
+type OpenAIChatRequestBody = Omit<
+ ChatCompletionCreateParamsStreaming,
+ 'reasoning_effort'
+> & {
+ reasoning_effort?: EffortLevel
+ thinking?: { type: 'enabled' }
+ prompt_cache_key?: string
+}
+
export function buildOpenAIRequestBody(params: {
model: string
messages: any[]
@@ -76,15 +77,10 @@ export function buildOpenAIRequestBody(params: {
enableThinking: boolean
maxTokens: number
temperatureOverride?: number
+ reasoningEffort?: EffortLevel
/** Override for tests; production uses the current CCB session id. */
promptCacheKey?: string
-}): ChatCompletionCreateParamsStreaming & {
- thinking?: { type: string }
- enable_thinking?: boolean
- chat_template_kwargs?: { thinking: boolean; enable_thinking: boolean }
- /** OpenAI prompt-cache routing key (not always in SDK types yet). */
- prompt_cache_key?: string
-} {
+}): OpenAIChatRequestBody {
const {
model,
messages,
@@ -93,12 +89,14 @@ export function buildOpenAIRequestBody(params: {
enableThinking,
maxTokens,
temperatureOverride,
+ reasoningEffort,
promptCacheKey,
} = params
return {
model,
messages,
max_tokens: maxTokens,
+ ...(reasoningEffort && { reasoning_effort: reasoningEffort }),
...(tools.length > 0 && {
tools,
...(toolChoice && { tool_choice: toolChoice }),
diff --git a/src/utils/__tests__/effort.test.ts b/src/utils/__tests__/effort.test.ts
index eb7c0a5005..d924f88cf6 100644
--- a/src/utils/__tests__/effort.test.ts
+++ b/src/utils/__tests__/effort.test.ts
@@ -16,20 +16,39 @@ mock.module('src/services/analytics/growthbook.js', () => ({
getFeatureValue_CACHED_MAY_BE_STALE: (_key: string, defaultValue: unknown) =>
defaultValue ?? {},
}))
-mock.module('src/utils/model/modelSupportOverrides.js', () => ({
- get3PModelCapabilityOverride: () => undefined,
-}))
-
const {
isEffortLevel,
parseEffortValue,
isValidNumericEffort,
convertEffortValueToLevel,
getEffortLevelDescription,
+ resolveOpenAICompatibleEffortSupport,
resolvePickerEffortPersistence,
EFFORT_LEVELS,
} = await import('src/utils/effort.js')
+describe('resolveOpenAICompatibleEffortSupport', () => {
+ test('defaults to enabled when no override is configured', () => {
+ expect(resolveOpenAICompatibleEffortSupport(undefined, undefined)).toBe(
+ true,
+ )
+ })
+
+ test('disables effort when OPENAI_ENABLE_THINKING is explicitly false', () => {
+ for (const value of ['0', 'false', 'no', 'off']) {
+ expect(resolveOpenAICompatibleEffortSupport(value, true)).toBe(false)
+ }
+ })
+
+ test('disables effort when the tier capability omits thinking', () => {
+ expect(resolveOpenAICompatibleEffortSupport('true', false)).toBe(false)
+ })
+
+ test('keeps effort enabled when thinking is explicitly supported', () => {
+ expect(resolveOpenAICompatibleEffortSupport('true', true)).toBe(true)
+ })
+})
+
// ─── EFFORT_LEVELS constant ────────────────────────────────────────────
describe('EFFORT_LEVELS', () => {
diff --git a/src/utils/effort.ts b/src/utils/effort.ts
index df1a756dae..c5314b3f20 100644
--- a/src/utils/effort.ts
+++ b/src/utils/effort.ts
@@ -5,7 +5,7 @@ import { isProSubscriber, isMaxSubscriber, isTeamSubscriber } from './auth.js'
import { getFeatureValue_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js'
import { getAPIProvider } from './model/providers.js'
import { get3PModelCapabilityOverride } from './model/modelSupportOverrides.js'
-import { isEnvTruthy } from './envUtils.js'
+import { isEnvDefinedFalsy, isEnvTruthy } from './envUtils.js'
import type { EffortLevel } from 'src/entrypoints/sdk/runtimeTypes.js'
import { resolveAntModel } from './model/antModels.js'
import { getAntModelOverrideConfig } from './model/antModels.js'
@@ -30,9 +30,26 @@ export const EFFORT_LEVELS = [
export type EffortValue = EffortLevel | number
+export function resolveOpenAICompatibleEffortSupport(
+ thinkingEnv: string | undefined,
+ thinkingCapability: boolean | undefined,
+): boolean {
+ return !isEnvDefinedFalsy(thinkingEnv) && thinkingCapability !== false
+}
+
// @[MODEL LAUNCH]: Add the new model to the allowlist if it supports the effort parameter.
export function modelSupportsEffort(model: string): boolean {
const m = model.toLowerCase()
+ const provider = getAPIProvider()
+ if (provider === 'openai') {
+ if (isChatGPTAuthMode()) {
+ return isChatGPTCodexReasoningModel(model)
+ }
+ return resolveOpenAICompatibleEffortSupport(
+ process.env.OPENAI_ENABLE_THINKING,
+ get3PModelCapabilityOverride(model, 'thinking'),
+ )
+ }
if (isEnvTruthy(process.env.CLAUDE_CODE_ALWAYS_ENABLE_EFFORT)) {
return true
}
@@ -40,13 +57,6 @@ export function modelSupportsEffort(model: string): boolean {
if (supported3P !== undefined) {
return supported3P
}
- if (
- getAPIProvider() === 'openai' &&
- isChatGPTAuthMode() &&
- isChatGPTCodexReasoningModel(model)
- ) {
- return true
- }
// Supported by a subset of Claude 4 models
if (
m.includes('opus-4-7') ||
diff --git a/src/utils/model/__tests__/modelSupportOverrides.test.ts b/src/utils/model/__tests__/modelSupportOverrides.test.ts
new file mode 100644
index 0000000000..e85e3683de
--- /dev/null
+++ b/src/utils/model/__tests__/modelSupportOverrides.test.ts
@@ -0,0 +1,38 @@
+import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
+import { get3PModelCapabilityOverride } from '../modelSupportOverrides.js'
+
+const ENV_KEYS = [
+ 'CLAUDE_CODE_USE_OPENAI',
+ 'OPENAI_DEFAULT_SONNET_MODEL',
+ 'OPENAI_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES',
+] as const
+
+const savedEnv: Record = {}
+
+describe('get3PModelCapabilityOverride', () => {
+ beforeEach(() => {
+ for (const key of ENV_KEYS) savedEnv[key] = process.env[key]
+ process.env.CLAUDE_CODE_USE_OPENAI = '1'
+ process.env.OPENAI_DEFAULT_SONNET_MODEL = 'cache-test-model'
+ })
+
+ afterEach(() => {
+ for (const key of ENV_KEYS) {
+ const value = savedEnv[key]
+ if (value === undefined) delete process.env[key]
+ else process.env[key] = value
+ }
+ })
+
+ test('re-evaluates capabilities after environment changes', () => {
+ process.env.OPENAI_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES = 'thinking'
+ expect(get3PModelCapabilityOverride('cache-test-model', 'thinking')).toBe(
+ true,
+ )
+
+ process.env.OPENAI_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES = ''
+ expect(get3PModelCapabilityOverride('cache-test-model', 'thinking')).toBe(
+ false,
+ )
+ })
+})
diff --git a/src/utils/model/modelSupportOverrides.ts b/src/utils/model/modelSupportOverrides.ts
index 7e842a1cac..11f05ae57d 100644
--- a/src/utils/model/modelSupportOverrides.ts
+++ b/src/utils/model/modelSupportOverrides.ts
@@ -64,5 +64,15 @@ export const get3PModelCapabilityOverride = memoize(
}
return undefined
},
- (model, capability) => `${model.toLowerCase()}:${capability}`,
+ (model, capability) => {
+ const provider = getAPIProvider()
+ const tiers = provider === 'openai' ? OPENAI_TIERS : ANTHROPIC_TIERS
+ const tierState = tiers
+ .map(
+ tier =>
+ `${process.env[tier.modelEnvVar] ?? ''}=${process.env[tier.capabilitiesEnvVar] ?? ''}`,
+ )
+ .join('|')
+ return `${provider}:${model.toLowerCase()}:${capability}:${tierState}`
+ },
)