use std::ffi::{CString, c_int};
use std::fmt;
use std::path::PathBuf;
use super::completion::Completion;
use super::tool::Tool;
use super::weights::Weights;
use crate::error::{Error, Result};
use crate::ffi::{self, EncodedAudio, Input, Kind, Slot};
use crate::whistle::TranscribeOptions;
const DEFAULT_MAX_NEW_TOKENS: u32 = 256;
const MIN_OUTPUT_CAPACITY: usize = 64 * 1024;
const AUDIO_HEADROOM: usize = 64 * 1024;
const DETAIL_EXCERPT: usize = 200;
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(Kind::Text)?;
let mut guard = ffi::lock();
slot.load(&mut guard, self.weights.as_bytes())?;
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 rc = guard.init(system.as_deref(), &tools, tool_index.as_deref());
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")?;
self.turn(
Input::Text(&input),
options.output_capacity(),
options,
None,
)
}
pub fn complete_audio(&mut self, pcm: &[f32]) -> Result<Completion> {
self.complete_audio_with_options(pcm, CompleteOptions::default(), &TranscribeOptions::new())
}
pub fn complete_audio_with_options(
&mut self,
pcm: &[f32],
options: CompleteOptions,
audio: &TranscribeOptions,
) -> Result<Completion> {
let audio = audio.encode()?;
crate::whistle::validate(pcm)?;
let capacity = options
.output_capacity()
.saturating_add(AUDIO_HEADROOM)
.min(i32::MAX as usize);
self.turn(Input::Audio(pcm), capacity, options, Some(&audio))
}
fn turn(
&mut self,
input: Input<'_>,
capacity: usize,
options: CompleteOptions,
audio: Option<&EncodedAudio>,
) -> Result<Completion> {
let budget = c_int::try_from(options.max_new_tokens()).unwrap_or(c_int::MAX);
self.out.resize(capacity, 0);
self.out[0] = 0;
let mut guard = ffi::lock();
if let Some(audio) = audio {
if guard.models() & Kind::Speech.bit() == 0 {
return Err(Error::SpeechModelNotLoaded);
}
guard.set_audio(audio);
}
let rc = guard.complete(input, budget, &mut self.out)?;
let end = self
.out
.iter()
.position(|byte| *byte == 0)
.unwrap_or(capacity);
let text = String::from_utf8_lossy(&self.out[..end]);
if rc < 0 {
let detail = if text.is_empty() {
Some(guard.last_error()).filter(|reason| !reason.is_empty())
} else {
None
};
return Err(Error::Complete {
detail: detail.unwrap_or_else(|| failure_detail(&text)),
});
}
drop(guard);
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 rc = ffi::lock().embed(Input::Text(&input), &mut vector)?;
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 = ffi::lock().embed_len(Input::Text(c""))?;
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) {
ffi::lock().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) {
ffi::lock().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 an_audio_turn_gets_headroom_within_an_int() {
let small = CompleteOptions::new().output_capacity() + AUDIO_HEADROOM;
assert_eq!(small, MIN_OUTPUT_CAPACITY + 64 * 1024);
let huge = CompleteOptions::new()
.with_max_new_tokens(u32::MAX)
.output_capacity()
.saturating_add(AUDIO_HEADROOM)
.min(i32::MAX as usize);
assert_eq!(huge, i32::MAX as usize);
}
#[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)"));
}
}