diff --git a/docs/my-website/docs/proxy/customer_usage.md b/docs/my-website/docs/proxy/customer_usage.md index 8e366586b15..5a6c06fdc81 100644 --- a/docs/my-website/docs/proxy/customer_usage.md +++ b/docs/my-website/docs/proxy/customer_usage.md @@ -22,19 +22,49 @@ 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 + + + +### 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' \ + --data '{ + "model": "gpt-3.5-turbo", + "user": "customer-123", + "messages": [ + { + "role": "user", + "content": "What is the capital of France?" + } + ] + }' +``` + + + + +### 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' \ # 👈 YOUR PROXY KEY + --header 'Authorization: Bearer sk-1234' \ + --header 'x-litellm-customer-id: customer-123' \ --data '{ "model": "gpt-3.5-turbo", - "user": "customer-123", # 👈 CUSTOMER ID "messages": [ { "role": "user", @@ -44,7 +74,22 @@ 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. +#### 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`. + + + + +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 diff --git a/litellm/constants.py b/litellm/constants.py index 4ea0be247b3..423cfb51d3f 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -1073,6 +1073,13 @@ ########################### LiteLLM Proxy Specific Constants ########################### ######################################################################################## + +# 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", +] MAX_SPENDLOG_ROWS_TO_QUERY = int( os.getenv("MAX_SPENDLOG_ROWS_TO_QUERY", 1_000_000) ) # if spendLogs has more than 1M rows, do not query the DB diff --git a/litellm/proxy/auth/auth_utils.py b/litellm/proxy/auth/auth_utils.py index 797540deaa4..1a7f05716b3 100644 --- a/litellm/proxy/auth/auth_utils.py +++ b/litellm/proxy/auth/auth_utils.py @@ -7,6 +7,7 @@ from litellm import Router, provider_list from litellm._logging import verbose_proxy_logger +from litellm.constants import STANDARD_CUSTOMER_ID_HEADERS from litellm.proxy._types import * from litellm.types.router import CONFIGURABLE_CLIENTSIDE_AUTH_PARAMS @@ -561,6 +562,32 @@ def get_customer_user_header_from_mapping(user_id_mapping) -> Optional[str]: return header_name return None +def _get_customer_id_from_standard_headers( + request_headers: Optional[dict], +) -> Optional[str]: + """ + Check standard customer ID headers for a customer/end-user ID. + + This enables tools like Claude Code to pass customer IDs via ANTHROPIC_CUSTOM_HEADERS. + No configuration required - these headers are always checked. + + Args: + request_headers: The request headers dict + + Returns: + The customer ID if found in standard headers, None otherwise + """ + if request_headers is None: + return 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 + return None + def get_end_user_id_from_request_body( request_body: dict, request_headers: Optional[dict] = None @@ -569,7 +596,12 @@ 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) + customer_id = _get_customer_id_from_standard_headers(request_headers=request_headers) + if customer_id is not None: + return customer_id + + # 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 +634,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") diff --git a/tests/test_litellm/proxy/auth/test_auth_utils.py b/tests/test_litellm/proxy/auth/test_auth_utils.py index b1bef63933e..62f9cc33b64 100644 --- a/tests/test_litellm/proxy/auth/test_auth_utils.py +++ b/tests/test_litellm/proxy/auth/test_auth_utils.py @@ -1,9 +1,13 @@ """ -Unit tests for auth_utils functions related to rate limiting. +Unit tests for auth_utils functions related to rate limiting and customer ID extraction. """ +from unittest.mock import patch + from litellm.proxy._types import UserAPIKeyAuth from litellm.proxy.auth.auth_utils import ( + _get_customer_id_from_standard_headers, + get_end_user_id_from_request_body, get_key_model_rpm_limit, get_key_model_tpm_limit, ) @@ -129,3 +133,56 @@ def test_model_max_budget_priority_over_team(self): ) result = get_key_model_tpm_limit(user_api_key_dict) assert result == {"gpt-4": 10000} + + +class TestGetCustomerIdFromStandardHeaders: + """Tests for _get_customer_id_from_standard_headers helper function.""" + + def test_should_return_customer_id_from_x_litellm_customer_id_header(self): + """Should extract customer ID from x-litellm-customer-id header.""" + headers = {"x-litellm-customer-id": "customer-123"} + result = _get_customer_id_from_standard_headers(request_headers=headers) + assert result == "customer-123" + + def test_should_return_customer_id_from_x_litellm_end_user_id_header(self): + """Should extract customer ID from x-litellm-end-user-id header.""" + headers = {"x-litellm-end-user-id": "end-user-456"} + result = _get_customer_id_from_standard_headers(request_headers=headers) + assert result == "end-user-456" + + def test_should_return_none_when_headers_is_none(self): + """Should return None when headers is None.""" + result = _get_customer_id_from_standard_headers(request_headers=None) + assert result is None + + def test_should_return_none_when_no_standard_headers_present(self): + """Should return None when no standard customer ID headers are present.""" + headers = {"x-other-header": "some-value"} + result = _get_customer_id_from_standard_headers(request_headers=headers) + assert result is None + + +class TestGetEndUserIdFromRequestBodyWithStandardHeaders: + """Tests for get_end_user_id_from_request_body with standard customer ID headers.""" + + def test_should_prioritize_standard_header_over_body_user(self): + """Standard customer ID header should take precedence over body user field.""" + headers = {"x-litellm-customer-id": "header-customer"} + request_body = {"user": "body-user"} + + with patch("litellm.proxy.proxy_server.general_settings", {}): + result = get_end_user_id_from_request_body( + request_body=request_body, request_headers=headers + ) + assert result == "header-customer" + + def test_should_fall_back_to_body_when_no_standard_header(self): + """Should fall back to body user when no standard headers are present.""" + headers = {"x-other-header": "value"} + request_body = {"user": "body-user"} + + with patch("litellm.proxy.proxy_server.general_settings", {}): + result = get_end_user_id_from_request_body( + request_body=request_body, request_headers=headers + ) + assert result == "body-user"