@yadimon/prio-llm-router 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,1058 @@
1
+ 'use strict';
2
+
3
+ var anthropic = require('@ai-sdk/anthropic');
4
+ var cohere = require('@ai-sdk/cohere');
5
+ var deepseek = require('@ai-sdk/deepseek');
6
+ var google = require('@ai-sdk/google');
7
+ var groq = require('@ai-sdk/groq');
8
+ var mistral = require('@ai-sdk/mistral');
9
+ var openai = require('@ai-sdk/openai');
10
+ var openaiCompatible = require('@ai-sdk/openai-compatible');
11
+ var perplexity = require('@ai-sdk/perplexity');
12
+ var togetherai = require('@ai-sdk/togetherai');
13
+ var xai = require('@ai-sdk/xai');
14
+ var ai = require('ai');
15
+ var aiSdkProvider = require('@openrouter/ai-sdk-provider');
16
+
17
+ // src/errors.ts
18
+ var PrioLlmRouterError = class extends Error {
19
+ constructor(message, options) {
20
+ super(message, options);
21
+ this.name = new.target.name;
22
+ }
23
+ };
24
+ var RouterConfigurationError = class extends PrioLlmRouterError {
25
+ };
26
+ var AllModelsFailedError = class extends PrioLlmRouterError {
27
+ attempts;
28
+ constructor(attempts, cause) {
29
+ super(buildFailureMessage(attempts), { cause });
30
+ this.attempts = attempts;
31
+ }
32
+ };
33
+ function serializeError(error) {
34
+ if (error instanceof Error) {
35
+ const maybeError = error;
36
+ const serialized = {
37
+ name: error.name,
38
+ message: error.message
39
+ };
40
+ if (maybeError.code !== void 0) {
41
+ serialized.code = maybeError.code;
42
+ }
43
+ const statusCode = maybeError.statusCode ?? maybeError.status;
44
+ if (statusCode !== void 0) {
45
+ serialized.statusCode = statusCode;
46
+ }
47
+ return serialized;
48
+ }
49
+ if (typeof error === "string") {
50
+ return {
51
+ name: "Error",
52
+ message: error
53
+ };
54
+ }
55
+ return {
56
+ name: "UnknownError",
57
+ message: "Unknown router error"
58
+ };
59
+ }
60
+ function isAbortError(error) {
61
+ if (!(error instanceof Error)) {
62
+ return false;
63
+ }
64
+ return error.name === "AbortError" || error.name === "TimeoutError";
65
+ }
66
+ function buildFailureMessage(attempts) {
67
+ const summary = attempts.map((attempt) => {
68
+ const errorMessage = attempt.error?.message ?? "Unknown error";
69
+ return `${attempt.targetName} (${attempt.providerName}/${attempt.model}): ${errorMessage}`;
70
+ }).join("; ");
71
+ return summary ? `All configured model attempts failed. ${summary}` : "All configured model attempts failed.";
72
+ }
73
+ function createDefaultTextGenerationExecutor(options) {
74
+ return new AiSdkTextGenerationExecutor(options?.defaultProviderMaxRetries ?? 0);
75
+ }
76
+ var AiSdkTextGenerationExecutor = class {
77
+ constructor(defaultProviderMaxRetries) {
78
+ this.defaultProviderMaxRetries = defaultProviderMaxRetries;
79
+ }
80
+ providerCache = /* @__PURE__ */ new Map();
81
+ async execute({
82
+ provider,
83
+ model,
84
+ request
85
+ }) {
86
+ const languageModel = this.getLanguageModel(provider, model.model);
87
+ const call = buildBaseTextCallOptions({
88
+ languageModel,
89
+ request,
90
+ defaultProviderMaxRetries: this.defaultProviderMaxRetries
91
+ });
92
+ const result = await ai.generateText(call);
93
+ const output = {
94
+ text: result.text,
95
+ finishReason: result.finishReason ?? null,
96
+ raw: result
97
+ };
98
+ const usage = normalizeUsage(result.usage);
99
+ if (usage) {
100
+ output.usage = usage;
101
+ }
102
+ const warnings = normalizeWarnings(result.warnings);
103
+ if (warnings) {
104
+ output.warnings = warnings;
105
+ }
106
+ return output;
107
+ }
108
+ async stream({
109
+ provider,
110
+ model,
111
+ request
112
+ }) {
113
+ await Promise.resolve();
114
+ const languageModel = this.getLanguageModel(provider, model.model);
115
+ const call = buildBaseTextCallOptions({
116
+ languageModel,
117
+ request,
118
+ defaultProviderMaxRetries: this.defaultProviderMaxRetries
119
+ });
120
+ const result = ai.streamText(call);
121
+ return {
122
+ textStream: result.textStream,
123
+ consumeStream: async () => {
124
+ await result.consumeStream();
125
+ },
126
+ finishReason: Promise.resolve(result.finishReason).then(
127
+ (value) => value ?? null
128
+ ),
129
+ usage: Promise.resolve(result.totalUsage).then(
130
+ (value) => normalizeUsage(value)
131
+ ),
132
+ warnings: Promise.resolve(result.warnings).then(
133
+ (value) => normalizeWarnings(value)
134
+ ),
135
+ raw: result
136
+ };
137
+ }
138
+ getLanguageModel(provider, modelId) {
139
+ const handle = this.providerCache.get(provider.name) ?? createProviderHandle(provider);
140
+ if (!this.providerCache.has(provider.name)) {
141
+ this.providerCache.set(provider.name, handle);
142
+ }
143
+ return resolveLanguageModel(handle, modelId, provider.name);
144
+ }
145
+ };
146
+ function buildBaseTextCallOptions({
147
+ languageModel,
148
+ request,
149
+ defaultProviderMaxRetries
150
+ }) {
151
+ const call = {
152
+ model: languageModel,
153
+ system: request.system,
154
+ temperature: request.temperature,
155
+ topP: request.topP,
156
+ maxRetries: request.providerMaxRetries ?? defaultProviderMaxRetries,
157
+ abortSignal: request.abortSignal
158
+ };
159
+ if (request.maxOutputTokens !== void 0) {
160
+ call.maxOutputTokens = request.maxOutputTokens;
161
+ }
162
+ if (request.stopSequences !== void 0) {
163
+ call.stopSequences = request.stopSequences;
164
+ }
165
+ if ("prompt" in request) {
166
+ call.prompt = request.prompt;
167
+ } else {
168
+ call.messages = request.messages;
169
+ }
170
+ return call;
171
+ }
172
+ function createProviderHandle(provider) {
173
+ const apiKey = provider.auth.apiKey.trim();
174
+ if (!apiKey) {
175
+ throw new RouterConfigurationError(
176
+ `Provider "${provider.name}" is missing an API key.`
177
+ );
178
+ }
179
+ switch (provider.type) {
180
+ case "anthropic": {
181
+ const options = { apiKey };
182
+ if (provider.baseURL) {
183
+ options.baseURL = provider.baseURL;
184
+ }
185
+ if (provider.headers) {
186
+ options.headers = provider.headers;
187
+ }
188
+ return anthropic.createAnthropic(options);
189
+ }
190
+ case "cohere": {
191
+ const options = { apiKey };
192
+ if (provider.baseURL) {
193
+ options.baseURL = provider.baseURL;
194
+ }
195
+ if (provider.headers) {
196
+ options.headers = provider.headers;
197
+ }
198
+ return cohere.createCohere(options);
199
+ }
200
+ case "deepseek": {
201
+ const options = { apiKey };
202
+ if (provider.baseURL) {
203
+ options.baseURL = provider.baseURL;
204
+ }
205
+ if (provider.headers) {
206
+ options.headers = provider.headers;
207
+ }
208
+ return deepseek.createDeepSeek(options);
209
+ }
210
+ case "google": {
211
+ const options = {
212
+ apiKey
213
+ };
214
+ if (provider.baseURL) {
215
+ options.baseURL = provider.baseURL;
216
+ }
217
+ if (provider.headers) {
218
+ options.headers = provider.headers;
219
+ }
220
+ return google.createGoogleGenerativeAI(options);
221
+ }
222
+ case "groq": {
223
+ const options = { apiKey };
224
+ if (provider.baseURL) {
225
+ options.baseURL = provider.baseURL;
226
+ }
227
+ if (provider.headers) {
228
+ options.headers = provider.headers;
229
+ }
230
+ return groq.createGroq(options);
231
+ }
232
+ case "mistral": {
233
+ const options = { apiKey };
234
+ if (provider.baseURL) {
235
+ options.baseURL = provider.baseURL;
236
+ }
237
+ if (provider.headers) {
238
+ options.headers = provider.headers;
239
+ }
240
+ return mistral.createMistral(options);
241
+ }
242
+ case "openai": {
243
+ const options = { apiKey };
244
+ if (provider.baseURL) {
245
+ options.baseURL = provider.baseURL;
246
+ }
247
+ if (provider.headers) {
248
+ options.headers = provider.headers;
249
+ }
250
+ return openai.createOpenAI(options);
251
+ }
252
+ case "openai-compatible": {
253
+ const options = {
254
+ name: provider.providerLabel ?? provider.name,
255
+ apiKey,
256
+ baseURL: provider.baseURL
257
+ };
258
+ if (provider.headers) {
259
+ options.headers = provider.headers;
260
+ }
261
+ if (provider.queryParams) {
262
+ options.queryParams = provider.queryParams;
263
+ }
264
+ return openaiCompatible.createOpenAICompatible(options);
265
+ }
266
+ case "openrouter": {
267
+ const options = {
268
+ apiKey
269
+ };
270
+ const headers = buildOpenRouterHeaders(provider);
271
+ if (headers) {
272
+ options.headers = headers;
273
+ }
274
+ if (provider.baseURL) {
275
+ options.baseURL = provider.baseURL;
276
+ }
277
+ return aiSdkProvider.createOpenRouter(options);
278
+ }
279
+ case "perplexity": {
280
+ const options = { apiKey };
281
+ if (provider.baseURL) {
282
+ options.baseURL = provider.baseURL;
283
+ }
284
+ if (provider.headers) {
285
+ options.headers = provider.headers;
286
+ }
287
+ return perplexity.createPerplexity(options);
288
+ }
289
+ case "togetherai": {
290
+ const options = { apiKey };
291
+ if (provider.baseURL) {
292
+ options.baseURL = provider.baseURL;
293
+ }
294
+ if (provider.headers) {
295
+ options.headers = provider.headers;
296
+ }
297
+ return togetherai.createTogetherAI(options);
298
+ }
299
+ case "xai": {
300
+ const options = { apiKey };
301
+ if (provider.baseURL) {
302
+ options.baseURL = provider.baseURL;
303
+ }
304
+ if (provider.headers) {
305
+ options.headers = provider.headers;
306
+ }
307
+ return xai.createXai(options);
308
+ }
309
+ default: {
310
+ const exhaustiveCheck = provider;
311
+ throw new RouterConfigurationError(
312
+ `Unsupported provider type: ${JSON.stringify(exhaustiveCheck)}`
313
+ );
314
+ }
315
+ }
316
+ }
317
+ function buildOpenRouterHeaders(provider) {
318
+ const headers = { ...provider.headers };
319
+ if (provider.appUrl) {
320
+ headers["HTTP-Referer"] = provider.appUrl;
321
+ }
322
+ if (provider.appName) {
323
+ headers["X-Title"] = provider.appName;
324
+ }
325
+ return Object.keys(headers).length > 0 ? headers : void 0;
326
+ }
327
+ function resolveLanguageModel(providerHandle, modelId, providerName) {
328
+ if (typeof providerHandle === "function") {
329
+ return providerHandle(modelId);
330
+ }
331
+ const dynamicHandle = providerHandle;
332
+ const candidates = ["languageModel", "chatModel", "chat"];
333
+ for (const candidate of candidates) {
334
+ const factory = dynamicHandle[candidate];
335
+ if (typeof factory === "function") {
336
+ return factory(modelId);
337
+ }
338
+ }
339
+ throw new RouterConfigurationError(
340
+ `Provider "${providerName}" does not expose a supported language model factory.`
341
+ );
342
+ }
343
+ function normalizeUsage(usage) {
344
+ if (!usage || typeof usage !== "object") {
345
+ return void 0;
346
+ }
347
+ const numericUsage = usage;
348
+ const normalized = {};
349
+ const keys = [
350
+ "inputTokens",
351
+ "outputTokens",
352
+ "totalTokens",
353
+ "reasoningTokens",
354
+ "cachedInputTokens"
355
+ ];
356
+ for (const key of keys) {
357
+ const value = numericUsage[key];
358
+ if (typeof value === "number") {
359
+ normalized[key] = value;
360
+ }
361
+ }
362
+ return Object.keys(normalized).length > 0 ? normalized : void 0;
363
+ }
364
+ function normalizeWarnings(warnings) {
365
+ return Array.isArray(warnings) ? warnings : void 0;
366
+ }
367
+
368
+ // src/source-builders.ts
369
+ function createLlmConnection(provider) {
370
+ assertConnectionProviderName(provider);
371
+ return {
372
+ provider
373
+ };
374
+ }
375
+ function createLlmSource(connection, config) {
376
+ assertSourceConfig(config);
377
+ const normalizedConfig = config.access === "free" ? config : {
378
+ ...config,
379
+ access: "standard"
380
+ };
381
+ return {
382
+ connection,
383
+ config: normalizedConfig
384
+ };
385
+ }
386
+ function assertConnectionProviderName(provider) {
387
+ if (!provider.name.trim()) {
388
+ throw new RouterConfigurationError(
389
+ "Connection provider names must be non-empty."
390
+ );
391
+ }
392
+ }
393
+ function assertSourceConfig(config) {
394
+ if (!config.name.trim()) {
395
+ throw new RouterConfigurationError("Source names must be non-empty.");
396
+ }
397
+ if (!config.model.trim()) {
398
+ throw new RouterConfigurationError("Source models must be non-empty.");
399
+ }
400
+ }
401
+
402
+ // src/prio-llm-router.ts
403
+ var PrioLlmRouter = class {
404
+ providersByName = /* @__PURE__ */ new Map();
405
+ modelsByName = /* @__PURE__ */ new Map();
406
+ defaultChain;
407
+ executor;
408
+ hooks;
409
+ constructor(options) {
410
+ const normalized = resolveRouterConfig(options);
411
+ if (normalized.providers.length === 0) {
412
+ throw new RouterConfigurationError(
413
+ "At least one provider configuration is required."
414
+ );
415
+ }
416
+ if (normalized.models.length === 0) {
417
+ throw new RouterConfigurationError(
418
+ "At least one model configuration is required."
419
+ );
420
+ }
421
+ this.defaultChain = normalized.defaultChain;
422
+ this.hooks = options.hooks;
423
+ this.executor = options.executor ?? (options.defaultProviderMaxRetries === void 0 ? createDefaultTextGenerationExecutor() : createDefaultTextGenerationExecutor({
424
+ defaultProviderMaxRetries: options.defaultProviderMaxRetries
425
+ }));
426
+ for (const provider of normalized.providers) {
427
+ this.assertUniqueName(
428
+ this.providersByName,
429
+ provider.name,
430
+ "provider configuration"
431
+ );
432
+ this.validateProvider(provider);
433
+ this.providersByName.set(provider.name, provider);
434
+ }
435
+ normalized.models.forEach((model, index) => {
436
+ this.assertUniqueName(this.modelsByName, model.name, "model configuration");
437
+ if (!this.providersByName.has(model.provider)) {
438
+ throw new RouterConfigurationError(
439
+ `Model "${model.name}" references unknown provider "${model.provider}".`
440
+ );
441
+ }
442
+ this.modelsByName.set(model.name, {
443
+ ...model,
444
+ __index: index
445
+ });
446
+ });
447
+ if (this.defaultChain) {
448
+ this.resolveNamedChain(this.defaultChain);
449
+ }
450
+ }
451
+ listProviders() {
452
+ return [...this.providersByName.values()];
453
+ }
454
+ listModels() {
455
+ return [...this.modelsByName.values()].sort(compareModels).map((model) => this.toResolvedTarget(model));
456
+ }
457
+ async generateText(request) {
458
+ const chain = this.resolveExecutionChain(request.chain);
459
+ const attempts = [];
460
+ let lastError;
461
+ for (const [index, model] of chain.entries()) {
462
+ const provider = this.providersByName.get(model.provider);
463
+ if (!provider) {
464
+ throw new RouterConfigurationError(
465
+ `Provider "${model.provider}" was not found for target "${model.name}".`
466
+ );
467
+ }
468
+ const pendingAttempt = {
469
+ attemptIndex: index,
470
+ targetName: model.name,
471
+ providerName: provider.name,
472
+ providerType: provider.type,
473
+ model: model.model,
474
+ startedAt: /* @__PURE__ */ new Date()
475
+ };
476
+ if (model.tier !== void 0) {
477
+ pendingAttempt.tier = model.tier;
478
+ }
479
+ this.hooks?.onAttemptStart?.(pendingAttempt);
480
+ try {
481
+ const result = await this.executor.execute({
482
+ provider,
483
+ model,
484
+ request
485
+ });
486
+ const finishedAt = /* @__PURE__ */ new Date();
487
+ const attemptRecord = {
488
+ ...pendingAttempt,
489
+ finishedAt,
490
+ durationMs: finishedAt.getTime() - pendingAttempt.startedAt.getTime(),
491
+ success: true
492
+ };
493
+ attempts.push(attemptRecord);
494
+ this.hooks?.onAttemptSuccess?.(attemptRecord);
495
+ const response = {
496
+ text: result.text,
497
+ target: this.toResolvedTarget(model),
498
+ attempts,
499
+ finishReason: result.finishReason,
500
+ raw: result.raw
501
+ };
502
+ if (result.usage) {
503
+ response.usage = result.usage;
504
+ }
505
+ if (result.warnings) {
506
+ response.warnings = result.warnings;
507
+ }
508
+ return response;
509
+ } catch (error) {
510
+ if (isAbortError(error)) {
511
+ throw error;
512
+ }
513
+ const finishedAt = /* @__PURE__ */ new Date();
514
+ const attemptRecord = {
515
+ ...pendingAttempt,
516
+ finishedAt,
517
+ durationMs: finishedAt.getTime() - pendingAttempt.startedAt.getTime(),
518
+ success: false,
519
+ error: serializeError(error)
520
+ };
521
+ attempts.push(attemptRecord);
522
+ this.hooks?.onAttemptFailure?.(attemptRecord);
523
+ lastError = error;
524
+ }
525
+ }
526
+ throw new AllModelsFailedError(attempts, lastError);
527
+ }
528
+ async streamText(request) {
529
+ const chain = this.resolveExecutionChain(request.chain);
530
+ const attempts = [];
531
+ let lastError;
532
+ for (const [index, model] of chain.entries()) {
533
+ const provider = this.providersByName.get(model.provider);
534
+ if (!provider) {
535
+ throw new RouterConfigurationError(
536
+ `Provider "${model.provider}" was not found for target "${model.name}".`
537
+ );
538
+ }
539
+ const pendingAttempt = createPendingAttempt(index, provider, model);
540
+ this.hooks?.onAttemptStart?.(pendingAttempt);
541
+ const { controller, cleanup, parentAborted } = createLinkedAbortController(
542
+ request.abortSignal
543
+ );
544
+ try {
545
+ const streamResult = await this.executor.stream({
546
+ provider,
547
+ model,
548
+ request: {
549
+ ...request,
550
+ abortSignal: controller.signal
551
+ }
552
+ });
553
+ const iterator = streamResult.textStream[Symbol.asyncIterator]();
554
+ const firstChunk = await this.waitForFirstChunk({
555
+ iterator,
556
+ timeoutMs: request.firstChunkTimeoutMs,
557
+ abortController: controller,
558
+ parentAborted
559
+ });
560
+ if (firstChunk.done) {
561
+ throw createEmptyFirstChunkError(model.name);
562
+ }
563
+ return this.createStreamingResult({
564
+ pendingAttempt,
565
+ attempts,
566
+ model,
567
+ iterator,
568
+ streamResult,
569
+ firstChunk: firstChunk.value,
570
+ cleanupAbortLink: cleanup
571
+ });
572
+ } catch (error) {
573
+ cleanup();
574
+ if (isAbortError(error) && parentAborted()) {
575
+ throw error;
576
+ }
577
+ const attemptRecord = createFailedAttemptRecord(pendingAttempt, error);
578
+ attempts.push(attemptRecord);
579
+ this.hooks?.onAttemptFailure?.(attemptRecord);
580
+ lastError = error;
581
+ }
582
+ }
583
+ throw new AllModelsFailedError(attempts, lastError);
584
+ }
585
+ createStreamingResult(options) {
586
+ const {
587
+ pendingAttempt,
588
+ attempts,
589
+ model,
590
+ iterator,
591
+ streamResult,
592
+ firstChunk,
593
+ cleanupAbortLink
594
+ } = options;
595
+ let started = false;
596
+ const textParts = [firstChunk];
597
+ let resolveFinal;
598
+ let rejectFinal;
599
+ let finalized = false;
600
+ const final = new Promise((resolve, reject) => {
601
+ resolveFinal = resolve;
602
+ rejectFinal = reject;
603
+ });
604
+ const finalizeSuccess = async () => {
605
+ if (finalized) {
606
+ return;
607
+ }
608
+ finalized = true;
609
+ const finishedAt = /* @__PURE__ */ new Date();
610
+ const attemptRecord = {
611
+ ...pendingAttempt,
612
+ finishedAt,
613
+ durationMs: finishedAt.getTime() - pendingAttempt.startedAt.getTime(),
614
+ success: true
615
+ };
616
+ attempts.push(attemptRecord);
617
+ this.hooks?.onAttemptSuccess?.(attemptRecord);
618
+ const result = {
619
+ text: textParts.join(""),
620
+ target: this.toResolvedTarget(model),
621
+ attempts: [...attempts],
622
+ finishReason: await streamResult.finishReason,
623
+ raw: streamResult.raw
624
+ };
625
+ const usage = await streamResult.usage;
626
+ if (usage) {
627
+ result.usage = usage;
628
+ }
629
+ const warnings = await streamResult.warnings;
630
+ if (warnings) {
631
+ result.warnings = warnings;
632
+ }
633
+ cleanupAbortLink();
634
+ resolveFinal(result);
635
+ };
636
+ const finalizeFailure = (error) => {
637
+ if (finalized) {
638
+ return;
639
+ }
640
+ finalized = true;
641
+ const attemptRecord = createFailedAttemptRecord(pendingAttempt, error);
642
+ attempts.push(attemptRecord);
643
+ this.hooks?.onAttemptFailure?.(attemptRecord);
644
+ cleanupAbortLink();
645
+ rejectFinal(error);
646
+ };
647
+ const wrappedStream = {
648
+ [Symbol.asyncIterator]: () => {
649
+ if (started) {
650
+ throw new RouterConfigurationError(
651
+ "This stream can only be consumed once."
652
+ );
653
+ }
654
+ started = true;
655
+ return createRouterTextStreamIterator({
656
+ firstChunk,
657
+ iterator,
658
+ onChunk: (chunk) => {
659
+ textParts.push(chunk);
660
+ },
661
+ onSuccess: finalizeSuccess,
662
+ onFailure: finalizeFailure
663
+ });
664
+ }
665
+ };
666
+ return {
667
+ target: this.toResolvedTarget(model),
668
+ selectedAttempt: pendingAttempt,
669
+ attempts: [...attempts],
670
+ textStream: wrappedStream,
671
+ final,
672
+ consumeStream: async () => {
673
+ if (!started) {
674
+ for await (const _ of wrappedStream) {
675
+ }
676
+ }
677
+ return final;
678
+ }
679
+ };
680
+ }
681
+ async waitForFirstChunk(options) {
682
+ const { iterator, timeoutMs, abortController, parentAborted } = options;
683
+ const nextPromise = iterator.next();
684
+ if (timeoutMs === void 0) {
685
+ return nextPromise;
686
+ }
687
+ const timeoutError = createFirstChunkTimeoutError(timeoutMs);
688
+ const timedRace = await Promise.race([
689
+ nextPromise.then(
690
+ (value) => ({ kind: "value", value }),
691
+ (error) => ({ kind: "error", error })
692
+ ),
693
+ delay(timeoutMs).then(() => ({ kind: "timeout" }))
694
+ ]);
695
+ if (timedRace.kind === "value") {
696
+ return timedRace.value;
697
+ }
698
+ if (timedRace.kind === "timeout") {
699
+ abortController.abort(timeoutError);
700
+ void nextPromise.catch(() => void 0);
701
+ throw timeoutError;
702
+ }
703
+ if (isAbortError(timedRace.error) && parentAborted()) {
704
+ throw timedRace.error;
705
+ }
706
+ throw timedRace.error;
707
+ }
708
+ resolveExecutionChain(chain) {
709
+ if (chain?.length) {
710
+ return this.resolveNamedChain(chain);
711
+ }
712
+ if (this.defaultChain?.length) {
713
+ return this.resolveNamedChain(this.defaultChain);
714
+ }
715
+ const implicitChain = [...this.modelsByName.values()].filter((model) => this.isModelEnabled(model)).sort(compareModels);
716
+ if (implicitChain.length === 0) {
717
+ throw new RouterConfigurationError(
718
+ "No enabled model targets are available for execution."
719
+ );
720
+ }
721
+ return implicitChain;
722
+ }
723
+ resolveNamedChain(chain) {
724
+ const resolved = [];
725
+ const seen = /* @__PURE__ */ new Set();
726
+ for (const targetName of chain) {
727
+ if (seen.has(targetName)) {
728
+ continue;
729
+ }
730
+ seen.add(targetName);
731
+ const model = this.modelsByName.get(targetName);
732
+ if (!model) {
733
+ throw new RouterConfigurationError(
734
+ `Model target "${targetName}" is not configured.`
735
+ );
736
+ }
737
+ if (!this.isModelEnabled(model)) {
738
+ throw new RouterConfigurationError(
739
+ `Model target "${targetName}" is disabled or its provider is disabled.`
740
+ );
741
+ }
742
+ resolved.push(model);
743
+ }
744
+ if (resolved.length === 0) {
745
+ throw new RouterConfigurationError(
746
+ "The resolved execution chain is empty."
747
+ );
748
+ }
749
+ return resolved;
750
+ }
751
+ isModelEnabled(model) {
752
+ if (model.enabled === false) {
753
+ return false;
754
+ }
755
+ const provider = this.providersByName.get(model.provider);
756
+ return provider?.enabled !== false;
757
+ }
758
+ toResolvedTarget(model) {
759
+ const provider = this.providersByName.get(model.provider);
760
+ if (!provider) {
761
+ throw new RouterConfigurationError(
762
+ `Model "${model.name}" references missing provider "${model.provider}".`
763
+ );
764
+ }
765
+ const resolvedTarget = {
766
+ name: model.name,
767
+ providerName: provider.name,
768
+ providerType: provider.type,
769
+ model: model.model
770
+ };
771
+ if (model.priority !== void 0) {
772
+ resolvedTarget.priority = model.priority;
773
+ }
774
+ if (model.tier !== void 0) {
775
+ resolvedTarget.tier = model.tier;
776
+ }
777
+ if (model.metadata !== void 0) {
778
+ resolvedTarget.metadata = model.metadata;
779
+ }
780
+ return resolvedTarget;
781
+ }
782
+ validateProvider(provider) {
783
+ if (!provider.name.trim()) {
784
+ throw new RouterConfigurationError(
785
+ "Provider configuration names must be non-empty."
786
+ );
787
+ }
788
+ if (!provider.auth.apiKey.trim()) {
789
+ throw new RouterConfigurationError(
790
+ `Provider "${provider.name}" requires a non-empty API key.`
791
+ );
792
+ }
793
+ }
794
+ assertUniqueName(registry, name, label) {
795
+ if (!name.trim()) {
796
+ throw new RouterConfigurationError(`${label} names must be non-empty.`);
797
+ }
798
+ if (registry.has(name)) {
799
+ throw new RouterConfigurationError(
800
+ `Duplicate ${label} name "${name}" detected.`
801
+ );
802
+ }
803
+ }
804
+ };
805
+ function createLlmRouter(options) {
806
+ return new PrioLlmRouter(options);
807
+ }
808
+ function resolveRouterConfig(options) {
809
+ if ("sources" in options) {
810
+ return compileSources(options.sources, options.defaultChain);
811
+ }
812
+ const normalized = {
813
+ providers: options.providers,
814
+ models: options.models
815
+ };
816
+ if (options.defaultChain !== void 0) {
817
+ normalized.defaultChain = options.defaultChain;
818
+ }
819
+ return normalized;
820
+ }
821
+ function compareModels(left, right) {
822
+ const leftPriority = left.priority ?? Number.MAX_SAFE_INTEGER;
823
+ const rightPriority = right.priority ?? Number.MAX_SAFE_INTEGER;
824
+ if (leftPriority !== rightPriority) {
825
+ return leftPriority - rightPriority;
826
+ }
827
+ return left.__index - right.__index;
828
+ }
829
+ function compileSources(sources, defaultChain) {
830
+ const providersByName = /* @__PURE__ */ new Map();
831
+ const models = [];
832
+ for (const source of sources) {
833
+ const provider = source.connection.provider;
834
+ const providerName = provider.name.trim();
835
+ const modelId = source.config.model.trim();
836
+ const access = source.config.access ?? "standard";
837
+ if (!providerName) {
838
+ throw new RouterConfigurationError(
839
+ "Connection provider names must be non-empty."
840
+ );
841
+ }
842
+ if (!source.config.name.trim()) {
843
+ throw new RouterConfigurationError("Source names must be non-empty.");
844
+ }
845
+ if (!modelId) {
846
+ throw new RouterConfigurationError("Source models must be non-empty.");
847
+ }
848
+ const existingProvider = providersByName.get(providerName);
849
+ if (existingProvider) {
850
+ assertMatchingSourceProvider(existingProvider, provider);
851
+ } else {
852
+ providersByName.set(providerName, provider);
853
+ }
854
+ if (access === "free") {
855
+ assertGuaranteedFreeSource(provider, source.config.model);
856
+ }
857
+ if (access === "free" && source.config.tier === "paid") {
858
+ throw new RouterConfigurationError(
859
+ `Free source "${source.config.name}" cannot be marked as paid.`
860
+ );
861
+ }
862
+ const compiledModel = {
863
+ name: source.config.name,
864
+ provider: providerName,
865
+ model: modelId
866
+ };
867
+ if (source.config.enabled !== void 0) {
868
+ compiledModel.enabled = source.config.enabled;
869
+ }
870
+ if (source.config.priority !== void 0) {
871
+ compiledModel.priority = source.config.priority;
872
+ }
873
+ const tier = source.config.tier ?? (access === "free" ? "free" : void 0);
874
+ if (tier !== void 0) {
875
+ compiledModel.tier = tier;
876
+ }
877
+ if (source.config.metadata !== void 0) {
878
+ compiledModel.metadata = source.config.metadata;
879
+ }
880
+ models.push(compiledModel);
881
+ }
882
+ const normalized = {
883
+ providers: [...providersByName.values()],
884
+ models
885
+ };
886
+ if (defaultChain !== void 0) {
887
+ normalized.defaultChain = defaultChain;
888
+ }
889
+ return normalized;
890
+ }
891
+ function assertMatchingSourceProvider(existingProvider, nextProvider) {
892
+ if (JSON.stringify(existingProvider) === JSON.stringify(nextProvider)) {
893
+ return;
894
+ }
895
+ throw new RouterConfigurationError(
896
+ `Connection provider "${existingProvider.name}" is configured more than once with different settings.`
897
+ );
898
+ }
899
+ function assertGuaranteedFreeSource(provider, model) {
900
+ if (provider.type !== "openrouter") {
901
+ throw new RouterConfigurationError(
902
+ `Provider "${provider.name}" does not support strict free sources. Only OpenRouter with explicit ":free" model variants is supported today.`
903
+ );
904
+ }
905
+ const normalizedModel = model.trim();
906
+ if (normalizedModel === "openrouter/free") {
907
+ throw new RouterConfigurationError(
908
+ `Free source "${provider.name}" cannot use "openrouter/free". Use an explicit ":free" model id instead.`
909
+ );
910
+ }
911
+ if (!normalizedModel.endsWith(":free")) {
912
+ throw new RouterConfigurationError(
913
+ `Free OpenRouter sources must use an explicit ":free" model id. Received "${normalizedModel}".`
914
+ );
915
+ }
916
+ }
917
+ function createPendingAttempt(attemptIndex, provider, model) {
918
+ const pendingAttempt = {
919
+ attemptIndex,
920
+ targetName: model.name,
921
+ providerName: provider.name,
922
+ providerType: provider.type,
923
+ model: model.model,
924
+ startedAt: /* @__PURE__ */ new Date()
925
+ };
926
+ if (model.tier !== void 0) {
927
+ pendingAttempt.tier = model.tier;
928
+ }
929
+ return pendingAttempt;
930
+ }
931
+ function createFailedAttemptRecord(pendingAttempt, error) {
932
+ const finishedAt = /* @__PURE__ */ new Date();
933
+ return {
934
+ ...pendingAttempt,
935
+ finishedAt,
936
+ durationMs: finishedAt.getTime() - pendingAttempt.startedAt.getTime(),
937
+ success: false,
938
+ error: serializeError(error)
939
+ };
940
+ }
941
+ function createRouterTextStreamIterator(options) {
942
+ const { firstChunk, iterator, onChunk, onSuccess, onFailure } = options;
943
+ let firstYielded = false;
944
+ let finished = false;
945
+ return {
946
+ async next() {
947
+ if (finished) {
948
+ return {
949
+ done: true,
950
+ value: void 0
951
+ };
952
+ }
953
+ if (!firstYielded) {
954
+ firstYielded = true;
955
+ return {
956
+ done: false,
957
+ value: firstChunk
958
+ };
959
+ }
960
+ try {
961
+ const next = await iterator.next();
962
+ if (next.done) {
963
+ finished = true;
964
+ await onSuccess();
965
+ return {
966
+ done: true,
967
+ value: void 0
968
+ };
969
+ }
970
+ onChunk(next.value);
971
+ return {
972
+ done: false,
973
+ value: next.value
974
+ };
975
+ } catch (error) {
976
+ finished = true;
977
+ onFailure(error);
978
+ throw error;
979
+ }
980
+ },
981
+ async return() {
982
+ finished = true;
983
+ onFailure(createStreamClosedEarlyError());
984
+ if (typeof iterator.return === "function") {
985
+ await iterator.return();
986
+ }
987
+ return {
988
+ done: true,
989
+ value: void 0
990
+ };
991
+ },
992
+ async throw(error) {
993
+ finished = true;
994
+ onFailure(error);
995
+ if (typeof iterator.throw === "function") {
996
+ return iterator.throw(error);
997
+ }
998
+ throw error;
999
+ }
1000
+ };
1001
+ }
1002
+ function createLinkedAbortController(parentSignal) {
1003
+ const controller = new AbortController();
1004
+ let abortedByParent = false;
1005
+ const abortFromParent = () => {
1006
+ abortedByParent = true;
1007
+ controller.abort(parentSignal?.reason);
1008
+ };
1009
+ if (parentSignal?.aborted) {
1010
+ abortFromParent();
1011
+ } else {
1012
+ parentSignal?.addEventListener("abort", abortFromParent, { once: true });
1013
+ }
1014
+ return {
1015
+ controller,
1016
+ cleanup: () => {
1017
+ parentSignal?.removeEventListener("abort", abortFromParent);
1018
+ },
1019
+ parentAborted: () => abortedByParent
1020
+ };
1021
+ }
1022
+ function createFirstChunkTimeoutError(timeoutMs) {
1023
+ const error = new Error(
1024
+ `The first stream chunk did not arrive within ${timeoutMs}ms.`
1025
+ );
1026
+ error.name = "FirstChunkTimeoutError";
1027
+ return error;
1028
+ }
1029
+ function createEmptyFirstChunkError(targetName) {
1030
+ const error = new Error(
1031
+ `Stream for target "${targetName}" completed before the first text chunk.`
1032
+ );
1033
+ error.name = "EmptyStreamError";
1034
+ return error;
1035
+ }
1036
+ function createStreamClosedEarlyError() {
1037
+ const error = new Error("The stream was closed before completion.");
1038
+ error.name = "StreamClosedError";
1039
+ return error;
1040
+ }
1041
+ function delay(ms) {
1042
+ return new Promise((resolve) => {
1043
+ setTimeout(resolve, ms);
1044
+ });
1045
+ }
1046
+
1047
+ exports.AllModelsFailedError = AllModelsFailedError;
1048
+ exports.PrioLlmRouter = PrioLlmRouter;
1049
+ exports.PrioLlmRouterError = PrioLlmRouterError;
1050
+ exports.RouterConfigurationError = RouterConfigurationError;
1051
+ exports.createDefaultTextGenerationExecutor = createDefaultTextGenerationExecutor;
1052
+ exports.createLlmConnection = createLlmConnection;
1053
+ exports.createLlmRouter = createLlmRouter;
1054
+ exports.createLlmSource = createLlmSource;
1055
+ exports.isAbortError = isAbortError;
1056
+ exports.serializeError = serializeError;
1057
+ //# sourceMappingURL=index.cjs.map
1058
+ //# sourceMappingURL=index.cjs.map