use super::{
ProviderReport, QuotaWindow,
common::{as_f64, humanize_key, parse_reset_ms, used_limit_percent},
};
use crate::{
core::{auth, model_resolver},
utils::http::get_json,
};
use reqwest::Client;
use serde_json::Value;
pub(super) const COPILOT_TOKEN_PATH: &str = "/copilot_internal/v2/token";
pub(super) async fn fetch_copilot(client: &Client) -> Option<ProviderReport> {
let cred = auth::read_oauth_credential("github-copilot")?;
let github_token = cred.refresh.clone();
if github_token.trim().is_empty() {
return None;
}
let domain = cred
.enterprise_url
.clone()
.unwrap_or_else(|| "github.com".to_string());
Some(fetch_copilot_at(client, &domain, &github_token).await)
}
pub(super) async fn fetch_copilot_at(
client: &Client,
domain: &str,
github_token: &str,
) -> ProviderReport {
let url = format!("https://api.{domain}{COPILOT_TOKEN_PATH}");
let mut headers = vec![("Authorization".to_string(), format!("token {github_token}"))];
headers.extend(model_resolver::provider_headers("github-copilot"));
match get_json(client, &url, &headers).await {
Ok(json) => parse_copilot(&json),
Err(err) => ProviderReport::failed("github-copilot", err),
}
}
pub(super) fn parse_copilot(json: &Value) -> ProviderReport {
let mut report = ProviderReport::new("github-copilot");
if let Some(sku) = json.get("sku").and_then(Value::as_str) {
report.extra.push(("Plan".to_string(), sku.to_string()));
}
let reset = json.get("limited_user_reset_date").and_then(parse_reset_ms);
if let Some(quotas) = json.get("limited_user_quotas").and_then(Value::as_object) {
for (name, value) in quotas {
let Some(object) = value.as_object() else {
continue;
};
let used = object.get("used").and_then(as_f64).unwrap_or(0.0);
let limit = object.get("limit").and_then(as_f64).unwrap_or(0.0);
let Some(percent) = used_limit_percent(used, limit) else {
continue;
};
report.windows.push(QuotaWindow {
name: humanize_key(name),
percent,
resets_at: reset,
});
report.extra.push((
humanize_key(name),
format!("{} / {} used", used as u64, limit as u64),
));
}
}
report
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_copilot_reads_quotas_and_reset_date() {
let json = serde_json::json!({
"sku": "copilot_pro",
"limited_user_reset_date": "2026-11-01",
"limited_user_quotas": {
"chat": {"used": 5, "limit": 10},
"premium_requests": {"used": 0, "limit": 0}
}
});
let report = parse_copilot(&json);
assert_eq!(report.windows.len(), 1, "limit=0 的窗口不画条");
assert_eq!(report.windows[0].name, "Chat");
assert_eq!(report.windows[0].percent, 50.0);
assert_eq!(report.windows[0].resets_at, Some(1_793_491_200_000));
assert!(
report
.extra
.contains(&("Plan".to_string(), "copilot_pro".to_string()))
);
}
}