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