llm/backends/
phind.rs

1/// Implementation of the Phind LLM provider.
2/// This module provides integration with Phind's language model API.
3#[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
24/// Represents a Phind LLM client with configuration options.
25pub struct Phind {
26    /// The model identifier to use (e.g. "Phind-70B")
27    pub model: String,
28    /// Maximum number of tokens to generate
29    pub max_tokens: Option<u32>,
30    /// Temperature for controlling randomness (0.0-1.0)
31    pub temperature: Option<f32>,
32    /// System prompt to prepend to conversations
33    pub system: Option<String>,
34    /// Request timeout in seconds
35    pub timeout_seconds: Option<u64>,
36    /// Whether to stream responses
37    pub stream: Option<bool>,
38    /// Top-p sampling parameter
39    pub top_p: Option<f32>,
40    /// Top-k sampling parameter
41    pub top_k: Option<u32>,
42    /// Base URL for the Phind API
43    pub api_base_url: String,
44    /// HTTP client for making requests
45    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    /// Creates a new Phind client with the specified configuration.
71    #[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    /// Creates the required headers for API requests.
101    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    /// Parses a single line from the streaming response.
111    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    /// Parses the complete streaming response into a single string.
126    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    /// Interprets the API response and handles any errors.
134    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/// Implementation of chat functionality for Phind.
173#[async_trait]
174impl ChatProvider for Phind {
175    /// Sends a chat request to Phind's API.
176    ///
177    /// # Arguments
178    ///
179    /// * `messages` - The conversation history as a slice of chat messages
180    ///
181    /// # Returns
182    ///
183    /// The provider's response text or an error
184    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    /// Sends a chat request to Phind's API with tools.
244    ///
245    /// # Arguments
246    ///
247    /// * `messages` - The conversation history as a slice of chat messages
248    /// * `tools` - Optional slice of tools to use in the chat
249    ///
250    /// # Returns
251    ///
252    /// The provider's response text or an error
253    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/// Implementation of completion functionality for Phind.
263#[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/// Implementation of embedding functionality for Phind.
282#[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/// Implementation of the LLMProvider trait for Phind.
305#[async_trait]
306impl TextToSpeechProvider for Phind {}
307impl LLMProvider for Phind {}