This commit is contained in:
Felix Pan 2026-10-11 15:01:00 +03:00 • committed by GitHub
commit e92d507a0b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 60 additions and 18 deletions

View file

@ -910,7 +910,10 @@ class CustomLogger: # https://docs.litellm.ai/docs/observability/custom_callbac
import litellm
from litellm import Choices, Message, ModelResponse
from litellm.litellm_core_utils.classifier_logging import CLASSIFIER_AUDIT_FIELDS
from litellm.litellm_core_utils.redact_messages import redacted_litellm_params
from litellm.litellm_core_utils.redact_messages import (
redacted_litellm_params,
redacted_standard_logging_payload,
)
turn_off_message_logging: Final[bool] = getattr(self, "turn_off_message_logging", False)
excluded_fields: Final[list[str] | None] = getattr(litellm, "standard_logging_payload_excluded_fields", None)
@ -945,22 +948,12 @@ class CustomLogger: # https://docs.litellm.ai/docs/observability/custom_callbac
if "response" not in (excluded_fields or ()) and standard_logging_object_copy.get("response") is not None:
response: Final = 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: Final = 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
redaction_payload: Final[dict[str, object]] = {} # mutable-ok: populate the typed wrapper once
redaction_payload["response"] = response
standard_logging_object_copy["response"] = redacted_standard_logging_payload(redaction_payload)[
"response"
]
else:
# Standard ModelResponse format
model_response: Final = ModelResponse(choices=[Choices(message=Message(content=redacted_str))])

View file

@ -79,6 +79,8 @@ def _redact_streaming_response(streaming_response):
redact_vertex_ai_metadata_from_logged_object(streaming_response)
elif hasattr(streaming_response, "output"):
_redact_responses_api_output(streaming_response.output)
if getattr(streaming_response, "instructions", None) is not None:
streaming_response.instructions = REDACTED_BY_LITELLM # rebind-ok: redact the logged stream in place
if hasattr(streaming_response, "reasoning") and streaming_response.reasoning is not None:
streaming_response.reasoning = None
@ -195,7 +197,8 @@ def _redact_standard_logging_object(payload: Mapping[str, object]) -> dict[str,
response: Final = standard_logging_object.get("response")
if response is not None:
if isinstance(response, dict) and "output" in response:
# ResponsesAPIResponse format - redact content in output items
if "instructions" in response and response["instructions"] is not None:
response["instructions"] = redacted_str
if isinstance(response.get("output"), list):
_redact_responses_api_output_dict(response["output"], redacted_str)
redact_vertex_ai_metadata_from_logged_object(response)
@ -330,11 +333,15 @@ def perform_redaction(model_call_details: dict, result, redact_streaming_respons
_redact_model_response_dict_choices(_result["choices"], REDACTED_BY_LITELLM)
redact_vertex_ai_metadata_from_logged_object(_result)
elif isinstance(_result, dict) and "output" in _result:
if "instructions" in _result and _result["instructions"] is not None:
_result["instructions"] = REDACTED_BY_LITELLM
if isinstance(_result.get("output"), list):
_redact_responses_api_output_dict(_result["output"], REDACTED_BY_LITELLM)
elif isinstance(_result, litellm.ResponsesAPIResponse):
if hasattr(_result, "output"):
_redact_responses_api_output(_result.output)
if getattr(_result, "instructions", None) is not None:
_result.instructions = REDACTED_BY_LITELLM
# Redact reasoning field in ResponsesAPIResponse
if hasattr(_result, "reasoning") and _result.reasoning is not None:
_result.reasoning = None

View file

@ -104,6 +104,34 @@ def test_get_callback_env_vars():
assert none_env_vars == []
def test_responses_redaction_covers_instructions_reasoning_and_tool_arguments() -> None:
response: Final = {
"instructions": "INSTRUCTION_CANARY",
"output": [
{"type": "message", "content": [{"type": "output_text", "text": "ANSWER_CANARY"}]},
{"type": "reasoning", "summary": [{"type": "summary_text", "text": "REASONING_CANARY"}]},
{"type": "function_call", "name": "example_tool", "arguments": '{"value":"ARGUMENT_CANARY"}'},
],
}
details: Final = {"standard_logging_object": {"response": response}}
logger: Final = CustomLogger(turn_off_message_logging=True)
stored: Final = logger.redact_standard_logging_payload_from_model_call_details(details)["standard_logging_object"]
assert stored["response"] == {
"instructions": "redacted-by-litellm",
"output": [
{"type": "message", "content": [{"type": "output_text", "text": "redacted-by-litellm"}]},
{"type": "reasoning", "summary": [{"type": "summary_text", "text": "redacted-by-litellm"}]},
{"type": "function_call", "name": "example_tool", "arguments": "redacted-by-litellm"},
],
}
assert details["standard_logging_object"]["response"] is response
assert response["instructions"] == "INSTRUCTION_CANARY"
assert response["output"][1]["summary"][0]["text"] == "REASONING_CANARY"
assert response["output"][2]["arguments"] == '{"value":"ARGUMENT_CANARY"}'
class TestStandardLoggingPayloadExcludedFields:
"""Test suite for standard_logging_payload_excluded_fields feature."""

View file

@ -203,6 +203,7 @@ class TestPerformRedaction:
"standard_logging_object": {
"messages": [{"role": "user", "content": "sensitive input"}],
"response": {
"instructions": "sensitive instructions",
"output": [
{"text": "top-level text"},
{"content": [{"text": "nested text"}]},
@ -213,6 +214,7 @@ class TestPerformRedaction:
},
}
result = {
"instructions": "sensitive result instructions",
"output": [
{"text": "top-level result"},
{"content": [{"text": "nested result"}]},
@ -228,16 +230,19 @@ class TestPerformRedaction:
assert details["input"] == ""
logged_response = details["standard_logging_object"]["response"]
assert logged_response["instructions"] == "redacted-by-litellm"
assert logged_response["usage"] == {"total_tokens": 1}
assert logged_response["output"][0]["text"] == "redacted-by-litellm"
assert logged_response["output"][1]["content"][0]["text"] == ("redacted-by-litellm")
assert logged_response["output"][2]["summary"][0]["text"] == ("redacted-by-litellm")
assert redacted["usage"] == {"total_tokens": 1}
assert redacted["instructions"] == "redacted-by-litellm"
assert redacted["output"][0]["text"] == "redacted-by-litellm"
assert redacted["output"][1]["content"][0]["text"] == "redacted-by-litellm"
assert redacted["output"][2]["summary"][0]["text"] == "redacted-by-litellm"
assert result["output"][0]["text"] == "top-level result"
assert result["instructions"] == "sensitive result instructions"
def test_redacts_model_response_dict_choices(self):
result = {
@ -692,12 +697,21 @@ class TestPerformRedaction:
assert redacted["output"][1]["content"][0]["text"] == "redacted-by-litellm"
def test_redacts_responses_api_response_object(self):
response = mock_responses_api_response("sensitive output")
response = mock_responses_api_response("sensitive output").model_copy(
update={"instructions": "sensitive instructions"}
)
redacted = perform_redaction({}, response)
assert redacted.output[0].content[0].text == "redacted-by-litellm"
assert redacted.instructions == "redacted-by-litellm"
assert response.output[0].content[0].text == "sensitive output"
assert response.instructions == "sensitive instructions"
streamed: Final = response.model_copy(deep=True)
perform_redaction({"stream": True, "complete_streaming_response": streamed}, result=None)
assert streamed.instructions == "redacted-by-litellm"
assert response.instructions == "sensitive instructions"
def test_redacts_vertex_provider_metadata_in_standard_logging_response(self):
details = {