This commit is contained in:
Sudhi Seshachala 2026-09-01 06:46:14 -04:00 • committed by GitHub
commit 97b676c955
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 333 additions and 0 deletions

View file

@ -0,0 +1,43 @@
from typing import TYPE_CHECKING, Final
from litellm.types.guardrails import SupportedGuardrailIntegrations
from .conduct import ConductGuardrail
if TYPE_CHECKING:
from litellm.types.guardrails import Guardrail, LitellmParams
def initialize_guardrail(litellm_params: "LitellmParams", guardrail: "Guardrail"):
"""Initialize the Conduct guardrail from LiteLLM's config block.
Maps LiteLLM's idiomatic ``api_base`` / ``api_key`` to Conduct's
``api_url`` / ``agent_token`` constructor arguments. All other
settings pass through unchanged.
"""
import litellm
_conduct_callback: Final = ConductGuardrail(
api_url=getattr(litellm_params, "api_base", None),
agent_token=getattr(litellm_params, "api_key", None),
workspace_id=getattr(litellm_params, "workspace_id", None),
fail_mode=getattr(litellm_params, "fail_mode", "fail_closed"),
tool_name=getattr(litellm_params, "tool_name", "llm_call"),
timeout=getattr(litellm_params, "timeout", 8.0),
guardrail_name=guardrail.get("guardrail_name", ""),
event_hook=litellm_params.mode,
default_on=litellm_params.default_on,
)
litellm.logging_callback_manager.add_litellm_callback(_conduct_callback)
return _conduct_callback
guardrail_initializer_registry: Final = {
SupportedGuardrailIntegrations.CONDUCT.value: initialize_guardrail,
}
guardrail_class_registry: Final = {
SupportedGuardrailIntegrations.CONDUCT.value: ConductGuardrail,
}

View file

@ -0,0 +1,26 @@
"""Conduct Guard as a LiteLLM guardrail.
Thin re-export. The adapter, response-envelope parser, session-ID chain,
and fail-mode logic all live in the `conduct-litellm-guard` PyPI package,
which is where issues, versioning, and standalone-user support live.
Install: `pip install conduct-litellm-guard`
Source: https://github.com/sseshachala/conductai/tree/main/packages/conduct-litellm-guard
Docs: https://conductai.ai/guard
"""
from __future__ import annotations
try:
from conduct_litellm_guard import ConductGuard as ConductGuardrail
from conduct_litellm_guard.guardrail import (
ConductGuardBlocked as ConductGuardrailBlocked,
)
from conduct_litellm_guard.guardrail import GuardDecision
except ImportError as _e:
raise ImportError(
"conduct-litellm-guard is required for the Conduct guardrail. Install with: pip install conduct-litellm-guard"
) from _e
__all__ = ["ConductGuardrail", "ConductGuardrailBlocked", "GuardDecision"]

View file

@ -136,6 +136,7 @@ class SupportedGuardrailIntegrations(Enum):
HEADROOM = "headroom"
COMPRESR = "compresr"
STRAIKER = "straiker"
CONDUCT = "conduct"
class Role(Enum):

View file

