chasm_cli/providers/cloud/
deepseek.rs

1// Copyright (c) 2024-2026 Nervosys LLC
2// SPDX-License-Identifier: Apache-2.0
3//! DeepSeek cloud provider
4//!
5//! Fetches conversation history from DeepSeek web interface.
6//!
7//! ## Authentication
8//!
9//! Requires either:
10//! - API key via `DEEPSEEK_API_KEY` environment variable
11//! - Session token for web interface access
12
13use super::common::{
14    build_http_client, CloudConversation, CloudMessage, CloudProvider, FetchOptions,
15    HttpClientConfig,
16};
17use anyhow::{anyhow, Result};
18use chrono::{DateTime, TimeZone, Utc};
19use serde::Deserialize;
20
21const DEEPSEEK_API: &str = "https://api.deepseek.com/v1";
22const DEEPSEEK_WEB_API: &str = "https://chat.deepseek.com/api";
23
24/// DeepSeek provider for fetching conversation history
25pub struct DeepSeekProvider {
26    api_key: Option<String>,
27    session_token: Option<String>,
28    client: Option<reqwest::blocking::Client>,
29}
30
31impl DeepSeekProvider {
32    pub fn new(api_key: Option<String>) -> Self {
33        Self {
34            api_key,
35            session_token: None,
36            client: None,
37        }
38    }
39
40    fn ensure_client(&mut self) -> Result<&reqwest::blocking::Client> {
41        if self.client.is_none() {
42            let config = HttpClientConfig::default();
43            self.client = Some(build_http_client(&config)?);
44        }
45        Ok(self.client.as_ref().unwrap())
46    }
47}
48
49#[derive(Debug, Deserialize)]
50struct DeepSeekConversation {
51    id: String,
52    #[serde(default)]
53    title: Option<String>,
54    created_at: i64,
55    updated_at: i64,
56    #[serde(default)]
57    messages: Vec<DeepSeekMessage>,
58    #[serde(default)]
59    model: Option<String>,
60}
61
62#[derive(Debug, Deserialize)]
63struct DeepSeekMessage {
64    id: String,
65    content: String,
66    role: String,
67    created_at: i64,
68}
69
70impl CloudProvider for DeepSeekProvider {
71    fn name(&self) -> &'static str {
72        "DeepSeek"
73    }
74
75    fn api_base_url(&self) -> &str {
76        DEEPSEEK_WEB_API
77    }
78
79    fn is_authenticated(&self) -> bool {
80        self.api_key.is_some() || self.session_token.is_some()
81    }
82
83    fn set_credentials(&mut self, api_key: Option<String>, session_token: Option<String>) {
84        self.api_key = api_key;
85        self.session_token = session_token;
86    }
87
88    fn list_conversations(&self, _options: &FetchOptions) -> Result<Vec<CloudConversation>> {
89        if !self.is_authenticated() {
90            return Err(anyhow!(
91                "DeepSeek requires authentication. Set DEEPSEEK_API_KEY or provide a session token."
92            ));
93        }
94
95        eprintln!("Note: DeepSeek conversation history requires web session authentication.");
96        eprintln!("The DeepSeek API is stateless and doesn't store conversation history.");
97
98        Ok(vec![])
99    }
100
101    fn fetch_conversation(&self, _id: &str) -> Result<CloudConversation> {
102        if !self.is_authenticated() {
103            return Err(anyhow!("DeepSeek requires authentication"));
104        }
105
106        Err(anyhow!(
107            "Fetching DeepSeek conversations requires web session authentication."
108        ))
109    }
110
111    fn api_key_env_var(&self) -> &'static str {
112        "DEEPSEEK_API_KEY"
113    }
114}
115
116/// Parse DeepSeek export data
117pub fn parse_deepseek_export(json_data: &str) -> Result<Vec<CloudConversation>> {
118    let conversations: Vec<DeepSeekConversation> = serde_json::from_str(json_data)?;
119
120    Ok(conversations
121        .into_iter()
122        .map(|conv| CloudConversation {
123            id: conv.id,
124            title: conv.title,
125            created_at: timestamp_millis_to_datetime(conv.created_at),
126            updated_at: Some(timestamp_millis_to_datetime(conv.updated_at)),
127            model: conv.model,
128            messages: conv
129                .messages
130                .into_iter()
131                .map(|msg| CloudMessage {
132                    id: Some(msg.id),
133                    role: msg.role,
134                    content: msg.content,
135                    timestamp: Some(timestamp_millis_to_datetime(msg.created_at)),
136                    model: None,
137                })
138                .collect(),
139            metadata: None,
140        })
141        .collect())
142}
143
144fn timestamp_millis_to_datetime(ts: i64) -> DateTime<Utc> {
145    Utc.timestamp_millis_opt(ts)
146        .single()
147        .unwrap_or_else(Utc::now)
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    #[test]
155    fn test_deepseek_provider_new() {
156        let provider = DeepSeekProvider::new(Some("test-key".to_string()));
157        assert_eq!(provider.name(), "DeepSeek");
158        assert!(provider.is_authenticated());
159    }
160
161    #[test]
162    fn test_deepseek_provider_unauthenticated() {
163        let provider = DeepSeekProvider::new(None);
164        assert!(!provider.is_authenticated());
165    }
166
167    #[test]
168    fn test_api_key_env_var() {
169        let provider = DeepSeekProvider::new(None);
170        assert_eq!(provider.api_key_env_var(), "DEEPSEEK_API_KEY");
171    }
172
173    #[test]
174    fn test_timestamp_millis_to_datetime() {
175        let ts = 1700000000000i64;
176        let dt = timestamp_millis_to_datetime(ts);
177        assert_eq!(dt.timestamp_millis(), ts);
178    }
179}