From 482b2640b5a5cae8b16a637319eaa497051adce9 Mon Sep 17 00:00:00 2001 From: ualtinok Date: Tue, 21 Jul 2026 18:53:59 +0200 Subject: [PATCH 1/2] cortexkit-provider-usage 0.3.0: add optional usedCount + totalCount to RateWindow Additive absolute-count fields for windows where the provider knows (or can derive) the consumed and total values. Enables human-facing UIs to show '10,336 / 40,000' alongside the percentage for richer context (e.g. qwen-cloud token-plan where the percentage is entitled but the absolute pair makes the enforcement gap legible). Both fields are skip-if-none + camelCase, so unpopulated windows serialize byte-identically to 0.2.0. --- commons | 1 + crates/cortexkit-provider-usage/Cargo.toml | 2 +- crates/cortexkit-provider-usage/src/lib.rs | 50 +++++++++++++++++++++- 3 files changed, 51 insertions(+), 2 deletions(-) create mode 120000 commons diff --git a/commons b/commons new file mode 120000 index 0000000..00697f0 --- /dev/null +++ b/commons @@ -0,0 +1 @@ +/Users/ufukaltinok/Work/Projects/CortexKit/commons \ No newline at end of file diff --git a/crates/cortexkit-provider-usage/Cargo.toml b/crates/cortexkit-provider-usage/Cargo.toml index f4962d7..c2fc221 100644 --- a/crates/cortexkit-provider-usage/Cargo.toml +++ b/crates/cortexkit-provider-usage/Cargo.toml @@ -11,7 +11,7 @@ # says; this crate makes no guarantee about how a producer derived the numbers. [package] name = "cortexkit-provider-usage" -version = "0.2.0" +version = "0.3.0" edition.workspace = true license.workspace = true repository.workspace = true diff --git a/crates/cortexkit-provider-usage/src/lib.rs b/crates/cortexkit-provider-usage/src/lib.rs index b6a4d29..a8470bd 100644 --- a/crates/cortexkit-provider-usage/src/lib.rs +++ b/crates/cortexkit-provider-usage/src/lib.rs @@ -22,7 +22,8 @@ //! //! # Serialization contract consumers depend on //! - camelCase keys (`usedPercent`, `resetsAt`, `windowMinutes`, -//! `extraRateWindows`, `rawUsedPercent`, `accountInfo`, `savedResets`). +//! `extraRateWindows`, `rawUsedPercent`, `accountInfo`, `savedResets`, +//! `usedCount`, `totalCount`). //! - A healthy entry MUST NOT carry `error` (consumers skip truthy-`error` //! entries), so it is omitted when absent. //! - A window is emitted when it has a `usedPercent`; `resetsAt` is OPTIONAL and @@ -53,6 +54,15 @@ pub struct RateWindow { /// the consumer then paces on utilization alone rather than a burn rate. #[serde(skip_serializing_if = "Option::is_none")] pub window_minutes: Option, + /// Absolute consumed count in the window (e.g. tokens, requests). Present + /// only when the provider reports or derives it; human-facing UIs can show + /// "10,336 / 40,000" alongside the percentage for richer context. + #[serde(skip_serializing_if = "Option::is_none", default)] + pub used_count: Option, + /// Absolute total cap for the window. Present alongside `used_count` when + /// the provider knows the ceiling; omitted otherwise. + #[serde(skip_serializing_if = "Option::is_none", default)] + pub total_count: Option, } /// A per-model window bundled under one account (e.g. Antigravity's Geminis). @@ -208,6 +218,8 @@ mod tests { raw_used_percent: None, resets_at: None, window_minutes: Some(300), + used_count: None, + total_count: None, }), ..Default::default() }, @@ -257,6 +269,8 @@ mod tests { raw_used_percent: None, resets_at: Some("2026-07-20T00:00:00Z".to_string()), window_minutes: Some(10080), + used_count: None, + total_count: None, }; let json = serde_json::to_string(&unrelaxed).unwrap(); assert!( @@ -269,6 +283,8 @@ mod tests { raw_used_percent: Some(70.0), resets_at: Some("2026-07-20T00:00:00Z".to_string()), window_minutes: Some(10080), + used_count: None, + total_count: None, }; let json = serde_json::to_string(&relaxed).unwrap(); assert!(json.contains("\"rawUsedPercent\":70.0")); @@ -303,4 +319,36 @@ mod tests { let back: ProviderUsage = serde_json::from_str(&json).unwrap(); assert_eq!(back, entry); } + + #[test] + fn used_count_and_total_count_are_camel_case_and_omitted_when_absent() { + let window = RateWindow { + used_percent: 25.8, + raw_used_percent: None, + resets_at: Some("2026-07-26T14:09:00Z".to_string()), + window_minutes: Some(10080), + used_count: None, + total_count: None, + }; + let json = serde_json::to_string(&window).unwrap(); + assert!( + !json.contains("usedCount"), + "absent used_count must be omitted" + ); + assert!( + !json.contains("totalCount"), + "absent total_count must be omitted" + ); + + let enriched = RateWindow { + used_count: Some(10336.0), + total_count: Some(40000.0), + ..window + }; + let json = serde_json::to_string(&enriched).unwrap(); + assert!(json.contains("\"usedCount\":10336.0")); + assert!(json.contains("\"totalCount\":40000.0")); + let back: RateWindow = serde_json::from_str(&json).unwrap(); + assert_eq!(back, enriched); + } } From 99ca881c76cc7a763acb5d72894b4c771ea904f4 Mon Sep 17 00:00:00 2001 From: ualtinok Date: Tue, 21 Jul 2026 19:32:13 +0200 Subject: [PATCH 2/2] drop accidental self-referencing symlink --- commons | 1 - 1 file changed, 1 deletion(-) delete mode 120000 commons diff --git a/commons b/commons deleted file mode 120000 index 00697f0..0000000 --- a/commons +++ /dev/null @@ -1 +0,0 @@ -/Users/ufukaltinok/Work/Projects/CortexKit/commons \ No newline at end of file