|
@@ -227,7 +227,7 @@ async function invokeWithRetry<T>(
|
|
|
return await withAiLog(fn, { callType, provider, model: modelId });
|
|
return await withAiLog(fn, { callType, provider, model: modelId });
|
|
|
} catch (error: any) {
|
|
} catch (error: any) {
|
|
|
// 不可切换的错误,直接抛出
|
|
// 不可切换的错误,直接抛出
|
|
|
- if (!config.models.shouldSwitchModel(error?.message || '')) {
|
|
|
|
|
|
|
+ if (!config.models.shouldSwitchModel(error)) {
|
|
|
throw error;
|
|
throw error;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -310,7 +310,7 @@ export async function callLLMWithMessages(
|
|
|
return responseContent;
|
|
return responseContent;
|
|
|
} catch (error: any) {
|
|
} catch (error: any) {
|
|
|
// 不可切换的错误,直接抛出
|
|
// 不可切换的错误,直接抛出
|
|
|
- if (!config.models.shouldSwitchModel(error?.message || '')) {
|
|
|
|
|
|
|
+ if (!config.models.shouldSwitchModel(error)) {
|
|
|
throw error;
|
|
throw error;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -464,7 +464,7 @@ export async function* callLLMStream(
|
|
|
yield content;
|
|
yield content;
|
|
|
}
|
|
}
|
|
|
} catch (error: any) {
|
|
} catch (error: any) {
|
|
|
- if (!config.models.shouldSwitchModel(error?.message || '')) {
|
|
|
|
|
|
|
+ if (!config.models.shouldSwitchModel(error)) {
|
|
|
throw error;
|
|
throw error;
|
|
|
}
|
|
}
|
|
|
// 流式无法完美重试(已部分yield),直接切换供应商
|
|
// 流式无法完美重试(已部分yield),直接切换供应商
|
|
@@ -504,12 +504,16 @@ function trySwitchModel(currentModelId: string, error: any): string | null {
|
|
|
const errorMessage = error?.message || '';
|
|
const errorMessage = error?.message || '';
|
|
|
const registry = getLlmRegistry();
|
|
const registry = getLlmRegistry();
|
|
|
|
|
|
|
|
- // 1. 额度耗尽检测:标记该供应商并切换(4小时自动恢复)
|
|
|
|
|
|
|
+ // 1. 额度耗尽检测:标记该供应商并立即切换(4小时自动恢复)
|
|
|
if (EXHAUSTED_PATTERNS.some(p => errorMessage.toLowerCase().includes(p))) {
|
|
if (EXHAUSTED_PATTERNS.some(p => errorMessage.toLowerCase().includes(p))) {
|
|
|
const currentNode = findProviderNodeForModel(currentModelId);
|
|
const currentNode = findProviderNodeForModel(currentModelId);
|
|
|
if (currentNode) {
|
|
if (currentNode) {
|
|
|
|
|
+ const exhaustedName = currentNode.provider.name;
|
|
|
// 4小时 TTL 自动恢复
|
|
// 4小时 TTL 自动恢复
|
|
|
- registry.markExhausted(currentNode.provider.name, errorMessage, 4 * 60 * 60 * 1000);
|
|
|
|
|
|
|
+ registry.markExhausted(exhaustedName, errorMessage, 4 * 60 * 60 * 1000);
|
|
|
|
|
+ console.log(`[LLM] 供应商 ${currentNode.provider.displayName} 额度耗尽,立即切换`);
|
|
|
|
|
+ // 传入被耗尽的供应商名,确保从它之后开始查找
|
|
|
|
|
+ return switchToNextVendorModel(currentModelId, exhaustedName);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -530,7 +534,7 @@ function trySwitchModel(currentModelId: string, error: any): string | null {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 4. 可恢复错误,调用通用判断(限流/余额/服务不可用等)
|
|
// 4. 可恢复错误,调用通用判断(限流/余额/服务不可用等)
|
|
|
- if (config.models.shouldSwitchModel(errorMessage)) {
|
|
|
|
|
|
|
+ if (config.models.shouldSwitchModel(error)) {
|
|
|
return switchToNextVendorModel(currentModelId);
|
|
return switchToNextVendorModel(currentModelId);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -542,9 +546,8 @@ function trySwitchModel(currentModelId: string, error: any): string | null {
|
|
|
* 优先查找同名/别名模型,保持输出一致性
|
|
* 优先查找同名/别名模型,保持输出一致性
|
|
|
* 找不到同名模型时回退到该供应商的第一个文本模型
|
|
* 找不到同名模型时回退到该供应商的第一个文本模型
|
|
|
*/
|
|
*/
|
|
|
-function switchToNextVendorModel(currentModelId: string): string | null {
|
|
|
|
|
|
|
+function switchToNextVendorModel(currentModelId: string, skipProviderName?: string): string | null {
|
|
|
const registry = getLlmRegistry();
|
|
const registry = getLlmRegistry();
|
|
|
- const currentNode = findProviderNodeForModel(currentModelId);
|
|
|
|
|
const available = registry.listAvailable();
|
|
const available = registry.listAvailable();
|
|
|
|
|
|
|
|
if (available.length === 0) {
|
|
if (available.length === 0) {
|
|
@@ -552,20 +555,49 @@ function switchToNextVendorModel(currentModelId: string): string | null {
|
|
|
return null;
|
|
return null;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 从当前供应商的下一个开始找
|
|
|
|
|
- const startIndex = currentNode
|
|
|
|
|
- ? (available.findIndex(n => n.provider.name === currentNode.provider.name) + 1) % available.length
|
|
|
|
|
- : 0;
|
|
|
|
|
|
|
+ // 切换供应商时必须清除模型缓存,否则 getLLM 可能返回旧供应商的客户端
|
|
|
|
|
+ // (缓存 key 只有 modelId,不区分供应商,切换后同名模型会命中旧缓存)
|
|
|
|
|
+ const invalidateAndReturn = (nextId: string): string => {
|
|
|
|
|
+ modelCache.delete(nextId);
|
|
|
|
|
+ modelCache.delete(currentModelId); // 旧 modelId 也需要清,防止后续调用命中原供应商缓存
|
|
|
|
|
+ return nextId;
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 确定起始查找位置
|
|
|
|
|
+ // 如果有 skipProviderName(被耗尽的供应商),从它后面开始找
|
|
|
|
|
+ // 否则从当前 modelId 所在供应商后面开始找
|
|
|
|
|
+ let startIndex = 0;
|
|
|
|
|
+ if (skipProviderName) {
|
|
|
|
|
+ // 被耗尽的供应商已不在 available 中,需要在全部已启用供应商中定位它
|
|
|
|
|
+ const allEnabled = registry.listEnabled();
|
|
|
|
|
+ const exhaustedPos = allEnabled.findIndex(n => n.provider.name === skipProviderName);
|
|
|
|
|
+ if (exhaustedPos >= 0) {
|
|
|
|
|
+ // 从耗尽供应商的下一个开始,找到第一个仍在 available 中的
|
|
|
|
|
+ for (let offset = 1; offset <= allEnabled.length; offset++) {
|
|
|
|
|
+ const checkName = allEnabled[(exhaustedPos + offset) % allEnabled.length].provider.name;
|
|
|
|
|
+ const availIdx = available.findIndex(n => n.provider.name === checkName);
|
|
|
|
|
+ if (availIdx >= 0) {
|
|
|
|
|
+ startIndex = availIdx;
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 无供应商被耗尽:从当前模型所在供应商之后开始
|
|
|
|
|
+ const currentNode = findProviderNodeForModel(currentModelId);
|
|
|
|
|
+ if (currentNode) {
|
|
|
|
|
+ startIndex = (available.findIndex(n => n.provider.name === currentNode.provider.name) + 1) % available.length;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
for (let i = 0; i < available.length; i++) {
|
|
for (let i = 0; i < available.length; i++) {
|
|
|
const idx = (startIndex + i) % available.length;
|
|
const idx = (startIndex + i) % available.length;
|
|
|
const node = available[idx];
|
|
const node = available[idx];
|
|
|
- if (node === currentNode) continue;
|
|
|
|
|
|
|
|
|
|
// 1. 精确匹配:下一个供应商是否也提供同名模型
|
|
// 1. 精确匹配:下一个供应商是否也提供同名模型
|
|
|
if (node.provider.hasModel(currentModelId)) {
|
|
if (node.provider.hasModel(currentModelId)) {
|
|
|
console.log(`[LLM] 同模型切换到供应商 ${node.provider.displayName},模型 ${currentModelId}`);
|
|
console.log(`[LLM] 同模型切换到供应商 ${node.provider.displayName},模型 ${currentModelId}`);
|
|
|
- return currentModelId;
|
|
|
|
|
|
|
+ return invalidateAndReturn(currentModelId);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 2. 别名匹配:下一个供应商中是否有 canonicalModel 指向当前模型的
|
|
// 2. 别名匹配:下一个供应商中是否有 canonicalModel 指向当前模型的
|
|
@@ -573,15 +605,19 @@ function switchToNextVendorModel(currentModelId: string): string | null {
|
|
|
const cfg = node.provider.getModelConfig(modelId);
|
|
const cfg = node.provider.getModelConfig(modelId);
|
|
|
if (cfg?.canonicalModel === currentModelId) {
|
|
if (cfg?.canonicalModel === currentModelId) {
|
|
|
console.log(`[LLM] 别名模型切换到供应商 ${node.provider.displayName},模型 ${modelId}`);
|
|
console.log(`[LLM] 别名模型切换到供应商 ${node.provider.displayName},模型 ${modelId}`);
|
|
|
- return modelId;
|
|
|
|
|
|
|
+ return invalidateAndReturn(modelId);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- // 3. 无同名/别名模型,使用该供应商的第一个文本模型
|
|
|
|
|
|
|
+ // 第一轮没找到同名/别名模型,第二轮 fallback:用第一个可用供应商的首个模型
|
|
|
|
|
+ for (let i = 0; i < available.length; i++) {
|
|
|
|
|
+ const idx = (startIndex + i) % available.length;
|
|
|
|
|
+ const node = available[idx];
|
|
|
if (node.provider.textModels.length > 0) {
|
|
if (node.provider.textModels.length > 0) {
|
|
|
const nextId = node.provider.textModels[0];
|
|
const nextId = node.provider.textModels[0];
|
|
|
console.log(`[LLM] 切换到供应商 ${node.provider.displayName},模型 ${nextId}`);
|
|
console.log(`[LLM] 切换到供应商 ${node.provider.displayName},模型 ${nextId}`);
|
|
|
- return nextId;
|
|
|
|
|
|
|
+ return invalidateAndReturn(nextId);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|