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