fix(antigravity): collect native part.functionCall into tool calls (#7037) (#7053)

* 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:
Adam
2026-07-18 22:34:27 +08:00
committed by GitHub
parent 688ff9d378
commit c55de7ab57
2 changed files with 100 additions and 1 deletions

View File

@@ -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()
);

View File

@@ -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" });
});