refactor: shim becomes a pure alias, hooks now on plugin's ConductGuard

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
This commit is contained in:
Conduct AI 2026-09-10 21:15:30 -05:00
parent 7f796fce9d
commit 46eac90614
No known key found for this signature in database
2 changed files with 29 additions and 68 deletions

View file

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

View file

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