Skip to main content

usage_monitor_cli/provider/
mod.rs

1pub mod abacus;
2pub mod anthropic;
3pub mod antigravity;
4pub mod claude;
5pub mod codex;
6pub mod copilot;
7pub mod cursor;
8pub mod deepgram;
9pub mod deepseek;
10pub mod devin;
11pub mod elevenlabs;
12pub mod gemini;
13pub mod gemini_oauth;
14pub mod grok;
15pub mod groq;
16pub mod kimi;
17pub mod kimik2;
18pub mod llmproxy;
19pub mod minimax;
20pub mod mistral;
21pub mod moonshot;
22pub mod ollama;
23pub mod openai;
24pub mod opencode_go;
25pub mod openrouter;
26pub mod perplexity;
27pub mod proto;
28pub mod registry;
29pub mod venice;
30pub mod windsurf;
31pub mod zai;
32
33use async_trait::async_trait;
34use std::collections::HashMap;
35
36use crate::error::SpendPanelError;
37use crate::model::UsageSnapshot;
38
39/// Context for fetching from a provider.
40#[derive(Debug, Clone)]
41pub struct ProviderContext {
42    /// Provider-specific configuration (key-value).
43    pub config: HashMap<String, String>,
44    /// Timeout in seconds.
45    pub timeout_secs: u64,
46}
47
48impl Default for ProviderContext {
49    fn default() -> Self {
50        Self::new()
51    }
52}
53
54impl ProviderContext {
55    pub fn new() -> Self {
56        Self {
57            config: HashMap::new(),
58            timeout_secs: 30,
59        }
60    }
61
62    pub fn with_api_key(key: impl Into<String>) -> Self {
63        let mut ctx = Self::new();
64        ctx.config.insert("api_key".into(), key.into());
65        ctx
66    }
67}
68
69/// Expands a user-configured credentials path: trims surrounding whitespace
70/// and expands a leading `~` (or `~/`) to `$HOME`. Falls back to the raw
71/// (trimmed) value when `HOME` is unavailable.
72pub fn expand_credentials_path(raw: &str) -> std::path::PathBuf {
73    let trimmed = raw.trim();
74    if let Some(rest) = trimmed.strip_prefix('~') {
75        if let Some(home) = std::env::var_os("HOME") {
76            let mut path = std::path::PathBuf::from(home);
77            let rest = rest.strip_prefix('/').unwrap_or(rest);
78            if !rest.is_empty() {
79                path.push(rest);
80            }
81            return path;
82        }
83    }
84    std::path::PathBuf::from(trimmed)
85}
86
87/// Resolves a configured credentials path to a file: expands `~`/whitespace
88/// and, when the result is an existing directory, joins `file_name`
89/// (e.g. `~/.codex-work` → `~/.codex-work/auth.json`).
90pub fn resolve_credentials_file(raw: &str, file_name: &str) -> std::path::PathBuf {
91    let path = expand_credentials_path(raw);
92    if path.is_dir() {
93        path.join(file_name)
94    } else {
95        path
96    }
97}
98
99/// Provider metadata.
100#[derive(Debug, Clone)]
101pub struct ProviderMetadata {
102    pub id: &'static str,
103    pub name: &'static str,
104    pub description: &'static str,
105    pub auth_methods: &'static [&'static str],
106    pub website: Option<&'static str>,
107}
108
109/// Trait every usage provider must implement.
110#[async_trait]
111pub trait UsageProvider: Send + Sync {
112    /// Returns the provider metadata.
113    fn metadata(&self) -> &ProviderMetadata;
114
115    /// Fetches usage data.
116    async fn fetch_usage(&self, ctx: &ProviderContext) -> Result<UsageSnapshot, SpendPanelError>;
117
118    /// Whether credentials for this provider are detectable on this machine
119    /// (used to auto-enable providers without an explicit toggle).
120    fn detect_credentials(&self) -> bool {
121        false
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128
129    #[test]
130    fn test_provider_context_default() {
131        let ctx = ProviderContext::new();
132        assert!(ctx.config.is_empty());
133        assert_eq!(ctx.timeout_secs, 30);
134    }
135
136    #[test]
137    fn test_provider_context_with_api_key() {
138        let ctx = ProviderContext::with_api_key("sk-test");
139        assert_eq!(ctx.config.get("api_key").unwrap(), "sk-test");
140    }
141
142    #[test]
143    fn test_expand_credentials_path_trims_and_expands_tilde() {
144        let home = std::env::var_os("HOME").expect("HOME set for test");
145        let expanded = expand_credentials_path("  ~/.codex-plus2/auth.json  ");
146        assert_eq!(
147            expanded,
148            std::path::PathBuf::from(home).join(".codex-plus2/auth.json")
149        );
150        assert_eq!(
151            expand_credentials_path("/tmp/auth.json "),
152            std::path::PathBuf::from("/tmp/auth.json")
153        );
154    }
155
156    #[test]
157    fn test_resolve_credentials_file_accepts_directory() {
158        let dir = std::env::temp_dir().join(format!("usage-monitor-creds-{}", std::process::id()));
159        std::fs::create_dir_all(&dir).unwrap();
160        let resolved = resolve_credentials_file(dir.to_str().unwrap(), "auth.json");
161        assert_eq!(resolved, dir.join("auth.json"));
162        std::fs::remove_dir_all(&dir).ok();
163    }
164}