mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
feat(ui): i18n for settings, route preview, transform request, and skill hub columns
This commit is contained in:
parent
3fd97f0fcc
commit
30a4169b08
9 changed files with 308 additions and 108 deletions
|
|
@ -4,7 +4,8 @@ import { SearchOutlined } from "@ant-design/icons";
|
|||
import { Input, Select } from "antd";
|
||||
import { Plugin } from "@/components/claude_code_plugins/types";
|
||||
import { ModelDataTable } from "@/components/model_dashboard/table";
|
||||
import { skillHubColumns } from "@/components/skill_hub_table_columns";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getSkillHubColumns } from "@/components/skill_hub_table_columns";
|
||||
import SkillDetail from "@/components/claude_code_plugins/skill_detail";
|
||||
|
||||
interface SkillHubDashboardProps {
|
||||
|
|
@ -24,6 +25,7 @@ const SkillHubDashboard: React.FC<SkillHubDashboardProps> = ({
|
|||
publicPage = false,
|
||||
onPublishSuccess,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [search, setSearch] = useState("");
|
||||
const [domainFilter, setDomainFilter] = useState<string | undefined>(undefined);
|
||||
const [selectedSkill, setSelectedSkill] = useState<Plugin | null>(null);
|
||||
|
|
@ -115,7 +117,7 @@ const SkillHubDashboard: React.FC<SkillHubDashboardProps> = ({
|
|||
</div>
|
||||
</div>
|
||||
<ModelDataTable
|
||||
columns={skillHubColumns((skill) => setSelectedSkill(skill), copyToClipboard, publicPage)}
|
||||
columns={getSkillHubColumns(t, (skill) => setSelectedSkill(skill), copyToClipboard, publicPage)}
|
||||
data={filteredSkills}
|
||||
isLoading={false}
|
||||
defaultSorting={[{ id: "name", desc: false }]}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import React, { useState } from "react";
|
|||
import { Button, Space } from "antd";
|
||||
import { MinusCircleOutlined, PlusOutlined } from "@ant-design/icons";
|
||||
import { TextInput } from "@tremor/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface QueryParamInputProps {
|
||||
value?: Record<string, string>;
|
||||
|
|
@ -9,6 +10,7 @@ interface QueryParamInputProps {
|
|||
}
|
||||
|
||||
const QueryParamInput: React.FC<QueryParamInputProps> = ({ value = {}, onChange }) => {
|
||||
const { t } = useTranslation();
|
||||
const [pairs, setPairs] = useState<[string, string][]>(Object.entries(value));
|
||||
|
||||
const handleAdd = () => {
|
||||
|
|
@ -33,12 +35,12 @@ const QueryParamInput: React.FC<QueryParamInputProps> = ({ value = {}, onChange
|
|||
{pairs.map(([key, val], index) => (
|
||||
<Space key={index} style={{ display: "flex", marginBottom: 8 }} align="center">
|
||||
<TextInput
|
||||
placeholder="Parameter Name (e.g., version)"
|
||||
placeholder={t("queryParamInput.paramNamePlaceholder")}
|
||||
value={key}
|
||||
onChange={(e) => handleChange(index, e.target.value, val)}
|
||||
/>
|
||||
<TextInput
|
||||
placeholder="Parameter Value (e.g., v1)"
|
||||
placeholder={t("queryParamInput.paramValuePlaceholder")}
|
||||
value={val}
|
||||
onChange={(e) => handleChange(index, key, e.target.value)}
|
||||
/>
|
||||
|
|
@ -48,7 +50,7 @@ const QueryParamInput: React.FC<QueryParamInputProps> = ({ value = {}, onChange
|
|||
</Space>
|
||||
))}
|
||||
<Button type="dashed" onClick={handleAdd} icon={<PlusOutlined />}>
|
||||
Add Query Parameter
|
||||
{t("queryParamInput.addButton")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import React from "react";
|
||||
import { Card, Typography } from "antd";
|
||||
import { RightOutlined, InfoCircleOutlined } from "@ant-design/icons";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
import { getProxyBaseUrl } from "./networking";
|
||||
|
|
@ -12,6 +13,7 @@ interface RoutePreviewProps {
|
|||
}
|
||||
|
||||
const RoutePreview: React.FC<RoutePreviewProps> = ({ pathValue, targetValue, includeSubpath }) => {
|
||||
const { t } = useTranslation();
|
||||
const proxyBaseUrl = getProxyBaseUrl();
|
||||
|
||||
const getLiteLLMProxyUrl = () => {
|
||||
|
|
@ -26,20 +28,20 @@ const RoutePreview: React.FC<RoutePreviewProps> = ({ pathValue, targetValue, inc
|
|||
return (
|
||||
<Card className="p-5">
|
||||
<Title level={5} className="text-lg font-semibold text-gray-900 mb-2">
|
||||
Route Preview
|
||||
{t("routePreview.title")}
|
||||
</Title>
|
||||
<Text type="secondary" className="text-gray-600 mb-5" style={{ display: "block" }}>
|
||||
How your requests will be routed
|
||||
{t("routePreview.subtitle")}
|
||||
</Text>
|
||||
|
||||
<div className="space-y-5">
|
||||
{/* Basic routing */}
|
||||
<div>
|
||||
<div className="text-base font-semibold text-gray-900 mb-3">Basic routing:</div>
|
||||
<div className="text-base font-semibold text-gray-900 mb-3">{t("routePreview.basicRouting")}</div>
|
||||
<div className="flex items-center gap-4">
|
||||
{/* Your endpoint */}
|
||||
<div className="flex-1 bg-gray-50 border border-gray-200 rounded-lg p-3">
|
||||
<div className="text-sm text-gray-600 mb-2">Your endpoint</div>
|
||||
<div className="text-sm text-gray-600 mb-2">{t("routePreview.yourEndpoint")}</div>
|
||||
<code className="font-mono text-sm text-gray-900">{getLiteLLMProxyUrl()}</code>
|
||||
</div>
|
||||
|
||||
|
|
@ -50,7 +52,7 @@ const RoutePreview: React.FC<RoutePreviewProps> = ({ pathValue, targetValue, inc
|
|||
|
||||
{/* Forwards to */}
|
||||
<div className="flex-1 bg-gray-50 border border-gray-200 rounded-lg p-3">
|
||||
<div className="text-sm text-gray-600 mb-2">Forwards to</div>
|
||||
<div className="text-sm text-gray-600 mb-2">{t("routePreview.forwardsTo")}</div>
|
||||
<code className="font-mono text-sm text-gray-900">{targetValue}</code>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -60,11 +62,11 @@ const RoutePreview: React.FC<RoutePreviewProps> = ({ pathValue, targetValue, inc
|
|||
<>
|
||||
{/* With subpaths */}
|
||||
<div>
|
||||
<div className="text-base font-semibold text-gray-900 mb-3">With subpaths:</div>
|
||||
<div className="text-base font-semibold text-gray-900 mb-3">{t("routePreview.withSubpaths")}</div>
|
||||
<div className="flex items-center gap-4">
|
||||
{/* Your endpoint + subpath */}
|
||||
<div className="flex-1 bg-gray-50 border border-gray-200 rounded-lg p-3">
|
||||
<div className="text-sm text-gray-600 mb-2">Your endpoint + subpath</div>
|
||||
<div className="text-sm text-gray-600 mb-2">{t("routePreview.yourEndpointSubpath")}</div>
|
||||
<code className="font-mono text-sm text-gray-900">
|
||||
{pathValue && `${proxyBaseUrl}${pathValue}`}
|
||||
<span className="text-blue-600">/v1/text-to-image/base/model</span>
|
||||
|
|
@ -78,7 +80,7 @@ const RoutePreview: React.FC<RoutePreviewProps> = ({ pathValue, targetValue, inc
|
|||
|
||||
{/* Forwards to with subpath */}
|
||||
<div className="flex-1 bg-gray-50 border border-gray-200 rounded-lg p-3">
|
||||
<div className="text-sm text-gray-600 mb-2">Forwards to</div>
|
||||
<div className="text-sm text-gray-600 mb-2">{t("routePreview.forwardsTo")}</div>
|
||||
<code className="font-mono text-sm text-gray-900">
|
||||
{targetValue}
|
||||
<span className="text-blue-600">/v1/text-to-image/base/model</span>
|
||||
|
|
@ -87,9 +89,7 @@ const RoutePreview: React.FC<RoutePreviewProps> = ({ pathValue, targetValue, inc
|
|||
</div>
|
||||
|
||||
{/* Note */}
|
||||
<div className="mt-3 text-sm text-gray-600">
|
||||
Any path after {pathValue} will be appended to the target URL
|
||||
</div>
|
||||
<div className="mt-3 text-sm text-gray-600">{t("routePreview.subpathNote", { pathValue })}</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
|
@ -99,10 +99,13 @@ const RoutePreview: React.FC<RoutePreviewProps> = ({ pathValue, targetValue, inc
|
|||
<div className="flex items-start">
|
||||
<InfoCircleOutlined className="text-blue-500 mt-0.5 mr-2 flex-shrink-0" />
|
||||
<div className="text-sm text-blue-700">
|
||||
<span className="font-medium">Not seeing the routing you wanted?</span> Try enabling - Include Subpaths
|
||||
- above - this allows subroutes like{" "}
|
||||
<code className="bg-blue-100 px-1 py-0.5 rounded font-mono text-xs">/api/v1/models</code> to be
|
||||
forwarded automatically.
|
||||
<Trans
|
||||
i18nKey="routePreview.subpathHint"
|
||||
components={{
|
||||
bold: <span className="font-medium" />,
|
||||
code: <code className="bg-blue-100 px-1 py-0.5 rounded font-mono text-xs" />,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import {
|
|||
TextInput,
|
||||
} from "@tremor/react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
|
||||
import { Button as Button2, Form, Input, Modal, Select, Typography } from "antd";
|
||||
import EmailSettings from "./email_settings";
|
||||
|
|
@ -62,6 +63,8 @@ interface DynamicParamsFieldsProps {
|
|||
}
|
||||
|
||||
const DynamicParamsFields: React.FC<DynamicParamsFieldsProps> = ({ params, callbackConfigs, selectedCallback }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!params || params.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -86,7 +89,7 @@ const DynamicParamsFields: React.FC<DynamicParamsFieldsProps> = ({ params, callb
|
|||
? [
|
||||
{
|
||||
required: true,
|
||||
message: `Please enter the ${fieldLabel.toLowerCase()}`,
|
||||
message: t("settings.dynamicParamRequired", { fieldLabel: fieldLabel.toLowerCase() }),
|
||||
},
|
||||
]
|
||||
: undefined
|
||||
|
|
@ -95,14 +98,14 @@ const DynamicParamsFields: React.FC<DynamicParamsFieldsProps> = ({ params, callb
|
|||
{paramType === "password" ? (
|
||||
<Input.Password
|
||||
size="large"
|
||||
placeholder={`Enter your ${fieldLabel.toLowerCase()}`}
|
||||
placeholder={t("settings.dynamicParamEnterYour", { fieldLabel: fieldLabel.toLowerCase() })}
|
||||
className="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
|
||||
/>
|
||||
) : paramType === "number" ? (
|
||||
<Input
|
||||
type="number"
|
||||
size="large"
|
||||
placeholder={`Enter ${fieldLabel.toLowerCase()}`}
|
||||
placeholder={t("settings.dynamicParamEnter", { fieldLabel: fieldLabel.toLowerCase() })}
|
||||
className="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
|
||||
min={0}
|
||||
max={1}
|
||||
|
|
@ -111,7 +114,7 @@ const DynamicParamsFields: React.FC<DynamicParamsFieldsProps> = ({ params, callb
|
|||
) : (
|
||||
<Input
|
||||
size="large"
|
||||
placeholder={`Enter your ${fieldLabel.toLowerCase()}`}
|
||||
placeholder={t("settings.dynamicParamEnterYour", { fieldLabel: fieldLabel.toLowerCase() })}
|
||||
className="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
|
||||
/>
|
||||
)}
|
||||
|
|
@ -136,14 +139,16 @@ const CallbackSelector: React.FC<CallbackSelectorProps> = ({
|
|||
onCallbackChange,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<FormItem
|
||||
label="Callback"
|
||||
label={t("settings.callbackLabel")}
|
||||
name="callback"
|
||||
rules={disabled ? undefined : [{ required: true, message: "Please select a callback" }]}
|
||||
rules={disabled ? undefined : [{ required: true, message: t("settings.callbackRequired") }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="Choose a logging callback..."
|
||||
placeholder={t("settings.callbackPlaceholder")}
|
||||
size="large"
|
||||
className="w-full"
|
||||
showSearch
|
||||
|
|
@ -214,6 +219,7 @@ const buildCallbackPayload = (formValues: Record<string, any>, callbackName: str
|
|||
};
|
||||
|
||||
const Settings: React.FC<SettingsPageProps> = ({ accessToken, userRole, userID, premiumUser }) => {
|
||||
const { t } = useTranslation();
|
||||
const [callbacks, setCallbacks] = useState<AlertingObject[]>([]);
|
||||
const [alerts, setAlerts] = useState<any[]>([]);
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
|
|
@ -256,7 +262,7 @@ const Settings: React.FC<SettingsPageProps> = ({ accessToken, userRole, userID,
|
|||
setCallbackConfigs(data || []);
|
||||
})
|
||||
.catch((error) => {
|
||||
NotificationsManager.fromBackend("Failed to load callback configs: " + parseErrorMessage(error));
|
||||
NotificationsManager.fromBackend(t("settings.loadCallbackConfigsFailed", { error: parseErrorMessage(error) }));
|
||||
});
|
||||
}, [accessToken]);
|
||||
|
||||
|
|
@ -337,7 +343,7 @@ const Settings: React.FC<SettingsPageProps> = ({ accessToken, userRole, userID,
|
|||
try {
|
||||
await setCallbacksCall(accessToken, payload);
|
||||
NotificationsManager.success(
|
||||
isEdit ? "Callback updated successfully" : `Callback ${callbackName} added successfully`,
|
||||
isEdit ? t("settings.callbackUpdatedSuccess") : t("settings.callbackAddedSuccess", { callbackName }),
|
||||
);
|
||||
|
||||
if (isEdit) {
|
||||
|
|
@ -412,7 +418,7 @@ const Settings: React.FC<SettingsPageProps> = ({ accessToken, userRole, userID,
|
|||
} catch (error) {
|
||||
NotificationsManager.fromBackend(error);
|
||||
}
|
||||
NotificationsManager.success("Alerts updated successfully");
|
||||
NotificationsManager.success(t("settings.alertsUpdatedSuccess"));
|
||||
};
|
||||
const handleSaveChanges = (callback: any) => {
|
||||
if (!accessToken) {
|
||||
|
|
@ -438,7 +444,7 @@ const Settings: React.FC<SettingsPageProps> = ({ accessToken, userRole, userID,
|
|||
} catch (error) {
|
||||
NotificationsManager.fromBackend(error);
|
||||
}
|
||||
NotificationsManager.success("Callback updated successfully");
|
||||
NotificationsManager.success(t("settings.callbackUpdatedSuccess"));
|
||||
};
|
||||
|
||||
const handleOk = () => {
|
||||
|
|
@ -541,7 +547,7 @@ const Settings: React.FC<SettingsPageProps> = ({ accessToken, userRole, userID,
|
|||
try {
|
||||
setIsDeletingCallback(true);
|
||||
await deleteCallback(accessToken, callbackToDelete.name);
|
||||
NotificationsManager.success(`Callback ${callbackToDelete.name} deleted successfully`);
|
||||
NotificationsManager.success(t("settings.callbackDeletedSuccess", { name: callbackToDelete.name }));
|
||||
|
||||
// Refresh the callbacks list
|
||||
if (userID && userRole) {
|
||||
|
|
@ -568,11 +574,11 @@ const Settings: React.FC<SettingsPageProps> = ({ accessToken, userRole, userID,
|
|||
<Grid numItems={1} className="gap-2 p-8 w-full mt-2">
|
||||
<TabGroup>
|
||||
<TabList variant="line" defaultValue="1">
|
||||
<Tab value="1">Logging Callbacks</Tab>
|
||||
<Tab value="2">CloudZero Cost Tracking</Tab>
|
||||
<Tab value="2">Alerting Types</Tab>
|
||||
<Tab value="3">Alerting Settings</Tab>
|
||||
<Tab value="4">Email Alerts</Tab>
|
||||
<Tab value="1">{t("settings.tabLoggingCallbacks")}</Tab>
|
||||
<Tab value="2">{t("settings.tabCloudZero")}</Tab>
|
||||
<Tab value="2">{t("settings.tabAlertingTypes")}</Tab>
|
||||
<Tab value="3">{t("settings.tabAlertingSettings")}</Tab>
|
||||
<Tab value="4">{t("settings.tabEmailAlerts")}</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
|
|
@ -588,7 +594,7 @@ const Settings: React.FC<SettingsPageProps> = ({ accessToken, userRole, userID,
|
|||
onTest={async (cb) => {
|
||||
try {
|
||||
await serviceHealthCheck(accessToken, cb.name);
|
||||
NotificationsManager.success("Health check triggered");
|
||||
NotificationsManager.success(t("settings.healthCheckTriggered"));
|
||||
} catch (error) {
|
||||
NotificationsManager.fromBackend(parseErrorMessage(error));
|
||||
}
|
||||
|
|
@ -603,17 +609,21 @@ const Settings: React.FC<SettingsPageProps> = ({ accessToken, userRole, userID,
|
|||
<TabPanel>
|
||||
<Card>
|
||||
<Text className="my-2">
|
||||
Alerts are only supported for Slack Webhook URLs. Get your webhook urls from{" "}
|
||||
<a href="https://api.slack.com/messaging/webhooks" target="_blank" style={{ color: "blue" }}>
|
||||
here
|
||||
</a>
|
||||
<Trans
|
||||
i18nKey="settings.alertsSlackOnly"
|
||||
components={{
|
||||
a: (
|
||||
<a href="https://api.slack.com/messaging/webhooks" target="_blank" style={{ color: "blue" }} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Text>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeaderCell></TableHeaderCell>
|
||||
<TableHeaderCell></TableHeaderCell>
|
||||
<TableHeaderCell>Slack Webhook URL</TableHeaderCell>
|
||||
<TableHeaderCell>{t("settings.slackWebhookUrlColumn")}</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
|
||||
|
|
@ -632,7 +642,7 @@ const Settings: React.FC<SettingsPageProps> = ({ accessToken, userRole, userID,
|
|||
) : (
|
||||
<Button className="flex items-center justify-center">
|
||||
<a href="https://forms.gle/W3U4PZpJGFHWtHyA9" target="_blank">
|
||||
✨ Enterprise Feature
|
||||
{t("settings.enterpriseFeature")}
|
||||
</a>
|
||||
</Button>
|
||||
)
|
||||
|
|
@ -664,23 +674,21 @@ const Settings: React.FC<SettingsPageProps> = ({ accessToken, userRole, userID,
|
|||
</TableBody>
|
||||
</Table>
|
||||
<Button size="xs" className="mt-2" onClick={handleSaveAlerts}>
|
||||
Save Changes
|
||||
{t("common.save")}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={async () => {
|
||||
try {
|
||||
await serviceHealthCheck(accessToken, "slack");
|
||||
NotificationsManager.success(
|
||||
"Alert test triggered. Test request to slack made - check logs/alerts on slack to verify",
|
||||
);
|
||||
NotificationsManager.success(t("settings.alertTestTriggered"));
|
||||
} catch (error) {
|
||||
NotificationsManager.fromBackend(parseErrorMessage(error));
|
||||
}
|
||||
}}
|
||||
className="mx-2"
|
||||
>
|
||||
Test Alerts
|
||||
{t("settings.testAlerts")}
|
||||
</Button>
|
||||
</Card>
|
||||
</TabPanel>
|
||||
|
|
@ -695,7 +703,7 @@ const Settings: React.FC<SettingsPageProps> = ({ accessToken, userRole, userID,
|
|||
</Grid>
|
||||
|
||||
<Modal
|
||||
title="Add Logging Callback"
|
||||
title={t("settings.addCallbackModalTitle")}
|
||||
open={showAddCallbacksModal}
|
||||
width={800}
|
||||
onCancel={() => {
|
||||
|
|
@ -712,7 +720,7 @@ const Settings: React.FC<SettingsPageProps> = ({ accessToken, userRole, userID,
|
|||
style={{ color: "blue" }}
|
||||
>
|
||||
{" "}
|
||||
LiteLLM Docs: Logging
|
||||
{t("settings.litellmDocsLogging")}
|
||||
</a>
|
||||
|
||||
<Form
|
||||
|
|
@ -744,10 +752,10 @@ const Settings: React.FC<SettingsPageProps> = ({ accessToken, userRole, userID,
|
|||
}}
|
||||
disabled={isAddingCallback}
|
||||
>
|
||||
Cancel
|
||||
{t("common.cancel")}
|
||||
</Button2>
|
||||
<Button2 htmlType="submit" loading={isAddingCallback} disabled={isAddingCallback}>
|
||||
{isAddingCallback ? "Adding..." : "Add Callback"}
|
||||
{isAddingCallback ? t("settings.adding") : t("settings.addCallback")}
|
||||
</Button2>
|
||||
</div>
|
||||
</Form>
|
||||
|
|
@ -756,7 +764,7 @@ const Settings: React.FC<SettingsPageProps> = ({ accessToken, userRole, userID,
|
|||
<Modal
|
||||
open={showEditCallback}
|
||||
width={800}
|
||||
title={"Edit Callback Settings"}
|
||||
title={t("settings.editCallbackModalTitle")}
|
||||
onCancel={() => {
|
||||
setShowEditCallback(false);
|
||||
setSelectedEditCallback(null);
|
||||
|
|
@ -801,7 +809,7 @@ const Settings: React.FC<SettingsPageProps> = ({ accessToken, userRole, userID,
|
|||
}}
|
||||
disabled={isUpdatingCallback}
|
||||
>
|
||||
Cancel
|
||||
{t("common.cancel")}
|
||||
</Button2>
|
||||
<Button2
|
||||
onClick={() => {
|
||||
|
|
@ -810,7 +818,7 @@ const Settings: React.FC<SettingsPageProps> = ({ accessToken, userRole, userID,
|
|||
loading={isUpdatingCallback}
|
||||
disabled={isUpdatingCallback}
|
||||
>
|
||||
{isUpdatingCallback ? "Saving..." : "Save Changes"}
|
||||
{isUpdatingCallback ? t("common.saving") : t("common.save")}
|
||||
</Button2>
|
||||
</div>
|
||||
</Form>
|
||||
|
|
@ -818,12 +826,12 @@ const Settings: React.FC<SettingsPageProps> = ({ accessToken, userRole, userID,
|
|||
|
||||
<DeleteResourceModal
|
||||
isOpen={showDeleteConfirmModal}
|
||||
title="Delete Callback"
|
||||
message="Are you sure you want to delete this callback? This action cannot be undone."
|
||||
resourceInformationTitle="Callback Information"
|
||||
title={t("settings.deleteCallbackTitle")}
|
||||
message={t("settings.deleteCallbackMessage")}
|
||||
resourceInformationTitle={t("settings.callbackInformationTitle")}
|
||||
resourceInformation={[
|
||||
{ label: "Callback Name", value: callbackToDelete?.name },
|
||||
{ label: "Mode", value: callbackToDelete?.mode || "success" },
|
||||
{ label: t("settings.callbackNameLabel"), value: callbackToDelete?.name },
|
||||
{ label: t("settings.callbackModeLabel"), value: callbackToDelete?.mode || "success" },
|
||||
]}
|
||||
onCancel={() => {
|
||||
setShowDeleteConfirmModal(false);
|
||||
|
|
|
|||
|
|
@ -2,15 +2,17 @@ import { ColumnDef } from "@tanstack/react-table";
|
|||
import { Badge, Text } from "@tremor/react";
|
||||
import { Tooltip } from "antd";
|
||||
import { CopyOutlined, LinkOutlined } from "@ant-design/icons";
|
||||
import { TFunction } from "i18next";
|
||||
import { Plugin } from "./claude_code_plugins/types";
|
||||
|
||||
export const skillHubColumns = (
|
||||
export const getSkillHubColumns = (
|
||||
t: TFunction,
|
||||
showModal: (skill: Plugin) => void,
|
||||
copyToClipboard: (text: string) => void,
|
||||
publicPage: boolean = false,
|
||||
): ColumnDef<Plugin>[] => [
|
||||
{
|
||||
header: "Skill Name",
|
||||
header: t("skillHubTableColumns.skillName"),
|
||||
accessorKey: "name",
|
||||
enableSorting: true,
|
||||
sortingFn: "alphanumeric",
|
||||
|
|
@ -26,7 +28,7 @@ export const skillHubColumns = (
|
|||
>
|
||||
{skill.name}
|
||||
</button>
|
||||
<Tooltip title="Copy skill name">
|
||||
<Tooltip title={t("skillHubTableColumns.copySkillName")}>
|
||||
<CopyOutlined
|
||||
onClick={() => copyToClipboard(skill.name)}
|
||||
className="cursor-pointer text-gray-500 hover:text-blue-500 text-xs"
|
||||
|
|
@ -41,13 +43,13 @@ export const skillHubColumns = (
|
|||
},
|
||||
},
|
||||
{
|
||||
header: "Description",
|
||||
header: t("skillHubTableColumns.description"),
|
||||
accessorKey: "description",
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => <Text className="text-xs line-clamp-2">{row.original.description || "-"}</Text>,
|
||||
},
|
||||
{
|
||||
header: "Category",
|
||||
header: t("skillHubTableColumns.category"),
|
||||
accessorKey: "category",
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => {
|
||||
|
|
@ -61,13 +63,13 @@ export const skillHubColumns = (
|
|||
},
|
||||
},
|
||||
{
|
||||
header: "Domain",
|
||||
header: t("skillHubTableColumns.domain"),
|
||||
accessorKey: "domain",
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => <Text className="text-xs">{row.original.domain || "-"}</Text>,
|
||||
},
|
||||
{
|
||||
header: "Source",
|
||||
header: t("skillHubTableColumns.source"),
|
||||
accessorKey: "source",
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => {
|
||||
|
|
@ -100,12 +102,12 @@ export const skillHubColumns = (
|
|||
},
|
||||
},
|
||||
{
|
||||
header: "Status",
|
||||
header: t("common.status"),
|
||||
accessorKey: "enabled",
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => (
|
||||
<Badge color={row.original.enabled ? "green" : "gray"} size="xs">
|
||||
{row.original.enabled ? "Public" : "Draft"}
|
||||
{row.original.enabled ? t("skillHubTableColumns.statusPublic") : t("skillHubTableColumns.statusDraft")}
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { CopyOutlined } from "@ant-design/icons";
|
|||
import { Title } from "@tremor/react";
|
||||
import { transformRequestCall } from "./networking";
|
||||
import NotificationsManager from "./molecules/notifications_manager";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
interface TransformRequestPanelProps {
|
||||
accessToken: string | null;
|
||||
}
|
||||
|
|
@ -15,6 +16,7 @@ interface TransformResponse {
|
|||
}
|
||||
|
||||
const TransformRequestPanel: React.FC<TransformRequestPanelProps> = ({ accessToken }) => {
|
||||
const { t } = useTranslation();
|
||||
const [originalRequestJSON, setOriginalRequestJSON] = useState(`{
|
||||
"model": "openai/gpt-4o",
|
||||
"messages": [
|
||||
|
|
@ -72,7 +74,7 @@ ${formattedBody}
|
|||
try {
|
||||
requestBody = JSON.parse(originalRequestJSON);
|
||||
} catch (e) {
|
||||
NotificationsManager.fromBackend("Invalid JSON in request body");
|
||||
NotificationsManager.fromBackend(t("transformRequest.invalidJson"));
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
|
@ -85,7 +87,7 @@ ${formattedBody}
|
|||
|
||||
// Make the API call using fetch
|
||||
if (!accessToken) {
|
||||
NotificationsManager.fromBackend("No access token found");
|
||||
NotificationsManager.fromBackend(t("transformRequest.noAccessToken"));
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
|
@ -103,17 +105,17 @@ ${formattedBody}
|
|||
|
||||
// Update state with the formatted curl command
|
||||
setTransformedResponse(formattedCurl);
|
||||
NotificationsManager.success("Request transformed successfully");
|
||||
NotificationsManager.success(t("transformRequest.transformedSuccess"));
|
||||
} else {
|
||||
// Handle the case where the API returns a different format
|
||||
// Try to extract the parts from a string response if needed
|
||||
const rawText = typeof data === "string" ? data : JSON.stringify(data);
|
||||
setTransformedResponse(rawText);
|
||||
NotificationsManager.info("Transformed request received in unexpected format");
|
||||
NotificationsManager.info(t("transformRequest.unexpectedFormat"));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error transforming request:", err);
|
||||
NotificationsManager.fromBackend("Failed to transform request");
|
||||
NotificationsManager.fromBackend(t("transformRequest.transformFailed"));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
|
@ -129,8 +131,8 @@ ${formattedBody}
|
|||
|
||||
return (
|
||||
<div className="w-full m-2" style={{ overflow: "hidden" }}>
|
||||
<Title>Playground</Title>
|
||||
<p className="text-sm text-gray-500">See how LiteLLM transforms your request for the specified provider.</p>
|
||||
<Title>{t("transformRequest.title")}</Title>
|
||||
<p className="text-sm text-gray-500">{t("transformRequest.subtitle")}</p>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
|
|
@ -156,10 +158,10 @@ ${formattedBody}
|
|||
}}
|
||||
>
|
||||
<div style={{ marginBottom: "24px" }}>
|
||||
<h2 style={{ fontSize: "24px", fontWeight: "bold", margin: "0 0 4px 0" }}>Original Request</h2>
|
||||
<p style={{ color: "#666", margin: 0 }}>
|
||||
The request you would send to LiteLLM /chat/completions endpoint.
|
||||
</p>
|
||||
<h2 style={{ fontSize: "24px", fontWeight: "bold", margin: "0 0 4px 0" }}>
|
||||
{t("transformRequest.originalRequestTitle")}
|
||||
</h2>
|
||||
<p style={{ color: "#666", margin: 0 }}>{t("transformRequest.originalRequestDesc")}</p>
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
|
|
@ -179,7 +181,7 @@ ${formattedBody}
|
|||
value={originalRequestJSON}
|
||||
onChange={(e) => setOriginalRequestJSON(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Press Cmd/Ctrl + Enter to transform"
|
||||
placeholder={t("transformRequest.keyboardShortcutHint")}
|
||||
/>
|
||||
|
||||
<div
|
||||
|
|
@ -200,7 +202,7 @@ ${formattedBody}
|
|||
onClick={handleTransform}
|
||||
loading={isLoading}
|
||||
>
|
||||
<span>Transform</span>
|
||||
<span>{t("transformRequest.transformButton")}</span>
|
||||
<span>→</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -221,11 +223,13 @@ ${formattedBody}
|
|||
}}
|
||||
>
|
||||
<div style={{ marginBottom: "24px" }}>
|
||||
<h2 style={{ fontSize: "24px", fontWeight: "bold", margin: "0 0 4px 0" }}>Transformed Request</h2>
|
||||
<p style={{ color: "#666", margin: 0 }}>How LiteLLM transforms your request for the specified provider.</p>
|
||||
<h2 style={{ fontSize: "24px", fontWeight: "bold", margin: "0 0 4px 0" }}>
|
||||
{t("transformRequest.transformedRequestTitle")}
|
||||
</h2>
|
||||
<p style={{ color: "#666", margin: 0 }}>{t("transformRequest.transformedRequestDesc")}</p>
|
||||
<br />
|
||||
<p style={{ color: "#666", margin: 0 }} className="text-xs">
|
||||
Note: Sensitive headers are not shown.
|
||||
{t("transformRequest.sensitiveHeadersNote")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -278,7 +282,7 @@ ${formattedBody}
|
|||
size="small"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(transformedResponse || "");
|
||||
NotificationsManager.success("Copied to clipboard");
|
||||
NotificationsManager.success(t("common.copied"));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -286,11 +290,12 @@ ${formattedBody}
|
|||
</div>
|
||||
<div className="mt-4 text-right w-full">
|
||||
<p className="text-sm text-gray-500">
|
||||
Found an error? File an issue{" "}
|
||||
<a href="https://github.com/BerriAI/litellm/issues" target="_blank" rel="noopener noreferrer">
|
||||
here
|
||||
</a>
|
||||
.
|
||||
<Trans
|
||||
i18nKey="transformRequest.foundAnError"
|
||||
components={{
|
||||
a: <a href="https://github.com/BerriAI/litellm/issues" target="_blank" rel="noopener noreferrer" />,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { Card, Title, Text, TextInput, Button } from "@tremor/react";
|
|||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
import { getProxyBaseUrl, getGlobalLitellmHeaderName } from "@/components/networking";
|
||||
import NotificationsManager from "./molecules/notifications_manager";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface UIThemeSettingsProps {
|
||||
userID: string | null;
|
||||
|
|
@ -11,6 +12,7 @@ interface UIThemeSettingsProps {
|
|||
}
|
||||
|
||||
const UIThemeSettings: React.FC<UIThemeSettingsProps> = ({ userID, userRole, accessToken }) => {
|
||||
const { t } = useTranslation();
|
||||
const { logoUrl, setLogoUrl, faviconUrl, setFaviconUrl } = useTheme();
|
||||
const [logoUrlInput, setLogoUrlInput] = useState<string>("");
|
||||
const [faviconUrlInput, setFaviconUrlInput] = useState<string>("");
|
||||
|
|
@ -62,7 +64,7 @@ const UIThemeSettings: React.FC<UIThemeSettingsProps> = ({ userID, userRole, acc
|
|||
}),
|
||||
});
|
||||
if (response.ok) {
|
||||
NotificationsManager.success("Theme settings updated successfully!");
|
||||
NotificationsManager.success(t("uiThemeSettings.saveSuccess"));
|
||||
setLogoUrl(logoUrlInput || null);
|
||||
setFaviconUrl(faviconUrlInput || null);
|
||||
} else {
|
||||
|
|
@ -70,7 +72,7 @@ const UIThemeSettings: React.FC<UIThemeSettingsProps> = ({ userID, userRole, acc
|
|||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating theme settings:", error);
|
||||
NotificationsManager.fromBackend("Failed to update theme settings");
|
||||
NotificationsManager.fromBackend(t("uiThemeSettings.saveFailed"));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
|
@ -94,13 +96,13 @@ const UIThemeSettings: React.FC<UIThemeSettingsProps> = ({ userID, userRole, acc
|
|||
body: JSON.stringify({ logo_url: null, favicon_url: null }),
|
||||
});
|
||||
if (response.ok) {
|
||||
NotificationsManager.success("Theme settings reset to default!");
|
||||
NotificationsManager.success(t("uiThemeSettings.resetSuccess"));
|
||||
} else {
|
||||
throw new Error("Failed to reset");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error resetting theme settings:", error);
|
||||
NotificationsManager.fromBackend("Failed to reset theme settings");
|
||||
NotificationsManager.fromBackend(t("uiThemeSettings.resetFailed"));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
|
@ -113,13 +115,13 @@ const UIThemeSettings: React.FC<UIThemeSettingsProps> = ({ userID, userRole, acc
|
|||
return (
|
||||
<div className="w-full mx-auto max-w-4xl px-6 py-8">
|
||||
<div className="mb-8">
|
||||
<Title className="text-2xl font-bold mb-2">UI Theme Customization</Title>
|
||||
<Text className="text-gray-600">Customize your LiteLLM admin dashboard with a custom logo and favicon.</Text>
|
||||
<Title className="text-2xl font-bold mb-2">{t("uiThemeSettings.title")}</Title>
|
||||
<Text className="text-gray-600">{t("uiThemeSettings.subtitle")}</Text>
|
||||
</div>
|
||||
<Card className="shadow-sm p-6">
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<Text className="text-sm font-medium text-gray-700 mb-2 block">Custom Logo URL</Text>
|
||||
<Text className="text-sm font-medium text-gray-700 mb-2 block">{t("uiThemeSettings.logoUrlLabel")}</Text>
|
||||
<TextInput
|
||||
placeholder="https://example.com/logo.png"
|
||||
value={logoUrlInput}
|
||||
|
|
@ -129,12 +131,10 @@ const UIThemeSettings: React.FC<UIThemeSettingsProps> = ({ userID, userRole, acc
|
|||
}}
|
||||
className="w-full"
|
||||
/>
|
||||
<Text className="text-xs text-gray-500 mt-1">
|
||||
Enter a URL for your custom logo or leave empty for default
|
||||
</Text>
|
||||
<Text className="text-xs text-gray-500 mt-1">{t("uiThemeSettings.logoUrlHint")}</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="text-sm font-medium text-gray-700 mb-2 block">Custom Favicon URL</Text>
|
||||
<Text className="text-sm font-medium text-gray-700 mb-2 block">{t("uiThemeSettings.faviconUrlLabel")}</Text>
|
||||
<TextInput
|
||||
placeholder="https://example.com/favicon.ico"
|
||||
value={faviconUrlInput}
|
||||
|
|
@ -144,16 +144,14 @@ const UIThemeSettings: React.FC<UIThemeSettingsProps> = ({ userID, userRole, acc
|
|||
}}
|
||||
className="w-full"
|
||||
/>
|
||||
<Text className="text-xs text-gray-500 mt-1">
|
||||
Enter a URL for your custom favicon (.ico, .png, or .svg) or leave empty for default
|
||||
</Text>
|
||||
<Text className="text-xs text-gray-500 mt-1">{t("uiThemeSettings.faviconUrlHint")}</Text>
|
||||
</div>
|
||||
<div className="flex gap-3 pt-4">
|
||||
<Button onClick={handleSave} loading={loading} disabled={loading} color="indigo">
|
||||
Save Changes
|
||||
{t("common.save")}
|
||||
</Button>
|
||||
<Button onClick={handleReset} loading={loading} disabled={loading} variant="secondary" color="gray">
|
||||
Reset to Default
|
||||
{t("uiThemeSettings.resetToDefault")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1773,5 +1773,95 @@
|
|||
"updateFailed": "Failed to update pass through endpoint",
|
||||
"deleteSuccess": "Pass through endpoint deleted successfully",
|
||||
"deleteFailed": "Failed to delete pass through endpoint"
|
||||
},
|
||||
"queryParamInput": {
|
||||
"paramNamePlaceholder": "Parameter Name (e.g., version)",
|
||||
"paramValuePlaceholder": "Parameter Value (e.g., v1)",
|
||||
"addButton": "Add Query Parameter"
|
||||
},
|
||||
"routePreview": {
|
||||
"title": "Route Preview",
|
||||
"subtitle": "How your requests will be routed",
|
||||
"basicRouting": "Basic routing:",
|
||||
"yourEndpoint": "Your endpoint",
|
||||
"forwardsTo": "Forwards to",
|
||||
"withSubpaths": "With subpaths:",
|
||||
"yourEndpointSubpath": "Your endpoint + subpath",
|
||||
"subpathNote": "Any path after {{pathValue}} will be appended to the target URL",
|
||||
"subpathHint": "<bold>Not seeing the routing you wanted?</bold> Try enabling - Include Subpaths - above - this allows subroutes like <code>/api/v1/models</code> to be forwarded automatically."
|
||||
},
|
||||
"settings": {
|
||||
"tabLoggingCallbacks": "Logging Callbacks",
|
||||
"tabCloudZero": "CloudZero Cost Tracking",
|
||||
"tabAlertingTypes": "Alerting Types",
|
||||
"tabAlertingSettings": "Alerting Settings",
|
||||
"tabEmailAlerts": "Email Alerts",
|
||||
"callbackLabel": "Callback",
|
||||
"callbackRequired": "Please select a callback",
|
||||
"callbackPlaceholder": "Choose a logging callback...",
|
||||
"dynamicParamRequired": "Please enter the {{fieldLabel}}",
|
||||
"dynamicParamEnterYour": "Enter your {{fieldLabel}}",
|
||||
"dynamicParamEnter": "Enter {{fieldLabel}}",
|
||||
"loadCallbackConfigsFailed": "Failed to load callback configs: {{error}}",
|
||||
"callbackUpdatedSuccess": "Callback updated successfully",
|
||||
"callbackAddedSuccess": "Callback {{callbackName}} added successfully",
|
||||
"callbackDeletedSuccess": "Callback {{name}} deleted successfully",
|
||||
"alertsUpdatedSuccess": "Alerts updated successfully",
|
||||
"healthCheckTriggered": "Health check triggered",
|
||||
"alertTestTriggered": "Alert test triggered. Test request to slack made - check logs/alerts on slack to verify",
|
||||
"alertsSlackOnly": "Alerts are only supported for Slack Webhook URLs. Get your webhook urls from <a>here</a>",
|
||||
"slackWebhookUrlColumn": "Slack Webhook URL",
|
||||
"enterpriseFeature": "✨ Enterprise Feature",
|
||||
"testAlerts": "Test Alerts",
|
||||
"addCallbackModalTitle": "Add Logging Callback",
|
||||
"litellmDocsLogging": "LiteLLM Docs: Logging",
|
||||
"adding": "Adding...",
|
||||
"addCallback": "Add Callback",
|
||||
"editCallbackModalTitle": "Edit Callback Settings",
|
||||
"deleteCallbackTitle": "Delete Callback",
|
||||
"deleteCallbackMessage": "Are you sure you want to delete this callback? This action cannot be undone.",
|
||||
"callbackInformationTitle": "Callback Information",
|
||||
"callbackNameLabel": "Callback Name",
|
||||
"callbackModeLabel": "Mode"
|
||||
},
|
||||
"skillHubTableColumns": {
|
||||
"skillName": "Skill Name",
|
||||
"copySkillName": "Copy skill name",
|
||||
"description": "Description",
|
||||
"category": "Category",
|
||||
"domain": "Domain",
|
||||
"source": "Source",
|
||||
"statusPublic": "Public",
|
||||
"statusDraft": "Draft"
|
||||
},
|
||||
"transformRequest": {
|
||||
"title": "Playground",
|
||||
"subtitle": "See how LiteLLM transforms your request for the specified provider.",
|
||||
"invalidJson": "Invalid JSON in request body",
|
||||
"noAccessToken": "No access token found",
|
||||
"transformedSuccess": "Request transformed successfully",
|
||||
"unexpectedFormat": "Transformed request received in unexpected format",
|
||||
"transformFailed": "Failed to transform request",
|
||||
"originalRequestTitle": "Original Request",
|
||||
"originalRequestDesc": "The request you would send to LiteLLM /chat/completions endpoint.",
|
||||
"keyboardShortcutHint": "Press Cmd/Ctrl + Enter to transform",
|
||||
"transformButton": "Transform",
|
||||
"transformedRequestTitle": "Transformed Request",
|
||||
"transformedRequestDesc": "How LiteLLM transforms your request for the specified provider.",
|
||||
"sensitiveHeadersNote": "Note: Sensitive headers are not shown.",
|
||||
"foundAnError": "Found an error? File an issue <a>here</a>."
|
||||
},
|
||||
"uiThemeSettings": {
|
||||
"title": "UI Theme Customization",
|
||||
"subtitle": "Customize your LiteLLM admin dashboard with a custom logo and favicon.",
|
||||
"logoUrlLabel": "Custom Logo URL",
|
||||
"logoUrlHint": "Enter a URL for your custom logo or leave empty for default",
|
||||
"faviconUrlLabel": "Custom Favicon URL",
|
||||
"faviconUrlHint": "Enter a URL for your custom favicon (.ico, .png, or .svg) or leave empty for default",
|
||||
"saveSuccess": "Theme settings updated successfully!",
|
||||
"saveFailed": "Failed to update theme settings",
|
||||
"resetSuccess": "Theme settings reset to default!",
|
||||
"resetFailed": "Failed to reset theme settings",
|
||||
"resetToDefault": "Reset to Default"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1773,5 +1773,95 @@
|
|||
"updateFailed": "更新透传端点失败",
|
||||
"deleteSuccess": "透传端点已成功删除",
|
||||
"deleteFailed": "删除透传端点失败"
|
||||
},
|
||||
"queryParamInput": {
|
||||
"paramNamePlaceholder": "参数名称(如:version)",
|
||||
"paramValuePlaceholder": "参数值(如:v1)",
|
||||
"addButton": "添加查询参数"
|
||||
},
|
||||
"routePreview": {
|
||||
"title": "路由预览",
|
||||
"subtitle": "您的请求将如何被路由",
|
||||
"basicRouting": "基本路由:",
|
||||
"yourEndpoint": "您的端点",
|
||||
"forwardsTo": "转发至",
|
||||
"withSubpaths": "包含子路径:",
|
||||
"yourEndpointSubpath": "您的端点 + 子路径",
|
||||
"subpathNote": "{{pathValue}} 之后的任何路径都将追加到目标 URL",
|
||||
"subpathHint": "<bold>未看到预期的路由?</bold>请尝试启用上方的「包含子路径」选项,这将允许子路由(如 <code>/api/v1/models</code>)自动转发。"
|
||||
},
|
||||
"settings": {
|
||||
"tabLoggingCallbacks": "日志回调",
|
||||
"tabCloudZero": "CloudZero 成本追踪",
|
||||
"tabAlertingTypes": "告警类型",
|
||||
"tabAlertingSettings": "告警设置",
|
||||
"tabEmailAlerts": "邮件告警",
|
||||
"callbackLabel": "回调",
|
||||
"callbackRequired": "请选择一个回调",
|
||||
"callbackPlaceholder": "选择日志回调...",
|
||||
"dynamicParamRequired": "请输入 {{fieldLabel}}",
|
||||
"dynamicParamEnterYour": "输入您的 {{fieldLabel}}",
|
||||
"dynamicParamEnter": "输入 {{fieldLabel}}",
|
||||
"loadCallbackConfigsFailed": "加载回调配置失败:{{error}}",
|
||||
"callbackUpdatedSuccess": "回调更新成功",
|
||||
"callbackAddedSuccess": "回调 {{callbackName}} 添加成功",
|
||||
"callbackDeletedSuccess": "回调 {{name}} 删除成功",
|
||||
"alertsUpdatedSuccess": "告警更新成功",
|
||||
"healthCheckTriggered": "健康检查已触发",
|
||||
"alertTestTriggered": "告警测试已触发,已向 Slack 发送测试请求,请检查 Slack 上的日志/告警进行验证",
|
||||
"alertsSlackOnly": "告警仅支持 Slack Webhook URL。从<a>此处</a>获取您的 Webhook URL",
|
||||
"slackWebhookUrlColumn": "Slack Webhook URL",
|
||||
"enterpriseFeature": "✨ 企业版功能",
|
||||
"testAlerts": "测试告警",
|
||||
"addCallbackModalTitle": "添加日志回调",
|
||||
"litellmDocsLogging": "LiteLLM 文档:日志",
|
||||
"adding": "添加中...",
|
||||
"addCallback": "添加回调",
|
||||
"editCallbackModalTitle": "编辑回调设置",
|
||||
"deleteCallbackTitle": "删除回调",
|
||||
"deleteCallbackMessage": "确定要删除此回调吗?此操作无法撤销。",
|
||||
"callbackInformationTitle": "回调信息",
|
||||
"callbackNameLabel": "回调名称",
|
||||
"callbackModeLabel": "模式"
|
||||
},
|
||||
"skillHubTableColumns": {
|
||||
"skillName": "技能名称",
|
||||
"copySkillName": "复制技能名称",
|
||||
"description": "描述",
|
||||
"category": "分类",
|
||||
"domain": "域",
|
||||
"source": "来源",
|
||||
"statusPublic": "公开",
|
||||
"statusDraft": "草稿"
|
||||
},
|
||||
"transformRequest": {
|
||||
"title": "Playground",
|
||||
"subtitle": "查看 LiteLLM 如何为指定提供商转换您的请求。",
|
||||
"invalidJson": "请求体中的 JSON 无效",
|
||||
"noAccessToken": "未找到访问令牌",
|
||||
"transformedSuccess": "请求转换成功",
|
||||
"unexpectedFormat": "收到格式异常的转换请求",
|
||||
"transformFailed": "请求转换失败",
|
||||
"originalRequestTitle": "原始请求",
|
||||
"originalRequestDesc": "您向 LiteLLM /chat/completions 端点发送的请求。",
|
||||
"keyboardShortcutHint": "按 Cmd/Ctrl + Enter 进行转换",
|
||||
"transformButton": "转换",
|
||||
"transformedRequestTitle": "转换后的请求",
|
||||
"transformedRequestDesc": "LiteLLM 如何为指定提供商转换您的请求。",
|
||||
"sensitiveHeadersNote": "注意:敏感请求头不会显示。",
|
||||
"foundAnError": "发现错误?请在<a>此处</a>提交 issue。"
|
||||
},
|
||||
"uiThemeSettings": {
|
||||
"title": "界面主题自定义",
|
||||
"subtitle": "为您的 LiteLLM 管理仪表盘自定义 Logo 和 Favicon。",
|
||||
"logoUrlLabel": "自定义 Logo URL",
|
||||
"logoUrlHint": "输入自定义 Logo 的 URL,留空则使用默认图标",
|
||||
"faviconUrlLabel": "自定义 Favicon URL",
|
||||
"faviconUrlHint": "输入自定义 Favicon 的 URL(.ico、.png 或 .svg),留空则使用默认图标",
|
||||
"saveSuccess": "主题设置更新成功!",
|
||||
"saveFailed": "更新主题设置失败",
|
||||
"resetSuccess": "主题设置已重置为默认!",
|
||||
"resetFailed": "重置主题设置失败",
|
||||
"resetToDefault": "重置为默认"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue