DeepSeek Harness tok/s reads 3-5x high on decodeMs

TroubleshootingPublished 2026-10-20Author: DeepSeek Plugin Market
DeepSeek HarnessDSHsessionStatstok/sdecodeMsreasoning
DeepSeek Harness tok/s runs 3-5x high, and tool-only steps can hit 30x, because decodeMs starts at the first token while outputTokens includes reasoning.

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.

  1. Open the failing session log and locate assistant/message.stream. Expected: content block events are present.
  2. Count reasoning-chunks. Expected: zero, which means reasoning was not streamed incrementally.
  3. Find chunk.block-start and check that blockType is reasoning. Expected: it exists and is followed by a whole-block block-end.
  4. Divide the same step's outputTokens by the full step duration. Expected: the display is 3-5x the end-to-end figure.
  5. Check a tool-only step on its own. Expected: the multiplier is worse, up to 30x.
Provider shapereasoning-deltaAnchor lands ontok/s
Incremental reasoningpresentreasoning startclose to real
Block reasoningabsentvisible text or tool call3-5x high
Tool-only stepabsenttool argumentsup 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.

  1. The projection's assistant/message branch takes open.firstTokenTime, falling back to assistantStreamFirstTokenTime. Expected: both paths look for a delta.
  2. That function walks the stream records and returns a time only where isTokenDelta is true. Expected: everything else is skipped.
  3. isTokenDelta accepts non-empty text-delta, reasoning-delta, and tool-call-delta. Expected: block-start never counts.
  4. A block-reasoning provider has no reasoning-delta, so the anchor moves later. Expected: it lands on the first text or tool-argument block.
  5. decodeMs starts 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.

  1. Add a stream reader that finds the first block-start and returns its time. Expected: it works for block and incremental streams.
  2. In the message branch, read both tokenAnchor and blockStart, combine them with Math.min, and handle undefined. Expected: normal streams are unchanged.
  3. Use decodeStart for decodeMs, and keep tokenAnchor for ttftMs and ttftSteps. Expected: TTFT stays the same and tok/s falls.
  4. Change all three points: host projection, client turn-metrics mirror, and the stats bar. Expected: UI and logs agree.
  5. Recompute a set of historical sessions. Expected: values approach end-to-end throughput.
ts
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.

  1. When a step opens, record the first reasoning block's start time and whether any reasoning-delta appeared. Expected: the two provider styles become distinguishable.
  2. Include block-start only when the step has a reasoning block but no reasoning-delta. Expected: incrementally streamed sessions keep their numbers.
  3. Do not mirror this anchor into the assistant/attempt branch. Expected: retries still use the first attempt's first token.
  4. 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.
  5. 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.

  1. Detect whether the current step has a reasoning block but no reasoning deltas. Expected: suspect steps are flagged.
  2. For those steps, use whole-step duration as the denominator and label the metric. Expected: the number matches a manual calculation.
  3. Or collapse the value to "—" while keeping the token total. Expected: no misleading high value.
  4. Say "estimate" in the tooltip. Expected: readers know it is not an exact decode rate.
  5. 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.

  1. Recompute a batch of historical sessions, not one new session. Expected: the block and incremental groups converge.
  2. Confirm numerator and denominator cover the same step set. Expected: no half-counted steps remain.
  3. Compare TTFT before and after. Expected: identical.
  4. Look at one tool-only step. Expected: the multiplier drops from 30x to single digits.
  5. Confirm all three implementation points took effect. Expected: logs, fallback path, and stats bar show the same value.
LayerRoleEffect if missed
Host projectionprimary source for logs and statslog values stay inflated
Client turn-metricsfallback when the projection is unavailableoffline or old sessions disagree
Stats barsecond speed entry pointtwo 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

Is the DeepSeek Harness tok/s several times higher than output tokens divided by elapsed time a bug?

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.

Why only some models or gateways show inflated DeepSeek Harness tok/s?

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.

Do I fix inflated DeepSeek Harness tok/s in one place, or on both host and client?

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.

Is subtracting reasoningTokens from outputTokens a valid shortcut for DeepSeek Harness tok/s?

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