langchain_rust/llm/deepseek/
client.rs1use crate::{
2 language_models::{llm::LLM, options::CallOptions, GenerateResult, LLMError, TokenUsage},
3 llm::DeepseekError,
4 schemas::{Message, StreamData},
5};
6use async_trait::async_trait;
7use futures::{Stream, StreamExt};
8use reqwest::Client;
9use std::pin::Pin;
10
11use super::models::{ApiResponse, DeepseekMessage, Payload, ResponseFormat};
12use crate::llm::sse::{sse_json_events, usage_from_chunk};
13
14pub enum DeepseekModel {
15 DeepseekChat,
16 DeepseekReasoner,
17}
18
19impl ToString for DeepseekModel {
20 fn to_string(&self) -> String {
21 match self {
22 DeepseekModel::DeepseekChat => "deepseek-chat".to_string(),
23 DeepseekModel::DeepseekReasoner => "deepseek-reasoner".to_string(),
24 }
25 }
26}
27
28#[derive(Clone)]
29pub struct Deepseek {
30 model: String,
31 options: CallOptions,
32 api_key: String,
33 base_url: String,
34 json_mode: bool,
35 include_reasoning: bool,
36}
37
38impl Default for Deepseek {
39 fn default() -> Self {
40 Self::new()
41 }
42}
43
44impl Deepseek {
45 pub fn new() -> Self {
46 Self {
47 model: DeepseekModel::DeepseekChat.to_string(),
48 options: CallOptions::default(),
49 api_key: std::env::var("DEEPSEEK_API_KEY").unwrap_or_default(),
50 base_url: "https://api.deepseek.com".to_string(),
51 json_mode: false,
52 include_reasoning: false,
53 }
54 }
55
56 pub fn with_model<S: Into<String>>(mut self, model: S) -> Self {
57 self.model = model.into();
58 self
59 }
60
61 pub fn with_options(mut self, options: CallOptions) -> Self {
62 self.options = options;
63 self
64 }
65
66 pub fn with_api_key<S: Into<String>>(mut self, api_key: S) -> Self {
67 self.api_key = api_key.into();
68 self
69 }
70
71 pub fn with_base_url<S: Into<String>>(mut self, base_url: S) -> Self {
72 self.base_url = base_url.into();
73 self
74 }
75
76 pub fn with_json_mode(mut self, json_mode: bool) -> Self {
77 self.json_mode = json_mode;
78 self
79 }
80
81 pub fn with_include_reasoning(mut self, include_reasoning: bool) -> Self {
82 self.include_reasoning = include_reasoning;
83 self
84 }
85
86 async fn generate(&self, messages: &[Message]) -> Result<GenerateResult, LLMError> {
87 let client = Client::new();
88 let is_stream = self.options.streaming_func.is_some();
89
90 let payload = self.build_payload(messages, is_stream);
91 let res = client
92 .post(&format!("{}/v1/chat/completions", self.base_url))
93 .header("Authorization", format!("Bearer {}", self.api_key))
94 .header("Content-Type", "application/json")
95 .json(&payload)
96 .send()
97 .await?;
98
99 let status = res.status().as_u16();
100
101 let res = match status {
102 400 => Err(LLMError::DeepseekError(DeepseekError::InvalidFormatError(
103 "Invalid request format".to_string(),
104 ))),
105 401 => Err(LLMError::DeepseekError(DeepseekError::AuthenticationError(
106 "Invalid API Key".to_string(),
107 ))),
108 402 => Err(LLMError::DeepseekError(
109 DeepseekError::InsufficientBalanceError("Insufficient balance".to_string()),
110 )),
111 422 => Err(LLMError::DeepseekError(
112 DeepseekError::InvalidParametersError("Invalid parameters".to_string()),
113 )),
114 429 => Err(LLMError::DeepseekError(DeepseekError::RateLimitError(
115 "Rate limit reached".to_string(),
116 ))),
117 500 => Err(LLMError::DeepseekError(DeepseekError::ServerError(
118 "Server error".to_string(),
119 ))),
120 503 => Err(LLMError::DeepseekError(
121 DeepseekError::ServerOverloadedError("Server overloaded".to_string()),
122 )),
123 _ => Ok(res.json::<ApiResponse>().await?),
124 }?;
125
126 let choice = res.choices.first();
127
128 let mut generation = choice
129 .map(|c| c.message.content.clone())
130 .unwrap_or_default();
131
132 if self.include_reasoning && self.model == DeepseekModel::DeepseekReasoner.to_string() {
135 if let Some(reasoning) = choice.and_then(|c| c.message.reasoning_content.clone()) {
136 generation = format!("Reasoning:\n{}\n\nAnswer:\n{}", reasoning, generation);
137 }
138 }
139
140 let tokens = Some(TokenUsage {
141 prompt_tokens: res.usage.prompt_tokens,
142 completion_tokens: res.usage.completion_tokens,
143 total_tokens: res.usage.total_tokens,
144 });
145
146 Ok(GenerateResult { tokens, generation })
147 }
148
149 fn build_payload(&self, messages: &[Message], stream: bool) -> Payload {
150 let mut response_format = None;
151 if self.json_mode {
152 response_format = Some(ResponseFormat {
153 format_type: "json_object".to_string(),
154 });
155 }
156
157 let mut payload = Payload {
158 model: self.model.clone(),
159 messages: messages
160 .iter()
161 .map(DeepseekMessage::from_message)
162 .collect::<Vec<_>>(),
163 max_tokens: self.options.max_tokens,
164 stream: None,
165 temperature: self.options.temperature,
166 top_p: self.options.top_p,
167 frequency_penalty: None,
168 presence_penalty: None,
169 stop: self.options.stop_words.clone(),
170 response_format,
171 };
172
173 if stream {
174 payload.stream = Some(true);
175 }
176
177 if let Some(fp) = self.options.frequency_penalty {
179 if fp >= -2.0 && fp <= 2.0 {
180 payload.frequency_penalty = Some(fp);
181 }
182 }
183
184 if let Some(pp) = self.options.presence_penalty {
186 if pp >= -2.0 && pp <= 2.0 {
187 payload.presence_penalty = Some(pp);
188 }
189 }
190
191 payload
192 }
193}
194
195#[async_trait]
196impl LLM for Deepseek {
197 async fn generate(&self, messages: &[Message]) -> Result<GenerateResult, LLMError> {
198 match &self.options.streaming_func {
199 Some(func) => {
200 let mut complete_response = String::new();
201 let mut stream = self.stream(messages).await?;
202 while let Some(data) = stream.next().await {
203 match data {
204 Ok(value) => {
205 let mut func = func.lock().await;
206 complete_response.push_str(&value.content);
207 let _ = func(value.content).await;
208 }
209 Err(e) => return Err(e),
210 }
211 }
212 let mut generate_result = GenerateResult::default();
213 generate_result.generation = complete_response;
214 Ok(generate_result)
215 }
216 None => self.generate(messages).await,
217 }
218 }
219
220 async fn stream(
221 &self,
222 messages: &[Message],
223 ) -> Result<Pin<Box<dyn Stream<Item = Result<StreamData, LLMError>> + Send>>, LLMError> {
224 let client = Client::new();
225 let payload = self.build_payload(messages, true);
226 let request = client
227 .post(&format!("{}/v1/chat/completions", self.base_url))
228 .header("Authorization", format!("Bearer {}", self.api_key))
229 .header("Content-Type", "application/json")
230 .json(&payload)
231 .build()?;
232
233 let stream = client.execute(request).await?;
234 let stream = stream.bytes_stream();
235
236 let include_reasoning = self.include_reasoning;
237 let is_reasoner = self.model == DeepseekModel::DeepseekReasoner.to_string();
238
239 let processed_stream = sse_json_events(stream).filter_map(move |event| async move {
240 let chunk = match event {
241 Ok(chunk) => chunk,
242 Err(e) => return Some(Err(e)),
243 };
244 let delta = chunk.get("choices")?.as_array()?.first()?.get("delta")?;
245 let usage = usage_from_chunk(&chunk);
246 if include_reasoning && is_reasoner {
247 if let Some(reasoning) = delta
248 .get("reasoning_content")
249 .and_then(|c| c.as_str())
250 .filter(|r| !r.is_empty())
251 {
252 let content = format!("Reasoning: {}", reasoning);
253 return Some(Ok(StreamData::new(chunk.clone(), usage, content)));
254 }
255 }
256 let content = delta
257 .get("content")
258 .and_then(|c| c.as_str())
259 .filter(|c| !c.is_empty())?
260 .to_string();
261 Some(Ok(StreamData::new(chunk, usage, content)))
262 });
263
264 Ok(Box::pin(processed_stream))
265 }
266
267 fn add_options(&mut self, options: CallOptions) {
268 self.options = options;
269 }
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275 use crate::schemas::{Message, MessageType};
276
277 #[tokio::test]
278 #[ignore]
279 async fn test_deepseek_generate() {
280 let messages = vec![Message {
281 content: "Hello".to_string(),
282 message_type: MessageType::HumanMessage,
283 id: Some("test_id".to_string()),
284 images: None,
285 tool_calls: None,
286 }];
287
288 let client = Deepseek::new();
289 let res = client.generate(&messages).await;
290 assert!(res.is_ok());
291 }
292
293 #[tokio::test]
294 #[ignore]
295 async fn test_deepseek_stream() {
296 let messages = vec![Message {
297 content: "Hello".to_string(),
298 message_type: MessageType::HumanMessage,
299 id: Some("test_id".to_string()),
300 images: None,
301 tool_calls: None,
302 }];
303
304 let client = Deepseek::new();
305 let res = client.stream(&messages).await;
306 assert!(res.is_ok());
307 }
308
309 #[tokio::test]
310 #[ignore]
311 async fn test_deepseek_reasoner() {
312 let messages = vec![Message {
313 content: "9.11 and 9.8, which is greater?".to_string(),
314 message_type: MessageType::HumanMessage,
315 id: Some("test_id".to_string()),
316 images: None,
317 tool_calls: None,
318 }];
319
320 let client = Deepseek::new()
322 .with_model(DeepseekModel::DeepseekReasoner.to_string())
323 .with_include_reasoning(true);
324
325 let res = client.generate(&messages).await;
326 assert!(res.is_ok());
327
328 if let Ok(result) = res {
330 println!("Generation result: {}", result.generation);
331 }
332 }
333}