mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-03 02:22:24 +00:00
fix(tickerr): fix TypeError, error-type fallback, and thread cap
- _latency_ms(): handle both datetime objects and floats — LiteLLM passes datetime, not float, causing TypeError on every invocation - _ERROR_TYPE_MAP fallback: removed default "overloaded" for unmapped codes (400, 404, 502, etc.) — now returns None for unrecognised codes - _fire_and_forget(): added semaphore cap (_MAX_INFLIGHT=5) to prevent unbounded thread spawning during burst failures - Removed dead is_resolution parameter from _report() — only called from failure hooks so it was always False
This commit is contained in:
parent
64aab2de56
commit
f4b0d84a98
1 changed files with 59 additions and 38 deletions
|
|
@ -18,40 +18,46 @@ from __future__ import annotations
|
|||
import os
|
||||
import re
|
||||
import threading
|
||||
from typing import Any, Dict, Optional
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, Optional, Union
|
||||
|
||||
from litellm.integrations.custom_logger import CustomLogger
|
||||
|
||||
_REPORT_URL = "https://tickerr.ai/api/v1/report"
|
||||
_UA = "litellm-tickerr/1.0"
|
||||
|
||||
# Cap concurrent in-flight reports to avoid thread exhaustion on burst failures
|
||||
_MAX_INFLIGHT = 5
|
||||
_inflight = threading.Semaphore(_MAX_INFLIGHT)
|
||||
|
||||
# Map litellm custom_llm_provider → Tickerr provider slug
|
||||
_PROVIDER_MAP: Dict[str, str] = {
|
||||
"openai": "openai",
|
||||
"anthropic": "anthropic",
|
||||
"google": "google",
|
||||
"vertex_ai": "google",
|
||||
"gemini": "google",
|
||||
"cohere": "cohere",
|
||||
"mistral": "mistral",
|
||||
"groq": "groq",
|
||||
"together_ai": "together",
|
||||
"huggingface": "huggingface",
|
||||
"replicate": "replicate",
|
||||
"deepinfra": "deepinfra",
|
||||
"perplexity": "perplexity",
|
||||
"fireworks_ai": "fireworks",
|
||||
"openrouter": "openrouter",
|
||||
"azure": "azure",
|
||||
"bedrock": "aws",
|
||||
"ai21": "ai21",
|
||||
"cerebras": "cerebras",
|
||||
"xai": "xai",
|
||||
"deepseek": "deepseek",
|
||||
"ollama": "ollama",
|
||||
"nlp_cloud": "nlp_cloud",
|
||||
"openai": "openai",
|
||||
"anthropic": "anthropic",
|
||||
"google": "google",
|
||||
"vertex_ai": "google",
|
||||
"gemini": "google",
|
||||
"cohere": "cohere",
|
||||
"mistral": "mistral",
|
||||
"groq": "groq",
|
||||
"together_ai": "together",
|
||||
"huggingface": "huggingface",
|
||||
"replicate": "replicate",
|
||||
"deepinfra": "deepinfra",
|
||||
"perplexity": "perplexity",
|
||||
"fireworks_ai": "fireworks",
|
||||
"openrouter": "openrouter",
|
||||
"azure": "azure",
|
||||
"bedrock": "aws",
|
||||
"ai21": "ai21",
|
||||
"cerebras": "cerebras",
|
||||
"xai": "xai",
|
||||
"deepseek": "deepseek",
|
||||
"ollama": "ollama",
|
||||
"nlp_cloud": "nlp_cloud",
|
||||
}
|
||||
|
||||
# Only map codes that unambiguously indicate the specific error type
|
||||
_ERROR_TYPE_MAP: Dict[int, str] = {
|
||||
429: "rate_limit",
|
||||
529: "overloaded",
|
||||
|
|
@ -105,8 +111,23 @@ def _extract_status_code(exception: Optional[BaseException]) -> Optional[int]:
|
|||
return None
|
||||
|
||||
|
||||
def _latency_ms(
|
||||
start_time: Union[datetime, float], end_time: Union[datetime, float]
|
||||
) -> int:
|
||||
"""Compute elapsed ms whether LiteLLM passes datetime objects or floats."""
|
||||
if isinstance(start_time, datetime) and isinstance(end_time, datetime):
|
||||
return round((end_time - start_time).total_seconds() * 1000)
|
||||
return round((float(end_time) - float(start_time)) * 1000) # type: ignore[arg-type]
|
||||
|
||||
|
||||
def _fire_and_forget(payload: Dict[str, Any]) -> None:
|
||||
"""POST to Tickerr in a daemon thread — never blocks the caller."""
|
||||
"""POST to Tickerr in a daemon thread — never blocks the caller.
|
||||
|
||||
A semaphore caps concurrent in-flight threads so a burst of failures
|
||||
(e.g. 100 errors/s) cannot exhaust the thread pool.
|
||||
"""
|
||||
if not _inflight.acquire(blocking=False):
|
||||
return # already at max concurrent reports — drop silently
|
||||
|
||||
def _send() -> None:
|
||||
try:
|
||||
|
|
@ -124,6 +145,8 @@ def _fire_and_forget(payload: Dict[str, Any]) -> None:
|
|||
pass
|
||||
except Exception:
|
||||
pass # never crash the caller
|
||||
finally:
|
||||
_inflight.release()
|
||||
|
||||
t = threading.Thread(target=_send, daemon=True)
|
||||
t.start()
|
||||
|
|
@ -152,10 +175,10 @@ class TickerrLogger(CustomLogger):
|
|||
self,
|
||||
kwargs: Dict[str, Any],
|
||||
response_obj: Any,
|
||||
start_time: float,
|
||||
end_time: float,
|
||||
start_time: Union[datetime, float],
|
||||
end_time: Union[datetime, float],
|
||||
) -> None:
|
||||
self._report(kwargs, start_time, end_time, is_resolution=False)
|
||||
self._report(kwargs, start_time, end_time)
|
||||
|
||||
# ── async ─────────────────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -163,27 +186,26 @@ class TickerrLogger(CustomLogger):
|
|||
self,
|
||||
kwargs: Dict[str, Any],
|
||||
response_obj: Any,
|
||||
start_time: float,
|
||||
end_time: float,
|
||||
start_time: Union[datetime, float],
|
||||
end_time: Union[datetime, float],
|
||||
) -> None:
|
||||
self._report(kwargs, start_time, end_time, is_resolution=False)
|
||||
self._report(kwargs, start_time, end_time)
|
||||
|
||||
# ── internal ──────────────────────────────────────────────────────────────
|
||||
|
||||
def _report(
|
||||
self,
|
||||
kwargs: Dict[str, Any],
|
||||
start_time: float,
|
||||
end_time: float,
|
||||
is_resolution: bool,
|
||||
start_time: Union[datetime, float],
|
||||
end_time: Union[datetime, float],
|
||||
) -> None:
|
||||
model: str = kwargs.get("model", "") or ""
|
||||
exception: Optional[BaseException] = kwargs.get("exception")
|
||||
latency_ms = round((end_time - start_time) * 1000)
|
||||
|
||||
provider = _normalize_provider(model, kwargs)
|
||||
status_code = _extract_status_code(exception)
|
||||
error_type = _ERROR_TYPE_MAP.get(status_code, "overloaded") if status_code else None
|
||||
# Only set error_type for codes we can classify with confidence
|
||||
error_type: Optional[str] = _ERROR_TYPE_MAP.get(status_code) if status_code is not None else None
|
||||
|
||||
# Strip provider prefix: "anthropic/claude-3-5-haiku" → "claude-3-5-haiku"
|
||||
model_clean = model.split("/", 1)[-1] if "/" in model else model
|
||||
|
|
@ -191,8 +213,7 @@ class TickerrLogger(CustomLogger):
|
|||
payload: Dict[str, Any] = {
|
||||
"provider": provider,
|
||||
"model": model_clean or None,
|
||||
"is_resolution": is_resolution,
|
||||
"latency_ms": latency_ms,
|
||||
"latency_ms": _latency_ms(start_time, end_time),
|
||||
}
|
||||
if status_code is not None:
|
||||
payload["error_code"] = status_code
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue