Merge pull request #158 from ZqinKing/main

feat(fallback): 新增跨协议模型 Fallback 映射功能
This commit is contained in:
何夕2077 2026-01-04 15:25:42 +08:00 committed by GitHub
commit 982babb5aa
8 changed files with 163 additions and 28 deletions

View file

@ -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"
]
}
}

View file

@ -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`;
}
}
}

View file

@ -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 {
}
}
}
}

View file

@ -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
};
}

View file

@ -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
};

View file

@ -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
};
};

View file

@ -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
};
};

View file

@ -701,6 +701,18 @@
<small class="form-text" data-i18n="config.advanced.fallbackChainNote">当某一 Provider Type 所有账号都不健康时,自动切换到配置的 Fallback 类型。JSON 格式,键为主类型,值为 Fallback 类型数组(按优先级排序)</small>
</div>
<div class="form-group pool-section">
<label for="modelFallbackMapping" data-i18n="config.advanced.modelFallbackMapping">跨协议模型 Fallback 映射</label>
<textarea id="modelFallbackMapping" class="form-control" rows="6" data-i18n-placeholder="config.advanced.modelFallbackMappingPlaceholder" placeholder='例如:
{
"gemini-claude-opus-4-5-thinking": {
"targetProviderType": "claude-kiro-oauth",
"targetModel": "claude-opus-4-5"
}
}'></textarea>
<small class="form-text" data-i18n="config.advanced.modelFallbackMappingNote">当主 Provider 不可用时,根据模型名映射到其他协议的 Provider 和模型。JSON 格式。</small>
</div>
<!-- 系统提示配置移到最下面 -->
<div class="form-group system-prompt-section">
<label for="systemPrompt" data-i18n="config.advanced.systemPrompt">系统提示</label>
@ -972,4 +984,3 @@
<script type="module" src="app/app.js"></script>
</body>
</html>