@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 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