claude_codex/providers/kimi/auth/
login.rs1use 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}