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