Skip to main content

just_deepseek/types/chat/
request.rs

1use serde::{Deserialize, Serialize};
2
3use super::{
4    ResponseFormat, StopSequence, StreamOptions, ToolChoice, ToolDefinition,
5    shared::ChatCompletionToolCall,
6};
7
8/// Wire DTO for `POST /chat/completions`.
9#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
10pub struct ChatCompletionRequest {
11    pub model: String,
12    pub messages: Vec<ChatMessage>,
13    #[serde(skip_serializing_if = "Option::is_none")]
14    pub thinking: Option<ThinkingConfig>,
15    #[serde(skip_serializing_if = "Option::is_none")]
16    pub max_tokens: Option<u32>,
17    #[serde(skip_serializing_if = "Option::is_none")]
18    pub response_format: Option<ResponseFormat>,
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub stop: Option<StopSequence>,
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub stream: Option<bool>,
23    #[serde(skip_serializing_if = "Option::is_none")]
24    pub stream_options: Option<StreamOptions>,
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub temperature: Option<f32>,
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub top_p: Option<f32>,
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub tools: Option<Vec<ToolDefinition>>,
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub tool_choice: Option<ToolChoice>,
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub logprobs: Option<bool>,
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub top_logprobs: Option<u8>,
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub reasoning_effort: Option<ReasoningEffort>,
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub user_id: Option<String>,
41}
42
43impl ChatCompletionRequest {
44    /// Creates a minimal request with common defaults.
45    pub fn new(model: impl Into<String>, messages: Vec<ChatMessage>) -> Self {
46        Self {
47            model: model.into(),
48            messages,
49            thinking: None,
50            max_tokens: None,
51            response_format: None,
52            stop: None,
53            stream: None,
54            stream_options: None,
55            temperature: None,
56            top_p: None,
57            tools: None,
58            tool_choice: None,
59            logprobs: None,
60            top_logprobs: None,
61            reasoning_effort: None,
62            user_id: None,
63        }
64    }
65}
66
67/// One request-side chat message in wire form.
68#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
69#[serde(untagged)]
70pub enum ChatMessage {
71    ToolCalls(ToolCallsMessage),
72    ToolResult(ToolResultMessage),
73    Message(TextMessage),
74}
75
76/// Plain role/content message.
77#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
78#[serde(deny_unknown_fields)]
79pub struct TextMessage {
80    /// Request-side roles remain free-form strings so callers can use DeepSeek-compatible
81    /// extensions such as `developer` without waiting for a curated enum surface.
82    pub role: String,
83    pub content: String,
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub name: Option<String>,
86    #[serde(skip_serializing_if = "Option::is_none")]
87    pub reasoning_content: Option<String>,
88}
89
90/// Assistant message that contains tool calls.
91#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
92#[serde(deny_unknown_fields)]
93pub struct ToolCallsMessage {
94    /// Outbound roles intentionally stay stringly typed for compatibility with provider-specific
95    /// role values layered on top of the shared chat format.
96    pub role: String,
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub content: Option<String>,
99    #[serde(skip_serializing_if = "Option::is_none")]
100    pub name: Option<String>,
101    pub tool_calls: Vec<ChatCompletionToolCall>,
102    #[serde(skip_serializing_if = "Option::is_none")]
103    pub reasoning_content: Option<String>,
104}
105
106/// Tool result message sent back to the model.
107#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
108#[serde(deny_unknown_fields)]
109pub struct ToolResultMessage {
110    /// Kept as a raw string for parity with the wire protocol and custom role extensions.
111    pub role: String,
112    pub content: String,
113    pub tool_call_id: String,
114}
115
116impl ChatMessage {
117    /// Creates a message with an explicit role string.
118    pub fn new(role: impl Into<String>, content: impl Into<String>) -> Self {
119        Self::Message(TextMessage {
120            role: role.into(),
121            content: content.into(),
122            name: None,
123            reasoning_content: None,
124        })
125    }
126
127    /// Creates a named message for providers that support the `name` field.
128    pub fn named(
129        role: impl Into<String>,
130        content: impl Into<String>,
131        name: impl Into<String>,
132    ) -> Self {
133        Self::Message(TextMessage {
134            role: role.into(),
135            content: content.into(),
136            name: Some(name.into()),
137            reasoning_content: None,
138        })
139    }
140
141    /// Creates a system message.
142    pub fn system(content: impl Into<String>) -> Self {
143        Self::new("system", content)
144    }
145
146    /// Creates a user message.
147    pub fn user(content: impl Into<String>) -> Self {
148        Self::new("user", content)
149    }
150
151    /// Creates an assistant message without tool calls.
152    pub fn assistant(content: impl Into<String>) -> Self {
153        Self::new("assistant", content)
154    }
155
156    /// Creates an assistant tool-call message without extra text content.
157    pub fn assistant_tool_calls(tool_calls: Vec<ChatCompletionToolCall>) -> Self {
158        Self::ToolCalls(ToolCallsMessage {
159            role: "assistant".to_owned(),
160            content: None,
161            name: None,
162            tool_calls,
163            reasoning_content: None,
164        })
165    }
166
167    /// Creates an assistant tool-call message with accompanying text content.
168    pub fn assistant_tool_calls_with_content(
169        content: impl Into<String>,
170        tool_calls: Vec<ChatCompletionToolCall>,
171    ) -> Self {
172        Self::ToolCalls(ToolCallsMessage {
173            role: "assistant".to_owned(),
174            content: Some(content.into()),
175            name: None,
176            tool_calls,
177            reasoning_content: None,
178        })
179    }
180
181    /// Creates a tool result message.
182    pub fn tool_result(content: impl Into<String>, tool_call_id: impl Into<String>) -> Self {
183        Self::ToolResult(ToolResultMessage {
184            role: "tool".to_owned(),
185            content: content.into(),
186            tool_call_id: tool_call_id.into(),
187        })
188    }
189
190    /// Alias for [`Self::tool_result`].
191    pub fn tool(content: impl Into<String>, tool_call_id: impl Into<String>) -> Self {
192        Self::tool_result(content, tool_call_id)
193    }
194}
195
196/// DeepSeek reasoning controls.
197#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
198pub struct ThinkingConfig {
199    #[serde(rename = "type")]
200    pub kind: ThinkingMode,
201}
202
203/// Whether reasoning is enabled for the request.
204#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
205#[serde(rename_all = "lowercase")]
206pub enum ThinkingMode {
207    Enabled,
208    Disabled,
209}
210
211/// Requested reasoning effort.
212#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
213#[serde(rename_all = "lowercase")]
214pub enum ReasoningEffort {
215    High,
216    Max,
217}