feat(ui): i18n for view_logs batch 1

This commit is contained in:
吹雪 2026-06-10 14:38:43 +09:00
parent e93492321a
commit e6704034de
8 changed files with 410 additions and 129 deletions

View file

@ -2,6 +2,7 @@ import { Drawer, Tag, Typography } from "antd";
import { CloseOutlined, CopyOutlined, CheckOutlined } from "@ant-design/icons";
import { useState, useCallback } from "react";
import moment from "moment";
import { useTranslation } from "react-i18next";
import { AuditLogEntry } from "../columns";
import DefaultProxyAdminTag from "../../common_components/DefaultProxyAdminTag";
@ -13,14 +14,6 @@ interface AuditLogDrawerProps {
log: AuditLogEntry | null;
}
const TABLE_NAME_DISPLAY: Record<string, string> = {
LiteLLM_VerificationToken: "Keys",
LiteLLM_TeamTable: "Teams",
LiteLLM_UserTable: "Users",
LiteLLM_OrganizationTable: "Organizations",
LiteLLM_ProxyModelTable: "Models",
};
const ACTION_COLOR: Record<string, string> = {
created: "green",
updated: "blue",
@ -29,6 +22,7 @@ const ACTION_COLOR: Record<string, string> = {
};
function CopyableJsonBlock({ label, value }: { label: string; value: Record<string, any> }) {
const { t } = useTranslation();
const [copied, setCopied] = useState(false);
const handleCopy = useCallback(async () => {
@ -61,7 +55,7 @@ function CopyableJsonBlock({ label, value }: { label: string; value: Record<stri
<button
onClick={handleCopy}
className="p-1 hover:bg-gray-200 rounded text-gray-500 hover:text-gray-700 transition-colors"
title="Copy JSON"
title={t("viewLogs.auditLogDrawer.copyJson")}
>
{copied ? <CheckOutlined className="text-green-600" /> : <CopyOutlined />}
</button>
@ -83,6 +77,7 @@ function MetadataRow({ label, value }: { label: string; value: React.ReactNode }
}
function DiffSection({ log }: { log: AuditLogEntry }) {
const { t } = useTranslation();
const { action, table_name, before_value, updated_values } = log;
const isKeyTable = table_name === "LiteLLM_VerificationToken";
const isUpdateAction = action === "updated" || action === "rotated";
@ -120,8 +115,10 @@ function DiffSection({ log }: { log: AuditLogEntry }) {
}
});
displayBefore = Object.keys(changedBefore).length > 0 ? changedBefore : { note: "No differing fields detected" };
displayAfter = Object.keys(changedAfter).length > 0 ? changedAfter : { note: "No differing fields detected" };
displayBefore =
Object.keys(changedBefore).length > 0 ? changedBefore : { note: t("viewLogs.auditLogDrawer.noDifferingFields") };
displayAfter =
Object.keys(changedAfter).length > 0 ? changedAfter : { note: t("viewLogs.auditLogDrawer.noDifferingFields") };
}
const renderValue = (label: string, value: Record<string, any> | null | undefined) => {
@ -149,17 +146,20 @@ function DiffSection({ log }: { log: AuditLogEntry }) {
<div className="px-3 py-3 space-y-1 text-xs">
{value.token !== undefined && (
<p>
<span className="text-gray-500">Token:</span> {value.token ?? "N/A"}
<span className="text-gray-500">{t("viewLogs.auditLogDrawer.tokenLabel")}</span>{" "}
{value.token ?? "N/A"}
</p>
)}
{value.spend !== undefined && (
<p>
<span className="text-gray-500">Spend:</span> ${Number(value.spend).toFixed(6)}
<span className="text-gray-500">{t("viewLogs.auditLogDrawer.spendLabel")}</span> $
{Number(value.spend).toFixed(6)}
</p>
)}
{value.max_budget !== undefined && (
<p>
<span className="text-gray-500">Max Budget:</span> ${Number(value.max_budget).toFixed(6)}
<span className="text-gray-500">{t("viewLogs.auditLogDrawer.maxBudgetLabel")}</span> $
{Number(value.max_budget).toFixed(6)}
</p>
)}
</div>
@ -173,13 +173,23 @@ function DiffSection({ log }: { log: AuditLogEntry }) {
return (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
{renderValue("Before", displayBefore)}
{renderValue("After", displayAfter)}
{renderValue(t("viewLogs.auditLogDrawer.before"), displayBefore)}
{renderValue(t("viewLogs.auditLogDrawer.after"), displayAfter)}
</div>
);
}
export function AuditLogDrawer({ open, onClose, log }: AuditLogDrawerProps) {
const { t } = useTranslation();
const TABLE_NAME_DISPLAY: Record<string, string> = {
LiteLLM_VerificationToken: t("viewLogs.auditLogDrawer.tableKeys"),
LiteLLM_TeamTable: t("viewLogs.auditLogDrawer.tableTeams"),
LiteLLM_UserTable: t("viewLogs.auditLogDrawer.tableUsers"),
LiteLLM_OrganizationTable: t("viewLogs.auditLogDrawer.tableOrganizations"),
LiteLLM_ProxyModelTable: t("viewLogs.auditLogDrawer.tableModels"),
};
if (!log) return null;
const tableDisplay = TABLE_NAME_DISPLAY[log.table_name] ?? log.table_name;
@ -209,7 +219,7 @@ export function AuditLogDrawer({ open, onClose, log }: AuditLogDrawerProps) {
<button
onClick={onClose}
className="w-8 h-8 flex items-center justify-center rounded hover:bg-gray-100 text-gray-500"
aria-label="Close"
aria-label={t("common.close")}
>
<CloseOutlined />
</button>
@ -219,19 +229,22 @@ export function AuditLogDrawer({ open, onClose, log }: AuditLogDrawerProps) {
<div className="px-6 py-5">
{/* Metadata */}
<div className="bg-gray-50 border rounded-lg p-4 mb-5">
<p className="text-xs font-semibold text-gray-700 mb-2 uppercase tracking-wide">Details</p>
<MetadataRow label="Table" value={tableDisplay} />
<p className="text-xs font-semibold text-gray-700 mb-2 uppercase tracking-wide">{t("common.details")}</p>
<MetadataRow label={t("viewLogs.auditLogDrawer.labelTable")} value={tableDisplay} />
<MetadataRow
label="Object ID"
label={t("viewLogs.auditLogDrawer.labelObjectId")}
value={
<Text copyable className="font-mono text-xs">
{log.object_id}
</Text>
}
/>
<MetadataRow label="Changed By" value={<DefaultProxyAdminTag userId={log.changed_by} />} />
<MetadataRow
label="API Key (Hash)"
label={t("viewLogs.auditLogDrawer.labelChangedBy")}
value={<DefaultProxyAdminTag userId={log.changed_by} />}
/>
<MetadataRow
label={t("viewLogs.auditLogDrawer.labelApiKeyHash")}
value={
log.changed_by_api_key ? (
<Text copyable className="font-mono text-xs break-all">

View file

@ -1,10 +1,13 @@
import React from "react";
import { Trans, useTranslation } from "react-i18next";
interface ConfigInfoMessageProps {
show: boolean;
}
export const ConfigInfoMessage: React.FC<ConfigInfoMessageProps> = ({ show }) => {
const { t } = useTranslation();
if (!show) return null;
return (
@ -27,20 +30,22 @@ export const ConfigInfoMessage: React.FC<ConfigInfoMessageProps> = ({ show }) =>
</svg>
</div>
<div>
<h4 className="text-sm font-medium text-blue-800">Request/Response Data Not Available</h4>
<h4 className="text-sm font-medium text-blue-800">{t("viewLogs.configInfoMessage.title")}</h4>
<p className="text-sm text-blue-700 mt-1">
To view request and response details, enable prompt storage in your LiteLLM configuration by adding the
following to your <code className="bg-blue-100 px-1 py-0.5 rounded">proxy_config.yaml</code> file, or toggle
the setting in <strong>Admin Settings → Logging Settings</strong>.
<Trans
i18nKey="viewLogs.configInfoMessage.description"
components={{
configFile: <code className="bg-blue-100 px-1 py-0.5 rounded" />,
settingsPath: <strong />,
}}
/>
</p>
<pre className="mt-2 bg-white p-3 rounded border border-blue-200 text-xs font-mono overflow-auto">
{`general_settings:
store_model_in_db: true
store_prompts_in_spend_logs: true`}
</pre>
<p className="text-xs text-blue-700 mt-2">
Note: This will only affect new requests after the configuration change.
</p>
<p className="text-xs text-blue-700 mt-2">{t("viewLogs.configInfoMessage.note")}</p>
</div>
</div>
);

View file

@ -1,5 +1,6 @@
import React from "react";
import { Collapse } from "antd";
import { useTranslation } from "react-i18next";
import { formatNumberWithCommas } from "@/utils/dataUtils";
export interface CostBreakdown {
@ -49,6 +50,7 @@ export const CostBreakdownViewer: React.FC<CostBreakdownViewerProps> = ({
cacheReadTokens,
cacheCreationTokens,
}) => {
const { t } = useTranslation();
const isCached = cacheHit?.toLowerCase() === "true";
const hasTokenCounts = promptTokens !== undefined || completionTokens !== undefined;
@ -97,12 +99,12 @@ export const CostBreakdownViewer: React.FC<CostBreakdownViewerProps> = ({
key: "1",
label: (
<div className="flex items-center justify-between w-full">
<h3 className="text-lg font-medium text-gray-900">Cost Breakdown</h3>
<h3 className="text-lg font-medium text-gray-900">{t("viewLogs.costBreakdownViewer.title")}</h3>
<div className="flex items-center space-x-2 mr-4">
<span className="text-sm text-gray-500">Total:</span>
<span className="text-sm text-gray-500">{t("common.total")}:</span>
<span className="text-sm font-semibold text-gray-900">
{formatCost(totalSpend)}
{isCached && " (Cached)"}
{isCached && ` (${t("viewLogs.costBreakdownViewer.cached")})`}
</span>
</div>
</div>
@ -124,24 +126,29 @@ export const CostBreakdownViewer: React.FC<CostBreakdownViewerProps> = ({
return (
<>
<div className="flex text-sm">
<span className="text-gray-600 font-medium w-1/3">Input Cost:</span>
<span className="text-gray-600 font-medium w-1/3">
{t("viewLogs.costBreakdownViewer.inputCost")}:
</span>
<span className="text-gray-900">
{formatCost(rawCost)}
{rawInputTokens !== undefined && rawInputTokens !== null && (
<span className="text-gray-500 font-normal ml-1">
({rawInputTokens.toLocaleString()} tokens)
({rawInputTokens.toLocaleString()} {t("viewLogs.costBreakdownViewer.tokens")})
</span>
)}
</span>
</div>
{(costBreakdown?.cache_read_cost ?? 0) > 0 && (
<div className="flex text-sm">
<span className="text-gray-600 font-medium w-1/3">Cache Read Cost:</span>
<span className="text-gray-600 font-medium w-1/3">
{t("viewLogs.costBreakdownViewer.cacheReadCost")}:
</span>
<span className="text-gray-900">
{formatCost(isCached ? 0 : costBreakdown?.cache_read_cost)}
{(cacheReadTokens ?? 0) > 0 && (
<span className="text-gray-500 font-normal ml-1">
({(cacheReadTokens ?? 0).toLocaleString()} tokens)
({(cacheReadTokens ?? 0).toLocaleString()}{" "}
{t("viewLogs.costBreakdownViewer.tokens")})
</span>
)}
</span>
@ -149,12 +156,15 @@ export const CostBreakdownViewer: React.FC<CostBreakdownViewerProps> = ({
)}
{(costBreakdown?.cache_creation_cost ?? 0) > 0 && (
<div className="flex text-sm">
<span className="text-gray-600 font-medium w-1/3">Cache Write Cost:</span>
<span className="text-gray-600 font-medium w-1/3">
{t("viewLogs.costBreakdownViewer.cacheWriteCost")}:
</span>
<span className="text-gray-900">
{formatCost(isCached ? 0 : costBreakdown?.cache_creation_cost)}
{(cacheCreationTokens ?? 0) > 0 && (
<span className="text-gray-500 font-normal ml-1">
({(cacheCreationTokens ?? 0).toLocaleString()} tokens)
({(cacheCreationTokens ?? 0).toLocaleString()}{" "}
{t("viewLogs.costBreakdownViewer.tokens")})
</span>
)}
</span>
@ -165,12 +175,14 @@ export const CostBreakdownViewer: React.FC<CostBreakdownViewerProps> = ({
}
return (
<div className="flex text-sm">
<span className="text-gray-600 font-medium w-1/3">Input Cost:</span>
<span className="text-gray-600 font-medium w-1/3">
{t("viewLogs.costBreakdownViewer.inputCost")}:
</span>
<span className="text-gray-900">
{formatCost(inputCost)}
{promptTokens !== undefined && (
<span className="text-gray-500 font-normal ml-1">
({promptTokens.toLocaleString()} prompt tokens)
({promptTokens.toLocaleString()} {t("viewLogs.costBreakdownViewer.promptTokens")})
</span>
)}
</span>
@ -178,19 +190,23 @@ export const CostBreakdownViewer: React.FC<CostBreakdownViewerProps> = ({
);
})()}
<div className="flex text-sm">
<span className="text-gray-600 font-medium w-1/3">Output Cost:</span>
<span className="text-gray-600 font-medium w-1/3">
{t("viewLogs.costBreakdownViewer.outputCost")}:
</span>
<span className="text-gray-900">
{formatCost(outputCost)}
{completionTokens !== undefined && (
<span className="text-gray-500 font-normal ml-1">
({completionTokens.toLocaleString()} completion tokens)
({completionTokens.toLocaleString()} {t("viewLogs.costBreakdownViewer.completionTokens")})
</span>
)}
</span>
</div>
{costBreakdown?.tool_usage_cost !== undefined && costBreakdown.tool_usage_cost > 0 && (
<div className="flex text-sm">
<span className="text-gray-600 font-medium w-1/3">Tool Usage Cost:</span>
<span className="text-gray-600 font-medium w-1/3">
{t("viewLogs.costBreakdownViewer.toolUsageCost")}:
</span>
<span className="text-gray-900">{formatCost(costBreakdown.tool_usage_cost)}</span>
</div>
)}
@ -209,7 +225,7 @@ export const CostBreakdownViewer: React.FC<CostBreakdownViewerProps> = ({
{!isCached && (
<div className="pt-2 border-t border-gray-100 max-w-2xl">
<div className="flex text-sm font-semibold">
<span className="text-gray-900 w-1/3">Original LLM Cost:</span>
<span className="text-gray-900 w-1/3">{t("viewLogs.costBreakdownViewer.originalLlmCost")}:</span>
<span className="text-gray-900">{formatCost(originalCost)}</span>
</div>
</div>
@ -224,7 +240,8 @@ export const CostBreakdownViewer: React.FC<CostBreakdownViewerProps> = ({
{costBreakdown.discount_percent !== undefined && costBreakdown.discount_percent !== 0 && (
<div className="flex text-sm text-gray-600">
<span className="font-medium w-1/3">
Discount ({formatPercent(costBreakdown.discount_percent)}):
{t("viewLogs.costBreakdownViewer.discount")} (
{formatPercent(costBreakdown.discount_percent)}):
</span>
<span className="text-gray-900">-{formatCost(costBreakdown.discount_amount)}</span>
</div>
@ -232,7 +249,9 @@ export const CostBreakdownViewer: React.FC<CostBreakdownViewerProps> = ({
{costBreakdown.discount_amount !== undefined &&
costBreakdown.discount_percent === undefined && (
<div className="flex text-sm text-gray-600">
<span className="font-medium w-1/3">Discount Amount:</span>
<span className="font-medium w-1/3">
{t("viewLogs.costBreakdownViewer.discountAmount")}:
</span>
<span className="text-gray-900">-{formatCost(costBreakdown.discount_amount)}</span>
</div>
)}
@ -245,7 +264,8 @@ export const CostBreakdownViewer: React.FC<CostBreakdownViewerProps> = ({
{costBreakdown.margin_percent !== undefined && costBreakdown.margin_percent !== 0 && (
<div className="flex text-sm text-gray-600">
<span className="font-medium w-1/3">
Margin ({formatPercent(costBreakdown.margin_percent)}):
{t("viewLogs.costBreakdownViewer.margin")} ({formatPercent(costBreakdown.margin_percent)}
):
</span>
<span className="text-gray-900">
+
@ -257,7 +277,7 @@ export const CostBreakdownViewer: React.FC<CostBreakdownViewerProps> = ({
)}
{costBreakdown.margin_fixed_amount !== undefined && costBreakdown.margin_fixed_amount !== 0 && (
<div className="flex text-sm text-gray-600">
<span className="font-medium w-1/3">Margin:</span>
<span className="font-medium w-1/3">{t("viewLogs.costBreakdownViewer.margin")}:</span>
<span className="text-gray-900">+{formatCost(costBreakdown.margin_fixed_amount)}</span>
</div>
)}
@ -269,10 +289,12 @@ export const CostBreakdownViewer: React.FC<CostBreakdownViewerProps> = ({
{/* Final Summary */}
<div className="mt-4 pt-4 border-t border-gray-200 max-w-2xl">
<div className="flex items-center">
<span className="font-bold text-sm text-gray-900 w-1/3">Final Calculated Cost:</span>
<span className="font-bold text-sm text-gray-900 w-1/3">
{t("viewLogs.costBreakdownViewer.finalCalculatedCost")}:
</span>
<span className="text-sm font-bold text-gray-900">
{formatCost(totalCost)}
{isCached && " (Cached)"}
{isCached && ` (${t("viewLogs.costBreakdownViewer.cached")})`}
</span>
</div>
</div>

View file

@ -1,4 +1,5 @@
import React from "react";
import { useTranslation } from "react-i18next";
interface ErrorViewerProps {
errorInfo: {
@ -11,6 +12,7 @@ interface ErrorViewerProps {
}
export const ErrorViewer: React.FC<ErrorViewerProps> = ({ errorInfo }) => {
const { t } = useTranslation();
const [expandedFrames, setExpandedFrames] = React.useState<{ [key: number]: boolean }>({});
const [allExpanded, setAllExpanded] = React.useState(false);
@ -89,20 +91,20 @@ export const ErrorViewer: React.FC<ErrorViewerProps> = ({ errorInfo }) => {
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
Error Details
{t("viewLogs.errorViewer.title")}
</h3>
</div>
<div className="p-4">
<div className="bg-red-50 rounded-md p-4 mb-4">
<div className="flex">
<span className="text-red-800 font-medium w-20">Type:</span>
<span className="text-red-700">{errorInfo.error_class || "Unknown Error"}</span>
<span className="text-red-800 font-medium w-20">{t("viewLogs.errorViewer.type")}:</span>
<span className="text-red-700">{errorInfo.error_class || t("viewLogs.errorViewer.unknownError")}</span>
</div>
<div className="flex mt-2">
<span className="text-red-800 font-medium w-20 flex-shrink-0">Message:</span>
<span className="text-red-800 font-medium w-20 flex-shrink-0">{t("viewLogs.errorViewer.message")}:</span>
<span className="text-red-700 break-words whitespace-pre-wrap">
{errorInfo.error_message || "Unknown error occurred"}
{errorInfo.error_message || t("viewLogs.errorViewer.unknownErrorOccurred")}
</span>
</div>
</div>
@ -110,18 +112,18 @@ export const ErrorViewer: React.FC<ErrorViewerProps> = ({ errorInfo }) => {
{errorInfo.traceback && (
<div className="mt-4">
<div className="flex justify-between items-center mb-2">
<h4 className="font-medium">Traceback</h4>
<h4 className="font-medium">{t("viewLogs.errorViewer.traceback")}</h4>
<div className="flex items-center space-x-4">
<button
onClick={toggleAllFrames}
className="text-gray-500 hover:text-gray-700 flex items-center text-sm"
>
{allExpanded ? "Collapse All" : "Expand All"}
{allExpanded ? t("viewLogs.errorViewer.collapseAll") : t("viewLogs.errorViewer.expandAll")}
</button>
<button
onClick={() => navigator.clipboard.writeText(errorInfo.traceback || "")}
className="text-gray-500 hover:text-gray-700 flex items-center"
title="Copy traceback"
title={t("viewLogs.errorViewer.copyTraceback")}
>
<svg
xmlns="http://www.w3.org/2000/svg"
@ -137,7 +139,7 @@ export const ErrorViewer: React.FC<ErrorViewerProps> = ({ errorInfo }) => {
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
<span className="ml-1">Copy</span>
<span className="ml-1">{t("common.copy")}</span>
</button>
</div>
</div>

View file

@ -1,6 +1,7 @@
import React from "react";
import { Card, Tag, Table, Typography, Space, Tooltip } from "antd";
import { CheckCircleOutlined, CloseCircleOutlined, ExperimentOutlined } from "@ant-design/icons";
import { useTranslation } from "react-i18next";
const { Text } = Typography;
@ -29,6 +30,7 @@ interface EvalViewerProps {
}
export default function EvalViewer({ data }: EvalViewerProps) {
const { t } = useTranslation();
const entries: EvalInformation[] = Array.isArray(data) ? data : [data];
if (!entries.length) return null;
@ -38,7 +40,7 @@ export default function EvalViewer({ data }: EvalViewerProps) {
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 12 }}>
<ExperimentOutlined style={{ fontSize: 16, color: "#6366f1" }} />
<Text strong style={{ fontSize: 15 }}>
LLM Judge Results
{t("viewLogs.evalViewer.title")}
</Text>
</div>
@ -50,6 +52,7 @@ export default function EvalViewer({ data }: EvalViewerProps) {
}
function EvalEntryCard({ entry }: { entry: EvalInformation }) {
const { t } = useTranslation();
const passed = entry.passed;
const scoreColor = passed ? "#52c41a" : "#ff4d4f";
@ -58,7 +61,7 @@ function EvalEntryCard({ entry }: { entry: EvalInformation }) {
const columns = [
{
title: "Criterion",
title: t("viewLogs.evalViewer.colCriterion"),
dataIndex: "criterion_name",
key: "criterion_name",
width: 160,
@ -69,7 +72,7 @@ function EvalEntryCard({ entry }: { entry: EvalInformation }) {
),
},
{
title: "Weight",
title: t("viewLogs.evalViewer.colWeight"),
dataIndex: "weight",
key: "weight",
width: 65,
@ -81,7 +84,7 @@ function EvalEntryCard({ entry }: { entry: EvalInformation }) {
) : null,
},
{
title: "Score",
title: t("viewLogs.evalViewer.colScore"),
dataIndex: "score",
key: "score",
width: 65,
@ -91,8 +94,10 @@ function EvalEntryCard({ entry }: { entry: EvalInformation }) {
},
{
title: (
<Tooltip title="Score × Weight — how much each criterion contributes to the final score">
<span style={{ borderBottom: "1px dashed #aaa", cursor: "help" }}>Weighted</span>
<Tooltip title={t("viewLogs.evalViewer.weightedTooltip")}>
<span style={{ borderBottom: "1px dashed #aaa", cursor: "help" }}>
{t("viewLogs.evalViewer.colWeighted")}
</span>
</Tooltip>
),
key: "weighted",
@ -108,7 +113,7 @@ function EvalEntryCard({ entry }: { entry: EvalInformation }) {
},
},
{
title: "Comment",
title: t("viewLogs.evalViewer.colComment"),
dataIndex: "reasoning",
key: "reasoning",
ellipsis: { showTitle: false },
@ -133,13 +138,13 @@ function EvalEntryCard({ entry }: { entry: EvalInformation }) {
<CloseCircleOutlined style={{ color: "#ff4d4f" }} />
)}
<Text strong>{entry.eval_name}</Text>
<Tag color={passed ? "success" : "error"}>{passed ? "PASSED" : "FAILED"}</Tag>
<Tooltip
title={`Weighted average of all criterion scores. Each criterion has a weight (%) set when the eval was created — higher-weight criteria count more toward the final score.`}
>
<Tag color={passed ? "success" : "error"}>
{passed ? t("viewLogs.evalViewer.passed") : t("viewLogs.evalViewer.failed")}
</Tag>
<Tooltip title={t("viewLogs.evalViewer.overallScoreTooltip")}>
<Text type="secondary" style={{ fontSize: 12, cursor: "help", borderBottom: "1px dashed #aaa" }}>
{entry.overall_score?.toFixed(0)} / 100
{entry.threshold != null && ` (threshold: ${entry.threshold})`}
{entry.threshold != null && ` (${t("viewLogs.evalViewer.threshold")}: ${entry.threshold})`}
</Text>
</Tooltip>
</Space>
@ -148,12 +153,12 @@ function EvalEntryCard({ entry }: { entry: EvalInformation }) {
<Space size="small">
{entry.judge_model && (
<Text type="secondary" style={{ fontSize: 12 }}>
Judge: {entry.judge_model}
{t("viewLogs.evalViewer.judge")}: {entry.judge_model}
</Text>
)}
{entry.iteration != null && (
<Text type="secondary" style={{ fontSize: 12 }}>
Iter: {entry.iteration + 1}
{t("viewLogs.evalViewer.iter")}: {entry.iteration + 1}
</Text>
)}
</Space>
@ -161,7 +166,7 @@ function EvalEntryCard({ entry }: { entry: EvalInformation }) {
>
{entry.eval_error && (
<Text type="warning" style={{ display: "block", marginBottom: 8, fontSize: 12 }}>
Judge error: {entry.eval_error}
{t("viewLogs.evalViewer.judgeError")}: {entry.eval_error}
</Text>
)}
@ -181,7 +186,7 @@ function EvalEntryCard({ entry }: { entry: EvalInformation }) {
<Table.Summary.Row>
<Table.Summary.Cell index={0}>
<Text strong style={{ fontSize: 12 }}>
Total
{t("common.total")}
</Text>
</Table.Summary.Cell>
<Table.Summary.Cell index={1} />
@ -198,7 +203,7 @@ function EvalEntryCard({ entry }: { entry: EvalInformation }) {
/>
) : (
<Text type="secondary" style={{ fontSize: 12 }}>
Score: {entry.overall_score?.toFixed(1)} — no per-criterion breakdown available.
{t("viewLogs.evalViewer.scoreNoCriterion", { score: entry.overall_score?.toFixed(1) })}
</Text>
)}
</Card>

View file

@ -1,4 +1,5 @@
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
export type BedrockGuardrailAction = "NONE" | "GUARDRAIL_INTERVENED";
@ -114,7 +115,10 @@ const chip = (text: React.ReactNode, tone: ChipTone = "slate") => {
return <span className={`px-2 py-1 rounded-md text-xs font-medium inline-block ${map[tone]}`}>{text}</span>;
};
const boolPill = (b?: boolean) => (b ? chip("detected", "red") : chip("not detected", "slate"));
const boolPill = (b: boolean | undefined, t: (key: string) => string) =>
b
? chip(t("viewLogs.bedrockGuardrailDetails.detected"), "red")
: chip(t("viewLogs.bedrockGuardrailDetails.notDetected"), "slate");
interface SectionProps {
title: string;
@ -169,6 +173,8 @@ const Divider: React.FC = () => <div className="my-3 border-t" />;
/** ====== Main component ====== */
export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailResponse }> = ({ response }) => {
const { t } = useTranslation();
if (!response) return null;
const outputs: BedrockOutputContent[] = (response.outputs ?? response.output ?? []) as BedrockOutputContent[];
@ -179,12 +185,18 @@ export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailRespo
<div className="flex flex-wrap gap-2">
{response.guardrailCoverage?.textCharacters &&
chip(
`text guarded ${response.guardrailCoverage.textCharacters.guarded ?? 0}/${response.guardrailCoverage.textCharacters.total ?? 0}`,
t("viewLogs.bedrockGuardrailDetails.textGuarded", {
guarded: response.guardrailCoverage.textCharacters.guarded ?? 0,
total: response.guardrailCoverage.textCharacters.total ?? 0,
}),
"blue",
)}
{response.guardrailCoverage?.images &&
chip(
`images guarded ${response.guardrailCoverage.images.guarded ?? 0}/${response.guardrailCoverage.images.total ?? 0}`,
t("viewLogs.bedrockGuardrailDetails.imagesGuarded", {
guarded: response.guardrailCoverage.images.guarded ?? 0,
total: response.guardrailCoverage.images.total ?? 0,
}),
"blue",
)}
</div>
@ -208,17 +220,21 @@ export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailRespo
<div className="border rounded-lg p-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<KV label="Action:">{chip(response.action ?? "N/A", actionTone)}</KV>
{response.actionReason && <KV label="Action Reason:">{response.actionReason}</KV>}
<KV label={t("viewLogs.bedrockGuardrailDetails.action") + ":"}>
{chip(response.action ?? "N/A", actionTone)}
</KV>
{response.actionReason && (
<KV label={t("viewLogs.bedrockGuardrailDetails.actionReason") + ":"}>{response.actionReason}</KV>
)}
{response.blockedResponse && (
<KV label="Blocked Response:">
<KV label={t("viewLogs.bedrockGuardrailDetails.blockedResponse") + ":"}>
<span className="italic">{response.blockedResponse}</span>
</KV>
)}
</div>
<div className="space-y-2">
<KV label="Coverage:">{coverageChips}</KV>
<KV label="Usage:">{usagePills}</KV>
<KV label={t("viewLogs.bedrockGuardrailDetails.coverage") + ":"}>{coverageChips}</KV>
<KV label={t("viewLogs.bedrockGuardrailDetails.usage") + ":"}>{usagePills}</KV>
</div>
</div>
@ -226,11 +242,13 @@ export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailRespo
{outputs.length > 0 && (
<>
<Divider />
<h4 className="font-medium mb-2">Outputs</h4>
<h4 className="font-medium mb-2">{t("viewLogs.bedrockGuardrailDetails.outputs")}</h4>
<div className="space-y-2">
{outputs.map((o, i) => (
<div key={i} className="p-3 bg-gray-50 rounded-md">
<div className="text-sm whitespace-pre-wrap">{o.text ?? <em>(non-text output)</em>}</div>
<div className="text-sm whitespace-pre-wrap">
{o.text ?? <em>{t("viewLogs.bedrockGuardrailDetails.nonTextOutput")}</em>}
</div>
</div>
))}
</div>
@ -256,7 +274,7 @@ export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailRespo
return (
<Section
key={idx}
title={`Assessment #${idx + 1}`}
title={t("viewLogs.bedrockGuardrailDetails.assessmentTitle", { number: idx + 1 })}
defaultOpen
right={
<div className="flex items-center gap-3">
@ -269,9 +287,9 @@ export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailRespo
{/* Word policy */}
{assess.wordPolicy && (
<div className="mb-3">
<h6 className="font-medium mb-2">Word Policy</h6>
<h6 className="font-medium mb-2">{t("viewLogs.bedrockGuardrailDetails.wordPolicy")}</h6>
{(assess.wordPolicy.customWords?.length ?? 0) > 0 && (
<Section title="Custom Words" defaultOpen>
<Section title={t("viewLogs.bedrockGuardrailDetails.customWords")} defaultOpen>
<div className="space-y-2">
{assess.wordPolicy.customWords!.map((w, i) => (
<div key={i} className="flex justify-between items-center p-2 bg-gray-50 rounded">
@ -279,14 +297,14 @@ export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailRespo
{chip(w.action ?? "N/A", w.detected ? "red" : "slate")}
<span className="font-mono text-sm break-all">{w.match}</span>
</div>
{boolPill(w.detected)}
{boolPill(w.detected, t)}
</div>
))}
</div>
</Section>
)}
{(assess.wordPolicy.managedWordLists?.length ?? 0) > 0 && (
<Section title="Managed Word Lists" defaultOpen={false}>
<Section title={t("viewLogs.bedrockGuardrailDetails.managedWordLists")} defaultOpen={false}>
<div className="space-y-2">
{assess.wordPolicy.managedWordLists!.map((w, i) => (
<div key={i} className="flex justify-between items-center p-2 bg-gray-50 rounded">
@ -295,7 +313,7 @@ export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailRespo
<span className="font-mono text-sm break-all">{w.match}</span>
{w.type && chip(w.type, "slate")}
</div>
{boolPill(w.detected)}
{boolPill(w.detected, t)}
</div>
))}
</div>
@ -307,16 +325,16 @@ export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailRespo
{/* Content policy */}
{assess.contentPolicy?.filters?.length ? (
<div className="mb-3">
<h6 className="font-medium mb-2">Content Policy</h6>
<h6 className="font-medium mb-2">{t("viewLogs.bedrockGuardrailDetails.contentPolicy")}</h6>
<div className="overflow-x-auto">
<table className="min-w-full text-sm">
<thead>
<tr className="text-left text-gray-600">
<th className="py-1 pr-4">Type</th>
<th className="py-1 pr-4">Action</th>
<th className="py-1 pr-4">Detected</th>
<th className="py-1 pr-4">Strength</th>
<th className="py-1 pr-4">Confidence</th>
<th className="py-1 pr-4">{t("viewLogs.bedrockGuardrailDetails.colType")}</th>
<th className="py-1 pr-4">{t("viewLogs.bedrockGuardrailDetails.colAction")}</th>
<th className="py-1 pr-4">{t("viewLogs.bedrockGuardrailDetails.colDetected")}</th>
<th className="py-1 pr-4">{t("viewLogs.bedrockGuardrailDetails.colStrength")}</th>
<th className="py-1 pr-4">{t("viewLogs.bedrockGuardrailDetails.colConfidence")}</th>
</tr>
</thead>
<tbody>
@ -324,7 +342,7 @@ export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailRespo
<tr key={i} className="border-t">
<td className="py-1 pr-4">{f.type ?? "—"}</td>
<td className="py-1 pr-4">{chip(f.action ?? "—", f.detected ? "red" : "slate")}</td>
<td className="py-1 pr-4">{boolPill(f.detected)}</td>
<td className="py-1 pr-4">{boolPill(f.detected, t)}</td>
<td className="py-1 pr-4">{f.filterStrength ?? "—"}</td>
<td className="py-1 pr-4">{f.confidence ?? "—"}</td>
</tr>
@ -338,16 +356,16 @@ export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailRespo
{/* Contextual grounding */}
{assess.contextualGroundingPolicy?.filters?.length ? (
<div className="mb-3">
<h6 className="font-medium mb-2">Contextual Grounding</h6>
<h6 className="font-medium mb-2">{t("viewLogs.bedrockGuardrailDetails.contextualGrounding")}</h6>
<div className="overflow-x-auto">
<table className="min-w-full text-sm">
<thead>
<tr className="text-left text-gray-600">
<th className="py-1 pr-4">Type</th>
<th className="py-1 pr-4">Action</th>
<th className="py-1 pr-4">Detected</th>
<th className="py-1 pr-4">Score</th>
<th className="py-1 pr-4">Threshold</th>
<th className="py-1 pr-4">{t("viewLogs.bedrockGuardrailDetails.colType")}</th>
<th className="py-1 pr-4">{t("viewLogs.bedrockGuardrailDetails.colAction")}</th>
<th className="py-1 pr-4">{t("viewLogs.bedrockGuardrailDetails.colDetected")}</th>
<th className="py-1 pr-4">{t("viewLogs.bedrockGuardrailDetails.colScore")}</th>
<th className="py-1 pr-4">{t("viewLogs.bedrockGuardrailDetails.colThreshold")}</th>
</tr>
</thead>
<tbody>
@ -355,7 +373,7 @@ export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailRespo
<tr key={i} className="border-t">
<td className="py-1 pr-4">{f.type ?? "—"}</td>
<td className="py-1 pr-4">{chip(f.action ?? "—", f.detected ? "red" : "slate")}</td>
<td className="py-1 pr-4">{boolPill(f.detected)}</td>
<td className="py-1 pr-4">{boolPill(f.detected, t)}</td>
<td className="py-1 pr-4">{f.score ?? "—"}</td>
<td className="py-1 pr-4">{f.threshold ?? "—"}</td>
</tr>
@ -369,9 +387,9 @@ export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailRespo
{/* Sensitive Information */}
{assess.sensitiveInformationPolicy && (
<div className="mb-3">
<h6 className="font-medium mb-2">Sensitive Information</h6>
<h6 className="font-medium mb-2">{t("viewLogs.bedrockGuardrailDetails.sensitiveInformation")}</h6>
{(assess.sensitiveInformationPolicy.piiEntities?.length ?? 0) > 0 && (
<Section title="PII Entities" defaultOpen>
<Section title={t("viewLogs.bedrockGuardrailDetails.piiEntities")} defaultOpen>
<div className="space-y-2">
{assess.sensitiveInformationPolicy.piiEntities!.map((p, i) => (
<div key={i} className="flex justify-between items-center p-2 bg-gray-50 rounded">
@ -380,14 +398,14 @@ export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailRespo
{p.type && chip(p.type, "slate")}
<span className="font-mono text-xs break-all">{p.match}</span>
</div>
{boolPill(p.detected)}
{boolPill(p.detected, t)}
</div>
))}
</div>
</Section>
)}
{(assess.sensitiveInformationPolicy.regexes?.length ?? 0) > 0 && (
<Section title="Custom Regexes" defaultOpen={false}>
<Section title={t("viewLogs.bedrockGuardrailDetails.customRegexes")} defaultOpen={false}>
<div className="space-y-2">
{assess.sensitiveInformationPolicy.regexes!.map((r, i) => (
<div
@ -400,7 +418,7 @@ export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailRespo
<span className="font-mono text-xs break-all">{r.regex}</span>
</div>
<div className="flex items-center gap-2">
{boolPill(r.detected)}
{boolPill(r.detected, t)}
{r.match && <span className="font-mono text-xs break-all">{r.match}</span>}
</div>
</div>
@ -414,15 +432,15 @@ export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailRespo
{/* Topic policy */}
{assess.topicPolicy?.topics?.length ? (
<div className="mb-3">
<h6 className="font-medium mb-2">Topic Policy</h6>
<h6 className="font-medium mb-2">{t("viewLogs.bedrockGuardrailDetails.topicPolicy")}</h6>
<div className="flex flex-wrap gap-2">
{assess.topicPolicy.topics!.map((t, i) => (
{assess.topicPolicy.topics!.map((topic, i) => (
<div key={i} className="px-3 py-1.5 bg-gray-50 rounded-md text-xs">
<div className="flex items-center gap-2">
{chip(t.action ?? "N/A", t.detected ? "red" : "slate")}
<span className="font-medium">{t.name ?? "topic"}</span>
{t.type && chip(t.type, "slate")}
{boolPill(t.detected)}
{chip(topic.action ?? "N/A", topic.detected ? "red" : "slate")}
<span className="font-medium">{topic.name ?? "topic"}</span>
{topic.type && chip(topic.type, "slate")}
{boolPill(topic.detected, t)}
</div>
</div>
))}
@ -432,31 +450,35 @@ export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailRespo
{/* Invocation metrics */}
{assess.invocationMetrics && (
<Section title="Invocation Metrics" defaultOpen={false}>
<Section title={t("viewLogs.bedrockGuardrailDetails.invocationMetrics")} defaultOpen={false}>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<KV label="Latency (ms)">{assess.invocationMetrics.guardrailProcessingLatency ?? "—"}</KV>
<KV label="Coverage:">
<KV label={t("viewLogs.bedrockGuardrailDetails.latencyMs")}>
{assess.invocationMetrics.guardrailProcessingLatency ?? "—"}
</KV>
<KV label={t("viewLogs.bedrockGuardrailDetails.coverage") + ":"}>
<div className="flex flex-wrap gap-2">
{assess.invocationMetrics.guardrailCoverage?.textCharacters &&
chip(
`text ${assess.invocationMetrics.guardrailCoverage.textCharacters.guarded ?? 0}/${
assess.invocationMetrics.guardrailCoverage.textCharacters.total ?? 0
}`,
t("viewLogs.bedrockGuardrailDetails.textCoverage", {
guarded: assess.invocationMetrics.guardrailCoverage.textCharacters.guarded ?? 0,
total: assess.invocationMetrics.guardrailCoverage.textCharacters.total ?? 0,
}),
"blue",
)}
{assess.invocationMetrics.guardrailCoverage?.images &&
chip(
`images ${assess.invocationMetrics.guardrailCoverage.images.guarded ?? 0}/${
assess.invocationMetrics.guardrailCoverage.images.total ?? 0
}`,
t("viewLogs.bedrockGuardrailDetails.imagesCoverage", {
guarded: assess.invocationMetrics.guardrailCoverage.images.guarded ?? 0,
total: assess.invocationMetrics.guardrailCoverage.images.total ?? 0,
}),
"blue",
)}
</div>
</KV>
</div>
<div className="space-y-2">
<KV label="Usage:">
<KV label={t("viewLogs.bedrockGuardrailDetails.usage") + ":"}>
<div className="flex flex-wrap gap-2">
{assess.invocationMetrics.usage &&
Object.entries(assess.invocationMetrics.usage).map(([k, v]) =>
@ -478,7 +500,7 @@ export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailRespo
{/* Automated reasoning (fallback render) */}
{assess.automatedReasoningPolicy?.findings?.length ? (
<Section title="Automated Reasoning Findings" defaultOpen={false}>
<Section title={t("viewLogs.bedrockGuardrailDetails.automatedReasoningFindings")} defaultOpen={false}>
<div className="space-y-2">
{assess.automatedReasoningPolicy.findings!.map((f, i) => (
<pre key={i} className="bg-gray-50 rounded p-2 text-xs overflow-x-auto">
@ -495,7 +517,7 @@ export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailRespo
) : null}
{/* Raw JSON (for debugging / completeness) */}
<Section title="Raw Bedrock Guardrail Response" defaultOpen={false}>
<Section title={t("viewLogs.bedrockGuardrailDetails.rawResponse")} defaultOpen={false}>
<pre className="bg-gray-50 rounded p-3 text-xs overflow-x-auto">{JSON.stringify(response, null, 2)}</pre>
</Section>
</div>

View file

@ -1950,6 +1950,112 @@
"labelTotalSpend": "Total Spend (USD)"
}
},
"viewLogs": {
"auditLogDrawer": {
"tableKeys": "Keys",
"tableTeams": "Teams",
"tableUsers": "Users",
"tableOrganizations": "Organizations",
"tableModels": "Models",
"copyJson": "Copy JSON",
"noDifferingFields": "No differing fields detected",
"tokenLabel": "Token:",
"spendLabel": "Spend:",
"maxBudgetLabel": "Max Budget:",
"before": "Before",
"after": "After",
"labelTable": "Table",
"labelObjectId": "Object ID",
"labelChangedBy": "Changed By",
"labelApiKeyHash": "API Key (Hash)"
},
"configInfoMessage": {
"title": "Request/Response Data Not Available",
"description": "To view request and response details, enable prompt storage in your LiteLLM configuration by adding the following to your <configFile>proxy_config.yaml</configFile> file, or toggle the setting in <settingsPath>Admin Settings → Logging Settings</settingsPath>.",
"note": "Note: This will only affect new requests after the configuration change."
},
"costBreakdownViewer": {
"title": "Cost Breakdown",
"cached": "Cached",
"inputCost": "Input Cost",
"outputCost": "Output Cost",
"cacheReadCost": "Cache Read Cost",
"cacheWriteCost": "Cache Write Cost",
"toolUsageCost": "Tool Usage Cost",
"tokens": "tokens",
"promptTokens": "prompt tokens",
"completionTokens": "completion tokens",
"originalLlmCost": "Original LLM Cost",
"discount": "Discount",
"discountAmount": "Discount Amount",
"margin": "Margin",
"finalCalculatedCost": "Final Calculated Cost"
},
"errorViewer": {
"title": "Error Details",
"type": "Type",
"message": "Message",
"unknownError": "Unknown Error",
"unknownErrorOccurred": "Unknown error occurred",
"traceback": "Traceback",
"collapseAll": "Collapse All",
"expandAll": "Expand All",
"copyTraceback": "Copy traceback"
},
"evalViewer": {
"title": "LLM Judge Results",
"colCriterion": "Criterion",
"colWeight": "Weight",
"colScore": "Score",
"colWeighted": "Weighted",
"weightedTooltip": "Score × Weight — how much each criterion contributes to the final score",
"colComment": "Comment",
"passed": "PASSED",
"failed": "FAILED",
"overallScoreTooltip": "Weighted average of all criterion scores. Each criterion has a weight (%) set when the eval was created — higher-weight criteria count more toward the final score.",
"threshold": "threshold",
"judge": "Judge",
"iter": "Iter",
"judgeError": "Judge error",
"scoreNoCriterion": "Score: {{score}} — no per-criterion breakdown available."
},
"bedrockGuardrailDetails": {
"detected": "detected",
"notDetected": "not detected",
"textGuarded": "text guarded {{guarded}}/{{total}}",
"imagesGuarded": "images guarded {{guarded}}/{{total}}",
"action": "Action",
"actionReason": "Action Reason",
"blockedResponse": "Blocked Response",
"coverage": "Coverage",
"usage": "Usage",
"outputs": "Outputs",
"nonTextOutput": "(non-text output)",
"assessmentTitle": "Assessment #{{number}}",
"wordPolicy": "Word Policy",
"customWords": "Custom Words",
"managedWordLists": "Managed Word Lists",
"contentPolicy": "Content Policy",
"colType": "Type",
"colAction": "Action",
"colDetected": "Detected",
"colStrength": "Strength",
"colConfidence": "Confidence",
"contextualGrounding": "Contextual Grounding",
"colScore": "Score",
"colThreshold": "Threshold",
"sensitiveInformation": "Sensitive Information",
"piiEntities": "PII Entities",
"customRegexes": "Custom Regexes",
"topicPolicy": "Topic Policy",
"invocationMetrics": "Invocation Metrics",
"latencyMs": "Latency (ms)",
"textCoverage": "text {{guarded}}/{{total}}",
"imagesCoverage": "images {{guarded}}/{{total}}",
"automatedReasoningFindings": "Automated Reasoning Findings",
"rawResponse": "Raw Bedrock Guardrail Response"
}
},
"userAgentActivity": {
"summaryTitle": "Summary by User Agent",
"summarySubtitle": "Performance metrics for different user agents",

View file

@ -1950,6 +1950,112 @@
"labelTotalSpend": "总花费(USD)"
}
},
"viewLogs": {
"auditLogDrawer": {
"tableKeys": "密钥",
"tableTeams": "团队",
"tableUsers": "用户",
"tableOrganizations": "组织",
"tableModels": "模型",
"copyJson": "复制 JSON",
"noDifferingFields": "未检测到差异字段",
"tokenLabel": "Token:",
"spendLabel": "花费:",
"maxBudgetLabel": "最大预算:",
"before": "变更前",
"after": "变更后",
"labelTable": "表",
"labelObjectId": "对象 ID",
"labelChangedBy": "操作人",
"labelApiKeyHash": "API 密钥(哈希)"
},
"configInfoMessage": {
"title": "请求/响应数据不可用",
"description": "要查看请求和响应详情,请在您的 LiteLLM 配置中启用提示词存储,方法是在 <configFile>proxy_config.yaml</configFile> 文件中添加以下内容,或在<settingsPath>管理员设置 → 日志设置</settingsPath>中切换该选项。",
"note": "注意:此设置仅对配置更改后的新请求生效。"
},
"costBreakdownViewer": {
"title": "费用明细",
"cached": "已缓存",
"inputCost": "输入费用",
"outputCost": "输出费用",
"cacheReadCost": "缓存读取费用",
"cacheWriteCost": "缓存写入费用",
"toolUsageCost": "工具使用费用",
"tokens": "Token",
"promptTokens": "提示词 Token",
"completionTokens": "补全 Token",
"originalLlmCost": "原始 LLM 费用",
"discount": "折扣",
"discountAmount": "折扣金额",
"margin": "利润",
"finalCalculatedCost": "最终计算费用"
},
"errorViewer": {
"title": "错误详情",
"type": "类型",
"message": "消息",
"unknownError": "未知错误",
"unknownErrorOccurred": "发生了未知错误",
"traceback": "堆栈跟踪",
"collapseAll": "全部折叠",
"expandAll": "全部展开",
"copyTraceback": "复制堆栈跟踪"
},
"evalViewer": {
"title": "LLM 评判结果",
"colCriterion": "标准",
"colWeight": "权重",
"colScore": "得分",
"colWeighted": "加权得分",
"weightedTooltip": "得分 × 权重 — 每个标准对最终得分的贡献度",
"colComment": "备注",
"passed": "通过",
"failed": "未通过",
"overallScoreTooltip": "所有标准得分的加权平均值。每个标准在创建评估时设置权重(%),权重越高的标准对最终得分影响越大。",
"threshold": "阈值",
"judge": "评判模型",
"iter": "迭代",
"judgeError": "评判错误",
"scoreNoCriterion": "得分:{{score}} — 无逐标准明细。"
},
"bedrockGuardrailDetails": {
"detected": "已检测",
"notDetected": "未检测",
"textGuarded": "文本已守护 {{guarded}}/{{total}}",
"imagesGuarded": "图片已守护 {{guarded}}/{{total}}",
"action": "动作",
"actionReason": "动作原因",
"blockedResponse": "已拦截响应",
"coverage": "覆盖范围",
"usage": "用量",
"outputs": "输出",
"nonTextOutput": "(非文本输出)",
"assessmentTitle": "评估 #{{number}}",
"wordPolicy": "词语策略",
"customWords": "自定义词语",
"managedWordLists": "托管词语列表",
"contentPolicy": "内容策略",
"colType": "类型",
"colAction": "动作",
"colDetected": "是否检测到",
"colStrength": "强度",
"colConfidence": "置信度",
"contextualGrounding": "上下文接地",
"colScore": "得分",
"colThreshold": "阈值",
"sensitiveInformation": "敏感信息",
"piiEntities": "PII 实体",
"customRegexes": "自定义正则",
"topicPolicy": "主题策略",
"invocationMetrics": "调用指标",
"latencyMs": "延迟(毫秒)",
"textCoverage": "文本 {{guarded}}/{{total}}",
"imagesCoverage": "图片 {{guarded}}/{{total}}",
"automatedReasoningFindings": "自动推理发现",
"rawResponse": "原始 Bedrock 护栏响应"
}
},
"userAgentActivity": {
"summaryTitle": "按 User Agent 汇总",
"summarySubtitle": "不同 User Agent 的性能指标",