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