mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-02 13:22:11 +03:00
* fix(antigravity): collect native part.functionCall into tool calls (#7037) * test(antigravity): add #7037 native functionCall regression coverage * fix(antigravity): do not clobber tool_calls finish reason with candidate STOP (#7037)
This commit is contained in:
@@ -96,9 +96,23 @@ export function processAntigravitySSEPayload(
|
||||
collected.textContent += part.text;
|
||||
}
|
||||
}
|
||||
// Native Gemini function calls. Non-streaming responses (and some
|
||||
// streaming ones) carry the tool call as `part.functionCall` rather than
|
||||
// the textual `[Tool call: ...]` markdown. Without this, a tool-only
|
||||
// response produced empty content and a 502 Provider error (#7037).
|
||||
if (part.functionCall && typeof part.functionCall.name === "string") {
|
||||
addAntigravityTextualToolCall(collected, {
|
||||
name: part.functionCall.name,
|
||||
args: part.functionCall.args ?? {},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (candidate?.finishReason) {
|
||||
// Preserve a tool-call finish reason: once a native `part.functionCall`
|
||||
// (or textual tool call) has populated `toolCalls`, the candidate's own
|
||||
// finish reason (often STOP) must not clobber it (#7037 — a tool-only
|
||||
// response would otherwise report STOP and lose its tool-call signal).
|
||||
if (candidate?.finishReason && collected.toolCalls.length === 0) {
|
||||
collected.finishReason = normalizeOpenAICompatibleFinishReasonString(
|
||||
String(candidate.finishReason).toLowerCase()
|
||||
);
|
||||
|
||||
@@ -75,3 +75,88 @@ test("processAntigravitySSEPayload ignores [DONE] and malformed payloads without
|
||||
processAntigravitySSEPayload("{not json", collected);
|
||||
assert.equal(collected.textContent, "");
|
||||
});
|
||||
|
||||
// #7037 — non-streaming (and tool-only) responses carry the tool call as a native
|
||||
// `part.functionCall` with no `part.text`. It must produce a tool call instead of
|
||||
// empty content (which previously surfaced as a 502 "Provider returned empty content").
|
||||
test("processAntigravitySSEPayload converts native part.functionCall into a tool call (#7037)", () => {
|
||||
const collected = emptyCollected();
|
||||
processAntigravitySSEPayload(
|
||||
JSON.stringify({
|
||||
response: {
|
||||
candidates: [
|
||||
{
|
||||
content: {
|
||||
parts: [{ functionCall: { name: "get_weather", args: { city: "Paris" } } }],
|
||||
},
|
||||
finishReason: "STOP",
|
||||
},
|
||||
],
|
||||
usageMetadata: { promptTokenCount: 12, candidatesTokenCount: 932, totalTokenCount: 944 },
|
||||
},
|
||||
}),
|
||||
collected
|
||||
);
|
||||
|
||||
assert.equal(collected.textContent, "");
|
||||
assert.equal(collected.toolCalls.length, 1);
|
||||
assert.equal(collected.toolCalls[0].type, "function");
|
||||
assert.equal(collected.toolCalls[0].function.name, "get_weather");
|
||||
assert.deepEqual(JSON.parse(collected.toolCalls[0].function.arguments), { city: "Paris" });
|
||||
assert.equal(collected.finishReason, "tool_calls");
|
||||
assert.ok(collected.usage !== null, "usage metadata should still be collected");
|
||||
});
|
||||
|
||||
test("processAntigravitySSEPayload handles a mixed text + functionCall response (#7037)", () => {
|
||||
const collected = emptyCollected();
|
||||
processAntigravitySSEPayload(
|
||||
JSON.stringify({
|
||||
response: {
|
||||
candidates: [
|
||||
{
|
||||
content: {
|
||||
parts: [
|
||||
{ text: "Let me check." },
|
||||
{ functionCall: { name: "get_weather", args: { city: "Paris" } } },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
collected
|
||||
);
|
||||
|
||||
assert.equal(collected.textContent, "Let me check.");
|
||||
assert.equal(collected.toolCalls.length, 1);
|
||||
assert.equal(collected.toolCalls[0].function.name, "get_weather");
|
||||
});
|
||||
|
||||
// #7037 — before the fix, a function-call-only payload yielded no text and no
|
||||
// tool call, so the non-streaming path returned empty content. Guard that the
|
||||
// textual-tool-call path is unaffected.
|
||||
test("processAntigravitySSEPayload still parses textual [Tool call:] when present", () => {
|
||||
const collected = emptyCollected();
|
||||
processAntigravitySSEPayload(
|
||||
JSON.stringify({
|
||||
response: {
|
||||
candidates: [
|
||||
{
|
||||
content: {
|
||||
parts: [
|
||||
{
|
||||
text: "[Tool call: get_weather]\nArguments: {\"city\":\"Paris\"}",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
collected
|
||||
);
|
||||
|
||||
assert.equal(collected.toolCalls.length, 1);
|
||||
assert.equal(collected.toolCalls[0].function.name, "get_weather");
|
||||
assert.deepEqual(JSON.parse(collected.toolCalls[0].function.arguments), { city: "Paris" });
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user