fix(router): coerce numeric litellm_params from os.environ/ strings

This commit is contained in:
devin-ai-integration[bot] 2026-08-06 17:10:46 +00:00 committed by GitHub
parent b66d4e6965
commit f3c35e0733
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 63 additions and 7 deletions

View file

@ -209,6 +209,22 @@ class CredentialLiteLLMParams(BaseModel):
_RESERVED_INIT_KEYS: Final = frozenset({"self", "params", "__class__"})
_NUMERIC_LITELLM_PARAMS: Final = frozenset(
{"weight", "tpm", "rpm", "itpm", "otpm", "max_retries", "max_parallel_requests", "order"}
)
def _coerce_numeric_litellm_param(value: object) -> object:
if not isinstance(value, str):
return value
try:
return int(value)
except ValueError:
try:
return float(value)
except ValueError:
return value
class GenericLiteLLMParams(CredentialLiteLLMParams, CustomPricingLiteLLMParams):
"""
@ -288,13 +304,15 @@ class GenericLiteLLMParams(CredentialLiteLLMParams, CustomPricingLiteLLMParams):
Pre-process input data before validation:
1. Filter out reserved Python keywords ('self', 'params', '__class__') to prevent
'got multiple values for argument' errors when user data contains these keys.
2. Convert max_retries from string to int if needed.
2. Coerce numeric router params (weight/rpm/tpm/...) from str to number, since
os.environ/ substitution is string-only and these are used arithmetically.
"""
if isinstance(data, dict):
filtered: Final = {k: v for k, v in data.items() if k not in _RESERVED_INIT_KEYS}
if "max_retries" in filtered and isinstance(filtered["max_retries"], str):
filtered["max_retries"] = int(filtered["max_retries"])
return filtered
return {
k: (_coerce_numeric_litellm_param(v) if k in _NUMERIC_LITELLM_PARAMS else v)
for k, v in data.items()
if k not in _RESERVED_INIT_KEYS
}
return data
def __contains__(self, key) -> bool:

View file

@ -6,8 +6,6 @@ This test verifies the fix for the bug where passing a dict containing 'self',
TypeError: LiteLLM_Params.__init__() got multiple values for argument 'self'
"""
import pytest
from litellm.types.router import GenericLiteLLMParams, LiteLLM_Params
@ -90,3 +88,43 @@ class TestLiteLLMParamsReservedKeys:
params = LiteLLM_Params(**params_dict)
assert params.model == "gpt-4"
assert params.api_key == "test-key"
class TestLiteLLMParamsNumericCoercion:
"""Numeric router params coming from os.environ/ substitution arrive as str.
They are used arithmetically (summed / divided in simple_shuffle, compared as
rate limits), so they must be coerced back to numbers or requests 500 at runtime.
"""
def test_weight_string_coerced_to_int(self):
"""weight is an undeclared extra field, so pydantic never coerced it before."""
params = LiteLLM_Params(model="gpt-4", weight="100")
assert params.get("weight") == 100
assert isinstance(params.get("weight"), int)
assert params.model_dump()["weight"] == 100
def test_weight_float_string_coerced_to_float(self):
params = LiteLLM_Params(model="gpt-4", weight="0.5")
assert params.get("weight") == 0.5
assert isinstance(params.get("weight"), float)
def test_rpm_tpm_order_string_coerced(self):
params = LiteLLM_Params(model="gpt-4", rpm="7", tpm="5", order="2")
assert params.rpm == 7 and isinstance(params.rpm, int)
assert params.tpm == 5 and isinstance(params.tpm, int)
assert params.get("order") == 2 and isinstance(params.get("order"), int)
def test_unresolved_os_environ_placeholder_left_untouched(self):
"""If substitution has not run yet the raw placeholder must not raise."""
params = LiteLLM_Params(model="gpt-4", weight="os.environ/A_WEIGHT")
assert params.get("weight") == "os.environ/A_WEIGHT"
def test_simple_shuffle_sum_no_longer_raises(self):
"""Regression for #36095: env-var weights crashed simple_shuffle's sum()."""
healthy_deployments = [
{"litellm_params": LiteLLM_Params(model="a", weight="100").model_dump()},
{"litellm_params": LiteLLM_Params(model="b", weight="0").model_dump()},
]
weights = [m["litellm_params"].get("weight", 0) for m in healthy_deployments]
assert sum(weights) == 100