use std::collections::HashMap;
use std::env;
pub const CLIENT_REQUEST_ID_HEADER: &str = "x-client-request-id";
fn is_env_truthy(value: Option<String>) -> bool {
value
.map(|v| v == "1" || v.to_lowercase() == "true")
.unwrap_or(false)
}
fn get_api_timeout_ms() -> u64 {
env::var("AI_CODE_API_TIMEOUT_MS")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(600_000)
}
fn get_session_id() -> String {
env::var("AI_CODE_SESSION_ID").unwrap_or_else(|_| uuid::Uuid::new_v4().to_string())
}
fn get_container_id() -> Option<String> {
env::var("AI_CODE_CONTAINER_ID").ok()
}
fn get_remote_session_id() -> Option<String> {
env::var("AI_CODE_REMOTE_SESSION_ID").ok()
}
fn get_client_app() -> Option<String> {
env::var("AI_AGENT_SDK_CLIENT_APP").ok()
}
fn get_custom_headers() -> HashMap<String, String> {
let mut headers = HashMap::new();
if let Ok(custom_headers_env) = env::var("AI_CODE_CUSTOM_HEADERS") {
if custom_headers_env.is_empty() {
return headers;
}
for header_string in custom_headers_env.lines() {
let header_string = header_string.trim();
if header_string.is_empty() {
continue;
}
if let Some(colon_idx) = header_string.find(':') {
let name = header_string[..colon_idx].trim().to_string();
let value = header_string[colon_idx + 1..].trim().to_string();
if !name.is_empty() {
headers.insert(name, value);
}
}
}
}
headers
}
fn is_additional_protection_enabled() -> bool {
is_env_truthy(env::var("AI_CODE_ADDITIONAL_PROTECTION").ok())
}
fn is_using_bedrock() -> bool {
is_env_truthy(env::var("AI_CODE_USE_BEDROCK").ok())
}
fn is_using_foundry() -> bool {
is_env_truthy(env::var("AI_CODE_USE_FOUNDRY").ok())
}
fn is_using_vertex() -> bool {
is_env_truthy(env::var("AI_CODE_USE_VERTEX").ok())
}
fn get_aws_region() -> String {
env::var("AI_CODE_AWS_REGION")
.or_else(|_| env::var("AWS_DEFAULT_REGION"))
.unwrap_or_else(|_| "us-east-1".to_string())
}
fn get_small_fast_model() -> String {
"claude-3-5-haiku-20241022".to_string()
}
fn get_bearer_token_bedrock() -> Option<String> {
env::var("AWS_BEARER_TOKEN_BEDROCK").ok()
}
fn is_skipping_bedrock_auth() -> bool {
is_env_truthy(env::var("AI_CODE_SKIP_BEDROCK_AUTH").ok())
}
fn is_skipping_vertex_auth() -> bool {
is_env_truthy(env::var("AI_CODE_SKIP_VERTEX_AUTH").ok())
}
fn get_vertex_project_id() -> Option<String> {
env::var("ANTHROPIC_VERTEX_PROJECT_ID").ok()
}
fn get_vertex_region_for_model(_model: Option<&str>) -> String {
"us-east5".to_string()
}
fn has_project_env_var() -> bool {
env::var("GCLOUD_PROJECT").is_ok()
|| env::var("GOOGLE_CLOUD_PROJECT").is_ok()
|| env::var("gcloud_project").is_ok()
|| env::var("google_cloud_project").is_ok()
}
fn has_key_file() -> bool {
env::var("GOOGLE_APPLICATION_CREDENTIALS").is_ok()
|| env::var("google_application_credentials").is_ok()
}
fn is_ant_user() -> bool {
env::var("AI_CODE_USER_TYPE")
.map(|v| v == "ant")
.unwrap_or(false)
}
fn is_using_staging_oauth() -> bool {
is_env_truthy(env::var("USE_STAGING_OAUTH").ok())
}
fn get_oauth_base_api_url() -> String {
env::var("AI_CODE_API_URL").unwrap_or_else(|_| "https://api.staging.anthropic.com".to_string())
}
fn is_debug_to_stderr() -> bool {
is_env_truthy(env::var("AI_CODE_DEBUG_TO_STDERR").ok())
}
pub fn create_default_headers() -> HashMap<String, String> {
let mut headers = HashMap::new();
headers.insert("x-app".to_string(), "cli".to_string());
headers.insert(
"User-Agent".to_string(),
format!("ai-agent/{}", env!("CARGO_PKG_VERSION")),
);
headers.insert("X-Claude-Code-Session-Id".to_string(), get_session_id());
for (k, v) in get_custom_headers() {
headers.insert(k, v);
}
if let Some(container_id) = get_container_id() {
headers.insert("x-claude-remote-container-id".to_string(), container_id);
}
if let Some(remote_session_id) = get_remote_session_id() {
headers.insert("x-claude-remote-session-id".to_string(), remote_session_id);
}
if let Some(client_app) = get_client_app() {
headers.insert("x-client-app".to_string(), client_app);
}
if is_additional_protection_enabled() {
headers.insert(
"x-anthropic-additional-protection".to_string(),
"true".to_string(),
);
}
headers
}
fn get_anthropic_api_key() -> Option<String> {
env::var("AI_CODE_API_KEY")
.ok()
.or_else(|| env::var("ANTHROPIC_API_KEY").ok())
}
fn is_claude_ai_subscriber() -> bool {
env::var("AI_CODE_OAUTH_TOKEN").is_ok()
}
fn get_claudeai_oauth_tokens() -> Option<OAuthTokens> {
env::var("AI_CODE_OAUTH_TOKEN")
.ok()
.map(|token| OAuthTokens {
access_token: token,
})
}
#[derive(Debug, Clone)]
pub struct OAuthTokens {
pub access_token: String,
}
async fn check_and_refresh_oauth_token_if_needed() {
log::debug!("[API:auth] OAuth token check");
}
async fn configure_api_key_headers(
headers: &mut HashMap<String, String>,
_is_non_interactive: bool,
) {
if let Ok(token) = env::var("ANTHROPIC_AUTH_TOKEN") {
if !token.is_empty() {
headers.insert("Authorization".to_string(), format!("Bearer {}", token));
}
}
}
#[derive(Debug, Clone)]
pub struct AnthropicClientConfig {
pub api_key: Option<String>,
pub auth_token: Option<String>,
pub base_url: Option<String>,
pub max_retries: u32,
pub timeout: u64,
}
pub async fn get_anthropic_client(
config: AnthropicClientConfig,
) -> Result<reqwest::Client, String> {
let mut default_headers = create_default_headers();
check_and_refresh_oauth_token_if_needed().await;
if !is_claude_ai_subscriber() {
configure_api_key_headers(&mut default_headers, false).await;
}
let mut header_map = reqwest::header::HeaderMap::new();
for (k, v) in default_headers {
if let (Ok(name), Ok(value)) = (
k.parse::<reqwest::header::HeaderName>(),
v.parse::<reqwest::header::HeaderValue>(),
) {
header_map.insert(name, value);
}
}
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_millis(config.timeout))
.default_headers(header_map)
.build()
.map_err(|e| e.to_string())?;
Ok(client)
}
pub async fn build_anthropic_client(
api_key: Option<String>,
max_retries: u32,
_model: Option<String>,
_source: Option<String>,
) -> Result<reqwest::Client, String> {
let config = AnthropicClientConfig {
api_key: if is_claude_ai_subscriber() {
None
} else {
api_key.or_else(get_anthropic_api_key)
},
auth_token: if is_claude_ai_subscriber() {
get_claudeai_oauth_tokens().map(|t| t.access_token)
} else {
None
},
base_url: if is_ant_user() && is_using_staging_oauth() {
Some(get_oauth_base_api_url())
} else {
None
},
max_retries,
timeout: get_api_timeout_ms(),
};
get_anthropic_client(config).await
}
pub fn get_proxy_fetch_options(_for_anthropic_api: bool) -> HashMap<String, String> {
HashMap::new()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_env_truthy() {
assert!(!is_env_truthy(None));
assert!(!is_env_truthy(Some("0".to_string())));
assert!(!is_env_truthy(Some("false".to_string())));
assert!(is_env_truthy(Some("1".to_string())));
assert!(is_env_truthy(Some("true".to_string())));
}
#[test]
fn test_create_default_headers_has_required() {
let headers = create_default_headers();
assert!(headers.contains_key("x-app"));
assert!(headers.contains_key("User-Agent"));
assert!(headers.contains_key("X-Claude-Code-Session-Id"));
}
#[test]
fn test_get_api_timeout_default() {
let timeout = get_api_timeout_ms();
assert_eq!(timeout, 600_000);
}
}