mailrs_intelligence/
openai_compatible.rs1use async_trait::async_trait;
2
3use crate::provider::LlmProvider;
4
5#[derive(Debug, Clone)]
12pub struct OpenAiCompatibleProvider {
13 url: String,
14 api_key: Option<String>,
15 model_id: String,
16 client: reqwest::Client,
17}
18
19impl OpenAiCompatibleProvider {
20 pub fn new(url: String, api_key: Option<String>, model_id: String) -> Self {
26 let client = reqwest::Client::builder()
27 .connect_timeout(std::time::Duration::from_secs(10))
28 .build()
29 .unwrap_or_default();
30 Self {
31 url,
32 api_key,
33 model_id,
34 client,
35 }
36 }
37
38 pub fn with_client(
41 url: String,
42 api_key: Option<String>,
43 model_id: String,
44 client: reqwest::Client,
45 ) -> Self {
46 Self {
47 url,
48 api_key,
49 model_id,
50 client,
51 }
52 }
53}
54
55#[async_trait]
56impl LlmProvider for OpenAiCompatibleProvider {
57 async fn complete(&self, system: &str, user_message: &str, temperature: f32) -> Option<String> {
58 let body = serde_json::json!({
59 "system": system,
60 "messages": [{"role": "user", "content": user_message}],
61 "temperature": temperature
62 });
63
64 for attempt in 0..3u32 {
67 let result = tokio::time::timeout(std::time::Duration::from_secs(900), async {
68 let mut req = self.client.post(&self.url).json(&body);
69 if let Some(ref key) = self.api_key {
70 req = req
71 .header("Authorization", format!("Bearer {key}"))
72 .header("x-caller", "mailrs");
73 }
74 let response = req.send().await?;
75
76 if response.status().as_u16() == 429 {
77 return Ok::<Option<String>, reqwest::Error>(Some("__429__".into()));
78 }
79
80 if !response.status().is_success() {
81 let status = response.status();
82 let text = response.text().await.unwrap_or_default();
83 tracing::warn!(
84 event = "llm_http_error",
85 status = %status,
86 body = %&text[..text.len().min(200)]
87 );
88 return Ok(None);
89 }
90
91 let json: serde_json::Value = response.json().await?;
92 Ok(json["content"].as_str().map(|s| s.to_string()))
93 })
94 .await;
95
96 match result {
97 Ok(Ok(Some(ref s))) if s == "__429__" => {
98 let wait = if attempt < 2 { 15 } else { 30 };
99 tracing::warn!(
100 event = "llm_rate_limited",
101 attempt = attempt + 1,
102 retry_in_s = wait
103 );
104 tokio::time::sleep(std::time::Duration::from_secs(wait)).await;
105 continue;
106 }
107 Ok(Ok(content)) => return content,
108 Ok(Err(e)) => {
109 tracing::warn!(event = "llm_request_error", error = %e);
110 return None;
111 }
112 Err(_) => {
113 tracing::warn!(event = "llm_request_timeout", timeout_s = 900);
114 return None;
115 }
116 }
117 }
118
119 tracing::warn!(event = "llm_rate_limited_giving_up");
120 None
121 }
122
123 async fn embed(&self, text: &str) -> Option<Vec<f32>> {
124 let embed_url = self.url.replace("/complete", "/embed");
125 let body = serde_json::json!({ "input": text });
126
127 for attempt in 0..2u32 {
128 let mut req = self.client.post(&embed_url).json(&body);
129 if let Some(ref key) = self.api_key {
130 req = req
131 .header("Authorization", format!("Bearer {key}"))
132 .header("x-caller", "mailrs");
133 }
134 let response =
135 match tokio::time::timeout(std::time::Duration::from_secs(10), req.send()).await {
136 Ok(Ok(resp)) => resp,
137 Ok(Err(e)) => {
138 tracing::warn!(event = "embedding_request_error", error = %e);
139 return None;
140 }
141 Err(_) => {
142 tracing::warn!(event = "embedding_request_timeout", timeout_s = 10);
143 return None;
144 }
145 };
146
147 if response.status().as_u16() == 429 {
148 let wait = if attempt < 2 { 15 } else { 30 };
149 tracing::warn!(
150 event = "embedding_rate_limited",
151 attempt = attempt + 1,
152 retry_in_s = wait
153 );
154 tokio::time::sleep(std::time::Duration::from_secs(wait)).await;
155 continue;
156 }
157
158 if !response.status().is_success() {
159 let status = response.status();
160 let text = response.text().await.unwrap_or_default();
161 tracing::warn!(
162 event = "embedding_http_error",
163 status = %status,
164 body = %&text[..text.len().min(200)]
165 );
166 return None;
167 }
168
169 let json: serde_json::Value = match response.json().await {
170 Ok(v) => v,
171 Err(e) => {
172 tracing::warn!(event = "embedding_parse_error", error = %e);
173 return None;
174 }
175 };
176
177 let values = json["embeddings"]
178 .as_array()
179 .and_then(|arr| arr.first())
180 .and_then(|v| v.as_array())?;
181
182 let embedding: Vec<f32> = values
183 .iter()
184 .filter_map(|v| v.as_f64().map(|f| f as f32))
185 .collect();
186
187 let dims = json["dimensions"].as_u64().unwrap_or(1024) as usize;
188 return if embedding.len() == dims {
189 Some(embedding)
190 } else {
191 tracing::warn!(
192 event = "embedding_bad_dim",
193 got = embedding.len(),
194 expected = dims
195 );
196 None
197 };
198 }
199
200 tracing::warn!(event = "embedding_giving_up");
201 None
202 }
203
204 fn model_id(&self) -> &str {
205 &self.model_id
206 }
207}