use crate::agent::runloop::unified::state::CtrlCState;
use anyhow::Result;
use std::time::Duration;
use vtcode_core::llm::provider::LLMError;
use vtcode_core::utils::ansi::{AnsiRenderer, MessageStyle};
pub(crate) fn display_error(
renderer: &mut AnsiRenderer,
category: &str,
error: &anyhow::Error,
) -> Result<()> {
renderer.line_if_not_empty(MessageStyle::Output)?;
renderer.line(
MessageStyle::Error,
&format!("{}: {}", category, error_message_for_user(error)),
)?;
if let Some(llm_err) = error.downcast_ref::<LLMError>()
&& let Some(raw_body) = llm_error_raw_body(llm_err)
{
let human = llm_error_human_message(llm_err);
if raw_body != human {
renderer.line(MessageStyle::Error, &format!("Full response: {}", raw_body))?;
}
}
Ok(())
}
pub(crate) fn error_message_for_user(error: &anyhow::Error) -> String {
let message = error
.downcast_ref::<LLMError>()
.map(llm_error_human_message)
.unwrap_or_else(|| error.to_string());
sanitize_error_for_display(&message)
}
fn llm_error_human_message(error: &LLMError) -> String {
match error {
LLMError::Authentication { metadata, .. }
| LLMError::InvalidRequest { metadata, .. }
| LLMError::Network { metadata, .. }
| LLMError::Provider { metadata, .. } => {
if let Some(meta) = metadata
&& let Some(raw) = &meta.message
{
let human =
vtcode_core::llm::providers::error_handling::extract_human_error_message(raw);
if human != *raw {
return human;
}
}
match error {
LLMError::Authentication { message, .. }
| LLMError::InvalidRequest { message, .. }
| LLMError::Network { message, .. }
| LLMError::Provider { message, .. } => message.clone(),
_ => error.to_string(),
}
}
LLMError::RateLimit { metadata } => metadata
.as_ref()
.and_then(|meta| {
meta.message.as_ref().map(|raw| {
vtcode_core::llm::providers::error_handling::extract_human_error_message(raw)
})
})
.unwrap_or_else(|| error.to_string()),
}
}
fn llm_error_raw_body(error: &LLMError) -> Option<String> {
let metadata = match error {
LLMError::Authentication { metadata, .. }
| LLMError::InvalidRequest { metadata, .. }
| LLMError::Network { metadata, .. }
| LLMError::Provider { metadata, .. }
| LLMError::RateLimit { metadata, .. } => metadata.as_ref(),
};
metadata.and_then(|meta| meta.message.clone())
}
pub(crate) fn display_status(renderer: &mut AnsiRenderer, message: &str) -> Result<()> {
renderer.line(MessageStyle::Info, message)
}
pub(crate) fn should_continue_operation(ctrl_c_state: &CtrlCState) -> bool {
!ctrl_c_state.is_cancel_requested() && !ctrl_c_state.is_exit_requested()
}
pub(crate) fn sanitize_error_for_display(raw: &str) -> String {
let first_line = raw
.lines()
.find(|l| {
let trimmed = l.trim();
!trimmed.is_empty()
&& !trimmed.starts_with("Caused by:")
&& !trimmed.starts_with("Stack backtrace")
&& !trimmed.starts_with(" ")
})
.unwrap_or(raw)
.trim();
first_line.to_string()
}
pub(crate) fn calculate_backoff(attempt: usize, base_ms: u64, max_ms: u64) -> Duration {
let exp = 2_u64.saturating_pow(attempt.min(4) as u32);
let backoff_ms = base_ms.saturating_mul(exp);
Duration::from_millis(backoff_ms.min(max_ms))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sanitize_strips_anyhow_chain() {
let raw = "Failed to read config\n\nCaused by:\n 0: IO error\n 1: file not found";
let result = sanitize_error_for_display(raw);
assert_eq!(result, "Failed to read config");
}
#[test]
fn sanitize_preserves_full_colon_chain() {
let raw = "outer context: middle1: middle2: actual root cause";
let result = sanitize_error_for_display(raw);
assert_eq!(result, raw);
}
#[test]
fn sanitize_preserves_short_message() {
let raw = "connection refused";
let result = sanitize_error_for_display(raw);
assert_eq!(result, "connection refused");
}
#[test]
fn sanitize_preserves_long_error_body() {
let raw = format!(
"OpenAI Responses API error (status 400) Body: {{\"detail\":\"The 'gpt-5.4' model is not supported. {}\"}}",
"x".repeat(300)
);
let result = sanitize_error_for_display(&raw);
assert_eq!(result, raw);
}
#[test]
fn user_error_message_prefers_llm_rate_limit_metadata() {
let error = anyhow::Error::new(LLMError::RateLimit {
metadata: Some(vtcode_core::llm::provider::LLMErrorMetadata::new(
"OpenAI",
Some(429),
Some("rate_limit_error".to_string()),
Some("req_123".to_string()),
None,
None,
Some("Project rate limit exceeded for model gpt-5.2.".to_string()),
)),
});
assert_eq!(
error_message_for_user(&error),
"Project rate limit exceeded for model gpt-5.2."
);
}
#[test]
fn user_error_extracts_detail_field_from_json_body() {
let body = r#"{"detail":"The 'gpt-5.4' model is not supported with this method."}"#;
let error = anyhow::Error::new(LLMError::InvalidRequest {
message: "Invalid request".to_string(),
metadata: Some(vtcode_core::llm::provider::LLMErrorMetadata::new(
"OpenAI",
Some(400),
Some("invalid_request".to_string()),
None,
None,
None,
Some(body.to_string()),
)),
});
assert_eq!(
error_message_for_user(&error),
"The 'gpt-5.4' model is not supported with this method."
);
}
#[test]
fn user_error_extracts_nested_error_message_from_json_body() {
let body = r#"{"error":{"message":"Model not found","type":"invalid_request_error","code":"model_not_found"}}"#;
let error = anyhow::Error::new(LLMError::Provider {
message: "Provider error".to_string(),
metadata: Some(vtcode_core::llm::provider::LLMErrorMetadata::new(
"OpenAI",
Some(404),
None,
None,
None,
None,
Some(body.to_string()),
)),
});
assert_eq!(error_message_for_user(&error), "Model not found");
}
#[test]
fn raw_body_extracted_from_metadata() {
let body = r#"{"detail":"Some error detail"}"#;
let llm_err = LLMError::InvalidRequest {
message: "Invalid request".to_string(),
metadata: Some(vtcode_core::llm::provider::LLMErrorMetadata::new(
"OpenAI",
Some(400),
None,
None,
None,
None,
Some(body.to_string()),
)),
};
let raw = llm_error_raw_body(&llm_err);
assert_eq!(raw.as_deref(), Some(body));
}
#[test]
fn raw_body_none_when_no_metadata() {
let llm_err = LLMError::Provider {
message: "some error".to_string(),
metadata: None,
};
assert!(llm_error_raw_body(&llm_err).is_none());
}
}