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.
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 +34 -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 +55 -7
@@ -0,0 +1,418 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.geminiAdapter = void 0;
4
+ exports.toCanonicalRequest = toCanonicalRequest;
5
+ exports.fromCanonicalRequest = fromCanonicalRequest;
6
+ exports.toCanonicalResponse = toCanonicalResponse;
7
+ exports.mapFinishReason = mapFinishReason;
8
+ exports.fromCanonicalResponse = fromCanonicalResponse;
9
+ exports.toCanonicalModelList = toCanonicalModelList;
10
+ exports.fromCanonicalModelList = fromCanonicalModelList;
11
+ exports.mapToGeminiFinishReason = mapToGeminiFinishReason;
12
+ exports.toCanonicalError = toCanonicalError;
13
+ exports.fromCanonicalError = fromCanonicalError;
14
+ const crypto_1 = require("crypto");
15
+ const canonical_error_1 = require("../../utils/canonical-error");
16
+ const errors_1 = require("../../utils/errors");
17
+ const ids_1 = require("../../utils/ids");
18
+ const gemini_stream_1 = require("./gemini.stream");
19
+ const gemini_tools_1 = require("./gemini.tools");
20
+ /**
21
+ * Gemini FinishReason values — duplicated locally so the SDK stays a
22
+ * type-only dependency. These are stable API constants (string enum
23
+ * values), not implementation details.
24
+ */
25
+ const FinishReason = {
26
+ STOP: "STOP",
27
+ MAX_TOKENS: "MAX_TOKENS",
28
+ SAFETY: "SAFETY",
29
+ RECITATION: "RECITATION",
30
+ BLOCKLIST: "BLOCKLIST",
31
+ PROHIBITED_CONTENT: "PROHIBITED_CONTENT",
32
+ SPII: "SPII",
33
+ IMAGE_SAFETY: "IMAGE_SAFETY",
34
+ };
35
+ exports.geminiAdapter = {
36
+ toCanonicalRequest,
37
+ fromCanonicalRequest,
38
+ toCanonicalResponse,
39
+ fromCanonicalResponse,
40
+ toCanonicalModelList,
41
+ fromCanonicalModelList,
42
+ toCanonicalError,
43
+ fromCanonicalError,
44
+ createStreamDecoder: gemini_stream_1.createStreamDecoder,
45
+ createStreamEncoder: gemini_stream_1.createStreamEncoder,
46
+ };
47
+ // ============ REQUEST: Gemini → Canonical ============
48
+ function toCanonicalRequest(request) {
49
+ const messages = [];
50
+ let systemPrompt;
51
+ const ids = (0, ids_1.createIdGenerator)();
52
+ // pending call ids per function name, for matching functionResponses
53
+ const pendingCalls = new Map();
54
+ // Extract system instruction
55
+ if (request.systemInstruction) {
56
+ systemPrompt = extractTextFromInstruction(request.systemInstruction);
57
+ }
58
+ // Convert contents to messages
59
+ for (const content of request.contents) {
60
+ const parts = content.parts ?? [];
61
+ const otherParts = parts.filter((p) => !(0, gemini_tools_1.isFunctionResponsePart)(p));
62
+ if (content.role === "model") {
63
+ const toolCalls = (0, gemini_tools_1.toCanonicalToolCalls)(otherParts, ids);
64
+ for (const call of toolCalls) {
65
+ (0, gemini_tools_1.recordCallId)(pendingCalls, call.function.name, call.id);
66
+ }
67
+ messages.push({
68
+ role: "assistant",
69
+ content: extractContentParts({
70
+ role: content.role,
71
+ parts: otherParts.filter((p) => !(0, gemini_tools_1.isFunctionCallPart)(p)),
72
+ }),
73
+ ...(toolCalls.length > 0 && { toolCalls }),
74
+ });
75
+ }
76
+ else {
77
+ messages.push(...(0, gemini_tools_1.toCanonicalToolMessages)(parts, pendingCalls));
78
+ if (otherParts.length > 0) {
79
+ messages.push({
80
+ role: "user",
81
+ content: extractContentParts({
82
+ role: content.role,
83
+ parts: otherParts,
84
+ }),
85
+ });
86
+ }
87
+ }
88
+ }
89
+ const result = {
90
+ model: "gemini-pro",
91
+ messages,
92
+ ...(systemPrompt && { systemPrompt }),
93
+ };
94
+ // Convert tool definitions
95
+ const tools = (0, gemini_tools_1.toCanonicalTools)(request.tools);
96
+ if (tools) {
97
+ result.tools = tools;
98
+ }
99
+ // Convert tool config
100
+ const toolChoice = (0, gemini_tools_1.toCanonicalToolChoice)(request.toolConfig);
101
+ if (toolChoice !== undefined) {
102
+ result.toolChoice = toolChoice;
103
+ }
104
+ // Convert generation config
105
+ if (request.generationConfig) {
106
+ const gc = request.generationConfig;
107
+ result.config = {
108
+ ...(gc.temperature !== undefined && { temperature: gc.temperature }),
109
+ ...(gc.maxOutputTokens !== undefined && {
110
+ maxTokens: gc.maxOutputTokens,
111
+ }),
112
+ ...(gc.topP !== undefined && { topP: gc.topP }),
113
+ ...(gc.topK !== undefined && { topK: gc.topK }),
114
+ ...(gc.stopSequences && { stopSequences: gc.stopSequences }),
115
+ ...(gc.candidateCount !== undefined && {
116
+ candidateCount: gc.candidateCount,
117
+ }),
118
+ };
119
+ }
120
+ return result;
121
+ }
122
+ function extractTextFromInstruction(instruction) {
123
+ if (!instruction)
124
+ return "";
125
+ if (typeof instruction === "string")
126
+ return instruction;
127
+ if ("text" in instruction && typeof instruction.text === "string") {
128
+ return instruction.text;
129
+ }
130
+ if ("parts" in instruction) {
131
+ return instruction.parts
132
+ .filter((p) => "text" in p)
133
+ .map((p) => p.text)
134
+ .join("");
135
+ }
136
+ return "";
137
+ }
138
+ function extractContentParts(content) {
139
+ if (!content.parts || content.parts.length === 0) {
140
+ return "";
141
+ }
142
+ // If all parts are text, join them
143
+ const allText = content.parts.every((p) => "text" in p);
144
+ if (allText) {
145
+ return content.parts
146
+ .filter((p) => "text" in p)
147
+ .map((p) => p.text)
148
+ .join("");
149
+ }
150
+ // Mixed content
151
+ return content.parts.map((part) => {
152
+ if ("text" in part) {
153
+ return { type: "text", text: part.text };
154
+ }
155
+ if ("inlineData" in part && part.inlineData) {
156
+ return {
157
+ type: "image",
158
+ image: {
159
+ data: part.inlineData.data,
160
+ mimeType: part.inlineData
161
+ .mimeType,
162
+ },
163
+ };
164
+ }
165
+ return { type: "text", text: "" };
166
+ });
167
+ }
168
+ // ============ REQUEST: Canonical → Gemini ============
169
+ function fromCanonicalRequest(request) {
170
+ // toolCallId → function name, resolved from earlier assistant messages
171
+ const callNames = new Map();
172
+ const contents = [];
173
+ for (const m of request.messages) {
174
+ if (m.role === "system")
175
+ continue;
176
+ if (m.role === "assistant") {
177
+ const calls = m.toolCalls ?? [];
178
+ let parts = convertToParts(m.content);
179
+ for (const call of calls) {
180
+ callNames.set(call.id, call.function.name);
181
+ }
182
+ if (calls.length > 0) {
183
+ parts = parts.filter((p) => !("text" in p && p.text === ""));
184
+ parts.push(...calls.map(gemini_tools_1.functionCallPart));
185
+ }
186
+ contents.push({ role: "model", parts });
187
+ }
188
+ else if (m.role === "tool") {
189
+ const name = m.toolCallId
190
+ ? callNames.get(m.toolCallId)
191
+ : undefined;
192
+ if (!name) {
193
+ throw new errors_1.ValidationError(`Tool result "${m.toolCallId ?? ""}" has no matching assistant tool call`);
194
+ }
195
+ (0, gemini_tools_1.appendFunctionResponsePart)(contents, (0, gemini_tools_1.functionResponsePart)(name, m.content));
196
+ }
197
+ else {
198
+ contents.push({
199
+ role: mapRoleToGemini(m.role),
200
+ parts: convertToParts(m.content),
201
+ });
202
+ }
203
+ }
204
+ const result = {
205
+ contents,
206
+ };
207
+ // Tool definitions; parameters filtered to Google's documented
208
+ // schema subset (spec §3.4)
209
+ const tools = (0, gemini_tools_1.toGeminiTools)(request.tools);
210
+ if (tools) {
211
+ result.tools = tools;
212
+ }
213
+ // Tool choice → functionCallingConfig
214
+ const toolConfig = (0, gemini_tools_1.toGeminiToolConfig)(request.toolChoice);
215
+ if (toolConfig) {
216
+ result.toolConfig = toolConfig;
217
+ }
218
+ // Add system instruction
219
+ const systemMsg = request.messages.find((m) => m.role === "system");
220
+ if (systemMsg || request.systemPrompt) {
221
+ const systemText = request.systemPrompt ||
222
+ (typeof systemMsg?.content === "string" ? systemMsg.content : "");
223
+ if (systemText) {
224
+ result.systemInstruction = {
225
+ role: "user",
226
+ parts: [{ text: systemText }],
227
+ };
228
+ }
229
+ }
230
+ // Add generation config
231
+ if (request.config) {
232
+ const c = request.config;
233
+ result.generationConfig = {
234
+ ...(c.temperature !== undefined && { temperature: c.temperature }),
235
+ ...(c.maxTokens !== undefined && { maxOutputTokens: c.maxTokens }),
236
+ ...(c.topP !== undefined && { topP: c.topP }),
237
+ ...(c.topK !== undefined && { topK: c.topK }),
238
+ ...(c.stopSequences && { stopSequences: c.stopSequences }),
239
+ ...(c.candidateCount !== undefined && {
240
+ candidateCount: c.candidateCount,
241
+ }),
242
+ };
243
+ }
244
+ return result;
245
+ }
246
+ function mapRoleToGemini(role) {
247
+ return role === "assistant" ? "model" : "user";
248
+ }
249
+ function convertToParts(content) {
250
+ if (typeof content === "string") {
251
+ return [{ text: content }];
252
+ }
253
+ return content.map((part) => {
254
+ if (part.type === "text") {
255
+ return { text: part.text || "" };
256
+ }
257
+ if (part.type === "image" && part.image) {
258
+ return {
259
+ inlineData: {
260
+ mimeType: part.image.mimeType,
261
+ data: part.image.data,
262
+ },
263
+ };
264
+ }
265
+ return { text: "" };
266
+ });
267
+ }
268
+ // ============ RESPONSE: Gemini → Canonical ============
269
+ function toCanonicalResponse(response, model, ids = (0, ids_1.createIdGenerator)()) {
270
+ const candidate = response.candidates?.[0];
271
+ const content = candidate?.content;
272
+ const parts = content?.parts ?? [];
273
+ const text = parts
274
+ .filter((p) => "text" in p)
275
+ .map((p) => p.text)
276
+ .join("");
277
+ const toolCalls = (0, gemini_tools_1.toCanonicalToolCalls)(parts, ids);
278
+ return {
279
+ id: `gemini-${(0, crypto_1.randomUUID)()}`,
280
+ model,
281
+ message: {
282
+ role: "assistant",
283
+ content: text,
284
+ ...(toolCalls.length > 0 && { toolCalls }),
285
+ },
286
+ finishReason: toolCalls.length > 0
287
+ ? "tool_call"
288
+ : mapFinishReason(candidate?.finishReason),
289
+ usage: response.usageMetadata
290
+ ? {
291
+ inputTokens: response.usageMetadata.promptTokenCount ?? 0,
292
+ outputTokens: response.usageMetadata.candidatesTokenCount ?? 0,
293
+ totalTokens: response.usageMetadata.totalTokenCount ?? 0,
294
+ }
295
+ : undefined,
296
+ };
297
+ }
298
+ function mapFinishReason(reason) {
299
+ switch (reason) {
300
+ case FinishReason.STOP:
301
+ case "STOP":
302
+ return "stop";
303
+ case FinishReason.MAX_TOKENS:
304
+ case "MAX_TOKENS":
305
+ return "length";
306
+ case FinishReason.SAFETY:
307
+ case "SAFETY":
308
+ case FinishReason.RECITATION:
309
+ case "RECITATION":
310
+ case FinishReason.BLOCKLIST:
311
+ case "BLOCKLIST":
312
+ case FinishReason.PROHIBITED_CONTENT:
313
+ case "PROHIBITED_CONTENT":
314
+ case FinishReason.SPII:
315
+ case "SPII":
316
+ case FinishReason.IMAGE_SAFETY:
317
+ case "IMAGE_SAFETY":
318
+ return "content_filter";
319
+ default:
320
+ return "stop";
321
+ }
322
+ }
323
+ // ============ RESPONSE: Canonical → Gemini ============
324
+ function fromCanonicalResponse(response) {
325
+ const content = typeof response.message.content === "string"
326
+ ? response.message.content
327
+ : "";
328
+ const toolCalls = response.message.toolCalls ?? [];
329
+ const parts = toolCalls.length > 0 && content === ""
330
+ ? toolCalls.map(gemini_tools_1.functionCallPart)
331
+ : [{ text: content }, ...toolCalls.map(gemini_tools_1.functionCallPart)];
332
+ return {
333
+ candidates: [
334
+ {
335
+ content: {
336
+ role: "model",
337
+ parts,
338
+ },
339
+ finishReason: mapToGeminiFinishReason(response.finishReason),
340
+ index: 0,
341
+ },
342
+ ],
343
+ usageMetadata: response.usage
344
+ ? {
345
+ promptTokenCount: response.usage.inputTokens,
346
+ candidatesTokenCount: response.usage.outputTokens,
347
+ totalTokenCount: response.usage.totalTokens,
348
+ }
349
+ : undefined,
350
+ };
351
+ }
352
+ function toCanonicalModelList(response) {
353
+ const res = response;
354
+ return {
355
+ models: (res.models ?? [])
356
+ .filter((m) => Boolean(m.name))
357
+ .map((m) => ({
358
+ // "models/gemini-2.0-flash" → "gemini-2.0-flash"
359
+ id: m.name.replace(/^models\//, ""),
360
+ displayName: m.displayName,
361
+ })),
362
+ };
363
+ }
364
+ function fromCanonicalModelList(list) {
365
+ return {
366
+ models: list.models.map((m) => ({
367
+ name: `models/${m.id}`,
368
+ displayName: m.displayName,
369
+ })),
370
+ };
371
+ }
372
+ function mapToGeminiFinishReason(reason) {
373
+ switch (reason) {
374
+ case "stop":
375
+ return FinishReason.STOP;
376
+ case "length":
377
+ return FinishReason.MAX_TOKENS;
378
+ case "content_filter":
379
+ return FinishReason.SAFETY;
380
+ default:
381
+ return FinishReason.STOP;
382
+ }
383
+ }
384
+ // ============ ERRORS ============
385
+ const GEMINI_ERROR_STATUS = {
386
+ invalid_request: "INVALID_ARGUMENT",
387
+ authentication: "UNAUTHENTICATED",
388
+ permission: "PERMISSION_DENIED",
389
+ not_found: "NOT_FOUND",
390
+ request_too_large: "RESOURCE_EXHAUSTED",
391
+ rate_limit: "RESOURCE_EXHAUSTED",
392
+ unsupported: "UNIMPLEMENTED",
393
+ upstream: "INTERNAL",
394
+ unavailable: "UNAVAILABLE",
395
+ timeout: "DEADLINE_EXCEEDED",
396
+ internal: "INTERNAL",
397
+ };
398
+ function toCanonicalError(status, body) {
399
+ const err = body?.error;
400
+ const message = typeof err?.message === "string" && err.message.length > 0
401
+ ? err.message
402
+ : `Upstream gemini returned ${status}`;
403
+ const details = err?.details;
404
+ const keyInvalid = Array.isArray(details) &&
405
+ details.some((d) => typeof d === "object" &&
406
+ d !== null &&
407
+ d.reason === "API_KEY_INVALID");
408
+ return (0, canonical_error_1.makeCanonicalError)(keyInvalid ? "authentication" : (0, canonical_error_1.kindFromStatus)(status), message);
409
+ }
410
+ function fromCanonicalError(err) {
411
+ return {
412
+ error: {
413
+ code: err.status,
414
+ message: err.message,
415
+ status: GEMINI_ERROR_STATUS[err.kind],
416
+ },
417
+ };
418
+ }
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ /**
3
+ * Filter a JSON Schema down to the subset the Gemini API documents for
4
+ * function-declaration `parameters` (and responseSchema), spec §3.4.
5
+ * Source: https://ai.google.dev/api/generate-content#Schema
6
+ * (read 2026-10-27) — the documented fields minus `default`, which the
7
+ * doc itself marks "accepted and ignored" (owner decision 2026-10-27).
8
+ * Returns a new object; the input is never mutated.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.toGeminiSchema = toGeminiSchema;
12
+ const ALLOWED_KEYS = new Set([
13
+ "type",
14
+ "format",
15
+ "title",
16
+ "description",
17
+ "nullable",
18
+ "enum",
19
+ "maxItems",
20
+ "minItems",
21
+ "properties",
22
+ "required",
23
+ "minProperties",
24
+ "maxProperties",
25
+ "minLength",
26
+ "maxLength",
27
+ "pattern",
28
+ "example",
29
+ "anyOf",
30
+ "propertyOrdering",
31
+ "items",
32
+ "minimum",
33
+ "maximum",
34
+ ]);
35
+ function isPlainObject(value) {
36
+ return typeof value === "object" && value !== null && !Array.isArray(value);
37
+ }
38
+ function toGeminiSchema(schema) {
39
+ const out = {};
40
+ for (const [key, value] of Object.entries(schema)) {
41
+ if (!ALLOWED_KEYS.has(key))
42
+ continue;
43
+ if (key === "properties" && isPlainObject(value)) {
44
+ const properties = {};
45
+ for (const [name, sub] of Object.entries(value)) {
46
+ properties[name] = isPlainObject(sub) ? toGeminiSchema(sub) : sub;
47
+ }
48
+ out[key] = properties;
49
+ }
50
+ else if (key === "items" && isPlainObject(value)) {
51
+ out[key] = toGeminiSchema(value);
52
+ }
53
+ else if (key === "anyOf" && Array.isArray(value)) {
54
+ out[key] = value.map((sub) => isPlainObject(sub) ? toGeminiSchema(sub) : sub);
55
+ }
56
+ else {
57
+ out[key] = value;
58
+ }
59
+ }
60
+ return out;
61
+ }