@inkdropapp/ai 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +472 -0
- package/package.json +44 -0
- package/src/errors.ts +117 -0
- package/src/index.ts +48 -0
- package/src/internal/map-ai-sdk-error.ts +96 -0
- package/src/internal/map-ai-sdk-stream.ts +111 -0
- package/src/key-store.ts +116 -0
- package/src/provider.ts +86 -0
- package/src/providers/anthropic.ts +175 -0
- package/src/providers/openai-compatible.ts +205 -0
- package/src/registry.ts +190 -0
- package/src/settings.ts +105 -0
- package/src/stream-events.ts +53 -0
- package/src/url.ts +41 -0
- package/types/capabilities.d.ts +37 -0
- package/types/catalog.d.ts +32 -0
- package/types/errors.d.ts +69 -0
- package/types/index.d.ts +10 -0
- package/types/internal/map-ai-sdk-error.d.ts +21 -0
- package/types/internal/map-ai-sdk-stream.d.ts +25 -0
- package/types/key-store.d.ts +45 -0
- package/types/provider.d.ts +74 -0
- package/types/providers/anthropic-models.d.ts +30 -0
- package/types/providers/anthropic.d.ts +41 -0
- package/types/providers/openai-compatible.d.ts +59 -0
- package/types/registry.d.ts +98 -0
- package/types/settings.d.ts +94 -0
- package/types/stream-events.d.ts +61 -0
- package/types/url.d.ts +15 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/** Discriminant for {@link AIError} subclasses. */
|
|
2
|
+
export type AIErrorKind = 'no-api-key' | 'rate-limit-exceeded' | 'server-overloaded' | 'authentication' | 'prompt-too-large' | 'upstream';
|
|
3
|
+
/**
|
|
4
|
+
* Base class for every error this library surfaces. Concrete subclasses are
|
|
5
|
+
* tagged with `kind` so consumers can switch on it without instanceof chains.
|
|
6
|
+
*
|
|
7
|
+
* Errors are emitted as `{ kind: 'error', error }` events inside completion
|
|
8
|
+
* streams rather than thrown — consumers that iterate the stream do not need
|
|
9
|
+
* try/catch.
|
|
10
|
+
*/
|
|
11
|
+
export declare abstract class AIError extends Error {
|
|
12
|
+
abstract readonly kind: AIErrorKind;
|
|
13
|
+
constructor(message: string, options?: {
|
|
14
|
+
cause?: unknown;
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Thrown / emitted when a provider has no resolvable API key — neither in its
|
|
19
|
+
* environment variable nor in the system keyring.
|
|
20
|
+
*/
|
|
21
|
+
export declare class NoApiKey extends AIError {
|
|
22
|
+
readonly providerId: string;
|
|
23
|
+
readonly envVarName: string | null;
|
|
24
|
+
readonly kind: "no-api-key";
|
|
25
|
+
constructor(providerId: string, envVarName: string | null);
|
|
26
|
+
}
|
|
27
|
+
/** Upstream returned 401 / 403 — the configured key was rejected. */
|
|
28
|
+
export declare class AuthenticationError extends AIError {
|
|
29
|
+
readonly providerId: string;
|
|
30
|
+
readonly kind: "authentication";
|
|
31
|
+
constructor(providerId: string, message?: string);
|
|
32
|
+
}
|
|
33
|
+
/** Upstream returned 429. `retryAfter` is in seconds, parsed from the response header. */
|
|
34
|
+
export declare class RateLimitExceeded extends AIError {
|
|
35
|
+
readonly providerId: string;
|
|
36
|
+
readonly retryAfter: number | undefined;
|
|
37
|
+
readonly kind: "rate-limit-exceeded";
|
|
38
|
+
constructor(providerId: string, retryAfter: number | undefined, message?: string);
|
|
39
|
+
}
|
|
40
|
+
/** Upstream returned 503 / 529. `retryAfter` is in seconds, parsed from the response header. */
|
|
41
|
+
export declare class ServerOverloaded extends AIError {
|
|
42
|
+
readonly providerId: string;
|
|
43
|
+
readonly retryAfter: number | undefined;
|
|
44
|
+
readonly kind: "server-overloaded";
|
|
45
|
+
constructor(providerId: string, retryAfter: number | undefined, message?: string);
|
|
46
|
+
}
|
|
47
|
+
/** Prompt exceeds the model's context window (HTTP 413, or message says so). */
|
|
48
|
+
export declare class PromptTooLarge extends AIError {
|
|
49
|
+
readonly providerId: string;
|
|
50
|
+
readonly tokenCount: number | undefined;
|
|
51
|
+
readonly maxTokens: number | undefined;
|
|
52
|
+
readonly kind: "prompt-too-large";
|
|
53
|
+
constructor(providerId: string, tokenCount: number | undefined, maxTokens: number | undefined, message?: string);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Catch-all for upstream failures that don't map to a more specific variant.
|
|
57
|
+
* `status` is the HTTP status code when known; `cause` preserves the original
|
|
58
|
+
* AI SDK error for debugging.
|
|
59
|
+
*/
|
|
60
|
+
export declare class UpstreamError extends AIError {
|
|
61
|
+
readonly providerId: string;
|
|
62
|
+
readonly status?: number | undefined;
|
|
63
|
+
readonly kind: "upstream";
|
|
64
|
+
constructor(providerId: string, message: string, status?: number | undefined, options?: {
|
|
65
|
+
cause?: unknown;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/** Type guard for {@link AIError} and any of its subclasses. */
|
|
69
|
+
export declare const isAIError: (error: unknown) => error is AIError;
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type { AIProvider, AIModel, CompletionRequest } from './provider.js';
|
|
2
|
+
export type { StreamEvent, Usage, FinishReason } from './stream-events.js';
|
|
3
|
+
export type { AISettings, SlotName, SlotConfig, CommonProviderConfig, AnthropicProviderConfig, AnthropicModelConfig, OpenAICompatibleProviderConfig, OpenAICompatibleModelConfig } from './settings.js';
|
|
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
|
+
export { AIError, NoApiKey, AuthenticationError, RateLimitExceeded, ServerOverloaded, PromptTooLarge, UpstreamError, isAIError, type AIErrorKind } from './errors.js';
|
|
6
|
+
export { KeyStore, type KeyStoreOptions } from './key-store.js';
|
|
7
|
+
export { normalizeBaseURL } from './url.js';
|
|
8
|
+
export { AnthropicProvider } from './providers/anthropic.js';
|
|
9
|
+
export { OpenAICompatibleProvider, deriveEnvVarName } from './providers/openai-compatible.js';
|
|
10
|
+
export { Registry, type RegistryOptions, type ResolvedSlot } from './registry.js';
|
|
@@ -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 '../stream-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>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export type KeyStoreOptions = {
|
|
2
|
+
/** Keyring service name. Defaults to `inkdrop.ai`. Override in tests. */
|
|
3
|
+
service?: string;
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* URL-keyed credential storage. The keyring entry's account is the *normalised*
|
|
7
|
+
* baseURL, not the provider id — so two endpoints to the same provider (e.g.
|
|
8
|
+
* staging + prod, or two Ollama hosts) get separate entries.
|
|
9
|
+
*
|
|
10
|
+
* Resolution order in {@link KeyStore.getKey}:
|
|
11
|
+
* 1. The provider's environment variable (if a name is given and the var is non-empty).
|
|
12
|
+
* 2. The system keyring (`@napi-rs/keyring`) under `service` + normalised `baseURL`.
|
|
13
|
+
*
|
|
14
|
+
* The cache is keyed on normalised `baseURL` to avoid repeated keyring round-trips.
|
|
15
|
+
* Cache reads do not check the env var — env-var changes mid-process are not
|
|
16
|
+
* reflected unless {@link KeyStore.invalidate} is called.
|
|
17
|
+
*/
|
|
18
|
+
export declare class KeyStore {
|
|
19
|
+
readonly service: string;
|
|
20
|
+
private readonly cache;
|
|
21
|
+
constructor(options?: KeyStoreOptions);
|
|
22
|
+
/**
|
|
23
|
+
* Resolves the API key for a given `baseURL`.
|
|
24
|
+
*
|
|
25
|
+
* @param envVarName - Provider's env-var name, or `null` to skip env lookup.
|
|
26
|
+
* @param baseURL - The endpoint URL. Normalised before keying.
|
|
27
|
+
* @returns The key, or `null` if neither env nor keyring has one.
|
|
28
|
+
*/
|
|
29
|
+
getKey(envVarName: string | null, baseURL: string): Promise<string | null>;
|
|
30
|
+
/** Stores `key` for `baseURL` in the system keyring and the in-memory cache. */
|
|
31
|
+
setKey(baseURL: string, key: string): Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* Removes the keyring entry for `baseURL` and drops it from the cache.
|
|
34
|
+
* No-ops if the entry doesn't exist.
|
|
35
|
+
*/
|
|
36
|
+
deleteKey(baseURL: string): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* Drops cached entries.
|
|
39
|
+
*
|
|
40
|
+
* @param baseURL - When provided, only that URL's cached key is dropped;
|
|
41
|
+
* when omitted, the entire cache is cleared. Call this when the user
|
|
42
|
+
* changes a provider's `baseURL` so the next request re-resolves the key.
|
|
43
|
+
*/
|
|
44
|
+
invalidate(baseURL?: string): void;
|
|
45
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { CacheConfiguration, ModelCapabilities } from '@inkdropapp/ai-catalog';
|
|
2
|
+
import type { ModelMessage } from 'ai';
|
|
3
|
+
import type { StreamEvent } from './stream-events.js';
|
|
4
|
+
/**
|
|
5
|
+
* Inputs for a single one-shot streaming completion.
|
|
6
|
+
*
|
|
7
|
+
* `messages` reuses the AI SDK's `ModelMessage` type so callers can construct
|
|
8
|
+
* messages with the same shape the underlying SDK expects.
|
|
9
|
+
*/
|
|
10
|
+
export type CompletionRequest = {
|
|
11
|
+
messages: ModelMessage[];
|
|
12
|
+
temperature?: number;
|
|
13
|
+
maxOutputTokens?: number;
|
|
14
|
+
/** Forwarded to the underlying SDK call; aborts in-flight HTTP and ends the stream. */
|
|
15
|
+
abortSignal?: AbortSignal;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* A single configured model belonging to a provider.
|
|
19
|
+
*
|
|
20
|
+
* `capabilities` are the source of truth for whether a feature (tools, images,
|
|
21
|
+
* thinking) is available — UI gating and request-construction logic should
|
|
22
|
+
* branch on these flags rather than on `provider.id`.
|
|
23
|
+
*/
|
|
24
|
+
export interface AIModel {
|
|
25
|
+
/** Stable model id as the provider expects it on the wire. */
|
|
26
|
+
readonly id: string;
|
|
27
|
+
/** Human-readable label for menus / picker UIs. */
|
|
28
|
+
readonly displayName: string;
|
|
29
|
+
/** Back-reference to the owning provider. */
|
|
30
|
+
readonly provider: AIProvider;
|
|
31
|
+
readonly capabilities: ModelCapabilities;
|
|
32
|
+
/** Present when the provider supports prompt caching for this model (Anthropic). */
|
|
33
|
+
readonly cacheConfiguration?: CacheConfiguration;
|
|
34
|
+
/**
|
|
35
|
+
* Streams a completion. The returned iterable always terminates — either
|
|
36
|
+
* with a `'finish'` event on success, or with an `'error'` event on failure.
|
|
37
|
+
* Consumers do not need to wrap iteration in try/catch.
|
|
38
|
+
*/
|
|
39
|
+
streamCompletion(request: CompletionRequest): AsyncIterable<StreamEvent>;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* One AI integration (one auth identity + one base URL) exposing a list of
|
|
43
|
+
* `AIModel` instances. Multiple instances of the same provider class can
|
|
44
|
+
* coexist as long as their `id`s differ — that's how the long-tail
|
|
45
|
+
* OpenAI-compatible providers (OpenRouter, Together, Ollama, …) are modelled.
|
|
46
|
+
*/
|
|
47
|
+
export interface AIProvider {
|
|
48
|
+
/** Stable provider id; appears in `AISettings.slots[*].providerId`. */
|
|
49
|
+
readonly id: string;
|
|
50
|
+
/** Human-readable label. */
|
|
51
|
+
readonly name: string;
|
|
52
|
+
/** Resolved base URL for HTTP calls; doubles as the keyring account key. */
|
|
53
|
+
readonly baseURL: string;
|
|
54
|
+
/**
|
|
55
|
+
* Environment variable consulted before the keyring during auth resolution.
|
|
56
|
+
* `null` for providers that cannot have an env-var convention.
|
|
57
|
+
*/
|
|
58
|
+
readonly envVarName: string | null;
|
|
59
|
+
listModels(): AIModel[];
|
|
60
|
+
getModel(id: string): AIModel | undefined;
|
|
61
|
+
/** "Best" model for long-form / general chat tasks. */
|
|
62
|
+
defaultModel(): AIModel | undefined;
|
|
63
|
+
/** Cheap / low-latency model for autocomplete-style features. */
|
|
64
|
+
defaultFastModel(): AIModel | undefined;
|
|
65
|
+
/**
|
|
66
|
+
* `true` iff a key is currently resolvable via env var or system keyring.
|
|
67
|
+
* Does not perform a network round-trip.
|
|
68
|
+
*/
|
|
69
|
+
isAuthenticated(): Promise<boolean>;
|
|
70
|
+
/** Persists `key` to the system keyring under this provider's `baseURL`. */
|
|
71
|
+
setApiKey(key: string): Promise<void>;
|
|
72
|
+
/** Removes the keyring entry for this provider's `baseURL`. */
|
|
73
|
+
clearApiKey(): Promise<void>;
|
|
74
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { CacheConfiguration, ModelCapabilities } from '../capabilities.js';
|
|
2
|
+
/** Static description of one Anthropic model. */
|
|
3
|
+
export type AnthropicModelDescriptor = {
|
|
4
|
+
id: string;
|
|
5
|
+
displayName: string;
|
|
6
|
+
capabilities: ModelCapabilities;
|
|
7
|
+
cacheConfiguration: CacheConfiguration;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Default prompt-cache thresholds applied to every Anthropic model unless the
|
|
11
|
+
* user explicitly overrides them per model.
|
|
12
|
+
*/
|
|
13
|
+
export declare const DEFAULT_ANTHROPIC_CACHE_CONFIG: CacheConfiguration;
|
|
14
|
+
/**
|
|
15
|
+
* Default capability flags for an Anthropic model. Used as the baseline for
|
|
16
|
+
* user-declared models; built-in entries override `maxTokens`/`maxOutputTokens`
|
|
17
|
+
* per model.
|
|
18
|
+
*/
|
|
19
|
+
export declare const DEFAULT_ANTHROPIC_CAPABILITIES: ModelCapabilities;
|
|
20
|
+
/**
|
|
21
|
+
* Hard-coded catalogue of the Claude 4.x models the library ships with.
|
|
22
|
+
*
|
|
23
|
+
* Adding a model here makes it visible in `provider.listModels()` and to the
|
|
24
|
+
* picker UI. A user can also expose ids that aren't here by mapping them
|
|
25
|
+
* onto the SDK directly — but for an Inkdrop-grade BYOK experience, this
|
|
26
|
+
* built-in list is the supported surface.
|
|
27
|
+
*/
|
|
28
|
+
export declare const ANTHROPIC_MODELS: readonly AnthropicModelDescriptor[];
|
|
29
|
+
export declare const DEFAULT_MODEL_ID = "claude-sonnet-4-6";
|
|
30
|
+
export declare const DEFAULT_FAST_MODEL_ID = "claude-haiku-4-5";
|
|
@@ -0,0 +1,41 @@
|
|
|
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, 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 sdkClient;
|
|
30
|
+
private sdkClientApiKey;
|
|
31
|
+
constructor(keyStore: KeyStore, config?: AnthropicProviderConfig, catalog?: AnthropicCatalog);
|
|
32
|
+
listModels(): AIModel[];
|
|
33
|
+
getModel(id: string): AIModel | undefined;
|
|
34
|
+
defaultModel(): AIModel | undefined;
|
|
35
|
+
defaultFastModel(): AIModel | undefined;
|
|
36
|
+
isAuthenticated(): Promise<boolean>;
|
|
37
|
+
setApiKey(key: string): Promise<void>;
|
|
38
|
+
clearApiKey(): Promise<void>;
|
|
39
|
+
/** Internal — used by AnthropicAIModel to resolve the SDK client lazily. */
|
|
40
|
+
getSdkClient(): Promise<SdkAnthropicProvider>;
|
|
41
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { type OpenAICompatibleProvider as SdkOpenAICompatibleProvider } from '@ai-sdk/openai-compatible';
|
|
2
|
+
import type { KeyStore } from '../key-store.js';
|
|
3
|
+
import type { AIModel, AIProvider } from '../provider.js';
|
|
4
|
+
import type { OpenAICompatibleProviderConfig } from '../settings.js';
|
|
5
|
+
/**
|
|
6
|
+
* Derives the env-var name for a user-named provider entry.
|
|
7
|
+
*
|
|
8
|
+
* Rules: replace any non-alphanumeric character with `_`, collapse runs of `_`,
|
|
9
|
+
* uppercase, append `_API_KEY`.
|
|
10
|
+
*
|
|
11
|
+
* Examples:
|
|
12
|
+
* `openrouter` → `OPENROUTER_API_KEY`
|
|
13
|
+
* `together_ai` → `TOGETHER_AI_API_KEY`
|
|
14
|
+
* `ollama-local` → `OLLAMA_LOCAL_API_KEY`
|
|
15
|
+
* `my.proxy` → `MY_PROXY_API_KEY`
|
|
16
|
+
*/
|
|
17
|
+
export declare const deriveEnvVarName: (id: string) => string;
|
|
18
|
+
/**
|
|
19
|
+
* Provider for any OpenAI-compatible chat-completions endpoint
|
|
20
|
+
* (OpenRouter, Together, Fireworks, Groq, vLLM, Ollama via `/v1`, LiteLLM, …).
|
|
21
|
+
*
|
|
22
|
+
* Each user-named entry in `AISettings.providers.openaiCompatible[]`
|
|
23
|
+
* becomes one instance. The env-var name is derived from the entry's `id`
|
|
24
|
+
* via {@link deriveEnvVarName}; the keyring account is the resolved `baseURL`.
|
|
25
|
+
*
|
|
26
|
+
* Capability flags are user-declared per model (with sensible defaults of
|
|
27
|
+
* `tools: true`, `images: false`, `thinking: false`); the host UI keys off
|
|
28
|
+
* those flags exactly the same as for Anthropic.
|
|
29
|
+
*
|
|
30
|
+
* Some endpoints (Ollama, vLLM, …) require no API key. The provider
|
|
31
|
+
* accommodates this by passing `apiKey: undefined` to the SDK rather than
|
|
32
|
+
* proactively throwing `NoApiKey` — auth failures only surface if the
|
|
33
|
+
* upstream actually returns 401.
|
|
34
|
+
*/
|
|
35
|
+
export declare class OpenAICompatibleProvider implements AIProvider {
|
|
36
|
+
readonly id: string;
|
|
37
|
+
readonly name: string;
|
|
38
|
+
readonly baseURL: string;
|
|
39
|
+
readonly envVarName: string;
|
|
40
|
+
private readonly config;
|
|
41
|
+
private readonly keyStore;
|
|
42
|
+
private readonly modelsById;
|
|
43
|
+
private sdkClient;
|
|
44
|
+
private sdkClientApiKey;
|
|
45
|
+
constructor(keyStore: KeyStore, config: OpenAICompatibleProviderConfig);
|
|
46
|
+
listModels(): AIModel[];
|
|
47
|
+
getModel(id: string): AIModel | undefined;
|
|
48
|
+
defaultModel(): AIModel | undefined;
|
|
49
|
+
defaultFastModel(): AIModel | undefined;
|
|
50
|
+
isAuthenticated(): Promise<boolean>;
|
|
51
|
+
setApiKey(key: string): Promise<void>;
|
|
52
|
+
clearApiKey(): Promise<void>;
|
|
53
|
+
/** Internal — used by OpenAICompatibleAIModel to resolve the SDK client lazily. */
|
|
54
|
+
getSdkClient(): Promise<SdkOpenAICompatibleProvider>;
|
|
55
|
+
/** Internal — exposed for tests / 401 handling: did the user configure an explicit auth value? */
|
|
56
|
+
hasConfiguredKey(): Promise<boolean>;
|
|
57
|
+
/** Internal — used by the model when upstream returns 401: surface a NoApiKey if the user never configured one. */
|
|
58
|
+
ensureAuthenticatedOrThrow(): Promise<void>;
|
|
59
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { AIModelCatalog } from '@inkdropapp/ai-catalog';
|
|
2
|
+
import { KeyStore } from './key-store.js';
|
|
3
|
+
import type { AIModel, AIProvider, CompletionRequest } from './provider.js';
|
|
4
|
+
import type { AISettings, SlotName } from './settings.js';
|
|
5
|
+
import type { StreamEvent } from './stream-events.js';
|
|
6
|
+
/** Result of resolving a slot to a concrete `(provider, model)` pair. */
|
|
7
|
+
export type ResolvedSlot = {
|
|
8
|
+
provider: AIProvider;
|
|
9
|
+
model: AIModel;
|
|
10
|
+
};
|
|
11
|
+
export type RegistryOptions = {
|
|
12
|
+
settings: AISettings;
|
|
13
|
+
/**
|
|
14
|
+
* Optional server-distributed catalogue of supported models per provider.
|
|
15
|
+
* When omitted, each provider uses its compiled-in defaults (e.g. `ANTHROPIC_MODELS`).
|
|
16
|
+
* The host typically fetches this from an API and either passes it at
|
|
17
|
+
* construction time or swaps it in later via {@link Registry.updateCatalog}.
|
|
18
|
+
*/
|
|
19
|
+
catalog?: AIModelCatalog;
|
|
20
|
+
/** Pass an existing `KeyStore` to share its in-memory cache across registries. */
|
|
21
|
+
keyStore?: KeyStore;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Top-level entrypoint to the library.
|
|
25
|
+
*
|
|
26
|
+
* Owns one instance of every configured provider, resolves task slots with
|
|
27
|
+
* fallback, and is the only place feature code calls to start a completion.
|
|
28
|
+
*
|
|
29
|
+
* The `Registry` is stateless beyond its provider list — it doesn't observe
|
|
30
|
+
* settings on its own. Call {@link Registry.updateSettings} when settings
|
|
31
|
+
* change to rebuild providers.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```ts
|
|
35
|
+
* const registry = new Registry({ settings })
|
|
36
|
+
* const events = await registry.streamCompletion('default', { messages })
|
|
37
|
+
* for await (const event of events) {
|
|
38
|
+
* if (event.kind === 'text-delta') process.stdout.write(event.delta)
|
|
39
|
+
* }
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export declare class Registry {
|
|
43
|
+
readonly keyStore: KeyStore;
|
|
44
|
+
private providers;
|
|
45
|
+
private settings;
|
|
46
|
+
private catalog;
|
|
47
|
+
constructor(options: RegistryOptions);
|
|
48
|
+
/** All configured providers, sorted alphabetically by id. */
|
|
49
|
+
listProviders(): AIProvider[];
|
|
50
|
+
getProvider(id: string): AIProvider | undefined;
|
|
51
|
+
/** Flat list of every model across every provider. Useful for picker UIs. */
|
|
52
|
+
listAllModels(): {
|
|
53
|
+
provider: AIProvider;
|
|
54
|
+
model: AIModel;
|
|
55
|
+
}[];
|
|
56
|
+
/**
|
|
57
|
+
* Resolves a slot to a concrete `(provider, model)` pair.
|
|
58
|
+
*
|
|
59
|
+
* Order:
|
|
60
|
+
* 1. Explicit binding `settings.slots[slot]`, if both provider and model resolve.
|
|
61
|
+
* 2. For `'fast'` only: fall back to `settings.slots.default`.
|
|
62
|
+
* 3. First authenticated provider in alphabetical id order, taking that
|
|
63
|
+
* provider's `defaultModel()` (or `defaultFastModel()` for the fast slot).
|
|
64
|
+
* Mirrors Zed's `available_fallback_model` without the Zed-Cloud preference.
|
|
65
|
+
* 4. `undefined` if no provider can satisfy the slot.
|
|
66
|
+
*
|
|
67
|
+
* Async because the third step calls `provider.isAuthenticated()`, which may
|
|
68
|
+
* touch the keyring.
|
|
69
|
+
*/
|
|
70
|
+
resolveSlot(slot: SlotName): Promise<ResolvedSlot | undefined>;
|
|
71
|
+
private tryBinding;
|
|
72
|
+
/**
|
|
73
|
+
* Streams a completion for a task slot.
|
|
74
|
+
*
|
|
75
|
+
* Resolves the slot via {@link Registry.resolveSlot}; throws {@link NoApiKey}
|
|
76
|
+
* if no provider can satisfy it (no slot binding, and no authenticated
|
|
77
|
+
* provider available for the implicit fallback). Otherwise returns the
|
|
78
|
+
* model's stream — note that *upstream* errors (auth, rate limit, etc.)
|
|
79
|
+
* arrive as `'error'` events inside the iterable, not as thrown exceptions.
|
|
80
|
+
*/
|
|
81
|
+
streamCompletion(slot: SlotName, request: CompletionRequest): Promise<AsyncIterable<StreamEvent>>;
|
|
82
|
+
/**
|
|
83
|
+
* Replaces the active settings and rebuilds the provider list.
|
|
84
|
+
* The shared `KeyStore` cache is preserved.
|
|
85
|
+
*/
|
|
86
|
+
updateSettings(next: AISettings): void;
|
|
87
|
+
/**
|
|
88
|
+
* Replaces the active model catalogue and rebuilds providers.
|
|
89
|
+
*
|
|
90
|
+
* Pass `undefined` to revert to providers' compiled-in defaults. Typical
|
|
91
|
+
* usage: app starts with no catalogue, fetches one from the server, then
|
|
92
|
+
* calls `updateCatalog(catalog)` once the response lands.
|
|
93
|
+
*
|
|
94
|
+
* The shared `KeyStore` cache and authentication state are preserved.
|
|
95
|
+
*/
|
|
96
|
+
updateCatalog(next: AIModelCatalog | undefined): void;
|
|
97
|
+
private rebuildProviders;
|
|
98
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { CacheConfiguration, ModelCapabilities } from '@inkdropapp/ai-catalog';
|
|
2
|
+
/**
|
|
3
|
+
* Task-routing slot names.
|
|
4
|
+
*
|
|
5
|
+
* - `default` — long-form generation; the user's "main" model.
|
|
6
|
+
* - `fast` — latency-sensitive features (NES, quick rewrite, title suggest).
|
|
7
|
+
*/
|
|
8
|
+
export type SlotName = 'default' | 'fast';
|
|
9
|
+
/** Explicit binding of a slot to a `(provider, model)` pair. */
|
|
10
|
+
export type SlotConfig = {
|
|
11
|
+
providerId: string;
|
|
12
|
+
modelId: string;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Fields every provider config carries. Per-provider configs extend this
|
|
16
|
+
* to add provider-specific knobs (and may narrow `baseURL` to required when
|
|
17
|
+
* there's no sensible default).
|
|
18
|
+
*/
|
|
19
|
+
export type CommonProviderConfig = {
|
|
20
|
+
/** Endpoint root. Falls back to a provider-specific default when omitted. */
|
|
21
|
+
baseURL?: string;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* One entry under an OpenAI-compatible provider's `models[]`. Capability flags
|
|
25
|
+
* are user-declared (a partial — sensible defaults are applied for fields the
|
|
26
|
+
* user omits).
|
|
27
|
+
*/
|
|
28
|
+
export type OpenAICompatibleModelConfig = {
|
|
29
|
+
id: string;
|
|
30
|
+
displayName?: string;
|
|
31
|
+
capabilities?: Partial<ModelCapabilities>;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* One user-named OpenAI-compatible provider entry (e.g. `openrouter`,
|
|
35
|
+
* `together_ai`, `ollama_local`). Each becomes a separate `AIProvider`
|
|
36
|
+
* instance at runtime with its own keyring entry.
|
|
37
|
+
*
|
|
38
|
+
* `baseURL` is required here — there's no sensible default when the user is
|
|
39
|
+
* pointing at an arbitrary endpoint.
|
|
40
|
+
*/
|
|
41
|
+
export type OpenAICompatibleProviderConfig = CommonProviderConfig & {
|
|
42
|
+
id: string;
|
|
43
|
+
displayName?: string;
|
|
44
|
+
/** Endpoint root, e.g. `https://openrouter.ai/api/v1`. */
|
|
45
|
+
baseURL: string;
|
|
46
|
+
models: OpenAICompatibleModelConfig[];
|
|
47
|
+
/** Defaults to `models[0]` when omitted. */
|
|
48
|
+
defaultModelId?: string;
|
|
49
|
+
/** Defaults to `defaultModel()` when omitted. */
|
|
50
|
+
defaultFastModelId?: string;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* One user-declared Anthropic model on top of the built-in Claude catalogue.
|
|
54
|
+
* Useful when Anthropic ships a new model id before this library does.
|
|
55
|
+
*
|
|
56
|
+
* If `id` matches a built-in model, this entry overrides it. Otherwise it's
|
|
57
|
+
* added to the catalogue.
|
|
58
|
+
*/
|
|
59
|
+
export type AnthropicModelConfig = {
|
|
60
|
+
id: string;
|
|
61
|
+
displayName?: string;
|
|
62
|
+
capabilities?: Partial<ModelCapabilities>;
|
|
63
|
+
cacheConfiguration?: CacheConfiguration;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Anthropic provider config.
|
|
67
|
+
*
|
|
68
|
+
* - `baseURL` is only needed for proxy / staging endpoints.
|
|
69
|
+
* - `models` adds (or overrides) Claude entries on top of the built-in
|
|
70
|
+
* {@link ANTHROPIC_MODELS} catalogue.
|
|
71
|
+
*
|
|
72
|
+
* Default-model selection is *not* configured here — use `slots` at the top
|
|
73
|
+
* of `AISettings` to bind features to specific models.
|
|
74
|
+
*/
|
|
75
|
+
export type AnthropicProviderConfig = CommonProviderConfig & {
|
|
76
|
+
models?: AnthropicModelConfig[];
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Top-level AI configuration. The host (Inkdrop main process) constructs this
|
|
80
|
+
* from whichever persistence layer it uses and passes it to `Registry`.
|
|
81
|
+
*
|
|
82
|
+
* The library never reads or writes settings to disk itself.
|
|
83
|
+
*/
|
|
84
|
+
export type AISettings = {
|
|
85
|
+
providers: {
|
|
86
|
+
anthropic?: AnthropicProviderConfig;
|
|
87
|
+
openaiCompatible?: OpenAICompatibleProviderConfig[];
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Per-slot model overrides. Unset slots fall back to the first registered
|
|
91
|
+
* provider's `defaultModel` / `defaultFastModel`.
|
|
92
|
+
*/
|
|
93
|
+
slots?: Partial<Record<SlotName, SlotConfig>>;
|
|
94
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
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
|
+
};
|
package/types/url.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conservative baseURL normalisation for keying credential entries.
|
|
3
|
+
*
|
|
4
|
+
* Rules:
|
|
5
|
+
* - lowercase scheme + host
|
|
6
|
+
* - drop default port (`:443` for https, `:80` for http) for the matching scheme
|
|
7
|
+
* - strip a single trailing `/` from the path
|
|
8
|
+
*
|
|
9
|
+
* Path case, query, and fragment are preserved verbatim. Under-normalising
|
|
10
|
+
* splits keyring entries that should share a key; over-normalising merges
|
|
11
|
+
* entries that should be separate. The rules above are intentionally minimal.
|
|
12
|
+
*
|
|
13
|
+
* @throws if `input` is not a parseable absolute URL.
|
|
14
|
+
*/
|
|
15
|
+
export declare const normalizeBaseURL: (input: string) => string;
|