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