diff --git a/packages/ai/src/providers/anthropic.ts b/packages/ai/src/providers/anthropic.ts index 2fe83370..d40224f2 100644 --- a/packages/ai/src/providers/anthropic.ts +++ b/packages/ai/src/providers/anthropic.ts @@ -26,7 +26,7 @@ import { AssistantMessageEventStream } from "../utils/event-stream.js"; import { parseStreamingJson } from "../utils/json-parse.js"; import { sanitizeSurrogates } from "../utils/sanitize-unicode.js"; -import { transformMessages } from "./transorm-messages.js"; +import { transformMessages } from "./transform-messages.js"; // Stealth mode: Mimic Claude Code's tool naming exactly const claudeCodeVersion = "2.1.2"; diff --git a/packages/ai/src/providers/google-shared.ts b/packages/ai/src/providers/google-shared.ts index 60d8b058..9c2381a7 100644 --- a/packages/ai/src/providers/google-shared.ts +++ b/packages/ai/src/providers/google-shared.ts @@ -5,7 +5,7 @@ import { type Content, FinishReason, FunctionCallingConfigMode, type Part, type Schema } from "@google/genai"; import type { Context, ImageContent, Model, StopReason, TextContent, Tool } from "../types.js"; import { sanitizeSurrogates } from "../utils/sanitize-unicode.js"; -import { transformMessages } from "./transorm-messages.js"; +import { transformMessages } from "./transform-messages.js"; type GoogleApiType = "google-generative-ai" | "google-gemini-cli" | "google-vertex"; @@ -42,6 +42,22 @@ export function retainThoughtSignature(existing: string | undefined, incoming: s return existing; } +// Thought signatures must be base64 for Google APIs (TYPE_BYTES). +const base64SignaturePattern = /^[A-Za-z0-9+/]+={0,2}$/; + +function isValidThoughtSignature(signature: string | undefined): boolean { + if (!signature) return false; + if (signature.length % 4 !== 0) return false; + return base64SignaturePattern.test(signature); +} + +/** + * Only keep signatures from the same provider/model and with valid base64. + */ +function resolveThoughtSignature(isSameProviderAndModel: boolean, signature: string | undefined): string | undefined { + return isSameProviderAndModel && isValidThoughtSignature(signature) ? signature : undefined; +} + /** * Convert internal messages to Gemini Content[] format. */ @@ -85,9 +101,10 @@ export function convertMessages(model: Model, contex if (block.type === "text") { // Skip empty text blocks - they can cause issues with some models (e.g. Claude via Antigravity) if (!block.text || block.text.trim() === "") continue; + const thoughtSignature = resolveThoughtSignature(isSameProviderAndModel, block.textSignature); parts.push({ text: sanitizeSurrogates(block.text), - ...(block.textSignature && { thoughtSignature: block.textSignature }), + ...(thoughtSignature && { thoughtSignature }), }); } else if (block.type === "thinking") { // Skip empty thinking blocks @@ -95,10 +112,11 @@ export function convertMessages(model: Model, contex // 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) { + const thoughtSignature = resolveThoughtSignature(isSameProviderAndModel, block.thinkingSignature); parts.push({ thought: true, text: sanitizeSurrogates(block.thinking), - ...(block.thinkingSignature && { thoughtSignature: block.thinkingSignature }), + ...(thoughtSignature && { thoughtSignature }), }); } else { parts.push({ @@ -112,8 +130,9 @@ export function convertMessages(model: Model, contex args: block.arguments, }, }; - if (block.thoughtSignature) { - part.thoughtSignature = block.thoughtSignature; + const thoughtSignature = resolveThoughtSignature(isSameProviderAndModel, block.thoughtSignature); + if (thoughtSignature) { + part.thoughtSignature = thoughtSignature; } parts.push(part); } diff --git a/packages/ai/src/providers/openai-codex-responses.ts b/packages/ai/src/providers/openai-codex-responses.ts index cbb73dbc..4bbda74a 100644 --- a/packages/ai/src/providers/openai-codex-responses.ts +++ b/packages/ai/src/providers/openai-codex-responses.ts @@ -42,7 +42,7 @@ import { transformRequestBody, } from "./openai-codex/request-transformer.js"; import { parseCodexError, parseCodexSseStream } from "./openai-codex/response-handler.js"; -import { transformMessages } from "./transorm-messages.js"; +import { transformMessages } from "./transform-messages.js"; export interface OpenAICodexResponsesOptions extends StreamOptions { reasoningEffort?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh"; diff --git a/packages/ai/src/providers/openai-completions.ts b/packages/ai/src/providers/openai-completions.ts index 091996ff..37c2dd13 100644 --- a/packages/ai/src/providers/openai-completions.ts +++ b/packages/ai/src/providers/openai-completions.ts @@ -27,7 +27,7 @@ import type { import { AssistantMessageEventStream } from "../utils/event-stream.js"; import { parseStreamingJson } from "../utils/json-parse.js"; import { sanitizeSurrogates } from "../utils/sanitize-unicode.js"; -import { transformMessages } from "./transorm-messages.js"; +import { transformMessages } from "./transform-messages.js"; /** * Normalize tool call ID for Mistral. diff --git a/packages/ai/src/providers/openai-responses.ts b/packages/ai/src/providers/openai-responses.ts index bd760440..c10dd68e 100644 --- a/packages/ai/src/providers/openai-responses.ts +++ b/packages/ai/src/providers/openai-responses.ts @@ -28,7 +28,7 @@ import type { import { AssistantMessageEventStream } from "../utils/event-stream.js"; import { parseStreamingJson } from "../utils/json-parse.js"; import { sanitizeSurrogates } from "../utils/sanitize-unicode.js"; -import { transformMessages } from "./transorm-messages.js"; +import { transformMessages } from "./transform-messages.js"; /** Fast deterministic hash to shorten long strings */ function shortHash(str: string): string { diff --git a/packages/ai/src/providers/transorm-messages.ts b/packages/ai/src/providers/transform-messages.ts similarity index 100% rename from packages/ai/src/providers/transorm-messages.ts rename to packages/ai/src/providers/transform-messages.ts