mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-03 02:22:24 +00:00
fix(panw_prisma_airs): coerce timeout to float so string config values do not crash the handler (#28594)
* fix(panw_prisma_airs): coerce timeout to float so string config values do not crash the handler When the panw_prisma_airs guardrail receives timeout as a string (which is what the dashboard UI persists to the DB, and what raw YAML preserves if the value is quoted), every request fails with a misleading "500 Security scan failed - request blocked for safety". No request is actually sent to AIRS; the string survives into httpx.AsyncClient.post, which raises TypeError on its internal '<=' comparison. The broad except in apply_guardrail buckets it as api_error and the proxy wraps that as the safety-themed 500. Two changes: 1) PanwPrismaAirsHandler.__init__ now coerces timeout defensively. Guards every init path (registry-based initializer, legacy initializer, direct instantiation). 2) BaseLitellmParams declares timeout: Optional[float] with a mode="before" validator that coerces strings to float. Fixes the same class of bug at the API boundary for any guardrail provider that pulls timeout from litellm_params via model_dump(). Adds regression tests in TestPanwAirsTimeoutCoercion covering both the handler init and the Pydantic boundary. No public API changes. Correctly-typed callers (timeout: 30) behave identically. Fixes #28540 * fix(panw_prisma_airs): handle None timeout in legacy initializer after schema change Greptile flagged a regression on PR #28594: declaring timeout: Optional[float] = None on BaseLitellmParams means the attribute is now always present (defaulting to None), which breaks the legacy initializer at guardrail_initializers.py:220. Before this commit: timeout=float(getattr(litellm_params, "timeout", 10.0)) The getattr default 10.0 was only returned when the attribute was absent. Now the attribute exists as None, so getattr returns None, and float(None) raises TypeError. Every PANW deployment that does not explicitly set timeout would crash on init. This change unwraps the getattr-then-coerce into an explicit None check that preserves the 10.0 default semantics while remaining safe under the new schema. Adds two regression tests: - test_legacy_initializer_handles_unset_timeout: exercises the exact crash path through initialize_panw_prisma_airs with no timeout provided. - test_litellm_params_empty_string_timeout_becomes_none: covers the validator's empty-string branch (dashboard form can send "").
This commit is contained in:
parent
3526d14be9
commit
d4a89bad1b
4 changed files with 124 additions and 2 deletions
|
|
@ -140,7 +140,12 @@ class PanwPrismaAirsHandler(CustomGuardrail):
|
|||
)
|
||||
|
||||
self.fallback_on_error = fallback_on_error
|
||||
self.timeout = timeout
|
||||
# Coerce defensively. The dashboard UI persists this field as a JSON
|
||||
# string, and Pydantic extras (the path that splats model_dump into
|
||||
# this handler) preserve whatever type the user supplied. A string
|
||||
# value would otherwise reach httpx, which raises TypeError on its
|
||||
# internal '<=' comparison and surfaces as a misleading api_error.
|
||||
self.timeout = float(timeout) if timeout is not None else 10.0
|
||||
|
||||
# Tri-state: None = not set (default-on for Anthropic), True = explicit on, False = explicit off
|
||||
self.experimental_use_latest_role_message_only: Optional[bool] = kwargs.get(
|
||||
|
|
|
|||
|
|
@ -217,7 +217,15 @@ def initialize_panw_prisma_airs(litellm_params, guardrail):
|
|||
mask_response_content=getattr(litellm_params, "mask_response_content", False),
|
||||
app_name=getattr(litellm_params, "app_name", None),
|
||||
fallback_on_error=getattr(litellm_params, "fallback_on_error", "block"),
|
||||
timeout=float(getattr(litellm_params, "timeout", 10.0)),
|
||||
# `timeout` is now declared on BaseLitellmParams (Optional[float] = None),
|
||||
# so the attribute always exists. The Pydantic validator on LitellmParams
|
||||
# coerces strings to float, but None still means "use handler default" —
|
||||
# guard against float(None) here.
|
||||
timeout=(
|
||||
float(getattr(litellm_params, "timeout", None))
|
||||
if getattr(litellm_params, "timeout", None) is not None
|
||||
else 10.0
|
||||
),
|
||||
violation_message_template=litellm_params.violation_message_template,
|
||||
)
|
||||
litellm.logging_callback_manager.add_litellm_callback(_panw_callback)
|
||||
|
|
|
|||
|
|
@ -761,6 +761,15 @@ class BaseLitellmParams(
|
|||
description="Python-like code containing the apply_guardrail function for custom guardrail logic",
|
||||
)
|
||||
|
||||
timeout: Optional[float] = Field(
|
||||
default=None,
|
||||
description=(
|
||||
"Per-request timeout for the guardrail provider API call (seconds). "
|
||||
"Accepts int, float, or numeric string; coerced to float on load. "
|
||||
"Each guardrail handler chooses its own default when unset."
|
||||
),
|
||||
)
|
||||
|
||||
model_config = ConfigDict(extra="allow", protected_namespaces=())
|
||||
|
||||
|
||||
|
|
@ -818,6 +827,18 @@ class LitellmParams(
|
|||
return [x.lower() if isinstance(x, str) else x for x in v]
|
||||
return v
|
||||
|
||||
@field_validator("timeout", mode="before", check_fields=False)
|
||||
@classmethod
|
||||
def coerce_timeout(cls, v):
|
||||
"""Accept string-valued timeouts (dashboard UI sends JSON strings)
|
||||
and coerce to float before any handler reads the value."""
|
||||
if v is None or v == "":
|
||||
return None
|
||||
try:
|
||||
return float(v)
|
||||
except (TypeError, ValueError) as e:
|
||||
raise ValueError(f"timeout must be numeric, got {v!r}") from e
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
default_on = kwargs.pop("default_on", None)
|
||||
if default_on is not None:
|
||||
|
|
|
|||
|
|
@ -5375,5 +5375,93 @@ class TestPanwAirsDualScanIndependence:
|
|||
assert mcp_call.get("content") is None
|
||||
|
||||
|
||||
class TestPanwAirsTimeoutCoercion:
|
||||
"""Regression tests for string-valued timeout handling.
|
||||
|
||||
Before the fix, a string `timeout` (which is what the dashboard UI persists
|
||||
and what raw YAML preserves if quoted) survived into httpx, which raised
|
||||
`TypeError: '<=' not supported between instances of 'str' and 'int'`. The
|
||||
broad except in apply_guardrail swallowed it and the proxy returned a
|
||||
misleading 500 'Security scan failed - request blocked for safety'.
|
||||
"""
|
||||
|
||||
def test_handler_coerces_string_timeout_to_float(self):
|
||||
handler = make_handler(timeout="30")
|
||||
assert handler.timeout == 30.0
|
||||
assert isinstance(handler.timeout, float)
|
||||
|
||||
def test_handler_accepts_int_timeout(self):
|
||||
handler = make_handler(timeout=15)
|
||||
assert handler.timeout == 15.0
|
||||
|
||||
def test_handler_accepts_float_timeout(self):
|
||||
handler = make_handler(timeout=7.5)
|
||||
assert handler.timeout == 7.5
|
||||
|
||||
def test_handler_none_timeout_falls_back_to_default(self):
|
||||
handler = make_handler(timeout=None)
|
||||
assert handler.timeout == 10.0
|
||||
|
||||
def test_handler_omitted_timeout_uses_default(self):
|
||||
handler = make_handler()
|
||||
assert handler.timeout == 10.0
|
||||
|
||||
def test_litellm_params_coerces_string_timeout(self):
|
||||
"""Boundary validation: the Pydantic model itself should normalize
|
||||
string timeouts before any handler reads the value via model_dump()."""
|
||||
params = LitellmParams(
|
||||
guardrail="panw_prisma_airs",
|
||||
mode="pre_call",
|
||||
api_key="test_key",
|
||||
profile_name="test_profile",
|
||||
timeout="30",
|
||||
)
|
||||
assert params.timeout == 30.0
|
||||
assert isinstance(params.timeout, float)
|
||||
|
||||
def test_litellm_params_rejects_garbage_timeout(self):
|
||||
with pytest.raises(ValueError):
|
||||
LitellmParams(
|
||||
guardrail="panw_prisma_airs",
|
||||
mode="pre_call",
|
||||
api_key="test_key",
|
||||
profile_name="test_profile",
|
||||
timeout="not-a-number",
|
||||
)
|
||||
|
||||
def test_litellm_params_empty_string_timeout_becomes_none(self):
|
||||
"""Empty-string timeout (which the dashboard form can send) should
|
||||
be coerced to None, not crash, and not produce float('')."""
|
||||
params = LitellmParams(
|
||||
guardrail="panw_prisma_airs",
|
||||
mode="pre_call",
|
||||
api_key="test_key",
|
||||
profile_name="test_profile",
|
||||
timeout="",
|
||||
)
|
||||
assert params.timeout is None
|
||||
|
||||
def test_legacy_initializer_handles_unset_timeout(self):
|
||||
"""Regression guard: with timeout now a declared Optional[float] = None
|
||||
on BaseLitellmParams, the legacy panw initializer at
|
||||
guardrail_initializers.py:220 must not crash on float(None) when the
|
||||
caller omits timeout entirely."""
|
||||
from litellm.proxy.guardrails.guardrail_initializers import (
|
||||
initialize_panw_prisma_airs,
|
||||
)
|
||||
|
||||
params = LitellmParams(
|
||||
guardrail="panw_prisma_airs",
|
||||
mode="pre_call",
|
||||
api_key="test_key",
|
||||
profile_name="test_profile",
|
||||
# timeout intentionally omitted - field defaults to None
|
||||
)
|
||||
guardrail_config = {"guardrail_name": "test_legacy"}
|
||||
handler = initialize_panw_prisma_airs(params, guardrail_config)
|
||||
# Default fallback applied, not crashed on float(None)
|
||||
assert handler.timeout == 10.0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue