@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.
@@ -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;
@@ -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;