mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
feat(ui): i18n for activity metrics, pass-through endpoints, and agents panel
This commit is contained in:
parent
1746865a7b
commit
b2abda084f
5 changed files with 314 additions and 113 deletions
|
|
@ -3,6 +3,7 @@ import { resolveTeamAliasFromTeamID } from "@/utils/teamUtils";
|
|||
import { AreaChart, BarChart, Card, Grid, Text, Title } from "@tremor/react";
|
||||
import { Collapse } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { CustomLegend, CustomTooltip } from "./common_components/chartUtils";
|
||||
import { Team } from "./key_team_helpers/key_list";
|
||||
import KeyModelUsageView from "./UsagePage/components/KeyModelUsageView";
|
||||
|
|
@ -23,47 +24,61 @@ const ModelSection = ({
|
|||
metrics: ModelActivityData;
|
||||
hidePromptCachingMetrics?: boolean;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{/* Summary Cards */}
|
||||
<Grid numItems={4} className="gap-4">
|
||||
<Card>
|
||||
<Text>Total Requests</Text>
|
||||
<Text>{t("activityMetrics.totalRequests")}</Text>
|
||||
<Title>{metrics.total_requests.toLocaleString()}</Title>
|
||||
</Card>
|
||||
<Card>
|
||||
<Text>Total Successful Requests</Text>
|
||||
<Text>{t("activityMetrics.totalSuccessfulRequests")}</Text>
|
||||
<Title>{metrics.total_successful_requests.toLocaleString()}</Title>
|
||||
</Card>
|
||||
<Card>
|
||||
<Text>Total Tokens</Text>
|
||||
<Text>
|
||||
{t("common.total")} {t("activityMetrics.tokens")}
|
||||
</Text>
|
||||
<Title>{metrics.total_tokens.toLocaleString()}</Title>
|
||||
<Text>{Math.round(metrics.total_tokens / metrics.total_successful_requests)} avg per successful request</Text>
|
||||
<Text>
|
||||
{Math.round(metrics.total_tokens / metrics.total_successful_requests)}{" "}
|
||||
{t("activityMetrics.avgPerSuccessfulRequest")}
|
||||
</Text>
|
||||
</Card>
|
||||
<Card>
|
||||
<Text>Total Spend</Text>
|
||||
<Text>{t("activityMetrics.totalSpend")}</Text>
|
||||
<Title>${formatNumberWithCommas(metrics.total_spend, 2)}</Title>
|
||||
<Text>
|
||||
${formatNumberWithCommas(metrics.total_spend / metrics.total_successful_requests, 3)} per successful request
|
||||
${formatNumberWithCommas(metrics.total_spend / metrics.total_successful_requests, 3)}{" "}
|
||||
{t("activityMetrics.perSuccessfulRequest")}
|
||||
</Text>
|
||||
</Card>
|
||||
</Grid>
|
||||
|
||||
{metrics.top_api_keys && metrics.top_api_keys.length > 0 && (
|
||||
<Card className="mt-4">
|
||||
<Title>Top Virtual Keys by Spend</Title>
|
||||
<Title>{t("activityMetrics.topVirtualKeysBySpend")}</Title>
|
||||
<div className="mt-3">
|
||||
<div className="grid grid-cols-1 gap-2">
|
||||
{metrics.top_api_keys.map((keyData, index) => (
|
||||
<div key={keyData.api_key} className="flex justify-between items-center p-3 bg-gray-50 rounded-lg">
|
||||
<div>
|
||||
<Text className="font-medium">{keyData.key_alias || `${keyData.api_key.substring(0, 10)}...`}</Text>
|
||||
{keyData.team_id && <Text className="text-xs text-gray-500">Team: {keyData.team_id}</Text>}
|
||||
{keyData.team_id && (
|
||||
<Text className="text-xs text-gray-500">
|
||||
{t("activityMetrics.teamLabel", { teamId: keyData.team_id })}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<Text className="font-medium">${formatNumberWithCommas(keyData.spend, 2)}</Text>
|
||||
<Text className="text-xs text-gray-500">
|
||||
{keyData.requests.toLocaleString()} requests | {keyData.tokens.toLocaleString()} tokens
|
||||
{t("activityMetrics.requestsAndTokens", {
|
||||
requests: keyData.requests.toLocaleString(),
|
||||
tokens: keyData.tokens.toLocaleString(),
|
||||
})}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -78,7 +93,7 @@ const ModelSection = ({
|
|||
{/* Spend per day - Full width card */}
|
||||
<Card className="mt-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<Title>Spend per day</Title>
|
||||
<Title>{t("activityMetrics.spendPerDay")}</Title>
|
||||
<CustomLegend categories={["metrics.spend"]} colors={["green"]} />
|
||||
</div>
|
||||
<BarChart
|
||||
|
|
@ -96,7 +111,9 @@ const ModelSection = ({
|
|||
<Grid numItems={2} className="gap-4 mt-4">
|
||||
<Card>
|
||||
<div className="flex justify-between items-center">
|
||||
<Title>Total Tokens</Title>
|
||||
<Title>
|
||||
{t("common.total")} {t("activityMetrics.tokens")}
|
||||
</Title>
|
||||
<CustomLegend
|
||||
categories={["metrics.prompt_tokens", "metrics.completion_tokens", "metrics.total_tokens"]}
|
||||
colors={["blue", "cyan", "indigo"]}
|
||||
|
|
@ -116,7 +133,7 @@ const ModelSection = ({
|
|||
|
||||
<Card>
|
||||
<div className="flex justify-between items-center">
|
||||
<Title>Requests per day</Title>
|
||||
<Title>{t("activityMetrics.requestsPerDay")}</Title>
|
||||
<CustomLegend categories={["metrics.api_requests"]} colors={["blue"]} />
|
||||
</div>
|
||||
<BarChart
|
||||
|
|
@ -133,7 +150,7 @@ const ModelSection = ({
|
|||
|
||||
<Card>
|
||||
<div className="flex justify-between items-center">
|
||||
<Title>Success vs Failed Requests</Title>
|
||||
<Title>{t("activityMetrics.successVsFailedRequests")}</Title>
|
||||
<CustomLegend
|
||||
categories={["metrics.successful_requests", "metrics.failed_requests"]}
|
||||
colors={["green", "red"]}
|
||||
|
|
@ -154,15 +171,23 @@ const ModelSection = ({
|
|||
{!hidePromptCachingMetrics && (
|
||||
<Card>
|
||||
<div className="flex justify-between items-center">
|
||||
<Title>Prompt Caching Metrics</Title>
|
||||
<Title>{t("activityMetrics.promptCachingMetrics")}</Title>
|
||||
<CustomLegend
|
||||
categories={["metrics.cache_read_input_tokens", "metrics.cache_creation_input_tokens"]}
|
||||
colors={["cyan", "purple"]}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-2">
|
||||
<Text>Cache Read: {metrics.total_cache_read_input_tokens?.toLocaleString() || 0} tokens</Text>
|
||||
<Text>Cache Creation: {metrics.total_cache_creation_input_tokens?.toLocaleString() || 0} tokens</Text>
|
||||
<Text>
|
||||
{t("activityMetrics.cacheRead", {
|
||||
count: metrics.total_cache_read_input_tokens?.toLocaleString() || 0,
|
||||
})}
|
||||
</Text>
|
||||
<Text>
|
||||
{t("activityMetrics.cacheCreation", {
|
||||
count: metrics.total_cache_creation_input_tokens?.toLocaleString() || 0,
|
||||
})}
|
||||
</Text>
|
||||
</div>
|
||||
<AreaChart
|
||||
className="mt-4"
|
||||
|
|
@ -182,6 +207,7 @@ const ModelSection = ({
|
|||
};
|
||||
|
||||
export const ActivityMetrics: React.FC<ActivityMetricsProps> = ({ modelMetrics, hidePromptCachingMetrics = false }) => {
|
||||
const { t } = useTranslation();
|
||||
const modelNames = Object.keys(modelMetrics).sort((a, b) => {
|
||||
if (a === "") return 1;
|
||||
if (b === "") return -1;
|
||||
|
|
@ -257,22 +283,24 @@ export const ActivityMetrics: React.FC<ActivityMetricsProps> = ({ modelMetrics,
|
|||
<div className="space-y-8">
|
||||
{/* Global Summary */}
|
||||
<div className="border rounded-lg p-4">
|
||||
<Title>Overall Usage</Title>
|
||||
<Title>{t("activityMetrics.overallUsage")}</Title>
|
||||
<Grid numItems={4} className="gap-4 mb-4">
|
||||
<Card>
|
||||
<Text>Total Requests</Text>
|
||||
<Text>{t("activityMetrics.totalRequests")}</Text>
|
||||
<Title>{totalMetrics.total_requests.toLocaleString()}</Title>
|
||||
</Card>
|
||||
<Card>
|
||||
<Text>Total Successful Requests</Text>
|
||||
<Text>{t("activityMetrics.totalSuccessfulRequests")}</Text>
|
||||
<Title>{totalMetrics.total_successful_requests.toLocaleString()}</Title>
|
||||
</Card>
|
||||
<Card>
|
||||
<Text>Total Tokens</Text>
|
||||
<Text>
|
||||
{t("common.total")} {t("activityMetrics.tokens")}
|
||||
</Text>
|
||||
<Title>{totalMetrics.total_tokens.toLocaleString()}</Title>
|
||||
</Card>
|
||||
<Card>
|
||||
<Text>Total Spend</Text>
|
||||
<Text>{t("activityMetrics.totalSpend")}</Text>
|
||||
<Title>${formatNumberWithCommas(totalMetrics.total_spend, 2)}</Title>
|
||||
</Card>
|
||||
</Grid>
|
||||
|
|
@ -280,7 +308,7 @@ export const ActivityMetrics: React.FC<ActivityMetricsProps> = ({ modelMetrics,
|
|||
<Grid numItems={2} className="gap-4">
|
||||
<Card>
|
||||
<div className="flex justify-between items-center">
|
||||
<Title>Total Tokens Over Time</Title>
|
||||
<Title>{t("activityMetrics.totalTokensOverTime")}</Title>
|
||||
<CustomLegend
|
||||
categories={["metrics.prompt_tokens", "metrics.completion_tokens", "metrics.total_tokens"]}
|
||||
colors={["blue", "cyan", "indigo"]}
|
||||
|
|
@ -299,7 +327,7 @@ export const ActivityMetrics: React.FC<ActivityMetricsProps> = ({ modelMetrics,
|
|||
</Card>
|
||||
<Card>
|
||||
<div className="flex justify-between items-center">
|
||||
<Title>Total Requests Over Time</Title>
|
||||
<Title>{t("activityMetrics.totalRequestsOverTime")}</Title>
|
||||
<CustomLegend
|
||||
categories={["metrics.successful_requests", "metrics.failed_requests"]}
|
||||
colors={["emerald", "red"]}
|
||||
|
|
@ -326,16 +354,18 @@ export const ActivityMetrics: React.FC<ActivityMetricsProps> = ({ modelMetrics,
|
|||
key={modelName}
|
||||
header={
|
||||
<div className="flex justify-between items-center w-full">
|
||||
<Title>{modelMetrics[modelName].label || "Unknown Item"}</Title>
|
||||
<Title>{modelMetrics[modelName].label || t("activityMetrics.unknownItem")}</Title>
|
||||
<div className="flex space-x-4 text-sm text-gray-500">
|
||||
<span>${formatNumberWithCommas(modelMetrics[modelName].total_spend, 2)}</span>
|
||||
<span>{modelMetrics[modelName].total_requests.toLocaleString()} requests</span>
|
||||
<span>
|
||||
{modelMetrics[modelName].total_requests.toLocaleString()} {t("activityMetrics.requests")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<ModelSection
|
||||
modelName={modelName || "Unknown Model"}
|
||||
modelName={modelName || t("activityMetrics.unknownModel")}
|
||||
metrics={modelMetrics[modelName]}
|
||||
hidePromptCachingMetrics={hidePromptCachingMetrics}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import RoutePreview from "./route_preview";
|
|||
import NotificationsManager from "./molecules/notifications_manager";
|
||||
import PassThroughSecuritySection from "./common_components/PassThroughSecuritySection";
|
||||
import PassThroughGuardrailsSection from "./common_components/PassThroughGuardrailsSection";
|
||||
import { useTranslation } from "react-i18next";
|
||||
const { Option } = Select2;
|
||||
|
||||
const HTTP_METHODS = ["GET", "POST", "PUT", "DELETE", "PATCH"];
|
||||
|
|
@ -34,6 +35,7 @@ const AddPassThroughEndpoint: React.FC<AddFallbacksProps> = ({
|
|||
passThroughItems,
|
||||
premiumUser = false,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [form] = Form.useForm();
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
|
@ -95,7 +97,7 @@ const AddPassThroughEndpoint: React.FC<AddFallbacksProps> = ({
|
|||
const updatedPassThroughSettings = [...passThroughItems, createdEndpoint];
|
||||
setPassThroughItems(updatedPassThroughSettings);
|
||||
|
||||
NotificationsManager.success("Pass-through endpoint created successfully");
|
||||
NotificationsManager.success(t("addPassThrough.createSuccess"));
|
||||
form.resetFields();
|
||||
setPathValue("");
|
||||
setTargetValue("");
|
||||
|
|
@ -104,7 +106,7 @@ const AddPassThroughEndpoint: React.FC<AddFallbacksProps> = ({
|
|||
setGuardrails({});
|
||||
setIsModalVisible(false);
|
||||
} catch (error) {
|
||||
NotificationsManager.fromBackend("Error creating pass-through endpoint: " + error);
|
||||
NotificationsManager.fromBackend(t("addPassThrough.createFailed", { error }));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
|
@ -112,19 +114,19 @@ const AddPassThroughEndpoint: React.FC<AddFallbacksProps> = ({
|
|||
|
||||
const copyToClipboard = (text: string) => {
|
||||
navigator.clipboard.writeText(text);
|
||||
NotificationsManager.success("Copied to clipboard!");
|
||||
NotificationsManager.success(t("common.copied"));
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button className="mx-auto mb-4 mt-4" onClick={() => setIsModalVisible(true)}>
|
||||
+ Add Pass-Through Endpoint
|
||||
{t("addPassThrough.addButton")}
|
||||
</Button>
|
||||
<Modal
|
||||
title={
|
||||
<div className="flex items-center space-x-3 pb-4 border-b border-gray-100">
|
||||
<ApiOutlined className="text-xl text-blue-500" />
|
||||
<h2 className="text-xl font-semibold text-gray-900">Add Pass-Through Endpoint</h2>
|
||||
<h2 className="text-xl font-semibold text-gray-900">{t("addPassThrough.modalTitle")}</h2>
|
||||
</div>
|
||||
}
|
||||
open={isModalVisible}
|
||||
|
|
@ -139,8 +141,8 @@ const AddPassThroughEndpoint: React.FC<AddFallbacksProps> = ({
|
|||
>
|
||||
<div className="mt-6">
|
||||
<Alert
|
||||
message="What is a Pass-Through Endpoint?"
|
||||
description="Route requests from your LiteLLM proxy to any external API. Perfect for custom models, image generation APIs, or any service you want to proxy through LiteLLM."
|
||||
message={t("addPassThrough.alertMessage")}
|
||||
description={t("addPassThrough.alertDescription")}
|
||||
type="info"
|
||||
showIcon
|
||||
className="mb-6"
|
||||
|
|
@ -159,19 +161,17 @@ const AddPassThroughEndpoint: React.FC<AddFallbacksProps> = ({
|
|||
>
|
||||
{/* Route Configuration Section */}
|
||||
<Card className="p-5">
|
||||
<Title className="text-lg font-semibold text-gray-900 mb-2">Route Configuration</Title>
|
||||
<Subtitle className="text-gray-600 mb-5">
|
||||
Configure how requests to your domain will be forwarded to the target API
|
||||
</Subtitle>
|
||||
<Title className="text-lg font-semibold text-gray-900 mb-2">{t("addPassThrough.routeConfigTitle")}</Title>
|
||||
<Subtitle className="text-gray-600 mb-5">{t("addPassThrough.routeConfigSubtitle")}</Subtitle>
|
||||
|
||||
<div className="space-y-5">
|
||||
<Form.Item
|
||||
label={<span className="text-sm font-medium text-gray-700">Path Prefix</span>}
|
||||
name="path"
|
||||
rules={[{ required: true, message: "Path is required", pattern: /^\// }]}
|
||||
extra={
|
||||
<div className="text-xs text-gray-500 mt-1">Example: /bria, /adobe-photoshop, /elasticsearch</div>
|
||||
label={
|
||||
<span className="text-sm font-medium text-gray-700">{t("addPassThrough.pathPrefixLabel")}</span>
|
||||
}
|
||||
name="path"
|
||||
rules={[{ required: true, message: t("addPassThrough.pathRequired"), pattern: /^\// }]}
|
||||
extra={<div className="text-xs text-gray-500 mt-1">{t("addPassThrough.pathExample")}</div>}
|
||||
className="mb-4"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
|
|
@ -185,13 +185,15 @@ const AddPassThroughEndpoint: React.FC<AddFallbacksProps> = ({
|
|||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={<span className="text-sm font-medium text-gray-700">Target URL</span>}
|
||||
label={
|
||||
<span className="text-sm font-medium text-gray-700">{t("addPassThrough.targetUrlLabel")}</span>
|
||||
}
|
||||
name="target"
|
||||
rules={[
|
||||
{ required: true, message: "Target URL is required" },
|
||||
{ type: "url", message: "Please enter a valid URL" },
|
||||
{ required: true, message: t("addPassThrough.targetUrlRequired") },
|
||||
{ type: "url", message: t("addPassThrough.targetUrlInvalid") },
|
||||
]}
|
||||
extra={<div className="text-xs text-gray-500 mt-1">Example:https://engine.prod.bria-api.com</div>}
|
||||
extra={<div className="text-xs text-gray-500 mt-1">{t("addPassThrough.targetUrlExample")}</div>}
|
||||
className="mb-4"
|
||||
>
|
||||
<TextInput
|
||||
|
|
@ -207,8 +209,8 @@ const AddPassThroughEndpoint: React.FC<AddFallbacksProps> = ({
|
|||
<Form.Item
|
||||
label={
|
||||
<span className="text-sm font-medium text-gray-700 flex items-center">
|
||||
HTTP Methods (Optional)
|
||||
<Tooltip title="Select specific HTTP methods. Leave empty to support all methods (GET, POST, PUT, DELETE, PATCH). Useful when the same path needs different targets for different methods.">
|
||||
{t("addPassThrough.httpMethodsLabel")}
|
||||
<Tooltip title={t("addPassThrough.httpMethodsTooltip")}>
|
||||
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
|
||||
</Tooltip>
|
||||
</span>
|
||||
|
|
@ -217,15 +219,15 @@ const AddPassThroughEndpoint: React.FC<AddFallbacksProps> = ({
|
|||
extra={
|
||||
<div className="text-xs text-gray-500 mt-1">
|
||||
{selectedMethods.length === 0
|
||||
? "All HTTP methods supported (default)"
|
||||
: `Only ${selectedMethods.join(", ")} requests will be routed to this endpoint`}
|
||||
? t("addPassThrough.allMethodsDefault")
|
||||
: t("addPassThrough.selectedMethodsDesc", { methods: selectedMethods.join(", ") })}
|
||||
</div>
|
||||
}
|
||||
className="mb-4"
|
||||
>
|
||||
<Select2
|
||||
mode="multiple"
|
||||
placeholder="Select methods (leave empty for all)"
|
||||
placeholder={t("addPassThrough.selectMethodsPlaceholder")}
|
||||
value={selectedMethods}
|
||||
onChange={setSelectedMethods}
|
||||
allowClear
|
||||
|
|
@ -241,10 +243,8 @@ const AddPassThroughEndpoint: React.FC<AddFallbacksProps> = ({
|
|||
|
||||
<div className="flex items-center justify-between py-3">
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-700">Include Subpaths</div>
|
||||
<div className="text-xs text-gray-500 mt-0.5">
|
||||
Forward all subpaths to the target API (recommended for REST APIs)
|
||||
</div>
|
||||
<div className="text-sm font-medium text-gray-700">{t("addPassThrough.includeSubpathsLabel")}</div>
|
||||
<div className="text-xs text-gray-500 mt-0.5">{t("addPassThrough.includeSubpathsDesc")}</div>
|
||||
</div>
|
||||
<Form.Item name="include_subpath" valuePropName="checked" className="mb-0">
|
||||
<Switch checked={includeSubpath} onChange={setIncludeSubpath} />
|
||||
|
|
@ -258,26 +258,26 @@ const AddPassThroughEndpoint: React.FC<AddFallbacksProps> = ({
|
|||
|
||||
{/* Headers Section */}
|
||||
<Card className="p-6">
|
||||
<Title className="text-lg font-semibold text-gray-900 mb-2">Headers</Title>
|
||||
<Subtitle className="text-gray-600 mb-6">
|
||||
Add headers that will be sent with every request to the target API
|
||||
</Subtitle>
|
||||
<Title className="text-lg font-semibold text-gray-900 mb-2">
|
||||
{t("addPassThrough.headersSectionTitle")}
|
||||
</Title>
|
||||
<Subtitle className="text-gray-600 mb-6">{t("addPassThrough.headersSectionSubtitle")}</Subtitle>
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<span className="text-sm font-medium text-gray-700 flex items-center">
|
||||
Authentication Headers
|
||||
<Tooltip title="Authentication and other headers to forward with requests">
|
||||
{t("addPassThrough.authHeadersLabel")}
|
||||
<Tooltip title={t("addPassThrough.authHeadersTooltip")}>
|
||||
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="headers"
|
||||
rules={[{ required: true, message: "Please configure the headers" }]}
|
||||
rules={[{ required: true, message: t("addPassThrough.headersRequired") }]}
|
||||
extra={
|
||||
<div className="text-xs text-gray-500 mt-2">
|
||||
<div className="font-medium mb-1">Add authentication tokens and other required headers</div>
|
||||
<div>Common examples: auth_token, Authorization, x-api-key</div>
|
||||
<div className="font-medium mb-1">{t("addPassThrough.headersExtraTitle")}</div>
|
||||
<div>{t("addPassThrough.headersExtraExamples")}</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
|
|
@ -287,16 +287,16 @@ const AddPassThroughEndpoint: React.FC<AddFallbacksProps> = ({
|
|||
|
||||
{/* Default Query Parameters Section */}
|
||||
<Card className="p-6">
|
||||
<Title className="text-lg font-semibold text-gray-900 mb-2">Default Query Parameters</Title>
|
||||
<Subtitle className="text-gray-600 mb-6">
|
||||
Add query parameters that will be automatically sent with every request to the target API
|
||||
</Subtitle>
|
||||
<Title className="text-lg font-semibold text-gray-900 mb-2">
|
||||
{t("addPassThrough.queryParamsSectionTitle")}
|
||||
</Title>
|
||||
<Subtitle className="text-gray-600 mb-6">{t("addPassThrough.queryParamsSectionSubtitle")}</Subtitle>
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<span className="text-sm font-medium text-gray-700 flex items-center">
|
||||
Default Query Parameters (Optional)
|
||||
<Tooltip title="Query parameters that will be added to all requests. Clients can override these by providing their own values.">
|
||||
{t("addPassThrough.queryParamsLabel")}
|
||||
<Tooltip title={t("addPassThrough.queryParamsTooltip")}>
|
||||
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
|
||||
</Tooltip>
|
||||
</span>
|
||||
|
|
@ -304,8 +304,8 @@ const AddPassThroughEndpoint: React.FC<AddFallbacksProps> = ({
|
|||
name="default_query_params"
|
||||
extra={
|
||||
<div className="text-xs text-gray-500 mt-2">
|
||||
<div className="font-medium mb-1">Parameters are sent with all GET, POST, PUT, PATCH requests</div>
|
||||
<div>Client parameters override defaults. Examples: version=v1, format=json, key=default</div>
|
||||
<div className="font-medium mb-1">{t("addPassThrough.queryParamsExtraTitle")}</div>
|
||||
<div>{t("addPassThrough.queryParamsExtraExamples")}</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
|
|
@ -328,24 +328,22 @@ const AddPassThroughEndpoint: React.FC<AddFallbacksProps> = ({
|
|||
|
||||
{/* Billing Section */}
|
||||
<Card className="p-6">
|
||||
<Title className="text-lg font-semibold text-gray-900 mb-2">Billing</Title>
|
||||
<Subtitle className="text-gray-600 mb-6">Optional cost tracking for this endpoint</Subtitle>
|
||||
<Title className="text-lg font-semibold text-gray-900 mb-2">
|
||||
{t("addPassThrough.billingSectionTitle")}
|
||||
</Title>
|
||||
<Subtitle className="text-gray-600 mb-6">{t("addPassThrough.billingSectionSubtitle")}</Subtitle>
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<span className="text-sm font-medium text-gray-700 flex items-center">
|
||||
Cost Per Request (USD)
|
||||
<Tooltip title="Optional: Track costs for requests to this endpoint">
|
||||
{t("addPassThrough.costPerRequestLabel")}
|
||||
<Tooltip title={t("addPassThrough.costPerRequestTooltip")}>
|
||||
<InfoCircleOutlined className="ml-2 text-gray-400 hover:text-gray-600" />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="cost_per_request"
|
||||
extra={
|
||||
<div className="text-xs text-gray-500 mt-2">
|
||||
The cost charged for each request through this endpoint
|
||||
</div>
|
||||
}
|
||||
extra={<div className="text-xs text-gray-500 mt-2">{t("addPassThrough.costPerRequestExtra")}</div>}
|
||||
>
|
||||
<NumericalInput min={0} step={0.001} precision={4} placeholder="2.0000" size="large" />
|
||||
</Form.Item>
|
||||
|
|
@ -353,7 +351,7 @@ const AddPassThroughEndpoint: React.FC<AddFallbacksProps> = ({
|
|||
|
||||
<div className="flex items-center justify-end space-x-3 pt-6 border-t border-gray-100">
|
||||
<Button variant="secondary" onClick={handleCancel}>
|
||||
Cancel
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
|
|
@ -363,7 +361,7 @@ const AddPassThroughEndpoint: React.FC<AddFallbacksProps> = ({
|
|||
form.submit();
|
||||
}}
|
||||
>
|
||||
{isLoading ? "Creating..." : "Add Pass-Through Endpoint"}
|
||||
{isLoading ? t("addPassThrough.creating") : t("addPassThrough.addButton")}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import { Agent, AgentKeyInfo } from "./agents/types";
|
|||
import { Team } from "./key_team_helpers/key_list";
|
||||
import { formatNumberWithCommas } from "@/utils/dataUtils";
|
||||
import TableIconActionButton from "./common_components/IconActionButton/TableIconActionButtons/TableIconActionButton";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface AgentsPanelProps {
|
||||
accessToken: string | null;
|
||||
|
|
@ -34,6 +35,7 @@ interface AgentsResponse {
|
|||
}
|
||||
|
||||
const AgentsPanel: React.FC<AgentsPanelProps> = ({ accessToken, userRole, teams }) => {
|
||||
const { t } = useTranslation();
|
||||
const [agentsList, setAgentsList] = useState<Agent[]>([]);
|
||||
const [keyInfoMap, setKeyInfoMap] = useState<Record<string, AgentKeyInfo>>({});
|
||||
const [isAddModalVisible, setIsAddModalVisible] = useState(false);
|
||||
|
|
@ -126,11 +128,11 @@ const AgentsPanel: React.FC<AgentsPanelProps> = ({ accessToken, userRole, teams
|
|||
setIsDeleting(true);
|
||||
try {
|
||||
await deleteAgentCall(accessToken, agentToDelete.id);
|
||||
NotificationsManager.success(`Agent "${agentToDelete.name}" deleted successfully`);
|
||||
NotificationsManager.success(t("agents.deleteSuccess", { name: agentToDelete.name }));
|
||||
fetchAgents();
|
||||
} catch (error) {
|
||||
console.error("Error deleting agent:", error);
|
||||
NotificationsManager.fromBackend("Failed to delete agent");
|
||||
NotificationsManager.fromBackend(t("agents.deleteFailed"));
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
setAgentToDelete(null);
|
||||
|
|
@ -152,14 +154,11 @@ const AgentsPanel: React.FC<AgentsPanelProps> = ({ accessToken, userRole, teams
|
|||
return (
|
||||
<div className="w-full mx-auto flex-auto overflow-y-auto m-8 p-2">
|
||||
<div className="flex flex-col gap-2 mb-4">
|
||||
<h1 className="text-2xl font-bold">Agents</h1>
|
||||
<p className="text-sm text-gray-600">
|
||||
List of A2A-spec agents that are available to be used in your organization. Go to AI Hub, to make agents
|
||||
public.
|
||||
</p>
|
||||
<h1 className="text-2xl font-bold">{t("agents.title")}</h1>
|
||||
<p className="text-sm text-gray-600">{t("agents.subtitle")}</p>
|
||||
<Alert
|
||||
message="Why do agents need keys?"
|
||||
description="Keys scope access to an agent and allow it to call MCP tools. Assign a key when creating an agent or from the Virtual Keys page."
|
||||
message={t("agents.alertMessage")}
|
||||
description={t("agents.alertDescription")}
|
||||
type="info"
|
||||
showIcon
|
||||
className="mb-3"
|
||||
|
|
@ -167,13 +166,13 @@ const AgentsPanel: React.FC<AgentsPanelProps> = ({ accessToken, userRole, teams
|
|||
<div className="mt-2 flex items-center gap-4">
|
||||
{isAdmin && (
|
||||
<Button onClick={handleAddAgent} disabled={!accessToken}>
|
||||
+ Add New Agent
|
||||
{t("agents.addNewAgent")}
|
||||
</Button>
|
||||
)}
|
||||
<Tooltip title="When enabled, only agents with reachable URLs are shown">
|
||||
<Tooltip title={t("agents.healthCheckTooltip")}>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircleOutlined className={healthCheckEnabled ? "text-green-500" : "text-gray-400"} />
|
||||
<span className="text-sm text-gray-600">Health Check</span>
|
||||
<span className="text-sm text-gray-600">{t("agents.healthCheck")}</span>
|
||||
<Switch
|
||||
size="small"
|
||||
checked={healthCheckEnabled}
|
||||
|
|
@ -200,22 +199,20 @@ const AgentsPanel: React.FC<AgentsPanelProps> = ({ accessToken, userRole, teams
|
|||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Agent Name</TableHeaderCell>
|
||||
<TableHeaderCell>Agent ID</TableHeaderCell>
|
||||
<TableHeaderCell>Spend (USD)</TableHeaderCell>
|
||||
<TableHeaderCell>Model</TableHeaderCell>
|
||||
<TableHeaderCell>Created</TableHeaderCell>
|
||||
<TableHeaderCell>Status</TableHeaderCell>
|
||||
{isAdmin && <TableHeaderCell>Actions</TableHeaderCell>}
|
||||
<TableHeaderCell>{t("agents.colAgentName")}</TableHeaderCell>
|
||||
<TableHeaderCell>{t("agents.colAgentId")}</TableHeaderCell>
|
||||
<TableHeaderCell>{t("agents.colSpend")}</TableHeaderCell>
|
||||
<TableHeaderCell>{t("agents.colModel")}</TableHeaderCell>
|
||||
<TableHeaderCell>{t("agents.colCreated")}</TableHeaderCell>
|
||||
<TableHeaderCell>{t("common.status")}</TableHeaderCell>
|
||||
{isAdmin && <TableHeaderCell>{t("common.actions")}</TableHeaderCell>}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{sortedAgents.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columnCount}>
|
||||
<Text className="text-center">
|
||||
No agents found. Click "+ Add New Agent" to create one.
|
||||
</Text>
|
||||
<Text className="text-center">{t("agents.noAgentsFound")}</Text>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
|
|
@ -241,17 +238,19 @@ const AgentsPanel: React.FC<AgentsPanelProps> = ({ accessToken, userRole, teams
|
|||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge size="xs" color="blue">
|
||||
{agent.litellm_params?.model || "N/A"}
|
||||
{agent.litellm_params?.model || t("common.unknown")}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Text>{agent.created_at ? new Date(agent.created_at).toLocaleDateString() : "N/A"}</Text>
|
||||
<Text>
|
||||
{agent.created_at ? new Date(agent.created_at).toLocaleDateString() : t("common.unknown")}
|
||||
</Text>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{keyInfoMap[agent.agent_id]?.has_key ? (
|
||||
<Badge color="green">Active</Badge>
|
||||
<Badge color="green">{t("common.active")}</Badge>
|
||||
) : (
|
||||
<Badge color="yellow">Needs Setup</Badge>
|
||||
<Badge color="yellow">{t("agents.needsSetup")}</Badge>
|
||||
)}
|
||||
</TableCell>
|
||||
{isAdmin && (
|
||||
|
|
@ -281,16 +280,16 @@ const AgentsPanel: React.FC<AgentsPanelProps> = ({ accessToken, userRole, teams
|
|||
|
||||
{agentToDelete && (
|
||||
<Modal
|
||||
title="Delete Agent"
|
||||
title={t("agents.deleteModalTitle")}
|
||||
open={agentToDelete !== null}
|
||||
onOk={handleDeleteConfirm}
|
||||
onCancel={handleDeleteCancel}
|
||||
confirmLoading={isDeleting}
|
||||
okText="Delete"
|
||||
okText={t("common.delete")}
|
||||
okButtonProps={{ danger: true }}
|
||||
>
|
||||
<p>Are you sure you want to delete agent: {agentToDelete.name}?</p>
|
||||
<p>This action cannot be undone.</p>
|
||||
<p>{t("agents.deleteConfirm", { name: agentToDelete.name })}</p>
|
||||
<p>{t("agents.deleteCannotUndo")}</p>
|
||||
</Modal>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -922,6 +922,93 @@
|
|||
"monthsRemaining_other": "{{count}} months remaining",
|
||||
"expiringSoon": "Expiring soon"
|
||||
},
|
||||
"activityMetrics": {
|
||||
"totalRequests": "Total Requests",
|
||||
"totalSuccessfulRequests": "Total Successful Requests",
|
||||
"tokens": "Tokens",
|
||||
"avgPerSuccessfulRequest": "avg per successful request",
|
||||
"totalSpend": "Total Spend",
|
||||
"perSuccessfulRequest": "per successful request",
|
||||
"topVirtualKeysBySpend": "Top Virtual Keys by Spend",
|
||||
"teamLabel": "Team: {{teamId}}",
|
||||
"requestsAndTokens": "{{requests}} requests | {{tokens}} tokens",
|
||||
"spendPerDay": "Spend per day",
|
||||
"requestsPerDay": "Requests per day",
|
||||
"successVsFailedRequests": "Success vs Failed Requests",
|
||||
"promptCachingMetrics": "Prompt Caching Metrics",
|
||||
"cacheRead": "Cache Read: {{count}} tokens",
|
||||
"cacheCreation": "Cache Creation: {{count}} tokens",
|
||||
"overallUsage": "Overall Usage",
|
||||
"totalTokensOverTime": "Total Tokens Over Time",
|
||||
"totalRequestsOverTime": "Total Requests Over Time",
|
||||
"unknownItem": "Unknown Item",
|
||||
"unknownModel": "Unknown Model",
|
||||
"requests": "requests"
|
||||
},
|
||||
"addPassThrough": {
|
||||
"addButton": "+ Add Pass-Through Endpoint",
|
||||
"modalTitle": "Add Pass-Through Endpoint",
|
||||
"alertMessage": "What is a Pass-Through Endpoint?",
|
||||
"alertDescription": "Route requests from your LiteLLM proxy to any external API. Perfect for custom models, image generation APIs, or any service you want to proxy through LiteLLM.",
|
||||
"routeConfigTitle": "Route Configuration",
|
||||
"routeConfigSubtitle": "Configure how requests to your domain will be forwarded to the target API",
|
||||
"pathPrefixLabel": "Path Prefix",
|
||||
"pathRequired": "Path is required",
|
||||
"pathExample": "Example: /bria, /adobe-photoshop, /elasticsearch",
|
||||
"targetUrlLabel": "Target URL",
|
||||
"targetUrlRequired": "Target URL is required",
|
||||
"targetUrlInvalid": "Please enter a valid URL",
|
||||
"targetUrlExample": "Example:https://engine.prod.bria-api.com",
|
||||
"httpMethodsLabel": "HTTP Methods (Optional)",
|
||||
"httpMethodsTooltip": "Select specific HTTP methods. Leave empty to support all methods (GET, POST, PUT, DELETE, PATCH). Useful when the same path needs different targets for different methods.",
|
||||
"allMethodsDefault": "All HTTP methods supported (default)",
|
||||
"selectedMethodsDesc": "Only {{methods}} requests will be routed to this endpoint",
|
||||
"selectMethodsPlaceholder": "Select methods (leave empty for all)",
|
||||
"includeSubpathsLabel": "Include Subpaths",
|
||||
"includeSubpathsDesc": "Forward all subpaths to the target API (recommended for REST APIs)",
|
||||
"headersSectionTitle": "Headers",
|
||||
"headersSectionSubtitle": "Add headers that will be sent with every request to the target API",
|
||||
"authHeadersLabel": "Authentication Headers",
|
||||
"authHeadersTooltip": "Authentication and other headers to forward with requests",
|
||||
"headersRequired": "Please configure the headers",
|
||||
"headersExtraTitle": "Add authentication tokens and other required headers",
|
||||
"headersExtraExamples": "Common examples: auth_token, Authorization, x-api-key",
|
||||
"queryParamsSectionTitle": "Default Query Parameters",
|
||||
"queryParamsSectionSubtitle": "Add query parameters that will be automatically sent with every request to the target API",
|
||||
"queryParamsLabel": "Default Query Parameters (Optional)",
|
||||
"queryParamsTooltip": "Query parameters that will be added to all requests. Clients can override these by providing their own values.",
|
||||
"queryParamsExtraTitle": "Parameters are sent with all GET, POST, PUT, PATCH requests",
|
||||
"queryParamsExtraExamples": "Client parameters override defaults. Examples: version=v1, format=json, key=default",
|
||||
"billingSectionTitle": "Billing",
|
||||
"billingSectionSubtitle": "Optional cost tracking for this endpoint",
|
||||
"costPerRequestLabel": "Cost Per Request (USD)",
|
||||
"costPerRequestTooltip": "Optional: Track costs for requests to this endpoint",
|
||||
"costPerRequestExtra": "The cost charged for each request through this endpoint",
|
||||
"creating": "Creating...",
|
||||
"createSuccess": "Pass-through endpoint created successfully",
|
||||
"createFailed": "Error creating pass-through endpoint: {{error}}"
|
||||
},
|
||||
"agents": {
|
||||
"title": "Agents",
|
||||
"subtitle": "List of A2A-spec agents that are available to be used in your organization. Go to AI Hub, to make agents public.",
|
||||
"alertMessage": "Why do agents need keys?",
|
||||
"alertDescription": "Keys scope access to an agent and allow it to call MCP tools. Assign a key when creating an agent or from the Virtual Keys page.",
|
||||
"addNewAgent": "+ Add New Agent",
|
||||
"healthCheck": "Health Check",
|
||||
"healthCheckTooltip": "When enabled, only agents with reachable URLs are shown",
|
||||
"colAgentName": "Agent Name",
|
||||
"colAgentId": "Agent ID",
|
||||
"colSpend": "Spend (USD)",
|
||||
"colModel": "Model",
|
||||
"colCreated": "Created",
|
||||
"noAgentsFound": "No agents found. Click \"+ Add New Agent\" to create one.",
|
||||
"needsSetup": "Needs Setup",
|
||||
"deleteModalTitle": "Delete Agent",
|
||||
"deleteConfirm": "Are you sure you want to delete agent: {{name}}?",
|
||||
"deleteCannotUndo": "This action cannot be undone.",
|
||||
"deleteSuccess": "Agent \"{{name}}\" deleted successfully",
|
||||
"deleteFailed": "Failed to delete agent"
|
||||
},
|
||||
"common": {
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
|
|
|
|||
|
|
@ -922,6 +922,93 @@
|
|||
"monthsRemaining_other": "剩余 {{count}} 个月",
|
||||
"expiringSoon": "即将过期"
|
||||
},
|
||||
"activityMetrics": {
|
||||
"totalRequests": "总请求数",
|
||||
"totalSuccessfulRequests": "总成功请求数",
|
||||
"tokens": "Token",
|
||||
"avgPerSuccessfulRequest": "每次成功请求平均值",
|
||||
"totalSpend": "总花费",
|
||||
"perSuccessfulRequest": "每次成功请求",
|
||||
"topVirtualKeysBySpend": "花费最多的虚拟密钥",
|
||||
"teamLabel": "团队:{{teamId}}",
|
||||
"requestsAndTokens": "{{requests}} 次请求 | {{tokens}} Token",
|
||||
"spendPerDay": "每日花费",
|
||||
"requestsPerDay": "每日请求数",
|
||||
"successVsFailedRequests": "成功 vs 失败请求",
|
||||
"promptCachingMetrics": "提示词缓存指标",
|
||||
"cacheRead": "缓存读取:{{count}} Token",
|
||||
"cacheCreation": "缓存创建:{{count}} Token",
|
||||
"overallUsage": "整体用量",
|
||||
"totalTokensOverTime": "Token 总量随时间变化",
|
||||
"totalRequestsOverTime": "请求总量随时间变化",
|
||||
"unknownItem": "未知项目",
|
||||
"unknownModel": "未知模型",
|
||||
"requests": "次请求"
|
||||
},
|
||||
"addPassThrough": {
|
||||
"addButton": "+ 添加透传端点",
|
||||
"modalTitle": "添加透传端点",
|
||||
"alertMessage": "什么是透传端点?",
|
||||
"alertDescription": "将来自 LiteLLM 代理的请求路由到任意外部 API。适用于自定义模型、图像生成 API 或任何您希望通过 LiteLLM 代理的服务。",
|
||||
"routeConfigTitle": "路由配置",
|
||||
"routeConfigSubtitle": "配置如何将您域名上的请求转发到目标 API",
|
||||
"pathPrefixLabel": "路径前缀",
|
||||
"pathRequired": "路径为必填项",
|
||||
"pathExample": "示例:/bria、/adobe-photoshop、/elasticsearch",
|
||||
"targetUrlLabel": "目标 URL",
|
||||
"targetUrlRequired": "目标 URL 为必填项",
|
||||
"targetUrlInvalid": "请输入有效的 URL",
|
||||
"targetUrlExample": "示例:https://engine.prod.bria-api.com",
|
||||
"httpMethodsLabel": "HTTP 方法(可选)",
|
||||
"httpMethodsTooltip": "选择特定的 HTTP 方法。留空则支持所有方法(GET、POST、PUT、DELETE、PATCH)。当同一路径对不同方法需要不同目标时很有用。",
|
||||
"allMethodsDefault": "支持所有 HTTP 方法(默认)",
|
||||
"selectedMethodsDesc": "仅 {{methods}} 请求将路由到此端点",
|
||||
"selectMethodsPlaceholder": "选择方法(留空表示全部)",
|
||||
"includeSubpathsLabel": "包含子路径",
|
||||
"includeSubpathsDesc": "将所有子路径转发到目标 API(推荐用于 REST API)",
|
||||
"headersSectionTitle": "请求头",
|
||||
"headersSectionSubtitle": "添加每次请求到目标 API 时都会发送的请求头",
|
||||
"authHeadersLabel": "认证请求头",
|
||||
"authHeadersTooltip": "随请求转发的认证及其他请求头",
|
||||
"headersRequired": "请配置请求头",
|
||||
"headersExtraTitle": "添加认证 Token 和其他必需的请求头",
|
||||
"headersExtraExamples": "常见示例:auth_token、Authorization、x-api-key",
|
||||
"queryParamsSectionTitle": "默认查询参数",
|
||||
"queryParamsSectionSubtitle": "添加每次请求到目标 API 时自动发送的查询参数",
|
||||
"queryParamsLabel": "默认查询参数(可选)",
|
||||
"queryParamsTooltip": "将添加到所有请求的查询参数。客户端可通过提供自己的值来覆盖这些默认值。",
|
||||
"queryParamsExtraTitle": "参数将随所有 GET、POST、PUT、PATCH 请求发送",
|
||||
"queryParamsExtraExamples": "客户端参数可覆盖默认值。示例:version=v1、format=json、key=default",
|
||||
"billingSectionTitle": "计费",
|
||||
"billingSectionSubtitle": "此端点的可选成本追踪",
|
||||
"costPerRequestLabel": "每次请求费用(USD)",
|
||||
"costPerRequestTooltip": "可选:追踪此端点的请求费用",
|
||||
"costPerRequestExtra": "通过此端点每次请求收取的费用",
|
||||
"creating": "创建中...",
|
||||
"createSuccess": "透传端点创建成功",
|
||||
"createFailed": "创建透传端点时出错:{{error}}"
|
||||
},
|
||||
"agents": {
|
||||
"title": "Agents",
|
||||
"subtitle": "可在您的组织中使用的 A2A 规范 Agent 列表。前往 AI Hub 将 Agent 设为公开。",
|
||||
"alertMessage": "为什么 Agent 需要密钥?",
|
||||
"alertDescription": "密钥用于限定对 Agent 的访问范围,并允许其调用 MCP 工具。可在创建 Agent 时或从虚拟密钥页面分配密钥。",
|
||||
"addNewAgent": "+ 添加新 Agent",
|
||||
"healthCheck": "健康检查",
|
||||
"healthCheckTooltip": "启用后,仅显示 URL 可访问的 Agent",
|
||||
"colAgentName": "Agent 名称",
|
||||
"colAgentId": "Agent ID",
|
||||
"colSpend": "花费(USD)",
|
||||
"colModel": "模型",
|
||||
"colCreated": "创建时间",
|
||||
"noAgentsFound": "未找到 Agent。点击「+ 添加新 Agent」以创建一个。",
|
||||
"needsSetup": "待配置",
|
||||
"deleteModalTitle": "删除 Agent",
|
||||
"deleteConfirm": "确定要删除 Agent:{{name}}?",
|
||||
"deleteCannotUndo": "此操作无法撤销。",
|
||||
"deleteSuccess": "Agent「{{name}}」已成功删除",
|
||||
"deleteFailed": "删除 Agent 失败"
|
||||
},
|
||||
"common": {
|
||||
"save": "保存",
|
||||
"cancel": "取消",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue