Add enforce user param functionality (#17088)

* feat: Add reject_metadata_tags to proxy config

Co-authored-by: krrishdholakia <krrishdholakia@gmail.com>

* Refactor: Rename reject_metadata_tags to reject_clientside_metadata_tags

Co-authored-by: krrishdholakia <krrishdholakia@gmail.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
Krish Dholakia
2025-11-25 09:36:24 -08:00
committed by GitHub
co-authored by Cursor Agent
parent 6dcb5425a5
commit 00e17c81a1
6 changed files with 337 additions and 0 deletions
@@ -104,6 +104,7 @@ general_settings:
disable_responses_id_security: boolean # turn off response ID security checks that prevent users from accessing other users' responses
enable_jwt_auth: boolean # allow proxy admin to auth in via jwt tokens with 'litellm_proxy_admin' in claims
enforce_user_param: boolean # requires all openai endpoint requests to have a 'user' param
reject_clientside_metadata_tags: boolean # if true, rejects requests with client-side 'metadata.tags' to prevent users from influencing budgets
allowed_routes: ["route1", "route2"] # list of allowed proxy API routes - a user can access. (currently JWT-Auth only)
key_management_system: google_kms # either google_kms or azure_kms
master_key: string
@@ -201,6 +202,7 @@ router_settings:
| disable_responses_id_security | boolean | If true, disables response ID security checks that prevent users from accessing response IDs from other users. When false (default), response IDs are encrypted with user information to ensure users can only access their own responses. Applies to /v1/responses endpoints |
| enable_jwt_auth | boolean | allow proxy admin to auth in via jwt tokens with 'litellm_proxy_admin' in claims. [Doc on JWT Tokens](token_auth) |
| enforce_user_param | boolean | If true, requires all OpenAI endpoint requests to have a 'user' param. [Doc on call hooks](call_hooks)|
| reject_clientside_metadata_tags | boolean | If true, rejects requests that contain client-side 'metadata.tags' to prevent users from influencing budgets by sending different tags. Tags can only be inherited from the API key metadata. |
| allowed_routes | array of strings | List of allowed proxy API routes a user can access [Doc on controlling allowed routes](enterprise#control-available-public-private-routes)|
| key_management_system | string | Specifies the key management system. [Doc Secret Managers](../secret) |
| master_key | string | The master key for the proxy [Set up Virtual Keys](virtual_keys) |
@@ -0,0 +1,120 @@
# Reject Client-Side Metadata Tags
## Overview
The `reject_clientside_metadata_tags` setting allows you to prevent users from passing client-side `metadata.tags` in their API requests. This ensures that tags are only inherited from the API key metadata and cannot be overridden by users to potentially influence budget tracking or routing decisions.
## Use Case
This feature is particularly useful in multi-tenant scenarios where:
- You want to enforce strict budget tracking based on API key tags
- You want to prevent users from manipulating routing decisions by sending custom client-side tags
- You need to ensure consistent tag-based filtering and reporting
## Configuration
Add the following to your `config.yaml`:
```yaml
general_settings:
reject_clientside_metadata_tags: true # Default is false/null
```
## Behavior
### When `reject_clientside_metadata_tags: true`
**Rejected Request Example:**
```bash
curl -X POST http://localhost:4000/chat/completions \
-H "Authorization: Bearer sk-1234" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-3.5-turbo",
"messages": [{"role": "user", "content": "Hello"}],
"metadata": {
"tags": ["custom-tag"] # This will be rejected
}
}'
```
**Error Response:**
```json
{
"error": {
"message": "Client-side 'metadata.tags' not allowed in request. 'reject_clientside_metadata_tags'=True. Tags can only be set via API key metadata.",
"type": "bad_request_error",
"param": "metadata.tags",
"code": 400
}
}
```
**Allowed Request Example:**
```bash
curl -X POST http://localhost:4000/chat/completions \
-H "Authorization: Bearer sk-1234" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-3.5-turbo",
"messages": [{"role": "user", "content": "Hello"}],
"metadata": {
"custom_field": "value" # Other metadata fields are allowed
}
}'
```
### When `reject_clientside_metadata_tags: false` or not set
All requests are allowed, including those with client-side `metadata.tags`.
## Setting Tags via API Key
When `reject_clientside_metadata_tags` is enabled, tags should be set on the API key metadata:
```bash
curl -X POST http://localhost:4000/key/generate \
-H "Authorization: Bearer sk-master-key" \
-H "Content-Type: application/json" \
-d '{
"metadata": {
"tags": ["team-a", "production"]
}
}'
```
These tags will be automatically inherited by all requests made with that API key.
## Complete Example Configuration
```yaml
model_list:
- model_name: gpt-3.5-turbo
litellm_params:
model: gpt-3.5-turbo
api_key: os.environ/OPENAI_API_KEY
general_settings:
master_key: sk-1234
database_url: "postgresql://user:password@localhost:5432/litellm"
# Reject client-side tags
reject_clientside_metadata_tags: true
# Optional: Also enforce user parameter
enforce_user_param: true
```
## Similar Features
- `enforce_user_param` - Requires all requests to include a 'user' parameter
- Tag-based routing - Use tags for intelligent request routing
- Budget tracking - Track spending per tag
## Notes
- This check only applies to LLM API routes (e.g., `/chat/completions`, `/embeddings`)
- Management endpoints (e.g., `/key/generate`) are not affected
- The check validates that client-side `metadata.tags` is not present in the request body
- Other metadata fields can still be passed in requests
- Tags set on API keys will still be applied to all requests
+4
View File
@@ -1889,6 +1889,10 @@ class ConfigGeneralSettings(LiteLLMPydanticObjectBase):
allowed_routes: Optional[List] = Field(
None, description="Proxy API Endpoints you want users to be able to access"
)
reject_clientside_metadata_tags: Optional[bool] = Field(
None,
description="When set to True, rejects requests that contain client-side 'metadata.tags' to prevent users from influencing budgets by sending different tags. Tags can only be inherited from the API key metadata.",
)
enable_public_model_hub: bool = Field(
default=False,
description="Public model hub for users to see what models they have access to, supported openai params, etc.",
+18
View File
@@ -186,6 +186,24 @@ async def common_checks(
raise Exception(
f"'user' param not passed in. 'enforce_user_param'={general_settings['enforce_user_param']}"
)
# 6.1 [OPTIONAL] If 'reject_clientside_metadata_tags' enabled - reject request if it has client-side 'metadata.tags'
if (
general_settings.get("reject_clientside_metadata_tags", None) is not None
and general_settings["reject_clientside_metadata_tags"] is True
):
if (
RouteChecks.is_llm_api_route(route=route)
and "metadata" in request_body
and isinstance(request_body["metadata"], dict)
and "tags" in request_body["metadata"]
):
raise ProxyException(
message=f"Client-side 'metadata.tags' not allowed in request. 'reject_clientside_metadata_tags'={general_settings['reject_clientside_metadata_tags']}. Tags can only be set via API key metadata.",
type=ProxyErrorTypes.bad_request_error,
param="metadata.tags",
code=status.HTTP_400_BAD_REQUEST,
)
# 7. [OPTIONAL] If 'litellm.max_budget' is set (>0), is proxy under budget
if (
litellm.max_budget > 0
@@ -0,0 +1,14 @@
model_list:
- model_name: gpt-3.5-turbo
litellm_params:
model: gpt-3.5-turbo
api_key: os.environ/OPENAI_API_KEY
general_settings:
master_key: sk-1234
database_url: "postgresql://user:password@localhost:5432/litellm"
# Reject requests that contain client-side metadata.tags
# This prevents users from influencing budgets by sending different tags
# Tags can only be inherited from the API key metadata
reject_clientside_metadata_tags: true
@@ -732,3 +732,182 @@ async def test_get_team_object_raises_404_when_not_found():
assert exc_info.value.status_code == 404
assert "Team doesn't exist in db" in str(exc_info.value.detail)
# Reject Client-Side Metadata Tags Tests
@pytest.mark.asyncio
async def test_reject_clientside_metadata_tags_enabled_with_tags():
"""Test that common_checks rejects request when reject_clientside_metadata_tags is True and metadata.tags is present"""
from litellm.proxy.auth.auth_checks import common_checks
from fastapi import Request
request_body = {
"model": "gpt-3.5-turbo",
"messages": [{"role": "user", "content": "test"}],
"metadata": {"tags": ["custom-tag"]},
}
general_settings = {"reject_clientside_metadata_tags": True}
# Create a mock request object
mock_request = MagicMock(spec=Request)
with pytest.raises(ProxyException) as exc_info:
await common_checks(
request_body=request_body,
team_object=None,
user_object=None,
end_user_object=None,
global_proxy_spend=None,
general_settings=general_settings,
route="/chat/completions",
llm_router=None,
proxy_logging_obj=MagicMock(),
valid_token=None,
request=mock_request,
)
assert exc_info.value.type == ProxyErrorTypes.bad_request_error
assert "metadata.tags" in exc_info.value.message
assert exc_info.value.param == "metadata.tags"
assert exc_info.value.code == 400
@pytest.mark.asyncio
async def test_reject_clientside_metadata_tags_enabled_without_tags():
"""Test that common_checks allows request when reject_clientside_metadata_tags is True but no metadata.tags is present"""
from litellm.proxy.auth.auth_checks import common_checks
from fastapi import Request
request_body = {
"model": "gpt-3.5-turbo",
"messages": [{"role": "user", "content": "test"}],
"metadata": {"custom_field": "value"}, # No tags field
}
general_settings = {"reject_clientside_metadata_tags": True}
# Create a mock request object
mock_request = MagicMock(spec=Request)
# Should not raise an exception
result = await common_checks(
request_body=request_body,
team_object=None,
user_object=None,
end_user_object=None,
global_proxy_spend=None,
general_settings=general_settings,
route="/chat/completions",
llm_router=None,
proxy_logging_obj=MagicMock(),
valid_token=None,
request=mock_request,
)
assert result is True
@pytest.mark.asyncio
async def test_reject_clientside_metadata_tags_disabled_with_tags():
"""Test that common_checks allows request with metadata.tags when reject_clientside_metadata_tags is False"""
from litellm.proxy.auth.auth_checks import common_checks
from fastapi import Request
request_body = {
"model": "gpt-3.5-turbo",
"messages": [{"role": "user", "content": "test"}],
"metadata": {"tags": ["custom-tag"]},
}
general_settings = {"reject_clientside_metadata_tags": False}
# Create a mock request object
mock_request = MagicMock(spec=Request)
# Should not raise an exception
result = await common_checks(
request_body=request_body,
team_object=None,
user_object=None,
end_user_object=None,
global_proxy_spend=None,
general_settings=general_settings,
route="/chat/completions",
llm_router=None,
proxy_logging_obj=MagicMock(),
valid_token=None,
request=mock_request,
)
assert result is True
@pytest.mark.asyncio
async def test_reject_clientside_metadata_tags_not_set_with_tags():
"""Test that common_checks allows request with metadata.tags when reject_clientside_metadata_tags is not set"""
from litellm.proxy.auth.auth_checks import common_checks
from fastapi import Request
request_body = {
"model": "gpt-3.5-turbo",
"messages": [{"role": "user", "content": "test"}],
"metadata": {"tags": ["custom-tag"]},
}
general_settings = {} # No reject_clientside_metadata_tags setting
# Create a mock request object
mock_request = MagicMock(spec=Request)
# Should not raise an exception
result = await common_checks(
request_body=request_body,
team_object=None,
user_object=None,
end_user_object=None,
global_proxy_spend=None,
general_settings=general_settings,
route="/chat/completions",
llm_router=None,
proxy_logging_obj=MagicMock(),
valid_token=None,
request=mock_request,
)
assert result is True
@pytest.mark.asyncio
async def test_reject_clientside_metadata_tags_non_llm_route():
"""Test that reject_clientside_metadata_tags check only applies to LLM API routes"""
from litellm.proxy.auth.auth_checks import common_checks
from fastapi import Request
request_body = {
"metadata": {"tags": ["custom-tag"]},
}
general_settings = {"reject_clientside_metadata_tags": True}
# Create a mock request object
mock_request = MagicMock(spec=Request)
# Should not raise an exception for non-LLM route
result = await common_checks(
request_body=request_body,
team_object=None,
user_object=None,
end_user_object=None,
global_proxy_spend=None,
general_settings=general_settings,
route="/key/generate", # Management route, not LLM route
llm_router=None,
proxy_logging_obj=MagicMock(),
valid_token=None,
request=mock_request,
)
assert result is True