From d6f8b6ac5162efb82df2465d8f457951c47a8812 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 4 Jan 2026 12:23:50 +0100 Subject: [PATCH] fix: update pi-ai patch and tests --- patches/@mariozechner__pi-ai.patch | 60 +++++++++---- pnpm-lock.yaml | 10 +-- src/providers/google-shared.test.ts | 128 ++++++++++++++++++++++++++-- 3 files changed, 169 insertions(+), 29 deletions(-) diff --git a/patches/@mariozechner__pi-ai.patch b/patches/@mariozechner__pi-ai.patch index 5bc1e51b0..0167c7b85 100644 --- a/patches/@mariozechner__pi-ai.patch +++ b/patches/@mariozechner__pi-ai.patch @@ -1,12 +1,42 @@ diff --git a/dist/providers/google-shared.js b/dist/providers/google-shared.js -index ff9cbcfebfac6b4370d85dc838f5cacf2a60ed64..42096c82aec925b412258348a36ba4a7025b283b 100644 --- a/dist/providers/google-shared.js +++ b/dist/providers/google-shared.js -@@ -140,6 +140,72 @@ export function convertMessages(model, context) { - } +@@ -52,19 +52,25 @@ + } + else if (block.type === "thinking") { + // Thinking blocks require signatures for Claude via Antigravity. +- // If signature is missing (e.g. from GPT-OSS), convert to regular text with delimiters. +- if (block.thinkingSignature) { ++ // Only send thought signatures for Claude models - Gemini doesn't support them ++ // and will mimic tags if we include them as text. ++ if (block.thinkingSignature && model.id.includes("claude")) { + parts.push({ + thought: true, + text: sanitizeSurrogates(block.thinking), + thoughtSignature: block.thinkingSignature, + }); + } +- else { +- parts.push({ +- text: `\n${sanitizeSurrogates(block.thinking)}\n`, +- }); ++ else if (!model.id.includes("gemini")) { ++ // For non-Gemini, non-Claude models, include as text with delimiters ++ // Skip entirely for Gemini to avoid it mimicking the pattern ++ if (block.thinking && block.thinking.trim()) { ++ parts.push({ ++ text: `\n${sanitizeSurrogates(block.thinking)}\n`, ++ }); ++ } + } ++ // For Gemini models without Claude signature: skip thinking blocks entirely + } + else if (block.type === "toolCall") { + const part = { +@@ -147,6 +153,77 @@ return contents; } -+/** + /** + * Sanitize JSON Schema for Google Cloud Code Assist API. + * Removes unsupported keywords like patternProperties, const, anyOf, etc. + * and converts to a format compatible with Google's function declarations. @@ -70,12 +100,18 @@ index ff9cbcfebfac6b4370d85dc838f5cacf2a60ed64..42096c82aec925b412258348a36ba4a7 + sanitized[key] = value; + } + } ++ // Ensure type: "object" is present when properties or required exist ++ // Google API requires type to be set when these fields are present ++ if (('properties' in sanitized || 'required' in sanitized) && !('type' in sanitized)) { ++ sanitized.type = 'object'; ++ } + return sanitized; +} - /** ++/** * Convert tools to Gemini function declarations format. */ -@@ -151,7 +216,7 @@ export function convertTools(tools) { + export function convertTools(tools) { +@@ -157,7 +234,7 @@ functionDeclarations: tools.map((tool) => ({ name: tool.name, description: tool.description, @@ -84,15 +120,3 @@ index ff9cbcfebfac6b4370d85dc838f5cacf2a60ed64..42096c82aec925b412258348a36ba4a7 })), }, ]; -diff --git a/dist/providers/openai-responses.js b/dist/providers/openai-responses.js -index 20fb0a22aaa28f7ff7c2f44a8b628fa1d9d7d936..31bae0aface1319487ce62d35f1f3b6ed334863e 100644 ---- a/dist/providers/openai-responses.js -+++ b/dist/providers/openai-responses.js -@@ -486,7 +486,6 @@ function convertTools(tools) { - name: tool.name, - description: tool.description, - parameters: tool.parameters, // TypeBox already generates JSON Schema -- strict: null, - })); - } - function mapStopReason(status) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 36128c3e2..8b9d6b142 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,7 +9,7 @@ overrides: patchedDependencies: '@mariozechner/pi-ai': - hash: 969db6f3f4cc91fec48124e1f5e515b386b1f1bed807769d0a80c28abadbaaae + hash: 85d925c76d088e594be0dc961cc06cb72fe8b408e28344c4e5a7989417442476 path: patches/@mariozechner__pi-ai.patch '@mariozechner/pi-coding-agent@0.32.3': hash: d0d5ffa1bfda8a0f9d14a5e73a074014346d3edbdb2ffc91444d3be5119f5745 @@ -33,7 +33,7 @@ importers: version: 0.32.3(ws@8.18.3)(zod@4.3.4) '@mariozechner/pi-ai': specifier: ^0.32.3 - version: 0.32.3(patch_hash=969db6f3f4cc91fec48124e1f5e515b386b1f1bed807769d0a80c28abadbaaae)(ws@8.18.3)(zod@4.3.4) + version: 0.32.3(patch_hash=85d925c76d088e594be0dc961cc06cb72fe8b408e28344c4e5a7989417442476)(ws@8.18.3)(zod@4.3.4) '@mariozechner/pi-coding-agent': specifier: ^0.32.3 version: 0.32.3(patch_hash=d0d5ffa1bfda8a0f9d14a5e73a074014346d3edbdb2ffc91444d3be5119f5745)(ws@8.18.3)(zod@4.3.4) @@ -3490,7 +3490,7 @@ snapshots: '@mariozechner/pi-agent-core@0.32.3(ws@8.18.3)(zod@4.3.4)': dependencies: - '@mariozechner/pi-ai': 0.32.3(patch_hash=969db6f3f4cc91fec48124e1f5e515b386b1f1bed807769d0a80c28abadbaaae)(ws@8.18.3)(zod@4.3.4) + '@mariozechner/pi-ai': 0.32.3(patch_hash=85d925c76d088e594be0dc961cc06cb72fe8b408e28344c4e5a7989417442476)(ws@8.18.3)(zod@4.3.4) '@mariozechner/pi-tui': 0.32.3 transitivePeerDependencies: - '@modelcontextprotocol/sdk' @@ -3500,7 +3500,7 @@ snapshots: - ws - zod - '@mariozechner/pi-ai@0.32.3(patch_hash=969db6f3f4cc91fec48124e1f5e515b386b1f1bed807769d0a80c28abadbaaae)(ws@8.18.3)(zod@4.3.4)': + '@mariozechner/pi-ai@0.32.3(patch_hash=85d925c76d088e594be0dc961cc06cb72fe8b408e28344c4e5a7989417442476)(ws@8.18.3)(zod@4.3.4)': dependencies: '@anthropic-ai/sdk': 0.71.2(zod@4.3.4) '@google/genai': 1.34.0 @@ -3523,7 +3523,7 @@ snapshots: '@mariozechner/pi-coding-agent@0.32.3(patch_hash=d0d5ffa1bfda8a0f9d14a5e73a074014346d3edbdb2ffc91444d3be5119f5745)(ws@8.18.3)(zod@4.3.4)': dependencies: '@mariozechner/pi-agent-core': 0.32.3(ws@8.18.3)(zod@4.3.4) - '@mariozechner/pi-ai': 0.32.3(patch_hash=969db6f3f4cc91fec48124e1f5e515b386b1f1bed807769d0a80c28abadbaaae)(ws@8.18.3)(zod@4.3.4) + '@mariozechner/pi-ai': 0.32.3(patch_hash=85d925c76d088e594be0dc961cc06cb72fe8b408e28344c4e5a7989417442476)(ws@8.18.3)(zod@4.3.4) '@mariozechner/pi-tui': 0.32.3 chalk: 5.6.2 cli-highlight: 2.1.11 diff --git a/src/providers/google-shared.test.ts b/src/providers/google-shared.test.ts index 4e07d2421..a50e52727 100644 --- a/src/providers/google-shared.test.ts +++ b/src/providers/google-shared.test.ts @@ -1,5 +1,8 @@ -import { convertTools } from "@mariozechner/pi-ai/dist/providers/google-shared.js"; -import type { Tool } from "@mariozechner/pi-ai/dist/types.js"; +import { + convertMessages, + convertTools, +} from "@mariozechner/pi-ai/dist/providers/google-shared.js"; +import type { Context, Model, Tool } from "@mariozechner/pi-ai/dist/types.js"; import { describe, expect, it } from "vitest"; const asRecord = (value: unknown): Record => { @@ -9,9 +12,47 @@ const asRecord = (value: unknown): Record => { return value as Record; }; +const makeModel = (id: string): Model<"google-generative-ai"> => + ({ + id, + name: id, + api: "google-generative-ai", + provider: "google", + baseUrl: "https://example.invalid", + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 1, + maxTokens: 1, + }) as Model<"google-generative-ai">; + describe("google-shared convertTools", () => { + it("adds type:object when properties/required exist but type is missing", () => { + const tools = [ + { + name: "noType", + description: "Tool with properties but no type", + parameters: { + properties: { + action: { type: "string" }, + }, + required: ["action"], + }, + }, + ] as unknown as Tool[]; + + const converted = convertTools(tools); + const params = asRecord( + converted?.[0]?.functionDeclarations?.[0]?.parameters, + ); + + expect(params.type).toBe("object"); + expect(params.properties).toBeDefined(); + expect(params.required).toEqual(["action"]); + }); + it("strips unsupported JSON Schema keywords", () => { - const tools: Tool[] = [ + const tools = [ { name: "example", description: "Example tool", @@ -40,7 +81,7 @@ describe("google-shared convertTools", () => { required: ["mode"], }, }, - ]; + ] as unknown as Tool[]; const converted = convertTools(tools); const params = asRecord( @@ -61,7 +102,7 @@ describe("google-shared convertTools", () => { }); it("keeps supported schema fields", () => { - const tools: Tool[] = [ + const tools = [ { name: "settings", description: "Settings tool", @@ -83,7 +124,7 @@ describe("google-shared convertTools", () => { required: ["config"], }, }, - ]; + ] as unknown as Tool[]; const converted = convertTools(tools); const params = asRecord( @@ -104,3 +145,78 @@ describe("google-shared convertTools", () => { expect(params.required).toEqual(["config"]); }); }); + +describe("google-shared convertMessages", () => { + it("skips thinking blocks for Gemini to avoid mimicry", () => { + const model = makeModel("gemini-1.5-pro"); + const context = { + messages: [ + { + role: "assistant", + content: [ + { + type: "thinking", + thinking: "hidden", + thinkingSignature: "sig", + }, + ], + api: "google-generative-ai", + provider: "google", + model: "gemini-1.5-pro", + usage: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + totalTokens: 0, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, + }, + stopReason: "stop", + timestamp: 0, + }, + ], + } as unknown as Context; + + const contents = convertMessages(model, context); + expect(contents).toHaveLength(0); + }); + + it("keeps thought signatures for Claude models", () => { + const model = makeModel("claude-3-opus"); + const context = { + messages: [ + { + role: "assistant", + content: [ + { + type: "thinking", + thinking: "structured", + thinkingSignature: "sig", + }, + ], + api: "google-generative-ai", + provider: "google", + model: "claude-3-opus", + usage: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + totalTokens: 0, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, + }, + stopReason: "stop", + timestamp: 0, + }, + ], + } as unknown as Context; + + const contents = convertMessages(model, context); + const parts = contents?.[0]?.parts ?? []; + expect(parts).toHaveLength(1); + expect(parts[0]).toMatchObject({ + thought: true, + thoughtSignature: "sig", + }); + }); +});