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:
Ninad Phalak 2026-09-03 21:09:48 -05:00
parent 46438d7cf7
commit a0abb9a499
No known key found for this signature in database
GPG key ID: 59119ED515433744
12 changed files with 62 additions and 61 deletions

View file

@ -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,
}

View file

@ -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

View file

@ -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

View file

@ -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):

View file

@ -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"

View file

@ -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

View file

@ -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

View file

Before

Width:  |  Height:  |  Size: 498 B

After

Width:  |  Height:  |  Size: 498 B

View file

@ -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"],

View file

@ -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", () => {

View file

@ -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",
},
];

View file

@ -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 =>