openai-interface 0.8.1

A low-level Rust interface for the OpenAI API
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
//! Request body and POST method for the Responses API (`POST /responses`).
//!
//! The request format follows the OpenAI Responses API as implemented by the
//! official Python SDK, restricted to the parameters documented by OpenAI and
//! the compatible providers (DeepSeek, Qwen). Provider-proprietary parameters
//! are opt-in via the `deepseek` / `qwen` cargo features.

use std::collections::HashMap;

use serde::{Deserialize, Serialize};
use url::Url;

use crate::{
    chat::{ServiceTier, create::request::ReasoningEffort},
    errors::OapiError,
    rest::post::{Post, PostNoStream, PostStream},
};

/// Creates a model response for the given input.
///
/// # Example
///
/// ```rust,no_run
/// use openai_interface::responses::create::request::{Input, RequestBody};
/// use openai_interface::rest::{default_client, post::PostNoStream};
///
/// const DEEPSEEK_URL: &'static str = "https://api.deepseek.com";
/// const DEEPSEEK_MODEL: &'static str = "deepseek-v4-flash";
///
/// #[tokio::main]
/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
///     let request = RequestBody {
///         model: DEEPSEEK_MODEL.to_string(),
///         input: Input::Text("Hello!".to_string()),
///         instructions: Some("You are a helpful assistant.".to_string()),
///         ..Default::default()
///     };
///
///     let response = request
///         .get_response(&default_client(), DEEPSEEK_URL, "YOUR_API_KEY")
///         .await?;
///     println!("{}", response.output_text());
///     Ok(())
/// }
/// ```
#[derive(Serialize, Debug, Default, Clone)]
pub struct RequestBody {
    /// Whether to run the model response in the background, returning a
    /// queued response immediately. Not supported by DeepSeek and Qwen
    /// (synchronous calls only).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub background: Option<bool>,

    /// The ID of an existing conversation to prepend to the input. Items
    /// from this response are automatically added to the conversation after
    /// it completes. Mutually exclusive with `previous_response_id`.
    /// Not supported by DeepSeek (stateless API).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub conversation: Option<String>,

    /// Additional output data to include in the model response, e.g.
    /// `"message.output_text.logprobs"` or `"reasoning.encrypted_content"`.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub include: Option<Vec<String>>,

    /// Text, image, or file inputs to the model, used to generate a
    /// response. Either a plain string (treated as one `user` message) or
    /// a list of input items. Together with [`Self::instructions`], at
    /// least one must be provided.
    pub input: Input,

    /// A system (or developer) message inserted into the model's context.
    /// When used along with `previous_response_id`, the instructions from a
    /// previous response are not carried over.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub instructions: Option<String>,

    /// An upper bound for the number of tokens that can be generated for a
    /// response, including visible output tokens and reasoning tokens.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub max_output_tokens: Option<u32>,

    /// The maximum number of total calls to built-in tools that can be
    /// processed in a response.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub max_tool_calls: Option<u32>,

    /// Set of 16 key-value pairs that can be attached to an object. Keys
    /// are strings with a maximum length of 64 characters; values are
    /// strings with a maximum length of 512 characters. Not supported by
    /// DeepSeek.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub metadata: Option<HashMap<String, String>>,

    /// Model ID used to generate the response, e.g. `deepseek-v4-flash` or
    /// `qwen3.8-max`.
    pub model: String,

    /// Whether to allow the model to run tool calls in parallel. Ignored by
    /// DeepSeek (parallel tool calls are always enabled).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub parallel_tool_calls: Option<bool>,

    /// The unique ID of the previous response to the model. Use this to
    /// create multi-turn conversations without resending the full input
    /// history. Mutually exclusive with `conversation`. Not supported by
    /// DeepSeek (stateless API); supported by Qwen.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub previous_response_id: Option<String>,

    /// Used by OpenAI to cache responses for similar requests to optimize
    /// cache hit rates. Replaces the `user` field. Not supported by
    /// DeepSeek (caching is automatic there).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub prompt_cache_key: Option<String>,

