opendev_http/adapters/anthropic/
mod.rs1mod request;
10mod response;
11
12use serde_json::{Value, json};
13
14const DEFAULT_API_URL: &str = "https://api.anthropic.com/v1/messages";
15const ANTHROPIC_VERSION: &str = "2023-06-01";
16
17#[derive(Debug, Clone)]
19pub struct AnthropicAdapter {
20 api_url: String,
21 enable_caching: bool,
22}
23
24impl AnthropicAdapter {
25 pub fn new() -> Self {
27 Self {
28 api_url: DEFAULT_API_URL.to_string(),
29 enable_caching: true,
30 }
31 }
32
33 pub fn with_url(url: impl Into<String>) -> Self {
35 Self {
36 api_url: url.into(),
37 enable_caching: true,
38 }
39 }
40
41 pub fn with_caching(mut self, enable: bool) -> Self {
43 self.enable_caching = enable;
44 self
45 }
46}
47
48fn supports_thinking(model: &str) -> bool {
50 let m = model.to_lowercase();
51 m.starts_with("claude-3-7")
52 || m.starts_with("claude-3.7")
53 || m.starts_with("claude-4")
54 || m.starts_with("claude-opus")
55 || m.starts_with("claude-sonnet-4")
56 || m.starts_with("claude-sonnet-5")
57}
58
59fn supports_adaptive_thinking(model: &str) -> bool {
63 let m = model.to_lowercase();
64 m.contains("opus-4-6")
65 || m.contains("opus-4.6")
66 || m.contains("sonnet-4-6")
67 || m.contains("sonnet-4.6")
68}
69
70impl Default for AnthropicAdapter {
71 fn default() -> Self {
72 Self::new()
73 }
74}
75
76#[async_trait::async_trait]
77impl super::base::ProviderAdapter for AnthropicAdapter {
78 fn provider_name(&self) -> &str {
79 "anthropic"
80 }
81
82 fn convert_request(&self, mut payload: Value) -> Value {
83 let reasoning_effort = payload
85 .as_object_mut()
86 .and_then(|obj| obj.remove("_reasoning_effort"))
87 .and_then(|v| v.as_str().map(String::from));
88
89 Self::extract_system(&mut payload);
90 Self::convert_image_blocks(&mut payload);
91 Self::convert_tools(&mut payload);
92 Self::convert_tool_messages(&mut payload);
93 Self::ensure_max_tokens(&mut payload);
94
95 let model = payload
97 .get("model")
98 .and_then(|m| m.as_str())
99 .unwrap_or("")
100 .to_string();
101 if let Some(ref effort) = reasoning_effort
102 && effort != "none"
103 && supports_thinking(&model)
104 {
105 if supports_adaptive_thinking(&model) {
106 match effort.as_str() {
109 "low" => {
110 payload["thinking"] = json!({
111 "type": "adaptive",
112 "budget_tokens": 8000
113 });
114 }
115 "medium" => {
116 payload["thinking"] = json!({
117 "type": "adaptive",
118 "budget_tokens": 16000
119 });
120 }
121 _ => {
122 payload["thinking"] = json!({
124 "type": "adaptive"
125 });
126 }
127 }
128 } else {
129 let budget_tokens: u64 = match effort.as_str() {
131 "low" => 4000,
132 "medium" => 16000,
133 "high" => 31999,
134 _ => 16000,
135 };
136 payload["thinking"] = json!({
137 "type": "enabled",
138 "budget_tokens": budget_tokens
139 });
140 let current_max = payload
142 .get("max_tokens")
143 .and_then(|v| v.as_u64())
144 .unwrap_or(16384);
145 let min_max = budget_tokens + 1024;
146 if current_max < min_max {
147 payload["max_tokens"] = json!(min_max);
148 }
149 }
150 payload["temperature"] = json!(1);
152 }
153
154 if self.enable_caching {
155 Self::add_cache_control(&mut payload);
156 }
157
158 if let Some(obj) = payload.as_object_mut() {
160 obj.remove("n");
161 obj.remove("frequency_penalty");
162 obj.remove("presence_penalty");
163 obj.remove("logprobs");
164 }
165
166 payload
167 }
168
169 fn convert_response(&self, response: Value) -> Value {
170 Self::response_to_chat_completions(response)
171 }
172
173 fn api_url(&self) -> &str {
174 &self.api_url
175 }
176
177 fn supports_streaming(&self) -> bool {
178 true
179 }
180
181 fn enable_streaming(&self, payload: &mut Value) {
182 payload["stream"] = json!(true);
183 }
184
185 fn parse_stream_event(
186 &self,
187 event_type: &str,
188 data: &Value,
189 ) -> Option<crate::streaming::StreamEvent> {
190 self.parse_stream_event_impl(event_type, data)
191 }
192
193 fn extra_headers(&self) -> Vec<(String, String)> {
194 let mut headers = vec![("anthropic-version".into(), ANTHROPIC_VERSION.into())];
195 let mut beta_features = Vec::new();
197 if self.enable_caching {
198 beta_features.push("prompt-caching-2024-07-31");
199 }
200 beta_features.push("interleaved-thinking-2025-05-14");
202 if !beta_features.is_empty() {
203 headers.push(("anthropic-beta".into(), beta_features.join(",")));
204 }
205 headers
206 }
207}
208
209#[cfg(test)]
210mod tests;