@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/LICENSE +21 -0
- package/README.en.md +419 -0
- package/README.md +420 -0
- package/dist/assets/txt/model/graph-summary.txt +54 -0
- package/dist/assets/txt/model/knowledge-summary.txt +81 -0
- package/dist/assets/txt/model/memory-summary.txt +80 -0
- package/dist/assets/txt/model/summary.txt +53 -0
- package/dist/chunks/chunk-3BANRW36.mjs +2 -0
- package/dist/chunks/chunk-3IYCLIOJ.cjs +1 -0
- package/dist/chunks/chunk-S4DPVKMV.mjs +1 -0
- package/dist/chunks/chunk-YDIWY3EH.cjs +2 -0
- package/dist/chunks/copilot-5N6U442L.cjs +1 -0
- package/dist/chunks/copilot-TJATIV2S.mjs +1 -0
- package/dist/chunks/models-snapshot-3EXCDFMS.mjs +2 -0
- package/dist/chunks/models-snapshot-U7TQPEPU.cjs +2 -0
- package/dist/index.cjs +73 -0
- package/dist/index.d.cts +1903 -0
- package/dist/index.d.ts +1903 -0
- package/dist/index.mjs +73 -0
- package/package.json +113 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 houjallen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.en.md
ADDED
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
# @easbot/llm
|
|
2
|
+
|
|
3
|
+
English | [中文](./README.md)
|
|
4
|
+
|
|
5
|
+
`@easbot/llm` is the **AI model aggregation layer** of the EASBot ecosystem. It handles dynamic loading of multi-vendor SDKs, model routing, configuration management, and high-level Model Provider capability wrapping. It provides business packages (agent / codebase / memory / note / skills / gateway) with a unified LLM calling abstraction, avoiding the duplication of provider routing, auth management, and error normalization across every tool package.
|
|
6
|
+
|
|
7
|
+
## Current Status (after V4 refactor)
|
|
8
|
+
|
|
9
|
+
This package has evolved from an early placeholder of `@easbot/types` into a production-grade AI gateway layer that hosts two core modules: `provider/*` and `model/*`:
|
|
10
|
+
|
|
11
|
+
- ✅ **`src/provider/`** (19 files): complete industrial-grade provider abstraction
|
|
12
|
+
- `Provider` namespace — 25+ built-in SDK (OpenAI / Anthropic / Azure / Bedrock / Vertex / OpenRouter / Copilot / Ollama / local models, etc.) dynamic loaders, `ModelsDev` catalog merging, model caching, SDK instance pooling
|
|
13
|
+
- `ProviderError` namespace — APICallError parsing (including 25+ "context overflow" regex patterns)
|
|
14
|
+
- `ProviderAuth` namespace — OAuth + API key dual-track authentication
|
|
15
|
+
- `ProviderTransform` namespace — model variant / tool / limit handling
|
|
16
|
+
- `ModelsDev` namespace — models.dev catalog zod schema + loading + remote refresh
|
|
17
|
+
- `definition.ts` — DEFAULT_ENABLED_PROVIDERS whitelist + CUSTOM_PROVIDER_DEFINITIONS local SDK snapshot + PROVIDER_DISPLAY_NAMES friendly name mapping
|
|
18
|
+
- `sdk/copilot/` — self-developed GitHub Copilot-compatible SDK (chat + responses + 6 tools)
|
|
19
|
+
- ✅ **`src/model/`** (4 namespaces): high-level Model Providers
|
|
20
|
+
- `RerankProvider` namespace — LLM-driven document reranking (unified with Memory hybrid search weights)
|
|
21
|
+
- `GraphProvider` namespace — extract entities and relations from unstructured text (11 EntityType + 9 RelationType)
|
|
22
|
+
- `SummaryProvider` namespace — note / memory summarization (LLM-based)
|
|
23
|
+
- ✅ **`src/config/`** (V3 new): independent LLM Config module, mirroring `packages/gateway/src/config/` pattern
|
|
24
|
+
- `LLMConfig / ProviderConfig / ProviderAuthMethod` — zod schemas (standalone usable, or as source-of-truth for injected Config)
|
|
25
|
+
- `Config` namespace — `get()` / `provider(id)` (prefer injected, fall back to local loader)
|
|
26
|
+
- ✅ **`src/interfaces.ts`** (V3 new / V4 simplified): **Adapter Registry** + Env / Flag namespace
|
|
27
|
+
- 4 Provider interfaces (`IConfigProvider` / `IInstanceProvider` / `IGlobalPathProvider` / `IInstallationProvider`; Auth moved to `@easbot/utils.Auth`)
|
|
28
|
+
- `setAdapterRegistry(...)` — one-shot injection at agent-package startup, **completely decoupling** this package from agent's internal modules
|
|
29
|
+
- `getI<X>Provider()` convenient accessors — fail-fast throws when not registered (**no silent degradation**)
|
|
30
|
+
- `readEnvString / readFlagBool / readFlagNumber` — static readers, avoiding dependency on injection
|
|
31
|
+
- `Env / Flag` namespaces — static literals mirroring `process.env` (for IDE hints / doc generation / config introspection)
|
|
32
|
+
- ✅ **`src/index.ts`** — aggregate export entry (4 provider namespaces + 3 model namespaces + self-developed Copilot SDK + 3 constants + Config / interfaces + AdapterRegistry + 4 Provider interfaces)
|
|
33
|
+
|
|
34
|
+
## Core Features
|
|
35
|
+
|
|
36
|
+
- **25+ built-in AI SDK dynamic loading**: lazy `import()` to avoid bundle bloat
|
|
37
|
+
- **Three-layer data source fusion**: models.dev remote catalog + build-time snapshot + local CUSTOM_PROVIDER_DEFINITIONS (for ollama / easbot-local which don't appear on models.dev)
|
|
38
|
+
- **Unified Provider namespace**: `Provider.list() / getModel() / parseModel()` — one-liner calls, decoupled from concrete SDKs
|
|
39
|
+
- **OAuth + API key dual-track**: `ProviderAuth` namespace manages OAuth flow / API key persistence / injection into SDK options
|
|
40
|
+
- **Context Overflow auto-detection**: cross-vendor unified "context exceeded" semantics (25+ regex patterns + 4 HTTP status fallbacks)
|
|
41
|
+
- **Self-developed GitHub Copilot-compatible SDK**: complete chat + responses API + 6 tools (code-interpreter / file-search / web-search / image-generation / local-shell / web-search-preview)
|
|
42
|
+
- **Persistent model cache**: build-time generated `models-snapshot.ts`, runtime prefers local to avoid cold-start network calls
|
|
43
|
+
- **Minimal aggregate export**: a single `@easbot/llm` import line gives you 5+3 namespaces + self-developed SDK + key constants
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pnpm add @easbot/llm
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Quick Start
|
|
52
|
+
|
|
53
|
+
### 1. List all configured providers
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
import { Provider } from '@easbot/llm';
|
|
57
|
+
|
|
58
|
+
const allProviders = await Provider.list();
|
|
59
|
+
// { openai: { id, name, source, env, models, ... }, anthropic: {...}, ... }
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 2. Get a specific model (routed via `provider/model` string)
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
import { Provider } from '@easbot/llm';
|
|
66
|
+
|
|
67
|
+
const gpt4 = await Provider.getModel('openai/gpt-4');
|
|
68
|
+
// gpt4: LanguageModelV3 (AI SDK v6 spec)
|
|
69
|
+
|
|
70
|
+
const embed = await Provider.getModel('openai/text-embedding-3-small');
|
|
71
|
+
// embed: EmbeddingModelV3
|
|
72
|
+
|
|
73
|
+
const rerank = await Provider.getModel('easbot-local/rerank-proxy');
|
|
74
|
+
// rerank: RerankingModelV3
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 3. Parse model ID (safe split, no throws)
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
import { Provider } from '@easbot/llm';
|
|
81
|
+
|
|
82
|
+
const { providerId, modelId } = Provider.parseModel('openai/gpt-4o');
|
|
83
|
+
// { providerId: 'openai', modelId: 'gpt-4o' }
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 4. Parse API errors (with context overflow detection)
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
import { ProviderError, APICallError } from 'ai';
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
await someProvider.doCall(...);
|
|
93
|
+
} catch (e) {
|
|
94
|
+
if (e instanceof APICallError) {
|
|
95
|
+
const parsed = ProviderError.parseAPICallError({ providerId: 'openai', error: e });
|
|
96
|
+
if (parsed.type === 'context_overflow') {
|
|
97
|
+
// UI: "Context exceeded, please shorten input or switch model"
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### 5. Direct calls to high-level Model Providers
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
import { RerankProvider, GraphProvider, SummaryProvider } from '@easbot/llm';
|
|
107
|
+
|
|
108
|
+
// Document reranking
|
|
109
|
+
const reranked = await RerankProvider.rerank({
|
|
110
|
+
query: '...',
|
|
111
|
+
documents: ['...', '...'],
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Graph entity extraction
|
|
115
|
+
const graph = await GraphProvider.generateGraph({
|
|
116
|
+
chunks: ['chunk1...', 'chunk2...', '...'], // any-length array
|
|
117
|
+
prompt: { maxChunks: 5, maxConcurrency: 3 }, // v0.8: per-call slicing + 3 concurrent
|
|
118
|
+
});
|
|
119
|
+
// graph.entities / relations / summary are directly ingestable
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**v0.8 behavior contract** (`packages/llm/src/model/graph.ts`):
|
|
123
|
+
|
|
124
|
+
- **All chunks are attempted**: chunks are sliced by `prompt.maxChunks` (default 5) into multiple groups; each group triggers one LLM call. No more v0.7-style hard truncation to the first 5 chunks.
|
|
125
|
+
- **Chunk-level 3-way concurrency**: multiple groups run via lightweight semaphore (`prompt.maxConcurrency`, default 3) to avoid triggering provider rate limits.
|
|
126
|
+
- **Local failure tolerance**: a single group's LLM failure only loses that group's result; others continue. One failure no longer wipes the entire document's KG.
|
|
127
|
+
- **Boundary-aware truncation**: overlong chunks are further split via `splitByBoundary` (nearest `\n\n` / `\n` / whitespace), ensuring entity names are never cut in half.
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
// Note knowledge base summarization
|
|
131
|
+
const summary = await SummaryProvider.summarizeNote({
|
|
132
|
+
query: '...',
|
|
133
|
+
chunks: [...],
|
|
134
|
+
});
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### 6. Text embedding (EmbeddingProvider)
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
import { EmbeddingProvider } from '@easbot/llm';
|
|
141
|
+
|
|
142
|
+
// Single-text embedding — returns EmbedOutput (with embedding + usage.tokens)
|
|
143
|
+
const single = await EmbeddingProvider.embedText({ value: 'some text' });
|
|
144
|
+
// single.embedding: number[] (default bge-base-zh-v1.5 outputs 512 dimensions)
|
|
145
|
+
// single.usage.tokens: number
|
|
146
|
+
|
|
147
|
+
// Batch embedding (recommended: one API call beats N aiEmbed calls)
|
|
148
|
+
const batch = await EmbeddingProvider.embedTexts({ values: ['a', 'b', 'c'] });
|
|
149
|
+
// batch.embeddings: number[][] (one-to-one with values)
|
|
150
|
+
// batch.usage.tokens: number
|
|
151
|
+
|
|
152
|
+
// Explicit model override (skips Provider.defaultModel)
|
|
153
|
+
import { Provider } from '@easbot/llm';
|
|
154
|
+
const customModel = Provider.parseModel('openai/text-embedding-3-small');
|
|
155
|
+
const r = await EmbeddingProvider.embedText({ value: 'hi', model: customModel });
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### 7. Use the self-developed GitHub Copilot SDK
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
import { createOpenaiCompatible, openaiCompatible } from '@easbot/llm';
|
|
162
|
+
|
|
163
|
+
const copilot = createOpenaiCompatible({
|
|
164
|
+
apiKey: process.env.GITHUB_TOKEN,
|
|
165
|
+
});
|
|
166
|
+
const model = copilot('gpt-4o');
|
|
167
|
+
// Or use the default instance
|
|
168
|
+
const model2 = openaiCompatible('gpt-4o');
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Top-level Export List
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
// Provider core namespaces
|
|
175
|
+
export * as Provider from './provider/provider';
|
|
176
|
+
export * as ProviderError from './provider/error';
|
|
177
|
+
export * as ProviderAuth from './provider/auth';
|
|
178
|
+
export * as ProviderTransform from './provider/transform';
|
|
179
|
+
export * as ModelsDev from './provider/models';
|
|
180
|
+
|
|
181
|
+
// Provider constants
|
|
182
|
+
export {
|
|
183
|
+
DEFAULT_ENABLED_PROVIDERS,
|
|
184
|
+
CUSTOM_PROVIDER_DEFINITIONS,
|
|
185
|
+
PROVIDER_DISPLAY_NAMES,
|
|
186
|
+
} from './provider/definition';
|
|
187
|
+
|
|
188
|
+
// Top-level functions
|
|
189
|
+
export { initModelsRefresh } from './provider/models';
|
|
190
|
+
|
|
191
|
+
// Self-developed GitHub Copilot SDK
|
|
192
|
+
export {
|
|
193
|
+
createOpenaiCompatible,
|
|
194
|
+
openaiCompatible,
|
|
195
|
+
} from './provider/sdk/copilot';
|
|
196
|
+
export type {
|
|
197
|
+
OpenaiCompatibleProvider,
|
|
198
|
+
OpenaiCompatibleProviderSettings,
|
|
199
|
+
OpenaiCompatibleModelId,
|
|
200
|
+
} from './provider/sdk/copilot';
|
|
201
|
+
|
|
202
|
+
// High-level Model Provider namespaces
|
|
203
|
+
export * as RerankProvider from './model/rerank';
|
|
204
|
+
export * as GraphProvider from './model/graph';
|
|
205
|
+
export * as SummaryProvider from './model/summary';
|
|
206
|
+
export * as EmbeddingProvider from './model/embedding';
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
**EmbeddingProvider public contract** (`packages/llm/src/model/embedding.ts`):
|
|
210
|
+
|
|
211
|
+
- `embedText(input, abortSignal?) → Promise<EmbedOutput>`
|
|
212
|
+
- `EmbedOutput = { embedding: number[]; usage: { tokens: number } }`
|
|
213
|
+
- Empty `value` returns `{ embedding: [], usage: { tokens: 0 } }` **without** calling `ai.embed`
|
|
214
|
+
- `embedTexts(inputs, abortSignal?) → Promise<EmbedBatchOutput>`
|
|
215
|
+
- `EmbedBatchOutput = { embeddings: number[][]; usage: { tokens: number } }`
|
|
216
|
+
- Empty `values` returns `{ embeddings: [], usage: { tokens: 0 } }` **without** calling `ai.embedMany`
|
|
217
|
+
- Default model: `easbot-local/bge-base-zh-v1.5` (exported as `EmbeddingProvider.DEFAULT_MODEL`)
|
|
218
|
+
- `model` input supports three forms: omitted → `Provider.defaultModel` chain; `Provider.Model` → `Provider.getEmbedding`; directly passing an `EmbeddingModel` instance (duck-typed via `specificationVersion`) → skip the Provider chain
|
|
219
|
+
|
|
220
|
+
## Configuration Injection (migration guide for switching LLM implementation)
|
|
221
|
+
|
|
222
|
+
After the V4 refactor, the `Provider` / `ModelsDev` / `ProviderAuth` / `ProviderTransform` namespaces are **fully decoupled** from the agent package via the **Adapter Registry** pattern — they no longer directly `import` any module from agent. All runtime dependencies are explicitly injected through 4 Provider interfaces (Auth moved to `@easbot/utils.Auth`).
|
|
223
|
+
|
|
224
|
+
### 1. Adapter Registry overview
|
|
225
|
+
|
|
226
|
+
The agent package calls `setAdapterRegistry(...)` once at startup, injecting all 4 Provider implementations:
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
import {
|
|
230
|
+
setAdapterRegistry,
|
|
231
|
+
type AdapterRegistry,
|
|
232
|
+
} from '@easbot/llm';
|
|
233
|
+
|
|
234
|
+
// At the agent-package startup entry (e.g. before Instance.provide)
|
|
235
|
+
setAdapterRegistry({
|
|
236
|
+
config: Config, // IConfigProvider — get / provider
|
|
237
|
+
instance: Instance, // IInstanceProvider — getDirectory / getWorktree
|
|
238
|
+
global: Global.Path, // IGlobalPathProvider — cache / data / config
|
|
239
|
+
installation: Installation, // IInstallationProvider — getVersion
|
|
240
|
+
// V4: Auth has moved to `@easbot/utils.Auth`, no longer injected via AdapterRegistry.
|
|
241
|
+
// Internally the LLM package calls `import { Auth } from '@easbot/utils'` directly.
|
|
242
|
+
} satisfies AdapterRegistry);
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
**When not registered, all `getI<X>Provider()` calls fail-fast with a descriptive error**:
|
|
246
|
+
|
|
247
|
+
```
|
|
248
|
+
[LLM] adapter 'config' (IConfigProvider) not registered.
|
|
249
|
+
Call setAdapterRegistry({ config: ... }) before using LLM.
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
> Design intent: **no silent degradation**. Forgetting to inject throws immediately, avoiding production scenarios where "undefined config" silently breaks model routing.
|
|
253
|
+
|
|
254
|
+
### 2. 4 Provider interface contracts
|
|
255
|
+
|
|
256
|
+
| Interface | Methods | Original agent module | Purpose |
|
|
257
|
+
|-----------|---------|----------------------|---------|
|
|
258
|
+
| `IConfigProvider` | `get<T>(): Promise<T>` / `provider<T>(id): Promise<T \| undefined>` | `Config` | User `~/.config/easbot/config.json` llm block (whitelist / blacklist / variant override) |
|
|
259
|
+
| `IInstanceProvider` | `getDirectory(): string` / `getWorktree(): string` | `Instance` | Instance directory + worktree paths |
|
|
260
|
+
| `IGlobalPathProvider` | `cache: string` / `data: string` / `config: string` | `Global.Path` | Model snapshot cache / user data / global config directory |
|
|
261
|
+
| `IInstallationProvider` | `getVersion(): string` | `Installation` | User-Agent / compatibility checks / version info |
|
|
262
|
+
|
|
263
|
+
> **V4 change**: `IInstanceProvider.state(init)` and `IPluginProvider` have been **decomposed** (ADR 0071).
|
|
264
|
+
> - Provider internal state caching now uses `@easbot/utils.lazyAsync(init)` (per-process singleton + explicit reset)
|
|
265
|
+
> - Plugin registration uses module-level `_pluginAuthRegistry: Map<providerId, methods[]>` + `ProviderAuth.registerPluginAuthProvider()` entry injection
|
|
266
|
+
|
|
267
|
+
### 3. LLM Config (standalone + integrated mode)
|
|
268
|
+
|
|
269
|
+
The V3-new `src/config/` module mirrors `packages/gateway/src/config/`:
|
|
270
|
+
|
|
271
|
+
- **Standalone**: directly read the `llm` block from `easbot.json` / `easbot.jsonc`
|
|
272
|
+
- **Integrated**: when AdapterRegistry is injected, `Config.get()` prefers the injected implementation (let agent reuse)
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
import { Config, LLMConfig } from '@easbot/llm';
|
|
276
|
+
|
|
277
|
+
const cfg = await Config.get(); // injected or fallback loader
|
|
278
|
+
const openaiCfg = await Config.provider('openai');
|
|
279
|
+
|
|
280
|
+
// Schema validation (standalone)
|
|
281
|
+
const parsed = LLMConfig.parse({
|
|
282
|
+
model: 'openai/gpt-4o',
|
|
283
|
+
provider: {
|
|
284
|
+
openai: {
|
|
285
|
+
npm: '@ai-sdk/openai',
|
|
286
|
+
options: { apiKey: 'sk-...' },
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
});
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### 4. Env / Flag namespace (mirroring process.env)
|
|
293
|
+
|
|
294
|
+
`Flag` namespace and reader functions (`readEnvString` / `readFlagBool`) are still provided by the llm package. `Env` has been moved down to `@easbot/utils` — llm only re-exports it, callers may also import `Env` directly from `@easbot/utils`:
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
import { Flag, readEnvString, readFlagBool } from '@easbot/llm';
|
|
298
|
+
import { Env } from '@easbot/llm'; // equivalent to: import { Env } from '@easbot/utils'
|
|
299
|
+
|
|
300
|
+
Flag.EASBOT_OUTPUT_TOKEN_MAX; // numeric field, internally dynamic
|
|
301
|
+
readEnvString('OPENAI_API_KEY'); // wraps process.env
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### 5. Call-site migration table
|
|
305
|
+
|
|
306
|
+
| Module | Pre-V3 | V3 | V4 (this refactor) |
|
|
307
|
+
|--------|--------|----|----------------------|
|
|
308
|
+
| `provider/auth.ts` | `Instance.state(...)` | `getIInstanceProvider().state(init)` | `lazyAsync(init)` + module-level `_pluginAuthRegistry` |
|
|
309
|
+
| `provider/auth.ts` | `Auth.set(providerId, info)` | `Auth.set(...)` calls `@easbot/utils.Auth` directly (moved to utils) | `Auth.set(...)` calls `@easbot/utils.Auth` directly (moved to utils) |
|
|
310
|
+
| `provider/auth.ts` | `Plugin.definitions()` | `getIPluginProvider().definitions()` | dependency removed; replaced by `registerPluginAuthProvider()` entry injection |
|
|
311
|
+
| `provider/provider.ts` | `Instance.state(...)` | `getIInstanceProvider().state(init)` | `lazyAsync(createState)` (module-level per-process singleton) |
|
|
312
|
+
| `config/loader.ts` | `Instance.state(...)` | `getIInstanceProvider().state(init)` | `lazyAsync(createState)` (module-level per-process singleton) |
|
|
313
|
+
| `provider/models.ts` | `Global.Path.cache` | `getIGlobalPathProvider().cache` | (same as V3) |
|
|
314
|
+
| `config/index.ts` | `Config.get()` | `getIConfigProvider().get()` | (same as V3) |
|
|
315
|
+
|
|
316
|
+
**V4 refactor motivation**: `IInstanceProvider.state(init)` couples "caching" with "Instance abstraction",
|
|
317
|
+
but state caching is fundamentally a **per-process singleton with explicit reset** — independent of
|
|
318
|
+
`Instance.directory`. After sinking to `@easbot/utils.lazyAsync()`:
|
|
319
|
+
- **Decoupled**: `IInstanceProvider` shrinks to 2 methods (`getDirectory` / `getWorktree`), single responsibility
|
|
320
|
+
- **Unified**: every subsystem needing state caching (skills / mcp / plugin / etc.) reuses the same utility
|
|
321
|
+
- **Testable**: `lazyAsync().reset()` is more direct than mocking a state factory
|
|
322
|
+
|
|
323
|
+
### 6. Architectural advantages
|
|
324
|
+
|
|
325
|
+
- **Zero hard-coded dependency**: `provider/*` and `model/*` files do not directly `import` any agent-package module
|
|
326
|
+
- **Test-friendly**: tests use mock AdapterRegistry injection, no agent instance needed (`tests/setup.ts` provides a complete mock example)
|
|
327
|
+
- **Multi-entry reuse**: same LLM implementation can be reused by web / cli / gateway / monitor — each just injects AdapterRegistry
|
|
328
|
+
- **Clear contract**: fully aligned with the AdapterRegistry pattern used by ADR 0044 (skills) / 0045 (mcp) / 0046 (plugin)
|
|
329
|
+
- **Fail-fast safety**: throws immediately on missing injection, no silent degradation
|
|
330
|
+
|
|
331
|
+
## Provider Routing Priority
|
|
332
|
+
|
|
333
|
+
`Provider` merges the provider registry in the following order during `createState`:
|
|
334
|
+
|
|
335
|
+
1. **models.dev remote catalog**: at startup, if `Flag.EASBOT_MODELS_FETCH` is enabled, pull from `https://models.dev/api.json` (with local cache)
|
|
336
|
+
2. **Build-time snapshot**: if remote fetch fails or cache is missing, use `models-snapshot.ts` (statically injected at build time)
|
|
337
|
+
3. **CUSTOM_PROVIDER_DEFINITIONS**: local definition snapshot for self-developed SDKs (`@easbot/ollama-sdk` / `@easbot/local-model-sdk`)
|
|
338
|
+
4. **config.provider**: override from user's `~/.config/easbot/config.json` provider block (e.g. custom baseURL / model options)
|
|
339
|
+
5. **Environment variables + Auth persistence**: env vars + API keys persisted by `easbot auth login`
|
|
340
|
+
6. **Plugin injection**: plugins can inject special providers via `plugin.auth.loader` (e.g. github-copilot's OAuth)
|
|
341
|
+
7. **CUSTOM_LOADERS**: each provider's special initialization (anthropic headers, azure baseURL, amazon-bedrock AWS credentials, etc.)
|
|
342
|
+
|
|
343
|
+
## Testing
|
|
344
|
+
|
|
345
|
+
```bash
|
|
346
|
+
pnpm --filter @easbot/llm test:run
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
Test files are in `tests/`:
|
|
350
|
+
|
|
351
|
+
- `provider-smoke.test.ts` — validates pure constants in `definition.ts` (whitelist / local SDK snapshot / friendly name mapping) + **AdapterRegistry injection/get/fail-fast** + Env/Flag static fields + LLMConfig/ProviderConfig/ProviderAuthMethod zod schemas
|
|
352
|
+
- `interfaces.test.ts` — V4-added: 5 AdapterRegistry interfaces + static reader functions (`readEnvString` / `readFlagBool` / `readFlagNumber`) + `lazyAsync()` reset behavior
|
|
353
|
+
- `model-smoke.test.ts` — validates the type export path stability of `model/types.ts`
|
|
354
|
+
- `setup.ts` — global test environment (sets NODE_ENV=development + **injects a complete mock AdapterRegistry** to prevent chain import triggering fail-fast)
|
|
355
|
+
|
|
356
|
+
**All 75 tests currently green** (across 3 test files). Coverage breakdown:
|
|
357
|
+
|
|
358
|
+
| Category | Case count | Validation |
|
|
359
|
+
|----------|-----------|------------|
|
|
360
|
+
| AdapterRegistry interfaces (V4 new) | 11 | 4 Provider interface contracts / `setAdapterRegistry` injection / `hasAdapterRegistry` boolean / fail-fast throw / static reader functions |
|
|
361
|
+
| Provider constants | 11 | DEFAULT_ENABLED_PROVIDERS / CUSTOM_PROVIDER_DEFINITIONS / PROVIDER_DISPLAY_NAMES |
|
|
362
|
+
| ProviderAuth lazyAsync behavior (V4 new) | 3 | `registerPluginAuthProvider` triggers `_state.reset()` / `clearPluginAuthProviders` resets cache / `ProviderAuth.resetState()` clears cache |
|
|
363
|
+
| Model type export stability | 6 | RerankInput/Result/Response + Entity/Relation/GraphInput + Note/MemorySummary |
|
|
364
|
+
| Env / Flag namespace static fields | 6 | Env mirrors process.env keys / Flag contains LLM-package internal flags / reader function signatures (empty string vs undefined) |
|
|
365
|
+
| LLMConfig schema | 4 | Empty object passes / Complete fields pass / Invalid model rejected / Non-array enabled_providers rejected |
|
|
366
|
+
| ProviderConfig schema | 3 | Minimal config passes / partial design (all fields optional) / options.apiKey type validation |
|
|
367
|
+
| ProviderAuthMethod schema | 4 | api / oauth types pass / Invalid type rejected / Missing label rejected |
|
|
368
|
+
|
|
369
|
+
**Full integration test boundary**: After the AdapterRegistry decoupling, the llm package can mock all 4 Providers independently, and a complete end-to-end flow of `Provider.list / getModel / parseModel` no longer depends on the agent package. Follow-up task: when the agent package switches LLM implementation, add cross-package integration tests (verifying that real injected Providers match the llm-package contract).
|
|
370
|
+
|
|
371
|
+
## Development Commands
|
|
372
|
+
|
|
373
|
+
```bash
|
|
374
|
+
# Build (tsup, outputs ESM + CJS + DTS)
|
|
375
|
+
pnpm --filter @easbot/llm build
|
|
376
|
+
|
|
377
|
+
# Watch mode build
|
|
378
|
+
pnpm --filter @easbot/llm dev
|
|
379
|
+
|
|
380
|
+
# Regenerate models.dev snapshot (requires network)
|
|
381
|
+
pnpm --filter @easbot/llm build:snapshot
|
|
382
|
+
|
|
383
|
+
# Type check
|
|
384
|
+
pnpm --filter @easbot/llm type-check
|
|
385
|
+
|
|
386
|
+
# Lint (biome)
|
|
387
|
+
pnpm --filter @easbot/llm lint
|
|
388
|
+
|
|
389
|
+
# Lint fix
|
|
390
|
+
pnpm --filter @easbot/llm lint:fix
|
|
391
|
+
|
|
392
|
+
# Unit tests
|
|
393
|
+
pnpm --filter @easbot/llm test:run
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
## Design Principles
|
|
397
|
+
|
|
398
|
+
1. **Provider namespace is the only entry point**: business code should not directly import `@ai-sdk/openai` or other concrete SDKs — always go through `Provider.getModel('openai/gpt-4')`
|
|
399
|
+
2. **Configuration vs injection separation**: `Config` (persistent config) and `Env/Auth/Plugin` (runtime injection) don't mix
|
|
400
|
+
3. **Error normalization**: `ProviderError.parseAPICallError` unifies 25+ vendors' different error formats into `context_overflow | api_error` two-category
|
|
401
|
+
4. **Local SDK priority over remote**: self-developed SDKs (ollama / easbot-local) don't appear on models.dev, so we use `CUSTOM_PROVIDER_DEFINITIONS` static injection to avoid remote catalog drift
|
|
402
|
+
5. **Lazy SDK loading**: each provider's `BUNDLED_PROVIDERS[].loader()` is a `() => import('@ai-sdk/openai')` style dynamic import — business code that doesn't reference a provider doesn't download its SDK
|
|
403
|
+
|
|
404
|
+
## Differences from Historical Versions
|
|
405
|
+
|
|
406
|
+
| Version | Key Changes |
|
|
407
|
+
|---------|-------------|
|
|
408
|
+
| **Early placeholder (@easbot/types residue)** | Only a 5-line index.ts comment, no business capability |
|
|
409
|
+
| **V3 (ADR 0070)** | Added `src/interfaces.ts` (6 Provider interfaces + AdapterRegistry) + `src/config/` three-file module; refactored provider/{auth,models,provider,transform}.ts to use AdapterRegistry throughout |
|
|
410
|
+
| **V4 (ADR 0071)** | Removed `IInstanceProvider.state()` (decomposed to `@easbot/utils.lazyAsync()`) + removed `IPluginProvider` dependency (now module-level `_pluginAuthRegistry` + `ProviderAuth.registerPluginAuthProvider()` entry); Provider interfaces reduced from 6 to 5 |
|
|
411
|
+
|
|
412
|
+
**V3 → V4 refactor motivation**:
|
|
413
|
+
- `IInstanceProvider.state()` couples "per-process singleton + explicit reset" with the `Instance.directory` abstraction, but state caching has nothing to do with Instance paths
|
|
414
|
+
- After decomposition to `@easbot/utils.lazyAsync()`, `IInstanceProvider` shrinks to 2 methods (`getDirectory` / `getWorktree`), with clean responsibility
|
|
415
|
+
- `ProviderAuth` no longer depends on `IPluginProvider`, instead uses a module-level list + registration entry, with clearer semantics (plugin registration = push to list)
|
|
416
|
+
|
|
417
|
+
See:
|
|
418
|
+
- [ADR 0070 llm-adapter-registry](../../docs/decisions/0070-llm-adapter-registry.md) — AdapterRegistry injection pattern
|
|
419
|
+
- [ADR 0071 llm-instance-state-decompose](../../docs/decisions/0071-llm-instance-state-decompose.md) — state decomposition
|