Show thinking output for Google Gemini API (thinking-capable models)
Context
When using the Google Gemini API with thinking_budget set, thinking-capable models
(gemini-2.5-pro, gemini-2.5-flash, etc.) include thinking content in their streaming
response as separate parts entries marked with thought: true.
ThunderAI currently only reads parts[0].text and discards everything else, so the
thinking content is silently dropped.
This issue tracks adding support for capturing and displaying it in the chat window,
collapsed by default (similar to how <think> blocks from Ollama/OpenAI Compatible
models are handled — see #636).
How the thinking arrives in the stream
The Gemini streaming response wraps content in candidates[0].content.parts[]. When
thinking is active, parts can have two shapes:
Thinking part (one or more, arriving first):
{
"candidates": [{
"content": {
"parts": [{ "text": "...thinking chunk...", "thought": true }],
"role": "model"
}
}]
}
Normal text part (arrives after thinking is complete):
{
"candidates": [{
"content": {
"parts": [{ "text": "...answer chunk..." }],
"role": "model"
}
}]
}
The thought field is only present (and true) on thinking parts. Normal text parts
do not have the field at all.
Current behaviour
In js/workers/model-worker-google_gemini.js, the worker only reads parts[0].text
without checking for the thought field or iterating over all parts:
const { text } = parts[0];
if (text) {
assistantResponseAccumulator += text;
postMessage({ type: 'newToken', payload: { token: text } });
}
This means:
- If
parts[0] is a thinking part, its text is mistakenly included in the visible response.
- If a chunk has multiple parts (thinking + text), only the first is read.
Required changes
1. js/workers/model-worker-google_gemini.js
Replace the current single-part read with a loop over all parts, splitting thinking from
normal text:
let thinkingAccumulator = '';
// replace current parts[0] handling:
for (const part of parts) {
if (!part.text) continue;
if (part.thought === true) {
thinkingAccumulator += part.text;
postMessage({ type: 'newThinkingToken', payload: { token: part.text } });
} else {
assistantResponseAccumulator += part.text;
postMessage({ type: 'newToken', payload: { token: part.text } });
}
}
On tokensDone, pass the accumulated thinking to the UI:
postMessage({ type: 'tokensDone', payload: { thinking: thinkingAccumulator } });
thinkingAccumulator = '';
2. api_webchat/messagesArea.js
Handle newThinkingToken alongside newToken, and on tokensDone prepend the thinking
block (as a collapsed <details> element) before rendering the markdown of the main response.
3. Settings/preferences
A global toggle (already planned in #636) to completely hide thinking blocks should cover
this case as well — no separate preference needed.
Important constraints / risks
- The
thought field is not part of the stable documented Gemini API contract as of
April 2026. It is present in practice for thinking-capable models but Google may change
the field name or structure without notice. This implementation should be treated as
best-effort.
- Thinking is only active when
thinking_budget is set to a value > 0 or -1 (dynamic).
With thinking_budget: 0 the API disables thinking entirely and no thought parts appear.
- Not all Gemini models support thinking: currently gemini-2.5-pro and gemini-2.5-flash.
For other models this change is a no-op (no thought parts will ever appear).
- Relevant Google documentation:
Gemini thinking models
Show thinking output for Google Gemini API (thinking-capable models)
Context
When using the Google Gemini API with
thinking_budgetset, thinking-capable models(gemini-2.5-pro, gemini-2.5-flash, etc.) include thinking content in their streaming
response as separate
partsentries marked withthought: true.ThunderAI currently only reads
parts[0].textand discards everything else, so thethinking content is silently dropped.
This issue tracks adding support for capturing and displaying it in the chat window,
collapsed by default (similar to how
<think>blocks from Ollama/OpenAI Compatiblemodels are handled — see #636).
How the thinking arrives in the stream
The Gemini streaming response wraps content in
candidates[0].content.parts[]. Whenthinking is active, parts can have two shapes:
Thinking part (one or more, arriving first):
{ "candidates": [{ "content": { "parts": [{ "text": "...thinking chunk...", "thought": true }], "role": "model" } }] }Normal text part (arrives after thinking is complete):
{ "candidates": [{ "content": { "parts": [{ "text": "...answer chunk..." }], "role": "model" } }] }The
thoughtfield is only present (andtrue) on thinking parts. Normal text partsdo not have the field at all.
Current behaviour
In
js/workers/model-worker-google_gemini.js, the worker only readsparts[0].textwithout checking for the
thoughtfield or iterating over all parts:This means:
parts[0]is a thinking part, its text is mistakenly included in the visible response.Required changes
1.
js/workers/model-worker-google_gemini.jsReplace the current single-part read with a loop over all parts, splitting thinking from
normal text:
On
tokensDone, pass the accumulated thinking to the UI:2.
api_webchat/messagesArea.jsHandle
newThinkingTokenalongsidenewToken, and ontokensDoneprepend the thinkingblock (as a collapsed
<details>element) before rendering the markdown of the main response.3. Settings/preferences
A global toggle (already planned in #636) to completely hide thinking blocks should cover
this case as well — no separate preference needed.
Important constraints / risks
thoughtfield is not part of the stable documented Gemini API contract as ofApril 2026. It is present in practice for thinking-capable models but Google may change
the field name or structure without notice. This implementation should be treated as
best-effort.
thinking_budgetis set to a value> 0or-1(dynamic).With
thinking_budget: 0the API disables thinking entirely and nothoughtparts appear.For other models this change is a no-op (no
thoughtparts will ever appear).Gemini thinking models