1use serde::Deserialize;
19
20use crate::api::llm::LlmRequest;
21use crate::error::{FlowError, Result};
22use crate::json::Json;
23
24use super::request::{
25 AnnotatedLlmRequest, GenerationParams, Message, MessageContent, ToolChoice, ToolChoiceFunction,
26 ToolChoiceFunctionName, ToolDefinition,
27};
28use super::response::{
29 AnnotatedLlmResponse, ApiSpecificResponse, FinishReason, RawUsageCost, ResponseToolCall, Usage,
30 estimate_cost_for_provider, infer_model_provider, provider_reported_cost,
31};
32use super::traits::{LlmCodec, LlmResponseCodec};
33
34pub struct OpenAIResponsesCodec;
40
41#[derive(Deserialize)]
46struct RawResponsesResponse {
47 id: Option<String>,
48 model: Option<String>,
49 status: Option<String>,
50 output: Option<Vec<Json>>,
51 usage: Option<RawResponsesUsage>,
52 incomplete_details: Option<Json>,
53 previous_response_id: Option<String>,
54 store: Option<bool>,
55 service_tier: Option<String>,
56 truncation: Option<Json>,
57 reasoning: Option<Json>,
58 #[serde(flatten)]
59 extra: serde_json::Map<String, Json>,
60}
61
62#[derive(Deserialize)]
63struct RawResponsesUsage {
64 input_tokens: Option<u64>,
65 output_tokens: Option<u64>,
66 total_tokens: Option<u64>,
67 input_tokens_details: Option<RawInputTokensDetails>,
68 output_tokens_details: Option<RawOutputTokensDetails>,
69 #[serde(rename = "cost_usd")]
70 provider_cost: Option<f64>,
71 cost: Option<RawUsageCost>,
72}
73
74#[derive(Deserialize, Clone)]
75struct RawInputTokensDetails {
76 cached_tokens: Option<u64>,
77 #[serde(flatten)]
78 extra: serde_json::Map<String, Json>,
79}
80
81#[derive(Deserialize, Clone)]
82struct RawOutputTokensDetails {
83 reasoning_tokens: Option<u64>,
84 #[serde(flatten)]
85 extra: serde_json::Map<String, Json>,
86}
87
88fn map_responses_finish_reason(
94 status: Option<&str>,
95 incomplete_details: Option<&Json>,
96) -> Option<FinishReason> {
97 let incomplete_reason = incomplete_details
98 .and_then(|d| d.get("reason"))
99 .and_then(|r| r.as_str());
100
101 match status {
102 Some("completed") => Some(FinishReason::Complete),
103 Some("incomplete") => match incomplete_reason {
104 Some("max_output_tokens") => Some(FinishReason::Length),
105 Some("content_filter") => Some(FinishReason::ContentFilter),
106 Some(other) => Some(FinishReason::Unknown(other.to_string())),
107 None => Some(FinishReason::Unknown("incomplete".to_string())),
108 },
109 Some(other) => Some(FinishReason::Unknown(other.to_string())),
110 None => None,
111 }
112}
113
114fn parse_arguments(arguments: &str) -> Json {
118 serde_json::from_str(arguments).unwrap_or_else(|_| Json::String(arguments.to_string()))
119}
120
121fn input_tokens_details_to_json(details: &RawInputTokensDetails) -> Json {
122 let mut obj = serde_json::Map::new();
123 if let Some(cached_tokens) = details.cached_tokens {
124 obj.insert("cached_tokens".into(), Json::from(cached_tokens));
125 }
126 obj.extend(details.extra.clone());
127 Json::Object(obj)
128}
129
130fn output_tokens_details_to_json(details: &RawOutputTokensDetails) -> Json {
131 let mut obj = serde_json::Map::new();
132 if let Some(reasoning_tokens) = details.reasoning_tokens {
133 obj.insert("reasoning_tokens".into(), Json::from(reasoning_tokens));
134 }
135 obj.extend(details.extra.clone());
136 Json::Object(obj)
137}
138
139const MODELED_REQUEST_KEYS: &[&str] = &[
141 "input",
142 "instructions",
143 "model",
144 "max_output_tokens",
145 "temperature",
146 "top_p",
147 "tools",
148 "tool_choice",
149 "store",
150 "previous_response_id",
151 "truncation",
152 "reasoning",
153 "include",
154 "user",
155 "metadata",
156 "service_tier",
157 "parallel_tool_calls",
158 "max_tool_calls",
159 "top_logprobs",
160 "stream",
161];
162const UNPARSED_INPUT_ITEMS_KEY: &str = "_openai_responses_unparsed_input_items";
163
164fn json_f64(v: f64) -> Json {
166 serde_json::Number::from_f64(v)
167 .map(Json::Number)
168 .unwrap_or(Json::Null)
169}
170
171fn collect_output_parts(items: Option<&[Json]>) -> (Vec<String>, Vec<ResponseToolCall>) {
172 let mut text_parts = Vec::new();
173 let mut tool_calls = Vec::new();
174
175 if let Some(items) = items {
176 for item in items {
177 collect_output_item(item, &mut text_parts, &mut tool_calls);
178 }
179 }
180
181 (text_parts, tool_calls)
182}
183
184fn collect_output_item(
185 item: &Json,
186 text_parts: &mut Vec<String>,
187 tool_calls: &mut Vec<ResponseToolCall>,
188) {
189 match item
190 .get("type")
191 .and_then(|value| value.as_str())
192 .unwrap_or("")
193 {
194 "message" => collect_message_text_parts(item, text_parts),
195 "function_call" => tool_calls.push(parse_function_call(item)),
196 _ => {}
197 }
198}
199
200fn collect_message_text_parts(item: &Json, text_parts: &mut Vec<String>) {
201 let Some(content) = item.get("content").and_then(|value| value.as_array()) else {
202 return;
203 };
204
205 for block in content {
206 if let Some(text) = output_text_block(block) {
207 text_parts.push(text);
208 }
209 }
210}
211
212fn output_text_block(block: &Json) -> Option<String> {
213 (block.get("type").and_then(|value| value.as_str()) == Some("output_text"))
214 .then(|| block.get("text").and_then(|value| value.as_str()))
215 .flatten()
216 .map(str::to_string)
217}
218
219fn parse_function_call(item: &Json) -> ResponseToolCall {
220 ResponseToolCall {
221 id: item
222 .get("call_id")
223 .and_then(|value| value.as_str())
224 .unwrap_or("")
225 .to_string(),
226 name: item
227 .get("name")
228 .and_then(|value| value.as_str())
229 .unwrap_or("")
230 .to_string(),
231 arguments: item
232 .get("arguments")
233 .and_then(|value| value.as_str())
234 .map(parse_arguments)
235 .unwrap_or(Json::Object(serde_json::Map::new())),
236 }
237}
238
239fn message_from_text_parts(text_parts: Vec<String>) -> Option<MessageContent> {
240 match text_parts.as_slice() {
241 [] => None,
242 [text] => Some(MessageContent::Text(text.clone())),
243 _ => Some(MessageContent::Text(text_parts.join("\n"))),
244 }
245}
246
247fn optional_vec<T>(items: Vec<T>) -> Option<Vec<T>> {
248 (!items.is_empty()).then_some(items)
249}
250
251fn split_system_and_input_messages(messages: &[Message]) -> (Option<String>, Vec<&Message>) {
252 let mut system_text = None;
253 let mut input_messages = Vec::new();
254
255 for msg in messages {
256 match msg {
257 Message::System { content, .. } => {
258 if let MessageContent::Text(text) = content {
259 system_text = Some(text.clone());
260 }
261 }
262 other => input_messages.push(other),
263 }
264 }
265
266 (system_text, input_messages)
267}
268
269fn set_or_remove_string(obj: &mut serde_json::Map<String, Json>, key: &str, value: Option<String>) {
270 if let Some(value) = value {
271 obj.insert(key.into(), Json::String(value));
272 } else {
273 obj.remove(key);
274 }
275}
276
277fn insert_serialized<T: serde::Serialize>(
278 obj: &mut serde_json::Map<String, Json>,
279 key: &str,
280 value: &T,
281 context: &str,
282) -> Result<()> {
283 let json = serde_json::to_value(value)
284 .map_err(|e| FlowError::Internal(format!("OpenAI Responses {context} encode: {e}")))?;
285 obj.insert(key.into(), json);
286 Ok(())
287}
288
289fn overlay_generation_params(obj: &mut serde_json::Map<String, Json>, params: &GenerationParams) {
290 if let Some(temp) = params.temperature {
291 obj.insert("temperature".into(), json_f64(temp));
292 }
293 if let Some(top_p) = params.top_p {
294 obj.insert("top_p".into(), json_f64(top_p));
295 }
296 if let Some(max_tokens) = params.max_tokens {
297 obj.insert("max_output_tokens".into(), Json::from(max_tokens));
298 obj.remove("max_tokens");
299 }
300}
301
302fn encode_openai_responses_input(
303 obj: &mut serde_json::Map<String, Json>,
304 annotated: &AnnotatedLlmRequest,
305) -> Result<()> {
306 let (system_text, input_messages) = split_system_and_input_messages(&annotated.messages);
307 set_or_remove_string(obj, "instructions", system_text);
308 if let Some(raw_input_items) = annotated.extra.get(UNPARSED_INPUT_ITEMS_KEY) {
309 obj.insert("input".into(), raw_input_items.clone());
310 } else {
311 insert_serialized(obj, "input", &input_messages, "input")?;
312 }
313 Ok(())
314}
315
316fn encode_openai_responses_tools(
317 obj: &mut serde_json::Map<String, Json>,
318 annotated: &AnnotatedLlmRequest,
319) -> Result<()> {
320 if let Some(ref tools) = annotated.tools {
321 insert_serialized(obj, "tools", tools, "tools")?;
322 }
323 if let Some(ref tool_choice) = annotated.tool_choice {
324 insert_serialized(obj, "tool_choice", tool_choice, "tool_choice")?;
325 }
326 Ok(())
327}
328
329fn overlay_openai_responses_fields(
330 obj: &mut serde_json::Map<String, Json>,
331 annotated: &AnnotatedLlmRequest,
332) {
333 if let Some(ref model) = annotated.model {
334 obj.insert("model".into(), Json::String(model.clone()));
335 }
336 overlay_openai_responses_json_fields(obj, annotated);
337 overlay_openai_responses_string_fields(obj, annotated);
338 overlay_openai_responses_bool_fields(obj, annotated);
339 overlay_openai_responses_u64_fields(obj, annotated);
340}
341
342fn overlay_openai_responses_json_fields(
343 obj: &mut serde_json::Map<String, Json>,
344 annotated: &AnnotatedLlmRequest,
345) {
346 for (key, value) in [
347 ("truncation", &annotated.truncation),
348 ("reasoning", &annotated.reasoning),
349 ("include", &annotated.include),
350 ("metadata", &annotated.metadata),
351 ] {
352 if let Some(value) = value {
353 obj.insert(key.into(), value.clone());
354 }
355 }
356}
357
358fn overlay_openai_responses_string_fields(
359 obj: &mut serde_json::Map<String, Json>,
360 annotated: &AnnotatedLlmRequest,
361) {
362 for (key, value) in [
363 ("previous_response_id", &annotated.previous_response_id),
364 ("user", &annotated.user),
365 ("service_tier", &annotated.service_tier),
366 ] {
367 if let Some(value) = value {
368 obj.insert(key.into(), Json::String(value.clone()));
369 }
370 }
371}
372
373fn overlay_openai_responses_bool_fields(
374 obj: &mut serde_json::Map<String, Json>,
375 annotated: &AnnotatedLlmRequest,
376) {
377 for (key, value) in [
378 ("store", annotated.store),
379 ("parallel_tool_calls", annotated.parallel_tool_calls),
380 ("stream", annotated.stream),
381 ] {
382 if let Some(value) = value {
383 obj.insert(key.into(), Json::Bool(value));
384 }
385 }
386}
387
388fn overlay_openai_responses_u64_fields(
389 obj: &mut serde_json::Map<String, Json>,
390 annotated: &AnnotatedLlmRequest,
391) {
392 for (key, value) in [
393 ("max_output_tokens", annotated.max_output_tokens),
394 ("max_tool_calls", annotated.max_tool_calls),
395 ("top_logprobs", annotated.top_logprobs),
396 ] {
397 if let Some(value) = value {
398 obj.insert(key.into(), Json::from(value));
399 }
400 }
401}
402
403fn merge_openai_responses_extra_fields(
404 obj: &mut serde_json::Map<String, Json>,
405 extra: &serde_json::Map<String, Json>,
406) {
407 for (k, v) in extra {
408 if k != UNPARSED_INPUT_ITEMS_KEY {
409 obj.insert(k.clone(), v.clone());
410 }
411 }
412}
413
414fn decode_openai_or_anthropic_tool_choice(value: &Json) -> Option<ToolChoice> {
415 if let Ok(parsed) = serde_json::from_value::<ToolChoice>(value.clone()) {
416 return Some(parsed);
417 }
418
419 let obj = value.as_object()?;
420 match obj.get("type").and_then(|v| v.as_str()) {
421 Some("auto") => Some(ToolChoice::Auto),
422 Some("any") => Some(ToolChoice::Required),
423 Some("none") => Some(ToolChoice::None),
424 Some("tool") => {
425 let name = obj.get("name").and_then(|v| v.as_str())?.to_string();
426 Some(ToolChoice::Specific(ToolChoiceFunction {
427 choice_type: "function".to_string(),
428 function: ToolChoiceFunctionName { name },
429 }))
430 }
431 _ => None,
432 }
433}
434
435fn decode_openai_or_anthropic_parallel_tool_calls(
436 obj: &serde_json::Map<String, Json>,
437) -> Option<bool> {
438 if let Some(value) = obj.get("parallel_tool_calls").and_then(|v| v.as_bool()) {
439 return Some(value);
440 }
441 let tool_choice = obj.get("tool_choice")?.as_object()?;
442 tool_choice
443 .get("disable_parallel_tool_use")
444 .and_then(|v| v.as_bool())
445 .map(|disabled| !disabled)
446}
447
448impl LlmResponseCodec for OpenAIResponsesCodec {
453 fn decode_response(&self, response: &Json) -> Result<AnnotatedLlmResponse> {
454 let raw: RawResponsesResponse = serde_json::from_value(response.clone())
455 .map_err(|e| FlowError::Internal(format!("OpenAI Responses response decode: {e}")))?;
456
457 let all_output_items = raw.output.clone();
458 let (text_parts, tool_calls) = collect_output_parts(raw.output.as_deref());
459 let message = message_from_text_parts(text_parts);
460 let tool_calls = optional_vec(tool_calls);
461
462 let finish_reason =
464 map_responses_finish_reason(raw.status.as_deref(), raw.incomplete_details.as_ref());
465
466 let input_tokens_details = raw.usage.as_ref().and_then(|u| {
467 u.input_tokens_details
468 .as_ref()
469 .map(input_tokens_details_to_json)
470 });
471 let output_tokens_details = raw.usage.as_ref().and_then(|u| {
472 u.output_tokens_details
473 .as_ref()
474 .map(output_tokens_details_to_json)
475 });
476
477 let model_for_pricing = raw.model.as_deref();
479 let model_provider = infer_model_provider("openai", model_for_pricing);
480 let usage = raw.usage.map(|u| {
481 let mut usage = Usage {
482 prompt_tokens: u.input_tokens,
483 completion_tokens: u.output_tokens,
484 total_tokens: u.total_tokens,
485 cache_read_tokens: u
486 .input_tokens_details
487 .as_ref()
488 .and_then(|d| d.cached_tokens),
489 cache_write_tokens: None,
490 cost: provider_reported_cost(u.provider_cost, u.cost),
491 };
492 if usage.cost.is_none() {
493 usage.cost = model_for_pricing.and_then(|model| {
494 estimate_cost_for_provider(model_provider.as_deref(), model, &usage)
495 });
496 }
497 usage
498 });
499
500 let api_specific = Some(ApiSpecificResponse::OpenAIResponses {
502 output_items: all_output_items,
503 status: raw.status,
504 incomplete_details: raw.incomplete_details,
505 previous_response_id: raw.previous_response_id,
506 store: raw.store,
507 service_tier: raw.service_tier,
508 truncation: raw.truncation,
509 reasoning: raw.reasoning,
510 input_tokens_details,
511 output_tokens_details,
512 });
513
514 Ok(AnnotatedLlmResponse {
515 id: raw.id,
516 model: raw.model,
517 message,
518 tool_calls,
519 finish_reason,
520 usage,
521 api_specific,
522 extra: raw.extra,
523 })
524 }
525}
526
527impl LlmCodec for OpenAIResponsesCodec {
532 fn decode(&self, request: &LlmRequest) -> Result<AnnotatedLlmRequest> {
533 let obj = request
534 .content
535 .as_object()
536 .ok_or_else(|| FlowError::Internal("request content is not an object".into()))?;
537
538 let mut messages: Vec<Message> = Vec::new();
539 let mut preserved_unparsed_input: Option<Json> = None;
540
541 if let Some(instructions) = obj.get("instructions").and_then(|v| v.as_str()) {
543 messages.push(Message::System {
544 content: MessageContent::Text(instructions.to_string()),
545 name: None,
546 });
547 }
548
549 if let Some(input) = obj.get("input") {
551 if let Some(s) = input.as_str() {
552 messages.push(Message::User {
554 content: MessageContent::Text(s.to_string()),
555 name: None,
556 });
557 } else if input.is_array() {
558 match serde_json::from_value::<Vec<Message>>(input.clone()) {
560 Ok(input_messages) => messages.extend(input_messages),
561 Err(_) => {
562 preserved_unparsed_input = Some(input.clone());
564 }
565 }
566 }
567 }
568
569 let model = obj.get("model").and_then(|v| v.as_str()).map(String::from);
571
572 let temperature = obj.get("temperature").and_then(|v| v.as_f64());
574 let top_p = obj.get("top_p").and_then(|v| v.as_f64());
575 let max_tokens = obj.get("max_output_tokens").and_then(|v| v.as_u64());
576 let params = if temperature.is_some() || max_tokens.is_some() || top_p.is_some() {
579 Some(GenerationParams {
580 temperature,
581 max_tokens,
582 top_p,
583 stop: None,
584 })
585 } else {
586 None
587 };
588
589 let tools: Option<Vec<ToolDefinition>> = obj
591 .get("tools")
592 .map(|v| serde_json::from_value(v.clone()))
593 .transpose()
594 .map_err(|e| FlowError::Internal(format!("OpenAI Responses tools decode: {e}")))?;
595
596 let tool_choice: Option<ToolChoice> = obj
598 .get("tool_choice")
599 .and_then(decode_openai_or_anthropic_tool_choice);
600
601 let mut extra: serde_json::Map<String, Json> = obj
603 .iter()
604 .filter(|(k, _)| !MODELED_REQUEST_KEYS.contains(&k.as_str()))
605 .map(|(k, v)| (k.clone(), v.clone()))
606 .collect();
607 if let Some(input_items) = preserved_unparsed_input {
608 extra.insert(UNPARSED_INPUT_ITEMS_KEY.into(), input_items);
609 }
610
611 Ok(AnnotatedLlmRequest {
612 messages,
613 model,
614 params,
615 tools,
616 tool_choice,
617 store: obj.get("store").and_then(|v| v.as_bool()),
618 previous_response_id: obj
619 .get("previous_response_id")
620 .and_then(|v| v.as_str())
621 .map(String::from),
622 truncation: obj.get("truncation").cloned(),
623 reasoning: obj.get("reasoning").cloned(),
624 include: obj.get("include").cloned(),
625 user: obj.get("user").and_then(|v| v.as_str()).map(String::from),
626 metadata: obj.get("metadata").cloned(),
627 service_tier: obj
628 .get("service_tier")
629 .and_then(|v| v.as_str())
630 .map(String::from),
631 parallel_tool_calls: decode_openai_or_anthropic_parallel_tool_calls(obj),
632 max_output_tokens: obj.get("max_output_tokens").and_then(|v| v.as_u64()),
633 max_tool_calls: obj.get("max_tool_calls").and_then(|v| v.as_u64()),
634 top_logprobs: obj.get("top_logprobs").and_then(|v| v.as_u64()),
635 stream: obj.get("stream").and_then(|v| v.as_bool()),
636 extra,
637 })
638 }
639
640 fn encode(&self, annotated: &AnnotatedLlmRequest, original: &LlmRequest) -> Result<LlmRequest> {
641 let mut content = original.content.clone();
642 let obj = content
643 .as_object_mut()
644 .ok_or_else(|| FlowError::Internal("original content is not an object".into()))?;
645
646 encode_openai_responses_input(obj, annotated)?;
647 if let Some(ref params) = annotated.params {
648 overlay_generation_params(obj, params);
649 }
650 encode_openai_responses_tools(obj, annotated)?;
651 overlay_openai_responses_fields(obj, annotated);
652 merge_openai_responses_extra_fields(obj, &annotated.extra);
653
654 Ok(LlmRequest {
655 headers: original.headers.clone(),
656 content,
657 })
658 }
659}
660
661pub struct OpenAIResponsesStreamingCodec {
694 state: std::sync::Arc<std::sync::Mutex<OpenAIResponsesStreamingState>>,
695}
696
697impl OpenAIResponsesStreamingCodec {
698 pub fn new() -> Self {
700 Self {
701 state: std::sync::Arc::new(std::sync::Mutex::new(
702 OpenAIResponsesStreamingState::default(),
703 )),
704 }
705 }
706}
707
708impl Default for OpenAIResponsesStreamingCodec {
709 fn default() -> Self {
710 Self::new()
711 }
712}
713
714impl super::streaming::StreamingCodec for OpenAIResponsesStreamingCodec {
715 fn collector(&self) -> crate::api::runtime::LlmCollectorFn {
716 let state = std::sync::Arc::clone(&self.state);
717 Box::new(move |event: Json| -> Result<()> {
718 let mut guard = state
719 .lock()
720 .unwrap_or_else(|poisoned| poisoned.into_inner());
721 guard.observe(&event);
722 Ok(())
723 })
724 }
725
726 fn finalizer(&self) -> crate::api::runtime::LlmFinalizerFn {
727 let state = std::sync::Arc::clone(&self.state);
728 Box::new(move || -> Json {
729 let mut guard = state
730 .lock()
731 .unwrap_or_else(|poisoned| poisoned.into_inner());
732 std::mem::take(&mut *guard).finalize()
733 })
734 }
735}
736
737#[derive(Debug, Default)]
738struct OpenAIResponsesStreamingState {
739 response: Option<serde_json::Map<String, Json>>,
742 items: std::collections::BTreeMap<usize, Json>,
746}
747
748impl OpenAIResponsesStreamingState {
749 fn observe(&mut self, event: &Json) {
750 let event_type = event.get("type").and_then(Json::as_str).unwrap_or("");
751 match event_type {
752 "response.created"
753 | "response.in_progress"
754 | "response.completed"
755 | "response.failed"
756 | "response.incomplete" => self.observe_response_snapshot(event),
757 "response.output_item.added" | "response.output_item.done" => {
758 self.observe_output_item(event);
759 }
760 _ => {}
764 }
765 }
766
767 fn observe_response_snapshot(&mut self, event: &Json) {
768 let Some(response) = event.get("response") else {
769 return;
770 };
771 if let Json::Object(map) = response {
772 self.response = Some(map.clone());
773 }
774 }
775
776 fn observe_output_item(&mut self, event: &Json) {
777 let Some(index) = event.get("output_index").and_then(Json::as_u64) else {
778 return;
779 };
780 let Some(item) = event.get("item") else {
781 return;
782 };
783 self.items.insert(index as usize, item.clone());
784 }
785
786 fn finalize(self) -> Json {
787 let mut output = self.response.unwrap_or_default();
788 let snapshot_output_empty = output
793 .get("output")
794 .and_then(Json::as_array)
795 .map(|arr| arr.is_empty())
796 .unwrap_or(true);
797 if snapshot_output_empty && !self.items.is_empty() {
798 let items: Vec<Json> = self.items.into_values().collect();
799 output.insert("output".to_string(), Json::Array(items));
800 }
801 Json::Object(output)
802 }
803}
804
805#[cfg(test)]
810#[path = "../../tests/unit/codec/openai_responses_tests.rs"]
811mod tests;