Skip to content

Provider API: historical reasoning_content is dropped by name for deepseek-v4.1-flash / v4-flash / v4-flash-vision-exp #840

Closed

Description

@AkaneRanma

Summary

On the Provider API (/provider/v1/chat/completions), the historical reasoning_content of an assistant message is dropped by key name before the request is serialized upstream, for these slugs:

  • deepseek/deepseek-v4.1-flash
  • deepseek/deepseek-v4-flash
  • deepseek/deepseek-v4-flash-vision-exp

It works correctly (reasoning_content is forwarded and re-inserted into context) for deepseek/deepseek-v4-pro and deepseek/deepseek-v4-flash-fast, and for the same request body against https://api.deepseek.com/v1.

Per DeepSeek's thinking-mode guide, when the request carries tools, the reasoning_content of every historical assistant message must be passed back and is re-inserted into context. Because it is dropped here, in a tool-calling loop the model can no longer see its own earlier chain of thought, so it re-derives its reasoning from scratch every turn. This degrades multi-turn coherence and is easy to misread as the model "getting dumber" on longer tasks.

The gateway evidently recognizes the field, so it isn't an unknown-key pass-through issue: sending a non-string value returns a clean schema error naming the exact parameter:

HTTP 400 {"error":{"message":"Invalid input: expected string, received object",
                   "type":"invalid_request_error",
                   "param":"messages.1.reasoning_content"}}

The same applies to reasoning (expected string, received number) and reasoning_details (expected array, received string). So the field is validated... and then stripped for those three slugs.

Expected Behavior

With tools present in the request, reasoning_content on historical assistant messages should be forwarded to the upstream model and re-inserted into context, as DeepSeek documents and as deepseek-v4-pro / deepseek-v4-flash-fast already do on the same gateway.

Actual Behavior

The field is schema-validated and then dropped. It contributes zero to the upstream prompt, while a mirrored copy of the exact same text in content counts in full.

Primary evidence is prompt_tokens accounting (3000-char reasoning block ≈ 1516 tokens), paired within a run:

request body (tool-loop shape, tools present) prompt_tokens delta
assistant message with no reasoning_content 352 —
reasoning_content = 3000 chars 352 +0
reasoning = 3000 chars 352 +0
reasoning_details = [{...3000 chars}] 352 +0
control: the same 3000 chars in content 1868 +1516

Behavioural confirmation (marker test, see repro below): a random 6-char code embedded in the historical reasoning cannot be repeated back by the model, while the identical code placed in content can.

Cross-model matrix — identical request body, tools present:

model historical reasoning reaches upstream
deepseek/deepseek-v4.1-flash ❌ dropped (352 → 352)
deepseek/deepseek-v4-flash ❌ dropped
deepseek/deepseek-v4-flash-vision-exp ❌ dropped
deepseek/deepseek-v4-pro ✅ forwarded (421 → 1937)
deepseek/deepseek-v4-flash-fast ✅ forwarded
https://api.deepseek.com/v1 (deepseek-v4-pro, same body) ✅ forwarded

Steps to reproduce the issue

1. Marker test — embed a known code in historical reasoning, then ask for it back:

