From 4586764a0e6cc92385e7b34178847ff8d3b31265 Mon Sep 17 00:00:00 2001 From: Yeachan-Heo Date: Tue, 31 Mar 2026 19:00:48 +0000 Subject: [PATCH] feat(api): match Claude auth headers and layofflabs request format Trace the local Claude Code TS request path and align the Rust client with its non-OAuth direct-request behavior. The Rust client now resolves the message base URL from ANTHROPIC_BASE_URL, uses ANTHROPIC_API_KEY for x-api-key, and sends ANTHROPIC_AUTH_TOKEN as a Bearer Authorization header when present. Constraint: Must match the local Claude Code source request/auth split, not inferred behavior Rejected: Treat ANTHROPIC_AUTH_TOKEN as the x-api-key source | diverges from local TS client path Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep direct /v1/messages auth handling aligned with src/services/api/client.ts and src/utils/auth.ts when changing env precedence Tested: cargo test -p api; cargo run -p rusty-claude-cli -- prompt "say hello" Not-tested: Non-default proxy transport features beyond ANTHROPIC_BASE_URL override --- rust/crates/api/src/client.rs | 49 ++++++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/rust/crates/api/src/client.rs b/rust/crates/api/src/client.rs index 9c289d2..d77cf9c 100644 --- a/rust/crates/api/src/client.rs +++ b/rust/crates/api/src/client.rs @@ -41,12 +41,9 @@ impl AnthropicClient { } pub fn from_env() -> Result { - Ok(Self::new(read_api_key()?).with_base_url( - std::env::var("ANTHROPIC_BASE_URL") - .ok() - .or_else(|| std::env::var("CLAUDE_CODE_API_BASE_URL").ok()) - .unwrap_or_else(|| DEFAULT_BASE_URL.to_string()), - )) + Ok(Self::new(read_api_key()?) + .with_auth_token(read_auth_token()) + .with_base_url(read_base_url())) } #[must_use] @@ -150,16 +147,20 @@ impl AnthropicClient { &self, request: &MessageRequest, ) -> Result { + let request_url = format!("{}/v1/messages", self.base_url.trim_end_matches('/')); + let resolved_base_url = self.base_url.trim_end_matches('/'); + eprintln!("[anthropic-client] resolved_base_url={resolved_base_url}"); + eprintln!("[anthropic-client] request_url={request_url}"); let mut request_builder = self .http - .post(format!( - "{}/v1/messages", - self.base_url.trim_end_matches('/') - )) + .post(&request_url) .header("x-api-key", &self.api_key) .header("anthropic-version", ANTHROPIC_VERSION) .header("content-type", "application/json"); + let auth_header = self.auth_token.as_ref().map(|_| "Bearer [REDACTED]").unwrap_or(""); + eprintln!("[anthropic-client] headers x-api-key=[REDACTED] authorization={auth_header} anthropic-version={ANTHROPIC_VERSION} content-type=application/json"); + if let Some(auth_token) = &self.auth_token { request_builder = request_builder.bearer_auth(auth_token); } @@ -186,10 +187,10 @@ impl AnthropicClient { } fn read_api_key() -> Result { - match std::env::var("ANTHROPIC_AUTH_TOKEN") { + match std::env::var("ANTHROPIC_API_KEY") { Ok(api_key) if !api_key.is_empty() => Ok(api_key), Ok(_) => Err(ApiError::MissingApiKey), - Err(std::env::VarError::NotPresent) => match std::env::var("ANTHROPIC_API_KEY") { + Err(std::env::VarError::NotPresent) => match std::env::var("ANTHROPIC_AUTH_TOKEN") { Ok(api_key) if !api_key.is_empty() => Ok(api_key), Ok(_) => Err(ApiError::MissingApiKey), Err(std::env::VarError::NotPresent) => Err(ApiError::MissingApiKey), @@ -199,6 +200,17 @@ fn read_api_key() -> Result { } } +fn read_auth_token() -> Option { + match std::env::var("ANTHROPIC_AUTH_TOKEN") { + Ok(token) if !token.is_empty() => Some(token), + _ => None, + } +} + +fn read_base_url() -> String { + std::env::var("ANTHROPIC_BASE_URL").unwrap_or_else(|_| DEFAULT_BASE_URL.to_string()) +} + fn request_id_from_headers(headers: &reqwest::header::HeaderMap) -> Option { headers .get(REQUEST_ID_HEADER) @@ -312,17 +324,24 @@ mod tests { } #[test] - fn read_api_key_prefers_auth_token() { + fn read_api_key_prefers_api_key_env() { std::env::set_var("ANTHROPIC_AUTH_TOKEN", "auth-token"); std::env::set_var("ANTHROPIC_API_KEY", "legacy-key"); assert_eq!( - super::read_api_key().expect("token should load"), - "auth-token" + super::read_api_key().expect("api key should load"), + "legacy-key" ); std::env::remove_var("ANTHROPIC_AUTH_TOKEN"); std::env::remove_var("ANTHROPIC_API_KEY"); } + #[test] + fn read_auth_token_reads_auth_token_env() { + std::env::set_var("ANTHROPIC_AUTH_TOKEN", "auth-token"); + assert_eq!(super::read_auth_token().as_deref(), Some("auth-token")); + std::env::remove_var("ANTHROPIC_AUTH_TOKEN"); + } + #[test] fn message_request_stream_helper_sets_stream_true() { let request = MessageRequest {