use smol_str::SmolStr;
use std::path::PathBuf;
use thiserror::Error;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum Error {
#[error("file not found: {0}")]
NotFound(PathBuf),
#[error(transparent)]
Io(#[from] std::io::Error),
#[cfg(feature = "inference")]
#[error(transparent)]
Ort(#[from] ort::Error),
#[cfg(feature = "inference")]
#[error(transparent)]
Tokenizer(Box<dyn std::error::Error + Send + Sync>),
#[cfg(feature = "inference")]
#[error("session contract mismatch on {input}: expected {expected}, got {got:?}")]
SessionContractMismatch {
input: &'static str,
expected: &'static str,
got: ort::value::TensorElementType,
},
#[cfg(feature = "inference")]
#[error("session shape mismatch on {input}: expected {expected}, got {got:?}")]
SessionShapeMismatch {
input: &'static str,
expected: &'static str,
got: Vec<i64>,
},
#[cfg(feature = "inference")]
#[error(
"decoder cache mismatch: expected {expected_conv} conv + {expected_attn} attn, \
got {got_conv} conv + {got_attn} attn"
)]
DecoderCacheMismatch {
expected_conv: usize,
expected_attn: usize,
got_conv: usize,
got_attn: usize,
},
#[cfg(feature = "decoders")]
#[error(transparent)]
ImageDecode(#[from] image::ImageError),
#[error("image {w}x{h} too small for ImageBudget (need at least {min_w}x{min_h})")]
ImageTooSmall {
w: u32,
h: u32,
min_w: u32,
min_h: u32,
},
#[error("image {w}x{h} would allocate ~{bytes} bytes when decoded (cap is {max_bytes} bytes)")]
ImageDecodedBufferTooLarge {
w: u32,
h: u32,
bytes: u64,
max_bytes: u64,
},
#[error("no valid tile grid for image {w}x{h} under budget {budget:?}")]
TileGridImpossible {
w: u32,
h: u32,
budget: crate::options::ImageBudget,
},
#[error("expected {expected} <image> placeholder(s) in prompt, got {got}")]
ImageTokenCountMismatch {
expected: usize,
got: usize,
},
#[error(
"context length exceeded: prompt_tokens={prompt_tokens} + max_new_tokens={max_new_tokens} > model_context={model_context}"
)]
ContextLengthExceeded {
prompt_tokens: usize,
max_new_tokens: usize,
model_context: usize,
},
#[error(
"image grid layout mismatch: prompt rendered for {expected_rows}x{expected_cols} grid, decoded image yielded {actual_rows}x{actual_cols} (markers and vision features would bind to wrong spatial positions)"
)]
ImageGridLayoutMismatch {
expected_rows: usize,
expected_cols: usize,
actual_rows: usize,
actual_cols: usize,
},
#[cfg(feature = "inference")]
#[error(transparent)]
LlGuidance(Box<dyn std::error::Error + Send + Sync>),
#[error("llguidance produced empty mask at step {step}: {state}")]
LlGuidanceDeadEnd {
step: usize,
state: SmolStr,
},
#[error(
"sampler logits include non-finite value(s) (model output NaN/Inf, or penalty × negative logit overflow)"
)]
SamplerNonFinite,
#[error("hit max_new_tokens={max} (schema_complete={schema_complete})")]
MaxTokensExceeded {
max: usize,
schema_complete: bool,
},
#[error("detokenize produced invalid UTF-8")]
InvalidUtf8,
#[error("generation produced empty output")]
Empty,
#[error("invalid RequestOptions: {0}")]
InvalidRequest(&'static str),
#[error("invalid ImageBudget: {0}")]
InvalidBudget(&'static str),
#[error(transparent)]
Parse(#[from] llmtask::JsonParseError),
}
impl Error {
#[allow(dead_code)]
#[cfg(feature = "inference")]
pub(crate) fn tokenizer<E>(e: E) -> Self
where
E: Into<Box<dyn std::error::Error + Send + Sync>>,
{
Self::Tokenizer(e.into())
}
#[allow(dead_code)]
#[cfg(feature = "inference")]
pub(crate) fn llguidance<E>(e: E) -> Self
where
E: Into<Box<dyn std::error::Error + Send + Sync>>,
{
Self::LlGuidance(e.into())
}
}
#[cfg(test)]
mod tests {
use super::*;
use smol_str::SmolStr;
#[test]
fn ll_guidance_dead_end_state_inlined_smolstr() {
let e = Error::LlGuidanceDeadEnd {
step: 5,
state: SmolStr::new_inline("regex stuck"),
};
let msg = format!("{e}");
assert!(msg.contains("step 5"));
assert!(msg.contains("regex stuck"));
}
#[test]
fn invalid_request_uses_static_str() {
fn classify(e: &Error) -> &'static str {
match e {
Error::InvalidRequest(s) => s,
_ => "other",
}
}
let e = Error::InvalidRequest("max_new_tokens must be > 0");
assert_eq!(classify(&e), "max_new_tokens must be > 0");
}
#[cfg(feature = "inference")]
#[test]
fn tokenizer_constructor_round_trips_through_box_dyn() {
let inner = std::io::Error::new(std::io::ErrorKind::InvalidData, "bad utf8");
let e = Error::tokenizer(inner);
let display = format!("{e}");
assert!(display.contains("bad utf8"));
}
#[cfg(feature = "inference")]
#[test]
fn llguidance_constructor_round_trips_through_box_dyn() {
let inner = std::io::Error::other("matcher exhausted");
let e = Error::llguidance(inner);
let display = format!("{e}");
assert!(display.contains("matcher exhausted"));
}
#[test]
fn from_io_works_via_question_mark() {
fn inner() -> Result<()> {
std::fs::read("/no/such/path/here").map(|_| ())?;
Ok(())
}
let e = inner().unwrap_err();
assert!(matches!(e, Error::Io(_)));
}
}