deepseek-rust-cli 1.20.7

A lightweight, high-speed autonomous CLI system agent port of DeepSeek CLI.
Documentation
use std::{sync::Arc, time::Duration};

use anyhow::Result;
use reqwest::{Client, Response};

use crate::api::types::{ChatRequest, Message, ResponseFormat, ThinkingConfig, Tool};

/// High-performance HTTP client with connection pooling, HTTP/2, and retry logic.
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))
            // Connection pooling — reuse connections for multiple requests
            .pool_idle_timeout(Some(Duration::from_secs(90)))
            .pool_max_idle_per_host(4)
            // TCP keep-alive for persistent connections
            .tcp_keepalive(Some(Duration::from_secs(60)))
            // Use HTTP/2 via ALPN negotiation (works behind proxies/firewalls)
            .http2_adaptive_window(true)
            // Auto-decompress responses
            .gzip(true)
            .brotli(true)
            .zstd(true)
            // Connection timeout
            .connect_timeout(Duration::from_secs(10))
            // User agent
            .user_agent(concat!(
                "deepseek-rust-cli/",
                env!("CARGO_PKG_VERSION")
            ));

        // Accept invalid TLS certs (for corporate proxies / MITM appliances)
        if danger_accept_invalid_certs {
            tracing::warn!(
                "DANGER: Accepting invalid TLS certificates (--danger-accept-invalid-certs)"
            );
            builder = builder.danger_accept_invalid_certs(true);
        }

        // Apply proxy configuration if provided
        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;
        // Exponential backoff: 500ms, 1s, 2s
        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")))
    }
}