From eef8c01d4ee2c43ac2f1da13a782969f94c20f32 Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Sat, 8 Nov 2025 12:29:38 -0800 Subject: [PATCH] test_redaction_responses_api --- litellm/integrations/custom_logger.py | 28 ++++++++++--- .../test_logging_redaction_e2e_test.py | 42 +++++++++++++++++-- 2 files changed, 61 insertions(+), 9 deletions(-) diff --git a/litellm/integrations/custom_logger.py b/litellm/integrations/custom_logger.py index fd8ab2bad9d..c425a806352 100644 --- a/litellm/integrations/custom_logger.py +++ b/litellm/integrations/custom_logger.py @@ -557,11 +557,29 @@ class CustomLogger: # https://docs.litellm.ai/docs/observability/custom_callbac standard_logging_object_copy["messages"] = [Message(content=redacted_str).model_dump()] if standard_logging_object_copy.get("response") is not None: - model_response = ModelResponse( - choices=[Choices(message=Message(content=redacted_str))] - ) - model_response_dict = model_response.model_dump() - standard_logging_object_copy["response"] = model_response_dict + response = standard_logging_object_copy["response"] + # Check if this is a ResponsesAPIResponse (has "output" field) + if isinstance(response, dict) and "output" in response: + # Make a copy to avoid modifying the original + from copy import deepcopy + response_copy = deepcopy(response) + # Redact content in output array + if isinstance(response_copy.get("output"), list): + for output_item in response_copy["output"]: + if isinstance(output_item, dict) and "content" in output_item: + if isinstance(output_item["content"], list): + # Redact text in content items + for content_item in output_item["content"]: + if isinstance(content_item, dict) and "text" in content_item: + content_item["text"] = redacted_str + standard_logging_object_copy["response"] = response_copy + else: + # Standard ModelResponse format + model_response = ModelResponse( + choices=[Choices(message=Message(content=redacted_str))] + ) + model_response_dict = model_response.model_dump() + standard_logging_object_copy["response"] = model_response_dict model_call_details_copy["standard_logging_object"] = standard_logging_object_copy return model_call_details_copy diff --git a/tests/logging_callback_tests/test_logging_redaction_e2e_test.py b/tests/logging_callback_tests/test_logging_redaction_e2e_test.py index 07ec6126467..c093957ec4a 100644 --- a/tests/logging_callback_tests/test_logging_redaction_e2e_test.py +++ b/tests/logging_callback_tests/test_logging_redaction_e2e_test.py @@ -145,8 +145,25 @@ async def test_redaction_responses_api(): assert standard_logging_payload is not None # Verify redaction in ResponsesAPIResponse format - assert standard_logging_payload["response"] == {"text": "redacted-by-litellm"} - assert standard_logging_payload["messages"][0]["content"] == "redacted-by-litellm" + # The response is now the full ResponsesAPIResponse object with transformed usage + assert isinstance(standard_logging_payload["response"], dict) + assert "usage" in standard_logging_payload["response"] + # Check that usage has been transformed to chat completion format + assert "prompt_tokens" in standard_logging_payload["response"]["usage"] + assert "completion_tokens" in standard_logging_payload["response"]["usage"] + from litellm.types.utils import LiteLLMCommonStrings + redacted_str = LiteLLMCommonStrings.redacted_by_litellm.value + + assert standard_logging_payload["messages"][0]["content"] == redacted_str + + # Verify that output content is redacted + assert "output" in standard_logging_payload["response"] + output_items = standard_logging_payload["response"]["output"] + for output_item in output_items: + if "content" in output_item and isinstance(output_item["content"], list): + for content_item in output_item["content"]: + if "text" in content_item: + assert content_item["text"] == redacted_str, f"Expected redacted text but got: {content_item['text']}" print( "logged standard logging payload for ResponsesAPIResponse", json.dumps(standard_logging_payload, indent=2), @@ -194,8 +211,25 @@ async def test_redaction_responses_api_stream(): assert standard_logging_payload is not None # Verify redaction in ResponsesAPIResponse format - assert standard_logging_payload["response"] == {"text": "redacted-by-litellm"} - assert standard_logging_payload["messages"][0]["content"] == "redacted-by-litellm" + from litellm.types.utils import LiteLLMCommonStrings + redacted_str = LiteLLMCommonStrings.redacted_by_litellm.value + + # The streaming response is in ModelResponse format (choices), not ResponsesAPIResponse format (output) + assert isinstance(standard_logging_payload["response"], dict) + assert standard_logging_payload["messages"][0]["content"] == redacted_str + + # Verify that response content is redacted (ModelResponse format) + if "choices" in standard_logging_payload["response"]: + # ModelResponse format + assert standard_logging_payload["response"]["choices"][0]["message"]["content"] == redacted_str + elif "output" in standard_logging_payload["response"]: + # ResponsesAPIResponse format + output_items = standard_logging_payload["response"]["output"] + for output_item in output_items: + if "content" in output_item and isinstance(output_item["content"], list): + for content_item in output_item["content"]: + if "text" in content_item: + assert content_item["text"] == redacted_str, f"Expected redacted text but got: {content_item['text']}" print( "logged standard logging payload for ResponsesAPIResponse stream", json.dumps(standard_logging_payload, indent=2),