Skip to main content

claude_codex/providers/kimi/auth/
login.rs

1use serde::Deserialize;
2
3use super::constants::{CLIENT_ID, oauth_host};
4use super::headers::common_headers;
5
6#[derive(Debug, Clone, Deserialize)]
7pub struct TokenResponse {
8    pub access_token: String,
9    #[serde(default)]
10    pub refresh_token: Option<String>,
11    #[serde(default)]
12    pub expires_in: Option<u64>,
13    #[serde(default)]
14    pub scope: Option<String>,
15    #[serde(default)]
16    pub token_type: Option<String>,
17}
18
19#[derive(Debug, Deserialize)]
20#[allow(dead_code)]
21struct DeviceAuthResponse {
22    user_code: String,
23    device_code: String,
24    #[serde(default)]
25    verification_uri: Option<String>,
26    verification_uri_complete: String,
27    #[serde(default)]
28    expires_in: Option<u64>,
29    #[serde(default)]
30    interval: Option<u64>,
31}
32
33const GRANT_DEVICE_CODE: &str = "urn:ietf:params:oauth:grant-type:device_code";
34const POLL_SAFETY_MARGIN_MS: u64 = 500;
35
36pub fn run_device_login() -> Result<TokenResponse, anyhow::Error> {
37    let headers = common_headers()?;
38    let client = reqwest::blocking::Client::new();
39
40    let init_resp = client
41        .post(format!("{}/api/oauth/device_authorization", oauth_host()))
42        .headers(build_header_map(&headers))
43        .form(&[("client_id", CLIENT_ID)])
44        .send()?;
45
46    if !init_resp.status().is_success() {
47        let status = init_resp.status();
48        let text = init_resp.text().unwrap_or_default();
49        anyhow::bail!("Device authorization failed: {status} {text}");
50    }
51
52    let auth: DeviceAuthResponse = init_resp.json()?;
53    let interval_ms = (auth.interval.unwrap_or(5).max(1)) * 1000;
54
55    eprintln!();
56    eprintln!("Visit: {}", auth.verification_uri_complete);
57    eprintln!("Code:  {}", auth.user_code);
58    eprintln!();
59
60    loop {
61        let resp = client
62            .post(format!("{}/api/oauth/token", oauth_host()))
63            .headers(build_header_map(&headers))
64            .form(&[
65                ("client_id", CLIENT_ID),
66                ("device_code", &auth.device_code),
67                ("grant_type", GRANT_DEVICE_CODE),
68            ])
69            .send()?;
70
71        let status = resp.status();
72        if status.as_u16() == 200 {
73            return Ok(resp.json::<TokenResponse>()?);
74        }
75
76        let body: serde_json::Value = resp.json().unwrap_or(serde_json::json!({}));
77        let error = body
78            .get("error")
79            .and_then(|v| v.as_str())
80            .unwrap_or("unknown");
81
82        match error {
83            "expired_token" => {
84                anyhow::bail!("Device code expired. Run login again.");
85            }
86            "authorization_pending" | "slow_down" => {
87                std::thread::sleep(std::time::Duration::from_millis(
88                    interval_ms + POLL_SAFETY_MARGIN_MS,
89                ));
90                continue;
91            }
92            _ => {
93                let desc = body
94                    .get("error_description")
95                    .and_then(|v| v.as_str())
96                    .unwrap_or("");
97                anyhow::bail!(
98                    "Device token poll failed ({}): {}{}",
99                    status,
100                    error,
101                    if desc.is_empty() {
102                        "".to_string()
103                    } else {
104                        format!(" - {desc}")
105                    }
106                );
107            }
108        }
109    }
110}
111
112fn build_header_map(
113    headers: &std::collections::HashMap<String, String>,
114) -> reqwest::header::HeaderMap {
115    use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
116    let mut map = HeaderMap::new();
117    for (k, v) in headers {
118        if let Ok(name) = HeaderName::from_bytes(k.as_bytes())
119            && let Ok(value) = HeaderValue::from_str(v)
120        {
121            map.insert(name, value);
122        }
123    }
124    map
125}