Skip to main content

chronicle/provider/
mod.rs

1pub mod anthropic;
2pub mod claude_code;
3
4pub use anthropic::AnthropicProvider;
5pub use claude_code::ClaudeCodeProvider;
6
7use crate::error::ProviderError;
8use serde::{Deserialize, Serialize};
9
10/// Normalized LLM provider trait. MVP implements Anthropic only.
11pub trait LlmProvider: Send + Sync {
12    fn complete(&self, request: &CompletionRequest) -> Result<CompletionResponse, ProviderError>;
13    fn check_auth(&self) -> Result<AuthStatus, ProviderError>;
14    fn name(&self) -> &str;
15    fn model(&self) -> &str;
16}
17
18#[derive(Debug, Clone)]
19pub enum AuthStatus {
20    Valid,
21    Invalid(String),
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct CompletionRequest {
26    pub system: String,
27    pub messages: Vec<Message>,
28    pub tools: Vec<ToolDefinition>,
29    pub max_tokens: u32,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct CompletionResponse {
34    pub content: Vec<ContentBlock>,
35    pub stop_reason: StopReason,
36    pub usage: TokenUsage,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct Message {
41    pub role: Role,
42    pub content: Vec<ContentBlock>,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
46#[serde(rename_all = "snake_case")]
47pub enum Role {
48    User,
49    Assistant,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
53#[serde(tag = "type", rename_all = "snake_case")]
54pub enum ContentBlock {
55    Text {
56        text: String,
57    },
58    ToolUse {
59        id: String,
60        name: String,
61        input: serde_json::Value,
62    },
63    ToolResult {
64        tool_use_id: String,
65        content: String,
66        #[serde(skip_serializing_if = "Option::is_none")]
67        is_error: Option<bool>,
68    },
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
72#[serde(rename_all = "snake_case")]
73pub enum StopReason {
74    EndTurn,
75    ToolUse,
76    MaxTokens,
77    StopSequence,
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize, Default)]
81pub struct TokenUsage {
82    pub input_tokens: u32,
83    pub output_tokens: u32,
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct ToolDefinition {
88    pub name: String,
89    pub description: String,
90    pub input_schema: serde_json::Value,
91}
92
93/// Discover the best available provider.
94///
95/// Priority:
96/// 1. User-level config (~/.git-chronicle.toml)
97/// 2. Environment variable detection (ANTHROPIC_API_KEY)
98/// 3. Error: no provider configured
99pub fn discover_provider() -> Result<Box<dyn LlmProvider>, ProviderError> {
100    use crate::config::user_config::{ProviderType, UserConfig};
101
102    // 1. Check user-level config
103    if let Ok(Some(config)) = UserConfig::load() {
104        match config.provider.provider_type {
105            ProviderType::ClaudeCode => {
106                return Ok(Box::new(ClaudeCodeProvider::new(config.provider.model)));
107            }
108            ProviderType::Anthropic => {
109                let key_env = config
110                    .provider
111                    .api_key_env
112                    .unwrap_or_else(|| "ANTHROPIC_API_KEY".to_string());
113                if let Ok(api_key) = std::env::var(&key_env) {
114                    if !api_key.is_empty() {
115                        return Ok(Box::new(AnthropicProvider::new(
116                            api_key,
117                            config.provider.model,
118                        )));
119                    }
120                }
121                // Config says anthropic but key not found — fall through to env check
122            }
123            ProviderType::None => {
124                // Explicitly configured as none — fall through to env check
125            }
126        }
127    }
128
129    // 2. Fall back to env var detection
130    if let Ok(api_key) = std::env::var("ANTHROPIC_API_KEY") {
131        if !api_key.is_empty() {
132            return Ok(Box::new(AnthropicProvider::new(api_key, None)));
133        }
134    }
135
136    snafu::ensure!(false, crate::error::provider_error::NoCredentialsSnafu);
137    unreachable!()
138}