Skip to content

Show thinking output for Google Gemini API (thinking-capable models) #774

Description

@micz

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

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions