1use serde_json::{json, Map, Value};
7
8use crate::codecs::stream::{
9 record_source_identity, target_message_id_or_source_message_id, target_model_or_source_model,
10 StreamCodec, StreamTranslationState,
11};
12use crate::format::{FormatId, WireFormat};
13use crate::llm::LlmStreamEvent;
14use crate::util::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<LlmStreamEvent> {
29 decode_anthropic_stream(state, event)
30 }
31
32 fn encode_event(
33 &self,
34 state: &mut StreamTranslationState,
35 event: LlmStreamEvent,
36 ) -> Vec<Value> {
37 encode_anthropic_stream(state, event)
38 }
39
40 fn finish(&self, state: &mut StreamTranslationState) -> Vec<Value> {
41 finish_anthropic_stream(state)
42 }
43}
44
45fn decode_anthropic_stream(
47 state: &mut StreamTranslationState,
48 event: &Value,
49) -> Vec<LlmStreamEvent> {
50 let Some(object) = event.as_object() else {
51 return vec![LlmStreamEvent::Error {
52 message: "Anthropic stream event is not an object".to_string(),
53 }];
54 };
55 match object.get("type").and_then(Value::as_str) {
56 Some("message_start") => {
57 state.saw_message_start = true;
58 let message = object.get("message").and_then(Value::as_object);
59 if let Some(model) = message
60 .and_then(|message| message.get("model"))
61 .and_then(Value::as_str)
62 {
63 state.model = Some(model.to_string());
64 }
65 if let Some(id) = message
66 .and_then(|message| message.get("id"))
67 .and_then(Value::as_str)
68 {
69 state.message_id = Some(id.to_string());
70 }
71 if let Some(message) = message {
72 if let Some(usage) = message.get("usage") {
73 capture_anthropic_usage(state, usage);
74 }
75 }
76 vec![LlmStreamEvent::MessageStart {
77 id: state.message_id.clone(),
78 model: state.model.clone(),
79 }]
80 }
81 Some("content_block_start") => decode_anthropic_content_block_start(object),
82 Some("content_block_delta") => decode_anthropic_content_block_delta(object),
83 Some("message_delta") => {
84 let mut out = Vec::new();
85 if let Some(usage) = object.get("usage") {
86 capture_anthropic_usage(state, usage);
87 out.push(LlmStreamEvent::Usage(state.usage.clone()));
88 }
89 if let Some(stop_reason) = object
90 .get("delta")
91 .and_then(Value::as_object)
92 .and_then(|delta| delta.get("stop_reason"))
93 .and_then(Value::as_str)
94 {
95 out.push(LlmStreamEvent::MessageStop {
96 reason: Some(stop_reason.to_string()),
97 });
98 }
99 out
100 }
101 Some("message_stop") => vec![LlmStreamEvent::MessageStop { reason: None }],
102 Some("error") => vec![LlmStreamEvent::Error {
103 message: object
104 .get("error")
105 .and_then(Value::as_object)
106 .and_then(|error| error.get("message"))
107 .and_then(Value::as_str)
108 .unwrap_or("unknown Anthropic stream error")
109 .to_string(),
110 }],
111 _ => Vec::new(),
112 }
113}
114
115fn encode_anthropic_stream(
117 state: &mut StreamTranslationState,
118 event: LlmStreamEvent,
119) -> Vec<Value> {
120 match event {
121 LlmStreamEvent::MessageStart { id, model } => {
122 record_source_identity(state, id, model);
123 if state.emitted_message_start {
124 Vec::new()
125 } else {
126 state.emitted_message_start = true;
127 vec![json!({
128 "type": "message_start",
129 "message": {
130 "id": anthropic_message_id(state),
131 "type": "message",
132 "role": "assistant",
133 "model": target_model_or_source_model(state),
134 "content": [],
135 "stop_reason": Value::Null,
136 "stop_sequence": Value::Null,
137 "usage": {"input_tokens": 0, "output_tokens": 0},
138 },
139 })]
140 }
141 }
142 LlmStreamEvent::TextDelta { text, .. } => {
143 state.output_tokens_seen += 1;
144 let mut out = ensure_anthropic_text_block(state);
145 out.push(json!({
146 "type": "content_block_delta",
147 "index": state.text_block_index.unwrap_or(0),
148 "delta": {"type": "text_delta", "text": text},
149 }));
150 out
151 }
152 LlmStreamEvent::ReasoningDelta { text, .. } => {
153 let mut out = ensure_anthropic_reasoning_block(state);
154 out.push(json!({
155 "type": "content_block_delta",
156 "index": state.reasoning_block_index.unwrap_or(0),
157 "delta": {"type": "thinking_delta", "thinking": text},
158 }));
159 out
160 }
161 LlmStreamEvent::ToolCallDelta {
162 index,
163 id,
164 name,
165 arguments_delta,
166 } => encode_anthropic_tool_delta(state, index, id, name, arguments_delta),
167 LlmStreamEvent::Usage(usage) => {
168 state.usage = usage;
169 state.saw_backend_usage = true;
170 Vec::new()
171 }
172 LlmStreamEvent::MessageStop { reason } => {
173 state.stop_reason = reason.or_else(|| state.stop_reason.clone());
174 Vec::new()
175 }
176 LlmStreamEvent::Error { message } => {
177 vec![json!({"type": "error", "error": {"message": message}})]
178 }
179 }
180}
181
182fn finish_anthropic_stream(state: &mut StreamTranslationState) -> Vec<Value> {
184 let mut out = Vec::new();
185 if !state.emitted_message_start {
186 out.extend(encode_anthropic_stream(
187 state,
188 LlmStreamEvent::MessageStart {
189 id: state.message_id.clone(),
190 model: state.model.clone(),
191 },
192 ));
193 }
194
195 if state.text_block_started {
196 if let Some(index) = state.text_block_index {
197 out.push(json!({"type": "content_block_stop", "index": index}));
198 }
199 state.text_block_started = false;
200 }
201
202 out.extend(close_anthropic_reasoning_block(state));
203
204 for tool in state.tool_states.values_mut() {
205 if tool.started {
206 if let Some(index) = tool.content_index {
207 out.push(json!({"type": "content_block_stop", "index": index}));
208 }
209 tool.started = false;
210 }
211 }
212
213 if !state.emitted_content_block {
214 out.push(json!({
215 "type": "content_block_start",
216 "index": 0,
217 "content_block": {"type": "text", "text": ""},
218 }));
219 out.push(json!({"type": "content_block_stop", "index": 0}));
220 }
221
222 out.push(json!({
223 "type": "message_delta",
224 "delta": {
225 "stop_reason": anthropic_stop_reason(state.stop_reason.as_deref()),
226 "stop_sequence": Value::Null,
227 },
228 "usage": anthropic_stream_usage(state),
229 }));
230 out.push(json!({"type": "message_stop"}));
231 state.finished = true;
232 out
233}
234
235fn decode_anthropic_content_block_start(object: &Map<String, Value>) -> Vec<LlmStreamEvent> {
237 let index = object.get("index").and_then(Value::as_u64).unwrap_or(0) as usize;
238 let block = object.get("content_block").and_then(Value::as_object);
239 match block
240 .and_then(|block| block.get("type"))
241 .and_then(Value::as_str)
242 {
243 Some("text") => block
244 .and_then(|block| block.get("text"))
245 .and_then(Value::as_str)
246 .filter(|text| !text.is_empty())
247 .map(|text| {
248 vec![LlmStreamEvent::TextDelta {
249 index,
250 text: text.to_string(),
251 }]
252 })
253 .unwrap_or_default(),
254 Some("thinking") => block
255 .and_then(|block| block.get("thinking"))
256 .and_then(Value::as_str)
257 .filter(|text| !text.is_empty())
258 .map(|text| {
259 vec![LlmStreamEvent::ReasoningDelta {
260 index,
261 text: text.to_string(),
262 }]
263 })
264 .unwrap_or_default(),
265 Some("tool_use") => {
266 let Some(block) = block else {
267 return Vec::new();
268 };
269 vec![LlmStreamEvent::ToolCallDelta {
270 index,
271 id: block
272 .get("id")
273 .and_then(Value::as_str)
274 .map(ToOwned::to_owned),
275 name: block
276 .get("name")
277 .and_then(Value::as_str)
278 .map(ToOwned::to_owned),
279 arguments_delta: block.get("input").and_then(tool_input_delta),
280 }]
281 }
282 _ => Vec::new(),
283 }
284}
285
286fn decode_anthropic_content_block_delta(object: &Map<String, Value>) -> Vec<LlmStreamEvent> {
288 let index = object.get("index").and_then(Value::as_u64).unwrap_or(0) as usize;
289 let Some(delta) = object.get("delta").and_then(Value::as_object) else {
290 return Vec::new();
291 };
292 match delta.get("type").and_then(Value::as_str) {
293 Some("text_delta") => delta
294 .get("text")
295 .and_then(Value::as_str)
296 .map(|text| {
297 vec![LlmStreamEvent::TextDelta {
298 index,
299 text: text.to_string(),
300 }]
301 })
302 .unwrap_or_default(),
303 Some("thinking_delta") => delta
304 .get("thinking")
305 .and_then(Value::as_str)
306 .map(|text| {
307 vec![LlmStreamEvent::ReasoningDelta {
308 index,
309 text: text.to_string(),
310 }]
311 })
312 .unwrap_or_default(),
313 Some("signature_delta") => Vec::new(),
314 Some("input_json_delta") => delta
315 .get("partial_json")
316 .and_then(Value::as_str)
317 .map(|partial_json| {
318 vec![LlmStreamEvent::ToolCallDelta {
319 index,
320 id: None,
321 name: None,
322 arguments_delta: Some(partial_json.to_string()),
323 }]
324 })
325 .unwrap_or_default(),
326 _ => Vec::new(),
327 }
328}
329
330fn ensure_anthropic_text_block(state: &mut StreamTranslationState) -> Vec<Value> {
332 let mut out = close_anthropic_reasoning_block(state);
333 if state.text_block_started {
334 return out;
335 }
336 let index = state.next_content_index;
337 state.next_content_index += 1;
338 state.text_block_index = Some(index);
339 state.text_block_started = true;
340 state.emitted_content_block = true;
341 out.push(json!({
342 "type": "content_block_start",
343 "index": index,
344 "content_block": {"type": "text", "text": ""},
345 }));
346 out
347}
348
349fn ensure_anthropic_reasoning_block(state: &mut StreamTranslationState) -> Vec<Value> {
351 let mut out = Vec::new();
352 if state.text_block_started {
353 if let Some(index) = state.text_block_index {
354 out.push(json!({"type": "content_block_stop", "index": index}));
355 }
356 state.text_block_started = false;
357 }
358 if state.reasoning_block_started {
359 return out;
360 }
361 let index = state.next_content_index;
362 state.next_content_index += 1;
363 state.reasoning_block_index = Some(index);
364 state.reasoning_block_started = true;
365 state.emitted_content_block = true;
366 out.push(json!({
367 "type": "content_block_start",
368 "index": index,
369 "content_block": {"type": "thinking", "thinking": "", "signature": ""},
370 }));
371 out
372}
373
374fn close_anthropic_reasoning_block(state: &mut StreamTranslationState) -> Vec<Value> {
376 if !state.reasoning_block_started {
377 return Vec::new();
378 }
379 let mut out = Vec::new();
380 if let Some(index) = state.reasoning_block_index {
381 out.push(json!({
382 "type": "content_block_delta",
383 "index": index,
384 "delta": {"type": "signature_delta", "signature": ""},
385 }));
386 out.push(json!({"type": "content_block_stop", "index": index}));
387 }
388 state.reasoning_block_started = false;
389 out
390}
391
392fn encode_anthropic_tool_delta(
394 state: &mut StreamTranslationState,
395 index: usize,
396 id: Option<String>,
397 name: Option<String>,
398 arguments_delta: Option<String>,
399) -> Vec<Value> {
400 let mut out = Vec::new();
401 out.extend(close_anthropic_reasoning_block(state));
402 if state.text_block_started {
403 if let Some(index) = state.text_block_index {
404 out.push(json!({"type": "content_block_stop", "index": index}));
405 }
406 state.text_block_started = false;
407 }
408
409 let tool = state.tool_states.entry(index).or_default();
410 if id.is_some() {
411 tool.id = id.map(|id| sanitize_anthropic_tool_use_id(&id));
412 }
413 if name.is_some() {
414 tool.name = name;
415 }
416 if let Some(delta) = arguments_delta {
417 tool.arguments.push_str(&delta);
418 tool.pending_arguments.push_str(&delta);
419 }
420
421 if !tool.started {
422 let Some(name) = tool.name.clone() else {
423 return out;
424 };
425 let content_index = state.next_content_index;
426 state.next_content_index += 1;
427 tool.content_index = Some(content_index);
428 tool.started = true;
429 state.emitted_content_block = true;
430 out.push(json!({
431 "type": "content_block_start",
432 "index": content_index,
433 "content_block": {
434 "type": "tool_use",
435 "id": tool.id.clone().unwrap_or_else(|| format!("toolu_{index}")),
436 "name": name,
437 "input": {},
438 },
439 }));
440 if !tool.pending_arguments.is_empty() {
441 out.push(json!({
442 "type": "content_block_delta",
443 "index": content_index,
444 "delta": {
445 "type": "input_json_delta",
446 "partial_json": tool.pending_arguments,
447 },
448 }));
449 tool.pending_arguments.clear();
450 }
451 return out;
452 }
453
454 if let Some(content_index) = tool.content_index {
455 if !tool.pending_arguments.is_empty() {
456 out.push(json!({
457 "type": "content_block_delta",
458 "index": content_index,
459 "delta": {
460 "type": "input_json_delta",
461 "partial_json": tool.pending_arguments,
462 },
463 }));
464 tool.pending_arguments.clear();
465 }
466 }
467 out
468}
469
470fn capture_anthropic_usage(state: &mut StreamTranslationState, usage: &Value) {
472 let Some(usage) = usage.as_object() else {
473 return;
474 };
475 for key in [
476 "input_tokens",
477 "output_tokens",
478 "cache_creation_input_tokens",
479 "cache_read_input_tokens",
480 ] {
481 if let Some(value) = usage.get(key).and_then(Value::as_u64) {
482 state.usage_extras.insert(key.to_string(), value);
483 }
484 }
485 state.usage.input_tokens = state.usage_extras.get("input_tokens").copied();
486 state.usage.output_tokens = state.usage_extras.get("output_tokens").copied();
487}
488
489fn anthropic_stream_usage(state: &StreamTranslationState) -> Value {
491 let mut usage = Map::new();
492 if state.saw_backend_usage {
493 if let Some(input_tokens) = state.usage.input_tokens {
494 usage.insert("input_tokens".to_string(), json!(input_tokens));
495 }
496 usage.insert(
497 "output_tokens".to_string(),
498 json!(state.usage.output_tokens.unwrap_or(0)),
499 );
500 } else {
501 usage.insert("output_tokens".to_string(), json!(state.output_tokens_seen));
502 }
503 for (key, value) in &state.usage_extras {
504 if key == "input_tokens" || key == "output_tokens" {
505 continue;
506 }
507 usage.insert(key.clone(), json!(value));
508 }
509 Value::Object(usage)
510}
511
512fn anthropic_message_id(state: &StreamTranslationState) -> String {
514 let Some(id) = target_message_id_or_source_message_id(state) else {
515 return "msg_switchyard".to_string();
516 };
517 if id.starts_with("msg_") {
518 id.to_string()
519 } else {
520 format!("msg_{id}")
521 }
522}
523
524fn anthropic_stop_reason(reason: Option<&str>) -> String {
526 match reason {
527 Some("length") => "max_tokens".to_string(),
528 Some("tool_calls") | Some("function_call") => "tool_use".to_string(),
529 Some("end_turn") | Some("max_tokens") | Some("tool_use") | Some("stop_sequence") => {
530 reason.unwrap_or("end_turn").to_string()
531 }
532 _ => "end_turn".to_string(),
533 }
534}
535
536fn tool_input_delta(value: &Value) -> Option<String> {
538 match value {
539 Value::String(text) if !text.is_empty() => Some(text.clone()),
540 Value::Object(object) if !object.is_empty() => serde_json::to_string(value).ok(),
541 _ => None,
542 }
543}