mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-10 03:28:53 +00:00
fix(logging): wholesale-clear response provider_specific_fields when message logging is off
Some checks failed
Unit Tests: Security / security (push) Has been cancelled
Unit Tests: Proxy DB Operations / assert-shard-coverage (push) Has been cancelled
Unit Tests: Proxy DB Operations / budgets (push) Has been cancelled
Unit Tests: Proxy DB Operations / custom-logging (push) Has been cancelled
Unit Tests: Proxy DB Operations / db-and-spend (push) Has been cancelled
Unit Tests: Proxy DB Operations / endpoints-and-responses (push) Has been cancelled
Unit Tests: Proxy DB Operations / guardrails-hooks (push) Has been cancelled
Unit Tests: Proxy DB Operations / jwt-and-keys (push) Has been cancelled
Unit Tests: Proxy DB Operations / key-generation (push) Has been cancelled
Unit Tests: Proxy DB Operations / logging-misc (push) Has been cancelled
Unit Tests: Proxy DB Operations / proxy-runtime (push) Has been cancelled
Unit Tests: Proxy DB Operations / proxy-server-core (push) Has been cancelled
Unit Tests: Proxy DB Operations / schema-migration (push) Has been cancelled
Unit Tests: Proxy DB Operations / proxy-utils (push) Has been cancelled
Unit Tests: Proxy DB Operations / auth-checks (push) Has been cancelled
Some checks failed
Unit Tests: Security / security (push) Has been cancelled
Unit Tests: Proxy DB Operations / assert-shard-coverage (push) Has been cancelled
Unit Tests: Proxy DB Operations / budgets (push) Has been cancelled
Unit Tests: Proxy DB Operations / custom-logging (push) Has been cancelled
Unit Tests: Proxy DB Operations / db-and-spend (push) Has been cancelled
Unit Tests: Proxy DB Operations / endpoints-and-responses (push) Has been cancelled
Unit Tests: Proxy DB Operations / guardrails-hooks (push) Has been cancelled
Unit Tests: Proxy DB Operations / jwt-and-keys (push) Has been cancelled
Unit Tests: Proxy DB Operations / key-generation (push) Has been cancelled
Unit Tests: Proxy DB Operations / logging-misc (push) Has been cancelled
Unit Tests: Proxy DB Operations / proxy-runtime (push) Has been cancelled
Unit Tests: Proxy DB Operations / proxy-server-core (push) Has been cancelled
Unit Tests: Proxy DB Operations / schema-migration (push) Has been cancelled
Unit Tests: Proxy DB Operations / proxy-utils (push) Has been cancelled
Unit Tests: Proxy DB Operations / auth-checks (push) Has been cancelled
This commit is contained in:
parent
e223a02c8e
commit
1b766e1c34
2 changed files with 54 additions and 44 deletions
|
|
@ -63,33 +63,26 @@ def _redact_choice_content(choice):
|
|||
)
|
||||
|
||||
|
||||
# Keys inside Message.provider_specific_fields that duplicate reasoning
|
||||
# content already covered by the flat-field scrub. Anthropic populates
|
||||
# "thinking_blocks" / "reasoning_content" (the latter also carries the
|
||||
# raw "signature" blob); Bedrock converse populates "reasoningContentBlocks".
|
||||
#
|
||||
# This is an intentional allowlist: we redact known reasoning-bearing keys
|
||||
# rather than wiping all of provider_specific_fields (which also carries
|
||||
# benign provider metadata). Any future provider field that embeds raw
|
||||
# reasoning or prompt content must be added here to be redacted.
|
||||
_PROVIDER_SPECIFIC_REASONING_KEYS = (
|
||||
"reasoning_content",
|
||||
"thinking_blocks",
|
||||
"reasoningContentBlocks",
|
||||
)
|
||||
def _redact_provider_specific_fields(psf):
|
||||
"""Wholesale-clear a response Message/Delta ``provider_specific_fields`` dict.
|
||||
|
||||
|
||||
def _redact_provider_specific_fields(psf, redacted_str: str = "redacted-by-litellm"):
|
||||
"""Scrub reasoning-content duplicates inside Message.provider_specific_fields."""
|
||||
This is a provider-NATIVE grab-bag. Beyond the reasoning duplicates
|
||||
(``reasoning_content``/``thinking_blocks``/``reasoningContentBlocks``),
|
||||
providers stash output-bearing content under ~20 other keys an allowlist can
|
||||
never enumerate: Anthropic ``citations``/``web_search_results``/
|
||||
``tool_results``/``code_interpreter_results``/``compaction_blocks``, Bedrock
|
||||
``citationsContent``, Gemini ``thought_signatures``/
|
||||
``server_side_tool_invocations``, Cohere ``tool_plan``, MCP
|
||||
``mcp_call_results``, RAG ``search_results``, and so on — the same
|
||||
unenumerable shape as the provider-native request body. Redaction only ever
|
||||
runs on the logging copy, and no consumer reads a named key off that copy
|
||||
(streaming reassembly and multi-turn replay read the untouched live response
|
||||
/ request history instead), so we clear the whole dict to close the class
|
||||
instead of chasing keys one provider at a time.
|
||||
"""
|
||||
if not isinstance(psf, dict):
|
||||
return
|
||||
for key in _PROVIDER_SPECIFIC_REASONING_KEYS:
|
||||
if key not in psf:
|
||||
continue
|
||||
if key == "reasoning_content":
|
||||
psf[key] = redacted_str
|
||||
else:
|
||||
psf[key] = None
|
||||
psf.clear()
|
||||
|
||||
|
||||
def _redact_responses_api_output(output_items):
|
||||
|
|
@ -276,7 +269,7 @@ def _redact_model_response_dict_choices(choices, redacted_str: str):
|
|||
if "audio" in choice["message"]:
|
||||
choice["message"]["audio"] = None
|
||||
_redact_provider_specific_fields(
|
||||
choice["message"].get("provider_specific_fields"), redacted_str
|
||||
choice["message"].get("provider_specific_fields")
|
||||
)
|
||||
elif "delta" in choice and isinstance(choice["delta"], dict):
|
||||
choice["delta"]["content"] = redacted_str
|
||||
|
|
@ -287,7 +280,7 @@ def _redact_model_response_dict_choices(choices, redacted_str: str):
|
|||
if "audio" in choice["delta"]:
|
||||
choice["delta"]["audio"] = None
|
||||
_redact_provider_specific_fields(
|
||||
choice["delta"].get("provider_specific_fields"), redacted_str
|
||||
choice["delta"].get("provider_specific_fields")
|
||||
)
|
||||
else:
|
||||
_redact_choice_content(choice)
|
||||
|
|
|
|||
|
|
@ -398,9 +398,10 @@ class TestPerformRedaction:
|
|||
)
|
||||
|
||||
def test_redacts_provider_specific_fields_on_object_choices(self):
|
||||
"""Anthropic reasoning content lives in both message.thinking_blocks
|
||||
AND message.provider_specific_fields.thinking_blocks. The flat field
|
||||
is scrubbed; ensure the duplicate is too (plus the signature blob)."""
|
||||
"""provider_specific_fields is a provider-native grab-bag carrying
|
||||
output content (reasoning, citations, web-search/tool/code-interpreter
|
||||
results, compaction). Redaction wholesale-clears it — every key gone,
|
||||
including otherwise-benign metadata — so no content can slip through."""
|
||||
result = litellm.ModelResponse(
|
||||
choices=[
|
||||
litellm.Choices(
|
||||
|
|
@ -420,6 +421,12 @@ class TestPerformRedaction:
|
|||
"signature": "RAW_SIGNATURE_BLOB",
|
||||
}
|
||||
],
|
||||
"citations": ["CANARY_CITATION"],
|
||||
"web_search_results": ["CANARY_SEARCH"],
|
||||
"tool_results": ["CANARY_TOOL"],
|
||||
"code_interpreter_results": ["CANARY_CODE"],
|
||||
"compaction_blocks": ["CANARY_COMPACTION"],
|
||||
"container": {"id": "benign-container-id"},
|
||||
},
|
||||
)
|
||||
)
|
||||
|
|
@ -429,12 +436,15 @@ class TestPerformRedaction:
|
|||
redacted = perform_redaction({}, result)
|
||||
|
||||
psf = redacted.choices[0].message.provider_specific_fields
|
||||
assert psf["reasoning_content"] == "redacted-by-litellm"
|
||||
assert psf["thinking_blocks"] is None
|
||||
assert psf == {}
|
||||
assert "CANARY" not in str(psf)
|
||||
assert "RAW_SIGNATURE_BLOB" not in str(psf)
|
||||
|
||||
def test_redacts_provider_specific_fields_bedrock_reasoning_content_blocks(self):
|
||||
"""Bedrock converse populates provider_specific_fields.reasoningContentBlocks.
|
||||
Covered by code symmetry — assert it via the dict path."""
|
||||
def test_redacts_provider_specific_fields_multi_provider_dict_path(self):
|
||||
"""The dict path (standard_logging_object) wholesale-clears the same
|
||||
grab-bag across providers — Bedrock reasoning/citations, Gemini thought
|
||||
signatures, MCP tool results, RAG search results — not just the old
|
||||
reasoning keys."""
|
||||
details = {
|
||||
"standard_logging_object": {
|
||||
"response": {
|
||||
|
|
@ -445,13 +455,13 @@ class TestPerformRedaction:
|
|||
"reasoning_content": "flat reasoning",
|
||||
"provider_specific_fields": {
|
||||
"reasoningContentBlocks": [
|
||||
{
|
||||
"reasoningText": {
|
||||
"text": "BEDROCK_THOUGHT",
|
||||
"signature": "sig",
|
||||
}
|
||||
}
|
||||
{"reasoningText": {"text": "BEDROCK_THOUGHT"}}
|
||||
],
|
||||
"citationsContent": ["CANARY_BEDROCK_CITE"],
|
||||
"thought_signatures": ["CANARY_SIGNATURE"],
|
||||
"server_side_tool_invocations": ["CANARY_SSTI"],
|
||||
"mcp_call_results": ["CANARY_MCP"],
|
||||
"search_results": ["CANARY_RAG"],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -464,10 +474,14 @@ class TestPerformRedaction:
|
|||
|
||||
choice = details["standard_logging_object"]["response"]["choices"][0]
|
||||
psf = choice["message"]["provider_specific_fields"]
|
||||
assert psf["reasoningContentBlocks"] is None
|
||||
assert psf == {}
|
||||
assert "CANARY" not in str(psf)
|
||||
assert "BEDROCK_THOUGHT" not in str(psf)
|
||||
|
||||
def test_redacts_provider_specific_fields_on_dict_delta(self):
|
||||
"""Streaming-style dict path: choice['delta']['provider_specific_fields']."""
|
||||
"""Streaming-style dict path: choice['delta']['provider_specific_fields']
|
||||
is wholesale-cleared too (streaming-only keys like compaction_delta
|
||||
included)."""
|
||||
result = {
|
||||
"choices": [
|
||||
{
|
||||
|
|
@ -480,6 +494,8 @@ class TestPerformRedaction:
|
|||
{"type": "thinking", "thinking": "STREAMED_THOUGHT"}
|
||||
],
|
||||
"reasoning_content": "psf streamed reasoning",
|
||||
"compaction_delta": {"content": "CANARY_COMPACTION_DELTA"},
|
||||
"citation": {"text": "CANARY_CITATION_DELTA"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -489,8 +505,9 @@ class TestPerformRedaction:
|
|||
redacted = perform_redaction({}, result)
|
||||
|
||||
psf = redacted["choices"][0]["delta"]["provider_specific_fields"]
|
||||
assert psf["thinking_blocks"] is None
|
||||
assert psf["reasoning_content"] == "redacted-by-litellm"
|
||||
assert psf == {}
|
||||
assert "CANARY" not in str(psf)
|
||||
assert "STREAMED_THOUGHT" not in str(psf)
|
||||
|
||||
def test_redact_provider_specific_fields_is_safe_when_absent(self):
|
||||
"""Messages without provider_specific_fields (the common case) must
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue