From 9500fc18d189ca2335a9dfdc693833e31a168f1b Mon Sep 17 00:00:00 2001 From: Krish Dholakia Date: Mon, 9 Mar 2026 19:33:52 -0700 Subject: [PATCH] Fix TypeError: LiteLLM_Params.__init__() got multiple values for argument 'self' (#23220) The bug occurred when user data inadvertently contained reserved Python keywords like 'self', 'params', or '__class__' as keys. When such a dict was unpacked via **kwargs to LiteLLM_Params() or GenericLiteLLMParams(), Python raised TypeError because 'self' was passed both implicitly and as a keyword argument. The fix: - Add a Pydantic model_validator(mode='before') to GenericLiteLLMParams that filters out reserved keys ('self', 'params', '__class__') before validation - Move the max_retries str-to-int conversion into the same validator - Remove the custom __init__ methods from both GenericLiteLLMParams and LiteLLM_Params, since the validator now handles the preprocessing - Clean up unused VERTEX_CREDENTIALS_TYPES import This fix applies to all classes that inherit from GenericLiteLLMParams, including LiteLLM_Params and updateLiteLLMParams. Added comprehensive tests in tests/test_litellm/test_litellm_params_reserved_keys.py Co-authored-by: Cursor Agent --- litellm/types/router.py | 131 +++--------------- .../test_litellm_params_reserved_keys.py | 92 ++++++++++++ 2 files changed, 111 insertions(+), 112 deletions(-) create mode 100644 tests/test_litellm/test_litellm_params_reserved_keys.py diff --git a/litellm/types/router.py b/litellm/types/router.py index d917d845ad2..f0c1ea5e32a 100644 --- a/litellm/types/router.py +++ b/litellm/types/router.py @@ -8,7 +8,7 @@ from dataclasses import dataclass from typing import Any, Dict, List, Literal, Optional, Tuple, Union, get_type_hints import httpx -from pydantic import BaseModel, ConfigDict, Field +from pydantic import BaseModel, ConfigDict, Field, model_validator from typing_extensions import Required, TypedDict from litellm._uuid import uuid @@ -16,7 +16,6 @@ from litellm._uuid import uuid from .completion import CompletionRequest from .embedding import EmbeddingRequest from .llms.openai import OpenAIFileObject -from .llms.vertex_ai import VERTEX_CREDENTIALS_TYPES from .search import SearchProvider from .utils import CustomPricingLiteLLMParams, ModelResponse @@ -162,6 +161,9 @@ class CredentialLiteLLMParams(BaseModel): watsonx_region_name: Optional[str] = None +_RESERVED_INIT_KEYS = frozenset({"self", "params", "__class__"}) + + class GenericLiteLLMParams(CredentialLiteLLMParams, CustomPricingLiteLLMParams): """ LiteLLM Params without 'model' arg (used across completion / assistants api) @@ -215,76 +217,21 @@ class GenericLiteLLMParams(CredentialLiteLLMParams, CustomPricingLiteLLMParams): vector_store_id: Optional[str] = None milvus_text_field: Optional[str] = None - def __init__( - self, - custom_llm_provider: Optional[str] = None, - max_retries: Optional[Union[int, str]] = None, - tpm: Optional[int] = None, - rpm: Optional[int] = None, - api_key: Optional[str] = None, - api_base: Optional[str] = None, - api_version: Optional[str] = None, - timeout: Optional[Union[float, str]] = None, # if str, pass in as os.environ/ - stream_timeout: Optional[Union[float, str]] = ( - None # timeout when making stream=True calls, if str, pass in as os.environ/ - ), - organization: Optional[str] = None, # for openai orgs - ## LOGGING PARAMS ## - litellm_trace_id: Optional[str] = None, - ## UNIFIED PROJECT/REGION ## - region_name: Optional[str] = None, - ## VERTEX AI ## - vertex_project: Optional[str] = None, - vertex_location: Optional[str] = None, - vertex_credentials: Optional[VERTEX_CREDENTIALS_TYPES] = None, - ## AWS BEDROCK / SAGEMAKER ## - aws_access_key_id: Optional[str] = None, - aws_secret_access_key: Optional[str] = None, - aws_region_name: Optional[str] = None, - ## IBM WATSONX ## - watsonx_region_name: Optional[str] = None, - input_cost_per_token: Optional[float] = None, - output_cost_per_token: Optional[float] = None, - input_cost_per_second: Optional[float] = None, - output_cost_per_second: Optional[float] = None, - max_file_size_mb: Optional[float] = None, - # Deployment budgets - max_budget: Optional[float] = None, - budget_duration: Optional[str] = None, - # Pass through params - use_in_pass_through: Optional[bool] = False, - # Dynamic param to force using litellm proxy - use_litellm_proxy: Optional[bool] = False, - # This will merge the reasoning content in the choices - merge_reasoning_content_in_choices: Optional[bool] = False, - model_info: Optional[Dict] = None, - mock_response: Optional[Union[str, ModelResponse, Exception, Any]] = None, - # auto-router params - auto_router_config_path: Optional[str] = None, - auto_router_config: Optional[str] = None, - auto_router_default_model: Optional[str] = None, - auto_router_embedding_model: Optional[str] = None, - # complexity-router params - complexity_router_config: Optional[Dict] = None, - complexity_router_default_model: Optional[str] = None, - # Batch/File API Params - s3_bucket_name: Optional[str] = None, - s3_encryption_key_id: Optional[str] = None, - gcs_bucket_name: Optional[str] = None, - **params, - ): - args = locals() - args.pop("max_retries", None) - args.pop("self", None) - args.pop("params", None) - args.pop("__class__", None) - if max_retries is not None and isinstance(max_retries, str): - max_retries = int(max_retries) # cast to int - # We need to keep max_retries in args since it's a parameter of GenericLiteLLMParams - args[ - "max_retries" - ] = max_retries # Put max_retries back in args after popping it - super().__init__(**args, **params) + @model_validator(mode="before") + @classmethod + def preprocess_input_data(cls, data: Any) -> Any: + """ + 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. + """ + if isinstance(data, dict): + filtered = {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 data def __contains__(self, key): # Define custom behavior for the 'in' operator @@ -311,46 +258,6 @@ class LiteLLM_Params(GenericLiteLLMParams): model: str model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True) - def __init__( - self, - model: str, - custom_llm_provider: Optional[str] = None, - max_retries: Optional[Union[int, str]] = None, - tpm: Optional[int] = None, - rpm: Optional[int] = None, - api_key: Optional[str] = None, - api_base: Optional[str] = None, - api_version: Optional[str] = None, - timeout: Optional[Union[float, str]] = None, # if str, pass in as os.environ/ - stream_timeout: Optional[Union[float, str]] = ( - None # timeout when making stream=True calls, if str, pass in as os.environ/ - ), - organization: Optional[str] = None, # for openai orgs - ## VERTEX AI ## - vertex_project: Optional[str] = None, - vertex_location: Optional[str] = None, - ## AWS BEDROCK / SAGEMAKER ## - aws_access_key_id: Optional[str] = None, - aws_secret_access_key: Optional[str] = None, - aws_region_name: Optional[str] = None, - # OpenAI / Azure Whisper - # set a max-size of file that can be passed to litellm proxy - max_file_size_mb: Optional[float] = None, - # will use deployment on pass-through endpoints if True - use_in_pass_through: Optional[bool] = False, - use_litellm_proxy: Optional[bool] = False, - **params, - ): - args = locals() - args.pop("max_retries", None) - args.pop("self", None) - args.pop("params", None) - args.pop("__class__", None) - if max_retries is not None and isinstance(max_retries, str): - max_retries = int(max_retries) # cast to int - args["max_retries"] = max_retries - super().__init__(**{**args, **params}) - def __contains__(self, key): # Define custom behavior for the 'in' operator return hasattr(self, key) diff --git a/tests/test_litellm/test_litellm_params_reserved_keys.py b/tests/test_litellm/test_litellm_params_reserved_keys.py new file mode 100644 index 00000000000..f49651bd814 --- /dev/null +++ b/tests/test_litellm/test_litellm_params_reserved_keys.py @@ -0,0 +1,92 @@ +""" +Test that LiteLLM_Params and GenericLiteLLMParams handle reserved keys gracefully. + +This test verifies the fix for the bug where passing a dict containing 'self', +'params', or '__class__' keys to LiteLLM_Params() would cause: + TypeError: LiteLLM_Params.__init__() got multiple values for argument 'self' +""" + +import pytest + +from litellm.types.router import GenericLiteLLMParams, LiteLLM_Params + + +class TestLiteLLMParamsReservedKeys: + """Test that reserved keys in input data are filtered out gracefully.""" + + def test_litellm_params_with_self_key(self): + """Test LiteLLM_Params handles 'self' key in input dict.""" + params_dict = {"model": "gpt-4", "self": "some_value", "api_key": "test-key"} + params = LiteLLM_Params(**params_dict) + assert params.model == "gpt-4" + assert params.api_key == "test-key" + assert not hasattr(params, "self") or params.get("self") is None + + def test_litellm_params_with_params_key(self): + """Test LiteLLM_Params handles 'params' key in input dict.""" + params_dict = {"model": "gpt-4", "params": "bad_value"} + params = LiteLLM_Params(**params_dict) + assert params.model == "gpt-4" + + def test_litellm_params_with_class_key(self): + """Test LiteLLM_Params handles '__class__' key in input dict.""" + params_dict = {"model": "gpt-4", "__class__": "bad_value"} + params = LiteLLM_Params(**params_dict) + assert params.model == "gpt-4" + + def test_generic_litellm_params_with_self_key(self): + """Test GenericLiteLLMParams handles 'self' key in input dict.""" + params_dict = {"self": "some_value", "api_key": "test-key"} + params = GenericLiteLLMParams(**params_dict) + assert params.api_key == "test-key" + + def test_generic_litellm_params_with_params_key(self): + """Test GenericLiteLLMParams handles 'params' key in input dict.""" + params_dict = {"params": "bad_value", "api_key": "test-key"} + params = GenericLiteLLMParams(**params_dict) + assert params.api_key == "test-key" + + def test_generic_litellm_params_with_class_key(self): + """Test GenericLiteLLMParams handles '__class__' key in input dict.""" + params_dict = {"__class__": "bad_value", "api_key": "test-key"} + params = GenericLiteLLMParams(**params_dict) + assert params.api_key == "test-key" + + def test_max_retries_string_conversion(self): + """Test that max_retries is converted from string to int.""" + params = LiteLLM_Params(model="gpt-4", max_retries="5") + assert params.max_retries == 5 + assert isinstance(params.max_retries, int) + + def test_extra_fields_preserved(self): + """Test that extra fields are preserved when reserved keys are filtered.""" + params_dict = { + "model": "gpt-4", + "self": "ignored", + "custom_field": "custom_value", + } + params = LiteLLM_Params(**params_dict) + assert params.model == "gpt-4" + assert params.custom_field == "custom_value" + + def test_normal_instantiation_still_works(self): + """Test that normal instantiation without reserved keys works.""" + params = LiteLLM_Params( + model="gpt-4", api_key="test-key", custom_llm_provider="openai" + ) + assert params.model == "gpt-4" + assert params.api_key == "test-key" + assert params.custom_llm_provider == "openai" + + def test_multiple_reserved_keys(self): + """Test filtering multiple reserved keys at once.""" + params_dict = { + "model": "gpt-4", + "self": "value1", + "params": "value2", + "__class__": "value3", + "api_key": "test-key", + } + params = LiteLLM_Params(**params_dict) + assert params.model == "gpt-4" + assert params.api_key == "test-key"