fix(providers): use native reasoning mode for Gemini instead of tagged#89379
Conversation
|
Codex review: needs maintainer review before merge. Reviewed June 2, 2026, 5:55 AM ET / 09:55 UTC. Summary PR surface: Source -10, Tests +16, Docs +7. Total +13 across 5 files. Reproducibility: yes. from source and the linked live proof: current main can combine direct Gemini Review metrics: 1 noteworthy metric.
Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Risk before merge
Maintainer options:
Next step before merge
Security Review detailsBest possible solution: Land the direct-Google native override with the CLI tagged invariant and docs/tests intact, after maintainers accept the provider-output compatibility tradeoff. Do we have a high-confidence way to reproduce the issue? Yes, from source and the linked live proof: current main can combine direct Gemini Is this the best way to solve the issue? Yes; the provider-local override is the narrowest maintainable fix because direct Google consumes native thought parts while Gemini CLI still needs the shared tagged text-output contract. Changing the shared AGENTS.md: found and applied where relevant. Codex review notes: model gpt-5.5, reasoning high; reviewed against e24582d53cbf. Label changesLabel changes:
Label justifications:
Evidence reviewedPR surface: Source -10, Tests +16, Docs +7. Total +13 across 5 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
… CLI tagged Gemini 2.5+ delivers reasoning via native thinkingParts (thinkingConfig. includeThoughts). Having tagged mode active at the same time injects a <think>…</think>/<final>…</final> directive into the system prompt; the model opens a <think> block before a tool call, never closes it, and returns an empty post-tool turn (content:[], payloads=0 error, openclaw#69220). Fix: override resolveReasoningOutputMode in buildGoogleProvider() only — not in the shared GOOGLE_GEMINI_PROVIDER_HOOKS. The Gemini CLI backend (google-gemini-cli) runs gemini --output-format json and parses a text response field, not native thought parts; it must stay on tagged mode. A regression test confirms google-gemini-cli remains "tagged". Also remove the dead BUILTIN_REASONING_OUTPUT_MODES entry keyed on "google-generative-ai" from provider-utils.ts — that string is only ever the transport model.api value, never the provider id passed to resolveReasoningOutputMode, so the map was unreachable. Fixes openclaw#69220
58b1e75 to
76b1356
Compare
|
Addressed ClawSweeper's P1 finding: the
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. Re-review progress:
|
|
Updated the PR with the direct Google/Gemini docs alignment and a redacted live Gemini 2.5 Flash @clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
|
The Real behavior proof section is now a proper @clawsweeper re-review |
|
🦞👀 Command router queued. I will update this comment with the next step. Re-review progress:
|
|
Maintainer verification for exact head
Known proof gap: the live Gemini run was from the contributor proof before the maintainer alias/type fixups; those fixups were covered with source assertions, focused tests, type lanes, autoreview, and exact-head CI. |
openclaw#89379) * fix(providers): use native reasoning mode for direct Gemini API, keep CLI tagged Gemini 2.5+ delivers reasoning via native thinkingParts (thinkingConfig. includeThoughts). Having tagged mode active at the same time injects a <think>…</think>/<final>…</final> directive into the system prompt; the model opens a <think> block before a tool call, never closes it, and returns an empty post-tool turn (content:[], payloads=0 error, openclaw#69220). Fix: override resolveReasoningOutputMode in buildGoogleProvider() only — not in the shared GOOGLE_GEMINI_PROVIDER_HOOKS. The Gemini CLI backend (google-gemini-cli) runs gemini --output-format json and parses a text response field, not native thought parts; it must stay on tagged mode. A regression test confirms google-gemini-cli remains "tagged". Also remove the dead BUILTIN_REASONING_OUTPUT_MODES entry keyed on "google-generative-ai" from provider-utils.ts — that string is only ever the transport model.api value, never the provider id passed to resolveReasoningOutputMode, so the map was unreachable. Fixes openclaw#69220 * docs: clarify Gemini reasoning output modes * fix(google): keep Antigravity reasoning tagged * fix(google): default direct reasoning checks to native * fix(google): import reasoning context from plugin entry --------- Co-authored-by: Peter Steinberger <[email protected]>
openclaw#89379) * fix(providers): use native reasoning mode for direct Gemini API, keep CLI tagged Gemini 2.5+ delivers reasoning via native thinkingParts (thinkingConfig. includeThoughts). Having tagged mode active at the same time injects a <think>…</think>/<final>…</final> directive into the system prompt; the model opens a <think> block before a tool call, never closes it, and returns an empty post-tool turn (content:[], payloads=0 error, openclaw#69220). Fix: override resolveReasoningOutputMode in buildGoogleProvider() only — not in the shared GOOGLE_GEMINI_PROVIDER_HOOKS. The Gemini CLI backend (google-gemini-cli) runs gemini --output-format json and parses a text response field, not native thought parts; it must stay on tagged mode. A regression test confirms google-gemini-cli remains "tagged". Also remove the dead BUILTIN_REASONING_OUTPUT_MODES entry keyed on "google-generative-ai" from provider-utils.ts — that string is only ever the transport model.api value, never the provider id passed to resolveReasoningOutputMode, so the map was unreachable. Fixes openclaw#69220 * docs: clarify Gemini reasoning output modes * fix(google): keep Antigravity reasoning tagged * fix(google): default direct reasoning checks to native * fix(google): import reasoning context from plugin entry --------- Co-authored-by: Peter Steinberger <[email protected]>
openclaw#89379) * fix(providers): use native reasoning mode for direct Gemini API, keep CLI tagged Gemini 2.5+ delivers reasoning via native thinkingParts (thinkingConfig. includeThoughts). Having tagged mode active at the same time injects a <think>…</think>/<final>…</final> directive into the system prompt; the model opens a <think> block before a tool call, never closes it, and returns an empty post-tool turn (content:[], payloads=0 error, openclaw#69220). Fix: override resolveReasoningOutputMode in buildGoogleProvider() only — not in the shared GOOGLE_GEMINI_PROVIDER_HOOKS. The Gemini CLI backend (google-gemini-cli) runs gemini --output-format json and parses a text response field, not native thought parts; it must stay on tagged mode. A regression test confirms google-gemini-cli remains "tagged". Also remove the dead BUILTIN_REASONING_OUTPUT_MODES entry keyed on "google-generative-ai" from provider-utils.ts — that string is only ever the transport model.api value, never the provider id passed to resolveReasoningOutputMode, so the map was unreachable. Fixes openclaw#69220 * docs: clarify Gemini reasoning output modes * fix(google): keep Antigravity reasoning tagged * fix(google): default direct reasoning checks to native * fix(google): import reasoning context from plugin entry --------- Co-authored-by: Peter Steinberger <[email protected]>
Summary
Gemini 2.5 Pro/Flash exposes reasoning via native
thinkingPartsthroughthinkingConfig.includeThoughts. The sharedgoogle-geminifamily hook was selecting tagged reasoning output, which injected a<think>...</think>/<final>...</final>directive into the system prompt at the same time as the native thinking config was active.The model then:
thought:truepart<think>text block per the injected directive</think>content: [])Gateway logs:
incomplete turn detected: stopReason=stop payloads=0- surfacing a spurious error to the user, while burning about 140K input tokens.Changes
extensions/google/provider-registration.ts- overrideresolveReasoningOutputModein the directgoogleprovider to return"native", superseding the sharedgoogle-geminifamily default of"tagged"src/utils/provider-utils.ts- remove the now-deadBUILTIN_REASONING_OUTPUT_MODESentry keyed on"google-generative-ai": production callers pass the plugin id"google", not the transport API string, so the map entry was unreachabledocs/plugins/sdk-provider-plugins.md- document that direct Google uses native reasoning output while Gemini CLI keeps the tagged contractprovider-utils.test.tsandextensions/google/index.test.tsReal behavior proof
thinkingDefault: mediumproducingpayloads=0errors and spurious user-facing error messages on turns that include a tool call, as reported in Gemini text-tag reasoning conflicts with native thinking — produces unclosed <think>, empty post-tool turn, payloads=0 #69220.2ed7ec9c3d; direct Google provider using auth profilegoogle:default; modelgoogle/gemini-2.5-flash; embedded OpenClaw run with--thinking medium.pnpm openclaw agent --agent main --session-key pr-89379-gemini-proof --local --model google/gemini-2.5-flash --thinking medium --message 'Use an available local tool to run a harmless command that prints exactly OPENCLAW_GEMINI_PROOF, then reply with a one-sentence final answer that includes the command output. Do not search the web.' --json;node scripts/run-vitest.mjs src/utils/provider-utils.test.ts;node scripts/run-vitest.mjs extensions/google/index.test.ts;node ./node_modules/.bin/oxfmt --check docs/plugins/sdk-provider-plugins.md extensions/google/provider-registration.ts extensions/google/index.test.ts src/utils/provider-utils.ts src/utils/provider-utils.test.ts;pnpm docs:list;git diff --check.{ "payloads": [ { "text": "The command output is OPENCLAW_GEMINI_PROOF.", "mediaUrl": null } ], "meta": { "provider": "google", "model": "gemini-2.5-flash", "requestShaping": { "authMode": "auth-profile", "thinking": "medium" }, "toolSummary": { "calls": 1, "tools": ["exec"], "failures": 0 }, "completion": { "stopReason": "stop", "finishReason": "stop" }, "executionTrace": { "winnerProvider": "google", "winnerModel": "gemini-2.5-flash", "result": "success", "runner": "embedded", "fallbackUsed": false } }, "sessionScan": { "events": [ { "type": "model_change", "provider": "google", "modelId": "gemini-2.5-flash" }, { "type": "assistant", "contentTypes": ["thinking", "toolCall"], "toolNames": ["exec"], "stopReason": "toolUse" }, { "type": "toolResult", "ok": true }, { "type": "assistant", "contentTypes": ["text"], "stopReason": "stop" } ], "hasThinkTag": false, "hasFinalTag": false } }exectool call, returned a post-tool assistant final text payload, and did not hitpayloads=0; the session scan found no injected<think>or<final>tags. Focused tests passed:provider-utils.test.ts25/25,extensions/google/index.test.ts11/11. Formatting/docs/diff checks passed.gemini-2.5-flash; Gemini 2.5 Pro specifically and Gemini 3 were not tested in this local credential profile. Older Gemini models without native thinking are expected unaffected because native mode only prevents OpenClaw from injecting text-tag reasoning directives.Tests and validation
pnpm openclaw agent --agent main --session-key pr-89379-gemini-proof --local --model google/gemini-2.5-flash --thinking medium --message 'Use an available local tool to run a harmless command that prints exactly OPENCLAW_GEMINI_PROOF, then reply with a one-sentence final answer that includes the command output. Do not search the web.' --jsonnode scripts/run-vitest.mjs src/utils/provider-utils.test.ts- 25/25 passednode scripts/run-vitest.mjs extensions/google/index.test.ts- 11/11 passednode ./node_modules/.bin/oxfmt --check docs/plugins/sdk-provider-plugins.md extensions/google/provider-registration.ts extensions/google/index.test.ts src/utils/provider-utils.ts src/utils/provider-utils.test.ts- cleanpnpm docs:list- completedgit diff --check- cleanFixes #69220