1use anyhow::{Context, Result};
2use async_openai::{
3 config::OpenAIConfig,
4 types::chat::{
5 ChatCompletionRequestSystemMessage, ChatCompletionRequestUserMessage,
6 CreateChatCompletionRequestArgs,
7 },
8 Client,
9};
10use async_trait::async_trait;
11
12use super::prompt::split_prompt;
13use super::AIProvider;
14use crate::config::accounts::AccountConfig;
15use crate::config::Config;
16use crate::utils::retry::retry_async;
17
18pub struct OpenAIProvider {
19 client: Client<OpenAIConfig>,
20 model: String,
21}
22
23impl OpenAIProvider {
24 pub fn new(config: &Config) -> Result<Self> {
25 let api_key = config
26 .api_key
27 .as_ref()
28 .context("OpenAI API key not configured.\nRun: rco config set RCO_API_KEY=<your_key>\nGet your API key from: https://platform.openai.com/api-keys")?;
29
30 let openai_config = OpenAIConfig::new().with_api_key(api_key).with_api_base(
31 config
32 .api_url
33 .as_deref()
34 .unwrap_or("https://api.openai.com/v1"),
35 );
36
37 let client = Client::with_config(openai_config);
38 let model = config.model.as_deref().unwrap_or("gpt-4o-mini").to_string();
39
40 Ok(Self { client, model })
41 }
42
43 #[allow(dead_code)]
45 pub fn from_account(account: &AccountConfig, api_key: &str, config: &Config) -> Result<Self> {
46 let openai_config = OpenAIConfig::new().with_api_key(api_key).with_api_base(
47 account
48 .api_url
49 .as_deref()
50 .or(config.api_url.as_deref())
51 .unwrap_or("https://api.openai.com/v1"),
52 );
53
54 let client = Client::with_config(openai_config);
55 let model = account
56 .model
57 .as_deref()
58 .or(config.model.as_deref())
59 .unwrap_or("gpt-4o-mini")
60 .to_string();
61
62 Ok(Self { client, model })
63 }
64}
65
66#[async_trait]
67impl AIProvider for OpenAIProvider {
68 async fn generate_commit_message(
69 &self,
70 diff: &str,
71 context: Option<&str>,
72 full_gitmoji: bool,
73 config: &Config,
74 ) -> Result<String> {
75 let (system_prompt, user_prompt) = split_prompt(diff, context, config, full_gitmoji);
76
77 let messages = vec![
78 ChatCompletionRequestSystemMessage::from(system_prompt).into(),
79 ChatCompletionRequestUserMessage::from(user_prompt).into(),
80 ];
81
82 let request = if self.model.contains("gpt-5-nano") {
84 CreateChatCompletionRequestArgs::default()
86 .model(&self.model)
87 .messages(messages)
88 .temperature(1.0)
89 .max_tokens(config.tokens_max_output.unwrap_or(500) as u16)
90 .build()?
91 } else {
92 CreateChatCompletionRequestArgs::default()
94 .model(&self.model)
95 .messages(messages)
96 .temperature(0.7)
97 .max_tokens(config.tokens_max_output.unwrap_or(500) as u16)
98 .build()?
99 };
100
101 let response = retry_async(|| async {
102 match self.client.chat().create(request.clone()).await {
103 Ok(resp) => Ok(resp),
104 Err(e) => {
105 let error_msg = e.to_string();
106 if error_msg.contains("401") || error_msg.contains("invalid_api_key") {
107 Err(anyhow::anyhow!("Invalid OpenAI API key. Please check your API key configuration."))
108 } else if error_msg.contains("insufficient_quota") {
109 Err(anyhow::anyhow!("OpenAI API quota exceeded. Please check your billing status."))
110 } else {
111 Err(anyhow::anyhow!(e).context("Failed to generate commit message from OpenAI"))
112 }
113 }
114 }
115 }).await.context("Failed to generate commit message from OpenAI after retries. Please check your internet connection and API configuration.")?;
116
117 let message = response
118 .choices
119 .first()
120 .and_then(|choice| choice.message.content.as_ref())
121 .context("OpenAI returned an empty response. The model may be overloaded - please try again.")?
122 .trim()
123 .to_string();
124
125 Ok(message)
126 }
127}
128
129#[allow(dead_code)]
132pub struct OpenAICompatibleProvider {
133 pub name: &'static str,
134 pub aliases: Vec<&'static str>,
135 pub default_api_url: &'static str,
136 pub default_model: Option<&'static str>,
137 pub compatible_providers: std::collections::HashMap<&'static str, &'static str>,
138}
139
140impl OpenAICompatibleProvider {
141 pub fn new() -> Self {
142 let mut compat = std::collections::HashMap::new();
143
144 compat.insert("deepseek", "https://api.deepseek.com/v1");
146 compat.insert("groq", "https://api.groq.com/openai/v1");
147 compat.insert("openrouter", "https://openrouter.ai/api/v1");
148 compat.insert("together", "https://api.together.ai/v1");
149 compat.insert("deepinfra", "https://api.deepinfra.com/v1/openai");
150 compat.insert("mistral", "https://api.mistral.ai/v1");
151 compat.insert("github-models", "https://models.inference.ai.azure.com");
152 compat.insert("fireworks", "https://api.fireworks.ai/v1");
153 compat.insert("fireworks-ai", "https://api.fireworks.ai/v1");
154 compat.insert("moonshot", "https://api.moonshot.cn/v1");
155 compat.insert("moonshot-ai", "https://api.moonshot.cn/v1");
156 compat.insert("dashscope", "https://dashscope.console.aliyuncs.com/api/v1");
157 compat.insert("alibaba", "https://dashscope.console.aliyuncs.com/api/v1");
158 compat.insert("qwen", "https://dashscope.console.aliyuncs.com/api/v1");
159 compat.insert(
160 "qwen-coder",
161 "https://dashscope.console.aliyuncs.com/api/v1",
162 );
163 compat.insert("codex", "https://api.openai.com/v1");
164
165 compat.insert("cohere", "https://api.cohere.com/v1");
173 compat.insert("cohere-ai", "https://api.cohere.com/v1");
174 compat.insert("ai21", "https://api.ai21.com/studio/v1");
175 compat.insert("ai21-labs", "https://api.ai21.com/studio/v1");
176 compat.insert("upstage", "https://api.upstage.ai/v1/solar");
177 compat.insert("upstage-ai", "https://api.upstage.ai/v1/solar");
178 compat.insert("solar", "https://api.upstage.ai/v1/solar");
179 compat.insert("solar-pro", "https://api.upstage.ai/v1/solar");
180
181 compat.insert("nebius", "https://api.studio.nebius.ai/v1");
185 compat.insert("nebius-ai", "https://api.studio.nebius.ai/v1");
186 compat.insert("nebius-studio", "https://api.studio.nebius.ai/v1");
187 compat.insert("ovh", "https://api.ovh.com/v1");
188 compat.insert("ovhcloud", "https://api.ovh.com/v1");
189 compat.insert("ovh-ai", "https://api.ovh.com/v1");
190 compat.insert("scaleway", "https://api.scaleway.ai/v1");
191 compat.insert("scaleway-ai", "https://api.scaleway.ai/v1");
192 compat.insert("friendli", "https://api.friendli.ai/v1");
193 compat.insert("friendli-ai", "https://api.friendli.ai/v1");
194 compat.insert("baseten", "https://api.baseten.co/v1");
195 compat.insert("baseten-ai", "https://api.baseten.co/v1");
196 compat.insert("chutes", "https://api.chutes.ai/v1");
197 compat.insert("chutes-ai", "https://api.chutes.ai/v1");
198 compat.insert("ionet", "https://api.io.net/v1");
199 compat.insert("io-net", "https://api.io.net/v1");
200 compat.insert("modelscope", "https://api.modelscope.cn/v1");
201 compat.insert("requesty", "https://api.requesty.ai/v1");
202 compat.insert("morph", "https://api.morph.so/v1");
203 compat.insert("morph-labs", "https://api.morph.so/v1");
204 compat.insert("synthetic", "https://api.syntheticai.com/v1");
205 compat.insert("nano-gpt", "https://api.nano-gpt.com/v1");
206 compat.insert("nanogpt", "https://api.nano-gpt.com/v1");
207 compat.insert("zenmux", "https://api.zenmux.com/v1");
208 compat.insert("v0", "https://api.v0.dev/v1");
209 compat.insert("v0-vercel", "https://api.v0.dev/v1");
210 compat.insert("iflowcn", "https://api.iflow.cn/v1");
211 compat.insert("venice", "https://api.venice.ai/v1");
212 compat.insert("venice-ai", "https://api.venice.ai/v1");
213 compat.insert("cortecs", "https://api.cortecs.ai/v1");
214 compat.insert("cortecs-ai", "https://api.cortecs.ai/v1");
215 compat.insert("kimi-coding", "https://api.moonshot.cn/v1");
216 compat.insert("abacus", "https://api.abacus.ai/v1");
217 compat.insert("abacus-ai", "https://api.abacus.ai/v1");
218 compat.insert("bailing", "https://api.bailing.ai/v1");
219 compat.insert("fastrouter", "https://api.fastrouter.ai/v1");
220 compat.insert("inference", "https://api.inference.net/v1");
221 compat.insert("inference-net", "https://api.inference.net/v1");
222 compat.insert("submodel", "https://api.submodel.ai/v1");
223 compat.insert("zai", "https://api.z.ai/v1");
224 compat.insert("zai-coding", "https://api.z.ai/v1");
225 compat.insert("zhipu-coding", "https://open.bigmodel.cn/api/paas/v4");
226 compat.insert("poe", "https://api.poe.com/v1");
227 compat.insert("poe-ai", "https://api.poe.com/v1");
228 compat.insert("cerebras", "https://api.cerebras.ai/v1");
229 compat.insert("cerebras-ai", "https://api.cerebras.ai/v1");
230 compat.insert("sambanova", "https://api.sambanova.ai/v1");
231 compat.insert("sambanova-ai", "https://api.sambanova.ai/v1");
232 compat.insert("novita", "https://api.novita.ai/v3/openai");
233 compat.insert("novita-ai", "https://api.novita.ai/v3/openai");
234 compat.insert("predibase", "https://api.predibase.com/v1");
235 compat.insert("tensorops", "https://api.tensorops.ai/v1");
236 compat.insert("hyperbolic", "https://api.hyperbolic.ai/v1");
237 compat.insert("hyperbolic-ai", "https://api.hyperbolic.ai/v1");
238 compat.insert("kluster", "https://api.kluster.ai/v1");
239 compat.insert("kluster-ai", "https://api.kluster.ai/v1");
240 compat.insert("lambda", "https://api.lambda.ai/v1");
241 compat.insert("lambda-labs", "https://api.lambda.ai/v1");
242 compat.insert("replicate", "https://api.replicate.com/v1");
243 compat.insert("targon", "https://api.targon.com/v1");
244 compat.insert("corcel", "https://api.corcel.io/v1");
245 compat.insert("cybernative", "https://api.cybernative.ai/v1");
246 compat.insert("cybernative-ai", "https://api.cybernative.ai/v1");
247 compat.insert("edgen", "https://api.edgen.co/v1");
248 compat.insert("gigachat", "https://api.gigachat.ru/v1");
249 compat.insert("gigachat-ai", "https://api.gigachat.ru/v1");
250 compat.insert("hydra", "https://api.hydraai.com/v1");
251 compat.insert("hydra-ai", "https://api.hydraai.com/v1");
252 compat.insert("jina", "https://api.jina.ai/v1");
253 compat.insert("jina-ai", "https://api.jina.ai/v1");
254 compat.insert("lingyi", "https://api.lingyiwanwu.com/v1");
255 compat.insert("lingyiwanwu", "https://api.lingyiwanwu.com/v1");
256 compat.insert("monica", "https://api.monica.ai/v1");
257 compat.insert("monica-ai", "https://api.monica.ai/v1");
258 compat.insert("pollinations", "https://api.pollinations.ai/v1");
259 compat.insert("pollinations-ai", "https://api.pollinations.ai/v1");
260 compat.insert("rawechat", "https://api.rawe.chat/v1");
261 compat.insert("shuttleai", "https://api.shuttleai.com/v1");
262 compat.insert("shuttle-ai", "https://api.shuttleai.com/v1");
263 compat.insert("teknium", "https://api.teknium.ai/v1");
264 compat.insert("theb", "https://api.theb.ai/v1");
265 compat.insert("theb-ai", "https://api.theb.ai/v1");
266 compat.insert("tryleap", "https://api.tryleap.ai/v1");
267 compat.insert("leap-ai", "https://api.tryleap.ai/v1");
268
269 compat.insert("lmstudio", "http://localhost:1234/v1");
273 compat.insert("lm-studio", "http://localhost:1234/v1");
274 compat.insert("llamacpp", "http://localhost:8080/v1");
275 compat.insert("llama-cpp", "http://localhost:8080/v1");
276 compat.insert("kobold", "http://localhost:5000/v1");
277 compat.insert("koboldcpp", "http://localhost:5000/v1");
278 compat.insert("textgen", "http://localhost:5000/v1");
279 compat.insert("text-generation", "http://localhost:5000/v1");
280 compat.insert("tabby", "http://localhost:8080/v1");
281
282 compat.insert("siliconflow", "https://api.siliconflow.cn/v1");
286 compat.insert("silicon-flow", "https://api.siliconflow.cn/v1");
287 compat.insert("zhipu", "https://open.bigmodel.cn/api/paas/v4");
288 compat.insert("zhipu-ai", "https://open.bigmodel.cn/api/paas/v4");
289 compat.insert("bigmodel", "https://open.bigmodel.cn/api/paas/v4");
290 compat.insert("minimax", "https://api.minimax.chat/v1");
291 compat.insert("minimax-ai", "https://api.minimax.chat/v1");
292 compat.insert("glm", "https://open.bigmodel.cn/api/paas/v4");
293 compat.insert("chatglm", "https://open.bigmodel.cn/api/paas/v4");
294 compat.insert("baichuan", "https://api.baichuan-ai.com/v1");
295 compat.insert("01-ai", "https://api.01.ai/v1");
296 compat.insert("yi", "https://api.01.ai/v1");
297
298 compat.insert("aimlapi", "https://api.aimlapi.com/v1");
302 compat.insert("ai-ml-api", "https://api.aimlapi.com/v1");
303
304 compat.insert("helicone", "https://gateway.helicone.ai/v1");
308 compat.insert("helicone-ai", "https://gateway.helicone.ai/v1");
309 compat.insert(
310 "workers-ai",
311 "https://api.cloudflare.com/client/v4/accounts/{account_id}/ai/v1",
312 );
313 compat.insert("cloudflare-ai", "https://gateway.ai.cloudflare.com/v1");
314 compat.insert("cloudflare-gateway", "https://gateway.ai.cloudflare.com/v1");
315 compat.insert("vercel-ai", "https://api.vercel.ai/v1");
316 compat.insert("vercel-gateway", "https://api.vercel.ai/v1");
317
318 compat.insert("302ai", "https://api.302.ai/v1");
322 compat.insert("302-ai", "https://api.302.ai/v1");
323 compat.insert("sap-ai", "https://api.ai.sap.com/v1");
324 compat.insert("sap-ai-core", "https://api.ai.sap.com/v1");
325
326 Self {
327 name: "openai",
328 aliases: vec!["openai"],
329 default_api_url: "https://api.openai.com/v1",
330 default_model: Some("gpt-4o-mini"),
331 compatible_providers: compat,
332 }
333 }
334}
335
336impl Default for OpenAICompatibleProvider {
337 fn default() -> Self {
338 Self::new()
339 }
340}
341
342impl super::registry::ProviderBuilder for OpenAICompatibleProvider {
343 fn name(&self) -> &'static str {
344 self.name
345 }
346
347 fn aliases(&self) -> Vec<&'static str> {
348 self.aliases.clone()
349 }
350
351 fn category(&self) -> super::registry::ProviderCategory {
352 super::registry::ProviderCategory::OpenAICompatible
353 }
354
355 fn create(&self, config: &Config) -> Result<Box<dyn super::AIProvider>> {
356 Ok(Box::new(OpenAIProvider::new(config)?))
357 }
358
359 fn requires_api_key(&self) -> bool {
360 true
361 }
362
363 fn default_model(&self) -> Option<&'static str> {
364 self.default_model
365 }
366}