mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 08:50:43 +00:00
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:
committed by
GitHub
parent
4eb30fc13a
commit
ccb8472daf
@@ -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.
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user