From ad779be9010aa3d7b2c686cd78b051863cd2ca56 Mon Sep 17 00:00:00 2001 From: Conduct AI Date: Thu, 10 Sep 2026 20:28:31 -0500 Subject: [PATCH] style: satisfy type-discipline budget (LIT008, LIT009) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../guardrail_hooks/conduct/__init__.py | 6 +- .../guardrail_hooks/conduct/conduct.py | 79 ++++++++++--------- .../guardrails/test_conduct_guardrail.py | 26 +++--- 3 files changed, 63 insertions(+), 48 deletions(-) diff --git a/litellm/proxy/guardrails/guardrail_hooks/conduct/__init__.py b/litellm/proxy/guardrails/guardrail_hooks/conduct/__init__.py index 2a056ab5b6c..383400365e4 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/conduct/__init__.py +++ b/litellm/proxy/guardrails/guardrail_hooks/conduct/__init__.py @@ -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 diff --git a/litellm/proxy/guardrails/guardrail_hooks/conduct/conduct.py b/litellm/proxy/guardrails/guardrail_hooks/conduct/conduct.py index a3a872c3035..4aab252e565 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/conduct/conduct.py +++ b/litellm/proxy/guardrails/guardrail_hooks/conduct/conduct.py @@ -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", +] diff --git a/tests/test_litellm/proxy/guardrails/test_conduct_guardrail.py b/tests/test_litellm/proxy/guardrails/test_conduct_guardrail.py index 3796a380757..11338772db9 100644 --- a/tests/test_litellm/proxy/guardrails/test_conduct_guardrail.py +++ b/tests/test_litellm/proxy/guardrails/test_conduct_guardrail.py @@ -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