chasm_cli/providers/cloud/
deepseek.rs1use 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
24pub 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
116pub 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}