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.
- 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 +21 -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 +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
|
+
}
|