    /// Qwen: whether to enable thinking mode for hybrid-thinking models.
    /// The thinking content is returned via `reasoning` output items.
    /// Non-standard parameter; `reasoning.effort` is preferred.
    #[cfg(feature = "qwen")]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub enable_thinking: Option<bool>,

    /// Configuration options for reasoning models.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub reasoning: Option<ReasoningConfig>,

    /// A stable identifier used to help detect users of your application
    /// that may be violating the usage policies.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub safety_identifier: Option<String>,

    /// Specifies the processing type used for serving the request. Not
    /// supported by DeepSeek.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub service_tier: Option<ServiceTier>,

    /// Whether to store the generated model response for later retrieval
    /// via the retrieve endpoint. DeepSeek always stores nothing (its
    /// Responses API is stateless).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub store: Option<bool>,

    /// If set to `true`, the response is streamed back as semantic
    /// server-sent events (see [`super::response::ResponseStreamEvent`]).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub stream: Option<bool>,

    /// What sampling temperature to use, between 0 and 2. Higher values
    /// make the output more random. Has no effect in thinking mode.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub temperature: Option<f32>,

    /// Configuration options for a text response from the model: plain
    /// text or structured JSON data.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub text: Option<TextConfig>,

    /// How the model should select which tool (or tools) to use when
    /// generating a response.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub tool_choice: Option<ToolChoice>,

    /// An array of tools the model may call while generating a response.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub tools: Option<Vec<Tool>>,

    /// An integer between 0 and 20 specifying the number of most likely
    /// tokens to return at each token position, each with an associated
    /// log probability.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub top_logprobs: Option<u32>,

    /// An alternative to sampling with temperature, called nucleus
    /// sampling. Has no effect in thinking mode.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub top_p: Option<f32>,

    /// The truncation strategy to use for the model response. DeepSeek
    /// only supports `disabled` (over-long inputs return an error).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub truncation: Option<Truncation>,

    /// A stable identifier for your end-users. Being replaced by
    /// `safety_identifier` and `prompt_cache_key`.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub user: Option<String>,

    /// Other request body fields that are not covered above. Useful for
    /// provider-specific parameters such as Qwen's `ocr_options`.
    #[serde(flatten, skip_serializing_if = "Option::is_none")]
    pub extra_body_map: Option<serde_json::Map<String, serde_json::Value>>,
}

/// The `input` parameter: either a plain string (treated as a single `user`
/// message) or a list of input items.
#[derive(Serialize, Debug, Clone)]
#[serde(untagged)]
pub enum Input {
    /// A plain-text input, treated as a single `user` message.
    Text(String),
    /// A list of input items describing the conversation so far.
    Items(Vec<InputItem>),
}

impl Default for Input {
    fn default() -> Self {
        Self::Text(String::new())
    }
}

impl From<&str> for Input {
    fn from(value: &str) -> Self {
        Self::Text(value.to_string())
    }
}

impl From<String> for Input {
    fn from(value: String) -> Self {
        Self::Text(value)
    }
}

impl From<Vec<InputItem>> for Input {
    fn from(value: Vec<InputItem>) -> Self {
        Self::Items(value)
    }
}

