use anyhow::{Context, Result};
use candle_core::Device;
use candle_transformers::models::qwen2::{Config as Qwen2Config, ModelForCausalLM};
use hf_hub::api::tokio::ApiRepo;
use std::path::{Path, PathBuf};
use tokenizers::Tokenizer;
use tracing::{debug, info, warn};
use super::model::Qwen3EmbeddingModel;
use crate::types::{CachePath, ModelName};
pub async fn download_file(repo: &ApiRepo, filename: &str, cache_dir: &Path) -> Result<PathBuf> {
let local_path = cache_dir.join(filename);
if local_path.exists() {
debug!("Using cached file: {}", local_path.display());
return Ok(local_path);
}
info!("[HUGGINGFACE] [DOWNLOAD] Downloading {}", filename);
let remote_file = repo
.get(filename)
.await
.with_context(|| format!("Failed to download {}", filename))?;
std::fs::copy(&remote_file, &local_path)
.with_context(|| format!("Failed to copy {} to cache", filename))?;
debug!("Downloaded and cached: {}", local_path.display());
Ok(local_path)
}
pub fn load_qwen3_weights(
model_path: &Path,
config: &Qwen2Config,
device: &Device,
) -> Result<ModelForCausalLM> {
debug!("Loading model weights from: {}", model_path.display());
let weights = if model_path.extension().and_then(|s| s.to_str()) == Some("safetensors") {
candle_core::safetensors::load(model_path, device)
.with_context(|| format!("Failed to load safetensors from {}", model_path.display()))?
} else {
debug!(
"Loading PyTorch model weights from: {}",
model_path.display()
);
match candle_core::pickle::read_all(model_path) {
Ok(tensor_vec) => {
debug!(
"[HUGGINGFACE] [LOAD] Successfully loaded {} PyTorch tensors",
tensor_vec.len()
);
tensor_vec
.into_iter()
.collect::<std::collections::HashMap<_, _>>()
}
Err(e) => {
warn!("[HUGGINGFACE] [LOAD] PyTorch model loading failed: {}", e);
return Err(anyhow::anyhow!(
"[HUGGINGFACE] [LOAD] failed: Cannot load PyTorch model from {}. Error: {}. Recommendation: Convert model to safetensors format for better compatibility, or verify model file integrity.",
model_path.display(),
e
));
}
}
};
let vb = candle_nn::VarBuilder::from_tensors(weights, candle_core::DType::F32, device);
let model = ModelForCausalLM::new(config, vb).with_context(|| {
format!(
"[HUGGINGFACE] [LOAD] failed: Cannot initialize model from weights at {}. Verify model architecture compatibility and weight completeness.",
model_path.display()
)
})?;
debug!("[HUGGINGFACE] [LOAD] Model weights loaded successfully");
Ok(model)
}
pub async fn load_qwen3_model(
api: &hf_hub::api::tokio::Api,
device: &Device,
model_name: &ModelName,
cache_dir: &CachePath,
) -> Result<Qwen3EmbeddingModel> {
info!("[HUGGINGFACE] [LOAD] Loading Qwen3 model: {}", model_name);
let repo = api.model(model_name.as_str().to_string());
let model_cache_dir = cache_dir.join(model_name.as_str().replace('/', "_"));
if !model_cache_dir.exists() {
std::fs::create_dir_all(&model_cache_dir).with_context(|| {
format!(
"Failed to create model cache directory: {}",
model_cache_dir.as_path().display()
)
})?;
}
let config_path = download_file(&repo, "config.json", model_cache_dir.as_path()).await?;
let tokenizer_path = download_file(&repo, "tokenizer.json", model_cache_dir.as_path()).await?;
let model_path = match download_file(&repo, "model.safetensors", model_cache_dir.as_path())
.await
{
Ok(path) => path,
Err(e) => {
warn!("[HUGGINGFACE] [DOWNLOAD] safetensors download failed: {}. Trying pytorch_model.bin as fallback", e);
download_file(&repo, "pytorch_model.bin", model_cache_dir.as_path())
.await
.context("[HUGGINGFACE] [DOWNLOAD] failed: Neither safetensors nor pytorch model weights could be downloaded. Check network connectivity and model repository availability.")?
}
};
let tokenizer = Tokenizer::from_file(&tokenizer_path).map_err(|e| {
anyhow::anyhow!(
"Failed to load tokenizer from {}: {}",
tokenizer_path.display(),
e
)
})?;
let config_content = std::fs::read_to_string(&config_path)
.with_context(|| format!("Failed to read model config from {}", config_path.display()))?;
let config_json: serde_json::Value = serde_json::from_str(&config_content)
.context("[HUGGINGFACE] [CONFIG] failed: Cannot parse model configuration JSON. Verify config file format and completeness.")?;
let qwen_config = super::config::parse_qwen2_config(&config_json)?;
let model = load_qwen3_weights(&model_path, &qwen_config, device)?;
Ok(Qwen3EmbeddingModel {
model: tokio::sync::RwLock::new(model),
tokenizer,
config: qwen_config,
device: device.clone(),
model_name: model_name.to_string(),
})
}