use thiserror::Error;
pub type Result<T> = std::result::Result<T, VsCodeError>;
#[derive(Error, Debug)]
pub enum VsCodeError {
#[error("Failed to initialize client: {0}")]
ClientInit(String),
#[error("Proxy unavailable: {0}. Is copilot-api running on localhost:4141?")]
ProxyUnavailable(String),
#[error("Network error: {0}")]
Network(String),
#[error("Authentication failed: {0}")]
Authentication(String),
#[error("Rate limited. Try again later.")]
RateLimited,
#[error("Invalid request: {0}")]
InvalidRequest(String),
#[error("Service unavailable")]
ServiceUnavailable,
#[error("API error: {0}")]
ApiError(String),
#[error("Failed to decode response: {0}")]
Decode(String),
#[error("Stream error: {0}")]
Stream(String),
}
impl VsCodeError {
pub fn is_retryable(&self) -> bool {
matches!(
self,
VsCodeError::Network(_) | VsCodeError::RateLimited | VsCodeError::ServiceUnavailable
)
}
}
impl From<VsCodeError> for crate::error::LlmError {
fn from(err: VsCodeError) -> Self {
match err {
VsCodeError::ClientInit(msg) => Self::ConfigError(msg),
VsCodeError::ProxyUnavailable(msg) => Self::NetworkError(msg),
VsCodeError::Network(msg) => Self::NetworkError(msg),
VsCodeError::Authentication(msg) => Self::AuthError(msg),
VsCodeError::RateLimited => Self::RateLimited("Rate limit exceeded".to_string()),
VsCodeError::InvalidRequest(msg) => Self::InvalidRequest(msg),
VsCodeError::ServiceUnavailable => {
Self::NetworkError("Service unavailable".to_string())
}
VsCodeError::ApiError(msg) => Self::ApiError(msg),
VsCodeError::Decode(msg) => Self::ApiError(format!("Decode: {}", msg)),
VsCodeError::Stream(msg) => Self::ApiError(format!("Stream: {}", msg)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::LlmError;
#[test]
fn test_vscode_error_display_client_init() {
let err = VsCodeError::ClientInit("TLS handshake failed".to_string());
let msg = err.to_string();
assert!(msg.contains("Failed to initialize client"));
assert!(msg.contains("TLS handshake failed"));
}
#[test]
fn test_vscode_error_display_proxy_unavailable() {
let err = VsCodeError::ProxyUnavailable("connection refused".to_string());
let msg = err.to_string();
assert!(msg.contains("Proxy unavailable"));
assert!(msg.contains("connection refused"));
assert!(msg.contains("localhost:4141")); }
#[test]
fn test_vscode_error_display_network() {
let err = VsCodeError::Network("timeout after 30s".to_string());
assert_eq!(err.to_string(), "Network error: timeout after 30s");
}
#[test]
fn test_vscode_error_display_authentication() {
let err = VsCodeError::Authentication("token expired".to_string());
assert_eq!(err.to_string(), "Authentication failed: token expired");
}
#[test]
fn test_vscode_error_display_rate_limited() {
let err = VsCodeError::RateLimited;
assert_eq!(err.to_string(), "Rate limited. Try again later.");
}
#[test]
fn test_vscode_error_display_service_unavailable() {
let err = VsCodeError::ServiceUnavailable;
assert_eq!(err.to_string(), "Service unavailable");
}
#[test]
fn test_conversion_client_init_to_config_error() {
let vscode_err = VsCodeError::ClientInit("init failed".to_string());
let llm_err: LlmError = vscode_err.into();
match llm_err {
LlmError::ConfigError(msg) => assert_eq!(msg, "init failed"),
other => panic!("Expected ConfigError, got {:?}", other),
}
}
#[test]
fn test_conversion_proxy_unavailable_to_network_error() {
let vscode_err = VsCodeError::ProxyUnavailable("refused".to_string());
let llm_err: LlmError = vscode_err.into();
match llm_err {
LlmError::NetworkError(msg) => assert_eq!(msg, "refused"),
other => panic!("Expected NetworkError, got {:?}", other),
}
}
#[test]
fn test_conversion_network_to_network_error() {
let vscode_err = VsCodeError::Network("dns lookup failed".to_string());
let llm_err: LlmError = vscode_err.into();
match llm_err {
LlmError::NetworkError(msg) => assert_eq!(msg, "dns lookup failed"),
other => panic!("Expected NetworkError, got {:?}", other),
}
}
#[test]
fn test_conversion_authentication_to_auth_error() {
let vscode_err = VsCodeError::Authentication("invalid token".to_string());
let llm_err: LlmError = vscode_err.into();
match llm_err {
LlmError::AuthError(msg) => assert_eq!(msg, "invalid token"),
other => panic!("Expected AuthError, got {:?}", other),
}
}
#[test]
fn test_conversion_rate_limited() {
let vscode_err = VsCodeError::RateLimited;
let llm_err: LlmError = vscode_err.into();
match llm_err {
LlmError::RateLimited(msg) => assert!(msg.contains("Rate limit")),
other => panic!("Expected RateLimited, got {:?}", other),
}
}
#[test]
fn test_conversion_invalid_request() {
let vscode_err = VsCodeError::InvalidRequest("missing model".to_string());
let llm_err: LlmError = vscode_err.into();
match llm_err {
LlmError::InvalidRequest(msg) => assert_eq!(msg, "missing model"),
other => panic!("Expected InvalidRequest, got {:?}", other),
}
}
#[test]
fn test_conversion_service_unavailable() {
let vscode_err = VsCodeError::ServiceUnavailable;
let llm_err: LlmError = vscode_err.into();
match llm_err {
LlmError::NetworkError(msg) => assert!(msg.contains("unavailable")),
other => panic!("Expected NetworkError, got {:?}", other),
}
}
#[test]
fn test_conversion_api_error() {
let vscode_err = VsCodeError::ApiError("internal server error".to_string());
let llm_err: LlmError = vscode_err.into();
match llm_err {
LlmError::ApiError(msg) => assert_eq!(msg, "internal server error"),
other => panic!("Expected ApiError, got {:?}", other),
}
}
#[test]
fn test_conversion_decode_error() {
let vscode_err = VsCodeError::Decode("invalid JSON".to_string());
let llm_err: LlmError = vscode_err.into();
match llm_err {
LlmError::ApiError(msg) => {
assert!(msg.contains("Decode"));
assert!(msg.contains("invalid JSON"));
}
other => panic!("Expected ApiError, got {:?}", other),
}
}
#[test]
fn test_conversion_stream_error() {
let vscode_err = VsCodeError::Stream("connection reset".to_string());
let llm_err: LlmError = vscode_err.into();
match llm_err {
LlmError::ApiError(msg) => {
assert!(msg.contains("Stream"));
assert!(msg.contains("connection reset"));
}
other => panic!("Expected ApiError, got {:?}", other),
}
}
#[test]
fn test_is_retryable_network_error() {
let err = VsCodeError::Network("connection timeout".to_string());
assert!(err.is_retryable(), "Network errors should be retryable");
}
#[test]
fn test_is_retryable_rate_limited() {
let err = VsCodeError::RateLimited;
assert!(
err.is_retryable(),
"Rate limited errors should be retryable"
);
}
#[test]
fn test_is_retryable_service_unavailable() {
let err = VsCodeError::ServiceUnavailable;
assert!(
err.is_retryable(),
"Service unavailable should be retryable"
);
}
#[test]
fn test_is_not_retryable_auth_error() {
let err = VsCodeError::Authentication("token expired".to_string());
assert!(!err.is_retryable(), "Auth errors should not be retryable");
}
#[test]
fn test_is_not_retryable_invalid_request() {
let err = VsCodeError::InvalidRequest("missing model".to_string());
assert!(
!err.is_retryable(),
"Invalid request should not be retryable"
);
}
#[test]
fn test_is_not_retryable_client_init() {
let err = VsCodeError::ClientInit("TLS failed".to_string());
assert!(
!err.is_retryable(),
"Client init errors should not be retryable"
);
}
#[test]
fn test_is_not_retryable_api_error() {
let err = VsCodeError::ApiError("internal error".to_string());
assert!(!err.is_retryable(), "API errors should not be retryable");
}
#[test]
fn test_is_not_retryable_decode_error() {
let err = VsCodeError::Decode("invalid JSON".to_string());
assert!(!err.is_retryable(), "Decode errors should not be retryable");
}
}