Repository navigation
[Bug] google: do not emit thought:true part text as visible assistant text #1503
Description
Client or integration
Direct HTTP/API client
Area
Provider adapter
Summary
The Google adapter classifies every non-empty Gemini Part.text as visible assistant output, even when the same part is marked thought: true.
Both the streaming and non-streaming parsers currently emit { type: "text_delta", text: part.text } without checking the thought flag:
- Streaming:
src/adapters/google.tslines 605-619 - Non-streaming:
src/adapters/google.tslines 825-836
The downstream Responses bridge treats text_delta as ordinary assistant output. As a result, model-internal thought text can cross the hidden-reasoning boundary and appear as visible answer text.
Expected: when part.thought === true, its text should produce a hidden reasoning event such as reasoning_raw_delta or thinking_delta. It must not produce text_delta, and reasoning.summary: "none" should continue to suppress its visible rendering. Thought-signature replay should remain unchanged.
Actual: both parsers produce visible text_delta for the thought text.
This affects Google-family routes handled by this adapter when the upstream returns a text-bearing thought part. It is not specific to one client transport or to streaming mode. Normal parts without thought: true should remain visible text.
Reproduction
A synthetic provider response is sufficient; no credential or live model request is needed.
Use this minimal candidate in the Google adapter response parser:
{
"candidates": [
{
"content": {
"parts": [
{ "thought": true, "text": "internal reasoning" }
]
},
"finishReason": "STOP"
}
]
}Non-streaming fixture:
const response = new Response(JSON.stringify(payload), {
headers: { "content-type": "application/json" },
});
const events = await adapter.parseResponse!(response, budget);
expect(events).not.toContainEqual({
type: "text_delta",
text: "internal reasoning",
});
expect(events).toContainEqual({
type: "reasoning_raw_delta",
text: "internal reasoning",
});For the streaming fixture, put the same candidate object in one data: SSE event and collect adapter.parseStream(...).
Current result in both modes:
{ type: "text_delta", text: "internal reasoning" }Expected result in both modes:
{ type: "reasoning_raw_delta", text: "internal reasoning" }A focused regression test should also cover:
- standalone
{ thought: true, text }; - a thought part followed by a
functionCall, preserving signature replay; - an ordinary
{ text }part, which must remain visible; - both
parseStreamandparseResponse.
A small shared part classifier used by both parser paths would prevent the two implementations from drifting.
Version
2.12.0 and current main/dev as of 2026-08-12. Verified at main commit 6d881db206c6a74da6b64fa22b6980faf05d0122; the same classification is present on dev commit e8db4e0365b12a314d1c08ec2cf81599efe5b2d3.
Operating system
Not operating-system-specific; this is deterministic TypeScript response classification in the Google adapter.
Provider and model
Google adapter (google, including applicable Google AI Studio, Vertex, and Cloud Code Assist routes) with a Gemini-family response containing a text-bearing thought: true part.
Logs or error output
No upstream error is required. The incorrect adapter event is:
{ type: "text_delta", text: "internal reasoning" }Screenshots and supporting files
None. The synthetic fixture above is self-contained.
Redacted configuration
{}No provider credential or local configuration is required for the synthetic parser fixture.
Checks
- I searched existing issues and documentation.
- I removed secrets, tokens, account details, request credentials, and personal data.
Activity
[-]google: do not emit thought:true part text as visible assistant text[/-][+][Bug] google: do not emit thought:true part text as visible assistant text[/+]Ingwannu commentedon Aug 12, 2026
Confirmed on current
dev@b8a2e51b81aafec33a4865675d2bb29aa1f9eea1. Both Google response parsers classify every non-emptyPart.textastext_delta; neither path checkspart.thought === true, so a text-bearing thought part crosses the hidden-reasoning boundary in both SSE and buffered JSON modes.I will implement a focused shared part classifier in the Google adapter so:
thought: true+ non-emptytextemitsreasoning_raw_delta, nevertext_delta;text_delta;I found no open PR covering this exact visibility-boundary fix. I will keep the issue open until the fix PR is independently reviewed and integrated.
Ingwannu commentedon Aug 12, 2026
Implemented the confirmed fix in draft PR #1508, based on the latest
dev@37139818d2d3fe7b033e958ccf87080e4be2b4b7:thought: truetext now emitsreasoning_raw_delta, never visibletext_delta;structure/04_transports-and-sidecars.md.Verification on PR head
320f9058620afa42e02bfc22be9416f5eae2cca8:bun run typecheck: passed;bun run privacy:scan: passed.The repository-wide run reached the local 600-second resource limit before completion. The new Google regressions passed within that run. Separate reruns reproduced unrelated baseline timing/environment failures in Claude replay, native-main ownership, and server-auth paths; none exercises this Google adapter classification.
I am leaving this issue open until #1508 receives independent maintainer review and is integrated into
dev.Ingwannu commentedon Aug 12, 2026
Update: draft PR #1508 was rebased again after
devadvanced. Current exact head isb3ba2838e2d226cd7967e2a17b0545f8a653d260ondev@4fed8d3fe431ad23be83f3aff2af18ef8b8ecd71. The Google focused suite remains 81/81 green, and typecheck plus privacy scan pass on this head. Cross-platform CI is running; the issue remains open pending independent review and integration.fix(openai-chat): refuse to emit an unnamed streamed tool call (#1531)
lidge-jun commentedon Aug 12, 2026
Fixed on
devby #1508, merged as567a6d4ffe6a7d7b341b5cc11e736081e1bb4522. The implementation is @Ingwannu's; commit219e7f365is preserved in the history.Both parsers now route
thought: truetext through a shared classifier: thought text becomesreasoning_raw_delta, ordinary text staystext_delta. Since the two loops call one helper, they cannot drift apart again.The reported behavior matches the vendor contract.
Part.thoughtis documented as marking the model's thought process, thought summaries are opt-in viaincludeThoughts, and the official SDK examples branch on the flag to render thoughts in a separate channel — so a text-bearing thought part was never the answer channel.Two contracts the change touches implicitly were pinned with additional tests before merge:
emittedContentEventis set for the reasoning event too. It only drives synthetic-heartbeat suppression, and a thought delta is real upstream activity, so emitting a heartbeat alongside it would claim the stream was idle while the model was working. The visible-text case is asserted next to it as the control.thoughtSignature. A payload mixing a thought part with a signed function call now asserts the signature is still replayed, instead of inferring safety from unrelated fixtures.Verification at the merged head:
bun x tsc --noEmitexit 0;tests/google-hardening.test.ts+tests/google-antigravity-wire.test.ts79 pass / 0 fail; full suite 11,319 pass / 0 fail;privacy:scanpass; 26 CI checks green including all Linux shards and macOS. Reverting onlysrc/adapters/google.tsto the pre-fix state makes the new tests fail, so they exercise the change rather than passing vacuously.fix(openai-chat): refuse to emit an unnamed streamed tool call (lidge…