@inkdropapp/ai 0.1.0 → 0.1.2
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 +24 -21
- package/package.json +6 -4
- package/src/index.ts +2 -2
- package/src/provider.ts +4 -1
- package/src/registry.ts +27 -11
- package/src/settings.ts +12 -3
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Headless TypeScript library for multi-provider AI integration. Designed to run
|
|
|
4
4
|
in the Inkdrop Electron main process.
|
|
5
5
|
|
|
6
6
|
- **Two-interface design** — `AIProvider` (auth + model catalogue) is separate from `AIModel` (capabilities + streaming).
|
|
7
|
-
- **
|
|
7
|
+
- **AIRegistry 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
8
|
- **Two providers ship out of the box**:
|
|
9
9
|
- `AnthropicProvider` (Claude 4.x) — built-in model catalogue, prompt-cache configuration.
|
|
10
10
|
- `OpenAICompatibleProvider` — instantiated per user-named entry; covers OpenRouter, Together, Fireworks, Groq, vLLM, Ollama (`/v1`), LiteLLM, etc. with a single class.
|
|
@@ -31,7 +31,7 @@ The package is **ESM-only**. Use `import`, not `require`.
|
|
|
31
31
|
## Quickstart
|
|
32
32
|
|
|
33
33
|
```ts
|
|
34
|
-
import {
|
|
34
|
+
import { AIRegistry, type AISettings } from '@inkdropapp/ai'
|
|
35
35
|
|
|
36
36
|
const settings: AISettings = {
|
|
37
37
|
providers: {
|
|
@@ -44,13 +44,11 @@ const settings: AISettings = {
|
|
|
44
44
|
}
|
|
45
45
|
]
|
|
46
46
|
},
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
fast: { providerId: 'anthropic', modelId: 'claude-haiku-4-5' }
|
|
50
|
-
}
|
|
47
|
+
// Pick one provider — slot models come from its catalogue defaults.
|
|
48
|
+
providerId: 'anthropic'
|
|
51
49
|
}
|
|
52
50
|
|
|
53
|
-
const registry = new
|
|
51
|
+
const registry = new AIRegistry({ settings })
|
|
54
52
|
|
|
55
53
|
// Configure once — keys go to the system keyring (or env var).
|
|
56
54
|
const anthropic = registry.getProvider('anthropic')!
|
|
@@ -102,13 +100,18 @@ Inkdrop's AI features map onto two slots:
|
|
|
102
100
|
|
|
103
101
|
A slot resolves to `(provider, model)` via:
|
|
104
102
|
|
|
105
|
-
1. Explicit binding from `settings.slots[slot]`, if both the provider id and model id resolve.
|
|
106
|
-
2.
|
|
107
|
-
3. `
|
|
103
|
+
1. Explicit binding from `settings.slots[slot]`, if both the provider id and model id resolve. *(Advanced — pin a specific model per slot or mix providers across slots.)*
|
|
104
|
+
2. For `'fast'` only: fall back to `settings.slots.default` if that resolves.
|
|
105
|
+
3. Preferred provider from `settings.providerId`, if configured — takes its `defaultModel()` (for `default`) or `defaultFastModel()` (for `fast`). *(The simple UI knob — one provider dropdown drives every slot.)*
|
|
106
|
+
4. First **authenticated** registered provider in alphabetical id order, taking its `defaultModel()` / `defaultFastModel()`.
|
|
107
|
+
5. `undefined` if no provider can satisfy the slot.
|
|
108
108
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
109
|
+
Steps 1 and 3 do **not** check authentication — the user picked those
|
|
110
|
+
explicitly, so auth errors surface inline as `'error'` events at stream time
|
|
111
|
+
rather than silently swapping providers. Step 4 is a courtesy fallback for
|
|
112
|
+
hosts that don't wire any preference at all; it skips unauthenticated
|
|
113
|
+
providers. `streamCompletion` throws `NoApiKey` only when resolution returns
|
|
114
|
+
`undefined` (i.e. no provider is configured at all).
|
|
112
115
|
|
|
113
116
|
### Credential resolution
|
|
114
117
|
|
|
@@ -139,7 +142,7 @@ An `AIModelCatalog` is a per-provider override of that list, designed to be
|
|
|
139
142
|
distributed by a server endpoint and swapped in at runtime:
|
|
140
143
|
|
|
141
144
|
```ts
|
|
142
|
-
import {
|
|
145
|
+
import { AIRegistry, type AIModelCatalog } from '@inkdropapp/ai'
|
|
143
146
|
|
|
144
147
|
const catalog: AIModelCatalog = {
|
|
145
148
|
anthropic: {
|
|
@@ -167,14 +170,14 @@ const catalog: AIModelCatalog = {
|
|
|
167
170
|
}
|
|
168
171
|
}
|
|
169
172
|
|
|
170
|
-
const registry = new
|
|
173
|
+
const registry = new AIRegistry({ settings, catalog })
|
|
171
174
|
```
|
|
172
175
|
|
|
173
176
|
Resolution order, from highest to lowest priority:
|
|
174
177
|
|
|
175
178
|
1. **User-declared `config.models`** (`settings.providers.<name>.models`) — the
|
|
176
179
|
user's local overrides always win, regardless of where the catalogue came from.
|
|
177
|
-
2. **The catalogue passed to `
|
|
180
|
+
2. **The catalogue passed to `AIRegistry`** — overrides the provider's built-in list.
|
|
178
181
|
3. **The provider's compiled-in catalogue** (`ANTHROPIC_MODELS`, …) — fallback.
|
|
179
182
|
|
|
180
183
|
`defaultModelId` / `defaultFastModelId` override the provider's hard-coded
|
|
@@ -191,7 +194,7 @@ Typical flow: app starts with no catalogue (built-in defaults apply), fetches
|
|
|
191
194
|
one from the server in the background, then swaps it in:
|
|
192
195
|
|
|
193
196
|
```ts
|
|
194
|
-
const registry = new
|
|
197
|
+
const registry = new AIRegistry({ settings })
|
|
195
198
|
|
|
196
199
|
fetch('https://config.inkdrop.app/ai-catalog.json')
|
|
197
200
|
.then(r => r.json() as Promise<AIModelCatalog>)
|
|
@@ -266,10 +269,10 @@ hosts that want to predict the name (e.g. for an onboarding hint).
|
|
|
266
269
|
|
|
267
270
|
## API reference
|
|
268
271
|
|
|
269
|
-
### `
|
|
272
|
+
### `AIRegistry`
|
|
270
273
|
|
|
271
274
|
```ts
|
|
272
|
-
new
|
|
275
|
+
new AIRegistry({
|
|
273
276
|
settings: AISettings,
|
|
274
277
|
catalog?: AIModelCatalog, // server-distributed model lists; see "Model catalogues"
|
|
275
278
|
keyStore?: KeyStore
|
|
@@ -299,8 +302,8 @@ store.deleteKey(baseURL: string): Promise<void> // no-op if absent
|
|
|
299
302
|
store.invalidate(baseURL?: string): void // omit url to clear all
|
|
300
303
|
```
|
|
301
304
|
|
|
302
|
-
You generally don't construct a `KeyStore` yourself — `
|
|
303
|
-
Pass an existing instance via `
|
|
305
|
+
You generally don't construct a `KeyStore` yourself — `AIRegistry` creates one.
|
|
306
|
+
Pass an existing instance via `AIRegistryOptions.keyStore` if you want to share
|
|
304
307
|
its cache across registries.
|
|
305
308
|
|
|
306
309
|
### `AIProvider`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inkdropapp/ai",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "AI integration common module",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -18,12 +18,14 @@
|
|
|
18
18
|
],
|
|
19
19
|
"author": "Takuya Matsuyama <t@inkdrop.app>",
|
|
20
20
|
"license": "UNLICENSED",
|
|
21
|
+
"packageManager": "pnpm@11.1.1",
|
|
21
22
|
"dependencies": {
|
|
22
|
-
"@ai-sdk/anthropic": "
|
|
23
|
-
"@ai-sdk/openai-compatible": "
|
|
23
|
+
"@ai-sdk/anthropic": "4.0.0-beta.42",
|
|
24
|
+
"@ai-sdk/openai-compatible": "3.0.0-beta.36",
|
|
25
|
+
"@ai-sdk/provider": "4.0.0-beta.14",
|
|
24
26
|
"@inkdropapp/ai-catalog": "^0.1.0",
|
|
25
27
|
"@napi-rs/keyring": "^1.2.0",
|
|
26
|
-
"ai": "
|
|
28
|
+
"ai": "7.0.0-beta.116"
|
|
27
29
|
},
|
|
28
30
|
"devDependencies": {
|
|
29
31
|
"@types/jest": "^30.0.0",
|
package/src/index.ts
CHANGED
package/src/provider.ts
CHANGED
|
@@ -52,7 +52,10 @@ export interface AIModel {
|
|
|
52
52
|
* OpenAI-compatible providers (OpenRouter, Together, Ollama, …) are modelled.
|
|
53
53
|
*/
|
|
54
54
|
export interface AIProvider {
|
|
55
|
-
/**
|
|
55
|
+
/**
|
|
56
|
+
* Stable provider id; appears in both `AISettings.providerId` and
|
|
57
|
+
* `AISettings.slots[*].providerId`.
|
|
58
|
+
*/
|
|
56
59
|
readonly id: string
|
|
57
60
|
/** Human-readable label. */
|
|
58
61
|
readonly name: string
|
package/src/registry.ts
CHANGED
|
@@ -13,13 +13,13 @@ export type ResolvedSlot = {
|
|
|
13
13
|
model: AIModel
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
export type
|
|
16
|
+
export type AIRegistryOptions = {
|
|
17
17
|
settings: AISettings
|
|
18
18
|
/**
|
|
19
19
|
* Optional server-distributed catalogue of supported models per provider.
|
|
20
20
|
* When omitted, each provider uses its compiled-in defaults (e.g. `ANTHROPIC_MODELS`).
|
|
21
21
|
* The host typically fetches this from an API and either passes it at
|
|
22
|
-
* construction time or swaps it in later via {@link
|
|
22
|
+
* construction time or swaps it in later via {@link AIRegistry.updateCatalog}.
|
|
23
23
|
*/
|
|
24
24
|
catalog?: AIModelCatalog
|
|
25
25
|
/** Pass an existing `KeyStore` to share its in-memory cache across registries. */
|
|
@@ -32,26 +32,26 @@ export type RegistryOptions = {
|
|
|
32
32
|
* Owns one instance of every configured provider, resolves task slots with
|
|
33
33
|
* fallback, and is the only place feature code calls to start a completion.
|
|
34
34
|
*
|
|
35
|
-
* The `
|
|
36
|
-
* settings on its own. Call {@link
|
|
35
|
+
* The `AIRegistry` is stateless beyond its provider list — it doesn't observe
|
|
36
|
+
* settings on its own. Call {@link AIRegistry.updateSettings} when settings
|
|
37
37
|
* change to rebuild providers.
|
|
38
38
|
*
|
|
39
39
|
* @example
|
|
40
40
|
* ```ts
|
|
41
|
-
* const registry = new
|
|
41
|
+
* const registry = new AIRegistry({ settings })
|
|
42
42
|
* const events = await registry.streamCompletion('default', { messages })
|
|
43
43
|
* for await (const event of events) {
|
|
44
44
|
* if (event.kind === 'text-delta') process.stdout.write(event.delta)
|
|
45
45
|
* }
|
|
46
46
|
* ```
|
|
47
47
|
*/
|
|
48
|
-
export class
|
|
48
|
+
export class AIRegistry {
|
|
49
49
|
readonly keyStore: KeyStore
|
|
50
50
|
private providers: Map<string, AIProvider> = new Map()
|
|
51
51
|
private settings: AISettings
|
|
52
52
|
private catalog: AIModelCatalog | undefined
|
|
53
53
|
|
|
54
|
-
constructor(options:
|
|
54
|
+
constructor(options: AIRegistryOptions) {
|
|
55
55
|
this.keyStore = options.keyStore ?? new KeyStore()
|
|
56
56
|
this.settings = options.settings
|
|
57
57
|
this.catalog = options.catalog
|
|
@@ -84,12 +84,17 @@ export class Registry {
|
|
|
84
84
|
* Order:
|
|
85
85
|
* 1. Explicit binding `settings.slots[slot]`, if both provider and model resolve.
|
|
86
86
|
* 2. For `'fast'` only: fall back to `settings.slots.default`.
|
|
87
|
-
* 3.
|
|
87
|
+
* 3. Preferred provider `settings.providerId`, if that provider is configured,
|
|
88
|
+
* taking its `defaultModel()` (or `defaultFastModel()` for the fast slot).
|
|
89
|
+
* Authentication is not gated here — the user explicitly picked this
|
|
90
|
+
* provider; auth errors surface at stream time rather than silently
|
|
91
|
+
* swapping providers.
|
|
92
|
+
* 4. First authenticated provider in alphabetical id order, taking that
|
|
88
93
|
* provider's `defaultModel()` (or `defaultFastModel()` for the fast slot).
|
|
89
94
|
* Mirrors Zed's `available_fallback_model` without the Zed-Cloud preference.
|
|
90
|
-
*
|
|
95
|
+
* 5. `undefined` if no provider can satisfy the slot.
|
|
91
96
|
*
|
|
92
|
-
* Async because the
|
|
97
|
+
* Async because the fourth step calls `provider.isAuthenticated()`, which may
|
|
93
98
|
* touch the keyring.
|
|
94
99
|
*/
|
|
95
100
|
async resolveSlot(slot: SlotName): Promise<ResolvedSlot | undefined> {
|
|
@@ -101,6 +106,17 @@ export class Registry {
|
|
|
101
106
|
if (fallback) return fallback
|
|
102
107
|
}
|
|
103
108
|
|
|
109
|
+
if (this.settings.providerId) {
|
|
110
|
+
const provider = this.providers.get(this.settings.providerId)
|
|
111
|
+
if (provider) {
|
|
112
|
+
const model =
|
|
113
|
+
slot === 'fast'
|
|
114
|
+
? provider.defaultFastModel()
|
|
115
|
+
: provider.defaultModel()
|
|
116
|
+
if (model) return { provider, model }
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
104
120
|
for (const provider of this.providers.values()) {
|
|
105
121
|
if (!(await provider.isAuthenticated())) continue
|
|
106
122
|
const model =
|
|
@@ -124,7 +140,7 @@ export class Registry {
|
|
|
124
140
|
/**
|
|
125
141
|
* Streams a completion for a task slot.
|
|
126
142
|
*
|
|
127
|
-
* Resolves the slot via {@link
|
|
143
|
+
* Resolves the slot via {@link AIRegistry.resolveSlot}; throws {@link NoApiKey}
|
|
128
144
|
* if no provider can satisfy it (no slot binding, and no authenticated
|
|
129
145
|
* provider available for the implicit fallback). Otherwise returns the
|
|
130
146
|
* model's stream — note that *upstream* errors (auth, rate limit, etc.)
|
package/src/settings.ts
CHANGED
|
@@ -88,7 +88,7 @@ export type AnthropicProviderConfig = CommonProviderConfig & {
|
|
|
88
88
|
|
|
89
89
|
/**
|
|
90
90
|
* Top-level AI configuration. The host (Inkdrop main process) constructs this
|
|
91
|
-
* from whichever persistence layer it uses and passes it to `
|
|
91
|
+
* from whichever persistence layer it uses and passes it to `AIRegistry`.
|
|
92
92
|
*
|
|
93
93
|
* The library never reads or writes settings to disk itself.
|
|
94
94
|
*/
|
|
@@ -98,8 +98,17 @@ export type AISettings = {
|
|
|
98
98
|
openaiCompatible?: OpenAICompatibleProviderConfig[]
|
|
99
99
|
}
|
|
100
100
|
/**
|
|
101
|
-
*
|
|
102
|
-
*
|
|
101
|
+
* Preferred provider for every task slot. When set (and the per-slot
|
|
102
|
+
* `slots[slot]` override isn't), each slot resolves to this provider's
|
|
103
|
+
* `defaultModel` / `defaultFastModel`. This is the simple UI knob — bind it
|
|
104
|
+
* to a single "AI provider" dropdown.
|
|
105
|
+
*/
|
|
106
|
+
providerId?: string
|
|
107
|
+
/**
|
|
108
|
+
* Per-slot `(provider, model)` overrides. Wins over `providerId` when set.
|
|
109
|
+
* Useful for advanced configurations that pin a specific model per slot or
|
|
110
|
+
* mix providers across slots (e.g. Claude for `default`, a local model for
|
|
111
|
+
* `fast`).
|
|
103
112
|
*/
|
|
104
113
|
slots?: Partial<Record<SlotName, SlotConfig>>
|
|
105
114
|
}
|