1#[cfg(feature = "phind")]
4use crate::{
5 chat::{ChatMessage, ChatProvider, ChatRole},
6 completion::{CompletionProvider, CompletionRequest, CompletionResponse},
7 embedding::EmbeddingProvider,
8 error::LLMError,
9 models::ModelsProvider,
10 stt::SpeechToTextProvider,
11 tts::TextToSpeechProvider,
12 LLMProvider,
13};
14use crate::{
15 chat::{ChatResponse, Tool},
16 ToolCall,
17};
18use async_trait::async_trait;
19use reqwest::header::{HeaderMap, HeaderValue};
20use reqwest::StatusCode;
21use reqwest::{Client, Response};
22use serde_json::{json, Value};
23
24pub struct Phind {
26 pub model: String,
28 pub max_tokens: Option<u32>,
30 pub temperature: Option<f32>,
32 pub system: Option<String>,
34 pub timeout_seconds: Option<u64>,
36 pub stream: Option<bool>,
38 pub top_p: Option<f32>,
40 pub top_k: Option<u32>,
42 pub api_base_url: String,
44 client: Client,
46}
47
48#[derive(Debug)]
49pub struct PhindResponse {
50 content: String,
51}
52
53impl std::fmt::Display for PhindResponse {
54 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55 write!(f, "{}", self.content)
56 }
57}
58
59impl ChatResponse for PhindResponse {
60 fn text(&self) -> Option<String> {
61 Some(self.content.clone())
62 }
63
64 fn tool_calls(&self) -> Option<Vec<ToolCall>> {
65 None
66 }
67}
68
69impl Phind {
70 #[allow(clippy::too_many_arguments)]
72 pub fn new(
73 model: Option<String>,
74 max_tokens: Option<u32>,
75 temperature: Option<f32>,
76 timeout_seconds: Option<u64>,
77 system: Option<String>,
78 stream: Option<bool>,
79 top_p: Option<f32>,
80 top_k: Option<u32>,
81 ) -> Self {
82 let mut builder = Client::builder();
83 if let Some(sec) = timeout_seconds {
84 builder = builder.timeout(std::time::Duration::from_secs(sec));
85 }
86 Self {
87 model: model.unwrap_or_else(|| "Phind-70B".to_string()),
88 max_tokens,
89 temperature,
90 system,
91 timeout_seconds,
92 stream,
93 top_p,
94 top_k,
95 api_base_url: "https://https.extension.phind.com/agent/".to_string(),
96 client: builder.build().expect("Failed to build reqwest Client"),
97 }
98 }
99
100 fn create_headers() -> Result<HeaderMap, LLMError> {
102 let mut headers = HeaderMap::new();
103 headers.insert("Content-Type", HeaderValue::from_static("application/json"));
104 headers.insert("User-Agent", HeaderValue::from_static(""));
105 headers.insert("Accept", HeaderValue::from_static("*/*"));
106 headers.insert("Accept-Encoding", HeaderValue::from_static("Identity"));
107 Ok(headers)
108 }
109
110 fn parse_line(line: &str) -> Option<String> {
112 let data = line.strip_prefix("data: ")?;
113 let json_value: Value = serde_json::from_str(data).ok()?;
114
115 json_value
116 .get("choices")?
117 .as_array()?
118 .first()?
119 .get("delta")?
120 .get("content")?
121 .as_str()
122 .map(String::from)
123 }
124
125 fn parse_stream_response(response_text: &str) -> String {
127 response_text
128 .split('\n')
129 .filter_map(Self::parse_line)
130 .collect()
131 }
132
133 async fn interpret_response(
135 &self,
136 response: Response,
137 ) -> Result<Box<dyn ChatResponse>, LLMError> {
138 let status = response.status();
139 match status {
140 StatusCode::OK => {
141 let response_text = response.text().await?;
142 let full_text = Self::parse_stream_response(&response_text);
143 if full_text.is_empty() {
144 Err(LLMError::ProviderError(
145 "No completion choice returned.".to_string(),
146 ))
147 } else {
148 Ok(Box::new(PhindResponse { content: full_text }))
149 }
150 }
151 _ => {
152 let error_text = response.text().await?;
153 let error_json: Value = serde_json::from_str(&error_text)
154 .unwrap_or_else(|_| json!({"error": {"message": "Unknown error"}}));
155
156 let error_message = error_json
157 .get("error")
158 .and_then(|err| err.get("message"))
159 .and_then(|msg| msg.as_str())
160 .unwrap_or("Unexpected error from Phind")
161 .to_string();
162
163 Err(LLMError::ProviderError(format!(
164 "APIError {}: {}",
165 status, error_message
166 )))
167 }
168 }
169 }
170}
171
172#[async_trait]
174impl ChatProvider for Phind {
175 async fn chat(&self, messages: &[ChatMessage]) -> Result<Box<dyn ChatResponse>, LLMError> {
185 let mut message_history = vec![];
186 for m in messages {
187 let role_str = match m.role {
188 ChatRole::User => "user",
189 ChatRole::Assistant => "assistant",
190 };
191 message_history.push(json!({
192 "content": m.content,
193 "role": role_str
194 }));
195 }
196
197 if let Some(system_prompt) = &self.system {
198 message_history.insert(
199 0,
200 json!({
201 "content": system_prompt,
202 "role": "system"
203 }),
204 );
205 }
206
207 let payload = json!({
208 "additional_extension_context": "",
209 "allow_magic_buttons": true,
210 "is_vscode_extension": true,
211 "message_history": message_history,
212 "requested_model": self.model,
213 "user_input": messages
214 .iter()
215 .rev()
216 .find(|m| m.role == ChatRole::User)
217 .map(|m| m.content.clone())
218 .unwrap_or_default(),
219 });
220
221 if log::log_enabled!(log::Level::Trace) {
222 log::trace!("Phind request payload: {}", payload);
223 }
224
225 let headers = Self::create_headers()?;
226 let mut request = self
227 .client
228 .post(&self.api_base_url)
229 .headers(headers)
230 .json(&payload);
231
232 if let Some(timeout) = self.timeout_seconds {
233 request = request.timeout(std::time::Duration::from_secs(timeout));
234 }
235
236 let response = request.send().await?;
237
238 log::debug!("Phind HTTP status: {}", response.status());
239
240 self.interpret_response(response).await
241 }
242
243 async fn chat_with_tools(
254 &self,
255 _messages: &[ChatMessage],
256 _tools: Option<&[Tool]>,
257 ) -> Result<Box<dyn ChatResponse>, LLMError> {
258 todo!()
259 }
260}
261
262#[async_trait]
264impl CompletionProvider for Phind {
265 async fn complete(&self, _req: &CompletionRequest) -> Result<CompletionResponse, LLMError> {
266 let chat_resp = self
267 .chat(&[crate::chat::ChatMessage::user()
268 .content(_req.prompt.clone())
269 .build()])
270 .await?;
271 if let Some(text) = chat_resp.text() {
272 Ok(CompletionResponse { text })
273 } else {
274 Err(LLMError::ProviderError(
275 "No completion text returned by Phind".to_string(),
276 ))
277 }
278 }
279}
280
281#[cfg(feature = "phind")]
283#[async_trait]
284impl EmbeddingProvider for Phind {
285 async fn embed(&self, _input: Vec<String>) -> Result<Vec<Vec<f32>>, LLMError> {
286 Err(LLMError::ProviderError(
287 "Phind does not implement embeddings endpoint yet.".into(),
288 ))
289 }
290}
291
292#[async_trait]
293impl SpeechToTextProvider for Phind {
294 async fn transcribe(&self, _audio: Vec<u8>) -> Result<String, LLMError> {
295 Err(LLMError::ProviderError(
296 "Phind does not implement speech to text endpoint yet.".into(),
297 ))
298 }
299}
300
301#[async_trait]
302impl ModelsProvider for Phind {}
303
304#[async_trait]
306impl TextToSpeechProvider for Phind {}
307impl LLMProvider for Phind {}