1use std::collections::HashMap;
2
3use crate::anthropic::sse::encode_sse_event;
4use crate::traffic::TrafficCapture;
5
6use super::read_rewrite::sanitize_read_args;
7use super::reducer::{
8 CodexUsage, STOP_END_TURN, STOP_MAX_TOKENS, STOP_TOOL_USE, map_codex_usage_to_anthropic,
9};
10
11const BUFFERED_READ_REPAIR_TRAILING_WHITESPACE_BYTES: usize = 1_024;
12const BUFFERED_TOOL_MAX_ARGS_BYTES: usize = 5_000_000;
13
14enum LiveBlock {
15 Text {
16 index: usize,
17 text: String,
18 deferred: bool,
19 },
20 Tool {
21 index: usize,
22 call_id: String,
23 name: String,
24 args_accum: String,
25 had_delta: bool,
26 buffer_until_done: bool,
27 emitted_args: bool,
28 },
29}
30
31struct LiveWebSearch {
32 index: usize,
33 result_index: usize,
34 id: String,
35 query: String,
36}
37
38#[derive(Clone)]
39struct LiveWebSearchResult {
40 title: String,
41 url: String,
42}
43
44pub struct LiveStreamTranslator {
45 message_id: String,
46 model: String,
47 message_started: bool,
48 blocks_by_output_index: HashMap<usize, LiveBlock>,
49 item_id_to_output_index: HashMap<String, usize>,
50 anthropic_index: usize,
51 thinking_index: Option<usize>,
52 saw_tool_use: bool,
53 web_search_requests: usize,
54 web_searches: Vec<LiveWebSearch>,
55 web_search_results: Vec<LiveWebSearchResult>,
56 deferred_text: Vec<(usize, String)>,
57 finished: bool,
58}
59
60impl LiveStreamTranslator {
61 pub fn new(message_id: impl Into<String>, model: impl Into<String>) -> Self {
62 Self {
63 message_id: message_id.into(),
64 model: model.into(),
65 message_started: false,
66 blocks_by_output_index: HashMap::new(),
67 item_id_to_output_index: HashMap::new(),
68 anthropic_index: 0,
69 thinking_index: None,
70 saw_tool_use: false,
71 web_search_requests: 0,
72 web_searches: Vec::new(),
73 web_search_results: Vec::new(),
74 deferred_text: Vec::new(),
75 finished: false,
76 }
77 }
78
79 pub fn accept(
80 &mut self,
81 payload: &serde_json::Value,
82 traffic: Option<&TrafficCapture>,
83 ) -> Result<Vec<u8>, String> {
84 if self.finished {
85 return Ok(Vec::new());
86 }
87
88 let kind = payload.get("type").and_then(|v| v.as_str()).unwrap_or("");
89 let mut out = Vec::new();
90
91 match kind {
92 "codex.rate_limits" => {
93 if payload
94 .get("rate_limits")
95 .and_then(|r| r.get("limit_reached"))
96 .and_then(|v| v.as_bool())
97 == Some(true)
98 {
99 return Err("rate limit reached".to_string());
100 }
101 }
102 "keepalive" => {}
103 "response.failed" | "response.error" | "error" => {
104 return Err(error_message(payload));
105 }
106 "response.web_search_call.in_progress"
107 | "response.web_search_call.searching"
108 | "response.web_search_call.completed" => {}
109 "response.output_item.added" => {
110 self.output_item_added(payload, traffic, &mut out);
111 }
112 "response.reasoning_summary_part.added" => {
113 if let Some(index) = self.thinking_index {
114 self.emit(
115 traffic,
116 &mut out,
117 "content_block_delta",
118 &serde_json::json!({
119 "type": "content_block_delta",
120 "index": index,
121 "delta": {"type": "thinking_delta", "thinking": "\n\n"}
122 }),
123 );
124 }
125 }
126 "response.reasoning_summary_text.delta" => {
127 self.reasoning_delta(payload, traffic, &mut out);
128 }
129 "response.output_text.delta" => {
130 self.text_delta(payload, traffic, &mut out);
131 }
132 "response.output_text.annotation.added" => {
133 self.web_search_annotation(payload);
134 }
135 "response.function_call_arguments.delta" => {
136 self.tool_delta(payload, traffic, &mut out)?;
137 }
138 "response.function_call_arguments.done" => {
139 self.tool_arguments_done(payload);
140 }
141 "response.output_item.done" => {
142 self.output_item_done(payload, traffic, &mut out);
143 }
144 "response.completed" | "response.incomplete" | "response.done" => {
145 self.finish(payload, traffic, &mut out);
146 }
147 _ => {}
148 }
149
150 Ok(out)
151 }
152
153 pub fn is_finished(&self) -> bool {
154 self.finished
155 }
156
157 pub fn finish_after_closed_completed_tool_call(
158 &mut self,
159 traffic: Option<&TrafficCapture>,
160 ) -> Vec<u8> {
161 let mut out = Vec::new();
162 if self.finished || !self.saw_tool_use || !self.blocks_by_output_index.is_empty() {
163 return out;
164 }
165 self.close_thinking(traffic, &mut out);
166 self.ensure_message_start(traffic, &mut out);
167 self.emit_finish(STOP_TOOL_USE, None, traffic, &mut out);
168 out
169 }
170
171 pub fn error_chunk(
172 &mut self,
173 message: &str,
174 error_type: &str,
175 traffic: Option<&TrafficCapture>,
176 ) -> Vec<u8> {
177 let mut out = Vec::new();
178 if self.finished {
179 return out;
180 }
181 self.close_open_blocks(traffic, &mut out);
182 self.ensure_message_start(traffic, &mut out);
183 self.emit(
184 traffic,
185 &mut out,
186 "error",
187 &serde_json::json!({
188 "type": "error",
189 "error": {
190 "type": error_type,
191 "message": message,
192 }
193 }),
194 );
195 self.finished = true;
196 out
197 }
198
199 fn ensure_message_start(&mut self, traffic: Option<&TrafficCapture>, out: &mut Vec<u8>) {
200 if self.message_started {
201 return;
202 }
203 self.message_started = true;
204 self.emit(
205 traffic,
206 out,
207 "message_start",
208 &serde_json::json!({
209 "type": "message_start",
210 "message": {
211 "id": self.message_id,
212 "type": "message",
213 "role": "assistant",
214 "model": self.model,
215 "content": [],
216 "stop_reason": null,
217 "stop_sequence": null,
218 "usage": {
219 "input_tokens": 0,
220 "output_tokens": 0
221 }
222 }
223 }),
224 );
225 }
226
227 fn emit(
228 &self,
229 traffic: Option<&TrafficCapture>,
230 out: &mut Vec<u8>,
231 event: &str,
232 data: &serde_json::Value,
233 ) {
234 if let Some(traffic) = traffic {
235 traffic.write_json_event(
236 "050-downstream-event",
237 &serde_json::json!({
238 "event": event,
239 "data": data,
240 }),
241 );
242 }
243 out.extend_from_slice(&encode_sse_event(Some(event), &data.to_string()));
244 }
245
246 fn output_item_added(
247 &mut self,
248 payload: &serde_json::Value,
249 traffic: Option<&TrafficCapture>,
250 out: &mut Vec<u8>,
251 ) {
252 let Some(item) = payload.get("item") else {
253 return;
254 };
255 let output_index = output_index(payload);
256 let item_type = item.get("type").and_then(|v| v.as_str()).unwrap_or("");
257
258 match item_type {
259 "message" => {
260 self.close_thinking(traffic, out);
261 let index = self.anthropic_index;
262 self.anthropic_index += 1;
263 if let Some(id) = item.get("id").and_then(|v| v.as_str()) {
264 self.item_id_to_output_index
265 .insert(id.to_string(), output_index);
266 }
267 let deferred = !self.web_searches.is_empty();
268 self.blocks_by_output_index.insert(
269 output_index,
270 LiveBlock::Text {
271 index,
272 text: String::new(),
273 deferred,
274 },
275 );
276 if !deferred {
277 self.ensure_message_start(traffic, out);
278 self.emit(
279 traffic,
280 out,
281 "content_block_start",
282 &serde_json::json!({
283 "type": "content_block_start",
284 "index": index,
285 "content_block": {"type": "text", "text": ""}
286 }),
287 );
288 }
289 }
290 "function_call" => {
291 self.close_thinking(traffic, out);
292 self.saw_tool_use = true;
293 let index = self.anthropic_index;
294 self.anthropic_index += 1;
295 let call_id = item
296 .get("call_id")
297 .and_then(|v| v.as_str())
298 .unwrap_or("")
299 .to_string();
300 let name = item
301 .get("name")
302 .and_then(|v| v.as_str())
303 .unwrap_or("")
304 .to_string();
305 self.blocks_by_output_index.insert(
306 output_index,
307 LiveBlock::Tool {
308 index,
309 call_id: call_id.clone(),
310 name: name.clone(),
311 args_accum: String::new(),
312 had_delta: false,
313 buffer_until_done: name == "Read",
314 emitted_args: false,
315 },
316 );
317 self.ensure_message_start(traffic, out);
318 self.emit(
319 traffic,
320 out,
321 "content_block_start",
322 &serde_json::json!({
323 "type": "content_block_start",
324 "index": index,
325 "content_block": {
326 "type": "tool_use",
327 "id": call_id,
328 "name": name,
329 "input": {}
330 }
331 }),
332 );
333 }
334 "web_search_call" => {
335 self.web_search_requests += 1;
336 }
337 _ => {}
338 }
339 }
340
341 fn reasoning_delta(
342 &mut self,
343 payload: &serde_json::Value,
344 traffic: Option<&TrafficCapture>,
345 out: &mut Vec<u8>,
346 ) {
347 let delta = payload.get("delta").and_then(|v| v.as_str()).unwrap_or("");
348 if delta.is_empty() {
349 return;
350 }
351 if self.thinking_index.is_none() {
352 let index = self.anthropic_index;
353 self.anthropic_index += 1;
354 self.thinking_index = Some(index);
355 self.ensure_message_start(traffic, out);
356 self.emit(
357 traffic,
358 out,
359 "content_block_start",
360 &serde_json::json!({
361 "type": "content_block_start",
362 "index": index,
363 "content_block": {"type": "thinking", "thinking": "", "signature": ""}
364 }),
365 );
366 }
367 let index = self.thinking_index.unwrap();
368 self.emit(
369 traffic,
370 out,
371 "content_block_delta",
372 &serde_json::json!({
373 "type": "content_block_delta",
374 "index": index,
375 "delta": {"type": "thinking_delta", "thinking": delta}
376 }),
377 );
378 }
379
380 fn text_delta(
381 &mut self,
382 payload: &serde_json::Value,
383 traffic: Option<&TrafficCapture>,
384 out: &mut Vec<u8>,
385 ) {
386 self.close_thinking(traffic, out);
387 let delta = payload.get("delta").and_then(|v| v.as_str()).unwrap_or("");
388 if delta.is_empty() {
389 return;
390 }
391
392 let output_index = payload
393 .get("output_index")
394 .and_then(|v| v.as_u64())
395 .map(|v| v as usize)
396 .or_else(|| {
397 payload
398 .get("item_id")
399 .and_then(|v| v.as_str())
400 .and_then(|id| self.item_id_to_output_index.get(id).copied())
401 })
402 .unwrap_or(0);
403
404 if !self.blocks_by_output_index.contains_key(&output_index) {
405 let index = self.anthropic_index;
406 self.anthropic_index += 1;
407 let deferred = !self.web_searches.is_empty();
408 self.blocks_by_output_index.insert(
409 output_index,
410 LiveBlock::Text {
411 index,
412 text: String::new(),
413 deferred,
414 },
415 );
416 if !deferred {
417 self.ensure_message_start(traffic, out);
418 self.emit(
419 traffic,
420 out,
421 "content_block_start",
422 &serde_json::json!({
423 "type": "content_block_start",
424 "index": index,
425 "content_block": {"type": "text", "text": ""}
426 }),
427 );
428 }
429 }
430
431 let Some(LiveBlock::Text {
432 index,
433 text,
434 deferred,
435 }) = self.blocks_by_output_index.get_mut(&output_index)
436 else {
437 return;
438 };
439 text.push_str(delta);
440 if *deferred {
441 return;
442 }
443 let index = *index;
444 self.emit(
445 traffic,
446 out,
447 "content_block_delta",
448 &serde_json::json!({
449 "type": "content_block_delta",
450 "index": index,
451 "delta": {"type": "text_delta", "text": delta}
452 }),
453 );
454 }
455
456 fn tool_delta(
457 &mut self,
458 payload: &serde_json::Value,
459 traffic: Option<&TrafficCapture>,
460 out: &mut Vec<u8>,
461 ) -> Result<(), String> {
462 let Some(output_index) = payload
463 .get("output_index")
464 .and_then(|v| v.as_u64())
465 .map(|v| v as usize)
466 else {
467 return Ok(());
468 };
469 let delta = payload.get("delta").and_then(|v| v.as_str()).unwrap_or("");
470 if delta.is_empty() {
471 return Ok(());
472 }
473 let mut repaired_read: Option<(usize, String)> = None;
474 let Some(LiveBlock::Tool {
475 index,
476 call_id,
477 name,
478 args_accum,
479 had_delta,
480 buffer_until_done,
481 emitted_args,
482 ..
483 }) = self.blocks_by_output_index.get_mut(&output_index)
484 else {
485 return Ok(());
486 };
487 args_accum.push_str(delta);
488 *had_delta = true;
489 if *buffer_until_done {
490 if args_accum.len() > BUFFERED_TOOL_MAX_ARGS_BYTES {
491 return Err(format!(
492 "Buffered {name} tool arguments exceeded safe limits"
493 ));
494 }
495 if let Some(repaired) =
496 repair_whitespace_stalled_read_args(name, args_accum, Some(call_id.as_str()))
497 {
498 *args_accum = repaired.clone();
499 *emitted_args = true;
500 repaired_read = Some((*index, repaired));
501 }
502 } else {
503 *emitted_args = true;
504 let index = *index;
505 self.emit(
506 traffic,
507 out,
508 "content_block_delta",
509 &serde_json::json!({
510 "type": "content_block_delta",
511 "index": index,
512 "delta": {
513 "type": "input_json_delta",
514 "partial_json": delta
515 }
516 }),
517 );
518 return Ok(());
519 }
520 if let Some((index, repaired)) = repaired_read {
521 self.blocks_by_output_index.remove(&output_index);
522 self.emit(
523 traffic,
524 out,
525 "content_block_delta",
526 &serde_json::json!({
527 "type": "content_block_delta",
528 "index": index,
529 "delta": {
530 "type": "input_json_delta",
531 "partial_json": repaired
532 }
533 }),
534 );
535 self.emit(
536 traffic,
537 out,
538 "content_block_stop",
539 &serde_json::json!({
540 "type": "content_block_stop",
541 "index": index,
542 }),
543 );
544 self.ensure_message_start(traffic, out);
545 self.emit_finish(STOP_TOOL_USE, None, traffic, out);
546 }
547 Ok(())
548 }
549
550 fn tool_arguments_done(&mut self, payload: &serde_json::Value) {
551 let Some(output_index) = payload
552 .get("output_index")
553 .and_then(|v| v.as_u64())
554 .map(|v| v as usize)
555 else {
556 return;
557 };
558 let Some(args) = payload.get("arguments").and_then(|v| v.as_str()) else {
559 return;
560 };
561 let Some(LiveBlock::Tool { args_accum, .. }) =
562 self.blocks_by_output_index.get_mut(&output_index)
563 else {
564 return;
565 };
566 if args_accum.is_empty() {
567 *args_accum = args.to_string();
568 }
569 }
570
571 fn output_item_done(
572 &mut self,
573 payload: &serde_json::Value,
574 traffic: Option<&TrafficCapture>,
575 out: &mut Vec<u8>,
576 ) {
577 let output_index = output_index(payload);
578 if payload
579 .get("item")
580 .and_then(|item| item.get("type"))
581 .and_then(|v| v.as_str())
582 == Some("reasoning")
583 {
584 self.close_thinking(traffic, out);
585 return;
586 }
587
588 if payload
589 .get("item")
590 .and_then(|item| item.get("type"))
591 .and_then(|v| v.as_str())
592 == Some("web_search_call")
593 {
594 self.close_thinking(traffic, out);
595 let item = &payload["item"];
596 let index = self.anthropic_index;
597 self.anthropic_index += 1;
598 let result_index = self.anthropic_index;
599 self.anthropic_index += 1;
600 let raw_id = item.get("id").and_then(|v| v.as_str()).unwrap_or("");
601 self.web_searches.push(LiveWebSearch {
602 index,
603 result_index,
604 id: super::web_search_compat::server_tool_use_id_from_codex_web_search_id(raw_id),
605 query: web_search_query(item),
606 });
607 return;
608 }
609
610 let Some(mut state) = self.blocks_by_output_index.remove(&output_index) else {
611 return;
612 };
613
614 match &mut state {
615 LiveBlock::Text {
616 index,
617 text,
618 deferred,
619 } => {
620 if *deferred {
621 self.deferred_text.push((*index, std::mem::take(text)));
622 } else {
623 self.emit(
624 traffic,
625 out,
626 "content_block_stop",
627 &serde_json::json!({
628 "type": "content_block_stop",
629 "index": index,
630 }),
631 );
632 }
633 }
634 LiveBlock::Tool {
635 index,
636 name,
637 call_id,
638 args_accum,
639 had_delta,
640 buffer_until_done,
641 emitted_args,
642 ..
643 } => {
644 if let Some(final_args) = payload
645 .get("item")
646 .and_then(|item| item.get("arguments"))
647 .and_then(|v| v.as_str())
648 .filter(|s| !s.is_empty())
649 && (args_accum.is_empty() || (!*had_delta && !*emitted_args))
650 {
651 *args_accum = final_args.to_string();
652 }
653 if !args_accum.is_empty() {
654 *args_accum = sanitize_read_args(name, args_accum, Some(call_id.as_str()));
655 if *buffer_until_done || !*emitted_args {
656 *emitted_args = true;
657 self.emit(
658 traffic,
659 out,
660 "content_block_delta",
661 &serde_json::json!({
662 "type": "content_block_delta",
663 "index": index,
664 "delta": {
665 "type": "input_json_delta",
666 "partial_json": args_accum
667 }
668 }),
669 );
670 }
671 }
672 self.emit(
673 traffic,
674 out,
675 "content_block_stop",
676 &serde_json::json!({
677 "type": "content_block_stop",
678 "index": index,
679 }),
680 );
681 }
682 }
683 }
684
685 fn web_search_annotation(&mut self, payload: &serde_json::Value) {
686 let Some(annotation) = payload.get("annotation") else {
687 return;
688 };
689 if annotation.get("type").and_then(|v| v.as_str()) != Some("url_citation") {
690 return;
691 }
692 let Some(url) = annotation.get("url").and_then(|v| v.as_str()) else {
693 return;
694 };
695 if self
696 .web_search_results
697 .iter()
698 .any(|result| result.url == url)
699 {
700 return;
701 }
702 let title = annotation
703 .get("title")
704 .and_then(|v| v.as_str())
705 .filter(|title| !title.is_empty())
706 .unwrap_or(url);
707 self.web_search_results.push(LiveWebSearchResult {
708 title: title.to_string(),
709 url: url.to_string(),
710 });
711 }
712
713 fn emit_web_searches(&mut self, traffic: Option<&TrafficCapture>, out: &mut Vec<u8>) {
714 let searches = std::mem::take(&mut self.web_searches);
715 for search in searches {
716 self.ensure_message_start(traffic, out);
717 self.emit(
718 traffic,
719 out,
720 "content_block_start",
721 &serde_json::json!({
722 "type": "content_block_start",
723 "index": search.index,
724 "content_block": {
725 "type": "server_tool_use",
726 "id": search.id,
727 "name": "web_search",
728 "input": {}
729 }
730 }),
731 );
732 self.emit(
733 traffic,
734 out,
735 "content_block_delta",
736 &serde_json::json!({
737 "type": "content_block_delta",
738 "index": search.index,
739 "delta": {
740 "type": "input_json_delta",
741 "partial_json": serde_json::to_string(&serde_json::json!({"query": search.query})).unwrap_or_default()
742 }
743 }),
744 );
745 self.emit(
746 traffic,
747 out,
748 "content_block_stop",
749 &serde_json::json!({"type": "content_block_stop", "index": search.index}),
750 );
751 let results: Vec<_> = self
752 .web_search_results
753 .iter()
754 .map(|result| {
755 serde_json::json!({
756 "type": "web_search_result",
757 "title": result.title,
758 "url": result.url,
759 })
760 })
761 .collect();
762 self.emit(
763 traffic,
764 out,
765 "content_block_start",
766 &serde_json::json!({
767 "type": "content_block_start",
768 "index": search.result_index,
769 "content_block": {
770 "type": "web_search_tool_result",
771 "tool_use_id": search.id,
772 "content": results
773 }
774 }),
775 );
776 self.emit(
777 traffic,
778 out,
779 "content_block_stop",
780 &serde_json::json!({"type": "content_block_stop", "index": search.result_index}),
781 );
782 }
783
784 for (index, text) in std::mem::take(&mut self.deferred_text) {
785 self.emit(
786 traffic,
787 out,
788 "content_block_start",
789 &serde_json::json!({
790 "type": "content_block_start",
791 "index": index,
792 "content_block": {"type": "text", "text": ""}
793 }),
794 );
795 if !text.is_empty() {
796 self.emit(
797 traffic,
798 out,
799 "content_block_delta",
800 &serde_json::json!({
801 "type": "content_block_delta",
802 "index": index,
803 "delta": {"type": "text_delta", "text": text}
804 }),
805 );
806 }
807 self.emit(
808 traffic,
809 out,
810 "content_block_stop",
811 &serde_json::json!({"type": "content_block_stop", "index": index}),
812 );
813 }
814 }
815
816 fn finish(
817 &mut self,
818 payload: &serde_json::Value,
819 traffic: Option<&TrafficCapture>,
820 out: &mut Vec<u8>,
821 ) {
822 self.close_thinking(traffic, out);
823 self.close_open_blocks(traffic, out);
824 self.emit_web_searches(traffic, out);
825 self.ensure_message_start(traffic, out);
826 let usage = payload.get("response").map(parse_codex_usage);
827 let incomplete = response_is_incomplete(payload);
828 let stop_reason = if incomplete {
829 STOP_MAX_TOKENS
830 } else if self.saw_tool_use {
831 STOP_TOOL_USE
832 } else {
833 STOP_END_TURN
834 };
835 self.emit_finish(stop_reason, usage, traffic, out);
836 }
837
838 fn emit_finish(
839 &mut self,
840 stop_reason: &str,
841 usage: Option<CodexUsage>,
842 traffic: Option<&TrafficCapture>,
843 out: &mut Vec<u8>,
844 ) {
845 let mapped = map_codex_usage_to_anthropic(&usage, Some(self.web_search_requests));
846 self.emit(
847 traffic,
848 out,
849 "message_delta",
850 &serde_json::json!({
851 "type": "message_delta",
852 "delta": {
853 "stop_reason": stop_reason,
854 "stop_sequence": null
855 },
856 "usage": mapped,
857 }),
858 );
859 self.emit(
860 traffic,
861 out,
862 "message_stop",
863 &serde_json::json!({"type": "message_stop"}),
864 );
865 self.finished = true;
866 }
867
868 fn close_open_blocks(&mut self, traffic: Option<&TrafficCapture>, out: &mut Vec<u8>) {
869 self.close_thinking(traffic, out);
870 let open: Vec<usize> = self.blocks_by_output_index.keys().copied().collect();
871 for output_index in open {
872 let Some(state) = self.blocks_by_output_index.remove(&output_index) else {
873 continue;
874 };
875 let index = match state {
876 LiveBlock::Text {
877 index,
878 text,
879 deferred: true,
880 } => {
881 self.deferred_text.push((index, text));
882 continue;
883 }
884 LiveBlock::Text { index, .. } => index,
885 LiveBlock::Tool { index, .. } => index,
886 };
887 self.emit(
888 traffic,
889 out,
890 "content_block_stop",
891 &serde_json::json!({
892 "type": "content_block_stop",
893 "index": index,
894 }),
895 );
896 }
897 }
898
899 fn close_thinking(&mut self, traffic: Option<&TrafficCapture>, out: &mut Vec<u8>) {
900 let Some(index) = self.thinking_index.take() else {
901 return;
902 };
903 self.emit(
904 traffic,
905 out,
906 "content_block_stop",
907 &serde_json::json!({
908 "type": "content_block_stop",
909 "index": index,
910 }),
911 );
912 }
913}
914
915fn web_search_query(item: &serde_json::Value) -> String {
916 let Some(action) = item.get("action") else {
917 return String::new();
918 };
919 action
920 .get("query")
921 .and_then(|v| v.as_str())
922 .or_else(|| {
923 action
924 .get("queries")
925 .and_then(|v| v.as_array())
926 .and_then(|queries| queries.iter().find_map(|query| query.as_str()))
927 })
928 .unwrap_or("")
929 .to_string()
930}
931
932fn output_index(payload: &serde_json::Value) -> usize {
933 payload
934 .get("output_index")
935 .and_then(|v| v.as_u64())
936 .unwrap_or(0) as usize
937}
938
939fn parse_codex_usage(response: &serde_json::Value) -> CodexUsage {
940 let usage = match response.get("usage") {
941 Some(u) => u,
942 None => return CodexUsage::default(),
943 };
944 CodexUsage {
945 input_tokens: usage.get("input_tokens").and_then(|v| v.as_u64()),
946 output_tokens: usage.get("output_tokens").and_then(|v| v.as_u64()),
947 input_tokens_details_cached: usage
948 .get("input_tokens_details")
949 .and_then(|d| d.get("cached_tokens"))
950 .and_then(|v| v.as_u64()),
951 output_tokens_details_reasoning: usage
952 .get("output_tokens_details")
953 .and_then(|d| d.get("reasoning_tokens"))
954 .and_then(|v| v.as_u64()),
955 }
956}
957
958fn response_is_incomplete(payload: &serde_json::Value) -> bool {
959 payload.get("type").and_then(|v| v.as_str()) == Some("response.incomplete")
960 || payload
961 .get("response")
962 .and_then(|r| r.get("status"))
963 .and_then(|v| v.as_str())
964 == Some("incomplete")
965 || payload
966 .get("response")
967 .and_then(|r| r.get("incomplete_details"))
968 .and_then(|d| d.get("reason"))
969 .and_then(|v| v.as_str())
970 .is_some()
971}
972
973fn repair_whitespace_stalled_read_args(
974 name: &str,
975 args: &str,
976 call_id: Option<&str>,
977) -> Option<String> {
978 if name != "Read" {
979 return None;
980 }
981 let trimmed = args.trim_end();
982 let trailing_whitespace = args.len().saturating_sub(trimmed.len());
983 if trailing_whitespace < BUFFERED_READ_REPAIR_TRAILING_WHITESPACE_BYTES {
984 return None;
985 }
986 parse_read_args_candidate(trimmed, call_id).or_else(|| {
987 let with_brace = format!("{trimmed}}}");
988 parse_read_args_candidate(&with_brace, call_id)
989 })
990}
991
992fn parse_read_args_candidate(args: &str, call_id: Option<&str>) -> Option<String> {
993 let parsed: serde_json::Value = serde_json::from_str(args).ok()?;
994 if !is_valid_read_args(&parsed) {
995 return None;
996 }
997 Some(sanitize_read_args(
998 "Read",
999 &serde_json::to_string(&parsed).ok()?,
1000 call_id,
1001 ))
1002}
1003
1004fn is_valid_read_args(value: &serde_json::Value) -> bool {
1005 let Some(obj) = value.as_object() else {
1006 return false;
1007 };
1008 for key in obj.keys() {
1009 if !matches!(key.as_str(), "file_path" | "offset" | "limit" | "pages") {
1010 return false;
1011 }
1012 }
1013 let Some(file_path) = obj.get("file_path").and_then(|v| v.as_str()) else {
1014 return false;
1015 };
1016 if file_path.is_empty() {
1017 return false;
1018 }
1019 if let Some(offset) = obj.get("offset").and_then(|v| v.as_i64())
1020 && offset < 0
1021 {
1022 return false;
1023 }
1024 if let Some(limit) = obj.get("limit").and_then(|v| v.as_i64())
1025 && limit <= 0
1026 {
1027 return false;
1028 }
1029 if obj.get("offset").is_some_and(|v| !v.is_i64()) {
1030 return false;
1031 }
1032 if obj.get("limit").is_some_and(|v| !v.is_i64()) {
1033 return false;
1034 }
1035 if obj.get("pages").is_some_and(|v| !v.is_string()) {
1036 return false;
1037 }
1038 true
1039}
1040
1041fn error_message(payload: &serde_json::Value) -> String {
1042 payload
1043 .get("response")
1044 .and_then(|r| r.get("error"))
1045 .and_then(|e| e.get("message"))
1046 .and_then(|v| v.as_str())
1047 .or_else(|| {
1048 payload
1049 .get("error")
1050 .and_then(|e| e.get("message"))
1051 .and_then(|v| v.as_str())
1052 })
1053 .unwrap_or("Upstream error")
1054 .to_string()
1055}
1056
1057#[cfg(test)]
1058mod tests {
1059 use super::*;
1060 use serde_json::json;
1061
1062 fn render(events: Vec<serde_json::Value>) -> String {
1063 let mut translator = LiveStreamTranslator::new("msg_1", "gpt-5.5");
1064 let mut out = Vec::new();
1065 for event in events {
1066 out.extend(translator.accept(&event, None).unwrap());
1067 }
1068 String::from_utf8(out).unwrap()
1069 }
1070
1071 #[test]
1072 fn emits_text_delta_before_terminal_event() {
1073 let mut translator = LiveStreamTranslator::new("msg_1", "gpt-5.5");
1074 let out = translator
1075 .accept(
1076 &json!({
1077 "type": "response.output_text.delta",
1078 "output_index": 0,
1079 "delta": "hello"
1080 }),
1081 None,
1082 )
1083 .unwrap();
1084 let out = String::from_utf8(out).unwrap();
1085 assert!(out.contains("message_start"));
1086 assert!(out.contains("content_block_start"));
1087 assert!(out.contains("text_delta"));
1088 assert!(out.contains("hello"));
1089 assert!(!out.contains("message_stop"));
1090 }
1091
1092 #[test]
1093 fn finishes_text_stream() {
1094 let out = render(vec![
1095 json!({
1096 "type": "response.output_item.added",
1097 "output_index": 0,
1098 "item": {"type": "message", "id": "msg_up"}
1099 }),
1100 json!({
1101 "type": "response.output_text.delta",
1102 "output_index": 0,
1103 "delta": "hello"
1104 }),
1105 json!({
1106 "type": "response.output_item.done",
1107 "output_index": 0,
1108 "item": {"type": "message"}
1109 }),
1110 json!({
1111 "type": "response.completed",
1112 "response": {"id": "resp_1", "status": "completed", "incomplete_details": null, "usage": {"input_tokens": 2, "output_tokens": 1}}
1113 }),
1114 ]);
1115 assert!(out.contains("content_block_stop"));
1116 assert!(out.contains("message_delta"));
1117 assert!(out.contains(r#""stop_reason":"end_turn""#));
1118 assert!(out.contains("message_stop"));
1119 }
1120
1121 #[test]
1122 fn completed_response_with_null_incomplete_details_is_end_turn() {
1123 let out = render(vec![json!({
1124 "type": "response.completed",
1125 "response": {"id": "resp_1", "status": "completed", "incomplete_details": null, "usage": {}}
1126 })]);
1127 assert!(out.contains(r#""stop_reason":"end_turn""#));
1128 assert!(!out.contains(r#""stop_reason":"max_tokens""#));
1129 }
1130
1131 #[test]
1132 fn buffers_read_tool_args_until_done() {
1133 let out = render(vec![
1134 json!({
1135 "type": "response.output_item.added",
1136 "output_index": 0,
1137 "item": {"type": "function_call", "call_id": "call_1", "name": "Read"}
1138 }),
1139 json!({
1140 "type": "response.function_call_arguments.delta",
1141 "output_index": 0,
1142 "delta": "{\"file_path\":\"/tmp/a\",\"pages\":\"\"}"
1143 }),
1144 json!({
1145 "type": "response.output_item.done",
1146 "output_index": 0,
1147 "item": {
1148 "type": "function_call",
1149 "call_id": "call_1",
1150 "name": "Read",
1151 "arguments": "{\"file_path\":\"/tmp/a\",\"pages\":\"\"}"
1152 }
1153 }),
1154 json!({
1155 "type": "response.completed",
1156 "response": {"id": "resp_1", "usage": {}}
1157 }),
1158 ]);
1159 assert!(out.contains("tool_use"));
1160 assert!(out.contains("input_json_delta"));
1161 assert!(out.contains("/tmp/a"));
1162 assert!(!out.contains("pages"));
1163 }
1164
1165 #[test]
1166 fn repairs_whitespace_stalled_read_args_as_tool_use_finish() {
1167 let mut translator = LiveStreamTranslator::new("msg_1", "gpt-5.5");
1168 let mut out = Vec::new();
1169 out.extend(
1170 translator
1171 .accept(
1172 &json!({
1173 "type": "response.output_item.added",
1174 "output_index": 0,
1175 "item": {"type":"function_call","call_id":"call_1","name":"Read"}
1176 }),
1177 None,
1178 )
1179 .unwrap(),
1180 );
1181 out.extend(
1182 translator
1183 .accept(
1184 &json!({
1185 "type": "response.function_call_arguments.delta",
1186 "output_index": 0,
1187 "delta": format!("{{\"file_path\":\"/tmp/a\",\"pages\":\"\"{}", " ".repeat(1024))
1188 }),
1189 None,
1190 )
1191 .unwrap(),
1192 );
1193 let rendered = String::from_utf8(out).unwrap();
1194 assert!(rendered.contains(r#""partial_json":"{\"file_path\":\"/tmp/a\"}""#));
1195 assert!(rendered.contains(r#""stop_reason":"tool_use""#));
1196 assert!(rendered.contains("message_stop"));
1197 assert!(translator.is_finished());
1198 }
1199
1200 #[test]
1201 fn finishes_after_closed_completed_tool_call() {
1202 let mut translator = LiveStreamTranslator::new("msg_1", "gpt-5.5");
1203 let mut out = Vec::new();
1204 for event in [
1205 json!({
1206 "type": "response.output_item.added",
1207 "output_index": 0,
1208 "item": {"type":"function_call","call_id":"call_1","name":"WebSearch"}
1209 }),
1210 json!({
1211 "type": "response.function_call_arguments.done",
1212 "output_index": 0,
1213 "arguments": "{\"query\":\"claude-code-proxy github\"}"
1214 }),
1215 json!({
1216 "type": "response.output_item.done",
1217 "output_index": 0,
1218 "item": {
1219 "type":"function_call",
1220 "call_id":"call_1",
1221 "name":"WebSearch",
1222 "arguments":"{\"query\":\"claude-code-proxy github\"}"
1223 }
1224 }),
1225 ] {
1226 out.extend(translator.accept(&event, None).unwrap());
1227 }
1228 out.extend(translator.finish_after_closed_completed_tool_call(None));
1229 let rendered = String::from_utf8(out).unwrap();
1230 assert!(rendered.contains("content_block_start"));
1231 assert!(rendered.contains("input_json_delta"));
1232 assert!(rendered.contains(r#""stop_reason":"tool_use""#));
1233 assert!(rendered.contains("message_stop"));
1234 assert!(!rendered.contains("event: error"));
1235 }
1236
1237 #[test]
1238 fn emits_web_search_results_from_citations_before_deferred_text() {
1239 let out = render(vec![
1240 json!({
1241 "type": "response.output_item.added",
1242 "output_index": 0,
1243 "item": {"type": "web_search_call", "id": "ws_1"}
1244 }),
1245 json!({
1246 "type": "response.output_item.done",
1247 "output_index": 0,
1248 "item": {
1249 "type": "web_search_call",
1250 "id": "ws_1",
1251 "action": {"query": "grok reasoning effort"}
1252 }
1253 }),
1254 json!({
1255 "type": "response.output_item.added",
1256 "output_index": 1,
1257 "item": {"type": "message", "id": "msg_up"}
1258 }),
1259 json!({
1260 "type": "response.output_text.delta",
1261 "output_index": 1,
1262 "delta": "See the official docs."
1263 }),
1264 json!({
1265 "type": "response.output_text.annotation.added",
1266 "annotation": {
1267 "type": "url_citation",
1268 "title": "Reasoning",
1269 "url": "https://docs.x.ai/docs/guides/reasoning"
1270 }
1271 }),
1272 json!({
1273 "type": "response.output_item.done",
1274 "output_index": 1,
1275 "item": {"type": "message"}
1276 }),
1277 json!({
1278 "type": "response.completed",
1279 "response": {"status": "completed", "usage": {}}
1280 }),
1281 ]);
1282
1283 let tool = out.find("server_tool_use").unwrap();
1284 let result = out.find("web_search_tool_result").unwrap();
1285 let text = out.find("See the official docs.").unwrap();
1286 assert!(tool < result && result < text);
1287 assert!(out.contains("https://docs.x.ai/docs/guides/reasoning"));
1288 assert!(out.contains(r#""web_search_requests":1"#));
1289 }
1290
1291 #[test]
1292 fn rate_limit_event_returns_error() {
1293 let mut translator = LiveStreamTranslator::new("msg_1", "gpt-5.5");
1294 let err = translator
1295 .accept(
1296 &json!({
1297 "type": "codex.rate_limits",
1298 "rate_limits": {"limit_reached": true}
1299 }),
1300 None,
1301 )
1302 .unwrap_err();
1303 assert_eq!(err, "rate limit reached");
1304 }
1305}