From df390267523616c6384631b2a51b3c8cad8dc6a8 Mon Sep 17 00:00:00 2001 From: xodn348 Date: Sat, 2 May 2026 09:56:01 +0000 Subject: [PATCH] fix: guard end_user_id or-fallback with disable_end_user_cost_tracking flag When `litellm.disable_end_user_cost_tracking=True` the helper `get_end_user_id_for_cost_tracking()` correctly returns `None`, but `get_logging_payload()` would then immediately override that `None` via an or-fallback reading `standard_logging_payload["metadata"]["user_api_key_end_user_id"]`, causing end-user IDs to leak into SpendLogs even when tracking was disabled. Wrapping the fallback in `if not litellm.disable_end_user_cost_tracking:` ensures the flag is respected end-to-end. Fixes #27038 --- .../spend_tracking/spend_tracking_utils.py | 7 ++- .../logging_callback_tests/test_spend_logs.py | 57 +++++++++++++++++++ 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/litellm/proxy/spend_tracking/spend_tracking_utils.py b/litellm/proxy/spend_tracking/spend_tracking_utils.py index ec6245f47e9..dcd85d2567c 100644 --- a/litellm/proxy/spend_tracking/spend_tracking_utils.py +++ b/litellm/proxy/spend_tracking/spend_tracking_utils.py @@ -295,9 +295,10 @@ def get_logging_payload( # noqa: PLR0915 or standard_logging_payload["metadata"].get("user_api_key_hash") or "" ) - end_user_id = end_user_id or standard_logging_payload["metadata"].get( - "user_api_key_end_user_id" - ) + if not litellm.disable_end_user_cost_tracking: + end_user_id = end_user_id or standard_logging_payload["metadata"].get( + "user_api_key_end_user_id" + ) # BUG FIX: Don't overwrite api_key when standard_logging_payload is None # The api_key was already extracted from metadata (line 243) and hashed (lines 256-259) request_tags = ( diff --git a/tests/logging_callback_tests/test_spend_logs.py b/tests/logging_callback_tests/test_spend_logs.py index 131de5992fa..f125138def6 100644 --- a/tests/logging_callback_tests/test_spend_logs.py +++ b/tests/logging_callback_tests/test_spend_logs.py @@ -558,3 +558,60 @@ def test_truncation_preserves_beginning_and_end(): kept_chars = expected_start_chars + expected_end_chars expected_skipped = total_chars - kept_chars assert str(expected_skipped) in truncated_content + + +def test_disable_end_user_cost_tracking_blocks_or_fallback(): + """ + Regression test for https://github.com/BerriAI/litellm/issues/27038 + + When litellm.disable_end_user_cost_tracking=True, the or-fallback inside + get_logging_payload() must not repopulate end_user from + standard_logging_payload["metadata"]["user_api_key_end_user_id"]. + """ + original = litellm.disable_end_user_cost_tracking + try: + litellm.disable_end_user_cost_tracking = True + + input_args: dict = { + "kwargs": { + "model": "gpt-3.5-turbo", + "messages": [{"role": "user", "content": "Hello"}], + "litellm_params": { + "metadata": { + "user_api_key": "fake_key", + "user_api_key_end_user_id": "should-be-blocked", + } + }, + "standard_logging_object": { + "metadata": { + "user_api_key_end_user_id": "should-be-blocked", + }, + "request_tags": [], + "model_map_information": {"tpm": 0, "rpm": 0}, + }, + }, + "response_obj": litellm.ModelResponse( + id="chatcmpl-test-disable-end-user", + choices=[ + litellm.Choices( + finish_reason="stop", + index=0, + message=litellm.Message(content="Hi", role="assistant"), + ) + ], + model="gpt-3.5-turbo", + usage=litellm.Usage( + completion_tokens=1, prompt_tokens=1, total_tokens=2 + ), + ), + "start_time": datetime.datetime.now(), + "end_time": datetime.datetime.now(), + } + + payload: SpendLogsPayload = get_logging_payload(**input_args) + + assert payload["end_user"] == "", ( + f"Expected end_user='' when disable_end_user_cost_tracking=True, got {payload['end_user']!r}" + ) + finally: + litellm.disable_end_user_cost_tracking = original