use serde_json::Value;
use std::fmt;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Role {
User,
Assistant,
}
#[derive(Clone, Debug, PartialEq)]
pub enum Block {
Text(String),
Image {
media_type: String,
data_base64: String,
name: Option<String>,
},
ToolUse {
id: String,
name: String,
input: Value,
},
ToolResult {
id: String,
content: String,
is_error: bool,
},
}
#[derive(Clone, Debug, PartialEq)]
pub struct Message {
pub role: Role,
pub blocks: Vec<Block>,
}
impl Message {
pub fn user(text: impl Into<String>) -> Self {
Self { role: Role::User, blocks: vec![Block::Text(text.into())] }
}
pub fn assistant(blocks: Vec<Block>) -> Self {
Self { role: Role::Assistant, blocks }
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct ToolSpec {
pub name: String,
pub description: String,
pub schema: Value,
}
#[derive(Clone, Debug)]
pub struct Request {
pub model: String,
pub system: Option<String>,
pub messages: Vec<Message>,
pub tools: Vec<ToolSpec>,
pub max_tokens: u32,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum StopReason {
EndTurn,
ToolUse,
MaxTokens,
Other(String),
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Usage {
pub input_tokens: u32,
pub output_tokens: u32,
}
#[derive(Clone, Debug, PartialEq)]
pub enum Event {
Text(String),
ToolUse {
id: String,
name: String,
input: Value,
},
Usage(Usage),
Done(StopReason),
}
#[derive(Debug)]
pub enum AiError {
Transport(String),
Status {
code: u16,
body: String,
},
Decode(String),
Api(String),
Cancelled,
MissingKey(&'static str),
Config(&'static str),
}
fn status_message(code: u16, body: &str) -> String {
let meaning = match code {
400 | 422 => "el proveedor rechazó la petición",
401 => "clave o sesión no válida: revisa la conexión en el selector de modelos",
403 => "esta cuenta no tiene permiso para usar este modelo o servicio",
404 => "modelo o dirección no encontrados: revisa el ID del modelo y la dirección base",
413 => "la conversación es demasiado grande para el modelo",
429 => "demasiadas peticiones o cuota agotada: espera un momento",
529 => "el servicio de modelos está saturado: inténtalo de nuevo en un rato",
500..=599 => "el servicio de modelos falló: inténtalo de nuevo",
_ => "error del servidor",
};
let json: Option<Value> = serde_json::from_str(body).ok();
let detail = json
.as_ref()
.and_then(|json| json["error"]["message"].as_str().or(json["error"].as_str()).or(json["message"].as_str()))
.map(str::to_owned)
.or_else(|| Some(body.trim().to_owned()).filter(|text| !text.is_empty() && json.is_none()));
match detail {
Some(detail) => format!("{meaning} ({code}): {detail}"),
None => format!("{meaning} ({code})"),
}
}
impl fmt::Display for AiError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AiError::Transport(message) => write!(formatter, "error de red: {message}"),
AiError::Status { code, body } => write!(formatter, "{}", status_message(*code, body)),
AiError::Decode(message) => write!(formatter, "respuesta inválida: {message}"),
AiError::Api(message) => write!(formatter, "error del proveedor: {message}"),
AiError::Cancelled => write!(formatter, "cancelado"),
AiError::MissingKey(provider) => write!(formatter, "falta la clave de API de {provider}"),
AiError::Config(message) => write!(formatter, "ajustes de IA: {message}"),
}
}
}
impl std::error::Error for AiError {}
#[cfg(test)]
mod tests {
use super::*;
fn status(code: u16, body: &str) -> String {
AiError::Status { code, body: body.into() }.to_string()
}
#[test]
fn los_errores_http_se_explican_y_conservan_el_detalle() {
let anthropic =
status(401, r#"{"type":"error","error":{"type":"authentication_error","message":"invalid x-api-key"}}"#);
assert_eq!(
anthropic,
"clave o sesión no válida: revisa la conexión en el selector de modelos (401): invalid x-api-key"
);
assert!(status(404, r#"{"error":{"message":"model: x"}}"#).contains("revisa el ID del modelo"));
assert!(status(429, "").contains("espera un momento") && status(529, "{}").contains("saturado"));
assert_eq!(
status(502, "<html>Bad Gateway</html>"),
"el servicio de modelos falló: inténtalo de nuevo (502): <html>Bad Gateway</html>"
);
assert_eq!(
status(400, r#"{"error":"clave inválida"}"#),
"el proveedor rechazó la petición (400): clave inválida"
);
assert_eq!(status(418, ""), "error del servidor (418)");
}
}