@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.cjs +909 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1328 -0
- package/dist/index.d.ts +1328 -0
- package/dist/index.js +886 -0
- package/dist/index.js.map +1 -0
- package/package.json +53 -0
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
|