1use std::collections::{BTreeMap, BTreeSet};
16
17use serde_json::{Map, Value, json};
18
19use crate::completion::FinishReason;
20use crate::error::ProviderError;
21use crate::json_utils::Lenient;
22use crate::message::{CallId, DocumentRange, Source, SourceLocation, ToolName};
23use crate::observe::ObservedError;
24use crate::operation::{Block, CallFragment, Completion, Finish};
25use crate::providers::internal::set_fields;
26use crate::providers::internal::wire;
27use crate::wire::{
28 AdapterVerdict, Decoder, Flow, ObservationSink, Out, WireCitation, WireEvent, WireFrame,
29};
30
31const KNOWN_EVENT_TYPES: &[&str] = &[
34 "message",
35 "message_start",
36 "content_block_start",
37 "content_block_delta",
38 "content_block_stop",
39 "message_delta",
40 "message_stop",
41 "ping",
42 "error",
43];
44
45#[derive(Debug, Clone, PartialEq)]
49pub struct MessagesEvent {
50 pub fields: Value,
52 raw: Option<String>,
55}
56
57impl MessagesEvent {
58 fn kind(&self) -> &str {
59 self.fields.str("type").unwrap_or_default()
60 }
61
62 fn index(&self) -> Result<usize, ProviderError> {
64 self.fields
65 .u64("index")
66 .and_then(|index| usize::try_from(index).ok())
67 .ok_or_else(|| {
68 ProviderError::Response(format!("Anthropic `{}` names no block index", self.kind()))
69 })
70 }
71
72 fn item(&self, key: &str) -> Result<&Map<String, Value>, ProviderError> {
74 match self.fields.get(key) {
75 Some(Value::Object(item)) if item.get("type").is_some_and(Value::is_string) => Ok(item),
76 _ => Err(ProviderError::Response(format!(
77 "Anthropic `{}` carries no `{key}` with a string `type`",
78 self.kind()
79 ))),
80 }
81 }
82}
83
84#[derive(Debug, Clone, Default, PartialEq)]
86struct Counts {
87 input: Option<u64>,
88 output: Option<u64>,
89 cache_read: Option<u64>,
90 cache_creation: Option<u64>,
91 cache_creation_split: Option<Value>,
94 thinking: Option<u64>,
95}
96
97impl Counts {
98 fn of(usage: Option<&Value>) -> Self {
99 let count = |pointer: &str| usage?.at(pointer)?.as_u64_lenient();
100 Self {
101 input: count("/input_tokens"),
102 output: count("/output_tokens"),
103 cache_read: count("/cache_read_input_tokens"),
104 cache_creation: count("/cache_creation_input_tokens"),
105 cache_creation_split: usage
106 .and_then(|usage| usage.get("cache_creation"))
107 .filter(|split| !split.is_null())
108 .cloned(),
109 thinking: count("/output_tokens_details/thinking_tokens"),
110 }
111 }
112
113 fn usage(&self) -> crate::completion::Usage {
117 let input = self.input.map(|uncached| {
118 uncached + self.cache_read.unwrap_or(0) + self.cache_creation.unwrap_or(0)
119 });
120 crate::completion::Usage {
121 input_tokens: input,
122 output_tokens: self.output,
123 cached_input_tokens: self.cache_read,
124 cache_creation_input_tokens: self.cache_creation,
125 reasoning_tokens: self.thinking,
126 total_tokens: input.zip(self.output).map(|(input, output)| input + output),
127 tool_use_prompt_tokens: None,
128 cost: None,
129 }
130 }
131
132 fn write_cost(&self, pricing: &crate::catalog::Pricing) -> Option<f64> {
138 let written = self.cache_creation.unwrap_or(0);
139 if written == 0 {
140 return Some(0.0);
141 }
142 let split = self.cache_creation_split.as_ref()?;
143 let short = split.get("ephemeral_5m_input_tokens")?.as_u64()?;
144 let long = split.get("ephemeral_1h_input_tokens")?.as_u64()?;
145 if short.checked_add(long)? != written {
146 return None;
147 }
148 let short_cost = if short == 0 {
149 0.0
150 } else {
151 short as f64 * pricing.cache_write?
152 };
153 Some((short_cost + long as f64 * pricing.input * 2.0) / 1_000_000.0)
154 }
155}
156
157fn finish_of(reason: &str, details: Option<&Value>) -> (Option<FinishReason>, Option<String>) {
161 let reason = match reason {
162 "end_turn" | "stop_sequence" | "pause_turn" => FinishReason::Stop,
165 "max_tokens" | "model_context_window_exceeded" => FinishReason::Length,
166 "tool_use" => FinishReason::ToolCalls,
167 "refusal" => {
169 let explanation = details
170 .and_then(|details| details.str("explanation"))
171 .filter(|explanation| !explanation.is_empty())
172 .unwrap_or("The model refused to complete the request");
173 return (
174 Some(FinishReason::ContentFilter),
175 Some(explanation.to_owned()),
176 );
177 }
178 other => FinishReason::Other(other.to_owned()),
179 };
180 (Some(reason), None)
181}
182
183fn citation_of(citation: &Value) -> Option<WireCitation> {
189 let number = |key: &str| citation.u64(key).and_then(|n| u32::try_from(n).ok());
190 let range = |start: &str, end: &str| Some(number(start)?..number(end)?);
191 let location = match citation.str("type")? {
192 "char_location" => SourceLocation::Document {
193 index: number("document_index"),
194 id: citation.str("file_id").map(str::to_owned),
195 within: citation
196 .u64("start_char_index")
197 .zip(citation.u64("end_char_index"))
198 .map(|(start, end)| DocumentRange::Chars(start..end)),
199 },
200 "page_location" => SourceLocation::Document {
201 index: number("document_index"),
202 id: citation.str("file_id").map(str::to_owned),
203 within: range("start_page_number", "end_page_number").map(DocumentRange::Pages),
204 },
205 "content_block_location" => SourceLocation::Document {
206 index: number("document_index"),
207 id: citation.str("file_id").map(str::to_owned),
208 within: range("start_block_index", "end_block_index").map(DocumentRange::Blocks),
209 },
210 "search_result_location" => SourceLocation::SearchResult {
211 index: number("search_result_index")?,
212 source: citation.str("source")?.to_owned(),
213 blocks: range("start_block_index", "end_block_index"),
214 },
215 "web_search_result_location" => SourceLocation::Url {
216 url: citation.str("url")?.to_owned(),
217 },
218 _ => return None,
219 };
220 let mut source = Source::new(location);
221 if let Some(title) = citation
222 .str("document_title")
223 .or_else(|| citation.str("title"))
224 {
225 source = source.title(title);
226 }
227 if let Some(cited) = citation.str("cited_text") {
228 source = source.cited_text(cited);
229 }
230 Some(WireCitation::new(None, vec![source]))
231}
232
233#[derive(Debug, Clone, Copy, PartialEq, Eq)]
235enum Kind {
236 Text,
237 Thinking,
238 Redacted,
239 Call,
240 Opaque,
241}
242
243fn replayed_start(
246 previous: &[Map<String, Value>],
247 block: &Map<String, Value>,
248 reconnecting: bool,
249) -> bool {
250 reconnecting && previous.contains(block)
251}
252
253#[derive(Debug)]
258pub struct MessagesDecoder {
259 open: BTreeMap<usize, (Kind, String)>,
261 started: BTreeMap<usize, Vec<Map<String, Value>>>,
263 reconnecting: bool,
264 replayed: BTreeSet<usize>,
266 message_started: bool,
268 opened: bool,
270 unsigned_thinking: bool,
272 cache_ttl_pricing: bool,
275 start: Counts,
278 message_id: Option<String>,
279 response_model: Option<String>,
280 container: Option<Value>,
281}
282
283impl Default for MessagesDecoder {
284 fn default() -> Self {
285 Self::new(super::wire::Quirks::anthropic())
286 }
287}
288
289impl MessagesDecoder {
290 pub fn new(quirks: super::wire::Quirks) -> Self {
292 Self {
293 open: BTreeMap::new(),
294 started: BTreeMap::new(),
295 reconnecting: false,
296 replayed: BTreeSet::new(),
297 message_started: false,
298 opened: false,
299 unsigned_thinking: quirks.unsigned_thinking,
300 cache_ttl_pricing: quirks.cache_ttl_pricing,
301 start: Counts::default(),
302 message_id: None,
303 response_model: None,
304 container: None,
305 }
306 }
307
308 fn start(
310 &mut self,
311 index: usize,
312 block: Map<String, Value>,
313 out: &mut Out<'_, Completion>,
314 ) -> Result<(), ProviderError> {
315 if self.open.contains_key(&index) {
316 return Ok(());
317 }
318 if self
319 .started
320 .get(&index)
321 .is_some_and(|previous| replayed_start(previous, &block, self.reconnecting))
322 {
323 self.replayed.insert(index);
324 return Ok(());
325 }
326 self.replayed.remove(&index);
327 self.reconnecting = false;
328 self.started.entry(index).or_default().push(block.clone());
329 let block = Value::Object(block);
330 let kind = block.str("type").unwrap_or_default();
331 if kind == "fallback" {
334 if self.opened {
335 return Err(ProviderError::Response(
336 "Anthropic performed an unsupported mid-output model fallback".to_owned(),
337 ));
338 }
339 self.open.insert(index, (Kind::Opaque, String::new()));
340 return out.open(index, Block::Opaque { replay: false }, block);
341 }
342 self.opened = true;
343 let (opened, kind, text) = match kind {
344 "text" => (Block::Text, Kind::Text, block.str("text")),
345 "thinking" => (
346 Block::Reasoning { redacted: false },
347 Kind::Thinking,
348 block.str("thinking"),
349 ),
350 "redacted_thinking" => (Block::Reasoning { redacted: true }, Kind::Redacted, None),
351 "tool_use" => {
352 self.open.insert(index, (Kind::Call, String::new()));
353 let id = block.str("id").unwrap_or_default().to_owned();
354 let input = block.get("input").cloned().unwrap_or_default();
355 match ToolName::new(block.str("name").unwrap_or_default()) {
356 Ok(name) => {
357 let id = CallId::from_wire(&id);
358 out.open(index, Block::Call { id, name }, block)?;
359 }
360 Err(_) => out.fragment(
362 Some(index),
363 CallFragment {
364 id: Some(&id),
365 ..CallFragment::default()
366 },
367 )?,
368 }
369 return out.announce(index, input);
372 }
373 _ => (Block::Opaque { replay: true }, Kind::Opaque, None),
374 };
375 let text = text.unwrap_or_default().to_owned();
376 let citations: Vec<WireCitation> = match kind {
377 Kind::Text => block
378 .arr("citations")
379 .iter()
380 .filter_map(citation_of)
381 .collect(),
382 _ => Vec::new(),
383 };
384 self.open.insert(index, (kind, String::new()));
385 out.open(index, opened, block)?;
386 for citation in citations {
387 out.cite(index, citation);
388 }
389 out.push(index, &text)
390 }
391
392 fn delta(
397 &mut self,
398 index: usize,
399 delta: &Map<String, Value>,
400 out: &mut Out<'_, Completion>,
401 ) -> Result<(), ProviderError> {
402 if self.replayed.contains(&index) {
403 return Ok(());
404 }
405 let kind = delta
406 .get("type")
407 .and_then(Value::as_str)
408 .unwrap_or_default();
409 let fragment = |key: &str| {
411 delta.get(key).and_then(Value::as_str).ok_or_else(|| {
412 ProviderError::Response(format!("Anthropic `{kind}` carries no string `{key}`"))
413 })
414 };
415 if !self.started.contains_key(&index) {
418 let opened = match kind {
419 "text_delta" => Some(json!({"type": "text", "text": ""})),
420 "thinking_delta" => Some(json!({"type": "thinking", "thinking": ""})),
421 _ => None,
422 };
423 if let Some(Value::Object(block)) = opened {
424 self.start(index, block, out)?;
425 }
426 }
427 match kind {
428 "text_delta" => out.push(index, fragment("text")?)?,
429 "thinking_delta" => out.push(index, fragment("thinking")?)?,
430 "input_json_delta" => {
431 let fragment = fragment("partial_json")?;
432 let Some((kind @ (Kind::Call | Kind::Opaque), json)) = self.open.get_mut(&index)
433 else {
434 return Err(ProviderError::Response(format!(
435 "Anthropic streamed input to content block {index}, which takes none"
436 )));
437 };
438 json.push_str(fragment);
439 if *kind == Kind::Call {
440 out.push(index, fragment)?;
441 }
442 return Ok(());
443 }
444 "citations_delta" => {
445 let citation = delta.get("citation").cloned().unwrap_or_default();
446 let cited = citation_of(&citation);
447 out.edit(index, |item| {
448 if let Some(item) = item.as_object_mut() {
449 match item.get_mut("citations") {
450 Some(Value::Array(citations)) => citations.push(citation),
451 _ => {
452 item.insert("citations".to_owned(), Value::Array(vec![citation]));
453 }
454 }
455 }
456 })?;
457 if let Some(cited) = cited {
458 out.cite(index, cited);
459 }
460 return Ok(());
461 }
462 _ => {}
463 }
464 out.edit(index, |item| {
467 crate::operation::completion::merge(item, delta)
468 })
469 }
470
471 fn stop(&mut self, index: usize, out: &mut Out<'_, Completion>) -> Result<(), ProviderError> {
478 if self.replayed.remove(&index) {
479 return Ok(());
480 }
481 let Some((kind, json)) = self.open.remove(&index) else {
482 return out.finish(index);
483 };
484 let streamed = (!json.is_empty()).then(|| crate::json_utils::parse_tool_arguments(&json));
485 let unsigned = self.unsigned_thinking;
486 let mut complete = true;
487 out.edit(index, |item| {
488 let input = match streamed {
489 Some(Ok(parsed)) => Ok(Some(parsed)),
490 Some(Err(_)) => Err(()),
491 None => Ok(item.get("input").cloned()),
492 };
493 let kept = match (kind, input) {
494 (Kind::Text, _) => !item.str("text").unwrap_or_default().trim().is_empty(),
495 (Kind::Thinking, _) => {
496 unsigned || item.str("signature").is_some_and(|s| !s.is_empty())
497 }
498 (Kind::Redacted, _) => item.str("data").is_some_and(|data| !data.is_empty()),
499 (Kind::Opaque, Ok(None)) => true,
500 (Kind::Call | Kind::Opaque, Ok(Some(input @ Value::Object(_)))) => {
501 crate::operation::completion::merge(
502 item,
503 &set_fields([("input", Some(input))]),
504 );
505 true
506 }
507 (Kind::Call, Ok(None | Some(Value::Null))) => {
508 crate::operation::completion::merge(
509 item,
510 &set_fields([("input", Some(json!({})))]),
511 );
512 true
513 }
514 (Kind::Call, _) => false,
515 (Kind::Opaque, _) => {
516 complete = false;
517 true
518 }
519 };
520 if !kept {
521 *item = Value::Null;
522 }
523 })?;
524 if complete {
525 out.finish(index)
526 } else {
527 out.close(index)
528 }
529 }
530
531 fn metadata(&mut self, message: &Value) {
533 self.start = Counts::of(message.get("usage"));
534 self.message_id = message.str("id").map(str::to_owned);
535 self.response_model = message.str("model").map(str::to_owned);
536 self.note_container(message.get("container"));
537 }
538
539 fn note_container(&mut self, container: Option<&Value>) {
540 self.container = container
541 .filter(|c| !c.is_null())
542 .or(self.container.as_ref())
543 .cloned();
544 }
545
546 fn end(
551 &mut self,
552 usage: &Counts,
553 stop_reason: Option<&str>,
554 details: Option<&Value>,
555 mut out: Out<'_, Completion>,
556 ) -> Result<Flow, ProviderError> {
557 let open: Vec<usize> = self
558 .open
559 .iter()
560 .filter(|(_, (kind, json))| {
561 *kind != Kind::Call
562 || json.is_empty()
563 || crate::json_utils::parse_tool_arguments(json).is_ok_and(|v| v.is_object())
564 })
565 .map(|(index, _)| *index)
566 .collect();
567 for index in open {
568 self.stop(index, &mut out)?;
569 }
570 if let Some(container) = &self.container {
571 let index = out.fresh_index();
572 let item = json!({ "type": "container", "container": container });
573 out.whole(index, Block::Opaque { replay: true }, item, "")?;
574 }
575 let (reason, error) = stop_reason.map_or((None, None), |reason| finish_of(reason, details));
576 let mut counted = usage.usage();
577 if self.cache_ttl_pricing {
578 counted.cost = out
579 .catalog_pricing(self.response_model.as_deref())
580 .and_then(|pricing| {
581 let write = usage.write_cost(&pricing);
582 pricing.cost_with_cache_write(&counted, write)
583 });
584 }
585 Ok(out.end(Finish {
586 usage: counted,
587 reason,
588 response_id: self.message_id.clone(),
589 model: self.response_model.clone(),
590 error,
591 }))
592 }
593
594 fn whole(
598 &mut self,
599 mut message: Value,
600 mut out: Out<'_, Completion>,
601 ) -> Result<Flow, ProviderError> {
602 self.metadata(&message);
603 let content = match message.get_mut("content").map(Value::take) {
604 Some(Value::Array(content)) => content,
605 _ => Vec::new(),
606 };
607 for (index, block) in content.into_iter().enumerate() {
608 let block = match block {
609 Value::Object(block) if block.get("type").is_some_and(Value::is_string) => block,
610 _ => {
611 return Err(ProviderError::Response(format!(
612 "Anthropic content block {index} has no string `type`"
613 )));
614 }
615 };
616 self.start(index, block, &mut out)?;
617 self.stop(index, &mut out)?;
618 }
619 let usage = self.start.clone();
620 self.end(
621 &usage,
622 message.str("stop_reason"),
623 message.get("stop_details"),
624 out,
625 )
626 }
627
628 fn terminal(&self, usage: Option<&Value>) -> Counts {
631 let (terminal, start) = (Counts::of(usage), self.start.clone());
632 Counts {
633 input: terminal.input.filter(|tokens| *tokens > 0).or(start.input),
636 cache_read: terminal.cache_read.or(start.cache_read),
637 cache_creation: terminal.cache_creation.or(start.cache_creation),
638 cache_creation_split: terminal.cache_creation_split.or(start.cache_creation_split),
639 ..terminal
640 }
641 }
642}
643
644impl<'id> Decoder<'id, Completion> for MessagesDecoder {
645 type Event = MessagesEvent;
646
647 fn classify(&self, frame: WireFrame) -> WireEvent<MessagesEvent> {
648 let data = frame.as_str();
649 wire::classify_tagged_frame::<Value>(&data, "type", |tag| KNOWN_EVENT_TYPES.contains(&tag))
650 .map(|fields| {
651 let raw = (fields.str("type") == Some("error")).then(|| data.to_string());
655 MessagesEvent { fields, raw }
656 })
657 }
658
659 fn decode(
660 &mut self,
661 event: MessagesEvent,
662 mut out: Out<'id, Completion>,
663 ) -> Result<Flow, ProviderError> {
664 match event.kind() {
665 "message" => return self.whole(event.fields, out),
666 "message_start" => {
669 if self.message_started && event.fields.get("message").is_some_and(Value::is_object)
670 {
671 self.reconnecting = true;
672 }
673 if !self.message_started
674 && let Some(message) = event.fields.get("message").filter(|m| m.is_object())
675 {
676 self.message_started = true;
677 self.metadata(message);
678 }
679 }
680 "content_block_start" => {
681 let block = event.item("content_block")?.clone();
682 self.start(event.index()?, block, &mut out)?;
683 }
684 "content_block_delta" => {
685 let delta = event.item("delta")?;
686 self.delta(event.index()?, delta, &mut out)?;
687 }
688 "content_block_stop" => self.stop(event.index()?, &mut out)?,
689 "message_delta" => {
690 let delta = event.fields.get("delta");
691 self.note_container(delta.and_then(|delta| delta.get("container")));
692 let Some(reason) = delta.and_then(|delta| delta.str("stop_reason")) else {
696 if let Some(usage) = event.fields.get("usage") {
697 self.start = self.terminal(Some(usage));
698 }
699 return Ok(Flow::More);
700 };
701 let usage = self.terminal(event.fields.get("usage"));
702 let details = delta.and_then(|delta| delta.get("stop_details"));
703 return self.end(&usage, Some(reason), details, out);
704 }
705 "error" => {
708 return Err(ProviderError::from_provider_body(
709 event.raw.unwrap_or_default(),
710 ));
711 }
712 _ => {}
715 }
716 Ok(Flow::More)
717 }
718}
719
720impl MessagesDecoder {
721 pub(crate) fn project(payload: &[u8], sink: &mut ObservationSink<'_>) {
726 let Ok(payload) = serde_json::from_slice::<Value>(payload) else {
727 return;
728 };
729 let fields = payload
730 .get("message")
731 .filter(|m| m.is_object())
732 .unwrap_or(&payload);
733 let stop_reason = fields
734 .str("stop_reason")
735 .or_else(|| payload.at("/delta/stop_reason").and_then(Value::as_str));
736 let verdict = AdapterVerdict {
737 finish_reason: stop_reason.map(|reason| sink.scrub(reason)),
738 block_reason: None,
739 detail: None,
740 model: fields.str("model").map(|model| sink.scrub(model)),
741 };
742 let response_id = fields.str("id").map(|id| sink.scrub(id));
743 sink.provider(verdict, response_id);
744 if let Some(error) = ObservedError::of(&payload) {
745 error.emit(sink);
746 }
747 }
748}
749
750pub(crate) mod document;
751
752pub use document::Message as MessagesDocument;
754
755#[cfg(test)]
756mod tests;