use std::ffi::CString;
use llama_cpp_sys_4 as sys;
use crate::shim::{check_status, read_i32s, read_string, ShimError};
pub type RuntimeError = ShimError;
type Result<T> = std::result::Result<T, RuntimeError>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SpeculativeType(pub i32);
impl SpeculativeType {
pub fn name(self) -> Result<String> {
read_string(|buf, len, expected| unsafe {
sys::common_shim_speculative_type_to_str(self.0, buf, len, expected)
})
}
pub fn from_name(name: &str) -> Result<Self> {
let c_name = CString::new(name)?;
let mut raw = 0i32;
let status =
unsafe { sys::common_shim_speculative_type_from_name(c_name.as_ptr(), &raw mut raw) };
check_status(status)?;
Ok(Self(raw))
}
pub fn all_names() -> Result<String> {
read_string(|buf, len, expected| unsafe {
sys::common_shim_speculative_all_types_str(buf, len, expected)
})
}
}
pub fn speculative_types_from_gguf(path: &str) -> Result<Vec<SpeculativeType>> {
let c_path = CString::new(path)?;
let raw = read_i32s(|out, cap, len| unsafe {
sys::common_shim_speculative_types_from_gguf(c_path.as_ptr(), out, cap, len)
})?;
Ok(raw.into_iter().map(SpeculativeType).collect())
}
pub mod log {
use super::{check_status, CString, Result};
use llama_cpp_sys_4 as sys;
pub fn set_verbosity(verbosity: i32) {
unsafe { sys::common_shim_log_set_verbosity(verbosity) }
}
#[must_use]
pub fn verbosity_for_level(level: i32) -> i32 {
unsafe { sys::common_shim_log_get_verbosity(level) }
}
pub fn set_timestamps(timestamps: bool) {
unsafe { sys::common_shim_log_set_timestamps(timestamps) }
}
pub fn set_prefix(prefix: bool) {
unsafe { sys::common_shim_log_set_prefix(prefix) }
}
pub fn set_colors(colors: bool) {
unsafe { sys::common_shim_log_set_colors(colors) }
}
pub fn set_jsonl(jsonl: bool) {
unsafe { sys::common_shim_log_set_jsonl(jsonl) }
}
pub fn set_file(path: Option<&str>) -> Result<()> {
let c_path = path.map(CString::new).transpose()?;
let ptr = c_path.as_ref().map_or(std::ptr::null(), |c| c.as_ptr());
let status = unsafe { sys::common_shim_log_set_file(ptr) };
check_status(status)
}
pub fn pause() {
unsafe { sys::common_shim_log_pause() }
}
pub fn resume() {
unsafe { sys::common_shim_log_resume() }
}
}
pub mod download {
use super::{check_status, read_string, CString, Result, RuntimeError};
use llama_cpp_sys_4 as sys;
pub fn resolve_hf(repo_with_tag: &str, file: Option<&str>) -> Result<String> {
let c_repo = CString::new(repo_with_tag)?;
let c_file = file.map(CString::new).transpose()?;
let file_ptr = c_file.as_ref().map_or(std::ptr::null(), |c| c.as_ptr());
read_string(|buf, len, expected| unsafe {
sys::common_shim_download_resolve_path(c_repo.as_ptr(), file_ptr, buf, len, expected)
})
}
pub fn split_repo_tag(repo_with_tag: &str) -> Result<(String, String)> {
let c_repo = CString::new(repo_with_tag)?;
let mut repo_len: usize = 0;
let mut tag_len: usize = 0;
let status = unsafe {
sys::common_shim_download_split_repo_tag(
c_repo.as_ptr(),
std::ptr::null_mut(),
0,
&raw mut repo_len,
std::ptr::null_mut(),
0,
&raw mut tag_len,
)
};
if status != sys::LLAMA_SHIM_BUFFER_TOO_SMALL {
check_status(status)?;
}
let mut repo_buf = vec![0u8; repo_len.max(1)];
let mut tag_buf = vec![0u8; tag_len.max(1)];
let status = unsafe {
sys::common_shim_download_split_repo_tag(
c_repo.as_ptr(),
repo_buf.as_mut_ptr().cast::<std::ffi::c_char>(),
repo_buf.len(),
&raw mut repo_len,
tag_buf.as_mut_ptr().cast::<std::ffi::c_char>(),
tag_buf.len(),
&raw mut tag_len,
)
};
check_status(status)?;
Ok((trim_nul(repo_buf)?, trim_nul(tag_buf)?))
}
fn trim_nul(mut buf: Vec<u8>) -> Result<String> {
let end = buf.iter().position(|b| *b == 0).unwrap_or(buf.len());
buf.truncate(end);
String::from_utf8(buf).map_err(RuntimeError::from)
}
pub fn remove_cached(repo_with_tag: &str) -> Result<bool> {
let c_repo = CString::new(repo_with_tag)?;
let rc = unsafe { sys::common_shim_download_remove(c_repo.as_ptr()) };
if rc < 0 {
check_status(rc)?;
}
Ok(rc == 1)
}
pub fn list_cached_json() -> Result<String> {
read_string(|buf, len, expected| unsafe {
sys::common_shim_list_cached_models(buf, len, expected)
})
}
pub fn resolve_docker(reference: &str) -> Result<String> {
let c_ref = CString::new(reference)?;
read_string(|buf, len, expected| unsafe {
sys::common_shim_docker_resolve_model(c_ref.as_ptr(), buf, len, expected)
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn speculative_type_names_round_trip() {
let none = SpeculativeType::from_name("none").expect("parse 'none'");
assert_eq!(none.name().unwrap(), "none");
}
#[test]
fn speculative_type_rejects_unknown_name() {
assert!(SpeculativeType::from_name("not-a-strategy").is_err());
}
#[test]
fn speculative_type_rejects_interior_nul() {
assert!(matches!(
SpeculativeType::from_name("no\0ne"),
Err(RuntimeError::Nul(_))
));
}
#[test]
fn all_speculative_names_is_non_empty() {
let all = SpeculativeType::all_names().expect("all names");
assert!(!all.is_empty(), "no speculative types listed");
assert!(all.contains("none"), "got {all}");
}
#[test]
fn speculative_types_from_a_missing_file_is_empty() {
let types = speculative_types_from_gguf("/definitely/not/a/model.gguf").unwrap();
assert!(types.is_empty(), "got {types:?}");
}
#[test]
fn speculative_types_rejects_interior_nul() {
assert!(matches!(
speculative_types_from_gguf("a\0b"),
Err(RuntimeError::Nul(_))
));
}
#[test]
fn speculative_types_from_a_plain_model_is_empty() {
let Some(path) = std::env::var_os("LLAMA_TEST_MODEL") else {
eprintln!("SKIP: no test model available");
return;
};
let types = speculative_types_from_gguf(&path.to_string_lossy()).unwrap();
assert!(
types.iter().all(|t| t.name().unwrap_or_default() != "draft-eagle3"),
"a plain model should not advertise EAGLE-3: {types:?}"
);
}
#[test]
fn split_repo_tag_separates_the_parts() {
let (repo, tag) = download::split_repo_tag("ggml-org/models:Q4_K_M").unwrap();
assert_eq!(repo, "ggml-org/models");
assert_eq!(tag, "Q4_K_M");
}
#[test]
fn split_repo_tag_leaves_a_missing_tag_empty() {
let (repo, tag) = download::split_repo_tag("ggml-org/models").unwrap();
assert_eq!(repo, "ggml-org/models");
assert_eq!(tag, "");
}
#[test]
fn split_repo_tag_rejects_a_malformed_repo() {
assert!(download::split_repo_tag("not-a-repo").is_err());
assert!(download::split_repo_tag("too/many/parts").is_err());
}
#[test]
fn split_repo_tag_rejects_interior_nul() {
assert!(matches!(
download::split_repo_tag("a\0b"),
Err(RuntimeError::Nul(_))
));
}
#[test]
fn list_cached_models_returns_json() {
let json = download::list_cached_json().expect("cache listing");
assert!(
json.starts_with('['),
"expected a JSON array, got {json:.40}"
);
}
#[test]
fn log_controls_do_not_panic() {
log::set_timestamps(true);
log::set_prefix(true);
log::set_colors(false);
log::set_timestamps(false);
log::set_prefix(false);
assert!(log::set_file(None).is_ok());
}
#[test]
fn log_set_file_rejects_interior_nul() {
assert!(matches!(
log::set_file(Some("a\0b")),
Err(RuntimeError::Nul(_))
));
}
}