mode-router.js
2.92 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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
/**
* createModeRouter —— 场景名(mode)→ 与单实例 `Llm` 同形的实例(含其 fallback 链)。
*
* configSource 出口已归一为 `{ [mode]: Array<{provider,model}> }`(见 config.js),故本文件内部
* 零判空、零 JSON.parse、零形态分支:chain 恒为非空对象数组。加场景 = 加表行,零代码改动。
*/
import { withFallback } from './fallback.js'
import { LLMError } from '@drama/llm'
/** @typedef {import('@drama/llm').Llm} Llm */
/**
* @param {object} deps
* @param {{ get: () => Promise<{ [mode: string]: Array<{ provider: string, model: string }> }>, invalidate: () => void }} deps.configSource
* @drama/config-source 的 createVersionedSource 产物;get() 返回已归一配置,invalidate() 失效版本缓存。
* @param {(cfg: { provider: string, model: string }) => Llm} deps.factory
* 装配层绑定为 `(cfg) => createLLM({ ...sharedPorts, provider: cfg.provider, model: cfg.model })`。
* @returns {{ forMode: (mode: string) => Promise<Llm>, invalidate: () => void }}
*/
export function createModeRouter({ configSource, factory }) {
/** @type {Map<string, Llm>} mode → 懒建缓存的实例(含其 fallback 链)。 */
const cache = new Map()
/** @type {object | null} 上次 configSource.get() 的配置对象引用;据其变化判定热更新。 */
let cachedConfig = null
/**
* @param {string} mode
* @returns {Promise<Llm>}
*/
async function forMode(mode) {
// 实例缓存与配置版本绑定:每次先 get()——createVersionedSource 版本未变返回同一对象引用,
// 版本变化返回新对象。引用变即热更新,清实例缓存令下次按新 chain 重建;引用未变直接走缓存。
// 探测频率由装配层 ttlMs 节流(get 在新鲜窗口内不触达 DB),故此处每次调用无额外 IO 成本。
const config = await configSource.get()
if (config !== cachedConfig) {
cache.clear()
cachedConfig = config
}
const cached = cache.get(mode)
if (cached) return cached
const chain = config[mode]
// 配置缺失是调用方错误,不可降级:抛 badRequest,与网关错误分类一致。
if (!chain) {
throw new LLMError({
provider: 'llm-router',
reason: 'badRequest',
retryable: false,
message: `forMode: no chain configured for mode '${mode}'`,
})
}
const llms = chain.map(({ provider, model }) => factory({ provider, model }))
// mode 随链下沉:链上每次 attempt 的审计行都要认领"我是哪个场景的调用"。
const llm = withFallback(llms, { mode })
cache.set(mode, llm)
return llm
}
/** 清本地实例缓存 + 透传失效 configSource 版本缓存,保证「改表 + invalidate → 下次走新 chain 重建」。 */
function invalidate() {
cache.clear()
cachedConfig = null
configSource.invalidate()
}
return { forMode, invalidate }
}