style: satisfy type-discipline budget (LIT008, LIT009)

BerriAI/litellm CI's type-discipline budget check flagged the
subclass __init__ shim. Fixes:

- Drop the __init__ override entirely — the subclass now inherits
  __init__ from _BaseConductGuard (when the standalone package is
  installed) or from CustomGuardrail (fallback). Removes both the
  banned **kwargs (LIT008) and all four inert # type: ignore markers
  (LIT009 x 4).
- Move the missing-package check into a dedicated
  raise_if_missing_package() helper called by
  initialize_guardrail before construction. Preserves the
  cursor[bot] fix (silent-drop-on-import-failure) without needing
  a custom __init__.
- Fallback branch aliases _BaseConductGuard = CustomGuardrail
  directly, no type-ignore comment needed.
- Test updated to exercise the helper instead of the removed
  __init__ path; new companion test asserts the helper is a no-op
  when the package IS installed.

Local: ruff --select ANN,TID passes clean. ruff format applied.

Same behavioral surface — user-visible error message unchanged.
This commit is contained in:
Conduct AI 2026-09-10 20:28:31 -05:00
parent 2e1f320192
commit ad779be901
No known key found for this signature in database
3 changed files with 63 additions and 48 deletions

View file

@ -2,7 +2,7 @@ from typing import TYPE_CHECKING, Final
from litellm.types.guardrails import SupportedGuardrailIntegrations
from .conduct import ConductGuardrail
from .conduct import ConductGuardrail, raise_if_missing_package
if TYPE_CHECKING:
from litellm.types.guardrails import Guardrail, LitellmParams
@ -37,6 +37,10 @@ def initialize_guardrail(litellm_params: "LitellmParams", guardrail: "Guardrail"
"""
import litellm
# Surface the missing-package error at config load, not silently at
# module import (cursor[bot] finding — see conduct.py header comment).
raise_if_missing_package()
# ``getattr(..., default)`` only fires when the attribute is missing;
# ``LitellmParams`` always defines ``timeout`` and defaults it to
# ``None``, so the default was never applied. Use ``or`` so an

View file

@ -24,14 +24,15 @@ _IMPORT_ERROR_MESSAGE = (
)
# ── Base plugin import — deferred to init-time ─────────────────────────
# Reason: the guardrail-hook auto-discovery loop treats a module-level
# ``raise ImportError`` as "hook unavailable" and silently drops the
# registration. A user who installed LiteLLM but forgot the
# ``conduct-litellm-guard`` dependency would see their config load with
# no guardrail active and no error message (cursor[bot] finding on
# BerriAI/litellm#38143). Import here without raising; surface the
# missing dep at ``__init__`` time when it is actionable.
# ── 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.
# (cursor[bot] finding on BerriAI/litellm#38143.)
try:
from conduct_litellm_guard import ConductGuard as _BaseConductGuard
@ -41,45 +42,36 @@ try:
from conduct_litellm_guard.guardrail import GuardDecision
_IMPORT_ERROR: ImportError | None = None
except ImportError as _e:
_BaseConductGuard = None # type: ignore[assignment,misc]
ConductGuardrailBlocked = None # type: ignore[assignment,misc]
GuardDecision = None # type: ignore[assignment,misc]
_IMPORT_ERROR = _e
except ImportError as _import_err:
_BaseConductGuard = CustomGuardrail
ConductGuardrailBlocked = None
GuardDecision = None
_IMPORT_ERROR = _import_err
# When the base package isn't installed we still need a real class so
# LiteLLM's registry lookup succeeds; the friendly error surfaces on
# construction.
_ParentClass = _BaseConductGuard if _BaseConductGuard is not None else CustomGuardrail
class ConductGuardrail(_ParentClass): # type: ignore[valid-type,misc]
class ConductGuardrail(_BaseConductGuard):
"""LiteLLM adapter over ``conduct_litellm_guard.ConductGuard``.
Subclass exists so we can:
- Advertise supported event hooks honestly to LiteLLM (see
``get_supported_event_hooks``).
- Raise a friendly error at construction time when the base package
isn't installed (rather than at module import — see comment
above).
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
# 0.3.x once the underlying Conduct response gate is wired through
# ``guard_check_response`` (tracked in the plugin repo). Advertising
# only pre_call today prevents silent bypass of ``during_call``
# 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,)
def __init__(self, *args: object, **kwargs: object) -> None:
if _IMPORT_ERROR is not None or _BaseConductGuard is None:
raise ImportError(_IMPORT_ERROR_MESSAGE) from _IMPORT_ERROR
super().__init__(*args, **kwargs)
@classmethod
def get_supported_event_hooks(cls) -> list[GuardrailEventHooks]:
"""LiteLLM calls this during config validation to reject
@ -88,4 +80,19 @@ class ConductGuardrail(_ParentClass): # type: ignore[valid-type,misc]
return list(cls.SUPPORTED_EVENT_HOOKS)
__all__ = ["ConductGuardrail", "ConductGuardrailBlocked", "GuardDecision"]
def raise_if_missing_package() -> None:
"""Called by ``initialize_guardrail`` before constructing the class.
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
__all__ = [
"ConductGuardrail",
"ConductGuardrailBlocked",
"GuardDecision",
"raise_if_missing_package",
]

View file

@ -182,24 +182,28 @@ def test_initialize_applies_timeout_default_when_field_is_none(
assert captured["timeout"] == 8.0
def test_missing_standalone_package_raises_at_construction() -> None:
def test_missing_standalone_package_raises_at_initialize() -> None:
"""Regression for cursor[bot] finding — the previous shim raised
``ImportError`` at module load, which the guardrail-hook auto-loader
treats as "hook unavailable" and silently drops. The subclass now
imports lazily and raises at ``__init__`` time when actionable."""
treats as "hook unavailable" and silently drops. The check is now
deferred to :func:`raise_if_missing_package` which
``initialize_guardrail`` calls at config-load time when actionable."""
from litellm.proxy.guardrails.guardrail_hooks.conduct import conduct as _mod
original_base = _mod._BaseConductGuard
original_error = _mod._IMPORT_ERROR
try:
_mod._BaseConductGuard = None # type: ignore[assignment]
_mod._IMPORT_ERROR = ImportError("simulated missing package")
# Class is still importable — no module-load side effect.
from litellm.proxy.guardrails.guardrail_hooks.conduct import ConductGuardrail
with pytest.raises(ImportError, match="pip install"):
ConductGuardrail()
_mod.raise_if_missing_package()
finally:
_mod._BaseConductGuard = original_base # type: ignore[assignment]
_mod._IMPORT_ERROR = original_error
def test_raise_if_missing_package_is_noop_when_present() -> None:
"""The check should be silent when ``conduct-litellm-guard`` is
importable (the normal case for anyone who ``pip install``ed it)."""
from litellm.proxy.guardrails.guardrail_hooks.conduct.conduct import (
raise_if_missing_package,
)
raise_if_missing_package() # must not raise