fallback model/providers (#259)
Co-authored-by: zuolingxuan <zuolingxuan@bytedance.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
bc47b455b5
commit
e52916a7ef
@@ -36,6 +36,26 @@ import { cn } from '@/lib/utils';
|
||||
import { toast } from 'sonner';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function normalizeFallbackProviderIds(ids?: string[]): string[] {
|
||||
return Array.from(new Set((ids ?? []).filter(Boolean)));
|
||||
}
|
||||
|
||||
function fallbackProviderIdsEqual(a?: string[], b?: string[]): boolean {
|
||||
const left = normalizeFallbackProviderIds(a).sort();
|
||||
const right = normalizeFallbackProviderIds(b).sort();
|
||||
return left.length === right.length && left.every((id, index) => id === right[index]);
|
||||
}
|
||||
|
||||
function normalizeFallbackModels(models?: string[]): string[] {
|
||||
return Array.from(new Set((models ?? []).map((model) => model.trim()).filter(Boolean)));
|
||||
}
|
||||
|
||||
function fallbackModelsEqual(a?: string[], b?: string[]): boolean {
|
||||
const left = normalizeFallbackModels(a);
|
||||
const right = normalizeFallbackModels(b);
|
||||
return left.length === right.length && left.every((model, index) => model === right[index]);
|
||||
}
|
||||
|
||||
export function ProvidersSettings() {
|
||||
const { t } = useTranslation('settings');
|
||||
const {
|
||||
@@ -144,6 +164,7 @@ export function ProvidersSettings() {
|
||||
<ProviderCard
|
||||
key={provider.id}
|
||||
provider={provider}
|
||||
allProviders={providers}
|
||||
isDefault={provider.id === defaultProviderId}
|
||||
isEditing={editingProvider === provider.id}
|
||||
onEdit={() => setEditingProvider(provider.id)}
|
||||
@@ -179,6 +200,7 @@ export function ProvidersSettings() {
|
||||
|
||||
interface ProviderCardProps {
|
||||
provider: ProviderWithKeyInfo;
|
||||
allProviders: ProviderWithKeyInfo[];
|
||||
isDefault: boolean;
|
||||
isEditing: boolean;
|
||||
onEdit: () => void;
|
||||
@@ -196,6 +218,7 @@ interface ProviderCardProps {
|
||||
|
||||
function ProviderCard({
|
||||
provider,
|
||||
allProviders,
|
||||
isDefault,
|
||||
isEditing,
|
||||
onEdit,
|
||||
@@ -209,12 +232,18 @@ function ProviderCard({
|
||||
const [newKey, setNewKey] = useState('');
|
||||
const [baseUrl, setBaseUrl] = useState(provider.baseUrl || '');
|
||||
const [modelId, setModelId] = useState(provider.model || '');
|
||||
const [fallbackModelsText, setFallbackModelsText] = useState(
|
||||
normalizeFallbackModels(provider.fallbackModels).join('\n')
|
||||
);
|
||||
const [fallbackProviderIds, setFallbackProviderIds] = useState<string[]>(
|
||||
normalizeFallbackProviderIds(provider.fallbackProviderIds)
|
||||
);
|
||||
const [showKey, setShowKey] = useState(false);
|
||||
const [validating, setValidating] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
const typeInfo = PROVIDER_TYPE_INFO.find((t) => t.id === provider.type);
|
||||
const canEditConfig = Boolean(typeInfo?.showBaseUrl || typeInfo?.showModelId);
|
||||
const canEditModelConfig = Boolean(typeInfo?.showBaseUrl || typeInfo?.showModelId);
|
||||
|
||||
useEffect(() => {
|
||||
if (isEditing) {
|
||||
@@ -222,13 +251,26 @@ function ProviderCard({
|
||||
setShowKey(false);
|
||||
setBaseUrl(provider.baseUrl || '');
|
||||
setModelId(provider.model || '');
|
||||
setFallbackModelsText(normalizeFallbackModels(provider.fallbackModels).join('\n'));
|
||||
setFallbackProviderIds(normalizeFallbackProviderIds(provider.fallbackProviderIds));
|
||||
}
|
||||
}, [isEditing, provider.baseUrl, provider.model]);
|
||||
}, [isEditing, provider.baseUrl, provider.fallbackModels, provider.fallbackProviderIds, provider.model]);
|
||||
|
||||
const fallbackOptions = allProviders.filter((candidate) => candidate.id !== provider.id);
|
||||
|
||||
const toggleFallbackProvider = (providerId: string) => {
|
||||
setFallbackProviderIds((current) => (
|
||||
current.includes(providerId)
|
||||
? current.filter((id) => id !== providerId)
|
||||
: [...current, providerId]
|
||||
));
|
||||
};
|
||||
|
||||
const handleSaveEdits = async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
const payload: { newApiKey?: string; updates?: Partial<ProviderConfig> } = {};
|
||||
const normalizedFallbackModels = normalizeFallbackModels(fallbackModelsText.split('\n'));
|
||||
|
||||
if (newKey.trim()) {
|
||||
setValidating(true);
|
||||
@@ -244,7 +286,7 @@ function ProviderCard({
|
||||
payload.newApiKey = newKey.trim();
|
||||
}
|
||||
|
||||
if (canEditConfig) {
|
||||
{
|
||||
if (typeInfo?.showModelId && !modelId.trim()) {
|
||||
toast.error(t('aiProviders.toast.modelRequired'));
|
||||
setSaving(false);
|
||||
@@ -252,12 +294,18 @@ function ProviderCard({
|
||||
}
|
||||
|
||||
const updates: Partial<ProviderConfig> = {};
|
||||
if ((baseUrl.trim() || undefined) !== (provider.baseUrl || undefined)) {
|
||||
if (typeInfo?.showBaseUrl && (baseUrl.trim() || undefined) !== (provider.baseUrl || undefined)) {
|
||||
updates.baseUrl = baseUrl.trim() || undefined;
|
||||
}
|
||||
if ((modelId.trim() || undefined) !== (provider.model || undefined)) {
|
||||
if (typeInfo?.showModelId && (modelId.trim() || undefined) !== (provider.model || undefined)) {
|
||||
updates.model = modelId.trim() || undefined;
|
||||
}
|
||||
if (!fallbackModelsEqual(normalizedFallbackModels, provider.fallbackModels)) {
|
||||
updates.fallbackModels = normalizedFallbackModels;
|
||||
}
|
||||
if (!fallbackProviderIdsEqual(fallbackProviderIds, provider.fallbackProviderIds)) {
|
||||
updates.fallbackProviderIds = normalizeFallbackProviderIds(fallbackProviderIds);
|
||||
}
|
||||
if (Object.keys(updates).length > 0) {
|
||||
payload.updates = updates;
|
||||
}
|
||||
@@ -308,9 +356,10 @@ function ProviderCard({
|
||||
|
||||
{/* Key row */}
|
||||
{isEditing ? (
|
||||
<div className="space-y-2">
|
||||
{canEditConfig && (
|
||||
<>
|
||||
<div className="space-y-4">
|
||||
{canEditModelConfig && (
|
||||
<div className="space-y-3 rounded-md border p-3">
|
||||
<p className="text-sm font-medium">{t('aiProviders.sections.model')}</p>
|
||||
{typeInfo?.showBaseUrl && (
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">{t('aiProviders.dialog.baseUrl')}</Label>
|
||||
@@ -333,87 +382,158 @@ function ProviderCard({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{typeInfo?.apiKeyUrl && (
|
||||
<div className="flex justify-start mb-1">
|
||||
<a
|
||||
href={typeInfo.apiKeyUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-xs text-primary hover:underline flex items-center gap-1"
|
||||
tabIndex={-1}
|
||||
>
|
||||
{t('aiProviders.oauth.getApiKey')} <ExternalLink className="h-3 w-3" />
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-2">
|
||||
<div className="relative flex-1">
|
||||
<Input
|
||||
type={showKey ? 'text' : 'password'}
|
||||
placeholder={typeInfo?.requiresApiKey ? typeInfo?.placeholder : (typeInfo?.id === 'ollama' ? t('aiProviders.notRequired') : t('aiProviders.card.editKey'))}
|
||||
value={newKey}
|
||||
onChange={(e) => setNewKey(e.target.value)}
|
||||
className="pr-10 h-9 text-sm"
|
||||
<div className="space-y-3 rounded-md border p-3">
|
||||
<p className="text-sm font-medium">{t('aiProviders.sections.fallback')}</p>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">{t('aiProviders.dialog.fallbackModelIds')}</Label>
|
||||
<textarea
|
||||
value={fallbackModelsText}
|
||||
onChange={(e) => setFallbackModelsText(e.target.value)}
|
||||
placeholder={t('aiProviders.dialog.fallbackModelIdsPlaceholder')}
|
||||
className="min-h-24 w-full rounded-md border bg-background px-3 py-2 text-sm outline-none"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowKey(!showKey)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
{showKey ? <EyeOff className="h-3.5 w-3.5" /> : <Eye className="h-3.5 w-3.5" />}
|
||||
</button>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t('aiProviders.dialog.fallbackModelIdsHelp')}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleSaveEdits}
|
||||
disabled={
|
||||
validating
|
||||
|| saving
|
||||
|| (
|
||||
!newKey.trim()
|
||||
&& (baseUrl.trim() || undefined) === (provider.baseUrl || undefined)
|
||||
&& (modelId.trim() || undefined) === (provider.model || undefined)
|
||||
)
|
||||
|| Boolean(typeInfo?.showModelId && !modelId.trim())
|
||||
}
|
||||
>
|
||||
{validating || saving ? (
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">{t('aiProviders.dialog.fallbackProviders')}</Label>
|
||||
{fallbackOptions.length === 0 ? (
|
||||
<p className="text-xs text-muted-foreground">{t('aiProviders.dialog.noFallbackOptions')}</p>
|
||||
) : (
|
||||
<Check className="h-3.5 w-3.5" />
|
||||
<div className="space-y-2 rounded-md border p-2">
|
||||
{fallbackOptions.map((candidate) => (
|
||||
<label key={candidate.id} className="flex items-center gap-2 text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={fallbackProviderIds.includes(candidate.id)}
|
||||
onChange={() => toggleFallbackProvider(candidate.id)}
|
||||
/>
|
||||
<span className="font-medium">{candidate.name}</span>
|
||||
<span className="text-xs text-muted-foreground">{candidate.model || candidate.type}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" onClick={onCancelEdit}>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3 rounded-md border p-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">{t('aiProviders.dialog.apiKey')}</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{provider.hasKey
|
||||
? t('aiProviders.dialog.apiKeyConfigured')
|
||||
: t('aiProviders.dialog.apiKeyMissing')}
|
||||
</p>
|
||||
</div>
|
||||
{provider.hasKey ? (
|
||||
<Badge variant="secondary">{t('aiProviders.card.configured')}</Badge>
|
||||
) : null}
|
||||
</div>
|
||||
{typeInfo?.apiKeyUrl && (
|
||||
<div className="flex justify-start">
|
||||
<a
|
||||
href={typeInfo.apiKeyUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-xs text-primary hover:underline flex items-center gap-1"
|
||||
tabIndex={-1}
|
||||
>
|
||||
{t('aiProviders.oauth.getApiKey')} <ExternalLink className="h-3 w-3" />
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">{t('aiProviders.dialog.replaceApiKey')}</Label>
|
||||
<div className="flex gap-2">
|
||||
<div className="relative flex-1">
|
||||
<Input
|
||||
type={showKey ? 'text' : 'password'}
|
||||
placeholder={typeInfo?.requiresApiKey ? typeInfo?.placeholder : (typeInfo?.id === 'ollama' ? t('aiProviders.notRequired') : t('aiProviders.card.editKey'))}
|
||||
value={newKey}
|
||||
onChange={(e) => setNewKey(e.target.value)}
|
||||
className="pr-10 h-9 text-sm"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowKey(!showKey)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
{showKey ? <EyeOff className="h-3.5 w-3.5" /> : <Eye className="h-3.5 w-3.5" />}
|
||||
</button>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleSaveEdits}
|
||||
disabled={
|
||||
validating
|
||||
|| saving
|
||||
|| (
|
||||
!newKey.trim()
|
||||
&& (baseUrl.trim() || undefined) === (provider.baseUrl || undefined)
|
||||
&& (modelId.trim() || undefined) === (provider.model || undefined)
|
||||
&& fallbackModelsEqual(normalizeFallbackModels(fallbackModelsText.split('\n')), provider.fallbackModels)
|
||||
&& fallbackProviderIdsEqual(fallbackProviderIds, provider.fallbackProviderIds)
|
||||
)
|
||||
|| Boolean(typeInfo?.showModelId && !modelId.trim())
|
||||
}
|
||||
>
|
||||
{validating || saving ? (
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
) : (
|
||||
<Check className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" onClick={onCancelEdit}>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t('aiProviders.dialog.replaceApiKeyHelp')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-between rounded-md bg-muted/50 px-3 py-2">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
{typeInfo?.isOAuth ? (
|
||||
<>
|
||||
<Key className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
|
||||
<Badge variant="secondary" className="text-xs shrink-0">{t('aiProviders.card.configured')}</Badge>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Key className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
|
||||
<span className="text-sm font-mono text-muted-foreground truncate">
|
||||
{provider.hasKey
|
||||
? (provider.keyMasked && provider.keyMasked.length > 12
|
||||
? `${provider.keyMasked.substring(0, 4)}...${provider.keyMasked.substring(provider.keyMasked.length - 4)}`
|
||||
: provider.keyMasked)
|
||||
: t('aiProviders.card.noKey')}
|
||||
</span>
|
||||
{provider.hasKey && (
|
||||
<div className="min-w-0 space-y-1">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
{typeInfo?.isOAuth ? (
|
||||
<>
|
||||
<Key className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
|
||||
<Badge variant="secondary" className="text-xs shrink-0">{t('aiProviders.card.configured')}</Badge>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Key className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
|
||||
<span className="text-sm font-mono text-muted-foreground truncate">
|
||||
{provider.hasKey
|
||||
? (provider.keyMasked && provider.keyMasked.length > 12
|
||||
? `${provider.keyMasked.substring(0, 4)}...${provider.keyMasked.substring(provider.keyMasked.length - 4)}`
|
||||
: provider.keyMasked)
|
||||
: t('aiProviders.card.noKey')}
|
||||
</span>
|
||||
{provider.hasKey && (
|
||||
<Badge variant="secondary" className="text-xs shrink-0">{t('aiProviders.card.configured')}</Badge>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground truncate">
|
||||
{t('aiProviders.card.fallbacks', {
|
||||
count: (provider.fallbackModels?.length ?? 0) + (provider.fallbackProviderIds?.length ?? 0),
|
||||
names: [
|
||||
...normalizeFallbackModels(provider.fallbackModels),
|
||||
...normalizeFallbackProviderIds(provider.fallbackProviderIds)
|
||||
.map((fallbackId) => allProviders.find((candidate) => candidate.id === fallbackId)?.name)
|
||||
.filter(Boolean),
|
||||
].join(', ') || t('aiProviders.card.none'),
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-0.5 shrink-0 ml-2">
|
||||
<Button
|
||||
|
||||
@@ -13,6 +13,10 @@
|
||||
"aiProviders": {
|
||||
"title": "AI Providers",
|
||||
"description": "Configure your AI model providers and API keys",
|
||||
"sections": {
|
||||
"model": "Model Settings",
|
||||
"fallback": "Fallback Settings"
|
||||
},
|
||||
"add": "Add Provider",
|
||||
"custom": "Custom",
|
||||
"notRequired": "Not required",
|
||||
@@ -26,9 +30,19 @@
|
||||
"desc": "Configure a new AI model provider",
|
||||
"displayName": "Display Name",
|
||||
"apiKey": "API Key",
|
||||
"apiKeyConfigured": "An API key is already stored for this provider.",
|
||||
"apiKeyMissing": "No API key is stored for this provider yet.",
|
||||
"apiKeyStored": "Your API key is stored locally on your machine.",
|
||||
"replaceApiKey": "Replace API Key",
|
||||
"replaceApiKeyHelp": "Leave this field empty if you want to keep the currently stored API key.",
|
||||
"baseUrl": "Base URL",
|
||||
"modelId": "Model ID",
|
||||
"fallbackModels": "Fallback Models",
|
||||
"fallbackProviders": "Fallback Providers",
|
||||
"fallbackModelIds": "Fallback Model IDs",
|
||||
"fallbackModelIdsPlaceholder": "gpt-4.1-mini\nanother-model-id",
|
||||
"fallbackModelIdsHelp": "One model ID per line. These models use the current provider config before falling back to other providers.",
|
||||
"noFallbackOptions": "Add another provider first to use it as a fallback target.",
|
||||
"cancel": "Cancel",
|
||||
"change": "Change provider",
|
||||
"add": "Add Provider",
|
||||
@@ -39,6 +53,9 @@
|
||||
"default": "Default",
|
||||
"configured": "Configured",
|
||||
"noKey": "No API key set",
|
||||
"none": "None",
|
||||
"fallbacks_one": "Fallback: {{names}}",
|
||||
"fallbacks_other": "Fallbacks ({{count}}): {{names}}",
|
||||
"setDefault": "Set as default",
|
||||
"editKey": "Edit API key",
|
||||
"delete": "Delete provider"
|
||||
|
||||
@@ -13,6 +13,10 @@
|
||||
"aiProviders": {
|
||||
"title": "AI プロバイダー",
|
||||
"description": "AI モデルプロバイダーと API キーを設定",
|
||||
"sections": {
|
||||
"model": "モデル設定",
|
||||
"fallback": "フォールバック設定"
|
||||
},
|
||||
"add": "プロバイダーを追加",
|
||||
"custom": "カスタム",
|
||||
"notRequired": "不要",
|
||||
@@ -26,9 +30,19 @@
|
||||
"desc": "新しい AI モデルプロバイダーを構成",
|
||||
"displayName": "表示名",
|
||||
"apiKey": "API キー",
|
||||
"apiKeyConfigured": "このプロバイダーには API キーが保存されています。",
|
||||
"apiKeyMissing": "このプロバイダーにはまだ API キーが保存されていません。",
|
||||
"apiKeyStored": "API キーはローカルマシンに保存されます。",
|
||||
"replaceApiKey": "API キーを置き換える",
|
||||
"replaceApiKeyHelp": "現在保存されている API キーをそのまま使う場合は、この欄を空のままにしてください。",
|
||||
"baseUrl": "ベース URL",
|
||||
"modelId": "モデル ID",
|
||||
"fallbackModels": "フォールバックモデル",
|
||||
"fallbackProviders": "別プロバイダーへのフォールバック",
|
||||
"fallbackModelIds": "同一プロバイダーのフォールバックモデル ID",
|
||||
"fallbackModelIdsPlaceholder": "gpt-4.1-mini\nanother-model-id",
|
||||
"fallbackModelIdsHelp": "1 行につき 1 つのモデル ID を指定します。まず現在のプロバイダー内でこれらを試し、その後ほかのプロバイダーへフォールバックします。",
|
||||
"noFallbackOptions": "フォールバック先にするには、先に別のプロバイダーを追加してください。",
|
||||
"cancel": "キャンセル",
|
||||
"change": "プロバイダーを変更",
|
||||
"add": "プロバイダーを追加",
|
||||
@@ -39,6 +53,9 @@
|
||||
"default": "デフォルト",
|
||||
"configured": "構成済み",
|
||||
"noKey": "API キー未設定",
|
||||
"none": "なし",
|
||||
"fallbacks_one": "フォールバック: {{names}}",
|
||||
"fallbacks_other": "フォールバック ({{count}}): {{names}}",
|
||||
"setDefault": "デフォルトに設定",
|
||||
"editKey": "API キーを編集",
|
||||
"delete": "プロバイダーを削除"
|
||||
@@ -53,7 +70,8 @@
|
||||
"updated": "プロバイダーが更新されました",
|
||||
"failedUpdate": "プロバイダーの更新に失敗しました",
|
||||
"invalidKey": "無効な API キー",
|
||||
"modelRequired": "モデル ID が必要です"
|
||||
"modelRequired": "モデル ID が必要です",
|
||||
"minimaxConflict": "MiniMax (Global) と MiniMax (CN) は同時に追加できません。"
|
||||
},
|
||||
"oauth": {
|
||||
"loginMode": "OAuthログイン",
|
||||
|
||||
@@ -13,6 +13,10 @@
|
||||
"aiProviders": {
|
||||
"title": "AI 模型提供商",
|
||||
"description": "配置 AI 模型提供商和 API 密钥",
|
||||
"sections": {
|
||||
"model": "模型配置",
|
||||
"fallback": "回退配置"
|
||||
},
|
||||
"add": "添加提供商",
|
||||
"custom": "自定义",
|
||||
"notRequired": "非必填",
|
||||
@@ -26,9 +30,19 @@
|
||||
"desc": "配置新的 AI 模型提供商",
|
||||
"displayName": "显示名称",
|
||||
"apiKey": "API 密钥",
|
||||
"apiKeyConfigured": "这个 provider 已经保存了 API key。",
|
||||
"apiKeyMissing": "这个 provider 还没有保存 API key。",
|
||||
"apiKeyStored": "您的 API 密钥存储在本地机器上。",
|
||||
"replaceApiKey": "替换 API Key",
|
||||
"replaceApiKeyHelp": "如果想保留当前已保存的 API key,这里留空即可。",
|
||||
"baseUrl": "基础 URL",
|
||||
"modelId": "模型 ID",
|
||||
"fallbackModels": "回退模型",
|
||||
"fallbackProviders": "跨 Provider 回退",
|
||||
"fallbackModelIds": "同 Provider 回退模型 ID",
|
||||
"fallbackModelIdsPlaceholder": "gpt-4.1-mini\nanother-model-id",
|
||||
"fallbackModelIdsHelp": "每行一个模型 ID。会先使用当前 provider 的这些模型,再回退到其他 provider。",
|
||||
"noFallbackOptions": "请先添加其他 provider,才能把它设为回退目标。",
|
||||
"cancel": "取消",
|
||||
"change": "更换提供商",
|
||||
"add": "添加提供商",
|
||||
@@ -39,6 +53,9 @@
|
||||
"default": "默认",
|
||||
"configured": "已配置",
|
||||
"noKey": "未设置 API 密钥",
|
||||
"none": "无",
|
||||
"fallbacks_one": "回退:{{names}}",
|
||||
"fallbacks_other": "回退({{count}} 个):{{names}}",
|
||||
"setDefault": "设为默认",
|
||||
"editKey": "编辑 API 密钥",
|
||||
"delete": "删除提供商"
|
||||
|
||||
@@ -29,6 +29,8 @@ export interface ProviderConfig {
|
||||
type: ProviderType;
|
||||
baseUrl?: string;
|
||||
model?: string;
|
||||
fallbackModels?: string[];
|
||||
fallbackProviderIds?: string[];
|
||||
enabled: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
|
||||
Reference in New Issue
Block a user