locode_provider/anthropic/
mod.rs1pub mod build;
13mod client;
14pub mod config;
15pub mod error;
16pub mod parse;
17pub mod retry;
18pub mod wire;
19
20use std::sync::{Arc, Mutex, PoisonError};
21
22use async_trait::async_trait;
23
24pub use build::{build_request, count_cache_controls, normalize_input_schema};
25pub use config::{ApiBackend, AuthScheme, DeveloperRendering, ModelConfig, ReasoningEncoding};
26pub use error::{HttpFailure, classify, parse_retry_after};
27pub use parse::response_to_completion;
28pub use retry::{RetryPolicy, backoff, run_with_retry};
29
30use crate::completion::Completion;
31use crate::provider::{Provider, ProviderError};
32use crate::repair::repair_pairing;
33use crate::request::ConversationRequest;
34
35pub trait AuthRefresh: Send + Sync {
42 fn refresh(&self) -> Option<AuthScheme>;
44}
45
46pub struct AnthropicProvider {
48 http: reqwest::Client,
49 config: ModelConfig,
50 retry: RetryPolicy,
51 auth: Mutex<AuthScheme>,
53 auth_refresh: Option<Arc<dyn AuthRefresh>>,
54}
55
56impl AnthropicProvider {
57 pub fn new(config: ModelConfig) -> Result<Self, ProviderError> {
62 Ok(Self {
63 http: crate::http::build_http_client()?,
64 auth: Mutex::new(config.auth.clone()),
65 config,
66 retry: RetryPolicy::default(),
67 auth_refresh: None,
68 })
69 }
70
71 pub fn from_env() -> Result<Self, ProviderError> {
78 Self::new(ModelConfig::from_env()?)
79 }
80
81 #[must_use]
83 pub fn with_retry_policy(mut self, retry: RetryPolicy) -> Self {
84 self.retry = retry;
85 self
86 }
87
88 #[must_use]
90 pub fn with_auth_refresh(mut self, refresh: Arc<dyn AuthRefresh>) -> Self {
91 self.auth_refresh = Some(refresh);
92 self
93 }
94
95 #[must_use]
97 pub fn config(&self) -> &ModelConfig {
98 &self.config
99 }
100
101 fn current_auth(&self) -> AuthScheme {
102 self.auth
103 .lock()
104 .unwrap_or_else(PoisonError::into_inner)
105 .clone()
106 }
107
108 fn try_refresh(&self) -> bool {
111 let Some(refresher) = &self.auth_refresh else {
112 return false;
113 };
114 let Some(new_auth) = refresher.refresh() else {
115 return false;
116 };
117 let mut current = self.auth.lock().unwrap_or_else(PoisonError::into_inner);
118 if *current == new_auth {
119 return false;
120 }
121 *current = new_auth;
122 true
123 }
124
125 async fn exchange(&self, request: &wire::MessagesRequest) -> Result<Completion, ProviderError> {
127 let auth = self.current_auth();
128 run_with_retry(&self.retry, |_attempt| {
129 client::send_once(&self.http, &self.config, &auth, request)
130 })
131 .await
132 }
133}
134
135#[async_trait]
136impl Provider for AnthropicProvider {
137 #[allow(clippy::unnecessary_literal_bound)] fn api_schema(&self) -> &str {
139 "anthropic"
140 }
141
142 async fn complete(&self, request: &ConversationRequest) -> Result<Completion, ProviderError> {
143 if let Some(crate::request::ReasoningEffort::Other(tier)) =
147 &request.sampling_args.reasoning_effort
148 && self.config.reasoning_encoding == config::ReasoningEncoding::Budget
149 {
150 return Err(ProviderError::Config(format!(
151 "unknown reasoning-effort tier {tier:?} has no budget mapping on the \
152 anthropic wire (Budget encoding)"
153 )));
154 }
155
156 let mut repaired = request.clone();
161 let _ = repair_pairing(&mut repaired.messages);
162
163 let wire_request = build_request(&repaired, &self.config);
165
166 match self.exchange(&wire_request).await {
169 Err(ProviderError::Auth(message)) => {
170 if self.try_refresh() {
171 self.exchange(&wire_request).await
172 } else {
173 Err(ProviderError::Auth(message))
174 }
175 }
176 other => other,
177 }
178 }
179}