mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-05 02:41:56 +00:00
feat(ui): i18n for pass-through settings, per-user usage, price reload, and prompts
This commit is contained in:
parent
38f2137eca
commit
b03c393a54
6 changed files with 286 additions and 101 deletions
|
|
@ -9,6 +9,7 @@ import { DataTable } from "./view_logs/table";
|
|||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { Eye, EyeOff } from "lucide-react";
|
||||
import NotificationsManager from "./molecules/notifications_manager";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface GeneralSettingsPageProps {
|
||||
accessToken: string | null;
|
||||
|
|
@ -66,6 +67,7 @@ const PassThroughSettings: React.FC<GeneralSettingsPageProps> = ({
|
|||
modelData,
|
||||
premiumUser,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [generalSettings, setGeneralSettings] = useState<passThroughItem[]>([]);
|
||||
const [selectedEndpointId, setSelectedEndpointId] = useState<string | null>(null);
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
|
|
@ -108,10 +110,10 @@ const PassThroughSettings: React.FC<GeneralSettingsPageProps> = ({
|
|||
const updatedSettings = generalSettings.filter((setting) => setting.id !== endpointToDelete);
|
||||
setGeneralSettings(updatedSettings);
|
||||
|
||||
NotificationsManager.success("Endpoint deleted successfully.");
|
||||
NotificationsManager.success(t("passThroughSettings.deleteSuccess"));
|
||||
} catch (error) {
|
||||
console.error("Error deleting the endpoint:", error);
|
||||
NotificationsManager.fromBackend("Error deleting the endpoint: " + error);
|
||||
NotificationsManager.fromBackend(t("passThroughSettings.deleteFailed", { error }));
|
||||
}
|
||||
|
||||
// Close the confirmation modal and reset the endpointToDelete
|
||||
|
|
@ -133,7 +135,7 @@ const PassThroughSettings: React.FC<GeneralSettingsPageProps> = ({
|
|||
// Define columns for the DataTable
|
||||
const columns: ColumnDef<passThroughItem>[] = [
|
||||
{
|
||||
header: "ID",
|
||||
header: t("passThroughSettings.colId"),
|
||||
accessorKey: "id",
|
||||
cell: (info: any) => (
|
||||
<Tooltip title={info.row.original.id}>
|
||||
|
|
@ -147,19 +149,19 @@ const PassThroughSettings: React.FC<GeneralSettingsPageProps> = ({
|
|||
),
|
||||
},
|
||||
{
|
||||
header: "Path",
|
||||
header: t("passThroughSettings.colPath"),
|
||||
accessorKey: "path",
|
||||
},
|
||||
{
|
||||
header: "Target",
|
||||
header: t("passThroughSettings.colTarget"),
|
||||
accessorKey: "target",
|
||||
cell: (info: any) => <Text>{info.getValue()}</Text>,
|
||||
},
|
||||
{
|
||||
header: () => (
|
||||
<div className="flex items-center gap-1">
|
||||
<span>Methods</span>
|
||||
<Tooltip title="HTTP methods supported by this endpoint">
|
||||
<span>{t("passThroughSettings.colMethods")}</span>
|
||||
<Tooltip title={t("passThroughSettings.methodsTooltip")}>
|
||||
<InformationCircleIcon className="w-4 h-4 text-gray-400 cursor-help" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
|
@ -184,22 +186,24 @@ const PassThroughSettings: React.FC<GeneralSettingsPageProps> = ({
|
|||
{
|
||||
header: () => (
|
||||
<div className="flex items-center gap-1">
|
||||
<span>Authentication</span>
|
||||
<Tooltip title="LiteLLM Virtual Key required to call endpoint">
|
||||
<span>{t("passThroughSettings.colAuthentication")}</span>
|
||||
<Tooltip title={t("passThroughSettings.authTooltip")}>
|
||||
<InformationCircleIcon className="w-4 h-4 text-gray-400 cursor-help" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
),
|
||||
accessorKey: "auth",
|
||||
cell: (info: any) => <Badge color={info.getValue() ? "green" : "gray"}>{info.getValue() ? "Yes" : "No"}</Badge>,
|
||||
cell: (info: any) => (
|
||||
<Badge color={info.getValue() ? "green" : "gray"}>{info.getValue() ? t("common.yes") : t("common.no")}</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: "Headers",
|
||||
header: t("passThroughSettings.colHeaders"),
|
||||
accessorKey: "headers",
|
||||
cell: (info: any) => <PasswordField value={info.getValue() || {}} />,
|
||||
},
|
||||
{
|
||||
header: "Actions",
|
||||
header: t("common.actions"),
|
||||
id: "actions",
|
||||
cell: ({ row }) => (
|
||||
<div className="flex space-x-1">
|
||||
|
|
@ -207,13 +211,13 @@ const PassThroughSettings: React.FC<GeneralSettingsPageProps> = ({
|
|||
icon={PencilAltIcon}
|
||||
size="sm"
|
||||
onClick={() => row.original.id && setSelectedEndpointId(row.original.id)}
|
||||
title="Edit"
|
||||
title={t("common.edit")}
|
||||
/>
|
||||
<Icon
|
||||
icon={TrashIcon}
|
||||
size="sm"
|
||||
onClick={() => handleResetField(row.original.id!, row.index)}
|
||||
title="Delete"
|
||||
title={t("common.delete")}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
|
|
@ -232,7 +236,7 @@ const PassThroughSettings: React.FC<GeneralSettingsPageProps> = ({
|
|||
const selectedEndpoint = generalSettings.find((endpoint) => endpoint.id === selectedEndpointId);
|
||||
|
||||
if (!selectedEndpoint) {
|
||||
return <div>Endpoint not found</div>;
|
||||
return <div>{t("passThroughSettings.endpointNotFound")}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -250,8 +254,8 @@ const PassThroughSettings: React.FC<GeneralSettingsPageProps> = ({
|
|||
return (
|
||||
<div>
|
||||
<div>
|
||||
<Title>Pass Through Endpoints</Title>
|
||||
<Text className="text-tremor-content">Configure and manage your pass-through endpoints</Text>
|
||||
<Title>{t("passThroughSettings.title")}</Title>
|
||||
<Text className="text-tremor-content">{t("passThroughSettings.subtitle")}</Text>
|
||||
</div>
|
||||
|
||||
<AddPassThroughEndpoint
|
||||
|
|
@ -267,7 +271,7 @@ const PassThroughSettings: React.FC<GeneralSettingsPageProps> = ({
|
|||
renderSubComponent={() => <div></div>}
|
||||
getRowCanExpand={() => false}
|
||||
isLoading={false}
|
||||
noDataMessage="No pass-through endpoints configured"
|
||||
noDataMessage={t("passThroughSettings.noData")}
|
||||
/>
|
||||
|
||||
{isDeleteModalOpen && (
|
||||
|
|
@ -287,20 +291,20 @@ const PassThroughSettings: React.FC<GeneralSettingsPageProps> = ({
|
|||
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||
<div className="sm:flex sm:items-start">
|
||||
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900">Delete Pass-Through Endpoint</h3>
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900">
|
||||
{t("passThroughSettings.deleteModalTitle")}
|
||||
</h3>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-gray-500">
|
||||
Are you sure you want to delete this pass-through endpoint? This action cannot be undone.
|
||||
</p>
|
||||
<p className="text-sm text-gray-500">{t("passThroughSettings.deleteModalMessage")}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
|
||||
<Button onClick={confirmDelete} color="red" className="ml-2">
|
||||
Delete
|
||||
{t("common.delete")}
|
||||
</Button>
|
||||
<Button onClick={cancelDelete}>Cancel</Button>
|
||||
<Button onClick={cancelDelete}>{t("common.cancel")}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import {
|
|||
TabPanels,
|
||||
} from "@tremor/react";
|
||||
import { perUserAnalyticsCall } from "./networking";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface PerUserMetrics {
|
||||
user_id: string;
|
||||
|
|
@ -45,6 +46,7 @@ interface PerUserUsageProps {
|
|||
}
|
||||
|
||||
const PerUserUsage: React.FC<PerUserUsageProps> = ({ accessToken, selectedTags, formatAbbreviatedNumber }) => {
|
||||
const { t } = useTranslation();
|
||||
// Maximum number of user agent categories to show in charts to prevent color palette overflow
|
||||
const MAX_USER_AGENTS = 8;
|
||||
const [perUserData, setPerUserData] = useState<PerUserAnalyticsResponse>({
|
||||
|
|
@ -95,13 +97,13 @@ const PerUserUsage: React.FC<PerUserUsageProps> = ({ accessToken, selectedTags,
|
|||
|
||||
return (
|
||||
<div className="mb-6">
|
||||
<Title>Per User Usage</Title>
|
||||
<Subtitle>Individual developer usage metrics</Subtitle>
|
||||
<Title>{t("perUserUsage.title")}</Title>
|
||||
<Subtitle>{t("perUserUsage.subtitle")}</Subtitle>
|
||||
|
||||
<TabGroup>
|
||||
<TabList className="mb-6">
|
||||
<Tab>User Details</Tab>
|
||||
<Tab>Usage Distribution</Tab>
|
||||
<Tab>{t("perUserUsage.tabUserDetails")}</Tab>
|
||||
<Tab>{t("perUserUsage.tabUsageDistribution")}</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabPanels>
|
||||
|
|
@ -110,13 +112,13 @@ const PerUserUsage: React.FC<PerUserUsageProps> = ({ accessToken, selectedTags,
|
|||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeaderCell>User ID</TableHeaderCell>
|
||||
<TableHeaderCell>User Email</TableHeaderCell>
|
||||
<TableHeaderCell>User Agent</TableHeaderCell>
|
||||
<TableHeaderCell className="text-right">Success Generations</TableHeaderCell>
|
||||
<TableHeaderCell className="text-right">Total Tokens</TableHeaderCell>
|
||||
<TableHeaderCell className="text-right">Failed Requests</TableHeaderCell>
|
||||
<TableHeaderCell className="text-right">Total Cost</TableHeaderCell>
|
||||
<TableHeaderCell>{t("perUserUsage.colUserId")}</TableHeaderCell>
|
||||
<TableHeaderCell>{t("perUserUsage.colUserEmail")}</TableHeaderCell>
|
||||
<TableHeaderCell>{t("perUserUsage.colUserAgent")}</TableHeaderCell>
|
||||
<TableHeaderCell className="text-right">{t("perUserUsage.colSuccessGenerations")}</TableHeaderCell>
|
||||
<TableHeaderCell className="text-right">{t("perUserUsage.colTotalTokens")}</TableHeaderCell>
|
||||
<TableHeaderCell className="text-right">{t("perUserUsage.colFailedRequests")}</TableHeaderCell>
|
||||
<TableHeaderCell className="text-right">{t("perUserUsage.colTotalCost")}</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
|
|
@ -126,10 +128,10 @@ const PerUserUsage: React.FC<PerUserUsageProps> = ({ accessToken, selectedTags,
|
|||
<Text className="font-medium">{item.user_id}</Text>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Text>{item.user_email || "N/A"}</Text>
|
||||
<Text>{item.user_email || t("perUserUsage.naValue")}</Text>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Text>{item.user_agent || "Unknown"}</Text>
|
||||
<Text>{item.user_agent || t("common.unknown")}</Text>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Text>{formatAbbreviatedNumber(item.successful_requests)}</Text>
|
||||
|
|
@ -150,10 +152,12 @@ const PerUserUsage: React.FC<PerUserUsageProps> = ({ accessToken, selectedTags,
|
|||
|
||||
{perUserData.results.length > 10 && (
|
||||
<div className="mt-4 flex justify-between items-center">
|
||||
<Text className="text-sm text-gray-500">Showing 10 of {perUserData.total_count} results</Text>
|
||||
<Text className="text-sm text-gray-500">
|
||||
{t("perUserUsage.showing10Of", { total: perUserData.total_count })}
|
||||
</Text>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="secondary" onClick={handlePrevPage} disabled={currentPage === 1}>
|
||||
Previous
|
||||
{t("common.previous")}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
|
|
@ -161,7 +165,7 @@ const PerUserUsage: React.FC<PerUserUsageProps> = ({ accessToken, selectedTags,
|
|||
onClick={handleNextPage}
|
||||
disabled={currentPage >= perUserData.total_pages}
|
||||
>
|
||||
Next
|
||||
{t("common.next")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -171,8 +175,8 @@ const PerUserUsage: React.FC<PerUserUsageProps> = ({ accessToken, selectedTags,
|
|||
{/* Tab 2: Usage Distribution Histogram */}
|
||||
<TabPanel>
|
||||
<div className="mb-4">
|
||||
<Title className="text-lg">User Usage Distribution</Title>
|
||||
<Subtitle>Number of users by successful request frequency</Subtitle>
|
||||
<Title className="text-lg">{t("perUserUsage.distributionTitle")}</Title>
|
||||
<Subtitle>{t("perUserUsage.distributionSubtitle")}</Subtitle>
|
||||
</div>
|
||||
|
||||
<BarChart
|
||||
|
|
@ -180,7 +184,7 @@ const PerUserUsage: React.FC<PerUserUsageProps> = ({ accessToken, selectedTags,
|
|||
// Get top user agents by frequency first
|
||||
const userAgentCounts = new Map<string, number>();
|
||||
perUserData.results.forEach((item: PerUserMetrics) => {
|
||||
const agent = item.user_agent || "Unknown";
|
||||
const agent = item.user_agent || t("common.unknown");
|
||||
userAgentCounts.set(agent, (userAgentCounts.get(agent) || 0) + 1);
|
||||
});
|
||||
|
||||
|
|
@ -234,7 +238,7 @@ const PerUserUsage: React.FC<PerUserUsageProps> = ({ accessToken, selectedTags,
|
|||
// Count user agents by frequency and get top ones
|
||||
const userAgentCounts = new Map<string, number>();
|
||||
perUserData.results.forEach((item: PerUserMetrics) => {
|
||||
const agent = item.user_agent || "Unknown";
|
||||
const agent = item.user_agent || t("common.unknown");
|
||||
userAgentCounts.set(agent, (userAgentCounts.get(agent) || 0) + 1);
|
||||
});
|
||||
|
||||
|
|
@ -245,7 +249,7 @@ const PerUserUsage: React.FC<PerUserUsageProps> = ({ accessToken, selectedTags,
|
|||
.map(([agent]) => agent);
|
||||
})()}
|
||||
colors={["blue", "green", "orange", "red", "purple", "yellow", "pink", "indigo"]}
|
||||
valueFormatter={(value: number) => `${value} users`}
|
||||
valueFormatter={(value: number) => t("perUserUsage.usersCount", { count: value })}
|
||||
yAxisWidth={80}
|
||||
showLegend={true}
|
||||
stack={true}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button, Popconfirm, Modal, InputNumber, Space, Typography, Tag, Card, Tooltip, Divider } from "antd";
|
||||
import {
|
||||
ReloadOutlined,
|
||||
|
|
@ -48,12 +49,14 @@ interface PriceDataReloadProps {
|
|||
const PriceDataReload: React.FC<PriceDataReloadProps> = ({
|
||||
accessToken,
|
||||
onReloadSuccess,
|
||||
buttonText = "Reload Price Data",
|
||||
buttonText,
|
||||
showIcon = true,
|
||||
size = "middle",
|
||||
type = "primary",
|
||||
className = "",
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const resolvedButtonText = buttonText ?? t("priceDataReload.reloadButton");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isScheduling, setIsScheduling] = useState(false);
|
||||
const [isCancelling, setIsCancelling] = useState(false);
|
||||
|
|
@ -117,7 +120,7 @@ const PriceDataReload: React.FC<PriceDataReloadProps> = ({
|
|||
|
||||
const handleHardRefresh = async () => {
|
||||
if (!accessToken) {
|
||||
NotificationsManager.fromBackend("No access token available");
|
||||
NotificationsManager.fromBackend(t("priceDataReload.noAccessToken"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -126,29 +129,29 @@ const PriceDataReload: React.FC<PriceDataReloadProps> = ({
|
|||
const response = await reloadModelCostMap(accessToken);
|
||||
|
||||
if (response.status === "success") {
|
||||
NotificationsManager.success(`Price data reloaded successfully! ${response.models_count || 0} models updated.`);
|
||||
NotificationsManager.success(t("priceDataReload.reloadSuccess", { count: response.models_count || 0 }));
|
||||
onReloadSuccess?.();
|
||||
// Refresh status and source info after successful reload
|
||||
await fetchReloadStatus();
|
||||
await fetchSourceInfo();
|
||||
} else {
|
||||
NotificationsManager.fromBackend("Failed to reload price data");
|
||||
NotificationsManager.fromBackend(t("priceDataReload.reloadFailed"));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error reloading price data:", error);
|
||||
NotificationsManager.fromBackend("Failed to reload price data. Please try again.");
|
||||
NotificationsManager.fromBackend(t("priceDataReload.reloadFailedRetry"));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
const handleScheduleReload = async () => {
|
||||
if (!accessToken) {
|
||||
NotificationsManager.fromBackend("No access token available");
|
||||
NotificationsManager.fromBackend(t("priceDataReload.noAccessToken"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (hours <= 0) {
|
||||
NotificationsManager.fromBackend("Hours must be greater than 0");
|
||||
NotificationsManager.fromBackend(t("priceDataReload.hoursMustBePositive"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -157,15 +160,15 @@ const PriceDataReload: React.FC<PriceDataReloadProps> = ({
|
|||
const response = await scheduleModelCostMapReload(accessToken, hours);
|
||||
|
||||
if (response.status === "success") {
|
||||
NotificationsManager.success(`Periodic reload scheduled for every ${hours} hours`);
|
||||
NotificationsManager.success(t("priceDataReload.scheduleSuccess", { hours }));
|
||||
setShowScheduleModal(false);
|
||||
await fetchReloadStatus();
|
||||
} else {
|
||||
NotificationsManager.fromBackend("Failed to schedule periodic reload");
|
||||
NotificationsManager.fromBackend(t("priceDataReload.scheduleFailed"));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error scheduling reload:", error);
|
||||
NotificationsManager.fromBackend("Failed to schedule periodic reload. Please try again.");
|
||||
NotificationsManager.fromBackend(t("priceDataReload.scheduleFailedRetry"));
|
||||
} finally {
|
||||
setIsScheduling(false);
|
||||
}
|
||||
|
|
@ -173,7 +176,7 @@ const PriceDataReload: React.FC<PriceDataReloadProps> = ({
|
|||
|
||||
const handleCancelReload = async () => {
|
||||
if (!accessToken) {
|
||||
NotificationsManager.fromBackend("No access token available");
|
||||
NotificationsManager.fromBackend(t("priceDataReload.noAccessToken"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -182,21 +185,21 @@ const PriceDataReload: React.FC<PriceDataReloadProps> = ({
|
|||
const response = await cancelModelCostMapReload(accessToken);
|
||||
|
||||
if (response.status === "success") {
|
||||
NotificationsManager.success("Periodic reload cancelled successfully");
|
||||
NotificationsManager.success(t("priceDataReload.cancelSuccess"));
|
||||
await fetchReloadStatus();
|
||||
} else {
|
||||
NotificationsManager.fromBackend("Failed to cancel periodic reload");
|
||||
NotificationsManager.fromBackend(t("priceDataReload.cancelFailed"));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error cancelling reload:", error);
|
||||
NotificationsManager.fromBackend("Failed to cancel periodic reload. Please try again.");
|
||||
NotificationsManager.fromBackend(t("priceDataReload.cancelFailedRetry"));
|
||||
} finally {
|
||||
setIsCancelling(false);
|
||||
}
|
||||
};
|
||||
|
||||
const formatDateTime = (dateTimeString: string | null) => {
|
||||
if (!dateTimeString) return "Never";
|
||||
if (!dateTimeString) return t("common.never");
|
||||
try {
|
||||
return new Date(dateTimeString).toLocaleString();
|
||||
} catch {
|
||||
|
|
@ -205,9 +208,9 @@ const PriceDataReload: React.FC<PriceDataReloadProps> = ({
|
|||
};
|
||||
|
||||
const getStatusText = () => {
|
||||
if (!reloadStatus?.scheduled) return "Not scheduled";
|
||||
if (!reloadStatus.last_run) return "Ready";
|
||||
return "Active";
|
||||
if (!reloadStatus?.scheduled) return t("priceDataReload.statusNotScheduled");
|
||||
if (!reloadStatus.last_run) return t("priceDataReload.statusReady");
|
||||
return t("priceDataReload.statusActive");
|
||||
};
|
||||
|
||||
const getStatusColor = () => {
|
||||
|
|
@ -222,11 +225,11 @@ const PriceDataReload: React.FC<PriceDataReloadProps> = ({
|
|||
<Space direction="horizontal" size="middle" style={{ marginBottom: 16 }}>
|
||||
{/* Hard Refresh Button - Always visible */}
|
||||
<Popconfirm
|
||||
title="Hard Refresh Price Data"
|
||||
description="This will immediately fetch the latest pricing information from the remote source. Continue?"
|
||||
title={t("priceDataReload.hardRefreshTitle")}
|
||||
description={t("priceDataReload.hardRefreshDesc")}
|
||||
onConfirm={handleHardRefresh}
|
||||
okText="Yes"
|
||||
cancelText="No"
|
||||
okText={t("common.yes")}
|
||||
cancelText={t("common.no")}
|
||||
okButtonProps={{
|
||||
style: {
|
||||
backgroundColor: "#6366f1",
|
||||
|
|
@ -272,7 +275,7 @@ const PriceDataReload: React.FC<PriceDataReloadProps> = ({
|
|||
e.currentTarget.style.backgroundColor = "#6366f1";
|
||||
}}
|
||||
>
|
||||
{buttonText}
|
||||
{resolvedButtonText}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
|
||||
|
|
@ -294,7 +297,7 @@ const PriceDataReload: React.FC<PriceDataReloadProps> = ({
|
|||
lineHeight: "1.25rem",
|
||||
}}
|
||||
>
|
||||
Set Up Periodic Reload
|
||||
{t("priceDataReload.setupPeriodicReload")}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
|
|
@ -315,7 +318,7 @@ const PriceDataReload: React.FC<PriceDataReloadProps> = ({
|
|||
lineHeight: "1.25rem",
|
||||
}}
|
||||
>
|
||||
Cancel Periodic Reload
|
||||
{t("priceDataReload.cancelPeriodicReload")}
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
|
|
@ -340,13 +343,13 @@ const PriceDataReload: React.FC<PriceDataReloadProps> = ({
|
|||
<DatabaseOutlined style={{ color: "#fa8c16", fontSize: 16 }} />
|
||||
)}
|
||||
<Text strong style={{ fontSize: "13px" }}>
|
||||
Pricing Data Source
|
||||
{t("priceDataReload.pricingDataSource")}
|
||||
</Text>
|
||||
<Tag
|
||||
color={sourceInfo.source === "remote" ? "blue" : "orange"}
|
||||
style={{ marginLeft: "auto", fontWeight: 600, textTransform: "uppercase", fontSize: "11px" }}
|
||||
>
|
||||
{sourceInfo.source === "remote" ? "Remote" : "Local"}
|
||||
{sourceInfo.source === "remote" ? t("priceDataReload.sourceRemote") : t("priceDataReload.sourceLocal")}
|
||||
</Tag>
|
||||
</div>
|
||||
|
||||
|
|
@ -355,7 +358,7 @@ const PriceDataReload: React.FC<PriceDataReloadProps> = ({
|
|||
{/* Model count */}
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<Text type="secondary" style={{ fontSize: "12px" }}>
|
||||
Models loaded:
|
||||
{t("priceDataReload.modelsLoaded")}
|
||||
</Text>
|
||||
<Text strong style={{ fontSize: "12px" }}>
|
||||
{sourceInfo.model_count.toLocaleString()}
|
||||
|
|
@ -366,7 +369,7 @@ const PriceDataReload: React.FC<PriceDataReloadProps> = ({
|
|||
{sourceInfo.url && (
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", gap: 8 }}>
|
||||
<Text type="secondary" style={{ fontSize: "12px", whiteSpace: "nowrap" }}>
|
||||
{sourceInfo.source === "remote" ? "Loaded from:" : "Attempted URL:"}
|
||||
{sourceInfo.source === "remote" ? t("priceDataReload.loadedFrom") : t("priceDataReload.attemptedUrl")}
|
||||
</Text>
|
||||
<Tooltip title={sourceInfo.url}>
|
||||
<Text
|
||||
|
|
@ -392,7 +395,7 @@ const PriceDataReload: React.FC<PriceDataReloadProps> = ({
|
|||
<div style={{ display: "flex", alignItems: "center", gap: 6, marginTop: 2 }}>
|
||||
<InfoCircleOutlined style={{ color: "#fa8c16", fontSize: 12 }} />
|
||||
<Text type="secondary" style={{ fontSize: "11px" }}>
|
||||
Local mode forced via <code>LITELLM_LOCAL_MODEL_COST_MAP=True</code>
|
||||
{t("priceDataReload.localModeForced")}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -413,7 +416,7 @@ const PriceDataReload: React.FC<PriceDataReloadProps> = ({
|
|||
>
|
||||
<WarningOutlined style={{ color: "#fa8c16", fontSize: 12, marginTop: 2 }} />
|
||||
<Text style={{ fontSize: "11px", color: "#614700" }}>
|
||||
Fell back to local: {sourceInfo.fallback_reason}
|
||||
{t("priceDataReload.fellBackToLocal", { reason: sourceInfo.fallback_reason })}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -435,16 +438,16 @@ const PriceDataReload: React.FC<PriceDataReloadProps> = ({
|
|||
{reloadStatus.scheduled ? (
|
||||
<div>
|
||||
<Tag color="green" icon={<ClockCircleOutlined />}>
|
||||
Scheduled every {reloadStatus.interval_hours} hours
|
||||
{t("priceDataReload.scheduledEvery", { hours: reloadStatus.interval_hours })}
|
||||
</Tag>
|
||||
</div>
|
||||
) : (
|
||||
<Text type="secondary">No periodic reload scheduled</Text>
|
||||
<Text type="secondary">{t("priceDataReload.noPeriodicReload")}</Text>
|
||||
)}
|
||||
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<Text type="secondary" style={{ fontSize: "12px" }}>
|
||||
Last run:
|
||||
{t("priceDataReload.lastRun")}
|
||||
</Text>
|
||||
<Text style={{ fontSize: "12px" }}>{formatDateTime(reloadStatus.last_run)}</Text>
|
||||
</div>
|
||||
|
|
@ -454,14 +457,14 @@ const PriceDataReload: React.FC<PriceDataReloadProps> = ({
|
|||
{reloadStatus.next_run && (
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<Text type="secondary" style={{ fontSize: "12px" }}>
|
||||
Next run:
|
||||
{t("priceDataReload.nextRun")}
|
||||
</Text>
|
||||
<Text style={{ fontSize: "12px" }}>{formatDateTime(reloadStatus.next_run)}</Text>
|
||||
</div>
|
||||
)}
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<Text type="secondary" style={{ fontSize: "12px" }}>
|
||||
Status:
|
||||
{t("common.status")}
|
||||
</Text>
|
||||
<Tag color={getStatusColor()}>{getStatusText()}</Tag>
|
||||
</div>
|
||||
|
|
@ -473,13 +476,13 @@ const PriceDataReload: React.FC<PriceDataReloadProps> = ({
|
|||
|
||||
{/* Schedule Modal */}
|
||||
<Modal
|
||||
title="Set Up Periodic Reload"
|
||||
title={t("priceDataReload.scheduleModalTitle")}
|
||||
open={showScheduleModal}
|
||||
onOk={handleScheduleReload}
|
||||
onCancel={() => setShowScheduleModal(false)}
|
||||
confirmLoading={isScheduling}
|
||||
okText="Schedule"
|
||||
cancelText="Cancel"
|
||||
okText={t("priceDataReload.scheduleButton")}
|
||||
cancelText={t("common.cancel")}
|
||||
okButtonProps={{
|
||||
style: {
|
||||
backgroundColor: "#6366f1",
|
||||
|
|
@ -489,7 +492,7 @@ const PriceDataReload: React.FC<PriceDataReloadProps> = ({
|
|||
}}
|
||||
>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Text>Set up automatic reload of price data every:</Text>
|
||||
<Text>{t("priceDataReload.scheduleModalDesc")}</Text>
|
||||
</div>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<InputNumber
|
||||
|
|
@ -497,14 +500,12 @@ const PriceDataReload: React.FC<PriceDataReloadProps> = ({
|
|||
max={168} // 1 week max
|
||||
value={hours}
|
||||
onChange={(value) => setHours(value || 6)}
|
||||
addonAfter="hours"
|
||||
addonAfter={t("priceDataReload.hoursLabel")}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Text type="secondary">
|
||||
This will automatically fetch the latest pricing data from the remote source every {hours} hours.
|
||||
</Text>
|
||||
<Text type="secondary">{t("priceDataReload.scheduleModalHint", { hours })}</Text>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import AddPromptForm from "./prompts/add_prompt_form";
|
|||
import PromptEditorView from "./prompts/prompt_editor_view";
|
||||
import NotificationsManager from "./molecules/notifications_manager";
|
||||
import { isAdminRole, isProxyAdminRole } from "@/utils/roles";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface PromptsProps {
|
||||
accessToken: string | null;
|
||||
|
|
@ -16,6 +17,7 @@ interface PromptsProps {
|
|||
}
|
||||
|
||||
const PromptsPanel: React.FC<PromptsProps> = ({ accessToken, userRole }) => {
|
||||
const { t } = useTranslation();
|
||||
const [promptsList, setPromptsList] = useState<PromptSpec[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [selectedEnvironment, setSelectedEnvironment] = useState<string | undefined>(undefined);
|
||||
|
|
@ -101,11 +103,11 @@ const PromptsPanel: React.FC<PromptsProps> = ({ accessToken, userRole }) => {
|
|||
setIsDeleting(true);
|
||||
try {
|
||||
await deletePromptCall(accessToken, promptToDelete.id);
|
||||
NotificationsManager.success(`Prompt "${promptToDelete.name}" deleted successfully`);
|
||||
NotificationsManager.success(t("prompts.deleteSuccess", { name: promptToDelete.name }));
|
||||
fetchPrompts(); // Refresh the list
|
||||
} catch (error) {
|
||||
console.error("Error deleting prompt:", error);
|
||||
NotificationsManager.fromBackend("Failed to delete prompt");
|
||||
NotificationsManager.fromBackend(t("prompts.deleteFailed"));
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
setPromptToDelete(null);
|
||||
|
|
@ -141,24 +143,24 @@ const PromptsPanel: React.FC<PromptsProps> = ({ accessToken, userRole }) => {
|
|||
{canModify && (
|
||||
<>
|
||||
<Button onClick={handleAddPrompt} disabled={!accessToken}>
|
||||
+ Add New Prompt
|
||||
{t("prompts.addNewPrompt")}
|
||||
</Button>
|
||||
<Button onClick={handleAddPromptFromFile} disabled={!accessToken} variant="secondary">
|
||||
Upload .prompt File
|
||||
{t("prompts.uploadPromptFile")}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<Select
|
||||
placeholder="All Environments"
|
||||
placeholder={t("prompts.allEnvironments")}
|
||||
allowClear
|
||||
value={selectedEnvironment}
|
||||
onChange={(value) => setSelectedEnvironment(value)}
|
||||
style={{ width: 180 }}
|
||||
options={[
|
||||
{ label: "Development", value: "development" },
|
||||
{ label: "Staging", value: "staging" },
|
||||
{ label: "Production", value: "production" },
|
||||
{ label: t("prompts.envDevelopment"), value: "development" },
|
||||
{ label: t("prompts.envStaging"), value: "staging" },
|
||||
{ label: t("prompts.envProduction"), value: "production" },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -183,16 +185,16 @@ const PromptsPanel: React.FC<PromptsProps> = ({ accessToken, userRole }) => {
|
|||
|
||||
{promptToDelete && (
|
||||
<Modal
|
||||
title="Delete Prompt"
|
||||
title={t("prompts.deleteModalTitle")}
|
||||
open={promptToDelete !== null}
|
||||
onOk={handleDeleteConfirm}
|
||||
onCancel={handleDeleteCancel}
|
||||
confirmLoading={isDeleting}
|
||||
okText="Delete"
|
||||
okText={t("common.delete")}
|
||||
okButtonProps={{ danger: true }}
|
||||
>
|
||||
<p>Are you sure you want to delete prompt: {promptToDelete.name} ?</p>
|
||||
<p>This action cannot be undone.</p>
|
||||
<p>{t("prompts.deleteConfirm", { name: promptToDelete.name })}</p>
|
||||
<p>{t("prompts.deleteCannotUndo")}</p>
|
||||
</Modal>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1530,6 +1530,93 @@
|
|||
"deleteSuccess": "Organization deleted successfully",
|
||||
"createSuccess": "Organization created successfully"
|
||||
},
|
||||
"passThroughSettings": {
|
||||
"title": "Pass Through Endpoints",
|
||||
"subtitle": "Configure and manage your pass-through endpoints",
|
||||
"colId": "ID",
|
||||
"colPath": "Path",
|
||||
"colTarget": "Target",
|
||||
"colMethods": "Methods",
|
||||
"methodsTooltip": "HTTP methods supported by this endpoint",
|
||||
"colAuthentication": "Authentication",
|
||||
"authTooltip": "LiteLLM Virtual Key required to call endpoint",
|
||||
"colHeaders": "Headers",
|
||||
"noData": "No pass-through endpoints configured",
|
||||
"endpointNotFound": "Endpoint not found",
|
||||
"deleteModalTitle": "Delete Pass-Through Endpoint",
|
||||
"deleteModalMessage": "Are you sure you want to delete this pass-through endpoint? This action cannot be undone.",
|
||||
"deleteSuccess": "Endpoint deleted successfully.",
|
||||
"deleteFailed": "Error deleting the endpoint: {{error}}"
|
||||
},
|
||||
"perUserUsage": {
|
||||
"title": "Per User Usage",
|
||||
"subtitle": "Individual developer usage metrics",
|
||||
"tabUserDetails": "User Details",
|
||||
"tabUsageDistribution": "Usage Distribution",
|
||||
"colUserId": "User ID",
|
||||
"colUserEmail": "User Email",
|
||||
"colUserAgent": "User Agent",
|
||||
"colSuccessGenerations": "Success Generations",
|
||||
"colTotalTokens": "Total Tokens",
|
||||
"colFailedRequests": "Failed Requests",
|
||||
"colTotalCost": "Total Cost",
|
||||
"naValue": "N/A",
|
||||
"showing10Of": "Showing 10 of {{total}} results",
|
||||
"distributionTitle": "User Usage Distribution",
|
||||
"distributionSubtitle": "Number of users by successful request frequency",
|
||||
"usersCount": "{{count}} users"
|
||||
},
|
||||
"priceDataReload": {
|
||||
"reloadButton": "Reload Price Data",
|
||||
"noAccessToken": "No access token available",
|
||||
"reloadSuccess": "Price data reloaded successfully! {{count}} models updated.",
|
||||
"reloadFailed": "Failed to reload price data",
|
||||
"reloadFailedRetry": "Failed to reload price data. Please try again.",
|
||||
"hoursMustBePositive": "Hours must be greater than 0",
|
||||
"scheduleSuccess": "Periodic reload scheduled for every {{hours}} hours",
|
||||
"scheduleFailed": "Failed to schedule periodic reload",
|
||||
"scheduleFailedRetry": "Failed to schedule periodic reload. Please try again.",
|
||||
"cancelSuccess": "Periodic reload cancelled successfully",
|
||||
"cancelFailed": "Failed to cancel periodic reload",
|
||||
"cancelFailedRetry": "Failed to cancel periodic reload. Please try again.",
|
||||
"statusNotScheduled": "Not scheduled",
|
||||
"statusReady": "Ready",
|
||||
"statusActive": "Active",
|
||||
"hardRefreshTitle": "Hard Refresh Price Data",
|
||||
"hardRefreshDesc": "This will immediately fetch the latest pricing information from the remote source. Continue?",
|
||||
"setupPeriodicReload": "Set Up Periodic Reload",
|
||||
"cancelPeriodicReload": "Cancel Periodic Reload",
|
||||
"pricingDataSource": "Pricing Data Source",
|
||||
"sourceRemote": "Remote",
|
||||
"sourceLocal": "Local",
|
||||
"modelsLoaded": "Models loaded:",
|
||||
"loadedFrom": "Loaded from:",
|
||||
"attemptedUrl": "Attempted URL:",
|
||||
"localModeForced": "Local mode forced via LITELLM_LOCAL_MODEL_COST_MAP=True",
|
||||
"fellBackToLocal": "Fell back to local: {{reason}}",
|
||||
"scheduledEvery": "Scheduled every {{hours}} hours",
|
||||
"noPeriodicReload": "No periodic reload scheduled",
|
||||
"lastRun": "Last run:",
|
||||
"nextRun": "Next run:",
|
||||
"scheduleModalTitle": "Set Up Periodic Reload",
|
||||
"scheduleButton": "Schedule",
|
||||
"scheduleModalDesc": "Set up automatic reload of price data every:",
|
||||
"hoursLabel": "hours",
|
||||
"scheduleModalHint": "This will automatically fetch the latest pricing data from the remote source every {{hours}} hours."
|
||||
},
|
||||
"prompts": {
|
||||
"addNewPrompt": "+ Add New Prompt",
|
||||
"uploadPromptFile": "Upload .prompt File",
|
||||
"allEnvironments": "All Environments",
|
||||
"envDevelopment": "Development",
|
||||
"envStaging": "Staging",
|
||||
"envProduction": "Production",
|
||||
"deleteModalTitle": "Delete Prompt",
|
||||
"deleteConfirm": "Are you sure you want to delete prompt: {{name}} ?",
|
||||
"deleteCannotUndo": "This action cannot be undone.",
|
||||
"deleteSuccess": "Prompt \"{{name}}\" deleted successfully",
|
||||
"deleteFailed": "Failed to delete prompt"
|
||||
},
|
||||
"passThroughInfo": {
|
||||
"notFound": "Pass through endpoint not found",
|
||||
"backButton": "← Back",
|
||||
|
|
|
|||
|
|
@ -1530,6 +1530,93 @@
|
|||
"deleteSuccess": "组织已成功删除",
|
||||
"createSuccess": "组织已成功创建"
|
||||
},
|
||||
"passThroughSettings": {
|
||||
"title": "透传端点",
|
||||
"subtitle": "配置并管理您的透传端点",
|
||||
"colId": "ID",
|
||||
"colPath": "路径",
|
||||
"colTarget": "目标",
|
||||
"colMethods": "方法",
|
||||
"methodsTooltip": "此端点支持的 HTTP 方法",
|
||||
"colAuthentication": "认证",
|
||||
"authTooltip": "调用端点需要 LiteLLM 虚拟密钥",
|
||||
"colHeaders": "请求头",
|
||||
"noData": "未配置透传端点",
|
||||
"endpointNotFound": "未找到端点",
|
||||
"deleteModalTitle": "删除透传端点",
|
||||
"deleteModalMessage": "确定要删除此透传端点吗?此操作无法撤销。",
|
||||
"deleteSuccess": "端点已成功删除。",
|
||||
"deleteFailed": "删除端点时出错:{{error}}"
|
||||
},
|
||||
"perUserUsage": {
|
||||
"title": "每用户用量",
|
||||
"subtitle": "开发者个人用量指标",
|
||||
"tabUserDetails": "用户详情",
|
||||
"tabUsageDistribution": "用量分布",
|
||||
"colUserId": "用户 ID",
|
||||
"colUserEmail": "用户邮箱",
|
||||
"colUserAgent": "User Agent",
|
||||
"colSuccessGenerations": "成功生成次数",
|
||||
"colTotalTokens": "总 Token 数",
|
||||
"colFailedRequests": "失败请求数",
|
||||
"colTotalCost": "总费用",
|
||||
"naValue": "N/A",
|
||||
"showing10Of": "显示第 10 条,共 {{total}} 条",
|
||||
"distributionTitle": "用户用量分布",
|
||||
"distributionSubtitle": "按成功请求频率统计的用户数量",
|
||||
"usersCount": "{{count}} 个用户"
|
||||
},
|
||||
"priceDataReload": {
|
||||
"reloadButton": "重新加载价格数据",
|
||||
"noAccessToken": "无可用访问令牌",
|
||||
"reloadSuccess": "价格数据已成功重新加载!已更新 {{count}} 个模型。",
|
||||
"reloadFailed": "重新加载价格数据失败",
|
||||
"reloadFailedRetry": "重新加载价格数据失败,请重试。",
|
||||
"hoursMustBePositive": "小时数必须大于 0",
|
||||
"scheduleSuccess": "已设置每 {{hours}} 小时定期重新加载",
|
||||
"scheduleFailed": "设置定期重新加载失败",
|
||||
"scheduleFailedRetry": "设置定期重新加载失败,请重试。",
|
||||
"cancelSuccess": "定期重新加载已成功取消",
|
||||
"cancelFailed": "取消定期重新加载失败",
|
||||
"cancelFailedRetry": "取消定期重新加载失败,请重试。",
|
||||
"statusNotScheduled": "未设置计划",
|
||||
"statusReady": "就绪",
|
||||
"statusActive": "活跃",
|
||||
"hardRefreshTitle": "硬刷新价格数据",
|
||||
"hardRefreshDesc": "这将立即从远程源获取最新定价信息。是否继续?",
|
||||
"setupPeriodicReload": "设置定期重新加载",
|
||||
"cancelPeriodicReload": "取消定期重新加载",
|
||||
"pricingDataSource": "定价数据来源",
|
||||
"sourceRemote": "远程",
|
||||
"sourceLocal": "本地",
|
||||
"modelsLoaded": "已加载模型数:",
|
||||
"loadedFrom": "加载来源:",
|
||||
"attemptedUrl": "尝试的 URL:",
|
||||
"localModeForced": "已通过 LITELLM_LOCAL_MODEL_COST_MAP=True 强制使用本地模式",
|
||||
"fellBackToLocal": "已回退到本地:{{reason}}",
|
||||
"scheduledEvery": "每 {{hours}} 小时定期执行",
|
||||
"noPeriodicReload": "未设置定期重新加载",
|
||||
"lastRun": "上次运行:",
|
||||
"nextRun": "下次运行:",
|
||||
"scheduleModalTitle": "设置定期重新加载",
|
||||
"scheduleButton": "设置计划",
|
||||
"scheduleModalDesc": "设置每隔以下时间自动重新加载价格数据:",
|
||||
"hoursLabel": "小时",
|
||||
"scheduleModalHint": "系统将每 {{hours}} 小时自动从远程源获取最新定价数据。"
|
||||
},
|
||||
"prompts": {
|
||||
"addNewPrompt": "+ 添加新提示词",
|
||||
"uploadPromptFile": "上传 .prompt 文件",
|
||||
"allEnvironments": "所有环境",
|
||||
"envDevelopment": "开发环境",
|
||||
"envStaging": "预发环境",
|
||||
"envProduction": "生产环境",
|
||||
"deleteModalTitle": "删除提示词",
|
||||
"deleteConfirm": "确定要删除提示词:{{name}} 吗?",
|
||||
"deleteCannotUndo": "此操作无法撤销。",
|
||||
"deleteSuccess": "提示词「{{name}}」已成功删除",
|
||||
"deleteFailed": "删除提示词失败"
|
||||
},
|
||||
"passThroughInfo": {
|
||||
"notFound": "未找到透传端点",
|
||||
"backButton": "← 返回",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue