diff --git a/litellm/types/router.py b/litellm/types/router.py index 8b4b547bdcc..0fa950430cb 100644 --- a/litellm/types/router.py +++ b/litellm/types/router.py @@ -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: diff --git a/tests/test_litellm/test_litellm_params_reserved_keys.py b/tests/test_litellm/test_litellm_params_reserved_keys.py index f49651bd814..954ab83c6e1 100644 --- a/tests/test_litellm/test_litellm_params_reserved_keys.py +++ b/tests/test_litellm/test_litellm_params_reserved_keys.py @@ -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