mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-10 08:28:25 +00:00
fix(proxy): keep a pass-through response when a recorded guardrail value can't be encoded
A custom guardrail that records a value JSON can't encode, or a mixed-key dict in a list field, raised while the guardrail headers were built and turned the provider's reply into a 500. The guardrail headers are now left out on that response.
This commit is contained in:
parent
4afb8292d5
commit
a9670e88db
2 changed files with 32 additions and 2 deletions
|
|
@ -290,7 +290,7 @@ def _with_header_in_budget(kept: Mapping[str, str], header: tuple[str, str]) ->
|
|||
return MappingProxyType({**kept, header[0]: header[1]}) if size <= _GUARDRAIL_HEADERS_MAX_BYTES else kept
|
||||
|
||||
|
||||
def _guardrail_response_headers(
|
||||
def _recorded_guardrail_headers(
|
||||
parsed_body: object = None, kwargs: object = None, post_call_data: object = None
|
||||
) -> Mapping[str, str]:
|
||||
"""The ``x-litellm-applied-guardrails`` family of response headers for what guardrails recorded on this request.
|
||||
|
|
@ -329,6 +329,19 @@ def _guardrail_response_headers(
|
|||
return reduce(_with_header_in_budget, sendable, no_headers)
|
||||
|
||||
|
||||
def _guardrail_response_headers(
|
||||
parsed_body: object = None, kwargs: object = None, post_call_data: object = None
|
||||
) -> Mapping[str, str]:
|
||||
"""``_recorded_guardrail_headers``, or none of them when a recorded value can't be merged or JSON-encoded."""
|
||||
try:
|
||||
return _recorded_guardrail_headers(parsed_body, kwargs, post_call_data)
|
||||
except (TypeError, ValueError, RecursionError) as error:
|
||||
verbose_proxy_logger.warning(
|
||||
"Leaving out guardrail response headers: a recorded value can't be encoded (%s)", type(error).__name__
|
||||
)
|
||||
return MappingProxyType({})
|
||||
|
||||
|
||||
class _RegisteredPassThroughRoute(TypedDict):
|
||||
endpoint_id: ReadOnly[str]
|
||||
path: ReadOnly[str | None]
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import zlib
|
|||
from collections.abc import AsyncIterator, Callable, Mapping, Sequence
|
||||
from contextlib import ExitStack, contextmanager
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from io import BytesIO
|
||||
from types import MappingProxyType, ModuleType, SimpleNamespace
|
||||
from typing import Final, Optional, cast
|
||||
|
|
@ -13804,6 +13804,23 @@ class TestGuardrailResponseHeaders:
|
|||
== {}
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"entry",
|
||||
[{"s1"}, {"at": datetime(2026, 1, 1, tzinfo=timezone.utc)}, object()],
|
||||
ids=["set", "datetime", "object"],
|
||||
)
|
||||
def test_scan_metadata_json_cannot_encode_costs_the_headers_not_the_response(self, entry):
|
||||
assert _guardrail_response_headers(post_call_data={"metadata": {"guardrail_scan_metadata": [entry]}}) == {}
|
||||
|
||||
@pytest.mark.parametrize("field", ["applied_guardrails", "guardrail_scan_ids", "guardrail_scan_metadata"])
|
||||
def test_mixed_key_entry_recorded_before_and_after_the_call_costs_the_headers_not_the_response(self, field):
|
||||
entries: Final = [{1: "a", "b": 2}]
|
||||
|
||||
assert (
|
||||
_guardrail_response_headers({"metadata": {field: entries}}, post_call_data={"metadata": {field: entries}})
|
||||
== {}
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize("control", ["\x01", "\x1f", "\x7f", "\x00", "\n"])
|
||||
def test_guardrail_name_with_a_control_character_stays_out_of_the_header(self, control):
|
||||
assert _guardrail_response_headers({"metadata": {"applied_guardrails": [f"a{control}b"]}}) == {}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue