Files
OmniRoute/src/lib/quota/burnRate.ts
Paijo 1c8f3bee97 fix(quota): resolve poolUsage dead code, burn rate, saturation signals, webhooks, and embeddings enforcement (#3280)
Integrated into release/v3.8.12. Quota Sharing Engine fixes: poolUsageWithDimensions promoted to the QuotaStore interface, single-snapshot burn rate, zero-weight normalization, Anthropic saturation signals, quota.exceeded webhook on block, and embeddings enforcement. Validated: 10/10 PR tests + 34 quota/embedding regression files green, typecheck + lint clean. Dropped the committed .omo/ agent-tooling artifacts.
2026-06-06 03:41:03 -03:00

112 lines
3.6 KiB
TypeScript

/**
* burnRate.ts — Burn-rate EMA estimator for quota consumption.
*
* Computes an exponential moving average (alpha=0.3) over a series of
* (timestamp, consumed) samples and projects time to exhaustion.
*
* Part of: Group B — Quota Sharing Engine (plan 22, frente F6).
*/
const EMA_ALPHA = 0.3;
export interface BurnRateSample {
ts: number; // epoch ms
consumed: number; // cumulative consumed value at this ts
}
export interface BurnRateResult {
/** Estimated tokens (or units) consumed per second. */
tokensPerSecond: number;
/**
* Estimated milliseconds until the remaining quota is exhausted.
* null if rate is 0 or the caller did not provide a remaining value.
*/
timeToExhaustionMs: number | null;
}
/**
* Compute burn rate from a single snapshot using the sliding window context.
*
* When only one sample is available (the common case for on-demand pool usage
* queries), we derive the rate from the consumption within the current window:
* rate = consumedTotal / elapsedInWindow
*
* This assumes consumption is roughly uniform within the window — a reasonable
* approximation for token/request budgets over hourly/daily/weekly periods.
*
* @param consumedTotal Cumulative consumption in the current sliding window.
* @param windowMs The window duration in milliseconds (e.g. 5h = 18_000_000).
* @param remaining Optional remaining quota (same unit as consumedTotal).
*/
export function computeBurnRateFromWindow(
consumedTotal: number,
windowMs: number,
remaining?: number
): BurnRateResult {
if (consumedTotal <= 0 || windowMs <= 0) {
return { tokensPerSecond: 0, timeToExhaustionMs: null };
}
const nowMs = Date.now();
const currentBucketIndex = Math.floor(nowMs / windowMs);
const windowStartMs = currentBucketIndex * windowMs;
const elapsedMs = Math.max(1, nowMs - windowStartMs); // avoid division by zero
const safeRate = consumedTotal / (elapsedMs / 1000); // per second
const timeToExhaustionMs =
safeRate > 0 && remaining !== undefined && remaining >= 0
? (remaining / safeRate) * 1000
: null;
return { tokensPerSecond: safeRate, timeToExhaustionMs };
}
/**
* Compute the current burn rate from a series of samples.
*
* @param history Array of { ts, consumed } ordered oldest → newest.
* Needs at least 2 entries; fewer returns zeros.
* @param remaining Optional remaining quota (same unit as consumed).
* When provided, `timeToExhaustionMs` is calculated.
*/
export function computeBurnRate(
history: BurnRateSample[],
remaining?: number
): BurnRateResult {
if (history.length < 2) {
return { tokensPerSecond: 0, timeToExhaustionMs: null };
}
// Build EMA over consecutive deltas.
let emaRate = 0;
let initialized = false;
for (let i = 1; i < history.length; i++) {
const deltaConsumed = history[i].consumed - history[i - 1].consumed;
const deltaTs = history[i].ts - history[i - 1].ts; // ms
if (deltaTs <= 0) continue; // skip duplicate or out-of-order timestamps
const instantRate = deltaConsumed / (deltaTs / 1000); // per second
if (!initialized) {
emaRate = instantRate;
initialized = true;
} else {
emaRate = EMA_ALPHA * instantRate + (1 - EMA_ALPHA) * emaRate;
}
}
if (!initialized) {
return { tokensPerSecond: 0, timeToExhaustionMs: null };
}
const safeRate = Math.max(0, emaRate);
const timeToExhaustionMs =
safeRate > 0 && remaining !== undefined && remaining >= 0
? (remaining / safeRate) * 1000
: null;
return { tokensPerSecond: safeRate, timeToExhaustionMs };
}