mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-14 09:03:16 +00:00
Merge branch 'litellm_passthrough_auth_guardrails' into litellm_lakera_v2_gemini_contents
This commit is contained in:
commit
f0c416a9df
3 changed files with 28 additions and 13 deletions
|
|
@ -24,7 +24,7 @@ litellm/proxy/pass_through_endpoints/
|
|||
The `PassThroughEndpointHandler` enables guardrails to run on passthrough endpoint requests by:
|
||||
|
||||
1. **Field Targeting**: Extracts specific fields from the request/response using JSONPath expressions configured in `request_fields` / `response_fields`
|
||||
2. **Full Payload Fallback**: If no field targeting is configured, processes the entire request payload. A response is read whole, integers included, except a provider's reply, where the ids, timestamps and usage counts around the text are left out
|
||||
2. **Full Payload Fallback**: If no field targeting is configured, processes the entire request payload. A response is read whole, field names and integers included, except a provider's reply, where the ids, timestamps and usage counts around the text are left out
|
||||
3. **Config Access**: Uses `get_passthrough_guardrails_config()` / `set_passthrough_guardrails_config()` helpers to access the passthrough guardrails configuration stored in request metadata
|
||||
|
||||
## Example Config
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ It uses the field targeting configuration from litellm_logging_obj
|
|||
to extract specific fields for guardrail processing.
|
||||
"""
|
||||
|
||||
from collections.abc import AsyncIterable, AsyncIterator, Awaitable, Callable, Mapping, Sequence
|
||||
from collections.abc import AsyncIterable, AsyncIterator, Awaitable, Callable, Iterator, Mapping, Sequence
|
||||
from types import MappingProxyType
|
||||
from typing import TYPE_CHECKING, Final, Optional, Protocol, runtime_checkable
|
||||
|
||||
|
|
@ -73,10 +73,21 @@ def _reply_field_position(name: str, skipped: frozenset[str] | None) -> frozense
|
|||
return _REPLY_NESTED_FIELDS.get(name, _NO_FIELDS)
|
||||
|
||||
|
||||
def _reply_entries(
|
||||
fields: Mapping[str, object], skipped: frozenset[str] | None
|
||||
) -> Iterator[tuple[object, frozenset[str] | None]]:
|
||||
for name, item in fields.items():
|
||||
if skipped is None:
|
||||
yield name, None
|
||||
if skipped is None or name not in skipped:
|
||||
yield item, _reply_field_position(name, skipped)
|
||||
|
||||
|
||||
def _response_text(response: Mapping[str, object]) -> str:
|
||||
"""The text a guardrail reads from a JSON response. In a provider's reply that is its strings without the ids and
|
||||
encoded data around them, so a timestamp or a usage count doesn't read as a phone or card number. What the model
|
||||
wrote into a tool call, and any other API's JSON, is read whole, integers included. Floats are scores and vectors."""
|
||||
wrote into a tool call, and any other API's JSON, is read whole, field names and integers included. Floats are
|
||||
scores and vectors."""
|
||||
top_level: Final = _REPLY_TOP_LEVEL_FIELDS if _is_provider_reply(response) else None
|
||||
pending: Final[list[tuple[object, frozenset[str] | None]]] = [(response, top_level)] # mutable-ok: stack
|
||||
texts: Final[list[str]] = [] # mutable-ok: local to this walk
|
||||
|
|
@ -89,15 +100,7 @@ def _response_text(response: Mapping[str, object]) -> str:
|
|||
elif skipped is None and isinstance(value, int) and not isinstance(value, bool):
|
||||
texts.append(str(value))
|
||||
elif fields is not None:
|
||||
pending.extend(
|
||||
reversed(
|
||||
[
|
||||
(item, _reply_field_position(name, skipped))
|
||||
for name, item in fields.items()
|
||||
if skipped is None or name not in skipped
|
||||
]
|
||||
)
|
||||
)
|
||||
pending.extend(reversed(tuple(_reply_entries(fields, skipped))))
|
||||
elif items is not None:
|
||||
pending.extend((item, skipped) for item in reversed(items))
|
||||
return "\n".join(text for text in texts if text)
|
||||
|
|
|
|||
|
|
@ -289,7 +289,19 @@ async def test_post_call_scan_reads_a_field_named_like_the_envelope_in_what_the_
|
|||
],
|
||||
)
|
||||
async def test_post_call_scan_reads_an_integer_in_what_the_model_or_api_wrote(response, expected):
|
||||
assert await _scanned_response_texts(response) == (expected,)
|
||||
assert any(expected in text for text in await _scanned_response_texts(response))
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"response",
|
||||
[
|
||||
{"results": {"alice@example.com": "active"}},
|
||||
{"candidates": [{"content": {"parts": [{"functionCall": {"args": {"alice@example.com": "active"}}}]}}]},
|
||||
],
|
||||
)
|
||||
async def test_post_call_scan_reads_a_field_name_in_what_the_model_or_api_wrote(response):
|
||||
assert any("alice@example.com" in text for text in await _scanned_response_texts(response))
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue