From 883789f0431dc958937151e35def06198439ad41 Mon Sep 17 00:00:00 2001 From: guanjiawei Date: Fri, 10 Apr 2026 18:05:24 +0800 Subject: [PATCH 1/2] feat add codex fast mode support --- src/QueryEngine.ts | 14 +- src/cli/print.ts | 27 +- src/commands/fast/fast.tsx | 617 +++++++++++------- src/commands/fast/index.ts | 11 +- src/commands/model/model.tsx | 30 +- src/components/LogoV2/CondensedLogo.tsx | 14 +- src/components/LogoV2/LogoV2.tsx | 13 +- src/components/ModelPicker.tsx | 25 +- src/components/PromptInput/PromptInput.tsx | 16 +- .../PromptInput/PromptInputHelpMenu.tsx | 4 +- src/components/StartupScreen.ts | 39 +- src/main.tsx | 11 +- src/services/api/claude.ts | 15 +- src/services/api/client.ts | 5 + src/services/api/codexShim.test.ts | 50 ++ src/services/api/codexShim.ts | 4 + src/services/api/openaiShim.ts | 16 +- src/services/api/providerConfig.ts | 5 + src/utils/messages/systemInit.ts | 7 +- src/utils/model/codexDisplay.test.ts | 20 + src/utils/model/codexDisplay.ts | 39 ++ src/utils/model/providerModelSettings.test.ts | 51 ++ src/utils/model/providerModelSettings.ts | 50 +- src/utils/providerFastMode.ts | 205 ++++++ src/utils/settings/types.ts | 3 +- src/utils/status.tsx | 32 +- 26 files changed, 990 insertions(+), 333 deletions(-) create mode 100644 src/utils/model/codexDisplay.test.ts create mode 100644 src/utils/model/codexDisplay.ts create mode 100644 src/utils/providerFastMode.ts diff --git a/src/QueryEngine.ts b/src/QueryEngine.ts index d5a120015..d216d3779 100644 --- a/src/QueryEngine.ts +++ b/src/QueryEngine.ts @@ -47,7 +47,7 @@ import { getGlobalConfig } from './utils/config.js' import { getCwd } from './utils/cwd.js' import { isBareMode, isEnvTruthy } from './utils/envUtils.js' import { logForDebugging } from './utils/debug.js' -import { getFastModeState } from './utils/fastMode.js' +import { getProviderFastModeState } from './utils/providerFastMode.js' import { type FileHistoryState, fileHistoryEnabled, @@ -630,7 +630,7 @@ export class QueryEngine { usage: this.totalUsage, modelUsage: getModelUsage(), permission_denials: this.permissionDenials, - fast_mode_state: getFastModeState( + fast_mode_state: getProviderFastModeState( mainLoopModel, initialAppState.fastMode, ), @@ -875,7 +875,7 @@ export class QueryEngine { usage: this.totalUsage, modelUsage: getModelUsage(), permission_denials: this.permissionDenials, - fast_mode_state: getFastModeState( + fast_mode_state: getProviderFastModeState( mainLoopModel, initialAppState.fastMode, ), @@ -1005,7 +1005,7 @@ export class QueryEngine { usage: this.totalUsage, modelUsage: getModelUsage(), permission_denials: this.permissionDenials, - fast_mode_state: getFastModeState( + fast_mode_state: getProviderFastModeState( mainLoopModel, initialAppState.fastMode, ), @@ -1048,7 +1048,7 @@ export class QueryEngine { usage: this.totalUsage, modelUsage: getModelUsage(), permission_denials: this.permissionDenials, - fast_mode_state: getFastModeState( + fast_mode_state: getProviderFastModeState( mainLoopModel, initialAppState.fastMode, ), @@ -1107,7 +1107,7 @@ export class QueryEngine { usage: this.totalUsage, modelUsage: getModelUsage(), permission_denials: this.permissionDenials, - fast_mode_state: getFastModeState( + fast_mode_state: getProviderFastModeState( mainLoopModel, initialAppState.fastMode, ), @@ -1161,7 +1161,7 @@ export class QueryEngine { modelUsage: getModelUsage(), permission_denials: this.permissionDenials, structured_output: structuredOutputFromTool, - fast_mode_state: getFastModeState( + fast_mode_state: getProviderFastModeState( mainLoopModel, initialAppState.fastMode, ), diff --git a/src/cli/print.ts b/src/cli/print.ts index 00b5c8340..cae5afbea 100644 --- a/src/cli/print.ts +++ b/src/cli/print.ts @@ -168,11 +168,14 @@ import { import { settingsChangeDetector } from 'src/utils/settings/changeDetector.js' import { applySettingsChange } from 'src/utils/settings/applySettingsChange.js' import { - isFastModeAvailable, - isFastModeEnabled, isFastModeSupportedByModel, - getFastModeState, } from 'src/utils/fastMode.js' +import { + getInitialProviderFastModeSetting, + getProviderFastModeState, + isFastModeToggleAvailable, + isFastModeToggleEnabled, +} from 'src/utils/providerFastMode.js' import { isAutoModeGateEnabled, getAutoModeUnavailableNotification, @@ -516,10 +519,15 @@ export async function runHeadless( // In headless mode, also sync the denormalized fastMode field from // settings. The TUI manages fastMode via the UI so it skips this. - if (isFastModeEnabled()) { + if (isFastModeToggleEnabled()) { setAppState(prev => { - const s = prev.settings as Record - const fastMode = s.fastMode === true && !s.fastModePerSessionOptIn + const fastMode = getInitialProviderFastModeSetting( + prev.mainLoopModel, + { + settings: getSettings_DEPRECATED(), + targetKey: prev.providerSelectionTargetKey, + }, + ) return { ...prev, fastMode } }) } @@ -4471,9 +4479,12 @@ async function handleInitializeRequest( pid: process.pid, } - if (isFastModeEnabled() && isFastModeAvailable()) { + if ( + isFastModeToggleEnabled() && + (isFastModeToggleAvailable() || getAppState().fastMode) + ) { const appState = getAppState() - initResponse.fast_mode_state = getFastModeState( + initResponse.fast_mode_state = getProviderFastModeState( options.userSpecifiedModel ?? null, appState.fastMode, ) diff --git a/src/commands/fast/fast.tsx b/src/commands/fast/fast.tsx index af460c56a..fc5b43e27 100644 --- a/src/commands/fast/fast.tsx +++ b/src/commands/fast/fast.tsx @@ -1,268 +1,395 @@ -import { c as _c } from "react-compiler-runtime"; -import * as React from 'react'; -import { useState } from 'react'; -import type { CommandResultDisplay, LocalJSXCommandContext } from '../../commands.js'; -import { Dialog } from '../../components/design-system/Dialog.js'; -import { FastIcon, getFastIconString } from '../../components/FastIcon.js'; -import { Box, Link, Text } from '../../ink.js'; -import { useKeybindings } from '../../keybindings/useKeybinding.js'; -import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../services/analytics/index.js'; -import { type AppState, useAppState, useSetAppState } from '../../state/AppState.js'; -import type { LocalJSXCommandOnDone } from '../../types/command.js'; -import { clearFastModeCooldown, FAST_MODE_MODEL_DISPLAY, getFastModeModel, getFastModeRuntimeState, getFastModeUnavailableReason, isFastModeEnabled, isFastModeSupportedByModel, prefetchFastModeStatus } from '../../utils/fastMode.js'; -import { formatDuration } from '../../utils/format.js'; -import { formatModelPricing, getOpus46CostTier } from '../../utils/modelCost.js'; -import { updateSettingsForSource } from '../../utils/settings/settings.js'; -function applyFastMode(enable: boolean, setAppState: (f: (prev: AppState) => AppState) => void): void { - clearFastModeCooldown(); +import * as React from 'react' +import { useState } from 'react' +import type { LocalJSXCommandContext } from '../../commands.js' +import { FastIcon, getFastIconString } from '../../components/FastIcon.js' +import { Dialog } from '../../components/design-system/Dialog.js' +import { Box, Link, Text } from '../../ink.js' +import { useKeybindings } from '../../keybindings/useKeybinding.js' +import { + type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, + logEvent, +} from '../../services/analytics/index.js' +import { type AppState, useAppState, useSetAppState } from '../../state/AppState.js' +import type { LocalJSXCommandOnDone } from '../../types/command.js' +import { + clearFastModeCooldown, + FAST_MODE_MODEL_DISPLAY, + getFastModeModel, + getFastModeRuntimeState, + isFastModeSupportedByModel, + prefetchFastModeStatus, +} from '../../utils/fastMode.js' +import { formatDuration } from '../../utils/format.js' +import { formatModelPricing, getOpus46CostTier } from '../../utils/modelCost.js' +import { + buildProviderModelSettingsUpdate, + type PersistedServiceTier, +} from '../../utils/model/providerModelSettings.js' +import { + getFastModeUnavailableReasonForProvider, + isFastModeCooldownForProvider, + isFastModeToggleEnabled, + resolveFastModeProvider, +} from '../../utils/providerFastMode.js' +import { getSettingsForSource, updateSettingsForSource } from '../../utils/settings/settings.js' + +type FastModeCommandProvider = ReturnType + +function setCodexFastModeSelection( + targetKey: string, + serviceTier: PersistedServiceTier | null, +): void { + const userSettings = getSettingsForSource('userSettings') || {} + updateSettingsForSource( + 'userSettings', + buildProviderModelSettingsUpdate({ + settings: userSettings, + provider: 'codex', + targetKey, + serviceTier, + }), + ) +} + +function applyFastMode(options: { + enable: boolean + provider: FastModeCommandProvider + targetKey: string + setAppState: (f: (prev: AppState) => AppState) => void +}): { modelUpdated: boolean } { + const { enable, provider, targetKey, setAppState } = options + + if (provider === 'codex') { + setCodexFastModeSelection(targetKey, enable ? 'fast' : null) + setAppState(prev => ({ + ...prev, + fastMode: enable, + })) + return { modelUpdated: false } + } + + if (provider !== 'firstParty') { + return { modelUpdated: false } + } + + clearFastModeCooldown() updateSettingsForSource('userSettings', { - fastMode: enable ? true : undefined - }); + fastMode: enable ? true : undefined, + }) + if (enable) { + let modelUpdated = false setAppState(prev => { - // Only switch model if current model doesn't support fast mode - const needsModelSwitch = !isFastModeSupportedByModel(prev.mainLoopModel); + modelUpdated = !isFastModeSupportedByModel(prev.mainLoopModel) return { ...prev, - ...(needsModelSwitch ? { - mainLoopModel: getFastModeModel(), - mainLoopModelForSession: null - } : {}), - fastMode: true - }; - }); - } else { - setAppState(prev => ({ - ...prev, - fastMode: false - })); + ...(modelUpdated + ? { + mainLoopModel: getFastModeModel(), + mainLoopModelForSession: null, + } + : {}), + fastMode: true, + } + }) + return { modelUpdated } } + + setAppState(prev => ({ + ...prev, + fastMode: false, + })) + return { modelUpdated: false } } -export function FastModePicker(t0) { - const $ = _c(30); - const { - onDone, - unavailableReason - } = t0; - const model = useAppState(_temp); - const initialFastMode = useAppState(_temp2); - const setAppState = useSetAppState(); - const [enableFastMode, setEnableFastMode] = useState(initialFastMode ?? false); - let t1; - if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - t1 = getFastModeRuntimeState(); - $[0] = t1; - } else { - t1 = $[0]; + +function getFastModeSubtitle(provider: FastModeCommandProvider): string { + if (provider === 'codex') { + return 'Switch Codex requests between normal and fast.' } - const runtimeState = t1; - const isCooldown = runtimeState.status === "cooldown"; - const isUnavailable = unavailableReason !== null; - let t2; - if ($[1] === Symbol.for("react.memo_cache_sentinel")) { - t2 = formatModelPricing(getOpus46CostTier(true)); - $[1] = t2; - } else { - t2 = $[1]; + return `High-speed mode for ${FAST_MODE_MODEL_DISPLAY}. Billed as extra usage at a premium rate. Separate rate limits apply.` +} + +function getFastModeConfirmMessage(options: { + enable: boolean + provider: FastModeCommandProvider + modelUpdated: boolean +}): string { + if (!options.enable) { + return 'Fast mode OFF' } - const pricing = t2; - let t3; - if ($[2] !== enableFastMode || $[3] !== isUnavailable || $[4] !== model || $[5] !== onDone || $[6] !== setAppState) { - t3 = function handleConfirm() { - if (isUnavailable) { - return; - } - applyFastMode(enableFastMode, setAppState); - logEvent("tengu_fast_mode_toggled", { - enabled: enableFastMode, - source: "picker" as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS - }); - if (enableFastMode) { - const fastIcon = getFastIconString(enableFastMode); - const modelUpdated = !isFastModeSupportedByModel(model) ? ` · model set to ${FAST_MODE_MODEL_DISPLAY}` : ""; - onDone(`${fastIcon} Fast mode ON${modelUpdated} · ${pricing}`); - } else { - setAppState(_temp3); - onDone("Fast mode OFF"); - } - }; - $[2] = enableFastMode; - $[3] = isUnavailable; - $[4] = model; - $[5] = onDone; - $[6] = setAppState; - $[7] = t3; - } else { - t3 = $[7]; + + const fastIcon = getFastIconString(true) + if (options.provider === 'codex') { + return `${fastIcon} Fast mode ON` } - const handleConfirm = t3; - let t4; - if ($[8] !== initialFastMode || $[9] !== isUnavailable || $[10] !== onDone || $[11] !== setAppState) { - t4 = function handleCancel() { - if (isUnavailable) { - if (initialFastMode) { - applyFastMode(false, setAppState); - } - onDone("Fast mode OFF", { - display: "system" - }); - return; + + const pricing = formatModelPricing(getOpus46CostTier(true)) + const modelUpdated = options.modelUpdated + ? ` · model set to ${FAST_MODE_MODEL_DISPLAY}` + : '' + return `${fastIcon} Fast mode ON${modelUpdated} · ${pricing}` +} + +export function FastModePicker({ + onDone, + provider, + unavailableReason, +}: { + onDone: LocalJSXCommandOnDone + provider: FastModeCommandProvider + unavailableReason: string | null +}): React.ReactNode { + const model = useAppState((s: AppState) => s.mainLoopModel) + const initialFastMode = useAppState((s: AppState) => s.fastMode ?? false) + const providerSelectionTargetKey = useAppState( + (s: AppState) => s.providerSelectionTargetKey, + ) + const setAppState = useSetAppState() + const [enableFastMode, setEnableFastMode] = useState(initialFastMode) + + const runtimeState = getFastModeRuntimeState() + const isCooldown = + provider === 'firstParty' && + isFastModeCooldownForProvider({ provider }) && + runtimeState.status === 'cooldown' + const pricing = + provider === 'firstParty' + ? formatModelPricing(getOpus46CostTier(true)) + : undefined + + const handleConfirm = React.useCallback(() => { + if (unavailableReason) { + return + } + + const { modelUpdated } = applyFastMode({ + enable: enableFastMode, + provider, + targetKey: providerSelectionTargetKey, + setAppState, + }) + + logEvent('tengu_fast_mode_toggled', { + enabled: enableFastMode, + source: 'picker' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, + }) + + if ( + provider === 'firstParty' && + !enableFastMode && + !isFastModeSupportedByModel(model) + ) { + setAppState((prev: AppState) => ({ + ...prev, + fastMode: false, + })) + } + + onDone( + getFastModeConfirmMessage({ + enable: enableFastMode, + provider, + modelUpdated, + }), + ) + }, [ + enableFastMode, + model, + onDone, + provider, + providerSelectionTargetKey, + setAppState, + unavailableReason, + ]) + + const handleCancel = React.useCallback(() => { + if (unavailableReason) { + if (initialFastMode) { + applyFastMode({ + enable: false, + provider, + targetKey: providerSelectionTargetKey, + setAppState, + }) } - const message = initialFastMode ? `${getFastIconString()} Kept Fast mode ON` : "Kept Fast mode OFF"; - onDone(message, { - display: "system" - }); - }; - $[8] = initialFastMode; - $[9] = isUnavailable; - $[10] = onDone; - $[11] = setAppState; - $[12] = t4; - } else { - t4 = $[12]; - } - const handleCancel = t4; - let t5; - if ($[13] !== isUnavailable) { - t5 = function handleToggle() { - if (isUnavailable) { - return; + onDone('Fast mode OFF', { display: 'system' }) + return + } + + onDone( + initialFastMode + ? `${getFastIconString()} Kept Fast mode ON` + : 'Kept Fast mode OFF', + { display: 'system' }, + ) + }, [ + initialFastMode, + onDone, + provider, + providerSelectionTargetKey, + setAppState, + unavailableReason, + ]) + + const handleToggle = React.useCallback(() => { + if (unavailableReason) { + return + } + setEnableFastMode((prev: boolean) => !prev) + }, [unavailableReason]) + + useKeybindings( + { + 'confirm:yes': handleConfirm, + 'confirm:nextField': handleToggle, + 'confirm:next': handleToggle, + 'confirm:previous': handleToggle, + 'confirm:cycleMode': handleToggle, + 'confirm:toggle': handleToggle, + }, + { context: 'Confirmation' }, + ) + + return ( + + Fast mode + {provider === 'firstParty' ? ' (research preview)' : ''} + } - setEnableFastMode(_temp4); - }; - $[13] = isUnavailable; - $[14] = t5; - } else { - t5 = $[14]; - } - const handleToggle = t5; - let t6; - if ($[15] !== handleConfirm || $[16] !== handleToggle) { - t6 = { - "confirm:yes": handleConfirm, - "confirm:nextField": handleToggle, - "confirm:next": handleToggle, - "confirm:previous": handleToggle, - "confirm:cycleMode": handleToggle, - "confirm:toggle": handleToggle - }; - $[15] = handleConfirm; - $[16] = handleToggle; - $[17] = t6; - } else { - t6 = $[17]; - } - let t7; - if ($[18] === Symbol.for("react.memo_cache_sentinel")) { - t7 = { - context: "Confirmation" - }; - $[18] = t7; - } else { - t7 = $[18]; - } - useKeybindings(t6, t7); - let t8; - if ($[19] === Symbol.for("react.memo_cache_sentinel")) { - t8 = Fast mode (research preview); - $[19] = t8; - } else { - t8 = $[19]; - } - const title = t8; - let t9; - if ($[20] !== isUnavailable) { - t9 = exitState => exitState.pending ? Press {exitState.keyName} again to exit : isUnavailable ? Esc to cancel : Tab to toggle · Enter to confirm · Esc to cancel; - $[20] = isUnavailable; - $[21] = t9; - } else { - t9 = $[21]; - } - let t10; - if ($[22] !== enableFastMode || $[23] !== unavailableReason) { - t10 = unavailableReason ? {unavailableReason} : <>Fast mode{enableFastMode ? "ON " : "OFF"}{pricing}{isCooldown && runtimeState.status === "cooldown" && {runtimeState.reason === "overloaded" ? "Fast mode overloaded and is temporarily unavailable" : "You've hit your fast limit"}{" \xB7 resets in "}{formatDuration(runtimeState.resetAt - Date.now(), { - hideTrailingZeros: true - })}}; - $[22] = enableFastMode; - $[23] = unavailableReason; - $[24] = t10; - } else { - t10 = $[24]; - } - let t11; - if ($[25] === Symbol.for("react.memo_cache_sentinel")) { - t11 = Learn more:{" "}https://code.claude.com/docs/en/fast-mode; - $[25] = t11; - } else { - t11 = $[25]; - } - let t12; - if ($[26] !== handleCancel || $[27] !== t10 || $[28] !== t9) { - t12 = {t10}{t11}; - $[26] = handleCancel; - $[27] = t10; - $[28] = t9; - $[29] = t12; - } else { - t12 = $[29]; - } - return t12; -} -function _temp4(prev_0) { - return !prev_0; -} -function _temp3(prev) { - return { - ...prev, - fastMode: false - }; -} -function _temp2(s_0) { - return s_0.fastMode; -} -function _temp(s) { - return s.mainLoopModel; + subtitle={getFastModeSubtitle(provider)} + onCancel={handleCancel} + color="fastMode" + inputGuide={(exitState: { pending: boolean; keyName: string }) => + exitState.pending ? ( + Press {exitState.keyName} again to exit + ) : unavailableReason ? ( + Esc to cancel + ) : ( + Tab to toggle · Enter to confirm · Esc to cancel + ) + } + > + {unavailableReason ? ( + + {unavailableReason} + + ) : ( + <> + + + Fast mode + + {enableFastMode ? 'ON' : 'OFF'} + + {pricing ? {pricing} : null} + + {provider === 'codex' ? ( + Normal or fast, applied to the current Codex target. + ) : null} + + {isCooldown ? ( + + + {runtimeState.reason === 'overloaded' + ? 'Fast mode overloaded and is temporarily unavailable' + : "You've hit your fast limit"} + {' · resets in '} + {formatDuration(runtimeState.resetAt - Date.now(), { + hideTrailingZeros: true, + })} + + + ) : null} + {provider === 'firstParty' ? ( + + Learn more:{' '} + + https://code.claude.com/docs/en/fast-mode + + + ) : null} + + )} + + ) } -async function handleFastModeShortcut(enable: boolean, getAppState: () => AppState, setAppState: (f: (prev: AppState) => AppState) => void): Promise { - const unavailableReason = getFastModeUnavailableReason(); + +async function handleFastModeShortcut(options: { + enable: boolean + provider: FastModeCommandProvider + getAppState: () => AppState + setAppState: (f: (prev: AppState) => AppState) => void +}): Promise { + const unavailableReason = getFastModeUnavailableReasonForProvider({ + provider: options.provider ?? undefined, + }) if (unavailableReason) { - return `Fast mode unavailable: ${unavailableReason}`; + return `Fast mode unavailable: ${unavailableReason}` } - const { - mainLoopModel - } = getAppState(); - applyFastMode(enable, setAppState); + + const appState = options.getAppState() + const { modelUpdated } = applyFastMode({ + enable: options.enable, + provider: options.provider, + targetKey: appState.providerSelectionTargetKey, + setAppState: options.setAppState, + }) + logEvent('tengu_fast_mode_toggled', { - enabled: enable, - source: 'shortcut' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS - }); - if (enable) { - const fastIcon = getFastIconString(true); - const modelUpdated = !isFastModeSupportedByModel(mainLoopModel) ? ` · model set to ${FAST_MODE_MODEL_DISPLAY}` : ''; - const pricing = formatModelPricing(getOpus46CostTier(true)); - return `${fastIcon} Fast mode ON${modelUpdated} · ${pricing}`; - } else { - return `Fast mode OFF`; - } + enabled: options.enable, + source: 'shortcut' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, + }) + + return getFastModeConfirmMessage({ + enable: options.enable, + provider: options.provider, + modelUpdated, + }) } -export async function call(onDone: LocalJSXCommandOnDone, context: LocalJSXCommandContext, args?: string): Promise { - if (!isFastModeEnabled()) { - return null; + +export async function call( + onDone: LocalJSXCommandOnDone, + context: LocalJSXCommandContext, + args?: string, +): Promise { + const provider = resolveFastModeProvider({ + targetKey: context.getAppState().providerSelectionTargetKey, + }) + + if (!provider || !isFastModeToggleEnabled({ provider })) { + return null + } + + if (provider === 'firstParty') { + await prefetchFastModeStatus() } - // Fetch org fast mode status before showing the picker. We must know - // whether the org has disabled fast mode before allowing any toggle. - // If a startup prefetch is already in flight, this awaits it. - await prefetchFastModeStatus(); - const arg = args?.trim().toLowerCase(); + const arg = args?.trim().toLowerCase() if (arg === 'on' || arg === 'off') { - const result = await handleFastModeShortcut(arg === 'on', context.getAppState, context.setAppState); - onDone(result); - return null; + const result = await handleFastModeShortcut({ + enable: arg === 'on', + provider, + getAppState: context.getAppState, + setAppState: context.setAppState, + }) + onDone(result) + return null } - const unavailableReason = getFastModeUnavailableReason(); + + const unavailableReason = getFastModeUnavailableReasonForProvider({ provider }) logEvent('tengu_fast_mode_picker_shown', { - unavailable_reason: (unavailableReason ?? '') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS - }); - return ; + unavailable_reason: (unavailableReason ?? '') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, + }) + + return ( + + ) } + +export default FastModePicker diff --git a/src/commands/fast/index.ts b/src/commands/fast/index.ts index 88ed5506d..a553ea07c 100644 --- a/src/commands/fast/index.ts +++ b/src/commands/fast/index.ts @@ -1,20 +1,17 @@ import type { Command } from '../../commands.js' -import { - FAST_MODE_MODEL_DISPLAY, - isFastModeEnabled, -} from '../../utils/fastMode.js' +import { isFastModeToggleEnabled } from '../../utils/providerFastMode.js' import { shouldInferenceConfigCommandBeImmediate } from '../../utils/immediateCommand.js' const fast = { type: 'local-jsx', name: 'fast', get description() { - return `Toggle fast mode (${FAST_MODE_MODEL_DISPLAY} only)` + return 'Toggle fast mode' }, availability: ['claude-ai', 'console'], - isEnabled: () => isFastModeEnabled(), + isEnabled: () => isFastModeToggleEnabled(), get isHidden() { - return !isFastModeEnabled() + return !isFastModeToggleEnabled() }, argumentHint: '[on|off]', get immediate() { diff --git a/src/commands/model/model.tsx b/src/commands/model/model.tsx index 91f70cac7..32bcf5c5f 100644 --- a/src/commands/model/model.tsx +++ b/src/commands/model/model.tsx @@ -10,13 +10,14 @@ import { useAppState, useSetAppState } from '../../state/AppState.js'; import type { LocalJSXCommandCall } from '../../types/command.js'; import type { EffortValue } from '../../utils/effort.js'; import { isBilledAsExtraUsage } from '../../utils/extraUsage.js'; -import { clearFastModeCooldown, isFastModeAvailable, isFastModeEnabled, isFastModeSupportedByModel } from '../../utils/fastMode.js'; +import { clearFastModeCooldown } from '../../utils/fastMode.js'; import { MODEL_ALIASES } from '../../utils/model/aliases.js'; import { checkOpus1mAccess, checkSonnet1mAccess } from '../../utils/model/check1mAccess.js'; import type { ModelOption } from '../../utils/model/modelOptions.js'; import { discoverOpenAICompatibleModelOptions } from '../../utils/model/openaiModelDiscovery.js'; import { resolveModelSettingForTarget, resolveProviderSelectionTargetOption } from '../../utils/model/providerTargets.js'; import { getAPIProvider } from '../../utils/model/providers.js'; +import { getInitialProviderFastModeSetting, isFastModeSupportedForModel, isFastModeSupportedForProviderModel, isFastModeToggleAvailable, isFastModeToggleEnabled } from '../../utils/providerFastMode.js'; import { getActiveOpenAIModelOptionsCache, setActiveOpenAIModelOptionsCache } from '../../utils/providerProfiles.js'; import { getDefaultMainLoopModelSetting, isOpus1mMergeEnabled, renderDefaultModelSetting } from '../../utils/model/model.js'; import { isModelAllowed } from '../../utils/model/modelAllowlist.js'; @@ -43,6 +44,9 @@ function ModelPickerWrapper(t0) { const target = resolveProviderSelectionTargetOption(selection.targetKey); const resolvedModel = target ? resolveModelSettingForTarget(target, selection.model) : (selection.model ?? mainLoopModel ?? getDefaultMainLoopModelSetting()); const targetChanged = selection.targetKey !== providerSelectionTargetKey; + const targetFastMode = targetChanged ? getInitialProviderFastModeSetting(resolvedModel, { + targetKey: selection.targetKey + }) : undefined; logEvent("tengu_model_command_menu", { action: (selection.model ?? 'default') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, from_model: mainLoopModel as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, @@ -54,6 +58,9 @@ function ModelPickerWrapper(t0) { mainLoopModel: selection.model, mainLoopModelForSession: null, effortValue: selection.effort, + ...(targetChanged ? { + fastMode: targetFastMode + } : {}), ...(targetChanged ? { authVersion: prev.authVersion + 1 } : {}) @@ -63,15 +70,24 @@ function ModelPickerWrapper(t0) { message = message + ` · effort ${chalk.bold(String(selection.effort))}`; } let wasFastModeToggledOn = undefined; - if (isFastModeEnabled()) { + if (targetChanged) { + if (targetFastMode) { + message = message + " \xB7 Fast mode ON"; + wasFastModeToggledOn = true; + } else if (isFastMode) { + wasFastModeToggledOn = false; + } + } else if (isFastModeToggleEnabled()) { clearFastModeCooldown(); - if (!isFastModeSupportedByModel(resolvedModel) && isFastMode) { + if (!isFastModeSupportedForProviderModel(target?.provider, resolvedModel) && isFastMode) { setAppState(prev => ({ ...prev, fastMode: false })); wasFastModeToggledOn = false; - } else if (isFastModeSupportedByModel(resolvedModel) && isFastModeAvailable() && isFastMode) { + } else if (isFastModeSupportedForProviderModel(target?.provider, resolvedModel) && isFastModeToggleAvailable({ + provider: target?.provider + }) && isFastMode) { message = message + " \xB7 Fast mode ON"; wasFastModeToggledOn = true; } @@ -174,16 +190,16 @@ function SetModelAndClose({ })); let message = `Set model to ${chalk.bold(renderModelLabel(modelValue))}`; let wasFastModeToggledOn = undefined; - if (isFastModeEnabled()) { + if (isFastModeToggleEnabled()) { clearFastModeCooldown(); - if (!isFastModeSupportedByModel(modelValue) && isFastMode) { + if (!isFastModeSupportedForModel(modelValue) && isFastMode) { setAppState(prev_0 => ({ ...prev_0, fastMode: false })); wasFastModeToggledOn = false; // Do not update fast mode in settings since this is an automatic downgrade - } else if (isFastModeSupportedByModel(modelValue) && isFastMode) { + } else if (isFastModeSupportedForModel(modelValue) && isFastMode) { message += ` · Fast mode ON`; wasFastModeToggledOn = true; } diff --git a/src/components/LogoV2/CondensedLogo.tsx b/src/components/LogoV2/CondensedLogo.tsx index bc5e97a81..21200c440 100644 --- a/src/components/LogoV2/CondensedLogo.tsx +++ b/src/components/LogoV2/CondensedLogo.tsx @@ -5,12 +5,14 @@ import { useMainLoopModel } from '../../hooks/useMainLoopModel.js'; import { useTerminalSize } from '../../hooks/useTerminalSize.js'; import { stringWidth } from '../../ink/stringWidth.js'; import { Box, Text } from '../../ink.js'; -import { useAppState } from '../../state/AppState.js'; +import { type AppState, useAppState } from '../../state/AppState.js'; import { getEffortSuffix } from '../../utils/effort.js'; import { truncate } from '../../utils/format.js'; import { isFullscreenEnvEnabled } from '../../utils/fullscreen.js'; import { formatModelAndBilling, getLogoDisplayData, truncatePath } from '../../utils/logoV2Utils.js'; +import { formatCodexModelDisplay } from '../../utils/model/codexDisplay.js'; import { renderModelSetting } from '../../utils/model/model.js'; +import { getAPIProvider } from '../../utils/model/providers.js'; import { OffscreenFreeze } from '../OffscreenFreeze.js'; import { AnimatedClawd } from './AnimatedClawd.js'; import { Clawd } from './Clawd.js'; @@ -22,9 +24,15 @@ export function CondensedLogo() { columns } = useTerminalSize(); const agent = useAppState(_temp); + const fastMode = useAppState((s: AppState) => s.fastMode); const effortValue = useAppState(_temp2); const model = useMainLoopModel(); - const modelDisplayName = renderModelSetting(model); + const isCodexProvider = getAPIProvider() === 'codex'; + const modelDisplayName = isCodexProvider ? formatCodexModelDisplay({ + model, + effortValue, + fastMode + }) : renderModelSetting(model); const { version, cwd, @@ -71,7 +79,7 @@ export function CondensedLogo() { useEffect(t2, t3); const textWidth = Math.max(columns - 15, 20); const truncatedVersion = truncate(version, Math.max(textWidth - 13, 6)); - const effortSuffix = getEffortSuffix(model, effortValue); + const effortSuffix = isCodexProvider ? '' : getEffortSuffix(model, effortValue); const { shouldSplit, truncatedModel, diff --git a/src/components/LogoV2/LogoV2.tsx b/src/components/LogoV2/LogoV2.tsx index 883cb9f82..76d0e187d 100644 --- a/src/components/LogoV2/LogoV2.tsx +++ b/src/components/LogoV2/LogoV2.tsx @@ -39,10 +39,11 @@ import { SandboxManager } from 'src/utils/sandbox/sandbox-adapter.js'; import { useShowGuestPassesUpsell, incrementGuestPassesSeenCount } from './GuestPassesUpsell.js'; import { useShowOverageCreditUpsell, incrementOverageCreditUpsellSeenCount, createOverageCreditFeed } from './OverageCreditUpsell.js'; import { plural } from '../../utils/stringUtils.js'; -import { useAppState } from '../../state/AppState.js'; +import { type AppState, useAppState } from '../../state/AppState.js'; import { getEffortSuffix } from '../../utils/effort.js'; import { getAPIProvider } from '../../utils/model/providers.js'; import { useMainLoopModel } from '../../hooks/useMainLoopModel.js'; +import { formatCodexModelDisplay } from '../../utils/model/codexDisplay.js'; import { renderModelSetting } from '../../utils/model/model.js'; const LEFT_PANEL_MAX_WIDTH = 50; export function LogoV2() { @@ -72,6 +73,7 @@ export function LogoV2() { const showGuestPassesUpsell = useShowGuestPassesUpsell(); const showOverageCreditUpsell = useShowOverageCreditUpsell(); const agent = useAppState(_temp); + const fastMode = useAppState((s: AppState) => s.fastMode); const effortValue = useAppState(_temp2); const config = getGlobalConfig(); let changelog; @@ -159,7 +161,12 @@ export function LogoV2() { } useEffect(t7, t8); const model = useMainLoopModel(); - const fullModelDisplayName = renderModelSetting(model); + const isCodexProvider = getAPIProvider() === 'codex'; + const fullModelDisplayName = isCodexProvider ? formatCodexModelDisplay({ + model, + effortValue, + fastMode + }) : renderModelSetting(model); const { version, cwd, @@ -167,7 +174,7 @@ export function LogoV2() { agentName: agentNameFromSettings } = getLogoDisplayData(); const agentName = agent ?? agentNameFromSettings; - const effortSuffix = getEffortSuffix(model, effortValue); + const effortSuffix = isCodexProvider ? '' : getEffortSuffix(model, effortValue); const t9 = fullModelDisplayName + effortSuffix; let t10; if ($[13] !== t9) { diff --git a/src/components/ModelPicker.tsx b/src/components/ModelPicker.tsx index d8bb4b427..13549e9e8 100644 --- a/src/components/ModelPicker.tsx +++ b/src/components/ModelPicker.tsx @@ -4,7 +4,8 @@ import * as React from 'react'; import { useCallback, useMemo, useState } from 'react'; import { useExitOnCtrlCDWithKeybindings } from 'src/hooks/useExitOnCtrlCDWithKeybindings.js'; import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js'; -import { FAST_MODE_MODEL_DISPLAY, isFastModeAvailable, isFastModeCooldown, isFastModeEnabled } from 'src/utils/fastMode.js'; +import { FAST_MODE_MODEL_DISPLAY } from 'src/utils/fastMode.js'; +import { isFastModeCooldownForProvider, isFastModeToggleAvailable, isFastModeToggleEnabled, resolveFastModeProvider } from 'src/utils/providerFastMode.js'; import { Box, Text } from '../ink.js'; import { useKeybindings } from '../keybindings/useKeybinding.js'; import { useAppState, useSetAppState } from '../state/AppState.js'; @@ -230,9 +231,10 @@ export function ModelPicker(t0) { }); if (!skipSettingsWrite) { const userSettings = getSettingsForSource("userSettings") || {}; - const effortLevel = resolvePickerEffortPersistence(effort, getDefaultEffortLevelForOption(value_0), getPersistedEffortSettingForProvider({ + const persistedEffort = getPersistedEffortSettingForProvider({ settings: userSettings - }), hasToggledEffort); + }); + const effortLevel = resolvePickerEffortPersistence(effort, getDefaultEffortLevelForOption(value_0), persistedEffort !== undefined ? convertEffortValueToLevel(persistedEffort) : undefined, hasToggledEffort); const persistable = toPersistableEffort(effortLevel); updateSettingsForSource("userSettings", { ...buildProviderModelSettingsUpdate({ @@ -341,7 +343,7 @@ export function ModelPicker(t0) { } let t25; if ($[67] !== showFastModeNotice) { - t25 = isFastModeEnabled() ? showFastModeNotice ? Fast mode is ON and available with{" "}{FAST_MODE_MODEL_DISPLAY} only (/fast). Switching to other models turn off fast mode. : isFastModeAvailable() && !isFastModeCooldown() ? Use /fast to turn on Fast mode ({FAST_MODE_MODEL_DISPLAY} only). : null : null; + t25 = getFastModeNotice(showFastModeNotice); $[67] = showFastModeNotice; $[68] = t25; } else { @@ -401,7 +403,7 @@ function _temp2(s_0) { return s_0.effortValue; } function _temp(s) { - return isFastModeEnabled() ? s.fastMode : false; + return isFastModeToggleEnabled() ? s.fastMode : false; } function resolveOptionModel(value?: string): string | undefined { if (!value) return undefined; @@ -450,3 +452,16 @@ function getDefaultEffortLevelForOption(value?: string): EffortLevel { const defaultValue = getDefaultEffortForModel(resolved); return defaultValue !== undefined ? convertEffortValueToLevel(defaultValue) : 'high'; } +function getFastModeNotice(showFastModeNotice?: boolean): React.ReactNode { + const provider = resolveFastModeProvider(); + if (!provider || !isFastModeToggleEnabled({ provider })) { + return null; + } + if (provider === 'codex') { + return Use /fast to switch this Codex target between normal and fast{showFastModeNotice ? '. Fast mode is currently ON.' : '.'}; + } + if (showFastModeNotice) { + return Fast mode is ON and available with{" "}{FAST_MODE_MODEL_DISPLAY} only (/fast). Switching to other models turn off fast mode.; + } + return isFastModeToggleAvailable({ provider }) && !isFastModeCooldownForProvider({ provider }) ? Use /fast to turn on Fast mode ({FAST_MODE_MODEL_DISPLAY} only). : null; +} diff --git a/src/components/PromptInput/PromptInput.tsx b/src/components/PromptInput/PromptInput.tsx index 4fea00013..26194f6b1 100644 --- a/src/components/PromptInput/PromptInput.tsx +++ b/src/components/PromptInput/PromptInput.tsx @@ -64,7 +64,7 @@ import type { EffortLevel } from '../../utils/effort.js'; import { env } from '../../utils/env.js'; import { errorMessage } from '../../utils/errors.js'; import { isBilledAsExtraUsage } from '../../utils/extraUsage.js'; -import { getFastModeUnavailableReason, isFastModeAvailable, isFastModeCooldown, isFastModeEnabled, isFastModeSupportedByModel } from '../../utils/fastMode.js'; +import { getFastModeUnavailableReasonForProvider, isFastModeCooldownForProvider, isFastModeSupportedForModel, isFastModeToggleAvailable, isFastModeToggleEnabled, resolveFastModeProvider, shouldShowFastModeIcon } from '../../utils/providerFastMode.js'; import { isFullscreenEnvEnabled } from '../../utils/fullscreen.js'; import type { PromptInputHelpers } from '../../utils/handlePromptSubmit.js'; import { extractDraggedFilePaths } from '../../utils/dragDropPaths.js'; @@ -337,7 +337,7 @@ function PromptInput({ const mainLoopModel_ = useAppState(s => s.mainLoopModel); const mainLoopModelForSession = useAppState(s => s.mainLoopModelForSession); const thinkingEnabled = useAppState(s => s.thinkingEnabled); - const isFastMode = useAppState(s => isFastModeEnabled() ? s.fastMode : false); + const isFastMode = useAppState(s => isFastModeToggleEnabled() ? s.fastMode : false); const effortValue = useAppState(s => s.effortValue); const viewedTeammate = getViewedTeammateTask(store.getState()); const viewingAgentName = viewedTeammate?.identity.agentName; @@ -1721,7 +1721,7 @@ function PromptInput({ // Fast mode keybinding is only active when fast mode is enabled and available useKeybinding('chat:fastMode', handleFastModePicker, { context: 'Chat', - isActive: !isModalOverlayActive && isFastModeEnabled() && isFastModeAvailable() + isActive: !isModalOverlayActive && isFastModeToggleEnabled() && isFastModeToggleAvailable() }); // Handle help:dismiss keybinding (ESC closes help menu) @@ -1999,8 +1999,8 @@ function PromptInput({ } }); const swarmBanner = useSwarmBanner(); - const fastModeCooldown = isFastModeEnabled() ? isFastModeCooldown() : false; - const showFastIcon = isFastModeEnabled() ? isFastMode && (isFastModeAvailable() || fastModeCooldown) : false; + const fastModeCooldown = isFastModeCooldownForProvider(); + const showFastIcon = shouldShowFastModeIcon(isFastMode ?? false); const showFastIconHint = useShowFastIconHint(showFastIcon ?? false); // Show effort notification on startup and when effort changes. @@ -2060,7 +2060,7 @@ function PromptInput({ const handleModelSelect = useCallback((model: string | null, _effort: EffortLevel | undefined) => { let wasFastModeDisabled = false; setAppState(prev => { - wasFastModeDisabled = isFastModeEnabled() && !isFastModeSupportedByModel(model) && !!prev.fastMode; + wasFastModeDisabled = isFastModeToggleEnabled() && !isFastModeSupportedForModel(model) && !!prev.fastMode; return { ...prev, mainLoopModel: model, @@ -2099,7 +2099,7 @@ function PromptInput({ const modelPickerElement = useMemo(() => { if (!showModelPicker) return null; return - + ; }, [showModelPicker, mainLoopModel_, mainLoopModelForSession, handleModelSelect, handleModelCancel]); const handleFastModeSelect = useCallback((result?: string) => { @@ -2118,7 +2118,7 @@ function PromptInput({ const fastModePickerElement = useMemo(() => { if (!showFastModePicker) return null; return - + ; }, [showFastModePicker, handleFastModeSelect]); diff --git a/src/components/PromptInput/PromptInputHelpMenu.tsx b/src/components/PromptInput/PromptInputHelpMenu.tsx index 87d732435..b04c7e4fb 100644 --- a/src/components/PromptInput/PromptInputHelpMenu.tsx +++ b/src/components/PromptInput/PromptInputHelpMenu.tsx @@ -6,7 +6,7 @@ import { getPlatform } from 'src/utils/platform.js'; import { isKeybindingCustomizationEnabled } from '../../keybindings/loadUserBindings.js'; import { useShortcutDisplay } from '../../keybindings/useShortcutDisplay.js'; import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js'; -import { isFastModeAvailable, isFastModeEnabled } from '../../utils/fastMode.js'; +import { isFastModeToggleAvailable, isFastModeToggleEnabled } from '../../utils/providerFastMode.js'; import { getNewlineInstructions } from './utils.js'; /** Format a shortcut for display in the help menu (e.g., "ctrl+o" → "ctrl + o") */ @@ -293,7 +293,7 @@ export function PromptInputHelpMenu(props) { } let t40; if ($[73] !== dimColor || $[74] !== fastModeShortcut) { - t40 = isFastModeEnabled() && isFastModeAvailable() && {fastModeShortcut} to toggle fast mode; + t40 = isFastModeToggleEnabled() && isFastModeToggleAvailable() && {fastModeShortcut} to toggle fast mode; $[73] = dimColor; $[74] = fastModeShortcut; $[75] = t40; diff --git a/src/components/StartupScreen.ts b/src/components/StartupScreen.ts index 59f89739a..619940afc 100644 --- a/src/components/StartupScreen.ts +++ b/src/components/StartupScreen.ts @@ -5,9 +5,17 @@ * Addresses: https://github.com/Gitlawb/openclaude/issues/55 */ -import { isLocalProviderUrl, resolveProviderRequest } from '../services/api/providerConfig.js' +import { + DEFAULT_CODEX_BASE_URL, + isLocalProviderUrl, + resolveProviderRequest, +} from '../services/api/providerConfig.js' +import { getPersistedEffortSettingForProvider } from '../utils/model/providerModelSettings.js' +import { getAPIProvider } from '../utils/model/providers.js' +import { formatCodexModelDisplay } from '../utils/model/codexDisplay.js' import { getLocalOpenAICompatibleProviderLabel } from '../utils/providerDiscovery.js' -import { getSettings_DEPRECATED } from '../utils/settings/settings.js' +import { getInitialProviderFastModeSetting } from '../utils/providerFastMode.js' +import { getInitialSettings, getSettings_DEPRECATED } from '../utils/settings/settings.js' import { parseUserSpecifiedModel } from '../utils/model/model.js' declare const MACRO: { VERSION: string; DISPLAY_VERSION?: string } @@ -84,10 +92,12 @@ const LOGO_CLAUDE = [ // ─── Provider detection ─────────────────────────────────────────────────────── function detectProvider(): { name: string; model: string; baseUrl: string; isLocal: boolean } { + const apiProvider = getAPIProvider() const useGemini = process.env.CLAUDE_CODE_USE_GEMINI === '1' || process.env.CLAUDE_CODE_USE_GEMINI === 'true' const useGithub = process.env.CLAUDE_CODE_USE_GITHUB === '1' || process.env.CLAUDE_CODE_USE_GITHUB === 'true' const useOpenAI = process.env.CLAUDE_CODE_USE_OPENAI === '1' || process.env.CLAUDE_CODE_USE_OPENAI === 'true' const useMistral = process.env.CLAUDE_CODE_USE_MISTRAL === '1' || process.env.CLAUDE_CODE_USE_MISTRAL === 'true' + const initialSettings = getInitialSettings() if (useGemini) { const model = process.env.GEMINI_MODEL || 'gemini-2.0-flash' @@ -108,6 +118,27 @@ function detectProvider(): { name: string; model: string; baseUrl: string; isLoc return { name: 'GitHub Copilot', model, baseUrl, isLocal: false } } + if (apiProvider === 'codex') { + const model = process.env.OPENAI_MODEL || 'codexplan' + const baseUrl = process.env.OPENAI_BASE_URL || DEFAULT_CODEX_BASE_URL + return { + name: 'Codex', + model: formatCodexModelDisplay({ + model, + effortValue: getPersistedEffortSettingForProvider({ + settings: initialSettings, + provider: 'codex', + }), + fastMode: getInitialProviderFastModeSetting(model, { + provider: 'codex', + settings: initialSettings, + }), + }), + baseUrl, + isLocal: isLocalProviderUrl(baseUrl), + } + } + if (useOpenAI) { const rawModel = process.env.OPENAI_MODEL || 'gpt-4o' const resolvedRequest = resolveProviderRequest({ @@ -128,13 +159,11 @@ function detectProvider(): { name: string; model: string; baseUrl: string; isLoc else if (/azure/i.test(baseUrl)) name = 'Azure OpenAI' else if (/llama/i.test(rawModel)) name = 'Meta Llama' else if (isLocal) name = getLocalOpenAICompatibleProviderLabel(baseUrl) - - // Resolve model alias to actual model name + reasoning effort let displayModel = resolvedRequest.resolvedModel if (resolvedRequest.reasoning?.effort) { displayModel = `${displayModel} (${resolvedRequest.reasoning.effort})` } - + return { name, model: displayModel, baseUrl, isLocal } } diff --git a/src/main.tsx b/src/main.tsx index b9ae6cc8f..bbc69a602 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -53,10 +53,11 @@ import { getSubscriptionType, isClaudeAISubscriber, prefetchAwsCredentialsAndBed import { checkHasTrustDialogAccepted, getGlobalConfig, getRemoteControlAtStartup, isAutoUpdaterDisabled, saveGlobalConfig } from './utils/config.js'; import { seedEarlyInput, stopCapturingEarlyInput } from './utils/earlyInput.js'; import { getInitialEffortSetting, parseEffortValue } from './utils/effort.js'; -import { getInitialFastModeSetting, isFastModeEnabled, prefetchFastModeStatus, resolveFastModeStatusFromCache } from './utils/fastMode.js'; +import { prefetchFastModeStatus, resolveFastModeStatusFromCache } from './utils/fastMode.js'; import { applyConfigEnvironmentVariables } from './utils/managedEnv.js'; import { createSystemMessage, createUserMessage } from './utils/messages.js'; import { getPlatform } from './utils/platform.js'; +import { getInitialProviderFastModeSetting } from './utils/providerFastMode.js'; import { getBaseRenderOptions } from './utils/renderOptions.js'; import { getSessionIngressAuthToken } from './utils/sessionIngressAuth.js'; import { settingsChangeDetector } from './utils/settings/changeDetector.js'; @@ -2624,8 +2625,8 @@ async function run(): Promise { toolPermissionContext, providerSelectionTargetKey: initialProviderSelectionTargetKey, effortValue: parseEffortValue(options.effort) ?? getInitialEffortSetting(), - ...(isFastModeEnabled() && { - fastMode: getInitialFastModeSetting(effectiveModel ?? null) + fastMode: getInitialProviderFastModeSetting(effectiveModel ?? null, { + targetKey: initialProviderSelectionTargetKey }), ...(isAdvisorEnabled() && advisorModel && { advisorModel @@ -3017,7 +3018,9 @@ async function run(): Promise { } : null, effortValue: parseEffortValue(options.effort) ?? getInitialEffortSetting(), activeOverlays: new Set(), - fastMode: getInitialFastModeSetting(resolvedInitialModel), + fastMode: getInitialProviderFastModeSetting(resolvedInitialModel, { + targetKey: getCurrentProviderSelectionTarget().targetKey + }), ...(isAdvisorEnabled() && advisorModel && { advisorModel }), diff --git a/src/services/api/claude.ts b/src/services/api/claude.ts index 053bdb6c2..dae2517dd 100644 --- a/src/services/api/claude.ts +++ b/src/services/api/claude.ts @@ -173,6 +173,7 @@ import { isFastModeEnabled, isFastModeSupportedByModel, } from 'src/utils/fastMode.js' +import { getCodexServiceTierForFastMode } from 'src/utils/providerFastMode.js' import { returnValue } from 'src/utils/generators.js' import { headlessProfilerCheckpoint } from 'src/utils/headlessProfiler.js' import { isMcpInstructionsDeltaEnabled } from 'src/utils/mcpInstructionsDelta.js' @@ -1412,6 +1413,11 @@ async function* queryModel( !isFastModeCooldown() && isFastModeSupportedByModel(options.model) && !!options.fastMode + const codexServiceTier = getCodexServiceTierForFastMode({ + enabled: options.fastMode, + provider: getAPIProvider(), + }) + const isProviderFastMode = isFastMode || codexServiceTier !== undefined // Sticky-on latches for dynamic beta headers. Each header, once first // sent, keeps being sent for the rest of the session so mid-session @@ -1514,7 +1520,7 @@ async function* queryModel( options.model, newContext, messagesForAPI, - isFastMode, + isProviderFastMode, ) const startIncludingRetries = Date.now() @@ -1771,7 +1777,7 @@ async function* queryModel( queryTracking: options.queryTracking, thinkingType: logThinkingType, effortValue: logEffortValue, - fastMode: isFastMode, + fastMode: isProviderFastMode, previousRequestId, }) }) @@ -1789,7 +1795,7 @@ async function* queryModel( let maxOutputTokens = 0 let responseHeaders: globalThis.Headers | undefined = undefined let research: unknown = undefined - let isFastModeRequest = isFastMode // Keep separate state as it may change if falling back + let isFastModeRequest = isProviderFastMode // Keep separate state as it may change if falling back let isAdvisorInProgress = false try { @@ -1799,13 +1805,14 @@ async function* queryModel( getAnthropicClient({ maxRetries: 0, // Disabled auto-retry in favor of manual implementation model: options.model, + serviceTier: codexServiceTier, fetchOverride: options.fetchOverride, source: options.querySource, providerOverride: options.providerOverride, }), async (anthropic, attempt, context) => { attemptNumber = attempt - isFastModeRequest = context.fastMode ?? false + isFastModeRequest = context.fastMode ?? codexServiceTier !== undefined start = Date.now() attemptStartTimes.push(start) // Client has been created by withRetry's getClient() call. This fires diff --git a/src/services/api/client.ts b/src/services/api/client.ts index dbeb86510..5a0f0a389 100644 --- a/src/services/api/client.ts +++ b/src/services/api/client.ts @@ -1,5 +1,6 @@ import Anthropic, { type ClientOptions } from '@anthropic-ai/sdk' import { randomUUID } from 'crypto' +import type { ProviderServiceTier } from './providerConfig.js' import { checkAndRefreshOAuthTokenIfNeeded, getAnthropicApiKey, @@ -93,6 +94,7 @@ export async function getAnthropicClient({ apiKey, maxRetries, model, + serviceTier, fetchOverride, source, providerOverride, @@ -100,6 +102,7 @@ export async function getAnthropicClient({ apiKey?: string maxRetries: number model?: string + serviceTier?: ProviderServiceTier fetchOverride?: ClientOptions['fetch'] source?: string providerOverride?: { model: string; baseURL: string; apiKey: string } @@ -171,6 +174,7 @@ export async function getAnthropicClient({ defaultHeaders: safeHeaders, maxRetries, timeout: parseInt(process.env.API_TIMEOUT_MS || String(600 * 1000), 10), + serviceTier, providerOverride, }) as unknown as Anthropic } @@ -185,6 +189,7 @@ export async function getAnthropicClient({ defaultHeaders, maxRetries, timeout: parseInt(process.env.API_TIMEOUT_MS || String(600 * 1000), 10), + serviceTier, }) as unknown as Anthropic } if (isEnvTruthy(process.env.CLAUDE_CODE_USE_BEDROCK)) { diff --git a/src/services/api/codexShim.test.ts b/src/services/api/codexShim.test.ts index d2e39aae8..6c51096b1 100644 --- a/src/services/api/codexShim.test.ts +++ b/src/services/api/codexShim.test.ts @@ -7,9 +7,11 @@ import { convertAnthropicMessagesToResponsesInput, convertCodexResponseToAnthropicMessage, convertToolsToResponsesTools, + performCodexRequest, } from './codexShim.js' const tempDirs: string[] = [] +const originalFetch = globalThis.fetch const originalEnv = { OPENAI_BASE_URL: process.env.OPENAI_BASE_URL, OPENAI_API_BASE: process.env.OPENAI_API_BASE, @@ -29,6 +31,7 @@ afterEach(() => { if (originalEnv.OPENAI_MODEL === undefined) delete process.env.OPENAI_MODEL else process.env.OPENAI_MODEL = originalEnv.OPENAI_MODEL + globalThis.fetch = originalFetch while (tempDirs.length > 0) { const dir = tempDirs.pop() @@ -104,6 +107,17 @@ describe('Codex provider config', () => { expect(resolved.baseUrl).toBe('https://chatgpt.com/backend-api/codex') }) + test('applies explicit Codex service tiers to resolved requests', async () => { + const { resolveProviderRequest } = await importFreshProviderConfigModule() + const resolved = resolveProviderRequest({ + model: 'gpt-5.4', + serviceTierOverride: 'priority', + }) + + expect(resolved.transport).toBe('codex_responses') + expect(resolved.serviceTier).toBe('priority') + }) + test('does not force Codex transport when a local non-Codex base URL is explicit', async () => { const { resolveProviderRequest } = await importFreshProviderConfigModule() const resolved = resolveProviderRequest({ @@ -219,6 +233,42 @@ describe('Codex provider config', () => { }) describe('Codex request translation', () => { + test('forwards service tier to the Codex responses payload', async () => { + let requestBody: Record | undefined + globalThis.fetch = (async (_input, init) => { + requestBody = JSON.parse(String(init?.body ?? '{}')) as Record + return new Response('event: response.completed\ndata: {"response":{"status":"completed"}}\n\n', { + status: 200, + headers: { + 'content-type': 'text/event-stream', + }, + }) + }) as typeof fetch + + await performCodexRequest({ + request: { + transport: 'codex_responses', + requestedModel: 'gpt-5.4', + resolvedModel: 'gpt-5.4', + baseUrl: 'https://chatgpt.com/backend-api/codex', + serviceTier: 'priority', + }, + credentials: { + apiKey: 'test-token', + source: 'env', + }, + params: { + model: 'gpt-5.4', + messages: [], + stream: true, + max_tokens: 256, + } as any, + defaultHeaders: {}, + }) + + expect(requestBody?.service_tier).toBe('priority') + }) + test('normalizes optional parameters into strict Responses schemas', () => { const tools = convertToolsToResponsesTools([ { diff --git a/src/services/api/codexShim.ts b/src/services/api/codexShim.ts index 4f3995dc7..cd1d24fee 100644 --- a/src/services/api/codexShim.ts +++ b/src/services/api/codexShim.ts @@ -535,6 +535,10 @@ export async function performCodexRequest(options: { body.reasoning = options.request.reasoning } + if (options.request.serviceTier) { + body.service_tier = options.request.serviceTier + } + const isTargetModel = options.request.resolvedModel?.toLowerCase().includes('gpt') || options.request.resolvedModel?.toLowerCase().includes('codex') diff --git a/src/services/api/openaiShim.ts b/src/services/api/openaiShim.ts index 8beb115e6..514acb968 100644 --- a/src/services/api/openaiShim.ts +++ b/src/services/api/openaiShim.ts @@ -52,6 +52,7 @@ import { resolveRuntimeCodexCredentials, resolveProviderRequest, getGithubEndpointType, + type ProviderServiceTier, } from './providerConfig.js' import { sanitizeSchemaForOpenAICompat } from '../../utils/schemaSanitizer.js' import { redactSecretValueForDisplay } from '../../utils/providerProfile.js' @@ -1096,11 +1097,13 @@ class OpenAIShimStream { class OpenAIShimMessages { private defaultHeaders: Record private reasoningEffort?: 'low' | 'medium' | 'high' | 'xhigh' + private serviceTier?: ProviderServiceTier private providerOverride?: { model: string; baseURL: string; apiKey: string } - constructor(defaultHeaders: Record, reasoningEffort?: 'low' | 'medium' | 'high' | 'xhigh', providerOverride?: { model: string; baseURL: string; apiKey: string }) { + constructor(defaultHeaders: Record, reasoningEffort?: 'low' | 'medium' | 'high' | 'xhigh', serviceTier?: ProviderServiceTier, providerOverride?: { model: string; baseURL: string; apiKey: string }) { this.defaultHeaders = filterAnthropicHeaders(defaultHeaders) this.reasoningEffort = reasoningEffort + this.serviceTier = serviceTier this.providerOverride = providerOverride } @@ -1113,7 +1116,7 @@ class OpenAIShimMessages { let httpResponse: Response | undefined const promise = (async () => { - const request = resolveProviderRequest({ model: self.providerOverride?.model ?? params.model, baseUrl: self.providerOverride?.baseURL, reasoningEffortOverride: self.reasoningEffort }) + const request = resolveProviderRequest({ model: self.providerOverride?.model ?? params.model, baseUrl: self.providerOverride?.baseURL, reasoningEffortOverride: self.reasoningEffort, serviceTierOverride: self.serviceTier }) const response = await self._doRequest(request, params, options) httpResponse = response @@ -1662,10 +1665,12 @@ class OpenAIShimMessages { class OpenAIShimBeta { messages: OpenAIShimMessages reasoningEffort?: 'low' | 'medium' | 'high' | 'xhigh' + serviceTier?: ProviderServiceTier - constructor(defaultHeaders: Record, reasoningEffort?: 'low' | 'medium' | 'high' | 'xhigh', providerOverride?: { model: string; baseURL: string; apiKey: string }) { - this.messages = new OpenAIShimMessages(defaultHeaders, reasoningEffort, providerOverride) + constructor(defaultHeaders: Record, reasoningEffort?: 'low' | 'medium' | 'high' | 'xhigh', serviceTier?: ProviderServiceTier, providerOverride?: { model: string; baseURL: string; apiKey: string }) { + this.messages = new OpenAIShimMessages(defaultHeaders, reasoningEffort, serviceTier, providerOverride) this.reasoningEffort = reasoningEffort + this.serviceTier = serviceTier } } @@ -1674,6 +1679,7 @@ export function createOpenAIShimClient(options: { maxRetries?: number timeout?: number reasoningEffort?: 'low' | 'medium' | 'high' | 'xhigh' + serviceTier?: ProviderServiceTier providerOverride?: { model: string; baseURL: string; apiKey: string } }): unknown { hydrateGeminiAccessTokenFromSecureStorage() @@ -1708,7 +1714,7 @@ export function createOpenAIShimClient(options: { const beta = new OpenAIShimBeta({ ...(options.defaultHeaders ?? {}), - }, options.reasoningEffort, options.providerOverride) + }, options.reasoningEffort, options.serviceTier, options.providerOverride) return { beta, diff --git a/src/services/api/providerConfig.ts b/src/services/api/providerConfig.ts index 9ea3a9c8a..fce5b181e 100644 --- a/src/services/api/providerConfig.ts +++ b/src/services/api/providerConfig.ts @@ -68,6 +68,7 @@ const CODEX_ALIAS_MODELS: Record< type CodexAlias = keyof typeof CODEX_ALIAS_MODELS type ReasoningEffort = 'low' | 'medium' | 'high' | 'xhigh' +export type ProviderServiceTier = 'priority' const OPENAI_CODEX_SHORTCUT_ALIASES = new Set(['codexplan', 'codexspark']) @@ -81,6 +82,7 @@ export type ResolvedProviderRequest = { reasoning?: { effort: ReasoningEffort } + serviceTier?: ProviderServiceTier } export type ResolvedCodexCredentials = { @@ -350,6 +352,7 @@ export function resolveProviderRequest(options?: { baseUrl?: string fallbackModel?: string reasoningEffortOverride?: ReasoningEffort + serviceTierOverride?: ProviderServiceTier }): ResolvedProviderRequest { const isGithubMode = isEnvTruthy(process.env.CLAUDE_CODE_USE_GITHUB) const isMistralMode = isEnvTruthy(process.env.CLAUDE_CODE_USE_MISTRAL) @@ -426,6 +429,7 @@ export function resolveProviderRequest(options?: { const reasoning = options?.reasoningEffortOverride ? { effort: options.reasoningEffortOverride } : descriptor.reasoning + const serviceTier = options?.serviceTierOverride const defaultBaseUrl = transport === 'codex_responses' ? (isGithubMode ? GITHUB_COPILOT_BASE_URL : DEFAULT_CODEX_BASE_URL) @@ -437,6 +441,7 @@ export function resolveProviderRequest(options?: { resolvedModel, baseUrl: (rawBaseUrl ?? defaultBaseUrl).replace(/\/+$/, ''), reasoning, + serviceTier, } } diff --git a/src/utils/messages/systemInit.ts b/src/utils/messages/systemInit.ts index 4a0ac522c..3274a245f 100644 --- a/src/utils/messages/systemInit.ts +++ b/src/utils/messages/systemInit.ts @@ -13,7 +13,7 @@ import { } from 'src/tools/AgentTool/constants.js' import { getAnthropicApiKeyWithSource } from '../auth.js' import { getCwd } from '../cwd.js' -import { getFastModeState } from '../fastMode.js' +import { getProviderFastModeState } from '../providerFastMode.js' import { getSettings_DEPRECATED } from '../settings/settings.js' // TODO(next-minor): remove this translation once SDK consumers have migrated @@ -91,6 +91,9 @@ export function buildSystemInitMessage(inputs: SystemInitInputs): SDKMessage { require('../udsMessaging.js').getUdsMessagingSocketPath() /* eslint-enable @typescript-eslint/no-require-imports */ } - initMessage.fast_mode_state = getFastModeState(inputs.model, inputs.fastMode) + initMessage.fast_mode_state = getProviderFastModeState( + inputs.model, + inputs.fastMode, + ) return initMessage } diff --git a/src/utils/model/codexDisplay.test.ts b/src/utils/model/codexDisplay.test.ts new file mode 100644 index 000000000..4bb3bcd3c --- /dev/null +++ b/src/utils/model/codexDisplay.test.ts @@ -0,0 +1,20 @@ +import { expect, test } from 'bun:test' + +import { formatCodexModelDisplay } from './codexDisplay.js' + +test('formats codex aliases using resolved model defaults', () => { + expect(formatCodexModelDisplay({ model: 'codexplan' })).toBe('gpt-5.4 high') + expect(formatCodexModelDisplay({ model: 'codexspark' })).toBe( + 'gpt-5.3-codex-spark', + ) +}) + +test('formats explicit codex effort and fast mode like native Codex', () => { + expect( + formatCodexModelDisplay({ + model: 'codexplan', + effortValue: 'xhigh', + fastMode: true, + }), + ).toBe('gpt-5.4 xhigh fast') +}) diff --git a/src/utils/model/codexDisplay.ts b/src/utils/model/codexDisplay.ts new file mode 100644 index 000000000..e68f14d70 --- /dev/null +++ b/src/utils/model/codexDisplay.ts @@ -0,0 +1,39 @@ +import { resolveProviderRequest } from '../../services/api/providerConfig.js' +import { + isOpenAIEffortLevel, + resolveAppliedEffort, + type EffortValue, +} from '../effort.js' + +function getCodexDisplayEffort( + model: string, + effortValue: EffortValue | undefined, +): 'low' | 'medium' | 'high' | 'xhigh' | undefined { + const resolvedEffort = resolveAppliedEffort(model, effortValue) + return typeof resolvedEffort === 'string' && isOpenAIEffortLevel(resolvedEffort) + ? resolvedEffort + : undefined +} + +export function formatCodexModelDisplay(options: { + model: string + effortValue?: EffortValue + fastMode?: boolean +}): string { + const resolved = resolveProviderRequest({ + model: options.model, + reasoningEffortOverride: getCodexDisplayEffort( + options.model, + options.effortValue, + ), + }) + + const parts = [resolved.resolvedModel] + if (resolved.reasoning?.effort) { + parts.push(resolved.reasoning.effort) + } + if (options.fastMode) { + parts.push('fast') + } + return parts.join(' ') +} diff --git a/src/utils/model/providerModelSettings.test.ts b/src/utils/model/providerModelSettings.test.ts index de17bed66..c2bc84182 100644 --- a/src/utils/model/providerModelSettings.test.ts +++ b/src/utils/model/providerModelSettings.test.ts @@ -5,6 +5,7 @@ import { buildProviderModelSettingsUpdate, getPersistedEffortSettingForProvider, getPersistedModelSettingForProvider, + getPersistedServiceTierForProvider, resolveProviderSelectionTarget, } from './providerModelSettings.js' @@ -82,6 +83,33 @@ test('provider-target effort settings override the legacy global effort level', ).toBe('low') }) +test('provider-target service tier settings are scoped to the active codex target', () => { + const settings: SettingsJson = { + providerTargetSelections: { + codex: { + serviceTier: 'fast', + }, + openai: { + serviceTier: 'fast', + }, + }, + } + + expect( + getPersistedServiceTierForProvider({ + settings, + provider: 'codex', + }), + ).toBe('fast') + + expect( + getPersistedServiceTierForProvider({ + settings, + provider: 'github', + }), + ).toBeUndefined() +}) + test('buildProviderModelSettingsUpdate writes provider-target scoped model and effort patches', () => { const settings: SettingsJson = { model: 'gpt-4o', @@ -117,3 +145,26 @@ test('buildProviderModelSettingsUpdate writes provider-target scoped model and e }, }) }) + +test('buildProviderModelSettingsUpdate writes provider-target service tier patches', () => { + const settings: SettingsJson = { + providerTargetSelections: { + codex: { + serviceTier: 'fast', + }, + }, + } + + const update = buildProviderModelSettingsUpdate({ + settings, + provider: 'codex', + targetKey: 'profile:provider_456', + serviceTier: 'fast', + }) + + expect(update.providerTargetSelections).toEqual({ + 'profile:provider_456': { + serviceTier: 'fast', + }, + }) +}) diff --git a/src/utils/model/providerModelSettings.ts b/src/utils/model/providerModelSettings.ts index 15a3997e6..765e55e1f 100644 --- a/src/utils/model/providerModelSettings.ts +++ b/src/utils/model/providerModelSettings.ts @@ -7,10 +7,12 @@ import { getActiveProviderProfileSelectionTargetKey } from '../providerProfiles. export type ProviderModelSettings = Partial> export type PersistedEffortLevel = EffortLevel | OpenAIEffortLevel +export type PersistedServiceTier = 'fast' export type ProviderTargetSelection = { model?: string effortLevel?: PersistedEffortLevel + serviceTier?: PersistedServiceTier } export type ProviderTargetSelections = Record< @@ -129,6 +131,12 @@ function getPersistedEffortLevel( return undefined } +function getPersistedServiceTier( + value: unknown, +): PersistedServiceTier | undefined { + return value === 'fast' ? value : undefined +} + export function resolveSettingsModelProvider(options?: { provider?: APIProvider model?: string @@ -202,6 +210,7 @@ function buildProviderTargetSelectionPatch(options: { targetKey: string model?: string | null effortLevel?: PersistedEffortLevel | null + serviceTier?: PersistedServiceTier | null }): ProviderTargetSelection | undefined { const current = getProviderTargetSelection(options.settings, options.targetKey) const next: ProviderTargetSelection = current ? { ...current } : {} @@ -227,6 +236,16 @@ function buildProviderTargetSelectionPatch(options: { } } + if (Object.prototype.hasOwnProperty.call(options, 'serviceTier')) { + touched = true + const serviceTier = getPersistedServiceTier(options.serviceTier) + if (serviceTier) { + next.serviceTier = serviceTier + } else { + delete next.serviceTier + } + } + if (!touched) { return undefined } @@ -298,11 +317,34 @@ export function getPersistedEffortSettingForProvider(options?: { return getPersistedEffortLevel(settings?.effortLevel) } +export function getPersistedServiceTierForProvider(options?: { + settings?: SettingsJson + provider?: APIProvider + model?: string + baseUrl?: string + targetKey?: string + profileId?: string +}): PersistedServiceTier | undefined { + const settings = options?.settings + const target = resolveProviderSelectionTarget({ + provider: options?.provider, + model: options?.model, + baseUrl: options?.baseUrl, + targetKey: options?.targetKey, + profileId: options?.profileId, + }) + + return getPersistedServiceTier( + getProviderTargetSelection(settings, target.targetKey)?.serviceTier, + ) +} + export function buildProviderModelSettingsUpdate(options: { settings?: SettingsJson provider?: APIProvider model?: string | null effortLevel?: PersistedEffortLevel | null + serviceTier?: PersistedServiceTier | null baseUrl?: string targetKey?: string profileId?: string @@ -326,6 +368,9 @@ export function buildProviderModelSettingsUpdate(options: { ...(Object.prototype.hasOwnProperty.call(options, 'effortLevel') ? { effortLevel: options.effortLevel } : {}), + ...(Object.prototype.hasOwnProperty.call(options, 'serviceTier') + ? { serviceTier: options.serviceTier } + : {}), }) const update: Partial = {} @@ -355,11 +400,12 @@ export function buildProviderModelSettingsUpdate(options: { } as SettingsJson['providerTargetSelections'] } else if ( Object.prototype.hasOwnProperty.call(options, 'model') || - Object.prototype.hasOwnProperty.call(options, 'effortLevel') + Object.prototype.hasOwnProperty.call(options, 'effortLevel') || + Object.prototype.hasOwnProperty.call(options, 'serviceTier') ) { update.providerTargetSelections = { [target.targetKey]: undefined, - } as SettingsJson['providerTargetSelections'] + } as unknown as SettingsJson['providerTargetSelections'] } return update diff --git a/src/utils/providerFastMode.ts b/src/utils/providerFastMode.ts new file mode 100644 index 000000000..788a0db8c --- /dev/null +++ b/src/utils/providerFastMode.ts @@ -0,0 +1,205 @@ +import type { ModelSetting } from './model/model.js' +import type { APIProvider } from './model/providers.js' +import { getAPIProvider } from './model/providers.js' +import { + getPersistedServiceTierForProvider, + type PersistedServiceTier, +} from './model/providerModelSettings.js' +import { + getCurrentProviderSelectionTarget, + resolveProviderSelectionTargetOption, +} from './model/providerTargets.js' +import { getInitialSettings } from './settings/settings.js' +import type { SettingsJson } from './settings/types.js' +import { isEnvTruthy } from './envUtils.js' +import { + getFastModeState as getAnthropicFastModeState, + getFastModeUnavailableReason as getAnthropicFastModeUnavailableReason, + getInitialFastModeSetting as getAnthropicInitialFastModeSetting, + isFastModeAvailable as isAnthropicFastModeAvailable, + isFastModeCooldown as isAnthropicFastModeCooldown, + isFastModeEnabled as isAnthropicFastModeEnabled, + isFastModeSupportedByModel as isAnthropicFastModeSupportedByModel, +} from './fastMode.js' + +export type FastModeProvider = 'firstParty' | 'codex' +export type CodexServiceTier = 'priority' + +function asFastModeProvider( + provider: APIProvider | undefined, +): FastModeProvider | null { + return provider === 'firstParty' || provider === 'codex' ? provider : null +} + +export function resolveFastModeProvider(options?: { + provider?: APIProvider + targetKey?: string +}): FastModeProvider | null { + const targetProvider = options?.targetKey + ? resolveProviderSelectionTargetOption(options.targetKey)?.provider + : undefined + return asFastModeProvider(targetProvider ?? options?.provider ?? getAPIProvider()) +} + +export function isFastModeToggleEnabled(options?: { + provider?: APIProvider + targetKey?: string +}): boolean { + if (isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_FAST_MODE)) { + return false + } + return resolveFastModeProvider(options) !== null +} + +export function isFastModeToggleAvailable(options?: { + provider?: APIProvider + targetKey?: string +}): boolean { + const provider = resolveFastModeProvider(options) + if (provider === 'firstParty') { + return isAnthropicFastModeEnabled() && isAnthropicFastModeAvailable() + } + if (provider === 'codex') { + return isFastModeToggleEnabled(options) + } + return false +} + +export function isFastModeCooldownForProvider(options?: { + provider?: APIProvider + targetKey?: string +}): boolean { + return resolveFastModeProvider(options) === 'firstParty' + ? isAnthropicFastModeCooldown() + : false +} + +export function getFastModeUnavailableReasonForProvider(options?: { + provider?: APIProvider + targetKey?: string +}): string | null { + const provider = resolveFastModeProvider(options) + if (provider === 'firstParty') { + return getAnthropicFastModeUnavailableReason() + } + if (provider === 'codex') { + return isFastModeToggleEnabled(options) ? null : 'Fast mode is not available' + } + return 'Fast mode is not available for this provider' +} + +export function isFastModeSupportedForProviderModel( + provider: APIProvider | undefined, + model: ModelSetting, +): boolean { + if (provider === 'firstParty') { + return isAnthropicFastModeSupportedByModel(model) + } + if (provider === 'codex') { + return !isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_FAST_MODE) + } + return false +} + +export function isFastModeSupportedForModel( + model: ModelSetting, + options?: { + provider?: APIProvider + targetKey?: string + }, +): boolean { + return isFastModeSupportedForProviderModel( + resolveFastModeProvider(options) ?? undefined, + model, + ) +} + +export function shouldShowFastModeIcon( + enabled: boolean | undefined, + options?: { + provider?: APIProvider + targetKey?: string + }, +): boolean { + if (!enabled) { + return false + } + + const provider = resolveFastModeProvider(options) + if (provider === 'firstParty') { + return isFastModeToggleAvailable(options) || isFastModeCooldownForProvider(options) + } + return provider === 'codex' && isFastModeToggleEnabled(options) +} + +export function getInitialProviderFastModeSetting( + model: ModelSetting, + options?: { + provider?: APIProvider + targetKey?: string + settings?: SettingsJson + }, +): boolean { + const provider = resolveFastModeProvider(options) + if (provider === 'firstParty') { + return getAnthropicInitialFastModeSetting(model) + } + if (provider === 'codex') { + const currentTarget = getCurrentProviderSelectionTarget() + return ( + getPersistedServiceTierForProvider({ + settings: options?.settings ?? getInitialSettings(), + provider, + targetKey: + options?.targetKey ?? + (currentTarget.provider === 'codex' ? currentTarget.targetKey : 'codex'), + }) === 'fast' + ) + } + return false +} + +export function getProviderFastModeState( + model: ModelSetting, + fastModeUserEnabled: boolean | undefined, + options?: { + provider?: APIProvider + targetKey?: string + }, +): 'off' | 'cooldown' | 'on' { + const provider = resolveFastModeProvider(options) + if (provider === 'firstParty') { + return getAnthropicFastModeState(model, fastModeUserEnabled) + } + if (provider === 'codex') { + return fastModeUserEnabled ? 'on' : 'off' + } + return 'off' +} + +export function getPersistedCodexFastModeSelection(options?: { + settings?: SettingsJson + targetKey?: string +}): PersistedServiceTier | undefined { + const currentTarget = getCurrentProviderSelectionTarget() + const targetKey = + options?.targetKey ?? + (currentTarget.provider === 'codex' ? currentTarget.targetKey : 'codex') + return getPersistedServiceTierForProvider({ + settings: options?.settings ?? getInitialSettings(), + provider: 'codex', + targetKey, + }) +} + +export function getCodexServiceTierForFastMode(options?: { + enabled?: boolean + provider?: APIProvider + targetKey?: string +}): CodexServiceTier | undefined { + return resolveFastModeProvider(options) === 'codex' && + isFastModeToggleEnabled(options) && + options?.enabled + ? 'priority' + : undefined +} diff --git a/src/utils/settings/types.ts b/src/utils/settings/types.ts index 54b824d98..91f30097b 100644 --- a/src/utils/settings/types.ts +++ b/src/utils/settings/types.ts @@ -410,11 +410,12 @@ export const SettingsSchema = lazySchema(() => z.object({ model: z.string().optional(), effortLevel: z.enum(PERSISTED_EFFORT_LEVELS).optional(), + serviceTier: z.enum(['fast']).optional(), }), ) .optional() .describe( - 'Provider-target scoped model and effort selections. Keys may be provider families or future profile-scoped targets. This is the canonical persisted state for /model and /effort.', + 'Provider-target scoped model, effort, and fast-mode selections. Keys may be provider families or future profile-scoped targets. This is the canonical persisted state for /model, /effort, and provider-aware /fast.', ), activeProviderTarget: z .string() diff --git a/src/utils/status.tsx b/src/utils/status.tsx index 9f14a2fc4..f60ef6495 100644 --- a/src/utils/status.tsx +++ b/src/utils/status.tsx @@ -11,7 +11,10 @@ import { getDisplayPath } from './file.js'; import { formatNumber } from './format.js'; import { getIdeClientName, type IDEExtensionInstallationStatus, isJetBrainsIde, toIDEDisplayName } from './ide.js'; import { getClaudeAiUserDefaultModelDescription, modelDisplayString } from './model/model.js'; +import { formatCodexModelDisplay } from './model/codexDisplay.js'; +import { getPersistedEffortSettingForProvider } from './model/providerModelSettings.js'; import { getAPIProvider } from './model/providers.js'; +import { getPersistedCodexFastModeSelection } from './providerFastMode.js'; import { resolveProviderRequest } from '../services/api/providerConfig.js'; import { getMTLSConfig } from './mtls.js'; import { checkInstall } from './nativeInstaller/index.js'; @@ -19,7 +22,7 @@ import { getProxyUrl } from './proxy.js'; import { SandboxManager } from './sandbox/sandbox-adapter.js'; import { getSettingsWithAllErrors } from './settings/allErrors.js'; import { getEnabledSettingSources, getSettingSourceDisplayNameCapitalized } from './settings/constants.js'; -import { getManagedFileSettingsPresence, getPolicySettingsOrigin, getSettingsForSource } from './settings/settings.js'; +import { getInitialSettings, getManagedFileSettingsPresence, getPolicySettingsOrigin, getSettingsForSource } from './settings/settings.js'; import type { ThemeName } from './theme.js'; import { redactSecretValueForDisplay } from './providerProfile.js'; export type Property = { @@ -351,7 +354,7 @@ export function buildAPIProviderProperties(): Property[] { } properties.push({ label: 'Model', - value: redactSecretValueForDisplay(modelDisplay, process.env) ?? modelDisplay + value: modelDisplay }); } } else if (apiProvider === 'codex') { @@ -364,21 +367,20 @@ export function buildAPIProviderProperties(): Property[] { } const openaiModel = process.env.OPENAI_MODEL; if (openaiModel) { - // Build display model string with resolved model + reasoning effort - let modelDisplay = openaiModel; - const resolved = resolveProviderRequest({ model: openaiModel }); - const resolvedModel = resolved.resolvedModel; - const reasoningEffort = resolved.reasoning?.effort; - if (resolvedModel && resolvedModel !== openaiModel.toLowerCase()) { - // Show resolved model name - modelDisplay = resolvedModel; - } - if (reasoningEffort) { - modelDisplay = `${modelDisplay} (${reasoningEffort})`; - } + const settings = getInitialSettings(); + const modelDisplay = formatCodexModelDisplay({ + model: openaiModel, + effortValue: getPersistedEffortSettingForProvider({ + settings, + provider: 'codex', + }), + fastMode: getPersistedCodexFastModeSelection({ + settings, + }) === 'fast', + }); properties.push({ label: 'Model', - value: redactSecretValueForDisplay(modelDisplay, process.env) ?? modelDisplay + value: modelDisplay }); } } else if (apiProvider === 'gemini') { From 830de2740f4fb63fa84dd0a52d36fca117b5325e Mon Sep 17 00:00:00 2001 From: guanjiawei Date: Fri, 17 Apr 2026 16:18:20 +0800 Subject: [PATCH 2/2] Fix fast mode review follow-ups --- src/commands/fast/fast.tsx | 13 ------------- src/utils/status.tsx | 4 ++-- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/src/commands/fast/fast.tsx b/src/commands/fast/fast.tsx index fc5b43e27..828c37311 100644 --- a/src/commands/fast/fast.tsx +++ b/src/commands/fast/fast.tsx @@ -139,7 +139,6 @@ export function FastModePicker({ provider: FastModeCommandProvider unavailableReason: string | null }): React.ReactNode { - const model = useAppState((s: AppState) => s.mainLoopModel) const initialFastMode = useAppState((s: AppState) => s.fastMode ?? false) const providerSelectionTargetKey = useAppState( (s: AppState) => s.providerSelectionTargetKey, @@ -174,17 +173,6 @@ export function FastModePicker({ source: 'picker' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, }) - if ( - provider === 'firstParty' && - !enableFastMode && - !isFastModeSupportedByModel(model) - ) { - setAppState((prev: AppState) => ({ - ...prev, - fastMode: false, - })) - } - onDone( getFastModeConfirmMessage({ enable: enableFastMode, @@ -194,7 +182,6 @@ export function FastModePicker({ ) }, [ enableFastMode, - model, onDone, provider, providerSelectionTargetKey, diff --git a/src/utils/status.tsx b/src/utils/status.tsx index f60ef6495..618fa827a 100644 --- a/src/utils/status.tsx +++ b/src/utils/status.tsx @@ -354,7 +354,7 @@ export function buildAPIProviderProperties(): Property[] { } properties.push({ label: 'Model', - value: modelDisplay + value: redactSecretValueForDisplay(modelDisplay, process.env) ?? modelDisplay }); } } else if (apiProvider === 'codex') { @@ -380,7 +380,7 @@ export function buildAPIProviderProperties(): Property[] { }); properties.push({ label: 'Model', - value: modelDisplay + value: redactSecretValueForDisplay(modelDisplay, process.env) ?? modelDisplay }); } } else if (apiProvider === 'gemini') {