Skip to main content

just_deepseek/
client.rs

1//! DeepSeek API client.
2
3use just_common::transport::http::{endpoint_url, ensure_success, parse_json};
4use reqwest::header::CONTENT_TYPE;
5
6use crate::{
7    Error,
8    types::{
9        balance::GetUserBalanceResponse,
10        chat::{ChatCompletion, ChatCompletionRequest},
11        models::ListModelsResponse,
12    },
13};
14
15/// Async DeepSeek API client.
16///
17/// Holds a pre-configured `reqwest::Client` and base URL. Construct via
18/// [`DeepSeekClient::builder()`] or [`DeepSeekClient::new()`].
19#[derive(Clone, Debug)]
20pub struct DeepSeekClient {
21    http: reqwest::Client,
22    base_url: String,
23}
24
25impl DeepSeekClient {
26    // --- construction, accessors, and the prepare/send (raw HTTP) surface ---
27
28    /// Creates a new client from pre-built components.
29    ///
30    /// The HTTP client should already have auth headers set (e.g. via
31    /// [`just_common::transport::http::build_client`]).
32    pub fn new(http: reqwest::Client, base_url: String) -> Self {
33        Self { http, base_url }
34    }
35
36    /// Returns a builder for constructing a new client.
37    pub fn builder() -> crate::client_builder::DeepSeekClientBuilder {
38        crate::client_builder::DeepSeekClientBuilder::new()
39    }
40
41    /// Returns the underlying HTTP client.
42    pub fn http_client(&self) -> &reqwest::Client {
43        &self.http
44    }
45
46    /// Returns the configured base URL.
47    pub fn base_url(&self) -> &str {
48        &self.base_url
49    }
50
51    /// Prepares a non-streaming chat completion request for later execution.
52    ///
53    /// Serializes the request body and builds a complete `reqwest::Request`.
54    /// This is a synchronous operation (no IO).
55    pub fn prepare(&self, request: ChatCompletionRequest) -> Result<reqwest::Request, Error> {
56        if request.stream.unwrap_or(false) {
57            return Err(Error::InvalidRequest(
58                "stream=true is not supported by prepare; use prepare_streaming instead".into(),
59            ));
60        }
61        self.build_request(request, "/chat/completions")
62    }
63
64    /// Prepares a streaming chat completion request for later execution.
65    ///
66    /// Forces `stream = true` on the request, then serializes and builds.
67    /// This is a synchronous operation (no IO).
68    pub fn prepare_streaming(
69        &self,
70        mut request: ChatCompletionRequest,
71    ) -> Result<reqwest::Request, Error> {
72        request.stream = Some(true);
73        self.build_request(request, "/chat/completions")
74    }
75
76    /// Sends a prepared request and returns the raw HTTP response without checking status.
77    ///
78    /// Callers must handle non-success statuses themselves. For automatic status checking and
79    /// deserialization, use [`chat_completion`](Self::chat_completion) or
80    /// [`stream_chat_completion`](Self::stream_chat_completion).
81    pub async fn send(&self, request: reqwest::Request) -> Result<reqwest::Response, Error> {
82        self.http
83            .execute(request)
84            .await
85            .map_err(just_common::error::TransportError::Transport)
86            .map_err(Error::from)
87    }
88
89    /// Builds a `reqwest::Request` from a serializable body and endpoint path.
90    fn build_request(
91        &self,
92        body: impl serde::Serialize,
93        path: &str,
94    ) -> Result<reqwest::Request, Error> {
95        let url = endpoint_url(&self.base_url, path)?;
96        let payload = serde_json::to_vec(&body)?;
97        self.http
98            .post(url)
99            .header(CONTENT_TYPE, "application/json")
100            .body(payload)
101            .build()
102            .map_err(just_common::error::TransportError::Transport)
103            .map_err(Error::from)
104    }
105}
106
107impl DeepSeekClient {
108    // --- typed operations (hide HTTP entirely) ---
109
110    /// Parses a raw HTTP response into a provider-native non-streaming completion.
111    ///
112    /// Performs HTTP status checking and JSON deserialization. Combine with
113    /// [`prepare`](Self::prepare) and [`send`](Self::send) for full control over the request
114    /// lifecycle, e.g. to inspect response headers before consuming the body.
115    pub async fn parse(&self, response: reqwest::Response) -> Result<ChatCompletion, Error> {
116        parse_json(response).await
117    }
118
119    /// Parses a raw HTTP response into a provider-native streaming chunk stream.
120    ///
121    /// Checks the HTTP status first (the SSE stream parser assumes a 2xx event stream).
122    pub async fn parse_streaming(
123        &self,
124        response: reqwest::Response,
125    ) -> Result<crate::ChatCompletionStream, Error> {
126        let response = ensure_success(response).await?;
127        crate::ChatCompletionStream::from_response(response).map_err(Error::Transport)
128    }
129
130    /// Executes a non-streaming chat completion request.
131    pub async fn chat_completion(
132        &self,
133        request: ChatCompletionRequest,
134    ) -> Result<ChatCompletion, Error> {
135        let response = self.send(self.prepare(request)?).await?;
136        self.parse(response).await
137    }
138
139    /// Starts a streaming chat completion request.
140    pub async fn stream_chat_completion(
141        &self,
142        request: ChatCompletionRequest,
143    ) -> Result<crate::ChatCompletionStream, Error> {
144        let response = self.send(self.prepare_streaming(request)?).await?;
145        self.parse_streaming(response).await
146    }
147
148    /// Lists models currently exposed by the configured endpoint.
149    pub async fn list_models(&self) -> Result<ListModelsResponse, Error> {
150        let url = endpoint_url(&self.base_url, "/models")?;
151        let response = self
152            .http
153            .get(url)
154            .send()
155            .await
156            .map_err(just_common::error::TransportError::Transport)?;
157        parse_json(response).await
158    }
159
160    /// Returns the current user balance state.
161    pub async fn get_user_balance(&self) -> Result<GetUserBalanceResponse, Error> {
162        let url = endpoint_url(&self.base_url, "/user/balance")?;
163        let response = self
164            .http
165            .get(url)
166            .send()
167            .await
168            .map_err(just_common::error::TransportError::Transport)?;
169        parse_json(response).await
170    }
171}