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 <steipete@gmail.com>
This commit is contained in:
Vyctor Huggo Przozwski da Silva
2026-04-29 16:16:52 -03:00
committed by GitHub
parent 4eb30fc13a
commit ccb8472daf
3 changed files with 110 additions and 6 deletions

View File

@@ -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.

View File

@@ -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<typeof streamFn>[1],
{
apiKey: "meridian-key",
} as Parameters<typeof streamFn>[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(),

View File

@@ -924,28 +924,55 @@ export function createAnthropicMessagesTransportStreamFn(): StreamFn {
const contentBlock = event.content_block as Record<string, unknown> | 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;
}