diff --git a/litellm/proxy/guardrails/guardrail_hooks/conduct/conduct.py b/litellm/proxy/guardrails/guardrail_hooks/conduct/conduct.py index 4aab252e565..b4747c14e3c 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/conduct/conduct.py +++ b/litellm/proxy/guardrails/guardrail_hooks/conduct/conduct.py @@ -1,83 +1,46 @@ """Conduct Guard as a LiteLLM guardrail. -Thin adapter over the ``conduct-litellm-guard`` PyPI package. The -adapter, response-envelope parser, session-ID chain, fail-mode logic, -and the ``guard_check_prompt`` wire client all live in that package — -this file only wires the base runtime into LiteLLM's ``CustomGuardrail`` -contract. +Pure alias for the ``conduct-litellm-guard`` PyPI package. The +``ConductGuard`` class ships with ``SUPPORTED_EVENT_HOOKS`` + +``get_supported_event_hooks`` since plugin 0.2.3, so this file no +longer needs a subclass wrapper — keeps LiteLLM's type-discipline / +basedpyright / test-quality budget gates satisfied. -Install: ``pip install "conduct-litellm-guard>=0.2.2"`` +Install: ``pip install "conduct-litellm-guard>=0.2.3"`` Source: https://github.com/sseshachala/conductai/tree/main/packages/conduct-litellm-guard Docs: https://conductai.ai/guard """ from __future__ import annotations -from typing import ClassVar - -from litellm.integrations.custom_guardrail import CustomGuardrail -from litellm.types.guardrails import GuardrailEventHooks - -_IMPORT_ERROR_MESSAGE = ( +_import_error_message = ( "conduct-litellm-guard is required for the Conduct guardrail. " - 'Install it with: pip install "conduct-litellm-guard>=0.2.2"' + 'Install it with: pip install "conduct-litellm-guard>=0.2.3"' ) -# ── Base plugin import — deferred to init-time via raise_if_missing_package ── -# Raising ImportError at module load caused the guardrail-hook auto-loader to -# treat a missing ``conduct-litellm-guard`` as "hook unavailable" and silently -# drop the registration. Users saw configs load with no guardrail active and -# no error message. Instead we fall back to ``CustomGuardrail`` at module load -# so the class hierarchy stays intact; ``initialize_guardrail`` (in -# ``__init__.py``) calls :func:`raise_if_missing_package` before construction -# so the friendly error surfaces when actionable. +# ── Base plugin import ───────────────────────────────────────────────── +# Raising ImportError at module load caused the guardrail-hook auto-loader +# to treat a missing ``conduct-litellm-guard`` as "hook unavailable" and +# silently drop the registration. Users saw configs load with no guardrail +# active and no error message. Instead we surface the friendly error at +# config-load time from :func:`raise_if_missing_package` — called by +# ``initialize_guardrail`` before construction. # (cursor[bot] finding on BerriAI/litellm#38143.) try: - from conduct_litellm_guard import ConductGuard as _BaseConductGuard + from conduct_litellm_guard import ConductGuard as ConductGuardrail from conduct_litellm_guard.guardrail import ( ConductGuardBlocked as ConductGuardrailBlocked, ) from conduct_litellm_guard.guardrail import GuardDecision - _IMPORT_ERROR: ImportError | None = None + _import_error: ImportError | None = None except ImportError as _import_err: - _BaseConductGuard = CustomGuardrail + ConductGuardrail = None ConductGuardrailBlocked = None GuardDecision = None - _IMPORT_ERROR = _import_err - - -class ConductGuardrail(_BaseConductGuard): - """LiteLLM adapter over ``conduct_litellm_guard.ConductGuard``. - - Inherits its ``__init__`` from the base runtime when the standalone - package is installed; otherwise inherits from ``CustomGuardrail`` - and ``initialize_guardrail`` short-circuits with a friendly error - before this class is ever constructed. - - Only two additions on this side: - - ``SUPPORTED_EVENT_HOOKS`` / ``get_supported_event_hooks`` so - LiteLLM validates configs against modes we actually implement. - """ - - # Advertised event hooks. The plugin currently runs at pre_call - # (input rail) — a policy block short-circuits before the model - # sees the prompt, which is the semantic LiteLLM users expect for - # a "guardrail". ``during_call`` / ``post_call`` support lands - # with plugin 0.3.x once the underlying Conduct response gate is - # wired through ``guard_check_response``. Advertising only - # pre_call today prevents silent bypass of ``during_call`` - # configurations — see veria-ai finding on BerriAI/litellm#38143. - SUPPORTED_EVENT_HOOKS: ClassVar[tuple[GuardrailEventHooks, ...]] = (GuardrailEventHooks.pre_call,) - - @classmethod - def get_supported_event_hooks(cls) -> list[GuardrailEventHooks]: - """LiteLLM calls this during config validation to reject - unsupported ``mode:`` values (e.g. ``during_call`` while only - pre_call is implemented).""" - return list(cls.SUPPORTED_EVENT_HOOKS) + _import_error = _import_err def raise_if_missing_package() -> None: @@ -86,8 +49,8 @@ def raise_if_missing_package() -> None: Surfaces the friendly ``pip install`` error at the actionable moment (config load) rather than silently dropping the hook at module load. """ - if _IMPORT_ERROR is not None: - raise ImportError(_IMPORT_ERROR_MESSAGE) from _IMPORT_ERROR + if _import_error is not None: + raise ImportError(_import_error_message) from _import_error __all__ = [ diff --git a/tests/test_litellm/proxy/guardrails/test_conduct_guardrail.py b/tests/test_litellm/proxy/guardrails/test_conduct_guardrail.py index ce7c888ca6e..9531d403555 100644 --- a/tests/test_litellm/proxy/guardrails/test_conduct_guardrail.py +++ b/tests/test_litellm/proxy/guardrails/test_conduct_guardrail.py @@ -13,7 +13,6 @@ here we only verify the LiteLLM-tree wiring: from __future__ import annotations import importlib -import sys from types import SimpleNamespace from unittest.mock import MagicMock @@ -59,15 +58,14 @@ def test_registries_populated() -> None: def test_only_pre_call_event_hook_advertised() -> None: """Regression for veria-ai finding on #38143 — - ``during_call`` mode was silently accepted but never evaluated - because ``async_moderation_hook`` was not overridden. We only - advertise pre_call today so LiteLLM validates configs against - supported hooks and rejects unsupported modes.""" + ``during_call`` mode was silently accepted but never evaluated. + Since plugin 0.2.3 the supported-hooks contract lives on + ``ConductGuard`` in the plugin package itself; the LiteLLM shim + is a pure alias, so we verify against the alias.""" from litellm.proxy.guardrails.guardrail_hooks.conduct import ConductGuardrail - from litellm.types.guardrails import GuardrailEventHooks hooks = ConductGuardrail.get_supported_event_hooks() - assert hooks == [GuardrailEventHooks.pre_call] + assert hooks == ["pre_call"] def test_initialize_guardrail_returns_wired_callback( @@ -190,13 +188,13 @@ def test_missing_standalone_package_raises_at_initialize() -> None: ``initialize_guardrail`` calls at config-load time when actionable.""" from litellm.proxy.guardrails.guardrail_hooks.conduct import conduct as _mod - original_error = _mod._IMPORT_ERROR + original_error = _mod._import_error try: - _mod._IMPORT_ERROR = ImportError("simulated missing package") + _mod._import_error = ImportError("simulated missing package") with pytest.raises(ImportError, match="pip install"): _mod.raise_if_missing_package() finally: - _mod._IMPORT_ERROR = original_error + _mod._import_error = original_error def test_raise_if_missing_package_is_noop_when_present() -> None: