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 = "role", 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    pub description: String,
477    /// The schema for the response format, described as a JSON Schema object. Learn how
478    /// to build JSON schemas [here](https://json-schema.org/).
479    pub schema: serde_json::Map<String, serde_json::Value>,
480    /// Whether to enable strict schema adherence when generating the output. If set to
481    /// true, the model will always follow the exact schema defined in the `schema`
482    /// field. Only a subset of JSON Schema is supported when `strict` is `true`. To
483    /// learn more, read the
484    /// [Structured Outputs guide](https://platform.openai.com/docs/guides/structured-outputs).
485    pub strict: Option<bool>,
486}
487
488#[derive(Serialize, Debug, Clone)]
489#[serde(rename_all = "snake_case")]
490pub enum Modality {
491    Text,
492    Audio,
493}
494
495/// Parameters for audio output of a chat completion.
496#[derive(Serialize, Debug, Clone)]
497pub struct ChatCompletionAudioParam {
498    /// Specifies the output audio format. Must be one of `wav`, `aac`, `mp3`,
499    /// `flac`, `opus`, or `pcm16`.
500    pub format: AudioFormat,
501    /// The voice the model uses to respond.
502    pub voice: Voice,
503}
504
505/// The output audio format of a chat completion.
506#[derive(Serialize, Debug, Clone)]
507#[serde(rename_all = "snake_case")]
508pub enum AudioFormat {
509    Wav,
510    Aac,
511    Mp3,
512    Flac,
513    Opus,
514    Pcm16,
515}
516
517/// The voice the model uses to respond with audio output.
518#[derive(Serialize, Debug, Clone)]
519#[serde(untagged)]
520pub enum Voice {
521    /// A built-in voice name, e.g. `alloy`, `ash`, `ballad`, `coral`, `echo`,
522    /// `sage`, `shimmer`, or `verse`.
523    BuiltIn(String),
524    /// A custom voice reference, e.g. `{ "id": "voice_1234" }`.
525    Custom {
526        /// The custom voice ID, e.g. `voice_1234`.
527        id: String,
528    },
529}
530
531#[derive(Serialize, Debug, Clone)]
532pub struct ChatCompletionPredictionContentParam {
533    /// The content that should be matched when generating a model response. If
534    /// generated tokens would match this content, the entire model response can be
535    /// returned much more quickly.
536    pub content: ChatCompletionPredictionContentParamContent,
537
538    /// The type of the predicted content you want to provide.
539    /// This type is currently always `content`.
540    pub type_: ChatCompletionPredictionContentParamType,
541}
542
543#[derive(Serialize, Debug, Clone)]
544#[serde(untagged)]
545pub enum ChatCompletionPredictionContentParamContent {
546    Text(String),
547    ChatCompletionContentPartTextParam {
548        /// The text content.
549        text: String,
550        /// The type of the content part.
551        #[serde(rename = "type")]
552        type_: ChatCompletionContentPartTextParamType,
553    },
554}
555
556#[derive(Serialize, Debug, Clone)]
557#[serde(rename_all = "snake_case")]
558pub enum ChatCompletionContentPartTextParamType {
559    Text,
560}
561
562#[derive(Serialize, Debug, Clone)]
563#[serde(rename_all = "snake_case")]
564pub enum ChatCompletionPredictionContentParamType {
565    Content,
566}
567
568/// DeepSeek: skip-serialization helper for the Beta `prefix` message field.
569#[cfg(feature = "deepseek")]
570#[inline]
571fn is_false(value: &bool) -> bool {
572    !value
573}
574
575#[derive(Serialize, Debug, Clone)]
576#[serde(untagged)]
577pub enum StopKeywords {
578    Word(String),
579    Words(Vec<String>),
580}
581
582#[derive(Serialize, Debug, Clone)]
583#[serde(rename_all = "snake_case")]
584pub enum LowMediumHighEnum {
585    Low,
586    Medium,
587    High,
588}
589
590#[derive(Serialize, Debug, Clone)]
591pub struct WebSearchOptions {
592    /// High level guidance for the amount of context window space to use for the
593    /// search. One of `low`, `medium`, or `high`. `medium` is the default.
594    pub search_context_size: LowMediumHighEnum,
595
596    pub user_location: Option<WebSearchOptionsUserLocation>,
597}
598
599#[derive(Serialize, Debug, Clone)]
600#[serde(tag = "type", rename_all = "snake_case")]
601pub enum WebSearchOptionsUserLocation {
602    /// The type of location approximation. Always `approximate`.
603    Approximate(WebSearchOptionsUserLocationApproximate),
604}
605
606#[derive(Serialize, Debug, Clone)]
607pub struct WebSearchOptionsUserLocationApproximate {
608    /// Free text input for the city of the user, e.g. `San Francisco`.
609    pub city: String,
610
611    /// The two-letter [ISO country code](https://en.wikipedia.org/wiki/ISO_3166-1) of
612    /// the user, e.g. `US`.
613    pub country: String,
614
615    /// Free text input for the region of the user, e.g. `California`.
616    pub region: String,
617
618    /// The [IANA timezone](https://timeapi.io/documentation/iana-timezones) of the
619    /// user, e.g. `America/Los_Angeles`.
620    pub timezone: String,
621}
622
623#[derive(Serialize, Debug, Clone)]
624pub struct StreamOptions {
625    /// If set, an additional chunk will be streamed before the `data: [DONE]` message.
626    ///
627    /// The `usage` field on this chunk shows the token usage statistics for the entire
628    /// request, and the `choices` field will always be an empty array.
629    ///
630    /// All other chunks will also include a `usage` field, but with a null value.
631    /// **NOTE:** If the stream is interrupted, you may not receive the final usage
632    /// chunk which contains the total token usage for the request.
633    pub include_usage: bool,
634}
635
636#[derive(Serialize, Debug, Clone)]
637#[serde(tag = "type", rename_all = "snake_case")]
638pub enum RequestTool {
639    /// The type of the tool. Currently, only `function` is supported.
640    Function { function: ToolFunction },
641    /// The type of the custom tool. Always `custom`.
642    Custom {
643        /// Properties of the custom tool.
644        custom: ToolCustom,
645    },
646}
647
648#[derive(Serialize, Debug, Clone)]
649pub struct ToolFunction {
650    /// The name of the function to be called. Must be a-z, A-Z, 0-9, or
651    /// contain underscores and dashes, with a maximum length
652    /// of 64.
653    pub name: String,
654    /// A description of what the function does, used by the model to choose when and
655    /// how to call the function.
656    pub description: String,
657    /// The parameters the functions accepts, described as a JSON Schema object.
658    ///
659    /// See the
660    /// [openai function calling guide](https://platform.openai.com/docs/guides/function-calling)
661    /// for examples, and the
662    /// [JSON Schema reference](https://json-schema.org/understanding-json-schema/) for
663    /// documentation about the format.
664    ///
665    /// Omitting `parameters` defines a function with an empty parameter list.
666    pub parameters: serde_json::Map<String, serde_json::Value>,
667    /// Whether to enable strict schema adherence when generating the function call.
668    ///
669    /// If set to true, the model will follow the exact schema defined in the
670    /// `parameters` field. Only a subset of JSON Schema is supported when `strict` is
671    /// `true`. Learn more about Structured Outputs in the
672    /// [openai function calling guide](https://platform.openai.com/docs/guides/function-calling).
673    #[serde(skip_serializing_if = "Option::is_none")]
674    pub strict: Option<bool>,
675}
676
677#[derive(Serialize, Debug, Clone)]
678pub struct ToolCustom {
679    /// The name of the custom tool, used to identify it in tool calls.
680    pub name: String,
681    /// Optional description of the custom tool, used to provide more context.
682    pub description: String,
683    /// The input format for the custom tool. Default is unconstrained text.
684    pub format: String,
685}
686
687#[derive(Serialize, Debug, Clone)]
688#[serde(rename_all = "snake_case", tag = "type")]
689pub enum ToolCustomFormat {
690    /// Unconstrained text format. Always `text`.
691    CustomFormatText,
692    /// Grammar format. Always `grammar`.
693    CustomFormatGrammar {
694        /// Your chosen grammar.
695        grammar: ToolCustomFormatGrammarGrammar,
696    },
697}
698
699#[derive(Debug, Serialize, Clone)]
700pub struct ToolCustomFormatGrammarGrammar {
701    /// The grammar definition.
702    pub definition: String,
703    /// The syntax of the grammar definition. One of `lark` or `regex`.
704    pub syntax: ToolCustomFormatGrammarGrammarSyntax,
705}
706
707#[derive(Debug, Serialize, Clone)]
708#[serde(rename_all = "snake_case")]
709pub enum ToolCustomFormatGrammarGrammarSyntax {
710    Lark,
711    Regex,
712}
713
714#[derive(Debug, Serialize, Clone)]
715#[serde(rename_all = "snake_case")]
716pub enum ToolChoice {
717    None,
718    Auto,
719    Required,
720    #[serde(untagged)]
721    Specific(ToolChoiceSpecific),
722}
723
724#[derive(Debug, Serialize, Clone)]
725#[serde(rename_all = "snake_case", tag = "type")]
726pub enum ToolChoiceSpecific {
727    /// Allowed tool configuration type. Always `allowed_tools`.
728    AllowedTools {
729        /// Constrains the tools available to the model to a pre-defined set.
730        allowed_tools: ToolChoiceAllowedTools,
731    },
732    /// For function calling, the type is always `function`.
733    Function { function: ToolChoiceFunction },
734    /// For custom tool calling, the type is always `custom`.
735    Custom { custom: ToolChoiceCustom },
736}
737
738#[derive(Debug, Serialize, Clone)]
739pub struct ToolChoiceAllowedTools {
740    /// Constrains the tools available to the model to a pre-defined set.
741    ///
742    /// - `auto` allows the model to pick from among the allowed tools and generate a
743    ///   message.
744    /// - `required` requires the model to call one or more of the allowed tools.
745    pub mode: ToolChoiceAllowedToolsMode,
746    /// A list of tool definitions that the model should be allowed to call.
747    ///
748    /// For the Chat Completions API, the list of tool definitions might look like:
749    ///
750    /// ```json
751    /// [
752    ///   { "type": "function", "function": { "name": "get_weather" } },
753    ///   { "type": "function", "function": { "name": "get_time" } }
754    /// ]
755    /// ```
756    pub tools: serde_json::Map<String, serde_json::Value>,
757}
758
759/// The mode for allowed tools in tool choice.
760///
761/// Controls how the model should handle the set of allowed tools:
762///
763/// - `auto` allows the model to pick from among the allowed tools and generate a
764///   message.
765/// - `required` requires the model to call one or more of the allowed tools.
766#[derive(Debug, Serialize, Clone)]
767#[serde(rename_all = "lowercase")]
768pub enum ToolChoiceAllowedToolsMode {
769    /// The model can choose whether to use the allowed tools or not.
770    Auto,
771    /// The model must use at least one of the allowed tools.
772    Required,
773}
774
775#[derive(Debug, Serialize, Clone)]
776pub struct ToolChoiceFunction {
777    /// The name of the function to call.
778    pub name: String,
779}
780
781#[derive(Debug, Serialize, Clone)]
782pub struct ToolChoiceCustom {
783    /// The name of the custom tool to call.
784    pub name: String,
785}
786
787/// DeepSeek: controls the switch between thinking and non-thinking mode.
788#[cfg(feature = "deepseek")]
789#[derive(Debug, Serialize, Clone, PartialEq, Eq)]
790pub struct DeepSeekThinking {
791    /// Whether to use thinking mode (`enabled`) or non-thinking mode
792    /// (`disabled`). Defaults to `enabled`.
793    #[serde(rename = "type")]
794    pub type_: DeepSeekThinkingType,
795}
796
797/// DeepSeek: whether thinking mode is enabled or disabled.
798#[cfg(feature = "deepseek")]
799#[derive(Debug, Serialize, Clone, PartialEq, Eq)]
800#[serde(rename_all = "lowercase")]
801pub enum DeepSeekThinkingType {
802    Enabled,
803    Disabled,
804}
805
806/// Constrains the effort on reasoning for reasoning models. This is an
807/// official OpenAI parameter; reasoning providers such as DeepSeek and Qwen
808/// accept a subset of these values and map the rest to their nearest effort
809/// level.
810#[derive(Debug, Serialize, Clone, PartialEq, Eq)]
811#[serde(rename_all = "lowercase")]
812pub enum ReasoningEffort {
813    None,
814    Minimal,
815    Low,
816    Medium,
817    High,
818    Xhigh,
819    Max,
820}
821
822impl RequestBody {
823    /// Whether this request asks for a streamed response. Defaults to
824    /// `false` when [`RequestBody::stream`] is `None`.
825    pub fn is_streaming(&self) -> bool {
826        self.stream.unwrap_or(false)
827    }
828}
829
830impl Post for RequestBody {
831    fn is_streaming(&self) -> bool {
832        RequestBody::is_streaming(self)
833    }
834
835    /// Builds the URL for the request.
836    ///
837    /// `base_url` should be like <https://api.openai.com/v1>
838    fn build_url(&self, base_url: &str) -> Result<String, OapiError> {
839        let mut url = Url::parse(base_url.trim_end_matches('/')).map_err(OapiError::UrlError)?;
840        url.path_segments_mut()
841            .map_err(|_| OapiError::UrlCannotBeBase(base_url.to_string()))?
842            .push("chat")
843            .push("completions");
844
845        Ok(url.to_string())
846    }
847}
848
849impl PostNoStream for RequestBody {
850    type Response = super::response::no_streaming::ChatCompletion;
851}
852
853impl PostStream for RequestBody {
854    type Response = super::response::streaming::ChatCompletionChunk;
855}
856
857#[cfg(test)]
858mod request_test {
859    use futures_util::StreamExt;
860
861    use super::*;
862
863    const DEEPSEEK_CHAT_URL: &str = "https://api.deepseek.com";
864    const DEEPSEEK_MODEL: &str = "deepseek-v4-flash";
865
866    fn deepseek_api_key() -> Option<String> {
867        std::env::var("DEEPSEEK_API_KEY")
868            .ok()
869            .map(|key| key.trim().to_string())
870            .filter(|key| !key.is_empty())
871    }
872
873    #[tokio::test]
874    async fn test_deepseek_no_stream() {
875        let Some(api_key) = deepseek_api_key() else {
876            println!("Skipping: set DEEPSEEK_API_KEY to run this test");
877            return;
878        };
879
880        let request = RequestBody {
881            messages: vec![
882                Message::System {
883                    content: "This is a request of test purpose. Reply briefly".to_string(),
884                    name: None,
885                },
886                Message::User {
887                    content: "What's your name?".to_string(),
888                    name: None,
889                },
890            ],
891            model: DEEPSEEK_MODEL.to_string(),
892            stream: Some(false),
893            ..Default::default()
894        };
895
896        let response = request
897            .get_response_string(&crate::rest::default_client(), DEEPSEEK_CHAT_URL, &api_key)
898            .await
899            .unwrap();
900
901        println!("{}", response);
902
903        assert!(response.to_ascii_lowercase().contains("deepseek"));
904    }
905
906    #[tokio::test]
907    async fn test_deepseek_stream() {
908        let Some(api_key) = deepseek_api_key() else {
909            println!("Skipping: set DEEPSEEK_API_KEY to run this test");
910            return;
911        };
912
913        let request = RequestBody {
914            messages: vec![
915                Message::System {
916                    content: "This is a request of test purpose. Reply briefly".to_string(),
917                    name: None,
918                },
919                Message::User {
920                    content: "Who are you?".to_string(),
921                    name: None,
922                },
923            ],
924            model: DEEPSEEK_MODEL.to_string(),
925            stream: Some(true),
926            ..Default::default()
927        };
928
929        let mut response = request
930            .get_stream_response_string(&crate::rest::default_client(), DEEPSEEK_CHAT_URL, &api_key)
931            .await
932            .unwrap();
933
934        while let Some(chunk) = response.next().await {
935            println!("{}", chunk.unwrap());
936        }
937    }
938
939    /// Serializes the OpenAI `reasoning_effort` parameter.
940    #[test]
941    fn reasoning_effort_serialization() {
942        let request = RequestBody {
943            messages: vec![Message::User {
944                content: "What's your name?".to_string(),
945                name: None,
946            }],
947            model: "gpt-5".to_string(),
948            reasoning_effort: Some(ReasoningEffort::Xhigh),
949            ..Default::default()
950        };
951
952        let json = serde_json::to_string(&request).unwrap();
953        assert!(
954            json.contains(r#""reasoning_effort":"xhigh""#),
955            "json: {json}"
956        );
957    }
958
959    /// Serializes the DeepSeek Beta chat prefix completion fields.
960    #[cfg(feature = "deepseek")]
961    #[test]
962    fn deepseek_assistant_prefix_serialization() {
963        let request = RequestBody {
964            messages: vec![
965                Message::User {
966                    content: "Please write quick sort code".to_string(),
967                    name: None,
968                },
969                Message::Assistant {
970                    content: Some("```python\n".to_string()),
971                    audio: None,
972                    refusal: None,
973                    name: None,
974                    prefix: true,
975                    reasoning_content: None,
976                    tool_calls: None,
977                },
978            ],
979            model: DEEPSEEK_MODEL.to_string(),
980            ..Default::default()
981        };
982
983        let json = serde_json::to_string(&request).unwrap();
984        assert!(json.contains(r#""prefix":true"#), "json: {json}");
985    }
986
987    /// Serializes the DeepSeek `thinking`, `reasoning_effort` and `user_id`
988    /// request parameters.
989    #[cfg(feature = "deepseek")]
990    #[test]
991    fn deepseek_thinking_params_serialization() {
992        let request = RequestBody {
993            messages: vec![Message::User {
994                content: "What's your name?".to_string(),
995                name: None,
996            }],
997            model: DEEPSEEK_MODEL.to_string(),
998            thinking: Some(DeepSeekThinking {
999                type_: DeepSeekThinkingType::Disabled,
1000            }),
1001            user_id: Some("user-123".to_string()),
1002            ..Default::default()
1003        };
1004
1005        let json = serde_json::to_string(&request).unwrap();
1006        assert!(
1007            json.contains(r#""thinking":{"type":"disabled"}"#),
1008            "json: {json}"
1009        );
1010        assert!(json.contains(r#""user_id":"user-123""#), "json: {json}");
1011    }
1012
1013    /// Serializes the Qwen `enable_thinking`, `thinking_budget` and `top_k`
1014    /// request parameters.
1015    #[cfg(feature = "qwen")]
1016    #[test]
1017    fn qwen_params_serialization() {
1018        let request = RequestBody {
1019            messages: vec![Message::User {
1020                content: "What's your name?".to_string(),
1021                name: None,
1022            }],
1023            model: "qwen-plus".to_string(),
1024            enable_thinking: Some(false),
1025            thinking_budget: Some(1024),
1026            top_k: Some(20),
1027            ..Default::default()
1028        };
1029
1030        let json = serde_json::to_string(&request).unwrap();
1031        assert!(json.contains(r#""enable_thinking":false"#), "json: {json}");
1032        assert!(json.contains(r#""thinking_budget":1024"#), "json: {json}");
1033        assert!(json.contains(r#""top_k":20"#), "json: {json}");
1034    }
1035}