@stackstackstack/dsh-llm-pi-ai 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.i18n.yaml +6 -0
- package/README.md +204 -0
- package/README.zh.md +205 -0
- package/lib/index.js +1868 -0
- package/lib/invariant.js +23 -0
- package/lib/types/adapter.d.ts +69 -0
- package/lib/types/catalog.d.ts +183 -0
- package/lib/types/config.d.ts +176 -0
- package/lib/types/context.d.ts +24 -0
- package/lib/types/discovery.d.ts +38 -0
- package/lib/types/index.d.ts +68 -0
- package/lib/types/invariant.d.ts +16 -0
- package/lib/types/provider.d.ts +59 -0
- package/lib/types/replay.d.ts +48 -0
- package/lib/types/stream.d.ts +38 -0
- package/package.json +59 -0
package/lib/invariant.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region lib/types/invariant.js
|
|
2
|
+
/**
|
|
3
|
+
* Package-owned invariant companion for `@stackstackstack/dsh-llm-pi-ai`.
|
|
4
|
+
* @module @stackstackstack/dsh-llm-pi-ai/invariant
|
|
5
|
+
*/
|
|
6
|
+
const PACKAGE_NAME = "@stackstackstack/dsh-llm-pi-ai";
|
|
7
|
+
/** Cordis companion plugin name. */
|
|
8
|
+
const name = "llm-pi-ai-invariant";
|
|
9
|
+
/** Service required before the companion can reserve package ownership. */
|
|
10
|
+
const inject = ["invariants"];
|
|
11
|
+
/**
|
|
12
|
+
* No runtime invariant: this package exposes no independent event sequence or mutable data relation
|
|
13
|
+
* beyond contracts enforced at its owning seam.
|
|
14
|
+
*/
|
|
15
|
+
const install = () => {};
|
|
16
|
+
/**
|
|
17
|
+
* Register this package's invariant companion.
|
|
18
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
19
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
20
|
+
*/
|
|
21
|
+
const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
22
|
+
//#endregion
|
|
23
|
+
export { apply, inject, name };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic pi-ai-backed implementation of the Harness LLM seam.
|
|
3
|
+
*
|
|
4
|
+
* Each resolution produces one **immutable** snapshot — the profiles plus a
|
|
5
|
+
* `Models` collection holding the `Provider` each route built — and an
|
|
6
|
+
* operation captures a whole snapshot before its first `await`. A
|
|
7
|
+
* configuration change builds a *new* collection rather than mutating the one
|
|
8
|
+
* in use, because `Models.streamSimple()` is lazy: it resolves the provider
|
|
9
|
+
* when the stream is first consumed, which is after the credential await, so a
|
|
10
|
+
* mutated collection would let a request that started under one configuration
|
|
11
|
+
* finish under another — or fail with a provider that no longer exists. This is
|
|
12
|
+
* what makes the seam's per-step call freeze (`llm.prepareCall()`) hold all the
|
|
13
|
+
* way down: switching models mid-reply takes effect on the next step, never
|
|
14
|
+
* inside the one in flight.
|
|
15
|
+
*
|
|
16
|
+
* Credentials stay outside that collection. The harness resolves a route's key
|
|
17
|
+
* through its own seam and passes it as the request's `apiKey` option, which
|
|
18
|
+
* pi-ai treats as the highest-priority auth override — so `Models` never holds
|
|
19
|
+
* a credential store and the harness keeps its fail-loud reference semantics.
|
|
20
|
+
*
|
|
21
|
+
* @module dsh-llm-pi-ai/adapter
|
|
22
|
+
*/
|
|
23
|
+
import { LlmAdapter } from '@stackstackstack/dsh-llm';
|
|
24
|
+
import type { GenerateOptions, LlmModelInfo, LlmProviderInfo, LlmResolvedModelInfo, ResolvedRetryPolicy, StreamChunk } from '@stackstackstack/dsh-llm';
|
|
25
|
+
import type { AttachmentStore } from '@stackstackstack/dsh-attachment';
|
|
26
|
+
import type { ResolvedPiAiProviderProfile } from './config.ts';
|
|
27
|
+
/** Constructor options for {@link PiAiAdapter}: the two resolution hooks the plugin owns. */
|
|
28
|
+
export interface PiAiAdapterOptions {
|
|
29
|
+
/** Current validated profiles by provider route; called once per operation. */
|
|
30
|
+
profiles: () => ReadonlyMap<string, ResolvedPiAiProviderProfile>;
|
|
31
|
+
/**
|
|
32
|
+
* Resolve the credential for one already-resolved profile; called once per
|
|
33
|
+
* stream call and frozen for that call. `undefined` defers to the route's own
|
|
34
|
+
* pi-ai auth, which for an installed catalog route is its provider-native
|
|
35
|
+
* ambient discovery; the plugin allows that only for a profile naming no
|
|
36
|
+
* credential at all, because a named reference that misses throws `LlmError`
|
|
37
|
+
* `MISSING_CREDENTIAL` rather than falling back.
|
|
38
|
+
*/
|
|
39
|
+
resolveApiKey: (provider: string, profile: ResolvedPiAiProviderProfile) => Promise<string | undefined>;
|
|
40
|
+
/** Resolve the optional durable attachment service at request time. */
|
|
41
|
+
resolveAttachments?: () => AttachmentStore | undefined;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* pi-ai-backed multi-provider adapter. Each operation reads the current
|
|
45
|
+
* profiles, so a configuration change reaches the next request without a
|
|
46
|
+
* restart; model descriptors come from the collection those profiles built.
|
|
47
|
+
*/
|
|
48
|
+
export declare class PiAiAdapter extends LlmAdapter {
|
|
49
|
+
private readonly config;
|
|
50
|
+
private snapshot;
|
|
51
|
+
constructor(config: PiAiAdapterOptions);
|
|
52
|
+
/**
|
|
53
|
+
* The snapshot for the current profiles. Resolution memoizes its result, so
|
|
54
|
+
* an unchanged configuration is recognized by identity; a changed one gets a
|
|
55
|
+
* brand-new collection, leaving any snapshot an operation already captured
|
|
56
|
+
* untouched for as long as that operation holds it.
|
|
57
|
+
*/
|
|
58
|
+
private current;
|
|
59
|
+
/** The profile for one route within one snapshot, or the not-owned failure. */
|
|
60
|
+
private profileOf;
|
|
61
|
+
/** The configured descriptor for one exact route/model pair within one snapshot. */
|
|
62
|
+
private modelOf;
|
|
63
|
+
providerInfo(provider: string): LlmProviderInfo;
|
|
64
|
+
providerRetryPolicy(provider: string): ResolvedRetryPolicy | undefined;
|
|
65
|
+
listModels(provider: string): Promise<readonly LlmModelInfo[]>;
|
|
66
|
+
resolveModel(provider: string, model: string, _signal?: AbortSignal): Promise<LlmResolvedModelInfo>;
|
|
67
|
+
stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=adapter.d.ts.map
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Materialization of one provider route's model catalog. The installed pi-ai
|
|
3
|
+
* catalog supplies defaults keyed by model id, and a profile's own model
|
|
4
|
+
* entries override them field by field, so a route naming a catalog provider
|
|
5
|
+
* stays configuration-free while a route pi-ai has never heard of is fully
|
|
6
|
+
* describable from `settings.yaml`.
|
|
7
|
+
*
|
|
8
|
+
* Every pi-ai `Model` field the harness cannot default is required here rather
|
|
9
|
+
* than at request time: an unserviceable route fails while its configuration is
|
|
10
|
+
* being resolved, which is the earliest point that can name the offending key.
|
|
11
|
+
*
|
|
12
|
+
* @module dsh-llm-pi-ai/catalog
|
|
13
|
+
*/
|
|
14
|
+
import type { Api, Model, ModelThinkingLevel, OpenAICompletionsCompat, Provider } from '@earendil-works/pi-ai';
|
|
15
|
+
/** One request modality a pi-ai model may accept. */
|
|
16
|
+
export type PiAiModality = Model<Api>['input'][number];
|
|
17
|
+
/** Every request modality a profile may declare. */
|
|
18
|
+
export declare const MODALITIES: readonly PiAiModality[];
|
|
19
|
+
/** Every pi-ai thinking level a profile may declare, in escalation order. */
|
|
20
|
+
export declare const THINKING_LEVELS: readonly ModelThinkingLevel[];
|
|
21
|
+
/** The `compat.thinkingFormat` spellings pi-ai accepts on an `openai-completions` model. */
|
|
22
|
+
type PiThinkingFormat = NonNullable<OpenAICompletionsCompat['thinkingFormat']>;
|
|
23
|
+
/**
|
|
24
|
+
* pi-ai thinking formats a profile cannot name: both drive the request through
|
|
25
|
+
* `chatTemplateKwargs`, which this configuration does not expose.
|
|
26
|
+
*/
|
|
27
|
+
type WithheldThinkingFormat = 'chat-template' | 'qwen-chat-template';
|
|
28
|
+
/** One reasoning-dispatch wire format a profile may name. */
|
|
29
|
+
export type PiAiThinkingFormat = Exclude<PiThinkingFormat, WithheldThinkingFormat>;
|
|
30
|
+
/** Reasoning-dispatch wire formats a profile may name, most-reached first. */
|
|
31
|
+
export declare const SUPPORTED_THINKING_FORMATS: readonly PiAiThinkingFormat[];
|
|
32
|
+
/**
|
|
33
|
+
* The installed catalog provider for one route, when pi-ai ships one.
|
|
34
|
+
* @param provider - provider route key.
|
|
35
|
+
* @returns the catalog provider, or `undefined` for a route pi-ai does not ship.
|
|
36
|
+
*/
|
|
37
|
+
export declare function catalogProvider(provider: string): Provider | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* Every provider route the installed pi-ai catalog ships.
|
|
40
|
+
* @returns the catalog provider ids.
|
|
41
|
+
*/
|
|
42
|
+
export declare function catalogProviderIds(): readonly string[];
|
|
43
|
+
/**
|
|
44
|
+
* Whether the installed catalog provider for one route declares an api-key
|
|
45
|
+
* method — the only authentication this adapter obtains on its own.
|
|
46
|
+
*
|
|
47
|
+
* A key is what the harness resolves through its own credential seam and hands
|
|
48
|
+
* pi-ai per request. pi-ai's other method, OAuth, resolves from a *stored*
|
|
49
|
+
* OAuth credential alone: `resolveProviderAuth` has no ambient path for it,
|
|
50
|
+
* this adapter builds its `Models` collection with no credential store, and
|
|
51
|
+
* nothing here runs a login flow. So a provider offering OAuth by itself
|
|
52
|
+
* leaves nothing for this adapter to authenticate with, and the posture such a
|
|
53
|
+
* provider invites — no key configured, credentials discovered by the provider
|
|
54
|
+
* — fails every request with `Provider is not configured`.
|
|
55
|
+
* @param provider - provider route key.
|
|
56
|
+
* @returns whether the catalog provider takes an api key; false for a route
|
|
57
|
+
* pi-ai does not ship, which the caller answers for separately.
|
|
58
|
+
*/
|
|
59
|
+
export declare function catalogProviderTakesApiKey(provider: string): boolean;
|
|
60
|
+
/**
|
|
61
|
+
* The installed catalog models for one route, indexed by model id.
|
|
62
|
+
* @param provider - provider route key.
|
|
63
|
+
* @returns catalog models by id; empty for a route pi-ai does not ship.
|
|
64
|
+
*/
|
|
65
|
+
export declare function catalogModels(provider: string): Map<string, Model<Api>>;
|
|
66
|
+
/**
|
|
67
|
+
* Selectable reasoning efforts for one model: each key is a level the model
|
|
68
|
+
* offers (and selectors show), and its value is the wire spelling dispatch
|
|
69
|
+
* sends for it. `off` alone may leave its value empty — "supported, send
|
|
70
|
+
* nothing" — because for most providers not thinking is the parameter's
|
|
71
|
+
* absence; every other declared level must name a wire value. A level absent
|
|
72
|
+
* from the dict is not offered.
|
|
73
|
+
*/
|
|
74
|
+
export type PiAiReasoningEfforts = Partial<Record<ModelThinkingLevel, string | null>>;
|
|
75
|
+
/**
|
|
76
|
+
* Reasoning-dispatch compatibility switches, set on the route (its models'
|
|
77
|
+
* default) or per model (winning over the route). Only the switches pi-ai's
|
|
78
|
+
* reasoning dispatch reads are offered; the rest of pi-ai's compat surface
|
|
79
|
+
* keeps its baseURL-derived auto-detection. pi-ai types both fields only on
|
|
80
|
+
* `OpenAICompletionsCompat` — the other wire protocols define their reasoning
|
|
81
|
+
* fields in the protocol itself — so resolution rejects a model-level switch
|
|
82
|
+
* anywhere else, while a route-level default skips past models it cannot fit.
|
|
83
|
+
*/
|
|
84
|
+
export interface PiAiCompatProfile {
|
|
85
|
+
/** Reasoning parameter format the endpoint expects; absent keeps the catalog entry's, then pi-ai's baseURL-derived guess. */
|
|
86
|
+
thinkingFormat?: PiAiThinkingFormat;
|
|
87
|
+
/** Whether the endpoint accepts `reasoning_effort`; absent keeps the catalog entry's, then pi-ai's baseURL-derived guess. */
|
|
88
|
+
supportsReasoningEffort?: boolean;
|
|
89
|
+
}
|
|
90
|
+
/** One configured model entry: an id plus the catalog fields it overrides. */
|
|
91
|
+
export interface PiAiModelProfile {
|
|
92
|
+
/** Model id sent to the provider and accepted by {@link GenerateOptions.model}. */
|
|
93
|
+
id: string;
|
|
94
|
+
/** Display name for selectors; defaults to the catalog name, then the id. */
|
|
95
|
+
name?: string;
|
|
96
|
+
/** Maximum combined request and response context in tokens. */
|
|
97
|
+
contextWindow?: number;
|
|
98
|
+
/**
|
|
99
|
+
* Maximum output tokens. Configuring one also makes it this model's
|
|
100
|
+
* per-request default; a value inherited from the installed catalog, or the
|
|
101
|
+
* route's fallback, is the model's capability and never becomes a request
|
|
102
|
+
* default on its own.
|
|
103
|
+
*/
|
|
104
|
+
maxTokens?: number;
|
|
105
|
+
/**
|
|
106
|
+
* Request modalities this model accepts. Absent — or empty, which describes
|
|
107
|
+
* a model that accepts nothing and so states no answer either — keeps the
|
|
108
|
+
* installed catalog entry's modalities, then the route's `defaultInput`.
|
|
109
|
+
* Declaring images is what makes a hand-declared vision model usable, and
|
|
110
|
+
* declaring text alone corrects a catalog model whose gateway does not serve
|
|
111
|
+
* what the catalog records. This is a claim about the endpoint, not a check
|
|
112
|
+
* of it: nothing interrogates a gateway for what it accepts, so a model
|
|
113
|
+
* claiming images its endpoint refuses is refused by the provider instead,
|
|
114
|
+
* mid-turn.
|
|
115
|
+
*/
|
|
116
|
+
input?: PiAiModality[];
|
|
117
|
+
/**
|
|
118
|
+
* Selectable reasoning efforts. Absent inherits the installed catalog
|
|
119
|
+
* entry's capability (a hand-declared model has none and does not reason);
|
|
120
|
+
* `false` declares a non-reasoning model, which is how a profile strips
|
|
121
|
+
* reasoning from a catalog model its gateway cannot serve; a non-empty dict
|
|
122
|
+
* declares the offered levels and their wire spellings.
|
|
123
|
+
*/
|
|
124
|
+
reasoningEfforts?: false | PiAiReasoningEfforts;
|
|
125
|
+
/** Reasoning-dispatch switches for this model, winning over the route's. */
|
|
126
|
+
compat?: PiAiCompatProfile;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Customization of one installed catalog model, keyed by its id in the
|
|
130
|
+
* route's `modelOverrides` dict — the same fields a `models` entry may set,
|
|
131
|
+
* with the id living in the key. Unlike a `models` list, overrides leave the
|
|
132
|
+
* rest of the catalog serving untouched, which is what makes "correct one
|
|
133
|
+
* model, keep the other thirty-seven" a three-line edit.
|
|
134
|
+
*/
|
|
135
|
+
export type PiAiModelOverride = Omit<PiAiModelProfile, 'id'>;
|
|
136
|
+
/** The route-level facts model materialization reads. */
|
|
137
|
+
export interface RouteCatalogRequest {
|
|
138
|
+
/** Provider route key, stamped onto every materialized model. */
|
|
139
|
+
provider: string;
|
|
140
|
+
/** Wire protocol override; absent defers to each catalog model's own API. */
|
|
141
|
+
api?: string;
|
|
142
|
+
/** Endpoint override; absent defers to the catalog model, then the catalog provider. */
|
|
143
|
+
baseURL?: string;
|
|
144
|
+
/** Configured catalog; absent means the whole installed catalog for this route. */
|
|
145
|
+
models?: readonly PiAiModelProfile[];
|
|
146
|
+
/** Installed-catalog customizations by model id; only meaningful while `models` is absent. */
|
|
147
|
+
modelOverrides?: Readonly<Record<string, PiAiModelOverride>>;
|
|
148
|
+
/** Reasoning-dispatch switches for every `openai-completions` model on the route; entries override per field. */
|
|
149
|
+
compat?: PiAiCompatProfile;
|
|
150
|
+
/** Context capacity for a model neither the entry nor the catalog sizes. */
|
|
151
|
+
defaultContextWindow: number;
|
|
152
|
+
/** Output capability for a model neither the entry nor the catalog sizes. */
|
|
153
|
+
defaultMaxTokens: number;
|
|
154
|
+
/** Modalities for a model neither the entry nor the catalog declares. */
|
|
155
|
+
defaultInput: Model<Api>['input'];
|
|
156
|
+
}
|
|
157
|
+
/** One route's materialized catalog, plus the request caps its profile chose. */
|
|
158
|
+
export interface RouteCatalog {
|
|
159
|
+
/** The materialized models in configuration order. */
|
|
160
|
+
models: readonly Model<Api>[];
|
|
161
|
+
/**
|
|
162
|
+
* Per-request output caps this profile explicitly configured, by model id.
|
|
163
|
+
*
|
|
164
|
+
* Separate from `Model.maxTokens` because the two answer different
|
|
165
|
+
* questions: pi-ai requires `maxTokens` as the model's output *capability*,
|
|
166
|
+
* while the harness seam's `defaultMaxTokens` is a cap the deployment chose
|
|
167
|
+
* to send on requests that name none. Materializing a catalog capability as
|
|
168
|
+
* a request default would start capping every request at a number nobody
|
|
169
|
+
* picked, so only an explicit configuration lands here.
|
|
170
|
+
*/
|
|
171
|
+
configuredMaxTokens: ReadonlyMap<string, number>;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Materialize one route's catalog by merging the installed catalog defaults
|
|
175
|
+
* under the configured entries. A route with no configured `models` serves the
|
|
176
|
+
* installed catalog unchanged, which is what keeps an existing
|
|
177
|
+
* `providers: { deepseek: { apiKeyEnv: … } }` profile working untouched.
|
|
178
|
+
* @param request - the route-level catalog facts.
|
|
179
|
+
* @returns the materialized models and the explicitly configured request caps.
|
|
180
|
+
*/
|
|
181
|
+
export declare function resolveRouteModels(request: RouteCatalogRequest): RouteCatalog;
|
|
182
|
+
export {};
|
|
183
|
+
//# sourceMappingURL=catalog.d.ts.map
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration schema and provider-profile validation for the pi-ai adapter.
|
|
3
|
+
* Profiles are a dict keyed by provider route, so the composition base and a
|
|
4
|
+
* user-settings layer merge per provider and the route set is structural.
|
|
5
|
+
*
|
|
6
|
+
* A route key is not required to name an installed pi-ai provider. When it does,
|
|
7
|
+
* that provider's endpoint, protocol, display name, and model catalog are the
|
|
8
|
+
* profile's defaults and the profile overrides them field by field; when it does
|
|
9
|
+
* not, the profile is the whole provider declaration. Resolution therefore ends
|
|
10
|
+
* in a built pi-ai `Provider` per route: everything a request needs is decided
|
|
11
|
+
* once, while the configuration key that made a route unserviceable can still be
|
|
12
|
+
* named in the failure.
|
|
13
|
+
*
|
|
14
|
+
* @module dsh-llm-pi-ai/config
|
|
15
|
+
*/
|
|
16
|
+
import type { CacheRetention, ModelThinkingLevel, Provider, ThinkingBudgets, Transport } from '@earendil-works/pi-ai';
|
|
17
|
+
import z from '@deepseek-ai/schemastery';
|
|
18
|
+
import type { CredentialRef } from '@stackstackstack/dsh-credentials';
|
|
19
|
+
import type { ResolvedRetryPolicy, RetryPolicyConfig } from '@stackstackstack/dsh-llm';
|
|
20
|
+
import type { PiAiCompatProfile, PiAiModality, PiAiModelOverride, PiAiModelProfile } from './catalog.ts';
|
|
21
|
+
/** Default maximum idle interval while an adapter stream read is outstanding. */
|
|
22
|
+
export declare const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 300000;
|
|
23
|
+
/** Context capacity assumed for a model neither configuration nor the catalog sizes. */
|
|
24
|
+
export declare const DEFAULT_CONTEXT_WINDOW = 262144;
|
|
25
|
+
/** Output capability assumed for a model neither configuration nor the catalog sizes. */
|
|
26
|
+
export declare const DEFAULT_MAX_TOKENS = 32768;
|
|
27
|
+
/**
|
|
28
|
+
* Modalities assumed for a model neither configuration nor the catalog
|
|
29
|
+
* declares. Text is the floor every supported protocol certainly carries, so
|
|
30
|
+
* this is the absence of a declaration rather than a guess at the endpoint:
|
|
31
|
+
* nothing can interrogate a gateway for its modalities, and the two wrong
|
|
32
|
+
* answers do not cost the same. Under-claiming refuses the image before it is
|
|
33
|
+
* attached, naming the model. Over-claiming admits one the provider then
|
|
34
|
+
* rejects mid-turn, after the message is durable, leaving the session
|
|
35
|
+
* repeating a request that cannot succeed.
|
|
36
|
+
*/
|
|
37
|
+
export declare const DEFAULT_INPUT: readonly PiAiModality[];
|
|
38
|
+
export type { PiAiCompatProfile, PiAiModality, PiAiModelOverride, PiAiModelProfile, PiAiReasoningEfforts, PiAiThinkingFormat, } from './catalog.ts';
|
|
39
|
+
/** Configuration for one pi-ai provider route; the `providers` dict key IS the route. */
|
|
40
|
+
export interface PiAiProviderProfile {
|
|
41
|
+
/** Credential reference (environment-variable name) resolved per request through `ctx.credentials`. */
|
|
42
|
+
apiKeyEnv?: string;
|
|
43
|
+
/** Name shown by configuration surfaces; defaults to the route key. */
|
|
44
|
+
displayName?: string;
|
|
45
|
+
/**
|
|
46
|
+
* Wire protocol every model on this route speaks. Omission keeps each
|
|
47
|
+
* installed catalog model's own protocol, which is why a catalog route needs
|
|
48
|
+
* no protocol at all; a route the catalog does not ship must name one.
|
|
49
|
+
*/
|
|
50
|
+
api?: string;
|
|
51
|
+
/** Endpoint for this route's models; defaults to the installed catalog's endpoint. */
|
|
52
|
+
baseURL?: string;
|
|
53
|
+
/**
|
|
54
|
+
* This route's model catalog. Omission serves the installed catalog for the
|
|
55
|
+
* route unchanged; an explicit list replaces it, each entry defaulting its
|
|
56
|
+
* unset fields from the installed model of the same id.
|
|
57
|
+
*/
|
|
58
|
+
models?: PiAiModelProfile[];
|
|
59
|
+
/**
|
|
60
|
+
* Installed-catalog customizations by model id: each entry reshapes that
|
|
61
|
+
* one model with the same fields a {@link models} entry takes, while the
|
|
62
|
+
* rest of the catalog keeps serving untouched. Only meaningful on a catalog
|
|
63
|
+
* route with no `models` list — `models` already replaces the catalog, so
|
|
64
|
+
* an override beside it, on a route the catalog does not ship, or naming a
|
|
65
|
+
* model the catalog does not describe is refused rather than skipped.
|
|
66
|
+
*/
|
|
67
|
+
modelOverrides?: Record<string, PiAiModelOverride>;
|
|
68
|
+
/**
|
|
69
|
+
* Reasoning-dispatch switches for every `openai-completions` model on this
|
|
70
|
+
* route; each model's own `compat` overrides per field. What neither sets
|
|
71
|
+
* keeps the installed catalog entry's value, then pi-ai's baseURL-derived
|
|
72
|
+
* detection.
|
|
73
|
+
*/
|
|
74
|
+
compat?: PiAiCompatProfile;
|
|
75
|
+
/**
|
|
76
|
+
* Context capacity for a model this route lists that neither the entry nor
|
|
77
|
+
* the installed catalog sizes (default 262,144). A guess by construction, so
|
|
78
|
+
* a deployment whose gateway serves smaller models corrects it here.
|
|
79
|
+
*/
|
|
80
|
+
defaultContextWindow?: number;
|
|
81
|
+
/**
|
|
82
|
+
* Output capability for a model this route lists that neither the entry nor
|
|
83
|
+
* the installed catalog sizes (default 32,768). This sizes the model; it
|
|
84
|
+
* never becomes a per-request cap on its own.
|
|
85
|
+
*/
|
|
86
|
+
defaultMaxTokens?: number;
|
|
87
|
+
/**
|
|
88
|
+
* Request modalities for a model this route lists that neither its entry's
|
|
89
|
+
* {@link PiAiModelProfile.input} nor the installed catalog declares (default
|
|
90
|
+
* `[text]`). A fallback like the capacities above, not an override: a
|
|
91
|
+
* catalog model keeps the modalities the catalog records for it, and this
|
|
92
|
+
* value never narrows one. A gateway serving vision models the catalog does
|
|
93
|
+
* not describe declares `[text, image]` once here instead of on every entry.
|
|
94
|
+
* Unlike an entry's list, this one may not be empty — nothing sits below it
|
|
95
|
+
* to answer instead.
|
|
96
|
+
*/
|
|
97
|
+
defaultInput?: PiAiModality[];
|
|
98
|
+
/** Provider request headers; Harness attribution wins reserved names. */
|
|
99
|
+
headers?: Record<string, string>;
|
|
100
|
+
/** Provider-neutral pi-ai reasoning level. */
|
|
101
|
+
reasoning?: ModelThinkingLevel;
|
|
102
|
+
/** Token budgets used by reasoning providers that support them. */
|
|
103
|
+
thinkingBudgets?: ThinkingBudgets;
|
|
104
|
+
/** Prompt-cache retention preference. */
|
|
105
|
+
cacheRetention?: CacheRetention;
|
|
106
|
+
/** Streaming transport preference. */
|
|
107
|
+
transport?: Transport;
|
|
108
|
+
/** HTTP/provider SDK timeout in milliseconds. */
|
|
109
|
+
timeoutMs?: number;
|
|
110
|
+
/** WebSocket connection timeout in milliseconds. */
|
|
111
|
+
websocketConnectTimeoutMs?: number;
|
|
112
|
+
/** Maximum provider idle time while one stream read is outstanding. */
|
|
113
|
+
streamIdleTimeoutMs?: number;
|
|
114
|
+
/** Provider-owned model-request retry policy; omission uses normal defaults. */
|
|
115
|
+
retryPolicy?: RetryPolicyConfig;
|
|
116
|
+
}
|
|
117
|
+
/** Validated profile with its route stamped and every adapter-owned default resolved. */
|
|
118
|
+
export interface ResolvedPiAiProviderProfile extends Omit<PiAiProviderProfile, 'apiKeyEnv' | 'retryPolicy' | 'models' | 'displayName'> {
|
|
119
|
+
/** Harness route key and the `Models` collection key (the configuration dict key). */
|
|
120
|
+
provider: string;
|
|
121
|
+
/** Resolved display name for selectors and configuration surfaces. */
|
|
122
|
+
displayName: string;
|
|
123
|
+
/** Validated credential reference, when one is configured. */
|
|
124
|
+
apiKeyEnv?: CredentialRef;
|
|
125
|
+
/** Positive finite provider-idle interval after defaulting. */
|
|
126
|
+
streamIdleTimeoutMs: number;
|
|
127
|
+
/** Immutable retry policy captured with this provider route. */
|
|
128
|
+
retryPolicy: ResolvedRetryPolicy;
|
|
129
|
+
/**
|
|
130
|
+
* The pi-ai provider this route registers, built from the resolved models.
|
|
131
|
+
* Construction happens here so an unserviceable protocol or an underspecified
|
|
132
|
+
* model fails with the rest of resolution, leaving the last good route set
|
|
133
|
+
* serving requests.
|
|
134
|
+
*/
|
|
135
|
+
piProvider: Provider;
|
|
136
|
+
/**
|
|
137
|
+
* Per-request output caps this profile explicitly configured, by model id.
|
|
138
|
+
* The seam materializes one only into a request that names no cap of its
|
|
139
|
+
* own, so a catalog capability must not appear here.
|
|
140
|
+
*/
|
|
141
|
+
configuredMaxTokens: ReadonlyMap<string, number>;
|
|
142
|
+
}
|
|
143
|
+
/** Plugin configuration: the provider routes this instance owns. */
|
|
144
|
+
export interface Config {
|
|
145
|
+
/**
|
|
146
|
+
* pi-ai provider routes, keyed by provider. An empty (or omitted) dict is
|
|
147
|
+
* the dormant settings-driven posture: the adapter mounts with no routes
|
|
148
|
+
* and registers them the moment a settings section supplies profiles.
|
|
149
|
+
*/
|
|
150
|
+
providers?: Record<string, PiAiProviderProfile>;
|
|
151
|
+
}
|
|
152
|
+
/** Runtime schema for {@link Config}. */
|
|
153
|
+
export declare const Config: z<Config>;
|
|
154
|
+
/**
|
|
155
|
+
* Reject a section this adapter could not serve. Registered as the settings
|
|
156
|
+
* namespace's validator, so an unserviceable profile is refused where it is
|
|
157
|
+
* *written* — `settings.mutate` answers `settings-rejected` with the offending
|
|
158
|
+
* route and model named — instead of being stored and then quietly disabling
|
|
159
|
+
* every route in the namespace. It stays a validator rather than a schema
|
|
160
|
+
* transform because the schema is also the shape a configuration surface
|
|
161
|
+
* renders and the value an absent section resolves to; wrapping it would break
|
|
162
|
+
* both.
|
|
163
|
+
* @param config - the resolved section to check.
|
|
164
|
+
* @throws Error naming the route and model that cannot be served.
|
|
165
|
+
*/
|
|
166
|
+
export declare function assertServiceable(config: Config): void;
|
|
167
|
+
/**
|
|
168
|
+
* Validate profiles and return a detached route-keyed map suitable for
|
|
169
|
+
* per-request reads. This is the one explicit resolve step, so an omitted dict
|
|
170
|
+
* resolves to the empty (dormant) route set here rather than through a hidden
|
|
171
|
+
* fallback, and each route's models and pi-ai provider are materialized once.
|
|
172
|
+
* @param providers - configured provider profiles keyed by route.
|
|
173
|
+
* @returns validated profiles in configuration order.
|
|
174
|
+
*/
|
|
175
|
+
export declare function resolveProfiles(providers: Readonly<Record<string, PiAiProviderProfile>> | undefined): Map<string, ResolvedPiAiProviderProfile>;
|
|
176
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harness request-history conversion into pi-ai's Context vocabulary.
|
|
3
|
+
*
|
|
4
|
+
* @module dsh-llm-pi-ai/context
|
|
5
|
+
*/
|
|
6
|
+
import type { GenerateOptions } from '@stackstackstack/dsh-llm';
|
|
7
|
+
import type { AttachmentStore } from '@stackstackstack/dsh-attachment';
|
|
8
|
+
import type { Context as PiContext } from '@earendil-works/pi-ai';
|
|
9
|
+
/**
|
|
10
|
+
* Convert text-only harness history to a synchronous pi-ai Context. Tool
|
|
11
|
+
* result names are recovered from preceding assistant tool calls.
|
|
12
|
+
* @param options - the harness request; `options.system` maps to pi-ai's single `systemPrompt` slot.
|
|
13
|
+
* @returns the pi-ai context; `tools` is omitted when the request declares none.
|
|
14
|
+
*/
|
|
15
|
+
export declare function toPiContext(options: GenerateOptions): PiContext;
|
|
16
|
+
/**
|
|
17
|
+
* Convert harness history to a pi-ai Context while resolving durable images.
|
|
18
|
+
* Tool result names are recovered from preceding assistant tool calls.
|
|
19
|
+
* @param options - the harness request; `options.system` maps to pi-ai's single `systemPrompt` slot.
|
|
20
|
+
* @param attachments - durable byte resolver for image references.
|
|
21
|
+
* @returns the asynchronously resolved pi-ai context.
|
|
22
|
+
*/
|
|
23
|
+
export declare function toPiContext(options: GenerateOptions, attachments: AttachmentStore): Promise<PiContext>;
|
|
24
|
+
//# sourceMappingURL=context.d.ts.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Answering "which models can this provider serve?" for the configuration
|
|
3
|
+
* surface's "fetch available models" action.
|
|
4
|
+
*
|
|
5
|
+
* A route the installed pi-ai catalog ships is answered **from that catalog**,
|
|
6
|
+
* with no network call at all: pi-ai's registry is the authoritative list for
|
|
7
|
+
* its own providers, and it carries the capacities a listing endpoint would
|
|
8
|
+
* not disclose. Only a route the catalog does not describe — a gateway, a
|
|
9
|
+
* self-hosted server — is interrogated over the wire.
|
|
10
|
+
*
|
|
11
|
+
* Neither path is a catalog refresh. Nothing here is stored: the request
|
|
12
|
+
* carries a draft the user is still editing, and the reply is candidate
|
|
13
|
+
* metadata the surface offers for adoption. `settings.yaml` remains the only
|
|
14
|
+
* thing that decides what a route serves.
|
|
15
|
+
*
|
|
16
|
+
* Only OpenAI-compatible protocols are interrogated. Their listing is the one
|
|
17
|
+
* shape a gateway, a self-hosted server, and the official endpoints all agree
|
|
18
|
+
* on, which is the case this action exists for; every other protocol reports
|
|
19
|
+
* that it cannot be interrogated so the surface falls back to hand-entry
|
|
20
|
+
* rather than guessing a response shape.
|
|
21
|
+
*
|
|
22
|
+
* @module dsh-llm-pi-ai/discovery
|
|
23
|
+
*/
|
|
24
|
+
import type { LlmDiscoveredModel, LlmModelDiscoveryRequest } from '@stackstackstack/dsh-llm';
|
|
25
|
+
/**
|
|
26
|
+
* Interrogate one draft provider endpoint for the models it advertises.
|
|
27
|
+
* @param request - the endpoint, protocol, and one-shot credential to use.
|
|
28
|
+
* @param storedApiKey - the credential the named route already stored, asked
|
|
29
|
+
* for only when the draft carries none and only on the path that reaches the
|
|
30
|
+
* network. A configuration surface never holds a stored secret — it edits a
|
|
31
|
+
* redacted descriptor — so without this an already-configured route would be
|
|
32
|
+
* interrogated unauthenticated and answer 401.
|
|
33
|
+
* @returns the advertised models in endpoint order.
|
|
34
|
+
* @throws LlmError when the protocol has no readable listing, the endpoint
|
|
35
|
+
* refuses or fails the request, or the reply is not a model listing.
|
|
36
|
+
*/
|
|
37
|
+
export declare function discoverModels(request: LlmModelDiscoveryRequest, storedApiKey?: () => Promise<string | undefined>): Promise<readonly LlmDiscoveredModel[]>;
|
|
38
|
+
//# sourceMappingURL=discovery.d.ts.map
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic pi-ai-backed LLM adapter plugin. One plugin instance owns a dict of
|
|
3
|
+
* provider routes; a route naming an installed pi-ai provider inherits that
|
|
4
|
+
* provider's endpoint, protocol, and model catalog as defaults, and a route
|
|
5
|
+
* pi-ai does not ship is declared outright. Profile facts resolve per request
|
|
6
|
+
* over the optional `llm-pi-ai` user-settings section and the optional
|
|
7
|
+
* credential seam, so a changed key, endpoint, model, or knob reaches the next
|
|
8
|
+
* request without a restart; a changed *route set* (or a route's
|
|
9
|
+
* registration-captured retry policy) re-registers the same adapter instance
|
|
10
|
+
* in place.
|
|
11
|
+
*
|
|
12
|
+
* ```yaml
|
|
13
|
+
* - id: llm
|
|
14
|
+
* name: '@stackstackstack/dsh-llm-pi-ai'
|
|
15
|
+
* config:
|
|
16
|
+
* providers:
|
|
17
|
+
* # Catalog route: everything but the credential comes from pi-ai.
|
|
18
|
+
* openai:
|
|
19
|
+
* apiKeyEnv: OPENAI_API_KEY
|
|
20
|
+
* retryPolicy:
|
|
21
|
+
* mode: normal
|
|
22
|
+
* maxRetries: 2
|
|
23
|
+
* # Catalog route with the catalog narrowed and one capacity corrected.
|
|
24
|
+
* anthropic:
|
|
25
|
+
* apiKeyEnv: ANTHROPIC_API_KEY
|
|
26
|
+
* models:
|
|
27
|
+
* - id: claude-sonnet-4-5
|
|
28
|
+
* contextWindow: 200000
|
|
29
|
+
* # Hand-declared route: pi-ai ships nothing under this key.
|
|
30
|
+
* acme-gateway:
|
|
31
|
+
* displayName: Acme Gateway
|
|
32
|
+
* apiKeyEnv: ACME_GATEWAY_API_KEY
|
|
33
|
+
* api: openai-completions
|
|
34
|
+
* baseURL: https://gateway.acme.example/v1
|
|
35
|
+
* # Reasoning dialect for a URL pi-ai cannot recognize.
|
|
36
|
+
* compat:
|
|
37
|
+
* thinkingFormat: deepseek
|
|
38
|
+
* models:
|
|
39
|
+
* - id: acme-large
|
|
40
|
+
* name: Acme Large
|
|
41
|
+
* contextWindow: 65536
|
|
42
|
+
* maxTokens: 4096
|
|
43
|
+
* - id: acme-think
|
|
44
|
+
* name: Acme Think
|
|
45
|
+
* contextWindow: 262144
|
|
46
|
+
* maxTokens: 32768
|
|
47
|
+
* # key = selectable level, value = wire spelling; only off may
|
|
48
|
+
* # leave the value empty (supported, send nothing).
|
|
49
|
+
* reasoningEfforts:
|
|
50
|
+
* off:
|
|
51
|
+
* high: high
|
|
52
|
+
* max: ultra
|
|
53
|
+
* ```
|
|
54
|
+
*
|
|
55
|
+
* @module @stackstackstack/dsh-llm-pi-ai
|
|
56
|
+
*/
|
|
57
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
58
|
+
import { Config } from './config.ts';
|
|
59
|
+
export { PiAiAdapter } from './adapter.ts';
|
|
60
|
+
export type { PiAiAdapterOptions } from './adapter.ts';
|
|
61
|
+
export { Config } from './config.ts';
|
|
62
|
+
export type { PiAiCompatProfile, PiAiModality, PiAiModelOverride, PiAiModelProfile, PiAiProviderProfile, PiAiReasoningEfforts, PiAiThinkingFormat, ResolvedPiAiProviderProfile, } from './config.ts';
|
|
63
|
+
export { supportedProtocols } from './provider.ts';
|
|
64
|
+
export declare const name = "llm-pi-ai";
|
|
65
|
+
export declare const inject: string[];
|
|
66
|
+
/** Register one generic pi-ai adapter for all configured provider routes. */
|
|
67
|
+
export declare function apply(ctx: Context, config: Config): void;
|
|
68
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-owned invariant companion for `@stackstackstack/dsh-llm-pi-ai`.
|
|
3
|
+
* @module @stackstackstack/dsh-llm-pi-ai/invariant
|
|
4
|
+
*/
|
|
5
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
6
|
+
/** Cordis companion plugin name. */
|
|
7
|
+
export declare const name = "llm-pi-ai-invariant";
|
|
8
|
+
/** Service required before the companion can reserve package ownership. */
|
|
9
|
+
export declare const inject: string[];
|
|
10
|
+
/**
|
|
11
|
+
* Register this package's invariant companion.
|
|
12
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
13
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
14
|
+
*/
|
|
15
|
+
export declare const apply: (ctx: Context) => Promise<() => void>;
|
|
16
|
+
//# sourceMappingURL=invariant.d.ts.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Construction of the pi-ai `Provider` that one configured route registers into
|
|
3
|
+
* the adapter's `Models` collection.
|
|
4
|
+
*
|
|
5
|
+
* Two constructions, one decision: a route the installed catalog ships, whose
|
|
6
|
+
* profile does not override the wire protocol, **reuses that catalog provider**
|
|
7
|
+
* with its models replaced — the catalog provider owns API implementations this
|
|
8
|
+
* package cannot reconstruct (Bedrock loads its Smithy module through a
|
|
9
|
+
* separate entry point), so rebuilding it from parts would silently narrow
|
|
10
|
+
* which providers work. Every other route — one pi-ai has never heard of, or a
|
|
11
|
+
* catalog route pointed at a different protocol — is built by `createProvider`
|
|
12
|
+
* over the protocol table below.
|
|
13
|
+
*
|
|
14
|
+
* Credentials never reach this module's storage: the harness resolves a route's
|
|
15
|
+
* key through `ctx.credentials` before the request enters pi-ai and hands it
|
|
16
|
+
* over as a stream option, which `Models` presents to `resolve()` as the
|
|
17
|
+
* credential key.
|
|
18
|
+
*
|
|
19
|
+
* @module dsh-llm-pi-ai/provider
|
|
20
|
+
*/
|
|
21
|
+
import type { Api, Model, Provider } from '@earendil-works/pi-ai';
|
|
22
|
+
/**
|
|
23
|
+
* Every wire protocol a configured route may name, most-reached first. The
|
|
24
|
+
* order is the table's and therefore stable; a configuration surface offering
|
|
25
|
+
* a choice presents the first as its default, which is why the protocol a
|
|
26
|
+
* hand-declared gateway most often speaks — and the one endpoint interrogation
|
|
27
|
+
* can read — leads.
|
|
28
|
+
* @returns the supported protocol identifiers.
|
|
29
|
+
*/
|
|
30
|
+
export declare function supportedProtocols(): readonly string[];
|
|
31
|
+
/** The resolved route facts provider construction reads. */
|
|
32
|
+
export interface ProviderSpec {
|
|
33
|
+
/** Provider route key; also the `Models` collection key and each model's `provider`. */
|
|
34
|
+
provider: string;
|
|
35
|
+
/** Display name for selectors and status labels. */
|
|
36
|
+
displayName: string;
|
|
37
|
+
/** Wire protocol override; absent means each model keeps its catalog protocol. */
|
|
38
|
+
api?: string;
|
|
39
|
+
/** Endpoint override already applied to {@link models}; kept for provider-level display. */
|
|
40
|
+
baseURL?: string;
|
|
41
|
+
/** The route's materialized models, in configuration order. */
|
|
42
|
+
models: readonly Model<Api>[];
|
|
43
|
+
/**
|
|
44
|
+
* Whether the profile names a credential, which it does through `apiKeyEnv`
|
|
45
|
+
* alone: configuration carries the reference, never the secret. Only that
|
|
46
|
+
* decides whether {@link routeAuth} adds the harness's own api-key method to
|
|
47
|
+
* a catalog provider that offers none; the key itself still arrives per
|
|
48
|
+
* request, never at construction.
|
|
49
|
+
*/
|
|
50
|
+
namesCredential: boolean;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Build the pi-ai provider for one resolved route.
|
|
54
|
+
* @param spec - the resolved route facts.
|
|
55
|
+
* @returns the provider to register in the adapter's `Models` collection.
|
|
56
|
+
* @throws Error when the route names a wire protocol this build cannot serve.
|
|
57
|
+
*/
|
|
58
|
+
export declare function buildProvider(spec: ProviderSpec): Provider;
|
|
59
|
+
//# sourceMappingURL=provider.d.ts.map
|