Skip to content

Commit 5c1c5fa

Browse files
committed
Document OpenAI error structure and reformat error handling
1 parent d44b395 commit 5c1c5fa

File tree

3 files changed

+139
-29
lines changed

3 files changed

+139
-29
lines changed

src/error.rs

Lines changed: 117 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,31 @@
22
//!
33
//! This module provides helpers for detecting and handling specific error types,
44
//! particularly authentication failures from the OpenAI API.
5+
//!
6+
//! # OpenAI Error Structure
7+
//!
8+
//! According to the official async-openai documentation:
9+
//! - `OpenAIError::ApiError(ApiError)` contains structured error information from OpenAI
10+
//! - `ApiError` has fields: `message`, `type`, `param`, and `code`
11+
//! - Authentication errors have `code` set to `"invalid_api_key"`
12+
//! - `OpenAIError::Reqwest(Error)` contains HTTP-level errors (connection issues, etc.)
13+
//!
14+
//! Reference: https://docs.rs/async-openai/latest/async_openai/error/
515
616
use anyhow::Error;
17+
use async_openai::error::OpenAIError;
718

819
/// Checks if an error represents an OpenAI API authentication failure.
920
///
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
21+
/// This function detects authentication failures by checking for:
22+
/// 1. **Structured API errors** (preferred): Checks if the error contains an `OpenAIError::ApiError`
23+
/// with `code` field set to `"invalid_api_key"` - this is the official OpenAI error code
24+
/// for authentication failures.
25+
/// 2. **String-based fallback**: As a fallback, checks for authentication-related keywords in
26+
/// the error message for cases where the error has been wrapped or converted to a string.
27+
///
28+
/// This approach is based on the official OpenAI API error codes documentation and the
29+
/// async-openai Rust library structure.
1430
///
1531
/// # Arguments
1632
///
@@ -30,9 +46,42 @@ use anyhow::Error;
3046
/// assert!(is_openai_auth_error(&error));
3147
/// ```
3248
pub fn is_openai_auth_error(error: &Error) -> bool {
49+
// First, try to downcast to OpenAIError for accurate detection
50+
if let Some(openai_err) = error.downcast_ref::<OpenAIError>() {
51+
match openai_err {
52+
// Official OpenAI API error with structured error code
53+
OpenAIError::ApiError(api_err) => {
54+
// Check for the official invalid_api_key error code
55+
if api_err.code.as_deref() == Some("invalid_api_key") {
56+
return true;
57+
}
58+
// Also check for authentication-related types
59+
if let Some(err_type) = &api_err.r#type {
60+
if err_type.contains("authentication") || err_type.contains("invalid_request_error") {
61+
// For invalid_request_error, check if the message mentions API key
62+
if err_type == "invalid_request_error" && api_err.message.to_lowercase().contains("api key") {
63+
return true;
64+
}
65+
}
66+
}
67+
}
68+
// HTTP-level errors (connection failures, malformed requests, etc.)
69+
OpenAIError::Reqwest(_) => {
70+
// Reqwest errors for auth issues typically manifest as connection errors
71+
// when the API key format is completely invalid (e.g., "dl://BA7...")
72+
let msg = error.to_string().to_lowercase();
73+
if msg.contains("error sending request") || msg.contains("connection") {
74+
return true;
75+
}
76+
}
77+
_ => {}
78+
}
79+
}
80+
81+
// Fallback: String-based detection for wrapped errors
3382
let msg = error.to_string().to_lowercase();
3483

