mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-10 08:28:25 +00:00
* feat(lite): offer each model group's reasoning efforts in pi, OpenCode and Codex lite pi, lite opencode and lite codex now read supported_reasoning_efforts from /model_group/info and write them into each client's model config. pi gets reasoning and a thinkingLevelMap, OpenCode gets reasoning_options plus one variant per effort, and Codex fallback entries get supported_reasoning_levels with a default level. The proxy-served Codex catalog on /v1/models?client_version= offers the same levels Codex before 0.138.0 fails the whole catalog on an effort it cannot name, so those releases, and an unreadable version, get only the six efforts every release parses. A group with unknown efforts keeps today's output, and a group with none is written as non-reasoning The strength order and nearest_declared_reasoning_effort move to an SDK-free module so the lite CLI, which loads without initializing the SDK, can import the Codex catalog * fix(lite): describe team models by their public name and keep Codex's none default /model_group/info now looks up a team's own deployments when the caller's team serves the requested name, so a team model reports its reasoning levels. Codex fallback entries default to none when the group lists it, and the proxy builds the Codex catalog in a worker thread * fix(lite): round Codex's default level down and describe a team's shared-name model from its own deployments Move the reasoning strength helper to litellm_core_utils so lite and the Codex catalog import it without the SDK or the router, and let make lint-format-check-changed diff the working tree so a staged rename passes. * fix(lite): keep pi's system role on reasoning model groups pi switches its system prompt to the developer role for any model marked reasoning, so an operator's cache_control_injection_points entry for role system stopped matching on Claude groups. lite pi now sets compat.supportsDeveloperRole false on those entries, so pi sends system as it did before. * fix(codex): treat a client_version too long to parse as an old Codex and keep undescribed stock levels A client_version past Python's integer digit limit made the Codex catalog answer 500. It now gets the levels every Codex parses. A stock Codex level with no description validates again, as on base, and lite codex treats undecodable codex --version output as an unknown version * fix(lite): keep REASONING_EFFORT_STRENGTH_ORDER importable from router_utils and skip the Codex sync on undecodable debug models output --------- Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
203 lines
11 KiB
Python
203 lines
11 KiB
Python
"""Stand-in Router entry points for tests that mock the router with a MagicMock."""
|
|
|
|
from types import SimpleNamespace
|
|
from typing import TypeVar
|
|
from unittest.mock import MagicMock, NonCallableMock
|
|
|
|
from litellm.router_api import SETTING_FIELDS, ApplyResult, ChangeSet, RouterSettings
|
|
|
|
__all__ = ["route_entry_points_to_stubs"]
|
|
|
|
_RouterDouble = TypeVar("_RouterDouble")
|
|
_NOT_GIVEN = object()
|
|
|
|
|
|
def route_entry_points_to_stubs(router: _RouterDouble) -> _RouterDouble:
|
|
"""Make a MagicMock router answer its entry points (``registry()``, ``settings()``,
|
|
``health()``) from per-query stubs set on the mock itself.
|
|
|
|
Tests stub one query by the name the router documents it under
|
|
(``router.get_model_list = MagicMock(...)``, ``router.model_list = [...]``,
|
|
``router.get_configured_display_name = ...``) and every caller sees that stub,
|
|
whichever entry point it reads through (``registry().deployments(...)``,
|
|
``registry().all()``, ``registry().group(name, field="display_name")``, ...). The
|
|
stubs are looked up at call time, so they can be set or replaced after this call.
|
|
The mapping is the one in litellm/router_deprecated.py.
|
|
|
|
A hand-written fake router (a plain class defining only the old members it needs) works
|
|
too: it is given mock entry points that read through to those members, and a member the
|
|
fake does not define raises AttributeError on the call that needs it, as it did before.
|
|
"""
|
|
for entry_point in ("registry", "health", "settings", "apply", "select"):
|
|
if not isinstance(getattr(router, entry_point, None), NonCallableMock):
|
|
object.__setattr__(router, entry_point, MagicMock()) # a frozen dataclass fake too
|
|
registry = router.registry.return_value
|
|
health = router.health.return_value
|
|
|
|
def _given(**kwargs):
|
|
# Pass on only the arguments a caller actually gave, the way the old call sites did.
|
|
return {k: v for k, v in kwargs.items() if v is not None and v is not False}
|
|
|
|
def _deployments(model_name=None, *, source="group", team_id=None, model_alias=None, request=None, call_type=None):
|
|
if source == "group":
|
|
return router.get_model_list(**_given(model_name=model_name, team_id=team_id))
|
|
if source == "all":
|
|
return router.get_all_deployments(model_name=model_name, **_given(model_alias=model_alias, team_id=team_id))
|
|
if source == "config":
|
|
return router.config_deployments()
|
|
if source == "alias":
|
|
return router.get_model_list_from_model_alias()
|
|
if source == "routing_groups":
|
|
return router.get_model_list_from_routing_groups(model_name=model_name)
|
|
if source == "routed_group":
|
|
return router.get_model_list_of_routed_group(model_name, **_given(team_id=team_id))
|
|
if source == "routable":
|
|
return router.get_routable_deployments(
|
|
model_name, *(() if request is None else (request,)), **_given(call_type=call_type)
|
|
)
|
|
if source == "first_in_group":
|
|
return router.get_deployment_by_model_group_name(model_group_name=model_name)
|
|
if source == "named":
|
|
return router.deployments_named(model_name)
|
|
if source == "wildcard":
|
|
return router.wildcard_route_deployments(model_name, team_id)
|
|
if source == "for_request":
|
|
return router.deployments_for_request(model_name, request or {})
|
|
raise AssertionError(f"unstubbed deployments source {source!r}")
|
|
|
|
def _names(model_name=None, *, kind="groups", team_id=None, exclude_team_models=False):
|
|
if kind == "groups":
|
|
return router.get_model_names(**_given(team_id=team_id))
|
|
if kind == "group_set":
|
|
return router.model_names
|
|
if kind == "ids":
|
|
return router.get_model_ids(**_given(model_name=model_name, exclude_team_models=exclude_team_models))
|
|
if kind == "deployment_names":
|
|
return router.deployment_names
|
|
if kind == "team_public":
|
|
return router.team_public_model_names
|
|
if kind == "litellm_models":
|
|
return router.resolved_litellm_models(model_name, **_given(team_id=team_id))
|
|
if kind == "route_candidates":
|
|
return router.get_candidate_model_ids_for_route(model_name, **_given(team_id=team_id))
|
|
if kind == "wildcard":
|
|
return router.wildcard_routed_models(model_name, team_id)
|
|
raise AssertionError(f"unstubbed names kind {kind!r}")
|
|
|
|
def _resolve(name, *, kind="group", team_id=None, custom_llm_provider=_NOT_GIVEN, served_model=None):
|
|
if kind == "group":
|
|
return router.routable_model_group(name)
|
|
if kind == "alias":
|
|
return router.get_model_from_alias(model=name)
|
|
if kind == "served":
|
|
return router.resolve_served_model_name(name, served_model)
|
|
if kind == "team_model":
|
|
return router.map_team_model(
|
|
name, team_id, custom_llm_provider=None if custom_llm_provider is _NOT_GIVEN else custom_llm_provider
|
|
)
|
|
if kind == "recognized":
|
|
return router.is_recognized_model(name)
|
|
if kind == "upstream":
|
|
return router.get_routable_upstream_model(name, **_given(team_id=team_id))
|
|
if kind == "search_tool_support":
|
|
return router.get_search_tool_support(name, **_given(team_id=team_id))
|
|
if kind == "from_id":
|
|
# The old call sites passed the provider positionally, exactly when the new ones pass it.
|
|
if custom_llm_provider is _NOT_GIVEN:
|
|
return router.resolve_model_name_from_model_id(name)
|
|
return router.resolve_model_name_from_model_id(name, custom_llm_provider)
|
|
if kind == "group_of_id":
|
|
return router.get_model_group(id=name)
|
|
if kind == "wildcard_access_group":
|
|
return router.is_model_access_group_for_wildcard_route(model_access_group=name)
|
|
if kind == "team_model_alternatives":
|
|
return router.team_model_has_alternatives(name)
|
|
if kind == "routing_group_alternatives":
|
|
return router.routing_group_has_alternatives(name)
|
|
raise AssertionError(f"unstubbed resolve kind {kind!r}")
|
|
|
|
def _group(name, *, field="info", include_hidden=False, team_id=None):
|
|
if field == "info":
|
|
return router.get_model_group_info(model_group=name, **_given(team_id=team_id))
|
|
if field == "display_name":
|
|
return router.get_configured_display_name(name)
|
|
if field == "service_tiers":
|
|
return router.get_configured_service_tiers(name, **_given(team_id=team_id))
|
|
if field == "listing_info":
|
|
return router.get_model_listing_info(name)
|
|
if field == "mode":
|
|
return router.get_configured_mode(name)
|
|
if field == "token_limits":
|
|
return router.get_configured_token_limits(name)
|
|
if field == "cached_info":
|
|
return router.cached_model_group_info(name)
|
|
raise AssertionError(f"unstubbed group field {field!r}")
|
|
|
|
registry.deployments.side_effect = _deployments
|
|
registry.names.side_effect = _names
|
|
registry.resolve.side_effect = _resolve
|
|
registry.group.side_effect = _group
|
|
registry.all.side_effect = lambda: router.model_list
|
|
registry.aliases.side_effect = lambda: router.model_group_alias
|
|
registry.deployment.side_effect = lambda id: router.get_deployment(model_id=id)
|
|
registry.deployment_dict.side_effect = lambda id: router.get_model_info(id=id)
|
|
registry.has_deployment.side_effect = lambda id: router.has_model_id(id)
|
|
registry.discovered_model_info.side_effect = lambda id: router.get_discovered_model_info(id)
|
|
|
|
def _credentials(id, *, form="params", **team):
|
|
# Old call sites passed team_id (even None) exactly when the new ones do, so pass it on as given.
|
|
if form == "params":
|
|
return router.get_deployment_credentials(model_id=id)
|
|
if form == "with_provider":
|
|
return router.get_deployment_credentials_with_provider(model_id=id, **team)
|
|
if form == "deployment":
|
|
return router.get_credential_deployment(model_id=id, **team)
|
|
raise AssertionError(f"unstubbed credentials form {form!r}")
|
|
|
|
registry.credentials.side_effect = _credentials
|
|
registry.access_groups.side_effect = lambda **kwargs: router.get_model_access_groups(**_given(**kwargs))
|
|
registry.plugins.side_effect = lambda kind: getattr(router, f"{kind}_routers")
|
|
health.fully_blocked_model_names.side_effect = lambda: router.get_fully_blocked_model_names()
|
|
health.split_priority.side_effect = lambda model, kwargs: router.split_request_priority(model=model, kwargs=kwargs)
|
|
registry.index.side_effect = lambda name: getattr(router, f"{name}_to_deployment_indices")
|
|
registry.patterns.side_effect = lambda team=False: router.team_pattern_routers if team else router.pattern_router
|
|
health.is_blocked.side_effect = lambda deployment: router.is_deployment_blocked(deployment)
|
|
health.all_blocked.side_effect = lambda deployments: router.are_all_deployments_blocked(deployments)
|
|
settings_backend = SimpleNamespace(
|
|
_core_get_settings=lambda: router.get_settings(),
|
|
_core_get_available_guardrail=lambda name: router.get_available_guardrail(guardrail_name=name),
|
|
_core_get_allowed_fails_from_policy=lambda exception: router.get_allowed_fails_from_policy(exception=exception),
|
|
)
|
|
router.settings.side_effect = lambda: RouterSettings(
|
|
**{name: getattr(router, name) for name in SETTING_FIELDS}, _router=settings_backend
|
|
)
|
|
|
|
def _apply(changes: ChangeSet) -> ApplyResult:
|
|
for name, value in changes.assign.items():
|
|
setattr(router, name, value)
|
|
if changes.settings:
|
|
router.update_settings(**changes.settings)
|
|
upserted = [router.upsert_deployment(deployment=d) for d in changes.upsert]
|
|
deleted = [router.delete_deployment(id=i) for i in changes.delete]
|
|
if changes.redis_cache is not None:
|
|
router.update_redis_cache(cache=changes.redis_cache)
|
|
return ApplyResult(version=0, upserted=upserted, deleted=deleted)
|
|
|
|
router.apply.side_effect = _apply
|
|
|
|
async def _select(model, request_kwargs=None, *, kind="pick", **extra):
|
|
given = _given(**extra)
|
|
if kind == "pick":
|
|
return await router.async_get_available_deployment(
|
|
model=model, request_kwargs=request_kwargs if request_kwargs is not None else {}, **given
|
|
)
|
|
if kind == "healthy":
|
|
return await router.async_get_healthy_deployments(
|
|
model=model, request_kwargs=request_kwargs if request_kwargs is not None else {}, **given
|
|
)
|
|
if kind == "pass_through":
|
|
return router.get_available_deployment_for_pass_through(model=model, request_kwargs=request_kwargs, **given)
|
|
raise AssertionError(f"unstubbed select kind {kind!r}")
|
|
|
|
router.select.side_effect = _select
|
|
return router
|