Skip to content

fix(deepseek): keep parallel reasoning replay on continuation - #1499

Closed
Wibias wants to merge 1 commit into
devfrom
ocx-155-bug-deepseek-v4-flash-responses-continuation-still-drops
Closed

fix(deepseek): keep parallel reasoning replay on continuation#1499
Wibias wants to merge 1 commit into
devfrom
ocx-155-bug-deepseek-v4-flash-responses-continuation-still-drops

Conversation

@Wibias

@Wibias Wibias commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Fix the DeepSeek Responses continuation so a parallel tool-call turn keeps its plaintext reasoning replay, fixing the reasoning_text in the thinking mode must be passed back 400 on deepseek/deepseek-v4-flash.
  • Normalize an unambiguous Responses history as one call batch followed by its matching outputs in call order, moving hook-injected context after the batch instead of splitting it inside a parallel turn.
  • Fail closed: missing, duplicate, or backward (out-of-order) call/result histories are left unchanged for the upstream to reject rather than being reordered or guessed.

Validation

  • bun test tests/deepseek-inbound-wire.test.ts — 37 pass (5 new regression tests, 99 expect calls)
  • bun test tests/deepseek-inbound-wire.test.ts tests/openai-responses-passthrough.test.ts tests/deepseek-reasoning-replay.test.ts tests/deepseek-reasoning-replay-gaps.test.ts tests/config.test.ts tests/provider-registry-parity.test.ts tests/config-save-boundary.test.ts — 294 pass; 6 pre-existing symlink EPERM failures in config.test.ts only (Windows lacks symlink privilege); none related to this diff
  • bun run typecheck — pass
  • bun run privacy:scan — pass
  • cd docs-site && bun install --frozen-lockfile && bun run build — 265 pages
  • git diff --check — pass

Review notes

  • Pre-open gate run: scripts/pre-open-gate.mjs — ready (all required bug lenses and security surfaces covered; test-honesty probe verified clean).

Fixes #1477

Summary by CodeRabbit

  • New Features
    • Improved handling of parallel tool calls in DeepSeek Responses conversations.
    • Complete, unambiguous call-and-result batches are now kept in the correct order.
    • Additional context is placed after completed tool-call batches.
  • Bug Fixes
    • Ambiguous histories—including missing, duplicated, or out-of-order results—remain unchanged to avoid unsafe reordering.
    • Sequential reasoning rounds continue to be handled separately.
  • Documentation
    • Added guidance describing DeepSeek tool-result ordering behavior.
  • Tests
    • Added coverage for parallel calls, injected context, sequential rounds, and ambiguous histories.

@github-actions

Copy link
Copy Markdown
Contributor

Deterministic PR hygiene checks passed.

@github-actions github-actions Bot added the bug Something isn't working label Aug 11, 2026
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

DeepSeek V4 Flash now normalizes complete, unambiguous parallel Responses tool-call batches. The adapter preserves ambiguous histories unchanged. Documentation and inbound-wire tests cover ordering, context placement, separate rounds, and invalid matches.

Changes

DeepSeek Responses normalization

Layer / File(s) Summary
Batch normalization and contract
src/adapters/openai-responses.ts, docs-site/src/content/docs/reference/adapters.md
normalizeResponsesToolResultAdjacency now validates complete forward-matching batches, emits calls before matching results, preserves intervening items, and leaves ambiguous histories unchanged.
DeepSeek provider wiring
structure/04-transports-and-sidecars.md
DeepSeek V4 Flash enables provider-scoped Responses history normalization.
Regression coverage
tests/deepseek-inbound-wire.test.ts
Tests cover parallel batches, injected context, separate reasoning rounds, and missing, reversed, or duplicate matches.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant InboundRequest
  participant normalizeResponsesToolResultAdjacency
  participant DeepSeekResponsesAPI
  InboundRequest->>normalizeResponsesToolResultAdjacency: Responses history with tool calls and results
  normalizeResponsesToolResultAdjacency->>normalizeResponsesToolResultAdjacency: Validate call/result order and uniqueness
  normalizeResponsesToolResultAdjacency->>DeepSeekResponsesAPI: Normalized complete batch or unchanged ambiguous history
Loading

Possibly related PRs

Suggested reviewers: ingwannu, lidge-jun

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the DeepSeek continuation fix for parallel reasoning replay.
Linked Issues check ✅ Passed The changes address issue #1477 by preserving and replaying reasoning through unambiguous parallel tool-call continuations.
Out of Scope Changes check ✅ Passed The code, tests, and documentation changes remain focused on DeepSeek Responses reasoning replay and tool-result ordering.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ocx-155-bug-deepseek-v4-flash-responses-continuation-still-drops

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/adapters/openai-responses.ts`:
- Around line 591-601: Update the normalization logic around the calls/outputs
validation and batch construction to reject orphan outputs by requiring every
entry in outputs to map to exactly one call. Before rewriting each batch,
require matched output indices to be strictly increasing in call order; preserve
body unchanged for missing, duplicate, reversed, or otherwise out-of-order
histories. Add regression coverage for an orphan output in an otherwise valid
batch and reversed parallel outputs.

In `@tests/deepseek-inbound-wire.test.ts`:
- Around line 102-104: Update deepseekReasoningProvider() to return the
unmodified deepseekProvider() result instead of overriding
preserveResponsesReasoningContent. Add an assertion that the returned
configuration has preserveResponsesReasoningContent set to true before the tests
use it, so the tests validate the registered DeepSeek preset behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 8e5ea2d3-4319-48bf-958d-4ed1e2635737

📥 Commits

Reviewing files that changed from the base of the PR and between e8db4e0 and ee54ca9.

📒 Files selected for processing (4)
  • docs-site/src/content/docs/reference/adapters.md
  • src/adapters/openai-responses.ts
  • structure/04_transports-and-sidecars.md
  • tests/deepseek-inbound-wire.test.ts

Comment on lines +591 to +601
// Fail closed: only normalize when every collected call has exactly one matching
// result and that result appears after its call. Missing, duplicate, or backward
// histories are ambiguous and must be left untouched for the upstream to reject.
const pairs: Array<{ callIndex: number; outputIndex: number }> = [];
for (const [key, callIndices] of calls) {
const outputIndices = outputs.get(key);
if (callIndices.length !== 1 || outputIndices?.length !== 1) continue;
if (callIndices.length !== 1 || outputIndices?.length !== 1) return body;
const callIndex = callIndices[0]!;
const outputIndex = outputIndices[0]!;
if (outputIndex === callIndex + 1) continue;
movedOutputIndices.add(outputIndex);
outputAfterCall.set(callIndex, input[outputIndex]);
if (outputIndex <= callIndex) return body;
pairs.push({ callIndex, outputIndex });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reject unmatched and result-reversed tool histories.

Lines 595-601 validate calls, but they do not reject an output whose call_id has no matching call. Lines 609-622 also accept callA, callB, outputB, outputA and rewrite it to callA, callB, outputA, outputB. Both cases are ambiguous histories. The adapter must return body unchanged.

Validate every entry in outputs against exactly one call. Before building each batch, require output indices to be strictly increasing in call order. Add regression cases for an orphan output beside an otherwise valid batch and for reversed parallel outputs.

Proposed fix
+  for (const [key, outputIndices] of outputs) {
+    if (calls.get(key)?.length !== 1 || outputIndices.length !== 1) return body;
+  }
+
   const pairs: Array<{ callIndex: number; outputIndex: number }> = [];
   for (const [key, callIndices] of calls) {
     const outputIndices = outputs.get(key);
     if (callIndices.length !== 1 || outputIndices?.length !== 1) return body;
     while (next < pairs.length && pairs[next]!.callIndex < firstOutputIndex) {
       group.push(pairs[next]!);
       firstOutputIndex = Math.min(firstOutputIndex, pairs[next]!.outputIndex);
       next += 1;
     }
+    if (group.some((pair, index) =>
+      index > 0 && pair.outputIndex <= group[index - 1]!.outputIndex,
+    )) return body;

As per path instructions, “Preserve histories with missing, duplicate, reversed, or otherwise out-of-order calls/results unchanged.”

Also applies to: 609-622

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/adapters/openai-responses.ts` around lines 591 - 601, Update the
normalization logic around the calls/outputs validation and batch construction
to reject orphan outputs by requiring every entry in outputs to map to exactly
one call. Before rewriting each batch, require matched output indices to be
strictly increasing in call order; preserve body unchanged for missing,
duplicate, reversed, or otherwise out-of-order histories. Add regression
coverage for an orphan output in an otherwise valid batch and reversed parallel
outputs.

Source: Path instructions

Comment on lines +102 to +104
function deepseekReasoningProvider(): OcxProviderConfig {
return { ...deepseekProvider(), preserveResponsesReasoningContent: true };
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Exercise the registered DeepSeek reasoning configuration.

deepseekReasoningProvider() forces preserveResponsesReasoningContent: true. The new tests therefore pass even if the DeepSeek registry entry stops enabling this required provider behavior. In that failure case, buildRequest can remove replayed plaintext reasoning before the request reaches DeepSeek.

Return the unmodified deepseekProvider() result. Assert that preserveResponsesReasoningContent is true before using it.

Proposed fix
 function deepseekReasoningProvider(): OcxProviderConfig {
-  return { ...deepseekProvider(), preserveResponsesReasoningContent: true };
+  const provider = deepseekProvider();
+  expect(provider.preserveResponsesReasoningContent).toBe(true);
+  return provider;
 }

As per path instructions, “A behavior change in src/ should come with a focused regression test near the existing tests for that subsystem.” The PR objective requires the built-in DeepSeek preset to preserve reasoning content.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function deepseekReasoningProvider(): OcxProviderConfig {
return { ...deepseekProvider(), preserveResponsesReasoningContent: true };
}
function deepseekReasoningProvider(): OcxProviderConfig {
const provider = deepseekProvider();
expect(provider.preserveResponsesReasoningContent).toBe(true);
return provider;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/deepseek-inbound-wire.test.ts` around lines 102 - 104, Update
deepseekReasoningProvider() to return the unmodified deepseekProvider() result
instead of overriding preserveResponsesReasoningContent. Add an assertion that
the returned configuration has preserveResponsesReasoningContent set to true
before the tests use it, so the tests validate the registered DeepSeek preset
behavior.

Source: Path instructions

@Wibias Wibias closed this Aug 11, 2026
@Wibias
Wibias deleted the ocx-155-bug-deepseek-v4-flash-responses-continuation-still-drops branch August 12, 2026 01:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant