This commit is contained in:
devin-ai-integration[bot] 2026-08-27 20:09:21 -05:00 committed by GitHub
commit 385db7f051
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 44 additions and 5 deletions

View file

@ -3165,6 +3165,8 @@ class ProxyBaseLLMRequestProcessing:
):
"""Raises ProxyException (OpenAI API compatible) if an exception is raised"""
_log_llm_api_exception(e)
# Capture before post_call_failure_hook pops it from the request data
_litellm_logging_obj: Final[LiteLLMLoggingObj | None] = self.data.get("litellm_logging_obj", None)
# Allow callbacks to transform the error response
transformed_exception: Final = await proxy_logging_obj.post_call_failure_hook(
user_api_key_dict=user_api_key_dict,
@ -3184,7 +3186,6 @@ class ProxyBaseLLMRequestProcessing:
timeout: Final = getattr(
e, "timeout", None
) # returns the timeout set by the wrapper. Used for testing if model-specific timeout are set correctly
_litellm_logging_obj: Final[LiteLLMLoggingObj | None] = self.data.get("litellm_logging_obj", None)
# Attempt to get model_id from logging object
#
@ -3789,10 +3790,12 @@ class ProxyBaseLLMRequestProcessing:
model_info = metadata.get("model_info") or {}
model_id = model_info.get("id", None)
# 3. Final fallback to self.data["litellm_metadata"] (for routes like /v1/responses that populate data before error)
# 3. Final fallback to request metadata (the router stamps the selected
# deployment's model_info there; "litellm_metadata" covers routes like /v1/responses)
if not model_id:
litellm_metadata: Final = self.data.get("litellm_metadata", {}) or {}
model_info = litellm_metadata.get("model_info", {}) or {}
model_id = model_info.get("id", None)
fallback_model_infos: Final = (
(self.data.get(key) or {}).get("model_info") or {} for key in ("metadata", "litellm_metadata")
)
model_id = next((info.get("id") for info in fallback_model_infos if info.get("id")), None)
return model_id

View file

@ -2855,6 +2855,42 @@ class TestHandleLLMApiExceptionDictDetail:
proxy_exc = await self._invoke(exc)
assert proxy_exc.code == "500"
async def test_error_headers_keep_model_id_after_failure_hook_pops_logging_obj(self):
"""post_call_failure_hook pops litellm_logging_obj from the request data;
the exception handler must capture it first so error responses (e.g. 429s
on /v1/embeddings) still carry x-litellm-model-id. Regression for the
embeddings deployment-id report (2026-08-19)."""
from litellm.proxy._types import ProxyException, UserAPIKeyAuth
logging_obj = MagicMock()
logging_obj.litellm_call_id = "call-abc"
logging_obj.litellm_params = {"model_info": {"id": "my-embed-deployment-id"}}
data = {"litellm_logging_obj": logging_obj}
processor = ProxyBaseLLMRequestProcessing(data=data)
async def popping_hook(user_api_key_dict, original_exception, request_data):
request_data.pop("litellm_logging_obj", None)
return None
proxy_logging_obj = MagicMock()
proxy_logging_obj.post_call_failure_hook = AsyncMock(side_effect=popping_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("provider 429"),
user_api_key_dict=UserAPIKeyAuth(api_key="sk-test"),
proxy_logging_obj=proxy_logging_obj,
)
assert exc_info.value.headers["x-litellm-model-id"] == "my-embed-deployment-id"
assert exc_info.value.headers["x-litellm-call-id"] == "call-abc"
async def test_maybe_get_model_id_falls_back_to_request_metadata(self):
"""The router stamps the selected deployment's model_info into the request
metadata; without a logging object that is the last place the id survives."""
processor = ProxyBaseLLMRequestProcessing(data={"metadata": {"model_info": {"id": "dep-from-metadata"}}})
assert processor.maybe_get_model_id(None) == "dep-from-metadata"
async def test_already_normalized_proxy_exception_is_honored(self):
"""A ProxyException raised mid-request (e.g. a guardrail block) is already
the OpenAI wire format. The funnel must re-raise it untouched instead of