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}