use crate::{
provider::{AIProvider, AIResponse, StreamingResponse},
types::{AIError, AIResult, CompletionOptions, Message, Role},
};
use async_trait::async_trait;
use futures::stream;
use std::process::Command;
pub struct ClaudeCLIProvider {
model: String,
}
impl ClaudeCLIProvider {
pub fn new(model: Option<String>) -> Self {
Self {
model: model.unwrap_or_else(|| "claude-sonnet-4".to_string()),
}
}
pub fn is_available() -> bool {
Command::new("claude")
.arg("--version")
.output()
.map(|output| output.status.success())
.unwrap_or(false)
}
}
#[async_trait]
impl AIProvider for ClaudeCLIProvider {
fn name(&self) -> &str {
"Claude CLI"
}
async fn complete(
&self,
messages: &[Message],
_options: Option<CompletionOptions>,
) -> AIResult<AIResponse> {
let mut formatted_prompt = String::new();
if let Some(system_msg) = messages.iter().find(|m| m.role == Role::System) {
formatted_prompt.push_str(&system_msg.content);
formatted_prompt.push_str("\n\n");
}
for msg in messages.iter() {
match msg.role {
Role::System => continue, Role::User => {
formatted_prompt.push_str("User: ");
formatted_prompt.push_str(&msg.content);
formatted_prompt.push('\n');
}
Role::Assistant => {
formatted_prompt.push_str("Assistant: ");
formatted_prompt.push_str(&msg.content);
formatted_prompt.push('\n');
}
}
}
let output = tokio::task::spawn_blocking({
let prompt = formatted_prompt.clone();
move || {
Command::new("claude")
.arg("-p") .arg(&prompt)
.output()
}
})
.await
.map_err(|e| AIError::Unknown(e.to_string()))?
.map_err(|e| AIError::Unknown(e.to_string()))?;
if !output.status.success() {
let error = String::from_utf8_lossy(&output.stderr);
return Err(AIError::ApiError(format!("Claude CLI failed: {}", error)));
}
let response = String::from_utf8_lossy(&output.stdout).to_string();
Ok(AIResponse {
content: response,
model: self.model.clone(),
tokens_used: None,
})
}
async fn stream(
&self,
messages: &[Message],
options: Option<CompletionOptions>,
) -> AIResult<StreamingResponse> {
let response = self.complete(messages, options).await?;
let stream_content = stream::once(async move { Ok(response.content) });
Ok(Box::pin(stream_content))
}
async fn health_check(&self) -> AIResult<bool> {
Ok(Self::is_available())
}
async fn list_models(&self) -> AIResult<Vec<String>> {
Ok(vec![self.model.clone()])
}
}