late-businessperson-58140
12/24/2025, 8:49 AMtall-airline-16756
12/24/2025, 8:49 AMtall-airline-16756
12/24/2025, 8:50 AMcontents being passed ends with a non-user role (e.g., assistant/model or similar), which Gemini treats as an invalid single-turn request.
What you can do next
1. Verify the chat context just before the LLM call:
◦ Log the messages array (role and content) you send to generate_content_stream.
◦ Confirm the last entry is role: "user" (or remove the role for that last item).
1. Common fixes (pick the one that matches your codepath):
◦ If you manually build contents, ensure it ends with a user message:
contents = [
{"role":"system","parts":["..."]},
{"role":"user","parts":["User question here"]} # <-- must be last
]
• If you use LiveKit/ChatContext helpers, ensure you do not append an assistant/model message before calling the LLM. Let the LLM produce the assistant message.
• If replaying history, ensure messages alternate and the last is user.
1. Quick debug steps:
◦ Add a print right before the call: print([(m.role, m.parts if hasattr(m,'parts') else m.content) for m in contents])
◦ If you see the last role is `assistant`/`model`, trace where that message was appended and stop that from happening before generating.
If you’d like, paste the small snippet that builds or transforms contents (the few lines before the generate_content_stream call) and I’ll point out exactly where to change it.late-businessperson-58140
12/24/2025, 8:53 AMtall-airline-16756
12/24/2025, 8:54 AMasync def _llm_one_shot(session: AgentSession, system_prompt: str) -> str:
llm = _session_llm(session)
# Build a context that ends with a user message (required by Gemini)
chat_ctx = ChatContext(
[
ChatMessage(type="message", role="system", content=[system_prompt]),
ChatMessage(type="message", role="user", content=["Proceed with the instructions."]),
]
)
# If your LLM wrapper expects a chat or contents object, pass chat_ctx/messages.
# Example using a typical LiveKit LLM call that returns a text reply:
response = await llm.chat(chat_ctx=chat_ctx) # or llm.generate_content(...)
# Extract the assistant text from the response (adjust per your SDK shape)
# Example: response.text or response.output[0].content[0]
if hasattr(response, "text") and response.text:
return response.text
# fallback for streaming/more complex shapes:
try:
parts = []
for msg in getattr(response, "output", []):
for part in msg.get("content", []):
parts.append(part)
return " ".join(parts).strip()
except Exception:
return ""
If you paste the exact llm call you use (for example generate_content_stream vs chat), I can tailor the extraction logic to that response shape.late-businessperson-58140
12/24/2025, 9:06 AMtall-airline-16756
12/24/2025, 9:06 AMuser message and call the Realtime API appropriately.
• If realtime audio: I’ll show how to include the final user text role when sending the realtime request alongside audio frames.
Which of these do you need?late-businessperson-58140
12/24/2025, 9:12 AMtall-airline-16756
12/24/2025, 9:12 AMsynthesize(text) -> bytes call you can play/send to LiveKit audio track. Adjust names to match your code.
# Example: send a single-turn text request to Gemini Realtime and synthesize with Sarvam TTS
from google import realtime
from google.genai import Modality
async def realtime_one_shot_with_sarvam(session, sarvam_tts, system_prompt: str, user_text: str):
# Build the RealtimeModel (your helper)
model = realtime.RealtimeModel(
model="gemini-2.5-flash-native-audio-preview-12-2025",
modalities=[Modality.AUDIO, Modality.TEXT],
api_key=settings.GOOGLE_API_KEY,
)
# Ensure the last message is from the user (required for single-turn requests)
contents = [
{"role": "system", "parts": [system_prompt]},
{"role": "user", "parts": [user_text]}, # <-- must be last
]
# Create a request for a single-shot/text generation on the realtime model
request = {
"input": {
"type": "text",
"contents": contents
},
# optional: any config like temperature, safety settings, etc.
"config": {
# example: "temperature": 0.7
}
}
# Call the RealtimeModel (non-streaming single-turn)
# Some SDKs use `generate` or `send_request` — match the call shape you have.
# Here we call a generic `generate`-like async method that returns a response object.
response = await model.generate(request) # replace with actual SDK call if different
# Extract text output. The exact path depends on SDK response shape.
# Common shapes: response.output[0].content[0] or response.text
out_text = ""
if hasattr(response, "text") and response.text:
out_text = response.text
else:
for out in getattr(response, "output", []):
for part in out.get("content", []):
out_text += part
out_text = out_text.strip()
# Pass the model text to Sarvam TTS (assuming async synthesize -> bytes)
tts_audio_bytes = await sarvam_tts.synthesize(out_text)
# Play/send the audio bytes into your LiveKit session (example helper)
await session.play_audio_bytes(tts_audio_bytes)
return out_text
Notes and adjustments:
• The critical fix for your previous 400 error is that contents ends with a user message. Confirm any code that mutates contents doesn’t append an assistant/model message before the generate call.
• If your Realtime SDK call is named differently (e.g., stream, create_stream, send_request), replace model.generate(request) with the correct method and adapt how you extract response.output.
• If you need real-time streaming audio (microphone → model → streamed text → TTS segments), I can provide a streaming example that sends audio frames, reads partial text responses, and feeds incremental TTS synthesis to Sarvam. Which streaming pattern do you want next? Continuous low-latency audio or segmented request/response turns?tall-airline-16756
12/27/2025, 5:49 PMtall-airline-16756
12/27/2025, 5:49 PMuser) and sends the model's text to Sarvam TTS, then plays the resulting audio into a LiveKit session. Adjust method names to match your SDK wrappers (I keep extraction robust to common response shapes).
from google import realtime
from google.genai import Modality
import settings # your settings module
async def realtime_one_shot_with_sarvam(session, sarvam_tts, system_prompt: str, user_text: str):
# 1) Build the RealtimeModel (as you provided)
model = realtime.RealtimeModel(
model="gemini-2.5-flash-native-audio-preview-12-2025",
modalities=[Modality.AUDIO, Modality.TEXT],
api_key=settings.GOOGLE_API_KEY,
)
# 2) Ensure contents ends with a user role (required by Gemini for single-turn)
contents = [
{"role": "system", "parts": [system_prompt]},
{"role": "user", "parts": [user_text]}, # <-- must be last
]
# 3) Build request for a single-shot text generation on the realtime model
request = {
"input": {
"type": "text",
"contents": contents,
},
# optional: config keys (temperature, safety, etc.)
"config": {}
}
# 4) Call the RealtimeModel. Replace `generate` with your actual SDK method if different.
response = await model.generate(request)
# 5) Extract text output (robust to common shapes)
out_text = ""
if hasattr(response, "text") and response.text:
out_text = response.text
else:
# Typical alternative shape: response.output -> list of { "content": [ ... ] }
for out in getattr(response, "output", []) or []:
for part in out.get("content", []) if isinstance(out.get("content", []), list) else []:
out_text += part
out_text = out_text.strip()
if not out_text:
# fallback: try `response.output_text` or similar fields if present
out_text = getattr(response, "output_text", "") or ""
# 6) Synthesize with Sarvam TTS (assumes async synthesize -> bytes)
tts_audio_bytes = await sarvam_tts.synthesize(out_text)
# 7) Send/play audio into LiveKit session.
# Replace this with your session's actual method: e.g., session.publish_audio_bytes, session.play_audio_bytes, etc.
await session.play_audio_bytes(tts_audio_bytes)
return out_text
If you want streaming (mic audio → realtime model → incremental text → chunked TTS playback), tell me and I’ll provide a streaming example that sends audio frames and plays incremental TTS as responses arrive.