1use std::collections::HashMap;
7
8use serde_json::{Map, Value, json};
9
10use crate::codecs::common::{
11 collect_responses_reasoning_text, encrypted_reasoning_data, encrypted_reasoning_item_id,
12 is_anthropic_request, is_known_role_name, provider_extensions, reasoning_text_from_blocks,
13 text_from_blocks,
14};
15use crate::codecs::openai_chat::{decode_file_source, decode_image_source};
16use crate::codecs::openai_media::{
17 ImagePayload, file_payload, file_source_text, image_payload, image_source_text,
18};
19use crate::codecs::structured_output::{decode_openai_schema_enforcement, encode_openai_format};
20use crate::codecs::{
21 DecodedRequest, DecodedResponse, EncodedRequest, EncodedResponse, FormatCodec,
22};
23use crate::diagnostic::TranslationDiagnostic;
24use crate::error::{Result, TranslationError};
25use crate::format::{FormatId, WireFormat};
26use crate::llm::{
27 AggLlmResponse, ContentBlock, FileSource, ImageSource, LlmRequest, MediaSource, Message,
28 OutputParams, ProviderExtensions, ReasoningParams, ResponseOutput, Role, SamplingParams,
29 StopReason, Tool, ToolCall, ToolChoice, ToolDefinition, ToolResult, Usage,
30};
31use crate::policy::{DeterministicIdPolicy, TranslationPolicy};
32use crate::util::{
33 capture_request_preservation, capture_response_preservation, embed_preservation,
34 exact_preserved_request, exact_preserved_response, is_responses_native_input_item, json_string,
35 push_lossy, stable_id, string_value, validate_request_capabilities,
36};
37
38pub struct OpenAiResponsesCodec;
40
41impl FormatCodec for OpenAiResponsesCodec {
42 fn format(&self) -> FormatId {
43 WireFormat::OpenAiResponses.into()
44 }
45
46 fn decode_request(&self, body: &Value, policy: &TranslationPolicy) -> Result<DecodedRequest> {
47 let body = crate::util::object(body, "$")?;
48 let mut diagnostics = Vec::new();
49 let response_format = decode_responses_text_format(body.get("text"));
50 let mut request = LlmRequest {
51 model: body
52 .get("model")
53 .and_then(Value::as_str)
54 .filter(|model| !model.is_empty())
55 .map(ToOwned::to_owned),
56 output: OutputParams {
57 max_output_tokens: body.get("max_output_tokens").and_then(Value::as_u64),
58 is_schema_enforced: decode_openai_schema_enforcement(response_format.as_ref()),
59 response_format,
60 },
61 reasoning: ReasoningParams {
62 effort: body
63 .get("reasoning")
64 .and_then(Value::as_object)
65 .and_then(|object| object.get("effort"))
66 .and_then(Value::as_str)
67 .map(ToOwned::to_owned),
68 raw: body.get("reasoning").cloned(),
69 },
70 sampling: SamplingParams {
71 temperature: body.get("temperature").and_then(Value::as_f64),
72 top_p: body.get("top_p").and_then(Value::as_f64),
73 top_k: None,
74 },
75 stream: body.get("stream").and_then(Value::as_bool).unwrap_or(false),
76 preservation: capture_request_preservation(
77 WireFormat::OpenAiResponses,
78 &Value::Object(body.clone()),
79 policy,
80 ),
81 ..LlmRequest::default()
82 };
83 if let Some(instructions) = body.get("instructions").and_then(Value::as_str)
84 && !instructions.is_empty()
85 {
86 request.instructions.push(crate::llm::InstructionBlock {
87 role: Role::System,
88 content: vec![ContentBlock::Text {
89 text: instructions.to_string(),
90 }],
91 });
92 }
93 let stored_state = body
96 .get("previous_response_id")
97 .and_then(Value::as_str)
98 .is_some_and(|id| !id.is_empty());
99 let mut custom_call_outputs = Vec::new();
100 request.messages = decode_responses_input(
101 body.get("input").unwrap_or(&Value::String(String::new())),
102 stored_state,
103 &mut custom_call_outputs,
104 &mut diagnostics,
105 policy,
106 )?;
107 let mut tool_namespaces = Map::new();
108 let mut custom_tools = Map::new();
109 request.tools =
110 decode_responses_tools(body.get("tools"), &mut tool_namespaces, &mut custom_tools)
111 .into_iter()
112 .map(Tool::from)
113 .collect();
114 let mut additional_tools = Vec::new();
119 if let Some(items) = body.get("input").and_then(Value::as_array) {
120 for item in items {
121 if let Some(item) = item.as_object()
122 && item.get("type").and_then(Value::as_str) == Some("additional_tools")
123 && let Some(tools) = item.get("tools").and_then(Value::as_array)
124 {
125 request.tools.extend(
126 decode_responses_tools(
127 Some(&Value::Array(tools.clone())),
128 &mut tool_namespaces,
129 &mut custom_tools,
130 )
131 .into_iter()
132 .map(Tool::from),
133 );
134 additional_tools.extend(tools.iter().cloned());
135 }
136 }
137 }
138 request.tool_choice = body
139 .get("tool_choice")
140 .and_then(decode_responses_tool_choice);
141 request.extensions.fields = provider_extensions(
142 body,
143 &[
144 "model",
145 "instructions",
146 "input",
147 "tools",
148 "tool_choice",
149 "max_output_tokens",
150 "text",
151 "reasoning",
152 "temperature",
153 "top_p",
154 "stream",
155 ],
156 );
157 crate::codex_namespaces::attach_tool_namespaces(&mut request.extensions, tool_namespaces);
158 crate::codex_custom_tools::attach_custom_tools(&mut request.extensions, custom_tools);
159 crate::codex_custom_tools::attach_custom_call_outputs(
160 &mut request.extensions,
161 custom_call_outputs,
162 );
163 crate::codex_custom_tools::attach_additional_tools(
164 &mut request.extensions,
165 additional_tools,
166 );
167 Ok(DecodedRequest {
168 request,
169 diagnostics,
170 })
171 }
172
173 fn encode_request(
174 &self,
175 request: &LlmRequest,
176 policy: &TranslationPolicy,
177 ) -> Result<EncodedRequest> {
178 if let Some(body) =
179 exact_preserved_request(&request.preservation, WireFormat::OpenAiResponses, policy)
180 {
181 return Ok(EncodedRequest {
182 body,
183 diagnostics: Vec::new(),
184 });
185 }
186 let mut diagnostics = Vec::new();
187 validate_request_capabilities(request, &mut diagnostics, policy)?;
188 let mut body = Map::new();
189 if let Some(model) = &request.model {
190 body.insert("model".to_string(), Value::String(model.clone()));
191 }
192 let instructions = request
193 .instructions
194 .iter()
195 .flat_map(|instruction| instruction.content.iter())
196 .filter_map(|block| match block {
197 ContentBlock::Text { text } | ContentBlock::Refusal { text } => Some(text.as_str()),
198 _ => None,
199 })
200 .collect::<Vec<_>>()
201 .join("\n\n");
202 if !instructions.is_empty() {
203 body.insert("instructions".to_string(), Value::String(instructions));
204 }
205 body.insert(
206 "input".to_string(),
207 encode_responses_input(
208 &request.messages,
209 &mut diagnostics,
210 policy,
211 crate::codex_namespaces::tool_namespaces(&request.extensions),
212 &crate::codex_custom_tools::custom_tool_names(&request.extensions),
213 crate::codex_custom_tools::custom_call_outputs(&request.extensions),
214 )?,
215 );
216 if let Some(additional) = crate::codex_custom_tools::additional_tools(&request.extensions) {
217 let item = json!({
220 "type": "additional_tools",
221 "role": "developer",
222 "tools": additional,
223 });
224 match body.get_mut("input") {
225 Some(Value::Array(input)) => input.insert(0, item),
226 _ => body
227 .insert("input".to_string(), Value::Array(vec![item]))
228 .map_or((), |_| ()),
229 }
230 } else if !request.tools.is_empty() {
231 body.insert(
232 "tools".to_string(),
233 encode_responses_tools(
234 &request.tools,
235 crate::codex_namespaces::tool_namespaces(&request.extensions),
236 crate::codex_custom_tools::custom_tools(&request.extensions),
237 )?,
238 );
239 }
240 if let Some(choice) = &request.tool_choice
241 && let Some(choice) = encode_responses_tool_choice(
242 choice,
243 crate::codex_namespaces::tool_namespaces(&request.extensions),
244 )
245 {
246 body.insert("tool_choice".to_string(), choice);
247 }
248 if let Some(max_output_tokens) = request.output.max_output_tokens {
249 body.insert("max_output_tokens".to_string(), json!(max_output_tokens));
250 }
251 if let Some(response_format) =
252 encode_openai_format(&request.output, &mut diagnostics, policy)?
253 {
254 body.insert(
255 "text".to_string(),
256 json!({"format": encode_responses_text_format(&response_format)}),
257 );
258 }
259 let mut reasoning = if is_anthropic_request(request) {
262 let mut reasoning = Map::new();
263 if request
264 .reasoning
265 .raw
266 .as_ref()
267 .and_then(|thinking| thinking.get("type"))
268 .and_then(Value::as_str)
269 == Some("disabled")
270 {
271 reasoning.insert("effort".to_string(), json!("none"));
272 }
273 reasoning
274 } else {
275 request
276 .reasoning
277 .raw
278 .as_ref()
279 .and_then(Value::as_object)
280 .cloned()
281 .unwrap_or_default()
282 };
283 if let Some(effort) = &request.reasoning.effort {
284 reasoning.insert("effort".to_string(), json!(effort));
285 }
286 if !reasoning.is_empty() {
287 body.insert("reasoning".to_string(), Value::Object(reasoning));
288 }
289 if let Some(value) = request.sampling.temperature {
290 body.insert("temperature".to_string(), json!(value));
291 }
292 if let Some(value) = request.sampling.top_p {
293 body.insert("top_p".to_string(), json!(value));
294 }
295 if request.stream {
296 body.insert("stream".to_string(), Value::Bool(true));
297 }
298 copy_responses_request_extensions(&mut body, &request.extensions.fields);
299
300 let body = embed_preservation(Value::Object(body), &request.preservation, policy);
301 Ok(EncodedRequest { body, diagnostics })
302 }
303
304 fn decode_response(&self, body: &Value, policy: &TranslationPolicy) -> Result<DecodedResponse> {
305 let body = crate::util::object(body, "$")?;
306 if body.get("status").and_then(Value::as_str) == Some("failed") {
309 return Err(TranslationError::UpstreamFailure {
310 error: body
311 .get("error")
312 .filter(|error| error.is_object())
313 .cloned()
314 .unwrap_or_else(|| {
315 json!({ "message": "provider reported status \"failed\" without error details" })
316 }),
317 });
318 }
319 let mut diagnostics = Vec::new();
320 let mut content = Vec::new();
321 let mut role = Role::Assistant;
322 let mut stop_reason = None;
323 let mut has_output = false;
324 let mut url_citations = Vec::new();
325 let mut text_offset = 0;
326 if let Some(items) = body.get("output").and_then(Value::as_array) {
327 for item in items {
328 if let Some(output) = decode_responses_output_item(item, &mut diagnostics, policy)?
329 {
330 has_output = true;
331 role = output.role;
332 for mut citation in output.url_citations {
333 citation.start_index += text_offset;
334 citation.end_index += text_offset;
335 url_citations.push(citation);
336 }
337 text_offset += output
338 .content
339 .iter()
340 .map(|block| match block {
341 ContentBlock::Text { text } | ContentBlock::Refusal { text } => {
342 text.chars().count()
343 }
344 _ => 0,
345 })
346 .sum::<usize>();
347 content.extend(output.content);
348 if output.stop_reason.is_some() {
349 stop_reason = output.stop_reason;
350 }
351 }
352 }
353 }
354 if body.get("status").and_then(Value::as_str) == Some("incomplete")
356 && body
357 .get("incomplete_details")
358 .and_then(|details| details.get("reason"))
359 .and_then(Value::as_str)
360 == Some("max_output_tokens")
361 {
362 stop_reason = Some(StopReason::MaxTokens);
363 }
364 let outputs = vec![if has_output {
365 ResponseOutput {
366 url_citations,
367 role,
368 content,
369 stop_reason,
370 }
371 } else {
372 ResponseOutput {
373 url_citations: Vec::new(),
374 role: Role::Assistant,
375 content: vec![ContentBlock::Text {
376 text: String::new(),
377 }],
378 stop_reason: Some(StopReason::EndTurn),
379 }
380 }];
381 let response = AggLlmResponse {
382 id: body
383 .get("id")
384 .and_then(Value::as_str)
385 .map(ToOwned::to_owned),
386 model: body
387 .get("model")
388 .and_then(Value::as_str)
389 .map(ToOwned::to_owned),
390 outputs,
391 usage: decode_responses_usage(body.get("usage")),
392 extensions: ProviderExtensions {
393 fields: provider_extensions(
394 body,
395 &["id", "model", "object", "output", "usage", "status"],
396 ),
397 },
398 preservation: capture_response_preservation(
399 WireFormat::OpenAiResponses,
400 &Value::Object(body.clone()),
401 policy,
402 ),
403 };
404 Ok(DecodedResponse {
405 response,
406 diagnostics,
407 })
408 }
409
410 fn encode_response(
411 &self,
412 response: &AggLlmResponse,
413 _policy: &TranslationPolicy,
414 ) -> Result<EncodedResponse> {
415 if let Some(body) =
416 exact_preserved_response(&response.preservation, WireFormat::OpenAiResponses, _policy)
417 {
418 return Ok(EncodedResponse {
419 body,
420 diagnostics: Vec::new(),
421 });
422 }
423 let is_truncated = matches!(
424 response
425 .first_output()
426 .and_then(|output| output.stop_reason),
427 Some(StopReason::MaxTokens)
428 );
429 let status = if is_truncated {
430 "incomplete"
431 } else {
432 "completed"
433 };
434 let incomplete_details = is_truncated.then(|| json!({ "reason": "max_output_tokens" }));
435 Ok(EncodedResponse {
436 body: embed_preservation(
437 json!({
438 "id": response.id.clone().unwrap_or_else(|| "resp_switchyard".to_string()),
439 "object": "response",
440 "created_at": 0,
441 "model": response.model.clone().unwrap_or_else(|| "unknown".to_string()),
442 "status": status,
443 "incomplete_details": incomplete_details,
444 "output": encode_responses_output(&response.outputs),
445 "usage": encode_responses_usage(&response.usage),
446 "parallel_tool_calls": true,
447 "tool_choice": "auto",
448 "tools": [],
449 }),
450 &response.preservation,
451 _policy,
452 ),
453 diagnostics: Vec::new(),
454 })
455 }
456}
457
458fn decode_responses_input(
460 value: &Value,
461 stored_state: bool,
462 custom_call_outputs: &mut Vec<String>,
463 diagnostics: &mut Vec<TranslationDiagnostic>,
464 policy: &TranslationPolicy,
465) -> Result<Vec<Message>> {
466 match value {
467 Value::String(text) => Ok(vec![Message::text(Role::User, text)]),
468 Value::Array(items) => {
469 let mut messages = Vec::new();
470 let mut pending_tool_calls = Vec::new();
471 let mut pending_tool_outputs = Vec::new();
472 let mut deferred_messages = Vec::new();
473 let mut pending_reasoning: Vec<ContentBlock> = Vec::new();
474 for (index, item) in items.iter().enumerate() {
475 let Some(item) = item.as_object() else {
476 push_lossy(
477 diagnostics,
478 policy,
479 format!("Responses input item {index} is not an object"),
480 )?;
481 continue;
482 };
483 let item_type = match item.get("type") {
484 Some(Value::String(item_type)) => Some(item_type.as_str()),
485 Some(_) => {
486 return Err(TranslationError::InvalidType {
487 path: format!("$.input[{index}].type"),
488 expected: "a string",
489 });
490 }
491 None => None,
492 };
493 let is_message = item_type == Some("message")
494 || (item_type.is_none()
495 && item.contains_key("role")
496 && item.contains_key("content"));
497 match item_type {
498 _ if is_message => {
499 let role = request_role_from_responses(
500 item.get("role").and_then(Value::as_str),
501 &format!("$.input[{index}].role"),
502 )?;
503 let content_value = item.get("content").unwrap_or(&Value::Null);
504 if is_responses_native_input_item(content_value) {
505 return Err(TranslationError::InvalidValue {
506 path: format!("$.input[{index}].content.type"),
507 message: "native Responses items must be top-level input items"
508 .to_string(),
509 });
510 }
511 if let Some(content_index) = content_value.as_array().and_then(|blocks| {
512 blocks.iter().position(is_responses_native_input_item)
513 }) {
514 return Err(TranslationError::InvalidValue {
515 path: format!("$.input[{index}].content[{content_index}].type"),
516 message: "native Responses items must be top-level input items"
517 .to_string(),
518 });
519 }
520 let mut content = decode_responses_content(content_value);
521 if matches!(role, Role::System | Role::Developer)
525 && !(pending_tool_calls.is_empty()
526 && pending_tool_outputs.is_empty()
527 && pending_reasoning.is_empty())
528 {
529 deferred_messages.push(Message::new(role, content));
530 continue;
531 }
532 if role == Role::Assistant
536 && pending_tool_calls.is_empty()
537 && !pending_reasoning.is_empty()
538 {
539 let mut merged = std::mem::take(&mut pending_reasoning);
540 merged.append(&mut content);
541 content = merged;
542 if pending_tool_outputs.is_empty() {
544 messages.append(&mut deferred_messages);
545 }
546 }
547 if role != Role::Assistant {
548 flush_unattached_responses_reasoning(
549 &mut messages,
550 &mut pending_tool_calls,
551 &mut pending_tool_outputs,
552 &mut deferred_messages,
553 &mut pending_reasoning,
554 );
555 }
556 push_responses_non_tool_message(
557 &mut messages,
558 &mut pending_tool_calls,
559 &mut pending_tool_outputs,
560 &mut deferred_messages,
561 &mut pending_reasoning,
562 Message {
563 role,
564 content,
565 phase: if role == Role::Assistant {
566 item.get("phase").and_then(Value::as_str).map(str::to_owned)
567 } else {
568 None
569 },
570 },
571 );
572 }
573 Some("reasoning") => {
574 if !pending_tool_outputs.is_empty() {
578 flush_responses_tool_block(
579 &mut messages,
580 &mut pending_tool_calls,
581 &mut pending_tool_outputs,
582 &mut deferred_messages,
583 &mut pending_reasoning,
584 );
585 }
586 pending_reasoning.extend(decode_responses_reasoning_item(item));
587 }
588 Some("function_call") => {
589 if !pending_tool_outputs.is_empty() {
590 flush_responses_tool_block(
591 &mut messages,
592 &mut pending_tool_calls,
593 &mut pending_tool_outputs,
594 &mut deferred_messages,
595 &mut pending_reasoning,
596 );
597 }
598 let id = item
599 .get("call_id")
600 .and_then(Value::as_str)
601 .filter(|id| !id.is_empty())
602 .map(ToOwned::to_owned)
603 .unwrap_or_else(|| match &policy.deterministic_ids {
604 DeterministicIdPolicy::GenerateStable { prefix } => {
605 stable_id(prefix, index + 1)
606 }
607 DeterministicIdPolicy::Preserve => String::new(),
608 });
609 let name = item
613 .get("name")
614 .and_then(Value::as_str)
615 .unwrap_or_default()
616 .to_string();
617 let name = match item
618 .get("namespace")
619 .and_then(Value::as_str)
620 .filter(|namespace| !namespace.is_empty())
621 {
622 Some(namespace) => {
623 crate::codex_namespaces::qualified_tool_name(namespace, &name)
624 }
625 None => name,
626 };
627 pending_tool_calls.push(ToolCall {
628 id,
629 name,
630 arguments: item.get("arguments").cloned().unwrap_or_else(|| json!({})),
631 });
632 }
633 kind @ (Some("function_call_output") | Some("custom_tool_call_output")) => {
634 let tool_call_id = item
635 .get("call_id")
636 .and_then(Value::as_str)
637 .unwrap_or_default()
638 .to_string();
639 let content = match item.get("output") {
642 Some(output @ Value::Array(_)) => decode_responses_content(output),
643 output => vec![ContentBlock::Text {
644 text: output.map(json_string).unwrap_or_default(),
645 }],
646 };
647 let answers_pending_call = pending_tool_calls
652 .iter()
653 .any(|call| call.id == tool_call_id);
654 if !answers_pending_call && !stored_state {
655 let output_text = text_from_blocks(&content, " ");
656 let message = Message::text(
657 Role::User,
658 format!("Tool result {tool_call_id}: {output_text}"),
659 );
660 push_responses_non_tool_message(
661 &mut messages,
662 &mut pending_tool_calls,
663 &mut pending_tool_outputs,
664 &mut deferred_messages,
665 &mut pending_reasoning,
666 message,
667 );
668 continue;
669 }
670 if !answers_pending_call && kind == Some("custom_tool_call_output") {
673 custom_call_outputs.push(tool_call_id.clone());
674 }
675 pending_tool_outputs.push(ToolResult {
676 tool_call_id,
677 content,
678 is_error: None,
679 });
680 }
681 Some("custom_tool_call") => {
682 if !pending_tool_outputs.is_empty() {
685 flush_responses_tool_block(
686 &mut messages,
687 &mut pending_tool_calls,
688 &mut pending_tool_outputs,
689 &mut deferred_messages,
690 &mut pending_reasoning,
691 );
692 }
693 let id = item
694 .get("call_id")
695 .and_then(Value::as_str)
696 .filter(|id| !id.is_empty())
697 .map(ToOwned::to_owned)
698 .unwrap_or_else(|| match &policy.deterministic_ids {
699 DeterministicIdPolicy::GenerateStable { prefix } => {
700 stable_id(prefix, index + 1)
701 }
702 DeterministicIdPolicy::Preserve => String::new(),
703 });
704 let name = item
705 .get("name")
706 .and_then(Value::as_str)
707 .unwrap_or_default()
708 .to_string();
709 let input = item
710 .get("input")
711 .and_then(Value::as_str)
712 .unwrap_or_default()
713 .to_string();
714 pending_tool_calls.push(ToolCall {
715 id,
716 name,
717 arguments: json!({crate::codex_custom_tools::INPUT_ARGUMENT: input}),
718 });
719 }
720 None => {
721 return Err(TranslationError::InvalidValue {
722 path: format!("$.input[{index}].type"),
723 message: "missing type discriminator on a non-message input item"
724 .to_string(),
725 });
726 }
727 Some("additional_tools") => {}
730 _ => {
731 let message = Message::new(
732 match item_type {
733 Some(
734 "shell_call"
735 | "apply_patch_call"
736 | "computer_call"
737 | "image_generation_call",
738 ) => Role::Assistant,
739 Some(
740 "shell_call_output"
741 | "apply_patch_call_output"
742 | "computer_call_output",
743 ) => Role::Tool,
744 _ => Role::User,
745 },
746 vec![ContentBlock::Unknown {
747 provider: WireFormat::OpenAiResponses.into(),
748 raw: Value::Object(item.clone()),
749 }],
750 );
751 flush_unattached_responses_reasoning(
752 &mut messages,
753 &mut pending_tool_calls,
754 &mut pending_tool_outputs,
755 &mut deferred_messages,
756 &mut pending_reasoning,
757 );
758 push_responses_non_tool_message(
759 &mut messages,
760 &mut pending_tool_calls,
761 &mut pending_tool_outputs,
762 &mut deferred_messages,
763 &mut pending_reasoning,
764 message,
765 );
766 }
767 }
768 }
769 if !pending_tool_calls.is_empty() || !pending_tool_outputs.is_empty() {
770 flush_responses_tool_block(
771 &mut messages,
772 &mut pending_tool_calls,
773 &mut pending_tool_outputs,
774 &mut deferred_messages,
775 &mut pending_reasoning,
776 );
777 }
778 if !pending_reasoning.is_empty() {
781 messages.push(Message::new(Role::Assistant, pending_reasoning));
782 }
783 messages.append(&mut deferred_messages);
784 Ok(messages)
785 }
786 _ => Err(TranslationError::InvalidType {
787 path: "$.input".to_string(),
788 expected: "string or array",
789 }),
790 }
791}
792
793fn push_responses_non_tool_message(
795 messages: &mut Vec<Message>,
796 pending_tool_calls: &mut Vec<ToolCall>,
797 pending_tool_outputs: &mut Vec<ToolResult>,
798 deferred_messages: &mut Vec<Message>,
799 pending_reasoning: &mut Vec<ContentBlock>,
800 message: Message,
801) {
802 if !pending_tool_calls.is_empty() && pending_tool_outputs.is_empty() {
803 deferred_messages.push(message);
804 return;
805 }
806 if !pending_tool_calls.is_empty() || !pending_tool_outputs.is_empty() {
807 flush_responses_tool_block(
808 messages,
809 pending_tool_calls,
810 pending_tool_outputs,
811 deferred_messages,
812 pending_reasoning,
813 );
814 }
815 messages.push(message);
816}
817
818fn flush_unattached_responses_reasoning(
822 messages: &mut Vec<Message>,
823 pending_tool_calls: &mut Vec<ToolCall>,
824 pending_tool_outputs: &mut Vec<ToolResult>,
825 deferred_messages: &mut Vec<Message>,
826 pending_reasoning: &mut Vec<ContentBlock>,
827) {
828 if pending_reasoning.is_empty() || !pending_tool_calls.is_empty() {
829 return;
830 }
831 let message = Message::new(Role::Assistant, std::mem::take(pending_reasoning));
832 push_responses_non_tool_message(
833 messages,
834 pending_tool_calls,
835 pending_tool_outputs,
836 deferred_messages,
837 pending_reasoning,
838 message,
839 );
840 messages.append(deferred_messages);
842}
843
844fn flush_responses_tool_block(
848 messages: &mut Vec<Message>,
849 pending_tool_calls: &mut Vec<ToolCall>,
850 pending_tool_outputs: &mut Vec<ToolResult>,
851 deferred_messages: &mut Vec<Message>,
852 pending_reasoning: &mut Vec<ContentBlock>,
853) {
854 if !pending_tool_calls.is_empty() {
855 let mut content = std::mem::take(pending_reasoning);
856 content.extend(
857 std::mem::take(pending_tool_calls)
858 .into_iter()
859 .map(ContentBlock::ToolCall),
860 );
861 messages.push(Message::new(Role::Assistant, content));
862 }
863
864 for output in std::mem::take(pending_tool_outputs) {
865 messages.push(Message::new(
866 Role::User,
867 vec![ContentBlock::ToolResult(output)],
868 ));
869 }
870
871 messages.append(deferred_messages);
872}
873
874fn decode_responses_reasoning_item(item: &Map<String, Value>) -> Vec<ContentBlock> {
876 if let Some((text, signature)) = item
877 .get("encrypted_content")
878 .and_then(Value::as_str)
879 .and_then(super::decode_anthropic_thinking)
880 {
881 return vec![ContentBlock::Reasoning {
882 text,
883 signature: Some(signature),
884 details: Vec::new(),
885 }];
886 }
887 let mut parts = Vec::new();
888 collect_responses_reasoning_text(item.get("content"), &mut parts);
889 collect_responses_reasoning_text(item.get("summary"), &mut parts);
890 if let Some(text) = item.get("text").and_then(Value::as_str) {
891 parts.push(text.to_string());
892 }
893 let details = if item.get("encrypted_content").is_some() {
896 vec![Value::Object(item.clone())]
897 } else {
898 Vec::new()
899 };
900 vec![ContentBlock::Reasoning {
901 text: parts.join("\n"),
902 signature: None,
903 details,
904 }]
905}
906
907fn decode_responses_content(value: &Value) -> Vec<ContentBlock> {
909 match value {
910 Value::String(text) => vec![ContentBlock::Text { text: text.clone() }],
911 Value::Array(blocks) => {
912 let mut out = Vec::new();
913 for block in blocks {
914 let Some(block) = block.as_object() else {
915 continue;
916 };
917 match block.get("type").and_then(Value::as_str) {
918 Some("input_text") | Some("output_text") | Some("text") => {
919 out.push(ContentBlock::Text {
920 text: block
921 .get("text")
922 .and_then(Value::as_str)
923 .unwrap_or_default()
924 .to_string(),
925 });
926 }
927 Some("refusal") => out.push(ContentBlock::Refusal {
928 text: block
929 .get("refusal")
930 .and_then(Value::as_str)
931 .unwrap_or_default()
932 .to_string(),
933 }),
934 Some("reasoning_text") | Some("summary_text") => {
935 out.push(ContentBlock::Reasoning {
936 text: block
937 .get("text")
938 .and_then(Value::as_str)
939 .unwrap_or_default()
940 .to_string(),
941 signature: None,
942 details: Vec::new(),
943 });
944 }
945 Some("input_image") => {
946 if let Some(source) = decode_image_source(block) {
947 out.push(ContentBlock::Image { source });
948 }
949 }
950 Some("input_audio") => out.push(ContentBlock::Audio {
951 source: MediaSource::Raw(Value::Object(block.clone())),
952 }),
953 Some("input_file") => out.push(ContentBlock::File {
954 source: decode_file_source(block),
955 }),
956 _ => out.push(ContentBlock::Unknown {
957 provider: WireFormat::OpenAiResponses.into(),
958 raw: Value::Object(block.clone()),
959 }),
960 }
961 }
962 if out.is_empty() {
963 out.push(ContentBlock::Text {
964 text: String::new(),
965 });
966 }
967 out
968 }
969 Value::Null => vec![ContentBlock::Text {
970 text: String::new(),
971 }],
972 other => vec![ContentBlock::Text {
973 text: string_value(other).unwrap_or_default(),
974 }],
975 }
976}
977
978fn role_from_responses(role: Option<&str>) -> Role {
982 match role {
983 Some("assistant") => Role::Assistant,
984 Some("system") => Role::System,
985 Some("developer") => Role::Developer,
986 _ => Role::User,
987 }
988}
989
990fn request_role_from_responses(role: Option<&str>, path: &str) -> Result<Role> {
998 match role {
999 Some("assistant") => Ok(Role::Assistant),
1000 Some("system") => Ok(Role::System),
1001 Some("developer") => Ok(Role::Developer),
1002 None => Ok(Role::User),
1003 Some(other) if is_known_role_name(other) => Ok(Role::User),
1004 Some(other) => Err(TranslationError::unsupported_role(path, other)),
1005 }
1006}
1007
1008fn decode_responses_tools(
1016 value: Option<&Value>,
1017 namespaces: &mut Map<String, Value>,
1018 custom_tools: &mut Map<String, Value>,
1019) -> Vec<ToolDefinition> {
1020 let Some(tools) = value.and_then(Value::as_array) else {
1021 return Vec::new();
1022 };
1023 let mut out = Vec::new();
1024 for tool in tools {
1025 let Some(tool) = tool.as_object() else {
1026 continue;
1027 };
1028 if tool.get("type").and_then(Value::as_str) == Some("namespace") {
1029 let container = tool
1032 .get("name")
1033 .and_then(Value::as_str)
1034 .filter(|name| !name.is_empty());
1035 let description = tool.get("description").and_then(Value::as_str);
1036 for mut child in decode_responses_tools(tool.get("tools"), namespaces, custom_tools) {
1037 let already_qualified = namespaces.contains_key(&child.name);
1040 if let Some(container) = container
1041 && !already_qualified
1042 {
1043 let qualified =
1044 crate::codex_namespaces::qualified_tool_name(container, &child.name);
1045 crate::codex_namespaces::record_tool_namespace(
1046 namespaces,
1047 &qualified,
1048 container,
1049 description,
1050 );
1051 child.name = qualified;
1052 }
1053 out.push(child);
1054 }
1055 } else if tool.get("type").and_then(Value::as_str) == Some("custom") {
1056 if let Some(name) = tool
1063 .get("name")
1064 .and_then(Value::as_str)
1065 .filter(|name| !name.is_empty())
1066 {
1067 custom_tools.insert(name.to_string(), Value::Object(tool.clone()));
1068 out.push(ToolDefinition {
1069 name: name.to_string(),
1070 description: tool
1071 .get("description")
1072 .and_then(Value::as_str)
1073 .map(ToOwned::to_owned),
1074 parameters: crate::codex_custom_tools::input_schema(),
1075 strict: None,
1076 });
1077 }
1078 } else if tool.get("type").and_then(Value::as_str) == Some("function") {
1079 if let Some(function) = tool.get("function").and_then(Value::as_object) {
1080 if let Some(name) = function.get("name").and_then(Value::as_str)
1081 && !name.is_empty()
1082 {
1083 out.push(ToolDefinition {
1084 name: name.to_string(),
1085 description: function
1086 .get("description")
1087 .and_then(Value::as_str)
1088 .map(ToOwned::to_owned),
1089 parameters: function
1090 .get("parameters")
1091 .cloned()
1092 .unwrap_or_else(|| json!({})),
1093 strict: function.get("strict").and_then(Value::as_bool),
1094 });
1095 }
1096 } else {
1097 if !push_responses_function_tool(&mut out, tool) {
1098 push_responses_id_tool(&mut out, tool);
1099 }
1100 }
1101 } else if tool.get("type").is_none() && tool.contains_key("name") {
1102 push_responses_function_tool(&mut out, tool);
1103 } else {
1104 push_responses_id_tool(&mut out, tool);
1105 }
1106 }
1107 out
1108}
1109
1110fn push_responses_function_tool(
1112 out: &mut Vec<ToolDefinition>,
1113 tool: &serde_json::Map<String, Value>,
1114) -> bool {
1115 let Some(name) = tool.get("name").and_then(Value::as_str) else {
1116 return false;
1117 };
1118 if name.is_empty() {
1119 return false;
1120 }
1121
1122 out.push(ToolDefinition {
1123 name: name.to_string(),
1124 description: tool
1125 .get("description")
1126 .and_then(Value::as_str)
1127 .map(ToOwned::to_owned),
1128 parameters: tool.get("parameters").cloned().unwrap_or_else(|| json!({})),
1129 strict: tool.get("strict").and_then(Value::as_bool),
1130 });
1131 true
1132}
1133
1134fn push_responses_id_tool(
1136 out: &mut Vec<ToolDefinition>,
1137 tool: &serde_json::Map<String, Value>,
1138) -> bool {
1139 let Some(id) = tool.get("id").and_then(Value::as_str) else {
1140 return false;
1141 };
1142 if id.is_empty() {
1143 return false;
1144 }
1145
1146 out.push(ToolDefinition {
1147 name: id.to_string(),
1148 description: tool
1149 .get("description")
1150 .and_then(Value::as_str)
1151 .map(ToOwned::to_owned),
1152 parameters: tool
1153 .get("inputSchema")
1154 .and_then(Value::as_object)
1155 .and_then(|schema| schema.get("jsonSchema"))
1156 .cloned()
1157 .unwrap_or_else(|| json!({})),
1158 strict: None,
1159 });
1160 true
1161}
1162
1163fn decode_responses_tool_choice(value: &Value) -> Option<ToolChoice> {
1165 match value {
1166 Value::String(text) if text == "auto" => Some(ToolChoice::Auto),
1167 Value::String(text) if text == "required" => Some(ToolChoice::Required),
1168 Value::String(text) if text == "none" => Some(ToolChoice::None),
1169 Value::Object(object) if object.get("type").and_then(Value::as_str) == Some("function") => {
1170 object.get("name").and_then(Value::as_str).map(|name| {
1173 match object
1174 .get("namespace")
1175 .and_then(Value::as_str)
1176 .filter(|namespace| !namespace.is_empty())
1177 {
1178 Some(namespace) => ToolChoice::Tool {
1179 name: crate::codex_namespaces::qualified_tool_name(namespace, name),
1180 },
1181 None => ToolChoice::Tool {
1182 name: name.to_string(),
1183 },
1184 }
1185 })
1186 }
1187 Value::Object(object)
1188 if object.get("type").and_then(Value::as_str) == Some("allowed_tools") =>
1189 {
1190 Some(ToolChoice::Raw(value.clone()))
1191 }
1192 Value::Object(_) => None,
1193 _ => Some(ToolChoice::Raw(value.clone())),
1194 }
1195}
1196
1197fn decode_responses_text_format(value: Option<&Value>) -> Option<Value> {
1199 let format = value
1200 .and_then(Value::as_object)
1201 .and_then(|object| object.get("format"))
1202 .and_then(Value::as_object)?;
1203
1204 match format.get("type").and_then(Value::as_str) {
1205 Some("json_schema") => {
1206 if let Some(json_schema) = format.get("json_schema").and_then(Value::as_object) {
1207 return Some(json!({
1208 "type": "json_schema",
1209 "json_schema": Value::Object(json_schema.clone()),
1210 }));
1211 }
1212
1213 let mut json_schema = Map::new();
1214 for field in ["name", "description", "schema", "strict"] {
1215 if let Some(value) = format.get(field) {
1216 json_schema.insert(field.to_string(), value.clone());
1217 }
1218 }
1219 (!json_schema.is_empty()).then_some(json!({
1220 "type": "json_schema",
1221 "json_schema": Value::Object(json_schema),
1222 }))
1223 }
1224 Some("json_object") => Some(json!({"type": "json_object"})),
1225 Some("text") => Some(json!({"type": "text"})),
1226 _ => None,
1227 }
1228}
1229
1230fn encode_responses_text_format(response_format: &Value) -> Value {
1232 let Some(format) = response_format.as_object() else {
1233 return response_format.clone();
1234 };
1235 let Some(response_type) = format
1236 .get("type")
1237 .filter(|value| value.as_str() == Some("json_schema"))
1238 else {
1239 return response_format.clone();
1240 };
1241
1242 let Some(Value::Object(json_schema)) = format.get("json_schema") else {
1243 return response_format.clone();
1244 };
1245 if !matches!(json_schema.get("name"), Some(Value::String(_)))
1246 || !matches!(json_schema.get("schema"), Some(Value::Object(_)))
1247 || json_schema
1248 .get("description")
1249 .is_some_and(|value| !value.is_string())
1250 || json_schema
1251 .get("strict")
1252 .is_some_and(|value| !value.is_boolean())
1253 {
1254 return response_format.clone();
1255 }
1256
1257 let mut output = Map::new();
1258 output.insert("type".to_string(), response_type.clone());
1259 for field in ["name", "description", "schema", "strict"] {
1260 if let Some(value) = json_schema.get(field) {
1261 output.insert(field.to_string(), value.clone());
1262 }
1263 }
1264 Value::Object(output)
1265}
1266
1267fn encode_responses_input(
1269 messages: &[Message],
1270 diagnostics: &mut Vec<TranslationDiagnostic>,
1271 policy: &TranslationPolicy,
1272 namespaces: Option<&Map<String, Value>>,
1273 custom_tools: &std::collections::HashSet<String>,
1274 mut custom_call_ids: std::collections::HashSet<String>,
1275) -> Result<Value> {
1276 let mut encoded = Vec::new();
1283 let mut call_names: HashMap<&str, &str> = HashMap::new();
1286 for message in messages {
1287 for block in &message.content {
1288 if let ContentBlock::ToolCall(call) = block {
1289 call_names.insert(call.id.as_str(), call.name.as_str());
1290 }
1291 }
1292 }
1293 for message in messages {
1294 if let [ContentBlock::Unknown { provider, raw }] = message.content.as_slice()
1297 && provider.as_str() == WireFormat::OpenAiResponses.as_str()
1298 && is_responses_native_input_item(raw)
1299 {
1300 encoded.push(raw.clone());
1301 continue;
1302 }
1303 let content = message
1305 .content
1306 .iter()
1307 .filter(|block| {
1308 !matches!(
1309 block,
1310 ContentBlock::Reasoning {
1311 signature: Some(_),
1312 ..
1313 }
1314 )
1315 })
1316 .cloned()
1317 .collect::<Vec<_>>();
1318 if content.is_empty() {
1319 continue;
1320 }
1321 if content.iter().any(|block| {
1322 matches!(
1323 block,
1324 ContentBlock::ToolCall(_) | ContentBlock::ToolResult(_)
1325 )
1326 }) {
1327 let mut visible_content = Vec::new();
1328 for block in &content {
1329 if let Some(item) = encode_responses_special_input(
1330 block,
1331 namespaces,
1332 &call_names,
1333 custom_tools,
1334 &mut custom_call_ids,
1335 diagnostics,
1336 policy,
1337 )? {
1338 if !visible_content.is_empty() {
1339 let content =
1340 encode_responses_content(&visible_content, diagnostics, policy)?;
1341 encoded.push(encode_responses_message(message, content));
1342 visible_content.clear();
1343 }
1344 encoded.push(item);
1345 } else if !matches!(block, ContentBlock::Reasoning { .. }) {
1346 visible_content.push(block.clone());
1347 }
1348 }
1349 if !visible_content.is_empty() {
1350 let content = encode_responses_content(&visible_content, diagnostics, policy)?;
1351 encoded.push(encode_responses_message(message, content));
1352 }
1353 continue;
1354 }
1355 let mut visible_content = Vec::new();
1356 let mut emitted_special = false;
1357 let mut omitted_reasoning = false;
1358 for block in &content {
1359 if let Some(item) = encode_responses_special_input(
1360 block,
1361 namespaces,
1362 &call_names,
1363 custom_tools,
1364 &mut custom_call_ids,
1365 diagnostics,
1366 policy,
1367 )? {
1368 encoded.push(item);
1369 emitted_special = true;
1370 } else if !matches!(block, ContentBlock::Reasoning { .. }) {
1371 visible_content.push(block.clone());
1372 } else {
1373 omitted_reasoning = true;
1374 }
1375 }
1376 if !visible_content.is_empty()
1377 || (!emitted_special && !omitted_reasoning)
1378 || (message.role == Role::Assistant && message.phase.is_some())
1379 {
1380 let content = encode_responses_content(&visible_content, diagnostics, policy)?;
1381 encoded.push(encode_responses_message(message, content));
1382 }
1383 }
1384 pair_tool_calls_with_outputs(&mut encoded);
1385 Ok(Value::Array(encoded))
1386}
1387
1388fn encode_responses_message(message: &Message, content: Value) -> Value {
1390 let mut item = json!({
1391 "type": "message",
1392 "role": role_to_responses(message.role),
1393 "content": content,
1394 });
1395 if let (Role::Assistant, Some(phase)) = (message.role, &message.phase) {
1396 item["phase"] = json!(phase);
1397 }
1398 item
1399}
1400
1401fn pair_tool_calls_with_outputs(items: &mut Vec<Value>) {
1405 let call_id = |item: &Value, kind: &str| {
1406 (item.get("type").and_then(Value::as_str) == Some(kind))
1407 .then(|| {
1408 item.get("call_id")
1409 .and_then(Value::as_str)
1410 .map(str::to_owned)
1411 })
1412 .flatten()
1413 };
1414 let mut index = 0;
1415 while index < items.len() {
1416 let (id, output_kind) = match call_id(&items[index], "function_call") {
1418 Some(id) => (id, "function_call_output"),
1419 None => match call_id(&items[index], "custom_tool_call") {
1420 Some(id) => (id, "custom_tool_call_output"),
1421 None => {
1422 index += 1;
1423 continue;
1424 }
1425 },
1426 };
1427 let output = items
1428 .iter()
1429 .skip(index + 1)
1430 .take_while(|item| item.get("type").and_then(Value::as_str) != Some("message"))
1431 .position(|item| call_id(item, output_kind).as_deref() == Some(&id))
1432 .map(|offset| index + 1 + offset);
1433 if let Some(output) = output
1434 && output != index + 1
1435 {
1436 let item = items.remove(output);
1437 items.insert(index + 1, item);
1438 }
1439 index += 1;
1440 }
1441}
1442
1443fn encode_responses_special_input(
1445 block: &ContentBlock,
1446 namespaces: Option<&Map<String, Value>>,
1447 call_names: &HashMap<&str, &str>,
1448 custom_tools: &std::collections::HashSet<String>,
1449 custom_call_ids: &mut std::collections::HashSet<String>,
1450 diagnostics: &mut Vec<TranslationDiagnostic>,
1451 policy: &TranslationPolicy,
1452) -> Result<Option<Value>> {
1453 Ok(match block {
1454 ContentBlock::Reasoning {
1455 text,
1456 signature: None,
1457 details,
1458 } => encode_responses_reasoning_input(text, details),
1459 ContentBlock::ToolCall(call) if custom_tools.contains(&call.name) => {
1460 custom_call_ids.insert(call.id.clone());
1462 Some(json!({
1463 "type": "custom_tool_call",
1464 "call_id": call.id,
1465 "name": call.name,
1466 "input": crate::codex_custom_tools::input_from_arguments(&call.arguments),
1467 }))
1468 }
1469 ContentBlock::ToolCall(call) => {
1470 let (name, namespace) = namespaces
1473 .and_then(|namespaces| {
1474 crate::codex_namespaces::split_qualified_name(namespaces, &call.name)
1475 })
1476 .map_or((call.name.as_str(), None), |(name, namespace)| {
1477 (name, Some(namespace))
1478 });
1479 let mut item = json!({
1480 "type": "function_call",
1481 "call_id": call.id,
1482 "name": name,
1483 "arguments": json_string(&call.arguments),
1484 });
1485 if let Some(namespace) = namespace {
1486 item["namespace"] = Value::String(namespace.to_string());
1487 }
1488 Some(item)
1489 }
1490 ContentBlock::ToolResult(result) => {
1491 let mut item = json!({
1494 "type": if custom_call_ids.contains(&result.tool_call_id) {
1495 "custom_tool_call_output"
1496 } else {
1497 "function_call_output"
1498 },
1499 "call_id": result.tool_call_id,
1500 "output": encode_responses_tool_output(&result.content, diagnostics, policy)?,
1501 });
1502 if let Some(name) = call_names.get(result.tool_call_id.as_str()) {
1505 let name = namespaces
1506 .and_then(|namespaces| {
1507 crate::codex_namespaces::split_qualified_name(namespaces, name)
1508 })
1509 .map_or(*name, |(name, _)| name);
1510 item["name"] = Value::String(name.to_string());
1511 }
1512 Some(item)
1513 }
1514 _ => None,
1515 })
1516}
1517
1518fn encode_responses_reasoning_input(text: &str, details: &[Value]) -> Option<Value> {
1521 for detail in details {
1522 let Some(detail) = detail.as_object() else {
1523 continue;
1524 };
1525 if detail.get("type").and_then(Value::as_str) != Some("reasoning") {
1526 continue;
1527 }
1528 let mut item = Map::new();
1529 item.insert("type".to_string(), Value::String("reasoning".to_string()));
1530 for field in ["id", "summary", "encrypted_content"] {
1531 if let Some(value) = detail.get(field) {
1532 item.insert(field.to_string(), value.clone());
1533 }
1534 }
1535 item.entry("summary".to_string())
1536 .or_insert_with(|| Value::Array(Vec::new()));
1537 return Some(Value::Object(item));
1538 }
1539
1540 if text.is_empty() {
1541 return None;
1542 }
1543 Some(json!({
1544 "type": "reasoning",
1545 "summary": [{"type": "summary_text", "text": text}],
1546 }))
1547}
1548
1549fn role_to_responses(role: Role) -> &'static str {
1551 match role {
1552 Role::Assistant => "assistant",
1553 Role::System => "system",
1554 Role::Developer => "developer",
1555 Role::User | Role::Tool => "user",
1556 }
1557}
1558
1559fn encode_responses_content(
1561 content: &[ContentBlock],
1562 diagnostics: &mut Vec<TranslationDiagnostic>,
1563 policy: &TranslationPolicy,
1564) -> Result<Value> {
1565 let has_non_text = content.iter().any(|block| {
1566 !matches!(
1567 block,
1568 ContentBlock::Text { .. }
1569 | ContentBlock::Refusal { .. }
1570 | ContentBlock::Reasoning { .. }
1571 )
1572 });
1573 if !has_non_text {
1574 return Ok(Value::String(text_from_blocks(content, "\n")));
1575 }
1576 let mut blocks = Vec::new();
1577 for block in content {
1578 match block {
1579 ContentBlock::Text { text } => blocks.push(json!({"type": "input_text", "text": text})),
1580 ContentBlock::Refusal { text } => {
1581 blocks.push(json!({"type": "refusal", "refusal": text}));
1582 }
1583 ContentBlock::Image { source } => match responses_image_part(source) {
1584 Some(part) => blocks.push(part),
1585 None => {
1586 push_lossy(
1587 diagnostics,
1588 policy,
1589 "Responses codec could not map image content",
1590 )?;
1591 blocks.push(json!({"type": "input_text", "text": image_source_text(source)}));
1592 }
1593 },
1594 ContentBlock::Audio { source } => {
1595 blocks.push(crate::codecs::openai_media::audio_part(source)?);
1596 }
1597 ContentBlock::Video { source } => blocks.push(match source {
1598 MediaSource::Raw(raw) => json!({"type": "input_text", "text": json_string(raw)}),
1599 MediaSource::Url { url, media_type } => json!({
1600 "type": "input_video",
1601 "video_url": url,
1602 "media_type": media_type,
1603 }),
1604 MediaSource::Base64 { media_type, data } => json!({
1605 "type": "input_video",
1606 "video": {"media_type": media_type, "data": data},
1607 }),
1608 }),
1609 ContentBlock::File { source } => match responses_file_part(source) {
1610 Some(part) => blocks.push(part),
1611 None => {
1612 push_lossy(
1613 diagnostics,
1614 policy,
1615 "Responses codec could not map file content",
1616 )?;
1617 blocks.push(json!({"type": "input_text", "text": file_source_text(source)}));
1618 }
1619 },
1620 ContentBlock::Mcp(_) => {
1621 return Err(TranslationError::LossyConversion(
1622 "MCP history encoding is not implemented".into(),
1623 ));
1624 }
1625 ContentBlock::Unknown { raw, .. } => {
1626 push_lossy(
1627 diagnostics,
1628 policy,
1629 "unknown content block encoded as text for Responses",
1630 )?;
1631 blocks.push(json!({"type": "input_text", "text": json_string(raw)}));
1632 }
1633 ContentBlock::Reasoning { .. }
1634 | ContentBlock::ToolCall(_)
1635 | ContentBlock::ToolResult(_) => {}
1636 }
1637 }
1638 Ok(Value::Array(blocks))
1639}
1640
1641fn encode_responses_tool_output(
1644 content: &[ContentBlock],
1645 diagnostics: &mut Vec<TranslationDiagnostic>,
1646 policy: &TranslationPolicy,
1647) -> Result<Value> {
1648 let text_only = content.iter().all(|block| {
1649 matches!(
1650 block,
1651 ContentBlock::Text { .. } | ContentBlock::Refusal { .. }
1652 )
1653 });
1654 if text_only {
1655 return Ok(Value::String(text_from_blocks(content, " ")));
1656 }
1657 encode_responses_content(content, diagnostics, policy)
1658}
1659
1660fn responses_image_part(source: &ImageSource) -> Option<Value> {
1661 if let ImageSource::Raw(raw) = source
1662 && raw.get("type").and_then(Value::as_str) == Some("input_image")
1663 && raw.get("file_id").and_then(Value::as_str).is_some()
1664 {
1665 return Some(raw.clone());
1666 }
1667 let ImagePayload { url, detail } = image_payload(source)?;
1668 let mut part = json!({"type": "input_image"});
1669 part["image_url"] = Value::String(url);
1670 if let Some(detail) = detail {
1671 part["detail"] = Value::String(detail);
1672 }
1673 Some(part)
1674}
1675
1676fn responses_file_part(source: &FileSource) -> Option<Value> {
1677 let mut part = file_payload(source)?;
1678 part.insert("type".to_string(), Value::String("input_file".to_string()));
1679 Some(Value::Object(part))
1680}
1681
1682fn encode_responses_tools(
1688 tools: &[Tool],
1689 namespaces: Option<&Map<String, Value>>,
1690 custom_tools: Option<&Map<String, Value>>,
1691) -> Result<Value> {
1692 let mut out: Vec<Value> = Vec::new();
1693 let mut containers: Vec<(String, Vec<Value>)> = Vec::new();
1694 for tool in tools {
1695 let tool = tool.function().ok_or_else(|| {
1696 TranslationError::LossyConversion(
1697 "MCP and provider tool encoding is not implemented".into(),
1698 )
1699 })?;
1700 if let Some(custom) = custom_tools.and_then(|custom| custom.get(&tool.name)) {
1702 out.push(custom.clone());
1703 continue;
1704 }
1705 let mut item = json!({
1706 "type": "function",
1707 "name": tool.name,
1708 "description": tool.description.clone().unwrap_or_default(),
1709 "parameters": tool.parameters,
1710 });
1711 if let Some(strict) = tool.strict {
1712 item["strict"] = Value::Bool(strict);
1713 }
1714 let split = namespaces.and_then(|namespaces| {
1715 crate::codex_namespaces::split_qualified_name(namespaces, &tool.name)
1716 });
1717 match split {
1718 None => out.push(item),
1719 Some((name, namespace)) => {
1720 item["name"] = Value::String(name.to_string());
1721 match containers
1722 .iter_mut()
1723 .find(|(existing, _)| *existing == namespace)
1724 {
1725 Some((_, children)) => children.push(item),
1726 None => containers.push((namespace.to_string(), vec![item])),
1727 }
1728 }
1729 }
1730 }
1731 for (namespace, children) in containers {
1732 let description = namespaces
1733 .and_then(|namespaces| {
1734 crate::codex_namespaces::namespace_description(namespaces, &namespace)
1735 })
1736 .unwrap_or_default();
1737 out.push(json!({
1738 "type": "namespace",
1739 "name": namespace,
1740 "description": description,
1741 "tools": children,
1742 }));
1743 }
1744 Ok(Value::Array(out))
1745}
1746
1747fn encode_responses_tool_choice(
1749 choice: &ToolChoice,
1750 namespaces: Option<&Map<String, Value>>,
1751) -> Option<Value> {
1752 match choice {
1753 ToolChoice::Auto => Some(Value::String("auto".to_string())),
1754 ToolChoice::Required => Some(Value::String("required".to_string())),
1755 ToolChoice::None => Some(Value::String("none".to_string())),
1756 ToolChoice::Tool { name } => {
1757 let split = namespaces.and_then(|namespaces| {
1758 crate::codex_namespaces::split_qualified_name(namespaces, name)
1759 });
1760 Some(match split {
1761 Some((tool, namespace)) => {
1762 json!({"type": "function", "name": tool, "namespace": namespace})
1763 }
1764 None => json!({"type": "function", "name": name}),
1765 })
1766 }
1767 ToolChoice::Raw(value) => Some(value.clone()),
1768 }
1769}
1770
1771fn decode_responses_output_item(
1773 item: &Value,
1774 diagnostics: &mut Vec<TranslationDiagnostic>,
1775 policy: &TranslationPolicy,
1776) -> Result<Option<ResponseOutput>> {
1777 let Some(item) = item.as_object() else {
1778 push_lossy(
1779 diagnostics,
1780 policy,
1781 "Responses output item is not an object",
1782 )?;
1783 return Ok(None);
1784 };
1785 match item.get("type").and_then(Value::as_str) {
1786 Some("message") => Ok(Some(ResponseOutput {
1787 url_citations: decode_responses_url_citations(item.get("content")),
1788 role: role_from_responses(item.get("role").and_then(Value::as_str)),
1789 content: decode_responses_content(item.get("content").unwrap_or(&Value::Null)),
1790 stop_reason: Some(StopReason::EndTurn),
1791 })),
1792 Some("function_call") => Ok(Some(ResponseOutput {
1793 url_citations: Vec::new(),
1794 role: Role::Assistant,
1795 content: vec![ContentBlock::ToolCall(ToolCall {
1796 id: item
1797 .get("call_id")
1798 .and_then(Value::as_str)
1799 .unwrap_or_default()
1800 .to_string(),
1801 name: item
1802 .get("name")
1803 .and_then(Value::as_str)
1804 .unwrap_or_default()
1805 .to_string(),
1806 arguments: item.get("arguments").cloned().unwrap_or_else(|| json!({})),
1807 })],
1808 stop_reason: Some(StopReason::ToolUse),
1809 })),
1810 Some("custom_tool_call") => Ok(Some(ResponseOutput {
1811 url_citations: Vec::new(),
1812 role: Role::Assistant,
1813 content: vec![ContentBlock::ToolCall(ToolCall {
1814 id: item
1815 .get("call_id")
1816 .and_then(Value::as_str)
1817 .unwrap_or_default()
1818 .to_string(),
1819 name: item
1820 .get("name")
1821 .and_then(Value::as_str)
1822 .unwrap_or_default()
1823 .to_string(),
1824 arguments: json!({
1825 crate::codex_custom_tools::INPUT_ARGUMENT:
1826 item.get("input").and_then(Value::as_str).unwrap_or_default()
1827 }),
1828 })],
1829 stop_reason: Some(StopReason::ToolUse),
1830 })),
1831 Some("reasoning") => Ok(Some(ResponseOutput {
1832 url_citations: Vec::new(),
1833 role: Role::Assistant,
1834 content: decode_responses_reasoning_item(item),
1835 stop_reason: None,
1836 })),
1837 Some(kind) if super::is_native_output(kind) => Ok(Some(ResponseOutput {
1838 url_citations: Vec::new(),
1839 role: Role::Assistant,
1840 content: vec![ContentBlock::Unknown {
1841 provider: WireFormat::OpenAiResponses.into(),
1842 raw: Value::Object(item.clone()),
1843 }],
1844 stop_reason: None,
1845 })),
1846 _ => Ok(None),
1847 }
1848}
1849
1850fn decode_responses_url_citations(content: Option<&Value>) -> Vec<crate::UrlCitation> {
1851 let mut citations = Vec::new();
1852 let mut text_offset = 0;
1853 for part in content.and_then(Value::as_array).into_iter().flatten() {
1854 for annotation in part
1855 .get("annotations")
1856 .and_then(Value::as_array)
1857 .into_iter()
1858 .flatten()
1859 {
1860 if annotation["type"] == "url_citation"
1861 && let Ok(mut citation) =
1862 serde_json::from_value::<crate::UrlCitation>(annotation.clone())
1863 {
1864 citation.start_index += text_offset;
1865 citation.end_index += text_offset;
1866 citations.push(citation);
1867 }
1868 }
1869 let text = match part.get("type").and_then(Value::as_str) {
1870 Some("input_text" | "output_text" | "text") => part.get("text"),
1871 Some("refusal") => part.get("refusal"),
1872 _ => None,
1873 };
1874 text_offset += text
1875 .and_then(Value::as_str)
1876 .unwrap_or_default()
1877 .chars()
1878 .count();
1879 }
1880 citations
1881}
1882
1883fn encode_responses_output(outputs: &[ResponseOutput]) -> Value {
1885 Value::Array(
1886 outputs
1887 .iter()
1888 .flat_map(|output| {
1889 let has_output_items = output.content.iter().any(|block| {
1890 matches!(block, ContentBlock::ToolCall(_))
1891 || matches!(block, ContentBlock::Unknown { provider, raw }
1892 if provider.as_str() == WireFormat::OpenAiResponses.as_str()
1893 && raw["type"].as_str().is_some_and(super::is_native_output))
1894 });
1895 let text = text_from_blocks(&output.content, "");
1896 let reasoning = reasoning_text_from_blocks(&output.content, "\n");
1897 let status = if matches!(output.stop_reason, Some(StopReason::MaxTokens)) {
1898 "incomplete"
1899 } else {
1900 "completed"
1901 };
1902 let mut items = Vec::new();
1903
1904 let encrypted_reasoning = output.content.iter().find_map(|block| match block {
1905 ContentBlock::Reasoning { details, .. } => encrypted_reasoning_data(details),
1906 _ => None,
1907 });
1908 let encrypted_reasoning_id = output.content.iter().find_map(|block| match block {
1909 ContentBlock::Reasoning { details, .. } => encrypted_reasoning_item_id(details),
1910 _ => None,
1911 });
1912 let has_signed_reasoning = output.content.iter().any(|block| {
1913 matches!(
1914 block,
1915 ContentBlock::Reasoning {
1916 signature: Some(_),
1917 ..
1918 }
1919 )
1920 });
1921 if has_signed_reasoning {
1922 for (index, block) in output.content.iter().enumerate() {
1923 if let ContentBlock::Reasoning {
1924 text,
1925 signature,
1926 details,
1927 } = block
1928 {
1929 let encrypted = signature
1930 .as_deref()
1931 .map(|signature| super::encode_anthropic_thinking(text, signature))
1932 .or_else(|| encrypted_reasoning_data(details));
1933 let id = encrypted_reasoning_item_id(details)
1934 .unwrap_or_else(|| format!("rs_switchyard_{index}"));
1935 items.push(encode_responses_reasoning_output(
1936 text,
1937 encrypted.as_deref(),
1938 Some(&id),
1939 ));
1940 }
1941 }
1942 } else if !reasoning.is_empty() || encrypted_reasoning.is_some() {
1943 items.push(encode_responses_reasoning_output(
1944 &reasoning,
1945 encrypted_reasoning.as_deref(),
1946 encrypted_reasoning_id.as_deref(),
1947 ));
1948 }
1949
1950 if !text.is_empty() || (!has_output_items && reasoning.is_empty()) {
1951 items.push(json!({
1952 "type": "message",
1953 "id": "msg_switchyard",
1954 "status": status,
1955 "role": role_to_responses(output.role),
1956 "content": [{
1957 "type": "output_text",
1958 "text": text,
1959 "annotations": output.url_citations.iter().map(|citation| json!({
1960 "type": "url_citation",
1961 "start_index": citation.start_index,
1962 "end_index": citation.end_index,
1963 "url": citation.url,
1964 "title": citation.title,
1965 })).collect::<Vec<_>>(),
1966 }],
1967 }));
1968 }
1969
1970 items.extend(output.content.iter().filter_map(|block| match block {
1971 ContentBlock::ToolCall(call) => Some(json!({
1972 "type": "function_call",
1973 "call_id": call.id,
1974 "name": call.name,
1975 "arguments": json_string_python_style(&call.arguments),
1976 })),
1977 ContentBlock::Unknown { provider, raw }
1978 if provider.as_str() == WireFormat::OpenAiResponses.as_str()
1979 && raw["type"].as_str().is_some_and(super::is_native_output) =>
1980 {
1981 Some(raw.clone())
1982 }
1983 _ => None,
1984 }));
1985
1986 items
1987 })
1988 .collect(),
1989 )
1990}
1991
1992fn encode_responses_reasoning_output(
1995 text: &str,
1996 encrypted: Option<&str>,
1997 item_id: Option<&str>,
1998) -> Value {
1999 let mut summary = Vec::new();
2001 if !text.is_empty() {
2002 summary.push(json!({"type": "summary_text", "text": text}));
2003 }
2004 let mut item = json!({
2006 "type": "reasoning",
2007 "id": item_id.unwrap_or("rs_switchyard"),
2008 "status": "completed",
2009 "summary": summary,
2010 });
2011 if let Some(encrypted) = encrypted {
2012 item["encrypted_content"] = Value::String(encrypted.to_string());
2013 }
2014 item
2015}
2016
2017fn json_string_python_style(value: &Value) -> String {
2019 match value {
2020 Value::Null => "null".to_string(),
2021 Value::Bool(value) => value.to_string(),
2022 Value::Number(value) => value.to_string(),
2023 Value::String(value) => serde_json::to_string(value).unwrap_or_else(|_| value.clone()),
2024 Value::Array(items) => {
2025 let body = items
2026 .iter()
2027 .map(json_string_python_style)
2028 .collect::<Vec<_>>()
2029 .join(", ");
2030 format!("[{body}]")
2031 }
2032 Value::Object(object) => {
2033 let body = object
2034 .iter()
2035 .map(|(key, value)| {
2036 let key = serde_json::to_string(key).unwrap_or_else(|_| format!("\"{key}\""));
2037 format!("{key}: {}", json_string_python_style(value))
2038 })
2039 .collect::<Vec<_>>()
2040 .join(", ");
2041 format!("{{{body}}}")
2042 }
2043 }
2044}
2045
2046fn decode_responses_usage(value: Option<&Value>) -> Usage {
2048 let Some(value) = value.and_then(Value::as_object) else {
2049 return Usage::default();
2050 };
2051 let aggregate_input_tokens = value
2052 .get("input_tokens")
2053 .or_else(|| value.get("prompt_tokens"))
2054 .and_then(Value::as_u64);
2055 let cached_input_tokens = value
2056 .get("input_tokens_details")
2057 .or_else(|| value.get("prompt_tokens_details"))
2058 .and_then(|details| details.get("cached_tokens"))
2059 .and_then(Value::as_u64);
2060 let cache_creation_input_tokens = value
2061 .get("input_tokens_details")
2062 .and_then(|details| details.get("cache_write_tokens"))
2063 .and_then(Value::as_u64)
2064 .or_else(|| {
2065 value.get("prompt_tokens_details").and_then(|details| {
2066 details
2067 .get("cache_write_tokens")
2068 .and_then(Value::as_u64)
2069 .or_else(|| details.get("cache_creation_tokens").and_then(Value::as_u64))
2070 })
2071 });
2072 let input_tokens = aggregate_input_tokens.map(|tokens| {
2073 tokens
2074 .saturating_sub(cached_input_tokens.unwrap_or(0))
2075 .saturating_sub(cache_creation_input_tokens.unwrap_or(0))
2076 });
2077 let output_tokens = value
2078 .get("output_tokens")
2079 .or_else(|| value.get("completion_tokens"))
2080 .and_then(Value::as_u64);
2081 Usage {
2082 provider_cost: crate::codecs::common::provider_cost(value),
2083 input_tokens,
2084 cache: crate::codecs::common::cache_usage_details(
2085 value,
2086 cached_input_tokens,
2087 cache_creation_input_tokens,
2088 ),
2089 output_tokens,
2090 total_tokens: value
2091 .get("total_tokens")
2092 .and_then(Value::as_u64)
2093 .or_else(|| {
2094 aggregate_input_tokens
2095 .zip(output_tokens)
2096 .map(|(input, output)| input + output)
2097 }),
2098 reasoning_tokens: value
2099 .get("output_tokens_details")
2100 .and_then(|details| details.get("reasoning_tokens"))
2101 .or_else(|| {
2102 value
2103 .get("completion_tokens_details")
2104 .and_then(|details| details.get("reasoning_tokens"))
2105 })
2106 .and_then(Value::as_u64),
2107 }
2108}
2109
2110pub(super) fn encode_responses_usage(usage: &Usage) -> Value {
2112 let input_tokens = usage.input_tokens.unwrap_or(0)
2113 + usage.cached_input_tokens().unwrap_or(0)
2114 + usage.cache_creation_input_tokens().unwrap_or(0);
2115 crate::codecs::common::encode_usage_metadata(
2120 json!({
2121 "input_tokens": input_tokens,
2122 "output_tokens": usage.output_tokens.unwrap_or(0),
2123 "total_tokens": usage
2124 .total_tokens
2125 .or_else(|| Some(input_tokens + usage.output_tokens.unwrap_or(0)))
2126 .unwrap_or(0),
2127 "input_tokens_details": {
2128 "cached_tokens": usage.cached_input_tokens().unwrap_or(0),
2129 "cache_write_tokens": usage.cache_creation_input_tokens().unwrap_or(0),
2130 },
2131 "output_tokens_details": {"reasoning_tokens": usage.reasoning_tokens.unwrap_or(0)},
2132 }),
2133 usage,
2134 )
2135}
2136
2137fn copy_responses_request_extensions(
2145 body: &mut Map<String, Value>,
2146 extensions: &Map<String, Value>,
2147) {
2148 for field in [
2149 "include",
2150 "metadata",
2151 "parallel_tool_calls",
2152 "previous_response_id",
2153 "prompt_cache_key",
2154 "prompt_cache_retention",
2155 "safety_identifier",
2156 "service_tier",
2157 "store",
2158 "user",
2159 ] {
2160 if let Some(value) = extensions.get(field) {
2161 body.entry(field.to_string())
2162 .or_insert_with(|| value.clone());
2163 }
2164 }
2165}