1use serde::Serialize;
7use serde_json::{Value, json};
8
9use crate::LlmResponseChunk;
10use crate::codecs::common::{
11 collect_responses_reasoning_text, encrypted_reasoning_data, encrypted_reasoning_item_id,
12};
13use crate::codecs::stream::{
14 StreamCodec, StreamTranslationState, record_source_identity,
15 target_message_id_or_source_message_id, target_model_or_source_model,
16};
17use crate::format::{FormatId, WireFormat};
18use crate::llm::Usage;
19
20use super::buffered::encode_responses_usage;
21
22pub struct OpenAiResponsesStreamCodec;
24
25impl StreamCodec for OpenAiResponsesStreamCodec {
26 fn format(&self) -> FormatId {
27 WireFormat::OpenAiResponses.into()
28 }
29
30 fn is_terminal_event(&self, event: &Value) -> bool {
31 matches!(
32 event
33 .get("type")
34 .or_else(|| event.get("event"))
35 .and_then(Value::as_str),
36 Some("response.completed" | "response.incomplete" | "response.failed")
37 )
38 }
39
40 fn stops_at_terminal_event(&self) -> bool {
41 true
42 }
43
44 fn sse_done_is_terminal(&self) -> bool {
45 true
46 }
47
48 fn stamp_streamed_response_model(&self, event: &mut Value, served_model: &str) {
49 if let Some(response) = event.get_mut("response").and_then(Value::as_object_mut) {
50 response.insert("model".to_string(), Value::String(served_model.to_string()));
51 }
52 }
53
54 fn decode_event(
55 &self,
56 state: &mut StreamTranslationState,
57 event: &Value,
58 ) -> Vec<LlmResponseChunk> {
59 decode_responses_stream(state, event)
60 }
61
62 fn encode_event(
63 &self,
64 state: &mut StreamTranslationState,
65 event: LlmResponseChunk,
66 ) -> Vec<Value> {
67 let events = encode_responses_stream(state, event);
68 add_sequence_numbers(state, events)
69 }
70
71 fn observe_replayed_event(
72 &self,
73 state: &mut StreamTranslationState,
74 raw: &Value,
75 normalized: Vec<LlmResponseChunk>,
76 ) {
77 let replayed_terminal = normalized
78 .iter()
79 .any(|chunk| matches!(chunk, LlmResponseChunk::MessageStop { .. }));
80 for chunk in normalized {
83 drop(encode_responses_stream(state, chunk));
84 }
85 state.response_sequence_number = raw
86 .get("sequence_number")
87 .and_then(Value::as_u64)
88 .map_or(state.response_sequence_number.saturating_add(1), |number| {
89 number.saturating_add(1)
90 });
91 if replayed_terminal {
92 state.finished = true;
93 }
94 }
95
96 fn finish(&self, state: &mut StreamTranslationState) -> Vec<Value> {
97 let events = finish_responses_stream(state);
98 add_sequence_numbers(state, events)
99 }
100}
101
102#[derive(Serialize)]
104struct ResponsesStreamResponse {
105 id: String,
106 object: &'static str,
107 created_at: u64,
108 completed_at: Option<u64>,
109 error: Option<Value>,
110 incomplete_details: Option<Value>,
111 instructions: Option<Value>,
112 metadata: Option<Value>,
113 model: String,
114 output: Vec<Value>,
115 parallel_tool_calls: bool,
116 frequency_penalty: Option<f64>,
117 presence_penalty: Option<f64>,
118 status: &'static str,
119 temperature: Option<f64>,
120 tool_choice: &'static str,
121 tools: Vec<Value>,
122 top_p: Option<f64>,
123 usage: Value,
124}
125
126fn decode_responses_stream(
128 state: &mut StreamTranslationState,
129 event: &Value,
130) -> Vec<LlmResponseChunk> {
131 tracing::trace!(target: "switchyard_translation::responses::raw", raw = %event);
134 let event_type = event
135 .get("type")
136 .or_else(|| event.get("event"))
137 .and_then(Value::as_str);
138 match event_type {
139 Some("response.created") => {
140 state.saw_message_start = true;
141 let response = event.get("response").and_then(Value::as_object);
142 if let Some(model) = response
143 .and_then(|response| response.get("model"))
144 .and_then(Value::as_str)
145 {
146 state.model = Some(model.to_string());
147 }
148 if let Some(id) = response
149 .and_then(|response| response.get("id"))
150 .and_then(Value::as_str)
151 {
152 state.message_id = Some(id.to_string());
153 }
154 vec![LlmResponseChunk::MessageStart {
155 id: state.message_id.clone(),
156 model: state.model.clone(),
157 }]
158 }
159 Some("response.output_text.delta") => event
160 .get("delta")
161 .and_then(Value::as_str)
162 .map(|text| {
163 let index = event
164 .get("output_index")
165 .and_then(Value::as_u64)
166 .unwrap_or(0) as usize;
167 let content_index = event
168 .get("content_index")
169 .and_then(Value::as_u64)
170 .unwrap_or(0) as usize;
171 state
172 .decoded_response_text
173 .entry(index)
174 .or_default()
175 .entry(content_index)
176 .or_default()
177 .push_str(text);
178 vec![LlmResponseChunk::TextDelta {
179 index,
180 text: text.to_string(),
181 }]
182 })
183 .unwrap_or_default(),
184 Some("response.reasoning_text.delta") | Some("response.reasoning_summary_text.delta") => {
185 event
186 .get("delta")
187 .or_else(|| event.get("text"))
188 .and_then(Value::as_str)
189 .map(|text| {
190 let index = event
191 .get("output_index")
192 .and_then(Value::as_u64)
193 .unwrap_or(0) as usize;
194 state
197 .decoded_reasoning
198 .entry(index)
199 .or_default()
200 .push_str(text);
201 vec![LlmResponseChunk::ReasoningDelta {
202 index,
203 text: text.to_string(),
204 }]
205 })
206 .unwrap_or_default()
207 }
208 Some("response.output_item.added") => decode_responses_output_item_added(event, state),
209 Some("response.function_call_arguments.delta") => {
210 let output_index = event
211 .get("output_index")
212 .and_then(Value::as_u64)
213 .unwrap_or(0);
214 event
215 .get("delta")
216 .and_then(Value::as_str)
217 .filter(|_| {
219 !state
220 .tool_states
221 .get(&(output_index as usize))
222 .is_some_and(|tool| tool.decoded_custom_call)
223 })
224 .map(|delta| {
225 state.decoded_tool_call = true;
226 state
229 .tool_states
230 .entry(output_index as usize)
231 .or_default()
232 .decoded_arguments
233 .push_str(delta);
234 vec![LlmResponseChunk::ToolCallDelta {
235 index: output_index as usize,
236 id: None,
237 name: None,
238 arguments_delta: Some(delta.to_string()),
239 }]
240 })
241 .unwrap_or_default()
242 }
243 Some("response.output_item.done") => decode_responses_output_item_done(event, state),
244 Some("response.reasoning_text.done") | Some("response.reasoning_summary_text.done") => {
245 let index = event
246 .get("output_index")
247 .and_then(Value::as_u64)
248 .unwrap_or(0) as usize;
249 event
250 .get("text")
251 .and_then(Value::as_str)
252 .filter(|text| !text.is_empty())
253 .filter(|_| {
254 state
255 .decoded_reasoning
256 .get(&index)
257 .is_none_or(String::is_empty)
258 })
259 .map(|text| {
260 state.decoded_reasoning.insert(index, text.to_string());
261 vec![LlmResponseChunk::ReasoningDelta {
262 index,
263 text: text.to_string(),
264 }]
265 })
266 .unwrap_or_default()
267 }
268 Some("response.reasoning_summary_part.done") => {
269 let index = event
270 .get("output_index")
271 .and_then(Value::as_u64)
272 .unwrap_or(0) as usize;
273 event
274 .get("part")
275 .and_then(|part| part.get("text"))
276 .and_then(Value::as_str)
277 .filter(|text| !text.is_empty())
278 .filter(|_| {
279 state
280 .decoded_reasoning
281 .get(&index)
282 .is_none_or(String::is_empty)
283 })
284 .map(|text| {
285 state.decoded_reasoning.insert(index, text.to_string());
286 vec![LlmResponseChunk::ReasoningDelta {
287 index,
288 text: text.to_string(),
289 }]
290 })
291 .unwrap_or_default()
292 }
293 Some("response.completed") => {
294 let mut out = Vec::new();
295 if let Some(items) = event
298 .get("response")
299 .and_then(|response| response.get("output"))
300 .and_then(Value::as_array)
301 {
302 for (position, item) in items.iter().enumerate() {
303 if let Some(item) = item.as_object() {
304 out.extend(decode_responses_completed_item(item, position, state));
305 if matches!(out.last(), Some(LlmResponseChunk::DecodeError { .. })) {
306 return out;
307 }
308 }
309 }
310 }
311 decode_terminal_usage(state, event, &mut out);
312 let reason = state.decoded_tool_call.then(|| "tool_use".to_string());
318 out.push(LlmResponseChunk::MessageStop { reason });
319 out
320 }
321 Some("response.incomplete") => {
323 let mut out = Vec::new();
324 decode_terminal_usage(state, event, &mut out);
325 out.push(LlmResponseChunk::MessageStop {
326 reason: Some("max_tokens".to_string()),
327 });
328 out
329 }
330 Some("response.failed") => vec![LlmResponseChunk::StreamError {
331 message: event
332 .get("response")
333 .and_then(|response| response.get("error"))
334 .and_then(|error| error.get("message"))
335 .and_then(Value::as_str)
336 .unwrap_or("unknown Responses stream error")
337 .to_string(),
338 }],
339 Some("error") => vec![LlmResponseChunk::StreamError {
340 message: event
341 .get("message")
342 .and_then(Value::as_str)
343 .unwrap_or("unknown Responses stream error")
344 .to_string(),
345 }],
346 _ => Vec::new(),
347 }
348}
349
350fn encode_responses_stream(
352 state: &mut StreamTranslationState,
353 event: LlmResponseChunk,
354) -> Vec<Value> {
355 if state.errored {
357 return Vec::new();
358 }
359 match event {
360 LlmResponseChunk::MessageStart { id, model } => {
361 record_source_identity(state, id, model);
362 ensure_responses_created(state)
363 }
364 LlmResponseChunk::TextDelta { text, .. } => encode_responses_text_delta(state, text),
365 LlmResponseChunk::ReasoningDelta { index, text } => {
366 encode_responses_reasoning_delta(state, index, text)
367 }
368 LlmResponseChunk::ReasoningDetailsDelta {
369 index,
370 details,
371 text,
372 } => {
373 let data = encrypted_reasoning_data(&details);
376 let item = state.response_reasoning.entry(index).or_default();
377 for detail in &details {
378 if detail.get("type").and_then(Value::as_str) == Some("anthropic.signature_delta")
379 && let Some(signature) = detail.get("signature").and_then(Value::as_str)
380 {
381 item.anthropic_signature
382 .get_or_insert_default()
383 .push_str(signature);
384 }
385 }
386 match reasoning_details_item_id(&details) {
387 Some(id) if item.started && item.item_id.as_deref() != Some(id.as_str()) => {
388 tracing::warn!(
391 item_id = %id,
392 "encrypted reasoning arrived after its item opened under a different id; dropping payload"
393 );
394 }
395 id => {
396 if id.is_some() {
397 item.item_id = id;
398 }
399 if data.is_some() {
400 item.encrypted = data;
401 }
402 }
403 }
404 if !item.started && item.item_id.is_none() && summary_lacks_item_id(&details) {
408 item.pending_text.push_str(&text);
409 return ensure_responses_created(state);
410 }
411 let mut out = ensure_responses_reasoning_started(state, index);
412 if !text.is_empty() {
413 out.extend(encode_responses_reasoning_delta(state, index, text));
414 }
415 out
416 }
417 LlmResponseChunk::ToolCallDelta {
418 index,
419 id,
420 name,
421 arguments_delta,
422 } => encode_responses_tool_delta(state, index, id, name, arguments_delta),
423 LlmResponseChunk::Usage(usage) => {
424 state.usage = usage;
425 state.saw_backend_usage = true;
426 Vec::new()
427 }
428 LlmResponseChunk::MessageStop { reason } => {
429 state.stop_reason = reason.or_else(|| state.stop_reason.clone());
430 Vec::new()
431 }
432 LlmResponseChunk::DecodeError { message } | LlmResponseChunk::StreamError { message } => {
433 state.finished = true; state.errored = true; vec![json!({"type": "error", "message": message})]
437 }
438 }
439}
440
441fn finish_responses_stream(state: &mut StreamTranslationState) -> Vec<Value> {
443 if state.finished {
444 return Vec::new();
445 }
446 let is_truncated = matches!(
447 state.stop_reason.as_deref(),
448 Some("length") | Some("max_tokens")
449 );
450 let (event_type, status) = if is_truncated {
451 ("response.incomplete", "incomplete")
452 } else {
453 ("response.completed", "completed")
454 };
455 let incomplete_details = is_truncated.then(|| json!({ "reason": "max_output_tokens" }));
456 let mut out = open_held_reasoning(state, None);
457 out.extend(ensure_responses_created(state));
458 if state.response_text_started
459 && let Some(output_index) = state.response_text_output_index
460 {
461 let item_id = responses_item_id(state, "msg", output_index);
462 out.push(json!({
463 "type": "response.output_text.done",
464 "item_id": item_id,
465 "output_index": output_index,
466 "content_index": 0,
467 "text": state.response_text,
468 "logprobs": [],
469 }));
470 out.push(json!({
471 "type": "response.content_part.done",
472 "item_id": item_id,
473 "output_index": output_index,
474 "content_index": 0,
475 "part": {"type": "output_text", "text": state.response_text, "annotations": [], "logprobs": []},
476 }));
477 out.push(json!({
478 "type": "response.output_item.done",
479 "output_index": output_index,
480 "item": {
481 "type": "message",
482 "id": item_id,
483 "role": "assistant",
484 "status": status,
485 "content": [{"type": "output_text", "text": state.response_text, "annotations": [], "logprobs": []}],
486 },
487 }));
488 }
489
490 let mut final_items: Vec<(usize, Value)> = Vec::new();
491 let mut reasoning_items: Vec<(usize, usize)> = state
492 .response_reasoning
493 .iter()
494 .filter(|(_, item)| item.started)
495 .filter_map(|(index, item)| item.output_index.map(|output| (*index, output)))
496 .collect();
497 reasoning_items.sort_by_key(|(_, output_index)| *output_index);
498 for (index, output_index) in reasoning_items {
499 let item_id = responses_reasoning_item_id(state, index);
502 let reasoning = &state.response_reasoning[&index];
503 let mut summary = Vec::new();
504 if !reasoning.text.is_empty() {
505 out.push(json!({
506 "type": "response.reasoning_summary_text.done",
507 "item_id": item_id,
508 "output_index": output_index,
509 "summary_index": 0,
510 "text": reasoning.text,
511 }));
512 out.push(json!({
513 "type": "response.reasoning_summary_part.done",
514 "item_id": item_id,
515 "output_index": output_index,
516 "summary_index": 0,
517 "part": {"type": "summary_text", "text": reasoning.text},
518 }));
519 summary.push(json!({
520 "type": "summary_text",
521 "text": reasoning.text,
522 }));
523 }
524 let mut item = json!({
525 "type": "reasoning",
526 "id": item_id,
527 "status": "completed",
528 "summary": summary,
529 });
530 if let Some(signature) = &reasoning.anthropic_signature {
531 item["encrypted_content"] =
532 Value::String(super::encode_anthropic_thinking(&reasoning.text, signature));
533 } else if let Some(encrypted) = &reasoning.encrypted {
534 item["encrypted_content"] = Value::String(encrypted.clone());
535 }
536 out.push(json!({
537 "type": "response.output_item.done",
538 "output_index": output_index,
539 "item": item,
540 }));
541 final_items.push((output_index, item));
542 }
543 if state.response_text_started
544 && let Some(output_index) = state.response_text_output_index
545 {
546 final_items.push((
547 output_index,
548 json!({
549 "type": "message",
550 "id": responses_item_id(state, "msg", output_index),
551 "role": "assistant",
552 "status": status,
553 "content": [{"type": "output_text", "text": state.response_text, "annotations": [], "logprobs": []}],
554 }),
555 ));
556 }
557
558 for tool in state.tool_states.values() {
559 if !tool.started {
560 continue;
561 }
562 let output_index = tool.response_output_index.unwrap_or(0);
563 let item_id = tool
564 .response_item_id
565 .clone()
566 .unwrap_or_else(|| responses_item_id(state, "fc", output_index));
567 out.push(json!({
568 "type": "response.function_call_arguments.done",
569 "item_id": item_id,
570 "output_index": output_index,
571 "name": tool.name.clone().unwrap_or_default(),
572 "arguments": tool.arguments,
573 }));
574 let item = json!({
575 "type": "function_call",
576 "id": item_id,
577 "call_id": tool.id.clone().unwrap_or_else(|| format!("call_{output_index}")),
578 "name": tool.name.clone().unwrap_or_default(),
579 "arguments": tool.arguments,
580 "status": "completed",
581 });
582 out.push(json!({
583 "type": "response.output_item.done",
584 "output_index": output_index,
585 "item": item,
586 }));
587 final_items.push((output_index, item));
588 }
589
590 final_items.sort_by_key(|(index, _)| *index);
591 let output = final_items
592 .into_iter()
593 .map(|(_, item)| item)
594 .collect::<Vec<_>>();
595
596 out.push(json!({
597 "type": event_type,
598 "response": responses_stream_response(state, status, incomplete_details, output),
599 }));
600 state.finished = true;
601 out
602}
603
604fn decode_responses_reasoning_item(
611 item: &serde_json::Map<String, Value>,
612 index: usize,
613 state: &mut StreamTranslationState,
614) -> Vec<LlmResponseChunk> {
615 let mut out = Vec::new();
616 let has_encrypted = item
617 .get("encrypted_content")
618 .and_then(Value::as_str)
619 .is_some_and(|data| !data.is_empty());
620 if let Some(id) = item
624 .get("id")
625 .and_then(Value::as_str)
626 .filter(|id| !id.is_empty())
627 && !state.decoded_reasoning_ids.contains(&index)
628 {
629 state.decoded_reasoning_ids.insert(index);
630 if !has_encrypted {
631 out.push(LlmResponseChunk::ReasoningDetailsDelta {
632 index,
633 details: vec![json!({"type": "reasoning.encrypted", "id": id})],
634 text: String::new(),
635 });
636 }
637 }
638 let mut parts = Vec::new();
639 collect_responses_reasoning_text(item.get("content"), &mut parts);
640 collect_responses_reasoning_text(item.get("summary"), &mut parts);
641 collect_responses_reasoning_text(item.get("text"), &mut parts);
642 let text = parts.join("\n");
643 let already = state
644 .decoded_reasoning
645 .get(&index)
646 .map(String::as_str)
647 .unwrap_or("");
648 if !text.is_empty() && already.is_empty() {
649 state.decoded_reasoning.insert(index, text.clone());
650 out.push(LlmResponseChunk::ReasoningDelta { index, text });
651 }
652 if let Some(data) = item
653 .get("encrypted_content")
654 .and_then(Value::as_str)
655 .filter(|data| !data.is_empty())
656 && !state.decoded_reasoning_encrypted.contains(&index)
657 {
658 state.decoded_reasoning_encrypted.insert(index);
659 let mut detail = json!({"type": "reasoning.encrypted", "data": data});
661 if let Some(id) = item
662 .get("id")
663 .and_then(Value::as_str)
664 .filter(|id| !id.is_empty())
665 {
666 detail["id"] = Value::String(id.to_string());
667 }
668 out.push(LlmResponseChunk::ReasoningDetailsDelta {
669 index,
670 details: vec![detail],
671 text: String::new(),
672 });
673 }
674 out
675}
676
677fn decode_responses_output_item_added(
678 event: &Value,
679 state: &mut StreamTranslationState,
680) -> Vec<LlmResponseChunk> {
681 let Some(item) = event.get("item").and_then(Value::as_object) else {
682 return Vec::new();
683 };
684 let index = event
685 .get("output_index")
686 .and_then(Value::as_u64)
687 .unwrap_or(0) as usize;
688 if item.get("type").and_then(Value::as_str) == Some("reasoning") {
689 return decode_responses_reasoning_item(item, index, state);
690 }
691 let item_type = item.get("type").and_then(Value::as_str);
692 if item_type != Some("function_call") && item_type != Some("custom_tool_call") {
693 return Vec::new();
694 }
695 state.decoded_tool_call = true;
696 let id = item
697 .get("call_id")
698 .or_else(|| item.get("id"))
699 .and_then(Value::as_str);
700 let name = item.get("name").and_then(Value::as_str);
701 let tool = state.tool_states.entry(index).or_default();
702 tool.has_decoded_identity |=
703 id.is_some_and(|id| !id.is_empty()) && name.is_some_and(|name| !name.is_empty());
704 let custom = item_type == Some("custom_tool_call");
705 tool.decoded_custom_call |= custom;
706 let arguments_delta = if custom {
709 None
710 } else {
711 item.get("arguments")
712 .and_then(Value::as_str)
713 .filter(|arguments| !arguments.is_empty())
714 .map(ToOwned::to_owned)
715 };
716 if let Some(arguments) = arguments_delta.as_deref() {
717 tool.decoded_arguments.push_str(arguments);
718 }
719 vec![LlmResponseChunk::ToolCallDelta {
720 index,
721 id: id.map(ToOwned::to_owned),
722 name: name.map(ToOwned::to_owned),
723 arguments_delta,
724 }]
725}
726
727fn decode_responses_output_item_done(
728 event: &Value,
729 state: &mut StreamTranslationState,
730) -> Vec<LlmResponseChunk> {
731 let Some(item) = event.get("item").and_then(Value::as_object) else {
732 return Vec::new();
733 };
734 let index = event
735 .get("output_index")
736 .and_then(Value::as_u64)
737 .unwrap_or(0) as usize;
738 decode_responses_completed_item(item, index, state)
739}
740
741fn decode_responses_completed_item(
742 item: &serde_json::Map<String, Value>,
743 index: usize,
744 state: &mut StreamTranslationState,
745) -> Vec<LlmResponseChunk> {
746 if item.get("type").and_then(Value::as_str) == Some("reasoning") {
747 return decode_responses_reasoning_item(item, index, state);
748 }
749 let item_type = item.get("type").and_then(Value::as_str);
750 if item_type == Some("message") {
751 let mut out = Vec::new();
752 if let Some(content) = item.get("content").and_then(Value::as_array) {
753 for (content_index, part) in content.iter().enumerate() {
754 if part.get("type").and_then(Value::as_str) != Some("output_text") {
755 continue;
756 }
757 if let Some(text) = part.get("text").and_then(Value::as_str) {
758 let decoded = state
759 .decoded_response_text
760 .entry(index)
761 .or_default()
762 .entry(content_index)
763 .or_default();
764 match snapshot_suffix(decoded, text) {
765 Ok(Some(text)) => out.push(LlmResponseChunk::TextDelta { index, text }),
766 Ok(None) => {}
767 Err(error) => return vec![error],
768 }
769 }
770 }
771 }
772 return out;
773 }
774 if item_type != Some("function_call") && item_type != Some("custom_tool_call") {
775 return Vec::new();
776 }
777 state.decoded_tool_call = true;
778 let custom_arguments = (item_type == Some("custom_tool_call")).then(|| {
779 json!({
780 crate::codex_custom_tools::INPUT_ARGUMENT:
781 item.get("input").and_then(Value::as_str).unwrap_or_default()
782 })
783 .to_string()
784 });
785 let arguments = custom_arguments
786 .as_deref()
787 .or_else(|| item.get("arguments").and_then(Value::as_str));
788 let tool = state.tool_states.entry(index).or_default();
789 let arguments_delta =
790 match arguments.map(|arguments| snapshot_suffix(&mut tool.decoded_arguments, arguments)) {
791 Some(Ok(delta)) => delta,
792 Some(Err(error)) => return vec![error],
793 None => None,
794 };
795 let id = item
796 .get("call_id")
797 .or_else(|| item.get("id"))
798 .and_then(Value::as_str);
799 let name = item.get("name").and_then(Value::as_str);
800 let needs_identity = !tool.has_decoded_identity;
801 tool.has_decoded_identity |=
802 id.is_some_and(|id| !id.is_empty()) && name.is_some_and(|name| !name.is_empty());
803 if needs_identity || arguments_delta.is_some() {
804 return vec![LlmResponseChunk::ToolCallDelta {
805 index,
806 id: id.filter(|_| needs_identity).map(ToOwned::to_owned),
807 name: name.filter(|_| needs_identity).map(ToOwned::to_owned),
808 arguments_delta,
809 }];
810 }
811 Vec::new()
812}
813
814fn snapshot_suffix(
817 decoded: &mut String,
818 snapshot: &str,
819) -> Result<Option<String>, LlmResponseChunk> {
820 let Some(suffix) = snapshot.strip_prefix(decoded.as_str()) else {
821 return Err(LlmResponseChunk::DecodeError {
822 message: "Responses snapshot conflicts with streamed content".to_string(),
823 });
824 };
825 if suffix.is_empty() {
826 return Ok(None);
827 }
828 decoded.push_str(suffix);
829 Ok(Some(suffix.to_owned()))
830}
831
832fn ensure_responses_created(state: &mut StreamTranslationState) -> Vec<Value> {
834 if state.response_created {
835 return Vec::new();
836 }
837 if state.target_message_id.is_none() {
839 state.target_message_id = Some(responses_id(state));
840 }
841 state.response_created = true;
842 vec![json!({
843 "type": "response.created",
844 "response": responses_stream_response(state, "in_progress", None, Vec::new()),
845 })]
846}
847
848fn responses_stream_response(
850 state: &StreamTranslationState,
851 status: &'static str,
852 incomplete_details: Option<Value>,
853 output: Vec<Value>,
854) -> ResponsesStreamResponse {
855 ResponsesStreamResponse {
856 id: responses_id(state),
857 object: "response",
858 created_at: 0,
859 completed_at: None,
860 error: None,
861 incomplete_details,
862 instructions: None,
863 metadata: None,
864 model: target_model_or_source_model(state),
865 output,
866 parallel_tool_calls: true,
867 frequency_penalty: None,
868 presence_penalty: None,
869 status,
870 temperature: None,
871 tool_choice: "auto",
872 tools: Vec::new(),
873 top_p: None,
874 usage: encode_responses_usage(&state.usage),
875 }
876}
877
878fn add_sequence_numbers(state: &mut StreamTranslationState, mut events: Vec<Value>) -> Vec<Value> {
880 for event in &mut events {
881 if let Some(object) = event.as_object_mut() {
882 object.insert(
883 "sequence_number".to_string(),
884 Value::from(state.response_sequence_number),
885 );
886 state.response_sequence_number = state.response_sequence_number.saturating_add(1);
887 }
888 }
889 events
890}
891
892fn encode_responses_text_delta(state: &mut StreamTranslationState, text: String) -> Vec<Value> {
894 let mut out = open_held_reasoning(state, None);
895 out.extend(ensure_responses_created(state));
896 if !state.response_text_started {
897 state.response_text_started = true;
898 let output_index = state.next_response_output_index;
899 state.next_response_output_index += 1;
900 state.response_text_output_index = Some(output_index);
901 out.push(json!({
902 "type": "response.output_item.added",
903 "output_index": output_index,
904 "item": {
905 "type": "message",
906 "id": responses_item_id(state, "msg", output_index),
907 "role": "assistant",
908 "status": "in_progress",
909 "content": [],
910 },
911 }));
912 out.push(json!({
913 "type": "response.content_part.added",
914 "item_id": responses_item_id(state, "msg", output_index),
915 "output_index": output_index,
916 "content_index": 0,
917 "part": {"type": "output_text", "text": "", "annotations": [], "logprobs": []},
918 }));
919 }
920 state.response_text.push_str(&text);
921 out.push(json!({
922 "type": "response.output_text.delta",
923 "item_id": responses_item_id(state, "msg", state.response_text_output_index.unwrap_or(0)),
924 "output_index": state.response_text_output_index.unwrap_or(0),
925 "content_index": 0,
926 "delta": text,
927 "logprobs": [],
928 }));
929 out
930}
931
932fn reasoning_details_item_id(details: &[Value]) -> Option<String> {
936 encrypted_reasoning_item_id(details).or_else(|| {
937 details
938 .iter()
939 .filter_map(Value::as_object)
940 .filter(|detail| {
941 matches!(
942 detail.get("type").and_then(Value::as_str),
943 Some("reasoning.summary" | "reasoning.text")
944 )
945 })
946 .find_map(|detail| {
947 detail
948 .get("id")
949 .and_then(Value::as_str)
950 .filter(|id| !id.is_empty())
951 })
952 .map(ToOwned::to_owned)
953 })
954}
955
956fn summary_lacks_item_id(details: &[Value]) -> bool {
958 details.iter().filter_map(Value::as_object).any(|detail| {
959 detail.get("type").and_then(Value::as_str) == Some("reasoning.summary")
960 && detail
961 .get("id")
962 .and_then(Value::as_str)
963 .is_none_or(str::is_empty)
964 })
965}
966
967fn open_held_reasoning(state: &mut StreamTranslationState, before: Option<usize>) -> Vec<Value> {
972 let held: Vec<usize> = state
973 .response_reasoning
974 .iter()
975 .filter(|(index, item)| {
976 before.is_none_or(|bound| **index < bound)
977 && !item.started
978 && !item.pending_text.is_empty()
979 })
980 .map(|(index, _)| *index)
981 .collect();
982 let mut out = Vec::new();
983 for index in held {
984 out.extend(ensure_responses_reasoning_started(state, index));
985 }
986 out
987}
988
989fn responses_reasoning_item_id(state: &StreamTranslationState, index: usize) -> String {
992 let item = state.response_reasoning.get(&index);
993 item.and_then(|item| item.item_id.clone())
994 .unwrap_or_else(|| {
995 let output_index = item.and_then(|item| item.output_index).unwrap_or(0);
996 responses_item_id(state, "rs", output_index)
997 })
998}
999
1000fn ensure_responses_reasoning_started(
1002 state: &mut StreamTranslationState,
1003 index: usize,
1004) -> Vec<Value> {
1005 let mut out = ensure_responses_created(state);
1006 if state
1007 .response_reasoning
1008 .get(&index)
1009 .is_some_and(|item| item.started)
1010 {
1011 return out;
1012 }
1013 out.extend(open_held_reasoning(state, Some(index)));
1015 let output_index = state.next_response_output_index;
1016 state.next_response_output_index += 1;
1017 let item = state.response_reasoning.entry(index).or_default();
1018 item.started = true;
1019 item.output_index = Some(output_index);
1020 let pending = std::mem::take(&mut item.pending_text);
1021 let item_id = responses_reasoning_item_id(state, index);
1022 out.push(json!({
1025 "type": "response.output_item.added",
1026 "output_index": output_index,
1027 "item": {
1028 "type": "reasoning",
1029 "id": item_id,
1030 "status": "in_progress",
1031 "summary": [],
1032 },
1033 }));
1034 if !pending.is_empty() {
1036 out.extend(encode_responses_reasoning_delta(state, index, pending));
1037 }
1038 out
1039}
1040
1041fn ensure_responses_reasoning_summary_started(
1045 state: &mut StreamTranslationState,
1046 index: usize,
1047) -> Vec<Value> {
1048 let mut out = ensure_responses_reasoning_started(state, index);
1049 let item_id = responses_reasoning_item_id(state, index);
1050 let item = state.response_reasoning.entry(index).or_default();
1051 if item.summary_started {
1052 return out;
1053 }
1054 item.summary_started = true;
1055 out.push(json!({
1056 "type": "response.reasoning_summary_part.added",
1057 "item_id": item_id,
1058 "output_index": item.output_index.unwrap_or(0),
1059 "summary_index": 0,
1060 "part": {"type": "summary_text", "text": ""},
1061 }));
1062 out
1063}
1064
1065fn encode_responses_reasoning_delta(
1067 state: &mut StreamTranslationState,
1068 index: usize,
1069 text: String,
1070) -> Vec<Value> {
1071 if let Some(item) = state.response_reasoning.get_mut(&index)
1073 && !item.started
1074 && !item.pending_text.is_empty()
1075 {
1076 item.pending_text.push_str(&text);
1077 return Vec::new();
1078 }
1079 if text.is_empty() {
1081 return ensure_responses_reasoning_started(state, index);
1082 }
1083 let mut out = ensure_responses_reasoning_summary_started(state, index);
1084 let item = state.response_reasoning.entry(index).or_default();
1085 item.text.push_str(&text);
1086 let output_index = item.output_index.unwrap_or(0);
1087 out.push(json!({
1088 "type": "response.reasoning_summary_text.delta",
1089 "item_id": responses_reasoning_item_id(state, index),
1090 "output_index": output_index,
1091 "summary_index": 0,
1092 "delta": text,
1093 }));
1094 out
1095}
1096
1097fn encode_responses_tool_delta(
1099 state: &mut StreamTranslationState,
1100 index: usize,
1101 id: Option<String>,
1102 name: Option<String>,
1103 arguments_delta: Option<String>,
1104) -> Vec<Value> {
1105 let mut out = ensure_responses_created(state);
1106 let resp_id = responses_id(state);
1107 let tool = state.tool_states.entry(index).or_default();
1108 if id.is_some() {
1109 tool.id = id;
1110 }
1111 if name.is_some() {
1112 tool.name = name;
1113 }
1114 if let Some(delta) = arguments_delta {
1115 tool.arguments.push_str(&delta);
1116 tool.pending_arguments.push_str(&delta);
1117 }
1118
1119 if !tool.started {
1120 let Some(name) = tool.name.clone() else {
1121 return out;
1122 };
1123 out.extend(open_held_reasoning(state, None));
1125 let output_index = state.next_response_output_index;
1126 state.next_response_output_index += 1;
1127 let tool = state.tool_states.entry(index).or_default();
1128 tool.response_output_index = Some(output_index);
1129 tool.response_item_id = Some(responses_item_id_from(&resp_id, "fc", output_index));
1130 tool.started = true;
1131 out.push(json!({
1132 "type": "response.output_item.added",
1133 "output_index": output_index,
1134 "item": {
1135 "type": "function_call",
1136 "id": tool.response_item_id.clone().unwrap_or_else(|| responses_item_id_from(&resp_id, "fc", output_index)),
1137 "call_id": tool.id.clone().unwrap_or_else(|| format!("call_{index}")),
1138 "name": name,
1139 "arguments": "",
1140 "status": "in_progress",
1141 },
1142 }));
1143 if !tool.pending_arguments.is_empty() {
1144 out.push(json!({
1145 "type": "response.function_call_arguments.delta",
1146 "item_id": tool.response_item_id,
1147 "output_index": output_index,
1148 "delta": tool.pending_arguments,
1149 }));
1150 tool.pending_arguments.clear();
1151 }
1152 return out;
1153 }
1154
1155 if let Some(output_index) = tool.response_output_index
1156 && !tool.pending_arguments.is_empty()
1157 {
1158 out.push(json!({
1159 "type": "response.function_call_arguments.delta",
1160 "item_id": tool.response_item_id,
1161 "output_index": output_index,
1162 "delta": tool.pending_arguments,
1163 }));
1164 tool.pending_arguments.clear();
1165 }
1166 out
1167}
1168
1169fn decode_terminal_usage(
1171 state: &mut StreamTranslationState,
1172 event: &Value,
1173 out: &mut Vec<LlmResponseChunk>,
1174) {
1175 if let Some(usage) = event
1176 .get("response")
1177 .and_then(Value::as_object)
1178 .and_then(|response| response.get("usage"))
1179 .and_then(Value::as_object)
1180 {
1181 let usage = responses_usage(usage);
1182 state.usage = usage.clone();
1183 state.saw_backend_usage = true;
1184 out.push(LlmResponseChunk::Usage(usage));
1185 }
1186}
1187
1188fn responses_usage(usage: &serde_json::Map<String, Value>) -> Usage {
1190 let aggregate_input_tokens = usage.get("input_tokens").and_then(Value::as_u64);
1191 let cached_input_tokens = usage
1192 .get("input_tokens_details")
1193 .and_then(|details| details.get("cached_tokens"))
1194 .and_then(Value::as_u64);
1195 let cache_creation_input_tokens = usage
1196 .get("input_tokens_details")
1197 .and_then(|details| details.get("cache_write_tokens"))
1198 .and_then(Value::as_u64);
1199 let input_tokens = aggregate_input_tokens.map(|tokens| {
1200 tokens
1201 .saturating_sub(cached_input_tokens.unwrap_or(0))
1202 .saturating_sub(cache_creation_input_tokens.unwrap_or(0))
1203 });
1204 let output_tokens = usage.get("output_tokens").and_then(Value::as_u64);
1205 Usage {
1206 provider_cost: crate::codecs::common::provider_cost(usage),
1207 input_tokens,
1208 cache: crate::codecs::common::cache_usage_details(
1209 usage,
1210 cached_input_tokens,
1211 cache_creation_input_tokens,
1212 ),
1213 output_tokens,
1214 total_tokens: usage
1215 .get("total_tokens")
1216 .and_then(Value::as_u64)
1217 .or_else(|| Some(aggregate_input_tokens.unwrap_or(0) + output_tokens.unwrap_or(0))),
1218 reasoning_tokens: usage
1219 .get("output_tokens_details")
1220 .and_then(|details| details.get("reasoning_tokens"))
1221 .or_else(|| {
1222 usage
1223 .get("completion_tokens_details")
1224 .and_then(|details| details.get("reasoning_tokens"))
1225 })
1226 .and_then(Value::as_u64),
1227 }
1228}
1229
1230const ITEM_ID_DISCRIMINATOR_CHARS: usize = 40;
1234
1235fn responses_item_id(state: &StreamTranslationState, prefix: &str, output_index: usize) -> String {
1239 responses_item_id_from(&responses_id(state), prefix, output_index)
1240}
1241
1242fn responses_item_id_from(resp_id: &str, prefix: &str, output_index: usize) -> String {
1245 let resp = resp_id.strip_prefix("resp_").unwrap_or(resp_id);
1246 if resp.chars().count() <= ITEM_ID_DISCRIMINATOR_CHARS {
1247 format!("{prefix}_{resp}_{output_index}")
1248 } else {
1249 format!("{prefix}_{:016x}_{output_index}", fnv1a_64(resp))
1250 }
1251}
1252
1253fn fnv1a_64(text: &str) -> u64 {
1256 text.bytes().fold(0xcbf2_9ce4_8422_2325_u64, |hash, byte| {
1257 (hash ^ u64::from(byte)).wrapping_mul(0x0000_0100_0000_01b3)
1258 })
1259}
1260
1261fn responses_id(state: &StreamTranslationState) -> String {
1263 target_message_id_or_source_message_id(state)
1264 .unwrap_or("resp_switchyard")
1265 .to_string()
1266}
1267
1268#[cfg(test)]
1269mod tests {
1270 use super::*;
1271
1272 #[test]
1273 fn recognizes_stream_completion() {
1274 let codec = OpenAiResponsesStreamCodec;
1275 assert!(codec.sse_done_is_terminal());
1276 for kind in [
1277 "response.completed",
1278 "response.incomplete",
1279 "response.failed",
1280 ] {
1281 assert!(codec.is_terminal_event(&json!({"type": kind})));
1282 assert!(codec.is_terminal_event(&json!({"event": kind})));
1283 }
1284 assert!(!codec.is_terminal_event(&json!({"type": "response.created"})));
1285 }
1286
1287 #[test]
1288 fn responses_replay_stamps_the_served_model_inside_response() {
1289 let mut event = json!({
1290 "type": "response.created",
1291 "response": {
1292 "id": "resp_1",
1293 "model": "provider/model",
1294 "provider_extension": true,
1295 },
1296 });
1297
1298 OpenAiResponsesStreamCodec.stamp_streamed_response_model(&mut event, "served/model");
1299
1300 assert_eq!(event["response"]["model"], "served/model");
1301 assert_eq!(event["response"]["provider_extension"], true);
1302 }
1303}