Google Realtime (Vertex AI): update_instructions() appends a model turn instead of replacing the system instruction
I maintainer di solito rispondono entro 1 giorno
Valutazione
- Difficoltà
- 2/5
- Tempo stimato
- 1-3 ore
- Idoneità per principianti
- 25/100
Direzione di ricerca
The bug is in update_instructions in livekit/plugins/google/realtime/realtime_api.py, where the turn is sent with a hard-coded role="model". Read that method and the reproduction script in the issue, which compares prompt-token counts across roles on Vertex AI. Work is already under way in linked PR #7802, so check that before starting. Done means a Vertex-only role change plus a unit test covering vertexai=True and False.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
Bug Description
On Vertex AI, RealtimeSession.update_instructions() sends new instructions to a live session as a client_content turn with role="model" (livekit/plugins/google/realtime/realtime_api.py, update_instructions). That does not change the session's system instruction:
- the previous instructions stay in context;
- the new instructions are only appended as a model turn, which the model may ignore;
- context-window compression (
SlidingWindow) can later drop that turn, while the old system instruction is always kept.
So after update_instructions() the session runs on the old instructions plus a conversation turn, not on the new instructions.
Google documents a mid-session update that does replace it. From Start and manage live sessions → Update system instructions during a session (https://docs.cloud.google.com/gemini-enterprise-agent-platform/models/live-api/start-manage-session):
To update the system instructions mid-session, you can send text content with the system role. The updated system instruction will remain in effect for the remaining session.
The code comment next to the role says "Both APIs error on role="system"". On Vertex AI I see no error: the server accepts the turn and swaps the instruction. The evidence is below.
Expected Behavior
On Vertex AI, after update_instructions(new) on an active session, the session should run on new as its system instruction, with the previous instructions gone. This is what mutable_instructions=True implies, and what happens when instructions are set before connecting.
Reproduction Steps
The script below uses only `google-genai` (no LiveKit) to show what the server does with each role. It measures the prompt tokens of the same user turn in three Vertex AI sessions:
- **fresh B:** `system_instruction=B`
- **A + system B:** `system_instruction=A`, then a `role="system"` turn with B
- **A + model B:** `system_instruction=A`, then a `role="model"` turn with B (what `update_instructions` sends today)
Instruction A is about 577 tokens and contains a fact (a backup word) that B does not.
Results, identical in two runs:
| Model | fresh B | A + system B | A + model B |
|---|---|---|---|
| `gemini-live-2.5-flash-native-audio` | 34 | **34** | 611 |
| `gemini-live-2.5-flash` | 34 | **34** | 611 |
| `gemini-3.8-live` | 34 | **34** | 611 |
- **`role="system"`:** the prompt is exactly the size of a fresh session on B, so A is gone. The model also no longer knows A's backup word.
- **`role="model"`:** A is still there (+577 tokens). On `gemini-live-2.5-flash` the model kept answering from A in both runs: *"The secret code is APPLE-17 and the backup word is CHERRY."*
"""Does a mid-session role="system" turn replace the system instruction on Vertex AI Live?
GOOGLE_CLOUD_PROJECT=... GOOGLE_CLOUD_LOCATION=us-central1 python repro_system_role.py [model ...]
"""
import asyncio
import os
import sys
from google import genai
from google.genai import types
FILLER = " ".join(f"Note {i}: this sentence only adds length to instruction A." for i in range(40))
A = (
"You are assistant A. The secret code is APPLE-17. The backup word is CHERRY. "
"Answer in one short sentence. " + FILLER
)
B = "You are assistant B. The secret code is BANANA-42. Answer in one short sentence."
Q = "What is the secret code, and what is the backup word?"
async def ask(client: genai.Client, model: str, system: str, update_role: str | None = None):
config = types.LiveConnectConfig(
response_modalities=[types.Modality.AUDIO],
system_instruction=types.Content(parts=[types.Part(text=system)]),
output_audio_transcription=types.AudioTranscriptionConfig(),
)
async with client.aio.live.connect(model=model, config=config) as session:
if update_role:
await session.send_client_content(
turns=types.Content(role=update_role, parts=[types.Part(text=B)]),
turn_complete=False,
)
await session.send_client_content(
turns=types.Content(role="user", parts=[types.Part(text=Q)]), turn_complete=True
)
tokens, said = None, ""
async for msg in session.receive(): # ends at turn_complete
if msg.usage_metadata and msg.usage_metadata.prompt_token_count:
tokens = msg.usage_metadata.prompt_token_count
sc = msg.server_content
if sc and sc.output_transcription and sc.output_transcription.text:
said += sc.output_transcription.text
return tokens, said.strip()
async def main() -> None:
client = genai.Client(
vertexai=True,
project=os.environ["GOOGLE_CLOUD_PROJECT"],
location=os.environ.get("GOOGLE_CLOUD_LOCATION", "us-central1"),
)
for model in sys.argv[1:] or ["gemini-live-2.5-flash-native-audio", "gemini-3.8-live"]:
results = await asyncio.gather(
ask(client, model, B), ask(client, model, A, "system"), ask(client, model, A, "model")
)
print(model)
for name, (tokens, said) in zip(("fresh B", "A + system B", "A + model B"), results):
print(f" {name:13} prompt_tokens={tokens!s:<6} reply={said!r}")
if __name__ == "__main__":
asyncio.run(main())
I also confirmed this inside a LiveKit `AgentSession` on Vertex AI with production-size prompts (about 1k tokens, replaced mid-call by about 17k), on `gemini-live-2.5-flash` and `gemini-3.8-live`:
- With `role="system"`, the prompt-token count after the update equalled a fresh session started on the new prompt.
- The new instruction survived `SlidingWindow` compression. The same text sent as a `user` or `model` turn was dropped by the window.
- There were no errors or connection closes.
Operating System
macOS (agent host); Vertex AI us-central1
Models Used
Vertex AI Live: gemini-live-2.5-flash-native-audio, gemini-live-2.5-flash, gemini-3.8-live
Package Versions
livekit-agents==1.7.1
livekit-plugins-google==1.7.1
google-genai==2.19.0
`update_instructions` still hard-codes `role="model"` in `livekit-plugins-google` 1.8.6
Session/Room/Call IDs
Not applicable: reproduced with the standalone script above
Proposed Solution
Send `role="system"` on Vertex AI and keep `role="model"` for the Gemini API. In `update_instructions` (1.8.6):
turns=[
types.Content(
parts=[types.Part(text=instructions)],
- # Both APIs error on role="system". This was role=None on the
- # Gemini API, which 2.5 accepted as the system role but 3.1 and
- # 3.8 reject with a 1007 close that kills the session. "model"
- # is accepted by all three and by Vertex.
- role="model",
+ # On Vertex AI, role="system" replaces the session's system
+ # instruction (see "Update system instructions during a session"
+ # in the Vertex AI Live API docs). role="model" is only appended
+ # as a model turn, so the previous instructions stay in effect.
+ # The Gemini API errors on role="system" (and 3.1/3.8 close with
+ # 1007 on role=None), so it keeps role="model".
+ role="system" if self._opts.vertexai else "model",
)
],
turn_complete=False,
- **Scope:** only `vertexai=True` with an active session. Instructions set before connecting still go into the setup message.
- **Also covers handoffs:** when `AgentActivity` reuses a realtime session across agents and pushes the new agent's instructions, those would then really take effect on Vertex.
- **Not tested by me:** the Gemini API (unchanged by the proposal), and 3.1 models on Vertex.
Happy to open a PR with this change, plus a unit test asserting the role sent for `vertexai=True` and `False`.
Additional Context
google-genai'sContent.roledocstring says "Must be either 'user' or 'model'", but Vertex AI accepts"system"onclient_contentas documented- Workaround until this is fixed: subclass
RealtimeSession, overrideupdate_instructionsto sendrole="system"on Vertex, and subclassRealtimeModel.session()to return it. This is what I use for now
Screenshots and Recordings
- Lingua principale
- Python
- Stelle
- 14.6k
- Fork
- 3.9k
- Merge medio
- 3g 1h
- PR unite (30g)
- 173
Preparare l'ambiente
Questo progetto non fornisce container di sviluppo, Dockerfile né guida per i contributori, quindi l'ambiente è a tuo carico: parti dal suo README e consulta la nostra guida al primo contributo per i passaggi generali.
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di livekit/agents
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 68/100
I maintainer di solito rispondono entro 1 giorno
-
Assistant markup is stripped from chat history when expressive mode is offForse già presa @ShivamSharma43 l’ha presa 8 giorni fa. Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
I maintainer di solito rispondono entro 1 giorno
-
docs: openai.LLM vs openai.responses.LLM for HTTP OpenAI-compat hostsForse di nuovo libera @AbarnaaSree l’ha presa 47 giorni fa e non c’è nessuna pull request aperta. Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
livekit/agents#7022 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 4/5 3-5 giorni Idoneità per principianti 25/100
I maintainer di solito rispondono entro 1 giorno
-
DuplexRealtimeAdapter: transcript fragment for audio that is never sent anchors the next burst, shifting transcripts by one utteranceForse già presa @dyyfk l’ha presa oggi. Aperta
Difficoltà 4/5 3-5 giorni Idoneità per principianti 45/100
I maintainer di solito rispondono entro 1 giorno
Tutte le issue di livekit/agents
Issue simili
-
documentation good first issue
Difficoltà 2/5 Mezza giornata Idoneità per principianti 72/100
sujithputta02/Esapay#13 ·
-
bug
Difficoltà 2/5 1-3 ore Idoneità per principianti 70/100
I maintainer di solito rispondono entro 1 giorno
-
Flaky: credential-proxy admission metric test asserts a wall-clock bound it does not establishApertaneeds-triage
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
gke-labs/kube-agents#3277 ·
I maintainer di solito rispondono entro 1 giorno
-
bug
Difficoltà 2/5 1-3 ore Idoneità per principianti 68/100
jbacalso24/mimry#291 ·
I maintainer di solito rispondono entro 1 giorno
-
good first issue
Difficoltà 2/5 Meno di un'ora Idoneità per principianti 82/100
eli-alvarado/us-watchlist-alerts#14 ·
I maintainer di solito rispondono entro 1 giorno