Merge pull request #158 from ZqinKing/main
feat(fallback): 新增跨协议模型 Fallback 映射功能
This commit is contained in:
commit
982babb5aa
8 changed files with 163 additions and 28 deletions
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
|||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue