mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-08 03:08:45 +00:00
refactor(guardrails): name the guardrail llm_shield_proxy throughout
The integration was called llm_shield in code, llm-shield in the example config, and LLM Shield in the dashboard, while the product and its PyPI package are both llm-shield-proxy. An operator who saw the guardrail in LiteLLM could not tell what to install. One identifier now: llm_shield_proxy for the enum value, module, directory, class, config model, logo and environment variables, with LLM Shield Proxy as the display name. That matches `pip install llm-shield-proxy`. Renames only; no behaviour change.
This commit is contained in:
parent
46438d7cf7
commit
a0abb9a499
12 changed files with 62 additions and 61 deletions
|
|
@ -2,16 +2,16 @@ from typing import TYPE_CHECKING, Final
|
|||
|
||||
from litellm.types.guardrails import SupportedGuardrailIntegrations
|
||||
|
||||
from .llm_shield import LLMShieldGuardrail
|
||||
from .llm_shield_proxy import LLMShieldProxyGuardrail
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from litellm.types.guardrails import Guardrail, LitellmParams
|
||||
|
||||
|
||||
def initialize_guardrail(litellm_params: "LitellmParams", guardrail: "Guardrail"):
|
||||
def initialize_guardrail(litellm_params: "LitellmParams", guardrail: "Guardrail") -> LLMShieldProxyGuardrail:
|
||||
import litellm
|
||||
|
||||
_llm_shield_guardrail_callback: Final = LLMShieldGuardrail(
|
||||
_llm_shield_guardrail_callback: Final = LLMShieldProxyGuardrail(
|
||||
api_key=litellm_params.api_key,
|
||||
api_base=litellm_params.api_base,
|
||||
guardrail_name=guardrail.get("guardrail_name", ""),
|
||||
|
|
@ -24,10 +24,10 @@ def initialize_guardrail(litellm_params: "LitellmParams", guardrail: "Guardrail"
|
|||
|
||||
|
||||
guardrail_initializer_registry: Final = { # mutable-ok: module-level registry, built once and never mutated
|
||||
SupportedGuardrailIntegrations.LLM_SHIELD.value: initialize_guardrail,
|
||||
SupportedGuardrailIntegrations.LLM_SHIELD_PROXY.value: initialize_guardrail,
|
||||
}
|
||||
|
||||
|
||||
guardrail_class_registry: Final = { # mutable-ok: module-level registry, built once and never mutated
|
||||
SupportedGuardrailIntegrations.LLM_SHIELD.value: LLMShieldGuardrail,
|
||||
SupportedGuardrailIntegrations.LLM_SHIELD_PROXY.value: LLMShieldProxyGuardrail,
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
# Example LiteLLM Proxy configuration for LLM Shield
|
||||
# LLM Shield is a self-hosted PII gateway: https://github.com/ninadphalak/LLM-Shield-Proxy
|
||||
# Example LiteLLM Proxy configuration for LLM Shield Proxy
|
||||
# LLM Shield Proxy is a self-hosted PII gateway: https://github.com/ninadphalak/LLM-Shield-Proxy
|
||||
#
|
||||
# Unlike a masking guardrail, LLM Shield's substitution is reversible. Personal data is
|
||||
# Unlike a masking guardrail, LLM Shield Proxy's substitution is reversible. Personal data is
|
||||
# replaced with placeholders before the request goes to the provider, and the original
|
||||
# values are put back into the model's reply, so the end user still sees real data while
|
||||
# the provider never received it.
|
||||
|
|
@ -15,25 +15,25 @@ model_list:
|
|||
guardrails:
|
||||
# Both modes belong on ONE entry. pre_call redacts the outbound request and post_call
|
||||
# restores the reply; listing only pre_call would send placeholders back to the user.
|
||||
- guardrail_name: "llm-shield"
|
||||
- guardrail_name: "llm_shield_proxy"
|
||||
litellm_params:
|
||||
guardrail: llm_shield
|
||||
guardrail: llm_shield_proxy
|
||||
mode: ["pre_call", "post_call"]
|
||||
default_on: true
|
||||
# Your own LLM Shield deployment. Defaults to http://localhost:8000, and also reads
|
||||
# LLM_SHIELD_API_BASE from the environment.
|
||||
# Your own LLM Shield Proxy deployment. Defaults to http://localhost:8000, and also reads
|
||||
# LLM_SHIELD_PROXY_API_BASE from the environment.
|
||||
api_base: "http://localhost:8000"
|
||||
# A virtual key configured on that deployment. Also reads LLM_SHIELD_API_KEY.
|
||||
api_key: os.environ/LLM_SHIELD_API_KEY
|
||||
# A virtual key configured on that deployment. Also reads LLM_SHIELD_PROXY_API_KEY.
|
||||
api_key: os.environ/LLM_SHIELD_PROXY_API_KEY
|
||||
|
||||
# Usage:
|
||||
#
|
||||
# 1. Run LLM Shield somewhere the proxy can reach:
|
||||
# 1. Run LLM Shield Proxy somewhere the proxy can reach:
|
||||
# pip install llm-shield-proxy
|
||||
# llm-shield-proxy --port 8000
|
||||
#
|
||||
# 2. Point this config at it and start the proxy:
|
||||
# export LLM_SHIELD_API_KEY="your-virtual-key"
|
||||
# export LLM_SHIELD_PROXY_API_KEY="your-virtual-key"
|
||||
# litellm --config example_config.yaml
|
||||
#
|
||||
# 3. Send a request containing personal data:
|
||||
|
|
@ -47,7 +47,7 @@ guardrails:
|
|||
#
|
||||
# Notes:
|
||||
#
|
||||
# - Requests are refused if LLM Shield is unreachable or returns an error, rather than
|
||||
# - Requests are refused if LLM Shield Proxy is unreachable or returns an error, rather than
|
||||
# being forwarded. Sending them on would hand the provider exactly the data this
|
||||
# guardrail exists to withhold.
|
||||
# - Restoring a value requires the request and the reply to share a session. LiteLLM's
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
# +-------------------------------------------------------------+
|
||||
#
|
||||
# Use LLM Shield for reversible PII redaction
|
||||
# Use LLM Shield Proxy for reversible PII redaction
|
||||
# https://github.com/ninadphalak/LLM-Shield-Proxy
|
||||
#
|
||||
# +-------------------------------------------------------------+
|
||||
|
|
@ -38,7 +38,7 @@ if TYPE_CHECKING:
|
|||
from litellm.caching.caching import DualCache
|
||||
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
|
||||
|
||||
GUARDRAIL_NAME: Final = "llm_shield"
|
||||
GUARDRAIL_NAME: Final = "llm_shield_proxy"
|
||||
|
||||
_DEFAULT_API_BASE: Final = "http://localhost:8000"
|
||||
_REDACT_PATH: Final = "/v1/guard/redact"
|
||||
|
|
@ -172,7 +172,7 @@ def _choice_index(choice: object) -> int:
|
|||
return index if isinstance(index, int) else 0
|
||||
|
||||
|
||||
class LLMShieldGuardrail(CustomGuardrail):
|
||||
class LLMShieldProxyGuardrail(CustomGuardrail):
|
||||
"""Redacts PII before it leaves the proxy and restores it in the response.
|
||||
|
||||
Unlike a masking guardrail, the substitution is reversible. Outbound text is
|
||||
|
|
@ -199,8 +199,9 @@ class LLMShieldGuardrail(CustomGuardrail):
|
|||
**kwargs: Any, # noqa: LIT008 # kwargs-ok: forwarded verbatim to CustomGuardrail.__init__
|
||||
) -> None:
|
||||
self.async_handler = get_async_httpx_client(llm_provider=httpxSpecialProvider.GuardrailCallback)
|
||||
self.api_base: Final = (api_base or os.environ.get("LLM_SHIELD_API_BASE") or _DEFAULT_API_BASE).rstrip("/")
|
||||
self.api_key: Final = api_key or os.environ.get("LLM_SHIELD_API_KEY")
|
||||
env_base: Final = os.environ.get("LLM_SHIELD_PROXY_API_BASE")
|
||||
self.api_base: Final = (api_base or env_base or _DEFAULT_API_BASE).rstrip("/")
|
||||
self.api_key: Final = api_key or os.environ.get("LLM_SHIELD_PROXY_API_KEY")
|
||||
super().__init__(guardrail_name=guardrail_name, **kwargs)
|
||||
|
||||
@classmethod
|
||||
|
|
@ -219,7 +220,7 @@ class LLMShieldGuardrail(CustomGuardrail):
|
|||
return headers
|
||||
|
||||
async def _call_shield(self, path: str, session_id: str, payload: JsonBody) -> Mapping[str, object]:
|
||||
"""Posts to LLM Shield, failing closed on any transport or status error.
|
||||
"""Posts to LLM Shield Proxy, failing closed on any transport or status error.
|
||||
|
||||
A redaction guardrail that fails open sends the very data it exists to
|
||||
protect to a third-party provider, so an unreachable or erroring shield
|
||||
|
|
@ -235,16 +236,16 @@ class LLMShieldGuardrail(CustomGuardrail):
|
|||
response.raise_for_status()
|
||||
return response.json()
|
||||
except httpx.HTTPStatusError as exc:
|
||||
verbose_proxy_logger.exception("LLM Shield returned %s for %s", exc.response.status_code, path)
|
||||
verbose_proxy_logger.exception("LLM Shield Proxy returned %s for %s", exc.response.status_code, path)
|
||||
raise GuardrailRaisedException(
|
||||
guardrail_name=self.guardrail_name,
|
||||
message=f"LLM Shield returned {exc.response.status_code}; blocking the request.",
|
||||
message=f"LLM Shield Proxy returned {exc.response.status_code}; blocking the request.",
|
||||
) from exc
|
||||
except Exception as exc:
|
||||
verbose_proxy_logger.exception("LLM Shield call to %s failed", path)
|
||||
verbose_proxy_logger.exception("LLM Shield Proxy call to %s failed", path)
|
||||
raise GuardrailRaisedException(
|
||||
guardrail_name=self.guardrail_name,
|
||||
message="LLM Shield is unreachable; blocking the request.",
|
||||
message="LLM Shield Proxy is unreachable; blocking the request.",
|
||||
) from exc
|
||||
|
||||
async def _redact(self, texts: Sequence[str], session_id: str) -> Sequence[str]:
|
||||
|
|
@ -262,7 +263,7 @@ class LLMShieldGuardrail(CustomGuardrail):
|
|||
if not isinstance(returned, list) or len(returned) != len(sent):
|
||||
raise GuardrailRaisedException(
|
||||
guardrail_name=self.guardrail_name,
|
||||
message=f"LLM Shield {operation} returned an unexpected payload; blocking the request.",
|
||||
message=f"LLM Shield Proxy {operation} returned an unexpected payload; blocking the request.",
|
||||
)
|
||||
return tuple(returned)
|
||||
|
||||
|
|
@ -530,7 +531,7 @@ class LLMShieldGuardrail(CustomGuardrail):
|
|||
if not isinstance(emitted, str) or not isinstance(remaining, str):
|
||||
raise GuardrailRaisedException(
|
||||
guardrail_name=self.guardrail_name,
|
||||
message="LLM Shield stream rehydration returned an unexpected payload.",
|
||||
message="LLM Shield Proxy stream rehydration returned an unexpected payload.",
|
||||
)
|
||||
return emitted, remaining
|
||||
|
||||
|
|
@ -137,7 +137,7 @@ class SupportedGuardrailIntegrations(Enum):
|
|||
COMPRESR = "compresr"
|
||||
STRAIKER = "straiker"
|
||||
ALICE = "alice"
|
||||
LLM_SHIELD = "llm_shield"
|
||||
LLM_SHIELD_PROXY = "llm_shield_proxy"
|
||||
|
||||
|
||||
class Role(Enum):
|
||||
|
|
|
|||
|
|
@ -3,22 +3,22 @@ from pydantic import Field
|
|||
from .base import GuardrailConfigModel
|
||||
|
||||
|
||||
class LLMShieldGuardrailConfigModel(GuardrailConfigModel):
|
||||
class LLMShieldProxyGuardrailConfigModel(GuardrailConfigModel):
|
||||
api_key: str | None = Field(
|
||||
default=None,
|
||||
description=(
|
||||
"The virtual key for the LLM Shield instance. If not provided, the "
|
||||
"`LLM_SHIELD_API_KEY` environment variable is checked."
|
||||
"The virtual key for the LLM Shield Proxy instance. If not provided, the "
|
||||
"`LLM_SHIELD_PROXY_API_KEY` environment variable is checked."
|
||||
),
|
||||
)
|
||||
api_base: str | None = Field(
|
||||
default=None,
|
||||
description=(
|
||||
"The base URL of the LLM Shield instance. If not provided, the `LLM_SHIELD_API_BASE` "
|
||||
"The base URL of the LLM Shield Proxy instance. If not provided, the `LLM_SHIELD_PROXY_API_BASE` "
|
||||
"environment variable is checked, then `http://localhost:8000`."
|
||||
),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def ui_friendly_name() -> str:
|
||||
return "LLM Shield"
|
||||
return "LLM Shield Proxy"
|
||||
|
|
@ -33,7 +33,7 @@ external = [
|
|||
# Same reason: `**kwargs` forwards verbatim to CustomGuardrail.__init__, and the lifecycle
|
||||
# hook signatures inherit `Any` for `response` from CustomLogger, so narrowing them here
|
||||
# would break the override rather than describe it.
|
||||
"litellm/proxy/guardrails/guardrail_hooks/llm_shield/llm_shield.py" = ["ANN401"]
|
||||
"litellm/proxy/guardrails/guardrail_hooks/llm_shield_proxy/llm_shield_proxy.py" = ["ANN401"]
|
||||
|
||||
[lint.mccabe]
|
||||
max-complexity = 15
|
||||
|
|
|
|||
|
|
@ -6,16 +6,16 @@ from httpx import Request, Response
|
|||
|
||||
import litellm
|
||||
from litellm.exceptions import GuardrailRaisedException
|
||||
from litellm.proxy.guardrails.guardrail_hooks.llm_shield.llm_shield import (
|
||||
from litellm.proxy.guardrails.guardrail_hooks.llm_shield_proxy.llm_shield_proxy import (
|
||||
GUARDRAIL_NAME,
|
||||
LLMShieldGuardrail,
|
||||
LLMShieldProxyGuardrail,
|
||||
)
|
||||
from litellm.proxy.guardrails.init_guardrails import init_guardrails_v2
|
||||
from litellm.types.guardrails import GuardrailEventHooks
|
||||
from litellm.types.utils import Choices, Delta, Message, ModelResponse, ModelResponseStream, StreamingChoices
|
||||
|
||||
|
||||
def _guardrail(**overrides: object) -> LLMShieldGuardrail:
|
||||
def _guardrail(**overrides: object) -> LLMShieldProxyGuardrail:
|
||||
params: dict[str, object] = {
|
||||
"api_key": "test-key",
|
||||
"api_base": "http://shield.test",
|
||||
|
|
@ -24,7 +24,7 @@ def _guardrail(**overrides: object) -> LLMShieldGuardrail:
|
|||
"default_on": True,
|
||||
}
|
||||
params.update(overrides)
|
||||
return LLMShieldGuardrail(**params)
|
||||
return LLMShieldProxyGuardrail(**params)
|
||||
|
||||
|
||||
def _response(payload: dict, status_code: int = 200) -> Response:
|
||||
|
|
@ -35,7 +35,7 @@ def _response(payload: dict, status_code: int = 200) -> Response:
|
|||
)
|
||||
|
||||
|
||||
def _mock_post(guardrail: LLMShieldGuardrail, *payloads: dict) -> AsyncMock:
|
||||
def _mock_post(guardrail: LLMShieldProxyGuardrail, *payloads: dict) -> AsyncMock:
|
||||
"""Queues one shield response per expected call."""
|
||||
mock = AsyncMock(side_effect=[_response(p) for p in payloads])
|
||||
guardrail.async_handler.post = mock # type: ignore[method-assign]
|
||||
|
|
@ -55,30 +55,30 @@ async def _drain(generator) -> list:
|
|||
def test_llm_shield_guardrail_config(monkeypatch: pytest.MonkeyPatch):
|
||||
"""Should register through init_guardrails_v2 like any other provider."""
|
||||
monkeypatch.setattr(litellm, "guardrail_name_config_map", {})
|
||||
monkeypatch.setenv("LLM_SHIELD_API_KEY", "test-key")
|
||||
monkeypatch.setenv("LLM_SHIELD_PROXY_API_KEY", "test-key")
|
||||
|
||||
init_guardrails_v2(
|
||||
all_guardrails=[
|
||||
{
|
||||
"guardrail_name": "llm_shield",
|
||||
"litellm_params": {"guardrail": "llm_shield", "mode": "pre_call", "default_on": True},
|
||||
"guardrail_name": "llm_shield_proxy",
|
||||
"litellm_params": {"guardrail": "llm_shield_proxy", "mode": "pre_call", "default_on": True},
|
||||
}
|
||||
],
|
||||
config_file_path="",
|
||||
)
|
||||
|
||||
registered = [cb for cb in litellm.callbacks if isinstance(cb, LLMShieldGuardrail)]
|
||||
registered = [cb for cb in litellm.callbacks if isinstance(cb, LLMShieldProxyGuardrail)]
|
||||
assert len(registered) == 1
|
||||
assert registered[0].guardrail_name == "llm_shield"
|
||||
assert registered[0].guardrail_name == "llm_shield_proxy"
|
||||
|
||||
|
||||
class TestLLMShieldInitialization:
|
||||
class TestLLMShieldProxyInitialization:
|
||||
def test_api_base_defaults_to_localhost(self, monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.delenv("LLM_SHIELD_API_BASE", raising=False)
|
||||
monkeypatch.delenv("LLM_SHIELD_PROXY_API_BASE", raising=False)
|
||||
assert _guardrail(api_base=None).api_base == "http://localhost:8000"
|
||||
|
||||
def test_api_base_reads_environment(self, monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.setenv("LLM_SHIELD_API_BASE", "http://shield.internal:9000")
|
||||
monkeypatch.setenv("LLM_SHIELD_PROXY_API_BASE", "http://shield.internal:9000")
|
||||
assert _guardrail(api_base=None).api_base == "http://shield.internal:9000"
|
||||
|
||||
def test_trailing_slash_is_stripped(self):
|
||||
|
|
@ -135,7 +135,7 @@ class TestRedaction:
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_request_without_text_is_untouched(self):
|
||||
"""No text to redact means no call to LLM Shield.
|
||||
"""No text to redact means no call to LLM Shield Proxy.
|
||||
|
||||
This deliberately uses a request with no caller text at all. An earlier
|
||||
version used a Responses-API `input`, which asserted the very bypass that
|
||||
|
Before Width: | Height: | Size: 498 B After Width: | Height: | Size: 498 B |
|
|
@ -320,9 +320,9 @@ export const GUARDRAIL_PRESETS: Record<string, GuardrailPreset> = {
|
|||
mode: "pre_call",
|
||||
defaultOn: false,
|
||||
},
|
||||
llm_shield: {
|
||||
provider: "LLM Shield",
|
||||
guardrailNameSuggestion: "LLM Shield",
|
||||
llm_shield_proxy: {
|
||||
provider: "LLM Shield Proxy",
|
||||
guardrailNameSuggestion: "LLM Shield Proxy",
|
||||
// Both halves are required. With only pre_call the request is redacted and the
|
||||
// placeholders are handed straight back to the caller.
|
||||
mode: ["pre_call", "post_call"],
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ const EXPECTED_PARTNER_LOGO_FILES: Record<string, string> = {
|
|||
repelloai: "repelloai.png",
|
||||
straiker: "straiker.svg",
|
||||
alice: "alice.svg",
|
||||
llm_shield: "llm_shield.svg",
|
||||
llm_shield_proxy: "llm_shield_proxy.svg",
|
||||
};
|
||||
|
||||
describe("guardrail_garden_data logos", () => {
|
||||
|
|
|
|||
|
|
@ -475,14 +475,14 @@ export const PARTNER_GUARDRAIL_CARDS: GuardrailCardInfo[] = [
|
|||
providerKey: "Alice",
|
||||
},
|
||||
{
|
||||
id: "llm_shield",
|
||||
name: "LLM Shield",
|
||||
id: "llm_shield_proxy",
|
||||
name: "LLM Shield Proxy",
|
||||
description:
|
||||
"Self-hosted PII redaction that puts the original values back into the model's response, so the provider never receives personal data while the end user still sees it.",
|
||||
category: "partner",
|
||||
logo: guardrailLogoMap["LLM Shield"],
|
||||
logo: guardrailLogoMap["LLM Shield Proxy"],
|
||||
tags: ["PII", "Data Privacy", "Compliance", "Streaming"],
|
||||
providerKey: "LLM Shield",
|
||||
providerKey: "LLM Shield Proxy",
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import aimSecurityLogo from "../../../../../public/assets/logos/aim_security.jpeg";
|
||||
import aktoLogo from "../../../../../public/assets/logos/akto.svg";
|
||||
import aliceLogo from "../../../../../public/assets/logos/alice.svg";
|
||||
import llmShieldLogo from "../../../../../public/assets/logos/llm_shield.svg";
|
||||
import llmShieldProxyLogo from "../../../../../public/assets/logos/llm_shield_proxy.svg";
|
||||
import aporiaLogo from "../../../../../public/assets/logos/aporia.png";
|
||||
import bedrockLogo from "../../../../../public/assets/logos/bedrock.svg";
|
||||
import catoNetworksLogo from "../../../../../public/assets/logos/cato_networks.svg";
|
||||
|
|
@ -86,7 +86,7 @@ export const guardrail_provider_map: Record<string, string> = {
|
|||
QostodianNexus: "qostodian_nexus",
|
||||
Repelloai: "repelloai",
|
||||
Alice: "alice",
|
||||
"LLM Shield": "llm_shield",
|
||||
"LLM Shield Proxy": "llm_shield_proxy",
|
||||
};
|
||||
|
||||
// Function to populate provider map from API response - updates the original map
|
||||
|
|
@ -210,7 +210,7 @@ export const guardrailLogoMap = {
|
|||
"RepelloAI Argus": repelloAiLogo.src,
|
||||
Straiker: straikerLogo.src,
|
||||
Alice: aliceLogo.src,
|
||||
"LLM Shield": llmShieldLogo.src,
|
||||
"LLM Shield Proxy": llmShieldProxyLogo.src,
|
||||
} satisfies Record<string, string>;
|
||||
|
||||
export const getGuardrailLogo = (displayName: string): string | undefined =>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue