feat(ui): i18n for guardrail params, table, LLM judge, and PII components

This commit is contained in:
吹雪 2026-06-10 18:31:29 +09:00
parent eccf54a990
commit dc9c44f3b1
9 changed files with 316 additions and 91 deletions

View file

@ -1,5 +1,6 @@
import React from "react";
import { Form, Select, Typography, Input, Button } from "antd";
import { useTranslation } from "react-i18next";
import NumericalInput from "../shared/numerical_input";
const { Title } = Typography;
@ -30,6 +31,7 @@ interface DictFieldProps {
}
const DictField: React.FC<DictFieldProps> = ({ field, fieldKey, fullFieldKey, value }) => {
const { t } = useTranslation();
const [selectedEntries, setSelectedEntries] = React.useState<Array<{ key: string; id: string }>>([]);
const [availableKeys, setAvailableKeys] = React.useState<string[]>(field.dict_key_options || []);
@ -87,19 +89,23 @@ const DictField: React.FC<DictFieldProps> = ({ field, fieldKey, fullFieldKey, va
}
>
{field.dict_value_type === "number" ? (
<NumericalInput step={1} width={200} placeholder={`Enter ${entry.key} value`} />
<NumericalInput
step={1}
width={200}
placeholder={t("guardrails.guardrailOptionalParams.enterValue", { key: entry.key })}
/>
) : field.dict_value_type === "boolean" ? (
<Select placeholder={`Select ${entry.key} value`}>
<Select placeholder={t("guardrails.guardrailOptionalParams.selectValue", { key: entry.key })}>
<Select.Option value={true}>True</Select.Option>
<Select.Option value={false}>False</Select.Option>
</Select>
) : (
<Input placeholder={`Enter ${entry.key} value`} />
<Input placeholder={t("guardrails.guardrailOptionalParams.enterValue", { key: entry.key })} />
)}
</Form.Item>
</div>
<Button type="text" danger size="small" onClick={() => removeEntry(entry.id, entry.key)}>
Remove
{t("common.remove")}
</Button>
</div>
))}
@ -108,7 +114,7 @@ const DictField: React.FC<DictFieldProps> = ({ field, fieldKey, fullFieldKey, va
{availableKeys.length > 0 && (
<div className="flex items-center space-x-3 mt-2">
<Select
placeholder="Select category to configure"
placeholder={t("guardrails.guardrailOptionalParams.selectCategoryPlaceholder")}
style={{ width: 200 }}
onSelect={(value: string | undefined) => value && addEntry(value)}
value={undefined}
@ -119,7 +125,7 @@ const DictField: React.FC<DictFieldProps> = ({ field, fieldKey, fullFieldKey, va
</Select.Option>
))}
</Select>
<span className="text-sm text-gray-500">Select a category to add threshold configuration</span>
<span className="text-sm text-gray-500">{t("guardrails.guardrailOptionalParams.selectCategoryHint")}</span>
</div>
)}
</div>
@ -131,6 +137,8 @@ const GuardrailOptionalParams: React.FC<GuardrailOptionalParamsProps> = ({
parentFieldKey,
values,
}) => {
const { t } = useTranslation();
const renderField = (fieldKey: string, field: ProviderParam) => {
const fullFieldKey = `${parentFieldKey}.${fieldKey}`;
const value = values?.[fieldKey];
@ -156,7 +164,11 @@ const GuardrailOptionalParams: React.FC<GuardrailOptionalParamsProps> = ({
<p className="text-sm text-gray-600 mt-1">{field.description}</p>
</div>
}
rules={field.required ? [{ required: true, message: `${fieldKey} is required` }] : undefined}
rules={
field.required
? [{ required: true, message: t("guardrails.guardrailOptionalParams.fieldRequired", { fieldKey }) }]
: undefined
}
className="mb-0"
initialValue={value !== undefined ? value : field.default_value}
normalize={
@ -210,10 +222,10 @@ const GuardrailOptionalParams: React.FC<GuardrailOptionalParamsProps> = ({
<div className="guardrail-optional-params">
<div className="mb-8 pb-4 border-b border-gray-100">
<Title level={3} className="mb-2 font-semibold text-gray-900">
Optional Parameters
{t("guardrails.guardrailOptionalParams.title")}
</Title>
<p className="text-gray-600 text-sm">
{optionalParams.description || "Configure additional settings for this guardrail provider"}
{optionalParams.description || t("guardrails.guardrailOptionalParams.defaultDescription")}
</p>
</div>

View file

@ -1,5 +1,6 @@
import React, { useState, useEffect } from "react";
import { Form, Select, Spin, Input, Slider } from "antd";
import { useTranslation } from "react-i18next";
import {
guardrail_provider_map,
populateGuardrailProviders,
@ -41,6 +42,7 @@ const GuardrailProviderFields: React.FC<GuardrailProviderFieldsProps> = ({
providerParams: providerParamsProp = null,
value = null,
}) => {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [providerParams, setProviderParams] = useState<ProviderParamsResponse | null>(providerParamsProp);
const [error, setError] = useState<string | null>(null);
@ -69,7 +71,7 @@ const GuardrailProviderFields: React.FC<GuardrailProviderFieldsProps> = ({
populateGuardrailProviderMap(data);
} catch (error) {
console.error("Error fetching provider params:", error);
setError("Failed to load provider parameters");
setError(t("guardrails.guardrailProviderFields.loadFailed"));
} finally {
setLoading(false);
}
@ -88,7 +90,7 @@ const GuardrailProviderFields: React.FC<GuardrailProviderFieldsProps> = ({
// Show loading state
if (loading) {
return <Spin tip="Loading provider parameters..." />;
return <Spin tip={t("guardrails.guardrailProviderFields.loadingTip")} />;
}
// Show error state
@ -106,7 +108,7 @@ const GuardrailProviderFields: React.FC<GuardrailProviderFieldsProps> = ({
console.log("Provider fields:", providerFields);
if (!providerFields || Object.keys(providerFields).length === 0) {
return <div>No configuration fields available for this provider.</div>;
return <div>{t("guardrails.guardrailProviderFields.noFields")}</div>;
}
console.log("Value:", value);
@ -166,7 +168,11 @@ const GuardrailProviderFields: React.FC<GuardrailProviderFieldsProps> = ({
name={fullFieldKey}
label={fieldKey}
tooltip={field.description}
rules={field.required ? [{ required: true, message: `${fieldKey} is required` }] : undefined}
rules={
field.required
? [{ required: true, message: t("guardrails.guardrailProviderFields.fieldRequired", { fieldKey }) }]
: undefined
}
initialValue={resolvedInitialValue}
>
{field.type === "select" && field.options ? (

View file

@ -1,4 +1,5 @@
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow, Icon, Button } from "@tremor/react";
import { TrashIcon, SwitchVerticalIcon, ChevronUpIcon, ChevronDownIcon } from "@heroicons/react/outline";
import { Tooltip } from "antd";
@ -39,6 +40,7 @@ const GuardrailTable: React.FC<GuardrailTableProps> = ({
isAdmin = false,
onGuardrailClick,
}) => {
const { t } = useTranslation();
const [sorting, setSorting] = useState<SortingState>([{ id: "created_at", desc: true }]);
const [editModalVisible, setEditModalVisible] = useState(false);
const [selectedGuardrail, setSelectedGuardrail] = useState<Guardrail | null>(null);
@ -63,7 +65,7 @@ const GuardrailTable: React.FC<GuardrailTableProps> = ({
const columns: ColumnDef<Guardrail>[] = [
{
header: "Guardrail ID",
header: t("guardrails.guardrailTable.colGuardrailId"),
accessorKey: "guardrail_id",
cell: (info: any) => (
<Tooltip title={String(info.getValue() || "")}>
@ -79,7 +81,7 @@ const GuardrailTable: React.FC<GuardrailTableProps> = ({
),
},
{
header: "Name",
header: t("guardrails.guardrailTable.colName"),
accessorKey: "guardrail_name",
cell: ({ row }) => {
const guardrail = row.original;
@ -91,7 +93,7 @@ const GuardrailTable: React.FC<GuardrailTableProps> = ({
},
},
{
header: "Provider",
header: t("guardrails.guardrailTable.colProvider"),
accessorKey: "litellm_params.guardrail",
cell: ({ row }) => {
const guardrail = row.original;
@ -115,7 +117,7 @@ const GuardrailTable: React.FC<GuardrailTableProps> = ({
},
},
{
header: "Mode",
header: t("guardrails.guardrailTable.colMode"),
accessorKey: "litellm_params.mode",
cell: ({ row }) => {
const guardrail = row.original;
@ -123,7 +125,7 @@ const GuardrailTable: React.FC<GuardrailTableProps> = ({
},
},
{
header: "Default On",
header: t("guardrails.guardrailTable.colDefaultOn"),
accessorKey: "litellm_params.default_on",
cell: ({ row }) => {
const guardrail = row.original;
@ -133,13 +135,15 @@ const GuardrailTable: React.FC<GuardrailTableProps> = ({
className="text-xs font-normal"
size="xs"
>
{guardrail.litellm_params?.default_on ? "Default On" : "Default Off"}
{guardrail.litellm_params?.default_on
? t("guardrails.guardrailTable.defaultOn")
: t("guardrails.guardrailTable.defaultOff")}
</Badge>
);
},
},
{
header: "Created At",
header: t("common.createdAt"),
accessorKey: "created_at",
cell: ({ row }) => {
const guardrail = row.original;
@ -151,7 +155,7 @@ const GuardrailTable: React.FC<GuardrailTableProps> = ({
},
},
{
header: "Updated At",
header: t("common.updatedAt"),
accessorKey: "updated_at",
cell: ({ row }) => {
const guardrail = row.original;
@ -164,31 +168,34 @@ const GuardrailTable: React.FC<GuardrailTableProps> = ({
},
{
id: "actions",
header: "Actions",
header: t("common.actions"),
cell: ({ row }) => {
const guardrail = row.original;
const isConfigGuardrail = guardrail.guardrail_definition_location === GuardrailDefinitionLocation.CONFIG;
return (
<div className="flex space-x-2">
{isConfigGuardrail ? (
<Tooltip title="Config guardrail cannot be deleted on the dashboard. Please delete it from the config file.">
<Tooltip title={t("guardrails.guardrailTable.configDeleteTooltip")}>
<Icon
data-testid="config-delete-icon"
icon={TrashIcon}
size="sm"
className="cursor-not-allowed text-gray-400"
title="Config guardrail cannot be deleted on the dashboard. Please delete it from the config file."
aria-label="Delete guardrail (config)"
title={t("guardrails.guardrailTable.configDeleteTooltip")}
aria-label={t("guardrails.guardrailTable.configDeleteAriaLabel")}
/>
</Tooltip>
) : (
<Tooltip title="Delete guardrail">
<Tooltip title={t("guardrails.guardrailTable.deleteTooltip")}>
<Icon
icon={TrashIcon}
size="sm"
onClick={() =>
guardrail.guardrail_id &&
onDeleteClick(guardrail.guardrail_id, guardrail.guardrail_name || "Unnamed Guardrail")
onDeleteClick(
guardrail.guardrail_id,
guardrail.guardrail_name || t("guardrails.guardrailTable.unnamedGuardrail"),
)
}
className="cursor-pointer hover:text-red-500"
/>
@ -254,7 +261,7 @@ const GuardrailTable: React.FC<GuardrailTableProps> = ({
<TableRow>
<TableCell colSpan={columns.length} className="h-8 text-center">
<div className="text-center text-gray-500">
<p>Loading...</p>
<p>{t("common.loading")}</p>
</div>
</TableCell>
</TableRow>
@ -279,7 +286,7 @@ const GuardrailTable: React.FC<GuardrailTableProps> = ({
<TableRow>
<TableCell colSpan={columns.length} className="h-8 text-center">
<div className="text-center text-gray-500">
<p>No guardrails found</p>
<p>{t("guardrails.guardrailTable.noGuardrails")}</p>
</div>
</TableCell>
</TableRow>

View file

@ -4,6 +4,7 @@ import React from "react";
import { Form, Select, InputNumber, Input, Tooltip } from "antd";
import { PlusOutlined, QuestionCircleOutlined } from "@ant-design/icons";
import { Button } from "antd";
import { useTranslation, Trans } from "react-i18next";
interface LLMJudgeFieldsProps {
availableModels: string[];
@ -11,6 +12,7 @@ interface LLMJudgeFieldsProps {
}
const LLMJudgeFields: React.FC<LLMJudgeFieldsProps> = ({ availableModels, form }) => {
const { t } = useTranslation();
return (
<>
<div
@ -24,25 +26,24 @@ const LLMJudgeFields: React.FC<LLMJudgeFieldsProps> = ({ availableModels, form }
color: "#389e0d",
}}
>
After each LLM response, the <strong>Judge Model</strong> scores it 0–100 against your criteria. If the weighted
average falls below the threshold, the response is blocked (or logged).
<Trans i18nKey="guardrails.lLMJudgeFields.description" components={{ strong: <strong /> }} />
</div>
<Form.Item
name="judge_model"
label={
<span>
Judge Model&nbsp;
<Tooltip title="The LLM that reads each response and grades it. Pick a capable model — it never sees end-user data beyond what the LLM returned.">
{t("guardrails.lLMJudgeFields.judgeModelLabel")}&nbsp;
<Tooltip title={t("guardrails.lLMJudgeFields.judgeModelTooltip")}>
<QuestionCircleOutlined style={{ color: "#8c8c8c" }} />
</Tooltip>
</span>
}
rules={[{ required: true, message: "Select a judge model" }]}
rules={[{ required: true, message: t("guardrails.lLMJudgeFields.judgeModelRequired") }]}
>
<Select
showSearch
placeholder="Select a model"
placeholder={t("guardrails.lLMJudgeFields.judgeModelPlaceholder")}
options={availableModels.map((m) => ({ label: m, value: m }))}
/>
</Form.Item>
@ -51,8 +52,8 @@ const LLMJudgeFields: React.FC<LLMJudgeFieldsProps> = ({ availableModels, form }
name="overall_threshold"
label={
<span>
Minimum Score to Pass&nbsp;
<Tooltip title="0–100. If the weighted average of criterion scores falls below this, the guardrail triggers. 80 is a good default.">
{t("guardrails.lLMJudgeFields.minScoreLabel")}&nbsp;
<Tooltip title={t("guardrails.lLMJudgeFields.minScoreTooltip")}>
<QuestionCircleOutlined style={{ color: "#8c8c8c" }} />
</Tooltip>
</span>
@ -66,8 +67,8 @@ const LLMJudgeFields: React.FC<LLMJudgeFieldsProps> = ({ availableModels, form }
name="on_failure"
label={
<span>
On Failure&nbsp;
<Tooltip title="Block: return HTTP 422 when the score is too low. Log: record the result but let the response through.">
{t("guardrails.lLMJudgeFields.onFailureLabel")}&nbsp;
<Tooltip title={t("guardrails.lLMJudgeFields.onFailureTooltip")}>
<QuestionCircleOutlined style={{ color: "#8c8c8c" }} />
</Tooltip>
</span>
@ -75,16 +76,16 @@ const LLMJudgeFields: React.FC<LLMJudgeFieldsProps> = ({ availableModels, form }
initialValue="block"
>
<Select>
<Select.Option value="block">Block (return 422)</Select.Option>
<Select.Option value="log">Log only</Select.Option>
<Select.Option value="block">{t("guardrails.lLMJudgeFields.onFailureBlock")}</Select.Option>
<Select.Option value="log">{t("guardrails.lLMJudgeFields.onFailureLog")}</Select.Option>
</Select>
</Form.Item>
<Form.Item
label={
<span>
Evaluation Criteria&nbsp;
<Tooltip title="Each criterion is something the judge checks. Weights must add up to 100%.">
{t("guardrails.lLMJudgeFields.criteriaLabel")}&nbsp;
<Tooltip title={t("guardrails.lLMJudgeFields.criteriaTooltip")}>
<QuestionCircleOutlined style={{ color: "#8c8c8c" }} />
</Tooltip>
</span>
@ -107,22 +108,23 @@ const LLMJudgeFields: React.FC<LLMJudgeFieldsProps> = ({ availableModels, form }
<Form.Item
{...restField}
name={[name, "name"]}
rules={[{ required: true, message: "Enter criterion name" }]}
rules={[{ required: true, message: t("guardrails.lLMJudgeFields.criterionNameRequired") }]}
style={{ flex: 2, marginBottom: 8 }}
>
<Input placeholder="Criterion name (e.g. Policy accuracy)" />
<Input placeholder={t("guardrails.lLMJudgeFields.criterionNamePlaceholder")} />
</Form.Item>
<Form.Item
{...restField}
name={[name, "weight"]}
label={
<Tooltip title="How much this criterion counts toward the final score. All weights must add up to 100%.">
<Tooltip title={t("guardrails.lLMJudgeFields.weightTooltip")}>
<span style={{ fontSize: 12, color: "#595959" }}>
Weight <QuestionCircleOutlined style={{ color: "#bfbfbf" }} />
{t("guardrails.lLMJudgeFields.weightLabel")}{" "}
<QuestionCircleOutlined style={{ color: "#bfbfbf" }} />
</span>
</Tooltip>
}
rules={[{ required: true, message: "Enter weight" }]}
rules={[{ required: true, message: t("guardrails.lLMJudgeFields.weightRequired") }]}
style={{ flex: 1, marginBottom: 8 }}
>
<InputNumber min={0} max={100} addonAfter="%" style={{ width: "100%" }} placeholder="e.g. 50" />
@ -136,10 +138,10 @@ const LLMJudgeFields: React.FC<LLMJudgeFieldsProps> = ({ availableModels, form }
<Form.Item
{...restField}
name={[name, "description"]}
rules={[{ required: true, message: "Describe what to check" }]}
rules={[{ required: true, message: t("guardrails.lLMJudgeFields.criterionDescRequired") }]}
style={{ marginBottom: 8 }}
>
<Input placeholder="What should the judge check for this criterion?" />
<Input placeholder={t("guardrails.lLMJudgeFields.criterionDescPlaceholder")} />
</Form.Item>
</div>
))}
@ -150,7 +152,7 @@ const LLMJudgeFields: React.FC<LLMJudgeFieldsProps> = ({ availableModels, form }
onClick={() => add({ name: "", weight: 0, description: "" })}
icon={<PlusOutlined />}
>
Add Criterion
{t("guardrails.lLMJudgeFields.addCriterion")}
</Button>
{fields.length > 0 && (
<Form.Item shouldUpdate noStyle>
@ -160,7 +162,8 @@ const LLMJudgeFields: React.FC<LLMJudgeFieldsProps> = ({ availableModels, form }
const weightOk = weightTotal === 100;
return (
<div style={{ marginTop: 6, fontSize: 12, color: weightOk ? "#52c41a" : "#faad14" }}>
Weights total: {weightTotal}%{weightOk ? " ✓" : " — must add up to 100%"}
{t("guardrails.lLMJudgeFields.weightsTotal", { weightTotal })}
{weightOk ? " ✓" : ` — ${t("guardrails.lLMJudgeFields.weightsMustSum")}`}
</div>
);
}}

View file

@ -1,6 +1,7 @@
import React from "react";
import { Typography, Select, Button, Checkbox, Tooltip, Tag } from "antd";
import { CloseOutlined, EyeInvisibleOutlined, StopOutlined, FilterOutlined } from "@ant-design/icons";
import { useTranslation } from "react-i18next";
import { PiiEntityCategory } from "./types";
const { Text } = Typography;
@ -30,15 +31,16 @@ export interface CategoryFilterProps {
}
export const CategoryFilter: React.FC<CategoryFilterProps> = ({ categories, selectedCategories, onChange }) => {
const { t } = useTranslation();
return (
<div>
<div className="flex items-center mb-2">
<FilterOutlined className="text-gray-500 mr-1" />
<Text className="text-gray-500 font-medium">Filter by category</Text>
<Text className="text-gray-500 font-medium">{t("guardrails.piiComponents.filterByCategory")}</Text>
</div>
<Select
mode="multiple"
placeholder="Select categories to filter by"
placeholder={t("guardrails.piiComponents.selectCategoriesPlaceholder")}
style={{ width: "100%" }}
onChange={onChange}
value={selectedCategories}
@ -70,14 +72,15 @@ export interface QuickActionsProps {
}
export const QuickActions: React.FC<QuickActionsProps> = ({ onSelectAll, onUnselectAll, hasSelectedEntities }) => {
const { t } = useTranslation();
return (
<div className="bg-gray-50 p-5 rounded-lg mb-6 border border-gray-200 shadow-sm">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center">
<Text strong className="text-gray-700 text-base">
Quick Actions
{t("guardrails.piiComponents.quickActions")}
</Text>
<Tooltip title="Apply action to all PII types at once">
<Tooltip title={t("guardrails.piiComponents.quickActionsTooltip")}>
<div className="ml-2 text-gray-400 cursor-help text-xs">ⓘ</div>
</Tooltip>
</div>
@ -88,7 +91,7 @@ export const QuickActions: React.FC<QuickActionsProps> = ({ onSelectAll, onUnsel
disabled={!hasSelectedEntities}
icon={<CloseOutlined />}
>
Unselect All
{t("guardrails.piiComponents.unselectAll")}
</Button>
</div>
<div className="grid grid-cols-2 gap-4">
@ -100,7 +103,7 @@ export const QuickActions: React.FC<QuickActionsProps> = ({ onSelectAll, onUnsel
block
icon={<EyeInvisibleOutlined />}
>
Select All & Mask
{t("guardrails.piiComponents.selectAllMask")}
</Button>
<Button
color="danger"
@ -110,7 +113,7 @@ export const QuickActions: React.FC<QuickActionsProps> = ({ onSelectAll, onUnsel
block
icon={<StopOutlined />}
>
Select All & Block
{t("guardrails.piiComponents.selectAllBlock")}
</Button>
</div>
</div>
@ -137,19 +140,20 @@ export const PiiEntityList: React.FC<PiiEntityListProps> = ({
onActionSelect,
entityToCategoryMap,
}) => {
const { t } = useTranslation();
return (
<div className="border rounded-lg overflow-hidden shadow-sm">
<div className="bg-gray-50 px-5 py-3 border-b flex">
<Text strong className="flex-1 text-gray-700">
PII Type
{t("guardrails.piiComponents.piiTypeHeader")}
</Text>
<Text strong className="w-32 text-right text-gray-700">
Action
{t("guardrails.piiComponents.actionHeader")}
</Text>
</div>
<div className="max-h-[400px] overflow-y-auto">
{entities.length === 0 ? (
<div className="py-10 text-center text-gray-500">No PII types match your filter criteria</div>
<div className="py-10 text-center text-gray-500">{t("guardrails.piiComponents.noMatch")}</div>
) : (
entities.map((entity) => (
<div

View file

@ -1,5 +1,6 @@
import { Typography } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { CategoryFilter, PiiEntityList, QuickActions } from "./pii_components";
import { PiiConfigurationProps } from "./types";
@ -18,6 +19,7 @@ const PiiConfiguration: React.FC<PiiConfigurationProps> = ({
onActionSelect,
entityCategories = [],
}) => {
const { t } = useTranslation();
const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
// Create a lookup map to quickly find an entity's category
@ -58,10 +60,12 @@ const PiiConfiguration: React.FC<PiiConfigurationProps> = ({
<div className="flex justify-between items-center mb-5">
<div className="flex items-center">
<Title level={4} className="!m-0 font-semibold text-gray-800">
Configure PII Protection
{t("guardrails.piiConfiguration.title")}
</Title>
</div>
<Text className="text-gray-500">{selectedEntities.length} items selected</Text>
<Text className="text-gray-500">
{t("guardrails.piiConfiguration.itemsSelected", { count: selectedEntities.length })}
</Text>
</div>
<div className="mb-6">

View file

@ -2,6 +2,7 @@ import React from "react";
import { Card, Text } from "@tremor/react";
import { Button, Divider, Empty, Input, Select, Space, Tooltip } from "antd";
import { InfoCircleOutlined, PlusOutlined, DeleteOutlined } from "@ant-design/icons";
import { useTranslation } from "react-i18next";
export type ToolPermissionDecision = "allow" | "deny";
export type ToolPermissionDefaultAction = "allow" | "deny";
@ -42,6 +43,7 @@ const ensureConfig = (config?: ToolPermissionConfig): ToolPermissionConfig => ({
});
const ToolPermissionRulesEditor: React.FC<ToolPermissionRulesEditorProps> = ({ value, onChange, disabled = false }) => {
const { t } = useTranslation();
const config = ensureConfig(value);
const updateConfig = (partial: Partial<ToolPermissionConfig>) => {
@ -119,14 +121,14 @@ const ToolPermissionRulesEditor: React.FC<ToolPermissionRulesEditorProps> = ({ v
size="small"
onClick={() => updateRule(index, { allowed_param_patterns: { "": "" } })}
>
+ Restrict tool arguments (optional)
{t("guardrails.toolPermissionRulesEditor.restrictArgs")}
</Button>
);
}
return (
<div className="space-y-2">
<Text className="text-sm text-gray-600">Argument constraints (dot or array paths)</Text>
<Text className="text-sm text-gray-600">{t("guardrails.toolPermissionRulesEditor.argConstraints")}</Text>
{entries.map(([path, pattern], patternIndex) => (
<Space key={`${rule.id || index}-${patternIndex}`} align="start">
<Input
@ -165,7 +167,7 @@ const ToolPermissionRulesEditor: React.FC<ToolPermissionRulesEditorProps> = ({ v
})
}
>
+ Add another constraint
{t("guardrails.toolPermissionRulesEditor.addConstraint")}
</Button>
</div>
);
@ -175,11 +177,8 @@ const ToolPermissionRulesEditor: React.FC<ToolPermissionRulesEditorProps> = ({ v
<Card>
<div className="flex items-center justify-between">
<div>
<Text className="text-lg font-semibold">LiteLLM Tool Permission Guardrail</Text>
<Text className="text-sm text-gray-500">
Provide regex patterns (e.g., ^mcp__github_.*$) for tool names or types and optionally constrain payload
fields.
</Text>
<Text className="text-lg font-semibold">{t("guardrails.toolPermissionRulesEditor.title")}</Text>
<Text className="text-sm text-gray-500">{t("guardrails.toolPermissionRulesEditor.subtitle")}</Text>
</div>
{!disabled && (
<Button
@ -188,7 +187,7 @@ const ToolPermissionRulesEditor: React.FC<ToolPermissionRulesEditorProps> = ({ v
onClick={addRule}
className="!bg-blue-600 !text-white hover:!bg-blue-500"
>
Add Rule
{t("guardrails.toolPermissionRulesEditor.addRule")}
</Button>
)}
</div>
@ -196,13 +195,15 @@ const ToolPermissionRulesEditor: React.FC<ToolPermissionRulesEditorProps> = ({ v
<Divider />
{config.rules.length === 0 ? (
<Empty description="No tool rules added yet" />
<Empty description={t("guardrails.toolPermissionRulesEditor.noRules")} />
) : (
<div className="space-y-4">
{config.rules.map((rule, index) => (
<Card key={rule.id || index} className="bg-gray-50">
<div className="flex items-center justify-between mb-3">
<Text className="font-semibold">Rule {index + 1}</Text>
<Text className="font-semibold">
{t("guardrails.toolPermissionRulesEditor.ruleLabel", { index: index + 1 })}
</Text>
<Button
icon={<DeleteOutlined />}
danger
@ -210,12 +211,12 @@ const ToolPermissionRulesEditor: React.FC<ToolPermissionRulesEditorProps> = ({ v
disabled={disabled}
onClick={() => removeRule(index)}
>
Remove
{t("common.remove")}
</Button>
</div>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<div>
<Text className="text-sm font-medium">Rule ID</Text>
<Text className="text-sm font-medium">{t("guardrails.toolPermissionRulesEditor.ruleId")}</Text>
<Input
disabled={disabled}
placeholder="unique_rule_id"
@ -224,7 +225,7 @@ const ToolPermissionRulesEditor: React.FC<ToolPermissionRulesEditorProps> = ({ v
/>
</div>
<div>
<Text className="text-sm font-medium">Tool Name (optional)</Text>
<Text className="text-sm font-medium">{t("guardrails.toolPermissionRulesEditor.toolName")}</Text>
<Input
disabled={disabled}
placeholder="^mcp__github_.*$"
@ -240,7 +241,7 @@ const ToolPermissionRulesEditor: React.FC<ToolPermissionRulesEditorProps> = ({ v
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 mt-4">
<div>
<Text className="text-sm font-medium">Tool Type (optional)</Text>
<Text className="text-sm font-medium">{t("guardrails.toolPermissionRulesEditor.toolType")}</Text>
<Input
disabled={disabled}
placeholder="^function$"
@ -255,15 +256,15 @@ const ToolPermissionRulesEditor: React.FC<ToolPermissionRulesEditorProps> = ({ v
</div>
<div className="mt-4 flex flex-col gap-2">
<Text className="text-sm font-medium">Decision</Text>
<Text className="text-sm font-medium">{t("guardrails.toolPermissionRulesEditor.decision")}</Text>
<Select
disabled={disabled}
value={rule.decision}
style={{ width: 200 }}
onChange={(value) => updateRule(index, { decision: value as ToolPermissionDecision })}
>
<Select.Option value="allow">Allow</Select.Option>
<Select.Option value="deny">Deny</Select.Option>
<Select.Option value="allow">{t("guardrails.toolPermissionRulesEditor.allow")}</Select.Option>
<Select.Option value="deny">{t("guardrails.toolPermissionRulesEditor.deny")}</Select.Option>
</Select>
</div>
@ -277,20 +278,20 @@ const ToolPermissionRulesEditor: React.FC<ToolPermissionRulesEditorProps> = ({ v
<div className="grid gap-4 md:grid-cols-2">
<div>
<Text className="text-sm font-medium">Default action</Text>
<Text className="text-sm font-medium">{t("guardrails.toolPermissionRulesEditor.defaultAction")}</Text>
<Select
disabled={disabled}
value={config.default_action}
onChange={(value) => updateConfig({ default_action: value as ToolPermissionDefaultAction })}
>
<Select.Option value="allow">Allow</Select.Option>
<Select.Option value="deny">Deny</Select.Option>
<Select.Option value="allow">{t("guardrails.toolPermissionRulesEditor.allow")}</Select.Option>
<Select.Option value="deny">{t("guardrails.toolPermissionRulesEditor.deny")}</Select.Option>
</Select>
</div>
<div>
<Text className="text-sm font-medium flex items-center gap-1">
On disallowed action
<Tooltip title="Block returns an error when a forbidden tool is invoked. Rewrite strips the tool call but lets the rest of the response continue.">
{t("guardrails.toolPermissionRulesEditor.onDisallowedAction")}
<Tooltip title={t("guardrails.toolPermissionRulesEditor.onDisallowedTooltip")}>
<InfoCircleOutlined />
</Tooltip>
</Text>
@ -299,18 +300,18 @@ const ToolPermissionRulesEditor: React.FC<ToolPermissionRulesEditorProps> = ({ v
value={config.on_disallowed_action}
onChange={(value) => updateConfig({ on_disallowed_action: value as ToolPermissionOnDisallowedAction })}
>
<Select.Option value="block">Block</Select.Option>
<Select.Option value="rewrite">Rewrite</Select.Option>
<Select.Option value="block">{t("guardrails.toolPermissionRulesEditor.block")}</Select.Option>
<Select.Option value="rewrite">{t("guardrails.toolPermissionRulesEditor.rewrite")}</Select.Option>
</Select>
</div>
</div>
<div className="mt-4">
<Text className="text-sm font-medium">Violation message (optional)</Text>
<Text className="text-sm font-medium">{t("guardrails.toolPermissionRulesEditor.violationMessage")}</Text>
<Input.TextArea
disabled={disabled}
rows={3}
placeholder="This violates our org policy..."
placeholder={t("guardrails.toolPermissionRulesEditor.violationMessagePlaceholder")}
value={config.violation_message_template}
onChange={(e) => updateConfig({ violation_message_template: e.target.value })}
/>

View file

@ -1365,6 +1365,100 @@
"addAtLeastOneRule": "Add at least one tool permission rule",
"createSuccess": "Guardrail created successfully",
"createFailed": "Failed to create guardrail: {{error}}"
},
"guardrailOptionalParams": {
"title": "Optional Parameters",
"defaultDescription": "Configure additional settings for this guardrail provider",
"enterValue": "Enter {{key}} value",
"selectValue": "Select {{key}} value",
"selectCategoryPlaceholder": "Select category to configure",
"selectCategoryHint": "Select a category to add threshold configuration",
"fieldRequired": "{{fieldKey}} is required"
},
"guardrailProviderFields": {
"loadFailed": "Failed to load provider parameters",
"loadingTip": "Loading provider parameters...",
"noFields": "No configuration fields available for this provider.",
"fieldRequired": "{{fieldKey}} is required"
},
"guardrailTable": {
"colGuardrailId": "Guardrail ID",
"colName": "Name",
"colProvider": "Provider",
"colMode": "Mode",
"colDefaultOn": "Default On",
"defaultOn": "Default On",
"defaultOff": "Default Off",
"deleteTooltip": "Delete guardrail",
"configDeleteTooltip": "Config guardrail cannot be deleted on the dashboard. Please delete it from the config file.",
"configDeleteAriaLabel": "Delete guardrail (config)",
"unnamedGuardrail": "Unnamed Guardrail",
"noGuardrails": "No guardrails found"
},
"lLMJudgeFields": {
"description": "After each LLM response, the <strong>Judge Model</strong> scores it 0–100 against your criteria. If the weighted average falls below the threshold, the response is blocked (or logged).",
"judgeModelLabel": "Judge Model",
"judgeModelTooltip": "The LLM that reads each response and grades it. Pick a capable model — it never sees end-user data beyond what the LLM returned.",
"judgeModelRequired": "Select a judge model",
"judgeModelPlaceholder": "Select a model",
"minScoreLabel": "Minimum Score to Pass",
"minScoreTooltip": "0–100. If the weighted average of criterion scores falls below this, the guardrail triggers. 80 is a good default.",
"onFailureLabel": "On Failure",
"onFailureTooltip": "Block: return HTTP 422 when the score is too low. Log: record the result but let the response through.",
"onFailureBlock": "Block (return 422)",
"onFailureLog": "Log only",
"criteriaLabel": "Evaluation Criteria",
"criteriaTooltip": "Each criterion is something the judge checks. Weights must add up to 100%.",
"criterionNameRequired": "Enter criterion name",
"criterionNamePlaceholder": "Criterion name (e.g. Policy accuracy)",
"weightLabel": "Weight",
"weightTooltip": "How much this criterion counts toward the final score. All weights must add up to 100%.",
"weightRequired": "Enter weight",
"criterionDescRequired": "Describe what to check",
"criterionDescPlaceholder": "What should the judge check for this criterion?",
"addCriterion": "Add Criterion",
"weightsTotal": "Weights total: {{weightTotal}}%",
"weightsMustSum": "must add up to 100%"
},
"piiComponents": {
"filterByCategory": "Filter by category",
"selectCategoriesPlaceholder": "Select categories to filter by",
"quickActions": "Quick Actions",
"quickActionsTooltip": "Apply action to all PII types at once",
"unselectAll": "Unselect All",
"selectAllMask": "Select All & Mask",
"selectAllBlock": "Select All & Block",
"piiTypeHeader": "PII Type",
"actionHeader": "Action",
"noMatch": "No PII types match your filter criteria"
},
"piiConfiguration": {
"title": "Configure PII Protection",
"itemsSelected_one": "{{count}} item selected",
"itemsSelected_other": "{{count}} items selected"
},
"toolPermissionRulesEditor": {
"title": "LiteLLM Tool Permission Guardrail",
"subtitle": "Provide regex patterns (e.g., ^mcp__github_.*$) for tool names or types and optionally constrain payload fields.",
"addRule": "Add Rule",
"noRules": "No tool rules added yet",
"ruleLabel": "Rule {{index}}",
"ruleId": "Rule ID",
"toolName": "Tool Name (optional)",
"toolType": "Tool Type (optional)",
"decision": "Decision",
"allow": "Allow",
"deny": "Deny",
"restrictArgs": "+ Restrict tool arguments (optional)",
"argConstraints": "Argument constraints (dot or array paths)",
"addConstraint": "+ Add another constraint",
"defaultAction": "Default action",
"onDisallowedAction": "On disallowed action",
"onDisallowedTooltip": "Block returns an error when a forbidden tool is invoked. Rewrite strips the tool call but lets the rest of the response continue.",
"block": "Block",
"rewrite": "Rewrite",
"violationMessage": "Violation message (optional)",
"violationMessagePlaceholder": "This violates our org policy..."
}
},
"keyValueInput": {

View file

@ -1365,6 +1365,100 @@
"addAtLeastOneRule": "请至少添加一条工具权限规则",
"createSuccess": "护栏创建成功",
"createFailed": "创建护栏失败:{{error}}"
},
"guardrailOptionalParams": {
"title": "可选参数",
"defaultDescription": "为此护栏提供商配置附加设置",
"enterValue": "输入 {{key}} 的值",
"selectValue": "选择 {{key}} 的值",
"selectCategoryPlaceholder": "选择要配置的类别",
"selectCategoryHint": "选择类别以添加阈值配置",
"fieldRequired": "{{fieldKey}} 为必填项"
},
"guardrailProviderFields": {
"loadFailed": "加载提供商参数失败",
"loadingTip": "正在加载提供商参数...",
"noFields": "此提供商暂无可配置字段。",
"fieldRequired": "{{fieldKey}} 为必填项"
},
"guardrailTable": {
"colGuardrailId": "护栏 ID",
"colName": "名称",
"colProvider": "提供商",
"colMode": "模式",
"colDefaultOn": "默认启用",
"defaultOn": "默认启用",
"defaultOff": "默认关闭",
"deleteTooltip": "删除护栏",
"configDeleteTooltip": "配置文件中的护栏无法在仪表盘上删除,请从配置文件中删除。",
"configDeleteAriaLabel": "删除护栏(配置文件)",
"unnamedGuardrail": "未命名护栏",
"noGuardrails": "未找到护栏"
},
"lLMJudgeFields": {
"description": "每次 LLM 响应后,<strong>Judge Model</strong> 会根据您的标准对其评分(0–100)。若加权平均分低于阈值,响应将被拦截(或记录)。",
"judgeModelLabel": "Judge Model",
"judgeModelTooltip": "读取每条响应并评分的 LLM。请选择能力较强的模型——它仅能看到 LLM 返回的内容,不会接触最终用户数据。",
"judgeModelRequired": "请选择 Judge Model",
"judgeModelPlaceholder": "选择模型",
"minScoreLabel": "最低通过分数",
"minScoreTooltip": "0–100。若标准分数的加权平均值低于此值,护栏将触发。建议默认值为 80。",
"onFailureLabel": "失败时的处理",
"onFailureTooltip": "拦截:分数过低时返回 HTTP 422。仅记录:记录结果但允许响应通过。",
"onFailureBlock": "拦截(返回 422)",
"onFailureLog": "仅记录",
"criteriaLabel": "评估标准",
"criteriaTooltip": "每个标准是 Judge 需要检查的一项内容,权重之和必须为 100%。",
"criterionNameRequired": "请输入标准名称",
"criterionNamePlaceholder": "标准名称(例如:策略准确性)",
"weightLabel": "权重",
"weightTooltip": "此标准在最终分数中所占的比重,所有权重之和必须为 100%。",
"weightRequired": "请输入权重",
"criterionDescRequired": "请描述要检查的内容",
"criterionDescPlaceholder": "Judge 应针对此标准检查什么?",
"addCriterion": "添加标准",
"weightsTotal": "权重合计:{{weightTotal}}%",
"weightsMustSum": "必须合计为 100%"
},
"piiComponents": {
"filterByCategory": "按类别筛选",
"selectCategoriesPlaceholder": "选择要筛选的类别",
"quickActions": "快速操作",
"quickActionsTooltip": "一次性对所有 PII 类型应用操作",
"unselectAll": "取消全选",
"selectAllMask": "全选并脱敏",
"selectAllBlock": "全选并拦截",
"piiTypeHeader": "PII 类型",
"actionHeader": "操作",
"noMatch": "没有 PII 类型匹配您的筛选条件"
},
"piiConfiguration": {
"title": "配置 PII 保护",
"itemsSelected_one": "已选择 {{count}} 项",
"itemsSelected_other": "已选择 {{count}} 项"
},
"toolPermissionRulesEditor": {
"title": "LiteLLM 工具权限护栏",
"subtitle": "为工具名称或类型提供正则表达式模式(例如 ^mcp__github_.*$),并可选择性地约束载荷字段。",
"addRule": "添加规则",
"noRules": "尚未添加工具规则",
"ruleLabel": "规则 {{index}}",
"ruleId": "规则 ID",
"toolName": "工具名称(可选)",
"toolType": "工具类型(可选)",
"decision": "决策",
"allow": "允许",
"deny": "拒绝",
"restrictArgs": "+ 限制工具参数(可选)",
"argConstraints": "参数约束(点路径或数组路径)",
"addConstraint": "+ 添加另一个约束",
"defaultAction": "默认动作",
"onDisallowedAction": "禁止时的处理",
"onDisallowedTooltip": "拦截:调用被禁止工具时返回错误。改写:去除工具调用但允许响应的其余部分继续。",
"block": "拦截",
"rewrite": "改写",
"violationMessage": "违规消息(可选)",
"violationMessagePlaceholder": "这违反了我们的组织策略..."
}
},
"keyValueInput": {