mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-13 03:59:12 +00:00
Merge 3697023a02 into 2a01fe6e5c
This commit is contained in:
commit
e92d507a0b
4 changed files with 60 additions and 18 deletions
|
|
@ -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))])
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue