Merge pull request #37493 from BerriAI/devin_ai_lit5815_guardrail_tag_mode_ui

fix(ui): render tag-based guardrail mode instead of crashing the guardrails page
This commit is contained in:
ryan-crabbe-berri 2026-08-25 13:24:19 -07:00 • committed by GitHub
commit e5adf7d926
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 114 additions and 12 deletions

View file

@ -6,13 +6,15 @@ import { toast } from "@/lib/toast";
import { Card, CardContent } from "@/components/ui/card";
import { InputGroup, InputGroupAddon, InputGroupInput } from "@/components/ui/input-group";
import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner";
import { GuardrailMode } from "@/components/guardrails/types";
import { formatGuardrailMode } from "./guardrail_info_helpers";
interface GuardrailItem {
guardrail_id?: string;
guardrail_name: string | null;
litellm_params: {
guardrail: string;
mode: string;
mode: GuardrailMode;
default_on: boolean;
};
guardrail_info: Record<string, any> | null;
@ -171,7 +173,9 @@ const GuardrailTestPlayground: React.FC<GuardrailTestPlaygroundProps> = ({
</div>
<div>
<span className="font-medium">Mode: </span>
<span className="text-muted-foreground">{guardrail.litellm_params.mode}</span>
<span className="text-muted-foreground">
{formatGuardrailMode(guardrail.litellm_params.mode)}
</span>
</div>
</div>
</li>

View file

@ -18,7 +18,7 @@ import GuardrailTestPlayground from "./GuardrailTestPlayground";
import { toast } from "@/lib/toast";
import { Guardrail } from "@/components/guardrails/types";
import DeleteResourceModal from "@/components/common_components/DeleteResourceModal";
import { getGuardrailLogoAndName } from "./guardrail_info_helpers";
import { formatGuardrailMode, getGuardrailLogoAndName } from "./guardrail_info_helpers";
import { CustomCodeModal } from "./custom_code";
import GuardrailGarden from "./guardrail_garden";
import { TeamGuardrailsTab } from "./TeamGuardrailsTab";
@ -211,7 +211,7 @@ const GuardrailsPanel: React.FC<GuardrailsPanelProps> = ({ accessToken, userRole
{ label: "Name", value: guardrailToDelete?.guardrail_name },
{ label: "ID", value: guardrailToDelete?.guardrail_id, code: true },
{ label: "Provider", value: providerDisplayName },
{ label: "Mode", value: guardrailToDelete?.litellm_params.mode },
{ label: "Mode", value: formatGuardrailMode(guardrailToDelete?.litellm_params.mode) },
{
label: "Default On",
value: guardrailToDelete?.litellm_params.default_on ? "Yes" : "No",

View file

@ -15,7 +15,7 @@ import {
} from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/cva.config";
import { getGuardrailLogoAndName } from "./guardrail_info_helpers";
import { formatGuardrailMode, getGuardrailLogoAndName } from "./guardrail_info_helpers";
import { Logo } from "@/components/molecules/logo/Logo";
const CONFIG_DELETE_HINT = "Config guardrails are defined in the config file and cannot be deleted from the dashboard.";
@ -117,9 +117,14 @@ export const getGuardrailTableColumns = ({
header: "Mode",
size: 130,
enableSorting: false,
cell: ({ row }) => (
<span className="font-mono text-xs text-muted-foreground">{row.original.litellm_params.mode}</span>
),
cell: ({ row }) => {
const mode = formatGuardrailMode(row.original.litellm_params.mode);
return (
<span className="font-mono text-xs text-muted-foreground" title={mode || undefined}>
{mode || "-"}
</span>
);
},
},
{
id: "default_on",

View file

@ -82,6 +82,36 @@ describe("Guardrail Info", () => {
expect(getByText("Settings")).toBeInTheDocument();
});
it("should render a tag-based mode object rather than crashing the detail view", async () => {
vi.mocked(networking.getGuardrailInfo).mockResolvedValue({
guardrail_id: "123",
guardrail_name: "Test Guardrail",
litellm_params: {
guardrail: "bedrock",
mode: { tags: { "Service-Type: internal-service": "post_call" }, default: ["pre_call", "post_call"] },
default_on: true,
},
created_at: "2024-01-01T00:00:00Z",
updated_at: "2024-01-01T00:00:00Z",
guardrail_definition_location: "database",
});
vi.mocked(networking.getGuardrailUISettings).mockResolvedValue({
supported_entities: [],
supported_actions: [],
pii_entity_categories: [],
supported_modes: ["pre_call", "post_call"],
});
vi.mocked(networking.getGuardrailProviderSpecificParams).mockResolvedValue({});
const { findAllByText } = render(
<GuardrailInfoView guardrailId="123" onClose={() => {}} accessToken="123" isAdmin={true} />,
);
expect(await findAllByText("pre_call, post_call (tag-based)")).not.toHaveLength(0);
});
it("should render the provider logo from the bundled guardrail logo map", async () => {
vi.mocked(networking.getGuardrailInfo).mockResolvedValue({
guardrail_id: "123",

View file

@ -33,6 +33,7 @@ import {
import ContentFilterManager, { formatContentFilterDataForAPI } from "./content_filter/ContentFilterManager";
import CustomCodeModal, { EditGuardrailData } from "./custom_code/CustomCodeModal";
import {
formatGuardrailMode,
getGuardrailLogoAndName,
guardrail_provider_map,
skipSystemMessageToChoice,
@ -559,7 +560,9 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
<Card className="block p-6">
<p>Mode</p>
<div className="mt-2">
<h3 className="text-lg font-medium">{guardrailData.litellm_params?.mode || "-"}</h3>
<h3 className="text-lg font-medium">
{formatGuardrailMode(guardrailData.litellm_params?.mode) || "-"}
</h3>
<Badge variant={guardrailData.litellm_params?.default_on ? "secondary" : "outline"}>
{guardrailData.litellm_params?.default_on ? "Default On" : "Default Off"}
</Badge>
@ -856,7 +859,7 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
</div>
<div>
<p className="font-medium">Mode</p>
<div>{guardrailData.litellm_params?.mode || "-"}</div>
<div>{formatGuardrailMode(guardrailData.litellm_params?.mode) || "-"}</div>
</div>
<div>
<p className="font-medium">Default On</p>

View file

@ -14,6 +14,7 @@ import {
choiceToSkipSystemForCreate,
skipToolMessageToChoice,
choiceToSkipToolForCreate,
formatGuardrailMode,
} from "./guardrail_info_helpers";
describe("guardrail_info_helpers", () => {
@ -210,6 +211,34 @@ describe("guardrail_info_helpers", () => {
});
});
describe("formatGuardrailMode", () => {
it("renders a single mode and a list of modes", () => {
expect(formatGuardrailMode("pre_call")).toBe("pre_call");
expect(formatGuardrailMode(["pre_call", "post_call"])).toBe("pre_call, post_call");
});
it("flattens a tag-based mode object into deduped modes instead of returning it verbatim", () => {
const mode = {
tags: { "Service-Type: internal-service": "post_call", "Service-Type: batch": ["during_call", "post_call"] },
default: ["pre_call", "post_call"],
};
expect(formatGuardrailMode(mode)).toBe("pre_call, post_call, during_call (tag-based)");
});
it("handles a tag-based mode with no default and with no tags", () => {
expect(formatGuardrailMode({ tags: { "team: a": "post_call" } })).toBe("post_call (tag-based)");
expect(formatGuardrailMode({ default: "pre_call" })).toBe("pre_call (tag-based)");
});
it("returns an empty string for missing or unusable modes", () => {
expect(formatGuardrailMode(undefined)).toBe("");
expect(formatGuardrailMode(null)).toBe("");
expect(formatGuardrailMode({})).toBe("");
expect(formatGuardrailMode({ tags: {}, default: null })).toBe("");
});
});
describe("skipSystemMessageToChoice / choiceToSkipSystemForCreate", () => {
it("maps API values to form choices and back for create", () => {
expect(skipSystemMessageToChoice(undefined)).toBe("inherit");

View file

@ -110,6 +110,17 @@ export const toModeArray = (raw: unknown): string[] => {
return [];
};
export const formatGuardrailMode = (raw: unknown): string => {
const flat: string[] = toModeArray(raw);
if (flat.length > 0) return flat.join(", ");
if (raw === null || typeof raw !== "object") return "";
const { tags, default: fallback } = raw as { tags?: Record<string, unknown>; default?: unknown };
const tagged: string[] = tags && typeof tags === "object" ? Object.values(tags).flatMap(toModeArray) : [];
const modes: string[] = Array.from(new Set([...toModeArray(fallback), ...tagged]));
return modes.length > 0 ? `${modes.join(", ")} (tag-based)` : "";
};
// Resolves the supported modes for the selected provider, falling back to the global list
export const getSupportedModesForProvider = (
settings: { supported_modes?: string[]; supported_modes_by_provider?: Record<string, string[]> } | null,

View file

@ -46,6 +46,18 @@ describe("GuardrailTable", () => {
expect(screen.getByText("m")).toBeInTheDocument();
});
it("renders a tag-based mode object instead of crashing the table", () => {
const guardrail = makeGuardrail({
litellm_params: {
guardrail: "bedrock",
mode: { tags: { "Service-Type: internal-service": "post_call" }, default: ["pre_call", "post_call"] },
default_on: true,
},
});
render(<GuardrailTable guardrailsList={[guardrail]} {...baseProps} />);
expect(screen.getByText("pre_call, post_call (tag-based)")).toBeInTheDocument();
});
it("deletes a DB guardrail through the actions menu", async () => {
const user = userEvent.setup();
const onDeleteClick = vi.fn();

View file

@ -12,6 +12,7 @@ import {
} from "@/components/ui/dialog";
import { Separator } from "@/components/ui/separator";
import { CheckCircle2, Info } from "lucide-react";
import { formatGuardrailMode } from "@/app/(dashboard)/guardrails/_components/guardrail_info_helpers";
interface GuardrailInfo {
guardrail_name: string;
@ -163,7 +164,9 @@ const GuardrailSelectionModal: React.FC<GuardrailSelectionModalProps> = ({
{/* Show guardrail type and mode */}
<div className="flex gap-2 mt-2">
<Badge variant="outline">{guardrail.definition?.litellm_params?.guardrail || "unknown"}</Badge>
<Badge variant="secondary">{guardrail.definition?.litellm_params?.mode || "unknown"}</Badge>
<Badge variant="secondary">
{formatGuardrailMode(guardrail.definition?.litellm_params?.mode) || "unknown"}
</Badge>
{guardrail.definition?.litellm_params?.patterns && (
<Badge variant="secondary">
{guardrail.definition.litellm_params.patterns.length} pattern(s)

View file

@ -18,12 +18,17 @@ export interface PiiConfigurationProps {
entityCategories?: PiiEntityCategory[];
}
export type GuardrailMode =
| string
| string[]
| { tags?: Record<string, string | string[]>; default?: string | string[] | null };
export interface Guardrail {
guardrail_id: string;
guardrail_name: string | null;
litellm_params: {
guardrail: string;
mode: string;
mode: GuardrailMode;
default_on: boolean;
pii_entities_config?: { [key: string]: string };
[key: string]: any;