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

  1. Upgrade litellm to pick up the latest realtime transformer.
  2. Log the failing message and skip the frame instead of tearing down the session.
  3. 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

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


AI-assisted analysis of BerriAI/litellm@6c2dcb801b (2026-08-15). Data as JSON: /api/errors/84c6f2b0d64bf100. Report an issue: GitHub.