use std::sync::Arc;
use async_trait::async_trait;
use secrecy::{ExposeSecret, SecretString};
use tokio::sync::RwLock;
use super::config::{BetaFeature, ProviderConfig};
use super::traits::ProviderAdapter;
use crate::auth::{Credential, CredentialProvider, OAuthConfig};
use crate::client::messages::{
CountTokensRequest, CountTokensResponse, CreateMessageRequest, ErrorResponse,
};
use crate::types::ApiResponse;
use crate::{Error, Result};
const BASE_URL: &str = "https://api.anthropic.com";
#[derive(Clone)]
enum AuthMethod {
ApiKey(SecretString),
OAuth {
token: SecretString,
config: OAuthConfig,
},
}
impl std::fmt::Debug for AuthMethod {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ApiKey(_) => f.debug_tuple("ApiKey").field(&"[redacted]").finish(),
Self::OAuth { config, .. } => f
.debug_struct("OAuth")
.field("token", &"[redacted]")
.field("config", config)
.finish(),
}
}
}
impl AuthMethod {
fn from_credential(credential: &Credential, oauth_config: Option<OAuthConfig>) -> Self {
match credential {
Credential::ApiKey(key) => Self::ApiKey(key.clone()),
Credential::OAuth(oauth) => Self::OAuth {
token: oauth.access_token.clone(),
config: oauth_config.unwrap_or_default(),
},
}
}
fn update_token(&mut self, credential: &Credential) {
match credential {
Credential::ApiKey(key) => *self = Self::ApiKey(key.clone()),
Credential::OAuth(oauth) => {
if let Self::OAuth { token, .. } = self {
*token = oauth.access_token.clone();
} else {
*self = Self::OAuth {
token: oauth.access_token.clone(),
config: OAuthConfig::default(),
};
}
}
}
}
}
pub struct AnthropicAdapter {
config: ProviderConfig,
base_url: String,
auth: RwLock<AuthMethod>,
credential_provider: Option<Arc<dyn CredentialProvider>>,
}
impl std::fmt::Debug for AnthropicAdapter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AnthropicAdapter")
.field("config", &self.config)
.field("base_url", &self.base_url)
.finish()
}
}
impl AnthropicAdapter {
pub fn new(config: ProviderConfig) -> Self {
Self {
config,
base_url: Self::base_url_from_env(),
auth: RwLock::new(AuthMethod::ApiKey(Self::api_key_from_env())),
credential_provider: None,
}
}
fn api_key_from_env() -> SecretString {
SecretString::from(std::env::var("ANTHROPIC_API_KEY").unwrap_or_default())
}
fn base_url_from_env() -> String {
std::env::var("ANTHROPIC_BASE_URL").unwrap_or_else(|_| BASE_URL.into())
}
pub fn from_credential(
config: ProviderConfig,
credential: &Credential,
oauth_config: Option<OAuthConfig>,
) -> Self {
Self {
config,
base_url: Self::base_url_from_env(),
auth: RwLock::new(AuthMethod::from_credential(credential, oauth_config)),
credential_provider: None,
}
}
pub fn from_credential_provider(
config: ProviderConfig,
credential: &Credential,
oauth_config: Option<OAuthConfig>,
provider: Arc<dyn CredentialProvider>,
) -> Self {
Self {
config,
base_url: Self::base_url_from_env(),
auth: RwLock::new(AuthMethod::from_credential(credential, oauth_config)),
credential_provider: Some(provider),
}
}
pub fn api_key(self, key: impl Into<String>) -> Self {
Self {
auth: RwLock::new(AuthMethod::ApiKey(SecretString::from(key.into()))),
..self
}
}
pub fn base_url(mut self, url: impl Into<String>) -> Self {
self.base_url = url.into();
self
}
fn build_endpoint_url(&self, auth: &AuthMethod, endpoint: &str) -> String {
match auth {
AuthMethod::OAuth { config, .. } => config.build_url(&self.base_url, endpoint),
AuthMethod::ApiKey(_) => format!("{}{}", self.base_url, endpoint),
}
}
fn build_headers(
&self,
req: reqwest::RequestBuilder,
auth: &AuthMethod,
) -> reqwest::RequestBuilder {
let mut r = match auth {
AuthMethod::ApiKey(key) => req
.header("x-api-key", key.expose_secret())
.header("anthropic-version", &self.config.api_version)
.header("content-type", "application/json"),
AuthMethod::OAuth { token, config } => config.apply_headers(
req,
token.expose_secret(),
&self.config.api_version,
&self.config.beta,
),
};
if let AuthMethod::ApiKey(_) = auth
&& let Some(beta) = self.config.beta.header_value()
{
r = r.header("anthropic-beta", beta);
}
for (k, v) in &self.config.extra_headers {
r = r.header(k.as_str(), v.as_str());
}
r
}
fn prepare_request_with_auth(
&self,
mut request: CreateMessageRequest,
auth: &AuthMethod,
) -> CreateMessageRequest {
if let AuthMethod::OAuth { .. } = auth {
use crate::prompts::CLI_IDENTITY;
let mut blocks = vec![crate::types::SystemBlock::uncached(CLI_IDENTITY)];
match &request.system {
Some(crate::types::SystemPrompt::Text(existing)) if !existing.is_empty() => {
if !existing.starts_with(CLI_IDENTITY) {
blocks.push(crate::types::SystemBlock::uncached(existing));
}
}
Some(crate::types::SystemPrompt::Blocks(existing_blocks))
if !existing_blocks.is_empty() =>
{
for block in existing_blocks {
if block.text != CLI_IDENTITY {
blocks.push(block.clone());
}
}
}
_ => {}
}
request.system = Some(crate::types::SystemPrompt::Blocks(blocks));
}
request
}
async fn do_refresh(&self) -> Result<()> {
if let Some(ref provider) = self.credential_provider {
let new_credential = provider.refresh().await?;
self.auth.write().await.update_token(&new_credential);
}
Ok(())
}
pub fn credential_provider(&self) -> Option<&Arc<dyn CredentialProvider>> {
self.credential_provider.as_ref()
}
fn needs_structured_outputs(request: &CreateMessageRequest) -> bool {
let has_strict_tools = request
.tools
.as_ref()
.is_some_and(|tools| tools.iter().any(|t| t.is_strict()));
request.output_format.is_some() || has_strict_tools
}
fn apply_structured_outputs_header(
&self,
req: reqwest::RequestBuilder,
needs: bool,
) -> reqwest::RequestBuilder {
if needs && !self.config.beta.has(BetaFeature::StructuredOutputs) {
req.header(
"anthropic-beta",
BetaFeature::StructuredOutputs.header_value(),
)
} else {
req
}
}
async fn check_error_response(response: reqwest::Response) -> Result<reqwest::Response> {
if !response.status().is_success() {
let status = response.status().as_u16();
let error: ErrorResponse = response.json().await?;
return Err(error.into_error(status));
}
Ok(response)
}
}
#[async_trait]
impl ProviderAdapter for AnthropicAdapter {
fn config(&self) -> &ProviderConfig {
&self.config
}
fn name(&self) -> &'static str {
"anthropic"
}
fn base_url(&self) -> &str {
&self.base_url
}
async fn build_url(&self, _model: &str, _stream: bool) -> String {
let auth = self.auth.read().await;
self.build_endpoint_url(&auth, "/v1/messages")
}
async fn transform_request(&self, request: CreateMessageRequest) -> Result<serde_json::Value> {
let auth = self.auth.read().await;
let prepared = self.prepare_request_with_auth(request, &auth);
serde_json::to_value(&prepared).map_err(|e| Error::InvalidRequest(e.to_string()))
}
async fn apply_auth_headers(&self, req: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
let auth = self.auth.read().await;
self.build_headers(req, &auth)
}
async fn send(
&self,
http: &reqwest::Client,
request: CreateMessageRequest,
) -> Result<ApiResponse> {
let needs = Self::needs_structured_outputs(&request);
let (url, body) = {
let auth = self.auth.read().await;
let url = self.build_endpoint_url(&auth, "/v1/messages");
let prepared = self.prepare_request_with_auth(request, &auth);
(url, serde_json::to_value(&prepared)?)
};
let req = self.apply_auth_headers(http.post(&url)).await;
let req = self.apply_structured_outputs_header(req, needs);
let response = req.json(&body).send().await?;
let response = Self::check_error_response(response).await?;
let json: serde_json::Value = response.json().await?;
self.transform_response(json)
}
async fn send_stream(
&self,
http: &reqwest::Client,
mut request: CreateMessageRequest,
) -> Result<reqwest::Response> {
let needs = Self::needs_structured_outputs(&request);
request.stream = Some(true);
let (url, body) = {
let auth = self.auth.read().await;
let url = self.build_endpoint_url(&auth, "/v1/messages");
let prepared = self.prepare_request_with_auth(request, &auth);
(url, serde_json::to_value(&prepared)?)
};
let req = self.apply_auth_headers(http.post(&url)).await;
let req = self.apply_structured_outputs_header(req, needs);
let response = req.json(&body).send().await?;
Self::check_error_response(response).await
}
fn supports_credential_refresh(&self) -> bool {
self.credential_provider
.as_ref()
.is_some_and(|p| p.supports_refresh())
}
async fn ensure_fresh_credentials(&self) -> Result<()> {
if let Some(ref provider) = self.credential_provider {
let current = provider.resolve().await?;
if current.needs_refresh() && provider.supports_refresh() {
let new_cred = provider.refresh().await?;
self.auth.write().await.update_token(&new_cred);
}
}
Ok(())
}
async fn refresh_credentials(&self) -> Result<()> {
self.do_refresh().await
}
async fn count_tokens(
&self,
http: &reqwest::Client,
request: CountTokensRequest,
) -> Result<CountTokensResponse> {
let (url, body) = {
let auth = self.auth.read().await;
let url = self.build_endpoint_url(&auth, "/v1/messages/count_tokens");
(url, serde_json::to_value(&request)?)
};
let response = self
.apply_auth_headers(http.post(&url))
.await
.json(&body)
.send()
.await?;
let response = Self::check_error_response(response).await?;
Ok(response.json().await?)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::client::adapter::{BetaConfig, BetaFeature, ModelConfig};
use crate::types::Message;
#[tokio::test]
async fn test_build_url() {
let adapter = AnthropicAdapter::new(ProviderConfig::new(ModelConfig::anthropic()));
let url = adapter.build_url("claude-sonnet-4-5", false).await;
assert!(url.contains("/v1/messages"));
}
#[tokio::test]
async fn test_transform_request() {
let adapter = AnthropicAdapter::new(ProviderConfig::new(ModelConfig::anthropic()));
let request = CreateMessageRequest::new("claude-sonnet-4-5", vec![Message::user("Hello")]);
let body = adapter.transform_request(request).await.unwrap();
assert!(body.get("model").is_some());
assert!(body.get("messages").is_some());
}
#[tokio::test]
async fn test_oauth_url_params() {
let credential = Credential::oauth("test-token");
let adapter = AnthropicAdapter::from_credential(
ProviderConfig::new(ModelConfig::anthropic()),
&credential,
None,
);
let url = adapter.build_url("model", false).await;
assert!(url.contains("beta=true"));
}
#[tokio::test]
async fn test_oauth_system_prompt() {
let credential = Credential::oauth("test-token");
let adapter = AnthropicAdapter::from_credential(
ProviderConfig::new(ModelConfig::anthropic()),
&credential,
None,
);
let request = CreateMessageRequest::new("model", vec![Message::user("Hi")]);
let body = adapter.transform_request(request).await.unwrap();
let system_blocks = body
.get("system")
.and_then(|v| v.as_array())
.expect("OAuth should produce system blocks");
let first_text = system_blocks[0]
.get("text")
.and_then(|v| v.as_str())
.unwrap_or("");
assert!(first_text.contains("Claude Code"));
}
#[test]
fn test_api_key_with_beta() {
let config = ProviderConfig::new(ModelConfig::anthropic())
.beta(BetaFeature::InterleavedThinking)
.beta(BetaFeature::ContextManagement);
let adapter = AnthropicAdapter::new(config);
assert!(adapter.config.beta.has(BetaFeature::InterleavedThinking));
assert!(adapter.config.beta.has(BetaFeature::ContextManagement));
}
#[test]
fn test_api_key_with_custom_beta() {
let beta = BetaConfig::new().custom("new-feature-2026-01-01");
let config = ProviderConfig::new(ModelConfig::anthropic()).beta_config(beta);
let adapter = AnthropicAdapter::new(config);
let header = adapter.config.beta.header_value().unwrap();
assert!(header.contains("new-feature-2026-01-01"));
}
#[tokio::test]
async fn test_oauth_prepends_cli_identity_to_system_prompt() {
let credential = Credential::oauth("test-token");
let adapter = AnthropicAdapter::from_credential(
ProviderConfig::new(ModelConfig::anthropic()),
&credential,
None,
);
let request = CreateMessageRequest::new("model", vec![Message::user("Hi")])
.system("Custom user system prompt");
let body = adapter.transform_request(request).await.unwrap();
let system_blocks = body
.get("system")
.and_then(|v| v.as_array())
.expect("OAuth should produce system blocks");
assert!(system_blocks.len() >= 2, "Should have at least 2 blocks");
let first_text = system_blocks[0]
.get("text")
.and_then(|v| v.as_str())
.unwrap_or("");
assert!(
first_text.starts_with("You are Claude Code"),
"First block should be Claude Code identity: {}",
first_text
);
let second_text = system_blocks[1]
.get("text")
.and_then(|v| v.as_str())
.unwrap_or("");
assert_eq!(
second_text, "Custom user system prompt",
"Second block should preserve original"
);
}
#[tokio::test]
async fn test_api_key_does_not_modify_system_prompt() {
let adapter =
AnthropicAdapter::new(ProviderConfig::new(ModelConfig::anthropic())).api_key("sk-test");
let request = CreateMessageRequest::new("model", vec![Message::user("Hi")])
.system("Custom user system prompt");
let body = adapter.transform_request(request).await.unwrap();
let system = body.get("system").and_then(|v| v.as_str()).unwrap_or("");
assert_eq!(
system, "Custom user system prompt",
"API key auth should not modify system prompt"
);
assert!(
!system.contains("Claude Code"),
"API key auth should not add CLI identity: {}",
system
);
}
}