fix(antigravity): update user-agent and fix schema validation

- Update hardcoded User-Agent `antigravity/1.11.5` to `1.15.8` in `@mariozechner/pi-ai` via pnpm patch to resolve "version not supported" error.
- Add `sanitizeToolUseInput` helper to ensure `tool_use` payloads include the required `input` field.
- Update `src/infra` usage fetcher User-Agent to match supported version.
This commit is contained in:
Harry Wu 2026-01-30 14:58:15 +08:00
parent 6af205a13a
commit f2958be6e0
9 changed files with 217 additions and 8 deletions

View File

@ -250,6 +250,9 @@
"@sinclair/typebox": "0.34.47",
"hono": "4.11.4",
"tar": "7.5.4"
},
"patchedDependencies": {
"@mariozechner/pi-ai": "patches/@mariozechner__pi-ai.patch"
}
},
"vitest": {

View File

@ -0,0 +1,13 @@
diff --git a/dist/providers/google-gemini-cli.js b/dist/providers/google-gemini-cli.js
index 3741764906f41e87eda9259f567b1d5332551a63..41fc6a92fc4c1f6f503b92320248391013badfb0 100644
--- a/dist/providers/google-gemini-cli.js
+++ b/dist/providers/google-gemini-cli.js
@@ -22,7 +22,7 @@ const GEMINI_CLI_HEADERS = {
};
// Headers for Antigravity (sandbox endpoint) - requires specific User-Agent
const ANTIGRAVITY_HEADERS = {
- "User-Agent": "antigravity/1.11.5 darwin/arm64",
+ "User-Agent": "antigravity/1.15.8 darwin/arm64",
"X-Goog-Api-Client": "google-cloud-sdk vscode_cloudshelleditor/0.1",
"Client-Metadata": JSON.stringify({
ideType: "IDE_UNSPECIFIED",

13
pnpm-lock.yaml generated
View File

@ -9,6 +9,11 @@ overrides:
hono: 4.11.4
tar: 7.5.4
patchedDependencies:
'@mariozechner/pi-ai':
hash: a959dedd4f17a3a05dc9bfe16a6ad07d57d53abd992ee4526e451a80c4909c36
path: patches/@mariozechner__pi-ai.patch
importers:
.:
@ -45,7 +50,7 @@ importers:
version: 0.49.3(ws@8.19.0)(zod@4.3.6)
'@mariozechner/pi-ai':
specifier: 0.49.3
version: 0.49.3(ws@8.19.0)(zod@4.3.6)
version: 0.49.3(patch_hash=a959dedd4f17a3a05dc9bfe16a6ad07d57d53abd992ee4526e451a80c4909c36)(ws@8.19.0)(zod@4.3.6)
'@mariozechner/pi-coding-agent':
specifier: 0.49.3
version: 0.49.3(ws@8.19.0)(zod@4.3.6)
@ -6996,7 +7001,7 @@ snapshots:
'@mariozechner/pi-agent-core@0.49.3(ws@8.19.0)(zod@4.3.6)':
dependencies:
'@mariozechner/pi-ai': 0.49.3(ws@8.19.0)(zod@4.3.6)
'@mariozechner/pi-ai': 0.49.3(patch_hash=a959dedd4f17a3a05dc9bfe16a6ad07d57d53abd992ee4526e451a80c4909c36)(ws@8.19.0)(zod@4.3.6)
'@mariozechner/pi-tui': 0.49.3
transitivePeerDependencies:
- '@modelcontextprotocol/sdk'
@ -7007,7 +7012,7 @@ snapshots:
- ws
- zod
'@mariozechner/pi-ai@0.49.3(ws@8.19.0)(zod@4.3.6)':
'@mariozechner/pi-ai@0.49.3(patch_hash=a959dedd4f17a3a05dc9bfe16a6ad07d57d53abd992ee4526e451a80c4909c36)(ws@8.19.0)(zod@4.3.6)':
dependencies:
'@anthropic-ai/sdk': 0.71.2(zod@4.3.6)
'@aws-sdk/client-bedrock-runtime': 3.972.0
@ -7034,7 +7039,7 @@ snapshots:
'@mariozechner/clipboard': 0.3.0
'@mariozechner/jiti': 2.6.5
'@mariozechner/pi-agent-core': 0.49.3(ws@8.19.0)(zod@4.3.6)
'@mariozechner/pi-ai': 0.49.3(ws@8.19.0)(zod@4.3.6)
'@mariozechner/pi-ai': 0.49.3(patch_hash=a959dedd4f17a3a05dc9bfe16a6ad07d57d53abd992ee4526e451a80c4909c36)(ws@8.19.0)(zod@4.3.6)
'@mariozechner/pi-tui': 0.49.3
'@silvia-odwyer/photon-node': 0.3.4
chalk: 5.6.2

View File

@ -32,7 +32,11 @@ export {
parseImageDimensionError,
parseImageSizeError,
} from "./pi-embedded-helpers/errors.js";
export { isGoogleModelApi, sanitizeGoogleTurnOrdering } from "./pi-embedded-helpers/google.js";
export {
isGoogleModelApi,
sanitizeGoogleTurnOrdering,
sanitizeToolUseInput,
} from "./pi-embedded-helpers/google.js";
export { downgradeOpenAIReasoningBlocks } from "./pi-embedded-helpers/openai.js";
export {

View File

@ -0,0 +1,88 @@
import { describe, it, expect } from "vitest";
import type { AgentMessage } from "@mariozechner/pi-agent-core";
import { sanitizeToolUseInput } from "./google.js";
describe("sanitizeToolUseInput", () => {
it("should add empty input to toolUse blocks missing it", () => {
const messages: AgentMessage[] = [
{
role: "assistant",
content: [
{
type: "toolUse",
id: "tool-1",
name: "readFile",
// missing input
} as any,
{
type: "text",
text: "Searching...",
},
],
},
];
const sanitized = sanitizeToolUseInput(messages);
const content = sanitized[0].content;
const toolUse = (Array.isArray(content) ? content[0] : null) as any;
expect(toolUse).toBeDefined();
expect(toolUse.input).toEqual({});
});
it("should preserve existing input", () => {
const messages: AgentMessage[] = [
{
role: "assistant",
content: [
{
type: "toolUse",
id: "tool-2",
name: "writeFile",
input: { path: "foo.txt" },
},
],
},
];
const sanitized = sanitizeToolUseInput(messages);
const content = sanitized[0].content;
const toolUse = (Array.isArray(content) ? content[0] : null) as any;
expect(toolUse.input).toEqual({ path: "foo.txt" });
});
it("should handle non-array content gracefully", () => {
const messages: AgentMessage[] = [
{
role: "user",
content: "Hello",
},
];
const sanitized = sanitizeToolUseInput(messages);
expect(sanitized).toEqual(messages);
});
it("should recurse through all messages", () => {
const messages: AgentMessage[] = [
{ role: "user", content: "Hi" },
{
role: "assistant",
content: [
{ type: "toolUse", id: "1", name: "a" } as any, // fix me
],
},
{
role: "assistant",
content: [
{ type: "toolUse", id: "2", name: "b", input: { foo: 1 } }, // leave me
],
},
];
const sanitized = sanitizeToolUseInput(messages);
expect((sanitized[1].content as any[])[0].input).toEqual({});
expect((sanitized[2].content as any[])[0].input).toEqual({ foo: 1 });
});
});

View File

@ -18,3 +18,25 @@ export function isAntigravityClaude(params: {
}
export { sanitizeGoogleTurnOrdering };
export function sanitizeToolUseInput(messages: any[]): any[] {
return messages.map((msg) => {
if (!msg || typeof msg !== "object") return msg;
if (msg.role !== "assistant" && msg.role !== "toolUse") return msg;
if (!Array.isArray(msg.content)) return msg;
return {
...msg,
content: msg.content.map((block: any) => {
if (!block || typeof block !== "object") return block;
if (block.type === "toolUse" || block.type === "toolCall") {
// If input is missing, add empty object
if (!("input" in block) || block.input === undefined) {
return { ...block, input: {} };
}
}
return block;
}),
};
});
}

View File

@ -11,6 +11,7 @@ import {
isGoogleModelApi,
sanitizeGoogleTurnOrdering,
sanitizeSessionMessagesImages,
sanitizeToolUseInput,
} from "../pi-embedded-helpers.js";
import { sanitizeToolUseResultPairing } from "../session-transcript-repair.js";
import { log } from "./logger.js";
@ -336,6 +337,9 @@ export async function sanitizeSessionHistory(params: {
? sanitizeToolUseResultPairing(sanitizedThinking)
: sanitizedThinking;
// Ensure toolUse blocks have input field (fixes schema validation "Field required")
const sanitizedInputs = sanitizeToolUseInput(repairedTools);
const isOpenAIResponsesApi =
params.modelApi === "openai-responses" || params.modelApi === "openai-codex-responses";
const hasSnapshot = Boolean(params.provider || params.modelApi || params.modelId);
@ -350,8 +354,8 @@ export async function sanitizeSessionHistory(params: {
: false;
const sanitizedOpenAI =
isOpenAIResponsesApi && modelChanged
? downgradeOpenAIReasoningBlocks(repairedTools)
: repairedTools;
? downgradeOpenAIReasoningBlocks(sanitizedInputs)
: sanitizedInputs;
if (hasSnapshot && (!priorSnapshot || modelChanged)) {
appendModelSnapshot(params.sessionManager, {

View File

@ -174,7 +174,7 @@ export async function fetchAntigravityUsage(
const headers: Record<string, string> = {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
"User-Agent": "antigravity",
"User-Agent": "antigravity/1.15.8",
"X-Goog-Api-Client": "google-cloud-sdk vscode_cloudshelleditor/0.1",
};

70
verify_logic.mjs Normal file
View File

@ -0,0 +1,70 @@
// Logic from src/agents/pi-embedded-helpers/google.ts
function sanitizeToolUseInput(messages) {
return messages.map((msg) => {
if (!msg || typeof msg !== "object") return msg;
if (msg.role !== "assistant" && msg.role !== "toolUse") return msg;
if (!Array.isArray(msg.content)) return msg;
return {
...msg,
content: msg.content.map((block) => {
if (!block || typeof block !== "object") return block;
if (block.type === "toolUse" || block.type === "toolCall") {
// If input is missing, add empty object
if (!("input" in block) || block.input === undefined) {
return { ...block, input: {} };
}
}
return block;
}),
};
});
}
// Test cases
function runTests() {
console.log("Running tests...");
let passed = 0;
let failed = 0;
function assert(condition, message) {
if (condition) {
console.log(`✅ PASS: ${message}`);
passed++;
} else {
console.error(`❌ FAIL: ${message}`);
failed++;
}
}
// Test 1: Add empty input
const msgs1 = [
{
role: "assistant",
content: [{ type: "toolUse", id: "1", name: "f" }],
},
];
const res1 = sanitizeToolUseInput(msgs1);
assert(res1[0].content[0].input && Object.keys(res1[0].content[0].input).length === 0, "Should add empty input");
// Test 2: Preserve existing input
const msgs2 = [
{
role: "assistant",
content: [{ type: "toolUse", id: "2", name: "f", input: { a: 1 } }],
},
];
const res2 = sanitizeToolUseInput(msgs2);
assert(res2[0].content[0].input.a === 1, "Should preserve existing input");
// Test 3: Ignore other roles
const msgs3 = [{ role: "user", content: "hi" }];
const res3 = sanitizeToolUseInput(msgs3);
assert(res3[0] === msgs3[0], "Should ignore user messages");
console.log(`\nResults: ${passed} passed, ${failed} failed.`);
if (failed > 0) process.exit(1);
}
runTests();