fallback.js
2.13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/**
* withFallback —— 把一条模型链(主→备)包成与单实例 `Llm` 同形的实例。
*
* 候选遍历与降级判定收敛在 @drama/service-call 的通用 withFallback:唯一降级信号是
* 结构化错误的 `retryable === true`(duck-type;`@drama/llm` 已把 rateLimit/transient 归一为 true,
* auth/badRequest/aborted/unknown 为 false)。不读 `err.reason` 字符串、不匹配 `err.message`、
* 不判 HTTP 状态。本文件只负责 LLM 域的 attempt 上下文:mode / correlation_id / attempt_index 注入。
*
* 返回值 `{ chat }` 与 `createLLM` 逐字一致;`opts`(on_content/on_thinking/on_tool_call/signal/billing_ctx)
* 整体透传、不解构,故流式回调与 signal 天然透传给每个被尝试的实例。
*/
import { randomUUID } from 'node:crypto'
import { LLMError } from '@drama/llm'
import { withFallback as chainCandidates } from '@drama/service-call'
/** @typedef {import('@drama/llm').Llm} Llm */
/**
* @param {Llm[]} llms 非空模型链,索引 0 为主、末位为最后兜底。
* @param {object} [cfg]
* @param {string} [cfg.mode] 场景名(`mode_model_config.mode`);由 mode-router 传入,随每次 attempt 落审计。
* @returns {Llm}
*/
export function withFallback(llms, { mode } = {}) {
if (llms.length < 1) {
throw new LLMError({
provider: 'llm-router',
reason: 'badRequest',
retryable: false,
message: 'withFallback: llms must be a non-empty array',
})
}
/**
* @param {import('@drama/llm').ChatRequest} req
* @param {import('@drama/llm').ChatOpts} [opts]
* @returns {Promise<import('@drama/llm').ChatResult>}
*/
async function chat(req, opts) {
// 一次 chat = 一个 correlation_id:链上每个 attempt 各自是一行 llm_call_log,靠它串成"这次 turn 降级了"。
// 链上每个实例各自 genId 出自己的 requestId(对账键,不可共用),故关联只能另立一个 id。
const correlation_id = randomUUID()
return chainCandidates(
llms.map((llm, i) => () => llm.chat(req, { ...opts, mode, correlation_id, attempt_index: i })),
)()
}
return { chat }
}