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
- Reproduce with curl: POST a sample {request, response} envelope to the configured URL and inspect the top-level JSON type of the reply
- Correct RUBRIK_WEBHOOK_URL to the moderation endpoint that answers with an object (or empty object)
- Remove/adjust intermediaries that convert the body into a list or wrap it differently
- 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
- Smoke-test the webhook with a sample envelope at deploy time and assert the reply is a JSON object
- Point RUBRIK_WEBHOOK_URL only at the moderation endpoint, not list/collection APIs
- Keep intermediaries from reshaping response bodies on the webhook path
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
- Cannot normalize tool_call of type
- Rubrik webhook URL not configured. Set RUBRIK_WEBHOOK_URL…
- {blocked.explanation}
- {refusal}
- AgentCore: Failed to read/parse JSON response body
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)