1use crate::completion::history::Replay;
15use crate::completion::options::{BaseInput, FinalBody, RawAt, Rewrite, request_params};
16use crate::error::EncodeError;
17use crate::json_utils;
18use crate::json_utils::Lenient;
19use crate::message::{
20 AssistantContent, Document, DocumentData, DocumentMediaType, DocumentSourceKind, Message,
21 MimeType, ToolResultContent, UserContent,
22};
23use crate::providers::internal::object;
24use crate::providers::internal::wire_ids::WireIds;
25use crate::wire::Mode;
26use crate::{completion, message};
27use serde::{Deserialize, Serialize, Serializer};
28use serde_json::{Map, Value, json};
29use std::sync::Arc;
30
31pub mod streaming;
32#[cfg(feature = "websocket")]
33#[cfg_attr(docsrs, doc(cfg(feature = "websocket")))]
34pub mod websocket;
35pub mod wire;
36
37fn image_part(image: &message::Image) -> Option<Value> {
41 let (key, source) = match &image.data {
42 DocumentSourceKind::Base64(data) => (
43 "image_url",
44 format!(
45 "data:{};base64,{data}",
46 image.media_type.as_ref()?.to_mime_type()
47 ),
48 ),
49 DocumentSourceKind::Url(url) => ("image_url", url.clone()),
50 DocumentSourceKind::FileId(file_id) => ("file_id", file_id.clone()),
51 _ => return None,
52 };
53 let mut part =
54 json!({"type": "input_image", "detail": image.detail.clone().unwrap_or_default()});
55 set(&mut part, key, Value::String(source));
56 Some(part)
57}
58
59fn document_part(document: &Document) -> Option<Value> {
63 let source = match &document.data {
64 DocumentData::Text(text) => return Some(json!({"type": "input_text", "text": text})),
65 DocumentData::File(source) => source,
66 };
67 Some(match source {
68 DocumentSourceKind::FileId(file_id) => json!({"type": "input_file", "file_id": file_id}),
69 DocumentSourceKind::Url(url) => json!({"type": "input_file", "file_url": url}),
71 DocumentSourceKind::Base64(data) if document.media_type == Some(DocumentMediaType::PDF) => {
72 object! {
73 "type": "input_file",
74 "file_data": format!("data:application/pdf;base64,{data}"),
75 "filename": "document.pdf",
76 }
77 }
78 _ => return None,
79 })
80}
81
82fn set(value: &mut Value, key: &str, field: Value) {
84 if let Some(fields) = value.as_object_mut() {
85 fields.insert(key.to_owned(), field);
86 }
87}
88
89fn unsendable(part: &str) -> EncodeError {
91 EncodeError::request(format!(
92 "the Responses API cannot carry this {part}; prepare the request first"
93 ))
94}
95
96fn result_output(content: &[ToolResultContent]) -> Result<Value, EncodeError> {
99 let mut parts = content
100 .iter()
101 .map(|part| match part {
102 ToolResultContent::Text(text) => Ok(json!({"type": "input_text", "text": text.text})),
103 ToolResultContent::Json { value } => {
104 Ok(json!({"type": "input_text", "text": value.to_string()}))
105 }
106 ToolResultContent::Image(image) => {
107 image_part(image).ok_or_else(|| unsendable("tool-result image"))
108 }
109 })
110 .collect::<Result<Vec<_>, _>>()?;
111 Ok(match parts.as_mut_slice() {
112 [part] if part.str("type") == Some("input_text") => {
113 part.get_mut("text").map(Value::take).unwrap_or_default()
114 }
115 _ => Value::Array(parts),
116 })
117}
118
119struct Custom {
123 tools: std::collections::HashSet<String>,
125 calls: std::collections::HashMap<String, bool>,
127}
128
129fn input(
140 history: &[Arc<Message>],
141 target: &wire::Responses,
142 model: &str,
143 custom: &mut Custom,
144 stateless: bool,
145) -> Result<Vec<Value>, EncodeError> {
146 let ids = WireIds::for_target(history, target, model);
147 let mut items = Vec::new();
148 for (position, message) in history.iter().enumerate() {
149 match &**message {
150 Message::System { content } => items.push(json!({
151 "type": "message",
152 "role": "system",
153 "content": [{"type": "input_text", "text": content}],
154 })),
155 Message::User { content } => {
156 for part in content {
157 let part = match part {
158 UserContent::Text(text) if text.text.trim().is_empty() => continue,
160 UserContent::Text(text) => json!({"type": "input_text", "text": text.text}),
161 UserContent::ToolResult(result) => {
164 let call_id = ids.spell(&result.call);
165 let output = result_output(&result.content)?;
166 items.push(
167 if custom.calls.get(&call_id).copied().unwrap_or_else(|| {
168 custom.tools.contains(result.name.as_str())
169 }) {
170 object! {"type": "custom_tool_call_output", "call_id": call_id, "output": output}
171 } else {
172 object! {"type": "function_call_output", "call_id": call_id, "output": output, "status": "completed"}
173 },
174 );
175 continue;
176 }
177 UserContent::Image(image) => {
178 image_part(image).ok_or_else(|| unsendable("image"))?
179 }
180 UserContent::Document(document) => {
181 document_part(document).ok_or_else(|| unsendable("document"))?
182 }
183 UserContent::Audio(_) => return Err(unsendable("audio")),
184 UserContent::Video(_) => return Err(unsendable("video")),
185 };
186 items.push(object! {"type": "message", "role": "user", "content": vec![part]});
187 }
188 }
189 Message::Assistant(turn) => {
190 let mut texts = 0usize;
191 let mut unpaired = false;
192 for block in &turn.content {
193 let mut replay = block.replay(target, &ids);
194 if unpaired && !matches!(block, AssistantContent::Reasoning(_)) {
195 unpaired = false;
196 replay = Replay::Rebuild;
197 }
198 let ciphertext = |item: &Map<String, Value>| {
199 item.get("encrypted_content")
200 .and_then(Value::as_str)
201 .is_some_and(|cipher| !cipher.is_empty())
202 };
203 let unresolvable = stateless
204 && matches!(block, AssistantContent::Reasoning(_))
205 && match &replay {
206 Replay::Item(item) => !item.as_object().is_some_and(ciphertext),
207 Replay::Identity(identity) => !ciphertext(identity),
208 Replay::Rebuild => false,
209 };
210 if unresolvable {
211 unpaired = true;
212 continue;
213 }
214 let identity = match replay {
215 Replay::Item(item) => {
216 if let Some(call_id) = item.str("call_id") {
217 let kind = item.str("type");
218 if matches!(kind, Some("custom_tool_call" | "function_call")) {
219 custom.calls.insert(
220 call_id.to_owned(),
221 kind == Some("custom_tool_call"),
222 );
223 }
224 }
225 items.push(item.into_owned());
226 continue;
227 }
228 Replay::Identity(identity) => identity,
229 Replay::Rebuild => Map::new(),
230 };
231 let id = |prefix: &str| {
232 identity
233 .get("id")
234 .and_then(Value::as_str)
235 .filter(|id| !id.is_empty() && id.starts_with(prefix) && id.len() <= 64)
236 .map(str::to_owned)
237 };
238 let item = match block {
239 AssistantContent::Text(text) => {
240 let synthetic = match texts {
241 0 => format!("msg_rig_{position}"),
242 n => format!("msg_rig_{position}_{n}"),
243 };
244 texts += 1;
245 let mut item = json!({
246 "type": "message",
247 "role": "assistant",
248 "content": [{"type": "output_text", "text": text.text, "annotations": []}],
249 "status": "completed",
250 "id": id("").unwrap_or(synthetic),
251 });
252 if let Some(phase) =
253 identity.get("phase").filter(|phase| phase.is_string())
254 {
255 set(&mut item, "phase", phase.clone());
256 }
257 items.push(item);
258 continue;
259 }
260 AssistantContent::ToolCall(call) => {
261 let call_id = ids.spell(&call.id);
262 let name = call.function.name.as_str();
263 let kind = identity.get("type").and_then(Value::as_str);
264 let arguments = || {
265 serde_json::to_string(&call.function.arguments).unwrap_or_default()
266 };
267 let (kind, key, payload, prefix) = if kind == Some("custom_tool_call")
268 || (kind.is_none() && custom.tools.contains(name))
269 {
270 custom.calls.insert(call_id.clone(), true);
271 let input =
272 call.function.arguments.get("input").and_then(Value::as_str);
273 let input = input.map_or_else(arguments, str::to_owned);
274 ("custom_tool_call", "input", input, "ctc_")
275 } else {
276 custom.calls.insert(call_id.clone(), false);
277 ("function_call", "arguments", arguments(), "fc_")
278 };
279 let mut item = json!({"type": kind, "call_id": call_id, "name": name});
280 set(&mut item, key, Value::String(payload));
281 if let Some(id) = id(prefix) {
282 set(&mut item, "id", json!(id));
283 }
284 item
285 }
286 AssistantContent::Reasoning(reasoning) => {
289 let Some(rs) = id("") else {
290 continue;
291 };
292 let summary: Vec<Value> = (!reasoning.text.is_empty())
293 .then(|| json!({"type": "summary_text", "text": reasoning.text}))
294 .into_iter()
295 .collect();
296 let mut item =
297 object! {"type": "reasoning", "id": rs, "summary": summary};
298 if let Some(ciphertext) = identity.get("encrypted_content") {
299 set(&mut item, "encrypted_content", ciphertext.clone());
300 }
301 item
302 }
303 AssistantContent::Opaque(opaque) if opaque.replay => opaque.item.clone(),
304 AssistantContent::Opaque(_) => continue,
305 AssistantContent::Image(_) => return Err(unsendable("assistant image")),
306 };
307 items.push(item);
308 }
309 }
310 }
311 }
312 Ok(items)
313}
314
315fn tool_choice(choice: message::ToolChoice) -> Result<Value, EncodeError> {
317 Ok(match choice {
318 message::ToolChoice::Auto => json!("auto"),
319 message::ToolChoice::None => json!("none"),
320 message::ToolChoice::Required => json!("required"),
321 message::ToolChoice::Specific { function_names } => match function_names.as_slice() {
322 [] => {
323 return Err(EncodeError::request(
324 "ToolChoice::Specific requires at least one function name",
325 ));
326 }
327 [name] => json!({"type": "function", "name": name}),
328 names => json!({
329 "type": "allowed_tools",
330 "mode": "required",
331 "tools": names.iter().map(|name| json!({"type": "function", "name": name})).collect::<Vec<_>>(),
332 }),
333 },
334 })
335}
336
337pub(crate) fn include_ciphertext(body: &mut Map<String, Value>) {
340 const CIPHERTEXT: &str = "reasoning.encrypted_content";
341 let mut include = body
342 .get("include")
343 .and_then(Value::as_array)
344 .cloned()
345 .unwrap_or_default();
346 if !include.iter().any(|item| item == CIPHERTEXT) {
347 include.push(json!(CIPHERTEXT));
348 }
349 body.insert("include".to_owned(), Value::Array(include));
350}
351
352#[derive(Clone, Copy, Debug, PartialEq, Eq)]
355pub(crate) enum Delivery {
356 Http(Mode),
358 WebSocket,
360}
361
362impl wire::Responses {
363 pub(crate) fn responses_request(
369 &self,
370 request: &completion::CompletionRequest,
371 delivery: Delivery,
372 ) -> Result<FinalBody, EncodeError> {
373 let codex =
374 self.provider.dialect.quirks.responses.contract == wire::ResponsesContract::Codex;
375 let mut rewrites = Vec::new();
376 match delivery {
377 Delivery::Http(Mode::Streaming) => rewrites.push(Rewrite::Stream(true)),
378 Delivery::Http(Mode::Unary) | Delivery::WebSocket => rewrites.push(Rewrite::NoStream),
379 }
380 if delivery == Delivery::WebSocket {
381 rewrites.push(Rewrite::NoBackground);
382 }
383 if codex {
384 rewrites.push(Rewrite::CodexStore);
385 }
386 rewrites.push(Rewrite::ReasoningCiphertext(codex));
388 rewrites.push(Rewrite::MoveLast("input"));
393 rewrites.push(Rewrite::MoveLast("max_output_tokens"));
394 let body = request_params(
395 self,
396 request,
397 |layers| self.base(request, codex, layers),
398 RawAt::Top,
399 &rewrites,
400 )?;
401 crate::providers::openai::options::check_body(
402 self,
403 &self.facts,
404 request,
405 body,
406 crate::providers::openai::options::Endpoint::Responses,
407 )
408 }
409
410 fn base(
415 &self,
416 request: &completion::CompletionRequest,
417 codex: bool,
418 layers: &mut BaseInput<'_>,
419 ) -> Result<Map<String, Value>, EncodeError> {
420 let model = request.model.clone().unwrap_or_else(|| self.model.clone());
421 let mut tools: Vec<ResponsesToolDefinition> = request
422 .tools
423 .iter()
424 .map(|tool| ResponsesToolDefinition::from(completion::ToolDefinition::clone(tool)))
425 .collect();
426 let extra = layers.raw_tools()?;
427 if !extra.is_empty() {
428 tools.extend(
429 serde_json::from_value::<Vec<ResponsesToolDefinition>>(Value::Array(extra))
430 .map_err(|err| {
431 EncodeError::request(format!(
432 "Invalid OpenAI Responses tools payload in additional_params: {err}"
433 ))
434 })?,
435 );
436 }
437 tools.extend(self.tools.iter().cloned());
438 if self.strict_tools {
439 tools = tools
440 .into_iter()
441 .map(ResponsesToolDefinition::with_strict)
442 .collect();
443 }
444 let mut custom = Custom {
445 tools: tools
446 .iter()
447 .filter(|tool| tool.kind == "custom")
448 .map(|tool| tool.name.clone())
449 .collect(),
450 calls: Default::default(),
451 };
452 let stateless = codex || layers.param("store") == Some(&json!(false));
453 let mut items = input(&request.chat_history, self, &model, &mut custom, stateless)?;
454
455 let system = |item: &Value| {
456 (item.str("role") == Some("system")).then(|| {
457 item.at("/content/0/text")
458 .and_then(Value::as_str)
459 .unwrap_or_default()
460 .to_owned()
461 })
462 };
463 let before = items.len();
464 let mut lifted = Vec::new();
465 match self.system_instructions {
466 SystemInstructionsPlacement::Instructions => {
469 let leading = items
470 .iter()
471 .take_while(|item| system(item).is_some())
472 .count();
473 if leading < items.len() {
474 lifted.extend(items.drain(..leading).filter_map(|item| system(&item)));
475 }
476 }
477 SystemInstructionsPlacement::AllInstructions => {
478 items.retain(|item| match system(item) {
479 Some(text) => {
480 lifted.push(text);
481 false
482 }
483 None => true,
484 })
485 }
486 SystemInstructionsPlacement::InputSystemMessages => {}
487 }
488 if items.is_empty() {
489 return Err(EncodeError::request(if items.len() < before {
490 "OpenAI Responses request input must contain at least one non-system item \
491 (system messages were lifted into the top-level `instructions` field)"
492 } else {
493 "OpenAI Responses request input must contain at least one item"
494 }));
495 }
496 let lifted: Vec<&str> = lifted
497 .iter()
498 .map(|text| text.trim())
499 .filter(|text| !text.is_empty())
500 .collect();
501 let lifted = lifted.join("\n\n");
502 let instructions = match &self.provider.instructions {
504 Some(gateway) if lifted.is_empty() => Some(gateway.clone()),
505 Some(gateway) if !lifted.contains(gateway.as_str()) => {
506 Some(format!("{gateway}\n\n{lifted}"))
507 }
508 _ => (!lifted.is_empty()).then_some(lifted),
509 };
510 let text = request
511 .output_schema
512 .clone()
513 .filter(|_| !codex)
514 .map(|schema| {
515 let (name, schema) = super::structured_output_schema(schema);
516 json!({"format": {"type": "json_schema", "name": name, "schema": schema, "strict": true}})
517 });
518
519 Ok(crate::providers::internal::set_fields([
520 ("model", Some(Value::String(model))),
521 ("input", Some(Value::Array(items))),
522 ("instructions", instructions.map(Value::from)),
523 (
524 "max_output_tokens",
525 request.max_tokens.filter(|_| !codex).map(Value::from),
526 ),
527 (
528 "temperature",
529 request.temperature.filter(|_| !codex).map(Value::from),
530 ),
531 (
532 "tool_choice",
533 request.tool_choice.clone().map(tool_choice).transpose()?,
534 ),
535 ("tools", (!tools.is_empty()).then(|| json!(tools))),
536 ("text", text),
537 ]))
538 }
539}
540
541#[derive(Debug, Deserialize, Clone, PartialEq)]
543pub struct ResponsesToolDefinition {
544 #[serde(rename = "type")]
546 pub kind: String,
547 #[serde(default)]
549 pub name: String,
550 #[serde(default)]
553 pub parameters: serde_json::Value,
554 #[serde(default, deserialize_with = "json_utils::null_or_default")]
562 pub strict: bool,
563 #[serde(default, deserialize_with = "json_utils::null_or_default")]
565 pub description: String,
566 #[serde(flatten, default)]
568 pub config: Map<String, Value>,
569}
570
571impl Serialize for ResponsesToolDefinition {
572 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
573 where
574 S: Serializer,
575 {
576 use serde::ser::SerializeMap;
577
578 let mut map = serializer.serialize_map(None)?;
579 map.serialize_entry("type", &self.kind)?;
580 if !self.name.is_empty() {
581 map.serialize_entry("name", &self.name)?;
582 }
583 if !self.parameters.is_null() {
584 map.serialize_entry("parameters", &self.parameters)?;
585 }
586 if self.kind == "function" {
587 map.serialize_entry("strict", &self.strict)?;
588 }
589 if !self.description.is_empty() {
590 map.serialize_entry("description", &self.description)?;
591 }
592 for (key, value) in &self.config {
593 map.serialize_entry(key, value)?;
594 }
595 map.end()
596 }
597}
598
599impl ResponsesToolDefinition {
600 pub fn function(
602 name: impl Into<String>,
603 description: impl Into<String>,
604 parameters: serde_json::Value,
605 ) -> Self {
606 Self {
607 kind: "function".to_string(),
608 name: name.into(),
609 parameters,
610 strict: false,
611 description: description.into(),
612 config: Map::new(),
613 }
614 }
615
616 pub fn strict_function(
621 name: impl Into<String>,
622 description: impl Into<String>,
623 parameters: serde_json::Value,
624 ) -> Self {
625 Self::function(name, description, parameters).with_strict()
626 }
627
628 pub fn with_strict(mut self) -> Self {
633 if self.kind == "function" {
634 super::sanitize_schema(&mut self.parameters);
635 self.strict = true;
636 }
637 self
638 }
639
640 pub fn hosted(kind: impl Into<String>) -> Self {
642 Self {
643 kind: kind.into(),
644 name: String::new(),
645 parameters: Value::Null,
646 strict: false,
647 description: String::new(),
648 config: Map::new(),
649 }
650 }
651
652 pub fn web_search() -> Self {
654 Self::hosted("web_search")
655 }
656
657 pub fn file_search() -> Self {
659 Self::hosted("file_search")
660 }
661
662 pub fn computer_use() -> Self {
664 Self::hosted("computer_use")
665 }
666
667 pub fn with_config(mut self, key: impl Into<String>, value: Value) -> Self {
669 self.config.insert(key.into(), value);
670 self
671 }
672}
673
674impl From<completion::ToolDefinition> for ResponsesToolDefinition {
675 fn from(value: completion::ToolDefinition) -> Self {
676 let completion::ToolDefinition {
678 name,
679 parameters,
680 description,
681 annotations: _,
682 } = value;
683
684 Self::function(name, description, parameters)
685 }
686}
687
688#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
693#[serde(rename_all = "snake_case")]
694pub enum SystemInstructionsPlacement {
695 #[default]
700 Instructions,
701 AllInstructions,
706 InputSystemMessages,
711}
712
713#[cfg(test)]
714mod history_tests;
715#[cfg(test)]
716mod tests;