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,292 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.anthropicAdapter = 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.mapToAnthropicStopReason = mapToAnthropicStopReason;
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 logger_1 = require("../../utils/logger");
17
+ const anthropic_messages_1 = require("./anthropic.messages");
18
+ const anthropic_stream_1 = require("./anthropic.stream");
19
+ exports.anthropicAdapter = {
20
+ toCanonicalRequest,
21
+ fromCanonicalRequest,
22
+ toCanonicalResponse,
23
+ fromCanonicalResponse,
24
+ toCanonicalModelList,
25
+ fromCanonicalModelList,
26
+ toCanonicalError,
27
+ fromCanonicalError,
28
+ createStreamDecoder: anthropic_stream_1.createStreamDecoder,
29
+ createStreamEncoder: anthropic_stream_1.createStreamEncoder,
30
+ };
31
+ // ============ REQUEST: Anthropic → Canonical ============
32
+ /**
33
+ * Claude Code fields with no canonical equivalent are dropped safely
34
+ * (spec §3.5) — a debug line names the dropped thing, never its content.
35
+ */
36
+ function logDropped(request) {
37
+ if (request.thinking !== undefined) {
38
+ logger_1.logger.debug('Dropping request field "thinking" (no canonical equivalent)');
39
+ }
40
+ if (request.metadata !== undefined) {
41
+ logger_1.logger.debug('Dropping request field "metadata" (no canonical equivalent)');
42
+ }
43
+ if (hasCacheControl(request)) {
44
+ logger_1.logger.debug('Dropping "cache_control" fields (no canonical equivalent)');
45
+ }
46
+ }
47
+ /** cache_control may sit on any block, tool or system part (§3.5). */
48
+ function hasCacheControl(value) {
49
+ if (Array.isArray(value))
50
+ return value.some(hasCacheControl);
51
+ if (value !== null && typeof value === "object") {
52
+ const obj = value;
53
+ if ("cache_control" in obj)
54
+ return true;
55
+ return Object.values(obj).some(hasCacheControl);
56
+ }
57
+ return false;
58
+ }
59
+ function toCanonicalRequest(request) {
60
+ logDropped(request);
61
+ const messages = request.messages.flatMap((m) => (0, anthropic_messages_1.toCanonicalMessage)(m));
62
+ // §3.5: a system array's text blocks join with a blank line.
63
+ const systemPrompt = !request.system
64
+ ? undefined
65
+ : typeof request.system === "string"
66
+ ? request.system
67
+ : request.system
68
+ .filter((b) => b.type === "text")
69
+ .map((b) => b.text)
70
+ .join("\n\n");
71
+ // Add system as first message if present
72
+ if (systemPrompt !== undefined) {
73
+ messages.unshift({
74
+ role: "system",
75
+ content: systemPrompt,
76
+ });
77
+ }
78
+ const result = {
79
+ model: request.model,
80
+ messages,
81
+ // Canonical treats absent and stream:false identically — only an
82
+ // explicit true turns streaming on.
83
+ ...(request.stream === true && { stream: true }),
84
+ ...(request.tools && { tools: request.tools.map(anthropic_messages_1.toCanonicalTool) }),
85
+ ...(request.tool_choice && {
86
+ toolChoice: (0, anthropic_messages_1.toCanonicalToolChoice)(request.tool_choice),
87
+ }),
88
+ ...(systemPrompt !== undefined && { systemPrompt }),
89
+ config: {
90
+ maxTokens: request.max_tokens,
91
+ ...(request.temperature !== undefined && {
92
+ temperature: request.temperature,
93
+ }),
94
+ ...(request.top_p !== undefined && { topP: request.top_p }),
95
+ ...(request.top_k !== undefined && { topK: request.top_k }),
96
+ ...(request.stop_sequences && { stopSequences: request.stop_sequences }),
97
+ },
98
+ };
99
+ return result;
100
+ }
101
+ // ============ REQUEST: Canonical → Anthropic ============
102
+ function fromCanonicalRequest(request) {
103
+ const nonSystem = request.messages.filter((m) => m.role !== "system");
104
+ const messages = [];
105
+ for (let i = 0; i < nonSystem.length; i++) {
106
+ const m = nonSystem[i];
107
+ if (m.role === "tool") {
108
+ // Merge the run of consecutive tool messages into ONE user
109
+ // message of tool_result blocks (spec §3.4).
110
+ const results = [];
111
+ while (i < nonSystem.length && nonSystem[i].role === "tool") {
112
+ results.push((0, anthropic_messages_1.toToolResultBlock)(nonSystem[i]));
113
+ i++;
114
+ }
115
+ i--;
116
+ messages.push({ role: "user", content: results });
117
+ continue;
118
+ }
119
+ messages.push((0, anthropic_messages_1.toMessageParam)(m));
120
+ }
121
+ const systemPrompt = request.systemPrompt ||
122
+ request.messages.find((m) => m.role === "system")?.content;
123
+ const result = {
124
+ model: request.model,
125
+ messages,
126
+ max_tokens: request.config?.maxTokens ?? 4096,
127
+ ...(systemPrompt &&
128
+ typeof systemPrompt === "string" && { system: systemPrompt }),
129
+ ...(request.config?.temperature !== undefined && {
130
+ temperature: request.config.temperature,
131
+ }),
132
+ ...(request.config?.topP !== undefined && { top_p: request.config.topP }),
133
+ ...(request.config?.topK !== undefined && { top_k: request.config.topK }),
134
+ ...(request.config?.stopSequences && {
135
+ stop_sequences: request.config.stopSequences,
136
+ }),
137
+ ...(request.tools && { tools: request.tools.map(anthropic_messages_1.toAnthropicTool) }),
138
+ ...(request.toolChoice && {
139
+ tool_choice: (0, anthropic_messages_1.toAnthropicToolChoice)(request.toolChoice),
140
+ }),
141
+ };
142
+ if (request.stream !== undefined) {
143
+ result.stream = request.stream;
144
+ }
145
+ return result;
146
+ }
147
+ // ============ RESPONSE: Anthropic → Canonical ============
148
+ function toCanonicalResponse(response, model) {
149
+ const textContent = response.content
150
+ .filter((block) => block.type === "text")
151
+ .map((block) => block.text)
152
+ .join("");
153
+ const toolCalls = response.content
154
+ .filter((block) => block.type === "tool_use")
155
+ .map((block) => ({
156
+ id: block.id,
157
+ type: "function",
158
+ function: {
159
+ name: block.name,
160
+ arguments: JSON.stringify(block.input ?? {}),
161
+ },
162
+ }));
163
+ return {
164
+ id: response.id || `anthropic-${(0, crypto_1.randomUUID)()}`,
165
+ model,
166
+ message: {
167
+ role: "assistant",
168
+ content: textContent,
169
+ ...(toolCalls.length > 0 && { toolCalls }),
170
+ },
171
+ finishReason: mapFinishReason(response.stop_reason),
172
+ usage: {
173
+ inputTokens: response.usage.input_tokens,
174
+ outputTokens: response.usage.output_tokens,
175
+ totalTokens: response.usage.input_tokens + response.usage.output_tokens,
176
+ },
177
+ };
178
+ }
179
+ function mapFinishReason(reason) {
180
+ switch (reason) {
181
+ case "end_turn":
182
+ return "stop";
183
+ case "max_tokens":
184
+ return "length";
185
+ case "tool_use":
186
+ return "tool_call";
187
+ default:
188
+ return "stop";
189
+ }
190
+ }
191
+ // ============ RESPONSE: Canonical → Anthropic ============
192
+ function fromCanonicalResponse(response) {
193
+ const text = typeof response.message.content === "string"
194
+ ? response.message.content
195
+ : "";
196
+ // Text comes before tool_use blocks (spec §3.4).
197
+ const content = [];
198
+ if (text) {
199
+ content.push({ type: "text", text });
200
+ }
201
+ for (const tc of response.message.toolCalls ?? []) {
202
+ content.push((0, anthropic_messages_1.toToolUseBlock)(tc));
203
+ }
204
+ if (content.length === 0) {
205
+ content.push({ type: "text", text: "" });
206
+ }
207
+ return {
208
+ id: response.id || `msg_${(0, crypto_1.randomUUID)()}`,
209
+ type: "message",
210
+ role: "assistant",
211
+ content: content,
212
+ model: response.model,
213
+ stop_reason: mapToAnthropicStopReason(response.finishReason),
214
+ stop_sequence: null,
215
+ usage: {
216
+ input_tokens: response.usage?.inputTokens ?? 0,
217
+ output_tokens: response.usage?.outputTokens ?? 0,
218
+ },
219
+ };
220
+ }
221
+ function toCanonicalModelList(response) {
222
+ const res = response;
223
+ return {
224
+ models: (res.data ?? [])
225
+ .filter((m) => Boolean(m.id))
226
+ .map((m) => ({
227
+ id: m.id,
228
+ displayName: m.display_name,
229
+ // ISO timestamp → Unix seconds
230
+ created: m.created_at
231
+ ? Math.floor(Date.parse(m.created_at) / 1000)
232
+ : undefined,
233
+ })),
234
+ };
235
+ }
236
+ function fromCanonicalModelList(list) {
237
+ return {
238
+ data: list.models.map((m) => ({
239
+ id: m.id,
240
+ display_name: m.displayName,
241
+ created_at: m.created
242
+ ? new Date(m.created * 1000).toISOString()
243
+ : undefined,
244
+ type: "model",
245
+ })),
246
+ has_more: false,
247
+ };
248
+ }
249
+ function mapToAnthropicStopReason(reason) {
250
+ switch (reason) {
251
+ case "stop":
252
+ return "end_turn";
253
+ case "length":
254
+ return "max_tokens";
255
+ case "tool_call":
256
+ return "tool_use";
257
+ default:
258
+ return "end_turn";
259
+ }
260
+ }
261
+ // ============ ERRORS ============
262
+ const ANTHROPIC_ERROR_TYPE = {
263
+ invalid_request: "invalid_request_error",
264
+ authentication: "authentication_error",
265
+ permission: "permission_error",
266
+ not_found: "not_found_error",
267
+ request_too_large: "request_too_large",
268
+ rate_limit: "rate_limit_error",
269
+ unsupported: "api_error",
270
+ upstream: "api_error",
271
+ unavailable: "overloaded_error",
272
+ timeout: "timeout_error",
273
+ internal: "api_error",
274
+ };
275
+ function toCanonicalError(status, body) {
276
+ const err = body?.error;
277
+ const message = typeof err?.message === "string" && err.message.length > 0
278
+ ? err.message
279
+ : `Upstream anthropic returned ${status}`;
280
+ return (0, canonical_error_1.makeCanonicalError)((0, canonical_error_1.kindFromStatus)(status), message);
281
+ }
282
+ function fromCanonicalError(err) {
283
+ return {
284
+ type: "error",
285
+ error: {
286
+ type: ANTHROPIC_ERROR_TYPE[err.kind],
287
+ message: err.message,
288
+ },
289
+ };
290
+ }
291
+ // Message/tool conversion lives in ./anthropic.messages; streaming in
292
+ // ./anthropic.stream — re-exported on the adapter above.
@@ -0,0 +1,261 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toCanonicalMessage = toCanonicalMessage;
4
+ exports.toCanonicalTool = toCanonicalTool;
5
+ exports.toCanonicalToolChoice = toCanonicalToolChoice;
6
+ exports.toMessageParam = toMessageParam;
7
+ exports.toToolResultBlock = toToolResultBlock;
8
+ exports.toToolUseBlock = toToolUseBlock;
9
+ exports.toAnthropicTool = toAnthropicTool;
10
+ exports.toAnthropicToolChoice = toAnthropicToolChoice;
11
+ const tool_args_1 = require("../../utils/tool-args");
12
+ const errors_1 = require("../../utils/errors");
13
+ const logger_1 = require("../../utils/logger");
14
+ // ============ Anthropic → Canonical ============
15
+ /**
16
+ * One Anthropic message → one or more canonical messages (spec §3.4):
17
+ * a user message's tool_result blocks become one canonical tool message
18
+ * each (in order), followed by a user message for any remaining
19
+ * text/image blocks. Assistant tool_use blocks become toolCalls.
20
+ */
21
+ function toCanonicalMessage(m) {
22
+ if (typeof m.content === "string" || !Array.isArray(m.content)) {
23
+ return [
24
+ {
25
+ role: m.role,
26
+ content: typeof m.content === "string" ? m.content : "",
27
+ },
28
+ ];
29
+ }
30
+ const blocks = m.content;
31
+ const toolResults = blocks.filter((b) => b.type === "tool_result");
32
+ const toolUses = blocks.filter((b) => b.type === "tool_use");
33
+ const rest = blocks.filter((b) => b.type !== "tool_result" && b.type !== "tool_use");
34
+ if (m.role === "user" && toolResults.length > 0) {
35
+ const out = toolResults.map((r) => ({
36
+ role: "tool",
37
+ toolCallId: r.tool_use_id,
38
+ content: toolResultContentToString(r.content),
39
+ ...(r.is_error === true && { isError: true }),
40
+ }));
41
+ if (rest.length > 0) {
42
+ out.push({ role: "user", content: extractContent(rest) });
43
+ }
44
+ return out;
45
+ }
46
+ const msg = {
47
+ role: m.role,
48
+ content: extractContent(rest),
49
+ };
50
+ if (toolUses.length > 0) {
51
+ msg.toolCalls = toolUses.map(toolUseToCanonical);
52
+ }
53
+ return [msg];
54
+ }
55
+ /**
56
+ * tool_result.content is a string or an array of blocks (spec §3.4). Text
57
+ * blocks join with "\n"; canonical tool results carry no images, so any
58
+ * other block type is a 400 naming it — never a silent drop.
59
+ */
60
+ function toolResultContentToString(content) {
61
+ if (content === undefined) {
62
+ return "";
63
+ }
64
+ if (typeof content === "string") {
65
+ return content;
66
+ }
67
+ return content
68
+ .map((block) => {
69
+ if (block.type === "text") {
70
+ return block.text;
71
+ }
72
+ throw new errors_1.ValidationError(`Unsupported content block type "${block.type}" in tool_result`);
73
+ })
74
+ .join("\n");
75
+ }
76
+ function toolUseToCanonical(block) {
77
+ return {
78
+ id: block.id,
79
+ type: "function",
80
+ function: {
81
+ name: block.name,
82
+ arguments: JSON.stringify(block.input ?? {}),
83
+ },
84
+ };
85
+ }
86
+ function toCanonicalTool(tool) {
87
+ if (!("name" in tool)) {
88
+ // Server-side toolsets have no canonical equivalent — 400 naming
89
+ // the type rather than silently dropping the declaration.
90
+ throw new errors_1.ValidationError(`Unsupported tool type "${tool.type}" in tools[]`);
91
+ }
92
+ const fn = { name: tool.name };
93
+ if ("description" in tool && typeof tool.description === "string") {
94
+ fn.description = tool.description;
95
+ }
96
+ if ("input_schema" in tool && tool.input_schema) {
97
+ fn.parameters = tool.input_schema;
98
+ }
99
+ return { type: "function", function: fn };
100
+ }
101
+ // auto→auto, any→required, tool{name}→function choice, none→none (§3.4).
102
+ function toCanonicalToolChoice(choice) {
103
+ if ("disable_parallel_tool_use" in choice &&
104
+ choice.disable_parallel_tool_use !== undefined) {
105
+ logger_1.logger.debug('Dropping tool_choice field "disable_parallel_tool_use" (no canonical equivalent)');
106
+ }
107
+ switch (choice.type) {
108
+ case "auto":
109
+ return "auto";
110
+ case "any":
111
+ return "required";
112
+ case "none":
113
+ return "none";
114
+ case "tool":
115
+ return { type: "function", function: { name: choice.name } };
116
+ }
117
+ }
118
+ /**
119
+ * §3.5: `thinking`/`redacted_thinking` blocks are dropped safely (debug
120
+ * log names the type only); any other unrecognised block type is a 400
121
+ * naming it — Claude Code blocks we can't honour must fail loudly, not
122
+ * silently turn into empty text.
123
+ */
124
+ function extractContent(content) {
125
+ if (typeof content === "string") {
126
+ return content;
127
+ }
128
+ const parts = [];
129
+ for (const block of content) {
130
+ if (block.type === "text") {
131
+ parts.push({ type: "text", text: block.text });
132
+ continue;
133
+ }
134
+ if (block.type === "image" && "source" in block) {
135
+ const source = block.source;
136
+ if (source.type === "base64" && source.data) {
137
+ parts.push({
138
+ type: "image",
139
+ image: {
140
+ data: source.data,
141
+ mimeType: (source.media_type ||
142
+ "image/png"),
143
+ },
144
+ });
145
+ }
146
+ else {
147
+ // image without a usable base64 source keeps the old placeholder
148
+ parts.push({ type: "text", text: "" });
149
+ }
150
+ continue;
151
+ }
152
+ if (block.type === "thinking" || block.type === "redacted_thinking") {
153
+ logger_1.logger.debug(`Dropping content block "${block.type}" (no canonical equivalent)`);
154
+ continue;
155
+ }
156
+ throw new errors_1.ValidationError(`Unsupported content block type "${block.type}"`);
157
+ }
158
+ return parts.length > 0 ? parts : "";
159
+ }
160
+ // ============ Canonical → Anthropic ============
161
+ function toMessageParam(m) {
162
+ const content = convertToAnthropicContent(m.content);
163
+ if (m.role !== "assistant" || !m.toolCalls || m.toolCalls.length === 0) {
164
+ return { role: m.role === "assistant" ? "assistant" : "user", content };
165
+ }
166
+ // Assistant message: text (and other content blocks) first, then the
167
+ // tool_use blocks (spec §3.4).
168
+ const blocks = typeof content === "string"
169
+ ? content
170
+ ? [{ type: "text", text: content }]
171
+ : []
172
+ : content;
173
+ return {
174
+ role: "assistant",
175
+ content: [...blocks, ...m.toolCalls.map(toToolUseBlock)],
176
+ };
177
+ }
178
+ function toToolResultBlock(m) {
179
+ return {
180
+ type: "tool_result",
181
+ tool_use_id: m.toolCallId ?? "",
182
+ content: convertToAnthropicContent(m.content),
183
+ // is_error is sent only when true; Anthropic defaults it to false.
184
+ ...(m.isError === true && { is_error: true }),
185
+ };
186
+ }
187
+ function toToolUseBlock(tc) {
188
+ const parsed = (0, tool_args_1.parseToolArguments)(tc.function.arguments);
189
+ if (!parsed.valid) {
190
+ logger_1.logger.warn("Tool call arguments are not valid JSON; sending _invalid_arguments wrapper");
191
+ }
192
+ return {
193
+ type: "tool_use",
194
+ id: tc.id,
195
+ name: tc.function.name,
196
+ input: parsed.args,
197
+ };
198
+ }
199
+ function toAnthropicTool(tool) {
200
+ return {
201
+ name: tool.function.name,
202
+ ...(tool.function.description && {
203
+ description: tool.function.description,
204
+ }),
205
+ input_schema: {
206
+ type: "object",
207
+ ...(tool.function.parameters ?? {}),
208
+ },
209
+ };
210
+ }
211
+ // auto→auto, required→any, none→none, function choice→tool{name} (§3.4).
212
+ function toAnthropicToolChoice(choice) {
213
+ if (typeof choice === "object") {
214
+ return { type: "tool", name: choice.function.name };
215
+ }
216
+ switch (choice) {
217
+ case "auto":
218
+ return { type: "auto" };
219
+ case "required":
220
+ return { type: "any" };
221
+ case "none":
222
+ return { type: "none" };
223
+ }
224
+ }
225
+ function convertToAnthropicContent(content) {
226
+ if (typeof content === "string") {
227
+ return content;
228
+ }
229
+ const blocks = content.map((part) => {
230
+ if (part.type === "text") {
231
+ return { type: "text", text: part.text || "" };
232
+ }
233
+ if (part.type === "image" && part.image) {
234
+ return {
235
+ type: "image",
236
+ source: {
237
+ type: "base64",
238
+ media_type: validateMediaType(part.image.mimeType),
239
+ data: part.image.data,
240
+ },
241
+ };
242
+ }
243
+ return { type: "text", text: "" };
244
+ });
245
+ // If only one text block, return as string
246
+ if (blocks.length === 1 && blocks[0].type === "text") {
247
+ return blocks[0].text;
248
+ }
249
+ return blocks;
250
+ }
251
+ function validateMediaType(type) {
252
+ const validTypes = [
253
+ "image/jpeg",
254
+ "image/png",
255
+ "image/gif",
256
+ "image/webp",
257
+ ];
258
+ return validTypes.includes(type)
259
+ ? type
260
+ : "image/png";
261
+ }