Skip to main content

deepseek_sdk/
lib.rs

1#![deny(unsafe_code)]
2
3//! DeepSeek API client for Rust.
4//!
5//! This crate provides:
6//! - Chat completions (`/chat/completions`)
7//! - FIM completions (beta, `/beta/completions`)
8//! - Responses API (OpenAI Responses format, `/responses`)
9//! - Model listing (`/models`)
10//! - Account balance (`/user/balance`)
11//! - Files API (`/files`) for uploading and managing images
12//!
13//! Streaming is supported in both async and blocking forms. The async API returns
14//! a `tokio::mpsc::Receiver`, while the blocking API returns an iterator that
15//! yields stream items.
16//!
17//! ```ignore
18//! use deepseek_sdk::chat::request::{ChatMessage, ChatRequestBuilder, Thinking};
19//! use deepseek_sdk::{DeepSeekClient, DeepSeekRequest, DEFAULT_BASE_URL};
20//!
21//! # async fn run() -> Result<(), Box<dyn std::error::Error>> {
22//! let req = ChatRequestBuilder::default()
23//!     .client(DeepSeekClient::new("sk-...", DEFAULT_BASE_URL.clone()))
24//!     .model("deepseek-v4-flash")
25//!     .message(ChatMessage::User { content: "Hi".into(), name: None })
26//!     .thinking(Thinking::disabled())
27//!     .build()?;
28//! let _resp = req.send().await?;
29//! # Ok(()) }
30//! ```
31/// Account balance (`/user/balance`).
32#[cfg(feature = "balance")]
33pub mod balance;
34/// Chat completions (`/chat/completions`).
35#[cfg(feature = "chat")]
36pub mod chat;
37/// Beta completions (FIM, beta chat).
38#[cfg(feature = "completion")]
39pub mod completion;
40pub mod error;
41/// Files API (`/files`) for uploading and managing images.
42#[cfg(feature = "files")]
43pub mod files;
44/// Model listing (`/models`).
45#[cfg(feature = "models")]
46pub mod models;
47/// Open AI Responses format
48#[cfg(feature = "responses")]
49pub mod responses;
50
51use crate::error::{ApiErrorEnvelope, DeepSeekError};
52
53use futures_util::StreamExt;
54use reqwest::{Client, Method, RequestBuilder, Response, header::AUTHORIZATION};
55use reqwest_eventsource::{Event, EventSource, RequestBuilderExt};
56use serde::{Serialize, de::DeserializeOwned};
57use std::future::Future;
58use std::sync::LazyLock;
59use tokio::sync::mpsc;
60
61/// Default base URL for stable API endpoints.
62pub static DEFAULT_BASE_URL: LazyLock<String> =
63    LazyLock::new(|| String::from("https://api.deepseek.com"));
64/// Default base URL for beta endpoints (e.g. FIM completion).
65pub static DEFAULT_BETA_BASE_URL: LazyLock<String> =
66    LazyLock::new(|| String::from("https://api.deepseek.com/beta"));
67
68/// API credentials for a DeepSeek endpoint.
69#[derive(Clone, Debug, Eq, PartialEq)]
70struct Credentials {
71    pub(crate) api_key: String,
72    pub(crate) base_url: String,
73}
74
75#[derive(Clone, Debug)]
76pub struct DeepSeekClient {
77    pub(crate) credentials: Credentials,
78    pub client: Client,
79}
80
81impl PartialEq for DeepSeekClient {
82    fn eq(&self, other: &Self) -> bool {
83        self.credentials == other.credentials
84    }
85}
86
87impl Eq for DeepSeekClient {}
88
89impl DeepSeekClient {
90    pub fn new(api_key: impl Into<String>, base_url: impl Into<String>) -> Self {
91        DeepSeekClient {
92            credentials: Credentials::new(api_key, base_url),
93            client: Client::new(),
94        }
95    }
96
97    pub fn with_client(mut self, client: Client) -> Self {
98        self.client = client;
99        self
100    }
101
102    pub fn with_credentials(
103        mut self,
104        api_key: impl Into<String>,
105        base_url: impl Into<String>,
106    ) -> Self {
107        self.credentials = Credentials::new(api_key, base_url);
108        self
109    }
110}
111
112impl Credentials {
113    /// Create credentials with an API key and base URL.
114    pub(crate) fn new(api_key: impl Into<String>, base_url: impl Into<String>) -> Self {
115        Credentials {
116            api_key: api_key.into(),
117            base_url: base_url.into(),
118        }
119    }
120}
121
122/// Unified request interface for DeepSeek endpoints.
123///
124/// Requests that support streaming should return stream items through `stream`
125/// and a blocking iterator through `stream_blocking`.
126pub trait DeepSeekRequest: Sized {
127    /// Full response type for non-streaming calls.
128    type Response;
129
130    /// Item type emitted by streaming calls.
131    type StreamItem;
132    /// Blocking stream iterator type.
133    type BlockingStream: Iterator<Item = Self::StreamItem>;
134
135    /// Send a non-streaming request.
136    fn send(self) -> impl Future<Output = Result<Self::Response, DeepSeekError>> + Send;
137    /// Send a streaming request (SSE), returning a receiver of stream items.
138    fn stream(
139        self,
140    ) -> impl Future<Output = Result<mpsc::Receiver<Self::StreamItem>, DeepSeekError>> + Send;
141    /// Send a streaming request but consume results via a blocking iterator.
142    fn stream_blocking(self) -> Result<Self::BlockingStream, DeepSeekError>;
143}
144
145#[allow(dead_code)]
146async fn api_request_json<F, T>(
147    method: Method,
148    route: &str,
149    builder: F,
150    deepseek_client: DeepSeekClient,
151) -> Result<T, DeepSeekError>
152where
153    F: FnOnce(RequestBuilder) -> RequestBuilder,
154    T: DeserializeOwned,
155{
156    let response = api_request(method, route, builder, deepseek_client).await?;
157    let status = response.status();
158
159    let text = response.text().await?;
160
161    if !status.is_success() {
162        if let Ok(envelope) = serde_json::from_str::<ApiErrorEnvelope>(&text) {
163            return Err(DeepSeekError::api(
164                envelope.error,
165                Some(status.as_u16()),
166                Some(text),
167            ));
168        }
169
170        return Err(DeepSeekError::http(status.as_u16(), text));
171    }
172
173    serde_json::from_str::<T>(&text).map_err(|err| DeepSeekError::decode(err.to_string(), text))
174}
175
176#[allow(dead_code)]
177async fn api_request<F>(
178    method: Method,
179    route: &str,
180    builder: F,
181    deepseek_client: DeepSeekClient,
182) -> Result<Response, DeepSeekError>
183where
184    F: FnOnce(RequestBuilder) -> RequestBuilder,
185{
186    let client = deepseek_client.client;
187    let mut request = client.request(
188        method,
189        format!("{}{route}", deepseek_client.credentials.base_url),
190    );
191    request = builder(request);
192    let response = request
193        .header(
194            AUTHORIZATION,
195            format!("Bearer {}", deepseek_client.credentials.api_key),
196        )
197        .send()
198        .await?;
199    Ok(response)
200}
201
202#[allow(dead_code)]
203async fn api_request_stream<F>(
204    method: Method,
205    route: &str,
206    builder: F,
207    deepseek_client: DeepSeekClient,
208) -> Result<EventSource, DeepSeekError>
209where
210    F: FnOnce(RequestBuilder) -> RequestBuilder,
211{
212    let mut request = deepseek_client.client.request(
213        method,
214        format!("{}{route}", deepseek_client.credentials.base_url),
215    );
216    request = builder(request);
217    let stream = request
218        .header(
219            AUTHORIZATION,
220            format!("Bearer {}", deepseek_client.credentials.api_key),
221        )
222        .eventsource()
223        .map_err(|err| DeepSeekError::decode(err.to_string(), String::new()))?;
224    Ok(stream)
225}
226
227/// Consume an SSE event source, mapping each `data:` payload through `parse`.
228///
229/// `Ok(Some(item))` forwards the item downstream, `Ok(None)` ends the stream
230/// cleanly, and `Err(err)` forwards the error and ends the stream. A clean EOF
231/// (which the Responses API uses instead of a `data: [DONE]` message) is treated
232/// as normal stream termination.
233#[allow(dead_code)]
234pub(crate) fn consume_sse<T, F>(
235    mut event_source: EventSource,
236    mut parse: F,
237) -> mpsc::Receiver<Result<T, DeepSeekError>>
238where
239    T: Send + 'static,
240    F: FnMut(String) -> Result<Option<T>, DeepSeekError> + Send + 'static,
241{
242    let (tx, rx) = mpsc::channel(32);
243
244    tokio::spawn(async move {
245        while let Some(event) = event_source.next().await {
246            match event {
247                Ok(Event::Open) => {}
248                Ok(Event::Message(message)) => {
249                    if message.data == "[DONE]" {
250                        break;
251                    }
252                    match parse(message.data) {
253                        Ok(Some(item)) => {
254                            if tx.send(Ok(item)).await.is_err() {
255                                break;
256                            }
257                        }
258                        Ok(None) => break,
259                        Err(err) => {
260                            let _ = tx.send(Err(err)).await;
261                            break;
262                        }
263                    }
264                }
265                Err(err) => {
266                    if matches!(err, reqwest_eventsource::Error::StreamEnded) {
267                        break;
268                    }
269                    let _ = tx
270                        .send(Err(DeepSeekError::decode(err.to_string(), String::new())))
271                        .await;
272                    break;
273                }
274            }
275        }
276    });
277
278    rx
279}
280
281/// Drive `fut` to a stream receiver on a dedicated std thread with its own Tokio
282/// runtime, forwarding items to a blocking `std::sync::mpsc` receiver.
283///
284/// `T` is the success type of a stream item; the forwarded items are
285/// `Result<T, DeepSeekError>`.
286#[allow(dead_code)]
287pub(crate) fn spawn_blocking_stream<T>(
288    fut: impl Future<Output = Result<mpsc::Receiver<Result<T, DeepSeekError>>, DeepSeekError>>
289    + Send
290    + 'static,
291) -> Result<std::sync::mpsc::Receiver<Result<T, DeepSeekError>>, DeepSeekError>
292where
293    T: Send + 'static,
294{
295    let (tx, rx) = std::sync::mpsc::channel();
296
297    std::thread::spawn(move || {
298        let runtime = match tokio::runtime::Builder::new_current_thread()
299            .enable_all()
300            .build()
301        {
302            Ok(runtime) => runtime,
303            Err(err) => {
304                let _ = tx.send(Err(DeepSeekError::decode(err.to_string(), String::new())));
305                return;
306            }
307        };
308
309        runtime.block_on(async move {
310            match fut.await {
311                Ok(mut stream_rx) => {
312                    while let Some(item) = stream_rx.recv().await {
313                        if tx.send(item).is_err() {
314                            break;
315                        }
316                    }
317                }
318                Err(err) => {
319                    let _ = tx.send(Err(err));
320                }
321            }
322        });
323    });
324
325    Ok(rx)
326}
327
328/// Send a GET request and decode the JSON response.
329#[allow(dead_code)]
330async fn api_get<T>(route: &str, client: DeepSeekClient) -> Result<T, DeepSeekError>
331where
332    T: DeserializeOwned,
333{
334    api_request_json(Method::GET, route, |request| request, client).await
335}
336
337/// Send a POST request with JSON body and decode the response.
338#[allow(dead_code)]
339async fn api_post<J, T>(route: &str, json: &J, client: DeepSeekClient) -> Result<T, DeepSeekError>
340where
341    J: Serialize + ?Sized,
342    T: DeserializeOwned,
343{
344    api_request_json(Method::POST, route, |request| request.json(json), client).await
345}
346
347/// Send a DELETE request and decode the JSON response.
348#[allow(dead_code)]
349async fn api_delete<T>(route: &str, client: DeepSeekClient) -> Result<T, DeepSeekError>
350where
351    T: DeserializeOwned,
352{
353    api_request_json(Method::DELETE, route, |request| request, client).await
354}
355
356/// Send a POST request with multipart form body and decode the JSON response.
357#[allow(dead_code)]
358async fn api_post_multipart<T>(
359    route: &str,
360    form: reqwest::multipart::Form,
361    client: DeepSeekClient,
362) -> Result<T, DeepSeekError>
363where
364    T: DeserializeOwned,
365{
366    api_request_json(
367        Method::POST,
368        route,
369        |request| request.multipart(form),
370        client,
371    )
372    .await
373}