From bc49a02530357feda8e96ca89e81e804130aa315 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Thu, 8 Oct 2026 01:15:00 -0700 Subject: [PATCH] fix(enterprise): expire background response rows on any provider 404 status The GET path maps the provider's 404 body to litellm.BadRequestError that still carries status_code 404, so an except on NotFoundError never fired and the poll job kept retrying the row every cycle. Key the terminal decision on the status code instead of the class --- .../common_utils/check_responses_cost.py | 21 +++---- .../common_utils/test_check_responses_cost.py | 63 +++++++++++++++++++ 2 files changed, 73 insertions(+), 11 deletions(-) diff --git a/enterprise/litellm_enterprise/proxy/common_utils/check_responses_cost.py b/enterprise/litellm_enterprise/proxy/common_utils/check_responses_cost.py index a913ce3d424..5d8139f7888 100644 --- a/enterprise/litellm_enterprise/proxy/common_utils/check_responses_cost.py +++ b/enterprise/litellm_enterprise/proxy/common_utils/check_responses_cost.py @@ -47,6 +47,10 @@ def _managed_object_table(prisma_client: "PrismaClient") -> "TableActions[_Manag return ManagedObjectRepository(prisma_client).table +def _is_provider_not_found(error: Exception) -> bool: + return getattr(error, "status_code", None) == 404 + + class CheckResponsesCost: def __init__( self, @@ -210,18 +214,13 @@ class CheckResponsesCost: f"Response {unified_object_id} status: {response.status}, model: {model_name}" ) - except litellm.NotFoundError as e: - if not via_router: - verbose_proxy_logger.warning( - f"Skipping job {unified_object_id} due to error: {e}" - ) - continue - verbose_proxy_logger.info( - f"Response {unified_object_id} no longer available at provider (404), marking stale_expired: {e}" - ) - expired_jobs.append(job) - continue except Exception as e: + if via_router and _is_provider_not_found(e): + verbose_proxy_logger.info( + f"Response {unified_object_id} no longer available at provider (404), marking stale_expired: {e}" + ) + expired_jobs.append(job) + continue verbose_proxy_logger.warning( f"Skipping job {unified_object_id} due to error: {e}" ) diff --git a/tests/unit/proxy/common_utils/test_check_responses_cost.py b/tests/unit/proxy/common_utils/test_check_responses_cost.py index 2d63bcafdf2..4e41f9434aa 100644 --- a/tests/unit/proxy/common_utils/test_check_responses_cost.py +++ b/tests/unit/proxy/common_utils/test_check_responses_cost.py @@ -409,6 +409,69 @@ class TestCheckResponsesCost: assert update_many.call_args.kwargs["where"] == {"id": {"in": ["job-404"]}} assert update_many.call_args.kwargs["data"] == {"status": "stale_expired"} + @pytest.mark.asyncio + async def test_check_responses_cost_marks_mapped_provider_404_stale_expired( + self, check_responses_cost_instance, mock_prisma_client, mock_llm_router + ): + """The provider's own 404 body, mapped the way the GET path maps it, still expires the row.""" + import json + + import openai + + import litellm + from litellm.llms.openai.common_utils import OpenAIError + from litellm.responses.utils import ResponsesAPIRequestUtils + + encoded_response_id = ResponsesAPIRequestUtils._build_responses_api_response_id( + custom_llm_provider="openai", + model_id="deployment-404-mapped", + response_id="resp_upstream_404_mapped", + ) + provider_body = json.dumps( + { + "error": { + "message": "Response with id 'resp_upstream_404_mapped' not found.", + "type": "invalid_request_error", + "param": None, + "code": None, + } + } + ) + with pytest.raises(openai.APIStatusError) as mapped: + raise litellm.exception_type( + model="gpt-5.5", + custom_llm_provider="openai", + original_exception=OpenAIError(message=provider_body, status_code=404), + completion_kwargs={}, + extra_kwargs={}, + ) + assert mapped.value.status_code == 404 + + mock_job = MagicMock() + mock_job.unified_object_id = encoded_response_id + mock_job.created_by = "test-user" + mock_job.id = "job-404-mapped" + mock_job.file_object = {"model": "gpt-5.5", "id": encoded_response_id} + + mock_llm_router.get_deployment.return_value = {"model_id": "deployment-404-mapped"} + mock_llm_router.aget_responses = AsyncMock(side_effect=mapped.value) + + mock_prisma_client.db.litellm_managedobjecttable.find_many = AsyncMock( + return_value=[mock_job] + ) + mock_prisma_client.db.litellm_managedobjecttable.update_many = AsyncMock( + return_value=1 + ) + + with patch("litellm.aget_responses", new_callable=AsyncMock) as mock_sdk_aget: + await check_responses_cost_instance.check_responses_cost() + + mock_sdk_aget.assert_not_called() + update_many = mock_prisma_client.db.litellm_managedobjecttable.update_many + update_many.assert_awaited_once() + assert update_many.call_args.kwargs["where"] == {"id": {"in": ["job-404-mapped"]}} + assert update_many.call_args.kwargs["data"] == {"status": "stale_expired"} + @pytest.mark.asyncio async def test_check_responses_cost_404_without_router_deployment_keeps_row_for_retry( self, check_responses_cost_instance, mock_prisma_client, mock_llm_router