fix(translator): strict Anthropic content-block compliance in antigravity→openai request (#5935)

Integrated into release/v3.8.44 — strict Anthropic content-block compliance in antigravity→openai (port upstream #2296). PR test green (9/9). UNSTABLE red is the pre-existing environmental setup-claude base-red (opencode-plugin dist not built in fast-path), not a regression from this PR.
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-07-02 17:30:29 -03:00
committed by GitHub
parent 058cfd4f95
commit 18cf641df4
3 changed files with 95 additions and 6 deletions

View File

@@ -12,7 +12,7 @@ _TBD_
### 🔧 Bug Fixes
- **fix(sse):** strip ANSI/VT100 escape codes from gemini-cli stream frames so ANSI-prefixed `data:` lines are no longer silently dropped. (thanks @anki1kr)
- **fix(translator):** antigravity→openai request now emits Anthropic-compliant content blocks — drops empty text blocks and preserves tool calls/text co-located with tool results. (thanks @SahrulRamadhanHardiansyah)
### 📝 Maintenance

View File

@@ -229,14 +229,17 @@ function convertContent(content) {
continue;
}
// Text with thoughtSignature = regular text after thinking
// Text with thoughtSignature = regular text after thinking.
// Skip empty text — Anthropic rejects empty content blocks with a 400.
if (part.thoughtSignature && part.text !== undefined) {
textParts.push({ type: "text", text: part.text });
if (part.text) {
textParts.push({ type: "text", text: part.text });
}
continue;
}
// Regular text
if (part.text !== undefined) {
// Regular text — skip empty strings (Anthropic rejects empty content blocks).
if (part.text !== undefined && part.text !== "") {
textParts.push({ type: "text", text: part.text });
}
@@ -274,8 +277,26 @@ function convertContent(content) {
}
}
// Content with only functionResponses → return array of tool messages
// Function responses may be co-located with function calls / text / reasoning in
// the same content. Emit the tool messages AND the accompanying assistant message so
// nothing is dropped (previously only the tool messages survived).
if (toolResults.length > 0) {
if (toolCalls.length > 0 || textParts.length > 0 || reasoningContent) {
const assistantMsg: JsonRecord = { role: "assistant" };
if (textParts.length > 0) {
assistantMsg.content =
textParts.length === 1 && textParts[0].type === "text"
? textParts[0].text
: textParts;
}
if (reasoningContent) {
assistantMsg.reasoning_content = reasoningContent;
}
if (toolCalls.length > 0) {
assistantMsg.tool_calls = toolCalls;
}
return [...toolResults, assistantMsg];
}
return toolResults;
}

View File

@@ -154,6 +154,74 @@ test("Antigravity -> OpenAI returns tool messages when content contains only fun
]);
});
test("Antigravity -> OpenAI keeps co-located function response, function call and text", () => {
const result = antigravityToOpenAIRequest(
"gpt-4o",
{
request: {
contents: [
{
role: "model",
parts: [
{ text: "Let me look that up." },
{ functionResponse: { id: "call_9", name: "lookup", response: { result: { ok: true } } } },
{ functionCall: { id: "call_10", name: "lookup", args: { q: "weather" } } },
],
},
],
},
},
false
);
// Both the tool-result message AND the accompanying assistant message must survive.
const toolMsg = result.messages.find((m) => m.role === "tool");
const assistantMsg = result.messages.find((m) => m.role === "assistant");
assert.ok(toolMsg, "expected a role:tool message");
assert.equal(toolMsg.tool_call_id, "call_9");
assert.ok(assistantMsg, "expected a role:assistant message");
assert.equal(assistantMsg.content, "Let me look that up.");
assert.deepEqual(assistantMsg.tool_calls, [
{
id: "call_10",
type: "function",
function: { name: "lookup", arguments: '{"q":"weather"}' },
},
]);
});
test("Antigravity -> OpenAI drops empty thoughtSignature text instead of emitting empty content", () => {
const result = antigravityToOpenAIRequest(
"gpt-4o",
{
request: {
contents: [
{
role: "model",
parts: [
{ thoughtSignature: "sig", text: "" },
{ functionCall: { id: "call_11", name: "noop", args: {} } },
],
},
],
},
},
false
);
const assistantMsg = result.messages.find((m) => m.role === "assistant");
assert.ok(assistantMsg, "expected a role:assistant message");
// No empty content block should be emitted (Anthropic rejects it with a 400).
assert.equal("content" in assistantMsg, false);
assert.deepEqual(assistantMsg.tool_calls, [
{
id: "call_11",
type: "function",
function: { name: "noop", arguments: "{}" },
},
]);
});
test("Antigravity -> OpenAI lowers schema types recursively", () => {
const result = antigravityToOpenAIRequest(
"gpt-4o",