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