fix: replay Xiaomi Anthropic reasoning blocks

This commit is contained in:
Peter Steinberger
2026-05-18 06:52:19 +01:00
parent 476bd35431
commit 102e4f2c9d
3 changed files with 86 additions and 13 deletions

View File

@@ -33,6 +33,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Providers/Xiaomi: replay MiMo Anthropic-compatible `reasoning_content` as provider-required thinking blocks even when OpenClaw thinking is disabled, fixing follow-up tool turns for `mimo-v2-flash`. Fixes #83407. Thanks @Xgenious7.
- Agents/exec approvals: forward approval-runtime credentials on agent-owned Gateway approval calls so approved async commands complete through the existing runtime path instead of stalling on unauthenticated follow-up calls. Thanks @IWhatsskill, @Patrick-Erichsen, and @jesse-merhi.
- Gateway/skills: preflight remote macOS skill-bin refreshes with a WebSocket connectivity check so stale node sessions skip quickly instead of logging slow `system.which` timeout warnings.
- GitHub Copilot: drop unsafe native Responses reasoning replay items with non-replayable IDs before dispatch, preventing affected Copilot sessions from failing with `invalid_request_body`. Fixes #83220. Thanks @galiniliev.

View File

@@ -1027,7 +1027,7 @@ describe("anthropic transport stream", () => {
]);
});
it("backfills empty reasoning_content for compatible Anthropic tool-use replays", async () => {
it("backfills empty reasoning_content thinking blocks for compatible Anthropic tool-use replays", async () => {
await runTransportStream(
makeAnthropicTransportModel({
id: "mimo-v2.6-pro",
@@ -1066,13 +1066,69 @@ describe("anthropic transport stream", () => {
latestAnthropicRequest().payload.messages,
(record) => record.role === "assistant",
);
expect(assistantMessage.reasoning_content).toBe("");
expect(assistantMessage).not.toHaveProperty("reasoning_content");
expect(assistantMessage.content).toEqual([
{
type: "thinking",
thinking: "",
signature: "reasoning_content",
},
{ type: "tool_use", id: "call_1", name: "lookup", input: {} },
]);
});
it("backfills empty reasoning_content for compatible Anthropic text replays", async () => {
it("backfills MiMo v2-flash tool-use replay when OpenClaw thinking is off", async () => {
await runTransportStream(
makeAnthropicTransportModel({
id: "mimo-v2-flash",
name: "MiMo V2 Flash",
provider: "xiaomi",
baseUrl: "https://api.xiaomimimo.com/anthropic",
reasoning: false,
}),
{
messages: [
{ role: "user", content: "look this up" },
{
role: "assistant",
provider: "xiaomi",
api: "anthropic-messages",
model: "mimo-v2-flash",
stopReason: "toolUse",
timestamp: 0,
content: [{ type: "toolCall", id: "call_1", name: "lookup", arguments: {} }],
},
{
role: "toolResult",
toolCallId: "call_1",
content: [{ type: "text", text: "found" }],
isError: false,
},
{ role: "user", content: "continue" },
],
} as AnthropicStreamContext,
{
apiKey: "sk-xiaomi-test",
} as AnthropicStreamOptions,
);
const assistantMessage = findRecord(
latestAnthropicRequest().payload.messages,
(record) => record.role === "assistant",
);
expect(latestAnthropicRequest().payload).not.toHaveProperty("thinking");
expect(assistantMessage).not.toHaveProperty("reasoning_content");
expect(assistantMessage.content).toEqual([
{
type: "thinking",
thinking: "",
signature: "reasoning_content",
},
{ type: "tool_use", id: "call_1", name: "lookup", input: {} },
]);
});
it("backfills empty reasoning_content thinking blocks for compatible Anthropic text replays", async () => {
await runTransportStream(
makeAnthropicTransportModel({
id: "mimo-v2.6-pro",
@@ -1105,8 +1161,15 @@ describe("anthropic transport stream", () => {
latestAnthropicRequest().payload.messages,
(record) => record.role === "assistant",
);
expect(assistantMessage.reasoning_content).toBe("");
expect(assistantMessage.content).toEqual([{ type: "text", text: "Hello!" }]);
expect(assistantMessage).not.toHaveProperty("reasoning_content");
expect(assistantMessage.content).toEqual([
{
type: "thinking",
thinking: "",
signature: "reasoning_content",
},
{ type: "text", text: "Hello!" },
]);
});
it("does not backfill reasoning_content for generic Anthropic-compatible tool-use replays", async () => {
@@ -1154,7 +1217,7 @@ describe("anthropic transport stream", () => {
]);
});
it("does not replay reasoning_content when compatible Anthropic thinking is disabled", async () => {
it("replays observed reasoning_content for compatible Anthropic routes when thinking is disabled", async () => {
await runTransportStream(
makeAnthropicTransportModel({
id: "mimo-v2.6-pro",
@@ -1194,8 +1257,15 @@ describe("anthropic transport stream", () => {
(record) => record.role === "assistant",
);
expect(latestAnthropicRequest().payload.thinking).toEqual({ type: "disabled" });
expect(assistantMessage).not.toHaveProperty("reasoning_content");
expect(assistantMessage.content).toEqual([{ type: "text", text: "Hello!" }]);
expect(assistantMessage.reasoning_content).toBe("Need to answer politely.");
expect(assistantMessage.content).toEqual([
{
type: "thinking",
thinking: "Need to answer politely.",
signature: "reasoning_content",
},
{ type: "text", text: "Hello!" },
]);
});
it("does not replay synthetic reasoning_content to native Anthropic models", async () => {

View File

@@ -411,7 +411,11 @@ function convertAnthropicMessages(
if (reasoningContent.length > 0) {
assistantMsg.reasoning_content = reasoningContent.join("\n");
} else if (allowReasoningContentReplay) {
assistantMsg.reasoning_content = "";
blocks.unshift({
type: "thinking",
thinking: "",
signature: "reasoning_content",
});
}
params.push(assistantMsg);
}
@@ -788,8 +792,7 @@ function buildAnthropicParams(
model: model.id,
messages: ensureNonEmptyAnthropicMessages(
convertAnthropicMessages(context.messages, model, isOAuthToken, {
allowReasoningContentReplay:
supportsReasoningContentReplay(model) && options?.thinkingEnabled === true,
allowReasoningContentReplay: supportsReasoningContentReplay(model),
}),
),
max_tokens: maxTokens,
@@ -946,8 +949,7 @@ export function createAnthropicMessagesTransportStreamFn(): StreamFn {
);
stream.push({ type: "start", partial: output as never });
const blocks = output.content;
const allowReasoningContentReplay =
supportsReasoningContentReplay(model) && transportOptions.thinkingEnabled === true;
const allowReasoningContentReplay = supportsReasoningContentReplay(model);
const reasoningContentThinkingBlocks = new Map<number, number>();
const reasoningContentTextBlocks = new Map<number, number>();
const eventIndexKey = (eventIndex: unknown) =>