@ -0,0 +1,263 @@
"""Wiring tests for the Conduct guardrail integration.
The real adapter, response-envelope parser, session-ID chain, and
fail-mode logic live in the ``conduct-litellm-guard`` PyPI package and
are tested there. This file only verifies that the LiteLLM-tree
wrapper wires the enum, registries, and initializer correctly.
To keep coverage useful on BerriAI's CI (where the standalone package
is not installed by default), we mock ``conduct_litellm_guard`` into
``sys.modules`` before importing the wrapper. This lets every test run
regardless of whether the real PyPI package is present in the test
environment.
"""
from __future__ import annotations
import importlib
import sys
import types
from types import SimpleNamespace
from typing import Any
from unittest.mock import MagicMock
import pytest
# ---------------------------------------------------------------------------
# Fake standalone package
# ---------------------------------------------------------------------------
#
# The wrapper in ``litellm/proxy/guardrails/guardrail_hooks/conduct/conduct.py``
# imports three symbols:
#
# from conduct_litellm_guard import ConductGuard
# from conduct_litellm_guard.guardrail import ConductGuardBlocked, GuardDecision
#
# We stand up a fake package tree in ``sys.modules`` that mirrors that shape
# so imports succeed without the real PyPI package being installed.
def _install_fake_conduct_package() -> type:
"""Register a fake ``conduct_litellm_guard`` package tree in sys.modules.
Returns the fake ``ConductGuard`` class so tests can assert against it.
"""
from litellm.integrations.custom_guardrail import CustomGuardrail
class _FakeConductGuard(CustomGuardrail):
def __init__(self, **kwargs: Any) -> None:
self.init_kwargs = kwargs
super().__init__(guardrail_name=kwargs.get("guardrail_name", ""))
class _FakeConductGuardBlocked(Exception):
pass
class _FakeGuardDecision:
ALLOW = "allow"
BLOCK = "block"
root = types.ModuleType("conduct_litellm_guard")
root.ConductGuard = _FakeConductGuard # type: ignore[attr-defined]
guardrail_mod = types.ModuleType("conduct_litellm_guard.guardrail")
guardrail_mod.ConductGuardBlocked = _FakeConductGuardBlocked # type: ignore[attr-defined]
guardrail_mod.GuardDecision = _FakeGuardDecision # type: ignore[attr-defined]
sys.modules["conduct_litellm_guard"] = root
sys.modules["conduct_litellm_guard.guardrail"] = guardrail_mod
return _FakeConductGuard
def _forget_wrapper_modules() -> None:
"""Drop cached wrapper imports so subsequent imports re-run the module body."""
for name in list(sys.modules):
if name.startswith("litellm.proxy.guardrails.guardrail_hooks.conduct"):
del sys.modules[name]
@pytest.fixture
def fake_conduct(monkeypatch: pytest.MonkeyPatch) -> type:
"""Install the fake package and reset wrapper imports for each test."""
_forget_wrapper_modules()
fake_cls = _install_fake_conduct_package()
yield fake_cls
_forget_wrapper_modules()
sys.modules.pop("conduct_litellm_guard", None)
sys.modules.pop("conduct_litellm_guard.guardrail", None)
# ---------------------------------------------------------------------------
# Wiring smoke tests
# ---------------------------------------------------------------------------
def test_wrapper_module_imports(fake_conduct: type) -> None:
"""The wrapper module imports cleanly against the fake package."""
module = importlib.import_module("litellm.proxy.guardrails.guardrail_hooks.conduct")
assert module.ConductGuardrail is fake_conduct
def test_wrapper_exports_expected_symbols(fake_conduct: type) -> None:
"""Public re-exports match the documented ``__all__``."""
module = importlib.import_module(
"litellm.proxy.guardrails.guardrail_hooks.conduct.conduct"
)
assert set(module.__all__) == {
"ConductGuardrail",
"ConductGuardrailBlocked",
"GuardDecision",
}
assert module.ConductGuardrail is fake_conduct
assert issubclass(module.ConductGuardrailBlocked, Exception)
def test_class_is_custom_guardrail_subclass(fake_conduct: type) -> None:
from litellm.integrations.custom_guardrail import CustomGuardrail
from litellm.proxy.guardrails.guardrail_hooks.conduct import ConductGuardrail
assert issubclass(ConductGuardrail, CustomGuardrail)
def test_enum_value_registered() -> None:
"""Enum entry exists regardless of standalone package availability."""
from litellm.types.guardrails import SupportedGuardrailIntegrations
assert SupportedGuardrailIntegrations.CONDUCT.value == "conduct"
def test_registries_populated(fake_conduct: type) -> None:
from litellm.proxy.guardrails.guardrail_hooks.conduct import (
guardrail_class_registry,
guardrail_initializer_registry,
)
assert guardrail_class_registry["conduct"] is fake_conduct
assert callable(guardrail_initializer_registry["conduct"])
# ---------------------------------------------------------------------------
# initialize_guardrail — cover happy path and populated-params path
# ---------------------------------------------------------------------------
def test_initialize_guardrail_defaults(
fake_conduct: type,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""When LiteLLM params are minimal, defaults are passed to ConductGuard."""
added_callbacks: list[object] = []
fake_manager = SimpleNamespace(
add_litellm_callback=lambda cb: added_callbacks.append(cb),
)
import litellm
monkeypatch.setattr(litellm, "logging_callback_manager", fake_manager)
from litellm.proxy.guardrails.guardrail_hooks.conduct import (
initialize_guardrail,
)
litellm_params = SimpleNamespace(
api_base=None,
api_key=None,
mode="pre_call",
default_on=True,
)
guardrail = MagicMock()
guardrail.get.return_value = "conduct-guard"
callback = initialize_guardrail(litellm_params, guardrail)
assert isinstance(callback, fake_conduct)
assert added_callbacks == [callback]
kwargs = callback.init_kwargs
assert kwargs["api_url"] is None
assert kwargs["agent_token"] is None
assert kwargs["workspace_id"] is None
assert kwargs["fail_mode"] == "fail_closed"
assert kwargs["tool_name"] == "llm_call"
assert kwargs["timeout"] == 8.0
assert kwargs["event_hook"] == "pre_call"
assert kwargs["default_on"] is True
def test_initialize_guardrail_populated_params(
fake_conduct: type,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Non-default LiteLLM params flow through to ConductGuard constructor."""
added_callbacks: list[object] = []
fake_manager = SimpleNamespace(
add_litellm_callback=lambda cb: added_callbacks.append(cb),
)
import litellm
monkeypatch.setattr(litellm, "logging_callback_manager", fake_manager)
from litellm.proxy.guardrails.guardrail_hooks.conduct import (
initialize_guardrail,
)
litellm_params = SimpleNamespace(
api_base="https://api.conductai.ai/guard",
api_key="cond_agt_test_placeholder",
workspace_id="ws_123",
fail_mode="fail_open",
tool_name="chat_completion",
timeout=15.0,
mode="post_call",
default_on=False,
)
guardrail = MagicMock()
guardrail.get.return_value = "conduct-prod"
callback = initialize_guardrail(litellm_params, guardrail)
assert isinstance(callback, fake_conduct)
kwargs = callback.init_kwargs
assert kwargs["api_url"] == "https://api.conductai.ai/guard"
assert kwargs["agent_token"] == "cond_agt_test_placeholder"
assert kwargs["workspace_id"] == "ws_123"
assert kwargs["fail_mode"] == "fail_open"
assert kwargs["tool_name"] == "chat_completion"
assert kwargs["timeout"] == 15.0
assert kwargs["guardrail_name"] == "conduct-prod"
assert kwargs["event_hook"] == "post_call"
assert kwargs["default_on"] is False
# ---------------------------------------------------------------------------
# Missing-package branch
# ---------------------------------------------------------------------------
def test_missing_standalone_package_raises_helpful_error(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""The wrapper points users at ``pip install`` when the standalone
``conduct-litellm-guard`` package is not installed."""
_forget_wrapper_modules()
sys.modules.pop("conduct_litellm_guard", None)
sys.modules.pop("conduct_litellm_guard.guardrail", None)
real_import = (
__builtins__["__import__"] # type: ignore[index]
if isinstance(__builtins__, dict)
else __builtins__.__import__
)
def _fake_import(name: str, *args: object, **kwargs: object) -> object:
if name.startswith("conduct_litellm_guard"):
raise ImportError("simulated missing package")
return real_import(name, *args, **kwargs)
monkeypatch.setattr("builtins.__import__", _fake_import)
with pytest.raises(ImportError, match="pip install conduct-litellm-guard"):
importlib.import_module(
"litellm.proxy.guardrails.guardrail_hooks.conduct.conduct"
)
_forget_wrapper_modules()