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
616use 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/// ```
3248pub 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) ]
5099mod 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}
0 commit comments