From 16e142ef7d9eb86c29e4466513874d5be3935bbd Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Thu, 8 Jan 2026 21:19:16 +0100 Subject: [PATCH] fix(ai): remove tag wrapping, convert to plain text on cross-model handoff - Remove tag generation from google-shared.ts, transorm-messages.ts, openai-completions.ts - Thinking blocks now convert to plain text when switching models (prevents models mimicking tags) - Skip empty thinking blocks to avoid API errors - Keep thinking blocks only when same provider AND same model fixes #561 --- packages/ai/CHANGELOG.md | 1 + packages/ai/src/providers/google-shared.ts | 14 +++++++++----- packages/ai/src/providers/openai-completions.ts | 6 ++---- packages/ai/src/providers/transorm-messages.ts | 7 ++++--- packages/coding-agent/CHANGELOG.md | 1 + 5 files changed, 17 insertions(+), 12 deletions(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 9de0768a..9c1e08d6 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -6,6 +6,7 @@ - Fixed Gemini CLI abort handling: detect native `AbortError` in retry catch block, cancel SSE reader when abort signal fires ([#568](https://github.com/badlogic/pi-mono/pull/568) by [@tmustier](https://github.com/tmustier)) - Fixed Antigravity provider 429 errors by aligning request payload with CLIProxyAPI v6.6.89: inject Antigravity system instruction with `role: "user"`, set `requestType: "agent"`, and use `antigravity` userAgent. Added bridge prompt to override Antigravity behavior (identity, paths, web dev guidelines) with Pi defaults. ([#571](https://github.com/badlogic/pi-mono/pull/571) by [@ben-vargas](https://github.com/ben-vargas)) +- Fixed thinking block handling for cross-model conversations: thinking blocks are now converted to plain text (no `` tags) when switching models. Previously, `` tags caused models to mimic the pattern and output literal tags. Also fixed empty thinking blocks causing API errors. ([#561](https://github.com/badlogic/pi-mono/issues/561)) ## [0.38.0] - 2026-01-08 diff --git a/packages/ai/src/providers/google-shared.ts b/packages/ai/src/providers/google-shared.ts index 50610a9c..52a83b9f 100644 --- a/packages/ai/src/providers/google-shared.ts +++ b/packages/ai/src/providers/google-shared.ts @@ -77,6 +77,8 @@ export function convertMessages(model: Model, contex } } else if (msg.role === "assistant") { const parts: Part[] = []; + // Check if message is from same provider and model - only then keep thinking blocks + const isSameProviderAndModel = msg.provider === model.provider && msg.model === model.id; for (const block of msg.content) { if (block.type === "text") { @@ -84,17 +86,19 @@ export function convertMessages(model: Model, contex if (!block.text || block.text.trim() === "") continue; parts.push({ text: sanitizeSurrogates(block.text) }); } 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) { + // Skip empty thinking blocks + if (!block.thinking || block.thinking.trim() === "") continue; + // Only keep as thinking block if same provider AND same model + // Otherwise convert to plain text (no tags to avoid model mimicking them) + if (isSameProviderAndModel) { parts.push({ thought: true, text: sanitizeSurrogates(block.thinking), - thoughtSignature: block.thinkingSignature, + ...(block.thinkingSignature && { thoughtSignature: block.thinkingSignature }), }); } else { parts.push({ - text: `\n${sanitizeSurrogates(block.thinking)}\n`, + text: sanitizeSurrogates(block.thinking), }); } } else if (block.type === "toolCall") { diff --git a/packages/ai/src/providers/openai-completions.ts b/packages/ai/src/providers/openai-completions.ts index 30ee6ca2..0c32784d 100644 --- a/packages/ai/src/providers/openai-completions.ts +++ b/packages/ai/src/providers/openai-completions.ts @@ -490,10 +490,8 @@ function convertMessages( const nonEmptyThinkingBlocks = thinkingBlocks.filter((b) => b.thinking && b.thinking.trim().length > 0); if (nonEmptyThinkingBlocks.length > 0) { if (compat.requiresThinkingAsText) { - // Convert thinking blocks to text with delimiters - const thinkingText = nonEmptyThinkingBlocks - .map((b) => `\n${b.thinking}\n`) - .join("\n"); + // Convert thinking blocks to plain text (no tags to avoid model mimicking them) + const thinkingText = nonEmptyThinkingBlocks.map((b) => b.thinking).join("\n\n"); const textContent = assistantMsg.content as Array<{ type: "text"; text: string }> | null; if (textContent) { textContent.unshift({ type: "text", text: thinkingText }); diff --git a/packages/ai/src/providers/transorm-messages.ts b/packages/ai/src/providers/transorm-messages.ts index a7226977..253416bb 100644 --- a/packages/ai/src/providers/transorm-messages.ts +++ b/packages/ai/src/providers/transorm-messages.ts @@ -45,12 +45,13 @@ export function transformMessages(messages: Message[], model: assistantMsg.api !== model.api; // Transform message from different provider/model - const transformedContent = assistantMsg.content.map((block) => { + const transformedContent = assistantMsg.content.flatMap((block) => { if (block.type === "thinking") { - // Convert thinking block to text block with tags + // Skip empty thinking blocks, convert others to plain text + if (!block.thinking || block.thinking.trim() === "") return []; return { type: "text" as const, - text: `\n${block.thinking}\n`, + text: block.thinking, }; } // Normalize tool call IDs for github-copilot cross-API switches diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index a9c287b8..0a163443 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -22,6 +22,7 @@ - ESC key now works during "Working..." state after auto-retry ([#568](https://github.com/badlogic/pi-mono/pull/568) by [@tmustier](https://github.com/tmustier)) - Abort messages now show correct retry attempt count (e.g., "Aborted after 2 retry attempts") ([#568](https://github.com/badlogic/pi-mono/pull/568) by [@tmustier](https://github.com/tmustier)) - Fixed Antigravity provider returning 429 errors despite available quota ([#571](https://github.com/badlogic/pi-mono/pull/571) by [@ben-vargas](https://github.com/ben-vargas)) +- Fixed malformed thinking text in Gemini/Antigravity responses where thinking content appeared as regular text or vice versa. Cross-model conversations now properly convert thinking blocks to plain text. ([#561](https://github.com/badlogic/pi-mono/issues/561)) ## [0.38.0] - 2026-01-08