use toolkit_canonical_errors::{CanonicalError, resource_error};
use crate::domain::error::ChatEngineError;
#[resource_error(gts_id!("cf.core.chat_engine.session.v1~"))]
pub struct ChatEngineSessionError;
#[resource_error(gts_id!("cf.core.chat_engine.message.v1~"))]
pub struct ChatEngineMessageError;
#[resource_error(gts_id!("cf.core.chat_engine.plugin.v1~"))]
pub struct ChatEnginePluginError;
#[resource_error(gts_id!("cf.core.chat_engine.resource.v1~"))]
pub struct ChatEngineResourceError;
impl From<ChatEngineError> for CanonicalError {
fn from(err: ChatEngineError) -> Self {
match err {
ChatEngineError::NotFound { resource, id } => {
let detail = format!("{resource} not found: {id}");
match resource {
"session" | "shared_session" => ChatEngineSessionError::not_found(detail)
.with_resource(id)
.create(),
"message" | "variant" => ChatEngineMessageError::not_found(detail)
.with_resource(id)
.create(),
_ => ChatEngineResourceError::not_found(detail)
.with_resource(id)
.create(),
}
}
ChatEngineError::Forbidden { reason } => {
tracing::debug!(reason = %reason, "permission denied");
ChatEngineSessionError::permission_denied()
.with_reason("AUTHZ_DENIED")
.create()
}
ChatEngineError::Conflict { reason } => {
ChatEngineSessionError::already_exists(reason.clone())
.with_resource(reason)
.create()
}
ChatEngineError::BadRequest { reason } => ChatEngineResourceError::invalid_argument()
.with_format(reason)
.create(),
ChatEngineError::BackendUnavailable {
reason,
retry_after,
source,
} => {
let _ = source;
map_backend_unavailable(reason, retry_after)
}
ChatEngineError::Internal { reason, source } => {
tracing::error!(
operator_detail = %reason,
has_source = source.is_some(),
"chat-engine internal error",
);
CanonicalError::internal("Internal server error").create()
}
ChatEngineError::NotImplemented { reason } => {
ChatEngineResourceError::unimplemented(reason).create()
}
}
}
}
fn map_backend_unavailable(
reason: String,
retry_after: Option<std::time::Duration>,
) -> CanonicalError {
let cls = classify_backend(&reason, retry_after);
if !cls.user_facing {
tracing::warn!(
operator_detail = %reason,
status = cls.suggested_status,
"backend unavailable (operator-only detail redacted from wire)",
);
}
match cls.suggested_status {
429 => {
let mut b = CanonicalError::service_unavailable();
if let Some(d) = retry_after {
b = b.with_retry_after_seconds(d.as_secs());
}
b.create()
}
504 | 408 => CanonicalError::service_unavailable()
.with_retry_after_seconds(5)
.create(),
400 => ChatEnginePluginError::invalid_argument()
.with_format(reason)
.create(),
401 | 403 => {
tracing::debug!(reason = %reason, "plugin denied access");
ChatEnginePluginError::permission_denied()
.with_reason("BACKEND_UNAUTHORIZED")
.create()
}
404 => ChatEnginePluginError::not_found(reason.clone())
.with_resource(reason)
.create(),
_ => {
let mut b = CanonicalError::service_unavailable();
if let Some(d) = retry_after {
b = b.with_retry_after_seconds(d.as_secs());
} else {
b = b.with_retry_after_seconds(5);
}
b.create()
}
}
}
struct BackendClass {
suggested_status: u16,
user_facing: bool,
}
fn classify_backend(reason: &str, retry_after: Option<std::time::Duration>) -> BackendClass {
if retry_after.is_some() || reason.contains("rate limited") {
BackendClass {
suggested_status: 429,
user_facing: true,
}
} else if reason.contains("timeout") {
BackendClass {
suggested_status: 504,
user_facing: false,
}
} else {
BackendClass {
suggested_status: 503,
user_facing: false,
}
}
}
#[cfg(test)]
#[path = "error_tests.rs"]
mod error_tests;