use crate::batch::BatchProcessorBuilder;
use crate::cache::embedding_cache::EmbeddingCache;
use crate::cache::prefix_cache::PrefixCache;
use crate::cache::token_cache::TokenCache;
use crate::cache::{CacheStats, CacheStore};
use crate::config::{EngineConfig, NormalizationMode, TruncateTokens};
use crate::error::{Error, Result};
use crate::model::EmbeddingModel;
use llama_cpp_2::llama_backend::LlamaBackend;
use parking_lot::RwLock;
use std::cell::RefCell;
use std::collections::HashMap;
use std::sync::{Arc, Mutex, OnceLock};
use tracing::{debug, info, instrument};
static INSTANCE: RwLock<Option<Arc<Mutex<EmbeddingEngine>>>> = RwLock::new(None);
static INIT_LOCK: Mutex<()> = Mutex::new(());
static BACKEND: OnceLock<Arc<Mutex<LlamaBackend>>> = OnceLock::new();
thread_local! {
static THREAD_MODELS: RefCell<HashMap<String, EmbeddingModel>> = RefCell::new(HashMap::new());
}
thread_local! {
static THREAD_TOKEN_CACHE: RefCell<Option<Arc<TokenCache>>> = const { RefCell::new(None) };
}
pub struct EmbeddingEngine {
backend: Arc<Mutex<LlamaBackend>>,
model_configs: Arc<RwLock<HashMap<String, EngineConfig>>>,
default_model: Option<String>,
embedding_cache: Option<Arc<EmbeddingCache>>,
token_cache: Option<Arc<TokenCache>>,
prefix_cache: Option<Arc<PrefixCache>>,
}
impl EmbeddingEngine {
fn get_or_create_backend() -> Result<Arc<Mutex<LlamaBackend>>> {
if let Some(backend) = BACKEND.get() {
return Ok(Arc::clone(backend));
}
let mut backend = LlamaBackend::init().map_err(|e| {
let error_str = format!("{e}");
if error_str.contains("BackendAlreadyInitialized") {
Error::ConfigurationError {
message: "LlamaBackend already initialized. This is an internal error."
.to_string(),
}
} else {
Error::ModelInitError {
message: "Failed to initialize llama backend".to_string(),
source: Some(anyhow::anyhow!("{e}")),
}
}
})?;
backend.void_logs();
let backend_arc = Arc::new(Mutex::new(backend));
match BACKEND.set(Arc::clone(&backend_arc)) {
Ok(()) => Ok(backend_arc),
Err(_) => Ok(Arc::clone(BACKEND.get().unwrap())),
}
}
#[instrument(skip(config), fields(model_name = %config.model_config.model_name))]
pub fn get_or_init(config: EngineConfig) -> Result<Arc<Mutex<Self>>> {
{
let instance_guard = INSTANCE.read();
if let Some(ref instance) = *instance_guard {
debug!("Returning existing engine instance");
return Ok(Arc::clone(instance));
}
}
let _lock = INIT_LOCK.lock().map_err(|_| Error::LockPoisoned)?;
{
let instance_guard = INSTANCE.read();
if let Some(ref instance) = *instance_guard {
debug!("Returning existing engine instance (after lock)");
return Ok(Arc::clone(instance));
}
}
info!("Initializing singleton embedding engine");
let engine = Self::new_internal(config)?;
let arc_engine = Arc::new(Mutex::new(engine));
{
let mut instance_guard = INSTANCE.write();
*instance_guard = Some(Arc::clone(&arc_engine));
}
Ok(arc_engine)
}
pub fn instance() -> Option<Arc<Mutex<Self>>> {
let instance_guard = INSTANCE.read();
instance_guard.as_ref().map(Arc::clone)
}
#[cfg(test)]
pub fn reset() {
let _lock = INIT_LOCK.lock().unwrap();
THREAD_MODELS.with(|models| {
models.borrow_mut().clear();
});
let mut instance_guard = INSTANCE.write();
if let Some(instance) = instance_guard.take() {
if Arc::strong_count(&instance) > 1 {
*instance_guard = Some(instance);
panic!(
"Cannot reset engine: other references exist. Ensure tests are marked with #[serial]"
);
}
drop(instance);
debug!("Dropped engine instance and backend");
}
info!("Engine singleton reset - backend dropped");
}
#[cfg(test)]
pub fn fresh_instance(config: EngineConfig) -> Result<Arc<Mutex<Self>>> {
Self::reset();
Self::get_or_init(config)
}
fn new_internal(config: EngineConfig) -> Result<Self> {
config.validate()?;
let model_name = config.model_config.model_name.clone();
info!("Initializing embedding engine with model: {}", model_name);
let backend = Self::get_or_create_backend()?;
info!("Llama backend ready");
let (embedding_cache, token_cache, prefix_cache) = if let Some(cache_config) = &config.cache
{
if cache_config.enabled {
info!(
"Initializing embedding cache with {} max entries",
cache_config.embedding_cache_size
);
let embedding_cache = Some(Arc::new(EmbeddingCache::new(
cache_config.embedding_cache_size as u64,
cache_config.ttl_seconds,
)));
info!(
"Initializing token cache with {} max entries",
cache_config.token_cache_size
);
let token_cache = Some(Arc::new(TokenCache::with_ttl(
cache_config.token_cache_size,
Some(cache_config.ttl_seconds),
)));
let prefix_cache = if cache_config.prefix_cache_enabled {
info!(
"Initializing prefix cache with {} max sessions",
cache_config.prefix_cache_size
);
Some(Arc::new(
PrefixCache::new(
cache_config.prefix_cache_size,
cache_config.ttl_seconds,
5, None, )
.map_err(|e| Error::ConfigurationError {
message: format!("Failed to create prefix cache: {e}"),
})?,
))
} else {
None
};
(embedding_cache, token_cache, prefix_cache)
} else {
(None, None, None)
}
} else {
(None, None, None)
};
let mut model_configs = HashMap::new();
model_configs.insert(model_name.clone(), config);
let engine = Self {
backend,
model_configs: Arc::new(RwLock::new(model_configs)),
default_model: Some(model_name.clone()),
embedding_cache,
token_cache: token_cache.clone(),
prefix_cache,
};
if let Some(ref cache) = token_cache {
THREAD_TOKEN_CACHE.with(|tc| {
*tc.borrow_mut() = Some(Arc::clone(cache));
});
}
engine.ensure_model_loaded(&model_name)?;
info!("Embedding engine initialized successfully");
Ok(engine)
}
pub fn new(config: EngineConfig) -> Result<Self> {
Self::new_internal(config)
}
#[instrument(skip(self, config), fields(model_name = %config.model_config.model_name))]
pub fn load_model(&mut self, config: EngineConfig) -> Result<()> {
config.validate()?;
let model_name = config.model_config.model_name.clone();
{
let configs = self.model_configs.read();
if configs.contains_key(&model_name) {
return Err(Error::ConfigurationError {
message: format!("Model '{model_name}' is already loaded"),
});
}
}
{
let mut configs = self.model_configs.write();
configs.insert(model_name.clone(), config);
}
if self.default_model.is_none() {
self.default_model = Some(model_name.clone());
}
info!("Model '{}' configuration added to registry", model_name);
Ok(())
}
#[instrument(skip(self))]
pub fn unregister_model(&mut self, model_name: &str) -> Result<()> {
{
let mut configs = self.model_configs.write();
if !configs.contains_key(model_name) {
return Err(Error::ModelNotFound {
name: model_name.to_string(),
});
}
configs.remove(model_name);
}
if self.default_model.as_ref() == Some(&model_name.to_string()) {
let configs = self.model_configs.read();
self.default_model = configs.keys().next().cloned();
}
info!("Model '{}' unregistered from config registry", model_name);
Ok(())
}
#[instrument(skip(self))]
pub fn drop_model_from_thread(&self, model_name: &str) -> Result<()> {
{
let configs = self.model_configs.read();
if !configs.contains_key(model_name) {
return Err(Error::ModelNotFound {
name: model_name.to_string(),
});
}
}
THREAD_MODELS.with(|models| {
let mut models = models.borrow_mut();
if models.remove(model_name).is_some() {
info!("Model '{}' dropped from current thread", model_name);
} else {
debug!("Model '{}' was not loaded in current thread", model_name);
}
});
Ok(())
}
#[instrument(skip(self))]
pub fn unload_model(&mut self, model_name: &str) -> Result<()> {
self.drop_model_from_thread(model_name)?;
self.unregister_model(model_name)?;
info!("Model '{}' fully unloaded", model_name);
Ok(())
}
fn ensure_model_loaded(&self, model_name: &str) -> Result<()> {
THREAD_MODELS.with(|models| {
let mut models = models.borrow_mut();
if models.contains_key(model_name) {
debug!("Model '{}' already loaded in current thread", model_name);
return Ok(());
}
let config = {
let configs = self.model_configs.read();
configs
.get(model_name)
.ok_or_else(|| Error::ModelNotFound {
name: model_name.to_string(),
})?
.clone()
};
info!("Loading model '{}' in current thread", model_name);
let backend_guard = self.backend.lock().map_err(|_| Error::LockPoisoned)?;
let model = EmbeddingModel::new(&backend_guard, &config.model_config)?;
drop(backend_guard);
{
let resolved = model.config();
let mut configs = self.model_configs.write();
if let Some(stored) = configs.get_mut(model_name) {
stored.model_config.pooling_strategy = resolved.pooling_strategy;
stored.model_config.normalization_mode = resolved.normalization_mode;
}
}
models.insert(model_name.to_string(), model);
info!(
"Model '{}' loaded successfully in current thread",
model_name
);
Ok(())
})
}
#[instrument(skip(self, text), fields(text_len = text.len()))]
pub fn embed(&self, model_name: Option<&str>, text: &str) -> Result<Vec<f32>> {
let model_name = model_name
.map(std::string::ToString::to_string)
.or_else(|| self.default_model.clone())
.ok_or_else(|| Error::ConfigurationError {
message: "No model specified and no default model set".to_string(),
})?;
let config = self
.model_configs
.read()
.get(&model_name)
.ok_or_else(|| Error::ModelNotFound {
name: model_name.clone(),
})?
.clone();
let truncate = config
.embedding
.as_ref()
.map_or(TruncateTokens::No, |e| e.truncate_tokens);
if let Some(cache) = &self.embedding_cache {
let key = EmbeddingCache::compute_key(
text,
&model_name,
config.model_config.pooling_strategy.unwrap_or_default(),
config.model_config.normalization_mode.unwrap_or_default(),
);
if let Some(embedding) = cache.get(&key) {
debug!("Cache hit for text of length {}", text.len());
return Ok(embedding);
}
debug!("Cache miss for text of length {}", text.len());
}
self.ensure_model_loaded(&model_name)?;
let embedding = THREAD_MODELS.with(|models| {
let mut models = models.borrow_mut();
let model = models
.get_mut(&model_name)
.ok_or_else(|| Error::ModelNotFound {
name: model_name.clone(),
})?;
if let Some(ref prefix_cache) = self.prefix_cache {
let tokens = model.tokenize(text)?;
let token_ids: Vec<i32> = tokens.iter().map(|t| t.0).collect();
if let Some((_prefix_len, _session_data)) =
prefix_cache.find_prefix_session(text, &token_ids)
{
debug!("Prefix cache hit for text of length {}", text.len());
return THREAD_TOKEN_CACHE.with(|tc| {
let cache_ref = tc.borrow();
model.generate_embedding_with_prefix(
text,
Some(prefix_cache.as_ref()),
cache_ref.as_deref(),
truncate,
)
});
}
}
THREAD_TOKEN_CACHE.with(|tc| {
let cache_ref = tc.borrow();
if let Some(ref cache) = *cache_ref {
model.generate_embedding_cached(text, Some(cache.as_ref()), truncate)
} else {
model.generate_embedding(text)
}
})
})?;
if let Some(cache) = &self.embedding_cache {
let config = self.model_configs.read();
let config = config.get(&model_name).unwrap();
let key = EmbeddingCache::compute_key(
text,
&model_name,
config.model_config.pooling_strategy.unwrap_or_default(),
config.model_config.normalization_mode.unwrap_or_default(),
);
cache.insert(key, embedding.clone());
debug!("Cached embedding for text of length {}", text.len());
}
Ok(embedding)
}
#[instrument(skip(self, text), fields(text_len = text.len()))]
pub fn embed_multi(&self, model_name: Option<&str>, text: &str) -> Result<Vec<Vec<f32>>> {
let model_name = model_name
.map(std::string::ToString::to_string)
.or_else(|| self.default_model.clone())
.ok_or_else(|| Error::ConfigurationError {
message: "No model specified and no default model set".to_string(),
})?;
let config = self
.model_configs
.read()
.get(&model_name)
.ok_or_else(|| Error::ModelNotFound {
name: model_name.clone(),
})?
.clone();
let truncate = config
.embedding
.as_ref()
.map_or(TruncateTokens::No, |e| e.truncate_tokens);
self.ensure_model_loaded(&model_name)?;
THREAD_MODELS.with(|models| {
let mut models = models.borrow_mut();
let model = models
.get_mut(&model_name)
.ok_or_else(|| Error::ModelNotFound {
name: model_name.clone(),
})?;
THREAD_TOKEN_CACHE.with(|tc| {
let cache_ref = tc.borrow();
model.generate_multi_embedding(text, cache_ref.as_deref(), truncate)
})
})
}
#[instrument(skip(self, texts), fields(batch_size = texts.len()))]
pub fn embed_batch_multi(
&self,
model_name: Option<&str>,
texts: &[&str],
) -> Result<Vec<Vec<Vec<f32>>>> {
let model_name = model_name
.map(std::string::ToString::to_string)
.or_else(|| self.default_model.clone())
.ok_or_else(|| Error::ConfigurationError {
message: "No model specified and no default model set".to_string(),
})?;
let config = self
.model_configs
.read()
.get(&model_name)
.ok_or_else(|| Error::ModelNotFound {
name: model_name.clone(),
})?
.clone();
let truncate = config
.embedding
.as_ref()
.map_or(TruncateTokens::No, |e| e.truncate_tokens);
self.ensure_model_loaded(&model_name)?;
THREAD_MODELS.with(|models| {
let mut models = models.borrow_mut();
let model = models
.get_mut(&model_name)
.ok_or_else(|| Error::ModelNotFound {
name: model_name.clone(),
})?;
let token_sequences: Vec<Vec<_>> = texts
.iter()
.map(|text| model.tokenize(text))
.collect::<Result<Vec<_>>>()?;
model.process_batch_tokens_multi(&token_sequences, truncate)
})
}
#[instrument(skip(self, texts), fields(batch_size = texts.len()))]
pub fn embed_batch(&self, model_name: Option<&str>, texts: &[&str]) -> Result<Vec<Vec<f32>>> {
let model_name = model_name
.map(std::string::ToString::to_string)
.or_else(|| self.default_model.clone())
.ok_or_else(|| Error::ConfigurationError {
message: "No model specified and no default model set".to_string(),
})?;
let config = self
.model_configs
.read()
.get(&model_name)
.ok_or_else(|| Error::ModelNotFound {
name: model_name.clone(),
})?
.clone();
let truncate = config
.embedding
.as_ref()
.map_or(TruncateTokens::No, |e| e.truncate_tokens);
let mut results = Vec::with_capacity(texts.len());
let mut uncached_indices = Vec::new();
let mut uncached_texts = Vec::new();
if let Some(cache) = &self.embedding_cache {
for (i, text) in texts.iter().enumerate() {
let key = EmbeddingCache::compute_key(
text,
&model_name,
config.model_config.pooling_strategy.unwrap_or_default(),
config.model_config.normalization_mode.unwrap_or_default(),
);
if let Some(embedding) = cache.get(&key) {
debug!("Batch cache hit for text {} of length {}", i, text.len());
results.push(Some(embedding));
} else {
debug!("Batch cache miss for text {} of length {}", i, text.len());
results.push(None);
uncached_indices.push(i);
uncached_texts.push(*text);
}
}
if uncached_texts.is_empty() {
debug!("All {} texts found in cache", texts.len());
return Ok(results.into_iter().map(|r| r.unwrap()).collect());
}
debug!(
"Processing {} uncached texts out of {}",
uncached_texts.len(),
texts.len()
);
} else {
uncached_texts = texts.to_vec();
}
self.ensure_model_loaded(&model_name)?;
let batch_processor = BatchProcessorBuilder::default()
.with_max_batch_size(64) .with_normalization(
config.model_config.normalization_mode.unwrap_or_default()
!= NormalizationMode::None,
)
.with_pooling_strategy(config.model_config.pooling_strategy.unwrap_or_default())
.build();
let new_embeddings = THREAD_MODELS.with(|models| {
let mut models = models.borrow_mut();
let model = models
.get_mut(&model_name)
.ok_or_else(|| Error::ModelNotFound {
name: model_name.clone(),
})?;
batch_processor.process_batch(model, &uncached_texts, truncate)
})?;
if let Some(cache) = &self.embedding_cache {
for (idx, embedding) in new_embeddings.into_iter().enumerate() {
let text = uncached_texts[idx];
let key = EmbeddingCache::compute_key(
text,
&model_name,
config.model_config.pooling_strategy.unwrap_or_default(),
config.model_config.normalization_mode.unwrap_or_default(),
);
cache.insert(key, embedding.clone());
let original_idx = uncached_indices[idx];
results[original_idx] = Some(embedding);
}
Ok(results.into_iter().map(|r| r.unwrap()).collect())
} else {
Ok(new_embeddings)
}
}
#[instrument(skip(self, query, documents), fields(query_len = query.len(), n_docs = documents.len()))]
pub fn rerank(
&self,
model_name: Option<&str>,
query: &str,
documents: &[&str],
top_n: Option<usize>,
normalize: bool,
) -> Result<Vec<crate::config::RerankResult>> {
let model_name = model_name
.map(std::string::ToString::to_string)
.or_else(|| self.default_model.clone())
.ok_or_else(|| Error::ConfigurationError {
message: "No model specified and no default model set".to_string(),
})?;
let config = self
.model_configs
.read()
.get(&model_name)
.ok_or_else(|| Error::ModelNotFound {
name: model_name.clone(),
})?
.clone();
let truncate = config
.embedding
.as_ref()
.map_or(TruncateTokens::No, |e| e.truncate_tokens);
self.ensure_model_loaded(&model_name)?;
let raw_scores = THREAD_MODELS.with(|models| {
let mut models = models.borrow_mut();
let model = models
.get_mut(&model_name)
.ok_or_else(|| Error::ModelNotFound {
name: model_name.clone(),
})?;
model.generate_rerank_scores_batch(query, documents, truncate)
})?;
let mut results: Vec<crate::config::RerankResult> = raw_scores
.into_iter()
.enumerate()
.map(|(index, score)| {
let relevance_score = if normalize {
1.0 / (1.0 + (-score).exp())
} else {
score
};
crate::config::RerankResult {
index,
relevance_score,
}
})
.collect();
for r in &results {
if r.relevance_score.is_nan() {
return Err(Error::EmbeddingGenerationError {
message: "Model produced NaN relevance score".to_string(),
source: None,
});
}
}
results.sort_by(|a, b| b.relevance_score.total_cmp(&a.relevance_score));
if let Some(n) = top_n {
results.truncate(n);
}
Ok(results)
}
pub fn list_models(&self) -> Vec<String> {
let configs = self.model_configs.read();
configs.keys().cloned().collect()
}
pub fn get_model_details(&self) -> Vec<(String, Option<u32>)> {
let configs = self.model_configs.read();
configs
.iter()
.map(|(name, config)| {
let context_size = config
.model_config
.context_size
.or(config.model_config.n_ctx);
(name.clone(), context_size)
})
.collect()
}
pub fn get_cache_stats(&self) -> Option<CacheStats> {
self.embedding_cache.as_ref().map(|cache| cache.stats())
}
pub fn clear_cache(&self) {
if let Some(cache) = &self.embedding_cache {
cache.clear();
info!("Embedding cache cleared");
}
if let Some(cache) = &self.token_cache {
cache.clear();
info!("Token cache cleared");
}
if let Some(cache) = &self.prefix_cache {
cache.clear();
info!("Prefix cache cleared");
}
}
pub fn warm_cache(&self, model_name: Option<&str>, texts: &[&str]) -> Result<()> {
if self.embedding_cache.is_none() {
return Ok(()); }
info!("Warming cache with {} texts", texts.len());
for text in texts {
self.embed(model_name, text)?;
}
info!("Cache warmed successfully");
Ok(())
}
pub fn is_cache_enabled(&self) -> bool {
self.embedding_cache.is_some()
}
pub fn is_model_registered(&self, model_name: &str) -> bool {
let configs = self.model_configs.read();
configs.contains_key(model_name)
}
pub fn is_model_loaded_in_thread(&self, model_name: &str) -> bool {
THREAD_MODELS.with(|models| {
let models = models.borrow();
models.contains_key(model_name)
})
}
pub fn default_model(&self) -> Option<String> {
self.default_model.clone()
}
pub fn set_default_model(&mut self, model_name: &str) -> Result<()> {
if !self.is_model_registered(model_name) {
return Err(Error::ModelNotFound {
name: model_name.to_string(),
});
}
self.default_model = Some(model_name.to_string());
Ok(())
}
pub fn model_info(&self, model_name: &str) -> Result<ModelInfo> {
self.ensure_model_loaded(model_name)?;
THREAD_MODELS.with(|models| {
let models = models.borrow();
let model = models.get(model_name).ok_or_else(|| Error::ModelNotFound {
name: model_name.to_string(),
})?;
Ok(ModelInfo {
name: model_name.to_string(),
dimensions: model.embedding_dimensions(),
max_tokens: model.max_sequence_length(),
model_size: model.model_size(),
})
})
}
pub fn warmup_model(&self, model_name: Option<&str>) -> Result<()> {
let resolved_name = model_name
.map(std::string::ToString::to_string)
.or_else(|| self.default_model.clone())
.ok_or_else(|| Error::ConfigurationError {
message: "No model specified and no default model set".to_string(),
})?;
self.ensure_model_loaded(&resolved_name)?;
let is_reranker = {
let configs = self.model_configs.read();
configs
.get(&resolved_name)
.and_then(|c| c.model_config.pooling_strategy)
== Some(crate::config::PoolingStrategy::Rank)
};
if is_reranker {
let _ = self.rerank(
Some(&resolved_name),
"warmup query",
&["warmup document"],
None,
false,
)?;
} else {
let _ = self.embed(
Some(&resolved_name),
"This is a warmup text for model initialization.",
)?;
}
debug!("Model warmed up successfully");
Ok(())
}
pub fn cleanup_thread_models(&self) {
THREAD_MODELS.with(|models| {
let mut models = models.borrow_mut();
let count = models.len();
models.clear();
if count > 0 {
info!("Cleared {} thread-local models", count);
}
});
}
pub fn register_prefix(&self, model_name: Option<&str>, prefix: &str) -> Result<()> {
if let Some(cache) = &self.prefix_cache {
let model_name = model_name
.map(std::string::ToString::to_string)
.or_else(|| self.default_model.clone())
.ok_or_else(|| Error::ConfigurationError {
message: "No model specified and no default model set".to_string(),
})?;
self.ensure_model_loaded(&model_name)?;
let (tokens, session_data) = THREAD_MODELS.with(|models| {
let mut models = models.borrow_mut();
let model = models
.get_mut(&model_name)
.ok_or_else(|| Error::ModelNotFound {
name: model_name.clone(),
})?;
let tokens = model.tokenize(prefix)?;
model.generate_embedding(prefix)?;
let session_data = model.save_session_state()?;
Ok::<_, Error>((tokens, session_data))
})?;
let token_ids: Vec<i32> = tokens.iter().map(|t| t.0).collect();
cache.register_prefix(prefix, &token_ids, session_data)?;
info!(
"Registered prefix of {} tokens for caching",
token_ids.len()
);
Ok(())
} else {
Err(Error::ConfigurationError {
message: "Prefix cache is not enabled".to_string(),
})
}
}
pub fn get_prefix_cache_stats(&self) -> Option<crate::cache::prefix_cache::PrefixCacheStats> {
self.prefix_cache.as_ref().map(|cache| cache.stats())
}
pub fn clear_prefix_cache(&self) {
if let Some(cache) = &self.prefix_cache {
cache.clear();
info!("Prefix cache cleared");
}
}
pub fn list_cached_prefixes(&self) -> Vec<String> {
if let Some(_cache) = &self.prefix_cache {
vec![]
} else {
vec![]
}
}
pub fn is_prefix_cache_enabled(&self) -> bool {
self.prefix_cache.is_some()
}
}
#[derive(Debug, Clone)]
pub struct ModelInfo {
pub name: String,
pub dimensions: usize,
pub max_tokens: usize,
pub model_size: Option<usize>,
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::tempdir;
fn create_test_config() -> EngineConfig {
let dir = tempdir().unwrap();
let model_path = dir.path().join("test_model.gguf");
fs::write(&model_path, b"dummy model file").unwrap();
EngineConfig::builder()
.with_model_path(model_path)
.with_model_name("test-model")
.build()
.unwrap()
}
#[test]
fn test_engine_creation() {
}
#[test]
fn test_model_listing() {
}
#[test]
#[ignore = "Requires actual GGUF model file"]
fn test_embedding_generation() {
let config = create_test_config();
let engine = EmbeddingEngine::new(config).unwrap();
let text = "Hello, world!";
let embedding = engine.embed(None, text).unwrap();
assert!(!embedding.is_empty());
}
}