mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
feat(ui): i18n for router fallbacks settings
This commit is contained in:
parent
07ec61d295
commit
06e934a367
5 changed files with 133 additions and 47 deletions
|
|
@ -6,6 +6,7 @@
|
|||
import { Select, Tooltip } from "antd";
|
||||
import { AlertCircle, ArrowDown, X } from "lucide-react";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface FallbackGroup {
|
||||
id: string;
|
||||
|
|
@ -21,6 +22,7 @@ interface FallbackGroupConfigProps {
|
|||
}
|
||||
|
||||
export function FallbackGroupConfig({ group, onChange, availableModels, maxFallbacks }: FallbackGroupConfigProps) {
|
||||
const { t } = useTranslation();
|
||||
// Filter available options for fallbacks (exclude primary only, allow already selected to be shown for deselection)
|
||||
const availableFallbackOptions = availableModels.filter((m) => m !== group.primaryModel);
|
||||
|
||||
|
|
@ -62,12 +64,12 @@ export function FallbackGroupConfig({ group, onChange, availableModels, maxFallb
|
|||
{/* Primary Model Section */}
|
||||
<div className="relative">
|
||||
<label className="block text-sm font-semibold text-gray-700 mb-2">
|
||||
Primary Model <span className="text-red-500">*</span>
|
||||
{t("settingsPages.fallbackGroupConfig.primaryModelLabel")} <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<Select
|
||||
className="w-full h-12"
|
||||
size="large"
|
||||
placeholder="Select primary model"
|
||||
placeholder={t("settingsPages.fallbackGroupConfig.selectPrimaryModelPlaceholder")}
|
||||
value={group.primaryModel}
|
||||
onChange={handlePrimaryChange}
|
||||
showSearch
|
||||
|
|
@ -78,7 +80,7 @@ export function FallbackGroupConfig({ group, onChange, availableModels, maxFallb
|
|||
{!group.primaryModel && (
|
||||
<div className="mt-2 flex items-center gap-2 text-amber-600 text-xs bg-amber-50 p-2 rounded">
|
||||
<AlertCircle className="w-4 h-4" />
|
||||
<span>Select a model to begin configuring fallbacks</span>
|
||||
<span>{t("settingsPages.fallbackGroupConfig.selectModelHint")}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -87,7 +89,7 @@ export function FallbackGroupConfig({ group, onChange, availableModels, maxFallb
|
|||
<div className="flex items-center justify-center -my-4 z-10">
|
||||
<div className="bg-indigo-50 text-indigo-500 px-4 py-1 rounded-full text-xs font-bold border border-indigo-100 flex items-center gap-2 shadow-sm">
|
||||
<ArrowDown className="w-4 h-4" />
|
||||
IF FAILS, TRY...
|
||||
{t("settingsPages.fallbackGroupConfig.ifFailsTry")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -96,8 +98,10 @@ export function FallbackGroupConfig({ group, onChange, availableModels, maxFallb
|
|||
className={`transition-opacity duration-300 ${!group.primaryModel ? "opacity-50 pointer-events-none" : "opacity-100"}`}
|
||||
>
|
||||
<label className="block text-sm font-semibold text-gray-700 mb-2">
|
||||
Fallback Chain <span className="text-red-500">*</span>
|
||||
<span className="text-xs text-gray-500 font-normal ml-2">(Max {maxFallbacks} fallbacks at a time)</span>
|
||||
{t("settingsPages.fallbackGroupConfig.fallbackChainLabel")} <span className="text-red-500">*</span>
|
||||
<span className="text-xs text-gray-500 font-normal ml-2">
|
||||
{t("settingsPages.fallbackGroupConfig.maxFallbacksHint", { maxFallbacks })}
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<div className="bg-gray-50 rounded-xl p-4 border border-gray-200">
|
||||
|
|
@ -108,7 +112,9 @@ export function FallbackGroupConfig({ group, onChange, availableModels, maxFallb
|
|||
className="w-full"
|
||||
size="large"
|
||||
placeholder={
|
||||
canAddMoreFallbacks ? "Select fallback models to add..." : `Maximum ${maxFallbacks} fallbacks reached`
|
||||
canAddMoreFallbacks
|
||||
? t("settingsPages.fallbackGroupConfig.selectFallbackPlaceholder")
|
||||
: t("settingsPages.fallbackGroupConfig.maxFallbacksReached", { maxFallbacks })
|
||||
}
|
||||
value={group.fallbackModels}
|
||||
onChange={handleFallbackSelect}
|
||||
|
|
@ -146,8 +152,11 @@ export function FallbackGroupConfig({ group, onChange, availableModels, maxFallb
|
|||
/>
|
||||
<p className="text-xs text-gray-500 mt-1 ml-1">
|
||||
{canAddMoreFallbacks
|
||||
? `Search and select multiple models. Selected models will appear below in order. (${group.fallbackModels.length}/${maxFallbacks} used)`
|
||||
: `Maximum ${maxFallbacks} fallbacks reached. Remove some to add more.`}
|
||||
? t("settingsPages.fallbackGroupConfig.selectHint", {
|
||||
used: group.fallbackModels.length,
|
||||
max: maxFallbacks,
|
||||
})
|
||||
: t("settingsPages.fallbackGroupConfig.maxReachedHint", { maxFallbacks })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -155,8 +164,8 @@ export function FallbackGroupConfig({ group, onChange, availableModels, maxFallb
|
|||
<div className="space-y-2 min-h-[100px]">
|
||||
{group.fallbackModels.length === 0 ? (
|
||||
<div className="h-32 border-2 border-dashed border-gray-300 rounded-lg flex flex-col items-center justify-center text-gray-400">
|
||||
<span className="text-sm">No fallback models selected</span>
|
||||
<span className="text-xs mt-1">Add models from the dropdown above</span>
|
||||
<span className="text-sm">{t("settingsPages.fallbackGroupConfig.noFallbacksSelected")}</span>
|
||||
<span className="text-xs mt-1">{t("settingsPages.fallbackGroupConfig.addFromDropdown")}</span>
|
||||
</div>
|
||||
) : (
|
||||
group.fallbackModels.map((modelValue, index) => {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { Button } from "@tremor/react";
|
|||
import { Tabs } from "antd";
|
||||
import { Plus } from "lucide-react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import MessageManager from "@/components/molecules/message_manager";
|
||||
import { FallbackGroup, FallbackGroupConfig } from "./FallbackGroupConfig";
|
||||
|
||||
|
|
@ -26,6 +27,7 @@ export function FallbackSelectionForm({
|
|||
maxFallbacks = 10,
|
||||
maxGroups = 5,
|
||||
}: FallbackSelectionFormProps) {
|
||||
const { t } = useTranslation();
|
||||
const [activeKey, setActiveKey] = useState(groups.length > 0 ? groups[0].id : "1");
|
||||
|
||||
// Reset activeKey when groups change (e.g., when modal reopens)
|
||||
|
|
@ -61,7 +63,7 @@ export function FallbackSelectionForm({
|
|||
|
||||
const handleRemoveGroup = (targetId: string) => {
|
||||
if (groups.length === 1) {
|
||||
MessageManager.warning("At least one group is required");
|
||||
MessageManager.warning(t("settingsPages.fallbackSelectionForm.atLeastOneGroup"));
|
||||
return;
|
||||
}
|
||||
const newGroups = groups.filter((g) => g.id !== targetId);
|
||||
|
|
@ -78,7 +80,9 @@ export function FallbackSelectionForm({
|
|||
|
||||
// Generate tab items
|
||||
const items = groups.map((group, index) => {
|
||||
const label = group.primaryModel ? group.primaryModel : `Group ${index + 1}`;
|
||||
const label = group.primaryModel
|
||||
? group.primaryModel
|
||||
: t("settingsPages.fallbackSelectionForm.groupLabel", { number: index + 1 });
|
||||
return {
|
||||
key: group.id,
|
||||
label: label,
|
||||
|
|
@ -97,9 +101,9 @@ export function FallbackSelectionForm({
|
|||
if (groups.length === 0) {
|
||||
return (
|
||||
<div className="text-center py-12 bg-gray-50 rounded-lg border border-dashed border-gray-300">
|
||||
<p className="text-gray-500 mb-4">No fallback groups configured</p>
|
||||
<p className="text-gray-500 mb-4">{t("settingsPages.fallbackSelectionForm.noGroupsConfigured")}</p>
|
||||
<Button variant="primary" onClick={handleAddGroup} icon={() => <Plus className="w-4 h-4" />}>
|
||||
Create First Group
|
||||
{t("settingsPages.fallbackSelectionForm.createFirstGroup")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import { Icon, Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow
|
|||
import { Tooltip, Typography } from "antd";
|
||||
import openai from "openai";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { TFunction } from "i18next";
|
||||
import DeleteResourceModal from "../../../common_components/DeleteResourceModal";
|
||||
import { ProviderLogo } from "../../../molecules/models/ProviderLogo";
|
||||
import NotificationsManager from "../../../molecules/notifications_manager";
|
||||
|
|
@ -68,7 +70,7 @@ interface FallbacksProps {
|
|||
modelData: any;
|
||||
}
|
||||
|
||||
async function testFallbackModelResponse(selectedModel: string, accessToken: string) {
|
||||
async function testFallbackModelResponse(selectedModel: string, accessToken: string, t: TFunction) {
|
||||
const isLocal = process.env.NODE_ENV === "development";
|
||||
if (isLocal != true) {
|
||||
console.log = function () {};
|
||||
|
|
@ -81,7 +83,7 @@ async function testFallbackModelResponse(selectedModel: string, accessToken: str
|
|||
});
|
||||
|
||||
try {
|
||||
NotificationsManager.info("Testing fallback model response...");
|
||||
NotificationsManager.info(t("settingsPages.fallbacks.testingFallback"));
|
||||
|
||||
const response = await client.chat.completions.create({
|
||||
model: selectedModel,
|
||||
|
|
@ -96,26 +98,29 @@ async function testFallbackModelResponse(selectedModel: string, accessToken: str
|
|||
});
|
||||
|
||||
NotificationsManager.success(
|
||||
<span>
|
||||
Test model=<strong>{selectedModel}</strong>, received model=
|
||||
<strong>{response.model}</strong>. See{" "}
|
||||
<a
|
||||
href="#"
|
||||
onClick={() => window.open("https://docs.litellm.ai/docs/proxy/reliability", "_blank")}
|
||||
style={{ textDecoration: "underline", color: "blue" }}
|
||||
>
|
||||
curl
|
||||
</a>
|
||||
</span>,
|
||||
<Trans
|
||||
i18nKey="settingsPages.fallbacks.testSuccess"
|
||||
values={{ selectedModel, responseModel: response.model }}
|
||||
components={{
|
||||
testModel: <strong />,
|
||||
receivedModel: <strong />,
|
||||
curlLink: (
|
||||
<a
|
||||
href="#"
|
||||
onClick={() => window.open("https://docs.litellm.ai/docs/proxy/reliability", "_blank")}
|
||||
style={{ textDecoration: "underline", color: "blue" }}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
} catch (error) {
|
||||
NotificationsManager.fromBackend(
|
||||
`Error occurred while generating model response. Please try again. Error: ${error}`,
|
||||
);
|
||||
NotificationsManager.fromBackend(t("settingsPages.fallbacks.testError", { error }));
|
||||
}
|
||||
}
|
||||
|
||||
const Fallbacks: React.FC<FallbacksProps> = ({ accessToken, userRole, userID, modelData }) => {
|
||||
const { t } = useTranslation();
|
||||
const [routerSettings, setRouterSettings] = useState<{ [key: string]: any }>({});
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [fallbackToDelete, setFallbackToDelete] = useState<FallbackEntry | null>(null);
|
||||
|
|
@ -181,9 +186,9 @@ const Fallbacks: React.FC<FallbacksProps> = ({ accessToken, userRole, userID, mo
|
|||
try {
|
||||
await setCallbacksCall(accessToken, payload);
|
||||
setRouterSettings(updatedSettings);
|
||||
NotificationsManager.success("Router settings updated successfully");
|
||||
NotificationsManager.success(t("settingsPages.fallbacks.updateSuccess"));
|
||||
} catch (error) {
|
||||
NotificationsManager.fromBackend("Failed to update router settings: " + error);
|
||||
NotificationsManager.fromBackend(t("settingsPages.fallbacks.updateFailed", { error }));
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
setIsDeleteModalOpen(false);
|
||||
|
|
@ -220,7 +225,7 @@ const Fallbacks: React.FC<FallbacksProps> = ({ accessToken, userRole, userID, mo
|
|||
setRouterSettings(updatedSettings);
|
||||
} catch (error) {
|
||||
// Revert on error by refetching from server
|
||||
NotificationsManager.fromBackend("Failed to update router settings: " + error);
|
||||
NotificationsManager.fromBackend(t("settingsPages.fallbacks.updateFailed", { error }));
|
||||
if (accessToken && userRole && userID) {
|
||||
getCallbacksCall(accessToken, userID, userRole).then((data) => {
|
||||
let router_settings = data.router_settings;
|
||||
|
|
@ -251,17 +256,15 @@ const Fallbacks: React.FC<FallbacksProps> = ({ accessToken, userRole, userID, mo
|
|||
)}
|
||||
{!hasFallbacks ? (
|
||||
<div className="rounded-lg border border-gray-200 bg-gray-50 px-4 py-6 text-center">
|
||||
<Typography.Text type="secondary">
|
||||
No fallbacks configured. Add fallbacks to automatically try another model when the primary fails.
|
||||
</Typography.Text>
|
||||
<Typography.Text type="secondary">{t("settingsPages.fallbacks.noFallbacksConfigured")}</Typography.Text>
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Model Name</TableHeaderCell>
|
||||
<TableHeaderCell>Fallbacks</TableHeaderCell>
|
||||
<TableHeaderCell>Actions</TableHeaderCell>
|
||||
<TableHeaderCell>{t("settingsPages.fallbacks.modelNameColumn")}</TableHeaderCell>
|
||||
<TableHeaderCell>{t("settingsPages.fallbacks.fallbacksColumn")}</TableHeaderCell>
|
||||
<TableHeaderCell>{t("common.actions")}</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
|
||||
|
|
@ -276,15 +279,15 @@ const Fallbacks: React.FC<FallbacksProps> = ({ accessToken, userRole, userID, mo
|
|||
<TableCell className="align-top">
|
||||
{canModify && (
|
||||
<>
|
||||
<Tooltip title="Test fallback">
|
||||
<Tooltip title={t("settingsPages.fallbacks.testFallbackTooltip")}>
|
||||
<Icon
|
||||
icon={PlayIcon}
|
||||
size="sm"
|
||||
onClick={() => testFallbackModelResponse(Object.keys(item)[0], accessToken || "")}
|
||||
onClick={() => testFallbackModelResponse(Object.keys(item)[0], accessToken || "", t)}
|
||||
className="cursor-pointer hover:text-blue-600"
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="Delete fallback">
|
||||
<Tooltip title={t("settingsPages.fallbacks.deleteFallbackTooltip")}>
|
||||
<span
|
||||
data-testid="delete-fallback-button"
|
||||
role="button"
|
||||
|
|
@ -307,12 +310,12 @@ const Fallbacks: React.FC<FallbacksProps> = ({ accessToken, userRole, userID, mo
|
|||
)}
|
||||
<DeleteResourceModal
|
||||
isOpen={isDeleteModalOpen}
|
||||
title="Delete Fallback?"
|
||||
message="Are you sure you want to delete this fallback? This action cannot be undone."
|
||||
resourceInformationTitle="Fallback Information"
|
||||
title={t("settingsPages.fallbacks.deleteModalTitle")}
|
||||
message={t("settingsPages.fallbacks.deleteModalMessage")}
|
||||
resourceInformationTitle={t("settingsPages.fallbacks.deleteModalResourceTitle")}
|
||||
resourceInformation={[
|
||||
{
|
||||
label: "Model Name",
|
||||
label: t("settingsPages.fallbacks.modelNameColumn"),
|
||||
value: fallbackToDelete ? Object.keys(fallbackToDelete)[0] : "",
|
||||
code: true,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4450,6 +4450,41 @@
|
|||
"addFallbacksModal": {
|
||||
"title": "Configure Model Fallbacks",
|
||||
"subtitle": "Manage multiple fallback chains for different models (up to 5 groups at a time)"
|
||||
},
|
||||
"fallbackGroupConfig": {
|
||||
"primaryModelLabel": "Primary Model",
|
||||
"selectPrimaryModelPlaceholder": "Select primary model",
|
||||
"selectModelHint": "Select a model to begin configuring fallbacks",
|
||||
"ifFailsTry": "IF FAILS, TRY...",
|
||||
"fallbackChainLabel": "Fallback Chain",
|
||||
"maxFallbacksHint": "(Max {{maxFallbacks}} fallbacks at a time)",
|
||||
"selectFallbackPlaceholder": "Select fallback models to add...",
|
||||
"maxFallbacksReached": "Maximum {{maxFallbacks}} fallbacks reached",
|
||||
"selectHint": "Search and select multiple models. Selected models will appear below in order. ({{used}}/{{max}} used)",
|
||||
"maxReachedHint": "Maximum {{maxFallbacks}} fallbacks reached. Remove some to add more.",
|
||||
"noFallbacksSelected": "No fallback models selected",
|
||||
"addFromDropdown": "Add models from the dropdown above"
|
||||
},
|
||||
"fallbackSelectionForm": {
|
||||
"atLeastOneGroup": "At least one group is required",
|
||||
"groupLabel": "Group {{number}}",
|
||||
"noGroupsConfigured": "No fallback groups configured",
|
||||
"createFirstGroup": "Create First Group"
|
||||
},
|
||||
"fallbacks": {
|
||||
"testingFallback": "Testing fallback model response...",
|
||||
"testSuccess": "Test model=<testModel>{{selectedModel}}</testModel>, received model=<receivedModel>{{responseModel}}</receivedModel>. See <curlLink>curl</curlLink>",
|
||||
"testError": "Error occurred while generating model response. Please try again. Error: {{error}}",
|
||||
"updateSuccess": "Router settings updated successfully",
|
||||
"updateFailed": "Failed to update router settings: {{error}}",
|
||||
"noFallbacksConfigured": "No fallbacks configured. Add fallbacks to automatically try another model when the primary fails.",
|
||||
"modelNameColumn": "Model Name",
|
||||
"fallbacksColumn": "Fallbacks",
|
||||
"testFallbackTooltip": "Test fallback",
|
||||
"deleteFallbackTooltip": "Delete fallback",
|
||||
"deleteModalTitle": "Delete Fallback?",
|
||||
"deleteModalMessage": "Are you sure you want to delete this fallback? This action cannot be undone.",
|
||||
"deleteModalResourceTitle": "Fallback Information"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4450,6 +4450,41 @@
|
|||
"addFallbacksModal": {
|
||||
"title": "配置模型回退",
|
||||
"subtitle": "管理不同模型的多个回退链(每次最多 5 组)"
|
||||
},
|
||||
"fallbackGroupConfig": {
|
||||
"primaryModelLabel": "主模型",
|
||||
"selectPrimaryModelPlaceholder": "选择主模型",
|
||||
"selectModelHint": "选择一个模型以开始配置回退",
|
||||
"ifFailsTry": "若失败,尝试...",
|
||||
"fallbackChainLabel": "回退链",
|
||||
"maxFallbacksHint": "(每次最多 {{maxFallbacks}} 个回退)",
|
||||
"selectFallbackPlaceholder": "选择要添加的回退模型...",
|
||||
"maxFallbacksReached": "已达到最大 {{maxFallbacks}} 个回退",
|
||||
"selectHint": "搜索并选择多个模型,已选模型将按顺序显示在下方。(已用 {{used}}/{{max}})",
|
||||
"maxReachedHint": "已达到最大 {{maxFallbacks}} 个回退,请先移除一些再添加。",
|
||||
"noFallbacksSelected": "未选择回退模型",
|
||||
"addFromDropdown": "从上方下拉菜单添加模型"
|
||||
},
|
||||
"fallbackSelectionForm": {
|
||||
"atLeastOneGroup": "至少需要一个组",
|
||||
"groupLabel": "组 {{number}}",
|
||||
"noGroupsConfigured": "未配置回退组",
|
||||
"createFirstGroup": "创建第一个组"
|
||||
},
|
||||
"fallbacks": {
|
||||
"testingFallback": "正在测试回退模型响应...",
|
||||
"testSuccess": "测试模型=<testModel>{{selectedModel}}</testModel>,收到模型=<receivedModel>{{responseModel}}</receivedModel>。查看 <curlLink>curl</curlLink>",
|
||||
"testError": "生成模型响应时出错,请重试。错误:{{error}}",
|
||||
"updateSuccess": "路由器设置更新成功",
|
||||
"updateFailed": "更新路由器设置失败:{{error}}",
|
||||
"noFallbacksConfigured": "未配置回退。添加回退可在主模型失败时自动尝试其他模型。",
|
||||
"modelNameColumn": "模型名称",
|
||||
"fallbacksColumn": "回退",
|
||||
"testFallbackTooltip": "测试回退",
|
||||
"deleteFallbackTooltip": "删除回退",
|
||||
"deleteModalTitle": "删除回退?",
|
||||
"deleteModalMessage": "确定要删除此回退吗?此操作无法撤销。",
|
||||
"deleteModalResourceTitle": "回退信息"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue