@inkdropapp/ai 0.1.4 → 0.1.6
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 +65 -6
- package/package.json +26 -14
- package/src/constants.ts +43 -0
- package/src/events.ts +105 -0
- package/src/index.ts +19 -6
- package/src/provider.ts +13 -1
- package/src/providers/anthropic/constants.ts +4 -0
- package/src/providers/{anthropic.ts → anthropic/provider.ts} +28 -13
- package/src/{provider-constants.ts → providers/openai-compatible/constants.ts} +0 -25
- package/src/providers/{openai-compatible.ts → openai-compatible/provider.ts} +27 -10
- 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/constants.d.ts +30 -0
- package/types/events.d.ts +99 -0
- package/types/index.d.ts +5 -5
- package/types/provider-constants.d.ts +2 -29
- 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/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[]
|
|
@@ -503,9 +560,11 @@ for (const { provider, model } of registry.listAllModels()) {
|
|
|
503
560
|
|
|
504
561
|
```sh
|
|
505
562
|
npm install
|
|
506
|
-
npm test
|
|
563
|
+
npm test # runs the full test suite against the real system keyring
|
|
507
564
|
npm run lint
|
|
508
|
-
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/
|
|
509
568
|
```
|
|
510
569
|
|
|
511
570
|
Tests use a unique per-run keyring service name (`inkdrop.ai.test.<hex>`) so they
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inkdropapp/ai",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "AI integration common module",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -11,24 +11,27 @@
|
|
|
11
11
|
"default": "./src/index.ts"
|
|
12
12
|
},
|
|
13
13
|
"./constants": {
|
|
14
|
-
"types": "./types/
|
|
15
|
-
"default": "./src/
|
|
14
|
+
"types": "./types/constants.d.ts",
|
|
15
|
+
"default": "./src/constants.ts"
|
|
16
16
|
}
|
|
17
17
|
},
|
|
18
|
-
"scripts": {
|
|
19
|
-
"lint": "eslint src __tests__",
|
|
20
|
-
"test": "node --experimental-vm-modules --no-warnings=ExperimentalWarning node_modules/jest/bin/jest.js --config jest.config.cjs --runInBand",
|
|
21
|
-
"build": "tsc --declaration --emitDeclarationOnly",
|
|
22
|
-
"format": "prettier --write .",
|
|
23
|
-
"prepublishOnly": "npm-run-all lint test build"
|
|
24
|
-
},
|
|
25
18
|
"keywords": [
|
|
26
19
|
"ai",
|
|
27
20
|
"inkdrop"
|
|
28
21
|
],
|
|
29
|
-
"author":
|
|
22
|
+
"author": {
|
|
23
|
+
"name": "Takuya Matsuyama",
|
|
24
|
+
"email": "t@inkdrop.app"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://github.com/inkdropapp/ai#readme",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/inkdropapp/ai.git"
|
|
30
|
+
},
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/inkdropapp/ai/issues"
|
|
33
|
+
},
|
|
30
34
|
"license": "UNLICENSED",
|
|
31
|
-
"packageManager": "pnpm@11.1.1",
|
|
32
35
|
"dependencies": {
|
|
33
36
|
"@ai-sdk/anthropic": "4.0.0-beta.42",
|
|
34
37
|
"@ai-sdk/openai-compatible": "3.0.0-beta.36",
|
|
@@ -36,6 +39,7 @@
|
|
|
36
39
|
"@inkdropapp/ai-catalog": "^0.1.0",
|
|
37
40
|
"@napi-rs/keyring": "^1.2.0",
|
|
38
41
|
"ai": "7.0.0-beta.116",
|
|
42
|
+
"ajv": "^8.20.0",
|
|
39
43
|
"npm-run-all2": "^8.0.4"
|
|
40
44
|
},
|
|
41
45
|
"devDependencies": {
|
|
@@ -52,5 +56,13 @@
|
|
|
52
56
|
"files": [
|
|
53
57
|
"src",
|
|
54
58
|
"types"
|
|
55
|
-
]
|
|
56
|
-
|
|
59
|
+
],
|
|
60
|
+
"scripts": {
|
|
61
|
+
"lint": "eslint src __tests__",
|
|
62
|
+
"test": "node --experimental-vm-modules --no-warnings=ExperimentalWarning node_modules/jest/bin/jest.js --config jest.config.cjs --runInBand",
|
|
63
|
+
"build:validators": "node scripts/build-validators.mjs",
|
|
64
|
+
"build:types": "tsc --declaration --emitDeclarationOnly",
|
|
65
|
+
"build": "npm-run-all build:validators build:types",
|
|
66
|
+
"format": "prettier --write ."
|
|
67
|
+
}
|
|
68
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure-data constants 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
|
+
* SLOT_DEFAULT,
|
|
19
|
+
* SLOT_FAST
|
|
20
|
+
* } from '@inkdropapp/ai/constants'
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { SlotName } from './settings.js'
|
|
24
|
+
|
|
25
|
+
export {
|
|
26
|
+
ANTHROPIC_PROVIDER_ID,
|
|
27
|
+
ANTHROPIC_PROVIDER_NAME,
|
|
28
|
+
ANTHROPIC_ENV_VAR,
|
|
29
|
+
ANTHROPIC_DEFAULT_BASE_URL
|
|
30
|
+
} from './providers/anthropic/constants.js'
|
|
31
|
+
export {
|
|
32
|
+
OPENAI_COMPATIBLE_PROVIDER_ID,
|
|
33
|
+
OPENAI_COMPATIBLE_PROVIDER_NAME,
|
|
34
|
+
deriveEnvVarName
|
|
35
|
+
} from './providers/openai-compatible/constants.js'
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* String literals for the two task slots exposed by {@link SlotName}.
|
|
39
|
+
* `satisfies SlotName` preserves the narrow literal type while verifying at
|
|
40
|
+
* compile time that the value is a valid `SlotName`.
|
|
41
|
+
*/
|
|
42
|
+
export const SLOT_DEFAULT = 'default' satisfies SlotName
|
|
43
|
+
export const SLOT_FAST = 'fast' satisfies SlotName
|
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,
|
|
@@ -43,10 +54,12 @@ export {
|
|
|
43
54
|
ANTHROPIC_DEFAULT_BASE_URL,
|
|
44
55
|
OPENAI_COMPATIBLE_PROVIDER_ID,
|
|
45
56
|
OPENAI_COMPATIBLE_PROVIDER_NAME,
|
|
46
|
-
deriveEnvVarName
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
57
|
+
deriveEnvVarName,
|
|
58
|
+
SLOT_DEFAULT,
|
|
59
|
+
SLOT_FAST
|
|
60
|
+
} from './constants.js'
|
|
61
|
+
export { AnthropicProvider } from './providers/anthropic/provider.js'
|
|
62
|
+
export { OpenAICompatibleProvider } from './providers/openai-compatible/provider.js'
|
|
50
63
|
export {
|
|
51
64
|
AIRegistry,
|
|
52
65
|
type AIRegistryOptions,
|
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,22 +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 {
|
|
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'
|
|
26
|
+
import type {
|
|
27
|
+
AnthropicModelConfig,
|
|
28
|
+
AnthropicProviderConfig
|
|
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'
|
|
21
33
|
import {
|
|
22
34
|
ANTHROPIC_DEFAULT_BASE_URL,
|
|
23
35
|
ANTHROPIC_ENV_VAR,
|
|
24
36
|
ANTHROPIC_PROVIDER_ID,
|
|
25
37
|
ANTHROPIC_PROVIDER_NAME
|
|
26
|
-
} from '
|
|
27
|
-
import type { AIModel, AIProvider, CompletionRequest } from '../provider.js'
|
|
28
|
-
import type {
|
|
29
|
-
AnthropicModelConfig,
|
|
30
|
-
AnthropicProviderConfig
|
|
31
|
-
} from '../settings.js'
|
|
32
|
-
import type { StreamEvent } from '../stream-events.js'
|
|
38
|
+
} from './constants.js'
|
|
33
39
|
|
|
34
40
|
/**
|
|
35
41
|
* The Anthropic (Claude) provider.
|
|
@@ -55,6 +61,7 @@ export class AnthropicProvider implements AIProvider {
|
|
|
55
61
|
private readonly modelsById: Map<string, AnthropicAIModel>
|
|
56
62
|
private readonly defaultId: string
|
|
57
63
|
private readonly defaultFastId: string
|
|
64
|
+
private readonly apiCallListener: AIModelApiCallListener | undefined
|
|
58
65
|
|
|
59
66
|
private sdkClient: SdkAnthropicProvider | undefined
|
|
60
67
|
private sdkClientApiKey: string | undefined
|
|
@@ -62,10 +69,12 @@ export class AnthropicProvider implements AIProvider {
|
|
|
62
69
|
constructor(
|
|
63
70
|
keyStore: KeyStore,
|
|
64
71
|
config: AnthropicProviderConfig = {},
|
|
65
|
-
catalog?: AnthropicCatalog
|
|
72
|
+
catalog?: AnthropicCatalog,
|
|
73
|
+
options: AIModelOptions = {}
|
|
66
74
|
) {
|
|
67
75
|
this.keyStore = keyStore
|
|
68
76
|
this.baseURL = config.baseURL ?? ANTHROPIC_DEFAULT_BASE_URL
|
|
77
|
+
this.apiCallListener = options.apiCallListener
|
|
69
78
|
|
|
70
79
|
const baseModels = catalog?.models ?? ANTHROPIC_MODELS
|
|
71
80
|
this.defaultId = catalog?.defaultModelId ?? ANTHROPIC_DEFAULT_MODEL_ID
|
|
@@ -122,7 +131,13 @@ export class AnthropicProvider implements AIProvider {
|
|
|
122
131
|
throw new NoApiKey(this.id, this.envVarName)
|
|
123
132
|
}
|
|
124
133
|
if (this.sdkClient && this.sdkClientApiKey === apiKey) return this.sdkClient
|
|
125
|
-
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
|
+
})
|
|
126
141
|
this.sdkClientApiKey = apiKey
|
|
127
142
|
return this.sdkClient
|
|
128
143
|
}
|
|
@@ -1,28 +1,3 @@
|
|
|
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 const ANTHROPIC_PROVIDER_ID = 'anthropic'
|
|
22
|
-
export const ANTHROPIC_PROVIDER_NAME = 'Anthropic'
|
|
23
|
-
export const ANTHROPIC_ENV_VAR = 'ANTHROPIC_API_KEY'
|
|
24
|
-
export const ANTHROPIC_DEFAULT_BASE_URL = 'https://api.anthropic.com'
|
|
25
|
-
|
|
26
1
|
/**
|
|
27
2
|
* The `AISettings.providers` key for OpenAI-compatible entries. Use this to
|
|
28
3
|
* tag a settings-UI row as "an OpenAI-compatible provider" (vs. the built-in
|
|
@@ -7,17 +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
|
-
|
|
15
|
-
|
|
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'
|
|
16
19
|
import type {
|
|
17
20
|
OpenAICompatibleModelConfig,
|
|
18
21
|
OpenAICompatibleProviderConfig
|
|
19
|
-
} from '
|
|
20
|
-
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'
|
|
21
28
|
|
|
22
29
|
const DEFAULT_CAPABILITIES: ModelCapabilities = {
|
|
23
30
|
supportsTools: true,
|
|
@@ -60,13 +67,20 @@ export class OpenAICompatibleProvider implements AIProvider {
|
|
|
60
67
|
private readonly config: OpenAICompatibleProviderConfig
|
|
61
68
|
private readonly keyStore: KeyStore
|
|
62
69
|
private readonly modelsById: Map<string, OpenAICompatibleAIModel>
|
|
70
|
+
private readonly apiCallListener: AIModelApiCallListener | undefined
|
|
63
71
|
|
|
64
72
|
private sdkClient: SdkOpenAICompatibleProvider | undefined
|
|
65
73
|
private sdkClientApiKey: string | undefined
|
|
66
74
|
|
|
67
|
-
constructor(
|
|
75
|
+
constructor(
|
|
76
|
+
keyStore: KeyStore,
|
|
77
|
+
config: OpenAICompatibleProviderConfig,
|
|
78
|
+
options: AIModelOptions = {}
|
|
79
|
+
) {
|
|
80
|
+
validateOpenAICompatibleProviderConfig(config)
|
|
68
81
|
this.config = config
|
|
69
82
|
this.keyStore = keyStore
|
|
83
|
+
this.apiCallListener = options.apiCallListener
|
|
70
84
|
this.id = config.id
|
|
71
85
|
this.name = config.displayName ?? config.id
|
|
72
86
|
this.baseURL = config.baseURL
|
|
@@ -128,7 +142,10 @@ export class OpenAICompatibleProvider implements AIProvider {
|
|
|
128
142
|
this.sdkClient = createOpenAICompatible({
|
|
129
143
|
name: this.id,
|
|
130
144
|
baseURL: this.baseURL,
|
|
131
|
-
apiKey: apiKey ?? undefined
|
|
145
|
+
apiKey: apiKey ?? undefined,
|
|
146
|
+
fetch: this.apiCallListener
|
|
147
|
+
? createLoggingFetch(this.id, this.apiCallListener)
|
|
148
|
+
: undefined
|
|
132
149
|
})
|
|
133
150
|
this.sdkClientApiKey = apiKey ?? ''
|
|
134
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 }
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// AUTO-GENERATED by scripts/build-validators.mjs. Do not edit by hand.
|
|
2
|
+
import __ajv_runtime_0 from "ajv/dist/runtime/ucs2length.js";
|
|
3
|
+
const func2 = __ajv_runtime_0.default ?? __ajv_runtime_0;
|
|
4
|
+
"use strict";export const validate = validate10;export default validate10;const schema11 = {"type":"object","required":["id","baseURL","models"],"additionalProperties":false,"properties":{"id":{"type":"string","minLength":1},"displayName":{"type":"string"},"baseURL":{"type":"string","minLength":1},"models":{"type":"array","minItems":1,"items":{"type":"object","required":["id"],"additionalProperties":false,"properties":{"id":{"type":"string","minLength":1},"displayName":{"type":"string"},"capabilities":{"type":"object","additionalProperties":false,"properties":{"supportsTools":{"type":"boolean"},"supportsImages":{"type":"boolean"},"supportsThinking":{"type":"boolean"},"supportsStreamingTools":{"type":"boolean"},"maxTokens":{"type":"integer","minimum":1}}}}}},"defaultModelId":{"type":"string"},"defaultFastModelId":{"type":"string"}}};function validate10(data, {instancePath="", parentData, parentDataProperty, rootData=data}={}){let vErrors = null;let errors = 0;if(data && typeof data == "object" && !Array.isArray(data)){if(data.id === undefined){const err0 = {instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: "id"},message:"must have required property '"+"id"+"'"};if(vErrors === null){vErrors = [err0];}else {vErrors.push(err0);}errors++;}if(data.baseURL === undefined){const err1 = {instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: "baseURL"},message:"must have required property '"+"baseURL"+"'"};if(vErrors === null){vErrors = [err1];}else {vErrors.push(err1);}errors++;}if(data.models === undefined){const err2 = {instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: "models"},message:"must have required property '"+"models"+"'"};if(vErrors === null){vErrors = [err2];}else {vErrors.push(err2);}errors++;}for(const key0 in data){if(!((((((key0 === "id") || (key0 === "displayName")) || (key0 === "baseURL")) || (key0 === "models")) || (key0 === "defaultModelId")) || (key0 === "defaultFastModelId"))){const err3 = {instancePath,schemaPath:"#/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key0},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err3];}else {vErrors.push(err3);}errors++;}}if(data.id !== undefined){let data0 = data.id;if(typeof data0 === "string"){if(func2(data0) < 1){const err4 = {instancePath:instancePath+"/id",schemaPath:"#/properties/id/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err4];}else {vErrors.push(err4);}errors++;}}else {const err5 = {instancePath:instancePath+"/id",schemaPath:"#/properties/id/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err5];}else {vErrors.push(err5);}errors++;}}if(data.displayName !== undefined){if(typeof data.displayName !== "string"){const err6 = {instancePath:instancePath+"/displayName",schemaPath:"#/properties/displayName/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err6];}else {vErrors.push(err6);}errors++;}}if(data.baseURL !== undefined){let data2 = data.baseURL;if(typeof data2 === "string"){if(func2(data2) < 1){const err7 = {instancePath:instancePath+"/baseURL",schemaPath:"#/properties/baseURL/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err7];}else {vErrors.push(err7);}errors++;}}else {const err8 = {instancePath:instancePath+"/baseURL",schemaPath:"#/properties/baseURL/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err8];}else {vErrors.push(err8);}errors++;}}if(data.models !== undefined){let data3 = data.models;if(Array.isArray(data3)){if(data3.length < 1){const err9 = {instancePath:instancePath+"/models",schemaPath:"#/properties/models/minItems",keyword:"minItems",params:{limit: 1},message:"must NOT have fewer than 1 items"};if(vErrors === null){vErrors = [err9];}else {vErrors.push(err9);}errors++;}const len0 = data3.length;for(let i0=0; i0<len0; i0++){let data4 = data3[i0];if(data4 && typeof data4 == "object" && !Array.isArray(data4)){if(data4.id === undefined){const err10 = {instancePath:instancePath+"/models/" + i0,schemaPath:"#/properties/models/items/required",keyword:"required",params:{missingProperty: "id"},message:"must have required property '"+"id"+"'"};if(vErrors === null){vErrors = [err10];}else {vErrors.push(err10);}errors++;}for(const key1 in data4){if(!(((key1 === "id") || (key1 === "displayName")) || (key1 === "capabilities"))){const err11 = {instancePath:instancePath+"/models/" + i0,schemaPath:"#/properties/models/items/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key1},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err11];}else {vErrors.push(err11);}errors++;}}if(data4.id !== undefined){let data5 = data4.id;if(typeof data5 === "string"){if(func2(data5) < 1){const err12 = {instancePath:instancePath+"/models/" + i0+"/id",schemaPath:"#/properties/models/items/properties/id/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err12];}else {vErrors.push(err12);}errors++;}}else {const err13 = {instancePath:instancePath+"/models/" + i0+"/id",schemaPath:"#/properties/models/items/properties/id/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err13];}else {vErrors.push(err13);}errors++;}}if(data4.displayName !== undefined){if(typeof data4.displayName !== "string"){const err14 = {instancePath:instancePath+"/models/" + i0+"/displayName",schemaPath:"#/properties/models/items/properties/displayName/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err14];}else {vErrors.push(err14);}errors++;}}if(data4.capabilities !== undefined){let data7 = data4.capabilities;if(data7 && typeof data7 == "object" && !Array.isArray(data7)){for(const key2 in data7){if(!(((((key2 === "supportsTools") || (key2 === "supportsImages")) || (key2 === "supportsThinking")) || (key2 === "supportsStreamingTools")) || (key2 === "maxTokens"))){const err15 = {instancePath:instancePath+"/models/" + i0+"/capabilities",schemaPath:"#/properties/models/items/properties/capabilities/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key2},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err15];}else {vErrors.push(err15);}errors++;}}if(data7.supportsTools !== undefined){if(typeof data7.supportsTools !== "boolean"){const err16 = {instancePath:instancePath+"/models/" + i0+"/capabilities/supportsTools",schemaPath:"#/properties/models/items/properties/capabilities/properties/supportsTools/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"};if(vErrors === null){vErrors = [err16];}else {vErrors.push(err16);}errors++;}}if(data7.supportsImages !== undefined){if(typeof data7.supportsImages !== "boolean"){const err17 = {instancePath:instancePath+"/models/" + i0+"/capabilities/supportsImages",schemaPath:"#/properties/models/items/properties/capabilities/properties/supportsImages/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"};if(vErrors === null){vErrors = [err17];}else {vErrors.push(err17);}errors++;}}if(data7.supportsThinking !== undefined){if(typeof data7.supportsThinking !== "boolean"){const err18 = {instancePath:instancePath+"/models/" + i0+"/capabilities/supportsThinking",schemaPath:"#/properties/models/items/properties/capabilities/properties/supportsThinking/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"};if(vErrors === null){vErrors = [err18];}else {vErrors.push(err18);}errors++;}}if(data7.supportsStreamingTools !== undefined){if(typeof data7.supportsStreamingTools !== "boolean"){const err19 = {instancePath:instancePath+"/models/" + i0+"/capabilities/supportsStreamingTools",schemaPath:"#/properties/models/items/properties/capabilities/properties/supportsStreamingTools/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"};if(vErrors === null){vErrors = [err19];}else {vErrors.push(err19);}errors++;}}if(data7.maxTokens !== undefined){let data12 = data7.maxTokens;if(!(((typeof data12 == "number") && (!(data12 % 1) && !isNaN(data12))) && (isFinite(data12)))){const err20 = {instancePath:instancePath+"/models/" + i0+"/capabilities/maxTokens",schemaPath:"#/properties/models/items/properties/capabilities/properties/maxTokens/type",keyword:"type",params:{type: "integer"},message:"must be integer"};if(vErrors === null){vErrors = [err20];}else {vErrors.push(err20);}errors++;}if((typeof data12 == "number") && (isFinite(data12))){if(data12 < 1 || isNaN(data12)){const err21 = {instancePath:instancePath+"/models/" + i0+"/capabilities/maxTokens",schemaPath:"#/properties/models/items/properties/capabilities/properties/maxTokens/minimum",keyword:"minimum",params:{comparison: ">=", limit: 1},message:"must be >= 1"};if(vErrors === null){vErrors = [err21];}else {vErrors.push(err21);}errors++;}}}}else {const err22 = {instancePath:instancePath+"/models/" + i0+"/capabilities",schemaPath:"#/properties/models/items/properties/capabilities/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err22];}else {vErrors.push(err22);}errors++;}}}else {const err23 = {instancePath:instancePath+"/models/" + i0,schemaPath:"#/properties/models/items/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err23];}else {vErrors.push(err23);}errors++;}}}else {const err24 = {instancePath:instancePath+"/models",schemaPath:"#/properties/models/type",keyword:"type",params:{type: "array"},message:"must be array"};if(vErrors === null){vErrors = [err24];}else {vErrors.push(err24);}errors++;}}if(data.defaultModelId !== undefined){if(typeof data.defaultModelId !== "string"){const err25 = {instancePath:instancePath+"/defaultModelId",schemaPath:"#/properties/defaultModelId/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err25];}else {vErrors.push(err25);}errors++;}}if(data.defaultFastModelId !== undefined){if(typeof data.defaultFastModelId !== "string"){const err26 = {instancePath:instancePath+"/defaultFastModelId",schemaPath:"#/properties/defaultFastModelId/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err26];}else {vErrors.push(err26);}errors++;}}}else {const err27 = {instancePath,schemaPath:"#/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err27];}else {vErrors.push(err27);}errors++;}validate10.errors = vErrors;return errors === 0;}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ErrorObject } from 'ajv'
|
|
2
|
+
import type { OpenAICompatibleProviderConfig } from '../../settings.js'
|
|
3
|
+
import validate from './validator-compiled.js'
|
|
4
|
+
|
|
5
|
+
const formatErrors = (
|
|
6
|
+
errors: ReadonlyArray<ErrorObject> | null | undefined
|
|
7
|
+
): string => {
|
|
8
|
+
if (!errors || errors.length === 0) return 'unknown validation error'
|
|
9
|
+
return errors
|
|
10
|
+
.map(e => `config${e.instancePath ?? ''} ${e.message ?? ''}`.trim())
|
|
11
|
+
.join(', ')
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Throws if `config` doesn't conform to the OpenAI-compatible provider schema.
|
|
16
|
+
*
|
|
17
|
+
* The validator is precompiled to a standalone ESM module by
|
|
18
|
+
* `scripts/build-validators.mjs`, so loading this module does not call
|
|
19
|
+
* `new Function(...)` — making it safe to use in Electron renderer processes
|
|
20
|
+
* under a strict CSP without `unsafe-eval`.
|
|
21
|
+
*
|
|
22
|
+
* Catches the kinds of mistakes a settings UI or hand-written config can make:
|
|
23
|
+
* missing required fields, wrong types, unknown keys, empty `models[]`, or an
|
|
24
|
+
* empty string where a non-empty one is required.
|
|
25
|
+
*/
|
|
26
|
+
export const validateOpenAICompatibleProviderConfig = (
|
|
27
|
+
config: OpenAICompatibleProviderConfig
|
|
28
|
+
): void => {
|
|
29
|
+
if (validate(config)) return
|
|
30
|
+
throw new Error(
|
|
31
|
+
`Invalid OpenAI-compatible provider config: ${formatErrors(validate.errors)}`
|
|
32
|
+
)
|
|
33
|
+
}
|