From f4b0d84a98d320b73d6f07da093d838f82b5c607 Mon Sep 17 00:00:00 2001 From: imviky-ctrl <272028991+imviky-ctrl@users.noreply.github.com> Date: Tue, 5 May 2026 15:02:51 +0530 Subject: [PATCH] fix(tickerr): fix TypeError, error-type fallback, and thread cap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - _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 --- litellm/integrations/tickerr.py | 97 ++++++++++++++++++++------------- 1 file changed, 59 insertions(+), 38 deletions(-) diff --git a/litellm/integrations/tickerr.py b/litellm/integrations/tickerr.py index 02b3ae973ac..9fb933bd937 100644 --- a/litellm/integrations/tickerr.py +++ b/litellm/integrations/tickerr.py @@ -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