Skip to main content

a3s_code_core/llm/
factory.rs

1//! LLM client factory
2
3use super::anthropic::AnthropicClient;
4use super::openai::OpenAiClient;
5use super::types::SecretString;
6use super::LlmClient;
7use crate::retry::RetryConfig;
8use std::sync::Arc;
9
10/// LLM client configuration
11#[derive(Clone, Default)]
12pub struct LlmConfig {
13    pub provider: String,
14    pub model: String,
15    pub api_key: SecretString,
16    pub base_url: Option<String>,
17    pub retry_config: Option<RetryConfig>,
18    /// Sampling temperature (0.0–1.0). None uses the provider default.
19    pub temperature: Option<f32>,
20    /// Maximum tokens to generate. None uses the client default.
21    pub max_tokens: Option<usize>,
22    /// Extended thinking budget in tokens (Anthropic only).
23    pub thinking_budget: Option<usize>,
24}
25
26impl std::fmt::Debug for LlmConfig {
27    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        f.debug_struct("LlmConfig")
29            .field("provider", &self.provider)
30            .field("model", &self.model)
31            .field("api_key", &"[REDACTED]")
32            .field("base_url", &self.base_url)
33            .field("retry_config", &self.retry_config)
34            .field("temperature", &self.temperature)
35            .field("max_tokens", &self.max_tokens)
36            .field("thinking_budget", &self.thinking_budget)
37            .finish()
38    }
39}
40
41impl LlmConfig {
42    pub fn new(
43        provider: impl Into<String>,
44        model: impl Into<String>,
45        api_key: impl Into<String>,
46    ) -> Self {
47        Self {
48            provider: provider.into(),
49            model: model.into(),
50            api_key: SecretString::new(api_key.into()),
51            base_url: None,
52            retry_config: None,
53            temperature: None,
54            max_tokens: None,
55            thinking_budget: None,
56        }
57    }
58
59    pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
60        self.base_url = Some(base_url.into());
61        self
62    }
63
64    pub fn with_retry_config(mut self, retry_config: RetryConfig) -> Self {
65        self.retry_config = Some(retry_config);
66        self
67    }
68
69    pub fn with_temperature(mut self, temperature: f32) -> Self {
70        self.temperature = Some(temperature);
71        self
72    }
73
74    pub fn with_max_tokens(mut self, max_tokens: usize) -> Self {
75        self.max_tokens = Some(max_tokens);
76        self
77    }
78
79    pub fn with_thinking_budget(mut self, budget: usize) -> Self {
80        self.thinking_budget = Some(budget);
81        self
82    }
83}
84
85/// Create LLM client with full configuration (supports custom base_url)
86pub fn create_client_with_config(config: LlmConfig) -> Arc<dyn LlmClient> {
87    let retry = config.retry_config.unwrap_or_default();
88    let api_key = config.api_key.expose().to_string();
89
90    match config.provider.as_str() {
91        "anthropic" | "claude" => {
92            let mut client = AnthropicClient::new(api_key, config.model).with_retry_config(retry);
93            if let Some(base_url) = config.base_url {
94                client = client.with_base_url(base_url);
95            }
96            if let Some(temp) = config.temperature {
97                client = client.with_temperature(temp);
98            }
99            if let Some(max) = config.max_tokens {
100                client = client.with_max_tokens(max);
101            }
102            if let Some(budget) = config.thinking_budget {
103                client = client.with_thinking_budget(budget);
104            }
105            Arc::new(client)
106        }
107        "openai" | "gpt" => {
108            let mut client = OpenAiClient::new(api_key, config.model).with_retry_config(retry);
109            if let Some(base_url) = config.base_url {
110                client = client.with_base_url(base_url);
111            }
112            if let Some(temp) = config.temperature {
113                client = client.with_temperature(temp);
114            }
115            if let Some(max) = config.max_tokens {
116                client = client.with_max_tokens(max);
117            }
118            Arc::new(client)
119        }
120        // OpenAI-compatible providers (deepseek, groq, together, ollama, etc.)
121        _ => {
122            tracing::info!(
123                "Using OpenAI-compatible client for provider '{}'",
124                config.provider
125            );
126            let mut client = OpenAiClient::new(api_key, config.model).with_retry_config(retry);
127            if let Some(base_url) = config.base_url {
128                client = client.with_base_url(base_url);
129            }
130            if let Some(temp) = config.temperature {
131                client = client.with_temperature(temp);
132            }
133            if let Some(max) = config.max_tokens {
134                client = client.with_max_tokens(max);
135            }
136            Arc::new(client)
137        }
138    }
139}