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