Skip to content

fix: handle google/gemini-3.5-flash thought token leak in inference - #7045

Closed
HoneyTyagii wants to merge 1 commit into
livekit:mainfrom
HoneyTyagii:fix/gemini-3.5-flash-thought-leak
Closed

fix: handle google/gemini-3.5-flash thought token leak in inference#7045
HoneyTyagii wants to merge 1 commit into
livekit:mainfrom
HoneyTyagii:fix/gemini-3.5-flash-thought-leak

Conversation

@HoneyTyagii

Copy link
Copy Markdown

Add _MODEL_THINK_TAGS entry for google/gemini-3.5-flash with empty end tag to match the observed behavior where the thought token has no closing delimiter. The strip_thinking_tokens function already handles empty end tags correctly by treating the remainder as visible content after the start tag is found.

Fixes issue where thought tokens were leaked into delta.content and spoken to callers via TTS/transcription.

Closes #6942

@HoneyTyagii
HoneyTyagii requested a review from a team as a code owner August 30, 2026 14:20
Copilot AI lite review requested due to automatic review settings August 30, 2026 14:20

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 potential issues.

1 flag not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)

Devin Review


_MODEL_THINK_TAGS = {
"google/gemma-4-31b-it": ("<|channel>thought", "<channel|>"),
"google/gemini-3.5-flash": ("thought", ""),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Unpackaged fix never reaches users

The new _MODEL_THINK_TAGS entry sits outside the source tree selected by the workspace build. Installed agents retain the thought-token leak.

Prompt for agents
Move the Gemini model-tag configuration and the empty-end-tag filter change into the actual livekit-agents package under livekit-agents/livekit/agents/inference/llm.py and livekit-agents/livekit/agents/llm/utils.py. Remove the accidental top-level livekit/agents copies. Add a unit test that imports the packaged inference module and verifies the Gemini stream behavior so an out-of-package edit cannot pass unnoticed.
Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


_MODEL_THINK_TAGS = {
"google/gemma-4-31b-it": ("<|channel>thought", "<channel|>"),
"google/gemini-3.5-flash": ("thought", ""),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Ordinary replies lose “thought”

For Gemini 3.5 Flash, _MODEL_THINK_TAGS treats every occurrence of “thought” as a control token. Valid replies are silently altered before speech.

Prompt for agents
Restrict recognition of Gemini's bare thought marker to the exact protocol position or framing in which the gateway emits it, rather than searching all streamed response text. Preserve state across chunks so a split initial marker is still recognized, but never remove later natural-language occurrences. Add tests for marker framing, arbitrary chunk splits, and ordinary replies containing “thought”.
Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Add _MODEL_THINK_TAGS entry for google/gemini-3.5-flash with empty end tag
to match the observed behavior where the thought token has no closing delimiter.
The strip_thinking_tokens function already handles empty end tags correctly
by treating the remainder as visible content after the start tag is found.

Fixes issue where thought tokens were leaked into delta.content and spoken
to callers via TTS/transcription.

Closes livekit#6942
@HoneyTyagii
HoneyTyagii force-pushed the fix/gemini-3.5-flash-thought-leak branch from c4b18ef to 973f83b Compare August 30, 2026 15:02

@STiFLeR7 STiFLeR7 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-checked this against the leak samples in #6942 and I think there's a residual gap in strip_thinking_tokens's end_tag == "" branch:

idx = state._buffer.find(state.start_tag)
if idx >= 0:
    visible.append(state._buffer[:idx])
    state._buffer = state._buffer[idx + len(state.start_tag) :]
    if state.end_tag == "":
        # For empty end tag, we do not look for an end tag: the rest is visible.
        visible.append(state._buffer)
        state._buffer = ""
        break

This strips only the literal 4-character marker "thought" and appends everything after it as visible — it doesn't consume the separator artifact that follows in the real leak shapes #6942 sampled:

thought[]I see you're calling from the number we have on file...
thought() I can help you with that...
thought [] Muddy waters...

After this filter, those become []I see you're calling..., () I can help you with that..., [] Muddy waters... — the "thought" word is gone, but the []/()/leading-space artifact immediately after it is still spoken/shown, since it's unconditionally appended along with the real content.

The issue body's own proposed app-side workaround already accounts for this shape:

_THOUGHT_PREFIX = re.compile(r"^thought\s*(?:\[[^\]]*\]|\([^)]*\))?\s*(?![a-z])")

i.e. it strips "thought" plus an optional trailing [...]/(...) and whitespace, not just the bare word. Might be worth folding an equivalent pattern into the end_tag == "" branch here — e.g. matching start_tag plus an optional \s*(?:\[[^\]]*\]|\([^)]*\))?\s* immediately after it — so the artifact doesn't leak through even when this PR's fix is applied. (The 8/27 cases with a non-empty thought body and no separator at all are correctly called out in the issue as unfixable without a real delimiter — this is specifically about the empty-body cases where a separator artifact is present and could be stripped.)

@longcw

longcw commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

closing since it's fixed in gateway

@longcw longcw closed this Sep 2, 2026
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.

Inference: google/gemini-3.5-flash leaks its thought part into content (no closing delimiter)

5 participants