fix: preserve assistant reasoning_content when translating history#5
fix: preserve assistant reasoning_content when translating history#5meumlpontocom wants to merge 3 commits intoLordymine:masterfrom
Conversation
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>
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>
|
Update: after testing end-to-end against a real Third commit in this PR ( Test suite: 101 pass, 0 fail. |
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):The Anthropic request that Claude Code replays on each turn embeds
thinkingblocks inside prior assistantcontentarrays. TheAnthropic → OpenAI translator in
src/proxy/request-conversion.tsonly handled
textandtool_useblocks, silently dropping anythinking/redacted_thinkingentries. By the time the historyreached 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 fromassistant
thinkingblocks and emit it asreasoning_contentonthe OpenAI-shaped message.
redacted_thinkingblocks (provider-opaque) become a
[redacted]placeholder so the field is stillnon-empty when the provider requires it.
tests/request-conversion.test.ts— 5 new tests:thinking+ text contentthinking+tool_usein the same assistant turnthinkingblocks joined with\nredacted_thinkingplaceholderreasoning_contentstays absentNo behavior changes for messages that don't include thinking blocks,
so requests to providers that ignore
reasoning_content(or rejectunknown fields) are unaffected — the field is only emitted when the
incoming history actually carries thinking.
Test plan
bun run typecheck— cleanbun test— 84 pass (was 79; 5 new), 0 failbun run build— cleankimi-k2.6via Claude Code: tool-useturn 8 that previously returned HTTP 400 now round-trips
successfully.
🤖 Generated with Claude Code