diff --git a/litellm/proxy/auth/model_checks.py b/litellm/proxy/auth/model_checks.py index ad651b45f48..9c3e0f4b67c 100644 --- a/litellm/proxy/auth/model_checks.py +++ b/litellm/proxy/auth/model_checks.py @@ -565,28 +565,22 @@ def wildcard_filled_model(model_name_pattern: str, litellm_model: str, listed_na ) -def _deployment_routed_model(source: Mapping[str, object], listed_name: str) -> str | None: - source_pattern: Final = source.get("model_name") - source_model: Final = _litellm_model(source) - return ( - wildcard_filled_model(source_pattern, source_model, listed_name) - if isinstance(source_pattern, str) and isinstance(source_model, str) - else None - ) - - -def wildcard_routed_model(row: Mapping[str, object], llm_router: Router | None) -> str | None: +def wildcard_row_priced_model(row: Mapping[str, object], llm_router: Router | None) -> str | None: model_id: Final = _model_info_field(row, "id") listed_name: Final = row.get("model_name") if llm_router is None or not isinstance(model_id, str) or not isinstance(listed_name, str): return None registry: Final = llm_router.registry() source: Final = _router_deployment(registry, model_id) - if source is None or _litellm_model(row) == _litellm_model(source): + if source is None: + return None + source_pattern: Final = source.get("model_name") + source_model: Final = _litellm_model(source) + if not isinstance(source_pattern, str) or not isinstance(source_model, str) or _litellm_model(row) == source_model: return None if model_id not in registry.names(listed_name, kind="route_candidates", team_id=_deployment_team_id(row)): - return None - return _deployment_routed_model(source, listed_name) + return source_model + return wildcard_filled_model(source_pattern, source_model, listed_name) or source_model def _get_wildcard_models( diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index daf2f226e46..d73dd268b93 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -443,7 +443,7 @@ from litellm.proxy.auth.model_checks import ( get_key_models, get_mcp_server_ids, get_team_models, - wildcard_routed_model, + wildcard_row_priced_model, ) from litellm.proxy.auth.password_policy import validate_password_not_breached, validate_password_policy from litellm.proxy.auth.user_api_key_auth import ( # noqa: F401, RUF100 # legacy module exports @@ -16145,11 +16145,9 @@ def _enrich_model_info_with_litellm_data( # read litellm model_prices_and_context_window.json to get the following: # input_cost_per_token, output_cost_per_token, max_tokens - routed_model: Final = wildcard_routed_model(model, llm_router) + priced_as: Final = wildcard_row_priced_model(model, llm_router) priced_model: Final = ( - model - if routed_model is None - else {**model, "litellm_params": {**model["litellm_params"], "model": routed_model}} + model if priced_as is None else {**model, "litellm_params": {**model["litellm_params"], "model": priced_as}} ) litellm_model_info = get_litellm_model_info(model=priced_model) diff --git a/tests/unit/proxy/proxy_server/test_routes_model_info.py b/tests/unit/proxy/proxy_server/test_routes_model_info.py index 0968ecb1d24..08947a54a0c 100644 --- a/tests/unit/proxy/proxy_server/test_routes_model_info.py +++ b/tests/unit/proxy/proxy_server/test_routes_model_info.py @@ -678,8 +678,9 @@ def test_model_info_never_prices_a_wildcard_row_as_a_route_its_calls_skip( ) -> None: """``acme-lite`` has a deployment of its own, ``acme-pro`` is an alias of it, ``acme-max`` is a routing group over it, and ``my-openai/gpt-acme-*`` is a narrower wildcard, so calls to those names - never reach the catch-all or ``my-openai/*``, which still list them. Such a row may go unpriced, but - any price a row shows must be the one its calls are billed at.""" + never reach the catch-all or ``my-openai/*``, which still list them. Such a row shows no price, even + where another provider's model goes by the same name, and every price shown is the one calls are + billed at.""" from litellm.proxy.auth import model_checks catalog: Final = { @@ -713,6 +714,18 @@ def test_model_info_never_prices_a_wildcard_row_as_a_route_its_calls_skip( "litellm_provider": "openai", "mode": "chat", }, + "acme-pro": { + "input_cost_per_token": 9e-07, + "output_cost_per_token": 1e-06, + "litellm_provider": "deepseek", + "mode": "chat", + }, + "acme-max": { + "input_cost_per_token": 1.1e-06, + "output_cost_per_token": 1.2e-06, + "litellm_provider": "deepseek", + "mode": "chat", + }, } monkeypatch.setattr(litellm, "model_cost", {**litellm.model_cost, **catalog}) _invalidate_model_cost_lowercase_map() @@ -779,6 +792,12 @@ def test_model_info_never_prices_a_wildcard_row_as_a_route_its_calls_skip( ("openai-wildcard", "my-openai/gpt-acme-lite"), ] assert listed[("deepseek-acme-lite", "acme-lite")] == (7e-07, 8e-07) + assert {key: prices for key, prices in listed.items() if key[0] != "deepseek-acme-lite"} == { + ("fireworks-wildcard", "acme-lite"): (None, None), + ("fireworks-wildcard", "acme-max"): (None, None), + ("fireworks-wildcard", "acme-pro"): (None, None), + ("openai-wildcard", "my-openai/gpt-acme-lite"): (None, None), + } calls: Final = { model_name: router.completion( model=model_name, messages=[{"role": "user", "content": "hi"}], mock_response="hello there"