deepseek_rust_cli/api/
client.rs1use std::{sync::Arc, time::Duration};
2
3use anyhow::Result;
4use reqwest::{Client, Response};
5
6use crate::api::types::{ChatRequest, Message, ResponseFormat, ThinkingConfig, Tool};
7
8pub struct DeepSeekClient {
10 client: Client,
11 api_key: Arc<str>,
12 base_url: Arc<str>,
13}
14
15impl DeepSeekClient {
16 pub fn new(
17 api_key: String,
18 base_url: String,
19 timeout_secs: u64,
20 proxy_url: Option<String>,
21 proxy_username: Option<String>,
22 proxy_password: Option<String>,
23 danger_accept_invalid_certs: bool,
24 ) -> Self {
25 let mut builder = Client::builder()
26 .timeout(Duration::from_secs(timeout_secs))
27 .pool_idle_timeout(Some(Duration::from_secs(90)))
29 .pool_max_idle_per_host(4)
30 .tcp_keepalive(Some(Duration::from_secs(60)))
32 .http2_adaptive_window(true)
34 .gzip(true)
36 .brotli(true)
37 .zstd(true)
38 .connect_timeout(Duration::from_secs(10))
40 .user_agent(concat!(
42 "deepseek-rust-cli/",
43 env!("CARGO_PKG_VERSION")
44 ));
45
46 if danger_accept_invalid_certs {
48 tracing::warn!(
49 "DANGER: Accepting invalid TLS certificates (--danger-accept-invalid-certs)"
50 );
51 builder = builder.danger_accept_invalid_certs(true);
52 }
53
54 if let Some(ref proxy_url) = proxy_url {
56 let mut proxy = match reqwest::Proxy::all(proxy_url) {
57 Ok(p) => p,
58 Err(e) => {
59 tracing::warn!("Invalid proxy URL '{}': {}", proxy_url, e);
60 reqwest::Proxy::custom(|_url| None::<reqwest::Url>)
61 }
62 };
63
64 if let (Some(user), Some(pass)) = (&proxy_username, &proxy_password) {
65 proxy = proxy.basic_auth(user, pass);
66 }
67
68 tracing::info!("Using proxy: {}", proxy_url);
69 builder = builder.proxy(proxy);
70 }
71
72 let client = builder.build().unwrap_or_else(|_| Client::new());
73
74 Self {
75 client,
76 api_key: Arc::from(api_key),
77 base_url: Arc::from(base_url),
78 }
79 }
80
81 pub async fn chat_completions(
82 &self,
83 model: &str,
84 messages: Vec<Message>,
85 tools: Option<Vec<Tool>>,
86 options: crate::api::types::ChatOptions,
87 ) -> Result<Response> {
88 let url = format!("{}/chat/completions", self.base_url.trim_end_matches('/'));
89 tracing::info!("API request to: {}", url);
90 tracing::info!("Model: {}, Messages count: {}", model, messages.len());
91
92 let thinking_cfg = ThinkingConfig {
93 r#type: if options.thinking_enabled {
94 "enabled"
95 } else {
96 "disabled"
97 }
98 .to_string(),
99 };
100 let request = ChatRequest {
101 model: model.to_string(),
102 messages,
103 stream: true,
104 tools,
105 tool_choice: Some("auto".to_string()),
106 temperature: options.temperature,
107 top_p: options.top_p,
108 presence_penalty: options.presence_penalty,
109 frequency_penalty: options.frequency_penalty,
110 max_tokens: options.max_tokens,
111 thinking: Some(thinking_cfg),
112 reasoning_effort: options.reasoning_effort.clone(),
113 response_format: if options.json_mode {
114 Some(ResponseFormat {
115 r#type: "json_object".to_string(),
116 })
117 } else {
118 None
119 },
120 stop: None,
121 };
122
123 let mut last_err = None;
124 for attempt in 0..3 {
126 if attempt > 0 {
127 tracing::info!("Retry attempt {}...", attempt + 1);
128 tokio::time::sleep(Duration::from_millis(500 * (1 << attempt))).await;
129 }
130
131 let response_res = self
132 .client
133 .post(&url)
134 .bearer_auth(self.api_key.as_ref())
135 .json(&request)
136 .send()
137 .await;
138
139 match response_res {
140 Ok(response) => {
141 let status = response.status();
142 tracing::info!("API response status: {}", status);
143 if response.status().is_success() {
144 return Ok(response);
145 }
146 let err_text = response.text().await.unwrap_or_default();
147 tracing::error!("API error response: {}", err_text);
148
149 if status.is_server_error() || status.as_u16() == 429 {
150 last_err = Some(anyhow::anyhow!("API Error ({}): {}", status, err_text));
151 continue;
152 } else {
153 anyhow::bail!("API Error ({}): {}", status, err_text);
154 }
155 }
156 Err(e) => {
157 tracing::error!("Network error on attempt {}: {:#}", attempt + 1, e);
158 if e.is_timeout() {
159 tracing::error!("Connection timed out");
160 }
161 if e.is_connect() {
162 tracing::error!("Connection failed (DNS/TLS/refused)");
163 }
164 last_err = Some(anyhow::anyhow!("Network Error: {}", e));
165 continue;
166 }
167 }
168 }
169
170 Err(last_err.unwrap_or_else(|| anyhow::anyhow!("API Request failed after retries")))
171 }
172}