usage_monitor_cli/provider/
mod.rs1pub 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#[derive(Debug, Clone)]
41pub struct ProviderContext {
42 pub config: HashMap<String, String>,
44 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
69pub 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
87pub 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#[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#[async_trait]
111pub trait UsageProvider: Send + Sync {
112 fn metadata(&self) -> &ProviderMetadata;
114
115 async fn fetch_usage(&self, ctx: &ProviderContext) -> Result<UsageSnapshot, SpendPanelError>;
117
118 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}