use std::path::{Path, PathBuf};
use crate::discovery::gguf_models::is_mmproj_name;
use crate::discovery::modality_hints::{
Hint, SentenceTransformersLayout, from_config_json, sentence_transformers_layout,
};
use crate::discovery::weights::{gguf_tree, primary_of};
use crate::records::{
Capability, ExecutionMode, JsonValue, Modality, ModelRecord, ParamSpec, ParamType, RunTier,
RuntimeId, SourceKind,
};
use crate::resolution::format::{
GgufFacts, ModelFormat, gguf_architecture_profile, ollama_profile,
};
use crate::resolution::gguf::{gguf_facts, has_ggml_magic, has_gguf_magic};
use crate::resolution::pipelines::{
PipelineFamilyRegistry, SchedulerFacts, diffusers_pipeline_class,
};
use crate::resolution::safetensors::safetensors_format;
#[derive(Debug, Clone, PartialEq)]
pub struct IdentifiedModel {
pub format: ModelFormat,
pub modality: Option<Modality>,
pub capabilities: Vec<Capability>,
pub execution: ExecutionMode,
pub params: Vec<ParamSpec>,
pub pipeline_class: Option<String>,
pub context_length: Option<i64>,
pub has_chat_template: Option<bool>,
pub quantization: Option<String>,
}
impl IdentifiedModel {
pub fn new(
format: ModelFormat,
modality: Option<Modality>,
capabilities: Vec<Capability>,
execution: ExecutionMode,
) -> Self {
Self {
format,
modality,
capabilities,
execution,
params: Vec::new(),
pipeline_class: None,
context_length: None,
has_chat_template: None,
quantization: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct RuntimeBid {
pub tier: RunTier,
pub preference: i64,
pub alternatives: Vec<RuntimeId>,
}
impl RuntimeBid {
pub fn new(tier: RunTier, preference: i64) -> Self {
Self {
tier,
preference,
alternatives: Vec::new(),
}
}
pub fn with_alternatives(tier: RunTier, preference: i64, alternatives: Vec<RuntimeId>) -> Self {
Self {
tier,
preference,
alternatives,
}
}
}
pub fn identify(record: &ModelRecord) -> IdentifiedModel {
let kind = &record.source.kind;
if *kind == SourceKind::builtin() {
return chat_model(ModelFormat::Builtin, builtin_params());
}
if *kind == SourceKind::endpoint() {
return chat_model(ModelFormat::Endpoint, endpoint_params());
}
if *kind == SourceKind::ollama() {
let facts = record
.primary_weight_path
.as_deref()
.and_then(|path| gguf_facts(Path::new(path)));
let profile = ollama_profile(
manifest_has_projector_layer(&record.source.path),
facts
.as_ref()
.and_then(|facts| facts.architecture.as_deref()),
);
let mut model = IdentifiedModel::new(
ModelFormat::OllamaStore,
Some(profile.modality),
profile.capabilities,
profile.execution,
);
model.quantization = facts.and_then(|facts| facts.quantization);
return model;
}
let base = Path::new(&record.source.path);
let container = container_url(base, record);
let extension = base
.extension()
.and_then(|ext| ext.to_str())
.map(str::to_ascii_lowercase);
if extension.as_deref() == Some("bin") && has_ggml_magic(base) {
return IdentifiedModel::new(
ModelFormat::GgmlBin,
Some(Modality::audio()),
vec![Capability::transcribe()],
ExecutionMode::Stream,
);
}
if extension.as_deref() == Some("gguf") || has_gguf_magic(base) {
return identify_gguf(base, has_mmproj_companion(base));
}
let model_index = container.join("model_index.json");
if model_index.exists() {
return identify_diffusers(&model_index, &container, record);
}
let config = container.join("config.json");
let hint = from_config_json(&config);
if let Some(format) = safetensors_format(&container, &config) {
return identify_safetensors(format, hint.as_ref(), &container);
}
if let Some((weights, has_projector)) =
gguf_weights(&container, record.primary_weight_path.as_deref())
{
return identify_gguf(&weights, has_projector);
}
match hint {
Some(hint) => {
let mut model = IdentifiedModel::new(
ModelFormat::Unknown,
hint.modality,
hint.capabilities,
hint.execution,
);
model.context_length = hint.context_length;
model
}
None => IdentifiedModel::new(ModelFormat::Unknown, None, Vec::new(), ExecutionMode::Sync),
}
}
fn gguf_weights(container: &Path, primary: Option<&str>) -> Option<(PathBuf, bool)> {
if let Some(primary) = primary
&& !has_gguf_magic(Path::new(primary))
{
return None;
}
let tree = gguf_tree(container);
let weights = primary_of(&tree.weights)?;
Some((weights, tree.has_projector))
}
fn identify_gguf(base: &Path, has_projector: bool) -> IdentifiedModel {
let name = base
.file_name()
.and_then(|name| name.to_str())
.unwrap_or_default();
if is_mmproj_name(name) {
return IdentifiedModel::new(
ModelFormat::Gguf,
Some(Modality::vision()),
Vec::new(),
ExecutionMode::Sync,
);
}
let facts = gguf_facts(base);
if let Some(architecture) = facts
.as_ref()
.and_then(|facts| facts.architecture.as_deref())
&& let Some(profile) = gguf_architecture_profile(architecture)
{
let mut capabilities = profile.capabilities;
if !has_projector {
capabilities.retain(|capability| *capability != Capability::see());
}
let mut model = IdentifiedModel::new(
ModelFormat::Gguf,
Some(profile.modality),
capabilities,
profile.execution,
);
apply_facts(&mut model, facts.as_ref());
return model;
}
let capabilities = if has_projector {
vec![
Capability::chat(),
Capability::complete(),
Capability::see(),
]
} else {
vec![Capability::chat(), Capability::complete()]
};
let mut model = IdentifiedModel::new(
ModelFormat::Gguf,
Some(Modality::text()),
capabilities,
ExecutionMode::Stream,
);
apply_facts(&mut model, facts.as_ref());
model
}
fn identify_safetensors(
format: ModelFormat,
hint: Option<&Hint>,
container: &Path,
) -> IdentifiedModel {
let hint_modality = hint.and_then(|hint| hint.modality.clone());
let text = Some(Modality::text());
if hint_modality.is_none() || hint_modality == text {
let refined = match sentence_transformers_layout(container) {
Some(SentenceTransformersLayout::Embedder) => {
Some((Modality::embedding(), vec![Capability::embed()]))
}
Some(SentenceTransformersLayout::CrossEncoder) => Some((Modality::text(), Vec::new())),
None => None,
};
if let Some((modality, capabilities)) = refined {
let mut model =
IdentifiedModel::new(format, Some(modality), capabilities, ExecutionMode::Stream);
apply_hint(&mut model, hint);
return model;
}
}
let mut model = IdentifiedModel::new(
format,
hint.and_then(|hint| hint.modality.clone()),
hint.map(|hint| hint.capabilities.clone())
.unwrap_or_default(),
hint.map_or(ExecutionMode::Sync, |hint| hint.execution),
);
apply_hint(&mut model, hint);
model
}
fn apply_facts(model: &mut IdentifiedModel, facts: Option<&GgufFacts>) {
model.context_length = facts.and_then(|facts| facts.context_length);
model.has_chat_template = facts.map(|facts| facts.has_chat_template);
model.quantization = facts.and_then(|facts| facts.quantization.clone());
}
fn apply_hint(model: &mut IdentifiedModel, hint: Option<&Hint>) {
model.context_length = hint.and_then(|hint| hint.context_length);
model.quantization = hint.and_then(|hint| hint.quantization.clone());
}
fn chat_model(format: ModelFormat, params: Vec<ParamSpec>) -> IdentifiedModel {
let mut model = IdentifiedModel::new(
format,
Some(Modality::text()),
vec![Capability::chat(), Capability::complete()],
ExecutionMode::Stream,
);
model.params = params;
model
}
fn container_url(base: &Path, record: &ModelRecord) -> PathBuf {
if record.source.kind == SourceKind::huggingface_cache()
&& let Some(reference) = &record.source.reference
{
let snapshot = base.join("snapshots").join(reference);
if snapshot.exists() {
return snapshot;
}
}
base.to_path_buf()
}
fn manifest_has_projector_layer(path: &str) -> bool {
let Ok(bytes) = std::fs::read(path) else {
return false;
};
let Ok(JsonValue::Object(object)) = serde_json::from_slice::<JsonValue>(&bytes) else {
return false;
};
let Some(JsonValue::Array(layers)) = object.get("layers") else {
return false;
};
layers.iter().any(|layer| {
layer
.as_object()
.and_then(|fields| fields.get("mediaType"))
.and_then(JsonValue::as_str)
.is_some_and(|media| media.ends_with(".projector"))
})
}
fn has_mmproj_companion(base: &Path) -> bool {
let Some(directory) = base.parent() else {
return false;
};
let base_name = base.file_name().and_then(|name| name.to_str());
let Ok(entries) = std::fs::read_dir(directory) else {
return false;
};
entries.flatten().any(|entry| {
let path = entry.path();
let name = path.file_name().and_then(|name| name.to_str());
name != base_name
&& name.is_some_and(|name| !name.starts_with('.') && is_mmproj_name(name))
&& path
.extension()
.and_then(|ext| ext.to_str())
.is_some_and(|ext| ext.eq_ignore_ascii_case("gguf"))
})
}
fn identify_diffusers(
model_index: &Path,
container: &Path,
record: &ModelRecord,
) -> IdentifiedModel {
let pipeline_class = diffusers_pipeline_class(model_index);
let scheduler = scheduler_facts(container);
let repo_hint = record.source.repo.as_deref().unwrap_or(&record.name);
let profile = pipeline_class.as_deref().and_then(|class| {
PipelineFamilyRegistry::shared().profile(class, scheduler.as_ref(), Some(repo_hint))
});
let Some(profile) = profile else {
let mut model =
IdentifiedModel::new(ModelFormat::Diffusers, None, Vec::new(), ExecutionMode::Job);
model.pipeline_class = pipeline_class;
return model;
};
let mut params = profile.params;
if pipeline_class.as_deref() == Some("FluxPipeline") && !flux_uses_guidance(container) {
params.retain(|spec| spec.key != "guidance");
}
let mut model = IdentifiedModel::new(
ModelFormat::Diffusers,
Some(profile.modality),
profile.capabilities,
ExecutionMode::Job,
);
model.params = params;
model.pipeline_class = pipeline_class;
model
}
fn scheduler_facts(container: &Path) -> Option<SchedulerFacts> {
let path = container.join("scheduler").join("scheduler_config.json");
let bytes = std::fs::read(path).ok()?;
let JsonValue::Object(config) = serde_json::from_slice::<JsonValue>(&bytes).ok()? else {
return None;
};
Some(SchedulerFacts::new(
config
.get("_class_name")
.and_then(JsonValue::as_str)
.map(str::to_owned),
config
.get("timestep_spacing")
.and_then(JsonValue::as_str)
.map(str::to_owned),
))
}
fn flux_uses_guidance(container: &Path) -> bool {
let path = container.join("transformer").join("config.json");
let Ok(bytes) = std::fs::read(path) else {
return false;
};
let Ok(JsonValue::Object(config)) = serde_json::from_slice::<JsonValue>(&bytes) else {
return false;
};
config
.get("guidance_embeds")
.and_then(JsonValue::as_bool)
.unwrap_or(false)
}
fn param(key: &str, param_type: ParamType, range: Option<Vec<JsonValue>>) -> ParamSpec {
ParamSpec {
key: key.to_owned(),
param_type,
default_value: None,
range,
values: None,
}
}
fn builtin_params() -> Vec<ParamSpec> {
vec![
param(
"temperature",
ParamType::Float,
Some(vec![JsonValue::Double(0.0), JsonValue::Double(2.0)]),
),
param(
"top_p",
ParamType::Float,
Some(vec![JsonValue::Double(0.0), JsonValue::Double(1.0)]),
),
param(
"top_k",
ParamType::Int,
Some(vec![JsonValue::Int(0), JsonValue::Int(100)]),
),
param(
"max_tokens",
ParamType::Int,
Some(vec![JsonValue::Int(1), JsonValue::Int(4096)]),
),
param("seed", ParamType::Int, None),
]
}
fn endpoint_params() -> Vec<ParamSpec> {
vec![
param(
"temperature",
ParamType::Float,
Some(vec![JsonValue::Double(0.0), JsonValue::Double(2.0)]),
),
param(
"top_p",
ParamType::Float,
Some(vec![JsonValue::Double(0.0), JsonValue::Double(1.0)]),
),
param(
"max_tokens",
ParamType::Int,
Some(vec![JsonValue::Int(1), JsonValue::Int(32768)]),
),
param("stop", ParamType::String, None),
param("seed", ParamType::Int, None),
param(
"frequency_penalty",
ParamType::Float,
Some(vec![JsonValue::Double(-2.0), JsonValue::Double(2.0)]),
),
param(
"presence_penalty",
ParamType::Float,
Some(vec![JsonValue::Double(-2.0), JsonValue::Double(2.0)]),
),
]
}