From 845596063d257482652bbc56a89d437435d73972 Mon Sep 17 00:00:00 2001 From: yassin Date: Mon, 14 Sep 2026 09:25:47 +0000 Subject: [PATCH] fix(prometheus): label pre-call rate limit failures with the resolved api_provider Pre-call limiters reject before a deployment is attached to request_data, so the failure hook could not resolve api_provider for router aliases and emitted api_provider="None" on litellm_proxy_failed_requests_metric_total and litellm_proxy_total_requests_metric_total. Fall back to the provider the limiter already resolved onto RateLimitError.llm_provider, keeping request data as the first source and ignoring the proxy placeholder. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/integrations/prometheus.py | 14 +++- .../integrations/test_prometheus_labels.py | 71 +++++++++++++++++++ 2 files changed, 84 insertions(+), 1 deletion(-) diff --git a/litellm/integrations/prometheus.py b/litellm/integrations/prometheus.py index 2528f07f92c..2ba5d471e75 100644 --- a/litellm/integrations/prometheus.py +++ b/litellm/integrations/prometheus.py @@ -41,6 +41,7 @@ from litellm.proxy._types import ( LiteLLM_UserTable, UserAPIKeyAuth, ) +from litellm.proxy.hooks.rate_limiter_utils import PROXY_LLM_PROVIDER_FALLBACK from litellm.repositories.base_repository import BaseRepository from litellm.repositories.organization_repository import OrganizationRepository from litellm.repositories.team_repository import TeamRepository @@ -2581,6 +2582,15 @@ class PrometheusLogger(CustomLogger): ) return None + @staticmethod + def _extract_api_provider_from_exception(exception: Exception) -> str | None: + if not isinstance(exception, litellm.exceptions.RateLimitError): + return None + llm_provider: Final = exception.llm_provider + if not llm_provider or llm_provider == PROXY_LLM_PROVIDER_FALLBACK: + return None + return llm_provider + async def async_post_call_failure_hook( self, request_data: dict, @@ -2616,7 +2626,9 @@ class PrometheusLogger(CustomLogger): _metadata: Final = request_data.get("metadata", {}) or {} model_id: Final = _metadata.get("model_info", {}).get("id") or request_data.get("model_info", {}).get("id") rate_limit_category, rate_limit_type = self._extract_rate_limit_labels(original_exception) - api_provider: Final = self._extract_api_provider_from_request_data(request_data) + api_provider: Final = self._extract_api_provider_from_request_data( + request_data + ) or self._extract_api_provider_from_exception(original_exception) enum_values: Final = UserAPIKeyLabelValues( end_user=user_api_key_dict.end_user_id, user=user_api_key_dict.user_id, diff --git a/tests/test_litellm/integrations/test_prometheus_labels.py b/tests/test_litellm/integrations/test_prometheus_labels.py index 859cdd30c11..200f8e65add 100644 --- a/tests/test_litellm/integrations/test_prometheus_labels.py +++ b/tests/test_litellm/integrations/test_prometheus_labels.py @@ -716,6 +716,77 @@ async def test_failure_hook_emits_api_provider_value_on_failed_requests_metric() _clear_prometheus_registry() +async def _failed_requests_api_provider_labels( + request_data: dict[str, object], + original_exception: Exception, +) -> list[str]: + from litellm.integrations.prometheus import PrometheusLogger + from litellm.proxy._types import UserAPIKeyAuth + + _clear_prometheus_registry() + try: + await PrometheusLogger().async_post_call_failure_hook( + request_data=request_data, + original_exception=original_exception, + user_api_key_dict=UserAPIKeyAuth(token="tok"), + ) + return [ + s.labels.get("api_provider") + for s in _collected_samples("litellm_proxy_failed_requests_metric_total") + ] + finally: + _clear_prometheus_registry() + + +@pytest.mark.asyncio +async def test_failure_hook_emits_api_provider_from_pre_call_rate_limit_error_for_router_alias(): + """ + Pre-call limiters reject before a deployment lands on request_data and a + router alias cannot be inferred from its name, so the provider the limiter + resolved onto the exception is the only source for the label. + """ + from litellm.exceptions import RateLimitType + from litellm.proxy.common_utils.proxy_rate_limit_error import ProxyRateLimitError + + err = ProxyRateLimitError( + detail={"error": "rpm exceeded"}, + rate_limit_type=RateLimitType.REQUESTS, + model="openai/gpt-5.4-mini", + llm_provider="openai", + ) + + assert await _failed_requests_api_provider_labels( + {"model": "team-chat-model", "metadata": {}}, err + ) == ["openai"] + + +@pytest.mark.asyncio +async def test_failure_hook_leaves_api_provider_unset_when_rate_limiter_could_not_resolve_provider(): + from litellm.proxy.common_utils.proxy_rate_limit_error import ProxyRateLimitError + + err = ProxyRateLimitError(detail={"error": "rpm exceeded"}, model="unknown-alias") + + assert await _failed_requests_api_provider_labels( + {"model": "unknown-alias", "metadata": {}}, err + ) == ["None"] + + +@pytest.mark.asyncio +async def test_failure_hook_prefers_request_data_provider_over_exception_provider(): + from litellm.exceptions import RateLimitError + + err = RateLimitError(message="upstream 429", llm_provider="openai", model="gpt-4o") + + assert await _failed_requests_api_provider_labels( + { + "model": "gpt-4o", + "metadata": {}, + "litellm_params": {"custom_llm_provider": "azure"}, + }, + err, + ) == ["azure"] + + if __name__ == "__main__": test_user_email_in_required_metrics() test_user_email_label_exists()