mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-25 01:02:15 +00:00
Merge pull request #42046 from BerriAI/litellm_cost_poll_404_no_cooldown
This commit is contained in:
commit
56b3422cc2
3 changed files with 143 additions and 2 deletions
|
|
@ -179,6 +179,7 @@ from litellm.router_utils.cooldown_handlers import (
|
|||
_get_cooldown_deployments,
|
||||
_set_cooldown_deployments,
|
||||
is_advisor_orchestration_failure,
|
||||
is_background_response_cost_poll_not_found,
|
||||
is_caller_timeout_408,
|
||||
)
|
||||
from litellm.router_utils.fallback_event_handlers import (
|
||||
|
|
@ -8137,12 +8138,19 @@ class Router:
|
|||
)
|
||||
return False
|
||||
|
||||
exception_status: Final = getattr(exception, "status_code", "")
|
||||
|
||||
# Cache litellm_params to avoid repeated dict lookups
|
||||
litellm_params: Final = kwargs.get("litellm_params", {})
|
||||
_model_info: Final = litellm_params.get("model_info", {})
|
||||
|
||||
if is_background_response_cost_poll_not_found(exception, litellm_params):
|
||||
verbose_router_logger.debug(
|
||||
"Router: Exiting 'deployment_callback_on_failure' without cooldown. "
|
||||
"Provider 404 came from the background response cost poll, not the deployment's health."
|
||||
)
|
||||
return False
|
||||
|
||||
exception_status: Final = getattr(exception, "status_code", "")
|
||||
|
||||
if is_caller_timeout_408(kwargs, exception_status):
|
||||
verbose_router_logger.debug(
|
||||
"Router: Exiting 'deployment_callback_on_failure' without cooldown. "
|
||||
|
|
|
|||
|
|
@ -20,9 +20,11 @@ from litellm.constants import (
|
|||
DEFAULT_COOLDOWN_TIME_SECONDS,
|
||||
DEFAULT_FAILURE_THRESHOLD_MINIMUM_REQUESTS,
|
||||
DEFAULT_FAILURE_THRESHOLD_PERCENT,
|
||||
INTERNAL_CALL_ORIGIN_METADATA_KEY,
|
||||
SINGLE_DEPLOYMENT_TRAFFIC_FAILURE_THRESHOLD,
|
||||
)
|
||||
from litellm.router_utils.cooldown_callbacks import router_cooldown_event_callback
|
||||
from litellm.types.utils import BACKGROUND_RESPONSE_COST_POLL_CALL_ORIGIN
|
||||
|
||||
from .router_callbacks.track_deployment_metrics import (
|
||||
get_deployment_failures_for_current_minute,
|
||||
|
|
@ -62,6 +64,15 @@ def is_advisor_orchestration_failure(exception: BaseException | None) -> bool:
|
|||
return bool(getattr(exception, _ADVISOR_ORCHESTRATION_FAILURE_ATTR, False))
|
||||
|
||||
|
||||
def is_background_response_cost_poll_not_found(exception: Exception, litellm_params: Mapping[str, object]) -> bool:
|
||||
"""Whether a background response cost poll failed with a provider 404."""
|
||||
return getattr(exception, "status_code", None) == 404 and any(
|
||||
isinstance(candidate, Mapping)
|
||||
and candidate.get(INTERNAL_CALL_ORIGIN_METADATA_KEY) == BACKGROUND_RESPONSE_COST_POLL_CALL_ORIGIN
|
||||
for candidate in (litellm_params.get("metadata"), litellm_params.get("litellm_metadata"))
|
||||
)
|
||||
|
||||
|
||||
_EXCEPTION_POLICY_FIELDS: Final[tuple[tuple[type, str], ...]] = (
|
||||
# ContentPolicyViolationError subclasses BadRequestError, so it must be checked first.
|
||||
(litellm.ContentPolicyViolationError, "ContentPolicyViolationErrorAllowedFails"),
|
||||
|
|
|
|||
|
|
@ -8779,6 +8779,128 @@ class TestAdvisorSubCallCooldown:
|
|||
assert "dep-1" not in self._cooled_down_ids(router)
|
||||
|
||||
|
||||
class TestBackgroundResponseCostPollCooldown:
|
||||
def _router(self):
|
||||
return litellm.Router(
|
||||
model_list=[
|
||||
{
|
||||
"model_name": "gpt-4.1",
|
||||
"litellm_params": {"model": "openai/gpt-4.1"},
|
||||
"model_info": {"id": "dep-1"},
|
||||
}
|
||||
],
|
||||
allowed_fails=0,
|
||||
)
|
||||
|
||||
def _cooled_down_ids(self, router):
|
||||
active = router.cooldown_cache.get_active_cooldowns(model_ids=["dep-1"], parent_otel_span=None)
|
||||
return [entry[0] for entry in active]
|
||||
|
||||
def _not_found(self):
|
||||
return litellm.NotFoundError(
|
||||
message="Response with id 'resp_gone' not found.", llm_provider="openai", model="gpt-4.1"
|
||||
)
|
||||
|
||||
def _deployment_callback_on_failure(self, router, kwargs):
|
||||
from datetime import datetime
|
||||
|
||||
now = datetime.now()
|
||||
return router.deployment_callback_on_failure(kwargs, None, now, now)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_untagged_not_found_cools_down_deployment(self):
|
||||
router = self._router()
|
||||
assert (
|
||||
self._deployment_callback_on_failure(
|
||||
router,
|
||||
{
|
||||
"exception": self._not_found(),
|
||||
"litellm_params": {"model_info": {"id": "dep-1"}, "metadata": {}},
|
||||
},
|
||||
)
|
||||
is True
|
||||
)
|
||||
assert "dep-1" in self._cooled_down_ids(router)
|
||||
|
||||
def test_cost_poll_not_found_does_not_cool_down_deployment(self):
|
||||
from datetime import datetime
|
||||
|
||||
from litellm.constants import INTERNAL_CALL_ORIGIN_METADATA_KEY
|
||||
from litellm.router_utils.router_callbacks.track_deployment_metrics import (
|
||||
get_deployment_failures_for_current_minute,
|
||||
)
|
||||
from litellm.types.utils import BACKGROUND_RESPONSE_COST_POLL_CALL_ORIGIN
|
||||
|
||||
router = self._router()
|
||||
now = datetime.now()
|
||||
assert (
|
||||
router.deployment_callback_on_failure(
|
||||
{
|
||||
"exception": self._not_found(),
|
||||
"litellm_params": {
|
||||
"model_info": {"id": "dep-1"},
|
||||
"litellm_metadata": {
|
||||
INTERNAL_CALL_ORIGIN_METADATA_KEY: BACKGROUND_RESPONSE_COST_POLL_CALL_ORIGIN
|
||||
},
|
||||
},
|
||||
},
|
||||
None,
|
||||
now,
|
||||
now,
|
||||
)
|
||||
is False
|
||||
)
|
||||
assert self._cooled_down_ids(router) == []
|
||||
value = get_deployment_failures_for_current_minute(litellm_router_instance=router, deployment_id="dep-1")
|
||||
assert not value
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cost_poll_non_404_still_cools_down_deployment(self):
|
||||
from litellm.constants import INTERNAL_CALL_ORIGIN_METADATA_KEY
|
||||
from litellm.types.utils import BACKGROUND_RESPONSE_COST_POLL_CALL_ORIGIN
|
||||
|
||||
router = self._router()
|
||||
assert (
|
||||
self._deployment_callback_on_failure(
|
||||
router,
|
||||
{
|
||||
"exception": litellm.InternalServerError(
|
||||
message="upstream 500", llm_provider="openai", model="gpt-4.1"
|
||||
),
|
||||
"litellm_params": {
|
||||
"model_info": {"id": "dep-1"},
|
||||
"litellm_metadata": {
|
||||
INTERNAL_CALL_ORIGIN_METADATA_KEY: BACKGROUND_RESPONSE_COST_POLL_CALL_ORIGIN
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
is True
|
||||
)
|
||||
assert "dep-1" in self._cooled_down_ids(router)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_other_internal_origin_not_found_still_cools_down_deployment(self):
|
||||
from litellm.constants import INTERNAL_CALL_ORIGIN_METADATA_KEY
|
||||
from litellm.types.utils import AUTOROUTER_CLASSIFIER_CALL_ORIGIN
|
||||
|
||||
router = self._router()
|
||||
assert (
|
||||
self._deployment_callback_on_failure(
|
||||
router,
|
||||
{
|
||||
"exception": self._not_found(),
|
||||
"litellm_params": {
|
||||
"model_info": {"id": "dep-1"},
|
||||
"litellm_metadata": {INTERNAL_CALL_ORIGIN_METADATA_KEY: AUTOROUTER_CLASSIFIER_CALL_ORIGIN},
|
||||
},
|
||||
},
|
||||
)
|
||||
is True
|
||||
)
|
||||
assert "dep-1" in self._cooled_down_ids(router)
|
||||
|
||||
|
||||
class TestCallerTimeoutCooldown:
|
||||
"""A timeout the caller set (the proxy's `timeout` body field or x-litellm-timeout
|
||||
header) comes back as a 408 whatever the deployment's health, so it must neither
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue