1use std::{fmt, sync::Arc};
2
3use agent_sdk_core::{
4 AgentError, ProviderAdapter, ProviderCapabilities, ProviderMessageRole,
5 ProviderProjectionPolicy, ProviderRequest, ProviderResponse, ProviderStopReason,
6 ProviderToolCall, ProviderUsage, RetryClassification, ToolCallId,
7 tool_records::CanonicalToolName,
8};
9use serde::{Deserialize, Serialize};
10use serde_json::{Value, json};
11
12use crate::{
13 ProviderApiKey, ProviderToolArgumentSink,
14 error::{provider_failure, unsupported_response},
15 http::{CurlJsonHttpTransport, JsonHttpRequest, JsonHttpTransport},
16};
17
18#[derive(Clone, Debug, Eq, PartialEq)]
19pub struct AnthropicMessagesConfig {
21 pub provider_ref: String,
23 pub model: String,
25 pub endpoint_url: String,
27 pub api_version: String,
29 pub max_tokens: u32,
31 pub max_input_tokens: Option<u32>,
33}
34
35impl AnthropicMessagesConfig {
36 pub fn new(model: impl Into<String>) -> Self {
38 Self {
39 provider_ref: "provider.anthropic.messages".to_string(),
40 model: model.into(),
41 endpoint_url: "https://api.anthropic.com/v1/messages".to_string(),
42 api_version: "2023-06-01".to_string(),
43 max_tokens: 1024,
44 max_input_tokens: None,
45 }
46 }
47
48 pub fn provider_ref(mut self, provider_ref: impl Into<String>) -> Self {
50 self.provider_ref = provider_ref.into();
51 self
52 }
53
54 pub fn endpoint_url(mut self, endpoint_url: impl Into<String>) -> Self {
56 self.endpoint_url = endpoint_url.into();
57 self
58 }
59
60 pub fn api_version(mut self, api_version: impl Into<String>) -> Self {
62 self.api_version = api_version.into();
63 self
64 }
65
66 pub fn max_tokens(mut self, max_tokens: u32) -> Self {
68 self.max_tokens = max_tokens;
69 self
70 }
71
72 pub fn max_input_tokens(mut self, max_input_tokens: u32) -> Self {
74 self.max_input_tokens = Some(max_input_tokens);
75 self
76 }
77}
78
79#[derive(Clone)]
80pub struct AnthropicMessagesAdapter {
82 config: AnthropicMessagesConfig,
83 api_key: ProviderApiKey,
84 http: Arc<dyn JsonHttpTransport>,
85 argument_sink: Option<Arc<dyn ProviderToolArgumentSink>>,
86}
87
88impl AnthropicMessagesAdapter {
89 pub fn from_env(model: impl Into<String>) -> Result<Self, AgentError> {
91 Self::new(
92 AnthropicMessagesConfig::new(model),
93 ProviderApiKey::from_env("ANTHROPIC_API_KEY")?,
94 )
95 }
96
97 pub fn new(
99 config: AnthropicMessagesConfig,
100 api_key: ProviderApiKey,
101 ) -> Result<Self, AgentError> {
102 Self::with_transport(config, api_key, Arc::new(CurlJsonHttpTransport::new()))
103 }
104
105 pub fn with_transport(
107 config: AnthropicMessagesConfig,
108 api_key: ProviderApiKey,
109 http: Arc<dyn JsonHttpTransport>,
110 ) -> Result<Self, AgentError> {
111 Ok(Self {
112 config,
113 api_key,
114 http,
115 argument_sink: None,
116 })
117 }
118
119 pub fn with_argument_sink(mut self, sink: Arc<dyn ProviderToolArgumentSink>) -> Self {
121 self.argument_sink = Some(sink);
122 self
123 }
124
125 fn wire_request(&self, request: &ProviderRequest) -> Value {
126 let mut system = Vec::new();
127 let mut messages = Vec::new();
128 for message in &request.messages {
129 match message.role {
130 ProviderMessageRole::System | ProviderMessageRole::Developer => {
131 system.push(message.content.clone());
132 }
133 ProviderMessageRole::Assistant => {
134 messages.push(json!({"role": "assistant", "content": message.content}));
135 }
136 ProviderMessageRole::Tool => {
137 messages.push(json!({
138 "role": "user",
139 "content": format!("Tool result:\n{}", message.content),
140 }));
141 }
142 ProviderMessageRole::Context | ProviderMessageRole::User => {
143 messages.push(json!({"role": "user", "content": message.content}));
144 }
145 }
146 }
147
148 let mut body = json!({
149 "model": self.config.model.clone(),
150 "max_tokens": self.config.max_tokens,
151 "messages": messages,
152 });
153 if !system.is_empty() {
154 body["system"] = Value::String(system.join("\n\n"));
155 }
156 if let Some(output_config) = anthropic_output_config(request) {
157 body["output_config"] = output_config;
158 }
159 body
160 }
161
162 fn map_response(
163 &self,
164 response: AnthropicMessagesResponse,
165 ) -> Result<ProviderResponse, AgentError> {
166 let tool_calls = self.tool_calls_from_response(&response)?;
167 let usage = response.usage.clone().map(ProviderUsage::from);
168 if !tool_calls.is_empty() {
169 let mut mapped = ProviderResponse::tool_use(tool_calls);
170 mapped.usage = usage;
171 return Ok(mapped);
172 }
173
174 Ok(ProviderResponse {
175 schema_version: ProviderResponse::SCHEMA_VERSION,
176 output_text: response.output_text(),
177 stop_reason: response.stop_reason(),
178 tool_calls: Vec::new(),
179 usage,
180 })
181 }
182
183 fn tool_calls_from_response(
184 &self,
185 response: &AnthropicMessagesResponse,
186 ) -> Result<Vec<ProviderToolCall>, AgentError> {
187 let mut calls = Vec::new();
188 for item in &response.content {
189 if item.kind != "tool_use" {
190 continue;
191 }
192 let call_id = item.id.as_deref().ok_or_else(|| {
193 unsupported_response("Anthropic Messages", "tool_use block missing id")
194 })?;
195 let name = item.name.as_deref().ok_or_else(|| {
196 unsupported_response("Anthropic Messages", "tool_use block missing name")
197 })?;
198 let canonical_tool_name = CanonicalToolName::new(name);
199 let mut call = ProviderToolCall::new(
200 ToolCallId::new(call_id),
201 canonical_tool_name.clone(),
202 format!("provider requested tool {name} with arguments stored as content refs"),
203 );
204 if let (Some(sink), Some(input)) = (self.argument_sink.as_ref(), item.input.as_ref()) {
205 let raw_arguments = serde_json::to_string(input).map_err(|error| {
206 provider_failure(
207 RetryClassification::RepairNeeded,
208 format!("Anthropic tool input could not be serialized: {error}"),
209 )
210 })?;
211 if let Some(args_ref) = sink.store_tool_arguments(
212 &self.config.provider_ref,
213 call_id,
214 &canonical_tool_name,
215 &raw_arguments,
216 )? {
217 call = call.with_args_ref(args_ref);
218 }
219 }
220 calls.push(call);
221 }
222 Ok(calls)
223 }
224}
225
226impl ProviderAdapter for AnthropicMessagesAdapter {
227 fn capabilities(&self) -> ProviderCapabilities {
228 let mut capabilities = ProviderCapabilities::text_only(self.config.provider_ref.clone());
229 capabilities.supports_usage = true;
230 capabilities.max_input_tokens = self.config.max_input_tokens;
231 capabilities
232 }
233
234 fn project_request(
235 &self,
236 projection: &agent_sdk_core::ContextProjection,
237 policy: &ProviderProjectionPolicy,
238 ) -> Result<ProviderRequest, AgentError> {
239 agent_sdk_core::projection::project_context_projection(projection, policy)
240 }
241
242 fn complete(&self, request: &ProviderRequest) -> Result<ProviderResponse, AgentError> {
243 let http_request =
244 JsonHttpRequest::new(self.config.endpoint_url.clone(), self.wire_request(request))
245 .header("x-api-key", self.api_key.expose_secret())
246 .header("anthropic-version", self.config.api_version.clone())
247 .header("Content-Type", "application/json");
248 let response = self.http.post_json(http_request)?;
249 let message = serde_json::from_value::<AnthropicMessagesResponse>(response.body)
250 .map_err(|error| unsupported_response("Anthropic Messages", error.to_string()))?;
251 self.map_response(message)
252 }
253}
254
255fn anthropic_output_config(request: &ProviderRequest) -> Option<Value> {
256 let hint = request.structured_output_hint.as_ref()?;
257 let schema = hint.redacted_schema.clone()?;
258 Some(json!({
259 "format": {
260 "type": "json_schema",
261 "schema": schema,
262 }
263 }))
264}
265
266#[derive(Clone, Deserialize, Eq, PartialEq, Serialize)]
267pub struct AnthropicMessagesResponse {
269 pub id: Option<String>,
271 #[serde(default)]
273 pub content: Vec<AnthropicContentBlock>,
274 pub stop_reason: Option<String>,
276 pub usage: Option<AnthropicUsage>,
278}
279
280impl AnthropicMessagesResponse {
281 pub fn text(text: impl Into<String>) -> Self {
283 Self {
284 id: Some("msg_test".to_string()),
285 content: vec![AnthropicContentBlock::text(text)],
286 stop_reason: Some("end_turn".to_string()),
287 usage: None,
288 }
289 }
290
291 pub fn tool_use(id: impl Into<String>, name: impl Into<String>, input: Value) -> Self {
293 Self {
294 id: Some("msg_tool".to_string()),
295 content: vec![AnthropicContentBlock::tool_use(id, name, input)],
296 stop_reason: Some("tool_use".to_string()),
297 usage: None,
298 }
299 }
300
301 fn output_text(&self) -> String {
302 self.content
303 .iter()
304 .filter(|item| item.kind == "text")
305 .filter_map(|item| item.text.as_deref())
306 .collect::<Vec<_>>()
307 .join("")
308 }
309
310 fn stop_reason(&self) -> ProviderStopReason {
311 match self.stop_reason.as_deref().unwrap_or("end_turn") {
312 "end_turn" => ProviderStopReason::EndTurn,
313 "max_tokens" => ProviderStopReason::MaxTokens,
314 "tool_use" => ProviderStopReason::ToolUse,
315 "stop_sequence" => ProviderStopReason::EndTurn,
316 _ => ProviderStopReason::Unknown,
317 }
318 }
319}
320
321impl fmt::Debug for AnthropicMessagesResponse {
322 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
323 formatter
324 .debug_struct("AnthropicMessagesResponse")
325 .field("id", &self.id)
326 .field("content_count", &self.content.len())
327 .field("content", &self.content)
328 .field("stop_reason", &self.stop_reason)
329 .field("usage", &self.usage)
330 .finish()
331 }
332}
333
334#[derive(Clone, Deserialize, Eq, PartialEq, Serialize)]
335pub struct AnthropicContentBlock {
337 #[serde(rename = "type")]
338 pub kind: String,
340 pub text: Option<String>,
342 pub id: Option<String>,
344 pub name: Option<String>,
346 pub input: Option<Value>,
348}
349
350impl AnthropicContentBlock {
351 pub fn text(text: impl Into<String>) -> Self {
353 Self {
354 kind: "text".to_string(),
355 text: Some(text.into()),
356 id: None,
357 name: None,
358 input: None,
359 }
360 }
361
362 pub fn tool_use(id: impl Into<String>, name: impl Into<String>, input: Value) -> Self {
364 Self {
365 kind: "tool_use".to_string(),
366 text: None,
367 id: Some(id.into()),
368 name: Some(name.into()),
369 input: Some(input),
370 }
371 }
372}
373
374impl fmt::Debug for AnthropicContentBlock {
375 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
376 formatter
377 .debug_struct("AnthropicContentBlock")
378 .field("kind", &self.kind)
379 .field(
380 "text_chars",
381 &self.text.as_ref().map(|value| value.chars().count()),
382 )
383 .field("id", &self.id)
384 .field("name", &self.name)
385 .field("input", &"<redacted>")
386 .field("input_present", &self.input.is_some())
387 .finish()
388 }
389}
390
391#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
392pub struct AnthropicUsage {
394 pub input_tokens: Option<u32>,
396 pub output_tokens: Option<u32>,
398}
399
400impl From<AnthropicUsage> for ProviderUsage {
401 fn from(value: AnthropicUsage) -> Self {
402 let total_tokens = match (value.input_tokens, value.output_tokens) {
403 (Some(input), Some(output)) => Some(input + output),
404 _ => None,
405 };
406 Self {
407 input_tokens: value.input_tokens,
408 output_tokens: value.output_tokens,
409 total_tokens,
410 }
411 }
412}