Skip to content

fix(deepseek): preserve Responses reasoning replay on continuations (#875) - #906

Closed
Yuxin-Qiao wants to merge 1 commit into
lidge-jun:devfrom
Yuxin-Qiao:codex/deepseek-reasoning-replay
Closed

fix(deepseek): preserve Responses reasoning replay on continuations (#875)#906
Yuxin-Qiao wants to merge 1 commit into
lidge-jun:devfrom
Yuxin-Qiao:codex/deepseek-reasoning-replay

Conversation

@Yuxin-Qiao

@Yuxin-Qiao Yuxin-Qiao commented Aug 2, 2026

Copy link
Copy Markdown

Summary

Fixes the proxy half of #875: DeepSeek V4 Flash Responses turns stall after tool calls because the passthrough sanitizer blanked reasoning.content for every provider. DeepSeek's Responses API is stateless and requires replaying prior assistant reasoning text on every continuation; an emptied reasoning item breaks the replay, so tool-call loops cannot finish.

  • sanitizeReasoningInputContent now keeps raw reasoning_text content for models in preserveReasoningContentModels (already seeded for DeepSeek thinking models, including deepseek-v4-flash) and keeps the existing blanking for ChatGPT-native passthrough, which accepts reasoning input only with empty content.
  • Proxy-minted ocxr1 envelopes are still stripped unconditionally.
  • This is the "local half" of the [Bug] DeepSeek V4 Flash Responses route stalls after tool calls #875 triage; PR Fix DeepSeek Responses over Codex WebSocket #890 covers the complementary Codex Desktop WebSocket terminal-event half.

Why it matters for subagents

With this fix, deepseek/deepseek-v4-flash can be selected as a sub-agent worker (ocx agent subagents set deepseek/deepseek-v4-flash or "subagentModels": ["deepseek/deepseek-v4-flash"]) and its tool-call loops continue correctly. Added a regression test proving the routed entry is a viable sub-agent candidate.

Change

  • src/adapters/openai-responses.ts: sanitizeReasoningInputContent(body, opts) with a preserve list + model id; content kept for preserve-listed models, blanked otherwise; envelopes always stripped. Call site passes provider.preserveReasoningContentModels and parsed.modelId.
  • tests/deepseek-reasoning-replay.test.ts: new file, 5 tests covering preserve/blank/envelope unit behavior, an end-to-end handleResponses continuation carrying reasoning + tool output upstream, and sub-agent routability.

Validation

  • bun test tests/deepseek-reasoning-replay.test.ts tests/deepseek-inbound-wire.test.ts — 19 pass, 0 fail
  • bun run typecheck — pass
  • git diff --check — pass
  • Adapter/subagent-adjacent regression set (76 files): failure set is identical to the pre-change baseline on this machine (environmental port/timeout failures only). The full-suite run crashed under the local sandbox (Bun segfault); CI remains the authority for the full suite.

Source commits

Summary by CodeRabbit

  • Bug Fixes

    • Improved handling of reasoning content for supported models, preserving valid content during follow-up requests.
    • Removed proxy-generated encrypted reasoning data that should not be forwarded.
    • Improved continued tool-call requests and fallback behavior for eligible models, including DeepSeek.
    • Prevented unnecessary request updates when no content changes are needed.
  • Tests

    • Added coverage for reasoning replay, tool-call continuation, proxy envelope removal, and model-specific behavior.

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown

PR quality gates passed

This pull request now targets dev with acceptable ancestry and description.

The title was left unchanged. The pull request has been marked ready for review again.

@github-actions
github-actions Bot marked this pull request as draft August 2, 2026 19:23
@github-actions github-actions Bot added the bug Something isn't working label Aug 2, 2026
@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

DeepSeek replay now preserves configured raw reasoning content, removes ocxr1 encrypted envelopes, and forwards reasoning with tool output during continuations. The passthrough pipeline supplies model-specific configuration, and tests cover sanitization and fallback routing.

Changes

Reasoning replay sanitization

Layer / File(s) Summary
Model-specific sanitization rules
src/adapters/openai-responses.ts
sanitizeReasoningInputContent accepts optional model configuration. It preserves raw reasoning content for configured models, removes ocxr1 encrypted content, and tracks only actual mutations.
Pipeline wiring and DeepSeek validation
src/adapters/openai-responses.ts, tests/deepseek-reasoning-replay.test.ts
The passthrough pipeline passes provider preservation settings and the parsed model ID. Tests cover DeepSeek sanitization, continuation forwarding, proxy envelope removal, non-preserving models, and fallback ordering.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested reviewers: ingwannu, lidge-jun, wibias

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes preserving DeepSeek Responses reasoning replay during continuations, which is the main change.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 72313f1a54

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +56 to +58
if (hasOcxEnvelope) {
delete next.encrypted_content;
mutated = true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Decode hidden reasoning before stripping its envelope

When reasoning.summary is absent or none, src/bridge.ts:380-397 stores the prior raw reasoning only in a txt-only ocxr1 envelope, with no content array. On the next DeepSeek continuation, this branch deletes that sole copy, while preserveContent cannot help because hasRawContent is false; the stateless upstream therefore still receives no reasoning replay and tool-call loops can stall in the default hidden-summary mode. Decode the envelope's txt into content: [{ type: "reasoning_text", text }] for preserve-listed models before removing encrypted_content, and cover a bridge-output-to-passthrough round trip.

AGENTS.md reference: src/AGENTS.md:L19-L19

Useful? React with 👍 / 👎.

@Yuxin-Qiao
Yuxin-Qiao force-pushed the codex/deepseek-reasoning-replay branch from 72313f1 to 2691d00 Compare August 2, 2026 19:28
@github-actions
github-actions Bot marked this pull request as ready for review August 2, 2026 19:29

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 1042-1045: Refactor the sanitization pipeline in the surrounding
response-body handling code into sequential intermediate assignments, reusing a
mutable value for each step from scrubOcxCompactionItems through
sanitizeReasoningInputContent, stripUnsupportedHostedTools, stripInvalidItemIds,
stripItemIdsWhenUnstored, stripUnsupportedReasoningParams,
stripSparkCompatibility, and normalizeToolSchemas. Preserve the existing call
order and pass the same preserveContentModels and modelId options to
sanitizeReasoningInputContent.
🪄 Autofix (Beta)

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: 4e1e9495-ad1f-4bbf-9f43-e35278bcbcba

📥 Commits

Reviewing files that changed from the base of the PR and between 72313f1 and 2691d00.

📒 Files selected for processing (2)
  • src/adapters/openai-responses.ts
  • tests/deepseek-reasoning-replay.test.ts

Comment on lines +1042 to +1045
const sanitizedBody = normalizeToolSchemas(stripSparkCompatibility(stripUnsupportedReasoningParams(stripItemIdsWhenUnstored(stripInvalidItemIds(stripUnsupportedHostedTools(sanitizeReasoningInputContent(scrubOcxCompactionItems(outBody), {
preserveContentModels: provider.preserveReasoningContentModels,
modelId: parsed.modelId,
})))))));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Deeply nested call chain gets harder to read with each new parameter.

This line already chains eight function calls (normalizeToolSchemasstripSparkCompatibility → ... → sanitizeReasoningInputContentscrubOcxCompactionItems), and this PR adds a two-key options object inside the innermost call, pushing the single logical statement further out. Reviewers and future editors must track matching parens across the whole line. Consider unwinding this into sequential let-reassignments so each sanitization step is independently readable and diffable.
[optional_refactor_low_effort_high_reward_placeholder]

♻️ Proposed refactor: sequential pipeline instead of nested calls
-      const sanitizedBody = normalizeToolSchemas(stripSparkCompatibility(stripUnsupportedReasoningParams(stripItemIdsWhenUnstored(stripInvalidItemIds(stripUnsupportedHostedTools(sanitizeReasoningInputContent(scrubOcxCompactionItems(outBody), {
-        preserveContentModels: provider.preserveReasoningContentModels,
-        modelId: parsed.modelId,
-      })))))));
+      let sanitizedBody = scrubOcxCompactionItems(outBody);
+      sanitizedBody = sanitizeReasoningInputContent(sanitizedBody, {
+        preserveContentModels: provider.preserveReasoningContentModels,
+        modelId: parsed.modelId,
+      });
+      sanitizedBody = stripUnsupportedHostedTools(sanitizedBody);
+      sanitizedBody = stripInvalidItemIds(sanitizedBody);
+      sanitizedBody = stripItemIdsWhenUnstored(sanitizedBody);
+      sanitizedBody = stripUnsupportedReasoningParams(sanitizedBody);
+      sanitizedBody = stripSparkCompatibility(sanitizedBody);
+      sanitizedBody = normalizeToolSchemas(sanitizedBody);
📝 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
const sanitizedBody = normalizeToolSchemas(stripSparkCompatibility(stripUnsupportedReasoningParams(stripItemIdsWhenUnstored(stripInvalidItemIds(stripUnsupportedHostedTools(sanitizeReasoningInputContent(scrubOcxCompactionItems(outBody), {
preserveContentModels: provider.preserveReasoningContentModels,
modelId: parsed.modelId,
})))))));
let sanitizedBody = scrubOcxCompactionItems(outBody);
sanitizedBody = sanitizeReasoningInputContent(sanitizedBody, {
preserveContentModels: provider.preserveReasoningContentModels,
modelId: parsed.modelId,
});
sanitizedBody = stripUnsupportedHostedTools(sanitizedBody);
sanitizedBody = stripInvalidItemIds(sanitizedBody);
sanitizedBody = stripItemIdsWhenUnstored(sanitizedBody);
sanitizedBody = stripUnsupportedReasoningParams(sanitizedBody);
sanitizedBody = stripSparkCompatibility(sanitizedBody);
sanitizedBody = normalizeToolSchemas(sanitizedBody);
🤖 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 1042 - 1045, Refactor the
sanitization pipeline in the surrounding response-body handling code into
sequential intermediate assignments, reusing a mutable value for each step from
scrubOcxCompactionItems through sanitizeReasoningInputContent,
stripUnsupportedHostedTools, stripInvalidItemIds, stripItemIdsWhenUnstored,
stripUnsupportedReasoningParams, stripSparkCompatibility, and
normalizeToolSchemas. Preserve the existing call order and pass the same
preserveContentModels and modelId options to sanitizeReasoningInputContent.

@lidge-jun

Copy link
Copy Markdown
Owner

Closing as a functional duplicate: the proxy half of #875 already landed in #892 (wt3, b42d573) — the registry seeds preserveResponsesReasoningContent: true for DeepSeek and the passthrough sanitizer keeps raw reasoning content for it, with the same regression file (tests/deepseek-reasoning-replay.test.ts). Your per-model preserveReasoningContentModels list is the finer-grained mechanism; if the provider-wide flag ever proves too broad, that refinement is welcome as a follow-up. Thanks for the careful write-up.

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.

2 participants