feat(guardrails): add Conduct config model and Admin UI garden entry

Expose ConductGuardrailConfigModel through get_config_model() so
/guardrails/ui/provider_specific_params returns the api_key, api_base,
workspace_id, tool_name, timeout and unreachable_fallback fields, and
add the Conduct Guard partner card, preset and logo to the guardrail
garden so the integration can be created from the Admin UI

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
yucheng 2026-09-12 00:15:16 +00:00
parent 7ffde11054
commit 48c2fe1879
8 changed files with 106 additions and 1 deletions

View file

@ -15,6 +15,7 @@ from pydantic import BaseModel, ConfigDict
from litellm.integrations.custom_guardrail import CustomGuardrail, log_guardrail_information
from litellm.types.llms.openai import ChatCompletionUserMessage
from litellm.types.proxy.guardrails.guardrail_hooks.conduct import ConductGuardrailConfigModel
if TYPE_CHECKING:
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
@ -104,9 +105,17 @@ except ImportError as import_error:
def __init__(self, **kwargs: object) -> None: # kwargs-ok: mirrors the plugin constructor, only raises
raise ImportError(MISSING_PACKAGE_MESSAGE) from _import_error
@staticmethod
def get_config_model() -> type[ConductGuardrailConfigModel]:
return ConductGuardrailConfigModel
else:
class ConductGuardrail(ConductGuard): # pyright: ignore[reportUntypedBaseClass] # optional dep, absent at type-check
@staticmethod
def get_config_model() -> type[ConductGuardrailConfigModel]:
return ConductGuardrailConfigModel
@log_guardrail_information
async def apply_guardrail(
self,

View file

@ -0,0 +1,42 @@
from __future__ import annotations
from typing import Literal
from pydantic import BaseModel, Field
from .base import GuardrailConfigModel
class ConductGuardrailConfigModelOptionalParams(BaseModel):
workspace_id: str | None = Field(
default=None,
description="Conduct workspace id, sent as the X-Workspace-Id header. Env: CONDUCT_WORKSPACE_ID.",
)
tool_name: str | None = Field(
default="llm_call",
description="Conduct tool name the prompt is evaluated under. Match the tool your rules target.",
)
timeout: float | None = Field(
default=8.0,
gt=0.0,
description="Timeout in seconds for the Conduct check.",
)
unreachable_fallback: Literal["fail_open", "fail_closed"] | None = Field(
default="fail_closed",
description="Behavior when Conduct is unreachable, times out, or rejects the token.",
)
class ConductGuardrailConfigModel(GuardrailConfigModel[ConductGuardrailConfigModelOptionalParams]):
api_key: str = Field(
min_length=1,
description="Conduct agent token. Env: CONDUCT_AGENT_TOKEN.",
)
api_base: str | None = Field(
default="https://api.conductai.ai",
description="Conduct API base URL. The MCP endpoint is derived as <api_base>/mcp.",
)
@staticmethod
def ui_friendly_name() -> str:
return "Conduct Guard"

View file

@ -13,6 +13,7 @@ from fastapi import HTTPException
import litellm
from litellm.integrations.custom_guardrail import CustomGuardrail
from litellm.proxy.guardrails.guardrail_endpoints import get_guardrail_ui_settings, get_provider_specific_params
from litellm.proxy.guardrails.guardrail_hooks.conduct import (
DEFAULT_TIMEOUT_SECONDS,
ConductGuardrail,
@ -23,10 +24,13 @@ from litellm.proxy.guardrails.guardrail_hooks.conduct.conduct import (
record_decision,
request_payload,
)
from litellm.proxy.guardrails.guardrail_endpoints import get_guardrail_ui_settings
from litellm.proxy.guardrails.guardrail_registry import InMemoryGuardrailHandler
from litellm.types.guardrails import Guardrail, GuardrailEventHooks, LitellmParams
from litellm.types.llms.openai import ChatCompletionAssistantMessage
from litellm.types.proxy.guardrails.guardrail_hooks.conduct import (
ConductGuardrailConfigModel,
ConductGuardrailConfigModelOptionalParams,
)
from litellm.types.utils import GenericGuardrailAPIInputs
PACKAGE_INSTALLED: Final = importlib.util.find_spec("conduct_litellm_guard") is not None
@ -157,6 +161,36 @@ def test_defaults_when_optional_config_is_omitted() -> None:
assert callback.tool_name == "llm_call"
def test_ui_form_defaults_match_what_the_initializer_forwards() -> None:
optional: Final = ConductGuardrailConfigModelOptionalParams()
model: Final = ConductGuardrailConfigModel(api_key="cond_agt_test")
callback: Final = _init(
_params(**{**model.model_dump(exclude={"api_key", "optional_params"}), **optional.model_dump()})
)
assert callback.api_url == model.api_base
assert callback.fail_mode == optional.unreachable_fallback
assert callback.timeout == optional.timeout
assert callback.workspace_id == optional.workspace_id
assert callback.tool_name == optional.tool_name
@pytest.mark.asyncio
async def test_ui_offers_conduct_fields_without_the_package() -> None:
assert ConductGuardrail.get_config_model() is ConductGuardrailConfigModel
fields: Final = (await get_provider_specific_params())["conduct"]
assert fields["ui_friendly_name"] == "Conduct Guard"
assert fields["api_key"]["required"] is True
assert fields["api_base"]["default_value"] == "https://api.conductai.ai"
optional: Final = fields["optional_params"]["fields"]
assert set(optional) == {"workspace_id", "tool_name", "timeout", "unreachable_fallback"}
assert optional["unreachable_fallback"]["type"] == "select"
assert optional["unreachable_fallback"]["options"] == ["fail_open", "fail_closed"]
assert optional["timeout"]["default_value"] == DEFAULT_TIMEOUT_SECONDS
@pytest.mark.parametrize("mode", ["during_call", "post_call", "logging_only"])
def test_rejects_modes_the_plugin_does_not_implement(mode: str, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("LITELLM_STRICT_GUARDRAIL_MODES", raising=False)

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -318,4 +318,10 @@ export const GUARDRAIL_PRESETS: Record<string, GuardrailPreset> = {
mode: "pre_call",
defaultOn: false,
},
conduct: {
provider: "Conduct",
guardrailNameSuggestion: "Conduct Guard",
mode: "pre_call",
defaultOn: false,
},
};

View file

@ -28,6 +28,7 @@ const EXPECTED_PARTNER_LOGO_FILES: Record<string, string> = {
repelloai: "repelloai.png",
straiker: "straiker.svg",
alice: "alice.svg",
conduct: "conduct.png",
};
describe("guardrail_garden_data logos", () => {

View file

@ -474,6 +474,16 @@ export const PARTNER_GUARDRAIL_CARDS: GuardrailCardInfo[] = [
tags: ["Content Moderation", "Prompt Injection", "PII", "Policy"],
providerKey: "Alice",
},
{
id: "conduct",
name: "Conduct Guard",
description:
"Conduct Guard evaluates prompts against workspace rules before the model call: prompt injection, PII, and custom policies, with block, warning, and approval verdicts.",
category: "partner",
logo: guardrailLogoMap["Conduct Guard"],
tags: ["Security", "Prompt Injection", "PII", "Policy"],
providerKey: "Conduct",
},
];
export const ALL_CARDS = [...LITELLM_CONTENT_FILTER_CARDS, ...PARTNER_GUARDRAIL_CARDS];

View file

@ -1,6 +1,7 @@
import aimSecurityLogo from "../../../../../public/assets/logos/aim_security.jpeg";
import aktoLogo from "../../../../../public/assets/logos/akto.svg";
import aliceLogo from "../../../../../public/assets/logos/alice.svg";
import conductLogo from "../../../../../public/assets/logos/conduct.png";
import aporiaLogo from "../../../../../public/assets/logos/aporia.png";
import bedrockLogo from "../../../../../public/assets/logos/bedrock.svg";
import catoNetworksLogo from "../../../../../public/assets/logos/cato_networks.svg";
@ -85,6 +86,7 @@ export const guardrail_provider_map: Record<string, string> = {
QostodianNexus: "qostodian_nexus",
Repelloai: "repelloai",
Alice: "alice",
Conduct: "conduct",
};
// Function to populate provider map from API response - updates the original map
@ -208,6 +210,7 @@ export const guardrailLogoMap = {
"RepelloAI Argus": repelloAiLogo.src,
Straiker: straikerLogo.src,
Alice: aliceLogo.src,
"Conduct Guard": conductLogo.src,
} satisfies Record<string, string>;
export const getGuardrailLogo = (displayName: string): string | undefined =>