From ccb8472daf1674e6bba4e7be2e0ba2e6bf120ca2 Mon Sep 17 00:00:00 2001 From: Vyctor Huggo Przozwski da Silva <51521767+vyctorbrzezowski@users.noreply.github.com> Date: Wed, 29 Apr 2026 16:16:52 -0300 Subject: [PATCH] fix(agents): preserve seeded Anthropic text blocks * fix(agents): preserve seeded Anthropic text blocks * docs(changelog): note Anthropic seeded block fix --------- Co-authored-by: Peter Steinberger --- CHANGELOG.md | 1 + src/agents/anthropic-transport-stream.test.ts | 76 +++++++++++++++++++ src/agents/anthropic-transport-stream.ts | 39 ++++++++-- 3 files changed, 110 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce193fc21c2..16ce2bef13b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Anthropic/Meridian: preserve text and thinking content seeded on `content_block_start` in anthropic-messages streams, so `[thinking, text]` replies no longer persist as empty turns or trigger empty-response fallbacks. Fixes #74410. Thanks @vyctorbrzezowski. - Media: include redacted per-attempt resize failures and resolved model input capabilities in vision-pipeline errors so ARM64 image failures are diagnosable without closing the remaining routing investigation. Refs #74552. Thanks @1yihui. - Auto-reply: honor explicit `silentReply.direct: "allow"` for clean empty or reasoning-only direct chat turns while keeping the default direct-chat empty-response guard conservative. Fixes #74409. Thanks @jesuskannolis. - OpenAI Codex: send a non-empty Responses input item when a Codex turn only has systemPrompt-backed instructions, avoiding ChatGPT backend 400s from `input: []`. Fixes #73820. Thanks @woodhouse-bot. diff --git a/src/agents/anthropic-transport-stream.test.ts b/src/agents/anthropic-transport-stream.test.ts index 0c094315036..d309c629621 100644 --- a/src/agents/anthropic-transport-stream.test.ts +++ b/src/agents/anthropic-transport-stream.test.ts @@ -457,6 +457,82 @@ describe("anthropic transport stream", () => { ); }); + it("preserves text seeded on a text block after a thinking block", async () => { + guardedFetchMock.mockResolvedValueOnce( + createSseResponse([ + { + type: "message_start", + message: { id: "msg_1", usage: { input_tokens: 6, output_tokens: 0 } }, + }, + { + type: "content_block_start", + index: 0, + content_block: { type: "thinking", thinking: "checking", signature: "sig_1" }, + }, + { + type: "content_block_delta", + index: 0, + delta: { type: "signature_delta", signature: "sig_2" }, + }, + { + type: "content_block_stop", + index: 0, + }, + { + type: "content_block_start", + index: 1, + content_block: { type: "text", text: "NO_REPLY" }, + }, + { + type: "content_block_stop", + index: 1, + }, + { + type: "message_delta", + delta: { stop_reason: "end_turn" }, + usage: { input_tokens: 6, output_tokens: 9 }, + }, + ]), + ); + const streamFn = createAnthropicMessagesTransportStreamFn(); + const stream = await Promise.resolve( + streamFn( + makeAnthropicTransportModel({ provider: "meridian", baseUrl: "http://127.0.0.1:3456" }), + { + messages: [{ role: "user", content: "heartbeat" }], + } as Parameters[1], + { + apiKey: "meridian-key", + } as Parameters[2], + ), + ); + const events: Array<{ type?: string; delta?: string; content?: string }> = []; + for await (const event of stream as AsyncIterable<{ + type?: string; + delta?: string; + content?: string; + }>) { + events.push(event); + } + const result = await stream.result(); + + expect(result.content).toEqual([ + expect.objectContaining({ + type: "thinking", + thinking: "checking", + thinkingSignature: "sig_2", + }), + { type: "text", text: "NO_REPLY" }, + ]); + expect(events).toEqual( + expect.arrayContaining([ + expect.objectContaining({ type: "text_delta", delta: "NO_REPLY" }), + expect.objectContaining({ type: "text_end", content: "NO_REPLY" }), + ]), + ); + expect(result.usage.output).toBe(9); + }); + it("skips malformed tools when building Anthropic payloads", async () => { await runTransportStream( makeAnthropicTransportModel(), diff --git a/src/agents/anthropic-transport-stream.ts b/src/agents/anthropic-transport-stream.ts index 6f4dd277ca7..7c0675386cb 100644 --- a/src/agents/anthropic-transport-stream.ts +++ b/src/agents/anthropic-transport-stream.ts @@ -924,28 +924,55 @@ export function createAnthropicMessagesTransportStreamFn(): StreamFn { const contentBlock = event.content_block as Record | undefined; const index = typeof event.index === "number" ? event.index : -1; if (contentBlock?.type === "text") { - const block: TransportContentBlock = { type: "text", text: "", index }; + const text = + typeof contentBlock.text === "string" + ? sanitizeTransportPayloadText(contentBlock.text) + : ""; + const block: TransportContentBlock = { type: "text", text, index }; output.content.push(block); + const contentIndex = output.content.length - 1; stream.push({ type: "text_start", - contentIndex: output.content.length - 1, + contentIndex, partial: output as never, }); + if (text.length > 0) { + stream.push({ + type: "text_delta", + contentIndex, + delta: text, + partial: output as never, + }); + } continue; } if (contentBlock?.type === "thinking") { + const thinking = + typeof contentBlock.thinking === "string" + ? sanitizeTransportPayloadText(contentBlock.thinking) + : ""; const block: TransportContentBlock = { type: "thinking", - thinking: "", - thinkingSignature: "", + thinking, + thinkingSignature: + typeof contentBlock.signature === "string" ? contentBlock.signature : "", index, }; output.content.push(block); + const contentIndex = output.content.length - 1; stream.push({ type: "thinking_start", - contentIndex: output.content.length - 1, + contentIndex, partial: output as never, }); + if (thinking.length > 0) { + stream.push({ + type: "thinking_delta", + contentIndex, + delta: thinking, + partial: output as never, + }); + } continue; } if (contentBlock?.type === "redacted_thinking") { @@ -1042,7 +1069,7 @@ export function createAnthropicMessagesTransportStreamFn(): StreamFn { delta?.type === "signature_delta" && typeof delta.signature === "string" ) { - block.thinkingSignature = `${block.thinkingSignature ?? ""}${delta.signature}`; + block.thinkingSignature = delta.signature; } continue; }