fallback model/providers (#259)
Co-authored-by: zuolingxuan <zuolingxuan@bytedance.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
bc47b455b5
commit
e52916a7ef
@@ -16,6 +16,7 @@ import {
|
|||||||
hasApiKey,
|
hasApiKey,
|
||||||
saveProvider,
|
saveProvider,
|
||||||
getProvider,
|
getProvider,
|
||||||
|
getAllProviders,
|
||||||
deleteProvider,
|
deleteProvider,
|
||||||
setDefaultProvider,
|
setDefaultProvider,
|
||||||
getDefaultProvider,
|
getDefaultProvider,
|
||||||
@@ -48,6 +49,7 @@ import { checkUvInstalled, installUv, setupManagedPython } from '../utils/uv-set
|
|||||||
import { updateSkillConfig, getSkillConfig, getAllSkillConfigs } from '../utils/skill-config';
|
import { updateSkillConfig, getSkillConfig, getAllSkillConfigs } from '../utils/skill-config';
|
||||||
import { whatsAppLoginManager } from '../utils/whatsapp-login';
|
import { whatsAppLoginManager } from '../utils/whatsapp-login';
|
||||||
import { getProviderConfig } from '../utils/provider-registry';
|
import { getProviderConfig } from '../utils/provider-registry';
|
||||||
|
import { getProviderDefaultModel } from '../utils/provider-registry';
|
||||||
import { deviceOAuthManager, OAuthProviderType } from '../utils/device-oauth';
|
import { deviceOAuthManager, OAuthProviderType } from '../utils/device-oauth';
|
||||||
import { applyProxySettings } from './proxy';
|
import { applyProxySettings } from './proxy';
|
||||||
import { proxyAwareFetch } from '../utils/proxy-fetch';
|
import { proxyAwareFetch } from '../utils/proxy-fetch';
|
||||||
@@ -73,6 +75,54 @@ export function getOpenClawProviderKey(type: string, providerId: string): string
|
|||||||
return type;
|
return type;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getProviderModelRef(config: ProviderConfig): string | undefined {
|
||||||
|
const providerKey = getOpenClawProviderKey(config.type, config.id);
|
||||||
|
|
||||||
|
if (config.model) {
|
||||||
|
return config.model.startsWith(`${providerKey}/`)
|
||||||
|
? config.model
|
||||||
|
: `${providerKey}/${config.model}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return getProviderDefaultModel(config.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getProviderFallbackModelRefs(config: ProviderConfig): Promise<string[]> {
|
||||||
|
const allProviders = await getAllProviders();
|
||||||
|
const providerMap = new Map(allProviders.map((provider) => [provider.id, provider]));
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const results: string[] = [];
|
||||||
|
const providerKey = getOpenClawProviderKey(config.type, config.id);
|
||||||
|
|
||||||
|
for (const fallbackModel of config.fallbackModels ?? []) {
|
||||||
|
const normalizedModel = fallbackModel.trim();
|
||||||
|
if (!normalizedModel) continue;
|
||||||
|
|
||||||
|
const modelRef = normalizedModel.startsWith(`${providerKey}/`)
|
||||||
|
? normalizedModel
|
||||||
|
: `${providerKey}/${normalizedModel}`;
|
||||||
|
|
||||||
|
if (seen.has(modelRef)) continue;
|
||||||
|
seen.add(modelRef);
|
||||||
|
results.push(modelRef);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const fallbackId of config.fallbackProviderIds ?? []) {
|
||||||
|
if (!fallbackId || fallbackId === config.id) continue;
|
||||||
|
|
||||||
|
const fallbackProvider = providerMap.get(fallbackId);
|
||||||
|
if (!fallbackProvider) continue;
|
||||||
|
|
||||||
|
const modelRef = getProviderModelRef(fallbackProvider);
|
||||||
|
if (!modelRef || seen.has(modelRef)) continue;
|
||||||
|
|
||||||
|
seen.add(modelRef);
|
||||||
|
results.push(modelRef);
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register all IPC handlers
|
* Register all IPC handlers
|
||||||
*/
|
*/
|
||||||
@@ -1107,6 +1157,7 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
|
|||||||
|
|
||||||
// Sync the provider configuration to openclaw.json so Gateway knows about it
|
// Sync the provider configuration to openclaw.json so Gateway knows about it
|
||||||
try {
|
try {
|
||||||
|
const fallbackModels = await getProviderFallbackModelRefs(nextConfig);
|
||||||
const meta = getProviderConfig(nextConfig.type);
|
const meta = getProviderConfig(nextConfig.type);
|
||||||
const api = nextConfig.type === 'custom' || nextConfig.type === 'ollama' ? 'openai-completions' : meta?.api;
|
const api = nextConfig.type === 'custom' || nextConfig.type === 'ollama' ? 'openai-completions' : meta?.api;
|
||||||
|
|
||||||
@@ -1141,12 +1192,12 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
|
|||||||
? `${ock}/${nextConfig.model}`
|
? `${ock}/${nextConfig.model}`
|
||||||
: undefined;
|
: undefined;
|
||||||
if (nextConfig.type !== 'custom' && nextConfig.type !== 'ollama') {
|
if (nextConfig.type !== 'custom' && nextConfig.type !== 'ollama') {
|
||||||
await setOpenClawDefaultModel(nextConfig.type, modelOverride);
|
await setOpenClawDefaultModel(ock, modelOverride, fallbackModels);
|
||||||
} else {
|
} else {
|
||||||
await setOpenClawDefaultModelWithOverride(ock, modelOverride, {
|
await setOpenClawDefaultModelWithOverride(ock, modelOverride, {
|
||||||
baseUrl: nextConfig.baseUrl,
|
baseUrl: nextConfig.baseUrl,
|
||||||
api: 'openai-completions',
|
api: 'openai-completions',
|
||||||
});
|
}, fallbackModels);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1222,6 +1273,7 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
|
|||||||
try {
|
try {
|
||||||
const ock = getOpenClawProviderKey(provider.type, providerId);
|
const ock = getOpenClawProviderKey(provider.type, providerId);
|
||||||
const providerKey = await getApiKey(providerId);
|
const providerKey = await getApiKey(providerId);
|
||||||
|
const fallbackModels = await getProviderFallbackModelRefs(provider);
|
||||||
|
|
||||||
// OAuth providers (qwen-portal, minimax-portal, minimax-portal-cn) might use OAuth OR a direct API key.
|
// OAuth providers (qwen-portal, minimax-portal, minimax-portal-cn) might use OAuth OR a direct API key.
|
||||||
// Treat them as OAuth only if they don't have a local API key configured.
|
// Treat them as OAuth only if they don't have a local API key configured.
|
||||||
@@ -1240,9 +1292,9 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
|
|||||||
await setOpenClawDefaultModelWithOverride(ock, modelOverride, {
|
await setOpenClawDefaultModelWithOverride(ock, modelOverride, {
|
||||||
baseUrl: provider.baseUrl,
|
baseUrl: provider.baseUrl,
|
||||||
api: 'openai-completions',
|
api: 'openai-completions',
|
||||||
});
|
}, fallbackModels);
|
||||||
} else {
|
} else {
|
||||||
await setOpenClawDefaultModel(provider.type, modelOverride);
|
await setOpenClawDefaultModel(ock, modelOverride, fallbackModels);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep auth-profiles in sync with the default provider instance.
|
// Keep auth-profiles in sync with the default provider instance.
|
||||||
@@ -1270,13 +1322,13 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
|
|||||||
? 'minimax-portal'
|
? 'minimax-portal'
|
||||||
: provider.type;
|
: provider.type;
|
||||||
|
|
||||||
await setOpenClawDefaultModelWithOverride(targetProviderKey, undefined, {
|
await setOpenClawDefaultModelWithOverride(targetProviderKey, getProviderModelRef(provider), {
|
||||||
baseUrl,
|
baseUrl,
|
||||||
api,
|
api,
|
||||||
authHeader: targetProviderKey === 'minimax-portal' ? true : undefined,
|
authHeader: targetProviderKey === 'minimax-portal' ? true : undefined,
|
||||||
// Relies on OpenClaw Gateway native auth-profiles syncing
|
// Relies on OpenClaw Gateway native auth-profiles syncing
|
||||||
apiKeyEnv: targetProviderKey === 'minimax-portal' ? 'minimax-oauth' : 'qwen-oauth',
|
apiKeyEnv: targetProviderKey === 'minimax-portal' ? 'minimax-oauth' : 'qwen-oauth',
|
||||||
});
|
}, fallbackModels);
|
||||||
|
|
||||||
logger.info(`Configured openclaw.json for OAuth provider "${provider.type}"`);
|
logger.info(`Configured openclaw.json for OAuth provider "${provider.type}"`);
|
||||||
|
|
||||||
|
|||||||
@@ -358,7 +358,11 @@ export function buildProviderEnvVars(providers: Array<{ type: string; apiKey: st
|
|||||||
* Update the OpenClaw config to use the given provider and model
|
* Update the OpenClaw config to use the given provider and model
|
||||||
* Writes to ~/.openclaw/openclaw.json
|
* Writes to ~/.openclaw/openclaw.json
|
||||||
*/
|
*/
|
||||||
export async function setOpenClawDefaultModel(provider: string, modelOverride?: string): Promise<void> {
|
export async function setOpenClawDefaultModel(
|
||||||
|
provider: string,
|
||||||
|
modelOverride?: string,
|
||||||
|
fallbackModels: string[] = []
|
||||||
|
): Promise<void> {
|
||||||
const config = await readOpenClawJson();
|
const config = await readOpenClawJson();
|
||||||
|
|
||||||
const model = modelOverride || getProviderDefaultModel(provider);
|
const model = modelOverride || getProviderDefaultModel(provider);
|
||||||
@@ -370,11 +374,17 @@ export async function setOpenClawDefaultModel(provider: string, modelOverride?:
|
|||||||
const modelId = model.startsWith(`${provider}/`)
|
const modelId = model.startsWith(`${provider}/`)
|
||||||
? model.slice(provider.length + 1)
|
? model.slice(provider.length + 1)
|
||||||
: model;
|
: model;
|
||||||
|
const fallbackModelIds = fallbackModels
|
||||||
|
.filter((fallback) => fallback.startsWith(`${provider}/`))
|
||||||
|
.map((fallback) => fallback.slice(provider.length + 1));
|
||||||
|
|
||||||
// Set the default model for the agents
|
// Set the default model for the agents
|
||||||
const agents = (config.agents || {}) as Record<string, unknown>;
|
const agents = (config.agents || {}) as Record<string, unknown>;
|
||||||
const defaults = (agents.defaults || {}) as Record<string, unknown>;
|
const defaults = (agents.defaults || {}) as Record<string, unknown>;
|
||||||
defaults.model = { primary: model };
|
defaults.model = {
|
||||||
|
primary: model,
|
||||||
|
fallbacks: fallbackModels,
|
||||||
|
};
|
||||||
agents.defaults = defaults;
|
agents.defaults = defaults;
|
||||||
config.agents = agents;
|
config.agents = agents;
|
||||||
|
|
||||||
@@ -401,8 +411,10 @@ export async function setOpenClawDefaultModel(provider: string, modelOverride?:
|
|||||||
mergedModels.push(item);
|
mergedModels.push(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (modelId && !mergedModels.some((m) => m.id === modelId)) {
|
for (const candidateModelId of [modelId, ...fallbackModelIds]) {
|
||||||
mergedModels.push({ id: modelId, name: modelId });
|
if (candidateModelId && !mergedModels.some((m) => m.id === candidateModelId)) {
|
||||||
|
mergedModels.push({ id: candidateModelId, name: candidateModelId });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const providerEntry: Record<string, unknown> = {
|
const providerEntry: Record<string, unknown> = {
|
||||||
@@ -500,7 +512,8 @@ export async function syncProviderConfigToOpenClaw(
|
|||||||
export async function setOpenClawDefaultModelWithOverride(
|
export async function setOpenClawDefaultModelWithOverride(
|
||||||
provider: string,
|
provider: string,
|
||||||
modelOverride: string | undefined,
|
modelOverride: string | undefined,
|
||||||
override: RuntimeProviderConfigOverride
|
override: RuntimeProviderConfigOverride,
|
||||||
|
fallbackModels: string[] = []
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const config = await readOpenClawJson();
|
const config = await readOpenClawJson();
|
||||||
|
|
||||||
@@ -513,10 +526,16 @@ export async function setOpenClawDefaultModelWithOverride(
|
|||||||
const modelId = model.startsWith(`${provider}/`)
|
const modelId = model.startsWith(`${provider}/`)
|
||||||
? model.slice(provider.length + 1)
|
? model.slice(provider.length + 1)
|
||||||
: model;
|
: model;
|
||||||
|
const fallbackModelIds = fallbackModels
|
||||||
|
.filter((fallback) => fallback.startsWith(`${provider}/`))
|
||||||
|
.map((fallback) => fallback.slice(provider.length + 1));
|
||||||
|
|
||||||
const agents = (config.agents || {}) as Record<string, unknown>;
|
const agents = (config.agents || {}) as Record<string, unknown>;
|
||||||
const defaults = (agents.defaults || {}) as Record<string, unknown>;
|
const defaults = (agents.defaults || {}) as Record<string, unknown>;
|
||||||
defaults.model = { primary: model };
|
defaults.model = {
|
||||||
|
primary: model,
|
||||||
|
fallbacks: fallbackModels,
|
||||||
|
};
|
||||||
agents.defaults = defaults;
|
agents.defaults = defaults;
|
||||||
config.agents = agents;
|
config.agents = agents;
|
||||||
|
|
||||||
@@ -525,7 +544,11 @@ export async function setOpenClawDefaultModelWithOverride(
|
|||||||
const providers = (models.providers || {}) as Record<string, unknown>;
|
const providers = (models.providers || {}) as Record<string, unknown>;
|
||||||
|
|
||||||
const nextModels: Array<Record<string, unknown>> = [];
|
const nextModels: Array<Record<string, unknown>> = [];
|
||||||
if (modelId) nextModels.push({ id: modelId, name: modelId });
|
for (const candidateModelId of [modelId, ...fallbackModelIds]) {
|
||||||
|
if (candidateModelId && !nextModels.some((entry) => entry.id === candidateModelId)) {
|
||||||
|
nextModels.push({ id: candidateModelId, name: candidateModelId });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const nextProvider: Record<string, unknown> = {
|
const nextProvider: Record<string, unknown> = {
|
||||||
baseUrl: override.baseUrl,
|
baseUrl: override.baseUrl,
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ export interface ProviderConfig {
|
|||||||
type: ProviderType;
|
type: ProviderType;
|
||||||
baseUrl?: string;
|
baseUrl?: string;
|
||||||
model?: string;
|
model?: string;
|
||||||
|
fallbackModels?: string[];
|
||||||
|
fallbackProviderIds?: string[];
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
|
|||||||
@@ -36,6 +36,26 @@ import { cn } from '@/lib/utils';
|
|||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { useTranslation } from 'react-i18next';
|
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() {
|
export function ProvidersSettings() {
|
||||||
const { t } = useTranslation('settings');
|
const { t } = useTranslation('settings');
|
||||||
const {
|
const {
|
||||||
@@ -144,6 +164,7 @@ export function ProvidersSettings() {
|
|||||||
<ProviderCard
|
<ProviderCard
|
||||||
key={provider.id}
|
key={provider.id}
|
||||||
provider={provider}
|
provider={provider}
|
||||||
|
allProviders={providers}
|
||||||
isDefault={provider.id === defaultProviderId}
|
isDefault={provider.id === defaultProviderId}
|
||||||
isEditing={editingProvider === provider.id}
|
isEditing={editingProvider === provider.id}
|
||||||
onEdit={() => setEditingProvider(provider.id)}
|
onEdit={() => setEditingProvider(provider.id)}
|
||||||
@@ -179,6 +200,7 @@ export function ProvidersSettings() {
|
|||||||
|
|
||||||
interface ProviderCardProps {
|
interface ProviderCardProps {
|
||||||
provider: ProviderWithKeyInfo;
|
provider: ProviderWithKeyInfo;
|
||||||
|
allProviders: ProviderWithKeyInfo[];
|
||||||
isDefault: boolean;
|
isDefault: boolean;
|
||||||
isEditing: boolean;
|
isEditing: boolean;
|
||||||
onEdit: () => void;
|
onEdit: () => void;
|
||||||
@@ -196,6 +218,7 @@ interface ProviderCardProps {
|
|||||||
|
|
||||||
function ProviderCard({
|
function ProviderCard({
|
||||||
provider,
|
provider,
|
||||||
|
allProviders,
|
||||||
isDefault,
|
isDefault,
|
||||||
isEditing,
|
isEditing,
|
||||||
onEdit,
|
onEdit,
|
||||||
@@ -209,12 +232,18 @@ function ProviderCard({
|
|||||||
const [newKey, setNewKey] = useState('');
|
const [newKey, setNewKey] = useState('');
|
||||||
const [baseUrl, setBaseUrl] = useState(provider.baseUrl || '');
|
const [baseUrl, setBaseUrl] = useState(provider.baseUrl || '');
|
||||||
const [modelId, setModelId] = useState(provider.model || '');
|
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 [showKey, setShowKey] = useState(false);
|
||||||
const [validating, setValidating] = useState(false);
|
const [validating, setValidating] = useState(false);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
||||||
const typeInfo = PROVIDER_TYPE_INFO.find((t) => t.id === provider.type);
|
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(() => {
|
useEffect(() => {
|
||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
@@ -222,13 +251,26 @@ function ProviderCard({
|
|||||||
setShowKey(false);
|
setShowKey(false);
|
||||||
setBaseUrl(provider.baseUrl || '');
|
setBaseUrl(provider.baseUrl || '');
|
||||||
setModelId(provider.model || '');
|
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 () => {
|
const handleSaveEdits = async () => {
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
try {
|
try {
|
||||||
const payload: { newApiKey?: string; updates?: Partial<ProviderConfig> } = {};
|
const payload: { newApiKey?: string; updates?: Partial<ProviderConfig> } = {};
|
||||||
|
const normalizedFallbackModels = normalizeFallbackModels(fallbackModelsText.split('\n'));
|
||||||
|
|
||||||
if (newKey.trim()) {
|
if (newKey.trim()) {
|
||||||
setValidating(true);
|
setValidating(true);
|
||||||
@@ -244,7 +286,7 @@ function ProviderCard({
|
|||||||
payload.newApiKey = newKey.trim();
|
payload.newApiKey = newKey.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (canEditConfig) {
|
{
|
||||||
if (typeInfo?.showModelId && !modelId.trim()) {
|
if (typeInfo?.showModelId && !modelId.trim()) {
|
||||||
toast.error(t('aiProviders.toast.modelRequired'));
|
toast.error(t('aiProviders.toast.modelRequired'));
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
@@ -252,12 +294,18 @@ function ProviderCard({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const updates: Partial<ProviderConfig> = {};
|
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;
|
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;
|
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) {
|
if (Object.keys(updates).length > 0) {
|
||||||
payload.updates = updates;
|
payload.updates = updates;
|
||||||
}
|
}
|
||||||
@@ -308,9 +356,10 @@ function ProviderCard({
|
|||||||
|
|
||||||
{/* Key row */}
|
{/* Key row */}
|
||||||
{isEditing ? (
|
{isEditing ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-4">
|
||||||
{canEditConfig && (
|
{canEditModelConfig && (
|
||||||
<>
|
<div className="space-y-3 rounded-md border p-3">
|
||||||
|
<p className="text-sm font-medium">{t('aiProviders.sections.model')}</p>
|
||||||
{typeInfo?.showBaseUrl && (
|
{typeInfo?.showBaseUrl && (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-xs">{t('aiProviders.dialog.baseUrl')}</Label>
|
<Label className="text-xs">{t('aiProviders.dialog.baseUrl')}</Label>
|
||||||
@@ -333,87 +382,158 @@ function ProviderCard({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
||||||
)}
|
)}
|
||||||
<div className="flex gap-2">
|
<div className="space-y-3 rounded-md border p-3">
|
||||||
<div className="relative flex-1">
|
<p className="text-sm font-medium">{t('aiProviders.sections.fallback')}</p>
|
||||||
<Input
|
<div className="space-y-1">
|
||||||
type={showKey ? 'text' : 'password'}
|
<Label className="text-xs">{t('aiProviders.dialog.fallbackModelIds')}</Label>
|
||||||
placeholder={typeInfo?.requiresApiKey ? typeInfo?.placeholder : (typeInfo?.id === 'ollama' ? t('aiProviders.notRequired') : t('aiProviders.card.editKey'))}
|
<textarea
|
||||||
value={newKey}
|
value={fallbackModelsText}
|
||||||
onChange={(e) => setNewKey(e.target.value)}
|
onChange={(e) => setFallbackModelsText(e.target.value)}
|
||||||
className="pr-10 h-9 text-sm"
|
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
|
<p className="text-xs text-muted-foreground">
|
||||||
type="button"
|
{t('aiProviders.dialog.fallbackModelIdsHelp')}
|
||||||
onClick={() => setShowKey(!showKey)}
|
</p>
|
||||||
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>
|
</div>
|
||||||
<Button
|
<div className="space-y-2">
|
||||||
variant="outline"
|
<Label className="text-xs">{t('aiProviders.dialog.fallbackProviders')}</Label>
|
||||||
size="sm"
|
{fallbackOptions.length === 0 ? (
|
||||||
onClick={handleSaveEdits}
|
<p className="text-xs text-muted-foreground">{t('aiProviders.dialog.noFallbackOptions')}</p>
|
||||||
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" />
|
|
||||||
) : (
|
) : (
|
||||||
<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>
|
</div>
|
||||||
<Button variant="ghost" size="sm" onClick={onCancelEdit}>
|
</div>
|
||||||
<X className="h-3.5 w-3.5" />
|
<div className="space-y-3 rounded-md border p-3">
|
||||||
</Button>
|
<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>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center justify-between rounded-md bg-muted/50 px-3 py-2">
|
<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">
|
<div className="min-w-0 space-y-1">
|
||||||
{typeInfo?.isOAuth ? (
|
<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" />
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<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>
|
<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>
|
||||||
<div className="flex gap-0.5 shrink-0 ml-2">
|
<div className="flex gap-0.5 shrink-0 ml-2">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -13,6 +13,10 @@
|
|||||||
"aiProviders": {
|
"aiProviders": {
|
||||||
"title": "AI Providers",
|
"title": "AI Providers",
|
||||||
"description": "Configure your AI model providers and API keys",
|
"description": "Configure your AI model providers and API keys",
|
||||||
|
"sections": {
|
||||||
|
"model": "Model Settings",
|
||||||
|
"fallback": "Fallback Settings"
|
||||||
|
},
|
||||||
"add": "Add Provider",
|
"add": "Add Provider",
|
||||||
"custom": "Custom",
|
"custom": "Custom",
|
||||||
"notRequired": "Not required",
|
"notRequired": "Not required",
|
||||||
@@ -26,9 +30,19 @@
|
|||||||
"desc": "Configure a new AI model provider",
|
"desc": "Configure a new AI model provider",
|
||||||
"displayName": "Display Name",
|
"displayName": "Display Name",
|
||||||
"apiKey": "API Key",
|
"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.",
|
"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",
|
"baseUrl": "Base URL",
|
||||||
"modelId": "Model ID",
|
"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",
|
"cancel": "Cancel",
|
||||||
"change": "Change provider",
|
"change": "Change provider",
|
||||||
"add": "Add Provider",
|
"add": "Add Provider",
|
||||||
@@ -39,6 +53,9 @@
|
|||||||
"default": "Default",
|
"default": "Default",
|
||||||
"configured": "Configured",
|
"configured": "Configured",
|
||||||
"noKey": "No API key set",
|
"noKey": "No API key set",
|
||||||
|
"none": "None",
|
||||||
|
"fallbacks_one": "Fallback: {{names}}",
|
||||||
|
"fallbacks_other": "Fallbacks ({{count}}): {{names}}",
|
||||||
"setDefault": "Set as default",
|
"setDefault": "Set as default",
|
||||||
"editKey": "Edit API key",
|
"editKey": "Edit API key",
|
||||||
"delete": "Delete provider"
|
"delete": "Delete provider"
|
||||||
|
|||||||
@@ -13,6 +13,10 @@
|
|||||||
"aiProviders": {
|
"aiProviders": {
|
||||||
"title": "AI プロバイダー",
|
"title": "AI プロバイダー",
|
||||||
"description": "AI モデルプロバイダーと API キーを設定",
|
"description": "AI モデルプロバイダーと API キーを設定",
|
||||||
|
"sections": {
|
||||||
|
"model": "モデル設定",
|
||||||
|
"fallback": "フォールバック設定"
|
||||||
|
},
|
||||||
"add": "プロバイダーを追加",
|
"add": "プロバイダーを追加",
|
||||||
"custom": "カスタム",
|
"custom": "カスタム",
|
||||||
"notRequired": "不要",
|
"notRequired": "不要",
|
||||||
@@ -26,9 +30,19 @@
|
|||||||
"desc": "新しい AI モデルプロバイダーを構成",
|
"desc": "新しい AI モデルプロバイダーを構成",
|
||||||
"displayName": "表示名",
|
"displayName": "表示名",
|
||||||
"apiKey": "API キー",
|
"apiKey": "API キー",
|
||||||
|
"apiKeyConfigured": "このプロバイダーには API キーが保存されています。",
|
||||||
|
"apiKeyMissing": "このプロバイダーにはまだ API キーが保存されていません。",
|
||||||
"apiKeyStored": "API キーはローカルマシンに保存されます。",
|
"apiKeyStored": "API キーはローカルマシンに保存されます。",
|
||||||
|
"replaceApiKey": "API キーを置き換える",
|
||||||
|
"replaceApiKeyHelp": "現在保存されている API キーをそのまま使う場合は、この欄を空のままにしてください。",
|
||||||
"baseUrl": "ベース URL",
|
"baseUrl": "ベース URL",
|
||||||
"modelId": "モデル ID",
|
"modelId": "モデル ID",
|
||||||
|
"fallbackModels": "フォールバックモデル",
|
||||||
|
"fallbackProviders": "別プロバイダーへのフォールバック",
|
||||||
|
"fallbackModelIds": "同一プロバイダーのフォールバックモデル ID",
|
||||||
|
"fallbackModelIdsPlaceholder": "gpt-4.1-mini\nanother-model-id",
|
||||||
|
"fallbackModelIdsHelp": "1 行につき 1 つのモデル ID を指定します。まず現在のプロバイダー内でこれらを試し、その後ほかのプロバイダーへフォールバックします。",
|
||||||
|
"noFallbackOptions": "フォールバック先にするには、先に別のプロバイダーを追加してください。",
|
||||||
"cancel": "キャンセル",
|
"cancel": "キャンセル",
|
||||||
"change": "プロバイダーを変更",
|
"change": "プロバイダーを変更",
|
||||||
"add": "プロバイダーを追加",
|
"add": "プロバイダーを追加",
|
||||||
@@ -39,6 +53,9 @@
|
|||||||
"default": "デフォルト",
|
"default": "デフォルト",
|
||||||
"configured": "構成済み",
|
"configured": "構成済み",
|
||||||
"noKey": "API キー未設定",
|
"noKey": "API キー未設定",
|
||||||
|
"none": "なし",
|
||||||
|
"fallbacks_one": "フォールバック: {{names}}",
|
||||||
|
"fallbacks_other": "フォールバック ({{count}}): {{names}}",
|
||||||
"setDefault": "デフォルトに設定",
|
"setDefault": "デフォルトに設定",
|
||||||
"editKey": "API キーを編集",
|
"editKey": "API キーを編集",
|
||||||
"delete": "プロバイダーを削除"
|
"delete": "プロバイダーを削除"
|
||||||
@@ -53,7 +70,8 @@
|
|||||||
"updated": "プロバイダーが更新されました",
|
"updated": "プロバイダーが更新されました",
|
||||||
"failedUpdate": "プロバイダーの更新に失敗しました",
|
"failedUpdate": "プロバイダーの更新に失敗しました",
|
||||||
"invalidKey": "無効な API キー",
|
"invalidKey": "無効な API キー",
|
||||||
"modelRequired": "モデル ID が必要です"
|
"modelRequired": "モデル ID が必要です",
|
||||||
|
"minimaxConflict": "MiniMax (Global) と MiniMax (CN) は同時に追加できません。"
|
||||||
},
|
},
|
||||||
"oauth": {
|
"oauth": {
|
||||||
"loginMode": "OAuthログイン",
|
"loginMode": "OAuthログイン",
|
||||||
|
|||||||
@@ -13,6 +13,10 @@
|
|||||||
"aiProviders": {
|
"aiProviders": {
|
||||||
"title": "AI 模型提供商",
|
"title": "AI 模型提供商",
|
||||||
"description": "配置 AI 模型提供商和 API 密钥",
|
"description": "配置 AI 模型提供商和 API 密钥",
|
||||||
|
"sections": {
|
||||||
|
"model": "模型配置",
|
||||||
|
"fallback": "回退配置"
|
||||||
|
},
|
||||||
"add": "添加提供商",
|
"add": "添加提供商",
|
||||||
"custom": "自定义",
|
"custom": "自定义",
|
||||||
"notRequired": "非必填",
|
"notRequired": "非必填",
|
||||||
@@ -26,9 +30,19 @@
|
|||||||
"desc": "配置新的 AI 模型提供商",
|
"desc": "配置新的 AI 模型提供商",
|
||||||
"displayName": "显示名称",
|
"displayName": "显示名称",
|
||||||
"apiKey": "API 密钥",
|
"apiKey": "API 密钥",
|
||||||
|
"apiKeyConfigured": "这个 provider 已经保存了 API key。",
|
||||||
|
"apiKeyMissing": "这个 provider 还没有保存 API key。",
|
||||||
"apiKeyStored": "您的 API 密钥存储在本地机器上。",
|
"apiKeyStored": "您的 API 密钥存储在本地机器上。",
|
||||||
|
"replaceApiKey": "替换 API Key",
|
||||||
|
"replaceApiKeyHelp": "如果想保留当前已保存的 API key,这里留空即可。",
|
||||||
"baseUrl": "基础 URL",
|
"baseUrl": "基础 URL",
|
||||||
"modelId": "模型 ID",
|
"modelId": "模型 ID",
|
||||||
|
"fallbackModels": "回退模型",
|
||||||
|
"fallbackProviders": "跨 Provider 回退",
|
||||||
|
"fallbackModelIds": "同 Provider 回退模型 ID",
|
||||||
|
"fallbackModelIdsPlaceholder": "gpt-4.1-mini\nanother-model-id",
|
||||||
|
"fallbackModelIdsHelp": "每行一个模型 ID。会先使用当前 provider 的这些模型,再回退到其他 provider。",
|
||||||
|
"noFallbackOptions": "请先添加其他 provider,才能把它设为回退目标。",
|
||||||
"cancel": "取消",
|
"cancel": "取消",
|
||||||
"change": "更换提供商",
|
"change": "更换提供商",
|
||||||
"add": "添加提供商",
|
"add": "添加提供商",
|
||||||
@@ -39,6 +53,9 @@
|
|||||||
"default": "默认",
|
"default": "默认",
|
||||||
"configured": "已配置",
|
"configured": "已配置",
|
||||||
"noKey": "未设置 API 密钥",
|
"noKey": "未设置 API 密钥",
|
||||||
|
"none": "无",
|
||||||
|
"fallbacks_one": "回退:{{names}}",
|
||||||
|
"fallbacks_other": "回退({{count}} 个):{{names}}",
|
||||||
"setDefault": "设为默认",
|
"setDefault": "设为默认",
|
||||||
"editKey": "编辑 API 密钥",
|
"editKey": "编辑 API 密钥",
|
||||||
"delete": "删除提供商"
|
"delete": "删除提供商"
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ export interface ProviderConfig {
|
|||||||
type: ProviderType;
|
type: ProviderType;
|
||||||
baseUrl?: string;
|
baseUrl?: string;
|
||||||
model?: string;
|
model?: string;
|
||||||
|
fallbackModels?: string[];
|
||||||
|
fallbackProviderIds?: string[];
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user