#!/usr/bin/env python3 import sys p = "/data/ai/audio/server/src/services/llm/index.ts" src = open(p).read() old = """async function invokeWithRetry( fn: (modelId: string) => Promise, modelId: string, callType: string = 'llm_chat', ): Promise { const tried = new Map(); let cur = modelId; let lastErr: any = null; for (let round = 0; round < HA_RETRY_CONFIG.maxFallbackRounds; round++) { // 关键: 选下一个未 tried 的 node (而不是用 resolveProviderKey 取第一个匹配) const node = resolveNodeForModel(cur, tried); if (!node) { console.warn(`[LLM-HA] 已尝试所有 node,停止 fallback`); break; } const fp = node.provider.vendor + ':' + ((node.provider as any)._apiKey || '').slice(-12); tried.set(fp, { model: cur }); let providerError: any = null; for (let attempt = 0; attempt < HA_RETRY_CONFIG.maxRetriesPerProvider; attempt++) { try { const result = await _runWithTriedNodes(new Set(tried.keys()), () => fn(cur)); return result; } catch (error: any) { const em = error?.message || String(error); lastErr = error; providerError = error; if (isBusinessError(em) && !isNetworkError(em)) { console.error(`[LLM-HA] ${fp}/${cur} 业务错误,立即抛出: ${em.substring(0, 100)}`); throw error; } if (isNetworkError(em)) { console.warn(`[LLM-HA] ${fp}/${cur} 网络错误,立即切换: ${em.substring(0, 100)}`); break; } if (attempt < HA_RETRY_CONFIG.maxRetriesPerProvider - 1) { const delay = backoffWithJitter(attempt); console.log(`[LLM-HA] ${fp}/${cur} 第${attempt + 1}次失败: ${em.substring(0, 100)}, ${delay}ms后重试`); await new Promise(r => setTimeout(r, delay)); } } }""" new = """// 单次 LLM 调用超时:防止某个 provider 永久 hang 而 invokeWithRetry // 永远得不到 error 来触发切换。 // 设为 3 分钟,比 ChatOpenAI 内部 timeout 略大,确保正常请求有足够时间响应。 const LLM_INVOKE_TIMEOUT_MS = 180_000; function invokeWithTimeout(fn: () => Promise, ms: number): Promise { let timer: ReturnType | null = null; const timeoutPromise = new Promise((_, reject) => { timer = setTimeout( () => reject(new Error(`Request timeout after ${ms}ms (forced by invokeWithTimeout)`)), ms, ); }); return Promise.race([fn(), timeoutPromise]).finally(() => { if (timer) clearTimeout(timer); }) as Promise; } async function invokeWithRetry( fn: (modelId: string) => Promise, modelId: string, callType: string = 'llm_chat', ): Promise { const tried = new Map(); let cur = modelId; let lastErr: any = null; for (let round = 0; round < HA_RETRY_CONFIG.maxFallbackRounds; round++) { // 关键: 选下一个未 tried 的 node (而不是用 resolveProviderKey 取第一个匹配) const node = resolveNodeForModel(cur, tried); if (!node) { console.warn(`[LLM-HA] 已尝试所有 node,停止 fallback`); break; } const fp = node.provider.vendor + ':' + ((node.provider as any)._apiKey || '').slice(-12); tried.set(fp, { model: cur }); let providerError: any = null; for (let attempt = 0; attempt < HA_RETRY_CONFIG.maxRetriesPerProvider; attempt++) { try { const result = await invokeWithTimeout( () => _runWithTriedNodes(new Set(tried.keys()), () => fn(cur)), LLM_INVOKE_TIMEOUT_MS, ); return result; } catch (error: any) { const em = error?.message || String(error); lastErr = error; providerError = error; if (isBusinessError(em) && !isNetworkError(em)) { console.error(`[LLM-HA] ${fp}/${cur} 业务错误,立即抛出: ${em.substring(0, 100)}`); throw error; } if (isNetworkError(em)) { console.warn(`[LLM-HA] ${fp}/${cur} 网络错误,立即切换: ${em.substring(0, 100)}`); break; } if (attempt < HA_RETRY_CONFIG.maxRetriesPerProvider - 1) { const delay = backoffWithJitter(attempt); console.log(`[LLM-HA] ${fp}/${cur} 第${attempt + 1}次失败: ${em.substring(0, 100)}, ${delay}ms后重试`); await new Promise(r => setTimeout(r, delay)); } } }""" if old not in src: print("ERROR: old block not found") sys.exit(1) src = src.replace(old, new, 1) open(p, 'w').write(src) print("patched llm/index.ts OK")