mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
feat(ui): i18n for add_model batch 1
This commit is contained in:
parent
13276aea0a
commit
203eaff982
8 changed files with 516 additions and 179 deletions
|
|
@ -7,6 +7,7 @@ import type { FormInstance } from "antd";
|
|||
import { Select as AntdSelect, Button, Card, Col, Form, Modal, Row, Tooltip, Typography, Alert } from "antd";
|
||||
import type { UploadProps } from "antd/es/upload";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import TeamDropdown from "../common_components/team_dropdown";
|
||||
import type { Team } from "../key_team_helpers/key_list";
|
||||
import { type CredentialItem, type ProviderCreateInfo, modelAvailableCall } from "../networking";
|
||||
|
|
@ -17,7 +18,7 @@ import ConditionalPublicModelName from "./conditional_public_model_name";
|
|||
import LiteLLMModelNameField from "./litellm_model_name";
|
||||
import ConnectionErrorDisplay from "./model_connection_test";
|
||||
import ProviderSpecificFields from "./provider_specific_fields";
|
||||
import { TEST_MODES } from "./add_model_modes";
|
||||
import { getTestModes } from "./add_model_modes";
|
||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
|
||||
interface AddModelFormProps {
|
||||
|
|
@ -51,6 +52,7 @@ const AddModelForm: React.FC<AddModelFormProps> = ({
|
|||
teams,
|
||||
credentials,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [testMode, setTestMode] = useState<string>("chat");
|
||||
const [isResultModalVisible, setIsResultModalVisible] = useState<boolean>(false);
|
||||
const [isTestingConnection, setIsTestingConnection] = useState<boolean>(false);
|
||||
|
|
@ -86,6 +88,8 @@ const AddModelForm: React.FC<AddModelFormProps> = ({
|
|||
fetchModelAccessGroups();
|
||||
}, [accessToken]);
|
||||
|
||||
const testModes = useMemo(() => getTestModes(t), [t]);
|
||||
|
||||
const sortedProviderMetadata: ProviderCreateInfo[] = useMemo(() => {
|
||||
if (!providerMetadata) {
|
||||
return [];
|
||||
|
|
@ -96,7 +100,7 @@ const AddModelForm: React.FC<AddModelFormProps> = ({
|
|||
const providerMetadataErrorText = providerMetadataError
|
||||
? providerMetadataError instanceof Error
|
||||
? providerMetadataError.message
|
||||
: "Failed to load providers"
|
||||
: t("addModel.addModelForm.providerLoadFailed")
|
||||
: null;
|
||||
|
||||
const isAdmin = all_admin_roles.includes(userRole);
|
||||
|
|
@ -104,7 +108,7 @@ const AddModelForm: React.FC<AddModelFormProps> = ({
|
|||
|
||||
return (
|
||||
<>
|
||||
<Title level={2}>Add Model</Title>
|
||||
<Title level={2}>{t("addModel.addModelForm.title")}</Title>
|
||||
|
||||
<Card>
|
||||
<Form
|
||||
|
|
@ -126,10 +130,10 @@ const AddModelForm: React.FC<AddModelFormProps> = ({
|
|||
{isTeamAdmin && !isAdmin && (
|
||||
<>
|
||||
<Form.Item
|
||||
label="Select Team"
|
||||
label={t("addModel.addModelForm.selectTeamLabel")}
|
||||
name="team_id"
|
||||
rules={[{ required: true, message: "Please select a team to continue" }]}
|
||||
tooltip="Select the team for which you want to add this model"
|
||||
rules={[{ required: true, message: t("addModel.addModelForm.selectTeamRequired") }]}
|
||||
tooltip={t("addModel.addModelForm.selectTeamTooltip")}
|
||||
>
|
||||
<TeamDropdown
|
||||
onChange={(value) => {
|
||||
|
|
@ -139,8 +143,8 @@ const AddModelForm: React.FC<AddModelFormProps> = ({
|
|||
</Form.Item>
|
||||
{!teamAdminSelectedTeam && (
|
||||
<Alert
|
||||
message="Team Selection Required"
|
||||
description="As a team admin, you need to select your team first before adding models."
|
||||
message={t("addModel.addModelForm.teamSelectionRequiredTitle")}
|
||||
description={t("addModel.addModelForm.teamSelectionRequiredDesc")}
|
||||
type="info"
|
||||
showIcon
|
||||
className="mb-4"
|
||||
|
|
@ -151,10 +155,10 @@ const AddModelForm: React.FC<AddModelFormProps> = ({
|
|||
{(isAdmin || (isTeamAdmin && teamAdminSelectedTeam)) && (
|
||||
<>
|
||||
<Form.Item
|
||||
rules={[{ required: true, message: "Required" }]}
|
||||
label="Provider:"
|
||||
rules={[{ required: true, message: t("addModel.addModelForm.providerRequired") }]}
|
||||
label={t("addModel.addModelForm.providerLabel")}
|
||||
name="custom_llm_provider"
|
||||
tooltip="E.g. OpenAI, Azure OpenAI, Anthropic, Bedrock, etc."
|
||||
tooltip={t("addModel.addModelForm.providerTooltip")}
|
||||
labelCol={{ span: 10 }}
|
||||
labelAlign="left"
|
||||
>
|
||||
|
|
@ -162,7 +166,11 @@ const AddModelForm: React.FC<AddModelFormProps> = ({
|
|||
virtual={false}
|
||||
showSearch
|
||||
loading={isProviderMetadataLoading}
|
||||
placeholder={isProviderMetadataLoading ? "Loading providers..." : "Select a provider"}
|
||||
placeholder={
|
||||
isProviderMetadataLoading
|
||||
? t("addModel.addModelForm.providerLoadingPlaceholder")
|
||||
: t("addModel.addModelForm.providerSelectPlaceholder")
|
||||
}
|
||||
optionFilterProp="data-label"
|
||||
onChange={(value) => {
|
||||
setSelectedProvider(value as Providers);
|
||||
|
|
@ -207,22 +215,31 @@ const AddModelForm: React.FC<AddModelFormProps> = ({
|
|||
<ConditionalPublicModelName />
|
||||
|
||||
{/* Select Mode */}
|
||||
<Form.Item label="Mode" name="mode" className="mb-1">
|
||||
<Form.Item label={t("addModel.addModelForm.modeLabel")} name="mode" className="mb-1">
|
||||
<AntdSelect
|
||||
style={{ width: "100%" }}
|
||||
value={testMode}
|
||||
onChange={(value) => setTestMode(value)}
|
||||
options={TEST_MODES}
|
||||
options={testModes}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Row>
|
||||
<Col span={10}></Col>
|
||||
<Col span={10}>
|
||||
<Text className="mb-5 mt-1">
|
||||
<strong>Optional</strong> - LiteLLM endpoint to use when health checking this model{" "}
|
||||
<Link href="https://docs.litellm.ai/docs/proxy/health#health" target="_blank">
|
||||
Learn more
|
||||
</Link>
|
||||
<Trans
|
||||
i18nKey="addModel.addModelForm.modeHelp"
|
||||
components={{
|
||||
strong: <strong key="strong" />,
|
||||
learnMoreLink: (
|
||||
<Link
|
||||
key="learnMoreLink"
|
||||
href="https://docs.litellm.ai/docs/proxy/health#health"
|
||||
target="_blank"
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Text>
|
||||
</Col>
|
||||
</Row>
|
||||
|
|
@ -230,18 +247,22 @@ const AddModelForm: React.FC<AddModelFormProps> = ({
|
|||
{/* Credentials */}
|
||||
<div className="mb-4">
|
||||
<Typography.Text className="text-sm text-gray-500 mb-2">
|
||||
Either select existing credentials OR enter new provider credentials below
|
||||
{t("addModel.addModelForm.credentialsSectionDesc")}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
|
||||
<Form.Item label="Existing Credentials" name="litellm_credential_name" initialValue={null}>
|
||||
<Form.Item
|
||||
label={t("addModel.addModelForm.existingCredentialsLabel")}
|
||||
name="litellm_credential_name"
|
||||
initialValue={null}
|
||||
>
|
||||
<AntdSelect
|
||||
showSearch
|
||||
placeholder="Select or search for existing credentials"
|
||||
placeholder={t("addModel.addModelForm.existingCredentialsPlaceholder")}
|
||||
optionFilterProp="children"
|
||||
filterOption={(input, option) => (option?.label ?? "").toLowerCase().includes(input.toLowerCase())}
|
||||
options={[
|
||||
{ value: null, label: "None" },
|
||||
{ value: null, label: t("addModel.addModelForm.credentialsNone") },
|
||||
...credentials.map((credential) => ({
|
||||
value: credential.credential_name,
|
||||
label: credential.credential_name,
|
||||
|
|
@ -279,22 +300,20 @@ const AddModelForm: React.FC<AddModelFormProps> = ({
|
|||
</Form.Item>
|
||||
<div className="flex items-center my-4">
|
||||
<div className="flex-grow border-t border-gray-200"></div>
|
||||
<span className="px-4 text-gray-500 text-sm">Additional Model Info Settings</span>
|
||||
<span className="px-4 text-gray-500 text-sm">
|
||||
{t("addModel.addModelForm.additionalModelInfoDivider")}
|
||||
</span>
|
||||
<div className="flex-grow border-t border-gray-200"></div>
|
||||
</div>
|
||||
{/* Team-only Model Switch - Only show for proxy admins, not team admins */}
|
||||
{(isAdmin || !isTeamAdmin) && (
|
||||
<Form.Item
|
||||
label="Team-BYOK Model"
|
||||
tooltip="Only use this model + credential combination for this team. Useful when teams want to onboard their own OpenAI keys."
|
||||
label={t("addModel.addModelForm.teamByokLabel")}
|
||||
tooltip={t("addModel.addModelForm.teamByokTooltip")}
|
||||
className="mb-4"
|
||||
>
|
||||
<Tooltip
|
||||
title={
|
||||
!premiumUser
|
||||
? "This is an enterprise-only feature. Upgrade to premium to restrict model+credential combinations to a specific team."
|
||||
: ""
|
||||
}
|
||||
title={!premiumUser ? t("addModel.addModelForm.teamByokPremiumTooltip") : ""}
|
||||
placement="top"
|
||||
>
|
||||
<Switch
|
||||
|
|
@ -314,14 +333,14 @@ const AddModelForm: React.FC<AddModelFormProps> = ({
|
|||
{/* Conditional Team Selection */}
|
||||
{isTeamOnly && (isAdmin || !isTeamAdmin) && (
|
||||
<Form.Item
|
||||
label="Select Team"
|
||||
label={t("addModel.addModelForm.selectTeamByokLabel")}
|
||||
name="team_id"
|
||||
className="mb-4"
|
||||
tooltip="Only keys for this team will be able to call this model."
|
||||
tooltip={t("addModel.addModelForm.selectTeamByokTooltip")}
|
||||
rules={[
|
||||
{
|
||||
required: isTeamOnly && !isAdmin,
|
||||
message: "Please select a team.",
|
||||
message: t("addModel.addModelForm.selectTeamByokRequired"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
|
|
@ -331,15 +350,15 @@ const AddModelForm: React.FC<AddModelFormProps> = ({
|
|||
{isAdmin && (
|
||||
<>
|
||||
<Form.Item
|
||||
label="Model Access Group"
|
||||
label={t("addModel.addModelForm.modelAccessGroupLabel")}
|
||||
name="model_access_group"
|
||||
className="mb-4"
|
||||
tooltip="Use model access groups to give users access to select models, and add new ones to the group over time."
|
||||
tooltip={t("addModel.addModelForm.modelAccessGroupTooltip")}
|
||||
>
|
||||
<AntdSelect
|
||||
mode="tags"
|
||||
showSearch
|
||||
placeholder="Select existing groups or type to create new ones"
|
||||
placeholder={t("addModel.addModelForm.modelAccessGroupPlaceholder")}
|
||||
optionFilterProp="children"
|
||||
tokenSeparators={[","]}
|
||||
options={modelAccessGroups.map((group) => ({
|
||||
|
|
@ -363,15 +382,17 @@ const AddModelForm: React.FC<AddModelFormProps> = ({
|
|||
</>
|
||||
)}
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<Tooltip title="Get help on our github">
|
||||
<Typography.Link href="https://github.com/BerriAI/litellm/issues">Need Help?</Typography.Link>
|
||||
<Tooltip title={t("addModel.addModelForm.needHelpTooltip")}>
|
||||
<Typography.Link href="https://github.com/BerriAI/litellm/issues">
|
||||
{t("addModel.addModelForm.needHelp")}
|
||||
</Typography.Link>
|
||||
</Tooltip>
|
||||
<div className="space-x-2">
|
||||
<Button data-testid="test-connect-btn" onClick={handleTestConnection} loading={isTestingConnection}>
|
||||
Test Connect
|
||||
{t("addModel.addModelForm.testConnectButton")}
|
||||
</Button>
|
||||
<Button data-testid="add-model-btn" htmlType="submit">
|
||||
Add Model
|
||||
{t("addModel.addModelForm.addModelButton")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -381,7 +402,7 @@ const AddModelForm: React.FC<AddModelFormProps> = ({
|
|||
|
||||
{/* Test Connection Results Modal */}
|
||||
<Modal
|
||||
title="Connection Test Results"
|
||||
title={t("addModel.addModelForm.connectionTestResultsTitle")}
|
||||
open={isResultModalVisible}
|
||||
onCancel={() => {
|
||||
setIsResultModalVisible(false);
|
||||
|
|
@ -395,7 +416,7 @@ const AddModelForm: React.FC<AddModelFormProps> = ({
|
|||
setIsTestingConnection(false);
|
||||
}}
|
||||
>
|
||||
Close
|
||||
{t("common.close")}
|
||||
</Button>,
|
||||
]}
|
||||
width={700}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { InfoCircleOutlined } from "@ant-design/icons";
|
||||
import { Select as AntdSelect, Card, Divider, Space, Tooltip, Typography } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ModelGroup } from "../playground/llm_calls/fetch_models";
|
||||
|
||||
const { Text } = Typography;
|
||||
|
|
@ -18,30 +19,41 @@ interface ComplexityRouterConfigProps {
|
|||
onChange: (tiers: ComplexityTiers) => void;
|
||||
}
|
||||
|
||||
const TIER_DESCRIPTIONS: Record<keyof ComplexityTiers, { label: string; description: string; examples: string }> = {
|
||||
SIMPLE: {
|
||||
label: "Simple",
|
||||
description: "Basic questions, greetings, simple factual queries",
|
||||
examples: '"Hello!", "What is Python?", "Thanks!"',
|
||||
},
|
||||
MEDIUM: {
|
||||
label: "Medium",
|
||||
description: "Standard queries requiring some reasoning or explanation",
|
||||
examples: '"Explain how REST APIs work", "Debug this error"',
|
||||
},
|
||||
COMPLEX: {
|
||||
label: "Complex",
|
||||
description: "Technical, multi-part requests requiring deep knowledge",
|
||||
examples: '"Design a microservices architecture", "Implement a rate limiter"',
|
||||
},
|
||||
REASONING: {
|
||||
label: "Reasoning",
|
||||
description: "Chain-of-thought, analysis, explicit reasoning requests",
|
||||
examples: '"Think step by step...", "Analyze the pros and cons..."',
|
||||
},
|
||||
};
|
||||
|
||||
const ComplexityRouterConfig: React.FC<ComplexityRouterConfigProps> = ({ modelInfo, value, onChange }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const TIER_CONFIGS: Array<{
|
||||
key: keyof ComplexityTiers;
|
||||
labelKey: string;
|
||||
descriptionKey: string;
|
||||
examplesKey: string;
|
||||
}> = [
|
||||
{
|
||||
key: "SIMPLE",
|
||||
labelKey: "addModel.complexityRouterConfig.simpleTierLabel",
|
||||
descriptionKey: "addModel.complexityRouterConfig.simpleTierDescription",
|
||||
examplesKey: "addModel.complexityRouterConfig.simpleTierExamples",
|
||||
},
|
||||
{
|
||||
key: "MEDIUM",
|
||||
labelKey: "addModel.complexityRouterConfig.mediumTierLabel",
|
||||
descriptionKey: "addModel.complexityRouterConfig.mediumTierDescription",
|
||||
examplesKey: "addModel.complexityRouterConfig.mediumTierExamples",
|
||||
},
|
||||
{
|
||||
key: "COMPLEX",
|
||||
labelKey: "addModel.complexityRouterConfig.complexTierLabel",
|
||||
descriptionKey: "addModel.complexityRouterConfig.complexTierDescription",
|
||||
examplesKey: "addModel.complexityRouterConfig.complexTierExamples",
|
||||
},
|
||||
{
|
||||
key: "REASONING",
|
||||
labelKey: "addModel.complexityRouterConfig.reasoningTierLabel",
|
||||
descriptionKey: "addModel.complexityRouterConfig.reasoningTierDescription",
|
||||
examplesKey: "addModel.complexityRouterConfig.reasoningTierExamples",
|
||||
},
|
||||
];
|
||||
|
||||
// Prepare model options for dropdowns
|
||||
const modelOptions = modelInfo.map((model) => ({
|
||||
value: model.model_group,
|
||||
|
|
@ -59,40 +71,43 @@ const ComplexityRouterConfig: React.FC<ComplexityRouterConfigProps> = ({ modelIn
|
|||
<div className="w-full max-w-none">
|
||||
<Space align="center" style={{ marginBottom: 16 }}>
|
||||
<Typography.Title level={4} style={{ margin: 0 }}>
|
||||
Complexity Tier Configuration
|
||||
{t("addModel.complexityRouterConfig.title")}
|
||||
</Typography.Title>
|
||||
<Tooltip title="Map each complexity tier to a model. Simple queries use cheaper/faster models, complex queries use more capable models.">
|
||||
<Tooltip title={t("addModel.complexityRouterConfig.titleTooltip")}>
|
||||
<InfoCircleOutlined className="text-gray-400" />
|
||||
</Tooltip>
|
||||
</Space>
|
||||
|
||||
<Text type="secondary" style={{ display: "block", marginBottom: 24 }}>
|
||||
The complexity router automatically classifies requests by complexity using rule-based scoring (no API calls,
|
||||
<1ms latency). Configure which model handles each tier.
|
||||
{t("addModel.complexityRouterConfig.subtitle")}
|
||||
</Text>
|
||||
|
||||
<Card>
|
||||
{(Object.keys(TIER_DESCRIPTIONS) as Array<keyof ComplexityTiers>).map((tier, index) => {
|
||||
const tierInfo = TIER_DESCRIPTIONS[tier];
|
||||
{TIER_CONFIGS.map((tierConfig, index) => {
|
||||
const label = t(tierConfig.labelKey);
|
||||
const description = t(tierConfig.descriptionKey);
|
||||
const examples = t(tierConfig.examplesKey);
|
||||
return (
|
||||
<div key={tier}>
|
||||
<div key={tierConfig.key}>
|
||||
{index > 0 && <Divider style={{ margin: "16px 0" }} />}
|
||||
<div className="mb-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Text strong style={{ fontSize: 16 }}>
|
||||
{tierInfo.label} Tier
|
||||
{t("addModel.complexityRouterConfig.tierLabel", { label })}
|
||||
</Text>
|
||||
<Tooltip title={tierInfo.description}>
|
||||
<Tooltip title={description}>
|
||||
<InfoCircleOutlined className="text-gray-400" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Text type="secondary" style={{ display: "block", marginBottom: 8, fontSize: 12 }}>
|
||||
Examples: {tierInfo.examples}
|
||||
{t("addModel.complexityRouterConfig.examplesLabel", { examples })}
|
||||
</Text>
|
||||
<AntdSelect
|
||||
value={value[tier]}
|
||||
onChange={(model) => handleTierChange(tier, model)}
|
||||
placeholder={`Select model for ${tierInfo.label.toLowerCase()} queries`}
|
||||
value={value[tierConfig.key]}
|
||||
onChange={(model) => handleTierChange(tierConfig.key, model)}
|
||||
placeholder={t("addModel.complexityRouterConfig.selectModelPlaceholder", {
|
||||
label: label.toLowerCase(),
|
||||
})}
|
||||
showSearch
|
||||
style={{ width: "100%" }}
|
||||
options={modelOptions}
|
||||
|
|
@ -107,12 +122,10 @@ const ComplexityRouterConfig: React.FC<ComplexityRouterConfigProps> = ({ modelIn
|
|||
|
||||
<Card className="bg-gray-50">
|
||||
<Text strong style={{ display: "block", marginBottom: 8 }}>
|
||||
How Classification Works
|
||||
{t("addModel.complexityRouterConfig.howClassificationWorksTitle")}
|
||||
</Text>
|
||||
<Text type="secondary" style={{ fontSize: 13 }}>
|
||||
The router scores each request across 7 dimensions: token count, code presence, reasoning markers, technical
|
||||
terms, simple indicators, multi-step patterns, and question complexity. The weighted score determines the
|
||||
tier:
|
||||
{t("addModel.complexityRouterConfig.howClassificationWorksDesc")}
|
||||
</Text>
|
||||
<ul style={{ marginTop: 8, marginBottom: 0, paddingLeft: 20, fontSize: 13, color: "rgba(0, 0, 0, 0.45)" }}>
|
||||
<li>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {
|
|||
Typography,
|
||||
} from "antd";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ModelGroup } from "../playground/llm_calls/fetch_models";
|
||||
|
||||
const { Text } = Typography;
|
||||
|
|
@ -48,6 +49,7 @@ interface RouterConfigBuilderProps {
|
|||
}
|
||||
|
||||
const RouterConfigBuilder: React.FC<RouterConfigBuilderProps> = ({ modelInfo, value, onChange }) => {
|
||||
const { t } = useTranslation();
|
||||
const [routes, setRoutes] = useState<Route[]>([]);
|
||||
const [showJsonPreview, setShowJsonPreview] = useState<boolean>(false);
|
||||
const [expandedRoutes, setExpandedRoutes] = useState<string[]>([]);
|
||||
|
|
@ -106,8 +108,8 @@ const RouterConfigBuilder: React.FC<RouterConfigBuilderProps> = ({ modelInfo, va
|
|||
};
|
||||
|
||||
// Handle updating a route
|
||||
const updateRoute = (routeId: string, field: keyof Route, value: any) => {
|
||||
const updatedRoutes = routes.map((route) => (route.id === routeId ? { ...route, [field]: value } : route));
|
||||
const updateRoute = (routeId: string, field: keyof Route, val: any) => {
|
||||
const updatedRoutes = routes.map((route) => (route.id === routeId ? { ...route, [field]: val } : route));
|
||||
setRoutes(updatedRoutes);
|
||||
updateConfig(updatedRoutes);
|
||||
};
|
||||
|
|
@ -156,21 +158,21 @@ const RouterConfigBuilder: React.FC<RouterConfigBuilderProps> = ({ modelInfo, va
|
|||
<Flex justify="space-between" align="center" gap="middle" style={{ width: "100%", marginBottom: 24 }}>
|
||||
<Space align="center">
|
||||
<Typography.Title level={4} style={{ margin: 0 }}>
|
||||
Routes Configuration
|
||||
{t("addModel.routerConfigBuilder.title")}
|
||||
</Typography.Title>
|
||||
<Tooltip title="Configure routing logic to automatically select the best model based on user input patterns">
|
||||
<Tooltip title={t("addModel.routerConfigBuilder.titleTooltip")}>
|
||||
<InfoCircleOutlined className="text-gray-400" />
|
||||
</Tooltip>
|
||||
</Space>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={addRoute} className="bg-blue-600 hover:bg-blue-700">
|
||||
Add Route
|
||||
{t("addModel.routerConfigBuilder.addRouteButton")}
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
{/* Routes */}
|
||||
{routes.length === 0 ? (
|
||||
<Card>
|
||||
<Empty description='No routes configured. Click "Add Route" to get started.' />
|
||||
<Empty description={t("addModel.routerConfigBuilder.noRoutesDesc")} />
|
||||
</Card>
|
||||
) : (
|
||||
<Collapse
|
||||
|
|
@ -181,7 +183,10 @@ const RouterConfigBuilder: React.FC<RouterConfigBuilderProps> = ({ modelInfo, va
|
|||
key: route.id,
|
||||
label: (
|
||||
<Text style={{ fontSize: 16 }}>
|
||||
Route {index + 1}: {route.model || "Unnamed"}
|
||||
{t("addModel.routerConfigBuilder.routeLabel", {
|
||||
index: index + 1,
|
||||
model: route.model || t("addModel.routerConfigBuilder.routeUnnamed"),
|
||||
})}
|
||||
</Text>
|
||||
),
|
||||
extra: (
|
||||
|
|
@ -200,11 +205,11 @@ const RouterConfigBuilder: React.FC<RouterConfigBuilderProps> = ({ modelInfo, va
|
|||
<Card key={route.id}>
|
||||
{/* Model Selection */}
|
||||
<div className="mb-4 w-full">
|
||||
<Text className="text-sm font-medium mb-2 block">Model</Text>
|
||||
<Text className="text-sm font-medium mb-2 block">{t("addModel.routerConfigBuilder.modelLabel")}</Text>
|
||||
<AntdSelect
|
||||
value={route.model}
|
||||
onChange={(value) => updateRoute(route.id, "model", value)}
|
||||
placeholder="Select model"
|
||||
onChange={(val) => updateRoute(route.id, "model", val)}
|
||||
placeholder={t("addModel.routerConfigBuilder.modelPlaceholder")}
|
||||
showSearch
|
||||
style={{ width: "100%" }}
|
||||
options={modelOptions}
|
||||
|
|
@ -213,11 +218,13 @@ const RouterConfigBuilder: React.FC<RouterConfigBuilderProps> = ({ modelInfo, va
|
|||
|
||||
{/* Description */}
|
||||
<div className="mb-4 w-full">
|
||||
<Text className="text-sm font-medium mb-2 block">Description</Text>
|
||||
<Text className="text-sm font-medium mb-2 block">
|
||||
{t("addModel.routerConfigBuilder.descriptionLabel")}
|
||||
</Text>
|
||||
<TextArea
|
||||
value={route.description}
|
||||
onChange={(e) => updateRoute(route.id, "description", e.target.value)}
|
||||
placeholder="Describe when this route should be used..."
|
||||
placeholder={t("addModel.routerConfigBuilder.descriptionPlaceholder")}
|
||||
rows={2}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
|
|
@ -226,14 +233,14 @@ const RouterConfigBuilder: React.FC<RouterConfigBuilderProps> = ({ modelInfo, va
|
|||
{/* Score Threshold */}
|
||||
<div className="mb-4 w-full">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Text className="text-sm font-medium">Score Threshold</Text>
|
||||
<Tooltip title="Minimum similarity score to route to this model (0-1)">
|
||||
<Text className="text-sm font-medium">{t("addModel.routerConfigBuilder.scoreThresholdLabel")}</Text>
|
||||
<Tooltip title={t("addModel.routerConfigBuilder.scoreThresholdTooltip")}>
|
||||
<InfoCircleOutlined className="text-gray-400" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<InputNumber
|
||||
value={route.score_threshold}
|
||||
onChange={(value) => updateRoute(route.id, "score_threshold", value || 0)}
|
||||
onChange={(val) => updateRoute(route.id, "score_threshold", val || 0)}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.1}
|
||||
|
|
@ -245,19 +252,19 @@ const RouterConfigBuilder: React.FC<RouterConfigBuilderProps> = ({ modelInfo, va
|
|||
{/* Example Utterances */}
|
||||
<div className="w-full">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Text className="text-sm font-medium">Example Utterances</Text>
|
||||
<Tooltip title="Training examples for this route. Type an utterance and press Enter to add it.">
|
||||
<Text className="text-sm font-medium">
|
||||
{t("addModel.routerConfigBuilder.exampleUtterancesLabel")}
|
||||
</Text>
|
||||
<Tooltip title={t("addModel.routerConfigBuilder.exampleUtterancesTooltip")}>
|
||||
<InfoCircleOutlined className="text-gray-400" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Text className="text-xs text-gray-500 mb-2">
|
||||
Type an utterance and press Enter to add it. You can also paste multiple lines.
|
||||
</Text>
|
||||
<Text className="text-xs text-gray-500 mb-2">{t("addModel.routerConfigBuilder.utterancesHint")}</Text>
|
||||
<AntdSelect
|
||||
mode="tags"
|
||||
value={route.utterances}
|
||||
onChange={(utterances) => updateRoute(route.id, "utterances", utterances)}
|
||||
placeholder="Type an utterance and press Enter..."
|
||||
placeholder={t("addModel.routerConfigBuilder.utterancesPlaceholder")}
|
||||
style={{ width: "100%" }}
|
||||
tokenSeparators={["\n"]}
|
||||
maxTagCount="responsive"
|
||||
|
|
@ -273,9 +280,11 @@ const RouterConfigBuilder: React.FC<RouterConfigBuilderProps> = ({ modelInfo, va
|
|||
{/* JSON Preview */}
|
||||
<Divider />
|
||||
<div className="flex justify-between items-center mb-4 w-full">
|
||||
<Text className="text-lg font-semibold">JSON Preview</Text>
|
||||
<Text className="text-lg font-semibold">{t("addModel.routerConfigBuilder.jsonPreviewLabel")}</Text>
|
||||
<Button type="link" onClick={() => setShowJsonPreview(!showJsonPreview)} className="text-blue-600 p-0">
|
||||
{showJsonPreview ? "Hide" : "Show"}
|
||||
{showJsonPreview
|
||||
? t("addModel.routerConfigBuilder.hideButton")
|
||||
: t("addModel.routerConfigBuilder.showButton")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import React, { useEffect, useState } from "react";
|
|||
import { Card, Form, Button, Tooltip, Typography, Select as AntdSelect, Modal, Radio, Badge, Space } from "antd";
|
||||
import type { FormInstance } from "antd";
|
||||
import { Text, TextInput } from "@tremor/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { modelAvailableCall } from "../networking";
|
||||
import ConnectionErrorDisplay from "./model_connection_test";
|
||||
import { all_admin_roles } from "@/utils/roles";
|
||||
|
|
@ -31,6 +32,7 @@ interface ComplexityTiers {
|
|||
const { Title, Link } = Typography;
|
||||
|
||||
const AddAutoRouterTab: React.FC<AddAutoRouterTabProps> = ({ form, handleOk, accessToken, userRole }) => {
|
||||
const { t } = useTranslation();
|
||||
// State for connection testing
|
||||
const [isResultModalVisible, setIsResultModalVisible] = useState<boolean>(false);
|
||||
const [isTestingConnection, setIsTestingConnection] = useState<boolean>(false);
|
||||
|
|
@ -95,7 +97,7 @@ const AddAutoRouterTab: React.FC<AddAutoRouterTabProps> = ({ form, handleOk, acc
|
|||
|
||||
// Check basic required fields first
|
||||
if (!currentFormValues.auto_router_name) {
|
||||
NotificationManager.fromBackend("Please enter an Auto Router Name");
|
||||
NotificationManager.fromBackend(t("addModel.addAutoRouterTab.enterRouterNameError"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -104,7 +106,7 @@ const AddAutoRouterTab: React.FC<AddAutoRouterTabProps> = ({ form, handleOk, acc
|
|||
// Complexity Router validation
|
||||
const filledTiers = Object.values(complexityTiers).filter(Boolean);
|
||||
if (filledTiers.length === 0) {
|
||||
NotificationManager.fromBackend("Please select at least one model for a complexity tier");
|
||||
NotificationManager.fromBackend(t("addModel.addAutoRouterTab.selectTierModelError"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -143,12 +145,12 @@ const AddAutoRouterTab: React.FC<AddAutoRouterTabProps> = ({ form, handleOk, acc
|
|||
})
|
||||
.catch((error) => {
|
||||
console.error("Validation failed:", error);
|
||||
NotificationManager.fromBackend("Please fill in all required fields");
|
||||
NotificationManager.fromBackend(t("addModel.addAutoRouterTab.fillRequiredFieldsError"));
|
||||
});
|
||||
} else {
|
||||
// Semantic Router validation (existing logic)
|
||||
if (!currentFormValues.auto_router_default_model) {
|
||||
NotificationManager.fromBackend("Please select a Default Model");
|
||||
NotificationManager.fromBackend(t("addModel.addAutoRouterTab.selectDefaultModelError"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -160,7 +162,7 @@ const AddAutoRouterTab: React.FC<AddAutoRouterTabProps> = ({ form, handleOk, acc
|
|||
|
||||
// Custom validation for router config
|
||||
if (!routerConfig || !routerConfig.routes || routerConfig.routes.length === 0) {
|
||||
NotificationManager.fromBackend("Please configure at least one route for the auto router");
|
||||
NotificationManager.fromBackend(t("addModel.addAutoRouterTab.configureRouteError"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -170,9 +172,7 @@ const AddAutoRouterTab: React.FC<AddAutoRouterTabProps> = ({ form, handleOk, acc
|
|||
);
|
||||
|
||||
if (invalidRoutes.length > 0) {
|
||||
NotificationManager.fromBackend(
|
||||
"Please ensure all routes have a target model, description, and at least one utterance",
|
||||
);
|
||||
NotificationManager.fromBackend(t("addModel.addAutoRouterTab.invalidRoutesError"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -195,17 +195,17 @@ const AddAutoRouterTab: React.FC<AddAutoRouterTabProps> = ({ form, handleOk, acc
|
|||
const missingFields = fieldErrors.map((field: any) => {
|
||||
const fieldName = field.name[0];
|
||||
const friendlyNames: { [key: string]: string } = {
|
||||
auto_router_name: "Auto Router Name",
|
||||
auto_router_default_model: "Default Model",
|
||||
auto_router_embedding_model: "Embedding Model",
|
||||
auto_router_name: t("addModel.addAutoRouterTab.autoRouterNameLabel"),
|
||||
auto_router_default_model: t("addModel.addAutoRouterTab.defaultModelLabel"),
|
||||
auto_router_embedding_model: t("addModel.addAutoRouterTab.embeddingModelLabel"),
|
||||
};
|
||||
return friendlyNames[fieldName] || fieldName;
|
||||
});
|
||||
NotificationManager.fromBackend(
|
||||
`Please fill in the following required fields: ${missingFields.join(", ")}`,
|
||||
t("addModel.addAutoRouterTab.missingFieldsError", { fields: missingFields.join(", ") }),
|
||||
);
|
||||
} else {
|
||||
NotificationManager.fromBackend("Please fill in all required fields");
|
||||
NotificationManager.fromBackend(t("addModel.addAutoRouterTab.fillRequiredFieldsError"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -213,23 +213,20 @@ const AddAutoRouterTab: React.FC<AddAutoRouterTabProps> = ({ form, handleOk, acc
|
|||
|
||||
return (
|
||||
<>
|
||||
<Title level={2}>Add Auto Router</Title>
|
||||
<Text className="text-gray-600 mb-6">
|
||||
Create an auto router that automatically selects the best model based on request complexity or semantic
|
||||
matching.
|
||||
</Text>
|
||||
<Title level={2}>{t("addModel.addAutoRouterTab.title")}</Title>
|
||||
<Text className="text-gray-600 mb-6">{t("addModel.addAutoRouterTab.subtitle")}</Text>
|
||||
|
||||
<Card className="mb-4">
|
||||
<div className="mb-4">
|
||||
<Text className="text-sm font-medium mb-2 block">Router Type</Text>
|
||||
<Text className="text-sm font-medium mb-2 block">{t("addModel.addAutoRouterTab.routerTypeLabel")}</Text>
|
||||
<Radio.Group value={routerType} onChange={(e) => setRouterType(e.target.value)} className="w-full">
|
||||
<Space direction="vertical" className="w-full">
|
||||
<Radio value="complexity" className="w-full">
|
||||
<div className="flex items-center gap-2">
|
||||
<ThunderboltOutlined className="text-yellow-500" />
|
||||
<span className="font-medium">Complexity Router</span>
|
||||
<span className="font-medium">{t("addModel.addAutoRouterTab.complexityRouterLabel")}</span>
|
||||
<Badge
|
||||
count="Recommended"
|
||||
count={t("addModel.addAutoRouterTab.complexityRouterBadge")}
|
||||
style={{
|
||||
backgroundColor: "#52c41a",
|
||||
fontSize: "10px",
|
||||
|
|
@ -238,21 +235,22 @@ const AddAutoRouterTab: React.FC<AddAutoRouterTabProps> = ({ form, handleOk, acc
|
|||
/>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 ml-6 mt-1">
|
||||
Automatically routes based on request complexity. No training data needed — just pick 4 models and go.
|
||||
{t("addModel.addAutoRouterTab.complexityRouterDesc")}
|
||||
<br />
|
||||
<span className="text-green-600">✓ Zero API calls</span> ·{" "}
|
||||
<span className="text-green-600">✓ <1ms latency</span> ·{" "}
|
||||
<span className="text-green-600">✓ No cost</span>
|
||||
<span className="text-green-600">
|
||||
✓ {t("addModel.addAutoRouterTab.complexityRouterZeroApi")}
|
||||
</span> ·{" "}
|
||||
<span className="text-green-600">✓ {t("addModel.addAutoRouterTab.complexityRouterLatency")}</span> ·{" "}
|
||||
<span className="text-green-600">✓ {t("addModel.addAutoRouterTab.complexityRouterNoCost")}</span>
|
||||
</div>
|
||||
</Radio>
|
||||
<Radio value="semantic" className="w-full mt-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<BranchesOutlined className="text-blue-500" />
|
||||
<span className="font-medium">Semantic Router</span>
|
||||
<span className="font-medium">{t("addModel.addAutoRouterTab.semanticRouterLabel")}</span>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 ml-6 mt-1">
|
||||
Routes based on semantic similarity to example utterances. Requires embedding model and training
|
||||
examples.
|
||||
{t("addModel.addAutoRouterTab.semanticRouterDesc")}
|
||||
</div>
|
||||
</Radio>
|
||||
</Space>
|
||||
|
|
@ -270,14 +268,14 @@ const AddAutoRouterTab: React.FC<AddAutoRouterTabProps> = ({ form, handleOk, acc
|
|||
>
|
||||
{/* Auto Router Name */}
|
||||
<Form.Item
|
||||
rules={[{ required: true, message: "Auto router name is required" }]}
|
||||
label="Auto Router Name"
|
||||
rules={[{ required: true, message: t("addModel.addAutoRouterTab.autoRouterNameRequired") }]}
|
||||
label={t("addModel.addAutoRouterTab.autoRouterNameLabel")}
|
||||
name="auto_router_name"
|
||||
tooltip="Unique name for this auto router configuration"
|
||||
tooltip={t("addModel.addAutoRouterTab.autoRouterNameTooltip")}
|
||||
labelCol={{ span: 10 }}
|
||||
labelAlign="left"
|
||||
>
|
||||
<TextInput placeholder="e.g., smart_router, auto_router_1" />
|
||||
<TextInput placeholder={t("addModel.addAutoRouterTab.autoRouterNamePlaceholder")} />
|
||||
</Form.Item>
|
||||
|
||||
{/* Conditional rendering based on router type */}
|
||||
|
|
@ -309,15 +307,17 @@ const AddAutoRouterTab: React.FC<AddAutoRouterTabProps> = ({ form, handleOk, acc
|
|||
|
||||
{/* Auto Router Default Model */}
|
||||
<Form.Item
|
||||
rules={[{ required: routerType === "semantic", message: "Default model is required" }]}
|
||||
label="Default Model"
|
||||
rules={[
|
||||
{ required: routerType === "semantic", message: t("addModel.addAutoRouterTab.defaultModelRequired") },
|
||||
]}
|
||||
label={t("addModel.addAutoRouterTab.defaultModelLabel")}
|
||||
name="auto_router_default_model"
|
||||
tooltip="Fallback model to use when auto routing logic cannot determine the best model"
|
||||
tooltip={t("addModel.addAutoRouterTab.defaultModelTooltip")}
|
||||
labelCol={{ span: 10 }}
|
||||
labelAlign="left"
|
||||
>
|
||||
<AntdSelect
|
||||
placeholder="Select a default model"
|
||||
placeholder={t("addModel.addAutoRouterTab.defaultModelPlaceholder")}
|
||||
onChange={(value) => {
|
||||
setShowCustomDefaultModel(value === "custom");
|
||||
}}
|
||||
|
|
@ -326,7 +326,7 @@ const AddAutoRouterTab: React.FC<AddAutoRouterTabProps> = ({ form, handleOk, acc
|
|||
value: model_group,
|
||||
label: model_group,
|
||||
})),
|
||||
{ value: "custom", label: "Enter custom model name" },
|
||||
{ value: "custom", label: t("addModel.addAutoRouterTab.enterCustomModelName") },
|
||||
]}
|
||||
style={{ width: "100%" }}
|
||||
showSearch={true}
|
||||
|
|
@ -335,15 +335,15 @@ const AddAutoRouterTab: React.FC<AddAutoRouterTabProps> = ({ form, handleOk, acc
|
|||
|
||||
{/* Auto Router Embedding Model */}
|
||||
<Form.Item
|
||||
label="Embedding Model"
|
||||
label={t("addModel.addAutoRouterTab.embeddingModelLabel")}
|
||||
name="auto_router_embedding_model"
|
||||
tooltip="Optional: Embedding model to use for semantic routing decisions"
|
||||
tooltip={t("addModel.addAutoRouterTab.embeddingModelTooltip")}
|
||||
labelCol={{ span: 10 }}
|
||||
labelAlign="left"
|
||||
>
|
||||
<AntdSelect
|
||||
value={form.getFieldValue("auto_router_embedding_model")}
|
||||
placeholder="Select an embedding model (optional)"
|
||||
placeholder={t("addModel.addAutoRouterTab.embeddingModelPlaceholder")}
|
||||
onChange={(value) => {
|
||||
setShowCustomEmbeddingModel(value === "custom");
|
||||
form.setFieldValue("auto_router_embedding_model", value);
|
||||
|
|
@ -353,7 +353,7 @@ const AddAutoRouterTab: React.FC<AddAutoRouterTabProps> = ({ form, handleOk, acc
|
|||
value: model_group,
|
||||
label: model_group,
|
||||
})),
|
||||
{ value: "custom", label: "Enter custom model name" },
|
||||
{ value: "custom", label: t("addModel.addAutoRouterTab.enterCustomModelName") },
|
||||
]}
|
||||
style={{ width: "100%" }}
|
||||
showSearch={true}
|
||||
|
|
@ -365,22 +365,24 @@ const AddAutoRouterTab: React.FC<AddAutoRouterTabProps> = ({ form, handleOk, acc
|
|||
|
||||
<div className="flex items-center my-4">
|
||||
<div className="flex-grow border-t border-gray-200"></div>
|
||||
<span className="px-4 text-gray-500 text-sm">Additional Settings</span>
|
||||
<span className="px-4 text-gray-500 text-sm">
|
||||
{t("addModel.addAutoRouterTab.additionalSettingsDivider")}
|
||||
</span>
|
||||
<div className="flex-grow border-t border-gray-200"></div>
|
||||
</div>
|
||||
|
||||
{/* Model Access Groups - Admin only */}
|
||||
{isAdmin && (
|
||||
<Form.Item
|
||||
label="Model Access Group"
|
||||
label={t("addModel.addAutoRouterTab.modelAccessGroupLabel")}
|
||||
name="model_access_group"
|
||||
className="mb-4"
|
||||
tooltip="Use model access groups to control who can access this auto router"
|
||||
tooltip={t("addModel.addAutoRouterTab.modelAccessGroupTooltip")}
|
||||
>
|
||||
<AntdSelect
|
||||
mode="tags"
|
||||
showSearch
|
||||
placeholder="Select existing groups or type to create new ones"
|
||||
placeholder={t("addModel.addAutoRouterTab.modelAccessGroupPlaceholder")}
|
||||
optionFilterProp="children"
|
||||
tokenSeparators={[","]}
|
||||
options={modelAccessGroups.map((group) => ({
|
||||
|
|
@ -394,12 +396,14 @@ const AddAutoRouterTab: React.FC<AddAutoRouterTabProps> = ({ form, handleOk, acc
|
|||
)}
|
||||
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<Tooltip title="Get help on our github">
|
||||
<Typography.Link href="https://github.com/BerriAI/litellm/issues">Need Help?</Typography.Link>
|
||||
<Tooltip title={t("addModel.addAutoRouterTab.needHelpTooltip")}>
|
||||
<Typography.Link href="https://github.com/BerriAI/litellm/issues">
|
||||
{t("addModel.addAutoRouterTab.needHelp")}
|
||||
</Typography.Link>
|
||||
</Tooltip>
|
||||
<div className="space-x-2">
|
||||
<Button onClick={handleTestConnection} loading={isTestingConnection}>
|
||||
Test Connection
|
||||
{t("addModel.addAutoRouterTab.testConnectionButton")}
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
|
|
@ -408,7 +412,7 @@ const AddAutoRouterTab: React.FC<AddAutoRouterTabProps> = ({ form, handleOk, acc
|
|||
handleAutoRouterSubmit();
|
||||
}}
|
||||
>
|
||||
Add Auto Router
|
||||
{t("addModel.addAutoRouterTab.addAutoRouterButton")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -417,7 +421,7 @@ const AddAutoRouterTab: React.FC<AddAutoRouterTabProps> = ({ form, handleOk, acc
|
|||
|
||||
{/* Test Connection Results Modal */}
|
||||
<Modal
|
||||
title="Connection Test Results"
|
||||
title={t("addModel.addAutoRouterTab.connectionTestResultsTitle")}
|
||||
open={isResultModalVisible}
|
||||
onCancel={() => {
|
||||
setIsResultModalVisible(false);
|
||||
|
|
@ -431,7 +435,7 @@ const AddAutoRouterTab: React.FC<AddAutoRouterTabProps> = ({ form, handleOk, acc
|
|||
setIsTestingConnection(false);
|
||||
}}
|
||||
>
|
||||
Close
|
||||
{t("common.close")}
|
||||
</Button>,
|
||||
]}
|
||||
width={700}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,24 @@
|
|||
// Define the available test modes
|
||||
export const TEST_MODES = [
|
||||
{ value: "chat", label: "Chat - /chat/completions" },
|
||||
{ value: "completion", label: "Completion - /completions" },
|
||||
{ value: "embedding", label: "Embedding - /embeddings" },
|
||||
{ value: "audio_speech", label: "Audio Speech - /audio/speech" },
|
||||
{ value: "audio_transcription", label: "Audio Transcription - /audio/transcriptions" },
|
||||
{ value: "image_generation", label: "Image Generation - /images/generations" },
|
||||
{ value: "video_generation", label: "Video Generation - /videos" },
|
||||
{ value: "rerank", label: "Rerank - /rerank" },
|
||||
{ value: "realtime", label: "Realtime - /realtime" },
|
||||
{ value: "batch", label: "Batch - /batch" },
|
||||
{ value: "ocr", label: "OCR - /ocr" },
|
||||
import type { TFunction } from "i18next";
|
||||
|
||||
export const getTestModes = (t: TFunction) => [
|
||||
{ value: "chat", label: t("addModel.addModelModes.chatLabel") },
|
||||
{ value: "completion", label: t("addModel.addModelModes.completionLabel") },
|
||||
{ value: "embedding", label: t("addModel.addModelModes.embeddingLabel") },
|
||||
{ value: "audio_speech", label: t("addModel.addModelModes.audioSpeechLabel") },
|
||||
{ value: "audio_transcription", label: t("addModel.addModelModes.audioTranscriptionLabel") },
|
||||
{ value: "image_generation", label: t("addModel.addModelModes.imageGenerationLabel") },
|
||||
{ value: "video_generation", label: t("addModel.addModelModes.videoGenerationLabel") },
|
||||
{ value: "rerank", label: t("addModel.addModelModes.rerankLabel") },
|
||||
{ value: "realtime", label: t("addModel.addModelModes.realtimeLabel") },
|
||||
{ value: "batch", label: t("addModel.addModelModes.batchLabel") },
|
||||
{ value: "ocr", label: t("addModel.addModelModes.ocrLabel") },
|
||||
];
|
||||
|
||||
// Define the available auto router routing strategies
|
||||
export const AUTO_ROUTER_MODES = [
|
||||
{ value: "simple-shuffle", label: "Simple Shuffle - Random selection from available models" },
|
||||
{ value: "least-busy", label: "Least Busy - Route to model with lowest current load" },
|
||||
{ value: "latency-based", label: "Latency Based - Route to model with best response time" },
|
||||
{ value: "cost-based", label: "Cost Based - Route to most cost-effective model" },
|
||||
{ value: "usage-based", label: "Usage Based - Route based on historical usage patterns" },
|
||||
{ value: "custom", label: "Custom - Use custom routing logic defined in config" },
|
||||
export const getAutoRouterModes = (t: TFunction) => [
|
||||
{ value: "simple-shuffle", label: t("addModel.addModelModes.simpleShuffleLabel") },
|
||||
{ value: "least-busy", label: t("addModel.addModelModes.leastBusyLabel") },
|
||||
{ value: "latency-based", label: t("addModel.addModelModes.latencyBasedLabel") },
|
||||
{ value: "cost-based", label: t("addModel.addModelModes.costBasedLabel") },
|
||||
{ value: "usage-based", label: t("addModel.addModelModes.usageBasedLabel") },
|
||||
{ value: "custom", label: t("addModel.addModelModes.customLabel") },
|
||||
];
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import type { FormInstance } from "antd";
|
|||
import { Form } from "antd";
|
||||
import type { UploadProps } from "antd/es/upload";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { Team } from "../key_team_helpers/key_list";
|
||||
import { type CredentialItem } from "../networking";
|
||||
import { Providers } from "../provider_info_helpers";
|
||||
|
|
@ -43,6 +44,7 @@ const AddModelTab: React.FC<AddModelTabProps> = ({
|
|||
accessToken,
|
||||
userRole,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
// Create separate form instance for auto router
|
||||
const [autoRouterForm] = Form.useForm();
|
||||
|
||||
|
|
@ -61,8 +63,8 @@ const AddModelTab: React.FC<AddModelTabProps> = ({
|
|||
<>
|
||||
<TabGroup className="w-full">
|
||||
<TabList className="mb-4">
|
||||
<Tab>Add Model</Tab>
|
||||
<Tab>Add Auto Router</Tab>
|
||||
<Tab>{t("addModel.addModelTab.addModelTab")}</Tab>
|
||||
<Tab>{t("addModel.addModelTab.addAutoRouterTab")}</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
|
|
|
|||
|
|
@ -5070,5 +5070,149 @@
|
|||
"send": "Send",
|
||||
"refineHint": "Give instructions to add, remove, or change competitors. Press Enter to send."
|
||||
}
|
||||
},
|
||||
"addModel": {
|
||||
"addModelForm": {
|
||||
"title": "Add Model",
|
||||
"selectTeamLabel": "Select Team",
|
||||
"selectTeamRequired": "Please select a team to continue",
|
||||
"selectTeamTooltip": "Select the team for which you want to add this model",
|
||||
"teamSelectionRequiredTitle": "Team Selection Required",
|
||||
"teamSelectionRequiredDesc": "As a team admin, you need to select your team first before adding models.",
|
||||
"providerLabel": "Provider:",
|
||||
"providerRequired": "Required",
|
||||
"providerTooltip": "E.g. OpenAI, Azure OpenAI, Anthropic, Bedrock, etc.",
|
||||
"providerLoadingPlaceholder": "Loading providers...",
|
||||
"providerLoadFailed": "Failed to load providers",
|
||||
"providerSelectPlaceholder": "Select a provider",
|
||||
"modeLabel": "Mode",
|
||||
"modeHelp": "<strong>Optional</strong> - LiteLLM endpoint to use when health checking this model <learnMoreLink>Learn more</learnMoreLink>",
|
||||
"credentialsSectionDesc": "Either select existing credentials OR enter new provider credentials below",
|
||||
"existingCredentialsLabel": "Existing Credentials",
|
||||
"existingCredentialsPlaceholder": "Select or search for existing credentials",
|
||||
"credentialsNone": "None",
|
||||
"additionalModelInfoDivider": "Additional Model Info Settings",
|
||||
"teamByokLabel": "Team-BYOK Model",
|
||||
"teamByokTooltip": "Only use this model + credential combination for this team. Useful when teams want to onboard their own OpenAI keys.",
|
||||
"teamByokPremiumTooltip": "This is an enterprise-only feature. Upgrade to premium to restrict model+credential combinations to a specific team.",
|
||||
"selectTeamByokLabel": "Select Team",
|
||||
"selectTeamByokTooltip": "Only keys for this team will be able to call this model.",
|
||||
"selectTeamByokRequired": "Please select a team.",
|
||||
"modelAccessGroupLabel": "Model Access Group",
|
||||
"modelAccessGroupTooltip": "Use model access groups to give users access to select models, and add new ones to the group over time.",
|
||||
"modelAccessGroupPlaceholder": "Select existing groups or type to create new ones",
|
||||
"needHelpTooltip": "Get help on our github",
|
||||
"needHelp": "Need Help?",
|
||||
"testConnectButton": "Test Connect",
|
||||
"addModelButton": "Add Model",
|
||||
"connectionTestResultsTitle": "Connection Test Results"
|
||||
},
|
||||
"addModelModes": {
|
||||
"chatLabel": "Chat - /chat/completions",
|
||||
"completionLabel": "Completion - /completions",
|
||||
"embeddingLabel": "Embedding - /embeddings",
|
||||
"audioSpeechLabel": "Audio Speech - /audio/speech",
|
||||
"audioTranscriptionLabel": "Audio Transcription - /audio/transcriptions",
|
||||
"imageGenerationLabel": "Image Generation - /images/generations",
|
||||
"videoGenerationLabel": "Video Generation - /videos",
|
||||
"rerankLabel": "Rerank - /rerank",
|
||||
"realtimeLabel": "Realtime - /realtime",
|
||||
"batchLabel": "Batch - /batch",
|
||||
"ocrLabel": "OCR - /ocr",
|
||||
"simpleShuffleLabel": "Simple Shuffle - Random selection from available models",
|
||||
"leastBusyLabel": "Least Busy - Route to model with lowest current load",
|
||||
"latencyBasedLabel": "Latency Based - Route to model with best response time",
|
||||
"costBasedLabel": "Cost Based - Route to most cost-effective model",
|
||||
"usageBasedLabel": "Usage Based - Route based on historical usage patterns",
|
||||
"customLabel": "Custom - Use custom routing logic defined in config"
|
||||
},
|
||||
"addModelTab": {
|
||||
"addModelTab": "Add Model",
|
||||
"addAutoRouterTab": "Add Auto Router"
|
||||
},
|
||||
"addAutoRouterTab": {
|
||||
"title": "Add Auto Router",
|
||||
"subtitle": "Create an auto router that automatically selects the best model based on request complexity or semantic matching.",
|
||||
"routerTypeLabel": "Router Type",
|
||||
"complexityRouterLabel": "Complexity Router",
|
||||
"complexityRouterBadge": "Recommended",
|
||||
"complexityRouterDesc": "Automatically routes based on request complexity. No training data needed — just pick 4 models and go.",
|
||||
"complexityRouterZeroApi": "Zero API calls",
|
||||
"complexityRouterLatency": "<1ms latency",
|
||||
"complexityRouterNoCost": "No cost",
|
||||
"semanticRouterLabel": "Semantic Router",
|
||||
"semanticRouterDesc": "Routes based on semantic similarity to example utterances. Requires embedding model and training examples.",
|
||||
"autoRouterNameLabel": "Auto Router Name",
|
||||
"autoRouterNameRequired": "Auto router name is required",
|
||||
"autoRouterNameTooltip": "Unique name for this auto router configuration",
|
||||
"autoRouterNamePlaceholder": "e.g., smart_router, auto_router_1",
|
||||
"defaultModelLabel": "Default Model",
|
||||
"defaultModelRequired": "Default model is required",
|
||||
"defaultModelTooltip": "Fallback model to use when auto routing logic cannot determine the best model",
|
||||
"defaultModelPlaceholder": "Select a default model",
|
||||
"enterCustomModelName": "Enter custom model name",
|
||||
"embeddingModelLabel": "Embedding Model",
|
||||
"embeddingModelTooltip": "Optional: Embedding model to use for semantic routing decisions",
|
||||
"embeddingModelPlaceholder": "Select an embedding model (optional)",
|
||||
"additionalSettingsDivider": "Additional Settings",
|
||||
"modelAccessGroupLabel": "Model Access Group",
|
||||
"modelAccessGroupTooltip": "Use model access groups to control who can access this auto router",
|
||||
"modelAccessGroupPlaceholder": "Select existing groups or type to create new ones",
|
||||
"needHelpTooltip": "Get help on our github",
|
||||
"needHelp": "Need Help?",
|
||||
"testConnectionButton": "Test Connection",
|
||||
"addAutoRouterButton": "Add Auto Router",
|
||||
"connectionTestResultsTitle": "Connection Test Results",
|
||||
"enterRouterNameError": "Please enter an Auto Router Name",
|
||||
"selectTierModelError": "Please select at least one model for a complexity tier",
|
||||
"fillRequiredFieldsError": "Please fill in all required fields",
|
||||
"selectDefaultModelError": "Please select a Default Model",
|
||||
"configureRouteError": "Please configure at least one route for the auto router",
|
||||
"invalidRoutesError": "Please ensure all routes have a target model, description, and at least one utterance",
|
||||
"missingFieldsError": "Please fill in the following required fields: {{fields}}"
|
||||
},
|
||||
"complexityRouterConfig": {
|
||||
"title": "Complexity Tier Configuration",
|
||||
"titleTooltip": "Map each complexity tier to a model. Simple queries use cheaper/faster models, complex queries use more capable models.",
|
||||
"subtitle": "The complexity router automatically classifies requests by complexity using rule-based scoring (no API calls, <1ms latency). Configure which model handles each tier.",
|
||||
"simpleTierLabel": "Simple",
|
||||
"simpleTierDescription": "Basic questions, greetings, simple factual queries",
|
||||
"simpleTierExamples": "\"Hello!\", \"What is Python?\", \"Thanks!\"",
|
||||
"mediumTierLabel": "Medium",
|
||||
"mediumTierDescription": "Standard queries requiring some reasoning or explanation",
|
||||
"mediumTierExamples": "\"Explain how REST APIs work\", \"Debug this error\"",
|
||||
"complexTierLabel": "Complex",
|
||||
"complexTierDescription": "Technical, multi-part requests requiring deep knowledge",
|
||||
"complexTierExamples": "\"Design a microservices architecture\", \"Implement a rate limiter\"",
|
||||
"reasoningTierLabel": "Reasoning",
|
||||
"reasoningTierDescription": "Chain-of-thought, analysis, explicit reasoning requests",
|
||||
"reasoningTierExamples": "\"Think step by step...\", \"Analyze the pros and cons...\"",
|
||||
"tierLabel": "{{label}} Tier",
|
||||
"examplesLabel": "Examples: {{examples}}",
|
||||
"selectModelPlaceholder": "Select model for {{label}} queries",
|
||||
"howClassificationWorksTitle": "How Classification Works",
|
||||
"howClassificationWorksDesc": "The router scores each request across 7 dimensions: token count, code presence, reasoning markers, technical terms, simple indicators, multi-step patterns, and question complexity. The weighted score determines the tier:"
|
||||
},
|
||||
"routerConfigBuilder": {
|
||||
"title": "Routes Configuration",
|
||||
"titleTooltip": "Configure routing logic to automatically select the best model based on user input patterns",
|
||||
"addRouteButton": "Add Route",
|
||||
"noRoutesDesc": "No routes configured. Click \"Add Route\" to get started.",
|
||||
"routeLabel": "Route {{index}}: {{model}}",
|
||||
"routeUnnamed": "Unnamed",
|
||||
"modelLabel": "Model",
|
||||
"modelPlaceholder": "Select model",
|
||||
"descriptionLabel": "Description",
|
||||
"descriptionPlaceholder": "Describe when this route should be used...",
|
||||
"scoreThresholdLabel": "Score Threshold",
|
||||
"scoreThresholdTooltip": "Minimum similarity score to route to this model (0-1)",
|
||||
"exampleUtterancesLabel": "Example Utterances",
|
||||
"exampleUtterancesTooltip": "Training examples for this route. Type an utterance and press Enter to add it.",
|
||||
"utterancesHint": "Type an utterance and press Enter to add it. You can also paste multiple lines.",
|
||||
"utterancesPlaceholder": "Type an utterance and press Enter...",
|
||||
"jsonPreviewLabel": "JSON Preview",
|
||||
"showButton": "Show",
|
||||
"hideButton": "Hide"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5070,5 +5070,149 @@
|
|||
"send": "发送",
|
||||
"refineHint": "给出添加、移除或修改竞品的指令,按 Enter 发送。"
|
||||
}
|
||||
},
|
||||
"addModel": {
|
||||
"addModelForm": {
|
||||
"title": "添加模型",
|
||||
"selectTeamLabel": "选择团队",
|
||||
"selectTeamRequired": "请选择团队以继续",
|
||||
"selectTeamTooltip": "选择您要为其添加此模型的团队",
|
||||
"teamSelectionRequiredTitle": "需要选择团队",
|
||||
"teamSelectionRequiredDesc": "作为团队管理员,您需要先选择团队,然后才能添加模型。",
|
||||
"providerLabel": "提供商:",
|
||||
"providerRequired": "必填",
|
||||
"providerTooltip": "例如:OpenAI、Azure OpenAI、Anthropic、Bedrock 等",
|
||||
"providerLoadingPlaceholder": "正在加载提供商...",
|
||||
"providerLoadFailed": "加载提供商失败",
|
||||
"providerSelectPlaceholder": "选择提供商",
|
||||
"modeLabel": "模式",
|
||||
"modeHelp": "<strong>可选</strong> - 对此模型进行健康检查时使用的 LiteLLM 端点 <learnMoreLink>了解更多</learnMoreLink>",
|
||||
"credentialsSectionDesc": "选择已有凭据或在下方输入新的提供商凭据",
|
||||
"existingCredentialsLabel": "已有凭据",
|
||||
"existingCredentialsPlaceholder": "选择或搜索已有凭据",
|
||||
"credentialsNone": "无",
|
||||
"additionalModelInfoDivider": "附加模型信息设置",
|
||||
"teamByokLabel": "团队自带密钥模型",
|
||||
"teamByokTooltip": "仅将此模型与凭据组合用于该团队。适用于团队希望使用自己的 OpenAI 密钥的情况。",
|
||||
"teamByokPremiumTooltip": "这是企业专属功能。升级到高级版以将模型与凭据组合限制为特定团队。",
|
||||
"selectTeamByokLabel": "选择团队",
|
||||
"selectTeamByokTooltip": "只有此团队的密钥才能调用此模型。",
|
||||
"selectTeamByokRequired": "请选择一个团队。",
|
||||
"modelAccessGroupLabel": "模型访问组",
|
||||
"modelAccessGroupTooltip": "使用模型访问组为用户授予对特定模型的访问权限,并可随时向组中添加新模型。",
|
||||
"modelAccessGroupPlaceholder": "选择已有组或输入新组名",
|
||||
"needHelpTooltip": "在 GitHub 上获取帮助",
|
||||
"needHelp": "需要帮助?",
|
||||
"testConnectButton": "测试连接",
|
||||
"addModelButton": "添加模型",
|
||||
"connectionTestResultsTitle": "连接测试结果"
|
||||
},
|
||||
"addModelModes": {
|
||||
"chatLabel": "Chat - /chat/completions",
|
||||
"completionLabel": "Completion - /completions",
|
||||
"embeddingLabel": "Embedding - /embeddings",
|
||||
"audioSpeechLabel": "Audio Speech - /audio/speech",
|
||||
"audioTranscriptionLabel": "Audio Transcription - /audio/transcriptions",
|
||||
"imageGenerationLabel": "Image Generation - /images/generations",
|
||||
"videoGenerationLabel": "Video Generation - /videos",
|
||||
"rerankLabel": "Rerank - /rerank",
|
||||
"realtimeLabel": "Realtime - /realtime",
|
||||
"batchLabel": "Batch - /batch",
|
||||
"ocrLabel": "OCR - /ocr",
|
||||
"simpleShuffleLabel": "Simple Shuffle - 从可用模型中随机选择",
|
||||
"leastBusyLabel": "Least Busy - 路由到当前负载最低的模型",
|
||||
"latencyBasedLabel": "Latency Based - 路由到响应时间最短的模型",
|
||||
"costBasedLabel": "Cost Based - 路由到最具成本效益的模型",
|
||||
"usageBasedLabel": "Usage Based - 基于历史用量模式进行路由",
|
||||
"customLabel": "Custom - 使用配置中定义的自定义路由逻辑"
|
||||
},
|
||||
"addModelTab": {
|
||||
"addModelTab": "添加模型",
|
||||
"addAutoRouterTab": "添加自动路由器"
|
||||
},
|
||||
"addAutoRouterTab": {
|
||||
"title": "添加自动路由器",
|
||||
"subtitle": "创建一个自动路由器,根据请求复杂度或语义匹配自动选择最佳模型。",
|
||||
"routerTypeLabel": "路由器类型",
|
||||
"complexityRouterLabel": "复杂度路由器",
|
||||
"complexityRouterBadge": "推荐",
|
||||
"complexityRouterDesc": "根据请求复杂度自动路由。无需训练数据——只需选择 4 个模型即可开始。",
|
||||
"complexityRouterZeroApi": "零 API 调用",
|
||||
"complexityRouterLatency": "<1ms 延迟",
|
||||
"complexityRouterNoCost": "零成本",
|
||||
"semanticRouterLabel": "语义路由器",
|
||||
"semanticRouterDesc": "根据与示例话语的语义相似度进行路由。需要嵌入模型和训练示例。",
|
||||
"autoRouterNameLabel": "自动路由器名称",
|
||||
"autoRouterNameRequired": "自动路由器名称为必填项",
|
||||
"autoRouterNameTooltip": "此自动路由器配置的唯一名称",
|
||||
"autoRouterNamePlaceholder": "例如:smart_router、auto_router_1",
|
||||
"defaultModelLabel": "默认模型",
|
||||
"defaultModelRequired": "默认模型为必填项",
|
||||
"defaultModelTooltip": "当自动路由逻辑无法确定最佳模型时使用的回退模型",
|
||||
"defaultModelPlaceholder": "选择默认模型",
|
||||
"enterCustomModelName": "输入自定义模型名称",
|
||||
"embeddingModelLabel": "嵌入模型",
|
||||
"embeddingModelTooltip": "可选:用于语义路由决策的嵌入模型",
|
||||
"embeddingModelPlaceholder": "选择嵌入模型(可选)",
|
||||
"additionalSettingsDivider": "附加设置",
|
||||
"modelAccessGroupLabel": "模型访问组",
|
||||
"modelAccessGroupTooltip": "使用模型访问组控制谁可以访问此自动路由器",
|
||||
"modelAccessGroupPlaceholder": "选择已有组或输入新组名",
|
||||
"needHelpTooltip": "在 GitHub 上获取帮助",
|
||||
"needHelp": "需要帮助?",
|
||||
"testConnectionButton": "测试连接",
|
||||
"addAutoRouterButton": "添加自动路由器",
|
||||
"connectionTestResultsTitle": "连接测试结果",
|
||||
"enterRouterNameError": "请输入自动路由器名称",
|
||||
"selectTierModelError": "请为复杂度层级至少选择一个模型",
|
||||
"fillRequiredFieldsError": "请填写所有必填字段",
|
||||
"selectDefaultModelError": "请选择默认模型",
|
||||
"configureRouteError": "请为自动路由器至少配置一条路由",
|
||||
"invalidRoutesError": "请确保所有路由都有目标模型、描述和至少一个话语示例",
|
||||
"missingFieldsError": "请填写以下必填字段:{{fields}}"
|
||||
},
|
||||
"complexityRouterConfig": {
|
||||
"title": "复杂度层级配置",
|
||||
"titleTooltip": "将每个复杂度层级映射到一个模型。简单查询使用更便宜/更快的模型,复杂查询使用更强大的模型。",
|
||||
"subtitle": "复杂度路由器使用基于规则的评分(无 API 调用,<1ms 延迟)自动按复杂度对请求进行分类。配置每个层级由哪个模型处理。",
|
||||
"simpleTierLabel": "简单",
|
||||
"simpleTierDescription": "基本问题、问候语、简单事实查询",
|
||||
"simpleTierExamples": "\"你好!\"、\"Python 是什么?\"、\"谢谢!\"",
|
||||
"mediumTierLabel": "中等",
|
||||
"mediumTierDescription": "需要一定推理或解释的标准查询",
|
||||
"mediumTierExamples": "\"解释 REST API 的工作原理\"、\"调试此错误\"",
|
||||
"complexTierLabel": "复杂",
|
||||
"complexTierDescription": "需要深度知识的技术性、多部分请求",
|
||||
"complexTierExamples": "\"设计微服务架构\"、\"实现速率限制器\"",
|
||||
"reasoningTierLabel": "推理",
|
||||
"reasoningTierDescription": "思维链、分析、明确的推理请求",
|
||||
"reasoningTierExamples": "\"请逐步思考...\"、\"分析优缺点...\"",
|
||||
"tierLabel": "{{label}}层级",
|
||||
"examplesLabel": "示例:{{examples}}",
|
||||
"selectModelPlaceholder": "为{{label}}查询选择模型",
|
||||
"howClassificationWorksTitle": "分类原理",
|
||||
"howClassificationWorksDesc": "路由器从 7 个维度对每个请求进行评分:Token 数量、代码存在性、推理标记、技术术语、简单指标、多步骤模式和问题复杂度。加权分数决定层级:"
|
||||
},
|
||||
"routerConfigBuilder": {
|
||||
"title": "路由配置",
|
||||
"titleTooltip": "配置路由逻辑,根据用户输入模式自动选择最佳模型",
|
||||
"addRouteButton": "添加路由",
|
||||
"noRoutesDesc": "未配置路由。点击「添加路由」开始。",
|
||||
"routeLabel": "路由 {{index}}:{{model}}",
|
||||
"routeUnnamed": "未命名",
|
||||
"modelLabel": "模型",
|
||||
"modelPlaceholder": "选择模型",
|
||||
"descriptionLabel": "描述",
|
||||
"descriptionPlaceholder": "描述何时应使用此路由...",
|
||||
"scoreThresholdLabel": "分数阈值",
|
||||
"scoreThresholdTooltip": "路由到此模型的最低相似度分数(0-1)",
|
||||
"exampleUtterancesLabel": "示例话语",
|
||||
"exampleUtterancesTooltip": "此路由的训练示例。输入话语后按 Enter 添加。",
|
||||
"utterancesHint": "输入话语后按 Enter 添加,也可粘贴多行内容。",
|
||||
"utterancesPlaceholder": "输入话语后按 Enter...",
|
||||
"jsonPreviewLabel": "JSON 预览",
|
||||
"showButton": "显示",
|
||||
"hideButton": "隐藏"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue