diff --git a/packages/ai/src/api/anthropic-messages.ts b/packages/ai/src/api/anthropic-messages.ts index 2221ce82..b2c6a220 100644 --- a/packages/ai/src/api/anthropic-messages.ts +++ b/packages/ai/src/api/anthropic-messages.ts @@ -1081,11 +1081,13 @@ function convertMessages( }); continue; } - if (block.thinking.trim().length === 0) continue; + const thinkingSignature = block.thinkingSignature; + const hasThinkingSignature = !!thinkingSignature && thinkingSignature.trim().length > 0; + if (block.thinking.trim().length === 0 && !hasThinkingSignature) continue; // If thinking signature is missing/empty (e.g., from aborted stream), // convert to plain text for Anthropic. Some compatible providers emit // and accept empty signatures, so let marked models preserve the block. - if (!block.thinkingSignature || block.thinkingSignature.trim().length === 0) { + if (!hasThinkingSignature) { blocks.push( allowEmptySignature ? { @@ -1102,7 +1104,7 @@ function convertMessages( blocks.push({ type: "thinking", thinking: sanitizeSurrogates(block.thinking), - signature: block.thinkingSignature, + signature: thinkingSignature, }); } } else if (block.type === "toolCall") { diff --git a/packages/ai/test/anthropic-empty-thinking-signature-compat.test.ts b/packages/ai/test/anthropic-empty-thinking-signature-compat.test.ts index f5c88368..0a3720fb 100644 --- a/packages/ai/test/anthropic-empty-thinking-signature-compat.test.ts +++ b/packages/ai/test/anthropic-empty-thinking-signature-compat.test.ts @@ -32,10 +32,10 @@ function makeModel(allowEmptySignature?: boolean): Model<"anthropic-messages"> { }; } -function makeContext(thinkingSignature: string): Context { +function makeContext(thinkingSignature: string, thinking = "internal reasoning"): Context { const assistant: AssistantMessage = { role: "assistant", - content: [{ type: "thinking", thinking: "internal reasoning", thinkingSignature }], + content: [{ type: "thinking", thinking, thinkingSignature }], provider: "xiaomi-token-plan-ams", api: "anthropic-messages", model: "mimo-v2.5-pro", @@ -80,6 +80,12 @@ describe("Anthropic empty thinking signature compat", () => { expect(assistant?.content).toEqual([{ type: "text", text: "internal reasoning" }]); }); + it("preserves empty thinking text when the signature is present", async () => { + const payload = await capturePayload(makeModel(), makeContext("signed-thinking", "")); + const assistant = payload.messages?.find((message) => message.role === "assistant"); + expect(assistant?.content).toEqual([{ type: "thinking", thinking: "", signature: "signed-thinking" }]); + }); + it("preserves empty-signature thinking when allowEmptySignature is enabled", async () => { const payload = await capturePayload(makeModel(true), makeContext(" ")); const assistant = payload.messages?.find((message) => message.role === "assistant");