mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-01 02:02:20 +00:00
refactor(guardrails): keep agent 365 fail closed by default and make fail_open an explicit opt-in
Restores the shared unreachable_fallback default and the sibling guardrail initializers, drops the Admin UI YAML preview that only existed for the per-guardrail default, and reworks the unit and integration tests so the default blocks with HTTP 503 while unreachable_fallback: fail_open lets availability failures through as Unscanned Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
ff59e6d25b
commit
27f1013806
19 changed files with 203 additions and 340 deletions
|
|
@ -10405,20 +10405,14 @@
|
|||
"title": "Timeout"
|
||||
},
|
||||
"unreachable_fallback": {
|
||||
"anyOf": [
|
||||
{
|
||||
"enum": [
|
||||
"fail_closed",
|
||||
"fail_open"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
"default": "fail_closed",
|
||||
"description": "Behavior when a guardrail endpoint is unreachable due to network errors. Implemented by guardrail='generic_guardrail_api', 'agent_365', 'akto', 'vigil_guard', 'repelloai', 'headroom', 'compresr', and 'typesafe'. 'fail_closed' raises an error (default). 'fail_open' logs a critical error and allows the request to proceed.",
|
||||
"enum": [
|
||||
"fail_closed",
|
||||
"fail_open"
|
||||
],
|
||||
"description": "Behavior when a guardrail endpoint is unreachable due to network errors. Implemented by guardrail='generic_guardrail_api', 'agent_365', 'akto', 'vigil_guard', 'repelloai', 'headroom', 'compresr', and 'typesafe'. 'fail_closed' raises an error. 'fail_open' logs a critical error and allows the request to proceed. Unset applies the guardrail's own default: 'agent_365' and 'typesafe' fail open, the others fail closed.",
|
||||
"title": "Unreachable Fallback"
|
||||
"title": "Unreachable Fallback",
|
||||
"type": "string"
|
||||
},
|
||||
"violation_message_template": {
|
||||
"anyOf": [
|
||||
|
|
@ -13279,20 +13273,14 @@
|
|||
"title": "Tracker Api Key"
|
||||
},
|
||||
"unreachable_fallback": {
|
||||
"anyOf": [
|
||||
{
|
||||
"enum": [
|
||||
"fail_closed",
|
||||
"fail_open"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
"default": "fail_closed",
|
||||
"description": "Behavior when the headroom compression service is unreachable or errors. 'fail_closed' raises an error (default). 'fail_open' logs a critical error and forwards the request uncompressed instead of blocking it.",
|
||||
"enum": [
|
||||
"fail_closed",
|
||||
"fail_open"
|
||||
],
|
||||
"description": "Behavior when the guardrail endpoint is unreachable. 'fail_closed' blocks, 'fail_open' allows and logs. Unset applies the guardrail's own default: 'agent_365' and 'typesafe' fail open, the others fail closed.",
|
||||
"title": "Unreachable Fallback"
|
||||
"title": "Unreachable Fallback",
|
||||
"type": "string"
|
||||
},
|
||||
"use_v2": {
|
||||
"anyOf": [
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ def initialize_guardrail(litellm_params: "LitellmParams", guardrail: "Guardrail"
|
|||
agent_id=litellm_params.agent_id,
|
||||
authority_host=authority_host,
|
||||
request_timeout=litellm_params.timeout if litellm_params.timeout is not None else 10.0,
|
||||
unreachable_fallback=litellm_params.unreachable_fallback or "fail_open",
|
||||
unreachable_fallback=litellm_params.unreachable_fallback,
|
||||
event_hook=litellm_params.mode,
|
||||
default_on=litellm_params.default_on,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -159,7 +159,7 @@ class Agent365Guardrail(CustomGuardrail):
|
|||
agent_id: str | None = None,
|
||||
authority_host: str = AGENT_365_DEFAULT_AUTHORITY_HOST,
|
||||
request_timeout: float = 10.0,
|
||||
unreachable_fallback: Literal["fail_closed", "fail_open"] = "fail_open",
|
||||
unreachable_fallback: Literal["fail_closed", "fail_open"] = "fail_closed",
|
||||
async_handler: AsyncHTTPHandler | None = None,
|
||||
**kwargs, # noqa: ANN003 # kwargs-ok: forwarded verbatim to CustomGuardrail (event_hook, default_on)
|
||||
) -> None:
|
||||
|
|
@ -180,7 +180,7 @@ class Agent365Guardrail(CustomGuardrail):
|
|||
self.authority_host = authority if "://" in authority else f"https://{authority}"
|
||||
self.request_timeout = request_timeout
|
||||
self.unreachable_fallback: Literal["fail_closed", "fail_open"] = (
|
||||
"fail_closed" if unreachable_fallback == "fail_closed" else "fail_open"
|
||||
"fail_open" if unreachable_fallback == "fail_open" else "fail_closed"
|
||||
)
|
||||
self.async_handler = async_handler or get_async_httpx_client(
|
||||
llm_provider=httpxSpecialProvider.GuardrailCallback
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ def initialize_guardrail(litellm_params: "LitellmParams", guardrail: "Guardrail"
|
|||
akto_api_key=getattr(litellm_params, "akto_api_key", None),
|
||||
akto_account_id=getattr(litellm_params, "akto_account_id", None),
|
||||
akto_vxlan_id=getattr(litellm_params, "akto_vxlan_id", None),
|
||||
unreachable_fallback=litellm_params.unreachable_fallback or "fail_closed",
|
||||
unreachable_fallback=getattr(litellm_params, "unreachable_fallback", "fail_closed"),
|
||||
guardrail_timeout=getattr(litellm_params, "guardrail_timeout", None),
|
||||
guardrail_name=guardrail.get("guardrail_name", ""),
|
||||
event_hook=litellm_params.mode,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ def initialize_guardrail(litellm_params: "LitellmParams", guardrail: "Guardrail"
|
|||
_alice_guardrail_callback: Final = AliceGuardrail(
|
||||
api_key=litellm_params.api_key,
|
||||
api_base=litellm_params.api_base,
|
||||
unreachable_fallback=litellm_params.unreachable_fallback or "fail_closed",
|
||||
unreachable_fallback=getattr(litellm_params, "unreachable_fallback", "fail_closed"),
|
||||
guardrail_name=guardrail.get("guardrail_name", ""),
|
||||
event_hook=litellm_params.mode,
|
||||
default_on=litellm_params.default_on,
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ def initialize_guardrail(
|
|||
agent_token=litellm_params.api_key,
|
||||
workspace_id=extras.get("workspace_id"),
|
||||
tool_name=extras.get("tool_name", "llm_call"),
|
||||
unreachable_fallback=litellm_params.unreachable_fallback or "fail_closed",
|
||||
unreachable_fallback=litellm_params.unreachable_fallback,
|
||||
timeout=DEFAULT_TIMEOUT_SECONDS if litellm_params.timeout is None else litellm_params.timeout,
|
||||
guardrail_name=guardrail.get("guardrail_name", ""),
|
||||
event_hook=litellm_params.mode,
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ def initialize_guardrail(litellm_params: "LitellmParams", guardrail: "Guardrail"
|
|||
api_base=litellm_params.api_base,
|
||||
api_key=litellm_params.api_key,
|
||||
firewall_id=getattr(litellm_params, "deepkeep_firewall_id", None),
|
||||
unreachable_fallback=litellm_params.unreachable_fallback or "fail_closed",
|
||||
unreachable_fallback=getattr(litellm_params, "unreachable_fallback", "fail_closed"),
|
||||
extra_headers=getattr(litellm_params, "extra_headers", None),
|
||||
guardrail_name=guardrail.get("guardrail_name", ""),
|
||||
event_hook=litellm_params.mode,
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ def initialize_guardrail(litellm_params: "LitellmParams", guardrail: "Guardrail"
|
|||
api_key=litellm_params.api_key,
|
||||
headers=getattr(litellm_params, "headers", None),
|
||||
additional_provider_specific_params=getattr(litellm_params, "additional_provider_specific_params", {}),
|
||||
unreachable_fallback=litellm_params.unreachable_fallback or "fail_closed",
|
||||
unreachable_fallback=getattr(litellm_params, "unreachable_fallback", "fail_closed"),
|
||||
fail_on_error=getattr(litellm_params, "fail_on_error", True),
|
||||
extra_headers=getattr(litellm_params, "extra_headers", None),
|
||||
guardrail_name=guardrail.get("guardrail_name", ""),
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ def initialize_guardrail(litellm_params: "LitellmParams", guardrail: "Guardrail"
|
|||
api_key=litellm_params.api_key,
|
||||
api_base=litellm_params.api_base,
|
||||
asset_id=litellm_params.asset_id,
|
||||
unreachable_fallback=litellm_params.unreachable_fallback or "fail_closed",
|
||||
unreachable_fallback=litellm_params.unreachable_fallback,
|
||||
event_hook=_event_hook_from_mode(litellm_params.mode),
|
||||
default_on=litellm_params.default_on or False,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -55,7 +55,9 @@ def initialize_guardrail(litellm_params: LitellmParams, guardrail: Guardrail) ->
|
|||
guardrail_name=guardrail["guardrail_name"],
|
||||
event_hook=_coerce_event_hook(litellm_params.mode),
|
||||
default_on=litellm_params.default_on or False,
|
||||
unreachable_fallback=litellm_params.unreachable_fallback,
|
||||
unreachable_fallback=(
|
||||
litellm_params.unreachable_fallback if "unreachable_fallback" in litellm_params.model_fields_set else None
|
||||
),
|
||||
)
|
||||
litellm.logging_callback_manager.add_litellm_callback( # pyright: ignore[reportUnknownMemberType] # callback manager is untyped
|
||||
_callback
|
||||
|
|
|
|||
|
|
@ -1055,13 +1055,12 @@ class BaseLitellmParams(ContentFilterConfigModel): # works for new and patch up
|
|||
description="Additional provider-specific parameters for generic guardrail APIs",
|
||||
)
|
||||
|
||||
unreachable_fallback: Literal["fail_closed", "fail_open"] | None = Field(
|
||||
default=None,
|
||||
unreachable_fallback: Literal["fail_closed", "fail_open"] = Field(
|
||||
default="fail_closed",
|
||||
description=(
|
||||
"Behavior when a guardrail endpoint is unreachable due to network errors. "
|
||||
"Implemented by guardrail='generic_guardrail_api', 'agent_365', 'akto', 'vigil_guard', 'repelloai', 'headroom', 'compresr', and 'typesafe'. "
|
||||
"'fail_closed' raises an error. 'fail_open' logs a critical error and allows the request to proceed. "
|
||||
"Unset applies the guardrail's own default: 'agent_365' and 'typesafe' fail open, the others fail closed."
|
||||
"'fail_closed' raises an error (default). 'fail_open' logs a critical error and allows the request to proceed."
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -1205,13 +1204,6 @@ class LitellmParams( # pyright: ignore[reportIncompatibleVariableOverride] # o
|
|||
mode: str | list[str] | Mode = Field(
|
||||
description="When to apply the guardrail (pre_call, post_call, during_call, logging_only)"
|
||||
)
|
||||
unreachable_fallback: Literal["fail_closed", "fail_open"] | None = Field( # pyright: ignore[reportIncompatibleVariableOverride] # mixins pin a default; unset defers to the guardrail's own
|
||||
default=None,
|
||||
description=(
|
||||
"Behavior when the guardrail endpoint is unreachable. 'fail_closed' blocks, 'fail_open' allows and logs. "
|
||||
"Unset applies the guardrail's own default: 'agent_365' and 'typesafe' fail open, the others fail closed."
|
||||
),
|
||||
)
|
||||
|
||||
@field_validator("timeout", mode="before", check_fields=False)
|
||||
@classmethod
|
||||
|
|
|
|||
|
|
@ -72,13 +72,12 @@ class Agent365GuardrailConfigModel(GuardrailConfigModel):
|
|||
)
|
||||
|
||||
unreachable_fallback: Literal["fail_closed", "fail_open"] = Field(
|
||||
default="fail_open",
|
||||
default="fail_closed",
|
||||
description=(
|
||||
"Behavior when Agent 365 or Entra is unreachable, times out, returns 5xx, skips the evaluation, or "
|
||||
"rejects the gateway's own client credentials. 'fail_open' (default) allows the tool call and records "
|
||||
"it as Unscanned in the logs and OpenTelemetry. "
|
||||
"'fail_closed' blocks it with HTTP 503. Policy blocks, 4xx rejections, throttling and a rejected "
|
||||
"caller token always block."
|
||||
"rejects the gateway's own client credentials. 'fail_closed' (default) blocks the tool call with HTTP 503. "
|
||||
"'fail_open' allows it, logs an error and records it as Unscanned in the logs and OpenTelemetry. "
|
||||
"Policy blocks, 4xx rejections, throttling and a rejected caller token always block."
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -265,14 +265,50 @@ def _chat(rig: Rig, model: str, marker: str) -> httpx.Response:
|
|||
)
|
||||
|
||||
|
||||
def test_default_lets_the_call_through_unscanned_when_agent_365_cannot_evaluate(
|
||||
def test_default_blocks_with_503_and_never_reaches_upstream_when_agent_365_cannot_evaluate(
|
||||
gateway: Gateway, tmp_path: Path
|
||||
) -> None:
|
||||
with _rig(gateway, tmp_path, fallback=None) as rig:
|
||||
unavailable: Final = "could not authorize the tool call"
|
||||
outage: Final = rig.caller.call(f"{rig.alias}-outage", {"a": 1})
|
||||
assert outage.text == '{"a": 1}', f"Agent 365 down must fail open by default: {outage.raw}"
|
||||
assert outage.error is not None and unavailable in outage.raw, (
|
||||
f"Agent 365 down must block by default: {outage.raw}"
|
||||
)
|
||||
skipped: Final = rig.caller.call(f"{rig.alias}-skipped", {"a": 2})
|
||||
assert skipped.text == '{"a": 2}', f"Defender skipping the call must fail open by default: {skipped.raw}"
|
||||
assert skipped.error is not None and unavailable in skipped.raw, (
|
||||
f"Defender skip must block by default: {skipped.raw}"
|
||||
)
|
||||
denied: Final = rig.caller.call(f"{rig.alias}-denied", {"a": 3})
|
||||
assert denied.error is not None and "Blocked by Microsoft Defender" in denied.raw, denied.raw
|
||||
assert rig.caller.call(f"{rig.alias}-add", {"a": 4}).text == '{"a": 4}'
|
||||
assert rig.upstream_tool_names() == ("add",)
|
||||
statuses: Final = eventually(lambda: _guardrail_statuses(rig.key), lambda seen: len(seen) >= 4, seconds=70)
|
||||
assert statuses == {
|
||||
"outage": "guardrail_failed_to_respond",
|
||||
"skipped": "guardrail_failed_to_respond",
|
||||
"denied": "guardrail_intervened",
|
||||
"add": "success",
|
||||
}, statuses
|
||||
|
||||
|
||||
def test_explicit_fail_closed_matches_the_default(gateway: Gateway, tmp_path: Path) -> None:
|
||||
with _rig(gateway, tmp_path, fallback="fail_closed") as rig:
|
||||
outage: Final = rig.caller.call(f"{rig.alias}-outage", {"a": 1})
|
||||
assert outage.error is not None and "could not authorize the tool call" in outage.raw, outage.raw
|
||||
assert rig.caller.call(f"{rig.alias}-add", {"a": 2}).text == '{"a": 2}'
|
||||
assert rig.upstream_tool_names() == ("add",)
|
||||
statuses: Final = eventually(lambda: _guardrail_statuses(rig.key), lambda seen: len(seen) >= 2, seconds=70)
|
||||
assert statuses == {"outage": "guardrail_failed_to_respond", "add": "success"}, statuses
|
||||
|
||||
|
||||
def test_opted_in_fail_open_lets_the_call_through_unscanned_and_still_blocks_policy_denials(
|
||||
gateway: Gateway, tmp_path: Path
|
||||
) -> None:
|
||||
with _rig(gateway, tmp_path, fallback="fail_open") as rig:
|
||||
outage: Final = rig.caller.call(f"{rig.alias}-outage", {"a": 1})
|
||||
assert outage.text == '{"a": 1}', f"Agent 365 down must fail open once opted in: {outage.raw}"
|
||||
skipped: Final = rig.caller.call(f"{rig.alias}-skipped", {"a": 2})
|
||||
assert skipped.text == '{"a": 2}', f"Defender skipping the call must fail open once opted in: {skipped.raw}"
|
||||
denied: Final = rig.caller.call(f"{rig.alias}-denied", {"a": 3})
|
||||
assert denied.error is not None and "Blocked by Microsoft Defender" in denied.raw, denied.raw
|
||||
assert rig.upstream_tool_names() == ("outage", "skipped")
|
||||
|
|
@ -284,32 +320,8 @@ def test_default_lets_the_call_through_unscanned_when_agent_365_cannot_evaluate(
|
|||
}, statuses
|
||||
|
||||
|
||||
def test_explicit_fail_closed_blocks_with_503_and_never_reaches_upstream_when_agent_365_is_down(
|
||||
gateway: Gateway, tmp_path: Path
|
||||
) -> None:
|
||||
with _rig(gateway, tmp_path, fallback="fail_closed") as rig:
|
||||
outage: Final = rig.caller.call(f"{rig.alias}-outage", {"a": 1})
|
||||
assert outage.error is not None and "could not authorize the tool call" in outage.raw, outage.raw
|
||||
assert rig.caller.call(f"{rig.alias}-add", {"a": 2}).text == '{"a": 2}'
|
||||
assert rig.upstream_tool_names() == ("add",)
|
||||
statuses: Final = eventually(lambda: _guardrail_statuses(rig.key), lambda seen: len(seen) >= 2, seconds=70)
|
||||
assert statuses == {"outage": "guardrail_failed_to_respond", "add": "success"}, statuses
|
||||
|
||||
|
||||
def test_explicit_fail_open_matches_the_default_and_still_blocks_policy_denials(
|
||||
gateway: Gateway, tmp_path: Path
|
||||
) -> None:
|
||||
def test_opted_in_fail_open_covers_malformed_or_stalled_agent_365_replies(gateway: Gateway, tmp_path: Path) -> None:
|
||||
with _rig(gateway, tmp_path, fallback="fail_open") as rig:
|
||||
assert rig.caller.call(f"{rig.alias}-outage", {"a": 1}).text == '{"a": 1}'
|
||||
denied: Final = rig.caller.call(f"{rig.alias}-denied", {"a": 2})
|
||||
assert denied.error is not None and "Blocked by Microsoft Defender" in denied.raw, denied.raw
|
||||
assert rig.upstream_tool_names() == ("outage",)
|
||||
statuses: Final = eventually(lambda: _guardrail_statuses(rig.key), lambda seen: len(seen) >= 2, seconds=70)
|
||||
assert statuses == {"outage": "guardrail_failed_to_respond", "denied": "guardrail_intervened"}, statuses
|
||||
|
||||
|
||||
def test_default_fails_open_on_malformed_or_stalled_agent_365_replies(gateway: Gateway, tmp_path: Path) -> None:
|
||||
with _rig(gateway, tmp_path, fallback=None) as rig:
|
||||
for index, tool in enumerate(("nonjson", "nobool", "slow")):
|
||||
outcome: Final = rig.caller.call(f"{rig.alias}-{tool}", {"a": index})
|
||||
assert outcome.text == json.dumps({"a": index}), f"{tool}: {outcome.raw}"
|
||||
|
|
@ -318,10 +330,10 @@ def test_default_fails_open_on_malformed_or_stalled_agent_365_replies(gateway: G
|
|||
assert statuses == dict.fromkeys(("nonjson", "nobool", "slow"), "guardrail_failed_to_respond"), statuses
|
||||
|
||||
|
||||
def test_default_fails_open_when_entra_is_down_stalled_malformed_or_refuses_the_gateway_credentials(
|
||||
def test_opted_in_fail_open_covers_entra_down_stalled_malformed_or_refusing_the_gateway_credentials(
|
||||
gateway: Gateway, tmp_path: Path
|
||||
) -> None:
|
||||
with _rig(gateway, tmp_path, fallback=None) as rig:
|
||||
with _rig(gateway, tmp_path, fallback="fail_open") as rig:
|
||||
cases: Final = ("entra-outage", "entra-slow", "entra-nonjson", "entra-misconfigured")
|
||||
for index, case in enumerate(cases):
|
||||
outcome: Final = rig.caller_for("mcp", _caller_token(case)).call(f"{rig.alias}-add", {"a": index})
|
||||
|
|
@ -339,10 +351,10 @@ def test_default_fails_open_when_entra_is_down_stalled_malformed_or_refuses_the_
|
|||
assert [row["gi"][0]["guardrail_status"] for row in rows] == ["guardrail_failed_to_respond"] * len(cases)
|
||||
|
||||
|
||||
def test_throttling_and_ordinary_4xx_from_agent_365_keep_blocking_under_the_default(
|
||||
def test_throttling_and_ordinary_4xx_from_agent_365_keep_blocking_even_when_opted_in_to_fail_open(
|
||||
gateway: Gateway, tmp_path: Path
|
||||
) -> None:
|
||||
with _rig(gateway, tmp_path, fallback=None) as rig:
|
||||
with _rig(gateway, tmp_path, fallback="fail_open") as rig:
|
||||
throttled: Final = rig.caller.call(f"{rig.alias}-throttled", {"a": 1})
|
||||
assert throttled.error == "Error: Agent 365 guardrail could not authorize the tool call", throttled.raw
|
||||
rejected: Final = rig.caller.call(f"{rig.alias}-rejected", {"a": 2})
|
||||
|
|
@ -352,8 +364,10 @@ def test_throttling_and_ordinary_4xx_from_agent_365_keep_blocking_under_the_defa
|
|||
assert statuses == {"throttled": "guardrail_failed_to_respond", "rejected": "guardrail_intervened"}, statuses
|
||||
|
||||
|
||||
def test_caller_authentication_failures_keep_blocking_under_the_default(gateway: Gateway, tmp_path: Path) -> None:
|
||||
with _rig(gateway, tmp_path, fallback=None) as rig:
|
||||
def test_caller_authentication_failures_keep_blocking_even_when_opted_in_to_fail_open(
|
||||
gateway: Gateway, tmp_path: Path
|
||||
) -> None:
|
||||
with _rig(gateway, tmp_path, fallback="fail_open") as rig:
|
||||
rejected: Final = "Error: Agent 365 guardrail rejected the tool call"
|
||||
missing: Final = rig.call_without_bearer("add")
|
||||
assert missing.error == rejected, missing.raw
|
||||
|
|
@ -372,8 +386,8 @@ def test_caller_authentication_failures_keep_blocking_under_the_default(gateway:
|
|||
assert [row["gi"][0]["guardrail_status"] for row in rows] == ["guardrail_intervened"] * 3
|
||||
|
||||
|
||||
def test_every_mcp_entry_point_fails_open_on_outage_and_blocks_denials(gateway: Gateway, tmp_path: Path) -> None:
|
||||
with _rig(gateway, tmp_path, fallback=None) as rig:
|
||||
def test_every_mcp_entry_point_honors_the_fail_open_opt_in_and_blocks_denials(gateway: Gateway, tmp_path: Path) -> None:
|
||||
with _rig(gateway, tmp_path, fallback="fail_open") as rig:
|
||||
for entry in ENTRY_POINTS:
|
||||
caller: Final = rig.caller_for(entry)
|
||||
passed: Final = caller.call(f"{rig.alias}-outage", {"entry": entry}, server_id=rig.server_id)
|
||||
|
|
@ -383,10 +397,10 @@ def test_every_mcp_entry_point_fails_open_on_outage_and_blocks_denials(gateway:
|
|||
assert rig.upstream_tool_names() == ("outage",) * len(ENTRY_POINTS)
|
||||
|
||||
|
||||
def test_chat_completions_never_touch_agent_365_while_a_sibling_guardrail_keeps_its_fail_closed_default(
|
||||
def test_agent_365_fail_open_opt_in_does_not_leak_to_a_sibling_guardrail_on_chat_completions(
|
||||
gateway: Gateway, tmp_path: Path
|
||||
) -> None:
|
||||
with _rig(gateway, tmp_path, fallback=None, sibling=True) as rig, rig.candidate.scenario() as scenario:
|
||||
with _rig(gateway, tmp_path, fallback="fail_open", sibling=True) as rig, rig.candidate.scenario() as scenario:
|
||||
model: Final = scenario.model()
|
||||
chat: Final = _chat(rig, model, "sibling-" + uuid.uuid4().hex)
|
||||
assert chat.status_code == 500 and "Generic Guardrail API failed" in chat.text, chat.text
|
||||
|
|
@ -433,7 +447,7 @@ def test_agent365_authority_host_env_wins_over_azure_authority_host_when_config_
|
|||
def test_thirty_call_burst_against_a_flapping_agent_365_reaches_upstream_exactly_once_each_on_two_workers(
|
||||
gateway: Gateway, tmp_path: Path
|
||||
) -> None:
|
||||
with _rig(gateway, tmp_path, fallback=None, workers=2) as rig:
|
||||
with _rig(gateway, tmp_path, fallback="fail_open", workers=2) as rig:
|
||||
markers: Final = tuple(("outage" if index % 2 else "add", uuid.uuid4().hex) for index in range(30))
|
||||
with ThreadPoolExecutor(max_workers=10) as pool:
|
||||
outcomes: Final = tuple(
|
||||
|
|
@ -449,10 +463,10 @@ def test_thirty_call_burst_against_a_flapping_agent_365_reaches_upstream_exactly
|
|||
assert rig.upstream_tool_names() == ()
|
||||
|
||||
|
||||
def test_default_survives_a_worker_kill_and_keeps_blocking_denials_on_two_workers(
|
||||
def test_opted_in_fail_open_survives_a_worker_kill_and_keeps_blocking_denials_on_two_workers(
|
||||
gateway: Gateway, tmp_path: Path
|
||||
) -> None:
|
||||
with _rig(gateway, tmp_path, fallback=None, workers=2) as rig:
|
||||
with _rig(gateway, tmp_path, fallback="fail_open", workers=2) as rig:
|
||||
before: Final = rig.every_worker_serves_the_catalog(2)
|
||||
victim: Final = min(before)
|
||||
os.kill(victim, signal.SIGKILL)
|
||||
|
|
|
|||
|
|
@ -234,31 +234,19 @@ class TestInitializeGuardrail:
|
|||
assert guardrail.client_secret == "env-secret"
|
||||
assert guardrail.api_base == "https://env.example.test"
|
||||
assert guardrail.resource_app_id == AGENT_365_PROD_RESOURCE_APP_ID
|
||||
assert guardrail.unreachable_fallback == "fail_open"
|
||||
assert guardrail.unreachable_fallback == "fail_closed"
|
||||
|
||||
def test_unset_fallback_survives_a_model_dump_round_trip(self):
|
||||
stored: Final = LitellmParams(
|
||||
guardrail="agent_365", mode="pre_mcp_call", tenant_id="t", client_id="c", client_secret="s"
|
||||
).model_dump()
|
||||
assert stored["unreachable_fallback"] is None
|
||||
guardrail: Final = initialize_guardrail(LitellmParams(**stored), {"guardrail_name": "a365-db"})
|
||||
assert guardrail.unreachable_fallback == "fail_open"
|
||||
|
||||
def test_explicit_fail_closed_is_kept(self):
|
||||
def test_fail_open_is_opt_in_through_litellm_params(self):
|
||||
params: Final = LitellmParams(
|
||||
guardrail="agent_365",
|
||||
mode="pre_mcp_call",
|
||||
tenant_id="t",
|
||||
client_id="c",
|
||||
client_secret="s",
|
||||
unreachable_fallback="fail_closed",
|
||||
unreachable_fallback="fail_open",
|
||||
)
|
||||
guardrail: Final = initialize_guardrail(params, {"guardrail_name": "a365-closed"})
|
||||
assert guardrail.unreachable_fallback == "fail_closed"
|
||||
|
||||
def test_agent_365_default_leaves_other_guardrails_unset(self):
|
||||
assert LitellmParams(guardrail="generic_guardrail_api", mode="pre_call").unreachable_fallback is None
|
||||
assert Agent365GuardrailConfigModel.model_fields["unreachable_fallback"].default == "fail_open"
|
||||
guardrail: Final = initialize_guardrail(params, {"guardrail_name": "a365-open"})
|
||||
assert guardrail.unreachable_fallback == "fail_open"
|
||||
|
||||
def test_authority_host_defaults_to_public_entra(self, monkeypatch):
|
||||
monkeypatch.delenv("AGENT365_AUTHORITY_HOST", raising=False)
|
||||
|
|
@ -569,45 +557,51 @@ class TestDefenderNotEvaluated:
|
|||
assert "rejected" in exc_info.value.detail["error"]
|
||||
|
||||
|
||||
class TestFailOpenDefault:
|
||||
AVAILABILITY_FAILURES: Final = (
|
||||
pytest.param([_token_response(), httpx.ReadTimeout("timed out")], id="evaluate-timeout"),
|
||||
pytest.param([_token_response(), _response(502, text="bad gateway")], id="evaluate-5xx"),
|
||||
pytest.param([_token_response(), _not_evaluated_response("Skipped")], id="evaluate-skipped"),
|
||||
pytest.param([_response(503, text="entra down")], id="entra-5xx"),
|
||||
)
|
||||
|
||||
|
||||
class TestOptInFailOpen:
|
||||
@pytest.mark.asyncio
|
||||
async def test_constructor_default_lets_timed_out_evaluation_through_unscanned(self, caplog):
|
||||
handler: Final = FakeHandler([_token_response(), httpx.ReadTimeout("timed out")])
|
||||
guardrail: Final = _default_fallback_guardrail(handler)
|
||||
assert guardrail.unreachable_fallback == "fail_open"
|
||||
@pytest.mark.parametrize("responses", AVAILABILITY_FAILURES)
|
||||
async def test_constructor_default_blocks_each_availability_failure_with_503(self, responses):
|
||||
guardrail: Final = _default_fallback_guardrail(FakeHandler(responses))
|
||||
assert guardrail.unreachable_fallback == "fail_closed"
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await _run(guardrail, _mcp_data())
|
||||
assert exc_info.value.status_code == 503
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("responses", AVAILABILITY_FAILURES)
|
||||
async def test_opted_in_fail_open_lets_each_availability_failure_through_as_failed_to_respond(self, responses):
|
||||
guardrail: Final = _make_guardrail(FakeHandler(responses), unreachable_fallback="fail_open")
|
||||
data: Final = _mcp_data()
|
||||
with caplog.at_level(logging.ERROR, logger="LiteLLM Proxy"):
|
||||
assert await _run(guardrail, data) is data
|
||||
assert await _run(guardrail, data) is data
|
||||
info: Final = _guardrail_info(data)
|
||||
assert info["guardrail_status"] == "guardrail_failed_to_respond"
|
||||
assert info["guardrail_response"]["verdict"] == "Unscanned"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_opted_in_fail_open_logs_the_unscanned_call_at_error_level(self, caplog):
|
||||
handler: Final = FakeHandler([_token_response(), httpx.ReadTimeout("timed out")])
|
||||
guardrail: Final = _make_guardrail(handler, unreachable_fallback="fail_open")
|
||||
with caplog.at_level(logging.ERROR, logger="LiteLLM Proxy"):
|
||||
await _run(guardrail, _mcp_data())
|
||||
fail_open_logs: Final = [r for r in caplog.records if "unreachable_fallback='fail_open'" in r.getMessage()]
|
||||
assert [r.levelno for r in fail_open_logs] == [logging.ERROR], caplog.text
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_constructor_default_still_blocks_a_policy_block(self):
|
||||
async def test_opted_in_fail_open_still_blocks_a_policy_block(self):
|
||||
handler: Final = FakeHandler([_token_response(), _block_response()])
|
||||
guardrail: Final = _default_fallback_guardrail(handler)
|
||||
guardrail: Final = _make_guardrail(handler, unreachable_fallback="fail_open")
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await _run(guardrail, _mcp_data())
|
||||
assert exc_info.value.status_code == 400
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"responses",
|
||||
[
|
||||
[_token_response(), httpx.ReadTimeout("timed out")],
|
||||
[_token_response(), _response(502, text="bad gateway")],
|
||||
[_token_response(), _not_evaluated_response("Skipped")],
|
||||
[_response(503, text="entra down")],
|
||||
],
|
||||
)
|
||||
async def test_each_availability_failure_lets_the_call_through_as_failed_to_respond(self, responses):
|
||||
guardrail: Final = _default_fallback_guardrail(FakeHandler(responses))
|
||||
data: Final = _mcp_data()
|
||||
assert await _run(guardrail, data) is data
|
||||
assert _guardrail_info(data)["guardrail_status"] == "guardrail_failed_to_respond"
|
||||
|
||||
|
||||
class TestUnreachableFallback:
|
||||
@pytest.mark.asyncio
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import json
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
from litellm.proxy.guardrails.guardrail_registry import InMemoryGuardrailHandler
|
||||
from litellm.proxy.guardrails.init_guardrails import init_guardrails_v2
|
||||
from litellm.types.guardrails import SupportedGuardrailIntegrations
|
||||
|
|
@ -165,29 +167,6 @@ def test_initialize_guardrail_sets_run_in_parallel(config_value, expected):
|
|||
assert custom_guardrail.run_in_parallel is expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"guardrail, provider_params, expected",
|
||||
[
|
||||
("agent_365", {"tenant_id": "t", "client_id": "c", "client_secret": "s", "mode": "pre_mcp_call"}, "fail_open"),
|
||||
("typesafe", {"api_key": "k"}, "fail_open"),
|
||||
("generic_guardrail_api", {"api_base": "http://127.0.0.1:1/guard"}, "fail_closed"),
|
||||
("akto", {"akto_base_url": "http://127.0.0.1:1", "akto_api_key": "k", "akto_account_id": "1"}, "fail_closed"),
|
||||
("alice", {"api_key": "k", "api_base": "http://127.0.0.1:1"}, "fail_closed"),
|
||||
("deepkeep", {"api_base": "http://127.0.0.1:1", "api_key": "k", "deepkeep_firewall_id": "f"}, "fail_closed"),
|
||||
("repelloai", {"api_key": "k", "api_base": "http://127.0.0.1:1", "asset_id": "a"}, "fail_closed"),
|
||||
],
|
||||
)
|
||||
def test_unset_unreachable_fallback_applies_each_guardrails_own_default(guardrail, provider_params, expected):
|
||||
litellm_params = {"guardrail": guardrail, "mode": "pre_call", **provider_params}
|
||||
guardrail_handler = InMemoryGuardrailHandler()
|
||||
result = guardrail_handler.initialize_guardrail(
|
||||
guardrail={"guardrail_name": f"default-fallback-{guardrail}", "litellm_params": litellm_params},
|
||||
)
|
||||
|
||||
custom_guardrail = guardrail_handler.guardrail_id_to_custom_guardrail[result["guardrail_id"]]
|
||||
assert custom_guardrail.unreachable_fallback == expected, f"{guardrail} with unreachable_fallback unset"
|
||||
|
||||
|
||||
def test_initialize_presidio_forwards_analyze_chunk_size_bytes():
|
||||
"""Regression (LIT-4785): `presidio_analyze_chunk_size_bytes` set in
|
||||
config.yaml must reach the guardrail instance. The field lives on
|
||||
|
|
@ -216,7 +195,8 @@ def test_initialize_presidio_forwards_analyze_chunk_size_bytes():
|
|||
initialized = [
|
||||
callback
|
||||
for callback in litellm.callbacks
|
||||
if isinstance(callback, _OPTIONAL_PresidioPIIMasking) and callback.guardrail_name == "test_presidio_chunk_size"
|
||||
if isinstance(callback, _OPTIONAL_PresidioPIIMasking)
|
||||
and callback.guardrail_name == "test_presidio_chunk_size"
|
||||
]
|
||||
assert initialized, "presidio guardrail was not registered as a callback"
|
||||
assert initialized[-1].presidio_analyze_chunk_size_bytes == 250_000
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@ import { Textarea } from "@/components/ui/textarea";
|
|||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { isValidUrl } from "@/lib/forms/urlValidation";
|
||||
import { useZodForm } from "@/lib/forms/useZodForm";
|
||||
import { buildEquivalentConfigYaml, type TeamGuardrail, type TeamGuardrailStatus } from "./teamGuardrailConfigYaml";
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
|
|
@ -98,7 +97,32 @@ const labelWithHint = (label: string, hint: string): React.ReactNode => (
|
|||
</>
|
||||
);
|
||||
|
||||
function mapStatus(apiStatus: string): TeamGuardrailStatus {
|
||||
type GuardrailStatus = "active" | "pending" | "rejected";
|
||||
|
||||
type TeamGuardrail = {
|
||||
id: string;
|
||||
team: string;
|
||||
name: string;
|
||||
endpoint: string;
|
||||
status: GuardrailStatus;
|
||||
model: string;
|
||||
forwardKey: boolean;
|
||||
description: string;
|
||||
method: "POST" | "GET";
|
||||
customHeaders: {
|
||||
key: string;
|
||||
value: string;
|
||||
}[];
|
||||
extraHeaders: string[];
|
||||
submittedAt: string;
|
||||
submittedBy: string;
|
||||
mode?: string;
|
||||
unreachable_fallback?: string;
|
||||
additionalProviderParams?: Record<string, unknown>;
|
||||
guardrailType?: string;
|
||||
};
|
||||
|
||||
function mapStatus(apiStatus: string): GuardrailStatus {
|
||||
if (apiStatus === "pending_review") return "pending";
|
||||
if (apiStatus === "active" || apiStatus === "rejected") return apiStatus;
|
||||
return "active";
|
||||
|
|
@ -150,13 +174,13 @@ function submissionToTeamGuardrail(item: GuardrailSubmissionItem): TeamGuardrail
|
|||
submittedAt: formatSubmissionDate(item.submitted_at),
|
||||
submittedBy: item.submitted_by_email ?? item.submitted_by_user_id ?? "—",
|
||||
mode: params.mode as string | undefined,
|
||||
unreachable_fallback: params.unreachable_fallback as string | null | undefined,
|
||||
unreachable_fallback: params.unreachable_fallback as string | undefined,
|
||||
additionalProviderParams: params.additional_provider_specific_params as Record<string, unknown> | undefined,
|
||||
guardrailType: params.guardrail as string | undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const STATUS_CONFIG: Record<TeamGuardrailStatus, { label: string; bg: string; text: string; dot: string }> = {
|
||||
const STATUS_CONFIG: Record<GuardrailStatus, { label: string; bg: string; text: string; dot: string }> = {
|
||||
active: {
|
||||
label: "Active",
|
||||
bg: "bg-success/10",
|
||||
|
|
@ -186,6 +210,44 @@ const TEAM_COLORS: Record<string, string> = {
|
|||
Finance: "bg-success/15 text-success",
|
||||
};
|
||||
|
||||
function buildEquivalentConfigYaml(g: TeamGuardrail): string {
|
||||
const lines: string[] = [
|
||||
"litellm_settings:",
|
||||
" guardrails:",
|
||||
` - guardrail_name: "${g.name.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`,
|
||||
" litellm_params:",
|
||||
` guardrail: ${g.guardrailType ?? "generic_guardrail_api"}`,
|
||||
` mode: ${g.mode ?? "pre_call"} # or post_call, during_call`,
|
||||
` api_base: ${g.endpoint || "https://your-guardrail-api.com"}`,
|
||||
" api_key: os.environ/YOUR_GUARDRAIL_API_KEY # optional",
|
||||
` unreachable_fallback: ${g.unreachable_fallback ?? "fail_closed"} # default: fail_closed. Set to fail_open to proceed if the guardrail endpoint is unreachable.`,
|
||||
` forward_api_key: ${g.forwardKey}`,
|
||||
];
|
||||
if (g.model && g.model !== "—") {
|
||||
lines.push(` model: "${g.model}" # LLM model name sent to the guardrail for context`);
|
||||
}
|
||||
if (g.customHeaders.length > 0) {
|
||||
lines.push(" headers: # static headers (sent with every request)");
|
||||
for (const h of g.customHeaders) {
|
||||
lines.push(` ${h.key}: "${String(h.value).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`);
|
||||
}
|
||||
}
|
||||
if (g.extraHeaders.length > 0) {
|
||||
lines.push(" extra_headers: # forward these client request headers to the guardrail");
|
||||
for (const name of g.extraHeaders) {
|
||||
lines.push(` - ${name}`);
|
||||
}
|
||||
}
|
||||
if (g.additionalProviderParams && Object.keys(g.additionalProviderParams).length > 0) {
|
||||
lines.push(" additional_provider_specific_params:");
|
||||
for (const [k, v] of Object.entries(g.additionalProviderParams)) {
|
||||
const val = typeof v === "string" ? `"${v}"` : String(v);
|
||||
lines.push(` ${k}: ${val}`);
|
||||
}
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function StatCard({ label, value, color }: { label: string; value: number; color: string }) {
|
||||
return (
|
||||
<div className="bg-card border border-border rounded-lg px-4 py-3">
|
||||
|
|
@ -763,7 +825,7 @@ export function TeamGuardrailsTab({ accessToken }: TeamGuardrailsTabProps) {
|
|||
});
|
||||
const [search, setSearch] = useState("");
|
||||
const [searchDebounced] = useDebouncedValue(search, { wait: DEBOUNCE_WAIT_MS });
|
||||
const [statusFilter, setStatusFilter] = useState<"all" | TeamGuardrailStatus>("all");
|
||||
const [statusFilter, setStatusFilter] = useState<"all" | GuardrailStatus>("all");
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
const [expandedHeaders, setExpandedHeaders] = useState<Set<string>>(new Set());
|
||||
const [confirmAction, setConfirmAction] = useState<{
|
||||
|
|
|
|||
|
|
@ -1,94 +0,0 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { buildEquivalentConfigYaml, defaultUnreachableFallback, type TeamGuardrail } from "./teamGuardrailConfigYaml";
|
||||
|
||||
function guardrail(overrides: Partial<TeamGuardrail>): TeamGuardrail {
|
||||
return {
|
||||
id: "g-1",
|
||||
team: "Security",
|
||||
name: "agent365-mcp",
|
||||
endpoint: "",
|
||||
status: "active",
|
||||
model: "—",
|
||||
forwardKey: false,
|
||||
description: "",
|
||||
method: "POST",
|
||||
customHeaders: [],
|
||||
extraHeaders: [],
|
||||
submittedAt: "2026-01-01",
|
||||
submittedBy: "ops@example.com",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function fallbackLine(yaml: string): string {
|
||||
const line = yaml.split("\n").find((l) => l.includes("unreachable_fallback:"));
|
||||
if (line === undefined) throw new Error(`no unreachable_fallback line in:\n${yaml}`);
|
||||
return line.trim().split(" #")[0];
|
||||
}
|
||||
|
||||
describe("defaultUnreachableFallback", () => {
|
||||
it("fails open for agent_365 and typesafe, closed for everything else including unknown", () => {
|
||||
expect(defaultUnreachableFallback("agent_365")).toBe("fail_open");
|
||||
expect(defaultUnreachableFallback("typesafe")).toBe("fail_open");
|
||||
expect(defaultUnreachableFallback("generic_guardrail_api")).toBe("fail_closed");
|
||||
expect(defaultUnreachableFallback("akto")).toBe("fail_closed");
|
||||
expect(defaultUnreachableFallback(undefined)).toBe("fail_closed");
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildEquivalentConfigYaml unreachable_fallback line", () => {
|
||||
it("shows fail_open for an agent_365 guardrail with no explicit fallback", () => {
|
||||
const yaml = buildEquivalentConfigYaml(guardrail({ guardrailType: "agent_365" }));
|
||||
expect(fallbackLine(yaml)).toBe("unreachable_fallback: fail_open");
|
||||
expect(yaml).toContain(" guardrail: agent_365");
|
||||
});
|
||||
|
||||
it("shows fail_open for a typesafe guardrail with no explicit fallback", () => {
|
||||
expect(fallbackLine(buildEquivalentConfigYaml(guardrail({ guardrailType: "typesafe" })))).toBe(
|
||||
"unreachable_fallback: fail_open",
|
||||
);
|
||||
});
|
||||
|
||||
it("shows fail_closed for a generic guardrail and when the type is unknown", () => {
|
||||
expect(fallbackLine(buildEquivalentConfigYaml(guardrail({ guardrailType: "generic_guardrail_api" })))).toBe(
|
||||
"unreachable_fallback: fail_closed",
|
||||
);
|
||||
const untyped = buildEquivalentConfigYaml(guardrail({}));
|
||||
expect(fallbackLine(untyped)).toBe("unreachable_fallback: fail_closed");
|
||||
expect(untyped).toContain(" guardrail: generic_guardrail_api");
|
||||
});
|
||||
|
||||
it("treats a null fallback from the API as unset and shows the per-guardrail default", () => {
|
||||
expect(
|
||||
fallbackLine(buildEquivalentConfigYaml(guardrail({ guardrailType: "agent_365", unreachable_fallback: null }))),
|
||||
).toBe("unreachable_fallback: fail_open");
|
||||
expect(
|
||||
fallbackLine(buildEquivalentConfigYaml(guardrail({ guardrailType: "akto", unreachable_fallback: null }))),
|
||||
).toBe("unreachable_fallback: fail_closed");
|
||||
});
|
||||
|
||||
it("keeps an explicit override over the per-guardrail default", () => {
|
||||
expect(
|
||||
fallbackLine(
|
||||
buildEquivalentConfigYaml(guardrail({ guardrailType: "agent_365", unreachable_fallback: "fail_closed" })),
|
||||
),
|
||||
).toBe("unreachable_fallback: fail_closed");
|
||||
expect(
|
||||
fallbackLine(buildEquivalentConfigYaml(guardrail({ guardrailType: "akto", unreachable_fallback: "fail_open" }))),
|
||||
).toBe("unreachable_fallback: fail_open");
|
||||
});
|
||||
|
||||
it("explains the shown value is the guardrail's default only when nothing was set explicitly", () => {
|
||||
const rawLine = (g: TeamGuardrail) =>
|
||||
buildEquivalentConfigYaml(g)
|
||||
.split("\n")
|
||||
.find((l) => l.includes("unreachable_fallback:"));
|
||||
expect(rawLine(guardrail({ guardrailType: "agent_365" }))).toBe(
|
||||
" unreachable_fallback: fail_open # fail_closed blocks, fail_open proceeds when the guardrail endpoint is unreachable. Shown value is this guardrail's default.",
|
||||
);
|
||||
expect(rawLine(guardrail({ guardrailType: "agent_365", unreachable_fallback: "fail_closed" }))).toBe(
|
||||
" unreachable_fallback: fail_closed # fail_closed blocks, fail_open proceeds when the guardrail endpoint is unreachable",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
export type TeamGuardrailStatus = "active" | "pending" | "rejected";
|
||||
|
||||
export type TeamGuardrail = {
|
||||
id: string;
|
||||
team: string;
|
||||
name: string;
|
||||
endpoint: string;
|
||||
status: TeamGuardrailStatus;
|
||||
model: string;
|
||||
forwardKey: boolean;
|
||||
description: string;
|
||||
method: "POST" | "GET";
|
||||
customHeaders: {
|
||||
key: string;
|
||||
value: string;
|
||||
}[];
|
||||
extraHeaders: string[];
|
||||
submittedAt: string;
|
||||
submittedBy: string;
|
||||
mode?: string;
|
||||
unreachable_fallback?: string | null;
|
||||
additionalProviderParams?: Record<string, unknown>;
|
||||
guardrailType?: string;
|
||||
};
|
||||
|
||||
const FAIL_OPEN_BY_DEFAULT_GUARDRAILS: ReadonlySet<string> = new Set(["agent_365", "typesafe"]);
|
||||
|
||||
export function defaultUnreachableFallback(guardrailType: string | undefined): "fail_open" | "fail_closed" {
|
||||
return guardrailType !== undefined && FAIL_OPEN_BY_DEFAULT_GUARDRAILS.has(guardrailType)
|
||||
? "fail_open"
|
||||
: "fail_closed";
|
||||
}
|
||||
|
||||
function unreachableFallbackLine(g: TeamGuardrail): string {
|
||||
const hint = "fail_closed blocks, fail_open proceeds when the guardrail endpoint is unreachable";
|
||||
if (g.unreachable_fallback) {
|
||||
return ` unreachable_fallback: ${g.unreachable_fallback} # ${hint}`;
|
||||
}
|
||||
return ` unreachable_fallback: ${defaultUnreachableFallback(g.guardrailType)} # ${hint}. Shown value is this guardrail's default.`;
|
||||
}
|
||||
|
||||
export function buildEquivalentConfigYaml(g: TeamGuardrail): string {
|
||||
const lines: string[] = [
|
||||
"litellm_settings:",
|
||||
" guardrails:",
|
||||
` - guardrail_name: "${g.name.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`,
|
||||
" litellm_params:",
|
||||
` guardrail: ${g.guardrailType ?? "generic_guardrail_api"}`,
|
||||
` mode: ${g.mode ?? "pre_call"} # or post_call, during_call`,
|
||||
` api_base: ${g.endpoint || "https://your-guardrail-api.com"}`,
|
||||
" api_key: os.environ/YOUR_GUARDRAIL_API_KEY # optional",
|
||||
unreachableFallbackLine(g),
|
||||
` forward_api_key: ${g.forwardKey}`,
|
||||
];
|
||||
if (g.model && g.model !== "—") {
|
||||
lines.push(` model: "${g.model}" # LLM model name sent to the guardrail for context`);
|
||||
}
|
||||
if (g.customHeaders.length > 0) {
|
||||
lines.push(" headers: # static headers (sent with every request)");
|
||||
for (const h of g.customHeaders) {
|
||||
lines.push(` ${h.key}: "${String(h.value).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`);
|
||||
}
|
||||
}
|
||||
if (g.extraHeaders.length > 0) {
|
||||
lines.push(" extra_headers: # forward these client request headers to the guardrail");
|
||||
for (const name of g.extraHeaders) {
|
||||
lines.push(` - ${name}`);
|
||||
}
|
||||
}
|
||||
if (g.additionalProviderParams && Object.keys(g.additionalProviderParams).length > 0) {
|
||||
lines.push(" additional_provider_specific_params:");
|
||||
for (const [k, v] of Object.entries(g.additionalProviderParams)) {
|
||||
const val = typeof v === "string" ? `"${v}"` : String(v);
|
||||
lines.push(` ${k}: ${val}`);
|
||||
}
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
12
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
12
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
|
|
@ -25570,9 +25570,11 @@ export interface components {
|
|||
timeout?: number | null;
|
||||
/**
|
||||
* Unreachable Fallback
|
||||
* @description Behavior when a guardrail endpoint is unreachable due to network errors. Implemented by guardrail='generic_guardrail_api', 'agent_365', 'akto', 'vigil_guard', 'repelloai', 'headroom', 'compresr', and 'typesafe'. 'fail_closed' raises an error. 'fail_open' logs a critical error and allows the request to proceed. Unset applies the guardrail's own default: 'agent_365' and 'typesafe' fail open, the others fail closed.
|
||||
* @description Behavior when a guardrail endpoint is unreachable due to network errors. Implemented by guardrail='generic_guardrail_api', 'agent_365', 'akto', 'vigil_guard', 'repelloai', 'headroom', 'compresr', and 'typesafe'. 'fail_closed' raises an error (default). 'fail_open' logs a critical error and allows the request to proceed.
|
||||
* @default fail_closed
|
||||
* @enum {string}
|
||||
*/
|
||||
unreachable_fallback?: ("fail_closed" | "fail_open") | null;
|
||||
unreachable_fallback: "fail_closed" | "fail_open";
|
||||
/**
|
||||
* Violation Message Template
|
||||
* @description Custom message when a guardrail blocks an action. Supports placeholders like {tool_name}, {rule_id}, and {default_message}.
|
||||
|
|
@ -34794,9 +34796,11 @@ export interface components {
|
|||
tracker_api_key?: string | null;
|
||||
/**
|
||||
* Unreachable Fallback
|
||||
* @description Behavior when the guardrail endpoint is unreachable. 'fail_closed' blocks, 'fail_open' allows and logs. Unset applies the guardrail's own default: 'agent_365' and 'typesafe' fail open, the others fail closed.
|
||||
* @description Behavior when the headroom compression service is unreachable or errors. 'fail_closed' raises an error (default). 'fail_open' logs a critical error and forwards the request uncompressed instead of blocking it.
|
||||
* @default fail_closed
|
||||
* @enum {string}
|
||||
*/
|
||||
unreachable_fallback?: ("fail_closed" | "fail_open") | null;
|
||||
unreachable_fallback: "fail_closed" | "fail_open";
|
||||
/**
|
||||
* Use V2
|
||||
* @description If True and guardrail='noma', route to the new Noma v2 implementation instead of the legacy implementation.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue