From 46eac90614b9664b063c9dbaad19516d64fe48e7 Mon Sep 17 00:00:00 2001 From: Conduct AI Date: Thu, 10 Sep 2026 21:15:30 -0500 Subject: [PATCH] refactor: shim becomes a pure alias, hooks now on plugin's ConductGuard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plugin conduct-litellm-guard 0.2.3 ships SUPPORTED_EVENT_HOOKS + get_supported_event_hooks on ConductGuard directly. The upstream shim's subclass wrapper is now redundant — dropping it clears every strict-rule budget gate (ruff-strict / test-quality / type-discipline / basedpyright) in one pass. Changes: - conduct.py: subclass removed; ConductGuardrail is now an alias for the plugin's ConductGuard (no dynamic base class, no reassignment, no # type: ignore). raise_if_missing_package helper unchanged. - test file: _IMPORT_ERROR → _import_error rename to satisfy reportConstantRedefinition (basedpyright treats SCREAMING_CASE as constant). Also drops unused sys import. - Pin bumped to conduct-litellm-guard>=0.2.3 in the module docstring. Verified all four LiteLLM gate scripts locally against upstream/litellm_internal_staging: ruff_strict_gate OK test_quality_gate OK type_discipline_gate OK type_check_gate OK --- .../guardrail_hooks/conduct/conduct.py | 79 +++++-------------- .../guardrails/test_conduct_guardrail.py | 18 ++--- 2 files changed, 29 insertions(+), 68 deletions(-) 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: