use std::ffi::{CString, c_char, c_int, c_ulonglong};
use std::fmt;
use std::path::PathBuf;
use std::ptr;
use std::sync::OnceLock;
use std::sync::atomic::{AtomicBool, Ordering};
use super::completion::Completion;
use super::tool::Tool;
use super::weights::Weights;
use crate::error::{Error, Result};
static ENGINE_TAKEN: AtomicBool = AtomicBool::new(false);
static LOADED: OnceLock<u64> = OnceLock::new();
const DEFAULT_MAX_NEW_TOKENS: u32 = 256;
const MIN_OUTPUT_CAPACITY: usize = 64 * 1024;
const DETAIL_EXCERPT: usize = 200;
struct Slot;
impl Slot {
fn take() -> Result<Self> {
match ENGINE_TAKEN.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire) {
Ok(_) => Ok(Slot),
Err(_) => Err(Error::EngineBusy),
}
}
}
impl Drop for Slot {
fn drop(&mut self) {
ENGINE_TAKEN.store(false, Ordering::Release);
}
}
fn fingerprint(bytes: &[u8]) -> u64 {
const PRIME: u64 = 0x0000_0100_0000_01b3;
let mut hash = 0xcbf2_9ce4_8422_2325_u64 ^ bytes.len() as u64;
let mut words = bytes.chunks_exact(8);
for word in &mut words {
let mut buffer = [0u8; 8];
buffer.copy_from_slice(word);
hash = (hash ^ u64::from_le_bytes(buffer)).wrapping_mul(PRIME);
}
for byte in words.remainder() {
hash = (hash ^ u64::from(*byte)).wrapping_mul(PRIME);
}
hash
}
fn load_once(slot: &Slot, weights: &Weights) -> Result<()> {
let _ = slot;
let wanted = fingerprint(weights.as_bytes());
if let Some(loaded) = LOADED.get() {
return if *loaded == wanted {
Ok(())
} else {
Err(Error::WeightsAlreadyLoaded)
};
}
let bytes = weights.as_bytes();
let rc = unsafe { cactus_sys::needle_load(bytes.as_ptr(), bytes.len() as c_ulonglong) };
if rc < 0 {
return Err(Error::Load);
}
let _ = LOADED.set(wanted);
Ok(())
}
fn c_string(text: &str, field: &'static str) -> Result<CString> {
CString::new(text).map_err(|_| Error::InteriorNul { field })
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct CompleteOptions {
max_new_tokens: u32,
}
impl Default for CompleteOptions {
fn default() -> Self {
Self::new()
}
}
impl CompleteOptions {
#[must_use]
pub const fn new() -> Self {
CompleteOptions {
max_new_tokens: DEFAULT_MAX_NEW_TOKENS,
}
}
#[must_use]
pub const fn with_max_new_tokens(mut self, max_new_tokens: u32) -> Self {
self.max_new_tokens = max_new_tokens;
self
}
#[inline]
#[must_use]
pub const fn max_new_tokens(&self) -> u32 {
self.max_new_tokens
}
fn output_capacity(self) -> usize {
let per_token = (self.max_new_tokens as usize).saturating_mul(64);
let wanted = 4096_usize.saturating_add(per_token);
wanted.max(MIN_OUTPUT_CAPACITY).min(i32::MAX as usize)
}
}
#[derive(Debug, Clone)]
pub struct NeedleBuilder {
weights: Weights,
system: Option<String>,
tools: Vec<Tool>,
tool_index: Option<PathBuf>,
}
impl NeedleBuilder {
#[must_use]
pub fn system<S>(mut self, system: S) -> Self
where
S: Into<String>,
{
self.system = Some(system.into());
self
}
#[must_use]
pub fn tool(mut self, tool: Tool) -> Self {
self.tools.push(tool);
self
}
#[must_use]
pub fn tools<I>(mut self, tools: I) -> Self
where
I: IntoIterator<Item = Tool>,
{
self.tools.extend(tools);
self
}
#[must_use]
pub fn tool_index<P>(mut self, path: P) -> Self
where
P: Into<PathBuf>,
{
self.tool_index = Some(path.into());
self
}
pub fn build(self) -> Result<Needle> {
let slot = Slot::take()?;
load_once(&slot, &self.weights)?;
drop(self.weights);
let system = self
.system
.as_deref()
.map(|text| c_string(text, "system prompt"))
.transpose()?;
let tools_json =
serde_json::to_string(&self.tools).map_err(|source| Error::Tools { source })?;
let tools = c_string(&tools_json, "tools")?;
let tool_index = self
.tool_index
.as_ref()
.map(|path| c_string(&path.to_string_lossy(), "tool index"))
.transpose()?;
let system_ptr = system.as_ref().map_or(ptr::null(), |text| text.as_ptr());
let index_ptr = tool_index
.as_ref()
.map_or(ptr::null(), |text| text.as_ptr());
let rc = unsafe { cactus_sys::needle_init(system_ptr, tools.as_ptr(), index_ptr) };
if rc <= 0 {
return Err(Error::Init);
}
Ok(Needle {
_slot: slot,
prefix_tokens: u32::try_from(rc).unwrap_or_default(),
out: Vec::new(),
dimension: None,
})
}
}
pub struct Needle {
_slot: Slot,
prefix_tokens: u32,
out: Vec<u8>,
dimension: Option<usize>,
}
impl Needle {
#[must_use]
pub fn builder(weights: Weights) -> NeedleBuilder {
NeedleBuilder {
weights,
system: None,
tools: Vec::new(),
tool_index: None,
}
}
pub fn complete<S>(&mut self, input: S) -> Result<Completion>
where
S: AsRef<str>,
{
self.complete_with_options(input, CompleteOptions::default())
}
pub fn complete_with_options<S>(
&mut self,
input: S,
options: CompleteOptions,
) -> Result<Completion>
where
S: AsRef<str>,
{
let input = c_string(input.as_ref(), "input")?;
let capacity = options.output_capacity();
self.out.resize(capacity, 0);
self.out[0] = 0;
let budget = c_int::try_from(options.max_new_tokens()).unwrap_or(c_int::MAX);
let out_capacity = c_int::try_from(capacity).unwrap_or(c_int::MAX);
let rc = unsafe {
cactus_sys::needle_complete(
input.as_ptr(),
budget,
self.out.as_mut_ptr().cast::<c_char>(),
out_capacity,
)
};
let end = self
.out
.iter()
.position(|byte| *byte == 0)
.unwrap_or(capacity);
let text = String::from_utf8_lossy(&self.out[..end]);
if rc < 0 {
return Err(Error::Complete {
detail: failure_detail(&text),
});
}
if end >= capacity - 1 {
return Err(Error::Truncated { capacity });
}
text.parse()
}
pub fn embed<S>(&mut self, input: S) -> Result<Vec<f32>>
where
S: AsRef<str>,
{
let dimension = self.embedding_dimension()?;
let input = c_string(input.as_ref(), "input")?;
let mut vector = vec![0.0_f32; dimension];
let capacity = c_int::try_from(dimension).unwrap_or(c_int::MAX);
let rc = unsafe { cactus_sys::needle_embed(input.as_ptr(), vector.as_mut_ptr(), capacity) };
if usize::try_from(rc) != Ok(dimension) {
return Err(Error::Embed);
}
Ok(vector)
}
pub fn embedding_dimension(&mut self) -> Result<usize> {
if let Some(dimension) = self.dimension {
return Ok(dimension);
}
let rc = unsafe { cactus_sys::needle_embed(ptr::null(), ptr::null_mut(), 0) };
let dimension = usize::try_from(rc).map_err(|_| Error::Embed)?;
if dimension == 0 {
return Err(Error::Embed);
}
self.dimension = Some(dimension);
Ok(dimension)
}
pub fn reset(&mut self) {
unsafe { cactus_sys::needle_reset() };
}
#[inline]
#[must_use]
pub const fn prefix_tokens(&self) -> u32 {
self.prefix_tokens
}
}
fn failure_detail(text: &str) -> String {
let reported = serde_json::from_str::<serde_json::Value>(text)
.ok()
.and_then(|envelope| {
envelope
.get("error")
.and_then(serde_json::Value::as_str)
.map(str::to_owned)
});
reported.unwrap_or_else(|| match text.char_indices().nth(DETAIL_EXCERPT) {
Some((cut, _)) => format!("{}... (truncated)", &text[..cut]),
None if text.is_empty() => "the engine wrote nothing".to_owned(),
None => text.to_owned(),
})
}
impl Drop for Needle {
fn drop(&mut self) {
unsafe { cactus_sys::needle_reset() };
}
}
impl fmt::Debug for Needle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Needle")
.field("prefix_tokens", &self.prefix_tokens)
.field("embedding_dimension", &self.dimension)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_needle_is_send_and_sync() {
fn assert_bounds<T: Send + Sync>() {}
assert_bounds::<Needle>();
assert_bounds::<NeedleBuilder>();
}
#[test]
fn the_buffer_floor_holds_for_small_budgets() {
assert_eq!(
CompleteOptions::new()
.with_max_new_tokens(1)
.output_capacity(),
MIN_OUTPUT_CAPACITY
);
}
#[test]
fn the_buffer_grows_with_the_budget() {
let capacity = CompleteOptions::new()
.with_max_new_tokens(8192)
.output_capacity();
assert_eq!(capacity, 4096 + 8192 * 64);
}
#[test]
fn the_buffer_never_exceeds_an_int() {
let capacity = CompleteOptions::new()
.with_max_new_tokens(u32::MAX)
.output_capacity();
assert_eq!(capacity, i32::MAX as usize);
}
#[test]
fn different_archives_fingerprint_differently() {
assert_ne!(fingerprint(b"needle-a"), fingerprint(b"needle-b"));
assert_ne!(fingerprint(b"needle"), fingerprint(b"needle\0"));
assert_eq!(fingerprint(b"needle"), fingerprint(b"needle"));
}
#[test]
fn an_interior_nul_names_its_field() {
let error = c_string("kit\0chen", "input").expect_err("a NUL is not a C string");
assert!(matches!(error, Error::InteriorNul { field: "input" }));
}
#[test]
fn a_failure_detail_prefers_the_engines_own_words() {
let detail = failure_detail(r#"{"type":"error","error":"needle_init not called"}"#);
assert_eq!(detail, "needle_init not called");
}
#[test]
fn a_failure_detail_falls_back_to_the_text() {
assert_eq!(failure_detail("segfault imminent"), "segfault imminent");
assert_eq!(failure_detail(""), "the engine wrote nothing");
assert!(failure_detail(&"x".repeat(4096)).ends_with("... (truncated)"));
}
}