babelgate 0.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +2 -0
- package/README.md +467 -1
- package/dist/app.js +40 -0
- package/dist/cli.js +203 -0
- package/dist/config/env-config.js +99 -0
- package/dist/index.js +60 -0
- package/dist/middleware/error-handler.js +158 -0
- package/dist/middleware/request-logger.js +66 -0
- package/dist/middleware/require-api-key.js +80 -0
- package/dist/middleware/resolve-source.js +28 -0
- package/dist/routes/proxy.js +74 -0
- package/dist/services/http-client.js +220 -0
- package/dist/services/proxy.service.js +329 -0
- package/dist/services/stream-pipeline.js +119 -0
- package/dist/services/upstream-errors.js +34 -0
- package/dist/transformers/adapters/anthropic.adapter.js +292 -0
- package/dist/transformers/adapters/anthropic.messages.js +261 -0
- package/dist/transformers/adapters/anthropic.stream.js +378 -0
- package/dist/transformers/adapters/gemini.adapter.js +418 -0
- package/dist/transformers/adapters/gemini.schema.js +61 -0
- package/dist/transformers/adapters/gemini.stream.js +341 -0
- package/dist/transformers/adapters/gemini.tools.js +246 -0
- package/dist/transformers/adapters/index.js +141 -0
- package/dist/transformers/adapters/ollama.adapter.js +288 -0
- package/dist/transformers/adapters/ollama.stream.js +294 -0
- package/dist/transformers/adapters/ollama.tools.js +107 -0
- package/dist/transformers/adapters/openai.adapter.js +460 -0
- package/dist/transformers/adapters/openai.stream.js +229 -0
- package/dist/transformers/index.js +110 -0
- package/dist/types/anthropic.types.js +2 -0
- package/dist/types/canonical.schema.js +160 -0
- package/dist/types/canonical.types.js +8 -0
- package/dist/types/gemini.types.js +2 -0
- package/dist/types/ollama.types.js +4 -0
- package/dist/types/openai.types.js +2 -0
- package/dist/utils/canonical-error.js +77 -0
- package/dist/utils/errors.js +95 -0
- package/dist/utils/ids.js +17 -0
- package/dist/utils/logger.js +21 -0
- package/dist/utils/model-map.js +39 -0
- package/dist/utils/sse.js +147 -0
- package/dist/utils/stream-reassemble.js +104 -0
- package/dist/utils/token-estimate.js +36 -0
- package/dist/utils/tool-args.js +26 -0
- package/package.json +54 -7
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createStreamDecoder = createStreamDecoder;
|
|
4
|
+
exports.createStreamEncoder = createStreamEncoder;
|
|
5
|
+
const ids_1 = require("../../utils/ids");
|
|
6
|
+
const canonical_error_1 = require("../../utils/canonical-error");
|
|
7
|
+
const tool_args_1 = require("../../utils/tool-args");
|
|
8
|
+
const sse_1 = require("../../utils/sse");
|
|
9
|
+
const logger_1 = require("../../utils/logger");
|
|
10
|
+
const token_estimate_1 = require("../../utils/token-estimate");
|
|
11
|
+
const anthropic_adapter_1 = require("./anthropic.adapter");
|
|
12
|
+
/**
|
|
13
|
+
* HTTP status an Anthropic in-stream error `type` corresponds to
|
|
14
|
+
* (docs.claude.com/en/api/errors); the canonical kind comes from
|
|
15
|
+
* `kindFromStatus` like the non-streaming path.
|
|
16
|
+
*/
|
|
17
|
+
const ANTHROPIC_STREAM_ERROR_STATUS = {
|
|
18
|
+
invalid_request_error: 400,
|
|
19
|
+
billing_error: 402,
|
|
20
|
+
authentication_error: 401,
|
|
21
|
+
permission_error: 403,
|
|
22
|
+
not_found_error: 404,
|
|
23
|
+
conflict_error: 409,
|
|
24
|
+
request_too_large: 413,
|
|
25
|
+
rate_limit_error: 429,
|
|
26
|
+
api_error: 500,
|
|
27
|
+
timeout_error: 504,
|
|
28
|
+
overloaded_error: 529,
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Anthropic event-typed SSE → canonical events (spec §3.3,
|
|
32
|
+
* docs.claude.com/en/api/messages-streaming):
|
|
33
|
+
* `message_start` carries the Message skeleton and input-token usage;
|
|
34
|
+
* `content_block_start{tool_use}` opens a tool call (re-keyed from wire
|
|
35
|
+
* block index to canonical index in first-appearance order) whose
|
|
36
|
+
* `input_json_delta.partial_json` chunks become `tool_call_delta`s;
|
|
37
|
+
* `content_block_start{text}` and non-tool blocks (thinking,
|
|
38
|
+
* server_tool_use, …) produce nothing, as do `thinking_delta`,
|
|
39
|
+
* `signature_delta` and `ping`; `message_delta` contributes the
|
|
40
|
+
* cumulative `usage` and the stop reason; `message_stop` is terminal —
|
|
41
|
+
* the held usage and `message_end` are emitted there, so a stream cut
|
|
42
|
+
* before `message_stop` ends in an `error` event. `event: error` maps
|
|
43
|
+
* through `toCanonicalError` and ends the stream.
|
|
44
|
+
*/
|
|
45
|
+
function createStreamDecoder(model, ids = (0, ids_1.createIdGenerator)()) {
|
|
46
|
+
const utf8 = (0, sse_1.createUtf8Decoder)();
|
|
47
|
+
const sse = (0, sse_1.createSseParser)();
|
|
48
|
+
let finished = false;
|
|
49
|
+
let nextToolIndex = 0;
|
|
50
|
+
let inputTokens;
|
|
51
|
+
let outputTokens;
|
|
52
|
+
let finish;
|
|
53
|
+
// wire content-block index → block kind / canonical tool index
|
|
54
|
+
const toolIndexByWire = new Map();
|
|
55
|
+
const openCalls = [];
|
|
56
|
+
// §3.5: dropped thinking types are named once per stream at debug.
|
|
57
|
+
const loggedDrops = new Set();
|
|
58
|
+
function logDrop(type) {
|
|
59
|
+
if (loggedDrops.has(type))
|
|
60
|
+
return;
|
|
61
|
+
loggedDrops.add(type);
|
|
62
|
+
logger_1.logger.debug(`Dropping stream block "${type}" (no canonical equivalent)`);
|
|
63
|
+
}
|
|
64
|
+
function closeAll(out) {
|
|
65
|
+
for (const index of openCalls.splice(0)) {
|
|
66
|
+
out.push({ type: "tool_call_end", index });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function handleEvent(eventType, data, out) {
|
|
70
|
+
let event;
|
|
71
|
+
try {
|
|
72
|
+
event = JSON.parse(data);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
out.push({
|
|
76
|
+
type: "error",
|
|
77
|
+
error: (0, canonical_error_1.makeCanonicalError)("upstream", "Upstream sent an undecodable stream chunk"),
|
|
78
|
+
});
|
|
79
|
+
finished = true;
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const type = typeof event.type === "string" ? event.type : eventType;
|
|
83
|
+
switch (type) {
|
|
84
|
+
case "message_start": {
|
|
85
|
+
const message = event.message ?? {};
|
|
86
|
+
inputTokens = message.usage?.input_tokens;
|
|
87
|
+
out.push({
|
|
88
|
+
type: "message_start",
|
|
89
|
+
id: typeof message.id === "string" ? message.id : ids(),
|
|
90
|
+
model: typeof message.model === "string" ? message.model : model,
|
|
91
|
+
});
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
case "content_block_start": {
|
|
95
|
+
const index = typeof event.index === "number" ? event.index : 0;
|
|
96
|
+
const block = event.content_block ?? {};
|
|
97
|
+
if (block.type === "tool_use") {
|
|
98
|
+
const toolIndex = nextToolIndex++;
|
|
99
|
+
toolIndexByWire.set(index, toolIndex);
|
|
100
|
+
openCalls.push(toolIndex);
|
|
101
|
+
out.push({
|
|
102
|
+
type: "tool_call_start",
|
|
103
|
+
index: toolIndex,
|
|
104
|
+
id: typeof block.id === "string" ? block.id : ids(),
|
|
105
|
+
name: typeof block.name === "string" ? block.name : "",
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
if (block.type === "thinking" || block.type === "redacted_thinking") {
|
|
109
|
+
logDrop(block.type);
|
|
110
|
+
}
|
|
111
|
+
// text, thinking, server_tool_use, … blocks produce nothing
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
case "content_block_delta": {
|
|
115
|
+
const index = typeof event.index === "number" ? event.index : 0;
|
|
116
|
+
const delta = event.delta ?? {};
|
|
117
|
+
if (delta.type === "text_delta" && typeof delta.text === "string") {
|
|
118
|
+
out.push({ type: "text_delta", text: delta.text });
|
|
119
|
+
}
|
|
120
|
+
else if (delta.type === "thinking_delta") {
|
|
121
|
+
logDrop("thinking_delta");
|
|
122
|
+
}
|
|
123
|
+
else if (delta.type === "signature_delta") {
|
|
124
|
+
logDrop("signature_delta");
|
|
125
|
+
}
|
|
126
|
+
else if (delta.type === "input_json_delta") {
|
|
127
|
+
const toolIndex = toolIndexByWire.get(index);
|
|
128
|
+
if (toolIndex !== undefined &&
|
|
129
|
+
typeof delta.partial_json === "string" &&
|
|
130
|
+
delta.partial_json.length > 0) {
|
|
131
|
+
out.push({
|
|
132
|
+
type: "tool_call_delta",
|
|
133
|
+
index: toolIndex,
|
|
134
|
+
argumentsDelta: delta.partial_json,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// citations_delta and anything else → nothing
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
case "content_block_stop": {
|
|
142
|
+
const index = typeof event.index === "number" ? event.index : 0;
|
|
143
|
+
const toolIndex = toolIndexByWire.get(index);
|
|
144
|
+
if (toolIndex !== undefined) {
|
|
145
|
+
toolIndexByWire.delete(index);
|
|
146
|
+
openCalls.splice(openCalls.indexOf(toolIndex), 1);
|
|
147
|
+
out.push({ type: "tool_call_end", index: toolIndex });
|
|
148
|
+
}
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
case "message_delta": {
|
|
152
|
+
// Held until message_stop — a stream cut before it must end in
|
|
153
|
+
// an error event, never a half-emitted message_end.
|
|
154
|
+
const stopReason = event.delta?.stop_reason;
|
|
155
|
+
if (typeof stopReason === "string") {
|
|
156
|
+
finish = (0, anthropic_adapter_1.mapFinishReason)(stopReason);
|
|
157
|
+
}
|
|
158
|
+
if (event.usage?.input_tokens != null) {
|
|
159
|
+
inputTokens = event.usage.input_tokens;
|
|
160
|
+
}
|
|
161
|
+
if (event.usage?.output_tokens != null) {
|
|
162
|
+
outputTokens = event.usage.output_tokens;
|
|
163
|
+
}
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
case "message_stop": {
|
|
167
|
+
if (inputTokens !== undefined || outputTokens !== undefined) {
|
|
168
|
+
const input = inputTokens ?? 0;
|
|
169
|
+
const output = outputTokens ?? 0;
|
|
170
|
+
out.push({
|
|
171
|
+
type: "usage",
|
|
172
|
+
usage: {
|
|
173
|
+
inputTokens: input,
|
|
174
|
+
outputTokens: output,
|
|
175
|
+
totalTokens: input + output,
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
out.push({
|
|
180
|
+
type: "message_end",
|
|
181
|
+
finishReason: finish ?? "unknown",
|
|
182
|
+
});
|
|
183
|
+
finished = true;
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
case "error": {
|
|
187
|
+
const status = ANTHROPIC_STREAM_ERROR_STATUS[event.error?.type] ?? 500;
|
|
188
|
+
out.push({ type: "error", error: (0, anthropic_adapter_1.toCanonicalError)(status, event) });
|
|
189
|
+
finished = true;
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
// ping and anything unrecognised → nothing
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function drain(events, out) {
|
|
196
|
+
for (const e of events) {
|
|
197
|
+
if (finished)
|
|
198
|
+
break;
|
|
199
|
+
handleEvent(e.event, e.data, out);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
feed(chunk) {
|
|
204
|
+
const out = [];
|
|
205
|
+
drain(sse.feed(utf8(chunk)), out);
|
|
206
|
+
return out;
|
|
207
|
+
},
|
|
208
|
+
end() {
|
|
209
|
+
const out = [];
|
|
210
|
+
drain(sse.feed(utf8(new Uint8Array(0))), out);
|
|
211
|
+
drain(sse.end(), out);
|
|
212
|
+
if (!finished) {
|
|
213
|
+
closeAll(out);
|
|
214
|
+
out.push({
|
|
215
|
+
type: "error",
|
|
216
|
+
error: (0, canonical_error_1.makeCanonicalError)("upstream", "Upstream stream ended without completing"),
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
return out;
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Canonical events → Anthropic event-typed SSE (spec §3.3, §3.4): text
|
|
225
|
+
* deltas run inside an open text block; tool calls are buffered until
|
|
226
|
+
* `tool_call_end`, then written as one `content_block_start{tool_use}` +
|
|
227
|
+
* a single `input_json_delta` (the parsed arguments object, or the
|
|
228
|
+
* `_invalid_arguments` wrapper) + `content_block_stop` — an open text
|
|
229
|
+
* block stops first, since Anthropic never overlaps content blocks.
|
|
230
|
+
* Content-block indices are contiguous in emission order — the
|
|
231
|
+
* canonical tool `index` keys the buffer but is not the wire index.
|
|
232
|
+
* `usage` is held into the closing `message_delta`, followed by
|
|
233
|
+
* `message_stop`; `error` becomes `event: error` via
|
|
234
|
+
* `fromCanonicalError`. `message_end` and `error` are terminal: any
|
|
235
|
+
* event after them encodes to nothing.
|
|
236
|
+
*/
|
|
237
|
+
function createStreamEncoder(ctx) {
|
|
238
|
+
// §3.5 + owner note (task 11): Claude Code reads message_start's usage
|
|
239
|
+
// for its context display, but the real counts only arrive in the
|
|
240
|
+
// final message_delta — so message_start carries the local estimate
|
|
241
|
+
// of the client's request, derived from ctx.sourceRequest (the same
|
|
242
|
+
// body the service already decoded; adding an estimate field to
|
|
243
|
+
// StreamEncoderContext would be a canonical interface change).
|
|
244
|
+
const estimatedInput = (0, token_estimate_1.estimateInputTokens)((0, anthropic_adapter_1.toCanonicalRequest)(ctx.sourceRequest));
|
|
245
|
+
let nextBlockIndex = 0;
|
|
246
|
+
let openTextIndex;
|
|
247
|
+
let finished = false;
|
|
248
|
+
let usage;
|
|
249
|
+
const calls = new Map();
|
|
250
|
+
const sse = (type, data) => (0, sse_1.formatSse)(JSON.stringify(data), type);
|
|
251
|
+
function closeText() {
|
|
252
|
+
if (openTextIndex === undefined)
|
|
253
|
+
return "";
|
|
254
|
+
const index = openTextIndex;
|
|
255
|
+
openTextIndex = undefined;
|
|
256
|
+
return sse("content_block_stop", {
|
|
257
|
+
type: "content_block_stop",
|
|
258
|
+
index,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
return {
|
|
262
|
+
contentType: "text/event-stream",
|
|
263
|
+
encode(event) {
|
|
264
|
+
if (finished)
|
|
265
|
+
return "";
|
|
266
|
+
switch (event.type) {
|
|
267
|
+
case "message_start":
|
|
268
|
+
return sse("message_start", {
|
|
269
|
+
type: "message_start",
|
|
270
|
+
message: {
|
|
271
|
+
id: event.id,
|
|
272
|
+
type: "message",
|
|
273
|
+
role: "assistant",
|
|
274
|
+
model: ctx.requestedModel,
|
|
275
|
+
content: [],
|
|
276
|
+
stop_reason: null,
|
|
277
|
+
stop_sequence: null,
|
|
278
|
+
usage: { input_tokens: estimatedInput, output_tokens: 0 },
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
case "text_delta": {
|
|
282
|
+
let out = "";
|
|
283
|
+
if (openTextIndex === undefined) {
|
|
284
|
+
openTextIndex = nextBlockIndex++;
|
|
285
|
+
out += sse("content_block_start", {
|
|
286
|
+
type: "content_block_start",
|
|
287
|
+
index: openTextIndex,
|
|
288
|
+
content_block: { type: "text", text: "" },
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
return (out +
|
|
292
|
+
sse("content_block_delta", {
|
|
293
|
+
type: "content_block_delta",
|
|
294
|
+
index: openTextIndex,
|
|
295
|
+
delta: { type: "text_delta", text: event.text },
|
|
296
|
+
}));
|
|
297
|
+
}
|
|
298
|
+
case "tool_call_start": {
|
|
299
|
+
const out = closeText();
|
|
300
|
+
calls.set(event.index, {
|
|
301
|
+
id: event.id,
|
|
302
|
+
name: event.name,
|
|
303
|
+
args: "",
|
|
304
|
+
});
|
|
305
|
+
return out;
|
|
306
|
+
}
|
|
307
|
+
case "tool_call_delta": {
|
|
308
|
+
const call = calls.get(event.index);
|
|
309
|
+
if (call)
|
|
310
|
+
call.args += event.argumentsDelta;
|
|
311
|
+
return "";
|
|
312
|
+
}
|
|
313
|
+
case "tool_call_end": {
|
|
314
|
+
const call = calls.get(event.index);
|
|
315
|
+
if (!call)
|
|
316
|
+
return "";
|
|
317
|
+
calls.delete(event.index);
|
|
318
|
+
const parsed = (0, tool_args_1.parseToolArguments)(call.args);
|
|
319
|
+
if (!parsed.valid) {
|
|
320
|
+
logger_1.logger.warn("Tool call arguments are not valid JSON; sending _invalid_arguments wrapper");
|
|
321
|
+
}
|
|
322
|
+
const out = closeText();
|
|
323
|
+
const index = nextBlockIndex++;
|
|
324
|
+
return (out +
|
|
325
|
+
sse("content_block_start", {
|
|
326
|
+
type: "content_block_start",
|
|
327
|
+
index,
|
|
328
|
+
content_block: {
|
|
329
|
+
type: "tool_use",
|
|
330
|
+
id: call.id,
|
|
331
|
+
name: call.name,
|
|
332
|
+
input: {},
|
|
333
|
+
},
|
|
334
|
+
}) +
|
|
335
|
+
sse("content_block_delta", {
|
|
336
|
+
type: "content_block_delta",
|
|
337
|
+
index,
|
|
338
|
+
delta: {
|
|
339
|
+
type: "input_json_delta",
|
|
340
|
+
partial_json: JSON.stringify(parsed.args),
|
|
341
|
+
},
|
|
342
|
+
}) +
|
|
343
|
+
sse("content_block_stop", {
|
|
344
|
+
type: "content_block_stop",
|
|
345
|
+
index,
|
|
346
|
+
}));
|
|
347
|
+
}
|
|
348
|
+
case "usage":
|
|
349
|
+
usage = event.usage;
|
|
350
|
+
return "";
|
|
351
|
+
case "message_end":
|
|
352
|
+
finished = true;
|
|
353
|
+
return (closeText() +
|
|
354
|
+
sse("message_delta", {
|
|
355
|
+
type: "message_delta",
|
|
356
|
+
delta: {
|
|
357
|
+
stop_reason: (0, anthropic_adapter_1.mapToAnthropicStopReason)(event.finishReason),
|
|
358
|
+
stop_sequence: null,
|
|
359
|
+
},
|
|
360
|
+
usage: {
|
|
361
|
+
...(usage && {
|
|
362
|
+
input_tokens: usage.inputTokens,
|
|
363
|
+
}),
|
|
364
|
+
output_tokens: usage?.outputTokens ?? 0,
|
|
365
|
+
},
|
|
366
|
+
}) +
|
|
367
|
+
sse("message_stop", { type: "message_stop" }));
|
|
368
|
+
case "error":
|
|
369
|
+
finished = true;
|
|
370
|
+
return (closeText() +
|
|
371
|
+
(0, sse_1.formatSse)(JSON.stringify((0, anthropic_adapter_1.fromCanonicalError)(event.error)), "error"));
|
|
372
|
+
}
|
|
373
|
+
},
|
|
374
|
+
end() {
|
|
375
|
+
return "";
|
|
376
|
+
},
|
|
377
|
+
};
|
|
378
|
+
}
|