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
This commit is contained in:
@@ -41,12 +41,9 @@ impl AnthropicClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_env() -> Result<Self, ApiError> {
|
pub fn from_env() -> Result<Self, ApiError> {
|
||||||
Ok(Self::new(read_api_key()?).with_base_url(
|
Ok(Self::new(read_api_key()?)
|
||||||
std::env::var("ANTHROPIC_BASE_URL")
|
.with_auth_token(read_auth_token())
|
||||||
.ok()
|
.with_base_url(read_base_url()))
|
||||||
.or_else(|| std::env::var("CLAUDE_CODE_API_BASE_URL").ok())
|
|
||||||
.unwrap_or_else(|| DEFAULT_BASE_URL.to_string()),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
@@ -150,16 +147,20 @@ impl AnthropicClient {
|
|||||||
&self,
|
&self,
|
||||||
request: &MessageRequest,
|
request: &MessageRequest,
|
||||||
) -> Result<reqwest::Response, ApiError> {
|
) -> Result<reqwest::Response, ApiError> {
|
||||||
|
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
|
let mut request_builder = self
|
||||||
.http
|
.http
|
||||||
.post(format!(
|
.post(&request_url)
|
||||||
"{}/v1/messages",
|
|
||||||
self.base_url.trim_end_matches('/')
|
|
||||||
))
|
|
||||||
.header("x-api-key", &self.api_key)
|
.header("x-api-key", &self.api_key)
|
||||||
.header("anthropic-version", ANTHROPIC_VERSION)
|
.header("anthropic-version", ANTHROPIC_VERSION)
|
||||||
.header("content-type", "application/json");
|
.header("content-type", "application/json");
|
||||||
|
|
||||||
|
let auth_header = self.auth_token.as_ref().map(|_| "Bearer [REDACTED]").unwrap_or("<absent>");
|
||||||
|
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 {
|
if let Some(auth_token) = &self.auth_token {
|
||||||
request_builder = request_builder.bearer_auth(auth_token);
|
request_builder = request_builder.bearer_auth(auth_token);
|
||||||
}
|
}
|
||||||
@@ -186,10 +187,10 @@ impl AnthropicClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn read_api_key() -> Result<String, ApiError> {
|
fn read_api_key() -> Result<String, ApiError> {
|
||||||
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(api_key) if !api_key.is_empty() => Ok(api_key),
|
||||||
Ok(_) => Err(ApiError::MissingApiKey),
|
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(api_key) if !api_key.is_empty() => Ok(api_key),
|
||||||
Ok(_) => Err(ApiError::MissingApiKey),
|
Ok(_) => Err(ApiError::MissingApiKey),
|
||||||
Err(std::env::VarError::NotPresent) => Err(ApiError::MissingApiKey),
|
Err(std::env::VarError::NotPresent) => Err(ApiError::MissingApiKey),
|
||||||
@@ -199,6 +200,17 @@ fn read_api_key() -> Result<String, ApiError> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn read_auth_token() -> Option<String> {
|
||||||
|
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<String> {
|
fn request_id_from_headers(headers: &reqwest::header::HeaderMap) -> Option<String> {
|
||||||
headers
|
headers
|
||||||
.get(REQUEST_ID_HEADER)
|
.get(REQUEST_ID_HEADER)
|
||||||
@@ -312,17 +324,24 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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_AUTH_TOKEN", "auth-token");
|
||||||
std::env::set_var("ANTHROPIC_API_KEY", "legacy-key");
|
std::env::set_var("ANTHROPIC_API_KEY", "legacy-key");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
super::read_api_key().expect("token should load"),
|
super::read_api_key().expect("api key should load"),
|
||||||
"auth-token"
|
"legacy-key"
|
||||||
);
|
);
|
||||||
std::env::remove_var("ANTHROPIC_AUTH_TOKEN");
|
std::env::remove_var("ANTHROPIC_AUTH_TOKEN");
|
||||||
std::env::remove_var("ANTHROPIC_API_KEY");
|
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]
|
#[test]
|
||||||
fn message_request_stream_helper_sets_stream_true() {
|
fn message_request_stream_helper_sets_stream_true() {
|
||||||
let request = MessageRequest {
|
let request = MessageRequest {
|
||||||
|
|||||||
Reference in New Issue
Block a user