Summary
LiveKit Inference's google/gemma-4-31b-it emits Gemma 4 channel-thought markup that leaks into customer-facing agent output (transcription / TTS / chat history). strip_thinking_tokens only strips <think>/</think>, so Gemma's markers pass through unchanged.
Markers
Per Gemma 4 model card:
- Open:
<|channel>thought (often followed by \n)
- Close:
<channel|>
- Body: internal reasoning (or empty when thinking is "off" — larger models still emit the empty wrapper)
Example stream content that reaches the agent:
<|channel>thought
Customer wants a $100 refund; check policy.
<channel|>I can help start a return instead.
Or empty:
<|channel>thought
<channel|>Happy to help with that.
Where it leaks
In the Agents SDK Inference LLM path, deltas go through strip_thinking_tokens before becoming ChatChunks:
https://github.com/livekit/agents/blob/main/livekit-agents/livekit/agents/inference/llm.py
THINK_TAG_START = "<think>"
THINK_TAG_END = "</think>"
def strip_thinking_tokens(content: str | None, thinking: asyncio.Event) -> str | None:
...
Those tags don't match Gemma's channel markers, so the markup reaches llm_node → TTS / transcription_node / session history.
Expected
Inference (or strip_thinking_tokens) should treat Gemma channel-thought blocks like <think> blocks: drop the markers and any body between them so only the final answer is customer-visible. Prior-turn thoughts also must not re-enter chat history (Gemma model card: "No Thinking Content in History").
Actual
Raw <|channel>thought / <channel|> (and sometimes the thought body) appear in the LiveKit transcription stream used by text chat UIs, and would be spoken on voice.
Repro (Agents SDK + Inference)
AgentSession with inference.LLM(model="google/gemma-4-31b-it")
- Multimodal/text chat so agent replies ride
lk.transcription
- Ask something that triggers a tool-using support turn (refund / damage claim)
- Inspect the transcription / chat history for
<|channel>thought / <channel|>
Workaround
App-side llm_node filter that strips the Gemma markers before chunks reach TTS/transcription/history (same state-machine pattern as the chain-of-thought recipe, with Gemma's tags).
Related
Summary
LiveKit Inference's
google/gemma-4-31b-itemits Gemma 4 channel-thought markup that leaks into customer-facing agent output (transcription / TTS / chat history).strip_thinking_tokensonly strips<think>/</think>, so Gemma's markers pass through unchanged.Markers
Per Gemma 4 model card:
<|channel>thought(often followed by\n)<channel|>Example stream content that reaches the agent:
Or empty:
Where it leaks
In the Agents SDK Inference LLM path, deltas go through
strip_thinking_tokensbefore becomingChatChunks:https://github.com/livekit/agents/blob/main/livekit-agents/livekit/agents/inference/llm.py
Those tags don't match Gemma's channel markers, so the markup reaches
llm_node→ TTS /transcription_node/ session history.Expected
Inference (or
strip_thinking_tokens) should treat Gemma channel-thought blocks like<think>blocks: drop the markers and any body between them so only the final answer is customer-visible. Prior-turn thoughts also must not re-enter chat history (Gemma model card: "No Thinking Content in History").Actual
Raw
<|channel>thought/<channel|>(and sometimes the thought body) appear in the LiveKit transcription stream used by text chat UIs, and would be spoken on voice.Repro (Agents SDK + Inference)
AgentSessionwithinference.LLM(model="google/gemma-4-31b-it")lk.transcription<|channel>thought/<channel|>Workaround
App-side
llm_nodefilter that strips the Gemma markers before chunks reach TTS/transcription/history (same state-machine pattern as the chain-of-thought recipe, with Gemma's tags).Related
strip_thinking_tokenslist-delta crash: strip_thinking_tokens crashes on list-shaped streaming delta content (AttributeError: 'list' object has no attribute 'find') #6323