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,341 @@
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 gemini_adapter_1 = require("./gemini.adapter");
11
+ const isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
12
+ /**
13
+ * Gemini `streamGenerateContent?alt=sse` → canonical events (spec §3.3,
14
+ * ai.google.dev/api/generate-content#method:-models.streamgeneratecontent
15
+ * and the GenerateContentResponse/Candidate/FunctionCall/UsageMetadata
16
+ * references there): each `data:` frame is a GenerateContentResponse.
17
+ * The first frame emits `message_start` (`responseId` → id,
18
+ * `modelVersion` → model). `candidates[0]` text parts become
19
+ * `text_delta`s (empty strings — e.g. thoughtSignature-only parts —
20
+ * emit nothing); `functionCall` parts arrive whole — each becomes
21
+ * start + one delta (`JSON.stringify(args)`) + end at sequential
22
+ * canonical indices, keeping the wire `id` when present and drawing
23
+ * from `ids` otherwise. `usageMetadata` is cumulative on every frame —
24
+ * the last one seen is emitted once as `usage` when a `finishReason`
25
+ * ends the stream; a stream that ends in function calls reports
26
+ * `tool_call`, `MAX_TOKENS` maps to `length` via `mapFinishReason`.
27
+ * An `{"error":{code,message,status}}` frame maps through
28
+ * `toCanonicalError` and ends the stream. A payload that parses as
29
+ * JSON but isn't a plain object, or `candidates`/`parts` that aren't
30
+ * arrays, is an undecodable chunk — one `error` event and the stream
31
+ * is finished. A stream ending without `finishReason` ends in an
32
+ * `error` event.
33
+ */
34
+ function createStreamDecoder(model, ids = (0, ids_1.createIdGenerator)()) {
35
+ const utf8 = (0, sse_1.createUtf8Decoder)();
36
+ const sse = (0, sse_1.createSseParser)();
37
+ let started = false;
38
+ let finished = false;
39
+ let nextIndex = 0;
40
+ let sawToolCalls = false;
41
+ let usage;
42
+ function failUndecodable(out) {
43
+ out.push({
44
+ type: "error",
45
+ error: (0, canonical_error_1.makeCanonicalError)("upstream", "Upstream sent an undecodable stream chunk"),
46
+ });
47
+ finished = true;
48
+ }
49
+ function handleObject(obj, out) {
50
+ // An in-stream {"error":{code,message,status}} frame — the same
51
+ // failure shape as a non-streaming error body.
52
+ if (obj.error != null) {
53
+ const code = typeof obj.error?.code === "number" ? obj.error.code : 500;
54
+ out.push({ type: "error", error: (0, gemini_adapter_1.toCanonicalError)(code, obj) });
55
+ finished = true;
56
+ return;
57
+ }
58
+ if (obj.candidates != null &&
59
+ (!Array.isArray(obj.candidates) ||
60
+ obj.candidates.some((c) => !isObj(c)))) {
61
+ failUndecodable(out);
62
+ return;
63
+ }
64
+ if (!started) {
65
+ started = true;
66
+ out.push({
67
+ type: "message_start",
68
+ id: typeof obj.responseId === "string" ? obj.responseId : ids(),
69
+ model: typeof obj.modelVersion === "string" ? obj.modelVersion : model,
70
+ });
71
+ }
72
+ // Single candidate (§2.2 — multiple candidates are out of scope),
73
+ // matching toCanonicalResponse's candidates[0].
74
+ const candidate = obj.candidates?.[0];
75
+ const parts = candidate?.content?.parts;
76
+ if (parts != null &&
77
+ (!Array.isArray(parts) || parts.some((p) => !isObj(p)))) {
78
+ failUndecodable(out);
79
+ return;
80
+ }
81
+ for (const part of parts ?? []) {
82
+ if (typeof part?.text === "string" && part.text.length > 0) {
83
+ out.push({ type: "text_delta", text: part.text });
84
+ }
85
+ const fc = part?.functionCall;
86
+ if (fc != null) {
87
+ if (typeof fc !== "object") {
88
+ failUndecodable(out);
89
+ return;
90
+ }
91
+ sawToolCalls = true;
92
+ const index = nextIndex++;
93
+ out.push({
94
+ type: "tool_call_start",
95
+ index,
96
+ id: typeof fc.id === "string" ? fc.id : ids(),
97
+ name: typeof fc.name === "string" ? fc.name : "",
98
+ });
99
+ out.push({
100
+ type: "tool_call_delta",
101
+ index,
102
+ argumentsDelta: JSON.stringify(fc.args ?? {}),
103
+ });
104
+ out.push({ type: "tool_call_end", index });
105
+ }
106
+ }
107
+ // Cumulative token counts — the last frame's values win.
108
+ if (obj.usageMetadata != null)
109
+ usage = obj.usageMetadata;
110
+ const finishReason = candidate?.finishReason;
111
+ if (typeof finishReason === "string") {
112
+ if (usage != null) {
113
+ const input = usage.promptTokenCount ?? 0;
114
+ const output = usage.candidatesTokenCount ?? 0;
115
+ out.push({
116
+ type: "usage",
117
+ usage: {
118
+ inputTokens: input,
119
+ outputTokens: output,
120
+ totalTokens: usage.totalTokenCount ?? input + output,
121
+ },
122
+ });
123
+ }
124
+ out.push({
125
+ type: "message_end",
126
+ finishReason: sawToolCalls
127
+ ? "tool_call"
128
+ : (0, gemini_adapter_1.mapFinishReason)(finishReason),
129
+ });
130
+ finished = true;
131
+ }
132
+ }
133
+ function handleEvent(data, out) {
134
+ if (finished)
135
+ return;
136
+ let obj;
137
+ try {
138
+ obj = JSON.parse(data);
139
+ }
140
+ catch {
141
+ failUndecodable(out);
142
+ return;
143
+ }
144
+ if (!isObj(obj)) {
145
+ failUndecodable(out);
146
+ return;
147
+ }
148
+ handleObject(obj, out);
149
+ }
150
+ function drain(events, out) {
151
+ for (const e of events)
152
+ handleEvent(e.data, out);
153
+ }
154
+ return {
155
+ feed(chunk) {
156
+ const out = [];
157
+ drain(sse.feed(utf8(chunk)), out);
158
+ return out;
159
+ },
160
+ end() {
161
+ const out = [];
162
+ drain(sse.feed(utf8(new Uint8Array(0))), out);
163
+ drain(sse.end(), out);
164
+ if (!finished) {
165
+ out.push({
166
+ type: "error",
167
+ error: (0, canonical_error_1.makeCanonicalError)("upstream", "Upstream stream ended without completing"),
168
+ });
169
+ finished = true;
170
+ }
171
+ return out;
172
+ },
173
+ };
174
+ }
175
+ /**
176
+ * Canonical events → Gemini streamGenerateContent responses (spec §3.3,
177
+ * §3.4): `ctx.query.alt === "sse"` selects `data: <response>\r\n\r\n`
178
+ * frames (`text/event-stream`); anything else is the streamed JSON
179
+ * array (`[`, elements separated by `,\r\n`, `]`) with content type
180
+ * `application/json` — the `]` still closes after an error element so
181
+ * the body stays valid JSON. `message_start` produces no element (the
182
+ * wire has no start frame); its id is echoed as `responseId` and
183
+ * `ctx.requestedModel` as `modelVersion` on every element. Each
184
+ * `text_delta` is one single-part candidate element; tool calls are
185
+ * buffered by canonical `index` until `tool_call_end`, then written as
186
+ * one element whose part is `functionCall{name, args, id}` — `args`
187
+ * via `parseToolArguments` (`{}` for empty, `_invalid_arguments`
188
+ * wrapper for invalid), `id` preserved since FunctionCall.id is a
189
+ * documented optional field. `usage` is held into the closing
190
+ * element's `usageMetadata`, which also carries the `finishReason`
191
+ * (`mapToGeminiFinishReason`). `error` becomes an
192
+ * `{"error":{code,message,status}}` element via `fromCanonicalError`.
193
+ * Calls still open at `message_end` flush in ascending canonical
194
+ * index just before the final element (one `logger.warn` naming the
195
+ * count); before `error` they are dropped. `message_end` and `error`
196
+ * are terminal: any event after them encodes to nothing.
197
+ */
198
+ function createStreamEncoder(ctx) {
199
+ const sse = ctx.query.alt === "sse";
200
+ let finished = false;
201
+ let arrayOpen = false;
202
+ let arrayClosed = false;
203
+ let responseId;
204
+ let usage;
205
+ const calls = new Map();
206
+ /** One wire element: SSE `data:` frame, or the next JSON-array element. */
207
+ function element(obj) {
208
+ const json = JSON.stringify(obj);
209
+ if (sse)
210
+ return `data: ${json}\r\n\r\n`;
211
+ const prefix = arrayOpen ? ",\r\n" : "[";
212
+ arrayOpen = true;
213
+ return prefix + json;
214
+ }
215
+ /** A GenerateContentResponse carrying one candidate. */
216
+ function chunk(candidate) {
217
+ return element({
218
+ candidates: [{ index: 0, ...candidate }],
219
+ ...(responseId !== undefined && { responseId }),
220
+ modelVersion: ctx.requestedModel,
221
+ });
222
+ }
223
+ const toolCallChunk = (call) => {
224
+ const parsed = (0, tool_args_1.parseToolArguments)(call.args);
225
+ if (!parsed.valid) {
226
+ logger_1.logger.warn("Tool call arguments are not valid JSON; sending _invalid_arguments wrapper");
227
+ }
228
+ return chunk({
229
+ content: {
230
+ role: "model",
231
+ parts: [
232
+ {
233
+ functionCall: {
234
+ id: call.id,
235
+ name: call.name,
236
+ args: parsed.args,
237
+ },
238
+ },
239
+ ],
240
+ },
241
+ });
242
+ };
243
+ const warnUnknownIndex = (type, index) => {
244
+ logger_1.logger.warn(`Gemini stream encoder: ${type} for unknown tool-call index ${index}; dropping`);
245
+ };
246
+ return {
247
+ contentType: sse ? "text/event-stream" : "application/json",
248
+ encode(event) {
249
+ if (finished)
250
+ return "";
251
+ switch (event.type) {
252
+ case "message_start":
253
+ responseId = event.id;
254
+ return "";
255
+ case "text_delta":
256
+ return chunk({
257
+ content: {
258
+ role: "model",
259
+ parts: [{ text: event.text }],
260
+ },
261
+ });
262
+ case "tool_call_start":
263
+ if (calls.has(event.index)) {
264
+ logger_1.logger.warn(`Gemini stream encoder: ${event.type} for already-open tool-call index ${event.index}; keeping the first call`);
265
+ return "";
266
+ }
267
+ calls.set(event.index, {
268
+ id: event.id,
269
+ name: event.name,
270
+ args: "",
271
+ });
272
+ return "";
273
+ case "tool_call_delta": {
274
+ const call = calls.get(event.index);
275
+ if (call) {
276
+ call.args += event.argumentsDelta;
277
+ }
278
+ else {
279
+ warnUnknownIndex(event.type, event.index);
280
+ }
281
+ return "";
282
+ }
283
+ case "tool_call_end": {
284
+ const call = calls.get(event.index);
285
+ if (!call) {
286
+ warnUnknownIndex(event.type, event.index);
287
+ return "";
288
+ }
289
+ calls.delete(event.index);
290
+ return toolCallChunk(call);
291
+ }
292
+ case "usage":
293
+ usage = event.usage;
294
+ return "";
295
+ case "message_end": {
296
+ finished = true;
297
+ let out = "";
298
+ if (calls.size > 0) {
299
+ logger_1.logger.warn(`Gemini stream ended with ${calls.size} tool call(s) still open; flushing before finishReason`);
300
+ const pending = [...calls.entries()].sort(([a], [b]) => a - b);
301
+ calls.clear();
302
+ for (const [, call] of pending)
303
+ out += toolCallChunk(call);
304
+ }
305
+ return (out +
306
+ element({
307
+ candidates: [
308
+ {
309
+ index: 0,
310
+ content: { role: "model", parts: [] },
311
+ finishReason: (0, gemini_adapter_1.mapToGeminiFinishReason)(event.finishReason),
312
+ },
313
+ ],
314
+ ...(usage !== undefined && {
315
+ usageMetadata: {
316
+ promptTokenCount: usage.inputTokens,
317
+ candidatesTokenCount: usage.outputTokens,
318
+ totalTokenCount: usage.totalTokens,
319
+ },
320
+ }),
321
+ ...(responseId !== undefined && { responseId }),
322
+ modelVersion: ctx.requestedModel,
323
+ }));
324
+ }
325
+ case "error":
326
+ finished = true;
327
+ if (calls.size > 0) {
328
+ logger_1.logger.warn(`Gemini stream ended with ${calls.size} tool call(s) still open; dropping`);
329
+ calls.clear();
330
+ }
331
+ return element((0, gemini_adapter_1.fromCanonicalError)(event.error));
332
+ }
333
+ },
334
+ end() {
335
+ if (sse || arrayClosed)
336
+ return "";
337
+ arrayClosed = true;
338
+ return arrayOpen ? "]" : "[]";
339
+ },
340
+ };
341
+ }
@@ -0,0 +1,246 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isFunctionCallPart = isFunctionCallPart;
4
+ exports.isFunctionResponsePart = isFunctionResponsePart;
5
+ exports.recordCallId = recordCallId;
6
+ exports.resolveCallId = resolveCallId;
7
+ exports.toCanonicalToolCalls = toCanonicalToolCalls;
8
+ exports.toCanonicalToolMessages = toCanonicalToolMessages;
9
+ exports.toCanonicalTools = toCanonicalTools;
10
+ exports.toCanonicalToolChoice = toCanonicalToolChoice;
11
+ exports.functionResponseContent = functionResponseContent;
12
+ exports.functionCallPart = functionCallPart;
13
+ exports.functionResponsePart = functionResponsePart;
14
+ exports.appendFunctionResponsePart = appendFunctionResponsePart;
15
+ exports.toolResultResponse = toolResultResponse;
16
+ exports.toGeminiTools = toGeminiTools;
17
+ exports.toGeminiToolConfig = toGeminiToolConfig;
18
+ const errors_1 = require("../../utils/errors");
19
+ const logger_1 = require("../../utils/logger");
20
+ const tool_args_1 = require("../../utils/tool-args");
21
+ const gemini_schema_1 = require("./gemini.schema");
22
+ function isFunctionCallPart(part) {
23
+ return "functionCall" in part && part.functionCall != null;
24
+ }
25
+ function isFunctionResponsePart(part) {
26
+ return "functionResponse" in part && part.functionResponse != null;
27
+ }
28
+ function recordCallId(pending, name, id) {
29
+ const queue = pending.get(name);
30
+ if (queue) {
31
+ queue.push(id);
32
+ }
33
+ else {
34
+ pending.set(name, [id]);
35
+ }
36
+ }
37
+ /**
38
+ * Resolve the call id for a functionResponse: a wire `id` wins (and is
39
+ * consumed so it cannot match twice); otherwise the oldest pending call
40
+ * id for that name.
41
+ */
42
+ function resolveCallId(pending, res) {
43
+ const queue = pending.get(res.name) ?? [];
44
+ if (res.id !== undefined) {
45
+ const i = queue.indexOf(res.id);
46
+ if (i !== -1)
47
+ queue.splice(i, 1);
48
+ return res.id;
49
+ }
50
+ return queue.shift();
51
+ }
52
+ /** Gemini functionCall parts → canonical toolCalls (wire id or generated). */
53
+ function toCanonicalToolCalls(parts, ids) {
54
+ return parts.filter(isFunctionCallPart).map((part) => {
55
+ const call = part.functionCall;
56
+ return {
57
+ id: call.id ?? ids(),
58
+ type: "function",
59
+ function: {
60
+ name: call.name,
61
+ arguments: JSON.stringify(call.args ?? {}),
62
+ },
63
+ };
64
+ });
65
+ }
66
+ /** Gemini functionResponse parts → canonical tool messages. */
67
+ function toCanonicalToolMessages(parts, pending) {
68
+ return parts.filter(isFunctionResponsePart).map((part) => {
69
+ const res = part.functionResponse;
70
+ const toolCallId = resolveCallId(pending, res);
71
+ if (!toolCallId) {
72
+ throw new errors_1.ValidationError(`Function response "${res.name}" has no matching function call in this request`);
73
+ }
74
+ return {
75
+ role: "tool",
76
+ toolCallId,
77
+ content: functionResponseContent(res),
78
+ };
79
+ });
80
+ }
81
+ /**
82
+ * Gemini `tools[]` → canonical tools. Only `functionDeclarations`
83
+ * entries have a canonical equivalent; any other tool type
84
+ * (`codeExecution`, `googleSearch`, `googleSearchRetrieval`, …) →
85
+ * ValidationError naming the key.
86
+ */
87
+ function toCanonicalTools(tools) {
88
+ const declarations = [];
89
+ for (const tool of tools ?? []) {
90
+ for (const key of Object.keys(tool)) {
91
+ if (key !== "functionDeclarations") {
92
+ throw new errors_1.ValidationError(`Gemini tool type "${key}" is not supported`);
93
+ }
94
+ }
95
+ if ("functionDeclarations" in tool) {
96
+ declarations.push(...(tool.functionDeclarations ?? []));
97
+ }
98
+ }
99
+ if (declarations.length === 0)
100
+ return undefined;
101
+ return declarations.map((decl) => ({
102
+ type: "function",
103
+ function: {
104
+ name: decl.name,
105
+ ...(decl.description !== undefined && {
106
+ description: decl.description,
107
+ }),
108
+ ...(decl.parameters !== undefined && {
109
+ parameters: decl.parameters,
110
+ }),
111
+ },
112
+ }));
113
+ }
114
+ /** Gemini `toolConfig.functionCallingConfig` → canonical toolChoice. */
115
+ function toCanonicalToolChoice(toolConfig) {
116
+ const fcc = toolConfig?.functionCallingConfig;
117
+ if (fcc?.mode === undefined)
118
+ return undefined;
119
+ switch (fcc.mode) {
120
+ case "AUTO":
121
+ case "MODE_UNSPECIFIED":
122
+ return "auto";
123
+ case "NONE":
124
+ return "none";
125
+ case "ANY": {
126
+ const names = fcc.allowedFunctionNames ?? [];
127
+ if (names.length === 0) {
128
+ return "required";
129
+ }
130
+ if (names.length === 1) {
131
+ return {
132
+ type: "function",
133
+ function: { name: names[0] },
134
+ };
135
+ }
136
+ throw new errors_1.ValidationError("Gemini functionCallingConfig with multiple allowedFunctionNames has no canonical equivalent");
137
+ }
138
+ default:
139
+ throw new errors_1.ValidationError(`Gemini functionCallingConfig mode "${fcc.mode}" is not supported`);
140
+ }
141
+ }
142
+ /** Gemini functionResponse → canonical tool-message content string. */
143
+ function functionResponseContent(res) {
144
+ const response = res.response;
145
+ if (response !== null &&
146
+ typeof response === "object" &&
147
+ !Array.isArray(response)) {
148
+ const keys = Object.keys(response);
149
+ // Unwrap the { "content": "<string>" } wrapper this adapter emits
150
+ if (keys.length === 1 &&
151
+ typeof response.content === "string") {
152
+ return response.content;
153
+ }
154
+ return JSON.stringify(response);
155
+ }
156
+ return JSON.stringify(response ?? {});
157
+ }
158
+ // ============ ENCODE: Canonical → Gemini ============
159
+ /** Canonical tool call → Gemini functionCall part (id dropped on the wire). */
160
+ function functionCallPart(call) {
161
+ const parsed = (0, tool_args_1.parseToolArguments)(call.function.arguments);
162
+ if (!parsed.valid) {
163
+ logger_1.logger.warn("Tool call arguments are not valid JSON; sending _invalid_arguments wrapper");
164
+ }
165
+ return {
166
+ functionCall: { name: call.function.name, args: parsed.args },
167
+ };
168
+ }
169
+ /** Canonical tool message → Gemini functionResponse part. */
170
+ function functionResponsePart(name, content) {
171
+ return {
172
+ functionResponse: {
173
+ name,
174
+ response: toolResultResponse(content),
175
+ },
176
+ };
177
+ }
178
+ /**
179
+ * Append a functionResponse part to `contents`, sharing the previous
180
+ * user turn when it consists solely of functionResponses.
181
+ */
182
+ function appendFunctionResponsePart(contents, part) {
183
+ const last = contents[contents.length - 1];
184
+ if (last?.role === "user" &&
185
+ last.parts.every(isFunctionResponsePart)) {
186
+ last.parts.push(part);
187
+ }
188
+ else {
189
+ contents.push({ role: "user", parts: [part] });
190
+ }
191
+ }
192
+ /** Canonical tool-result content → Gemini functionResponse.response object. */
193
+ function toolResultResponse(content) {
194
+ const text = typeof content === "string"
195
+ ? content
196
+ : content
197
+ .filter((p) => p.type === "text")
198
+ .map((p) => p.text ?? "")
199
+ .join("");
200
+ const parsed = (0, tool_args_1.parseToolArguments)(text);
201
+ if (parsed.valid && text.trim() !== "") {
202
+ return parsed.args;
203
+ }
204
+ return { content: text };
205
+ }
206
+ /**
207
+ * Canonical tools → a single Gemini `functionDeclarations` entry;
208
+ * parameters filtered to Google's documented schema subset (spec §3.4).
209
+ */
210
+ function toGeminiTools(tools) {
211
+ if (!tools?.length)
212
+ return undefined;
213
+ return [
214
+ {
215
+ functionDeclarations: tools.map((tool) => ({
216
+ name: tool.function.name,
217
+ ...(tool.function.description !== undefined && {
218
+ description: tool.function.description,
219
+ }),
220
+ ...(tool.function.parameters !== undefined && {
221
+ parameters: (0, gemini_schema_1.toGeminiSchema)(tool.function.parameters),
222
+ }),
223
+ })),
224
+ },
225
+ ];
226
+ }
227
+ /** Canonical toolChoice → Gemini toolConfig.functionCallingConfig. */
228
+ function toGeminiToolConfig(choice) {
229
+ if (choice === undefined)
230
+ return undefined;
231
+ return {
232
+ functionCallingConfig: toFunctionCallingConfig(choice),
233
+ };
234
+ }
235
+ function toFunctionCallingConfig(choice) {
236
+ if (choice === "auto")
237
+ return { mode: "AUTO" };
238
+ if (choice === "none")
239
+ return { mode: "NONE" };
240
+ if (choice === "required")
241
+ return { mode: "ANY" };
242
+ return {
243
+ mode: "ANY",
244
+ allowedFunctionNames: [choice.function.name],
245
+ };
246
+ }