Skip to content

[BUG] Reasoning content is corrupted mid-task on Bedrock, and passed through unsanitized on MiniMax #1665

Description

@LouisClt

This report is the result of a code-level investigation carried out with the help of Claude (Anthropic's Claude Code), reading through the actual provider/transform code paths cited below. All the file/line references were verified against the current main branch.

Describe the bug

During a single task's tool-call loop (reasoning → tool call → tool result → reasoning → ...), some providers are supposed to carry the model's reasoning across steps via ModelInfo.preserveReasoning, the same way DeepSeek, MiMo, and Z.ai/GLM already do correctly (src/api/providers/opencode-go.ts:172-177). For Bedrock and the MiniMax direct provider, this is still broken today, so the model re-derives the same reasoning from scratch at every step of a tool loop instead of building on it — this is only about continuity within one task's tool loop, not about remembering reasoning across genuinely new user messages (every vendor intentionally drops it there, which is expected).

Root causes found by reading the code:

  • BedrockconvertToBedrockConverseMessages (src/api/transform/bedrock-converse-format.ts:30-209) has no handling at all for reasoning/thinking blocks; they fall into the "Default case for unknown block types" and get replaced by the literal string "[Unknown Block Type]" (bedrock-converse-format.ts:198-201). This affects the 2 models that have preserveReasoning: true today (moonshot.kimi-k2-thinking line 551, minimax.minimax-m2 line 561, in packages/types/src/providers/bedrock.ts).
    • Claude on Bedrock is not hit by this today, but only incidentally: no anthropic.claude-* entry sets preserveReasoning, so Task.buildCleanConversationHistory() (see below) already strips Claude's reasoning block before it ever reaches this converter — Claude never gets the chance to hit the "[Unknown Block Type]" path. This bug is latent, not absent, for Claude: the moment someone enables preserveReasoning for a Claude Bedrock model (e.g. as part of resolution path chore: disable automatic deploy workflows #3 below), Claude would immediately fall into the exact same corruption unless this converter has been fixed first. This is why path feat: support OAuth 2.1 for streamable-http MCP servers #1 should land before path chore: disable automatic deploy workflows #3 is ever attempted on Bedrock.
  • MiniMax (direct provider)MiniMaxHandler.createMessage (src/api/providers/minimax.ts:80-116) sends Anthropic-shaped messages straight to this.client.messages.create() with no sanitization, unlike anthropic.ts and anthropic-vertex.ts, which both call filterNonAnthropicBlocks before sending. Several MiniMax models have preserveReasoning: true; whether MiniMax's API tolerates a raw {type:"reasoning", text} block reaching it unfiltered is unverified.
  • Claude's extended thinking (all surfaces, separate/bigger issue)getThoughtSignature(), needed to build a real signed type:"thinking" block (src/core/task/apiConversationHistory.ts:14,48), is only implemented for Gemini (src/api/providers/gemini.ts:628). Neither anthropic.ts:361 nor anthropic-vertex.ts:206 implement it — both still carry the comment "Signature for multi-turn thinking would require using stream.finalMessage() ... which requires restructuring the streaming approach." So Claude's thinking is never preserved across tool-loop steps anywhere today, on top of the Bedrock-specific corruption above.
  • The stripping gate itself is at src/core/task/Task.ts:4926 (buildCleanConversationHistory) / Task.ts:5030 (shouldPreserveForApi = this.api.getModel().info.preserveReasoning === true) — still in place and working as designed for providers that implement the flag correctly.

Note: a previously-identified Fireworks bug (the same preserveReasoning flag being silently ignored because BaseOpenAiCompatibleProvider never round-tripped reasoning_content) turned out to already be fixed by commit 8e76b8d7c ("fix(deepseek): round-trip reasoning_content in thinking mode to prevent 400 errors", #775) — the fix landed in the shared convertToOpenAiMessages (src/api/transform/openai-format.ts:274-280,516-583), so it incidentally also fixed Fireworks. No action needed there anymore; not included in this report.

To Reproduce

For Bedrock (any model with preserveReasoning: true, e.g. moonshot.kimi-k2-thinking):

  1. Configure Zoo Code with the Bedrock provider and moonshot.kimi-k2-thinking (or minimax.minimax-m2).
  2. Start a task that requires several tool calls in a row (e.g. read a file, then edit it, then run a command).
  3. Let the model reason, then call a tool, then reason again for the next tool call.
  4. Inspect the actual request payload sent on the second/third tool-call step (e.g. via a logging proxy) — the previous step's reasoning content has been replaced with the literal string "[Unknown Block Type]".
  5. Observe that the model's reasoning at each step re-derives the same analysis instead of building on the previous step, and/or reacts to "[Unknown Block Type]" as if it were real prior content.

Expected behavior

Reasoning content produced during a step of the tool-call loop should be preserved and correctly forwarded on the next step of the same task, exactly as already happens for DeepSeek, MiMo, and Z.ai/GLM models. It should never be replaced by a placeholder string, and preserveReasoning: true should have an actual effect regardless of which provider serves the model.

Screenshots

N/A — not visible in the Zoo Code UI itself (reasoning displays correctly there); only shows up by inspecting the outgoing request payload. Happy to provide a captured request body showing "[Unknown Block Type]" on Bedrock if useful.

Video

N/A

What version of zoo are you running

3.82.0+ (confirmed against current main, commit 500152b78, as of this investigation — not tied to a specific tagged release).

Additional context

Related existing issues/PRs :

None of the existing issues cover the Bedrock corruption or MiniMax cases above, or the Claude signature-capture gap.

Consequences:

  • Wasted tokens and cost: the model regenerates the same multi-step reasoning from scratch at every tool-call round trip instead of building on it once. On long agentic tasks this multiplies reasoning-token cost roughly by the number of steps — directly hits users on pay-per-token Bedrock/MiniMax plans for no benefit.
  • Wasted latency: re-deriving the same reasoning adds real wall-clock time to every step; [BUG] Reasoning content not preserved across turns for generic OpenRouter provider, causes repeated/looping thinking (e.g. poolside/laguna-s-2.1) #1107 (same bug family, OpenRouter) reports this costing "several minutes per turn" on affected models.
  • Degraded tool-use quality: models like Kimi K2 Thinking are documented to rely on seeing their prior reasoning to keep tool-call arguments/sequencing consistent across a multi-step plan; losing it mid-task can make the model contradict or forget decisions it just made a step earlier.
  • Silent failure mode: no error or warning anywhere — the reasoning displays correctly in the Zoo Code UI right up until it's dropped/corrupted, so nothing looks wrong unless you inspect the actual request payload.
  • Task stalling risk: per [BUG] Reasoning content not preserved across turns for generic OpenRouter provider, causes repeated/looping thinking (e.g. poolside/laguna-s-2.1) #1107, this repeated re-derivation can loop long enough to effectively stall a task.
  • Corrupted context, not just lost context, on Bedrock: the "[Unknown Block Type]" literal is actively sent to the model as if it were real prior content — worse than simply not having the reasoning at all.
  • Inconsistent product behavior: preserveReasoning is an advertised capability of the model catalog, but whether it actually works depends entirely on which provider happens to serve the model — DeepSeek/MiMo/Z.ai/Fireworks users get correct behavior, Bedrock/MiniMax users silently don't, for models otherwise equally capable of interleaved thinking.

Proposed resolution paths:

  1. Teach bedrock-converse-format.ts to map reasoning/thinking blocks to the Converse API's reasoningContent shape instead of "[Unknown Block Type]". Fixes the 2 Bedrock models that already set the flag, low risk.
  2. Audit MiniMax's direct provider: sanitize/handle the reasoning block explicitly (e.g. reuse filterNonAnthropicBlocks or an equivalent) instead of an unverified raw passthrough.
  3. (Separate, larger effort) Implement signature capture for Claude (stream.finalMessage()) on anthropic.ts/anthropic-vertex.ts, then extend to Bedrock on top of fix feat: support OAuth 2.1 for streamable-http MCP servers #1. More work, higher risk of API rejection if done wrong — scope independently from 1–2.

Suggest doing 1–2 first, then 3 as a separate follow-up epic.

Another idea could be to add an "auto-condense" feature for reasoning: each time reasoning is produced, it would be condensed into a shorter form, so we can save context without losing the reasoning that was done.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions