Skip to main content

ripl/providers/
mod.rs

1use std::sync::mpsc;
2use std::sync::Arc;
3
4use crate::config::{resolve_provider_key, resolve_provider_name, Config};
5use serde::{Deserialize, Serialize};
6
7mod anthropic;
8mod openai;
9mod openrouter;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12pub enum Role {
13    System,
14    User,
15    Assistant,
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct Message {
20    pub role: Role,
21    pub content: String,
22}
23
24#[derive(Debug, Clone)]
25pub enum ApiResponse {
26    TokenChunk { token: String },
27    TurnComplete,
28    Error { message: String },
29    Exit,
30}
31
32pub trait Provider: Send + Sync + 'static {
33    fn stream(&self, messages: &[Message], tx: mpsc::Sender<ApiResponse>);
34    /// Called when the user issues a slash command not handled by the app.
35    /// Provider streams responses via `tx` as if it were a normal turn.
36    fn handle_command(&self, _cmd: &str, _tx: mpsc::Sender<ApiResponse>) {}
37
38    /// Optional: return extra help lines to display in response to `/help`.
39    /// Default implementation returns an empty slice.
40    fn help_lines(&self) -> &[&str] {
41        &[]
42    }
43}
44
45pub enum ProviderKind {
46    Anthropic,
47    OpenAi,
48    OpenRouter,
49}
50
51pub struct ProviderResolved {
52    pub kind: ProviderKind,
53    pub api_key: String,
54    pub model: String,
55}
56
57impl ProviderResolved {
58    pub fn kind_name(&self) -> &'static str {
59        match self.kind {
60            ProviderKind::Anthropic => "anthropic",
61            ProviderKind::OpenAi => "openai",
62            ProviderKind::OpenRouter => "openrouter",
63        }
64    }
65}
66
67pub fn resolve_provider(cfg: &Config) -> Option<ProviderResolved> {
68    let name = resolve_provider_name(cfg)?;
69    let api_key = resolve_provider_key(cfg)?;
70    let model = cfg
71        .provider
72        .as_ref()
73        .and_then(|p| p.model.clone())
74        .unwrap_or_else(|| {
75            match name.as_str() {
76                "anthropic" => "claude-sonnet-4-6",
77                "openai" => "gpt-4o-mini",
78                "openrouter" => "openai/gpt-4o-mini",
79                _ => "default",
80            }
81            .to_string()
82        });
83
84    let kind = match name.as_str() {
85        "anthropic" => ProviderKind::Anthropic,
86        "openai" => ProviderKind::OpenAi,
87        "openrouter" => ProviderKind::OpenRouter,
88        _ => return None,
89    };
90
91    Some(ProviderResolved { kind, api_key, model })
92}
93
94pub fn build_provider(cfg: &Config) -> Option<Arc<dyn Provider>> {
95    let resolved = resolve_provider(cfg)?;
96    let provider: Arc<dyn Provider> = match resolved.kind {
97        ProviderKind::Anthropic => Arc::new(anthropic::AnthropicProvider {
98            api_key: resolved.api_key,
99            model: resolved.model,
100        }),
101        ProviderKind::OpenAi => Arc::new(openai::OpenAiProvider {
102            api_key: resolved.api_key,
103            model: resolved.model,
104        }),
105        ProviderKind::OpenRouter => Arc::new(openrouter::OpenRouterProvider {
106            api_key: resolved.api_key,
107            model: resolved.model,
108        }),
109    };
110    Some(provider)
111}