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    /// When true, temperature is never sent to the API (e.g., o1 models).
25    pub disable_temperature: bool,
26}
27
28impl std::fmt::Debug for LlmConfig {
29    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30        f.debug_struct("LlmConfig")
31            .field("provider", &self.provider)
32            .field("model", &self.model)
33            .field("api_key", &"[REDACTED]")
34            .field("base_url", &self.base_url)
35            .field("retry_config", &self.retry_config)
36            .field("temperature", &self.temperature)
37            .field("max_tokens", &self.max_tokens)
38            .field("thinking_budget", &self.thinking_budget)
39            .field("disable_temperature", &self.disable_temperature)
40            .finish()
41    }
42}
43
44impl LlmConfig {
45    pub fn new(
46        provider: impl Into<String>,
47        model: impl Into<String>,
48        api_key: impl Into<String>,
49    ) -> Self {
50        Self {
51            provider: provider.into(),
52            model: model.into(),
53            api_key: SecretString::new(api_key.into()),
54            base_url: None,
55            retry_config: None,
56            temperature: None,
57            max_tokens: None,
58            thinking_budget: None,
59            disable_temperature: false,
60        }
61    }
62
63    pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
64        self.base_url = Some(base_url.into());
65        self
66    }
67
68    pub fn with_retry_config(mut self, retry_config: RetryConfig) -> Self {
69        self.retry_config = Some(retry_config);
70        self
71    }
72
73    pub fn with_temperature(mut self, temperature: f32) -> Self {
74        self.temperature = Some(temperature);
75        self
76    }
77
78    pub fn with_max_tokens(mut self, max_tokens: usize) -> Self {
79        self.max_tokens = Some(max_tokens);
80        self
81    }
82
83    pub fn with_thinking_budget(mut self, budget: usize) -> Self {
84        self.thinking_budget = Some(budget);
85        self
86    }
87}
88
89/// Create LLM client with full configuration (supports custom base_url)
90pub fn create_client_with_config(config: LlmConfig) -> Arc<dyn LlmClient> {
91    let retry = config.retry_config.unwrap_or_default();
92    let api_key = config.api_key.expose().to_string();
93
94    match config.provider.as_str() {
95        "anthropic" | "claude" => {
96            let mut client = AnthropicClient::new(api_key, config.model)
97                .with_provider_name(config.provider.clone())
98                .with_retry_config(retry);
99            if let Some(base_url) = config.base_url {
100                client = client.with_base_url(base_url);
101            }
102            if !config.disable_temperature {
103                if let Some(temp) = config.temperature {
104                    client = client.with_temperature(temp);
105                }
106            }
107            if let Some(max) = config.max_tokens {
108                client = client.with_max_tokens(max);
109            }
110            if let Some(budget) = config.thinking_budget {
111                client = client.with_thinking_budget(budget);
112            }
113            Arc::new(client)
114        }
115        "openai" | "gpt" => {
116            let mut client = OpenAiClient::new(api_key, config.model)
117                .with_provider_name(config.provider.clone())
118                .with_retry_config(retry);
119            if let Some(base_url) = config.base_url {
120                client = client.with_base_url(base_url);
121            }
122            if !config.disable_temperature {
123                if let Some(temp) = config.temperature {
124                    client = client.with_temperature(temp);
125                }
126            }
127            if let Some(max) = config.max_tokens {
128                client = client.with_max_tokens(max);
129            }
130            Arc::new(client)
131        }
132        "glm" | "zhipu" | "bigmodel" => {
133            let mut client = OpenAiClient::new(api_key, config.model)
134                .with_provider_name(config.provider.clone())
135                .with_retry_config(retry)
136                .with_chat_completions_path("/api/paas/v4/chat/completions");
137            if let Some(base_url) = config.base_url {
138                client = client.with_base_url(base_url);
139            }
140            if !config.disable_temperature {
141                if let Some(temp) = config.temperature {
142                    client = client.with_temperature(temp);
143                }
144            }
145            if let Some(max) = config.max_tokens {
146                client = client.with_max_tokens(max);
147            }
148            Arc::new(client)
149        }
150        // OpenAI-compatible providers (deepseek, groq, together, ollama, etc.)
151        _ => {
152            tracing::info!(
153                "Using OpenAI-compatible client for provider '{}'",
154                config.provider
155            );
156            let mut client = OpenAiClient::new(api_key, config.model)
157                .with_provider_name(config.provider.clone())
158                .with_retry_config(retry);
159            if let Some(base_url) = config.base_url {
160                client = client.with_base_url(base_url);
161            }
162            if !config.disable_temperature {
163                if let Some(temp) = config.temperature {
164                    client = client.with_temperature(temp);
165                }
166            }
167            if let Some(max) = config.max_tokens {
168                client = client.with_max_tokens(max);
169            }
170            Arc::new(client)
171        }
172    }
173}