mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-05 02:41:56 +00:00
fix(responses): restore isinstance guards dropped in guardrail masking loops
The Any-cleanup pass on streaming_iterator.py collapsed the two-step "read into a variable, then isinstance-guard before iterating" pattern for output_item["content"]/["summary"] into direct one-liner for-loops, dropping the isinstance(..., list) checks in _unmask_response_event and _mask_response_completed. A response.completed event whose content or summary field is a truthy non-iterable value (malformed upstream payload) then raised TypeError instead of being skipped. Restores the original guards and adds a regression test. Also fixes two follow-on regressions surfaced by this and the earlier typing changes: _get_default_team_param in team_endpoints.py was returning a tuple instead of a list for list-shaped defaults, breaking callers that compare against list literals; and typing key_management_endpoints.py's prisma_client parameter as Optional required a None-guard in _validate_update_key_data that pushed its McCabe complexity 2 over the ratcheted budget, suppressed with a reasoned noqa since the guard itself is a correctness fix, not something to remove. Re-ran make lint-budget-update to reflect the net state after these fixes.
This commit is contained in:
parent
375d7199bf
commit
f4b77e0458
7 changed files with 90 additions and 42 deletions
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"reportAny": {
|
||||
"limit": 31306
|
||||
"limit": 30112
|
||||
},
|
||||
"reportArgumentType": {
|
||||
"limit": 2641
|
||||
"limit": 2633
|
||||
},
|
||||
"reportAssignmentType": {
|
||||
"limit": 329
|
||||
|
|
@ -24,7 +24,7 @@
|
|||
"limit": 42
|
||||
},
|
||||
"reportExplicitAny": {
|
||||
"limit": 10089
|
||||
"limit": 9839
|
||||
},
|
||||
"reportFunctionMemberAccess": {
|
||||
"limit": 11
|
||||
|
|
@ -54,10 +54,10 @@
|
|||
"limit": 0
|
||||
},
|
||||
"reportMissingParameterType": {
|
||||
"limit": 5858
|
||||
"limit": 5836
|
||||
},
|
||||
"reportMissingTypeArgument": {
|
||||
"limit": 15833
|
||||
"limit": 15777
|
||||
},
|
||||
"reportMissingTypeStubs": {
|
||||
"limit": 41
|
||||
|
|
@ -99,19 +99,19 @@
|
|||
"limit": 0
|
||||
},
|
||||
"reportUnknownArgumentType": {
|
||||
"limit": 45345
|
||||
"limit": 45301
|
||||
},
|
||||
"reportUnknownLambdaType": {
|
||||
"limit": 113
|
||||
},
|
||||
"reportUnknownMemberType": {
|
||||
"limit": 40390
|
||||
"limit": 40216
|
||||
},
|
||||
"reportUnknownParameterType": {
|
||||
"limit": 20299
|
||||
"limit": 20221
|
||||
},
|
||||
"reportUnknownVariableType": {
|
||||
"limit": 31974
|
||||
"limit": 31834
|
||||
},
|
||||
"reportUnnecessaryCast": {
|
||||
"limit": 177
|
||||
|
|
@ -123,7 +123,7 @@
|
|||
"limit": 7
|
||||
},
|
||||
"reportUnnecessaryIsInstance": {
|
||||
"limit": 1204
|
||||
"limit": 1202
|
||||
},
|
||||
"reportUntypedBaseClass": {
|
||||
"limit": 165
|
||||
|
|
|
|||
|
|
@ -2311,7 +2311,7 @@ async def _validate_mcp_servers_for_key_update(
|
|||
return normalized_object_permission
|
||||
|
||||
|
||||
async def _validate_update_key_data(
|
||||
async def _validate_update_key_data( # noqa: C901 # prisma_client None-guard needed now that it's typed Optional
|
||||
data: UpdateKeyRequest,
|
||||
existing_key_row: LiteLLM_VerificationToken,
|
||||
user_api_key_dict: UserAPIKeyAuth,
|
||||
|
|
|
|||
|
|
@ -498,7 +498,9 @@ def _get_default_team_param(field: str) -> _DefaultTeamParamValue | None:
|
|||
if raw_value is None:
|
||||
return None
|
||||
if isinstance(raw_value, list):
|
||||
return tuple(v.value if hasattr(v, "value") else v for v in raw_value)
|
||||
# mutable-ok: callers compare this against list literals (e.g. team_member_permissions
|
||||
# equality checks), so it must stay a real list, not a tuple, to match prior behavior.
|
||||
return [v.value if hasattr(v, "value") else v for v in raw_value]
|
||||
return _DEFAULT_TEAM_PARAM_ADAPTER.validate_python(raw_value)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1687,7 +1687,10 @@ class ResponsesWebSocketStreaming:
|
|||
for output_item in response_obj.get("output") or []:
|
||||
if not isinstance(output_item, dict):
|
||||
continue
|
||||
for content_block in output_item.get("content") or []:
|
||||
content = output_item.get("content") or []
|
||||
if not isinstance(content, list):
|
||||
continue
|
||||
for content_block in content:
|
||||
if not isinstance(content_block, dict):
|
||||
continue
|
||||
text = content_block.get("text")
|
||||
|
|
@ -1750,21 +1753,26 @@ class ResponsesWebSocketStreaming:
|
|||
if masked_args != arguments:
|
||||
output_item["arguments"] = masked_args
|
||||
modified = True
|
||||
for summary_block in output_item.get("summary") or []:
|
||||
if not isinstance(summary_block, dict):
|
||||
continue
|
||||
summary_text = summary_block.get("text")
|
||||
if isinstance(summary_text, str):
|
||||
masked_summary = await cb.check_pii(
|
||||
text=summary_text,
|
||||
output_parse_pii=False,
|
||||
presidio_config=presidio_config,
|
||||
request_data=self.request_data,
|
||||
)
|
||||
if masked_summary != summary_text:
|
||||
summary_block["text"] = masked_summary
|
||||
modified = True
|
||||
for content_block in output_item.get("content") or []:
|
||||
summary = output_item.get("summary") or []
|
||||
if isinstance(summary, list):
|
||||
for summary_block in summary:
|
||||
if not isinstance(summary_block, dict):
|
||||
continue
|
||||
summary_text = summary_block.get("text")
|
||||
if isinstance(summary_text, str):
|
||||
masked_summary = await cb.check_pii(
|
||||
text=summary_text,
|
||||
output_parse_pii=False,
|
||||
presidio_config=presidio_config,
|
||||
request_data=self.request_data,
|
||||
)
|
||||
if masked_summary != summary_text:
|
||||
summary_block["text"] = masked_summary
|
||||
modified = True
|
||||
content = output_item.get("content") or []
|
||||
if not isinstance(content, list):
|
||||
continue
|
||||
for content_block in content:
|
||||
if not isinstance(content_block, dict):
|
||||
continue
|
||||
text = content_block.get("text")
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"ANN001": {
|
||||
"limit": 3107
|
||||
"limit": 3085
|
||||
},
|
||||
"ANN002": {
|
||||
"limit": 69
|
||||
|
|
@ -9,13 +9,13 @@
|
|||
"limit": 831
|
||||
},
|
||||
"ANN201": {
|
||||
"limit": 2135
|
||||
"limit": 2129
|
||||
},
|
||||
"ANN202": {
|
||||
"limit": 943
|
||||
"limit": 941
|
||||
},
|
||||
"ANN204": {
|
||||
"limit": 723
|
||||
"limit": 721
|
||||
},
|
||||
"ANN205": {
|
||||
"limit": 127
|
||||
|
|
@ -24,7 +24,7 @@
|
|||
"limit": 130
|
||||
},
|
||||
"ANN401": {
|
||||
"limit": 1931
|
||||
"limit": 1773
|
||||
},
|
||||
"ASYNC230": {
|
||||
"limit": 14
|
||||
|
|
@ -135,7 +135,7 @@
|
|||
"limit": 30
|
||||
},
|
||||
"PERF401": {
|
||||
"limit": 140
|
||||
"limit": 136
|
||||
},
|
||||
"PERF402": {
|
||||
"limit": 9
|
||||
|
|
@ -222,7 +222,7 @@
|
|||
"limit": 38
|
||||
},
|
||||
"RET504": {
|
||||
"limit": 712
|
||||
"limit": 704
|
||||
},
|
||||
"RUF010": {
|
||||
"limit": 874
|
||||
|
|
@ -273,7 +273,7 @@
|
|||
"limit": 6
|
||||
},
|
||||
"SIM114": {
|
||||
"limit": 110
|
||||
"limit": 108
|
||||
},
|
||||
"SIM115": {
|
||||
"limit": 5
|
||||
|
|
@ -324,7 +324,7 @@
|
|||
"limit": 879
|
||||
},
|
||||
"UP006": {
|
||||
"limit": 12075
|
||||
"limit": 11949
|
||||
},
|
||||
"UP007": {
|
||||
"limit": 2526
|
||||
|
|
@ -360,9 +360,9 @@
|
|||
"limit": 4
|
||||
},
|
||||
"UP037": {
|
||||
"limit": 104
|
||||
"limit": 102
|
||||
},
|
||||
"UP045": {
|
||||
"limit": 17797
|
||||
"limit": 17781
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1084,6 +1084,44 @@ class TestNativeWebSocketGuardrails:
|
|||
assert handler._unmask_response_event(event) == event
|
||||
assert await handler._mask_response_completed(event) == event
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_completed_event_with_non_list_content_and_summary_passes_through(
|
||||
self,
|
||||
):
|
||||
"""Regression test: an output item whose "content"/"summary" is a truthy
|
||||
non-iterable value (malformed upstream payload) must be skipped, not raise."""
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from litellm.responses.streaming_iterator import ResponsesWebSocketStreaming
|
||||
|
||||
class Guardrail:
|
||||
def get_presidio_settings_from_request_data(self, request_data):
|
||||
return None
|
||||
|
||||
def _unmask_pii_text(self, text, pii_tokens):
|
||||
return text
|
||||
|
||||
event = json.dumps(
|
||||
{
|
||||
"type": "response.completed",
|
||||
"response": {
|
||||
"output": [{"type": "message", "content": 42, "summary": True}]
|
||||
},
|
||||
}
|
||||
)
|
||||
guardrail = Guardrail()
|
||||
handler = ResponsesWebSocketStreaming(
|
||||
websocket=MagicMock(),
|
||||
backend_ws=MagicMock(),
|
||||
logging_obj=MagicMock(),
|
||||
request_data={"metadata": {"pii_tokens": {"<TOKEN_1>": "secret"}}},
|
||||
guardrail_callbacks=[guardrail],
|
||||
output_guardrail_callbacks=[guardrail],
|
||||
)
|
||||
|
||||
assert handler._unmask_response_event(event) == event
|
||||
assert await handler._mask_response_completed(event) == event
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_output_masking_suppresses_delta_without_calling_presidio(self):
|
||||
import json
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"LIT001": {
|
||||
"limit": 23151
|
||||
"limit": 22947
|
||||
},
|
||||
"LIT002": {
|
||||
"limit": 27409
|
||||
"limit": 27375
|
||||
},
|
||||
"LIT003": {
|
||||
"limit": 292
|
||||
|
|
@ -24,6 +24,6 @@
|
|||
"limit": 1004
|
||||
},
|
||||
"LIT009": {
|
||||
"limit": 2466
|
||||
"limit": 2450
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue