The footer showed invented percentages. It now shows what two harnesses actually report, or says it does not know. lumbridge-core gains an append-only per-profile UsageLedger and a projection that labels every derived value estimated, withholds a burn rate from a single sample, withholds a window fraction with no reported ceiling, withholds an exhaustion estimate that lands after the reset, and reports an expired window as rolled over rather than freezing its last percentage. A missing fact renders as missing, never as zero. (0012) lumbridge-harness is the impure side: processes, clocks, and untrusted wire text in, observations out. Three adapters: - Codex's account/rateLimits/read over the app-server's JSON-RPC stdio. The client cannot express a request outside a two-variant enum and answers every server-to-client request with -32601, so a harness asking Lumbridge for a credential is refused by construction. (0013) - Claude Code's session transcripts, as a byte-offset tail follower that reports nothing until the backlog is read to EOF — a partially-read backlog is indistinguishable from a burst of spend, and the first run against 20 MB reported forty-six billion tokens an hour. The parser models four counters, so the conversations in those files are not representable. (0014) - Claude Code's five-hour and seven-day subscription windows, via a bridge installed as its statusLine command. 0014 had claimed no such surface existed; it does, and the record is corrected in place rather than quietly edited. Lumbridge does not read the OAuth credential to call the account usage endpoint, which is what comparable tools do — AGENTS.md forbids it, and 0015 says so rather than leaving the gap unexplained. Also in here: a capability-check ordering fix in the workspace reducer, where the applied-request replay table was consulted before the capability check and so answered questions the caller had no right to ask; the GPUI spike wired to the live probes with per-harness gauges and provenance chips; and a launcher that matches its own window by PID, because GPUI sets WM_NAME but not _NET_WM_NAME and a title match never succeeded. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1372 lines
44 KiB
Rust
1372 lines
44 KiB
Rust
//! Append-only usage observations and the derived footer projection.
|
|
//!
|
|
//! Decision 0002 requires every usage value to carry its provenance, and the
|
|
//! product footer must answer five questions without inventing precision:
|
|
//! which profile is active, how much of the window is consumed, when the window
|
|
//! resets, what the burn rate is, and how trustworthy those numbers are.
|
|
//!
|
|
//! This module stores facts, derives views from them, and reports what it
|
|
//! cannot know. Nothing here reads a clock: callers supply timestamps so the
|
|
//! projection is deterministic and testable. All arithmetic is integer
|
|
//! arithmetic; fractions are carried as permille so a rendered percentage never
|
|
//! drifts from the stored fact.
|
|
//!
|
|
//! ```
|
|
//! use lumbridge_core::{
|
|
//! AccountProfile, FooterUsage, UsageLedger, UsageObservation, UsageProvenance, UsageUnit,
|
|
//! UsageWindow,
|
|
//! };
|
|
//!
|
|
//! let profile = AccountProfile::new("codex-plus", "Codex", "ChatGPT", "gpt-5.2", "work")?;
|
|
//! let window = UsageWindow::new(0, 18_000_000)?;
|
|
//! let mut ledger = UsageLedger::new();
|
|
//!
|
|
//! for (at_ms, consumed) in [(0, 0), (3_600_000, 620_000)] {
|
|
//! ledger.record(
|
|
//! UsageObservation::counted(
|
|
//! profile.id().clone(),
|
|
//! UsageUnit::Tokens,
|
|
//! consumed,
|
|
//! UsageProvenance::ProviderReported,
|
|
//! at_ms,
|
|
//! )?
|
|
//! .with_limit(1_000_000)?
|
|
//! .with_window(window)?,
|
|
//! )?;
|
|
//! }
|
|
//!
|
|
//! let projection = ledger.project(profile.id(), 3_600_000);
|
|
//! assert_eq!(projection.consumed_permille(), Some(620));
|
|
//! assert_eq!(projection.burn_per_hour(), Some(620_000));
|
|
//! let footer = FooterUsage::new(&profile, &projection);
|
|
//! assert_eq!(footer.window(), "62.0% used");
|
|
//! # Ok::<(), lumbridge_core::UsageError>(())
|
|
//! ```
|
|
|
|
use std::collections::{BTreeMap, VecDeque};
|
|
use std::fmt;
|
|
|
|
use thiserror::Error;
|
|
|
|
const MAX_IDENTITY_BYTES: usize = 256;
|
|
const DEFAULT_RETENTION_PER_PROFILE: usize = 64;
|
|
const MIN_RETENTION_PER_PROFILE: usize = 2;
|
|
const DEFAULT_STALENESS_BUDGET_MS: u64 = 300_000;
|
|
const MIN_BURN_SAMPLE_MS: u64 = 1_000;
|
|
const MILLIS_PER_HOUR: u128 = 3_600_000;
|
|
const MILLIS_PER_MINUTE: u64 = 60_000;
|
|
const MILLIS_PER_SECOND: u64 = 1_000;
|
|
const MINUTES_PER_HOUR: u64 = 60;
|
|
const HOURS_PER_DAY: u64 = 24;
|
|
const PERMILLE_SCALE: u128 = 1_000;
|
|
const MAX_PERMILLE: u64 = 1_000;
|
|
|
|
/// Failures produced while recording or shaping usage facts.
|
|
#[derive(Clone, Debug, Eq, Error, PartialEq)]
|
|
pub enum UsageError {
|
|
#[error("invalid {0}")]
|
|
InvalidIdentifier(&'static str),
|
|
#[error("a usage window must reset after it starts")]
|
|
EmptyWindow,
|
|
#[error("a usage limit must be greater than zero")]
|
|
ZeroLimit,
|
|
#[error("observation at {observed_at_ms} ms precedes the newest observation at {newest_ms} ms")]
|
|
OutOfOrderObservation { observed_at_ms: u64, newest_ms: u64 },
|
|
#[error("observation at {observed_at_ms} ms falls outside its declared window")]
|
|
ObservationOutsideWindow { observed_at_ms: u64 },
|
|
#[error("retention must keep at least two observations per profile")]
|
|
RetentionTooSmall,
|
|
#[error("an unavailable observation cannot carry a counted value")]
|
|
UnavailableWithValue,
|
|
}
|
|
|
|
/// Where a usage number came from.
|
|
///
|
|
/// The ordering is deliberate: variants are declared from most to least
|
|
/// authoritative so [`UsageProvenance::weakest`] can take the maximum.
|
|
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
|
pub enum UsageProvenance {
|
|
/// The provider's own account or quota API reported this value.
|
|
ProviderReported,
|
|
/// The upstream harness reported it through a documented surface.
|
|
HarnessReported,
|
|
/// Lumbridge counted it locally from traffic it owns.
|
|
LocallyMeasured,
|
|
/// Lumbridge derived it from other observations.
|
|
Estimated,
|
|
/// No usable value exists. This is a fact, not a zero.
|
|
Unavailable,
|
|
}
|
|
|
|
impl UsageProvenance {
|
|
/// The label kept in detail views and exports.
|
|
#[must_use]
|
|
pub const fn label(self) -> &'static str {
|
|
match self {
|
|
Self::ProviderReported => "provider-reported",
|
|
Self::HarnessReported => "harness-reported",
|
|
Self::LocallyMeasured => "locally measured",
|
|
Self::Estimated => "estimated",
|
|
Self::Unavailable => "unavailable",
|
|
}
|
|
}
|
|
|
|
/// The short label a compact surface such as the footer may substitute.
|
|
#[must_use]
|
|
pub const fn compact_label(self) -> &'static str {
|
|
match self {
|
|
Self::ProviderReported => "provider",
|
|
Self::HarnessReported => "harness",
|
|
Self::LocallyMeasured => "local",
|
|
Self::Estimated => "est",
|
|
Self::Unavailable => "none",
|
|
}
|
|
}
|
|
|
|
/// Whether an external party stated this value rather than Lumbridge.
|
|
#[must_use]
|
|
pub const fn is_reported(self) -> bool {
|
|
matches!(self, Self::ProviderReported | Self::HarnessReported)
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn is_available(self) -> bool {
|
|
!matches!(self, Self::Unavailable)
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn confidence(self) -> UsageConfidence {
|
|
match self {
|
|
Self::ProviderReported => UsageConfidence::Exact,
|
|
Self::HarnessReported | Self::LocallyMeasured => UsageConfidence::Approximate,
|
|
Self::Estimated => UsageConfidence::Projected,
|
|
Self::Unavailable => UsageConfidence::Unknown,
|
|
}
|
|
}
|
|
|
|
/// The less authoritative of two provenances.
|
|
///
|
|
/// A derived value is only as trustworthy as its weakest input.
|
|
#[must_use]
|
|
pub fn weakest(self, other: Self) -> Self {
|
|
self.max(other)
|
|
}
|
|
}
|
|
|
|
/// How much a displayed number can be trusted.
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub enum UsageConfidence {
|
|
Exact,
|
|
Approximate,
|
|
Projected,
|
|
Unknown,
|
|
}
|
|
|
|
impl UsageConfidence {
|
|
#[must_use]
|
|
pub const fn label(self) -> &'static str {
|
|
match self {
|
|
Self::Exact => "exact",
|
|
Self::Approximate => "approximate",
|
|
Self::Projected => "projected",
|
|
Self::Unknown => "unknown",
|
|
}
|
|
}
|
|
}
|
|
|
|
/// What a usage number counts.
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub enum UsageUnit {
|
|
Tokens,
|
|
Requests,
|
|
/// A window fraction the provider reports directly, in permille.
|
|
WindowPermille,
|
|
}
|
|
|
|
impl UsageUnit {
|
|
#[must_use]
|
|
pub const fn label(self) -> &'static str {
|
|
match self {
|
|
Self::Tokens => "tokens",
|
|
Self::Requests => "requests",
|
|
Self::WindowPermille => "window",
|
|
}
|
|
}
|
|
|
|
/// Renders an amount in this unit without inflating its precision.
|
|
#[must_use]
|
|
pub fn format_amount(self, amount: u64) -> String {
|
|
match self {
|
|
Self::Tokens => format_count(amount, "tokens"),
|
|
Self::Requests => format_count(amount, "requests"),
|
|
Self::WindowPermille => format_permille(amount),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn format_count(amount: u64, suffix: &str) -> String {
|
|
if amount >= 1_000_000 {
|
|
format!(
|
|
"{}.{}M {suffix}",
|
|
amount / 1_000_000,
|
|
(amount % 1_000_000) / 100_000
|
|
)
|
|
} else if amount >= 1_000 {
|
|
format!("{}.{}k {suffix}", amount / 1_000, (amount % 1_000) / 100)
|
|
} else {
|
|
format!("{amount} {suffix}")
|
|
}
|
|
}
|
|
|
|
fn format_permille(permille: u64) -> String {
|
|
format!("{}.{}%", permille / 10, permille % 10)
|
|
}
|
|
|
|
/// Renders a duration for a compact surface. Never rounds up to a false zero.
|
|
#[must_use]
|
|
pub fn format_duration_ms(duration_ms: u64) -> String {
|
|
if duration_ms < MILLIS_PER_MINUTE {
|
|
return format!("{}s", duration_ms / MILLIS_PER_SECOND);
|
|
}
|
|
let minutes = duration_ms / MILLIS_PER_MINUTE;
|
|
if minutes < MINUTES_PER_HOUR {
|
|
return format!("{minutes}m");
|
|
}
|
|
let hours = minutes / MINUTES_PER_HOUR;
|
|
if hours < HOURS_PER_DAY {
|
|
return format!("{hours}h {}m", minutes % MINUTES_PER_HOUR);
|
|
}
|
|
// Weekly and monthly quota windows are common, and "151h 30m" is not a
|
|
// duration anyone reads at a glance.
|
|
format!("{}d {}h", hours / HOURS_PER_DAY, hours % HOURS_PER_DAY)
|
|
}
|
|
|
|
fn validate_identity(value: &str, label: &'static str) -> Result<(), UsageError> {
|
|
if value.trim().is_empty()
|
|
|| value.len() > MAX_IDENTITY_BYTES
|
|
|| value.chars().any(char::is_control)
|
|
{
|
|
return Err(UsageError::InvalidIdentifier(label));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// A stable identifier for one harness/provider/account combination.
|
|
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
|
pub struct AccountProfileId(String);
|
|
|
|
impl AccountProfileId {
|
|
/// Creates a stable, non-empty account profile identifier.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns [`UsageError::InvalidIdentifier`] for an empty, oversized, or
|
|
/// control-bearing identifier.
|
|
pub fn new(value: impl Into<String>) -> Result<Self, UsageError> {
|
|
let value = value.into();
|
|
validate_identity(&value, "account profile ID")?;
|
|
Ok(Self(value))
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn as_str(&self) -> &str {
|
|
&self.0
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for AccountProfileId {
|
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
formatter.write_str(&self.0)
|
|
}
|
|
}
|
|
|
|
/// The active harness, provider, model, and account label shown in the footer.
|
|
///
|
|
/// These are display names only. Credentials stay in the operating-system
|
|
/// credential store and are never part of this type.
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
pub struct AccountProfile {
|
|
id: AccountProfileId,
|
|
harness: String,
|
|
provider: String,
|
|
model: String,
|
|
account: String,
|
|
}
|
|
|
|
impl AccountProfile {
|
|
/// Creates a footer-ready account profile.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns [`UsageError::InvalidIdentifier`] when any field is empty,
|
|
/// oversized, or contains control characters.
|
|
pub fn new(
|
|
id: impl Into<String>,
|
|
harness: impl Into<String>,
|
|
provider: impl Into<String>,
|
|
model: impl Into<String>,
|
|
account: impl Into<String>,
|
|
) -> Result<Self, UsageError> {
|
|
let harness = harness.into();
|
|
let provider = provider.into();
|
|
let model = model.into();
|
|
let account = account.into();
|
|
validate_identity(&harness, "harness name")?;
|
|
validate_identity(&provider, "provider name")?;
|
|
validate_identity(&model, "model name")?;
|
|
validate_identity(&account, "account label")?;
|
|
Ok(Self {
|
|
id: AccountProfileId::new(id)?,
|
|
harness,
|
|
provider,
|
|
model,
|
|
account,
|
|
})
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn id(&self) -> &AccountProfileId {
|
|
&self.id
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn harness(&self) -> &str {
|
|
&self.harness
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn provider(&self) -> &str {
|
|
&self.provider
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn model(&self) -> &str {
|
|
&self.model
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn account(&self) -> &str {
|
|
&self.account
|
|
}
|
|
|
|
/// The footer's first answer: which harness, provider, and model is active.
|
|
#[must_use]
|
|
pub fn identity_line(&self) -> String {
|
|
format!("{} · {} · {}", self.harness, self.provider, self.model)
|
|
}
|
|
}
|
|
|
|
/// A provider-declared usage window.
|
|
///
|
|
/// The start is optional because providers report these fields independently:
|
|
/// a reset time can arrive without a window length. Knowing when a window ends
|
|
/// is the useful half — it bounds the exhaustion forecast and identifies which
|
|
/// window an observation belongs to — so discarding it for want of a start
|
|
/// would throw away a fact the provider actually stated.
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub struct UsageWindow {
|
|
started_at_ms: Option<u64>,
|
|
resets_at_ms: u64,
|
|
}
|
|
|
|
impl UsageWindow {
|
|
/// Creates a window that resets strictly after it starts.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns [`UsageError::EmptyWindow`] when the reset is not after the start.
|
|
pub const fn new(started_at_ms: u64, resets_at_ms: u64) -> Result<Self, UsageError> {
|
|
if resets_at_ms <= started_at_ms {
|
|
return Err(UsageError::EmptyWindow);
|
|
}
|
|
Ok(Self {
|
|
started_at_ms: Some(started_at_ms),
|
|
resets_at_ms,
|
|
})
|
|
}
|
|
|
|
/// Creates a window whose reset is known and whose start is not.
|
|
///
|
|
/// Two of these compare equal when they reset at the same moment, which is
|
|
/// what lets a burn rate be scoped to one window without inventing a start.
|
|
#[must_use]
|
|
pub const fn until(resets_at_ms: u64) -> Self {
|
|
Self {
|
|
started_at_ms: None,
|
|
resets_at_ms,
|
|
}
|
|
}
|
|
|
|
/// When the window opened, if the provider said.
|
|
#[must_use]
|
|
pub const fn started_at_ms(self) -> Option<u64> {
|
|
self.started_at_ms
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn resets_at_ms(self) -> u64 {
|
|
self.resets_at_ms
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn contains(self, at_ms: u64) -> bool {
|
|
if at_ms >= self.resets_at_ms {
|
|
return false;
|
|
}
|
|
match self.started_at_ms {
|
|
Some(started_at_ms) => at_ms >= started_at_ms,
|
|
None => true,
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn has_expired(self, now_ms: u64) -> bool {
|
|
now_ms >= self.resets_at_ms
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn remaining_ms(self, now_ms: u64) -> u64 {
|
|
self.resets_at_ms.saturating_sub(now_ms)
|
|
}
|
|
}
|
|
|
|
/// One recorded usage fact. Observations are never edited, only appended.
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
pub struct UsageObservation {
|
|
profile: AccountProfileId,
|
|
unit: Option<UsageUnit>,
|
|
consumed: u64,
|
|
limit: Option<u64>,
|
|
window: Option<UsageWindow>,
|
|
provenance: UsageProvenance,
|
|
observed_at_ms: u64,
|
|
}
|
|
|
|
impl UsageObservation {
|
|
/// Records a counted usage value with its provenance.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns [`UsageError::UnavailableWithValue`] when the caller labels a
|
|
/// counted value as unavailable. Use [`UsageObservation::unavailable`] to
|
|
/// record a genuine gap.
|
|
pub fn counted(
|
|
profile: AccountProfileId,
|
|
unit: UsageUnit,
|
|
consumed: u64,
|
|
provenance: UsageProvenance,
|
|
observed_at_ms: u64,
|
|
) -> Result<Self, UsageError> {
|
|
if !provenance.is_available() {
|
|
return Err(UsageError::UnavailableWithValue);
|
|
}
|
|
Ok(Self {
|
|
profile,
|
|
unit: Some(unit),
|
|
consumed,
|
|
limit: None,
|
|
window: None,
|
|
provenance,
|
|
observed_at_ms,
|
|
})
|
|
}
|
|
|
|
/// Records that no usage value could be obtained at this moment.
|
|
///
|
|
/// This is deliberately a first-class observation. Recording it stops the
|
|
/// projection from presenting an older reading as current.
|
|
#[must_use]
|
|
pub const fn unavailable(profile: AccountProfileId, observed_at_ms: u64) -> Self {
|
|
Self {
|
|
profile,
|
|
unit: None,
|
|
consumed: 0,
|
|
limit: None,
|
|
window: None,
|
|
provenance: UsageProvenance::Unavailable,
|
|
observed_at_ms,
|
|
}
|
|
}
|
|
|
|
/// Attaches the quota ceiling this observation was measured against.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns [`UsageError::ZeroLimit`] for a zero limit and
|
|
/// [`UsageError::UnavailableWithValue`] for an unavailable observation.
|
|
pub fn with_limit(mut self, limit: u64) -> Result<Self, UsageError> {
|
|
if !self.provenance.is_available() {
|
|
return Err(UsageError::UnavailableWithValue);
|
|
}
|
|
if limit == 0 {
|
|
return Err(UsageError::ZeroLimit);
|
|
}
|
|
self.limit = Some(limit);
|
|
Ok(self)
|
|
}
|
|
|
|
/// Attaches the provider window this observation belongs to.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns [`UsageError::ObservationOutsideWindow`] when the observation
|
|
/// time is not inside the window, and [`UsageError::UnavailableWithValue`]
|
|
/// for an unavailable observation.
|
|
pub fn with_window(mut self, window: UsageWindow) -> Result<Self, UsageError> {
|
|
if !self.provenance.is_available() {
|
|
return Err(UsageError::UnavailableWithValue);
|
|
}
|
|
if !window.contains(self.observed_at_ms) {
|
|
return Err(UsageError::ObservationOutsideWindow {
|
|
observed_at_ms: self.observed_at_ms,
|
|
});
|
|
}
|
|
self.window = Some(window);
|
|
Ok(self)
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn profile(&self) -> &AccountProfileId {
|
|
&self.profile
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn unit(&self) -> Option<UsageUnit> {
|
|
self.unit
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn consumed(&self) -> u64 {
|
|
self.consumed
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn limit(&self) -> Option<u64> {
|
|
self.limit
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn window(&self) -> Option<UsageWindow> {
|
|
self.window
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn provenance(&self) -> UsageProvenance {
|
|
self.provenance
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn observed_at_ms(&self) -> u64 {
|
|
self.observed_at_ms
|
|
}
|
|
|
|
/// Units left before the ceiling, when a ceiling is known.
|
|
#[must_use]
|
|
pub fn remaining(&self) -> Option<u64> {
|
|
self.limit.map(|limit| limit.saturating_sub(self.consumed))
|
|
}
|
|
}
|
|
|
|
/// A bounded, append-only observation stream per account profile.
|
|
#[derive(Clone, Debug)]
|
|
pub struct UsageLedger {
|
|
retention_per_profile: usize,
|
|
staleness_budget_ms: u64,
|
|
observations: BTreeMap<AccountProfileId, VecDeque<UsageObservation>>,
|
|
}
|
|
|
|
impl Default for UsageLedger {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl UsageLedger {
|
|
#[must_use]
|
|
pub fn new() -> Self {
|
|
Self {
|
|
retention_per_profile: DEFAULT_RETENTION_PER_PROFILE,
|
|
staleness_budget_ms: DEFAULT_STALENESS_BUDGET_MS,
|
|
observations: BTreeMap::new(),
|
|
}
|
|
}
|
|
|
|
/// Creates a ledger that retains a bounded history per profile.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns [`UsageError::RetentionTooSmall`] below two observations, which
|
|
/// is the minimum a burn rate can be derived from.
|
|
pub fn with_retention(retention_per_profile: usize) -> Result<Self, UsageError> {
|
|
if retention_per_profile < MIN_RETENTION_PER_PROFILE {
|
|
return Err(UsageError::RetentionTooSmall);
|
|
}
|
|
Ok(Self {
|
|
retention_per_profile,
|
|
..Self::new()
|
|
})
|
|
}
|
|
|
|
/// Sets how old the newest observation may be before it reads as stale.
|
|
#[must_use]
|
|
pub const fn with_staleness_budget_ms(mut self, staleness_budget_ms: u64) -> Self {
|
|
self.staleness_budget_ms = staleness_budget_ms;
|
|
self
|
|
}
|
|
|
|
/// Appends an observation to its profile's history.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns [`UsageError::OutOfOrderObservation`] when the observation is
|
|
/// older than the newest one already recorded for that profile. The stream
|
|
/// is append-only, so time must not move backwards.
|
|
pub fn record(&mut self, observation: UsageObservation) -> Result<(), UsageError> {
|
|
let history = self
|
|
.observations
|
|
.entry(observation.profile.clone())
|
|
.or_default();
|
|
if let Some(newest) = history.back()
|
|
&& observation.observed_at_ms < newest.observed_at_ms
|
|
{
|
|
return Err(UsageError::OutOfOrderObservation {
|
|
observed_at_ms: observation.observed_at_ms,
|
|
newest_ms: newest.observed_at_ms,
|
|
});
|
|
}
|
|
history.push_back(observation);
|
|
while history.len() > self.retention_per_profile {
|
|
history.pop_front();
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn latest(&self, profile: &AccountProfileId) -> Option<&UsageObservation> {
|
|
self.observations.get(profile).and_then(VecDeque::back)
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn observation_count(&self, profile: &AccountProfileId) -> usize {
|
|
self.observations.get(profile).map_or(0, VecDeque::len)
|
|
}
|
|
|
|
pub fn profiles(&self) -> impl Iterator<Item = &AccountProfileId> {
|
|
self.observations.keys()
|
|
}
|
|
|
|
/// Derives the current view of a profile's usage.
|
|
///
|
|
/// Reported facts keep their own provenance. Anything this method computes
|
|
/// is labelled [`UsageProvenance::Estimated`], even when every input was
|
|
/// provider-reported: a derived rate is not a reported fact.
|
|
#[must_use]
|
|
pub fn project(&self, profile: &AccountProfileId, now_ms: u64) -> UsageProjection {
|
|
let Some(history) = self.observations.get(profile) else {
|
|
return UsageProjection::unavailable(None);
|
|
};
|
|
let Some(latest) = history.back() else {
|
|
return UsageProjection::unavailable(None);
|
|
};
|
|
if !latest.provenance.is_available() {
|
|
return UsageProjection::unavailable(Some(latest.observed_at_ms));
|
|
}
|
|
if let Some(window) = latest.window
|
|
&& window.has_expired(now_ms)
|
|
{
|
|
return UsageProjection::window_rolled(latest.observed_at_ms);
|
|
}
|
|
|
|
let consumed_permille = latest
|
|
.limit
|
|
.map(|limit| permille_of(latest.consumed, limit))
|
|
.or_else(|| {
|
|
matches!(latest.unit, Some(UsageUnit::WindowPermille))
|
|
.then_some(latest.consumed.min(MAX_PERMILLE))
|
|
});
|
|
let burn_per_hour = burn_per_hour(history, latest);
|
|
let resets_in_ms = latest.window.map(|window| window.remaining_ms(now_ms));
|
|
let exhaustion_in_ms = burn_per_hour
|
|
.and_then(|rate| exhaustion_in_ms(latest.remaining()?, rate, resets_in_ms));
|
|
|
|
UsageProjection {
|
|
unit: latest.unit,
|
|
consumed: Some(latest.consumed),
|
|
limit: latest.limit,
|
|
consumed_permille,
|
|
resets_in_ms,
|
|
burn_per_hour,
|
|
exhaustion_in_ms,
|
|
provenance: latest.provenance,
|
|
stale: now_ms.saturating_sub(latest.observed_at_ms) > self.staleness_budget_ms,
|
|
window_rolled: false,
|
|
observed_at_ms: Some(latest.observed_at_ms),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn permille_of(consumed: u64, limit: u64) -> u64 {
|
|
if limit == 0 {
|
|
return 0;
|
|
}
|
|
let permille = u128::from(consumed) * PERMILLE_SCALE / u128::from(limit);
|
|
u64::try_from(permille)
|
|
.unwrap_or(MAX_PERMILLE)
|
|
.min(MAX_PERMILLE)
|
|
}
|
|
|
|
/// Averages consumption across the newest observation's window.
|
|
///
|
|
/// Returns `None` unless two observations share a window and a unit, are far
|
|
/// enough apart to divide by, and did not move backwards. A single reading
|
|
/// yields no rate at all rather than a rate of zero.
|
|
fn burn_per_hour(history: &VecDeque<UsageObservation>, latest: &UsageObservation) -> Option<u64> {
|
|
let baseline = history.iter().find(|candidate| {
|
|
candidate.window == latest.window
|
|
&& candidate.unit == latest.unit
|
|
&& candidate.provenance.is_available()
|
|
&& candidate.observed_at_ms < latest.observed_at_ms
|
|
})?;
|
|
if latest.consumed < baseline.consumed {
|
|
return None;
|
|
}
|
|
let elapsed_ms = latest.observed_at_ms - baseline.observed_at_ms;
|
|
if elapsed_ms < MIN_BURN_SAMPLE_MS {
|
|
return None;
|
|
}
|
|
let consumed_delta = u128::from(latest.consumed - baseline.consumed);
|
|
let rate = consumed_delta * MILLIS_PER_HOUR / u128::from(elapsed_ms);
|
|
u64::try_from(rate).ok()
|
|
}
|
|
|
|
/// Projects when a ceiling is reached, unless the window resets first.
|
|
fn exhaustion_in_ms(remaining: u64, burn_per_hour: u64, resets_in_ms: Option<u64>) -> Option<u64> {
|
|
if burn_per_hour == 0 {
|
|
return None;
|
|
}
|
|
let millis = u128::from(remaining) * MILLIS_PER_HOUR / u128::from(burn_per_hour);
|
|
let millis = u64::try_from(millis).ok()?;
|
|
match resets_in_ms {
|
|
Some(resets_in_ms) if millis >= resets_in_ms => None,
|
|
_ => Some(millis),
|
|
}
|
|
}
|
|
|
|
/// The derived view a footer or detail panel renders.
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub struct UsageProjection {
|
|
unit: Option<UsageUnit>,
|
|
consumed: Option<u64>,
|
|
limit: Option<u64>,
|
|
consumed_permille: Option<u64>,
|
|
resets_in_ms: Option<u64>,
|
|
burn_per_hour: Option<u64>,
|
|
exhaustion_in_ms: Option<u64>,
|
|
provenance: UsageProvenance,
|
|
stale: bool,
|
|
window_rolled: bool,
|
|
observed_at_ms: Option<u64>,
|
|
}
|
|
|
|
impl UsageProjection {
|
|
const fn unavailable(observed_at_ms: Option<u64>) -> Self {
|
|
Self {
|
|
unit: None,
|
|
consumed: None,
|
|
limit: None,
|
|
consumed_permille: None,
|
|
resets_in_ms: None,
|
|
burn_per_hour: None,
|
|
exhaustion_in_ms: None,
|
|
provenance: UsageProvenance::Unavailable,
|
|
stale: false,
|
|
window_rolled: false,
|
|
observed_at_ms,
|
|
}
|
|
}
|
|
|
|
const fn window_rolled(observed_at_ms: u64) -> Self {
|
|
Self {
|
|
stale: true,
|
|
window_rolled: true,
|
|
..Self::unavailable(Some(observed_at_ms))
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn unit(self) -> Option<UsageUnit> {
|
|
self.unit
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn consumed(self) -> Option<u64> {
|
|
self.consumed
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn limit(self) -> Option<u64> {
|
|
self.limit
|
|
}
|
|
|
|
/// Share of the window consumed, in permille, when a ceiling is known.
|
|
#[must_use]
|
|
pub const fn consumed_permille(self) -> Option<u64> {
|
|
self.consumed_permille
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn resets_in_ms(self) -> Option<u64> {
|
|
self.resets_in_ms
|
|
}
|
|
|
|
/// Units per hour averaged over the current window.
|
|
#[must_use]
|
|
pub const fn burn_per_hour(self) -> Option<u64> {
|
|
self.burn_per_hour
|
|
}
|
|
|
|
/// Time until the ceiling is reached, absent unless it precedes the reset.
|
|
#[must_use]
|
|
pub const fn exhaustion_in_ms(self) -> Option<u64> {
|
|
self.exhaustion_in_ms
|
|
}
|
|
|
|
/// The provenance of the reported facts in this projection.
|
|
#[must_use]
|
|
pub const fn provenance(self) -> UsageProvenance {
|
|
self.provenance
|
|
}
|
|
|
|
/// The provenance of the burn rate, which is always derived.
|
|
#[must_use]
|
|
pub const fn burn_provenance(self) -> UsageProvenance {
|
|
match self.burn_per_hour {
|
|
Some(_) => UsageProvenance::Estimated,
|
|
None => UsageProvenance::Unavailable,
|
|
}
|
|
}
|
|
|
|
/// The trust classification a detail view must keep.
|
|
#[must_use]
|
|
pub const fn confidence(self) -> UsageConfidence {
|
|
if self.stale {
|
|
return UsageConfidence::Unknown;
|
|
}
|
|
self.provenance.confidence()
|
|
}
|
|
|
|
/// Whether the newest observation is older than the staleness budget.
|
|
#[must_use]
|
|
pub const fn is_stale(self) -> bool {
|
|
self.stale
|
|
}
|
|
|
|
/// Whether the last known window ended before this projection was taken.
|
|
#[must_use]
|
|
pub const fn window_has_rolled(self) -> bool {
|
|
self.window_rolled
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn observed_at_ms(self) -> Option<u64> {
|
|
self.observed_at_ms
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn is_available(self) -> bool {
|
|
self.provenance.is_available()
|
|
}
|
|
}
|
|
|
|
/// Renders the five footer answers for one profile.
|
|
///
|
|
/// Every method returns an explicit unavailable phrase instead of a placeholder
|
|
/// number, so a missing fact reads as missing rather than as zero.
|
|
#[derive(Clone, Copy, Debug)]
|
|
pub struct FooterUsage<'a> {
|
|
profile: &'a AccountProfile,
|
|
projection: &'a UsageProjection,
|
|
}
|
|
|
|
impl<'a> FooterUsage<'a> {
|
|
#[must_use]
|
|
pub const fn new(profile: &'a AccountProfile, projection: &'a UsageProjection) -> Self {
|
|
Self {
|
|
profile,
|
|
projection,
|
|
}
|
|
}
|
|
|
|
/// Which harness, provider, model, and account profile is active.
|
|
#[must_use]
|
|
pub fn identity(&self) -> String {
|
|
format!(
|
|
"{} · {}",
|
|
self.profile.identity_line(),
|
|
self.profile.account()
|
|
)
|
|
}
|
|
|
|
/// How much of the current usage window has been consumed.
|
|
#[must_use]
|
|
pub fn window(&self) -> String {
|
|
match (
|
|
self.projection.consumed_permille(),
|
|
self.projection.consumed(),
|
|
self.projection.unit(),
|
|
) {
|
|
(Some(permille), _, _) => format!("{} used", format_permille(permille)),
|
|
(None, Some(consumed), Some(unit)) => {
|
|
format!(
|
|
"{} used · no ceiling reported",
|
|
unit.format_amount(consumed)
|
|
)
|
|
}
|
|
_ if self.projection.window_has_rolled() => "window rolled over".to_owned(),
|
|
_ => "usage unavailable".to_owned(),
|
|
}
|
|
}
|
|
|
|
/// When the window resets.
|
|
#[must_use]
|
|
pub fn reset(&self) -> String {
|
|
self.projection.resets_in_ms().map_or_else(
|
|
|| "reset time unavailable".to_owned(),
|
|
|remaining| format!("resets in {}", format_duration_ms(remaining)),
|
|
)
|
|
}
|
|
|
|
/// The recent burn rate and, when meaningful, the projected exhaustion.
|
|
#[must_use]
|
|
pub fn burn(&self) -> String {
|
|
let Some(rate) = self.projection.burn_per_hour() else {
|
|
return "burn rate unavailable".to_owned();
|
|
};
|
|
let unit = self.projection.unit().unwrap_or(UsageUnit::Tokens);
|
|
let rate = format!("~{}/hr est", unit.format_amount(rate));
|
|
self.projection
|
|
.exhaustion_in_ms()
|
|
.map_or(rate.clone(), |exhaustion| {
|
|
format!("{rate} · empty in {}", format_duration_ms(exhaustion))
|
|
})
|
|
}
|
|
|
|
/// How trustworthy the numbers above are.
|
|
#[must_use]
|
|
pub fn trust(&self) -> String {
|
|
let provenance = self.projection.provenance();
|
|
if !provenance.is_available() {
|
|
return if self.projection.window_has_rolled() {
|
|
"awaiting a new window reading".to_owned()
|
|
} else {
|
|
"no usage source".to_owned()
|
|
};
|
|
}
|
|
let base = format!(
|
|
"{} · {}",
|
|
provenance.compact_label(),
|
|
self.projection.confidence().label()
|
|
);
|
|
if self.projection.is_stale() {
|
|
return format!("{base} · stale");
|
|
}
|
|
base
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for FooterUsage<'_> {
|
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
write!(
|
|
formatter,
|
|
"{} · {} · {} · {} · {}",
|
|
self.identity(),
|
|
self.window(),
|
|
self.reset(),
|
|
self.burn(),
|
|
self.trust()
|
|
)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::{
|
|
AccountProfile, AccountProfileId, FooterUsage, UsageConfidence, UsageError, UsageLedger,
|
|
UsageObservation, UsageProvenance, UsageUnit, UsageWindow, format_duration_ms,
|
|
};
|
|
|
|
const HOUR_MS: u64 = 3_600_000;
|
|
|
|
fn profile() -> AccountProfile {
|
|
AccountProfile::new("codex-plus", "Codex", "ChatGPT", "gpt-5.2", "work")
|
|
.expect("the fixture profile is valid")
|
|
}
|
|
|
|
fn window() -> UsageWindow {
|
|
UsageWindow::new(0, 5 * HOUR_MS).expect("the fixture window is valid")
|
|
}
|
|
|
|
fn observation(consumed: u64, at_ms: u64, provenance: UsageProvenance) -> UsageObservation {
|
|
UsageObservation::counted(
|
|
AccountProfileId::new("codex-plus").expect("valid ID"),
|
|
UsageUnit::Tokens,
|
|
consumed,
|
|
provenance,
|
|
at_ms,
|
|
)
|
|
.expect("a counted observation is available")
|
|
.with_limit(1_000_000)
|
|
.expect("a positive limit is valid")
|
|
.with_window(window())
|
|
.expect("the observation is inside the window")
|
|
}
|
|
|
|
fn ledger_with(samples: &[(u64, u64)]) -> UsageLedger {
|
|
let mut ledger = UsageLedger::new();
|
|
for &(consumed, at_ms) in samples {
|
|
ledger
|
|
.record(observation(
|
|
consumed,
|
|
at_ms,
|
|
UsageProvenance::ProviderReported,
|
|
))
|
|
.expect("fixture observations are ordered");
|
|
}
|
|
ledger
|
|
}
|
|
|
|
#[test]
|
|
fn provenance_labels_and_confidence_are_stable() {
|
|
assert_eq!(
|
|
UsageProvenance::ProviderReported.label(),
|
|
"provider-reported"
|
|
);
|
|
assert_eq!(UsageProvenance::Estimated.compact_label(), "est");
|
|
assert!(UsageProvenance::HarnessReported.is_reported());
|
|
assert!(!UsageProvenance::LocallyMeasured.is_reported());
|
|
assert_eq!(
|
|
UsageProvenance::ProviderReported.confidence(),
|
|
UsageConfidence::Exact
|
|
);
|
|
assert_eq!(
|
|
UsageProvenance::Unavailable.confidence(),
|
|
UsageConfidence::Unknown
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn the_weakest_provenance_wins() {
|
|
assert_eq!(
|
|
UsageProvenance::ProviderReported.weakest(UsageProvenance::Estimated),
|
|
UsageProvenance::Estimated
|
|
);
|
|
assert_eq!(
|
|
UsageProvenance::Estimated.weakest(UsageProvenance::Unavailable),
|
|
UsageProvenance::Unavailable
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn a_counted_value_cannot_be_labelled_unavailable() {
|
|
let error = UsageObservation::counted(
|
|
AccountProfileId::new("codex-plus").expect("valid ID"),
|
|
UsageUnit::Tokens,
|
|
10,
|
|
UsageProvenance::Unavailable,
|
|
0,
|
|
)
|
|
.expect_err("unavailable counted values are rejected");
|
|
assert_eq!(error, UsageError::UnavailableWithValue);
|
|
}
|
|
|
|
#[test]
|
|
fn windows_and_limits_are_validated() {
|
|
assert_eq!(UsageWindow::new(10, 10), Err(UsageError::EmptyWindow));
|
|
let observation = UsageObservation::counted(
|
|
AccountProfileId::new("codex-plus").expect("valid ID"),
|
|
UsageUnit::Tokens,
|
|
10,
|
|
UsageProvenance::ProviderReported,
|
|
9 * HOUR_MS,
|
|
)
|
|
.expect("a counted observation is available");
|
|
assert_eq!(
|
|
observation.clone().with_limit(0),
|
|
Err(UsageError::ZeroLimit)
|
|
);
|
|
assert!(matches!(
|
|
observation.with_window(window()),
|
|
Err(UsageError::ObservationOutsideWindow { .. })
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn the_ledger_refuses_to_move_backwards() {
|
|
let mut ledger = ledger_with(&[(0, 0), (100, HOUR_MS)]);
|
|
let error = ledger
|
|
.record(observation(
|
|
50,
|
|
HOUR_MS / 2,
|
|
UsageProvenance::ProviderReported,
|
|
))
|
|
.expect_err("an older observation is rejected");
|
|
assert!(matches!(
|
|
error,
|
|
UsageError::OutOfOrderObservation { newest_ms, .. } if newest_ms == HOUR_MS
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn retention_is_bounded_and_keeps_the_newest_samples() {
|
|
let mut ledger = UsageLedger::with_retention(2).expect("two samples are allowed");
|
|
for step in 0..5 {
|
|
ledger
|
|
.record(observation(
|
|
step * 1_000,
|
|
step * HOUR_MS / 4,
|
|
UsageProvenance::ProviderReported,
|
|
))
|
|
.expect("ordered observations are accepted");
|
|
}
|
|
let id = AccountProfileId::new("codex-plus").expect("valid ID");
|
|
assert_eq!(ledger.observation_count(&id), 2);
|
|
assert_eq!(
|
|
ledger.latest(&id).map(UsageObservation::consumed),
|
|
Some(4_000)
|
|
);
|
|
assert!(matches!(
|
|
UsageLedger::with_retention(1),
|
|
Err(UsageError::RetentionTooSmall)
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn a_single_observation_yields_no_burn_rate() {
|
|
let ledger = ledger_with(&[(620_000, 0)]);
|
|
let projection = ledger.project(profile().id(), 0);
|
|
assert_eq!(projection.consumed_permille(), Some(620));
|
|
assert_eq!(projection.burn_per_hour(), None, "a rate needs two samples");
|
|
assert_eq!(projection.exhaustion_in_ms(), None);
|
|
}
|
|
|
|
#[test]
|
|
fn burn_and_exhaustion_are_derived_from_the_window() {
|
|
let ledger = ledger_with(&[(0, 0), (300_000, HOUR_MS)]);
|
|
let projection = ledger.project(profile().id(), HOUR_MS);
|
|
assert_eq!(projection.burn_per_hour(), Some(300_000));
|
|
assert_eq!(projection.consumed_permille(), Some(300));
|
|
assert_eq!(projection.resets_in_ms(), Some(4 * HOUR_MS));
|
|
assert_eq!(
|
|
projection.exhaustion_in_ms(),
|
|
Some(2 * HOUR_MS + 20 * 60_000),
|
|
"700k remaining at 300k/hr is reached before the four-hour reset"
|
|
);
|
|
assert_eq!(projection.burn_provenance(), UsageProvenance::Estimated);
|
|
assert_eq!(projection.provenance(), UsageProvenance::ProviderReported);
|
|
}
|
|
|
|
#[test]
|
|
fn exhaustion_is_withheld_when_the_window_resets_first() {
|
|
let ledger = ledger_with(&[(0, 0), (10_000, HOUR_MS)]);
|
|
let projection = ledger.project(profile().id(), HOUR_MS);
|
|
assert_eq!(projection.burn_per_hour(), Some(10_000));
|
|
assert_eq!(
|
|
projection.exhaustion_in_ms(),
|
|
None,
|
|
"the window resets long before the ceiling is reached"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn an_unavailable_observation_invalidates_older_facts() {
|
|
let mut ledger = ledger_with(&[(0, 0), (200_000, HOUR_MS)]);
|
|
ledger
|
|
.record(UsageObservation::unavailable(
|
|
AccountProfileId::new("codex-plus").expect("valid ID"),
|
|
2 * HOUR_MS,
|
|
))
|
|
.expect("a gap is recordable");
|
|
let projection = ledger.project(profile().id(), 2 * HOUR_MS);
|
|
assert!(!projection.is_available());
|
|
assert_eq!(projection.consumed_permille(), None);
|
|
assert_eq!(projection.burn_per_hour(), None);
|
|
assert_eq!(projection.confidence(), UsageConfidence::Unknown);
|
|
}
|
|
|
|
#[test]
|
|
fn an_expired_window_is_not_presented_as_current() {
|
|
let ledger = ledger_with(&[(0, 0), (200_000, HOUR_MS)]);
|
|
let projection = ledger.project(profile().id(), 6 * HOUR_MS);
|
|
assert!(projection.window_has_rolled());
|
|
assert!(projection.is_stale());
|
|
assert_eq!(projection.consumed_permille(), None);
|
|
assert_eq!(projection.burn_per_hour(), None);
|
|
}
|
|
|
|
#[test]
|
|
fn an_unknown_profile_projects_as_unavailable() {
|
|
let ledger = UsageLedger::new();
|
|
let projection = ledger.project(profile().id(), 0);
|
|
assert!(!projection.is_available());
|
|
assert_eq!(projection.observed_at_ms(), None);
|
|
}
|
|
|
|
#[test]
|
|
fn staleness_demotes_confidence_without_erasing_the_fact() {
|
|
let ledger = UsageLedger::new().with_staleness_budget_ms(1_000);
|
|
let mut ledger = ledger;
|
|
ledger
|
|
.record(observation(200_000, 0, UsageProvenance::ProviderReported))
|
|
.expect("ordered");
|
|
ledger
|
|
.record(observation(
|
|
400_000,
|
|
HOUR_MS,
|
|
UsageProvenance::ProviderReported,
|
|
))
|
|
.expect("ordered");
|
|
let projection = ledger.project(profile().id(), HOUR_MS + 5_000);
|
|
assert!(projection.is_stale());
|
|
assert_eq!(projection.consumed_permille(), Some(400));
|
|
assert_eq!(projection.confidence(), UsageConfidence::Unknown);
|
|
}
|
|
|
|
#[test]
|
|
fn the_footer_answers_five_questions() {
|
|
let ledger = ledger_with(&[(0, 0), (620_000, HOUR_MS)]);
|
|
let projection = ledger.project(profile().id(), HOUR_MS);
|
|
let profile = profile();
|
|
let footer = FooterUsage::new(&profile, &projection);
|
|
assert_eq!(footer.identity(), "Codex · ChatGPT · gpt-5.2 · work");
|
|
assert_eq!(footer.window(), "62.0% used");
|
|
assert_eq!(footer.reset(), "resets in 4h 0m");
|
|
assert!(footer.burn().starts_with("~620.0k tokens/hr est"));
|
|
assert_eq!(footer.trust(), "provider · exact");
|
|
}
|
|
|
|
#[test]
|
|
fn the_footer_says_unavailable_instead_of_zero() {
|
|
let ledger = UsageLedger::new();
|
|
let projection = ledger.project(profile().id(), 0);
|
|
let profile = profile();
|
|
let footer = FooterUsage::new(&profile, &projection);
|
|
assert_eq!(footer.window(), "usage unavailable");
|
|
assert_eq!(footer.reset(), "reset time unavailable");
|
|
assert_eq!(footer.burn(), "burn rate unavailable");
|
|
assert_eq!(footer.trust(), "no usage source");
|
|
assert!(!footer.to_string().contains('%'));
|
|
}
|
|
|
|
#[test]
|
|
fn a_locally_measured_count_without_a_ceiling_says_so() {
|
|
let mut ledger = UsageLedger::new();
|
|
ledger
|
|
.record(
|
|
UsageObservation::counted(
|
|
AccountProfileId::new("codex-plus").expect("valid ID"),
|
|
UsageUnit::Requests,
|
|
42,
|
|
UsageProvenance::LocallyMeasured,
|
|
0,
|
|
)
|
|
.expect("available"),
|
|
)
|
|
.expect("ordered");
|
|
let projection = ledger.project(profile().id(), 0);
|
|
let profile = profile();
|
|
let footer = FooterUsage::new(&profile, &projection);
|
|
assert_eq!(footer.window(), "42 requests used · no ceiling reported");
|
|
assert_eq!(footer.trust(), "local · approximate");
|
|
}
|
|
|
|
#[test]
|
|
fn durations_render_without_false_precision() {
|
|
assert_eq!(format_duration_ms(45_000), "45s");
|
|
assert_eq!(format_duration_ms(14 * 60_000), "14m");
|
|
assert_eq!(format_duration_ms(2 * HOUR_MS + 14 * 60_000), "2h 14m");
|
|
// A weekly quota window is a real provider shape, not a hypothetical.
|
|
assert_eq!(format_duration_ms(10_080 * 60_000), "7d 0h");
|
|
assert_eq!(format_duration_ms(151 * HOUR_MS + 30 * 60_000), "6d 7h");
|
|
}
|
|
|
|
#[test]
|
|
fn a_window_can_know_its_reset_without_knowing_its_start() {
|
|
let window = UsageWindow::until(5 * HOUR_MS);
|
|
assert_eq!(window.started_at_ms(), None);
|
|
assert_eq!(window.resets_at_ms(), 5 * HOUR_MS);
|
|
assert!(window.contains(0), "an unknown start bounds nothing below");
|
|
assert!(window.contains(5 * HOUR_MS - 1));
|
|
assert!(
|
|
!window.contains(5 * HOUR_MS),
|
|
"the reset still bounds it above"
|
|
);
|
|
assert!(window.has_expired(5 * HOUR_MS));
|
|
assert_eq!(window.remaining_ms(HOUR_MS), 4 * HOUR_MS);
|
|
}
|
|
|
|
#[test]
|
|
fn windows_with_the_same_reset_share_an_identity() {
|
|
assert_eq!(UsageWindow::until(HOUR_MS), UsageWindow::until(HOUR_MS));
|
|
assert_ne!(UsageWindow::until(HOUR_MS), UsageWindow::until(2 * HOUR_MS));
|
|
assert_ne!(
|
|
UsageWindow::until(HOUR_MS),
|
|
UsageWindow::new(0, HOUR_MS).expect("a bounded window is valid"),
|
|
"a known start is a different fact from an unknown one"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn a_start_only_window_still_scopes_a_burn_rate_to_one_period() {
|
|
// The provider reported a reset but no window length, then the quota
|
|
// rolled. A baseline must not be drawn across the two periods.
|
|
let id = AccountProfileId::new("codex-plus").expect("valid ID");
|
|
let mut ledger = UsageLedger::new();
|
|
for (consumed, at_ms, resets_at_ms) in [
|
|
(800, 0, 5 * HOUR_MS),
|
|
(50, 5 * HOUR_MS + 1_000, 10 * HOUR_MS),
|
|
(850, 6 * HOUR_MS, 10 * HOUR_MS),
|
|
] {
|
|
ledger
|
|
.record(
|
|
UsageObservation::counted(
|
|
id.clone(),
|
|
UsageUnit::WindowPermille,
|
|
consumed,
|
|
UsageProvenance::ProviderReported,
|
|
at_ms,
|
|
)
|
|
.expect("available")
|
|
.with_limit(1_000)
|
|
.expect("a positive limit")
|
|
.with_window(UsageWindow::until(resets_at_ms))
|
|
.expect("the observation precedes its reset"),
|
|
)
|
|
.expect("ordered");
|
|
}
|
|
let projection = ledger.project(&id, 6 * HOUR_MS);
|
|
assert_eq!(projection.resets_in_ms(), Some(4 * HOUR_MS));
|
|
assert_eq!(
|
|
projection.burn_per_hour(),
|
|
Some(800),
|
|
"the baseline must be the reading from this window, not the last one"
|
|
);
|
|
assert_eq!(
|
|
projection.exhaustion_in_ms(),
|
|
Some(675_000),
|
|
"150 permille left at 800 per hour is about eleven minutes"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn identities_reject_empty_and_control_values() {
|
|
assert!(matches!(
|
|
AccountProfile::new("id", "", "p", "m", "a"),
|
|
Err(UsageError::InvalidIdentifier("harness name"))
|
|
));
|
|
assert!(matches!(
|
|
AccountProfileId::new("bad\nid"),
|
|
Err(UsageError::InvalidIdentifier("account profile ID"))
|
|
));
|
|
}
|
|
}
|