Skip to main content

codex_cli/rate_limits/
client.rs

1use anyhow::{Context, Result};
2use reqwest::blocking::Client;
3use serde_json::Value;
4use std::path::{Path, PathBuf};
5use std::time::Duration;
6
7use crate::json;
8use crate::paths;
9
10pub struct UsageResponse {
11    pub body: String,
12    pub json: Value,
13}
14
15pub struct UsageRequest {
16    pub target_file: PathBuf,
17    pub refresh_on_401: bool,
18    pub base_url: String,
19    pub connect_timeout_seconds: u64,
20    pub max_time_seconds: u64,
21}
22
23pub fn fetch_usage(request: &UsageRequest) -> Result<UsageResponse> {
24    let (access_token, account_id) = read_tokens(&request.target_file)?;
25    let mut response = send_request(request, &access_token, account_id.as_deref())?;
26
27    if response.status == 401 && request.refresh_on_401 {
28        refresh_target(&request.target_file);
29        if let Ok((access_token, account_id)) = read_tokens(&request.target_file) {
30            response = send_request(request, &access_token, account_id.as_deref())?;
31        }
32    }
33
34    if response.status != 200 {
35        let preview = response
36            .body
37            .chars()
38            .take(200)
39            .collect::<String>()
40            .replace(['\n', '\r'], " ");
41        if preview.is_empty() {
42            anyhow::bail!(
43                "codex-rate-limits: GET {} failed (HTTP {})",
44                response.url,
45                response.status
46            );
47        }
48        anyhow::bail!(
49            "codex-rate-limits: GET {} failed (HTTP {})\ncodex-rate-limits: body: {}",
50            response.url,
51            response.status,
52            preview
53        );
54    }
55
56    let json: Value =
57        serde_json::from_str(&response.body).context("invalid JSON from usage endpoint")?;
58
59    Ok(UsageResponse {
60        body: response.body,
61        json,
62    })
63}
64
65pub fn read_tokens(target_file: &Path) -> Result<(String, Option<String>)> {
66    let value = json::read_json(target_file)?;
67    let access_token = json::string_at(&value, &["tokens", "access_token"])
68        .ok_or_else(|| anyhow::anyhow!("missing access_token"))?;
69    let account_id = json::string_at(&value, &["tokens", "account_id"])
70        .or_else(|| json::string_at(&value, &["account_id"]));
71    Ok((access_token, account_id))
72}
73
74fn send_request(
75    request: &UsageRequest,
76    access_token: &str,
77    account_id: Option<&str>,
78) -> Result<HttpResponse> {
79    let client = Client::builder()
80        .connect_timeout(Duration::from_secs(request.connect_timeout_seconds))
81        .timeout(Duration::from_secs(request.max_time_seconds))
82        .build()?;
83
84    let url = format!("{}/wham/usage", request.base_url.trim_end_matches('/'));
85    let mut req = client
86        .get(&url)
87        .header("Authorization", format!("Bearer {}", access_token))
88        .header("Accept", "application/json")
89        .header("User-Agent", "codex-cli");
90    if let Some(account_id) = account_id {
91        req = req.header("ChatGPT-Account-Id", account_id);
92    }
93
94    let resp = req.send();
95    let resp = match resp {
96        Ok(value) => value,
97        Err(_) => {
98            anyhow::bail!("codex-rate-limits: request failed: {}", url);
99        }
100    };
101
102    let status = resp.status().as_u16();
103    let body = resp.text().unwrap_or_default();
104    Ok(HttpResponse { status, body, url })
105}
106
107fn refresh_target(target_file: &Path) {
108    if let Some(auth_file) = paths::resolve_auth_file()
109        && auth_file == target_file
110    {
111        let _ = crate::auth::refresh::run(&[]);
112        return;
113    }
114
115    if let Some(secret_dir) = paths::resolve_secret_dir()
116        && let Some(file_name) = target_file.file_name().and_then(|n| n.to_str())
117    {
118        let path = secret_dir.join(file_name);
119        if path == target_file {
120            let _ = crate::auth::refresh::run(&[file_name.to_string()]);
121        }
122    }
123}
124
125struct HttpResponse {
126    status: u16,
127    body: String,
128    url: String,
129}