@inkdropapp/ai 0.1.0

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,472 @@
1
+ # @inkdropapp/ai
2
+
3
+ Headless TypeScript library for multi-provider AI integration. Designed to run
4
+ in the Inkdrop Electron main process.
5
+
6
+ - **Two-interface design** — `AIProvider` (auth + model catalogue) is separate from `AIModel` (capabilities + streaming).
7
+ - **Registry with task-slot routing** — slot binding plus fallback to per-provider defaults, so a single AI feature gets a sensible model without any user setup.
8
+ - **Two providers ship out of the box**:
9
+ - `AnthropicProvider` (Claude 4.x) — built-in model catalogue, prompt-cache configuration.
10
+ - `OpenAICompatibleProvider` — instantiated per user-named entry; covers OpenRouter, Together, Fireworks, Groq, vLLM, Ollama (`/v1`), LiteLLM, etc. with a single class.
11
+ - **Swappable model catalogues** — providers' built-in model lists can be replaced at runtime with an `AIModelCatalog` (e.g. fetched from a server), so new model releases ship without an app update.
12
+ - **URL-keyed credential storage** via `@napi-rs/keyring`. Two endpoints to the same provider get separate keyring entries automatically.
13
+ - **Single streaming event type** — every provider's stream is reduced to a `StreamEvent` discriminated union.
14
+ - **Retry-aware error type** — `RateLimitExceeded` / `ServerOverloaded` carry `retryAfter` (seconds) parsed from the response header.
15
+ - **No tools, no agents, no UI, no persistence in v0.1** — see [Out of scope](#out-of-scope-deferred).
16
+
17
+ ---
18
+
19
+ ## Install
20
+
21
+ ```sh
22
+ npm install @inkdropapp/ai
23
+ ```
24
+
25
+ Required peers (already declared as dependencies): `ai@beta`, `@ai-sdk/anthropic@beta`, `@ai-sdk/openai-compatible@beta`, `@napi-rs/keyring`.
26
+
27
+ The package is **ESM-only**. Use `import`, not `require`.
28
+
29
+ ---
30
+
31
+ ## Quickstart
32
+
33
+ ```ts
34
+ import { Registry, type AISettings } from '@inkdropapp/ai'
35
+
36
+ const settings: AISettings = {
37
+ providers: {
38
+ anthropic: {},
39
+ openaiCompatible: [
40
+ {
41
+ id: 'openrouter',
42
+ baseURL: 'https://openrouter.ai/api/v1',
43
+ models: [{ id: 'meta/llama-3-70b' }]
44
+ }
45
+ ]
46
+ },
47
+ slots: {
48
+ default: { providerId: 'anthropic', modelId: 'claude-sonnet-4-6' },
49
+ fast: { providerId: 'anthropic', modelId: 'claude-haiku-4-5' }
50
+ }
51
+ }
52
+
53
+ const registry = new Registry({ settings })
54
+
55
+ // Configure once — keys go to the system keyring (or env var).
56
+ const anthropic = registry.getProvider('anthropic')!
57
+ if (!(await anthropic.isAuthenticated())) {
58
+ await anthropic.setApiKey('sk-ant-...')
59
+ }
60
+
61
+ const events = await registry.streamCompletion('default', {
62
+ messages: [{ role: 'user', content: 'Summarise this note in one sentence.' }]
63
+ })
64
+
65
+ for await (const event of events) {
66
+ switch (event.kind) {
67
+ case 'text-delta':
68
+ process.stdout.write(event.delta)
69
+ break
70
+ case 'finish':
71
+ console.log('\n[done]', event.usage)
72
+ break
73
+ case 'error':
74
+ console.error('[error]', event.error.kind, event.error.message)
75
+ break
76
+ }
77
+ }
78
+ ```
79
+
80
+ ---
81
+
82
+ ## Concepts
83
+
84
+ ### Provider vs Model
85
+
86
+ An `AIProvider` is one authenticated integration (one API key + one `baseURL`).
87
+ It exposes a list of `AIModel` instances. The model carries the capability flags
88
+ (`supportsTools`, `supportsImages`, `supportsThinking`, `maxTokens`, …) and the
89
+ streaming entrypoint.
90
+
91
+ UI gating and request-construction logic should branch on **model capabilities**,
92
+ not on `provider.id`. That keeps the host code provider-agnostic.
93
+
94
+ ### Slots
95
+
96
+ Inkdrop's AI features map onto two slots:
97
+
98
+ | Slot | Used for |
99
+ | --------- | ---------------------------------------------------------------------------------- |
100
+ | `default` | Long-form generation. The user's "main" model. |
101
+ | `fast` | Latency-sensitive tasks: Next-Edit Suggestions, quick rewrites, title suggestions. |
102
+
103
+ A slot resolves to `(provider, model)` via:
104
+
105
+ 1. Explicit binding from `settings.slots[slot]`, if both the provider id and model id resolve.
106
+ 2. The first registered provider's `defaultModel()` (for `default`) or `defaultFastModel()` (for `fast`).
107
+ 3. `undefined` if no provider can satisfy the slot.
108
+
109
+ Authentication is **not** checked during slot resolution — that's
110
+ `streamCompletion`'s job, which throws `NoApiKey` if no provider is configured
111
+ at all and surfaces upstream auth failures inline as `'error'` events.
112
+
113
+ ### Credential resolution
114
+
115
+ `KeyStore` resolves an API key in this order:
116
+
117
+ 1. The provider's environment variable (e.g. `ANTHROPIC_API_KEY`, or
118
+ `<UPPER_SNAKE_ID>_API_KEY` for an OpenAI-compatible entry — see
119
+ [`deriveEnvVarName`](#environment-variable-naming)).
120
+ 2. System keyring entry under the service `inkdrop.ai` and account = the
121
+ _normalised_ `baseURL`.
122
+
123
+ The keyring account is the URL, not the provider id, so two endpoints to the
124
+ same provider get **separate** entries (e.g. staging + prod, or Ollama at home
125
+
126
+ - at work). The host never has to model that distinction.
127
+
128
+ The cache is in-memory, keyed on normalised `baseURL`. Env-var changes mid-process
129
+ are not reflected until you call `keyStore.invalidate()`. `setApiKey` /
130
+ `clearApiKey` invalidate automatically.
131
+
132
+ ### Model catalogues
133
+
134
+ Each provider has a compiled-in list of models (e.g. `ANTHROPIC_MODELS`). That
135
+ list is fine until a new Claude version ships and you don't want to wait for an
136
+ Inkdrop release to use it.
137
+
138
+ An `AIModelCatalog` is a per-provider override of that list, designed to be
139
+ distributed by a server endpoint and swapped in at runtime:
140
+
141
+ ```ts
142
+ import { Registry, type AIModelCatalog } from '@inkdropapp/ai'
143
+
144
+ const catalog: AIModelCatalog = {
145
+ anthropic: {
146
+ models: [
147
+ {
148
+ id: 'claude-sonnet-4-7',
149
+ displayName: 'Claude Sonnet 4.7',
150
+ capabilities: {
151
+ supportsTools: true,
152
+ supportsImages: true,
153
+ supportsThinking: true,
154
+ supportsStreamingTools: true,
155
+ maxTokens: 200_000,
156
+ maxOutputTokens: 64_000
157
+ },
158
+ cacheConfiguration: {
159
+ minTotalTokens: 2048,
160
+ minCachedTokens: 1024,
161
+ shouldSpeculate: true
162
+ }
163
+ }
164
+ ],
165
+ defaultModelId: 'claude-sonnet-4-7',
166
+ defaultFastModelId: 'claude-haiku-4-5'
167
+ }
168
+ }
169
+
170
+ const registry = new Registry({ settings, catalog })
171
+ ```
172
+
173
+ Resolution order, from highest to lowest priority:
174
+
175
+ 1. **User-declared `config.models`** (`settings.providers.<name>.models`) — the
176
+ user's local overrides always win, regardless of where the catalogue came from.
177
+ 2. **The catalogue passed to `Registry`** — overrides the provider's built-in list.
178
+ 3. **The provider's compiled-in catalogue** (`ANTHROPIC_MODELS`, …) — fallback.
179
+
180
+ `defaultModelId` / `defaultFastModelId` override the provider's hard-coded
181
+ defaults the same way.
182
+
183
+ Each provider entry on `AIModelCatalog` is independent and optional — you can
184
+ ship `{ anthropic: ... }` and leave other providers on their built-in
185
+ catalogues. Today `anthropic` is the only entry; other providers will be added
186
+ as they need it.
187
+
188
+ #### Updating the catalogue at runtime
189
+
190
+ Typical flow: app starts with no catalogue (built-in defaults apply), fetches
191
+ one from the server in the background, then swaps it in:
192
+
193
+ ```ts
194
+ const registry = new Registry({ settings })
195
+
196
+ fetch('https://config.inkdrop.app/ai-catalog.json')
197
+ .then(r => r.json() as Promise<AIModelCatalog>)
198
+ .then(catalog => registry.updateCatalog(catalog))
199
+ .catch(err => console.warn('Catalog fetch failed; using built-ins', err))
200
+ ```
201
+
202
+ `updateCatalog(undefined)` reverts to the providers' compiled-in catalogues.
203
+ The shared `KeyStore` cache and authentication state are preserved across the swap.
204
+
205
+ ### Streaming events
206
+
207
+ Every provider's stream collapses into the same discriminated union:
208
+
209
+ ```ts
210
+ type StreamEvent =
211
+ | { kind: 'text-delta'; delta: string }
212
+ | { kind: 'tool-call'; id: string; name: string; input: unknown }
213
+ | { kind: 'tool-result'; id: string; name: string; result: unknown }
214
+ | { kind: 'usage-update'; usage: Usage }
215
+ | { kind: 'finish'; finishReason: FinishReason; usage: Usage }
216
+ | { kind: 'error'; error: AIError }
217
+ ```
218
+
219
+ Three contracts you can rely on:
220
+
221
+ 1. **Errors are events, not exceptions.** Mid-stream failures arrive as
222
+ `{ kind: 'error', ... }` and the iterator returns. No try/catch around the
223
+ `for await` loop is needed.
224
+ 2. **The iterator always terminates.** Every successful stream ends with a
225
+ `'finish'` event; every failed one ends with an `'error'` event.
226
+ 3. **Unknown SDK chunk types are silently dropped.** Forward-compat with future
227
+ AI SDK chunk variants without a library bump.
228
+
229
+ The library is currently text-only — `tool-call` / `tool-result` variants are
230
+ typed for forward compat but never produced.
231
+
232
+ ### Errors
233
+
234
+ `AIError` is the base class. Variants:
235
+
236
+ | Class | Kind | When |
237
+ | --------------------- | --------------------- | -------------------------------------------------------------------------- |
238
+ | `NoApiKey` | `no-api-key` | The provider has no key configured (env var or keyring). |
239
+ | `AuthenticationError` | `authentication` | Upstream returned 401/403. |
240
+ | `RateLimitExceeded` | `rate-limit-exceeded` | Upstream returned 429. Carries `retryAfter` (seconds). |
241
+ | `ServerOverloaded` | `server-overloaded` | Upstream returned 503/529. Carries `retryAfter`. |
242
+ | `PromptTooLarge` | `prompt-too-large` | Upstream returned 413, or the message indicates a context-length overflow. |
243
+ | `UpstreamError` | `upstream` | Anything else; preserves `status` and `cause`. |
244
+
245
+ Switch on `error.kind` rather than on `instanceof`. Use `isAIError(value)` if
246
+ you need a type guard for the union.
247
+
248
+ ### Environment variable naming
249
+
250
+ For Anthropic the env-var name is fixed: `ANTHROPIC_API_KEY`.
251
+
252
+ For OpenAI-compatible entries it's derived from the entry's `id`:
253
+
254
+ | Entry id | Env var |
255
+ | -------------- | ---------------------- |
256
+ | `openrouter` | `OPENROUTER_API_KEY` |
257
+ | `together_ai` | `TOGETHER_AI_API_KEY` |
258
+ | `ollama-local` | `OLLAMA_LOCAL_API_KEY` |
259
+ | `my.proxy` | `MY_PROXY_API_KEY` |
260
+
261
+ The transform: replace any non-alphanumeric character with `_`, collapse runs
262
+ of `_`, uppercase, append `_API_KEY`. Exposed as `deriveEnvVarName(id)` for
263
+ hosts that want to predict the name (e.g. for an onboarding hint).
264
+
265
+ ---
266
+
267
+ ## API reference
268
+
269
+ ### `Registry`
270
+
271
+ ```ts
272
+ new Registry({
273
+ settings: AISettings,
274
+ catalog?: AIModelCatalog, // server-distributed model lists; see "Model catalogues"
275
+ keyStore?: KeyStore
276
+ })
277
+
278
+ registry.listProviders(): AIProvider[]
279
+ registry.getProvider(id: string): AIProvider | undefined
280
+ registry.listAllModels(): { provider: AIProvider; model: AIModel }[]
281
+ registry.resolveSlot(slot: SlotName): ResolvedSlot | undefined
282
+ registry.streamCompletion(slot: SlotName, request: CompletionRequest): Promise<AsyncIterable<StreamEvent>>
283
+ registry.updateSettings(next: AISettings): void
284
+ registry.updateCatalog(next: AIModelCatalog | undefined): void
285
+ ```
286
+
287
+ `updateSettings` and `updateCatalog` both rebuild providers but preserve the
288
+ `KeyStore` cache. Pass `undefined` to `updateCatalog` to revert to providers'
289
+ compiled-in catalogues.
290
+
291
+ ### `KeyStore`
292
+
293
+ ```ts
294
+ new KeyStore({ service?: string }) // service defaults to 'inkdrop.ai'
295
+
296
+ store.getKey(envVarName: string | null, baseURL: string): Promise<string | null>
297
+ store.setKey(baseURL: string, key: string): Promise<void>
298
+ store.deleteKey(baseURL: string): Promise<void> // no-op if absent
299
+ store.invalidate(baseURL?: string): void // omit url to clear all
300
+ ```
301
+
302
+ You generally don't construct a `KeyStore` yourself — `Registry` creates one.
303
+ Pass an existing instance via `RegistryOptions.keyStore` if you want to share
304
+ its cache across registries.
305
+
306
+ ### `AIProvider`
307
+
308
+ ```ts
309
+ provider.id // stable id, appears in slot config
310
+ provider.name // human label
311
+ provider.baseURL // doubles as keyring account
312
+ provider.envVarName // string | null
313
+
314
+ provider.listModels(): AIModel[]
315
+ provider.getModel(id: string): AIModel | undefined
316
+ provider.defaultModel(): AIModel | undefined
317
+ provider.defaultFastModel(): AIModel | undefined
318
+
319
+ provider.isAuthenticated(): Promise<boolean>
320
+ provider.setApiKey(key: string): Promise<void>
321
+ provider.clearApiKey(): Promise<void>
322
+ ```
323
+
324
+ ### `AIModel`
325
+
326
+ ```ts
327
+ model.id
328
+ model.displayName
329
+ model.provider // back-reference
330
+ model.capabilities // ModelCapabilities
331
+ model.cacheConfiguration? // CacheConfiguration | undefined
332
+
333
+ model.streamCompletion(request: CompletionRequest): AsyncIterable<StreamEvent>
334
+ ```
335
+
336
+ ---
337
+
338
+ ## Examples
339
+
340
+ ### Multiple OpenAI-compatible providers
341
+
342
+ ```ts
343
+ const settings: AISettings = {
344
+ providers: {
345
+ openaiCompatible: [
346
+ {
347
+ id: 'openrouter',
348
+ baseURL: 'https://openrouter.ai/api/v1',
349
+ models: [{ id: 'meta/llama-3-70b' }]
350
+ },
351
+ {
352
+ id: 'ollama_local',
353
+ baseURL: 'http://localhost:11434/v1',
354
+ models: [
355
+ {
356
+ id: 'llama3.2',
357
+ displayName: 'Llama 3.2',
358
+ capabilities: { supportsImages: false, maxTokens: 128_000 }
359
+ }
360
+ ]
361
+ }
362
+ ]
363
+ }
364
+ }
365
+ ```
366
+
367
+ `openrouter` looks up `OPENROUTER_API_KEY`; `ollama_local` looks up
368
+ `OLLAMA_LOCAL_API_KEY`. Each gets its own keyring entry under its `baseURL`.
369
+
370
+ ### Mixing slots across providers
371
+
372
+ ```ts
373
+ const settings: AISettings = {
374
+ providers: {
375
+ anthropic: {},
376
+ openaiCompatible: [
377
+ {
378
+ id: 'ollama_local',
379
+ baseURL: 'http://localhost:11434/v1',
380
+ models: [{ id: 'llama3.2' }]
381
+ }
382
+ ]
383
+ },
384
+ slots: {
385
+ default: { providerId: 'anthropic', modelId: 'claude-sonnet-4-6' },
386
+ fast: { providerId: 'ollama_local', modelId: 'llama3.2' }
387
+ }
388
+ }
389
+ ```
390
+
391
+ ### Custom Anthropic baseURL (proxy / staging)
392
+
393
+ ```ts
394
+ const settings: AISettings = {
395
+ providers: {
396
+ anthropic: { baseURL: 'https://anthropic-proxy.internal/v1' }
397
+ }
398
+ }
399
+ ```
400
+
401
+ The keyring entry for the proxy is separate from the one for
402
+ `https://api.anthropic.com`, so you can switch back and forth without
403
+ re-entering the key.
404
+
405
+ ### Aborting a stream
406
+
407
+ ```ts
408
+ const controller = new AbortController()
409
+ const events = await registry.streamCompletion('default', {
410
+ messages: [{ role: 'user', content: 'Long task...' }],
411
+ abortSignal: controller.signal
412
+ })
413
+
414
+ // Later:
415
+ controller.abort()
416
+ ```
417
+
418
+ ### Handling rate limits
419
+
420
+ ```ts
421
+ for await (const event of events) {
422
+ if (event.kind === 'error') {
423
+ if (event.error.kind === 'rate-limit-exceeded') {
424
+ const retry = event.error.retryAfter ?? 5
425
+ console.warn(`Rate limited; retrying in ${retry}s`)
426
+ // schedule retry…
427
+ }
428
+ break
429
+ }
430
+ }
431
+ ```
432
+
433
+ ### Listing every model for a picker UI
434
+
435
+ ```ts
436
+ for (const { provider, model } of registry.listAllModels()) {
437
+ console.log(`${provider.name} / ${model.displayName}`, model.capabilities)
438
+ }
439
+ ```
440
+
441
+ ---
442
+
443
+ ## Out of scope (deferred)
444
+
445
+ - Tools / agentic loops / MCP integration.
446
+ - Embedding models.
447
+ - Auto-discovery (e.g. Ollama `/api/tags`).
448
+ - Settings file persistence — pass `AISettings` in as plain data; the host owns its config layer.
449
+ - Reactivity / observable settings — the host's React app should re-create the registry (or call `updateSettings`) when settings change.
450
+ - React / UI components.
451
+
452
+ ---
453
+
454
+ ## Development
455
+
456
+ ```sh
457
+ npm install
458
+ npm test # runs all 82 tests against the real system keyring
459
+ npm run lint
460
+ npm run gen # emits .d.ts files into types/
461
+ ```
462
+
463
+ Tests use a unique per-run keyring service name (`inkdrop.ai.test.<hex>`) so they
464
+ never touch production entries on a dev machine, and an `afterEach` hook deletes
465
+ every entry created during the test.
466
+
467
+ CI runs the same suite on macOS, Windows, and Ubuntu (with gnome-keyring +
468
+ dbus-run-session for the keyring).
469
+
470
+ ## License
471
+
472
+ UNLICENSED — internal Inkdrop module.
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@inkdropapp/ai",
3
+ "version": "0.1.0",
4
+ "description": "AI integration common module",
5
+ "type": "module",
6
+ "main": "src/index.ts",
7
+ "types": "types/index.d.ts",
8
+ "scripts": {
9
+ "lint": "eslint src __tests__",
10
+ "test": "node --experimental-vm-modules --no-warnings=ExperimentalWarning node_modules/jest/bin/jest.js --config jest.config.cjs --runInBand",
11
+ "gen": "tsc --declaration --emitDeclarationOnly",
12
+ "format": "prettier --write .",
13
+ "prepublishOnly": "npm-run-all lint test gen"
14
+ },
15
+ "keywords": [
16
+ "ai",
17
+ "inkdrop"
18
+ ],
19
+ "author": "Takuya Matsuyama <t@inkdrop.app>",
20
+ "license": "UNLICENSED",
21
+ "dependencies": {
22
+ "@ai-sdk/anthropic": "^4.0.0-beta.40",
23
+ "@ai-sdk/openai-compatible": "^3.0.0-beta.33",
24
+ "@inkdropapp/ai-catalog": "^0.1.0",
25
+ "@napi-rs/keyring": "^1.2.0",
26
+ "ai": "^7.0.0-beta.113"
27
+ },
28
+ "devDependencies": {
29
+ "@types/jest": "^30.0.0",
30
+ "@types/node": "^25.7.0",
31
+ "eslint": "^10.3.0",
32
+ "eslint-config-prettier": "^10.1.8",
33
+ "jest": "^30.4.2",
34
+ "npm-run-all": "^4.1.5",
35
+ "prettier": "^3.8.3",
36
+ "ts-jest": "^29.4.9",
37
+ "typescript": "^6.0.3",
38
+ "typescript-eslint": "^8.59.3"
39
+ },
40
+ "files": [
41
+ "src",
42
+ "types"
43
+ ]
44
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,117 @@
1
+ /** Discriminant for {@link AIError} subclasses. */
2
+ export type AIErrorKind =
3
+ | 'no-api-key'
4
+ | 'rate-limit-exceeded'
5
+ | 'server-overloaded'
6
+ | 'authentication'
7
+ | 'prompt-too-large'
8
+ | 'upstream'
9
+
10
+ /**
11
+ * Base class for every error this library surfaces. Concrete subclasses are
12
+ * tagged with `kind` so consumers can switch on it without instanceof chains.
13
+ *
14
+ * Errors are emitted as `{ kind: 'error', error }` events inside completion
15
+ * streams rather than thrown — consumers that iterate the stream do not need
16
+ * try/catch.
17
+ */
18
+ export abstract class AIError extends Error {
19
+ abstract readonly kind: AIErrorKind
20
+
21
+ constructor(message: string, options?: { cause?: unknown }) {
22
+ super(message, options)
23
+ this.name = new.target.name
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Thrown / emitted when a provider has no resolvable API key — neither in its
29
+ * environment variable nor in the system keyring.
30
+ */
31
+ export class NoApiKey extends AIError {
32
+ readonly kind = 'no-api-key' as const
33
+
34
+ constructor(
35
+ public readonly providerId: string,
36
+ public readonly envVarName: string | null
37
+ ) {
38
+ const hint = envVarName
39
+ ? ` Set ${envVarName} or store the key in the system keychain.`
40
+ : ' Store the key in the system keychain.'
41
+ super(`Provider "${providerId}" has no API key configured.${hint}`)
42
+ }
43
+ }
44
+
45
+ /** Upstream returned 401 / 403 — the configured key was rejected. */
46
+ export class AuthenticationError extends AIError {
47
+ readonly kind = 'authentication' as const
48
+
49
+ constructor(
50
+ public readonly providerId: string,
51
+ message?: string
52
+ ) {
53
+ super(message ?? `Authentication failed for provider "${providerId}".`)
54
+ }
55
+ }
56
+
57
+ /** Upstream returned 429. `retryAfter` is in seconds, parsed from the response header. */
58
+ export class RateLimitExceeded extends AIError {
59
+ readonly kind = 'rate-limit-exceeded' as const
60
+
61
+ constructor(
62
+ public readonly providerId: string,
63
+ public readonly retryAfter: number | undefined,
64
+ message?: string
65
+ ) {
66
+ super(message ?? `Rate limit exceeded on provider "${providerId}".`)
67
+ }
68
+ }
69
+
70
+ /** Upstream returned 503 / 529. `retryAfter` is in seconds, parsed from the response header. */
71
+ export class ServerOverloaded extends AIError {
72
+ readonly kind = 'server-overloaded' as const
73
+
74
+ constructor(
75
+ public readonly providerId: string,
76
+ public readonly retryAfter: number | undefined,
77
+ message?: string
78
+ ) {
79
+ super(message ?? `Provider "${providerId}" is overloaded.`)
80
+ }
81
+ }
82
+
83
+ /** Prompt exceeds the model's context window (HTTP 413, or message says so). */
84
+ export class PromptTooLarge extends AIError {
85
+ readonly kind = 'prompt-too-large' as const
86
+
87
+ constructor(
88
+ public readonly providerId: string,
89
+ public readonly tokenCount: number | undefined,
90
+ public readonly maxTokens: number | undefined,
91
+ message?: string
92
+ ) {
93
+ super(message ?? `Prompt exceeds the model's context window.`)
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Catch-all for upstream failures that don't map to a more specific variant.
99
+ * `status` is the HTTP status code when known; `cause` preserves the original
100
+ * AI SDK error for debugging.
101
+ */
102
+ export class UpstreamError extends AIError {
103
+ readonly kind = 'upstream' as const
104
+
105
+ constructor(
106
+ public readonly providerId: string,
107
+ message: string,
108
+ public readonly status?: number,
109
+ options?: { cause?: unknown }
110
+ ) {
111
+ super(message, options)
112
+ }
113
+ }
114
+
115
+ /** Type guard for {@link AIError} and any of its subclasses. */
116
+ export const isAIError = (error: unknown): error is AIError =>
117
+ error instanceof AIError
package/src/index.ts ADDED
@@ -0,0 +1,48 @@
1
+ export type { AIProvider, AIModel, CompletionRequest } from './provider.js'
2
+ export type { StreamEvent, Usage, FinishReason } from './stream-events.js'
3
+ export type {
4
+ AISettings,
5
+ SlotName,
6
+ SlotConfig,
7
+ CommonProviderConfig,
8
+ AnthropicProviderConfig,
9
+ AnthropicModelConfig,
10
+ OpenAICompatibleProviderConfig,
11
+ OpenAICompatibleModelConfig
12
+ } from './settings.js'
13
+ export {
14
+ ANTHROPIC_MODELS,
15
+ ANTHROPIC_DEFAULT_MODEL_ID,
16
+ ANTHROPIC_DEFAULT_FAST_MODEL_ID,
17
+ DEFAULT_ANTHROPIC_CACHE_CONFIG,
18
+ DEFAULT_ANTHROPIC_CAPABILITIES,
19
+ BUILT_IN_CATALOG,
20
+ type AIModelCatalog,
21
+ type AnthropicCatalog,
22
+ type AnthropicModelDescriptor,
23
+ type CacheConfiguration,
24
+ type ModelCapabilities
25
+ } from '@inkdropapp/ai-catalog'
26
+ export {
27
+ AIError,
28
+ NoApiKey,
29
+ AuthenticationError,
30
+ RateLimitExceeded,
31
+ ServerOverloaded,
32
+ PromptTooLarge,
33
+ UpstreamError,
34
+ isAIError,
35
+ type AIErrorKind
36
+ } from './errors.js'
37
+ export { KeyStore, type KeyStoreOptions } from './key-store.js'
38
+ export { normalizeBaseURL } from './url.js'
39
+ export { AnthropicProvider } from './providers/anthropic.js'
40
+ export {
41
+ OpenAICompatibleProvider,
42
+ deriveEnvVarName
43
+ } from './providers/openai-compatible.js'
44
+ export {
45
+ Registry,
46
+ type RegistryOptions,
47
+ type ResolvedSlot
48
+ } from './registry.js'