1use std::collections::BTreeMap;
2
3use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
4use serde::{Deserialize, Serialize};
5use serde_json::{Map, Value, json};
6
7use crate::{
8 Capabilities, ModelUsage, ProviderAdapter, ProviderError, ProviderRequest, ProviderResponse,
9 ProviderStreamDecoder, ProviderStreamEvent, SseEvent, Surface, is_rate_limit_payload,
10};
11
12const DEFAULT_MAX_TOKENS: u64 = 4096;
13const MIN_THINKING_BUDGET: u64 = 1024;
14pub const REASONING_DETAIL_FORMAT: &str = "anthropic-claude-v1";
15
16#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
17pub struct SignedThinking {
18 pub thinking: String,
19 pub signature: String,
20}
21
22pub fn encrypted_reasoning_detail(tool_call_id: &str, blocks: &[SignedThinking]) -> Value {
23 let data = BASE64.encode(serde_json::to_vec(blocks).unwrap_or_default());
24 json!({
25 "type": "reasoning.encrypted",
26 "id": tool_call_id,
27 "data": data,
28 "format": REASONING_DETAIL_FORMAT,
29 })
30}
31
32pub fn signed_thinking_from_details(message: &Value) -> Vec<SignedThinking> {
33 message
34 .get("reasoning_details")
35 .and_then(Value::as_array)
36 .into_iter()
37 .flatten()
38 .filter(|detail| {
39 detail.get("type").and_then(Value::as_str) == Some("reasoning.encrypted")
40 && detail
41 .get("format")
42 .and_then(Value::as_str)
43 .is_none_or(|format| format == REASONING_DETAIL_FORMAT)
44 })
45 .filter_map(|detail| detail.get("data").and_then(Value::as_str))
46 .filter_map(|data| BASE64.decode(data).ok())
47 .filter_map(|decoded| serde_json::from_slice::<Vec<SignedThinking>>(&decoded).ok())
48 .flatten()
49 .collect()
50}
51
52pub fn thinking_blocks_from_details(message: &Value) -> Vec<Value> {
53 signed_thinking_from_details(message)
54 .into_iter()
55 .map(|block| {
56 json!({
57 "type": "thinking",
58 "thinking": block.thinking,
59 "signature": block.signature,
60 })
61 })
62 .collect()
63}
64
65pub struct AnthropicAdapter;
66
67impl AnthropicAdapter {
68 pub const VERSION: &'static str = "2023-06-01";
69
70 pub fn new() -> Self {
71 Self
72 }
73}
74
75impl Default for AnthropicAdapter {
76 fn default() -> Self {
77 Self::new()
78 }
79}
80
81impl ProviderAdapter for AnthropicAdapter {
82 fn name(&self) -> &'static str {
83 "anthropic"
84 }
85
86 fn capabilities(&self) -> Capabilities {
87 Capabilities {
88 chat: true,
89 responses: false,
90 vision: true,
91 reasoning: true,
92 embeddings: false,
93 }
94 }
95
96 fn encode_request(
97 &self,
98 surface: Surface,
99 request: ProviderRequest,
100 ) -> Result<Value, ProviderError> {
101 if surface != Surface::ChatCompletions {
102 return Err(ProviderError::Unsupported(
103 "Anthropic supports chat completions through the native Messages adapter".into(),
104 ));
105 }
106 Ok(build_request(&request.model, &request.body))
107 }
108
109 fn decode_response(
110 &self,
111 surface: Surface,
112 response: Value,
113 ) -> Result<ProviderResponse, ProviderError> {
114 if surface != Surface::ChatCompletions {
115 return Err(ProviderError::Unsupported("Anthropic Responses API".into()));
116 }
117 let usage = anthropic_usage(response.get("usage").unwrap_or(&Value::Null));
118 let mut text = String::new();
119 let mut reasoning = String::new();
120 let mut signed_thinking = Vec::new();
121 let mut tool_calls = Vec::new();
122 for block in response
123 .get("content")
124 .and_then(Value::as_array)
125 .into_iter()
126 .flatten()
127 {
128 match block.get("type").and_then(Value::as_str) {
129 Some("text") => text.push_str(block.get("text").and_then(Value::as_str).unwrap_or_default()),
130 Some("thinking") => {
131 let thinking = block
132 .get("thinking")
133 .and_then(Value::as_str)
134 .unwrap_or_default();
135 reasoning.push_str(thinking);
136 if let Some(signature) = block.get("signature").and_then(Value::as_str) {
137 signed_thinking.push(SignedThinking {
138 thinking: thinking.to_owned(),
139 signature: signature.to_owned(),
140 });
141 }
142 }
143 Some("tool_use") => tool_calls.push(json!({
144 "id": block.get("id").cloned().unwrap_or(Value::Null),
145 "type": "function",
146 "function": {
147 "name": block.get("name").cloned().unwrap_or(Value::Null),
148 "arguments": serde_json::to_string(block.get("input").unwrap_or(&Value::Null)).unwrap_or_else(|_| "{}".into())
149 }
150 })),
151 _ => {}
152 }
153 }
154 let finish_reason = match response.get("stop_reason").and_then(Value::as_str) {
155 Some("tool_use") => "tool_calls",
156 Some("max_tokens") => "length",
157 _ => "stop",
158 };
159 let mut message = json!({
160 "role": "assistant",
161 "content": if text.is_empty() { Value::Null } else { json!(text) }
162 });
163 if !reasoning.is_empty() {
164 message["reasoning_content"] = json!(reasoning);
165 }
166 if !tool_calls.is_empty() {
167 if !signed_thinking.is_empty() {
168 let tool_call_id = tool_calls[0]
169 .get("id")
170 .and_then(Value::as_str)
171 .unwrap_or_default();
172 message["reasoning_details"] =
173 json!([encrypted_reasoning_detail(tool_call_id, &signed_thinking)]);
174 }
175 message["tool_calls"] = Value::Array(tool_calls);
176 }
177 Ok(ProviderResponse {
178 body: json!({
179 "id": response.get("id").cloned().unwrap_or(Value::Null),
180 "object": "chat.completion",
181 "model": response.get("model").cloned().unwrap_or(Value::Null),
182 "choices": [{ "index": 0, "message": message, "finish_reason": finish_reason }],
183 "usage": openai_usage(usage)
184 }),
185 usage,
186 })
187 }
188
189 fn stream_decoder(
190 &self,
191 surface: Surface,
192 ) -> Result<Box<dyn ProviderStreamDecoder>, ProviderError> {
193 if surface != Surface::ChatCompletions {
194 return Err(ProviderError::Unsupported("Anthropic Responses API".into()));
195 }
196 Ok(Box::new(AnthropicStreamDecoder::default()))
197 }
198}
199
200#[derive(Default)]
201struct AnthropicStreamDecoder {
202 usage: ModelUsage,
203 first_delta: bool,
204 pending_thinking: BTreeMap<u64, SignedThinking>,
205 completed_thinking: Vec<SignedThinking>,
206 tool_blocks: BTreeMap<u64, u64>,
207 next_tool_index: u64,
208 thinking_attached: bool,
209 saw_tool_call: bool,
210 stop_reason: Option<String>,
211 terminal_emitted: bool,
212 done: bool,
213}
214
215impl AnthropicStreamDecoder {
216 fn data(&mut self, mut delta: Value) -> ProviderStreamEvent {
217 if !self.first_delta {
218 if let Some(delta) = delta.as_object_mut() {
219 delta.insert("role".into(), json!("assistant"));
220 }
221 self.first_delta = true;
222 }
223 ProviderStreamEvent::Data {
224 event: None,
225 data: json!({
226 "choices": [{
227 "index": 0,
228 "delta": delta,
229 "finish_reason": null
230 }]
231 }),
232 }
233 }
234
235 fn terminal(&self) -> ProviderStreamEvent {
236 let finish_reason = map_stop_reason(self.stop_reason.as_deref(), self.saw_tool_call);
237 ProviderStreamEvent::Data {
238 event: None,
239 data: json!({
240 "choices": [{
241 "index": 0,
242 "delta": {},
243 "finish_reason": finish_reason
244 }],
245 "usage": openai_usage(self.usage)
246 }),
247 }
248 }
249}
250
251impl ProviderStreamDecoder for AnthropicStreamDecoder {
252 fn decode(&mut self, event: SseEvent) -> Result<Vec<ProviderStreamEvent>, ProviderError> {
253 let data: Value = serde_json::from_str(&event.data)
254 .map_err(|error| ProviderError::InvalidStream(error.to_string()))?;
255 let kind = event
256 .event
257 .as_deref()
258 .or_else(|| data.get("type").and_then(Value::as_str));
259 match kind {
260 Some("message_start") => {
261 merge_anthropic_usage(
262 &mut self.usage,
263 data.pointer("/message/usage").unwrap_or(&Value::Null),
264 );
265 Ok(Vec::new())
266 }
267 Some("content_block_start") => {
268 let index = data.get("index").and_then(Value::as_u64).unwrap_or(0);
269 let block = data.get("content_block").unwrap_or(&Value::Null);
270 match block.get("type").and_then(Value::as_str) {
271 Some("thinking") => {
272 self.pending_thinking.insert(
273 index,
274 SignedThinking {
275 thinking: block
276 .get("thinking")
277 .and_then(Value::as_str)
278 .unwrap_or_default()
279 .to_owned(),
280 signature: block
281 .get("signature")
282 .and_then(Value::as_str)
283 .unwrap_or_default()
284 .to_owned(),
285 },
286 );
287 Ok(Vec::new())
288 }
289 Some("tool_use") => {
290 let tool_index = self.next_tool_index;
291 self.next_tool_index = self.next_tool_index.saturating_add(1);
292 self.tool_blocks.insert(index, tool_index);
293 self.saw_tool_call = true;
294 let id = block.get("id").and_then(Value::as_str).unwrap_or_default();
295 let mut delta = json!({
296 "tool_calls": [{
297 "index": tool_index,
298 "id": id,
299 "type": "function",
300 "function": {
301 "name": block
302 .get("name")
303 .and_then(Value::as_str)
304 .unwrap_or_default(),
305 "arguments": ""
306 }
307 }]
308 });
309 if !self.thinking_attached && !self.completed_thinking.is_empty() {
310 delta["reasoning_details"] =
311 json!([encrypted_reasoning_detail(id, &self.completed_thinking)]);
312 self.thinking_attached = true;
313 }
314 Ok(vec![self.data(delta)])
315 }
316 _ => Ok(Vec::new()),
317 }
318 }
319 Some("content_block_delta") => {
320 let index = data.get("index").and_then(Value::as_u64).unwrap_or(0);
321 let delta = data.get("delta").unwrap_or(&Value::Null);
322 match delta.get("type").and_then(Value::as_str) {
323 Some("text_delta") => Ok(vec![self.data(json!({
324 "content": delta.get("text").and_then(Value::as_str).unwrap_or_default()
325 }))]),
326 Some("thinking_delta") => {
327 let thinking = delta
328 .get("thinking")
329 .and_then(Value::as_str)
330 .unwrap_or_default();
331 self.pending_thinking
332 .entry(index)
333 .or_insert_with(|| SignedThinking {
334 thinking: String::new(),
335 signature: String::new(),
336 })
337 .thinking
338 .push_str(thinking);
339 Ok(vec![self.data(json!({ "reasoning_content": thinking }))])
340 }
341 Some("signature_delta") => {
342 let signature = delta
343 .get("signature")
344 .and_then(Value::as_str)
345 .unwrap_or_default();
346 self.pending_thinking
347 .entry(index)
348 .or_insert_with(|| SignedThinking {
349 thinking: String::new(),
350 signature: String::new(),
351 })
352 .signature
353 .push_str(signature);
354 Ok(Vec::new())
355 }
356 Some("input_json_delta") => {
357 let Some(tool_index) = self.tool_blocks.get(&index).copied() else {
358 return Err(ProviderError::InvalidStream(format!(
359 "input_json_delta for unknown content block {index}"
360 )));
361 };
362 Ok(vec![self.data(json!({
363 "tool_calls": [{
364 "index": tool_index,
365 "function": {
366 "arguments": delta
367 .get("partial_json")
368 .and_then(Value::as_str)
369 .unwrap_or_default()
370 }
371 }]
372 }))])
373 }
374 _ => Ok(Vec::new()),
375 }
376 }
377 Some("content_block_stop") => {
378 let index = data.get("index").and_then(Value::as_u64).unwrap_or(0);
379 if let Some(thinking) = self.pending_thinking.remove(&index)
380 && !thinking.signature.is_empty()
381 {
382 self.completed_thinking.push(thinking);
383 }
384 self.tool_blocks.remove(&index);
385 Ok(Vec::new())
386 }
387 Some("message_delta") => {
388 merge_anthropic_usage(&mut self.usage, data.get("usage").unwrap_or(&Value::Null));
389 if let Some(reason) = data.pointer("/delta/stop_reason").and_then(Value::as_str) {
390 self.stop_reason = Some(reason.to_owned());
391 }
392 self.terminal_emitted = true;
393 Ok(vec![self.terminal()])
394 }
395 Some("message_stop") => {
396 let mut events = Vec::with_capacity(2);
397 if !self.terminal_emitted {
398 events.push(self.terminal());
399 self.terminal_emitted = true;
400 }
401 if !self.done {
402 events.push(ProviderStreamEvent::Done(self.usage));
403 self.done = true;
404 }
405 Ok(events)
406 }
407 Some("error") => {
408 let message = data
409 .pointer("/error/message")
410 .and_then(Value::as_str)
411 .unwrap_or("Anthropic stream error")
412 .to_owned();
413 if crate::is_rate_limit_payload(&data) {
414 Err(ProviderError::RateLimitedStream(message))
415 } else {
416 Err(ProviderError::InvalidStream(message))
417 }
418 }
419 _ => Ok(Vec::new()),
420 }
421 }
422
423 fn finish(&mut self) -> Result<Vec<ProviderStreamEvent>, ProviderError> {
424 if self.done {
425 return Ok(Vec::new());
426 }
427 let mut events = Vec::with_capacity(2);
428 if !self.terminal_emitted {
429 events.push(self.terminal());
430 self.terminal_emitted = true;
431 }
432 events.push(ProviderStreamEvent::Done(self.usage));
433 self.done = true;
434 Ok(events)
435 }
436}
437
438pub fn native_message_usage(response: &Value) -> ModelUsage {
443 anthropic_usage(response.get("usage").unwrap_or(&Value::Null))
444}
445
446#[derive(Default)]
451pub struct NativeMessagesDecoder {
452 usage: ModelUsage,
453 done: bool,
454}
455
456impl NativeMessagesDecoder {
457 pub fn new() -> Self {
458 Self::default()
459 }
460}
461
462impl ProviderStreamDecoder for NativeMessagesDecoder {
463 fn decode(&mut self, event: SseEvent) -> Result<Vec<ProviderStreamEvent>, ProviderError> {
464 let data: Value = serde_json::from_str(&event.data)
465 .map_err(|error| ProviderError::InvalidStream(error.to_string()))?;
466 let kind = event
467 .event
468 .clone()
469 .or_else(|| data.get("type").and_then(Value::as_str).map(str::to_owned));
470 if is_rate_limit_payload(&data) {
471 return Err(ProviderError::RateLimitedStream(
472 data.pointer("/error/message")
473 .and_then(Value::as_str)
474 .unwrap_or("Anthropic stream rate limited")
475 .to_owned(),
476 ));
477 }
478 match kind.as_deref() {
479 Some("message_start") => merge_anthropic_usage(
480 &mut self.usage,
481 data.pointer("/message/usage").unwrap_or(&Value::Null),
482 ),
483 Some("message_delta") => {
484 merge_anthropic_usage(&mut self.usage, data.get("usage").unwrap_or(&Value::Null))
485 }
486 Some("error") => {
487 return Err(ProviderError::InvalidStream(
488 data.pointer("/error/message")
489 .and_then(Value::as_str)
490 .unwrap_or("Anthropic stream error")
491 .to_owned(),
492 ));
493 }
494 _ => {}
495 }
496 let terminal = kind.as_deref() == Some("message_stop");
497 let mut events = vec![ProviderStreamEvent::Data { event: kind, data }];
498 if terminal && !self.done {
499 self.done = true;
500 events.push(ProviderStreamEvent::Done(self.usage));
501 }
502 Ok(events)
503 }
504
505 fn finish(&mut self) -> Result<Vec<ProviderStreamEvent>, ProviderError> {
506 if self.done {
507 return Ok(Vec::new());
508 }
509 self.done = true;
510 Ok(vec![ProviderStreamEvent::Done(self.usage)])
511 }
512}
513
514fn build_request(model: &str, body: &Value) -> Value {
515 let max_tokens = body
516 .get("max_tokens")
517 .or_else(|| body.get("max_completion_tokens"))
518 .and_then(Value::as_u64)
519 .unwrap_or(DEFAULT_MAX_TOKENS);
520 let thinking = thinking_budget(body, max_tokens);
521 let mut request = Map::from_iter([
522 ("model".into(), json!(model)),
523 ("max_tokens".into(), json!(max_tokens)),
524 ("messages".into(), Value::Array(Vec::new())),
525 ]);
526 let (system, messages) = build_messages(body, thinking.is_some());
527 request.insert("messages".into(), Value::Array(messages));
528 if !system.is_empty() {
529 request.insert("system".into(), json!(system));
530 }
531 if let Some(tools) = parse_tools(body) {
532 request.insert("tools".into(), Value::Array(tools));
533 if let Some(choice) = parse_tool_choice(body) {
534 request.insert("tool_choice".into(), choice);
535 }
536 }
537 if let Some(budget) = thinking {
538 request.insert(
539 "thinking".into(),
540 json!({ "type": "enabled", "budget_tokens": budget }),
541 );
542 } else {
543 if let Some(value) = body.get("temperature") {
544 request.insert("temperature".into(), value.clone());
545 }
546 if let Some(value) = body.get("top_p") {
547 request.insert("top_p".into(), value.clone());
548 }
549 }
550 if let Some(stops) = parse_stop_sequences(body) {
551 request.insert("stop_sequences".into(), Value::Array(stops));
552 }
553 request.insert(
554 "stream".into(),
555 body.get("stream").cloned().unwrap_or(json!(false)),
556 );
557 Value::Object(request)
558}
559
560fn thinking_budget(body: &Value, max_tokens: u64) -> Option<u64> {
561 let fraction = match body.get("reasoning_effort").and_then(Value::as_str)? {
562 "minimal" => 10,
563 "low" => 20,
564 "medium" => 50,
565 "high" | "xhigh" => 80,
566 _ => return None,
567 };
568 let cap = max_tokens.checked_sub(MIN_THINKING_BUDGET)?;
569 if cap < MIN_THINKING_BUDGET {
570 return None;
571 }
572 let scaled = max_tokens.saturating_mul(fraction) / 100;
573 Some(scaled.clamp(MIN_THINKING_BUDGET, cap))
574}
575
576fn parse_tools(body: &Value) -> Option<Vec<Value>> {
577 let tools = body
578 .get("tools")
579 .and_then(Value::as_array)?
580 .iter()
581 .filter_map(|tool| {
582 let function = tool.get("function")?;
583 let name = function.get("name")?.as_str()?;
584 let mut translated = Map::new();
585 translated.insert("name".into(), json!(name));
586 if let Some(description) = function.get("description").and_then(Value::as_str) {
587 translated.insert("description".into(), json!(description));
588 }
589 translated.insert(
590 "input_schema".into(),
591 function
592 .get("parameters")
593 .cloned()
594 .unwrap_or_else(|| json!({ "type": "object" })),
595 );
596 Some(Value::Object(translated))
597 })
598 .collect::<Vec<_>>();
599 (!tools.is_empty()).then_some(tools)
600}
601
602fn parse_tool_choice(body: &Value) -> Option<Value> {
603 match body.get("tool_choice")? {
604 Value::String(choice) => match choice.as_str() {
605 "auto" => Some(json!({ "type": "auto" })),
606 "required" => Some(json!({ "type": "any" })),
607 "none" => Some(json!({ "type": "none" })),
608 _ => None,
609 },
610 choice @ Value::Object(_) => Some(json!({
611 "type": "tool",
612 "name": choice.pointer("/function/name")?.as_str()?
613 })),
614 _ => None,
615 }
616}
617
618fn parse_stop_sequences(body: &Value) -> Option<Vec<Value>> {
619 match body.get("stop")? {
620 Value::String(stop) => Some(vec![json!(stop)]),
621 Value::Array(stops) => {
622 let stops = stops
623 .iter()
624 .filter(|stop| stop.is_string())
625 .cloned()
626 .collect::<Vec<_>>();
627 (!stops.is_empty()).then_some(stops)
628 }
629 _ => None,
630 }
631}
632
633fn build_messages(body: &Value, thinking_enabled: bool) -> (String, Vec<Value>) {
634 let mut system = String::new();
635 let mut messages: Vec<Value> = Vec::new();
636 for message in body
637 .get("messages")
638 .and_then(Value::as_array)
639 .into_iter()
640 .flatten()
641 {
642 let role = message
643 .get("role")
644 .and_then(Value::as_str)
645 .unwrap_or("user");
646 if matches!(role, "system" | "developer") {
647 let text = content_text(message.get("content"));
648 if !text.is_empty() {
649 if !system.is_empty() {
650 system.push('\n');
651 }
652 system.push_str(&text);
653 }
654 continue;
655 }
656 let output_role = if role == "assistant" {
657 "assistant"
658 } else {
659 "user"
660 };
661 let mut blocks = if role == "tool" {
662 vec![json!({
663 "type": "tool_result",
664 "tool_use_id": message.get("tool_call_id").cloned().unwrap_or(Value::Null),
665 "content": content_text(message.get("content"))
666 })]
667 } else {
668 content_blocks(message.get("content"))
669 };
670 if role == "assistant" {
671 if thinking_enabled {
672 let mut thinking = thinking_blocks_from_details(message);
673 thinking.append(&mut blocks);
674 blocks = thinking;
675 }
676 for call in message
677 .get("tool_calls")
678 .and_then(Value::as_array)
679 .into_iter()
680 .flatten()
681 {
682 let function = call.get("function").unwrap_or(&Value::Null);
683 let input = function
684 .get("arguments")
685 .and_then(Value::as_str)
686 .and_then(|value| serde_json::from_str(value).ok())
687 .unwrap_or_else(|| json!({}));
688 blocks.push(json!({
689 "type": "tool_use",
690 "id": call.get("id").cloned().unwrap_or(Value::Null),
691 "name": function.get("name").cloned().unwrap_or(Value::Null),
692 "input": input
693 }));
694 }
695 }
696 if blocks.is_empty() {
697 continue;
698 }
699 if let Some(previous) = messages.last_mut()
700 && previous.get("role").and_then(Value::as_str) == Some(output_role)
701 && let Some(content) = previous.get_mut("content").and_then(Value::as_array_mut)
702 {
703 content.extend(blocks);
704 } else {
705 messages.push(json!({ "role": output_role, "content": blocks }));
706 }
707 }
708 (system, messages)
709}
710
711fn content_text(content: Option<&Value>) -> String {
712 match content {
713 Some(Value::String(value)) => value.clone(),
714 Some(Value::Array(parts)) => parts
715 .iter()
716 .filter_map(|part| part.get("text").and_then(Value::as_str))
717 .collect(),
718 _ => String::new(),
719 }
720}
721
722fn content_blocks(content: Option<&Value>) -> Vec<Value> {
723 match content {
724 Some(Value::String(value)) if !value.is_empty() => {
725 vec![json!({ "type": "text", "text": value })]
726 }
727 Some(Value::Array(parts)) => parts
728 .iter()
729 .filter_map(|part| match part.get("type").and_then(Value::as_str) {
730 Some("text") => Some(json!({ "type": "text", "text": part.get("text")? })),
731 Some("image_url") => {
732 let url = part.pointer("/image_url/url").and_then(Value::as_str)?;
733 Some(image_block(url))
734 }
735 _ => None,
736 })
737 .collect(),
738 _ => Vec::new(),
739 }
740}
741
742fn image_block(url: &str) -> Value {
743 if let Some(data) = url.strip_prefix("data:")
744 && let Some((metadata, payload)) = data.split_once(',')
745 {
746 return json!({
747 "type": "image",
748 "source": {
749 "type": "base64",
750 "media_type": metadata.split(';').next().unwrap_or("image/png"),
751 "data": payload
752 }
753 });
754 }
755 json!({ "type": "image", "source": { "type": "url", "url": url } })
756}
757
758fn map_stop_reason(stop_reason: Option<&str>, saw_tool_call: bool) -> &'static str {
759 match stop_reason {
760 Some("tool_use") => "tool_calls",
761 Some("max_tokens") => "length",
762 Some(_) | None if saw_tool_call => "tool_calls",
763 _ => "stop",
764 }
765}
766
767fn openai_usage(usage: ModelUsage) -> Value {
768 json!({
771 "prompt_tokens": usage.input_tokens
772 .saturating_add(usage.cache_read_tokens)
773 .saturating_add(usage.cache_write_tokens),
774 "completion_tokens": usage.output_tokens,
775 "total_tokens": usage.total_tokens(),
776 "completion_tokens_details": {
777 "reasoning_tokens": usage.reasoning_tokens
778 },
779 "prompt_tokens_details": {
780 "cached_tokens": usage.cache_read_tokens,
781 "cache_write_tokens": usage.cache_write_tokens
782 }
783 })
784}
785
786fn merge_anthropic_usage(usage: &mut ModelUsage, value: &Value) {
787 let next = anthropic_usage(value);
788 if value.get("input_tokens").is_some() {
789 usage.input_tokens = next.input_tokens;
790 }
791 if value.get("output_tokens").is_some() {
792 usage.output_tokens = next.output_tokens;
793 }
794 if value.get("reasoning_tokens").is_some() {
795 usage.reasoning_tokens = next.reasoning_tokens;
796 }
797 if value.get("cache_read_input_tokens").is_some() {
798 usage.cache_read_tokens = next.cache_read_tokens;
799 }
800 if value.get("cache_creation_input_tokens").is_some() {
801 usage.cache_write_tokens = next.cache_write_tokens;
802 }
803}
804
805fn anthropic_usage(value: &Value) -> ModelUsage {
806 ModelUsage {
807 input_tokens: value
808 .get("input_tokens")
809 .and_then(Value::as_u64)
810 .unwrap_or_default(),
811 output_tokens: value
812 .get("output_tokens")
813 .and_then(Value::as_u64)
814 .unwrap_or_default(),
815 reasoning_tokens: value
816 .get("reasoning_tokens")
817 .and_then(Value::as_u64)
818 .unwrap_or_default(),
819 cache_read_tokens: value
820 .get("cache_read_input_tokens")
821 .and_then(Value::as_u64)
822 .unwrap_or_default(),
823 cache_write_tokens: value
824 .get("cache_creation_input_tokens")
825 .and_then(Value::as_u64)
826 .unwrap_or_default(),
827 }
828}
829
830#[cfg(test)]
831mod tests {
832 use super::*;
833 use crate::provider::chat_usage;
834
835 #[test]
836 fn cached_usage_round_trips_through_both_provider_conventions() {
837 for wire in [
838 json!({
839 "prompt_tokens": 22,
840 "completion_tokens": 9,
841 "prompt_tokens_details": { "cached_tokens": 3 }
842 }),
843 json!({
844 "input_tokens": 19,
845 "output_tokens": 9,
846 "cache_read_input_tokens": 3
847 }),
848 ] {
849 let usage = chat_usage(&wire);
850 assert_eq!(chat_usage(&openai_usage(usage)), usage);
851 }
852 }
853
854 #[test]
855 fn cache_writes_fold_into_openai_prompt_total() {
856 let usage = ModelUsage {
857 input_tokens: 19,
858 output_tokens: 9,
859 cache_read_tokens: 3,
860 cache_write_tokens: 4,
861 ..ModelUsage::default()
862 };
863 let wire = openai_usage(usage);
864 assert_eq!(wire["prompt_tokens"], 26);
865 assert_eq!(wire["completion_tokens"], 9);
866 assert_eq!(wire["total_tokens"], 35);
867 assert_eq!(
868 wire["total_tokens"],
869 wire["prompt_tokens"].as_u64().unwrap() + wire["completion_tokens"].as_u64().unwrap()
870 );
871 }
872
873 #[test]
874 fn translates_openai_messages_tools_and_usage() {
875 let adapter = AnthropicAdapter::new();
876 let request = adapter
877 .encode_request(
878 Surface::ChatCompletions,
879 ProviderRequest {
880 model: "claude-sonnet".into(),
881 body: json!({
882 "messages": [
883 { "role": "system", "content": "safe" },
884 { "role": "user", "content": "hello" }
885 ],
886 "tools": [{ "type": "function", "function": { "name": "lookup", "parameters": { "type": "object" } } }]
887 }),
888 },
889 )
890 .unwrap();
891 assert_eq!(request["system"], "safe");
892 assert_eq!(request["messages"][0]["content"][0]["text"], "hello");
893 assert_eq!(request["tools"][0]["name"], "lookup");
894
895 let response = adapter
896 .decode_response(
897 Surface::ChatCompletions,
898 json!({
899 "id": "msg_1",
900 "model": "claude-sonnet",
901 "content": [{ "type": "text", "text": "answer" }],
902 "stop_reason": "end_turn",
903 "usage": { "input_tokens": 10, "output_tokens": 4 }
904 }),
905 )
906 .unwrap();
907 assert_eq!(response.body["choices"][0]["message"]["content"], "answer");
908 assert_eq!(response.usage.total_tokens(), 14);
909 }
910
911 #[test]
912 fn translates_reasoning_tool_choice_and_stop_sequences() {
913 let request = build_request(
914 "claude",
915 &json!({
916 "messages": [{ "role": "user", "content": "hello" }],
917 "max_tokens": 8000,
918 "reasoning_effort": "medium",
919 "temperature": 0.2,
920 "top_p": 0.9,
921 "stop": ["END", 42],
922 "tools": [{
923 "type": "function",
924 "function": { "name": "lookup", "parameters": { "type": "object" } }
925 }],
926 "tool_choice": {
927 "type": "function",
928 "function": { "name": "lookup" }
929 }
930 }),
931 );
932 assert_eq!(request["thinking"]["budget_tokens"], 4000);
933 assert!(request.get("temperature").is_none());
934 assert!(request.get("top_p").is_none());
935 assert_eq!(
936 request["tool_choice"],
937 json!({ "type": "tool", "name": "lookup" })
938 );
939 assert_eq!(request["stop_sequences"], json!(["END"]));
940 assert_eq!(
941 thinking_budget(&json!({ "reasoning_effort": "minimal" }), 4096),
942 Some(1024)
943 );
944 assert_eq!(
945 thinking_budget(&json!({ "reasoning_effort": "high" }), 4096),
946 Some(3072)
947 );
948 assert_eq!(
949 thinking_budget(&json!({ "reasoning_effort": "medium" }), 1500),
950 None
951 );
952 }
953
954 #[test]
955 fn signed_thinking_round_trips_through_encrypted_details() {
956 let adapter = AnthropicAdapter::new();
957 let response = adapter
958 .decode_response(
959 Surface::ChatCompletions,
960 json!({
961 "content": [
962 { "type": "thinking", "thinking": "check", "signature": "sig" },
963 { "type": "tool_use", "id": "toolu_1", "name": "lookup", "input": {} }
964 ],
965 "stop_reason": "tool_use"
966 }),
967 )
968 .unwrap();
969 let message = &response.body["choices"][0]["message"];
970 assert_eq!(
971 signed_thinking_from_details(message),
972 vec![SignedThinking {
973 thinking: "check".into(),
974 signature: "sig".into()
975 }]
976 );
977 let request = build_request(
978 "claude",
979 &json!({
980 "reasoning_effort": "low",
981 "max_tokens": 8000,
982 "messages": [message]
983 }),
984 );
985 assert_eq!(
986 request["messages"][0]["content"][0],
987 json!({ "type": "thinking", "thinking": "check", "signature": "sig" })
988 );
989 assert_eq!(request["messages"][0]["content"][1]["type"], "tool_use");
990 }
991
992 fn sse(data: Value) -> SseEvent {
993 SseEvent {
994 event: None,
995 data: data.to_string(),
996 }
997 }
998
999 #[test]
1000 fn stream_preserves_fragmented_tools_signed_reasoning_and_terminal_usage() {
1001 let adapter = AnthropicAdapter::new();
1002 let mut decoder = adapter.stream_decoder(Surface::ChatCompletions).unwrap();
1003 let upstream = [
1004 json!({ "type": "message_start", "message": { "usage": {
1005 "input_tokens": 12,
1006 "output_tokens": 0,
1007 "cache_read_input_tokens": 3,
1008 "cache_creation_input_tokens": 2
1009 }}}),
1010 json!({ "type": "content_block_start", "index": 0, "content_block": { "type": "thinking" }}),
1011 json!({ "type": "content_block_delta", "index": 0, "delta": { "type": "thinking_delta", "thinking": "check " }}),
1012 json!({ "type": "content_block_delta", "index": 0, "delta": { "type": "thinking_delta", "thinking": "weather" }}),
1013 json!({ "type": "content_block_delta", "index": 0, "delta": { "type": "signature_delta", "signature": "sig-" }}),
1014 json!({ "type": "content_block_delta", "index": 0, "delta": { "type": "signature_delta", "signature": "1" }}),
1015 json!({ "type": "content_block_stop", "index": 0 }),
1016 json!({ "type": "content_block_start", "index": 2, "content_block": {
1017 "type": "tool_use", "id": "toolu_1", "name": "lookup"
1018 }}),
1019 json!({ "type": "content_block_delta", "index": 2, "delta": {
1020 "type": "input_json_delta", "partial_json": "{\"city\":"
1021 }}),
1022 json!({ "type": "content_block_delta", "index": 2, "delta": {
1023 "type": "input_json_delta", "partial_json": "\"Paris\"}"
1024 }}),
1025 json!({ "type": "content_block_stop", "index": 2 }),
1026 json!({ "type": "message_delta", "delta": { "stop_reason": "tool_use" }, "usage": {
1027 "output_tokens": 5, "reasoning_tokens": 2
1028 }}),
1029 json!({ "type": "message_stop" }),
1030 ];
1031 let mut events = Vec::new();
1032 for event in upstream {
1033 events.extend(decoder.decode(sse(event)).unwrap());
1034 }
1035
1036 let mut assembler = crate::ToolCallAssembler::new();
1037 for event in &events {
1038 assembler.push_event(event).unwrap();
1039 }
1040 let calls = assembler.finish().unwrap();
1041 assert_eq!(calls.len(), 1);
1042 assert_eq!(calls[0].id, "toolu_1");
1043 assert_eq!(calls[0].name, "lookup");
1044 assert_eq!(
1045 calls[0].arguments_json().unwrap(),
1046 json!({ "city": "Paris" })
1047 );
1048
1049 let details = events.iter().find_map(|event| match event {
1050 ProviderStreamEvent::Data { data, .. }
1051 if data.pointer("/choices/0/delta/reasoning_details/0/type")
1052 == Some(&json!("reasoning.encrypted")) =>
1053 {
1054 Some(&data["choices"][0]["delta"])
1055 }
1056 _ => None,
1057 });
1058 let details = details.expect("encrypted reasoning detail on tool start");
1059 assert_eq!(
1060 signed_thinking_from_details(details),
1061 vec![SignedThinking {
1062 thinking: "check weather".into(),
1063 signature: "sig-1".into()
1064 }]
1065 );
1066
1067 let terminal = events.iter().find_map(|event| match event {
1068 ProviderStreamEvent::Data { data, .. }
1069 if data.pointer("/choices/0/finish_reason") == Some(&json!("tool_calls")) =>
1070 {
1071 Some(data)
1072 }
1073 _ => None,
1074 });
1075 let terminal = terminal.expect("terminal chunk");
1076 assert_eq!(terminal["usage"]["prompt_tokens"], 17);
1077 assert_eq!(terminal["usage"]["completion_tokens"], 5);
1078 assert_eq!(terminal["usage"]["total_tokens"], 22);
1079 assert_eq!(
1080 terminal["usage"]["completion_tokens_details"]["reasoning_tokens"],
1081 2
1082 );
1083 assert_eq!(
1084 terminal["usage"]["prompt_tokens_details"]["cache_write_tokens"],
1085 2
1086 );
1087 assert!(matches!(
1088 events.last(),
1089 Some(ProviderStreamEvent::Done(ModelUsage {
1090 input_tokens: 12,
1091 output_tokens: 5,
1092 reasoning_tokens: 2,
1093 cache_read_tokens: 3,
1094 cache_write_tokens: 2,
1095 }))
1096 ));
1097 }
1098
1099 #[test]
1100 fn stream_finish_closes_an_upstream_stream_without_message_stop() {
1101 let mut decoder = AnthropicAdapter::new()
1102 .stream_decoder(Surface::ChatCompletions)
1103 .unwrap();
1104 decoder
1105 .decode(sse(json!({
1106 "type": "content_block_delta",
1107 "index": 0,
1108 "delta": { "type": "text_delta", "text": "answer" }
1109 })))
1110 .unwrap();
1111 let terminal = decoder.finish().unwrap();
1112 assert_eq!(terminal.len(), 2);
1113 assert!(matches!(terminal[0], ProviderStreamEvent::Data { .. }));
1114 assert!(matches!(terminal[1], ProviderStreamEvent::Done(_)));
1115 assert!(decoder.finish().unwrap().is_empty());
1116 }
1117
1118 #[test]
1119 fn native_response_usage_maps_cache_counters() {
1120 assert_eq!(
1121 native_message_usage(&json!({
1122 "id": "msg_1",
1123 "content": [{ "type": "text", "text": "answer" }],
1124 "usage": {
1125 "input_tokens": 11,
1126 "output_tokens": 4,
1127 "cache_creation_input_tokens": 7,
1128 "cache_read_input_tokens": 5
1129 }
1130 })),
1131 ModelUsage {
1132 input_tokens: 11,
1133 output_tokens: 4,
1134 reasoning_tokens: 0,
1135 cache_read_tokens: 5,
1136 cache_write_tokens: 7,
1137 }
1138 );
1139 assert_eq!(native_message_usage(&json!({})), ModelUsage::default());
1140 }
1141
1142 #[test]
1143 fn native_stream_forwards_events_verbatim_and_folds_split_usage() {
1144 let mut decoder = NativeMessagesDecoder::new();
1145 let thinking = json!({
1146 "type": "content_block_start",
1147 "index": 0,
1148 "content_block": { "type": "thinking", "thinking": "why", "signature": "sig-1" }
1149 });
1150 let upstream = vec![
1151 json!({ "type": "message_start", "message": { "usage": {
1152 "input_tokens": 12,
1153 "cache_read_input_tokens": 3,
1154 "cache_creation_input_tokens": 2
1155 }}}),
1156 thinking.clone(),
1157 json!({ "type": "content_block_stop", "index": 0 }),
1158 json!({ "type": "message_delta", "delta": { "stop_reason": "end_turn" }, "usage": {
1159 "output_tokens": 9
1160 }}),
1161 json!({ "type": "message_stop" }),
1162 ];
1163 let mut events = Vec::new();
1164 for event in &upstream {
1165 events.extend(decoder.decode(sse(event.clone())).unwrap());
1166 }
1167
1168 let forwarded: Vec<&Value> = events
1169 .iter()
1170 .filter_map(|event| match event {
1171 ProviderStreamEvent::Data { data, .. } => Some(data),
1172 ProviderStreamEvent::Done(_) => None,
1173 })
1174 .collect();
1175 assert_eq!(forwarded, upstream.iter().collect::<Vec<_>>());
1176 assert_eq!(forwarded[1]["content_block"], thinking["content_block"]);
1179 assert_eq!(
1180 events.last(),
1181 Some(&ProviderStreamEvent::Done(ModelUsage {
1182 input_tokens: 12,
1183 output_tokens: 9,
1184 reasoning_tokens: 0,
1185 cache_read_tokens: 3,
1186 cache_write_tokens: 2,
1187 }))
1188 );
1189 assert!(decoder.finish().unwrap().is_empty());
1190 }
1191
1192 #[test]
1193 fn native_stream_reports_an_error_event_and_closes_a_truncated_stream() {
1194 let mut decoder = NativeMessagesDecoder::new();
1195 let error = decoder
1196 .decode(sse(
1197 json!({ "type": "error", "error": { "message": "overloaded" } }),
1198 ))
1199 .unwrap_err();
1200 assert!(matches!(error, ProviderError::InvalidStream(message) if message == "overloaded"));
1201
1202 let mut truncated = NativeMessagesDecoder::new();
1203 truncated
1204 .decode(sse(
1205 json!({ "type": "message_start", "message": { "usage": { "input_tokens": 4 } } }),
1206 ))
1207 .unwrap();
1208 assert_eq!(
1209 truncated.finish().unwrap(),
1210 vec![ProviderStreamEvent::Done(ModelUsage {
1211 input_tokens: 4,
1212 ..ModelUsage::default()
1213 })]
1214 );
1215 }
1216}