use thiserror::Error;
#[derive(Error, Debug, Clone)]
pub enum RagrigError {
#[error("prompt size {current} tokens exceeds model context window of {max} tokens")]
ContextSizeExceeded {
current: usize,
max: usize,
},
#[error("embedding model '{model}' not found — is it pulled? Try: ollama pull {model}")]
EmbedModelNotFound {
model: String,
},
#[error("vector store at '{path}' is corrupt — delete it and re-index")]
StoreCorrupt {
path: String,
},
#[error("no documents found in '{folder}' — add PDF, EPUB, or HTML files")]
NoDocumentsFound {
folder: String,
},
#[error("cannot reach Ollama server while trying to {context} — is it running? Try: ollama serve")]
OllamaUnreachable {
context: String,
},
#[error("{backend} ({model}) generation failed: {detail}")]
GenerationFailed {
backend: String,
model: String,
detail: String,
},
#[error(
"index was created with {stored_model} ({stored_dims} dims) \
but current embedder is {current_model} ({current_dims} dims)"
)]
EmbeddingMismatch {
stored_model: String,
stored_dims: usize,
current_model: String,
current_dims: usize,
},
#[error(
"download from '{url}' exceeded the {limit_bytes}-byte limit \
after receiving {received_bytes} bytes"
)]
DownloadLimitExceeded {
url: String,
limit_bytes: u64,
received_bytes: u64,
},
}
impl RagrigError {
pub fn current_size(&self) -> usize {
match self {
Self::ContextSizeExceeded { current, .. } => *current,
_ => 0,
}
}
pub fn max_size(&self) -> usize {
match self {
Self::ContextSizeExceeded { max, .. } => *max,
_ => 0,
}
}
pub fn model_name(&self) -> &str {
match self {
Self::EmbedModelNotFound { model } => model,
_ => "",
}
}
pub fn store_path(&self) -> &str {
match self {
Self::StoreCorrupt { path } => path,
_ => "",
}
}
pub fn folder(&self) -> &str {
match self {
Self::NoDocumentsFound { folder } => folder,
_ => "",
}
}
pub fn suggested_context_tokens(&self) -> Option<usize> {
match self {
Self::ContextSizeExceeded { max, .. } => Some(max.saturating_sub(512)),
_ => None,
}
}
pub fn log(&self) {
log::error!("{}", self);
log::error!(" -> {}", self.suggested_action());
}
pub fn log_or(err: &anyhow::Error, fallback_msg: &str) {
if let Some(re) = err.downcast_ref::<Self>() {
re.log();
} else {
log::error!("{}: {}", fallback_msg, err);
}
}
pub fn suggested_action(&self) -> String {
match self {
Self::ContextSizeExceeded { max, .. } => {
format!(
"Reduce the context budget with `/chat context {}` or use a model with a larger window.",
max.saturating_sub(512)
)
}
Self::EmbedModelNotFound { model } => {
format!("Pull the model first: ollama pull {model}")
}
Self::StoreCorrupt { path } => {
format!("Delete the corrupt store at '{path}' and re-index with `/embed index`.")
}
Self::NoDocumentsFound { folder } => {
format!("Add PDF, EPUB, or HTML files to '{folder}'.")
}
Self::OllamaUnreachable { .. } => {
"Start Ollama with `ollama serve` and try again.".into()
}
Self::GenerationFailed { model, .. } => {
format!(
"Check that '{model}' is pulled (`ollama pull {model}`) and that it fits in VRAM. \
Try a smaller model or restart Ollama."
)
}
Self::EmbeddingMismatch {
current_model, ..
} => {
format!(
"Re-index your documents with the current embedder '{current_model}', \
or switch back to the original embedding model."
)
}
Self::DownloadLimitExceeded { limit_bytes, .. } => {
format!(
"Increase the download limit (currently {limit_bytes} bytes) or use \
a different source for the document."
)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn context_size_exceeded_accessors() {
let err = RagrigError::ContextSizeExceeded { current: 5000, max: 4096 };
assert_eq!(err.current_size(), 5000);
assert_eq!(err.max_size(), 4096);
}
#[test]
fn context_size_exceeded_display() {
let err = RagrigError::ContextSizeExceeded { current: 5000, max: 4096 };
let msg = err.to_string();
assert!(msg.contains("5000"));
assert!(msg.contains("4096"));
}
#[test]
fn embed_model_not_found_display() {
let err = RagrigError::EmbedModelNotFound { model: "nomic-embed-text:latest".into() };
let msg = err.to_string();
assert!(msg.contains("nomic-embed-text"));
assert!(msg.contains("ollama pull"));
}
#[test]
fn store_corrupt_display() {
let err = RagrigError::StoreCorrupt { path: "/tmp/store".into() };
let msg = err.to_string();
assert!(msg.contains("/tmp/store"));
assert!(msg.contains("corrupt"));
}
#[test]
fn no_documents_found_display() {
let err = RagrigError::NoDocumentsFound { folder: "/docs".into() };
let msg = err.to_string();
assert!(msg.contains("/docs"));
assert!(msg.contains("no documents"));
}
#[test]
fn ollama_unreachable_display() {
let err = RagrigError::OllamaUnreachable { context: "create chat client".into() };
let msg = err.to_string();
assert!(msg.contains("cannot reach Ollama"));
assert!(msg.contains("ollama serve"));
}
#[test]
fn generation_failed_display() {
let err = RagrigError::GenerationFailed {
backend: "ollama".into(),
model: "llama3.2".into(),
detail: "connection timed out".into(),
};
let msg = err.to_string();
assert!(msg.contains("ollama"));
assert!(msg.contains("llama3.2"));
assert!(msg.contains("connection timed out"));
}
#[test]
fn embedding_mismatch_display() {
let err = RagrigError::EmbeddingMismatch {
stored_model: "nomic-embed-text".into(),
stored_dims: 768,
current_model: "bge-large".into(),
current_dims: 1024,
};
let msg = err.to_string();
assert!(msg.contains("nomic-embed-text"));
assert!(msg.contains("768"));
assert!(msg.contains("bge-large"));
assert!(msg.contains("1024"));
}
#[test]
fn download_limit_exceeded_display() {
let err = RagrigError::DownloadLimitExceeded {
url: "https://example.com/big.pdf".into(),
limit_bytes: 1048576,
received_bytes: 1048577,
};
let msg = err.to_string();
assert!(msg.contains("example.com"));
assert!(msg.contains("1048576"));
}
#[test]
fn downcast_from_anyhow() {
let err = anyhow::Error::new(RagrigError::ContextSizeExceeded { current: 100, max: 50 });
let downcast = err.downcast_ref::<RagrigError>();
assert!(downcast.is_some());
assert_eq!(downcast.unwrap().current_size(), 100);
}
#[test]
fn accessors_return_default_on_wrong_variant() {
let err = RagrigError::NoDocumentsFound { folder: "x".into() };
assert_eq!(err.current_size(), 0);
assert_eq!(err.max_size(), 0);
assert_eq!(err.store_path(), "");
assert_eq!(err.model_name(), "");
}
#[test]
fn suggested_context_tokens_for_overflow() {
let err = RagrigError::ContextSizeExceeded { current: 5000, max: 4096 };
assert_eq!(err.suggested_context_tokens(), Some(4096 - 512));
}
#[test]
fn suggested_context_tokens_none_for_other_variants() {
let err = RagrigError::NoDocumentsFound { folder: "x".into() };
assert_eq!(err.suggested_context_tokens(), None);
}
#[test]
fn suggested_action_not_empty_for_all_variants() {
let variants = [
RagrigError::ContextSizeExceeded { current: 100, max: 50 },
RagrigError::EmbedModelNotFound { model: "x".into() },
RagrigError::StoreCorrupt { path: "/tmp".into() },
RagrigError::NoDocumentsFound { folder: "/tmp".into() },
RagrigError::OllamaUnreachable { context: "test".into() },
RagrigError::GenerationFailed {
backend: "ollama".into(),
model: "x".into(),
detail: "test".into(),
},
RagrigError::EmbeddingMismatch {
stored_model: "nomic-embed-text".into(),
stored_dims: 768,
current_model: "bge-large".into(),
current_dims: 1024,
},
RagrigError::DownloadLimitExceeded {
url: "https://example.com".into(),
limit_bytes: 52428800,
received_bytes: 52428801,
},
];
for v in &variants {
assert!(!v.suggested_action().is_empty(), "empty action for {v:?}");
}
}
}