chasm_cli/providers/cloud/
common.rs1use crate::models::{ChatMessage, ChatRequest, ChatSession};
6use anyhow::Result;
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, Default)]
12pub struct FetchOptions {
13 pub limit: Option<usize>,
15 pub after: Option<DateTime<Utc>>,
17 pub before: Option<DateTime<Utc>>,
19 pub include_archived: bool,
21 pub session_token: Option<String>,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct CloudConversation {
28 pub id: String,
30 pub title: Option<String>,
32 pub created_at: DateTime<Utc>,
34 pub updated_at: Option<DateTime<Utc>>,
36 pub model: Option<String>,
38 pub messages: Vec<CloudMessage>,
40 pub metadata: Option<serde_json::Value>,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct CloudMessage {
47 pub id: Option<String>,
49 pub role: String,
51 pub content: String,
53 pub timestamp: Option<DateTime<Utc>>,
55 pub model: Option<String>,
57}
58
59impl CloudConversation {
60 pub fn to_chat_session(&self, provider_name: &str) -> ChatSession {
62 use uuid::Uuid;
63
64 let mut requests = Vec::new();
66 let mut i = 0;
67 while i < self.messages.len() {
68 let msg = &self.messages[i];
69
70 if msg.role == "user" {
71 let mut request = ChatRequest {
73 timestamp: msg.timestamp.map(|t| t.timestamp_millis()),
74 message: Some(ChatMessage {
75 text: Some(msg.content.clone()),
76 parts: None,
77 }),
78 response: None,
79 variable_data: None,
80 request_id: Some(msg.id.clone().unwrap_or_else(|| Uuid::new_v4().to_string())),
81 response_id: None,
82 model_id: self.model.clone(),
83 agent: None,
84 result: None,
85 followups: None,
86 is_canceled: Some(false),
87 content_references: None,
88 code_citations: None,
89 response_markdown_info: None,
90 source_session: Some(format!("{}:{}", provider_name, self.id)),
91 };
92
93 if i + 1 < self.messages.len() && self.messages[i + 1].role == "assistant" {
95 let assistant_msg = &self.messages[i + 1];
96 request.response = Some(serde_json::json!({
97 "text": assistant_msg.content,
98 "result": {
99 "metadata": {
100 "provider": provider_name,
101 "model": assistant_msg.model.clone().or_else(|| self.model.clone())
102 }
103 }
104 }));
105 request.response_id = Some(
106 assistant_msg
107 .id
108 .clone()
109 .unwrap_or_else(|| Uuid::new_v4().to_string()),
110 );
111 i += 1;
112 }
113
114 requests.push(request);
115 } else if msg.role == "system" {
116 }
119 i += 1;
120 }
121
122 ChatSession {
123 version: 3,
124 session_id: Some(format!("{}:{}", provider_name, self.id)),
125 creation_date: self.created_at.timestamp_millis(),
126 last_message_date: self
127 .updated_at
128 .unwrap_or(self.created_at)
129 .timestamp_millis(),
130 is_imported: true,
131 initial_location: "panel".to_string(),
132 custom_title: self.title.clone(),
133 requester_username: None,
134 requester_avatar_icon_uri: None,
135 responder_username: Some(provider_name.to_string()),
136 responder_avatar_icon_uri: None,
137 requests,
138 }
139 }
140}
141
142pub trait CloudProvider: Send + Sync {
144 fn name(&self) -> &'static str;
146
147 fn api_base_url(&self) -> &str;
149
150 fn is_authenticated(&self) -> bool;
152
153 fn set_credentials(&mut self, api_key: Option<String>, session_token: Option<String>);
155
156 fn list_conversations(&self, options: &FetchOptions) -> Result<Vec<CloudConversation>>;
158
159 fn fetch_conversation(&self, id: &str) -> Result<CloudConversation>;
161
162 fn fetch_all_conversations(&self, options: &FetchOptions) -> Result<Vec<ChatSession>> {
164 let conversations = self.list_conversations(options)?;
165 let mut sessions = Vec::new();
166
167 for conv in conversations {
168 if !conv.messages.is_empty() {
170 sessions.push(conv.to_chat_session(self.name()));
171 } else {
172 match self.fetch_conversation(&conv.id) {
174 Ok(full_conv) => sessions.push(full_conv.to_chat_session(self.name())),
175 Err(e) => {
176 eprintln!("Warning: Failed to fetch conversation {}: {}", conv.id, e);
177 }
178 }
179 }
180 }
181
182 Ok(sessions)
183 }
184
185 fn api_key_env_var(&self) -> &'static str;
187
188 fn load_api_key_from_env(&self) -> Option<String> {
190 std::env::var(self.api_key_env_var()).ok()
191 }
192}
193
194#[derive(Debug, Clone)]
196pub struct HttpClientConfig {
197 pub timeout_secs: u64,
198 pub user_agent: String,
199 pub accept_invalid_certs: bool,
200}
201
202impl Default for HttpClientConfig {
203 fn default() -> Self {
204 Self {
205 timeout_secs: 30,
206 user_agent: format!("csm/{}", env!("CARGO_PKG_VERSION")),
207 accept_invalid_certs: false,
208 }
209 }
210}
211
212pub fn build_http_client(config: &HttpClientConfig) -> Result<reqwest::blocking::Client> {
214 use std::time::Duration;
215
216 reqwest::blocking::Client::builder()
217 .timeout(Duration::from_secs(config.timeout_secs))
218 .user_agent(&config.user_agent)
219 .danger_accept_invalid_certs(config.accept_invalid_certs)
220 .build()
221 .map_err(|e| anyhow::anyhow!("Failed to build HTTP client: {}", e))
222}