From 580604e13b98eebe5e382167f3e1fc4bc41b7e8c Mon Sep 17 00:00:00 2001 From: Michael Neale <14976+michaelneale@users.noreply.github.com> Date: Tue, 15 Sep 2026 11:35:00 +1000 Subject: [PATCH 1/2] fix(console): answer chat questions instead of thinking about them Ask a reasoning model a casual question in the console today and it spends tens of seconds deliberating before it answers. On a 9B at Q4 it often never answers at all: the reasoning consumes the whole token budget, `finish_reason` comes back `length` and `content` is null, so the chat window shows a growing thinking trace and then nothing. Worse, the deliberation degenerates -- one measured run repeated "Wait, could it be ..." 39 times before terminating. Measured on Qwen3.5-9B-GGUF:Q4_K_M, prompt "what is updog", max_tokens 4000: thinking off 350 tokens, 4.6 s, answered default 4000 tokens, 38 s+, 13.5 KB of reasoning, no answer The console now asks for a direct answer, as it did before the UI rewrite: `00296cc3a` added `chat_template_kwargs: {enable_thinking: false}` to console chat requests in March for exactly this symptom, and #545 dropped it when the request builder was rewritten. API callers are unaffected and keep full control: `reasoning_effort`, `reasoning.enabled` and `chat_template_kwargs` all work, verified against a live node. Only the console's own requests change. Co-authored-by: Michael Neale <14976+michaelneale@users.noreply.github.com> Signed-off-by: Michael Neale <14976+michaelneale@users.noreply.github.com> --- .../src/features/chat/api/build-input.test.ts | 11 +++++++++++ .../mesh-llm-ui/src/features/chat/api/build-input.ts | 8 +++++++- crates/mesh-llm-ui/src/lib/api/types.ts | 1 + 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/crates/mesh-llm-ui/src/features/chat/api/build-input.test.ts b/crates/mesh-llm-ui/src/features/chat/api/build-input.test.ts index 12245f7e86..a5c79c58d5 100644 --- a/crates/mesh-llm-ui/src/features/chat/api/build-input.test.ts +++ b/crates/mesh-llm-ui/src/features/chat/api/build-input.test.ts @@ -88,6 +88,17 @@ describe('buildResponsesInput', () => { expect(request.input).toEqual([{ role: 'user', content: 'Hello mesh' }]) }) + it('asks for a direct answer instead of hidden reasoning', async () => { + const request = await buildResponsesInput( + [createMessage([{ type: 'text', content: 'what is updog' }])], + 'model-a', + 'client-123', + 'request-456' + ) + + expect(request.chat_template_kwargs).toEqual({ enable_thinking: false }) + }) + it('prepends the saved system prompt as a responses system message', async () => { const request = await buildResponsesInput( [createMessage([{ type: 'text', content: 'Explain the cluster status' }])], diff --git a/crates/mesh-llm-ui/src/features/chat/api/build-input.ts b/crates/mesh-llm-ui/src/features/chat/api/build-input.ts index 066d06fc6d..78aa08ca86 100644 --- a/crates/mesh-llm-ui/src/features/chat/api/build-input.ts +++ b/crates/mesh-llm-ui/src/features/chat/api/build-input.ts @@ -186,6 +186,12 @@ export async function buildResponsesInput( request_id: requestId, input: messagesWithSystemPrompt, stream: true, - stream_options: { include_usage: true } + stream_options: { include_usage: true }, + // Reasoning models think by default, and for chat that means tens of + // seconds of hidden deliberation over a casual question -- with weaker + // ones it degenerates into a repetition loop instead of answering. The + // console asks for a direct answer. API callers are unaffected: they + // choose their own reasoning controls. + chat_template_kwargs: { enable_thinking: false } } } diff --git a/crates/mesh-llm-ui/src/lib/api/types.ts b/crates/mesh-llm-ui/src/lib/api/types.ts index 18f51e6193..1ae86f0ed0 100644 --- a/crates/mesh-llm-ui/src/lib/api/types.ts +++ b/crates/mesh-llm-ui/src/lib/api/types.ts @@ -301,6 +301,7 @@ export interface ResponsesRequest { input: ResponsesInputMessage[] stream: boolean stream_options?: { include_usage: boolean } + chat_template_kwargs?: { enable_thinking?: boolean } } export interface ChatSSEDeltaEvent { From 77c93c429d22d54155c1826643ce4133c4c685d9 Mon Sep 17 00:00:00 2001 From: Michael Neale <14976+michaelneale@users.noreply.github.com> Date: Tue, 15 Sep 2026 11:58:41 +1000 Subject: [PATCH 2/2] fix(console): ask chat models for minimal reasoning effort A reasoning model asked a casual question in the console deliberates for tens of seconds before answering, because nothing in the request asks it not to. On a 9B at Q4 the deliberation sometimes never converges: it consumes the whole token budget, `finish_reason` is `length` and `content` is null, so the window shows a growing thinking trace and then nothing. The console now sends `reasoning_effort: "minimal"`. Thinking stays on and the trace still renders -- the console has the UI for it and it is worth showing -- but the model is asked for the least of it. This is a mitigation, not a cure, and the honest numbers say so. Measured on Qwen3.5-9B-GGUF:Q4_K_M, "what is updog", max_tokens 4000, three runs: minimal 2158 / 4000 / 3888 tokens -- answered, exhausted, answered Effort is a hint to the chat template, not an enforced budget, so the failure is less likely rather than impossible. The same prompt with reasoning off answers every time in ~350 tokens; that was rejected because it throws away a feature the console exists to display. API callers are unaffected and keep full control: `reasoning_effort`, `reasoning.enabled` and `chat_template_kwargs` all work. Only the console's own requests change, and `chat_template_kwargs` is deliberately not sent with the effort hint -- an explicit template setting wins over an effort alias (`normalize_reasoning_template_options`), so sending both would silently override the hint this change exists to send. Co-authored-by: Michael Neale <14976+michaelneale@users.noreply.github.com> Signed-off-by: Michael Neale <14976+michaelneale@users.noreply.github.com> --- .../src/features/chat/api/build-input.test.ts | 5 +++-- .../mesh-llm-ui/src/features/chat/api/build-input.ts | 11 +++++------ crates/mesh-llm-ui/src/lib/api/types.ts | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/crates/mesh-llm-ui/src/features/chat/api/build-input.test.ts b/crates/mesh-llm-ui/src/features/chat/api/build-input.test.ts index a5c79c58d5..852e7ac7e2 100644 --- a/crates/mesh-llm-ui/src/features/chat/api/build-input.test.ts +++ b/crates/mesh-llm-ui/src/features/chat/api/build-input.test.ts @@ -88,7 +88,7 @@ describe('buildResponsesInput', () => { expect(request.input).toEqual([{ role: 'user', content: 'Hello mesh' }]) }) - it('asks for a direct answer instead of hidden reasoning', async () => { + it('asks for minimal reasoning effort', async () => { const request = await buildResponsesInput( [createMessage([{ type: 'text', content: 'what is updog' }])], 'model-a', @@ -96,7 +96,8 @@ describe('buildResponsesInput', () => { 'request-456' ) - expect(request.chat_template_kwargs).toEqual({ enable_thinking: false }) + expect(request.reasoning_effort).toBe('minimal') + expect(request).not.toHaveProperty('chat_template_kwargs') }) it('prepends the saved system prompt as a responses system message', async () => { diff --git a/crates/mesh-llm-ui/src/features/chat/api/build-input.ts b/crates/mesh-llm-ui/src/features/chat/api/build-input.ts index 78aa08ca86..32e084445e 100644 --- a/crates/mesh-llm-ui/src/features/chat/api/build-input.ts +++ b/crates/mesh-llm-ui/src/features/chat/api/build-input.ts @@ -187,11 +187,10 @@ export async function buildResponsesInput( input: messagesWithSystemPrompt, stream: true, stream_options: { include_usage: true }, - // Reasoning models think by default, and for chat that means tens of - // seconds of hidden deliberation over a casual question -- with weaker - // ones it degenerates into a repetition loop instead of answering. The - // console asks for a direct answer. API callers are unaffected: they - // choose their own reasoning controls. - chat_template_kwargs: { enable_thinking: false } + // Reasoning models deliberate for tens of seconds over a casual question + // when nothing asks them not to. The console keeps thinking -- it renders + // the trace -- but asks for the least of it. API callers are unaffected: + // they choose their own reasoning controls. + reasoning_effort: 'minimal' } } diff --git a/crates/mesh-llm-ui/src/lib/api/types.ts b/crates/mesh-llm-ui/src/lib/api/types.ts index 1ae86f0ed0..a16a19b6af 100644 --- a/crates/mesh-llm-ui/src/lib/api/types.ts +++ b/crates/mesh-llm-ui/src/lib/api/types.ts @@ -301,7 +301,7 @@ export interface ResponsesRequest { input: ResponsesInputMessage[] stream: boolean stream_options?: { include_usage: boolean } - chat_template_kwargs?: { enable_thinking?: boolean } + reasoning_effort?: 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max' } export interface ChatSSEDeltaEvent {