mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-13 08:52:02 +00:00
588 lines
25 KiB
Python
588 lines
25 KiB
Python
"""
|
|
Router entry points and DeprecatedRouterMembers.
|
|
|
|
Every member that moved into DeprecatedRouterMembers must return what its entry point returns,
|
|
warn once per access from outside the router, and count the access. Internal access (the router's
|
|
own modules) must stay silent.
|
|
"""
|
|
|
|
import asyncio
|
|
import copy
|
|
import dataclasses
|
|
import warnings
|
|
|
|
import httpx
|
|
import pytest
|
|
|
|
import litellm
|
|
from litellm import Router
|
|
from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler
|
|
from litellm.router_api import SETTING_FIELDS, ChangeSet, RouterSettings
|
|
from litellm.router_deprecated import DEPRECATED_ROUTER_MEMBER_USES, DeprecatedRouterMembers
|
|
from litellm.types.router import Deployment
|
|
|
|
|
|
def _router() -> Router:
|
|
return Router(
|
|
model_list=[
|
|
{
|
|
"model_name": "gpt",
|
|
"litellm_params": {"model": "openai/gpt-4o-mini", "api_key": "sk-a"},
|
|
"model_info": {"id": "dep-a", "access_groups": ["grp"]},
|
|
},
|
|
{
|
|
"model_name": "gpt",
|
|
"litellm_params": {"model": "openai/gpt-4o", "api_key": "sk-b"},
|
|
"model_info": {"id": "dep-b"},
|
|
},
|
|
{
|
|
"model_name": "team-internal",
|
|
"litellm_params": {"model": "openai/gpt-4o-mini", "api_key": "sk-t"},
|
|
"model_info": {"id": "dep-team", "team_id": "team-1", "team_public_model_name": "team-gpt"},
|
|
},
|
|
{
|
|
"model_name": "openai/*",
|
|
"litellm_params": {"model": "openai/*", "api_key": "sk-w"},
|
|
"model_info": {"id": "dep-wild"},
|
|
},
|
|
],
|
|
model_group_alias={"alias-gpt": "gpt"},
|
|
fallbacks=[{"gpt": ["team-internal"]}],
|
|
cooldown_time=7,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def router():
|
|
r = _router()
|
|
yield r
|
|
with warnings.catch_warnings():
|
|
warnings.simplefilter("ignore")
|
|
r.retire()
|
|
|
|
|
|
# (old member, how to read it through the old name, how to read it through the entry point)
|
|
READS = [
|
|
("model_list", lambda r: r.model_list, lambda r: r.registry().all()),
|
|
("model_names", lambda r: r.model_names, lambda r: r.registry().names(kind="group_set")),
|
|
("deployment_names", lambda r: r.deployment_names, lambda r: r.registry().names(kind="deployment_names")),
|
|
("team_public_model_names", lambda r: r.team_public_model_names, lambda r: r.registry().names(kind="team_public")),
|
|
(
|
|
"model_name_to_deployment_indices",
|
|
lambda r: r.model_name_to_deployment_indices,
|
|
lambda r: r.registry().index("model_name"),
|
|
),
|
|
(
|
|
"team_model_to_deployment_indices",
|
|
lambda r: r.team_model_to_deployment_indices,
|
|
lambda r: r.registry().index("team_model"),
|
|
),
|
|
("model_group_alias", lambda r: r.model_group_alias, lambda r: r.registry().aliases()),
|
|
("pattern_router", lambda r: r.pattern_router, lambda r: r.registry().patterns()),
|
|
("team_pattern_routers", lambda r: r.team_pattern_routers, lambda r: r.registry().patterns(team=True)),
|
|
("routing_groups", lambda r: r.routing_groups, lambda r: r.registry().routing_groups()),
|
|
("auto_routers", lambda r: r.auto_routers, lambda r: r.registry().plugins("auto")),
|
|
("complexity_routers", lambda r: r.complexity_routers, lambda r: r.registry().plugins("complexity")),
|
|
("quality_routers", lambda r: r.quality_routers, lambda r: r.registry().plugins("quality")),
|
|
("adaptive_routers", lambda r: r.adaptive_routers, lambda r: r.registry().plugins("adaptive")),
|
|
("routing_plugins", lambda r: r.routing_plugins, lambda r: r.registry().plugins("routing")),
|
|
(
|
|
"prompt_caching_pin_cache",
|
|
lambda r: r.prompt_caching_pin_cache,
|
|
lambda r: r.registry().plugins("prompt_caching_pins"),
|
|
),
|
|
("cache", lambda r: r.cache, lambda r: r.health().store),
|
|
("cooldown_cache", lambda r: r.cooldown_cache, lambda r: r.health().cooldowns),
|
|
("health_state_cache", lambda r: r.health_state_cache, lambda r: r.health().health_checks),
|
|
("deployment_latency_map", lambda r: r.deployment_latency_map, lambda r: r.health().latency),
|
|
("get_deployment", lambda r: r.get_deployment("dep-a"), lambda r: r.registry().deployment("dep-a")),
|
|
("has_model_id", lambda r: r.has_model_id("dep-b"), lambda r: r.registry().has_deployment("dep-b")),
|
|
("get_model_info", lambda r: r.get_model_info("dep-a"), lambda r: r.registry().deployment_dict("dep-a")),
|
|
("get_model_list", lambda r: r.get_model_list("gpt"), lambda r: r.registry().deployments("gpt")),
|
|
("get_model_list_all", lambda r: r.get_model_list(), lambda r: r.registry().deployments()),
|
|
("get_model_list_alias", lambda r: r.get_model_list("alias-gpt"), lambda r: r.registry().deployments("alias-gpt")),
|
|
(
|
|
"get_all_deployments",
|
|
lambda r: r.get_all_deployments("gpt"),
|
|
lambda r: r.registry().deployments("gpt", source="all"),
|
|
),
|
|
(
|
|
"config_deployments",
|
|
lambda r: list(r.config_deployments()),
|
|
lambda r: list(r.registry().deployments(source="config")),
|
|
),
|
|
(
|
|
"deployments_named",
|
|
lambda r: r.deployments_named("gpt"),
|
|
lambda r: r.registry().deployments("gpt", source="named"),
|
|
),
|
|
(
|
|
"get_model_list_from_model_alias",
|
|
lambda r: r.get_model_list_from_model_alias("alias-gpt"),
|
|
lambda r: r.registry().deployments("alias-gpt", source="alias"),
|
|
),
|
|
(
|
|
"get_deployment_by_model_group_name",
|
|
lambda r: r.get_deployment_by_model_group_name("gpt"),
|
|
lambda r: r.registry().deployments("gpt", source="first_in_group"),
|
|
),
|
|
("get_model_names", lambda r: r.get_model_names(), lambda r: r.registry().names()),
|
|
(
|
|
"get_model_names_team",
|
|
lambda r: r.get_model_names(team_id="team-1"),
|
|
lambda r: r.registry().names(team_id="team-1"),
|
|
),
|
|
("get_model_ids", lambda r: r.get_model_ids("gpt"), lambda r: r.registry().names("gpt", kind="ids")),
|
|
(
|
|
"get_model_ids_all",
|
|
lambda r: r.get_model_ids(exclude_team_models=True),
|
|
lambda r: r.registry().names(kind="ids", exclude_team_models=True),
|
|
),
|
|
(
|
|
"get_model_from_alias",
|
|
lambda r: r.get_model_from_alias("alias-gpt"),
|
|
lambda r: r.registry().resolve("alias-gpt", kind="alias"),
|
|
),
|
|
(
|
|
"is_recognized_model",
|
|
lambda r: r.is_recognized_model("gpt"),
|
|
lambda r: r.registry().resolve("gpt", kind="recognized"),
|
|
),
|
|
(
|
|
"routable_model_group",
|
|
lambda r: r.routable_model_group("alias-gpt"),
|
|
lambda r: r.registry().resolve("alias-gpt"),
|
|
),
|
|
(
|
|
"map_team_model",
|
|
lambda r: r.map_team_model("team-gpt", "team-1"),
|
|
lambda r: r.registry().resolve("team-gpt", kind="team_model", team_id="team-1"),
|
|
),
|
|
(
|
|
"get_model_group",
|
|
lambda r: r.get_model_group("dep-a"),
|
|
lambda r: r.registry().resolve("dep-a", kind="group_of_id"),
|
|
),
|
|
(
|
|
"get_search_tool_support",
|
|
lambda r: r.get_search_tool_support("gpt"),
|
|
lambda r: r.registry().resolve("gpt", kind="search_tool_support"),
|
|
),
|
|
("get_model_access_groups", lambda r: r.get_model_access_groups(), lambda r: r.registry().access_groups()),
|
|
(
|
|
"get_deployment_credentials",
|
|
lambda r: r.get_deployment_credentials("dep-a"),
|
|
lambda r: r.registry().credentials("dep-a"),
|
|
),
|
|
(
|
|
"get_deployment_credentials_with_provider",
|
|
lambda r: r.get_deployment_credentials_with_provider("dep-a"),
|
|
lambda r: r.registry().credentials("dep-a", form="with_provider"),
|
|
),
|
|
(
|
|
"get_credential_deployment",
|
|
lambda r: r.get_credential_deployment("dep-a"),
|
|
lambda r: r.registry().credentials("dep-a", form="deployment"),
|
|
),
|
|
("get_configured_mode", lambda r: r.get_configured_mode("gpt"), lambda r: r.registry().group("gpt", field="mode")),
|
|
(
|
|
"get_configured_token_limits",
|
|
lambda r: r.get_configured_token_limits("gpt"),
|
|
lambda r: r.registry().group("gpt", field="token_limits"),
|
|
),
|
|
(
|
|
"get_fully_blocked_model_names",
|
|
lambda r: r.get_fully_blocked_model_names(),
|
|
lambda r: r.health().fully_blocked_model_names(),
|
|
),
|
|
("get_settings", lambda r: r.get_settings(), lambda r: r.settings().as_dict()),
|
|
("fallbacks", lambda r: r.fallbacks, lambda r: r.settings().fallbacks),
|
|
("cooldown_time", lambda r: r.cooldown_time, lambda r: r.settings().cooldown_time),
|
|
("routing_strategy", lambda r: r.routing_strategy, lambda r: r.settings().routing_strategy),
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize("name, old, new", READS, ids=[r[0] for r in READS])
|
|
def test_deprecated_read_matches_entry_point(router, name, old, new):
|
|
with warnings.catch_warnings(record=True) as caught:
|
|
warnings.simplefilter("always")
|
|
old_value = old(router)
|
|
new_value = new(router)
|
|
if isinstance(old_value, (list, dict, set)) or hasattr(old_value, "__dict__"):
|
|
assert old_value == new_value
|
|
else:
|
|
assert old_value == new_value
|
|
assert any(issubclass(w.category, DeprecationWarning) for w in caught), name
|
|
|
|
|
|
def test_live_objects_not_copies(router):
|
|
with warnings.catch_warnings():
|
|
warnings.simplefilter("ignore")
|
|
assert router.model_list is router.registry().all()
|
|
assert router.fallbacks is router.settings().fallbacks
|
|
assert router.cache is router.health().store
|
|
assert router.model_group_alias is router.registry().aliases()
|
|
|
|
|
|
def test_assignment_goes_through_apply(router):
|
|
with warnings.catch_warnings():
|
|
warnings.simplefilter("ignore")
|
|
router.fallbacks = [{"gpt": ["openai/gpt-4.1"]}]
|
|
assert router.settings().fallbacks == [{"gpt": ["openai/gpt-4.1"]}]
|
|
router.search_tools = [{"search_tool_name": "s", "litellm_params": {"search_provider": "tavily"}}]
|
|
assert router.settings().search_tools[0]["search_tool_name"] == "s"
|
|
router.apply(ChangeSet(assign={"cooldown_time": 3}))
|
|
assert router.settings().cooldown_time == 3
|
|
with pytest.raises(ValueError, match="not assignable router settings"):
|
|
router.apply(ChangeSet(assign={"not_a_setting": 1}))
|
|
|
|
|
|
def test_apply_upsert_and_delete_bump_version(router):
|
|
v0 = router.registry().version
|
|
added = Deployment(
|
|
model_name="new-group",
|
|
litellm_params={"model": "openai/gpt-4o-mini", "api_key": "sk-n"},
|
|
model_info={"id": "dep-new"},
|
|
)
|
|
result = router.apply(ChangeSet(upsert=[added]))
|
|
assert result.version == v0 + 1
|
|
assert router.registry().has_deployment("dep-new")
|
|
assert router.registry().deployments("new-group")[0]["model_info"]["id"] == "dep-new"
|
|
result = router.apply(ChangeSet(delete=["dep-new"]))
|
|
assert result.version == v0 + 2
|
|
assert result.deleted[0] is not None
|
|
assert not router.registry().has_deployment("dep-new")
|
|
with warnings.catch_warnings():
|
|
warnings.simplefilter("ignore")
|
|
assert router.upsert_deployment(added) is added
|
|
assert router.upsert_deployment(added) is None # unchanged
|
|
assert router.delete_deployment("dep-new") is not None
|
|
|
|
|
|
def test_old_deployment_writes_also_advance_the_version(router):
|
|
v0 = router.registry().version
|
|
with warnings.catch_warnings():
|
|
warnings.simplefilter("ignore")
|
|
router.add_deployment(
|
|
Deployment(
|
|
model_name="added",
|
|
litellm_params={"model": "openai/gpt-4o-mini", "api_key": "sk-x"},
|
|
model_info={"id": "dep-added"},
|
|
)
|
|
)
|
|
v1 = router.registry().version
|
|
router.set_model_list([{"model_name": "only", "litellm_params": {"model": "openai/gpt-4o", "api_key": "sk-o"}}])
|
|
assert v0 < v1 < router.registry().version
|
|
assert router.registry().names() == ["only"]
|
|
v2 = router.registry().version
|
|
with warnings.catch_warnings():
|
|
warnings.simplefilter("ignore")
|
|
router.model_list = []
|
|
assert router.registry().version > v2
|
|
|
|
|
|
def test_update_settings_through_apply(router):
|
|
router.apply(ChangeSet(settings={"cooldown_time": "11", "not_allowed": 1}))
|
|
assert router.settings().cooldown_time == 11
|
|
|
|
|
|
def test_aapply_applies_changes_then_refreshes_model_info(monkeypatch):
|
|
monkeypatch.setattr(litellm, "model_cost", copy.deepcopy(litellm.model_cost))
|
|
router = Router(
|
|
model_list=[
|
|
{
|
|
"model_name": "local",
|
|
"litellm_params": {
|
|
"model": "hosted_vllm/local-model",
|
|
"api_base": "https://capacity.test/v1",
|
|
"api_key": "local-key",
|
|
},
|
|
"model_info": {"id": "dep-local"},
|
|
}
|
|
]
|
|
)
|
|
with pytest.raises(TypeError, match="needs aapply"):
|
|
router.apply(ChangeSet(refresh_model_info=True))
|
|
|
|
async def refresh() -> None:
|
|
handler = AsyncHTTPHandler()
|
|
await handler.client.aclose()
|
|
async with httpx.AsyncClient(
|
|
transport=httpx.MockTransport(
|
|
lambda request: httpx.Response(200, json={"data": [{"id": "local-model", "max_model_len": 4096}]})
|
|
)
|
|
) as client:
|
|
handler.client = client
|
|
await router.aapply(ChangeSet(assign={"cooldown_time": 4}, refresh_model_info=True, refresh_client=handler))
|
|
|
|
asyncio.run(refresh())
|
|
assert router.settings().cooldown_time == 4
|
|
assert router.registry().group("local", field="token_limits") == (4096, 4096)
|
|
|
|
|
|
def test_settings_snapshot_is_reused_until_a_setting_is_written(router):
|
|
first = router.settings()
|
|
assert router.settings() is first
|
|
router.apply(ChangeSet(assign={"cooldown_time": 7}))
|
|
assert router.settings() is not first
|
|
assert router.settings().cooldown_time == 7
|
|
router.apply(ChangeSet(settings={"cooldown_time": "8"}))
|
|
assert router.settings().cooldown_time == 8
|
|
with warnings.catch_warnings():
|
|
warnings.simplefilter("ignore")
|
|
router.fallbacks = [{"gpt": ["team-gpt"]}]
|
|
assert router.settings().fallbacks == [{"gpt": ["team-gpt"]}]
|
|
|
|
|
|
def test_deleting_a_setting_shows_as_unset(router):
|
|
router.apply(ChangeSet(assign={"cooldown_time": 3}))
|
|
assert router.settings().is_set("cooldown_time")
|
|
del router._core_cooldown_time
|
|
assert router.settings().is_set("cooldown_time") is (hasattr(router, "_core_cooldown_time"))
|
|
|
|
|
|
def test_settings_snapshot_is_frozen(router):
|
|
snapshot = router.settings()
|
|
assert isinstance(snapshot, RouterSettings)
|
|
with pytest.raises(dataclasses.FrozenInstanceError):
|
|
snapshot.cooldown_time = 1 # type: ignore[misc]
|
|
assert set(SETTING_FIELDS) <= set(RouterSettings.__dataclass_fields__)
|
|
|
|
|
|
def test_bind_to_request_logging_none_raises_as_before(router):
|
|
# The old member bound unconditionally, so None fails on the first callback registration.
|
|
with warnings.catch_warnings():
|
|
warnings.simplefilter("ignore")
|
|
with pytest.raises(AttributeError, match="add_dynamic_function_callbacks"):
|
|
router.bind_to_request_logging(None)
|
|
|
|
|
|
def test_select_picks_like_async_get_available_deployment(router):
|
|
picked = asyncio.run(router.select("gpt", {}))
|
|
with warnings.catch_warnings():
|
|
warnings.simplefilter("ignore")
|
|
old = asyncio.run(router.async_get_available_deployment(model="gpt", request_kwargs={}))
|
|
assert picked["model_name"] == old["model_name"] == "gpt"
|
|
healthy = asyncio.run(router.select("gpt", {}, kind="healthy"))
|
|
assert {d["model_info"]["id"] for d in healthy} == {"dep-a", "dep-b"}
|
|
|
|
|
|
def test_counter_and_internal_silence(router):
|
|
before = DEPRECATED_ROUTER_MEMBER_USES["get_model_ids"]
|
|
with warnings.catch_warnings(record=True) as caught:
|
|
warnings.simplefilter("always")
|
|
router.get_model_ids()
|
|
# Router internals call the core names; a completion must not warn.
|
|
asyncio.run(router.acompletion(model="gpt", messages=[{"role": "user", "content": "hi"}], mock_response="ok"))
|
|
assert DEPRECATED_ROUTER_MEMBER_USES["get_model_ids"] == before + 1
|
|
deprecations = [w for w in caught if issubclass(w.category, DeprecationWarning) and "Router." in str(w.message)]
|
|
assert [str(w.message).split(" ")[0] for w in deprecations] == ["Router.get_model_ids"]
|
|
assert deprecations[0].filename == __file__
|
|
|
|
|
|
def test_every_deprecated_member_names_a_replacement():
|
|
import inspect
|
|
|
|
from litellm.router_deprecated import _DeprecatedAttribute
|
|
|
|
for name, member in vars(DeprecatedRouterMembers).items():
|
|
if name.startswith("__"):
|
|
continue
|
|
if isinstance(member, _DeprecatedAttribute):
|
|
assert member.replacement
|
|
continue
|
|
func = member.__func__ if isinstance(member, staticmethod) else member
|
|
if callable(func):
|
|
assert "_note(" in inspect.getsource(func), name
|
|
|
|
|
|
def test_generate_model_id_is_the_module_function():
|
|
from litellm.router_api import deployment_id
|
|
|
|
params = {"model": "openai/gpt-4o", "api_key": "k", "extra": {"a": 1}}
|
|
with warnings.catch_warnings():
|
|
warnings.simplefilter("ignore")
|
|
assert Router.generate_model_id("g", params) == deployment_id("g", params)
|
|
|
|
|
|
def test_router_cannot_be_subclassed():
|
|
with pytest.raises(TypeError, match=r"^litellm\.Router cannot be subclassed"):
|
|
|
|
class Sub(litellm.Router):
|
|
pass
|
|
|
|
|
|
def test_core_classes_can_still_be_subclassed():
|
|
from litellm.router_rust.core import RustCore
|
|
|
|
class RustSub(RustCore):
|
|
pass
|
|
|
|
assert issubclass(RustSub, RustCore)
|
|
|
|
|
|
def test_old_name_writes_and_deletes_warn(router):
|
|
with pytest.warns(DeprecationWarning, match=r"^Router\.fallbacks is deprecated"):
|
|
router.fallbacks = []
|
|
with pytest.warns(DeprecationWarning, match=r"^Router\.fallbacks is deprecated"):
|
|
del router.fallbacks
|
|
|
|
|
|
def test_alias_attribute_writes_the_same_storage(router):
|
|
with warnings.catch_warnings():
|
|
warnings.simplefilter("ignore")
|
|
replacement = {}
|
|
router._routing_groups = replacement
|
|
assert router.routing_groups is replacement
|
|
assert router.registry().routing_groups() is replacement
|
|
|
|
|
|
def test_wrong_arity_errors_still_name_router(router):
|
|
with warnings.catch_warnings():
|
|
warnings.simplefilter("ignore")
|
|
with pytest.raises(TypeError, match=r"^Router\.get_deployment\(\) takes 2 positional arguments"):
|
|
router.get_deployment("dep-a", "extra") # pyright: ignore[reportCallIssue]
|
|
with pytest.raises(TypeError, match=r"^Router\.get_available_guardrail\(\) missing 1 required"):
|
|
router.get_available_guardrail() # pyright: ignore[reportCallIssue]
|
|
with pytest.raises(TypeError, match=r"^litellm\.router\.Router\.has_model_id\(\) argument after \*\*"):
|
|
router.has_model_id(**None) # pyright: ignore[reportCallIssue, reportArgumentType]
|
|
with pytest.raises(TypeError, match=r"^Router\.is_deployment_blocked\(\) missing 1 required"):
|
|
router.is_deployment_blocked() # pyright: ignore[reportCallIssue]
|
|
|
|
|
|
def test_registry_reads_without_a_core_forward(router):
|
|
registry = router.registry()
|
|
assert registry.includes(router.registry().deployment("dep-a").model_dump(), team_id=None) is True
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"call, error",
|
|
[
|
|
(lambda r: r.registry().deployments(source="named"), TypeError),
|
|
(lambda r: r.registry().deployments("gpt", source="nope"), ValueError),
|
|
(lambda r: r.registry().index("nope"), ValueError),
|
|
(lambda r: r.registry().names(kind="litellm_models"), TypeError),
|
|
(lambda r: r.registry().names("gpt", kind="nope"), ValueError),
|
|
(lambda r: r.registry().resolve("team-gpt", kind="team_model"), TypeError),
|
|
(lambda r: r.registry().resolve(None, kind="alias"), TypeError),
|
|
(lambda r: r.registry().resolve("gpt", kind="nope"), ValueError),
|
|
(lambda r: r.registry().group("gpt", field="nope"), ValueError),
|
|
(lambda r: r.registry().credentials("dep-a", form="nope"), ValueError),
|
|
(lambda r: r.registry().plugins("nope"), ValueError),
|
|
(lambda r: asyncio.run(r.health().usage("gpt", kind="nope")), ValueError),
|
|
(lambda r: RouterSettings().as_dict(), AttributeError),
|
|
],
|
|
)
|
|
def test_entry_points_reject_unknown_kinds_and_missing_names(router, call, error):
|
|
with pytest.raises(error):
|
|
call(router)
|
|
|
|
|
|
def test_health_is_blocked_and_the_old_static_name_agree():
|
|
paused = Deployment(
|
|
model_name="m",
|
|
litellm_params={"model": "openai/gpt-4o-mini"}, # pyright: ignore[reportArgumentType]
|
|
model_info={"id": "d1", "blocked": True}, # pyright: ignore[reportArgumentType]
|
|
)
|
|
from litellm.router_api import RouterHealth
|
|
|
|
with warnings.catch_warnings():
|
|
warnings.simplefilter("ignore")
|
|
assert Router.is_deployment_blocked(paused) is RouterHealth.is_blocked(paused)
|
|
|
|
|
|
def test_old_members_refuse_objects_that_are_not_routers():
|
|
from litellm.router_deprecated import _as_router
|
|
|
|
with pytest.raises(TypeError, match=r"is not a litellm\.Router"):
|
|
_as_router(object()) # pyright: ignore[reportArgumentType]
|
|
|
|
|
|
def test_routing_context_forwards_to_the_router(router):
|
|
from litellm.router_api import RoutingContext
|
|
|
|
ctx = RoutingContext(router)
|
|
assert ctx.registry().version == router.registry().version
|
|
assert ctx.settings() is router.settings()
|
|
assert isinstance(ctx.health(), type(router.health()))
|
|
|
|
|
|
def test_attribute_errors_about_other_objects_pass_through_unchanged(router):
|
|
from litellm.router_deprecated import _public_attribute_error
|
|
|
|
other = AttributeError("'dict' object has no attribute 'x'", name="x", obj={})
|
|
assert _public_attribute_error(router, "_core_x", "x", other) is other
|
|
|
|
|
|
# Router-family helpers whose Router parameter became a RoutingContext keep the old spellings.
|
|
|
|
|
|
def test_renamed_router_keyword_still_works_and_warns(router):
|
|
from litellm.router_utils.router_callbacks.track_deployment_metrics import (
|
|
get_deployment_failures_for_current_minute,
|
|
increment_deployment_failures_for_current_minute,
|
|
)
|
|
|
|
deployment_id = router.registry().names(kind="ids")[0]
|
|
label = (
|
|
"litellm.router_utils.router_callbacks.track_deployment_metrics."
|
|
"increment_deployment_failures_for_current_minute"
|
|
)
|
|
before = DEPRECATED_ROUTER_MEMBER_USES[label]
|
|
|
|
with pytest.warns(DeprecationWarning, match=r"litellm_router_instance= is deprecated; pass ctx="):
|
|
increment_deployment_failures_for_current_minute(litellm_router_instance=router, deployment_id=deployment_id)
|
|
|
|
assert DEPRECATED_ROUTER_MEMBER_USES[label] == before + 1
|
|
with warnings.catch_warnings():
|
|
warnings.simplefilter("error")
|
|
assert get_deployment_failures_for_current_minute(ctx=router._core_ctx, deployment_id=deployment_id) == 1
|
|
|
|
|
|
def test_router_passed_where_the_context_goes_is_converted_with_a_warning(router):
|
|
from litellm.router_utils.cooldown_handlers import get_cooldown_deployments
|
|
|
|
with pytest.warns(DeprecationWarning, match="passing a Router is deprecated"):
|
|
assert get_cooldown_deployments(router, None) == []
|
|
with pytest.warns(DeprecationWarning, match="a Router as ctx= is deprecated"):
|
|
assert get_cooldown_deployments(ctx=router, parent_otel_span=None) == []
|
|
|
|
|
|
def test_renamed_router_keyword_alongside_the_new_one_is_rejected(router):
|
|
from litellm.router_utils.cooldown_handlers import get_cooldown_deployments
|
|
|
|
with pytest.raises(TypeError, match="got both litellm_router_instance= and ctx="):
|
|
get_cooldown_deployments(ctx=router._core_ctx, litellm_router_instance=router, parent_otel_span=None)
|
|
|
|
|
|
def test_renamed_router_param_maps_to_a_differently_named_context(router):
|
|
from litellm.router_api import RoutingContext
|
|
from litellm.router_deprecated import renamed_router_param
|
|
|
|
@renamed_router_param("litellm_router", new="litellm_routing_ctx")
|
|
def helper(*, litellm_routing_ctx: RoutingContext) -> RoutingContext:
|
|
return litellm_routing_ctx
|
|
|
|
with pytest.warns(DeprecationWarning, match="litellm_router= is deprecated; pass litellm_routing_ctx="):
|
|
assert helper(litellm_router=router) is router._core_ctx
|
|
with warnings.catch_warnings():
|
|
warnings.simplefilter("error")
|
|
assert helper(litellm_routing_ctx=router._core_ctx) is router._core_ctx
|
|
|
|
|
|
def test_renamed_router_attribute_reads_and_writes_the_context(router):
|
|
from litellm.router_utils.pre_call_checks.encrypted_content_affinity_check import (
|
|
EncryptedContentAffinityCheck,
|
|
)
|
|
|
|
check = EncryptedContentAffinityCheck(ctx=router._core_ctx)
|
|
with pytest.warns(DeprecationWarning, match=r"EncryptedContentAffinityCheck\.router: reading this attribute"):
|
|
assert check.router is router._core_ctx
|
|
|
|
other = _router()
|
|
with pytest.warns(DeprecationWarning, match="setting this attribute is deprecated; set .ctx"):
|
|
check.router = other
|
|
assert check.ctx is other._core_ctx
|
|
with warnings.catch_warnings():
|
|
warnings.simplefilter("ignore")
|
|
other.retire()
|