mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-11 03:38:38 +00:00
refactor(types): move complexity router request models next to their base config (#45780)
* refactor(types): move complexity router request models next to their base config Part of LIT-9423 * test(types): cover the old auto-router import path for the moved request models Part of LIT-9423
This commit is contained in:
parent
4606bf532e
commit
0fc6d4d16d
9 changed files with 149 additions and 119 deletions
|
|
@ -58,6 +58,10 @@ from litellm.repositories.team_repository import TeamRepository
|
|||
from litellm.repositories.user_repository import UserRepository
|
||||
from litellm.repositories.verification_token_repository import VerificationTokenRepository
|
||||
from litellm.router_strategy.complexity_router import ComplexityRouter
|
||||
from litellm.router_strategy.complexity_router.request_models import (
|
||||
AutoRouterRoutingTestRequest,
|
||||
RequestComplexityRouterConfig,
|
||||
)
|
||||
from litellm.router_utils.auto_router_model_naming import (
|
||||
StrategyRouterDependencyRole,
|
||||
classify_strategy_router_model,
|
||||
|
|
@ -73,12 +77,10 @@ from litellm.types.management_endpoints.auto_router_endpoints import (
|
|||
AutoRouterBenchmarkTotals,
|
||||
AutoRouterCacheBucket,
|
||||
AutoRouterCacheStats,
|
||||
AutoRouterRoutingTestRequest,
|
||||
AutoRouterRoutingTestResponse,
|
||||
AutoRouterSessionResponse,
|
||||
ComplexityRouterConfigValidationRequest,
|
||||
ComplexityRouterConfigValidationResponse,
|
||||
RequestComplexityRouterConfig,
|
||||
ShadowEvalDirection,
|
||||
ShadowEvalJobResponse,
|
||||
ShadowEvalJobTargetResponse,
|
||||
|
|
|
|||
|
|
@ -38,9 +38,9 @@ from litellm.router_strategy.complexity_router.config import (
|
|||
ComplexityRouterConfigWrite,
|
||||
resolve_complexity_router_config_write,
|
||||
)
|
||||
from litellm.router_strategy.complexity_router.request_models import RequestComplexityRouterConfig
|
||||
from litellm.router_utils.auto_router_model_naming import classify_strategy_router_model, strategy_router_dependencies
|
||||
from litellm.types.llms.base import LiteLLMBaseModel
|
||||
from litellm.types.management_endpoints.auto_router_endpoints import RequestComplexityRouterConfig
|
||||
from litellm.types.router import Deployment, updateDeployment
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
|
|||
116
litellm/router_strategy/complexity_router/request_models.py
Normal file
116
litellm/router_strategy/complexity_router/request_models.py
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
from collections.abc import Mapping, Sequence
|
||||
from types import MappingProxyType
|
||||
|
||||
from pydantic import Field, field_validator, model_validator
|
||||
|
||||
from litellm.router_strategy.complexity_router.config import ComplexityRouterConfig
|
||||
from litellm.types.llms.base import LiteLLMBaseModel
|
||||
from litellm.types.management_endpoints.auto_router_endpoints import DEFAULT_ROUTING_TEST_ROUTER_NAME
|
||||
|
||||
|
||||
class RequestComplexityRouterConfig(ComplexityRouterConfig):
|
||||
"""The part of a complexity-router config a request can carry.
|
||||
|
||||
`plugins` holds live RoutingPlugin objects, which no JSON body can express and which have no
|
||||
OpenAPI schema, so it is closed off here rather than left as an arbitrary-type field.
|
||||
"""
|
||||
|
||||
plugins: None = Field(default=None, description="Not settable over HTTP; routing plugins are runtime objects")
|
||||
classifier_plugin: None = Field( # pyright: ignore[reportIncompatibleVariableOverride] # narrowing to None is the point: runtime objects are not settable over HTTP
|
||||
default=None, description="Not settable over HTTP; the classifier plugin is a runtime object"
|
||||
)
|
||||
|
||||
|
||||
class AutoRouterRoutingTestRequest(LiteLLMBaseModel):
|
||||
"""A single request to classify against a complexity-router config that need not be saved yet.
|
||||
|
||||
Carries the same fields the serving path carries, so a dry run classifies what a real turn
|
||||
would classify. `messages`, `system` and `tools` are forwarded to the routing hook untranslated,
|
||||
which is why they are typed loosely: the hook reads whatever dialect the surface produced, and
|
||||
validating them against one surface's schema would reject the others.
|
||||
"""
|
||||
|
||||
prompt: str | None = Field(
|
||||
default=None,
|
||||
description="A single ask to route, as an end user would send it. Mutually exclusive with messages",
|
||||
)
|
||||
messages: Sequence[Mapping[str, object]] | None = Field(
|
||||
default=None,
|
||||
description="The full message list to route, exactly as the serving path would receive it. Mutually exclusive with prompt",
|
||||
)
|
||||
system: str | Sequence[Mapping[str, object]] | None = Field(
|
||||
default=None,
|
||||
description="The top-level system prompt an Anthropic /v1/messages body carries beside its messages",
|
||||
)
|
||||
tools: Sequence[Mapping[str, object]] | None = Field(
|
||||
default=None,
|
||||
description="The tool definitions the request advertises, which decide whether the plan-mode floor applies",
|
||||
)
|
||||
complexity_router_config: RequestComplexityRouterConfig = Field(
|
||||
description="The complexity router config to route against, in the shape /model/new accepts",
|
||||
)
|
||||
saved_model_id: str | None = Field(
|
||||
default=None,
|
||||
min_length=1,
|
||||
description="Test this saved deployment's server-side configuration instead of the supplied config and default model",
|
||||
)
|
||||
default_model: str | None = Field(
|
||||
default=None,
|
||||
description="Model to route to when no tier resolves, i.e. complexity_router_default_model",
|
||||
)
|
||||
router_name: str = Field(
|
||||
default=DEFAULT_ROUTING_TEST_ROUTER_NAME,
|
||||
description="Name reported as the router in the routing decision. Display only",
|
||||
)
|
||||
team_id: str | None = Field(
|
||||
default=None,
|
||||
description="Team the router is being created for. Required for a team admin, who may only test their own team's routers",
|
||||
)
|
||||
|
||||
@field_validator("messages")
|
||||
@classmethod
|
||||
def _reject_messages_no_surface_accepts(
|
||||
cls, value: Sequence[Mapping[str, object]] | None
|
||||
) -> Sequence[Mapping[str, object]] | None:
|
||||
"""Reject what every supported surface rejects, and nothing beyond it.
|
||||
|
||||
A real request carrying a message with no string role, or with content that is neither text
|
||||
nor a block list, is a 400 on the serving path, so answering it here with a routed tier
|
||||
would promise a decision the request never gets. Only the two keys the dialects agree on
|
||||
are constrained: anything else in a message stays untranslated and unread.
|
||||
"""
|
||||
if value is None:
|
||||
return value
|
||||
for index, message in enumerate(value):
|
||||
if not isinstance(role := message.get("role"), str) or not role.strip():
|
||||
raise ValueError(f"messages[{index}] needs a non-empty string role")
|
||||
if (content := message.get("content")) is not None and not isinstance(content, str | list):
|
||||
raise ValueError(f"messages[{index}] content must be a string, a list of blocks, or null")
|
||||
return value
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _resolve_request_carrier(self) -> "AutoRouterRoutingTestRequest":
|
||||
if self.prompt is not None and not self.prompt.strip():
|
||||
raise ValueError("prompt must not be blank")
|
||||
if self.messages is not None and not self.messages:
|
||||
raise ValueError("messages must not be empty")
|
||||
if (self.prompt is None) == (self.messages is None):
|
||||
raise ValueError("provide exactly one of prompt or messages")
|
||||
if self.messages is not None:
|
||||
return self
|
||||
return self.model_copy(update={"messages": [{"role": "user", "content": self.prompt}]})
|
||||
|
||||
def wire_body(self) -> Mapping[str, object]:
|
||||
"""The request kwargs a serving-path request would carry for this body.
|
||||
|
||||
Every value is handed out by identity rather than copied, so the messages the routing hook
|
||||
classifies and the messages its raw-body plan-mode scan reads are one value, as they are on
|
||||
the serving path.
|
||||
"""
|
||||
return MappingProxyType(
|
||||
{
|
||||
key: value
|
||||
for key, value in (("messages", self.messages), ("system", self.system), ("tools", self.tools))
|
||||
if value is not None
|
||||
}
|
||||
)
|
||||
|
|
@ -2,31 +2,20 @@
|
|||
Types for auto-router management endpoints
|
||||
"""
|
||||
|
||||
from collections.abc import Mapping, Sequence
|
||||
import importlib
|
||||
from collections.abc import Mapping
|
||||
from datetime import datetime, timezone
|
||||
from types import MappingProxyType
|
||||
from typing import Final, Literal, TypeAlias
|
||||
|
||||
from pydantic import Field, computed_field, field_validator, model_validator
|
||||
|
||||
from litellm.router_strategy.complexity_router.config import ComplexityRouterConfig
|
||||
from litellm.types.llms.base import LiteLLMBaseModel
|
||||
from litellm.types.utils import StandardLoggingRoutingDecision
|
||||
|
||||
DEFAULT_ROUTING_TEST_ROUTER_NAME: Final[str] = "auto_router_routing_test"
|
||||
|
||||
|
||||
class RequestComplexityRouterConfig(ComplexityRouterConfig):
|
||||
"""The part of a complexity-router config a request can carry.
|
||||
|
||||
`plugins` holds live RoutingPlugin objects, which no JSON body can express and which have no
|
||||
OpenAPI schema, so it is closed off here rather than left as an arbitrary-type field.
|
||||
"""
|
||||
|
||||
plugins: None = Field(default=None, description="Not settable over HTTP; routing plugins are runtime objects")
|
||||
classifier_plugin: None = Field( # pyright: ignore[reportIncompatibleVariableOverride] # narrowing to None is the point: runtime objects are not settable over HTTP
|
||||
default=None, description="Not settable over HTTP; the classifier plugin is a runtime object"
|
||||
)
|
||||
_REQUEST_MODELS_MODULE: Final = "litellm.router_strategy.complexity_router.request_models"
|
||||
_REQUEST_MODELS_EXPORTS: Final = frozenset({"RequestComplexityRouterConfig", "AutoRouterRoutingTestRequest"})
|
||||
|
||||
|
||||
class ComplexityRouterConfigValidationRequest(LiteLLMBaseModel):
|
||||
|
|
@ -64,101 +53,6 @@ class AutoRouterAvailabilityResponse(LiteLLMBaseModel):
|
|||
error: str | None = None
|
||||
|
||||
|
||||
class AutoRouterRoutingTestRequest(LiteLLMBaseModel):
|
||||
"""A single request to classify against a complexity-router config that need not be saved yet.
|
||||
|
||||
Carries the same fields the serving path carries, so a dry run classifies what a real turn
|
||||
would classify. `messages`, `system` and `tools` are forwarded to the routing hook untranslated,
|
||||
which is why they are typed loosely: the hook reads whatever dialect the surface produced, and
|
||||
validating them against one surface's schema would reject the others.
|
||||
"""
|
||||
|
||||
prompt: str | None = Field(
|
||||
default=None,
|
||||
description="A single ask to route, as an end user would send it. Mutually exclusive with messages",
|
||||
)
|
||||
messages: Sequence[Mapping[str, object]] | None = Field(
|
||||
default=None,
|
||||
description="The full message list to route, exactly as the serving path would receive it. Mutually exclusive with prompt",
|
||||
)
|
||||
system: str | Sequence[Mapping[str, object]] | None = Field(
|
||||
default=None,
|
||||
description="The top-level system prompt an Anthropic /v1/messages body carries beside its messages",
|
||||
)
|
||||
tools: Sequence[Mapping[str, object]] | None = Field(
|
||||
default=None,
|
||||
description="The tool definitions the request advertises, which decide whether the plan-mode floor applies",
|
||||
)
|
||||
complexity_router_config: RequestComplexityRouterConfig = Field(
|
||||
description="The complexity router config to route against, in the shape /model/new accepts",
|
||||
)
|
||||
saved_model_id: str | None = Field(
|
||||
default=None,
|
||||
min_length=1,
|
||||
description="Test this saved deployment's server-side configuration instead of the supplied config and default model",
|
||||
)
|
||||
default_model: str | None = Field(
|
||||
default=None,
|
||||
description="Model to route to when no tier resolves, i.e. complexity_router_default_model",
|
||||
)
|
||||
router_name: str = Field(
|
||||
default=DEFAULT_ROUTING_TEST_ROUTER_NAME,
|
||||
description="Name reported as the router in the routing decision. Display only",
|
||||
)
|
||||
team_id: str | None = Field(
|
||||
default=None,
|
||||
description="Team the router is being created for. Required for a team admin, who may only test their own team's routers",
|
||||
)
|
||||
|
||||
@field_validator("messages")
|
||||
@classmethod
|
||||
def _reject_messages_no_surface_accepts(
|
||||
cls, value: Sequence[Mapping[str, object]] | None
|
||||
) -> Sequence[Mapping[str, object]] | None:
|
||||
"""Reject what every supported surface rejects, and nothing beyond it.
|
||||
|
||||
A real request carrying a message with no string role, or with content that is neither text
|
||||
nor a block list, is a 400 on the serving path, so answering it here with a routed tier
|
||||
would promise a decision the request never gets. Only the two keys the dialects agree on
|
||||
are constrained: anything else in a message stays untranslated and unread.
|
||||
"""
|
||||
if value is None:
|
||||
return value
|
||||
for index, message in enumerate(value):
|
||||
if not isinstance(role := message.get("role"), str) or not role.strip():
|
||||
raise ValueError(f"messages[{index}] needs a non-empty string role")
|
||||
if (content := message.get("content")) is not None and not isinstance(content, str | list):
|
||||
raise ValueError(f"messages[{index}] content must be a string, a list of blocks, or null")
|
||||
return value
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _resolve_request_carrier(self) -> "AutoRouterRoutingTestRequest":
|
||||
if self.prompt is not None and not self.prompt.strip():
|
||||
raise ValueError("prompt must not be blank")
|
||||
if self.messages is not None and not self.messages:
|
||||
raise ValueError("messages must not be empty")
|
||||
if (self.prompt is None) == (self.messages is None):
|
||||
raise ValueError("provide exactly one of prompt or messages")
|
||||
if self.messages is not None:
|
||||
return self
|
||||
return self.model_copy(update={"messages": [{"role": "user", "content": self.prompt}]})
|
||||
|
||||
def wire_body(self) -> Mapping[str, object]:
|
||||
"""The request kwargs a serving-path request would carry for this body.
|
||||
|
||||
Every value is handed out by identity rather than copied, so the messages the routing hook
|
||||
classifies and the messages its raw-body plan-mode scan reads are one value, as they are on
|
||||
the serving path.
|
||||
"""
|
||||
return MappingProxyType(
|
||||
{
|
||||
key: value
|
||||
for key, value in (("messages", self.messages), ("system", self.system), ("tools", self.tools))
|
||||
if value is not None
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class AutoRouterRoutingTestResponse(LiteLLMBaseModel):
|
||||
"""Where one prompt would have been routed, and why."""
|
||||
|
||||
|
|
@ -765,3 +659,9 @@ class ShadowEvalJobResponse(LiteLLMBaseModel):
|
|||
if all(target.stopped_at is not None for target in self.targets):
|
||||
return "stopped"
|
||||
return "running"
|
||||
|
||||
|
||||
def __getattr__(name: str) -> object:
|
||||
if name in _REQUEST_MODELS_EXPORTS:
|
||||
return getattr(importlib.import_module(_REQUEST_MODELS_MODULE), name)
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
|
|
|||
|
|
@ -36,10 +36,8 @@ from litellm.router_strategy.complexity_router.jev_classifier import (
|
|||
JevClassifierClient,
|
||||
JevSystemOneResponse,
|
||||
)
|
||||
from litellm.types.management_endpoints.auto_router_endpoints import (
|
||||
AutoRouterBenchmarksResponse,
|
||||
AutoRouterRoutingTestRequest,
|
||||
)
|
||||
from litellm.router_strategy.complexity_router.request_models import AutoRouterRoutingTestRequest
|
||||
from litellm.types.management_endpoints.auto_router_endpoints import AutoRouterBenchmarksResponse
|
||||
from litellm.types.router import Deployment
|
||||
from litellm.types.utils import Choices, Message, ModelResponse
|
||||
|
||||
|
|
|
|||
|
|
@ -264,7 +264,7 @@ async def test_member_authorization_and_persistence_resolve_the_same_classifier(
|
|||
_strategy_router_write_violation,
|
||||
update_db_model,
|
||||
)
|
||||
from litellm.types.management_endpoints.auto_router_endpoints import RequestComplexityRouterConfig
|
||||
from litellm.router_strategy.complexity_router.request_models import RequestComplexityRouterConfig
|
||||
|
||||
monkeypatch.setenv("LITELLM_SALT_KEY", "member-router-test-salt")
|
||||
stored_config: Final = {
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ from litellm.types.router import (
|
|||
TaggedPreRoutingStrategy,
|
||||
)
|
||||
from litellm.types.llms.openai import ResponsesAPIResponse
|
||||
from litellm.types.management_endpoints.auto_router_endpoints import RequestComplexityRouterConfig
|
||||
from litellm.router_strategy.complexity_router.request_models import RequestComplexityRouterConfig
|
||||
from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler
|
||||
|
||||
|
||||
|
|
|
|||
0
tests/unit/types/management_endpoints/__init__.py
Normal file
0
tests/unit/types/management_endpoints/__init__.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import pytest
|
||||
|
||||
from litellm.router_strategy.complexity_router import request_models
|
||||
from litellm.types.management_endpoints import auto_router_endpoints
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", ["RequestComplexityRouterConfig", "AutoRouterRoutingTestRequest"])
|
||||
def test_old_import_path_returns_the_moved_request_models(name: str) -> None:
|
||||
assert getattr(auto_router_endpoints, name) is getattr(request_models, name)
|
||||
|
||||
|
||||
def test_old_import_path_still_raises_for_unknown_attributes() -> None:
|
||||
with pytest.raises(AttributeError):
|
||||
getattr(auto_router_endpoints, "NotARequestModel")
|
||||
Loading…
Add table
Reference in a new issue