From 0c23db8f263593ccd4c528afb150daa4c5ea5148 Mon Sep 17 00:00:00 2001 From: brtydse100 Date: Sun, 8 Mar 2026 22:33:37 +0200 Subject: [PATCH 1/8] added the header mapping feature --- litellm/proxy/auth/auth_utils.py | 15 ++++++++++----- tests/local_testing/test_auth_utils.py | 23 ++++++++++++++++++++++- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/litellm/proxy/auth/auth_utils.py b/litellm/proxy/auth/auth_utils.py index 9a24041faad..7771bb5b516 100644 --- a/litellm/proxy/auth/auth_utils.py +++ b/litellm/proxy/auth/auth_utils.py @@ -662,11 +662,12 @@ def _has_user_setup_sso(): return sso_setup -def get_customer_user_header_from_mapping(user_id_mapping) -> Optional[str]: +def get_customer_user_header_from_mapping(user_id_mapping) -> Optional[list]: """Return the header_name mapped to CUSTOMER role, if any (dict-based).""" if not user_id_mapping: return None items = user_id_mapping if isinstance(user_id_mapping, list) else [user_id_mapping] + customer_headers_mappings = [] for item in items: if not isinstance(item, dict): continue @@ -675,7 +676,11 @@ def get_customer_user_header_from_mapping(user_id_mapping) -> Optional[str]: if role is None or not header_name: continue if str(role).lower() == str(LitellmUserRoles.CUSTOMER).lower(): - return header_name + customer_headers_mappings.append(header_name.lower()) + + if customer_headers_mappings: + return customer_headers_mappings + return None @@ -724,7 +729,7 @@ def get_end_user_id_from_request_body( # 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: - custom_header_name_to_check: Optional[str] = None + custom_header_name_to_check: Optional[Union[list, str]] = None # Prefer user mappings (new behavior) user_id_mapping = general_settings.get("user_header_mappings", None) @@ -741,9 +746,9 @@ def get_end_user_id_from_request_body( custom_header_name_to_check = value # If we have a header name to check, try to read it from request headers - if isinstance(custom_header_name_to_check, str): + if isinstance(custom_header_name_to_check, list): for header_name, header_value in request_headers.items(): - if header_name.lower() == custom_header_name_to_check.lower(): + if header_name.lower() in custom_header_name_to_check: user_id_from_header = header_value user_id_str = ( str(user_id_from_header) diff --git a/tests/local_testing/test_auth_utils.py b/tests/local_testing/test_auth_utils.py index d36f96b1a39..f06c7852d72 100644 --- a/tests/local_testing/test_auth_utils.py +++ b/tests/local_testing/test_auth_utils.py @@ -295,7 +295,7 @@ def test_get_internal_user_header_from_mapping_returns_internal_header(): ] result = LiteLLMProxyRequestSetup.get_internal_user_header_from_mapping(mappings) - assert result == "X-OpenWebUI-User-Id" + assert result == "X-OpenWebUI-User" def test_get_internal_user_header_from_mapping_no_internal_returns_none(): @@ -383,3 +383,24 @@ def test_get_model_from_request_vertex_ai_passthrough(request_data, route, expec model = get_model_from_request(request_data, route) assert model == expected_model + + +# def test_get_end_user_id_from_request_body_no_user_found(): +# """Test that function returns None when no user ID is found anywhere""" +# from litellm.proxy.auth.auth_utils import get_end_user_id_from_request_body +# from fastapi import Request +# from unittest.mock import MagicMock, patch + +# # Create a mock Request object with no relevant headers +# mock_request = MagicMock(spec=Request) +# mock_request.headers = {"X-Other-Header": "some-value"} + +# # Mock general_settings with user_header_name that doesn't match headers +# general_settings_config = {"user_header_name": "X-User-ID"} + +# # Request body with no user identifiers +# request_body = {"model": "gpt-4", "messages": [{"role": "user", "content": "hello"}]} + +# with patch('litellm.proxy.proxy_server.general_settings', general_settings_config): +# end_user_id = get_end_user_id_from_request_body(request_body, dict(mock_request.headers)) +# assert end_user_id is None \ No newline at end of file From 8e99eb8932ea74f2b80f9329324de2e32c449066 Mon Sep 17 00:00:00 2001 From: Iaagl Date: Sat, 14 Mar 2026 13:12:46 +0200 Subject: [PATCH 2/8] added tests --- .../llms/databricks/responses/__init__.py | 0 .../proxy/auth/test_auth_utils.py | 110 ++++++++++++++++++ 2 files changed, 110 insertions(+) delete mode 100644 tests/test_litellm/llms/databricks/responses/__init__.py diff --git a/tests/test_litellm/llms/databricks/responses/__init__.py b/tests/test_litellm/llms/databricks/responses/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/test_litellm/proxy/auth/test_auth_utils.py b/tests/test_litellm/proxy/auth/test_auth_utils.py index 82920ce1d80..e07f0c261da 100644 --- a/tests/test_litellm/proxy/auth/test_auth_utils.py +++ b/tests/test_litellm/proxy/auth/test_auth_utils.py @@ -209,3 +209,113 @@ def test_get_model_from_request_supports_google_model_names_with_slashes(): def test_get_model_from_request_vertex_passthrough_still_works(): route = "/vertex_ai/v1/projects/p/locations/l/publishers/google/models/gemini-1.5-pro:generateContent" assert get_model_from_request(request_data={}, route=route) == "gemini-1.5-pro" + + +def test_get_customer_user_header_returns_none_when_no_customer_role(): + from litellm.proxy.auth.auth_utils import get_customer_user_header_from_mapping + + mappings = [ + {"header_name": "X-OpenWebUI-User-Id", "litellm_user_role": "internal_user"} + ] + result = get_customer_user_header_from_mapping(mappings) + assert result is None + + +def test_get_customer_user_header_returns_none_for_single_non_customer_mapping(): + from litellm.proxy.auth.auth_utils import get_customer_user_header_from_mapping + + mapping = {"header_name": "X-Only-Internal", "litellm_user_role": "internal_user"} + result = get_customer_user_header_from_mapping(mapping) + assert result is None + +def test_get_customer_user_header_from_mapping_returns_customer_header(): + from litellm.proxy.auth.auth_utils import get_customer_user_header_from_mapping + + mappings = [ + {"header_name": "X-OpenWebUI-User-Id", "litellm_user_role": "internal_user"}, + {"header_name": "X-OpenWebUI-User-Email", "litellm_user_role": "customer"}, + ] + result = get_customer_user_header_from_mapping(mappings) + assert result == ["x-openwebui-user-email"] + + +def test_get_customer_user_header_returns_customers_header_when_multiple_exist(): + from litellm.proxy.auth.auth_utils import get_customer_user_header_from_mapping + + mappings = [ + {"header_name": "X-OpenWebUI-User-Id", "litellm_user_role": "internal_user"}, + {"header_name": "X-OpenWebUI-User-Email", "litellm_user_role": "customer"}, + {"header_name": "X-User-Id", "litellm_user_role": "customer"}, + ] + result = get_customer_user_header_from_mapping(mappings) + assert result == ['x-openwebui-user-email', 'x-user-id'] + +def test_get_customer_user_header_returns_sorted_customers_header_when_multiple_exist(): + from litellm.proxy.auth.auth_utils import get_customer_user_header_from_mapping + + mappings = [ + {"header_name": "X-OpenWebUI-User-Id", "litellm_user_role": "internal_user"}, + {"header_name": "X-User-Id", "litellm_user_role": "customer"}, + {"header_name": "X-OpenWebUI-User-Email", "litellm_user_role": "customer"}, + ] + result = get_customer_user_header_from_mapping(mappings) + assert result == ['x-user-id', 'x-openwebui-user-email'] + + +#################################### + + +def test_get_end_user_id_returns_id_from_user_header_mappings(): + from litellm.proxy.auth.auth_utils import get_end_user_id_from_request_body + + mappings = [ + {"header_name": "x-openwebui-user-id", "litellm_user_role": "internal_user"}, + {"header_name": "x-openwebui-user-email", "litellm_user_role": "customer"}, + ] + general_settings = {"user_header_mappings": mappings} + headers = {"x-openwebui-user-email": "1234"} + + with patch("litellm.proxy.auth.auth_utils._get_customer_id_from_standard_headers", return_value=None), \ + patch("litellm.proxy.proxy_server.general_settings", general_settings): + result = get_end_user_id_from_request_body(request_body={}, request_headers=headers) + + assert result == "1234" + + +def test_get_end_user_id_returns_first_customer_header_when_multiple_mappings_exist(): + from litellm.proxy.auth.auth_utils import get_end_user_id_from_request_body + + mappings = [ + {"header_name": "x-openwebui-user-id", "litellm_user_role": "internal_user"}, + {"header_name": "x-user-id", "litellm_user_role": "customer"}, + {"header_name": "x-openwebui-user-email", "litellm_user_role": "customer"}, + ] + general_settings = {"user_header_mappings": mappings} + headers = { + "x-user-id": "user-456", + "x-openwebui-user-email": "user@example.com", + } + + with patch("litellm.proxy.auth.auth_utils._get_customer_id_from_standard_headers", return_value=None), \ + patch("litellm.proxy.proxy_server.general_settings", general_settings): + result = get_end_user_id_from_request_body(request_body={}, request_headers=headers) + + assert result == "user-456" + + +def test_get_end_user_id_returns_none_when_no_customer_role_in_mappings(): + from litellm.proxy.auth.auth_utils import get_end_user_id_from_request_body + + mappings = [ + {"header_name": "x-openwebui-user-id", "litellm_user_role": "internal_user"}, + ] + general_settings = {"user_header_mappings": mappings} + headers = {"x-openwebui-user-id": "user-789"} + + with patch("litellm.proxy.auth.auth_utils._get_customer_id_from_standard_headers", return_value=None), \ + patch("litellm.proxy.proxy_server.general_settings", general_settings): + result = get_end_user_id_from_request_body(request_body={}, request_headers=headers) + + assert result is None + + From 0f6c097e26d0ed7ed5123d83c007ba1af9699d6f Mon Sep 17 00:00:00 2001 From: Iaagl Date: Sat, 14 Mar 2026 22:19:16 +0200 Subject: [PATCH 3/8] final cleanup --- tests/local_testing/test_auth_utils.py | 21 ------------------- .../proxy/auth/test_auth_utils.py | 5 +---- 2 files changed, 1 insertion(+), 25 deletions(-) diff --git a/tests/local_testing/test_auth_utils.py b/tests/local_testing/test_auth_utils.py index f06c7852d72..aec8aee3c79 100644 --- a/tests/local_testing/test_auth_utils.py +++ b/tests/local_testing/test_auth_utils.py @@ -383,24 +383,3 @@ def test_get_model_from_request_vertex_ai_passthrough(request_data, route, expec model = get_model_from_request(request_data, route) assert model == expected_model - - -# def test_get_end_user_id_from_request_body_no_user_found(): -# """Test that function returns None when no user ID is found anywhere""" -# from litellm.proxy.auth.auth_utils import get_end_user_id_from_request_body -# from fastapi import Request -# from unittest.mock import MagicMock, patch - -# # Create a mock Request object with no relevant headers -# mock_request = MagicMock(spec=Request) -# mock_request.headers = {"X-Other-Header": "some-value"} - -# # Mock general_settings with user_header_name that doesn't match headers -# general_settings_config = {"user_header_name": "X-User-ID"} - -# # Request body with no user identifiers -# request_body = {"model": "gpt-4", "messages": [{"role": "user", "content": "hello"}]} - -# with patch('litellm.proxy.proxy_server.general_settings', general_settings_config): -# end_user_id = get_end_user_id_from_request_body(request_body, dict(mock_request.headers)) -# assert end_user_id is None \ No newline at end of file diff --git a/tests/test_litellm/proxy/auth/test_auth_utils.py b/tests/test_litellm/proxy/auth/test_auth_utils.py index e07f0c261da..7fa04d587a3 100644 --- a/tests/test_litellm/proxy/auth/test_auth_utils.py +++ b/tests/test_litellm/proxy/auth/test_auth_utils.py @@ -260,10 +260,7 @@ def test_get_customer_user_header_returns_sorted_customers_header_when_multiple_ ] result = get_customer_user_header_from_mapping(mappings) assert result == ['x-user-id', 'x-openwebui-user-email'] - - -#################################### - + def test_get_end_user_id_returns_id_from_user_header_mappings(): from litellm.proxy.auth.auth_utils import get_end_user_id_from_request_body From 2138fb1962e8c90b25f31dec254f81738513d478 Mon Sep 17 00:00:00 2001 From: Iaagl Date: Sat, 14 Mar 2026 22:21:30 +0200 Subject: [PATCH 4/8] final cleanup --- tests/local_testing/test_auth_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/local_testing/test_auth_utils.py b/tests/local_testing/test_auth_utils.py index aec8aee3c79..d36f96b1a39 100644 --- a/tests/local_testing/test_auth_utils.py +++ b/tests/local_testing/test_auth_utils.py @@ -295,7 +295,7 @@ def test_get_internal_user_header_from_mapping_returns_internal_header(): ] result = LiteLLMProxyRequestSetup.get_internal_user_header_from_mapping(mappings) - assert result == "X-OpenWebUI-User" + assert result == "X-OpenWebUI-User-Id" def test_get_internal_user_header_from_mapping_no_internal_returns_none(): From 0f63d0e5e1de03b8a1ee875febcc579b3e20e39b Mon Sep 17 00:00:00 2001 From: Iaagl Date: Sat, 14 Mar 2026 23:22:13 +0200 Subject: [PATCH 5/8] added missing test and logic --- litellm/proxy/auth/auth_utils.py | 5 ++++ .../proxy/auth/test_auth_utils.py | 23 +++++++++---------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/litellm/proxy/auth/auth_utils.py b/litellm/proxy/auth/auth_utils.py index 7771bb5b516..971d836634e 100644 --- a/litellm/proxy/auth/auth_utils.py +++ b/litellm/proxy/auth/auth_utils.py @@ -757,6 +757,11 @@ def get_end_user_id_from_request_body( ) if user_id_str.strip(): return user_id_str + elif isinstance(custom_header_name_to_check, str): + user_id_from_header = request_headers.get(custom_header_name_to_check) + user_id_str = str(user_id_from_header) if user_id_from_header is not None else "" + if user_id_str.strip(): + return user_id_str # Check 3: 'user' field in request_body (commonly OpenAI) if "user" in request_body and request_body["user"] is not None: diff --git a/tests/test_litellm/proxy/auth/test_auth_utils.py b/tests/test_litellm/proxy/auth/test_auth_utils.py index 7fa04d587a3..5e42b110aa0 100644 --- a/tests/test_litellm/proxy/auth/test_auth_utils.py +++ b/tests/test_litellm/proxy/auth/test_auth_utils.py @@ -239,7 +239,7 @@ def test_get_customer_user_header_from_mapping_returns_customer_header(): assert result == ["x-openwebui-user-email"] -def test_get_customer_user_header_returns_customers_header_when_multiple_exist(): +def test_get_customer_user_header_returns_customers_header_in_config_order_when_multiple_exist(): from litellm.proxy.auth.auth_utils import get_customer_user_header_from_mapping mappings = [ @@ -250,17 +250,6 @@ def test_get_customer_user_header_returns_customers_header_when_multiple_exist() result = get_customer_user_header_from_mapping(mappings) assert result == ['x-openwebui-user-email', 'x-user-id'] -def test_get_customer_user_header_returns_sorted_customers_header_when_multiple_exist(): - from litellm.proxy.auth.auth_utils import get_customer_user_header_from_mapping - - mappings = [ - {"header_name": "X-OpenWebUI-User-Id", "litellm_user_role": "internal_user"}, - {"header_name": "X-User-Id", "litellm_user_role": "customer"}, - {"header_name": "X-OpenWebUI-User-Email", "litellm_user_role": "customer"}, - ] - result = get_customer_user_header_from_mapping(mappings) - assert result == ['x-user-id', 'x-openwebui-user-email'] - def test_get_end_user_id_returns_id_from_user_header_mappings(): from litellm.proxy.auth.auth_utils import get_end_user_id_from_request_body @@ -315,4 +304,14 @@ def test_get_end_user_id_returns_none_when_no_customer_role_in_mappings(): assert result is None +def test_get_end_user_id_falls_back_to_deprecated_user_header_name(): + from litellm.proxy.auth.auth_utils import get_end_user_id_from_request_body + + general_settings = {"user_header_name": "x-custom-user-id"} + headers = {"x-custom-user-id": "user-legacy"} + + with patch("litellm.proxy.auth.auth_utils._get_customer_id_from_standard_headers", return_value=None), \ + patch("litellm.proxy.proxy_server.general_settings", general_settings): + result = get_end_user_id_from_request_body(request_body={}, request_headers=headers) + assert result == "user-legacy" From c3d7808debc93b487680ec16f0cc40fc387e06e1 Mon Sep 17 00:00:00 2001 From: Iaagl Date: Sat, 14 Mar 2026 23:34:18 +0200 Subject: [PATCH 6/8] fixed header sending bug --- litellm/proxy/auth/auth_utils.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/litellm/proxy/auth/auth_utils.py b/litellm/proxy/auth/auth_utils.py index 971d836634e..f5a5de7f001 100644 --- a/litellm/proxy/auth/auth_utils.py +++ b/litellm/proxy/auth/auth_utils.py @@ -747,21 +747,24 @@ def get_end_user_id_from_request_body( # If we have a header name to check, try to read it from request headers if isinstance(custom_header_name_to_check, list): + headers_lower = {k.lower(): v for k, v in request_headers.items()} + for expected_header in custom_header_name_to_check: + header_value = headers_lower.get(expected_header) + if header_value is not None: + user_id_str = str(header_value) + if user_id_str.strip(): + return user_id_str + + elif isinstance(custom_header_name_to_check, str): for header_name, header_value in request_headers.items(): - if header_name.lower() in custom_header_name_to_check: - user_id_from_header = header_value + if header_name.lower() == custom_header_name_to_check: user_id_str = ( - str(user_id_from_header) - if user_id_from_header is not None + str(header_value) + if header_value is not None else "" ) if user_id_str.strip(): return user_id_str - elif isinstance(custom_header_name_to_check, str): - user_id_from_header = request_headers.get(custom_header_name_to_check) - user_id_str = str(user_id_from_header) if user_id_from_header is not None else "" - if user_id_str.strip(): - return user_id_str # Check 3: 'user' field in request_body (commonly OpenAI) if "user" in request_body and request_body["user"] is not None: From 34f140b5268bb3089cd4bce98d64546541ca60cd Mon Sep 17 00:00:00 2001 From: brtydse100 <92057527+brtydse100@users.noreply.github.com> Date: Sat, 14 Mar 2026 23:46:08 +0200 Subject: [PATCH 7/8] Update litellm/proxy/auth/auth_utils.py Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- litellm/proxy/auth/auth_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/proxy/auth/auth_utils.py b/litellm/proxy/auth/auth_utils.py index f5a5de7f001..0d3c627446b 100644 --- a/litellm/proxy/auth/auth_utils.py +++ b/litellm/proxy/auth/auth_utils.py @@ -757,7 +757,7 @@ def get_end_user_id_from_request_body( elif isinstance(custom_header_name_to_check, str): for header_name, header_value in request_headers.items(): - if header_name.lower() == custom_header_name_to_check: + if header_name.lower() == custom_header_name_to_check.lower(): user_id_str = ( str(header_value) if header_value is not None From a4db364bdb391763a97d83014bb24cb140c90bf6 Mon Sep 17 00:00:00 2001 From: Iaagl Date: Sun, 15 Mar 2026 00:03:31 +0200 Subject: [PATCH 8/8] added back init file in responses + fixed test_auth_utils.py int local_testing --- tests/local_testing/test_auth_utils.py | 2 +- tests/test_litellm/llms/databricks/responses/__init__.py | 0 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 tests/test_litellm/llms/databricks/responses/__init__.py diff --git a/tests/local_testing/test_auth_utils.py b/tests/local_testing/test_auth_utils.py index d36f96b1a39..bffcb40baf7 100644 --- a/tests/local_testing/test_auth_utils.py +++ b/tests/local_testing/test_auth_utils.py @@ -268,7 +268,7 @@ def test_get_customer_user_header_from_mapping_returns_customer_header(): {"header_name": "X-OpenWebUI-User-Email", "litellm_user_role": "customer"}, ] result = get_customer_user_header_from_mapping(mappings) - assert result == "X-OpenWebUI-User-Email" + assert result == ["x-openwebui-user-email"] def test_get_customer_user_header_from_mapping_no_customer_returns_none(): diff --git a/tests/test_litellm/llms/databricks/responses/__init__.py b/tests/test_litellm/llms/databricks/responses/__init__.py new file mode 100644 index 00000000000..e69de29bb2d