mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
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:
parent
133e72c8fd
commit
b8680e6bae
7 changed files with 102 additions and 7 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue