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.
Files changed (46) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +2 -0
  3. package/README.md +467 -1
  4. package/dist/app.js +40 -0
  5. package/dist/cli.js +203 -0
  6. package/dist/config/env-config.js +99 -0
  7. package/dist/index.js +60 -0
  8. package/dist/middleware/error-handler.js +158 -0
  9. package/dist/middleware/request-logger.js +66 -0
  10. package/dist/middleware/require-api-key.js +80 -0
  11. package/dist/middleware/resolve-source.js +28 -0
  12. package/dist/routes/proxy.js +74 -0
  13. package/dist/services/http-client.js +220 -0
  14. package/dist/services/proxy.service.js +329 -0
  15. package/dist/services/stream-pipeline.js +119 -0
  16. package/dist/services/upstream-errors.js +34 -0
  17. package/dist/transformers/adapters/anthropic.adapter.js +292 -0
  18. package/dist/transformers/adapters/anthropic.messages.js +261 -0
  19. package/dist/transformers/adapters/anthropic.stream.js +378 -0
  20. package/dist/transformers/adapters/gemini.adapter.js +418 -0
  21. package/dist/transformers/adapters/gemini.schema.js +61 -0
  22. package/dist/transformers/adapters/gemini.stream.js +341 -0
  23. package/dist/transformers/adapters/gemini.tools.js +246 -0
  24. package/dist/transformers/adapters/index.js +141 -0
  25. package/dist/transformers/adapters/ollama.adapter.js +288 -0
  26. package/dist/transformers/adapters/ollama.stream.js +294 -0
  27. package/dist/transformers/adapters/ollama.tools.js +107 -0
  28. package/dist/transformers/adapters/openai.adapter.js +460 -0
  29. package/dist/transformers/adapters/openai.stream.js +229 -0
  30. package/dist/transformers/index.js +110 -0
  31. package/dist/types/anthropic.types.js +2 -0
  32. package/dist/types/canonical.schema.js +160 -0
  33. package/dist/types/canonical.types.js +8 -0
  34. package/dist/types/gemini.types.js +2 -0
  35. package/dist/types/ollama.types.js +4 -0
  36. package/dist/types/openai.types.js +2 -0
  37. package/dist/utils/canonical-error.js +77 -0
  38. package/dist/utils/errors.js +95 -0
  39. package/dist/utils/ids.js +17 -0
  40. package/dist/utils/logger.js +21 -0
  41. package/dist/utils/model-map.js +39 -0
  42. package/dist/utils/sse.js +147 -0
  43. package/dist/utils/stream-reassemble.js +104 -0
  44. package/dist/utils/token-estimate.js +36 -0
  45. package/dist/utils/tool-args.js +26 -0
  46. package/package.json +54 -7
@@ -0,0 +1,460 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.openaiAdapter = void 0;
4
+ exports.toCanonicalRequest = toCanonicalRequest;
5
+ exports.fromCanonicalRequest = fromCanonicalRequest;
6
+ exports.toCanonicalResponse = toCanonicalResponse;
7
+ exports.fromCanonicalResponse = fromCanonicalResponse;
8
+ exports.toCanonicalModelList = toCanonicalModelList;
9
+ exports.fromCanonicalModelList = fromCanonicalModelList;
10
+ exports.mapToOpenaiFinishReason = mapToOpenaiFinishReason;
11
+ exports.toCanonicalError = toCanonicalError;
12
+ exports.fromCanonicalError = fromCanonicalError;
13
+ const crypto_1 = require("crypto");
14
+ const canonical_error_1 = require("../../utils/canonical-error");
15
+ const errors_1 = require("../../utils/errors");
16
+ const logger_1 = require("../../utils/logger");
17
+ const openai_stream_1 = require("./openai.stream");
18
+ exports.openaiAdapter = {
19
+ toCanonicalRequest,
20
+ fromCanonicalRequest,
21
+ toCanonicalResponse,
22
+ fromCanonicalResponse,
23
+ toCanonicalModelList,
24
+ fromCanonicalModelList,
25
+ toCanonicalError,
26
+ fromCanonicalError,
27
+ createStreamDecoder: openai_stream_1.createStreamDecoder,
28
+ createStreamEncoder: openai_stream_1.createStreamEncoder,
29
+ };
30
+ // ============ REQUEST: OpenAI → Canonical ============
31
+ function toCanonicalRequest(request) {
32
+ const messages = [];
33
+ let systemPrompt;
34
+ for (const msg of request.messages) {
35
+ if (msg.role === "system") {
36
+ systemPrompt = extractTextContent(msg);
37
+ }
38
+ messages.push(convertToCanonicalMessage(msg));
39
+ }
40
+ // Canonical has no field for `parallel_tool_calls` (a spec
41
+ // non-goal) — make the drop visible instead of silent, same as
42
+ // Anthropic's `disable_parallel_tool_use` (task 9).
43
+ if (request.parallel_tool_calls !== undefined) {
44
+ logger_1.logger.debug('Dropping request field "parallel_tool_calls" (no canonical equivalent)');
45
+ }
46
+ return {
47
+ model: request.model,
48
+ messages,
49
+ ...(systemPrompt && { systemPrompt }),
50
+ config: {
51
+ ...(request.temperature !== undefined &&
52
+ request.temperature !== null && { temperature: request.temperature }),
53
+ ...(request.max_tokens !== undefined &&
54
+ request.max_tokens !== null && { maxTokens: request.max_tokens }),
55
+ ...(request.top_p !== undefined &&
56
+ request.top_p !== null && { topP: request.top_p }),
57
+ ...(request.stop && {
58
+ stopSequences: Array.isArray(request.stop)
59
+ ? request.stop
60
+ : [request.stop],
61
+ }),
62
+ ...(request.n !== undefined &&
63
+ request.n !== null && { candidateCount: request.n }),
64
+ ...(request.seed !== undefined &&
65
+ request.seed !== null && { seed: request.seed }),
66
+ ...(request.frequency_penalty !== undefined &&
67
+ request.frequency_penalty !== null && {
68
+ frequencyPenalty: request.frequency_penalty,
69
+ }),
70
+ ...(request.presence_penalty !== undefined &&
71
+ request.presence_penalty !== null && {
72
+ presencePenalty: request.presence_penalty,
73
+ }),
74
+ },
75
+ ...(request.stream === true && { stream: true }),
76
+ ...(request.stream === false && { stream: false }),
77
+ ...(request.tools && { tools: toCanonicalTools(request.tools) }),
78
+ ...(request.tool_choice !== undefined && {
79
+ toolChoice: toCanonicalToolChoice(request.tool_choice),
80
+ }),
81
+ };
82
+ }
83
+ function toCanonicalTools(tools) {
84
+ return tools.map((tool) => {
85
+ if (tool.type !== "function" || !("function" in tool)) {
86
+ throw new errors_1.ValidationError(`Unsupported OpenAI tool type "${String(tool.type)}"`);
87
+ }
88
+ // `strict` has no canonical equivalent — dropped, but visibly.
89
+ if (tool.function.strict != null) {
90
+ logger_1.logger.debug('Dropping tool field "strict" (no canonical equivalent)');
91
+ }
92
+ return {
93
+ type: "function",
94
+ function: {
95
+ name: tool.function.name,
96
+ ...(tool.function.description !== undefined && {
97
+ description: tool.function.description,
98
+ }),
99
+ ...(tool.function.parameters !== undefined && {
100
+ parameters: tool.function.parameters,
101
+ }),
102
+ },
103
+ };
104
+ });
105
+ }
106
+ function toCanonicalToolChoice(choice) {
107
+ if (choice === "auto" || choice === "none" || choice === "required") {
108
+ return choice;
109
+ }
110
+ if (typeof choice === "object" &&
111
+ choice.type === "function" &&
112
+ typeof choice.function?.name === "string") {
113
+ return { type: "function", function: { name: choice.function.name } };
114
+ }
115
+ throw new errors_1.ValidationError(`Unsupported OpenAI tool_choice "${typeof choice === "object" ? String(choice.type) : String(choice)}"`);
116
+ }
117
+ function convertToCanonicalMessage(msg) {
118
+ const base = {
119
+ role: msg.role,
120
+ content: "",
121
+ };
122
+ if ("content" in msg && msg.content !== null) {
123
+ if (typeof msg.content === "string") {
124
+ base.content = msg.content;
125
+ }
126
+ else if (Array.isArray(msg.content)) {
127
+ base.content = msg.content.map(convertToCanonicalPart);
128
+ }
129
+ }
130
+ if ("tool_calls" in msg && msg.tool_calls) {
131
+ base.toolCalls = msg.tool_calls.map((tc) => {
132
+ if (tc.type !== "function" || !("function" in tc)) {
133
+ // e.g. type:"custom" — no canonical equivalent; 400 naming
134
+ // the type rather than silently filtering the call out.
135
+ throw new errors_1.ValidationError(`OpenAI tool call type "${String(tc.type)}" is not supported`);
136
+ }
137
+ return {
138
+ id: tc.id,
139
+ type: "function",
140
+ function: {
141
+ name: tc.function.name,
142
+ arguments: tc.function.arguments,
143
+ },
144
+ };
145
+ });
146
+ }
147
+ if ("tool_call_id" in msg && msg.tool_call_id) {
148
+ base.toolCallId = msg.tool_call_id;
149
+ }
150
+ if ("name" in msg && msg.name) {
151
+ base.name = msg.name;
152
+ }
153
+ return base;
154
+ }
155
+ function convertToCanonicalPart(part) {
156
+ if (part.type === "text" && part.text) {
157
+ return { type: "text", text: part.text };
158
+ }
159
+ if (part.type === "image_url" && part.image_url) {
160
+ const url = part.image_url.url;
161
+ if (url.startsWith("data:")) {
162
+ const [meta, data] = url.split(",");
163
+ const mimeType = (meta.split(":")[1]?.split(";")[0] ||
164
+ "image/png");
165
+ return {
166
+ type: "image",
167
+ image: { data, mimeType },
168
+ };
169
+ }
170
+ // URL-based images - store as text placeholder
171
+ return { type: "text", text: `[Image: ${url}]` };
172
+ }
173
+ return { type: "text", text: "" };
174
+ }
175
+ function extractTextContent(msg) {
176
+ if (!("content" in msg) || msg.content === null)
177
+ return "";
178
+ if (typeof msg.content === "string")
179
+ return msg.content;
180
+ if (Array.isArray(msg.content)) {
181
+ return msg.content
182
+ .filter((p) => p.type === "text")
183
+ .map((p) => p.text)
184
+ .join("\n");
185
+ }
186
+ return "";
187
+ }
188
+ // ============ REQUEST: Canonical → OpenAI ============
189
+ function fromCanonicalRequest(request) {
190
+ const messages = request.messages.map(convertFromCanonicalMessage);
191
+ const result = {
192
+ model: request.model,
193
+ messages,
194
+ };
195
+ if (request.config) {
196
+ const c = request.config;
197
+ if (c.temperature !== undefined)
198
+ result.temperature = c.temperature;
199
+ if (c.maxTokens !== undefined)
200
+ result.max_tokens = c.maxTokens;
201
+ if (c.topP !== undefined)
202
+ result.top_p = c.topP;
203
+ if (c.stopSequences && c.stopSequences.length > 0) {
204
+ result.stop = c.stopSequences;
205
+ }
206
+ if (c.candidateCount !== undefined)
207
+ result.n = c.candidateCount;
208
+ if (c.seed !== undefined)
209
+ result.seed = c.seed;
210
+ if (c.frequencyPenalty !== undefined) {
211
+ result.frequency_penalty = c.frequencyPenalty;
212
+ }
213
+ if (c.presencePenalty !== undefined) {
214
+ result.presence_penalty = c.presencePenalty;
215
+ }
216
+ }
217
+ if (request.tools && request.tools.length > 0) {
218
+ result.tools = request.tools.map((tool) => ({
219
+ type: "function",
220
+ function: {
221
+ name: tool.function.name,
222
+ ...(tool.function.description !== undefined && {
223
+ description: tool.function.description,
224
+ }),
225
+ ...(tool.function.parameters !== undefined && {
226
+ parameters: tool.function.parameters,
227
+ }),
228
+ },
229
+ }));
230
+ }
231
+ if (request.toolChoice !== undefined) {
232
+ result.tool_choice = request.toolChoice;
233
+ }
234
+ if (request.stream !== undefined) {
235
+ result.stream = request.stream;
236
+ }
237
+ if (request.stream === true) {
238
+ // Ask the upstream for a final usage chunk (spec §3.2).
239
+ result
240
+ .stream_options = { include_usage: true };
241
+ }
242
+ return result;
243
+ }
244
+ function convertFromCanonicalMessage(msg) {
245
+ const content = convertFromCanonicalContent(msg.content);
246
+ if (msg.role === "assistant") {
247
+ const assistantMsg = {
248
+ role: "assistant",
249
+ content: typeof content === "string" ? content : null,
250
+ };
251
+ if (msg.toolCalls && msg.toolCalls.length > 0) {
252
+ assistantMsg.tool_calls = msg.toolCalls.map((tc) => ({
253
+ id: tc.id,
254
+ type: "function",
255
+ function: {
256
+ name: tc.function.name,
257
+ arguments: tc.function.arguments,
258
+ },
259
+ }));
260
+ }
261
+ return assistantMsg;
262
+ }
263
+ if (msg.role === "tool") {
264
+ return {
265
+ role: "tool",
266
+ content: typeof content === "string" ? content : "",
267
+ tool_call_id: msg.toolCallId || "",
268
+ };
269
+ }
270
+ if (msg.role === "system") {
271
+ return {
272
+ role: "system",
273
+ content: typeof content === "string" ? content : "",
274
+ };
275
+ }
276
+ // User message
277
+ return { role: "user", content };
278
+ }
279
+ function convertFromCanonicalContent(content) {
280
+ if (typeof content === "string")
281
+ return content;
282
+ return content.map((part) => {
283
+ if (part.type === "text") {
284
+ return { type: "text", text: part.text || "" };
285
+ }
286
+ if (part.type === "image" && part.image) {
287
+ return {
288
+ type: "image_url",
289
+ image_url: {
290
+ url: `data:${part.image.mimeType};base64,${part.image.data}`,
291
+ },
292
+ };
293
+ }
294
+ return { type: "text", text: "" };
295
+ });
296
+ }
297
+ // ============ RESPONSE: OpenAI → Canonical ============
298
+ function toCanonicalResponse(response, model) {
299
+ const choice = response.choices[0];
300
+ const message = {
301
+ role: "assistant",
302
+ content: choice?.message?.content ?? "",
303
+ };
304
+ if (choice?.message?.tool_calls) {
305
+ message.toolCalls = choice.message.tool_calls.map((tc) => {
306
+ if (tc.type !== "function" || !("function" in tc)) {
307
+ // e.g. type:"custom" — an upstream wire shape canonical
308
+ // cannot express; error naming the type (kind "upstream",
309
+ // like the stream decoder's undecodable-chunk error) rather
310
+ // than returning an empty toolCalls.
311
+ throw new errors_1.UpstreamError(`Upstream openai sent unsupported tool call type "${String(tc.type)}"`);
312
+ }
313
+ return {
314
+ id: tc.id,
315
+ type: "function",
316
+ function: {
317
+ name: tc.function.name,
318
+ arguments: tc.function.arguments,
319
+ },
320
+ };
321
+ });
322
+ }
323
+ return {
324
+ id: response.id,
325
+ model: model || response.model,
326
+ message,
327
+ finishReason: mapFinishReason(choice?.finish_reason),
328
+ usage: response.usage
329
+ ? {
330
+ inputTokens: response.usage.prompt_tokens,
331
+ outputTokens: response.usage.completion_tokens,
332
+ totalTokens: response.usage.total_tokens,
333
+ }
334
+ : undefined,
335
+ created: response.created,
336
+ };
337
+ }
338
+ function mapFinishReason(reason) {
339
+ switch (reason) {
340
+ case "stop":
341
+ return "stop";
342
+ case "length":
343
+ return "length";
344
+ case "tool_calls":
345
+ case "function_call":
346
+ return "tool_call";
347
+ case "content_filter":
348
+ return "content_filter";
349
+ default:
350
+ return "stop";
351
+ }
352
+ }
353
+ // ============ RESPONSE: Canonical → OpenAI ============
354
+ function fromCanonicalResponse(response) {
355
+ const toolCalls = response.message.toolCalls?.map((tc) => ({
356
+ id: tc.id,
357
+ type: "function",
358
+ function: {
359
+ name: tc.function.name,
360
+ arguments: tc.function.arguments,
361
+ },
362
+ }));
363
+ return {
364
+ id: response.id || `chatcmpl-${(0, crypto_1.randomUUID)()}`,
365
+ object: "chat.completion",
366
+ created: response.created || Math.floor(Date.now() / 1000),
367
+ model: response.model,
368
+ choices: [
369
+ {
370
+ index: 0,
371
+ message: {
372
+ role: "assistant",
373
+ content: typeof response.message.content === "string"
374
+ ? response.message.content
375
+ : "",
376
+ refusal: null,
377
+ ...(toolCalls && { tool_calls: toolCalls }),
378
+ },
379
+ finish_reason: mapToOpenaiFinishReason(response.finishReason),
380
+ logprobs: null,
381
+ },
382
+ ],
383
+ usage: response.usage
384
+ ? {
385
+ prompt_tokens: response.usage.inputTokens,
386
+ completion_tokens: response.usage.outputTokens,
387
+ total_tokens: response.usage.totalTokens,
388
+ }
389
+ : undefined,
390
+ };
391
+ }
392
+ function toCanonicalModelList(response) {
393
+ const res = response;
394
+ return {
395
+ models: (res.data ?? [])
396
+ .filter((m) => Boolean(m.id))
397
+ .map((m) => ({
398
+ id: m.id,
399
+ created: m.created,
400
+ ownedBy: m.owned_by,
401
+ })),
402
+ };
403
+ }
404
+ function fromCanonicalModelList(list) {
405
+ return {
406
+ object: "list",
407
+ data: list.models.map((m) => ({
408
+ id: m.id,
409
+ object: "model",
410
+ created: m.created ?? 0,
411
+ owned_by: m.ownedBy ?? "babelgate",
412
+ })),
413
+ };
414
+ }
415
+ function mapToOpenaiFinishReason(reason) {
416
+ switch (reason) {
417
+ case "stop":
418
+ return "stop";
419
+ case "length":
420
+ return "length";
421
+ case "tool_call":
422
+ return "tool_calls";
423
+ case "content_filter":
424
+ return "content_filter";
425
+ default:
426
+ return "stop";
427
+ }
428
+ }
429
+ // ============ ERRORS ============
430
+ const OPENAI_ERROR_TYPE = {
431
+ invalid_request: "invalid_request_error",
432
+ authentication: "authentication_error",
433
+ permission: "permission_error",
434
+ not_found: "not_found_error",
435
+ request_too_large: "invalid_request_error",
436
+ rate_limit: "rate_limit_error",
437
+ unsupported: "invalid_request_error",
438
+ upstream: "api_error",
439
+ unavailable: "service_unavailable_error",
440
+ timeout: "api_error",
441
+ internal: "api_error",
442
+ };
443
+ function toCanonicalError(status, body) {
444
+ const err = body?.error;
445
+ const message = typeof err?.message === "string" && err.message.length > 0
446
+ ? err.message
447
+ : `Upstream openai returned ${status}`;
448
+ return (0, canonical_error_1.makeCanonicalError)((0, canonical_error_1.kindFromStatus)(status), message);
449
+ }
450
+ function fromCanonicalError(err) {
451
+ return {
452
+ error: {
453
+ message: err.message,
454
+ type: OPENAI_ERROR_TYPE[err.kind],
455
+ param: null,
456
+ code: err.kind,
457
+ },
458
+ };
459
+ }
460
+ // Streaming lives in ./openai.stream — re-exported on the adapter above.
@@ -0,0 +1,229 @@
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 sse_1 = require("../../utils/sse");
8
+ const openai_adapter_1 = require("./openai.adapter");
9
+ const OPENAI_FINISH = {
10
+ stop: "stop",
11
+ length: "length",
12
+ tool_calls: "tool_call",
13
+ function_call: "tool_call",
14
+ content_filter: "content_filter",
15
+ };
16
+ /**
17
+ * OpenAI SSE → canonical events (spec §3.3): every `data:` line carries a
18
+ * complete chat.completion.chunk JSON payload, `data: [DONE]` ends the
19
+ * stream. Lines are handled individually — the blank-line event
20
+ * separators real OpenAI sends are just ignored, so recordings without
21
+ * them decode identically. Tool calls are re-keyed from wire
22
+ * `tool_calls[].index` to canonical indices in first-appearance order; a
23
+ * call closes when the finish chunk arrives (or at end-of-stream). The
24
+ * `choices: []` usage chunk is the usage event. A stream that ends
25
+ * without `[DONE]` ends in an `error` event.
26
+ */
27
+ function createStreamDecoder(model, ids = (0, ids_1.createIdGenerator)()) {
28
+ const utf8 = (0, sse_1.createUtf8Decoder)();
29
+ const lines = (0, sse_1.createLineSplitter)();
30
+ let started = false;
31
+ let finished = false;
32
+ let finish;
33
+ let usage;
34
+ let nextIndex = 0;
35
+ const callsByWire = new Map();
36
+ const openCalls = [];
37
+ function closeAll(out) {
38
+ for (const index of openCalls.splice(0)) {
39
+ out.push({ type: "tool_call_end", index });
40
+ }
41
+ }
42
+ function handleChunk(chunk, out) {
43
+ if (chunk.error) {
44
+ // In-stream error object (spec §3.3): ends the stream.
45
+ out.push({ type: "error", error: (0, openai_adapter_1.toCanonicalError)(502, chunk) });
46
+ finished = true;
47
+ return;
48
+ }
49
+ if (!started) {
50
+ started = true;
51
+ out.push({
52
+ type: "message_start",
53
+ id: typeof chunk.id === "string" ? chunk.id : ids(),
54
+ model: typeof chunk.model === "string" ? chunk.model : model,
55
+ });
56
+ }
57
+ for (const choice of chunk.choices ?? []) {
58
+ const delta = choice?.delta;
59
+ if (typeof delta?.content === "string" && delta.content.length > 0) {
60
+ out.push({ type: "text_delta", text: delta.content });
61
+ }
62
+ for (const tc of delta?.tool_calls ?? []) {
63
+ const wireIndex = typeof tc.index === "number" ? tc.index : 0;
64
+ let index = callsByWire.get(wireIndex);
65
+ if (index === undefined) {
66
+ index = nextIndex++;
67
+ callsByWire.set(wireIndex, index);
68
+ openCalls.push(index);
69
+ out.push({
70
+ type: "tool_call_start",
71
+ index,
72
+ id: typeof tc.id === "string" ? tc.id : ids(),
73
+ name: tc.function?.name ?? "",
74
+ });
75
+ }
76
+ if (typeof tc.function?.arguments === "string" &&
77
+ tc.function.arguments.length > 0) {
78
+ out.push({
79
+ type: "tool_call_delta",
80
+ index,
81
+ argumentsDelta: tc.function.arguments,
82
+ });
83
+ }
84
+ }
85
+ if (choice?.finish_reason && finish === undefined) {
86
+ finish = OPENAI_FINISH[choice.finish_reason] ?? "unknown";
87
+ closeAll(out);
88
+ }
89
+ }
90
+ if (chunk.usage && usage === undefined) {
91
+ usage = {
92
+ inputTokens: chunk.usage.prompt_tokens ?? 0,
93
+ outputTokens: chunk.usage.completion_tokens ?? 0,
94
+ totalTokens: chunk.usage.total_tokens ??
95
+ (chunk.usage.prompt_tokens ?? 0) +
96
+ (chunk.usage.completion_tokens ?? 0),
97
+ };
98
+ }
99
+ }
100
+ function handleLine(line, out) {
101
+ if (finished || !line.startsWith("data:"))
102
+ return;
103
+ const data = line.slice(5).replace(/^ /, "");
104
+ if (data === "[DONE]") {
105
+ finished = true;
106
+ closeAll(out);
107
+ if (usage)
108
+ out.push({ type: "usage", usage });
109
+ out.push({ type: "message_end", finishReason: finish ?? "unknown" });
110
+ return;
111
+ }
112
+ if (data === "")
113
+ return;
114
+ let chunk;
115
+ try {
116
+ chunk = JSON.parse(data);
117
+ }
118
+ catch {
119
+ out.push({
120
+ type: "error",
121
+ error: (0, canonical_error_1.makeCanonicalError)("upstream", "Upstream sent an undecodable stream chunk"),
122
+ });
123
+ finished = true;
124
+ return;
125
+ }
126
+ handleChunk(chunk, out);
127
+ }
128
+ function drain(completed, out) {
129
+ for (const line of completed)
130
+ handleLine(line, out);
131
+ }
132
+ return {
133
+ feed(chunk) {
134
+ const out = [];
135
+ drain(lines.feed(utf8(chunk)), out);
136
+ return out;
137
+ },
138
+ end() {
139
+ const out = [];
140
+ drain(lines.feed(utf8(new Uint8Array(0))), out);
141
+ drain(lines.end(), out);
142
+ if (!finished) {
143
+ closeAll(out);
144
+ out.push({
145
+ type: "error",
146
+ error: (0, canonical_error_1.makeCanonicalError)("upstream", "Upstream stream ended without completing"),
147
+ });
148
+ }
149
+ return out;
150
+ },
151
+ };
152
+ }
153
+ /**
154
+ * Canonical events → OpenAI SSE (spec §3.3): text deltas and tool-call
155
+ * pieces become chat.completion.chunk data lines; a usage event becomes
156
+ * the `choices: []` usage chunk, but only when the client asked for it
157
+ * (`stream_options.include_usage`); an error event is a
158
+ * `data: {"error":{…}}` line. `end()` writes `data: [DONE]`.
159
+ */
160
+ function createStreamEncoder(ctx) {
161
+ const includeUsage = ctx.sourceRequest.stream_options?.include_usage === true;
162
+ const created = Math.floor(Date.now() / 1000);
163
+ let id = "chatcmpl-babelgate";
164
+ const chunk = (delta, finishReason = null) => (0, sse_1.formatSse)(JSON.stringify({
165
+ id,
166
+ object: "chat.completion.chunk",
167
+ created,
168
+ model: ctx.requestedModel,
169
+ choices: [
170
+ { index: 0, delta, logprobs: null, finish_reason: finishReason },
171
+ ],
172
+ }));
173
+ return {
174
+ contentType: "text/event-stream",
175
+ encode(event) {
176
+ switch (event.type) {
177
+ case "message_start":
178
+ id = event.id;
179
+ return chunk({ role: "assistant", content: "" });
180
+ case "text_delta":
181
+ return chunk({ content: event.text });
182
+ case "tool_call_start":
183
+ return chunk({
184
+ tool_calls: [
185
+ {
186
+ index: event.index,
187
+ id: event.id,
188
+ type: "function",
189
+ function: { name: event.name, arguments: "" },
190
+ },
191
+ ],
192
+ });
193
+ case "tool_call_delta":
194
+ return chunk({
195
+ tool_calls: [
196
+ {
197
+ index: event.index,
198
+ function: { arguments: event.argumentsDelta },
199
+ },
200
+ ],
201
+ });
202
+ case "tool_call_end":
203
+ return "";
204
+ case "usage":
205
+ if (!includeUsage)
206
+ return "";
207
+ return (0, sse_1.formatSse)(JSON.stringify({
208
+ id,
209
+ object: "chat.completion.chunk",
210
+ created,
211
+ model: ctx.requestedModel,
212
+ choices: [],
213
+ usage: {
214
+ prompt_tokens: event.usage.inputTokens,
215
+ completion_tokens: event.usage.outputTokens,
216
+ total_tokens: event.usage.totalTokens,
217
+ },
218
+ }));
219
+ case "message_end":
220
+ return chunk({}, (0, openai_adapter_1.mapToOpenaiFinishReason)(event.finishReason));
221
+ case "error":
222
+ return (0, sse_1.formatSse)(JSON.stringify((0, openai_adapter_1.fromCanonicalError)(event.error)));
223
+ }
224
+ },
225
+ end() {
226
+ return (0, sse_1.formatSse)("[DONE]");
227
+ },
228
+ };
229
+ }