combined-ai 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,1796 @@
1
+ 'use strict';
2
+
3
+ // src/combine/index.ts
4
+ var STRATEGY_NAMES = [
5
+ "consensus",
6
+ "pipeline",
7
+ "ensemble",
8
+ "broadcast"
9
+ ];
10
+
11
+ // src/combine/shared.ts
12
+ function makeEmitter(onEvent) {
13
+ return (event) => {
14
+ try {
15
+ onEvent?.(event);
16
+ } catch {
17
+ }
18
+ };
19
+ }
20
+ var SANITIZE_FRAMING = 'Rewrite the following answer so it reads as a single, self-contained reply addressed directly to the user. Remove any meta-commentary about how it was produced \u2014 any reference to other answers, drafts, candidates, sources, reviewers, or a synthesis or selection process (for example "this answer synthesizes the drafts", "among the candidates", or "Answer A"). Preserve the substance, wording, and length as much as possible; change only what is needed. Output only the rewritten answer, with no preamble.';
21
+ function completionFor(request, system, messages, overrides) {
22
+ const completion = { messages };
23
+ if (system !== void 0) {
24
+ completion.system = system;
25
+ }
26
+ const model = overrides?.model ?? request.model;
27
+ if (model !== void 0) {
28
+ completion.model = model;
29
+ }
30
+ const maxTokens = overrides?.maxTokens ?? request.maxTokens;
31
+ if (maxTokens !== void 0) {
32
+ completion.maxTokens = maxTokens;
33
+ }
34
+ if (request.signal !== void 0) {
35
+ completion.signal = request.signal;
36
+ }
37
+ if (request.responseFormat !== void 0) {
38
+ completion.responseFormat = request.responseFormat;
39
+ }
40
+ return completion;
41
+ }
42
+ function composeSystem(userSystem, framing) {
43
+ return userSystem === void 0 ? framing : `${userSystem}
44
+
45
+ ${framing}`;
46
+ }
47
+ async function runOutcome(id, provider, run) {
48
+ try {
49
+ return { id, provider, status: "ok", result: await run() };
50
+ } catch (error) {
51
+ return {
52
+ id,
53
+ provider,
54
+ status: "failed",
55
+ error: error instanceof Error ? error : new Error(String(error))
56
+ };
57
+ }
58
+ }
59
+ async function respondAll(roster, request, emit) {
60
+ return Promise.all(
61
+ roster.map(async (entry) => {
62
+ const outcome = await runOutcome(
63
+ entry.id,
64
+ entry.providerName,
65
+ () => entry.provider.complete(
66
+ completionFor(request, request.system, request.messages, entry)
67
+ )
68
+ );
69
+ emit({
70
+ type: "response",
71
+ id: entry.id,
72
+ provider: entry.providerName,
73
+ status: outcome.status
74
+ });
75
+ return outcome;
76
+ })
77
+ );
78
+ }
79
+ function noResultError(message, outcomes) {
80
+ return aggregateError(
81
+ message,
82
+ outcomes.flatMap((o) => o.status === "failed" ? [o.error] : [])
83
+ );
84
+ }
85
+ async function sanitizeAnswer(provider, request, answer, overrides) {
86
+ try {
87
+ const result = await provider.complete(
88
+ completionFor(
89
+ request,
90
+ composeSystem(request.system, SANITIZE_FRAMING),
91
+ [{ role: "user", content: answer }],
92
+ overrides
93
+ )
94
+ );
95
+ return {
96
+ text: result.text.trim() === "" ? answer : result.text,
97
+ usage: result.usage
98
+ };
99
+ } catch {
100
+ return { text: answer };
101
+ }
102
+ }
103
+ function outcomeUsage(outcomes) {
104
+ return outcomes.map((o) => ({
105
+ id: o.id,
106
+ usage: o.status === "ok" ? o.result.usage : void 0
107
+ }));
108
+ }
109
+ function aggregateUsage(entries) {
110
+ const byParticipant = {};
111
+ const total = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
112
+ for (const { id, usage } of entries) {
113
+ if (usage === void 0) {
114
+ continue;
115
+ }
116
+ total.inputTokens += usage.inputTokens;
117
+ total.outputTokens += usage.outputTokens;
118
+ total.totalTokens += usage.totalTokens;
119
+ const acc = byParticipant[id];
120
+ byParticipant[id] = {
121
+ inputTokens: (acc?.inputTokens ?? 0) + usage.inputTokens,
122
+ outputTokens: (acc?.outputTokens ?? 0) + usage.outputTokens,
123
+ totalTokens: (acc?.totalTokens ?? 0) + usage.totalTokens
124
+ };
125
+ }
126
+ return Object.keys(byParticipant).length === 0 ? void 0 : { total, byParticipant };
127
+ }
128
+ function textOf(content) {
129
+ return typeof content === "string" ? content : content.filter((part) => part.type === "text").map((part) => part.text).join("");
130
+ }
131
+ function renderConversation(messages) {
132
+ if (messages.length === 1) {
133
+ return textOf(messages[0]?.content ?? "");
134
+ }
135
+ return messages.map(
136
+ (m) => `${m.role === "user" ? "User" : "Assistant"}: ${textOf(m.content)}`
137
+ ).join("\n\n");
138
+ }
139
+
140
+ // src/combine/broadcast.ts
141
+ async function broadcast(roster, request, onEvent) {
142
+ const emit = makeEmitter(onEvent);
143
+ const responses = await respondAll(roster, request, emit);
144
+ if (!responses.some((o) => o.status === "ok")) {
145
+ throw noResultError(
146
+ "Broadcast failed: no participant returned a response.",
147
+ responses
148
+ );
149
+ }
150
+ return {
151
+ strategy: "broadcast",
152
+ responses,
153
+ usage: aggregateUsage(outcomeUsage(responses))
154
+ };
155
+ }
156
+
157
+ // src/combine/consensus.ts
158
+ var CONCISE_DIRECTIVE = "Skip greetings, sign-offs, and preamble \u2014 begin directly with the content. Include your reasoning, assumptions, and any caveats; your reply is read by other AI assistants, not an end user, so favor complete substance over brevity of style.";
159
+ var CRITIQUE_FRAMING = "You are one of several AI assistants that independently answered the same question. Below are all the drafts. Critically evaluate them: point out errors, gaps, and risks in each, and scrutinize the reasoning, not just the conclusions. Judge every answer on its merits regardless of which assistant wrote it, including any you may have written yourself; refer to answers by their heading. Do not write the final answer yet \u2014 produce only a critique, then end with exactly three lines:\nBEST: <heading of the strongest answer>\nKEY FIX: <the single most important improvement to it>\nCONFIDENCE: <low | medium | high>";
160
+ var SYNTH_FRAMING = `You are the lead assistant writing the final answer to the user's question below. You are given several draft answers from other assistants, plus their critiques (each ending with that critic's pick and most important fix), as private input material \u2014 the user has not seen any of it and must not learn it exists. Use it to produce the most correct answer. Prefer correctness over popularity: a single correct draft beats a wrong majority, so adopt it and do not average conflicting claims; blend points only when they are genuinely complementary, not contradictory; if the material is inconclusive, say so plainly rather than papering over it. Judge every draft on its merits regardless of source \u2014 one may be your own, which you must not favor. Write ONLY the answer itself, addressed to the user as if answering for the first time. Do not mention or allude to this material, the other assistants, the drafting or critique process, or the existence of multiple answers, and do not use words like "candidates", "the drafts", or "the options" or labels like "Answer A"/"Answer B".`;
161
+ var LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
162
+ async function consensus(roster, synthesizer, request, onEvent) {
163
+ const anonymized = (request.attribution ?? "anonymized") === "anonymized";
164
+ const minParticipants = request.minParticipants ?? 2;
165
+ const emit = makeEmitter(onEvent);
166
+ emit({ type: "phase", phase: "drafting" });
167
+ const draftSystem = composeSystem(request.system, CONCISE_DIRECTIVE);
168
+ const draftResults = await Promise.all(
169
+ roster.map(async (entry) => {
170
+ const outcome = await runOutcome(
171
+ entry.id,
172
+ entry.providerName,
173
+ () => entry.provider.complete(
174
+ completionFor(request, draftSystem, request.messages, entry)
175
+ )
176
+ );
177
+ emit({
178
+ type: "draft",
179
+ id: entry.id,
180
+ provider: entry.providerName,
181
+ status: outcome.status
182
+ });
183
+ return { entry, outcome };
184
+ })
185
+ );
186
+ const drafts = draftResults.map((d) => d.outcome);
187
+ const survivors = draftResults.flatMap(
188
+ (d) => d.outcome.status === "ok" && d.outcome.result.text.trim() !== "" ? [{ ...d.entry, result: d.outcome.result }] : []
189
+ );
190
+ const [firstSurvivor] = survivors;
191
+ if (firstSurvivor === void 0) {
192
+ throw noResultError(
193
+ "Consensus failed: no participant produced a draft.",
194
+ drafts
195
+ );
196
+ }
197
+ if (roster.length === 1) {
198
+ return {
199
+ text: firstSurvivor.result.text,
200
+ strategy: "consensus",
201
+ synthesizer: firstSurvivor.id,
202
+ model: firstSurvivor.result.model,
203
+ drafts,
204
+ critiques: [],
205
+ usage: aggregateUsage(outcomeUsage(drafts))
206
+ };
207
+ }
208
+ if (survivors.length < minParticipants) {
209
+ throw noResultError(
210
+ `Consensus failed: only ${String(survivors.length)} of ${String(roster.length)} participants produced a draft (minimum ${String(minParticipants)}).`,
211
+ drafts
212
+ );
213
+ }
214
+ emit({ type: "phase", phase: "critiquing" });
215
+ const answersBlock = renderAnswers(survivors, anonymized);
216
+ const question = renderConversation(request.messages);
217
+ const critiqueBody = `## Original question
218
+ ${question}
219
+
220
+ ## Drafts
221
+ ${answersBlock}`;
222
+ const critiqueSystem = composeSystem(
223
+ request.system,
224
+ `${CONCISE_DIRECTIVE}
225
+
226
+ ${CRITIQUE_FRAMING}`
227
+ );
228
+ const critiques = await Promise.all(
229
+ survivors.map(async (s) => {
230
+ const outcome = await runOutcome(
231
+ s.id,
232
+ s.providerName,
233
+ () => s.provider.complete(
234
+ completionFor(
235
+ request,
236
+ critiqueSystem,
237
+ [{ role: "user", content: critiqueBody }],
238
+ s
239
+ )
240
+ )
241
+ );
242
+ emit({
243
+ type: "critique",
244
+ id: s.id,
245
+ provider: s.providerName,
246
+ status: outcome.status
247
+ });
248
+ return outcome;
249
+ })
250
+ );
251
+ emit({ type: "phase", phase: "synthesizing" });
252
+ const critiquesRendered = renderCritiques(critiques, anonymized);
253
+ const critiquesBlock = critiquesRendered === "" ? "" : `
254
+
255
+ ## Critiques
256
+ ${critiquesRendered}`;
257
+ const synthBody = `${critiqueBody}${critiquesBlock}`;
258
+ const synthSystem = composeSystem(request.system, SYNTH_FRAMING);
259
+ let lastError;
260
+ const synthUsage = [];
261
+ for (const candidate of synthesizerOrder(survivors, synthesizer)) {
262
+ try {
263
+ const result = await candidate.provider.complete(
264
+ completionFor(
265
+ request,
266
+ synthSystem,
267
+ [{ role: "user", content: synthBody }],
268
+ candidate
269
+ )
270
+ );
271
+ synthUsage.push({ id: candidate.id, usage: result.usage });
272
+ if (result.text.trim() === "") {
273
+ lastError = new Error(`${candidate.id} produced an empty synthesis`);
274
+ continue;
275
+ }
276
+ const sanitized = await sanitizeAnswer(
277
+ candidate.provider,
278
+ request,
279
+ result.text,
280
+ candidate
281
+ );
282
+ synthUsage.push({ id: candidate.id, usage: sanitized.usage });
283
+ return {
284
+ text: sanitized.text,
285
+ strategy: "consensus",
286
+ synthesizer: candidate.id,
287
+ model: result.model,
288
+ drafts,
289
+ critiques,
290
+ usage: aggregateUsage([
291
+ ...outcomeUsage(drafts),
292
+ ...outcomeUsage(critiques),
293
+ ...synthUsage
294
+ ])
295
+ };
296
+ } catch (error) {
297
+ lastError = error instanceof Error ? error : new Error(String(error));
298
+ }
299
+ }
300
+ throw new Error(
301
+ `Consensus synthesis failed for all participants: ${lastError?.message ?? "unknown error"}`
302
+ );
303
+ }
304
+ function synthesizerOrder(survivors, synthesizer) {
305
+ const requested = survivors.find((s) => s.id === synthesizer);
306
+ const rest = survivors.filter((s) => s !== requested);
307
+ return requested ? [requested, ...rest] : rest;
308
+ }
309
+ function renderAnswers(survivors, anonymized) {
310
+ return survivors.map((s, i) => {
311
+ const label = anonymized ? `Answer ${LETTERS[i] ?? `#${String(i + 1)}`}` : `Answer from ${s.id}`;
312
+ return `### ${label}
313
+ ${s.result.text}`;
314
+ }).join("\n\n");
315
+ }
316
+ function renderCritiques(critiques, anonymized) {
317
+ const blocks = [];
318
+ for (const [i, critique] of critiques.entries()) {
319
+ if (critique.status !== "ok") {
320
+ continue;
321
+ }
322
+ const label = anonymized ? `Critique ${LETTERS[i] ?? `#${String(i + 1)}`}` : `Critique from ${critique.id}`;
323
+ blocks.push(`### ${label}
324
+ ${critique.result.text}`);
325
+ }
326
+ return blocks.join("\n\n");
327
+ }
328
+
329
+ // src/combine/ensemble.ts
330
+ async function ensemble(roster, request, onEvent) {
331
+ const emit = makeEmitter(onEvent);
332
+ const responses = await respondAll(roster, request, emit);
333
+ const objects = responses.flatMap(
334
+ (o) => o.status === "ok" && isPlainObject(o.result.parsed) ? [o.result.parsed] : []
335
+ );
336
+ if (objects.length === 0) {
337
+ throw noResultError(
338
+ "Ensemble failed: no participant returned a valid structured object.",
339
+ responses
340
+ );
341
+ }
342
+ const { merged, agreement } = mergeObjects(objects);
343
+ return {
344
+ text: JSON.stringify(merged),
345
+ strategy: "ensemble",
346
+ merged,
347
+ agreement,
348
+ responses,
349
+ usage: aggregateUsage(outcomeUsage(responses))
350
+ };
351
+ }
352
+ function isPlainObject(value) {
353
+ return typeof value === "object" && value !== null && !Array.isArray(value);
354
+ }
355
+ function mergeObjects(objects) {
356
+ const byKey = /* @__PURE__ */ new Map();
357
+ for (const object of objects) {
358
+ for (const [key, value] of Object.entries(object)) {
359
+ const values = byKey.get(key);
360
+ if (values === void 0) {
361
+ byKey.set(key, [value]);
362
+ } else {
363
+ values.push(value);
364
+ }
365
+ }
366
+ }
367
+ const merged = {};
368
+ const byField = {};
369
+ for (const [key, values] of byKey) {
370
+ const field = mergeField(values, objects.length);
371
+ merged[key] = field.value;
372
+ byField[key] = field.agreement;
373
+ }
374
+ const scores = Object.values(byField);
375
+ const overall = scores.length === 0 ? 1 : scores.reduce((a, b) => a + b, 0) / scores.length;
376
+ return { merged, agreement: { overall, byField } };
377
+ }
378
+ function mergeField(values, total) {
379
+ const counts = /* @__PURE__ */ new Map();
380
+ let mode = values[0];
381
+ let maxCount = 0;
382
+ for (const value of values) {
383
+ const key = stableKey(value);
384
+ const count = (counts.get(key) ?? 0) + 1;
385
+ counts.set(key, count);
386
+ if (count > maxCount) {
387
+ maxCount = count;
388
+ mode = value;
389
+ }
390
+ }
391
+ return { value: mode, agreement: maxCount / total };
392
+ }
393
+ function stableKey(value) {
394
+ return JSON.stringify(value, (_key, val) => {
395
+ if (!isPlainObject(val)) {
396
+ return val;
397
+ }
398
+ const sortedKeys = Object.keys(val).sort();
399
+ return Object.fromEntries(sortedKeys.map((key) => [key, val[key]]));
400
+ });
401
+ }
402
+
403
+ // src/combine/pipeline.ts
404
+ var PIPELINE_FIRST_FRAMING = "You are the first stage in a pipeline of AI assistants answering the user's question below. Write the best, most complete and correct answer you can. Output only the answer itself, addressed to the user \u2014 no preamble and no notes about your process.";
405
+ var PIPELINE_REFINE_FRAMING = "You are one stage in a pipeline of AI assistants improving an answer to the user's question. Below are the question and the current answer from an earlier stage. Treat the current answer as a strong baseline: revise the current answer only to improve its correctness and completeness \u2014 fix errors, fill genuine gaps, and sharpen unclear wording. Preserve everything that is already correct and keep its substance and length; do not drop correct content or rewrite it merely to sound different. If you cannot improve it, return it unchanged. Output only the improved answer, addressed to the user as if answering for the first time \u2014 do not mention the earlier answer, the revision, or that multiple assistants were involved.";
406
+ async function pipeline(roster, request, onEvent) {
407
+ const emit = makeEmitter(onEvent);
408
+ const question = renderConversation(request.messages);
409
+ const firstSystem = composeSystem(request.system, PIPELINE_FIRST_FRAMING);
410
+ const refineSystem = composeSystem(request.system, PIPELINE_REFINE_FRAMING);
411
+ const stages = [];
412
+ let current;
413
+ for (const [index, entry] of roster.entries()) {
414
+ const completion = current === void 0 ? completionFor(request, firstSystem, request.messages, entry) : completionFor(
415
+ request,
416
+ refineSystem,
417
+ [
418
+ {
419
+ role: "user",
420
+ content: `## Question
421
+ ${question}
422
+
423
+ ## Current answer
424
+ ${current.text}`
425
+ }
426
+ ],
427
+ entry
428
+ );
429
+ const outcome = await runOutcome(
430
+ entry.id,
431
+ entry.providerName,
432
+ () => entry.provider.complete(completion)
433
+ );
434
+ stages.push(outcome);
435
+ emit({
436
+ type: "stage",
437
+ id: entry.id,
438
+ provider: entry.providerName,
439
+ status: outcome.status,
440
+ index
441
+ });
442
+ if (outcome.status === "ok" && outcome.result.text.trim() !== "") {
443
+ const text = outcome.result.text;
444
+ const needsSanitize = current !== void 0 && (text !== current.text || current.needsSanitize);
445
+ current = { entry, text, model: outcome.result.model, needsSanitize };
446
+ }
447
+ }
448
+ if (current === void 0) {
449
+ throw noResultError(
450
+ "Pipeline failed: no participant produced an answer.",
451
+ stages
452
+ );
453
+ }
454
+ const sanitized = current.needsSanitize ? await sanitizeAnswer(
455
+ current.entry.provider,
456
+ request,
457
+ current.text,
458
+ current.entry
459
+ ) : { text: current.text };
460
+ return {
461
+ text: sanitized.text,
462
+ strategy: "pipeline",
463
+ finalParticipant: current.entry.id,
464
+ model: current.model,
465
+ stages,
466
+ usage: aggregateUsage([
467
+ ...outcomeUsage(stages),
468
+ { id: current.entry.id, usage: sanitized.usage }
469
+ ])
470
+ };
471
+ }
472
+
473
+ // src/providers/extract.ts
474
+ function isRecord(value) {
475
+ return typeof value === "object" && value !== null;
476
+ }
477
+ function extractModel(data, fallback, field = "model") {
478
+ if (isRecord(data)) {
479
+ const value = data[field];
480
+ if (typeof value === "string") {
481
+ return value;
482
+ }
483
+ }
484
+ return fallback;
485
+ }
486
+
487
+ // src/providers/sse.ts
488
+ var DONE = /* @__PURE__ */ Symbol("sse-done");
489
+ async function* sseJson(body) {
490
+ const reader = body.getReader();
491
+ const decoder = new TextDecoder();
492
+ let buffer = "";
493
+ try {
494
+ for (let result = await reader.read(); !result.done; result = await reader.read()) {
495
+ buffer += decoder.decode(result.value, { stream: true });
496
+ const lines = buffer.split("\n");
497
+ buffer = lines.pop() ?? "";
498
+ for (const line of lines) {
499
+ const event2 = classifyLine(line);
500
+ if (event2 === DONE) {
501
+ return;
502
+ }
503
+ if (event2 !== void 0) {
504
+ yield event2;
505
+ }
506
+ }
507
+ }
508
+ const event = classifyLine(buffer);
509
+ if (event !== void 0 && event !== DONE) {
510
+ yield event;
511
+ }
512
+ } finally {
513
+ await reader.cancel();
514
+ }
515
+ }
516
+ function classifyLine(rawLine) {
517
+ const line = rawLine.trim();
518
+ if (!line.startsWith("data:")) {
519
+ return void 0;
520
+ }
521
+ const payload = line.slice("data:".length).trim();
522
+ if (payload === "") {
523
+ return void 0;
524
+ }
525
+ if (payload === "[DONE]") {
526
+ return DONE;
527
+ }
528
+ try {
529
+ const parsed = JSON.parse(payload);
530
+ return isRecord(parsed) ? parsed : void 0;
531
+ } catch {
532
+ return void 0;
533
+ }
534
+ }
535
+
536
+ // src/providers/structured.ts
537
+ function parseStructured(request, text) {
538
+ if (request.responseFormat === void 0) {
539
+ return void 0;
540
+ }
541
+ try {
542
+ return JSON.parse(text);
543
+ } catch {
544
+ return void 0;
545
+ }
546
+ }
547
+
548
+ // src/transport.ts
549
+ async function providerFetch(provider, input, init) {
550
+ try {
551
+ return await fetch(input, init);
552
+ } catch (cause) {
553
+ const reason = cause instanceof Error ? cause.message : String(cause);
554
+ throw new ProviderError(`${provider} request failed: ${reason}`, {
555
+ provider,
556
+ kind: "transport",
557
+ cause
558
+ });
559
+ }
560
+ }
561
+ var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 503, 529]);
562
+ var DEFAULT_MAX_RETRIES = 2;
563
+ var DEFAULT_BASE_DELAY_MS = 500;
564
+ var MAX_BACKOFF_MS = 6e4;
565
+ async function requestWithRetry(provider, input, init, retry) {
566
+ const maxRetries = Math.max(0, retry?.maxRetries ?? DEFAULT_MAX_RETRIES);
567
+ const baseDelayMs = retry?.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;
568
+ for (let attempt = 0; ; attempt++) {
569
+ const response = await providerFetch(provider, input, init);
570
+ if (response.ok || !RETRYABLE_STATUSES.has(response.status) || attempt >= maxRetries) {
571
+ return response;
572
+ }
573
+ const delayMs = retryDelayMs(response, attempt, baseDelayMs);
574
+ await response.body?.cancel();
575
+ await sleep(delayMs, init.signal ?? void 0);
576
+ }
577
+ }
578
+ function retryDelayMs(response, attempt, baseDelayMs) {
579
+ const retryAfter = parseRetryAfter(response.headers.get("retry-after"));
580
+ if (retryAfter !== void 0) {
581
+ return Math.min(retryAfter, MAX_BACKOFF_MS);
582
+ }
583
+ return Math.min(baseDelayMs * 2 ** attempt, MAX_BACKOFF_MS);
584
+ }
585
+ function parseRetryAfter(value) {
586
+ if (value === null) {
587
+ return void 0;
588
+ }
589
+ const trimmed = value.trim();
590
+ if (trimmed === "") {
591
+ return void 0;
592
+ }
593
+ const seconds = Number(trimmed);
594
+ if (Number.isFinite(seconds)) {
595
+ return Math.max(0, seconds * 1e3);
596
+ }
597
+ const date = Date.parse(trimmed);
598
+ if (Number.isNaN(date)) {
599
+ return void 0;
600
+ }
601
+ return Math.max(0, date - Date.now());
602
+ }
603
+ function sleep(ms, signal) {
604
+ return new Promise((resolve) => {
605
+ if (signal?.aborted) {
606
+ resolve();
607
+ return;
608
+ }
609
+ const onAbort = () => {
610
+ clearTimeout(timer);
611
+ resolve();
612
+ };
613
+ const timer = setTimeout(() => {
614
+ signal?.removeEventListener("abort", onAbort);
615
+ resolve();
616
+ }, ms);
617
+ signal?.addEventListener("abort", onAbort, { once: true });
618
+ });
619
+ }
620
+
621
+ // src/providers/anthropic.ts
622
+ var DEFAULT_MODEL = "claude-opus-4-8";
623
+ var DEFAULT_BASE_URL = "https://api.anthropic.com";
624
+ var ANTHROPIC_VERSION = "2023-06-01";
625
+ var DEFAULT_MAX_TOKENS = 16e3;
626
+ var DEFAULT_STREAM_MAX_TOKENS = 64e3;
627
+ var AnthropicProvider = class {
628
+ name = "anthropic";
629
+ #apiKey;
630
+ #model;
631
+ #baseUrl;
632
+ #retry;
633
+ constructor(options) {
634
+ this.#apiKey = options.apiKey;
635
+ this.#model = options.model ?? DEFAULT_MODEL;
636
+ this.#baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
637
+ this.#retry = options.retry;
638
+ }
639
+ async complete(request) {
640
+ const model = request.model ?? this.#model;
641
+ const response = await requestWithRetry(
642
+ "anthropic",
643
+ `${this.#baseUrl}/v1/messages`,
644
+ {
645
+ method: "POST",
646
+ headers: this.#headers(),
647
+ body: JSON.stringify(
648
+ this.#buildBody(request, model, DEFAULT_MAX_TOKENS, false)
649
+ ),
650
+ signal: request.signal
651
+ },
652
+ this.#retry
653
+ );
654
+ if (!response.ok) {
655
+ throw await apiError("anthropic", response);
656
+ }
657
+ const data = await response.json();
658
+ if (isRecord(data) && isRecord(data.error)) {
659
+ throw apiErrorFromBody("anthropic", response.status, data);
660
+ }
661
+ const rawFinishReason = extractFinishReason(data);
662
+ const text = extractText(data);
663
+ return {
664
+ text,
665
+ model: extractModel(data, model),
666
+ finishReason: normalizeFinishReason(rawFinishReason),
667
+ rawFinishReason,
668
+ refusal: extractRefusal(data),
669
+ usage: extractUsage(data),
670
+ parsed: parseStructured(request, text),
671
+ toolCalls: extractToolCalls(data)
672
+ };
673
+ }
674
+ async *stream(request) {
675
+ const model = request.model ?? this.#model;
676
+ const response = await requestWithRetry(
677
+ "anthropic",
678
+ `${this.#baseUrl}/v1/messages`,
679
+ {
680
+ method: "POST",
681
+ headers: this.#headers(),
682
+ body: JSON.stringify(
683
+ this.#buildBody(request, model, DEFAULT_STREAM_MAX_TOKENS, true)
684
+ ),
685
+ signal: request.signal
686
+ },
687
+ this.#retry
688
+ );
689
+ if (!response.ok) {
690
+ throw await apiError("anthropic", response);
691
+ }
692
+ if (!response.body) {
693
+ throw new Error("Anthropic streaming response had no body");
694
+ }
695
+ for await (const event of sseJson(response.body)) {
696
+ if (event.type === "message_stop") {
697
+ return;
698
+ }
699
+ if (event.type === "error") {
700
+ throw new Error(`Anthropic stream error: ${JSON.stringify(event)}`);
701
+ }
702
+ if (event.type === "content_block_delta") {
703
+ const delta = event.delta;
704
+ if (isRecord(delta) && delta.type === "text_delta" && typeof delta.text === "string") {
705
+ yield delta.text;
706
+ }
707
+ }
708
+ }
709
+ }
710
+ #headers() {
711
+ return {
712
+ "x-api-key": this.#apiKey,
713
+ "anthropic-version": ANTHROPIC_VERSION,
714
+ "content-type": "application/json"
715
+ };
716
+ }
717
+ #buildBody(request, model, defaultMaxTokens, stream) {
718
+ const body = {
719
+ model,
720
+ max_tokens: request.maxTokens ?? defaultMaxTokens,
721
+ messages: request.messages.map((message) => toAnthropicMessage(message))
722
+ };
723
+ if (request.system !== void 0) {
724
+ body.system = request.system;
725
+ }
726
+ if (request.responseFormat !== void 0) {
727
+ body.output_config = {
728
+ format: { type: "json_schema", schema: request.responseFormat.schema }
729
+ };
730
+ }
731
+ if (request.tools !== void 0) {
732
+ body.tools = request.tools.map((tool) => ({
733
+ name: tool.name,
734
+ description: tool.description,
735
+ input_schema: tool.parameters
736
+ }));
737
+ }
738
+ if (request.toolChoice !== void 0) {
739
+ body.tool_choice = toAnthropicToolChoice(request.toolChoice);
740
+ }
741
+ if (stream) {
742
+ body.stream = true;
743
+ }
744
+ return body;
745
+ }
746
+ };
747
+ function toAnthropicToolChoice(choice) {
748
+ if (typeof choice === "object") {
749
+ return { type: "tool", name: choice.name };
750
+ }
751
+ return { type: choice };
752
+ }
753
+ function toAnthropicMessage(message) {
754
+ if (typeof message.content === "string") {
755
+ return { role: message.role, content: message.content };
756
+ }
757
+ const blocks = message.content.map((part) => toAnthropicBlock(part));
758
+ const toolResults = blocks.filter((b) => b.type === "tool_result");
759
+ const rest = blocks.filter((b) => b.type !== "tool_result");
760
+ return { role: message.role, content: [...toolResults, ...rest] };
761
+ }
762
+ function toAnthropicBlock(part) {
763
+ switch (part.type) {
764
+ case "text":
765
+ return { type: "text", text: part.text };
766
+ case "image":
767
+ return { type: "image", source: toAnthropicSource(part.source) };
768
+ case "file":
769
+ return { type: "document", source: toAnthropicSource(part.source) };
770
+ case "tool_use":
771
+ return {
772
+ type: "tool_use",
773
+ id: part.id,
774
+ name: part.name,
775
+ input: part.input
776
+ };
777
+ case "tool_result":
778
+ return {
779
+ type: "tool_result",
780
+ tool_use_id: part.toolUseId,
781
+ content: part.content,
782
+ ...part.isError === void 0 ? {} : { is_error: part.isError }
783
+ };
784
+ }
785
+ }
786
+ function toAnthropicSource(source) {
787
+ return source.kind === "base64" ? { type: "base64", media_type: source.mediaType, data: source.data } : { type: "url", url: source.url };
788
+ }
789
+ function toArray(value) {
790
+ return Array.isArray(value) ? value : [];
791
+ }
792
+ function extractText(data) {
793
+ const content = isRecord(data) ? toArray(data.content) : [];
794
+ let text = "";
795
+ for (const block of content) {
796
+ if (isRecord(block) && block.type === "text" && typeof block.text === "string") {
797
+ text += block.text;
798
+ }
799
+ }
800
+ return text;
801
+ }
802
+ function extractToolCalls(data) {
803
+ const content = isRecord(data) ? toArray(data.content) : [];
804
+ const calls = [];
805
+ for (const block of content) {
806
+ if (isRecord(block) && block.type === "tool_use" && typeof block.name === "string") {
807
+ calls.push({
808
+ id: typeof block.id === "string" ? block.id : void 0,
809
+ name: block.name,
810
+ input: isRecord(block.input) ? block.input : {}
811
+ });
812
+ }
813
+ }
814
+ return calls.length > 0 ? calls : void 0;
815
+ }
816
+ function extractFinishReason(data) {
817
+ if (isRecord(data) && typeof data.stop_reason === "string") {
818
+ return data.stop_reason;
819
+ }
820
+ return void 0;
821
+ }
822
+ function normalizeFinishReason(raw) {
823
+ switch (raw) {
824
+ case void 0:
825
+ return void 0;
826
+ case "end_turn":
827
+ case "stop_sequence":
828
+ return "stop";
829
+ case "max_tokens":
830
+ return "length";
831
+ case "refusal":
832
+ return "content_filter";
833
+ case "tool_use":
834
+ return "tool_use";
835
+ default:
836
+ return "other";
837
+ }
838
+ }
839
+ function extractUsage(data) {
840
+ const usage = isRecord(data) ? data.usage : void 0;
841
+ if (!isRecord(usage)) {
842
+ return void 0;
843
+ }
844
+ const inputTokens = typeof usage.input_tokens === "number" ? usage.input_tokens : 0;
845
+ const outputTokens = typeof usage.output_tokens === "number" ? usage.output_tokens : 0;
846
+ return { inputTokens, outputTokens, totalTokens: inputTokens + outputTokens };
847
+ }
848
+ function extractRefusal(data) {
849
+ const content = isRecord(data) ? toArray(data.content) : [];
850
+ let refusal = "";
851
+ for (const block of content) {
852
+ if (isRecord(block) && block.type === "refusal" && typeof block.text === "string") {
853
+ refusal += block.text;
854
+ }
855
+ }
856
+ return refusal === "" ? void 0 : refusal;
857
+ }
858
+
859
+ // src/providers/google.ts
860
+ var DEFAULT_MODEL2 = "gemini-2.5-pro";
861
+ var DEFAULT_BASE_URL2 = "https://generativelanguage.googleapis.com";
862
+ var DEFAULT_MAX_TOKENS2 = 16e3;
863
+ var DEFAULT_STREAM_MAX_TOKENS2 = 64e3;
864
+ var GoogleProvider = class {
865
+ name = "google";
866
+ #apiKey;
867
+ #model;
868
+ #baseUrl;
869
+ #retry;
870
+ constructor(options) {
871
+ this.#apiKey = options.apiKey;
872
+ this.#model = options.model ?? DEFAULT_MODEL2;
873
+ this.#baseUrl = options.baseUrl ?? DEFAULT_BASE_URL2;
874
+ this.#retry = options.retry;
875
+ }
876
+ async complete(request) {
877
+ const model = request.model ?? this.#model;
878
+ const response = await requestWithRetry(
879
+ "google",
880
+ this.#url(model, false),
881
+ {
882
+ method: "POST",
883
+ headers: this.#headers(),
884
+ body: JSON.stringify(this.#buildBody(request, DEFAULT_MAX_TOKENS2)),
885
+ signal: request.signal
886
+ },
887
+ this.#retry
888
+ );
889
+ if (!response.ok) {
890
+ throw await apiError("google", response);
891
+ }
892
+ const data = await response.json();
893
+ if (isRecord(data) && isRecord(data.error)) {
894
+ throw apiErrorFromBody("google", response.status, data);
895
+ }
896
+ const rawFinishReason = extractFinishReason2(data);
897
+ const text = extractText2(data);
898
+ const toolCalls = extractToolCalls2(data);
899
+ const finishReason = normalizeFinishReason2(rawFinishReason);
900
+ return {
901
+ text,
902
+ model: extractModel(data, model, "modelVersion"),
903
+ // Gemini reports `STOP` even when it emits a function call, so surface
904
+ // `tool_use` — but only on a clean stop. A real `MAX_TOKENS`/`SAFETY` stop
905
+ // (e.g. truncated args) must not be masked, so keep its normalized reason.
906
+ finishReason: toolCalls !== void 0 && (finishReason === "stop" || finishReason === void 0) ? "tool_use" : finishReason,
907
+ rawFinishReason,
908
+ usage: extractUsage2(data),
909
+ parsed: parseStructured(request, text),
910
+ toolCalls
911
+ };
912
+ }
913
+ async *stream(request) {
914
+ const model = request.model ?? this.#model;
915
+ const response = await requestWithRetry(
916
+ "google",
917
+ this.#url(model, true),
918
+ {
919
+ method: "POST",
920
+ headers: this.#headers(),
921
+ body: JSON.stringify(
922
+ this.#buildBody(request, DEFAULT_STREAM_MAX_TOKENS2)
923
+ ),
924
+ signal: request.signal
925
+ },
926
+ this.#retry
927
+ );
928
+ if (!response.ok) {
929
+ throw await apiError("google", response);
930
+ }
931
+ if (!response.body) {
932
+ throw new Error("Google streaming response had no body");
933
+ }
934
+ for await (const event of sseJson(response.body)) {
935
+ if (isRecord(event.error)) {
936
+ throw new Error(`Google stream error: ${JSON.stringify(event)}`);
937
+ }
938
+ const text = extractText2(event);
939
+ if (text.length > 0) {
940
+ yield text;
941
+ }
942
+ }
943
+ }
944
+ /**
945
+ * Gemini puts the model and the action in the path (`:generateContent` vs
946
+ * `:streamGenerateContent`); streaming additionally needs `?alt=sse` to get an
947
+ * SSE body rather than a streamed JSON array.
948
+ */
949
+ #url(model, stream) {
950
+ const action = stream ? "streamGenerateContent?alt=sse" : "generateContent";
951
+ return `${this.#baseUrl}/v1beta/models/${model}:${action}`;
952
+ }
953
+ #headers() {
954
+ return {
955
+ "x-goog-api-key": this.#apiKey,
956
+ "content-type": "application/json"
957
+ };
958
+ }
959
+ #buildBody(request, defaultMaxTokens) {
960
+ const generationConfig = {
961
+ maxOutputTokens: request.maxTokens ?? defaultMaxTokens
962
+ };
963
+ if (request.responseFormat !== void 0) {
964
+ generationConfig.responseMimeType = "application/json";
965
+ generationConfig.responseSchema = toGeminiSchema(
966
+ request.responseFormat.schema
967
+ );
968
+ }
969
+ const body = {
970
+ contents: request.messages.map((message) => toGeminiContent(message)),
971
+ generationConfig
972
+ };
973
+ if (request.system !== void 0) {
974
+ body.systemInstruction = { parts: [{ text: request.system }] };
975
+ }
976
+ if (request.tools !== void 0) {
977
+ body.tools = [
978
+ {
979
+ functionDeclarations: request.tools.map((tool) => ({
980
+ name: tool.name,
981
+ description: tool.description,
982
+ parameters: toGeminiSchema(tool.parameters)
983
+ }))
984
+ }
985
+ ];
986
+ }
987
+ if (request.toolChoice !== void 0) {
988
+ body.toolConfig = {
989
+ functionCallingConfig: toGeminiFunctionCallingConfig(
990
+ request.toolChoice
991
+ )
992
+ };
993
+ }
994
+ return body;
995
+ }
996
+ };
997
+ function toGeminiFunctionCallingConfig(choice) {
998
+ if (typeof choice === "object") {
999
+ return { mode: "ANY", allowedFunctionNames: [choice.name] };
1000
+ }
1001
+ const mode = { auto: "AUTO", any: "ANY", none: "NONE" }[choice];
1002
+ return { mode };
1003
+ }
1004
+ function toGeminiSchema(schema) {
1005
+ if (Array.isArray(schema)) {
1006
+ return schema.map((entry) => toGeminiSchema(entry));
1007
+ }
1008
+ if (!isRecord(schema)) {
1009
+ return schema;
1010
+ }
1011
+ const out = {};
1012
+ for (const [key, value] of Object.entries(schema)) {
1013
+ if (key === "type" && typeof value === "string") {
1014
+ out[key] = value.toUpperCase();
1015
+ } else if (key === "properties" && isRecord(value)) {
1016
+ out[key] = Object.fromEntries(
1017
+ Object.entries(value).map(([name, propSchema]) => [
1018
+ name,
1019
+ toGeminiSchema(propSchema)
1020
+ ])
1021
+ );
1022
+ } else if (key === "items") {
1023
+ out[key] = toGeminiSchema(value);
1024
+ } else if (["anyOf", "allOf", "oneOf"].includes(key) && Array.isArray(value)) {
1025
+ out[key] = value.map((entry) => toGeminiSchema(entry));
1026
+ } else {
1027
+ out[key] = value;
1028
+ }
1029
+ }
1030
+ return out;
1031
+ }
1032
+ function toGeminiContent(message) {
1033
+ return {
1034
+ role: message.role === "assistant" ? "model" : "user",
1035
+ parts: typeof message.content === "string" ? [{ text: message.content }] : message.content.map((part) => toGeminiPart(part))
1036
+ };
1037
+ }
1038
+ function toGeminiPart(part) {
1039
+ switch (part.type) {
1040
+ case "text":
1041
+ return { text: part.text };
1042
+ case "image":
1043
+ case "file":
1044
+ return toGeminiMedia(part.source);
1045
+ case "tool_use":
1046
+ return {
1047
+ functionCall: {
1048
+ name: part.name,
1049
+ ...part.id === void 0 ? {} : { id: part.id },
1050
+ args: part.input
1051
+ }
1052
+ };
1053
+ case "tool_result":
1054
+ if (part.name === void 0) {
1055
+ throw new Error(
1056
+ "Gemini requires the tool name on each tool result; set ToolResultPart.name to the called tool's name."
1057
+ );
1058
+ }
1059
+ return {
1060
+ functionResponse: {
1061
+ name: part.name,
1062
+ ...part.toolUseId === void 0 ? {} : { id: part.toolUseId },
1063
+ response: { result: part.content }
1064
+ }
1065
+ };
1066
+ }
1067
+ }
1068
+ function toGeminiMedia(source) {
1069
+ if (source.kind === "base64") {
1070
+ return { inlineData: { mimeType: source.mediaType, data: source.data } };
1071
+ }
1072
+ return {
1073
+ fileData: source.mediaType === void 0 ? { fileUri: source.url } : { mimeType: source.mediaType, fileUri: source.url }
1074
+ };
1075
+ }
1076
+ function toArray2(value) {
1077
+ return Array.isArray(value) ? value : [];
1078
+ }
1079
+ function firstCandidate(data) {
1080
+ const candidates = isRecord(data) ? toArray2(data.candidates) : [];
1081
+ const first = candidates[0];
1082
+ return isRecord(first) ? first : void 0;
1083
+ }
1084
+ function firstCandidateParts(data) {
1085
+ const content = firstCandidate(data)?.content;
1086
+ return isRecord(content) ? toArray2(content.parts) : [];
1087
+ }
1088
+ function extractText2(data) {
1089
+ let text = "";
1090
+ for (const part of firstCandidateParts(data)) {
1091
+ if (isRecord(part) && typeof part.text === "string") {
1092
+ text += part.text;
1093
+ }
1094
+ }
1095
+ return text;
1096
+ }
1097
+ function extractToolCalls2(data) {
1098
+ const parts = firstCandidateParts(data);
1099
+ const calls = [];
1100
+ for (const part of parts) {
1101
+ const call = isRecord(part) ? part.functionCall : void 0;
1102
+ if (isRecord(call) && typeof call.name === "string") {
1103
+ calls.push({
1104
+ id: typeof call.id === "string" ? call.id : void 0,
1105
+ name: call.name,
1106
+ input: isRecord(call.args) ? call.args : {}
1107
+ });
1108
+ }
1109
+ }
1110
+ return calls.length > 0 ? calls : void 0;
1111
+ }
1112
+ function extractFinishReason2(data) {
1113
+ const first = firstCandidate(data);
1114
+ if (first !== void 0 && typeof first.finishReason === "string") {
1115
+ return first.finishReason;
1116
+ }
1117
+ const feedback = isRecord(data) ? data.promptFeedback : void 0;
1118
+ if (isRecord(feedback) && typeof feedback.blockReason === "string") {
1119
+ return feedback.blockReason;
1120
+ }
1121
+ return void 0;
1122
+ }
1123
+ function extractUsage2(data) {
1124
+ const usage = isRecord(data) ? data.usageMetadata : void 0;
1125
+ if (!isRecord(usage)) {
1126
+ return void 0;
1127
+ }
1128
+ const inputTokens = typeof usage.promptTokenCount === "number" ? usage.promptTokenCount : 0;
1129
+ const outputTokens = typeof usage.candidatesTokenCount === "number" ? usage.candidatesTokenCount : 0;
1130
+ const totalTokens = typeof usage.totalTokenCount === "number" ? usage.totalTokenCount : inputTokens + outputTokens;
1131
+ return { inputTokens, outputTokens, totalTokens };
1132
+ }
1133
+ function normalizeFinishReason2(raw) {
1134
+ switch (raw) {
1135
+ case void 0:
1136
+ return void 0;
1137
+ case "STOP":
1138
+ return "stop";
1139
+ case "MAX_TOKENS":
1140
+ return "length";
1141
+ case "SAFETY":
1142
+ case "RECITATION":
1143
+ case "BLOCKLIST":
1144
+ case "PROHIBITED_CONTENT":
1145
+ case "SPII":
1146
+ return "content_filter";
1147
+ default:
1148
+ return "other";
1149
+ }
1150
+ }
1151
+
1152
+ // src/providers/openai.ts
1153
+ var DEFAULT_MODEL3 = "gpt-4.1";
1154
+ var DEFAULT_BASE_URL3 = "https://api.openai.com";
1155
+ var DEFAULT_MAX_TOKENS3 = 16e3;
1156
+ var DEFAULT_STREAM_MAX_TOKENS3 = 64e3;
1157
+ var OpenAIProvider = class {
1158
+ name;
1159
+ #apiKey;
1160
+ #model;
1161
+ #baseUrl;
1162
+ #extraHeaders;
1163
+ #retry;
1164
+ /**
1165
+ * `name` is the provider's identity, used as `this.name` and in error
1166
+ * attribution (`ProviderError.provider`). It defaults to `"openai"`; the
1167
+ * registry passes a custom name for an OpenAI-compatible gateway so its errors
1168
+ * aren't mislabeled `"openai"`. It's a constructor argument rather than a
1169
+ * public option because only the registry sets it — it isn't user config.
1170
+ */
1171
+ constructor(options, name = "openai") {
1172
+ this.name = name;
1173
+ this.#apiKey = options.apiKey;
1174
+ this.#model = options.model ?? DEFAULT_MODEL3;
1175
+ this.#baseUrl = options.baseUrl ?? DEFAULT_BASE_URL3;
1176
+ this.#extraHeaders = options.headers;
1177
+ this.#retry = options.retry;
1178
+ }
1179
+ async complete(request) {
1180
+ const model = request.model ?? this.#model;
1181
+ const response = await requestWithRetry(
1182
+ this.name,
1183
+ `${this.#baseUrl}/v1/chat/completions`,
1184
+ {
1185
+ method: "POST",
1186
+ headers: this.#headers(),
1187
+ body: JSON.stringify(
1188
+ this.#buildBody(request, model, DEFAULT_MAX_TOKENS3, false)
1189
+ ),
1190
+ signal: request.signal
1191
+ },
1192
+ this.#retry
1193
+ );
1194
+ if (!response.ok) {
1195
+ throw await apiError(this.name, response);
1196
+ }
1197
+ const data = await response.json();
1198
+ if (isRecord(data) && isRecord(data.error)) {
1199
+ throw apiErrorFromBody(this.name, response.status, data);
1200
+ }
1201
+ const rawFinishReason = extractFinishReason3(data);
1202
+ const refusal = extractRefusal2(data);
1203
+ const text = extractText3(data);
1204
+ return {
1205
+ text,
1206
+ model: extractModel(data, model),
1207
+ // A refusal is a content-filter outcome even though OpenAI still reports
1208
+ // finish_reason: "stop", so surface it as such regardless of the raw value.
1209
+ finishReason: refusal === void 0 ? normalizeFinishReason3(rawFinishReason) : "content_filter",
1210
+ rawFinishReason,
1211
+ refusal,
1212
+ usage: extractUsage3(data),
1213
+ parsed: parseStructured(request, text),
1214
+ toolCalls: extractToolCalls3(data)
1215
+ };
1216
+ }
1217
+ async *stream(request) {
1218
+ const model = request.model ?? this.#model;
1219
+ const response = await requestWithRetry(
1220
+ this.name,
1221
+ `${this.#baseUrl}/v1/chat/completions`,
1222
+ {
1223
+ method: "POST",
1224
+ headers: this.#headers(),
1225
+ body: JSON.stringify(
1226
+ this.#buildBody(request, model, DEFAULT_STREAM_MAX_TOKENS3, true)
1227
+ ),
1228
+ signal: request.signal
1229
+ },
1230
+ this.#retry
1231
+ );
1232
+ if (!response.ok) {
1233
+ throw await apiError(this.name, response);
1234
+ }
1235
+ if (!response.body) {
1236
+ throw new Error("OpenAI streaming response had no body");
1237
+ }
1238
+ for await (const event of sseJson(response.body)) {
1239
+ if (isRecord(event.error)) {
1240
+ throw new Error(`OpenAI stream error: ${JSON.stringify(event)}`);
1241
+ }
1242
+ const delta = firstChoiceDelta(event);
1243
+ if (isRecord(delta) && typeof delta.content === "string") {
1244
+ yield delta.content;
1245
+ }
1246
+ }
1247
+ }
1248
+ #headers() {
1249
+ return {
1250
+ authorization: `Bearer ${this.#apiKey}`,
1251
+ "content-type": "application/json",
1252
+ ...this.#extraHeaders
1253
+ };
1254
+ }
1255
+ #buildBody(request, model, defaultMaxTokens, stream) {
1256
+ const body = {
1257
+ model,
1258
+ max_completion_tokens: request.maxTokens ?? defaultMaxTokens,
1259
+ messages: toOpenAIMessages(request)
1260
+ };
1261
+ if (request.responseFormat !== void 0) {
1262
+ body.response_format = {
1263
+ type: "json_schema",
1264
+ json_schema: {
1265
+ name: request.responseFormat.name ?? "response",
1266
+ strict: true,
1267
+ schema: request.responseFormat.schema
1268
+ }
1269
+ };
1270
+ }
1271
+ if (request.tools !== void 0) {
1272
+ body.tools = request.tools.map((tool) => ({
1273
+ type: "function",
1274
+ function: {
1275
+ name: tool.name,
1276
+ description: tool.description,
1277
+ parameters: tool.parameters
1278
+ }
1279
+ }));
1280
+ }
1281
+ if (request.toolChoice !== void 0) {
1282
+ body.tool_choice = toOpenAIToolChoice(request.toolChoice);
1283
+ }
1284
+ if (stream) {
1285
+ body.stream = true;
1286
+ }
1287
+ return body;
1288
+ }
1289
+ };
1290
+ function toOpenAIToolChoice(choice) {
1291
+ if (typeof choice === "object") {
1292
+ return { type: "function", function: { name: choice.name } };
1293
+ }
1294
+ return choice === "any" ? "required" : choice;
1295
+ }
1296
+ function toOpenAIMessages(request) {
1297
+ const messages = request.messages.flatMap(
1298
+ (message) => toOpenAIWireMessages(message)
1299
+ );
1300
+ if (request.system !== void 0) {
1301
+ messages.unshift({ role: "system", content: request.system });
1302
+ }
1303
+ return messages;
1304
+ }
1305
+ function toOpenAIWireMessages(message) {
1306
+ if (typeof message.content === "string") {
1307
+ return [{ role: message.role, content: message.content }];
1308
+ }
1309
+ const toolUses = message.content.filter((p) => p.type === "tool_use");
1310
+ const toolResults = message.content.filter((p) => p.type === "tool_result");
1311
+ const rest = message.content.filter(
1312
+ (p) => ["text", "image", "file"].includes(p.type)
1313
+ );
1314
+ if (toolUses.length > 0) {
1315
+ return [
1316
+ {
1317
+ role: message.role,
1318
+ content: rest.length > 0 ? rest.map((p) => toOpenAIPart(p)) : null,
1319
+ tool_calls: toolUses.map((u) => {
1320
+ if (u.id === void 0) {
1321
+ throw new Error(
1322
+ "OpenAI requires an id on each tool call; replay the ToolCall.id you received from the model."
1323
+ );
1324
+ }
1325
+ return {
1326
+ id: u.id,
1327
+ type: "function",
1328
+ function: { name: u.name, arguments: JSON.stringify(u.input) }
1329
+ };
1330
+ })
1331
+ }
1332
+ ];
1333
+ }
1334
+ const out = toolResults.map((r) => {
1335
+ if (r.toolUseId === void 0) {
1336
+ throw new Error(
1337
+ "OpenAI requires toolUseId on each tool result; set it to the id of the ToolCall you are answering."
1338
+ );
1339
+ }
1340
+ return { role: "tool", tool_call_id: r.toolUseId, content: r.content };
1341
+ });
1342
+ if (rest.length > 0) {
1343
+ out.push({ role: message.role, content: rest.map((p) => toOpenAIPart(p)) });
1344
+ }
1345
+ return out;
1346
+ }
1347
+ function toOpenAIPart(part) {
1348
+ switch (part.type) {
1349
+ case "text":
1350
+ return { type: "text", text: part.text };
1351
+ case "image":
1352
+ return {
1353
+ type: "image_url",
1354
+ image_url: { url: toOpenAIUrl(part.source) }
1355
+ };
1356
+ case "file":
1357
+ return { type: "file", file: toOpenAIFile(part) };
1358
+ }
1359
+ }
1360
+ function toOpenAIUrl(source) {
1361
+ return source.kind === "base64" ? `data:${source.mediaType};base64,${source.data}` : source.url;
1362
+ }
1363
+ function toOpenAIFile(part) {
1364
+ if (part.source.kind !== "base64") {
1365
+ throw new Error(
1366
+ "OpenAI (Chat Completions) does not support a URL file source; use a base64 source."
1367
+ );
1368
+ }
1369
+ const file_data = `data:${part.source.mediaType};base64,${part.source.data}`;
1370
+ return part.filename === void 0 ? { file_data } : { filename: part.filename, file_data };
1371
+ }
1372
+ function toArray3(value) {
1373
+ return Array.isArray(value) ? value : [];
1374
+ }
1375
+ function firstChoice(data) {
1376
+ const choices = isRecord(data) ? toArray3(data.choices) : [];
1377
+ const first = choices[0];
1378
+ return isRecord(first) ? first : void 0;
1379
+ }
1380
+ function extractText3(data) {
1381
+ const message = firstChoice(data)?.message;
1382
+ if (isRecord(message) && typeof message.content === "string") {
1383
+ return message.content;
1384
+ }
1385
+ return "";
1386
+ }
1387
+ function firstChoiceDelta(data) {
1388
+ return firstChoice(data)?.delta;
1389
+ }
1390
+ function extractToolCalls3(data) {
1391
+ const message = firstChoice(data)?.message;
1392
+ const toolCalls = isRecord(message) ? toArray3(message.tool_calls) : [];
1393
+ const calls = [];
1394
+ for (const call of toolCalls) {
1395
+ if (!isRecord(call) || !isRecord(call.function)) {
1396
+ continue;
1397
+ }
1398
+ const fn = call.function;
1399
+ if (typeof fn.name !== "string") {
1400
+ continue;
1401
+ }
1402
+ calls.push({
1403
+ id: typeof call.id === "string" ? call.id : void 0,
1404
+ name: fn.name,
1405
+ input: parseArguments(fn.arguments)
1406
+ });
1407
+ }
1408
+ return calls.length > 0 ? calls : void 0;
1409
+ }
1410
+ function parseArguments(args) {
1411
+ if (typeof args !== "string") {
1412
+ return {};
1413
+ }
1414
+ try {
1415
+ const parsed = JSON.parse(args);
1416
+ return isRecord(parsed) ? parsed : {};
1417
+ } catch {
1418
+ return {};
1419
+ }
1420
+ }
1421
+ function extractFinishReason3(data) {
1422
+ const reason = firstChoice(data)?.finish_reason;
1423
+ return typeof reason === "string" ? reason : void 0;
1424
+ }
1425
+ function normalizeFinishReason3(raw) {
1426
+ switch (raw) {
1427
+ case void 0:
1428
+ return void 0;
1429
+ case "stop":
1430
+ return "stop";
1431
+ case "length":
1432
+ return "length";
1433
+ case "content_filter":
1434
+ return "content_filter";
1435
+ case "tool_calls":
1436
+ case "function_call":
1437
+ return "tool_use";
1438
+ default:
1439
+ return "other";
1440
+ }
1441
+ }
1442
+ function extractUsage3(data) {
1443
+ const usage = isRecord(data) ? data.usage : void 0;
1444
+ if (!isRecord(usage)) {
1445
+ return void 0;
1446
+ }
1447
+ const inputTokens = typeof usage.prompt_tokens === "number" ? usage.prompt_tokens : 0;
1448
+ const outputTokens = typeof usage.completion_tokens === "number" ? usage.completion_tokens : 0;
1449
+ const totalTokens = typeof usage.total_tokens === "number" ? usage.total_tokens : inputTokens + outputTokens;
1450
+ return { inputTokens, outputTokens, totalTokens };
1451
+ }
1452
+ function extractRefusal2(data) {
1453
+ const message = firstChoice(data)?.message;
1454
+ if (isRecord(message) && typeof message.refusal === "string" && message.refusal !== "") {
1455
+ return message.refusal;
1456
+ }
1457
+ return void 0;
1458
+ }
1459
+
1460
+ // src/registry.ts
1461
+ var BUILT_IN_NAMES = ["anthropic", "openai", "google"];
1462
+ var ProviderRegistry = class {
1463
+ #providers = /* @__PURE__ */ new Map();
1464
+ /**
1465
+ * Construct the providers present in `config`. A provider is registered only
1466
+ * if its entry is supplied; the rest are left out.
1467
+ */
1468
+ constructor(config) {
1469
+ if (config.anthropic) {
1470
+ this.#providers.set("anthropic", new AnthropicProvider(config.anthropic));
1471
+ }
1472
+ if (config.openai) {
1473
+ this.#providers.set("openai", new OpenAIProvider(config.openai));
1474
+ }
1475
+ if (config.google) {
1476
+ this.#providers.set("google", new GoogleProvider(config.google));
1477
+ }
1478
+ if (config.custom) {
1479
+ const builtInNames = BUILT_IN_NAMES;
1480
+ for (const [name, custom] of Object.entries(config.custom)) {
1481
+ if (builtInNames.includes(name)) {
1482
+ throw new Error(
1483
+ `Custom provider name "${name}" collides with a built-in; choose a different name.`
1484
+ );
1485
+ }
1486
+ this.#providers.set(name, constructCustom(name, custom));
1487
+ }
1488
+ }
1489
+ }
1490
+ /**
1491
+ * Return the provider registered under `name`. Throws a clear error listing
1492
+ * the configured names if that provider was not configured.
1493
+ */
1494
+ select(name) {
1495
+ const provider = this.#providers.get(name);
1496
+ if (provider === void 0) {
1497
+ throw new Error(
1498
+ `No provider "${name}" configured. Configured: ${this.#configuredList()}`
1499
+ );
1500
+ }
1501
+ return provider;
1502
+ }
1503
+ /**
1504
+ * Combine several configured providers to cooperate on one prompt, dispatching
1505
+ * on `request.strategy` (defaults to `"consensus"`).
1506
+ *
1507
+ * Generic over the strategy: `S` is inferred from the `strategy` field, so a
1508
+ * **literal** `strategy` at the call site makes the return that strategy's
1509
+ * concrete result (e.g. `strategy: "ensemble"` → `EnsembleResult`) — the caller
1510
+ * does **not** narrow a union. When `strategy` is only known at runtime, `S`
1511
+ * widens to {@link StrategyName} and the return is the full
1512
+ * {@link CombineResult} union to narrow.
1513
+ *
1514
+ * The request stays the broad {@link CombineRequest} here; for compile-time
1515
+ * enforcement of a strategy's specific options (e.g. `responseFormat` required
1516
+ * for ensemble) call the per-strategy method ({@link ProviderRegistry.consensus},
1517
+ * {@link ProviderRegistry.pipeline}, {@link ProviderRegistry.ensemble},
1518
+ * {@link ProviderRegistry.broadcast}), which takes that strategy's request type.
1519
+ */
1520
+ async combine(request, options) {
1521
+ const strategy = request.strategy ?? "consensus";
1522
+ const knownStrategies = STRATEGY_NAMES;
1523
+ if (!knownStrategies.includes(strategy)) {
1524
+ throw new Error(
1525
+ `Unknown combine strategy "${strategy}". Known: ${STRATEGY_NAMES.join(", ")}`
1526
+ );
1527
+ }
1528
+ let result;
1529
+ switch (strategy) {
1530
+ case "consensus":
1531
+ result = await this.consensus(request, options);
1532
+ break;
1533
+ case "pipeline":
1534
+ result = await this.pipeline(request, options);
1535
+ break;
1536
+ case "ensemble":
1537
+ result = await this.ensemble(request, options);
1538
+ break;
1539
+ case "broadcast":
1540
+ result = await this.broadcast(request, options);
1541
+ break;
1542
+ default: {
1543
+ const unreachable = strategy;
1544
+ throw new Error(`Unhandled combine strategy "${String(unreachable)}"`);
1545
+ }
1546
+ }
1547
+ return result;
1548
+ }
1549
+ /**
1550
+ * Run the `consensus` strategy (draft → critique → synthesize) over the
1551
+ * configured participants. Strategy-specific: `synthesizer` (defaults to the
1552
+ * first participant), `attribution`, `minParticipants` (default 2).
1553
+ */
1554
+ async consensus(request, options) {
1555
+ const { roster, ids, firstId } = this.#prepare(request);
1556
+ this.#rejectResponseFormat(request, "consensus");
1557
+ this.#validateConsensusOptions(request, ids);
1558
+ const synthesizer = request.synthesizer ?? firstId;
1559
+ return consensus(roster, synthesizer, request, options?.onEvent);
1560
+ }
1561
+ /**
1562
+ * Run the `pipeline` strategy (sequential refinement) — participants refine a
1563
+ * running answer in roster order; the last advancing stage wins.
1564
+ */
1565
+ async pipeline(request, options) {
1566
+ const { roster } = this.#prepare(request);
1567
+ this.#rejectResponseFormat(request, "pipeline");
1568
+ return pipeline(roster, request, options?.onEvent);
1569
+ }
1570
+ /**
1571
+ * Run the `ensemble` strategy (multi-model vote on structured output) — every
1572
+ * participant answers under `request.responseFormat`; the typed objects are
1573
+ * merged field-wise by majority vote with a per-field agreement score.
1574
+ */
1575
+ async ensemble(request, options) {
1576
+ const { roster } = this.#prepare(request);
1577
+ const { responseFormat } = request;
1578
+ if (responseFormat === void 0) {
1579
+ throw new Error(
1580
+ 'The "ensemble" strategy requires a responseFormat (the JSON Schema every participant answers under).'
1581
+ );
1582
+ }
1583
+ const rootType = responseFormat.schema.type;
1584
+ if (typeof rootType === "string" && rootType !== "object") {
1585
+ throw new Error(
1586
+ `The "ensemble" strategy requires an object schema (its field-wise vote needs named fields); got a "${rootType}" schema.`
1587
+ );
1588
+ }
1589
+ return ensemble(roster, request, options?.onEvent);
1590
+ }
1591
+ /**
1592
+ * Run the `broadcast` strategy (fan-out, no combine) — every participant
1593
+ * answers the raw prompt in parallel; all raw responses are returned.
1594
+ */
1595
+ async broadcast(request, options) {
1596
+ const { roster } = this.#prepare(request);
1597
+ this.#rejectResponseFormat(request, "broadcast");
1598
+ return broadcast(roster, request, options?.onEvent);
1599
+ }
1600
+ /**
1601
+ * Shared combine validation: normalize the participant specs, enforce ≥1
1602
+ * participant with unique ids, require ≥1 message, and reject tool calling
1603
+ * (no strategy supports it). Returns the resolved roster, the participant ids,
1604
+ * and the first participant's id (the default consensus synthesizer).
1605
+ */
1606
+ #prepare(request) {
1607
+ const normalized = request.participants.map(
1608
+ (spec) => normalizeParticipant(spec)
1609
+ );
1610
+ const [first] = normalized;
1611
+ if (first === void 0) {
1612
+ throw new Error("combine requires at least one participant");
1613
+ }
1614
+ const ids = normalized.map((p) => p.id);
1615
+ if (new Set(ids).size !== ids.length) {
1616
+ throw new Error(
1617
+ `combine participant labels must be unique: ${ids.join(", ")}. Set a distinct \`label\` when two participants share a provider and model.`
1618
+ );
1619
+ }
1620
+ if (request.messages.length === 0) {
1621
+ throw new Error("combine requires at least one message");
1622
+ }
1623
+ if (request.tools !== void 0 || request.toolChoice !== void 0) {
1624
+ throw new Error(
1625
+ "combine does not support tool calling (tools/toolChoice); use registry.select() for a single-provider tool loop."
1626
+ );
1627
+ }
1628
+ const roster = normalized.map((p) => ({
1629
+ ...p,
1630
+ provider: this.select(p.providerName)
1631
+ }));
1632
+ return { roster, ids, firstId: first.id };
1633
+ }
1634
+ /**
1635
+ * Reject `responseFormat` on a non-ensemble strategy. It only means something
1636
+ * for ensemble (where every participant answers under the schema); on the prose
1637
+ * strategies it would be silently forwarded, so reject it loudly instead.
1638
+ */
1639
+ #rejectResponseFormat(request, strategy) {
1640
+ if (request.responseFormat !== void 0) {
1641
+ throw new Error(
1642
+ `responseFormat is only supported by the "ensemble" strategy, not "${strategy}".`
1643
+ );
1644
+ }
1645
+ }
1646
+ /** Validate the consensus-only request options (`minParticipants`, `synthesizer`). */
1647
+ #validateConsensusOptions(request, ids) {
1648
+ const { minParticipants } = request;
1649
+ if (minParticipants !== void 0) {
1650
+ if (!Number.isInteger(minParticipants) || minParticipants < 1) {
1651
+ throw new Error("combine minParticipants must be a positive integer");
1652
+ }
1653
+ if (minParticipants > ids.length) {
1654
+ throw new Error(
1655
+ `combine minParticipants (${String(minParticipants)}) cannot exceed the number of participants (${String(ids.length)})`
1656
+ );
1657
+ }
1658
+ }
1659
+ if (request.synthesizer !== void 0 && !ids.includes(request.synthesizer)) {
1660
+ throw new Error(
1661
+ `Synthesizer "${request.synthesizer}" must be one of the participants: ${ids.join(", ")}`
1662
+ );
1663
+ }
1664
+ }
1665
+ /** Whether a provider is configured under `name`. */
1666
+ has(name) {
1667
+ return this.#providers.has(name);
1668
+ }
1669
+ /** The names of all configured providers. */
1670
+ names() {
1671
+ return [...this.#providers.keys()];
1672
+ }
1673
+ #configuredList() {
1674
+ const names = this.names();
1675
+ return names.length > 0 ? names.join(", ") : "(none)";
1676
+ }
1677
+ };
1678
+ function normalizeParticipant(spec) {
1679
+ if (typeof spec === "string") {
1680
+ return { id: spec, providerName: spec };
1681
+ }
1682
+ if (spec.model?.trim() === "") {
1683
+ throw new Error(
1684
+ `combine participant for "${spec.provider}" has an empty model; omit \`model\` to use the default.`
1685
+ );
1686
+ }
1687
+ if (spec.maxTokens !== void 0 && (!Number.isInteger(spec.maxTokens) || spec.maxTokens < 1)) {
1688
+ throw new Error(
1689
+ `combine participant for "${spec.provider}" has an invalid maxTokens (${String(spec.maxTokens)}); must be a positive integer.`
1690
+ );
1691
+ }
1692
+ const id = spec.label ?? (spec.model === void 0 ? spec.provider : `${spec.provider}-${spec.model}`);
1693
+ return {
1694
+ id,
1695
+ providerName: spec.provider,
1696
+ model: spec.model,
1697
+ maxTokens: spec.maxTokens
1698
+ };
1699
+ }
1700
+ function constructCustom(name, custom) {
1701
+ switch (custom.kind) {
1702
+ case "openai-compatible":
1703
+ return new OpenAIProvider(
1704
+ {
1705
+ apiKey: custom.apiKey,
1706
+ baseUrl: custom.baseUrl,
1707
+ model: custom.model,
1708
+ headers: custom.headers,
1709
+ retry: custom.retry
1710
+ },
1711
+ name
1712
+ );
1713
+ case "provider":
1714
+ return custom.provider;
1715
+ default: {
1716
+ const unreachable = custom;
1717
+ throw new Error(
1718
+ `Unhandled custom provider kind: ${JSON.stringify(unreachable)}`
1719
+ );
1720
+ }
1721
+ }
1722
+ }
1723
+
1724
+ // src/errors.ts
1725
+ var ProviderError = class extends Error {
1726
+ name = "ProviderError";
1727
+ /** Which provider produced the failure. */
1728
+ provider;
1729
+ /** Transport failure (no response) vs API failure (error response). */
1730
+ kind;
1731
+ /** HTTP status for `kind: "api"`; `undefined` for transport failures. */
1732
+ status;
1733
+ /** Machine-readable code parsed from the error body, where the provider sends one. */
1734
+ code;
1735
+ /** Error category parsed from the body (Anthropic/OpenAI `type`, Gemini `status`). */
1736
+ type;
1737
+ /** The raw response body, for `kind: "api"`. */
1738
+ body;
1739
+ constructor(message, init) {
1740
+ super(
1741
+ message,
1742
+ init.cause === void 0 ? void 0 : { cause: init.cause }
1743
+ );
1744
+ this.provider = init.provider;
1745
+ this.kind = init.kind;
1746
+ this.status = init.status;
1747
+ this.code = init.code;
1748
+ this.type = init.type;
1749
+ this.body = init.body;
1750
+ }
1751
+ };
1752
+ async function apiError(provider, response) {
1753
+ const body = await response.text();
1754
+ const { code, type } = parseErrorBody(body);
1755
+ return new ProviderError(
1756
+ `${provider} request failed (${String(response.status)}): ${body}`,
1757
+ { provider, kind: "api", status: response.status, code, type, body }
1758
+ );
1759
+ }
1760
+ function apiErrorFromBody(provider, status, data) {
1761
+ const body = JSON.stringify(data);
1762
+ const { code, type } = parseErrorFields(data);
1763
+ return new ProviderError(
1764
+ `${provider} request failed (${String(status)}): ${body}`,
1765
+ { provider, kind: "api", status, code, type, body }
1766
+ );
1767
+ }
1768
+ function aggregateError(message, causes) {
1769
+ return causes.length > 0 ? new AggregateError(causes, message) : new Error(message);
1770
+ }
1771
+ function parseErrorBody(body) {
1772
+ let parsed;
1773
+ try {
1774
+ parsed = JSON.parse(body);
1775
+ } catch {
1776
+ return {};
1777
+ }
1778
+ return parseErrorFields(parsed);
1779
+ }
1780
+ function parseErrorFields(parsed) {
1781
+ if (!isRecord2(parsed)) {
1782
+ return {};
1783
+ }
1784
+ const error = isRecord2(parsed.error) ? parsed.error : parsed;
1785
+ const code = typeof error.code === "string" ? error.code : void 0;
1786
+ const type = typeof error.type === "string" ? error.type : typeof error.status === "string" ? error.status : void 0;
1787
+ return { code, type };
1788
+ }
1789
+ function isRecord2(value) {
1790
+ return typeof value === "object" && value !== null;
1791
+ }
1792
+
1793
+ exports.ProviderError = ProviderError;
1794
+ exports.ProviderRegistry = ProviderRegistry;
1795
+ //# sourceMappingURL=index.cjs.map
1796
+ //# sourceMappingURL=index.cjs.map