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