Summary
When the user interrupts the agent (barge-in) in a cascaded AgentSession using the Google plugin (Vertex AI), the truncated assistant message is committed to the chat context after the user's transcript. The next LLM request is then sent with a history that ends with a model turn, which newer Gemini models (observed with gemini-3.6-flash) reject deterministically:
400 INVALID_ARGUMENT: "Requests ending with a model turn are not supported."
The plugin marks the error as retryable: true, so LLMStream.run retries the identical request in a loop (3 attempts in our runs) — every retry fails with the same 400, and the agent goes silent for ~25 seconds until the user happens to speak again, which appends a user turn and unblocks the conversation.
Older models (gemini-3.1-flash-lite, gemini-3.5-flash-lite) tolerate trailing model turns, so the bug only surfaces on newer models. Edit 2026-07-27: no longer accurate — gemini-3.5-flash-lite reproduces this in production too (84 errors / 21 lost turns / 7 calls in ~24h). See the update comment below. Google has documented this as intended API behavior for newer Gemini models (prefilled model turns are no longer supported) — see references below.
Environment
@livekit/agents: 1.5.5
@livekit/agents-plugin-google: 1.5.5 (LLM via Vertex AI, vertexai: true, location global)
- Model:
gemini-3.6-flash (also expected on other current-gen models enforcing the constraint)
- Node.js v24.18.0, Linux (Rocky 10)
- Cascaded pipeline: Google Cloud STT →
google.LLM → Google Cloud TTS, local VAD (inference.VAD), allowInterruptions: true
- Telephony (LiveKit SIP), Romanian language — irrelevant to the bug, included for completeness
Timeline from production logs (redacted)
The agent proposes an address; the user barges in with a confirmation while the agent is speaking; the agent's reply is truncated by the interruption:
14:07:22 [METRIC] llm ttft=849ms dur=1161ms
14:07:29 [USER] Da, da, da, da, acolo. <- user turn committed (barge-in)
14:07:29 [AGENT] Am găsit Liceul Teoretic Dumitru Tăuțan, de pe strada <- truncated by interruption,
committed AFTER the user turn
14:07:29 {"llm":"google.LLM","attempt":1,"error":{"type":"APIConnectionError","message":"Google LLM: API error -
{\"error\":{\"code\":400,\"message\":\"Requests ending with a model turn are not supported.\",
\"status\":\"INVALID_ARGUMENT\"}}","retryable":true},"msg":"failed to generate LLM completion, retrying in 0.1ms"}
14:07:29 ... attempt=2, same 400 ...
14:07:32 ... attempt=3, same 400 ...
14:07:34 [METRIC] llm ttft=-1ms dur=5381ms <- generation gave up
(~25s of dead air on the phone call)
14:07:49 [USER] Alo <- user speaks again -> a user turn is appended
14:07:50 [METRIC] llm ttft=961ms dur=1250ms <- next request succeeds
Root cause
LLMStream.run in agents-plugin-google/src/llm.ts sends the chat history exactly as produced by chatCtx.toProviderFormat('google'), with no normalization of the final turn:
const [turns, extraData] = await this.chatCtx.toProviderFormat("google");
const contents = turns.map((turn) => ({ role: turn.role, parts: turn.parts }));
After a barge-in, the committed order in the chat context is [..., user: "Da, da...", model: "<truncated reply>"], so contents ends with a model turn. Newer Gemini models reject that request with a deterministic 400.
Two compounding problems:
- No sanitization of the trailing turn for the Google provider, even though the Gemini API now requires the last content to be a
user turn.
- The 400 is treated as retryable — the identical request is retried and fails identically, turning a deterministic client-side formatting issue into a multi-second outage of the voice session.
Expected behavior
The plugin (or toProviderFormat('google')) should guarantee the request never ends with a model turn — e.g. by reordering a trailing assistant message before the preceding user message (chronologically faithful for the interruption case: the agent spoke the partial reply, then the user reacted), or by any equivalent normalization. Deterministic INVALID_ARGUMENT responses should also not be retried with an unchanged payload.
Repro steps
- Cascaded
AgentSession with google.LLM on gemini-3.6-flash (Vertex, any location), any STT/TTS, allowInterruptions: true.
- Let the agent produce a reply of a few seconds.
- Interrupt it mid-sentence with a short user utterance (so the truncated assistant message is committed after the user transcript).
- The next generation fails with
400 INVALID_ARGUMENT: "Requests ending with a model turn are not supported." and is retried in a loop; the session produces no speech until a new user turn arrives.
References
Summary
When the user interrupts the agent (barge-in) in a cascaded
AgentSessionusing the Google plugin (Vertex AI), the truncated assistant message is committed to the chat context after the user's transcript. The next LLM request is then sent with a history that ends with amodelturn, which newer Gemini models (observed withgemini-3.6-flash) reject deterministically:The plugin marks the error as
retryable: true, soLLMStream.runretries the identical request in a loop (3 attempts in our runs) — every retry fails with the same 400, and the agent goes silent for ~25 seconds until the user happens to speak again, which appends auserturn and unblocks the conversation.Older models (Edit 2026-07-27: no longer accurate —gemini-3.1-flash-lite,gemini-3.5-flash-lite) tolerate trailing model turns, so the bug only surfaces on newer models.gemini-3.5-flash-litereproduces this in production too (84 errors / 21 lost turns / 7 calls in ~24h). See the update comment below. Google has documented this as intended API behavior for newer Gemini models (prefilled model turns are no longer supported) — see references below.Environment
@livekit/agents: 1.5.5@livekit/agents-plugin-google: 1.5.5 (LLM via Vertex AI,vertexai: true, locationglobal)gemini-3.6-flash(also expected on other current-gen models enforcing the constraint)google.LLM→ Google Cloud TTS, local VAD (inference.VAD),allowInterruptions: trueTimeline from production logs (redacted)
The agent proposes an address; the user barges in with a confirmation while the agent is speaking; the agent's reply is truncated by the interruption:
Root cause
LLMStream.runinagents-plugin-google/src/llm.tssends the chat history exactly as produced bychatCtx.toProviderFormat('google'), with no normalization of the final turn:After a barge-in, the committed order in the chat context is
[..., user: "Da, da...", model: "<truncated reply>"], socontentsends with amodelturn. Newer Gemini models reject that request with a deterministic 400.Two compounding problems:
userturn.Expected behavior
The plugin (or
toProviderFormat('google')) should guarantee the request never ends with amodelturn — e.g. by reordering a trailing assistant message before the preceding user message (chronologically faithful for the interruption case: the agent spoke the partial reply, then the user reacted), or by any equivalent normalization. DeterministicINVALID_ARGUMENTresponses should also not be retried with an unchanged payload.Repro steps
AgentSessionwithgoogle.LLMongemini-3.6-flash(Vertex, any location), any STT/TTS,allowInterruptions: true.400 INVALID_ARGUMENT: "Requests ending with a model turn are not supported."and is retried in a loop; the session produces no speech until a new user turn arrives.References