mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-07 08:26:10 +00:00
fix(proxy): treat malformed cost-map token limits as absent on /v1/models
create_model_info_response cast cost-map max_input_tokens / max_output_tokens with unguarded int(). The surrounding try/except covers only the get_model_info lookup, so a deployment whose model_info carries a non-numeric limit (e.g. "128,000" or an empty string) raised inside the per-model listing loop and failed the entire GET /v1/models and /models response with a 500, taking healthy deployments down with it. A deployment's model_info is registered into litellm.model_cost verbatim, so the malformed value reaches the cost map and not just the router index. Router.get_configured_token_limits already coerced this safely for the deployment path; the cost-map path was missed, so the two together still regressed. Both now share coerce_token_limit in litellm_core_utils, which returns None for a malformed value so the listing omits that one limit instead of failing, matching the graceful degradation the endpoint had before the cost-map switch.
This commit is contained in:
parent
b83c60b9b7
commit
ab02127b50
4 changed files with 98 additions and 17 deletions
|
|
@ -57,6 +57,35 @@ def safe_divide(
|
|||
return numerator / denominator
|
||||
|
||||
|
||||
def coerce_token_limit(value: object) -> int | None:
|
||||
"""
|
||||
Coerce a max_input_tokens / max_output_tokens value to an int, treating a
|
||||
malformed value as absent.
|
||||
|
||||
A deployment's model_info is registered into litellm.model_cost verbatim, so a
|
||||
config value like "128,000" or "" reaches the /v1/models listing uncoerced from
|
||||
both the router index and the cost map. Returning None omits that one limit
|
||||
instead of failing the whole listing.
|
||||
|
||||
Args:
|
||||
value: The raw configured or cost-map value
|
||||
|
||||
Returns:
|
||||
The value as an int, or None if it is missing or not a usable number.
|
||||
Bools are rejected because True/False is never a meaningful token limit.
|
||||
"""
|
||||
if isinstance(value, bool):
|
||||
return None
|
||||
if isinstance(value, int):
|
||||
return value
|
||||
if isinstance(value, (str, float)):
|
||||
try:
|
||||
return int(value)
|
||||
except (TypeError, ValueError, OverflowError):
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
_FINISH_REASON_MAP: dict[str, OpenAIChatCompletionFinishReason] = {
|
||||
# Anthropic
|
||||
"stop_sequence": "stop",
|
||||
|
|
|
|||
|
|
@ -104,6 +104,7 @@ from litellm.integrations.custom_logger import CustomLogger
|
|||
from litellm.integrations.prometheus import PrometheusLogger
|
||||
from litellm.integrations.SlackAlerting.slack_alerting import SlackAlerting
|
||||
from litellm.integrations.SlackAlerting.utils import _add_langfuse_trace_id_to_alert
|
||||
from litellm.litellm_core_utils.core_helpers import coerce_token_limit
|
||||
from litellm.litellm_core_utils.litellm_logging import Logging
|
||||
from litellm.litellm_core_utils.safe_json_dumps import safe_dumps
|
||||
from litellm.litellm_core_utils.safe_json_loads import safe_json_loads
|
||||
|
|
@ -6128,12 +6129,8 @@ def create_model_info_response(
|
|||
max_input_tokens: int | None = None
|
||||
max_output_tokens: int | None = None
|
||||
if model_cost_info is not None:
|
||||
cost_map_input = model_cost_info.get("max_input_tokens")
|
||||
if cost_map_input is not None:
|
||||
max_input_tokens = int(cost_map_input)
|
||||
cost_map_output = model_cost_info.get("max_output_tokens")
|
||||
if cost_map_output is not None:
|
||||
max_output_tokens = int(cost_map_output)
|
||||
max_input_tokens = coerce_token_limit(model_cost_info.get("max_input_tokens"))
|
||||
max_output_tokens = coerce_token_limit(model_cost_info.get("max_output_tokens"))
|
||||
|
||||
if llm_router is not None:
|
||||
configured_input, configured_output = llm_router.get_configured_token_limits(model_id)
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ from litellm.litellm_core_utils.request_timeout_resolver import (
|
|||
)
|
||||
from litellm.litellm_core_utils.core_helpers import (
|
||||
_get_parent_otel_span_from_kwargs,
|
||||
coerce_token_limit,
|
||||
get_metadata_variable_name_from_kwargs,
|
||||
)
|
||||
from litellm.litellm_core_utils.coroutine_checker import coroutine_checker
|
||||
|
|
@ -8544,18 +8545,10 @@ class Router:
|
|||
if deployment is None:
|
||||
return (None, None)
|
||||
|
||||
def _as_int(value: object) -> "int | None":
|
||||
if value is None or isinstance(value, bool):
|
||||
return None
|
||||
try:
|
||||
return int(value)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
model_info = deployment.model_info
|
||||
return (
|
||||
_as_int(model_info.get("max_input_tokens")),
|
||||
_as_int(model_info.get("max_output_tokens")),
|
||||
coerce_token_limit(model_info.get("max_input_tokens")),
|
||||
coerce_token_limit(model_info.get("max_output_tokens")),
|
||||
)
|
||||
|
||||
def get_deployment_credentials_with_provider(
|
||||
|
|
|
|||
|
|
@ -478,11 +478,12 @@ class TestPostCallFailureHookLiftsRecoveredPartialSpend:
|
|||
|
||||
from typing import cast
|
||||
|
||||
import litellm
|
||||
from litellm.proxy.utils import create_model_info_response
|
||||
from litellm.types.utils import ModelInfo
|
||||
|
||||
|
||||
def _fake_model_info(**fields: int) -> ModelInfo:
|
||||
def _fake_model_info(**fields: object) -> ModelInfo:
|
||||
return cast(ModelInfo, dict(fields))
|
||||
|
||||
|
||||
|
|
@ -581,6 +582,67 @@ def test_create_model_info_response_survives_malformed_configured_limits():
|
|||
assert "max_output_tokens" not in response
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bad_value", ["128,000", "", "unlimited", [128000], {"max": 128000}, True])
|
||||
def test_create_model_info_response_survives_malformed_cost_map_limits(bad_value):
|
||||
response = create_model_info_response(
|
||||
model_id="some-model",
|
||||
provider="openai",
|
||||
llm_router=None,
|
||||
get_model_info=lambda _model: _fake_model_info(
|
||||
max_input_tokens=bad_value, max_output_tokens=bad_value
|
||||
),
|
||||
)
|
||||
|
||||
assert response["id"] == "some-model"
|
||||
assert "max_input_tokens" not in response
|
||||
assert "max_output_tokens" not in response
|
||||
|
||||
|
||||
def test_create_model_info_response_keeps_valid_cost_map_limit_beside_malformed_one():
|
||||
response = create_model_info_response(
|
||||
model_id="some-model",
|
||||
provider="openai",
|
||||
llm_router=None,
|
||||
get_model_info=lambda _model: _fake_model_info(
|
||||
max_input_tokens="128,000", max_output_tokens=16384
|
||||
),
|
||||
)
|
||||
|
||||
assert "max_input_tokens" not in response
|
||||
assert response["max_output_tokens"] == 16384
|
||||
|
||||
|
||||
def test_create_model_info_response_survives_malformed_limits_registered_by_router():
|
||||
"""A deployment's model_info is registered into litellm.model_cost verbatim, so a
|
||||
malformed configured limit reaches the listing through the real cost-map lookup and
|
||||
not just the router index. Guarding only the index path still 500s the whole listing."""
|
||||
from litellm import Router
|
||||
|
||||
saved_model_cost = dict(litellm.model_cost)
|
||||
try:
|
||||
router = Router(
|
||||
model_list=[
|
||||
{
|
||||
"model_name": "openai/some-unmapped-model",
|
||||
"litellm_params": {"model": "openai/some-unmapped-model"},
|
||||
"model_info": {"max_input_tokens": "128,000"},
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
response = create_model_info_response(
|
||||
model_id="openai/some-unmapped-model",
|
||||
provider="openai",
|
||||
llm_router=router,
|
||||
)
|
||||
finally:
|
||||
litellm.model_cost.clear()
|
||||
litellm.model_cost.update(saved_model_cost)
|
||||
|
||||
assert response["id"] == "openai/some-unmapped-model"
|
||||
assert "max_input_tokens" not in response
|
||||
|
||||
|
||||
def test_create_model_info_response_emits_integer_token_counts():
|
||||
response = create_model_info_response(
|
||||
model_id="some-model",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue