use std::{
cell::RefCell,
ffi::{CStr, c_char, c_void},
};
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("mlx: {0}")]
Mlx(String),
#[error("io: {0}")]
Io(#[from] std::io::Error),
#[error("config: {0}")]
Config(String),
#[error("model: {0}")]
Model(String),
#[error("tokenizer: {0}")]
Tokenizer(String),
#[error("template: {0}")]
Template(String),
#[error(
"context window exceeded: prompt {prompt_tokens} + output {max_output_tokens} > {limit}"
)]
ContextExceeded {
prompt_tokens: usize,
max_output_tokens: usize,
limit: usize,
},
#[error("capability {capability} is unavailable: {reason}")]
CapabilityUnavailable {
capability: &'static str,
reason: String,
},
#[error("generation cancelled")]
Cancelled,
}
pub type Result<T> = std::result::Result<T, Error>;
thread_local! {
static LAST_MLX_ERROR: RefCell<Option<String>> = const { RefCell::new(None) };
}
unsafe extern "C" fn record_error(msg: *const c_char, _data: *mut c_void) {
let text = if msg.is_null() {
String::from("unknown MLX error")
} else {
unsafe { CStr::from_ptr(msg) }
.to_string_lossy()
.into_owned()
};
LAST_MLX_ERROR.with(|slot| *slot.borrow_mut() = Some(text));
}
pub fn install_error_handler() {
use std::sync::Once;
static ONCE: Once = Once::new();
ONCE.call_once(|| unsafe {
crate::engine::sys::mlx_set_error_handler(Some(record_error), std::ptr::null_mut(), None);
});
}
pub(crate) fn check(status: i32) -> Result<()> {
if status == 0 {
return Ok(());
}
Err(take_last_error("MLX call failed without a message"))
}
pub(crate) fn take_last_error(fallback: &str) -> Error {
let message = LAST_MLX_ERROR
.with(|slot| slot.borrow_mut().take())
.unwrap_or_else(|| fallback.to_string());
Error::Mlx(message)
}