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:
imviky-ctrl 2026-05-05 15:02:51 +05:30
parent 64aab2de56
commit f4b0d84a98

View file

@ -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