From 97474e4ae7b99494aa841ff25160729012b81fb1 Mon Sep 17 00:00:00 2001 From: Yucheng He Date: Tue, 6 Oct 2026 01:58:29 -0700 Subject: [PATCH] fix(proxy): drop caller metadata from proxy-only failure logs metadata and litellm_metadata leave the proxy-only failure-log allowlist, so a rejected request can no longer point its failure log at its own Langsmith or Langfuse host through nested metadata. (cherry picked from commit 30f6715f480ee7e2854514fe53b91d1b16e1be03) --- litellm/proxy/utils.py | 2 +- ..._utils_model_creation_and_error_logging.py | 55 ++----------------- .../test_post_call_failure_hook.py | 8 +-- 3 files changed, 9 insertions(+), 56 deletions(-) diff --git a/litellm/proxy/utils.py b/litellm/proxy/utils.py index d1a09b0b065..58b103ec739 100644 --- a/litellm/proxy/utils.py +++ b/litellm/proxy/utils.py @@ -1329,7 +1329,7 @@ def _call_type_for_route(route: str | None) -> str | None: _PROXY_ONLY_LLM_API_ERRORS: Final = (HTTPException, ProxyException, GuardrailRaisedException) _LOG_DB_METRICS_CALL_TYPES: Final = frozenset(("get_data", "insert_data", "update_data", "delete_data")) _PROXY_ONLY_ERROR_LOGGED_FIELDS: Final = frozenset( - {"model", "messages", "input", "prompt", "user", "stream", "metadata", "litellm_metadata", "litellm_call_id"} + {"model", "messages", "input", "prompt", "user", "stream", "litellm_call_id"} ) diff --git a/tests/unit/proxy/test_proxy_utils_model_creation_and_error_logging.py b/tests/unit/proxy/test_proxy_utils_model_creation_and_error_logging.py index 29f99bb76da..4121dca8ae1 100644 --- a/tests/unit/proxy/test_proxy_utils_model_creation_and_error_logging.py +++ b/tests/unit/proxy/test_proxy_utils_model_creation_and_error_logging.py @@ -116,59 +116,12 @@ async def test_proxy_only_error_log_marks_no_upstream_llm_call(): @pytest.mark.asyncio -async def test_proxy_only_error_log_keeps_litellm_metadata_in_litellm_params(): - """Responses API requests carry guardrail info under ``litellm_metadata`` - (not ``metadata``). It must land in litellm_params so - ``merge_litellm_metadata`` can surface ``guardrail_information`` in the - spend-log failure row, matching the chat completions path.""" - from litellm.proxy._types import UserAPIKeyAuth - - proxy_logging_obj = ProxyLogging(user_api_key_cache=DualCache()) - captured = {} - guardrail_info = [{"guardrail_name": "test-guard", "guardrail_status": "blocked"}] - - def fake_update_environment_variables(self, *args, **kwargs): - captured["litellm_params"] = kwargs.get("litellm_params") - captured["optional_params"] = kwargs.get("optional_params") - - from litellm.litellm_core_utils.litellm_logging import Logging - - orig_update_env = Logging.update_environment_variables - orig_pre_call = Logging.pre_call - orig_async_failure = Logging.async_failure_handler - - async def _noop_async_failure(self, *args, **kwargs): - return None - - Logging.update_environment_variables = fake_update_environment_variables - Logging.pre_call = lambda self, *args, **kwargs: None - Logging.async_failure_handler = _noop_async_failure - try: - await proxy_logging_obj._handle_logging_proxy_only_error( - request_data={ - "model": "gpt-4o", - "input": "blocked prompt", - "litellm_metadata": {"standard_logging_guardrail_information": guardrail_info}, - }, - user_api_key_dict=UserAPIKeyAuth(api_key="sk-9876", request_route="/v1/responses"), - route="/v1/responses", - original_exception=HTTPException(status_code=400, detail="blocked"), - ) - finally: - Logging.update_environment_variables = orig_update_env - Logging.pre_call = orig_pre_call - Logging.async_failure_handler = orig_async_failure - - assert captured["litellm_params"]["litellm_metadata"]["standard_logging_guardrail_information"] == guardrail_info - assert "litellm_metadata" not in captured["optional_params"] - - -@pytest.mark.asyncio -async def test_proxy_only_error_log_drops_caller_logging_fields_and_keeps_key_details(): +@pytest.mark.parametrize("metadata_field", ["metadata", "litellm_metadata"]) +async def test_proxy_only_error_log_drops_caller_logging_fields_and_metadata(metadata_field: str): request_data: Final[dict[str, object]] = { "model": "gpt-4o", "messages": [{"role": "user", "content": "hi"}], - "litellm_metadata": {"tags": ["t"]}, + metadata_field: {"langsmith_base_url": "https://caller.invalid", "langsmith_api_key": "caller-key"}, "langsmith_base_url": "https://caller.invalid", "standard_callback_dynamic_params": {"gcs_path_service_account": "/caller/sa.json"}, } @@ -183,7 +136,7 @@ async def test_proxy_only_error_log_drops_caller_logging_fields_and_keeps_key_de details: Final = request_data["litellm_logging_obj"].model_call_details assert "caller" not in str(details["standard_callback_dynamic_params"]) assert "langsmith_base_url" not in details - assert details["litellm_params"]["metadata"]["user_api_key_team_id"] == "team-a" + assert "caller" not in str(details["litellm_params"]) @pytest.mark.asyncio diff --git a/tests/unit/proxy/utils/proxy_logging/test_post_call_failure_hook.py b/tests/unit/proxy/utils/proxy_logging/test_post_call_failure_hook.py index 0e007429789..a9b399c550a 100644 --- a/tests/unit/proxy/utils/proxy_logging/test_post_call_failure_hook.py +++ b/tests/unit/proxy/utils/proxy_logging/test_post_call_failure_hook.py @@ -529,9 +529,9 @@ async def test_post_call_failure_hook_omits_provider_when_a_deployment_does_not_ async def test_handle_logging_proxy_only_path_attributes_with_read_only_metadata( proxy_logging, make_user_api_key_auth, monkeypatch ): - """With a logging object already on the request, its metadata is taken as given; - a read-only mapping there must not crash the stamp, and the failure handler - still receives the provider attribution.""" + """With a logging object already on the request, a read-only metadata mapping + must not crash the stamp, the failure handler still receives the provider + attribution, and the request's metadata is not copied into the update.""" from litellm.proxy import proxy_server monkeypatch.setattr( @@ -568,7 +568,7 @@ async def test_handle_logging_proxy_only_path_attributes_with_read_only_metadata update_kwargs = logging_obj.update_environment_variables.call_args.kwargs assert update_kwargs["custom_llm_provider"] == "openai" assert update_kwargs["litellm_params"]["custom_llm_provider"] == "openai" - assert update_kwargs["litellm_params"]["metadata"] == {"user_api_key_alias": "frozen"} + assert "user_api_key_alias" not in update_kwargs["litellm_params"]["metadata"] @pytest.mark.asyncio