DeepSeek Harness tok/s reads 3-5x high on decodeMs
The tok/s in the bottom stats bar is decodeTokens / decodeMs: the numerator is provider-reported outputTokens, including reasoning tokens, and the denominator runs from assembly completion minus the first token. That first-token anchor is the flaw, because DeepSeek Harness (DSH) counts only non-empty text, reasoning, and tool-argument deltas, and a provider that sends reasoning in one block produces no reasoning-delta. The anchor slides to the first visible text or tool call, the denominator shrinks, and the numerator does not (source).
Confirm the inflation from DeepSeek Harness logs
The fingerprint is zero reasoning-chunks, only reasoning block-start and block-end, while the stats bar still divides reasoning-inclusive outputTokens by the shorter interval.
- Open the failing session log and locate
assistant/message.stream. Expected: content block events are present. - Count
reasoning-chunks. Expected: zero, which means reasoning was not streamed incrementally. - Find
chunk.block-startand check thatblockTypeisreasoning. Expected: it exists and is followed by a whole-blockblock-end. - Divide the same step's
outputTokensby the full step duration. Expected: the display is 3-5x the end-to-end figure. - Check a tool-only step on its own. Expected: the multiplier is worse, up to 30x.
| Provider shape | reasoning-delta | Anchor lands on | tok/s |
|---|---|---|---|
| Incremental reasoning | present | reasoning start | close to real |
| Block reasoning | absent | visible text or tool call | 3-5x high |
| Tool-only step | absent | tool arguments | up to 30x |
The reporter recomputed 47 sessions offline; grouped by provider and API, the block-reasoning groups showed the largest inflation (#9899).
Why the DeepSeek Harness denominator anchor slides
The "first token" anchor assumes content arrives as deltas, a premise that is not part of the provider contract.
- The projection's
assistant/messagebranch takesopen.firstTokenTime, falling back toassistantStreamFirstTokenTime. Expected: both paths look for a delta. - That function walks the stream records and returns a time only where
isTokenDeltais true. Expected: everything else is skipped. isTokenDeltaaccepts non-emptytext-delta,reasoning-delta, andtool-call-delta. Expected:block-startnever counts.- A block-reasoning provider has no
reasoning-delta, so the anchor moves later. Expected: it lands on the first text or tool-argument block. decodeMsstarts accumulating there. Expected: the denominator is far too small.
There is a second error in the other direction: a step with no text delta and no tool-argument delta gets no anchor at all, so its outputTokens never enters the numerator. A smaller denominator and a missing numerator coexist, which totals cannot distinguish. Split the numbers by step.
Path 1: take the earlier anchor with Math.min in DeepSeek Harness
The smallest change is to start decoding at whichever comes first, the first token or the first block-start, while TTFT keeps using the token anchor.
- Add a stream reader that finds the first
block-startand returns its time. Expected: it works for block and incremental streams. - In the message branch, read both
tokenAnchorandblockStart, combine them withMath.min, and handleundefined. Expected: normal streams are unchanged. - Use
decodeStartfordecodeMs, and keeptokenAnchorforttftMsandttftSteps. Expected: TTFT stays the same and tok/s falls. - Change all three points: host projection, client
turn-metricsmirror, and the stats bar. Expected: UI and logs agree. - Recompute a set of historical sessions. Expected: values approach end-to-end throughput.
const tokenAnchor = open.firstTokenTime ?? assistantStreamFirstTokenTime(event.data.stream) ?? null
const blockStart = assistantStreamFirstBlockStartTime(event.data.stream)
const decodeStart = tokenAnchor !== null && blockStart !== undefined
? Math.min(blockStart, tokenAnchor)
: tokenAnchor
Why not use the whole step duration instead: step duration includes queueing, retries, and tool waits, so swapping it in turns over-reporting into under-reporting. If you want end-to-end numbers, sum numerator and denominator over the same step set and label the metric.
Path 2: choose the anchor by block type in DeepSeek Harness
Instead of a global reader, only block-reasoning steps treat the reasoning block-start as the start; ordinary steps keep the old logic.
- When a step opens, record the first reasoning block's start time and whether any
reasoning-deltaappeared. Expected: the two provider styles become distinguishable. - Include block-start only when the step has a reasoning block but no
reasoning-delta. Expected: incrementally streamed sessions keep their numbers. - Do not mirror this anchor into the
assistant/attemptbranch. Expected: retries still use the first attempt's first token. - Add a retry test where the second attempt's block-start is later than the first attempt's token. Expected: the first token still wins.
- Compare sessions from two providers. Expected: both land in a plausible range.
The semantics are narrower and the regression surface smaller. The cost is one more branch in the projection, which a reader must understand before touching the anchor.
Path 3: stop showing an untrustworthy DeepSeek Harness number
If you are a user and cannot change the host, the honest option is to stop presenting the figure as precise.
- Detect whether the current step has a reasoning block but no reasoning deltas. Expected: suspect steps are flagged.
- For those steps, use whole-step duration as the denominator and label the metric. Expected: the number matches a manual calculation.
- Or collapse the value to "—" while keeping the token total. Expected: no misleading high value.
- Say "estimate" in the tooltip. Expected: readers know it is not an exact decode rate.
- Keep the original fields when a DSH plugin replaces the display. Expected: other consumers are not broken.
Do not use outputTokens - reasoningTokens as a general fix: reasoningTokens is optional and only some providers report it, so mixing the two breaks cross-provider comparability.
Verify and set bounds across the DeepSeek Harness layers
Check three things together: tok/s falls to end-to-end scale, TTFT is unchanged, and tool-only steps no longer show extreme multipliers.
- Recompute a batch of historical sessions, not one new session. Expected: the block and incremental groups converge.
- Confirm numerator and denominator cover the same step set. Expected: no half-counted steps remain.
- Compare TTFT before and after. Expected: identical.
- Look at one tool-only step. Expected: the multiplier drops from 30x to single digits.
- Confirm all three implementation points took effect. Expected: logs, fallback path, and stats bar show the same value.
| Layer | Role | Effect if missed |
|---|---|---|
| Host projection | primary source for logs and stats | log values stay inflated |
| Client turn-metrics | fallback when the projection is unavailable | offline or old sessions disagree |
| Stats bar | second speed entry point | two different tok/s on one screen |
Three limits apply: update the metric documentation to state the premise behind "first token", do not declare success from one new session, and no official fixed version appears in the discussion. For the related status bar display issue, see status bar shows only tok/s.
FAQ
It is a definition flaw in DeepSeek Harness, not your arithmetic. The numerator includes reasoning tokens while decodeMs starts at the first token, so a block-reasoning provider slides the anchor forward and shrinks the denominator.
The DeepSeek Harness defect assumes reasoning streams incrementally, which the provider contract does not require. Providers that send it as one block emit block-start and block-end but no reasoning-delta, so the anchor moves.
The DeepSeek Harness rule appears in three places: the host projection, the client turn-metrics mirror, and the stats bar. Changing only the host leaves the fallback path showing the inflated value, so all three need the same edit.
DeepSeek Harness reasoningTokens is optional, so subtracting it is not a general fix. Only some providers report it, and mixing the two breaks cross-provider comparability; fixing the denominator anchor is the stable route.
Related Terms
- decodeMs
- decodeMs is the decoding duration in DeepSeek Harness session statistics, accumulated as assembly completion time minus first-token time. It is the denominator of tok/s, so where it starts defines the metric.— https://github.com/deepseek-ai/deepseek-harness/discussions/9899
- isTokenDelta
- isTokenDelta decides whether a stream fragment carries a token in DeepSeek Harness. It accepts non-empty text-delta, reasoning-delta, and tool-call-delta; block-start, block-end, usage, and finish do not count.— https://github.com/deepseek-ai/deepseek-harness/discussions/9899
- block-start
- block-start marks the beginning of a content block in a streaming response and carries a blockType such as reasoning or text. It holds no token, so it is not counted as the first token, even though generation has begun.— https://github.com/deepseek-ai/deepseek-harness/discussions/9899
Sources
- #9899 — The sessionStats TPS definition is flawed: the decodeMs start slides to visible text or a tool call· deepseek-ai (GitHub Discussions)
- DeepSeek Harness official repository· deepseek-ai (GitHub)