/** * 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, invalidate: () => void }} */ export function createModeRouter({ configSource, factory }) { /** @type {Map} mode → 懒建缓存的实例(含其 fallback 链)。 */ const cache = new Map() /** @type {object | null} 上次 configSource.get() 的配置对象引用;据其变化判定热更新。 */ let cachedConfig = null /** * @param {string} mode * @returns {Promise} */ 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 } }