use std::collections::HashMap;
use crate::utils::http::get_user_agent;
use std::sync::Mutex;
use std::time::{SystemTime, UNIX_EPOCH};
use once_cell::sync::Lazy;
const CACHE_TTL_MS: u64 = 60 * 60 * 1000;
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub struct OverageCreditGrantInfo {
pub available: bool,
pub eligible: bool,
pub granted: bool,
pub amount_minor_units: Option<i64>,
pub currency: Option<String>,
}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub struct CachedGrantEntry {
pub info: OverageCreditGrantInfo,
pub timestamp: u64,
}
fn now_ms() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64
}
fn is_essential_traffic_only() -> bool {
std::env::var("AI_CODE_PRIVACY_LEVEL")
.map(|v| v == "essential")
.unwrap_or(false)
}
fn get_oauth_config() -> OauthConfig {
OauthConfig {
base_api_url: std::env::var("AI_CODE_API_URL")
.unwrap_or_else(|_| "https://api.anthropic.com".to_string()),
}
}
#[derive(Debug, Clone)]
pub struct OauthConfig {
pub base_api_url: String,
}
fn get_oauth_headers(access_token: &str) -> HashMap<String, String> {
let mut headers = HashMap::new();
headers.insert(
"Authorization".to_string(),
format!("Bearer {}", access_token),
);
headers.insert("User-Agent".to_string(), get_user_agent());
headers
}
async fn prepare_api_request() -> PrepareApiResult {
PrepareApiResult {
access_token: String::new(),
org_uuid: String::new(),
}
}
#[derive(Debug, Clone)]
pub struct PrepareApiResult {
pub access_token: String,
pub org_uuid: String,
}
fn get_oauth_account_info() -> Option<OauthAccountInfo> {
None
}
#[derive(Debug, Clone)]
pub struct OauthAccountInfo {
pub organization_uuid: String,
}
fn get_global_config() -> GlobalConfig {
GlobalConfig::default()
}
#[derive(Debug, Clone, Default)]
pub struct GlobalConfig {
pub overage_credit_grant_cache: Option<HashMap<String, CachedGrantEntry>>,
}
fn save_global_config(_update: impl FnOnce(&mut GlobalConfig)) {
}
async fn fetch_overage_credit_grant() -> Option<OverageCreditGrantInfo> {
let request = prepare_api_request().await;
let config = get_oauth_config();
let url = format!(
"{}/api/oauth/organizations/{}/overage_credit_grant",
config.base_api_url, request.org_uuid
);
let client = match reqwest::Client::builder()
.timeout(std::time::Duration::from_millis(5000))
.build()
{
Ok(c) => c,
Err(e) => {
log::debug!("fetchOverageCreditGrant failed: {}", e);
return None;
}
};
let headers = get_oauth_headers(&request.access_token);
let response = client
.get(&url)
.headers(
headers
.into_iter()
.map(|(k, v)| (k.parse().unwrap(), v.parse().unwrap()))
.collect(),
)
.send()
.await;
match response {
Ok(resp) => match resp.json::<OverageCreditGrantInfo>().await {
Ok(data) => Some(data),
Err(e) => {
log::debug!("fetchOverageCreditGrant failed: {}", e);
None
}
},
Err(e) => {
log::debug!("fetchOverageCreditGrant failed: {}", e);
None
}
}
}
pub fn get_cached_overage_credit_grant() -> Option<OverageCreditGrantInfo> {
let org_id = get_oauth_account_info()?.organization_uuid;
let config = get_global_config();
let cache = config.overage_credit_grant_cache?;
let cached = cache.get(&org_id)?;
if now_ms() - cached.timestamp > CACHE_TTL_MS {
return None;
}
Some(cached.info.clone())
}
pub fn invalidate_overage_credit_grant_cache() {
let org_id = match get_oauth_account_info() {
Some(info) => info.organization_uuid,
None => return,
};
let mut config = get_global_config();
let cache = match &mut config.overage_credit_grant_cache {
Some(c) => c,
None => return,
};
if !cache.contains_key(&org_id) {
return;
}
save_global_config(|cfg| {
if let Some(ref mut c) = cfg.overage_credit_grant_cache {
c.remove(&org_id);
}
});
}
pub async fn refresh_overage_credit_grant_cache() {
if is_essential_traffic_only() {
return;
}
let org_id = match get_oauth_account_info() {
Some(info) => info.organization_uuid,
None => return,
};
let info = match fetch_overage_credit_grant().await {
Some(i) => i,
None => return,
};
save_global_config(|prev| {
let prev_cached = prev
.overage_credit_grant_cache
.as_ref()
.and_then(|c| c.get(&org_id))
.cloned();
let existing = prev_cached.as_ref().map(|c| &c.info);
let data_unchanged = existing
.map(|e| {
e.available == info.available
&& e.eligible == info.eligible
&& e.granted == info.granted
&& e.amount_minor_units == info.amount_minor_units
&& e.currency == info.currency
})
.unwrap_or(false);
if data_unchanged {
if let Some(ref prev_cached) = prev_cached {
if now_ms() - prev_cached.timestamp <= CACHE_TTL_MS {
return;
}
}
}
let entry = CachedGrantEntry {
info: if data_unchanged {
existing.cloned().unwrap_or(info)
} else {
info
},
timestamp: now_ms(),
};
let mut cache = prev.overage_credit_grant_cache.clone().unwrap_or_default();
cache.insert(org_id, entry);
prev.overage_credit_grant_cache = Some(cache);
});
}
pub fn format_grant_amount(info: &OverageCreditGrantInfo) -> Option<String> {
let amount = info.amount_minor_units?;
let currency = info.currency.as_ref()?;
if currency.to_uppercase() == "USD" {
let dollars = amount as f64 / 100.0;
if dollars.fract() == 0.0 {
Some(format!("${}", dollars as i64))
} else {
Some(format!("${:.2}", dollars))
}
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_grant_amount_usd_whole() {
let info = OverageCreditGrantInfo {
available: true,
eligible: true,
granted: false,
amount_minor_units: Some(250000),
currency: Some("USD".to_string()),
};
let result = format_grant_amount(&info);
assert_eq!(result, Some("$2500".to_string()));
}
#[test]
fn test_format_grant_amount_usd_cents() {
let info = OverageCreditGrantInfo {
available: true,
eligible: true,
granted: false,
amount_minor_units: Some(254999),
currency: Some("USD".to_string()),
};
let result = format_grant_amount(&info);
assert_eq!(result, Some("$2549.99".to_string()));
}
#[test]
fn test_format_grant_amount_no_amount() {
let info = OverageCreditGrantInfo {
available: false,
eligible: false,
granted: false,
amount_minor_units: None,
currency: None,
};
let result = format_grant_amount(&info);
assert_eq!(result, None);
}
#[test]
fn test_format_grant_amount_unknown_currency() {
let info = OverageCreditGrantInfo {
available: true,
eligible: true,
granted: false,
amount_minor_units: Some(1000),
currency: Some("EUR".to_string()),
};
let result = format_grant_amount(&info);
assert_eq!(result, None);
}
}