mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-10 03:28:53 +00:00
fix(guardrails): accept Gemini-native tools without a type field
GuardrailToolParam required type: str, so payloads like {"googleSearch": {}}
failed validation under generic_guardrail_api / headroom with default_on and
surfaced as a 500 before the guardrail ran. Make type optional and omit a
null type on serialize so provider-native tools are forwarded verbatim.
Fixes #42742
This commit is contained in:
parent
e73f949fbb
commit
b45175e1af
2 changed files with 48 additions and 5 deletions
|
|
@ -1,7 +1,7 @@
|
|||
from collections.abc import Mapping, Sequence
|
||||
from typing import Any, Final, Literal, cast # noqa: TID251 # JSON chat rows have no typed constructor across roles
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from pydantic import BaseModel, ConfigDict, Field, model_serializer
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from litellm.types.llms.openai import (
|
||||
|
|
@ -15,13 +15,22 @@ from litellm.types.utils import ChatCompletionMessageToolCall
|
|||
class GuardrailToolParam(BaseModel):
|
||||
"""A tool forwarded verbatim to the guardrail for inspection.
|
||||
|
||||
Built-in tools (code_interpreter, file_search, ...) have no ``function`` block
|
||||
and stash their config in tool-specific keys, so only ``type`` is required and
|
||||
``extra="allow"`` preserves the rest instead of stripping it.
|
||||
OpenAI-style tools carry a ``type`` (function / code_interpreter / file_search,
|
||||
...). Provider-native tools such as Gemini ``{"googleSearch": {}}`` have neither
|
||||
``type`` nor ``function``; ``extra="allow"`` keeps their keys intact so the
|
||||
guardrail still sees the original payload. ``type`` is therefore optional.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(extra="allow")
|
||||
type: str
|
||||
type: str | None = None
|
||||
|
||||
@model_serializer(mode="wrap")
|
||||
def _omit_null_type(self, handler):
|
||||
"""Keep provider-native tools free of a synthetic ``type: null`` field."""
|
||||
data = handler(self)
|
||||
if isinstance(data, dict) and data.get("type") is None:
|
||||
data.pop("type", None)
|
||||
return data
|
||||
|
||||
|
||||
class GenericGuardrailAPIMetadata(TypedDict, total=False):
|
||||
|
|
|
|||
|
|
@ -2015,6 +2015,40 @@ class TestToolSupport:
|
|||
|
||||
assert forwarded_tools == tools
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_gemini_native_tools_without_type_do_not_crash(
|
||||
self, generic_guardrail
|
||||
):
|
||||
"""Gemini-native tools have no ``type`` key at all (e.g. googleSearch).
|
||||
|
||||
Regression for #42742: GuardrailToolParam required ``type: str``, so
|
||||
validating ``{"googleSearch": {}}`` raised before the guardrail ran and
|
||||
surfaced as a 500 under default_on. The payload must still be forwarded
|
||||
verbatim (no synthetic ``type: null``).
|
||||
"""
|
||||
tools = [
|
||||
{"googleSearch": {}},
|
||||
{"codeExecution": {}},
|
||||
{"type": "function", "function": {"name": "get_weather", "parameters": {}}},
|
||||
]
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = {"action": "NONE", "texts": ["hi"]}
|
||||
mock_response.raise_for_status = MagicMock()
|
||||
|
||||
with patch.object(
|
||||
generic_guardrail.async_handler, "post", return_value=mock_response
|
||||
) as mock_post:
|
||||
await generic_guardrail.apply_guardrail(
|
||||
inputs={"texts": ["hi"], "tools": tools},
|
||||
request_data={},
|
||||
input_type="request",
|
||||
)
|
||||
|
||||
forwarded_tools = mock_post.call_args.kwargs["json"]["tools"]
|
||||
|
||||
assert forwarded_tools == tools
|
||||
|
||||
|
||||
class TestFailOnError:
|
||||
"""Test fail_on_error: complete fail-open on any guardrail error"""
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue