@deepseek-ai/dsh-llm 0.1.1-rc.2 → 0.1.2-alpha.2

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.
@@ -5,17 +5,51 @@
5
5
  *
6
6
  * @module @deepseek-ai/dsh-llm
7
7
  */
8
- import { Service } from '@deepseek-ai/cordis';
8
+ var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
9
+ var useValue = arguments.length > 2;
10
+ for (var i = 0; i < initializers.length; i++) {
11
+ value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
12
+ }
13
+ return useValue ? value : void 0;
14
+ };
15
+ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
16
+ function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
17
+ var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
18
+ var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
19
+ var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
20
+ var _, done = false;
21
+ for (var i = decorators.length - 1; i >= 0; i--) {
22
+ var context = {};
23
+ for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
24
+ for (var p in contextIn.access) context.access[p] = contextIn.access[p];
25
+ context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
26
+ var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
27
+ if (kind === "accessor") {
28
+ if (result === void 0) continue;
29
+ if (result === null || typeof result !== "object") throw new TypeError("Object expected");
30
+ if (_ = accept(result.get)) descriptor.get = _;
31
+ if (_ = accept(result.set)) descriptor.set = _;
32
+ if (_ = accept(result.init)) initializers.unshift(_);
33
+ }
34
+ else if (_ = accept(result)) {
35
+ if (kind === "field") initializers.unshift(_);
36
+ else descriptor[key] = _;
37
+ }
38
+ }
39
+ if (target) Object.defineProperty(target, contextIn.name, descriptor);
40
+ done = true;
41
+ };
42
+ import { Remote, RemoteError, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
43
+ import { deepFreeze } from '@deepseek-ai/dsh-util-values';
9
44
  import { freezeMessage } from "./message.js";
10
45
  import { resolveRetryPolicy } from "./retry-policy.js";
11
- import { callConfigEquals, deepFreeze } from "./call-config.js";
46
+ import { callConfigEquals } from "./call-config.js";
12
47
  import { HarnessError, INVALID_CREDENTIAL_CODE } from "./error.js";
13
48
  import { normalizeLlmFailure } from "./adapter-failure.js";
14
49
  import { normalizeApiKey } from "./api-key.js";
15
50
  import { contentHasImage, projectImagesForTextModel } from "./content.js";
16
51
  export * from "./attribution.js";
17
52
  export * from "./brand.js";
18
- export * from "./never.js";
19
53
  export * from "./error.js";
20
54
  export * from "./api-key.js";
21
55
  export * from "./types.js";
@@ -23,7 +57,7 @@ export * from "./content.js";
23
57
  export * from "./message.js";
24
58
  export * from "./retry-policy.js";
25
59
  export { BlockAssembler } from "./assembler.js";
26
- export { callConfigEquals, deepFreeze, isAgentLoopRequest, markAgentLoopRequest } from "./call-config.js";
60
+ export { callConfigEquals, isAgentLoopRequest, markAgentLoopRequest } from "./call-config.js";
27
61
  /**
28
62
  * Typed error for LLM-related failures. Extends {@link HarnessError}, so the
29
63
  * `code` string (e.g. `AUTH`, `RATE_LIMIT`, `NO_ADAPTER`) is shared taxonomy.
@@ -119,6 +153,18 @@ export class LlmAdapter {
119
153
  providerRetryPolicy(_provider) {
120
154
  return undefined;
121
155
  }
156
+ /**
157
+ * Resolve provider-side request-image pricing for one exact model route.
158
+ * The default declares none, so consumers fall back to their own neutral
159
+ * estimate. Implementations must answer synchronously without I/O; the
160
+ * token meter resolves this per measurement.
161
+ * @param _provider - a route passed to `registerAdapter()` for this instance.
162
+ * @param _model - exact model id passed to {@link GenerateOptions.model}.
163
+ * @returns route-owned image pricing, or `undefined` when the route declares none.
164
+ */
165
+ imageRequestPricing(_provider, _model) {
166
+ return undefined;
167
+ }
122
168
  /**
123
169
  * List models this adapter can currently advertise for one owned provider.
124
170
  * The result is advisory: an adapter may accept unlisted model ids, and
@@ -161,614 +207,666 @@ export class LlmAdapter {
161
207
  * The abstract `llm` service: an adapter registry plus a streaming model-call
162
208
  * API, interceptable via the `llm/stream` waterfall.
163
209
  */
164
- export class LlmRuntime extends Service {
165
- adapters = new Map();
166
- directory = new Map();
167
- discoveries = new Map();
168
- constructor(ctx) {
169
- super(ctx, 'llm');
170
- }
171
- /** Notify topology observers without letting one broken listener veto the commit. */
172
- emitAdaptersUpdated() {
173
- // Cordis emit uses Array.map: one synchronous throw starves later
174
- // listeners. Registry notifications are non-vetoing, so contain each
175
- // callback independently; INVARIANT-coded failures still surface.
176
- let invariantFailure;
177
- for (const listener of this.ctx.events.dispatch('emit', ['llm/adapters-updated'])) {
178
- try {
179
- const returned = listener();
180
- if (returned != null && typeof returned.then === 'function') {
181
- // An emit listener may still be an async function; its rejection
182
- // cannot reach the synchronous INVARIANT rethrow below, so it is
183
- // contained here instead of becoming an unhandled rejection.
184
- void Promise.resolve(returned).then(undefined, (error) => {
185
- this.warnAdaptersListenerFailure(error);
186
- });
210
+ let LlmRuntime = (() => {
211
+ let _classSuper = TypertRemoteService;
212
+ let _instanceExtraInitializers = [];
213
+ let _listProviders_decorators;
214
+ let _listConfigurableProviders_decorators;
215
+ let _remoteDiscoverModels_decorators;
216
+ return class LlmRuntime extends _classSuper {
217
+ static {
218
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
219
+ _listProviders_decorators = [Remote];
220
+ _listConfigurableProviders_decorators = [Remote];
221
+ _remoteDiscoverModels_decorators = [Remote('discoverModels')];
222
+ __esDecorate(this, null, _listProviders_decorators, { kind: "method", name: "listProviders", static: false, private: false, access: { has: obj => "listProviders" in obj, get: obj => obj.listProviders }, metadata: _metadata }, null, _instanceExtraInitializers);
223
+ __esDecorate(this, null, _listConfigurableProviders_decorators, { kind: "method", name: "listConfigurableProviders", static: false, private: false, access: { has: obj => "listConfigurableProviders" in obj, get: obj => obj.listConfigurableProviders }, metadata: _metadata }, null, _instanceExtraInitializers);
224
+ __esDecorate(this, null, _remoteDiscoverModels_decorators, { kind: "method", name: "remoteDiscoverModels", static: false, private: false, access: { has: obj => "remoteDiscoverModels" in obj, get: obj => obj.remoteDiscoverModels }, metadata: _metadata }, null, _instanceExtraInitializers);
225
+ if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
226
+ }
227
+ adapters = (__runInitializers(this, _instanceExtraInitializers), new Map());
228
+ directory = new Map();
229
+ discoveries = new Map();
230
+ constructor(ctx) {
231
+ super(ctx, 'llm');
232
+ }
233
+ /** Notify topology observers without letting one broken listener veto the commit. */
234
+ emitAdaptersUpdated() {
235
+ // Cordis emit uses Array.map: one synchronous throw starves later
236
+ // listeners. Registry notifications are non-vetoing, so contain each
237
+ // callback independently; INVARIANT-coded failures still surface.
238
+ let invariantFailure;
239
+ for (const listener of this.ctx.events.dispatch('emit', ['llm/adapters-updated'])) {
240
+ try {
241
+ const returned = listener();
242
+ if (returned != null && typeof returned.then === 'function') {
243
+ // An emit listener may still be an async function; its rejection
244
+ // cannot reach the synchronous INVARIANT rethrow below, so it is
245
+ // contained here instead of becoming an unhandled rejection.
246
+ void Promise.resolve(returned).then(undefined, (error) => {
247
+ this.warnAdaptersListenerFailure(error);
248
+ });
249
+ }
187
250
  }
188
- }
189
- catch (error) {
190
- if (error?.code === 'INVARIANT') {
191
- invariantFailure ??= error;
192
- continue;
251
+ catch (error) {
252
+ if (error?.code === 'INVARIANT') {
253
+ invariantFailure ??= error;
254
+ continue;
255
+ }
256
+ this.warnAdaptersListenerFailure(error);
193
257
  }
194
- this.warnAdaptersListenerFailure(error);
195
258
  }
259
+ if (invariantFailure !== undefined)
260
+ throw invariantFailure;
196
261
  }
197
- if (invariantFailure !== undefined)
198
- throw invariantFailure;
199
- }
200
- /** Contained-listener diagnostic shared by the sync and async failure paths. */
201
- warnAdaptersListenerFailure(error) {
202
- this.ctx.logger.warn('llm: an llm/adapters-updated listener failed');
203
- this.ctx.logger.warn(error);
204
- }
205
- /**
206
- * Register an adapter for the given provider routes. Throws `LlmError` with code
207
- * `DUPLICATE_ADAPTER` if any provider already has an adapter (all-or-nothing).
208
- * Disposed with the fiber.
209
- * @param providers - every provider route this adapter should serve.
210
- * @param adapter - the adapter that streams calls for those providers.
211
- * @returns the disposer, carrying {@link AdapterRegistrationHandle.replace}.
212
- */
213
- registerAdapter(providers, adapter) {
214
- // The routes this registration currently holds; `replace` rewrites it, and
215
- // the disposer releases whatever it holds at disposal time.
216
- const owned = new Set();
217
- // The disposer has run: `owned` being empty cannot say so on its own,
218
- // because `replace([])` legally leaves a live registration holding none.
219
- let released = false;
220
- const dispose = this.ctx.effect(function* () {
221
- if (providers.length === 0)
222
- throw new LlmError('an adapter must register at least one provider', 'INVALID_ADAPTER');
223
- this.commitRoutes(owned, this.prepareRoutes(providers, adapter, owned));
224
- yield () => {
225
- released = true;
226
- for (const provider of owned)
227
- this.adapters.delete(provider);
228
- owned.clear();
229
- this.emitAdaptersUpdated();
230
- };
231
- }.bind(this), 'llm.registerAdapter()');
232
- // ctx.effect's disposer returns Promise<void>; our disposer API is
233
- // synchronous fire-and-forget — discard the (always-resolved) promise.
234
- const handle = (() => void dispose());
235
- handle.replace = (next) => {
236
- // Registering here would leak: the effect's disposer already ran, so
237
- // nothing remains to release whatever this call would put in the map.
238
- if (released) {
239
- throw new LlmError('a disposed adapter registration cannot replace its routes', 'REGISTRATION_DISPOSED');
240
- }
241
- this.commitRoutes(owned, this.prepareRoutes(next, adapter, owned));
242
- };
243
- return handle;
244
- }
245
- /**
246
- * Validate one candidate route set for `adapter`, treating routes this
247
- * registration already holds as available. Nothing is mutated: a rejected
248
- * candidate leaves the registry exactly as it was.
249
- */
250
- prepareRoutes(providers, adapter, owned) {
251
- const unique = new Set();
252
- const registrations = [];
253
- for (const provider of providers) {
254
- if (provider.length === 0)
255
- throw new LlmError('adapter provider names must be non-empty', 'INVALID_ADAPTER');
256
- if (unique.has(provider) || (this.adapters.has(provider) && !owned.has(provider))) {
257
- throw new LlmError(`an adapter for provider "${provider}" is already registered`, 'DUPLICATE_ADAPTER');
258
- }
259
- const info = adapter.providerInfo(provider);
260
- if (typeof info.id !== 'string' || info.id !== provider || typeof info.name !== 'string' || info.name.length === 0) {
261
- throw new LlmError(`adapter metadata for provider "${provider}" must preserve its id and have a non-empty name`, 'INVALID_ADAPTER');
262
- }
263
- unique.add(provider);
264
- const retryPolicy = adapter.providerRetryPolicy(provider)
265
- ?? resolveRetryPolicy(undefined, `llm: provider "${provider}" retryPolicy`);
266
- registrations.push({
267
- adapter,
268
- provider: { id: info.id, name: info.name },
269
- retryPolicy,
270
- });
262
+ /** Contained-listener diagnostic shared by the sync and async failure paths. */
263
+ warnAdaptersListenerFailure(error) {
264
+ this.ctx.logger.warn('llm: an llm/adapters-updated listener failed');
265
+ this.ctx.logger.warn(error);
271
266
  }
272
- return registrations;
273
- }
274
- /**
275
- * Swap this registration's routes for the prepared ones in one synchronous
276
- * section, so no observer can see the registry between the release and the
277
- * re-registration. The route set's one mutation point is also where
278
- * `llm/adapters-updated` is published, so a `replace` announces itself
279
- * exactly like a first registration.
280
- */
281
- commitRoutes(owned, registrations) {
282
- for (const provider of owned)
283
- this.adapters.delete(provider);
284
- owned.clear();
285
- for (const registration of registrations) {
286
- this.adapters.set(registration.provider.id, registration);
287
- owned.add(registration.provider.id);
288
- }
289
- this.emitAdaptersUpdated();
290
- }
291
- /**
292
- * Describe provider routes with a registered adapter.
293
- * @returns detached provider metadata in registration order.
294
- */
295
- listProviders() {
296
- return [...this.adapters.values()].map(({ provider }) => ({ ...provider }));
297
- }
298
- /**
299
- * Declare provider routes an adapter plugin can activate through
300
- * configuration. Registration is all-or-nothing: an empty list, invalid
301
- * entry, or a provider already declared by any registration throws
302
- * `LlmError` without registering the rest. Disposed with the fiber.
303
- * @param entries - every configurable provider this plugin owns.
304
- * @returns a handle that withdraws all of them, and can atomically replace them.
305
- */
306
- registerConfigurableProviders(entries) {
307
- let held = [];
308
- let disposed = false;
309
267
  /**
310
- * Validate a candidate set in full against everything this registration
311
- * does not already hold, then publish it. Nothing is written until the
312
- * whole set passes, so a refused candidate leaves the current entries in
313
- * place — the property that makes `replace` a swap rather than a
314
- * delete-then-add that can strand the directory empty.
268
+ * Register an adapter for the given provider routes. Throws `LlmError` with code
269
+ * `DUPLICATE_ADAPTER` if any provider already has an adapter (all-or-nothing).
270
+ * Disposed with the fiber.
271
+ * @param providers - every provider route this adapter should serve.
272
+ * @param adapter - the adapter that streams calls for those providers.
273
+ * @returns the disposer, carrying {@link AdapterRegistrationHandle.replace}.
315
274
  */
316
- const commit = (candidates) => {
317
- const detached = [];
318
- const own = new Set(held.map(entry => entry.provider));
319
- for (const entry of candidates) {
320
- if (entry.provider.length === 0 || entry.displayName.length === 0 || entry.settingsNs.length === 0) {
321
- throw new LlmError('configurable providers need a non-empty provider, displayName, and settingsNs', 'INVALID_DIRECTORY');
275
+ registerAdapter(providers, adapter) {
276
+ // The routes this registration currently holds; `replace` rewrites it, and
277
+ // the disposer releases whatever it holds at disposal time.
278
+ const owned = new Set();
279
+ // The disposer has run: `owned` being empty cannot say so on its own,
280
+ // because `replace([])` legally leaves a live registration holding none.
281
+ let released = false;
282
+ const dispose = this.ctx.effect(function* () {
283
+ if (providers.length === 0)
284
+ throw new LlmError('an adapter must register at least one provider', 'INVALID_ADAPTER');
285
+ this.commitRoutes(owned, this.prepareRoutes(providers, adapter, owned));
286
+ yield () => {
287
+ released = true;
288
+ for (const provider of owned)
289
+ this.adapters.delete(provider);
290
+ owned.clear();
291
+ this.emitAdaptersUpdated();
292
+ };
293
+ }.bind(this), 'llm.registerAdapter()');
294
+ // ctx.effect's disposer returns Promise<void>; our disposer API is
295
+ // synchronous fire-and-forget — discard the (always-resolved) promise.
296
+ const handle = (() => void dispose());
297
+ handle.replace = (next) => {
298
+ // Registering here would leak: the effect's disposer already ran, so
299
+ // nothing remains to release whatever this call would put in the map.
300
+ if (released) {
301
+ throw new LlmError('a disposed adapter registration cannot replace its routes', 'REGISTRATION_DISPOSED');
322
302
  }
323
- if (entry.settingsPath.some(segment => segment.length === 0)) {
324
- throw new LlmError(`configurable provider "${entry.provider}" has an empty settingsPath segment`, 'INVALID_DIRECTORY');
303
+ this.commitRoutes(owned, this.prepareRoutes(next, adapter, owned));
304
+ };
305
+ return handle;
306
+ }
307
+ /**
308
+ * Validate one candidate route set for `adapter`, treating routes this
309
+ * registration already holds as available. Nothing is mutated: a rejected
310
+ * candidate leaves the registry exactly as it was.
311
+ */
312
+ prepareRoutes(providers, adapter, owned) {
313
+ const unique = new Set();
314
+ const registrations = [];
315
+ for (const provider of providers) {
316
+ if (provider.length === 0)
317
+ throw new LlmError('adapter provider names must be non-empty', 'INVALID_ADAPTER');
318
+ if (unique.has(provider) || (this.adapters.has(provider) && !owned.has(provider))) {
319
+ throw new LlmError(`an adapter for provider "${provider}" is already registered`, 'DUPLICATE_ADAPTER');
325
320
  }
326
- if ((this.directory.has(entry.provider) && !own.has(entry.provider))
327
- || detached.some(seen => seen.provider === entry.provider)) {
328
- throw new LlmError(`configurable provider "${entry.provider}" is already declared`, 'DUPLICATE_DIRECTORY');
321
+ const info = adapter.providerInfo(provider);
322
+ if (typeof info.id !== 'string' || info.id !== provider || typeof info.name !== 'string' || info.name.length === 0) {
323
+ throw new LlmError(`adapter metadata for provider "${provider}" must preserve its id and have a non-empty name`, 'INVALID_ADAPTER');
329
324
  }
330
- detached.push({ ...entry, settingsPath: [...entry.settingsPath] });
325
+ unique.add(provider);
326
+ const retryPolicy = adapter.providerRetryPolicy(provider)
327
+ ?? resolveRetryPolicy(undefined, `llm: provider "${provider}" retryPolicy`);
328
+ registrations.push({
329
+ adapter,
330
+ provider: { id: info.id, name: info.name },
331
+ retryPolicy,
332
+ });
331
333
  }
332
- for (const entry of held)
333
- this.directory.delete(entry.provider);
334
- for (const entry of detached)
335
- this.directory.set(entry.provider, entry);
336
- held = detached;
337
- this.emitAdaptersUpdated();
338
- };
339
- const dispose = this.ctx.effect(function* () {
340
- if (entries.length === 0) {
341
- throw new LlmError('a configurable-provider registration must declare at least one provider', 'INVALID_DIRECTORY');
334
+ return registrations;
335
+ }
336
+ /**
337
+ * Swap this registration's routes for the prepared ones in one synchronous
338
+ * section, so no observer can see the registry between the release and the
339
+ * re-registration. The route set's one mutation point is also where
340
+ * `llm/adapters-updated` is published, so a `replace` announces itself
341
+ * exactly like a first registration.
342
+ */
343
+ commitRoutes(owned, registrations) {
344
+ for (const provider of owned)
345
+ this.adapters.delete(provider);
346
+ owned.clear();
347
+ for (const registration of registrations) {
348
+ this.adapters.set(registration.provider.id, registration);
349
+ owned.add(registration.provider.id);
342
350
  }
343
- commit(entries);
344
- yield () => {
345
- disposed = true;
351
+ this.emitAdaptersUpdated();
352
+ }
353
+ /**
354
+ * Describe provider routes with a registered adapter.
355
+ * @returns detached provider metadata in registration order.
356
+ */
357
+ listProviders() {
358
+ return [...this.adapters.values()].map(({ provider }) => ({ ...provider }));
359
+ }
360
+ /**
361
+ * Declare provider routes an adapter plugin can activate through
362
+ * configuration. Registration is all-or-nothing: an empty list, invalid
363
+ * entry, or a provider already declared by any registration throws
364
+ * `LlmError` without registering the rest. Disposed with the fiber.
365
+ * @param entries - every configurable provider this plugin owns.
366
+ * @returns a handle that withdraws all of them, and can atomically replace them.
367
+ */
368
+ registerConfigurableProviders(entries) {
369
+ let held = [];
370
+ let disposed = false;
371
+ /**
372
+ * Validate a candidate set in full against everything this registration
373
+ * does not already hold, then publish it. Nothing is written until the
374
+ * whole set passes, so a refused candidate leaves the current entries in
375
+ * place — the property that makes `replace` a swap rather than a
376
+ * delete-then-add that can strand the directory empty.
377
+ */
378
+ const commit = (candidates) => {
379
+ const detached = [];
380
+ const own = new Set(held.map(entry => entry.provider));
381
+ for (const entry of candidates) {
382
+ if (entry.provider.length === 0 || entry.displayName.length === 0 || entry.settingsNs.length === 0) {
383
+ throw new LlmError('configurable providers need a non-empty provider, displayName, and settingsNs', 'INVALID_DIRECTORY');
384
+ }
385
+ if (entry.settingsPath.some(segment => segment.length === 0)) {
386
+ throw new LlmError(`configurable provider "${entry.provider}" has an empty settingsPath segment`, 'INVALID_DIRECTORY');
387
+ }
388
+ if ((this.directory.has(entry.provider) && !own.has(entry.provider))
389
+ || detached.some(seen => seen.provider === entry.provider)) {
390
+ throw new LlmError(`configurable provider "${entry.provider}" is already declared`, 'DUPLICATE_DIRECTORY');
391
+ }
392
+ detached.push({ ...entry, settingsPath: [...entry.settingsPath] });
393
+ }
346
394
  for (const entry of held)
347
395
  this.directory.delete(entry.provider);
348
- held = [];
396
+ for (const entry of detached)
397
+ this.directory.set(entry.provider, entry);
398
+ held = detached;
349
399
  this.emitAdaptersUpdated();
350
400
  };
351
- }.bind(this), 'llm.registerConfigurableProviders()');
352
- const handle = (() => void dispose());
353
- handle.replace = (next) => {
354
- if (disposed) {
355
- throw new LlmError('this configurable-provider registration was disposed', 'REGISTRATION_DISPOSED');
401
+ const dispose = this.ctx.effect(function* () {
402
+ if (entries.length === 0) {
403
+ throw new LlmError('a configurable-provider registration must declare at least one provider', 'INVALID_DIRECTORY');
404
+ }
405
+ commit(entries);
406
+ yield () => {
407
+ disposed = true;
408
+ for (const entry of held)
409
+ this.directory.delete(entry.provider);
410
+ held = [];
411
+ this.emitAdaptersUpdated();
412
+ };
413
+ }.bind(this), 'llm.registerConfigurableProviders()');
414
+ const handle = (() => void dispose());
415
+ handle.replace = (next) => {
416
+ if (disposed) {
417
+ throw new LlmError('this configurable-provider registration was disposed', 'REGISTRATION_DISPOSED');
418
+ }
419
+ commit(next);
420
+ };
421
+ return handle;
422
+ }
423
+ /**
424
+ * List every declared configurable provider, registered or dormant.
425
+ * @returns detached directory entries in declaration order.
426
+ */
427
+ listConfigurableProviders() {
428
+ return [...this.directory.values()].map(entry => ({ ...entry, settingsPath: [...entry.settingsPath] }));
429
+ }
430
+ /**
431
+ * Offer to interrogate provider endpoints on behalf of the settings
432
+ * namespace this plugin owns. The namespace is the key because that is what
433
+ * a configuration surface already holds from the configurable-provider
434
+ * directory, and because a provider being *added* has no route to name yet.
435
+ * Disposed with the fiber.
436
+ * @param settingsNs - the namespace whose profiles this discovery serves.
437
+ * @param discover - interrogates one endpoint and must honor the supplied signal.
438
+ * @returns the disposer that withdraws the offer.
439
+ */
440
+ registerModelDiscovery(settingsNs, discover) {
441
+ const dispose = this.ctx.effect(function* () {
442
+ if (settingsNs.length === 0) {
443
+ throw new LlmError('model discovery needs a non-empty settings namespace', 'INVALID_DISCOVERY');
444
+ }
445
+ if (this.discoveries.has(settingsNs)) {
446
+ throw new LlmError(`model discovery for "${settingsNs}" is already registered`, 'DUPLICATE_DISCOVERY');
447
+ }
448
+ this.discoveries.set(settingsNs, discover);
449
+ yield () => {
450
+ this.discoveries.delete(settingsNs);
451
+ };
452
+ }.bind(this), 'llm.registerModelDiscovery()');
453
+ return () => void dispose();
454
+ }
455
+ /**
456
+ * Interrogate one provider endpoint for the models it advertises. The
457
+ * request describes a draft, not a stored route, so nothing here reads or
458
+ * writes settings or credentials — the caller owns both, and the reply is
459
+ * candidate metadata a surface may offer for adoption.
460
+ * @param settingsNs - namespace whose registered discovery serves this draft.
461
+ * @param request - the endpoint, protocol, and one-shot credential to use.
462
+ * @param signal - caller cancellation.
463
+ * @returns the advertised models, deduplicated in endpoint order.
464
+ */
465
+ async discoverModels(settingsNs, request, signal) {
466
+ const discover = this.discoveries.get(settingsNs);
467
+ if (discover === undefined) {
468
+ throw new LlmError(`no model discovery is registered for "${settingsNs}"`, 'NO_DISCOVERY');
356
469
  }
357
- commit(next);
358
- };
359
- return handle;
360
- }
361
- /**
362
- * List every declared configurable provider, registered or dormant.
363
- * @returns detached directory entries in declaration order.
364
- */
365
- listConfigurableProviders() {
366
- return [...this.directory.values()].map(entry => ({ ...entry, settingsPath: [...entry.settingsPath] }));
367
- }
368
- /**
369
- * Offer to interrogate provider endpoints on behalf of the settings
370
- * namespace this plugin owns. The namespace is the key because that is what
371
- * a configuration surface already holds from the configurable-provider
372
- * directory, and because a provider being *added* has no route to name yet.
373
- * Disposed with the fiber.
374
- * @param settingsNs - the namespace whose profiles this discovery serves.
375
- * @param discover - interrogates one endpoint; must honor `request.signal`.
376
- * @returns the disposer that withdraws the offer.
377
- */
378
- registerModelDiscovery(settingsNs, discover) {
379
- const dispose = this.ctx.effect(function* () {
380
- if (settingsNs.length === 0) {
381
- throw new LlmError('model discovery needs a non-empty settings namespace', 'INVALID_DISCOVERY');
470
+ // One of the two identifies what to describe: a route the adapter knows, or
471
+ // an endpoint to ask. Neither leaves nothing to answer about.
472
+ if ((request.provider ?? '').length === 0 && (request.baseURL ?? '').length === 0) {
473
+ throw new LlmError('model discovery needs a provider route or a baseURL', 'INVALID_DISCOVERY');
382
474
  }
383
- if (this.discoveries.has(settingsNs)) {
384
- throw new LlmError(`model discovery for "${settingsNs}" is already registered`, 'DUPLICATE_DISCOVERY');
475
+ const discovered = signal === undefined
476
+ ? await discover(request)
477
+ : await discover(request, signal);
478
+ const seen = new Set();
479
+ const models = [];
480
+ for (const model of discovered) {
481
+ if (typeof model.id !== 'string' || model.id.length === 0 || seen.has(model.id))
482
+ continue;
483
+ seen.add(model.id);
484
+ models.push({
485
+ id: model.id,
486
+ ...model.name === undefined ? {} : { name: model.name },
487
+ ...model.contextWindow === undefined ? {} : { contextWindow: model.contextWindow },
488
+ ...model.maxTokens === undefined ? {} : { maxTokens: model.maxTokens },
489
+ });
385
490
  }
386
- this.discoveries.set(settingsNs, discover);
387
- yield () => {
388
- this.discoveries.delete(settingsNs);
389
- };
390
- }.bind(this), 'llm.registerModelDiscovery()');
391
- return () => void dispose();
392
- }
393
- /**
394
- * Interrogate one provider endpoint for the models it advertises. The
395
- * request describes a draft, not a stored route, so nothing here reads or
396
- * writes settings or credentials — the caller owns both, and the reply is
397
- * candidate metadata a surface may offer for adoption.
398
- * @param settingsNs - namespace whose registered discovery serves this draft.
399
- * @param request - the endpoint, protocol, and one-shot credential to use.
400
- * @returns the advertised models, deduplicated in endpoint order.
401
- */
402
- async discoverModels(settingsNs, request) {
403
- const discover = this.discoveries.get(settingsNs);
404
- if (discover === undefined) {
405
- throw new LlmError(`no model discovery is registered for "${settingsNs}"`, 'NO_DISCOVERY');
406
- }
407
- // One of the two identifies what to describe: a route the adapter knows, or
408
- // an endpoint to ask. Neither leaves nothing to answer about.
409
- if ((request.provider ?? '').length === 0 && (request.baseURL ?? '').length === 0) {
410
- throw new LlmError('model discovery needs a provider route or a baseURL', 'INVALID_DISCOVERY');
411
- }
412
- const discovered = await discover(request);
413
- const seen = new Set();
414
- const models = [];
415
- for (const model of discovered) {
416
- if (typeof model.id !== 'string' || model.id.length === 0 || seen.has(model.id))
417
- continue;
418
- seen.add(model.id);
419
- models.push({
420
- id: model.id,
421
- ...model.name === undefined ? {} : { name: model.name },
422
- ...model.contextWindow === undefined ? {} : { contextWindow: model.contextWindow },
423
- ...model.maxTokens === undefined ? {} : { maxTokens: model.maxTokens },
491
+ return models;
492
+ }
493
+ /**
494
+ * Remote adapter for one draft provider interrogation.
495
+ * @param settingsNs - namespace whose registered discovery serves this draft.
496
+ * @param request - endpoint, protocol, and one-shot credential to use.
497
+ * @param signal - caller cancellation supplied by the Remote carrier.
498
+ * @returns advertised models in endpoint order.
499
+ * @throws RemoteError with `llm/model-discovery-rejected` when discovery refuses or fails.
500
+ */
501
+ async remoteDiscoverModels(settingsNs, request, signal) {
502
+ try {
503
+ return await this.discoverModels(settingsNs, request, signal);
504
+ }
505
+ catch (error) {
506
+ throw new RemoteError('llm/model-discovery-rejected', error instanceof Error ? error.message : String(error), {
507
+ settingsNs,
508
+ ...request.baseURL === undefined ? {} : { baseURL: request.baseURL },
509
+ }, { cause: error });
510
+ }
511
+ }
512
+ /**
513
+ * Resolve the retry policy captured when one provider route was registered.
514
+ * @param provider - registered provider route to inspect.
515
+ * @returns the provider-owned policy, with normal defaults already resolved.
516
+ */
517
+ providerRetryPolicy(provider) {
518
+ return this.registration(provider).retryPolicy;
519
+ }
520
+ /**
521
+ * Resolve provider-side request-image pricing for one exact route, or
522
+ * `undefined` when the provider is unregistered or declares none. Unknown
523
+ * providers degrade to `undefined` rather than throwing because callers
524
+ * price durable history whose route may no longer be mounted.
525
+ * @param provider - provider route named by a request header.
526
+ * @param model - exact model id named by the same header.
527
+ * @returns the owning adapter's image pricing for the route, when declared.
528
+ */
529
+ imageRequestPricing(provider, model) {
530
+ return this.adapters.get(provider)?.adapter.imageRequestPricing(provider, model);
531
+ }
532
+ /** Detach typed adapter-owned modality metadata. */
533
+ detachedModalities(modalities) {
534
+ return modalities === undefined ? undefined : [...modalities];
535
+ }
536
+ /**
537
+ * Discover models advertised by one registered provider. Catalog membership
538
+ * is advisory and never changes routing or request validation.
539
+ * @param provider - registered provider route to inspect.
540
+ * @returns detached model metadata in adapter-preferred order.
541
+ */
542
+ async listModels(provider) {
543
+ const adapter = this.registration(provider).adapter;
544
+ const models = await adapter.listModels(provider);
545
+ const seen = new Set();
546
+ return models.map((model) => {
547
+ if (typeof model.provider !== 'string'
548
+ || model.provider !== provider
549
+ || typeof model.id !== 'string'
550
+ || model.id.length === 0
551
+ || typeof model.name !== 'string'
552
+ || model.name.length === 0
553
+ || (model.description !== undefined && typeof model.description !== 'string')
554
+ || seen.has(model.id)) {
555
+ throw new LlmError(`adapter returned invalid or duplicate model metadata for provider "${provider}"`, 'INVALID_CATALOG');
556
+ }
557
+ seen.add(model.id);
558
+ const inputModalities = this.detachedModalities(model.inputModalities);
559
+ return {
560
+ provider: model.provider,
561
+ id: model.id,
562
+ name: model.name,
563
+ ...model.description === undefined ? {} : { description: model.description },
564
+ ...inputModalities === undefined ? {} : { inputModalities },
565
+ };
424
566
  });
425
567
  }
426
- return models;
427
- }
428
- /**
429
- * Resolve the retry policy captured when one provider route was registered.
430
- * @param provider - registered provider route to inspect.
431
- * @returns the provider-owned policy, with normal defaults already resolved.
432
- */
433
- providerRetryPolicy(provider) {
434
- return this.registration(provider).retryPolicy;
435
- }
436
- /** Detach typed adapter-owned modality metadata. */
437
- detachedModalities(modalities) {
438
- return modalities === undefined ? undefined : [...modalities];
439
- }
440
- /**
441
- * Discover models advertised by one registered provider. Catalog membership
442
- * is advisory and never changes routing or request validation.
443
- * @param provider - registered provider route to inspect.
444
- * @returns detached model metadata in adapter-preferred order.
445
- */
446
- async listModels(provider) {
447
- const adapter = this.registration(provider).adapter;
448
- const models = await adapter.listModels(provider);
449
- const seen = new Set();
450
- return models.map((model) => {
451
- if (typeof model.provider !== 'string'
452
- || model.provider !== provider
453
- || typeof model.id !== 'string'
454
- || model.id.length === 0
455
- || typeof model.name !== 'string'
456
- || model.name.length === 0
457
- || (model.description !== undefined && typeof model.description !== 'string')
458
- || seen.has(model.id)) {
459
- throw new LlmError(`adapter returned invalid or duplicate model metadata for provider "${provider}"`, 'INVALID_CATALOG');
568
+ /**
569
+ * Resolve and validate all metadata from the adapter that owns one exact
570
+ * route. The result is detached from adapter-owned objects; catalog
571
+ * membership remains advisory and does not control request routing.
572
+ * @param provider - registered provider route to inspect.
573
+ * @param model - exact model id passed to the adapter.
574
+ * @param signal - optional cancellation for adapter-owned asynchronous lookup.
575
+ * @returns exact model identity plus available context and reasoning metadata.
576
+ */
577
+ async resolveModelInfo(provider, model, signal) {
578
+ return this.resolveModelInfoFor(this.registration(provider), model, signal);
579
+ }
580
+ async resolveModelInfoFor(registration, model, signal) {
581
+ const resolved = await registration.adapter.resolveModel(registration.provider.id, model, signal);
582
+ return this.normalizeModelInfo(registration, model, resolved);
583
+ }
584
+ /** Validate and detach one adapter-returned exact model result. */
585
+ normalizeModelInfo(registration, model, resolved) {
586
+ const provider = registration.provider.id;
587
+ if (typeof resolved.provider !== 'string'
588
+ || resolved.provider !== provider
589
+ || typeof resolved.id !== 'string'
590
+ || resolved.id !== model
591
+ || typeof resolved.name !== 'string'
592
+ || resolved.name.length === 0
593
+ || (resolved.description !== undefined && typeof resolved.description !== 'string')) {
594
+ throw new LlmError(`adapter returned invalid exact model metadata for provider "${provider}" model "${model}"`, 'INVALID_MODEL_INFO');
460
595
  }
461
- seen.add(model.id);
462
- const inputModalities = this.detachedModalities(model.inputModalities);
463
- return {
464
- provider: model.provider,
465
- id: model.id,
466
- name: model.name,
467
- ...model.description === undefined ? {} : { description: model.description },
596
+ const context = resolved.context;
597
+ if (context !== undefined && (!Number.isInteger(context.contextWindow) || context.contextWindow <= 0)) {
598
+ throw new LlmError(`adapter returned invalid context metadata for provider "${provider}" model "${model}"`, 'INVALID_MODEL_CONTEXT');
599
+ }
600
+ // Capability metadata rides through: an explicit modality omission is
601
+ // negative capability downstream preflights act on (image admission).
602
+ const inputModalities = this.detachedModalities(resolved.inputModalities);
603
+ const defaultMaxTokens = resolved.defaultMaxTokens;
604
+ if (defaultMaxTokens !== undefined
605
+ && (!Number.isSafeInteger(defaultMaxTokens) || defaultMaxTokens <= 0)) {
606
+ throw new LlmError(`adapter returned invalid default maxTokens for provider "${provider}" model "${model}"`, 'INVALID_MODEL_MAX_TOKENS');
607
+ }
608
+ const info = {
609
+ provider,
610
+ id: model,
611
+ name: resolved.name,
612
+ ...resolved.description === undefined ? {} : { description: resolved.description },
468
613
  ...inputModalities === undefined ? {} : { inputModalities },
614
+ ...context === undefined ? {} : { context: { contextWindow: context.contextWindow } },
615
+ ...defaultMaxTokens === undefined ? {} : { defaultMaxTokens },
469
616
  };
470
- });
471
- }
472
- /**
473
- * Resolve and validate all metadata from the adapter that owns one exact
474
- * route. The result is detached from adapter-owned objects; catalog
475
- * membership remains advisory and does not control request routing.
476
- * @param provider - registered provider route to inspect.
477
- * @param model - exact model id passed to the adapter.
478
- * @param signal - optional cancellation for adapter-owned asynchronous lookup.
479
- * @returns exact model identity plus available context and reasoning metadata.
480
- */
481
- async resolveModelInfo(provider, model, signal) {
482
- return this.resolveModelInfoFor(this.registration(provider), model, signal);
483
- }
484
- async resolveModelInfoFor(registration, model, signal) {
485
- const resolved = await registration.adapter.resolveModel(registration.provider.id, model, signal);
486
- return this.normalizeModelInfo(registration, model, resolved);
487
- }
488
- /** Validate and detach one adapter-returned exact model result. */
489
- normalizeModelInfo(registration, model, resolved) {
490
- const provider = registration.provider.id;
491
- if (typeof resolved.provider !== 'string'
492
- || resolved.provider !== provider
493
- || typeof resolved.id !== 'string'
494
- || resolved.id !== model
495
- || typeof resolved.name !== 'string'
496
- || resolved.name.length === 0
497
- || (resolved.description !== undefined && typeof resolved.description !== 'string')) {
498
- throw new LlmError(`adapter returned invalid exact model metadata for provider "${provider}" model "${model}"`, 'INVALID_MODEL_INFO');
499
- }
500
- const context = resolved.context;
501
- if (context !== undefined && (!Number.isInteger(context.contextWindow) || context.contextWindow <= 0)) {
502
- throw new LlmError(`adapter returned invalid context metadata for provider "${provider}" model "${model}"`, 'INVALID_MODEL_CONTEXT');
503
- }
504
- // Capability metadata rides through: an explicit modality omission is
505
- // negative capability downstream preflights act on (image admission).
506
- const inputModalities = this.detachedModalities(resolved.inputModalities);
507
- const defaultMaxTokens = resolved.defaultMaxTokens;
508
- if (defaultMaxTokens !== undefined
509
- && (!Number.isSafeInteger(defaultMaxTokens) || defaultMaxTokens <= 0)) {
510
- throw new LlmError(`adapter returned invalid default maxTokens for provider "${provider}" model "${model}"`, 'INVALID_MODEL_MAX_TOKENS');
511
- }
512
- const info = {
513
- provider,
514
- id: model,
515
- name: resolved.name,
516
- ...resolved.description === undefined ? {} : { description: resolved.description },
517
- ...inputModalities === undefined ? {} : { inputModalities },
518
- ...context === undefined ? {} : { context: { contextWindow: context.contextWindow } },
519
- ...defaultMaxTokens === undefined ? {} : { defaultMaxTokens },
520
- };
521
- const reasoning = resolved.reasoning;
522
- if (reasoning === undefined)
523
- return info;
524
- if (reasoning.efforts.length === 0) {
525
- throw new LlmError(`adapter returned invalid reasoning metadata for provider "${provider}" model "${model}"`, 'INVALID_MODEL_REASONING');
526
- }
527
- const seen = new Set();
528
- const efforts = reasoning.efforts.map((effort) => {
529
- if (typeof effort.id !== 'string'
530
- || effort.id.length === 0
531
- || typeof effort.name !== 'string'
532
- || effort.name.length === 0
533
- || (effort.description !== undefined && typeof effort.description !== 'string')
534
- || seen.has(effort.id)) {
535
- throw new LlmError(`adapter returned invalid or duplicate reasoning effort metadata for provider "${provider}" model "${model}"`, 'INVALID_MODEL_REASONING');
617
+ const reasoning = resolved.reasoning;
618
+ if (reasoning === undefined)
619
+ return info;
620
+ if (reasoning.efforts.length === 0) {
621
+ throw new LlmError(`adapter returned invalid reasoning metadata for provider "${provider}" model "${model}"`, 'INVALID_MODEL_REASONING');
622
+ }
623
+ const seen = new Set();
624
+ const efforts = reasoning.efforts.map((effort) => {
625
+ if (typeof effort.id !== 'string'
626
+ || effort.id.length === 0
627
+ || typeof effort.name !== 'string'
628
+ || effort.name.length === 0
629
+ || (effort.description !== undefined && typeof effort.description !== 'string')
630
+ || seen.has(effort.id)) {
631
+ throw new LlmError(`adapter returned invalid or duplicate reasoning effort metadata for provider "${provider}" model "${model}"`, 'INVALID_MODEL_REASONING');
632
+ }
633
+ seen.add(effort.id);
634
+ return {
635
+ id: effort.id,
636
+ name: effort.name,
637
+ ...effort.description === undefined ? {} : { description: effort.description },
638
+ };
639
+ });
640
+ if (reasoning.defaultEffort !== undefined && !seen.has(reasoning.defaultEffort)) {
641
+ throw new LlmError(`adapter returned an unknown default reasoning effort for provider "${provider}" model "${model}"`, 'INVALID_MODEL_REASONING');
536
642
  }
537
- seen.add(effort.id);
538
643
  return {
539
- id: effort.id,
540
- name: effort.name,
541
- ...effort.description === undefined ? {} : { description: effort.description },
644
+ ...info,
645
+ reasoning: {
646
+ efforts,
647
+ ...reasoning.defaultEffort === undefined ? {} : { defaultEffort: reasoning.defaultEffort },
648
+ },
542
649
  };
543
- });
544
- if (reasoning.defaultEffort !== undefined && !seen.has(reasoning.defaultEffort)) {
545
- throw new LlmError(`adapter returned an unknown default reasoning effort for provider "${provider}" model "${model}"`, 'INVALID_MODEL_REASONING');
546
650
  }
547
- return {
548
- ...info,
549
- reasoning: {
550
- efforts,
551
- ...reasoning.defaultEffort === undefined ? {} : { defaultEffort: reasoning.defaultEffort },
552
- },
553
- };
554
- }
555
- /**
556
- * Validate a conversation call config against its exact model capability and
557
- * materialize adapter-configured defaults. Unsupported explicit efforts
558
- * reject before provider I/O; no clamping or aliasing is performed. This
559
- * standalone query does not bind a later dispatch; use {@link prepareCall}
560
- * when logging and streaming must share one adapter registration.
561
- * @param config - provider/model route and optional request controls.
562
- * @param signal - optional cancellation for adapter-owned capability lookup.
563
- * @returns a detached config only when a default must be materialized.
564
- */
565
- async resolveCallConfig(config, signal) {
566
- return (await this.resolveCallFor(this.registration(config.provider), config, signal)).config;
567
- }
568
- async resolveCallFor(registration, config, signal) {
569
- const info = await this.resolveModelInfoFor(registration, config.model, signal);
570
- return this.resolveCallWithInfo(config, info);
571
- }
572
- /** Validate request controls against one already-bound exact model result. */
573
- resolveCallWithInfo(config, info) {
574
- const defaulted = config.maxTokens === undefined && info.defaultMaxTokens !== undefined
575
- ? { ...config, maxTokens: info.defaultMaxTokens }
576
- : config;
577
- const reasoning = info.reasoning;
578
- const requested = defaulted.reasoningEffort;
579
- let resolvedConfig = defaulted;
580
- if (reasoning === undefined) {
581
- if (requested !== undefined) {
582
- throw new LlmError(`provider "${config.provider}" model "${config.model}" does not support reasoning effort "${requested}"`, 'UNSUPPORTED_REASONING_EFFORT');
583
- }
651
+ /**
652
+ * Validate a conversation call config against its exact model capability and
653
+ * materialize adapter-configured defaults. Unsupported explicit efforts
654
+ * reject before provider I/O; no clamping or aliasing is performed. This
655
+ * standalone query does not bind a later dispatch; use {@link prepareCall}
656
+ * when logging and streaming must share one adapter registration.
657
+ * @param config - provider/model route and optional request controls.
658
+ * @param signal - optional cancellation for adapter-owned capability lookup.
659
+ * @returns a detached config only when a default must be materialized.
660
+ */
661
+ async resolveCallConfig(config, signal) {
662
+ return (await this.resolveCallFor(this.registration(config.provider), config, signal)).config;
584
663
  }
585
- else {
586
- const effective = requested ?? reasoning.defaultEffort;
587
- if (effective !== undefined) {
588
- if (!reasoning.efforts.some(effort => effort.id === effective)) {
589
- throw new LlmError(`provider "${config.provider}" model "${config.model}" does not support reasoning effort "${effective}"`, 'UNSUPPORTED_REASONING_EFFORT');
664
+ async resolveCallFor(registration, config, signal) {
665
+ const info = await this.resolveModelInfoFor(registration, config.model, signal);
666
+ return this.resolveCallWithInfo(config, info);
667
+ }
668
+ /** Validate request controls against one already-bound exact model result. */
669
+ resolveCallWithInfo(config, info) {
670
+ const defaulted = config.maxTokens === undefined && info.defaultMaxTokens !== undefined
671
+ ? { ...config, maxTokens: info.defaultMaxTokens }
672
+ : config;
673
+ const reasoning = info.reasoning;
674
+ const requested = defaulted.reasoningEffort;
675
+ let resolvedConfig = defaulted;
676
+ if (reasoning === undefined) {
677
+ if (requested !== undefined) {
678
+ throw new LlmError(`provider "${config.provider}" model "${config.model}" does not support reasoning effort "${requested}"`, 'UNSUPPORTED_REASONING_EFFORT');
679
+ }
680
+ }
681
+ else {
682
+ const effective = requested ?? reasoning.defaultEffort;
683
+ if (effective !== undefined) {
684
+ if (!reasoning.efforts.some(effort => effort.id === effective)) {
685
+ throw new LlmError(`provider "${config.provider}" model "${config.model}" does not support reasoning effort "${effective}"`, 'UNSUPPORTED_REASONING_EFFORT');
686
+ }
687
+ if (requested !== effective)
688
+ resolvedConfig = { ...defaulted, reasoningEffort: effective };
590
689
  }
591
- if (requested !== effective)
592
- resolvedConfig = { ...defaulted, reasoningEffort: effective };
593
690
  }
691
+ return {
692
+ config: resolvedConfig,
693
+ ...info.context === undefined ? {} : { context: info.context },
694
+ modelInfo: info,
695
+ };
594
696
  }
595
- return {
596
- config: resolvedConfig,
597
- ...info.context === undefined ? {} : { context: info.context },
598
- modelInfo: info,
599
- };
600
- }
601
- /**
602
- * Resolve one call under its current adapter registration. The returned
603
- * one-shot handle keeps that registration across header logging and dispatch,
604
- * so HMR cannot combine one adapter's capability result with another adapter.
605
- * @param config - provider/model route and optional request controls.
606
- * @param signal - optional cancellation for adapter-owned capability lookup.
607
- * @returns a prepared config and its registration-bound stream entry point.
608
- */
609
- async prepareCall(config, signal) {
610
- const registration = this.registration(config.provider);
611
- const adapterCall = await registration.adapter.prepareCall(config.provider, config.model, signal);
612
- const modelInfo = this.normalizeModelInfo(registration, config.model, adapterCall.model);
613
- const resolved = this.resolveCallWithInfo(config, modelInfo);
614
- const resolvedConfig = deepFreeze(structuredClone(resolved.config));
615
- const context = resolved.context === undefined
616
- ? undefined
617
- : deepFreeze(structuredClone(resolved.context));
618
- const adapterDefaults = deepFreeze({
619
- ...config.reasoningEffort === undefined && resolvedConfig.reasoningEffort !== undefined
620
- ? { reasoningEffort: true }
621
- : {},
622
- ...config.maxTokens === undefined && resolvedConfig.maxTokens !== undefined
623
- ? { maxTokens: true }
624
- : {},
625
- });
626
- let dispatched = false;
627
- return Object.freeze({
628
- config: resolvedConfig,
629
- retryPolicy: registration.retryPolicy,
630
- adapterDefaults,
631
- ...context === undefined ? {} : { context },
632
- ...modelInfo.inputModalities === undefined
633
- ? {}
634
- : { inputModalities: Object.freeze([...modelInfo.inputModalities]) },
635
- stream: (options) => {
636
- if (dispatched) {
637
- throw new LlmError('a prepared LLM call can only be dispatched once', 'INVALID_PREPARED_CALL');
697
+ /**
698
+ * Resolve one call under its current adapter registration. The returned
699
+ * one-shot handle keeps that registration across header logging and dispatch,
700
+ * so HMR cannot combine one adapter's capability result with another adapter.
701
+ * @param config - provider/model route and optional request controls.
702
+ * @param signal - optional cancellation for adapter-owned capability lookup.
703
+ * @returns a prepared config and its registration-bound stream entry point.
704
+ */
705
+ async prepareCall(config, signal) {
706
+ const registration = this.registration(config.provider);
707
+ const adapterCall = await registration.adapter.prepareCall(config.provider, config.model, signal);
708
+ const modelInfo = this.normalizeModelInfo(registration, config.model, adapterCall.model);
709
+ const resolved = this.resolveCallWithInfo(config, modelInfo);
710
+ const resolvedConfig = deepFreeze(structuredClone(resolved.config));
711
+ const context = resolved.context === undefined
712
+ ? undefined
713
+ : deepFreeze(structuredClone(resolved.context));
714
+ const adapterDefaults = deepFreeze({
715
+ ...config.reasoningEffort === undefined && resolvedConfig.reasoningEffort !== undefined
716
+ ? { reasoningEffort: true }
717
+ : {},
718
+ ...config.maxTokens === undefined && resolvedConfig.maxTokens !== undefined
719
+ ? { maxTokens: true }
720
+ : {},
721
+ });
722
+ let dispatched = false;
723
+ return Object.freeze({
724
+ config: resolvedConfig,
725
+ retryPolicy: registration.retryPolicy,
726
+ adapterDefaults,
727
+ ...context === undefined ? {} : { context },
728
+ ...modelInfo.inputModalities === undefined
729
+ ? {}
730
+ : { inputModalities: Object.freeze([...modelInfo.inputModalities]) },
731
+ stream: (options) => {
732
+ if (dispatched) {
733
+ throw new LlmError('a prepared LLM call can only be dispatched once', 'INVALID_PREPARED_CALL');
734
+ }
735
+ if (!callConfigEquals(options, resolvedConfig)) {
736
+ throw new LlmError('prepared LLM call config changed before adapter dispatch', 'INVALID_PREPARED_CALL');
737
+ }
738
+ dispatched = true;
739
+ return this.streamWithRegistration(options, {
740
+ registration,
741
+ config: resolvedConfig,
742
+ modelInfo,
743
+ dispatch: options => adapterCall.stream(options),
744
+ });
745
+ },
746
+ });
747
+ }
748
+ registration(provider) {
749
+ const registration = this.adapters.get(provider);
750
+ if (!registration)
751
+ throw new LlmError(`no adapter registered for provider "${provider}"`, 'NO_ADAPTER');
752
+ return registration;
753
+ }
754
+ /** Remove replay state whose historical route is owned by another adapter. */
755
+ forAdapter(options, adapter) {
756
+ const messages = options.messages.map((message) => {
757
+ const source = message.source;
758
+ if (message.role !== 'assistant' || source.kind !== 'model' || source.replayState === undefined)
759
+ return message;
760
+ if (this.adapters.get(source.provider)?.adapter === adapter)
761
+ return message;
762
+ return freezeMessage({
763
+ ...message,
764
+ source: { kind: 'model', provider: source.provider, model: source.model },
765
+ });
766
+ });
767
+ if (messages.every((message, index) => message === options.messages[index]))
768
+ return options;
769
+ const filtered = { ...options, messages };
770
+ return Object.isFrozen(options) ? deepFreeze(filtered) : filtered;
771
+ }
772
+ /**
773
+ * Final adapter boundary. Adapter selection, dispatch, iterator construction,
774
+ * and iteration failures become one terminal failure chunk. Middleware and
775
+ * downstream consumer failures remain thrown plugin or consumer errors.
776
+ */
777
+ async *adapterStream(options, prepared) {
778
+ let iterator;
779
+ try {
780
+ const registration = prepared?.registration ?? this.registration(options.provider);
781
+ const adapter = registration.adapter;
782
+ let modelInfo;
783
+ let resolvedConfig;
784
+ let dispatch;
785
+ if (prepared === undefined) {
786
+ const adapterCall = await adapter.prepareCall(options.provider, options.model, options.signal);
787
+ modelInfo = this.normalizeModelInfo(registration, options.model, adapterCall.model);
788
+ resolvedConfig = this.resolveCallWithInfo(options, modelInfo).config;
789
+ dispatch = options => adapterCall.stream(options);
790
+ }
791
+ else {
792
+ modelInfo = prepared.modelInfo;
793
+ resolvedConfig = prepared.config;
794
+ dispatch = prepared.dispatch;
638
795
  }
639
- if (!callConfigEquals(options, resolvedConfig)) {
796
+ if (prepared !== undefined && !callConfigEquals(options, resolvedConfig)) {
640
797
  throw new LlmError('prepared LLM call config changed before adapter dispatch', 'INVALID_PREPARED_CALL');
641
798
  }
642
- dispatched = true;
643
- return this.streamWithRegistration(options, {
644
- registration,
645
- config: resolvedConfig,
646
- modelInfo,
647
- dispatch: options => adapterCall.stream(options),
648
- });
649
- },
650
- });
651
- }
652
- registration(provider) {
653
- const registration = this.adapters.get(provider);
654
- if (!registration)
655
- throw new LlmError(`no adapter registered for provider "${provider}"`, 'NO_ADAPTER');
656
- return registration;
657
- }
658
- /** Remove replay state whose historical route is owned by another adapter. */
659
- forAdapter(options, adapter) {
660
- const messages = options.messages.map((message) => {
661
- const source = message.source;
662
- if (message.role !== 'assistant' || source.kind !== 'model' || source.replayState === undefined)
663
- return message;
664
- if (this.adapters.get(source.provider)?.adapter === adapter)
665
- return message;
666
- return freezeMessage({
667
- ...message,
668
- source: { kind: 'model', provider: source.provider, model: source.model },
669
- });
670
- });
671
- if (messages.every((message, index) => message === options.messages[index]))
672
- return options;
673
- const filtered = { ...options, messages };
674
- return Object.isFrozen(options) ? deepFreeze(filtered) : filtered;
675
- }
676
- /**
677
- * Final adapter boundary. Adapter selection, dispatch, iterator construction,
678
- * and iteration failures become one terminal failure chunk. Middleware and
679
- * downstream consumer failures remain thrown plugin or consumer errors.
680
- */
681
- async *adapterStream(options, prepared) {
682
- let iterator;
683
- try {
684
- const registration = prepared?.registration ?? this.registration(options.provider);
685
- const adapter = registration.adapter;
686
- let modelInfo;
687
- let resolvedConfig;
688
- let dispatch;
689
- if (prepared === undefined) {
690
- const adapterCall = await adapter.prepareCall(options.provider, options.model, options.signal);
691
- modelInfo = this.normalizeModelInfo(registration, options.model, adapterCall.model);
692
- resolvedConfig = this.resolveCallWithInfo(options, modelInfo).config;
693
- dispatch = options => adapterCall.stream(options);
694
- }
695
- else {
696
- modelInfo = prepared.modelInfo;
697
- resolvedConfig = prepared.config;
698
- dispatch = prepared.dispatch;
799
+ const resolvedOptions = callConfigEquals(options, resolvedConfig)
800
+ ? options
801
+ : Object.isFrozen(options)
802
+ ? deepFreeze({ ...options, ...resolvedConfig })
803
+ : { ...options, ...resolvedConfig };
804
+ const projectedOptions = modelInfo.inputModalities !== undefined
805
+ && !modelInfo.inputModalities.includes('image')
806
+ && resolvedOptions.messages.some(message => contentHasImage(message.content))
807
+ ? Object.isFrozen(resolvedOptions)
808
+ ? deepFreeze({ ...resolvedOptions, messages: projectImagesForTextModel(resolvedOptions.messages) })
809
+ : { ...resolvedOptions, messages: projectImagesForTextModel(resolvedOptions.messages) }
810
+ : resolvedOptions;
811
+ const stream = dispatch(this.forAdapter(projectedOptions, adapter));
812
+ iterator = stream[Symbol.asyncIterator]();
699
813
  }
700
- if (prepared !== undefined && !callConfigEquals(options, resolvedConfig)) {
701
- throw new LlmError('prepared LLM call config changed before adapter dispatch', 'INVALID_PREPARED_CALL');
814
+ catch (error) {
815
+ yield adapterFailureChunk(error, options.signal);
816
+ return;
702
817
  }
703
- const resolvedOptions = callConfigEquals(options, resolvedConfig)
704
- ? options
705
- : Object.isFrozen(options)
706
- ? deepFreeze({ ...options, ...resolvedConfig })
707
- : { ...options, ...resolvedConfig };
708
- const projectedOptions = modelInfo.inputModalities !== undefined
709
- && !modelInfo.inputModalities.includes('image')
710
- && resolvedOptions.messages.some(message => contentHasImage(message.content))
711
- ? Object.isFrozen(resolvedOptions)
712
- ? deepFreeze({ ...resolvedOptions, messages: projectImagesForTextModel(resolvedOptions.messages) })
713
- : { ...resolvedOptions, messages: projectImagesForTextModel(resolvedOptions.messages) }
714
- : resolvedOptions;
715
- const stream = dispatch(this.forAdapter(projectedOptions, adapter));
716
- iterator = stream[Symbol.asyncIterator]();
717
- }
718
- catch (error) {
719
- yield adapterFailureChunk(error, options.signal);
720
- return;
721
- }
722
- let completed = false;
723
- try {
724
- while (true) {
725
- let item;
726
- try {
727
- const next = await iterator.next();
728
- item = next.done
729
- ? { done: true }
730
- : { done: false, value: next.value };
731
- }
732
- catch (error) {
733
- completed = true;
734
- yield adapterFailureChunk(error, options.signal);
735
- return;
818
+ let completed = false;
819
+ try {
820
+ while (true) {
821
+ let item;
822
+ try {
823
+ const next = await iterator.next();
824
+ item = next.done
825
+ ? { done: true }
826
+ : { done: false, value: next.value };
827
+ }
828
+ catch (error) {
829
+ completed = true;
830
+ yield adapterFailureChunk(error, options.signal);
831
+ return;
832
+ }
833
+ if (item.done) {
834
+ completed = true;
835
+ return;
836
+ }
837
+ // End the adapter-owned try before yielding: consumer/middleware
838
+ // failures resumed into this generator must remain thrown.
839
+ yield item.value;
736
840
  }
737
- if (item.done) {
738
- completed = true;
739
- return;
841
+ }
842
+ finally {
843
+ if (!completed) {
844
+ const close = iterator.return?.bind(iterator);
845
+ if (close)
846
+ await close();
740
847
  }
741
- // End the adapter-owned try before yielding: consumer/middleware
742
- // failures resumed into this generator must remain thrown.
743
- yield item.value;
744
848
  }
745
849
  }
746
- finally {
747
- if (!completed) {
748
- const close = iterator.return?.bind(iterator);
749
- if (close)
750
- await close();
751
- }
850
+ /**
851
+ * Stream one model call as raw chunks (token-level deltas). Replay state is
852
+ * retained only when the same adapter instance owns its historical provider
853
+ * and the target provider. Final adapter selection remains fixed through
854
+ * asynchronous exact-model resolution and dispatch. Adapter selection,
855
+ * dispatch, and iteration failures become terminal `error` or `aborted`
856
+ * finish chunks; middleware, nested-call, cleanup, and consumer failures
857
+ * remain thrown.
858
+ * @param options - the full request; `options.provider` selects the adapter.
859
+ * @returns the chunk stream, possibly wrapped by `llm/stream` listeners.
860
+ */
861
+ stream(options) {
862
+ return this.streamWithRegistration(options);
752
863
  }
753
- }
754
- /**
755
- * Stream one model call as raw chunks (token-level deltas). Replay state is
756
- * retained only when the same adapter instance owns its historical provider
757
- * and the target provider. Final adapter selection remains fixed through
758
- * asynchronous exact-model resolution and dispatch. Adapter selection,
759
- * dispatch, and iteration failures become terminal `error` or `aborted`
760
- * finish chunks; middleware, nested-call, cleanup, and consumer failures
761
- * remain thrown.
762
- * @param options - the full request; `options.provider` selects the adapter.
763
- * @returns the chunk stream, possibly wrapped by `llm/stream` listeners.
764
- */
765
- stream(options) {
766
- return this.streamWithRegistration(options);
767
- }
768
- streamWithRegistration(options, prepared) {
769
- return this.ctx.waterfall(this, 'llm/stream', options, () => this.adapterStream(options, prepared));
770
- }
771
- }
864
+ streamWithRegistration(options, prepared) {
865
+ return this.ctx.waterfall(this, 'llm/stream', options, () => this.adapterStream(options, prepared));
866
+ }
867
+ };
868
+ })();
869
+ export { LlmRuntime };
772
870
  /** Convert one adapter throw into the stream protocol's terminal outcome. */
773
871
  function adapterFailureChunk(error, signal) {
774
872
  const failure = normalizeLlmFailure(error);