BerriAI/litellm · warning · Exception
DataDogLLMObs: Unexpected response - status_code
Error message
DataDogLLMObs: Unexpected response - status_code: {response.status_code}, text: {response.text} What it means
DataDogLLMObsLogger.flush path: async_send_batch POSTs the queued LLM-obs spans to the /api/intake/llm-obs/v1/trace/spans endpoint and requires HTTP 202. Unlike the sync datadog.py path there is no raise_for_status() first, so every non-202 — including 400/401/403/429/5xx — surfaces as this generic Exception. It is caught by the trailing `except Exception` and only logged; importantly log_queue.clear() only runs on success, so failed batches stay queued and are retried on the next flush.
Solutions
- Turn on verbose logging and read the logged status_code/text — 401/403 means key, 404 means site/URL, 413 means batch too large
- Verify DD_API_KEY and DD_SITE, or in agent mode verify LITELLM_DD_AGENT_HOST and LITELLM_DD_LLM_OBS_PORT point at the trace agent (default 8126)
- Test the intake directly with curl POST expecting 202
- If 413/429, reduce batch size / flush interval so payloads shrink
Example fix
# before export LITELLM_DD_AGENT_HOST=localhost # but trace agent not on 8126 -> persistent non-202 # after export LITELLM_DD_AGENT_HOST=localhost export LITELLM_DD_LLM_OBS_PORT=8126 # match the actual trace-agent port # or direct API: export DD_API_KEY=<key> DD_SITE=us5.datadoghq.com
Defensive patterns
Strategy: validation
Validate before calling
import os, httpx
def llm_obs_intake_reachable() -> bool:
if os.getenv("LITELLM_DD_AGENT_HOST"):
host = os.getenv("LITELLM_DD_AGENT_HOST")
port = os.getenv("LITELLM_DD_LLM_OBS_PORT", "8126")
url = f"http://{host}:{port}/api/intake/llm-obs/v1/trace/spans"
else:
if not (os.getenv("DD_API_KEY") and os.getenv("DD_SITE")):
return False
url = f"https://api.{os.getenv('DD_SITE')}/api/intake/llm-obs/v1/trace/spans"
try:
r = httpx.post(url, headers={"DD-API-KEY": os.getenv("DD_API_KEY", "")}, json={"data": {}})
return r.status_code in (202, 400) # 400 = reachable+authed but empty body
except httpx.HTTPError:
return False Try / catch
// The flush loop already catches and logs; callers add nothing. // Guard instead against unbounded retry: watch the queue on failure.
Prevention
- Health-check the llm-obs intake (202 path) from the same network as the proxy
- Keep batch_size modest so 413/429 responses can't wedge the queue
- Alert when 'DataDogLLMObs: Error sending batch' appears repeatedly — the queue retries forever until it succeeds
When it happens
Trigger: Invalid or revoked DD_API_KEY (403), wrong DD_SITE (404/DNS issues), oversized batch hitting 413, malformed span payload rejected with 400, rate limiting (429), or an agent-mode URL pointing at a port running the wrong agent (logs agent instead of trace agent).
Common situations: Key rotated in Datadog but not in the proxy deployment; LITELLM_DD_LLM_OBS_PORT left default 8126 while the trace agent listens elsewhere; large batch_size with big payloads; intermittent retries spamming logs because the queue never drains.
Related errors
- Response from datadog API status_code
- DataDogLLMObs: standard_logging_object is not set
- DD_API_KEY is not set, set 'DD_API_KEY=<'
- DD_SITE is not set, set 'DD_SITE=<>', example sit =…
- DD_SITE is not set, set 'DD_SITE=<>', example site =…
AI-assisted analysis of BerriAI/litellm@77b7c6c40c (2026-08-18).
Data as JSON: /api/errors/b86ede518594348b.
Report an issue: GitHub.
Appendix: source
Thrown at litellm/integrations/datadog/datadog_llm_obs.py:205
try:
verbose_logger.debug("payload %s", safe_dumps(payload))
except Exception as debug_error:
verbose_logger.debug("payload serialization failed: %s", str(debug_error))
json_payload: Final = safe_dumps(payload)
headers: Final = {"Content-Type": "application/json"}
if self.DD_API_KEY:
headers["DD-API-KEY"] = self.DD_API_KEY
response: Final = await self.async_client.post(
url=self.intake_url,
content=json_payload,
headers=headers,
)
if response.status_code != 202:
raise Exception(
f"DataDogLLMObs: Unexpected response - status_code: {response.status_code}, text: {response.text}"
)
if self.is_mock_mode:
verbose_logger.debug("[DATADOG MOCK] Batch of %s events successfully mocked", len(self.log_queue))
else:
verbose_logger.debug("DataDogLLMObs: Successfully sent batch - status_code: %s", response.status_code)
self.log_queue.clear()
except httpx.HTTPStatusError as e:
verbose_logger.exception("DataDogLLMObs: Error sending batch - %s", e.response.text)
except Exception as e:
verbose_logger.exception("DataDogLLMObs: Error sending batch - %s", e)
def create_llm_obs_payload(self, kwargs: dict, start_time: datetime, end_time: datetime) -> LLMObsPayload:
standard_logging_payload: Final[StandardLoggingPayload | None] = kwargs.get("standard_logging_object")
if standard_logging_payload is None:
raise Exception("DataDogLLMObs: standard_logging_object is not set")
View on GitHub (pinned to 77b7c6c40c)