fix: stamp failed_deployment_id in generic API path for weighted failover

_ageneric_api_call_with_fallbacks_helper (used by /v1/messages,
image_edit, generate_content, and other factory_function endpoints)
did not call _set_failed_deployment_id_on_exception on failures.
This meant the weighted-failover logic in
_maybe_run_weighted_failover always bailed out early (failed_id
was None), causing requests on these paths to skip healthy
same-group siblings and jump straight to cross-group fallbacks.

The fix adds the same two calls (_set_deployment_num_retries and
_set_failed_deployment_id) that _acompletion and _completion
already make in their exception handlers.
This commit is contained in:
devin-ai-integration[bot] 2026-07-01 03:36:41 +00:00 committed by GitHub
parent cca71a07c2
commit 5795f532e5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 86 additions and 0 deletions

View file

@ -4313,6 +4313,7 @@ class Router:
passthrough_on_no_deployment = kwargs.pop("passthrough_on_no_deployment", False)
function_name = "_ageneric_api_call_with_fallbacks"
deployment = None
try:
parent_otel_span = _get_parent_otel_span_from_kwargs(kwargs)
try:
@ -4392,6 +4393,9 @@ class Router:
)
if model is not None:
self.fail_calls[model] += 1
if deployment is not None:
self._set_deployment_num_retries_on_exception(e, deployment)
self._set_failed_deployment_id_on_exception(e, deployment)
raise e
async def _aresponses_with_streaming_fallbacks(

View file

@ -769,3 +769,85 @@ async def test_failover_falls_through_to_external_fallback_when_remaining_in_coo
)
assert response._hidden_params["model_id"] == "fallback"
# ---------------------------------------------------------------------------
# Regression: generic API path (used by /v1/messages, image_edit, etc.)
# must also stamp failed_deployment_id for weighted failover to work
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_generic_api_path_sets_failed_deployment_id():
"""_ageneric_api_call_with_fallbacks_helper (used by /v1/messages and
other factory_function endpoints) did not call
_set_failed_deployment_id_on_exception, so weighted failover always
bailed out. Verify the attribute is now stamped."""
router = Router(
model_list=[
{
"model_name": "test-model",
"litellm_params": {
"model": "gpt-4o",
"api_key": "bad",
"mock_response": Exception("region down"),
"weight": 1,
},
"model_info": {"id": "dep-A"},
},
],
routing_strategy="simple-shuffle",
num_retries=0,
enable_weighted_failover=True,
)
with pytest.raises(Exception) as exc_info:
await router._ageneric_api_call_with_fallbacks_helper(
model="test-model",
original_generic_function=router._acompletion,
messages=[{"role": "user", "content": "hi"}],
)
assert getattr(exc_info.value, "failed_deployment_id", None) == "dep-A"
@pytest.mark.asyncio
async def test_weighted_failover_works_on_generic_api_path():
"""End-to-end: weighted failover via the _ageneric_api_call_with_fallbacks
path (used by /v1/messages, image_edit, generate_content, etc.) should
re-pick a healthy sibling in the same model group before falling back
to a cross-group fallback. Uses anthropic_messages to exercise the
exact code path reported in the bug."""
router = Router(
model_list=[
{
"model_name": "test-model",
"litellm_params": {
"model": "gpt-4o",
"api_key": "bad",
"mock_response": Exception("region-A down"),
"weight": 1,
},
"model_info": {"id": "A"},
},
{
"model_name": "test-model",
"litellm_params": {
"model": "gpt-4o",
"api_key": "good",
"mock_response": "ok from B",
"weight": 0,
},
"model_info": {"id": "B"},
},
],
routing_strategy="simple-shuffle",
num_retries=0,
enable_weighted_failover=True,
)
response = await router.anthropic_messages(
model="test-model",
messages=[{"role": "user", "content": "hi"}],
)
assert response._hidden_params["model_id"] == "B"