Skip to content

fix(gemini): safely stream and round-trip thought parts - #58

Merged
CrazyBoyM merged 7 commits into
mainfrom
agent/fix-gemini-thinking-stream
Jul 10, 2026
Merged

fix(gemini): safely stream and round-trip thought parts#58
CrazyBoyM merged 7 commits into
mainfrom
agent/fix-gemini-thinking-stream

Conversation

@CrazyBoyM

Copy link
Copy Markdown
Contributor

Summary

This PR incorporates and extends #56. It merges all four original commits from @kirillreutski unchanged, preserving their exact SHAs and author attribution, then adds the protocol and regression hardening needed for a safe merge.

The original PR correctly enabled Gemini thought summaries and began exposing them as reasoning blocks. Pre-merge review found additional lifecycle, ordering, transport, signature, and compatibility edges that needed to be fixed together so the feature would be safe for direct provider consumers and Agent users.

What changed

  • add includeThoughts: true for Gemini thinking budgets/levels, including valid budgetTokens: 0 and -1
  • classify inbound part.thought === true as canonical reasoning for every history transport, preventing thought leakage into final answer text or omit history
  • preserve original Gemini Content.parts order across SSE events, tools, and JSON-array fallbacks
  • emit strictly paired, contiguous content_block_start -> delta* -> stop lifecycles, including error paths
  • support CRLF, multiline data:, arbitrary network chunk boundaries, bare JSON fallbacks, and batched arrays inside SSE frames
  • fail clearly on malformed framing while closing active blocks and releasing/cancelling the response reader
  • preserve thoughtSignature on thought, normal text, signature-only empty text, and function-call parts
  • preserve provider functionCall.id / matching functionResponse.id; synthetic SDK IDs are never sent back to older Gemini models that did not provide an ID
  • preserve streamed metadata through Agent, forward modelConfig.thinking, emit think_chunk_end, and remove retained reasoning from both content and metadata when retainThinking: false
  • accept both thought_signature and legacy thoughtSignature on resume
  • retain the pre-existing behavior of streaming all returned candidates
  • remove unrelated package-lock.json generator noise from fix(gemini): emit reasoning chunks and fix thought parts extraction #56

reasoningTransport: 'provider' remains the mode for lossless native thought/signature continuity. text intentionally rewrites reasoning into <think> parts for cross-provider compatibility, so it does not attach a native signature to a modified Part; omit removes reasoning from outbound history.

Verification

  • npm run build
  • npx tsc --noEmit
  • full local unit runner: 296 passed, 0 failed (local PostgreSQL service unavailable; CI runs that suite against postgres:16-alpine)
  • CI-runtime Node 20 targeted suites: 26/26 Gemini and 15/15 Agent
  • npm pack --dry-run
  • git diff --check
  • independent Standards review: no merge blockers or hard violations
  • independent Spec review against fix(gemini): emit reasoning chunks and fix thought parts extraction #56 and the follow-up acceptance criteria: no remaining gaps or scope creep

GitHub Actions remains the merge gate for PostgreSQL, real provider E2E, agent integration, and comprehensive agent workflows.

Attribution and collaboration

The branch contains #56 through merge commit 8be6c1d; original head ff5cf2a and all preceding author commits remain ancestors of this PR head. Thank you @kirillreutski for the original diagnosis and implementation direction.

Supersedes #56 after CI validation, without squashing away the original contributor history.

kirillreutski and others added 7 commits May 17, 2026 14:15
…ransport='provider'

Gemini API returns thinking content as parts with `thought: true`. Previously
these were pushed as regular text blocks regardless of reasoningTransport,
so think_chunk SSE events were never emitted for Gemini models.

When reasoningTransport is 'provider', parts with thought===true are now
pushed as { type: 'reasoning', reasoning } blocks. The agent emits these
as think_chunk_start / think_chunk / think_chunk_end progress events,
allowing clients to render thinking separately from the response text.

Default behavior (reasoningTransport: 'text') is unchanged — thought parts
continue to arrive as text blocks and get normalized by normalizeThinkBlocks.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Without includeThoughts, the model thinks internally but does not return
thought parts in the response. This adds it automatically whenever
thinkingBudget or thinkingLevel is set.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
parseGeminiChunk now separates parts with thought===true into thoughtChunks
when reasoningTransport is 'provider'. The streaming loop yields them as
content_block_start(reasoning) + content_block_delta(reasoning_delta) before
text chunks, so the agent emits think_chunk_start/think_chunk/think_chunk_end
SSE events that clients can render separately from the response text.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@CrazyBoyM
CrazyBoyM marked this pull request as ready for review July 10, 2026 23:19
@CrazyBoyM
CrazyBoyM merged commit 2596118 into main Jul 10, 2026
8 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.

2 participants