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