Skip to content

fix(sse): drop reasoning alias once reasoning_content carries the replay - #16646

Merged
diegosouzapw merged 2 commits into
release/v3.8.52from
fix/port-issue-4841-reasoning-alias-collision
Oct 15, 2026
Merged

diegosouzapw merged 2 commits into
release/v3.8.52from
fix/port-issue-4841-reasoning-alias-collision

Conversation

@diegosouzapw

Copy link
Copy Markdown
Collaborator

Summary

Reasoning-replay targets such as z-ai GLM thinking models and Moonshot Kimi (kimi-k2.x, kimi-k3) use Rust/serde request schemas that declare reasoning as an alias of reasoning_content. When an assistant turn in the replayed history carried both keys, OmniRoute forwarded both and the upstream rejected the request with 400 Failed to deserialize the JSON body ... duplicate field reasoning_content.

Both keys reached the upstream in two ways:

  • the client sent both. Some agent harnesses send both keys so that stacks reading either name are covered.
  • the client sent only reasoning. replayOpenAIReasoningMessage copied it into reasoning_content and left reasoning in place.

filterToOpenAIFormat keeps tool-call assistant turns intact when preserveReasoningContent is set. The OPENAI_INCOMPATIBLE_ECHO_FIELDS strip only runs for non-reasoner targets. Nothing removed the alias on the reasoner path.

The reasoning input policy (open-sse/services/reasoningInputPolicy.ts) only deletes fields, and it runs before translateRequest. For plaintext transports it leaves both keys untouched, so those requests also hit this replay step and are covered by the same fix. This likely also fixes the related duplicate-field report about GLM-5.x thinking combos.

Fix

In replayOpenAIReasoningMessage (open-sse/translator/index.ts), right after the reasoning → reasoning_content promotion: if reasoning_content is non-empty and reasoning is a string, delete reasoning. reasoning_content becomes the single canonical carrier. Turns that only carry reasoning are still promoted, exactly as before. Partial (prefix) Moonshot messages and non-reasoner targets are unchanged.

Attribution

Thanks to @likongkong666 for the original report.

Test plan

  • New tests/unit/translator-reasoning-alias-collision-4841.test.ts (glm glm-4.6-thinking, moonshot kimi-k2.5, moonshot kimi-k3; both-keys and reasoning-only tool-call turns): 6/6 fail on base, 6/6 pass after the fix
  • Related suites: every tests/unit + tests/integration node:test file touching translateRequest/translator/index/reasoningInputPolicy/reasoningCache with reasoning fields: 756/758 pass. The 2 failures are tests/integration/video-retention-http-12150.test.ts → "network guard blocks other loopback ports" (ECONNREFUSED vs UND_MOCK_ERR_MOCK_NOT_MATCHED) and its parent. They fail the same way with the base translator/index.ts, so they are pre-existing and environmental.
  • npx eslint and npx prettier --check on the changed files
  • npm run typecheck:core

⚠️ base-red inherited: #15306

Reasoning-replay targets (z-ai GLM thinking, Moonshot Kimi) use serde schemas
that alias `reasoning` to `reasoning_content`. An assistant turn carrying both
keys - sent by clients that cover either name, or produced when OmniRoute
promotes `reasoning` into `reasoning_content` - was forwarded with both and
rejected upstream with 400 "duplicate field `reasoning_content`".

replayOpenAIReasoningMessage now deletes a string `reasoning` once
`reasoning_content` is non-empty, keeping `reasoning_content` as the single
canonical carrier. Reasoning-only turns are still promoted.

Reported-by: likongkong666 (decolua/9router#4841)
@github-actions

Copy link
Copy Markdown
Contributor

CI Coverage Report

  • Coverage job: skipped
  • PR test policy: success

Coverage artifact was not available for this run.

@diegosouzapw
diegosouzapw merged commit 456a705 into release/v3.8.52 Oct 15, 2026
39 of 51 checks passed
@diegosouzapw
diegosouzapw deleted the fix/port-issue-4841-reasoning-alias-collision branch October 18, 2026 23:50
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.

1 participant