1use crate::canonical::{CanonChunk, Usage, json_str, write_json_str};
13#[cfg(feature = "axum")]
14use crate::error::ProxyError;
15#[cfg(feature = "axum")]
16use futures::Stream;
17
18#[derive(Debug, Default)]
26pub struct StopWindow {
27 seqs: Vec<String>,
28 max_len: usize,
29 tail: String,
31 block: Option<usize>,
32 kind: &'static str,
33 pub matched: Option<String>,
35}
36
37impl StopWindow {
38 fn new(seqs: Vec<String>) -> Option<Self> {
39 let seqs: Vec<String> = seqs.into_iter().filter(|s| !s.is_empty()).collect();
40 let max_len = seqs.iter().map(|s| s.chars().count()).max()?;
41 Some(StopWindow {
42 seqs,
43 max_len,
44 tail: String::new(),
45 block: None,
46 kind: "",
47 matched: None,
48 })
49 }
50
51 fn feed(&mut self, idx: usize, kind: &'static str, incoming: &str) -> String {
56 if self.matched.is_some() {
57 return String::new();
58 }
59 if self.block != Some(idx) {
60 self.block = Some(idx);
62 self.kind = kind;
63 self.tail.clear();
64 }
65 let mut buf = std::mem::take(&mut self.tail);
66 buf.push_str(incoming);
67 for s in &self.seqs {
68 if let Some(pos) = buf.find(s.as_str()) {
69 self.matched = Some(s.clone());
70 buf.truncate(pos);
71 return buf;
72 }
73 }
74 let keep = self.max_len.saturating_sub(1);
75 let split = buf
76 .char_indices()
77 .rev()
78 .take(keep)
79 .last()
80 .map(|(i, _)| i)
81 .unwrap_or(buf.len());
82 self.tail = buf.split_off(split);
83 buf
84 }
85
86 fn take_tail(&mut self) -> Option<(usize, &'static str, String)> {
89 let idx = self.block?;
90 if self.tail.is_empty() {
91 return None;
92 }
93 Some((idx, self.kind, std::mem::take(&mut self.tail)))
94 }
95}
96
97pub struct StreamState {
98 pub first: bool,
100 pub blocks: Vec<bool>,
102 pub thinking_index: Option<usize>,
105 pub text_index: Option<usize>,
108 pub tool_base_index: Option<usize>,
111 pub input_tokens: u64,
114 pub stop_reason: Option<String>,
115 pub pending_usage: Option<TerminalUsage>,
119 pub message_stopped: bool,
120 pub stop_window: Option<StopWindow>,
122}
123
124impl StreamState {
125 pub fn new() -> Self {
126 Self {
127 first: true,
128 blocks: Vec::new(),
129 thinking_index: None,
130 text_index: None,
131 tool_base_index: None,
132 input_tokens: 0,
133 stop_reason: None,
134 pending_usage: None,
135 message_stopped: false,
136 stop_window: None,
137 }
138 }
139
140 pub fn with_stop_sequences(seqs: Vec<String>) -> Self {
142 Self {
143 stop_window: StopWindow::new(seqs),
144 ..Self::new()
145 }
146 }
147}
148
149impl Default for StreamState {
150 fn default() -> Self {
151 Self::new()
152 }
153}
154
155#[derive(Debug, Clone, Copy, Default)]
160pub struct TerminalUsage {
161 pub input: u64,
162 pub output: u64,
163 pub cached_read: u64,
164 pub cache_write: u64,
165}
166
167impl TerminalUsage {
168 pub fn from_usage(u: &Usage) -> Self {
171 Self {
172 input: u.prompt_tokens,
173 output: u.completion_tokens,
174 cached_read: u.cached_read_tokens,
175 cache_write: u.cache_write_tokens,
176 }
177 }
178 fn wire_input(&self, fallback: u64) -> u64 {
180 (if self.input > 0 { self.input } else { fallback })
181 .saturating_sub(self.cached_read + self.cache_write)
182 }
183}
184
185fn block_delta(idx: usize, delta_json: String) -> (&'static str, String) {
191 (
192 "content_block_delta",
193 format!("{{\"delta\":{delta_json},\"index\":{idx},\"type\":\"content_block_delta\"}}"),
194 )
195}
196
197fn text_delta(idx: usize, text: &str) -> (&'static str, String) {
201 let mut data = String::with_capacity(64 + text.len());
202 data.push_str("{\"delta\":{\"text\":");
203 write_json_str(&mut data, text);
204 data.push_str(",\"type\":\"text_delta\"},\"index\":");
205 data.push_str(&idx.to_string());
206 data.push_str(",\"type\":\"content_block_delta\"}");
207 ("content_block_delta", data)
208}
209
210fn block_stop(idx: usize) -> (&'static str, String) {
212 (
213 "content_block_stop",
214 format!("{{\"index\":{idx},\"type\":\"content_block_stop\"}}"),
215 )
216}
217
218fn open_block(
219 out: &mut Vec<(&'static str, String)>,
220 state: &mut StreamState,
221 idx: usize,
222 block: &str,
223) {
224 flush_stop_tail(out, state);
225 for i in 0..idx {
227 if i < state.blocks.len() && state.blocks[i] {
228 state.blocks[i] = false;
229 out.push(block_stop(i));
230 }
231 }
232 if state.blocks.len() <= idx {
233 state.blocks.resize(idx + 1, false);
234 }
235 state.blocks[idx] = true;
236 out.push((
237 "content_block_start",
238 format!("{{\"content_block\":{block},\"index\":{idx},\"type\":\"content_block_start\"}}"),
239 ));
240}
241
242fn flush_stop_tail(out: &mut Vec<(&'static str, String)>, state: &mut StreamState) {
245 let Some((idx, kind, text)) = state.stop_window.as_mut().and_then(StopWindow::take_tail) else {
246 return;
247 };
248 if state.blocks.get(idx) != Some(&true) {
249 return;
250 }
251 let mut delta = String::with_capacity(48 + text.len());
252 if kind == "thinking" {
253 delta.push_str("{\"thinking\":");
254 write_json_str(&mut delta, &text);
255 delta.push_str(",\"type\":\"thinking_delta\"}");
256 } else {
257 delta.push_str("{\"text\":");
258 write_json_str(&mut delta, &text);
259 delta.push_str(",\"type\":\"text_delta\"}");
260 }
261 out.push(block_delta(idx, delta));
262}
263
264fn close_block(out: &mut Vec<(&'static str, String)>, state: &mut StreamState, upto: usize) {
265 flush_stop_tail(out, state);
266 for (i, open) in state.blocks.iter_mut().enumerate().take(upto) {
267 if *open {
268 *open = false;
269 out.push(block_stop(i));
270 }
271 }
272}
273
274fn next_index(state: &StreamState) -> usize {
277 state.blocks.len()
278}
279
280fn write_terminal_usage(
285 buf: &mut String,
286 input: u64,
287 output: u64,
288 cached_read: u64,
289 cache_write: u64,
290) {
291 buf.push('{');
293 if cache_write > 0 {
294 buf.push_str("\"cache_creation_input_tokens\":");
295 buf.push_str(&cache_write.to_string());
296 buf.push(',');
297 }
298 if cached_read > 0 {
299 buf.push_str("\"cache_read_input_tokens\":");
300 buf.push_str(&cached_read.to_string());
301 buf.push(',');
302 }
303 buf.push_str("\"input_tokens\":");
304 buf.push_str(&input.to_string());
305 buf.push_str(",\"output_tokens\":");
306 buf.push_str(&output.to_string());
307 buf.push('}');
308}
309
310fn emit_terminal(
314 out: &mut Vec<(&'static str, String)>,
315 state: &mut StreamState,
316 stop_reason: String,
317 usage: TerminalUsage,
318) {
319 close_block(out, state, state.blocks.len());
320 let prompt = usage.wire_input(state.input_tokens);
321 let matched = state.stop_window.as_ref().and_then(|w| w.matched.clone());
325 let mut data = String::with_capacity(112);
326 data.push_str("{\"delta\":{\"stop_reason\":");
327 match &matched {
328 Some(s) => {
329 data.push_str("\"stop_sequence\",\"stop_sequence\":");
330 write_json_str(&mut data, s);
331 }
332 None => {
333 write_json_str(&mut data, &stop_reason);
334 data.push_str(",\"stop_sequence\":null");
335 }
336 }
337 data.push_str("},\"type\":\"message_delta\",\"usage\":");
338 write_terminal_usage(
339 &mut data,
340 prompt,
341 usage.output,
342 usage.cached_read,
343 usage.cache_write,
344 );
345 data.push('}');
346 out.push(("message_delta", data));
347 out.push(("message_stop", "{\"type\":\"message_stop\"}".to_string()));
348 state.message_stopped = true;
349}
350
351pub fn chunk_to_sse_events(
358 chunk: &CanonChunk,
359 model: &str,
360 state: &mut StreamState,
361 msg_id: &str,
362) -> Vec<(&'static str, String)> {
363 if state.message_stopped {
364 return Vec::new();
365 }
366 let mut out = Vec::new();
367
368 if let Some(n) = chunk.input_tokens.filter(|n| *n > 0) {
373 state.input_tokens = n;
374 }
375
376 if state.first {
377 state.first = false;
378 let mut data = String::with_capacity(208 + msg_id.len() + model.len());
379 data.push_str("{\"message\":{\"content\":[],\"id\":");
380 write_json_str(&mut data, msg_id);
381 data.push_str(",\"model\":");
382 write_json_str(&mut data, model);
383 data.push_str(",\"role\":\"assistant\",\"stop_reason\":null,\"stop_sequence\":null,\"type\":\"message\",\"usage\":{\"input_tokens\":");
384 data.push_str(&state.input_tokens.to_string());
385 data.push_str(",\"output_tokens\":0}},\"type\":\"message_start\"}");
386 out.push(("message_start", data));
387 }
388
389 let is_trailer = chunk.finish_reason.is_none()
395 && chunk.usage.is_some()
396 && chunk.delta_text.is_empty()
397 && chunk.thinking.is_none()
398 && chunk.tool_calls.is_none();
399 let merged_usage = chunk.usage.as_ref().map(TerminalUsage::from_usage);
400 if is_trailer {
401 let Some(sr) = state.stop_reason.take() else {
402 if let Some(u) = merged_usage {
406 state.pending_usage = Some(u);
407 }
408 return out;
409 };
410 let usage = merged_usage
412 .or(state.pending_usage.take())
413 .unwrap_or_default();
414 emit_terminal(&mut out, state, sr, usage);
415 return out;
416 }
417
418 if let Some(th) = &chunk.thinking {
423 let idx = match state.thinking_index {
424 Some(i) if state.blocks.get(i) == Some(&true) => i,
425 _ => {
426 let i = next_index(state);
427 state.thinking_index = Some(i);
428 open_block(
429 &mut out,
430 state,
431 i,
432 "{\"thinking\":\"\",\"type\":\"thinking\"}",
433 );
434 i
435 }
436 };
437 let text = match th.kind {
441 "signature" => Some(std::borrow::Cow::Borrowed(&th.text)),
444 _ => match state.stop_window.as_mut() {
445 Some(w) => match w.feed(idx, "thinking", &th.text) {
446 s if s.is_empty() => None,
447 s => Some(std::borrow::Cow::Owned(s)),
448 },
449 None => Some(std::borrow::Cow::Borrowed(&th.text)),
450 },
451 };
452 if let Some(text) = text {
453 let mut delta = String::with_capacity(48 + text.len());
454 if th.kind == "signature" {
455 delta.push_str("{\"signature\":");
456 write_json_str(&mut delta, &text);
457 delta.push_str(",\"type\":\"signature_delta\"}");
458 } else {
459 delta.push_str("{\"thinking\":");
460 write_json_str(&mut delta, &text);
461 delta.push_str(",\"type\":\"thinking_delta\"}");
462 }
463 out.push(block_delta(idx, delta));
464 }
465 }
466 if !chunk.delta_text.is_empty() {
467 let idx = match state.text_index {
470 Some(i) if state.blocks.get(i) == Some(&true) => i,
471 _ => {
472 let i = next_index(state);
473 state.text_index = Some(i);
474 open_block(&mut out, state, i, "{\"text\":\"\",\"type\":\"text\"}");
475 i
476 }
477 };
478 if state.stop_window.is_some() {
479 let emit = state
480 .stop_window
481 .as_mut()
482 .map(|w| w.feed(idx, "text", &chunk.delta_text))
483 .unwrap_or_default();
484 if !emit.is_empty() {
485 out.push(block_delta(
486 idx,
487 format!("{{\"text\":{},\"type\":\"text_delta\"}}", json_str(&emit)),
488 ));
489 }
490 } else if !chunk.delta_text.is_empty() {
491 out.push(text_delta(idx, &chunk.delta_text));
494 }
495 }
496 if let Some(tcs) = chunk.tool_calls.as_ref().and_then(|t| t.as_array()) {
500 if state.tool_base_index.is_none() {
501 state.tool_base_index = Some(next_index(state));
502 }
503 let base = state.tool_base_index.unwrap_or(0);
504 for tc in tcs {
505 let idx = tc["index"].as_u64().unwrap_or(0) as usize + base;
506 let tc_id = tc["id"].as_str().filter(|s| !s.is_empty());
507 if let Some(name) = tc["function"]["name"].as_str() {
508 let id = anthropic_tool_use_id(
511 tc_id,
512 msg_id,
513 tc["index"].as_u64().unwrap_or(0) as usize,
514 );
515 let mut block = String::with_capacity(48 + id.len() + name.len());
516 block.push_str("{\"id\":");
517 write_json_str(&mut block, &id);
518 block.push_str(",\"input\":{},\"name\":");
519 write_json_str(&mut block, name);
520 block.push_str(",\"type\":\"tool_use\"}");
521 open_block(&mut out, state, idx, &block);
522 }
523 if let Some(args) = tc["function"]["arguments"].as_str()
524 && !args.is_empty()
525 {
526 if state.blocks.get(idx) != Some(&true) {
527 tracing::warn!(
532 index = idx,
533 "dropping tool argument delta for closed/unopened content block"
534 );
535 continue;
536 }
537 let mut delta = String::with_capacity(48 + args.len());
538 delta.push_str("{\"partial_json\":");
539 write_json_str(&mut delta, args);
540 delta.push_str(",\"type\":\"input_json_delta\"}");
541 out.push(block_delta(idx, delta));
542 }
543 }
544 }
545 if let Some(fr) = &chunk.finish_reason {
546 let sr = map_stop_reason_outbound(fr).to_string();
547 if let Some(usage) = merged_usage {
548 emit_terminal(&mut out, state, sr, usage);
552 } else {
553 state.stop_reason = Some(sr);
556 }
557 }
558 out
559}
560
561pub(super) fn anthropic_tool_use_id(
571 upstream_id: Option<&str>,
572 msg_id: &str,
573 index: usize,
574) -> String {
575 let msg_id = msg_id.strip_prefix("chatcmpl-").unwrap_or(msg_id);
578 match upstream_id.filter(|s| !s.is_empty()) {
579 Some(id) if id.starts_with("toolu_") => id.to_string(),
582 Some(id) => {
583 let slug: String = id
584 .chars()
585 .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
586 .collect();
587 format!("toolu_x_{msg_id}_{slug}")
588 }
589 None => format!("toolu_x_{msg_id}_{index}"),
590 }
591}
592
593pub(super) fn map_stop_reason_outbound(fr: &str) -> &str {
594 match fr {
595 "stop" => "end_turn",
596 "length" => "max_tokens",
597 "tool_calls" => "tool_use",
598 "content_filter" => "refusal",
601 known @ ("end_turn" | "max_tokens" | "stop_sequence" | "tool_use" | "pause_turn"
603 | "refusal") => known,
604 _ => "end_turn",
606 }
607}
608
609pub fn finalize_stream(state: &mut StreamState) -> Vec<(&'static str, String)> {
613 if state.message_stopped {
614 return Vec::new();
615 }
616 state.message_stopped = true;
617 if state.first {
618 return Vec::new();
620 }
621 let mut out = Vec::new();
622 let usage = state.pending_usage.take().unwrap_or_default();
623 let sr = state
624 .stop_reason
625 .take()
626 .unwrap_or_else(|| "end_turn".to_string());
627 emit_terminal(&mut out, state, sr, usage);
628 out
629}
630
631#[cfg(feature = "axum")]
635pub fn anthropic_stream_response<S>(
636 inner: S,
637 model: String,
638 msg_id: String,
639 stop_sequences: Vec<String>,
640) -> axum::response::Response
641where
642 S: Stream<Item = Result<CanonChunk, ProxyError>> + Unpin + Send + 'static,
643{
644 let state = std::sync::Arc::new(std::sync::Mutex::new(StreamState::with_stop_sequences(
645 stop_sequences,
646 )));
647 let state_done = state.clone();
648 crate::dialect::sse::sse_response(
649 inner,
650 move |out, item| {
651 let mut st = state.lock().unwrap();
652 match item {
653 Ok(chunk) => {
654 for (ev, d) in chunk_to_sse_events(&chunk, &model, &mut st, &msg_id) {
655 out.push(format!("event: {ev}\ndata: {d}\n\n"));
656 }
657 }
658 Err(e) => {
659 st.message_stopped = true;
661 let (_, j) = super::out::error_json(&e);
662 out.push(format!("event: error\ndata: {j}\n\n"));
663 }
664 }
665 },
666 move |out| {
667 let mut st = state_done.lock().unwrap();
668 for (ev, d) in finalize_stream(&mut st) {
669 out.push(format!("event: {ev}\ndata: {d}\n\n"));
670 }
671 },
672 )
673}
674
675#[cfg(all(test, feature = "axum"))]
676mod tests {
677 use super::*;
678 use crate::canonical::{ThinkingDelta, Usage};
679
680 fn run_stream(stops: Vec<String>, chunks: Vec<CanonChunk>) -> String {
682 let mut st = StreamState::with_stop_sequences(stops);
683 let mut out = String::new();
684 for c in &chunks {
685 for (ev, d) in chunk_to_sse_events(c, "m", &mut st, "msg_1") {
686 out.push_str(&format!("event: {ev}\ndata: {d}\n\n"));
687 }
688 }
689 out
690 }
691
692 fn streamed_text(frames: &str) -> String {
693 frames
694 .lines()
695 .filter(|l| l.starts_with("data: "))
696 .filter_map(|l| serde_json::from_str::<serde_json::Value>(&l[6..]).ok())
697 .filter(|v| v["type"] == "content_block_delta")
698 .filter_map(|v| {
699 v["delta"]["text"]
700 .as_str()
701 .or(v["delta"]["thinking"].as_str())
702 .map(str::to_string)
703 })
704 .collect()
705 }
706
707 fn finish_stop() -> CanonChunk {
708 CanonChunk {
709 finish_reason: Some("stop".into()),
710 usage: Some(usage(5, 5)),
711 ..Default::default()
712 }
713 }
714
715 #[test]
716 fn streamed_stop_sequence_is_reported_and_withheld() {
717 let frames = run_stream(
718 vec!["<END>".into()],
719 vec![
720 text("one two "),
721 text("<END>"),
722 text(" three"),
723 finish_stop(),
724 ],
725 );
726 assert_eq!(streamed_text(&frames), "one two ");
727 assert!(
728 frames.contains(r#""stop_reason":"stop_sequence""#),
729 "{frames}"
730 );
731 assert!(frames.contains(r#""stop_sequence":"<END>""#), "{frames}");
732 }
733
734 #[test]
735 fn stop_sequence_split_across_deltas_is_still_caught() {
736 let frames = run_stream(
738 vec!["<END>".into()],
739 vec![
740 text("keep"),
741 text("<E"),
742 text("N"),
743 text("D> drop"),
744 finish_stop(),
745 ],
746 );
747 assert_eq!(streamed_text(&frames), "keep");
748 assert!(frames.contains(r#""stop_sequence":"<END>""#), "{frames}");
749 }
750
751 #[test]
752 fn withheld_tail_is_flushed_when_no_stop_fires() {
753 let frames = run_stream(
756 vec!["<END>".into()],
757 vec![text("all of it <EN"), finish_stop()],
758 );
759 assert_eq!(streamed_text(&frames), "all of it <EN");
760 assert!(frames.contains(r#""stop_reason":"end_turn""#), "{frames}");
761 }
762
763 #[test]
764 fn stop_sequence_in_streamed_thinking_is_caught() {
765 let th = |t: &str| CanonChunk {
766 thinking: Some(crate::canonical::ThinkingDelta {
767 kind: "thinking",
768 text: t.into(),
769 block_index: 0,
770 }),
771 ..Default::default()
772 };
773 let frames = run_stream(
774 vec!["FIVE".into()],
775 vec![th("ONE TWO "), th("FIVE SIX"), finish_stop()],
776 );
777 assert_eq!(streamed_text(&frames), "ONE TWO ");
778 assert!(frames.contains(r#""stop_sequence":"FIVE""#), "{frames}");
779 }
780
781 #[test]
782 fn no_stop_sequences_streams_byte_for_byte() {
783 let frames = run_stream(vec![], vec![text("a"), text("b"), text("c"), finish_stop()]);
784 assert_eq!(streamed_text(&frames), "abc");
785 assert!(frames.contains(r#""stop_sequence":null"#), "{frames}");
786 }
787
788 fn text(s: &str) -> CanonChunk {
789 CanonChunk {
790 delta_text: s.into(),
791 ..Default::default()
792 }
793 }
794
795 fn usage(p: u64, c: u64) -> Usage {
796 Usage {
797 prompt_tokens: p,
798 completion_tokens: c,
799 cached_read_tokens: 0,
800 cache_write_tokens: 0,
801 reasoning_tokens: None,
802 }
803 }
804
805 fn usage_with(p: u64, c: u64, cr: u64, cw: u64) -> Usage {
806 Usage {
807 prompt_tokens: p,
808 completion_tokens: c,
809 cached_read_tokens: cr,
810 cache_write_tokens: cw,
811 reasoning_tokens: None,
812 }
813 }
814
815 fn frames(chunks: Vec<CanonChunk>) -> Vec<(&'static str, String)> {
818 let mut st = StreamState::new();
819 let mut all = Vec::new();
820 for c in &chunks {
821 all.extend(chunk_to_sse_events(c, "route-alias", &mut st, "msg_x"));
822 }
823 all.extend(finalize_stream(&mut st));
824 all
825 }
826
827 fn types(all: &[(&'static str, String)]) -> Vec<&'static str> {
828 all.iter().map(|(e, _)| *e).collect()
829 }
830
831 fn data_of<'a>(all: &'a [(&'static str, String)], ev: &'static str) -> Vec<&'a str> {
832 all.iter()
833 .filter(|(e, _)| *e == ev)
834 .map(|(_, d)| d.as_str())
835 .collect()
836 }
837
838 fn starts_indices(all: &[(&'static str, String)]) -> Vec<i64> {
839 data_of(all, "content_block_start")
840 .iter()
841 .filter_map(|d| serde_json::from_str::<serde_json::Value>(d).unwrap()["index"].as_i64())
842 .collect()
843 }
844
845 #[test]
846 fn stream_chunks_emit_message_start_then_text() {
847 let mut st = StreamState::new();
848 let evs = chunk_to_sse_events(&text("Hi"), "translate-model", &mut st, "msg_1");
849 assert_eq!(evs[0].0, "message_start");
850 assert!(evs.iter().any(|(t, _)| *t == "content_block_delta"));
851 assert!(
852 !evs.iter().any(|(t, _)| *t == "ping"),
853 "ping is one-shot preamble noise; real Anthropic streams ping periodically, not here"
854 );
855
856 let evs2 = chunk_to_sse_events(
857 &CanonChunk {
858 finish_reason: Some("stop".into()),
859 usage: Some(usage(3, 1)),
860 ..text("")
861 },
862 "translate-model",
863 &mut st,
864 "msg_1",
865 );
866 let t = types(&evs2);
867 assert!(t.contains(&"message_delta"));
868 assert!(t.contains(&"message_stop"));
869 let delta = data_of(&evs2, "message_delta")[0];
870 assert!(delta.contains("end_turn"));
871 }
872
873 #[test]
874 fn message_start_carries_preamble_input_tokens() {
875 let mut st = StreamState::new();
880 let evs = chunk_to_sse_events(
881 &CanonChunk {
882 input_tokens: Some(42),
883 ..text("")
884 },
885 "m",
886 &mut st,
887 "msg_1",
888 );
889 assert_eq!(evs[0].0, "message_start");
890 let ms = serde_json::from_str::<serde_json::Value>(&evs[0].1).unwrap();
891 assert_eq!(
892 ms["message"]["usage"]["input_tokens"], 42,
893 "preamble must carry the upstream prompt count: {ms}"
894 );
895 }
896
897 #[test]
898 fn terminal_usage_reports_fresh_only_input_tokens() {
899 let all = frames(vec![
902 text("hi"),
903 CanonChunk {
904 finish_reason: Some("stop".into()),
905 usage: Some(usage_with(18204, 7, 18000, 200)),
906 ..text("")
907 },
908 ]);
909 let md =
910 serde_json::from_str::<serde_json::Value>(data_of(&all, "message_delta")[0]).unwrap();
911 assert_eq!(md["usage"]["input_tokens"], 4);
912 assert_eq!(md["usage"]["output_tokens"], 7);
913 assert_eq!(md["usage"]["cache_read_input_tokens"], 18000);
914 assert_eq!(md["usage"]["cache_creation_input_tokens"], 200);
915 }
916
917 #[test]
918 fn anthropic_upstream_tool_stream_is_well_formed() {
919 let all = frames(vec![
923 text("checking"),
924 CanonChunk {
925 tool_calls: Some(serde_json::json!([
926 {"index":0,"id":"toolu_1","function":{"name":"bash","arguments":""}}
927 ])),
928 ..text("")
929 },
930 CanonChunk {
931 tool_calls: Some(serde_json::json!([
932 {"index":0,"function":{"arguments":"{}"}}
933 ])),
934 ..text("")
935 },
936 CanonChunk {
937 finish_reason: Some("tool_calls".into()),
938 usage: Some(usage(10, 4)),
939 ..text("")
940 },
941 ]);
942 let deltas = data_of(&all, "message_delta");
943 assert_eq!(deltas.len(), 1, "exactly one message_delta: {deltas:?}");
944 assert!(deltas[0].contains("tool_use"));
945 assert!(deltas[0].contains("\"input_tokens\":10"));
946 let stops = data_of(&all, "content_block_stop");
947 assert_eq!(
948 stops.len(),
949 2,
950 "text block 0 + tool block 1 each closed once"
951 );
952 assert_eq!(data_of(&all, "message_stop").len(), 1);
953 }
954
955 #[test]
956 fn reopened_block_gets_fresh_index_when_closed() {
957 let all = frames(vec![
960 CanonChunk {
961 thinking: Some(ThinkingDelta {
962 block_index: 0,
963 kind: "thinking",
964 text: "h1".into(),
965 }),
966 ..text("")
967 },
968 text("t"),
969 CanonChunk {
970 thinking: Some(ThinkingDelta {
971 block_index: 0,
972 kind: "thinking",
973 text: "h2".into(),
974 }),
975 ..text("")
976 },
977 ]);
978 let indices = starts_indices(&all);
979 assert_eq!(
980 indices,
981 vec![0, 1, 2],
982 "blocks must never reuse an index: {indices:?}"
983 );
984 let mut open: std::collections::HashSet<i64> = Default::default();
986 let mut seen_started: std::collections::HashSet<i64> = Default::default();
987 for (ev, d) in &all {
988 let v: serde_json::Value = serde_json::from_str(d).unwrap();
989 let idx = v["index"].as_i64();
990 match *ev {
991 "content_block_start" => {
992 if let Some(i) = idx {
993 assert!(seen_started.insert(i), "block {i} started twice");
994 open.insert(i);
995 }
996 }
997 "content_block_stop" => {
998 if let Some(i) = idx {
999 open.remove(&i);
1000 }
1001 }
1002 "content_block_delta" => {
1003 if let Some(i) = idx {
1004 assert!(open.contains(&i), "delta on non-open block {i}");
1005 }
1006 }
1007 _ => {}
1008 }
1009 }
1010 }
1011
1012 #[test]
1013 fn thinking_delta_streams_with_own_block_index() {
1014 let mut st = StreamState::new();
1015 let mut events: Vec<String> = Vec::new();
1016 for c in [
1017 CanonChunk {
1018 thinking: Some(ThinkingDelta {
1019 block_index: 0,
1020 kind: "thinking",
1021 text: "let me".into(),
1022 }),
1023 ..text("")
1024 },
1025 CanonChunk {
1026 thinking: Some(ThinkingDelta {
1027 block_index: 0,
1028 kind: "thinking",
1029 text: " think".into(),
1030 }),
1031 ..text("")
1032 },
1033 CanonChunk {
1034 thinking: Some(ThinkingDelta {
1035 block_index: 0,
1036 kind: "signature",
1037 text: "sig123".into(),
1038 }),
1039 ..text("")
1040 },
1041 text("answer"),
1042 CanonChunk {
1043 finish_reason: Some("stop".into()),
1044 usage: Some(usage(5, 3)),
1045 ..text("")
1046 },
1047 ] {
1048 for (ev, d) in chunk_to_sse_events(&c, "m", &mut st, "msg_1") {
1049 events.push(format!("{ev}: {d}"));
1050 }
1051 }
1052 let joined = events.join("\n");
1053 assert!(
1054 joined.contains("thinking_delta"),
1055 "missing thinking_delta: {joined}"
1056 );
1057 assert!(
1058 joined.contains("signature_delta"),
1059 "missing signature_delta"
1060 );
1061 assert!(
1062 joined.contains("\"index\":0"),
1063 "thinking block should be index 0"
1064 );
1065 assert!(joined.contains("\"index\":1") && joined.contains("\"type\":\"text\""));
1067 }
1068
1069 #[test]
1070 fn usage_every_chunk_does_not_double_stop() {
1071 let mut st = StreamState::new();
1075 let mut all: Vec<(&'static str, String)> = Vec::new();
1076 for _ in 0..2 {
1077 all.extend(chunk_to_sse_events(
1078 &CanonChunk {
1079 usage: Some(usage(5, 2)),
1080 ..text("")
1081 },
1082 "m",
1083 &mut st,
1084 "msg_1",
1085 ));
1086 }
1087 assert!(!types(&all).contains(&"message_stop"));
1089 assert!(!st.message_stopped);
1090 all.extend(chunk_to_sse_events(
1092 &CanonChunk {
1093 finish_reason: Some("stop".into()),
1094 ..text("")
1095 },
1096 "m",
1097 &mut st,
1098 "msg_1",
1099 ));
1100 all.extend(finalize_stream(&mut st));
1101 assert_eq!(data_of(&all, "message_stop").len(), 1);
1102 assert_eq!(data_of(&all, "message_delta").len(), 1);
1103 }
1104
1105 #[test]
1106 fn trailer_usage_carries_input_and_cache_tokens() {
1107 let mut st = StreamState::new();
1112 let mut evs = chunk_to_sse_events(&text("hi"), "m", &mut st, "msg_1");
1113 evs.extend(chunk_to_sse_events(
1114 &CanonChunk {
1115 finish_reason: Some("stop".into()),
1116 ..text("")
1117 },
1118 "m",
1119 &mut st,
1120 "msg_1",
1121 ));
1122 evs.extend(chunk_to_sse_events(
1123 &CanonChunk {
1124 usage: Some(usage_with(151, 7, 100, 9)),
1125 ..text("")
1126 },
1127 "m",
1128 &mut st,
1129 "msg_1",
1130 ));
1131 let md = serde_json::from_str::<serde_json::Value>(
1132 evs.iter()
1133 .find(|(e, _)| *e == "message_delta")
1134 .map(|(_, d)| d.as_str())
1135 .unwrap(),
1136 )
1137 .unwrap();
1138 assert_eq!(md["usage"]["input_tokens"], 42);
1139 assert_eq!(md["usage"]["output_tokens"], 7);
1140 assert_eq!(md["usage"]["cache_read_input_tokens"], 100);
1141 assert_eq!(md["usage"]["cache_creation_input_tokens"], 9);
1142 }
1143
1144 #[test]
1147 fn repro_anthropic_upstream_tool_use() {
1148 let all = frames(vec![
1149 text("Let me check"),
1150 CanonChunk {
1151 tool_calls: Some(serde_json::json!([
1152 {"index":0,"id":"toolu_1","type":"function","function":{"name":"get_weather","arguments":""}}
1153 ])),
1154 ..text("")
1155 },
1156 CanonChunk {
1157 tool_calls: Some(serde_json::json!([
1158 {"index":0,"function":{"arguments":"{\"city\":\"Rome\"}"}}
1159 ])),
1160 ..text("")
1161 },
1162 CanonChunk {
1164 finish_reason: Some("tool_calls".into()),
1165 usage: Some(usage(100, 20)),
1166 ..text("")
1167 },
1168 ]);
1169 let deltas = data_of(&all, "message_delta");
1170 assert_eq!(deltas.len(), 1);
1171 assert!(
1172 deltas[0].contains("\"stop_reason\":\"tool_use\""),
1173 "{deltas:?}"
1174 );
1175 assert!(deltas[0].contains("\"input_tokens\":100"));
1176 assert_eq!(data_of(&all, "message_stop").len(), 1);
1177 let stops = data_of(&all, "content_block_stop");
1178 assert_eq!(stops.len(), 2, "text + tool blocks closed once each");
1179 }
1180
1181 #[test]
1182 fn repro_openai_upstream() {
1183 let all = frames(vec![
1186 text("Hi"),
1187 text(" there"),
1188 CanonChunk {
1189 finish_reason: Some("stop".into()),
1190 ..text("")
1191 },
1192 CanonChunk {
1193 usage: Some(usage(5, 2)),
1194 ..text("")
1195 },
1196 ]);
1197 let deltas = data_of(&all, "message_delta");
1198 assert_eq!(deltas.len(), 1, "{deltas:?}");
1199 assert!(
1200 deltas[0].contains("\"stop_reason\":\"end_turn\""),
1201 "{deltas:?}"
1202 );
1203 assert!(deltas[0].contains("\"input_tokens\":5"));
1204 assert!(deltas[0].contains("\"output_tokens\":2"));
1205 assert_eq!(data_of(&all, "message_stop").len(), 1);
1206 }
1207
1208 #[test]
1209 fn repro_gemini_upstream_usage_every_chunk() {
1210 let all = frames(vec![
1213 CanonChunk {
1214 usage: Some(usage(5, 2)),
1215 ..text("Hello")
1216 },
1217 CanonChunk {
1218 usage: Some(usage(5, 2)),
1219 ..text(" world")
1220 },
1221 CanonChunk {
1222 usage: Some(usage(5, 2)),
1223 finish_reason: Some("stop".into()),
1224 ..text("!")
1225 },
1226 ]);
1227 assert_eq!(data_of(&all, "message_delta").len(), 1);
1228 assert_eq!(data_of(&all, "message_stop").len(), 1);
1229 let text_deltas = data_of(&all, "content_block_delta");
1230 assert_eq!(text_deltas.len(), 3, "{text_deltas:?}");
1231 }
1232
1233 #[test]
1235 fn pure_tool_turn_indices() {
1236 let mut st = StreamState::new();
1237 let chunks = [
1238 CanonChunk {
1239 thinking: Some(ThinkingDelta {
1240 block_index: 0,
1241 kind: "thinking",
1242 text: "let me check".into(),
1243 }),
1244 ..text("")
1245 },
1246 CanonChunk {
1247 tool_calls: Some(serde_json::json!([
1248 {"index":0,"id":"call_1","type":"function","function":{"name":"Bash","arguments":""}}
1249 ])),
1250 ..text("")
1251 },
1252 CanonChunk {
1253 tool_calls: Some(serde_json::json!([
1254 {"index":0,"function":{"arguments":"{\"command\":\"ls\"}"}}
1255 ])),
1256 ..text("")
1257 },
1258 CanonChunk {
1259 finish_reason: Some("tool_calls".into()),
1260 usage: Some(usage(100, 30)),
1261 ..text("")
1262 },
1263 ];
1264 let mut all: Vec<(&'static str, String)> = Vec::new();
1265 for c in chunks {
1266 all.extend(chunk_to_sse_events(&c, "m", &mut st, "msg_1"));
1267 }
1268 all.extend(finalize_stream(&mut st));
1269 assert_eq!(starts_indices(&all), vec![0, 1]);
1271 let stops: Vec<usize> = data_of(&all, "content_block_stop")
1272 .iter()
1273 .filter_map(|d| {
1274 serde_json::from_str::<serde_json::Value>(d).unwrap()["index"]
1275 .as_u64()
1276 .map(|x| x as usize)
1277 })
1278 .collect();
1279 assert_eq!(stops, vec![0, 1]);
1280 let joined = show(&all);
1281 assert!(joined.contains("\"stop_reason\":\"tool_use\""));
1282 assert_eq!(data_of(&all, "message_delta").len(), 1);
1283 assert_eq!(data_of(&all, "message_stop").len(), 1);
1284 }
1285
1286 #[test]
1287 fn mixed_text_plus_tool_in_one_upstream_chunk() {
1288 let mut st = StreamState::new();
1289 let c = CanonChunk {
1291 delta_text: "Checking the code".into(),
1292 tool_calls: Some(serde_json::json!([
1293 {"index":0,"id":"call_1","type":"function","function":{"name":"get_weather","arguments":"{\"city\":\"Paris\"}"}}
1294 ])),
1295 ..Default::default()
1296 };
1297 let events = chunk_to_sse_events(&c, "m", &mut st, "msg_1");
1298 assert_eq!(
1299 starts_indices(&events),
1300 vec![0, 1],
1301 "text block 0 then tool block 1, exactly once"
1302 );
1303 }
1304
1305 #[test]
1306 fn a_text_tool_text() {
1307 let all = frames(vec![
1310 text("Let me check."),
1311 CanonChunk {
1312 tool_calls: Some(serde_json::json!([
1313 {"index":0,"id":"call_1","type":"function","function":{"name":"Bash","arguments":"{}"}}
1314 ])),
1315 ..text("")
1316 },
1317 text(" Done."),
1318 CanonChunk {
1319 finish_reason: Some("tool_calls".into()),
1320 usage: Some(usage(10, 4)),
1321 ..text("")
1322 },
1323 ]);
1324 assert_eq!(starts_indices(&all), vec![0, 1, 2], "{:?}", show(&all));
1325 assert_eq!(data_of(&all, "message_stop").len(), 1);
1326 }
1327
1328 #[test]
1329 fn b_tool_then_text() {
1330 let all = frames(vec![
1332 CanonChunk {
1333 tool_calls: Some(serde_json::json!([
1334 {"index":0,"id":"call_1","type":"function","function":{"name":"Bash","arguments":"{}"}}
1335 ])),
1336 ..text("")
1337 },
1338 text("trailing prose"),
1339 CanonChunk {
1340 finish_reason: Some("tool_calls".into()),
1341 usage: Some(usage(10, 4)),
1342 ..text("")
1343 },
1344 ]);
1345 assert_eq!(starts_indices(&all), vec![0, 1], "{:?}", show(&all));
1346 assert_eq!(data_of(&all, "message_stop").len(), 1);
1347 }
1348
1349 #[test]
1350 fn c_text_then_reasoning() {
1351 let all = frames(vec![
1354 text("visible"),
1355 CanonChunk {
1356 thinking: Some(ThinkingDelta {
1357 block_index: 0,
1358 kind: "thinking",
1359 text: "hidden".into(),
1360 }),
1361 ..text("")
1362 },
1363 text("more"),
1364 CanonChunk {
1365 finish_reason: Some("stop".into()),
1366 usage: Some(usage(1, 1)),
1367 ..text("")
1368 },
1369 ]);
1370 assert_eq!(starts_indices(&all), vec![0, 1, 2], "{:?}", show(&all));
1371 assert_eq!(data_of(&all, "message_stop").len(), 1);
1372 }
1373
1374 fn show(all: &[(&'static str, String)]) -> String {
1375 all.iter()
1376 .map(|(e, d)| format!("event: {e}\ndata: {d}"))
1377 .collect::<Vec<_>>()
1378 .join("\n\n")
1379 }
1380
1381 #[tokio::test]
1386 async fn midstream_error_terminates_with_error_frame() {
1387 use crate::error::ProxyError;
1388 let chunks: Vec<Result<CanonChunk, ProxyError>> = vec![
1389 Ok(text("partial")),
1390 Err(ProxyError::upstream(502, "secret upstream body".into())),
1391 ];
1392 let resp = anthropic_stream_response(
1393 Box::pin(futures::stream::iter(chunks)),
1394 "m".into(),
1395 "msg_1".into(),
1396 vec![],
1397 );
1398 let bytes = axum::body::to_bytes(resp.into_body(), 1 << 20)
1399 .await
1400 .unwrap();
1401 let s = String::from_utf8(bytes.to_vec()).unwrap();
1402 assert_eq!(
1403 s.matches("event: error").count(),
1404 1,
1405 "exactly one error frame: {s}"
1406 );
1407 let err = s
1408 .split("\n\n")
1409 .find(|f| f.starts_with("event: error"))
1410 .unwrap();
1411 assert!(err.contains("\"type\":\""), "anthropic error shape: {err}");
1412 assert!(!s.contains("secret upstream body"), "body leaked: {s}");
1414 assert_eq!(
1415 s.matches("event: message_stop").count(),
1416 0,
1417 "message_stop after an error frame: {s}"
1418 );
1419 assert!(
1421 s.find("text_delta").expect("no deltas") < s.find("event: error").unwrap(),
1422 "deltas must precede the error frame: {s}"
1423 );
1424 }
1425}