1use anyhow::{anyhow, bail, Result};
2use maplit::hashmap;
3use mustache;
4use async_openai::Client;
5
6use crate::{config, debug_output, openai, profile};
7use crate::model::Model;
8use crate::config::AppConfig;
9use crate::multi_step_integration::{generate_commit_message_local, generate_commit_message_multi_step};
10
11const INSTRUCTION_TEMPLATE: &str = include_str!("../resources/prompt.md");
13
14#[doc(hidden)]
22pub fn get_instruction_template() -> Result<String> {
23 profile!("Generate instruction template");
24 let max_length = config::APP_CONFIG
25 .max_commit_length
26 .unwrap_or(72)
27 .to_string();
28 let template = mustache::compile_str(INSTRUCTION_TEMPLATE)
29 .map_err(|e| anyhow!("Template compilation error: {}", e))?
30 .render_to_string(&hashmap! {
31 "max_length" => max_length
32 })
33 .map_err(|e| anyhow!("Template rendering error: {}", e))?;
34 Ok(template)
35}
36
37#[doc(hidden)]
49pub fn create_commit_request(diff: String, max_tokens: usize, model: Model) -> Result<openai::Request> {
50 profile!("Prepare OpenAI request");
51 let template = get_instruction_template()?;
52 Ok(openai::Request {
53 system: template,
54 prompt: diff,
55 max_tokens: max_tokens.try_into().unwrap_or(u16::MAX),
56 model
57 })
58}
59
60pub async fn generate(patch: String, remaining_tokens: usize, model: Model, settings: Option<&AppConfig>) -> Result<openai::Response> {
77 profile!("Generate commit message");
78
79 if remaining_tokens == 0 {
80 bail!("Maximum token count must be greater than zero")
81 }
82
83 let max_length = settings
85 .and_then(|s| s.max_commit_length)
86 .or(config::APP_CONFIG.max_commit_length);
87
88 let has_valid_api_key = if let Some(custom_settings) = settings {
90 custom_settings
91 .openai_api_key
92 .as_ref()
93 .map(|key| !key.is_empty() && key != "<PLACE HOLDER FOR YOUR API KEY>")
94 .unwrap_or(false)
95 } else {
96 config::APP_CONFIG
98 .openai_api_key
99 .as_ref()
100 .map(|key| !key.is_empty() && key != "<PLACE HOLDER FOR YOUR API KEY>")
101 .unwrap_or(false)
102 || std::env::var("OPENAI_API_KEY")
103 .map(|key| !key.is_empty())
104 .unwrap_or(false)
105 };
106
107 if !has_valid_api_key {
108 bail!("OpenAI API key not configured. Please set your API key using:\n git-ai config set openai-api-key <your-key>\nor set the OPENAI_API_KEY environment variable.");
109 }
110
111 if let Some(custom_settings) = settings {
113 if let Some(api_key) = &custom_settings.openai_api_key {
114 if !api_key.is_empty() && api_key != "<PLACE HOLDER FOR YOUR API KEY>" {
115 match openai::create_openai_config(custom_settings) {
116 Ok(config) => {
117 let client = Client::with_config(config);
118 let model_str = model.to_string();
119
120 match generate_commit_message_multi_step(&client, &model_str, &patch, max_length).await {
121 Ok(message) => return Ok(openai::Response { response: message }),
122 Err(e) => {
123 if e.to_string().contains("invalid_api_key") || e.to_string().contains("Incorrect API key") {
125 bail!("Invalid OpenAI API key. Please check your API key configuration.");
126 }
127 log::warn!("Multi-step generation with custom settings failed: {e}");
128 if let Some(session) = debug_output::debug_session() {
129 session.set_multi_step_error(e.to_string());
130 }
131 }
132 }
133 }
134 Err(e) => {
135 return Err(e);
137 }
138 }
139 }
140 }
141 } else {
142 let client = match openai::create_openai_config(&config::APP_CONFIG) {
149 Ok(config) => Some(Client::with_config(config)),
150 Err(_) => match std::env::var("OPENAI_API_KEY") {
151 Ok(key) if !key.is_empty() => Some(Client::new()),
152 _ => None
153 }
154 };
155
156 if let Some(client) = client {
157 let model_str = model.to_string();
158
159 match generate_commit_message_multi_step(&client, &model_str, &patch, max_length).await {
160 Ok(message) => return Ok(openai::Response { response: message }),
161 Err(e) => {
162 if e.to_string().contains("invalid_api_key") || e.to_string().contains("Incorrect API key") {
164 bail!("Invalid OpenAI API key. Please check your API key configuration.");
165 }
166 log::warn!("Multi-step generation failed: {e}");
167 if let Some(session) = debug_output::debug_session() {
168 session.set_multi_step_error(e.to_string());
169 }
170 }
171 }
172 }
173 }
174
175 match generate_commit_message_local(&patch, max_length) {
177 Ok(message) => return Ok(openai::Response { response: message }),
178 Err(e) => {
179 log::warn!("Local multi-step generation failed: {e}");
180 }
181 }
182
183 if let Some(session) = debug_output::debug_session() {
185 session.set_single_step_success(true);
186 }
187
188 let request = create_commit_request(patch, remaining_tokens, model)?;
190
191 match settings {
193 Some(custom_settings) => {
194 match openai::create_openai_config(custom_settings) {
196 Ok(config) => openai::call_with_config(request, config).await,
197 Err(e) => Err(e)
198 }
199 }
200 None => {
201 openai::call(request).await
203 }
204 }
205}
206
207pub fn token_used(model: &Model) -> Result<usize> {
208 get_instruction_token_count(model)
209}
210
211pub fn get_instruction_token_count(model: &Model) -> Result<usize> {
219 profile!("Calculate instruction tokens");
220 let template = get_instruction_template()?;
221 model.count_tokens(&template)
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227
228 #[tokio::test]
229 async fn test_missing_api_key_error() {
230 let settings = AppConfig {
232 openai_api_key: None,
233 openai_base_url: None,
234 model: Some("gpt-4.1-mini".to_string()),
235 max_tokens: Some(1024),
236 max_commit_length: Some(72),
237 timeout: Some(30)
238 };
239
240 let original_key = std::env::var("OPENAI_API_KEY").ok();
242 std::env::remove_var("OPENAI_API_KEY");
243
244 let result = generate(
246 "diff --git a/test.txt b/test.txt\n+Hello World".to_string(),
247 1024,
248 Model::GPT41Mini,
249 Some(&settings)
250 )
251 .await;
252
253 if let Some(key) = original_key {
255 std::env::set_var("OPENAI_API_KEY", key);
256 }
257
258 assert!(result.is_err());
259 let error_message = result.unwrap_err().to_string();
260 assert!(
261 error_message.contains("OpenAI API key not configured"),
262 "Expected error message about missing API key, got: {}",
263 error_message
264 );
265 }
266
267 #[tokio::test]
268 async fn test_invalid_api_key_error() {
269 let settings = AppConfig {
271 openai_api_key: Some("<PLACE HOLDER FOR YOUR API KEY>".to_string()),
272 openai_base_url: None,
273 model: Some("gpt-4.1-mini".to_string()),
274 max_tokens: Some(1024),
275 max_commit_length: Some(72),
276 timeout: Some(30)
277 };
278
279 let result = generate(
281 "diff --git a/test.txt b/test.txt\n+Hello World".to_string(),
282 1024,
283 Model::GPT41Mini,
284 Some(&settings)
285 )
286 .await;
287
288 assert!(result.is_err());
289 let error_message = result.unwrap_err().to_string();
290 assert!(
291 error_message.contains("OpenAI API key not configured"),
292 "Expected error message about invalid API key, got: {}",
293 error_message
294 );
295 }
296}