Skip to content

[Bug] google: do not emit thought:true part text as visible assistant text #1503

Closed

Description

@LoneFireBlossom

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:

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:

  1. standalone { thought: true, text };
  2. a thought part followed by a functionCall, preserving signature replay;
  3. an ordinary { text } part, which must remain visible;
  4. both parseStream and parseResponse.

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

  1. changed the title [-]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[/+] on Aug 12, 2026
  2. added
    providerProvider adapters, OpenAI-compat presets, upstream API quirks
    streamingSSE, WebSocket, terminal stream frames
    on Aug 12, 2026
  3. Ingwannu commented on Aug 12, 2026

    @Ingwannu
    Owner

    Confirmed on current dev@b8a2e51b81aafec33a4865675d2bb29aa1f9eea1. Both Google response parsers classify every non-empty Part.text as text_delta; neither path checks part.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-empty text emits reasoning_raw_delta, never text_delta;
    • ordinary text remains text_delta;
    • streaming and non-streaming parsing use the same rule;
    • function calls and the existing Antigravity thought-signature observation/replay path remain unchanged;
    • focused regressions cover standalone thought text, thought text followed by a function call, ordinary text, and both transports.

    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.

  4. Ingwannu commented on Aug 12, 2026

    @Ingwannu
    Owner

    Implemented the confirmed fix in draft PR #1508, based on the latest dev@37139818d2d3fe7b033e958ccf87080e4be2b4b7:

    • thought: true text now emits reasoning_raw_delta, never visible text_delta;
    • ordinary Google text remains visible;
    • SSE and buffered JSON use one shared classifier;
    • function-call ordering, inline images, and Antigravity/Vertex thought-signature observation remain intact;
    • the transport decision and its tradeoffs are documented in structure/04_transports-and-sidecars.md.

    Verification on PR head 320f9058620afa42e02bfc22be9416f5eae2cca8:

    • Google focused tests: 81 passed, 0 failed;
    • 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.

  5. Ingwannu commented on Aug 12, 2026

    @Ingwannu
    Owner

    Update: draft PR #1508 was rebased again after dev advanced. Current exact head is b3ba2838e2d226cd7967e2a17b0545f8a653d260 on dev@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.

  6. lidge-jun commented on Aug 12, 2026

    @lidge-jun
    Owner

    Fixed on dev by #1508, merged as 567a6d4ffe6a7d7b341b5cc11e736081e1bb4522. The implementation is @Ingwannu's; commit 219e7f365 is preserved in the history.

    Both parsers now route thought: true text through a shared classifier: thought text becomes reasoning_raw_delta, ordinary text stays text_delta. Since the two loops call one helper, they cannot drift apart again.

    The reported behavior matches the vendor contract. Part.thought is documented as marking the model's thought process, thought summaries are opt-in via includeThoughts, 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:

    • Heartbeat classification. emittedContentEvent is 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.
    • Thought-signature replay. Gemini 3 returns 400 when the first function-call part of a step loses its 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 --noEmit exit 0; tests/google-hardening.test.ts + tests/google-antigravity-wire.test.ts 79 pass / 0 fail; full suite 11,319 pass / 0 fail; privacy:scan pass; 26 CI checks green including all Linux shards and macOS. Reverting only src/adapters/google.ts to the pre-fix state makes the new tests fail, so they exercise the change rather than passing vacuously.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingproviderProvider adapters, OpenAI-compat presets, upstream API quirksstreamingSSE, WebSocket, terminal stream frames

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions