mode-router.js 2.92 KB
/**
 * 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 }
}