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::{Deserialize, 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
66/// Stable semantics for one provider-specific quota window.
67///
68/// [`UsageWindow`] intentionally stays close to the upstream response. This
69/// descriptor supplies the extra identity and accounting semantics a host
70/// needs to match the same window across refreshes and completed periods.
71#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
72pub struct UsageWindowDescriptor {
73    /// Stable within one channel. Hosts that combine channels should namespace
74    /// this key with [`Channel::id`](crate::Channel::id).
75    pub key: String,
76    /// Which locally recorded traffic is governed by this window.
77    pub scope: UsageWindowScope,
78    /// The upstream unit represented by `used`, `limit`, or `used_percent`.
79    pub meter: UsageWindowMeter,
80    /// Inclusive period start, when it can be established, in unix seconds.
81    #[serde(skip_serializing_if = "Option::is_none")]
82    pub period_start_unix: Option<i64>,
83    /// How the period boundary was established.
84    pub boundary_source: UsageWindowBoundarySource,
85    /// Whether the complete boundary is exact, derived, partial, or unknown.
86    pub boundary_confidence: UsageWindowBoundaryConfidence,
87}
88
89impl UsageWindowDescriptor {
90    /// Conservative descriptor for a normalized window. Channel adapters can
91    /// refine its scope and meter through [`Channel::describe_usage_window`].
92    ///
93    /// [`Channel::describe_usage_window`]: crate::Channel::describe_usage_window
94    pub fn from_window(window: &UsageWindow) -> Self {
95        let period_start_unix = window
96            .resets_at_unix
97            .zip(window.window_seconds)
98            .filter(|(_, seconds)| *seconds > 0)
99            .map(|(reset, seconds)| reset.saturating_sub(seconds));
100        let (boundary_source, boundary_confidence) = if period_start_unix.is_some() {
101            (
102                UsageWindowBoundarySource::ResetAndDuration,
103                UsageWindowBoundaryConfidence::Exact,
104            )
105        } else if window.resets_at_unix.is_some() || window.resets_at.is_some() {
106            (
107                UsageWindowBoundarySource::ResetOnly,
108                UsageWindowBoundaryConfidence::Partial,
109            )
110        } else {
111            (
112                UsageWindowBoundarySource::Unknown,
113                UsageWindowBoundaryConfidence::Unknown,
114            )
115        };
116        Self {
117            key: window.name.clone(),
118            scope: UsageWindowScope::Unknown,
119            meter: UsageWindowMeter::Opaque,
120            period_start_unix,
121            boundary_source,
122            boundary_confidence,
123        }
124    }
125
126    pub fn scope(mut self, scope: UsageWindowScope) -> Self {
127        self.scope = scope;
128        self
129    }
130
131    pub fn meter(mut self, meter: UsageWindowMeter) -> Self {
132        self.meter = meter;
133        self
134    }
135
136    pub fn period_start(
137        mut self,
138        unix: i64,
139        source: UsageWindowBoundarySource,
140        confidence: UsageWindowBoundaryConfidence,
141    ) -> Self {
142        self.period_start_unix = Some(unix);
143        self.boundary_source = source;
144        self.boundary_confidence = confidence;
145        self
146    }
147}
148
149/// Local-usage scope governed by an upstream quota window.
150#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
151#[serde(tag = "kind", rename_all = "snake_case")]
152pub enum UsageWindowScope {
153    All,
154    Models { models: Vec<String> },
155    Feature { feature: String },
156    Unknown,
157}
158
159/// Upstream accounting unit for a quota window.
160#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
161#[serde(rename_all = "snake_case")]
162pub enum UsageWindowMeter {
163    Tokens,
164    Requests,
165    Credits,
166    Usd,
167    Opaque,
168}
169
170/// Origin of the normalized period boundary.
171#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
172#[serde(rename_all = "snake_case")]
173pub enum UsageWindowBoundarySource {
174    /// The upstream supplied an explicit start boundary.
175    Upstream,
176    /// The upstream supplied reset time and window duration.
177    ResetAndDuration,
178    /// The adapter derived the start from a documented/known window duration.
179    KnownWindow,
180    /// Only the reset/end boundary is known.
181    ResetOnly,
182    Unknown,
183}
184
185/// Confidence in the normalized period boundary.
186#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
187#[serde(rename_all = "snake_case")]
188pub enum UsageWindowBoundaryConfidence {
189    Exact,
190    Derived,
191    Partial,
192    Unknown,
193}
194
195impl UsageWindow {
196    /// A percentage-based window (`used_percent` in \[0, 100\]).
197    pub fn percent(name: impl Into<String>, used_percent: f64) -> Self {
198        Self {
199            name: name.into(),
200            used_percent: Some(used_percent),
201            ..Default::default()
202        }
203    }
204
205    /// An absolute-count window (`used` / `limit`).
206    pub fn amounts(name: impl Into<String>, used: f64, limit: f64) -> Self {
207        Self {
208            name: name.into(),
209            used: Some(used),
210            limit: Some(limit),
211            ..Default::default()
212        }
213    }
214
215    /// Attach an ISO-8601 reset timestamp.
216    pub fn resets_iso(mut self, iso: impl Into<String>) -> Self {
217        self.resets_at = Some(iso.into());
218        self
219    }
220
221    /// Attach a unix-seconds reset timestamp.
222    pub fn resets_unix(mut self, unix: i64) -> Self {
223        self.resets_at_unix = Some(unix);
224        self
225    }
226
227    /// Attach the window length in seconds.
228    pub fn window_secs(mut self, seconds: i64) -> Self {
229        self.window_seconds = Some(seconds);
230        self
231    }
232
233    /// Attach a display label for generated / scoped windows.
234    pub fn label(mut self, label: impl Into<String>) -> Self {
235        self.label = Some(label.into());
236        self
237    }
238}
239
240/// Money / credit balance and on-demand overage, where the channel exposes it
241/// (codex credits, claudecode `extra_usage`).
242#[derive(Debug, Clone, Default, Serialize)]
243pub struct UsageCredits {
244    #[serde(skip_serializing_if = "Option::is_none")]
245    pub has_credits: Option<bool>,
246    #[serde(skip_serializing_if = "Option::is_none")]
247    pub unlimited: Option<bool>,
248    /// Formatted balance string when the provider gives one (codex `balance`).
249    #[serde(skip_serializing_if = "Option::is_none")]
250    pub balance: Option<String>,
251    /// Credits consumed, normalized to the provider's display unit.
252    #[serde(skip_serializing_if = "Option::is_none")]
253    pub used_credits: Option<f64>,
254    /// Spending cap, normalized to the provider's display unit.
255    #[serde(skip_serializing_if = "Option::is_none")]
256    pub monthly_limit: Option<f64>,
257    #[serde(skip_serializing_if = "Option::is_none")]
258    pub currency: Option<String>,
259}
260
261#[derive(Debug, Clone, Default, Serialize)]
262pub struct RateLimitResetCredits {
263    pub available_count: i64,
264}
265
266#[derive(Debug, Clone, Serialize)]
267pub struct RateLimitResetCreditConsumeResponse {
268    pub outcome: RateLimitResetCreditConsumeOutcome,
269    #[serde(skip_serializing_if = "Option::is_none")]
270    pub windows_reset: Option<i64>,
271    pub raw: Value,
272}
273
274#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
275#[serde(rename_all = "snake_case")]
276pub enum RateLimitResetCreditConsumeOutcome {
277    Reset,
278    NothingToReset,
279    NoCredit,
280    AlreadyRedeemed,
281}