registry.js 2.05 KB
/**
 * Provider 自动注册表 —— 扫描 `providers/` 目录,按文件名 `llm.<apiType>.js` 建表。
 *
 * 「加厂商 = 加一个文件」:放进 `providers/llm.<x>.js` 并默认导出一个继承 LLMBase 的类,
 * 即被发现,无需改本文件或任何现有文件。注册键 = 文件名中 `llm.` 与 `.js` 之间的段。
 *
 * 不在模块顶层执行扫描(边界纪律:无顶层副作用);`loadRegistry()` 被 `createLLM` 调用时才读目录。
 * 目录内只有真实厂商适配器:假 provider 已删除,网关不提供任何「不发真实请求」的实现。
 */

import { readdirSync } from 'node:fs'
import path from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'

const here = path.dirname(fileURLToPath(import.meta.url))
const PROVIDERS_DIR = path.join(here, 'providers')

/** 文件名形如 `llm.openai.js` / `llm.azure_foundry.js` → 捕获 provider key。 */
const FILE_RE = /^llm\.([a-z0-9_-]+)\.js$/

/**
 * 扫描 providers/ 目录,返回 `{ apiType: () => Promise<ProviderClass> }` 的惰性加载表。
 * 惰性:只在真正 `createLLM({ provider })` 命中某 apiType 时才 dynamic import 该文件,
 * 避免加载未用到的子类(也避免任一子类的加载错误污染整张表)。
 * @param {string} [dir] 覆盖扫描目录(仅供测试自定义 providers 目录)。
 * @returns {Record<string, () => Promise<{ default: new (cfg: any) => any }>>}
 */
export function loadRegistry(dir = PROVIDERS_DIR) {
  /** @type {Record<string, () => Promise<{ default: new (cfg: any) => any }>>} */
  const table = {}
  for (const name of readdirSync(dir)) {
    const m = FILE_RE.exec(name)
    if (!m) continue
    const apiType = m[1]
    const fileUrl = pathToFileURL(path.join(dir, name)).href
    table[apiType] = () => import(fileUrl)
  }
  return table
}

/**
 * 列出已注册的 apiType(用于未知 provider 的报错信息)。
 * @param {string} [dir]
 * @returns {string[]}
 */
export function registeredProviders(dir = PROVIDERS_DIR) {
  return Object.keys(loadRegistry(dir)).sort()
}