fix(google): Gemini 3.x uses native thought parts, not <think> tags - #8
Merged
Merged
Conversation
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>
There was a problem hiding this comment.
✅ 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
hasNativeGeminiThoughtPartspredicate insrc/plugins/provider-replay-helpers.tsto identify Gemini 3.x models based on model identifiers (gemini-3,gemini-3.1,gemini-3.6withproorflashsuffixes). - It wires this check into both
buildProviderReplayFamilyHooks(plugin path) andresolveReasoningOutputMode(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.tsandsrc/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, andsrc/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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Release-train port of #7 (which landed on
main, the wrong base —mainis upstream at2026.4.6, this branch is the2026.5.22train 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):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
resolveReasoningOutputModeforcestaggedfor 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 literalFormat: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:
resolveGoogleThinkingConfigrequestsincludeThoughts: truefor Gemini 3.x, and the stream loop routespart.thought === trueinto thinking blocks. The tagged contract is redundant for these models — and actively harmful.Change
Resolve
nativefor Gemini 3.x on both paths that forcetagged:buildProviderReplayFamilyHooks({ family: "google-gemini" })— the path real deployments take (providergoogle, apigoogle-generative-ai), also inherited bygoogle-gemini-cliBUILTIN_REASONING_OUTPUT_MODES— the non-plugin fallbackBoth go through one shared predicate,
hasNativeGeminiThoughtParts, so the two cannot drift. Gemini 2.5 and older keeptagged; with nomodelIdin context the built-in map also keepstagged, soisReasoningTagProvider("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-previewtonativeandgemini-2.5-flash/gemini-2.5-flash-litetotagged. The two existing assertions that expectedtaggedforgemini-3.1-pro-preview(provider family table and the google extension) are flipped tonative— that expectation is exactly what this change inverts — and agemini-2.5-flashcase is added beside each so the tagged path keeps its coverage.Next
Cut
v2026.5.22-dojo.6from this branch, repin the Dockerfile indojo-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