@elsium-ai/gateway 0.1.6

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.js ADDED
@@ -0,0 +1,2591 @@
1
+ // @bun
2
+ // ../core/src/errors.ts
3
+ class ElsiumError extends Error {
4
+ code;
5
+ provider;
6
+ model;
7
+ statusCode;
8
+ retryable;
9
+ retryAfterMs;
10
+ cause;
11
+ metadata;
12
+ constructor(details) {
13
+ super(details.message);
14
+ this.name = "ElsiumError";
15
+ this.code = details.code;
16
+ this.provider = details.provider;
17
+ this.model = details.model;
18
+ this.statusCode = details.statusCode;
19
+ this.retryable = details.retryable;
20
+ this.retryAfterMs = details.retryAfterMs;
21
+ this.cause = details.cause;
22
+ this.metadata = details.metadata;
23
+ }
24
+ toJSON() {
25
+ return {
26
+ name: this.name,
27
+ code: this.code,
28
+ message: this.message,
29
+ provider: this.provider,
30
+ model: this.model,
31
+ statusCode: this.statusCode,
32
+ retryable: this.retryable,
33
+ retryAfterMs: this.retryAfterMs,
34
+ metadata: this.metadata
35
+ };
36
+ }
37
+ static providerError(message, opts) {
38
+ return new ElsiumError({
39
+ code: "PROVIDER_ERROR",
40
+ message,
41
+ provider: opts.provider,
42
+ statusCode: opts.statusCode,
43
+ retryable: opts.retryable ?? false,
44
+ cause: opts.cause
45
+ });
46
+ }
47
+ static rateLimit(provider, retryAfterMs) {
48
+ return new ElsiumError({
49
+ code: "RATE_LIMIT",
50
+ message: `Rate limited by ${provider}`,
51
+ provider,
52
+ statusCode: 429,
53
+ retryable: true,
54
+ retryAfterMs
55
+ });
56
+ }
57
+ static authError(provider) {
58
+ return new ElsiumError({
59
+ code: "AUTH_ERROR",
60
+ message: `Authentication failed for ${provider}. Check your API key.`,
61
+ provider,
62
+ statusCode: 401,
63
+ retryable: false
64
+ });
65
+ }
66
+ static timeout(provider, timeoutMs) {
67
+ return new ElsiumError({
68
+ code: "TIMEOUT",
69
+ message: `Request to ${provider} timed out after ${timeoutMs}ms`,
70
+ provider,
71
+ retryable: true
72
+ });
73
+ }
74
+ static validation(message, metadata) {
75
+ return new ElsiumError({
76
+ code: "VALIDATION_ERROR",
77
+ message,
78
+ retryable: false,
79
+ metadata
80
+ });
81
+ }
82
+ static budgetExceeded(spent, budget) {
83
+ return new ElsiumError({
84
+ code: "BUDGET_EXCEEDED",
85
+ message: `Token budget exceeded: spent ${spent}, budget ${budget}`,
86
+ retryable: false,
87
+ metadata: { spent, budget }
88
+ });
89
+ }
90
+ }
91
+ // ../core/src/utils.ts
92
+ import { randomBytes } from "crypto";
93
+ function cryptoHex(bytes) {
94
+ return randomBytes(bytes).toString("hex");
95
+ }
96
+ function generateId(prefix = "els") {
97
+ const timestamp = Date.now().toString(36);
98
+ const random = cryptoHex(4);
99
+ return `${prefix}_${timestamp}_${random}`;
100
+ }
101
+ function generateTraceId() {
102
+ const timestamp = Date.now().toString(36);
103
+ const random = cryptoHex(6);
104
+ return `trc_${timestamp}_${random}`;
105
+ }
106
+ function extractText(content) {
107
+ if (typeof content === "string")
108
+ return content;
109
+ return content.filter((part) => part.type === "text" && part.text).map((part) => part.text).join("");
110
+ }
111
+ async function sleep(ms) {
112
+ return new Promise((resolve) => setTimeout(resolve, ms));
113
+ }
114
+ function getRetryDelay(error, attempt, baseDelayMs, maxDelayMs) {
115
+ if (error && typeof error === "object" && "retryAfterMs" in error && typeof error.retryAfterMs === "number") {
116
+ return error.retryAfterMs;
117
+ }
118
+ return Math.min(baseDelayMs * 2 ** attempt, maxDelayMs);
119
+ }
120
+ function retry(fn, options = {}) {
121
+ const {
122
+ maxRetries = 3,
123
+ baseDelayMs = 1000,
124
+ maxDelayMs = 30000,
125
+ shouldRetry = (error) => {
126
+ if (error && typeof error === "object" && "retryable" in error) {
127
+ return error.retryable === true;
128
+ }
129
+ return false;
130
+ }
131
+ } = options;
132
+ return (async () => {
133
+ let lastError;
134
+ for (let attempt = 0;attempt <= maxRetries; attempt++) {
135
+ try {
136
+ return await fn();
137
+ } catch (error) {
138
+ lastError = error;
139
+ if (attempt === maxRetries || !shouldRetry(error)) {
140
+ throw error;
141
+ }
142
+ const delay = getRetryDelay(error, attempt, baseDelayMs, maxDelayMs);
143
+ const jitter = delay * (0.5 + Math.random() * 0.5);
144
+ await sleep(jitter);
145
+ }
146
+ }
147
+ throw lastError;
148
+ })();
149
+ }
150
+
151
+ // ../core/src/stream.ts
152
+ function shouldEmitCheckpoint(lastCheckpointTime, intervalMs, textLength) {
153
+ const elapsed = Date.now() - lastCheckpointTime;
154
+ return elapsed >= intervalMs && textLength > 0;
155
+ }
156
+ function createCheckpoint(textAccumulator, eventIndex, now) {
157
+ return {
158
+ id: generateId("ckpt"),
159
+ timestamp: now,
160
+ text: textAccumulator,
161
+ tokensSoFar: Math.ceil(textAccumulator.length / 1.5),
162
+ eventIndex
163
+ };
164
+ }
165
+ function toError(err) {
166
+ return err instanceof Error ? err : new Error(String(err));
167
+ }
168
+ function* emitErrorEvent(err, textAccumulator, onPartialRecovery) {
169
+ const error = toError(err);
170
+ if (textAccumulator.length > 0) {
171
+ onPartialRecovery?.(textAccumulator, error);
172
+ yield { type: "recovery", partialText: textAccumulator, error };
173
+ } else {
174
+ yield { type: "error", error };
175
+ }
176
+ }
177
+
178
+ class ElsiumStream {
179
+ source;
180
+ iterating = false;
181
+ constructor(source) {
182
+ this.source = source;
183
+ }
184
+ async* [Symbol.asyncIterator]() {
185
+ if (this.iterating) {
186
+ throw new Error("ElsiumStream supports only a single consumer");
187
+ }
188
+ this.iterating = true;
189
+ yield* this.source;
190
+ }
191
+ text() {
192
+ const source = this.source;
193
+ return {
194
+ async* [Symbol.asyncIterator]() {
195
+ for await (const event of source) {
196
+ if (event.type === "text_delta") {
197
+ yield event.text;
198
+ }
199
+ }
200
+ }
201
+ };
202
+ }
203
+ async toText() {
204
+ const parts = [];
205
+ for await (const text of this.text()) {
206
+ parts.push(text);
207
+ }
208
+ return parts.join("");
209
+ }
210
+ async toTextWithTimeout(timeoutMs) {
211
+ const parts = [];
212
+ const deadline = Date.now() + timeoutMs;
213
+ const iterator = this.source[Symbol.asyncIterator]();
214
+ try {
215
+ while (true) {
216
+ const remaining = deadline - Date.now();
217
+ if (remaining <= 0)
218
+ break;
219
+ let timer;
220
+ const timeoutPromise = new Promise((resolve) => {
221
+ timer = setTimeout(() => resolve({ value: undefined, done: true }), remaining);
222
+ });
223
+ const result = await Promise.race([iterator.next(), timeoutPromise]);
224
+ if (timer !== undefined)
225
+ clearTimeout(timer);
226
+ if (result.done)
227
+ break;
228
+ const event = result.value;
229
+ if (event.type === "text_delta") {
230
+ parts.push(event.text);
231
+ }
232
+ }
233
+ } catch (err) {
234
+ if (parts.length === 0)
235
+ throw err;
236
+ } finally {
237
+ await iterator.return?.();
238
+ }
239
+ return parts.join("");
240
+ }
241
+ async toResponse() {
242
+ const parts = [];
243
+ let usage = null;
244
+ let stopReason = null;
245
+ for await (const event of this.source) {
246
+ switch (event.type) {
247
+ case "text_delta":
248
+ parts.push(event.text);
249
+ break;
250
+ case "message_end":
251
+ usage = event.usage;
252
+ stopReason = event.stopReason;
253
+ break;
254
+ }
255
+ }
256
+ return { text: parts.join(""), usage, stopReason };
257
+ }
258
+ pipe(transform) {
259
+ return new ElsiumStream(transform(this.source));
260
+ }
261
+ resilient(options = {}) {
262
+ const { checkpointIntervalMs = 1000, onCheckpoint, onPartialRecovery } = options;
263
+ const source = this.source;
264
+ const resilientSource = {
265
+ async* [Symbol.asyncIterator]() {
266
+ let lastCheckpointTime = Date.now();
267
+ let textAccumulator = "";
268
+ let eventIndex = 0;
269
+ try {
270
+ for await (const event of source) {
271
+ eventIndex++;
272
+ if (event.type === "text_delta") {
273
+ textAccumulator += event.text;
274
+ }
275
+ yield event;
276
+ if (shouldEmitCheckpoint(lastCheckpointTime, checkpointIntervalMs, textAccumulator.length)) {
277
+ const now = Date.now();
278
+ const checkpoint = createCheckpoint(textAccumulator, eventIndex, now);
279
+ onCheckpoint?.(checkpoint);
280
+ yield { type: "checkpoint", checkpoint };
281
+ lastCheckpointTime = now;
282
+ }
283
+ }
284
+ } catch (err) {
285
+ yield* emitErrorEvent(err, textAccumulator, onPartialRecovery);
286
+ }
287
+ }
288
+ };
289
+ return new ElsiumStream(resilientSource);
290
+ }
291
+ }
292
+ var MAX_BUFFER_SIZE = 1e4;
293
+ function createStream(executor) {
294
+ let resolve = null;
295
+ const buffer = [];
296
+ let done = false;
297
+ let error = null;
298
+ let dropped = 0;
299
+ const source = {
300
+ [Symbol.asyncIterator]() {
301
+ return {
302
+ next() {
303
+ if (buffer.length > 0) {
304
+ const value = buffer.shift();
305
+ return Promise.resolve({ value, done: false });
306
+ }
307
+ if (done) {
308
+ return Promise.resolve({ value: undefined, done: true });
309
+ }
310
+ if (error) {
311
+ return Promise.reject(error);
312
+ }
313
+ return new Promise((r) => {
314
+ resolve = r;
315
+ });
316
+ }
317
+ };
318
+ }
319
+ };
320
+ const emit = (event) => {
321
+ if (resolve) {
322
+ const r = resolve;
323
+ resolve = null;
324
+ r({ value: event, done: false });
325
+ } else {
326
+ if (buffer.length < MAX_BUFFER_SIZE) {
327
+ buffer.push(event);
328
+ } else {
329
+ dropped++;
330
+ }
331
+ }
332
+ };
333
+ executor(emit).then(() => {
334
+ if (dropped > 0) {
335
+ emit({
336
+ type: "error",
337
+ error: new Error(`Stream buffer overflow: ${dropped} events dropped`)
338
+ });
339
+ }
340
+ done = true;
341
+ if (resolve) {
342
+ const r = resolve;
343
+ resolve = null;
344
+ r({ value: undefined, done: true });
345
+ }
346
+ }).catch((e) => {
347
+ error = e instanceof Error ? e : new Error(String(e));
348
+ if (resolve) {
349
+ resolve({ value: { type: "error", error }, done: false });
350
+ resolve = null;
351
+ }
352
+ });
353
+ return new ElsiumStream(source);
354
+ }
355
+ // ../core/src/logger.ts
356
+ var LOG_LEVELS = {
357
+ debug: 0,
358
+ info: 1,
359
+ warn: 2,
360
+ error: 3
361
+ };
362
+ function createLogger(options = {}) {
363
+ const { level = "info", pretty = false, context = {} } = options;
364
+ const minLevel = LOG_LEVELS[level];
365
+ function log(logLevel, message, data) {
366
+ if (LOG_LEVELS[logLevel] < minLevel)
367
+ return;
368
+ const entry = {
369
+ ...context,
370
+ level: logLevel,
371
+ message,
372
+ timestamp: new Date().toISOString(),
373
+ ...data ? { data } : {}
374
+ };
375
+ const output = pretty ? JSON.stringify(entry, null, 2) : JSON.stringify(entry);
376
+ if (logLevel === "error") {
377
+ console.error(output);
378
+ } else if (logLevel === "warn") {
379
+ console.warn(output);
380
+ } else {
381
+ console.log(output);
382
+ }
383
+ }
384
+ return {
385
+ debug: (msg, data) => log("debug", msg, data),
386
+ info: (msg, data) => log("info", msg, data),
387
+ warn: (msg, data) => log("warn", msg, data),
388
+ error: (msg, data) => log("error", msg, data),
389
+ child(childContext) {
390
+ return createLogger({
391
+ level,
392
+ pretty,
393
+ context: { ...context, ...childContext }
394
+ });
395
+ }
396
+ };
397
+ }
398
+ // ../core/src/circuit-breaker.ts
399
+ function defaultShouldCount(error) {
400
+ if (error && typeof error === "object" && "retryable" in error) {
401
+ return error.retryable === true;
402
+ }
403
+ return true;
404
+ }
405
+ function createCircuitBreaker(config) {
406
+ const failureThreshold = config?.failureThreshold ?? 5;
407
+ const resetTimeoutMs = config?.resetTimeoutMs ?? 30000;
408
+ const halfOpenMaxAttempts = config?.halfOpenMaxAttempts ?? 3;
409
+ const windowMs = config?.windowMs ?? 60000;
410
+ if (failureThreshold < 1 || !Number.isFinite(failureThreshold)) {
411
+ throw new ElsiumError({
412
+ code: "CONFIG_ERROR",
413
+ message: "failureThreshold must be >= 1",
414
+ retryable: false
415
+ });
416
+ }
417
+ if (resetTimeoutMs < 0 || !Number.isFinite(resetTimeoutMs)) {
418
+ throw new ElsiumError({
419
+ code: "CONFIG_ERROR",
420
+ message: "resetTimeoutMs must be >= 0 and finite",
421
+ retryable: false
422
+ });
423
+ }
424
+ if (halfOpenMaxAttempts < 1 || !Number.isFinite(halfOpenMaxAttempts)) {
425
+ throw new ElsiumError({
426
+ code: "CONFIG_ERROR",
427
+ message: "halfOpenMaxAttempts must be >= 1",
428
+ retryable: false
429
+ });
430
+ }
431
+ if (windowMs < 0 || !Number.isFinite(windowMs)) {
432
+ throw new ElsiumError({
433
+ code: "CONFIG_ERROR",
434
+ message: "windowMs must be >= 0 and finite",
435
+ retryable: false
436
+ });
437
+ }
438
+ const onStateChange = config?.onStateChange;
439
+ const shouldCount = config?.shouldCount ?? defaultShouldCount;
440
+ let currentState = "closed";
441
+ let failureTimestamps = [];
442
+ let lastOpenedAt = 0;
443
+ let halfOpenAttempts = 0;
444
+ let halfOpenInFlight = 0;
445
+ function transition(to) {
446
+ if (currentState === to)
447
+ return;
448
+ const from = currentState;
449
+ currentState = to;
450
+ onStateChange?.(from, to);
451
+ }
452
+ function recordFailure() {
453
+ const now = Date.now();
454
+ failureTimestamps.push(now);
455
+ failureTimestamps = failureTimestamps.filter((t) => now - t < windowMs);
456
+ if (failureTimestamps.length >= failureThreshold) {
457
+ lastOpenedAt = now;
458
+ halfOpenAttempts = 0;
459
+ transition("open");
460
+ }
461
+ }
462
+ function recordSuccess() {
463
+ if (currentState === "half-open") {
464
+ failureTimestamps = [];
465
+ halfOpenAttempts = 0;
466
+ halfOpenInFlight = 0;
467
+ transition("closed");
468
+ }
469
+ }
470
+ return {
471
+ get state() {
472
+ if (currentState === "open" && Date.now() - lastOpenedAt >= resetTimeoutMs) {
473
+ transition("half-open");
474
+ }
475
+ return currentState;
476
+ },
477
+ get failureCount() {
478
+ const now = Date.now();
479
+ return failureTimestamps.filter((t) => now - t < windowMs).length;
480
+ },
481
+ async execute(fn) {
482
+ const state = this.state;
483
+ if (state === "open") {
484
+ throw new ElsiumError({
485
+ code: "PROVIDER_ERROR",
486
+ message: "Circuit breaker is open",
487
+ retryable: true
488
+ });
489
+ }
490
+ if (state === "half-open" && halfOpenInFlight >= halfOpenMaxAttempts) {
491
+ lastOpenedAt = Date.now();
492
+ transition("open");
493
+ throw new ElsiumError({
494
+ code: "PROVIDER_ERROR",
495
+ message: "Circuit breaker is open",
496
+ retryable: true
497
+ });
498
+ }
499
+ if (state === "half-open") {
500
+ halfOpenAttempts++;
501
+ halfOpenInFlight++;
502
+ }
503
+ try {
504
+ const result = await fn();
505
+ recordSuccess();
506
+ return result;
507
+ } catch (error) {
508
+ if (shouldCount(error)) {
509
+ recordFailure();
510
+ }
511
+ throw error;
512
+ } finally {
513
+ if (state === "half-open") {
514
+ halfOpenInFlight = Math.max(0, halfOpenInFlight - 1);
515
+ }
516
+ }
517
+ },
518
+ reset() {
519
+ failureTimestamps = [];
520
+ halfOpenAttempts = 0;
521
+ halfOpenInFlight = 0;
522
+ transition("closed");
523
+ }
524
+ };
525
+ }
526
+ // src/provider.ts
527
+ var providerRegistry = new Map;
528
+ var metadataRegistry = new Map;
529
+ function registerProvider(name, factory) {
530
+ providerRegistry.set(name, factory);
531
+ }
532
+ function getProviderFactory(name) {
533
+ return providerRegistry.get(name);
534
+ }
535
+ function listProviders() {
536
+ return Array.from(providerRegistry.keys());
537
+ }
538
+ function registerProviderMetadata(name, metadata) {
539
+ metadataRegistry.set(name, metadata);
540
+ }
541
+ function getProviderMetadata(name) {
542
+ return metadataRegistry.get(name);
543
+ }
544
+
545
+ // src/middleware.ts
546
+ function composeMiddleware(middlewares) {
547
+ return (ctx, finalNext) => {
548
+ let index = -1;
549
+ function dispatch(i) {
550
+ if (i <= index) {
551
+ return Promise.reject(new Error("Middleware next() called multiple times"));
552
+ }
553
+ index = i;
554
+ const fn = i < middlewares.length ? middlewares[i] : finalNext;
555
+ if (i === middlewares.length) {
556
+ return finalNext(ctx);
557
+ }
558
+ return fn(ctx, () => dispatch(i + 1));
559
+ }
560
+ return dispatch(0);
561
+ };
562
+ }
563
+ function loggingMiddleware(logger) {
564
+ const log = logger ?? createLogger({ level: "info" });
565
+ return async (ctx, next) => {
566
+ log.info("LLM request", {
567
+ provider: ctx.provider,
568
+ model: ctx.model,
569
+ traceId: ctx.traceId,
570
+ messageCount: ctx.request.messages.length
571
+ });
572
+ const response = await next(ctx);
573
+ log.info("LLM response", {
574
+ provider: ctx.provider,
575
+ model: ctx.model,
576
+ traceId: ctx.traceId,
577
+ latencyMs: response.latencyMs,
578
+ inputTokens: response.usage.inputTokens,
579
+ outputTokens: response.usage.outputTokens,
580
+ totalCost: response.cost.totalCost
581
+ });
582
+ return response;
583
+ };
584
+ }
585
+ function costTrackingMiddleware() {
586
+ let totalCost = 0;
587
+ let totalTokens = 0;
588
+ let callCount = 0;
589
+ const middleware = async (ctx, next) => {
590
+ const response = await next(ctx);
591
+ totalCost += response.cost.totalCost;
592
+ totalTokens += response.usage.totalTokens;
593
+ callCount++;
594
+ return response;
595
+ };
596
+ middleware.getTotalCost = () => totalCost;
597
+ middleware.getTotalTokens = () => totalTokens;
598
+ middleware.getCallCount = () => callCount;
599
+ middleware.reset = () => {
600
+ totalCost = 0;
601
+ totalTokens = 0;
602
+ callCount = 0;
603
+ };
604
+ return middleware;
605
+ }
606
+ var SENSITIVE_HEADERS = ["x-api-key", "authorization", "api-key"];
607
+ function redactHeaders(headers) {
608
+ const redacted = {};
609
+ for (const [key, value] of Object.entries(headers)) {
610
+ if (SENSITIVE_HEADERS.includes(key.toLowerCase())) {
611
+ redacted[key] = "[REDACTED]";
612
+ } else {
613
+ redacted[key] = value;
614
+ }
615
+ }
616
+ return redacted;
617
+ }
618
+ var PROVIDER_URLS = {
619
+ anthropic: "https://api.anthropic.com/v1/messages",
620
+ openai: "https://api.openai.com/v1/chat/completions",
621
+ google: "https://generativelanguage.googleapis.com/v1beta/models"
622
+ };
623
+ function buildProviderHeaders(provider, metadata) {
624
+ const headers = { "Content-Type": "application/json" };
625
+ const apiKey = metadata._apiKey ?? "***";
626
+ const providerMeta = getProviderMetadata(provider);
627
+ const authStyle = providerMeta?.authStyle;
628
+ if (authStyle === "x-api-key" || provider === "anthropic") {
629
+ headers["x-api-key"] = apiKey;
630
+ } else {
631
+ headers.Authorization = apiKey;
632
+ }
633
+ return redactHeaders(headers);
634
+ }
635
+ function truncateContent(content) {
636
+ if (typeof content !== "string")
637
+ return "[complex content]";
638
+ if (content.length > 200)
639
+ return `${content.slice(0, 200)}...`;
640
+ return content;
641
+ }
642
+ function buildXRayRequest(ctx) {
643
+ const providerMeta = getProviderMetadata(ctx.provider);
644
+ return {
645
+ url: PROVIDER_URLS[ctx.provider] ?? providerMeta?.baseUrl ?? `https://${ctx.provider}.api/v1/messages`,
646
+ method: "POST",
647
+ headers: buildProviderHeaders(ctx.provider, ctx.metadata),
648
+ body: {
649
+ model: ctx.model,
650
+ messages: ctx.request.messages.map((m) => ({
651
+ role: m.role,
652
+ content: truncateContent(m.content)
653
+ })),
654
+ max_tokens: ctx.request.maxTokens,
655
+ ...ctx.request.temperature !== undefined ? { temperature: ctx.request.temperature } : {},
656
+ ...ctx.request.tools?.length ? { tools: ctx.request.tools.map((t) => t.name) } : {}
657
+ }
658
+ };
659
+ }
660
+ function buildXRayResponse(response) {
661
+ return {
662
+ status: 200,
663
+ headers: { "content-type": "application/json" },
664
+ body: {
665
+ id: response.id,
666
+ model: response.model,
667
+ stop_reason: response.stopReason,
668
+ content_preview: truncateContent(response.message.content)
669
+ }
670
+ };
671
+ }
672
+ function buildXRayData(ctx, response, latencyMs) {
673
+ return {
674
+ traceId: ctx.traceId,
675
+ timestamp: Date.now(),
676
+ provider: ctx.provider,
677
+ model: ctx.model,
678
+ latencyMs,
679
+ request: buildXRayRequest(ctx),
680
+ response: buildXRayResponse(response),
681
+ usage: response.usage,
682
+ cost: response.cost
683
+ };
684
+ }
685
+ function xrayMiddleware(options = {}) {
686
+ const maxHistory = options.maxHistory ?? 100;
687
+ const history = [];
688
+ const middleware = async (ctx, next) => {
689
+ const startTime = performance.now();
690
+ const response = await next(ctx);
691
+ const latencyMs = Math.round(performance.now() - startTime);
692
+ const xrayData = buildXRayData(ctx, response, latencyMs);
693
+ history.unshift(xrayData);
694
+ if (history.length > maxHistory) {
695
+ history.length = maxHistory;
696
+ }
697
+ return response;
698
+ };
699
+ middleware.lastCall = () => history[0] ?? null;
700
+ middleware.callHistory = (limit = 10) => history.slice(0, limit);
701
+ middleware.getByTraceId = (traceId) => history.find((d) => d.traceId === traceId);
702
+ middleware.clear = () => {
703
+ history.length = 0;
704
+ };
705
+ return middleware;
706
+ }
707
+
708
+ // src/pricing.ts
709
+ var log = createLogger();
710
+ var PRICING = {
711
+ "claude-opus-4-6": { inputPerMillion: 15, outputPerMillion: 75 },
712
+ "claude-sonnet-4-6": { inputPerMillion: 3, outputPerMillion: 15 },
713
+ "claude-haiku-4-5-20251001": { inputPerMillion: 1, outputPerMillion: 5 },
714
+ "gpt-4o": { inputPerMillion: 2.5, outputPerMillion: 10 },
715
+ "gpt-4o-mini": { inputPerMillion: 0.15, outputPerMillion: 0.6 },
716
+ "gpt-4.1": { inputPerMillion: 2, outputPerMillion: 8 },
717
+ "gpt-4.1-mini": { inputPerMillion: 0.4, outputPerMillion: 1.6 },
718
+ "gpt-4.1-nano": { inputPerMillion: 0.1, outputPerMillion: 0.4 },
719
+ "gpt-5": { inputPerMillion: 1.25, outputPerMillion: 10 },
720
+ "gpt-5-mini": { inputPerMillion: 0.25, outputPerMillion: 2 },
721
+ "gpt-5-nano": { inputPerMillion: 0.05, outputPerMillion: 0.4 },
722
+ o1: { inputPerMillion: 15, outputPerMillion: 60 },
723
+ "o1-mini": { inputPerMillion: 1.1, outputPerMillion: 4.4 },
724
+ o3: { inputPerMillion: 2, outputPerMillion: 8 },
725
+ "o3-mini": { inputPerMillion: 1.1, outputPerMillion: 4.4 },
726
+ "o3-pro": { inputPerMillion: 20, outputPerMillion: 80 },
727
+ "o4-mini": { inputPerMillion: 1.1, outputPerMillion: 4.4 },
728
+ "gemini-2.0-flash": { inputPerMillion: 0.1, outputPerMillion: 0.4 },
729
+ "gemini-2.0-flash-lite": { inputPerMillion: 0.075, outputPerMillion: 0.3 },
730
+ "gemini-2.5-pro": { inputPerMillion: 1.25, outputPerMillion: 10 },
731
+ "gemini-2.5-pro-preview-05-06": { inputPerMillion: 1.25, outputPerMillion: 10 },
732
+ "gemini-2.5-flash": { inputPerMillion: 0.15, outputPerMillion: 0.6 },
733
+ "gemini-2.5-flash-preview-04-17": { inputPerMillion: 0.15, outputPerMillion: 0.6 },
734
+ "gemini-2.5-flash-lite": { inputPerMillion: 0.075, outputPerMillion: 0.3 }
735
+ };
736
+ function resolveModelName(model) {
737
+ if (PRICING[model])
738
+ return model;
739
+ const base = model.replace(/-\d{4}-\d{2}-\d{2}$/, "");
740
+ if (base !== model && PRICING[base])
741
+ return base;
742
+ return model;
743
+ }
744
+ function calculateCost(model, usage) {
745
+ const pricing = PRICING[resolveModelName(model)];
746
+ if (!pricing) {
747
+ log.warn(`Unknown model "${model}" \u2014 cost will be reported as $0. Register pricing with registerPricing().`);
748
+ return {
749
+ inputCost: 0,
750
+ outputCost: 0,
751
+ totalCost: 0,
752
+ currency: "USD"
753
+ };
754
+ }
755
+ const inputCost = usage.inputTokens / 1e6 * pricing.inputPerMillion;
756
+ const outputCost = usage.outputTokens / 1e6 * pricing.outputPerMillion;
757
+ return {
758
+ inputCost: Math.round(inputCost * 1e6) / 1e6,
759
+ outputCost: Math.round(outputCost * 1e6) / 1e6,
760
+ totalCost: Math.round((inputCost + outputCost) * 1e6) / 1e6,
761
+ currency: "USD"
762
+ };
763
+ }
764
+ function registerPricing(model, pricing) {
765
+ PRICING[model] = pricing;
766
+ }
767
+
768
+ // src/providers/anthropic.ts
769
+ var DEFAULT_BASE_URL = "https://api.anthropic.com";
770
+ var API_VERSION = "2023-06-01";
771
+ var DEFAULT_MAX_TOKENS = 4096;
772
+ function createAnthropicProvider(config) {
773
+ const { apiKey, baseUrl = DEFAULT_BASE_URL, timeout = 60000, maxRetries = 2 } = config;
774
+ async function request(path, body, signal) {
775
+ const url = `${baseUrl}/v1${path}`;
776
+ const response = await fetch(url, {
777
+ method: "POST",
778
+ headers: {
779
+ "Content-Type": "application/json",
780
+ "x-api-key": apiKey,
781
+ "anthropic-version": API_VERSION
782
+ },
783
+ body: JSON.stringify(body),
784
+ signal
785
+ });
786
+ if (!response.ok) {
787
+ const errorBody = await response.text().catch(() => "Unknown error");
788
+ if (response.status === 401)
789
+ throw ElsiumError.authError("anthropic");
790
+ if (response.status === 429) {
791
+ const retryAfter = response.headers.get("retry-after");
792
+ throw ElsiumError.rateLimit("anthropic", retryAfter ? Number.parseInt(retryAfter) * 1000 : undefined);
793
+ }
794
+ throw ElsiumError.providerError(`Anthropic API error ${response.status}: ${errorBody}`, {
795
+ provider: "anthropic",
796
+ statusCode: response.status,
797
+ retryable: response.status >= 500
798
+ });
799
+ }
800
+ return response;
801
+ }
802
+ function extractSystemText(msg) {
803
+ if (typeof msg.content === "string")
804
+ return msg.content;
805
+ return msg.content.filter((p) => p.type === "text").map((p) => p.text).join(`
806
+ `);
807
+ }
808
+ function formatToolResultMessage(msg) {
809
+ const blocks = (msg.toolResults ?? []).map((tr) => ({
810
+ type: "tool_result",
811
+ tool_use_id: tr.toolCallId,
812
+ content: tr.content
813
+ }));
814
+ return { role: "user", content: blocks };
815
+ }
816
+ function formatStringContent(msg, role) {
817
+ const blocks = [];
818
+ const text = msg.content;
819
+ if (text) {
820
+ blocks.push({ type: "text", text });
821
+ }
822
+ if (msg.toolCalls?.length) {
823
+ for (const tc of msg.toolCalls) {
824
+ blocks.push({
825
+ type: "tool_use",
826
+ id: tc.id,
827
+ name: tc.name,
828
+ input: tc.arguments
829
+ });
830
+ }
831
+ }
832
+ if (blocks.length === 0) {
833
+ return { role, content: text };
834
+ }
835
+ return { role, content: blocks };
836
+ }
837
+ function convertContentPart(part) {
838
+ if (part.type === "text")
839
+ return { type: "text", text: part.text };
840
+ if (part.type === "image" && part.source?.type === "base64") {
841
+ return {
842
+ type: "image",
843
+ source: {
844
+ type: "base64",
845
+ media_type: part.source.mediaType,
846
+ data: part.source.data
847
+ }
848
+ };
849
+ }
850
+ return { type: "text", text: "[unsupported content]" };
851
+ }
852
+ function formatMultipartContent(msg, role) {
853
+ const content = msg.content;
854
+ const blocks = content.map(convertContentPart);
855
+ return { role, content: blocks };
856
+ }
857
+ function formatMessages(messages) {
858
+ let system;
859
+ const formatted = [];
860
+ for (const msg of messages) {
861
+ if (msg.role === "system") {
862
+ system = extractSystemText(msg);
863
+ continue;
864
+ }
865
+ if (msg.role === "tool") {
866
+ formatted.push(formatToolResultMessage(msg));
867
+ continue;
868
+ }
869
+ const role = msg.role === "assistant" ? "assistant" : "user";
870
+ if (typeof msg.content === "string") {
871
+ formatted.push(formatStringContent(msg, role));
872
+ } else {
873
+ formatted.push(formatMultipartContent(msg, role));
874
+ }
875
+ }
876
+ return { system, messages: formatted };
877
+ }
878
+ function buildSeedMetadata(req) {
879
+ if (req.seed === undefined)
880
+ return {};
881
+ return { metadata: { ...req.metadata ?? {}, seed: req.seed } };
882
+ }
883
+ function formatTools(tools) {
884
+ if (!tools?.length)
885
+ return;
886
+ return tools.map((t) => ({
887
+ name: t.name,
888
+ description: t.description,
889
+ input_schema: t.inputSchema
890
+ }));
891
+ }
892
+ function extractContentBlocks(content) {
893
+ const toolCalls = [];
894
+ const textParts = [];
895
+ for (const block of content) {
896
+ if (block.type === "text" && block.text) {
897
+ textParts.push(block.text);
898
+ } else if (block.type === "tool_use" && block.id && block.name) {
899
+ toolCalls.push({
900
+ id: block.id,
901
+ name: block.name,
902
+ arguments: block.input ?? {}
903
+ });
904
+ }
905
+ }
906
+ return { textParts, toolCalls };
907
+ }
908
+ function parseResponse(raw, latencyMs) {
909
+ const traceId = generateTraceId();
910
+ const { textParts, toolCalls } = extractContentBlocks(raw.content);
911
+ const usage = {
912
+ inputTokens: raw.usage.input_tokens,
913
+ outputTokens: raw.usage.output_tokens,
914
+ totalTokens: raw.usage.input_tokens + raw.usage.output_tokens,
915
+ cacheReadTokens: raw.usage.cache_read_input_tokens,
916
+ cacheWriteTokens: raw.usage.cache_creation_input_tokens
917
+ };
918
+ return {
919
+ id: raw.id,
920
+ message: {
921
+ role: "assistant",
922
+ content: textParts.join(""),
923
+ toolCalls: toolCalls.length > 0 ? toolCalls : undefined
924
+ },
925
+ usage,
926
+ cost: calculateCost(raw.model, usage),
927
+ model: raw.model,
928
+ provider: "anthropic",
929
+ stopReason: mapAnthropicStopReason(raw.stop_reason),
930
+ latencyMs,
931
+ traceId
932
+ };
933
+ }
934
+ return {
935
+ name: "anthropic",
936
+ defaultModel: "claude-sonnet-4-6",
937
+ metadata: {
938
+ baseUrl: "https://api.anthropic.com/v1/messages",
939
+ capabilities: ["tools", "vision", "streaming", "system"],
940
+ authStyle: "x-api-key"
941
+ },
942
+ async complete(req) {
943
+ const { system, messages } = formatMessages(req.messages);
944
+ const model = req.model ?? "claude-sonnet-4-6";
945
+ const body = {
946
+ model,
947
+ messages,
948
+ max_tokens: req.maxTokens ?? DEFAULT_MAX_TOKENS,
949
+ ...system || req.system ? { system: req.system ?? system } : {},
950
+ ...req.temperature !== undefined ? { temperature: req.temperature } : {},
951
+ ...req.topP !== undefined ? { top_p: req.topP } : {},
952
+ ...req.stopSequences?.length ? { stop_sequences: req.stopSequences } : {},
953
+ ...buildSeedMetadata(req)
954
+ };
955
+ const tools = formatTools(req.tools);
956
+ if (tools)
957
+ body.tools = tools;
958
+ const startTime = performance.now();
959
+ const raw = await retry(async () => {
960
+ const controller = new AbortController;
961
+ const timer = setTimeout(() => controller.abort(), timeout);
962
+ try {
963
+ const signals = [controller.signal, req.signal].filter(Boolean);
964
+ const mergedSignal = signals.length > 1 ? AbortSignal.any(signals) : signals[0];
965
+ const resp = await request("/messages", body, mergedSignal);
966
+ return await resp.json();
967
+ } finally {
968
+ clearTimeout(timer);
969
+ }
970
+ }, {
971
+ maxRetries,
972
+ baseDelayMs: 1000,
973
+ shouldRetry: (e) => e instanceof ElsiumError && e.retryable
974
+ });
975
+ const latencyMs = Math.round(performance.now() - startTime);
976
+ return parseResponse(raw, latencyMs);
977
+ },
978
+ stream(req) {
979
+ const { system, messages } = formatMessages(req.messages);
980
+ const model = req.model ?? "claude-sonnet-4-6";
981
+ const body = {
982
+ model,
983
+ messages,
984
+ max_tokens: req.maxTokens ?? DEFAULT_MAX_TOKENS,
985
+ stream: true,
986
+ ...system || req.system ? { system: req.system ?? system } : {},
987
+ ...req.temperature !== undefined ? { temperature: req.temperature } : {},
988
+ ...req.topP !== undefined ? { top_p: req.topP } : {},
989
+ ...req.stopSequences?.length ? { stop_sequences: req.stopSequences } : {},
990
+ ...buildSeedMetadata(req)
991
+ };
992
+ const tools = formatTools(req.tools);
993
+ if (tools)
994
+ body.tools = tools;
995
+ return createStream(async (emit) => {
996
+ const controller = new AbortController;
997
+ const timer = setTimeout(() => controller.abort(), timeout);
998
+ try {
999
+ const signals = [controller.signal, req.signal].filter(Boolean);
1000
+ const mergedSignal = signals.length > 1 ? AbortSignal.any(signals) : signals[0];
1001
+ const resp = await request("/messages", body, mergedSignal);
1002
+ if (!resp.body)
1003
+ throw new ElsiumError({
1004
+ code: "STREAM_ERROR",
1005
+ message: "Response body is null",
1006
+ provider: "anthropic",
1007
+ retryable: false
1008
+ });
1009
+ await processAnthropicSSEStream(resp.body, model, emit);
1010
+ } finally {
1011
+ clearTimeout(timer);
1012
+ }
1013
+ });
1014
+ },
1015
+ async listModels() {
1016
+ return ["claude-opus-4-6", "claude-sonnet-4-6", "claude-haiku-4-5-20251001"];
1017
+ }
1018
+ };
1019
+ }
1020
+ function mapAnthropicStopReason(reason) {
1021
+ if (reason === "end_turn")
1022
+ return "end_turn";
1023
+ if (reason === "max_tokens")
1024
+ return "max_tokens";
1025
+ if (reason === "tool_use")
1026
+ return "tool_use";
1027
+ return "end_turn";
1028
+ }
1029
+ function processSSELine(line, model, emit) {
1030
+ if (!line.startsWith("data: "))
1031
+ return;
1032
+ const data = line.slice(6).trim();
1033
+ if (data === "[DONE]")
1034
+ return;
1035
+ try {
1036
+ const event = JSON.parse(data);
1037
+ const mapped = mapSSEEvent(event, model);
1038
+ if (mapped)
1039
+ emit(mapped);
1040
+ } catch (err2) {
1041
+ emit({ type: "error", error: err2 instanceof Error ? err2 : new Error(String(err2)) });
1042
+ }
1043
+ }
1044
+ async function processAnthropicSSEStream(body, model, emit) {
1045
+ const reader = body.getReader();
1046
+ const decoder = new TextDecoder;
1047
+ let buffer = "";
1048
+ while (true) {
1049
+ const { done, value } = await reader.read();
1050
+ if (done)
1051
+ break;
1052
+ buffer += decoder.decode(value, { stream: true });
1053
+ const lines = buffer.split(`
1054
+ `);
1055
+ buffer = lines.pop() ?? "";
1056
+ for (const line of lines) {
1057
+ processSSELine(line, model, emit);
1058
+ }
1059
+ }
1060
+ }
1061
+ function mapSSEEventMessageStart(event, model) {
1062
+ const msg = event.message;
1063
+ return {
1064
+ type: "message_start",
1065
+ id: msg?.id ?? generateId("msg"),
1066
+ model
1067
+ };
1068
+ }
1069
+ function mapSSEEventContentBlockStart(event) {
1070
+ const block = event.content_block;
1071
+ if (block?.type === "tool_use") {
1072
+ return {
1073
+ type: "tool_call_start",
1074
+ toolCall: { id: block.id ?? "", name: block.name ?? "" }
1075
+ };
1076
+ }
1077
+ return null;
1078
+ }
1079
+ function mapSSEEventContentBlockDelta(event) {
1080
+ const delta = event.delta;
1081
+ if (delta?.type === "text_delta" && delta.text) {
1082
+ return { type: "text_delta", text: delta.text };
1083
+ }
1084
+ if (delta?.type === "input_json_delta" && delta.partial_json) {
1085
+ return {
1086
+ type: "tool_call_delta",
1087
+ toolCallId: "",
1088
+ arguments: delta.partial_json
1089
+ };
1090
+ }
1091
+ return null;
1092
+ }
1093
+ function mapSSEEventMessageDelta(event) {
1094
+ const delta = event.delta;
1095
+ const usage = event.usage;
1096
+ if (!delta?.stop_reason)
1097
+ return null;
1098
+ const inputTokens = usage?.input_tokens ?? 0;
1099
+ const outputTokens = usage?.output_tokens ?? 0;
1100
+ return {
1101
+ type: "message_end",
1102
+ usage: {
1103
+ inputTokens,
1104
+ outputTokens,
1105
+ totalTokens: inputTokens + outputTokens
1106
+ },
1107
+ stopReason: mapAnthropicStopReason(delta.stop_reason)
1108
+ };
1109
+ }
1110
+ function mapSSEEvent(event, model) {
1111
+ switch (event.type) {
1112
+ case "message_start":
1113
+ return mapSSEEventMessageStart(event, model);
1114
+ case "content_block_start":
1115
+ return mapSSEEventContentBlockStart(event);
1116
+ case "content_block_delta":
1117
+ return mapSSEEventContentBlockDelta(event);
1118
+ case "content_block_stop":
1119
+ return null;
1120
+ case "message_delta":
1121
+ return mapSSEEventMessageDelta(event);
1122
+ default:
1123
+ return null;
1124
+ }
1125
+ }
1126
+
1127
+ // src/providers/google.ts
1128
+ var DEFAULT_BASE_URL2 = "https://generativelanguage.googleapis.com";
1129
+ function createGoogleProvider(config) {
1130
+ const { apiKey, baseUrl = DEFAULT_BASE_URL2, timeout = 60000, maxRetries = 2 } = config;
1131
+ function extractGeminiSystemText(msg) {
1132
+ if (typeof msg.content === "string")
1133
+ return msg.content;
1134
+ return msg.content.filter((p) => p.type === "text").map((p) => p.text).join(`
1135
+ `);
1136
+ }
1137
+ function formatToolResultContents(msg) {
1138
+ const results = [];
1139
+ for (const tr of msg.toolResults ?? []) {
1140
+ const name = tr.toolName ?? tr.toolCallId;
1141
+ results.push({
1142
+ role: "user",
1143
+ parts: [
1144
+ {
1145
+ functionResponse: {
1146
+ name,
1147
+ response: { content: tr.content }
1148
+ }
1149
+ }
1150
+ ]
1151
+ });
1152
+ }
1153
+ return results;
1154
+ }
1155
+ function formatGeminiStringContent(msg, role) {
1156
+ const parts = [{ text: msg.content }];
1157
+ if (msg.toolCalls?.length) {
1158
+ for (const tc of msg.toolCalls) {
1159
+ parts.push({
1160
+ functionCall: { name: tc.name, args: tc.arguments }
1161
+ });
1162
+ }
1163
+ }
1164
+ return { role, parts };
1165
+ }
1166
+ function formatGeminiMultipartContent(msg, role) {
1167
+ const parts = msg.content.filter((p) => p.type === "text").map((p) => ({ text: p.text }));
1168
+ return { role, parts };
1169
+ }
1170
+ function formatMessages(messages) {
1171
+ let systemInstruction;
1172
+ const contents = [];
1173
+ for (const msg of messages) {
1174
+ if (msg.role === "system") {
1175
+ systemInstruction = { parts: [{ text: extractGeminiSystemText(msg) }] };
1176
+ continue;
1177
+ }
1178
+ if (msg.role === "tool") {
1179
+ contents.push(...formatToolResultContents(msg));
1180
+ continue;
1181
+ }
1182
+ const role = msg.role === "assistant" ? "model" : "user";
1183
+ if (typeof msg.content === "string") {
1184
+ contents.push(formatGeminiStringContent(msg, role));
1185
+ } else {
1186
+ contents.push(formatGeminiMultipartContent(msg, role));
1187
+ }
1188
+ }
1189
+ return { systemInstruction, contents };
1190
+ }
1191
+ function formatTools(tools) {
1192
+ if (!tools?.length)
1193
+ return;
1194
+ return [
1195
+ {
1196
+ functionDeclarations: tools.map((t) => ({
1197
+ name: t.name,
1198
+ description: t.description,
1199
+ parameters: t.inputSchema
1200
+ }))
1201
+ }
1202
+ ];
1203
+ }
1204
+ function extractGeminiParts(parts) {
1205
+ const toolCalls = [];
1206
+ const textParts = [];
1207
+ for (const part of parts) {
1208
+ if (part.text) {
1209
+ textParts.push(part.text);
1210
+ }
1211
+ if (part.functionCall) {
1212
+ toolCalls.push({
1213
+ id: generateId("tc"),
1214
+ name: part.functionCall.name,
1215
+ arguments: part.functionCall.args ?? {}
1216
+ });
1217
+ }
1218
+ }
1219
+ return { textParts, toolCalls };
1220
+ }
1221
+ function parseResponse(raw, model, latencyMs) {
1222
+ const traceId = generateTraceId();
1223
+ const candidate = raw.candidates?.[0];
1224
+ const parts = candidate?.content?.parts ?? [];
1225
+ const { textParts, toolCalls } = extractGeminiParts(parts);
1226
+ const usage = {
1227
+ inputTokens: raw.usageMetadata?.promptTokenCount ?? 0,
1228
+ outputTokens: raw.usageMetadata?.candidatesTokenCount ?? 0,
1229
+ totalTokens: raw.usageMetadata?.totalTokenCount ?? 0
1230
+ };
1231
+ return {
1232
+ id: generateId("msg"),
1233
+ message: {
1234
+ role: "assistant",
1235
+ content: textParts.join(""),
1236
+ toolCalls: toolCalls.length > 0 ? toolCalls : undefined
1237
+ },
1238
+ usage,
1239
+ cost: calculateCost(model, usage),
1240
+ model,
1241
+ provider: "google",
1242
+ stopReason: mapGeminiStopReason(candidate?.finishReason, toolCalls.length > 0),
1243
+ latencyMs,
1244
+ traceId
1245
+ };
1246
+ }
1247
+ function resolveSystemInstruction(reqSystem, parsed) {
1248
+ if (reqSystem)
1249
+ return { parts: [{ text: reqSystem }] };
1250
+ return parsed;
1251
+ }
1252
+ function buildGenerationConfig(req) {
1253
+ const config2 = {
1254
+ maxOutputTokens: req.maxTokens ?? 4096
1255
+ };
1256
+ if (req.temperature !== undefined)
1257
+ config2.temperature = req.temperature;
1258
+ if (req.seed !== undefined)
1259
+ config2.seed = req.seed;
1260
+ if (req.topP !== undefined)
1261
+ config2.topP = req.topP;
1262
+ if (req.stopSequences?.length)
1263
+ config2.stopSequences = req.stopSequences;
1264
+ return config2;
1265
+ }
1266
+ function buildRequestBody(req) {
1267
+ const { systemInstruction, contents } = formatMessages(req.messages);
1268
+ const resolved = resolveSystemInstruction(req.system, systemInstruction);
1269
+ const body = {
1270
+ contents,
1271
+ generationConfig: buildGenerationConfig(req)
1272
+ };
1273
+ if (resolved)
1274
+ body.systemInstruction = resolved;
1275
+ const tools = formatTools(req.tools);
1276
+ if (tools)
1277
+ body.tools = tools;
1278
+ return body;
1279
+ }
1280
+ return {
1281
+ name: "google",
1282
+ defaultModel: "gemini-2.0-flash",
1283
+ metadata: {
1284
+ baseUrl: "https://generativelanguage.googleapis.com/v1beta/models",
1285
+ capabilities: ["tools", "vision", "streaming", "system"],
1286
+ authStyle: "bearer"
1287
+ },
1288
+ async complete(req) {
1289
+ const model = req.model ?? "gemini-2.0-flash";
1290
+ const body = buildRequestBody(req);
1291
+ const startTime = performance.now();
1292
+ const raw = await retry(() => googleRequest(baseUrl, model, apiKey, body, timeout, req.signal), {
1293
+ maxRetries,
1294
+ baseDelayMs: 1000,
1295
+ shouldRetry: (e) => e instanceof ElsiumError && e.retryable
1296
+ });
1297
+ const latencyMs = Math.round(performance.now() - startTime);
1298
+ return parseResponse(raw, model, latencyMs);
1299
+ },
1300
+ stream(req) {
1301
+ const model = req.model ?? "gemini-2.0-flash";
1302
+ const body = buildRequestBody(req);
1303
+ return createStream(async (emit) => {
1304
+ const controller = new AbortController;
1305
+ const timer = setTimeout(() => controller.abort(), timeout);
1306
+ try {
1307
+ const signals = [controller.signal, req.signal].filter(Boolean);
1308
+ const mergedSignal = signals.length > 1 ? AbortSignal.any(signals) : signals[0];
1309
+ const response = await fetchGoogleStream(baseUrl, model, apiKey, body, mergedSignal);
1310
+ emit({ type: "message_start", id: generateId("msg"), model });
1311
+ await processGeminiSSEStream(response.body, emit);
1312
+ } finally {
1313
+ clearTimeout(timer);
1314
+ }
1315
+ });
1316
+ },
1317
+ async listModels() {
1318
+ return [
1319
+ "gemini-2.0-flash",
1320
+ "gemini-2.0-flash-lite",
1321
+ "gemini-2.5-pro-preview-05-06",
1322
+ "gemini-2.5-flash-preview-04-17"
1323
+ ];
1324
+ }
1325
+ };
1326
+ }
1327
+ function mapGeminiStopReason(finishReason, hasToolCalls = false) {
1328
+ if (finishReason === "STOP")
1329
+ return "end_turn";
1330
+ if (finishReason === "MAX_TOKENS")
1331
+ return "max_tokens";
1332
+ if (finishReason === "TOOL_CALLS" || hasToolCalls)
1333
+ return "tool_use";
1334
+ return "end_turn";
1335
+ }
1336
+ async function handleGoogleErrorResponse(response) {
1337
+ const errorBody = await response.text().catch(() => "Unknown error");
1338
+ if (response.status === 401 || response.status === 403) {
1339
+ throw ElsiumError.authError("google");
1340
+ }
1341
+ if (response.status === 429) {
1342
+ throw ElsiumError.rateLimit("google");
1343
+ }
1344
+ throw ElsiumError.providerError(`Google API error ${response.status}: ${errorBody}`, {
1345
+ provider: "google",
1346
+ statusCode: response.status,
1347
+ retryable: response.status >= 500
1348
+ });
1349
+ }
1350
+ async function googleRequest(baseUrl, model, apiKey, body, timeout, reqSignal) {
1351
+ const controller = new AbortController;
1352
+ const timer = setTimeout(() => controller.abort(), timeout);
1353
+ try {
1354
+ const signals = [controller.signal, reqSignal].filter(Boolean);
1355
+ const mergedSignal = signals.length > 1 ? AbortSignal.any(signals) : signals[0];
1356
+ const url = `${baseUrl}/v1beta/models/${model}:generateContent`;
1357
+ const response = await fetch(url, {
1358
+ method: "POST",
1359
+ headers: { "Content-Type": "application/json", "x-goog-api-key": apiKey },
1360
+ body: JSON.stringify(body),
1361
+ signal: mergedSignal
1362
+ });
1363
+ if (!response.ok) {
1364
+ await handleGoogleErrorResponse(response);
1365
+ }
1366
+ return await response.json();
1367
+ } finally {
1368
+ clearTimeout(timer);
1369
+ }
1370
+ }
1371
+ async function fetchGoogleStream(baseUrl, model, apiKey, body, signal) {
1372
+ const url = `${baseUrl}/v1beta/models/${model}:streamGenerateContent?alt=sse`;
1373
+ const response = await fetch(url, {
1374
+ method: "POST",
1375
+ headers: { "Content-Type": "application/json", "x-goog-api-key": apiKey },
1376
+ body: JSON.stringify(body),
1377
+ signal
1378
+ });
1379
+ if (!response.ok) {
1380
+ const errorBody = await response.text().catch(() => "Unknown error");
1381
+ throw ElsiumError.providerError(`Google API error ${response.status}: ${errorBody}`, {
1382
+ provider: "google",
1383
+ retryable: response.status >= 500
1384
+ });
1385
+ }
1386
+ if (!response.body) {
1387
+ throw new ElsiumError({
1388
+ code: "STREAM_ERROR",
1389
+ message: "Response body is null",
1390
+ provider: "google",
1391
+ retryable: false
1392
+ });
1393
+ }
1394
+ return response;
1395
+ }
1396
+ function emitGeminiPartEvents(part, emit) {
1397
+ if (part.text) {
1398
+ emit({ type: "text_delta", text: part.text });
1399
+ }
1400
+ if (part.functionCall) {
1401
+ const toolCallId = generateId("tc");
1402
+ emit({
1403
+ type: "tool_call_start",
1404
+ toolCall: { id: toolCallId, name: part.functionCall.name }
1405
+ });
1406
+ emit({
1407
+ type: "tool_call_delta",
1408
+ toolCallId,
1409
+ arguments: JSON.stringify(part.functionCall.args)
1410
+ });
1411
+ emit({ type: "tool_call_end", toolCallId });
1412
+ }
1413
+ }
1414
+ function emitGeminiFinishEvent(event, emit) {
1415
+ const finishReason = event.candidates?.[0]?.finishReason;
1416
+ if (!finishReason)
1417
+ return;
1418
+ const usage = {
1419
+ inputTokens: event.usageMetadata?.promptTokenCount ?? 0,
1420
+ outputTokens: event.usageMetadata?.candidatesTokenCount ?? 0,
1421
+ totalTokens: event.usageMetadata?.totalTokenCount ?? 0
1422
+ };
1423
+ emit({
1424
+ type: "message_end",
1425
+ usage,
1426
+ stopReason: mapGeminiStopReason(finishReason, false)
1427
+ });
1428
+ }
1429
+ function processGeminiSSEEvent(event, emit) {
1430
+ const parts = event.candidates?.[0]?.content?.parts ?? [];
1431
+ for (const part of parts) {
1432
+ emitGeminiPartEvents(part, emit);
1433
+ }
1434
+ emitGeminiFinishEvent(event, emit);
1435
+ }
1436
+ function processGeminiSSELine(line, emit) {
1437
+ if (!line.startsWith("data: "))
1438
+ return;
1439
+ const data = line.slice(6).trim();
1440
+ try {
1441
+ const event = JSON.parse(data);
1442
+ processGeminiSSEEvent(event, emit);
1443
+ } catch (err2) {
1444
+ emit({ type: "error", error: err2 instanceof Error ? err2 : new Error(String(err2)) });
1445
+ }
1446
+ }
1447
+ async function processGeminiSSEStream(body, emit) {
1448
+ const reader = body.getReader();
1449
+ const decoder = new TextDecoder;
1450
+ let buffer = "";
1451
+ while (true) {
1452
+ const { done, value } = await reader.read();
1453
+ if (done)
1454
+ break;
1455
+ buffer += decoder.decode(value, { stream: true });
1456
+ const lines = buffer.split(`
1457
+ `);
1458
+ buffer = lines.pop() ?? "";
1459
+ for (const line of lines) {
1460
+ processGeminiSSELine(line, emit);
1461
+ }
1462
+ }
1463
+ }
1464
+
1465
+ // src/providers/openai.ts
1466
+ var DEFAULT_BASE_URL3 = "https://api.openai.com";
1467
+ var DEFAULT_MAX_TOKENS2 = 4096;
1468
+ function createOpenAIProvider(config) {
1469
+ const { apiKey, baseUrl = DEFAULT_BASE_URL3, timeout = 60000, maxRetries = 2 } = config;
1470
+ async function request(path, body, signal) {
1471
+ const url = `${baseUrl}/v1${path}`;
1472
+ const response = await fetch(url, {
1473
+ method: "POST",
1474
+ headers: {
1475
+ "Content-Type": "application/json",
1476
+ Authorization: `Bearer ${apiKey}`
1477
+ },
1478
+ body: JSON.stringify(body),
1479
+ signal
1480
+ });
1481
+ if (!response.ok) {
1482
+ const errorBody = await response.text().catch(() => "Unknown error");
1483
+ if (response.status === 401)
1484
+ throw ElsiumError.authError("openai");
1485
+ if (response.status === 429) {
1486
+ const retryAfter = response.headers.get("retry-after");
1487
+ throw ElsiumError.rateLimit("openai", retryAfter ? Number.parseInt(retryAfter) * 1000 : undefined);
1488
+ }
1489
+ throw ElsiumError.providerError(`OpenAI API error ${response.status}: ${errorBody}`, {
1490
+ provider: "openai",
1491
+ statusCode: response.status,
1492
+ retryable: response.status >= 500
1493
+ });
1494
+ }
1495
+ return response;
1496
+ }
1497
+ function extractTextContent(msg) {
1498
+ if (typeof msg.content === "string")
1499
+ return msg.content;
1500
+ return msg.content.filter((p) => p.type === "text").map((p) => p.text).join(`
1501
+ `);
1502
+ }
1503
+ function formatSystemMessage(msg) {
1504
+ return { role: "system", content: extractTextContent(msg) };
1505
+ }
1506
+ function formatToolMessages(msg) {
1507
+ return (msg.toolResults ?? []).map((tr) => ({
1508
+ role: "tool",
1509
+ content: tr.content,
1510
+ tool_call_id: tr.toolCallId
1511
+ }));
1512
+ }
1513
+ function formatAssistantMessage(msg) {
1514
+ const content = extractTextContent(msg);
1515
+ const openaiMsg = { role: "assistant", content: content || null };
1516
+ if (msg.toolCalls?.length) {
1517
+ openaiMsg.tool_calls = msg.toolCalls.map((tc) => ({
1518
+ id: tc.id,
1519
+ type: "function",
1520
+ function: {
1521
+ name: tc.name,
1522
+ arguments: JSON.stringify(tc.arguments)
1523
+ }
1524
+ }));
1525
+ }
1526
+ return openaiMsg;
1527
+ }
1528
+ function formatMessages(messages) {
1529
+ const formatted = [];
1530
+ for (const msg of messages) {
1531
+ if (msg.role === "system") {
1532
+ formatted.push(formatSystemMessage(msg));
1533
+ continue;
1534
+ }
1535
+ if (msg.role === "tool") {
1536
+ formatted.push(...formatToolMessages(msg));
1537
+ continue;
1538
+ }
1539
+ if (msg.role === "assistant") {
1540
+ formatted.push(formatAssistantMessage(msg));
1541
+ continue;
1542
+ }
1543
+ formatted.push({ role: "user", content: extractTextContent(msg) });
1544
+ }
1545
+ return formatted;
1546
+ }
1547
+ function formatTools(tools) {
1548
+ if (!tools?.length)
1549
+ return;
1550
+ return tools.map((t) => ({
1551
+ type: "function",
1552
+ function: {
1553
+ name: t.name,
1554
+ description: t.description,
1555
+ parameters: t.inputSchema
1556
+ }
1557
+ }));
1558
+ }
1559
+ function parseResponse(raw, latencyMs) {
1560
+ const traceId = generateTraceId();
1561
+ const choice = raw.choices[0];
1562
+ const toolCalls = (choice?.message.tool_calls ?? []).map((tc) => {
1563
+ let args = {};
1564
+ try {
1565
+ args = JSON.parse(tc.function.arguments);
1566
+ } catch {
1567
+ args = { _raw: tc.function.arguments };
1568
+ }
1569
+ return { id: tc.id, name: tc.function.name, arguments: args };
1570
+ });
1571
+ const usage = {
1572
+ inputTokens: raw.usage.prompt_tokens,
1573
+ outputTokens: raw.usage.completion_tokens,
1574
+ totalTokens: raw.usage.total_tokens
1575
+ };
1576
+ const finishReason = choice?.finish_reason;
1577
+ const stopReason = finishReason === "stop" ? "end_turn" : finishReason === "length" ? "max_tokens" : finishReason === "tool_calls" ? "tool_use" : "end_turn";
1578
+ return {
1579
+ id: raw.id,
1580
+ message: {
1581
+ role: "assistant",
1582
+ content: choice?.message.content ?? "",
1583
+ toolCalls: toolCalls.length > 0 ? toolCalls : undefined
1584
+ },
1585
+ usage,
1586
+ cost: calculateCost(raw.model, usage),
1587
+ model: raw.model,
1588
+ provider: "openai",
1589
+ stopReason,
1590
+ latencyMs,
1591
+ traceId
1592
+ };
1593
+ }
1594
+ return {
1595
+ name: "openai",
1596
+ defaultModel: "gpt-4o",
1597
+ metadata: {
1598
+ baseUrl: "https://api.openai.com/v1/chat/completions",
1599
+ capabilities: ["tools", "vision", "streaming", "system", "json_mode"],
1600
+ authStyle: "bearer"
1601
+ },
1602
+ async complete(req) {
1603
+ const messages = formatMessages(req.messages);
1604
+ const model = req.model ?? "gpt-4o";
1605
+ if (req.system) {
1606
+ messages.unshift({ role: "system", content: req.system });
1607
+ }
1608
+ const body = {
1609
+ model,
1610
+ messages,
1611
+ max_tokens: req.maxTokens ?? DEFAULT_MAX_TOKENS2,
1612
+ ...req.temperature !== undefined ? { temperature: req.temperature } : {},
1613
+ ...req.seed !== undefined ? { seed: req.seed } : {},
1614
+ ...req.topP !== undefined ? { top_p: req.topP } : {},
1615
+ ...req.stopSequences?.length ? { stop: req.stopSequences } : {}
1616
+ };
1617
+ const tools = formatTools(req.tools);
1618
+ if (tools)
1619
+ body.tools = tools;
1620
+ const startTime = performance.now();
1621
+ const raw = await retry(async () => {
1622
+ const controller = new AbortController;
1623
+ const timer = setTimeout(() => controller.abort(), timeout);
1624
+ try {
1625
+ const signals = [controller.signal, req.signal].filter(Boolean);
1626
+ const mergedSignal = signals.length > 1 ? AbortSignal.any(signals) : signals[0];
1627
+ const resp = await request("/chat/completions", body, mergedSignal);
1628
+ return await resp.json();
1629
+ } finally {
1630
+ clearTimeout(timer);
1631
+ }
1632
+ }, {
1633
+ maxRetries,
1634
+ baseDelayMs: 1000,
1635
+ shouldRetry: (e) => e instanceof ElsiumError && e.retryable
1636
+ });
1637
+ const latencyMs = Math.round(performance.now() - startTime);
1638
+ return parseResponse(raw, latencyMs);
1639
+ },
1640
+ stream(req) {
1641
+ const messages = formatMessages(req.messages);
1642
+ const model = req.model ?? "gpt-4o";
1643
+ if (req.system) {
1644
+ messages.unshift({ role: "system", content: req.system });
1645
+ }
1646
+ const body = {
1647
+ model,
1648
+ messages,
1649
+ max_tokens: req.maxTokens ?? DEFAULT_MAX_TOKENS2,
1650
+ stream: true,
1651
+ stream_options: { include_usage: true },
1652
+ ...req.temperature !== undefined ? { temperature: req.temperature } : {},
1653
+ ...req.seed !== undefined ? { seed: req.seed } : {},
1654
+ ...req.topP !== undefined ? { top_p: req.topP } : {},
1655
+ ...req.stopSequences?.length ? { stop: req.stopSequences } : {}
1656
+ };
1657
+ const tools = formatTools(req.tools);
1658
+ if (tools)
1659
+ body.tools = tools;
1660
+ return createStream(async (emit) => {
1661
+ const controller = new AbortController;
1662
+ const timer = setTimeout(() => controller.abort(), timeout);
1663
+ try {
1664
+ const signals = [controller.signal, req.signal].filter(Boolean);
1665
+ const mergedSignal = signals.length > 1 ? AbortSignal.any(signals) : signals[0];
1666
+ const resp = await request("/chat/completions", body, mergedSignal);
1667
+ if (!resp.body)
1668
+ throw new ElsiumError({
1669
+ code: "STREAM_ERROR",
1670
+ message: "Response body is null",
1671
+ provider: "openai",
1672
+ retryable: false
1673
+ });
1674
+ emit({ type: "message_start", id: generateId("msg"), model });
1675
+ await processOpenAISSEStream(resp.body, emit);
1676
+ } finally {
1677
+ clearTimeout(timer);
1678
+ }
1679
+ });
1680
+ },
1681
+ async listModels() {
1682
+ return ["gpt-4o", "gpt-4o-mini", "o1", "o1-mini", "o3-mini"];
1683
+ }
1684
+ };
1685
+ }
1686
+ function emitOpenAIToolCallEvents(toolCalls, emit) {
1687
+ for (const tc of toolCalls) {
1688
+ if (tc.function?.name) {
1689
+ emit({
1690
+ type: "tool_call_start",
1691
+ toolCall: { id: tc.id ?? "", name: tc.function.name }
1692
+ });
1693
+ }
1694
+ if (tc.function?.arguments) {
1695
+ emit({
1696
+ type: "tool_call_delta",
1697
+ toolCallId: tc.id ?? "",
1698
+ arguments: tc.function.arguments
1699
+ });
1700
+ }
1701
+ }
1702
+ }
1703
+ function processOpenAISSEChunk(event, emit, state) {
1704
+ const eventUsage = event.usage;
1705
+ if (eventUsage) {
1706
+ state.usage = {
1707
+ inputTokens: eventUsage.prompt_tokens ?? 0,
1708
+ outputTokens: eventUsage.completion_tokens ?? 0,
1709
+ totalTokens: eventUsage.total_tokens ?? 0
1710
+ };
1711
+ }
1712
+ const delta = event.choices?.[0]?.delta;
1713
+ if (delta?.content) {
1714
+ emit({ type: "text_delta", text: delta.content });
1715
+ }
1716
+ if (delta?.tool_calls) {
1717
+ emitOpenAIToolCallEvents(delta.tool_calls, emit);
1718
+ }
1719
+ const finishReason = event.choices?.[0]?.finish_reason;
1720
+ if (finishReason === "tool_calls" && !state.endEmitted) {
1721
+ state.endEmitted = true;
1722
+ emit({
1723
+ type: "message_end",
1724
+ usage: state.usage,
1725
+ stopReason: "tool_use"
1726
+ });
1727
+ }
1728
+ }
1729
+ function processOpenAISSELine(line, emit, state) {
1730
+ if (!line.startsWith("data: "))
1731
+ return;
1732
+ const data = line.slice(6).trim();
1733
+ if (data === "[DONE]") {
1734
+ if (!state.endEmitted) {
1735
+ state.endEmitted = true;
1736
+ emit({
1737
+ type: "message_end",
1738
+ usage: state.usage,
1739
+ stopReason: "end_turn"
1740
+ });
1741
+ }
1742
+ return;
1743
+ }
1744
+ try {
1745
+ const event = JSON.parse(data);
1746
+ processOpenAISSEChunk(event, emit, state);
1747
+ } catch (err2) {
1748
+ emit({ type: "error", error: err2 instanceof Error ? err2 : new Error(String(err2)) });
1749
+ }
1750
+ }
1751
+ async function processOpenAISSEStream(body, emit) {
1752
+ const reader = body.getReader();
1753
+ const decoder = new TextDecoder;
1754
+ let buffer = "";
1755
+ const state = {
1756
+ endEmitted: false,
1757
+ usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 }
1758
+ };
1759
+ while (true) {
1760
+ const { done, value } = await reader.read();
1761
+ if (done)
1762
+ break;
1763
+ buffer += decoder.decode(value, { stream: true });
1764
+ const lines = buffer.split(`
1765
+ `);
1766
+ buffer = lines.pop() ?? "";
1767
+ for (const line of lines) {
1768
+ processOpenAISSELine(line, emit, state);
1769
+ }
1770
+ }
1771
+ }
1772
+
1773
+ // src/gateway.ts
1774
+ var PROVIDER_FACTORIES = {
1775
+ anthropic: createAnthropicProvider,
1776
+ openai: createOpenAIProvider,
1777
+ google: createGoogleProvider
1778
+ };
1779
+ function registerProviderFactory(name, factory) {
1780
+ PROVIDER_FACTORIES[name] = factory;
1781
+ }
1782
+ function gateway(config) {
1783
+ const factory = PROVIDER_FACTORIES[config.provider];
1784
+ if (!factory) {
1785
+ throw new ElsiumError({
1786
+ code: "CONFIG_ERROR",
1787
+ message: `Unknown provider: ${config.provider}. Available: ${Object.keys(PROVIDER_FACTORIES).join(", ")}`,
1788
+ retryable: false
1789
+ });
1790
+ }
1791
+ const provider = factory({
1792
+ apiKey: config.apiKey,
1793
+ baseUrl: config.baseUrl,
1794
+ timeout: config.timeout,
1795
+ maxRetries: config.maxRetries
1796
+ });
1797
+ if (provider.metadata) {
1798
+ registerProviderMetadata(provider.name, provider.metadata);
1799
+ if (provider.metadata.pricing) {
1800
+ for (const [model, pricing] of Object.entries(provider.metadata.pricing)) {
1801
+ registerPricing(model, pricing);
1802
+ }
1803
+ }
1804
+ }
1805
+ const defaultModel = config.model ?? provider.defaultModel;
1806
+ let xrayStore = null;
1807
+ const allMiddleware = [...config.middleware ?? []];
1808
+ if (config.xray) {
1809
+ const xrayOpts = typeof config.xray === "object" ? config.xray : {};
1810
+ const xm = xrayMiddleware(xrayOpts);
1811
+ xrayStore = xm;
1812
+ allMiddleware.push(xm);
1813
+ }
1814
+ const composedMiddleware = allMiddleware.length ? composeMiddleware(allMiddleware) : null;
1815
+ async function executeWithMiddleware(request) {
1816
+ const req = { ...request, model: request.model ?? defaultModel };
1817
+ if (!composedMiddleware) {
1818
+ return provider.complete(req);
1819
+ }
1820
+ const ctx = {
1821
+ request: req,
1822
+ provider: provider.name,
1823
+ model: req.model ?? defaultModel,
1824
+ traceId: generateTraceId(),
1825
+ startTime: performance.now(),
1826
+ metadata: request.metadata ?? {}
1827
+ };
1828
+ return composedMiddleware(ctx, async (c) => provider.complete(c.request));
1829
+ }
1830
+ return {
1831
+ provider,
1832
+ lastCall() {
1833
+ return xrayStore?.lastCall() ?? null;
1834
+ },
1835
+ callHistory(limit) {
1836
+ return xrayStore?.callHistory(limit) ?? [];
1837
+ },
1838
+ async complete(request) {
1839
+ return executeWithMiddleware(request);
1840
+ },
1841
+ stream(request) {
1842
+ const req = { ...request, model: request.model ?? defaultModel };
1843
+ if (composedMiddleware) {
1844
+ const ctx = {
1845
+ request: req,
1846
+ provider: provider.name,
1847
+ model: req.model ?? defaultModel,
1848
+ traceId: generateTraceId(),
1849
+ startTime: performance.now(),
1850
+ metadata: request.metadata ?? {}
1851
+ };
1852
+ return createStream(async (emit) => {
1853
+ await composedMiddleware(ctx, async (c) => {
1854
+ const stream = provider.stream(c.request);
1855
+ for await (const event of stream) {
1856
+ emit(event);
1857
+ }
1858
+ return {
1859
+ id: "",
1860
+ message: { role: "assistant", content: "" },
1861
+ usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
1862
+ cost: { inputCost: 0, outputCost: 0, totalCost: 0, currency: "USD" },
1863
+ model: c.model,
1864
+ provider: provider.name,
1865
+ stopReason: "end_turn",
1866
+ latencyMs: 0,
1867
+ traceId: ctx.traceId
1868
+ };
1869
+ });
1870
+ });
1871
+ }
1872
+ return provider.stream(req);
1873
+ },
1874
+ async generate(request) {
1875
+ const { schema, ...rest } = request;
1876
+ const jsonSchema = schemaToJsonSchema(schema);
1877
+ const systemPrompt = [
1878
+ rest.system ?? "",
1879
+ "You MUST respond with valid JSON matching this schema:",
1880
+ JSON.stringify(jsonSchema, null, 2),
1881
+ "Respond ONLY with the JSON object, no markdown or explanation."
1882
+ ].filter(Boolean).join(`
1883
+
1884
+ `);
1885
+ const response = await executeWithMiddleware({
1886
+ ...rest,
1887
+ system: systemPrompt
1888
+ });
1889
+ const text = typeof response.message.content === "string" ? response.message.content : "";
1890
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
1891
+ if (!jsonMatch) {
1892
+ throw ElsiumError.validation("LLM response did not contain valid JSON", {
1893
+ response: text
1894
+ });
1895
+ }
1896
+ const parsed = JSON.parse(jsonMatch[0]);
1897
+ const result = schema.safeParse(parsed);
1898
+ if (!result.success) {
1899
+ throw ElsiumError.validation("LLM response did not match schema", {
1900
+ errors: result.error.issues,
1901
+ response: text
1902
+ });
1903
+ }
1904
+ return { data: result.data, response };
1905
+ }
1906
+ };
1907
+ }
1908
+ function schemaToJsonSchema(schema) {
1909
+ try {
1910
+ if ("_def" in schema) {
1911
+ const def = schema._def;
1912
+ const result = convertZodDef(def);
1913
+ if (result)
1914
+ return result;
1915
+ }
1916
+ } catch {}
1917
+ return { type: "string" };
1918
+ }
1919
+ function zodDefKind(def) {
1920
+ return typeof def.type === "string" ? def.type : def.typeName;
1921
+ }
1922
+ function convertZodDef(def) {
1923
+ const kind = zodDefKind(def);
1924
+ switch (kind) {
1925
+ case "object":
1926
+ case "ZodObject":
1927
+ return convertZodObject(def);
1928
+ case "string":
1929
+ case "ZodString":
1930
+ return { type: "string" };
1931
+ case "number":
1932
+ case "ZodNumber":
1933
+ return { type: "number" };
1934
+ case "boolean":
1935
+ case "ZodBoolean":
1936
+ return { type: "boolean" };
1937
+ case "array":
1938
+ case "ZodArray":
1939
+ return convertZodArray(def);
1940
+ case "enum":
1941
+ case "ZodEnum": {
1942
+ const values = def.values ?? (def.entries ? Object.values(def.entries) : []);
1943
+ return { type: "string", enum: values };
1944
+ }
1945
+ case "optional":
1946
+ case "ZodOptional":
1947
+ return convertZodOptional(def);
1948
+ default:
1949
+ return null;
1950
+ }
1951
+ }
1952
+ function convertZodObject(def) {
1953
+ if (!def.shape)
1954
+ return null;
1955
+ const shape = typeof def.shape === "function" ? def.shape() : def.shape;
1956
+ const properties = {};
1957
+ const required = [];
1958
+ for (const [key, value] of Object.entries(shape)) {
1959
+ properties[key] = schemaToJsonSchema(value);
1960
+ const valDef = value._def;
1961
+ const valKind = zodDefKind(valDef);
1962
+ if (valKind !== "optional" && valKind !== "ZodOptional") {
1963
+ required.push(key);
1964
+ }
1965
+ }
1966
+ return { type: "object", properties, required };
1967
+ }
1968
+ function convertZodArray(def) {
1969
+ return {
1970
+ type: "array",
1971
+ items: schemaToJsonSchema(def.element ?? def.type)
1972
+ };
1973
+ }
1974
+ function convertZodOptional(def) {
1975
+ return schemaToJsonSchema(def.innerType ?? def.innerType);
1976
+ }
1977
+ // src/security.ts
1978
+ var INJECTION_PATTERNS = [
1979
+ {
1980
+ pattern: /ignore\s+(?:all\s+)?previous\s+instructions/i,
1981
+ detail: "Attempt to override previous instructions"
1982
+ },
1983
+ { pattern: /ignore\s+all\s+prior/i, detail: "Attempt to ignore prior context" },
1984
+ { pattern: /disregard\s+(?:the\s+)?above/i, detail: "Attempt to disregard above context" },
1985
+ { pattern: /^system\s*:/im, detail: "Attempt to inject system-level instruction" },
1986
+ { pattern: /<\|system\|>/i, detail: "Attempt to inject system token" },
1987
+ { pattern: /\[INST\]/i, detail: "Attempt to inject instruction token" },
1988
+ { pattern: /<<SYS>>/i, detail: "Attempt to inject system block" },
1989
+ { pattern: /<system>/i, detail: "Attempt to inject system tag" },
1990
+ {
1991
+ pattern: /you\s+are\s+now\b.*(?:override|ignore|forget|disregard|new\s+instructions)/i,
1992
+ detail: "Attempt to override agent identity"
1993
+ }
1994
+ ];
1995
+ var JAILBREAK_PATTERNS = [
1996
+ {
1997
+ pattern: /\bDAN\b.*(?:mode|prompt|jailbreak|do\s+anything|no\s+(?:restrictions|rules|limits))/i,
1998
+ detail: "DAN jailbreak attempt"
1999
+ },
2000
+ { pattern: /\bdo\s+anything\s+now\b/i, detail: "DAN (Do Anything Now) jailbreak attempt" },
2001
+ { pattern: /you\s+are\s+(?:now\s+)?DAN\b/i, detail: "DAN role assignment jailbreak attempt" },
2002
+ {
2003
+ pattern: /(?:pretend|act\s+as\s+if)\s+(?:you\s+)?(?:have\s+no|don'?t\s+have\s+any?)\s+(?:restrictions|limitations|rules|guidelines)/i,
2004
+ detail: "Attempt to remove restrictions"
2005
+ },
2006
+ {
2007
+ pattern: /(?:bypass|circumvent|ignore|disable)\s+(?:your\s+)?(?:safety|content|ethical)\s+(?:filters?|guidelines?|restrictions?|rules?)/i,
2008
+ detail: "Attempt to bypass safety filters"
2009
+ },
2010
+ {
2011
+ pattern: /developer\s+mode\s+(?:enabled|activated|on)/i,
2012
+ detail: "Developer mode jailbreak attempt"
2013
+ },
2014
+ {
2015
+ pattern: /(?:unlock|enable)\s+(?:all|unrestricted|unfiltered)\s+(?:mode|access|capabilities)/i,
2016
+ detail: "Attempt to unlock unrestricted mode"
2017
+ },
2018
+ {
2019
+ pattern: /opposite\s+(?:mode|day)|do\s+(?:the\s+)?opposite/i,
2020
+ detail: "Opposite mode jailbreak attempt"
2021
+ }
2022
+ ];
2023
+ var SECRET_PATTERNS = [
2024
+ {
2025
+ pattern: /\bsk-[a-zA-Z0-9_-]{20,}\b/g,
2026
+ detail: "API secret key detected",
2027
+ replacement: "[REDACTED_API_KEY]"
2028
+ },
2029
+ {
2030
+ pattern: /\bpk-[a-zA-Z0-9_-]{20,}\b/g,
2031
+ detail: "API public key detected",
2032
+ replacement: "[REDACTED_API_KEY]"
2033
+ },
2034
+ {
2035
+ pattern: /\bapi_key[=:]\s*["']?[a-zA-Z0-9_-]{16,}["']?/gi,
2036
+ detail: "API key assignment detected",
2037
+ replacement: "[REDACTED_API_KEY]"
2038
+ },
2039
+ {
2040
+ pattern: /\bAKIA[A-Z0-9]{16}\b/g,
2041
+ detail: "AWS access key detected",
2042
+ replacement: "[REDACTED_AWS_KEY]"
2043
+ },
2044
+ {
2045
+ pattern: /\bpassword\s*[=:]\s*["']?[^\s"']{4,}["']?/gi,
2046
+ detail: "Password assignment detected",
2047
+ replacement: "[REDACTED_PASSWORD]"
2048
+ },
2049
+ {
2050
+ pattern: /\b\d{3}-\d{2}-\d{4}\b/g,
2051
+ detail: "SSN pattern detected",
2052
+ replacement: "[REDACTED_SSN]"
2053
+ },
2054
+ {
2055
+ pattern: /\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g,
2056
+ detail: "Credit card number detected",
2057
+ replacement: "[REDACTED_CC]"
2058
+ },
2059
+ {
2060
+ pattern: /\bBearer\s+[a-zA-Z0-9_.-]{20,}\b/g,
2061
+ detail: "Bearer token detected",
2062
+ replacement: "[REDACTED_TOKEN]"
2063
+ }
2064
+ ];
2065
+ var PII_PATTERNS = {
2066
+ email: [
2067
+ {
2068
+ pattern: /\b[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\b/g,
2069
+ detail: "Email address detected",
2070
+ replacement: "[REDACTED_EMAIL]"
2071
+ }
2072
+ ],
2073
+ phone: [
2074
+ {
2075
+ pattern: /\b(?:\+?1[-.\s]?)?(?:\([2-9]\d{2}\)|[2-9]\d{2})[-.\s]?\d{3}[-.\s]?\d{4}\b/g,
2076
+ detail: "US phone number detected",
2077
+ replacement: "[REDACTED_PHONE]"
2078
+ }
2079
+ ],
2080
+ address: [
2081
+ {
2082
+ pattern: /\b\d{1,5}\s+[A-Z][a-zA-Z]*(?:\s+[A-Z][a-zA-Z]*){0,5}\s+(?:St|Street|Ave|Avenue|Blvd|Boulevard|Dr|Drive|Ln|Lane|Rd|Road|Ct|Court|Way|Pl|Place)\.?\b/g,
2083
+ detail: "Street address detected",
2084
+ replacement: "[REDACTED_ADDRESS]"
2085
+ }
2086
+ ],
2087
+ passport: [
2088
+ {
2089
+ pattern: /\b[A-Z]\d{8}\b/g,
2090
+ detail: "Passport number detected",
2091
+ replacement: "[REDACTED_PASSPORT]"
2092
+ }
2093
+ ]
2094
+ };
2095
+ function classifyContent(text) {
2096
+ const detectedTypes = [];
2097
+ for (const { pattern, detail } of SECRET_PATTERNS) {
2098
+ if (new RegExp(pattern.source, pattern.flags).test(text)) {
2099
+ detectedTypes.push(detail);
2100
+ }
2101
+ }
2102
+ if (detectedTypes.length > 0) {
2103
+ return { level: "restricted", detectedTypes };
2104
+ }
2105
+ for (const [, patterns] of Object.entries(PII_PATTERNS)) {
2106
+ for (const { pattern, detail } of patterns) {
2107
+ if (new RegExp(pattern.source, pattern.flags).test(text)) {
2108
+ detectedTypes.push(detail);
2109
+ }
2110
+ }
2111
+ }
2112
+ if (detectedTypes.length > 0) {
2113
+ return { level: "confidential", detectedTypes };
2114
+ }
2115
+ return { level: "public", detectedTypes: [] };
2116
+ }
2117
+ function normalizeText(text) {
2118
+ return text.normalize("NFKC");
2119
+ }
2120
+ function detectPromptInjection(text) {
2121
+ const violations = [];
2122
+ const normalized = normalizeText(text);
2123
+ for (const { pattern, detail } of INJECTION_PATTERNS) {
2124
+ if (pattern.test(normalized)) {
2125
+ violations.push({ type: "prompt_injection", detail, severity: "high" });
2126
+ }
2127
+ }
2128
+ return violations;
2129
+ }
2130
+ function detectJailbreak(text) {
2131
+ const violations = [];
2132
+ const normalized = normalizeText(text);
2133
+ for (const { pattern, detail } of JAILBREAK_PATTERNS) {
2134
+ if (pattern.test(normalized)) {
2135
+ violations.push({ type: "jailbreak", detail, severity: "high" });
2136
+ }
2137
+ }
2138
+ return violations;
2139
+ }
2140
+ function redactPatterns(text, patterns) {
2141
+ const found = [];
2142
+ let redacted = text;
2143
+ for (const { pattern, detail, replacement } of patterns) {
2144
+ const regex = new RegExp(pattern.source, pattern.flags);
2145
+ const result = redacted.replace(regex, replacement);
2146
+ if (result !== redacted) {
2147
+ found.push({ type: "secret_detected", detail, severity: "medium" });
2148
+ redacted = result;
2149
+ }
2150
+ }
2151
+ return { redacted, found };
2152
+ }
2153
+ function resolvePiiPatterns(piiTypes) {
2154
+ const typesToCheck = piiTypes.includes("all") ? Object.keys(PII_PATTERNS) : piiTypes.filter((t) => t !== "all");
2155
+ const patterns = [];
2156
+ for (const type of typesToCheck) {
2157
+ const typePatterns = PII_PATTERNS[type];
2158
+ if (typePatterns)
2159
+ patterns.push(...typePatterns);
2160
+ }
2161
+ return patterns;
2162
+ }
2163
+ function redactSecrets(text, piiTypes) {
2164
+ const secretResult = redactPatterns(text, SECRET_PATTERNS);
2165
+ let { redacted } = secretResult;
2166
+ const found = [...secretResult.found];
2167
+ if (piiTypes?.length) {
2168
+ const piiResult = redactPatterns(redacted, resolvePiiPatterns(piiTypes));
2169
+ redacted = piiResult.redacted;
2170
+ found.push(...piiResult.found);
2171
+ }
2172
+ return { redacted, found };
2173
+ }
2174
+ function checkBlockedPatterns(text, patterns) {
2175
+ const violations = [];
2176
+ for (const pattern of patterns) {
2177
+ pattern.lastIndex = 0;
2178
+ if (pattern.test(text)) {
2179
+ violations.push({
2180
+ type: "blocked_pattern",
2181
+ detail: `Blocked pattern matched: ${pattern.source}`,
2182
+ severity: "medium"
2183
+ });
2184
+ }
2185
+ }
2186
+ return violations;
2187
+ }
2188
+ function scanMessageForViolations(text, config) {
2189
+ const violations = [];
2190
+ if (config.promptInjection !== false) {
2191
+ violations.push(...detectPromptInjection(text));
2192
+ }
2193
+ if (config.jailbreakDetection) {
2194
+ violations.push(...detectJailbreak(text));
2195
+ }
2196
+ if (config.blockedPatterns?.length) {
2197
+ violations.push(...checkBlockedPatterns(text, config.blockedPatterns));
2198
+ }
2199
+ return violations;
2200
+ }
2201
+ function reportAndThrow(violations, config) {
2202
+ for (const v of violations) {
2203
+ config.onViolation?.(v);
2204
+ }
2205
+ throw ElsiumError.validation(`Security violation detected: ${violations.map((v) => v.detail).join("; ")}`);
2206
+ }
2207
+ function redactResponseSecrets(response, config) {
2208
+ const responseText = extractText(response.message.content);
2209
+ if (!responseText)
2210
+ return response;
2211
+ const { redacted, found } = redactSecrets(responseText, config.piiTypes);
2212
+ if (found.length === 0)
2213
+ return response;
2214
+ for (const v of found) {
2215
+ config.onViolation?.(v);
2216
+ }
2217
+ return {
2218
+ ...response,
2219
+ message: {
2220
+ ...response.message,
2221
+ content: redacted
2222
+ }
2223
+ };
2224
+ }
2225
+ function securityMiddleware(config) {
2226
+ return async (ctx, next) => {
2227
+ for (const message of ctx.request.messages) {
2228
+ const text = extractText(message.content);
2229
+ if (!text)
2230
+ continue;
2231
+ const violations = scanMessageForViolations(text, config);
2232
+ if (violations.length > 0) {
2233
+ reportAndThrow(violations, config);
2234
+ }
2235
+ }
2236
+ const response = await next(ctx);
2237
+ if (config.secretRedaction !== false) {
2238
+ return redactResponseSecrets(response, config);
2239
+ }
2240
+ return response;
2241
+ };
2242
+ }
2243
+ // src/bulkhead.ts
2244
+ function createBulkhead(config) {
2245
+ const maxConcurrent = config?.maxConcurrent ?? 10;
2246
+ const maxQueued = config?.maxQueued ?? 50;
2247
+ const queueTimeoutMs = config?.queueTimeoutMs ?? 30000;
2248
+ if (maxConcurrent < 1 || !Number.isFinite(maxConcurrent)) {
2249
+ throw new ElsiumError({
2250
+ code: "CONFIG_ERROR",
2251
+ message: "maxConcurrent must be >= 1",
2252
+ retryable: false
2253
+ });
2254
+ }
2255
+ if (maxQueued < 0 || !Number.isFinite(maxQueued)) {
2256
+ throw new ElsiumError({
2257
+ code: "CONFIG_ERROR",
2258
+ message: "maxQueued must be >= 0 and finite",
2259
+ retryable: false
2260
+ });
2261
+ }
2262
+ if (queueTimeoutMs < 0 || !Number.isFinite(queueTimeoutMs)) {
2263
+ throw new ElsiumError({
2264
+ code: "CONFIG_ERROR",
2265
+ message: "queueTimeoutMs must be >= 0 and finite",
2266
+ retryable: false
2267
+ });
2268
+ }
2269
+ let activeCount = 0;
2270
+ const queue = [];
2271
+ function tryDequeue() {
2272
+ const toResolve = [];
2273
+ while (activeCount < maxConcurrent && queue.length > 0) {
2274
+ const next = queue.shift();
2275
+ if (next) {
2276
+ activeCount++;
2277
+ toResolve.push(next);
2278
+ }
2279
+ }
2280
+ for (const entry of toResolve) {
2281
+ entry.resolve();
2282
+ }
2283
+ }
2284
+ return {
2285
+ get active() {
2286
+ return activeCount;
2287
+ },
2288
+ get queued() {
2289
+ return queue.length;
2290
+ },
2291
+ async execute(fn) {
2292
+ if (activeCount < maxConcurrent) {
2293
+ activeCount++;
2294
+ } else if (queue.length >= maxQueued) {
2295
+ throw ElsiumError.rateLimit("bulkhead");
2296
+ } else {
2297
+ await new Promise((resolve, reject) => {
2298
+ const entry = { resolve, reject };
2299
+ queue.push(entry);
2300
+ const timer = setTimeout(() => {
2301
+ const idx = queue.indexOf(entry);
2302
+ if (idx !== -1) {
2303
+ queue.splice(idx, 1);
2304
+ reject(new ElsiumError({
2305
+ code: "TIMEOUT",
2306
+ message: `Bulkhead queue timeout after ${queueTimeoutMs}ms`,
2307
+ retryable: true
2308
+ }));
2309
+ }
2310
+ }, queueTimeoutMs);
2311
+ const origResolve = entry.resolve;
2312
+ entry.resolve = () => {
2313
+ clearTimeout(timer);
2314
+ origResolve();
2315
+ };
2316
+ });
2317
+ }
2318
+ try {
2319
+ return await fn();
2320
+ } finally {
2321
+ activeCount--;
2322
+ tryDequeue();
2323
+ }
2324
+ }
2325
+ };
2326
+ }
2327
+ function bulkheadMiddleware(config) {
2328
+ const bulkhead = createBulkhead(config);
2329
+ return async (ctx, next) => {
2330
+ return bulkhead.execute(() => next(ctx));
2331
+ };
2332
+ }
2333
+ // src/router.ts
2334
+ var REASONING_KEYWORDS = /\b(prove|explain why|analyze|compare|contrast|evaluate|critique|debate|reason|deduce|infer|justify|argue|synthesize|hypothesize|derive)\b/i;
2335
+ var CODE_KEYWORDS = /\b(implement|refactor|debug|optimize|architect|design pattern|algorithm|data structure|write code|code review|fix the bug|type system)\b/i;
2336
+ var CREATIVE_KEYWORDS = /\b(write a (story|essay|poem|article|report|paper)|compose|draft|create a (plan|proposal|strategy))\b/i;
2337
+ var MATH_KEYWORDS = /\b(calculate|compute|solve|equation|integral|derivative|matrix|probability|statistical|proof|theorem|formula)\b/i;
2338
+ function extractTextContent(request) {
2339
+ const parts = [];
2340
+ for (const m of request.messages) {
2341
+ if (typeof m.content === "string") {
2342
+ parts.push(m.content);
2343
+ } else if (Array.isArray(m.content)) {
2344
+ for (const p of m.content) {
2345
+ if (p.type === "text")
2346
+ parts.push(p.text);
2347
+ }
2348
+ }
2349
+ }
2350
+ if (request.system)
2351
+ parts.push(request.system);
2352
+ return parts.join(" ");
2353
+ }
2354
+ function estimateComplexity(request) {
2355
+ let score = 0;
2356
+ const totalChars = request.messages.reduce((sum, m) => {
2357
+ const len = typeof m.content === "string" ? m.content.length : JSON.stringify(m.content).length;
2358
+ return sum + len;
2359
+ }, 0);
2360
+ if (totalChars > 2000)
2361
+ score += 0.3;
2362
+ if (totalChars > 5000)
2363
+ score += 0.2;
2364
+ if (request.tools?.length)
2365
+ score += 0.2;
2366
+ if ((request.tools?.length ?? 0) > 3)
2367
+ score += 0.1;
2368
+ if (request.system && request.system.length > 500)
2369
+ score += 0.1;
2370
+ if (request.messages.length > 10)
2371
+ score += 0.1;
2372
+ const text = extractTextContent(request);
2373
+ if (REASONING_KEYWORDS.test(text))
2374
+ score += 0.5;
2375
+ if (CODE_KEYWORDS.test(text))
2376
+ score += 0.5;
2377
+ if (CREATIVE_KEYWORDS.test(text))
2378
+ score += 0.2;
2379
+ if (MATH_KEYWORDS.test(text))
2380
+ score += 0.5;
2381
+ return Math.min(score, 1);
2382
+ }
2383
+ function createProviderMesh(config) {
2384
+ if (config.providers.length === 0) {
2385
+ throw new ElsiumError({
2386
+ code: "CONFIG_ERROR",
2387
+ message: "Provider mesh requires at least one provider",
2388
+ retryable: false
2389
+ });
2390
+ }
2391
+ const sortedProviders = [...config.providers].sort((a, b) => (a.priority ?? 99) - (b.priority ?? 99));
2392
+ const gateways = new Map;
2393
+ const circuitBreakers = new Map;
2394
+ for (const entry of sortedProviders) {
2395
+ const gw = gateway({
2396
+ provider: entry.name,
2397
+ apiKey: entry.config.apiKey,
2398
+ baseUrl: entry.config.baseUrl,
2399
+ model: entry.model
2400
+ });
2401
+ gateways.set(entry.name, gw);
2402
+ if (config.circuitBreaker) {
2403
+ const cbConfig = typeof config.circuitBreaker === "boolean" ? {} : config.circuitBreaker;
2404
+ circuitBreakers.set(entry.name, createCircuitBreaker(cbConfig));
2405
+ }
2406
+ }
2407
+ function callWithCircuitBreaker(providerName, fn) {
2408
+ const cb = circuitBreakers.get(providerName);
2409
+ return cb ? cb.execute(fn) : fn();
2410
+ }
2411
+ function isProviderAvailable(providerName) {
2412
+ const cb = circuitBreakers.get(providerName);
2413
+ return !cb || cb.state !== "open";
2414
+ }
2415
+ function getGateway(providerName) {
2416
+ const gw = gateways.get(providerName);
2417
+ if (!gw) {
2418
+ throw new ElsiumError({
2419
+ code: "CONFIG_ERROR",
2420
+ message: `Provider "${providerName}" not found in mesh`,
2421
+ retryable: false
2422
+ });
2423
+ }
2424
+ return gw;
2425
+ }
2426
+ async function fallbackComplete(request) {
2427
+ let lastError = null;
2428
+ for (const entry of sortedProviders) {
2429
+ if (!isProviderAvailable(entry.name))
2430
+ continue;
2431
+ try {
2432
+ const gw = getGateway(entry.name);
2433
+ return await callWithCircuitBreaker(entry.name, () => gw.complete({ ...request, model: request.model ?? entry.model }));
2434
+ } catch (err2) {
2435
+ lastError = err2 instanceof Error ? err2 : new Error(String(err2));
2436
+ }
2437
+ }
2438
+ throw lastError ?? new ElsiumError({
2439
+ code: "PROVIDER_ERROR",
2440
+ message: "All providers failed",
2441
+ retryable: false
2442
+ });
2443
+ }
2444
+ async function costOptimizedComplete(request) {
2445
+ const optimizer = config.costOptimizer;
2446
+ if (!optimizer) {
2447
+ return fallbackComplete(request);
2448
+ }
2449
+ const complexity = estimateComplexity(request);
2450
+ const threshold = optimizer.complexityThreshold ?? 0.5;
2451
+ const target = complexity < threshold ? optimizer.simpleModel : optimizer.complexModel;
2452
+ const gw = getGateway(target.provider);
2453
+ try {
2454
+ return await gw.complete({ ...request, model: target.model });
2455
+ } catch {
2456
+ return fallbackComplete(request);
2457
+ }
2458
+ }
2459
+ async function latencyOptimizedComplete(request) {
2460
+ const controller = new AbortController;
2461
+ const availableProviders = sortedProviders.filter((e) => isProviderAvailable(e.name));
2462
+ const promises = availableProviders.map(async (entry) => {
2463
+ const gw = getGateway(entry.name);
2464
+ return callWithCircuitBreaker(entry.name, () => gw.complete({
2465
+ ...request,
2466
+ model: request.model ?? entry.model,
2467
+ signal: controller.signal
2468
+ }));
2469
+ });
2470
+ try {
2471
+ const result = await Promise.any(promises);
2472
+ controller.abort();
2473
+ return result;
2474
+ } catch {
2475
+ throw new ElsiumError({
2476
+ code: "PROVIDER_ERROR",
2477
+ message: "All providers failed",
2478
+ retryable: false
2479
+ });
2480
+ }
2481
+ }
2482
+ function detectRequiredCapabilities(request) {
2483
+ const capabilities = [];
2484
+ if ((request.tools?.length ?? 0) > 0)
2485
+ capabilities.push("tools");
2486
+ const needsVision = request.messages.some((m) => Array.isArray(m.content) && m.content.some((p) => p.type === "image"));
2487
+ if (needsVision)
2488
+ capabilities.push("vision");
2489
+ return capabilities;
2490
+ }
2491
+ function filterCapableProviders(capabilities) {
2492
+ return sortedProviders.filter((entry) => {
2493
+ if (capabilities.length === 0)
2494
+ return true;
2495
+ const providerCaps = entry.capabilities ?? defaultCapabilities(entry.name);
2496
+ return capabilities.every((c) => providerCaps.includes(c));
2497
+ });
2498
+ }
2499
+ async function tryProviders(providers, request) {
2500
+ let lastError = null;
2501
+ for (const entry of providers) {
2502
+ if (!isProviderAvailable(entry.name))
2503
+ continue;
2504
+ try {
2505
+ const gw = getGateway(entry.name);
2506
+ return await callWithCircuitBreaker(entry.name, () => gw.complete({ ...request, model: request.model ?? entry.model }));
2507
+ } catch (err2) {
2508
+ lastError = err2 instanceof Error ? err2 : new Error(String(err2));
2509
+ }
2510
+ }
2511
+ throw lastError ?? new ElsiumError({
2512
+ code: "PROVIDER_ERROR",
2513
+ message: "No capable provider succeeded",
2514
+ retryable: false
2515
+ });
2516
+ }
2517
+ async function capabilityAwareComplete(request) {
2518
+ const capabilities = detectRequiredCapabilities(request);
2519
+ const capable = filterCapableProviders(capabilities);
2520
+ if (capable.length === 0) {
2521
+ return fallbackComplete(request);
2522
+ }
2523
+ return tryProviders(capable, request);
2524
+ }
2525
+ function defaultCapabilities(provider) {
2526
+ const meta = getProviderMetadata(provider);
2527
+ if (meta?.capabilities)
2528
+ return meta.capabilities;
2529
+ switch (provider) {
2530
+ case "anthropic":
2531
+ return ["tools", "vision", "streaming", "system"];
2532
+ case "openai":
2533
+ return ["tools", "vision", "streaming", "system", "json_mode"];
2534
+ case "google":
2535
+ return ["tools", "vision", "streaming", "system"];
2536
+ default:
2537
+ return ["streaming"];
2538
+ }
2539
+ }
2540
+ return {
2541
+ providers: sortedProviders.map((p) => p.name),
2542
+ strategy: config.strategy,
2543
+ async complete(request) {
2544
+ switch (config.strategy) {
2545
+ case "fallback":
2546
+ return fallbackComplete(request);
2547
+ case "cost-optimized":
2548
+ return costOptimizedComplete(request);
2549
+ case "latency-optimized":
2550
+ return latencyOptimizedComplete(request);
2551
+ case "capability-aware":
2552
+ return capabilityAwareComplete(request);
2553
+ default:
2554
+ return fallbackComplete(request);
2555
+ }
2556
+ },
2557
+ stream(request) {
2558
+ const available = sortedProviders.find((e) => isProviderAvailable(e.name));
2559
+ const entry = available ?? sortedProviders[0];
2560
+ const gw = getGateway(entry.name);
2561
+ return gw.stream({ ...request, model: request.model ?? entry.model });
2562
+ }
2563
+ };
2564
+ }
2565
+ export {
2566
+ xrayMiddleware,
2567
+ securityMiddleware,
2568
+ registerProviderMetadata,
2569
+ registerProviderFactory,
2570
+ registerProvider,
2571
+ registerPricing,
2572
+ redactSecrets,
2573
+ loggingMiddleware,
2574
+ listProviders,
2575
+ getProviderMetadata,
2576
+ getProviderFactory,
2577
+ gateway,
2578
+ detectPromptInjection,
2579
+ detectJailbreak,
2580
+ createProviderMesh,
2581
+ createOpenAIProvider,
2582
+ createGoogleProvider,
2583
+ createBulkhead,
2584
+ createAnthropicProvider,
2585
+ costTrackingMiddleware,
2586
+ composeMiddleware,
2587
+ classifyContent,
2588
+ checkBlockedPatterns,
2589
+ calculateCost,
2590
+ bulkheadMiddleware
2591
+ };