/** * The usage aggregator: turns the session event stream into per-session, * per-day, and per-month token/cost/carbon buckets plus per-model latency * windows. Pure state and pure accounting — the caller feeds events and the * aggregator owns no I/O, no timers, and no Cordis services, so every policy * path is unit-testable without a harness. * * @module dsh-budget/aggregate */ import type { TokenUsage } from '@deepseek-ai/dsh-llm'; import type { ResolvedConfig } from '../config.js'; /** One scope's token buckets (disjoint, mirroring TokenUsage). */ export interface TokenBuckets { inputTokens: number; outputTokens: number; cacheReadTokens: number; cacheWriteTokens: number; } /** One scope's aggregated usage. */ export interface ScopeUsage extends TokenBuckets { costUsd: number; carbonKg: number; } /** Empty usage record. */ export declare function emptyUsage(): ScopeUsage; /** One model's aggregated usage plus latency statistics. */ export interface ModelUsage extends ScopeUsage { provider: string; model: string; /** Latency samples kept for this model (ms). */ latencyMs: readonly number[]; /** `false` when the model is unpriced (cost contribution is 0, cost unknown). */ priced: boolean; } /** One persisted model usage entry (latency windows stay process-local, so they are excluded). */ export interface PersistedModelUsage { provider: string; model: string; inputTokens: number; outputTokens: number; cacheReadTokens: number; cacheWriteTokens: number; costUsd: number; carbonKg: number; /** `false` when the model was unpriced at record time (absent = priced). */ priced?: boolean | undefined; } /** One persisted day/month bucket. */ export interface PersistedBucket { total: ScopeUsage; models: Record; } /** The durable cross-restart subset of aggregator state (day + month buckets). */ export interface PersistedBudgetState { days: Record; months: Record; } /** One recorded threshold alert. */ export interface AlertRecord { scope: 'session' | 'daily' | 'monthly'; kind: 'warn' | 'over'; at: number; usedUsd: number; capUsd: number; } /** One day's aggregated usage in the per-day history. */ export interface DayUsage { /** UTC day key, `YYYY-MM-DD`. */ day: string; usage: ScopeUsage; } /** UTC day key, e.g. `2026-08-16`. */ export declare function dayKey(timestamp: number): string; /** UTC month key, e.g. `2026-08`. */ export declare function monthKey(timestamp: number): string; /** Everything the panel and the /budget command read (session-attributed via {@link BudgetAggregator.snapshotFor}). */ export interface BudgetSnapshot { session: ScopeUsage; today: ScopeUsage; thisMonth: ScopeUsage; models: readonly ModelUsage[]; /** Alerts in reverse chronological order. */ alerts: readonly AlertRecord[]; blockedScopes: readonly ('session' | 'daily' | 'monthly')[]; /** Per-day usage over the last `historyDays` days, oldest first. */ days: readonly DayUsage[]; } /** * The runtime aggregator. All methods are synchronous; the caller invokes * them from the session-event listener (the event hot path). */ export declare class BudgetAggregator { private readonly config; private readonly now; private readonly state; private readonly table; /** Provider/model pairs already reported as unpriced (warn-once dedupe). */ private readonly unpricedWarned; /** * @param config - resolved plugin config. * @param now - clock (defaults to Date.now). * @param onUnpriced - optional hook fired once per provider/model the first * time an unpriced model contributes usage (warn-once dedupe lives here * so tests can assert single calls without a logger). */ constructor(config: ResolvedConfig, now?: () => number, onUnpriced?: (provider: string, model: string) => void); private readonly onUnpriced; /** Update the current provider/model attribution from a request header. */ setAttribution(provider: string, model: string): void; /** The current provider/model attribution (latest request header). */ attribution(): { provider: string; model: string; }; /** * Record one usage payload against the current attribution. Unknown or * zero usage is a no-op. * * @param sessionId - owning session id (string form). * @param usage - the token usage payload. */ recordUsage(sessionId: string, usage: TokenUsage): void; /** Record one measured call duration for one model. */ recordLatency(model: string, durationMs: number): void; /** Merge one usage record into a per-model map. */ private addModel; /** Read one session's usage (empty when unknown). */ sessionUsage(sessionId: string): ScopeUsage; /** Read today's usage. */ todayUsage(): ScopeUsage; /** Read this month's usage. */ monthUsage(): ScopeUsage; /** * Per-day usage over the last `limit` days, oldest first, zero-filled for * days with no recorded usage. Days are UTC keys derived from the injected * clock, so a fixed test clock yields a stable series. * * @param limit - number of trailing days to return. */ dayHistory(limit: number): DayUsage[]; /** Per-model usage (today) with latency percentiles, sorted by cost. */ modelUsage(): ModelUsage[]; /** Record one threshold alert (bounded to the most recent 50). */ recordAlert(alert: AlertRecord): void; /** Milliseconds since the last alert of the same scope+kind (Infinity when none). */ lastAlertAge(scope: 'session' | 'daily' | 'monthly', kind: 'warn' | 'over', at: number): number; /** The current blocked scopes (budget crosses that the user has not lifted). */ get blockedScopes(): readonly ('session' | 'daily' | 'monthly')[]; /** Mark one scope blocked (over-limit with block/degrade policy). */ blockScope(scope: 'session' | 'daily' | 'monthly'): void; /** Lift a block (user confirmation through /budget unblock or the panel). */ unblockScope(scope: 'session' | 'daily' | 'monthly'): void; /** * The per-scope snapshot for one session (the panel and the command * attribute usage to the session that owns the view). * * @param sessionId - owning session id. * @returns the session-attributed snapshot. */ snapshotFor(sessionId: string): BudgetSnapshot; /** Serialize the day/month buckets (the durable cross-restart subset). */ exportState(): PersistedBudgetState; /** Merge a persisted state back into the live day/month buckets (restore on mount). */ restoreState(state: PersistedBudgetState): void; /** Serialize one day/month bucket map into plain JSON. */ private serializeBuckets; /** Merge one persisted bucket map into a live bucket map (accumulating, not replacing). */ private restoreBuckets; /** Add one scope usage into another (accumulate every field). */ private addToScope; } //# sourceMappingURL=usage.d.ts.map