1use std::collections::HashMap;
7
8use serde_json::{Map, Value, json};
9
10use crate::codecs::common::{
11 collect_responses_reasoning_text, encrypted_reasoning_data, encrypted_reasoning_item_id,
12 is_anthropic_request, is_known_role_name, provider_extensions, reasoning_text_from_blocks,
13 text_from_blocks,
14};
15use crate::codecs::openai_chat::{decode_file_source, decode_image_source};
16use crate::codecs::openai_media::{
17 ImagePayload, file_payload, file_source_text, image_payload, image_source_text,
18};
19use crate::codecs::structured_output::{decode_openai_schema_enforcement, encode_openai_format};
20use crate::codecs::{
21 DecodedRequest, DecodedResponse, EncodedRequest, EncodedResponse, FormatCodec,
22};
23use crate::diagnostic::TranslationDiagnostic;
24use crate::error::{Result, TranslationError};
25use crate::format::{FormatId, WireFormat};
26use crate::llm::{
27 AggLlmResponse, ContentBlock, FileSource, ImageSource, LlmRequest, MediaSource, Message,
28 OutputParams, ProviderExtensions, ReasoningParams, ResponseOutput, Role, SamplingParams,
29 StopReason, Tool, ToolCall, ToolChoice, ToolDefinition, ToolResult, Usage,
30};
31use crate::policy::{DeterministicIdPolicy, TranslationPolicy};
32use crate::util::{
33 capture_request_preservation, capture_response_preservation, embed_preservation,
34 exact_preserved_request, exact_preserved_response, is_responses_native_input_item, json_string,
35 push_lossy, stable_id, string_value, validate_request_capabilities,
36};
37
38pub struct OpenAiResponsesCodec;
40
41impl FormatCodec for OpenAiResponsesCodec {
42 fn format(&self) -> FormatId {
43 WireFormat::OpenAiResponses.into()
44 }
45
46 fn decode_request(&self, body: &Value, policy: &TranslationPolicy) -> Result<DecodedRequest> {
47 let body = crate::util::object(body, "$")?;
48 let mut diagnostics = Vec::new();
49 let response_format = decode_responses_text_format(body.get("text"));
50 let mut request = LlmRequest {
51 model: body
52 .get("model")
53 .and_then(Value::as_str)
54 .filter(|model| !model.is_empty())
55 .map(ToOwned::to_owned),
56 output: OutputParams {
57 max_output_tokens: body.get("max_output_tokens").and_then(Value::as_u64),
58 is_schema_enforced: decode_openai_schema_enforcement(response_format.as_ref()),
59 response_format,
60 },
61 reasoning: ReasoningParams {
62 effort: body
63 .get("reasoning")
64 .and_then(Value::as_object)
65 .and_then(|object| object.get("effort"))
66 .and_then(Value::as_str)
67 .map(ToOwned::to_owned),
68 raw: body.get("reasoning").cloned(),
69 },
70 sampling: SamplingParams {
71 temperature: body.get("temperature").and_then(Value::as_f64),
72 top_p: body.get("top_p").and_then(Value::as_f64),
73 top_k: None,
74 },
75 stream: body.get("stream").and_then(Value::as_bool).unwrap_or(false),
76 preservation: capture_request_preservation(
77 WireFormat::OpenAiResponses,
78 &Value::Object(body.clone()),
79 policy,
80 ),
81 ..LlmRequest::default()
82 };
83 if let Some(instructions) = body.get("instructions").and_then(Value::as_str)
84 && !instructions.is_empty()
85 {
86 request.instructions.push(crate::llm::InstructionBlock {
87 role: Role::System,
88 content: vec![ContentBlock::Text {
89 text: instructions.to_string(),
90 }],
91 });
92 }
93 let stored_state = body
96 .get("previous_response_id")
97 .and_then(Value::as_str)
98 .is_some_and(|id| !id.is_empty());
99 let mut custom_call_outputs = Vec::new();
100 request.messages = decode_responses_input(
101 body.get("input").unwrap_or(&Value::String(String::new())),
102 stored_state,
103 &mut custom_call_outputs,
104 &mut diagnostics,
105 policy,
106 )?;
107 let mut tool_namespaces = Map::new();
108 let mut custom_tools = Map::new();
109 request.tools =
110 decode_responses_tools(body.get("tools"), &mut tool_namespaces, &mut custom_tools)
111 .into_iter()
112 .map(Tool::from)
113 .collect();
114 let mut additional_tools = Vec::new();
119 if let Some(items) = body.get("input").and_then(Value::as_array) {
120 for item in items {
121 if let Some(item) = item.as_object()
122 && item.get("type").and_then(Value::as_str) == Some("additional_tools")
123 && let Some(tools) = item.get("tools").and_then(Value::as_array)
124 {
125 request.tools.extend(
126 decode_responses_tools(
127 Some(&Value::Array(tools.clone())),
128 &mut tool_namespaces,
129 &mut custom_tools,
130 )
131 .into_iter()
132 .map(Tool::from),
133 );
134 additional_tools.extend(tools.iter().cloned());
135 }
136 }
137 }
138 request.tool_choice = body
139 .get("tool_choice")
140 .and_then(decode_responses_tool_choice);
141 request.extensions.fields = provider_extensions(
142 body,
143 &[
144 "model",
145 "instructions",
146 "input",
147 "tools",
148 "tool_choice",
149 "max_output_tokens",
150 "text",
151 "reasoning",
152 "temperature",
153 "top_p",
154 "stream",
155 ],
156 );
157 crate::codex_namespaces::attach_tool_namespaces(&mut request.extensions, tool_namespaces);
158 crate::codex_custom_tools::attach_custom_tools(&mut request.extensions, custom_tools);
159 crate::codex_custom_tools::attach_custom_call_outputs(
160 &mut request.extensions,
161 custom_call_outputs,
162 );
163 crate::codex_custom_tools::attach_additional_tools(
164 &mut request.extensions,
165 additional_tools,
166 );
167 Ok(DecodedRequest {
168 request,
169 diagnostics,
170 })
171 }
172
173 fn encode_request(
174 &self,
175 request: &LlmRequest,
176 policy: &TranslationPolicy,
177 ) -> Result<EncodedRequest> {
178 if let Some(body) =
179 exact_preserved_request(&request.preservation, WireFormat::OpenAiResponses, policy)
180 {
181 return Ok(EncodedRequest {
182 body,
183 diagnostics: Vec::new(),
184 });
185 }
186 let mut diagnostics = Vec::new();
187 validate_request_capabilities(request, &mut diagnostics, policy)?;
188 let mut body = Map::new();
189 if let Some(model) = &request.model {
190 body.insert("model".to_string(), Value::String(model.clone()));
191 }
192 let instructions = request
193 .instructions
194 .iter()
195 .flat_map(|instruction| instruction.content.iter())
196 .filter_map(|block| match block {
197 ContentBlock::Text { text } | ContentBlock::Refusal { text } => Some(text.as_str()),
198 _ => None,
199 })
200 .collect::<Vec<_>>()
201 .join("\n\n");
202 if !instructions.is_empty() {
203 body.insert("instructions".to_string(), Value::String(instructions));
204 }
205 body.insert(
206 "input".to_string(),
207 encode_responses_input(
208 &request.messages,
209 &mut diagnostics,
210 policy,
211 crate::codex_namespaces::tool_namespaces(&request.extensions),
212 &crate::codex_custom_tools::custom_tool_names(&request.extensions),
213 crate::codex_custom_tools::custom_call_outputs(&request.extensions),
214 )?,
215 );
216 if let Some(additional) = crate::codex_custom_tools::additional_tools(&request.extensions) {
217 let item = json!({
220 "type": "additional_tools",
221 "role": "developer",
222 "tools": additional,
223 });
224 match body.get_mut("input") {
225 Some(Value::Array(input)) => input.insert(0, item),
226 _ => body
227 .insert("input".to_string(), Value::Array(vec![item]))
228 .map_or((), |_| ()),
229 }
230 } else if !request.tools.is_empty() {
231 body.insert(
232 "tools".to_string(),
233 encode_responses_tools(
234 &request.tools,
235 crate::codex_namespaces::tool_namespaces(&request.extensions),
236 crate::codex_custom_tools::custom_tools(&request.extensions),
237 )?,
238 );
239 }
240 if let Some(choice) = &request.tool_choice
241 && let Some(choice) = encode_responses_tool_choice(
242 choice,
243 crate::codex_namespaces::tool_namespaces(&request.extensions),
244 crate::codex_custom_tools::custom_tools(&request.extensions),
245 )
246 {
247 body.insert("tool_choice".to_string(), choice);
248 }
249 if let Some(max_output_tokens) = request.output.max_output_tokens {
250 body.insert("max_output_tokens".to_string(), json!(max_output_tokens));
251 }
252 if let Some(response_format) =
253 encode_openai_format(&request.output, &mut diagnostics, policy)?
254 {
255 body.insert(
256 "text".to_string(),
257 json!({"format": encode_responses_text_format(&response_format)}),
258 );
259 }
260 let mut reasoning = if is_anthropic_request(request) {
263 let mut reasoning = Map::new();
264 if request
265 .reasoning
266 .raw
267 .as_ref()
268 .and_then(|thinking| thinking.get("type"))
269 .and_then(Value::as_str)
270 == Some("disabled")
271 {
272 reasoning.insert("effort".to_string(), json!("none"));
273 }
274 reasoning
275 } else {
276 request
277 .reasoning
278 .raw
279 .as_ref()
280 .and_then(Value::as_object)
281 .cloned()
282 .unwrap_or_default()
283 };
284 if let Some(effort) = &request.reasoning.effort {
285 reasoning.insert("effort".to_string(), json!(effort));
286 }
287 if !reasoning.is_empty() {
288 body.insert("reasoning".to_string(), Value::Object(reasoning));
289 }
290 if let Some(value) = request.sampling.temperature {
291 body.insert("temperature".to_string(), json!(value));
292 }
293 if let Some(value) = request.sampling.top_p {
294 body.insert("top_p".to_string(), json!(value));
295 }
296 if request.stream {
297 body.insert("stream".to_string(), Value::Bool(true));
298 }
299 copy_responses_request_extensions(&mut body, &request.extensions.fields);
300
301 let body = embed_preservation(Value::Object(body), &request.preservation, policy);
302 Ok(EncodedRequest { body, diagnostics })
303 }
304
305 fn decode_response(&self, body: &Value, policy: &TranslationPolicy) -> Result<DecodedResponse> {
306 let body = crate::util::object(body, "$")?;
307 if body.get("status").and_then(Value::as_str) == Some("failed") {
310 return Err(TranslationError::UpstreamFailure {
311 error: body
312 .get("error")
313 .filter(|error| error.is_object())
314 .cloned()
315 .unwrap_or_else(|| {
316 json!({ "message": "provider reported status \"failed\" without error details" })
317 }),
318 });
319 }
320 let mut diagnostics = Vec::new();
321 let mut content = Vec::new();
322 let mut role = Role::Assistant;
323 let mut stop_reason = None;
324 let mut has_output = false;
325 let mut url_citations = Vec::new();
326 let mut text_offset = 0;
327 if let Some(items) = body.get("output").and_then(Value::as_array) {
328 for item in items {
329 if let Some(output) = decode_responses_output_item(item, &mut diagnostics, policy)?
330 {
331 has_output = true;
332 role = output.role;
333 for mut citation in output.url_citations {
334 citation.start_index += text_offset;
335 citation.end_index += text_offset;
336 url_citations.push(citation);
337 }
338 text_offset += output
339 .content
340 .iter()
341 .map(|block| match block {
342 ContentBlock::Text { text } | ContentBlock::Refusal { text } => {
343 text.chars().count()
344 }
345 _ => 0,
346 })
347 .sum::<usize>();
348 content.extend(output.content);
349 if output.stop_reason.is_some() {
350 stop_reason = output.stop_reason;
351 }
352 }
353 }
354 }
355 if body.get("status").and_then(Value::as_str) == Some("incomplete")
357 && body
358 .get("incomplete_details")
359 .and_then(|details| details.get("reason"))
360 .and_then(Value::as_str)
361 == Some("max_output_tokens")
362 {
363 stop_reason = Some(StopReason::MaxTokens);
364 }
365 let outputs = vec![if has_output {
366 ResponseOutput {
367 url_citations,
368 role,
369 content,
370 stop_reason,
371 }
372 } else {
373 ResponseOutput {
374 url_citations: Vec::new(),
375 role: Role::Assistant,
376 content: vec![ContentBlock::Text {
377 text: String::new(),
378 }],
379 stop_reason: Some(StopReason::EndTurn),
380 }
381 }];
382 let response = AggLlmResponse {
383 id: body
384 .get("id")
385 .and_then(Value::as_str)
386 .map(ToOwned::to_owned),
387 model: body
388 .get("model")
389 .and_then(Value::as_str)
390 .map(ToOwned::to_owned),
391 outputs,
392 usage: decode_responses_usage(body.get("usage")),
393 extensions: ProviderExtensions {
394 fields: provider_extensions(
395 body,
396 &["id", "model", "object", "output", "usage", "status"],
397 ),
398 },
399 preservation: capture_response_preservation(
400 WireFormat::OpenAiResponses,
401 &Value::Object(body.clone()),
402 policy,
403 ),
404 };
405 Ok(DecodedResponse {
406 response,
407 diagnostics,
408 })
409 }
410
411 fn encode_response(
412 &self,
413 response: &AggLlmResponse,
414 _policy: &TranslationPolicy,
415 ) -> Result<EncodedResponse> {
416 if let Some(body) =
417 exact_preserved_response(&response.preservation, WireFormat::OpenAiResponses, _policy)
418 {
419 return Ok(EncodedResponse {
420 body,
421 diagnostics: Vec::new(),
422 });
423 }
424 let is_truncated = matches!(
425 response
426 .first_output()
427 .and_then(|output| output.stop_reason),
428 Some(StopReason::MaxTokens)
429 );
430 let status = if is_truncated {
431 "incomplete"
432 } else {
433 "completed"
434 };
435 let incomplete_details = is_truncated.then(|| json!({ "reason": "max_output_tokens" }));
436 Ok(EncodedResponse {
437 body: embed_preservation(
438 json!({
439 "id": response.id.clone().unwrap_or_else(|| "resp_switchyard".to_string()),
440 "object": "response",
441 "created_at": 0,
442 "model": response.model.clone().unwrap_or_else(|| "unknown".to_string()),
443 "status": status,
444 "incomplete_details": incomplete_details,
445 "output": encode_responses_output(&response.outputs),
446 "usage": encode_responses_usage(&response.usage),
447 "parallel_tool_calls": true,
448 "tool_choice": "auto",
449 "tools": [],
450 }),
451 &response.preservation,
452 _policy,
453 ),
454 diagnostics: Vec::new(),
455 })
456 }
457}
458
459fn decode_responses_input(
461 value: &Value,
462 stored_state: bool,
463 custom_call_outputs: &mut Vec<String>,
464 diagnostics: &mut Vec<TranslationDiagnostic>,
465 policy: &TranslationPolicy,
466) -> Result<Vec<Message>> {
467 match value {
468 Value::String(text) => Ok(vec![Message::text(Role::User, text)]),
469 Value::Array(items) => {
470 let mut messages = Vec::new();
471 let mut pending_tool_calls = Vec::new();
472 let mut pending_tool_outputs = Vec::new();
473 let mut deferred_messages = Vec::new();
474 let mut pending_reasoning: Vec<ContentBlock> = Vec::new();
475 for (index, item) in items.iter().enumerate() {
476 let Some(item) = item.as_object() else {
477 push_lossy(
478 diagnostics,
479 policy,
480 format!("Responses input item {index} is not an object"),
481 )?;
482 continue;
483 };
484 let item_type = match item.get("type") {
485 Some(Value::String(item_type)) => Some(item_type.as_str()),
486 Some(_) => {
487 return Err(TranslationError::InvalidType {
488 path: format!("$.input[{index}].type"),
489 expected: "a string",
490 });
491 }
492 None => None,
493 };
494 let is_message = item_type == Some("message")
495 || (item_type.is_none()
496 && item.contains_key("role")
497 && item.contains_key("content"));
498 match item_type {
499 _ if is_message => {
500 let role = request_role_from_responses(
501 item.get("role").and_then(Value::as_str),
502 &format!("$.input[{index}].role"),
503 )?;
504 let content_value = item.get("content").unwrap_or(&Value::Null);
505 if is_responses_native_input_item(content_value) {
506 return Err(TranslationError::InvalidValue {
507 path: format!("$.input[{index}].content.type"),
508 message: "native Responses items must be top-level input items"
509 .to_string(),
510 });
511 }
512 if let Some(content_index) = content_value.as_array().and_then(|blocks| {
513 blocks.iter().position(is_responses_native_input_item)
514 }) {
515 return Err(TranslationError::InvalidValue {
516 path: format!("$.input[{index}].content[{content_index}].type"),
517 message: "native Responses items must be top-level input items"
518 .to_string(),
519 });
520 }
521 let mut content = decode_responses_content(content_value);
522 if matches!(role, Role::System | Role::Developer)
526 && !(pending_tool_calls.is_empty()
527 && pending_tool_outputs.is_empty()
528 && pending_reasoning.is_empty())
529 {
530 deferred_messages.push(Message::new(role, content));
531 continue;
532 }
533 if role == Role::Assistant
537 && pending_tool_calls.is_empty()
538 && !pending_reasoning.is_empty()
539 {
540 let mut merged = std::mem::take(&mut pending_reasoning);
541 merged.append(&mut content);
542 content = merged;
543 if pending_tool_outputs.is_empty() {
545 messages.append(&mut deferred_messages);
546 }
547 }
548 if role != Role::Assistant {
549 flush_unattached_responses_reasoning(
550 &mut messages,
551 &mut pending_tool_calls,
552 &mut pending_tool_outputs,
553 &mut deferred_messages,
554 &mut pending_reasoning,
555 );
556 }
557 push_responses_non_tool_message(
558 &mut messages,
559 &mut pending_tool_calls,
560 &mut pending_tool_outputs,
561 &mut deferred_messages,
562 &mut pending_reasoning,
563 Message {
564 role,
565 content,
566 phase: if role == Role::Assistant {
567 item.get("phase").and_then(Value::as_str).map(str::to_owned)
568 } else {
569 None
570 },
571 },
572 );
573 }
574 Some("reasoning") => {
575 if !pending_tool_outputs.is_empty() {
579 flush_responses_tool_block(
580 &mut messages,
581 &mut pending_tool_calls,
582 &mut pending_tool_outputs,
583 &mut deferred_messages,
584 &mut pending_reasoning,
585 );
586 }
587 pending_reasoning.extend(decode_responses_reasoning_item(item));
588 }
589 Some("function_call") => {
590 if !pending_tool_outputs.is_empty() {
591 flush_responses_tool_block(
592 &mut messages,
593 &mut pending_tool_calls,
594 &mut pending_tool_outputs,
595 &mut deferred_messages,
596 &mut pending_reasoning,
597 );
598 }
599 let id = item
600 .get("call_id")
601 .and_then(Value::as_str)
602 .filter(|id| !id.is_empty())
603 .map(ToOwned::to_owned)
604 .unwrap_or_else(|| match &policy.deterministic_ids {
605 DeterministicIdPolicy::GenerateStable { prefix } => {
606 stable_id(prefix, index + 1)
607 }
608 DeterministicIdPolicy::Preserve => String::new(),
609 });
610 let name = item
614 .get("name")
615 .and_then(Value::as_str)
616 .unwrap_or_default()
617 .to_string();
618 let name = match item
619 .get("namespace")
620 .and_then(Value::as_str)
621 .filter(|namespace| !namespace.is_empty())
622 {
623 Some(namespace) => {
624 crate::codex_namespaces::qualified_tool_name(namespace, &name)
625 }
626 None => name,
627 };
628 pending_tool_calls.push(ToolCall {
629 id,
630 name,
631 arguments: item.get("arguments").cloned().unwrap_or_else(|| json!({})),
632 });
633 }
634 kind @ (Some("function_call_output") | Some("custom_tool_call_output")) => {
635 let tool_call_id = item
636 .get("call_id")
637 .and_then(Value::as_str)
638 .unwrap_or_default()
639 .to_string();
640 let content = match item.get("output") {
643 Some(output @ Value::Array(_)) => decode_responses_content(output),
644 output => vec![ContentBlock::Text {
645 text: output.map(json_string).unwrap_or_default(),
646 }],
647 };
648 let answers_pending_call = pending_tool_calls
653 .iter()
654 .any(|call| call.id == tool_call_id);
655 if !answers_pending_call && !stored_state {
656 let output_text = text_from_blocks(&content, " ");
657 let message = Message::text(
658 Role::User,
659 format!("Tool result {tool_call_id}: {output_text}"),
660 );
661 push_responses_non_tool_message(
662 &mut messages,
663 &mut pending_tool_calls,
664 &mut pending_tool_outputs,
665 &mut deferred_messages,
666 &mut pending_reasoning,
667 message,
668 );
669 continue;
670 }
671 if !answers_pending_call && kind == Some("custom_tool_call_output") {
674 custom_call_outputs.push(tool_call_id.clone());
675 }
676 pending_tool_outputs.push(ToolResult {
677 tool_call_id,
678 content,
679 is_error: None,
680 });
681 }
682 Some("custom_tool_call") => {
683 if !pending_tool_outputs.is_empty() {
686 flush_responses_tool_block(
687 &mut messages,
688 &mut pending_tool_calls,
689 &mut pending_tool_outputs,
690 &mut deferred_messages,
691 &mut pending_reasoning,
692 );
693 }
694 let id = item
695 .get("call_id")
696 .and_then(Value::as_str)
697 .filter(|id| !id.is_empty())
698 .map(ToOwned::to_owned)
699 .unwrap_or_else(|| match &policy.deterministic_ids {
700 DeterministicIdPolicy::GenerateStable { prefix } => {
701 stable_id(prefix, index + 1)
702 }
703 DeterministicIdPolicy::Preserve => String::new(),
704 });
705 let name = item
706 .get("name")
707 .and_then(Value::as_str)
708 .unwrap_or_default()
709 .to_string();
710 let input = item
711 .get("input")
712 .and_then(Value::as_str)
713 .unwrap_or_default()
714 .to_string();
715 pending_tool_calls.push(ToolCall {
716 id,
717 name,
718 arguments: json!({crate::codex_custom_tools::INPUT_ARGUMENT: input}),
719 });
720 }
721 None => {
722 return Err(TranslationError::InvalidValue {
723 path: format!("$.input[{index}].type"),
724 message: "missing type discriminator on a non-message input item"
725 .to_string(),
726 });
727 }
728 Some("additional_tools") => {}
731 _ => {
732 let message = Message::new(
733 match item_type {
734 Some(
735 "shell_call"
736 | "apply_patch_call"
737 | "computer_call"
738 | "image_generation_call",
739 ) => Role::Assistant,
740 Some(
741 "shell_call_output"
742 | "apply_patch_call_output"
743 | "computer_call_output",
744 ) => Role::Tool,
745 _ => Role::User,
746 },
747 vec![ContentBlock::Unknown {
748 provider: WireFormat::OpenAiResponses.into(),
749 raw: Value::Object(item.clone()),
750 }],
751 );
752 flush_unattached_responses_reasoning(
753 &mut messages,
754 &mut pending_tool_calls,
755 &mut pending_tool_outputs,
756 &mut deferred_messages,
757 &mut pending_reasoning,
758 );
759 push_responses_non_tool_message(
760 &mut messages,
761 &mut pending_tool_calls,
762 &mut pending_tool_outputs,
763 &mut deferred_messages,
764 &mut pending_reasoning,
765 message,
766 );
767 }
768 }
769 }
770 if !pending_tool_calls.is_empty() || !pending_tool_outputs.is_empty() {
771 flush_responses_tool_block(
772 &mut messages,
773 &mut pending_tool_calls,
774 &mut pending_tool_outputs,
775 &mut deferred_messages,
776 &mut pending_reasoning,
777 );
778 }
779 if !pending_reasoning.is_empty() {
782 messages.push(Message::new(Role::Assistant, pending_reasoning));
783 }
784 messages.append(&mut deferred_messages);
785 Ok(messages)
786 }
787 _ => Err(TranslationError::InvalidType {
788 path: "$.input".to_string(),
789 expected: "string or array",
790 }),
791 }
792}
793
794fn push_responses_non_tool_message(
796 messages: &mut Vec<Message>,
797 pending_tool_calls: &mut Vec<ToolCall>,
798 pending_tool_outputs: &mut Vec<ToolResult>,
799 deferred_messages: &mut Vec<Message>,
800 pending_reasoning: &mut Vec<ContentBlock>,
801 message: Message,
802) {
803 if !pending_tool_calls.is_empty() && pending_tool_outputs.is_empty() {
804 deferred_messages.push(message);
805 return;
806 }
807 if !pending_tool_calls.is_empty() || !pending_tool_outputs.is_empty() {
808 flush_responses_tool_block(
809 messages,
810 pending_tool_calls,
811 pending_tool_outputs,
812 deferred_messages,
813 pending_reasoning,
814 );
815 }
816 messages.push(message);
817}
818
819fn flush_unattached_responses_reasoning(
823 messages: &mut Vec<Message>,
824 pending_tool_calls: &mut Vec<ToolCall>,
825 pending_tool_outputs: &mut Vec<ToolResult>,
826 deferred_messages: &mut Vec<Message>,
827 pending_reasoning: &mut Vec<ContentBlock>,
828) {
829 if pending_reasoning.is_empty() || !pending_tool_calls.is_empty() {
830 return;
831 }
832 let message = Message::new(Role::Assistant, std::mem::take(pending_reasoning));
833 push_responses_non_tool_message(
834 messages,
835 pending_tool_calls,
836 pending_tool_outputs,
837 deferred_messages,
838 pending_reasoning,
839 message,
840 );
841 messages.append(deferred_messages);
843}
844
845fn flush_responses_tool_block(
849 messages: &mut Vec<Message>,
850 pending_tool_calls: &mut Vec<ToolCall>,
851 pending_tool_outputs: &mut Vec<ToolResult>,
852 deferred_messages: &mut Vec<Message>,
853 pending_reasoning: &mut Vec<ContentBlock>,
854) {
855 if !pending_tool_calls.is_empty() {
856 let mut content = std::mem::take(pending_reasoning);
857 content.extend(
858 std::mem::take(pending_tool_calls)
859 .into_iter()
860 .map(ContentBlock::ToolCall),
861 );
862 messages.push(Message::new(Role::Assistant, content));
863 }
864
865 for output in std::mem::take(pending_tool_outputs) {
866 messages.push(Message::new(
867 Role::User,
868 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("custom") => {
1171 object
1172 .get("name")
1173 .and_then(Value::as_str)
1174 .map(|name| ToolChoice::Tool {
1175 name: name.to_string(),
1176 })
1177 }
1178 Value::Object(object) if object.get("type").and_then(Value::as_str) == Some("function") => {
1179 object.get("name").and_then(Value::as_str).map(|name| {
1182 match object
1183 .get("namespace")
1184 .and_then(Value::as_str)
1185 .filter(|namespace| !namespace.is_empty())
1186 {
1187 Some(namespace) => ToolChoice::Tool {
1188 name: crate::codex_namespaces::qualified_tool_name(namespace, name),
1189 },
1190 None => ToolChoice::Tool {
1191 name: name.to_string(),
1192 },
1193 }
1194 })
1195 }
1196 Value::Object(object)
1197 if object.get("type").and_then(Value::as_str) == Some("allowed_tools") =>
1198 {
1199 Some(ToolChoice::Raw(value.clone()))
1200 }
1201 Value::Object(_) => None,
1202 _ => Some(ToolChoice::Raw(value.clone())),
1203 }
1204}
1205
1206fn decode_responses_text_format(value: Option<&Value>) -> Option<Value> {
1208 let format = value
1209 .and_then(Value::as_object)
1210 .and_then(|object| object.get("format"))
1211 .and_then(Value::as_object)?;
1212
1213 match format.get("type").and_then(Value::as_str) {
1214 Some("json_schema") => {
1215 if let Some(json_schema) = format.get("json_schema").and_then(Value::as_object) {
1216 return Some(json!({
1217 "type": "json_schema",
1218 "json_schema": Value::Object(json_schema.clone()),
1219 }));
1220 }
1221
1222 let mut json_schema = Map::new();
1223 for field in ["name", "description", "schema", "strict"] {
1224 if let Some(value) = format.get(field) {
1225 json_schema.insert(field.to_string(), value.clone());
1226 }
1227 }
1228 (!json_schema.is_empty()).then_some(json!({
1229 "type": "json_schema",
1230 "json_schema": Value::Object(json_schema),
1231 }))
1232 }
1233 Some("json_object") => Some(json!({"type": "json_object"})),
1234 Some("text") => Some(json!({"type": "text"})),
1235 _ => None,
1236 }
1237}
1238
1239fn encode_responses_text_format(response_format: &Value) -> Value {
1241 let Some(format) = response_format.as_object() else {
1242 return response_format.clone();
1243 };
1244 let Some(response_type) = format
1245 .get("type")
1246 .filter(|value| value.as_str() == Some("json_schema"))
1247 else {
1248 return response_format.clone();
1249 };
1250
1251 let Some(Value::Object(json_schema)) = format.get("json_schema") else {
1252 return response_format.clone();
1253 };
1254 if !matches!(json_schema.get("name"), Some(Value::String(_)))
1255 || !matches!(json_schema.get("schema"), Some(Value::Object(_)))
1256 || json_schema
1257 .get("description")
1258 .is_some_and(|value| !value.is_string())
1259 || json_schema
1260 .get("strict")
1261 .is_some_and(|value| !value.is_boolean())
1262 {
1263 return response_format.clone();
1264 }
1265
1266 let mut output = Map::new();
1267 output.insert("type".to_string(), response_type.clone());
1268 for field in ["name", "description", "schema", "strict"] {
1269 if let Some(value) = json_schema.get(field) {
1270 output.insert(field.to_string(), value.clone());
1271 }
1272 }
1273 Value::Object(output)
1274}
1275
1276fn encode_responses_input(
1278 messages: &[Message],
1279 diagnostics: &mut Vec<TranslationDiagnostic>,
1280 policy: &TranslationPolicy,
1281 namespaces: Option<&Map<String, Value>>,
1282 custom_tools: &std::collections::HashSet<String>,
1283 mut custom_call_ids: std::collections::HashSet<String>,
1284) -> Result<Value> {
1285 let mut encoded = Vec::new();
1292 let mut call_names: HashMap<&str, &str> = HashMap::new();
1295 for message in messages {
1296 for block in &message.content {
1297 if let ContentBlock::ToolCall(call) = block {
1298 call_names.insert(call.id.as_str(), call.name.as_str());
1299 }
1300 }
1301 }
1302 for message in messages {
1303 if let [ContentBlock::Unknown { provider, raw }] = message.content.as_slice()
1306 && provider.as_str() == WireFormat::OpenAiResponses.as_str()
1307 && is_responses_native_input_item(raw)
1308 {
1309 encoded.push(raw.clone());
1310 continue;
1311 }
1312 let content = message
1314 .content
1315 .iter()
1316 .filter(|block| {
1317 !matches!(
1318 block,
1319 ContentBlock::Reasoning {
1320 signature: Some(_),
1321 ..
1322 }
1323 )
1324 })
1325 .cloned()
1326 .collect::<Vec<_>>();
1327 if content.is_empty() {
1328 continue;
1329 }
1330 if content.iter().any(|block| {
1331 matches!(
1332 block,
1333 ContentBlock::ToolCall(_) | ContentBlock::ToolResult(_)
1334 )
1335 }) {
1336 let mut visible_content = Vec::new();
1337 for block in &content {
1338 if let Some(item) = encode_responses_special_input(
1339 block,
1340 namespaces,
1341 &call_names,
1342 custom_tools,
1343 &mut custom_call_ids,
1344 diagnostics,
1345 policy,
1346 )? {
1347 if !visible_content.is_empty() {
1348 let content =
1349 encode_responses_content(&visible_content, diagnostics, policy)?;
1350 encoded.push(encode_responses_message(message, content));
1351 visible_content.clear();
1352 }
1353 encoded.push(item);
1354 } else if !matches!(block, ContentBlock::Reasoning { .. }) {
1355 visible_content.push(block.clone());
1356 }
1357 }
1358 if !visible_content.is_empty() {
1359 let content = encode_responses_content(&visible_content, diagnostics, policy)?;
1360 encoded.push(encode_responses_message(message, content));
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()
1386 || (!emitted_special && !omitted_reasoning)
1387 || (message.role == Role::Assistant && message.phase.is_some())
1388 {
1389 let content = encode_responses_content(&visible_content, diagnostics, policy)?;
1390 encoded.push(encode_responses_message(message, content));
1391 }
1392 }
1393 pair_tool_calls_with_outputs(&mut encoded);
1394 Ok(Value::Array(encoded))
1395}
1396
1397fn encode_responses_message(message: &Message, content: Value) -> Value {
1399 let mut item = json!({
1400 "type": "message",
1401 "role": role_to_responses(message.role),
1402 "content": content,
1403 });
1404 if let (Role::Assistant, Some(phase)) = (message.role, &message.phase) {
1405 item["phase"] = json!(phase);
1406 }
1407 item
1408}
1409
1410fn pair_tool_calls_with_outputs(items: &mut Vec<Value>) {
1414 let call_id = |item: &Value, kind: &str| {
1415 (item.get("type").and_then(Value::as_str) == Some(kind))
1416 .then(|| {
1417 item.get("call_id")
1418 .and_then(Value::as_str)
1419 .map(str::to_owned)
1420 })
1421 .flatten()
1422 };
1423 let mut index = 0;
1424 while index < items.len() {
1425 let (id, output_kind) = match call_id(&items[index], "function_call") {
1427 Some(id) => (id, "function_call_output"),
1428 None => match call_id(&items[index], "custom_tool_call") {
1429 Some(id) => (id, "custom_tool_call_output"),
1430 None => {
1431 index += 1;
1432 continue;
1433 }
1434 },
1435 };
1436 let output = items
1437 .iter()
1438 .skip(index + 1)
1439 .take_while(|item| item.get("type").and_then(Value::as_str) != Some("message"))
1440 .position(|item| call_id(item, output_kind).as_deref() == Some(&id))
1441 .map(|offset| index + 1 + offset);
1442 if let Some(output) = output
1443 && output != index + 1
1444 {
1445 let item = items.remove(output);
1446 items.insert(index + 1, item);
1447 }
1448 index += 1;
1449 }
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 custom_tools: Option<&Map<String, Value>>,
1761) -> Option<Value> {
1762 match choice {
1763 ToolChoice::Auto => Some(Value::String("auto".to_string())),
1764 ToolChoice::Required => Some(Value::String("required".to_string())),
1765 ToolChoice::None => Some(Value::String("none".to_string())),
1766 ToolChoice::Tool { name } if custom_tools.is_some_and(|tools| tools.contains_key(name)) => {
1767 Some(json!({"type": "custom", "name": name}))
1768 }
1769 ToolChoice::Tool { name } => {
1770 let split = namespaces.and_then(|namespaces| {
1771 crate::codex_namespaces::split_qualified_name(namespaces, name)
1772 });
1773 Some(match split {
1774 Some((tool, namespace)) => {
1775 json!({"type": "function", "name": tool, "namespace": namespace})
1776 }
1777 None => json!({"type": "function", "name": name}),
1778 })
1779 }
1780 ToolChoice::Raw(value) => Some(value.clone()),
1781 }
1782}
1783
1784fn decode_responses_output_item(
1786 item: &Value,
1787 diagnostics: &mut Vec<TranslationDiagnostic>,
1788 policy: &TranslationPolicy,
1789) -> Result<Option<ResponseOutput>> {
1790 let Some(item) = item.as_object() else {
1791 push_lossy(
1792 diagnostics,
1793 policy,
1794 "Responses output item is not an object",
1795 )?;
1796 return Ok(None);
1797 };
1798 match item.get("type").and_then(Value::as_str) {
1799 Some("message") => Ok(Some(ResponseOutput {
1800 url_citations: decode_responses_url_citations(item.get("content")),
1801 role: role_from_responses(item.get("role").and_then(Value::as_str)),
1802 content: decode_responses_content(item.get("content").unwrap_or(&Value::Null)),
1803 stop_reason: Some(StopReason::EndTurn),
1804 })),
1805 Some("function_call") => Ok(Some(ResponseOutput {
1806 url_citations: Vec::new(),
1807 role: Role::Assistant,
1808 content: vec![ContentBlock::ToolCall(ToolCall {
1809 id: item
1810 .get("call_id")
1811 .and_then(Value::as_str)
1812 .unwrap_or_default()
1813 .to_string(),
1814 name: item
1815 .get("name")
1816 .and_then(Value::as_str)
1817 .unwrap_or_default()
1818 .to_string(),
1819 arguments: item.get("arguments").cloned().unwrap_or_else(|| json!({})),
1820 })],
1821 stop_reason: Some(StopReason::ToolUse),
1822 })),
1823 Some("custom_tool_call") => Ok(Some(ResponseOutput {
1824 url_citations: Vec::new(),
1825 role: Role::Assistant,
1826 content: vec![ContentBlock::ToolCall(ToolCall {
1827 id: item
1828 .get("call_id")
1829 .and_then(Value::as_str)
1830 .unwrap_or_default()
1831 .to_string(),
1832 name: item
1833 .get("name")
1834 .and_then(Value::as_str)
1835 .unwrap_or_default()
1836 .to_string(),
1837 arguments: json!({
1838 crate::codex_custom_tools::INPUT_ARGUMENT:
1839 item.get("input").and_then(Value::as_str).unwrap_or_default()
1840 }),
1841 })],
1842 stop_reason: Some(StopReason::ToolUse),
1843 })),
1844 Some("reasoning") => Ok(Some(ResponseOutput {
1845 url_citations: Vec::new(),
1846 role: Role::Assistant,
1847 content: decode_responses_reasoning_item(item),
1848 stop_reason: None,
1849 })),
1850 Some(kind) if super::is_native_output(kind) => Ok(Some(ResponseOutput {
1851 url_citations: Vec::new(),
1852 role: Role::Assistant,
1853 content: vec![ContentBlock::Unknown {
1854 provider: WireFormat::OpenAiResponses.into(),
1855 raw: Value::Object(item.clone()),
1856 }],
1857 stop_reason: None,
1858 })),
1859 _ => Ok(None),
1860 }
1861}
1862
1863fn decode_responses_url_citations(content: Option<&Value>) -> Vec<crate::UrlCitation> {
1864 let mut citations = Vec::new();
1865 let mut text_offset = 0;
1866 for part in content.and_then(Value::as_array).into_iter().flatten() {
1867 for annotation in part
1868 .get("annotations")
1869 .and_then(Value::as_array)
1870 .into_iter()
1871 .flatten()
1872 {
1873 if annotation["type"] == "url_citation"
1874 && let Ok(mut citation) =
1875 serde_json::from_value::<crate::UrlCitation>(annotation.clone())
1876 {
1877 citation.start_index += text_offset;
1878 citation.end_index += text_offset;
1879 citations.push(citation);
1880 }
1881 }
1882 let text = match part.get("type").and_then(Value::as_str) {
1883 Some("input_text" | "output_text" | "text") => part.get("text"),
1884 Some("refusal") => part.get("refusal"),
1885 _ => None,
1886 };
1887 text_offset += text
1888 .and_then(Value::as_str)
1889 .unwrap_or_default()
1890 .chars()
1891 .count();
1892 }
1893 citations
1894}
1895
1896fn encode_responses_output(outputs: &[ResponseOutput]) -> Value {
1898 Value::Array(
1899 outputs
1900 .iter()
1901 .flat_map(|output| {
1902 let has_output_items = output.content.iter().any(|block| {
1903 matches!(block, ContentBlock::ToolCall(_))
1904 || matches!(block, ContentBlock::Unknown { provider, raw }
1905 if provider.as_str() == WireFormat::OpenAiResponses.as_str()
1906 && raw["type"].as_str().is_some_and(super::is_native_output))
1907 });
1908 let text = text_from_blocks(&output.content, "");
1909 let reasoning = reasoning_text_from_blocks(&output.content, "\n");
1910 let status = if matches!(output.stop_reason, Some(StopReason::MaxTokens)) {
1911 "incomplete"
1912 } else {
1913 "completed"
1914 };
1915 let mut items = Vec::new();
1916
1917 let encrypted_reasoning = output.content.iter().find_map(|block| match block {
1918 ContentBlock::Reasoning { details, .. } => encrypted_reasoning_data(details),
1919 _ => None,
1920 });
1921 let encrypted_reasoning_id = output.content.iter().find_map(|block| match block {
1922 ContentBlock::Reasoning { details, .. } => encrypted_reasoning_item_id(details),
1923 _ => None,
1924 });
1925 let has_signed_reasoning = output.content.iter().any(|block| {
1926 matches!(
1927 block,
1928 ContentBlock::Reasoning {
1929 signature: Some(_),
1930 ..
1931 }
1932 )
1933 });
1934 if has_signed_reasoning {
1935 for (index, block) in output.content.iter().enumerate() {
1936 if let ContentBlock::Reasoning {
1937 text,
1938 signature,
1939 details,
1940 } = block
1941 {
1942 let encrypted = signature
1943 .as_deref()
1944 .map(|signature| super::encode_anthropic_thinking(text, signature))
1945 .or_else(|| encrypted_reasoning_data(details));
1946 let id = encrypted_reasoning_item_id(details)
1947 .unwrap_or_else(|| format!("rs_switchyard_{index}"));
1948 items.push(encode_responses_reasoning_output(
1949 text,
1950 encrypted.as_deref(),
1951 Some(&id),
1952 ));
1953 }
1954 }
1955 } else if !reasoning.is_empty() || encrypted_reasoning.is_some() {
1956 items.push(encode_responses_reasoning_output(
1957 &reasoning,
1958 encrypted_reasoning.as_deref(),
1959 encrypted_reasoning_id.as_deref(),
1960 ));
1961 }
1962
1963 if !text.is_empty() || (!has_output_items && reasoning.is_empty()) {
1964 items.push(json!({
1965 "type": "message",
1966 "id": "msg_switchyard",
1967 "status": status,
1968 "role": role_to_responses(output.role),
1969 "content": [{
1970 "type": "output_text",
1971 "text": text,
1972 "annotations": output.url_citations.iter().map(|citation| json!({
1973 "type": "url_citation",
1974 "start_index": citation.start_index,
1975 "end_index": citation.end_index,
1976 "url": citation.url,
1977 "title": citation.title,
1978 })).collect::<Vec<_>>(),
1979 }],
1980 }));
1981 }
1982
1983 items.extend(output.content.iter().filter_map(|block| match block {
1984 ContentBlock::ToolCall(call) => Some(json!({
1985 "type": "function_call",
1986 "call_id": call.id,
1987 "name": call.name,
1988 "arguments": json_string_python_style(&call.arguments),
1989 })),
1990 ContentBlock::Unknown { provider, raw }
1991 if provider.as_str() == WireFormat::OpenAiResponses.as_str()
1992 && raw["type"].as_str().is_some_and(super::is_native_output) =>
1993 {
1994 Some(raw.clone())
1995 }
1996 _ => None,
1997 }));
1998
1999 items
2000 })
2001 .collect(),
2002 )
2003}
2004
2005fn encode_responses_reasoning_output(
2008 text: &str,
2009 encrypted: Option<&str>,
2010 item_id: Option<&str>,
2011) -> Value {
2012 let mut summary = Vec::new();
2014 if !text.is_empty() {
2015 summary.push(json!({"type": "summary_text", "text": text}));
2016 }
2017 let mut item = json!({
2019 "type": "reasoning",
2020 "id": item_id.unwrap_or("rs_switchyard"),
2021 "status": "completed",
2022 "summary": summary,
2023 });
2024 if let Some(encrypted) = encrypted {
2025 item["encrypted_content"] = Value::String(encrypted.to_string());
2026 }
2027 item
2028}
2029
2030fn json_string_python_style(value: &Value) -> String {
2032 match value {
2033 Value::Null => "null".to_string(),
2034 Value::Bool(value) => value.to_string(),
2035 Value::Number(value) => value.to_string(),
2036 Value::String(value) => serde_json::to_string(value).unwrap_or_else(|_| value.clone()),
2037 Value::Array(items) => {
2038 let body = items
2039 .iter()
2040 .map(json_string_python_style)
2041 .collect::<Vec<_>>()
2042 .join(", ");
2043 format!("[{body}]")
2044 }
2045 Value::Object(object) => {
2046 let body = object
2047 .iter()
2048 .map(|(key, value)| {
2049 let key = serde_json::to_string(key).unwrap_or_else(|_| format!("\"{key}\""));
2050 format!("{key}: {}", json_string_python_style(value))
2051 })
2052 .collect::<Vec<_>>()
2053 .join(", ");
2054 format!("{{{body}}}")
2055 }
2056 }
2057}
2058
2059fn decode_responses_usage(value: Option<&Value>) -> Usage {
2061 let Some(value) = value.and_then(Value::as_object) else {
2062 return Usage::default();
2063 };
2064 let aggregate_input_tokens = value
2065 .get("input_tokens")
2066 .or_else(|| value.get("prompt_tokens"))
2067 .and_then(Value::as_u64);
2068 let cached_input_tokens = value
2069 .get("input_tokens_details")
2070 .or_else(|| value.get("prompt_tokens_details"))
2071 .and_then(|details| details.get("cached_tokens"))
2072 .and_then(Value::as_u64);
2073 let cache_creation_input_tokens = value
2074 .get("input_tokens_details")
2075 .and_then(|details| details.get("cache_write_tokens"))
2076 .and_then(Value::as_u64)
2077 .or_else(|| {
2078 value.get("prompt_tokens_details").and_then(|details| {
2079 details
2080 .get("cache_write_tokens")
2081 .and_then(Value::as_u64)
2082 .or_else(|| details.get("cache_creation_tokens").and_then(Value::as_u64))
2083 })
2084 });
2085 let input_tokens = aggregate_input_tokens.map(|tokens| {
2086 tokens
2087 .saturating_sub(cached_input_tokens.unwrap_or(0))
2088 .saturating_sub(cache_creation_input_tokens.unwrap_or(0))
2089 });
2090 let output_tokens = value
2091 .get("output_tokens")
2092 .or_else(|| value.get("completion_tokens"))
2093 .and_then(Value::as_u64);
2094 Usage {
2095 provider_cost: crate::codecs::common::provider_cost(value),
2096 input_tokens,
2097 cache: crate::codecs::common::cache_usage_details(
2098 value,
2099 cached_input_tokens,
2100 cache_creation_input_tokens,
2101 ),
2102 output_tokens,
2103 total_tokens: value
2104 .get("total_tokens")
2105 .and_then(Value::as_u64)
2106 .or_else(|| {
2107 aggregate_input_tokens
2108 .zip(output_tokens)
2109 .map(|(input, output)| input + output)
2110 }),
2111 reasoning_tokens: value
2112 .get("output_tokens_details")
2113 .and_then(|details| details.get("reasoning_tokens"))
2114 .or_else(|| {
2115 value
2116 .get("completion_tokens_details")
2117 .and_then(|details| details.get("reasoning_tokens"))
2118 })
2119 .and_then(Value::as_u64),
2120 }
2121}
2122
2123pub(super) fn encode_responses_usage(usage: &Usage) -> Value {
2125 let input_tokens = usage.input_tokens.unwrap_or(0)
2126 + usage.cached_input_tokens().unwrap_or(0)
2127 + usage.cache_creation_input_tokens().unwrap_or(0);
2128 crate::codecs::common::encode_usage_metadata(
2133 json!({
2134 "input_tokens": input_tokens,
2135 "output_tokens": usage.output_tokens.unwrap_or(0),
2136 "total_tokens": usage
2137 .total_tokens
2138 .or_else(|| Some(input_tokens + usage.output_tokens.unwrap_or(0)))
2139 .unwrap_or(0),
2140 "input_tokens_details": {
2141 "cached_tokens": usage.cached_input_tokens().unwrap_or(0),
2142 "cache_write_tokens": usage.cache_creation_input_tokens().unwrap_or(0),
2143 },
2144 "output_tokens_details": {"reasoning_tokens": usage.reasoning_tokens.unwrap_or(0)},
2145 }),
2146 usage,
2147 )
2148}
2149
2150fn copy_responses_request_extensions(
2158 body: &mut Map<String, Value>,
2159 extensions: &Map<String, Value>,
2160) {
2161 for field in [
2162 "include",
2163 "metadata",
2164 "parallel_tool_calls",
2165 "previous_response_id",
2166 "prompt_cache_key",
2167 "prompt_cache_retention",
2168 "safety_identifier",
2169 "service_tier",
2170 "store",
2171 "user",
2172 ] {
2173 if let Some(value) = extensions.get(field) {
2174 body.entry(field.to_string())
2175 .or_insert_with(|| value.clone());
2176 }
2177 }
2178}