/// An item of the input list.
///
/// Item types not listed here can be sent through
/// [`Self::Other`] as raw JSON.
#[derive(Serialize, Debug, Clone)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum InputItem {
    /// A message with a role and content.
    Message {
        /// The role of the message author: `user`, `assistant`, `system`,
        /// or `developer`.
        role: Role,
        /// The message content: plain text or an array of content parts.
        content: MessageContent,
        /// The unique ID of the message (present when passing back an
        /// output message from a previous response).
        #[serde(skip_serializing_if = "Option::is_none")]
        id: Option<String>,
        /// The status of the message (present when passing back an output
        /// message from a previous response).
        #[serde(skip_serializing_if = "Option::is_none")]
        status: Option<String>,
    },
    /// A call to a user-defined function, e.g. returned by a previous
    /// response. Must be paired with a matching
    /// [`InputItem::FunctionCallOutput`].
    FunctionCall {
        /// The ID that pairs this call with its output.
        call_id: String,
        /// The name of the function to call.
        #[serde(skip_serializing_if = "Option::is_none")]
        name: Option<String>,
        /// The arguments of the call, as a JSON string.
        #[serde(skip_serializing_if = "Option::is_none")]
        arguments: Option<String>,
        /// The unique ID of the function call item.
        #[serde(skip_serializing_if = "Option::is_none")]
        id: Option<String>,
    },
    /// The output of a function call.
    FunctionCallOutput {
        /// The ID that pairs this output with the corresponding
        /// [`InputItem::FunctionCall`].
        call_id: String,
        /// The result of the function call: plain text or an array of
        /// content parts.
        output: FunctionCallOutputContent,
    },
    /// A reasoning item from a previous response.
    Reasoning {
        /// The unique ID of the reasoning item.
        #[serde(skip_serializing_if = "Option::is_none")]
        id: Option<String>,
        /// Plaintext reasoning content parts.
        #[serde(skip_serializing_if = "Option::is_none")]
        content: Option<Vec<ReasoningTextPart>>,
        /// Reasoning summaries.
        #[serde(skip_serializing_if = "Option::is_none")]
        summary: Option<Vec<SummaryTextPart>>,
    },
    /// A web search call from a previous response, passed back as-is so
    /// the server can restore the search results.
    WebSearchCall {
        /// The unique ID of the web search call.
        #[serde(skip_serializing_if = "Option::is_none")]
        id: Option<String>,
        /// The search action performed by the server.
        #[serde(skip_serializing_if = "Option::is_none")]
        action: Option<serde_json::Value>,
    },
    /// A call to a custom tool (e.g. the `apply_patch` tool used by
    /// Codex-style agents).
    CustomToolCall {
        /// The ID that pairs this call with its output.
        call_id: String,
        /// The name of the custom tool.
        name: String,
        /// The input of the tool call.
        input: String,
        /// The unique ID of the custom tool call item.
        #[serde(skip_serializing_if = "Option::is_none")]
        id: Option<String>,
    },
    /// The output of a custom tool call.
    CustomToolCallOutput {
        /// The ID that pairs this output with the corresponding
        /// [`InputItem::CustomToolCall`].
        call_id: String,
        /// The result of the tool call: plain text or an array of content
        /// parts.
        output: FunctionCallOutputContent,
    },
    /// An input item of a type not covered by the variants above, sent
    /// through as raw JSON.
    #[serde(untagged)]
    Other(serde_json::Value),
}

/// The role of a message author.
#[derive(Serialize, Debug, Clone, Copy, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum Role {
    User,
    Assistant,
    System,
    Developer,
}

/// The content of an input message: either plain text or an array of
/// content parts.
#[derive(Serialize, Debug, Clone)]
#[serde(untagged)]
pub enum MessageContent {
    /// Plain-text message content.
    Text(String),
    /// An array of content parts (`input_text`, `input_image`, ...).
    Parts(Vec<InputContentPart>),
}

impl From<&str> for MessageContent {
    fn from(value: &str) -> Self {
        Self::Text(value.to_string())
    }
}

impl From<String> for MessageContent {
    fn from(value: String) -> Self {
        Self::Text(value)
    }
}

impl From<Vec<InputContentPart>> for MessageContent {
    fn from(value: Vec<InputContentPart>) -> Self {
        Self::Parts(value)
    }
}

/// The output content of a function (or custom tool) call: either plain
/// text or an array of content parts.
#[derive(Serialize, Debug, Clone)]
#[serde(untagged)]
pub enum FunctionCallOutputContent {
    /// Plain-text output.
    Text(String),
    /// An array of content parts (`input_text`, `input_image`, ...).
    Parts(Vec<InputContentPart>),
}

impl From<&str> for FunctionCallOutputContent {
    fn from(value: &str) -> Self {
        Self::Text(value.to_string())
    }
}

impl From<String> for FunctionCallOutputContent {
    fn from(value: String) -> Self {
        Self::Text(value)
    }
}

