/** * Session usage for the /usage panel: the provider-reported token totals, the * context pressure and composition, and the exact per-turn accounting, folded * into the styled rows the panel draws. * * Every figure comes from the harness token meter. The four token buckets are * disjoint, so the prompt side is never double counted; the composition block * is a density estimate and is labelled as one. * * @module @deepseek-ai/dsh-tui/render/usage */ import type { SessionEvent } from '@deepseek-ai/dsh-session'; import type { TokenUsageProjection, TurnTokenUsage } from '@deepseek-ai/dsh-token-meter/client'; import { type StyledLine } from './lines.ts'; /** One completed turn's exact provider-reported accounting. */ export interface UsageTurn { /** Durable turn number (`turn/start`). */ readonly turn: number; readonly usage: TurnTokenUsage; /** * The model that billed this turn, or '' when nothing in the log names one. * The meter's own `routes` is preferred; it is absent whenever ONE attempt * went unattributed, so the turn's own assistant messages answer instead. */ readonly model: string; } /** * Everything the panel reads. The totals come from the mounted projection and * the per-turn rows from the meter's own fold; a missing projection renders as * explicitly unavailable rather than as zeros, because an unmounted deployment * and a session with no traffic are different facts. */ export interface UsageView { /** Provider-reported usage over the whole durable log. */ readonly totals?: TokenUsageProjection; /** Completed turns, oldest first. */ readonly turns: readonly UsageTurn[]; } /** Prompt-side tokens the provider billed: uncached input plus both cache buckets. */ export declare function billedInputTokens(totals: TokenUsageProjection): number; /** Prompt plus completion tokens over the whole log. */ export declare function usageTotalTokens(totals: TokenUsageProjection): number; /** * Cache-hit share of billed prompt-side input. * @param totals - cumulative provider-reported buckets. * @returns percent rounded to one decimal place, or null when nothing was billed. */ export declare function usageCacheHitPercent(totals: TokenUsageProjection): number | null; /** One complete turn's durable events, in log order. */ export interface TurnSlice { readonly turn: number; readonly events: readonly SessionEvent[]; } /** * Split a durable log into COMPLETE turns. A turn still running has no * `turn/end` yet, and an unfinished attempt has no exact accounting, so the * trailing slice is dropped rather than guessed at. * @param events - the whole durable log, in log order. * @returns one slice per `turn/start`…`turn/end` span, oldest first. */ export declare function completedTurns(events: readonly SessionEvent[]): readonly TurnSlice[]; /** * Exact usage for every completed turn that can be proven. * @param events - the whole durable log, in log order. * @param derive - the meter's per-turn fold, injected so this module stays pure. * @returns one row per provable turn, oldest first; turns whose attempts did * not all report usage are omitted rather than estimated, and so is a turn * that billed nothing at all — an empty row is noise in a usage table. */ export declare function turnUsages(events: readonly SessionEvent[], derive: (events: readonly SessionEvent[]) => TurnTokenUsage | undefined): readonly UsageTurn[]; /** A bucket some turn of a group did not report, leaving the group sum a floor. */ export type PartialBucket = 'cacheReadTokens' | 'cacheWriteTokens' | 'reasoningTokens'; /** One model's merged totals across every turn it billed. */ export interface ModelUsage { /** Model names joined with ` + ` (a turn that switched models lists both). */ readonly model: string; /** Turns attributed to this model. */ readonly turns: number; readonly uncachedInputTokens: number; readonly outputTokens: number; readonly totalTokens: number; /** Summed over the turns that reported it; absent when none did. */ readonly cacheReadTokens?: number; /** Summed over the turns that reported it; absent when none did. */ readonly cacheWriteTokens?: number; /** Summed over the turns that reported it; absent when none did. */ readonly reasoningTokens?: number; /** * Buckets some turn of the group left unreported. The corresponding sum (and * the hit share derived from it) counts only what WAS reported, so the * display marks it as a floor rather than hiding the group's known traffic. */ readonly partial: readonly PartialBucket[]; } /** * Merge the per-turn rows by the model that billed them, biggest spender * first. A turn lands in exactly one group — the group named by every model it * used — so the sums stay additive and a mid-turn model switch never counts * the same tokens twice. Each bucket is merged over the turns that reported * it, and {@link ModelUsage.partial} records the ones that stayed silent. * @param turns - provable per-turn rows, oldest first. * @returns one row per model group. */ export declare function modelTotals(turns: readonly UsageTurn[]): readonly ModelUsage[]; /** The model attribution for one turn: the meter's routes, else its messages. */ export declare function turnModel(slice: TurnSlice, usage: TurnTokenUsage): string; /** * Render the panel body. * @param view - the projection totals plus the derived per-turn rows. * @param columns - usable content columns inside the panel border. * @returns bounded, styled rows ready to draw. */ export declare function usageLines(view: UsageView, columns: number): readonly StyledLine[];