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}