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