From db6d5ca4b5d8a76fb9e3ade6cff9a2ab8682fba1 Mon Sep 17 00:00:00 2001 From: Supra4E8C Date: Mon, 15 Dec 2025 23:02:33 +0800 Subject: [PATCH] feat: A timeout failure was provided for the model test of OpenAI compatible providers --- src/i18n/locales/en.json | 51 +- src/i18n/locales/zh-CN.json | 19 +- src/pages/AiProvidersPage.tsx | 2033 ++++++++++++++++++--------------- 3 files changed, 1134 insertions(+), 969 deletions(-) diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 2fd8273..82e7ac9 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -137,15 +137,15 @@ "gemini_item_title": "Gemini Key", "gemini_add_modal_title": "Add Gemini API Key", "gemini_add_modal_key_label": "API Keys:", - "gemini_add_modal_key_placeholder": "Enter Gemini API key", - "gemini_add_modal_key_hint": "Add keys one by one and optionally specify a Base URL.", - "gemini_keys_add_btn": "Add Key", - "gemini_base_url_label": "Base URL (Optional):", - "gemini_base_url_placeholder": "e.g.: https://generativelanguage.googleapis.com", - "gemini_edit_modal_title": "Edit Gemini API Key", - "gemini_edit_modal_key_label": "API Key:", - "gemini_delete_confirm": "Are you sure you want to delete this Gemini key?", - "excluded_models_label": "Excluded models (optional):", + "gemini_add_modal_key_placeholder": "Enter Gemini API key", + "gemini_add_modal_key_hint": "Add keys one by one and optionally specify a Base URL.", + "gemini_keys_add_btn": "Add Key", + "gemini_base_url_label": "Base URL (Optional):", + "gemini_base_url_placeholder": "e.g.: https://generativelanguage.googleapis.com", + "gemini_edit_modal_title": "Edit Gemini API Key", + "gemini_edit_modal_key_label": "API Key:", + "gemini_delete_confirm": "Are you sure you want to delete this Gemini key?", + "excluded_models_label": "Excluded models (optional):", "excluded_models_placeholder": "Comma or newline separated, e.g. gemini-1.5-pro, gemini-1.5-flash", "excluded_models_hint": "Leave empty to allow all models; values are trimmed and deduplicated automatically.", "excluded_models_count": "Excluding {{count}} models", @@ -258,6 +258,7 @@ "openai_test_model_placeholder": "Model to test", "openai_test_action": "Run Test", "openai_test_running": "Sending test request...", + "openai_test_timeout": "Test request timed out after {{seconds}} seconds.", "openai_test_success": "Test succeeded. The model responded.", "openai_test_failed": "Test failed", "openai_test_select_placeholder": "Choose from current models", @@ -314,22 +315,22 @@ "type_aistudio": "AIStudio", "type_claude": "Claude", "type_codex": "Codex", - "type_antigravity": "Antigravity", - "type_iflow": "iFlow", - "type_vertex": "Vertex", - "type_empty": "Empty", - "type_unknown": "Other", - "type_virtual": "Virtual auth file", - "models_button": "Models", - "models_title": "Supported models", - "models_loading": "Loading model list...", - "models_empty": "No available models for this credential", - "models_empty_desc": "This credential may not be loaded by the server yet, or no models are bound to it.", - "models_unsupported": "This feature is not supported in the current version", - "models_unsupported_desc": "Please update CLI Proxy API to the latest version and try again", - "models_excluded_badge": "Excluded", - "models_excluded_hint": "This model is excluded by OAuth" - }, + "type_antigravity": "Antigravity", + "type_iflow": "iFlow", + "type_vertex": "Vertex", + "type_empty": "Empty", + "type_unknown": "Other", + "type_virtual": "Virtual auth file", + "models_button": "Models", + "models_title": "Supported models", + "models_loading": "Loading model list...", + "models_empty": "No available models for this credential", + "models_empty_desc": "This credential may not be loaded by the server yet, or no models are bound to it.", + "models_unsupported": "This feature is not supported in the current version", + "models_unsupported_desc": "Please update CLI Proxy API to the latest version and try again", + "models_excluded_badge": "Excluded", + "models_excluded_hint": "This model is excluded by OAuth" + }, "vertex_import": { "title": "Vertex AI Credential Import", "description": "Upload a Google service account JSON to store it as auth-dir/vertex-.json using the same rules as the CLI vertex-import helper.", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index aab9457..cf79d57 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -137,15 +137,15 @@ "gemini_item_title": "Gemini密钥", "gemini_add_modal_title": "添加Gemini API密钥", "gemini_add_modal_key_label": "API密钥", - "gemini_add_modal_key_placeholder": "输入 Gemini API 密钥", - "gemini_add_modal_key_hint": "逐条输入密钥,可同时指定可选 Base URL。", - "gemini_keys_add_btn": "添加密钥", - "gemini_base_url_label": "Base URL (可选)", - "gemini_base_url_placeholder": "例如: https://generativelanguage.googleapis.com", - "gemini_edit_modal_title": "编辑Gemini API密钥", - "gemini_edit_modal_key_label": "API密钥:", - "gemini_delete_confirm": "确定要删除这个Gemini密钥吗?", - "excluded_models_label": "排除的模型 (可选):", + "gemini_add_modal_key_placeholder": "输入 Gemini API 密钥", + "gemini_add_modal_key_hint": "逐条输入密钥,可同时指定可选 Base URL。", + "gemini_keys_add_btn": "添加密钥", + "gemini_base_url_label": "Base URL (可选)", + "gemini_base_url_placeholder": "例如: https://generativelanguage.googleapis.com", + "gemini_edit_modal_title": "编辑Gemini API密钥", + "gemini_edit_modal_key_label": "API密钥:", + "gemini_delete_confirm": "确定要删除这个Gemini密钥吗?", + "excluded_models_label": "排除的模型 (可选):", "excluded_models_placeholder": "用逗号或换行分隔,例如: gemini-1.5-pro, gemini-1.5-flash", "excluded_models_hint": "留空表示不过滤;保存时会自动去重并忽略空白。", "excluded_models_count": "排除 {{count}} 个模型", @@ -258,6 +258,7 @@ "openai_test_model_placeholder": "选择或输入要测试的模型", "openai_test_action": "发送测试", "openai_test_running": "正在发送测试请求...", + "openai_test_timeout": "测试请求超时({{seconds}}秒)。", "openai_test_success": "测试成功,模型可用。", "openai_test_failed": "测试失败", "openai_test_select_placeholder": "从当前模型列表选择", diff --git a/src/pages/AiProvidersPage.tsx b/src/pages/AiProvidersPage.tsx index 4f464f7..b35055c 100644 --- a/src/pages/AiProvidersPage.tsx +++ b/src/pages/AiProvidersPage.tsx @@ -17,7 +17,7 @@ import type { OpenAIProviderConfig, ApiKeyEntry, AmpcodeConfig, - AmpcodeModelMapping + AmpcodeModelMapping, } from '@/types'; import type { KeyStats, KeyStatBucket } from '@/utils/usage'; import type { ModelInfo } from '@/utils/models'; @@ -57,7 +57,8 @@ interface AmpcodeFormState { const DISABLE_ALL_MODELS_RULE = '*'; const hasDisableAllModelsRule = (models?: string[]) => - Array.isArray(models) && models.some((model) => String(model ?? '').trim() === DISABLE_ALL_MODELS_RULE); + Array.isArray(models) && + models.some((model) => String(model ?? '').trim() === DISABLE_ALL_MODELS_RULE); const stripDisableAllModelsRule = (models?: string[]) => Array.isArray(models) @@ -80,16 +81,21 @@ const parseExcludedModels = (text: string): string[] => .map((item) => item.trim()) .filter(Boolean); -const excludedModelsToText = (models?: string[]) => (Array.isArray(models) ? models.join('\n') : ''); +const excludedModelsToText = (models?: string[]) => + Array.isArray(models) ? models.join('\n') : ''; const buildOpenAIModelsEndpoint = (baseUrl: string): string => { - const trimmed = String(baseUrl || '').trim().replace(/\/+$/g, ''); + const trimmed = String(baseUrl || '') + .trim() + .replace(/\/+$/g, ''); if (!trimmed) return ''; return trimmed.endsWith('/v1') ? `${trimmed}/models` : `${trimmed}/v1/models`; }; const buildOpenAIChatCompletionsEndpoint = (baseUrl: string): string => { - const trimmed = String(baseUrl || '').trim().replace(/\/+$/g, ''); + const trimmed = String(baseUrl || '') + .trim() + .replace(/\/+$/g, ''); if (!trimmed) return ''; if (trimmed.endsWith('/chat/completions')) { return trimmed; @@ -97,6 +103,8 @@ const buildOpenAIChatCompletionsEndpoint = (baseUrl: string): string => { return trimmed.endsWith('/v1') ? `${trimmed}/chat/completions` : `${trimmed}/v1/chat/completions`; }; +const OPENAI_TEST_TIMEOUT_MS = 30_000; + // 根据 source (apiKey) 获取统计数据 - 与旧版逻辑一致 const getStatsBySource = ( apiKey: string, @@ -133,7 +141,7 @@ const getOpenAIProviderStats = ( const buildApiKeyEntry = (input?: Partial): ApiKeyEntry => ({ apiKey: input?.apiKey ?? '', proxyUrl: input?.proxyUrl ?? '', - headers: input?.headers ?? {} + headers: input?.headers ?? {}, }); const ampcodeMappingsToEntries = (mappings?: AmpcodeModelMapping[]): ModelEntry[] => { @@ -142,7 +150,7 @@ const ampcodeMappingsToEntries = (mappings?: AmpcodeModelMapping[]): ModelEntry[ } return mappings.map((mapping) => ({ name: mapping.from ?? '', - alias: mapping.to ?? '' + alias: mapping.to ?? '', })); }; @@ -168,7 +176,7 @@ const buildAmpcodeFormState = (ampcode?: AmpcodeConfig | null): AmpcodeFormState upstreamApiKey: '', restrictManagementToLocalhost: ampcode?.restrictManagementToLocalhost ?? true, forceModelMappings: ampcode?.forceModelMappings ?? false, - mappingEntries: ampcodeMappingsToEntries(ampcode?.modelMappings) + mappingEntries: ampcodeMappingsToEntries(ampcode?.modelMappings), }); export function AiProvidersPage() { @@ -197,9 +205,11 @@ export function AiProvidersPage() { baseUrl: '', headers: {}, excludedModels: [], - excludedText: '' + excludedText: '', }); - const [providerForm, setProviderForm] = useState({ + const [providerForm, setProviderForm] = useState< + ProviderKeyConfig & { modelEntries: ModelEntry[]; excludedText: string } + >({ apiKey: '', baseUrl: '', proxyUrl: '', @@ -207,16 +217,18 @@ export function AiProvidersPage() { models: [], excludedModels: [], modelEntries: [{ name: '', alias: '' }], - excludedText: '' + excludedText: '', }); const [openaiForm, setOpenaiForm] = useState({ name: '', baseUrl: '', headers: [], apiKeyEntries: [buildApiKeyEntry()], - modelEntries: [{ name: '', alias: '' }] + modelEntries: [{ name: '', alias: '' }], }); - const [ampcodeForm, setAmpcodeForm] = useState(() => buildAmpcodeFormState(null)); + const [ampcodeForm, setAmpcodeForm] = useState(() => + buildAmpcodeFormState(null) + ); const [ampcodeModalLoading, setAmpcodeModalLoading] = useState(false); const [ampcodeLoaded, setAmpcodeLoaded] = useState(false); const [ampcodeMappingsDirty, setAmpcodeMappingsDirty] = useState(false); @@ -230,7 +242,9 @@ export function AiProvidersPage() { const [openaiDiscoverySearch, setOpenaiDiscoverySearch] = useState(''); const [openaiDiscoverySelected, setOpenaiDiscoverySelected] = useState>(new Set()); const [openaiTestModel, setOpenaiTestModel] = useState(''); - const [openaiTestStatus, setOpenaiTestStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle'); + const [openaiTestStatus, setOpenaiTestStatus] = useState< + 'idle' | 'loading' | 'success' | 'error' + >('idle'); const [openaiTestMessage, setOpenaiTestMessage] = useState(''); const [saving, setSaving] = useState(false); const [configSwitchingKey, setConfigSwitchingKey] = useState(null); @@ -247,10 +261,7 @@ export function AiProvidersPage() { }); }, [openaiDiscoveryModels, openaiDiscoverySearch]); const openaiAvailableModels = useMemo( - () => - openaiForm.modelEntries - .map((entry) => entry.name.trim()) - .filter(Boolean), + () => openaiForm.modelEntries.map((entry) => entry.name.trim()).filter(Boolean), [openaiForm.modelEntries] ); @@ -297,7 +308,12 @@ export function AiProvidersPage() { if (config?.codexApiKeys) setCodexConfigs(config.codexApiKeys); if (config?.claudeApiKeys) setClaudeConfigs(config.claudeApiKeys); if (config?.openaiCompatibility) setOpenaiProviders(config.openaiCompatibility); - }, [config?.geminiApiKeys, config?.codexApiKeys, config?.claudeApiKeys, config?.openaiCompatibility]); + }, [ + config?.geminiApiKeys, + config?.codexApiKeys, + config?.claudeApiKeys, + config?.openaiCompatibility, + ]); const closeModal = () => { setModal(null); @@ -306,7 +322,7 @@ export function AiProvidersPage() { baseUrl: '', headers: {}, excludedModels: [], - excludedText: '' + excludedText: '', }); setProviderForm({ apiKey: '', @@ -316,7 +332,7 @@ export function AiProvidersPage() { models: [], excludedModels: [], modelEntries: [{ name: '', alias: '' }], - excludedText: '' + excludedText: '', }); setOpenaiForm({ name: '', @@ -324,7 +340,7 @@ export function AiProvidersPage() { headers: [], apiKeyEntries: [buildApiKeyEntry()], modelEntries: [{ name: '', alias: '' }], - testModel: undefined + testModel: undefined, }); setAmpcodeForm(buildAmpcodeFormState(null)); setAmpcodeModalLoading(false); @@ -348,7 +364,7 @@ export function AiProvidersPage() { const entry = geminiKeys[index]; setGeminiForm({ ...entry, - excludedText: excludedModelsToText(entry?.excludedModels) + excludedText: excludedModelsToText(entry?.excludedModels), }); } setModal({ type: 'gemini', index }); @@ -361,7 +377,7 @@ export function AiProvidersPage() { setProviderForm({ ...entry, modelEntries: modelsToEntries(entry?.models), - excludedText: excludedModelsToText(entry?.excludedModels) + excludedText: excludedModelsToText(entry?.excludedModels), }); } setModal({ type, index }); @@ -400,11 +416,13 @@ export function AiProvidersPage() { headers: headersToEntries(entry.headers), testModel: entry.testModel, modelEntries, - apiKeyEntries: entry.apiKeyEntries?.length ? entry.apiKeyEntries : [buildApiKeyEntry()] + apiKeyEntries: entry.apiKeyEntries?.length ? entry.apiKeyEntries : [buildApiKeyEntry()], }); const available = modelEntries.map((m) => m.name.trim()).filter(Boolean); const initialModel = - entry.testModel && available.includes(entry.testModel) ? entry.testModel : available[0] || ''; + entry.testModel && available.includes(entry.testModel) + ? entry.testModel + : available[0] || ''; setOpenaiTestModel(initialModel); } else { setOpenaiTestModel(''); @@ -422,7 +440,9 @@ export function AiProvidersPage() { setOpenaiDiscoveryError(''); }; - const fetchOpenaiModelDiscovery = async ({ allowFallback = true }: { allowFallback?: boolean } = {}) => { + const fetchOpenaiModelDiscovery = async ({ + allowFallback = true, + }: { allowFallback?: boolean } = {}) => { const baseUrl = openaiForm.baseUrl.trim(); if (!baseUrl) return; @@ -430,9 +450,15 @@ export function AiProvidersPage() { setOpenaiDiscoveryError(''); try { const headers = buildHeaderObject(openaiForm.headers); - const firstKey = openaiForm.apiKeyEntries.find((entry) => entry.apiKey?.trim())?.apiKey?.trim(); + const firstKey = openaiForm.apiKeyEntries + .find((entry) => entry.apiKey?.trim()) + ?.apiKey?.trim(); const hasAuthHeader = Boolean(headers.Authorization || headers['authorization']); - const list = await modelsApi.fetchModels(baseUrl, hasAuthHeader ? undefined : firstKey, headers); + const list = await modelsApi.fetchModels( + baseUrl, + hasAuthHeader ? undefined : firstKey, + headers + ); setOpenaiDiscoveryModels(list); } catch (err: any) { if (allowFallback) { @@ -447,7 +473,9 @@ export function AiProvidersPage() { } } else { setOpenaiDiscoveryModels([]); - setOpenaiDiscoveryError(`${t('ai_providers.openai_models_fetch_error')}: ${err?.message || ''}`); + setOpenaiDiscoveryError( + `${t('ai_providers.openai_models_fetch_error')}: ${err?.message || ''}` + ); } } finally { setOpenaiDiscoveryLoading(false); @@ -483,7 +511,9 @@ export function AiProvidersPage() { }; const applyOpenaiModelDiscoverySelection = () => { - const selectedModels = openaiDiscoveryModels.filter((model) => openaiDiscoverySelected.has(model.name)); + const selectedModels = openaiDiscoveryModels.filter((model) => + openaiDiscoverySelected.has(model.name) + ); if (!selectedModels.length) { closeOpenaiModelDiscovery(); return; @@ -507,12 +537,15 @@ export function AiProvidersPage() { const mergedEntries = Array.from(mergedMap.values()); setOpenaiForm((prev) => ({ ...prev, - modelEntries: mergedEntries.length ? mergedEntries : [{ name: '', alias: '' }] + modelEntries: mergedEntries.length ? mergedEntries : [{ name: '', alias: '' }], })); closeOpenaiModelDiscovery(); if (addedCount > 0) { - showNotification(t('ai_providers.openai_models_fetch_added', { count: addedCount }), 'success'); + showNotification( + t('ai_providers.openai_models_fetch_added', { count: addedCount }), + 'success' + ); } }; @@ -574,7 +607,7 @@ export function AiProvidersPage() { const customHeaders = buildHeaderObject(openaiForm.headers); const headers: Record = { 'Content-Type': 'application/json', - ...customHeaders + ...customHeaders, }; if (!headers.Authorization && !headers['authorization']) { headers.Authorization = `Bearer ${firstKeyEntry.apiKey.trim()}`; @@ -582,16 +615,20 @@ export function AiProvidersPage() { setOpenaiTestStatus('loading'); setOpenaiTestMessage(t('ai_providers.openai_test_running')); + + const controller = new AbortController(); + const timeoutId = window.setTimeout(() => controller.abort(), OPENAI_TEST_TIMEOUT_MS); try { const response = await fetch(endpoint, { method: 'POST', headers, + signal: controller.signal, body: JSON.stringify({ model: modelName, messages: [{ role: 'user', content: 'Hi' }], stream: false, - max_tokens: 5 - }) + max_tokens: 5, + }), }); const rawText = await response.text(); @@ -612,7 +649,15 @@ export function AiProvidersPage() { setOpenaiTestMessage(t('ai_providers.openai_test_success')); } catch (err: any) { setOpenaiTestStatus('error'); - setOpenaiTestMessage(`${t('ai_providers.openai_test_failed')}: ${err?.message || ''}`); + if (err?.name === 'AbortError') { + setOpenaiTestMessage( + t('ai_providers.openai_test_timeout', { seconds: OPENAI_TEST_TIMEOUT_MS / 1000 }) + ); + } else { + setOpenaiTestMessage(`${t('ai_providers.openai_test_failed')}: ${err?.message || ''}`); + } + } finally { + window.clearTimeout(timeoutId); } }; @@ -656,7 +701,9 @@ export function AiProvidersPage() { await ampcodeApi.clearUpstreamUrl(); } - await ampcodeApi.updateRestrictManagementToLocalhost(ampcodeForm.restrictManagementToLocalhost); + await ampcodeApi.updateRestrictManagementToLocalhost( + ampcodeForm.restrictManagementToLocalhost + ); await ampcodeApi.updateForceModelMappings(ampcodeForm.forceModelMappings); if (ampcodeLoaded || ampcodeMappingsDirty) { @@ -676,7 +723,7 @@ export function AiProvidersPage() { ...previous, upstreamUrl: upstreamUrl || undefined, restrictManagementToLocalhost: ampcodeForm.restrictManagementToLocalhost, - forceModelMappings: ampcodeForm.forceModelMappings + forceModelMappings: ampcodeForm.forceModelMappings, }; if (overrideKey) { @@ -711,7 +758,7 @@ export function AiProvidersPage() { apiKey: geminiForm.apiKey.trim(), baseUrl: geminiForm.baseUrl?.trim() || undefined, headers: buildHeaderObject(headersToEntries(geminiForm.headers as any)), - excludedModels: parseExcludedModels(geminiForm.excludedText) + excludedModels: parseExcludedModels(geminiForm.excludedText), }; const nextList = modal?.type === 'gemini' && modal.index !== null @@ -723,7 +770,9 @@ export function AiProvidersPage() { updateConfigValue('gemini-api-key', nextList); clearCache('gemini-api-key'); const message = - modal?.index !== null ? t('notification.gemini_key_updated') : t('notification.gemini_key_added'); + modal?.index !== null + ? t('notification.gemini_key_updated') + : t('notification.gemini_key_added'); showNotification(message, 'success'); closeModal(); } catch (err: any) { @@ -772,7 +821,10 @@ export function AiProvidersPage() { try { await providersApi.saveGeminiKeys(nextList); - showNotification(enabled ? t('notification.config_enabled') : t('notification.config_disabled'), 'success'); + showNotification( + enabled ? t('notification.config_enabled') : t('notification.config_disabled'), + 'success' + ); } catch (err: any) { setGeminiKeys(previousList); updateConfigValue('gemini-api-key', previousList); @@ -814,7 +866,10 @@ export function AiProvidersPage() { } else { await providersApi.saveClaudeConfigs(nextList); } - showNotification(enabled ? t('notification.config_enabled') : t('notification.config_disabled'), 'success'); + showNotification( + enabled ? t('notification.config_enabled') : t('notification.config_disabled'), + 'success' + ); } catch (err: any) { if (provider === 'codex') { setCodexConfigs(previousList); @@ -848,7 +903,7 @@ export function AiProvidersPage() { proxyUrl: providerForm.proxyUrl?.trim() || undefined, headers: buildHeaderObject(headersToEntries(providerForm.headers as any)), models: entriesToModels(providerForm.modelEntries), - excludedModels: parseExcludedModels(providerForm.excludedText) + excludedModels: parseExcludedModels(providerForm.excludedText), }; const nextList = @@ -862,7 +917,9 @@ export function AiProvidersPage() { updateConfigValue('codex-api-key', nextList); clearCache('codex-api-key'); const message = - modal?.index !== null ? t('notification.codex_config_updated') : t('notification.codex_config_added'); + modal?.index !== null + ? t('notification.codex_config_updated') + : t('notification.codex_config_added'); showNotification(message, 'success'); } else { await providersApi.saveClaudeConfigs(nextList); @@ -870,7 +927,9 @@ export function AiProvidersPage() { updateConfigValue('claude-api-key', nextList); clearCache('claude-api-key'); const message = - modal?.index !== null ? t('notification.claude_config_updated') : t('notification.claude_config_added'); + modal?.index !== null + ? t('notification.claude_config_updated') + : t('notification.claude_config_added'); showNotification(message, 'success'); } @@ -915,8 +974,8 @@ export function AiProvidersPage() { apiKeyEntries: openaiForm.apiKeyEntries.map((entry) => ({ apiKey: entry.apiKey.trim(), proxyUrl: entry.proxyUrl?.trim() || undefined, - headers: entry.headers - })) + headers: entry.headers, + })), }; if (openaiForm.testModel) payload.testModel = openaiForm.testModel.trim(); const models = entriesToModels(openaiForm.modelEntries); @@ -932,7 +991,9 @@ export function AiProvidersPage() { updateConfigValue('openai-compatibility', nextList); clearCache('openai-compatibility'); const message = - modal?.index !== null ? t('notification.openai_provider_updated') : t('notification.openai_provider_added'); + modal?.index !== null + ? t('notification.openai_provider_updated') + : t('notification.openai_provider_added'); showNotification(message, 'success'); closeModal(); } catch (err: any) { @@ -965,7 +1026,10 @@ export function AiProvidersPage() { const removeEntry = (idx: number) => { const next = list.filter((_, i) => i !== idx); - setOpenaiForm((prev) => ({ ...prev, apiKeyEntries: next.length ? next : [buildApiKeyEntry()] })); + setOpenaiForm((prev) => ({ + ...prev, + apiKeyEntries: next.length ? next : [buildApiKeyEntry()], + })); }; const addEntry = () => { @@ -1043,7 +1107,11 @@ export function AiProvidersPage() { {items.map((item, index) => { const rowDisabled = options?.getRowDisabled ? options.getRowDisabled(item, index) : false; return ( -
+
{renderContent(item, index)}
- } - > - {renderList( - geminiKeys, - (item) => item.apiKey, - (item, index) => { - const stats = getStatsBySource(item.apiKey, keyStats, maskApiKey); - const headerEntries = Object.entries(item.headers || {}); - const configDisabled = hasDisableAllModelsRule(item.excludedModels); - const excludedModels = item.excludedModels ?? []; - return ( - -
- {t('ai_providers.gemini_item_title')} #{index + 1} -
- {/* API Key 行 */} -
- {t('common.api_key')}: - {maskApiKey(item.apiKey)} -
- {/* Base URL 行 */} - {item.baseUrl && ( -
- {t('common.base_url')}: - {item.baseUrl} -
- )} - {/* 自定义请求头徽章 */} - {headerEntries.length > 0 && ( -
- {headerEntries.map(([key, value]) => ( - - {key}: {value} - - ))} -
- )} - {configDisabled && ( -
- {t('ai_providers.config_disabled_badge')} -
- )} - {/* 排除模型徽章 */} - {excludedModels.length ? ( -
-
- {t('ai_providers.excluded_models_count', { count: excludedModels.length })} -
-
- {excludedModels.map((model) => ( - - {model} - - ))} -
-
- ) : null} - {/* 成功/失败统计 */} -
- - {t('stats.success')}: {stats.success} - - - {t('stats.failure')}: {stats.failure} - -
-
- ); - }, - (index) => openGeminiModal(index), - (item) => deleteGemini(item.apiKey), - t('ai_providers.gemini_add_button'), - undefined, - { - getRowDisabled: (item) => hasDisableAllModelsRule(item.excludedModels), - renderExtraActions: (item, index) => ( - void setConfigEnabled('gemini', index, value)} - /> - ) - } - )} - - - openProviderModal('codex', null)} - disabled={disableControls || saving || Boolean(configSwitchingKey)} - > - {t('ai_providers.codex_add_button')} - - } - > - {renderList( - codexConfigs, - (item) => item.apiKey, - (item, _index) => { - const stats = getStatsBySource(item.apiKey, keyStats, maskApiKey); - const headerEntries = Object.entries(item.headers || {}); - const configDisabled = hasDisableAllModelsRule(item.excludedModels); - const excludedModels = item.excludedModels ?? []; - return ( - -
{t('ai_providers.codex_item_title')}
- {/* API Key 行 */} -
- {t('common.api_key')}: - {maskApiKey(item.apiKey)} -
- {/* Base URL 行 */} - {item.baseUrl && ( -
- {t('common.base_url')}: - {item.baseUrl} -
- )} - {/* Proxy URL 行 */} - {item.proxyUrl && ( -
- {t('common.proxy_url')}: - {item.proxyUrl} -
- )} - {/* 自定义请求头徽章 */} - {headerEntries.length > 0 && ( -
- {headerEntries.map(([key, value]) => ( - - {key}: {value} - - ))} -
- )} - {configDisabled && ( -
- {t('ai_providers.config_disabled_badge')} -
- )} - {/* 排除模型徽章 */} - {excludedModels.length ? ( -
-
- {t('ai_providers.excluded_models_count', { count: excludedModels.length })} -
-
- {excludedModels.map((model) => ( - - {model} - - ))} -
-
- ) : null} - {/* 成功/失败统计 */} -
- - {t('stats.success')}: {stats.success} - - - {t('stats.failure')}: {stats.failure} - -
-
- ); - }, - (index) => openProviderModal('codex', index), - (item) => deleteProviderEntry('codex', item.apiKey), - t('ai_providers.codex_add_button'), - undefined, - { - getRowDisabled: (item) => hasDisableAllModelsRule(item.excludedModels), - renderExtraActions: (item, index) => ( - void setConfigEnabled('codex', index, value)} - /> - ) - } - )} -
- - openProviderModal('claude', null)} - disabled={disableControls || saving || Boolean(configSwitchingKey)} - > - {t('ai_providers.claude_add_button')} - - } - > - {renderList( - claudeConfigs, - (item) => item.apiKey, - (item, _index) => { - const stats = getStatsBySource(item.apiKey, keyStats, maskApiKey); - const headerEntries = Object.entries(item.headers || {}); - const configDisabled = hasDisableAllModelsRule(item.excludedModels); - const excludedModels = item.excludedModels ?? []; - return ( - -
{t('ai_providers.claude_item_title')}
- {/* API Key 行 */} -
- {t('common.api_key')}: - {maskApiKey(item.apiKey)} -
- {/* Base URL 行 */} - {item.baseUrl && ( -
- {t('common.base_url')}: - {item.baseUrl} -
- )} - {/* Proxy URL 行 */} - {item.proxyUrl && ( -
- {t('common.proxy_url')}: - {item.proxyUrl} -
- )} - {/* 自定义请求头徽章 */} - {headerEntries.length > 0 && ( -
- {headerEntries.map(([key, value]) => ( - - {key}: {value} - - ))} -
- )} - {configDisabled && ( -
- {t('ai_providers.config_disabled_badge')} -
- )} - {/* 模型列表 */} - {item.models?.length ? ( -
- - {t('ai_providers.claude_models_count')}: {item.models.length} - - {item.models.map((model) => ( - - {model.name} - {model.alias && model.alias !== model.name && ( - {model.alias} - )} - - ))} -
- ) : null} - {/* 排除模型徽章 */} - {excludedModels.length ? ( -
-
- {t('ai_providers.excluded_models_count', { count: excludedModels.length })} -
-
- {excludedModels.map((model) => ( - - {model} - - ))} -
-
- ) : null} - {/* 成功/失败统计 */} -
- - {t('stats.success')}: {stats.success} - - - {t('stats.failure')}: {stats.failure} - -
-
- ); - }, - (index) => openProviderModal('claude', index), - (item) => deleteProviderEntry('claude', item.apiKey), - t('ai_providers.claude_add_button'), - undefined, - { - getRowDisabled: (item) => hasDisableAllModelsRule(item.excludedModels), - renderExtraActions: (item, index) => ( - void setConfigEnabled('claude', index, value)} - /> - ) - } - )} -
- - - {t('common.edit')} - - } - > - {loading ? ( -
{t('common.loading')}
- ) : ( - <> -
- {t('ai_providers.ampcode_upstream_url_label')}: - {config?.ampcode?.upstreamUrl || t('common.not_set')} -
-
- {t('ai_providers.ampcode_upstream_api_key_label')}: - - {config?.ampcode?.upstreamApiKey ? maskApiKey(config.ampcode.upstreamApiKey) : t('common.not_set')} - -
-
- {t('ai_providers.ampcode_restrict_management_label')}: - - {(config?.ampcode?.restrictManagementToLocalhost ?? true) ? t('common.yes') : t('common.no')} - -
-
- {t('ai_providers.ampcode_force_model_mappings_label')}: - - {(config?.ampcode?.forceModelMappings ?? false) ? t('common.yes') : t('common.no')} - -
-
- {t('ai_providers.ampcode_model_mappings_count')}: - {config?.ampcode?.modelMappings?.length || 0} -
- {config?.ampcode?.modelMappings?.length ? ( -
- {config.ampcode.modelMappings.slice(0, 5).map((mapping) => ( - - {mapping.from} - {mapping.to} - - ))} - {config.ampcode.modelMappings.length > 5 && ( - - +{config.ampcode.modelMappings.length - 5} - - )} -
- ) : null} - - )} -
- - openOpenaiModal(null)} - disabled={disableControls || saving || Boolean(configSwitchingKey)} - > - {t('ai_providers.openai_add_button')} - - } - > - {renderList( - openaiProviders, - (item) => item.name, - (item, _index) => { - const stats = getOpenAIProviderStats(item.apiKeyEntries, keyStats, maskApiKey); - const headerEntries = Object.entries(item.headers || {}); - const apiKeyEntries = item.apiKeyEntries || []; - return ( - -
{item.name}
- {/* Base URL 行 */} -
- {t('common.base_url')}: - {item.baseUrl} -
- {/* 自定义请求头徽章 */} - {headerEntries.length > 0 && ( -
- {headerEntries.map(([key, value]) => ( - - {key}: {value} - - ))} -
- )} - {/* API密钥条目二级卡片 */} - {apiKeyEntries.length > 0 && ( -
-
- {t('ai_providers.openai_keys_count')}: {apiKeyEntries.length} -
-
- {apiKeyEntries.map((entry, entryIndex) => { - const entryStats = getStatsBySource(entry.apiKey, keyStats, maskApiKey); - return ( -
- {entryIndex + 1} - {maskApiKey(entry.apiKey)} - {entry.proxyUrl && ( - {entry.proxyUrl} - )} -
- - {entryStats.success} - - - {entryStats.failure} - -
-
- ); - })} -
-
- )} - {/* 模型数量标签 */} -
- {t('ai_providers.openai_models_count')}: - {item.models?.length || 0} -
- {/* 模型列表徽章 */} - {item.models?.length ? ( -
- {item.models.map((model) => ( - - {model.name} - {model.alias && model.alias !== model.name && ( - {model.alias} - )} - - ))} -
- ) : null} - {/* 测试模型 */} - {item.testModel && ( -
- Test Model: - {item.testModel} -
- )} - {/* 成功/失败统计(汇总) */} -
- - {t('stats.success')}: {stats.success} - - - {t('stats.failure')}: {stats.failure} - -
-
- ); - }, - (index) => openOpenaiModal(index), - (item) => deleteOpenai(item.name), - t('ai_providers.openai_add_button') - )} -
- - {/* Ampcode Modal */} - - - - - } - > - {ampcodeModalError &&
{ampcodeModalError}
} - setAmpcodeForm((prev) => ({ ...prev, upstreamUrl: e.target.value }))} - disabled={ampcodeModalLoading || ampcodeSaving} - hint={t('ai_providers.ampcode_upstream_url_hint')} - /> - setAmpcodeForm((prev) => ({ ...prev, upstreamApiKey: e.target.value }))} - disabled={ampcodeModalLoading || ampcodeSaving} - hint={t('ai_providers.ampcode_upstream_api_key_hint')} - /> -
-
- {t('ai_providers.ampcode_upstream_api_key_current', { - key: config?.ampcode?.upstreamApiKey ? maskApiKey(config.ampcode.upstreamApiKey) : t('common.not_set') - })} -
- -
- -
- setAmpcodeForm((prev) => ({ ...prev, restrictManagementToLocalhost: value }))} - disabled={ampcodeModalLoading || ampcodeSaving} - /> -
{t('ai_providers.ampcode_restrict_management_hint')}
-
- -
- setAmpcodeForm((prev) => ({ ...prev, forceModelMappings: value }))} - disabled={ampcodeModalLoading || ampcodeSaving} - /> -
{t('ai_providers.ampcode_force_model_mappings_hint')}
-
- -
- - { - setAmpcodeMappingsDirty(true); - setAmpcodeForm((prev) => ({ ...prev, mappingEntries: entries })); - }} - addLabel={t('ai_providers.ampcode_model_mappings_add_btn')} - namePlaceholder={t('ai_providers.ampcode_model_mappings_from_placeholder')} - aliasPlaceholder={t('ai_providers.ampcode_model_mappings_to_placeholder')} - disabled={ampcodeModalLoading || ampcodeSaving} - /> -
{t('ai_providers.ampcode_model_mappings_hint')}
-
-
- - {/* Gemini Modal */} - - - - - } - > - setGeminiForm((prev) => ({ ...prev, apiKey: e.target.value }))} - /> - setGeminiForm((prev) => ({ ...prev, baseUrl: e.target.value }))} - /> - setGeminiForm((prev) => ({ ...prev, headers: buildHeaderObject(entries) }))} - addLabel={t('common.custom_headers_add')} - keyPlaceholder={t('common.custom_headers_key_placeholder')} - valuePlaceholder={t('common.custom_headers_value_placeholder')} - /> -
- -