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}