use crate::config::SemantexConfig;
use crate::model::capabilities::ModelCapabilities;
use crate::model::spec::{
EmbedderSpec, ModelRole, ModelSource, ModelSpec, Pooling, QuantKind, RerankerSpec, RoleData,
ScoreStrategyKind,
};
use anyhow::Result;
use serde::Deserialize;
use std::path::{Path, PathBuf};
pub fn builtin_specs() -> Vec<ModelSpec> {
let mut specs = vec![
ModelSpec {
id: "coderank-137m".to_string(),
role: ModelRole::Embedder,
source: ModelSource::Hf {
repo: "MisterTK/CodeRankEmbed-onnx-int8".to_string(),
files: vec![
"model_int8.onnx".to_string(),
"model_int8.onnx.data".to_string(),
"tokenizer.json".to_string(),
"config.json".to_string(),
],
},
capabilities: ModelCapabilities {
multi_vector: false,
..ModelCapabilities::default()
},
role_data: RoleData::Embedder(EmbedderSpec {
dims: 768,
max_context: 8192,
query_prefix: "Represent this query for searching relevant code: ".to_string(),
doc_prefix: String::new(),
pooling: Pooling::Mean,
normalize: true,
quant: QuantKind::Int8Symmetric,
}),
},
ModelSpec {
id: "lateon-colbert".to_string(),
role: ModelRole::Embedder,
source: ModelSource::Hf {
repo: "lightonai/LateOn-Code-edge".to_string(),
files: vec![
"model_int8.onnx".to_string(),
"tokenizer.json".to_string(),
"onnx_config.json".to_string(),
],
},
capabilities: ModelCapabilities {
multi_vector: true,
..ModelCapabilities::default()
},
role_data: RoleData::Embedder(EmbedderSpec {
dims: 48,
max_context: 512,
query_prefix: String::new(),
doc_prefix: String::new(),
pooling: Pooling::LateInteraction,
normalize: true,
quant: QuantKind::Int8Symmetric,
}),
},
ModelSpec {
id: "qwen3-embed-0.6b".to_string(),
role: ModelRole::Embedder,
source: ModelSource::Hf {
repo: "Qwen/Qwen3-Embedding-0.6B".to_string(),
files: vec![
"model_int8.onnx".to_string(),
"tokenizer.json".to_string(),
"config.json".to_string(),
],
},
capabilities: ModelCapabilities {
multi_vector: false,
instruction_aware: true,
..ModelCapabilities::default()
},
role_data: RoleData::Embedder(EmbedderSpec {
dims: 1024,
max_context: 32768,
query_prefix:
"Instruct: Given a code search query, retrieve relevant code snippets.\nQuery: "
.to_string(),
doc_prefix: String::new(),
pooling: Pooling::Mean,
normalize: true,
quant: QuantKind::Int8Symmetric,
}),
},
ModelSpec {
id: "bge-reranker-v2-m3".to_string(),
role: ModelRole::Reranker,
source: ModelSource::Hf {
repo: "BAAI/bge-reranker-v2-m3".to_string(),
files: vec![
"model_int8.onnx".to_string(),
"tokenizer.json".to_string(),
"config.json".to_string(),
],
},
capabilities: ModelCapabilities::default(),
role_data: RoleData::Reranker(RerankerSpec {
score_strategy: ScoreStrategyKind::ClassifierHead,
max_context: 512,
prompt_prefix: String::new(),
prompt_middle: String::new(),
prompt_suffix: String::new(),
yes_token_id: None,
no_token_id: None,
}),
},
ModelSpec {
id: "qwen3-reranker-0.6b".to_string(),
role: ModelRole::Reranker,
source: ModelSource::Hf {
repo: "MisterTK/Qwen3-Reranker-0.6B-onnx".to_string(),
files: vec![
"model.onnx".to_string(),
"tokenizer.json".to_string(),
"config.json".to_string(),
],
},
capabilities: ModelCapabilities::default(),
role_data: RoleData::Reranker(RerankerSpec {
score_strategy: ScoreStrategyKind::YesNoLogit,
max_context: 2048,
prompt_prefix: concat!(
"<|im_start|>system\n",
"Judge whether the Document meets the requirements based on the Query and the Instruct provided. ",
"Note that the answer can only be \"yes\" or \"no\".<|im_end|>\n",
"<|im_start|>user\n",
"<Instruct>: Given a code search query, judge whether the document is relevant.\n",
"<Query>: "
)
.to_string(),
prompt_middle: "\n<Document>: ".to_string(),
prompt_suffix: concat!(
"<|im_end|>\n",
"<|im_start|>assistant\n",
"<think>\n\n</think>\n\n"
)
.to_string(),
yes_token_id: Some(9693),
no_token_id: Some(2152),
}),
},
];
append_builtin_llm_specs(&mut specs);
specs
}
#[cfg(not(feature = "llm"))]
fn append_builtin_llm_specs(_specs: &mut Vec<ModelSpec>) {}
#[cfg(feature = "llm")]
fn append_builtin_llm_specs(specs: &mut Vec<ModelSpec>) {
use crate::model::spec::LlmSpec;
specs.push(ModelSpec {
id: "ollama-default".to_string(),
role: ModelRole::Llm,
source: ModelSource::Local {
dir: "ollama".to_string(),
},
capabilities: ModelCapabilities {
instruction_aware: true,
..ModelCapabilities::default()
},
role_data: RoleData::Llm(LlmSpec {
provider: "ollama".to_string(),
model: "qwen2.5-coder:7b".to_string(),
endpoint: String::new(),
}),
});
}
#[derive(Debug, Deserialize)]
struct UserManifest {
#[serde(default)]
model: Vec<ModelSpec>,
}
pub fn load_user_manifest(path: &Path) -> Result<Vec<ModelSpec>> {
let text = std::fs::read_to_string(path)
.map_err(|e| anyhow::anyhow!("failed to read model manifest {}: {e}", path.display()))?;
check_one_role_table_per_model(&text, path)?;
let manifest: UserManifest = toml::from_str(&text)
.map_err(|e| anyhow::anyhow!("failed to parse model manifest {}: {e}", path.display()))?;
for spec in &manifest.model {
spec.validate()
.map_err(|e| anyhow::anyhow!("invalid model in {}: {e}", path.display()))?;
}
Ok(manifest.model)
}
const ROLE_TABLE_KEYS: [&str; 3] = ["embedder", "reranker", "llm"];
fn check_one_role_table_per_model(text: &str, path: &Path) -> Result<()> {
let raw: toml::Value = toml::from_str(text)
.map_err(|e| anyhow::anyhow!("failed to parse model manifest {}: {e}", path.display()))?;
let Some(models) = raw.get("model").and_then(toml::Value::as_array) else {
return Ok(()); };
for (idx, model) in models.iter().enumerate() {
let present: Vec<&str> = ROLE_TABLE_KEYS
.iter()
.copied()
.filter(|k| model.get(*k).is_some())
.collect();
if present.len() != 1 {
let id = model
.get("id")
.and_then(toml::Value::as_str)
.map_or_else(|| format!("#{idx}"), |s| format!("`{s}`"));
anyhow::bail!(
"model {id} in {}: expected exactly one role table \
({}), found {} ({:?})",
path.display(),
ROLE_TABLE_KEYS.join(", "),
present.len(),
present,
);
}
}
Ok(())
}
pub fn user_manifest_path(project_path: Option<&Path>) -> Option<PathBuf> {
if let Some(project) = project_path {
let local = SemantexConfig::project_index_dir(project).join("models.toml");
if local.exists() {
return Some(local);
}
}
let global = SemantexConfig::semantex_home().join("models.toml");
if global.exists() {
return Some(global);
}
None
}
#[must_use]
pub fn merge(builtin: Vec<ModelSpec>, user: Vec<ModelSpec>) -> Vec<ModelSpec> {
let mut out = builtin;
for u in user {
if let Some(existing) = out.iter_mut().find(|s| s.id == u.id) {
*existing = u; } else {
out.push(u); }
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builtins_include_both_embedders_and_both_rerankers() {
let specs = builtin_specs();
let ids: Vec<&str> = specs.iter().map(|s| s.id.as_str()).collect();
assert!(
ids.contains(&"coderank-137m"),
"missing coderank-137m: {ids:?}"
);
assert!(
ids.contains(&"qwen3-embed-0.6b"),
"missing qwen3-embed-0.6b: {ids:?}"
);
assert!(ids.contains(&"bge-reranker-v2-m3"), "missing bge: {ids:?}");
assert!(
ids.contains(&"qwen3-reranker-0.6b"),
"missing qwen3: {ids:?}"
);
assert!(
ids.contains(&"lateon-colbert"),
"missing lateon-colbert: {ids:?}"
);
}
#[test]
fn coderank_embedder_carries_recorded_nuance() {
let s = builtin_specs()
.into_iter()
.find(|s| s.id == "coderank-137m")
.unwrap();
assert_eq!(s.role, ModelRole::Embedder);
let RoleData::Embedder(e) = &s.role_data else {
panic!("coderank-137m must be an embedder");
};
assert_eq!(e.dims, 768);
assert_eq!(e.pooling, Pooling::Mean);
assert_eq!(e.quant, QuantKind::Int8Symmetric);
assert!(
e.query_prefix.ends_with(' '),
"recorded prefix keeps trailing space"
);
assert!(e.doc_prefix.is_empty(), "documents get no prefix");
assert!(!s.capabilities.multi_vector);
}
#[test]
fn qwen3_embed_resolves_validates_and_routes_to_hnsw() {
use crate::model::capabilities::{BackendKind, backend_for};
let s = builtin_specs()
.into_iter()
.find(|s| s.id == "qwen3-embed-0.6b")
.expect("qwen3-embed-0.6b must be a built-in");
assert_eq!(s.role, ModelRole::Embedder);
s.validate().expect("qwen3-embed-0.6b must validate");
let RoleData::Embedder(e) = &s.role_data else {
panic!("qwen3-embed-0.6b must be an embedder");
};
assert_eq!(e.dims, 1024);
assert_eq!(e.pooling, Pooling::Mean);
assert!(e.normalize);
assert_eq!(e.quant, QuantKind::Int8Symmetric);
assert!(!s.capabilities.multi_vector);
assert!(s.capabilities.instruction_aware);
assert_eq!(
backend_for(&s.capabilities).unwrap(),
BackendKind::CoderankHnsw
);
}
#[test]
fn only_lateon_colbert_is_multi_vector() {
for s in builtin_specs() {
let expect_mv = s.id == "lateon-colbert";
assert_eq!(
s.capabilities.multi_vector, expect_mv,
"built-in `{}` multi_vector should be {expect_mv}",
s.id
);
}
}
#[test]
fn qwen3_reranker_is_yes_no_with_template() {
let s = builtin_specs()
.into_iter()
.find(|s| s.id == "qwen3-reranker-0.6b")
.unwrap();
assert_eq!(s.role, ModelRole::Reranker);
let RoleData::Reranker(r) = &s.role_data else {
panic!("qwen3 must be a reranker");
};
assert_eq!(r.score_strategy, ScoreStrategyKind::YesNoLogit);
assert!(r.yes_token_id.is_some());
assert!(r.no_token_id.is_some());
}
#[test]
fn bge_reranker_is_classifier_head() {
let s = builtin_specs()
.into_iter()
.find(|s| s.id == "bge-reranker-v2-m3")
.unwrap();
let RoleData::Reranker(r) = &s.role_data else {
panic!("bge must be a reranker");
};
assert_eq!(r.score_strategy, ScoreStrategyKind::ClassifierHead);
}
#[test]
fn all_builtins_validate() {
for s in builtin_specs() {
s.validate()
.unwrap_or_else(|e| panic!("builtin {} invalid: {e}", s.id));
}
}
#[test]
fn load_user_manifest_parses_a_second_embedder() {
let tmp = tempfile::TempDir::new().unwrap();
let path = tmp.path().join("models.toml");
std::fs::write(
&path,
r#"
[[model]]
id = "gte-modernbert-hnsw"
role = "embedder"
[model.source]
kind = "hf"
repo = "Alibaba-NLP/gte-modernbert-base"
files = ["model_int8.onnx", "tokenizer.json"]
[model.capabilities]
multi_vector = false
[model.embedder]
dims = 768
max_context = 8192
query_prefix = ""
pooling = "cls"
quant = "int8_symmetric"
"#,
)
.unwrap();
let specs = load_user_manifest(&path).unwrap();
assert_eq!(specs.len(), 1);
assert_eq!(specs[0].id, "gte-modernbert-hnsw");
assert_eq!(specs[0].role, ModelRole::Embedder);
specs[0].validate().unwrap();
}
#[test]
fn load_user_manifest_errors_clearly_on_bad_spec() {
let tmp = tempfile::TempDir::new().unwrap();
let path = tmp.path().join("models.toml");
std::fs::write(
&path,
r#"
[[model]]
id = "broken"
role = "embedder"
[model.source]
kind = "hf"
repo = "x/y"
files = ["model_int8.onnx"]
[model.embedder]
dims = 0
max_context = 8192
pooling = "mean"
quant = "none"
"#,
)
.unwrap();
let err = load_user_manifest(&path).expect_err("dims=0 must error");
let msg = err.to_string();
assert!(msg.contains("broken") && msg.contains("dims"), "got: {msg}");
}
#[test]
fn load_user_manifest_errors_on_multiple_role_tables() {
let tmp = tempfile::TempDir::new().unwrap();
let path = tmp.path().join("models.toml");
std::fs::write(
&path,
r#"
[[model]]
id = "two-tables"
role = "embedder"
[model.source]
kind = "hf"
repo = "x/y"
files = ["model_int8.onnx"]
[model.embedder]
dims = 768
max_context = 8192
pooling = "cls"
quant = "int8_symmetric"
[model.reranker]
score_strategy = "classifier_head"
"#,
)
.unwrap();
let err = load_user_manifest(&path).expect_err("two role tables must error");
let msg = err.to_string();
assert!(msg.contains("two-tables"), "must name the model id: {msg}");
assert!(
msg.contains("embedder") && msg.contains("reranker"),
"must name the conflicting tables: {msg}"
);
}
#[test]
fn merge_lets_user_override_a_builtin_by_id() {
let builtin = builtin_specs();
let mut overridden = builtin
.iter()
.find(|s| s.id == "coderank-137m")
.cloned()
.unwrap();
if let RoleData::Embedder(e) = &mut overridden.role_data {
e.max_context = 4096; }
let merged = merge(builtin.clone(), vec![overridden]);
assert_eq!(merged.len(), builtin.len());
let s = merged.iter().find(|s| s.id == "coderank-137m").unwrap();
let RoleData::Embedder(e) = &s.role_data else {
panic!()
};
assert_eq!(e.max_context, 4096, "user override must win");
}
#[test]
fn merge_appends_a_new_user_id() {
let builtin = builtin_specs();
let mut newspec = builtin
.iter()
.find(|s| s.id == "coderank-137m")
.cloned()
.unwrap();
newspec.id = "my-custom-embedder".to_string();
let merged = merge(builtin.clone(), vec![newspec]);
assert_eq!(merged.len(), builtin.len() + 1);
assert!(merged.iter().any(|s| s.id == "my-custom-embedder"));
}
}