Skip to main content

gproxy_channel_api/
usage.rs

1//! Per-credential upstream usage / quota snapshot (§17). OAuth subscription
2//! channels expose a usage endpoint that reports the account's rate-limit
3//! windows and (where applicable) credit balance for a single credential. Each
4//! channel parses its provider-specific response into this shared shape; the
5//! raw upstream JSON is retained in [`UsageSnapshot::raw`] so the admin UI can
6//! surface fields this normalization does not model.
7//!
8//! The fetch is driven exactly like a credential refresh (resolve the
9//! credential's pooled client → send [`Channel::prepare_usage_request`] →
10//! [`Channel::parse_usage`]); the host owns transport and persistence.
11//!
12//! [`Channel::prepare_usage_request`]: crate::Channel::prepare_usage_request
13//! [`Channel::parse_usage`]: crate::Channel::parse_usage
14
15use serde::Serialize;
16use serde_json::Value;
17
18/// Normalized usage/quota snapshot for one credential.
19#[derive(Debug, Clone, Default, Serialize)]
20pub struct UsageSnapshot {
21    /// Plan / subscription label when the provider reports one (`"pro"`,
22    /// `"KIRO PRO+"`, `"business"`, …).
23    #[serde(skip_serializing_if = "Option::is_none")]
24    pub plan: Option<String>,
25    /// Rate-limit / quota windows (5h + 7d, primary/secondary, per-model, per
26    /// feature). Empty when the provider only reports credits.
27    pub windows: Vec<UsageWindow>,
28    /// Money / credit balance + overage, when the channel exposes it.
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub credits: Option<UsageCredits>,
31    /// Earned rate-limit reset credits, when the upstream exposes them.
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub rate_limit_reset_credits: Option<RateLimitResetCredits>,
34    /// The original upstream response JSON, for display / debugging.
35    pub raw: Value,
36}
37
38/// A single rate-limit or quota window. Providers report usage either as a
39/// percentage (`used_percent`) or as absolute counts (`used` / `limit`); a
40/// window carries whichever the upstream gives. Reset time is kept verbatim as
41/// an ISO-8601 string (`resets_at`) and/or unix seconds (`resets_at_unix`).
42#[derive(Debug, Clone, Default, Serialize)]
43pub struct UsageWindow {
44    /// Window id (`"five_hour"`, `"seven_day"`, `"primary"`, a model id, …).
45    pub name: String,
46    /// Human-readable upstream label when `name` is generated.
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub label: Option<String>,
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub used_percent: Option<f64>,
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub used: Option<f64>,
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub limit: Option<f64>,
55    /// ISO-8601 reset timestamp, when the provider gives one.
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub resets_at: Option<String>,
58    /// Unix-seconds reset timestamp, when the provider gives one.
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub resets_at_unix: Option<i64>,
61    /// Window length in seconds, when known.
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub window_seconds: Option<i64>,
64}
65
66impl UsageWindow {
67    /// A percentage-based window (`used_percent` in \[0, 100\]).
68    pub fn percent(name: impl Into<String>, used_percent: f64) -> Self {
69        Self {
70            name: name.into(),
71            used_percent: Some(used_percent),
72            ..Default::default()
73        }
74    }
75
76    /// An absolute-count window (`used` / `limit`).
77    pub fn amounts(name: impl Into<String>, used: f64, limit: f64) -> Self {
78        Self {
79            name: name.into(),
80            used: Some(used),
81            limit: Some(limit),
82            ..Default::default()
83        }
84    }
85
86    /// Attach an ISO-8601 reset timestamp.
87    pub fn resets_iso(mut self, iso: impl Into<String>) -> Self {
88        self.resets_at = Some(iso.into());
89        self
90    }
91
92    /// Attach a unix-seconds reset timestamp.
93    pub fn resets_unix(mut self, unix: i64) -> Self {
94        self.resets_at_unix = Some(unix);
95        self
96    }
97
98    /// Attach the window length in seconds.
99    pub fn window_secs(mut self, seconds: i64) -> Self {
100        self.window_seconds = Some(seconds);
101        self
102    }
103
104    /// Attach a display label for generated / scoped windows.
105    pub fn label(mut self, label: impl Into<String>) -> Self {
106        self.label = Some(label.into());
107        self
108    }
109}
110
111/// Money / credit balance and on-demand overage, where the channel exposes it
112/// (codex credits, claudecode `extra_usage`).
113#[derive(Debug, Clone, Default, Serialize)]
114pub struct UsageCredits {
115    #[serde(skip_serializing_if = "Option::is_none")]
116    pub has_credits: Option<bool>,
117    #[serde(skip_serializing_if = "Option::is_none")]
118    pub unlimited: Option<bool>,
119    /// Formatted balance string when the provider gives one (codex `balance`).
120    #[serde(skip_serializing_if = "Option::is_none")]
121    pub balance: Option<String>,
122    /// Credits consumed, normalized to the provider's display unit.
123    #[serde(skip_serializing_if = "Option::is_none")]
124    pub used_credits: Option<f64>,
125    /// Spending cap, normalized to the provider's display unit.
126    #[serde(skip_serializing_if = "Option::is_none")]
127    pub monthly_limit: Option<f64>,
128    #[serde(skip_serializing_if = "Option::is_none")]
129    pub currency: Option<String>,
130}
131
132#[derive(Debug, Clone, Default, Serialize)]
133pub struct RateLimitResetCredits {
134    pub available_count: i64,
135}
136
137#[derive(Debug, Clone, Serialize)]
138pub struct RateLimitResetCreditConsumeResponse {
139    pub outcome: RateLimitResetCreditConsumeOutcome,
140    #[serde(skip_serializing_if = "Option::is_none")]
141    pub windows_reset: Option<i64>,
142    pub raw: Value,
143}
144
145#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
146#[serde(rename_all = "snake_case")]
147pub enum RateLimitResetCreditConsumeOutcome {
148    Reset,
149    NothingToReset,
150    NoCredit,
151    AlreadyRedeemed,
152}