diff --git a/configs/config.json.example b/configs/config.json.example index 80c7843..406c2bf 100644 --- a/configs/config.json.example +++ b/configs/config.json.example @@ -19,9 +19,31 @@ "claude-kiro-oauth": ["claude-custom"], "claude-custom": ["claude-kiro-oauth"] }, + "modelFallbackMapping": { + "gemini-claude-opus-4-5-thinking": { + "targetProviderType": "claude-kiro-oauth", + "targetModel": "claude-opus-4-5" + }, + "gemini-claude-sonnet-4-5-thinking": { + "targetProviderType": "claude-kiro-oauth", + "targetModel": "claude-sonnet-4-5" + }, + "gemini-claude-sonnet-4-5": { + "targetProviderType": "claude-kiro-oauth", + "targetModel": "claude-sonnet-4-5" + }, + "claude-opus-4-5": { + "targetProviderType": "gemini-antigravity", + "targetModel": "gemini-claude-opus-4-5-thinking" + }, + "claude-sonnet-4-5": { + "targetProviderType": "gemini-antigravity", + "targetModel": "gemini-claude-sonnet-4-5" + } + }, "PROXY_URL": "http://127.0.0.1:1089", "PROXY_ENABLED_PROVIDERS": [ "gemini-cli-oauth", "gemini-antigravity" ] - } \ No newline at end of file + } diff --git a/src/common.js b/src/common.js index da68d58..147a329 100644 --- a/src/common.js +++ b/src/common.js @@ -393,7 +393,7 @@ export async function handleContentGenerationRequest(req, res, service, endpoint } // 2. Extract model and determine if the request is for streaming. - const { model, isStream } = _extractModelAndStreamInfo(req, originalRequestBody, fromProvider); + let { model, isStream } = _extractModelAndStreamInfo(req, originalRequestBody, fromProvider); if (!model) { throw new Error("Could not determine the model from the request."); @@ -410,6 +410,12 @@ export async function handleContentGenerationRequest(req, res, service, endpoint toProvider = result.actualProviderType; actualUuid = result.uuid || pooluuid; + // 如果发生了模型级别的 fallback,需要更新请求使用的模型 + if (result.actualModel && result.actualModel !== model) { + console.log(`[Content Generation] Model Fallback: ${model} -> ${result.actualModel}`); + model = result.actualModel; + } + if (result.isFallback) { console.log(`[Content Generation] Fallback activated: ${CONFIG.MODEL_PROVIDER} -> ${toProvider} (uuid: ${actualUuid})`); } else { @@ -802,4 +808,4 @@ function createStreamErrorResponse(error, fromProvider) { }; return `data: ${JSON.stringify(defaultError)}\n\n`; } -} \ No newline at end of file +} diff --git a/src/provider-pool-manager.js b/src/provider-pool-manager.js index 4de8263..b40bc97 100644 --- a/src/provider-pool-manager.js +++ b/src/provider-pool-manager.js @@ -40,6 +40,9 @@ export class ProviderPoolManager { // Fallback 链配置 this.fallbackChain = options.globalConfig?.providerFallbackChain || {}; + // Model Fallback 映射配置 + this.modelFallbackMapping = options.globalConfig?.modelFallbackMapping || {}; + this.initializeProviderStatus(); } @@ -189,27 +192,19 @@ export class ProviderPoolManager { return null; } + // ========================== + // 优先级 1: Provider Fallback Chain (同协议/兼容协议的回退) + // ========================== + // 记录尝试过的类型,避免循环 const triedTypes = new Set(); const typesToTry = [providerType]; - // 添加 fallback 类型到尝试列表 - const fallbackTypes = this.fallbackChain[providerType]; - if (!fallbackTypes || fallbackTypes.length === 0) { - this._log('info', `No fallback types configured for ${providerType}`); - const selectedConfig = this.selectProvider(providerType, requestedModel, options); - if (selectedConfig) { - return { - config: selectedConfig, - actualProviderType: providerType, - isFallback: false - }; - } - } - + const fallbackTypes = this.fallbackChain[providerType] || []; if (Array.isArray(fallbackTypes)) { typesToTry.push(...fallbackTypes); } + for (const currentType of typesToTry) { // 避免重复尝试 if (triedTypes.has(currentType)) { @@ -219,7 +214,7 @@ export class ProviderPoolManager { // 检查该类型是否有配置的池 if (!this.providerStatus[currentType] || this.providerStatus[currentType].length === 0) { - this._log('info', `No provider pool configured for type: ${currentType}`); + this._log('debug', `No provider pool configured for type: ${currentType}`); continue; } @@ -230,14 +225,14 @@ export class ProviderPoolManager { const fallbackProtocol = getProtocolPrefix(currentType); if (primaryProtocol !== fallbackProtocol) { - this._log('info', `Skipping fallback type ${currentType}: protocol mismatch (${primaryProtocol} vs ${fallbackProtocol})`); + this._log('debug', `Skipping fallback type ${currentType}: protocol mismatch (${primaryProtocol} vs ${fallbackProtocol})`); continue; } // 检查 fallback 类型是否支持请求的模型 const supportedModels = getProviderModels(currentType); if (supportedModels.length > 0 && !supportedModels.includes(requestedModel)) { - this._log('info', `Skipping fallback type ${currentType}: model ${requestedModel} not supported`); + this._log('debug', `Skipping fallback type ${currentType}: model ${requestedModel} not supported`); continue; } } @@ -247,7 +242,7 @@ export class ProviderPoolManager { if (selectedConfig) { if (currentType !== providerType) { - this._log('info', `Fallback activated: ${providerType} -> ${currentType} (uuid: ${selectedConfig.uuid})`); + this._log('info', `Fallback activated (Chain): ${providerType} -> ${currentType} (uuid: ${selectedConfig.uuid})`); } return { config: selectedConfig, @@ -257,7 +252,72 @@ export class ProviderPoolManager { } } - this._log('warn', `None available provider found for ${providerType} or any of its fallback types: ${fallbackTypes?.join(', ') || 'none configured'}`); + // ========================== + // 优先级 2: Model Fallback Mapping (跨协议/特定模型的回退) + // ========================== + + if (requestedModel && this.modelFallbackMapping && this.modelFallbackMapping[requestedModel]) { + const mapping = this.modelFallbackMapping[requestedModel]; + const targetProviderType = mapping.targetProviderType; + const targetModel = mapping.targetModel; + + if (targetProviderType && targetModel) { + this._log('info', `Trying Model Fallback Mapping for ${requestedModel}: -> ${targetProviderType} (${targetModel})`); + + // 递归调用 selectProviderWithFallback,但这次针对目标提供商类型 + // 注意:这里我们直接尝试从目标提供商池中选择,因为如果再次递归可能会导致死循环或逻辑复杂化 + // 简单起见,我们直接尝试选择目标提供商 + + // 检查目标类型是否有配置的池 + if (this.providerStatus[targetProviderType] && this.providerStatus[targetProviderType].length > 0) { + // 尝试从目标类型选择提供商(使用转换后的模型名) + const selectedConfig = this.selectProvider(targetProviderType, targetModel, options); + + if (selectedConfig) { + this._log('info', `Fallback activated (Model Mapping): ${providerType} (${requestedModel}) -> ${targetProviderType} (${targetModel}) (uuid: ${selectedConfig.uuid})`); + return { + config: selectedConfig, + actualProviderType: targetProviderType, + isFallback: true, + actualModel: targetModel // 返回实际使用的模型名,供上层进行请求转换 + }; + } else { + // 如果目标类型的主池也不可用,尝试目标类型的 fallback chain + // 例如 claude-kiro-oauth (mapped) -> claude-custom (chain) + // 这需要我们小心处理,避免无限递归。 + // 我们可以手动检查目标类型的 fallback chain + + const targetFallbackTypes = this.fallbackChain[targetProviderType] || []; + for (const fallbackType of targetFallbackTypes) { + // 检查协议兼容性 (目标类型 vs 它的 fallback) + const targetProtocol = getProtocolPrefix(targetProviderType); + const fallbackProtocol = getProtocolPrefix(fallbackType); + + if (targetProtocol !== fallbackProtocol) continue; + + // 检查模型支持 + const supportedModels = getProviderModels(fallbackType); + if (supportedModels.length > 0 && !supportedModels.includes(targetModel)) continue; + + const fallbackSelectedConfig = this.selectProvider(fallbackType, targetModel, options); + if (fallbackSelectedConfig) { + this._log('info', `Fallback activated (Model Mapping -> Chain): ${providerType} (${requestedModel}) -> ${targetProviderType} -> ${fallbackType} (${targetModel}) (uuid: ${fallbackSelectedConfig.uuid})`); + return { + config: fallbackSelectedConfig, + actualProviderType: fallbackType, + isFallback: true, + actualModel: targetModel + }; + } + } + } + } else { + this._log('warn', `Model Fallback target provider ${targetProviderType} not configured or empty.`); + } + } + } + + this._log('warn', `None available provider found for ${providerType} (Model: ${requestedModel}) after checking fallback chain and model mapping.`); return null; } @@ -716,4 +776,4 @@ export class ProviderPoolManager { } } -} \ No newline at end of file +} diff --git a/src/service-manager.js b/src/service-manager.js index 5ba97dd..9b19f55 100644 --- a/src/service-manager.js +++ b/src/service-manager.js @@ -274,6 +274,7 @@ export async function getApiServiceWithFallback(config, requestedModel = null, o let actualProviderType = config.MODEL_PROVIDER; let isFallback = false; let selectedUuid = null; + let actualModel = null; if (providerPoolManager && config.providerPools && config.providerPools[config.MODEL_PROVIDER]) { const selectedResult = providerPoolManager.selectProviderWithFallback( @@ -283,7 +284,7 @@ export async function getApiServiceWithFallback(config, requestedModel = null, o ); if (selectedResult) { - const { config: selectedProviderConfig, actualProviderType: selectedType, isFallback: fallbackUsed } = selectedResult; + const { config: selectedProviderConfig, actualProviderType: selectedType, isFallback: fallbackUsed, actualModel: fallbackModel } = selectedResult; // 合并选中的提供者配置到当前请求的 config 中 serviceConfig = deepmerge(config, selectedProviderConfig); @@ -292,6 +293,7 @@ export async function getApiServiceWithFallback(config, requestedModel = null, o actualProviderType = selectedType; isFallback = fallbackUsed; selectedUuid = selectedProviderConfig.uuid; + actualModel = fallbackModel; // 如果发生了 fallback,需要更新 MODEL_PROVIDER if (isFallback) { @@ -311,7 +313,8 @@ export async function getApiServiceWithFallback(config, requestedModel = null, o serviceConfig, actualProviderType, isFallback, - uuid: selectedUuid + uuid: selectedUuid, + actualModel }; } diff --git a/src/ui-manager.js b/src/ui-manager.js index 294b3e5..aa77286 100644 --- a/src/ui-manager.js +++ b/src/ui-manager.js @@ -701,6 +701,7 @@ export async function handleUIApiRequests(method, pathParam, req, res, currentCo if (newConfig.PROVIDER_POOLS_FILE_PATH !== undefined) currentConfig.PROVIDER_POOLS_FILE_PATH = newConfig.PROVIDER_POOLS_FILE_PATH; if (newConfig.MAX_ERROR_COUNT !== undefined) currentConfig.MAX_ERROR_COUNT = newConfig.MAX_ERROR_COUNT; if (newConfig.providerFallbackChain !== undefined) currentConfig.providerFallbackChain = newConfig.providerFallbackChain; + if (newConfig.modelFallbackMapping !== undefined) currentConfig.modelFallbackMapping = newConfig.modelFallbackMapping; // Proxy settings if (newConfig.PROXY_URL !== undefined) currentConfig.PROXY_URL = newConfig.PROXY_URL; @@ -748,6 +749,7 @@ export async function handleUIApiRequests(method, pathParam, req, res, currentCo PROVIDER_POOLS_FILE_PATH: currentConfig.PROVIDER_POOLS_FILE_PATH, MAX_ERROR_COUNT: currentConfig.MAX_ERROR_COUNT, providerFallbackChain: currentConfig.providerFallbackChain, + modelFallbackMapping: currentConfig.modelFallbackMapping, PROXY_URL: currentConfig.PROXY_URL, PROXY_ENABLED_PROVIDERS: currentConfig.PROXY_ENABLED_PROVIDERS }; diff --git a/static/app/config-manager.js b/static/app/config-manager.js index 2ff3386..3b12635 100644 --- a/static/app/config-manager.js +++ b/static/app/config-manager.js @@ -70,6 +70,7 @@ async function loadConfiguration() { const providerPoolsFilePathEl = document.getElementById('providerPoolsFilePath'); const maxErrorCountEl = document.getElementById('maxErrorCount'); const providerFallbackChainEl = document.getElementById('providerFallbackChain'); + const modelFallbackMappingEl = document.getElementById('modelFallbackMapping'); if (systemPromptFilePathEl) systemPromptFilePathEl.value = data.SYSTEM_PROMPT_FILE_PATH || 'configs/input_system_prompt.txt'; if (systemPromptModeEl) systemPromptModeEl.value = data.SYSTEM_PROMPT_MODE || 'append'; @@ -90,6 +91,15 @@ async function loadConfiguration() { providerFallbackChainEl.value = ''; } } + + // 加载 Model Fallback 映射配置 + if (modelFallbackMappingEl) { + if (data.modelFallbackMapping && typeof data.modelFallbackMapping === 'object') { + modelFallbackMappingEl.value = JSON.stringify(data.modelFallbackMapping, null, 2); + } else { + modelFallbackMappingEl.value = ''; + } + } // 加载代理配置 const proxyUrlEl = document.getElementById('proxyUrl'); @@ -160,6 +170,19 @@ async function saveConfiguration() { } else { config.providerFallbackChain = {}; } + + // 保存 Model Fallback 映射配置 + const modelFallbackMappingValue = document.getElementById('modelFallbackMapping')?.value?.trim() || ''; + if (modelFallbackMappingValue) { + try { + config.modelFallbackMapping = JSON.parse(modelFallbackMappingValue); + } catch (e) { + showToast(t('common.error'), t('config.advanced.modelFallbackMappingInvalid') || 'Model Fallback 映射配置格式无效,请输入有效的 JSON', 'error'); + return; + } + } else { + config.modelFallbackMapping = {}; + } // 保存代理配置 config.PROXY_URL = document.getElementById('proxyUrl')?.value?.trim() || null; @@ -204,4 +227,4 @@ async function saveConfiguration() { export { loadConfiguration, saveConfiguration -}; \ No newline at end of file +}; diff --git a/static/app/i18n.js b/static/app/i18n.js index 427635d..b6e87f6 100644 --- a/static/app/i18n.js +++ b/static/app/i18n.js @@ -206,6 +206,10 @@ const translations = { 'config.advanced.fallbackChainPlaceholder': '例如:\n{\n "gemini-cli-oauth": ["gemini-antigravity"],\n "gemini-antigravity": ["gemini-cli-oauth"],\n "claude-kiro-oauth": ["claude-custom"]\n}', 'config.advanced.fallbackChainNote': '当某一 Provider Type 所有账号都不健康时,自动切换到配置的 Fallback 类型。JSON 格式,键为主类型,值为 Fallback 类型数组(按优先级排序)', 'config.advanced.fallbackChainInvalid': 'Fallback 链配置格式无效,请输入有效的 JSON', + 'config.advanced.modelFallbackMapping': '跨协议模型 Fallback 映射', + 'config.advanced.modelFallbackMappingPlaceholder': '例如:\n{\n "gemini-claude-opus-4-5-thinking": {\n "targetProviderType": "claude-kiro-oauth",\n "targetModel": "claude-opus-4-5"\n }\n}', + 'config.advanced.modelFallbackMappingNote': '当主 Provider 不可用时,根据模型名映射到其他协议的 Provider 和模型。优先级低于上方的 Fallback 链配置。JSON 格式。', + 'config.advanced.modelFallbackMappingInvalid': 'Model Fallback 映射配置格式无效,请输入有效的 JSON', 'config.advanced.systemPrompt': '系统提示', 'config.advanced.systemPromptPlaceholder': '输入系统提示...', 'config.advanced.adminPassword': '后台登录密码', @@ -621,6 +625,10 @@ const translations = { 'config.advanced.fallbackChainPlaceholder': 'Example:\n{\n "gemini-cli-oauth": ["gemini-antigravity"],\n "gemini-antigravity": ["gemini-cli-oauth"],\n "claude-kiro-oauth": ["claude-custom"]\n}', 'config.advanced.fallbackChainNote': 'When all accounts of a Provider Type are unhealthy, automatically switch to configured Fallback types. JSON format, key is primary type, value is Fallback type array (sorted by priority)', 'config.advanced.fallbackChainInvalid': 'Invalid Fallback chain config format, please enter valid JSON', + 'config.advanced.modelFallbackMapping': 'Cross-Protocol Model Fallback Mapping', + 'config.advanced.modelFallbackMappingPlaceholder': 'Example:\n{\n "gemini-claude-opus-4-5-thinking": {\n "targetProviderType": "claude-kiro-oauth",\n "targetModel": "claude-opus-4-5"\n }\n}', + 'config.advanced.modelFallbackMappingNote': 'When the primary Provider is unavailable, map to other protocol Providers and models by model name. Priority is lower than the Fallback Chain Config above. JSON format.', + 'config.advanced.modelFallbackMappingInvalid': 'Invalid Model Fallback mapping config format, please enter valid JSON', 'config.advanced.systemPrompt': 'System Prompt', 'config.advanced.systemPromptPlaceholder': 'Enter system prompt...', 'config.advanced.adminPassword': 'Admin Password', @@ -1047,4 +1055,4 @@ export default { setLanguage, getCurrentLanguage, initI18n -}; \ No newline at end of file +}; diff --git a/static/index.html b/static/index.html index 7547ba1..a3d3993 100644 --- a/static/index.html +++ b/static/index.html @@ -701,6 +701,18 @@ 当某一 Provider Type 所有账号都不健康时,自动切换到配置的 Fallback 类型。JSON 格式,键为主类型,值为 Fallback 类型数组(按优先级排序) +