curl -s https://api.commandcode.ai/provider/v1/chat/completions \
  -H "Authorization: Bearer $COMMANDCODE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "model": "deepseek/deepseek-v4.1-flash",
  "tools": [{"type":"function","function":{"name":"get_weather","description":"w",
    "parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}}}],
  "messages": [
    {"role":"user","content":"Check Beijing weather with get_weather."},
    {"role":"assistant",
     "reasoning_content":"Internal check: the session code is ZQ7K2M. I must repeat it when asked.",
     "tool_calls":[{"id":"c1","type":"function","function":{"name":"get_weather","arguments":"{\"city\":\"Beijing\"}"}}]},
    {"role":"tool","tool_call_id":"c1","content":"25C clear"},
    {"role":"user","content":"Repeat the session code from your earlier reasoning. Output only those 6 characters."}
  ]}'
  • Expected: ZQ7K2M — this is what api.deepseek.com returns for the identical body, and what this gateway returns for deepseek-v4-pro / deepseek-v4-flash-fast.
  • Actual: the model replies that it never recorded any code.

2. Token accounting — repeat the same request twice, once with and once without reasoning_content on the assistant message, and compare usage.prompt_tokens. The value does not move for the three slugs above (+0), while inserting the same text into content moves it by the expected amount.

Note: the marker test alone is not fully deterministic (the model occasionally declines even when the block clearly arrived), so prompt_tokens is the primary signal. The delta is paired within a run and stable across runs (alternated 3× on v4.1-flash: +0 without the header, +1516 with it).

Command Code Version

n/a — Provider API (/provider/v1) only; no CLI involved.

Operating System

Linux

Terminal/IDE

n/a — no CLI/IDE involved; raw HTTP requests against /provider/v1

Shell

n/a — repro is raw HTTP (curl/httpx), not an interactive shell session

Session file (optional)

No response

Fix prompt (optional)

On the Provider API (/provider/v1), historical assistant reasoning_content is stripped by key
name before the request reaches the upstream, for deepseek/deepseek-v4.1-flash,
deepseek/deepseek-v4-flash, and deepseek/deepseek-v4-flash-vision-exp. It works for
v4-pro and v4-flash-fast. DeepSeek requires the field to be replayed when tools is present.

Where to look:

  • The request-serialization path for the affected slugs' default (non-ZDR) upstream route.
    The key is schema-validated (a non-string returns 400 naming messages.N.reasoning_content),
    so it is recognized and then dropped during serialization — not an unknown-key pass-through.
  • deepseek-v4-flash is affected on BOTH the default and ZDR routes, so treat it as a separate
    entry from the v4.1-flash / v4-flash-vision-exp pair (at least two code paths).
  • Diff the serialization against the slugs that work (v4-pro, v4-flash-fast), and against
    api.deepseek.com for the same body.

Correct behaviour: with tools present, pass reasoning_content on historical assistant
messages through unchanged (and re-insert it into context upstream).

How to check:

  1. Send the same tool-loop body twice — with and without a 3000-char reasoning_content on the
    assistant message — and compare usage.prompt_tokens. It must grow by the block's token count;
    today it is byte-identical (+0) for the three affected slugs.
  2. Discriminator: also put the same 3000 chars in content. If content counts but
    reasoning_content still adds 0 in the SAME message, the strip is key-level, not a rewrite.
  3. Behavioural check (optional): embed a random 6-char code in the historical reasoning and ask
    the model to repeat it. Not fully deterministic — prefer the token delta.

Additional context

Analysis — the drop is a key-name strip, not a message rewrite

Putting a mirrored copy in content and the real value in reasoning_content in the same assistant message discriminates the two possibilities (3000-char block ≈ 1516 tokens):

route model A: base B: rc only C: content only D: both D − C
default v4.1-flash 352 352 (+0) 1868 (+1516) 1868 +0
default v4-pro 421 1937 (+1516) 1937 (+1516) 3453 +1516
ZDR v4.1-flash 352 1868 (+1516) 1868 (+1516) 3384 +1516
ZDR v4-pro 343 1859 (+1516) 1859 (+1516) 3375 +1516

On the default route for v4.1-flash, the content copy counts in full while reasoning_content adds exactly 0 — in the same message. The message itself is not rewritten; the key is stripped by name.

The drop is route-specific, not model-specific. With x-cmd-zdr: 1, the same request on the same slug forwards the field correctly (352 → 1868). Since ZDR only changes which upstream serves the request, the filter appears to live in the non-ZDR upstream's request adapter — and deepseek-v4-flash is +0 on both routes, so at least two separate code paths are involved.

Related but separate: on a tool-role message a reasoning_content key is silently accepted (HTTP 200) instead of being schema-rejected, so the validation is asymmetric as well.

Additional context

  • Plan: GOAT. Reproduced via raw curl/httpx against /provider/v1, and against api.deepseek.com/v1 as a control.
  • Reported and triaged in the Discord bug thread first; staff asked for a GitHub issue with the repro.
  • A workaround exists but has real costs, which is why this needs a fix on the default route rather than a client-side flag: forcing x-cmd-zdr: 1 restores forwarding, but per the ZDR docs it meters at the default allowance (the deepseek-v4.1-flash boosted allowance drops from $60 to $20 on GOAT), routes may be pricier, and it is strict — no fallback, a bare 422 cmd_zdr_no_providers if ZDR capacity is unavailable.
  • Related: Provider API: deepseek-v4.1-flash is missing reasoning effort metadata #837 (deepseek-v4.1-flash missing reasoning-effort metadata) is a different symptom on the same slug; both look like per-slug provider metadata/routing entries.
  • Nothing in the docs describes how reasoning_content on historical assistant messages is handled per slug, so there is no documented behaviour to hold clients to here — treating this as a backend fault.

Activity

  1. added theissue type on Sep 11, 2026
  2. ahmadawais commented on Sep 11, 2026

    @ahmadawais
    Member

    @ahmadbilaldev can you check.

  3. ahmadawais commented on Sep 11, 2026

    @ahmadawais
    Member

    I believe this was fixed.

    From Command Code I got this:

    Both. The bug they describe is fixed; the symptom they saw is still real.

    • Our forwarding: fixed. 0e472e2 (2026-09-08, in origin/main) declares all three fields and maps them per-provider. I ran their exact body through reasoning-echo.ts and reasoning_content comes out intact.
    • Their symptom: still broken, but upstream. We're not the one dropping it.

    Nothing to fix in this repo.

  4. AkaneRanma commented on Sep 11, 2026

    @AkaneRanma
    Author

    Re-tested against production after your comment — the affected slugs still drop the field on the default route.

    All timestamps below come from your edge's own date header (my host clock matches it), and the requests are plain HTTP against /provider/v1. No CLI is in the path, so there is nothing client-side to update here.

    1. New oracle: the drop is measurable in tokens

    Replace the field's value with a block far larger than anything I'd normally replay, and read usage.prompt_tokens:

    route model rc = 1.5 KB rc = 600 KB (≈105k tok)
    default deepseek-v4.1-flash 352 352
    ZDR deepseek-v4.1-flash 614 104 848
    default deepseek-v4-flash 352 352
    ZDR deepseek-v4-flash 342 104 839
    default deepseek-v4-pro 683 104 917
    default deepseek-v4-flash-fast 610 104 844

    On the working routes the block is counted to the token, so the text demonstrably reaches the model's tokenizer. On the affected default routes a 600 KB block adds exactly zero — and it isn't a length cap, since a 3 KB block also adds 0 (matrix in the issue body).

    So the removal happens before tokenization, at a point selected by the route/provider entry — not model behaviour, not a size threshold. That still doesn't by itself separate "your edge" from "the wire slug's provider", but it does mean the fix has to land in whatever construct picks that route.

    2. What "fixed several days ago" refers to

    Your changelog header says it covers "Command Code CLI and Desktop" — and in the 9/8–9/9 window there is no entry for reasoning forwarding at all. The only v4.1-flash entries are:

    • v1.51.0 (Sep 8) — Add DeepSeek V4.1 Flash Beta
    • v1.51.2 (Sep 9) — Remove DeepSeek V4.1 Flash Beta
    • v1.53.0 (Sep 9, published to npm 2026-09-10T06:13Z) — Add new deepseek/deepseek-v4.1-flash model

    0e472e2 is dated 2026-09-08, which lines up with adding that model to the CLI rather than with a gateway change. That reading matches your description exactly — "declares all three fields and maps them per-provider" is what a client does when it starts supporting a model. If so, reasoning-echo.ts is exercising the CLI's own request construction, and nothing in it touches the inbound path a third-party body takes.

    To be precise about what that test can show: a passing unit test on the transform function proves the field survives your mapping code. It can't show what the deployed gateway does with a request on a given route — the two are different layers, and "unit test green + production still drops it" is a perfectly consistent state.

    3. Client identity isn't the variable

    I also checked whether the gateway special-cases its own CLI — same request, different identity headers:

    sent as prompt_tokens field forwarded
    plain httpx (no UA) 352 no
    User-Agent: command-code/1.53.0 352 no
    User-Agent: CommandCode/1.53.0 + x-cmd-client: cli 352 no
    User-Agent: command-code/1.53.0 + x-cmd-version: 1.53.0 352 no
    User-Agent: OpenAI/NodeJS/4.0.0 352 no
    x-cmd-zdr: 1 (control) 1868 yes

    So it isn't the client, and it isn't "old CLI version" — the only thing that changes behaviour is the route. Pointing a client at the latest CLI build can't fix a request that never goes through the CLI.

    4. Two things that would settle the location

    • Is production actually serving 0e472e2? origin/main isn't checkable from outside. The public repo has 22 entries total — issue templates, logos, stickers, readme.md, and no code at all (verified via the git tree API), and the SHA returns 422 No commit found there. So "nothing to fix in this repo" is trivially true and can't say anything about the gateway; 0e472e2 must live in an internal repo. If prod is a build from before that commit, the observed behaviour is exactly what a deployment lag would look like.
    • If the field does leave your edge intact for these slugs, then it's the wire slug's provider — worth naming it and opening it there. Note ZDR on the same slug forwards correctly, so route selection is on your side, and v4-flash is broken on both routes (a second, independent path).
    • Either way, the cheapest decisive artefact is a redacted capture of the outbound body for one of the affected slugs on the default route, plus the per-provider entry it maps to. That single pair separates "our edge" from "the upstream adapter" — and it's the one thing I can't observe from outside. If the drop is in the upstream adapter, "maps them per-provider" is still the construct that selects it, so the entry is worth reading first.

    5. "v4-pro recalls ZQ7K2M through the identical code path, so we're not the one dropping it"

    Same code path, different per-provider entry — and that entry is the variable. These are not the same upstream: identical tool-loop bodies tokenize to different prompts per route (v4-pro 421 default / 343 ZDR; v4.1-flash 352), and v4-flash-fast returns a visibly different envelope (bare hex id, no system_fingerprint, extra metadata). v4-pro working on its own entry doesn't tell us the deepseek/deepseek-v4.1-flash wire-slug entry is fine.

    6. Possible scope mismatch worth checking on your side

    If 0e472e2 is a CLI-side fix — "declares all three fields and maps them per-provider" reads like client request-schema work — then it doesn't cover this report. The repro is a third-party client sending DeepSeek's native field name straight to /provider/v1; no CLI is involved, and the drop is visible in your own response's usage.prompt_tokens. If the CLI's request schema now declares the fields but the gateway's inbound path for the same body still drops them by name, both can be true at once.

    Current ask

    1. Run the paired token test above against production on the three slugs and post the deltas.
    2. Confirm which build serves /provider/v1 today (does it contain 0e472e2?).
    3. If the field leaves your edge, name the upstream wire slug provider and hand it over — with route selection in your hands, either route around it or document the per-slug limitation.

    Until then, the practical workaround for clients is unchanged: ZDR header on the affected slugs, or v4-pro / v4-flash-fast (which don't have native vision).

    Probes are plain httpx scripts (600 KB oracle + D−C discriminator) — happy to paste them here if useful.


    Two accuracy notes, since they cut against the report as much as for it:

    • DeepSeek's guide says a tool-carrying request that fails to pass the field back returns 400. In a direct check just now, api.deepseek.com returned 200 for this shape without reasoning_content (deepseek-flash and deepseek-v4-pro). So the documented 400 isn't universally enforced — the reason this matters is the functional one: with tools present the field is concatenated into context, which is what ZDR demonstrably restores and the affected routes demonstrably lose.
    • The marker test is directional, not deterministic — prompt_tokens is the primary signal, which is why the 600 KB oracle above is quoted instead of the recall test.
  5. cyx-slps commented on Sep 11, 2026

    @cyx-slps

    Duplicate report — same root cause as #754 and #829; still reproducible by multiple independent users

    This issue is the same defect as:

    All three describe one thing: echoed historical reasoning (reasoning_content / reasoning on an assistant message) never reaches the upstream model, so in a tool loop the model cannot continue from its own earlier chain of thought and re-derives or fabricates instead. The reports differ only in observation surface — #754 / #829 measure it behaviourally (0/N exact recall), this issue measures the same drop in usage.prompt_tokens (+0). The route-dependence finding here (ZDR forwards the field, default route does not) is new detail on that one root cause, not a separate bug.

    Reporters still reproducing it after the 9/7 "fixed"

    Reporter Date Surface / models Result
    @DDwsgood (#754) 2026-09-03 /provider/v1 + /alpha/generate, DeepSeek and GLM-5.2 not forwarded; localized to the final serialization step
    @Preacher7306 (#754) 2026-09-06 same behaviour —
    @j1232324 (#754) 2026-09-10 /provider/v1 — v4.1-flash (reasoning_content ×3, reasoning ×2), v4-flash ×2 0/7
    @yyxdlls (#829) 2026-09-10 /provider/v1 — v4.1-flash 0/5, v4-flash 0/4 (plus reasoning / content-parts variants, all 0/4) 0/9
    @cyx-slps (#754, #829) 2026-09-11 /alpha/generate clean probe 0/3; /provider/v1 clean self-generated marker 0/2 (v4-flash), 0/3 (v4.1-flash) 0/8
    @AkaneRanma (this issue) 2026-09-11 /provider/v1 — 600 KB block on the 3 affected slugs +0 tokens
    control 2026-09-10/11 api.deepseek.com, byte-identical body, v4-flash 3/3 PASS

    Six independent reporters, four different methods (behavioural recall, clean self-generated marker, token accounting, per-field-name variants), on both /alpha/generate and /provider/v1, across 4+ model slugs — all fail, while the DeepSeek official control with the identical request body passes every round.

    Timeline that matters

    A closed issue plus an internal-only commit SHA cannot be checked from outside (the public repo carries no code and that SHA returns 422 No commit found), so "fixed" currently has no observable artefact attached to it.

    Requests

    1. Treat [Bug][Critical] Reasoning echo-back silently dropped across ALL thinking models (DeepSeek/GLM/Kimi/MiMo…) — multi-turn agents hallucinate fake memories & suffer severe quality degradation #754 + OpenAI-compatible v1 endpoint still drops echoed assistant reasoning_content on tool-call continuation turns (#754 not fixed) #829 + Provider API: historical reasoning_content is dropped by name for deepseek-v4.1-flash / v4-flash / v4-flash-vision-exp #840 as one tracking item — reopen [Bug][Critical] Reasoning echo-back silently dropped across ALL thinking models (DeepSeek/GLM/Kimi/MiMo…) — multi-turn agents hallucinate fake memories & suffer severe quality degradation #754 or consolidate — so the status isn't split between a closed issue claiming fixed and two open issues reproducing it.
    2. The decisive artefact remains a redacted capture of the outbound body for one affected slug on the default route, plus the per-route/provider entry it maps to. ZDR forwards correctly on the same slug, so the drop happens inside the gateway, after route selection.

    Related but distinct (same per-slug provider-entry area, different symptoms): #807 (content-parts array dropped on tool-call turns) and #837 (v4.1-flash reasoning-effort metadata).

  6. ahmadbilaldev commented on Sep 11, 2026

    @ahmadbilaldev
    Contributor

    Taking a look

  7. pppopipupu commented on Sep 11, 2026

    @pppopipupu

    I found a temporary workaround: you can either manually modify the client to insert the contents of reasoning_content into content before sending the request, or enable the built-in requiresThinkingAsText flag in Pi agent.

  8. ahmadbilaldev commented on Sep 11, 2026

    @ahmadbilaldev
    Contributor

    Upstream issue, getting it fixed.

  9. ahmadbilaldev commented on Sep 11, 2026

    @ahmadbilaldev
    Contributor

    Should be fixed now. Thanks for reporting.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions