@easbot/llm 0.3.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,420 @@
1
+ # @easbot/llm
2
+
3
+ [English](./README.en.md) | 中文
4
+
5
+ `@easbot/llm` 是 EASBot 生态系统的 **AI 模型聚合层**,负责多厂商 SDK 的动态加载、模型路由、配置管理与高层 Model Provider 的能力封装。给业务包(agent / codebase / memory / note / skills / gateway)提供统一的 LLM 调用抽象,避免每个工具包重复实现 provider 路由、auth 管理和错误归一化。
6
+
7
+ ## 当前状态(V4 重构后)
8
+
9
+ 本包已从 `@easbot/types` 的早期占位实现演化为承载 **provider/* + model/* 两套核心模块** 的生产级 AI 网关层:
10
+
11
+ - ✅ **`src/provider/`**(7 文件):完整的工业级 provider 抽象
12
+ - `Provider` 命名空间 —— 25+ 内置 SDK(OpenAI / Anthropic / Azure / Bedrock / Vertex / OpenRouter / Copilot / Ollama / 本地模型等)的动态加载器、`ModelsDev` catalog 合并、模型缓存、SDK 实例池化
13
+ - `ProviderError` 命名空间 —— APICallError 解析(含 25+ 种"上下文溢出"模式正则识别)
14
+ - `ProviderAuth` 命名空间 —— OAuth + API key 双轨认证
15
+ - `ProviderTransform` 命名空间 —— 模型 variant / tool / 限制处理
16
+ - `ModelsDev` 命名空间 —— models.dev catalog 的 zod schema + 加载 + 远程刷新
17
+ - `definition.ts` —— DEFAULT_ENABLED_PROVIDERS 白名单 + CUSTOM_PROVIDER_DEFINITIONS 本地 SDK 快照 + PROVIDER_DISPLAY_NAMES 友好名映射
18
+ - `sdk/copilot/` —— 自研 GitHub Copilot 兼容 SDK(chat + responses + 6 个 tool)
19
+ - ✅ **`src/model/`**(4 命名空间):高层 Model Provider
20
+ - `RerankProvider` 命名空间 —— LLM 驱动的文档重排序(与 Memory 混合搜索权重统一)
21
+ - `GraphProvider` 命名空间 —— 从非结构化文本提取实体关系(11 类 EntityType + 9 类 RelationType)
22
+ - `SummaryProvider` 命名空间 —— note / memory 摘要(LLM-based)
23
+ - `EmbeddingProvider` 命名空间 —— 文本嵌入(单文本 / 批量;走 AI SDK 标准 `embed` / `embedMany`;默认模型 `easbot-local/bge-base-zh-v1.5`)
24
+ - ✅ **`src/config/`**(V3 新增):独立 LLM Config 模块,仿 `packages/gateway/src/config/` 模式
25
+ - `LLMConfig / ProviderConfig / ProviderAuthMethod` —— zod schema(独立可用,也可作为注入 Config 的实现来源)
26
+ - `Config` namespace —— `get()` / `provider(id)`(优先走注入,降级到本地 loader)
27
+ - ✅ **`src/interfaces.ts`**(V3 新增 / V4 精简):**Adapter Registry** + Env / Flag namespace
28
+ - 4 个 Provider 接口(`IConfigProvider` / `IInstanceProvider` / `IGlobalPathProvider` / `IInstallationProvider`;Auth 已下沉到 `@easbot/utils.Auth`)
29
+ - `setAdapterRegistry(...)` —— agent 包启动时一次注入,**完全解耦**本包对 agent 内部模块的所有依赖
30
+ - `getI<X>Provider()` 便捷访问器 —— 未注入时 fail-fast 抛错(**不静默退化**)
31
+ - `readEnvString / readFlagBool / readFlagNumber` —— 静态读取,避免对注入的依赖
32
+ - `Env / Flag` namespace —— 镜像 `process.env` 的静态字面(用于 IDE 提示 / 文档生成 / 配置检查)
33
+ - ✅ **`src/index.ts`** —— 聚合导出入口(4 provider 命名空间 + 3 model 命名空间 + 自研 Copilot SDK + 3 常量 + Config / interfaces 接入点 + AdapterRegistry + 4 Provider 接口)
34
+
35
+ ## 核心特性
36
+
37
+ - **25+ 内置 AI SDK 动态加载**:按需 import,避免打包产物膨胀
38
+ - **三层数据源融合**:models.dev 远程 catalog + 构建时 snapshot + 本地 CUSTOM_PROVIDER_DEFINITIONS(用于 ollama / easbot-local 这类不出现在 models.dev 的本仓自研 SDK)
39
+ - **统一 Provider 命名空间**:`Provider.list() / getModel() / parseModel()` 一行调用,与具体 SDK 解耦
40
+ - **OAuth + API key 双轨**:`ProviderAuth` namespace 管理 OAuth flow / API key 持久化 / 注入到 SDK options
41
+ - **Context Overflow 自动识别**:跨厂商统一的"上下文超限"语义(25+ 正则模式 + 4 种 HTTP status 兜底)
42
+ - **自研 GitHub Copilot 兼容 SDK**:完整实现 chat + responses API + 6 个 tool(code-interpreter / file-search / web-search / image-generation / local-shell / web-search-preview)
43
+ - **持久化模型缓存**:构建时生成 `models-snapshot.ts`,运行时优先用本地,避免冷启动连网
44
+ - **极简聚合导出**:`@easbot/llm` 一行引入 5+3 个命名空间 + 自研 SDK + 关键常量
45
+
46
+ ## 安装
47
+
48
+ ```bash
49
+ pnpm add @easbot/llm
50
+ ```
51
+
52
+ ## 快速上手
53
+
54
+ ### 1. 列出所有已配置的 provider
55
+
56
+ ```typescript
57
+ import { Provider } from '@easbot/llm';
58
+
59
+ const allProviders = await Provider.list();
60
+ // { openai: { id, name, source, env, models, ... }, anthropic: {...}, ... }
61
+ ```
62
+
63
+ ### 2. 获取特定模型(按 `provider/model` 字符串路由)
64
+
65
+ ```typescript
66
+ import { Provider } from '@easbot/llm';
67
+
68
+ const gpt4 = await Provider.getModel('openai/gpt-4');
69
+ // gpt4: LanguageModelV3(AI SDK v6 规范)
70
+
71
+ const embed = await Provider.getModel('openai/text-embedding-3-small');
72
+ // embed: EmbeddingModelV3
73
+
74
+ const rerank = await Provider.getModel('easbot-local/rerank-proxy');
75
+ // rerank: RerankingModelV3
76
+ ```
77
+
78
+ ### 3. 解析模型 ID(安全拆分,不抛错)
79
+
80
+ ```typescript
81
+ import { Provider } from '@easbot/llm';
82
+
83
+ const { providerId, modelId } = Provider.parseModel('openai/gpt-4o');
84
+ // { providerId: 'openai', modelId: 'gpt-4o' }
85
+ ```
86
+
87
+ ### 4. 解析 API 错误(含 context overflow 识别)
88
+
89
+ ```typescript
90
+ import { ProviderError, APICallError } from 'ai';
91
+
92
+ try {
93
+ await someProvider.doCall(...);
94
+ } catch (e) {
95
+ if (e instanceof APICallError) {
96
+ const parsed = ProviderError.parseAPICallError({ providerId: 'openai', error: e });
97
+ if (parsed.type === 'context_overflow') {
98
+ // UI: "上下文超限,请缩短输入或切换模型"
99
+ }
100
+ }
101
+ }
102
+ ```
103
+
104
+ ### 5. 高层 Model Provider 直接调用
105
+
106
+ ```typescript
107
+ import { RerankProvider, GraphProvider, SummaryProvider } from '@easbot/llm';
108
+
109
+ // 文档重排序
110
+ const reranked = await RerankProvider.rerank({
111
+ query: '...',
112
+ documents: ['...', '...'],
113
+ });
114
+
115
+ // 图谱实体提取
116
+ const graph = await GraphProvider.generateGraph({
117
+ chunks: ['chunk1...', 'chunk2...', '...'], // 任意长度数组
118
+ prompt: { maxChunks: 5, maxConcurrency: 3 }, // v0.8:per-call 切片 + 3 并发
119
+ });
120
+ // graph.entities / relations / summary 直接入库
121
+ ```
122
+
123
+ **v0.8 行为契约**(`packages/llm/src/model/graph.ts`):
124
+
125
+ - **所有 chunks 都被尝试抽取**:chunks 数组按 `prompt.maxChunks`(默认 5)切片成多组,每组调一次 LLM;不再像 v0.7 那样硬截断到前 5 个 chunk。
126
+ - **chunk 级 3 并发**:多组用轻量级 semaphore 并发跑(`prompt.maxConcurrency`,默认 3),避免触发 provider 限流。
127
+ - **局部失败容错**:单组 LLM 失败 → 仅丢该组结果,其他组继续。不会因一次失败让整个文档 KG 留空。
128
+ - **边界感知截断**:超长 chunk 走 `splitByBoundary` 进一步切分(按 `\n\n` / `\n` / 空白找最近边界),保证 entity name 不被切碎。
129
+
130
+ ```typescript
131
+ // Note 知识库摘要
132
+ const summary = await SummaryProvider.summarizeNote({
133
+ query: '...',
134
+ chunks: [...],
135
+ });
136
+ ```
137
+
138
+ ### 6. 文本嵌入(EmbeddingProvider)
139
+
140
+ ```typescript
141
+ import { EmbeddingProvider } from '@easbot/llm';
142
+
143
+ // 单文本嵌入 — 返回 EmbedOutput(含 embedding + usage.tokens)
144
+ const single = await EmbeddingProvider.embedText({ value: '一段中文文本' });
145
+ // single.embedding: number[](默认 bge-base-zh-v1.5 输出 512 维)
146
+ // single.usage.tokens: number
147
+
148
+ // 批量嵌入(推荐:单次 API 调用比 N 次 aiEmbed 更高效)
149
+ const batch = await EmbeddingProvider.embedTexts({ values: ['a', 'b', 'c'] });
150
+ // batch.embeddings: number[][](与 values 一一对应)
151
+ // batch.usage.tokens: number
152
+
153
+ // 显式指定模型(跳过 Provider.defaultModel)
154
+ import { Provider } from '@easbot/llm';
155
+ const customModel = Provider.parseModel('openai/text-embedding-3-small');
156
+ const r = await EmbeddingProvider.embedText({ value: 'hi', model: customModel });
157
+ ```
158
+
159
+ ### 7. 使用自研 GitHub Copilot SDK
160
+
161
+ ```typescript
162
+ import { createOpenaiCompatible, openaiCompatible } from '@easbot/llm';
163
+
164
+ const copilot = createOpenaiCompatible({
165
+ apiKey: process.env.GITHUB_TOKEN,
166
+ });
167
+ const model = copilot('gpt-4o');
168
+ // 或使用默认实例
169
+ const model2 = openaiCompatible('gpt-4o');
170
+ ```
171
+
172
+ ## 顶层导出清单
173
+
174
+ ```typescript
175
+ // Provider 核心命名空间
176
+ export * as Provider from './provider/provider';
177
+ export * as ProviderError from './provider/error';
178
+ export * as ProviderAuth from './provider/auth';
179
+ export * as ProviderTransform from './provider/transform';
180
+ export * as ModelsDev from './provider/models';
181
+
182
+ // Provider 常量
183
+ export {
184
+ DEFAULT_ENABLED_PROVIDERS,
185
+ CUSTOM_PROVIDER_DEFINITIONS,
186
+ PROVIDER_DISPLAY_NAMES,
187
+ } from './provider/definition';
188
+
189
+ // 顶层函数
190
+ export { initModelsRefresh } from './provider/models';
191
+
192
+ // 自研 GitHub Copilot SDK
193
+ export {
194
+ createOpenaiCompatible,
195
+ openaiCompatible,
196
+ } from './provider/sdk/copilot';
197
+ export type {
198
+ OpenaiCompatibleProvider,
199
+ OpenaiCompatibleProviderSettings,
200
+ OpenaiCompatibleModelId,
201
+ } from './provider/sdk/copilot';
202
+
203
+ // 高层 Model Provider 命名空间
204
+ export * as RerankProvider from './model/rerank';
205
+ export * as GraphProvider from './model/graph';
206
+ export * as SummaryProvider from './model/summary';
207
+ export * as EmbeddingProvider from './model/embedding';
208
+ ```
209
+
210
+ **EmbeddingProvider 公开契约**(`packages/llm/src/model/embedding.ts`):
211
+
212
+ - `embedText(input, abortSignal?) → Promise<EmbedOutput>`
213
+ - `EmbedOutput = { embedding: number[]; usage: { tokens: number } }`
214
+ - 空 `value` 返回 `{ embedding: [], usage: { tokens: 0 } }`,**不**调用 `ai.embed`
215
+ - `embedTexts(inputs, abortSignal?) → Promise<EmbedBatchOutput>`
216
+ - `EmbedBatchOutput = { embeddings: number[][]; usage: { tokens: number } }`
217
+ - 空 `values` 返回 `{ embeddings: [], usage: { tokens: 0 } }`,**不**调用 `ai.embedMany`
218
+ - 默认模型:`easbot-local/bge-base-zh-v1.5`(导出常量 `EmbeddingProvider.DEFAULT_MODEL`)
219
+ - `model` 入参支持三种形态:省略 → `Provider.defaultModel` 链路;`Provider.Model` → `Provider.getEmbedding`;直接传 `EmbeddingModel` 实例(duck-type 由 `specificationVersion` 字段判定)→ 跳过 Provider 链路
220
+
221
+ ## 配置注入(切换 LLM 实现的迁移指南)
222
+
223
+ V4 重构后,`Provider` / `ModelsDev` / `ProviderAuth` / `ProviderTransform` 命名空间已通过 **Adapter Registry** 模式与 agent 包完全解耦,**不再直接 `import` agent 包的任何模块**。所有运行时依赖都通过 4 个 Provider 接口显式注入(Auth 已下沉到 `@easbot/utils.Auth`)。
224
+
225
+ ### 1. Adapter Registry 总览
226
+
227
+ agent 包启动时调用一次 `setAdapterRegistry(...)`,注入 4 个 Provider 实现:
228
+
229
+ ```typescript
230
+ import {
231
+ setAdapterRegistry,
232
+ type AdapterRegistry,
233
+ } from '@easbot/llm';
234
+
235
+ // 在 agent 包启动入口处(例如 Instance.provide 之前)
236
+ setAdapterRegistry({
237
+ config: Config, // IConfigProvider — get / provider
238
+ instance: Instance, // IInstanceProvider — getDirectory / getWorktree
239
+ global: Global.Path, // IGlobalPathProvider — cache / data / config
240
+ installation: Installation, // IInstallationProvider — getVersion
241
+ // V4 起 Auth 已下沉到 `@easbot/utils.Auth`,不再通过 AdapterRegistry 注入。
242
+ // LLM 包内部直接 `import { Auth } from '@easbot/utils'` 调用即可。
243
+ } satisfies AdapterRegistry);
244
+ ```
245
+
246
+ **未注入时,所有 `getI<X>Provider()` 会 fail-fast 抛错**:
247
+
248
+ ```
249
+ [LLM] adapter 'config' (IConfigProvider) not registered.
250
+ Call setAdapterRegistry({ config: ... }) before using LLM.
251
+ ```
252
+
253
+ > 设计意图:**不静默退化**。若忘注入就抛错,避免线上"undefined config"导致模型路由静默失效。
254
+
255
+ ### 2. 4 个 Provider 接口契约
256
+
257
+ | 接口 | 方法 | 原 agent 模块 | 用途 |
258
+ |------|------|---------------|------|
259
+ | `IConfigProvider` | `get<T>(): Promise<T>` / `provider<T>(id): Promise<T \| undefined>` | `Config` | 用户 `~/.config/easbot/config.json` 的 llm 块(whitelist / blacklist / variant override) |
260
+ | `IInstanceProvider` | `getDirectory(): string` / `getWorktree(): string` | `Instance` | 实例路径与 worktree 信息 |
261
+ | `IGlobalPathProvider` | `cache: string` / `data: string` / `config: string` | `Global.Path` | 模型快照缓存目录 / 用户数据目录 / 全局配置目录 |
262
+ | `IInstallationProvider` | `getVersion(): string` | `Installation` | User-Agent / 兼容性检查 / 版本信息 |
263
+
264
+ > **V4 变更**:`IInstanceProvider.state(init)` 与 `IPluginProvider` 已被**拆分下沉**(ADR 0071)。
265
+ > - Provider 内部 state 缓存改用 `@easbot/utils.lazyAsync(init)`(per-process 单例 + 显式 reset)
266
+ > - Plugin 注册通过模块级 `_pluginAuthRegistry: Map<providerId, methods[]>` + `ProviderAuth.registerPluginAuthProvider()` 入口注入
267
+
268
+ ### 3. LLM Config(独立工作 + 集成工作)
269
+
270
+ V3 新增 `src/config/` 模块,仿 `packages/gateway/src/config/` 模式:
271
+
272
+ - **独立工作**:从 `easbot.json` / `easbot.jsonc` 直接读 `llm` 字段(模型、provider、auth 等)
273
+ - **集成工作**:当 AdapterRegistry 已注入时,`Config.get()` 优先走注入的实现(让 agent 包能复用)
274
+
275
+ ```typescript
276
+ import { Config, LLMConfig } from '@easbot/llm';
277
+
278
+ const cfg = await Config.get(); // 走注入或 fallback loader
279
+ const openaiCfg = await Config.provider('openai');
280
+
281
+ // Schema 校验(可单独使用)
282
+ const parsed = LLMConfig.parse({
283
+ model: 'openai/gpt-4o',
284
+ provider: {
285
+ openai: {
286
+ npm: '@ai-sdk/openai',
287
+ options: { apiKey: 'sk-...' },
288
+ },
289
+ },
290
+ });
291
+ ```
292
+
293
+ ### 4. Env / Flag namespace(镜像 process.env)
294
+
295
+ `Flag` namespace 与读取函数 (`readEnvString` / `readFlagBool`) 仍由 llm 包提供;`Env` 已下沉到 `@easbot/utils`,llm 仅透传 re-export,调用方也可直接 import `@easbot/utils` 的 `Env`:
296
+
297
+ ```typescript
298
+ import { Flag, readEnvString, readFlagBool } from '@easbot/llm';
299
+ import { Env } from '@easbot/llm'; // 等价于 import { Env } from '@easbot/utils'
300
+
301
+ Flag.EASBOT_OUTPUT_TOKEN_MAX; // 数值字段,内部动态计算
302
+ readEnvString('OPENAI_API_KEY'); // 包装 process.env
303
+ ```
304
+
305
+ ### 5. 调用点改造对照表
306
+
307
+ | 模块 | V3 之前 | V3 | V4(本次重构) |
308
+ |------|--------|----|----------------|
309
+ | `provider/auth.ts` | `Instance.state(...)` | `getIInstanceProvider().state(init)` | `lazyAsync(init)` + `_pluginAuthRegistry` 模块级 list |
310
+ | `provider/auth.ts` | `Auth.set(providerId, info)` | `Auth.set(...)` 直接调用 `@easbot/utils.Auth`(下沉到 utils) | `Auth.set(...)` 直接调用 `@easbot/utils.Auth`(下沉到 utils) |
311
+ | `provider/auth.ts` | `Plugin.definitions()` | `getIPluginProvider().definitions()` | 删除依赖,改用 `registerPluginAuthProvider()` 入口注入 |
312
+ | `provider/provider.ts` | `Instance.state(...)` | `getIInstanceProvider().state(init)` | `lazyAsync(createState)`(模块级 per-process 单例) |
313
+ | `config/loader.ts` | `Instance.state(...)` | `getIInstanceProvider().state(init)` | `lazyAsync(createState)`(模块级 per-process 单例) |
314
+ | `provider/models.ts` | `Global.Path.cache` | `getIGlobalPathProvider().cache` | (同 V3) |
315
+ | `config/index.ts` | `Config.get()` | `getIConfigProvider().get()` | (同 V3) |
316
+
317
+ **V4 重构动机**:`IInstanceProvider.state(init)` 把 "缓存" 与 "Instance 抽象" 耦合在一起,
318
+ 但 state 缓存本质上是 **per-process 单例 + 显式 reset**,与 Instance.directory 无关。
319
+ 下层到 `@easbot/utils.lazyAsync()` 后:
320
+ - **解耦**:`IInstanceProvider` 只剩 2 个方法(getDirectory / getWorktree),职责清晰
321
+ - **统一**:Project 所有需要 state 缓存的子系统(skills / mcp / plugin 等)复用同一工具
322
+ - **可测**:`lazyAsync().reset()` 比 mock 一个 state factory 更直接
323
+
324
+ ### 6. 架构优势
325
+
326
+ - **零硬编码依赖**:`provider/*` 与 `model/*` 文件不直接 `import` agent 包的任何模块
327
+ - **测试友好**:测试用例用 mock AdapterRegistry 注入,无需启动 agent 实例(`tests/setup.ts` 已提供完整 mock 范例)
328
+ - **多入口复用**:同一套 LLM 实现可被 web / cli / gateway / monitor 等任何进程复用,只需各自注入 AdapterRegistry
329
+ - **契约清晰**:与 ADR 0044 (skills) / 0045 (mcp) / 0046 (plugin) 的 AdapterRegistry 模式完全一致
330
+ - **fail-fast 安全**:未注入立即抛错,避免线上静默退化
331
+
332
+ ## Provider 路由优先级
333
+
334
+ `Provider` 在 `createState` 时按以下顺序合并 provider 注册表:
335
+
336
+ 1. **models.dev 远程 catalog**:启动时若 `Flag.EASBOT_MODELS_FETCH` 开启,从 `https://models.dev/api.json` 拉取(带本地缓存)
337
+ 2. **构建时 snapshot**:若远程拉取失败或缓存不存在,使用 `models-snapshot.ts`(构建时静态注入)
338
+ 3. **CUSTOM_PROVIDER_DEFINITIONS**:本仓自研 SDK(`@easbot/ollama-sdk` / `@easbot/local-model-sdk`)的本地定义快照
339
+ 4. **config.provider**:用户 `~/.config/easbot/config.json` 中的 provider 块覆盖(如自定义 baseURL / model options)
340
+ 5. **环境变量 + Auth 持久化**:env vars + `easbot auth login` 持久化的 API key
341
+ 6. **Plugin 注入**:插件可通过 `plugin.auth.loader` 注入特殊 provider(如 github-copilot 的 OAuth)
342
+ 7. **CUSTOM_LOADERS**:每个 provider 的特殊初始化(anthropic headers、azure baseURL、amazon-bedrock AWS credentials 等)
343
+
344
+ ## 测试
345
+
346
+ ```bash
347
+ pnpm --filter @easbot/llm test:run
348
+ ```
349
+
350
+ 测试文件位于 `tests/`:
351
+
352
+ - `provider-smoke.test.ts` —— 验证 `definition.ts` 的纯常量(白名单 / 本地 SDK 快照 / 友好名映射)+ **AdapterRegistry 注入/读取/fail-fast 抛错** + Env/Flag 静态字段 + LLMConfig/ProviderConfig/ProviderAuthMethod zod schema
353
+ - `interfaces.test.ts` —— V4 后新增 AdapterRegistry 接口 5 个 + 静态读取函数(`readEnvString` / `readFlagBool` / `readFlagNumber`)+ `lazyAsync()` reset 行为
354
+ - `model-smoke.test.ts` —— 验证 `model/types.ts` 的类型导出路径稳定性
355
+ - `setup.ts` —— 全局测试环境(设 NODE_ENV=development + **注入 mock AdapterRegistry 全套**,避免 chain import 触发 fail-fast)
356
+
357
+ **当前 75 个测试全绿**(3 test files)。覆盖范围:
358
+
359
+ | 测试大类 | 覆盖 case 数 | 验证要点 |
360
+ |---------|-------------|----------|
361
+ | AdapterRegistry 接口(V4 新增) | 11 | 4 Provider 接口契约 / `setAdapterRegistry` 注入 / `hasAdapterRegistry` 布尔 / fail-fast 抛错 / 静态读取函数 |
362
+ | Provider 常量纯函数 | 11 | DEFAULT_ENABLED_PROVIDERS / CUSTOM_PROVIDER_DEFINITIONS / PROVIDER_DISPLAY_NAMES |
363
+ | ProviderAuth lazyAsync 行为(V4 新增) | 3 | `registerPluginAuthProvider` 触发 `_state.reset()` / `clearPluginAuthProviders` 重置缓存 / `ProviderAuth.resetState()` 清空缓存 |
364
+ | 模型类型导出稳定性 | 6 | RerankInput/Result/Response + Entity/Relation/GraphInput + Note/MemorySummary |
365
+ | Env / Flag namespace 静态字段 | 6 | Env 镜像 process.env / Flag 包含 LLM 包内部 flag / 读取函数签名(空字符串 vs undefined) |
366
+ | LLMConfig schema | 4 | 空对象通过 / 完整字段通过 / 非法 model 拒绝 / 非数组 enabled_providers 拒绝 |
367
+ | ProviderConfig schema | 3 | 最小 config 通过 / partial 设计(所有字段可选) / options.apiKey 类型校验 |
368
+ | ProviderAuthMethod schema | 4 | api / oauth 类型通过 / 非法 type 拒绝 / 缺 label 拒绝 |
369
+
370
+ **完整集成测试边界**:AdapterRegistry 解耦后,llm 包可独立 mock 全部 4 个 Provider,完整跑 `Provider.list / getModel / parseModel` 的端到端流程不依赖 agent 包。后续任务:在 agent 包切换 LLM 实现时,补跨包集成测试(验证 setAdapterRegistry 真实注入的 Provider 与 llm 包契约匹配)。
371
+
372
+ ## 开发命令
373
+
374
+ ```bash
375
+ # 构建(tsup,输出 ESM + CJS + DTS)
376
+ pnpm --filter @easbot/llm build
377
+
378
+ # 监听模式构建
379
+ pnpm --filter @easbot/llm dev
380
+
381
+ # 重新生成 models.dev snapshot(需联网)
382
+ pnpm --filter @easbot/llm build:snapshot
383
+
384
+ # 类型检查
385
+ pnpm --filter @easbot/llm type-check
386
+
387
+ # Lint(biome)
388
+ pnpm --filter @easbot/llm lint
389
+
390
+ # Lint fix
391
+ pnpm --filter @easbot/llm lint:fix
392
+
393
+ # 单测
394
+ pnpm --filter @easbot/llm test:run
395
+ ```
396
+
397
+ ## 设计原则
398
+
399
+ 1. **Provider namespace 是唯一入口**:业务代码不应直接 import `@ai-sdk/openai` 等具体 SDK,统一通过 `Provider.getModel('openai/gpt-4')` 取模型
400
+ 2. **配置 vs 注入分离**:`Config`(持久化配置)和 `Env/Auth/Plugin`(运行时注入)互不混用
401
+ 3. **错误归一化**:`ProviderError.parseAPICallError` 把 25+ 厂商的不同错误格式统一成 `context_overflow | api_error` 二分类
402
+ 4. **本地 SDK 优先于远程**:本仓自研 SDK(ollama / easbot-local)不出现在 models.dev,因此用 `CUSTOM_PROVIDER_DEFINITIONS` 静态注入,避免远程 catalog 漂移
403
+ 5. **懒加载 SDK**:每个 provider 的 `BUNDLED_PROVIDERS[].loader()` 是 `() => import('@ai-sdk/openai')` 形式的动态 import,业务代码不引用某 provider 时不下载其 SDK
404
+
405
+ ## 与历史版本的差异
406
+
407
+ | 版本 | 关键差异 |
408
+ |------|----------|
409
+ | **早期占位(@easbot/types 残留)** | 仅 5 行注释的 index.ts,无业务能力 |
410
+ | **V3(ADR 0070)** | 新增 `src/interfaces.ts`(6 Provider 接口 + AdapterRegistry)+ `src/config/` 三文件模块;重构 provider/{auth,models,provider,transform}.ts 全部走 AdapterRegistry |
411
+ | **V4(ADR 0071)** | 删除 `IInstanceProvider.state()`(下沉到 `@easbot/utils.lazyAsync()`)+ 删除 `IPluginProvider` 依赖(改用模块级 `_pluginAuthRegistry` + `ProviderAuth.registerPluginAuthProvider()` 入口);Provider 接口 6→5 个 |
412
+
413
+ **V3 → V4 重构动机**:
414
+ - `IInstanceProvider.state()` 把"per-process 单例 + 显式 reset"与 `Instance.directory` 抽象耦合在一起,但 state 缓存与 Instance 路径无关
415
+ - 缓存下沉到 `@easbot/utils.lazyAsync()` 后,`IInstanceProvider` 收缩为 2 个方法(`getDirectory` / `getWorktree`),职责清晰
416
+ - `ProviderAuth` 不再依赖 `IPluginProvider`,改用模块级 list + 注册入口,概念清晰化(plugin 注册 = 推入 list)
417
+
418
+ 详见:
419
+ - [ADR 0070 llm-adapter-registry](../../docs/decisions/0070-llm-adapter-registry.md) —— AdapterRegistry 注入模式
420
+ - [ADR 0071 llm-instance-state-decompose](../../docs/decisions/0071-llm-instance-state-decompose.md) —— state 拆分下沉
@@ -0,0 +1,54 @@
1
+ # Knowledge Graph Summary Prompt
2
+
3
+ ## Task
4
+ Generate ONE combined summary from MULTIPLE knowledge graph entities.
5
+
6
+ **CRITICAL RULES:**
7
+ - NEVER generate relationships not present in the input
8
+ - NEVER invent connections or attributes
9
+ - NEVER elaborate beyond provided information
10
+ - NEVER guess entity properties
11
+ - If input has no valuable information: ONLY output "No relevant knowledge found."
12
+
13
+ ## Input Format
14
+ ```
15
+ [Entity 1]
16
+ Entity: {entity1}
17
+ Type: {entity_type1}
18
+ Description: {description1}
19
+ Related Entities: {related_entities1}
20
+ Relation Types: {relation_types1}
21
+
22
+ [Entity 2]
23
+ Entity: {entity2}
24
+ ...
25
+ ```
26
+
27
+ ## Output Format
28
+ **Length: 1-3 entities maximum**
29
+ - Lead with most relevant entities
30
+ - Group information clearly
31
+ - Cite source as "Knowledge Graph"
32
+
33
+ ```markdown
34
+ ## [Entity Name] (Priority: {priority})
35
+ **Type:** {entity_type}
36
+ **Source:** Knowledge Graph
37
+ **Description:** ...
38
+ **Related Entities:**
39
+ → [{relation}] → {target_entity}
40
+ ```
41
+
42
+ ## Fixed Response
43
+ If entities are irrelevant or insufficient:
44
+ ```
45
+ No relevant knowledge found.
46
+ ```
47
+ **DO NOT generate any other content.**
48
+
49
+ ## Rules
50
+ - Output ONE combined summary only
51
+ - Never separate summaries per entity
52
+ - Never invent attributes or relationships
53
+ - Never exceed provided information
54
+ - If uncertain: "No relevant knowledge found."
@@ -0,0 +1,81 @@
1
+ # Knowledge Summary Prompt
2
+
3
+ ## Task
4
+ Synthesize document chunks into a concise summary relevant to the query.
5
+
6
+ **CRITICAL RULES:**
7
+ - NEVER generate content not present in the input
8
+ - NEVER invent relationships or connections
9
+ - NEVER elaborate beyond provided information
10
+ - NEVER guess or assume user needs
11
+ - If input has no valuable information: ONLY output "No relevant knowledge found."
12
+
13
+ ## Priority Score
14
+ Priority = 0.6 × relevance + 0.4 × recency
15
+ - Priority > 0.6: high-priority (include)
16
+ - Priority < 0.3: low-priority (skip unless unique value)
17
+
18
+ ## Input Format
19
+ [Chunk N] Priority: {priority}
20
+ Source: [{filename}](./{filename}#{startLine}-{endLine})
21
+ {content}
22
+ Graph Nodes:
23
+ Node: {name} ({type}) | {properties}
24
+ → [{relation}] → target={targetType}:{targetName}
25
+
26
+ ## Output Format
27
+
28
+ ### Content Summary (REQUIRED)
29
+ **Length: 1-3 sentences maximum**
30
+ - Lead with highest-priority chunks ONLY
31
+ - Skip chunks Priority < 0.3
32
+ - Include lower-priority chunks only if they add unique value
33
+ - Cite sources: [{filename}](./{filename}#{startLine})
34
+
35
+ ### Entity & Relation Map (CONDITIONAL)
36
+ Only include if:
37
+ 1. Graph nodes are present AND
38
+ 2. They add meaningful connections not in Content Summary
39
+
40
+ Format:
41
+ **{EntityName}** ({type})
42
+ → [{relation}] → {TargetEntity} ({targetType})
43
+
44
+ ### Key Insights (CONDITIONAL)
45
+ Only include if:
46
+ 1. Notable patterns exist AND
47
+ 2. They are NOT already covered in Content Summary
48
+
49
+ Format:
50
+ - 1-2 bullets maximum
51
+
52
+ ## Fixed Response
53
+ If no chunks have Priority > 0.3 or all chunks are irrelevant:
54
+ ```
55
+ No relevant knowledge found.
56
+ ```
57
+ **DO NOT generate any other content.**
58
+
59
+ ## Examples
60
+
61
+ ### Good Output (relevant chunks)
62
+ ```
63
+ The codebase uses React for frontend (Priority: 0.78).
64
+ Source: [frontend/index.tsx](./frontend/index.tsx#L10-L50)
65
+ ```
66
+
67
+ ### Bad Output (NEVER generate)
68
+ ```
69
+ Hello! Based on the documents, I can help you with React setup.
70
+ The project structure follows a standard pattern.
71
+ You should use TypeScript for type safety.
72
+ ```
73
+ (These weren't in the input!)
74
+
75
+ ## Strict Rules
76
+ - Output ONLY content from input chunks
77
+ - Skip all sections with no data
78
+ - Maximum 3 sentences in Content Summary
79
+ - NEVER exceed provided information
80
+ - Cite sources when including facts
81
+ - If uncertain: "No relevant knowledge found."
@@ -0,0 +1,80 @@
1
+ # Memory Summary Prompt
2
+
3
+ ## Task
4
+ Synthesize memory facts into a concise summary relevant to the query.
5
+
6
+ **CRITICAL RULES:**
7
+ - NEVER generate facts not present in the input
8
+ - NEVER invent content or relationships
9
+ - NEVER elaborate beyond provided information
10
+ - NEVER guess or assume user intent
11
+ - If input has no valuable information: ONLY output "No relevant memory found."
12
+
13
+ ## Priority Score
14
+ Priority = 0.4 × relevance + 0.3 × recency + 0.3 × importance
15
+ - Priority > 0.6: high-priority (include)
16
+ - Priority < 0.3: low-priority (skip unless unique value)
17
+
18
+ ## Input Format
19
+ [Fact N] Priority: {priority}
20
+ Category: {category} | Importance: {importance} | Source: {source}
21
+ {content}
22
+ Graph Nodes:
23
+ Node: {name} ({type})
24
+ → [{relation}] → target={targetType}:{targetName}
25
+
26
+ ## Output Format
27
+
28
+ ### Content Summary (REQUIRED)
29
+ **Length: 1-3 sentences maximum**
30
+ - Lead with highest-priority facts ONLY
31
+ - Skip facts Priority < 0.3
32
+ - Include low-priority facts only if they add unique value not in high-priority facts
33
+ - State contradictions clearly: "High-priority fact X contradicts low-priority fact Y. Current truth: X."
34
+
35
+ ### Entity & Relation Map (CONDITIONAL)
36
+ Only include if:
37
+ 1. Graph nodes are present AND
38
+ 2. They add meaningful connections not in Content Summary
39
+
40
+ Format:
41
+ **{EntityName}** ({type})
42
+ → [{relation}] → {TargetEntity} ({targetType})
43
+
44
+ ### Key Insights (CONDITIONAL)
45
+ Only include if:
46
+ 1. Meaningful patterns exist AND
47
+ 2. They are NOT already covered in Content Summary
48
+
49
+ Format:
50
+ - 1-2 bullets maximum
51
+
52
+ ## Fixed Response
53
+ If no facts have Priority > 0.3 or all facts are irrelevant:
54
+ ```
55
+ No relevant memory found.
56
+ ```
57
+ **DO NOT generate any other content.**
58
+
59
+ ## Examples
60
+
61
+ ### Good Output (relevant facts)
62
+ ```
63
+ The user prefers dark mode (Priority: 0.72).
64
+ High-priority fact contradicts earlier light mode preference.
65
+ ```
66
+
67
+ ### Bad Output (NEVER generate)
68
+ ```
69
+ Hello! I can help you with dark mode settings.
70
+ The user's name is John and they work at Company X.
71
+ They like TypeScript and use VSCode daily.
72
+ ```
73
+ (These facts weren't in the input!)
74
+
75
+ ## Strict Rules
76
+ - Output ONLY facts from input
77
+ - Skip all sections with no data
78
+ - Maximum 3 sentences in Content Summary
79
+ - NEVER exceed provided information
80
+ - If uncertain: "No relevant memory found."