pub(crate) mod budget;
pub mod pace;
pub(crate) mod readings;
use crate::time;
use serde_json::Value;
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Window {
pub kind: String,
pub scope: Option<String>,
pub percent: f64,
pub resets_at: Option<i64>,
pub is_active: bool,
#[serde(default)]
pub severity: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub length_seconds: Option<i64>,
}
pub(crate) fn anthropic_window_length(kind: &str) -> Option<i64> {
match kind {
"session" | "five_hour" => Some(5 * 3600),
"seven_day" => Some(7 * 86_400),
weekly if weekly.starts_with("weekly_") => Some(7 * 86_400),
_ => None,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Source {
Live,
Remembered,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Snapshot {
pub windows: Vec<Window>,
pub observed_at: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub answered_at: Option<i64>,
#[serde(default)]
pub lists_every_limit: bool,
pub source: Source,
}
impl Snapshot {
pub(crate) fn answered(&self) -> bool {
self.answered_at.is_some()
}
}
impl Window {
pub(crate) fn used(&self, now: i64) -> f64 {
if self.resets_at.is_some_and(|at| at <= now) {
0.0
} else {
self.percent
}
}
pub(crate) fn same_limit(&self, other: &Window) -> bool {
limit_name(&self.kind) == limit_name(&other.kind) && self.scope == other.scope
}
pub(crate) fn same_window(&self, other: &Window) -> bool {
self.same_limit(other)
&& matches!(
(self.resets_at, other.resets_at),
(Some(x), Some(y)) if same_reset(x, y)
)
}
}
pub(crate) fn limit_name(kind: &str) -> &str {
match kind {
"five_hour" => "session",
"seven_day" => "weekly_all",
other => other,
}
}
pub fn whole(percent: f64) -> i64 {
percent.round() as i64
}
pub(crate) fn reached(percent: f64, share: u8) -> bool {
whole(percent) >= i64::from(share)
}
const SAME_RESET: u64 = 60;
pub fn same_reset(a: i64, b: i64) -> bool {
a.abs_diff(b) < SAME_RESET
}
pub(crate) fn may_still_run(at: i64, now: i64) -> bool {
at > now || same_reset(at, now)
}
pub(crate) fn answered(known: Option<&Snapshot>, answer: &Snapshot, now: i64) -> Snapshot {
let Some(known) = known else {
return answer.clone();
};
let stamped = |at: Option<i64>| at.filter(|at| *at <= now);
if answer.answered_at < stamped(known.answered_at) {
return known.clone();
}
let moved_since = stamped(known.observed_at) >= answer.answered_at;
let higher = |given: &Window| {
known
.windows
.iter()
.find(|had| moved_since && had.same_window(given) && had.percent > given.percent)
};
let take = |given: &Window| match higher(given) {
Some(had) => Window {
percent: had.percent,
severity: had.severity.clone(),
..given.clone()
},
None => given.clone(),
};
let windows = if answer.lists_every_limit {
answer.windows.iter().map(take).collect()
} else {
let answer_of = |had: &Window| answer.windows.iter().find(|given| given.same_limit(had));
let running = |had: &Window| {
had.resets_at
.zip(answer.answered_at)
.is_some_and(|(reset, taken)| reset > taken)
};
let added = answer
.windows
.iter()
.filter(|given| !known.windows.iter().any(|had| had.same_limit(given)));
known
.windows
.iter()
.filter_map(|had| match answer_of(had) {
Some(given) => Some(take(given)),
None => running(had).then(|| had.clone()),
})
.chain(added.map(take))
.collect()
};
Snapshot {
windows,
observed_at: stamped(known.observed_at).max(answer.observed_at),
answered_at: answer.answered_at,
lists_every_limit: answer.lists_every_limit,
source: answer.source,
}
}
pub(crate) fn moved(known: &Snapshot, passed: &[Window], now: i64) -> Option<Snapshot> {
let mut windows = known.windows.clone();
let mut changed = false;
for given in passed
.iter()
.filter(|w| w.resets_at.is_some_and(|at| at > now))
{
let Some(had) = windows.iter_mut().find(|had| had.same_limit(given)) else {
continue;
};
let running = had.resets_at.is_some_and(|at| at > now);
had.resets_at = match (had.same_window(given), running) {
(true, _) if given.percent > had.percent => had.resets_at,
(false, false) => given.resets_at,
_ => continue,
};
had.percent = given.percent;
had.severity = None;
changed = true;
}
changed.then(|| Snapshot {
windows,
observed_at: Some(now),
..known.clone()
})
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Room {
pub used: Option<f64>,
pub fullest: f64,
pub weekly: bool,
}
pub fn room(
theirs: &[Window],
lists_every_limit: bool,
mine: &[Window],
limit: &Window,
share: u8,
now: i64,
) -> Option<Room> {
if theirs.is_empty() || theirs.iter().any(|window| reached(window.used(now), share)) {
return None;
}
let given = |had: &Window| theirs.iter().find(|window| window.same_limit(had));
let gives = |kind: &str| {
theirs
.iter()
.any(|window| window.scope.is_none() && limit_name(&window.kind) == kind)
};
if !lists_every_limit {
let overall = gives("session") && gives("weekly_all");
let unknown = |had: &Window| {
given(had).is_none() && !(overall && had.scope.is_some() && !had.same_limit(limit))
};
if std::iter::once(limit).chain(mine).any(unknown) {
return None;
}
}
let fullest = theirs
.iter()
.filter(|window| !window.same_limit(limit))
.map(|window| window.used(now))
.fold(0.0, f64::max);
Some(Room {
used: given(limit).map(|window| window.used(now)),
fullest,
weekly: !lists_every_limit || gives("weekly_all"),
})
}
pub fn preferred<T>(candidates: impl IntoIterator<Item = (T, Room)>) -> Option<(T, Room)> {
candidates.into_iter().min_by(|(_, a), (_, b)| {
a.weekly
.cmp(&b.weekly)
.then_with(|| a.used.unwrap_or(0.0).total_cmp(&b.used.unwrap_or(0.0)))
.then_with(|| a.fullest.total_cmp(&b.fullest))
})
}
fn percent(v: &Value) -> Option<f64> {
let p = v.as_f64()?;
(p.is_finite() && p >= 0.0).then_some(p)
}
fn window_from_limit(l: &Value) -> Option<Window> {
let named = |what: &str| {
l.get("scope")
.and_then(|s| s.get(what))
.and_then(|m| m.get("display_name"))
.and_then(Value::as_str)
.map(str::to_owned)
};
Some(Window {
kind: l.get("kind")?.as_str()?.to_string(),
scope: named("model").or_else(|| named("surface")),
severity: l.get("severity").and_then(Value::as_str).map(str::to_owned),
percent: percent(l.get("percent")?)?,
resets_at: l
.get("resets_at")
.and_then(Value::as_str)
.and_then(time::parse),
is_active: l.get("is_active").and_then(Value::as_bool).unwrap_or(false),
length_seconds: l
.get("kind")
.and_then(Value::as_str)
.and_then(anthropic_window_length),
})
}
fn window_from_named(kind: &str, v: &Value) -> Option<Window> {
Some(Window {
kind: kind.to_string(),
scope: None,
severity: None,
percent: percent(v.get("utilization")?)?,
resets_at: v
.get("resets_at")
.and_then(Value::as_str)
.and_then(time::parse),
is_active: false,
length_seconds: anthropic_window_length(kind),
})
}
pub(crate) fn from_usage_object(u: &Value, observed_at: i64) -> Snapshot {
let limits = u.get("limits").and_then(Value::as_array);
let mut windows: Vec<Window> = limits
.into_iter()
.flatten()
.filter_map(window_from_limit)
.collect();
let lists_every_limit =
!windows.is_empty() && limits.is_some_and(|rows| rows.len() == windows.len());
if windows.is_empty() {
for kind in ["five_hour", "seven_day"] {
if let Some(w) = u.get(kind).and_then(|v| window_from_named(kind, v)) {
windows.push(w);
}
}
}
Snapshot {
windows,
observed_at: Some(observed_at),
answered_at: Some(observed_at),
lists_every_limit,
source: Source::Live,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn real_answer() -> Value {
serde_json::json!({
"five_hour": {"utilization": 62, "resets_at": "2026-09-20T22:20:00.095287+00:00"},
"seven_day": {"utilization": 48, "resets_at": "2026-09-27T02:00:00.095306+00:00"},
"limits": [
{"kind": "session", "group": "session", "percent": 62,
"resets_at": "2026-09-20T22:20:00.095287+00:00", "scope": null, "is_active": true},
{"kind": "weekly_all", "group": "weekly", "percent": 48,
"resets_at": "2026-09-27T02:00:00.095306+00:00", "scope": null, "is_active": false},
{"kind": "weekly_scoped", "group": "weekly", "percent": 0,
"resets_at": "2026-09-27T02:00:00+00:00",
"scope": {"model": {"id": null, "display_name": "Fable"}}, "is_active": false}
]})
}
#[test]
fn reads_the_real_answers_shape() {
let s = from_usage_object(&real_answer(), 1789933772);
assert_eq!(s.windows.len(), 3);
assert_eq!(s.observed_at, Some(1789933772));
assert_eq!(s.answered_at, Some(1789933772));
assert!(s.lists_every_limit);
assert_eq!(s.source, Source::Live);
let scoped = s
.windows
.iter()
.find(|w| w.kind == "weekly_scoped")
.unwrap();
assert_eq!(scoped.scope.as_deref(), Some("Fable"));
}
#[test]
fn falls_back_to_the_named_windows_when_limits_is_missing() {
let mut answer = real_answer();
answer.as_object_mut().unwrap().remove("limits");
let s = from_usage_object(&answer, 1789933772);
assert_eq!(s.windows.len(), 2);
assert_eq!(s.windows[0].kind, "five_hour");
assert_eq!(s.windows[0].percent, 62.0);
assert!(!s.lists_every_limit, "nothing scoped to a model is named");
}
#[test]
fn a_nonsense_percentage_is_dropped_rather_than_drawn() {
for nonsense in [serde_json::json!(-5), serde_json::json!("75")] {
let mut answer = real_answer();
answer["limits"][0]["percent"] = nonsense;
let s = from_usage_object(&answer, 1789933772);
assert_eq!(s.windows.len(), 2);
assert!(
!s.lists_every_limit,
"the row dropped is a limit all the same"
);
}
}
#[test]
fn an_exceeded_limit_is_kept_not_dropped() {
let mut answer = real_answer();
answer["limits"][0]["percent"] = serde_json::json!(104);
let s = from_usage_object(&answer, 1789933772);
assert_eq!(s.windows.len(), 3);
assert_eq!(s.windows[0].percent, 104.0);
}
#[test]
fn a_share_is_one_whole_percentage_a_half_rounded_up() {
assert_eq!(whole(94.5), 95);
assert_eq!(whole(94.49), 94);
assert_eq!(whole(104.0), 104);
assert_eq!(whole(0.4), 0);
}
const NOW: i64 = 1_789_935_000;
const HOUR: i64 = 3_600;
fn measured(kind: &str, percent: f64, resets_at: Option<i64>) -> Window {
Window {
kind: kind.into(),
scope: None,
percent,
resets_at,
is_active: true,
severity: None,
length_seconds: anthropic_window_length(kind),
}
}
fn answer(windows: Vec<Window>, at: i64) -> Snapshot {
Snapshot {
windows,
observed_at: Some(at),
answered_at: Some(at),
lists_every_limit: true,
source: Source::Live,
}
}
fn held(windows: Vec<Window>, observed_at: i64, answered_at: Option<i64>) -> Snapshot {
Snapshot {
windows,
observed_at: Some(observed_at),
answered_at,
lists_every_limit: true,
source: Source::Remembered,
}
}
fn shares(reading: &Snapshot) -> Vec<(&str, f64)> {
reading
.windows
.iter()
.map(|w| (w.kind.as_str(), w.percent))
.collect()
}
#[test]
fn resets_under_a_minute_apart_are_one_reset() {
assert!(same_reset(NOW, NOW));
assert!(same_reset(NOW, NOW + 59));
assert!(same_reset(NOW + 59, NOW));
assert!(!same_reset(NOW, NOW + 60));
assert!(!same_reset(NOW + 60, NOW));
assert!(
!same_reset(0, NOW),
"no reset known is no reset in particular"
);
}
#[test]
fn a_window_may_still_run_until_a_minute_past_its_reset() {
assert!(may_still_run(NOW, NOW - 1));
assert!(may_still_run(NOW, NOW));
assert!(may_still_run(NOW, NOW + 59));
assert!(!may_still_run(NOW, NOW + 60));
assert!(!may_still_run(0, NOW));
}
#[test]
fn an_answer_replaces_every_limit_it_does_not_give() {
let known = held(
vec![
measured("session", 10.0, Some(NOW + HOUR)),
measured("weekly_all", 100.0, Some(NOW + 50 * HOUR)),
],
NOW - 60,
None,
);
let given = answer(
vec![
measured("session", 20.0, Some(NOW + HOUR)),
Window {
scope: Some("Fable".into()),
..measured("weekly_scoped", 0.0, Some(NOW + 50 * HOUR))
},
],
NOW,
);
let reading = answered(Some(&known), &given, NOW);
assert_eq!(
shares(&reading),
[("session", 20.0), ("weekly_scoped", 0.0)]
);
assert_eq!(reading.answered_at, Some(NOW));
assert_eq!(reading.observed_at, Some(NOW));
}
#[test]
fn a_limit_whose_row_could_not_be_read_is_not_taken_away() {
let mut spent = real_answer();
spent["limits"][1]["percent"] = serde_json::json!(100);
let known = from_usage_object(&spent, NOW - 600);
let mut unread = spent.clone();
unread["limits"][0]["percent"] = serde_json::json!(70);
unread["limits"][1]["percent"] = Value::Null;
let reading = answered(Some(&known), &from_usage_object(&unread, NOW), NOW);
assert_eq!(
shares(&reading),
[
("session", 70.0),
("weekly_all", 100.0),
("weekly_scoped", 0.0)
]
);
}
#[test]
fn an_answer_in_the_older_shape_takes_no_limit_away() {
let mut spent = real_answer();
spent["limits"][2]["percent"] = serde_json::json!(100);
let known = from_usage_object(&spent, NOW - 600);
let mut older = real_answer();
older.as_object_mut().unwrap().remove("limits");
let reading = answered(Some(&known), &from_usage_object(&older, NOW), NOW);
assert_eq!(
shares(&reading),
[
("five_hour", 62.0),
("seven_day", 48.0),
("weekly_scoped", 100.0)
]
);
}
#[test]
fn a_reading_stamped_ahead_of_the_clock_holds_no_answer_back() {
let known = held(
vec![measured("session", 80.0, Some(NOW + 2 * HOUR))],
NOW + HOUR,
Some(NOW + HOUR),
);
let given = answer(vec![measured("session", 20.0, Some(NOW + 2 * HOUR))], NOW);
let reading = answered(Some(&known), &given, NOW);
assert_eq!(shares(&reading), [("session", 20.0)]);
assert_eq!(
(reading.observed_at, reading.answered_at),
(Some(NOW), Some(NOW))
);
}
#[test]
fn a_window_openai_leaves_out_stands_until_its_reset() {
let known = held(
vec![
measured("five_hour", 30.0, Some(NOW + HOUR)),
measured("1_day", 60.0, Some(NOW - 60)),
measured("2_day", 20.0, None),
measured("seven_day", 40.0, Some(NOW + 50 * HOUR)),
],
NOW - 600,
Some(NOW - 600),
);
let whole = answer(
vec![measured("seven_day", 45.0, Some(NOW + 50 * HOUR))],
NOW,
);
let given = Snapshot {
lists_every_limit: false,
..whole.clone()
};
let reading = answered(Some(&known), &given, NOW);
assert_eq!(shares(&reading), [("five_hour", 30.0), ("seven_day", 45.0)]);
assert!(!reading.lists_every_limit);
assert_eq!(
shares(&answered(Some(&known), &whole, NOW)),
[("seven_day", 45.0)],
"where the answer lists every limit"
);
}
#[test]
fn a_session_moves_a_share_only_inside_a_window_it_was_given() {
let known = held(
vec![measured("session", 22.0, Some(NOW + HOUR))],
NOW - 60,
Some(NOW - 60),
);
let higher = moved(
&known,
&[measured("five_hour", 25.0, Some(NOW + HOUR))],
NOW,
)
.expect("a share moved");
assert_eq!(
shares(&higher),
[("session", 25.0)],
"under the name the service gave it"
);
assert_eq!(higher.answered_at, Some(NOW - 60), "and no answer since");
assert_eq!(
moved(
&known,
&[measured("five_hour", 5.0, Some(NOW + 6 * HOUR))],
NOW
),
None,
"another reset still ahead"
);
assert_eq!(
moved(
&known,
&[measured("seven_day", 40.0, Some(NOW + 50 * HOUR))],
NOW
),
None,
"a limit the service did not give"
);
}
#[test]
fn a_session_takes_the_next_window_of_a_limit_whose_reset_has_passed() {
let mut over = measured("session", 90.0, Some(NOW - 60));
over.severity = Some("warning".into());
let not_running = measured("session", 0.0, None);
for had in [over, not_running] {
let known = held(vec![had.clone()], NOW - HOUR, Some(NOW - HOUR));
let next = moved(
&known,
&[measured("five_hour", 3.0, Some(NOW + 5 * HOUR))],
NOW,
)
.expect("the next window");
let window = &next.windows[0];
assert_eq!(window.kind, "session", "{had:?}");
assert_eq!(
(window.percent, window.resets_at),
(3.0, Some(NOW + 5 * HOUR))
);
assert_eq!(window.severity, None, "a share the service has not graded");
assert!(window.is_active, "working against it as the service said");
assert_eq!(next.observed_at, Some(NOW));
assert_eq!(next.answered_at, Some(NOW - HOUR));
}
}
#[test]
fn a_session_never_moves_a_share_back() {
let known = held(
vec![measured("session", 22.0, Some(NOW + HOUR))],
NOW - 60,
Some(NOW - 60),
);
for behind in [
measured("five_hour", 20.0, Some(NOW + HOUR)),
measured("five_hour", 22.0, Some(NOW + HOUR)),
measured("five_hour", 95.0, Some(NOW - 4 * HOUR)),
] {
assert_eq!(
moved(&known, std::slice::from_ref(&behind), NOW),
None,
"{behind:?}"
);
}
}
#[test]
fn resets_a_second_apart_are_one_window() {
let known = held(
vec![measured("session", 22.0, Some(NOW + HOUR))],
NOW - 60,
Some(NOW - 60),
);
let ahead = moved(
&known,
&[measured("five_hour", 25.0, Some(NOW + HOUR + 1))],
NOW,
)
.expect("a share moved");
assert_eq!(shares(&ahead), [("session", 25.0)]);
assert_eq!(
ahead.windows[0].resets_at,
Some(NOW + HOUR),
"the reset is the service's"
);
}
#[test]
fn an_answer_taken_after_everything_known_is_what_the_limits_are_now() {
let known = held(
vec![
measured("session", 0.0, None),
measured("weekly_all", 100.0, Some(NOW + 33 * HOUR)),
],
NOW - HOUR,
Some(NOW - HOUR),
);
let given = answer(
vec![
measured("session", 5.0, Some(NOW + 5 * HOUR)),
measured("weekly_all", 14.0, Some(NOW + 33 * HOUR)),
],
NOW,
);
let reading = answered(Some(&known), &given, NOW);
assert_eq!(shares(&reading), [("session", 5.0), ("weekly_all", 14.0)]);
assert_eq!(reading.observed_at, Some(NOW));
assert_eq!(reading.source, Source::Live);
let unused = answer(vec![measured("weekly_all", 0.0, None)], NOW);
assert_eq!(
shares(&answered(Some(&known), &unused, NOW)),
[("weekly_all", 0.0)],
"and one that finds nothing used and no window running"
);
}
#[test]
fn an_answer_taken_before_a_session_moved_a_share_does_not_lower_it() {
let known = held(
vec![measured("weekly_all", 100.0, Some(NOW + 33 * HOUR))],
NOW - 60,
Some(NOW - 2 * HOUR),
);
for taken in [NOW - 60, NOW - HOUR] {
let given = answer(
vec![measured("weekly_all", 14.0, Some(NOW + 33 * HOUR))],
taken,
);
let reading = answered(Some(&known), &given, NOW);
assert_eq!(
shares(&reading),
[("weekly_all", 100.0)],
"taken at {taken}"
);
assert_eq!(reading.observed_at, Some(NOW - 60));
assert_eq!(reading.answered_at, Some(taken));
}
}
#[test]
fn an_answer_older_than_the_last_changes_nothing() {
let known = held(
vec![measured("session", 40.0, Some(NOW + HOUR))],
NOW,
Some(NOW),
);
let late = answer(vec![measured("session", 30.0, Some(NOW + HOUR))], NOW - 60);
assert_eq!(answered(Some(&known), &late, NOW), known);
}
#[test]
fn a_session_leaves_the_limits_it_does_not_pass_as_they_were() {
let scoped = Window {
scope: Some("Fable".into()),
..measured("weekly_scoped", 5.0, Some(NOW + 50 * HOUR))
};
let known = held(
vec![measured("session", 22.0, Some(NOW + HOUR)), scoped],
NOW - 60,
Some(NOW - 60),
);
let reading = moved(
&known,
&[measured("five_hour", 25.0, Some(NOW + HOUR))],
NOW,
)
.expect("a share moved");
assert_eq!(
shares(&reading),
[("session", 25.0), ("weekly_scoped", 5.0)]
);
}
#[test]
fn a_session_never_takes_a_limit_away() {
let known = held(
vec![
measured("session", 40.0, Some(NOW - 10)),
measured("weekly_all", 30.0, Some(NOW + 50 * HOUR)),
],
NOW - HOUR,
Some(NOW - HOUR),
);
let reading = moved(
&known,
&[measured("seven_day", 31.0, Some(NOW + 50 * HOUR))],
NOW,
)
.expect("a share moved");
assert_eq!(shares(&reading), [("session", 40.0), ("weekly_all", 31.0)]);
assert_eq!(reading.windows[0].used(NOW), 0.0);
}
#[test]
fn a_first_answer_is_the_reading() {
let given = answer(vec![measured("session", 22.0, Some(NOW + HOUR))], NOW);
assert_eq!(answered(None, &given, NOW), given);
assert_eq!(
moved(&held(Vec::new(), NOW, None), &given.windows, NOW),
None
);
}
#[test]
fn a_reading_is_stamped_when_something_moves_or_the_service_answers() {
let known = held(
vec![measured("session", 22.0, Some(NOW + HOUR))],
NOW - 600,
Some(NOW - 600),
);
let repeated = measured("five_hour", 22.0, Some(NOW + HOUR));
assert_eq!(moved(&known, &[repeated], NOW), None);
let ahead = measured("five_hour", 23.0, Some(NOW + HOUR));
assert_eq!(moved(&known, &[ahead], NOW).unwrap().observed_at, Some(NOW));
let confirmed = answer(known.windows.clone(), NOW - 5);
let reading = answered(Some(&known), &confirmed, NOW);
assert_eq!(reading.windows, known.windows);
assert_eq!(
(reading.observed_at, reading.answered_at),
(Some(NOW - 5), Some(NOW - 5)),
"an answer that finds the same confirms it"
);
assert_eq!(reading.source, Source::Live);
}
#[test]
fn a_window_past_its_reset_is_reset_by_an_answer_that_finds_nothing_used() {
let known = held(
vec![measured("session", 100.0, Some(NOW - HOUR))],
NOW - 2 * HOUR,
Some(NOW - 2 * HOUR),
);
for said in [
measured("session", 0.0, None),
measured("session", 0.0, Some(NOW - HOUR)),
measured("five_hour", 0.0, None),
] {
let given = answer(vec![said.clone()], NOW - 5);
let reading = answered(Some(&known), &given, NOW);
assert_eq!(reading.windows, std::slice::from_ref(&said), "{said:?}");
assert_eq!(
answered(Some(&reading), &given, NOW),
reading,
"a repeat changes nothing"
);
}
assert_eq!(
moved(&known, &[measured("five_hour", 0.0, None)], NOW),
None,
"a session's window with no reset says nothing about now"
);
}
}