BerriAI/litellm · error · TypeError

returned non-dict JSON ( ); expected OpenAI chat completion…

Error message

{service_name} returned non-dict JSON ({type(result).__name__}); expected OpenAI chat completion shape or empty object.

What it means

The shared moderation POST (_post_to_endpoint style) does raise_for_status() and then requires the parsed JSON body to be a dict — the expected shape is an OpenAI-chat-completion-like object or {}; any other top-level JSON type (list, string, number) raises TypeError naming the service and the actual type. A 2xx with a JSON array therefore fails here even though the HTTP call succeeded.

Solutions

  1. Reproduce with curl: POST a sample {request, response} envelope to the configured URL and inspect the top-level JSON type of the reply
  2. Correct RUBRIK_WEBHOOK_URL to the moderation endpoint that answers with an object (or empty object)
  3. Remove/adjust intermediaries that convert the body into a list or wrap it differently
  4. Confirm the Rubrik event API version matches what this litellm integration expects (upgrade litellm if needed)

Example fix

# before
# RUBRIK_WEBHOOK_URL=https://rubrik.example.com/api/v1/events  (returns a JSON list)
# -> TypeError: rubrik returned non-dict JSON (list); expected OpenAI chat completion shape

# after
# RUBRIK_WEBHOOK_URL=https://rubrik.example.com/api/v1/moderation  (returns an object)
result = http_response.json()
assert isinstance(result, dict), f'unexpected JSON type: {type(result).__name__}'
Defensive patterns

Strategy: type-guard

Validate before calling

import httpx

resp = httpx.post(webhook_url, json=sample_envelope, headers=headers, timeout=10)
assert resp.status_code < 400, f'webhook unhealthy: {resp.status_code}'
body = resp.json()
assert isinstance(body, dict), f'webhook must return a JSON object, got {type(body).__name__}'

Type guard

def parse_moderation_json(text: str) -> dict:
    import json
    result = json.loads(text)
    if not isinstance(result, dict):
        raise TypeError(f'expected JSON object from moderation service, got {type(result).__name__}')
    return result

Try / catch

try:
    result = await client.post(endpoint, json=payload, headers=headers)
except TypeError as e:
    if 'non-dict JSON' in str(e):
        logger.error('webhook URL likely wrong (returns %s); check RUBRIK_WEBHOOK_URL', str(e))
    raise

Prevention

When it happens

Trigger: RUBRIK_WEBHOOK_URL pointing at an endpoint that returns a JSON list (a collection/list API instead of the moderation endpoint); an intermediary (ingress, BFF) reshaping bodies; a Rubrik event API version whose success responses are not objects.

Common situations: Wrong path in the webhook URL (copied from a browser list view); gateway transformations on the response path; mismatch between the configured URL and the API version the guardrail expects.

Related errors


AI-assisted analysis of BerriAI/litellm@77b7c6c40c (2026-08-18). Data as JSON: /api/errors/8cb9c8ddd713d685. Report an issue: GitHub.

Appendix: source

Thrown at litellm/integrations/rubrik.py:1032

    # -- Webhook services ------------------------------------------------------

    async def _post_json(self, endpoint: str, payload: Mapping[str, object], service_name: str) -> Mapping[str, Any]:
        """POST ``payload`` to a Rubrik webhook and return its dict response.

        Raises:
            Exception: If the service is unavailable or returns an error.
            TypeError: If the response JSON is not a dict.
        """
        verbose_logger.debug("Sending request to %s: %s", service_name, endpoint)
        http_response: Final = await self.moderation_client.post(
            endpoint,
            json=payload,
            headers=self._headers,
        )
        http_response.raise_for_status()
        result: Final[object] = http_response.json()
        if not isinstance(result, dict):
            raise TypeError(
                f"{service_name} returned non-dict JSON "
                f"({type(result).__name__}); expected OpenAI chat completion "
                "shape or empty object."
            )
        return result

    async def _post_to_response_moderation_endpoint(
        self,
        response_data: Mapping[str, object],
        request_data: Mapping[str, object],
    ) -> Mapping[str, Any]:
        """Post the ``{request, response}`` envelope to the after_completion
        webhook and return its (possibly rewritten) response.

        Args:
            response_data: The OpenAI-formatted response payload to send.
            request_data: Original LLM request data to include alongside
                the response for additional context. Empty dict if unavailable.

View on GitHub (pinned to 77b7c6c40c)