@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/src/registry.ts
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import type { AIModelCatalog } from '@inkdropapp/ai-catalog'
|
|
2
|
+
import type { AIModelApiCallListener, StreamEvent } from './events.js'
|
|
2
3
|
import { NoApiKey } from './errors.js'
|
|
3
4
|
import { KeyStore } from './key-store.js'
|
|
4
|
-
import type {
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
import type {
|
|
6
|
+
AIModel,
|
|
7
|
+
AIModelOptions,
|
|
8
|
+
AIProvider,
|
|
9
|
+
CompletionRequest
|
|
10
|
+
} from './provider.js'
|
|
11
|
+
import { AnthropicProvider } from './providers/anthropic/provider.js'
|
|
12
|
+
import { OpenAICompatibleProvider } from './providers/openai-compatible/provider.js'
|
|
7
13
|
import type { AISettings, SlotConfig, SlotName } from './settings.js'
|
|
8
|
-
import type { StreamEvent } from './stream-events.js'
|
|
9
14
|
|
|
10
15
|
/** Result of resolving a slot to a concrete `(provider, model)` pair. */
|
|
11
16
|
export type ResolvedSlot = {
|
|
@@ -24,6 +29,16 @@ export type AIRegistryOptions = {
|
|
|
24
29
|
catalog?: AIModelCatalog
|
|
25
30
|
/** Pass an existing `KeyStore` to share its in-memory cache across registries. */
|
|
26
31
|
keyStore?: KeyStore
|
|
32
|
+
/**
|
|
33
|
+
* Invoked once per upstream HTTP request made by any provider in this
|
|
34
|
+
* registry. The hook is best-effort logging only — exceptions thrown from
|
|
35
|
+
* the listener are swallowed so they cannot interrupt the request path.
|
|
36
|
+
*
|
|
37
|
+
* Use this to back a user-facing "API invocation log" view. Streaming
|
|
38
|
+
* response bodies are not captured; only error bodies (status >= 400) are.
|
|
39
|
+
* See {@link AIModelApiCallEvent}.
|
|
40
|
+
*/
|
|
41
|
+
apiCallListener?: AIModelApiCallListener
|
|
27
42
|
}
|
|
28
43
|
|
|
29
44
|
/**
|
|
@@ -50,11 +65,13 @@ export class AIRegistry {
|
|
|
50
65
|
private providers: Map<string, AIProvider> = new Map()
|
|
51
66
|
private settings: AISettings
|
|
52
67
|
private catalog: AIModelCatalog | undefined
|
|
68
|
+
private readonly apiCallListener: AIModelApiCallListener | undefined
|
|
53
69
|
|
|
54
70
|
constructor(options: AIRegistryOptions) {
|
|
55
71
|
this.keyStore = options.keyStore ?? new KeyStore()
|
|
56
72
|
this.settings = options.settings
|
|
57
73
|
this.catalog = options.catalog
|
|
74
|
+
this.apiCallListener = options.apiCallListener
|
|
58
75
|
this.rebuildProviders()
|
|
59
76
|
}
|
|
60
77
|
|
|
@@ -183,18 +200,25 @@ export class AIRegistry {
|
|
|
183
200
|
private rebuildProviders(): void {
|
|
184
201
|
const built: AIProvider[] = []
|
|
185
202
|
|
|
203
|
+
const providerOptions: AIModelOptions = {
|
|
204
|
+
apiCallListener: this.apiCallListener
|
|
205
|
+
}
|
|
206
|
+
|
|
186
207
|
if (this.settings.providers.anthropic) {
|
|
187
208
|
built.push(
|
|
188
209
|
new AnthropicProvider(
|
|
189
210
|
this.keyStore,
|
|
190
211
|
this.settings.providers.anthropic,
|
|
191
|
-
this.catalog?.anthropic
|
|
212
|
+
this.catalog?.anthropic,
|
|
213
|
+
providerOptions
|
|
192
214
|
)
|
|
193
215
|
)
|
|
194
216
|
}
|
|
195
217
|
|
|
196
218
|
for (const config of this.settings.providers.openaiCompatible ?? []) {
|
|
197
|
-
built.push(
|
|
219
|
+
built.push(
|
|
220
|
+
new OpenAICompatibleProvider(this.keyStore, config, providerOptions)
|
|
221
|
+
)
|
|
198
222
|
}
|
|
199
223
|
|
|
200
224
|
// Sort once at build time so `Map` iteration (insertion order) is
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { AIModelApiCallEvent, AIModelApiCallListener } from '../events.js'
|
|
2
|
+
|
|
3
|
+
type FetchFn = typeof fetch
|
|
4
|
+
type FetchInput = Parameters<FetchFn>[0]
|
|
5
|
+
type FetchInit = NonNullable<Parameters<FetchFn>[1]>
|
|
6
|
+
type FetchHeaders = FetchInit['headers']
|
|
7
|
+
|
|
8
|
+
const REDACTED_HEADERS = new Set([
|
|
9
|
+
'authorization',
|
|
10
|
+
'x-api-key',
|
|
11
|
+
'anthropic-api-key',
|
|
12
|
+
'api-key'
|
|
13
|
+
])
|
|
14
|
+
|
|
15
|
+
const sanitizeHeaders = (
|
|
16
|
+
headers: FetchHeaders | undefined
|
|
17
|
+
): Record<string, string> => {
|
|
18
|
+
const out: Record<string, string> = {}
|
|
19
|
+
const put = (key: string, value: string) => {
|
|
20
|
+
out[key] = REDACTED_HEADERS.has(key.toLowerCase()) ? '[redacted]' : value
|
|
21
|
+
}
|
|
22
|
+
if (!headers) return out
|
|
23
|
+
if (typeof Headers !== 'undefined' && headers instanceof Headers) {
|
|
24
|
+
headers.forEach((value, key) => put(key, value))
|
|
25
|
+
} else if (Array.isArray(headers)) {
|
|
26
|
+
for (const entry of headers) put(entry[0], entry[1])
|
|
27
|
+
} else {
|
|
28
|
+
for (const [key, value] of Object.entries(headers)) put(key, String(value))
|
|
29
|
+
}
|
|
30
|
+
return out
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const parseBody = (body: unknown): unknown => {
|
|
34
|
+
if (body == null) return undefined
|
|
35
|
+
if (typeof body !== 'string') return '[non-string body]'
|
|
36
|
+
try {
|
|
37
|
+
return JSON.parse(body)
|
|
38
|
+
} catch {
|
|
39
|
+
return body
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const extractUrl = (input: FetchInput): string => {
|
|
44
|
+
if (typeof input === 'string') return input
|
|
45
|
+
if (input instanceof URL) return input.toString()
|
|
46
|
+
return input.url
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const extractMethod = (
|
|
50
|
+
input: FetchInput,
|
|
51
|
+
init: FetchInit | undefined
|
|
52
|
+
): string => {
|
|
53
|
+
if (init?.method) return init.method
|
|
54
|
+
if (typeof input !== 'string' && !(input instanceof URL)) return input.method
|
|
55
|
+
return 'GET'
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const safeNotify = (
|
|
59
|
+
listener: AIModelApiCallListener,
|
|
60
|
+
event: AIModelApiCallEvent
|
|
61
|
+
): void => {
|
|
62
|
+
try {
|
|
63
|
+
listener(event)
|
|
64
|
+
} catch {
|
|
65
|
+
// Swallow — a misbehaving listener must not break the fetch path.
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Wraps an underlying `fetch` to emit an {@link AIModelApiCallEvent} per HTTP request.
|
|
71
|
+
*
|
|
72
|
+
* Emits exactly once per call. On HTTP error (`!response.ok`) the response is
|
|
73
|
+
* `clone()`'d and the body is read as text into `event.errorBody`; the
|
|
74
|
+
* original response is returned untouched so the SDK still consumes the
|
|
75
|
+
* stream normally. Successful streaming responses are *not* tee'd — only the
|
|
76
|
+
* status is recorded.
|
|
77
|
+
*/
|
|
78
|
+
export const createLoggingFetch = (
|
|
79
|
+
providerId: string,
|
|
80
|
+
listener: AIModelApiCallListener,
|
|
81
|
+
inner: FetchFn = fetch
|
|
82
|
+
): FetchFn => {
|
|
83
|
+
return async (input, init) => {
|
|
84
|
+
const startedAt = Date.now()
|
|
85
|
+
const url = extractUrl(input)
|
|
86
|
+
const method = extractMethod(input, init)
|
|
87
|
+
const headers = sanitizeHeaders(init?.headers)
|
|
88
|
+
const body = parseBody(init?.body)
|
|
89
|
+
|
|
90
|
+
const buildEvent = (extras: {
|
|
91
|
+
status: number | undefined
|
|
92
|
+
errorBody?: string
|
|
93
|
+
failure?: string
|
|
94
|
+
}): AIModelApiCallEvent => ({
|
|
95
|
+
providerId,
|
|
96
|
+
startedAt,
|
|
97
|
+
durationMs: Date.now() - startedAt,
|
|
98
|
+
request: { method, url, headers, body },
|
|
99
|
+
...extras
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const response = await inner(input, init)
|
|
104
|
+
if (!response.ok) {
|
|
105
|
+
let errorBody: string
|
|
106
|
+
try {
|
|
107
|
+
errorBody = await response.clone().text()
|
|
108
|
+
} catch {
|
|
109
|
+
errorBody = '[could not read error body]'
|
|
110
|
+
}
|
|
111
|
+
safeNotify(listener, buildEvent({ status: response.status, errorBody }))
|
|
112
|
+
} else {
|
|
113
|
+
safeNotify(listener, buildEvent({ status: response.status }))
|
|
114
|
+
}
|
|
115
|
+
return response
|
|
116
|
+
} catch (err) {
|
|
117
|
+
const failure = err instanceof Error ? err.message : String(err)
|
|
118
|
+
safeNotify(listener, buildEvent({ status: undefined, failure }))
|
|
119
|
+
throw err
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -4,7 +4,7 @@ import type {
|
|
|
4
4
|
TextStreamPart,
|
|
5
5
|
ToolSet
|
|
6
6
|
} from 'ai'
|
|
7
|
-
import type { FinishReason, StreamEvent, Usage } from '../
|
|
7
|
+
import type { FinishReason, StreamEvent, Usage } from '../events.js'
|
|
8
8
|
import { mapAiSdkError } from './map-ai-sdk-error.js'
|
|
9
9
|
|
|
10
10
|
const mapUsage = (usage: LanguageModelUsage | undefined): Usage => {
|
|
@@ -0,0 +1,30 @@
|
|
|
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
|
+
export { ANTHROPIC_PROVIDER_ID, ANTHROPIC_PROVIDER_NAME, ANTHROPIC_ENV_VAR, ANTHROPIC_DEFAULT_BASE_URL } from './providers/anthropic/constants.js';
|
|
23
|
+
export { OPENAI_COMPATIBLE_PROVIDER_ID, OPENAI_COMPATIBLE_PROVIDER_NAME, deriveEnvVarName } from './providers/openai-compatible/constants.js';
|
|
24
|
+
/**
|
|
25
|
+
* String literals for the two task slots exposed by {@link SlotName}.
|
|
26
|
+
* `satisfies SlotName` preserves the narrow literal type while verifying at
|
|
27
|
+
* compile time that the value is a valid `SlotName`.
|
|
28
|
+
*/
|
|
29
|
+
export declare const SLOT_DEFAULT = "default";
|
|
30
|
+
export declare const SLOT_FAST = "fast";
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { AIError } from './errors.js';
|
|
2
|
+
/**
|
|
3
|
+
* Why a completion stream stopped producing output.
|
|
4
|
+
*
|
|
5
|
+
* Mirrors the AI SDK's `FinishReason`. Unknown / provider-specific values are
|
|
6
|
+
* normalised to `'other'` rather than silently passed through.
|
|
7
|
+
*/
|
|
8
|
+
export type FinishReason = 'stop' | 'length' | 'content-filter' | 'tool-calls' | 'error' | 'other';
|
|
9
|
+
/**
|
|
10
|
+
* Token usage for a single completion. Every field is optional because
|
|
11
|
+
* not every provider reports every counter.
|
|
12
|
+
*/
|
|
13
|
+
export type Usage = {
|
|
14
|
+
/** Tokens consumed from the prompt. */
|
|
15
|
+
inputTokens?: number;
|
|
16
|
+
/** Tokens generated in the response. */
|
|
17
|
+
outputTokens?: number;
|
|
18
|
+
/** `inputTokens + outputTokens` as reported by the provider. */
|
|
19
|
+
totalTokens?: number;
|
|
20
|
+
/** Of `inputTokens`, how many were a cache hit (Anthropic prompt-cache reads). */
|
|
21
|
+
cacheReadInputTokens?: number;
|
|
22
|
+
/** Of `inputTokens`, how many were written to the prompt cache for future reads. */
|
|
23
|
+
cacheCreationInputTokens?: number;
|
|
24
|
+
/** Subset of `outputTokens` spent on reasoning / thinking content. */
|
|
25
|
+
reasoningTokens?: number;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Provider-agnostic streaming events. Every concrete provider's stream is
|
|
29
|
+
* reduced to this discriminated union so consumers never branch on provider id.
|
|
30
|
+
*
|
|
31
|
+
* Errors arrive as `{ kind: 'error', ... }` events, not as thrown exceptions —
|
|
32
|
+
* this lets the host pump events across an Electron IPC boundary one at a time
|
|
33
|
+
* without needing try/catch around the iterator.
|
|
34
|
+
*
|
|
35
|
+
* `tool-call` and `tool-result` are typed but never produced in the current
|
|
36
|
+
* version (one-shot text completions only). They're reserved for forward compat.
|
|
37
|
+
*/
|
|
38
|
+
export type StreamEvent = {
|
|
39
|
+
kind: 'text-delta';
|
|
40
|
+
delta: string;
|
|
41
|
+
} | {
|
|
42
|
+
kind: 'tool-call';
|
|
43
|
+
id: string;
|
|
44
|
+
name: string;
|
|
45
|
+
input: unknown;
|
|
46
|
+
} | {
|
|
47
|
+
kind: 'tool-result';
|
|
48
|
+
id: string;
|
|
49
|
+
name: string;
|
|
50
|
+
result: unknown;
|
|
51
|
+
} | {
|
|
52
|
+
kind: 'usage-update';
|
|
53
|
+
usage: Usage;
|
|
54
|
+
} | {
|
|
55
|
+
kind: 'finish';
|
|
56
|
+
finishReason: FinishReason;
|
|
57
|
+
usage: Usage;
|
|
58
|
+
} | {
|
|
59
|
+
kind: 'error';
|
|
60
|
+
error: AIError;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Wire-level record of one HTTP request the library made to an upstream
|
|
64
|
+
* provider API. Emitted via {@link AIRegistryOptions.apiCallListener} so the host
|
|
65
|
+
* can persist or surface an invocation log to end users.
|
|
66
|
+
*
|
|
67
|
+
* The listener is invoked exactly once per HTTP request — after the response
|
|
68
|
+
* headers arrive (or after the fetch itself fails). Streaming response bodies
|
|
69
|
+
* are *not* tee'd; only error bodies (HTTP status >= 400) are captured.
|
|
70
|
+
*
|
|
71
|
+
* API keys are redacted from `request.headers` before the event is emitted.
|
|
72
|
+
*/
|
|
73
|
+
export type AIModelApiCallEvent = {
|
|
74
|
+
/** Provider id that owns the SDK client which made the call. */
|
|
75
|
+
providerId: string;
|
|
76
|
+
/** Epoch ms at which the fetch was issued. */
|
|
77
|
+
startedAt: number;
|
|
78
|
+
/**
|
|
79
|
+
* Wall-clock time from fetch start to response headers (or to the fetch
|
|
80
|
+
* promise settling on failure). For streaming responses this is
|
|
81
|
+
* time-to-first-byte, not total stream duration.
|
|
82
|
+
*/
|
|
83
|
+
durationMs: number;
|
|
84
|
+
request: {
|
|
85
|
+
method: string;
|
|
86
|
+
url: string;
|
|
87
|
+
/** Outgoing headers; `authorization` / `x-api-key` / `anthropic-api-key` are redacted. */
|
|
88
|
+
headers: Record<string, string>;
|
|
89
|
+
/** Parsed JSON when the body was a JSON string; the raw string otherwise. */
|
|
90
|
+
body: unknown;
|
|
91
|
+
};
|
|
92
|
+
/** HTTP status, or `undefined` if the fetch itself failed before a response. */
|
|
93
|
+
status: number | undefined;
|
|
94
|
+
/** Response body — populated only when `status >= 400`. */
|
|
95
|
+
errorBody?: string;
|
|
96
|
+
/** Failure reason — populated only when fetch itself threw (network error, abort, …). */
|
|
97
|
+
failure?: string;
|
|
98
|
+
};
|
|
99
|
+
export type AIModelApiCallListener = (event: AIModelApiCallEvent) => void;
|
package/types/index.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
export type { AIProvider, AIModel, CompletionRequest } from './provider.js';
|
|
2
|
-
export type { StreamEvent, Usage, FinishReason } from './
|
|
1
|
+
export type { AIProvider, AIModel, AIModelOptions, CompletionRequest } from './provider.js';
|
|
2
|
+
export type { StreamEvent, Usage, FinishReason, AIModelApiCallEvent, AIModelApiCallListener } from './events.js';
|
|
3
3
|
export type { AISettings, SlotName, SlotConfig, CommonProviderConfig, AnthropicProviderConfig, AnthropicModelConfig, OpenAICompatibleProviderConfig, OpenAICompatibleModelConfig } from './settings.js';
|
|
4
4
|
export { ANTHROPIC_MODELS, ANTHROPIC_DEFAULT_MODEL_ID, ANTHROPIC_DEFAULT_FAST_MODEL_ID, DEFAULT_ANTHROPIC_CACHE_CONFIG, DEFAULT_ANTHROPIC_CAPABILITIES, BUILT_IN_CATALOG, type AIModelCatalog, type AnthropicCatalog, type AnthropicModelDescriptor, type CacheConfiguration, type ModelCapabilities } from '@inkdropapp/ai-catalog';
|
|
5
5
|
export { AIError, NoApiKey, AuthenticationError, RateLimitExceeded, ServerOverloaded, PromptTooLarge, UpstreamError, isAIError, type AIErrorKind } from './errors.js';
|
|
6
6
|
export { KeyStore, type KeyStoreOptions } from './key-store.js';
|
|
7
7
|
export { normalizeBaseURL } from './url.js';
|
|
8
|
-
export { ANTHROPIC_PROVIDER_ID, ANTHROPIC_PROVIDER_NAME, ANTHROPIC_ENV_VAR, ANTHROPIC_DEFAULT_BASE_URL, OPENAI_COMPATIBLE_PROVIDER_ID, OPENAI_COMPATIBLE_PROVIDER_NAME, deriveEnvVarName } from './
|
|
9
|
-
export { AnthropicProvider } from './providers/anthropic.js';
|
|
10
|
-
export { OpenAICompatibleProvider } from './providers/openai-compatible.js';
|
|
8
|
+
export { ANTHROPIC_PROVIDER_ID, ANTHROPIC_PROVIDER_NAME, ANTHROPIC_ENV_VAR, ANTHROPIC_DEFAULT_BASE_URL, OPENAI_COMPATIBLE_PROVIDER_ID, OPENAI_COMPATIBLE_PROVIDER_NAME, deriveEnvVarName, SLOT_DEFAULT, SLOT_FAST } from './constants.js';
|
|
9
|
+
export { AnthropicProvider } from './providers/anthropic/provider.js';
|
|
10
|
+
export { OpenAICompatibleProvider } from './providers/openai-compatible/provider.js';
|
|
11
11
|
export { AIRegistry, type AIRegistryOptions, type ResolvedSlot } from './registry.js';
|
|
@@ -17,32 +17,5 @@
|
|
|
17
17
|
* deriveEnvVarName
|
|
18
18
|
* } from '@inkdropapp/ai/constants'
|
|
19
19
|
*/
|
|
20
|
-
export
|
|
21
|
-
export
|
|
22
|
-
export declare const ANTHROPIC_ENV_VAR = "ANTHROPIC_API_KEY";
|
|
23
|
-
export declare const ANTHROPIC_DEFAULT_BASE_URL = "https://api.anthropic.com";
|
|
24
|
-
/**
|
|
25
|
-
* The `AISettings.providers` key for OpenAI-compatible entries. Use this to
|
|
26
|
-
* tag a settings-UI row as "an OpenAI-compatible provider" (vs. the built-in
|
|
27
|
-
* Anthropic row).
|
|
28
|
-
*
|
|
29
|
-
* Runtime provider ids for OpenAI-compatible entries are user-chosen
|
|
30
|
-
* (`AISettings.providers.openaiCompatible[].id`); this constant is the
|
|
31
|
-
* *type/kind*, not an instance id.
|
|
32
|
-
*/
|
|
33
|
-
export declare const OPENAI_COMPATIBLE_PROVIDER_ID = "openaiCompatible";
|
|
34
|
-
export declare const OPENAI_COMPATIBLE_PROVIDER_NAME = "OpenAI-Compatible";
|
|
35
|
-
/**
|
|
36
|
-
* Derives the env-var name for an OpenAI-compatible provider entry from its
|
|
37
|
-
* user-chosen `id`.
|
|
38
|
-
*
|
|
39
|
-
* Rules: replace any non-alphanumeric character with `_`, collapse runs of `_`,
|
|
40
|
-
* uppercase, append `_API_KEY`.
|
|
41
|
-
*
|
|
42
|
-
* Examples:
|
|
43
|
-
* `openrouter` → `OPENROUTER_API_KEY`
|
|
44
|
-
* `together_ai` → `TOGETHER_AI_API_KEY`
|
|
45
|
-
* `ollama-local` → `OLLAMA_LOCAL_API_KEY`
|
|
46
|
-
* `my.proxy` → `MY_PROXY_API_KEY`
|
|
47
|
-
*/
|
|
48
|
-
export declare const deriveEnvVarName: (id: string) => string;
|
|
20
|
+
export { ANTHROPIC_PROVIDER_ID, ANTHROPIC_PROVIDER_NAME, ANTHROPIC_ENV_VAR, ANTHROPIC_DEFAULT_BASE_URL } from './providers/anthropic/constants.js';
|
|
21
|
+
export { OPENAI_COMPATIBLE_PROVIDER_ID, OPENAI_COMPATIBLE_PROVIDER_NAME, deriveEnvVarName } from './providers/openai-compatible/constants.js';
|
package/types/provider.d.ts
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import type { CacheConfiguration, ModelCapabilities } from '@inkdropapp/ai-catalog';
|
|
2
2
|
import type { ModelMessage } from 'ai';
|
|
3
|
-
import type { StreamEvent } from './
|
|
3
|
+
import type { AIModelApiCallListener, StreamEvent } from './events.js';
|
|
4
|
+
/**
|
|
5
|
+
* Provider-construction options that apply to every model the provider hosts.
|
|
6
|
+
* Threaded by `AIRegistry` into each provider's constructor.
|
|
7
|
+
*/
|
|
8
|
+
export type AIModelOptions = {
|
|
9
|
+
/**
|
|
10
|
+
* Invoked once per upstream HTTP request made by any model on this provider.
|
|
11
|
+
* See {@link AIModelApiCallListener}.
|
|
12
|
+
*/
|
|
13
|
+
apiCallListener?: AIModelApiCallListener;
|
|
14
|
+
};
|
|
4
15
|
/**
|
|
5
16
|
* Inputs for a single one-shot streaming completion.
|
|
6
17
|
*
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type AnthropicProvider as SdkAnthropicProvider } from '@ai-sdk/anthropic';
|
|
2
|
+
import { type AnthropicCatalog } from '@inkdropapp/ai-catalog';
|
|
3
|
+
import type { KeyStore } from '../../key-store.js';
|
|
4
|
+
import type { AIModel, AIModelOptions, AIProvider } from '../../provider.js';
|
|
5
|
+
import type { AnthropicProviderConfig } from '../../settings.js';
|
|
6
|
+
/**
|
|
7
|
+
* The Anthropic (Claude) provider.
|
|
8
|
+
*
|
|
9
|
+
* Wraps `@ai-sdk/anthropic`. Loads its model catalogue from one of:
|
|
10
|
+
* 1. The {@link AnthropicCatalog} passed at construction time (server-distributed).
|
|
11
|
+
* 2. The compiled-in {@link ANTHROPIC_MODELS} list as a fallback.
|
|
12
|
+
*
|
|
13
|
+
* User-declared `config.models` are layered on top, overriding entries with
|
|
14
|
+
* matching ids regardless of source.
|
|
15
|
+
*
|
|
16
|
+
* Lazily constructs the SDK client on first stream and caches it across
|
|
17
|
+
* requests. The cached client is invalidated whenever `setApiKey` /
|
|
18
|
+
* `clearApiKey` is called or the resolved API key changes.
|
|
19
|
+
*/
|
|
20
|
+
export declare class AnthropicProvider implements AIProvider {
|
|
21
|
+
readonly id = "anthropic";
|
|
22
|
+
readonly name = "Anthropic";
|
|
23
|
+
readonly envVarName = "ANTHROPIC_API_KEY";
|
|
24
|
+
readonly baseURL: string;
|
|
25
|
+
private readonly keyStore;
|
|
26
|
+
private readonly modelsById;
|
|
27
|
+
private readonly defaultId;
|
|
28
|
+
private readonly defaultFastId;
|
|
29
|
+
private readonly apiCallListener;
|
|
30
|
+
private sdkClient;
|
|
31
|
+
private sdkClientApiKey;
|
|
32
|
+
constructor(keyStore: KeyStore, config?: AnthropicProviderConfig, catalog?: AnthropicCatalog, options?: AIModelOptions);
|
|
33
|
+
listModels(): AIModel[];
|
|
34
|
+
getModel(id: string): AIModel | undefined;
|
|
35
|
+
defaultModel(): AIModel | undefined;
|
|
36
|
+
defaultFastModel(): AIModel | undefined;
|
|
37
|
+
isAuthenticated(): Promise<boolean>;
|
|
38
|
+
setApiKey(key: string): Promise<void>;
|
|
39
|
+
clearApiKey(): Promise<void>;
|
|
40
|
+
/** Internal — used by AnthropicAIModel to resolve the SDK client lazily. */
|
|
41
|
+
getSdkClient(): Promise<SdkAnthropicProvider>;
|
|
42
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
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 declare const OPENAI_COMPATIBLE_PROVIDER_ID = "openaiCompatible";
|
|
11
|
+
export declare const OPENAI_COMPATIBLE_PROVIDER_NAME = "OpenAI-Compatible";
|
|
12
|
+
/**
|
|
13
|
+
* Derives the env-var name for an OpenAI-compatible provider entry from its
|
|
14
|
+
* user-chosen `id`.
|
|
15
|
+
*
|
|
16
|
+
* Rules: replace any non-alphanumeric character with `_`, collapse runs of `_`,
|
|
17
|
+
* uppercase, append `_API_KEY`.
|
|
18
|
+
*
|
|
19
|
+
* Examples:
|
|
20
|
+
* `openrouter` → `OPENROUTER_API_KEY`
|
|
21
|
+
* `together_ai` → `TOGETHER_AI_API_KEY`
|
|
22
|
+
* `ollama-local` → `OLLAMA_LOCAL_API_KEY`
|
|
23
|
+
* `my.proxy` → `MY_PROXY_API_KEY`
|
|
24
|
+
*/
|
|
25
|
+
export declare const deriveEnvVarName: (id: string) => string;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type OpenAICompatibleProvider as SdkOpenAICompatibleProvider } from '@ai-sdk/openai-compatible';
|
|
2
|
+
import type { KeyStore } from '../../key-store.js';
|
|
3
|
+
import type { AIModel, AIModelOptions, AIProvider } from '../../provider.js';
|
|
4
|
+
import type { OpenAICompatibleProviderConfig } from '../../settings.js';
|
|
5
|
+
/**
|
|
6
|
+
* Provider for any OpenAI-compatible chat-completions endpoint
|
|
7
|
+
* (OpenRouter, Together, Fireworks, Groq, vLLM, Ollama via `/v1`, LiteLLM, …).
|
|
8
|
+
*
|
|
9
|
+
* Each user-named entry in `AISettings.providers.openaiCompatible[]`
|
|
10
|
+
* becomes one instance. The env-var name is derived from the entry's `id`
|
|
11
|
+
* via {@link deriveEnvVarName}; the keyring account is the resolved `baseURL`.
|
|
12
|
+
*
|
|
13
|
+
* Capability flags are user-declared per model (with sensible defaults of
|
|
14
|
+
* `tools: true`, `images: false`, `thinking: false`); the host UI keys off
|
|
15
|
+
* those flags exactly the same as for Anthropic.
|
|
16
|
+
*
|
|
17
|
+
* Some endpoints (Ollama, vLLM, …) require no API key. The provider
|
|
18
|
+
* accommodates this by passing `apiKey: undefined` to the SDK rather than
|
|
19
|
+
* proactively throwing `NoApiKey` — auth failures only surface if the
|
|
20
|
+
* upstream actually returns 401.
|
|
21
|
+
*/
|
|
22
|
+
export declare class OpenAICompatibleProvider implements AIProvider {
|
|
23
|
+
readonly id: string;
|
|
24
|
+
readonly name: string;
|
|
25
|
+
readonly baseURL: string;
|
|
26
|
+
readonly envVarName: string;
|
|
27
|
+
private readonly config;
|
|
28
|
+
private readonly keyStore;
|
|
29
|
+
private readonly modelsById;
|
|
30
|
+
private readonly apiCallListener;
|
|
31
|
+
private sdkClient;
|
|
32
|
+
private sdkClientApiKey;
|
|
33
|
+
constructor(keyStore: KeyStore, config: OpenAICompatibleProviderConfig, options?: AIModelOptions);
|
|
34
|
+
listModels(): AIModel[];
|
|
35
|
+
getModel(id: string): AIModel | undefined;
|
|
36
|
+
defaultModel(): AIModel | undefined;
|
|
37
|
+
defaultFastModel(): AIModel | undefined;
|
|
38
|
+
isAuthenticated(): Promise<boolean>;
|
|
39
|
+
setApiKey(key: string): Promise<void>;
|
|
40
|
+
clearApiKey(): Promise<void>;
|
|
41
|
+
/** Internal — used by OpenAICompatibleAIModel to resolve the SDK client lazily. */
|
|
42
|
+
getSdkClient(): Promise<SdkOpenAICompatibleProvider>;
|
|
43
|
+
/** Internal — exposed for tests / 401 handling: did the user configure an explicit auth value? */
|
|
44
|
+
hasConfiguredKey(): Promise<boolean>;
|
|
45
|
+
/** Internal — used by the model when upstream returns 401: surface a NoApiKey if the user never configured one. */
|
|
46
|
+
ensureAuthenticatedOrThrow(): Promise<void>;
|
|
47
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function validate(data: any, { instancePath, parentData, parentDataProperty, rootData }?: {
|
|
2
|
+
instancePath?: string | undefined;
|
|
3
|
+
rootData?: any;
|
|
4
|
+
}): boolean;
|
|
5
|
+
export default validate10;
|
|
6
|
+
declare function validate10(data: any, { instancePath, parentData, parentDataProperty, rootData }?: {
|
|
7
|
+
instancePath?: string | undefined;
|
|
8
|
+
rootData?: any;
|
|
9
|
+
}): boolean;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { OpenAICompatibleProviderConfig } from '../../settings.js';
|
|
2
|
+
/**
|
|
3
|
+
* Throws if `config` doesn't conform to the OpenAI-compatible provider schema.
|
|
4
|
+
*
|
|
5
|
+
* The validator is precompiled to a standalone ESM module by
|
|
6
|
+
* `scripts/build-validators.mjs`, so loading this module does not call
|
|
7
|
+
* `new Function(...)` — making it safe to use in Electron renderer processes
|
|
8
|
+
* under a strict CSP without `unsafe-eval`.
|
|
9
|
+
*
|
|
10
|
+
* Catches the kinds of mistakes a settings UI or hand-written config can make:
|
|
11
|
+
* missing required fields, wrong types, unknown keys, empty `models[]`, or an
|
|
12
|
+
* empty string where a non-empty one is required.
|
|
13
|
+
*/
|
|
14
|
+
export declare const validateOpenAICompatibleProviderConfig: (config: OpenAICompatibleProviderConfig) => void;
|
package/types/registry.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { AIModelCatalog } from '@inkdropapp/ai-catalog';
|
|
2
|
+
import type { AIModelApiCallListener, StreamEvent } from './events.js';
|
|
2
3
|
import { KeyStore } from './key-store.js';
|
|
3
4
|
import type { AIModel, AIProvider, CompletionRequest } from './provider.js';
|
|
4
5
|
import type { AISettings, SlotName } from './settings.js';
|
|
5
|
-
import type { StreamEvent } from './stream-events.js';
|
|
6
6
|
/** Result of resolving a slot to a concrete `(provider, model)` pair. */
|
|
7
7
|
export type ResolvedSlot = {
|
|
8
8
|
provider: AIProvider;
|
|
@@ -19,6 +19,16 @@ export type AIRegistryOptions = {
|
|
|
19
19
|
catalog?: AIModelCatalog;
|
|
20
20
|
/** Pass an existing `KeyStore` to share its in-memory cache across registries. */
|
|
21
21
|
keyStore?: KeyStore;
|
|
22
|
+
/**
|
|
23
|
+
* Invoked once per upstream HTTP request made by any provider in this
|
|
24
|
+
* registry. The hook is best-effort logging only — exceptions thrown from
|
|
25
|
+
* the listener are swallowed so they cannot interrupt the request path.
|
|
26
|
+
*
|
|
27
|
+
* Use this to back a user-facing "API invocation log" view. Streaming
|
|
28
|
+
* response bodies are not captured; only error bodies (status >= 400) are.
|
|
29
|
+
* See {@link AIModelApiCallEvent}.
|
|
30
|
+
*/
|
|
31
|
+
apiCallListener?: AIModelApiCallListener;
|
|
22
32
|
};
|
|
23
33
|
/**
|
|
24
34
|
* Top-level entrypoint to the library.
|
|
@@ -44,6 +54,7 @@ export declare class AIRegistry {
|
|
|
44
54
|
private providers;
|
|
45
55
|
private settings;
|
|
46
56
|
private catalog;
|
|
57
|
+
private readonly apiCallListener;
|
|
47
58
|
constructor(options: AIRegistryOptions);
|
|
48
59
|
/** All configured providers, sorted alphabetically by id. */
|
|
49
60
|
listProviders(): AIProvider[];
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AIModelApiCallListener } from '../events.js';
|
|
2
|
+
type FetchFn = typeof fetch;
|
|
3
|
+
/**
|
|
4
|
+
* Wraps an underlying `fetch` to emit an {@link AIModelApiCallEvent} per HTTP request.
|
|
5
|
+
*
|
|
6
|
+
* Emits exactly once per call. On HTTP error (`!response.ok`) the response is
|
|
7
|
+
* `clone()`'d and the body is read as text into `event.errorBody`; the
|
|
8
|
+
* original response is returned untouched so the SDK still consumes the
|
|
9
|
+
* stream normally. Successful streaming responses are *not* tee'd — only the
|
|
10
|
+
* status is recorded.
|
|
11
|
+
*/
|
|
12
|
+
export declare const createLoggingFetch: (providerId: string, listener: AIModelApiCallListener, inner?: FetchFn) => FetchFn;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { AIError } from '../errors.js';
|
|
2
|
+
/**
|
|
3
|
+
* Reduces an arbitrary unknown error coming out of the AI SDK to one of our
|
|
4
|
+
* {@link AIError} variants.
|
|
5
|
+
*
|
|
6
|
+
* Mapping rules:
|
|
7
|
+
* - Existing `AIError` → returned unchanged.
|
|
8
|
+
* - `LoadAPIKeyError` → `NoApiKey`.
|
|
9
|
+
* - `APICallError` with status:
|
|
10
|
+
* - 401 / 403 → `AuthenticationError`
|
|
11
|
+
* - 429 → `RateLimitExceeded` (with `retryAfter` parsed from header)
|
|
12
|
+
* - 503 / 529 → `ServerOverloaded` (with `retryAfter`)
|
|
13
|
+
* - 413, or any message that "looks like" a context-length overflow → `PromptTooLarge`
|
|
14
|
+
* - anything else → `UpstreamError` carrying status + cause
|
|
15
|
+
* - Other `AISDKError` / `Error` instances → `UpstreamError` (or `PromptTooLarge`
|
|
16
|
+
* if the message matches).
|
|
17
|
+
*
|
|
18
|
+
* @param providerId - Used to attribute the error in messages and downstream
|
|
19
|
+
* routing decisions.
|
|
20
|
+
*/
|
|
21
|
+
export declare const mapAiSdkError: (error: unknown, providerId: string) => AIError;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { StreamTextResult, ToolSet } from 'ai';
|
|
2
|
+
import type { StreamEvent } from '../events.js';
|
|
3
|
+
/**
|
|
4
|
+
* Adapts the AI SDK's `streamText().fullStream` into the library's
|
|
5
|
+
* provider-agnostic {@link StreamEvent} stream.
|
|
6
|
+
*
|
|
7
|
+
* Three contracts that downstream code relies on:
|
|
8
|
+
*
|
|
9
|
+
* 1. **No throwing.** Errors thrown by the iterator (e.g. mid-stream HTTP
|
|
10
|
+
* failure) are caught and re-emitted as `{ kind: 'error', error }`.
|
|
11
|
+
* The iterator always terminates cleanly, so consumers that pump events
|
|
12
|
+
* over IPC don't need to wrap iteration in try/catch.
|
|
13
|
+
* 2. **Errors are terminal.** When an `error` event from the SDK is observed
|
|
14
|
+
* the iterator returns immediately — any subsequent SDK chunks are dropped.
|
|
15
|
+
* 3. **Unknown SDK part types are silently ignored.** Forward-compat with
|
|
16
|
+
* new AI SDK chunk variants without a library bump.
|
|
17
|
+
*
|
|
18
|
+
* Emits in this order over the lifetime of one completion:
|
|
19
|
+
* `text-delta*` → `usage-update?` → `finish | error`.
|
|
20
|
+
*
|
|
21
|
+
* @param result - The object returned by `streamText()`.
|
|
22
|
+
* Only its `fullStream` is consumed.
|
|
23
|
+
* @param providerId - Provider id used to attribute any wrapped errors.
|
|
24
|
+
*/
|
|
25
|
+
export declare function mapAiSdkStream(result: Pick<StreamTextResult<ToolSet, never, never>, 'fullStream'>, providerId: string): AsyncIterable<StreamEvent>;
|