/// A content part of an input message or tool output.
#[derive(Serialize, Debug, Clone)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum InputContentPart {
    /// A text input part.
    InputText {
        /// The text content.
        text: String,
    },
    /// An assistant text output part (only valid in `assistant` messages).
    OutputText {
        /// The text content.
        text: String,
    },
    /// An image input part. `image_url` and `file_id` are mutually
    /// exclusive; exactly one must be provided.
    InputImage {
        /// The image source: an `http(s)` URL, a base64 data URL
        /// (`data:image/jpeg;base64,...`), or OpenAI's object form.
        #[serde(skip_serializing_if = "Option::is_none")]
        image_url: Option<ImageUrl>,
        /// How the image should be processed.
        #[serde(skip_serializing_if = "Option::is_none")]
        detail: Option<ImageDetail>,
        /// The ID of an uploaded image file. Ignored together with
        /// `detail` by DeepSeek when set.
        #[serde(skip_serializing_if = "Option::is_none")]
        file_id: Option<String>,
    },
    /// Qwen: a file input part (PDF or image), referenced by a public URL.
    /// Only the `qwen3.5-ocr` model supports this part type.
    #[cfg(feature = "qwen")]
    InputFile {
        /// The public URL of the file.
        file_url: String,
    },
}

/// The source of an input image. DeepSeek and Qwen take a plain URL
/// string; OpenAI takes an object with a `url` field.
#[derive(Serialize, Debug, Clone)]
#[serde(untagged)]
pub enum ImageUrl {
    /// A plain URL string (DeepSeek / Qwen form).
    Url(String),
    /// The OpenAI object form.
    Object {
        /// The URL of the image.
        url: String,
    },
}

/// Controls how an input image is processed. `low` downscales the image
/// before inference (faster, fewer tokens); the other values keep the
/// original image.
#[derive(Serialize, Debug, Clone, Copy, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ImageDetail {
    Low,
    High,
    Auto,
    Original,
}

/// A plaintext reasoning content part.
#[derive(Serialize, Debug, Clone)]
#[serde(tag = "type", rename = "reasoning_text")]
pub struct ReasoningTextPart {
    /// The chain-of-thought text.
    pub text: String,
}

/// A reasoning summary content part.
#[derive(Serialize, Debug, Clone)]
#[serde(tag = "type", rename = "summary_text")]
pub struct SummaryTextPart {
    /// The summary text.
    pub text: String,
}

/// Configuration options for reasoning models.
#[derive(Serialize, Debug, Clone, Default)]
pub struct ReasoningConfig {
    /// Constrains the effort on reasoning. Reducing the effort can result
    /// in faster responses and fewer reasoning tokens. Providers accept
    /// subsets of the values and map unsupported values to their nearest
    /// effort level; DeepSeek maps `none` to thinking mode off.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub effort: Option<ReasoningEffort>,

    /// The verbosity of reasoning summaries generated by the model.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub summary: Option<SummaryEffort>,
}

/// The verbosity of reasoning summaries.
#[derive(Serialize, Debug, Clone, Copy, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum SummaryEffort {
    Concise,
    Detailed,
    Auto,
}

/// Configuration options for a text response from the model.
#[derive(Serialize, Debug, Clone, Default)]
pub struct TextConfig {
    /// The output format: plain text, JSON mode, or Structured Outputs
    /// with a JSON schema.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub format: Option<TextFormat>,
}

/// The format of the text output.
#[derive(Serialize, Debug, Clone)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum TextFormat {
    /// Plain-text output (the default).
    Text,
    /// JSON mode: the model output is guaranteed to be valid JSON.
    JsonObject,
    /// Structured Outputs: the model output matches the supplied JSON
    /// schema.
    JsonSchema {
        /// The name of the schema.
        name: String,
        /// A description of what the schema is for.
        #[serde(skip_serializing_if = "Option::is_none")]
        description: Option<String>,
        /// The JSON schema the output must conform to.
        schema: serde_json::Value,
        /// Whether to enable strict schema adherence.
        #[serde(skip_serializing_if = "Option::is_none")]
        strict: Option<bool>,
    },
}

/// How the model should select which tool (or tools) to use.
#[derive(Serialize, Debug, Clone)]
#[serde(untagged)]
pub enum ToolChoice {
    /// A mode string: `none`, `auto`, or `required`.
    Mode(ToolChoiceMode),
    /// A specific tool to force the model to call.
    Specific(ToolChoiceSpecific),
}

