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