1use serde_json::{Map, Value, json};
7
8use crate::LlmResponseChunk;
9use crate::codecs::stream::{
10 StreamCodec, StreamTranslationState, record_source_identity,
11 target_message_id_or_source_message_id, target_model_or_source_model,
12};
13use crate::format::{FormatId, WireFormat};
14use crate::util::{desanitize_anthropic_tool_use_id, sanitize_anthropic_tool_use_id};
15
16pub struct AnthropicMessagesStreamCodec;
18
19impl StreamCodec for AnthropicMessagesStreamCodec {
20 fn format(&self) -> FormatId {
21 WireFormat::AnthropicMessages.into()
22 }
23
24 fn is_terminal_event(&self, event: &Value) -> bool {
25 event.get("type").and_then(Value::as_str) == Some("message_stop")
26 }
27
28 fn sse_done_is_terminal(&self) -> bool {
29 false
30 }
31
32 fn stamp_streamed_response_model(&self, event: &mut Value, served_model: &str) {
33 if let Some(message) = event.get_mut("message").and_then(Value::as_object_mut) {
34 message.insert("model".to_string(), Value::String(served_model.to_string()));
35 }
36 }
37
38 fn decode_event(
39 &self,
40 state: &mut StreamTranslationState,
41 event: &Value,
42 ) -> Vec<LlmResponseChunk> {
43 decode_anthropic_stream(state, event)
44 }
45
46 fn encode_event(
47 &self,
48 state: &mut StreamTranslationState,
49 event: LlmResponseChunk,
50 ) -> Vec<Value> {
51 encode_anthropic_stream(state, event)
52 }
53
54 fn observe_replayed_event(
55 &self,
56 state: &mut StreamTranslationState,
57 raw: &Value,
58 normalized: Vec<LlmResponseChunk>,
59 ) {
60 let normalized_stop = normalized
61 .iter()
62 .any(|chunk| matches!(chunk, LlmResponseChunk::MessageStop { .. }));
63 for chunk in normalized {
64 drop(encode_anthropic_stream(state, chunk));
65 }
66
67 match raw.get("type").and_then(Value::as_str) {
68 Some("message_delta") if normalized_stop => state.emitted_message_delta = true,
71 Some("message_stop") => state.finished = true,
72 _ => {}
73 }
74 }
75
76 fn finish(&self, state: &mut StreamTranslationState) -> Vec<Value> {
77 finish_anthropic_stream(state)
78 }
79}
80
81fn decode_anthropic_stream(
83 state: &mut StreamTranslationState,
84 event: &Value,
85) -> Vec<LlmResponseChunk> {
86 let Some(object) = event.as_object() else {
87 return vec![LlmResponseChunk::DecodeError {
88 message: "Anthropic stream event is not an object".to_string(),
89 }];
90 };
91 match object.get("type").and_then(Value::as_str) {
92 Some("message_start") => {
93 state.saw_message_start = true;
94 let message = object.get("message").and_then(Value::as_object);
95 if let Some(model) = message
96 .and_then(|message| message.get("model"))
97 .and_then(Value::as_str)
98 {
99 state.model = Some(model.to_string());
100 }
101 if let Some(id) = message
102 .and_then(|message| message.get("id"))
103 .and_then(Value::as_str)
104 {
105 state.message_id = Some(id.to_string());
106 }
107 if let Some(message) = message
108 && let Some(usage) = message.get("usage")
109 {
110 capture_anthropic_usage(state, usage);
111 }
112 vec![LlmResponseChunk::MessageStart {
113 id: state.message_id.clone(),
114 model: state.model.clone(),
115 }]
116 }
117 Some("content_block_start") => decode_anthropic_content_block_start(state, object),
118 Some("content_block_delta") => decode_anthropic_content_block_delta(state, object),
119 Some("content_block_stop") => {
120 let index = object.get("index").and_then(Value::as_u64).unwrap_or(0) as usize;
121 if state.empty_anthropic_tool_inputs.remove(&index) {
122 vec![LlmResponseChunk::ToolCallDelta {
123 index,
124 id: None,
125 name: None,
126 arguments_delta: Some("{}".to_string()),
127 }]
128 } else {
129 Vec::new()
130 }
131 }
132 Some("message_delta") => {
133 let mut out = Vec::new();
134 if let Some(usage) = object.get("usage") {
135 capture_anthropic_usage(state, usage);
136 out.push(LlmResponseChunk::Usage(state.usage.clone()));
137 }
138 if let Some(stop_reason) = object
139 .get("delta")
140 .and_then(Value::as_object)
141 .and_then(|delta| delta.get("stop_reason"))
142 .and_then(Value::as_str)
143 {
144 state.stop_reason = Some(stop_reason.to_string());
147 state.stop_details = object
148 .get("delta")
149 .and_then(Value::as_object)
150 .and_then(|delta| delta.get("stop_details"))
151 .filter(|details| !details.is_null())
152 .cloned();
153 out.push(LlmResponseChunk::MessageStop {
154 reason: Some(stop_reason.to_string()),
155 });
156 }
157 out
158 }
159 Some("message_stop") => vec![LlmResponseChunk::MessageStop {
163 reason: state.stop_reason.clone(),
164 }],
165 Some("error") => vec![LlmResponseChunk::StreamError {
166 message: object
167 .get("error")
168 .and_then(Value::as_object)
169 .and_then(|error| error.get("message"))
170 .and_then(Value::as_str)
171 .unwrap_or("unknown Anthropic stream error")
172 .to_string(),
173 }],
174 _ => Vec::new(),
175 }
176}
177
178fn encode_anthropic_stream(
180 state: &mut StreamTranslationState,
181 event: LlmResponseChunk,
182) -> Vec<Value> {
183 if state.errored {
185 return Vec::new();
186 }
187 match event {
188 LlmResponseChunk::MessageStart { id, model } => {
189 record_source_identity(state, id, model);
190 if state.emitted_message_start {
191 Vec::new()
192 } else {
193 state.emitted_message_start = true;
194 vec![json!({
195 "type": "message_start",
196 "message": {
197 "id": anthropic_message_id(state),
198 "type": "message",
199 "role": "assistant",
200 "model": target_model_or_source_model(state),
201 "content": [],
202 "stop_reason": Value::Null,
203 "stop_sequence": Value::Null,
204 "usage": {"input_tokens": 0, "output_tokens": 0},
205 },
206 })]
207 }
208 }
209 LlmResponseChunk::TextDelta { text, .. } => {
210 state.output_tokens_seen += 1;
211 let mut out = ensure_anthropic_text_block(state);
212 out.push(json!({
213 "type": "content_block_delta",
214 "index": state.text_block_index.unwrap_or(0),
215 "delta": {"type": "text_delta", "text": text},
216 }));
217 out
218 }
219 LlmResponseChunk::ReasoningDelta { text, .. } => {
220 let mut out = ensure_anthropic_reasoning_block(state);
221 out.push(json!({
222 "type": "content_block_delta",
223 "index": state.reasoning_block_index.unwrap_or(0),
224 "delta": {"type": "thinking_delta", "thinking": text},
225 }));
226 out
227 }
228 LlmResponseChunk::ReasoningDetailsDelta { text, .. } => {
229 if text.is_empty() {
230 return Vec::new();
231 }
232 let mut out = ensure_anthropic_reasoning_block(state);
233 out.push(json!({
234 "type": "content_block_delta",
235 "index": state.reasoning_block_index.unwrap_or(0),
236 "delta": {"type": "thinking_delta", "thinking": text},
237 }));
238 out
239 }
240 LlmResponseChunk::ToolCallDelta {
241 index,
242 id,
243 name,
244 arguments_delta,
245 } => encode_anthropic_tool_delta(state, index, id, name, arguments_delta),
246 LlmResponseChunk::Usage(usage) => {
247 state.usage = usage;
248 state.saw_backend_usage = true;
249 Vec::new()
250 }
251 LlmResponseChunk::MessageStop { reason } => {
252 state.stop_reason = reason.or_else(|| state.stop_reason.clone());
253 Vec::new()
254 }
255 LlmResponseChunk::StreamError { message } | LlmResponseChunk::DecodeError { message } => {
256 state.finished = true; state.errored = true; vec![json!({"type": "error", "error": {"type": "api_error", "message": message}})]
260 }
261 }
262}
263
264fn finish_anthropic_stream(state: &mut StreamTranslationState) -> Vec<Value> {
266 if state.finished {
267 return Vec::new();
268 }
269 if state.tool_states.values().any(|tool| {
270 tool.id.as_deref().is_none_or(str::is_empty)
271 || tool.name.as_deref().is_none_or(str::is_empty)
272 }) {
273 return encode_anthropic_stream(
274 state,
275 LlmResponseChunk::StreamError {
276 message: "Tool call ended without a non-empty ID and name".to_string(),
277 },
278 );
279 }
280 let mut out = Vec::new();
281 if !state.emitted_message_start {
282 out.extend(encode_anthropic_stream(
283 state,
284 LlmResponseChunk::MessageStart {
285 id: state.message_id.clone(),
286 model: state.model.clone(),
287 },
288 ));
289 }
290
291 if state.text_block_started {
292 if let Some(index) = state.text_block_index {
293 out.push(json!({"type": "content_block_stop", "index": index}));
294 }
295 state.text_block_started = false;
296 }
297
298 out.extend(close_anthropic_reasoning_block(state));
299
300 for tool in state.tool_states.values_mut() {
301 if tool.started {
302 if let Some(index) = tool.content_index {
303 out.push(json!({"type": "content_block_stop", "index": index}));
304 }
305 tool.started = false;
306 }
307 }
308
309 state.active_anthropic_tool = None;
310 for index in std::mem::take(&mut state.deferred_anthropic_tools) {
311 out.extend(encode_anthropic_tool_delta(state, index, None, None, None));
312 if let Some(tool) = state.tool_states.get_mut(&index) {
313 if let Some(content_index) = tool.content_index {
314 out.push(json!({"type": "content_block_stop", "index": content_index}));
315 }
316 tool.started = false;
317 }
318 state.active_anthropic_tool = None;
319 }
320
321 if !state.emitted_content_block {
322 out.push(json!({
323 "type": "content_block_start",
324 "index": 0,
325 "content_block": {"type": "text", "text": ""},
326 }));
327 out.push(json!({"type": "content_block_stop", "index": 0}));
328 }
329
330 if !state.emitted_message_delta {
331 out.push(json!({
332 "type": "message_delta",
333 "delta": {
334 "stop_reason": anthropic_stop_reason(state.stop_reason.as_deref()),
335 "stop_sequence": Value::Null,
336 "stop_details": anthropic_stop_details(
337 state.stop_reason.as_deref(),
338 state.stop_details.as_ref(),
339 ),
340 },
341 "usage": anthropic_stream_usage(state),
342 }));
343 state.emitted_message_delta = true;
344 }
345 out.push(json!({"type": "message_stop"}));
346 state.finished = true;
347 out
348}
349
350fn decode_anthropic_content_block_start(
352 state: &mut StreamTranslationState,
353 object: &Map<String, Value>,
354) -> Vec<LlmResponseChunk> {
355 let index = object.get("index").and_then(Value::as_u64).unwrap_or(0) as usize;
356 let block = object.get("content_block").and_then(Value::as_object);
357 match block
358 .and_then(|block| block.get("type"))
359 .and_then(Value::as_str)
360 {
361 Some("text") => block
362 .and_then(|block| block.get("text"))
363 .and_then(Value::as_str)
364 .filter(|text| !text.is_empty())
365 .map(|text| {
366 vec![LlmResponseChunk::TextDelta {
367 index,
368 text: text.to_string(),
369 }]
370 })
371 .unwrap_or_default(),
372 Some("thinking") => {
373 let mut out = Vec::new();
374 if let Some(text) = block
375 .and_then(|block| block.get("thinking"))
376 .and_then(Value::as_str)
377 .filter(|text| !text.is_empty())
378 {
379 out.push(LlmResponseChunk::ReasoningDelta {
380 index,
381 text: text.to_string(),
382 });
383 }
384 if let Some(signature) = block
385 .and_then(|block| block.get("signature"))
386 .and_then(Value::as_str)
387 .filter(|signature| !signature.is_empty())
388 {
389 out.push(anthropic_signature_delta(index, signature));
390 }
391 out
392 }
393 Some("tool_use") => {
394 let Some(block) = block else {
395 return Vec::new();
396 };
397 if block
398 .get("input")
399 .and_then(Value::as_object)
400 .is_some_and(|input| input.is_empty())
401 {
402 state.empty_anthropic_tool_inputs.insert(index);
403 }
404 vec![LlmResponseChunk::ToolCallDelta {
405 index,
406 id: block
407 .get("id")
408 .and_then(Value::as_str)
409 .map(desanitize_anthropic_tool_use_id),
410 name: block
411 .get("name")
412 .and_then(Value::as_str)
413 .map(ToOwned::to_owned),
414 arguments_delta: block.get("input").and_then(tool_input_delta),
415 }]
416 }
417 _ => Vec::new(),
418 }
419}
420
421fn decode_anthropic_content_block_delta(
423 state: &mut StreamTranslationState,
424 object: &Map<String, Value>,
425) -> Vec<LlmResponseChunk> {
426 let index = object.get("index").and_then(Value::as_u64).unwrap_or(0) as usize;
427 let Some(delta) = object.get("delta").and_then(Value::as_object) else {
428 return Vec::new();
429 };
430 match delta.get("type").and_then(Value::as_str) {
431 Some("text_delta") => delta
432 .get("text")
433 .and_then(Value::as_str)
434 .map(|text| {
435 vec![LlmResponseChunk::TextDelta {
436 index,
437 text: text.to_string(),
438 }]
439 })
440 .unwrap_or_default(),
441 Some("thinking_delta") => delta
442 .get("thinking")
443 .and_then(Value::as_str)
444 .map(|text| {
445 vec![LlmResponseChunk::ReasoningDelta {
446 index,
447 text: text.to_string(),
448 }]
449 })
450 .unwrap_or_default(),
451 Some("signature_delta") => delta
452 .get("signature")
453 .and_then(Value::as_str)
454 .map(|signature| vec![anthropic_signature_delta(index, signature)])
455 .unwrap_or_default(),
456 Some("input_json_delta") => delta
457 .get("partial_json")
458 .and_then(Value::as_str)
459 .map(|partial_json| {
460 if !partial_json.is_empty() {
461 state.empty_anthropic_tool_inputs.remove(&index);
462 }
463 vec![LlmResponseChunk::ToolCallDelta {
464 index,
465 id: None,
466 name: None,
467 arguments_delta: Some(partial_json.to_string()),
468 }]
469 })
470 .unwrap_or_default(),
471 _ => Vec::new(),
472 }
473}
474
475fn anthropic_signature_delta(index: usize, signature: &str) -> LlmResponseChunk {
476 LlmResponseChunk::ReasoningDetailsDelta {
477 index,
478 details: vec![json!({"type": "anthropic.signature_delta", "signature": signature})],
479 text: String::new(),
480 }
481}
482
483fn ensure_anthropic_text_block(state: &mut StreamTranslationState) -> Vec<Value> {
485 let mut out = close_anthropic_reasoning_block(state);
486 if state.text_block_started {
487 return out;
488 }
489 let index = state.next_content_index;
490 state.next_content_index += 1;
491 state.text_block_index = Some(index);
492 state.text_block_started = true;
493 state.emitted_content_block = true;
494 out.push(json!({
495 "type": "content_block_start",
496 "index": index,
497 "content_block": {"type": "text", "text": ""},
498 }));
499 out
500}
501
502fn ensure_anthropic_reasoning_block(state: &mut StreamTranslationState) -> Vec<Value> {
504 let mut out = Vec::new();
505 if state.text_block_started {
506 if let Some(index) = state.text_block_index {
507 out.push(json!({"type": "content_block_stop", "index": index}));
508 }
509 state.text_block_started = false;
510 }
511 if state.reasoning_block_started {
512 return out;
513 }
514 let index = state.next_content_index;
515 state.next_content_index += 1;
516 state.reasoning_block_index = Some(index);
517 state.reasoning_block_started = true;
518 state.emitted_content_block = true;
519 out.push(json!({
520 "type": "content_block_start",
521 "index": index,
522 "content_block": {"type": "thinking", "thinking": "", "signature": ""},
523 }));
524 out
525}
526
527fn close_anthropic_reasoning_block(state: &mut StreamTranslationState) -> Vec<Value> {
529 if !state.reasoning_block_started {
530 return Vec::new();
531 }
532 let mut out = Vec::new();
533 if let Some(index) = state.reasoning_block_index {
534 out.push(json!({
535 "type": "content_block_delta",
536 "index": index,
537 "delta": {"type": "signature_delta", "signature": ""},
538 }));
539 out.push(json!({"type": "content_block_stop", "index": index}));
540 }
541 state.reasoning_block_started = false;
542 out
543}
544
545fn encode_anthropic_tool_delta(
547 state: &mut StreamTranslationState,
548 index: usize,
549 id: Option<String>,
550 name: Option<String>,
551 arguments_delta: Option<String>,
552) -> Vec<Value> {
553 let mut out = Vec::new();
554 out.extend(close_anthropic_reasoning_block(state));
555 if state.text_block_started {
556 if let Some(index) = state.text_block_index {
557 out.push(json!({"type": "content_block_stop", "index": index}));
558 }
559 state.text_block_started = false;
560 }
561
562 let other_tool_started = *state.active_anthropic_tool.get_or_insert(index) != index;
564 let tool = state.tool_states.entry(index).or_default();
565 if let Some(id) = id.filter(|id| !id.is_empty()) {
566 tool.id = Some(sanitize_anthropic_tool_use_id(&id));
567 }
568 if let Some(name) = name.filter(|name| !name.is_empty()) {
569 tool.name = Some(name);
570 }
571 if let Some(delta) = arguments_delta {
572 tool.arguments.push_str(&delta);
573 tool.pending_arguments.push_str(&delta);
574 }
575
576 if other_tool_started {
578 if !state.deferred_anthropic_tools.contains(&index) {
579 state.deferred_anthropic_tools.push(index);
580 }
581 return out;
582 }
583
584 if !tool.started {
585 let (Some(id), Some(name)) = (
587 tool.id.as_deref().filter(|id| !id.is_empty()),
588 tool.name.as_deref().filter(|name| !name.is_empty()),
589 ) else {
590 return out;
591 };
592 let content_index = state.next_content_index;
593 state.next_content_index += 1;
594 tool.content_index = Some(content_index);
595 tool.started = true;
596 state.emitted_content_block = true;
597 out.push(json!({
598 "type": "content_block_start",
599 "index": content_index,
600 "content_block": {
601 "type": "tool_use",
602 "id": id,
603 "name": name,
604 "input": {},
605 },
606 }));
607 if !tool.pending_arguments.is_empty() {
608 out.push(json!({
609 "type": "content_block_delta",
610 "index": content_index,
611 "delta": {
612 "type": "input_json_delta",
613 "partial_json": tool.pending_arguments,
614 },
615 }));
616 tool.pending_arguments.clear();
617 }
618 return out;
619 }
620
621 if let Some(content_index) = tool.content_index
622 && !tool.pending_arguments.is_empty()
623 {
624 out.push(json!({
625 "type": "content_block_delta",
626 "index": content_index,
627 "delta": {
628 "type": "input_json_delta",
629 "partial_json": tool.pending_arguments,
630 },
631 }));
632 tool.pending_arguments.clear();
633 }
634 out
635}
636
637fn capture_anthropic_usage(state: &mut StreamTranslationState, usage: &Value) {
639 let Some(usage) = usage.as_object() else {
640 return;
641 };
642 if let Some(value) = usage.get("input_tokens").and_then(Value::as_u64) {
643 state.usage.input_tokens = Some(value);
644 }
645 if let Some(cost) = crate::codecs::common::provider_cost(usage) {
646 state.usage.provider_cost = Some(cost);
647 }
648 if let Some(tokens) = usage
649 .get("cache_creation")
650 .and_then(|details| details.get("ephemeral_1h_input_tokens"))
651 .and_then(Value::as_u64)
652 {
653 state
654 .usage
655 .cache
656 .get_or_insert_with(Default::default)
657 .cache_creation_1h_input_tokens = Some(tokens);
658 }
659 if let Some(value) = usage.get("output_tokens").and_then(Value::as_u64) {
660 state.usage.output_tokens = Some(value);
661 }
662 if let Some(value) = usage
663 .get("cache_creation_input_tokens")
664 .and_then(Value::as_u64)
665 {
666 state.usage.set_cache_creation_input_tokens(value);
667 }
668 if let Some(value) = usage.get("cache_read_input_tokens").and_then(Value::as_u64) {
669 state.usage.set_cached_input_tokens(value);
670 }
671 if let Some(value) = usage.get("output_tokens_details").and_then(|details| {
672 details
673 .get("thinking_tokens")
674 .and_then(Value::as_u64)
675 .or_else(|| details.get("reasoning_tokens").and_then(Value::as_u64))
676 }) {
677 state.usage.reasoning_tokens = Some(value);
678 }
679}
680
681fn anthropic_stream_usage(state: &StreamTranslationState) -> Value {
683 let mut usage = Map::new();
684 if state.saw_backend_usage {
685 if let Some(input_tokens) = state.usage.input_tokens {
686 usage.insert("input_tokens".to_string(), json!(input_tokens));
687 }
688 usage.insert(
689 "output_tokens".to_string(),
690 json!(state.usage.output_tokens.unwrap_or(0)),
691 );
692 } else {
693 usage.insert("output_tokens".to_string(), json!(state.output_tokens_seen));
694 }
695 if let Some(value) = state.usage.cache_creation_input_tokens() {
696 usage.insert("cache_creation_input_tokens".to_string(), json!(value));
697 }
698 if let Some(value) = state.usage.cached_input_tokens() {
699 usage.insert("cache_read_input_tokens".to_string(), json!(value));
700 }
701 if let Some(value) = state.usage.reasoning_tokens {
702 usage.insert(
703 "output_tokens_details".to_string(),
704 json!({"thinking_tokens": value}),
705 );
706 }
707 crate::codecs::common::encode_usage_metadata(Value::Object(usage), &state.usage)
708}
709
710fn anthropic_message_id(state: &StreamTranslationState) -> String {
712 let Some(id) = target_message_id_or_source_message_id(state) else {
713 return "msg_switchyard".to_string();
714 };
715 if id.starts_with("msg_") {
716 id.to_string()
717 } else {
718 format!("msg_{id}")
719 }
720}
721
722fn anthropic_stop_reason(reason: Option<&str>) -> String {
724 match reason {
725 Some("length") => "max_tokens".to_string(),
726 Some("tool_calls") | Some("function_call") => "tool_use".to_string(),
727 Some("content_filter" | "refusal") => "refusal".to_string(),
728 Some(reason @ ("end_turn" | "max_tokens" | "tool_use" | "stop_sequence")) => {
729 reason.to_string()
730 }
731 _ => "end_turn".to_string(),
732 }
733}
734
735fn anthropic_stop_details(reason: Option<&str>, source: Option<&Value>) -> Value {
742 match reason {
743 Some("content_filter" | "refusal") => source.cloned().unwrap_or_else(|| {
744 json!({
745 "type": "refusal",
746 "category": Value::Null,
747 "explanation": Value::Null,
748 })
749 }),
750 _ => Value::Null,
751 }
752}
753
754fn tool_input_delta(value: &Value) -> Option<String> {
756 match value {
757 Value::String(text) if !text.is_empty() => Some(text.clone()),
758 Value::Object(object) if !object.is_empty() => serde_json::to_string(value).ok(),
759 _ => None,
760 }
761}
762
763#[cfg(test)]
764mod tests {
765 use super::*;
766
767 #[test]
768 fn recognizes_stream_completion() {
769 let codec = AnthropicMessagesStreamCodec;
770 assert!(!codec.sse_done_is_terminal());
771 assert!(codec.is_terminal_event(&json!({"type": "message_stop"})));
772 assert!(!codec.is_terminal_event(&json!({"type": "message_delta"})));
773 }
774
775 #[test]
776 fn anthropic_replay_stamps_the_served_model_inside_message() {
777 let mut event = json!({
778 "type": "message_start",
779 "message": {
780 "id": "msg_1",
781 "model": "provider/model",
782 "provider_extension": true,
783 },
784 });
785
786 AnthropicMessagesStreamCodec.stamp_streamed_response_model(&mut event, "served/model");
787
788 assert_eq!(event["message"]["model"], "served/model");
789 assert_eq!(event["message"]["provider_extension"], true);
790 }
791}