diff --git a/litellm/proxy/guardrails/guardrail_hooks/llama_guard/README.md b/litellm/proxy/guardrails/guardrail_hooks/llama_guard/README.md new file mode 100644 index 00000000000..5806bd0622f --- /dev/null +++ b/litellm/proxy/guardrails/guardrail_hooks/llama_guard/README.md @@ -0,0 +1,87 @@ +# Llama Guard Guardrail + +Content-safety guardrail backed by a [Llama Guard](https://github.com/meta-llama/PurpleLlama/tree/main/Llama-Guard3) classifier model. It screens request and/or response text against Meta's MLCommons hazard taxonomy (S1–S14) and blocks content the model flags as `unsafe`. + +The Llama Guard model is called through LiteLLM itself, so any provider that serves a Llama Guard model works: `together_ai`, `groq`, `fireworks_ai`, `ollama`, `huggingface`, self-hosted `hosted_vllm`, etc. + +## Features + +- Pre-call (screen the user prompt), during-call (parallel moderation), and post-call (screen the model's response) modes +- Enforce the full MLCommons taxonomy or a subset of hazard categories +- Fully custom category block via `unsafe_content_categories` +- Provider-agnostic: the classification prompt is self-contained, so it does not depend on the serving provider applying a Llama Guard chat template +- Fails open (logs and allows the request) if the classifier is unreachable, so an outage of the safety model does not take down traffic +- Violation errors name the exact hazard categories that were triggered + +## Configuration + +### Required parameters + +- `model`: the Llama Guard model to call, e.g. `together_ai/meta-llama/Llama-Guard-4-12B`, `groq/llama-guard-3-8b`, `ollama/llama-guard3`. + +### Optional parameters + +- `api_base`: base URL for the Llama Guard model endpoint. +- `api_key`: API key for the Llama Guard model endpoint (`os.environ/...` is supported). +- `categories`: list of hazard codes to enforce (e.g. `["S1", "S10", "S11"]`). Defaults to the full `S1`–`S14` taxonomy. +- `unsafe_content_categories`: a fully custom category block that overrides the built-in taxonomy text. +- `default_on` (default `false`): apply this guardrail to every request without opting in per request. + +### Hazard taxonomy (default) + +| Code | Category | Code | Category | +|------|----------|------|----------| +| S1 | Violent Crimes | S8 | Intellectual Property | +| S2 | Non-Violent Crimes | S9 | Indiscriminate Weapons | +| S3 | Sex-Related Crimes | S10 | Hate | +| S4 | Child Sexual Exploitation | S11 | Suicide & Self-Harm | +| S5 | Defamation | S12 | Sexual Content | +| S6 | Specialized Advice | S13 | Elections | +| S7 | Privacy | S14 | Code Interpreter Abuse | + +## Usage examples + +### Screen the user prompt (pre-call) + +```yaml +guardrails: + - guardrail_name: "llama-guard-input" + litellm_params: + guardrail: llama_guard + mode: pre_call + default_on: true + model: together_ai/meta-llama/Llama-Guard-4-12B + api_key: os.environ/TOGETHERAI_API_KEY +``` + +### Screen the model's response (post-call) + +```yaml +guardrails: + - guardrail_name: "llama-guard-output" + litellm_params: + guardrail: llama_guard + mode: post_call + default_on: true + model: groq/llama-guard-3-8b + api_key: os.environ/GROQ_API_KEY +``` + +### Enforce only a subset of categories + +```yaml +guardrails: + - guardrail_name: "llama-guard-strict" + litellm_params: + guardrail: llama_guard + mode: pre_call + model: ollama/llama-guard3 + api_base: http://localhost:11434 + categories: ["S1", "S9", "S11"] # violent crimes, weapons, self-harm +``` + +When Llama Guard flags a request, LiteLLM raises a `content_policy_violation` error that lists the triggered categories, for example: + +``` +Violated Llama Guard content policy. Categories: S9 (Indiscriminate Weapons) +``` diff --git a/litellm/proxy/guardrails/guardrail_hooks/llama_guard/__init__.py b/litellm/proxy/guardrails/guardrail_hooks/llama_guard/__init__.py new file mode 100644 index 00000000000..7f6aba5a80b --- /dev/null +++ b/litellm/proxy/guardrails/guardrail_hooks/llama_guard/__init__.py @@ -0,0 +1,48 @@ +from typing import TYPE_CHECKING, Final + +from litellm.types.guardrails import SupportedGuardrailIntegrations + +from .llama_guard import LlamaGuardGuardrail + +if TYPE_CHECKING: + from litellm.types.guardrails import Guardrail, LitellmParams + + +def initialize_guardrail(litellm_params: "LitellmParams", guardrail: "Guardrail"): + import litellm + + model: Final = litellm_params.get("model") + if not model: + raise ValueError("llama_guard guardrail requires `model` in litellm_params") + + _guardrail: Final = LlamaGuardGuardrail( + model=model, + guardrail_name=guardrail.get("guardrail_name", ""), + api_base=litellm_params.get("api_base"), + api_key=litellm_params.get("api_key"), + categories=litellm_params.get("categories"), + unsafe_content_categories=litellm_params.get("unsafe_content_categories"), + event_hook=litellm_params.get("mode"), + default_on=litellm_params.get("default_on", False), + ) + litellm.logging_callback_manager.add_litellm_callback(_guardrail) + + return _guardrail + + +guardrail_initializer_registry: Final = { + SupportedGuardrailIntegrations.LLAMA_GUARD.value: initialize_guardrail, +} + + +guardrail_class_registry: Final = { + SupportedGuardrailIntegrations.LLAMA_GUARD.value: LlamaGuardGuardrail, +} + + +__all__ = [ + "LlamaGuardGuardrail", + "guardrail_class_registry", + "guardrail_initializer_registry", + "initialize_guardrail", +] diff --git a/litellm/proxy/guardrails/guardrail_hooks/llama_guard/llama_guard.py b/litellm/proxy/guardrails/guardrail_hooks/llama_guard/llama_guard.py new file mode 100644 index 00000000000..a15aba0cf2b --- /dev/null +++ b/litellm/proxy/guardrails/guardrail_hooks/llama_guard/llama_guard.py @@ -0,0 +1,270 @@ +# +-------------------------------------------------------------+ +# +# Llama Guard content-safety guardrail +# Classifies request/response text with a Llama Guard model +# (Meta's LLM-based content-safety classifier) and blocks +# content that violates the configured MLCommons hazard +# categories. +# +# https://github.com/meta-llama/PurpleLlama/tree/main/Llama-Guard3 +# +# +-------------------------------------------------------------+ +from __future__ import annotations + +from collections.abc import Sequence +from typing import TYPE_CHECKING, Any, Final + +import litellm +from litellm._logging import verbose_proxy_logger +from litellm.integrations.custom_guardrail import CustomGuardrail +from litellm.proxy._types import ProxyException, UserAPIKeyAuth +from litellm.types.guardrails import GuardrailEventHooks +from litellm.types.utils import CallTypesLiteral, Choices, LLMResponseTypes, ModelResponse + +if TYPE_CHECKING: + from litellm import DualCache + +# The MLCommons hazard taxonomy Llama Guard 3 / 4 is trained on (S1-S14). +# https://github.com/meta-llama/PurpleLlama/blob/main/Llama-Guard3/8B/MODEL_CARD.md +DEFAULT_UNSAFE_CATEGORIES: Final[dict[str, str]] = { + "S1": "Violent Crimes", + "S2": "Non-Violent Crimes", + "S3": "Sex-Related Crimes", + "S4": "Child Sexual Exploitation", + "S5": "Defamation", + "S6": "Specialized Advice", + "S7": "Privacy", + "S8": "Intellectual Property", + "S9": "Indiscriminate Weapons", + "S10": "Hate", + "S11": "Suicide & Self-Harm", + "S12": "Sexual Content", + "S13": "Elections", + "S14": "Code Interpreter Abuse", +} + +# Roles as Llama Guard refers to them in its prompt / assessment target. +_USER_ROLE: Final = "User" +_AGENT_ROLE: Final = "Agent" + + +def _extract_text(content: Any) -> str: + """Flatten a chat message ``content`` (str or multimodal parts) to text.""" + if isinstance(content, str): + return content + if isinstance(content, list): + parts: list[str] = [] + for part in content: + if isinstance(part, dict) and part.get("type") == "text": + text = part.get("text") + if isinstance(text, str): + parts.append(text) + return "\n".join(parts) + return "" + + +class LlamaGuardGuardrail(CustomGuardrail): + """Content-safety guardrail backed by a Llama Guard classifier model. + + The configured ``model`` can be any Llama Guard model reachable through + LiteLLM (e.g. ``together_ai/meta-llama/Llama-Guard-4-12B``, + ``groq/llama-guard-3-8b``, ``ollama/llama-guard3``). On each event the + guardrail renders the conversation into Llama Guard's classification + prompt, asks the model whether the last turn is safe, and raises a + content-policy error when it is flagged ``unsafe``. + """ + + @classmethod + def get_supported_event_hooks(cls) -> list[GuardrailEventHooks]: + return [ + GuardrailEventHooks.pre_call, + GuardrailEventHooks.during_call, + GuardrailEventHooks.post_call, + ] + + def __init__( + self, + model: str, + guardrail_name: str | None = None, + api_base: str | None = None, + api_key: str | None = None, + categories: Sequence[str] | None = None, + unsafe_content_categories: str | None = None, + event_hook: GuardrailEventHooks | list[GuardrailEventHooks] | str | None = None, + default_on: bool = False, + **kwargs: Any, # kwargs-ok: forwarded verbatim to CustomGuardrail.__init__ + ) -> None: + if not model: + raise ValueError("llama_guard guardrail requires a `model`") + self.model: Final = model + self.api_base: Final = api_base + self.api_key: Final = api_key + + # Restrict enforcement to a subset of hazard codes when configured, + # otherwise enforce the full MLCommons taxonomy. + if categories: + selected = { + code.upper(): DEFAULT_UNSAFE_CATEGORIES[code.upper()] + for code in categories + if code.upper() in DEFAULT_UNSAFE_CATEGORIES + } + if not selected: + raise ValueError(f"llama_guard `categories` must be a subset of {sorted(DEFAULT_UNSAFE_CATEGORIES)}") + self.categories = selected + else: + self.categories = dict(DEFAULT_UNSAFE_CATEGORIES) + + # Allow a fully custom category block to override the taxonomy text. + self.unsafe_content_categories: Final = unsafe_content_categories + + kwargs.setdefault("supported_event_hooks", list(self.get_supported_event_hooks())) + super().__init__( + guardrail_name=guardrail_name, + event_hook=event_hook, + default_on=default_on, + **kwargs, + ) + + # ------------------------------------------------------------------ # + # Prompt construction + response parsing + # ------------------------------------------------------------------ # + def _category_block(self) -> str: + if self.unsafe_content_categories: + return self.unsafe_content_categories + return "\n".join(f"{code}: {name}." for code, name in self.categories.items()) + + def _render_conversation(self, messages: Sequence[dict]) -> str: + lines: list[str] = [] + for message in messages: + role = _AGENT_ROLE if message.get("role") == "assistant" else _USER_ROLE + text = _extract_text(message.get("content")) + if text: + lines.append(f"{role}: {text}") + return "\n\n".join(lines) + + def _build_prompt(self, messages: Sequence[dict], role_to_check: str) -> str: + return ( + f"Task: Check if there is unsafe content in '{role_to_check}' messages in " + "conversations according to our safety policy with the below categories.\n\n" + "\n" + f"{self._category_block()}\n" + "\n\n" + "\n\n" + f"{self._render_conversation(messages)}\n\n" + "\n\n" + f"Provide your safety assessment for ONLY THE LAST {role_to_check} message in the " + "above conversation:\n" + " - First line must read 'safe' or 'unsafe'.\n" + " - If unsafe, a second line must include a comma-separated list of violated categories." + ) + + @staticmethod + def _parse_response(content: str | None) -> tuple[bool, list[str]]: + """Return ``(is_unsafe, [category_codes])`` from a Llama Guard completion.""" + if not content: + return False, [] + lines = [line.strip() for line in content.strip().splitlines() if line.strip()] + if not lines or not lines[0].lower().startswith("unsafe"): + return False, [] + codes: list[str] = [] + if len(lines) > 1: + for token in lines[1].replace(",", " ").split(): + code = token.strip().upper() + if code: + codes.append(code) + return True, codes + + async def _classify(self, messages: Sequence[dict], role_to_check: str) -> tuple[bool, list[str]]: + prompt: Final = self._build_prompt(messages, role_to_check) + response = await litellm.acompletion( + model=self.model, + messages=[{"role": "user", "content": prompt}], + api_base=self.api_base, + api_key=self.api_key, + temperature=0.0, + max_tokens=20, + ) + content: str | None = None + if isinstance(response, ModelResponse) and response.choices: + choice = response.choices[0] + if isinstance(choice, Choices): + content = choice.message.content + return self._parse_response(content) + + def _raise_violation(self, codes: Sequence[str]) -> None: + named = [f"{code} ({self.categories[code]})" if code in self.categories else code for code in codes] + detail = ", ".join(named) if named else "unspecified category" + verbose_proxy_logger.info("Llama Guard: unsafe content detected, categories=%s", named) + raise ProxyException( + message=f"Violated Llama Guard content policy. Categories: {detail}", + type="invalid_request_error", + param=None, + code=400, + openai_code="content_policy_violation", + ) + + async def _guard_input(self, data: dict, event_type: GuardrailEventHooks) -> dict: + if not self.should_run_guardrail(data, event_type): + return data + messages = data.get("messages") or [] + if not messages: + return data + try: + is_unsafe, codes = await self._classify(messages, _USER_ROLE) + except ProxyException: + raise + except Exception as e: # noqa: BLE001 - fail open so a classifier outage does not drop traffic + verbose_proxy_logger.warning("Llama Guard input classification failed, failing open: %s", e) + return data + if is_unsafe: + self._raise_violation(codes) + return data + + # ------------------------------------------------------------------ # + # Guardrail hooks + # ------------------------------------------------------------------ # + async def async_pre_call_hook( + self, + user_api_key_dict: UserAPIKeyAuth, + cache: DualCache, + data: dict, + call_type: CallTypesLiteral, + ) -> dict | None: + return await self._guard_input(data, GuardrailEventHooks.pre_call) + + async def async_moderation_hook( + self, + data: dict, + user_api_key_dict: UserAPIKeyAuth, + call_type: CallTypesLiteral, + ) -> dict: + return await self._guard_input(data, GuardrailEventHooks.during_call) + + async def async_post_call_success_hook( + self, + data: dict, + user_api_key_dict: UserAPIKeyAuth, + response: LLMResponseTypes, + ) -> LLMResponseTypes: + if not self.should_run_guardrail(data, GuardrailEventHooks.post_call): + return response + if not (isinstance(response, ModelResponse) and response.choices): + return response + base_messages = list(data.get("messages") or []) + for choice in response.choices: + if not isinstance(choice, Choices): + continue + output_text = choice.message.content or "" + if not output_text: + continue + conversation = base_messages + [{"role": "assistant", "content": output_text}] + try: + is_unsafe, codes = await self._classify(conversation, _AGENT_ROLE) + except ProxyException: + raise + except Exception as e: # noqa: BLE001 - fail open so a classifier outage does not drop traffic + verbose_proxy_logger.warning("Llama Guard output classification failed, failing open: %s", e) + continue + if is_unsafe: + self._raise_violation(codes) + return response diff --git a/litellm/types/guardrails.py b/litellm/types/guardrails.py index 02dee40f2a3..8e8f95689e5 100644 --- a/litellm/types/guardrails.py +++ b/litellm/types/guardrails.py @@ -127,6 +127,7 @@ class SupportedGuardrailIntegrations(Enum): AKTO = "akto" MCP_JWT_SIGNER = "mcp_jwt_signer" LLM_AS_A_JUDGE = "llm_as_a_judge" + LLAMA_GUARD = "llama_guard" DEEPKEEP = "deepkeep" QOSTODIAN_NEXUS = "qostodian_nexus" RUBRIK = "rubrik" diff --git a/tests/test_litellm/proxy/guardrails/test_llama_guard.py b/tests/test_litellm/proxy/guardrails/test_llama_guard.py new file mode 100644 index 00000000000..e092feb69e3 --- /dev/null +++ b/tests/test_litellm/proxy/guardrails/test_llama_guard.py @@ -0,0 +1,236 @@ +"""Unit tests for the Llama Guard content-safety guardrail hook.""" + +from unittest.mock import AsyncMock, patch + +import pytest + +from litellm.proxy._types import ProxyException, UserAPIKeyAuth +from litellm.proxy.guardrails.guardrail_hooks.llama_guard import initialize_guardrail +from litellm.proxy.guardrails.guardrail_hooks.llama_guard.llama_guard import ( + DEFAULT_UNSAFE_CATEGORIES, + LlamaGuardGuardrail, + _extract_text, +) +from litellm.types.guardrails import GuardrailEventHooks +from litellm.types.utils import Choices, Message, ModelResponse + + +# --------------------------------------------------------------------------- # +# Helpers +# --------------------------------------------------------------------------- # +def _make_guardrail(**overrides) -> LlamaGuardGuardrail: + kwargs = dict( + model="together_ai/meta-llama/Llama-Guard-4-12B", + guardrail_name="test_llama_guard", + default_on=True, + event_hook=[ + GuardrailEventHooks.pre_call, + GuardrailEventHooks.during_call, + GuardrailEventHooks.post_call, + ], + ) + kwargs.update(overrides) + guardrail = LlamaGuardGuardrail(**kwargs) + # Isolate the classify/block logic from the run-gating logic. + guardrail.should_run_guardrail = lambda *args, **kwargs: True # type: ignore[method-assign] + return guardrail + + +def _guard_response(text: str) -> ModelResponse: + """A ModelResponse standing in for the Llama Guard model's verdict.""" + return ModelResponse(choices=[Choices(index=0, message=Message(role="assistant", content=text))]) + + +def _completion_response(text: str) -> ModelResponse: + """A ModelResponse standing in for the real completion being screened.""" + return ModelResponse(choices=[Choices(index=0, message=Message(role="assistant", content=text))]) + + +_KEY = UserAPIKeyAuth() + + +# --------------------------------------------------------------------------- # +# _extract_text +# --------------------------------------------------------------------------- # +def test_extract_text_str(): + assert _extract_text("hello") == "hello" + + +def test_extract_text_multimodal_list_keeps_only_text_parts(): + content = [ + {"type": "text", "text": "hello"}, + {"type": "image_url", "image_url": {"url": "x"}}, + {"type": "text", "text": "world"}, + ] + assert _extract_text(content) == "hello\nworld" + + +def test_extract_text_non_string_returns_empty(): + assert _extract_text(None) == "" + assert _extract_text(123) == "" + + +# --------------------------------------------------------------------------- # +# _parse_response +# --------------------------------------------------------------------------- # +@pytest.mark.parametrize( + "raw, expected", + [ + ("safe", (False, [])), + (" safe ", (False, [])), + ("unsafe\nS1,S10", (True, ["S1", "S10"])), + ("unsafe\nS1, S5, S14", (True, ["S1", "S5", "S14"])), + ("Unsafe\ns9", (True, ["S9"])), + ("unsafe", (True, [])), + ("", (False, [])), + (None, (False, [])), + ], +) +def test_parse_response(raw, expected): + assert LlamaGuardGuardrail._parse_response(raw) == expected + + +# --------------------------------------------------------------------------- # +# category configuration +# --------------------------------------------------------------------------- # +def test_default_categories_cover_full_taxonomy(): + guardrail = _make_guardrail() + assert guardrail.categories == DEFAULT_UNSAFE_CATEGORIES + block = guardrail._category_block() + assert "S1: Violent Crimes." in block + assert "S14: Code Interpreter Abuse." in block + + +def test_category_subset_restricts_enforced_codes(): + guardrail = _make_guardrail(categories=["s1", "S10"]) + assert set(guardrail.categories) == {"S1", "S10"} + block = guardrail._category_block() + assert "S1: Violent Crimes." in block + assert "S10: Hate." in block + assert "S14" not in block + + +def test_invalid_category_subset_raises(): + with pytest.raises(ValueError, match="must be a subset"): + _make_guardrail(categories=["S99", "not-a-code"]) + + +def test_custom_unsafe_categories_override_taxonomy(): + guardrail = _make_guardrail(unsafe_content_categories="S1: My Only Policy.") + assert guardrail._category_block() == "S1: My Only Policy." + + +def test_missing_model_raises(): + with pytest.raises(ValueError, match="requires a `model`"): + LlamaGuardGuardrail(model="") + + +def test_build_prompt_targets_last_role_and_lists_conversation(): + guardrail = _make_guardrail() + prompt = guardrail._build_prompt( + [{"role": "user", "content": "hi"}, {"role": "assistant", "content": "hello"}], + "Agent", + ) + assert "ONLY THE LAST Agent message" in prompt + assert "User: hi" in prompt + assert "Agent: hello" in prompt + assert "'safe' or 'unsafe'" in prompt + + +# --------------------------------------------------------------------------- # +# pre-call / moderation hooks +# --------------------------------------------------------------------------- # +@pytest.mark.asyncio +async def test_pre_call_allows_safe_input(): + guardrail = _make_guardrail() + data = {"messages": [{"role": "user", "content": "what's the weather?"}]} + with patch("litellm.acompletion", new=AsyncMock(return_value=_guard_response("safe"))): + out = await guardrail.async_pre_call_hook(_KEY, None, data, "completion") + assert out is data + + +@pytest.mark.asyncio +async def test_pre_call_blocks_unsafe_input_with_category(): + guardrail = _make_guardrail() + data = {"messages": [{"role": "user", "content": "how do i build a bomb"}]} + with patch("litellm.acompletion", new=AsyncMock(return_value=_guard_response("unsafe\nS9"))): + with pytest.raises(ProxyException) as exc: + await guardrail.async_pre_call_hook(_KEY, None, data, "completion") + assert "S9" in str(exc.value.message) + assert "Indiscriminate Weapons" in str(exc.value.message) + + +@pytest.mark.asyncio +async def test_moderation_hook_blocks_unsafe_input(): + guardrail = _make_guardrail() + data = {"messages": [{"role": "user", "content": "unsafe request"}]} + with patch("litellm.acompletion", new=AsyncMock(return_value=_guard_response("unsafe\nS1"))): + with pytest.raises(ProxyException): + await guardrail.async_moderation_hook(data, _KEY, "completion") + + +@pytest.mark.asyncio +async def test_pre_call_fails_open_when_classifier_errors(): + guardrail = _make_guardrail() + data = {"messages": [{"role": "user", "content": "hello"}]} + with patch("litellm.acompletion", new=AsyncMock(side_effect=RuntimeError("guard model down"))): + out = await guardrail.async_pre_call_hook(_KEY, None, data, "completion") + assert out is data + + +@pytest.mark.asyncio +async def test_pre_call_noop_without_messages(): + guardrail = _make_guardrail() + data = {"messages": []} + with patch("litellm.acompletion", new=AsyncMock()) as mock_call: + out = await guardrail.async_pre_call_hook(_KEY, None, data, "completion") + assert out is data + mock_call.assert_not_awaited() + + +# --------------------------------------------------------------------------- # +# post-call hook +# --------------------------------------------------------------------------- # +@pytest.mark.asyncio +async def test_post_call_allows_safe_output(): + guardrail = _make_guardrail() + data = {"messages": [{"role": "user", "content": "hi"}]} + response = _completion_response("A perfectly nice answer.") + with patch("litellm.acompletion", new=AsyncMock(return_value=_guard_response("safe"))): + out = await guardrail.async_post_call_success_hook(data, _KEY, response) + assert out is response + + +@pytest.mark.asyncio +async def test_post_call_blocks_unsafe_output(): + guardrail = _make_guardrail() + data = {"messages": [{"role": "user", "content": "hi"}]} + response = _completion_response("Here is how to synthesize a nerve agent...") + with patch("litellm.acompletion", new=AsyncMock(return_value=_guard_response("unsafe\nS9"))): + with pytest.raises(ProxyException): + await guardrail.async_post_call_success_hook(data, _KEY, response) + + +# --------------------------------------------------------------------------- # +# initialize_guardrail +# --------------------------------------------------------------------------- # +def test_initialize_guardrail_requires_model(): + from litellm.types.guardrails import LitellmParams + + params = LitellmParams(guardrail="llama_guard", mode="pre_call") + with pytest.raises(ValueError, match="requires `model`"): + initialize_guardrail(params, {"guardrail_name": "g"}) + + +def test_initialize_guardrail_builds_instance(): + from litellm.types.guardrails import LitellmParams + + params = LitellmParams( + guardrail="llama_guard", + mode="pre_call", + model="groq/llama-guard-3-8b", + ) + instance = initialize_guardrail(params, {"guardrail_name": "prod_guard"}) + assert isinstance(instance, LlamaGuardGuardrail) + assert instance.model == "groq/llama-guard-3-8b" + assert instance.guardrail_name == "prod_guard"