diff --git a/litellm/proxy/common_request_processing.py b/litellm/proxy/common_request_processing.py index dc8bc961296..37cc3fab720 100644 --- a/litellm/proxy/common_request_processing.py +++ b/litellm/proxy/common_request_processing.py @@ -2605,6 +2605,13 @@ class ProxyBaseLLMRequestProcessing: ): """Raises ProxyException (OpenAI API compatible) if an exception is raised""" _log_llm_api_exception(e) + _litellm_logging_obj: Optional[LiteLLMLoggingObj] = self.data.get("litellm_logging_obj", None) + + # Attempt to get model_id from logging object + # + # Note: We check the direct model_info path first (not nested in metadata) because that's where the router sets it. + # The nested metadata path is only a fallback for cases where model_info wasn't set at the top level. + model_id = self.maybe_get_model_id(_litellm_logging_obj) # Allow callbacks to transform the error response transformed_exception = await proxy_logging_obj.post_call_failure_hook( user_api_key_dict=user_api_key_dict, @@ -2624,13 +2631,6 @@ class ProxyBaseLLMRequestProcessing: timeout = getattr( e, "timeout", None ) # returns the timeout set by the wrapper. Used for testing if model-specific timeout are set correctly - _litellm_logging_obj: Optional[LiteLLMLoggingObj] = self.data.get("litellm_logging_obj", None) - - # Attempt to get model_id from logging object - # - # Note: We check the direct model_info path first (not nested in metadata) because that's where the router sets it. - # The nested metadata path is only a fallback for cases where model_info wasn't set at the top level. - model_id = self.maybe_get_model_id(_litellm_logging_obj) custom_headers = ProxyBaseLLMRequestProcessing.get_custom_headers( user_api_key_dict=user_api_key_dict, diff --git a/tests/test_litellm/proxy/test_common_request_processing.py b/tests/test_litellm/proxy/test_common_request_processing.py index 3bb84e095a0..8a59478be31 100644 --- a/tests/test_litellm/proxy/test_common_request_processing.py +++ b/tests/test_litellm/proxy/test_common_request_processing.py @@ -2948,6 +2948,47 @@ class TestHandleLLMApiExceptionRetryAfter: assert proxy_exc.headers["x-custom"] == "1" +class TestHandleLLMApiExceptionModelIdHeader: + """post_call_failure_hook pops litellm_logging_obj off request_data, so the + error handler must resolve model_id before invoking it or x-litellm-model-id + is dropped from error responses (the reported embeddings 429 bug).""" + + class _FakeLoggingObj: + def __init__(self, model_id: str): + self.litellm_params = {"model_info": {"id": model_id}} + self.litellm_call_id = "call-123" + self.kwargs: dict = {} + + async def test_model_id_survives_failure_hook_that_pops_logging_obj(self): + from litellm.proxy._types import ProxyException, UserAPIKeyAuth + + data = { + "model": "text-embedding-ada-002", + "litellm_call_id": "call-123", + "litellm_logging_obj": self._FakeLoggingObj("deployment-xyz"), + } + processor = ProxyBaseLLMRequestProcessing(data=data) + + async def _popping_failure_hook(request_data, **kwargs): + request_data.pop("litellm_logging_obj", None) + return None + + proxy_logging_obj = MagicMock() + proxy_logging_obj.post_call_failure_hook = AsyncMock( + side_effect=_popping_failure_hook + ) + proxy_logging_obj.post_call_response_headers_hook = AsyncMock(return_value={}) + + with pytest.raises(ProxyException) as exc_info: + await processor._handle_llm_api_exception( + e=ValueError("429 rate limit"), + user_api_key_dict=UserAPIKeyAuth(api_key="sk-test"), + proxy_logging_obj=proxy_logging_obj, + ) + + assert exc_info.value.headers["x-litellm-model-id"] == "deployment-xyz" + + class TestAsyncStreamingDataGeneratorFastPath: """Fast/slow path branching in async_streaming_data_generator."""