use crate::utils::user_agent::get_user_agent;
use std::collections::HashMap;
use crate::constants::oauth::OAUTH_BETA_HEADER;
use crate::constants::oauth::get_oauth_config as get_oauth_config_impl;
#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
pub struct BootstrapResponse {
#[serde(default)]
pub client_data: Option<serde_json::Value>,
#[serde(default)]
pub additional_model_options: Option<Vec<AdditionalModelOption>>,
}
#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
pub struct AdditionalModelOption {
pub model: String,
pub name: String,
pub description: String,
}
fn log_for_debugging(msg: &str) {
log::debug!("[Bootstrap] {}", msg);
}
fn is_essential_traffic_only() -> bool {
std::env::var("AI_CODE_PRIVACY_LEVEL")
.map(|v| v == "essential")
.unwrap_or(false)
}
fn get_api_provider() -> String {
std::env::var("AI_CODE_PROVIDER").unwrap_or_else(|_| "firstParty".to_string())
}
fn get_oauth_config() -> OauthConfig {
OauthConfig {
base_api_url: std::env::var("AI_CODE_API_URL")
.unwrap_or_else(|_| "https://api.anthropic.com".to_string()),
}
}
#[derive(Debug, Clone)]
pub struct OauthConfig {
pub base_api_url: String,
}
fn get_oauth_beta_header() -> String {
OAUTH_BETA_HEADER.to_string()
}
fn get_anthropic_api_key() -> Option<String> {
std::env::var("ANTHROPIC_API_KEY")
.ok()
.or_else(|| std::env::var("AI_API_KEY").ok())
}
#[derive(Debug, Clone)]
pub struct OAuthTokens {
pub access_token: Option<String>,
}
fn get_claude_ai_oauth_tokens() -> Option<OAuthTokens> {
if let Ok(token) = std::env::var("AI_CODE_OAUTH_TOKEN") {
return Some(OAuthTokens {
access_token: Some(token),
});
}
None
}
fn has_profile_scope() -> bool {
get_claude_ai_oauth_tokens().is_some()
}
fn get_global_config() -> GlobalConfig {
GlobalConfig::default()
}
#[derive(Debug, Clone, Default)]
pub struct GlobalConfig {
pub client_data_cache: Option<serde_json::Value>,
pub additional_model_options_cache: Option<Vec<AdditionalModelOption>>,
}
fn save_global_config(_update: impl FnOnce(&mut GlobalConfig)) {
}
fn get_claude_code_user_agent() -> String {
get_user_agent()
}
async fn fetch_bootstrap_api() -> Option<BootstrapResponse> {
if is_essential_traffic_only() {
log_for_debugging("Skipped: Nonessential traffic disabled");
return None;
}
if get_api_provider() != "firstParty" {
log_for_debugging("Skipped: 3P provider");
return None;
}
let api_key = get_anthropic_api_key();
let has_usable_oauth = get_claude_ai_oauth_tokens()
.map(|t| {
t.access_token
.as_ref()
.map(|s| !s.is_empty())
.unwrap_or(false)
})
.unwrap_or(false)
&& has_profile_scope();
if !has_usable_oauth && api_key.is_none() {
log_for_debugging("Skipped: no usable OAuth or API key");
return None;
}
let config = get_oauth_config_impl();
let endpoint = format!("{}/api/claude_cli/bootstrap", config.base_api_url);
log_for_debugging("Fetching");
let client = match reqwest::Client::builder()
.timeout(std::time::Duration::from_millis(5000))
.build()
{
Ok(c) => c,
Err(e) => {
log_for_debugging(&format!("Failed to build client: {}", e));
return None;
}
};
let mut headers = HashMap::new();
headers.insert("Content-Type".to_string(), "application/json".to_string());
headers.insert("User-Agent".to_string(), get_claude_code_user_agent());
if let Some(token) = get_claude_ai_oauth_tokens() {
if let Some(access_token) = token.access_token {
if has_profile_scope() {
headers.insert(
"Authorization".to_string(),
format!("Bearer {}", access_token),
);
headers.insert("anthropic-beta".to_string(), get_oauth_beta_header());
}
}
}
if headers.get("Authorization").is_none() {
if let Some(key) = &api_key {
headers.insert("x-api-key".to_string(), key.clone());
}
}
let reqwest_headers: reqwest::header::HeaderMap = headers
.into_iter()
.filter_map(|(k, v)| {
let key: reqwest::header::HeaderName = k.parse().ok()?;
let value: reqwest::header::HeaderValue = v.parse().ok()?;
Some((key, value))
})
.collect();
let response = match client.get(&endpoint).headers(reqwest_headers).send().await {
Ok(resp) => resp,
Err(e) => {
log_for_debugging(&format!("Fetch failed: {}", e));
return None;
}
};
if !response.status().is_success() {
log_for_debugging(&format!("Fetch failed: status {}", response.status()));
return None;
}
match response.json::<BootstrapResponse>().await {
Ok(data) => {
log_for_debugging("Fetch ok");
Some(data)
}
Err(e) => {
log_for_debugging(&format!("Response failed validation: {}", e));
None
}
}
}
pub async fn fetch_bootstrap_data() {
let response = match fetch_bootstrap_api().await {
Some(r) => r,
None => return,
};
let client_data = response.client_data.unwrap_or(serde_json::Value::Null);
let additional_model_options = response.additional_model_options.unwrap_or_default();
let config = get_global_config();
let client_data_unchanged = config.client_data_cache.as_ref() == Some(&client_data);
let model_options_unchanged = config
.additional_model_options_cache
.as_ref()
.map(|c| c == &additional_model_options)
.unwrap_or(false);
if client_data_unchanged && model_options_unchanged {
log_for_debugging("Cache unchanged, skipping write");
return;
}
log_for_debugging("Cache updated, persisting to disk");
save_global_config(|cfg| {
cfg.client_data_cache = Some(client_data);
cfg.additional_model_options_cache = Some(additional_model_options);
});
}