/// The tool choice mode.
#[derive(Serialize, Debug, Clone, Copy, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ToolChoiceMode {
    /// The model will not call any tool and instead generates a message.
    None,
    /// The model can pick between generating a message or calling one or
    /// more tools (the default).
    Auto,
    /// The model must call one or more tools.
    Required,
}

/// A specific tool the model is forced to call.
#[derive(Serialize, Debug, Clone)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ToolChoiceSpecific {
    /// Force a specific function call by name.
    Function {
        /// The name of the function to call.
        name: String,
    },
    /// Force the built-in web search tool. The `tools` list must then
    /// include a `web_search` tool.
    WebSearch,
    /// Force a custom tool by name.
    Custom {
        /// The name of the custom tool.
        name: String,
    },
}

/// A tool the model may call while generating a response.
#[derive(Serialize, Debug, Clone)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Tool {
    /// A user-defined function.
    Function {
        /// The name of the function. Must be non-empty, at most 128
        /// characters, and match `^[a-zA-Z0-9_-]+$`; names must be unique.
        name: String,
        /// A description of what the function does.
        #[serde(skip_serializing_if = "Option::is_none")]
        description: Option<String>,
        /// The function parameters, described as a JSON Schema object.
        /// Omitting this defines a function with no parameters.
        #[serde(skip_serializing_if = "Option::is_none")]
        parameters: Option<serde_json::Value>,
        /// Whether to enable strict schema adherence for the parameters.
        #[serde(skip_serializing_if = "Option::is_none")]
        strict: Option<bool>,
    },
    /// A server-side web search tool.
    WebSearch,
    /// Qwen: a web page extraction tool. Must be used together with
    /// `web_search`.
    #[cfg(feature = "qwen")]
    WebExtractor,
    /// Qwen: a code interpreter tool that executes code and returns the
    /// result. Some models require thinking mode to be enabled.
    #[cfg(feature = "qwen")]
    CodeInterpreter,
    /// Qwen: a text-to-image search tool.
    #[cfg(feature = "qwen")]
    WebSearchImage,
    /// Qwen: an image-to-image search tool; the input must contain an
    /// image URL.
    #[cfg(feature = "qwen")]
    ImageSearch,
    /// Qwen: a knowledge-base search tool over one uploaded vector store.
    #[cfg(feature = "qwen")]
    FileSearch {
        /// The knowledge-base (vector store) IDs to search. Currently only
        /// one ID is supported.
        vector_store_ids: Vec<String>,
    },
    /// Qwen: a Model Context Protocol (MCP) tool.
    #[cfg(feature = "qwen")]
    Mcp {
        /// The protocol used to talk to the MCP server, e.g. `sse`.
        server_protocol: String,
        /// A label identifying the MCP server.
        server_label: String,
        /// The endpoint URL of the MCP server.
        server_url: String,
        /// A description of the MCP server for the model.
        #[serde(skip_serializing_if = "Option::is_none")]
        server_description: Option<String>,
        /// Request headers, e.g. `Authorization`.
        #[serde(skip_serializing_if = "Option::is_none")]
        headers: Option<HashMap<String, String>>,
    },
}

/// The truncation strategy to use when the input exceeds the model's
/// context window.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum Truncation {
    /// Truncate items from the beginning of the conversation to fit the
    /// context window.
    Auto,
    /// Fail the request with a 400 error instead (the default; the only
    /// behavior supported by DeepSeek).
    Disabled,
}

impl RequestBody {
    /// Whether this request asks for a streamed response. Defaults to
    /// `false` when [`RequestBody::stream`] is `None`.
    pub fn is_streaming(&self) -> bool {
        self.stream.unwrap_or(false)
    }
}

impl Post for RequestBody {
    fn is_streaming(&self) -> bool {
        RequestBody::is_streaming(self)
    }

