mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
This commit is contained in:
committed by
GitHub
parent
2a20e5ca47
commit
29e04b7089
@@ -25,6 +25,8 @@
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- fix(providers): strip orphan tool_result blocks on the Antigravity MITM path before forwarding to Claude ([#6026](https://github.com/diegosouzapw/OmniRoute/issues/6026))
|
||||
|
||||
- **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))
|
||||
|
||||
- **kiro (system prompt leaked as raw user text):** when Claude Code routed through the Kiro/CodeWhisperer backend, the `system` message was normalized to a `user` turn with no wrapper, so the entire system prompt (environment info, tool definitions, memory instructions, etc.) appeared as if the user had typed it — polluting the model context. System-origin content is now wrapped in `<system-reminder>` tags before being merged into the Kiro user message, so the model can distinguish it from real user input. Real user turns are untouched. Regression guard: `tests/unit/kiro-system-reminder-2306.test.ts`. (thanks @VitzS7)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { register } from "../registry.ts";
|
||||
import { FORMATS } from "../formats.ts";
|
||||
import { adjustMaxTokens } from "../helpers/maxTokensHelper.ts";
|
||||
import { fixToolPairs } from "../../services/contextManager.ts";
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
|
||||
@@ -96,6 +97,19 @@ export function antigravityToOpenAIRequest(model, body, stream) {
|
||||
}
|
||||
}
|
||||
|
||||
// Guard against orphan tool_result/tool_use pairs (#6026). Antigravity IDE can ship a
|
||||
// truncated history whose first turn is a `functionResponse` with no preceding
|
||||
// `functionCall`. Left untouched, that becomes an orphan `role:"tool"` message here and,
|
||||
// after the openai→claude step, an orphan `tool_result` block — which Anthropic (Vertex
|
||||
// `claude-opus-4.6`) rejects with `unexpected tool_use_id found in tool_result blocks`.
|
||||
// `fixToolPairs` strips only genuine orphans and is idempotent on well-formed histories,
|
||||
// so paired functionCall/functionResponse turns pass through unchanged. This mirrors the
|
||||
// executor-side guard in `executors/base.ts` / `services/claudeCodeCompatible.ts`; the
|
||||
// Antigravity MITM path did not run it (no `fixToolPairs` under `src/mitm/`). We do NOT
|
||||
// run `fixToolAdjacency` here because this stage still emits OpenAI-format messages and
|
||||
// Claude's adjacency rule is enforced downstream per provider.
|
||||
result.messages = fixToolPairs(result.messages) as JsonRecord[];
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
127
tests/unit/antigravity-orphan-toolresult-6026.test.ts
Normal file
127
tests/unit/antigravity-orphan-toolresult-6026.test.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* Regression test for #6026.
|
||||
*
|
||||
* Antigravity IDE (via AgentBridge/MITM → `/v1/antigravity` → translator) can ship a
|
||||
* truncated history whose FIRST turn is a tool result (`functionResponse`) with no
|
||||
* preceding tool call. When that survives the antigravity→openai→claude chain, Anthropic
|
||||
* (Vertex `claude-opus-4.6`) rejects it with HTTP 400:
|
||||
*
|
||||
* messages.0.content.1: unexpected tool_use_id found in tool_result blocks:
|
||||
* toolu_vrtx_...: Each tool_result block must have a corresponding tool_use block in
|
||||
* the previous message.
|
||||
*
|
||||
* The fix strips orphan tool_results at the antigravity message-assembly point
|
||||
* (`antigravityToOpenAIRequest`) by reusing `fixToolPairs`, so the orphan never reaches
|
||||
* the upstream Claude request.
|
||||
*
|
||||
* PURE-FUNCTION ONLY — this test imports the translator + sanitizer functions directly.
|
||||
* It must NEVER start the MITM proxy, bind :443/:80, touch /etc/hosts, or install a CA.
|
||||
*/
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const { antigravityToOpenAIRequest } = await import(
|
||||
"../../open-sse/translator/request/antigravity-to-openai.ts"
|
||||
);
|
||||
const { fixToolPairs } = await import("../../open-sse/services/contextManager.ts");
|
||||
|
||||
test("#6026: antigravityToOpenAIRequest strips an orphan functionResponse (no preceding functionCall)", () => {
|
||||
const result = antigravityToOpenAIRequest(
|
||||
"ag/claude-opus-4-6",
|
||||
{
|
||||
request: {
|
||||
contents: [
|
||||
{
|
||||
// First (and only) turn is a tool result with NO preceding tool call.
|
||||
role: "user",
|
||||
parts: [
|
||||
{
|
||||
functionResponse: {
|
||||
id: "toolu_vrtx_test",
|
||||
name: "read_file",
|
||||
response: { result: { ok: true } },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
// The orphan tool message must be gone — otherwise the openai→claude step would emit an
|
||||
// orphan tool_result block and Anthropic would 400.
|
||||
const orphan = result.messages.find(
|
||||
(m: Record<string, unknown>) => m.role === "tool" && m.tool_call_id === "toolu_vrtx_test"
|
||||
);
|
||||
assert.equal(orphan, undefined, "orphan tool_result message must be stripped");
|
||||
assert.equal(
|
||||
result.messages.some((m: Record<string, unknown>) => m.role === "tool"),
|
||||
false,
|
||||
"no orphan tool messages should remain"
|
||||
);
|
||||
});
|
||||
|
||||
test("#6026: well-formed functionCall/functionResponse pair is preserved (no regression)", () => {
|
||||
const result = antigravityToOpenAIRequest(
|
||||
"ag/claude-opus-4-6",
|
||||
{
|
||||
request: {
|
||||
contents: [
|
||||
{
|
||||
role: "model",
|
||||
parts: [{ functionCall: { id: "toolu_vrtx_ok", name: "read_file", args: {} } }],
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
parts: [
|
||||
{
|
||||
functionResponse: {
|
||||
id: "toolu_vrtx_ok",
|
||||
name: "read_file",
|
||||
response: { result: { ok: true } },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
const assistant = result.messages.find(
|
||||
(m: Record<string, unknown>) => m.role === "assistant"
|
||||
);
|
||||
const tool = result.messages.find((m: Record<string, unknown>) => m.role === "tool");
|
||||
assert.ok(assistant, "assistant tool_call message must survive");
|
||||
assert.ok(tool, "matched tool_result message must survive");
|
||||
assert.equal((tool as Record<string, unknown>).tool_call_id, "toolu_vrtx_ok");
|
||||
});
|
||||
|
||||
test("#6026: fixToolPairs removes the exact Anthropic-shape orphan tool_result block", () => {
|
||||
// Mirrors the reporter's failing body: messages[0] is a user message whose content array
|
||||
// holds a tool_result block with no matching tool_use anywhere in the request.
|
||||
const messages: Record<string, unknown>[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: "continue" },
|
||||
{ type: "tool_result", tool_use_id: "toolu_vrtx_test", content: "stale" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const fixed = fixToolPairs(messages);
|
||||
|
||||
const stillHasOrphan = fixed.some(
|
||||
(m) =>
|
||||
m.role === "user" &&
|
||||
Array.isArray(m.content) &&
|
||||
(m.content as Record<string, unknown>[]).some(
|
||||
(b) => b.type === "tool_result" && b.tool_use_id === "toolu_vrtx_test"
|
||||
)
|
||||
);
|
||||
assert.equal(stillHasOrphan, false, "orphan tool_result block must be stripped");
|
||||
});
|
||||
@@ -121,7 +121,11 @@ test("Antigravity -> OpenAI extracts string system instructions and text-only me
|
||||
]);
|
||||
});
|
||||
|
||||
test("Antigravity -> OpenAI returns tool messages when content contains only function responses", () => {
|
||||
test("Antigravity -> OpenAI strips a lone function response with no matching function call (#6026)", () => {
|
||||
// A `functionResponse` with no preceding `functionCall` is an orphan tool_result. Left in
|
||||
// place it becomes an orphan `tool_result` block after the openai→claude step, which
|
||||
// Anthropic (Vertex claude-opus-4.6) rejects with HTTP 400 (#6026). `fixToolPairs` now
|
||||
// strips it at the antigravity assembly point, so the upstream request stays valid.
|
||||
const result = antigravityToOpenAIRequest(
|
||||
"gpt-4o",
|
||||
{
|
||||
@@ -145,16 +149,10 @@ test("Antigravity -> OpenAI returns tool messages when content contains only fun
|
||||
false
|
||||
);
|
||||
|
||||
assert.deepEqual(result.messages, [
|
||||
{
|
||||
role: "tool",
|
||||
tool_call_id: "call_2",
|
||||
content: '{"ok":true}',
|
||||
},
|
||||
]);
|
||||
assert.deepEqual(result.messages, []);
|
||||
});
|
||||
|
||||
test("Antigravity -> OpenAI keeps co-located function response, function call and text", () => {
|
||||
test("Antigravity -> OpenAI keeps co-located function call and text but strips the orphan function response (#6026)", () => {
|
||||
const result = antigravityToOpenAIRequest(
|
||||
"gpt-4o",
|
||||
{
|
||||
@@ -174,11 +172,12 @@ test("Antigravity -> OpenAI keeps co-located function response, function call an
|
||||
false
|
||||
);
|
||||
|
||||
// Both the tool-result message AND the accompanying assistant message must survive.
|
||||
// The accompanying assistant message (text + tool_call) survives, but the co-located
|
||||
// function response `call_9` has no matching function call, so it is an orphan tool_result
|
||||
// and must be stripped (#6026) — otherwise Anthropic 400s on the openai→claude request.
|
||||
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.equal(toolMsg, undefined, "orphan function-response tool message must be stripped");
|
||||
assert.ok(assistantMsg, "expected a role:assistant message");
|
||||
assert.equal(assistantMsg.content, "Let me look that up.");
|
||||
assert.deepEqual(assistantMsg.tool_calls, [
|
||||
|
||||
Reference in New Issue
Block a user