mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
feat(ui): i18n for molecules filter, model columns, and notifications manager
This commit is contained in:
parent
a851b90d4b
commit
019dfcbe23
5 changed files with 257 additions and 89 deletions
|
|
@ -2,6 +2,7 @@ import { FilterIcon } from "@heroicons/react/outline";
|
|||
import { Button, Input, Select } from "antd";
|
||||
import debounce from "lodash/debounce";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface FilterOptionCustomComponentProps {
|
||||
value?: string;
|
||||
|
|
@ -36,8 +37,9 @@ const FilterComponent: React.FC<FilterComponentProps> = ({
|
|||
onApplyFilters,
|
||||
onResetFilters,
|
||||
initialValues = {},
|
||||
buttonLabel = "Filters",
|
||||
buttonLabel,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [showFilters, setShowFilters] = useState<boolean>(false);
|
||||
const [tempValues, setTempValues] = useState<FilterValues>(initialValues);
|
||||
const [searchOptionsMap, setSearchOptionsMap] = useState<{
|
||||
|
|
@ -137,9 +139,9 @@ const FilterComponent: React.FC<FilterComponentProps> = ({
|
|||
onClick={() => setShowFilters(!showFilters)}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
{buttonLabel}
|
||||
{buttonLabel ?? t("molecules.filter.filters")}
|
||||
</Button>
|
||||
<Button onClick={resetFilters}>Reset Filters</Button>
|
||||
<Button onClick={resetFilters}>{t("molecules.filter.resetFilters")}</Button>
|
||||
</div>
|
||||
|
||||
{showFilters && (
|
||||
|
|
@ -152,7 +154,7 @@ const FilterComponent: React.FC<FilterComponentProps> = ({
|
|||
<Select
|
||||
showSearch
|
||||
className="w-full"
|
||||
placeholder={`Search ${option.label || option.name}...`}
|
||||
placeholder={t("molecules.filter.searchPlaceholder", { label: option.label || option.name })}
|
||||
value={tempValues[option.name] || undefined}
|
||||
onChange={(value) => handleFilterChange(option.name, value)}
|
||||
onOpenChange={(open) => handleDropdownVisibleChange(open, option)}
|
||||
|
|
@ -169,12 +171,12 @@ const FilterComponent: React.FC<FilterComponentProps> = ({
|
|||
loading={searchLoadingMap[option.name]}
|
||||
options={searchOptionsMap[option.name] || []}
|
||||
allowClear
|
||||
notFoundContent={searchLoadingMap[option.name] ? "Loading..." : "No results found"}
|
||||
notFoundContent={searchLoadingMap[option.name] ? t("common.loading") : t("common.noResults")}
|
||||
/>
|
||||
) : option.options ? (
|
||||
<Select
|
||||
className="w-full"
|
||||
placeholder={`Select ${option.label || option.name}...`}
|
||||
placeholder={t("molecules.filter.selectPlaceholder", { label: option.label || option.name })}
|
||||
value={tempValues[option.name] || undefined}
|
||||
onChange={(value) => handleFilterChange(option.name, value)}
|
||||
allowClear
|
||||
|
|
@ -192,7 +194,7 @@ const FilterComponent: React.FC<FilterComponentProps> = ({
|
|||
<CustomComponent
|
||||
value={tempValues[option.name] || undefined}
|
||||
onChange={(value) => handleFilterChange(option.name, value ?? "")}
|
||||
placeholder={`Select ${option.label || option.name}...`}
|
||||
placeholder={t("molecules.filter.selectPlaceholder", { label: option.label || option.name })}
|
||||
allFilters={tempValues}
|
||||
/>
|
||||
);
|
||||
|
|
@ -200,7 +202,7 @@ const FilterComponent: React.FC<FilterComponentProps> = ({
|
|||
) : (
|
||||
<Input
|
||||
className="w-full"
|
||||
placeholder={`Enter ${option.label || option.name}...`}
|
||||
placeholder={t("molecules.filter.enterPlaceholder", { label: option.label || option.name })}
|
||||
value={tempValues[option.name] || ""}
|
||||
onChange={(e) => handleFilterChange(option.name, e.target.value)}
|
||||
allowClear
|
||||
|
|
|
|||
|
|
@ -5,13 +5,14 @@ import { Badge, Button, Icon } from "@tremor/react";
|
|||
import { Divider, Flex, Popover, Space, Switch, Tooltip, Typography } from "antd";
|
||||
import { ModelData } from "../../model_dashboard/types";
|
||||
import { ProviderLogo } from "./ProviderLogo";
|
||||
import i18n from "@/lib/i18n";
|
||||
|
||||
const { Text, Title } = Typography;
|
||||
|
||||
const credentialsInfoPopoverContent = (
|
||||
const getCredentialsInfoPopoverContent = () => (
|
||||
<Space direction="vertical" size={12}>
|
||||
<Text strong style={{ fontSize: 13 }}>
|
||||
Credential types
|
||||
{i18n.t("molecules.modelsColumns.credentialTypes")}
|
||||
</Text>
|
||||
<Space direction="vertical" size={8}>
|
||||
<Flex align="center" gap={8}>
|
||||
|
|
@ -19,10 +20,10 @@ const credentialsInfoPopoverContent = (
|
|||
<Flex align="center" gap={8}>
|
||||
<SyncOutlined style={{ color: "#1890ff" }} />
|
||||
<Title level={5} style={{ margin: 0, color: "#1890ff" }}>
|
||||
Reusable
|
||||
{i18n.t("molecules.modelsColumns.reusable")}
|
||||
</Title>
|
||||
</Flex>
|
||||
<Text type="secondary">Credentials saved in LiteLLM that can be added to models repeatedly.</Text>
|
||||
<Text type="secondary">{i18n.t("molecules.modelsColumns.reusableDescription")}</Text>
|
||||
</Space>
|
||||
</Flex>
|
||||
<Divider size="small" />
|
||||
|
|
@ -31,10 +32,10 @@ const credentialsInfoPopoverContent = (
|
|||
<Flex align="center" gap={8}>
|
||||
<EditOutlined style={{ color: "#8c8c8c", fontSize: 14, flexShrink: 0 }} />
|
||||
<Title level={5} style={{ margin: 0 }}>
|
||||
Manual
|
||||
{i18n.t("molecules.modelsColumns.manual")}
|
||||
</Title>
|
||||
</Flex>
|
||||
<Text type="secondary">Credentials added directly during model creation or defined in the config file.</Text>
|
||||
<Text type="secondary">{i18n.t("molecules.modelsColumns.manualDescription")}</Text>
|
||||
</Space>
|
||||
</Flex>
|
||||
</Space>
|
||||
|
|
@ -57,7 +58,7 @@ export const columns = (
|
|||
pausingModelId?: string | null,
|
||||
): ColumnDef<ModelData>[] => [
|
||||
{
|
||||
header: () => <span className="text-sm font-semibold">Model ID</span>,
|
||||
header: () => <span className="text-sm font-semibold">{i18n.t("molecules.modelsColumns.modelId")}</span>,
|
||||
accessorKey: "model_info.id",
|
||||
enableSorting: false,
|
||||
size: 130,
|
||||
|
|
@ -82,7 +83,7 @@ export const columns = (
|
|||
},
|
||||
},
|
||||
{
|
||||
header: () => <span className="text-sm font-semibold">Model Information</span>,
|
||||
header: () => <span className="text-sm font-semibold">{i18n.t("molecules.modelsColumns.modelInformation")}</span>,
|
||||
accessorKey: "model_name",
|
||||
size: 250,
|
||||
minSize: 120,
|
||||
|
|
@ -94,14 +95,14 @@ export const columns = (
|
|||
<Flex align="center" gap={8}>
|
||||
<ProviderLogo provider={model.provider} />
|
||||
<Text type="secondary" style={{ fontSize: 12 }} ellipsis>
|
||||
{model.provider || "Unknown provider"}
|
||||
{model.provider || i18n.t("molecules.modelsColumns.unknownProvider")}
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
<Space direction="vertical" size={6}>
|
||||
<Space direction="vertical" size={2} style={{ width: "100%" }}>
|
||||
<Text type="secondary" style={{ fontSize: 11 }}>
|
||||
Public Model Name
|
||||
{i18n.t("molecules.modelsColumns.publicModelName")}
|
||||
</Text>
|
||||
<Text strong style={{ fontSize: 13, maxWidth: 480 }} ellipsis title={displayName}>
|
||||
{displayName}
|
||||
|
|
@ -110,7 +111,7 @@ export const columns = (
|
|||
|
||||
<Space direction="vertical" size={2}>
|
||||
<Text type="secondary" style={{ fontSize: 11 }}>
|
||||
LiteLLM Model Name
|
||||
{i18n.t("molecules.modelsColumns.litellmModelName")}
|
||||
</Text>
|
||||
<Text
|
||||
style={{ fontSize: 13 }}
|
||||
|
|
@ -161,8 +162,8 @@ export const columns = (
|
|||
{
|
||||
header: () => (
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="text-sm font-semibold">Credentials</span>
|
||||
<Popover content={credentialsInfoPopoverContent} placement="bottom" arrow={{ pointAtCenter: true }}>
|
||||
<span className="text-sm font-semibold">{i18n.t("molecules.modelsColumns.credentials")}</span>
|
||||
<Popover content={getCredentialsInfoPopoverContent()} placement="bottom" arrow={{ pointAtCenter: true }}>
|
||||
<InfoCircleOutlined className="cursor-pointer text-gray-400 hover:text-gray-600" style={{ fontSize: 12 }} />
|
||||
</Popover>
|
||||
</span>
|
||||
|
|
@ -188,7 +189,7 @@ export const columns = (
|
|||
) : (
|
||||
<>
|
||||
<EditOutlined className="flex-shrink-0" style={{ color: "#8c8c8c", fontSize: 14 }} />
|
||||
<span className="text-xs text-gray-500">Manual</span>
|
||||
<span className="text-xs text-gray-500">{i18n.t("molecules.modelsColumns.manual")}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -196,7 +197,7 @@ export const columns = (
|
|||
},
|
||||
},
|
||||
{
|
||||
header: () => <span className="text-sm font-semibold">Created By</span>,
|
||||
header: () => <span className="text-sm font-semibold">{i18n.t("molecules.modelsColumns.createdBy")}</span>,
|
||||
accessorKey: "model_info.created_by",
|
||||
sortingFn: "datetime",
|
||||
size: 160,
|
||||
|
|
@ -212,23 +213,25 @@ export const columns = (
|
|||
{/* Created By - Primary */}
|
||||
<div
|
||||
className="text-xs font-medium text-gray-900 truncate"
|
||||
title={isConfigModel ? "Defined in config" : createdBy || "Unknown"}
|
||||
title={
|
||||
isConfigModel ? i18n.t("molecules.modelsColumns.definedInConfig") : createdBy || i18n.t("common.unknown")
|
||||
}
|
||||
>
|
||||
{isConfigModel ? "Defined in config" : createdBy || "Unknown"}
|
||||
{isConfigModel ? i18n.t("molecules.modelsColumns.definedInConfig") : createdBy || i18n.t("common.unknown")}
|
||||
</div>
|
||||
{/* Created At - Secondary */}
|
||||
<div
|
||||
className="text-xs text-gray-500 truncate mt-0.5"
|
||||
title={isConfigModel ? "Config file" : createdAt || "Unknown date"}
|
||||
title={isConfigModel ? "-" : createdAt || i18n.t("molecules.modelsColumns.unknownDate")}
|
||||
>
|
||||
{isConfigModel ? "-" : createdAt || "Unknown date"}
|
||||
{isConfigModel ? "-" : createdAt || i18n.t("molecules.modelsColumns.unknownDate")}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: () => <span className="text-sm font-semibold">Updated At</span>,
|
||||
header: () => <span className="text-sm font-semibold">{i18n.t("common.updatedAt")}</span>,
|
||||
accessorKey: "model_info.updated_at",
|
||||
sortingFn: "datetime",
|
||||
size: 120,
|
||||
|
|
@ -243,7 +246,7 @@ export const columns = (
|
|||
},
|
||||
},
|
||||
{
|
||||
header: () => <span className="text-sm font-semibold">Costs</span>,
|
||||
header: () => <span className="text-sm font-semibold">{i18n.t("molecules.modelsColumns.costs")}</span>,
|
||||
accessorKey: "input_cost",
|
||||
size: 120,
|
||||
minSize: 80,
|
||||
|
|
@ -262,19 +265,27 @@ export const columns = (
|
|||
}
|
||||
|
||||
return (
|
||||
<Tooltip title="Cost per 1M tokens">
|
||||
<Tooltip title={i18n.t("molecules.modelsColumns.costPerMTokens")}>
|
||||
<div className="flex flex-col min-w-0 w-full">
|
||||
{/* Input Cost - Primary */}
|
||||
{inputCost != null && <div className="text-xs font-medium text-gray-900 truncate">In: ${inputCost}</div>}
|
||||
{inputCost != null && (
|
||||
<div className="text-xs font-medium text-gray-900 truncate">
|
||||
{i18n.t("molecules.modelsColumns.inputCost", { cost: inputCost })}
|
||||
</div>
|
||||
)}
|
||||
{/* Output Cost - Secondary */}
|
||||
{outputCost != null && <div className="text-xs text-gray-500 truncate mt-0.5">Out: ${outputCost}</div>}
|
||||
{outputCost != null && (
|
||||
<div className="text-xs text-gray-500 truncate mt-0.5">
|
||||
{i18n.t("molecules.modelsColumns.outputCost", { cost: outputCost })}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: () => <span className="text-sm font-semibold">Team ID</span>,
|
||||
header: () => <span className="text-sm font-semibold">{i18n.t("molecules.modelsColumns.teamId")}</span>,
|
||||
accessorKey: "model_info.team_id",
|
||||
enableSorting: false,
|
||||
size: 130,
|
||||
|
|
@ -303,7 +314,7 @@ export const columns = (
|
|||
},
|
||||
},
|
||||
{
|
||||
header: () => <span className="text-sm font-semibold">Model Access Group</span>,
|
||||
header: () => <span className="text-sm font-semibold">{i18n.t("molecules.modelsColumns.modelAccessGroup")}</span>,
|
||||
accessorKey: "model_info.model_access_group",
|
||||
enableSorting: false,
|
||||
size: 180,
|
||||
|
|
@ -364,7 +375,7 @@ export const columns = (
|
|||
},
|
||||
},
|
||||
{
|
||||
header: () => <span className="text-sm font-semibold">Status</span>,
|
||||
header: () => <span className="text-sm font-semibold">{i18n.t("common.status")}</span>,
|
||||
accessorKey: "model_info.db_model",
|
||||
size: 120,
|
||||
minSize: 80,
|
||||
|
|
@ -377,14 +388,16 @@ export const columns = (
|
|||
${model.model_info.db_model ? "bg-blue-50 text-blue-600" : "bg-gray-100 text-gray-600"}
|
||||
`}
|
||||
>
|
||||
{model.model_info.db_model ? "DB Model" : "Config Model"}
|
||||
{model.model_info.db_model
|
||||
? i18n.t("molecules.modelsColumns.dbModel")
|
||||
: i18n.t("molecules.modelsColumns.configModel")}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: () => <span className="text-sm font-semibold">Actions</span>,
|
||||
header: () => <span className="text-sm font-semibold">{i18n.t("common.actions")}</span>,
|
||||
size: 100,
|
||||
minSize: 80,
|
||||
enableResizing: false,
|
||||
|
|
@ -396,12 +409,12 @@ export const columns = (
|
|||
const isBlocked = model.model_info?.blocked === true;
|
||||
const isPauseToggleable = !isConfigModel && isAdmin && Boolean(onTogglePauseClick);
|
||||
const pauseTooltip = isConfigModel
|
||||
? "Config models cannot be paused from the dashboard. Pause is DB-backed."
|
||||
? i18n.t("molecules.modelsColumns.configNoPause")
|
||||
: !isAdmin
|
||||
? "Only proxy admins can pause or resume a model."
|
||||
? i18n.t("molecules.modelsColumns.nonAdminNoPause")
|
||||
: isBlocked
|
||||
? "Resume model — restore normal routing."
|
||||
: "Pause model — stop routing requests until resumed.";
|
||||
? i18n.t("molecules.modelsColumns.resumeTooltip")
|
||||
: i18n.t("molecules.modelsColumns.pauseTooltip");
|
||||
// antd's `loading` prop on Switch is purely cosmetic — it does not block
|
||||
// clicks. Pair `loading` with `disabled` derived from the same condition
|
||||
// so a double-click during a pending PATCH cannot send a second,
|
||||
|
|
@ -415,7 +428,9 @@ export const columns = (
|
|||
checked={!isBlocked}
|
||||
disabled={!isPauseToggleable || isPausing}
|
||||
loading={isPausing}
|
||||
aria-label={isBlocked ? "Resume model" : "Pause model"}
|
||||
aria-label={
|
||||
isBlocked ? i18n.t("molecules.modelsColumns.resumeModel") : i18n.t("molecules.modelsColumns.pauseModel")
|
||||
}
|
||||
onClick={(_, e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
|
|
@ -428,11 +443,11 @@ export const columns = (
|
|||
/>
|
||||
</Tooltip>
|
||||
{isConfigModel ? (
|
||||
<Tooltip title="Config model cannot be deleted on the dashboard. Please delete it from the config file.">
|
||||
<Tooltip title={i18n.t("molecules.modelsColumns.configNoDelete")}>
|
||||
<Icon icon={TrashIcon} size="sm" className="opacity-50 cursor-not-allowed" />
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip title="Delete model">
|
||||
<Tooltip title={i18n.t("molecules.modelsColumns.deleteModel")}>
|
||||
<Icon
|
||||
icon={TrashIcon}
|
||||
size="sm"
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { notification as staticNotification } from "antd";
|
|||
import type { NotificationInstance } from "antd/es/notification/interface";
|
||||
import { parseErrorMessage } from "../shared/errorUtils";
|
||||
import { ArgsProps } from "antd/es/notification";
|
||||
import i18n from "@/lib/i18n";
|
||||
|
||||
let notificationInstance: NotificationInstance | null = null;
|
||||
|
||||
|
|
@ -146,24 +147,42 @@ const CLOUDZERO_MATCH = [
|
|||
"cloudzero settings not found",
|
||||
];
|
||||
|
||||
function titleFor(status?: number, desc?: string): string {
|
||||
type ErrorCategory =
|
||||
| "authenticationError"
|
||||
| "accessDenied"
|
||||
| "serviceUnavailable"
|
||||
| "budgetExceeded"
|
||||
| "featureUnavailable"
|
||||
| "routingError"
|
||||
| "alreadyExists"
|
||||
| "contentBlocked"
|
||||
| "validationError"
|
||||
| "integrationError"
|
||||
| "notFound"
|
||||
| "rateLimitExceeded"
|
||||
| "serverError"
|
||||
| "requestError"
|
||||
| "info"
|
||||
| "error";
|
||||
|
||||
function categoryFor(status?: number, desc?: string): ErrorCategory {
|
||||
const d = (desc || "").toLowerCase();
|
||||
|
||||
if (AUTH_MATCH.some((s) => d.includes(s))) return "Authentication Error";
|
||||
if (FORBIDDEN_MATCH.some((s) => d.includes(s))) return "Access Denied";
|
||||
if (DB_MATCH?.some?.((s: string) => d.includes(s)) || status === 503) return "Service Unavailable";
|
||||
if (BUDGET_MATCH?.some?.((s: string) => d.includes(s))) return "Budget Exceeded";
|
||||
if (ENTERPRISE_MATCH?.some?.((s: string) => d.includes(s))) return "Feature Unavailable";
|
||||
if (ROUTER_MATCH?.some?.((s: string) => d.includes(s))) return "Routing Error";
|
||||
if (AUTH_MATCH.some((s) => d.includes(s))) return "authenticationError";
|
||||
if (FORBIDDEN_MATCH.some((s) => d.includes(s))) return "accessDenied";
|
||||
if (DB_MATCH?.some?.((s: string) => d.includes(s)) || status === 503) return "serviceUnavailable";
|
||||
if (BUDGET_MATCH?.some?.((s: string) => d.includes(s))) return "budgetExceeded";
|
||||
if (ENTERPRISE_MATCH?.some?.((s: string) => d.includes(s))) return "featureUnavailable";
|
||||
if (ROUTER_MATCH?.some?.((s: string) => d.includes(s))) return "routingError";
|
||||
|
||||
if (EXISTS_MATCH.some((s) => d.includes(s))) return "Already Exists";
|
||||
if (GUARDRAIL_MATCH.some((s) => d.includes(s))) return "Content Blocked";
|
||||
if (EXISTS_MATCH.some((s) => d.includes(s))) return "alreadyExists";
|
||||
if (GUARDRAIL_MATCH.some((s) => d.includes(s))) return "contentBlocked";
|
||||
|
||||
if (FILE_UPLOAD_MATCH.some((s) => d.includes(s))) return "Validation Error";
|
||||
if (CLOUDZERO_MATCH.some((s) => d.includes(s))) return "Integration Error";
|
||||
if (FILE_UPLOAD_MATCH.some((s) => d.includes(s))) return "validationError";
|
||||
if (CLOUDZERO_MATCH.some((s) => d.includes(s))) return "integrationError";
|
||||
|
||||
if (VALIDATION_MATCH.some((s) => d.includes(s))) return "Validation Error";
|
||||
if (status === 404 || d.includes("not found") || NOT_FOUND_MATCH.some((s) => d.includes(s))) return "Not Found";
|
||||
if (VALIDATION_MATCH.some((s) => d.includes(s))) return "validationError";
|
||||
if (status === 404 || d.includes("not found") || NOT_FOUND_MATCH.some((s) => d.includes(s))) return "notFound";
|
||||
if (
|
||||
status === 429 ||
|
||||
d.includes("rate limit") ||
|
||||
|
|
@ -171,13 +190,13 @@ function titleFor(status?: number, desc?: string): string {
|
|||
d.includes("rpm") ||
|
||||
RATE_LIMIT_EXTRA?.some?.((s: string) => d.includes(s))
|
||||
)
|
||||
return "Rate Limit Exceeded";
|
||||
if (status && status >= 500) return "Server Error";
|
||||
if (status === 401) return "Authentication Error";
|
||||
if (status === 403) return "Access Denied";
|
||||
if (d.includes("enterprise") || d.includes("premium")) return "Info";
|
||||
if (status && status >= 400) return "Request Error";
|
||||
return "Error";
|
||||
return "rateLimitExceeded";
|
||||
if (status && status >= 500) return "serverError";
|
||||
if (status === 401) return "authenticationError";
|
||||
if (status === 403) return "accessDenied";
|
||||
if (d.includes("enterprise") || d.includes("premium")) return "info";
|
||||
if (status && status >= 400) return "requestError";
|
||||
return "error";
|
||||
}
|
||||
|
||||
const SUCCESS_MATCH = [
|
||||
|
|
@ -221,10 +240,14 @@ const CONFIG_WARN_MATCH = [
|
|||
function classifyGeneralMessage(desc?: string): { kind: "success" | "info" | "warning"; title: string } | null {
|
||||
const d = (desc || "").toLowerCase();
|
||||
|
||||
if (SUCCESS_MATCH.some((s) => d.includes(s))) return { kind: "success", title: "Success" };
|
||||
if (DEPRECATION_FEATURE_WARN_MATCH.some((s) => d.includes(s))) return { kind: "warning", title: "Feature Notice" };
|
||||
if (CONFIG_WARN_MATCH.some((s) => d.includes(s))) return { kind: "warning", title: "Configuration Warning" };
|
||||
if (INFO_MATCH.some((s) => d.includes(s))) return { kind: "warning", title: "Rate Limit" }; // show as warning for visibility
|
||||
if (SUCCESS_MATCH.some((s) => d.includes(s)))
|
||||
return { kind: "success", title: i18n.t("molecules.notificationsManager.success") };
|
||||
if (DEPRECATION_FEATURE_WARN_MATCH.some((s) => d.includes(s)))
|
||||
return { kind: "warning", title: i18n.t("molecules.notificationsManager.featureNotice") };
|
||||
if (CONFIG_WARN_MATCH.some((s) => d.includes(s)))
|
||||
return { kind: "warning", title: i18n.t("molecules.notificationsManager.configurationWarning") };
|
||||
if (INFO_MATCH.some((s) => d.includes(s)))
|
||||
return { kind: "warning", title: i18n.t("molecules.notificationsManager.rateLimit") };
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
@ -260,7 +283,7 @@ function looksErrorPayload(input: any, status?: number): boolean {
|
|||
|
||||
const NotificationManager = {
|
||||
error(input: string | NotificationConfig) {
|
||||
const cfg = normalize(input, "Error");
|
||||
const cfg = normalize(input, i18n.t("molecules.notificationsManager.error"));
|
||||
getNotification().error({
|
||||
...COMMON_NOTIFICATION_PROPS,
|
||||
...cfg,
|
||||
|
|
@ -270,7 +293,7 @@ const NotificationManager = {
|
|||
},
|
||||
|
||||
warning(input: string | NotificationConfig) {
|
||||
const cfg = normalize(input, "Warning");
|
||||
const cfg = normalize(input, i18n.t("molecules.notificationsManager.warning"));
|
||||
getNotification().warning({
|
||||
...COMMON_NOTIFICATION_PROPS,
|
||||
...cfg,
|
||||
|
|
@ -280,7 +303,7 @@ const NotificationManager = {
|
|||
},
|
||||
|
||||
info(input: string | NotificationConfig) {
|
||||
const cfg = normalize(input, "Info");
|
||||
const cfg = normalize(input, i18n.t("molecules.notificationsManager.info"));
|
||||
getNotification().info({
|
||||
...COMMON_NOTIFICATION_PROPS,
|
||||
...cfg,
|
||||
|
|
@ -293,14 +316,14 @@ const NotificationManager = {
|
|||
if (React.isValidElement(input)) {
|
||||
getNotification().success({
|
||||
...COMMON_NOTIFICATION_PROPS,
|
||||
message: "Success",
|
||||
message: i18n.t("molecules.notificationsManager.success"),
|
||||
description: input,
|
||||
placement: defaultPlacement(),
|
||||
duration: 3.5,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const cfg = normalize(input as string | NotificationConfig, "Success");
|
||||
const cfg = normalize(input as string | NotificationConfig, i18n.t("molecules.notificationsManager.success"));
|
||||
getNotification().success({
|
||||
...COMMON_NOTIFICATION_PROPS,
|
||||
...cfg,
|
||||
|
|
@ -315,31 +338,31 @@ const NotificationManager = {
|
|||
const base = { ...(extra ?? {}), description, placement: extra?.placement ?? defaultPlacement() };
|
||||
|
||||
if (looksErrorPayload(input, status)) {
|
||||
const title = titleFor(status, description);
|
||||
const payload = { ...base, message: title };
|
||||
const category = categoryFor(status, description);
|
||||
const payload = { ...base, message: i18n.t(`molecules.notificationsManager.${category}`) };
|
||||
|
||||
if (
|
||||
title === "Rate Limit Exceeded" ||
|
||||
title === "Info" ||
|
||||
title === "Budget Exceeded" ||
|
||||
title === "Feature Unavailable" ||
|
||||
title === "Content Blocked" ||
|
||||
title === "Integration Error"
|
||||
category === "rateLimitExceeded" ||
|
||||
category === "info" ||
|
||||
category === "budgetExceeded" ||
|
||||
category === "featureUnavailable" ||
|
||||
category === "contentBlocked" ||
|
||||
category === "integrationError"
|
||||
) {
|
||||
getNotification().warning({ ...COMMON_NOTIFICATION_PROPS, ...payload, duration: extra?.duration ?? 7 });
|
||||
return;
|
||||
}
|
||||
if (title === "Server Error") {
|
||||
if (category === "serverError") {
|
||||
getNotification().error({ ...COMMON_NOTIFICATION_PROPS, ...payload, duration: extra?.duration ?? 8 });
|
||||
return;
|
||||
}
|
||||
if (
|
||||
title === "Request Error" ||
|
||||
title === "Authentication Error" ||
|
||||
title === "Access Denied" ||
|
||||
title === "Not Found" ||
|
||||
title === "Error" ||
|
||||
title === "Already Exists"
|
||||
category === "requestError" ||
|
||||
category === "authenticationError" ||
|
||||
category === "accessDenied" ||
|
||||
category === "notFound" ||
|
||||
category === "error" ||
|
||||
category === "alreadyExists"
|
||||
) {
|
||||
getNotification().error({ ...COMMON_NOTIFICATION_PROPS, ...payload, duration: extra?.duration ?? 6 });
|
||||
return;
|
||||
|
|
@ -350,7 +373,7 @@ const NotificationManager = {
|
|||
|
||||
// Non-error: success/info/warning classifier
|
||||
const cls = classifyGeneralMessage(description);
|
||||
const payload = { ...base, message: cls?.title ?? "Info" };
|
||||
const payload = { ...base, message: cls?.title ?? i18n.t("molecules.notificationsManager.info") };
|
||||
|
||||
if (cls?.kind === "success") {
|
||||
getNotification().success({ ...COMMON_NOTIFICATION_PROPS, ...payload, duration: extra?.duration ?? 3.5 });
|
||||
|
|
|
|||
|
|
@ -133,6 +133,70 @@
|
|||
"selected": "Selected"
|
||||
}
|
||||
},
|
||||
"molecules": {
|
||||
"filter": {
|
||||
"resetFilters": "Reset Filters",
|
||||
"filters": "Filters",
|
||||
"searchPlaceholder": "Search {{label}}...",
|
||||
"selectPlaceholder": "Select {{label}}...",
|
||||
"enterPlaceholder": "Enter {{label}}..."
|
||||
},
|
||||
"modelsColumns": {
|
||||
"modelId": "Model ID",
|
||||
"modelInformation": "Model Information",
|
||||
"credentials": "Credentials",
|
||||
"credentialTypes": "Credential types",
|
||||
"reusable": "Reusable",
|
||||
"reusableDescription": "Credentials saved in LiteLLM that can be added to models repeatedly.",
|
||||
"manual": "Manual",
|
||||
"manualDescription": "Credentials added directly during model creation or defined in the config file.",
|
||||
"createdBy": "Created By",
|
||||
"costs": "Costs",
|
||||
"teamId": "Team ID",
|
||||
"modelAccessGroup": "Model Access Group",
|
||||
"unknownProvider": "Unknown provider",
|
||||
"publicModelName": "Public Model Name",
|
||||
"litellmModelName": "LiteLLM Model Name",
|
||||
"definedInConfig": "Defined in config",
|
||||
"unknownDate": "Unknown date",
|
||||
"costPerMTokens": "Cost per 1M tokens",
|
||||
"inputCost": "In: ${{cost}}",
|
||||
"outputCost": "Out: ${{cost}}",
|
||||
"dbModel": "DB Model",
|
||||
"configModel": "Config Model",
|
||||
"resumeModel": "Resume model",
|
||||
"pauseModel": "Pause model",
|
||||
"configNoPause": "Config models cannot be paused from the dashboard. Pause is DB-backed.",
|
||||
"nonAdminNoPause": "Only proxy admins can pause or resume a model.",
|
||||
"resumeTooltip": "Resume model — restore normal routing.",
|
||||
"pauseTooltip": "Pause model — stop routing requests until resumed.",
|
||||
"configNoDelete": "Config model cannot be deleted on the dashboard. Please delete it from the config file.",
|
||||
"deleteModel": "Delete model"
|
||||
},
|
||||
"notificationsManager": {
|
||||
"authenticationError": "Authentication Error",
|
||||
"accessDenied": "Access Denied",
|
||||
"serviceUnavailable": "Service Unavailable",
|
||||
"budgetExceeded": "Budget Exceeded",
|
||||
"featureUnavailable": "Feature Unavailable",
|
||||
"routingError": "Routing Error",
|
||||
"alreadyExists": "Already Exists",
|
||||
"contentBlocked": "Content Blocked",
|
||||
"validationError": "Validation Error",
|
||||
"integrationError": "Integration Error",
|
||||
"notFound": "Not Found",
|
||||
"rateLimitExceeded": "Rate Limit Exceeded",
|
||||
"serverError": "Server Error",
|
||||
"requestError": "Request Error",
|
||||
"info": "Info",
|
||||
"error": "Error",
|
||||
"success": "Success",
|
||||
"warning": "Warning",
|
||||
"featureNotice": "Feature Notice",
|
||||
"configurationWarning": "Configuration Warning",
|
||||
"rateLimit": "Rate Limit"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
|
|
|
|||
|
|
@ -133,6 +133,70 @@
|
|||
"selected": "已选择"
|
||||
}
|
||||
},
|
||||
"molecules": {
|
||||
"filter": {
|
||||
"resetFilters": "重置筛选",
|
||||
"filters": "筛选",
|
||||
"searchPlaceholder": "搜索 {{label}}...",
|
||||
"selectPlaceholder": "选择 {{label}}...",
|
||||
"enterPlaceholder": "输入 {{label}}..."
|
||||
},
|
||||
"modelsColumns": {
|
||||
"modelId": "模型 ID",
|
||||
"modelInformation": "模型信息",
|
||||
"credentials": "凭据",
|
||||
"credentialTypes": "凭据类型",
|
||||
"reusable": "可复用",
|
||||
"reusableDescription": "保存在 LiteLLM 中可被多次添加到模型的凭据。",
|
||||
"manual": "手动",
|
||||
"manualDescription": "在创建模型时直接添加或在配置文件中定义的凭据。",
|
||||
"createdBy": "创建者",
|
||||
"costs": "费用",
|
||||
"teamId": "团队 ID",
|
||||
"modelAccessGroup": "模型访问组",
|
||||
"unknownProvider": "未知提供商",
|
||||
"publicModelName": "公开模型名称",
|
||||
"litellmModelName": "LiteLLM 模型名称",
|
||||
"definedInConfig": "在配置文件中定义",
|
||||
"unknownDate": "未知日期",
|
||||
"costPerMTokens": "每百万 Token 费用",
|
||||
"inputCost": "输入:${{cost}}",
|
||||
"outputCost": "输出:${{cost}}",
|
||||
"dbModel": "数据库模型",
|
||||
"configModel": "配置模型",
|
||||
"resumeModel": "恢复模型",
|
||||
"pauseModel": "暂停模型",
|
||||
"configNoPause": "配置模型无法从仪表盘暂停。暂停功能依赖数据库。",
|
||||
"nonAdminNoPause": "只有代理管理员可以暂停或恢复模型。",
|
||||
"resumeTooltip": "恢复模型 — 恢复正常路由。",
|
||||
"pauseTooltip": "暂停模型 — 停止路由请求直到恢复。",
|
||||
"configNoDelete": "配置模型无法在仪表盘上删除。请从配置文件中删除。",
|
||||
"deleteModel": "删除模型"
|
||||
},
|
||||
"notificationsManager": {
|
||||
"authenticationError": "认证错误",
|
||||
"accessDenied": "访问被拒绝",
|
||||
"serviceUnavailable": "服务不可用",
|
||||
"budgetExceeded": "预算超限",
|
||||
"featureUnavailable": "功能不可用",
|
||||
"routingError": "路由错误",
|
||||
"alreadyExists": "已存在",
|
||||
"contentBlocked": "内容已屏蔽",
|
||||
"validationError": "验证错误",
|
||||
"integrationError": "集成错误",
|
||||
"notFound": "未找到",
|
||||
"rateLimitExceeded": "超出速率限制",
|
||||
"serverError": "服务器错误",
|
||||
"requestError": "请求错误",
|
||||
"info": "信息",
|
||||
"error": "错误",
|
||||
"success": "成功",
|
||||
"warning": "警告",
|
||||
"featureNotice": "功能通知",
|
||||
"configurationWarning": "配置警告",
|
||||
"rateLimit": "速率限制"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"save": "保存",
|
||||
"cancel": "取消",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue