Skip to main content

deepseek_sdk/
lib.rs

1//! DeepSeek API client for Rust.
2//!
3//! This crate provides:
4//! - Chat completions (`/chat/completions`)
5//! - FIM completions (beta, `/beta/completions`)
6//! - Model listing (`/models`)
7//! - Account balance (`/user/balance`)
8//!
9//! Streaming is supported in both async and blocking forms. The async API returns
10//! a `tokio::mpsc::Receiver`, while the blocking API returns an iterator that
11//! yields stream items.
12//!
13//! ```no_run
14//! use deepseek_sdk::chat::request::{ChatMessage, ChatRequestBuilder, Thinking};
15//! use deepseek_sdk::{DeepSeekClient, DeepSeekRequest, DEFAULT_BASE_URL};
16//!
17//! # async fn run() -> Result<(), Box<dyn std::error::Error>> {
18//! let req = ChatRequestBuilder::default()
19//!     .client(DeepSeekClient::new("sk-...", DEFAULT_BASE_URL.clone()))
20//!     .model("deepseek-v4-flash")
21//!     .message(ChatMessage::User { content: "Hi".into(), name: None })
22//!     .thinking(Thinking::disabled())
23//!     .build()?;
24//! let _resp = req.send().await?;
25//! # Ok(()) }
26//! ```
27pub mod balance;
28pub mod chat;
29pub mod completion;
30pub mod error;
31pub mod models;
32
33use crate::error::{ApiErrorEnvelope, DeepSeekError};
34
35use reqwest::{Client, Method, RequestBuilder, Response, header::AUTHORIZATION};
36use reqwest_eventsource::{EventSource, RequestBuilderExt};
37use serde::{Serialize, de::DeserializeOwned};
38use std::future::Future;
39use std::sync::LazyLock;
40use tokio::sync::mpsc;
41
42/// Default base URL for stable API endpoints.
43pub static DEFAULT_BASE_URL: LazyLock<String> =
44    LazyLock::new(|| String::from("https://api.deepseek.com"));
45/// Default base URL for beta endpoints (e.g. FIM completion).
46pub static DEFAULT_BETA_BASE_URL: LazyLock<String> =
47    LazyLock::new(|| String::from("https://api.deepseek.com/beta"));
48
49/// API credentials for a DeepSeek endpoint.
50#[derive(Clone, Debug, Eq, PartialEq)]
51struct Credentials {
52    pub(crate) api_key: String,
53    pub(crate) base_url: String,
54}
55
56#[derive(Clone, Debug)]
57pub struct DeepSeekClient {
58    pub(crate) credentials: Credentials,
59    pub client: Client,
60}
61
62impl PartialEq for DeepSeekClient {
63    fn eq(&self, other: &Self) -> bool {
64        self.credentials == other.credentials
65    }
66}
67
68impl Eq for DeepSeekClient {}
69
70impl DeepSeekClient {
71    pub fn new(api_key: impl Into<String>, base_url: impl Into<String>) -> Self {
72        DeepSeekClient {
73            credentials: Credentials::new(api_key, base_url),
74            client: Client::new(),
75        }
76    }
77
78    pub fn with_client(mut self, client: Client) -> Self {
79        self.client = client;
80        self
81    }
82
83    pub fn with_credentials(
84        mut self,
85        api_key: impl Into<String>,
86        base_url: impl Into<String>,
87    ) -> Self {
88        self.credentials = Credentials::new(api_key, base_url);
89        self
90    }
91}
92
93impl Credentials {
94    /// Create credentials with an API key and base URL.
95    pub(crate) fn new(api_key: impl Into<String>, base_url: impl Into<String>) -> Self {
96        Credentials {
97            api_key: api_key.into(),
98            base_url: base_url.into(),
99        }
100    }
101}
102
103/// Unified request interface for DeepSeek endpoints.
104///
105/// Requests that support streaming should return stream items through `stream`
106/// and a blocking iterator through `stream_blocking`.
107pub trait DeepSeekRequest: Sized {
108    /// Full response type for non-streaming calls.
109    type Response;
110
111    /// Item type emitted by streaming calls.
112    type StreamItem;
113    /// Blocking stream iterator type.
114    type BlockingStream: Iterator<Item = Self::StreamItem>;
115
116    /// Send a non-streaming request.
117    fn send(self) -> impl Future<Output = Result<Self::Response, DeepSeekError>> + Send;
118    /// Send a streaming request (SSE), returning a receiver of stream items.
119    fn stream(
120        self,
121    ) -> impl Future<Output = Result<mpsc::Receiver<Self::StreamItem>, DeepSeekError>> + Send;
122    /// Send a streaming request but consume results via a blocking iterator.
123    fn stream_blocking(self) -> Result<Self::BlockingStream, DeepSeekError>;
124}
125
126async fn api_request_json<F, T>(
127    method: Method,
128    route: &str,
129    builder: F,
130    deepseek_client: DeepSeekClient,
131) -> Result<T, DeepSeekError>
132where
133    F: FnOnce(RequestBuilder) -> RequestBuilder,
134    T: DeserializeOwned,
135{
136    let response = api_request(method, route, builder, deepseek_client).await?;
137    let status = response.status();
138
139    let text = response.text().await?;
140
141    if !status.is_success() {
142        if let Ok(envelope) = serde_json::from_str::<ApiErrorEnvelope>(&text) {
143            return Err(DeepSeekError::api(
144                envelope.error,
145                Some(status.as_u16()),
146                Some(text),
147            ));
148        }
149
150        return Err(DeepSeekError::http(status.as_u16(), text));
151    }
152
153    serde_json::from_str::<T>(&text).map_err(|err| DeepSeekError::decode(err.to_string(), text))
154}
155
156async fn api_request<F>(
157    method: Method,
158    route: &str,
159    builder: F,
160    deepseek_client: DeepSeekClient,
161) -> Result<Response, DeepSeekError>
162where
163    F: FnOnce(RequestBuilder) -> RequestBuilder,
164{
165    let client = deepseek_client.client;
166    let mut request = client.request(
167        method,
168        format!("{}{route}", deepseek_client.credentials.base_url),
169    );
170    request = builder(request);
171    let response = request
172        .header(
173            AUTHORIZATION,
174            format!("Bearer {}", deepseek_client.credentials.api_key),
175        )
176        .send()
177        .await?;
178    Ok(response)
179}
180
181async fn api_request_stream<F>(
182    method: Method,
183    route: &str,
184    builder: F,
185    deepseek_client: DeepSeekClient,
186) -> Result<EventSource, DeepSeekError>
187where
188    F: FnOnce(RequestBuilder) -> RequestBuilder,
189{
190    let mut request = deepseek_client.client.request(
191        method,
192        format!("{}{route}", deepseek_client.credentials.base_url),
193    );
194    request = builder(request);
195    let stream = request
196        .header(
197            AUTHORIZATION,
198            format!("Bearer {}", deepseek_client.credentials.api_key),
199        )
200        .eventsource()
201        .map_err(|err| DeepSeekError::decode(err.to_string(), String::new()))?;
202    Ok(stream)
203}
204
205async fn api_get<T>(route: &str, client: DeepSeekClient) -> Result<T, DeepSeekError>
206where
207    T: DeserializeOwned,
208{
209    api_request_json(Method::GET, route, |request| request, client).await
210}
211
212async fn api_post<J, T>(route: &str, json: &J, client: DeepSeekClient) -> Result<T, DeepSeekError>
213where
214    J: Serialize + ?Sized,
215    T: DeserializeOwned,
216{
217    api_request_json(Method::POST, route, |request| request.json(json), client).await
218}