diffscope 0.5.3

A composable code review engine with smart analysis, confidence scoring, and professional reporting
use anyhow::{Context, Result};
use async_trait::async_trait;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use crate::adapters::llm::{LLMAdapter, LLMRequest, LLMResponse, ModelConfig, Usage};

pub struct AnthropicAdapter {
    client: Client,
    config: ModelConfig,
    api_key: String,
    base_url: String,
}

#[derive(Serialize)]
struct AnthropicRequest {
    model: String,
    messages: Vec<Message>,
    max_tokens: usize,
    temperature: f32,
    system: String,
}

#[derive(Serialize, Deserialize)]
struct Message {
    role: String,
    content: String,
}

#[derive(Deserialize)]
struct AnthropicResponse {
    content: Vec<Content>,
    model: String,
    usage: AnthropicUsage,
}

#[derive(Deserialize)]
struct Content {
    text: String,
    #[serde(rename = "type")]
    content_type: String,
}

#[derive(Deserialize)]
struct AnthropicUsage {
    input_tokens: usize,
    output_tokens: usize,
}

impl AnthropicAdapter {
    pub fn new(config: ModelConfig) -> Result<Self> {
        let api_key = config.api_key.clone()
            .or_else(|| std::env::var("ANTHROPIC_API_KEY").ok())
            .context("Anthropic API key not found. Set ANTHROPIC_API_KEY environment variable or provide in config")?;
        
        let base_url = config.base_url.clone()
            .unwrap_or_else(|| "https://api.anthropic.com/v1".to_string());
        
        let client = Client::builder()
            .timeout(std::time::Duration::from_secs(60))
            .build()?;
        
        Ok(Self {
            client,
            config,
            api_key,
            base_url,
        })
    }
}

#[async_trait]
impl LLMAdapter for AnthropicAdapter {
    async fn complete(&self, request: LLMRequest) -> Result<LLMResponse> {
        let messages = vec![
            Message {
                role: "user".to_string(),
                content: request.user_prompt,
            },
        ];
        
        let anthropic_request = AnthropicRequest {
            model: self.config.model_name.clone(),
            messages,
            max_tokens: request.max_tokens.unwrap_or(self.config.max_tokens),
            temperature: request.temperature.unwrap_or(self.config.temperature),
            system: request.system_prompt,
        };
        
        let response = self.client
            .post(format!("{}/messages", self.base_url))
            .header("x-api-key", &self.api_key)
            .header("anthropic-version", "2023-06-01")
            .header("anthropic-beta", "messages-2023-12-15")
            .header("Content-Type", "application/json")
            .json(&anthropic_request)
            .send()
            .await
            .context("Failed to send request to Anthropic")?;
        
        if !response.status().is_success() {
            let error_text = response.text().await?;
            anyhow::bail!("Anthropic API error: {}", error_text);
        }
        
        let anthropic_response: AnthropicResponse = response.json().await
            .context("Failed to parse Anthropic response")?;
        
        let content = anthropic_response.content
            .first()
            .map(|c| {
                // Verify it's a text content type
                if c.content_type == "text" {
                    c.text.clone()
                } else {
                    format!("Unsupported content type: {}", c.content_type)
                }
            })
            .unwrap_or_default();
        
        Ok(LLMResponse {
            content,
            model: anthropic_response.model,
            usage: Some(Usage {
                prompt_tokens: anthropic_response.usage.input_tokens,
                completion_tokens: anthropic_response.usage.output_tokens,
                total_tokens: anthropic_response.usage.input_tokens + anthropic_response.usage.output_tokens,
            }),
        })
    }
    
    fn model_name(&self) -> &str {
        &self.config.model_name
    }
}