Skip to main content

deepseek/
client.rs

1use crate::error::Result;
2use crate::types::*;
3use async_trait::async_trait;
4
5pub const DEFAULT_BASE_URL: &str = "https://api.deepseek.com/v1";
6
7/// Transport abstraction — reqwest for native, worker::Fetch for WASM.
8///
9/// Native builds require `Send`; WASM builds relax it via `?Send`.
10#[cfg_attr(not(feature = "wasm"), async_trait)]
11#[cfg_attr(feature = "wasm", async_trait(?Send))]
12pub trait HttpClient {
13    async fn post_json(
14        &self,
15        url: &str,
16        bearer_token: &str,
17        body: &ChatRequest,
18    ) -> Result<ChatResponse>;
19}
20
21/// Generic DeepSeek client over any transport.
22pub struct DeepSeekClient<H: HttpClient> {
23    pub http: H,
24    pub api_key: String,
25    pub base_url: String,
26}
27
28impl<H: HttpClient + Clone> Clone for DeepSeekClient<H> {
29    fn clone(&self) -> Self {
30        Self {
31            http: self.http.clone(),
32            api_key: self.api_key.clone(),
33            base_url: self.base_url.clone(),
34        }
35    }
36}
37
38impl<H: HttpClient> DeepSeekClient<H> {
39    pub fn new(http: H, api_key: impl Into<String>) -> Self {
40        Self {
41            http,
42            api_key: api_key.into(),
43            base_url: DEFAULT_BASE_URL.to_string(),
44        }
45    }
46
47    pub fn with_base_url(mut self, url: impl Into<String>) -> Self {
48        self.base_url = url.into();
49        self
50    }
51
52    /// Send a chat completion request.
53    pub async fn chat(&self, request: &ChatRequest) -> Result<ChatResponse> {
54        let url = format!("{}/chat/completions", self.base_url);
55        self.http.post_json(&url, &self.api_key, request).await
56    }
57}
58
59/// Build a ChatRequest with optional tool schemas.
60/// Free function — usable without a client instance.
61pub fn build_request(
62    model: &DeepSeekModel,
63    messages: Vec<ChatMessage>,
64    tools: Option<Vec<ToolSchema>>,
65    effort: &EffortLevel,
66) -> ChatRequest {
67    let has_tools = tools.is_some();
68    let reasoning_effort = match effort {
69        EffortLevel::Max => Some("max".to_string()),
70        _ => Some("high".to_string()),
71    };
72    ChatRequest {
73        model: model.as_str().to_string(),
74        messages,
75        tools,
76        tool_choice: if has_tools {
77            Some(serde_json::json!("auto"))
78        } else {
79            None
80        },
81        temperature: Some(effort.temperature()),
82        max_tokens: Some(effort.max_tokens()),
83        stream: Some(false),
84        reasoning_effort,
85        thinking: Some(serde_json::json!({"type": "enabled"})),
86    }
87}