@inkdropapp/ai 0.1.3 → 0.1.5
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 +110 -6
- package/package.json +21 -10
- package/src/events.ts +105 -0
- package/src/index.ts +22 -5
- package/src/provider-constants.ts +31 -0
- package/src/provider.ts +13 -1
- package/src/providers/anthropic/constants.ts +4 -0
- package/src/providers/{anthropic.ts → anthropic/provider.ts} +34 -18
- package/src/providers/openai-compatible/constants.ts +32 -0
- package/src/providers/{openai-compatible.ts → openai-compatible/provider.ts} +27 -29
- package/src/providers/openai-compatible/validator-compiled.d.ts +7 -0
- package/src/providers/openai-compatible/validator-compiled.js +4 -0
- package/src/providers/openai-compatible/validator.ts +33 -0
- package/src/registry.ts +30 -6
- package/src/utils/logging-fetch.ts +122 -0
- package/src/{internal → utils}/map-ai-sdk-stream.ts +1 -1
- package/types/events.d.ts +99 -0
- package/types/index.d.ts +5 -4
- package/types/provider-constants.d.ts +21 -0
- package/types/provider.d.ts +12 -1
- package/types/providers/anthropic/constants.d.ts +4 -0
- package/types/providers/anthropic/provider.d.ts +42 -0
- package/types/providers/openai-compatible/constants.d.ts +25 -0
- package/types/providers/openai-compatible/provider.d.ts +47 -0
- package/types/providers/openai-compatible/validator-compiled.d.ts +9 -0
- package/types/providers/openai-compatible/validator.d.ts +14 -0
- package/types/providers/openai-compatible.d.ts +0 -13
- package/types/registry.d.ts +12 -1
- package/types/utils/logging-fetch.d.ts +13 -0
- package/types/utils/map-ai-sdk-error.d.ts +21 -0
- package/types/utils/map-ai-sdk-stream.d.ts +25 -0
- package/src/stream-events.ts +0 -53
- /package/src/{internal → utils}/map-ai-sdk-error.ts +0 -0
package/README.md
CHANGED
|
@@ -12,6 +12,7 @@ in the Inkdrop Electron main process.
|
|
|
12
12
|
- **URL-keyed credential storage** via `@napi-rs/keyring`. Two endpoints to the same provider get separate keyring entries automatically.
|
|
13
13
|
- **Single streaming event type** — every provider's stream is reduced to a `StreamEvent` discriminated union.
|
|
14
14
|
- **Retry-aware error type** — `RateLimitExceeded` / `ServerOverloaded` carry `retryAfter` (seconds) parsed from the response header.
|
|
15
|
+
- **Per-call observability** — opt-in `apiCallListener` receives a structured event per upstream HTTP request (URL, sanitized headers, body, status, duration). Designed to back a user-facing API invocation log.
|
|
15
16
|
- **No tools, no agents, no UI, no persistence in v0.1** — see [Out of scope](#out-of-scope-deferred).
|
|
16
17
|
|
|
17
18
|
---
|
|
@@ -100,9 +101,9 @@ Inkdrop's AI features map onto two slots:
|
|
|
100
101
|
|
|
101
102
|
A slot resolves to `(provider, model)` via:
|
|
102
103
|
|
|
103
|
-
1. Explicit binding from `settings.slots[slot]`, if both the provider id and model id resolve.
|
|
104
|
+
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
105
|
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`).
|
|
106
|
+
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
107
|
4. First **authenticated** registered provider in alphabetical id order, taking its `defaultModel()` / `defaultFastModel()`.
|
|
107
108
|
5. `undefined` if no provider can satisfy the slot.
|
|
108
109
|
|
|
@@ -232,6 +233,61 @@ Three contracts you can rely on:
|
|
|
232
233
|
The library is currently text-only — `tool-call` / `tool-result` variants are
|
|
233
234
|
typed for forward compat but never produced.
|
|
234
235
|
|
|
236
|
+
### API call logging
|
|
237
|
+
|
|
238
|
+
Pass `apiCallListener` to `AIRegistry` to receive one event per upstream HTTP
|
|
239
|
+
request. Useful for backing a user-facing "API invocation log" view, or for
|
|
240
|
+
ad-hoc debugging in development.
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
import { AIRegistry, type AIModelApiCallEvent } from '@inkdropapp/ai'
|
|
244
|
+
|
|
245
|
+
const registry = new AIRegistry({
|
|
246
|
+
settings,
|
|
247
|
+
apiCallListener: (event: AIModelApiCallEvent) => {
|
|
248
|
+
const status = event.status ?? `failed: ${event.failure}`
|
|
249
|
+
console.log(
|
|
250
|
+
`[${event.providerId}] ${event.request.method} ${event.request.url}`,
|
|
251
|
+
`→ ${status} (${event.durationMs}ms)`
|
|
252
|
+
)
|
|
253
|
+
if (event.errorBody) console.error(event.errorBody)
|
|
254
|
+
}
|
|
255
|
+
})
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
The listener fires exactly once per HTTP request, after response headers
|
|
259
|
+
arrive (or after the fetch itself fails). For streaming responses, `durationMs`
|
|
260
|
+
is time-to-first-byte — the response stream is _not_ tee'd. Only error bodies
|
|
261
|
+
(`status >= 400`) are captured. API keys are redacted from `request.headers`
|
|
262
|
+
before the event is emitted.
|
|
263
|
+
|
|
264
|
+
The hook is best-effort logging only: exceptions thrown from the listener are
|
|
265
|
+
swallowed so they cannot interrupt the request path.
|
|
266
|
+
|
|
267
|
+
Event shape:
|
|
268
|
+
|
|
269
|
+
```ts
|
|
270
|
+
type AIModelApiCallEvent = {
|
|
271
|
+
providerId: string
|
|
272
|
+
startedAt: number // epoch ms
|
|
273
|
+
durationMs: number // headers received (TTFB for streams)
|
|
274
|
+
request: {
|
|
275
|
+
method: string
|
|
276
|
+
url: string
|
|
277
|
+
headers: Record<string, string> // authorization / x-api-key redacted
|
|
278
|
+
body: unknown // parsed JSON when possible, else string
|
|
279
|
+
}
|
|
280
|
+
status: number | undefined // undefined when fetch itself threw
|
|
281
|
+
errorBody?: string // populated only when status >= 400
|
|
282
|
+
failure?: string // populated only when fetch itself threw
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
**Gap to be aware of:** mid-stream errors (e.g. Anthropic's `event: error`
|
|
287
|
+
frames inside a `200 OK` SSE body) are surfaced via the `StreamEvent` union
|
|
288
|
+
(`{ kind: 'error' }`), _not_ through the API call listener. The listener only
|
|
289
|
+
sees what the HTTP layer sees.
|
|
290
|
+
|
|
235
291
|
### Errors
|
|
236
292
|
|
|
237
293
|
`AIError` is the base class. Variants:
|
|
@@ -274,8 +330,9 @@ hosts that want to predict the name (e.g. for an onboarding hint).
|
|
|
274
330
|
```ts
|
|
275
331
|
new AIRegistry({
|
|
276
332
|
settings: AISettings,
|
|
277
|
-
catalog?: AIModelCatalog,
|
|
278
|
-
keyStore?: KeyStore
|
|
333
|
+
catalog?: AIModelCatalog, // server-distributed model lists; see "Model catalogues"
|
|
334
|
+
keyStore?: KeyStore,
|
|
335
|
+
apiCallListener?: AIModelApiCallListener // wire-level invocation log; see "API call logging"
|
|
279
336
|
})
|
|
280
337
|
|
|
281
338
|
registry.listProviders(): AIProvider[]
|
|
@@ -370,6 +427,51 @@ const settings: AISettings = {
|
|
|
370
427
|
`openrouter` looks up `OPENROUTER_API_KEY`; `ollama_local` looks up
|
|
371
428
|
`OLLAMA_LOCAL_API_KEY`. Each gets its own keyring entry under its `baseURL`.
|
|
372
429
|
|
|
430
|
+
### Adding an OpenAI-compatible provider at runtime
|
|
431
|
+
|
|
432
|
+
Two-step flow: persist the new entry into `AISettings` and rebuild the
|
|
433
|
+
registry, then set the API key on the resulting provider instance.
|
|
434
|
+
|
|
435
|
+
```ts
|
|
436
|
+
// 1. Add the entry to settings and persist (host owns the config layer).
|
|
437
|
+
const next: AISettings = {
|
|
438
|
+
...settings,
|
|
439
|
+
providers: {
|
|
440
|
+
...settings.providers,
|
|
441
|
+
openaiCompatible: [
|
|
442
|
+
...(settings.providers.openaiCompatible ?? []),
|
|
443
|
+
{
|
|
444
|
+
id: 'groq',
|
|
445
|
+
baseURL: 'https://api.groq.com/openai/v1',
|
|
446
|
+
models: [{ id: 'llama-3.3-70b-versatile' }]
|
|
447
|
+
}
|
|
448
|
+
]
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
writeConfigFile(next) // your persistence layer
|
|
452
|
+
registry.updateSettings(next)
|
|
453
|
+
|
|
454
|
+
// 2. The provider now exists in the registry — set its API key.
|
|
455
|
+
const provider = registry.getProvider('groq')!
|
|
456
|
+
await provider.setApiKey('gsk-...')
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
`updateSettings` rebuilds the provider list but preserves the shared
|
|
460
|
+
`KeyStore`, so any keys already in the keyring stay resolved.
|
|
461
|
+
|
|
462
|
+
If your UX needs to **validate the key before persisting the entry**, you can
|
|
463
|
+
write directly to the `KeyStore` first — keys are stored against the
|
|
464
|
+
normalised `baseURL`, not the provider id, so this works without the entry
|
|
465
|
+
being registered yet:
|
|
466
|
+
|
|
467
|
+
```ts
|
|
468
|
+
await registry.keyStore.setKey('https://api.groq.com/openai/v1', 'gsk-...')
|
|
469
|
+
// later, once validated: persist the AISettings entry and updateSettings.
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
Once you add the entry and rebuild, `provider.isAuthenticated()` returns
|
|
473
|
+
`true` because the key already lives under that `baseURL`.
|
|
474
|
+
|
|
373
475
|
### Mixing slots across providers
|
|
374
476
|
|
|
375
477
|
```ts
|
|
@@ -458,9 +560,11 @@ for (const { provider, model } of registry.listAllModels()) {
|
|
|
458
560
|
|
|
459
561
|
```sh
|
|
460
562
|
npm install
|
|
461
|
-
npm test
|
|
563
|
+
npm test # runs the full test suite against the real system keyring
|
|
462
564
|
npm run lint
|
|
463
|
-
npm run
|
|
565
|
+
npm run build # regenerates standalone validators, then emits .d.ts into types/
|
|
566
|
+
npm run build:validators # regenerates standalone validators only (run after a schema change)
|
|
567
|
+
npm run build:types # emits .d.ts files into types/
|
|
464
568
|
```
|
|
465
569
|
|
|
466
570
|
Tests use a unique per-run keyring service name (`inkdrop.ai.test.<hex>`) so they
|
package/package.json
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inkdropapp/ai",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "AI integration common module",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
7
7
|
"types": "types/index.d.ts",
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
"
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./types/index.d.ts",
|
|
11
|
+
"default": "./src/index.ts"
|
|
12
|
+
},
|
|
13
|
+
"./constants": {
|
|
14
|
+
"types": "./types/provider-constants.d.ts",
|
|
15
|
+
"default": "./src/provider-constants.ts"
|
|
16
|
+
}
|
|
14
17
|
},
|
|
15
18
|
"keywords": [
|
|
16
19
|
"ai",
|
|
@@ -18,7 +21,6 @@
|
|
|
18
21
|
],
|
|
19
22
|
"author": "Takuya Matsuyama <t@inkdrop.app>",
|
|
20
23
|
"license": "UNLICENSED",
|
|
21
|
-
"packageManager": "pnpm@11.1.1",
|
|
22
24
|
"dependencies": {
|
|
23
25
|
"@ai-sdk/anthropic": "4.0.0-beta.42",
|
|
24
26
|
"@ai-sdk/openai-compatible": "3.0.0-beta.36",
|
|
@@ -26,6 +28,7 @@
|
|
|
26
28
|
"@inkdropapp/ai-catalog": "^0.1.0",
|
|
27
29
|
"@napi-rs/keyring": "^1.2.0",
|
|
28
30
|
"ai": "7.0.0-beta.116",
|
|
31
|
+
"ajv": "^8.20.0",
|
|
29
32
|
"npm-run-all2": "^8.0.4"
|
|
30
33
|
},
|
|
31
34
|
"devDependencies": {
|
|
@@ -42,5 +45,13 @@
|
|
|
42
45
|
"files": [
|
|
43
46
|
"src",
|
|
44
47
|
"types"
|
|
45
|
-
]
|
|
46
|
-
|
|
48
|
+
],
|
|
49
|
+
"scripts": {
|
|
50
|
+
"lint": "eslint src __tests__",
|
|
51
|
+
"test": "node --experimental-vm-modules --no-warnings=ExperimentalWarning node_modules/jest/bin/jest.js --config jest.config.cjs --runInBand",
|
|
52
|
+
"build:validators": "node scripts/build-validators.mjs",
|
|
53
|
+
"build:types": "tsc --declaration --emitDeclarationOnly",
|
|
54
|
+
"build": "npm-run-all build:validators build:types",
|
|
55
|
+
"format": "prettier --write ."
|
|
56
|
+
}
|
|
57
|
+
}
|
package/src/events.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { AIError } from './errors.js'
|
|
2
|
+
|
|
3
|
+
// ─── In-stream completion events ─────────────────────────────────────────────
|
|
4
|
+
//
|
|
5
|
+
// Fire while a single `streamCompletion` call is in flight. Provider-agnostic:
|
|
6
|
+
// every concrete provider's stream is reduced to `StreamEvent` so consumers
|
|
7
|
+
// never branch on provider id.
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Why a completion stream stopped producing output.
|
|
11
|
+
*
|
|
12
|
+
* Mirrors the AI SDK's `FinishReason`. Unknown / provider-specific values are
|
|
13
|
+
* normalised to `'other'` rather than silently passed through.
|
|
14
|
+
*/
|
|
15
|
+
export type FinishReason =
|
|
16
|
+
| 'stop'
|
|
17
|
+
| 'length'
|
|
18
|
+
| 'content-filter'
|
|
19
|
+
| 'tool-calls'
|
|
20
|
+
| 'error'
|
|
21
|
+
| 'other'
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Token usage for a single completion. Every field is optional because
|
|
25
|
+
* not every provider reports every counter.
|
|
26
|
+
*/
|
|
27
|
+
export type Usage = {
|
|
28
|
+
/** Tokens consumed from the prompt. */
|
|
29
|
+
inputTokens?: number
|
|
30
|
+
/** Tokens generated in the response. */
|
|
31
|
+
outputTokens?: number
|
|
32
|
+
/** `inputTokens + outputTokens` as reported by the provider. */
|
|
33
|
+
totalTokens?: number
|
|
34
|
+
/** Of `inputTokens`, how many were a cache hit (Anthropic prompt-cache reads). */
|
|
35
|
+
cacheReadInputTokens?: number
|
|
36
|
+
/** Of `inputTokens`, how many were written to the prompt cache for future reads. */
|
|
37
|
+
cacheCreationInputTokens?: number
|
|
38
|
+
/** Subset of `outputTokens` spent on reasoning / thinking content. */
|
|
39
|
+
reasoningTokens?: number
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Provider-agnostic streaming events. Every concrete provider's stream is
|
|
44
|
+
* reduced to this discriminated union so consumers never branch on provider id.
|
|
45
|
+
*
|
|
46
|
+
* Errors arrive as `{ kind: 'error', ... }` events, not as thrown exceptions —
|
|
47
|
+
* this lets the host pump events across an Electron IPC boundary one at a time
|
|
48
|
+
* without needing try/catch around the iterator.
|
|
49
|
+
*
|
|
50
|
+
* `tool-call` and `tool-result` are typed but never produced in the current
|
|
51
|
+
* version (one-shot text completions only). They're reserved for forward compat.
|
|
52
|
+
*/
|
|
53
|
+
export type StreamEvent =
|
|
54
|
+
| { kind: 'text-delta'; delta: string }
|
|
55
|
+
| { kind: 'tool-call'; id: string; name: string; input: unknown }
|
|
56
|
+
| { kind: 'tool-result'; id: string; name: string; result: unknown }
|
|
57
|
+
| { kind: 'usage-update'; usage: Usage }
|
|
58
|
+
| { kind: 'finish'; finishReason: FinishReason; usage: Usage }
|
|
59
|
+
| { kind: 'error'; error: AIError }
|
|
60
|
+
|
|
61
|
+
// ─── HTTP-level invocation events ────────────────────────────────────────────
|
|
62
|
+
//
|
|
63
|
+
// Fire once per upstream HTTP request, independent of the in-stream
|
|
64
|
+
// `StreamEvent` pipeline. Threaded into providers via
|
|
65
|
+
// `AIRegistryOptions.apiCallListener`.
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Wire-level record of one HTTP request the library made to an upstream
|
|
69
|
+
* provider API. Emitted via {@link AIRegistryOptions.apiCallListener} so the host
|
|
70
|
+
* can persist or surface an invocation log to end users.
|
|
71
|
+
*
|
|
72
|
+
* The listener is invoked exactly once per HTTP request — after the response
|
|
73
|
+
* headers arrive (or after the fetch itself fails). Streaming response bodies
|
|
74
|
+
* are *not* tee'd; only error bodies (HTTP status >= 400) are captured.
|
|
75
|
+
*
|
|
76
|
+
* API keys are redacted from `request.headers` before the event is emitted.
|
|
77
|
+
*/
|
|
78
|
+
export type AIModelApiCallEvent = {
|
|
79
|
+
/** Provider id that owns the SDK client which made the call. */
|
|
80
|
+
providerId: string
|
|
81
|
+
/** Epoch ms at which the fetch was issued. */
|
|
82
|
+
startedAt: number
|
|
83
|
+
/**
|
|
84
|
+
* Wall-clock time from fetch start to response headers (or to the fetch
|
|
85
|
+
* promise settling on failure). For streaming responses this is
|
|
86
|
+
* time-to-first-byte, not total stream duration.
|
|
87
|
+
*/
|
|
88
|
+
durationMs: number
|
|
89
|
+
request: {
|
|
90
|
+
method: string
|
|
91
|
+
url: string
|
|
92
|
+
/** Outgoing headers; `authorization` / `x-api-key` / `anthropic-api-key` are redacted. */
|
|
93
|
+
headers: Record<string, string>
|
|
94
|
+
/** Parsed JSON when the body was a JSON string; the raw string otherwise. */
|
|
95
|
+
body: unknown
|
|
96
|
+
}
|
|
97
|
+
/** HTTP status, or `undefined` if the fetch itself failed before a response. */
|
|
98
|
+
status: number | undefined
|
|
99
|
+
/** Response body — populated only when `status >= 400`. */
|
|
100
|
+
errorBody?: string
|
|
101
|
+
/** Failure reason — populated only when fetch itself threw (network error, abort, …). */
|
|
102
|
+
failure?: string
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export type AIModelApiCallListener = (event: AIModelApiCallEvent) => void
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
|
-
export type {
|
|
2
|
-
|
|
1
|
+
export type {
|
|
2
|
+
AIProvider,
|
|
3
|
+
AIModel,
|
|
4
|
+
AIModelOptions,
|
|
5
|
+
CompletionRequest
|
|
6
|
+
} from './provider.js'
|
|
7
|
+
export type {
|
|
8
|
+
StreamEvent,
|
|
9
|
+
Usage,
|
|
10
|
+
FinishReason,
|
|
11
|
+
AIModelApiCallEvent,
|
|
12
|
+
AIModelApiCallListener
|
|
13
|
+
} from './events.js'
|
|
3
14
|
export type {
|
|
4
15
|
AISettings,
|
|
5
16
|
SlotName,
|
|
@@ -36,11 +47,17 @@ export {
|
|
|
36
47
|
} from './errors.js'
|
|
37
48
|
export { KeyStore, type KeyStoreOptions } from './key-store.js'
|
|
38
49
|
export { normalizeBaseURL } from './url.js'
|
|
39
|
-
export { AnthropicProvider } from './providers/anthropic.js'
|
|
40
50
|
export {
|
|
41
|
-
|
|
51
|
+
ANTHROPIC_PROVIDER_ID,
|
|
52
|
+
ANTHROPIC_PROVIDER_NAME,
|
|
53
|
+
ANTHROPIC_ENV_VAR,
|
|
54
|
+
ANTHROPIC_DEFAULT_BASE_URL,
|
|
55
|
+
OPENAI_COMPATIBLE_PROVIDER_ID,
|
|
56
|
+
OPENAI_COMPATIBLE_PROVIDER_NAME,
|
|
42
57
|
deriveEnvVarName
|
|
43
|
-
} from './
|
|
58
|
+
} from './provider-constants.js'
|
|
59
|
+
export { AnthropicProvider } from './providers/anthropic/provider.js'
|
|
60
|
+
export { OpenAICompatibleProvider } from './providers/openai-compatible/provider.js'
|
|
44
61
|
export {
|
|
45
62
|
AIRegistry,
|
|
46
63
|
type AIRegistryOptions,
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure-data provider metadata for settings UIs.
|
|
3
|
+
*
|
|
4
|
+
* This module has no class or native-module imports, so it's safe to load in
|
|
5
|
+
* an Electron renderer process — unlike the main package entrypoint, which
|
|
6
|
+
* transitively pulls in `@napi-rs/keyring`.
|
|
7
|
+
*
|
|
8
|
+
* Consume via the `@inkdropapp/ai/constants` subpath:
|
|
9
|
+
*
|
|
10
|
+
* import {
|
|
11
|
+
* ANTHROPIC_PROVIDER_ID,
|
|
12
|
+
* ANTHROPIC_PROVIDER_NAME,
|
|
13
|
+
* ANTHROPIC_ENV_VAR,
|
|
14
|
+
* ANTHROPIC_DEFAULT_BASE_URL,
|
|
15
|
+
* OPENAI_COMPATIBLE_PROVIDER_ID,
|
|
16
|
+
* OPENAI_COMPATIBLE_PROVIDER_NAME,
|
|
17
|
+
* deriveEnvVarName
|
|
18
|
+
* } from '@inkdropapp/ai/constants'
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export {
|
|
22
|
+
ANTHROPIC_PROVIDER_ID,
|
|
23
|
+
ANTHROPIC_PROVIDER_NAME,
|
|
24
|
+
ANTHROPIC_ENV_VAR,
|
|
25
|
+
ANTHROPIC_DEFAULT_BASE_URL
|
|
26
|
+
} from './providers/anthropic/constants.js'
|
|
27
|
+
export {
|
|
28
|
+
OPENAI_COMPATIBLE_PROVIDER_ID,
|
|
29
|
+
OPENAI_COMPATIBLE_PROVIDER_NAME,
|
|
30
|
+
deriveEnvVarName
|
|
31
|
+
} from './providers/openai-compatible/constants.js'
|
package/src/provider.ts
CHANGED
|
@@ -3,7 +3,19 @@ import type {
|
|
|
3
3
|
ModelCapabilities
|
|
4
4
|
} from '@inkdropapp/ai-catalog'
|
|
5
5
|
import type { ModelMessage } from 'ai'
|
|
6
|
-
import type { StreamEvent } from './
|
|
6
|
+
import type { AIModelApiCallListener, StreamEvent } from './events.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Provider-construction options that apply to every model the provider hosts.
|
|
10
|
+
* Threaded by `AIRegistry` into each provider's constructor.
|
|
11
|
+
*/
|
|
12
|
+
export type AIModelOptions = {
|
|
13
|
+
/**
|
|
14
|
+
* Invoked once per upstream HTTP request made by any model on this provider.
|
|
15
|
+
* See {@link AIModelApiCallListener}.
|
|
16
|
+
*/
|
|
17
|
+
apiCallListener?: AIModelApiCallListener
|
|
18
|
+
}
|
|
7
19
|
|
|
8
20
|
/**
|
|
9
21
|
* Inputs for a single one-shot streaming completion.
|
|
@@ -14,21 +14,28 @@ import {
|
|
|
14
14
|
type ModelCapabilities
|
|
15
15
|
} from '@inkdropapp/ai-catalog'
|
|
16
16
|
import { streamText } from 'ai'
|
|
17
|
-
import { NoApiKey } from '
|
|
18
|
-
import type {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
|
|
17
|
+
import { NoApiKey } from '../../errors.js'
|
|
18
|
+
import type { AIModelApiCallListener, StreamEvent } from '../../events.js'
|
|
19
|
+
import type { KeyStore } from '../../key-store.js'
|
|
20
|
+
import type {
|
|
21
|
+
AIModel,
|
|
22
|
+
AIModelOptions,
|
|
23
|
+
AIProvider,
|
|
24
|
+
CompletionRequest
|
|
25
|
+
} from '../../provider.js'
|
|
22
26
|
import type {
|
|
23
27
|
AnthropicModelConfig,
|
|
24
28
|
AnthropicProviderConfig
|
|
25
|
-
} from '
|
|
26
|
-
import
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
} from '../../settings.js'
|
|
30
|
+
import { createLoggingFetch } from '../../utils/logging-fetch.js'
|
|
31
|
+
import { mapAiSdkError } from '../../utils/map-ai-sdk-error.js'
|
|
32
|
+
import { mapAiSdkStream } from '../../utils/map-ai-sdk-stream.js'
|
|
33
|
+
import {
|
|
34
|
+
ANTHROPIC_DEFAULT_BASE_URL,
|
|
35
|
+
ANTHROPIC_ENV_VAR,
|
|
36
|
+
ANTHROPIC_PROVIDER_ID,
|
|
37
|
+
ANTHROPIC_PROVIDER_NAME
|
|
38
|
+
} from './constants.js'
|
|
32
39
|
|
|
33
40
|
/**
|
|
34
41
|
* The Anthropic (Claude) provider.
|
|
@@ -45,15 +52,16 @@ const DEFAULT_BASE_URL = 'https://api.anthropic.com'
|
|
|
45
52
|
* `clearApiKey` is called or the resolved API key changes.
|
|
46
53
|
*/
|
|
47
54
|
export class AnthropicProvider implements AIProvider {
|
|
48
|
-
readonly id =
|
|
49
|
-
readonly name =
|
|
50
|
-
readonly envVarName =
|
|
55
|
+
readonly id = ANTHROPIC_PROVIDER_ID
|
|
56
|
+
readonly name = ANTHROPIC_PROVIDER_NAME
|
|
57
|
+
readonly envVarName = ANTHROPIC_ENV_VAR
|
|
51
58
|
readonly baseURL: string
|
|
52
59
|
|
|
53
60
|
private readonly keyStore: KeyStore
|
|
54
61
|
private readonly modelsById: Map<string, AnthropicAIModel>
|
|
55
62
|
private readonly defaultId: string
|
|
56
63
|
private readonly defaultFastId: string
|
|
64
|
+
private readonly apiCallListener: AIModelApiCallListener | undefined
|
|
57
65
|
|
|
58
66
|
private sdkClient: SdkAnthropicProvider | undefined
|
|
59
67
|
private sdkClientApiKey: string | undefined
|
|
@@ -61,10 +69,12 @@ export class AnthropicProvider implements AIProvider {
|
|
|
61
69
|
constructor(
|
|
62
70
|
keyStore: KeyStore,
|
|
63
71
|
config: AnthropicProviderConfig = {},
|
|
64
|
-
catalog?: AnthropicCatalog
|
|
72
|
+
catalog?: AnthropicCatalog,
|
|
73
|
+
options: AIModelOptions = {}
|
|
65
74
|
) {
|
|
66
75
|
this.keyStore = keyStore
|
|
67
|
-
this.baseURL = config.baseURL ??
|
|
76
|
+
this.baseURL = config.baseURL ?? ANTHROPIC_DEFAULT_BASE_URL
|
|
77
|
+
this.apiCallListener = options.apiCallListener
|
|
68
78
|
|
|
69
79
|
const baseModels = catalog?.models ?? ANTHROPIC_MODELS
|
|
70
80
|
this.defaultId = catalog?.defaultModelId ?? ANTHROPIC_DEFAULT_MODEL_ID
|
|
@@ -121,7 +131,13 @@ export class AnthropicProvider implements AIProvider {
|
|
|
121
131
|
throw new NoApiKey(this.id, this.envVarName)
|
|
122
132
|
}
|
|
123
133
|
if (this.sdkClient && this.sdkClientApiKey === apiKey) return this.sdkClient
|
|
124
|
-
this.sdkClient = createAnthropic({
|
|
134
|
+
this.sdkClient = createAnthropic({
|
|
135
|
+
apiKey,
|
|
136
|
+
baseURL: this.baseURL,
|
|
137
|
+
fetch: this.apiCallListener
|
|
138
|
+
? createLoggingFetch(this.id, this.apiCallListener)
|
|
139
|
+
: undefined
|
|
140
|
+
})
|
|
125
141
|
this.sdkClientApiKey = apiKey
|
|
126
142
|
return this.sdkClient
|
|
127
143
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `AISettings.providers` key for OpenAI-compatible entries. Use this to
|
|
3
|
+
* tag a settings-UI row as "an OpenAI-compatible provider" (vs. the built-in
|
|
4
|
+
* Anthropic row).
|
|
5
|
+
*
|
|
6
|
+
* Runtime provider ids for OpenAI-compatible entries are user-chosen
|
|
7
|
+
* (`AISettings.providers.openaiCompatible[].id`); this constant is the
|
|
8
|
+
* *type/kind*, not an instance id.
|
|
9
|
+
*/
|
|
10
|
+
export const OPENAI_COMPATIBLE_PROVIDER_ID = 'openaiCompatible'
|
|
11
|
+
export const OPENAI_COMPATIBLE_PROVIDER_NAME = 'OpenAI-Compatible'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Derives the env-var name for an OpenAI-compatible provider entry from its
|
|
15
|
+
* user-chosen `id`.
|
|
16
|
+
*
|
|
17
|
+
* Rules: replace any non-alphanumeric character with `_`, collapse runs of `_`,
|
|
18
|
+
* uppercase, append `_API_KEY`.
|
|
19
|
+
*
|
|
20
|
+
* Examples:
|
|
21
|
+
* `openrouter` → `OPENROUTER_API_KEY`
|
|
22
|
+
* `together_ai` → `TOGETHER_AI_API_KEY`
|
|
23
|
+
* `ollama-local` → `OLLAMA_LOCAL_API_KEY`
|
|
24
|
+
* `my.proxy` → `MY_PROXY_API_KEY`
|
|
25
|
+
*/
|
|
26
|
+
export const deriveEnvVarName = (id: string): string => {
|
|
27
|
+
const sanitized = id
|
|
28
|
+
.replace(/[^a-zA-Z0-9]+/g, '_')
|
|
29
|
+
.replace(/^_+|_+$/g, '')
|
|
30
|
+
.toUpperCase()
|
|
31
|
+
return `${sanitized}_API_KEY`
|
|
32
|
+
}
|
|
@@ -7,16 +7,24 @@ import type {
|
|
|
7
7
|
ModelCapabilities
|
|
8
8
|
} from '@inkdropapp/ai-catalog'
|
|
9
9
|
import { streamText } from 'ai'
|
|
10
|
-
import { NoApiKey } from '
|
|
11
|
-
import type {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
|
|
10
|
+
import { NoApiKey } from '../../errors.js'
|
|
11
|
+
import type { AIModelApiCallListener, StreamEvent } from '../../events.js'
|
|
12
|
+
import type { KeyStore } from '../../key-store.js'
|
|
13
|
+
import type {
|
|
14
|
+
AIModel,
|
|
15
|
+
AIModelOptions,
|
|
16
|
+
AIProvider,
|
|
17
|
+
CompletionRequest
|
|
18
|
+
} from '../../provider.js'
|
|
15
19
|
import type {
|
|
16
20
|
OpenAICompatibleModelConfig,
|
|
17
21
|
OpenAICompatibleProviderConfig
|
|
18
|
-
} from '
|
|
19
|
-
import
|
|
22
|
+
} from '../../settings.js'
|
|
23
|
+
import { createLoggingFetch } from '../../utils/logging-fetch.js'
|
|
24
|
+
import { mapAiSdkError } from '../../utils/map-ai-sdk-error.js'
|
|
25
|
+
import { mapAiSdkStream } from '../../utils/map-ai-sdk-stream.js'
|
|
26
|
+
import { deriveEnvVarName } from './constants.js'
|
|
27
|
+
import { validateOpenAICompatibleProviderConfig } from './validator.js'
|
|
20
28
|
|
|
21
29
|
const DEFAULT_CAPABILITIES: ModelCapabilities = {
|
|
22
30
|
supportsTools: true,
|
|
@@ -26,26 +34,6 @@ const DEFAULT_CAPABILITIES: ModelCapabilities = {
|
|
|
26
34
|
maxTokens: 32_000
|
|
27
35
|
}
|
|
28
36
|
|
|
29
|
-
/**
|
|
30
|
-
* Derives the env-var name for a user-named provider entry.
|
|
31
|
-
*
|
|
32
|
-
* Rules: replace any non-alphanumeric character with `_`, collapse runs of `_`,
|
|
33
|
-
* uppercase, append `_API_KEY`.
|
|
34
|
-
*
|
|
35
|
-
* Examples:
|
|
36
|
-
* `openrouter` → `OPENROUTER_API_KEY`
|
|
37
|
-
* `together_ai` → `TOGETHER_AI_API_KEY`
|
|
38
|
-
* `ollama-local` → `OLLAMA_LOCAL_API_KEY`
|
|
39
|
-
* `my.proxy` → `MY_PROXY_API_KEY`
|
|
40
|
-
*/
|
|
41
|
-
export const deriveEnvVarName = (id: string): string => {
|
|
42
|
-
const sanitized = id
|
|
43
|
-
.replace(/[^a-zA-Z0-9]+/g, '_')
|
|
44
|
-
.replace(/^_+|_+$/g, '')
|
|
45
|
-
.toUpperCase()
|
|
46
|
-
return `${sanitized}_API_KEY`
|
|
47
|
-
}
|
|
48
|
-
|
|
49
37
|
const mergeCapabilities = (
|
|
50
38
|
override: Partial<ModelCapabilities> | undefined
|
|
51
39
|
): ModelCapabilities => ({
|
|
@@ -79,13 +67,20 @@ export class OpenAICompatibleProvider implements AIProvider {
|
|
|
79
67
|
private readonly config: OpenAICompatibleProviderConfig
|
|
80
68
|
private readonly keyStore: KeyStore
|
|
81
69
|
private readonly modelsById: Map<string, OpenAICompatibleAIModel>
|
|
70
|
+
private readonly apiCallListener: AIModelApiCallListener | undefined
|
|
82
71
|
|
|
83
72
|
private sdkClient: SdkOpenAICompatibleProvider | undefined
|
|
84
73
|
private sdkClientApiKey: string | undefined
|
|
85
74
|
|
|
86
|
-
constructor(
|
|
75
|
+
constructor(
|
|
76
|
+
keyStore: KeyStore,
|
|
77
|
+
config: OpenAICompatibleProviderConfig,
|
|
78
|
+
options: AIModelOptions = {}
|
|
79
|
+
) {
|
|
80
|
+
validateOpenAICompatibleProviderConfig(config)
|
|
87
81
|
this.config = config
|
|
88
82
|
this.keyStore = keyStore
|
|
83
|
+
this.apiCallListener = options.apiCallListener
|
|
89
84
|
this.id = config.id
|
|
90
85
|
this.name = config.displayName ?? config.id
|
|
91
86
|
this.baseURL = config.baseURL
|
|
@@ -147,7 +142,10 @@ export class OpenAICompatibleProvider implements AIProvider {
|
|
|
147
142
|
this.sdkClient = createOpenAICompatible({
|
|
148
143
|
name: this.id,
|
|
149
144
|
baseURL: this.baseURL,
|
|
150
|
-
apiKey: apiKey ?? undefined
|
|
145
|
+
apiKey: apiKey ?? undefined,
|
|
146
|
+
fetch: this.apiCallListener
|
|
147
|
+
? createLoggingFetch(this.id, this.apiCallListener)
|
|
148
|
+
: undefined
|
|
151
149
|
})
|
|
152
150
|
this.sdkClientApiKey = apiKey ?? ''
|
|
153
151
|
return this.sdkClient
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// AUTO-GENERATED by scripts/build-validators.mjs. Do not edit by hand.
|
|
2
|
+
import type { ValidateFunction } from 'ajv'
|
|
3
|
+
import type { OpenAICompatibleProviderConfig } from '../../settings.js'
|
|
4
|
+
|
|
5
|
+
declare const validate: ValidateFunction<OpenAICompatibleProviderConfig>
|
|
6
|
+
export default validate
|
|
7
|
+
export { validate }
|