Skip to content

Commit fcaa4a9

Browse files
authored
Add user management functionality to Python client library & CLI (#10627)
* Add user mgmt functionality to client & CLI * Add user mgmt client & CLI tests * Add user mgmt client & CLI info to README.md's * lint fixes: litellm/proxy/client/users.py * Fix mypy errors
1 parent d680feb commit fcaa4a9

File tree

9 files changed

+340
-3
lines changed

9 files changed

+340
-3
lines changed

litellm/proxy/client/README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ The client is organized into several resource clients for different functionalit
3838
- `model_groups`: Model group management
3939
- `keys`: API key management
4040
- `credentials`: Credential management
41+
- `users`: User management
4142

4243
## Chat Completions
4344

@@ -157,6 +158,34 @@ groups = client.model_groups.list()
157158
client.model_groups.delete(name="gpt4-group")
158159
```
159160

161+
## Users Management
162+
163+
Manage users on your proxy:
164+
165+
```python
166+
from litellm.proxy.client import UsersManagementClient
167+
168+
users = UsersManagementClient(base_url="http://localhost:4000", api_key="sk-test")
169+
170+
# List users
171+
user_list = users.list_users()
172+
173+
# Get user info
174+
user_info = users.get_user(user_id="u1")
175+
176+
# Create a new user
177+
created = users.create_user({
178+
"user_email": "[email protected]",
179+
"user_role": "internal_user",
180+
"user_alias": "Alice",
181+
"teams": ["team1"],
182+
"max_budget": 100.0
183+
})
184+
185+
# Delete users
186+
users.delete_user(["u1", "u2"])
187+
```
188+
160189
## Low-Level HTTP Client
161190

162191
The client provides access to a low-level HTTP client for making direct requests

litellm/proxy/client/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@
33
from .models import ModelsManagementClient
44
from .model_groups import ModelGroupsManagementClient
55
from .exceptions import UnauthorizedError
6+
from .users import UsersManagementClient
67

7-
__all__ = ["Client", "ChatClient", "ModelsManagementClient", "ModelGroupsManagementClient", "UnauthorizedError"]
8+
__all__ = ["Client", "ChatClient", "ModelsManagementClient", "ModelGroupsManagementClient", "UsersManagementClient", "UnauthorizedError"]

litellm/proxy/client/cli/README.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,42 @@ Example:
254254
litellm-proxy keys info --key sk-key1
255255
```
256256

257+
### User Management
258+
259+
The CLI provides commands for managing users on your LiteLLM proxy server:
260+
261+
#### List Users
262+
263+
View all users:
264+
265+
```bash
266+
litellm-proxy users list
267+
```
268+
269+
#### Get User Info
270+
271+
Get information about a specific user:
272+
273+
```bash
274+
litellm-proxy users get --id <user-id>
275+
```
276+
277+
#### Create User
278+
279+
Create a new user:
280+
281+
```bash
282+
litellm-proxy users create --email [email protected] --role internal_user --alias "Alice" --team team1 --max-budget 100.0
283+
```
284+
285+
#### Delete User
286+
287+
Delete one or more users by user_id:
288+
289+
```bash
290+
litellm-proxy users delete <user-id-1> <user-id-2>
291+
```
292+
257293
### Chat Commands
258294

259295
The CLI provides commands for interacting with chat models through your LiteLLM proxy server:
@@ -397,6 +433,22 @@ litellm-proxy http request POST /chat/completions \
397433
-H "X-Custom-Header:value"
398434
```
399435

436+
8. User management:
437+
438+
```bash
439+
# List users
440+
litellm-proxy users list
441+
442+
# Get user info
443+
litellm-proxy users get --id u1
444+
445+
# Create a user
446+
litellm-proxy users create --email [email protected] --role internal_user --alias "Alice" --team team1 --max-budget 100.0
447+
448+
# Delete users
449+
litellm-proxy users delete u1 u2
450+
```
451+
400452
## Error Handling
401453

402454
The CLI will display appropriate error messages when:
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import click
2+
import rich
3+
from ... import UsersManagementClient
4+
5+
@click.group()
6+
def users():
7+
"""Manage users on your LiteLLM proxy server"""
8+
pass
9+
10+
@users.command("list")
11+
@click.pass_context
12+
def list_users(ctx: click.Context):
13+
"""List all users"""
14+
client = UsersManagementClient(base_url=ctx.obj["base_url"], api_key=ctx.obj["api_key"])
15+
users = client.list_users()
16+
if isinstance(users, dict) and "users" in users:
17+
users = users["users"]
18+
if not users:
19+
click.echo("No users found.")
20+
return
21+
from rich.table import Table
22+
from rich.console import Console
23+
table = Table(title="Users")
24+
table.add_column("User ID", style="cyan")
25+
table.add_column("Email", style="green")
26+
table.add_column("Role", style="magenta")
27+
table.add_column("Teams", style="yellow")
28+
for user in users:
29+
table.add_row(
30+
str(user.get("user_id", "")),
31+
str(user.get("user_email", "")),
32+
str(user.get("user_role", "")),
33+
", ".join(user.get("teams", []) or [])
34+
)
35+
console = Console()
36+
console.print(table)
37+
38+
@users.command("get")
39+
@click.option("--id", "user_id", help="ID of the user to retrieve")
40+
@click.pass_context
41+
def get_user(ctx: click.Context, user_id: str):
42+
"""Get information about a specific user"""
43+
client = UsersManagementClient(base_url=ctx.obj["base_url"], api_key=ctx.obj["api_key"])
44+
result = client.get_user(user_id=user_id)
45+
rich.print_json(data=result)
46+
47+
@users.command("create")
48+
@click.option("--email", required=True, help="User email")
49+
@click.option("--role", default="internal_user", help="User role")
50+
@click.option("--alias", default=None, help="User alias")
51+
@click.option("--team", multiple=True, help="Team IDs (can specify multiple)")
52+
@click.option("--max-budget", type=float, default=None, help="Max budget for user")
53+
@click.pass_context
54+
def create_user(ctx: click.Context, email, role, alias, team, max_budget):
55+
"""Create a new user"""
56+
client = UsersManagementClient(base_url=ctx.obj["base_url"], api_key=ctx.obj["api_key"])
57+
user_data = {
58+
"user_email": email,
59+
"user_role": role,
60+
}
61+
if alias:
62+
user_data["user_alias"] = alias
63+
if team:
64+
user_data["teams"] = list(team)
65+
if max_budget is not None:
66+
user_data["max_budget"] = max_budget
67+
result = client.create_user(user_data)
68+
rich.print_json(data=result)
69+
70+
@users.command("delete")
71+
@click.argument("user_ids", nargs=-1)
72+
@click.pass_context
73+
def delete_user(ctx: click.Context, user_ids):
74+
"""Delete one or more users by user_id"""
75+
client = UsersManagementClient(base_url=ctx.obj["base_url"], api_key=ctx.obj["api_key"])
76+
result = client.delete_user(list(user_ids))
77+
rich.print_json(data=result)

litellm/proxy/client/cli/main.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from .commands.chat import chat
1111
from .commands.http import http
1212
from .commands.keys import keys
13+
from .commands.users import users
1314

1415

1516
@click.group()
@@ -44,6 +45,8 @@ def cli(ctx: click.Context, base_url: str, api_key: Optional[str]) -> None:
4445
cli.add_command(http)
4546
# Add the keys command group
4647
cli.add_command(keys)
48+
# Add the users command group
49+
cli.add_command(users)
4750

4851

4952
if __name__ == "__main__":

litellm/proxy/client/exceptions.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@
44
class UnauthorizedError(Exception):
55
"""Exception raised when the API returns a 401 Unauthorized response."""
66

7-
def __init__(self, orig_exception: requests.exceptions.HTTPError):
7+
def __init__(self, orig_exception: requests.exceptions.HTTPError | str):
88
self.orig_exception = orig_exception
99
super().__init__(str(orig_exception))
1010

1111

1212
class NotFoundError(Exception):
1313
"""Exception raised when the API returns a 404 Not Found response or indicates a resource was not found."""
1414

15-
def __init__(self, orig_exception: requests.exceptions.HTTPError):
15+
def __init__(self, orig_exception: requests.exceptions.HTTPError | str):
1616
self.orig_exception = orig_exception
1717
super().__init__(str(orig_exception))

litellm/proxy/client/users.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import requests
2+
from typing import List, Dict, Any, Optional
3+
from .exceptions import UnauthorizedError, NotFoundError
4+
5+
6+
class UsersManagementClient:
7+
def __init__(self, base_url: str, api_key: Optional[str] = None):
8+
self.base_url = base_url.rstrip("/")
9+
self.api_key = api_key
10+
11+
def _get_headers(self) -> Dict[str, str]:
12+
headers = {"Content-Type": "application/json"}
13+
if self.api_key:
14+
headers["Authorization"] = f"Bearer {self.api_key}"
15+
return headers
16+
17+
def list_users(self, params: Optional[Dict[str, Any]] = None) -> List[Dict[str, Any]]:
18+
"""List users (GET /user/list)"""
19+
url = f"{self.base_url}/user/list"
20+
response = requests.get(url, headers=self._get_headers(), params=params)
21+
if response.status_code == 401:
22+
raise UnauthorizedError(response.text)
23+
response.raise_for_status()
24+
return response.json().get("users", response.json())
25+
26+
def get_user(self, user_id: Optional[str] = None) -> Dict[str, Any]:
27+
"""Get user info (GET /user/info)"""
28+
url = f"{self.base_url}/user/info"
29+
params = {"user_id": user_id} if user_id else {}
30+
response = requests.get(url, headers=self._get_headers(), params=params)
31+
if response.status_code == 401:
32+
raise UnauthorizedError(response.text)
33+
if response.status_code == 404:
34+
raise NotFoundError(response.text)
35+
response.raise_for_status()
36+
return response.json()
37+
38+
def create_user(self, user_data: Dict[str, Any]) -> Dict[str, Any]:
39+
"""Create a new user (POST /user/new)"""
40+
url = f"{self.base_url}/user/new"
41+
response = requests.post(url, headers=self._get_headers(), json=user_data)
42+
if response.status_code == 401:
43+
raise UnauthorizedError(response.text)
44+
response.raise_for_status()
45+
return response.json()
46+
47+
def delete_user(self, user_ids: List[str]) -> Dict[str, Any]:
48+
"""Delete users (POST /user/delete)"""
49+
url = f"{self.base_url}/user/delete"
50+
response = requests.post(url, headers=self._get_headers(), json={"user_ids": user_ids})
51+
if response.status_code == 401:
52+
raise UnauthorizedError(response.text)
53+
response.raise_for_status()
54+
return response.json()
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import pytest
2+
from click.testing import CliRunner
3+
from unittest.mock import patch
4+
from litellm.proxy.client.cli import cli
5+
6+
@pytest.fixture
7+
def cli_runner():
8+
return CliRunner()
9+
10+
@pytest.fixture(autouse=True)
11+
def mock_env():
12+
with patch.dict("os.environ", {"LITELLM_PROXY_URL": "http://localhost:4000", "LITELLM_PROXY_API_KEY": "sk-test"}):
13+
yield
14+
15+
@pytest.fixture
16+
def mock_users_client():
17+
with patch("litellm.proxy.client.cli.commands.users.UsersManagementClient") as MockClient:
18+
yield MockClient
19+
20+
def test_users_list(cli_runner, mock_users_client):
21+
mock_users_client.return_value.list_users.return_value = [
22+
{"user_id": "u1", "user_email": "[email protected]", "user_role": "internal_user", "teams": ["t1"]},
23+
{"user_id": "u2", "user_email": "[email protected]", "user_role": "proxy_admin", "teams": ["t2", "t3"]},
24+
]
25+
result = cli_runner.invoke(cli, ["users", "list"])
26+
assert result.exit_code == 0
27+
assert "u1" in result.output
28+
assert "[email protected]" in result.output
29+
assert "proxy_admin" in result.output
30+
assert "t3" in result.output
31+
mock_users_client.return_value.list_users.assert_called_once()
32+
33+
def test_users_get(cli_runner, mock_users_client):
34+
mock_users_client.return_value.get_user.return_value = {"user_id": "u1", "user_email": "[email protected]"}
35+
result = cli_runner.invoke(cli, ["users", "get", "--id", "u1"])
36+
assert result.exit_code == 0
37+
assert '"user_id": "u1"' in result.output
38+
assert '"user_email": "[email protected]"' in result.output
39+
mock_users_client.return_value.get_user.assert_called_once_with(user_id="u1")
40+
41+
def test_users_create(cli_runner, mock_users_client):
42+
mock_users_client.return_value.create_user.return_value = {"user_id": "u1", "user_email": "[email protected]"}
43+
result = cli_runner.invoke(cli, ["users", "create", "--email", "[email protected]", "--role", "internal_user"])
44+
assert result.exit_code == 0
45+
assert '"user_id": "u1"' in result.output
46+
assert '"user_email": "[email protected]"' in result.output
47+
mock_users_client.return_value.create_user.assert_called_once()
48+
49+
def test_users_delete(cli_runner, mock_users_client):
50+
mock_users_client.return_value.delete_user.return_value = {"deleted": 1}
51+
result = cli_runner.invoke(cli, ["users", "delete", "u1", "u2"])
52+
assert result.exit_code == 0
53+
assert '"deleted": 1' in result.output
54+
mock_users_client.return_value.delete_user.assert_called_once_with(["u1", "u2"])

0 commit comments

Comments
 (0)