//! Implements the Anthropic provider adapter. use async_stream::try_stream; use base64::{engine::general_purpose::STANDARD, Engine}; use eventsource_stream::Eventsource; use futures_util::StreamExt; use serde_json::{json, Value}; use crate::{ config::ProviderConfig, model::{ContentPart, ModelInvocation, ProjectedContent, ProjectedMessage, Role, Usage}, Error, Result, }; use super::{ map_sse_error, merge_extra_params, provider_event_error, recorder::recorded_headers, retry::{send_with_retry, Attempt, RetryPolicy}, CallRecorder, FinishReason, ModelEvent, Provider, ProviderStream, }; const DEFAULT_MAX_OUTPUT_TOKENS: u64 = 65_000; pub struct AnthropicProvider { client: reqwest::Client, config: ProviderConfig, recorder: Option, } impl AnthropicProvider { pub fn new(client: reqwest::Client, config: ProviderConfig) -> Self { Self { client, config, recorder: None, } } pub fn with_recorder(mut self, recorder: Option) -> Self { self.recorder = recorder; self } } impl Provider for AnthropicProvider { fn stream( &self, invocation: ModelInvocation, cancellation: tokio_util::sync::CancellationToken, ) -> ProviderStream { let client = self.client.clone(); let config = self.config.clone(); let recorder = self.recorder.clone(); Box::pin(try_stream! { let ModelInvocation { call_id, request, .. } = invocation; let mut messages = anthropic_messages(&request.history)?; mark_cache_breakpoint(&mut messages); let system = if request.prompt.instructions.is_empty() { Value::String(String::new()) } else { json!([{ "type": "text", "text": request.prompt.instructions, "cache_control": {"type": "ephemeral"} }]) }; let max_tokens = request.model.max_output_tokens.or(config.max_output_tokens) .unwrap_or(DEFAULT_MAX_OUTPUT_TOKENS); let mut body = json!({ "model": request.model.model_id, "system": system, "messages": messages, "max_tokens": max_tokens, "stream": true }); if !request.prompt.tools.is_empty() { let tool_count = request.prompt.tools.len(); body["tools"] = json!(request.prompt.tools.iter().enumerate().map(|(index, tool)| { let mut value = json!({ "name": tool.name, "description": tool.description, "input_schema": tool.parameters }); if index + 1 == tool_count { value["cache_control"] = json!({"type": "ephemeral"}); } value }).collect::>()); } apply_model(&mut body, &request.model)?; merge_extra_params(&mut body, &request.model.extra_params)?; let request_headers = recorded_headers( &config, &[("content-type", "application/json"), ("anthropic-version", "2023-06-01")], ); if let Some(recorder) = &recorder { recorder.request(request_headers.clone(), &body).await?; } let attempt = send_with_retry( "Anthropic", || client.post(&config.request_url) .header("x-api-key", &config.api_key).header("anthropic-version", "2023-06-01") .headers(config.custom_headers.clone()) .json(&body), RetryPolicy { retries: config.retry_count, ..RetryPolicy::default() }, &cancellation, recorder.as_ref(), request_headers, &body, ).await?; let Attempt::Response(response) = attempt else { return }; yield ModelEvent::Start { model_call_id: call_id }; let chunk_recorder = recorder.clone(); let chunks = response.bytes_stream() .map(|chunk| chunk.map_err(Error::from)) .then(move |chunk| { let recorder = chunk_recorder.clone(); async move { let chunk = chunk?; if let Some(recorder) = recorder { recorder.response_chunk(&chunk).await?; } Ok::<_, Error>(chunk) } }); let source = chunks.eventsource(); futures_util::pin_mut!(source); let mut block_types = std::collections::BTreeMap::::new(); let mut thinking_text = std::collections::HashMap::::new(); let mut thinking_signatures = std::collections::HashMap::::new(); let mut thinking_blocks = Vec::new(); let mut finish = None; let mut saw_tool = false; let mut terminal = false; let mut final_usage = None::; while let Some(event) = tokio::select! { _ = cancellation.cancelled() => { return; } event = source.next() => event, } { let event = event.map_err(|error| map_sse_error("Anthropic", error))?; let value: Value = serde_json::from_str(&event.data)?; if let Some(error) = provider_event_error("Anthropic", &value) { Err(error)?; } let data_kind = value.get("type").and_then(Value::as_str); let kind = match event.event.as_str() { "" | "message" => data_kind.unwrap_or(event.event.as_str()), kind => kind, }; match kind { "message_start" => if let Some(usage) = value.pointer("/message/usage") { merge_usage(final_usage.get_or_insert_default(), anthropic_usage(usage)); }, "content_block_start" => { let index = required_u64(&value, "index")? as usize; let block = value.get("content_block").unwrap_or(&Value::Null); let kind = required_string(block, "type")?; block_types.insert(index, kind.into()); match kind { "text" => yield ModelEvent::TextStart, "thinking" => { thinking_text.insert(index, String::new()); thinking_signatures.insert(index, String::new()); yield ModelEvent::ThinkingStart; } "redacted_thinking" => thinking_blocks.push(block.clone()), "tool_use" => { saw_tool = true; yield ModelEvent::ToolCallStart { index, call_id: required_string(block, "id")?.into(), name: required_string(block, "name")?.into(), }; } _ => {} } } "content_block_delta" => { let index = required_u64(&value, "index")? as usize; let delta = value.get("delta").unwrap_or(&Value::Null); let delta_kind = required_string(delta, "type")?; if let std::collections::btree_map::Entry::Vacant(entry) = block_types.entry(index) { match delta_kind { "text_delta" => { entry.insert("text".into()); yield ModelEvent::TextStart; } "thinking_delta" | "signature_delta" => { entry.insert("thinking".into()); thinking_text.insert(index, String::new()); thinking_signatures.insert(index, String::new()); yield ModelEvent::ThinkingStart; } _ => {} } } match delta_kind { "text_delta" => if let Some(text) = delta.get("text").and_then(Value::as_str) { yield ModelEvent::TextDelta(text.into()); }, "thinking_delta" => if let Some(text) = delta.get("thinking").and_then(Value::as_str) { thinking_text.entry(index).or_default().push_str(text); yield ModelEvent::ThinkingDelta(text.into()); }, "signature_delta" => if let Some(signature) = delta.get("signature").and_then(Value::as_str) { thinking_signatures.entry(index).or_default().push_str(signature); }, "input_json_delta" => if let Some(text) = delta.get("partial_json").and_then(Value::as_str) { yield ModelEvent::ToolCallArgumentsDelta { index, delta: text.into() }; }, _ => {} } } "content_block_stop" => { let index = required_u64(&value, "index")? as usize; if let Some(kind) = block_types.remove(&index) { for event in close_anthropic_block(index, &kind, &mut thinking_text, &mut thinking_signatures, &mut thinking_blocks) { yield event; } } } "message_delta" => { if let Some(usage) = value.get("usage") { merge_usage(final_usage.get_or_insert_default(), anthropic_usage(usage)); } finish = match value.pointer("/delta/stop_reason").and_then(Value::as_str) { Some("tool_use") => Some(FinishReason::ToolUse), Some("max_tokens" | "model_context_window_exceeded") => Some(FinishReason::Length), Some("end_turn" | "stop_sequence" | "pause_turn" | "refusal") => Some(FinishReason::Stop), None => finish, Some(_) => Some(if saw_tool { FinishReason::ToolUse } else { FinishReason::Stop }), }; } "message_stop" => { for (index, kind) in std::mem::take(&mut block_types) { for event in close_anthropic_block(index, &kind, &mut thinking_text, &mut thinking_signatures, &mut thinking_blocks) { yield event; } } terminal = true; if !thinking_blocks.is_empty() { yield ModelEvent::ProviderReplayState( crate::model::ProviderReplayState { provider_kind: "anthropic".into(), value: json!({"blocks": std::mem::take(&mut thinking_blocks)}), }, ); } if let Some(usage) = final_usage { yield ModelEvent::Usage(usage); } let finish = match finish { Some(FinishReason::Length) => FinishReason::Length, _ if saw_tool => FinishReason::ToolUse, Some(finish) => finish, None => FinishReason::Stop, }; yield ModelEvent::Done(finish); } _ => {} } } if !terminal && finish.is_some() { for (index, kind) in std::mem::take(&mut block_types) { for event in close_anthropic_block(index, &kind, &mut thinking_text, &mut thinking_signatures, &mut thinking_blocks) { yield event; } } if !thinking_blocks.is_empty() { yield ModelEvent::ProviderReplayState(crate::model::ProviderReplayState { provider_kind: "anthropic".into(), value: json!({"blocks": std::mem::take(&mut thinking_blocks)}), }); } if let Some(usage) = final_usage { yield ModelEvent::Usage(usage); } terminal = true; let finish = match finish { Some(FinishReason::Length) => FinishReason::Length, _ if saw_tool => FinishReason::ToolUse, Some(finish) => finish, None => FinishReason::Stop, }; yield ModelEvent::Done(finish); } if !terminal { Err(Error::Provider("Anthropic stream ended without message_stop".into()))?; } }) } } fn close_anthropic_block( index: usize, kind: &str, thinking_text: &mut std::collections::HashMap, thinking_signatures: &mut std::collections::HashMap, thinking_blocks: &mut Vec, ) -> Vec { match kind { "text" => vec![ModelEvent::TextEnd], "thinking" => { let thinking = thinking_text.remove(&index).unwrap_or_default(); let signature = thinking_signatures.remove(&index).unwrap_or_default(); if !signature.is_empty() { thinking_blocks.push(json!({ "type": "thinking", "thinking": thinking, "signature": signature, })); } vec![ModelEvent::ThinkingEnd] } "tool_use" => vec![ModelEvent::ToolCallEnd { index }], _ => Vec::new(), } } fn apply_model(body: &mut Value, model: &crate::model::ModelSpec) -> Result<()> { let object = body .as_object_mut() .ok_or_else(|| Error::Provider("Anthropic request body is not an object".into()))?; if model.reasoning.enabled { object.insert( "thinking".into(), json!({"type":"adaptive", "display":"summarized"}), ); } if let Some(effort) = &model.reasoning.effort { object.insert("output_config".into(), json!({"effort":effort})); } Ok(()) } fn merge_usage(total: &mut Usage, update: Usage) { merge_usage_field(&mut total.input_tokens, update.input_tokens); merge_usage_field(&mut total.output_tokens, update.output_tokens); merge_usage_field(&mut total.cache_read_tokens, update.cache_read_tokens); merge_usage_field(&mut total.cache_write_tokens, update.cache_write_tokens); merge_usage_field(&mut total.reasoning_tokens, update.reasoning_tokens); } fn merge_usage_field(total: &mut Option, update: Option) { if let Some(update) = update { *total = Some(total.map_or(update, |current| current.max(update))); } } fn anthropic_messages(messages: &[ProjectedMessage]) -> Result> { let mut output = Vec::new(); for message in messages { match &message.content { ProjectedContent::Parts(parts) => { let content = anthropic_parts(&message.role, parts)?; if !content.is_empty() { push_anthropic(&mut output, role_name(&message.role), content); } } ProjectedContent::ToolResult(result) => { let content = if result.provider_parts.is_empty() { Value::String(result.content.clone()) } else { Value::Array(anthropic_parts(&Role::User, &result.provider_parts)?) }; push_anthropic( &mut output, "user", vec![json!({ "type": "tool_result", "tool_use_id": result.call_id, "content": content, })], ); } ProjectedContent::Assistant { text, replay_state, calls, .. } => { let mut content = Vec::new(); if let Some(blocks) = replay_state .as_ref() .filter(|state| state.provider_kind == "anthropic") .and_then(|state| state.value.get("blocks")) .and_then(Value::as_array) { content.extend(blocks.iter().cloned()); } if !text.is_empty() { content.push(json!({"type": "text", "text": text})); } content.extend(calls.iter().map(|call| { json!({ "type": "tool_use", "id": call.call_id, "name": call.name, "input": call.arguments, }) })); if !content.is_empty() { push_anthropic(&mut output, "assistant", content); } } } } Ok(output) } fn mark_cache_breakpoint(messages: &mut [Value]) { let Some(message) = messages .iter_mut() .rev() .find(|message| message.get("role").and_then(Value::as_str) == Some("user")) else { return; }; let Some(content) = message.get_mut("content").and_then(Value::as_array_mut) else { return; }; for block in content.iter_mut().rev() { let kind = block.get("type").and_then(Value::as_str); if !matches!(kind, Some("text" | "image" | "tool_result")) { continue; } if let Some(block) = block.as_object_mut() { block.insert("cache_control".into(), json!({"type": "ephemeral"})); return; } } } fn anthropic_parts(role: &Role, parts: &[ContentPart]) -> Result> { parts .iter() .filter_map(|part| match part { ContentPart::Text { text } if text.is_empty() => None, ContentPart::Text { text } => Some(Ok(json!({"type":"text", "text":text}))), ContentPart::Image { mime_type, data } if *role == Role::User => Some(Ok(json!({ "type":"image", "source":{ "type":"base64", "media_type":mime_type, "data":STANDARD.encode(data), }, }))), ContentPart::Image { .. } => Some(Err(Error::Protocol( "Anthropic only accepts images in user messages".into(), ))), }) .collect() } fn push_anthropic(output: &mut Vec, role: &str, mut content: Vec) { if let Some(last) = output .last_mut() .filter(|last| last.get("role").and_then(Value::as_str) == Some(role)) { if let Some(existing) = last.get_mut("content").and_then(Value::as_array_mut) { existing.append(&mut content); return; } } output.push(json!({"role":role, "content":content})); } fn role_name(role: &Role) -> &'static str { match role { Role::System => "user", Role::User => "user", Role::Assistant => "assistant", Role::Tool => "user", } } fn required_string<'a>(value: &'a Value, name: &str) -> Result<&'a str> { value .get(name) .and_then(Value::as_str) .ok_or_else(|| Error::Provider(format!("Anthropic event is missing {name}"))) } fn required_u64(value: &Value, name: &str) -> Result { value .get(name) .and_then(Value::as_u64) .ok_or_else(|| Error::Provider(format!("Anthropic event is missing {name}"))) } fn anthropic_usage(value: &Value) -> Usage { Usage { input_tokens: value.get("input_tokens").and_then(Value::as_u64), output_tokens: value.get("output_tokens").and_then(Value::as_u64), total_tokens: value.get("total_tokens").and_then(Value::as_u64), cache_read_tokens: value.get("cache_read_input_tokens").and_then(Value::as_u64), cache_write_tokens: value .get("cache_creation_input_tokens") .and_then(Value::as_u64), reasoning_tokens: None, } }