| use serde::{Deserialize, Serialize}; |
| use serde_json::json; |
| use crate::models::QuotaData; |
|
|
| const QUOTA_API_URL: &str = "https://cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels"; |
| const USER_AGENT: &str = "antigravity/1.11.3 Darwin/arm64"; |
| const CLOUD_CODE_BASE_URL: &str = "https://cloudcode-pa.googleapis.com"; |
|
|
| #[derive(Debug, Serialize, Deserialize)] |
| struct QuotaResponse { |
| models: std::collections::HashMap<String, ModelInfo>, |
| } |
|
|
| #[derive(Debug, Serialize, Deserialize)] |
| struct ModelInfo { |
| #[serde(rename = "quotaInfo")] |
| quota_info: Option<QuotaInfo>, |
| } |
|
|
| #[derive(Debug, Serialize, Deserialize)] |
| struct QuotaInfo { |
| #[serde(rename = "remainingFraction")] |
| remaining_fraction: Option<f64>, |
| #[serde(rename = "resetTime")] |
| reset_time: Option<String>, |
| } |
|
|
| #[derive(Debug, Deserialize)] |
| struct LoadProjectResponse { |
| #[serde(rename = "cloudaicompanionProject")] |
| project_id: Option<String>, |
| #[serde(rename = "currentTier")] |
| current_tier: Option<Tier>, |
| #[serde(rename = "paidTier")] |
| paid_tier: Option<Tier>, |
| } |
|
|
| #[derive(Debug, Deserialize)] |
| struct Tier { |
| id: Option<String>, |
| #[serde(rename = "quotaTier")] |
| #[allow(dead_code)] |
| quota_tier: Option<String>, |
| #[allow(dead_code)] |
| name: Option<String>, |
| #[allow(dead_code)] |
| slug: Option<String>, |
| } |
|
|
| |
| fn create_client() -> reqwest::Client { |
| crate::utils::http::create_client(15) |
| } |
|
|
| |
| async fn fetch_project_id(access_token: &str, email: &str) -> (Option<String>, Option<String>) { |
| let client = create_client(); |
| let meta = json!({"metadata": {"ideType": "ANTIGRAVITY"}}); |
|
|
| let res = client |
| .post(format!("{}/v1internal:loadCodeAssist", CLOUD_CODE_BASE_URL)) |
| .header(reqwest::header::AUTHORIZATION, format!("Bearer {}", access_token)) |
| .header(reqwest::header::CONTENT_TYPE, "application/json") |
| .header(reqwest::header::USER_AGENT, "antigravity/windows/amd64") |
| .json(&meta) |
| .send() |
| .await; |
|
|
| match res { |
| Ok(res) => { |
| if res.status().is_success() { |
| if let Ok(data) = res.json::<LoadProjectResponse>().await { |
| let project_id = data.project_id.clone(); |
|
|
| |
| let subscription_tier = data.paid_tier |
| .and_then(|t| t.id) |
| .or_else(|| data.current_tier.and_then(|t| t.id)); |
|
|
| if let Some(ref tier) = subscription_tier { |
| crate::modules::logger::log_info(&format!( |
| "[{}] Subscription identified: {}", email, tier |
| )); |
| } |
|
|
| return (project_id, subscription_tier); |
| } |
| } else { |
| crate::modules::logger::log_warn(&format!( |
| "[{}] loadCodeAssist failed: Status: {}", email, res.status() |
| )); |
| } |
| } |
| Err(e) => { |
| crate::modules::logger::log_error(&format!("[{}] loadCodeAssist network error: {}", email, e)); |
| } |
| } |
|
|
| (None, None) |
| } |
|
|
| |
| pub async fn fetch_quota(access_token: &str, email: &str) -> crate::error::AppResult<(QuotaData, Option<String>)> { |
| fetch_quota_inner(access_token, email).await |
| } |
|
|
| |
| pub async fn fetch_quota_inner(access_token: &str, email: &str) -> crate::error::AppResult<(QuotaData, Option<String>)> { |
| use crate::error::AppError; |
|
|
| |
| let (project_id, subscription_tier) = fetch_project_id(access_token, email).await; |
|
|
| let final_project_id = project_id.as_deref().unwrap_or("bamboo-precept-lgxtn"); |
|
|
| let client = create_client(); |
| let payload = json!({ |
| "project": final_project_id |
| }); |
|
|
| let url = QUOTA_API_URL; |
| let max_retries = 3; |
| let mut last_error: Option<AppError> = None; |
|
|
| for attempt in 1..=max_retries { |
| match client |
| .post(url) |
| .bearer_auth(access_token) |
| .header("User-Agent", USER_AGENT) |
| .json(&json!(payload)) |
| .send() |
| .await |
| { |
| Ok(response) => { |
| |
| if response.error_for_status_ref().is_err() { |
| let status = response.status(); |
|
|
| |
| if status == reqwest::StatusCode::FORBIDDEN { |
| crate::modules::logger::log_warn("Account forbidden (403), marking as forbidden status"); |
| let mut q = QuotaData::new(); |
| q.is_forbidden = true; |
| q.subscription_tier = subscription_tier.clone(); |
| return Ok((q, project_id.clone())); |
| } |
|
|
| |
| if attempt < max_retries { |
| let text = response.text().await.unwrap_or_default(); |
| crate::modules::logger::log_warn(&format!("API error: {} - {} (attempt {}/{})", status, text, attempt, max_retries)); |
| last_error = Some(AppError::Unknown(format!("HTTP {} - {}", status, text))); |
| tokio::time::sleep(std::time::Duration::from_secs(1)).await; |
| continue; |
| } else { |
| let text = response.text().await.unwrap_or_default(); |
| return Err(AppError::Unknown(format!("API error: {} - {}", status, text))); |
| } |
| } |
|
|
| let quota_response: QuotaResponse = response |
| .json() |
| .await |
| .map_err(AppError::Network)?; |
|
|
| let mut quota_data = QuotaData::new(); |
|
|
| tracing::debug!("Quota API returned {} models", quota_response.models.len()); |
|
|
| for (name, info) in quota_response.models { |
| if let Some(quota_info) = info.quota_info { |
| let percentage = quota_info.remaining_fraction |
| .map(|f| (f * 100.0) as i32) |
| .unwrap_or(0); |
|
|
| let reset_time = quota_info.reset_time.unwrap_or_default(); |
|
|
| |
| if name.contains("gemini") || name.contains("claude") { |
| quota_data.add_model(name, percentage, reset_time); |
| } |
| } |
| } |
|
|
| |
| quota_data.subscription_tier = subscription_tier.clone(); |
|
|
| return Ok((quota_data, project_id.clone())); |
| }, |
| Err(e) => { |
| crate::modules::logger::log_warn(&format!("Request failed: {} (attempt {}/{})", e, attempt, max_retries)); |
| last_error = Some(AppError::Network(e)); |
| if attempt < max_retries { |
| tokio::time::sleep(std::time::Duration::from_secs(1)).await; |
| } |
| } |
| } |
| } |
|
|
| Err(last_error.unwrap_or_else(|| AppError::Unknown("Quota query failed".to_string()))) |
| } |
|
|
| |
| #[allow(dead_code)] |
| pub async fn fetch_all_quotas(accounts: Vec<(String, String)>) -> Vec<(String, crate::error::AppResult<QuotaData>)> { |
| let mut results = Vec::new(); |
|
|
| for (account_id, access_token) in accounts { |
| let result = fetch_quota(&access_token, &account_id).await.map(|(q, _)| q); |
| results.push((account_id, result)); |
| } |
|
|
| results |
| } |
|
|