Skip to content

fix(google): Gemini 3.x uses native thought parts, not <think> tags - #8

Merged
StevenMend merged 1 commit into
dojo/v2026.5.22-fixesfrom
steven/gemini3-native-train
Jul 28, 2026
Merged

fix(google): Gemini 3.x uses native thought parts, not <think> tags#8
StevenMend merged 1 commit into
dojo/v2026.5.22-fixesfrom
steven/gemini3-native-train

Conversation

@StevenMend

Copy link
Copy Markdown
Member

Release-train port of #7 (which landed on main, the wrong base — main is upstream at 2026.4.6, this branch is the 2026.5.22 train the Dockerfile pins).

Problem

Doji leaked its raw chain-of-thought to users in production. Captured live on www.trydojo.io, reproducible 3/3 (fresh conversation, ES and EN prompts):

content_len:   1661   <- the whole chain-of-thought arrives here
reasoning_len: 0      <- the reasoning channel is never used

The leaked text recites the injected system rules and real internal tool names (dojo_sql_query, dojo_course_enroll, dojo_platform_info, exec) straight into the answer bubble.

Cause

resolveReasoningOutputMode forces tagged for Google on two independent paths, so Gemini is told "ALL internal reasoning MUST be inside <think>...</think>". Gemini 3.x does not follow that instruction — it narrates it. One capture ends with a literal Format: line followed by an orphan backtick, because the model wrote the tag instruction as prose and the downstream SSE filter then latched onto those literal tags and ate the start of the real answer.

The transport already does the right thing natively: resolveGoogleThinkingConfig requests includeThoughts: true for Gemini 3.x, and the stream loop routes part.thought === true into thinking blocks. The tagged contract is redundant for these models — and actively harmful.

Change

Resolve native for Gemini 3.x on both paths that force tagged:

  • buildProviderReplayFamilyHooks({ family: "google-gemini" }) — the path real deployments take (provider google, api google-generative-ai), also inherited by google-gemini-cli
  • BUILTIN_REASONING_OUTPUT_MODES — the non-plugin fallback

Both go through one shared predicate, hasNativeGeminiThoughtParts, so the two cannot drift. Gemini 2.5 and older keep tagged; with no modelId in context the built-in map also keeps tagged, so isReasoningTagProvider("google-generative-ai") is unchanged.

Tests

56 tests pass locally across the four affected suites. New cases pin gemini-3.6-flash / gemini-3-flash / gemini-3.1-pro-preview to native and gemini-2.5-flash / gemini-2.5-flash-lite to tagged. The two existing assertions that expected tagged for gemini-3.1-pro-preview (provider family table and the google extension) are flipped to native — that expectation is exactly what this change inverts — and a gemini-2.5-flash case is added beside each so the tagged path keeps its coverage.

Next

Cut v2026.5.22-dojo.6 from this branch, repin the Dockerfile in dojo-agent-openclaw-plugin, verify on internal staging with the same live SSE capture, then promote to production.

Created by Claude Code on behalf of @StevenMendez

Gemini 3.x streams reasoning as native `thought` parts and the Google
transport already routes them into thinking blocks (`part.thought === true`,
with `includeThoughts: true` requested in `thinkingConfig`). Forcing the
provider into `tagged` mode on top of that asked the model to wrap its
reasoning in `<think>...</think>`; instead of following the instruction it
narrated it, so the chain-of-thought — including the injected system rules
and internal tool names — reached users verbatim in the text stream. The
reasoning channel stayed empty on every affected turn.

Resolve `native` for Gemini 3.x on both paths that force `tagged` for Google
(the `google-gemini` provider family hook and the built-in provider map),
behind one shared predicate so the two cannot drift. Older Gemini models,
which do not emit native thought parts, keep `tagged`. With no modelId in
context the built-in map also keeps `tagged`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@dojo-code-reviewer dojo-code-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Approved

Approved — no findings. Confidence: 5.00/5.00.

Walkthrough

Walkthrough of Changes

This pull request correctly resolves reasoning output mode behavior for Gemini 3.x models to prevent raw chain-of-thought outputs from leaking to the end-user. Specifically:

  • It introduces the hasNativeGeminiThoughtParts predicate in src/plugins/provider-replay-helpers.ts to identify Gemini 3.x models based on model identifiers (gemini-3, gemini-3.1, gemini-3.6 with pro or flash suffixes).
  • It wires this check into both buildProviderReplayFamilyHooks (plugin path) and resolveReasoningOutputMode (built-in fallback path) so they correctly return "native" instead of "tagged" for Gemini 3.x models.
  • It preserves "tagged" mode for Gemini 2.5 and older models, and retains the default fallback behavior when no model ID is present.

Reviewed Files and Areas

I have thoroughly reviewed:

  • src/plugins/provider-replay-helpers.ts: Checked the regex matches and string normalization logic.
  • src/utils/provider-utils.ts and src/plugin-sdk/provider-model-shared.ts: Inspected integration, type compatibilities, and resolving paths.
  • extensions/google/index.test.ts, src/plugin-sdk/provider-model-shared.test.ts, and src/utils/provider-utils.test.ts: Verified correctness of test assertions, coverage matrix, and handling of undefined/null model cases.

Safety Rationale

This change is safe to merge as it is fully back-compatible, preserves identical behavior for Gemini 2.5 and older models, matches exact types across plugin SDK boundaries, and is fully covered by robust unit tests.

Approved — no findings.

@StevenMend
StevenMend merged commit 6eefa01 into dojo/v2026.5.22-fixes Jul 28, 2026
86 of 92 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant