mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-01 02:02:20 +00:00
feat(guardrails): send a configured gateway_name from noma_v2 to Noma (#43678)
* feat(guardrails): send a configured gateway_name from noma_v2 to Noma The noma_v2 guardrail accepts a gateway_name param, falling back to the NOMA_GATEWAY_NAME env var. The value is stripped, and when it is non-empty it goes out as a top-level gateway_name field on /litellm/guardrail. The param works for both guardrail: noma_v2 and guardrail: noma with use_v2, and it is appended after the existing constructor params so positional callers keep their meaning * chore(ui): regenerate OpenAPI snapshot and dashboard types for gateway_name The new noma_v2 gateway_name param shows up in the proxy OpenAPI spec, so the lazy snapshot and the generated dashboard types need regenerating * Update litellm/proxy/guardrails/guardrail_hooks/noma/noma_v2.py Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
This commit is contained in:
parent
d2cbc94fc6
commit
0c553f0398
11 changed files with 132 additions and 0 deletions
|
|
@ -12133,6 +12133,18 @@
|
|||
"description": "Whether to fail the request if the guardrail encounters an error. Implemented by guardrail='model_armor', 'generic_guardrail_api' and 'crowdstrike_aidr'. True (default) raises the error. False logs a critical error and lets the request proceed, so only a valid guardrail response can block or modify it.",
|
||||
"title": "Fail On Error"
|
||||
},
|
||||
"gateway_name": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "noma_v2 only: name of this gateway, used as the gateway_host label on Noma scans",
|
||||
"title": "Gateway Name"
|
||||
},
|
||||
"grounding_check": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ def initialize_guardrail_v2(litellm_params: "LitellmParams", guardrail: "Guardra
|
|||
api_key=litellm_params.api_key,
|
||||
api_base=litellm_params.api_base,
|
||||
application_id=litellm_params.application_id,
|
||||
gateway_name=litellm_params.gateway_name,
|
||||
monitor_mode=litellm_params.monitor_mode,
|
||||
block_failures=litellm_params.block_failures,
|
||||
event_hook=litellm_params.mode,
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ class NomaV2Guardrail(CustomGuardrail):
|
|||
application_id: str | None = None,
|
||||
monitor_mode: bool | None = None,
|
||||
block_failures: bool | None = None,
|
||||
gateway_name: str | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
self.async_handler = get_async_httpx_client(llm_provider=httpxSpecialProvider.GuardrailCallback)
|
||||
|
|
@ -69,6 +70,9 @@ class NomaV2Guardrail(CustomGuardrail):
|
|||
self.api_key = api_key or os.environ.get("NOMA_API_KEY")
|
||||
self.api_base = (api_base or os.environ.get("NOMA_API_BASE") or _DEFAULT_API_BASE).rstrip("/")
|
||||
self.application_id = application_id or os.environ.get("NOMA_APPLICATION_ID")
|
||||
self.gateway_name = self._get_non_empty_str(gateway_name) or self._get_non_empty_str(
|
||||
os.environ.get("NOMA_GATEWAY_NAME")
|
||||
)
|
||||
if monitor_mode is None:
|
||||
self.monitor_mode = os.environ.get("NOMA_MONITOR_MODE", "false").lower() == "true"
|
||||
else:
|
||||
|
|
@ -166,6 +170,8 @@ class NomaV2Guardrail(CustomGuardrail):
|
|||
}
|
||||
if application_id:
|
||||
payload["application_id"] = application_id
|
||||
if self.gateway_name:
|
||||
payload["gateway_name"] = self.gateway_name
|
||||
return payload
|
||||
|
||||
@staticmethod
|
||||
|
|
|
|||
|
|
@ -783,6 +783,10 @@ class NomaGuardrailConfigModel(BaseModel):
|
|||
default=None,
|
||||
description="Application ID for Noma Security. Defaults to 'litellm' if not provided",
|
||||
)
|
||||
gateway_name: str | None = Field(
|
||||
default=None,
|
||||
description="noma_v2 only: name of this gateway, used as the gateway_host label on Noma scans",
|
||||
)
|
||||
monitor_mode: bool | None = Field(
|
||||
default=None,
|
||||
description="If True, logs violations without blocking. Defaults to False if not provided",
|
||||
|
|
|
|||
|
|
@ -39,6 +39,10 @@ class NomaV2GuardrailConfigModel(GuardrailConfigModel):
|
|||
default=None,
|
||||
description="The Noma Application ID. Reads from NOMA_APPLICATION_ID env var if None.",
|
||||
)
|
||||
gateway_name: str | None = Field(
|
||||
default=None,
|
||||
description="Gateway name, used as the gateway_host label on Noma scans. Falls back to NOMA_GATEWAY_NAME.",
|
||||
)
|
||||
monitor_mode: bool | None = Field(
|
||||
default=None,
|
||||
description="When true, run guardrail checks in monitor mode.",
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ class TestNomaV2Configuration:
|
|||
assert "api_key" in noma_v2_params
|
||||
assert "api_base" in noma_v2_params
|
||||
assert "application_id" in noma_v2_params
|
||||
assert "gateway_name" in noma_v2_params
|
||||
assert "monitor_mode" in noma_v2_params
|
||||
assert "block_failures" in noma_v2_params
|
||||
|
||||
|
|
|
|||
0
tests/unit/proxy/guardrails/__init__.py
Normal file
0
tests/unit/proxy/guardrails/__init__.py
Normal file
0
tests/unit/proxy/guardrails/guardrail_hooks/__init__.py
Normal file
0
tests/unit/proxy/guardrails/guardrail_hooks/__init__.py
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
import json
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
import respx
|
||||
|
||||
import litellm
|
||||
from litellm.proxy.guardrails.guardrail_hooks.noma import (
|
||||
NomaV2Guardrail,
|
||||
guardrail_initializer_registry,
|
||||
)
|
||||
from litellm.types.guardrails import LitellmParams
|
||||
|
||||
_API_BASE = "https://noma.example.test"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _fresh_httpx_client(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(litellm, "disable_aiohttp_transport", True)
|
||||
monkeypatch.setattr(litellm, "in_memory_llm_clients_cache", None)
|
||||
monkeypatch.delenv("NOMA_GATEWAY_NAME", raising=False)
|
||||
|
||||
|
||||
def _guardrail(gateway_name: str | None) -> NomaV2Guardrail:
|
||||
return NomaV2Guardrail(
|
||||
api_base=_API_BASE,
|
||||
gateway_name=gateway_name,
|
||||
guardrail_name="noma-guard",
|
||||
event_hook="pre_call",
|
||||
default_on=True,
|
||||
)
|
||||
|
||||
|
||||
async def _scan_body(guardrail: NomaV2Guardrail, respx_mock: respx.MockRouter) -> dict[str, object]:
|
||||
route = respx_mock.post(f"{_API_BASE}/litellm/guardrail").respond(json={"action": "NONE"})
|
||||
await guardrail.apply_guardrail(inputs={"texts": ["hello"]}, request_data={"metadata": {}}, input_type="request")
|
||||
assert route.call_count == 1
|
||||
return json.loads(route.calls.last.request.content)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(("guardrail_type", "extra_params"), [("noma_v2", {}), ("noma", {"use_v2": True})])
|
||||
async def test_gateway_name_from_guardrail_config_reaches_noma(
|
||||
guardrail_type: str, extra_params: dict[str, bool], respx_mock: respx.MockRouter
|
||||
) -> None:
|
||||
litellm_params = LitellmParams(
|
||||
guardrail=guardrail_type,
|
||||
mode="pre_call",
|
||||
api_base=_API_BASE,
|
||||
gateway_name="prod-us-east",
|
||||
**extra_params,
|
||||
)
|
||||
guardrail = guardrail_initializer_registry[guardrail_type](litellm_params, {"guardrail_name": "noma-guard"})
|
||||
|
||||
assert (await _scan_body(guardrail, respx_mock))["gateway_name"] == "prod-us-east"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
("configured", "env_value", "expected"),
|
||||
[
|
||||
(None, "env-gateway", "env-gateway"),
|
||||
("config-gateway", "env-gateway", "config-gateway"),
|
||||
(" config-gateway ", None, "config-gateway"),
|
||||
],
|
||||
)
|
||||
async def test_gateway_name_resolution(
|
||||
configured: str | None,
|
||||
env_value: str | None,
|
||||
expected: str,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
respx_mock: respx.MockRouter,
|
||||
) -> None:
|
||||
if env_value is not None:
|
||||
monkeypatch.setenv("NOMA_GATEWAY_NAME", env_value)
|
||||
|
||||
assert (await _scan_body(_guardrail(configured), respx_mock))["gateway_name"] == expected
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("configured", [None, "", " "])
|
||||
async def test_unset_or_blank_gateway_name_is_left_out(configured: str | None, respx_mock: respx.MockRouter) -> None:
|
||||
assert "gateway_name" not in await _scan_body(_guardrail(configured), respx_mock)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_positional_args_keep_their_meaning_after_gateway_name_was_added(respx_mock: respx.MockRouter) -> None:
|
||||
guardrail = NomaV2Guardrail("test-api-key", _API_BASE, "test-app", False, True)
|
||||
|
||||
body = await _scan_body(guardrail, respx_mock)
|
||||
|
||||
assert body["monitor_mode"] is False
|
||||
assert body["application_id"] == "test-app"
|
||||
assert "gateway_name" not in body
|
||||
respx_mock.post(f"{_API_BASE}/litellm/guardrail").respond(status_code=503)
|
||||
with pytest.raises(httpx.HTTPStatusError):
|
||||
await guardrail.apply_guardrail(
|
||||
inputs={"texts": ["hello"]}, request_data={"metadata": {}}, input_type="request"
|
||||
)
|
||||
5
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
5
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
|
|
@ -34348,6 +34348,11 @@ export interface components {
|
|||
* @default true
|
||||
*/
|
||||
fail_on_error: boolean | null;
|
||||
/**
|
||||
* Gateway Name
|
||||
* @description noma_v2 only: name of this gateway, used as the gateway_host label on Noma scans
|
||||
*/
|
||||
gateway_name?: string | null;
|
||||
/**
|
||||
* Grounding Check
|
||||
* @description Enable grounding verification to ensure output is grounded in provided context.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue