Skip to content

[Bug] preserveReasoningContentModels still emits bare tool_call continuations when the replay cache misses (long sessions) — DeepSeek thinking mode 400 #1193

Closed

Description

@gdxnpy

Client or integration

Codex App (also reproduced with a direct /v1/chat/completions client)

Area

Provider adapter

Summary

The #950 fix re-attaches reasoning_content from the in-process replay cache, but that cache is bounded (64 entries / 256 KiB / 1 h TTL, see src/responses/reasoning-replay-cache.ts). When the cache misses on a long session, src/adapters/openai-chat.ts still emits an assistant tool_call history message without reasoning_content, and DeepSeek thinking mode rejects it with HTTP 400:

Provider error 400: Error from provider (Console): Upstream request failed: [invalid_request_error] The `reasoning_content` in the thinking mode must be passed back to the API.

Both affected paths have no fallback when the cache lookup is empty:

  • Main assistant path (openai-chat.ts ~line 348): if (cached.length > 0) { reasoningContent = ... } — when cached.length === 0, the message goes out bare.
  • Orphan-repair path (openai-chat.ts ~line 412): ...(cachedReasoning ? { reasoning_content: cachedReasoning } : {}) — when cachedReasoning is undefined, the synthesized assistant tool_call has no reasoning_content.

Reproduction

  1. Configure the opencode-zen provider with preserveReasoningContentModels: ["deepseek-v4-flash-free"].
  2. Run a long multi-turn tool-calling session with DeepSeek thinking mode (reasoning_effort=max).
  3. Continue a turn whose assistant tool_call history entry lost its reasoning item (compaction, lost assistant turn, or resumed thread) and whose call id is no longer in the replay cache (evicted by >64 entries, >256 KiB, or >1 h TTL).
  4. The request fails with HTTP 400 as above.

Verified conditions:

  • A/B test with the same conversation shape: assistant tool_call message missing reasoning_content -> 400; same shape with a minimal placeholder (" ") -> 200.
  • Failures are intermittent and correlate exactly with cache misses: they appear only when peekReasoningForCall returns nothing for the call id.
  • Reproduced both after >1 h idle and after enough tool rounds to evict the entry.
  • The same session is stable with providers whose upstream tolerates missing reasoning_content (e.g. Kimi), which is why the bug is provider-specific.

Version

2.10.2 (npm @bitkyc08/opencodex); also confirmed present on current main.

Operating system

macOS (darwin, arm64)

Provider and model

opencode-zen / deepseek-v4-flash-free (OpenCode Zen Console, DeepSeek thinking mode)

Logs or error output

Provider error 400: Error from provider (Console): Upstream request failed: [invalid_request_error] The `reasoning_content` in the thinking mode must be passed back to the API.

Redacted configuration

{
  "providers": {
    "opencode-zen": {
      "baseUrl": "https://opencode.ai/zen/v1",
      "preserveReasoningContentModels": ["deepseek-v4-flash-free"]
    }
  }
}

Suggested fix

  • When the replay cache misses for a preserveReasoningContentModels provider, never emit a bare tool_call continuation: inject a minimal placeholder (" ") in both the main assistant path and the orphan-repair path of src/adapters/openai-chat.ts. Verified locally that DeepSeek thinking mode accepts the placeholder (both " " and "..." returned 200), so this closes the 400 deterministically.
  • Longer-term: instead of relying on the bounded in-process side cache, persist the reasoning attachment with the conversation state (e.g. responses-state) so resumed tool loops keep the original reasoning text, or make cache eviction aware of preserveReasoningContentModels call ids.

Checks

  • I searched existing issues and documentation.
  • I removed secrets, tokens, account details, request credentials, and personal data.

Activity

  1. github-actions commented on Aug 7, 2026

    @github-actions
    Contributor

    Automated translation bookkeeping — detected language: English.

  2. added
    bugSomething isn't working
    providerProvider adapters, OpenAI-compat presets, upstream API quirks
    toolstool_calls, MCP, web-search / sidecar tools
    on Aug 7, 2026
  3. gdxnpy commented on Aug 7, 2026

    @gdxnpy
    Author

    Here is the exact local workaround I have been running (OpenCodex 2.10.2 / also applies to current main): two hunks in src/adapters/openai-chat.ts that close this gap. When the bounded replay cache misses for a preserveReasoningContentModels provider, both paths inject a minimal placeholder (" ") instead of emitting a bare tool_call continuation. Verified A/B: same conversation shape missing reasoning_content -> 400, with the placeholder -> 200 (DeepSeek thinking mode also accepts "...").

    --- a/src/adapters/openai-chat.ts
    +++ b/src/adapters/openai-chat.ts
    @@ -346,6 +346,14 @@
               // recorded under every call id — join unique texts only.
               if (cached.length > 0) {
                 reasoningContent = [...new Set(cached)].join("\n");
    +          } else {
    +            // Fallback (extends #950): the replay cache is bounded (64 entries /
    +            // 256KB / 1h TTL) and can always miss on long sessions, and some
    +            // tool rounds carry no recorded reasoning at all. DeepSeek thinking
    +            // mode 400s on ANY tool_call assistant message missing
    +            // reasoning_content, so inject a minimal placeholder rather than
    +            // emit a bare continuation that the upstream will reject.
    +            reasoningContent = " ";
               }
             }
             if (reasoningContent.length > 0 && modelInList(provider.preserveReasoningContentModels, parsed.modelId)) {
    @@ -412,10 +420,17 @@
                 toolCallId && modelInList(provider.preserveReasoningContentModels, parsed.modelId)
                   ? peekReasoningForCall(toolCallId, replayCacheScope)
                   : undefined;
    +          // Same fallback as the main-assistant path: never emit a bare orphan
    +          // tool_call continuation on a thinking-mode provider — inject a
    +          // placeholder when the replay cache missed (bounded cache can always
    +          // miss on long sessions), or DeepSeek thinking mode 400s.
    +          const orphanReasoning =
    +            cachedReasoning
    +            ?? (modelInList(provider.preserveReasoningContentModels, parsed.modelId) ? " " : undefined);
               out.push({
                 role: "assistant",
                 content: emptyAssistantContent(provider),
    -            ...(cachedReasoning ? { reasoning_content: cachedReasoning } : {}),
    +            ...(orphanReasoning ? { reasoning_content: orphanReasoning } : {}),
                 tool_calls: [{
                   id: toolCallId,
                   type: "function",

    Notes:

    • Requires providers.opencode-zen.preserveReasoningContentModels: ["deepseek-v4-flash-free"] in config; the fallback exercises only for models in that list.
    • The placeholder is only used when the replay cache lookup is empty — normal turns still send the original reasoning text.
    • One trade-off to consider for the official fix: a " " placeholder is semantically "empty" to downstream compactions, so the maintainers may prefer persisting the reasoning with conversation state (e.g. responses-state) instead of a bounded side cache, as suggested above.
  4. Ingwannu commented on Aug 8, 2026

    @Ingwannu
    Owner

    Confirmed as a real cache-miss continuation bug. The implementation is now tracked in draft PR #1205, which covers both the normal assistant-history path and orphan-repair path while limiting placeholder fabrication to explicitly configured models. I am leaving this issue open: the PR still has a failing hygiene gate on an auth-surface allowlist, queued exact-head CI, and no independent maintainer approval. It should close only after those boundaries are reviewed and the fix merges into dev.

  5. added 3 commits that reference this issue on Aug 9, 2026
    817192f
    cd73c15
    c528847
  6. Wibias commented on Aug 9, 2026

    @Wibias
    Contributor

    Resolved on dev.

    The fix developed in #1205 has been landed directly with the contributor's authorship preserved:

    • 817192f
    • cd73c15
    • c528847

    with follow-up boundary-test coverage in:

    • 5cd3edb

    The landed implementation fixes both cache-miss continuation paths:

    • normal assistant tool-call history reconstruction;
    • orphan tool-call repair when the original assistant turn is missing.

    For models that require reasoning placeholders, a replay-cache miss now emits a minimal reasoning_content placeholder instead of a bare assistant tool_call continuation.

    The final implementation also separates models that merely preserve reasoning from models that require placeholder fabrication, with explicit opt-outs for providers/models where thinking can legitimately be disabled.

    The auth/config serialization boundaries introduced by that metadata were independently reviewed and covered so explicit non-empty and empty placeholder lists survive configuration handling without exposing credentials.

    This resolves the DeepSeek thinking-mode 400 described here.

    Closing as completed.

  7. added 3 commits that reference this issue on Sep 17, 2026
    5d25f20
    e42df60
    6f7b598
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingproviderProvider adapters, OpenAI-compat presets, upstream API quirkstoolstool_calls, MCP, web-search / sidecar tools

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions