fix: preserve empty reasoning_details arrays in multi-turn conversations - #487
Merged
Conversation
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>
Merged
This was referenced Apr 26, 2026
fix(cli,gateway): fully preserve DeepSeek reasoning_content across tool calls
Kilo-Org/kilocode#9502
Closed
This was referenced Apr 26, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 > 0guards in the conversion and streaming paths converted explicit emptyreasoning_details: []arrays toundefined, stripping a required signal that providers use to maintain conversation state.Changes
src/chat/index.tsopenrouterMetadata.reasoning_details, even when the accumulated array is empty (previously guarded bylength > 0)reasoning-endemit sites now always includeproviderMetadata.openrouter.reasoning_details, removing thelength > 0ternary that would drop the field entirelysrc/chat/convert-to-openrouter-chat-messages.tscandidateReasoningDetailsselection now usesArray.isArray(messageReasoningDetails)instead ofmessageReasoningDetails.length > 0— an explicit[]is now treated as "metadata was provided" rather than "metadata was absent"if (candidateReasoningDetails)guard no longer requireslength > 0; an empty candidate array still triggers the dedup/signature-filter blockfinalReasoningDetailsis now always set touniqueDetails(the deduplicated array), never collapsed toundefinedeffectiveReasoningstill requiresfinalReasoningDetails.length > 0— reasoning text is never sent alongside an empty details arrayTest plan
bun testpasses (401/401 tests)should preserve empty reasoning_details array from message-level providerOptions (DeepSeek V4)reasoning_details: []instead ofundefinedfor all cases where metadata was provided but produced no entriesRelated
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