#![allow(dead_code)]
use colored::*;
use std::fmt;
use std::time::Duration;
const MAX_RETRIES: u32 = 3;
const INITIAL_BACKOFF_MS: u64 = 1000;
const MAX_BACKOFF_MS: u64 = 30000;
#[derive(Debug, Clone)]
pub enum ApiError {
MissingApiKey,
Unauthorized,
Forbidden(String),
NotFound(String),
RateLimited,
ServerError(u16, String),
NetworkError(String),
Timeout,
Other(String),
}
impl std::error::Error for ApiError {}
impl fmt::Display for ApiError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ApiError::MissingApiKey => write!(f, "API key is required"),
ApiError::Unauthorized => write!(f, "Invalid API key"),
ApiError::Forbidden(feature) => write!(f, "Permission denied: {}", feature),
ApiError::NotFound(resource) => write!(f, "Not found: {}", resource),
ApiError::RateLimited => write!(f, "Rate limited"),
ApiError::ServerError(code, msg) => write!(f, "Server error {}: {}", code, msg),
ApiError::NetworkError(msg) => write!(f, "Network error: {}", msg),
ApiError::Timeout => write!(f, "Request timed out"),
ApiError::Other(msg) => write!(f, "Error: {}", msg),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Retryable {
Yes,
No,
YesWithBackoff,
}
impl ApiError {
pub fn retryable(&self) -> Retryable {
match self {
ApiError::MissingApiKey => Retryable::No,
ApiError::Unauthorized => Retryable::No,
ApiError::Forbidden(_) => Retryable::No,
ApiError::NotFound(_) => Retryable::No,
ApiError::RateLimited => Retryable::YesWithBackoff,
ApiError::ServerError(_, _) => Retryable::Yes,
ApiError::NetworkError(_) => Retryable::Yes,
ApiError::Timeout => Retryable::Yes,
ApiError::Other(_) => Retryable::Yes,
}
}
pub fn retry_after(&self) -> Option<Duration> {
match self {
ApiError::RateLimited => Some(Duration::from_secs(60)),
ApiError::ServerError(_, _) => Some(Duration::from_secs(5)),
_ => None,
}
}
}
pub fn is_missing_api_key(err: &anyhow::Error) -> bool {
let error_str = err.to_string().to_lowercase();
error_str.contains("api key") && (error_str.contains("required") || error_str.contains("none"))
}
pub fn is_unauthorized(err: &anyhow::Error) -> bool {
let error_str = err.to_string().to_lowercase();
error_str.contains("401")
|| error_str.contains("unauthorized")
|| error_str.contains("invalid api key")
}
pub fn is_forbidden(err: &anyhow::Error) -> bool {
let error_str = err.to_string().to_lowercase();
error_str.contains("403")
|| error_str.contains("forbidden")
|| error_str.contains("permission denied")
}
pub fn is_not_found(err: &anyhow::Error) -> bool {
let error_str = err.to_string().to_lowercase();
error_str.contains("404") || error_str.contains("not found")
}
pub fn is_rate_limited(err: &anyhow::Error) -> bool {
let error_str = err.to_string().to_lowercase();
error_str.contains("429")
|| error_str.contains("rate limit")
|| error_str.contains("too many requests")
}
pub fn is_server_error(err: &anyhow::Error) -> bool {
let error_str = err.to_string().to_lowercase();
error_str.contains("500")
|| error_str.contains("502")
|| error_str.contains("503")
|| error_str.contains("504")
|| error_str.contains("server error")
}
pub fn is_network_error(err: &anyhow::Error) -> bool {
let error_str = err.to_string().to_lowercase();
error_str.contains("connection")
|| error_str.contains("network")
|| error_str.contains("timeout")
|| error_str.contains("connect")
}
pub fn parse_api_error(err: &anyhow::Error) -> ApiError {
if is_missing_api_key(err) {
ApiError::MissingApiKey
} else if is_unauthorized(err) {
ApiError::Unauthorized
} else if is_forbidden(err) {
ApiError::Forbidden("Unknown feature".to_string())
} else if is_not_found(err) {
ApiError::NotFound("Unknown resource".to_string())
} else if is_rate_limited(err) {
ApiError::RateLimited
} else if is_server_error(err) {
ApiError::ServerError(500, err.to_string())
} else if is_network_error(err) {
ApiError::NetworkError(err.to_string())
} else {
ApiError::Other(err.to_string())
}
}
pub fn is_retryable(err: &anyhow::Error) -> Retryable {
parse_api_error(err).retryable()
}
pub fn print_api_error(err: &anyhow::Error) {
if is_missing_api_key(err) {
eprintln!("{}", "Error: API key is required".red());
eprintln!();
eprintln!("{}", "To get an API key:".yellow());
eprintln!(" 1. Go to https://elevenlabs.io/app/settings/api-keys");
eprintln!(" 2. Create a new API key");
eprintln!(" 3. Copy it and set it as ELEVENLABS_API_KEY environment variable");
eprintln!();
eprintln!("{}", "Or use the --api-key flag:".yellow());
eprintln!(" elevenlabs --api-key YOUR_API_KEY ...");
} else if is_unauthorized(err) {
eprintln!("{}", "Error: Invalid API key".red());
eprintln!();
eprintln!("{}", "Your API key may be invalid or expired.".yellow());
eprintln!("Get a new key from: https://elevenlabs.io/app/settings/api-keys");
} else if is_forbidden(err) {
eprintln!("{}", "Error: Permission denied".red());
eprintln!();
eprintln!(
"{}",
"Your API key doesn't have permission for this feature.".yellow()
);
eprintln!("Check your subscription tier at: https://elevenlabs.io/app/settings");
eprintln!();
eprintln!("Some features require:");
eprintln!(" - Paid subscription (for professional voice cloning)");
eprintln!(" - Business subscription (for agents, phone, etc.)");
eprintln!("See: https://elevenlabs.io/pricing");
} else if is_not_found(err) {
eprintln!("{}", "Error: Resource not found".red());
eprintln!("The requested resource doesn't exist or has been deleted.");
} else if is_rate_limited(err) {
eprintln!("{}", "Error: Rate limited".red());
eprintln!();
eprintln!("{}", "You have made too many requests.".yellow());
eprintln!("Please wait at least 60 seconds before trying again.");
eprintln!();
eprintln!("See: https://elevenlabs.io/docs/api-reference/rate-limits");
} else if is_server_error(err) {
eprintln!("{}", "Error: Server error".red());
eprintln!();
eprintln!("{}", "ElevenLabs servers are experiencing issues.".yellow());
eprintln!("This is usually temporary. Try again in a few moments.");
} else if is_network_error(err) {
eprintln!("{}", "Error: Network error".red());
eprintln!("Could not connect to ElevenLabs API.");
eprintln!("Check your internet connection and try again.");
} else {
eprintln!("{}", format!("Error: {}", err).red());
}
}
pub fn print_retry_error(err: &anyhow::Error, attempt: u32, max_retries: u32) {
let retryable = is_retryable(err);
match retryable {
Retryable::YesWithBackoff => {
eprintln!(
"{}",
format!("Rate limited (attempt {}/{})", attempt, max_retries).yellow()
);
eprintln!("Waiting before retry...");
}
Retryable::Yes => {
eprintln!(
"{}",
format!("Transient error (attempt {}/{})", attempt, max_retries).yellow()
);
eprintln!("Retrying...");
}
Retryable::No => {
print_api_error(err);
}
}
}
pub fn check_api_key(api_key: &Option<String>) -> Option<&String> {
match api_key {
Some(key) if !key.is_empty() => Some(key),
_ => {
eprintln!("{}", "Error: API key is required".red());
eprintln!();
eprintln!("{}", "Set your API key using:".yellow());
eprintln!(" export ELEVENLABS_API_KEY=your_api_key");
eprintln!();
eprintln!("Or use the --api-key flag:");
eprintln!(" elevenlabs --api-key YOUR_API_KEY <command>");
eprintln!();
eprintln!("Get your API key from: https://elevenlabs.io/app/settings/api-keys");
None
}
}
}
pub fn calculate_backoff(attempt: u32, retryable: Retryable) -> Duration {
let base_delay = match retryable {
Retryable::YesWithBackoff => {
INITIAL_BACKOFF_MS * 4
}
Retryable::Yes => {
INITIAL_BACKOFF_MS * (2u64.pow(attempt - 1))
}
Retryable::No => {
return Duration::ZERO;
}
};
let delay = base_delay.min(MAX_BACKOFF_MS);
let jitter = (rand_simple() as u64) * delay / 4000;
Duration::from_millis(delay + jitter)
}
fn rand_simple() -> u32 {
use std::time::{SystemTime, UNIX_EPOCH};
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.subsec_nanos();
nanos.wrapping_mul(1103515245).wrapping_add(12345)
}
pub async fn with_retry<T, F, Fut>(max_retries: u32, operation: F) -> anyhow::Result<T>
where
F: Fn() -> Fut,
Fut: std::future::Future<Output = anyhow::Result<T>>,
{
let mut last_error: Option<anyhow::Error> = None;
for attempt in 1..=max_retries {
match operation().await {
Ok(result) => return Ok(result),
Err(e) => {
last_error = Some(e);
let retryable = is_retryable(last_error.as_ref().unwrap());
if retryable == Retryable::No || attempt == max_retries {
break;
}
print_retry_error(last_error.as_ref().unwrap(), attempt, max_retries);
let delay = calculate_backoff(attempt, retryable);
tokio::time::sleep(delay).await;
}
}
}
Err(last_error.unwrap_or_else(|| anyhow::anyhow!("Unknown error")))
}
pub fn print_subscription_info(tier: &str) {
println!("\n{}", "Your Subscription:".bold().underline());
println!(" Tier: {}", tier.yellow());
println!();
println!("{}", "Feature Availability:".bold());
let features = match tier.to_lowercase().as_str() {
"free" => vec![
("TTS (Basic)", "✓", Some("Limited voices")),
("STT", "✓", Some("Limited usage")),
("Voice Library", "✓", Some("Browse only")),
(
"Professional Voice Cloning",
"✗",
Some("Upgrade to Starter"),
),
("Agents", "✗", Some("Upgrade to Starter")),
("Phone Numbers", "✗", Some("Upgrade to Starter")),
("Custom Pronunciations", "✗", Some("Upgrade to Starter")),
],
"starter" => vec![
("TTS (All Voices)", "✓", None),
("STT", "✓", None),
("Voice Library", "✓", None),
("Professional Voice Cloning", "✓", None),
("Voice Fine-tuning", "✓", None),
("Agents", "✗", Some("Upgrade to Creator")),
("Phone Numbers", "✗", Some("Upgrade to Creator")),
("Custom Pronunciations", "✗", Some("Upgrade to Creator")),
],
"creator" | "pro" | "business" => vec![
("All TTS Features", "✓", None),
("All STT Features", "✓", None),
("Voice Library", "✓", None),
("Professional Voice Cloning", "✓", None),
("Voice Fine-tuning", "✓", None),
("Agents", "✓", None),
("Phone Numbers", "✓", None),
("Custom Pronunciations", "✓", None),
("Priority Support", "✓", None),
],
_ => vec![
("TTS", "?", Some("Check your dashboard")),
("STT", "?", Some("Check your dashboard")),
("Voice Library", "?", Some("Check your dashboard")),
("Agents", "?", Some("Check your dashboard")),
],
};
for (feature, status, note) in features {
let status_colored = if status == "✓" {
status.green()
} else if status == "✗" {
status.red()
} else {
status.yellow()
};
if let Some(n) = note {
println!(" {} {} - {}", status_colored, feature, n.dimmed());
} else {
println!(" {} {}", status_colored, feature);
}
}
println!();
println!("{}", "View full feature comparison:".dimmed());
println!(" https://elevenlabs.io/pricing");
}