Skip to main content

ai_usagebar/openrouter/
types.rs

1//! Wire types for OpenRouter's `/api/v1/credits` and `/api/v1/key`.
2//!
3//! Both endpoints wrap their payload in `{ "data": { ... } }`, hence the
4//! generic [`OrEnvelope`] wrapper.
5
6use serde::Deserialize;
7
8use crate::usage::OpenRouterSnapshot;
9
10/// Wrapper used by all OpenRouter v1 endpoints.
11#[derive(Debug, Clone, Deserialize)]
12pub struct OrEnvelope<T> {
13    pub data: T,
14}
15
16/// `GET /api/v1/credits` — total_credits and total_usage, both USD doubles.
17#[derive(Debug, Clone, Deserialize, Default)]
18#[serde(default)]
19pub struct CreditsData {
20    pub total_credits: f64,
21    pub total_usage: f64,
22}
23
24/// `GET /api/v1/key` — per-key usage and free-tier flag.
25#[derive(Debug, Clone, Deserialize, Default)]
26#[serde(default)]
27pub struct KeyData {
28    pub label: String,
29    pub limit: Option<f64>,
30    pub limit_remaining: Option<f64>,
31    pub usage: f64,
32    pub usage_daily: f64,
33    pub usage_weekly: f64,
34    pub usage_monthly: f64,
35    pub is_free_tier: bool,
36}
37
38/// Combine the two endpoint responses into the canonical snapshot.
39pub fn combine(credits: CreditsData, key: KeyData) -> OpenRouterSnapshot {
40    let label = if key.label.is_empty() {
41        "OpenRouter".to_string()
42    } else {
43        format!("OpenRouter — {}", key.label)
44    };
45    OpenRouterSnapshot {
46        label,
47        total_credits: credits.total_credits,
48        total_usage: credits.total_usage,
49        usage_daily: key.usage_daily,
50        usage_weekly: key.usage_weekly,
51        usage_monthly: key.usage_monthly,
52        is_free_tier: key.is_free_tier,
53        limit: key.limit,
54        limit_remaining: key.limit_remaining,
55    }
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61
62    #[test]
63    fn parses_credits_envelope() {
64        let raw = r#"{"data":{"total_credits":100.0,"total_usage":25.5}}"#;
65        let env: OrEnvelope<CreditsData> = serde_json::from_str(raw).unwrap();
66        assert_eq!(env.data.total_credits, 100.0);
67        assert_eq!(env.data.total_usage, 25.5);
68    }
69
70    #[test]
71    fn parses_key_envelope_with_nulls() {
72        let raw = r#"{"data":{
73            "label":"my-key",
74            "limit":null,"limit_remaining":null,
75            "usage":12.34,"usage_daily":1.0,"usage_weekly":3.0,"usage_monthly":12.0,
76            "is_free_tier":false
77        }}"#;
78        let env: OrEnvelope<KeyData> = serde_json::from_str(raw).unwrap();
79        assert_eq!(env.data.label, "my-key");
80        assert!(env.data.limit.is_none());
81        assert_eq!(env.data.usage_monthly, 12.0);
82        assert!(!env.data.is_free_tier);
83    }
84
85    #[test]
86    fn combine_builds_snapshot() {
87        let c = CreditsData {
88            total_credits: 100.0,
89            total_usage: 30.0,
90        };
91        let k = KeyData {
92            label: "key-A".into(),
93            limit: Some(50.0),
94            limit_remaining: Some(20.0),
95            usage: 30.0,
96            usage_daily: 1.0,
97            usage_weekly: 5.0,
98            usage_monthly: 30.0,
99            is_free_tier: false,
100        };
101        let snap = combine(c, k);
102        assert_eq!(snap.label, "OpenRouter — key-A");
103        assert!((snap.balance() - 70.0).abs() < 1e-9);
104        assert_eq!(snap.consumed_pct(), 30);
105        assert_eq!(snap.usage_monthly, 30.0);
106    }
107
108    #[test]
109    fn combine_with_empty_label() {
110        let snap = combine(CreditsData::default(), KeyData::default());
111        assert_eq!(snap.label, "OpenRouter");
112    }
113
114    #[test]
115    fn consumed_pct_handles_zero_credits() {
116        let s = OpenRouterSnapshot {
117            label: "x".into(),
118            total_credits: 0.0,
119            total_usage: 5.0,
120            usage_daily: 0.0,
121            usage_weekly: 0.0,
122            usage_monthly: 0.0,
123            is_free_tier: true,
124            limit: None,
125            limit_remaining: None,
126        };
127        assert_eq!(s.consumed_pct(), 0);
128    }
129}