From 2df0b41923b489a1b29042a91e7916f0cd8a2ce8 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 +- tests/test_litellm/proxy/test_proxy_utils.py | 55 ++------------------ 2 files changed, 5 insertions(+), 52 deletions(-) diff --git a/litellm/proxy/utils.py b/litellm/proxy/utils.py index f1e8883a536..bded7a678f7 100644 --- a/litellm/proxy/utils.py +++ b/litellm/proxy/utils.py @@ -1195,7 +1195,7 @@ def _call_type_for_route(route: str | None) -> str | None: _PROXY_ONLY_LLM_API_ERRORS: Final = (HTTPException, ProxyException, GuardrailRaisedException) _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/test_litellm/proxy/test_proxy_utils.py b/tests/test_litellm/proxy/test_proxy_utils.py index 6a77464ed28..46d46549335 100644 --- a/tests/test_litellm/proxy/test_proxy_utils.py +++ b/tests/test_litellm/proxy/test_proxy_utils.py @@ -114,59 +114,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-1234", 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"}, } @@ -181,7 +134,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