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#[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
16pub 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 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
55pub 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}