babelgate 0.0.0 → 1.1.1
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 +34 -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 +55 -7
|
@@ -0,0 +1,294 @@
|
|
|
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 ollama_adapter_1 = require("./ollama.adapter");
|
|
11
|
+
// `done_reason` on the wire is "stop" or "length" (docs.ollama.com/api/chat);
|
|
12
|
+
// anything else has no canonical equivalent.
|
|
13
|
+
const OLLAMA_DONE_REASON = {
|
|
14
|
+
stop: "stop",
|
|
15
|
+
length: "length",
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Ollama NDJSON → canonical events (spec §3.3, docs.ollama.com/api/chat
|
|
19
|
+
* and github.com/ollama/ollama docs/api.md "Chat request (Streaming)").
|
|
20
|
+
* Each line is one complete chat response object: `message.content`
|
|
21
|
+
* fragments become text deltas (empty content emits nothing, as does
|
|
22
|
+
* `message.thinking`); `message.tool_calls` arrive whole — each call
|
|
23
|
+
* (arguments already an object) becomes start + one delta +
|
|
24
|
+
* end at sequential canonical indices, keeping the wire `id` when
|
|
25
|
+
* present and drawing from `ids` otherwise. The first line emits
|
|
26
|
+
* `message_start` with a generated id (Ollama has no message id). The
|
|
27
|
+
* `done: true` line carries usage (`prompt_eval_count`, `eval_count`)
|
|
28
|
+
* and `done_reason` — a stream that ended in tool calls reports
|
|
29
|
+
* `tool_call` as Ollama itself means it, `length` maps to `length`.
|
|
30
|
+
* `function.arguments` is normally an object (stringified into the
|
|
31
|
+
* canonical delta); a string passes through verbatim. A mid-stream
|
|
32
|
+
* `{"error":"…"}` line (docs.ollama.com/api/errors) maps
|
|
33
|
+
* through `toCanonicalError` — the wire carries no status, so the same
|
|
34
|
+
* failure returned pre-stream (HTTP 500 → `internal`) applies. A
|
|
35
|
+
* line that isn't a plain object, or a `tool_calls` that isn't an
|
|
36
|
+
* array, is an undecodable chunk — one `error` event and the stream
|
|
37
|
+
* is finished. A stream ending without `done: true` ends in an
|
|
38
|
+
* `error` event.
|
|
39
|
+
*/
|
|
40
|
+
function createStreamDecoder(model, ids = (0, ids_1.createIdGenerator)()) {
|
|
41
|
+
const utf8 = (0, sse_1.createUtf8Decoder)();
|
|
42
|
+
const lines = (0, sse_1.createLineSplitter)();
|
|
43
|
+
let started = false;
|
|
44
|
+
let finished = false;
|
|
45
|
+
let nextIndex = 0;
|
|
46
|
+
let sawToolCalls = false;
|
|
47
|
+
function handleObject(obj, out) {
|
|
48
|
+
if (typeof obj.error === "string") {
|
|
49
|
+
out.push({ type: "error", error: (0, ollama_adapter_1.toCanonicalError)(500, obj) });
|
|
50
|
+
finished = true;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (!started) {
|
|
54
|
+
started = true;
|
|
55
|
+
out.push({
|
|
56
|
+
type: "message_start",
|
|
57
|
+
id: ids(),
|
|
58
|
+
model: typeof obj.model === "string" ? obj.model : model,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
const message = obj.message;
|
|
62
|
+
if (typeof message?.content === "string" &&
|
|
63
|
+
message.content.length > 0) {
|
|
64
|
+
out.push({ type: "text_delta", text: message.content });
|
|
65
|
+
}
|
|
66
|
+
const toolCalls = message?.tool_calls;
|
|
67
|
+
if (toolCalls != null && !Array.isArray(toolCalls)) {
|
|
68
|
+
failUndecodable(out);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
for (const tc of toolCalls ?? []) {
|
|
72
|
+
sawToolCalls = true;
|
|
73
|
+
const index = nextIndex++;
|
|
74
|
+
out.push({
|
|
75
|
+
type: "tool_call_start",
|
|
76
|
+
index,
|
|
77
|
+
id: typeof tc?.id === "string" ? tc.id : ids(),
|
|
78
|
+
name: tc?.function?.name ?? "",
|
|
79
|
+
});
|
|
80
|
+
// `arguments` is normally an object (JSON.stringify it); a string
|
|
81
|
+
// is already the JSON encoding and passes through verbatim.
|
|
82
|
+
const args = tc?.function?.arguments;
|
|
83
|
+
out.push({
|
|
84
|
+
type: "tool_call_delta",
|
|
85
|
+
index,
|
|
86
|
+
argumentsDelta: typeof args === "string" ? args : JSON.stringify(args ?? {}),
|
|
87
|
+
});
|
|
88
|
+
out.push({ type: "tool_call_end", index });
|
|
89
|
+
}
|
|
90
|
+
if (obj.done === true) {
|
|
91
|
+
const input = obj.prompt_eval_count ?? 0;
|
|
92
|
+
const output = obj.eval_count ?? 0;
|
|
93
|
+
out.push({
|
|
94
|
+
type: "usage",
|
|
95
|
+
usage: {
|
|
96
|
+
inputTokens: input,
|
|
97
|
+
outputTokens: output,
|
|
98
|
+
totalTokens: input + output,
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
out.push({
|
|
102
|
+
type: "message_end",
|
|
103
|
+
finishReason: sawToolCalls
|
|
104
|
+
? "tool_call"
|
|
105
|
+
: (OLLAMA_DONE_REASON[obj.done_reason] ?? "unknown"),
|
|
106
|
+
});
|
|
107
|
+
finished = true;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function failUndecodable(out) {
|
|
111
|
+
out.push({
|
|
112
|
+
type: "error",
|
|
113
|
+
error: (0, canonical_error_1.makeCanonicalError)("upstream", "Upstream sent an undecodable stream chunk"),
|
|
114
|
+
});
|
|
115
|
+
finished = true;
|
|
116
|
+
}
|
|
117
|
+
function handleLine(line, out) {
|
|
118
|
+
if (finished)
|
|
119
|
+
return;
|
|
120
|
+
let obj;
|
|
121
|
+
try {
|
|
122
|
+
obj = JSON.parse(line);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
failUndecodable(out);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (typeof obj !== "object" || obj === null || Array.isArray(obj)) {
|
|
129
|
+
failUndecodable(out);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
handleObject(obj, out);
|
|
133
|
+
}
|
|
134
|
+
function drain(completed, out) {
|
|
135
|
+
for (const line of completed)
|
|
136
|
+
handleLine(line, out);
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
feed(chunk) {
|
|
140
|
+
const out = [];
|
|
141
|
+
drain(lines.feed(utf8(chunk)), out);
|
|
142
|
+
return out;
|
|
143
|
+
},
|
|
144
|
+
end() {
|
|
145
|
+
const out = [];
|
|
146
|
+
drain(lines.feed(utf8(new Uint8Array(0))), out);
|
|
147
|
+
drain(lines.end(), out);
|
|
148
|
+
if (!finished) {
|
|
149
|
+
out.push({
|
|
150
|
+
type: "error",
|
|
151
|
+
error: (0, canonical_error_1.makeCanonicalError)("upstream", "Upstream stream ended without completing"),
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
return out;
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Canonical events → Ollama NDJSON (spec §3.3, §3.4): `message_start`
|
|
160
|
+
* produces nothing — Ollama opens its stream with the first content
|
|
161
|
+
* line. Each `text_delta` is one `done:false` line; a tool call's
|
|
162
|
+
* arguments are buffered by canonical `index` until `tool_call_end`
|
|
163
|
+
* (§3.1 rule 5), then written as one `message.tool_calls` line with
|
|
164
|
+
* the call `id` and `function.index` as recorded Ollama 0.40.11 sends
|
|
165
|
+
* them and the parsed arguments object (or the `_invalid_arguments`
|
|
166
|
+
* wrapper). `message_end` is the `done:true` line with `done_reason`
|
|
167
|
+
* (canonical `tool_call` → "stop", which is what Ollama itself reports
|
|
168
|
+
* for a tool-call answer) and the held usage counts; `error` is one
|
|
169
|
+
* `{"error":"…"}` line. Calls still open when `message_end` arrives
|
|
170
|
+
* flush in ascending canonical index as `message.tool_calls` lines
|
|
171
|
+
* just before the `done:true` line (one `logger.warn` naming the
|
|
172
|
+
* count); before `error` they are dropped — the error is terminal.
|
|
173
|
+
* A `tool_call_delta`/`tool_call_end` for an index that never had
|
|
174
|
+
* `tool_call_start` logs a warning instead of dropping silently.
|
|
175
|
+
* `message_end` and `error` are terminal: any event after them
|
|
176
|
+
* encodes to nothing.
|
|
177
|
+
*/
|
|
178
|
+
function createStreamEncoder(ctx) {
|
|
179
|
+
const createdAt = new Date().toISOString();
|
|
180
|
+
let finished = false;
|
|
181
|
+
let usage;
|
|
182
|
+
const calls = new Map();
|
|
183
|
+
const line = (obj) => `${JSON.stringify(obj)}\n`;
|
|
184
|
+
const chunk = (message) => line({
|
|
185
|
+
model: ctx.requestedModel,
|
|
186
|
+
created_at: createdAt,
|
|
187
|
+
message,
|
|
188
|
+
done: false,
|
|
189
|
+
});
|
|
190
|
+
const toolCallLine = (index, call) => {
|
|
191
|
+
const parsed = (0, tool_args_1.parseToolArguments)(call.args);
|
|
192
|
+
if (!parsed.valid) {
|
|
193
|
+
logger_1.logger.warn("Tool call arguments are not valid JSON; sending _invalid_arguments wrapper");
|
|
194
|
+
}
|
|
195
|
+
return chunk({
|
|
196
|
+
role: "assistant",
|
|
197
|
+
content: "",
|
|
198
|
+
tool_calls: [
|
|
199
|
+
{
|
|
200
|
+
id: call.id,
|
|
201
|
+
function: {
|
|
202
|
+
index,
|
|
203
|
+
name: call.name,
|
|
204
|
+
arguments: parsed.args,
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
});
|
|
209
|
+
};
|
|
210
|
+
const warnUnknownIndex = (type, index) => {
|
|
211
|
+
logger_1.logger.warn(`Ollama stream encoder: ${type} for unknown tool-call index ${index}; dropping`);
|
|
212
|
+
};
|
|
213
|
+
return {
|
|
214
|
+
contentType: "application/x-ndjson",
|
|
215
|
+
encode(event) {
|
|
216
|
+
if (finished)
|
|
217
|
+
return "";
|
|
218
|
+
switch (event.type) {
|
|
219
|
+
case "message_start":
|
|
220
|
+
return "";
|
|
221
|
+
case "text_delta":
|
|
222
|
+
return chunk({
|
|
223
|
+
role: "assistant",
|
|
224
|
+
content: event.text,
|
|
225
|
+
});
|
|
226
|
+
case "tool_call_start":
|
|
227
|
+
calls.set(event.index, {
|
|
228
|
+
id: event.id,
|
|
229
|
+
name: event.name,
|
|
230
|
+
args: "",
|
|
231
|
+
});
|
|
232
|
+
return "";
|
|
233
|
+
case "tool_call_delta": {
|
|
234
|
+
const call = calls.get(event.index);
|
|
235
|
+
if (call) {
|
|
236
|
+
call.args += event.argumentsDelta;
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
warnUnknownIndex(event.type, event.index);
|
|
240
|
+
}
|
|
241
|
+
return "";
|
|
242
|
+
}
|
|
243
|
+
case "tool_call_end": {
|
|
244
|
+
const call = calls.get(event.index);
|
|
245
|
+
if (!call) {
|
|
246
|
+
warnUnknownIndex(event.type, event.index);
|
|
247
|
+
return "";
|
|
248
|
+
}
|
|
249
|
+
calls.delete(event.index);
|
|
250
|
+
return toolCallLine(event.index, call);
|
|
251
|
+
}
|
|
252
|
+
case "usage":
|
|
253
|
+
usage = event.usage;
|
|
254
|
+
return "";
|
|
255
|
+
case "message_end": {
|
|
256
|
+
finished = true;
|
|
257
|
+
let out = "";
|
|
258
|
+
if (calls.size > 0) {
|
|
259
|
+
logger_1.logger.warn(`Ollama stream ended with ${calls.size} tool call(s) still open; flushing before done`);
|
|
260
|
+
const pending = [...calls.entries()].sort(([a], [b]) => a - b);
|
|
261
|
+
calls.clear();
|
|
262
|
+
for (const [index, call] of pending) {
|
|
263
|
+
out += toolCallLine(index, call);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
out += line({
|
|
267
|
+
model: ctx.requestedModel,
|
|
268
|
+
created_at: createdAt,
|
|
269
|
+
message: { role: "assistant", content: "" },
|
|
270
|
+
done: true,
|
|
271
|
+
done_reason: event.finishReason === "length" ? "length" : "stop",
|
|
272
|
+
total_duration: 0,
|
|
273
|
+
load_duration: 0,
|
|
274
|
+
prompt_eval_count: usage?.inputTokens ?? 0,
|
|
275
|
+
prompt_eval_duration: 0,
|
|
276
|
+
eval_count: usage?.outputTokens ?? 0,
|
|
277
|
+
eval_duration: 0,
|
|
278
|
+
});
|
|
279
|
+
return out;
|
|
280
|
+
}
|
|
281
|
+
case "error":
|
|
282
|
+
finished = true;
|
|
283
|
+
if (calls.size > 0) {
|
|
284
|
+
logger_1.logger.warn(`Ollama stream ended with ${calls.size} tool call(s) still open; dropping`);
|
|
285
|
+
calls.clear();
|
|
286
|
+
}
|
|
287
|
+
return line((0, ollama_adapter_1.fromCanonicalError)(event.error));
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
end() {
|
|
291
|
+
return "";
|
|
292
|
+
},
|
|
293
|
+
};
|
|
294
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.toCanonicalToolCalls = toCanonicalToolCalls;
|
|
4
|
+
exports.toCanonicalTools = toCanonicalTools;
|
|
5
|
+
exports.recordCallId = recordCallId;
|
|
6
|
+
exports.resolveCallId = resolveCallId;
|
|
7
|
+
exports.toOllamaToolCalls = toOllamaToolCalls;
|
|
8
|
+
exports.toOllamaTools = toOllamaTools;
|
|
9
|
+
exports.resolveToolName = resolveToolName;
|
|
10
|
+
const errors_1 = require("../../utils/errors");
|
|
11
|
+
const logger_1 = require("../../utils/logger");
|
|
12
|
+
const tool_args_1 = require("../../utils/tool-args");
|
|
13
|
+
// ============ DECODE: Ollama → Canonical ============
|
|
14
|
+
function toCanonicalToolCalls(toolCalls, ids) {
|
|
15
|
+
return toolCalls.map((tc) => {
|
|
16
|
+
const args = tc?.function?.arguments;
|
|
17
|
+
return {
|
|
18
|
+
id: typeof tc?.id === "string" ? tc.id : ids(),
|
|
19
|
+
type: "function",
|
|
20
|
+
function: {
|
|
21
|
+
name: tc?.function?.name ?? "",
|
|
22
|
+
// `arguments` is normally an object (JSON.stringify it); a
|
|
23
|
+
// string is already the JSON encoding and passes through
|
|
24
|
+
// verbatim — same rule as the stream decoder.
|
|
25
|
+
arguments: typeof args === "string" ? args : JSON.stringify(args ?? {}),
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
/** Ollama `tools[]` → canonical tools; any non-`function` type →
|
|
31
|
+
* ValidationError naming it rather than a silent drop. */
|
|
32
|
+
function toCanonicalTools(tools) {
|
|
33
|
+
return tools.map((t) => {
|
|
34
|
+
if (t.type !== "function") {
|
|
35
|
+
throw new errors_1.ValidationError(`Unsupported Ollama tool type "${String(t.type)}"`);
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
type: "function",
|
|
39
|
+
function: {
|
|
40
|
+
name: t.function?.name ?? "",
|
|
41
|
+
...(t.function?.description !== undefined && {
|
|
42
|
+
description: t.function.description,
|
|
43
|
+
}),
|
|
44
|
+
...(t.function?.parameters !== undefined && {
|
|
45
|
+
parameters: t.function.parameters,
|
|
46
|
+
}),
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
function recordCallId(pending, name, id) {
|
|
52
|
+
const queue = pending.get(name);
|
|
53
|
+
if (queue) {
|
|
54
|
+
queue.push(id);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
pending.set(name, [id]);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** Consume the oldest pending call id for `tool_name`. */
|
|
61
|
+
function resolveCallId(pending, name) {
|
|
62
|
+
return pending.get(name)?.shift();
|
|
63
|
+
}
|
|
64
|
+
// ============ ENCODE: Canonical → Ollama ============
|
|
65
|
+
function toOllamaToolCalls(toolCalls) {
|
|
66
|
+
return toolCalls.map((tc, index) => {
|
|
67
|
+
const parsed = (0, tool_args_1.parseToolArguments)(tc.function.arguments);
|
|
68
|
+
if (!parsed.valid) {
|
|
69
|
+
logger_1.logger.warn("Tool call arguments are not valid JSON; sending _invalid_arguments wrapper");
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
id: tc.id,
|
|
73
|
+
function: {
|
|
74
|
+
index,
|
|
75
|
+
name: tc.function.name,
|
|
76
|
+
arguments: parsed.args,
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
/** Canonical tools → Ollama `tools[]` (definitions pass through). */
|
|
82
|
+
function toOllamaTools(tools) {
|
|
83
|
+
if (!tools?.length)
|
|
84
|
+
return undefined;
|
|
85
|
+
return tools.map((t) => ({
|
|
86
|
+
type: "function",
|
|
87
|
+
function: {
|
|
88
|
+
name: t.function.name,
|
|
89
|
+
...(t.function.description !== undefined && {
|
|
90
|
+
description: t.function.description,
|
|
91
|
+
}),
|
|
92
|
+
...(t.function.parameters !== undefined && {
|
|
93
|
+
parameters: t.function.parameters,
|
|
94
|
+
}),
|
|
95
|
+
},
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
/** The `tool_name` for a canonical tool message: the name of the
|
|
99
|
+
* matching earlier assistant call, else a ValidationError naming
|
|
100
|
+
* the id only (Ollama carries names, not call ids). */
|
|
101
|
+
function resolveToolName(callNameById, toolCallId) {
|
|
102
|
+
const name = toolCallId !== undefined ? callNameById.get(toolCallId) : undefined;
|
|
103
|
+
if (name === undefined) {
|
|
104
|
+
throw new errors_1.ValidationError(`Ollama tool message has no matching tool call for tool_call_id "${toolCallId ?? ""}"`);
|
|
105
|
+
}
|
|
106
|
+
return name;
|
|
107
|
+
}
|