Skip to content

fix: preserve empty reasoning_details arrays in multi-turn conversations - #487

Merged
robert-j-y merged 9 commits into
mainfrom
fix/preserve-empty-reasoning-details
Apr 26, 2026
Merged

fix: preserve empty reasoning_details arrays in multi-turn conversations#487
robert-j-y merged 9 commits into
mainfrom
fix/preserve-empty-reasoning-details

Conversation

@louisgv

@louisgv louisgv commented Apr 25, 2026

Copy link
Copy Markdown
Member

Summary

Fixes a bug where reasoning_details: [] (empty array) was silently dropped in multi-turn conversations, breaking providers like DeepSeek V4 in thinking mode that require this field to be echoed back even when empty.

Root cause

Multiple length > 0 guards in the conversion and streaming paths converted explicit empty reasoning_details: [] arrays to undefined, stripping a required signal that providers use to maintain conversation state.

Changes

src/chat/index.ts

  • Stream finish event now always sets openrouterMetadata.reasoning_details, even when the accumulated array is empty (previously guarded by length > 0)
  • Both reasoning-end emit sites now always include providerMetadata.openrouter.reasoning_details, removing the length > 0 ternary that would drop the field entirely

src/chat/convert-to-openrouter-chat-messages.ts

  • candidateReasoningDetails selection now uses Array.isArray(messageReasoningDetails) instead of messageReasoningDetails.length > 0 — an explicit [] is now treated as "metadata was provided" rather than "metadata was absent"
  • The top-level if (candidateReasoningDetails) guard no longer requires length > 0; an empty candidate array still triggers the dedup/signature-filter block
  • finalReasoningDetails is now always set to uniqueDetails (the deduplicated array), never collapsed to undefined
  • effectiveReasoning still requires finalReasoningDetails.length > 0 — reasoning text is never sent alongside an empty details array

Test plan

  • bun test passes (401/401 tests)
  • New regression test: should preserve empty reasoning_details array from message-level providerOptions (DeepSeek V4)
  • Existing tests updated to assert reasoning_details: [] instead of undefined for all cases where metadata was provided but produced no entries

Related

DeepSeek V4 and similar providers in thinking mode return reasoning_details: [] on assistant turns with no visible reasoning tokens, and require this field to be included in subsequent requests. Omitting it causes 4xx errors on follow-up turns. Identified via opencode multi-turn testing.

Reviewed by Perry

louisgv and others added 9 commits April 25, 2026 15:50
Always include reasoning_details in finish event metadata and reasoning-end
providerMetadata, even when the accumulated array is empty. DeepSeek V4 and
similar providers require the field to be sent back in subsequent turns to
maintain conversation state.
Replace length>0 checks with Array.isArray checks so that an explicit []
from message-level providerOptions is treated as 'metadata present but empty'
rather than 'metadata absent'. Preserve [] through dedup/signature-filter path.
…eaming, and #413 reasoning-delta metadata fix

Restores four pieces of behavior that were inadvertently removed when the
empty reasoning_details fix was applied:

- #419: Defensive usage fallback in stream finish handler that copies
  openrouterUsage promptTokens/completionTokens into usage.inputTokens.total
  /outputTokens.total when the standard fields are still undefined after
  computeTokenUsage().
- #388: Model-level settings fallbacks (?? this.settings.X) for max_tokens,
  temperature, top_p, frequency_penalty, presence_penalty, top_k.
- #443: eager_input_streaming forwarding from tool providerOptions to the
  function tool request body.
- #413: Stop attaching accumulated reasoning_details snapshots to
  reasoning-start/reasoning-delta events to avoid payload bloat. The
  always-include behavior remains on reasoning-end and the stream finish
  event, which is the only place opencode-style consumers read it from.

Keeps the legitimate empty reasoning_details fix in place so DeepSeek V4
and similar providers continue to receive the empty array on follow-up
turns.

Co-Authored-By: Robert Yeakel <robert.yeakel@openrouter.ai>
Restores the seven tests that were inadvertently deleted along with the
empty reasoning_details fix and flips the reasoning-start/reasoning-delta
providerMetadata assertions back to toBeUndefined() to lock in the
restored #413 behavior.

- 3 tests for eager_input_streaming forwarding from tool providerOptions.
- 4 tests for the #419 defensive usage fallback in the stream finish handler.
- Adjusts the two streaming reasoning_details tests to assert that
  reasoning-start and reasoning-delta events do NOT carry providerMetadata
  (the snapshot is exposed only on reasoning-end and the finish event).

Co-Authored-By: Robert Yeakel <robert.yeakel@openrouter.ai>
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