Skip to content

Commit d44b395

Browse files
committed
Refactor error handling and module structure for authentication clarity
1 parent a5fa349 commit d44b395

File tree

6 files changed

+149
-32
lines changed

6 files changed

+149
-32
lines changed

src/commit.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,11 @@ pub async fn generate(patch: String, remaining_tokens: usize, model: Model, sett
8080
bail!("Maximum token count must be greater than zero")
8181
}
8282

83-
// Try multi-step approach first
83+
// Try multi-step approach first (see multi_step_integration.rs for details)
84+
//
85+
// Error handling strategy:
86+
// - Authentication errors are propagated immediately using is_openai_auth_error()
87+
// - Other errors trigger fallback to local multi-step or single-step generation
8488
let max_length = settings
8589
.and_then(|s| s.max_commit_length)
8690
.or(config::APP_CONFIG.max_commit_length);
@@ -121,9 +125,7 @@ pub async fn generate(patch: String, remaining_tokens: usize, model: Model, sett
121125
Ok(message) => return Ok(openai::Response { response: message }),
122126
Err(e) => {
123127
// Check if it's an API key error
124-
if e.to_string().contains("invalid_api_key") ||
125-
e.to_string().contains("Incorrect API key") ||
126-
e.to_string().contains("OpenAI API authentication failed") {
128+
if crate::error::is_openai_auth_error(&e) {
127129
bail!("Invalid OpenAI API key. Please check your API key configuration.");
128130
}
129131
log::warn!("Multi-step generation with custom settings failed: {e}");
@@ -151,9 +153,7 @@ pub async fn generate(patch: String, remaining_tokens: usize, model: Model, sett
151153
Ok(message) => return Ok(openai::Response { response: message }),
152154
Err(e) => {
153155
// Check if it's an API key error
154-
if e.to_string().contains("invalid_api_key") ||
155-
e.to_string().contains("Incorrect API key") ||
156-
e.to_string().contains("OpenAI API authentication failed") {
156+
if crate::error::is_openai_auth_error(&e) {
157157
bail!("Invalid OpenAI API key. Please check your API key configuration.");
158158
}
159159
log::warn!("Multi-step generation failed: {e}");

src/error.rs

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
//! Error handling utilities for the git-ai CLI tool.
2+
//!
3+
//! This module provides helpers for detecting and handling specific error types,
4+
//! particularly authentication failures from the OpenAI API.
5+
6+
use anyhow::Error;
7+
8+
/// Checks if an error represents an OpenAI API authentication failure.
9+
///
10+
/// This function detects various authentication failure patterns including:
11+
/// - OpenAI-specific API key errors (invalid_api_key, incorrect API key)
12+
/// - Generic authentication/authorization failures
13+
/// - HTTP-level errors that typically indicate authentication issues when calling OpenAI
14+
///
15+
/// # Arguments
16+
///
17+
/// * `error` - The error to check
18+
///
19+
/// # Returns
20+
///
21+
/// `true` if the error appears to be an authentication failure, `false` otherwise
22+
///
23+
/// # Examples
24+
///
25+
/// ```
26+
/// use anyhow::anyhow;
27+
/// use ai::error::is_openai_auth_error;
28+
///
29+
/// let error = anyhow!("invalid_api_key: Incorrect API key provided");
30+
/// assert!(is_openai_auth_error(&error));
31+
/// ```
32+
pub fn is_openai_auth_error(error: &Error) -> bool {
33+
let msg = error.to_string().to_lowercase();
34+
35+
// OpenAI-specific API key errors
36+
msg.contains("invalid_api_key") ||
37+
msg.contains("incorrect api key") ||
38+
msg.contains("openai api authentication failed") ||
39+
40+
// Generic auth failures (scoped to avoid false positives)
41+
(msg.contains("authentication") && msg.contains("openai")) ||
42+
(msg.contains("unauthorized") && msg.contains("openai")) ||
43+
44+
// HTTP errors that typically indicate auth issues with OpenAI
45+
// This pattern catches connection issues when the API key is malformed
46+
(msg.contains("http error") && msg.contains("error sending request"))
47+
}
48+
49+
#[cfg(test)]
50+
mod tests {
51+
use super::*;
52+
use anyhow::anyhow;
53+
54+
#[test]
55+
fn test_detects_invalid_api_key() {
56+
let error = anyhow!("invalid_api_key: Incorrect API key provided");
57+
assert!(is_openai_auth_error(&error));
58+
}
59+
60+
#[test]
61+
fn test_detects_incorrect_api_key() {
62+
let error = anyhow!("Incorrect API key provided: sk-xxxxx");
63+
assert!(is_openai_auth_error(&error));
64+
}
65+
66+
#[test]
67+
fn test_detects_openai_auth_failed() {
68+
let error = anyhow!("OpenAI API authentication failed: http error");
69+
assert!(is_openai_auth_error(&error));
70+
}
71+
72+
#[test]
73+
fn test_detects_http_error_sending_request() {
74+
let error = anyhow!("http error: error sending request");
75+
assert!(is_openai_auth_error(&error));
76+
}
77+
78+
#[test]
79+
fn test_detects_openai_specific_auth() {
80+
let error = anyhow!("OpenAI authentication failed");
81+
assert!(is_openai_auth_error(&error));
82+
}
83+
84+
#[test]
85+
fn test_ignores_generic_auth_errors() {
86+
// Should not match generic auth errors without OpenAI context
87+
let error = anyhow!("Database authentication timeout");
88+
assert!(!is_openai_auth_error(&error));
89+
90+
let error = anyhow!("OAuth2 unauthorized redirect");
91+
assert!(!is_openai_auth_error(&error));
92+
}
93+
94+
#[test]
95+
fn test_ignores_unrelated_errors() {
96+
let error = anyhow!("File not found");
97+
assert!(!is_openai_auth_error(&error));
98+
}
99+
}

src/lib.rs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
pub mod commit;
22
pub mod config;
3-
pub mod hook;
4-
pub mod style;
5-
pub mod model;
3+
pub mod debug_output;
4+
pub mod error;
65
pub mod filesystem;
7-
pub mod openai;
8-
pub mod profiling;
96
pub mod function_calling;
7+
pub mod generation;
8+
pub mod hook;
9+
pub mod model;
1010
pub mod multi_step_analysis;
1111
pub mod multi_step_integration;
12+
pub mod openai;
13+
pub mod profiling;
1214
pub mod simple_multi_step;
13-
pub mod debug_output;
14-
pub mod generation;
15+
pub mod style;
1516

1617
// Re-exports
1718
pub use profiling::Profile;

src/multi_step_integration.rs

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,22 @@ pub struct ParsedFile {
2020
}
2121

2222
/// Main entry point for multi-step commit message generation
23+
///
24+
/// This function uses a sophisticated divide-and-conquer approach:
25+
/// 1. Parse the diff into individual files
26+
/// 2. Analyze each file concurrently using the OpenAI API
27+
/// 3. Calculate impact scores for each file
28+
/// 4. Generate multiple commit message candidates
29+
/// 5. Select the best message based on impact scores
30+
///
31+
/// # Error Handling
32+
///
33+
/// Authentication failures are detected early and propagated immediately to provide
34+
/// clear feedback to users. Uses [`crate::error::is_openai_auth_error`] to identify
35+
/// API key issues and other authentication problems.
36+
///
37+
/// Other errors (non-auth) are logged as warnings and processing continues with
38+
/// remaining files to maximize the chance of generating a useful commit message.
2339
pub async fn generate_commit_message_multi_step(
2440
client: &Client<OpenAIConfig>, model: &str, diff_content: &str, max_length: Option<usize>
2541
) -> Result<String> {
@@ -90,14 +106,7 @@ pub async fn generate_commit_message_multi_step(
90106
}
91107
Err(e) => {
92108
// Check if it's an API key or authentication error - if so, propagate it immediately
93-
let error_str = e.to_string();
94-
if error_str.contains("invalid_api_key") ||
95-
error_str.contains("Incorrect API key") ||
96-
error_str.contains("Invalid API key") ||
97-
error_str.contains("authentication") ||
98-
error_str.contains("unauthorized") ||
99-
// Detect HTTP errors that typically indicate auth issues when calling OpenAI
100-
(error_str.contains("http error") && error_str.contains("error sending request")) {
109+
if crate::error::is_openai_auth_error(&e) {
101110
return Err(anyhow::anyhow!("OpenAI API authentication failed: {}. Please check your API key configuration.", e));
102111
}
103112
log::warn!("Failed to analyze file {}: {}", file.path, e);

src/openai.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,16 @@ fn truncate_to_fit(text: &str, max_tokens: usize, model: &Model) -> Result<Strin
198198
}
199199

200200
/// Calls the OpenAI API with the provided configuration
201+
///
202+
/// Implements a fallback strategy:
203+
/// 1. Try multi-step analysis approach (default)
204+
/// 2. Fall back to single-step if multi-step fails (except for auth errors)
205+
///
206+
/// # Error Handling
207+
///
208+
/// Authentication errors detected by [`crate::error::is_openai_auth_error`] are
209+
/// propagated immediately without attempting fallback, ensuring users get clear
210+
/// feedback about API key issues rather than confusing secondary errors.
201211
pub async fn call_with_config(request: Request, config: OpenAIConfig) -> Result<Response> {
202212
profile!("OpenAI API call with custom config");
203213

@@ -209,9 +219,7 @@ pub async fn call_with_config(request: Request, config: OpenAIConfig) -> Result<
209219
Ok(message) => return Ok(Response { response: message }),
210220
Err(e) => {
211221
// Check if it's an API key error and propagate it
212-
if e.to_string().contains("invalid_api_key") ||
213-
e.to_string().contains("Incorrect API key") ||
214-
e.to_string().contains("OpenAI API authentication failed") {
222+
if crate::error::is_openai_auth_error(&e) {
215223
return Err(e);
216224
}
217225
log::warn!("Multi-step approach failed, falling back to single-step: {e}");

tests/api_key_error_test.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,15 @@ index 0000000..0000000
2525

2626
// Verify the behavior - it should return an error, not continue with other files
2727
assert!(result.is_err(), "Expected API key error to be propagated as error, not warning");
28-
28+
2929
let error_message = result.unwrap_err().to_string();
3030
println!("Actual error message: '{}'", error_message);
31-
32-
// Now it should properly detect authentication failures
31+
32+
// Verify it returns the specific authentication error message with actionable guidance
3333
assert!(
34-
error_message.contains("OpenAI API authentication failed") ||
35-
error_message.contains("API key"),
36-
"Expected error message to indicate authentication failure, got: {}",
34+
error_message.contains("OpenAI API authentication failed") &&
35+
error_message.contains("Please check your API key configuration"),
36+
"Expected specific authentication error message with guidance, got: {}",
3737
error_message
3838
);
39-
}
39+
}

0 commit comments

Comments
 (0)