@easbot/llm
v0.3.23
Published
AI model aggregation layer for EASBot ecosystem: multi-vendor SDK dynamic loading, model routing, configuration management, and high-level Model Provider capability wrapping.
Maintainers
Readme
@easbot/llm
English | 中文
@easbot/llm 是 EASBot 生态系统的 AI 模型聚合层,负责多厂商 SDK 的动态加载、模型路由、配置管理与高层 Model Provider 的能力封装。给业务包(agent / codebase / memory / note / skills / gateway)提供统一的 LLM 调用抽象,避免每个工具包重复实现 provider 路由、auth 管理和错误归一化。
当前状态(V4 重构后)
本包已从 @easbot/types 的早期占位实现演化为承载 provider/ + model/ 两套核心模块** 的生产级 AI 网关层:
- ✅
src/provider/(7 文件):完整的工业级 provider 抽象Provider命名空间 —— 25+ 内置 SDK(OpenAI / Anthropic / Azure / Bedrock / Vertex / OpenRouter / Copilot / Ollama / 本地模型等)的动态加载器、ModelsDevcatalog 合并、模型缓存、SDK 实例池化ProviderError命名空间 —— APICallError 解析(含 25+ 种"上下文溢出"模式正则识别)ProviderAuth命名空间 —— OAuth + API key 双轨认证ProviderTransform命名空间 —— 模型 variant / tool / 限制处理ModelsDev命名空间 —— models.dev catalog 的 zod schema + 加载 + 远程刷新definition.ts—— DEFAULT_ENABLED_PROVIDERS 白名单 + CUSTOM_PROVIDER_DEFINITIONS 本地 SDK 快照 + PROVIDER_DISPLAY_NAMES 友好名映射sdk/copilot/—— 自研 GitHub Copilot 兼容 SDK(chat + responses + 6 个 tool)
- ✅
src/model/(4 命名空间):高层 Model ProviderRerankProvider命名空间 —— LLM 驱动的文档重排序(与 Memory 混合搜索权重统一)GraphProvider命名空间 —— 从非结构化文本提取实体关系(11 类 EntityType + 9 类 RelationType)SummaryProvider命名空间 —— note / memory 摘要(LLM-based)EmbeddingProvider命名空间 —— 文本嵌入(单文本 / 批量;走 AI SDK 标准embed/embedMany;默认模型easbot-local/bge-base-zh-v1.5)
- ✅
src/config/(V3 新增):独立 LLM Config 模块,仿packages/gateway/src/config/模式LLMConfig / ProviderConfig / ProviderAuthMethod—— zod schema(独立可用,也可作为注入 Config 的实现来源)Confignamespace ——get()/provider(id)(优先走注入,降级到本地 loader)
- ✅
src/interfaces.ts(V3 新增 / V4 精简):Adapter Registry + Env / Flag namespace- 4 个 Provider 接口(
IConfigProvider/IInstanceProvider/IGlobalPathProvider/IInstallationProvider;Auth 已下沉到@easbot/utils.Auth) setAdapterRegistry(...)—— agent 包启动时一次注入,完全解耦本包对 agent 内部模块的所有依赖getI<X>Provider()便捷访问器 —— 未注入时 fail-fast 抛错(不静默退化)readEnvString / readFlagBool / readFlagNumber—— 静态读取,避免对注入的依赖Env / Flagnamespace —— 镜像process.env的静态字面(用于 IDE 提示 / 文档生成 / 配置检查)
- 4 个 Provider 接口(
- ✅
src/index.ts—— 聚合导出入口(4 provider 命名空间 + 3 model 命名空间 + 自研 Copilot SDK + 3 常量 + Config / interfaces 接入点 + AdapterRegistry + 4 Provider 接口)
核心特性
- 25+ 内置 AI SDK 动态加载:按需 import,避免打包产物膨胀
- 三层数据源融合:models.dev 远程 catalog + 构建时 snapshot + 本地 CUSTOM_PROVIDER_DEFINITIONS(用于 ollama / easbot-local 这类不出现在 models.dev 的本仓自研 SDK)
- 统一 Provider 命名空间:
Provider.list() / getModel() / parseModel()一行调用,与具体 SDK 解耦 - OAuth + API key 双轨:
ProviderAuthnamespace 管理 OAuth flow / API key 持久化 / 注入到 SDK options - Context Overflow 自动识别:跨厂商统一的"上下文超限"语义(25+ 正则模式 + 4 种 HTTP status 兜底)
- 自研 GitHub Copilot 兼容 SDK:完整实现 chat + responses API + 6 个 tool(code-interpreter / file-search / web-search / image-generation / local-shell / web-search-preview)
- 持久化模型缓存:构建时生成
models-snapshot.ts,运行时优先用本地,避免冷启动连网 - 极简聚合导出:
@easbot/llm一行引入 5+3 个命名空间 + 自研 SDK + 关键常量
安装
pnpm add @easbot/llm快速上手
1. 列出所有已配置的 provider
import { Provider } from '@easbot/llm';
const allProviders = await Provider.list();
// { openai: { id, name, source, env, models, ... }, anthropic: {...}, ... }2. 获取特定模型(按 provider/model 字符串路由)
import { Provider } from '@easbot/llm';
const gpt4 = await Provider.getModel('openai/gpt-4');
// gpt4: LanguageModelV3(AI SDK v6 规范)
const embed = await Provider.getModel('openai/text-embedding-3-small');
// embed: EmbeddingModelV3
const rerank = await Provider.getModel('easbot-local/rerank-proxy');
// rerank: RerankingModelV33. 解析模型 ID(安全拆分,不抛错)
import { Provider } from '@easbot/llm';
const { providerId, modelId } = Provider.parseModel('openai/gpt-4o');
// { providerId: 'openai', modelId: 'gpt-4o' }4. 解析 API 错误(含 context overflow 识别)
import { ProviderError, APICallError } from 'ai';
try {
await someProvider.doCall(...);
} catch (e) {
if (e instanceof APICallError) {
const parsed = ProviderError.parseAPICallError({ providerId: 'openai', error: e });
if (parsed.type === 'context_overflow') {
// UI: "上下文超限,请缩短输入或切换模型"
}
}
}5. 高层 Model Provider 直接调用
import { RerankProvider, GraphProvider, SummaryProvider } from '@easbot/llm';
// 文档重排序
const reranked = await RerankProvider.rerank({
query: '...',
documents: ['...', '...'],
});
// 图谱实体提取
const graph = await GraphProvider.generateGraph({
chunks: ['chunk1...', 'chunk2...', '...'], // 任意长度数组
prompt: { maxChunks: 5, maxConcurrency: 3 }, // v0.8:per-call 切片 + 3 并发
});
// graph.entities / relations / summary 直接入库v0.8 行为契约(packages/llm/src/model/graph.ts):
- 所有 chunks 都被尝试抽取:chunks 数组按
prompt.maxChunks(默认 5)切片成多组,每组调一次 LLM;不再像 v0.7 那样硬截断到前 5 个 chunk。 - chunk 级 3 并发:多组用轻量级 semaphore 并发跑(
prompt.maxConcurrency,默认 3),避免触发 provider 限流。 - 局部失败容错:单组 LLM 失败 → 仅丢该组结果,其他组继续。不会因一次失败让整个文档 KG 留空。
- 边界感知截断:超长 chunk 走
splitByBoundary进一步切分(按\n\n/\n/ 空白找最近边界),保证 entity name 不被切碎。
// Note 知识库摘要
const summary = await SummaryProvider.summarizeNote({
query: '...',
chunks: [...],
});6. 文本嵌入(EmbeddingProvider)
import { EmbeddingProvider } from '@easbot/llm';
// 单文本嵌入 — 返回 EmbedOutput(含 embedding + usage.tokens)
const single = await EmbeddingProvider.embedText({ value: '一段中文文本' });
// single.embedding: number[](默认 bge-base-zh-v1.5 输出 512 维)
// single.usage.tokens: number
// 批量嵌入(推荐:单次 API 调用比 N 次 aiEmbed 更高效)
const batch = await EmbeddingProvider.embedTexts({ values: ['a', 'b', 'c'] });
// batch.embeddings: number[][](与 values 一一对应)
// batch.usage.tokens: number
// 显式指定模型(跳过 Provider.defaultModel)
import { Provider } from '@easbot/llm';
const customModel = Provider.parseModel('openai/text-embedding-3-small');
const r = await EmbeddingProvider.embedText({ value: 'hi', model: customModel });7. 使用自研 GitHub Copilot SDK
import { createOpenaiCompatible, openaiCompatible } from '@easbot/llm';
const copilot = createOpenaiCompatible({
apiKey: process.env.GITHUB_TOKEN,
});
const model = copilot('gpt-4o');
// 或使用默认实例
const model2 = openaiCompatible('gpt-4o');顶层导出清单
// Provider 核心命名空间
export * as Provider from './provider/provider';
export * as ProviderError from './provider/error';
export * as ProviderAuth from './provider/auth';
export * as ProviderTransform from './provider/transform';
export * as ModelsDev from './provider/models';
// Provider 常量
export {
DEFAULT_ENABLED_PROVIDERS,
CUSTOM_PROVIDER_DEFINITIONS,
PROVIDER_DISPLAY_NAMES,
} from './provider/definition';
// 顶层函数
export { initModelsRefresh } from './provider/models';
// 自研 GitHub Copilot SDK
export {
createOpenaiCompatible,
openaiCompatible,
} from './provider/sdk/copilot';
export type {
OpenaiCompatibleProvider,
OpenaiCompatibleProviderSettings,
OpenaiCompatibleModelId,
} from './provider/sdk/copilot';
// 高层 Model Provider 命名空间
export * as RerankProvider from './model/rerank';
export * as GraphProvider from './model/graph';
export * as SummaryProvider from './model/summary';
export * as EmbeddingProvider from './model/embedding';EmbeddingProvider 公开契约(packages/llm/src/model/embedding.ts):
embedText(input, abortSignal?) → Promise<EmbedOutput>EmbedOutput = { embedding: number[]; usage: { tokens: number } }- 空
value返回{ embedding: [], usage: { tokens: 0 } },不调用ai.embed
embedTexts(inputs, abortSignal?) → Promise<EmbedBatchOutput>EmbedBatchOutput = { embeddings: number[][]; usage: { tokens: number } }- 空
values返回{ embeddings: [], usage: { tokens: 0 } },不调用ai.embedMany
- 默认模型:
easbot-local/bge-base-zh-v1.5(导出常量EmbeddingProvider.DEFAULT_MODEL) model入参支持三种形态:省略 →Provider.defaultModel链路;Provider.Model→Provider.getEmbedding;直接传EmbeddingModel实例(duck-type 由specificationVersion字段判定)→ 跳过 Provider 链路
配置注入(切换 LLM 实现的迁移指南)
V4 重构后,Provider / ModelsDev / ProviderAuth / ProviderTransform 命名空间已通过 Adapter Registry 模式与 agent 包完全解耦,不再直接 import agent 包的任何模块。所有运行时依赖都通过 4 个 Provider 接口显式注入(Auth 已下沉到 @easbot/utils.Auth)。
1. Adapter Registry 总览
agent 包启动时调用一次 setAdapterRegistry(...),注入 4 个 Provider 实现:
import {
setAdapterRegistry,
type AdapterRegistry,
} from '@easbot/llm';
// 在 agent 包启动入口处(例如 Instance.provide 之前)
setAdapterRegistry({
config: Config, // IConfigProvider — get / provider
instance: Instance, // IInstanceProvider — getDirectory / getWorktree
global: Global.Path, // IGlobalPathProvider — cache / data / config
installation: Installation, // IInstallationProvider — getVersion
// V4 起 Auth 已下沉到 `@easbot/utils.Auth`,不再通过 AdapterRegistry 注入。
// LLM 包内部直接 `import { Auth } from '@easbot/utils'` 调用即可。
} satisfies AdapterRegistry);未注入时,所有 getI<X>Provider() 会 fail-fast 抛错:
[LLM] adapter 'config' (IConfigProvider) not registered.
Call setAdapterRegistry({ config: ... }) before using LLM.设计意图:不静默退化。若忘注入就抛错,避免线上"undefined config"导致模型路由静默失效。
2. 4 个 Provider 接口契约
| 接口 | 方法 | 原 agent 模块 | 用途 |
|------|------|---------------|------|
| IConfigProvider | get<T>(): Promise<T> / provider<T>(id): Promise<T \| undefined> | Config | 用户 ~/.config/easbot/config.json 的 llm 块(whitelist / blacklist / variant override) |
| IInstanceProvider | getDirectory(): string / getWorktree(): string | Instance | 实例路径与 worktree 信息 |
| IGlobalPathProvider | cache: string / data: string / config: string | Global.Path | 模型快照缓存目录 / 用户数据目录 / 全局配置目录 |
| IInstallationProvider | getVersion(): string | Installation | User-Agent / 兼容性检查 / 版本信息 |
V4 变更:
IInstanceProvider.state(init)与IPluginProvider已被拆分下沉(ADR 0071)。
- Provider 内部 state 缓存改用
@easbot/utils.lazyAsync(init)(per-process 单例 + 显式 reset)- Plugin 注册通过模块级
_pluginAuthRegistry: Map<providerId, methods[]>+ProviderAuth.registerPluginAuthProvider()入口注入
3. LLM Config(独立工作 + 集成工作)
V3 新增 src/config/ 模块,仿 packages/gateway/src/config/ 模式:
- 独立工作:从
easbot.json/easbot.jsonc直接读llm字段(模型、provider、auth 等) - 集成工作:当 AdapterRegistry 已注入时,
Config.get()优先走注入的实现(让 agent 包能复用)
import { Config, LLMConfig } from '@easbot/llm';
const cfg = await Config.get(); // 走注入或 fallback loader
const openaiCfg = await Config.provider('openai');
// Schema 校验(可单独使用)
const parsed = LLMConfig.parse({
model: 'openai/gpt-4o',
provider: {
openai: {
npm: '@ai-sdk/openai',
options: { apiKey: 'sk-...' },
},
},
});4. Env / Flag namespace(镜像 process.env)
Flag namespace 与读取函数 (readEnvString / readFlagBool) 仍由 llm 包提供;Env 已下沉到 @easbot/utils,llm 仅透传 re-export,调用方也可直接 import @easbot/utils 的 Env:
import { Flag, readEnvString, readFlagBool } from '@easbot/llm';
import { Env } from '@easbot/llm'; // 等价于 import { Env } from '@easbot/utils'
Flag.EASBOT_OUTPUT_TOKEN_MAX; // 数值字段,内部动态计算
readEnvString('OPENAI_API_KEY'); // 包装 process.env5. 调用点改造对照表
| 模块 | V3 之前 | V3 | V4(本次重构) |
|------|--------|----|----------------|
| provider/auth.ts | Instance.state(...) | getIInstanceProvider().state(init) | lazyAsync(init) + _pluginAuthRegistry 模块级 list |
| provider/auth.ts | Auth.set(providerId, info) | Auth.set(...) 直接调用 @easbot/utils.Auth(下沉到 utils) | Auth.set(...) 直接调用 @easbot/utils.Auth(下沉到 utils) |
| provider/auth.ts | Plugin.definitions() | getIPluginProvider().definitions() | 删除依赖,改用 registerPluginAuthProvider() 入口注入 |
| provider/provider.ts | Instance.state(...) | getIInstanceProvider().state(init) | lazyAsync(createState)(模块级 per-process 单例) |
| config/loader.ts | Instance.state(...) | getIInstanceProvider().state(init) | lazyAsync(createState)(模块级 per-process 单例) |
| provider/models.ts | Global.Path.cache | getIGlobalPathProvider().cache | (同 V3) |
| config/index.ts | Config.get() | getIConfigProvider().get() | (同 V3) |
V4 重构动机:IInstanceProvider.state(init) 把 "缓存" 与 "Instance 抽象" 耦合在一起,
但 state 缓存本质上是 per-process 单例 + 显式 reset,与 Instance.directory 无关。
下层到 @easbot/utils.lazyAsync() 后:
- 解耦:
IInstanceProvider只剩 2 个方法(getDirectory / getWorktree),职责清晰 - 统一:Project 所有需要 state 缓存的子系统(skills / mcp / plugin 等)复用同一工具
- 可测:
lazyAsync().reset()比 mock 一个 state factory 更直接
6. 架构优势
- 零硬编码依赖:
provider/*与model/*文件不直接importagent 包的任何模块 - 测试友好:测试用例用 mock AdapterRegistry 注入,无需启动 agent 实例(
tests/setup.ts已提供完整 mock 范例) - 多入口复用:同一套 LLM 实现可被 web / cli / gateway / monitor 等任何进程复用,只需各自注入 AdapterRegistry
- 契约清晰:与 ADR 0044 (skills) / 0045 (mcp) / 0046 (plugin) 的 AdapterRegistry 模式完全一致
- fail-fast 安全:未注入立即抛错,避免线上静默退化
Provider 路由优先级
Provider 在 createState 时按以下顺序合并 provider 注册表:
- models.dev 远程 catalog:启动时若
Flag.EASBOT_MODELS_FETCH开启,从https://models.dev/api.json拉取(带本地缓存) - 构建时 snapshot:若远程拉取失败或缓存不存在,使用
models-snapshot.ts(构建时静态注入) - CUSTOM_PROVIDER_DEFINITIONS:本仓自研 SDK(
@easbot/ollama-sdk/@easbot/local-model-sdk)的本地定义快照 - config.provider:用户
~/.config/easbot/config.json中的 provider 块覆盖(如自定义 baseURL / model options) - 环境变量 + Auth 持久化:env vars +
easbot auth login持久化的 API key - Plugin 注入:插件可通过
plugin.auth.loader注入特殊 provider(如 github-copilot 的 OAuth) - CUSTOM_LOADERS:每个 provider 的特殊初始化(anthropic headers、azure baseURL、amazon-bedrock AWS credentials 等)
测试
pnpm --filter @easbot/llm test:run测试文件位于 tests/:
provider-smoke.test.ts—— 验证definition.ts的纯常量(白名单 / 本地 SDK 快照 / 友好名映射)+ AdapterRegistry 注入/读取/fail-fast 抛错 + Env/Flag 静态字段 + LLMConfig/ProviderConfig/ProviderAuthMethod zod schemainterfaces.test.ts—— V4 后新增 AdapterRegistry 接口 5 个 + 静态读取函数(readEnvString/readFlagBool/readFlagNumber)+lazyAsync()reset 行为model-smoke.test.ts—— 验证model/types.ts的类型导出路径稳定性setup.ts—— 全局测试环境(设 NODE_ENV=development + 注入 mock AdapterRegistry 全套,避免 chain import 触发 fail-fast)
当前 75 个测试全绿(3 test files)。覆盖范围:
| 测试大类 | 覆盖 case 数 | 验证要点 |
|---------|-------------|----------|
| AdapterRegistry 接口(V4 新增) | 11 | 4 Provider 接口契约 / setAdapterRegistry 注入 / hasAdapterRegistry 布尔 / fail-fast 抛错 / 静态读取函数 |
| Provider 常量纯函数 | 11 | DEFAULT_ENABLED_PROVIDERS / CUSTOM_PROVIDER_DEFINITIONS / PROVIDER_DISPLAY_NAMES |
| ProviderAuth lazyAsync 行为(V4 新增) | 3 | registerPluginAuthProvider 触发 _state.reset() / clearPluginAuthProviders 重置缓存 / ProviderAuth.resetState() 清空缓存 |
| 模型类型导出稳定性 | 6 | RerankInput/Result/Response + Entity/Relation/GraphInput + Note/MemorySummary |
| Env / Flag namespace 静态字段 | 6 | Env 镜像 process.env / Flag 包含 LLM 包内部 flag / 读取函数签名(空字符串 vs undefined) |
| LLMConfig schema | 4 | 空对象通过 / 完整字段通过 / 非法 model 拒绝 / 非数组 enabled_providers 拒绝 |
| ProviderConfig schema | 3 | 最小 config 通过 / partial 设计(所有字段可选) / options.apiKey 类型校验 |
| ProviderAuthMethod schema | 4 | api / oauth 类型通过 / 非法 type 拒绝 / 缺 label 拒绝 |
完整集成测试边界:AdapterRegistry 解耦后,llm 包可独立 mock 全部 4 个 Provider,完整跑 Provider.list / getModel / parseModel 的端到端流程不依赖 agent 包。后续任务:在 agent 包切换 LLM 实现时,补跨包集成测试(验证 setAdapterRegistry 真实注入的 Provider 与 llm 包契约匹配)。
开发命令
# 构建(tsup,输出 ESM + CJS + DTS)
pnpm --filter @easbot/llm build
# 监听模式构建
pnpm --filter @easbot/llm dev
# 重新生成 models.dev snapshot(需联网)
pnpm --filter @easbot/llm build:snapshot
# 类型检查
pnpm --filter @easbot/llm type-check
# Lint(biome)
pnpm --filter @easbot/llm lint
# Lint fix
pnpm --filter @easbot/llm lint:fix
# 单测
pnpm --filter @easbot/llm test:run设计原则
- Provider namespace 是唯一入口:业务代码不应直接 import
@ai-sdk/openai等具体 SDK,统一通过Provider.getModel('openai/gpt-4')取模型 - 配置 vs 注入分离:
Config(持久化配置)和Env/Auth/Plugin(运行时注入)互不混用 - 错误归一化:
ProviderError.parseAPICallError把 25+ 厂商的不同错误格式统一成context_overflow | api_error二分类 - 本地 SDK 优先于远程:本仓自研 SDK(ollama / easbot-local)不出现在 models.dev,因此用
CUSTOM_PROVIDER_DEFINITIONS静态注入,避免远程 catalog 漂移 - 懒加载 SDK:每个 provider 的
BUNDLED_PROVIDERS[].loader()是() => import('@ai-sdk/openai')形式的动态 import,业务代码不引用某 provider 时不下载其 SDK
与历史版本的差异
| 版本 | 关键差异 |
|------|----------|
| 早期占位(@easbot/types 残留) | 仅 5 行注释的 index.ts,无业务能力 |
| V3(ADR 0070) | 新增 src/interfaces.ts(6 Provider 接口 + AdapterRegistry)+ src/config/ 三文件模块;重构 provider/{auth,models,provider,transform}.ts 全部走 AdapterRegistry |
| V4(ADR 0071) | 删除 IInstanceProvider.state()(下沉到 @easbot/utils.lazyAsync())+ 删除 IPluginProvider 依赖(改用模块级 _pluginAuthRegistry + ProviderAuth.registerPluginAuthProvider() 入口);Provider 接口 6→5 个 |
V3 → V4 重构动机:
IInstanceProvider.state()把"per-process 单例 + 显式 reset"与Instance.directory抽象耦合在一起,但 state 缓存与 Instance 路径无关- 缓存下沉到
@easbot/utils.lazyAsync()后,IInstanceProvider收缩为 2 个方法(getDirectory/getWorktree),职责清晰 ProviderAuth不再依赖IPluginProvider,改用模块级 list + 注册入口,概念清晰化(plugin 注册 = 推入 list)
详见:
- ADR 0070 llm-adapter-registry —— AdapterRegistry 注入模式
- ADR 0071 llm-instance-state-decompose —— state 拆分下沉
