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}