mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-07 08:26:10 +00:00
feat: Add standard customer ID headers for tracking spend
Add support for x-litellm-customer-id and x-litellm-end-user-id headers that work out-of-the-box without any configuration. This enables tools like Claude Code to pass customer IDs via ANTHROPIC_CUSTOM_HEADERS. Changes: - Add STANDARD_CUSTOMER_ID_HEADERS constant with supported header names - Update get_end_user_id_from_request_body to check standard headers first - Add comprehensive tests for the new header-based customer ID feature - Update documentation with examples for header-based tracking and Claude Code setup The standard headers take precedence over configured user_header_name/user_header_mappings and request body fields (user, litellm_metadata.user, metadata.user_id). Co-authored-by: ishaan <ishaan@berri.ai>
This commit is contained in:
parent
e0a29cac99
commit
835e0c0e18
3 changed files with 150 additions and 10 deletions
|
|
@ -22,19 +22,22 @@ Customer Usage enables you to track spend and usage for individual customers (en
|
|||
|
||||
## How to Track Spend
|
||||
|
||||
Track customer spend by including a `user` field in your API requests. The customer ID will be automatically tracked and associated with all spend from that request.
|
||||
Track customer spend by including a `user` field in your API requests or by passing a customer ID header. The customer ID will be automatically tracked and associated with all spend from that request.
|
||||
|
||||
### Example using cURL
|
||||
<Tabs>
|
||||
<TabItem value="body" label="Request Body" default>
|
||||
|
||||
### Using Request Body
|
||||
|
||||
Make a `/chat/completions` call with the `user` field containing your customer ID:
|
||||
|
||||
```bash showLineNumbers title="Track spend with customer ID"
|
||||
```bash showLineNumbers title="Track spend with customer ID in body"
|
||||
curl -X POST 'http://0.0.0.0:4000/chat/completions' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'Authorization: Bearer sk-1234' \ # 👈 YOUR PROXY KEY
|
||||
--header 'Authorization: Bearer sk-1234' \
|
||||
--data '{
|
||||
"model": "gpt-3.5-turbo",
|
||||
"user": "customer-123", # 👈 CUSTOMER ID
|
||||
"user": "customer-123",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
|
|
@ -44,7 +47,49 @@ curl -X POST 'http://0.0.0.0:4000/chat/completions' \
|
|||
}'
|
||||
```
|
||||
|
||||
The customer ID (`customer-123`) will be automatically upserted into the database with the new spend. If the customer ID already exists, spend will be incremented.
|
||||
</TabItem>
|
||||
<TabItem value="header" label="Request Header">
|
||||
|
||||
### Using Request Headers
|
||||
|
||||
You can also pass the customer ID via HTTP headers. This is useful for tools that support custom headers but don't allow modifying the request body (like Claude Code with `ANTHROPIC_CUSTOM_HEADERS`).
|
||||
|
||||
LiteLLM automatically recognizes these standard headers (no configuration required):
|
||||
- `x-litellm-customer-id`
|
||||
- `x-litellm-end-user-id`
|
||||
|
||||
```bash showLineNumbers title="Track spend with customer ID in header"
|
||||
curl -X POST 'http://0.0.0.0:4000/chat/completions' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'Authorization: Bearer sk-1234' \
|
||||
--header 'x-litellm-customer-id: customer-123' \
|
||||
--data '{
|
||||
"model": "gpt-3.5-turbo",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "What is the capital of France?"
|
||||
}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
#### Using with Claude Code
|
||||
|
||||
Claude Code supports custom headers via the `ANTHROPIC_CUSTOM_HEADERS` environment variable. Set it to pass your customer ID:
|
||||
|
||||
```bash title="Configure Claude Code with customer tracking"
|
||||
export ANTHROPIC_BASE_URL="http://0.0.0.0:4000/v1/messages"
|
||||
export ANTHROPIC_API_KEY="sk-1234"
|
||||
export ANTHROPIC_CUSTOM_HEADERS="x-litellm-customer-id: my-customer-id"
|
||||
```
|
||||
|
||||
Now all requests from Claude Code will automatically track spend under `my-customer-id`.
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
The customer ID will be automatically upserted into the database with the new spend. If the customer ID already exists, spend will be incremented.
|
||||
|
||||
### Example using OpenWebUI
|
||||
|
||||
|
|
|
|||
|
|
@ -562,6 +562,14 @@ def get_customer_user_header_from_mapping(user_id_mapping) -> Optional[str]:
|
|||
return None
|
||||
|
||||
|
||||
# Standard headers that are always checked for customer/end-user ID (no configuration required)
|
||||
# These headers work out-of-the-box for tools like Claude Code that support custom headers
|
||||
STANDARD_CUSTOMER_ID_HEADERS = [
|
||||
"x-litellm-customer-id",
|
||||
"x-litellm-end-user-id",
|
||||
]
|
||||
|
||||
|
||||
def get_end_user_id_from_request_body(
|
||||
request_body: dict, request_headers: Optional[dict] = None
|
||||
) -> Optional[str]:
|
||||
|
|
@ -569,7 +577,19 @@ def get_end_user_id_from_request_body(
|
|||
# and to ensure it's fetched at runtime.
|
||||
from litellm.proxy.proxy_server import general_settings
|
||||
|
||||
# Check 1 : Follow the user header mappings feature, if not found, then check for deprecated user_header_name (only if request_headers is provided)
|
||||
# Check 1: Standard customer ID headers (always checked, no configuration required)
|
||||
# This enables tools like Claude Code to pass customer IDs via ANTHROPIC_CUSTOM_HEADERS
|
||||
if request_headers is not None:
|
||||
for standard_header in STANDARD_CUSTOMER_ID_HEADERS:
|
||||
for header_name, header_value in request_headers.items():
|
||||
if header_name.lower() == standard_header.lower():
|
||||
user_id_str = (
|
||||
str(header_value) if header_value is not None else ""
|
||||
)
|
||||
if user_id_str.strip():
|
||||
return user_id_str
|
||||
|
||||
# Check 2: Follow the user header mappings feature, if not found, then check for deprecated user_header_name (only if request_headers is provided)
|
||||
# User query: "system not respecting user_header_name property"
|
||||
# This implies the key in general_settings is 'user_header_name'.
|
||||
if request_headers is not None:
|
||||
|
|
@ -602,19 +622,19 @@ def get_end_user_id_from_request_body(
|
|||
if user_id_str.strip():
|
||||
return user_id_str
|
||||
|
||||
# Check 2: 'user' field in request_body (commonly OpenAI)
|
||||
# Check 3: 'user' field in request_body (commonly OpenAI)
|
||||
if "user" in request_body and request_body["user"] is not None:
|
||||
user_from_body_user_field = request_body["user"]
|
||||
return str(user_from_body_user_field)
|
||||
|
||||
# Check 3: 'litellm_metadata.user' in request_body (commonly Anthropic)
|
||||
# Check 4: 'litellm_metadata.user' in request_body (commonly Anthropic)
|
||||
litellm_metadata = request_body.get("litellm_metadata")
|
||||
if isinstance(litellm_metadata, dict):
|
||||
user_from_litellm_metadata = litellm_metadata.get("user")
|
||||
if user_from_litellm_metadata is not None:
|
||||
return str(user_from_litellm_metadata)
|
||||
|
||||
# Check 4: 'metadata.user_id' in request_body (another common pattern)
|
||||
# Check 5: 'metadata.user_id' in request_body (another common pattern)
|
||||
metadata_dict = request_body.get("metadata")
|
||||
if isinstance(metadata_dict, dict):
|
||||
user_id_from_metadata_field = metadata_dict.get("user_id")
|
||||
|
|
|
|||
|
|
@ -260,6 +260,81 @@ def test_get_model_from_request(request_data, expected_model):
|
|||
assert model == ["gpt-3.5-turbo", "gpt-4o-mini-general-deployment"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"headers, request_body, expected_user_id",
|
||||
[
|
||||
# Test 1: x-litellm-customer-id header takes precedence over body
|
||||
(
|
||||
{"x-litellm-customer-id": "customer-from-header"},
|
||||
{"user": "body-user-456"},
|
||||
"customer-from-header"
|
||||
),
|
||||
# Test 2: x-litellm-end-user-id header works as alternative
|
||||
(
|
||||
{"x-litellm-end-user-id": "end-user-from-header"},
|
||||
{"user": "body-user-456"},
|
||||
"end-user-from-header"
|
||||
),
|
||||
# Test 3: x-litellm-customer-id takes precedence over x-litellm-end-user-id
|
||||
(
|
||||
{
|
||||
"x-litellm-customer-id": "customer-id",
|
||||
"x-litellm-end-user-id": "end-user-id"
|
||||
},
|
||||
{"user": "body-user-456"},
|
||||
"customer-id"
|
||||
),
|
||||
# Test 4: Standard headers work with case-insensitive matching
|
||||
(
|
||||
{"X-LiteLLM-Customer-ID": "customer-uppercase"},
|
||||
{"user": "body-user-456"},
|
||||
"customer-uppercase"
|
||||
),
|
||||
# Test 5: Empty standard header falls back to body
|
||||
(
|
||||
{"x-litellm-customer-id": ""},
|
||||
{"user": "body-user-456"},
|
||||
"body-user-456"
|
||||
),
|
||||
# Test 6: Standard header works without any body user
|
||||
(
|
||||
{"x-litellm-customer-id": "header-only-customer"},
|
||||
{"model": "gpt-4"},
|
||||
"header-only-customer"
|
||||
),
|
||||
# Test 7: No standard header present, falls back to body
|
||||
(
|
||||
{"x-other-header": "some-value"},
|
||||
{"user": "body-user-456"},
|
||||
"body-user-456"
|
||||
),
|
||||
]
|
||||
)
|
||||
def test_get_end_user_id_from_standard_customer_headers(
|
||||
headers, request_body, expected_user_id
|
||||
):
|
||||
"""
|
||||
Test that standard customer ID headers (x-litellm-customer-id, x-litellm-end-user-id)
|
||||
work without any configuration. This enables tools like Claude Code to pass customer
|
||||
IDs via ANTHROPIC_CUSTOM_HEADERS.
|
||||
"""
|
||||
from litellm.proxy.auth.auth_utils import get_end_user_id_from_request_body
|
||||
from unittest.mock import patch
|
||||
|
||||
# Mock general_settings with no user_header_name configured
|
||||
with patch('litellm.proxy.proxy_server.general_settings', {}):
|
||||
end_user_id = get_end_user_id_from_request_body(request_body, headers)
|
||||
assert end_user_id == expected_user_id
|
||||
|
||||
|
||||
def test_standard_customer_headers_constant():
|
||||
"""Test that the standard customer ID headers constant is defined correctly."""
|
||||
from litellm.proxy.auth.auth_utils import STANDARD_CUSTOMER_ID_HEADERS
|
||||
|
||||
assert "x-litellm-customer-id" in STANDARD_CUSTOMER_ID_HEADERS
|
||||
assert "x-litellm-end-user-id" in STANDARD_CUSTOMER_ID_HEADERS
|
||||
|
||||
|
||||
def test_get_customer_user_header_from_mapping_returns_customer_header():
|
||||
from litellm.proxy.auth.auth_utils import get_customer_user_header_from_mapping
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue