Skip to content

fix(proxy): treat malformed cost-map token limits as absent on /v1/models - #33904

Merged
yuneng-berri merged 1 commit into
patch-1.93.0rc2from
litellm_models_listing_malformed_cost_map_limits_1930
Jul 19, 2026
Merged

fix(proxy): treat malformed cost-map token limits as absent on /v1/models#33904
yuneng-berri merged 1 commit into
patch-1.93.0rc2from
litellm_models_listing_malformed_cost_map_limits_1930

Conversation

@yuneng-berri

Copy link
Copy Markdown
Collaborator

Relevant issues

Backport of #33903 onto patch-1.93.0rc2

Linear ticket

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • My PR passes all CI/CD checks (e.g., lint, format, unit tests)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to re-request a review after pushing changes)

Delays in PR merge?

If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).

Screenshots / Proof of Fix

patch-1.93.0rc2 carries the regression, so the 1.93.0 stable cut ships a 500 on GET /v1/models without this pick

$ git grep -n "cost_map_input" origin/patch-1.93.0rc2 -- litellm/proxy/utils.py
origin/patch-1.93.0rc2:litellm/proxy/utils.py:6133:        cost_map_input = model_cost_info.get("max_input_tokens")
origin/patch-1.93.0rc2:litellm/proxy/utils.py:6134:        if cost_map_input is not None:
origin/patch-1.93.0rc2:litellm/proxy/utils.py:6135:            max_input_tokens = int(cost_map_input)

The line also already carries the router-side guard from #33864, so it is in the same state staging was in: one path guarded, the cost-map path not

$ git grep -n "_as_int" origin/patch-1.93.0rc2 -- litellm/router.py
origin/patch-1.93.0rc2:litellm/router.py:8374:        def _as_int(value: object) -> "int | None":

Runtime proof was captured on the staging PR (#33903) against a live proxy: 500 before, 200 with the malformed deployment's limits omitted after, plus a real OpenAI call through the same proxy. On this line, the picked commit resolves and degrades gracefully rather than raising, at db190ba

$ python -c "
import litellm
from litellm import Router
from litellm.proxy.utils import create_model_info_response
r = Router(model_list=[{'model_name':'openai/probe','litellm_params':{'model':'openai/probe'},'model_info':{'max_input_tokens':'128,000'}}])
print('RESULT:', create_model_info_response(model_id='openai/probe', provider='openai', llm_router=r))
"
RESULT: {'id': 'openai/probe', 'object': 'model', 'created': 1677610602, 'owned_by': 'openai'}
$ python -m pytest tests/test_litellm/proxy/test_proxy_utils.py tests/test_litellm/test_router.py -k "create_model_info_response or token_limits"
23 passed, 158 deselected

Type

🐛 Bug Fix

Changes

Clean cherry-pick of ab02127 from #33903, no conflicts and no adaptation needed for this line

create_model_info_response cast the cost-map max_input_tokens and max_output_tokens with a bare int(), outside the try/except that guards the get_model_info lookup. A deployment whose model_info carries a non-numeric limit therefore raised inside the per-model listing loop and failed the whole GET /v1/models and /models response with a 500, taking every healthy deployment down with it

Both call sites now share coerce_token_limit in litellm_core_utils/core_helpers.py, which returns None for anything that is not a usable number so the listing omits that one limit and keeps serving

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

…dels

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.

(cherry picked from commit ab02127)
@greptile-apps

greptile-apps Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR backports a targeted fix for a 500 error on GET /v1/models caused by a bare int() cast on cost-map token limits that sat outside the existing try/except guard, so any deployment with a non-numeric limit (e.g. "128,000") would crash the entire listing.

  • Introduces coerce_token_limit() in core_helpers.py as a shared utility that returns None for booleans, non-numeric strings, unsupported collection types, and infinity/NaN floats, replacing the bare int() in proxy/utils.py and the local _as_int helper in router.py.
  • Adds three new mock-only tests covering parametrized bad values, mixed valid/malformed limits, and the end-to-end path where a Router registers a malformed limit into litellm.model_cost; global state is saved and restored correctly in the integration-style test.

Confidence Score: 5/5

Safe to merge — the change is a one-directional tightening that degrades gracefully instead of raising, with no impact on well-formed deployments.

The fix is minimal and surgical: coerce_token_limit is a pure function with no side effects, and its behavior is identical to the old _as_int for all values that previously worked correctly. The only observable difference is that booleans and other non-numeric types now return None instead of raising, which is the intended fix. Tests cover the newly guarded path and the pre-existing router path, and global state mutation in the integration test is properly contained in a try/finally block.

No files require special attention.

Important Files Changed

Filename Overview
litellm/litellm_core_utils/core_helpers.py Adds coerce_token_limit() shared utility that safely converts token-limit values to int, treating booleans, non-numeric strings, and unexpected types as absent (None).
litellm/proxy/utils.py Replaces bare int() calls on cost-map token limits with coerce_token_limit(), preventing a 500 when a deployment carries a non-numeric limit string like "128,000".
litellm/router.py Removes the local _as_int helper from get_configured_token_limits and replaces it with the shared coerce_token_limit; functionally equivalent (both return None for booleans, non-numeric strings, and unsupported types).
tests/test_litellm/proxy/test_proxy_utils.py Adds three new tests covering malformed cost-map limits (parametrized), mixed valid/malformed limits, and the end-to-end path where a Router registers a bad limit into litellm.model_cost; global state is correctly saved and restored.

Reviews (1): Last reviewed commit: "fix(proxy): treat malformed cost-map tok..." | Re-trigger Greptile

@yuneng-berri
yuneng-berri merged commit 052b5a2 into patch-1.93.0rc2 Jul 19, 2026
5 checks passed
@yuneng-berri
yuneng-berri deleted the litellm_models_listing_malformed_cost_map_limits_1930 branch July 19, 2026 02:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant