From 7bf4458bbed31d514995c40b44191bc0222f6c99 Mon Sep 17 00:00:00 2001 From: Neerav Makwana Date: Mon, 11 May 2026 11:02:03 -0400 Subject: [PATCH] fix: guard empty MiniMax Anthropic messages (#74731) Fixes #74589. Thanks @neeravmakwana and @DerekEXS. --- CHANGELOG.md | 1 + src/agents/anthropic-transport-stream.test.ts | 55 +++++++++++++++++++ src/agents/anthropic-transport-stream.ts | 12 +++- 3 files changed, 67 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 185fa098765..5fb8f5dff53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,6 +60,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Bonjour/Gateway: treat active ciao probing and fresh name-conflict renames as in-progress so the mDNS watchdog waits for probe settlement before retrying, preventing rapid re-advertise loops on Windows, WSL, and other multicast-hostile hosts. (#74778) Refs #74242. Thanks @fuller-stack-dev. +- Providers/MiniMax: send a minimal Anthropic-compatible user fallback when message conversion filters a turn to an empty payload, so MiniMax M2.7 no longer returns `chat content is empty` after tool-heavy sessions. Fixes #74589. Thanks @neeravmakwana and @DerekEXS. - Cron: keep long manual cron runs active in the task registry until completion, preventing transient `lost` markers before durable recovery reconciles. Fixes #78233. (#78243) Thanks @Feelw00. - Doctor/GitHub CLI: surface a `GH_CONFIG_DIR` hint when the GitHub skill is usable but `gh` auth lives under a different operator HOME than the agent process, without warning for disabled or filtered skills. Fixes #78063. (#78095) Thanks @tmimmanuel. - Gateway: dedupe concurrent `send`, `poll`, and `message.action` requests while delivery is still in flight, preventing duplicate outbound work for the same idempotency key. (#68341) Thanks @thesomewhatyou. diff --git a/src/agents/anthropic-transport-stream.test.ts b/src/agents/anthropic-transport-stream.test.ts index 5d2aec82930..6b203f0ad60 100644 --- a/src/agents/anthropic-transport-stream.test.ts +++ b/src/agents/anthropic-transport-stream.test.ts @@ -718,6 +718,61 @@ describe("anthropic transport stream", () => { expect(toolUse.input).toEqual({}); }); + it.each([ + { + name: "empty history", + context: { messages: [] } as AnthropicStreamContext, + }, + { + name: "blank user content", + context: { + messages: [ + { + role: "user", + content: " \n\t ", + timestamp: 0, + }, + ], + } as AnthropicStreamContext, + }, + ])( + "sends a minimal user fallback when Anthropic message conversion has no content: $name", + async ({ context }) => { + await runTransportStream( + makeAnthropicTransportModel({ + id: "MiniMax-M2.7", + name: "MiniMax M2.7", + provider: "minimax", + baseUrl: "https://api.minimax.io/anthropic", + }), + context, + { + apiKey: "sk-minimax-test", + } as AnthropicStreamOptions, + ); + + expect(latestAnthropicRequest().payload).toMatchObject({ + model: "MiniMax-M2.7", + messages: [ + { + role: "user", + content: [ + { + type: "text", + text: ".", + cache_control: { type: "ephemeral" }, + }, + ], + }, + ], + }); + expect(guardedFetchMock).toHaveBeenCalledWith( + "https://api.minimax.io/anthropic/v1/messages", + expect.objectContaining({ method: "POST" }), + ); + }, + ); + it.each([ ["empty", ""], ["whitespace-only", " \n\t "], diff --git a/src/agents/anthropic-transport-stream.ts b/src/agents/anthropic-transport-stream.ts index cfc3eae22ba..dfb185d7f00 100644 --- a/src/agents/anthropic-transport-stream.ts +++ b/src/agents/anthropic-transport-stream.ts @@ -108,6 +108,8 @@ type MutableAssistantOutput = { errorMessage?: string; }; +const EMPTY_ANTHROPIC_MESSAGES_FALLBACK_TEXT = "."; + function isClaudeOpus47Model(modelId: string): boolean { return modelId.includes("opus-4-7") || modelId.includes("opus-4.7"); } @@ -425,6 +427,12 @@ function convertAnthropicMessages( return params; } +function ensureNonEmptyAnthropicMessages(messages: Array>) { + return messages.length > 0 + ? messages + : [{ role: "user", content: EMPTY_ANTHROPIC_MESSAGES_FALLBACK_TEXT }]; +} + function convertAnthropicTools(tools: Context["tools"], isOAuthToken: boolean) { if (!tools) { return []; @@ -754,7 +762,9 @@ function buildAnthropicParams( }); const params: Record = { model: model.id, - messages: convertAnthropicMessages(context.messages, model, isOAuthToken), + messages: ensureNonEmptyAnthropicMessages( + convertAnthropicMessages(context.messages, model, isOAuthToken), + ), max_tokens: maxTokens, stream: true, };