fix(ui): render tag-based guardrail mode instead of crashing guardrails page

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Devin AI 2026-08-19 18:22:31 +00:00
parent 133e72c8fd
commit b8680e6bae
7 changed files with 102 additions and 7 deletions

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

@ -35,6 +35,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,7 @@ 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>
@ -852,7 +853,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,19 @@ export const toModeArray = (raw: unknown): string[] => {
return [];
};
// Turns a guardrail mode into a renderable string. A mode is a single mode, a list of modes, or a
// tag-based `{ tags, default }` object, which React refuses to render as a child
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

@ -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;