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>
This commit is contained in:
yassin 2026-09-14 09:25:47 +00:00
parent b94b8bca21
commit 845596063d
2 changed files with 84 additions and 1 deletions

View file

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

View file

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