fix(router): restore four base behaviors the RoutingContext refactor changed

jev_input_targets takes a Router again through the renamed_router_param shim,
send_llm_exception_alert returns None without a router, litellm_to_list rejects anything but an
EmbeddingResponse, and get_deployments_for_tag with no model no longer reads tag filtering from the
healthy pool
This commit is contained in:
narmstrong-berry 2026-10-31 04:24:43 +00:00
parent 61efbd071e
commit 5ec3c1e1cc
No known key found for this signature in database
8 changed files with 83 additions and 5 deletions

View file

@ -14,13 +14,13 @@ else:
RoutingContext = Any
def litellm_to_list(embeds: litellm.EmbeddingResponse) -> list[list[float]]:
def litellm_to_list(embeds: object) -> list[list[float]]:
"""Convert a LiteLLM embedding response to a list of embeddings.
:param embeds: The LiteLLM embedding response.
:return: A list of embeddings.
"""
if not embeds or not embeds.data:
if not embeds or not isinstance(embeds, litellm.EmbeddingResponse) or not embeds.data:
raise ValueError("No embeddings found in LiteLLM embedding response.")
return [x["embedding"] for x in embeds.data]

View file

@ -68,6 +68,7 @@ class JevBudgetedRequest:
telemetry: StandardLoggingRoutingDecision
@renamed_router_param("router", "context")
def jev_input_targets(context: "RoutingContext", config: "OpenSourceClassifierConfig") -> tuple[JevInputTarget, ...]:
from litellm.types.router import Deployment

View file

@ -392,9 +392,11 @@ def _resolve_constraint_only_pool(
def _all_deployments_or_fallback(
ctx: "RoutingContext",
model: str,
model: str | None,
fallback: _DeploymentPool,
) -> Sequence[_DeploymentLike | DeploymentTypedDict] | Mapping[_DeploymentLike, object]:
if model is None: # a group of no name has no deployments, it is not a failed lookup
return ()
try:
return ctx.registry().deployments(model, source="all")
except Exception: # noqa: BLE001 # fail safe toward today's healthy-only behavior on lookup errors
@ -456,13 +458,15 @@ def _tags_that_route(request_tags: Sequence[str], metadata: object, routing_pref
def _tag_known_to_group(
ctx: "RoutingContext",
model: str,
model: str | None,
positive_tags: Sequence[str],
routing_confirmed: frozenset[str],
) -> bool:
tag_set: Final = frozenset(positive_tags)
if tag_set & routing_confirmed:
return True
if model is None:
return False
try:
all_deployments: Final = ctx.registry().deployments(model, source="all")
except Exception: # noqa: BLE001 # fail safe toward "unrecognized" so lookup errors preserve the existing silent-fallback behavior

View file

@ -23,7 +23,7 @@ else:
@renamed_router_param("litellm_router_instance")
async def send_llm_exception_alert(
ctx: "RoutingContext",
ctx: "RoutingContext | None",
request_kwargs: dict,
error_traceback_str: str,
original_exception,
@ -39,6 +39,9 @@ async def send_llm_exception_alert(
Returns:
None
"""
if ctx is None:
return
slack_alerting_logger: Final = ctx.observers().slack_alerting_logger
if slack_alerting_logger is None:
return

View file

@ -1316,3 +1316,24 @@ async def test_jev_open_breaker_preserves_input_budget_in_fallback_decision(fall
transport.handle_async_request.assert_awaited_once()
finally:
await handler.client.aclose()
def test_jev_input_targets_still_takes_a_router_with_a_deprecation_warning() -> None:
from litellm.router_strategy.complexity_router.jev_classifier import jev_input_targets
router = litellm.Router(
model_list=[
{
"model_name": "decision-deployment",
"litellm_params": {"model": "openai/gpt-4o-mini", "api_key": "sk-test"},
"model_info": {"id": "d1", "max_input_tokens": 1234},
}
]
)
config = JevClassifierConfig.model_validate({"deployment_name": "decision-deployment"})
with pytest.warns(DeprecationWarning, match="pass context"):
old_way = jev_input_targets(router, config)
assert old_way == jev_input_targets(router._core_ctx, config)
assert [target.max_tokens for target in old_way] == [1234]

View file

@ -134,3 +134,19 @@ class TestEmbeddingInputCap:
_encoder(router, max_input_chars=20).encode_queries(["route me please, then a huge pasted file"])
assert router.embedded_inputs == [["route me please, the"]]
@pytest.mark.parametrize(
"embeds",
[
{"data": [{"embedding": [0.1]}]},
[{"embedding": [0.1]}],
type("HasData", (), {"data": [{"embedding": [0.1]}]})(),
],
ids=["dict", "list", "object-with-data"],
)
def test_litellm_to_list_rejects_anything_but_an_embedding_response(embeds: object) -> None:
from litellm.router_strategy.auto_router.litellm_encoder import litellm_to_list
with pytest.raises(ValueError, match="No embeddings found"):
litellm_to_list(embeds)

View file

@ -1225,6 +1225,32 @@ def test_chain_tag_filtering_override_falls_back_to_healthy_deployments_on_looku
assert _chain_tag_filtering_override(_BrokenCtx(), "gpt-4", healthy_deployments) is False
@pytest.mark.asyncio()
async def test_get_deployments_for_tag_without_a_model_keeps_the_router_wide_setting():
# model=None names no group, so no group-level enable_tag_filtering applies: the router-wide
# setting (off here) decides, even when a healthy deployment opts in on its own
router = litellm.Router(
model_list=[
{
"model_name": "gpt-4",
"litellm_params": {"model": "gpt-4o", "api_key": "sk-test", "tags": ["teamA"]},
"model_info": {"id": "a", "enable_tag_filtering": True},
}
]
)
healthy_deployments = router.registry().deployments("gpt-4", source="all")
picked = await get_deployments_for_tag(
router._core_ctx,
None, # pyright: ignore[reportArgumentType] # a direct caller passing no model
healthy_deployments,
request_kwargs={"metadata": {"tags": ["zzz"]}},
request_deployments=healthy_deployments,
)
assert [d["model_info"]["id"] for d in picked] == ["a"]
@pytest.mark.asyncio()
async def test_chain_enable_tag_filtering_true_overrides_router_level_false():
# Router-wide tag filtering is off; this model group opts in on its own via

View file

@ -273,3 +273,10 @@ def setup_and_teardown():
yield
loop.close()
asyncio.set_event_loop(None)
@pytest.mark.asyncio
async def test_send_llm_exception_alert_without_a_router_does_nothing():
from litellm.router_utils.handle_error import send_llm_exception_alert
assert await send_llm_exception_alert(None, {"model": "gpt-5-mini"}, "traceback", Exception("boom")) is None