openai_interface/chat/
request.rs

1//! This module contains the request body and POST method for the chat completion API.
2
3use serde::Serialize;
4
5use crate::rest::post::{Post, PostNoStream, PostStream};
6
7/// Creates a model response for the given chat conversation.
8///
9/// # Example
10///
11/// ```rust
12/// use std::sync::LazyLock;
13/// use futures_util::StreamExt;
14/// use openai_interface::chat::request::{Message, RequestBody};
15/// use openai_interface::rest::post::PostStream;
16///
17/// const DEEPSEEK_API_KEY: LazyLock<&str> =
18///     LazyLock::new(|| include_str!("../.././keys/deepseek_domestic_key").trim());
19/// const DEEPSEEK_CHAT_URL: &'static str = "https://api.deepseek.com/chat/completions";
20/// const DEEPSEEK_MODEL: &'static str = "deepseek-chat";
21///
22/// #[tokio::main]
23/// async fn main() {
24///     let request = RequestBody {
25///         messages: vec![
26///             Message::System {
27///                 content: "This is a request of test purpose. Reply briefly".to_string(),
28///                 name: None,
29///             },
30///             Message::User {
31///                 content: "What's your name?".to_string(),
32///                 name: None,
33///             },
34///         ],
35///         model: DEEPSEEK_MODEL.to_string(),
36///         stream: true,
37///         ..Default::default()
38///     };
39///
40///     let mut response = request
41///         .get_stream_response_string(DEEPSEEK_CHAT_URL, *DEEPSEEK_API_KEY)
42///         .await
43///         .unwrap();
44///
45///     while let Some(chunk) = response.next().await {
46///         println!("{}", chunk.unwrap());
47///     }
48/// }
49/// ```
50#[derive(Serialize, Debug, Default, Clone)]
51pub struct RequestBody {
52    /// A list of messages comprising the conversation so far.
53    pub messages: Vec<Message>,
54
55    /// Name of the model to use to generate the response.
56    pub model: String,
57
58    /// Although it is optional, you should explicitly designate it
59    /// for an expected response.
60    pub stream: bool,
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    /// Number between -2.0 and 2.0. Positive values penalize new tokens based on
69    /// whether they appear in the text so far, increasing the model's likelihood to
70    /// talk about new topics.
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub presence_penalty: Option<f32>,
73
74    /// The maximum number of tokens that can be generated in the chat completion.
75    /// Deprecated according to OpenAI's Python SDK in favour of
76    /// `max_completion_tokens`.
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub max_tokens: Option<u32>,
79
80    /// An upper bound for the number of tokens that can be generated for a completion,
81    /// including visible output tokens and reasoning tokens.
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub max_completion_tokens: Option<u32>,
84
85    /// specifying the format that the model must output.
86    ///
87    /// Setting to `{ "type": "json_schema", "json_schema": {...} }` enables Structured
88    /// Outputs which ensures the model will match your supplied JSON schema. Learn more
89    /// in the
90    /// [Structured Outputs guide](https://platform.openai.com/docs/guides/structured-outputs).
91    /// Setting to `{ "type": "json_object" }` enables the older JSON mode, which
92    /// ensures the message the model generates is valid JSON. Using `json_schema` is
93    /// preferred for models that support it.
94    #[serde(skip_serializing_if = "Option::is_none")]
95    pub response_format: Option<ResponseFormat>, // The type of this attribute needs improvements.
96
97    /// A stable identifier used to help detect users of your application that may be
98    /// violating OpenAI's usage policies. The IDs should be a string that uniquely
99    /// identifies each user. It is recommended to hash their username or email address, in
100    /// order to avoid sending any identifying information.
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub safety_identifier: Option<String>,
103
104    /// If specified, the system will make a best effort to sample deterministically. Determinism
105    /// is not guaranteed, and you should refer to the `system_fingerprint` response parameter to
106    /// monitor changes in the backend.
107    #[serde(skip_serializing_if = "Option::is_none")]
108    pub seed: Option<i64>,
109
110    /// How many chat completion choices to generate for each input message. Note that
111    /// you will be charged based on the number of generated tokens across all of the
112    /// choices. Keep `n` as `1` to minimize costs.
113    #[serde(skip_serializing_if = "Option::is_none")]
114    pub n: Option<u32>,
115
116    /// Up to 4 sequences where the API will stop generating further tokens. The
117    /// returned text will not contain the stop sequence.
118    #[serde(skip_serializing_if = "Option::is_none")]
119    pub stop: Option<StopKeywords>,
120
121    /// Options for streaming response. Only set this when you set `stream: true`
122    #[serde(skip_serializing_if = "Option::is_none")]
123    pub stream_options: Option<StreamOptions>,
124
125    /// What sampling temperature to use, between 0 and 2. Higher values like 0.8 will
126    /// make the output more random, while lower values like 0.2 will make it more
127    /// focused and deterministic. It is generally recommended to alter this or `top_p` but
128    /// not both.
129    pub temperature: Option<f32>,
130
131    /// An alternative to sampling with temperature, called nucleus sampling, where the
132    /// model considers the results of the tokens with top_p probability mass. So 0.1
133    /// means only the tokens comprising the top 10% probability mass are considered.
134    ///
135    /// It is generally recommended to alter this or `temperature` but not both.
136    pub top_p: Option<f32>,
137
138    /// A list of tools the model may call.
139    #[serde(skip_serializing_if = "Option::is_none")]
140    pub tools: Option<Vec<RequestTool>>,
141
142    /// Controls which (if any) tool is called by the model. `none` means the model will
143    /// not call any tool and instead generates a message. `auto` means the model can
144    /// pick between generating a message or calling one or more tools. `required` means
145    /// the model must call one or more tools. Specifying a particular tool via
146    /// `{"type": "function", "function": {"name": "my_function"}}` forces the model to
147    /// call that tool.
148    #[serde(skip_serializing_if = "Option::is_none")]
149    pub tool_choice: Option<ToolChoice>,
150
151    /// Whether to return log probabilities of the output tokens or not. If true,
152    /// returns the log probabilities of each output token returned in the `content` of
153    /// `message`.
154    #[serde(skip_serializing_if = "Option::is_none")]
155    pub logprobs: Option<bool>,
156
157    /// An integer between 0 and 20 specifying the number of most likely tokens to
158    /// return at each token position, each with an associated log probability.
159    /// `logprobs` must be set to `true` if this parameter is used.
160    #[serde(skip_serializing_if = "Option::is_none")]
161    pub top_logprobs: Option<u32>,
162
163    /// Other request bodies that are not in standard OpenAI API.
164    #[serde(flatten, skip_serializing_if = "Option::is_none")]
165    pub extra_body: Option<ExtraBody>,
166
167    /// Other request bodies that are not in standard OpenAI API and
168    /// not included in the ExtraBody struct.
169    #[serde(flatten, skip_serializing_if = "Option::is_none")]
170    pub extra_body_map: Option<serde_json::Map<String, serde_json::Value>>,
171}
172
173#[derive(Serialize, Debug, Clone)]
174#[serde(tag = "role", rename_all = "lowercase")]
175pub enum Message {
176    /// In this case, the role of the message author is `system`.
177    /// The field `{ role = "system" }` is added automatically.
178    System {
179        /// The contents of the system message.
180        content: String,
181        /// An optional name for the participant.
182        ///
183        /// Provides the model information to differentiate between
184        /// participants of the same role.
185        #[serde(skip_serializing_if = "Option::is_none")]
186        name: Option<String>,
187    },
188    /// In this case, the role of the message author is `user`.
189    /// The field `{ role = "user" }` is added automatically.
190    User {
191        /// The contents of the user message.
192        content: String,
193        /// An optional name for the participant.
194        ///
195        /// Provides the model information to differentiate between
196        /// participants of the same role.
197        #[serde(skip_serializing_if = "Option::is_none")]
198        name: Option<String>,
199    },
200    /// In this case, the role of the message author is `assistant`.
201    /// The field `{ role = "assistant" }` is added automatically.
202    ///
203    /// Unimplemented params:
204    /// - _audio_: Data about a previous audio response from the model.
205    Assistant {
206        /// The contents of the assistant message. Required unless `tool_calls`
207        /// or `function_call` is specified. (Note that `function_call` is deprecated
208        /// in favour of `tool_calls`.)
209        content: Option<String>,
210        /// The refusal message by the assistant.
211        #[serde(skip_serializing_if = "Option::is_none")]
212        refusal: Option<String>,
213        #[serde(skip_serializing_if = "Option::is_none")]
214        name: Option<String>,
215        /// Set this to true for completion
216        #[serde(skip_serializing_if = "is_false")]
217        prefix: bool,
218        /// Used for the deepseek-reasoner model in the Chat Prefix
219        /// Completion feature as the input for the CoT in the last
220        /// assistant message. When using this feature, the prefix
221        /// parameter must be set to true.
222        #[serde(skip_serializing_if = "Option::is_none")]
223        reasoning_content: Option<String>,
224
225        /// The tool calls generated by the model, such as function calls.
226        #[serde(skip_serializing_if = "Option::is_none")]
227        tool_calls: Option<Vec<AssistantToolCall>>,
228    },
229    /// In this case, the role of the message author is `assistant`.
230    /// The field `{ role = "tool" }` is added automatically.
231    Tool {
232        /// The contents of the tool message.
233        content: String,
234        /// Tool call that this message is responding to.
235        tool_call_id: String,
236    },
237    /// In this case, the role of the message author is `function`.
238    /// The field `{ role = "function" }` is added automatically.
239    Function {
240        /// The contents of the function message.
241        content: String,
242        /// The name of the function to call.
243        name: String,
244    },
245    /// In this case, the role of the message author is `developer`.
246    /// The field `{ role = "developer" }` is added automatically.
247    Developer {
248        /// The contents of the developer message.
249        content: String,
250        /// An optional name for the participant.
251        ///
252        /// Provides the model information to differentiate between
253        /// participants of the same role.
254        name: Option<String>,
255    },
256}
257
258#[derive(Debug, Serialize, Clone)]
259#[serde(tag = "role", rename_all = "lowercase")]
260pub enum AssistantToolCall {
261    Function {
262        /// The ID of the tool call.
263        id: String,
264        /// The function that the model called.
265        function: ToolCallFunction,
266    },
267    Custom {
268        /// The ID of the tool call.
269        id: String,
270        /// The custom tool that the model called.
271        custom: ToolCallCustom,
272    },
273}
274
275#[derive(Debug, Serialize, Clone)]
276pub struct ToolCallFunction {
277    /// The arguments to call the function with, as generated by the model in JSON
278    /// format. Note that the model does not always generate valid JSON, and may
279    /// hallucinate parameters not defined by your function schema. Validate the
280    /// arguments in your code before calling your function.
281    arguments: String,
282    /// The name of the function to call.
283    name: String,
284}
285
286#[derive(Debug, Serialize, Clone)]
287pub struct ToolCallCustom {
288    /// The input for the custom tool call generated by the model.
289    input: String,
290    /// The name of the custom tool to call.
291    name: String,
292}
293
294#[derive(Debug, Serialize, Clone)]
295#[serde(tag = "type", rename_all = "snake_case")]
296pub enum ResponseFormat {
297    /// The type of response format being defined. Always `json_schema`.
298    JsonSchema {
299        /// Structured Outputs configuration options, including a JSON Schema.
300        json_schema: JSONSchema,
301    },
302    /// The type of response format being defined. Always `json_object`.
303    JsonObject,
304    /// The type of response format being defined. Always `text`.
305    Text,
306}
307
308#[derive(Debug, Serialize, Clone)]
309pub struct JSONSchema {
310    /// The name of the response format. Must be a-z, A-Z, 0-9, or contain
311    /// underscores and dashes, with a maximum length of 64.
312    pub name: String,
313    /// A description of what the response format is for, used by the model to determine
314    /// how to respond in the format.
315    pub description: String,
316    /// The schema for the response format, described as a JSON Schema object. Learn how
317    /// to build JSON schemas [here](https://json-schema.org/).
318    pub schema: serde_json::Map<String, serde_json::Value>,
319    /// Whether to enable strict schema adherence when generating the output. If set to
320    /// true, the model will always follow the exact schema defined in the `schema`
321    /// field. Only a subset of JSON Schema is supported when `strict` is `true`. To
322    /// learn more, read the
323    /// [Structured Outputs guide](https://platform.openai.com/docs/guides/structured-outputs).
324    pub strict: Option<bool>,
325}
326
327#[inline]
328fn is_false(value: &bool) -> bool {
329    !value
330}
331
332#[derive(Serialize, Debug, Clone)]
333#[serde(untagged)]
334pub enum StopKeywords {
335    Word(String),
336    Words(Vec<String>),
337}
338
339#[derive(Serialize, Debug, Clone)]
340pub struct StreamOptions {
341    /// If set, an additional chunk will be streamed before the `data: [DONE]` message.
342    ///
343    /// The `usage` field on this chunk shows the token usage statistics for the entire
344    /// request, and the `choices` field will always be an empty array.
345    ///
346    /// All other chunks will also include a `usage` field, but with a null value.
347    /// **NOTE:** If the stream is interrupted, you may not receive the final usage
348    /// chunk which contains the total token usage for the request.
349    pub include_usage: bool,
350}
351
352#[derive(Serialize, Debug, Clone)]
353#[serde(tag = "type", rename_all = "snake_case")]
354pub enum RequestTool {
355    /// The type of the tool. Currently, only `function` is supported.
356    Function { function: ToolFunction },
357    /// The type of the custom tool. Always `custom`.
358    Custom {
359        /// Properties of the custom tool.
360        custom: ToolCustom,
361    },
362}
363
364#[derive(Serialize, Debug, Clone)]
365pub struct ToolFunction {
366    /// The name of the function to be called. Must be a-z, A-Z, 0-9, or
367    /// contain underscores and dashes, with a maximum length
368    /// of 64.
369    pub name: String,
370    /// A description of what the function does, used by the model to choose when and
371    /// how to call the function.
372    pub description: String,
373    /// The parameters the functions accepts, described as a JSON Schema object.
374    ///
375    /// See the
376    /// [openai function calling guide](https://platform.openai.com/docs/guides/function-calling)
377    /// for examples, and the
378    /// [JSON Schema reference](https://json-schema.org/understanding-json-schema/) for
379    /// documentation about the format.
380    ///
381    /// Omitting `parameters` defines a function with an empty parameter list.
382    pub parameters: serde_json::Map<String, serde_json::Value>,
383    /// Whether to enable strict schema adherence when generating the function call.
384    ///
385    /// If set to true, the model will follow the exact schema defined in the
386    /// `parameters` field. Only a subset of JSON Schema is supported when `strict` is
387    /// `true`. Learn more about Structured Outputs in the
388    /// [openai function calling guide](https://platform.openai.com/docs/guides/function-calling).
389    #[serde(skip_serializing_if = "Option::is_none")]
390    pub strict: Option<bool>,
391}
392
393#[derive(Serialize, Debug, Clone)]
394pub struct ToolCustom {
395    /// The name of the custom tool, used to identify it in tool calls.
396    pub name: String,
397    /// Optional description of the custom tool, used to provide more context.
398    pub description: String,
399    /// The input format for the custom tool. Default is unconstrained text.
400    pub format: String,
401}
402
403#[derive(Serialize, Debug, Clone)]
404#[serde(rename_all = "snake_case", tag = "type")]
405pub enum ToolCustomFormat {
406    /// Unconstrained text format. Always `text`.
407    CustomFormatText,
408    /// Grammar format. Always `grammar`.
409    CustomFormatGrammar {
410        /// Your chosen grammar.
411        grammar: ToolCustomFormatGrammarGrammar,
412    },
413}
414
415#[derive(Debug, Serialize, Clone)]
416pub struct ToolCustomFormatGrammarGrammar {
417    /// The grammar definition.
418    pub definition: String,
419    /// The syntax of the grammar definition. One of `lark` or `regex`.
420    pub syntax: ToolCustomFormatGrammarGrammarSyntax,
421}
422
423#[derive(Debug, Serialize, Clone)]
424#[serde(rename_all = "snake_case")]
425pub enum ToolCustomFormatGrammarGrammarSyntax {
426    Lark,
427    Regex,
428}
429
430#[derive(Debug, Serialize, Clone)]
431#[serde(rename_all = "snake_case")]
432pub enum ToolChoice {
433    None,
434    Auto,
435    Required,
436    #[serde(untagged)]
437    Specific(ToolChoiceSpecific),
438}
439
440#[derive(Debug, Serialize, Clone)]
441#[serde(rename_all = "snake_case", tag = "type")]
442pub enum ToolChoiceSpecific {
443    /// Allowed tool configuration type. Always `allowed_tools`.
444    AllowedTools {
445        /// Constrains the tools available to the model to a pre-defined set.
446        allowed_tools: ToolChoiceAllowedTools,
447    },
448    /// For function calling, the type is always `function`.
449    Function { function: ToolChoiceFunction },
450    /// For custom tool calling, the type is always `custom`.
451    Custom { custom: ToolChoiceCustom },
452}
453
454#[derive(Debug, Serialize, Clone)]
455pub struct ToolChoiceAllowedTools {
456    /// Constrains the tools available to the model to a pre-defined set.
457    ///
458    /// - `auto` allows the model to pick from among the allowed tools and generate a
459    /// message.
460    /// - `required` requires the model to call one or more of the allowed tools.
461    pub mode: ToolChoiceAllowedToolsMode,
462    /// A list of tool definitions that the model should be allowed to call.
463    ///
464    /// For the Chat Completions API, the list of tool definitions might look like:
465    ///
466    /// ```json
467    /// [
468    ///   { "type": "function", "function": { "name": "get_weather" } },
469    ///   { "type": "function", "function": { "name": "get_time" } }
470    /// ]
471    /// ```
472    pub tools: serde_json::Map<String, serde_json::Value>,
473}
474
475/// The mode for allowed tools in tool choice.
476///
477/// Controls how the model should handle the set of allowed tools:
478///
479/// - `auto` allows the model to pick from among the allowed tools and generate a
480///   message.
481/// - `required` requires the model to call one or more of the allowed tools.
482#[derive(Debug, Serialize, Clone)]
483#[serde(rename_all = "lowercase")]
484pub enum ToolChoiceAllowedToolsMode {
485    /// The model can choose whether to use the allowed tools or not.
486    Auto,
487    /// The model must use at least one of the allowed tools.
488    Required,
489}
490
491#[derive(Debug, Serialize, Clone)]
492pub struct ToolChoiceFunction {
493    /// The name of the function to call.
494    pub name: String,
495}
496
497#[derive(Debug, Serialize, Clone)]
498pub struct ToolChoiceCustom {
499    /// The name of the custom tool to call.
500    pub name: String,
501}
502
503#[derive(Debug, Serialize, Clone)]
504pub struct ExtraBody {
505    /// Make sense only for Qwen API.
506    #[serde(skip_serializing_if = "Option::is_none")]
507    pub enable_thinking: Option<bool>,
508    /// Make sense only for Qwen API.
509    #[serde(skip_serializing_if = "Option::is_none")]
510    pub thinking_budget: Option<u32>,
511    ///The size of the candidate set for sampling during generation.
512    ///
513    /// Make sense only for Qwen API.
514    #[serde(skip_serializing_if = "Option::is_none")]
515    pub top_k: Option<u32>,
516}
517
518impl Post for RequestBody {
519    fn is_streaming(&self) -> bool {
520        self.stream
521    }
522}
523
524impl PostNoStream for RequestBody {
525    type Response = super::response::no_streaming::ChatCompletion;
526}
527
528impl PostStream for RequestBody {
529    type Response = super::response::streaming::ChatCompletionChunk;
530}
531
532#[cfg(test)]
533mod request_test {
534    use std::sync::LazyLock;
535
536    use futures_util::StreamExt;
537
538    use super::*;
539
540    const DEEPSEEK_API_KEY: LazyLock<&str> =
541        LazyLock::new(|| include_str!("../.././keys/deepseek_domestic_key").trim());
542    const DEEPSEEK_CHAT_URL: &'static str = "https://api.deepseek.com/chat/completions";
543    const DEEPSEEK_MODEL: &'static str = "deepseek-chat";
544
545    #[tokio::test]
546    async fn test_deepseek_no_stream() {
547        let request = RequestBody {
548            messages: vec![
549                Message::System {
550                    content: "This is a request of test purpose. Reply briefly".to_string(),
551                    name: None,
552                },
553                Message::User {
554                    content: "What's your name?".to_string(),
555                    name: None,
556                },
557            ],
558            model: DEEPSEEK_MODEL.to_string(),
559            stream: false,
560            ..Default::default()
561        };
562
563        let response = request
564            .get_response_string(DEEPSEEK_CHAT_URL, &*DEEPSEEK_API_KEY)
565            .await
566            .unwrap();
567
568        println!("{}", response);
569
570        assert!(response.to_ascii_lowercase().contains("deepseek"));
571    }
572
573    #[tokio::test]
574    async fn test_deepseek_stream() {
575        let request = RequestBody {
576            messages: vec![
577                Message::System {
578                    content: "This is a request of test purpose. Reply briefly".to_string(),
579                    name: None,
580                },
581                Message::User {
582                    content: "What's your name?".to_string(),
583                    name: None,
584                },
585            ],
586            model: DEEPSEEK_MODEL.to_string(),
587            stream: true,
588            ..Default::default()
589        };
590
591        let mut response = request
592            .get_stream_response_string(DEEPSEEK_CHAT_URL, *DEEPSEEK_API_KEY)
593            .await
594            .unwrap();
595
596        while let Some(chunk) = response.next().await {
597            println!("{}", chunk.unwrap());
598        }
599    }
600}