mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-30 16:23:35 +08:00
feat: scope desktop reasoning effort to sessions
Desktop reasoning effort now follows the selected session runtime instead of the General settings surface. Session metadata records provider, model, and effort so reconnects and prewarm restarts preserve the per-session runtime, while the default effort is max for new sessions. Constraint: Existing transcript and localStorage runtime data must remain readable without a required migration. Rejected: Keep effort as a General setting | it made one session change leak into every other session. Confidence: high Scope-risk: moderate Directive: Do not reintroduce global desktop effort controls without proving multi-session isolation. Tested: bun run check:server Tested: bun run check:desktop Tested: bun run check:native Tested: bun run check:policy Tested: bun run check:coverage Tested: cd desktop && bun run test -- src/components/controls/ModelSelector.test.tsx src/stores/chatStore.test.ts Tested: bun test src/server/__tests__/settings.test.ts --test-name-pattern "effort" Tested: bun test src/server/__tests__/conversations.test.ts --test-name-pattern "runtime" Not-tested: Live provider smoke for domestic providers without local credentials.
This commit is contained in:
parent
29586ce384
commit
1f9af7e578
@ -134,7 +134,7 @@ export function ChatInput({ variant = 'default', compact = false }: ChatInputPro
|
|||||||
const currentModel = useSettingsStore((state) => state.currentModel)
|
const currentModel = useSettingsStore((state) => state.currentModel)
|
||||||
const chatSendBehavior = useSettingsStore((state) => state.chatSendBehavior)
|
const chatSendBehavior = useSettingsStore((state) => state.chatSendBehavior)
|
||||||
const runtimeSelectionKey = runtimeSelection
|
const runtimeSelectionKey = runtimeSelection
|
||||||
? `${runtimeSelection.providerId ?? 'official'}:${runtimeSelection.modelId}`
|
? `${runtimeSelection.providerId ?? 'official'}:${runtimeSelection.modelId}:${runtimeSelection.effortLevel ?? 'auto'}`
|
||||||
: undefined
|
: undefined
|
||||||
const runtimeModelLabel = runtimeSelection?.modelId ?? currentModel?.name ?? currentModel?.id
|
const runtimeModelLabel = runtimeSelection?.modelId ?? currentModel?.name ?? currentModel?.id
|
||||||
const activeSession = useSessionStore((state) => activeTabId ? state.sessions.find((session) => session.id === activeTabId) ?? null : null)
|
const activeSession = useSessionStore((state) => activeTabId ? state.sessions.find((session) => session.id === activeTabId) ?? null : null)
|
||||||
|
|||||||
@ -57,16 +57,14 @@ describe('ModelSelector', () => {
|
|||||||
expect(onChange).toHaveBeenCalledWith('beta')
|
expect(onChange).toHaveBeenCalledWith('beta')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('routes uncontrolled model and effort changes through settings actions', async () => {
|
it('routes uncontrolled model changes through settings actions', async () => {
|
||||||
const setModel = vi.fn(async () => {})
|
const setModel = vi.fn(async () => {})
|
||||||
const setEffort = vi.fn(async () => {})
|
|
||||||
useSettingsStore.setState({
|
useSettingsStore.setState({
|
||||||
locale: 'en',
|
locale: 'en',
|
||||||
availableModels: MODELS,
|
availableModels: MODELS,
|
||||||
currentModel: MODELS[0],
|
currentModel: MODELS[0],
|
||||||
effortLevel: 'medium',
|
effortLevel: 'max',
|
||||||
setModel,
|
setModel,
|
||||||
setEffort,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
render(<ModelSelector />)
|
render(<ModelSelector />)
|
||||||
@ -74,10 +72,6 @@ describe('ModelSelector', () => {
|
|||||||
await clickByRole(/alpha/i)
|
await clickByRole(/alpha/i)
|
||||||
await clickByRole(/Beta/)
|
await clickByRole(/Beta/)
|
||||||
expect(setModel).toHaveBeenCalledWith('beta')
|
expect(setModel).toHaveBeenCalledWith('beta')
|
||||||
|
|
||||||
await clickByRole(/Alpha/)
|
|
||||||
await clickByRole(/^High$/)
|
|
||||||
expect(setEffort).toHaveBeenCalledWith('high')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('selects provider-scoped runtime models and mirrors session selections', async () => {
|
it('selects provider-scoped runtime models and mirrors session selections', async () => {
|
||||||
@ -122,13 +116,75 @@ describe('ModelSelector', () => {
|
|||||||
expect(useSessionRuntimeStore.getState().selections['session-1']).toEqual({
|
expect(useSessionRuntimeStore.getState().selections['session-1']).toEqual({
|
||||||
providerId: 'provider-a',
|
providerId: 'provider-a',
|
||||||
modelId: 'provider-fast',
|
modelId: 'provider-fast',
|
||||||
|
effortLevel: 'max',
|
||||||
})
|
})
|
||||||
expect(setSessionRuntime).toHaveBeenCalledWith('session-1', {
|
expect(setSessionRuntime).toHaveBeenCalledWith('session-1', {
|
||||||
providerId: 'provider-a',
|
providerId: 'provider-a',
|
||||||
modelId: 'provider-fast',
|
modelId: 'provider-fast',
|
||||||
|
effortLevel: 'max',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('keeps runtime effort scoped to the selected session', async () => {
|
||||||
|
const setSessionRuntime = vi.fn()
|
||||||
|
useSettingsStore.setState({
|
||||||
|
locale: 'en',
|
||||||
|
availableModels: MODELS,
|
||||||
|
currentModel: MODELS[0],
|
||||||
|
activeProviderName: 'Provider A',
|
||||||
|
effortLevel: 'max',
|
||||||
|
})
|
||||||
|
useProviderStore.setState({
|
||||||
|
providers: [{
|
||||||
|
id: 'provider-a',
|
||||||
|
presetId: 'custom',
|
||||||
|
name: 'Provider A',
|
||||||
|
apiKey: '***',
|
||||||
|
baseUrl: 'https://api.example.com',
|
||||||
|
apiFormat: 'anthropic',
|
||||||
|
models: {
|
||||||
|
main: 'provider-main',
|
||||||
|
haiku: 'provider-fast',
|
||||||
|
sonnet: 'provider-main',
|
||||||
|
opus: '',
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
activeId: 'provider-a',
|
||||||
|
hasLoadedProviders: true,
|
||||||
|
isLoading: true,
|
||||||
|
})
|
||||||
|
useSessionRuntimeStore.getState().setSelection('session-2', {
|
||||||
|
providerId: 'provider-a',
|
||||||
|
modelId: 'provider-main',
|
||||||
|
effortLevel: 'max',
|
||||||
|
})
|
||||||
|
useChatStore.setState({
|
||||||
|
setSessionRuntime,
|
||||||
|
} as Partial<ReturnType<typeof useChatStore.getState>>)
|
||||||
|
|
||||||
|
render(<ModelSelector runtimeKey="session-1" />)
|
||||||
|
|
||||||
|
await clickByRole(/alpha/i)
|
||||||
|
await clickByRole(/^High$/)
|
||||||
|
|
||||||
|
expect(useSessionRuntimeStore.getState().selections['session-1']).toEqual({
|
||||||
|
providerId: 'provider-a',
|
||||||
|
modelId: 'alpha',
|
||||||
|
effortLevel: 'high',
|
||||||
|
})
|
||||||
|
expect(useSessionRuntimeStore.getState().selections['session-2']).toEqual({
|
||||||
|
providerId: 'provider-a',
|
||||||
|
modelId: 'provider-main',
|
||||||
|
effortLevel: 'max',
|
||||||
|
})
|
||||||
|
expect(setSessionRuntime).toHaveBeenCalledWith('session-1', {
|
||||||
|
providerId: 'provider-a',
|
||||||
|
modelId: 'alpha',
|
||||||
|
effortLevel: 'high',
|
||||||
|
})
|
||||||
|
expect(useSettingsStore.getState().effortLevel).toBe('max')
|
||||||
|
})
|
||||||
|
|
||||||
it('uses the ChatGPT Official catalog when that built-in provider is active', async () => {
|
it('uses the ChatGPT Official catalog when that built-in provider is active', async () => {
|
||||||
const openAIModels: ModelInfo[] = [
|
const openAIModels: ModelInfo[] = [
|
||||||
{
|
{
|
||||||
@ -176,10 +232,12 @@ describe('ModelSelector', () => {
|
|||||||
expect(useSessionRuntimeStore.getState().selections['session-openai']).toEqual({
|
expect(useSessionRuntimeStore.getState().selections['session-openai']).toEqual({
|
||||||
providerId: OPENAI_OFFICIAL_PROVIDER_ID,
|
providerId: OPENAI_OFFICIAL_PROVIDER_ID,
|
||||||
modelId: 'gpt-5.5',
|
modelId: 'gpt-5.5',
|
||||||
|
effortLevel: 'max',
|
||||||
})
|
})
|
||||||
expect(setSessionRuntime).toHaveBeenCalledWith('session-openai', {
|
expect(setSessionRuntime).toHaveBeenCalledWith('session-openai', {
|
||||||
providerId: OPENAI_OFFICIAL_PROVIDER_ID,
|
providerId: OPENAI_OFFICIAL_PROVIDER_ID,
|
||||||
modelId: 'gpt-5.5',
|
modelId: 'gpt-5.5',
|
||||||
|
effortLevel: 'max',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -179,7 +179,6 @@ export function ModelSelector({
|
|||||||
effortLevel,
|
effortLevel,
|
||||||
activeProviderName,
|
activeProviderName,
|
||||||
setModel,
|
setModel,
|
||||||
setEffort,
|
|
||||||
} = useSettingsStore()
|
} = useSettingsStore()
|
||||||
const {
|
const {
|
||||||
providers,
|
providers,
|
||||||
@ -211,6 +210,7 @@ export function ModelSelector({
|
|||||||
const isRuntimeScoped =
|
const isRuntimeScoped =
|
||||||
!isControlled &&
|
!isControlled &&
|
||||||
(runtimeKey !== undefined || onRuntimeSelectionChange !== undefined)
|
(runtimeKey !== undefined || onRuntimeSelectionChange !== undefined)
|
||||||
|
const canEditRuntimeEffort = runtimeKey !== undefined
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isRuntimeScoped || providersLoading || requestedProvidersRef.current) return
|
if (!isRuntimeScoped || providersLoading || requestedProvidersRef.current) return
|
||||||
@ -351,6 +351,7 @@ export function ModelSelector({
|
|||||||
const buttonProviderLabel = isRuntimeScoped
|
const buttonProviderLabel = isRuntimeScoped
|
||||||
? selectedProviderChoice?.providerName ?? activeProviderName ?? t('settings.providers.officialName')
|
? selectedProviderChoice?.providerName ?? activeProviderName ?? t('settings.providers.officialName')
|
||||||
: null
|
: null
|
||||||
|
const selectedRuntimeEffort = activeRuntimeSelection?.effortLevel ?? effortLevel
|
||||||
|
|
||||||
const handleRuntimeSelect = (selection: RuntimeSelection) => {
|
const handleRuntimeSelect = (selection: RuntimeSelection) => {
|
||||||
onRuntimeSelectionChange?.(selection)
|
onRuntimeSelectionChange?.(selection)
|
||||||
@ -363,6 +364,14 @@ export function ModelSelector({
|
|||||||
setOpen(false)
|
setOpen(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleRuntimeEffortSelect = (level: EffortLevel) => {
|
||||||
|
if (!activeRuntimeSelection) return
|
||||||
|
handleRuntimeSelect({
|
||||||
|
...activeRuntimeSelection,
|
||||||
|
effortLevel: level,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const dropdownContent = (
|
const dropdownContent = (
|
||||||
<>
|
<>
|
||||||
<div className={`overflow-y-auto ${isMobileBrowser ? 'p-1' : 'p-3'}`} style={{ maxHeight: isMobileBrowser ? undefined : dropdownPosition?.maxHeight }}>
|
<div className={`overflow-y-auto ${isMobileBrowser ? 'p-1' : 'p-3'}`} style={{ maxHeight: isMobileBrowser ? undefined : dropdownPosition?.maxHeight }}>
|
||||||
@ -395,7 +404,11 @@ export function ModelSelector({
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={`${choice.providerId ?? 'official'}:${model.id}`}
|
key={`${choice.providerId ?? 'official'}:${model.id}`}
|
||||||
onClick={() => handleRuntimeSelect({ providerId: choice.providerId, modelId: model.id })}
|
onClick={() => handleRuntimeSelect({
|
||||||
|
providerId: choice.providerId,
|
||||||
|
modelId: model.id,
|
||||||
|
effortLevel: selectedRuntimeEffort,
|
||||||
|
})}
|
||||||
className={`
|
className={`
|
||||||
w-full rounded-lg border px-3 text-left transition-colors
|
w-full rounded-lg border px-3 text-left transition-colors
|
||||||
${isMobileBrowser ? 'min-h-[56px] py-3' : 'py-2.5'}
|
${isMobileBrowser ? 'min-h-[56px] py-3' : 'py-2.5'}
|
||||||
@ -481,20 +494,19 @@ export function ModelSelector({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!isControlled && !isRuntimeScoped && (
|
{canEditRuntimeEffort && (
|
||||||
<div className="border-t border-[var(--color-border)] p-3">
|
<div className="border-t border-[var(--color-border)] p-3">
|
||||||
<div className="mb-2 px-1 text-[10px] font-bold uppercase tracking-widest text-[var(--color-outline)]">
|
<div className="mb-2 px-1 text-[10px] font-bold uppercase tracking-widest text-[var(--color-outline)]">
|
||||||
{t('model.effort')}
|
{t('model.effort')}
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-4 gap-1.5">
|
<div className="grid grid-cols-4 gap-1.5">
|
||||||
{EFFORT_OPTIONS.map((opt) => {
|
{EFFORT_OPTIONS.map((opt) => {
|
||||||
const isSelected = opt.value === effortLevel
|
const isSelected = opt.value === selectedRuntimeEffort
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={opt.value}
|
key={opt.value}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
void setEffort(opt.value)
|
handleRuntimeEffortSelect(opt.value)
|
||||||
setOpen(false)
|
|
||||||
}}
|
}}
|
||||||
className={`
|
className={`
|
||||||
rounded-lg py-2 text-center text-xs font-semibold transition-colors
|
rounded-lg py-2 text-center text-xs font-semibold transition-colors
|
||||||
|
|||||||
@ -19,6 +19,7 @@ const TAB_STORAGE_KEY = 'cc-haha-open-tabs'
|
|||||||
const SESSION_RUNTIME_STORAGE_KEY = 'cc-haha-session-runtime'
|
const SESSION_RUNTIME_STORAGE_KEY = 'cc-haha-session-runtime'
|
||||||
const THEME_STORAGE_KEY = 'cc-haha-theme'
|
const THEME_STORAGE_KEY = 'cc-haha-theme'
|
||||||
const LOCALE_STORAGE_KEY = 'cc-haha-locale'
|
const LOCALE_STORAGE_KEY = 'cc-haha-locale'
|
||||||
|
const EFFORT_LEVELS = ['low', 'medium', 'high', 'max']
|
||||||
|
|
||||||
function readJson(storage: StorageLike, key: string): unknown {
|
function readJson(storage: StorageLike, key: string): unknown {
|
||||||
const raw = storage.getItem(key)
|
const raw = storage.getItem(key)
|
||||||
@ -88,7 +89,14 @@ function migrateSessionRuntime(storage: StorageLike, report: DesktopMigrationRep
|
|||||||
Object.entries(parsed).filter(([, selection]) => (
|
Object.entries(parsed).filter(([, selection]) => (
|
||||||
isRecord(selection) &&
|
isRecord(selection) &&
|
||||||
typeof selection.modelId === 'string' &&
|
typeof selection.modelId === 'string' &&
|
||||||
(selection.providerId === null || typeof selection.providerId === 'string')
|
(selection.providerId === null || typeof selection.providerId === 'string') &&
|
||||||
|
(
|
||||||
|
selection.effortLevel === undefined ||
|
||||||
|
(
|
||||||
|
typeof selection.effortLevel === 'string' &&
|
||||||
|
EFFORT_LEVELS.includes(selection.effortLevel)
|
||||||
|
)
|
||||||
|
)
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -116,7 +116,7 @@ export function EmptySession() {
|
|||||||
const lastPluginReloadSummary = usePluginStore((state) => state.lastReloadSummary)
|
const lastPluginReloadSummary = usePluginStore((state) => state.lastReloadSummary)
|
||||||
const draftRuntimeSelection = useSessionRuntimeStore((state) => state.selections[DRAFT_RUNTIME_SELECTION_KEY])
|
const draftRuntimeSelection = useSessionRuntimeStore((state) => state.selections[DRAFT_RUNTIME_SELECTION_KEY])
|
||||||
const draftRuntimeSelectionKey = draftRuntimeSelection
|
const draftRuntimeSelectionKey = draftRuntimeSelection
|
||||||
? `${draftRuntimeSelection.providerId ?? 'official'}:${draftRuntimeSelection.modelId}`
|
? `${draftRuntimeSelection.providerId ?? 'official'}:${draftRuntimeSelection.modelId}:${draftRuntimeSelection.effortLevel ?? 'auto'}`
|
||||||
: undefined
|
: undefined
|
||||||
const draftModelLabel = draftRuntimeSelection?.modelId ?? currentModel?.name ?? currentModel?.id
|
const draftModelLabel = draftRuntimeSelection?.modelId ?? currentModel?.name ?? currentModel?.id
|
||||||
const isMobileComposer = useMobileViewport() && !isTauriRuntime()
|
const isMobileComposer = useMobileViewport() && !isTauriRuntime()
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import { ConfirmDialog } from '../components/shared/ConfirmDialog'
|
|||||||
import { Input } from '../components/shared/Input'
|
import { Input } from '../components/shared/Input'
|
||||||
import { Button } from '../components/shared/Button'
|
import { Button } from '../components/shared/Button'
|
||||||
import { Dropdown } from '../components/shared/Dropdown'
|
import { Dropdown } from '../components/shared/Dropdown'
|
||||||
import type { EffortLevel, ThemeMode, UpdateProxyMode, NetworkProxyMode, WebSearchMode, AppMode, ChatSendBehavior } from '../types/settings'
|
import type { ThemeMode, UpdateProxyMode, NetworkProxyMode, WebSearchMode, AppMode, ChatSendBehavior } from '../types/settings'
|
||||||
import type { Locale } from '../i18n'
|
import type { Locale } from '../i18n'
|
||||||
import type { SavedProvider, UpdateProviderInput, ProviderTestResult, ModelMapping, ApiFormat, ProviderAuthStrategy } from '../types/provider'
|
import type { SavedProvider, UpdateProviderInput, ProviderTestResult, ModelMapping, ApiFormat, ProviderAuthStrategy } from '../types/provider'
|
||||||
import type { ProviderPreset } from '../types/providerPreset'
|
import type { ProviderPreset } from '../types/providerPreset'
|
||||||
@ -1430,8 +1430,6 @@ function ProviderFormModal({ open, onClose, mode, provider, presets }: ProviderF
|
|||||||
|
|
||||||
function GeneralSettings() {
|
function GeneralSettings() {
|
||||||
const {
|
const {
|
||||||
effortLevel,
|
|
||||||
setEffort,
|
|
||||||
thinkingEnabled,
|
thinkingEnabled,
|
||||||
setThinkingEnabled,
|
setThinkingEnabled,
|
||||||
locale,
|
locale,
|
||||||
@ -1517,13 +1515,6 @@ function GeneralSettings() {
|
|||||||
setPortableDirDraft(appMode.portableDir ?? appMode.defaultPortableDir ?? '')
|
setPortableDirDraft(appMode.portableDir ?? appMode.defaultPortableDir ?? '')
|
||||||
}, [appMode.defaultPortableDir, appMode.portableDir])
|
}, [appMode.defaultPortableDir, appMode.portableDir])
|
||||||
|
|
||||||
const EFFORT_LABELS: Record<EffortLevel, string> = {
|
|
||||||
low: t('settings.general.effort.low'),
|
|
||||||
medium: t('settings.general.effort.medium'),
|
|
||||||
high: t('settings.general.effort.high'),
|
|
||||||
max: t('settings.general.effort.max'),
|
|
||||||
}
|
|
||||||
|
|
||||||
const LANGUAGES: Array<{ value: Locale; label: string }> = [
|
const LANGUAGES: Array<{ value: Locale; label: string }> = [
|
||||||
{ value: 'en', label: 'English' },
|
{ value: 'en', label: 'English' },
|
||||||
{ value: 'zh', label: '中文' },
|
{ value: 'zh', label: '中文' },
|
||||||
@ -1963,25 +1954,6 @@ function GeneralSettings() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Effort Level */}
|
|
||||||
<h2 className="text-base font-semibold text-[var(--color-text-primary)] mb-1">{t('settings.general.effortTitle')}</h2>
|
|
||||||
<p className="text-sm text-[var(--color-text-tertiary)] mb-3">{t('settings.general.effortDescription')}</p>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
{(['low', 'medium', 'high', 'max'] as EffortLevel[]).map((level) => (
|
|
||||||
<button
|
|
||||||
key={level}
|
|
||||||
onClick={() => setEffort(level)}
|
|
||||||
className={`flex-1 py-2 text-xs font-semibold rounded-lg border transition-all ${
|
|
||||||
effortLevel === level
|
|
||||||
? 'bg-[var(--color-brand)] text-white border-[var(--color-brand)]'
|
|
||||||
: 'border-[var(--color-border)] text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-hover)]'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{EFFORT_LABELS[level]}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-8">
|
<div className="mt-8">
|
||||||
<h2 className="text-base font-semibold text-[var(--color-text-primary)] mb-1">{t('settings.general.thinkingTitle')}</h2>
|
<h2 className="text-base font-semibold text-[var(--color-text-primary)] mb-1">{t('settings.general.thinkingTitle')}</h2>
|
||||||
<p className="text-sm text-[var(--color-text-tertiary)] mb-3">{t('settings.general.thinkingDescription')}</p>
|
<p className="text-sm text-[var(--color-text-tertiary)] mb-3">{t('settings.general.thinkingDescription')}</p>
|
||||||
|
|||||||
@ -1619,6 +1619,7 @@ describe('chatStore history mapping', () => {
|
|||||||
useSessionRuntimeStore.getState().setSelection(TEST_SESSION_ID, {
|
useSessionRuntimeStore.getState().setSelection(TEST_SESSION_ID, {
|
||||||
providerId: 'provider-1',
|
providerId: 'provider-1',
|
||||||
modelId: 'kimi-k2.6',
|
modelId: 'kimi-k2.6',
|
||||||
|
effortLevel: 'high',
|
||||||
})
|
})
|
||||||
|
|
||||||
useChatStore.getState().connectToSession(TEST_SESSION_ID)
|
useChatStore.getState().connectToSession(TEST_SESSION_ID)
|
||||||
@ -1627,6 +1628,7 @@ describe('chatStore history mapping', () => {
|
|||||||
type: 'set_runtime_config',
|
type: 'set_runtime_config',
|
||||||
providerId: 'provider-1',
|
providerId: 'provider-1',
|
||||||
modelId: 'kimi-k2.6',
|
modelId: 'kimi-k2.6',
|
||||||
|
effortLevel: 'high',
|
||||||
})
|
})
|
||||||
expect(sendMock.mock.calls.slice(0, 2)).toEqual([
|
expect(sendMock.mock.calls.slice(0, 2)).toEqual([
|
||||||
[
|
[
|
||||||
@ -1635,6 +1637,7 @@ describe('chatStore history mapping', () => {
|
|||||||
type: 'set_runtime_config',
|
type: 'set_runtime_config',
|
||||||
providerId: 'provider-1',
|
providerId: 'provider-1',
|
||||||
modelId: 'kimi-k2.6',
|
modelId: 'kimi-k2.6',
|
||||||
|
effortLevel: 'high',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[TEST_SESSION_ID, { type: 'prewarm_session' }],
|
[TEST_SESSION_ID, { type: 'prewarm_session' }],
|
||||||
@ -1693,12 +1696,14 @@ describe('chatStore history mapping', () => {
|
|||||||
useChatStore.getState().setSessionRuntime(TEST_SESSION_ID, {
|
useChatStore.getState().setSessionRuntime(TEST_SESSION_ID, {
|
||||||
providerId: null,
|
providerId: null,
|
||||||
modelId: 'claude-opus-4-7',
|
modelId: 'claude-opus-4-7',
|
||||||
|
effortLevel: 'max',
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(sendMock).toHaveBeenCalledWith(TEST_SESSION_ID, {
|
expect(sendMock).toHaveBeenCalledWith(TEST_SESSION_ID, {
|
||||||
type: 'set_runtime_config',
|
type: 'set_runtime_config',
|
||||||
providerId: null,
|
providerId: null,
|
||||||
modelId: 'claude-opus-4-7',
|
modelId: 'claude-opus-4-7',
|
||||||
|
effortLevel: 'max',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -60,6 +60,7 @@ function resolveRuntimeRefreshSelection(
|
|||||||
modelId: modelIds.has(currentSelection.modelId)
|
modelId: modelIds.has(currentSelection.modelId)
|
||||||
? currentSelection.modelId
|
? currentSelection.modelId
|
||||||
: provider.models.main,
|
: provider.models.main,
|
||||||
|
...(currentSelection.effortLevel ? { effortLevel: currentSelection.effortLevel } : {}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -136,7 +136,7 @@ const DEFAULT_NETWORK_SETTINGS: NetworkSettings = {
|
|||||||
export const useSettingsStore = create<SettingsStore>((set, get) => ({
|
export const useSettingsStore = create<SettingsStore>((set, get) => ({
|
||||||
permissionMode: 'default',
|
permissionMode: 'default',
|
||||||
currentModel: null,
|
currentModel: null,
|
||||||
effortLevel: 'medium',
|
effortLevel: 'max',
|
||||||
thinkingEnabled: true,
|
thinkingEnabled: true,
|
||||||
availableModels: [],
|
availableModels: [],
|
||||||
activeProviderName: null,
|
activeProviderName: null,
|
||||||
|
|||||||
@ -1,4 +1,7 @@
|
|||||||
|
import type { EffortLevel } from './settings'
|
||||||
|
|
||||||
export type RuntimeSelection = {
|
export type RuntimeSelection = {
|
||||||
providerId: string | null
|
providerId: string | null
|
||||||
modelId: string
|
modelId: string
|
||||||
|
effortLevel?: EffortLevel
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2930,6 +2930,7 @@ describe('WebSocket Chat Integration', () => {
|
|||||||
type: 'set_runtime_config',
|
type: 'set_runtime_config',
|
||||||
providerId: providerA.id,
|
providerId: providerA.id,
|
||||||
modelId: 'model-a-sonnet',
|
modelId: 'model-a-sonnet',
|
||||||
|
effortLevel: 'medium',
|
||||||
}))
|
}))
|
||||||
ws.send(JSON.stringify({ type: 'user_message', content: 'first turn' }))
|
ws.send(JSON.stringify({ type: 'user_message', content: 'first turn' }))
|
||||||
phase = 'turn1'
|
phase = 'turn1'
|
||||||
@ -2950,6 +2951,7 @@ describe('WebSocket Chat Integration', () => {
|
|||||||
type: 'set_runtime_config',
|
type: 'set_runtime_config',
|
||||||
providerId: providerB.id,
|
providerId: providerB.id,
|
||||||
modelId: 'model-b-opus',
|
modelId: 'model-b-opus',
|
||||||
|
effortLevel: 'max',
|
||||||
}))
|
}))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -2989,6 +2991,7 @@ describe('WebSocket Chat Integration', () => {
|
|||||||
options: {
|
options: {
|
||||||
providerId: providerA.id,
|
providerId: providerA.id,
|
||||||
model: 'model-a-sonnet',
|
model: 'model-a-sonnet',
|
||||||
|
effort: 'medium',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
expect(startCalls[1]).toMatchObject({
|
expect(startCalls[1]).toMatchObject({
|
||||||
@ -2996,6 +2999,7 @@ describe('WebSocket Chat Integration', () => {
|
|||||||
options: {
|
options: {
|
||||||
providerId: providerB.id,
|
providerId: providerB.id,
|
||||||
model: 'model-b-opus',
|
model: 'model-b-opus',
|
||||||
|
effort: 'max',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@ -419,9 +419,9 @@ describe('Business Flow: Models & Effort', () => {
|
|||||||
expect(status).toBe(400)
|
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')
|
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'])
|
expect(data.available).toEqual(['low', 'medium', 'high', 'max'])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -691,7 +691,7 @@ describe('Models API', () => {
|
|||||||
|
|
||||||
expect(res.status).toBe(200)
|
expect(res.status).toBe(200)
|
||||||
const body = await res.json()
|
const body = await res.json()
|
||||||
expect(body.level).toBe('medium')
|
expect(body.level).toBe('max')
|
||||||
expect(body.available).toEqual(['low', 'medium', 'high', 'max'])
|
expect(body.available).toEqual(['low', 'medium', 'high', 'max'])
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -704,7 +704,7 @@ describe('Models API', () => {
|
|||||||
|
|
||||||
expect(res.status).toBe(200)
|
expect(res.status).toBe(200)
|
||||||
const body = await res.json()
|
const body = await res.json()
|
||||||
expect(body.level).toBe('medium')
|
expect(body.level).toBe('max')
|
||||||
expect(body.available).toEqual(['low', 'medium', 'high', 'max'])
|
expect(body.available).toEqual(['low', 'medium', 'high', 'max'])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -46,7 +46,7 @@ const DEFAULT_MODELS = [
|
|||||||
const EFFORT_LEVELS = ['low', 'medium', 'high', 'max'] as const
|
const EFFORT_LEVELS = ['low', 'medium', 'high', 'max'] as const
|
||||||
|
|
||||||
const DEFAULT_MODEL = 'claude-opus-4-7'
|
const DEFAULT_MODEL = 'claude-opus-4-7'
|
||||||
const DEFAULT_EFFORT = 'medium'
|
const DEFAULT_EFFORT = 'max'
|
||||||
|
|
||||||
const settingsService = new SettingsService()
|
const settingsService = new SettingsService()
|
||||||
const providerService = new ProviderService()
|
const providerService = new ProviderService()
|
||||||
|
|||||||
@ -72,6 +72,9 @@ export type SessionLaunchInfo = {
|
|||||||
transcriptMessageCount: number
|
transcriptMessageCount: number
|
||||||
customTitle: string | null
|
customTitle: string | null
|
||||||
permissionMode?: string
|
permissionMode?: string
|
||||||
|
runtimeProviderId?: string | null
|
||||||
|
runtimeModelId?: string
|
||||||
|
effortLevel?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TrimSessionResult = {
|
export type TrimSessionResult = {
|
||||||
@ -228,6 +231,9 @@ type SessionListSummary = {
|
|||||||
messageCount: number
|
messageCount: number
|
||||||
workDir: string | null
|
workDir: string | null
|
||||||
permissionMode?: string
|
permissionMode?: string
|
||||||
|
runtimeProviderId?: string | null
|
||||||
|
runtimeModelId?: string
|
||||||
|
effortLevel?: string
|
||||||
repository?: PreparedSessionWorkspace['repository']
|
repository?: PreparedSessionWorkspace['repository']
|
||||||
worktreeSession?: PersistedWorktreeSession | null
|
worktreeSession?: PersistedWorktreeSession | null
|
||||||
}
|
}
|
||||||
@ -239,6 +245,7 @@ const VALID_SESSION_PERMISSION_MODES = new Set([
|
|||||||
'bypassPermissions',
|
'bypassPermissions',
|
||||||
'dontAsk',
|
'dontAsk',
|
||||||
])
|
])
|
||||||
|
const VALID_SESSION_EFFORT_LEVELS = new Set(['low', 'medium', 'high', 'max'])
|
||||||
|
|
||||||
type ContentBlock = Record<string, unknown>
|
type ContentBlock = Record<string, unknown>
|
||||||
|
|
||||||
@ -348,6 +355,9 @@ export class SessionService {
|
|||||||
let latestWorkDir: string | null = null
|
let latestWorkDir: string | null = null
|
||||||
let latestCwd: string | null = null
|
let latestCwd: string | null = null
|
||||||
let permissionMode: string | undefined
|
let permissionMode: string | undefined
|
||||||
|
let runtimeProviderId: string | null | undefined
|
||||||
|
let runtimeModelId: string | undefined
|
||||||
|
let effortLevel: string | undefined
|
||||||
let repository: PreparedSessionWorkspace['repository'] | undefined
|
let repository: PreparedSessionWorkspace['repository'] | undefined
|
||||||
let worktreeSession: PersistedWorktreeSession | null | undefined
|
let worktreeSession: PersistedWorktreeSession | null | undefined
|
||||||
|
|
||||||
@ -393,6 +403,21 @@ export class SessionService {
|
|||||||
) {
|
) {
|
||||||
permissionMode = entry.permissionMode
|
permissionMode = entry.permissionMode
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
(entry as Record<string, unknown>).runtimeProviderId === null ||
|
||||||
|
typeof (entry as Record<string, unknown>).runtimeProviderId === 'string'
|
||||||
|
) {
|
||||||
|
runtimeProviderId = (entry as Record<string, unknown>).runtimeProviderId as string | null
|
||||||
|
}
|
||||||
|
if (typeof (entry as Record<string, unknown>).runtimeModelId === 'string') {
|
||||||
|
runtimeModelId = (entry as Record<string, unknown>).runtimeModelId as string
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
typeof (entry as Record<string, unknown>).effortLevel === 'string' &&
|
||||||
|
VALID_SESSION_EFFORT_LEVELS.has((entry as Record<string, unknown>).effortLevel as string)
|
||||||
|
) {
|
||||||
|
effortLevel = (entry as Record<string, unknown>).effortLevel as string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof entry.cwd === 'string' && entry.cwd.trim()) {
|
if (typeof entry.cwd === 'string' && entry.cwd.trim()) {
|
||||||
@ -454,6 +479,9 @@ export class SessionService {
|
|||||||
messageCount,
|
messageCount,
|
||||||
workDir: latestWorkDir || latestCwd || this.desanitizePath(projectDir),
|
workDir: latestWorkDir || latestCwd || this.desanitizePath(projectDir),
|
||||||
...(permissionMode ? { permissionMode } : {}),
|
...(permissionMode ? { permissionMode } : {}),
|
||||||
|
...(runtimeProviderId !== undefined ? { runtimeProviderId } : {}),
|
||||||
|
...(runtimeModelId ? { runtimeModelId } : {}),
|
||||||
|
...(effortLevel ? { effortLevel } : {}),
|
||||||
...(repository ? { repository } : {}),
|
...(repository ? { repository } : {}),
|
||||||
...(worktreeSession !== undefined ? { worktreeSession } : {}),
|
...(worktreeSession !== undefined ? { worktreeSession } : {}),
|
||||||
}
|
}
|
||||||
@ -1846,11 +1874,29 @@ export class SessionService {
|
|||||||
const worktreeSession = this.resolveWorktreeSessionFromEntries(entries)
|
const worktreeSession = this.resolveWorktreeSessionFromEntries(entries)
|
||||||
const permissionMode = this.resolvePermissionModeFromEntries(entries)
|
const permissionMode = this.resolvePermissionModeFromEntries(entries)
|
||||||
let customTitle: string | null = null
|
let customTitle: string | null = null
|
||||||
|
let runtimeProviderId: string | null | undefined
|
||||||
|
let runtimeModelId: string | undefined
|
||||||
|
let effortLevel: string | undefined
|
||||||
|
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
if (entry.type === 'custom-title' && typeof entry.customTitle === 'string') {
|
if (entry.type === 'custom-title' && typeof entry.customTitle === 'string') {
|
||||||
customTitle = entry.customTitle
|
customTitle = entry.customTitle
|
||||||
}
|
}
|
||||||
|
if (entry.type === 'session-meta') {
|
||||||
|
const record = entry as Record<string, unknown>
|
||||||
|
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)
|
const transcriptMessageCount = this.countTranscriptMessages(entries)
|
||||||
|
|
||||||
@ -1863,6 +1909,9 @@ export class SessionService {
|
|||||||
transcriptMessageCount,
|
transcriptMessageCount,
|
||||||
customTitle,
|
customTitle,
|
||||||
permissionMode,
|
permissionMode,
|
||||||
|
...(runtimeProviderId !== undefined ? { runtimeProviderId } : {}),
|
||||||
|
...(runtimeModelId ? { runtimeModelId } : {}),
|
||||||
|
...(effortLevel ? { effortLevel } : {}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1928,6 +1977,9 @@ export class SessionService {
|
|||||||
customTitle?: string | null
|
customTitle?: string | null
|
||||||
repository?: PreparedSessionWorkspace['repository']
|
repository?: PreparedSessionWorkspace['repository']
|
||||||
permissionMode?: string
|
permissionMode?: string
|
||||||
|
runtimeProviderId?: string | null
|
||||||
|
runtimeModelId?: string
|
||||||
|
effortLevel?: string
|
||||||
}
|
}
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const matches = await this.findSessionFiles(sessionId)
|
const matches = await this.findSessionFiles(sessionId)
|
||||||
@ -1957,6 +2009,13 @@ export class SessionService {
|
|||||||
...(metadata.permissionMode && VALID_SESSION_PERMISSION_MODES.has(metadata.permissionMode)
|
...(metadata.permissionMode && VALID_SESSION_PERMISSION_MODES.has(metadata.permissionMode)
|
||||||
? { permissionMode: 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(),
|
timestamp: new Date().toISOString(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -24,7 +24,7 @@ export type ClientMessage =
|
|||||||
response: ComputerUsePermissionResponse
|
response: ComputerUsePermissionResponse
|
||||||
}
|
}
|
||||||
| { type: 'set_permission_mode'; mode: string }
|
| { 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: 'stop_generation' }
|
||||||
| { type: 'ping' }
|
| { type: 'ping' }
|
||||||
|
|
||||||
|
|||||||
@ -70,15 +70,19 @@ const sessionTitleState = new Map<string, {
|
|||||||
const runtimeOverrides = new Map<string, {
|
const runtimeOverrides = new Map<string, {
|
||||||
providerId: string | null
|
providerId: string | null
|
||||||
modelId: string
|
modelId: string
|
||||||
|
effort?: string
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const runtimeTransitionPromises = new Map<string, Promise<void>>()
|
const runtimeTransitionPromises = new Map<string, Promise<void>>()
|
||||||
const sessionStartupPromises = new Map<string, Promise<void>>()
|
const sessionStartupPromises = new Map<string, Promise<void>>()
|
||||||
|
const runtimeOverrideVersions = new Map<string, number>()
|
||||||
|
const sessionStartupRuntimeVersions = new Map<string, number>()
|
||||||
const lastResolvedStartupWorkDirs = new Map<string, string>()
|
const lastResolvedStartupWorkDirs = new Map<string, string>()
|
||||||
const prewarmPendingSessions = new Set<string>()
|
const prewarmPendingSessions = new Set<string>()
|
||||||
const prewarmedSessions = new Set<string>()
|
const prewarmedSessions = new Set<string>()
|
||||||
const prewarmIdleTimers = new Map<string, ReturnType<typeof setTimeout>>()
|
const prewarmIdleTimers = new Map<string, ReturnType<typeof setTimeout>>()
|
||||||
const DEFAULT_PREWARM_IDLE_TIMEOUT_MS = 5 * 60_000
|
const DEFAULT_PREWARM_IDLE_TIMEOUT_MS = 5 * 60_000
|
||||||
|
const VALID_EFFORT_LEVELS = new Set(['low', 'medium', 'high', 'max'])
|
||||||
|
|
||||||
async function sendRepositoryStartupStatus(
|
async function sendRepositoryStartupStatus(
|
||||||
ws: ServerWebSocket<WebSocketData>,
|
ws: ServerWebSocket<WebSocketData>,
|
||||||
@ -549,44 +553,73 @@ async function handleSetRuntimeConfig(
|
|||||||
})
|
})
|
||||||
return
|
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 = {
|
const nextOverride = {
|
||||||
providerId: message.providerId ?? null,
|
providerId: message.providerId ?? null,
|
||||||
modelId,
|
modelId,
|
||||||
|
...(effortLevel ? { effort: effortLevel } : {}),
|
||||||
}
|
}
|
||||||
const prevOverride = runtimeOverrides.get(sessionId)
|
const prevOverride = runtimeOverrides.get(sessionId)
|
||||||
runtimeOverrides.set(sessionId, nextOverride)
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
prevOverride &&
|
prevOverride &&
|
||||||
prevOverride.providerId === nextOverride.providerId &&
|
prevOverride.providerId === nextOverride.providerId &&
|
||||||
prevOverride.modelId === nextOverride.modelId
|
prevOverride.modelId === nextOverride.modelId &&
|
||||||
|
prevOverride.effort === nextOverride.effort
|
||||||
) {
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!conversationService.hasSession(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
|
||||||
|
}
|
||||||
|
|
||||||
const pendingStartup = sessionStartupPromises.get(sessionId)
|
const pendingStartup = sessionStartupPromises.get(sessionId)
|
||||||
if (pendingStartup) {
|
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 enqueueRuntimeTransition(sessionId, async () => {
|
||||||
|
await persistSessionRuntimeConfig(sessionId, nextOverride)
|
||||||
await pendingStartup.catch(() => undefined)
|
await pendingStartup.catch(() => undefined)
|
||||||
const currentOverride = runtimeOverrides.get(sessionId)
|
const currentOverride = runtimeOverrides.get(sessionId)
|
||||||
if (
|
if (
|
||||||
currentOverride?.providerId !== nextOverride.providerId ||
|
currentOverride?.providerId !== nextOverride.providerId ||
|
||||||
currentOverride.modelId !== nextOverride.modelId ||
|
currentOverride.modelId !== nextOverride.modelId ||
|
||||||
|
currentOverride.effort !== nextOverride.effort ||
|
||||||
!conversationService.hasSession(sessionId)
|
!conversationService.hasSession(sessionId)
|
||||||
) {
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await restartSessionWithRuntimeConfig(ws, sessionId)
|
await restartSessionWithRuntimeConfig(ws, sessionId)
|
||||||
})
|
})
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await enqueueRuntimeTransition(sessionId, () =>
|
await persistSessionRuntimeConfig(sessionId, nextOverride)
|
||||||
restartSessionWithRuntimeConfig(ws, sessionId),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function restartSessionWithPermissionMode(
|
async function restartSessionWithPermissionMode(
|
||||||
@ -648,6 +681,24 @@ async function persistSessionPermissionMode(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function persistSessionRuntimeConfig(
|
||||||
|
sessionId: string,
|
||||||
|
runtime: { providerId: string | null; modelId: string; effort?: string },
|
||||||
|
): Promise<void> {
|
||||||
|
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(
|
async function restartSessionWithRuntimeConfig(
|
||||||
ws: ServerWebSocket<WebSocketData>,
|
ws: ServerWebSocket<WebSocketData>,
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
@ -981,6 +1032,9 @@ async function ensureCliSessionStarted(
|
|||||||
|
|
||||||
if (conversationService.hasSession(sessionId)) return
|
if (conversationService.hasSession(sessionId)) return
|
||||||
|
|
||||||
|
const startupRuntimeVersion = runtimeOverrideVersions.get(sessionId) ?? 0
|
||||||
|
sessionStartupRuntimeVersions.set(sessionId, startupRuntimeVersion)
|
||||||
|
|
||||||
const startup = (async () => {
|
const startup = (async () => {
|
||||||
const workDir = await resolveSessionWorkDir(sessionId)
|
const workDir = await resolveSessionWorkDir(sessionId)
|
||||||
lastResolvedStartupWorkDirs.set(sessionId, workDir)
|
lastResolvedStartupWorkDirs.set(sessionId, workDir)
|
||||||
@ -999,6 +1053,7 @@ async function ensureCliSessionStarted(
|
|||||||
} finally {
|
} finally {
|
||||||
if (sessionStartupPromises.get(sessionId) === startup) {
|
if (sessionStartupPromises.get(sessionId) === startup) {
|
||||||
sessionStartupPromises.delete(sessionId)
|
sessionStartupPromises.delete(sessionId)
|
||||||
|
sessionStartupRuntimeVersions.delete(sessionId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1859,10 +1914,23 @@ function isKnownRuntimeProviderId(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function getRuntimeSettings(sessionId?: string): Promise<RuntimeSettings> {
|
async function getRuntimeSettings(sessionId?: string): Promise<RuntimeSettings> {
|
||||||
|
const launchInfo = sessionId
|
||||||
|
? await sessionService.getSessionLaunchInfo(sessionId).catch(() => null)
|
||||||
|
: null
|
||||||
const sessionPermissionMode = sessionId
|
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
|
: undefined
|
||||||
const runtimeOverride = sessionId ? runtimeOverrides.get(sessionId) : undefined
|
|
||||||
if (runtimeOverride) {
|
if (runtimeOverride) {
|
||||||
if (typeof runtimeOverride.providerId === 'string') {
|
if (typeof runtimeOverride.providerId === 'string') {
|
||||||
const { providers } = await providerService.listProviders()
|
const { providers } = await providerService.listProviders()
|
||||||
@ -1881,16 +1949,12 @@ async function getRuntimeSettings(sessionId?: string): Promise<RuntimeSettings>
|
|||||||
}
|
}
|
||||||
|
|
||||||
const userSettings = await settingsService.getUserSettings()
|
const userSettings = await settingsService.getUserSettings()
|
||||||
const effort =
|
|
||||||
typeof userSettings.effort === 'string' && userSettings.effort.trim()
|
|
||||||
? userSettings.effort
|
|
||||||
: undefined
|
|
||||||
const thinking = resolveDesktopThinkingMode(userSettings)
|
const thinking = resolveDesktopThinkingMode(userSettings)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
permissionMode: sessionPermissionMode ?? await settingsService.getPermissionMode().catch(() => undefined),
|
permissionMode: sessionPermissionMode ?? await settingsService.getPermissionMode().catch(() => undefined),
|
||||||
model: runtimeOverride.modelId,
|
model: runtimeOverride.modelId,
|
||||||
effort,
|
effort: runtimeOverride.effort,
|
||||||
thinking,
|
thinking,
|
||||||
providerId: runtimeOverride.providerId,
|
providerId: runtimeOverride.providerId,
|
||||||
}
|
}
|
||||||
@ -1900,6 +1964,7 @@ async function getRuntimeSettings(sessionId?: string): Promise<RuntimeSettings>
|
|||||||
return {
|
return {
|
||||||
...defaults,
|
...defaults,
|
||||||
permissionMode: sessionPermissionMode ?? defaults.permissionMode,
|
permissionMode: sessionPermissionMode ?? defaults.permissionMode,
|
||||||
|
effort: launchInfo?.effortLevel ?? defaults.effort,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1995,6 +2060,7 @@ async function buildSessionStartupDiagnosticMessage(
|
|||||||
if (runtimeOverride) {
|
if (runtimeOverride) {
|
||||||
lines.push(`- runtimeOverride.providerId: ${runtimeOverride.providerId ?? '(official)'}`)
|
lines.push(`- runtimeOverride.providerId: ${runtimeOverride.providerId ?? '(official)'}`)
|
||||||
lines.push(`- runtimeOverride.modelId: ${runtimeOverride.modelId}`)
|
lines.push(`- runtimeOverride.modelId: ${runtimeOverride.modelId}`)
|
||||||
|
lines.push(`- runtimeOverride.effort: ${runtimeOverride.effort ?? '(auto)'}`)
|
||||||
} else {
|
} else {
|
||||||
lines.push('- runtimeOverride: (none)')
|
lines.push('- runtimeOverride: (none)')
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
||||||
import { get3PModelCapabilityOverride } from '../model/modelSupportOverrides.js'
|
import { get3PModelCapabilityOverride } from '../model/modelSupportOverrides.js'
|
||||||
import { resolveSideQueryThinkingConfig } from '../sideQuery.js'
|
import { resolveSideQueryThinkingConfig } from '../sideQuery.js'
|
||||||
|
import { modelSupportsEffort, modelSupportsMaxEffort } from '../effort.js'
|
||||||
import {
|
import {
|
||||||
modelSupportsAdaptiveThinking,
|
modelSupportsAdaptiveThinking,
|
||||||
modelSupportsThinking,
|
modelSupportsThinking,
|
||||||
@ -87,6 +88,8 @@ describe('provider-aware thinking support', () => {
|
|||||||
|
|
||||||
expect(modelSupportsThinking('deepseek-v4-pro')).toBe(true)
|
expect(modelSupportsThinking('deepseek-v4-pro')).toBe(true)
|
||||||
expect(modelSupportsAdaptiveThinking('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)
|
expect(shouldSendExplicitDisabledThinking()).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -98,6 +101,17 @@ describe('provider-aware thinking support', () => {
|
|||||||
|
|
||||||
expect(modelSupportsThinking('MiniMax-M2.7')).toBe(true)
|
expect(modelSupportsThinking('MiniMax-M2.7')).toBe(true)
|
||||||
expect(modelSupportsAdaptiveThinking('MiniMax-M2.7')).toBe(false)
|
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', () => {
|
test('side queries inherit explicit disabled thinking for opted-in providers', () => {
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { isUltrathinkEnabled } from './thinking.js'
|
|||||||
import { getInitialSettings } from './settings/settings.js'
|
import { getInitialSettings } from './settings/settings.js'
|
||||||
import { isProSubscriber, isMaxSubscriber, isTeamSubscriber } from './auth.js'
|
import { isProSubscriber, isMaxSubscriber, isTeamSubscriber } from './auth.js'
|
||||||
import { getFeatureValue_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.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 { get3PModelCapabilityOverride } from './model/modelSupportOverrides.js'
|
||||||
import { isEnvTruthy } from './envUtils.js'
|
import { isEnvTruthy } from './envUtils.js'
|
||||||
import type { EffortLevel } from 'src/entrypoints/sdk/runtimeTypes.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.
|
// Default to true for unknown model strings on 1P.
|
||||||
// Do not default to true for 3P as they have different formats for their
|
// Do not default to true for 3P as they have different formats for their
|
||||||
// model strings (ex. anthropics/claude-code#30795)
|
// 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.
|
// @[MODEL LAUNCH]: Add the new model to the allowlist if it supports 'max' effort.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user