6export type ModelAttempt = {model: string, classification: OrrErrClass, errMsg: string, at: number} 7export type FallbackDetailedResult<T> = {result?: T, attempts: ModelAttempt[]} 9export const fmtExhaustionMsg = (attempts: ModelAttempt[]) => 10 attempts.map(a => `${a.model} (${a.classification}: ${a.errMsg.slice(0, 80)})`).join('; ') 12const runFallback = async <T,>({ 13 candidates, callFn, isSuccess = (r: T) => r != null, context = 'modelFallback', 16 callFn: (model: string) => Promise<T> 17 isSuccess?: (r: T) => boolean 19}): Promise<FallbackDetailedResult<T>> => { 20 const attempts: ModelAttempt[] = [] 21 for (const model of candidates) { 24 attempts.push({model, classification: info?.classification || 'permanent', errMsg: 'pre-blacklisted', at: Date.now()}) 28 const result = await callFn(model) 29 if (isSuccess(result)) return {result, attempts} 30 attempts.push({model, classification: 'unknown', errMsg: 'no result', at: Date.now()}) 34 const errMsg = err?.message || String(err) 36 attempts.push({model, classification, errMsg, at: Date.now()}) 37 mcpDebugLog(`${context}: errored, trying next ${model} ${classification}: ${errMsg}`) 38 if (classification === 'permanent') await noThrowNotifErr(`modelOffline ${model}`, {context, classification, errMsg}) 41 return {result: undefined, attempts} 44export const tryWithModelFallback = async <T,>(opts: { 46 callFn: (model: string) => Promise<T> 47 isSuccess?: (r: T) => boolean 49}): Promise<T | undefined> => (await runFallback<T>(opts)).result 51export const tryWithModelFallbackDetailed = async <T,>(opts: { 53 callFn: (model: string) => Promise<T> 54 isSuccess?: (r: T) => boolean 56}): Promise<FallbackDetailedResult<T>> => runFallback<T>(opts)