@amux.ai/llm-bridge 0.2.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.js ADDED
@@ -0,0 +1,886 @@
1
+ // src/adapter/registry.ts
2
+ var AdapterRegistry = class {
3
+ adapters = /* @__PURE__ */ new Map();
4
+ /**
5
+ * Register an adapter
6
+ */
7
+ register(adapter) {
8
+ if (this.adapters.has(adapter.name)) {
9
+ throw new Error(`Adapter "${adapter.name}" is already registered`);
10
+ }
11
+ this.adapters.set(adapter.name, adapter);
12
+ }
13
+ /**
14
+ * Unregister an adapter
15
+ */
16
+ unregister(name) {
17
+ return this.adapters.delete(name);
18
+ }
19
+ /**
20
+ * Get an adapter by name
21
+ */
22
+ get(name) {
23
+ return this.adapters.get(name);
24
+ }
25
+ /**
26
+ * Check if an adapter is registered
27
+ */
28
+ has(name) {
29
+ return this.adapters.has(name);
30
+ }
31
+ /**
32
+ * List all registered adapters
33
+ */
34
+ list() {
35
+ return Array.from(this.adapters.values()).map(
36
+ (adapter) => adapter.getInfo()
37
+ );
38
+ }
39
+ /**
40
+ * Clear all adapters
41
+ */
42
+ clear() {
43
+ this.adapters.clear();
44
+ }
45
+ };
46
+ var globalRegistry = new AdapterRegistry();
47
+
48
+ // src/utils/sse-parser.ts
49
+ var SSELineParser = class {
50
+ buffer = "";
51
+ /**
52
+ * Process a chunk of SSE data and extract complete lines
53
+ * @param chunk - The data chunk to process
54
+ * @returns Array of complete lines
55
+ */
56
+ processChunk(chunk) {
57
+ this.buffer += chunk;
58
+ return this.extractLines();
59
+ }
60
+ /**
61
+ * Extract all complete lines from the buffer
62
+ * Incomplete lines remain in the buffer
63
+ * @private
64
+ */
65
+ extractLines() {
66
+ const lines = [];
67
+ let position = 0;
68
+ while (position < this.buffer.length) {
69
+ const newlineIndex = this.buffer.indexOf("\n", position);
70
+ if (newlineIndex === -1) {
71
+ this.buffer = this.buffer.slice(position);
72
+ break;
73
+ }
74
+ const line = this.buffer.slice(position, newlineIndex);
75
+ lines.push(line);
76
+ position = newlineIndex + 1;
77
+ }
78
+ if (position >= this.buffer.length) {
79
+ this.buffer = "";
80
+ }
81
+ return lines;
82
+ }
83
+ /**
84
+ * Get any remaining data in the buffer and clear it
85
+ * Call this when the stream ends to get the last incomplete line
86
+ */
87
+ flush() {
88
+ if (this.buffer.length === 0) {
89
+ return [];
90
+ }
91
+ const lines = this.buffer.split("\n").filter((line) => line.length > 0);
92
+ this.buffer = "";
93
+ return lines;
94
+ }
95
+ /**
96
+ * Check if buffer has any remaining data
97
+ */
98
+ hasRemaining() {
99
+ return this.buffer.length > 0;
100
+ }
101
+ /**
102
+ * Clear the buffer
103
+ */
104
+ clear() {
105
+ this.buffer = "";
106
+ }
107
+ };
108
+
109
+ // src/errors/index.ts
110
+ var LLMBridgeError = class extends Error {
111
+ code;
112
+ retryable;
113
+ details;
114
+ constructor(message, code, retryable = false, details) {
115
+ super(message);
116
+ this.name = "LLMBridgeError";
117
+ this.code = code;
118
+ this.retryable = retryable;
119
+ this.details = details;
120
+ Error.captureStackTrace(this, this.constructor);
121
+ }
122
+ };
123
+ var APIError = class extends LLMBridgeError {
124
+ status;
125
+ provider;
126
+ data;
127
+ response;
128
+ constructor(message, status, provider, data, response) {
129
+ super(message, "API_ERROR", status >= 500, data);
130
+ this.name = "APIError";
131
+ this.status = status;
132
+ this.provider = provider;
133
+ this.data = data;
134
+ this.response = response;
135
+ }
136
+ };
137
+ var NetworkError = class extends LLMBridgeError {
138
+ cause;
139
+ constructor(message, cause) {
140
+ super(message, "NETWORK_ERROR", true, cause);
141
+ this.name = "NetworkError";
142
+ this.cause = cause;
143
+ }
144
+ };
145
+ var TimeoutError = class extends LLMBridgeError {
146
+ timeout;
147
+ constructor(message, timeout) {
148
+ super(message, "TIMEOUT_ERROR", true, { timeout });
149
+ this.name = "TimeoutError";
150
+ this.timeout = timeout;
151
+ }
152
+ };
153
+ var ValidationError = class extends LLMBridgeError {
154
+ errors;
155
+ constructor(message, errors) {
156
+ super(message, "VALIDATION_ERROR", false, { errors });
157
+ this.name = "ValidationError";
158
+ this.errors = errors;
159
+ }
160
+ };
161
+ var AdapterError = class extends LLMBridgeError {
162
+ adapterName;
163
+ constructor(message, adapterName, details) {
164
+ super(message, "ADAPTER_ERROR", false, details);
165
+ this.name = "AdapterError";
166
+ this.adapterName = adapterName;
167
+ }
168
+ };
169
+ var BridgeError = class extends LLMBridgeError {
170
+ constructor(message, details) {
171
+ super(message, "BRIDGE_ERROR", false, details);
172
+ this.name = "BridgeError";
173
+ }
174
+ };
175
+
176
+ // src/bridge/http-client.ts
177
+ var HTTPClient = class {
178
+ defaultHeaders;
179
+ defaultTimeout;
180
+ maxRetries;
181
+ provider;
182
+ maxResponseSize;
183
+ constructor(options) {
184
+ this.defaultHeaders = options?.headers ?? {};
185
+ this.defaultTimeout = options?.timeout ?? 6e4;
186
+ this.maxRetries = options?.maxRetries ?? 3;
187
+ this.provider = options?.provider ?? "unknown";
188
+ this.maxResponseSize = options?.maxResponseSize ?? 100 * 1024 * 1024;
189
+ }
190
+ /**
191
+ * Determine if a status code should be retried
192
+ * @private
193
+ */
194
+ shouldRetry(status) {
195
+ const retryableStatuses = [408, 429, 500, 502, 503, 504];
196
+ return retryableStatuses.includes(status);
197
+ }
198
+ /**
199
+ * Calculate backoff delay with jitter
200
+ * @private
201
+ */
202
+ getBackoffDelay(attempt, retryAfter) {
203
+ if (retryAfter !== void 0 && retryAfter > 0) {
204
+ return retryAfter * 1e3;
205
+ }
206
+ const baseDelay = Math.pow(2, attempt) * 1e3;
207
+ const jitter = Math.random() * baseDelay * 0.1;
208
+ return baseDelay + jitter;
209
+ }
210
+ /**
211
+ * Make an HTTP request with retry logic
212
+ */
213
+ async request(options, retries = 0) {
214
+ const controller = new AbortController();
215
+ const timeoutId = setTimeout(() => {
216
+ controller.abort();
217
+ }, options.timeout ?? this.defaultTimeout);
218
+ try {
219
+ const response = await fetch(options.url, {
220
+ method: options.method,
221
+ headers: {
222
+ "Content-Type": "application/json",
223
+ ...this.defaultHeaders,
224
+ ...options.headers
225
+ },
226
+ body: options.body ? JSON.stringify(options.body) : void 0,
227
+ signal: options.signal ?? controller.signal
228
+ });
229
+ clearTimeout(timeoutId);
230
+ const contentLength = response.headers.get("content-length");
231
+ if (contentLength) {
232
+ const size = parseInt(contentLength, 10);
233
+ if (size > this.maxResponseSize) {
234
+ throw new NetworkError(
235
+ `Response size (${size} bytes) exceeds maximum allowed size (${this.maxResponseSize} bytes)`
236
+ );
237
+ }
238
+ }
239
+ if (!response.ok) {
240
+ const errorData = await response.json().catch(() => ({}));
241
+ const headers = Object.fromEntries(response.headers.entries());
242
+ throw new APIError(
243
+ `HTTP ${response.status}: ${response.statusText}`,
244
+ response.status,
245
+ this.provider,
246
+ errorData,
247
+ { headers }
248
+ );
249
+ }
250
+ const data = await response.json();
251
+ return {
252
+ status: response.status,
253
+ statusText: response.statusText,
254
+ headers: Object.fromEntries(response.headers.entries()),
255
+ data
256
+ };
257
+ } catch (error) {
258
+ clearTimeout(timeoutId);
259
+ if (error instanceof APIError) {
260
+ if (this.shouldRetry(error.status) && retries < this.maxRetries) {
261
+ const retryAfter = error.response?.headers?.["retry-after"];
262
+ const retryAfterSeconds = retryAfter ? parseInt(retryAfter, 10) : void 0;
263
+ const delay = this.getBackoffDelay(retries, retryAfterSeconds);
264
+ await new Promise((resolve) => setTimeout(resolve, delay));
265
+ return this.request(options, retries + 1);
266
+ }
267
+ throw error;
268
+ }
269
+ if (error instanceof Error && error.name === "AbortError") {
270
+ const timeoutError = new TimeoutError(
271
+ "Request timeout",
272
+ options.timeout ?? this.defaultTimeout
273
+ );
274
+ if (retries < this.maxRetries) {
275
+ const delay = this.getBackoffDelay(retries);
276
+ await new Promise((resolve) => setTimeout(resolve, delay));
277
+ return this.request(options, retries + 1);
278
+ }
279
+ throw timeoutError;
280
+ }
281
+ const networkError = new NetworkError("Network request failed", error);
282
+ if (retries < this.maxRetries) {
283
+ const delay = this.getBackoffDelay(retries);
284
+ await new Promise((resolve) => setTimeout(resolve, delay));
285
+ return this.request(options, retries + 1);
286
+ }
287
+ throw networkError;
288
+ }
289
+ }
290
+ /**
291
+ * Make a streaming HTTP request
292
+ */
293
+ async *requestStream(options) {
294
+ const controller = new AbortController();
295
+ const timeoutId = setTimeout(() => {
296
+ controller.abort();
297
+ }, options.timeout ?? this.defaultTimeout);
298
+ try {
299
+ const headers = {
300
+ "Content-Type": "application/json",
301
+ Accept: "text/event-stream",
302
+ ...this.defaultHeaders,
303
+ ...options.headers
304
+ };
305
+ const response = await fetch(options.url, {
306
+ method: options.method,
307
+ headers,
308
+ body: options.body ? JSON.stringify(options.body) : void 0,
309
+ signal: options.signal ?? controller.signal
310
+ });
311
+ clearTimeout(timeoutId);
312
+ if (!response.ok) {
313
+ const errorData = await response.json().catch(() => ({}));
314
+ const headers2 = Object.fromEntries(response.headers.entries());
315
+ throw new APIError(
316
+ `HTTP ${response.status}: ${response.statusText}`,
317
+ response.status,
318
+ this.provider,
319
+ errorData,
320
+ { headers: headers2 }
321
+ );
322
+ }
323
+ if (!response.body) {
324
+ throw new NetworkError("Response body is null");
325
+ }
326
+ const reader = response.body.getReader();
327
+ const decoder = new TextDecoder();
328
+ let totalBytes = 0;
329
+ try {
330
+ while (true) {
331
+ const { done, value } = await reader.read();
332
+ if (done) break;
333
+ totalBytes += value.length;
334
+ if (totalBytes > this.maxResponseSize) {
335
+ throw new NetworkError(
336
+ `Streaming response size (${totalBytes} bytes) exceeds maximum allowed size (${this.maxResponseSize} bytes)`
337
+ );
338
+ }
339
+ const chunk = decoder.decode(value, { stream: true });
340
+ yield chunk;
341
+ }
342
+ } finally {
343
+ reader.releaseLock();
344
+ }
345
+ } catch (error) {
346
+ clearTimeout(timeoutId);
347
+ if (error instanceof APIError) {
348
+ throw error;
349
+ }
350
+ if (error instanceof Error && error.name === "AbortError") {
351
+ throw new TimeoutError(
352
+ "Request timeout",
353
+ options.timeout ?? this.defaultTimeout
354
+ );
355
+ }
356
+ if (error instanceof NetworkError) {
357
+ throw error;
358
+ }
359
+ throw new NetworkError("Network request failed", error);
360
+ }
361
+ }
362
+ };
363
+
364
+ // src/bridge/bridge.ts
365
+ var Bridge = class {
366
+ inboundAdapter;
367
+ outboundAdapter;
368
+ config;
369
+ httpClient;
370
+ // Lifecycle hooks
371
+ hooks;
372
+ // Model mapping configuration
373
+ targetModel;
374
+ modelMapper;
375
+ modelMapping;
376
+ constructor(options) {
377
+ this.inboundAdapter = options.inbound;
378
+ this.outboundAdapter = options.outbound;
379
+ this.config = options.config;
380
+ this.hooks = options.hooks;
381
+ this.targetModel = options.targetModel;
382
+ this.modelMapper = options.modelMapper;
383
+ this.modelMapping = options.modelMapping;
384
+ const authHeaderName = this.config.authHeaderName ?? "Authorization";
385
+ const authHeaderPrefix = this.config.authHeaderPrefix ?? "Bearer";
386
+ const authHeaderValue = authHeaderPrefix ? `${authHeaderPrefix} ${this.config.apiKey}` : this.config.apiKey;
387
+ this.httpClient = new HTTPClient({
388
+ headers: {
389
+ [authHeaderName]: authHeaderValue,
390
+ ...this.config.headers
391
+ },
392
+ timeout: this.config.timeout,
393
+ maxRetries: this.config.maxRetries,
394
+ provider: this.outboundAdapter.name
395
+ });
396
+ }
397
+ /**
398
+ * Map model name from inbound to outbound
399
+ * Priority: targetModel > modelMapper > modelMapping > original model
400
+ */
401
+ mapModel(inboundModel) {
402
+ if (!inboundModel) return void 0;
403
+ if (this.targetModel) {
404
+ return this.targetModel;
405
+ }
406
+ if (this.modelMapper) {
407
+ return this.modelMapper(inboundModel);
408
+ }
409
+ if (this.modelMapping && this.modelMapping[inboundModel]) {
410
+ return this.modelMapping[inboundModel];
411
+ }
412
+ return inboundModel;
413
+ }
414
+ /**
415
+ * Validate that the IR request features are supported by outbound adapter
416
+ * @private
417
+ */
418
+ validateCapabilities(ir) {
419
+ const cap = this.outboundAdapter.capabilities;
420
+ if (ir.tools && ir.tools.length > 0 && !cap.tools) {
421
+ throw new Error(
422
+ `Outbound adapter '${this.outboundAdapter.name}' does not support tools`
423
+ );
424
+ }
425
+ if (!cap.vision) {
426
+ const hasVisionContent = ir.messages.some((msg) => {
427
+ if (typeof msg.content === "string") return false;
428
+ return msg.content.some((part) => part.type === "image");
429
+ });
430
+ if (hasVisionContent) {
431
+ throw new Error(
432
+ `Outbound adapter '${this.outboundAdapter.name}' does not support vision`
433
+ );
434
+ }
435
+ }
436
+ if (ir.generation?.thinking && !cap.reasoning) {
437
+ throw new Error(
438
+ `Outbound adapter '${this.outboundAdapter.name}' does not support reasoning/thinking`
439
+ );
440
+ }
441
+ }
442
+ /**
443
+ * Send a chat request
444
+ */
445
+ async chat(request) {
446
+ try {
447
+ const ir = this.inboundAdapter.inbound.parseRequest(request);
448
+ if (ir.model) {
449
+ ir.model = this.mapModel(ir.model);
450
+ }
451
+ if (this.hooks?.onRequest) {
452
+ await this.hooks.onRequest(ir);
453
+ }
454
+ if (this.inboundAdapter.validateRequest) {
455
+ const validation = this.inboundAdapter.validateRequest(ir);
456
+ if (!validation.valid) {
457
+ throw new Error(
458
+ `Invalid request: ${validation.errors?.join(", ") ?? "Unknown error"}`
459
+ );
460
+ }
461
+ }
462
+ this.validateCapabilities(ir);
463
+ const providerRequest = this.outboundAdapter.outbound.buildRequest(ir);
464
+ const baseURL = this.config.baseURL ?? this.getDefaultBaseURL();
465
+ const endpoint = this.getEndpoint(ir.model);
466
+ const response = await this.httpClient.request({
467
+ method: "POST",
468
+ url: `${baseURL}${endpoint}`,
469
+ body: providerRequest
470
+ });
471
+ const responseIR = this.outboundAdapter.inbound.parseResponse?.(
472
+ response.data
473
+ );
474
+ if (!responseIR) {
475
+ throw new Error("Outbound adapter does not support response parsing");
476
+ }
477
+ if (this.hooks?.onResponse) {
478
+ await this.hooks.onResponse(responseIR);
479
+ }
480
+ const finalResponse = this.inboundAdapter.outbound.buildResponse?.(responseIR);
481
+ if (!finalResponse) {
482
+ return responseIR;
483
+ }
484
+ return finalResponse;
485
+ } catch (error) {
486
+ if (this.hooks?.onError && error instanceof Error) {
487
+ try {
488
+ const errorIR = this.outboundAdapter.inbound.parseError?.(error) ?? {
489
+ message: error.message,
490
+ code: "UNKNOWN_ERROR",
491
+ type: "unknown"
492
+ };
493
+ await this.hooks.onError(errorIR);
494
+ } catch (hookError) {
495
+ console.warn("Error in onError hook:", hookError);
496
+ }
497
+ }
498
+ throw error;
499
+ }
500
+ }
501
+ /**
502
+ * Send a chat request (raw IR response)
503
+ * Returns raw IR response for custom processing
504
+ */
505
+ async chatRaw(request) {
506
+ const ir = this.inboundAdapter.inbound.parseRequest(request);
507
+ if (ir.model) {
508
+ ir.model = this.mapModel(ir.model);
509
+ }
510
+ if (this.inboundAdapter.validateRequest) {
511
+ const validation = this.inboundAdapter.validateRequest(ir);
512
+ if (!validation.valid) {
513
+ throw new Error(
514
+ `Invalid request: ${validation.errors?.join(", ") ?? "Unknown error"}`
515
+ );
516
+ }
517
+ }
518
+ const providerRequest = this.outboundAdapter.outbound.buildRequest(ir);
519
+ const baseURL = this.config.baseURL ?? this.getDefaultBaseURL();
520
+ const endpoint = this.getEndpoint(ir.model);
521
+ const response = await this.httpClient.request({
522
+ method: "POST",
523
+ url: `${baseURL}${endpoint}`,
524
+ body: providerRequest
525
+ });
526
+ const responseIR = this.outboundAdapter.inbound.parseResponse?.(
527
+ response.data
528
+ );
529
+ if (!responseIR) {
530
+ throw new Error("Outbound adapter does not support response parsing");
531
+ }
532
+ return responseIR;
533
+ }
534
+ /**
535
+ * Send a streaming chat request
536
+ * Returns SSE events in inbound adapter's format
537
+ */
538
+ async *chatStream(request) {
539
+ const streamBuilder = this.inboundAdapter.outbound.createStreamBuilder?.();
540
+ if (!streamBuilder) {
541
+ for await (const event of this.chatStreamRaw(request)) {
542
+ yield { event: "data", data: event };
543
+ }
544
+ return;
545
+ }
546
+ for await (const event of this.chatStreamRaw(request)) {
547
+ const sseEvents = streamBuilder.process(event);
548
+ for (const sse of sseEvents) {
549
+ yield sse;
550
+ }
551
+ }
552
+ if (streamBuilder.finalize) {
553
+ const finalEvents = streamBuilder.finalize();
554
+ for (const sse of finalEvents) {
555
+ if (sse.data === "[DONE]") {
556
+ continue;
557
+ }
558
+ yield sse;
559
+ }
560
+ }
561
+ }
562
+ /**
563
+ * Send a streaming chat request (raw IR events)
564
+ * Returns raw IR stream events for custom processing
565
+ */
566
+ async *chatStreamRaw(request) {
567
+ try {
568
+ const ir = this.inboundAdapter.inbound.parseRequest(request);
569
+ if (ir.model) {
570
+ ir.model = this.mapModel(ir.model);
571
+ }
572
+ ir.stream = true;
573
+ if (this.hooks?.onRequest) {
574
+ await this.hooks.onRequest(ir);
575
+ }
576
+ if (!this.outboundAdapter.capabilities.streaming) {
577
+ throw new Error(
578
+ `Outbound adapter '${this.outboundAdapter.name}' does not support streaming`
579
+ );
580
+ }
581
+ this.validateCapabilities(ir);
582
+ const providerRequest = this.outboundAdapter.outbound.buildRequest(ir);
583
+ const baseURL = this.config.baseURL ?? this.getDefaultBaseURL();
584
+ const endpoint = this.getEndpoint(ir.model);
585
+ const streamHandler = this.outboundAdapter.inbound.parseStream;
586
+ if (!streamHandler) {
587
+ throw new Error("Outbound adapter does not support streaming");
588
+ }
589
+ const sseParser = new SSELineParser();
590
+ for await (const chunk of this.httpClient.requestStream({
591
+ method: "POST",
592
+ url: `${baseURL}${endpoint}`,
593
+ body: providerRequest
594
+ })) {
595
+ const lines = sseParser.processChunk(chunk);
596
+ for await (const event of this.processSSELines(lines, streamHandler)) {
597
+ yield event;
598
+ }
599
+ }
600
+ if (sseParser.hasRemaining()) {
601
+ const lines = sseParser.flush();
602
+ for await (const event of this.processSSELines(lines, streamHandler)) {
603
+ yield event;
604
+ }
605
+ }
606
+ } catch (error) {
607
+ if (this.hooks?.onError && error instanceof Error) {
608
+ try {
609
+ const errorIR = this.outboundAdapter.inbound.parseError?.(error) ?? {
610
+ message: error.message,
611
+ code: "UNKNOWN_ERROR",
612
+ type: "unknown"
613
+ };
614
+ await this.hooks.onError(errorIR);
615
+ } catch (hookError) {
616
+ console.warn("Error in onError hook:", hookError);
617
+ }
618
+ }
619
+ throw error;
620
+ }
621
+ }
622
+ /**
623
+ * Process SSE lines and yield stream events
624
+ * @private
625
+ */
626
+ async *processSSELines(lines, streamHandler) {
627
+ for (const line of lines) {
628
+ if (line.startsWith("data: ")) {
629
+ const data = line.slice(6).trim();
630
+ if (data === "[DONE]") continue;
631
+ try {
632
+ const parsed = JSON.parse(data);
633
+ const events = streamHandler(parsed);
634
+ if (events) {
635
+ const eventArray = Array.isArray(events) ? events : [events];
636
+ for (const event of eventArray) {
637
+ if (this.hooks?.onStreamEvent) {
638
+ await this.hooks.onStreamEvent(event);
639
+ }
640
+ yield event;
641
+ }
642
+ }
643
+ } catch {
644
+ continue;
645
+ }
646
+ }
647
+ }
648
+ }
649
+ /**
650
+ * List available models from the provider
651
+ */
652
+ async listModels() {
653
+ const baseURL = this.config.baseURL ?? this.getDefaultBaseURL();
654
+ const modelsPath = this.getModelsPath();
655
+ const response = await this.httpClient.request({
656
+ method: "GET",
657
+ url: `${baseURL}${modelsPath}`
658
+ });
659
+ return response.data;
660
+ }
661
+ /**
662
+ * Check compatibility between adapters
663
+ */
664
+ checkCompatibility() {
665
+ const issues = [];
666
+ const warnings = [];
667
+ const inCap = this.inboundAdapter.capabilities;
668
+ const outCap = this.outboundAdapter.capabilities;
669
+ if (inCap.tools && !outCap.tools) {
670
+ issues.push(
671
+ "Inbound adapter supports tools but outbound adapter does not"
672
+ );
673
+ }
674
+ if (inCap.vision && !outCap.vision) {
675
+ warnings.push(
676
+ "Inbound adapter supports vision but outbound adapter does not"
677
+ );
678
+ }
679
+ if (inCap.streaming && !outCap.streaming) {
680
+ warnings.push(
681
+ "Inbound adapter supports streaming but outbound adapter does not"
682
+ );
683
+ }
684
+ if (inCap.reasoning && !outCap.reasoning) {
685
+ warnings.push(
686
+ "Inbound adapter supports reasoning but outbound adapter does not - reasoning content may be lost"
687
+ );
688
+ }
689
+ return {
690
+ compatible: issues.length === 0,
691
+ issues: issues.length > 0 ? issues : void 0,
692
+ warnings: warnings.length > 0 ? warnings : void 0
693
+ };
694
+ }
695
+ /**
696
+ * Get adapter information
697
+ */
698
+ getAdapters() {
699
+ return {
700
+ inbound: {
701
+ name: this.inboundAdapter.name,
702
+ version: this.inboundAdapter.version
703
+ },
704
+ outbound: {
705
+ name: this.outboundAdapter.name,
706
+ version: this.outboundAdapter.version
707
+ }
708
+ };
709
+ }
710
+ /**
711
+ * Get default base URL from outbound adapter
712
+ * Note: config.baseURL is checked before calling this method
713
+ */
714
+ getDefaultBaseURL() {
715
+ const endpoint = this.outboundAdapter.getInfo().endpoint;
716
+ return endpoint?.baseUrl ?? "";
717
+ }
718
+ /**
719
+ * Get chat endpoint path
720
+ * Supports dynamic model replacement for endpoints like /v1beta/models/{model}:generateContent
721
+ * Priority: config.chatPath > adapter.endpoint.chatPath
722
+ */
723
+ getEndpoint(model) {
724
+ const endpoint = this.outboundAdapter.getInfo().endpoint;
725
+ let chatPath = this.config.chatPath ?? endpoint?.chatPath;
726
+ if (!chatPath) {
727
+ throw new Error(
728
+ `No chatPath configured. Either provide chatPath in config or ensure adapter '${this.outboundAdapter.name}' defines endpoint.chatPath`
729
+ );
730
+ }
731
+ if (model && chatPath.includes("{model}")) {
732
+ chatPath = chatPath.replace("{model}", model);
733
+ }
734
+ return chatPath;
735
+ }
736
+ /**
737
+ * Get models endpoint path
738
+ * Priority: config.modelsPath > adapter.endpoint.modelsPath
739
+ */
740
+ getModelsPath() {
741
+ const endpoint = this.outboundAdapter.getInfo().endpoint;
742
+ const modelsPath = this.config.modelsPath ?? endpoint?.modelsPath;
743
+ if (!modelsPath) {
744
+ throw new Error(
745
+ `No modelsPath configured. Either provide modelsPath in config or ensure adapter '${this.outboundAdapter.name}' defines endpoint.modelsPath`
746
+ );
747
+ }
748
+ return modelsPath;
749
+ }
750
+ };
751
+
752
+ // src/bridge/factory.ts
753
+ function createBridge(options) {
754
+ return new Bridge(options);
755
+ }
756
+
757
+ // src/utils/error-parser.ts
758
+ var STANDARD_ERROR_TYPE_MAP = {
759
+ invalid_request_error: "validation",
760
+ authentication_error: "authentication",
761
+ permission_error: "permission",
762
+ not_found_error: "not_found",
763
+ rate_limit_error: "rate_limit",
764
+ api_error: "api",
765
+ server_error: "server",
766
+ insufficient_quota: "rate_limit",
767
+ // Common in some providers
768
+ invalid_api_key: "authentication"
769
+ // Zhipu-specific
770
+ };
771
+ var STANDARD_ERROR_CODE_MAP = {
772
+ InvalidParameter: "validation",
773
+ InvalidApiKey: "authentication",
774
+ AccessDenied: "permission",
775
+ ModelNotFound: "not_found",
776
+ Throttling: "rate_limit",
777
+ InternalError: "server"
778
+ };
779
+ var STANDARD_FINISH_REASON_MAP = {
780
+ stop: "stop",
781
+ length: "length",
782
+ tool_calls: "tool_calls",
783
+ content_filter: "content_filter",
784
+ function_call: "tool_calls",
785
+ // Legacy OpenAI
786
+ insufficient_system_resource: "error",
787
+ // DeepSeek-specific
788
+ sensitive: "content_filter"
789
+ // Zhipu-specific
790
+ };
791
+ function mapFinishReason(reason, customMappings, defaultReason = "stop") {
792
+ if (!reason) return defaultReason;
793
+ const reasonMap = customMappings ? { ...STANDARD_FINISH_REASON_MAP, ...customMappings } : STANDARD_FINISH_REASON_MAP;
794
+ return reasonMap[reason] ?? defaultReason;
795
+ }
796
+ function mapErrorType(type, code, customTypeMappings, customCodeMappings) {
797
+ if (code) {
798
+ const codeMap = customCodeMappings ? { ...STANDARD_ERROR_CODE_MAP, ...customCodeMappings } : STANDARD_ERROR_CODE_MAP;
799
+ if (codeMap[code]) return codeMap[code];
800
+ }
801
+ if (type) {
802
+ const typeMap = customTypeMappings ? { ...STANDARD_ERROR_TYPE_MAP, ...customTypeMappings } : STANDARD_ERROR_TYPE_MAP;
803
+ return typeMap[type] ?? "unknown";
804
+ }
805
+ return "unknown";
806
+ }
807
+ function parseOpenAICompatibleError(error, customTypeMappings, customCodeMappings) {
808
+ if (error && typeof error === "object" && "error" in error) {
809
+ const err = error.error;
810
+ return {
811
+ type: mapErrorType(err.type, err.code, customTypeMappings, customCodeMappings),
812
+ message: err.message,
813
+ code: err.code,
814
+ raw: error
815
+ };
816
+ }
817
+ return {
818
+ type: "unknown",
819
+ message: String(error),
820
+ raw: error
821
+ };
822
+ }
823
+
824
+ // src/utils/content-helpers.ts
825
+ function contentToString(content) {
826
+ if (typeof content === "string") {
827
+ return content || null;
828
+ }
829
+ if (!content || content.length === 0) {
830
+ return null;
831
+ }
832
+ return content.filter((part) => part.type === "text").map((part) => part.type === "text" ? part.text : "").join("") || null;
833
+ }
834
+ function isTextOnlyContent(content) {
835
+ if (typeof content === "string") {
836
+ return true;
837
+ }
838
+ return content.every((part) => part.type === "text");
839
+ }
840
+ function extractTextFromContent(content) {
841
+ if (typeof content === "string") {
842
+ return [content];
843
+ }
844
+ return content.filter((part) => part.type === "text").map((part) => part.type === "text" ? part.text : "");
845
+ }
846
+ function hasImageContent(content) {
847
+ if (typeof content === "string") {
848
+ return false;
849
+ }
850
+ return content.some((part) => part.type === "image");
851
+ }
852
+
853
+ // src/utils/usage-parser.ts
854
+ function parseOpenAIUsage(usage) {
855
+ if (!usage) return void 0;
856
+ return {
857
+ promptTokens: usage.prompt_tokens,
858
+ completionTokens: usage.completion_tokens,
859
+ totalTokens: usage.total_tokens,
860
+ details: usage.completion_tokens_details?.reasoning_tokens || usage.prompt_cache_hit_tokens ? {
861
+ reasoningTokens: usage.completion_tokens_details?.reasoning_tokens,
862
+ cachedTokens: usage.prompt_cache_hit_tokens
863
+ } : void 0
864
+ };
865
+ }
866
+ function buildOpenAIUsage(usage, includeReasoningTokens = true, includeCacheTokens = false) {
867
+ if (!usage) return void 0;
868
+ const result = {
869
+ prompt_tokens: usage.promptTokens,
870
+ completion_tokens: usage.completionTokens,
871
+ total_tokens: usage.totalTokens
872
+ };
873
+ if (includeReasoningTokens && usage.details?.reasoningTokens) {
874
+ result.completion_tokens_details = {
875
+ reasoning_tokens: usage.details.reasoningTokens
876
+ };
877
+ }
878
+ if (includeCacheTokens && usage.details?.cachedTokens) {
879
+ result.prompt_cache_hit_tokens = usage.details.cachedTokens;
880
+ }
881
+ return result;
882
+ }
883
+
884
+ export { APIError, AdapterError, AdapterRegistry, Bridge, BridgeError, HTTPClient, LLMBridgeError, NetworkError, SSELineParser, TimeoutError, ValidationError, buildOpenAIUsage, contentToString, createBridge, extractTextFromContent, globalRegistry, hasImageContent, isTextOnlyContent, mapErrorType, mapFinishReason, parseOpenAICompatibleError, parseOpenAIUsage };
885
+ //# sourceMappingURL=index.js.map
886
+ //# sourceMappingURL=index.js.map