Skip to main content

openai_interface/chat/create/
request.rs

1//! This module contains the request body and POST method for the chat completion API.
2
3use std::collections::HashMap;
4
5use serde::Serialize;
6use url::Url;
7
8use crate::{
9    chat::ServiceTier,
10    errors::OapiError,
11    rest::post::{Post, PostNoStream, PostStream},
12};
13
14/// Creates a model response for the given chat conversation.
15///
16/// # Example
17///
18/// ```rust,no_run
19/// use futures_util::StreamExt;
20/// use openai_interface::chat::create::request::{Message, RequestBody};
21/// use openai_interface::rest::{default_client, post::PostStream};
22///
23/// const DEEPSEEK_CHAT_URL: &'static str = "https://api.deepseek.com";
24/// const DEEPSEEK_MODEL: &'static str = "deepseek-v4-flash";
25///
26/// #[tokio::main]
27/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
28///     let request = RequestBody {
29///         messages: vec![
30///             Message::System {
31///                 content: "This is a request of test purpose. Reply briefly".to_string(),
32///                 name: None,
33///             },
34///             Message::User {
35///                 content: "What's your name?".to_string(),
36///                 name: None,
37///             },
38///         ],
39///         model: DEEPSEEK_MODEL.to_string(),
40///         stream: Some(true),
41///         ..Default::default()
42///     };
43///
44///     let mut response = request
45///         .get_stream_response_string(&default_client(), DEEPSEEK_CHAT_URL, "YOUR_API_KEY")
46///         .await?;
47///
48///     while let Some(chunk) = response.next().await {
49///         println!("{}", chunk?);
50///     }
51///     Ok(())
52/// }
53/// ```
54#[derive(Serialize, Debug, Default, Clone)]
55pub struct RequestBody {
56    /// Parameters for audio output. Required when audio output is requested
57    /// with `modalities: ["audio"]`.
58    /// [Learn more](https://platform.openai.com/docs/guides/audio).
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub audio: Option<ChatCompletionAudioParam>,
61
62    /// Number between -2.0 and 2.0. Positive values penalize new tokens based on their
63    /// existing frequency in the text so far, decreasing the model's likelihood to
64    /// repeat the same line verbatim.
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub frequency_penalty: Option<f32>,
67
68    /// Whether to return log probabilities of the output tokens or not. If true,
69    /// returns the log probabilities of each output token returned in the `content` of
70    /// `message`.
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub logprobs: Option<bool>,
73
74    /// An upper bound for the number of tokens that can be generated for a completion,
75    /// including visible output tokens and reasoning tokens.
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub max_completion_tokens: Option<u32>,
78
79    /// The maximum number of tokens that can be generated in the chat completion.
80    /// Deprecated according to OpenAI's Python SDK in favour of
81    /// `max_completion_tokens`.
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub max_tokens: Option<u32>,
84
85    /// A list of messages comprising the conversation so far.
86    pub messages: Vec<Message>,
87
88    /// Set of 16 key-value pairs that can be attached to an object. This can be useful
89    /// for storing additional information about the object in a structured format, and
90    /// querying for objects via API or the dashboard.
91    ///
92    /// Keys are strings with a maximum length of 64 characters. Values are strings with
93    /// a maximum length of 512 characters.
94    #[serde(skip_serializing_if = "Option::is_none")]
95    pub metadata: Option<HashMap<String, String>>,
96
97    /// Output types that you would like the model to generate. Most models are capable
98    /// of generating text, which is the default:
99    ///
100    /// `["text"]`
101    ///
102    /// The `gpt-4o-audio-preview` model can also be used to
103    /// [generate audio](https://platform.openai.com/docs/guides/audio). To request that
104    /// this model generate both text and audio responses, you can use:
105    ///
106    /// `["text", "audio"]`
107    #[serde(skip_serializing_if = "Option::is_none")]
108    pub modalities: Option<Vec<Modality>>,
109
110    /// Name of the model to use to generate the response.
111    pub model: String, // The type of this attribute needs improvements.
112
113    /// How many chat completion choices to generate for each input message. Note that
114    /// you will be charged based on the number of generated tokens across all of the
115    /// choices. Keep `n` as `1` to minimize costs.
116    #[serde(skip_serializing_if = "Option::is_none")]
117    pub n: Option<u32>,
118
119    /// Whether to enable
120    /// [parallel function calling](https://platform.openai.com/docs/guides/function-calling#configuring-parallel-function-calling)
121    /// during tool use.
122    #[serde(skip_serializing_if = "Option::is_none")]
123    pub parallel_tool_calls: Option<bool>,
124
125    /// Static predicted output content, such as the content of a text file that is
126    /// being regenerated.
127    #[serde(skip_serializing_if = "Option::is_none")]
128    pub prediction: Option<ChatCompletionPredictionContentParam>,
129
130    /// Number between -2.0 and 2.0. Positive values penalize new tokens based on
131    /// whether they appear in the text so far, increasing the model's likelihood to
132    /// talk about new topics.
133    #[serde(skip_serializing_if = "Option::is_none")]
134    pub presence_penalty: Option<f32>,
135
136    /// Used by OpenAI to cache responses for similar requests to optimize your cache
137    /// hit rates. Replaces the `user` field.
138    /// [Learn more](https://platform.openai.com/docs/guides/prompt-caching).
139    #[serde(skip_serializing_if = "Option::is_none")]
140    pub prompt_cache_key: Option<String>,
141
142    /// Constrains effort on reasoning for
143    /// [reasoning models](https://platform.openai.com/docs/guides/reasoning).
144    /// Currently supported values are `none`, `minimal`, `low`, `medium`,
145    /// `high`, `xhigh`, and `max` (model-dependent). Reducing reasoning
146    /// effort can result in faster responses and fewer tokens used on
147    /// reasoning in a response. Defaults are provider- and model-dependent:
148    /// e.g. `medium` for GPT-5.5. Providers map unsupported values to the
149    /// nearest effort level.
150    #[serde(skip_serializing_if = "Option::is_none")]
151    pub reasoning_effort: Option<ReasoningEffort>,
152
153    /// specifying the format that the model must output.
154    ///
155    /// Setting to `{ "type": "json_schema", "json_schema": {...} }` enables Structured
156    /// Outputs which ensures the model will match your supplied JSON schema. Learn more
157    /// in the
158    /// [Structured Outputs guide](https://platform.openai.com/docs/guides/structured-outputs).
159    /// Setting to `{ "type": "json_object" }` enables the older JSON mode, which
160    /// ensures the message the model generates is valid JSON. Using `json_schema` is
161    /// preferred for models that support it.
162    #[serde(skip_serializing_if = "Option::is_none")]
163    pub response_format: Option<ResponseFormat>,
164
165    /// A stable identifier used to help detect users of your application that may be
166    /// violating OpenAI's usage policies. The IDs should be a string that uniquely
167    /// identifies each user. It is recommended to hash their username or email address, in
168    /// order to avoid sending any identifying information.
169    #[serde(skip_serializing_if = "Option::is_none")]
170    pub safety_identifier: Option<String>,
171
172    /// If specified, the system will make a best effort to sample deterministically. Determinism
173    /// is not guaranteed, and you should refer to the `system_fingerprint` response parameter to
174    /// monitor changes in the backend.
175    #[serde(skip_serializing_if = "Option::is_none")]
176    pub seed: Option<i64>,
177
178    /// Specifies the processing type used for serving the request.
179    ///
180    /// - If set to 'auto', then the request will be processed with the service tier
181    ///   configured in the Project settings. Unless otherwise configured, the Project
182    ///   will use 'default'.
183    /// - If set to 'default', then the request will be processed with the standard
184    ///   pricing and performance for the selected model.
185    /// - If set to '[flex](https://platform.openai.com/docs/guides/flex-processing)' or
186    ///   '[priority](https://openai.com/api-priority-processing/)', then the request
187    ///   will be processed with the corresponding service tier.
188    /// - When not set, the default behavior is 'auto'.
189    ///
190    /// When the `service_tier` parameter is set, the response body will include the
191    /// `service_tier` value based on the processing mode actually used to serve the
192    /// request. This response value may be different from the value set in the
193    /// parameter.
194    #[serde(skip_serializing_if = "Option::is_none")]
195    pub service_tier: Option<ServiceTier>,
196
197    /// Up to 4 sequences where the API will stop generating further tokens. The
198    /// returned text will not contain the stop sequence.
199    #[serde(skip_serializing_if = "Option::is_none")]
200    pub stop: Option<StopKeywords>,
201
202    /// Whether or not to store the output of this chat completion request for use in
203    /// our [model distillation](https://platform.openai.com/docs/guides/distillation)
204    /// or [evals](https://platform.openai.com/docs/guides/evals) products.
205    ///
206    /// Supports text and image inputs. Note: image inputs over 8MB will be dropped.
207    #[serde(skip_serializing_if = "Option::is_none")]
208    pub store: Option<bool>,
209
210    /// Whether to stream back partial progress. If set to `true` (or left as
211    /// `Some(true)`), tokens will be sent as data-only
212    /// [server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format)
213    /// as they become available, with the stream terminated by a `data: [DONE]`
214    /// message.
215    ///
216    /// Although it is optional, you should explicitly designate it
217    /// for an expected response.
218    #[serde(skip_serializing_if = "Option::is_none")]
219    pub stream: Option<bool>,
220
221    /// Options for streaming response. Only set this when you set `stream: true`
222    #[serde(skip_serializing_if = "Option::is_none")]
223    pub stream_options: Option<StreamOptions>,
224
225    /// What sampling temperature to use, between 0 and 2. Higher values like 0.8 will
226    /// make the output more random, while lower values like 0.2 will make it more
227    /// focused and deterministic. It is generally recommended to alter this or `top_p` but
228    /// not both.
229    #[serde(skip_serializing_if = "Option::is_none")]
230    pub temperature: Option<f32>,
231
232    /// An alternative to sampling with temperature, called nucleus sampling, where the
233    /// model considers the results of the tokens with top_p probability mass. So 0.1
234    /// means only the tokens comprising the top 10% probability mass are considered.
235    ///
236    /// It is generally recommended to alter this or `temperature` but not both.
237    #[serde(skip_serializing_if = "Option::is_none")]
238    pub top_p: Option<f32>,
239
240    /// Controls which (if any) tool is called by the model. `none` means the model will
241    /// not call any tool and instead generates a message. `auto` means the model can
242    /// pick between generating a message or calling one or more tools. `required` means
243    /// the model must call one or more tools. Specifying a particular tool via
244    /// `{"type": "function", "function": {"name": "my_function"}}` forces the model to
245    /// call that tool.
246    #[serde(skip_serializing_if = "Option::is_none")]
247    pub tool_choice: Option<ToolChoice>,
248
249    /// A list of tools the model may call.
250    #[serde(skip_serializing_if = "Option::is_none")]
251    pub tools: Option<Vec<RequestTool>>,
252
253    /// An integer between 0 and 20 specifying the number of most likely tokens to
254    /// return at each token position, each with an associated log probability.
255    /// `logprobs` must be set to `true` if this parameter is used.
256    #[serde(skip_serializing_if = "Option::is_none")]
257    pub top_logprobs: Option<u32>,
258
259    /// DeepSeek: controls the switch between thinking and non-thinking mode.
260    /// Defaults to `enabled`. See
261    /// [the DeepSeek API reference](https://api-docs.deepseek.com/api/create-chat-completion).
262    #[cfg(feature = "deepseek")]
263    #[serde(skip_serializing_if = "Option::is_none")]
264    pub thinking: Option<DeepSeekThinking>,
265
266    /// DeepSeek: a custom user ID. Allowed character set is `[a-zA-Z0-9\-_]`
267    /// with a maximum length of 512. Do not include user privacy information.
268    /// It can be used to distinguish user identities for content safety
269    /// review, isolate KVCache, and schedule users.
270    #[cfg(feature = "deepseek")]
271    #[serde(skip_serializing_if = "Option::is_none")]
272    pub user_id: Option<String>,
273
274    /// Qwen: whether to enable thinking mode for hybrid-thinking models such
275    /// as Qwen3. When set to `true`, the thinking content is returned in the
276    /// `reasoning_content` field.
277    #[cfg(feature = "qwen")]
278    #[serde(skip_serializing_if = "Option::is_none")]
279    pub enable_thinking: Option<bool>,
280    /// Qwen: the maximum number of tokens available for the model's thinking
281    /// (chain-of-thought) process.
282    #[cfg(feature = "qwen")]
283    #[serde(skip_serializing_if = "Option::is_none")]
284    pub thinking_budget: Option<u32>,
285    /// Qwen: the size of the candidate set for sampling during generation.
286    /// Set to `null` or a value greater than 100 to disable `top_k` sampling.
287    #[cfg(feature = "qwen")]
288    #[serde(skip_serializing_if = "Option::is_none")]
289    pub top_k: Option<u32>,
290
291    /// This field is being replaced by `safety_identifier` and `prompt_cache_key`. Use
292    /// `prompt_cache_key` instead to maintain caching optimizations. A stable
293    /// identifier for your end-users. Used to boost cache hit rates by better bucketing
294    /// similar requests and to help OpenAI detect and prevent abuse.
295    /// [Learn more](https://platform.openai.com/docs/guides/safety-best-practices#safety-identifiers).
296    #[serde(skip_serializing_if = "Option::is_none")]
297    pub user: Option<String>,
298
299    /// Constrains the verbosity of the model's response. Lower values will result in
300    /// more concise responses, while higher values will result in more verbose
301    /// responses. Currently supported values are `low`, `medium`, and `high`.
302    #[serde(skip_serializing_if = "Option::is_none")]
303    pub verbosity: Option<LowMediumHighEnum>,
304
305    /// This tool searches the web for relevant results to use in a response. Learn more
306    /// about the
307    /// [web search tool](https://platform.openai.com/docs/guides/tools-web-search?api-mode=chat).
308    #[serde(rename = "web_search_options", skip_serializing_if = "Option::is_none")]
309    pub web_search_options: Option<WebSearchOptions>,
310
311    /// Other request bodies that are not in standard OpenAI API and
312    /// not covered by the fields above.
313    #[serde(flatten, skip_serializing_if = "Option::is_none")]
314    pub extra_body_map: Option<serde_json::Map<String, serde_json::Value>>,
315}
316
317#[derive(Serialize, Debug, Clone)]
318#[serde(tag = "role", rename_all = "lowercase")]
319pub enum Message {
320    /// In this case, the role of the message author is `system`.
321    /// The field `{ role = "system" }` is added automatically.
322    System {
323        /// The contents of the system message.
324        content: String,
325        /// An optional name for the participant.
326        ///
327        /// Provides the model information to differentiate between
328        /// participants of the same role.
329        #[serde(skip_serializing_if = "Option::is_none")]
330        name: Option<String>,
331    },
332    /// In this case, the role of the message author is `user`.
333    /// The field `{ role = "user" }` is added automatically.
334    User {
335        /// The contents of the user message.
336        content: String,
337        /// An optional name for the participant.
338        ///
339        /// Provides the model information to differentiate between
340        /// participants of the same role.
341        #[serde(skip_serializing_if = "Option::is_none")]
342        name: Option<String>,
343    },
344    /// In this case, the role of the message author is `assistant`.
345    /// The field `{ role = "assistant" }` is added automatically.
346    Assistant {
347        /// The contents of the assistant message. Required unless `tool_calls`
348        /// or `function_call` is specified. (Note that `function_call` is deprecated
349        /// in favour of `tool_calls`.)
350        content: Option<String>,
351        /// Data about a previous audio response from the model. Required for
352        /// multi-turn audio conversations.
353        #[serde(skip_serializing_if = "Option::is_none")]
354        audio: Option<AssistantAudio>,
355        /// The refusal message by the assistant.
356        #[serde(skip_serializing_if = "Option::is_none")]
357        refusal: Option<String>,
358        #[serde(skip_serializing_if = "Option::is_none")]
359        name: Option<String>,
360        /// DeepSeek (Beta): set this to `true` to force the model to start its
361        /// answer by the content of the supplied prefix in this assistant
362        /// message. Requires `base_url = "https://api.deepseek.com/beta"`.
363        #[cfg(feature = "deepseek")]
364        #[serde(skip_serializing_if = "is_false")]
365        prefix: bool,
366        /// DeepSeek (Beta): used for the thinking mode in the
367        /// [Chat Prefix Completion](https://api-docs.deepseek.com/guides/chat_prefix_completion)
368        /// feature as the input for the CoT in the last assistant message.
369        /// When using this feature, `prefix` must be set to `true`.
370        #[cfg(feature = "deepseek")]
371        #[serde(skip_serializing_if = "Option::is_none")]
372        reasoning_content: Option<String>,
373
374        /// The tool calls generated by the model, such as function calls.
375        #[serde(skip_serializing_if = "Option::is_none")]
376        tool_calls: Option<Vec<AssistantToolCall>>,
377    },
378    /// In this case, the role of the message author is `assistant`.
379    /// The field `{ role = "tool" }` is added automatically.
380    Tool {
381        /// The contents of the tool message.
382        content: String,
383        /// Tool call that this message is responding to.
384        tool_call_id: String,
385    },
386    /// In this case, the role of the message author is `function`.
387    /// The field `{ role = "function" }` is added automatically.
388    Function {
389        /// The contents of the function message.
390        content: String,
391        /// The name of the function to call.
392        name: String,
393    },
394    /// In this case, the role of the message author is `developer`.
395    /// The field `{ role = "developer" }` is added automatically.
396    Developer {
397        /// The contents of the developer message.
398        content: String,
399        /// An optional name for the participant.
400        ///
401        /// Provides the model information to differentiate between
402        /// participants of the same role.
403        name: Option<String>,
404    },
405}
406
407#[derive(Debug, Serialize, Clone)]
408#[serde(tag = "type", rename_all = "lowercase")]
409pub enum AssistantToolCall {
410    Function {
411        /// The ID of the tool call.
412        id: String,
413        /// The function that the model called.
414        function: ToolCallFunction,
415    },
416    Custom {
417        /// The ID of the tool call.
418        id: String,
419        /// The custom tool that the model called.
420        custom: ToolCallCustom,
421    },
422}
423
424#[derive(Debug, Serialize, Clone)]
425pub struct ToolCallFunction {
426    /// The arguments to call the function with, as generated by the model in JSON
427    /// format. Note that the model does not always generate valid JSON, and may
428    /// hallucinate parameters not defined by your function schema. Validate the
429    /// arguments in your code before calling your function.
430    arguments: String,
431    /// The name of the function to call.
432    name: String,
433}
434
435#[derive(Debug, Serialize, Clone)]
436pub struct ToolCallCustom {
437    /// The input for the custom tool call generated by the model.
438    input: String,
439    /// The name of the custom tool to call.
440    name: String,
441}
442
443/// Data about a previous audio response from the model, referenced in an
444/// assistant message for multi-turn audio conversations.
445#[derive(Debug, Serialize, Clone)]
446pub struct AssistantAudio {
447    /// Unique identifier for a previous audio response in a multi-turn
448    /// conversation.
449    pub id: String,
450    /// The audio data (base64 encoded) to insert as context. Optional.
451    #[serde(skip_serializing_if = "Option::is_none")]
452    pub data: Option<String>,
453}
454
455#[derive(Debug, Serialize, Clone)]
456#[serde(tag = "type", rename_all = "snake_case")]
457pub enum ResponseFormat {
458    /// The type of response format being defined. Always `json_schema`.
459    JsonSchema {
460        /// Structured Outputs configuration options, including a JSON Schema.
461        json_schema: JSONSchema,
462    },
463    /// The type of response format being defined. Always `json_object`.
464    JsonObject,
465    /// The type of response format being defined. Always `text`.
466    Text,
467}
468
469#[derive(Debug, Serialize, Clone)]
470pub struct JSONSchema {
471    /// The name of the response format. Must be a-z, A-Z, 0-9, or contain
472    /// underscores and dashes, with a maximum length of 64.
473    pub name: String,
474    /// A description of what the response format is for, used by the model to determine
475    /// how to respond in the format.
476    #[serde(skip_serializing_if = "Option::is_none")]
477    pub description: Option<String>,
478    /// The schema for the response format, described as a JSON Schema object. Learn how
479    /// to build JSON schemas [here](https://json-schema.org/).
480    #[serde(skip_serializing_if = "Option::is_none")]
481    pub schema: Option<serde_json::Map<String, serde_json::Value>>,
482    /// Whether to enable strict schema adherence when generating the output. If set to
483    /// true, the model will always follow the exact schema defined in the `schema`
484    /// field. Only a subset of JSON Schema is supported when `strict` is `true`. To
485    /// learn more, read the
486    /// [Structured Outputs guide](https://platform.openai.com/docs/guides/structured-outputs).
487    #[serde(skip_serializing_if = "Option::is_none")]
488    pub strict: Option<bool>,
489}
490
491#[derive(Serialize, Debug, Clone)]
492#[serde(rename_all = "snake_case")]
493pub enum Modality {
494    Text,
495    Audio,
496}
497
498/// Parameters for audio output of a chat completion.
499#[derive(Serialize, Debug, Clone)]
500pub struct ChatCompletionAudioParam {
501    /// Specifies the output audio format. Must be one of `wav`, `aac`, `mp3`,
502    /// `flac`, `opus`, or `pcm16`.
503    pub format: AudioFormat,
504    /// The voice the model uses to respond.
505    pub voice: Voice,
506}
507
508/// The output audio format of a chat completion.
509#[derive(Serialize, Debug, Clone)]
510#[serde(rename_all = "snake_case")]
511pub enum AudioFormat {
512    Wav,
513    Aac,
514    Mp3,
515    Flac,
516    Opus,
517    Pcm16,
518}
519
520/// The voice the model uses to respond with audio output.
521#[derive(Serialize, Debug, Clone)]
522#[serde(untagged)]
523pub enum Voice {
524    /// A built-in voice name, e.g. `alloy`, `ash`, `ballad`, `coral`, `echo`,
525    /// `sage`, `shimmer`, or `verse`.
526    BuiltIn(String),
527    /// A custom voice reference, e.g. `{ "id": "voice_1234" }`.
528    Custom {
529        /// The custom voice ID, e.g. `voice_1234`.
530        id: String,
531    },
532}
533
534#[derive(Serialize, Debug, Clone)]
535pub struct ChatCompletionPredictionContentParam {
536    /// The content that should be matched when generating a model response. If
537    /// generated tokens would match this content, the entire model response can be
538    /// returned much more quickly.
539    pub content: ChatCompletionPredictionContentParamContent,
540
541    /// The type of the predicted content you want to provide.
542    /// This type is currently always `content`.
543    #[serde(rename = "type")]
544    pub type_: ChatCompletionPredictionContentParamType,
545}
546
547#[derive(Serialize, Debug, Clone)]
548#[serde(untagged)]
549pub enum ChatCompletionPredictionContentParamContent {
550    Text(String),
551    ChatCompletionContentPartTextParam {
552        /// The text content.
553        text: String,
554        /// The type of the content part.
555        #[serde(rename = "type")]
556        type_: ChatCompletionContentPartTextParamType,
557    },
558}
559
560#[derive(Serialize, Debug, Clone)]
561#[serde(rename_all = "snake_case")]
562pub enum ChatCompletionContentPartTextParamType {
563    Text,
564}
565
566#[derive(Serialize, Debug, Clone)]
567#[serde(rename_all = "snake_case")]
568pub enum ChatCompletionPredictionContentParamType {
569    Content,
570}
571
572/// DeepSeek: skip-serialization helper for the Beta `prefix` message field.
573#[cfg(feature = "deepseek")]
574#[inline]
575fn is_false(value: &bool) -> bool {
576    !value
577}
578
579#[derive(Serialize, Debug, Clone)]
580#[serde(untagged)]
581pub enum StopKeywords {
582    Word(String),
583    Words(Vec<String>),
584}
585
586#[derive(Serialize, Debug, Clone)]
587#[serde(rename_all = "snake_case")]
588pub enum LowMediumHighEnum {
589    Low,
590    Medium,
591    High,
592}
593
594#[derive(Serialize, Debug, Clone, Default)]
595pub struct WebSearchOptions {
596    /// High level guidance for the amount of context window space to use for the
597    /// search. One of `low`, `medium`, or `high`. `medium` is the default.
598    #[serde(skip_serializing_if = "Option::is_none")]
599    pub search_context_size: Option<LowMediumHighEnum>,
600
601    #[serde(skip_serializing_if = "Option::is_none")]
602    pub user_location: Option<WebSearchOptionsUserLocation>,
603}
604
605#[derive(Serialize, Debug, Clone)]
606#[serde(tag = "type", rename_all = "snake_case")]
607pub enum WebSearchOptionsUserLocation {
608    /// The type of location approximation. Always `approximate`.
609    Approximate {
610        /// Approximate location parameters for the search.
611        approximate: WebSearchOptionsUserLocationApproximate,
612    },
613}
614
615#[derive(Serialize, Debug, Clone, Default)]
616pub struct WebSearchOptionsUserLocationApproximate {
617    /// Free text input for the city of the user, e.g. `San Francisco`.
618    #[serde(skip_serializing_if = "Option::is_none")]
619    pub city: Option<String>,
620
621    /// The two-letter [ISO country code](https://en.wikipedia.org/wiki/ISO_3166-1) of
622    /// the user, e.g. `US`.
623    #[serde(skip_serializing_if = "Option::is_none")]
624    pub country: Option<String>,
625
626    /// Free text input for the region of the user, e.g. `California`.
627    #[serde(skip_serializing_if = "Option::is_none")]
628    pub region: Option<String>,
629
630    /// The [IANA timezone](https://timeapi.io/documentation/iana-timezones) of the
631    /// user, e.g. `America/Los_Angeles`.
632    #[serde(skip_serializing_if = "Option::is_none")]
633    pub timezone: Option<String>,
634}
635
636#[derive(Serialize, Debug, Clone)]
637pub struct StreamOptions {
638    /// If set, an additional chunk will be streamed before the `data: [DONE]` message.
639    ///
640    /// The `usage` field on this chunk shows the token usage statistics for the entire
641    /// request, and the `choices` field will always be an empty array.
642    ///
643    /// All other chunks will also include a `usage` field, but with a null value.
644    /// **NOTE:** If the stream is interrupted, you may not receive the final usage
645    /// chunk which contains the total token usage for the request.
646    pub include_usage: bool,
647}
648
649#[derive(Serialize, Debug, Clone)]
650#[serde(tag = "type", rename_all = "snake_case")]
651pub enum RequestTool {
652    /// The type of the tool. Currently, only `function` is supported.
653    Function { function: ToolFunction },
654    /// The type of the custom tool. Always `custom`.
655    Custom {
656        /// Properties of the custom tool.
657        custom: ToolCustom,
658    },
659}
660
661#[derive(Serialize, Debug, Clone)]
662pub struct ToolFunction {
663    /// The name of the function to be called. Must be a-z, A-Z, 0-9, or
664    /// contain underscores and dashes, with a maximum length
665    /// of 64.
666    pub name: String,
667    /// A description of what the function does, used by the model to choose when and
668    /// how to call the function.
669    #[serde(skip_serializing_if = "Option::is_none")]
670    pub description: Option<String>,
671    /// The parameters the functions accepts, described as a JSON Schema object.
672    ///
673    /// See the
674    /// [openai function calling guide](https://platform.openai.com/docs/guides/function-calling)
675    /// for examples, and the
676    /// [JSON Schema reference](https://json-schema.org/understanding-json-schema/) for
677    /// documentation about the format.
678    ///
679    /// Omitting `parameters` defines a function with an empty parameter list.
680    #[serde(skip_serializing_if = "Option::is_none")]
681    pub parameters: Option<serde_json::Map<String, serde_json::Value>>,
682    /// Whether to enable strict schema adherence when generating the function call.
683    ///
684    /// If set to true, the model will follow the exact schema defined in the
685    /// `parameters` field. Only a subset of JSON Schema is supported when `strict` is
686    /// `true`. Learn more about Structured Outputs in the
687    /// [openai function calling guide](https://platform.openai.com/docs/guides/function-calling).
688    #[serde(skip_serializing_if = "Option::is_none")]
689    pub strict: Option<bool>,
690}
691
692#[derive(Serialize, Debug, Clone)]
693pub struct ToolCustom {
694    /// The name of the custom tool, used to identify it in tool calls.
695    pub name: String,
696    /// Optional description of the custom tool, used to provide more context.
697    #[serde(skip_serializing_if = "Option::is_none")]
698    pub description: Option<String>,
699    /// The input format for the custom tool. Default is unconstrained text.
700    #[serde(skip_serializing_if = "Option::is_none")]
701    pub format: Option<ToolCustomFormat>,
702}
703
704#[derive(Serialize, Debug, Clone)]
705#[serde(rename_all = "snake_case", tag = "type")]
706pub enum ToolCustomFormat {
707    /// Unconstrained text format. Always `text`.
708    Text,
709    /// Grammar format. Always `grammar`.
710    Grammar {
711        /// Your chosen grammar.
712        grammar: ToolCustomFormatGrammarGrammar,
713    },
714}
715
716#[derive(Debug, Serialize, Clone)]
717pub struct ToolCustomFormatGrammarGrammar {
718    /// The grammar definition.
719    pub definition: String,
720    /// The syntax of the grammar definition. One of `lark` or `regex`.
721    pub syntax: ToolCustomFormatGrammarGrammarSyntax,
722}
723
724#[derive(Debug, Serialize, Clone)]
725#[serde(rename_all = "snake_case")]
726pub enum ToolCustomFormatGrammarGrammarSyntax {
727    Lark,
728    Regex,
729}
730
731#[derive(Debug, Serialize, Clone)]
732#[serde(rename_all = "snake_case")]
733pub enum ToolChoice {
734    None,
735    Auto,
736    Required,
737    #[serde(untagged)]
738    Specific(ToolChoiceSpecific),
739}
740
741#[derive(Debug, Serialize, Clone)]
742#[serde(rename_all = "snake_case", tag = "type")]
743pub enum ToolChoiceSpecific {
744    /// Allowed tool configuration type. Always `allowed_tools`.
745    AllowedTools {
746        /// Constrains the tools available to the model to a pre-defined set.
747        allowed_tools: ToolChoiceAllowedTools,
748    },
749    /// For function calling, the type is always `function`.
750    Function { function: ToolChoiceFunction },
751    /// For custom tool calling, the type is always `custom`.
752    Custom { custom: ToolChoiceCustom },
753}
754
755#[derive(Debug, Serialize, Clone)]
756pub struct ToolChoiceAllowedTools {
757    /// Constrains the tools available to the model to a pre-defined set.
758    ///
759    /// - `auto` allows the model to pick from among the allowed tools and generate a
760    ///   message.
761    /// - `required` requires the model to call one or more of the allowed tools.
762    pub mode: ToolChoiceAllowedToolsMode,
763    /// A list of tool definitions that the model should be allowed to call.
764    ///
765    /// For the Chat Completions API, the list of tool definitions might look like:
766    ///
767    /// ```json
768    /// [
769    ///   { "type": "function", "function": { "name": "get_weather" } },
770    ///   { "type": "function", "function": { "name": "get_time" } }
771    /// ]
772    /// ```
773    pub tools: Vec<serde_json::Map<String, serde_json::Value>>,
774}
775
776/// The mode for allowed tools in tool choice.
777///
778/// Controls how the model should handle the set of allowed tools:
779///
780/// - `auto` allows the model to pick from among the allowed tools and generate a
781///   message.
782/// - `required` requires the model to call one or more of the allowed tools.
783#[derive(Debug, Serialize, Clone)]
784#[serde(rename_all = "lowercase")]
785pub enum ToolChoiceAllowedToolsMode {
786    /// The model can choose whether to use the allowed tools or not.
787    Auto,
788    /// The model must use at least one of the allowed tools.
789    Required,
790}
791
792#[derive(Debug, Serialize, Clone)]
793pub struct ToolChoiceFunction {
794    /// The name of the function to call.
795    pub name: String,
796}
797
798#[derive(Debug, Serialize, Clone)]
799pub struct ToolChoiceCustom {
800    /// The name of the custom tool to call.
801    pub name: String,
802}
803
804/// DeepSeek: controls the switch between thinking and non-thinking mode.
805#[cfg(feature = "deepseek")]
806#[derive(Debug, Serialize, Clone, PartialEq, Eq)]
807pub struct DeepSeekThinking {
808    /// Whether to use thinking mode (`enabled`) or non-thinking mode
809    /// (`disabled`). Defaults to `enabled`.
810    #[serde(rename = "type")]
811    pub type_: DeepSeekThinkingType,
812}
813
814/// DeepSeek: whether thinking mode is enabled or disabled.
815#[cfg(feature = "deepseek")]
816#[derive(Debug, Serialize, Clone, PartialEq, Eq)]
817#[serde(rename_all = "lowercase")]
818pub enum DeepSeekThinkingType {
819    Enabled,
820    Disabled,
821}
822
823/// Constrains the effort on reasoning for reasoning models. This is an
824/// official OpenAI parameter; reasoning providers such as DeepSeek and Qwen
825/// accept a subset of these values and map the rest to their nearest effort
826/// level.
827#[derive(Debug, Serialize, Clone, PartialEq, Eq)]
828#[serde(rename_all = "lowercase")]
829pub enum ReasoningEffort {
830    None,
831    Minimal,
832    Low,
833    Medium,
834    High,
835    Xhigh,
836    Max,
837}
838
839impl RequestBody {
840    /// Whether this request asks for a streamed response. Defaults to
841    /// `false` when [`RequestBody::stream`] is `None`.
842    pub fn is_streaming(&self) -> bool {
843        self.stream.unwrap_or(false)
844    }
845}
846
847impl Post for RequestBody {
848    fn is_streaming(&self) -> bool {
849        RequestBody::is_streaming(self)
850    }
851
852    /// Builds the URL for the request.
853    ///
854    /// `base_url` should be like <https://api.openai.com/v1>
855    fn build_url(&self, base_url: &str) -> Result<String, OapiError> {
856        let mut url = Url::parse(base_url.trim_end_matches('/')).map_err(OapiError::UrlError)?;
857        url.path_segments_mut()
858            .map_err(|_| OapiError::UrlCannotBeBase(base_url.to_string()))?
859            .push("chat")
860            .push("completions");
861
862        Ok(url.to_string())
863    }
864}
865
866impl PostNoStream for RequestBody {
867    type Response = super::response::no_streaming::ChatCompletion;
868}
869
870impl PostStream for RequestBody {
871    type Response = super::response::streaming::ChatCompletionChunk;
872}
873
874#[cfg(test)]
875mod request_test {
876    use futures_util::StreamExt;
877
878    use super::*;
879
880    const DEEPSEEK_CHAT_URL: &str = "https://api.deepseek.com";
881    const DEEPSEEK_MODEL: &str = "deepseek-v4-flash";
882
883    fn deepseek_api_key() -> Option<String> {
884        std::env::var("DEEPSEEK_API_KEY")
885            .ok()
886            .map(|key| key.trim().to_string())
887            .filter(|key| !key.is_empty())
888    }
889
890    #[tokio::test]
891    async fn test_deepseek_no_stream() {
892        let Some(api_key) = deepseek_api_key() else {
893            println!("Skipping: set DEEPSEEK_API_KEY to run this test");
894            return;
895        };
896
897        let request = RequestBody {
898            messages: vec![
899                Message::System {
900                    content: "This is a request of test purpose. Reply briefly".to_string(),
901                    name: None,
902                },
903                Message::User {
904                    content: "What's your name?".to_string(),
905                    name: None,
906                },
907            ],
908            model: DEEPSEEK_MODEL.to_string(),
909            stream: Some(false),
910            ..Default::default()
911        };
912
913        let response = request
914            .get_response_string(&crate::rest::default_client(), DEEPSEEK_CHAT_URL, &api_key)
915            .await
916            .unwrap();
917
918        println!("{}", response);
919
920        assert!(response.to_ascii_lowercase().contains("deepseek"));
921    }
922
923    #[tokio::test]
924    async fn test_deepseek_stream() {
925        let Some(api_key) = deepseek_api_key() else {
926            println!("Skipping: set DEEPSEEK_API_KEY to run this test");
927            return;
928        };
929
930        let request = RequestBody {
931            messages: vec![
932                Message::System {
933                    content: "This is a request of test purpose. Reply briefly".to_string(),
934                    name: None,
935                },
936                Message::User {
937                    content: "Who are you?".to_string(),
938                    name: None,
939                },
940            ],
941            model: DEEPSEEK_MODEL.to_string(),
942            stream: Some(true),
943            ..Default::default()
944        };
945
946        let mut response = request
947            .get_stream_response_string(&crate::rest::default_client(), DEEPSEEK_CHAT_URL, &api_key)
948            .await
949            .unwrap();
950
951        while let Some(chunk) = response.next().await {
952            println!("{}", chunk.unwrap());
953        }
954    }
955
956    /// Assistant tool calls serialize with the official `type` tag
957    /// (`{"type":"function",...}` / `{"type":"custom",...}`), not `role`.
958    #[test]
959    fn assistant_tool_call_serialization() {
960        let function_call = AssistantToolCall::Function {
961            id: "call_abc".to_string(),
962            function: ToolCallFunction {
963                arguments: "{\"city\":\"paris\"}".to_string(),
964                name: "get_weather".to_string(),
965            },
966        };
967        let json = serde_json::to_string(&function_call).unwrap();
968        assert!(json.contains(r#""type":"function""#), "json: {json}");
969        assert!(!json.contains(r#""role""#), "json: {json}");
970
971        let custom_call = AssistantToolCall::Custom {
972            id: "call_def".to_string(),
973            custom: ToolCallCustom {
974                input: "2+2".to_string(),
975                name: "calculator".to_string(),
976            },
977        };
978        let json = serde_json::to_string(&custom_call).unwrap();
979        assert!(json.contains(r#""type":"custom""#), "json: {json}");
980        assert!(!json.contains(r#""role""#), "json: {json}");
981    }
982
983    /// The `prediction` parameter sends its discriminator as `type`, not
984    /// as the Rust field name `type_`.
985    #[test]
986    fn prediction_type_serialization() {
987        let prediction = ChatCompletionPredictionContentParam {
988            content: ChatCompletionPredictionContentParamContent::Text(
989                "The capital of France is Paris.".to_string(),
990            ),
991            type_: ChatCompletionPredictionContentParamType::Content,
992        };
993        let json = serde_json::to_string(&prediction).unwrap();
994        assert!(json.contains(r#""type":"content""#), "json: {json}");
995        assert!(!json.contains("type_"), "json: {json}");
996    }
997
998    /// `tool_choice: allowed_tools` sends `tools` as a JSON array of tool
999    /// definitions, matching the official `Iterable[Dict[str, object]]`.
1000    #[test]
1001    fn allowed_tools_choice_serialization() {
1002        let mut weather = serde_json::Map::new();
1003        weather.insert("type".to_string(), serde_json::json!("function"));
1004        weather.insert(
1005            "function".to_string(),
1006            serde_json::json!({ "name": "get_weather" }),
1007        );
1008
1009        let choice = ToolChoiceSpecific::AllowedTools {
1010            allowed_tools: ToolChoiceAllowedTools {
1011                mode: ToolChoiceAllowedToolsMode::Required,
1012                tools: vec![weather],
1013            },
1014        };
1015        let json = serde_json::to_string(&choice).unwrap();
1016        assert!(json.contains(r#""type":"allowed_tools""#), "json: {json}");
1017        assert!(json.contains(r#""mode":"required""#), "json: {json}");
1018        // `tools` must serialize as an array, not an object.
1019        assert!(json.contains(r#""tools":[{"#), "json: {json}");
1020    }
1021
1022    /// `web_search_options` sends `search_context_size` as optional and the
1023    /// user location nested under an `approximate` key.
1024    #[test]
1025    fn web_search_options_serialization() {
1026        let options = WebSearchOptions {
1027            search_context_size: None,
1028            user_location: Some(WebSearchOptionsUserLocation::Approximate {
1029                approximate: WebSearchOptionsUserLocationApproximate {
1030                    city: Some("San Francisco".to_string()),
1031                    country: None,
1032                    region: None,
1033                    timezone: None,
1034                },
1035            }),
1036        };
1037        let json = serde_json::to_string(&options).unwrap();
1038        assert!(!json.contains("search_context_size"), "json: {json}");
1039        assert!(json.contains(r#""type":"approximate""#), "json: {json}");
1040        assert!(
1041            json.contains(r#""approximate":{"city":"San Francisco"}"#),
1042            "json: {json}"
1043        );
1044    }
1045
1046    /// `JSONSchema`/`ToolFunction` optional fields are omitted when unset.
1047    #[test]
1048    fn json_schema_optional_fields_serialization() {
1049        let schema = JSONSchema {
1050            name: "Answer".to_string(),
1051            description: None,
1052            schema: None,
1053            strict: None,
1054        };
1055        let json = serde_json::to_string(&schema).unwrap();
1056        assert_eq!(json, r#"{"name":"Answer"}"#);
1057
1058        let function = ToolFunction {
1059            name: "get_weather".to_string(),
1060            description: None,
1061            parameters: None,
1062            strict: None,
1063        };
1064        let json = serde_json::to_string(&function).unwrap();
1065        assert_eq!(json, r#"{"name":"get_weather"}"#);
1066    }
1067
1068    /// Serializes the OpenAI `reasoning_effort` parameter.
1069    #[test]
1070    fn reasoning_effort_serialization() {
1071        let request = RequestBody {
1072            messages: vec![Message::User {
1073                content: "What's your name?".to_string(),
1074                name: None,
1075            }],
1076            model: "gpt-5".to_string(),
1077            reasoning_effort: Some(ReasoningEffort::Xhigh),
1078            ..Default::default()
1079        };
1080
1081        let json = serde_json::to_string(&request).unwrap();
1082        assert!(
1083            json.contains(r#""reasoning_effort":"xhigh""#),
1084            "json: {json}"
1085        );
1086    }
1087
1088    /// Serializes the DeepSeek Beta chat prefix completion fields.
1089    #[cfg(feature = "deepseek")]
1090    #[test]
1091    fn deepseek_assistant_prefix_serialization() {
1092        let request = RequestBody {
1093            messages: vec![
1094                Message::User {
1095                    content: "Please write quick sort code".to_string(),
1096                    name: None,
1097                },
1098                Message::Assistant {
1099                    content: Some("```python\n".to_string()),
1100                    audio: None,
1101                    refusal: None,
1102                    name: None,
1103                    prefix: true,
1104                    reasoning_content: None,
1105                    tool_calls: None,
1106                },
1107            ],
1108            model: DEEPSEEK_MODEL.to_string(),
1109            ..Default::default()
1110        };
1111
1112        let json = serde_json::to_string(&request).unwrap();
1113        assert!(json.contains(r#""prefix":true"#), "json: {json}");
1114    }
1115
1116    /// Serializes the DeepSeek `thinking`, `reasoning_effort` and `user_id`
1117    /// request parameters.
1118    #[cfg(feature = "deepseek")]
1119    #[test]
1120    fn deepseek_thinking_params_serialization() {
1121        let request = RequestBody {
1122            messages: vec![Message::User {
1123                content: "What's your name?".to_string(),
1124                name: None,
1125            }],
1126            model: DEEPSEEK_MODEL.to_string(),
1127            thinking: Some(DeepSeekThinking {
1128                type_: DeepSeekThinkingType::Disabled,
1129            }),
1130            user_id: Some("user-123".to_string()),
1131            ..Default::default()
1132        };
1133
1134        let json = serde_json::to_string(&request).unwrap();
1135        assert!(
1136            json.contains(r#""thinking":{"type":"disabled"}"#),
1137            "json: {json}"
1138        );
1139        assert!(json.contains(r#""user_id":"user-123""#), "json: {json}");
1140    }
1141
1142    /// Serializes the Qwen `enable_thinking`, `thinking_budget` and `top_k`
1143    /// request parameters.
1144    #[cfg(feature = "qwen")]
1145    #[test]
1146    fn qwen_params_serialization() {
1147        let request = RequestBody {
1148            messages: vec![Message::User {
1149                content: "What's your name?".to_string(),
1150                name: None,
1151            }],
1152            model: "qwen-plus".to_string(),
1153            enable_thinking: Some(false),
1154            thinking_budget: Some(1024),
1155            top_k: Some(20),
1156            ..Default::default()
1157        };
1158
1159        let json = serde_json::to_string(&request).unwrap();
1160        assert!(json.contains(r#""enable_thinking":false"#), "json: {json}");
1161        assert!(json.contains(r#""thinking_budget":1024"#), "json: {json}");
1162        assert!(json.contains(r#""top_k":20"#), "json: {json}");
1163    }
1164}