    /// Builds the URL for the request.
    ///
    /// `base_url` should be like <https://api.openai.com/v1>
    fn build_url(&self, base_url: &str) -> Result<String, OapiError> {
        let mut url = Url::parse(base_url.trim_end_matches('/')).map_err(OapiError::UrlError)?;
        url.path_segments_mut()
            .map_err(|_| OapiError::UrlCannotBeBase(base_url.to_string()))?
            .push("responses");
        Ok(url.to_string())
    }
}

impl PostNoStream for RequestBody {
    type Response = crate::responses::Response;
}

impl PostStream for RequestBody {
    type Response = super::response::ResponseStreamEvent;
}

#[cfg(test)]
mod tests {
    //! Offline serialization tests pinning the wire format of the request
    //! body. No network access is involved.

    use super::*;

    fn base_request() -> RequestBody {
        RequestBody {
            model: "test-model".to_string(),
            input: Input::Text("你好".to_string()),
            ..Default::default()
        }
    }

    #[test]
    fn serializes_minimal_request() {
        let value = serde_json::to_value(base_request()).unwrap();
        assert_eq!(
            value,
            serde_json::json!({
                "model": "test-model",
                "input": "你好",
            })
        );
    }

    #[test]
    fn serializes_input_items() {
        let request = RequestBody {
            input: Input::Items(vec![
                InputItem::Message {
                    role: Role::System,
                    content: MessageContent::Text("You are helpful.".into()),
                    id: None,
                    status: None,
                },
                InputItem::Message {
                    role: Role::User,
                    content: MessageContent::Parts(vec![InputContentPart::InputText {
                        text: "hi".into(),
                    }]),
                    id: None,
                    status: None,
                },
                InputItem::FunctionCall {
                    call_id: "fc_1".into(),
                    name: Some("get_weather".into()),
                    arguments: Some("{\"city\": \"北京\"}".into()),
                    id: None,
                },
                InputItem::FunctionCallOutput {
                    call_id: "fc_1".into(),
                    output: FunctionCallOutputContent::Text("sunny".into()),
                },
            ]),
            ..base_request()
        };

        let value = serde_json::to_value(request).unwrap();
        assert_eq!(
            value["input"],
            serde_json::json!([
                {"type": "message", "role": "system", "content": "You are helpful."},
                {"type": "message", "role": "user", "content": [{"type": "input_text", "text": "hi"}]},
                {"type": "function_call", "call_id": "fc_1", "name": "get_weather", "arguments": "{\"city\": \"北京\"}"},
                {"type": "function_call_output", "call_id": "fc_1", "output": "sunny"},
            ])
        );
    }

    #[test]
    fn serializes_tools_and_tool_choice() {
        let request = RequestBody {
            tools: Some(vec![Tool::Function {
                name: "get_weather".into(),
                description: Some("获取天气".into()),
                parameters: Some(serde_json::json!({
                    "type": "object",
                    "properties": {"city": {"type": "string"}},
                    "required": ["city"],
                })),
                strict: None,
            }]),
            tool_choice: Some(ToolChoice::Specific(ToolChoiceSpecific::Function {
                name: "get_weather".into(),
            })),
            reasoning: Some(ReasoningConfig {
                effort: Some(ReasoningEffort::Low),
                summary: None,
            }),
            text: Some(TextConfig {
                format: Some(TextFormat::JsonObject),
            }),
            ..base_request()
        };

        let value = serde_json::to_value(request).unwrap();
        assert_eq!(
            value["tool_choice"],
            serde_json::json!({"type": "function", "name": "get_weather"})
        );
        assert_eq!(value["reasoning"], serde_json::json!({"effort": "low"}));
        assert_eq!(
            value["text"],
            serde_json::json!({"format": {"type": "json_object"}})
        );
        assert_eq!(value["tools"][0]["type"], "function");
        assert_eq!(value["tools"][0]["name"], "get_weather");
    }

    #[test]
    fn extra_body_flattens_into_top_level() {
        let mut request = base_request();
        request.extra_body_map = Some(
            serde_json::json!({"ocr_options": {}})
                .as_object()
                .unwrap()
                .clone(),
        );
        let value = serde_json::to_value(request).unwrap();
        assert_eq!(value["ocr_options"], serde_json::json!({}));
    }
}