- {(['low', 'medium', 'high', 'max'] as EffortLevel[]).map((level) => (
-
- ))}
-
-
{t('settings.general.thinkingTitle')}
{t('settings.general.thinkingDescription')}
diff --git a/desktop/src/stores/chatStore.test.ts b/desktop/src/stores/chatStore.test.ts
index 6aba3d3e..688f3cb9 100644
--- a/desktop/src/stores/chatStore.test.ts
+++ b/desktop/src/stores/chatStore.test.ts
@@ -1619,6 +1619,7 @@ describe('chatStore history mapping', () => {
useSessionRuntimeStore.getState().setSelection(TEST_SESSION_ID, {
providerId: 'provider-1',
modelId: 'kimi-k2.6',
+ effortLevel: 'high',
})
useChatStore.getState().connectToSession(TEST_SESSION_ID)
@@ -1627,6 +1628,7 @@ describe('chatStore history mapping', () => {
type: 'set_runtime_config',
providerId: 'provider-1',
modelId: 'kimi-k2.6',
+ effortLevel: 'high',
})
expect(sendMock.mock.calls.slice(0, 2)).toEqual([
[
@@ -1635,6 +1637,7 @@ describe('chatStore history mapping', () => {
type: 'set_runtime_config',
providerId: 'provider-1',
modelId: 'kimi-k2.6',
+ effortLevel: 'high',
},
],
[TEST_SESSION_ID, { type: 'prewarm_session' }],
@@ -1693,12 +1696,14 @@ describe('chatStore history mapping', () => {
useChatStore.getState().setSessionRuntime(TEST_SESSION_ID, {
providerId: null,
modelId: 'claude-opus-4-7',
+ effortLevel: 'max',
})
expect(sendMock).toHaveBeenCalledWith(TEST_SESSION_ID, {
type: 'set_runtime_config',
providerId: null,
modelId: 'claude-opus-4-7',
+ effortLevel: 'max',
})
})
diff --git a/desktop/src/stores/providerStore.ts b/desktop/src/stores/providerStore.ts
index 585ce6f9..e0f52833 100644
--- a/desktop/src/stores/providerStore.ts
+++ b/desktop/src/stores/providerStore.ts
@@ -60,6 +60,7 @@ function resolveRuntimeRefreshSelection(
modelId: modelIds.has(currentSelection.modelId)
? currentSelection.modelId
: provider.models.main,
+ ...(currentSelection.effortLevel ? { effortLevel: currentSelection.effortLevel } : {}),
}
}
diff --git a/desktop/src/stores/settingsStore.ts b/desktop/src/stores/settingsStore.ts
index e7edf80c..a18bf908 100644
--- a/desktop/src/stores/settingsStore.ts
+++ b/desktop/src/stores/settingsStore.ts
@@ -136,7 +136,7 @@ const DEFAULT_NETWORK_SETTINGS: NetworkSettings = {
export const useSettingsStore = create
((set, get) => ({
permissionMode: 'default',
currentModel: null,
- effortLevel: 'medium',
+ effortLevel: 'max',
thinkingEnabled: true,
availableModels: [],
activeProviderName: null,
diff --git a/desktop/src/types/runtime.ts b/desktop/src/types/runtime.ts
index 1c0fcd00..c47f815e 100644
--- a/desktop/src/types/runtime.ts
+++ b/desktop/src/types/runtime.ts
@@ -1,4 +1,7 @@
+import type { EffortLevel } from './settings'
+
export type RuntimeSelection = {
providerId: string | null
modelId: string
+ effortLevel?: EffortLevel
}
diff --git a/src/server/__tests__/conversations.test.ts b/src/server/__tests__/conversations.test.ts
index 501ff292..3d55857a 100644
--- a/src/server/__tests__/conversations.test.ts
+++ b/src/server/__tests__/conversations.test.ts
@@ -2930,6 +2930,7 @@ describe('WebSocket Chat Integration', () => {
type: 'set_runtime_config',
providerId: providerA.id,
modelId: 'model-a-sonnet',
+ effortLevel: 'medium',
}))
ws.send(JSON.stringify({ type: 'user_message', content: 'first turn' }))
phase = 'turn1'
@@ -2950,6 +2951,7 @@ describe('WebSocket Chat Integration', () => {
type: 'set_runtime_config',
providerId: providerB.id,
modelId: 'model-b-opus',
+ effortLevel: 'max',
}))
return
}
@@ -2989,6 +2991,7 @@ describe('WebSocket Chat Integration', () => {
options: {
providerId: providerA.id,
model: 'model-a-sonnet',
+ effort: 'medium',
},
})
expect(startCalls[1]).toMatchObject({
@@ -2996,6 +2999,7 @@ describe('WebSocket Chat Integration', () => {
options: {
providerId: providerB.id,
model: 'model-b-opus',
+ effort: 'max',
},
})
} finally {
diff --git a/src/server/__tests__/e2e/business-flow.test.ts b/src/server/__tests__/e2e/business-flow.test.ts
index d894aee1..6f0cfa72 100644
--- a/src/server/__tests__/e2e/business-flow.test.ts
+++ b/src/server/__tests__/e2e/business-flow.test.ts
@@ -419,9 +419,9 @@ describe('Business Flow: Models & Effort', () => {
expect(status).toBe(400)
})
- it('should default effort to medium', async () => {
+ it('should default effort to max', async () => {
const { data } = await api('GET', '/api/effort')
- expect(data.level).toBe('medium')
+ expect(data.level).toBe('max')
expect(data.available).toEqual(['low', 'medium', 'high', 'max'])
})
diff --git a/src/server/__tests__/settings.test.ts b/src/server/__tests__/settings.test.ts
index 6240658e..4d3aade9 100644
--- a/src/server/__tests__/settings.test.ts
+++ b/src/server/__tests__/settings.test.ts
@@ -691,7 +691,7 @@ describe('Models API', () => {
expect(res.status).toBe(200)
const body = await res.json()
- expect(body.level).toBe('medium')
+ expect(body.level).toBe('max')
expect(body.available).toEqual(['low', 'medium', 'high', 'max'])
})
@@ -704,7 +704,7 @@ describe('Models API', () => {
expect(res.status).toBe(200)
const body = await res.json()
- expect(body.level).toBe('medium')
+ expect(body.level).toBe('max')
expect(body.available).toEqual(['low', 'medium', 'high', 'max'])
})
diff --git a/src/server/api/models.ts b/src/server/api/models.ts
index 570b6338..73755672 100644
--- a/src/server/api/models.ts
+++ b/src/server/api/models.ts
@@ -46,7 +46,7 @@ const DEFAULT_MODELS = [
const EFFORT_LEVELS = ['low', 'medium', 'high', 'max'] as const
const DEFAULT_MODEL = 'claude-opus-4-7'
-const DEFAULT_EFFORT = 'medium'
+const DEFAULT_EFFORT = 'max'
const settingsService = new SettingsService()
const providerService = new ProviderService()
diff --git a/src/server/services/sessionService.ts b/src/server/services/sessionService.ts
index 663045f3..eb611d9e 100644
--- a/src/server/services/sessionService.ts
+++ b/src/server/services/sessionService.ts
@@ -72,6 +72,9 @@ export type SessionLaunchInfo = {
transcriptMessageCount: number
customTitle: string | null
permissionMode?: string
+ runtimeProviderId?: string | null
+ runtimeModelId?: string
+ effortLevel?: string
}
export type TrimSessionResult = {
@@ -228,6 +231,9 @@ type SessionListSummary = {
messageCount: number
workDir: string | null
permissionMode?: string
+ runtimeProviderId?: string | null
+ runtimeModelId?: string
+ effortLevel?: string
repository?: PreparedSessionWorkspace['repository']
worktreeSession?: PersistedWorktreeSession | null
}
@@ -239,6 +245,7 @@ const VALID_SESSION_PERMISSION_MODES = new Set([
'bypassPermissions',
'dontAsk',
])
+const VALID_SESSION_EFFORT_LEVELS = new Set(['low', 'medium', 'high', 'max'])
type ContentBlock = Record
@@ -348,6 +355,9 @@ export class SessionService {
let latestWorkDir: string | null = null
let latestCwd: string | null = null
let permissionMode: string | undefined
+ let runtimeProviderId: string | null | undefined
+ let runtimeModelId: string | undefined
+ let effortLevel: string | undefined
let repository: PreparedSessionWorkspace['repository'] | undefined
let worktreeSession: PersistedWorktreeSession | null | undefined
@@ -393,6 +403,21 @@ export class SessionService {
) {
permissionMode = entry.permissionMode
}
+ if (
+ (entry as Record).runtimeProviderId === null ||
+ typeof (entry as Record).runtimeProviderId === 'string'
+ ) {
+ runtimeProviderId = (entry as Record).runtimeProviderId as string | null
+ }
+ if (typeof (entry as Record).runtimeModelId === 'string') {
+ runtimeModelId = (entry as Record).runtimeModelId as string
+ }
+ if (
+ typeof (entry as Record).effortLevel === 'string' &&
+ VALID_SESSION_EFFORT_LEVELS.has((entry as Record).effortLevel as string)
+ ) {
+ effortLevel = (entry as Record).effortLevel as string
+ }
}
if (typeof entry.cwd === 'string' && entry.cwd.trim()) {
@@ -454,6 +479,9 @@ export class SessionService {
messageCount,
workDir: latestWorkDir || latestCwd || this.desanitizePath(projectDir),
...(permissionMode ? { permissionMode } : {}),
+ ...(runtimeProviderId !== undefined ? { runtimeProviderId } : {}),
+ ...(runtimeModelId ? { runtimeModelId } : {}),
+ ...(effortLevel ? { effortLevel } : {}),
...(repository ? { repository } : {}),
...(worktreeSession !== undefined ? { worktreeSession } : {}),
}
@@ -1846,11 +1874,29 @@ export class SessionService {
const worktreeSession = this.resolveWorktreeSessionFromEntries(entries)
const permissionMode = this.resolvePermissionModeFromEntries(entries)
let customTitle: string | null = null
+ let runtimeProviderId: string | null | undefined
+ let runtimeModelId: string | undefined
+ let effortLevel: string | undefined
for (const entry of entries) {
if (entry.type === 'custom-title' && typeof entry.customTitle === 'string') {
customTitle = entry.customTitle
}
+ if (entry.type === 'session-meta') {
+ const record = entry as Record
+ if (record.runtimeProviderId === null || typeof record.runtimeProviderId === 'string') {
+ runtimeProviderId = record.runtimeProviderId as string | null
+ }
+ if (typeof record.runtimeModelId === 'string') {
+ runtimeModelId = record.runtimeModelId
+ }
+ if (
+ typeof record.effortLevel === 'string' &&
+ VALID_SESSION_EFFORT_LEVELS.has(record.effortLevel)
+ ) {
+ effortLevel = record.effortLevel
+ }
+ }
}
const transcriptMessageCount = this.countTranscriptMessages(entries)
@@ -1863,6 +1909,9 @@ export class SessionService {
transcriptMessageCount,
customTitle,
permissionMode,
+ ...(runtimeProviderId !== undefined ? { runtimeProviderId } : {}),
+ ...(runtimeModelId ? { runtimeModelId } : {}),
+ ...(effortLevel ? { effortLevel } : {}),
}
}
@@ -1928,6 +1977,9 @@ export class SessionService {
customTitle?: string | null
repository?: PreparedSessionWorkspace['repository']
permissionMode?: string
+ runtimeProviderId?: string | null
+ runtimeModelId?: string
+ effortLevel?: string
}
): Promise {
const matches = await this.findSessionFiles(sessionId)
@@ -1957,6 +2009,13 @@ export class SessionService {
...(metadata.permissionMode && VALID_SESSION_PERMISSION_MODES.has(metadata.permissionMode)
? { permissionMode: metadata.permissionMode }
: {}),
+ ...(metadata.runtimeProviderId !== undefined
+ ? { runtimeProviderId: metadata.runtimeProviderId }
+ : {}),
+ ...(metadata.runtimeModelId ? { runtimeModelId: metadata.runtimeModelId } : {}),
+ ...(metadata.effortLevel && VALID_SESSION_EFFORT_LEVELS.has(metadata.effortLevel)
+ ? { effortLevel: metadata.effortLevel }
+ : {}),
timestamp: new Date().toISOString(),
})
diff --git a/src/server/ws/events.ts b/src/server/ws/events.ts
index ea9b6ea7..d729763d 100644
--- a/src/server/ws/events.ts
+++ b/src/server/ws/events.ts
@@ -24,7 +24,7 @@ export type ClientMessage =
response: ComputerUsePermissionResponse
}
| { type: 'set_permission_mode'; mode: string }
- | { type: 'set_runtime_config'; providerId: string | null; modelId: string }
+ | { type: 'set_runtime_config'; providerId: string | null; modelId: string; effortLevel?: string }
| { type: 'stop_generation' }
| { type: 'ping' }
diff --git a/src/server/ws/handler.ts b/src/server/ws/handler.ts
index 0f6a8400..37f1bc90 100644
--- a/src/server/ws/handler.ts
+++ b/src/server/ws/handler.ts
@@ -70,15 +70,19 @@ const sessionTitleState = new Map()
const runtimeTransitionPromises = new Map>()
const sessionStartupPromises = new Map>()
+const runtimeOverrideVersions = new Map()
+const sessionStartupRuntimeVersions = new Map()
const lastResolvedStartupWorkDirs = new Map()
const prewarmPendingSessions = new Set()
const prewarmedSessions = new Set()
const prewarmIdleTimers = new Map>()
const DEFAULT_PREWARM_IDLE_TIMEOUT_MS = 5 * 60_000
+const VALID_EFFORT_LEVELS = new Set(['low', 'medium', 'high', 'max'])
async function sendRepositoryStartupStatus(
ws: ServerWebSocket,
@@ -549,44 +553,73 @@ async function handleSetRuntimeConfig(
})
return
}
+ const effortLevel =
+ typeof message.effortLevel === 'string' ? message.effortLevel.trim() : undefined
+ if (effortLevel !== undefined && !VALID_EFFORT_LEVELS.has(effortLevel)) {
+ sendMessage(ws, {
+ type: 'error',
+ message: 'Runtime effort selection is invalid.',
+ code: 'RUNTIME_CONFIG_INVALID',
+ })
+ return
+ }
const nextOverride = {
providerId: message.providerId ?? null,
modelId,
+ ...(effortLevel ? { effort: effortLevel } : {}),
}
const prevOverride = runtimeOverrides.get(sessionId)
- runtimeOverrides.set(sessionId, nextOverride)
-
if (
prevOverride &&
prevOverride.providerId === nextOverride.providerId &&
- prevOverride.modelId === nextOverride.modelId
+ prevOverride.modelId === nextOverride.modelId &&
+ prevOverride.effort === nextOverride.effort
) {
return
}
- if (!conversationService.hasSession(sessionId)) {
- const pendingStartup = sessionStartupPromises.get(sessionId)
- if (pendingStartup) {
- await enqueueRuntimeTransition(sessionId, async () => {
- await pendingStartup.catch(() => undefined)
- const currentOverride = runtimeOverrides.get(sessionId)
- if (
- currentOverride?.providerId !== nextOverride.providerId ||
- currentOverride.modelId !== nextOverride.modelId ||
- !conversationService.hasSession(sessionId)
- ) {
- return
- }
- await restartSessionWithRuntimeConfig(ws, sessionId)
- })
- }
+ runtimeOverrides.set(sessionId, nextOverride)
+ runtimeOverrideVersions.set(
+ sessionId,
+ (runtimeOverrideVersions.get(sessionId) ?? 0) + 1,
+ )
+
+ if (conversationService.hasSession(sessionId)) {
+ await enqueueRuntimeTransition(sessionId, async () => {
+ await persistSessionRuntimeConfig(sessionId, nextOverride)
+ await restartSessionWithRuntimeConfig(ws, sessionId)
+ })
return
}
- await enqueueRuntimeTransition(sessionId, () =>
- restartSessionWithRuntimeConfig(ws, sessionId),
- )
+ const pendingStartup = sessionStartupPromises.get(sessionId)
+ if (pendingStartup) {
+ const startupRuntimeVersion = sessionStartupRuntimeVersions.get(sessionId) ?? 0
+ const currentRuntimeVersion = runtimeOverrideVersions.get(sessionId) ?? 0
+ if (startupRuntimeVersion >= currentRuntimeVersion) {
+ await persistSessionRuntimeConfig(sessionId, nextOverride)
+ return
+ }
+
+ await enqueueRuntimeTransition(sessionId, async () => {
+ await persistSessionRuntimeConfig(sessionId, nextOverride)
+ await pendingStartup.catch(() => undefined)
+ const currentOverride = runtimeOverrides.get(sessionId)
+ if (
+ currentOverride?.providerId !== nextOverride.providerId ||
+ currentOverride.modelId !== nextOverride.modelId ||
+ currentOverride.effort !== nextOverride.effort ||
+ !conversationService.hasSession(sessionId)
+ ) {
+ return
+ }
+ await restartSessionWithRuntimeConfig(ws, sessionId)
+ })
+ return
+ }
+
+ await persistSessionRuntimeConfig(sessionId, nextOverride)
}
async function restartSessionWithPermissionMode(
@@ -648,6 +681,24 @@ async function persistSessionPermissionMode(
})
}
+async function persistSessionRuntimeConfig(
+ sessionId: string,
+ runtime: { providerId: string | null; modelId: string; effort?: string },
+): Promise {
+ const workDir =
+ conversationService.getSessionWorkDir(sessionId) ||
+ await sessionService.getSessionWorkDir(sessionId).catch(() => null)
+
+ if (!workDir) return
+
+ await sessionService.appendSessionMetadata(sessionId, {
+ workDir,
+ runtimeProviderId: runtime.providerId,
+ runtimeModelId: runtime.modelId,
+ ...(runtime.effort ? { effortLevel: runtime.effort } : {}),
+ })
+}
+
async function restartSessionWithRuntimeConfig(
ws: ServerWebSocket,
sessionId: string,
@@ -981,6 +1032,9 @@ async function ensureCliSessionStarted(
if (conversationService.hasSession(sessionId)) return
+ const startupRuntimeVersion = runtimeOverrideVersions.get(sessionId) ?? 0
+ sessionStartupRuntimeVersions.set(sessionId, startupRuntimeVersion)
+
const startup = (async () => {
const workDir = await resolveSessionWorkDir(sessionId)
lastResolvedStartupWorkDirs.set(sessionId, workDir)
@@ -999,6 +1053,7 @@ async function ensureCliSessionStarted(
} finally {
if (sessionStartupPromises.get(sessionId) === startup) {
sessionStartupPromises.delete(sessionId)
+ sessionStartupRuntimeVersions.delete(sessionId)
}
}
}
@@ -1859,10 +1914,23 @@ function isKnownRuntimeProviderId(
}
async function getRuntimeSettings(sessionId?: string): Promise {
+ const launchInfo = sessionId
+ ? await sessionService.getSessionLaunchInfo(sessionId).catch(() => null)
+ : null
const sessionPermissionMode = sessionId
- ? await getSessionPermissionMode(sessionId)
+ ? launchInfo?.permissionMode ?? await getSessionPermissionMode(sessionId)
+ : undefined
+ const persistedRuntimeOverride =
+ launchInfo?.runtimeModelId
+ ? {
+ providerId: launchInfo.runtimeProviderId ?? null,
+ modelId: launchInfo.runtimeModelId,
+ ...(launchInfo.effortLevel ? { effort: launchInfo.effortLevel } : {}),
+ }
+ : undefined
+ const runtimeOverride = sessionId
+ ? runtimeOverrides.get(sessionId) ?? persistedRuntimeOverride
: undefined
- const runtimeOverride = sessionId ? runtimeOverrides.get(sessionId) : undefined
if (runtimeOverride) {
if (typeof runtimeOverride.providerId === 'string') {
const { providers } = await providerService.listProviders()
@@ -1881,16 +1949,12 @@ async function getRuntimeSettings(sessionId?: string): Promise
}
const userSettings = await settingsService.getUserSettings()
- const effort =
- typeof userSettings.effort === 'string' && userSettings.effort.trim()
- ? userSettings.effort
- : undefined
const thinking = resolveDesktopThinkingMode(userSettings)
return {
permissionMode: sessionPermissionMode ?? await settingsService.getPermissionMode().catch(() => undefined),
model: runtimeOverride.modelId,
- effort,
+ effort: runtimeOverride.effort,
thinking,
providerId: runtimeOverride.providerId,
}
@@ -1900,6 +1964,7 @@ async function getRuntimeSettings(sessionId?: string): Promise
return {
...defaults,
permissionMode: sessionPermissionMode ?? defaults.permissionMode,
+ effort: launchInfo?.effortLevel ?? defaults.effort,
}
}
@@ -1995,6 +2060,7 @@ async function buildSessionStartupDiagnosticMessage(
if (runtimeOverride) {
lines.push(`- runtimeOverride.providerId: ${runtimeOverride.providerId ?? '(official)'}`)
lines.push(`- runtimeOverride.modelId: ${runtimeOverride.modelId}`)
+ lines.push(`- runtimeOverride.effort: ${runtimeOverride.effort ?? '(auto)'}`)
} else {
lines.push('- runtimeOverride: (none)')
}
diff --git a/src/utils/__tests__/thinking.test.ts b/src/utils/__tests__/thinking.test.ts
index 1e398570..c8d2679d 100644
--- a/src/utils/__tests__/thinking.test.ts
+++ b/src/utils/__tests__/thinking.test.ts
@@ -1,6 +1,7 @@
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
import { get3PModelCapabilityOverride } from '../model/modelSupportOverrides.js'
import { resolveSideQueryThinkingConfig } from '../sideQuery.js'
+import { modelSupportsEffort, modelSupportsMaxEffort } from '../effort.js'
import {
modelSupportsAdaptiveThinking,
modelSupportsThinking,
@@ -87,6 +88,8 @@ describe('provider-aware thinking support', () => {
expect(modelSupportsThinking('deepseek-v4-pro')).toBe(true)
expect(modelSupportsAdaptiveThinking('deepseek-v4-pro')).toBe(true)
+ expect(modelSupportsEffort('deepseek-v4-pro')).toBe(true)
+ expect(modelSupportsMaxEffort('deepseek-v4-pro')).toBe(true)
expect(shouldSendExplicitDisabledThinking()).toBe(false)
})
@@ -98,6 +101,17 @@ describe('provider-aware thinking support', () => {
expect(modelSupportsThinking('MiniMax-M2.7')).toBe(true)
expect(modelSupportsAdaptiveThinking('MiniMax-M2.7')).toBe(false)
+ expect(modelSupportsEffort('MiniMax-M2.7')).toBe(false)
+ expect(modelSupportsMaxEffort('MiniMax-M2.7')).toBe(false)
+ })
+
+ test('third-party base URLs do not default unknown model names to effort support', () => {
+ process.env.ANTHROPIC_BASE_URL = 'https://api.moonshot.cn/anthropic'
+ delete process.env.ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES
+ clearCapabilityCache()
+
+ expect(modelSupportsEffort('kimi-k2.6')).toBe(false)
+ expect(modelSupportsMaxEffort('kimi-k2.6')).toBe(false)
})
test('side queries inherit explicit disabled thinking for opted-in providers', () => {
diff --git a/src/utils/effort.ts b/src/utils/effort.ts
index 2320e9c9..d38cfa5e 100644
--- a/src/utils/effort.ts
+++ b/src/utils/effort.ts
@@ -3,7 +3,7 @@ import { isUltrathinkEnabled } from './thinking.js'
import { getInitialSettings } from './settings/settings.js'
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 { getAPIProvider, isFirstPartyAnthropicBaseUrl } from './model/providers.js'
import { get3PModelCapabilityOverride } from './model/modelSupportOverrides.js'
import { isEnvTruthy } from './envUtils.js'
import type { EffortLevel } from 'src/entrypoints/sdk/runtimeTypes.js'
@@ -45,7 +45,7 @@ export function modelSupportsEffort(model: string): boolean {
// Default to true for unknown model strings on 1P.
// Do not default to true for 3P as they have different formats for their
// model strings (ex. anthropics/claude-code#30795)
- return getAPIProvider() === 'firstParty'
+ return getAPIProvider() === 'firstParty' && isFirstPartyAnthropicBaseUrl()
}
// @[MODEL LAUNCH]: Add the new model to the allowlist if it supports 'max' effort.