35-
// OpenAI-specific API key errors
84+
// OpenAI-specific API key errors (from API responses)
3685
msg.contains("invalid_api_key") ||
3786
msg.contains("incorrect api key") ||
3887
msg.contains("openai api authentication failed") ||
@@ -48,23 +97,76 @@ pub fn is_openai_auth_error(error: &Error) -> bool {
4897

4998
#[cfg(test)]
5099
mod tests {
51-
use super::*;
52100
use anyhow::anyhow;
101+
use async_openai::error::{ApiError, OpenAIError};
102+
103+
use super::*;
104+
105+
// Tests for structured OpenAIError detection (preferred method)
106+
107+
#[test]
108+
fn test_detects_structured_invalid_api_key() {
109+
let api_error = ApiError {
110+
message: "Incorrect API key provided: dl://BA7...".to_string(),
111+
r#type: Some("invalid_request_error".to_string()),
112+
param: None,
113+
code: Some("invalid_api_key".to_string())
114+
};
115+
let openai_error = OpenAIError::ApiError(api_error);
116+
let error: anyhow::Error = openai_error.into();
117+
assert!(is_openai_auth_error(&error));
118+
}
119+
120+
#[test]
121+
fn test_detects_invalid_request_with_api_key_message() {
122+
let api_error = ApiError {
123+
message: "You must provide a valid API key".to_string(),
124+
r#type: Some("invalid_request_error".to_string()),
125+
param: None,
126+
code: None
127+
};
128+
let openai_error = OpenAIError::ApiError(api_error);
129+
let error: anyhow::Error = openai_error.into();
130+
assert!(is_openai_auth_error(&error));
131+
}
132+
133+
#[test]
134+
fn test_detects_reqwest_error_sending_request() {
135+
// Simulate a wrapped reqwest error by using anyhow
136+
// In production, malformed API keys cause "error sending request" from reqwest
137+
let error = anyhow!("http error: error sending request");
138+
assert!(is_openai_auth_error(&error));
139+
}
140+
141+
#[test]
142+
fn test_ignores_structured_non_auth_error() {
143+
let api_error = ApiError {
144+
message: "Model not found".to_string(),
145+
r#type: Some("invalid_request_error".to_string()),
146+
param: Some("model".to_string()),
147+
code: Some("model_not_found".to_string())
148+
};
149+
let openai_error = OpenAIError::ApiError(api_error);
150+
let error: anyhow::Error = openai_error.into();
151+
assert!(!is_openai_auth_error(&error));
152+
}
153+
154+
// Tests for string-based fallback detection (for wrapped errors)
53155

54156
#[test]
55-
fn test_detects_invalid_api_key() {
157+
fn test_detects_invalid_api_key_string() {
56158
let error = anyhow!("invalid_api_key: Incorrect API key provided");
57159
assert!(is_openai_auth_error(&error));
58160
}
59161

60162
#[test]
61-
fn test_detects_incorrect_api_key() {
163+
fn test_detects_incorrect_api_key_string() {
62164
let error = anyhow!("Incorrect API key provided: sk-xxxxx");
63165
assert!(is_openai_auth_error(&error));
64166
}
65167

66168
#[test]
67-
fn test_detects_openai_auth_failed() {
169+
fn test_detects_openai_auth_failed_string() {
68170
let error = anyhow!("OpenAI API authentication failed: http error");
69171
assert!(is_openai_auth_error(&error));
70172
}
@@ -96,4 +198,10 @@ mod tests {
96198
let error = anyhow!("File not found");
97199
assert!(!is_openai_auth_error(&error));
98200
}
201+
202+
#[test]
203+
fn test_ignores_non_auth_openai_errors() {
204+
let error = anyhow!("OpenAI rate limit exceeded");
205+
assert!(!is_openai_auth_error(&error));
206+
}
99207
}

src/multi_step_integration.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,10 @@ pub async fn generate_commit_message_multi_step(
107107
Err(e) => {
108108
// Check if it's an API key or authentication error - if so, propagate it immediately
109109
if crate::error::is_openai_auth_error(&e) {
110-
return Err(anyhow::anyhow!("OpenAI API authentication failed: {}. Please check your API key configuration.", e));
110+
return Err(anyhow::anyhow!(
111+
"OpenAI API authentication failed: {}. Please check your API key configuration.",
112+
e
113+
));
111114
}
112115
log::warn!("Failed to analyze file {}: {}", file.path, e);
113116
// Continue with other files even if one fails

tests/api_key_error_test.rs

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@ use ai::multi_step_integration::generate_commit_message_multi_step;
44

55
#[tokio::test]
66
async fn test_invalid_api_key_propagates_error() {
7-
// Initialize logging to capture warnings
8-
let _ = env_logger::builder().is_test(true).try_init();
7+
// Initialize logging to capture warnings
8+
let _ = env_logger::builder().is_test(true).try_init();
99

10-
// Create a client with an invalid API key that matches the issue
11-
let config = OpenAIConfig::new().with_api_key("dl://BA7invalid_key_here");
12-
let client = Client::with_config(config);
10+
// Create a client with an invalid API key that matches the issue
11+
let config = OpenAIConfig::new().with_api_key("dl://BA7invalid_key_here");
12+
let client = Client::with_config(config);
1313

14-
let example_diff = r#"diff --git a/test.txt b/test.txt
14+
let example_diff = r#"diff --git a/test.txt b/test.txt
1515
new file mode 100644
1616
index 0000000..0000000
1717
--- /dev/null
@@ -20,20 +20,19 @@ index 0000000..0000000
2020
+Hello World
2121
"#;
2222

23-
// This should fail with an API key error, not log a warning and continue
24-
let result = generate_commit_message_multi_step(&client, "gpt-4o-mini", example_diff, Some(72)).await;
23+
// This should fail with an API key error, not log a warning and continue
24+
let result = generate_commit_message_multi_step(&client, "gpt-4o-mini", example_diff, Some(72)).await;
2525

26-
// Verify the behavior - it should return an error, not continue with other files
27-
assert!(result.is_err(), "Expected API key error to be propagated as error, not warning");
26+
// Verify the behavior - it should return an error, not continue with other files
27+
assert!(result.is_err(), "Expected API key error to be propagated as error, not warning");
2828

29-
let error_message = result.unwrap_err().to_string();
30-
println!("Actual error message: '{}'", error_message);
29+
let error_message = result.unwrap_err().to_string();
30+
println!("Actual error message: '{}'", error_message);
3131

32-
// Verify it returns the specific authentication error message with actionable guidance
33-
assert!(
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: {}",
37-
error_message
38-
);
32+
// Verify it returns the specific authentication error message with actionable guidance
33+
assert!(
34+
error_message.contains("OpenAI API authentication failed") && error_message.contains("Please check your API key configuration"),
35+
"Expected specific authentication error message with guidance, got: {}",
36+
error_message
37+
);
3938
}

0 commit comments

Comments
 (0)