1use serde_json::{Value, json};
33
34#[must_use]
40pub fn messages_to_chat(anthropic: &Value) -> Option<Value> {
41 let model = anthropic.get("model")?.as_str()?;
42 let src_messages = anthropic.get("messages")?.as_array()?;
43
44 let mut messages: Vec<Value> = Vec::with_capacity(src_messages.len() + 1);
45 if let Some(system) = anthropic.get("system")
46 && let Some(text) = system_text(system)
47 {
48 messages.push(json!({"role": "system", "content": text}));
49 }
50 for msg in src_messages {
51 translate_message(msg, &mut messages)?;
52 }
53
54 let mut out = json!({"model": model, "messages": messages});
55 let o = out.as_object_mut().expect("constructed as object");
56
57 if let Some(v) = anthropic.get("max_tokens").and_then(Value::as_u64) {
58 o.insert("max_tokens".into(), json!(v));
59 }
60 for key in ["temperature", "top_p"] {
61 if let Some(v) = anthropic.get(key).and_then(Value::as_f64) {
62 o.insert(key.into(), json!(v));
63 }
64 }
65 if let Some(stops) = anthropic.get("stop_sequences").and_then(Value::as_array)
66 && !stops.is_empty()
67 {
68 o.insert("stop".into(), Value::Array(stops.clone()));
69 }
70 if let Some(user) = anthropic
71 .pointer("/metadata/user_id")
72 .and_then(Value::as_str)
73 {
74 o.insert("user".into(), json!(user));
75 }
76 if let Some(tools) = anthropic.get("tools").and_then(Value::as_array) {
77 let translated: Vec<Value> = tools.iter().filter_map(translate_tool).collect();
78 if !translated.is_empty() {
79 o.insert("tools".into(), Value::Array(translated));
80 }
81 }
82 if let Some(choice) = anthropic.get("tool_choice")
83 && let Some(mapped) = translate_tool_choice(choice)
84 {
85 o.insert("tool_choice".into(), mapped);
86 }
87 if anthropic.get("stream").and_then(Value::as_bool) == Some(true) {
88 o.insert("stream".into(), json!(true));
89 o.insert("stream_options".into(), json!({"include_usage": true}));
92 }
93 Some(out)
94}
95
96fn system_text(system: &Value) -> Option<String> {
98 if let Some(s) = system.as_str() {
99 return Some(s.to_string());
100 }
101 let parts: Vec<&str> = system
102 .as_array()?
103 .iter()
104 .filter_map(|b| b.get("text").and_then(Value::as_str))
105 .collect();
106 if parts.is_empty() {
107 None
108 } else {
109 Some(parts.join("\n\n"))
110 }
111}
112
113fn translate_message(msg: &Value, out: &mut Vec<Value>) -> Option<()> {
118 let role = msg.get("role")?.as_str()?;
119 let content = msg.get("content")?;
120
121 if let Some(text) = content.as_str() {
122 out.push(json!({"role": role, "content": text}));
123 return Some(());
124 }
125 let blocks = content.as_array()?;
126 if role == "assistant" {
127 translate_assistant_blocks(blocks, out)
128 } else {
129 translate_user_blocks(blocks, out);
130 Some(())
131 }
132}
133
134fn translate_assistant_blocks(blocks: &[Value], out: &mut Vec<Value>) -> Option<()> {
137 let mut text = String::new();
138 let mut tool_calls: Vec<Value> = Vec::new();
139 for b in blocks {
140 match b.get("type").and_then(Value::as_str) {
141 Some("text") => {
142 if let Some(t) = b.get("text").and_then(Value::as_str) {
143 if !text.is_empty() {
144 text.push('\n');
145 }
146 text.push_str(t);
147 }
148 }
149 Some("tool_use") => {
150 let args = b.get("input").cloned().unwrap_or_else(|| json!({}));
151 tool_calls.push(json!({
152 "id": b.get("id").and_then(Value::as_str).unwrap_or_default(),
153 "type": "function",
154 "function": {
155 "name": b.get("name").and_then(Value::as_str).unwrap_or_default(),
156 "arguments": serde_json::to_string(&args).ok()?,
157 }
158 }));
159 }
160 _ => {}
161 }
162 }
163 let mut m = json!({"role": "assistant"});
164 let mo = m.as_object_mut().expect("object");
165 mo.insert(
166 "content".into(),
167 if text.is_empty() {
168 Value::Null
169 } else {
170 json!(text)
171 },
172 );
173 if !tool_calls.is_empty() {
174 mo.insert("tool_calls".into(), Value::Array(tool_calls));
175 }
176 out.push(m);
177 Some(())
178}
179
180fn translate_user_blocks(blocks: &[Value], out: &mut Vec<Value>) {
184 let mut parts: Vec<Value> = Vec::new();
185 let mut plain_text_only = true;
186 for b in blocks {
187 match b.get("type").and_then(Value::as_str) {
188 Some("tool_result") => {
189 out.push(json!({
190 "role": "tool",
191 "tool_call_id": b.get("tool_use_id").and_then(Value::as_str).unwrap_or_default(),
192 "content": tool_result_text(b),
193 }));
194 }
195 Some("text") => {
196 if let Some(t) = b.get("text").and_then(Value::as_str) {
197 parts.push(json!({"type": "text", "text": t}));
198 }
199 }
200 Some("image") => {
201 plain_text_only = false;
202 if let (Some(mt), Some(data)) = (
203 b.pointer("/source/media_type").and_then(Value::as_str),
204 b.pointer("/source/data").and_then(Value::as_str),
205 ) {
206 parts.push(json!({
207 "type": "image_url",
208 "image_url": {"url": format!("data:{mt};base64,{data}")}
209 }));
210 }
211 }
212 _ => {}
213 }
214 }
215 if !parts.is_empty() {
216 let content = if plain_text_only {
217 let joined: Vec<&str> = parts
219 .iter()
220 .filter_map(|p| p.get("text").and_then(Value::as_str))
221 .collect();
222 json!(joined.join("\n"))
223 } else {
224 Value::Array(parts)
225 };
226 out.push(json!({"role": "user", "content": content}));
227 }
228}
229
230fn tool_result_text(block: &Value) -> String {
232 match block.get("content") {
233 Some(Value::String(s)) => s.clone(),
234 Some(Value::Array(blocks)) => blocks
235 .iter()
236 .filter_map(|b| b.get("text").and_then(Value::as_str))
237 .collect::<Vec<_>>()
238 .join("\n"),
239 _ => String::new(),
240 }
241}
242
243fn translate_tool(tool: &Value) -> Option<Value> {
244 let name = tool.get("name")?.as_str()?;
245 let mut function = json!({
246 "name": name,
247 "parameters": tool.get("input_schema").cloned().unwrap_or_else(|| json!({"type": "object"})),
248 });
249 if let Some(desc) = tool.get("description").and_then(Value::as_str) {
250 function["description"] = json!(desc);
251 }
252 Some(json!({"type": "function", "function": function}))
253}
254
255fn translate_tool_choice(choice: &Value) -> Option<Value> {
256 match choice.get("type").and_then(Value::as_str)? {
257 "auto" => Some(json!("auto")),
258 "any" => Some(json!("required")),
259 "none" => Some(json!("none")),
260 "tool" => {
261 let name = choice.get("name")?.as_str()?;
262 Some(json!({"type": "function", "function": {"name": name}}))
263 }
264 _ => None,
265 }
266}
267
268#[must_use]
273pub fn chat_to_messages(openai: &Value) -> Option<Value> {
274 let choice = openai.get("choices")?.as_array()?.first()?;
275 let message = choice.get("message")?;
276
277 let mut content: Vec<Value> = Vec::new();
278 if let Some(text) = message.get("content").and_then(Value::as_str)
279 && !text.is_empty()
280 {
281 content.push(json!({"type": "text", "text": text}));
282 }
283 if let Some(calls) = message.get("tool_calls").and_then(Value::as_array) {
284 for call in calls {
285 let f = call.get("function")?;
286 let args: Value = f
287 .get("arguments")
288 .and_then(Value::as_str)
289 .and_then(|s| serde_json::from_str(s).ok())
290 .unwrap_or_else(|| json!({}));
291 content.push(json!({
292 "type": "tool_use",
293 "id": call.get("id").and_then(Value::as_str).unwrap_or_default(),
294 "name": f.get("name").and_then(Value::as_str).unwrap_or_default(),
295 "input": args,
296 }));
297 }
298 }
299
300 let finish = choice.get("finish_reason").and_then(Value::as_str);
301 let id = openai.get("id").and_then(Value::as_str).unwrap_or("xlat");
302 Some(json!({
303 "id": format!("msg_{id}"),
304 "type": "message",
305 "role": "assistant",
306 "model": openai.get("model").and_then(Value::as_str).unwrap_or_default(),
307 "content": content,
308 "stop_reason": stop_reason(finish),
309 "stop_sequence": Value::Null,
310 "usage": usage_to_anthropic(openai.get("usage")),
311 }))
312}
313
314#[must_use]
317pub fn error_to_anthropic(openai: &Value) -> Option<Value> {
318 let err = openai.get("error")?;
319 let message = err.get("message").and_then(Value::as_str).unwrap_or("");
320 let kind = match err.get("type").and_then(Value::as_str) {
321 Some("insufficient_quota" | "rate_limit_error" | "requests" | "tokens") => {
322 "rate_limit_error"
323 }
324 Some("invalid_request_error") => "invalid_request_error",
325 Some("authentication_error") => "authentication_error",
326 _ => "api_error",
327 };
328 Some(json!({
329 "type": "error",
330 "error": {"type": kind, "message": message},
331 }))
332}
333
334fn stop_reason(finish: Option<&str>) -> &'static str {
335 match finish {
336 Some("length") => "max_tokens",
337 Some("tool_calls" | "function_call") => "tool_use",
338 _ => "end_turn",
340 }
341}
342
343fn usage_to_anthropic(usage: Option<&Value>) -> Value {
344 let prompt = usage
345 .and_then(|u| u.get("prompt_tokens"))
346 .and_then(Value::as_u64)
347 .unwrap_or(0);
348 let completion = usage
349 .and_then(|u| u.get("completion_tokens"))
350 .and_then(Value::as_u64)
351 .unwrap_or(0);
352 let cached = usage
353 .and_then(|u| u.pointer("/prompt_tokens_details/cached_tokens"))
354 .and_then(Value::as_u64)
355 .unwrap_or(0);
356 json!({
357 "input_tokens": prompt.saturating_sub(cached),
360 "output_tokens": completion,
361 "cache_read_input_tokens": cached,
362 "cache_creation_input_tokens": 0,
363 })
364}
365
366#[derive(Debug, PartialEq)]
370enum OpenBlock {
371 Text,
372 Tool(u64),
374}
375
376#[derive(Default)]
380pub struct StreamXlat {
381 line_buf: Vec<u8>,
382 started: bool,
383 finished: bool,
384 block_index: u64,
385 open: Option<OpenBlock>,
386 finish_reason: Option<String>,
387 usage: Option<Value>,
388}
389
390const MAX_LINE_BYTES: usize = 1 << 20;
392
393impl StreamXlat {
394 pub fn feed(&mut self, chunk: &[u8]) -> Vec<u8> {
396 let mut out = Vec::new();
397 for byte in chunk {
398 if *byte == b'\n' {
399 let line = std::mem::take(&mut self.line_buf);
400 self.process_line(&line, &mut out);
401 } else if self.line_buf.len() < MAX_LINE_BYTES {
402 self.line_buf.push(*byte);
403 }
404 }
405 out
406 }
407
408 pub fn finish(&mut self) -> Vec<u8> {
410 let mut out = Vec::new();
411 self.emit_closing(&mut out);
412 out
413 }
414
415 fn process_line(&mut self, line: &[u8], out: &mut Vec<u8>) {
416 let line = std::str::from_utf8(line).unwrap_or("").trim();
417 let Some(data) = line.strip_prefix("data:").map(str::trim) else {
418 return; };
420 if data == "[DONE]" {
421 self.emit_closing(out);
422 return;
423 }
424 let Ok(chunk) = serde_json::from_str::<Value>(data) else {
425 return;
426 };
427
428 if let Some(usage) = chunk.get("usage")
430 && !usage.is_null()
431 {
432 self.usage = Some(usage_to_anthropic(Some(usage)));
433 }
434
435 if !self.started {
436 self.emit_message_start(&chunk, out);
437 }
438
439 let Some(choice) = chunk
440 .get("choices")
441 .and_then(Value::as_array)
442 .and_then(|c| c.first())
443 else {
444 return;
445 };
446 if let Some(reason) = choice.get("finish_reason").and_then(Value::as_str) {
447 self.finish_reason = Some(reason.to_string());
448 }
449 let Some(delta) = choice.get("delta") else {
450 return;
451 };
452
453 if let Some(text) = delta.get("content").and_then(Value::as_str)
454 && !text.is_empty()
455 {
456 if self.open != Some(OpenBlock::Text) {
457 self.close_block(out);
458 emit_event(
459 out,
460 "content_block_start",
461 &json!({
462 "type": "content_block_start",
463 "index": self.block_index,
464 "content_block": {"type": "text", "text": ""},
465 }),
466 );
467 self.open = Some(OpenBlock::Text);
468 }
469 emit_event(
470 out,
471 "content_block_delta",
472 &json!({
473 "type": "content_block_delta",
474 "index": self.block_index,
475 "delta": {"type": "text_delta", "text": text},
476 }),
477 );
478 }
479
480 if let Some(calls) = delta.get("tool_calls").and_then(Value::as_array) {
481 for call in calls {
482 self.process_tool_delta(call, out);
483 }
484 }
485 }
486
487 fn process_tool_delta(&mut self, call: &Value, out: &mut Vec<u8>) {
488 let idx = call.get("index").and_then(Value::as_u64).unwrap_or(0);
489 let name = call.pointer("/function/name").and_then(Value::as_str);
490
491 let continues = self.open == Some(OpenBlock::Tool(idx)) && name.is_none();
494 if !continues {
495 self.close_block(out);
496 emit_event(
497 out,
498 "content_block_start",
499 &json!({
500 "type": "content_block_start",
501 "index": self.block_index,
502 "content_block": {
503 "type": "tool_use",
504 "id": call.get("id").and_then(Value::as_str).unwrap_or_default(),
505 "name": name.unwrap_or_default(),
506 "input": {},
507 },
508 }),
509 );
510 self.open = Some(OpenBlock::Tool(idx));
511 }
512 if let Some(args) = call.pointer("/function/arguments").and_then(Value::as_str)
513 && !args.is_empty()
514 {
515 emit_event(
516 out,
517 "content_block_delta",
518 &json!({
519 "type": "content_block_delta",
520 "index": self.block_index,
521 "delta": {"type": "input_json_delta", "partial_json": args},
522 }),
523 );
524 }
525 }
526
527 fn emit_message_start(&mut self, chunk: &Value, out: &mut Vec<u8>) {
528 self.started = true;
529 let id = chunk.get("id").and_then(Value::as_str).unwrap_or("xlat");
530 let model = chunk.get("model").and_then(Value::as_str).unwrap_or("");
531 emit_event(
532 out,
533 "message_start",
534 &json!({
535 "type": "message_start",
536 "message": {
537 "id": format!("msg_{id}"),
538 "type": "message",
539 "role": "assistant",
540 "model": model,
541 "content": [],
542 "stop_reason": Value::Null,
543 "stop_sequence": Value::Null,
544 "usage": {"input_tokens": 0, "output_tokens": 0},
547 },
548 }),
549 );
550 }
551
552 fn close_block(&mut self, out: &mut Vec<u8>) {
553 if self.open.take().is_some() {
554 emit_event(
555 out,
556 "content_block_stop",
557 &json!({"type": "content_block_stop", "index": self.block_index}),
558 );
559 self.block_index += 1;
560 }
561 }
562
563 fn emit_closing(&mut self, out: &mut Vec<u8>) {
564 if self.finished {
565 return;
566 }
567 self.finished = true;
568 if !self.started {
569 self.emit_message_start(&json!({}), out);
571 }
572 self.close_block(out);
573 let usage = self
574 .usage
575 .take()
576 .unwrap_or_else(|| json!({"output_tokens": 0}));
577 emit_event(
578 out,
579 "message_delta",
580 &json!({
581 "type": "message_delta",
582 "delta": {
583 "stop_reason": stop_reason(self.finish_reason.as_deref()),
584 "stop_sequence": Value::Null,
585 },
586 "usage": usage,
587 }),
588 );
589 emit_event(out, "message_stop", &json!({"type": "message_stop"}));
590 }
591}
592
593fn emit_event(out: &mut Vec<u8>, event: &str, data: &Value) {
594 out.extend_from_slice(b"event: ");
595 out.extend_from_slice(event.as_bytes());
596 out.extend_from_slice(b"\ndata: ");
597 out.extend_from_slice(data.to_string().as_bytes());
598 out.extend_from_slice(b"\n\n");
599}
600
601pub fn to_anthropic_stream<S, B, E>(
605 inner: S,
606) -> impl futures::Stream<Item = Result<Vec<u8>, E>> + Send + 'static
607where
608 S: futures::Stream<Item = Result<B, E>> + Send + Unpin + 'static,
609 B: AsRef<[u8]> + Send + 'static,
610 E: Send + 'static,
611{
612 use futures::StreamExt;
613 futures::stream::unfold(
614 (inner, StreamXlat::default(), false),
615 |(mut inner, mut xlat, ended)| async move {
616 if ended {
617 return None;
618 }
619 loop {
620 match inner.next().await {
621 Some(Ok(chunk)) => {
622 let translated = xlat.feed(chunk.as_ref());
623 if translated.is_empty() {
624 continue; }
626 return Some((Ok(translated), (inner, xlat, false)));
627 }
628 Some(Err(e)) => return Some((Err(e), (inner, xlat, false))),
629 None => {
630 let tail = xlat.finish();
631 if tail.is_empty() {
632 return None;
633 }
634 return Some((Ok(tail), (inner, xlat, true)));
635 }
636 }
637 }
638 },
639 )
640}
641
642#[cfg(test)]
643mod tests {
644 use super::*;
645
646 fn full_anthropic_request() -> Value {
649 json!({
650 "model": "gpt-4o-mini",
651 "max_tokens": 1024,
652 "temperature": 0.5,
653 "stop_sequences": ["END"],
654 "metadata": {"user_id": "u-42"},
655 "system": [
656 {"type": "text", "text": "You are helpful.", "cache_control": {"type": "ephemeral"}}
657 ],
658 "messages": [
659 {"role": "user", "content": "What's the weather in Zurich?"},
660 {"role": "assistant", "content": [
661 {"type": "text", "text": "Let me check."},
662 {"type": "tool_use", "id": "toolu_1", "name": "get_weather",
663 "input": {"city": "Zurich"}}
664 ]},
665 {"role": "user", "content": [
666 {"type": "tool_result", "tool_use_id": "toolu_1",
667 "content": [{"type": "text", "text": "18°C, sunny"}]},
668 {"type": "text", "text": "And tomorrow?"}
669 ]}
670 ],
671 "tools": [
672 {"name": "get_weather", "description": "Weather lookup",
673 "input_schema": {"type": "object", "properties": {"city": {"type": "string"}}},
674 "cache_control": {"type": "ephemeral"}}
675 ],
676 "tool_choice": {"type": "auto"},
677 "stream": true
678 })
679 }
680
681 #[test]
682 fn request_maps_system_tools_and_history() {
683 let out = messages_to_chat(&full_anthropic_request()).expect("translates");
684 let messages = out["messages"].as_array().unwrap();
685
686 assert_eq!(messages[0]["role"], "system");
687 assert_eq!(messages[0]["content"], "You are helpful.");
688 assert_eq!(messages[1]["role"], "user");
689 assert_eq!(messages[1]["content"], "What's the weather in Zurich?");
690
691 assert_eq!(messages[2]["role"], "assistant");
693 assert_eq!(messages[2]["content"], "Let me check.");
694 let call = &messages[2]["tool_calls"][0];
695 assert_eq!(call["id"], "toolu_1");
696 assert_eq!(call["function"]["name"], "get_weather");
697 let args: Value = serde_json::from_str(call["function"]["arguments"].as_str().unwrap())
698 .expect("arguments are a JSON string");
699 assert_eq!(args["city"], "Zurich");
700
701 assert_eq!(messages[3]["role"], "tool");
703 assert_eq!(messages[3]["tool_call_id"], "toolu_1");
704 assert_eq!(messages[3]["content"], "18°C, sunny");
705 assert_eq!(messages[4]["role"], "user");
706 assert_eq!(messages[4]["content"], "And tomorrow?");
707
708 assert_eq!(out["tools"][0]["type"], "function");
710 assert_eq!(out["tools"][0]["function"]["name"], "get_weather");
711 assert!(out["tools"][0]["function"]["parameters"]["properties"]["city"].is_object());
712 assert_eq!(out["tool_choice"], "auto");
713 assert_eq!(out["max_tokens"], 1024);
714 assert_eq!(out["stop"][0], "END");
715 assert_eq!(out["user"], "u-42");
716 assert_eq!(out["stream"], true);
717 assert_eq!(out["stream_options"]["include_usage"], true);
718 assert!(out.to_string().find("cache_control").is_none());
720 }
721
722 #[test]
723 fn request_maps_images_and_forced_tool_choice() {
724 let body = json!({
725 "model": "gpt-4o", "max_tokens": 100,
726 "tool_choice": {"type": "tool", "name": "extract"},
727 "messages": [{"role": "user", "content": [
728 {"type": "text", "text": "Describe this"},
729 {"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": "AAAA"}}
730 ]}]
731 });
732 let out = messages_to_chat(&body).unwrap();
733 let content = out["messages"][0]["content"].as_array().unwrap();
734 assert_eq!(content[0]["type"], "text");
735 assert_eq!(content[1]["type"], "image_url");
736 assert_eq!(content[1]["image_url"]["url"], "data:image/png;base64,AAAA");
737 assert_eq!(out["tool_choice"]["function"]["name"], "extract");
738 }
739
740 #[test]
741 fn untranslatable_bodies_return_none() {
742 assert!(messages_to_chat(&json!({"model": "m"})).is_none());
743 assert!(messages_to_chat(&json!({"messages": []})).is_none());
744 }
745
746 #[test]
749 fn response_maps_text_tools_and_cached_usage() {
750 let openai = json!({
751 "id": "chatcmpl-9x", "object": "chat.completion", "model": "gpt-4o-mini",
752 "choices": [{"index": 0, "finish_reason": "tool_calls", "message": {
753 "role": "assistant", "content": "Checking.",
754 "tool_calls": [{"id": "call_7", "type": "function",
755 "function": {"name": "get_weather", "arguments": "{\"city\":\"Zurich\"}"}}]
756 }}],
757 "usage": {"prompt_tokens": 120, "completion_tokens": 30,
758 "prompt_tokens_details": {"cached_tokens": 100}}
759 });
760 let msg = chat_to_messages(&openai).expect("translates");
761 assert_eq!(msg["type"], "message");
762 assert_eq!(msg["id"], "msg_chatcmpl-9x");
763 assert_eq!(msg["model"], "gpt-4o-mini");
764 assert_eq!(msg["stop_reason"], "tool_use");
765 assert_eq!(msg["content"][0]["type"], "text");
766 assert_eq!(msg["content"][0]["text"], "Checking.");
767 assert_eq!(msg["content"][1]["type"], "tool_use");
768 assert_eq!(msg["content"][1]["id"], "call_7");
769 assert_eq!(msg["content"][1]["input"]["city"], "Zurich");
770 assert_eq!(msg["usage"]["input_tokens"], 20);
772 assert_eq!(msg["usage"]["output_tokens"], 30);
773 assert_eq!(msg["usage"]["cache_read_input_tokens"], 100);
774 }
775
776 #[test]
777 fn error_envelope_translates_to_anthropic_shape() {
778 let openai = json!({"error": {"message": "quota exceeded",
779 "type": "insufficient_quota", "code": "insufficient_quota"}});
780 let anthropic = error_to_anthropic(&openai).unwrap();
781 assert_eq!(anthropic["type"], "error");
782 assert_eq!(anthropic["error"]["type"], "rate_limit_error");
783 assert_eq!(anthropic["error"]["message"], "quota exceeded");
784 assert!(error_to_anthropic(&json!({"ok": true})).is_none());
785 }
786
787 fn parse_events(bytes: &[u8]) -> Vec<(String, Value)> {
791 let text = std::str::from_utf8(bytes).unwrap();
792 let mut events = Vec::new();
793 let mut current_event = String::new();
794 for line in text.lines() {
795 if let Some(e) = line.strip_prefix("event: ") {
796 current_event = e.to_string();
797 } else if let Some(d) = line.strip_prefix("data: ") {
798 events.push((current_event.clone(), serde_json::from_str(d).unwrap()));
799 }
800 }
801 events
802 }
803
804 #[test]
805 fn stream_translates_text_then_tool_call() {
806 let mut xlat = StreamXlat::default();
807 let mut out = Vec::new();
808 for line in [
809 r#"data: {"id":"c1","model":"gpt-4o-mini","choices":[{"index":0,"delta":{"role":"assistant","content":"Hel"}}]}"#,
810 r#"data: {"id":"c1","choices":[{"index":0,"delta":{"content":"lo"}}]}"#,
811 r#"data: {"id":"c1","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_1","type":"function","function":{"name":"get_weather","arguments":""}}]}}]}"#,
812 r#"data: {"id":"c1","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\"city\":\"ZRH\"}"}}]}}]}"#,
813 r#"data: {"id":"c1","choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}]}"#,
814 r#"data: {"id":"c1","choices":[],"usage":{"prompt_tokens":50,"completion_tokens":12}}"#,
815 "data: [DONE]",
816 ] {
817 out.extend(xlat.feed(line.as_bytes()));
818 out.extend(xlat.feed(b"\n\n"));
819 }
820
821 let events = parse_events(&out);
822 let names: Vec<&str> = events.iter().map(|(e, _)| e.as_str()).collect();
823 assert_eq!(
824 names,
825 vec![
826 "message_start",
827 "content_block_start", "content_block_delta",
829 "content_block_delta",
830 "content_block_stop",
831 "content_block_start", "content_block_delta",
833 "content_block_stop",
834 "message_delta",
835 "message_stop",
836 ]
837 );
838
839 assert_eq!(events[0].1["message"]["model"], "gpt-4o-mini");
840 assert_eq!(events[2].1["delta"]["text"], "Hel");
841 assert_eq!(events[3].1["delta"]["text"], "lo");
842 let tool_start = &events[5].1;
843 assert_eq!(tool_start["content_block"]["type"], "tool_use");
844 assert_eq!(tool_start["content_block"]["id"], "call_1");
845 assert_eq!(tool_start["content_block"]["name"], "get_weather");
846 assert_eq!(tool_start["index"], 1);
847 assert_eq!(events[6].1["delta"]["partial_json"], "{\"city\":\"ZRH\"}");
848 let delta = &events[8].1;
849 assert_eq!(delta["delta"]["stop_reason"], "tool_use");
850 assert_eq!(delta["usage"]["input_tokens"], 50);
851 assert_eq!(delta["usage"]["output_tokens"], 12);
852 }
853
854 #[test]
855 fn stream_survives_chunk_splits_mid_line() {
856 let full = concat!(
857 r#"data: {"id":"c2","model":"m","choices":[{"index":0,"delta":{"content":"split works"}}]}"#,
858 "\n\ndata: [DONE]\n\n"
859 );
860 let mut xlat = StreamXlat::default();
861 let mut out = Vec::new();
862 for chunk in full.as_bytes().chunks(7) {
864 out.extend(xlat.feed(chunk));
865 }
866 let events = parse_events(&out);
867 assert_eq!(events.first().unwrap().0, "message_start");
868 assert!(
869 events
870 .iter()
871 .any(|(e, d)| e == "content_block_delta" && d["delta"]["text"] == "split works")
872 );
873 assert_eq!(events.last().unwrap().0, "message_stop");
874 }
875
876 #[test]
877 fn stream_without_done_is_closed_by_finish() {
878 let mut xlat = StreamXlat::default();
879 let mut out = xlat.feed(
880 b"data: {\"id\":\"c3\",\"model\":\"m\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"hi\"},\"finish_reason\":\"stop\"}]}\n\n",
881 );
882 out.extend(xlat.finish());
883 out.extend(xlat.finish()); let events = parse_events(&out);
885 assert_eq!(events.last().unwrap().0, "message_stop");
886 let delta = events.iter().find(|(e, _)| e == "message_delta").unwrap();
887 assert_eq!(delta.1["delta"]["stop_reason"], "end_turn");
888 }
889
890 #[test]
891 fn stream_adapter_translates_and_flushes() {
892 let chunks: Vec<Result<Vec<u8>, std::convert::Infallible>> = vec![
893 Ok(br#"data: {"id":"c4","model":"m","choices":[{"index":0,"delta":{"content":"ok"}}]}"#.to_vec()),
894 Ok(b"\n\n".to_vec()),
895 ];
896 let inner = futures::stream::iter(chunks);
897 let translated = to_anthropic_stream(Box::pin(inner));
898 let collected: Vec<_> =
899 futures::executor::block_on(futures::StreamExt::collect::<Vec<_>>(translated));
900 let bytes: Vec<u8> = collected.into_iter().flat_map(Result::unwrap).collect();
901 let events = parse_events(&bytes);
902 assert_eq!(events.first().unwrap().0, "message_start");
903 assert_eq!(events.last().unwrap().0, "message_stop");
904 }
905}