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
This commit is contained in:
mateo-berri 2026-10-08 01:15:00 -07:00
parent 7a21fe76c0
commit bc49a02530
2 changed files with 73 additions and 11 deletions

View file

@ -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}"
)

View file

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