diff --git a/litellm/proxy/management_endpoints/internal_user_endpoints.py b/litellm/proxy/management_endpoints/internal_user_endpoints.py index 2ca1c4cc483..7d047ca2c29 100644 --- a/litellm/proxy/management_endpoints/internal_user_endpoints.py +++ b/litellm/proxy/management_endpoints/internal_user_endpoints.py @@ -1462,13 +1462,19 @@ async def get_users( where_conditions: Dict[str, Any] = {} if role: - where_conditions["user_role"] = role # Exact match instead of contains + where_conditions["user_role"] = role if user_ids and isinstance(user_ids, str): user_id_list = [uid.strip() for uid in user_ids.split(",") if uid.strip()] - where_conditions["user_id"] = { - "in": user_id_list, - } + if len(user_id_list) == 1: + where_conditions["user_id"] = { + "contains": user_id_list[0], + "mode": "insensitive", + } + else: + where_conditions["user_id"] = { + "in": user_id_list, + } if user_email is not None and isinstance(user_email, str): where_conditions["user_email"] = { diff --git a/tests/test_litellm/proxy/management_endpoints/test_internal_user_endpoints.py b/tests/test_litellm/proxy/management_endpoints/test_internal_user_endpoints.py index a112ae046ab..de7a847a918 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_internal_user_endpoints.py +++ b/tests/test_litellm/proxy/management_endpoints/test_internal_user_endpoints.py @@ -812,3 +812,62 @@ def test_process_keys_for_user_info_handles_empty_keys(monkeypatch): # Should return empty list assert result == [], "Should return empty list when keys is empty" + + +@pytest.mark.asyncio +async def test_get_users_user_id_partial_match(mocker): + """ + Test that /user/list endpoint uses partial matching for single user_id + and exact matching for multiple user_ids. + """ + from litellm.proxy._types import UserAPIKeyAuth + + mock_prisma_client = mocker.MagicMock() + + mock_user_data = { + "user_id": "test-user-partial-match", + "user_email": "test@example.com", + "user_role": "internal_user", + "created_at": datetime.now(timezone.utc), + "updated_at": datetime.now(timezone.utc), + } + mock_user_row = mocker.MagicMock() + mock_user_row.model_dump.return_value = mock_user_data + + captured_where_conditions = {} + + async def mock_find_many(*args, **kwargs): + if "where" in kwargs: + captured_where_conditions.update(kwargs["where"]) + return [mock_user_row] + + async def mock_count(*args, **kwargs): + return 1 + + mock_prisma_client.db.litellm_usertable.find_many = mock_find_many + mock_prisma_client.db.litellm_usertable.count = mock_count + + mocker.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma_client) + + async def mock_get_user_key_counts(*args, **kwargs): + return {"test-user-partial-match": 0} + + mocker.patch( + "litellm.proxy.management_endpoints.internal_user_endpoints.get_user_key_counts", + mock_get_user_key_counts, + ) + + captured_where_conditions.clear() + await get_users(user_ids="test-user", page=1, page_size=1) + + assert "user_id" in captured_where_conditions + assert "contains" in captured_where_conditions["user_id"] + assert captured_where_conditions["user_id"]["contains"] == "test-user" + assert captured_where_conditions["user_id"]["mode"] == "insensitive" + + captured_where_conditions.clear() + await get_users(user_ids="user1,user2,user3", page=1, page_size=1) + + assert "user_id" in captured_where_conditions + assert "in" in captured_where_conditions["user_id"] + assert captured_where_conditions["user_id"]["in"] == ["user1", "user2", "user3"]