Skip to main content

openai_interface/responses/create/
request.rs

1//! Request body and POST method for the Responses API (`POST /responses`).
2//!
3//! The request format follows the OpenAI Responses API as implemented by the
4//! official Python SDK, restricted to the parameters documented by OpenAI and
5//! the compatible providers (DeepSeek, Qwen). Provider-proprietary parameters
6//! are opt-in via the `deepseek` / `qwen` cargo features.
7
8use std::collections::HashMap;
9
10use serde::{Deserialize, Serialize};
11use url::Url;
12
13use crate::{
14    chat::{ServiceTier, create::request::ReasoningEffort},
15    errors::OapiError,
16    rest::post::{Post, PostNoStream, PostStream},
17};
18
19/// Creates a model response for the given input.
20///
21/// # Example
22///
23/// ```rust,no_run
24/// use openai_interface::responses::create::request::{Input, RequestBody};
25/// use openai_interface::rest::{default_client, post::PostNoStream, RequestOptions};
26///
27/// const DEEPSEEK_URL: &'static str = "https://api.deepseek.com";
28/// const DEEPSEEK_MODEL: &'static str = "deepseek-v4-flash";
29///
30/// #[tokio::main]
31/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
32///     // Needs the `ferritls` cargo feature; drop this line if you install
33///     // your own rustls crypto provider (see `openai_interface::rest`).
34///     # #[cfg(feature = "ferritls")]
35///     openai_interface::rest::install_crypto_provider().ok();
36///
37///     let request = RequestBody {
38///         model: DEEPSEEK_MODEL.to_string(),
39///         input: Input::Text("Hello!".to_string()),
40///         instructions: Some("You are a helpful assistant.".to_string()),
41///         ..Default::default()
42///     };
43///
44///     let response = request
45///         .get_response(&default_client(), DEEPSEEK_URL, &RequestOptions::bearer("YOUR_API_KEY"))
46///         .await?;
47///     println!("{}", response.output_text());
48///     Ok(())
49/// }
50/// ```
51#[derive(Serialize, Debug, Default, Clone)]
52pub struct RequestBody {
53    /// Whether to run the model response in the background, returning a
54    /// queued response immediately. Not supported by DeepSeek and Qwen
55    /// (synchronous calls only).
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub background: Option<bool>,
58
59    /// The ID of an existing conversation to prepend to the input. Items
60    /// from this response are automatically added to the conversation after
61    /// it completes. Mutually exclusive with `previous_response_id`.
62    /// Not supported by DeepSeek (stateless API).
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub conversation: Option<String>,
65
66    /// Additional output data to include in the model response, e.g.
67    /// `"message.output_text.logprobs"` or `"reasoning.encrypted_content"`.
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub include: Option<Vec<String>>,
70
71    /// Text, image, or file inputs to the model, used to generate a
72    /// response. Either a plain string (treated as one `user` message) or
73    /// a list of input items. Together with [`Self::instructions`], at
74    /// least one must be provided.
75    pub input: Input,
76
77    /// A system (or developer) message inserted into the model's context.
78    /// When used along with `previous_response_id`, the instructions from a
79    /// previous response are not carried over.
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub instructions: Option<String>,
82
83    /// An upper bound for the number of tokens that can be generated for a
84    /// response, including visible output tokens and reasoning tokens.
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub max_output_tokens: Option<u32>,
87
88    /// The maximum number of total calls to built-in tools that can be
89    /// processed in a response.
90    #[serde(skip_serializing_if = "Option::is_none")]
91    pub max_tool_calls: Option<u32>,
92
93    /// Set of 16 key-value pairs that can be attached to an object. Keys
94    /// are strings with a maximum length of 64 characters; values are
95    /// strings with a maximum length of 512 characters. Not supported by
96    /// DeepSeek.
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub metadata: Option<HashMap<String, String>>,
99
100    /// Model ID used to generate the response, e.g. `deepseek-v4-flash` or
101    /// `qwen3.8-max`.
102    pub model: String,
103
104    /// Whether to allow the model to run tool calls in parallel. Ignored by
105    /// DeepSeek (parallel tool calls are always enabled).
106    #[serde(skip_serializing_if = "Option::is_none")]
107    pub parallel_tool_calls: Option<bool>,
108
109    /// The unique ID of the previous response to the model. Use this to
110    /// create multi-turn conversations without resending the full input
111    /// history. Mutually exclusive with `conversation`. Not supported by
112    /// DeepSeek (stateless API); supported by Qwen.
113    #[serde(skip_serializing_if = "Option::is_none")]
114    pub previous_response_id: Option<String>,
115
116    /// Used by OpenAI to cache responses for similar requests to optimize
117    /// cache hit rates. Replaces the `user` field. Not supported by
118    /// DeepSeek (caching is automatic there).
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub prompt_cache_key: Option<String>,
121
122    /// Qwen: whether to enable thinking mode for hybrid-thinking models.
123    /// The thinking content is returned via `reasoning` output items.
124    /// Non-standard parameter; `reasoning.effort` is preferred.
125    #[cfg(feature = "qwen")]
126    #[serde(skip_serializing_if = "Option::is_none")]
127    pub enable_thinking: Option<bool>,
128
129    /// Configuration options for reasoning models.
130    #[serde(skip_serializing_if = "Option::is_none")]
131    pub reasoning: Option<ReasoningConfig>,
132
133    /// A stable identifier used to help detect users of your application
134    /// that may be violating the usage policies.
135    #[serde(skip_serializing_if = "Option::is_none")]
136    pub safety_identifier: Option<String>,
137
138    /// Specifies the processing type used for serving the request. Not
139    /// supported by DeepSeek.
140    #[serde(skip_serializing_if = "Option::is_none")]
141    pub service_tier: Option<ServiceTier>,
142
143    /// Whether to store the generated model response for later retrieval
144    /// via the retrieve endpoint. DeepSeek always stores nothing (its
145    /// Responses API is stateless).
146    #[serde(skip_serializing_if = "Option::is_none")]
147    pub store: Option<bool>,
148
149    /// If set to `true`, the response is streamed back as semantic
150    /// server-sent events (see [`super::response::ResponseStreamEvent`]).
151    #[serde(skip_serializing_if = "Option::is_none")]
152    pub stream: Option<bool>,
153
154    /// What sampling temperature to use, between 0 and 2. Higher values
155    /// make the output more random. Has no effect in thinking mode.
156    #[serde(skip_serializing_if = "Option::is_none")]
157    pub temperature: Option<f32>,
158
159    /// Configuration options for a text response from the model: plain
160    /// text or structured JSON data.
161    #[serde(skip_serializing_if = "Option::is_none")]
162    pub text: Option<TextConfig>,
163
164    /// How the model should select which tool (or tools) to use when
165    /// generating a response.
166    #[serde(skip_serializing_if = "Option::is_none")]
167    pub tool_choice: Option<ToolChoice>,
168
169    /// An array of tools the model may call while generating a response.
170    #[serde(skip_serializing_if = "Option::is_none")]
171    pub tools: Option<Vec<Tool>>,
172
173    /// An integer between 0 and 20 specifying the number of most likely
174    /// tokens to return at each token position, each with an associated
175    /// log probability.
176    #[serde(skip_serializing_if = "Option::is_none")]
177    pub top_logprobs: Option<u32>,
178
179    /// An alternative to sampling with temperature, called nucleus
180    /// sampling. Has no effect in thinking mode.
181    #[serde(skip_serializing_if = "Option::is_none")]
182    pub top_p: Option<f32>,
183
184    /// The truncation strategy to use for the model response. DeepSeek
185    /// only supports `disabled` (over-long inputs return an error).
186    #[serde(skip_serializing_if = "Option::is_none")]
187    pub truncation: Option<Truncation>,
188
189    /// A stable identifier for your end-users. Being replaced by
190    /// `safety_identifier` and `prompt_cache_key`.
191    #[serde(skip_serializing_if = "Option::is_none")]
192    pub user: Option<String>,
193
194    /// Other request body fields that are not covered above. Useful for
195    /// provider-specific parameters such as Qwen's `ocr_options`.
196    #[serde(flatten, skip_serializing_if = "Option::is_none")]
197    pub extra_body_map: Option<serde_json::Map<String, serde_json::Value>>,
198}
199
200/// The `input` parameter: either a plain string (treated as a single `user`
201/// message) or a list of input items.
202#[derive(Serialize, Debug, Clone)]
203#[serde(untagged)]
204pub enum Input {
205    /// A plain-text input, treated as a single `user` message.
206    Text(String),
207    /// A list of input items describing the conversation so far.
208    Items(Vec<InputItem>),
209}
210
211impl Default for Input {
212    fn default() -> Self {
213        Self::Text(String::new())
214    }
215}
216
217impl From<&str> for Input {
218    fn from(value: &str) -> Self {
219        Self::Text(value.to_string())
220    }
221}
222
223impl From<String> for Input {
224    fn from(value: String) -> Self {
225        Self::Text(value)
226    }
227}
228
229impl From<Vec<InputItem>> for Input {
230    fn from(value: Vec<InputItem>) -> Self {
231        Self::Items(value)
232    }
233}
234
235/// An item of the input list.
236///
237/// Item types not listed here can be sent through
238/// [`Self::Other`] as raw JSON.
239#[derive(Serialize, Debug, Clone)]
240#[serde(tag = "type", rename_all = "snake_case")]
241pub enum InputItem {
242    /// A message with a role and content.
243    Message {
244        /// The role of the message author: `user`, `assistant`, `system`,
245        /// or `developer`.
246        role: Role,
247        /// The message content: plain text or an array of content parts.
248        content: MessageContent,
249        /// The unique ID of the message (present when passing back an
250        /// output message from a previous response).
251        #[serde(skip_serializing_if = "Option::is_none")]
252        id: Option<String>,
253        /// The status of the message (present when passing back an output
254        /// message from a previous response).
255        #[serde(skip_serializing_if = "Option::is_none")]
256        status: Option<String>,
257    },
258    /// A call to a user-defined function, e.g. returned by a previous
259    /// response. Must be paired with a matching
260    /// [`InputItem::FunctionCallOutput`].
261    FunctionCall {
262        /// The ID that pairs this call with its output.
263        call_id: String,
264        /// The name of the function to call.
265        #[serde(skip_serializing_if = "Option::is_none")]
266        name: Option<String>,
267        /// The arguments of the call, as a JSON string.
268        #[serde(skip_serializing_if = "Option::is_none")]
269        arguments: Option<String>,
270        /// The unique ID of the function call item.
271        #[serde(skip_serializing_if = "Option::is_none")]
272        id: Option<String>,
273    },
274    /// The output of a function call.
275    FunctionCallOutput {
276        /// The ID that pairs this output with the corresponding
277        /// [`InputItem::FunctionCall`].
278        call_id: String,
279        /// The result of the function call: plain text or an array of
280        /// content parts.
281        output: FunctionCallOutputContent,
282    },
283    /// A reasoning item from a previous response.
284    Reasoning {
285        /// The unique ID of the reasoning item.
286        #[serde(skip_serializing_if = "Option::is_none")]
287        id: Option<String>,
288        /// Plaintext reasoning content parts.
289        #[serde(skip_serializing_if = "Option::is_none")]
290        content: Option<Vec<ReasoningTextPart>>,
291        /// Reasoning summaries.
292        #[serde(skip_serializing_if = "Option::is_none")]
293        summary: Option<Vec<SummaryTextPart>>,
294    },
295    /// A web search call from a previous response, passed back as-is so
296    /// the server can restore the search results.
297    WebSearchCall {
298        /// The unique ID of the web search call.
299        #[serde(skip_serializing_if = "Option::is_none")]
300        id: Option<String>,
301        /// The search action performed by the server.
302        #[serde(skip_serializing_if = "Option::is_none")]
303        action: Option<serde_json::Value>,
304    },
305    /// A call to a custom tool (e.g. the `apply_patch` tool used by
306    /// Codex-style agents).
307    CustomToolCall {
308        /// The ID that pairs this call with its output.
309        call_id: String,
310        /// The name of the custom tool.
311        name: String,
312        /// The input of the tool call.
313        input: String,
314        /// The unique ID of the custom tool call item.
315        #[serde(skip_serializing_if = "Option::is_none")]
316        id: Option<String>,
317    },
318    /// The output of a custom tool call.
319    CustomToolCallOutput {
320        /// The ID that pairs this output with the corresponding
321        /// [`InputItem::CustomToolCall`].
322        call_id: String,
323        /// The result of the tool call: plain text or an array of content
324        /// parts.
325        output: FunctionCallOutputContent,
326    },
327    /// An input item of a type not covered by the variants above, sent
328    /// through as raw JSON.
329    #[serde(untagged)]
330    Other(serde_json::Value),
331}
332
333/// The role of a message author.
334#[derive(Serialize, Debug, Clone, Copy, PartialEq, Eq)]
335#[serde(rename_all = "lowercase")]
336pub enum Role {
337    User,
338    Assistant,
339    System,
340    Developer,
341}
342
343/// The content of an input message: either plain text or an array of
344/// content parts.
345#[derive(Serialize, Debug, Clone)]
346#[serde(untagged)]
347pub enum MessageContent {
348    /// Plain-text message content.
349    Text(String),
350    /// An array of content parts (`input_text`, `input_image`, ...).
351    Parts(Vec<InputContentPart>),
352}
353
354impl From<&str> for MessageContent {
355    fn from(value: &str) -> Self {
356        Self::Text(value.to_string())
357    }
358}
359
360impl From<String> for MessageContent {
361    fn from(value: String) -> Self {
362        Self::Text(value)
363    }
364}
365
366impl From<Vec<InputContentPart>> for MessageContent {
367    fn from(value: Vec<InputContentPart>) -> Self {
368        Self::Parts(value)
369    }
370}
371
372/// The output content of a function (or custom tool) call: either plain
373/// text or an array of content parts.
374#[derive(Serialize, Debug, Clone)]
375#[serde(untagged)]
376pub enum FunctionCallOutputContent {
377    /// Plain-text output.
378    Text(String),
379    /// An array of content parts (`input_text`, `input_image`, ...).
380    Parts(Vec<InputContentPart>),
381}
382
383impl From<&str> for FunctionCallOutputContent {
384    fn from(value: &str) -> Self {
385        Self::Text(value.to_string())
386    }
387}
388
389impl From<String> for FunctionCallOutputContent {
390    fn from(value: String) -> Self {
391        Self::Text(value)
392    }
393}
394
395/// A content part of an input message or tool output.
396#[derive(Serialize, Debug, Clone)]
397#[serde(tag = "type", rename_all = "snake_case")]
398pub enum InputContentPart {
399    /// A text input part.
400    InputText {
401        /// The text content.
402        text: String,
403    },
404    /// An assistant text output part (only valid in `assistant` messages).
405    OutputText {
406        /// The text content.
407        text: String,
408    },
409    /// An image input part. `image_url` and `file_id` are mutually
410    /// exclusive; exactly one must be provided.
411    InputImage {
412        /// The image source: an `http(s)` URL, a base64 data URL
413        /// (`data:image/jpeg;base64,...`), or OpenAI's object form.
414        #[serde(skip_serializing_if = "Option::is_none")]
415        image_url: Option<ImageUrl>,
416        /// How the image should be processed.
417        #[serde(skip_serializing_if = "Option::is_none")]
418        detail: Option<ImageDetail>,
419        /// The ID of an uploaded image file. Ignored together with
420        /// `detail` by DeepSeek when set.
421        #[serde(skip_serializing_if = "Option::is_none")]
422        file_id: Option<String>,
423    },
424    /// Qwen: a file input part (PDF or image), referenced by a public URL.
425    /// Only the `qwen3.5-ocr` model supports this part type.
426    #[cfg(feature = "qwen")]
427    InputFile {
428        /// The public URL of the file.
429        file_url: String,
430    },
431}
432
433/// The source of an input image. DeepSeek and Qwen take a plain URL
434/// string; OpenAI takes an object with a `url` field.
435#[derive(Serialize, Debug, Clone)]
436#[serde(untagged)]
437pub enum ImageUrl {
438    /// A plain URL string (DeepSeek / Qwen form).
439    Url(String),
440    /// The OpenAI object form.
441    Object {
442        /// The URL of the image.
443        url: String,
444    },
445}
446
447/// Controls how an input image is processed. `low` downscales the image
448/// before inference (faster, fewer tokens); the other values keep the
449/// original image.
450#[derive(Serialize, Debug, Clone, Copy, PartialEq, Eq)]
451#[serde(rename_all = "lowercase")]
452pub enum ImageDetail {
453    Low,
454    High,
455    Auto,
456    Original,
457}
458
459/// A plaintext reasoning content part.
460#[derive(Serialize, Debug, Clone)]
461#[serde(tag = "type", rename = "reasoning_text")]
462pub struct ReasoningTextPart {
463    /// The chain-of-thought text.
464    pub text: String,
465}
466
467/// A reasoning summary content part.
468#[derive(Serialize, Debug, Clone)]
469#[serde(tag = "type", rename = "summary_text")]
470pub struct SummaryTextPart {
471    /// The summary text.
472    pub text: String,
473}
474
475/// Configuration options for reasoning models.
476#[derive(Serialize, Debug, Clone, Default)]
477pub struct ReasoningConfig {
478    /// Constrains the effort on reasoning. Reducing the effort can result
479    /// in faster responses and fewer reasoning tokens. Providers accept
480    /// subsets of the values and map unsupported values to their nearest
481    /// effort level; DeepSeek maps `none` to thinking mode off.
482    #[serde(skip_serializing_if = "Option::is_none")]
483    pub effort: Option<ReasoningEffort>,
484
485    /// The verbosity of reasoning summaries generated by the model.
486    #[serde(skip_serializing_if = "Option::is_none")]
487    pub summary: Option<SummaryEffort>,
488}
489
490/// The verbosity of reasoning summaries.
491#[derive(Serialize, Debug, Clone, Copy, PartialEq, Eq)]
492#[serde(rename_all = "lowercase")]
493pub enum SummaryEffort {
494    Concise,
495    Detailed,
496    Auto,
497}
498
499/// Configuration options for a text response from the model.
500#[derive(Serialize, Debug, Clone, Default)]
501pub struct TextConfig {
502    /// The output format: plain text, JSON mode, or Structured Outputs
503    /// with a JSON schema.
504    #[serde(skip_serializing_if = "Option::is_none")]
505    pub format: Option<TextFormat>,
506}
507
508/// The format of the text output.
509#[derive(Serialize, Debug, Clone)]
510#[serde(tag = "type", rename_all = "snake_case")]
511pub enum TextFormat {
512    /// Plain-text output (the default).
513    Text,
514    /// JSON mode: the model output is guaranteed to be valid JSON.
515    JsonObject,
516    /// Structured Outputs: the model output matches the supplied JSON
517    /// schema.
518    JsonSchema {
519        /// The name of the schema.
520        name: String,
521        /// A description of what the schema is for.
522        #[serde(skip_serializing_if = "Option::is_none")]
523        description: Option<String>,
524        /// The JSON schema the output must conform to.
525        schema: serde_json::Value,
526        /// Whether to enable strict schema adherence.
527        #[serde(skip_serializing_if = "Option::is_none")]
528        strict: Option<bool>,
529    },
530}
531
532/// How the model should select which tool (or tools) to use.
533#[derive(Serialize, Debug, Clone)]
534#[serde(untagged)]
535pub enum ToolChoice {
536    /// A mode string: `none`, `auto`, or `required`.
537    Mode(ToolChoiceMode),
538    /// A specific tool to force the model to call.
539    Specific(ToolChoiceSpecific),
540}
541
542/// The tool choice mode.
543#[derive(Serialize, Debug, Clone, Copy, PartialEq, Eq)]
544#[serde(rename_all = "lowercase")]
545pub enum ToolChoiceMode {
546    /// The model will not call any tool and instead generates a message.
547    None,
548    /// The model can pick between generating a message or calling one or
549    /// more tools (the default).
550    Auto,
551    /// The model must call one or more tools.
552    Required,
553}
554
555/// A specific tool the model is forced to call.
556#[derive(Serialize, Debug, Clone)]
557#[serde(tag = "type", rename_all = "snake_case")]
558pub enum ToolChoiceSpecific {
559    /// Force a specific function call by name.
560    Function {
561        /// The name of the function to call.
562        name: String,
563    },
564    /// Force the built-in web search tool. The `tools` list must then
565    /// include a `web_search` tool.
566    WebSearch,
567    /// Force a custom tool by name.
568    Custom {
569        /// The name of the custom tool.
570        name: String,
571    },
572}
573
574/// A tool the model may call while generating a response.
575#[derive(Serialize, Debug, Clone)]
576#[serde(tag = "type", rename_all = "snake_case")]
577pub enum Tool {
578    /// A user-defined function.
579    Function {
580        /// The name of the function. Must be non-empty, at most 128
581        /// characters, and match `^[a-zA-Z0-9_-]+$`; names must be unique.
582        name: String,
583        /// A description of what the function does.
584        #[serde(skip_serializing_if = "Option::is_none")]
585        description: Option<String>,
586        /// The function parameters, described as a JSON Schema object.
587        /// Omitting this defines a function with no parameters.
588        #[serde(skip_serializing_if = "Option::is_none")]
589        parameters: Option<serde_json::Value>,
590        /// Whether to enable strict schema adherence for the parameters.
591        #[serde(skip_serializing_if = "Option::is_none")]
592        strict: Option<bool>,
593    },
594    /// A server-side web search tool.
595    WebSearch,
596    /// Qwen: a web page extraction tool. Must be used together with
597    /// `web_search`.
598    #[cfg(feature = "qwen")]
599    WebExtractor,
600    /// Qwen: a code interpreter tool that executes code and returns the
601    /// result. Some models require thinking mode to be enabled.
602    #[cfg(feature = "qwen")]
603    CodeInterpreter,
604    /// Qwen: a text-to-image search tool.
605    #[cfg(feature = "qwen")]
606    WebSearchImage,
607    /// Qwen: an image-to-image search tool; the input must contain an
608    /// image URL.
609    #[cfg(feature = "qwen")]
610    ImageSearch,
611    /// Qwen: a knowledge-base search tool over one uploaded vector store.
612    #[cfg(feature = "qwen")]
613    FileSearch {
614        /// The knowledge-base (vector store) IDs to search. Currently only
615        /// one ID is supported.
616        vector_store_ids: Vec<String>,
617    },
618    /// Qwen: a Model Context Protocol (MCP) tool.
619    #[cfg(feature = "qwen")]
620    Mcp {
621        /// The protocol used to talk to the MCP server, e.g. `sse`.
622        server_protocol: String,
623        /// A label identifying the MCP server.
624        server_label: String,
625        /// The endpoint URL of the MCP server.
626        server_url: String,
627        /// A description of the MCP server for the model.
628        #[serde(skip_serializing_if = "Option::is_none")]
629        server_description: Option<String>,
630        /// Request headers, e.g. `Authorization`.
631        #[serde(skip_serializing_if = "Option::is_none")]
632        headers: Option<HashMap<String, String>>,
633    },
634}
635
636/// The truncation strategy to use when the input exceeds the model's
637/// context window.
638#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
639#[serde(rename_all = "lowercase")]
640pub enum Truncation {
641    /// Truncate items from the beginning of the conversation to fit the
642    /// context window.
643    Auto,
644    /// Fail the request with a 400 error instead (the default; the only
645    /// behavior supported by DeepSeek).
646    Disabled,
647}
648
649impl RequestBody {
650    /// Whether this request asks for a streamed response. Defaults to
651    /// `false` when [`RequestBody::stream`] is `None`.
652    pub fn is_streaming(&self) -> bool {
653        self.stream.unwrap_or(false)
654    }
655}
656
657impl Post for RequestBody {
658    fn is_streaming(&self) -> bool {
659        RequestBody::is_streaming(self)
660    }
661
662    /// Builds the URL for the request.
663    ///
664    /// `base_url` should be like <https://api.openai.com/v1>
665    fn build_url(&self, base_url: &str) -> Result<String, OapiError> {
666        let mut url = Url::parse(base_url.trim_end_matches('/')).map_err(OapiError::UrlError)?;
667        url.path_segments_mut()
668            .map_err(|_| OapiError::UrlCannotBeBase(base_url.to_string()))?
669            .push("responses");
670        Ok(url.to_string())
671    }
672}
673
674impl PostNoStream for RequestBody {
675    type Response = crate::responses::Response;
676}
677
678impl PostStream for RequestBody {
679    type Response = super::response::ResponseStreamEvent;
680}
681
682#[cfg(test)]
683mod tests {
684    //! Offline serialization tests pinning the wire format of the request
685    //! body. No network access is involved.
686
687    use super::*;
688
689    fn base_request() -> RequestBody {
690        RequestBody {
691            model: "test-model".to_string(),
692            input: Input::Text("你好".to_string()),
693            ..Default::default()
694        }
695    }
696
697    #[test]
698    fn serializes_minimal_request() {
699        let value = serde_json::to_value(base_request()).unwrap();
700        assert_eq!(
701            value,
702            serde_json::json!({
703                "model": "test-model",
704                "input": "你好",
705            })
706        );
707    }
708
709    #[test]
710    fn serializes_input_items() {
711        let request = RequestBody {
712            input: Input::Items(vec![
713                InputItem::Message {
714                    role: Role::System,
715                    content: MessageContent::Text("You are helpful.".into()),
716                    id: None,
717                    status: None,
718                },
719                InputItem::Message {
720                    role: Role::User,
721                    content: MessageContent::Parts(vec![InputContentPart::InputText {
722                        text: "hi".into(),
723                    }]),
724                    id: None,
725                    status: None,
726                },
727                InputItem::FunctionCall {
728                    call_id: "fc_1".into(),
729                    name: Some("get_weather".into()),
730                    arguments: Some("{\"city\": \"北京\"}".into()),
731                    id: None,
732                },
733                InputItem::FunctionCallOutput {
734                    call_id: "fc_1".into(),
735                    output: FunctionCallOutputContent::Text("sunny".into()),
736                },
737            ]),
738            ..base_request()
739        };
740
741        let value = serde_json::to_value(request).unwrap();
742        assert_eq!(
743            value["input"],
744            serde_json::json!([
745                {"type": "message", "role": "system", "content": "You are helpful."},
746                {"type": "message", "role": "user", "content": [{"type": "input_text", "text": "hi"}]},
747                {"type": "function_call", "call_id": "fc_1", "name": "get_weather", "arguments": "{\"city\": \"北京\"}"},
748                {"type": "function_call_output", "call_id": "fc_1", "output": "sunny"},
749            ])
750        );
751    }
752
753    #[test]
754    fn serializes_tools_and_tool_choice() {
755        let request = RequestBody {
756            tools: Some(vec![Tool::Function {
757                name: "get_weather".into(),
758                description: Some("获取天气".into()),
759                parameters: Some(serde_json::json!({
760                    "type": "object",
761                    "properties": {"city": {"type": "string"}},
762                    "required": ["city"],
763                })),
764                strict: None,
765            }]),
766            tool_choice: Some(ToolChoice::Specific(ToolChoiceSpecific::Function {
767                name: "get_weather".into(),
768            })),
769            reasoning: Some(ReasoningConfig {
770                effort: Some(ReasoningEffort::Low),
771                summary: None,
772            }),
773            text: Some(TextConfig {
774                format: Some(TextFormat::JsonObject),
775            }),
776            ..base_request()
777        };
778
779        let value = serde_json::to_value(request).unwrap();
780        assert_eq!(
781            value["tool_choice"],
782            serde_json::json!({"type": "function", "name": "get_weather"})
783        );
784        assert_eq!(value["reasoning"], serde_json::json!({"effort": "low"}));
785        assert_eq!(
786            value["text"],
787            serde_json::json!({"format": {"type": "json_object"}})
788        );
789        assert_eq!(value["tools"][0]["type"], "function");
790        assert_eq!(value["tools"][0]["name"], "get_weather");
791    }
792
793    #[test]
794    fn extra_body_flattens_into_top_level() {
795        let mut request = base_request();
796        request.extra_body_map = Some(
797            serde_json::json!({"ocr_options": {}})
798                .as_object()
799                .unwrap()
800                .clone(),
801        );
802        let value = serde_json::to_value(request).unwrap();
803        assert_eq!(value["ocr_options"], serde_json::json!({}));
804    }
805}