Skip to content

deepseek-flash reasoning still leaks into prose: the stream decoder gates on a literal "deepseek-v4" substring #6044

Description

@Hmbown

This reopens what 10aa19570 claimed to fix

That commit added catalog rows so model_supports_reasoning("deepseek-flash") returns true. Verified — it does. It was the wrong gate. Reasoning still lands in visible prose, because the stream decoder classifies reasoning routes through a different function that was never touched.

crates/tui/src/client/chat.rs:3232:

fn requires_reasoning_content(model: &str) -> bool {
    let lower = model.to_lowercase();
    // V4-family direct model IDs.
    lower.contains("deepseek-v4")
        || lower.starts_with("deepseek-chat")
        || lower.starts_with("deepseek-reasoner")
        || has_deepseek_r_series_marker(&lower)
}

deepseek-flash matches none of the four arms. It does not contain the literal deepseek-v4.

That result flows into is_reasoning_model_for_stream_on_route (:3391), which chooses ReasoningStreamStyle::SeparateField or ReasoningStreamStyle::None. With no match the style is None, so reasoning_content deltas are appended to answer text instead of becoming ContentBlock::Thinking.

Evidence from the founder's own session store

Every persisted session, counted by assistant content-block type:

model thinking text tool_use
deepseek-v4.1-flash-ex… (11 sessions) 13, 8, 16, 12, 25, 11, 58, 338, 45, 15, 28 3, 7, 2, 11, 22, 1, 2, 192, 2, 1, 1
GLM-5.3 27 16 34
deepseek-flash (2 sessions) 0 35 58
deepseek-flash 0 40 68

20 of 23 sessions carry thinking blocks. The only two that carry none are the two deepseek-flash sessions — and every working model id contains the literal substring deepseek-v4. On healthy routes the ratio inverts (58 thinking / 2 text); on deepseek-flash it is 0 thinking / 35 text, with individual text blocks up to 73,003 characters of visible internal monologue.

Second failure mode: 400s on tool-call turns

The comment directly above the function states the replay requirement:

both have thinking mode enabled by default, so any assistant message carrying tool_calls must replay reasoning_content on subsequent turns or the API returns 400.

deepseek-flash has thinking on by default and its sessions carry 58 and 68 tool_use blocks. Because the route is not classified as reasoning, nothing is captured to replay, so those turns are exposed to the same 400. This is not only a display defect.

Root pattern

This is the third hand-maintained table holding a capability fact for the same model, each missed in turn:

  1. model_supports_reasoning — catalog fallback pile (fixed in 10aa19570).
  2. route/capabilities.rs:96 — provider route table (deepseek-flash is the declared default but is not registered for the deepseek provider, so it fails to resolve #6043, still open).
  3. requires_reasoning_content — stream decoder substring test (this issue).

Fixing one does not fix the others, and each failure is silent. The 0.9.14 milestone names the durable answer: the catalog plus the cloud-facts channel owns capability, and these predicates read from it.

Fix shape

Immediate: make requires_reasoning_content recognise deepseek-flash. Prefer routing it through the catalog (model_supports_reasoning already returns true) over adding a fourth substring arm — a substring test on deepseek-v4 silently fails every future id that does not embed a version number, which is exactly how this happened.

Then add a test asserting that for each DeepSeek id the catalog marks reasoning-capable, reasoning_stream_style_for_route yields SeparateField and not None.

Verify by starting a fresh deepseek-flash session and confirming the assistant messages carry ContentBlock::Thinking, as reasoning_content_replayed_after_tool_call.rs does for deepseek-v4-pro.

Correction to the record

10aa19570's message asserts the leak was fixed. It was not; the fix addressed a gate that was not the one inlining the text. Session 5130a34d (2026-09-10) shows the leak with that commit installed.

Note on forensics: per-message created_at in the session journal is stamped at save time — all 103 entries in that session share 07:11:46.2346xx, incrementing by microseconds — so it cannot date individual messages. Worth fixing separately; it makes session files useless for sequencing.

Activity

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

    No labels
    No labels

    Projects

    • Status
      Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions