use anyhow::Result;
use tracing::info;
use crate::models::{ModelInfo, ModelManager};
use crate::types::ModelBackend;
pub async fn validate_model_selection(model_name: &str) -> Result<ModelInfo> {
let model_manager = ModelManager::default();
let available_models = model_manager.get_available_models();
let model_info = available_models
.iter()
.find(|m| m.name.as_str() == model_name)
.ok_or_else(|| {
anyhow::anyhow!(
"Model '{}' is not available. Use 'tp model list' to see available models.",
model_name
)
})?;
let manager = ModelManager::default();
if !manager.is_model_cached(&model_info.name) {
match model_info.backend {
ModelBackend::FastEmbed => {
info!("Model will be downloaded automatically on first use");
}
ModelBackend::Candle => {
return Err(anyhow::anyhow!(
"GGUF model '{}' is not cached. Download it first with: tp model download {}",
model_name,
model_name
));
}
ModelBackend::Custom => {
return Err(anyhow::anyhow!(
"HuggingFace model '{}' is not cached. Download it first with: tp model download {}",
model_name, model_name
));
}
}
}
Ok(model_info.clone())
}
pub fn validate_instruction_compatibility(
model_info: &ModelInfo,
instruction: Option<&str>,
) -> Result<()> {
if let Some(_instr) = instruction {
let supports_instructions = model_info.name.as_str().contains("qwen3")
|| model_info.name.as_str().contains("Qwen3");
if !supports_instructions {
return Err(anyhow::anyhow!(
"Model '{}' does not support instruction-based embeddings. \
The --instruction parameter can only be used with instruction-capable models like Qwen3. \
Use 'tp model list' to see which models support instructions.",
model_info.name.as_str()
));
}
}
Ok(())
}
pub fn resolve_effective_model(
command_model: Option<&str>,
global_model: Option<&str>,
config_default: Option<&str>,
) -> String {
command_model
.or(global_model)
.or(config_default)
.unwrap_or_else(|| ModelManager::default_model())
.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::ModelName;
#[tokio::test]
async fn test_validate_model_selection_existing() {
let result = validate_model_selection("sentence-transformers/all-MiniLM-L6-v2").await;
assert!(result.is_ok());
let model_info = result.unwrap();
assert_eq!(
model_info.name.as_str(),
"sentence-transformers/all-MiniLM-L6-v2"
);
}
#[tokio::test]
async fn test_validate_model_selection_nonexistent() {
let result = validate_model_selection("nonexistent-model").await;
assert!(result.is_err());
let error_msg = result.unwrap_err().to_string();
assert!(error_msg.contains("not available"));
assert!(error_msg.contains("tp model list"));
}
#[test]
fn test_validate_instruction_compatibility_qwen3() {
let model_info = ModelInfo::simple(
ModelName::from("Qwen/Qwen3-Embedding-0.6B"),
"Test Qwen3 model".to_string(),
1024,
1000000,
);
let result = validate_instruction_compatibility(&model_info, Some("test instruction"));
assert!(result.is_ok());
let result = validate_instruction_compatibility(&model_info, None);
assert!(result.is_ok());
}
#[test]
fn test_validate_instruction_compatibility_non_instruction_model() {
let model_info = ModelInfo::simple(
ModelName::from("sentence-transformers/all-MiniLM-L6-v2"),
"Test non-instruction model".to_string(),
384,
1000000,
);
let result = validate_instruction_compatibility(&model_info, Some("test instruction"));
assert!(result.is_err());
let error_msg = result.unwrap_err().to_string();
assert!(error_msg.contains("does not support instruction-based embeddings"));
let result = validate_instruction_compatibility(&model_info, None);
assert!(result.is_ok());
}
#[test]
fn test_resolve_effective_model_precedence() {
let result = resolve_effective_model(
Some("command-model"),
Some("global-model"),
Some("config-model"),
);
assert_eq!(result, "command-model");
let result = resolve_effective_model(None, Some("global-model"), Some("config-model"));
assert_eq!(result, "global-model");
let result = resolve_effective_model(None, None, Some("config-model"));
assert_eq!(result, "config-model");
let result = resolve_effective_model(None, None, None);
assert_eq!(result, ModelManager::default_model());
}
}