fix(router): let an exact base_model widen a deployment's context window (#46197)

* fix(router): honor explicit bedrock base model capacity

* fix(router): honor an exact base_model's capacity for every non-Azure provider

Extends the Bedrock-only check to every non-Azure provider and only uses the
base_model when it has its own exact cost-map entry, so an unmapped or wildcard
base_model falls back to the route id instead of raising in the router.

* test(router): call _maps_to_exact_model_info directly in its cost-map tests

* fix(router): let an exact base_model only widen a deployment's context window

A base_model used as a pricing stand-in with a smaller window no longer narrows the route's limit, and a route with no window never takes one from base_model

* test(router): count the base_model window helpers as covered through get_router_model_info

* fix(router): widen only from a model_info base_model naming the same model

A stand-in base_model for a different, larger model widened a smaller
deployment's window, so pre-call checks sent it prompts the provider
rejects with 400. The window now widens only when model_info.base_model
names the same provider model id as the route, which is also the field
/model/info reads.

* fix(router): match a base_model's model id across Bedrock region prefixes and @ qualifiers

* fix(router): read litellm_params.base_model too and look the base row up as written

* test(router): cover a same-model base_model window on chat, messages, responses, and complexity escalation

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
This commit is contained in:
Michelangelo Setaro 2026-10-16 08:37:40 +02:00 • committed by GitHub
parent c8b73cde84
commit c288dc7da0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 592 additions and 130 deletions

View file

