mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-07 08:26:10 +00:00
fix(router): cast model_info cost values to float in _set_model_group_info (#33556)
Cost values read from deployment model_info can be strings when the config YAML contains scientific notation with an integer mantissa (e.g. 1e-05), which YAML 1.2 parsers such as PyYAML 6.x treat as a string. Comparing that string against the running float aggregate in _set_model_group_info raised TypeError and broke /model_group/info, the prometheus remaining-usage callback, and the x-litellm-response-cost header. Coerce input/output cost values to float before comparing and storing them.
This commit is contained in:
parent
899ddef219
commit
c012373e1c
2 changed files with 145 additions and 8 deletions
|
|
@ -251,6 +251,15 @@ else:
|
|||
PreRoutingHookResponse = Any
|
||||
|
||||
|
||||
def _cost_value_as_float(value: Union[str, int, float, None]) -> Optional[float]:
|
||||
if value is None:
|
||||
return None
|
||||
try:
|
||||
return float(value)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
class RoutingArgs(enum.Enum):
|
||||
ttl = 60 # 1min (RPM/TPM expire key)
|
||||
|
||||
|
|
@ -8750,8 +8759,8 @@ class Router:
|
|||
# Get mode from database model_info if available, otherwise default to "chat"
|
||||
db_model_info = model.get("model_info", {})
|
||||
mode = db_model_info.get("mode", "chat")
|
||||
input_cost_per_token = db_model_info.get("input_cost_per_token")
|
||||
output_cost_per_token = db_model_info.get("output_cost_per_token")
|
||||
input_cost_per_token = _cost_value_as_float(db_model_info.get("input_cost_per_token"))
|
||||
output_cost_per_token = _cost_value_as_float(db_model_info.get("output_cost_per_token"))
|
||||
|
||||
model_info = ModelMapInfo(
|
||||
key=model_group,
|
||||
|
|
@ -8802,16 +8811,18 @@ class Router:
|
|||
)
|
||||
):
|
||||
model_group_info.max_output_tokens = model_info["max_output_tokens"]
|
||||
if model_info.get("input_cost_per_token", None) is not None and (
|
||||
_input_cost_per_token = _cost_value_as_float(model_info.get("input_cost_per_token"))
|
||||
if _input_cost_per_token is not None and (
|
||||
model_group_info.input_cost_per_token is None
|
||||
or (model_info["input_cost_per_token"] or 0.0) > (model_group_info.input_cost_per_token or 0.0)
|
||||
or _input_cost_per_token > (model_group_info.input_cost_per_token or 0.0)
|
||||
):
|
||||
model_group_info.input_cost_per_token = model_info["input_cost_per_token"]
|
||||
if model_info.get("output_cost_per_token", None) is not None and (
|
||||
model_group_info.input_cost_per_token = _input_cost_per_token
|
||||
_output_cost_per_token = _cost_value_as_float(model_info.get("output_cost_per_token"))
|
||||
if _output_cost_per_token is not None and (
|
||||
model_group_info.output_cost_per_token is None
|
||||
or (model_info["output_cost_per_token"] or 0.0) > (model_group_info.output_cost_per_token or 0.0)
|
||||
or _output_cost_per_token > (model_group_info.output_cost_per_token or 0.0)
|
||||
):
|
||||
model_group_info.output_cost_per_token = model_info["output_cost_per_token"]
|
||||
model_group_info.output_cost_per_token = _output_cost_per_token
|
||||
if (
|
||||
model_info.get("supports_parallel_function_calling", None) is not None
|
||||
and model_info["supports_parallel_function_calling"] is True # type: ignore
|
||||
|
|
|
|||
|
|
@ -1455,6 +1455,132 @@ def test_model_group_info_cost_none_when_db_model_info_has_no_cost():
|
|||
assert result.output_cost_per_token is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"value,expected",
|
||||
[
|
||||
("1e-05", 1e-05),
|
||||
("0.00001", 1e-05),
|
||||
(1e-05, 1e-05),
|
||||
(5, 5.0),
|
||||
(None, None),
|
||||
("not-a-number", None),
|
||||
],
|
||||
)
|
||||
def test_cost_value_as_float(value, expected):
|
||||
from litellm.router import _cost_value_as_float
|
||||
|
||||
assert _cost_value_as_float(value) == expected
|
||||
|
||||
|
||||
def test_model_group_info_with_stringified_cost_values():
|
||||
"""
|
||||
YAML 1.2 parsers emit '1e-05' (integer mantissa) as a string, so cost
|
||||
values in deployment model_info can arrive as str. Aggregating the model
|
||||
group must not raise TypeError('>' between str and float) and must return
|
||||
float costs.
|
||||
"""
|
||||
router = litellm.Router(
|
||||
model_list=[
|
||||
{
|
||||
"model_name": "my-custom-model",
|
||||
"litellm_params": {
|
||||
"model": "openai/my-custom-backend-1",
|
||||
"api_key": "fake",
|
||||
},
|
||||
"model_info": {
|
||||
"input_cost_per_token": "1e-05",
|
||||
"output_cost_per_token": "1e-05",
|
||||
},
|
||||
},
|
||||
{
|
||||
"model_name": "my-custom-model",
|
||||
"litellm_params": {
|
||||
"model": "openai/my-custom-backend-2",
|
||||
"api_key": "fake",
|
||||
},
|
||||
"model_info": {
|
||||
"input_cost_per_token": "2e-05",
|
||||
"output_cost_per_token": "2e-05",
|
||||
},
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
def _model_info_with_str_costs(model_id: str, model_name: str):
|
||||
for model in router.model_list:
|
||||
if model["model_info"]["id"] == model_id:
|
||||
return {
|
||||
"key": model_name,
|
||||
"input_cost_per_token": model["model_info"]["input_cost_per_token"],
|
||||
"output_cost_per_token": model["model_info"]["output_cost_per_token"],
|
||||
"litellm_provider": "openai",
|
||||
"mode": "chat",
|
||||
}
|
||||
return None
|
||||
|
||||
with patch.object(
|
||||
router, "get_deployment_model_info", side_effect=_model_info_with_str_costs
|
||||
):
|
||||
result = router._set_model_group_info(
|
||||
model_group="my-custom-model",
|
||||
user_facing_model_group_name="my-custom-model",
|
||||
)
|
||||
|
||||
assert result is not None
|
||||
assert result.input_cost_per_token == 2e-05
|
||||
assert result.output_cost_per_token == 2e-05
|
||||
assert isinstance(result.input_cost_per_token, float)
|
||||
assert isinstance(result.output_cost_per_token, float)
|
||||
|
||||
|
||||
def test_model_group_info_db_fallback_with_stringified_cost_values():
|
||||
"""
|
||||
Fallback path: when get_deployment_model_info returns nothing, costs are
|
||||
read straight from the deployment's model_info dict, which can hold
|
||||
stringified floats parsed from YAML. They must be coerced to float.
|
||||
"""
|
||||
router = litellm.Router(
|
||||
model_list=[
|
||||
{
|
||||
"model_name": "my-custom-model",
|
||||
"litellm_params": {
|
||||
"model": "openai/my-custom-backend-1",
|
||||
"api_key": "fake",
|
||||
},
|
||||
"model_info": {
|
||||
"input_cost_per_token": "1e-05",
|
||||
"output_cost_per_token": "3e-05",
|
||||
},
|
||||
},
|
||||
{
|
||||
"model_name": "my-custom-model",
|
||||
"litellm_params": {
|
||||
"model": "openai/my-custom-backend-2",
|
||||
"api_key": "fake",
|
||||
},
|
||||
"model_info": {
|
||||
"input_cost_per_token": "2e-05",
|
||||
"output_cost_per_token": "2e-05",
|
||||
},
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
with patch.object(
|
||||
router, "get_deployment_model_info", side_effect=Exception("not found")
|
||||
):
|
||||
result = router._set_model_group_info(
|
||||
model_group="my-custom-model",
|
||||
user_facing_model_group_name="my-custom-model",
|
||||
)
|
||||
|
||||
assert result is not None
|
||||
assert result.input_cost_per_token == 2e-05
|
||||
assert result.output_cost_per_token == 3e-05
|
||||
assert isinstance(result.input_cost_per_token, float)
|
||||
assert isinstance(result.output_cost_per_token, float)
|
||||
|
||||
|
||||
def test_get_model_access_groups_caching():
|
||||
"""
|
||||
Test that get_model_access_groups caches the no-args result
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue