diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 98efadc10a8..aa50500e50d 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -2378,6 +2378,10 @@ class ConfigGeneralSettings(LiteLLMPydanticObjectBase): description="[DEPRECATED] Use 'user_header_mappings' instead. When set, the header value is treated as the end user id unless overridden by user_header_mappings.", ) user_header_mappings: Optional[List[UserHeaderMapping]] = None + override_user_param: bool | None = Field( + None, + description="When True, the proxy overwrites any client-supplied 'user' param with the authenticated user id. Use when LiteLLM is the authentication boundary and clients must not be able to spoof 'user'.", + ) supported_db_objects: Optional[List[SupportedDBObjectType]] = Field( None, description="Fine-grained control over which object types to load from the database when store_model_in_db is True. Available types: 'models', 'mcp', 'guardrails', 'vector_stores', 'pass_through_endpoints', 'prompts', 'model_cost_map', 'tools', 'config_overrides'. If not set, all objects are loaded (default behavior).", diff --git a/litellm/proxy/litellm_pre_call_utils.py b/litellm/proxy/litellm_pre_call_utils.py index a4cc4a62009..8ce0883beeb 100644 --- a/litellm/proxy/litellm_pre_call_utils.py +++ b/litellm/proxy/litellm_pre_call_utils.py @@ -1465,6 +1465,10 @@ async def add_litellm_data_to_request( if "user" not in data: data["user"] = user + if general_settings is not None and general_settings.get("override_user_param") is True: + if user_api_key_dict.user_id is not None: + data["user"] = user_api_key_dict.user_id + if litellm.overwrite_user_with_key_hash is True: stampable_hash = _stampable_key_hash(user_api_key_dict) if stampable_hash is not None: diff --git a/tests/test_litellm/proxy/test_litellm_pre_call_utils.py b/tests/test_litellm/proxy/test_litellm_pre_call_utils.py index 1437899f561..c6f284b65bb 100644 --- a/tests/test_litellm/proxy/test_litellm_pre_call_utils.py +++ b/tests/test_litellm/proxy/test_litellm_pre_call_utils.py @@ -5421,3 +5421,54 @@ async def test_overwrite_user_with_key_hash_rejects_alias_without_marker(monkeyp ) assert updated_data["user"] == "caller-chosen-id" + + +@pytest.mark.asyncio +async def test_override_user_param_clobbers_client_supplied_user(): + user_api_key_dict = UserAPIKeyAuth(api_key="hashed-key", user_id="alice") + data = {"model": "gpt-4o", "user": "bob"} + + updated_data = await add_litellm_data_to_request( + data=data, + request=_make_chat_request_mock(), + user_api_key_dict=user_api_key_dict, + proxy_config=MagicMock(), + general_settings={"override_user_param": True}, + version="test-version", + ) + + assert updated_data["user"] == "alice" + + +@pytest.mark.asyncio +async def test_override_user_param_disabled_keeps_client_supplied_user(): + user_api_key_dict = UserAPIKeyAuth(api_key="hashed-key", user_id="alice") + data = {"model": "gpt-4o", "user": "bob"} + + updated_data = await add_litellm_data_to_request( + data=data, + request=_make_chat_request_mock(), + user_api_key_dict=user_api_key_dict, + proxy_config=MagicMock(), + general_settings={}, + version="test-version", + ) + + assert updated_data["user"] == "bob" + + +@pytest.mark.asyncio +async def test_override_user_param_without_authenticated_user_id_keeps_client_value(): + user_api_key_dict = UserAPIKeyAuth(api_key="hashed-key") + data = {"model": "gpt-4o", "user": "bob"} + + updated_data = await add_litellm_data_to_request( + data=data, + request=_make_chat_request_mock(), + user_api_key_dict=user_api_key_dict, + proxy_config=MagicMock(), + general_settings={"override_user_param": True}, + version="test-version", + ) + + assert updated_data["user"] == "bob" diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts index 825d06d6a38..acbd2d1363a 100644 --- a/ui/litellm-dashboard/src/lib/http/schema.d.ts +++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts @@ -22663,6 +22663,11 @@ export interface components { * @description [BETA] OpenTelemetry support - this might change, use with caution. */ otel?: boolean | null; + /** + * Override User Param + * @description When True, the proxy overwrites any client-supplied 'user' param with the authenticated user id. Use when LiteLLM is the authentication boundary and clients must not be able to spoof 'user'. + */ + override_user_param?: boolean | null; /** * Pass Through Endpoints * @description Set-up pass-through endpoints for provider-specific endpoints. Docs - https://docs.litellm.ai/docs/proxy/pass_through