@ -128,6 +128,7 @@ from litellm.llms.base_llm.vector_store.transformation import (
RouterVectorStoreEmbeddingExecutor,
vector_store_request_metadata,
)
from litellm.llms.bedrock.common_utils import get_bedrock_base_model
from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, get_async_httpx_client
from litellm.llms.openai_like.json_loader import JSONProviderRegistry
from litellm.llms.openai_like.model_info import (
@ -374,6 +375,7 @@ from litellm.utils import (
get_non_default_completion_params,
get_secret,
get_utc_datetime,
is_generalized_model_info,
is_region_allowed,
provider_rejectable_params,
set_live_deployment_replay,
@ -991,6 +993,49 @@ def as_output_cap(value: object) -> int | None:
return cap if cap >= 0 else None
def _exact_base_model_max_input_tokens(base_model: str) -> int | None:
try:
model_info: Final = litellm.get_model_info(model=base_model)
except Exception: # noqa: BLE001 # get_model_info raises ModelNotMappedError or a bare Exception for an unmapped model
return None
if is_generalized_model_info(model_info):
return None
return model_info.get("max_input_tokens")
def _provider_model_id(model_name: str, custom_llm_provider: str) -> str:
model_id: Final = model_name.rsplit("/", 1)[-1]
return get_bedrock_base_model(model_id) if custom_llm_provider.startswith("bedrock") else model_id
def _names_route_model(base_model: str, route_key: str, custom_llm_provider: str) -> bool:
route_model_id: Final = _provider_model_id(route_key, custom_llm_provider)
base_model_id: Final = _provider_model_id(base_model, custom_llm_provider)
return route_model_id in (base_model_id, base_model_id.split("@", 1)[0])
def _declared_base_model(user_model_info: Mapping[str, object]) -> str | None:
match user_model_info:
case {"base_model": str() as base_model}:
return base_model
case _:
return None
def _base_model_widened_window(
route_model_info: ModelMapInfo, base_model: str | None, custom_llm_provider: str
) -> Mapping[str, int]:
route_max_input_tokens: Final = route_model_info.get("max_input_tokens")
if base_model is None or route_max_input_tokens is None:
return MappingProxyType({})
if not _names_route_model(base_model, route_model_info["key"], custom_llm_provider):
return MappingProxyType({})
base_max_input_tokens: Final = _exact_base_model_max_input_tokens(base_model)
if base_max_input_tokens is None or base_max_input_tokens <= route_max_input_tokens:
return MappingProxyType({})
return MappingProxyType({"max_input_tokens": base_max_input_tokens})
class Router:
@property
def _routing_groups(self) -> dict[str, RoutingGroup]:
@ -11343,6 +11388,7 @@ class Router:
Augment litellm info with additional params set in `model_info`.
For azure models, ignore the `model:`. Only set max tokens, cost values if base_model is set.
For every other provider, a base_model naming the same model as `model:` can only widen its context window.
Returns
- ModelInfo - If found -> typed dict with max tokens, input cost, etc.
@ -11439,20 +11485,23 @@ class Router:
## CHECK USER SET MODEL INFO
raw_user_model_info: Final = deployment.get("model_info")
user_model_info: Final = (
user_model_info: Final = _MODEL_INFO_ADAPTER.validate_python(
raw_user_model_info.model_dump(exclude_none=True)
if isinstance(raw_user_model_info, BaseModel)
else raw_user_model_info
else raw_user_model_info or {}
)
# get_model_info() hands back an lru_cache'd dict, so merge into a copy; unset
# values are skipped or Deployment's None pricing defaults would erase the map's
merged_model_info: Final[ModelMapInfo] = {
**copy.deepcopy(model_info),
**self.get_discovered_model_info((deployment.get("model_info") or {}).get("id")),
**MappingProxyType(
{key: value for key, value in (user_model_info or MappingProxyType({})).items() if value is not None}
**_base_model_widened_window(
model_info,
_declared_base_model(user_model_info) or _declared_base_model(litellm_params.model_extra or {}),
custom_llm_provider,
),
**self.get_discovered_model_info((deployment.get("model_info") or {}).get("id")),
**MappingProxyType({key: value for key, value in user_model_info.items() if value is not None}),
}
return merged_model_info

View file

@ -114,6 +114,11 @@ ignored_function_names = [
"_deployment_that_just_failed", # Tested through the same-boundary hop test in test_router_order_fallback.py
"_collect_ptu_share_teams", # Cached behind team_ptu_ceiling, tested through the live model list test in test_router.py
"_resolve_team_ptu_ceiling", # Cached behind team_ptu_ceiling, tested through the live model list test in test_router.py
"_exact_base_model_max_input_tokens", # Tested through get_router_model_info in test_router_model_cost_isolation.py
"_base_model_widened_window", # Tested through get_router_model_info in test_router_model_cost_isolation.py
"_provider_model_id", # Tested through get_router_model_info in test_router_model_cost_isolation.py
"_names_route_model", # Tested through get_router_model_info in test_router_model_cost_isolation.py
"_declared_base_model", # Tested through get_router_model_info in test_router_model_cost_isolation.py
]

View file

@ -0,0 +1,171 @@
import json
import uuid
from collections.abc import Iterator, Mapping
from dataclasses import dataclass
from typing import Final
import pytest
from integration._support.client import gateway_from_environment
from integration._support.process import OwnedProxy, graceful_stop_seconds, owned_proxy_process
from integration._support.wire import Reply, Request, Wire, wire_server
from integration.providers._cache_control_marks_support import marker_of, owned_config
from pydantic import JsonValue
_TOKEN: Final = "synthetic-bedrock-bearer"
_KIMI: Final = "moonshotai.kimi-k2.5"
_SONNET: Final = "global.anthropic.claude-sonnet-4-6"
_KIMI_BASE: Final = f"bedrock/us-east-1/{_KIMI}"
_KIMI_TARGET: Final = f"/model/{_KIMI}/converse"
_SONNET_TARGET: Final = f"/model/{_SONNET}/converse"
_PAST_ROUTE_ROW_LINES: Final = 16250
_PAST_ESCALATION_BUFFER_LINES: Final = 15480
_SMALL_LINES: Final = 1300
_REPLY: Final = json.dumps(
{
"output": {"message": {"role": "assistant", "content": [{"text": "8115"}]}},
"stopReason": "end_turn",
"usage": {"inputTokens": 9, "outputTokens": 2, "totalTokens": 11},
"metrics": {"latencyMs": 1},
}
).encode()
@dataclass(frozen=True, slots=True)
class _Rig:
owned: OwnedProxy
wire: Wire
def _converse_peer(request: Request) -> Reply:
assert request.headers["authorization"] == f"Bearer {_TOKEN}", request.headers
return Reply(body=_REPLY)
def _deployment(
name: str,
model: str,
api_base: str,
*,
model_info: Mapping[str, JsonValue] | None = None,
**params: JsonValue,
) -> dict[str, JsonValue]:
return {
"model_name": name,
"litellm_params": {
"model": f"bedrock/converse/{model}",
"api_key": _TOKEN,
"aws_region_name": "us-east-1",
"aws_bedrock_runtime_endpoint": api_base,
**params,
},
"model_info": {"id": f"{name}-{uuid.uuid4().hex}", **(model_info or {})},
}
@pytest.fixture(scope="module")
def rig(tmp_path_factory: pytest.TempPathFactory) -> Iterator[_Rig]:
directory: Final = tmp_path_factory.mktemp("base-model-window")
with gateway_from_environment() as gateway, wire_server(_converse_peer) as wire:
config: Final = owned_config(
directory,
[
_deployment("base-in-model-info", _KIMI, wire.url, model_info={"base_model": _KIMI_BASE}),
_deployment("base-in-litellm-params", _KIMI, wire.url, base_model=_KIMI_BASE),
_deployment("no-base", _KIMI, wire.url),
_deployment("other-model-base", _KIMI, wire.url, model_info={"base_model": _SONNET}),
_deployment(
"declared-window",
_KIMI,
wire.url,
model_info={"base_model": _KIMI_BASE, "max_input_tokens": 257000},
),
_deployment(
"unmapped-base", _KIMI, wire.url, model_info={"base_model": f"{_KIMI_BASE}-not-in-cost-map"}
),
_deployment(
"smaller-other-model-base",
_KIMI,
wire.url,
model_info={"base_model": "bedrock/us-east-1/meta.llama3-8b-instruct-v1:0"},
),
_deployment("one-million", _SONNET, wire.url),
{
"model_name": "complexity",
"litellm_params": {
"model": "auto_router/complexity_router",
"complexity_router_config": {
"tiers": {
"SIMPLE": "base-in-model-info",
"MEDIUM": "base-in-model-info",
"COMPLEX": "one-million",
"REASONING": "one-million",
},
"enable_context_window_escalation": True,
},
},
},
],
litellm_settings={"cache": False},
router_settings={"enable_pre_call_checks": True},
)
with owned_proxy_process(gateway, directory, {}, config=config, workers=2, client_timeout=120) as owned:
yield _Rig(owned, wire)
def _prompt(lines: int, marker: str) -> str:
listing: Final = "\n".join(f"Line {i}: the value of item {i} is {(i * 7) % 1000}." for i in range(1, lines + 1))
return f"{listing}\n\nWhat is the value of item 12345? Answer with the number only. marker-{marker}"
def _request(surface: str, model: str, text: str) -> tuple[str, dict[str, JsonValue]]:
if surface == "messages":
return "/v1/messages", {"model": model, "max_tokens": 16, "messages": [{"role": "user", "content": text}]}
if surface == "responses":
return "/v1/responses", {"model": model, "max_output_tokens": 16, "input": text}
return "/v1/chat/completions", {"model": model, "max_tokens": 16, "messages": [{"role": "user", "content": text}]}
def _send(rig: _Rig, surface: str, model: str, lines: int) -> tuple[int, str, tuple[str, ...]]:
marker: Final = uuid.uuid4().hex
path, body = _request(surface, model, _prompt(lines, marker))
response: Final = rig.owned.gateway.request("POST", path, body)
targets: Final = tuple(request.target for request in rig.wire.drain() if marker_of(request) == marker)
return response.status_code, response.text, targets
@pytest.mark.timeout(2 * graceful_stop_seconds() + 120)
@pytest.mark.parametrize("surface", ["chat", "messages", "responses"])
def test_a_same_model_base_model_row_widens_the_window_pre_call_checks_enforce(rig: _Rig, surface: str) -> None:
status, text, targets = _send(rig, surface, "base-in-model-info", _PAST_ROUTE_ROW_LINES)
assert status == 200, text
assert targets == (_KIMI_TARGET,)
@pytest.mark.timeout(2 * graceful_stop_seconds() + 120)
def test_a_same_model_base_model_in_litellm_params_widens_the_window_too(rig: _Rig) -> None:
status, text, targets = _send(rig, "chat", "base-in-litellm-params", _PAST_ROUTE_ROW_LINES)
assert status == 200, text
assert targets == (_KIMI_TARGET,)
@pytest.mark.timeout(2 * graceful_stop_seconds() + 120)
@pytest.mark.parametrize("model", ["no-base", "other-model-base", "declared-window", "unmapped-base"])
def test_a_base_model_that_cannot_widen_keeps_the_route_window_and_calls_no_upstream(rig: _Rig, model: str) -> None:
status, text, targets = _send(rig, "chat", model, _PAST_ROUTE_ROW_LINES)
assert status == 400, text
assert "ContextWindowExceededError" in text, text
assert targets == ()
@pytest.mark.timeout(2 * graceful_stop_seconds() + 120)
def test_a_smaller_other_model_base_model_never_narrows_the_route_window(rig: _Rig) -> None:
status, text, targets = _send(rig, "chat", "smaller-other-model-base", _SMALL_LINES)
assert status == 200, text
assert targets == (_KIMI_TARGET,)
@pytest.mark.timeout(2 * graceful_stop_seconds() + 120)
def test_complexity_router_keeps_a_prompt_on_the_tier_whose_base_model_row_holds_it(rig: _Rig) -> None:
status, text, targets = _send(rig, "chat", "complexity", _PAST_ESCALATION_BUFFER_LINES)
assert status == 200, text
assert targets == (_KIMI_TARGET,)

View file

@ -119,27 +119,33 @@ async def test_discovered_limits_survive_deployment_growth_and_removal(
async def test_discovery_discards_metadata_for_a_replaced_deployment(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(litellm, "model_cost", copy.deepcopy(litellm.model_cost))
router: Final = Router(model_list=[{
"model_name": "local",
"litellm_params": {
"model": "hosted_vllm/local-model",
"api_base": "https://original.test/v1",
"api_key": "local-key",
},
"model_info": {"id": "replaced-deployment"},
}])
router: Final = Router(
model_list=[
{
"model_name": "local",
"litellm_params": {
"model": "hosted_vllm/local-model",
"api_base": "https://original.test/v1",
"api_key": "local-key",
},
"model_info": {"id": "replaced-deployment"},
}
]
)
def respond(request: httpx.Request) -> httpx.Response:
if request.url.host == "original.test":
router.upsert_deployment(Deployment(
model_name="local",
litellm_params=LiteLLM_Params(
model="hosted_vllm/local-model",
api_base="https://replacement.test/v1",
api_key="local-key",
),
model_info=ModelInfo(id="replaced-deployment"),
))
router.upsert_deployment(
Deployment(
model_name="local",
litellm_params=LiteLLM_Params(
model="hosted_vllm/local-model",
api_base="https://replacement.test/v1",
api_key="local-key",
),
model_info=ModelInfo(id="replaced-deployment"),
)
)
return httpx.Response(200, json={"data": [{"id": "local-model", "max_model_len": 8192}]})
assert request.url.host == "replacement.test"
return httpx.Response(200, json={"data": [{"id": "local-model", "max_model_len": 2048}]})
@ -158,15 +164,19 @@ async def test_discovery_discards_metadata_for_a_replaced_deployment(monkeypatch
async def test_discovery_is_isolated_across_routers_and_reused_ids(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(litellm, "model_cost", copy.deepcopy(litellm.model_cost))
first, second = tuple(
Router(model_list=[{
"model_name": "local",
"litellm_params": {
"model": "hosted_vllm/local-model",
"api_base": f"https://{host}.test/v1",
"api_key": "local-key",
},
"model_info": {"id": "shared-discovery-id"},
}])
Router(
model_list=[
{
"model_name": "local",
"litellm_params": {
"model": "hosted_vllm/local-model",
"api_base": f"https://{host}.test/v1",
"api_key": "local-key",
},
"model_info": {"id": "shared-discovery-id"},
}
]
)
for host in ("first", "second")
)
@ -187,15 +197,17 @@ async def test_discovery_is_isolated_across_routers_and_reused_ids(monkeypatch:
assert first.get_configured_token_limits("local") == (8192, 8192)
assert second.get_configured_token_limits("local") == (2048, 2048)
assert litellm.model_cost["shared-discovery-id"].get("max_input_tokens") is None
first.upsert_deployment(Deployment(
model_name="local",
litellm_params=LiteLLM_Params(
model="hosted_vllm/local-model",
api_base="https://unavailable.test/v1",
api_key="local-key",
),
model_info=ModelInfo(id="shared-discovery-id"),
))
first.upsert_deployment(
Deployment(
model_name="local",
litellm_params=LiteLLM_Params(
model="hosted_vllm/local-model",
api_base="https://unavailable.test/v1",
api_key="local-key",
),
model_info=ModelInfo(id="shared-discovery-id"),
)
)
assert first.get_configured_token_limits("local") == (None, None)
await first.arefresh_model_info(client=handler)
assert first.get_configured_token_limits("local") == (None, None)
@ -206,17 +218,19 @@ async def test_discovery_is_isolated_across_routers_and_reused_ids(monkeypatch:
async def test_discovery_refreshes_other_endpoints_while_one_is_pending(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(litellm, "model_cost", copy.deepcopy(litellm.model_cost))
second_started: Final = asyncio.Event()
router: Final = Router(model_list=[
{
"model_name": host,
"litellm_params": {
"model": "hosted_vllm/local-model",
"api_base": f"https://{host}.test/v1",
"api_key": "local-key",
},
}
for host in ("first", "second", "third")
])
router: Final = Router(
model_list=[
{
"model_name": host,
"litellm_params": {
"model": "hosted_vllm/local-model",
"api_base": f"https://{host}.test/v1",
"api_key": "local-key",
},
}
for host in ("first", "second", "third")
]
)
async def respond(request: httpx.Request) -> httpx.Response:
if request.url.host == "first.test":
@ -240,21 +254,27 @@ async def test_discovery_refreshes_other_endpoints_while_one_is_pending(monkeypa
async def test_discovered_limits_expire_after_the_last_successful_refresh(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(litellm, "model_cost", copy.deepcopy(litellm.model_cost))
clock: Final = Mock(return_value=0.0)
router: Final = Router(model_list=[{
"model_name": "local",
"litellm_params": {
"model": "hosted_vllm/local-model",
"api_base": "https://expiry.test/v1",
"api_key": "local-key",
},
"model_info": {"id": "expiring-discovery"},
}])
router: Final = Router(
model_list=[
{
"model_name": "local",
"litellm_params": {
"model": "hosted_vllm/local-model",
"api_base": "https://expiry.test/v1",
"api_key": "local-key",
},
"model_info": {"id": "expiring-discovery"},
}
]
)
router._discovered_model_info_cache = InMemoryCache(clock=clock, default_ttl=2 * MODEL_INFO_REFRESH_SECONDS)
responses: Final = iter((
httpx.Response(200, json={"data": [{"id": "local-model", "max_model_len": 4096}]}),
httpx.Response(200, json={"data": [{"id": "local-model", "max_model_len": 8192}]}),
httpx.Response(503),
))
responses: Final = iter(
(
httpx.Response(200, json={"data": [{"id": "local-model", "max_model_len": 4096}]}),
httpx.Response(200, json={"data": [{"id": "local-model", "max_model_len": 8192}]}),
httpx.Response(503),
)
)
handler: Final = AsyncHTTPHandler()
await handler.client.aclose()
async with httpx.AsyncClient(transport=httpx.MockTransport(lambda request: next(responses))) as client:
@ -343,10 +363,12 @@ async def test_discovered_limits_are_isolated_overridable_and_refreshable(
async def test_discovery_preserves_input_overrides_and_survives_outages(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(litellm, "model_cost", copy.deepcopy(litellm.model_cost))
responses: Final = iter((
httpx.Response(200, json={"data": [{"id": "local-model", "max_model_len": 4096}]}),
httpx.Response(503),
))
responses: Final = iter(
(
httpx.Response(200, json={"data": [{"id": "local-model", "max_model_len": 4096}]}),
httpx.Response(503),
)
)
def respond(request: httpx.Request) -> httpx.Response:
assert request.url.host == "backend.test"
@ -354,27 +376,29 @@ async def test_discovery_preserves_input_overrides_and_survives_outages(monkeypa
assert request.headers["x-tenant"] == "tenant"
return next(responses)
router: Final = Router(model_list=[
{
"model_name": "configured",
"litellm_params": {
"model": "hosted_vllm/local-model",
"api_base": "https://backend.test/v1",
"api_key": "unused-key",
"extra_headers": {"authorization": "Bearer local-key", "X-Tenant": "tenant"},
router: Final = Router(
model_list=[
{
"model_name": "configured",
"litellm_params": {
"model": "hosted_vllm/local-model",
"api_base": "https://backend.test/v1",
"api_key": "unused-key",
"extra_headers": {"authorization": "Bearer local-key", "X-Tenant": "tenant"},
},
"model_info": {"id": "configured", "max_input_tokens": 1024},
},
"model_info": {"id": "configured", "max_input_tokens": 1024},
},
{
"model_name": "byok",
"litellm_params": {
"model": "openai/local-model",
"api_base": "https://caller.test/v1",
"use_clientside_credentials": True,
{
"model_name": "byok",
"litellm_params": {
"model": "openai/local-model",
"api_base": "https://caller.test/v1",
"use_clientside_credentials": True,
},
},
},
{"model_name": "default-openai", "litellm_params": {"model": "openai/local-model", "api_key": "unused"}},
])
{"model_name": "default-openai", "litellm_params": {"model": "openai/local-model", "api_key": "unused"}},
]
)
handler: Final = AsyncHTTPHandler()
await handler.client.aclose()
responder: Final = Mock(side_effect=respond)
@ -944,8 +968,7 @@ def _register_azure_tier_backend() -> None:
def test_inherit_builtin_service_tier_pricing_fills_only_missing_fields() -> None:
model_cost_entries: Final = {
key: copy.deepcopy(litellm.model_cost.get(key))
for key in (_TIER_BACKEND_KEY, _TIER_BACKEND_MODEL)
key: copy.deepcopy(litellm.model_cost.get(key)) for key in (_TIER_BACKEND_KEY, _TIER_BACKEND_MODEL)
}
try:
_register_tier_backend()
@ -974,9 +997,7 @@ def test_inherit_builtin_service_tier_pricing_fills_only_missing_fields() -> Non
"output_cost_per_token_flex": _TIER_BACKEND_ENTRY["output_cost_per_token_flex"],
"input_cost_per_token_balanced": _TIER_BACKEND_ENTRY["input_cost_per_token_balanced"],
"output_cost_per_token_balanced": _TIER_BACKEND_ENTRY["output_cost_per_token_balanced"],
"cache_read_input_token_cost_ultrafast": _TIER_BACKEND_ENTRY[
"cache_read_input_token_cost_ultrafast"
],
"cache_read_input_token_cost_ultrafast": _TIER_BACKEND_ENTRY["cache_read_input_token_cost_ultrafast"],
"input_cost_per_token_above_272k_tokens_ultrafast": _TIER_BACKEND_ENTRY[
"input_cost_per_token_above_272k_tokens_ultrafast"
],
@ -991,8 +1012,7 @@ def test_inherit_builtin_service_tier_pricing_fills_only_missing_fields() -> Non
def test_inherit_builtin_service_tier_pricing_noop_without_base_rate_or_backend() -> None:
model_cost_entries: Final = {
key: copy.deepcopy(litellm.model_cost.get(key))
for key in (_TIER_BACKEND_KEY, _TIER_BACKEND_MODEL)
key: copy.deepcopy(litellm.model_cost.get(key)) for key in (_TIER_BACKEND_KEY, _TIER_BACKEND_MODEL)
}
try:
_register_tier_backend()
@ -1029,8 +1049,7 @@ def test_inherit_builtin_service_tier_pricing_noop_without_base_rate_or_backend(
def test_router_completion_uses_custom_standard_and_backend_ultrafast_pricing() -> None:
model_id: Final = "tier-priced-deployment"
model_cost_entries: Final = {
key: copy.deepcopy(litellm.model_cost.get(key))
for key in (_TIER_BACKEND_KEY, _TIER_BACKEND_MODEL, model_id)
key: copy.deepcopy(litellm.model_cost.get(key)) for key in (_TIER_BACKEND_KEY, _TIER_BACKEND_MODEL, model_id)
}
try:
_register_tier_backend()
@ -1090,8 +1109,7 @@ def test_router_completion_uses_custom_standard_and_backend_ultrafast_pricing()
def test_router_completion_uses_backend_ultrafast_long_context_rates() -> None:
model_id: Final = "tier-priced-long-context-deployment"
model_cost_entries: Final = {
key: copy.deepcopy(litellm.model_cost.get(key))
for key in (_TIER_BACKEND_KEY, _TIER_BACKEND_MODEL, model_id)
key: copy.deepcopy(litellm.model_cost.get(key)) for key in (_TIER_BACKEND_KEY, _TIER_BACKEND_MODEL, model_id)
}
try:
_register_tier_backend()
@ -1142,8 +1160,7 @@ def test_ptu_service_tier_pricing_is_disabled_only_when_attribution_is_enabled(
) -> None:
model_id: Final = f"ptu-tier-deployment-{ptu_enabled}"
model_cost_entries: Final = {
key: copy.deepcopy(litellm.model_cost.get(key))
for key in (_TIER_BACKEND_KEY, model_id)
key: copy.deepcopy(litellm.model_cost.get(key)) for key in (_TIER_BACKEND_KEY, model_id)
}
try:
_register_tier_backend()
@ -1200,8 +1217,7 @@ def test_azure_base_model_inherits_service_tier_pricing_for_registration_and_pay
model_id: Final = "azure-tier-priced-alias"
payload_id: Final = "azure-tier-priced-payload"
model_cost_entries: Final = {
key: copy.deepcopy(litellm.model_cost.get(key))
for key in (_AZURE_TIER_BACKEND_KEY, model_id, payload_id)
key: copy.deepcopy(litellm.model_cost.get(key)) for key in (_AZURE_TIER_BACKEND_KEY, model_id, payload_id)
}
try:
_register_azure_tier_backend()
@ -1257,12 +1273,8 @@ def test_azure_base_model_inherits_service_tier_pricing_for_registration_and_pay
)
)
assert payload["input_cost_per_token_priority"] == _AZURE_TIER_BACKEND_ENTRY[
"input_cost_per_token_priority"
]
assert payload["output_cost_per_token_priority"] == _AZURE_TIER_BACKEND_ENTRY[
"output_cost_per_token_priority"
]
assert payload["input_cost_per_token_priority"] == _AZURE_TIER_BACKEND_ENTRY["input_cost_per_token_priority"]
assert payload["output_cost_per_token_priority"] == _AZURE_TIER_BACKEND_ENTRY["output_cost_per_token_priority"]
finally:
_restore_model_cost_entries(model_cost_entries)
litellm.get_model_info.cache_clear()
@ -1364,12 +1376,10 @@ def test_inherit_builtin_base_rates_for_off_peak_carries_threshold_rates():
assert model_info["input_cost_per_token"] == builtin_info["input_cost_per_token"]
assert (
model_info["input_cost_per_token_above_200k_tokens"]
== builtin_info["input_cost_per_token_above_200k_tokens"]
model_info["input_cost_per_token_above_200k_tokens"] == builtin_info["input_cost_per_token_above_200k_tokens"]
)
assert (
model_info["output_cost_per_token_above_200k_tokens"]
== builtin_info["output_cost_per_token_above_200k_tokens"]
model_info["output_cost_per_token_above_200k_tokens"] == builtin_info["output_cost_per_token_above_200k_tokens"]
)
@ -2260,8 +2270,7 @@ def test_deployment_model_cost_payload_folds_in_litellm_params_pricing():
def test_deployment_model_cost_payload_includes_builtin_service_tier_pricing() -> None:
model_id: Final = "tier-priced-payload"
model_cost_entries: Final = {
key: copy.deepcopy(litellm.model_cost.get(key))
for key in (_TIER_BACKEND_KEY, _TIER_BACKEND_MODEL, model_id)
key: copy.deepcopy(litellm.model_cost.get(key)) for key in (_TIER_BACKEND_KEY, _TIER_BACKEND_MODEL, model_id)
}
try:
_register_tier_backend()
@ -2278,12 +2287,8 @@ def test_deployment_model_cost_payload_includes_builtin_service_tier_pricing() -
)
)
assert (
payload["input_cost_per_token_ultrafast"] == _TIER_BACKEND_ENTRY["input_cost_per_token_ultrafast"]
)
assert (
payload["output_cost_per_token_ultrafast"] == _TIER_BACKEND_ENTRY["output_cost_per_token_ultrafast"]
)
assert payload["input_cost_per_token_ultrafast"] == _TIER_BACKEND_ENTRY["input_cost_per_token_ultrafast"]
assert payload["output_cost_per_token_ultrafast"] == _TIER_BACKEND_ENTRY["output_cost_per_token_ultrafast"]
assert payload["input_cost_per_token_balanced"] == _TIER_BACKEND_ENTRY["input_cost_per_token_balanced"]
assert payload["input_cost_per_token"] == _CUSTOM_STANDARD_INPUT_RATE
assert payload["output_cost_per_token"] == _CUSTOM_STANDARD_OUTPUT_RATE
@ -3282,9 +3287,7 @@ def test_deployment_id_colliding_with_another_providers_catalog_key_keeps_its_ow
)
assert isinstance(response, litellm.ModelResponse)
assert response._hidden_params["response_cost"] == pytest.approx(
1000 * 0.00000096 + 500 * 0.00000302
)
assert response._hidden_params["response_cost"] == pytest.approx(1000 * 0.00000096 + 500 * 0.00000302)
assert litellm.model_cost[_COLLIDING_BUILTIN_KEY] == builtin_row_before
finally:
_restore_model_cost_entries(model_cost_entries)
@ -3297,8 +3300,7 @@ def test_deployment_id_matching_its_own_providers_catalog_key_still_merges() ->
matches against."""
model_id: Final = "baseten/zai-org/glm-5.2"
model_cost_entries: Final = {
key: copy.deepcopy(litellm.model_cost.get(key))
for key in (model_id, _COLLIDING_BUILTIN_KEY)
key: copy.deepcopy(litellm.model_cost.get(key)) for key in (model_id, _COLLIDING_BUILTIN_KEY)
}
try:
router: Final = Router(model_list=[_colliding_id_deployment(model_id, "baseten")])
@ -3313,9 +3315,244 @@ def test_deployment_id_matching_its_own_providers_catalog_key_still_merges() ->
)
assert isinstance(response, litellm.ModelResponse)
assert response._hidden_params["response_cost"] == pytest.approx(
1000 * 0.00000096 + 500 * 0.00000302
)
assert response._hidden_params["response_cost"] == pytest.approx(1000 * 0.00000096 + 500 * 0.00000302)
finally:
_restore_model_cost_entries(model_cost_entries)
litellm.get_model_info.cache_clear()
_CAPACITY_TEST_DEPLOYMENTS: Final = (
(
"bedrock",
"bedrock/converse/anthropic.claude-router-capacity-test",
"bedrock/us-east-1/anthropic.claude-router-capacity-test",
),
(
"bedrock_mantle",
"bedrock_mantle/openai.router-capacity-test",
"bedrock_mantle/us-east-1/openai.router-capacity-test",
),
("openai", "openai/router-capacity-test", "openai/us/router-capacity-test"),
)
def _capacity_test_metadata(provider: str, max_input_tokens: int) -> dict[str, object]:
return {
"litellm_provider": provider,
"mode": "chat",
"max_input_tokens": max_input_tokens,
"max_output_tokens": 512,
"max_tokens": 512,
"input_cost_per_token": 0,
"output_cost_per_token": 0,
}
def _capacity_test_router(route: str, base: str, base_location: str | None, explicit_limit: int | None) -> Router:
return Router(
model_list=[
{
"model_name": "capacity-test",
"litellm_params": {
"model": route,
**({"base_model": base} if base_location == "litellm_params" else {}),
},
"model_info": {
"id": "capacity-test",
**({"base_model": base} if base_location == "model_info" else {}),
**({"max_input_tokens": explicit_limit} if explicit_limit is not None else {}),
},
}
],
enable_pre_call_checks=True,
)
def _deployments_allowed_for_2048_input_tokens(router: Router) -> int:
try:
return len(
router._pre_call_checks(
model="capacity-test",
healthy_deployments=router.model_list,
input="synthetic input",
input_token_count=2048,
)
)
except litellm.ContextWindowExceededError:
return 0
def _effective_max_input_tokens(router: Router) -> object:
return router.get_router_model_info(id="capacity-test", deployment=None, received_model_name="capacity-test")[
"max_input_tokens"
]
@pytest.mark.parametrize(("provider", "route", "base"), _CAPACITY_TEST_DEPLOYMENTS)
@pytest.mark.parametrize("base_location", ("model_info", "litellm_params", None))
@pytest.mark.parametrize("explicit_limit", (None, 4096))
def test_router_capacity_widens_to_a_larger_base_model_window(
monkeypatch: pytest.MonkeyPatch,
provider: str,
route: str,
base: str,
base_location: str | None,
explicit_limit: int | None,
) -> None:
monkeypatch.setattr(
litellm,
"model_cost",
{
**litellm.model_cost,
route: _capacity_test_metadata(provider, 1024),
base: _capacity_test_metadata(provider, 8192),
},
)
_invalidate_model_cost_lowercase_map()
router: Final = _capacity_test_router(route, base, base_location, explicit_limit)
expected: Final = (
explicit_limit if explicit_limit is not None else (1024 if base_location is None else 8192)
)
assert _effective_max_input_tokens(router) == expected
assert _deployments_allowed_for_2048_input_tokens(router) == (1 if expected >= 2048 else 0)
_invalidate_model_cost_lowercase_map()
@pytest.mark.parametrize(("provider", "route", "base"), _CAPACITY_TEST_DEPLOYMENTS)
@pytest.mark.parametrize("base_location", ("model_info", "litellm_params"))
def test_router_capacity_never_narrows_to_a_smaller_base_model_window(
monkeypatch: pytest.MonkeyPatch, provider: str, route: str, base: str, base_location: str
) -> None:
monkeypatch.setattr(
litellm,
"model_cost",
{
**litellm.model_cost,
route: _capacity_test_metadata(provider, 8192),
base: _capacity_test_metadata(provider, 1024),
},
)
_invalidate_model_cost_lowercase_map()
router: Final = _capacity_test_router(route, base, base_location, None)
assert _effective_max_input_tokens(router) == 8192
assert _deployments_allowed_for_2048_input_tokens(router) == 1
_invalidate_model_cost_lowercase_map()
@pytest.mark.parametrize(("provider", "route", "base"), _CAPACITY_TEST_DEPLOYMENTS)
def test_router_capacity_never_takes_a_window_from_base_model_when_the_route_is_unmapped(
monkeypatch: pytest.MonkeyPatch, provider: str, route: str, base: str
) -> None:
monkeypatch.setattr(litellm, "model_cost", {**litellm.model_cost, base: _capacity_test_metadata(provider, 1024)})
_invalidate_model_cost_lowercase_map()
router: Final = _capacity_test_router(f"{route}-not-in-cost-map", base, "model_info", None)
assert _deployments_allowed_for_2048_input_tokens(router) == 1
_invalidate_model_cost_lowercase_map()
@pytest.mark.parametrize(("provider", "route", "base"), _CAPACITY_TEST_DEPLOYMENTS)
def test_router_capacity_never_takes_a_window_from_base_model_when_the_route_declares_none(
monkeypatch: pytest.MonkeyPatch, provider: str, route: str, base: str
) -> None:
route_without_window: Final = {
key: value
for key, value in _capacity_test_metadata(provider, 0).items()
if key not in ("max_input_tokens", "max_tokens")
}
monkeypatch.setattr(
litellm,
"model_cost",
{**litellm.model_cost, route: route_without_window, base: _capacity_test_metadata(provider, 1024)},
)
_invalidate_model_cost_lowercase_map()
router: Final = _capacity_test_router(route, base, "model_info", None)
assert _effective_max_input_tokens(router) is None
assert _deployments_allowed_for_2048_input_tokens(router) == 1
_invalidate_model_cost_lowercase_map()
_SAME_MODEL_ROUTE: Final = "bedrock/converse/anthropic.claude-routercapacitytest-9-9"
_SAME_MODEL_GLOBAL_ROUTE: Final = "bedrock/converse/global.anthropic.claude-routercapacitytest-9-9"
_SAME_MODEL_QUALIFIED_ROUTE: Final = "bedrock/converse/anthropic.claude-routercapacitytest-9-9@first"
@pytest.mark.parametrize(
("route", "base_model", "expected"),
(
(_SAME_MODEL_ROUTE, "bedrock/us-east-1/anthropic.claude-routercapacitytest-9-9", 8192),
(_SAME_MODEL_ROUTE, "us-east-1/anthropic.claude-routercapacitytest-9-9", 1024),
(_SAME_MODEL_ROUTE, "anthropic.claude-routercapacitytest-9-9", 1024),
(_SAME_MODEL_GLOBAL_ROUTE, "bedrock/us-east-1/anthropic.claude-routercapacitytest-9-9", 8192),
(_SAME_MODEL_ROUTE, "bedrock/anthropic.claude-routercapacitytest-9-9@global:us-east-1", 8192),
(_SAME_MODEL_GLOBAL_ROUTE, "bedrock/anthropic.claude-routercapacitytest-9-9@global:us-east-1", 8192),
(_SAME_MODEL_QUALIFIED_ROUTE, "bedrock/us-east-1/anthropic.claude-routercapacitytest-9-9@first", 8192),
(_SAME_MODEL_QUALIFIED_ROUTE, "bedrock/us-east-1/anthropic.claude-routercapacitytest-9-9@second", 1024),
(_SAME_MODEL_ROUTE, "bedrock/us-east-1/anthropic.claude-routercapacitytest-*", 1024),
(_SAME_MODEL_ROUTE, "bedrock/us-east-1/anthropic.claude-routercapacitystandin-9-9", 1024),
(_SAME_MODEL_ROUTE, "bedrock/us-east-1/anthropic.claude-routercapacitytest-9-9-extended", 1024),
(_SAME_MODEL_ROUTE, "bedrock/eu-west-1/anthropic.claude-routercapacitytest-9-9", 1024),
),
)
def test_router_capacity_widens_only_for_an_exact_base_model_entry_of_the_same_model(
monkeypatch: pytest.MonkeyPatch, route: str, base_model: str, expected: int
) -> None:
larger_window: Final = _capacity_test_metadata("bedrock", 8192)
monkeypatch.setattr(
litellm,
"model_cost",
{
**litellm.model_cost,
_SAME_MODEL_ROUTE: _capacity_test_metadata("bedrock", 1024),
_SAME_MODEL_GLOBAL_ROUTE: _capacity_test_metadata("bedrock", 1024),
_SAME_MODEL_QUALIFIED_ROUTE: _capacity_test_metadata("bedrock", 1024),
"bedrock/us-east-1/anthropic.claude-routercapacitytest-9-9@first": larger_window,
"bedrock/us-east-1/anthropic.claude-routercapacitytest-9-9@second": larger_window,
"bedrock/us-east-1/anthropic.claude-routercapacitytest-9-9": larger_window,
"anthropic.claude-routercapacitytest-9-9": _capacity_test_metadata("bedrock", 1024),
"bedrock/anthropic.claude-routercapacitytest-9-9": larger_window,
"bedrock/anthropic.claude-routercapacitytest-9-9@global:us-east-1": larger_window,
"bedrock/us-east-1/anthropic.claude-routercapacitytest-*": larger_window,
"bedrock/us-east-1/anthropic.claude-routercapacitystandin-9-9": larger_window,
"bedrock/us-east-1/anthropic.claude-routercapacitytest-9-9-extended": larger_window,
},
)
_invalidate_model_cost_lowercase_map()
router: Final = _capacity_test_router(route, base_model, "model_info", None)
assert _effective_max_input_tokens(router) == expected
assert _deployments_allowed_for_2048_input_tokens(router) == (1 if expected >= 2048 else 0)
_invalidate_model_cost_lowercase_map()
def test_router_capacity_reads_a_model_info_base_model_before_a_litellm_params_one(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(
litellm,
"model_cost",
{
**litellm.model_cost,
_SAME_MODEL_ROUTE: _capacity_test_metadata("bedrock", 1024),
"bedrock/us-east-1/anthropic.claude-routercapacitytest-9-9": _capacity_test_metadata("bedrock", 8192),
"bedrock/us-east-1/anthropic.claude-routercapacitystandin-9-9": _capacity_test_metadata("bedrock", 8192),
},
)
_invalidate_model_cost_lowercase_map()
router: Final = Router(
model_list=[
{
"model_name": "capacity-test",
"litellm_params": {
"model": _SAME_MODEL_ROUTE,
"base_model": "bedrock/us-east-1/anthropic.claude-routercapacitytest-9-9",
},
"model_info": {
"id": "capacity-test",
"base_model": "bedrock/us-east-1/anthropic.claude-routercapacitystandin-9-9",
},
}
],
enable_pre_call_checks=True,
)
assert _effective_max_input_tokens(router) == 1024
assert _deployments_allowed_for_2048_input_tokens(router) == 0
_invalidate_model_cost_lowercase_map()