1use serde_json::{json, Value};
7
8use crate::codecs::stream::{
9 record_source_identity, target_message_id_or_source_message_id, target_model_or_source_model,
10 StreamCodec, StreamTranslationState,
11};
12use crate::format::{FormatId, WireFormat};
13use crate::llm::{LlmStreamEvent, Usage};
14
15pub struct OpenAiResponsesStreamCodec;
17
18impl StreamCodec for OpenAiResponsesStreamCodec {
19 fn format(&self) -> FormatId {
20 WireFormat::OpenAiResponses.into()
21 }
22
23 fn decode_event(
24 &self,
25 state: &mut StreamTranslationState,
26 event: &Value,
27 ) -> Vec<LlmStreamEvent> {
28 decode_responses_stream(state, event)
29 }
30
31 fn encode_event(
32 &self,
33 state: &mut StreamTranslationState,
34 event: LlmStreamEvent,
35 ) -> Vec<Value> {
36 encode_responses_stream(state, event)
37 }
38
39 fn finish(&self, state: &mut StreamTranslationState) -> Vec<Value> {
40 finish_responses_stream(state)
41 }
42}
43
44fn decode_responses_stream(
46 state: &mut StreamTranslationState,
47 event: &Value,
48) -> Vec<LlmStreamEvent> {
49 let event_type = event
50 .get("type")
51 .or_else(|| event.get("event"))
52 .and_then(Value::as_str);
53 match event_type {
54 Some("response.created") => {
55 state.saw_message_start = true;
56 let response = event.get("response").and_then(Value::as_object);
57 if let Some(model) = response
58 .and_then(|response| response.get("model"))
59 .and_then(Value::as_str)
60 {
61 state.model = Some(model.to_string());
62 }
63 if let Some(id) = response
64 .and_then(|response| response.get("id"))
65 .and_then(Value::as_str)
66 {
67 state.message_id = Some(id.to_string());
68 }
69 vec![LlmStreamEvent::MessageStart {
70 id: state.message_id.clone(),
71 model: state.model.clone(),
72 }]
73 }
74 Some("response.output_text.delta") => event
75 .get("delta")
76 .and_then(Value::as_str)
77 .map(|text| {
78 vec![LlmStreamEvent::TextDelta {
79 index: event
80 .get("output_index")
81 .and_then(Value::as_u64)
82 .unwrap_or(0) as usize,
83 text: text.to_string(),
84 }]
85 })
86 .unwrap_or_default(),
87 Some("response.reasoning_text.delta") | Some("response.reasoning_summary_text.delta") => {
88 event
89 .get("delta")
90 .or_else(|| event.get("text"))
91 .and_then(Value::as_str)
92 .map(|text| {
93 vec![LlmStreamEvent::ReasoningDelta {
94 index: event
95 .get("output_index")
96 .and_then(Value::as_u64)
97 .unwrap_or(0) as usize,
98 text: text.to_string(),
99 }]
100 })
101 .unwrap_or_default()
102 }
103 Some("response.output_item.added") => decode_responses_output_item_added(event),
104 Some("response.function_call_arguments.delta") => {
105 let output_index = event
106 .get("output_index")
107 .and_then(Value::as_u64)
108 .unwrap_or(0);
109 event
110 .get("delta")
111 .and_then(Value::as_str)
112 .map(|delta| {
113 vec![LlmStreamEvent::ToolCallDelta {
114 index: output_index as usize,
115 id: None,
116 name: None,
117 arguments_delta: Some(delta.to_string()),
118 }]
119 })
120 .unwrap_or_default()
121 }
122 Some("response.output_item.done") => decode_responses_output_item_done(event, state),
123 Some("response.completed") => {
124 let mut out = Vec::new();
125 if let Some(usage) = event
126 .get("response")
127 .and_then(Value::as_object)
128 .and_then(|response| response.get("usage"))
129 .and_then(Value::as_object)
130 {
131 let usage = responses_usage(usage);
132 state.usage = usage.clone();
133 state.saw_backend_usage = true;
134 out.push(LlmStreamEvent::Usage(usage));
135 }
136 out.push(LlmStreamEvent::MessageStop { reason: None });
137 out
138 }
139 Some("error") => vec![LlmStreamEvent::Error {
140 message: event
141 .get("message")
142 .and_then(Value::as_str)
143 .unwrap_or("unknown Responses stream error")
144 .to_string(),
145 }],
146 _ => Vec::new(),
147 }
148}
149
150fn encode_responses_stream(
152 state: &mut StreamTranslationState,
153 event: LlmStreamEvent,
154) -> Vec<Value> {
155 match event {
156 LlmStreamEvent::MessageStart { id, model } => {
157 record_source_identity(state, id, model);
158 ensure_responses_created(state)
159 }
160 LlmStreamEvent::TextDelta { text, .. } => encode_responses_text_delta(state, text),
161 LlmStreamEvent::ReasoningDelta { text, .. } => {
162 encode_responses_reasoning_delta(state, text)
163 }
164 LlmStreamEvent::ToolCallDelta {
165 index,
166 id,
167 name,
168 arguments_delta,
169 } => encode_responses_tool_delta(state, index, id, name, arguments_delta),
170 LlmStreamEvent::Usage(usage) => {
171 state.usage = usage;
172 state.saw_backend_usage = true;
173 Vec::new()
174 }
175 LlmStreamEvent::MessageStop { reason } => {
176 state.stop_reason = reason.or_else(|| state.stop_reason.clone());
177 Vec::new()
178 }
179 LlmStreamEvent::Error { message } => {
180 vec![json!({"type": "error", "message": message})]
181 }
182 }
183}
184
185fn finish_responses_stream(state: &mut StreamTranslationState) -> Vec<Value> {
187 let mut out = ensure_responses_created(state);
188 if state.response_text_started {
189 if let Some(output_index) = state.response_text_output_index {
190 out.push(json!({
191 "type": "response.content_part.done",
192 "output_index": output_index,
193 "content_index": 0,
194 "part": {"type": "output_text", "text": state.response_text},
195 }));
196 out.push(json!({
197 "type": "response.output_item.done",
198 "output_index": output_index,
199 "item": {
200 "type": "message",
201 "role": "assistant",
202 "status": "completed",
203 "content": [{"type": "output_text", "text": state.response_text}],
204 },
205 }));
206 }
207 }
208
209 let mut final_items: Vec<(usize, Value)> = Vec::new();
210 if state.response_reasoning_started {
211 if let Some(output_index) = state.response_reasoning_output_index {
212 out.push(json!({
213 "type": "response.reasoning_text.done",
214 "output_index": output_index,
215 "content_index": 0,
216 "text": state.response_reasoning_text,
217 }));
218 let item = json!({
219 "type": "reasoning",
220 "id": format!("rs_{output_index}"),
221 "status": "completed",
222 "content": [{
223 "type": "reasoning_text",
224 "text": state.response_reasoning_text,
225 }],
226 "summary": [],
227 });
228 out.push(json!({
229 "type": "response.output_item.done",
230 "output_index": output_index,
231 "item": item,
232 }));
233 final_items.push((output_index, item));
234 }
235 }
236 if state.response_text_started {
237 if let Some(output_index) = state.response_text_output_index {
238 final_items.push((
239 output_index,
240 json!({
241 "type": "message",
242 "role": "assistant",
243 "status": "completed",
244 "content": [{"type": "output_text", "text": state.response_text}],
245 }),
246 ));
247 }
248 }
249
250 for tool in state.tool_states.values() {
251 if !tool.started {
252 continue;
253 }
254 let output_index = tool.response_output_index.unwrap_or(0);
255 out.push(json!({
256 "type": "response.function_call_arguments.done",
257 "output_index": output_index,
258 "arguments": tool.arguments,
259 }));
260 let item = json!({
261 "type": "function_call",
262 "id": tool.response_item_id.clone().unwrap_or_else(|| format!("fc_{output_index}")),
263 "call_id": tool.id.clone().unwrap_or_else(|| format!("call_{output_index}")),
264 "name": tool.name.clone().unwrap_or_default(),
265 "arguments": tool.arguments,
266 "status": "completed",
267 });
268 out.push(json!({
269 "type": "response.output_item.done",
270 "output_index": output_index,
271 "item": item,
272 }));
273 final_items.push((output_index, item));
274 }
275
276 final_items.sort_by_key(|(index, _)| *index);
277 let output = final_items
278 .into_iter()
279 .map(|(_, item)| item)
280 .collect::<Vec<_>>();
281
282 out.push(json!({
283 "type": "response.completed",
284 "response": {
285 "id": responses_id(state),
286 "object": "response",
287 "status": "completed",
288 "model": target_model_or_source_model(state),
289 "output": output,
290 "usage": responses_usage_value(&state.usage),
291 },
292 }));
293 state.finished = true;
294 out
295}
296
297fn decode_responses_output_item_added(event: &Value) -> Vec<LlmStreamEvent> {
299 let Some(item) = event.get("item").and_then(Value::as_object) else {
300 return Vec::new();
301 };
302 if item.get("type").and_then(Value::as_str) != Some("function_call") {
303 return Vec::new();
304 }
305 let index = event
306 .get("output_index")
307 .and_then(Value::as_u64)
308 .unwrap_or(0) as usize;
309 vec![LlmStreamEvent::ToolCallDelta {
310 index,
311 id: item
312 .get("call_id")
313 .or_else(|| item.get("id"))
314 .and_then(Value::as_str)
315 .map(ToOwned::to_owned),
316 name: item
317 .get("name")
318 .and_then(Value::as_str)
319 .map(ToOwned::to_owned),
320 arguments_delta: item
321 .get("arguments")
322 .and_then(Value::as_str)
323 .filter(|arguments| !arguments.is_empty())
324 .map(ToOwned::to_owned),
325 }]
326}
327
328fn decode_responses_output_item_done(
330 event: &Value,
331 state: &StreamTranslationState,
332) -> Vec<LlmStreamEvent> {
333 let Some(item) = event.get("item").and_then(Value::as_object) else {
334 return Vec::new();
335 };
336 if item.get("type").and_then(Value::as_str) != Some("function_call") {
337 return Vec::new();
338 }
339 let index = event
340 .get("output_index")
341 .and_then(Value::as_u64)
342 .unwrap_or(0) as usize;
343 let arguments = item.get("arguments").and_then(Value::as_str);
344 if let Some(arguments) = arguments {
345 let existing = state
346 .tool_states
347 .get(&index)
348 .map(|tool| tool.arguments.as_str())
349 .unwrap_or("");
350 if !arguments.is_empty() && arguments != existing {
351 return vec![LlmStreamEvent::ToolCallDelta {
352 index,
353 id: None,
354 name: None,
355 arguments_delta: Some(arguments.to_string()),
356 }];
357 }
358 }
359 Vec::new()
360}
361
362fn ensure_responses_created(state: &mut StreamTranslationState) -> Vec<Value> {
364 if state.response_created {
365 return Vec::new();
366 }
367 state.response_created = true;
368 vec![json!({
369 "type": "response.created",
370 "response": {
371 "id": responses_id(state),
372 "object": "response",
373 "status": "in_progress",
374 "model": target_model_or_source_model(state),
375 "output": [],
376 "usage": responses_usage_value(&state.usage),
377 },
378 })]
379}
380
381fn encode_responses_text_delta(state: &mut StreamTranslationState, text: String) -> Vec<Value> {
383 let mut out = ensure_responses_created(state);
384 if !state.response_text_started {
385 state.response_text_started = true;
386 let output_index = state.next_response_output_index;
387 state.next_response_output_index += 1;
388 state.response_text_output_index = Some(output_index);
389 out.push(json!({
390 "type": "response.output_item.added",
391 "output_index": output_index,
392 "item": {
393 "type": "message",
394 "id": format!("msg_{output_index}"),
395 "role": "assistant",
396 "status": "in_progress",
397 "content": [],
398 },
399 }));
400 out.push(json!({
401 "type": "response.content_part.added",
402 "output_index": output_index,
403 "content_index": 0,
404 "part": {"type": "output_text", "text": ""},
405 }));
406 }
407 state.response_text.push_str(&text);
408 out.push(json!({
409 "type": "response.output_text.delta",
410 "output_index": state.response_text_output_index.unwrap_or(0),
411 "content_index": 0,
412 "delta": text,
413 }));
414 out
415}
416
417fn encode_responses_reasoning_delta(
419 state: &mut StreamTranslationState,
420 text: String,
421) -> Vec<Value> {
422 let mut out = ensure_responses_created(state);
423 if !state.response_reasoning_started {
424 state.response_reasoning_started = true;
425 let output_index = state.next_response_output_index;
426 state.next_response_output_index += 1;
427 state.response_reasoning_output_index = Some(output_index);
428 out.push(json!({
429 "type": "response.output_item.added",
430 "output_index": output_index,
431 "item": {
432 "type": "reasoning",
433 "id": format!("rs_{output_index}"),
434 "status": "in_progress",
435 "content": [],
436 "summary": [],
437 },
438 }));
439 out.push(json!({
440 "type": "response.reasoning_text.added",
441 "output_index": output_index,
442 "content_index": 0,
443 "text": "",
444 }));
445 }
446 state.response_reasoning_text.push_str(&text);
447 out.push(json!({
448 "type": "response.reasoning_text.delta",
449 "output_index": state.response_reasoning_output_index.unwrap_or(0),
450 "content_index": 0,
451 "delta": text,
452 }));
453 out
454}
455
456fn encode_responses_tool_delta(
458 state: &mut StreamTranslationState,
459 index: usize,
460 id: Option<String>,
461 name: Option<String>,
462 arguments_delta: Option<String>,
463) -> Vec<Value> {
464 let mut out = ensure_responses_created(state);
465 let tool = state.tool_states.entry(index).or_default();
466 if id.is_some() {
467 tool.id = id;
468 }
469 if name.is_some() {
470 tool.name = name;
471 }
472 if let Some(delta) = arguments_delta {
473 tool.arguments.push_str(&delta);
474 tool.pending_arguments.push_str(&delta);
475 }
476
477 if !tool.started {
478 let Some(name) = tool.name.clone() else {
479 return out;
480 };
481 let output_index = state.next_response_output_index;
482 state.next_response_output_index += 1;
483 tool.response_output_index = Some(output_index);
484 tool.response_item_id = Some(format!("fc_{output_index}"));
485 tool.started = true;
486 out.push(json!({
487 "type": "response.output_item.added",
488 "output_index": output_index,
489 "item": {
490 "type": "function_call",
491 "id": tool.response_item_id.clone().unwrap_or_else(|| format!("fc_{output_index}")),
492 "call_id": tool.id.clone().unwrap_or_else(|| format!("call_{index}")),
493 "name": name,
494 "arguments": "",
495 "status": "in_progress",
496 },
497 }));
498 if !tool.pending_arguments.is_empty() {
499 out.push(json!({
500 "type": "response.function_call_arguments.delta",
501 "output_index": output_index,
502 "delta": tool.pending_arguments,
503 }));
504 tool.pending_arguments.clear();
505 }
506 return out;
507 }
508
509 if let Some(output_index) = tool.response_output_index {
510 if !tool.pending_arguments.is_empty() {
511 out.push(json!({
512 "type": "response.function_call_arguments.delta",
513 "output_index": output_index,
514 "delta": tool.pending_arguments,
515 }));
516 tool.pending_arguments.clear();
517 }
518 }
519 out
520}
521
522fn responses_usage(usage: &serde_json::Map<String, Value>) -> Usage {
524 let input_tokens = usage.get("input_tokens").and_then(Value::as_u64);
525 let output_tokens = usage.get("output_tokens").and_then(Value::as_u64);
526 Usage {
527 input_tokens,
528 output_tokens,
529 total_tokens: usage
530 .get("total_tokens")
531 .and_then(Value::as_u64)
532 .or_else(|| Some(input_tokens.unwrap_or(0) + output_tokens.unwrap_or(0))),
533 reasoning_tokens: usage
534 .get("output_tokens_details")
535 .and_then(|details| details.get("reasoning_tokens"))
536 .or_else(|| {
537 usage
538 .get("completion_tokens_details")
539 .and_then(|details| details.get("reasoning_tokens"))
540 })
541 .and_then(Value::as_u64),
542 }
543}
544
545fn responses_usage_value(usage: &Usage) -> Value {
547 let mut value = json!({
548 "input_tokens": usage.input_tokens.unwrap_or(0),
549 "output_tokens": usage.output_tokens.unwrap_or(0),
550 "total_tokens": usage.total_tokens.unwrap_or_else(|| {
551 usage.input_tokens.unwrap_or(0) + usage.output_tokens.unwrap_or(0)
552 }),
553 });
554 if let Some(reasoning_tokens) = usage.reasoning_tokens {
555 value["output_tokens_details"] = json!({
556 "reasoning_tokens": reasoning_tokens,
557 });
558 }
559 value
560}
561
562fn responses_id(state: &StreamTranslationState) -> String {
564 let Some(id) = target_message_id_or_source_message_id(state) else {
565 return "resp_switchyard".to_string();
566 };
567 if id.starts_with("resp_") {
568 id.to_string()
569 } else {
570 format!("resp_{id}")
571 }
572}