mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
fix(gemini): parse prefixed textual tool calls
This commit is contained in:
@@ -36,7 +36,15 @@ function firstPositiveNumber(...values: unknown[]): number {
|
||||
|
||||
function parseTextualToolCall(text: unknown): { name: string; args: unknown } | null {
|
||||
if (typeof text !== "string") return null;
|
||||
const match = text.match(/^\s*\[Tool call:\s*([^\]\n]+)\]\s*\nArguments:\s*([\s\S]+?)\s*$/);
|
||||
|
||||
// Gemini/Antigravity sometimes imitates the request-side fallback with small
|
||||
// variations, e.g. a leading "(empty)" marker or zero-width chars inserted
|
||||
// into argument strings. Normalize those variants before parsing so the
|
||||
// response is still surfaced as a structured OpenAI tool call.
|
||||
const normalized = text.replace(/[\u200B-\u200D\uFEFF]/g, "");
|
||||
const match = normalized.match(
|
||||
/^[\s\S]*?\[Tool call:\s*([^\]\n]+)\]\s*\nArguments:\s*([\s\S]+?)\s*$/
|
||||
);
|
||||
if (!match) return null;
|
||||
const name = match[1]?.trim();
|
||||
const rawArgs = match[2]?.trim();
|
||||
|
||||
@@ -25,7 +25,15 @@ type GeminiFunctionCallPart = {
|
||||
|
||||
function parseTextualToolCall(text: unknown): { name: string; args: unknown } | null {
|
||||
if (typeof text !== "string") return null;
|
||||
const match = text.match(/^\s*\[Tool call:\s*([^\]\n]+)\]\s*\nArguments:\s*([\s\S]+?)\s*$/);
|
||||
|
||||
// Gemini/Antigravity sometimes imitates the request-side fallback with small
|
||||
// variations, e.g. a leading "(empty)" marker or zero-width chars inserted
|
||||
// into argument strings. Normalize those variants before parsing so the
|
||||
// response is still surfaced as a structured OpenAI tool call.
|
||||
const normalized = text.replace(/[\u200B-\u200D\uFEFF]/g, "");
|
||||
const match = normalized.match(
|
||||
/^[\s\S]*?\[Tool call:\s*([^\]\n]+)\]\s*\nArguments:\s*([\s\S]+?)\s*$/
|
||||
);
|
||||
if (!match) return null;
|
||||
const name = match[1]?.trim();
|
||||
const rawArgs = match[2]?.trim();
|
||||
|
||||
@@ -381,6 +381,45 @@ test("Gemini stream: converts textual Tool call block to structured tool_calls",
|
||||
assert.equal(result.at(-1).choices[0].finish_reason, "tool_calls");
|
||||
});
|
||||
|
||||
test("Gemini stream: converts prefixed textual Tool call block with zero-width chars", () => {
|
||||
const state = createStreamingState();
|
||||
const result = geminiToOpenAIResponse(
|
||||
{
|
||||
responseId: "resp-textual-tool-prefixed",
|
||||
modelVersion: "gemini-3.5-flash-low",
|
||||
candidates: [
|
||||
{
|
||||
content: {
|
||||
parts: [
|
||||
{
|
||||
text: '(empty)[Tool call: terminal]\nArguments: {"command":"sqlite3 ~/.o\u200dmniroute/storage.sqlite"}',
|
||||
},
|
||||
],
|
||||
},
|
||||
finishReason: "STOP",
|
||||
},
|
||||
],
|
||||
},
|
||||
state
|
||||
);
|
||||
|
||||
const toolCall = result.find((event: any) => event.choices?.[0]?.delta?.tool_calls)?.choices[0]
|
||||
.delta.tool_calls[0];
|
||||
assert.ok(toolCall.id.startsWith("terminal-"));
|
||||
assert.equal(toolCall.function.name, "terminal");
|
||||
assert.equal(
|
||||
toolCall.function.arguments,
|
||||
JSON.stringify({
|
||||
command: "sqlite3 ~/.omniroute/storage.sqlite",
|
||||
})
|
||||
);
|
||||
assert.equal(
|
||||
result.some((event: any) => event.choices?.[0]?.delta?.content?.includes("[Tool call:")),
|
||||
false
|
||||
);
|
||||
assert.equal(result.at(-1).choices[0].finish_reason, "tool_calls");
|
||||
});
|
||||
|
||||
test("Gemini stream: tool calls without native IDs keep deterministic fallback shape", () => {
|
||||
const state = createStreamingState();
|
||||
const result = geminiToOpenAIResponse(
|
||||
|
||||
Reference in New Issue
Block a user