/** * Live decode throughput for the status row. * * The harness never publishes tokens per second. What it does publish, per * step, is authoritative: `assistant/message` carries the step's output-token * count and the packed stream's first-token time, so `decodeTokens / decodeMs` * over settled steps is a real measurement. What it does *not* publish is any * per-token count while a step is still streaming — adapters emit text deltas * and only hand over `usage` as the stream closes (verified against * `dsh-llm-deepseek` and `dsh-llm-pi-ai`). * * So there are exactly two honest numbers, and this module keeps them apart: * * 1. **Settled** — tokens and milliseconds of a completed step. Exact, and the * only number that is ever comparable across steps or sessions. * 2. **Live** — characters per second while a step streams, converted with a * ratio calibrated from this session's own settled steps. It is an estimate, * it is labelled with `~`, and it exists so a long reply shows *something* * that moves instead of a number frozen at the previous step. * * There is deliberately no health grading here. Tokens per second is a property * of the model, the provider, the reasoning effort and the prompt, not a signal * that something is wrong: 20 tok/s from a reasoning model and 20 tok/s from a * small fast one mean different things. The row paints it neutral. * * @module dsh-ssh-tui/throughput */ /** How much of the recent past the live rate is measured over. */ export declare const THROUGHPUT_WINDOW_MS = 4000; /** Minimum span between the two samples a rate is taken from. */ export declare const THROUGHPUT_MIN_SPAN_MS = 400; /** A live rate older than this describes a stream that stopped, not one running. */ export declare const THROUGHPUT_FRESH_MS = 2500; /** * Tokens per character, for the two ends of the script spectrum. * * Latin text averages roughly a third of a token per character; CJK is close to * one, because a Chinese character is usually its own token and most words are one * or two characters. Between those ends the estimate is interpolated by the * *measured* script mix of the stream being estimated, continuously: a threshold * ("30% CJK means Chinese") would put a mostly-English reply with one Chinese * paragraph on the wrong side of a cliff, and the number it produces is shown to * the reader. */ export declare const DEFAULT_TOKENS_PER_CHAR_LATIN = 0.32; export declare const DEFAULT_TOKENS_PER_CHAR_CJK = 0.85; /** The Latin end under its original name: the fallback for a stream with no text. */ export declare const DEFAULT_TOKENS_PER_CHAR = 0.32; /** What the status row paints, and what `/status` reports. */ export interface ThroughputView { /** Exact tokens per second of the most recent settled step, when it had usage. */ settledRate?: number; /** Characters the live estimate was built from, for the calibration report. */ liveChars: number; /** Estimated tokens per second over the streaming window. */ liveRate?: number; /** True when `liveRate` was measured recently enough to call it current. */ fresh: boolean; } /** * Characters to tokens, bounded and finite. * * The default is an average over mixed prose and code; every settled step * replaces half of it with this session's own measurement, which is what makes * the live estimate usable on CJK output (where one character is close to one * token) without hardcoding a language. */ export declare function boundTokensPerChar(value: number, fallback?: number): number; /** * The median of up to the last three rates, or the newest one alone. * * A single frame in which the terminal coalesced a burst of tokens into one * delta reads as a spike; a median of three ignores it, while a real change * survives because two agreeing samples decide it. Same rule, and the same * reasoning, as the link chip's RTT median. */ export declare function medianRate(values: readonly number[]): number | undefined; /** * Tracks one step's live decode rate from character deltas. * * Sampling is the caller's job (`note` per fragment): the window's own * minimum span is what keeps the number stable, so a burst of tiny deltas and * one coalesced delta produce the same answer. */ /** * Whether one code point is written in a script whose characters are close to one * token each: CJK ideographs, kana, Hangul syllables, and the fullwidth or CJK * punctuation that travels with them. * * Deliberately a property of the *text* rather than of the reader's locale: a * Chinese reader's English reply is Latin text and costs Latin tokens per * character, and `~tok/s` has to describe the tokens, not the person. */ export declare function isCjkCodePoint(codePoint: number): boolean; /** * The starting ratio for a stream of a given script mix: `cjkShare` of the * characters read as CJK, the rest as Latin. Continuous by construction, so a * reply that is 70% English and 30% Chinese lands 30% of the way up the range * instead of being classified as one or the other. */ export declare function scriptTokensPerChar(cjkShare: number): number; export declare class ThroughputTracker { /** The session's own ratio, once a step has settled. */ private calibrated; /** Characters and CJK characters seen in the step being estimated. */ private observedChars; private observedCjk; private samples; private chars; private baseAt; private startedAt; private lastDeltaAt; private lastStepChars; /** Rates already computed this step, kept for the median. */ private recentRates; private lastRate; private settledRate; /** * The ratio the estimate is using right now: this session's own measurement once * a step has settled, and the script mix of the stream being estimated before * that. `/status` and the tests read it. */ tokensPerChar(): number; /** Share of this step's characters that were CJK, 0 when none have arrived. */ cjkShare(): number; /** Whether the ratio is a settled measurement rather than a script estimate. */ calibratedRatio(): boolean; /** Drop a finished step's stream state, keeping calibration and last settle. */ reset(): void; /** * One observed fragment of model output. * * The fragment itself, not a count: the estimate is tokens per character, and * how many tokens a character is worth depends on which characters they are, so * the script mix has to be measured here rather than assumed once for the * session. Code points rather than UTF-16 units, so an emoji or a rare ideograph * outside the BMP counts as the one character the reader sees. * @param text - the fragment; empty is ignored. * @param time - wall clock the fragment arrived at, for tests and replay. */ note(text: string, time?: number): void; /** * Settle a step: fold its exact decode rate into the calibration and into the * number an idle row shows. * @param outputTokens - the step's authoritative output tokens. * @param decodeMs - milliseconds from first token to the settled message. */ settle(outputTokens: number, decodeMs: number): void; /** What the status row paints, and what `/status` reports. */ view(now?: number): ThroughputView; /** Drop samples that fell out of the window, and keep the newest few. */ private trim; /** Recompute the window's rate and fold it into the median buffer. */ private sampleRate; } /** * A rate as the row paints it: whole numbers, `<1` rather than nothing. * * A slow step that decoded 3 tokens over 30 seconds is still decoding; rounding * it to `0` reads as a stalled model. Above 1000 the number is compacted, since * four digits is cells the quota and context meters want more. */ export declare function formatRate(tokensPerSecond: number): string;