Skip to main content

deepseek/
client.rs

1use async_trait::async_trait;
2use crate::error::Result;
3use crate::types::*;
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(&self, url: &str, bearer_token: &str, body: &ChatRequest) -> Result<ChatResponse>;
14}
15
16/// Generic DeepSeek client over any transport.
17pub struct DeepSeekClient<H: HttpClient> {
18    pub http: H,
19    pub api_key: String,
20    pub base_url: String,
21}
22
23impl<H: HttpClient + Clone> Clone for DeepSeekClient<H> {
24    fn clone(&self) -> Self {
25        Self {
26            http: self.http.clone(),
27            api_key: self.api_key.clone(),
28            base_url: self.base_url.clone(),
29        }
30    }
31}
32
33impl<H: HttpClient> DeepSeekClient<H> {
34    pub fn new(http: H, api_key: impl Into<String>) -> Self {
35        Self {
36            http,
37            api_key: api_key.into(),
38            base_url: DEFAULT_BASE_URL.to_string(),
39        }
40    }
41
42    pub fn with_base_url(mut self, url: impl Into<String>) -> Self {
43        self.base_url = url.into();
44        self
45    }
46
47    /// Send a chat completion request.
48    pub async fn chat(&self, request: &ChatRequest) -> Result<ChatResponse> {
49        let url = format!("{}/chat/completions", self.base_url);
50        self.http.post_json(&url, &self.api_key, request).await
51    }
52
53}
54
55/// Build a ChatRequest with optional tool schemas.
56/// Free function — usable without a client instance.
57pub fn build_request(
58    model: &DeepSeekModel,
59    messages: Vec<ChatMessage>,
60    tools: Option<Vec<ToolSchema>>,
61    effort: &EffortLevel,
62) -> ChatRequest {
63    let has_tools = tools.is_some();
64    let reasoning_effort = match effort {
65        EffortLevel::Max => Some("max".to_string()),
66        _ => Some("high".to_string()),
67    };
68    ChatRequest {
69        model: model.as_str().to_string(),
70        messages,
71        tools,
72        tool_choice: if has_tools { Some(serde_json::json!("auto")) } else { None },
73        temperature: Some(effort.temperature()),
74        max_tokens: Some(effort.max_tokens()),
75        stream: Some(false),
76        reasoning_effort,
77        thinking: Some(serde_json::json!({"type": "enabled"})),
78    }
79}