use std::sync::Arc;
#[cfg(feature = "phind")]
use crate::{
chat::{ChatMessage, ChatProvider, ChatRole},
completion::{CompletionProvider, CompletionRequest, CompletionResponse},
embedding::EmbeddingProvider,
error::LLMError,
models::ModelsProvider,
stt::SpeechToTextProvider,
tts::TextToSpeechProvider,
LLMProvider,
};
use crate::{
chat::{ChatResponse, Tool},
ToolCall,
};
use async_trait::async_trait;
use reqwest::header::{HeaderMap, HeaderValue};
use reqwest::StatusCode;
use reqwest::{Client, Response};
use serde_json::{json, Value};
#[derive(Debug)]
pub struct PhindConfig {
pub model: String,
pub max_tokens: Option<u32>,
pub temperature: Option<f32>,
pub system: Option<String>,
pub timeout_seconds: Option<u64>,
pub top_p: Option<f32>,
pub top_k: Option<u32>,
pub api_base_url: String,
}
#[derive(Debug, Clone)]
pub struct Phind {
pub config: Arc<PhindConfig>,
pub client: Client,
}
const AUDIO_UNSUPPORTED: &str = "Phind does not support audio chat messages";
#[derive(Debug)]
pub struct PhindResponse {
content: String,
}
impl std::fmt::Display for PhindResponse {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.content)
}
}
impl ChatResponse for PhindResponse {
fn text(&self) -> Option<String> {
Some(self.content.clone())
}
fn tool_calls(&self) -> Option<Vec<ToolCall>> {
None
}
}
impl Phind {
#[allow(clippy::too_many_arguments)]
pub fn new(
model: Option<String>,
max_tokens: Option<u32>,
temperature: Option<f32>,
timeout_seconds: Option<u64>,
system: Option<String>,
top_p: Option<f32>,
top_k: Option<u32>,
) -> Self {
let mut builder = Client::builder();
if let Some(sec) = timeout_seconds {
builder = builder.timeout(std::time::Duration::from_secs(sec));
}
Self::with_client(
builder.build().expect("Failed to build reqwest Client"),
model,
max_tokens,
temperature,
timeout_seconds,
system,
top_p,
top_k,
)
}
#[allow(clippy::too_many_arguments)]
pub fn with_client(
client: Client,
model: Option<String>,
max_tokens: Option<u32>,
temperature: Option<f32>,
timeout_seconds: Option<u64>,
system: Option<String>,
top_p: Option<f32>,
top_k: Option<u32>,
) -> Self {
Self {
config: Arc::new(PhindConfig {
model: model.unwrap_or_else(|| "Phind-70B".to_string()),
max_tokens,
temperature,
system,
timeout_seconds,
top_p,
top_k,
api_base_url: "https://https.extension.phind.com/agent/".to_string(),
}),
client,
}
}
pub fn model(&self) -> &str {
&self.config.model
}
pub fn max_tokens(&self) -> Option<u32> {
self.config.max_tokens
}
pub fn temperature(&self) -> Option<f32> {
self.config.temperature
}
pub fn timeout_seconds(&self) -> Option<u64> {
self.config.timeout_seconds
}
pub fn system(&self) -> Option<&str> {
self.config.system.as_deref()
}
pub fn top_p(&self) -> Option<f32> {
self.config.top_p
}
pub fn top_k(&self) -> Option<u32> {
self.config.top_k
}
pub fn api_base_url(&self) -> &str {
&self.config.api_base_url
}
pub fn client(&self) -> &Client {
&self.client
}
fn create_headers() -> Result<HeaderMap, LLMError> {
let mut headers = HeaderMap::new();
headers.insert("Content-Type", HeaderValue::from_static("application/json"));
headers.insert("User-Agent", HeaderValue::from_static(""));
headers.insert("Accept", HeaderValue::from_static("*/*"));
headers.insert("Accept-Encoding", HeaderValue::from_static("Identity"));
Ok(headers)
}
fn parse_line(line: &str) -> Option<String> {
let data = line.strip_prefix("data: ")?;
let json_value: Value = serde_json::from_str(data).ok()?;
json_value
.get("choices")?
.as_array()?
.first()?
.get("delta")?
.get("content")?
.as_str()
.map(String::from)
}
fn parse_stream_response(response_text: &str) -> String {
response_text
.split('\n')
.filter_map(Self::parse_line)
.collect()
}
async fn interpret_response(
&self,
response: Response,
) -> Result<Box<dyn ChatResponse>, LLMError> {
let status = response.status();
match status {
StatusCode::OK => {
let response_text = response.text().await?;
let full_text = Self::parse_stream_response(&response_text);
if full_text.is_empty() {
Err(LLMError::ProviderError(
"No completion choice returned.".to_string(),
))
} else {
Ok(Box::new(PhindResponse { content: full_text }))
}
}
_ => {
let error_text = response.text().await?;
let error_json: Value = serde_json::from_str(&error_text)
.unwrap_or_else(|_| json!({"error": {"message": "Unknown error"}}));
let error_message = error_json
.get("error")
.and_then(|err| err.get("message"))
.and_then(|msg| msg.as_str())
.unwrap_or("Unexpected error from Phind")
.to_string();
Err(LLMError::ProviderError(format!(
"APIError {status}: {error_message}"
)))
}
}
}
}
#[async_trait]
impl ChatProvider for Phind {
async fn chat(&self, messages: &[ChatMessage]) -> Result<Box<dyn ChatResponse>, LLMError> {
crate::chat::ensure_no_audio(messages, AUDIO_UNSUPPORTED)?;
let mut message_history = vec![];
for m in messages {
let role_str = match m.role {
ChatRole::User => "user",
ChatRole::Assistant => "assistant",
};
message_history.push(json!({
"content": m.content,
"role": role_str
}));
}
if let Some(system_prompt) = &self.config.system {
message_history.insert(
0,
json!({
"content": system_prompt,
"role": "system"
}),
);
}
let payload = json!({
"additional_extension_context": "",
"allow_magic_buttons": true,
"is_vscode_extension": true,
"message_history": message_history,
"requested_model": self.config.model,
"user_input": messages
.iter()
.rev()
.find(|m| m.role == ChatRole::User)
.map(|m| m.content.clone())
.unwrap_or_default(),
});
if log::log_enabled!(log::Level::Trace) {
log::trace!("Phind request payload: {}", payload);
}
let headers = Self::create_headers()?;
let mut request = self
.client
.post(&self.config.api_base_url)
.headers(headers)
.json(&payload);
if let Some(timeout) = self.config.timeout_seconds {
request = request.timeout(std::time::Duration::from_secs(timeout));
}
let response = request.send().await?;
log::debug!("Phind HTTP status: {}", response.status());
self.interpret_response(response).await
}
async fn chat_with_tools(
&self,
_messages: &[ChatMessage],
_tools: Option<&[Tool]>,
) -> Result<Box<dyn ChatResponse>, LLMError> {
todo!()
}
}
#[async_trait]
impl CompletionProvider for Phind {
async fn complete(&self, _req: &CompletionRequest) -> Result<CompletionResponse, LLMError> {
let chat_resp = self
.chat(&[crate::chat::ChatMessage::user()
.content(_req.prompt.clone())
.build()])
.await?;
if let Some(text) = chat_resp.text() {
Ok(CompletionResponse { text })
} else {
Err(LLMError::ProviderError(
"No completion text returned by Phind".to_string(),
))
}
}
}
#[cfg(feature = "phind")]
#[async_trait]
impl EmbeddingProvider for Phind {
async fn embed(&self, _input: Vec<String>) -> Result<Vec<Vec<f32>>, LLMError> {
Err(LLMError::ProviderError(
"Phind does not implement embeddings endpoint yet.".into(),
))
}
}
#[async_trait]
impl SpeechToTextProvider for Phind {
async fn transcribe(&self, _audio: Vec<u8>) -> Result<String, LLMError> {
Err(LLMError::ProviderError(
"Phind does not implement speech to text endpoint yet.".into(),
))
}
}
#[async_trait]
impl ModelsProvider for Phind {}
#[async_trait]
impl TextToSpeechProvider for Phind {}
impl LLMProvider for Phind {}