Detect always-on reasoning models and show Think button as locked-on (#4654)
* Detect always-on reasoning models and show Think button as locked-on Models with hardcoded <think>/<think> tags or reasoning_content in their chat template (e.g. distilled reasoning models) always produce thinking output regardless of any toggle. Previously these models were not detected as reasoning-capable at all, so the Think button was grayed out even though the model was actively reasoning. Backend: - Detect <think>/<think> and reasoning_content in GGUF chat templates as a fallback when enable_thinking is not present - Add reasoning_always_on flag to LoadResponse and InferenceStatusResponse - Pass the flag through all GGUF load and status response paths Frontend: - Add reasoningAlwaysOn to the chat runtime store and API types - When reasoning_always_on is true, show the Think button as lit (active) but not clickable, with a tooltip explaining the model always uses thinking - Force reasoningEnabled=true when the model always reasons * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Use pointer-events-none instead of disabled for always-on Think button The HTML disabled attribute was not fully blocking clicks on the Think button for always-on reasoning models. Switch to pointer-events-none CSS class which prevents all mouse interaction at the CSS level. * Use a static span instead of disabled button for always-on Think Replace the button element with a plain span when reasoning is always on. This makes it physically impossible to toggle since there is no clickable element at all, avoiding any CSS or disabled-attribute edge cases. * Simplify always-on Think button to stay lit and remain toggleable Keep the Think button as a normal toggleable button but ensure it shows as lit when reasoning_always_on is true. The model always reasons regardless of the toggle state so there is no need to block interaction. --------- Co-authored-by: Daniel Han <danielhanchen@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
eacaf6827c
commit
e36f72c685
8 changed files with 46 additions and 3 deletions
|
|
@ -50,6 +50,7 @@ class LlamaCppBackend:
|
|||
self._effective_context_length: Optional[int] = None
|
||||
self._chat_template: Optional[str] = None
|
||||
self._supports_reasoning: bool = False
|
||||
self._reasoning_always_on: bool = False
|
||||
self._supports_tools: bool = False
|
||||
self._cache_type_kv: Optional[str] = None
|
||||
self._reasoning_default: bool = True
|
||||
|
|
@ -107,6 +108,10 @@ class LlamaCppBackend:
|
|||
def supports_reasoning(self) -> bool:
|
||||
return self._supports_reasoning
|
||||
|
||||
@property
|
||||
def reasoning_always_on(self) -> bool:
|
||||
return self._reasoning_always_on
|
||||
|
||||
@property
|
||||
def reasoning_default(self) -> bool:
|
||||
return self._reasoning_default
|
||||
|
|
@ -550,6 +555,7 @@ class LlamaCppBackend:
|
|||
self._context_length = None
|
||||
self._chat_template = None
|
||||
self._supports_reasoning = False
|
||||
self._reasoning_always_on = False
|
||||
self._supports_tools = False
|
||||
self._n_layers = None
|
||||
self._n_kv_heads = None
|
||||
|
|
@ -627,6 +633,20 @@ class LlamaCppBackend:
|
|||
logger.info(
|
||||
"GGUF metadata: model supports reasoning (DeepSeek thinking)"
|
||||
)
|
||||
# Models with hardcoded <think> tags or reasoning_content
|
||||
# in their chat template always produce thinking output
|
||||
# (no toggle to disable it).
|
||||
if not self._supports_reasoning:
|
||||
if (
|
||||
"<think>" in tpl
|
||||
and "</think>" in tpl
|
||||
or "reasoning_content" in tpl
|
||||
):
|
||||
self._supports_reasoning = True
|
||||
self._reasoning_always_on = True
|
||||
logger.info(
|
||||
"GGUF metadata: model always reasons (<think> tags in template)"
|
||||
)
|
||||
# Detect tool calling support from chat template
|
||||
tool_markers = [
|
||||
"{%- if tools %}",
|
||||
|
|
@ -1318,6 +1338,7 @@ class LlamaCppBackend:
|
|||
self._effective_context_length = None
|
||||
self._chat_template = None
|
||||
self._supports_reasoning = False
|
||||
self._reasoning_always_on = False
|
||||
self._supports_tools = False
|
||||
self._cache_type_kv = None
|
||||
self._n_layers = None
|
||||
|
|
|
|||
|
|
@ -136,6 +136,10 @@ class LoadResponse(BaseModel):
|
|||
False,
|
||||
description = "Whether model supports thinking/reasoning mode (enable_thinking)",
|
||||
)
|
||||
reasoning_always_on: bool = Field(
|
||||
False,
|
||||
description = "Whether reasoning is always on (hardcoded <think> tags, not toggleable)",
|
||||
)
|
||||
supports_tools: bool = Field(
|
||||
False,
|
||||
description = "Whether model supports tool calling (web search, etc.)",
|
||||
|
|
@ -193,6 +197,9 @@ class InferenceStatusResponse(BaseModel):
|
|||
supports_reasoning: bool = Field(
|
||||
False, description = "Whether the active model supports reasoning/thinking mode"
|
||||
)
|
||||
reasoning_always_on: bool = Field(
|
||||
False, description = "Whether reasoning is always on (not toggleable)"
|
||||
)
|
||||
supports_tools: bool = Field(
|
||||
False, description = "Whether the active model supports tool calling"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -156,6 +156,7 @@ async def load_model(
|
|||
inference = inference_config,
|
||||
context_length = llama_backend.context_length,
|
||||
supports_reasoning = llama_backend.supports_reasoning,
|
||||
reasoning_always_on = llama_backend.reasoning_always_on,
|
||||
chat_template = llama_backend.chat_template,
|
||||
)
|
||||
else:
|
||||
|
|
@ -280,6 +281,7 @@ async def load_model(
|
|||
inference = inference_config,
|
||||
context_length = llama_backend.context_length,
|
||||
supports_reasoning = llama_backend.supports_reasoning,
|
||||
reasoning_always_on = llama_backend.reasoning_always_on,
|
||||
supports_tools = llama_backend.supports_tools,
|
||||
cache_type_kv = llama_backend.cache_type_kv,
|
||||
chat_template = llama_backend.chat_template,
|
||||
|
|
@ -609,6 +611,7 @@ async def get_status(
|
|||
loaded = [_model_id],
|
||||
inference = _inference_cfg,
|
||||
supports_reasoning = llama_backend.supports_reasoning,
|
||||
reasoning_always_on = llama_backend.reasoning_always_on,
|
||||
supports_tools = llama_backend.supports_tools,
|
||||
context_length = llama_backend.context_length,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -307,6 +307,7 @@ async function autoLoadSmallestModel(): Promise<boolean> {
|
|||
useChatRuntimeStore.setState({
|
||||
ggufContextLength: loadResp.context_length ?? 131072,
|
||||
supportsReasoning: loadResp.supports_reasoning ?? false,
|
||||
reasoningAlwaysOn: loadResp.reasoning_always_on ?? false,
|
||||
reasoningEnabled: loadResp.supports_reasoning ?? false,
|
||||
supportsTools: loadResp.supports_tools ?? false,
|
||||
toolsEnabled: loadResp.supports_tools ?? false,
|
||||
|
|
@ -392,6 +393,7 @@ async function autoLoadSmallestModel(): Promise<boolean> {
|
|||
useChatRuntimeStore.setState({
|
||||
ggufContextLength: loadResp.context_length ?? 131072,
|
||||
supportsReasoning: loadResp.supports_reasoning ?? false,
|
||||
reasoningAlwaysOn: loadResp.reasoning_always_on ?? false,
|
||||
reasoningEnabled: loadResp.supports_reasoning ?? false,
|
||||
supportsTools: loadResp.supports_tools ?? false,
|
||||
toolsEnabled: loadResp.supports_tools ?? false,
|
||||
|
|
|
|||
|
|
@ -238,9 +238,11 @@ export function useChatModelRuntime() {
|
|||
|
||||
// Restore reasoning/tools support flags and context length
|
||||
const supportsReasoning = statusRes.supports_reasoning ?? false;
|
||||
const reasoningAlwaysOn = statusRes.reasoning_always_on ?? false;
|
||||
const supportsTools = statusRes.supports_tools ?? false;
|
||||
useChatRuntimeStore.setState({
|
||||
supportsReasoning,
|
||||
reasoningAlwaysOn,
|
||||
supportsTools,
|
||||
ggufContextLength: statusRes.is_gguf ? (statusRes.context_length ?? null) : null,
|
||||
});
|
||||
|
|
@ -420,10 +422,12 @@ export function useChatModelRuntime() {
|
|||
&& customContextLength !== nativeCtx
|
||||
? customContextLength
|
||||
: null;
|
||||
const reasoningAlwaysOn = loadResponse.reasoning_always_on ?? false;
|
||||
useChatRuntimeStore.setState({
|
||||
ggufContextLength: nativeCtx,
|
||||
supportsReasoning: loadResponse.supports_reasoning ?? false,
|
||||
reasoningEnabled: reasoningDefault,
|
||||
reasoningAlwaysOn,
|
||||
reasoningEnabled: reasoningAlwaysOn ? true : reasoningDefault,
|
||||
supportsTools: loadResponse.supports_tools ?? false,
|
||||
toolsEnabled: loadResponse.supports_tools ?? false,
|
||||
codeToolsEnabled: loadResponse.supports_tools ?? false,
|
||||
|
|
|
|||
|
|
@ -241,6 +241,7 @@ export function SharedComposer({
|
|||
(s) => !!s.params.checkpoint && !s.modelLoading,
|
||||
);
|
||||
const supportsReasoning = useChatRuntimeStore((s) => s.supportsReasoning);
|
||||
const reasoningAlwaysOn = useChatRuntimeStore((s) => s.reasoningAlwaysOn);
|
||||
const reasoningEnabled = useChatRuntimeStore((s) => s.reasoningEnabled);
|
||||
const setReasoningEnabled = useChatRuntimeStore((s) => s.setReasoningEnabled);
|
||||
const supportsTools = useChatRuntimeStore((s) => s.supportsTools);
|
||||
|
|
@ -528,6 +529,7 @@ export function SharedComposer({
|
|||
type="button"
|
||||
disabled={reasoningDisabled}
|
||||
onClick={() => {
|
||||
if (reasoningAlwaysOn) return;
|
||||
const next = !reasoningEnabled;
|
||||
setReasoningEnabled(next);
|
||||
// Qwen3/3.5: adjust params for thinking on/off
|
||||
|
|
@ -544,13 +546,13 @@ export function SharedComposer({
|
|||
"flex items-center gap-0.5 rounded-full px-2 py-0.5 text-xs font-medium transition-colors",
|
||||
reasoningDisabled
|
||||
? "cursor-not-allowed opacity-40"
|
||||
: reasoningEnabled
|
||||
: (reasoningEnabled || reasoningAlwaysOn)
|
||||
? "bg-primary/10 text-primary hover:bg-primary/20"
|
||||
: "bg-muted text-muted-foreground hover:bg-muted-foreground/15",
|
||||
)}
|
||||
aria-label={reasoningEnabled ? "Disable thinking" : "Enable thinking"}
|
||||
>
|
||||
{reasoningEnabled && !reasoningDisabled ? (
|
||||
{(reasoningEnabled || reasoningAlwaysOn) && !reasoningDisabled ? (
|
||||
<LightbulbIcon className="size-3" />
|
||||
) : (
|
||||
<LightbulbOffIcon className="size-3" />
|
||||
|
|
|
|||
|
|
@ -151,6 +151,7 @@ type ChatRuntimeStore = {
|
|||
activeGgufVariant: string | null;
|
||||
ggufContextLength: number | null;
|
||||
supportsReasoning: boolean;
|
||||
reasoningAlwaysOn: boolean;
|
||||
reasoningEnabled: boolean;
|
||||
supportsTools: boolean;
|
||||
toolsEnabled: boolean;
|
||||
|
|
@ -213,6 +214,7 @@ export const useChatRuntimeStore = create<ChatRuntimeStore>((set) => ({
|
|||
activeGgufVariant: null,
|
||||
ggufContextLength: null,
|
||||
supportsReasoning: false,
|
||||
reasoningAlwaysOn: false,
|
||||
reasoningEnabled: true,
|
||||
supportsTools: false,
|
||||
toolsEnabled: false,
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@ export interface LoadModelResponse {
|
|||
};
|
||||
context_length?: number | null;
|
||||
supports_reasoning?: boolean;
|
||||
reasoning_always_on?: boolean;
|
||||
supports_tools?: boolean;
|
||||
cache_type_kv?: string | null;
|
||||
chat_template?: string | null;
|
||||
|
|
@ -115,6 +116,7 @@ export interface InferenceStatusResponse {
|
|||
trust_remote_code?: boolean;
|
||||
};
|
||||
supports_reasoning?: boolean;
|
||||
reasoning_always_on?: boolean;
|
||||
supports_tools?: boolean;
|
||||
context_length?: number | null;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue