mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
fix(antigravity): preserve textual SSE tool calls
This commit is contained in:
@@ -123,10 +123,67 @@ function serializeAntigravityRequest(
|
||||
type AntigravityCollectedStream = {
|
||||
textContent: string;
|
||||
finishReason: string;
|
||||
toolCalls: Array<{
|
||||
id: string;
|
||||
index: number;
|
||||
type: "function";
|
||||
function: { name: string; arguments: string };
|
||||
}>;
|
||||
usage: Record<string, unknown> | null;
|
||||
remainingCredits: Array<{ creditType: string; creditAmount: string }> | null;
|
||||
};
|
||||
|
||||
function stripZeroWidth(value: unknown): unknown {
|
||||
if (typeof value === "string") {
|
||||
return value.replace(/[\u200B-\u200D\uFEFF]/g, "");
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => stripZeroWidth(item));
|
||||
}
|
||||
if (value && typeof value === "object") {
|
||||
return Object.fromEntries(
|
||||
Object.entries(value as Record<string, unknown>).map(([key, item]) => [
|
||||
key,
|
||||
stripZeroWidth(item),
|
||||
])
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function parseAntigravityTextualToolCall(text: unknown): { name: string; args: unknown } | null {
|
||||
if (typeof text !== "string") return null;
|
||||
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();
|
||||
if (!name || !rawArgs) return null;
|
||||
try {
|
||||
return { name, args: stripZeroWidth(JSON.parse(rawArgs)) };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function addAntigravityTextualToolCall(
|
||||
collected: AntigravityCollectedStream,
|
||||
parsed: { name: string; args: unknown }
|
||||
): void {
|
||||
collected.toolCalls.push({
|
||||
id: `${parsed.name}-${Date.now()}-${collected.toolCalls.length}`,
|
||||
index: collected.toolCalls.length,
|
||||
type: "function",
|
||||
function: {
|
||||
name: parsed.name,
|
||||
arguments: JSON.stringify(parsed.args || {}),
|
||||
},
|
||||
});
|
||||
collected.finishReason = "tool_calls";
|
||||
}
|
||||
|
||||
type AntigravityRequestEnvelope = Record<string, unknown> & {
|
||||
project: string;
|
||||
model: string;
|
||||
@@ -233,7 +290,12 @@ function processAntigravitySSEPayload(
|
||||
if (candidate?.content?.parts) {
|
||||
for (const part of candidate.content.parts) {
|
||||
if (typeof part.text === "string" && !part.thought && !part.thoughtSignature) {
|
||||
collected.textContent += part.text;
|
||||
const textualToolCall = parseAntigravityTextualToolCall(part.text);
|
||||
if (textualToolCall) {
|
||||
addAntigravityTextualToolCall(collected, textualToolCall);
|
||||
} else {
|
||||
collected.textContent += part.text;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -717,6 +779,7 @@ export class AntigravityExecutor extends BaseExecutor {
|
||||
const collected: AntigravityCollectedStream = {
|
||||
textContent: "",
|
||||
finishReason: "stop",
|
||||
toolCalls: [],
|
||||
usage: null,
|
||||
remainingCredits: null,
|
||||
};
|
||||
@@ -761,8 +824,19 @@ export class AntigravityExecutor extends BaseExecutor {
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
message: { role: "assistant", content: collected.textContent },
|
||||
finish_reason: timedOut ? "length" : collected.finishReason,
|
||||
message:
|
||||
collected.toolCalls.length > 0
|
||||
? {
|
||||
role: "assistant",
|
||||
content: collected.textContent || null,
|
||||
tool_calls: collected.toolCalls,
|
||||
}
|
||||
: { role: "assistant", content: collected.textContent },
|
||||
finish_reason: timedOut
|
||||
? "length"
|
||||
: collected.toolCalls.length > 0
|
||||
? "tool_calls"
|
||||
: collected.finishReason,
|
||||
},
|
||||
],
|
||||
...(collected.usage && { usage: collected.usage }),
|
||||
|
||||
@@ -476,6 +476,60 @@ test("AntigravityExecutor.collectStreamToResponse turns SSE Gemini chunks into a
|
||||
});
|
||||
});
|
||||
|
||||
test("AntigravityExecutor.collectStreamToResponse converts textual tool call SSE to structured tool_calls", async () => {
|
||||
const executor = new AntigravityExecutor();
|
||||
const response = new Response(
|
||||
[
|
||||
`data: ${JSON.stringify({
|
||||
response: {
|
||||
candidates: [
|
||||
{
|
||||
content: {
|
||||
parts: [
|
||||
{
|
||||
text: '[Tool call: search_files]\nArguments: {"file_glob":"*gemini*","output_mode":"files_only","path":"/opt/O\\u200dmniRoute","target":"files"}',
|
||||
},
|
||||
],
|
||||
},
|
||||
finishReason: "STOP",
|
||||
},
|
||||
],
|
||||
usageMetadata: {
|
||||
promptTokenCount: 7,
|
||||
candidatesTokenCount: 4,
|
||||
totalTokenCount: 11,
|
||||
},
|
||||
},
|
||||
})}\n\n`,
|
||||
].join(""),
|
||||
{
|
||||
status: 200,
|
||||
headers: { "Content-Type": "text/event-stream" },
|
||||
}
|
||||
);
|
||||
|
||||
const result = await executor.collectStreamToResponse(
|
||||
response,
|
||||
"gemini-3.5-flash-low",
|
||||
"https://example.com",
|
||||
{ Authorization: "Bearer ag-token" },
|
||||
{ request: {} }
|
||||
);
|
||||
const payload = await result.response.json();
|
||||
const choice = payload.choices[0];
|
||||
|
||||
assert.equal(choice.message.content, null);
|
||||
assert.equal(choice.finish_reason, "tool_calls");
|
||||
assert.equal(choice.message.tool_calls.length, 1);
|
||||
assert.equal(choice.message.tool_calls[0].function.name, "search_files");
|
||||
assert.deepEqual(JSON.parse(choice.message.tool_calls[0].function.arguments), {
|
||||
file_glob: "*gemini*",
|
||||
output_mode: "files_only",
|
||||
path: "/opt/OmniRoute",
|
||||
target: "files",
|
||||
});
|
||||
});
|
||||
|
||||
test("AntigravityExecutor.collectStreamToResponse parses fragmented SSE lines incrementally", async () => {
|
||||
const executor = new AntigravityExecutor();
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
Reference in New Issue
Block a user