Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

### 🔧 Bug Fixes

- **antigravity (orphan `tool_result` → 400):** requests routed through Antigravity (Claude-on-Vertex) could fail with `Each tool_result block must have a corresponding tool_use block in the previous message` when the client (e.g. the Antigravity IDE via AgentBridge/MITM) shipped a history whose first message already carried a `tool_result` with no preceding `tool_use` ([#6026](https://github.com/diegosouzapw/OmniRoute/issues/6026)). `AntigravityExecutor` overrides `BaseExecutor.execute()`, so the base tool-pair guard never ran on this path. Added `sanitizeAntigravityToolMessages()` (mirrors the base executor's Claude sequence: `fixToolPairs` → `fixToolAdjacency` → re-`fixToolPairs` → strip trailing orphan `tool_use`), applied to the upstream body before cloaking. Regression guard: `tests/unit/antigravity-orphan-toolresult-6026.test.ts`. (thanks @shabeer)
- **dashboard ("Update now" → Internal Server Error):** clicking **Update now** on the dashboard home could crash the page with a blank "Internal Server Error" screen (`Minified React error #31`). The handler POSTs the loopback-only `/api/system/version` auto-update endpoint and, on a non-OK JSON response (e.g. a `403` when the dashboard is reached through a reverse proxy / non-loopback origin), passed the raw error envelope object `{ error: { code, message, correlation_id } }` straight to `notify.error()`, which rendered the object as a React child and threw #31. The update-error path now funnels the body through `extractApiErrorMessage()` (the same safe extractor added in #5340), so a readable string always reaches the toast. Regression guard: `tests/unit/ui/home-update-error-render-5991.test.ts`. ([#5991](https://github.com/diegosouzapw/OmniRoute/issues/5991))

### 📝 Maintenance
Expand Down
34 changes: 34 additions & 0 deletions open-sse/executors/antigravity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ import {
getAntigravityModelFallbacks,
} from "../config/antigravityModelAliases.ts";
import { cloakAntigravityToolPayload } from "../config/toolCloaking.ts";
import {
fixToolPairs,
fixToolAdjacency,
stripTrailingAssistantOrphanToolUse,
} from "../services/contextManager.ts";
import {
shouldStripCloudCodeThinking,
stripCloudCodeThinkingConfig,
Expand Down Expand Up @@ -480,6 +485,31 @@ function sanitizeAntigravityGeminiRequest(
return clean;
}

/**
* Sanitize orphan tool_use / tool_result pairs on an Antigravity upstream body
* before it is sent to the (Claude-on-Vertex) backend (#6026).
*
* AntigravityExecutor overrides BaseExecutor.execute(), so it never reaches the
* base executor's tool-pair guard — a client (e.g. Antigravity IDE via
* AgentBridge) can ship a history whose first message already carries a
* `tool_result` with no preceding `tool_use`, which Anthropic rejects with
* "Each `tool_result` block must have a corresponding `tool_use` block in the
* previous message." Mirror the exact sequence base.ts uses for Claude-bound
* providers: strip orphan pairs, enforce strict adjacency, re-strip, then drop a
* trailing unmatched assistant(tool_use). Idempotent on clean histories.
*/
export function sanitizeAntigravityToolMessages(
body: Record<string, unknown>
): Record<string, unknown> {
if (!body || typeof body !== "object" || !Array.isArray(body.messages)) return body;
const messages = body.messages as Record<string, unknown>[];
const fixed = fixToolPairs(messages);
const adjacent = fixToolPairs(fixToolAdjacency(fixed));
const stripped = stripTrailingAssistantOrphanToolUse(adjacent);
body.messages = stripped;
return body;
}

export class AntigravityExecutor extends BaseExecutor {
constructor() {
super("antigravity", PROVIDERS.antigravity);
Expand Down Expand Up @@ -1143,6 +1173,10 @@ export class AntigravityExecutor extends BaseExecutor {
let transformedBody: Record<string, unknown> = transformed;

if (transformedBody && typeof transformedBody === "object") {
// #6026 — strip orphan tool_use/tool_result pairs before the upstream
// send (this executor overrides BaseExecutor.execute(), so the base
// guard never runs). Must precede cloaking, which only remaps names.
transformedBody = sanitizeAntigravityToolMessages(transformedBody);
const cloaked = cloakAntigravityToolPayload(transformedBody);
transformedBody = cloaked.body;
requestToolNameMap = cloaked.toolNameMap;
Expand Down
75 changes: 75 additions & 0 deletions tests/unit/antigravity-orphan-toolresult-6026.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import test from "node:test";
import assert from "node:assert/strict";

// Regression guard for #6026 — Antigravity IDE (via AgentBridge/MITM) can ship a
// history whose FIRST message already carries a `tool_result` with no preceding
// `tool_use`. Anthropic (Claude on Vertex, behind Antigravity) rejects it with:
// "messages.0.content.1: unexpected `tool_use_id` found in `tool_result` blocks:
// toolu_... Each `tool_result` block must have a corresponding `tool_use`
// block in the previous message."
// AntigravityExecutor overrides BaseExecutor.execute(), so the base tool-pair
// guard never ran on this path. sanitizeAntigravityToolMessages() now strips the
// orphan before the upstream send.

const { sanitizeAntigravityToolMessages } = await import(
"../../open-sse/executors/antigravity.ts"
);

test("#6026 strips an orphan tool_result at messages[0] (no preceding tool_use)", () => {
const body = {
model: "claude-opus-4.6",
messages: [
{
role: "user",
content: [
{ type: "text", text: "hi" },
{ type: "tool_result", tool_use_id: "toolu_vrtx_01N328k3yLh8R81CwtsUGipm", content: "x" },
],
},
],
};
const out = sanitizeAntigravityToolMessages(body);
const first = (out.messages as any[])[0];
// The orphan tool_result must be gone; the plain text block stays.
const hasOrphan = Array.isArray(first?.content)
? first.content.some((b: any) => b.type === "tool_result")
: false;
assert.equal(hasOrphan, false, "orphan tool_result must be stripped");
assert.ok(
first.content.some((b: any) => b.type === "text"),
"the real text block is preserved"
);
});

test("#6026 preserves a VALID tool_use → tool_result pair", () => {
const body = {
messages: [
{
role: "assistant",
content: [{ type: "tool_use", id: "toolu_ok", name: "read", input: {} }],
},
{
role: "user",
content: [{ type: "tool_result", tool_use_id: "toolu_ok", content: "done" }],
},
],
};
const out = sanitizeAntigravityToolMessages(body);
const msgs = out.messages as any[];
assert.ok(
msgs.some(
(m) =>
m.role === "user" &&
Array.isArray(m.content) &&
m.content.some((b: any) => b.type === "tool_result" && b.tool_use_id === "toolu_ok")
),
"a matched tool_result must be preserved"
);
});

test("#6026 is a no-op on bodies without a messages array", () => {
const noMsgs = { model: "x" };
assert.equal(sanitizeAntigravityToolMessages(noMsgs), noMsgs);
const badMsgs = { messages: "not-an-array" } as unknown as Record<string, unknown>;
assert.equal(sanitizeAntigravityToolMessages(badMsgs), badMsgs);
});
Loading