Skip to content

fix: preserve assistant reasoning_content when translating history#5

Open
meumlpontocom wants to merge 3 commits intoLordymine:masterfrom
meumlpontocom:fix/preserve-thinking-reasoning
Open

fix: preserve assistant reasoning_content when translating history#5
meumlpontocom wants to merge 3 commits intoLordymine:masterfrom
meumlpontocom:fix/preserve-thinking-reasoning

Conversation

@meumlpontocom
Copy link
Copy Markdown

Summary

Fixes a runtime proxy failure seen when running Claude Code against a
Moonshot-backed model through the OpenCode Zen proxy (e.g. kimi-k2.6):

API Error: 400 Provider returned error
thinking is enabled but reasoning_content is missing
in assistant tool call message at index 8
(provider: Moonshot AI)

The Anthropic request that Claude Code replays on each turn embeds
thinking blocks inside prior assistant content arrays. The
Anthropic → OpenAI translator in src/proxy/request-conversion.ts
only handled text and tool_use blocks, silently dropping any
thinking / redacted_thinking entries. By the time the history
reached the upstream provider the reasoning context was gone, and
Moonshot rejects such requests whenever extended thinking is on.

What changed

  • src/proxy/request-conversion.ts — accumulate thinking text from
    assistant thinking blocks and emit it as reasoning_content on
    the OpenAI-shaped message. redacted_thinking blocks (provider-
    opaque) become a [redacted] placeholder so the field is still
    non-empty when the provider requires it.
  • tests/request-conversion.test.ts — 5 new tests:
    • single thinking + text content
    • thinking + tool_use in the same assistant turn
    • multiple thinking blocks joined with \n
    • redacted_thinking placeholder
    • no thinking → reasoning_content stays absent

No behavior changes for messages that don't include thinking blocks,
so requests to providers that ignore reasoning_content (or reject
unknown fields) are unaffected — the field is only emitted when the
incoming history actually carries thinking.

Test plan

  • bun run typecheck — clean
  • bun test84 pass (was 79; 5 new), 0 fail
  • bun run build — clean
  • Manually reproduced against kimi-k2.6 via Claude Code: tool-use
    turn 8 that previously returned HTTP 400 now round-trips
    successfully.

🤖 Generated with Claude Code

Moonshot-backed models (e.g. kimi-k2.6 via the OpenCode Zen proxy) reject
multi-turn requests that have `thinking` enabled but carry assistant
messages without `reasoning_content`:

  thinking is enabled but reasoning_content is missing
  in assistant tool call message at index 8

The Anthropic request that Claude Code replays on each turn includes
`thinking` blocks inside prior assistant `content` arrays. The Anthropic →
OpenAI translator in `request-conversion.ts` was dropping those blocks,
so by the time the history reached the upstream provider, reasoning
context was gone and the provider refused the request.

This change accumulates `thinking` and `redacted_thinking` blocks found
on assistant messages and emits their text as `reasoning_content` on the
translated OpenAI-style message. `redacted_thinking` uses a `[redacted]`
placeholder so the field is non-empty when the provider insists on its
presence.

Tests cover single thinking blocks, thinking + tool_use turns, multiple
thinking blocks joined with newlines, `redacted_thinking`, and the
no-thinking path (field stays absent).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
miltonbastos and others added 2 commits April 21, 2026 20:22
The initial fix only propagated reasoning_content when Anthropic had
actually emitted a `thinking` block for that specific turn. In practice
Claude Code's replayed history contains assistant messages that carry
only `tool_use` blocks — no thinking block at all — and Moonshot still
rejects those with:

  thinking is enabled but reasoning_content is missing
  in assistant tool call message at index 5

When the incoming request has `thinking: { type: "enabled" }` (or the
equivalent) and an assistant message in the history has tool_calls but
no extractable reasoning, emit a placeholder `reasoning_content` so the
upstream accepts the request. Real thinking blocks, when present, still
win — the placeholder only fills in the gap.

Covered by 3 new tests: placeholder injection, no-placeholder when there
are no tool_calls, and real thinking blocks overriding the placeholder.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Some upstream providers (notably Moonshot AI routing kimi-k2.6 via the
OpenCode Zen proxy → OpenRouter) enable extended thinking server-side
regardless of whether the client requested it. In that configuration
every assistant message with tool_calls in the replayed history must
carry a non-empty `reasoning_content`, even when the original Anthropic
request never set `thinking: { ... }`.

Repro logged against a fresh session: Claude Code sent no `thinking`
field, the proxy forwarded the history untouched, and Moonshot rejected
with:

  thinking is enabled but reasoning_content is missing
  in assistant tool call message at index 2 / 5 / 8

Drop the `thinkingEnabled` gate and emit `reasoning_content` whenever an
assistant message has tool_calls. Real thinking blocks still win; when
none are present, a `[no reasoning provided]` placeholder fills the gap.
Providers that ignore the field are unaffected — it's a known OpenAI-
compatible extension used by DeepSeek, Moonshot, Qwen, etc.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@meumlpontocom
Copy link
Copy Markdown
Author

Update: after testing end-to-end against a real kimi-k2.6 session via OpenCode Zen, the original conditional fix (only inject reasoning_content when body.thinking is set) wasn't enough — upstream enables extended thinking server-side for that model even when Claude Code sends no thinking field. The error kept reproducing on multi-tool turns (reasoning_content is missing ... at index 2 / 5 / 8).

Third commit in this PR (c9cbcb4) drops the thinkingEnabled gate and emits reasoning_content on every assistant tool-call message: real thinking-block text when present, [no reasoning provided] placeholder otherwise. Verified fix against the same prompt that previously blew up — full plan generation with Skill + Explore + Bash tool calls now round-trips cleanly.

Test suite: 101 pass, 0 fail.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants