mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-24 00:52:24 +00:00
fix(router): explain fallback outcome in plain words in the raised error (#42509)
* fix(router): explain fallback outcome in plain words in the raised error Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(router): keep the fallback outcome trailer on the outermost hop only Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(router): explain failed context-window and content-policy fallbacks too Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(router): name both Router and proxy fallback config in the no-fallback hint Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: ryan <ryan@berri.ai> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
e0c2cbff21
commit
a788c4ab2b
3 changed files with 97 additions and 19 deletions
|
|
@ -176,6 +176,8 @@ from litellm.router_utils.common_utils import (
|
|||
_is_proxy_admin_request,
|
||||
filter_team_based_models,
|
||||
filter_web_search_deployments,
|
||||
format_fallback_outcome_message,
|
||||
format_no_fallback_group_message,
|
||||
get_request_team_id,
|
||||
provider_for_generic_call,
|
||||
resolve_model_group_alias,
|
||||
|
|
@ -7162,6 +7164,9 @@ class Router:
|
|||
# behind the router name, and fallbacks are configured per tier, not per router.
|
||||
lookup_groups: Final[tuple[str, ...]] = fallback_lookup_groups(kwargs, model_group)
|
||||
fallback_failure_exception_str = ""
|
||||
no_fallback_group_explained = False
|
||||
hop_depth: Final = kwargs.get("fallback_depth")
|
||||
nested_fallback_hop: Final = isinstance(hop_depth, int) and hop_depth > 0
|
||||
|
||||
if disable_fallbacks is True or original_model_group is None:
|
||||
raise e
|
||||
|
|
@ -7353,8 +7358,13 @@ class Router:
|
|||
" -> ".join(lookup_groups),
|
||||
masked_fallbacks,
|
||||
)
|
||||
if hasattr(original_exception, "message") and litellm.expose_router_debug_in_errors:
|
||||
original_exception.message += f"No fallback model group found for lookup_groups={' -> '.join(lookup_groups)}. Fallbacks={masked_fallbacks}"
|
||||
if (
|
||||
hasattr(original_exception, "message")
|
||||
and litellm.expose_router_debug_in_errors
|
||||
and not nested_fallback_hop
|
||||
):
|
||||
original_exception.message += format_no_fallback_group_message(lookup_groups, fallbacks)
|
||||
no_fallback_group_explained = True
|
||||
raise original_exception
|
||||
|
||||
input_kwargs.update(
|
||||
|
|
@ -7385,11 +7395,16 @@ class Router:
|
|||
cooldown_info,
|
||||
)
|
||||
|
||||
if hasattr(original_exception, "message") and litellm.expose_router_debug_in_errors:
|
||||
# add the available fallbacks to the exception
|
||||
original_exception.message += f". Received Model Group={model_group}\nAvailable Model Group Fallbacks={mask_sensitive_structure(fallback_model_group)}"
|
||||
if len(fallback_failure_exception_str) > 0:
|
||||
original_exception.message += f"\nError doing the fallback: {fallback_failure_exception_str}"
|
||||
attempted_fallback_group: Final = input_kwargs.get("fallback_model_group")
|
||||
if (
|
||||
hasattr(original_exception, "message")
|
||||
and litellm.expose_router_debug_in_errors
|
||||
and not no_fallback_group_explained
|
||||
and not nested_fallback_hop
|
||||
):
|
||||
original_exception.message += format_fallback_outcome_message(
|
||||
model_group, attempted_fallback_group, fallback_failure_exception_str
|
||||
)
|
||||
|
||||
raise original_exception
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import hashlib
|
||||
import json
|
||||
from collections.abc import Mapping
|
||||
from collections.abc import Mapping, Sequence
|
||||
from types import MappingProxyType
|
||||
from typing import TYPE_CHECKING, Final
|
||||
|
||||
|
|
@ -12,6 +12,7 @@ from litellm._logging import verbose_logger, verbose_router_logger
|
|||
from litellm.constants import ROUTER_FALLBACK_ERROR_DETAIL_MAX_CHARS
|
||||
from litellm.exceptions import BadRequestError
|
||||
from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider
|
||||
from litellm.litellm_core_utils.sensitive_data_masker import mask_sensitive_structure
|
||||
from litellm.types.router import CredentialLiteLLMParams
|
||||
from litellm.types.utils import LlmProviders
|
||||
|
||||
|
|
@ -76,6 +77,36 @@ def truncate_fallback_error_detail(detail: str) -> str:
|
|||
return f"{detail[:ROUTER_FALLBACK_ERROR_DETAIL_MAX_CHARS]}... [truncated {dropped} characters]"
|
||||
|
||||
|
||||
def format_no_fallback_group_message(lookup_groups: Sequence[str], fallbacks: Sequence[Mapping[str, object]]) -> str:
|
||||
"""User-facing explanation appended when a request fails and no fallback chain matches its model group."""
|
||||
requested: Final = " -> ".join(lookup_groups)
|
||||
configured: Final = tuple(dict.fromkeys(key for entry in fallbacks for key in entry))
|
||||
configured_text: Final = (
|
||||
f" Fallbacks are configured for: {', '.join(configured)}." if configured else " No fallbacks are configured."
|
||||
)
|
||||
return (
|
||||
f"\n\nLiteLLM: model group '{requested}' failed with the error above and no fallback model group was found "
|
||||
f"for it, so the request was not retried on another model.{configured_text}"
|
||||
" Add a fallbacks entry for that model group (Router fallbacks or proxy router_settings.fallbacks)"
|
||||
" to retry on another model."
|
||||
)
|
||||
|
||||
|
||||
def format_fallback_outcome_message(
|
||||
model_group: str | None,
|
||||
fallback_model_group: Sequence[object] | None,
|
||||
fallback_failure_detail: str,
|
||||
) -> str:
|
||||
"""User-facing explanation appended when the fallback orchestrator gives up and re-raises the primary error."""
|
||||
lead: Final = f"\n\nLiteLLM: model group '{model_group}' failed with the error above."
|
||||
if not fallback_model_group:
|
||||
return f"{lead} No fallback was attempted."
|
||||
targets: Final = ", ".join(str(mask_sensitive_structure(target)) for target in fallback_model_group)
|
||||
if not fallback_failure_detail:
|
||||
return f"{lead} Fallback model group(s) configured: {targets}."
|
||||
return f"{lead} Fallback to {targets} also failed: {fallback_failure_detail}"
|
||||
|
||||
|
||||
def get_litellm_params_sensitive_credential_hash(litellm_params: dict) -> str:
|
||||
"""
|
||||
Hash of the credential params, used for mapping the file id to the right model
|
||||
|
|
|
|||
|
|
@ -26,8 +26,8 @@ Five leak sites are gated in `litellm/router.py`:
|
|||
1. Deployment timeout debug after `litellm.Timeout`
|
||||
2. ContextWindowExceededError fallback hint
|
||||
3. ContentPolicyViolationError fallback hint
|
||||
4. "No fallback model group found for..." when fallbacks dict misses
|
||||
5. "Received Model Group=...\\nAvailable Model Group Fallbacks=..."
|
||||
4. "no fallback model group was found" when fallbacks dict misses
|
||||
5. "model group '...' failed with the error above" plus the fallback outcome
|
||||
(always fires on terminal raise from the fallback orchestrator)
|
||||
|
||||
Site 5 is the broadest — it fires for every failing call that goes
|
||||
|
|
@ -42,8 +42,9 @@ import pytest
|
|||
import litellm
|
||||
from litellm import Router
|
||||
|
||||
_RECEIVED_MODEL_GROUP_PHRASE = "Received Model Group="
|
||||
_AVAILABLE_FALLBACKS_PHRASE = "Available Model Group Fallbacks="
|
||||
_RECEIVED_MODEL_GROUP_PHRASE = "failed with the error above"
|
||||
_AVAILABLE_FALLBACKS_PHRASE = "No fallback was attempted"
|
||||
_NO_FALLBACK_GROUP_PHRASE = "no fallback model group was found"
|
||||
_CONTEXT_WINDOW_HINT_PHRASE = "context_window_fallbacks="
|
||||
_INTERNAL_MODEL_GROUP_NAME = "all-anthropic/claude-secret-internal"
|
||||
_FALLBACK_CREDENTIAL = "sk-INLINEFALLBACKSECRET1234567890"
|
||||
|
|
@ -124,7 +125,7 @@ def test_flag_defaults_on():
|
|||
assert litellm.expose_router_debug_in_errors is True
|
||||
|
||||
|
||||
# --- Site 5: "Received Model Group=..." on terminal raise --------------------
|
||||
# --- Site 5: fallback outcome on terminal raise --------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -192,7 +193,7 @@ async def test_flag_on_shows_context_window_fallback_hint(monkeypatch: pytest.Mo
|
|||
assert _INTERNAL_MODEL_GROUP_NAME in msg, msg
|
||||
|
||||
|
||||
# --- Site 4: "No fallback model group found..." when fallbacks miss ---------
|
||||
# --- Site 4: "no fallback model group was found" when fallbacks miss ---------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -221,7 +222,7 @@ async def test_flag_off_does_not_leak_when_no_fallback_group_found(monkeypatch:
|
|||
messages=[{"role": "user", "content": "hi"}],
|
||||
)
|
||||
msg = excinfo.value.message
|
||||
assert "No fallback model group found" not in msg, msg
|
||||
assert _NO_FALLBACK_GROUP_PHRASE not in msg, msg
|
||||
assert "some-other-group" not in msg, msg
|
||||
assert _INTERNAL_MODEL_GROUP_NAME not in msg, msg
|
||||
|
||||
|
|
@ -250,8 +251,12 @@ async def test_flag_on_shows_when_no_fallback_group_found(monkeypatch: pytest.Mo
|
|||
messages=[{"role": "user", "content": "hi"}],
|
||||
)
|
||||
msg = excinfo.value.message
|
||||
assert "No fallback model group found" in msg, msg
|
||||
assert _INTERNAL_MODEL_GROUP_NAME in msg, msg
|
||||
assert _NO_FALLBACK_GROUP_PHRASE in msg, msg
|
||||
assert f"model group '{_INTERNAL_MODEL_GROUP_NAME}' failed with the error above" in msg, msg
|
||||
assert "Fallbacks are configured for: some-other-group" in msg, msg
|
||||
assert "not retried on another model" in msg, msg
|
||||
assert _AVAILABLE_FALLBACKS_PHRASE not in msg, msg
|
||||
assert msg.count("failed with the error above") == 1, msg
|
||||
|
||||
|
||||
# --- Site 1: Deployment timeout debug on litellm.Timeout --------------------
|
||||
|
|
@ -349,6 +354,30 @@ async def test_flag_on_shows_content_policy_fallback_hint(monkeypatch: pytest.Mo
|
|||
assert _INTERNAL_MODEL_GROUP_NAME in msg, msg
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_flag_on_explains_failed_content_policy_fallback(monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.setattr(litellm, "expose_router_debug_in_errors", True)
|
||||
router = Router(
|
||||
model_list=[
|
||||
{"model_name": _INTERNAL_MODEL_GROUP_NAME, "litellm_params": {"model": "gpt-4o", "api_key": "key"}},
|
||||
{"model_name": "policy-safe-group", "litellm_params": {"model": "gpt-4o", "api_key": "key"}},
|
||||
],
|
||||
content_policy_fallbacks=[{_INTERNAL_MODEL_GROUP_NAME: ["policy-safe-group"]}],
|
||||
num_retries=0,
|
||||
)
|
||||
with pytest.raises(litellm.ContentPolicyViolationError) as excinfo:
|
||||
await router.acompletion(
|
||||
model=_INTERNAL_MODEL_GROUP_NAME,
|
||||
messages=[{"role": "user", "content": "hi"}],
|
||||
mock_response=_content_policy_error(),
|
||||
)
|
||||
msg = excinfo.value.message
|
||||
assert f"model group '{_INTERNAL_MODEL_GROUP_NAME}' failed with the error above" in msg, msg
|
||||
assert "Fallback to policy-safe-group also failed: " in msg, msg
|
||||
assert _AVAILABLE_FALLBACKS_PHRASE not in msg, msg
|
||||
assert msg.count("failed with the error above") == 1, msg
|
||||
|
||||
|
||||
# --- Credential masking: raw provider keys never leak, either flag state ----
|
||||
|
||||
|
||||
|
|
@ -387,7 +416,7 @@ async def test_flag_on_masks_fallback_credentials(monkeypatch: pytest.MonkeyPatc
|
|||
async def test_flag_on_scrubs_credential_from_inner_fallback_exception_string(monkeypatch: pytest.MonkeyPatch):
|
||||
"""If the fallback attempt itself raises an exception whose message embeds a
|
||||
raw provider credential (e.g. a provider SDK echoing back the api_key it was
|
||||
called with), that string is re-embedded via `Error doing the fallback: ...`
|
||||
called with), that string is re-embedded via `Fallback to ... also failed: ...`
|
||||
on the terminal raise. The router must scrub known secret patterns from it.
|
||||
The primary fails with a benign rate-limit; the fallback deployment fails
|
||||
with an exception whose text contains the secret."""
|
||||
|
|
@ -423,6 +452,9 @@ async def test_flag_on_scrubs_credential_from_inner_fallback_exception_string(mo
|
|||
messages=[{"role": "user", "content": "hi"}],
|
||||
)
|
||||
msg = excinfo.value.message
|
||||
assert "Error doing the fallback:" in msg, msg
|
||||
assert f"model group '{_INTERNAL_MODEL_GROUP_NAME}' failed with the error above" in msg, msg
|
||||
assert "Fallback to fallback-group also failed: " in msg, msg
|
||||
assert "content_filter_policy" in msg, msg
|
||||
assert msg.count("failed with the error above") == 1, msg
|
||||
assert inner_secret not in msg, msg
|
||||
assert "REDACTED" in msg, msg
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue