mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-03 13:52:09 +03:00
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:
committed by
GitHub
parent
058cfd4f95
commit
18cf641df4
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user