BerriAI/litellm · error · ValueError
Unexpected model turn event, no 'parts' key: {model_turn}
Error message
Unexpected model turn event, no 'parts' key: {model_turn} What it means
Raised in map_model_turn_event when the modelTurn dict has no 'parts' key at all. The method assumes every modelTurn carries a parts list; a malformed or schema-changed server message (empty turn, new envelope shape) hits the trailing ValueError that echoes the whole message.
Source
Thrown at litellm/llms/gemini/realtime/transformation.py:165
Returns either:
- response.text.delta - model_turn: {"parts": [{"text": "..."}]}
- response.audio.delta - model_turn: {"parts": [{"inlineData": {"mimeType": "audio/pcm", "data": "..."}}]}
Assumes parts is a single element list.
"""
if "parts" in model_turn:
parts: Final = model_turn["parts"]
if len(parts) != 1:
verbose_logger.warning("Realtime: Expected 1 part, got %s for Gemini model turn event.", len(parts))
part: Final = parts[0]
if "text" in part:
return OpenAIRealtimeEventTypes.RESPONSE_TEXT_DELTA
elif "inlineData" in part:
return OpenAIRealtimeEventTypes.RESPONSE_AUDIO_DELTA
else:
raise ValueError(f"Unexpected part type: {part}")
raise ValueError(f"Unexpected model turn event, no 'parts' key: {model_turn}")
def map_generation_complete_event(self, delta_type: ALL_DELTA_TYPES | None) -> OpenAIRealtimeEventTypes:
if delta_type == "text":
return OpenAIRealtimeEventTypes.RESPONSE_TEXT_DONE
elif delta_type == "audio":
return OpenAIRealtimeEventTypes.RESPONSE_AUDIO_DONE
else:
raise ValueError(f"Unexpected delta type: {delta_type}")
def get_audio_mime_type(self, input_audio_format: str = "pcm16"):
mime_types: Final = {
"pcm16": "audio/pcm;rate=24000",
"g711_ulaw": "audio/pcmu",
"g711_alaw": "audio/pcma",
}
return mime_types.get(input_audio_format, "application/octet-stream")
View on GitHub (pinned to 6c2dcb801b)
Solutions
- Upgrade litellm to pick up the latest realtime transformer.
- Log the failing message and skip the frame instead of tearing down the session.
- If writing tests/replays, feed complete, real captured modelTurn payloads.
Example fix
# before
event = config.map_model_turn_event(model_turn)
# after
if 'parts' not in model_turn:
logger.warning('modelTurn without parts, skipping: %r', model_turn)
return None
event = config.map_model_turn_event(model_turn)
Defensive patterns
Strategy: type-guard
Validate before calling
def is_valid_model_turn(turn: dict) -> bool:
return isinstance(turn, dict) and isinstance(turn.get("parts"), list) and len(turn["parts"]) > 0 Type guard
def is_valid_model_turn(turn: object) -> bool:
return (
isinstance(turn, dict)
and isinstance(turn.get("parts"), list)
and len(turn["parts"]) > 0
) Try / catch
try:
event = config.map_model_turn_event(model_turn)
except ValueError as e:
if "no 'parts' key" in str(e):
logger.warning("Skipping parts-less modelTurn: %s", e)
continue
raise Prevention
- Validate frame shape before mapping in replay/testing harnesses.
- Upgrade litellm when Gemini changes the realtime envelope.
- Log skipped frames with counts to detect systematic schema drift early.
When it happens
Trigger: Gemini realtime sends a modelTurn containing only metadata fields (e.g. a keep-alive or turn-heartbeat) or an API schema change renames/relocates 'parts'; also hand-crafted test messages replayed into the transformer.
Common situations: Replaying captured frames against a newer litellm; partial JSON deserialization upstream producing incomplete dicts; Gemini API evolution outpacing the installed litellm.
Related errors
- Unexpected part type: {part}
- Unexpected delta type: {delta_type}
- Error transforming content delta events: {e}, got message: {
- Unknown openai event: {key}, value: {value}
- Unknown openai event: {openai_event}
AI-assisted analysis of BerriAI/litellm@6c2dcb801b (2026-08-15).
Data as JSON: /api/errors/84c6f2b0d64bf100.
Report an issue: GitHub.