diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddDropdown.svelte b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddDropdown.svelte index 699605fd82..905c2fe6f0 100644 --- a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddDropdown.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddDropdown.svelte @@ -71,7 +71,9 @@
- + + {#snippet child({ props })} {#if reasoning.modelSupportsThinking} - + {#if reasoning.thinkingEnabled} - {:else} + {:else if reasoning.isOff} + {:else} + {/if} - {reasoning.thinkingEnabled ? reasoning.currentEffort : 'off'} + {reasoning.currentEffort} @@ -37,14 +37,13 @@ > {#each reasoning.levels as level (level.value)} {@const tokenLabel = reasoning.tokenLabel(level)} - + {/each} diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddSheet.svelte b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddSheet.svelte index c533acc750..021e0e4539 100644 --- a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddSheet.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddSheet.svelte @@ -116,14 +116,16 @@ {#if reasoning.thinkingEnabled} - {:else} + {:else if reasoning.isOff} + {:else} + {/if} Reasoning - {reasoning.thinkingEnabled ? reasoning.currentEffort : 'off'} + {reasoning.currentEffort} diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormReasoningEffortSubmenu.svelte b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormReasoningEffortSubmenu.svelte deleted file mode 100644 index 0a4277e94f..0000000000 --- a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormReasoningEffortSubmenu.svelte +++ /dev/null @@ -1,127 +0,0 @@ - - -{#if modelSupportsThinking} - - - {#if thinkingEnabled} - - {:else} - - {/if} - - Thinking - - {#if thinkingEnabled} - {currentEffort} - {:else} - off - {/if} - - - - {#each REASONING_EFFORT_LEVELS as level (level.value)} - - {/each} - - -{/if} diff --git a/tools/ui/src/lib/constants/reasoning-effort.ts b/tools/ui/src/lib/constants/reasoning-effort.ts index 28a24420e6..f21ea588ad 100644 --- a/tools/ui/src/lib/constants/reasoning-effort.ts +++ b/tools/ui/src/lib/constants/reasoning-effort.ts @@ -6,6 +6,7 @@ import type { ReasoningEffortLevel } from '$lib/types'; * Keys match the ReasoningEffort enum values for type-safe lookups. */ export const REASONING_EFFORT_LABELS: Record = { + [ReasoningEffort.DEFAULT]: 'Default', [ReasoningEffort.OFF]: 'Off', [ReasoningEffort.LOW]: 'Low', [ReasoningEffort.MEDIUM]: 'Medium', @@ -14,7 +15,8 @@ export const REASONING_EFFORT_LABELS: Record = { }; export const REASONING_EFFORT_LEVELS: ReasoningEffortLevel[] = [ - { value: ReasoningEffort.OFF, label: 'Off', isOff: true }, + { value: ReasoningEffort.DEFAULT, label: 'Default' }, + { value: ReasoningEffort.OFF, label: 'Off' }, { value: ReasoningEffort.LOW, label: 'Low' }, { value: ReasoningEffort.MEDIUM, label: 'Medium' }, { value: ReasoningEffort.HIGH, label: 'High' }, diff --git a/tools/ui/src/lib/enums/reasoning-effort.enums.ts b/tools/ui/src/lib/enums/reasoning-effort.enums.ts index 172f118e7f..6bf86ed4ec 100644 --- a/tools/ui/src/lib/enums/reasoning-effort.enums.ts +++ b/tools/ui/src/lib/enums/reasoning-effort.enums.ts @@ -3,6 +3,7 @@ * These values are sent to the server and mapped to token budgets. */ export enum ReasoningEffort { + DEFAULT = 'default', OFF = 'off', LOW = 'low', MEDIUM = 'medium', diff --git a/tools/ui/src/lib/hooks/use-reasoning-menu.svelte.ts b/tools/ui/src/lib/hooks/use-reasoning-menu.svelte.ts index 6a459e66bd..ce9b77884d 100644 --- a/tools/ui/src/lib/hooks/use-reasoning-menu.svelte.ts +++ b/tools/ui/src/lib/hooks/use-reasoning-menu.svelte.ts @@ -17,6 +17,7 @@ import { isRouterMode } from '$lib/stores/server.svelte'; export interface UseReasoningMenuReturn { readonly modelSupportsThinking: boolean; readonly thinkingEnabled: boolean; + readonly isOff: boolean; readonly currentEffort: ReasoningEffort; readonly levels: ReasoningEffortLevel[]; isSelected(level: ReasoningEffortLevel): boolean; @@ -59,8 +60,10 @@ export function useReasoningMenu(): UseReasoningMenuReturn { return supportsThinking() || modelSupportsThinkingFromMessages; }); - const thinkingEnabled = $derived(conversationsStore.getThinkingEnabled()); const currentEffort = $derived(conversationsStore.getReasoningEffort()); + const thinkingEnabled = $derived( + currentEffort !== ReasoningEffort.OFF && currentEffort !== ReasoningEffort.DEFAULT + ); return { get modelSupportsThinking() { @@ -69,6 +72,9 @@ export function useReasoningMenu(): UseReasoningMenuReturn { get thinkingEnabled() { return thinkingEnabled; }, + get isOff() { + return currentEffort === ReasoningEffort.OFF; + }, get currentEffort() { return currentEffort; }, @@ -76,20 +82,15 @@ export function useReasoningMenu(): UseReasoningMenuReturn { return REASONING_EFFORT_LEVELS; }, isSelected(level: ReasoningEffortLevel): boolean { - if (level.isOff) return !thinkingEnabled; - return thinkingEnabled && currentEffort === level.value; + return currentEffort === level.value; }, tokenLabel(level: ReasoningEffortLevel): string | null { - if (level.isOff) return null; + if (level.value === ReasoningEffort.DEFAULT) return 'Model default'; const tokens = REASONING_EFFORT_TOKENS[level.value]; + if (tokens === undefined) return null; return tokens === -1 ? 'Unlimited' : `Max ${tokens.toLocaleString()} tokens`; }, select(level: ReasoningEffortLevel): void { - if (level.isOff) { - conversationsStore.setThinkingEnabled(false); - return; - } - conversationsStore.setThinkingEnabled(true); conversationsStore.setReasoningEffort(level.value as ReasoningEffort); } }; diff --git a/tools/ui/src/lib/services/chat.service.ts b/tools/ui/src/lib/services/chat.service.ts index 15f4cc3b5f..d2455614fb 100644 --- a/tools/ui/src/lib/services/chat.service.ts +++ b/tools/ui/src/lib/services/chat.service.ts @@ -271,10 +271,14 @@ export class ChatService { const reasoningBudgetTokens = enableThinking && reasoningEffort ? (REASONING_EFFORT_TOKENS[reasoningEffort] ?? -1) : -1; - requestBody.chat_template_kwargs = { - ...(requestBody.chat_template_kwargs ?? {}), - enable_thinking: enableThinking - }; + // an explicit user choice injects the kwarg, otherwise it is omitted so + // the server default applies (--reasoning flag or chat template) + if (enableThinking !== undefined) { + requestBody.chat_template_kwargs = { + ...(requestBody.chat_template_kwargs ?? {}), + enable_thinking: enableThinking + }; + } if (reasoningBudgetTokens >= 0) { requestBody.thinking_budget_tokens = reasoningBudgetTokens; diff --git a/tools/ui/src/lib/stores/chat.svelte.ts b/tools/ui/src/lib/stores/chat.svelte.ts index ab418ed9cd..801d6c838f 100644 --- a/tools/ui/src/lib/stores/chat.svelte.ts +++ b/tools/ui/src/lib/stores/chat.svelte.ts @@ -2373,9 +2373,12 @@ class ChatStore { if (currentConfig.excludeReasoningFromContext) apiOptions.excludeReasoningFromContext = true; - apiOptions.enableThinking = conversationsStore.getThinkingEnabled(); + // an explicit reasoning choice overrides the server default, DEFAULT sends nothing const effort = conversationsStore.getReasoningEffort(); - if (effort !== ReasoningEffort.OFF) apiOptions.reasoningEffort = effort; + if (effort !== ReasoningEffort.DEFAULT) { + apiOptions.enableThinking = effort !== ReasoningEffort.OFF; + if (effort !== ReasoningEffort.OFF) apiOptions.reasoningEffort = effort; + } if (hasValue(currentConfig.temperature)) apiOptions.temperature = Number(currentConfig.temperature); diff --git a/tools/ui/src/lib/stores/conversations.svelte.ts b/tools/ui/src/lib/stores/conversations.svelte.ts index 5c54e72ace..9649cfe03d 100644 --- a/tools/ui/src/lib/stores/conversations.svelte.ts +++ b/tools/ui/src/lib/stores/conversations.svelte.ts @@ -80,25 +80,17 @@ class ConversationsStore { /** Whether the store has been initialized */ isInitialized = $state(false); - /** Global (non-conversation-specific) thinking toggle default, derived from reasoning effort */ - pendingThinkingEnabled = $state(false); - /** Global (non-conversation-specific) reasoning effort default */ - pendingReasoningEffort = $state( - ConversationsStore.loadReasoningEffortDefault() - ); + pendingReasoningEffort = $state(ConversationsStore.loadReasoningEffortDefault()); - /** Last non-off reasoning effort, restored when re-enabling thinking globally */ - private lastNonOffEffort: ReasoningEffort | null = null; - - /** Load reasoning effort default from localStorage */ - private static loadReasoningEffortDefault(): ReasoningEffort | ReasoningEffort.OFF { - if (typeof globalThis.localStorage === 'undefined') return ReasoningEffort.OFF; + /** Load reasoning effort default from localStorage, DEFAULT defers to the server */ + private static loadReasoningEffortDefault(): ReasoningEffort { + if (typeof globalThis.localStorage === 'undefined') return ReasoningEffort.DEFAULT; try { const raw = localStorage.getItem(REASONING_EFFORT_DEFAULT_LOCALSTORAGE_KEY); - return (raw as ReasoningEffort | ReasoningEffort.OFF) || ReasoningEffort.OFF; + return (raw as ReasoningEffort) || ReasoningEffort.DEFAULT; } catch { - return ReasoningEffort.OFF; + return ReasoningEffort.DEFAULT; } } @@ -235,17 +227,10 @@ class ConversationsStore { // servers without a per-conversation override to `mcpServers[i].enabled`, // and only explicit toggles are stored on the conversation. - // Inherit global thinking/reasoning defaults into the new conversation - const thinkingEnabled = this.getThinkingEnabled(); - conversation.thinkingEnabled = thinkingEnabled; - conversation.reasoningEffort = - this.pendingReasoningEffort === ReasoningEffort.OFF ? undefined : this.pendingReasoningEffort; + // Inherit the global reasoning default into the new conversation + conversation.reasoningEffort = this.pendingReasoningEffort; await DatabaseService.updateConversation(conversation.id, { - thinkingEnabled, - reasoningEffort: - this.pendingReasoningEffort === ReasoningEffort.OFF - ? undefined - : this.pendingReasoningEffort + reasoningEffort: this.pendingReasoningEffort }); this.conversations = [conversation, ...this.conversations]; @@ -793,63 +778,21 @@ class ConversationsStore { await this.setMcpServerOverride(serverId, undefined); } - /** - * Gets the effective thinking-enabled state for the active conversation. - * Returns the conversation override if set, otherwise the global default. - */ - getThinkingEnabled(): boolean { - if (this.activeConversation) { - if (this.activeConversation.thinkingEnabled !== undefined) { - return this.activeConversation.thinkingEnabled; - } - } - return this.getReasoningEffort() !== ReasoningEffort.OFF; - } - - /** - * Sets the thinking-enabled state for the active conversation. - * If no conversation exists, stores the global default. - * @param enabled - The enabled state - */ - async setThinkingEnabled(enabled: boolean): Promise { - if (!this.activeConversation) { - if (enabled) { - const effort = this.lastNonOffEffort ?? ReasoningEffort.LOW; - this.pendingReasoningEffort = effort; - this.saveReasoningEffortDefaults(); - } else { - if (this.pendingReasoningEffort !== ReasoningEffort.OFF) { - this.lastNonOffEffort = this.pendingReasoningEffort; - } - this.pendingReasoningEffort = ReasoningEffort.OFF; - this.saveReasoningEffortDefaults(); - } - return; - } - - this.activeConversation = { - ...this.activeConversation, - thinkingEnabled: enabled - }; - - await DatabaseService.updateConversation(this.activeConversation.id, { - thinkingEnabled: enabled - }); - - const convIndex = this.conversations.findIndex((c) => c.id === this.activeConversation!.id); - if (convIndex !== -1) { - this.conversations[convIndex].thinkingEnabled = enabled; - this.conversations = [...this.conversations]; - } - } - /** * Gets the effective reasoning effort for the active conversation. * Returns the conversation override if set, otherwise the global default. + * DEFAULT means no override is sent and the server decides. */ - getReasoningEffort(): ReasoningEffort | ReasoningEffort.OFF { + getReasoningEffort(): ReasoningEffort { if (this.activeConversation) { - return this.activeConversation.reasoningEffort ?? this.pendingReasoningEffort; + if (this.activeConversation.reasoningEffort !== undefined) { + return this.activeConversation.reasoningEffort; + } + // conversations created before the tri-state store an explicit + // opt-out only as thinkingEnabled = false + if (this.activeConversation.thinkingEnabled === false) { + return ReasoningEffort.OFF; + } } return this.pendingReasoningEffort; } @@ -857,7 +800,7 @@ class ConversationsStore { /** * Sets the reasoning effort for the active conversation. * If no conversation exists, stores the global default. - * @param effort - The effort level ('low' | 'medium' | 'high' | 'max') + * @param effort - The effort level ('default' | 'off' | 'low' | 'medium' | 'high' | 'max') */ async setReasoningEffort(effort: ReasoningEffort): Promise { if (!this.activeConversation) { diff --git a/tools/ui/src/lib/types/reasoning.ts b/tools/ui/src/lib/types/reasoning.ts index b72a3b77f1..417c79609f 100644 --- a/tools/ui/src/lib/types/reasoning.ts +++ b/tools/ui/src/lib/types/reasoning.ts @@ -1,6 +1,5 @@ export interface ReasoningEffortLevel { value: string; label: string; - isOff?: boolean; hasInfo?: boolean; }