use std::fs;
use std::path::Path;
use anyhow::{Context, Result, ensure};
use serde_json::Value;
use crate::gguf_writer::GgufKv;
use crate::inkling_metadata;
use crate::tokenizer_metadata::push_tokenizer_metadata;
#[derive(Debug, Clone, Copy)]
pub struct MetadataOptions {
pub include_mtp: bool,
}
impl Default for MetadataOptions {
fn default() -> Self {
Self { include_mtp: true }
}
}
pub fn metadata_from_hf_config(source: &Path, tensor_count: usize) -> Result<Vec<GgufKv>> {
metadata_from_hf_config_with_options(source, tensor_count, MetadataOptions::default())
}
pub fn mtp_layer_start_from_hf_config(source: &Path) -> Result<Option<u32>> {
let config = read_hf_config(source)?;
if inkling_metadata::is_inkling_config(&config) {
return inkling_metadata::mtp_layer_start(&config);
}
let Some(nextn_layers) = optional_u32(&config, "num_nextn_predict_layers")
.or_else(|| optional_u32(&config, "mtp_num_hidden_layers"))
else {
return Ok(None);
};
if nextn_layers == 0 {
return Ok(None);
}
required_u32(&config, "num_hidden_layers").map(Some)
}
pub fn metadata_from_hf_config_with_options(
source: &Path,
tensor_count: usize,
options: MetadataOptions,
) -> Result<Vec<GgufKv>> {
let config = read_hf_config(source)?;
if inkling_metadata::is_inkling_config(&config) {
return inkling_metadata::metadata(source, tensor_count, &config, options.include_mtp);
}
let arch = architecture_name(&config)?;
let mut metadata = vec![
GgufKv::string("general.architecture", arch),
GgufKv::string("general.name", model_name(source)),
GgufKv::bool("skippy.convert.raw_safetensors", false),
GgufKv::u64("skippy.convert.tensor_count", tensor_count as u64),
];
push_common_llm_metadata(&mut metadata, arch, &config, options)?;
push_attention_metadata(&mut metadata, arch, &config)?;
push_glm_dsa_indexer_metadata(&mut metadata, arch, &config)?;
push_moe_metadata(&mut metadata, arch, &config)?;
push_tokenizer_metadata(&mut metadata, source, &config)?;
if options.include_mtp {
push_if_u32(
&mut metadata,
arch,
"nextn_predict_layers",
&config,
"num_nextn_predict_layers",
);
}
Ok(metadata)
}
fn read_hf_config(source: &Path) -> Result<Value> {
let config_path = source.join("config.json");
serde_json::from_slice(
&fs::read(&config_path).with_context(|| format!("read {}", config_path.display()))?,
)
.with_context(|| format!("parse {}", config_path.display()))
}
fn architecture_name(config: &Value) -> Result<&'static str> {
let model_type = config
.get("model_type")
.and_then(Value::as_str)
.unwrap_or_default();
if matches!(model_type, "glm4_moe_lite" | "deepseek_v2") {
return Ok("deepseek2");
}
if matches!(model_type, "glm4_moe" | "glm4v_moe") {
return Ok("glm4moe");
}
if matches!(model_type, "glm_moe_dsa" | "glm-dsa") {
return Ok("glm-dsa");
}
if model_type == "granitemoehybrid" {
return Ok("granitehybrid");
}
if is_unsupported_qwen3_variant(model_type) {
anyhow::bail!(
"native GGUF metadata for model_type={model_type:?} requires \
Qwen3.5/Qwen3Next/Qwen3VL-specific metadata and tensor support; \
use the external convert_hf_to_gguf.py backend"
);
}
if model_type.starts_with("qwen3_moe") {
return Ok("qwen3moe");
}
if model_type.starts_with("qwen2_moe") {
return Ok("qwen2moe");
}
if model_type.starts_with("qwen3") {
return Ok("qwen3");
}
if model_type.starts_with("qwen2") {
return Ok("qwen2");
}
if matches!(model_type, "llama" | "mistral") {
return Ok("llama");
}
anyhow::bail!("unsupported native GGUF metadata template for model_type={model_type:?}")
}
fn is_unsupported_qwen3_variant(model_type: &str) -> bool {
let normalized = model_type
.chars()
.filter(|ch| ch.is_ascii_alphanumeric())
.flat_map(char::to_lowercase)
.collect::<String>();
normalized.starts_with("qwen3next")
|| normalized.starts_with("qwen3vl")
|| normalized.starts_with("qwen35")
}
fn push_common_llm_metadata(
metadata: &mut Vec<GgufKv>,
arch: &str,
config: &Value,
options: MetadataOptions,
) -> Result<()> {
push_required_u32(metadata, arch, "vocab_size", config, "vocab_size")?;
if arch == "granitehybrid" {
metadata.push(GgufKv::u32(&format!("{arch}.context_length"), 1 << 20));
} else {
push_required_u32(
metadata,
arch,
"context_length",
config,
"max_position_embeddings",
)?;
}
push_required_u32(metadata, arch, "embedding_length", config, "hidden_size")?;
let block_count = required_u32(config, "num_hidden_layers")?
+ if options.include_mtp {
optional_u32(config, "num_nextn_predict_layers").unwrap_or(0)
} else {
0
};
metadata.push(GgufKv::u32(&format!("{arch}.block_count"), block_count));
push_if_u32(
metadata,
arch,
"feed_forward_length",
config,
"intermediate_size",
);
if arch == "glm-dsa" {
push_required_f32(
metadata,
arch,
"attention.layer_norm_rms_epsilon",
config,
"rms_norm_eps",
)?;
} else if let Some(eps) = optional_f32(config, "rms_norm_eps") {
metadata.push(GgufKv::f32(
&format!("{arch}.attention.layer_norm_rms_epsilon"),
eps,
));
}
Ok(())
}
fn push_attention_metadata(metadata: &mut Vec<GgufKv>, arch: &str, config: &Value) -> Result<()> {
let head_count = required_u32(config, "num_attention_heads")?;
metadata.push(GgufKv::u32(
&format!("{arch}.attention.head_count"),
head_count,
));
let head_count_kv = optional_u32(config, "num_key_value_heads").unwrap_or(head_count);
if arch == "granitehybrid" {
let layer_types = config
.get("layer_types")
.and_then(Value::as_array)
.context("Granite Hybrid config missing layer_types")?;
let block_count = required_u32(config, "num_hidden_layers")? as usize;
ensure!(
layer_types.len() == block_count,
"Granite Hybrid layer_types length {} must match num_hidden_layers {block_count}",
layer_types.len()
);
let mut per_layer = Vec::with_capacity(block_count);
for layer_type in layer_types {
let layer_type = layer_type
.as_str()
.context("Granite Hybrid layer_types must contain strings")?;
ensure!(
matches!(layer_type, "attention" | "mamba"),
"unsupported Granite Hybrid layer type {layer_type:?}"
);
per_layer.push(if layer_type == "attention" {
i32::try_from(head_count_kv).context("head count exceeds i32")?
} else {
0
});
}
metadata.push(GgufKv::array_i32(
&format!("{arch}.attention.head_count_kv"),
per_layer,
));
} else {
metadata.push(GgufKv::u32(
&format!("{arch}.attention.head_count_kv"),
head_count_kv,
));
}
let mut key_len_mla = None;
let (key_len, rope_dim) = if let Some((nope, rope)) =
optional_u32(config, "qk_nope_head_dim").zip(optional_u32(config, "qk_rope_head_dim"))
{
let dense_key_len = nope + rope;
if arch == "glm-dsa" {
ensure!(
rope > 0,
"GLM-DSA qk_rope_head_dim must be greater than zero"
);
ensure!(
rope % 2 == 0,
"GLM-DSA qk_rope_head_dim must be even for native llama.cpp RoPE"
);
ensure!(
nope > 0,
"GLM-DSA qk_nope_head_dim must be greater than zero for native MLA split"
);
let kv_lora_rank = required_u32(config, "kv_lora_rank")?;
ensure!(
kv_lora_rank > 0,
"GLM-DSA kv_lora_rank must be greater than zero"
);
key_len_mla = Some(dense_key_len);
(kv_lora_rank + rope, rope)
} else {
(dense_key_len, rope)
}
} else {
let head_dim = optional_u32(config, "head_dim").unwrap_or(
required_u32(config, "hidden_size")? / required_u32(config, "num_attention_heads")?,
);
(head_dim, head_dim)
};
metadata.push(GgufKv::u32(
&format!("{arch}.attention.key_length"),
key_len,
));
metadata.push(GgufKv::u32(
&format!("{arch}.rope.dimension_count"),
rope_dim,
));
let value_len = optional_u32(config, "v_head_dim")
.or_else(|| optional_u32(config, "head_dim"))
.unwrap_or(
required_u32(config, "hidden_size")? / required_u32(config, "num_attention_heads")?,
);
if arch == "glm-dsa" {
ensure!(
value_len > 0,
"GLM-DSA v_head_dim must be greater than zero"
);
}
metadata.push(GgufKv::u32(
&format!("{arch}.attention.value_length"),
value_len,
));
if arch == "glm-dsa" {
let key_len_mla = key_len_mla.context(
"GLM-DSA metadata requires qk_nope_head_dim and qk_rope_head_dim for MLA key length",
)?;
metadata.push(GgufKv::u32(
&format!("{arch}.attention.key_length_mla"),
key_len_mla,
));
metadata.push(GgufKv::u32(
&format!("{arch}.attention.value_length_mla"),
value_len,
));
}
if let Some(theta) = optional_f32(config, "rope_theta") {
metadata.push(GgufKv::f32(&format!("{arch}.rope.freq_base"), theta));
}
if arch == "glm-dsa" {
push_required_u32(
metadata,
arch,
"attention.q_lora_rank",
config,
"q_lora_rank",
)?;
push_required_u32(
metadata,
arch,
"attention.kv_lora_rank",
config,
"kv_lora_rank",
)?;
} else {
push_if_u32(
metadata,
arch,
"attention.q_lora_rank",
config,
"q_lora_rank",
);
push_if_u32(
metadata,
arch,
"attention.kv_lora_rank",
config,
"kv_lora_rank",
);
}
Ok(())
}
fn push_glm_dsa_indexer_metadata(
metadata: &mut Vec<GgufKv>,
arch: &str,
config: &Value,
) -> Result<()> {
if arch != "glm-dsa" {
return Ok(());
}
push_required_first_u32(
metadata,
arch,
"attention.indexer.head_count",
config,
&["index_n_heads", "indexer_n_head"],
)?;
push_required_first_u32(
metadata,
arch,
"attention.indexer.key_length",
config,
&["index_head_dim", "indexer_head_size"],
)?;
push_required_first_u32(
metadata,
arch,
"attention.indexer.top_k",
config,
&["index_topk", "indexer_top_k"],
)?;
push_first_u32(
metadata,
arch,
"attention.indexer.top_k_frequency",
config,
&["index_topk_freq", "indexer_top_k_freq"],
);
push_first_u32(
metadata,
arch,
"attention.indexer.skip_top_k_offset",
config,
&["index_skip_topk_offset", "indexer_skip_top_k_offset"],
);
push_glm_dsa_indexer_types(metadata, arch, config)?;
validate_glm_dsa_indexer_contract(config)?;
Ok(())
}
fn validate_glm_dsa_indexer_contract(config: &Value) -> Result<()> {
let index_head_dim = optional_u32(config, "index_head_dim")
.or_else(|| optional_u32(config, "indexer_head_size"))
.context("GLM-DSA config missing index_head_dim/indexer_head_size")?;
let rope_dim = required_u32(config, "qk_rope_head_dim")?;
ensure!(
index_head_dim > rope_dim,
"GLM-DSA index_head_dim must be greater than qk_rope_head_dim"
);
let freq = optional_u32(config, "index_topk_freq")
.or_else(|| optional_u32(config, "indexer_top_k_freq"));
let offset = optional_u32(config, "index_skip_topk_offset")
.or_else(|| optional_u32(config, "indexer_skip_top_k_offset"));
if let Some(freq) = freq {
ensure!(
freq > 0,
"GLM-DSA index_topk_freq must be greater than zero when present"
);
let offset = offset.context(
"GLM-DSA index_skip_topk_offset/indexer_skip_top_k_offset is required when index_topk_freq is present",
)?;
validate_glm_dsa_indexer_types_match_frequency(config, freq, offset)?;
}
Ok(())
}
fn validate_glm_dsa_indexer_types_match_frequency(
config: &Value,
freq: u32,
offset: u32,
) -> Result<()> {
let Some(types) = config.get("indexer_types") else {
return Ok(());
};
let types = types
.as_array()
.context("config value \"indexer_types\" must be an array")?;
for (layer, value) in types.iter().enumerate() {
let role = value
.as_str()
.context("config value \"indexer_types\" must contain strings")?
.to_ascii_lowercase();
let role_full = role == "full";
let freq_full = glm_dsa_frequency_layer_is_full(layer as u32, offset, freq);
ensure!(
role_full == freq_full,
"GLM-DSA indexer_types conflicts with index_topk_freq at layer {layer}"
);
}
Ok(())
}
fn glm_dsa_frequency_layer_is_full(layer: u32, offset: u32, freq: u32) -> bool {
layer < offset || (layer - offset + 1).is_multiple_of(freq)
}
fn push_glm_dsa_indexer_types(
metadata: &mut Vec<GgufKv>,
arch: &str,
config: &Value,
) -> Result<()> {
let Some(types) = config.get("indexer_types") else {
return Ok(());
};
let types = types
.as_array()
.context("config value \"indexer_types\" must be an array")?;
let n_layers = required_u32(config, "num_hidden_layers")? as usize;
ensure!(
types.len() == n_layers,
"config value \"indexer_types\" length {} must match num_hidden_layers {n_layers}",
types.len()
);
let mut normalized = Vec::with_capacity(types.len());
for value in types {
let role = value
.as_str()
.context("config value \"indexer_types\" must contain strings")?
.to_ascii_lowercase();
ensure!(
role == "full" || role == "shared",
"config value \"indexer_types\" must contain only \"full\" or \"shared\""
);
normalized.push(role);
}
metadata.push(GgufKv::array_string(
&format!("{arch}.attention.indexer.types"),
normalized,
));
Ok(())
}
fn push_moe_metadata(metadata: &mut Vec<GgufKv>, arch: &str, config: &Value) -> Result<()> {
if arch == "granitehybrid" {
push_first_u32(
metadata,
arch,
"expert_count",
config,
&["num_local_experts"],
);
push_first_u32(
metadata,
arch,
"expert_used_count",
config,
&["num_experts_per_tok"],
);
push_first_u32(
metadata,
arch,
"expert_shared_feed_forward_length",
config,
&["shared_intermediate_size"],
);
push_granite_hybrid_metadata(metadata, arch, config)?;
return Ok(());
}
if arch == "glm-dsa" {
validate_glm_dsa_moe_contract(config)?;
push_required_first_u32(
metadata,
arch,
"expert_count",
config,
&["n_routed_experts", "num_experts"],
)?;
push_required_first_u32(
metadata,
arch,
"expert_used_count",
config,
&["num_experts_per_tok"],
)?;
push_required_first_u32(
metadata,
arch,
"expert_shared_count",
config,
&["n_shared_experts"],
)?;
push_required_first_u32(
metadata,
arch,
"expert_feed_forward_length",
config,
&["moe_intermediate_size"],
)?;
push_required_first_u32(
metadata,
arch,
"leading_dense_block_count",
config,
&["first_k_dense_replace"],
)?;
push_required_f32(
metadata,
arch,
"expert_weights_scale",
config,
"routed_scaling_factor",
)?;
push_required_bool(
metadata,
arch,
"expert_weights_norm",
config,
"norm_topk_prob",
)?;
push_first_u32(
metadata,
arch,
"expert_shared_feed_forward_length",
config,
&["shared_expert_intermediate_size"],
);
return Ok(());
}
push_first_u32(
metadata,
arch,
"expert_count",
config,
&["n_routed_experts", "num_experts"],
);
push_first_u32(
metadata,
arch,
"expert_used_count",
config,
&["num_experts_per_tok"],
);
push_first_u32(
metadata,
arch,
"expert_shared_count",
config,
&["n_shared_experts"],
);
push_first_u32(
metadata,
arch,
"expert_feed_forward_length",
config,
&["moe_intermediate_size"],
);
push_first_u32(
metadata,
arch,
"expert_shared_feed_forward_length",
config,
&["shared_expert_intermediate_size"],
);
push_first_u32(
metadata,
arch,
"leading_dense_block_count",
config,
&["first_k_dense_replace"],
);
if let Some(scale) = optional_f32(config, "routed_scaling_factor") {
metadata.push(GgufKv::f32(&format!("{arch}.expert_weights_scale"), scale));
}
if let Some(norm) = optional_bool(config, "norm_topk_prob") {
metadata.push(GgufKv::bool(&format!("{arch}.expert_weights_norm"), norm));
}
Ok(())
}
fn push_granite_hybrid_metadata(
metadata: &mut Vec<GgufKv>,
arch: &str,
config: &Value,
) -> Result<()> {
for (gguf_suffix, config_key) in [
("attention.scale", "attention_multiplier"),
("embedding_scale", "embedding_multiplier"),
("residual_scale", "residual_multiplier"),
("logit_scale", "logits_scaling"),
] {
if let Some(value) = optional_f32(config, config_key) {
metadata.push(GgufKv::f32(&format!("{arch}.{gguf_suffix}"), value));
}
}
push_required_u32(metadata, arch, "ssm.conv_kernel", config, "mamba_d_conv")?;
push_required_u32(metadata, arch, "ssm.state_size", config, "mamba_d_state")?;
push_required_u32(metadata, arch, "ssm.group_count", config, "mamba_n_groups")?;
let inner_size = required_u32(config, "mamba_expand")?
.checked_mul(required_u32(config, "hidden_size")?)
.context("Granite Hybrid SSM inner size overflow")?;
metadata.push(GgufKv::u32(&format!("{arch}.ssm.inner_size"), inner_size));
push_required_u32(
metadata,
arch,
"ssm.time_step_rank",
config,
"mamba_n_heads",
)?;
metadata.push(GgufKv::bool(
&format!("{arch}.rope.scaling.finetuned"),
false,
));
Ok(())
}
fn validate_glm_dsa_moe_contract(config: &Value) -> Result<()> {
let target_layers = required_u32(config, "num_hidden_layers")?;
ensure!(
target_layers > 0,
"GLM-DSA num_hidden_layers must be greater than zero"
);
let expert_count = optional_u32(config, "n_routed_experts")
.or_else(|| optional_u32(config, "num_experts"))
.context("GLM-DSA config missing n_routed_experts/num_experts")?;
let expert_used = required_u32(config, "num_experts_per_tok")?;
ensure!(
expert_used <= expert_count,
"GLM-DSA num_experts_per_tok must be less than or equal to expert count"
);
let dense_lead = optional_u32(config, "first_k_dense_replace").unwrap_or(0);
ensure!(
dense_lead < target_layers,
"GLM-DSA first_k_dense_replace must be less than num_hidden_layers"
);
Ok(())
}
fn push_required_first_u32(
metadata: &mut Vec<GgufKv>,
arch: &str,
gguf_suffix: &str,
config: &Value,
config_keys: &[&str],
) -> Result<()> {
for config_key in config_keys {
if let Some(value) = optional_u32(config, config_key) {
ensure!(
value > 0,
"config value {config_key:?} must be greater than zero"
);
metadata.push(GgufKv::u32(&format!("{arch}.{gguf_suffix}"), value));
return Ok(());
}
}
anyhow::bail!("config missing one of {config_keys:?}")
}
fn push_first_u32(
metadata: &mut Vec<GgufKv>,
arch: &str,
gguf_suffix: &str,
config: &Value,
config_keys: &[&str],
) {
for config_key in config_keys {
if let Some(value) = optional_u32(config, config_key) {
metadata.push(GgufKv::u32(&format!("{arch}.{gguf_suffix}"), value));
return;
}
}
}
fn push_required_u32(
metadata: &mut Vec<GgufKv>,
arch: &str,
gguf_suffix: &str,
config: &Value,
config_key: &str,
) -> Result<()> {
metadata.push(GgufKv::u32(
&format!("{arch}.{gguf_suffix}"),
required_u32(config, config_key)?,
));
Ok(())
}
fn push_required_f32(
metadata: &mut Vec<GgufKv>,
arch: &str,
gguf_suffix: &str,
config: &Value,
config_key: &str,
) -> Result<()> {
let value = optional_f32(config, config_key)
.with_context(|| format!("config missing {config_key:?}"))?;
metadata.push(GgufKv::f32(&format!("{arch}.{gguf_suffix}"), value));
Ok(())
}
fn push_required_bool(
metadata: &mut Vec<GgufKv>,
arch: &str,
gguf_suffix: &str,
config: &Value,
config_key: &str,
) -> Result<()> {
let value = optional_bool(config, config_key)
.with_context(|| format!("config missing {config_key:?}"))?;
metadata.push(GgufKv::bool(&format!("{arch}.{gguf_suffix}"), value));
Ok(())
}
fn push_if_u32(
metadata: &mut Vec<GgufKv>,
arch: &str,
gguf_suffix: &str,
config: &Value,
config_key: &str,
) {
if let Some(value) = optional_u32(config, config_key) {
metadata.push(GgufKv::u32(&format!("{arch}.{gguf_suffix}"), value));
}
}
fn required_u32(config: &Value, key: &str) -> Result<u32> {
let value = optional_u32(config, key).with_context(|| format!("config missing {key:?}"))?;
ensure!(value > 0, "config value {key:?} must be greater than zero");
Ok(value)
}
fn optional_u32(config: &Value, key: &str) -> Option<u32> {
config
.get(key)
.and_then(Value::as_u64)
.and_then(|value| u32::try_from(value).ok())
}
fn optional_f32(config: &Value, key: &str) -> Option<f32> {
config
.get(key)
.and_then(Value::as_f64)
.map(|value| value as f32)
}
fn optional_bool(config: &Value, key: &str) -> Option<bool> {
config.get(key).and_then(Value::as_bool)
}
fn model_name(source: &Path) -> &str {
source
.file_name()
.and_then(|value| value.to_str())
.unwrap_or("checkpoint")
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use super::*;
#[test]
fn builds_glm_moe_lite_metadata_from_config() {
let root = unique_temp_dir();
fs::create_dir_all(&root).unwrap();
fs::write(
root.join("config.json"),
r#"{
"model_type": "glm4_moe_lite",
"vocab_size": 154880,
"max_position_embeddings": 202752,
"hidden_size": 2048,
"intermediate_size": 10240,
"num_hidden_layers": 47,
"num_nextn_predict_layers": 1,
"num_attention_heads": 20,
"num_key_value_heads": 20,
"qk_nope_head_dim": 192,
"qk_rope_head_dim": 64,
"v_head_dim": 256,
"rope_theta": 1000000,
"q_lora_rank": 768,
"kv_lora_rank": 512,
"n_routed_experts": 64,
"num_experts_per_tok": 4,
"n_shared_experts": 1,
"moe_intermediate_size": 1536,
"first_k_dense_replace": 1,
"routed_scaling_factor": 1.8,
"norm_topk_prob": true,
"rms_norm_eps": 1e-5
}"#,
)
.unwrap();
let metadata = metadata_from_hf_config(&root, 3).unwrap();
let text = format!("{metadata:?}");
assert!(text.contains("general.architecture"));
assert!(text.contains("deepseek2"));
assert!(text.contains("deepseek2.block_count"));
assert!(text.contains("48"));
assert!(text.contains("deepseek2.attention.key_length"));
assert!(text.contains("256"));
assert!(text.contains("deepseek2.nextn_predict_layers"));
fs::remove_dir_all(root).unwrap();
}
#[test]
fn omits_mtp_metadata_when_requested() {
let root = unique_temp_dir();
fs::create_dir_all(&root).unwrap();
fs::write(
root.join("config.json"),
r#"{
"model_type": "glm4_moe_lite",
"vocab_size": 154880,
"max_position_embeddings": 202752,
"hidden_size": 2048,
"intermediate_size": 10240,
"num_hidden_layers": 47,
"num_nextn_predict_layers": 1,
"num_attention_heads": 20,
"num_key_value_heads": 20,
"qk_nope_head_dim": 192,
"qk_rope_head_dim": 64,
"v_head_dim": 256
}"#,
)
.unwrap();
let metadata =
metadata_from_hf_config_with_options(&root, 3, MetadataOptions { include_mtp: false })
.unwrap();
assert!(metadata.iter().any(|kv| {
matches!(
kv,
GgufKv::U32 { key, value }
if key == "deepseek2.block_count" && *value == 47
)
}));
assert!(!metadata.iter().any(|kv| {
matches!(
kv,
GgufKv::U32 { key, .. } if key == "deepseek2.nextn_predict_layers"
)
}));
fs::remove_dir_all(root).unwrap();
}
#[test]
fn builds_glm_dsa_indexer_metadata_from_config() {
let root = unique_temp_dir();
fs::create_dir_all(&root).unwrap();
fs::write(
root.join("config.json"),
r#"{
"model_type": "glm_moe_dsa",
"vocab_size": 154880,
"max_position_embeddings": 1048576,
"hidden_size": 6144,
"intermediate_size": 12288,
"num_hidden_layers": 78,
"num_nextn_predict_layers": 1,
"num_attention_heads": 64,
"num_key_value_heads": 64,
"qk_nope_head_dim": 192,
"qk_rope_head_dim": 64,
"v_head_dim": 256,
"q_lora_rank": 2048,
"kv_lora_rank": 512,
"index_n_heads": 32,
"index_head_dim": 128,
"index_topk": 2048,
"index_topk_freq": 4,
"index_skip_topk_offset": 3,
"indexer_types": [
"full", "full", "full", "shared", "shared", "shared", "full",
"shared", "shared", "shared", "full", "shared", "shared",
"shared", "full", "shared", "shared", "shared", "full",
"shared", "shared", "shared", "full", "shared", "shared",
"shared", "full", "shared", "shared", "shared", "full",
"shared", "shared", "shared", "full", "shared", "shared",
"shared", "full", "shared", "shared", "shared", "full",
"shared", "shared", "shared", "full", "shared", "shared",
"shared", "full", "shared", "shared", "shared", "full",
"shared", "shared", "shared", "full", "shared", "shared",
"shared", "full", "shared", "shared", "shared", "full",
"shared", "shared", "shared", "full", "shared", "shared",
"shared", "full", "shared", "shared", "shared"
],
"n_routed_experts": 256,
"num_experts_per_tok": 8,
"n_shared_experts": 1,
"moe_intermediate_size": 2048,
"first_k_dense_replace": 3,
"routed_scaling_factor": 2.5,
"norm_topk_prob": true,
"rms_norm_eps": 1e-5
}"#,
)
.unwrap();
let metadata = metadata_from_hf_config(&root, 3).unwrap();
assert!(metadata.iter().any(|kv| {
matches!(
kv,
GgufKv::U32 { key, value }
if key == "glm-dsa.attention.key_length" && *value == 576
)
}));
assert!(metadata.iter().any(|kv| {
matches!(
kv,
GgufKv::U32 { key, value }
if key == "glm-dsa.attention.key_length_mla" && *value == 256
)
}));
assert!(metadata.iter().any(|kv| {
matches!(
kv,
GgufKv::U32 { key, value }
if key == "glm-dsa.attention.value_length_mla" && *value == 256
)
}));
assert!(metadata.iter().any(|kv| {
matches!(
kv,
GgufKv::U32 { key, value }
if key == "glm-dsa.attention.indexer.head_count" && *value == 32
)
}));
assert!(metadata.iter().any(|kv| {
matches!(
kv,
GgufKv::U32 { key, value }
if key == "glm-dsa.attention.indexer.key_length" && *value == 128
)
}));
assert!(metadata.iter().any(|kv| {
matches!(
kv,
GgufKv::U32 { key, value }
if key == "glm-dsa.attention.indexer.top_k" && *value == 2048
)
}));
assert!(metadata.iter().any(|kv| {
matches!(
kv,
GgufKv::U32 { key, value }
if key == "glm-dsa.attention.indexer.top_k_frequency" && *value == 4
)
}));
assert!(metadata.iter().any(|kv| {
matches!(
kv,
GgufKv::U32 { key, value }
if key == "glm-dsa.attention.indexer.skip_top_k_offset" && *value == 3
)
}));
assert!(metadata.iter().any(|kv| {
matches!(
kv,
GgufKv::ArrayString { key, value }
if key == "glm-dsa.attention.indexer.types"
&& value.len() == 78
&& value[0] == "full"
&& value[3] == "shared"
&& value[6] == "full"
)
}));
assert!(metadata.iter().any(|kv| {
matches!(
kv,
GgufKv::U32 { key, value }
if key == "glm-dsa.expert_count" && *value == 256
)
}));
assert!(metadata.iter().any(|kv| {
matches!(
kv,
GgufKv::U32 { key, value }
if key == "glm-dsa.expert_used_count" && *value == 8
)
}));
assert!(metadata.iter().any(|kv| {
matches!(
kv,
GgufKv::U32 { key, value }
if key == "glm-dsa.expert_shared_count" && *value == 1
)
}));
assert!(metadata.iter().any(|kv| {
matches!(
kv,
GgufKv::U32 { key, value }
if key == "glm-dsa.expert_feed_forward_length" && *value == 2048
)
}));
assert!(metadata.iter().any(|kv| {
matches!(
kv,
GgufKv::U32 { key, value }
if key == "glm-dsa.leading_dense_block_count" && *value == 3
)
}));
assert!(metadata.iter().any(|kv| {
matches!(
kv,
GgufKv::F32 { key, value }
if key == "glm-dsa.expert_weights_scale" && (*value - 2.5).abs() < f32::EPSILON
)
}));
assert!(metadata.iter().any(|kv| {
matches!(
kv,
GgufKv::Bool { key, value }
if key == "glm-dsa.expert_weights_norm" && *value
)
}));
fs::remove_dir_all(root).unwrap();
}
#[test]
fn rejects_glm_dsa_config_missing_runtime_contract_metadata() {
let root = unique_temp_dir();
fs::create_dir_all(&root).unwrap();
fs::write(
root.join("config.json"),
r#"{
"model_type": "glm_moe_dsa",
"vocab_size": 154880,
"max_position_embeddings": 1048576,
"hidden_size": 6144,
"intermediate_size": 12288,
"num_hidden_layers": 78,
"num_attention_heads": 64,
"num_key_value_heads": 64,
"qk_nope_head_dim": 192,
"qk_rope_head_dim": 64,
"v_head_dim": 256,
"q_lora_rank": 2048,
"kv_lora_rank": 512,
"index_n_heads": 32,
"index_head_dim": 128,
"index_topk": 2048,
"n_routed_experts": 256,
"n_shared_experts": 1,
"moe_intermediate_size": 2048,
"first_k_dense_replace": 3,
"routed_scaling_factor": 2.5,
"norm_topk_prob": true,
"rms_norm_eps": 1e-5
}"#,
)
.unwrap();
let err = metadata_from_hf_config(&root, 3).unwrap_err();
assert!(
err.to_string().contains("num_experts_per_tok"),
"unexpected error: {err:#}"
);
fs::remove_dir_all(root).unwrap();
}
#[test]
fn builds_llama_metadata_from_config() {
let root = unique_temp_dir();
fs::create_dir_all(&root).unwrap();
fs::write(
root.join("config.json"),
r#"{
"model_type": "llama",
"vocab_size": 128256,
"max_position_embeddings": 131072,
"hidden_size": 4096,
"intermediate_size": 14336,
"num_hidden_layers": 32,
"num_attention_heads": 32,
"num_key_value_heads": 8,
"head_dim": 128,
"rope_theta": 500000,
"rms_norm_eps": 1e-5
}"#,
)
.unwrap();
let metadata = metadata_from_hf_config(&root, 3).unwrap();
let text = format!("{metadata:?}");
assert!(text.contains("general.architecture"));
assert!(text.contains("llama"));
assert!(text.contains("llama.block_count"));
assert!(text.contains("32"));
assert!(text.contains("llama.attention.head_count_kv"));
assert!(text.contains("8"));
fs::remove_dir_all(root).unwrap();
}
#[test]
fn builds_granite_hybrid_metadata_from_fixed_model_config() {
let root = unique_temp_dir();
fs::create_dir_all(&root).unwrap();
let mut layer_types = vec!["mamba"; 40];
for layer in [5_usize, 15, 25, 35] {
layer_types[layer] = "attention";
}
let config = serde_json::json!({
"architectures": ["GraniteMoeHybridForCausalLM"],
"model_type": "granitemoehybrid",
"vocab_size": 100352,
"hidden_size": 1536,
"intermediate_size": 4096,
"shared_intermediate_size": 4096,
"num_hidden_layers": 40,
"num_attention_heads": 12,
"num_key_value_heads": 4,
"head_dim": 128,
"layer_types": layer_types,
"num_local_experts": 0,
"num_experts_per_tok": 0,
"mamba_d_conv": 4,
"mamba_d_state": 128,
"mamba_n_groups": 1,
"mamba_expand": 2,
"mamba_n_heads": 48,
"attention_multiplier": 0.0078125,
"embedding_multiplier": 12.0,
"residual_multiplier": 0.22,
"logits_scaling": 6.0,
"rope_theta": 10000.0,
"rms_norm_eps": 1e-5
});
fs::write(
root.join("config.json"),
serde_json::to_vec(&config).unwrap(),
)
.unwrap();
let metadata = metadata_from_hf_config(&root, 506).unwrap();
assert!(metadata.iter().any(|kv| matches!(
kv,
GgufKv::String { key, value }
if key == "general.architecture" && value == "granitehybrid"
)));
assert!(metadata.iter().any(|kv| matches!(
kv,
GgufKv::U32 { key, value }
if key == "granitehybrid.context_length" && *value == 1 << 20
)));
assert!(metadata.iter().any(|kv| matches!(
kv,
GgufKv::ArrayI32 { key, value }
if key == "granitehybrid.attention.head_count_kv"
&& value.len() == 40
&& value[5] == 4
&& value[15] == 4
&& value.iter().filter(|head_count| **head_count == 4).count() == 4
&& value.iter().filter(|head_count| **head_count == 0).count() == 36
)));
for (key, expected) in [
("granitehybrid.ssm.conv_kernel", 4),
("granitehybrid.ssm.state_size", 128),
("granitehybrid.ssm.group_count", 1),
("granitehybrid.ssm.inner_size", 3072),
("granitehybrid.ssm.time_step_rank", 48),
("granitehybrid.expert_count", 0),
("granitehybrid.expert_used_count", 0),
("granitehybrid.expert_shared_feed_forward_length", 4096),
] {
assert!(
metadata.iter().any(|kv| matches!(
kv,
GgufKv::U32 { key: actual, value }
if actual == key && *value == expected
)),
"missing {key}={expected}"
);
}
assert!(metadata.iter().any(|kv| matches!(
kv,
GgufKv::Bool { key, value }
if key == "granitehybrid.rope.scaling.finetuned" && !*value
)));
fs::remove_dir_all(root).unwrap();
}
#[test]
fn defaults_kv_heads_to_attention_heads() {
let root = unique_temp_dir();
fs::create_dir_all(&root).unwrap();
fs::write(
root.join("config.json"),
r#"{
"model_type": "llama",
"vocab_size": 32000,
"max_position_embeddings": 4096,
"hidden_size": 4096,
"intermediate_size": 11008,
"num_hidden_layers": 32,
"num_attention_heads": 32
}"#,
)
.unwrap();
let metadata = metadata_from_hf_config(&root, 3).unwrap();
let text = format!("{metadata:?}");
assert!(text.contains("llama.attention.head_count_kv"));
assert!(text.contains("32"));
fs::remove_dir_all(root).unwrap();
}
#[test]
fn builds_qwen2_moe_metadata_from_qwen_config_keys() {
let root = unique_temp_dir();
fs::create_dir_all(&root).unwrap();
fs::write(
root.join("config.json"),
r#"{
"model_type": "qwen2_moe",
"vocab_size": 151936,
"max_position_embeddings": 32768,
"hidden_size": 2048,
"intermediate_size": 5632,
"num_hidden_layers": 24,
"num_attention_heads": 16,
"num_key_value_heads": 16,
"head_dim": 128,
"num_experts": 60,
"num_experts_per_tok": 4,
"moe_intermediate_size": 1408,
"shared_expert_intermediate_size": 5632,
"rope_theta": 1000000,
"rms_norm_eps": 1e-6
}"#,
)
.unwrap();
let metadata = metadata_from_hf_config(&root, 3).unwrap();
let text = format!("{metadata:?}");
assert!(text.contains("qwen2moe"));
assert!(text.contains("qwen2moe.expert_count"));
assert!(text.contains("60"));
assert!(text.contains("qwen2moe.expert_feed_forward_length"));
assert!(text.contains("1408"));
assert!(text.contains("qwen2moe.expert_shared_feed_forward_length"));
assert!(text.contains("5632"));
fs::remove_dir_all(root).unwrap();
}
#[test]
fn rejects_newer_qwen_variants_that_need_specific_native_templates() {
for model_type in [
"qwen3_next",
"qwen3next",
"qwen3_vl",
"qwen3vl_moe",
"qwen3.5",
"qwen3_5_moe",
"qwen35",
"qwen35moe",
] {
let root = unique_temp_dir();
fs::create_dir_all(&root).unwrap();
fs::write(
root.join("config.json"),
format!(
r#"{{
"model_type": "{model_type}",
"vocab_size": 151936,
"max_position_embeddings": 32768,
"hidden_size": 2048,
"intermediate_size": 5632,
"num_hidden_layers": 24,
"num_attention_heads": 16
}}"#,
),
)
.unwrap();
let err = metadata_from_hf_config(&root, 3).unwrap_err();
let message = err.to_string();
assert!(
message.contains("Qwen3.5/Qwen3Next/Qwen3VL-specific"),
"{model_type}: {message}"
);
assert!(
message.contains("external convert_hf_to_gguf.py backend"),
"{model_type}: {message}"
);
fs::remove_dir_all(root).unwrap();
}
}
#[test]
fn builds_inkling_multi_depth_mtp_metadata() {
let root = unique_temp_dir();
fs::create_dir_all(&root).unwrap();
fs::write(
root.join("config.json"),
r#"{
"model_type": "inkling_mm_model",
"eos_token_id": 200006,
"text_config": {
"model_max_length": 1048576,
"hidden_size": 6144,
"num_hidden_layers": 66,
"vocab_size": 201024,
"num_attention_heads": 64,
"num_key_value_heads": 8,
"head_dim": 128,
"d_rel": 16,
"rel_extent": 1024,
"log_scaling_n_floor": 128000,
"log_scaling_alpha": 0.1,
"rms_norm_eps": 1e-6,
"local_layer_ids": [0, 1, 2, 3, 4],
"dense_mlp_idx": 2,
"sconv_kernel_size": 4,
"unpadded_vocab_size": 200058,
"logits_mup_width_multiplier": 24.0,
"swa_num_key_value_heads": 16,
"sliding_window_size": 512,
"n_routed_experts": 256,
"num_experts_per_tok": 6,
"n_shared_experts": 2,
"dense_intermediate_size": 24576,
"intermediate_size": 3072,
"route_scale": 8.0,
"norm_after_topk": true,
"shared_expert_sink": true,
"use_sconv": true,
"use_embed_norm": true,
"use_gate_bias": true,
"use_global_scale": true,
"gate_activation": "sigmoid"
},
"mtp_config": {
"num_nextn_predict_layers": 8,
"chain_hidden_post_norm": false,
"local_layer_ids": [0, 2, 4, 5, 6, 7]
}
}"#,
)
.unwrap();
assert_eq!(mtp_layer_start_from_hf_config(&root).unwrap(), Some(66));
let metadata = metadata_from_hf_config(&root, 25).unwrap();
assert!(metadata.iter().any(|kv| {
matches!(kv, GgufKv::U32 { key, value } if key == "inkling.block_count" && *value == 74)
}));
assert!(metadata.iter().any(|kv| {
matches!(kv, GgufKv::U32 { key, value } if key == "inkling.nextn_predict_layers" && *value == 8)
}));
assert!(metadata.iter().any(|kv| {
matches!(kv, GgufKv::ArrayU32 { key, value } if key == "inkling.attention.head_count_kv" && value.len() == 74 && value[66] == 16 && value[67] == 8)
}));
assert!(metadata.iter().any(|kv| {
matches!(kv, GgufKv::ArrayBool { key, value } if key == "inkling.attention.sliding_window_pattern" && value.len() == 74 && value[66] && !value[67])
}));
fs::remove_dir_all(root).unwrap();
}
fn unique_temp_dir() -> PathBuf {
static NEXT_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let id = NEXT_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
std::env::temp_dir().join(format!("skippy-gguf-template-{nanos}-{id}"))
}
}