BerriAI/litellm · error · APIError

LiteLLM: provider returned a response with no 'choices'. Raw

Error message

LiteLLM: provider returned a response with no 'choices'. Raw keys: {list(response_object.keys())}

What it means

While converting a provider dict into a streaming ModelResponse, litellm found the response_object had no non-empty 'choices' key and raised APIError(500) with the raw key list included. This surfaces when a provider (or cache/middleware) returns a well-formed JSON dict that is not a chat completion — e.g. an error payload, moderation payload, or empty object. The message intentionally echoes response_object.keys() so you can identify what the payload actually was.

Source

Thrown at litellm/litellm_core_utils/llm_response_utils/convert_dict_to_response.py:185

        ModelResponse: The converted streaming response object.

    Returns:
        None
    """
    if response_object is None:
        raise Exception("Error in response object format")

    model_response_object: Final = ModelResponseStream()

    if model_response_object is None:
        raise Exception("Error in response creating model response object")

    choice_list: Final[list[StreamingChoices]] = []

    if not response_object.get("choices"):
        from litellm.exceptions import APIError

        raise APIError(
            status_code=500,
            message=(
                f"LiteLLM: provider returned a response with no 'choices'. Raw keys: {list(response_object.keys())}"
            ),
            llm_provider="",
            model="",
        )

    for idx, choice in enumerate(response_object["choices"]):
        if (
            choice["message"].get("tool_calls", None) is not None
            and isinstance(choice["message"]["tool_calls"], list)
            and len(choice["message"]["tool_calls"]) > 0
            and isinstance(choice["message"]["tool_calls"][0], dict)
        ):
            pydantic_tool_calls = []
            for index, t in enumerate(choice["message"]["tool_calls"]):
                if "index" not in t:

View on GitHub (pinned to 6c2dcb801b)

Solutions

  1. Inspect the 'Raw keys' in the message to identify what the payload actually is (error body, empty dict, other response type).
  2. If a custom provider/handler produced it, ensure its translation layer emits {'choices': [...]} for chat completions.
  3. Check provider status pages and retry — some providers emit non-choices JSON on transient faults.
  4. If caching is enabled, verify cache keys don't collide across response types and cached values are chat-shaped.

Example fix

// before (mock/edge server)
return JsonResponse({'id': 'x', 'output': 'hi'})

# after
return JsonResponse({'id': 'x', 'choices': [{'index': 0, 'message': {'role': 'assistant', 'content': 'hi'}, 'finish_reason': 'stop'}]})
Defensive patterns

Strategy: try-catch

Validate before calling

def provider_dict_is_chat_completion(d: dict) -> bool:
    return bool(d.get('choices'))

Try / catch

from litellm.exceptions import APIError
try:
    stream = litellm.completion(**params, stream=True)
except APIError as e:
    if "no 'choices'" in str(e):
        logging.error('provider sent non-completion payload: %s', e.message)
    raise

Prevention

When it happens

Trigger: A provider returns {'error': {...}} with HTTP 200; a cached embedding response is replayed into a chat stream path; middleware rewrites the response body and drops 'choices'; Azure/OpenAI-compatible gateways return empty {} on partial failures.

Common situations: Custom API gateways or mock servers that don't include 'choices'; provider outages returning JSON error bodies without error status codes; response-translation bugs in custom handlers; cache collisions between different response types under the same key.

Related errors


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