fix(router): report null cost for unpriced deployments instead of 0

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
jesus 2026-09-12 01:00:44 +00:00 committed by ryan-crabbe-berri
parent a7a3f6802e
commit 32e6a86319
5 changed files with 113 additions and 3 deletions

View file

@ -154,7 +154,7 @@ from litellm.types.utils import (
TextCompletionResponse,
TokenCountResponse,
)
from litellm.utils import load_credentials_from_list
from litellm.utils import cost_map_omits_token_price, load_credentials_from_list
if TYPE_CHECKING:
from aiohttp import ClientSession
@ -13618,9 +13618,10 @@ def _enrich_model_info_with_litellm_data(
discovered_model_info: Final = (
llm_router.get_discovered_model_info(model_info.get("id")) if llm_router is not None else MappingProxyType({})
)
unpriced: Final = cost_map_omits_token_price(model_info.get("id"), litellm_model_info.get("key"))
for k, v in MappingProxyType({**litellm_model_info, **discovered_model_info}).items():
if k not in model_info or (model_info[k] is None and k in discovered_model_info):
model_info[k] = v
model_info[k] = None if unpriced and k in ("input_cost_per_token", "output_cost_per_token") else v
model["model_info"] = model_info
# don't return the api key / vertex credentials
# don't return the llm credentials

View file

@ -10575,11 +10575,12 @@ class Router:
2. If not, check if litellm model name is in model info
3. If not, return None
"""
from litellm.utils import _update_dictionary
from litellm.utils import _update_dictionary, cost_map_omits_token_price
model_info: ModelInfo | None = None
custom_model_info: dict | None = None
litellm_model_name_model_info: ModelInfo | None = None
base_model_key: str | None = None
try:
custom_model_info = (
@ -10606,6 +10607,7 @@ class Router:
## update litellm model info with base model info
base_model_info: Final = copy.deepcopy(litellm.get_model_info(model=base_model))
if base_model_info is not None:
base_model_key = base_model_info.get("key")
# Base model provides defaults, custom model info overrides
custom_model_info = _update_dictionary(
cast(dict, base_model_info),
@ -10633,6 +10635,13 @@ class Router:
# custom_model_info already includes base_model defaults at this point, if applicable
model_info = cast(ModelInfo, custom_model_info)
if model_info is None:
return None
builtin_key: Final = (
litellm_model_name_model_info.get("key") if litellm_model_name_model_info is not None else None
)
if cost_map_omits_token_price(model_id, builtin_key, base_model_key):
return cast(ModelInfo, {**model_info, "input_cost_per_token": None, "output_cost_per_token": None})
return model_info
def _set_model_group_info(self, model_group: str, user_facing_model_group_name: str) -> ModelGroupInfo | None:

View file

@ -3156,6 +3156,22 @@ def reapply_runtime_model_cost_registrations() -> None:
register_model(model_cost=dict(_runtime_registered_model_cost)) # mutable-ok: snapshot, replay rewrites it
def cost_map_omits_token_price(*keys: object) -> bool:
"""Whether the raw ``litellm.model_cost`` entries under ``keys`` exist but none carries a per-token price.
``get_model_info`` substitutes 0 for a missing price, which reads exactly like a declared
zero. Surfaces that report pricing use this to keep an unpriced deployment at ``None``.
"""
entries: Final = tuple(
entry
for entry in (litellm.model_cost.get(key) for key in keys if isinstance(key, str))
if isinstance(entry, dict)
)
return len(entries) > 0 and not any(
"input_cost_per_token" in entry or "output_cost_per_token" in entry for entry in entries
)
def register_model(
model_cost: str | dict,
*,

View file

@ -286,6 +286,43 @@ def test_get_proxy_model_info_surfaces_supports_parallel_function_calling(local_
assert enriched["model_info"]["supports_parallel_function_calling"] is True
def test_model_info_reports_null_cost_for_unpriced_deployment_and_zero_for_declared_zero():
"""A deployment configured with no cost fields must not surface the 0 that ``get_model_info``
defaults to, since the zero-cost budget bypass only honours a declared zero. The declared zero
and a catalog price still come through."""
import litellm
router = litellm.Router(
model_list=[
{
"model_name": "vllm-unpriced",
"litellm_params": {"model": "openai/vllm-unpriced", "api_key": "x", "api_base": "http://vllm"},
},
{
"model_name": "vllm-free",
"litellm_params": {
"model": "openai/vllm-free",
"api_key": "x",
"api_base": "http://vllm",
"input_cost_per_token": 0,
"output_cost_per_token": 0,
},
},
{"model_name": "gpt-priced", "litellm_params": {"model": "gpt-4o", "api_key": "x"}},
]
)
def enriched_cost(model_name: str) -> tuple:
deployment = router.get_model_list(model_name=model_name)[0]
info = proxy_server._enrich_model_info_with_litellm_data({**deployment, "model_info": dict(deployment["model_info"])})["model_info"]
return info.get("input_cost_per_token"), info.get("output_cost_per_token")
assert enriched_cost("vllm-unpriced") == (None, None)
assert enriched_cost("vllm-free") == (0, 0)
input_cost, output_cost = enriched_cost("gpt-priced")
assert input_cost > 0 and output_cost > 0
def test_v1_model_info_star_wildcard_filter_keeps_provider_expansion(monkeypatch):
from litellm.proxy._types import SpecialModelNames, UserAPIKeyAuth
from litellm.proxy.auth import model_checks

View file

@ -1888,6 +1888,53 @@ def test_model_group_info_cost_none_when_db_model_info_has_no_cost():
assert result.output_cost_per_token is None
def test_model_group_info_cost_none_for_unpriced_deployment_but_zero_when_declared():
"""A deployment with no cost fields anywhere must report None, not the 0 that
get_model_info defaults to, so the reported price matches what the zero-cost
budget bypass accepts. A deployment declaring 0 keeps reporting 0."""
router = litellm.Router(
model_list=[
{
"model_name": "vllm-unpriced",
"litellm_params": {
"model": "openai/my-vllm-unpriced",
"api_key": "fake",
"api_base": "http://localhost:8000/v1",
},
},
{
"model_name": "vllm-free",
"litellm_params": {
"model": "openai/my-vllm-free",
"api_key": "fake",
"api_base": "http://localhost:8000/v1",
"input_cost_per_token": 0,
"output_cost_per_token": 0,
},
},
{
"model_name": "gpt-priced",
"litellm_params": {"model": "gpt-4o", "api_key": "fake"},
},
]
)
unpriced = router.get_model_group_info(model_group="vllm-unpriced")
assert unpriced is not None
assert unpriced.input_cost_per_token is None
assert unpriced.output_cost_per_token is None
free = router.get_model_group_info(model_group="vllm-free")
assert free is not None
assert free.input_cost_per_token == 0
assert free.output_cost_per_token == 0
priced = router.get_model_group_info(model_group="gpt-priced")
assert priced is not None
assert priced.input_cost_per_token is not None and priced.input_cost_per_token > 0
assert priced.output_cost_per_token is not None and priced.output_cost_per_token > 0
@pytest.mark.parametrize(
"value,expected",
[