Repository navigation
[BUG] Reasoning content is corrupted mid-task on Bedrock, and passed through unsanitized on MiniMax #1665
Description
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
mainbranch.
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:
- Bedrock —
convertToBedrockConverseMessages(src/api/transform/bedrock-converse-format.ts:30-209) has no handling at all forreasoning/thinkingblocks; 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 havepreserveReasoning: truetoday (moonshot.kimi-k2-thinkingline 551,minimax.minimax-m2line 561, inpackages/types/src/providers/bedrock.ts).- Claude on Bedrock is not hit by this today, but only incidentally: no
anthropic.claude-*entry setspreserveReasoning, soTask.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 enablespreserveReasoningfor 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.
- Claude on Bedrock is not hit by this today, but only incidentally: no
- MiniMax (direct provider) —
MiniMaxHandler.createMessage(src/api/providers/minimax.ts:80-116) sends Anthropic-shaped messages straight tothis.client.messages.create()with no sanitization, unlikeanthropic.tsandanthropic-vertex.ts, which both callfilterNonAnthropicBlocksbefore sending. Several MiniMax models havepreserveReasoning: 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 signedtype:"thinking"block (src/core/task/apiConversationHistory.ts:14,48), is only implemented for Gemini (src/api/providers/gemini.ts:628). Neitheranthropic.ts:361noranthropic-vertex.ts:206implement 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):
- Configure Zoo Code with the Bedrock provider and
moonshot.kimi-k2-thinking(orminimax.minimax-m2). - Start a task that requires several tool calls in a row (e.g. read a file, then edit it, then run a command).
- Let the model reason, then call a tool, then reason again for the next tool call.
- 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]". - 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 :
- [BUG] Reasoning is never sent back to the API for the OpenAI Compatible provider — preserveReasoning cannot be enabled from the UI #1096 and [BUG] Reasoning from local OpenAI-compatible models (llama.cpp/Ollama/LM Studio) is discarded — no official toggle #1118 report the identical bug for the OpenAI Compatible provider (
preserveReasoningcan never betruebecausegetModel()has no per-model catalog). PR fix(openai): preserve reasoning for local OpenAI-compatible models via R1 toggle #1119 already addresses this by making the "R1 format" toggle also setpreserveReasoning: true; as of last check it's active (awaiting-maintainer, not merged, blocked on a second round of change requests) — not abandoned, just not landed. This issue does not duplicate that work. - [BUG] Reasoning content not preserved across turns for generic OpenRouter provider, causes repeated/looping thinking (e.g. poolside/laguna-s-2.1) #1107 reports the same symptom ("repeated/looping thinking") for the generic OpenRouter provider — further evidence this bug pattern is widespread across providers.
- Anthropic adaptive-thinking models: thinking content invisible (display omitted) and thinking_tokens telemetry missing #1326 / fix(api): surface adaptive thinking display and thinking_tokens for Anthropic models #1327 are a related but distinct problem: Claude adaptive-thinking models return empty thinking text (display defaults to
"omitted") — that's about visibility, not the cross-turn persistence gap described here. - [ENHANCEMENT] Improve Amazon Bedrock model discovery, context handling, and output limits #124 / [ENHANCEMENT] Improve Amazon Bedrock support #125 / [Bedrock] Support adaptive thinking request payloads #306 are general Bedrock improvements (adaptive thinking request payload, model discovery) — they don't touch the response-side serialization bug described here.
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:
preserveReasoningis 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:
- Teach
bedrock-converse-format.tsto mapreasoning/thinkingblocks to the Converse API'sreasoningContentshape instead of"[Unknown Block Type]". Fixes the 2 Bedrock models that already set the flag, low risk. - Audit MiniMax's direct provider: sanitize/handle the reasoning block explicitly (e.g. reuse
filterNonAnthropicBlocksor an equivalent) instead of an unverified raw passthrough. - (Separate, larger effort) Implement signature capture for Claude (
stream.finalMessage()) onanthropic.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
PierrunoYT commentedon Sep 16, 2026
Working on it!
PierrunoYT commentedon Sep 28, 2026
Follow-up for the remaining half (Claude extended-thinking signature capture): #1837. PR #1666 covers the Bedrock serialization and MiniMax signature capture.