fix(codex): support dynamic reasoning efforts (#1012)

* fix(codex): support model-reported reasoning efforts

* fix(web): prevent service worker edge caching

* ci: retrigger stuck Actions run

* fix(codex): accept dynamic reasoning effort values

* fix(web): restore reasoning effort on model switch failure
This commit is contained in:
ejj.cc
2026-07-12 18:38:56 +08:00
committed by GitHub
parent 4c76668a6c
commit d160203bb2
21 changed files with 372 additions and 58 deletions
+72 -6
View File
@@ -21,6 +21,10 @@ import { buildConversationOutline } from '@/chat/outline'
import { buildVisibleChatBlocks, isToolGroupBlock, type ToolGroupBlock } from '@/chat/toolGroups'
import { isQueuedForInvocation, mergeMessages } from '@/lib/messages'
import { inactiveSessionCanResume } from '@/lib/sessionResume'
import {
getCodexModelReasoningEfforts,
supportsCodexReasoningEffort
} from '@/lib/codexModelCapabilities'
import { HappyComposer, type ComposerSendError } from '@/components/AssistantChat/HappyComposer'
import { codexModelAdvertisesFastTier } from '@/components/AssistantChat/codexFastMode'
import type { PendingSchedule } from '@/components/AssistantChat/ScheduleTimePicker'
@@ -64,6 +68,33 @@ import { useVoiceOptional } from '@/lib/voice-context'
import { VoiceBackendSession, registerSessionStore, registerVoiceHooksStore, voiceHooks } from '@/realtime'
import { isRemoteTerminalSupported } from '@/utils/terminalSupport'
type SessionModelSelection = { provider: string; modelId: string } | string | null
export async function applyModelChangeWithReasoningRollback(args: {
model: SessionModelSelection
previousModelReasoningEffort: string | null
shouldClearReasoningEffort: boolean
setModel: (model: SessionModelSelection) => Promise<void>
setModelReasoningEffort: (effort: string | null) => Promise<void>
}): Promise<void> {
let clearedReasoningEffort = false
try {
if (args.shouldClearReasoningEffort) {
await args.setModelReasoningEffort(null)
clearedReasoningEffort = true
}
await args.setModel(args.model)
} catch (error) {
if (clearedReasoningEffort && args.previousModelReasoningEffort) {
await args.setModelReasoningEffort(args.previousModelReasoningEffort).catch((restoreError) => {
console.error('Failed to restore model reasoning effort:', restoreError)
})
}
throw error
}
}
/**
* Returns whether a PendingSchedule should trigger an auto-clear timer.
*
@@ -516,6 +547,16 @@ function SessionChatInner(props: SessionChatProps) {
}
return options
}, [agentFlavor, codexModelsState.models])
const codexSupportedReasoningEfforts = useMemo(
() => agentFlavor === 'codex'
? getCodexModelReasoningEfforts(codexModelsState.models, props.session.model)
: undefined,
[agentFlavor, codexModelsState.models, props.session.model]
)
const codexReasoningEffortOptions = useMemo(
() => codexSupportedReasoningEfforts?.map((value) => ({ value })),
[codexSupportedReasoningEfforts]
)
const opencodeModelsState = useOpencodeModels({
api: props.api,
sessionId: props.session.id,
@@ -909,16 +950,39 @@ function SessionChatInner(props: SessionChatProps) {
}, [setCollaborationMode, props.onRefresh, haptic])
// Model mode change handler
const handleModelChange = useCallback(async (model: { provider: string; modelId: string } | string | null) => {
const handleModelChange = useCallback(async (model: SessionModelSelection) => {
const previousModelReasoningEffort = props.session.modelReasoningEffort
const shouldClearReasoningEffort = agentFlavor === 'codex'
&& Boolean(previousModelReasoningEffort)
&& supportsCodexReasoningEffort(
codexModelsState.models,
model,
previousModelReasoningEffort
) === false
try {
await setModel(model)
await applyModelChangeWithReasoningRollback({
model,
previousModelReasoningEffort,
shouldClearReasoningEffort,
setModel,
setModelReasoningEffort
})
haptic.notification('success')
props.onRefresh()
} catch (e) {
haptic.notification('error')
console.error('Failed to set model:', e)
}
}, [setModel, props.onRefresh, haptic])
}, [
agentFlavor,
codexModelsState.models,
props.session.modelReasoningEffort,
setModelReasoningEffort,
setModel,
props.onRefresh,
haptic
])
const handleCursorBaseModelChange = useCallback(async (baseKey: string | null) => {
if (!cursorPicker) {
@@ -1243,9 +1307,11 @@ function SessionChatInner(props: SessionChatProps) {
piModels={agentFlavor === 'pi' ? (piModelsState.availableModels.length > 0 ? piModelsState.availableModels : piCachedModels) : undefined}
piSelectedModel={agentFlavor === 'pi' ? piSelectedModel : undefined}
availableModelReasoningEffortOptions={
agentFlavor === 'opencode' && opencodeReasoningEffortState.options.length > 0
? opencodeReasoningEffortState.options
: undefined
agentFlavor === 'codex'
? codexReasoningEffortOptions
: agentFlavor === 'opencode' && opencodeReasoningEffortState.options.length > 0
? opencodeReasoningEffortState.options
: undefined
}
active={props.session.active}
allowSendWhenInactive