use std::{sync::Arc, time::Duration};
use anyhow::Result;
use reqwest::{Client, Response};
use crate::api::types::{ChatRequest, Message, ResponseFormat, ThinkingConfig, Tool};
pub struct DeepSeekClient {
client: Client,
api_key: Arc<str>,
base_url: Arc<str>,
}
impl DeepSeekClient {
pub fn new(
api_key: String,
base_url: String,
timeout_secs: u64,
proxy_url: Option<String>,
proxy_username: Option<String>,
proxy_password: Option<String>,
danger_accept_invalid_certs: bool,
) -> Self {
let mut builder = Client::builder()
.timeout(Duration::from_secs(timeout_secs))
.pool_idle_timeout(Some(Duration::from_secs(90)))
.pool_max_idle_per_host(4)
.tcp_keepalive(Some(Duration::from_secs(60)))
.http2_adaptive_window(true)
.gzip(true)
.brotli(true)
.zstd(true)
.connect_timeout(Duration::from_secs(10))
.user_agent(concat!(
"deepseek-rust-cli/",
env!("CARGO_PKG_VERSION")
));
if danger_accept_invalid_certs {
tracing::warn!(
"DANGER: Accepting invalid TLS certificates (--danger-accept-invalid-certs)"
);
builder = builder.danger_accept_invalid_certs(true);
}
if let Some(ref proxy_url) = proxy_url {
let mut proxy = match reqwest::Proxy::all(proxy_url) {
Ok(p) => p,
Err(e) => {
tracing::warn!("Invalid proxy URL '{}': {}", proxy_url, e);
reqwest::Proxy::custom(|_url| None::<reqwest::Url>)
}
};
if let (Some(user), Some(pass)) = (&proxy_username, &proxy_password) {
proxy = proxy.basic_auth(user, pass);
}
tracing::info!("Using proxy: {}", proxy_url);
builder = builder.proxy(proxy);
}
let client = builder.build().unwrap_or_else(|_| Client::new());
Self {
client,
api_key: Arc::from(api_key),
base_url: Arc::from(base_url),
}
}
pub async fn chat_completions(
&self,
model: &str,
messages: Vec<Message>,
tools: Option<Vec<Tool>>,
options: crate::api::types::ChatOptions,
) -> Result<Response> {
let url = format!("{}/chat/completions", self.base_url.trim_end_matches('/'));
tracing::info!("API request to: {}", url);
tracing::info!("Model: {}, Messages count: {}", model, messages.len());
let thinking_cfg = ThinkingConfig {
r#type: if options.thinking_enabled {
"enabled"
} else {
"disabled"
}
.to_string(),
};
let request = ChatRequest {
model: model.to_string(),
messages,
stream: true,
tools,
tool_choice: Some("auto".to_string()),
temperature: options.temperature,
top_p: options.top_p,
presence_penalty: options.presence_penalty,
frequency_penalty: options.frequency_penalty,
max_tokens: options.max_tokens,
thinking: Some(thinking_cfg),
reasoning_effort: options.reasoning_effort.clone(),
response_format: if options.json_mode {
Some(ResponseFormat {
r#type: "json_object".to_string(),
})
} else {
None
},
stop: None,
};
let mut last_err = None;
for attempt in 0..3 {
if attempt > 0 {
tracing::info!("Retry attempt {}...", attempt + 1);
tokio::time::sleep(Duration::from_millis(500 * (1 << attempt))).await;
}
let response_res = self
.client
.post(&url)
.bearer_auth(self.api_key.as_ref())
.json(&request)
.send()
.await;
match response_res {
Ok(response) => {
let status = response.status();
tracing::info!("API response status: {}", status);
if response.status().is_success() {
return Ok(response);
}
let err_text = response.text().await.unwrap_or_default();
tracing::error!("API error response: {}", err_text);
if status.is_server_error() || status.as_u16() == 429 {
last_err = Some(anyhow::anyhow!("API Error ({}): {}", status, err_text));
continue;
} else {
anyhow::bail!("API Error ({}): {}", status, err_text);
}
}
Err(e) => {
tracing::error!("Network error on attempt {}: {:#}", attempt + 1, e);
if e.is_timeout() {
tracing::error!("Connection timed out");
}
if e.is_connect() {
tracing::error!("Connection failed (DNS/TLS/refused)");
}
last_err = Some(anyhow::anyhow!("Network Error: {}", e));
continue;
}
}
}
Err(last_err.unwrap_or_else(|| anyhow::anyhow!("API Request failed after retries")))
}
}