diff --git a/CHANGELOG.md b/CHANGELOG.md index d22053ec6dc..ac8ce47e70f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/src/agents/anthropic-transport-stream.test.ts b/src/agents/anthropic-transport-stream.test.ts index 66cbccc715b..b9694a41252 100644 --- a/src/agents/anthropic-transport-stream.test.ts +++ b/src/agents/anthropic-transport-stream.test.ts @@ -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 () => { diff --git a/src/agents/anthropic-transport-stream.ts b/src/agents/anthropic-transport-stream.ts index d3e753f6ba1..96d3dba1bfb 100644 --- a/src/agents/anthropic-transport-stream.ts +++ b/src/agents/anthropic-transport-stream.ts @@ -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(); const reasoningContentTextBlocks = new Map(); const eventIndexKey = (eventIndex: unknown) =>