mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-07 02:59:05 +00:00
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:
commit
e5adf7d926
10 changed files with 114 additions and 12 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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