BerriAI/litellm · error · Exception

{e.response.text}

Error message

{e.response.text}

What it means

Exception raised in BraintrustLogger.log_success_event when the sync POST to /project_logs/{project_id}/insert returns an HTTP error status — the message is literally just the response body with no context about status code or URL. This is the final telemetry-insert step, so it fires after the project id was already resolved, meaning auth was once valid but now fails or the event payload is rejected.

Solutions

  1. Match the embedded response text to a status: re-POST a minimal event with curl using the same key to see the raw status code
  2. Restart the proxy after rotating BRAINTRUST_API_KEY so cached ids/clients are refreshed
  3. If payloads are huge, trim what gets logged or split the event
  4. For 429/5xx bodies, add retry with backoff around telemetry instead of failing the logging path
Defensive patterns

Strategy: try-catch

Validate before calling

import httpx, os

def can_insert_events(project_id: str) -> bool:
    try:
        r = httpx.post(
            f"https://api.braintrust.dev/v1/project_logs/{project_id}/insert",
            headers={"Authorization": f"Bearer {os.getenv('BRAINTRUST_API_KEY','')}"},
            json={"events": []}, timeout=5)
        return r.status_code < 400 or r.status_code == 400  # 400 on empty is fine; 401/404 is the signal
    except httpx.RequestError:
        return False

Try / catch

try:
    logger.log_success_event(kwargs, response_obj, start_time, end_time)
except Exception as e:
    # message is the raw response body — map it: 401 => rotate key; 404 => stale project; 413 => trim payload
    log_warning(f"braintrust telemetry dropped: {e}")  # don't fail the user request

Prevention

When it happens

Trigger: A successful LLM call being logged when the Braintrust key expired or was revoked mid-process (401), the project_id is stale/deleted (404), the event JSON is malformed or exceeds size limits (400/413), or the org is rate limited (429).

Common situations: Keys rotated while long-running proxies stay up; very large logged payloads (big prompts/responses) tripping request-size limits; project deleted in the Braintrust UI while the proxy holds its cached id.

Understand the failure class

Background: 'Something went wrong' / 'Request failed (500)' / 'HTTP error! status: 404' — what failed HTTP requests actually mean and how to find the real cause — this error's family across 28 libraries.

Related errors


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

Appendix: source

Thrown at litellm/integrations/braintrust_logging.py:250

            else:
                request_data["output"] = output

            if metrics is not None:
                request_data["metrics"] = metrics

            try:
                print_verbose(
                    f"self.global_braintrust_sync_http_handler.post: {self.global_braintrust_sync_http_handler.post}"
                )
                self.global_braintrust_sync_http_handler.post(
                    url=f"{self.api_base}/project_logs/{project_id}/insert",
                    json={"events": [request_data]},
                    headers=self.headers,
                )
                if self.is_mock_mode:
                    print_verbose("[BRAINTRUST MOCK] Sync event successfully mocked")
            except httpx.HTTPStatusError as e:
                raise Exception(e.response.text)
        except Exception as e:
            raise e  # don't use verbose_logger.exception, if exception is raised

    async def async_log_success_event(self, kwargs, response_obj, start_time, end_time):
        verbose_logger.debug("REACHES BRAINTRUST SUCCESS")
        try:
            litellm_call_id: Final = kwargs.get("litellm_call_id")
            standard_logging_object: Final = kwargs.get("standard_logging_object", {})
            prompt: Final = {"messages": kwargs.get("messages")}
            output = None
            choices = []
            if response_obj is not None and (
                kwargs.get("call_type", None) == "embedding" or isinstance(response_obj, litellm.EmbeddingResponse)
            ):
                output = None
            elif response_obj is not None and isinstance(response_obj, litellm.ModelResponse):
                output = response_obj["choices"][0]["message"].json()
                choices = response_obj["choices"]

View on GitHub (pinned to 77b7c6c40c)