Skip to main content

ferrum_models/vnext/
mod.rs

1//! Production model-family packages consumed by the vNext planner and runtime.
2
3use std::collections::BTreeSet;
4use std::fs;
5use std::num::{NonZeroU64, NonZeroUsize};
6use std::path::Path;
7use std::sync::Arc;
8
9use ferrum_interfaces::vnext::{
10    AttributeId, ElementType, ExternalModelMetadataId, ModelFamilyRegistration,
11    ModelFamilyRegistry, ModelSourceKind, OriginalModelSource, OriginalModelSources,
12    PreparedModelFamily, ProgramNode, SemanticValue, StateCapacityDemand, StateLifetime,
13    TypedFamilyRegistration, WeightComponentSource, ROUTED_SHARED_SWIGLU_MOE_OPERATION_ID,
14    ROUTED_SWIGLU_MOE_OPERATION_ID,
15};
16use ferrum_types::{
17    DataType, Device, ModelCapabilities, ModelId, ModelInfo, ModelType, MoeCapabilities,
18};
19use serde_json::Value;
20
21mod hf_metadata;
22pub mod qwen35;
23pub mod qwen3_moe;
24pub mod source;
25mod weight_layout;
26
27pub use source::{
28    huggingface_snapshot_identity, HuggingFaceSnapshotIdentity, ProductionModelSourceBundle,
29    ProductionWeightArtifact,
30};
31
32type PrepareModel =
33    fn(Arc<ProductionModelSourceBundle>) -> ferrum_types::Result<PreparedProductionModel>;
34type ValidateSemanticConfig = fn(&ExternalModelMetadataId, &[u8]) -> ferrum_types::Result<()>;
35type CreateFamilyRegistration = fn() -> ferrum_types::Result<Box<dyn ModelFamilyRegistration>>;
36
37struct ModelLoaderRegistration {
38    external_metadata_ids: &'static [&'static str],
39    gguf_architectures: &'static [&'static str],
40    execution_kind: ProductionExecutionKind,
41    validate_semantic_config: ValidateSemanticConfig,
42    prepare: PrepareModel,
43    create_family_registration: CreateFamilyRegistration,
44}
45
46const MODEL_LOADERS: &[ModelLoaderRegistration] = &[
47    ModelLoaderRegistration {
48        external_metadata_ids: &[
49            qwen35::EXTERNAL_METADATA_ID,
50            qwen35::MOE_EXTERNAL_METADATA_ID,
51        ],
52        gguf_architectures: &["qwen35", "qwen35moe"],
53        execution_kind: ProductionExecutionKind::CausalLanguage,
54        validate_semantic_config: qwen35::validate_semantic_config,
55        prepare: qwen35::prepare_from_sources,
56        create_family_registration: qwen35_family_registration,
57    },
58    ModelLoaderRegistration {
59        external_metadata_ids: &[qwen3_moe::EXTERNAL_METADATA_ID],
60        gguf_architectures: &["qwen3moe"],
61        execution_kind: ProductionExecutionKind::CausalLanguage,
62        validate_semantic_config: qwen3_moe::validate_semantic_config,
63        prepare: qwen3_moe::prepare_from_sources,
64        create_family_registration: qwen3_moe::family_registration,
65    },
66];
67
68/// Returns whether a GGUF architecture belongs to a family whose product
69/// execution has migrated to vNext. Direct files for these architectures must
70/// provide typed semantic and tokenizer sources instead of silently falling
71/// back to a legacy executor.
72pub fn gguf_architecture_requires_typed_product_sources(architecture: &str) -> bool {
73    MODEL_LOADERS
74        .iter()
75        .any(|registration| registration.gguf_architectures.contains(&architecture))
76}
77
78fn qwen35_family_registration() -> ferrum_types::Result<Box<dyn ModelFamilyRegistration>> {
79    let provider = qwen35::Qwen35FamilyProvider::new()
80        .map_err(|error| ferrum_types::FerrumError::model(error.to_string()))?;
81    Ok(Box::new(TypedFamilyRegistration::new(provider)))
82}
83
84/// Complete typed family registry for production vNext registrations.
85/// It is derived from `MODEL_LOADERS`, so family preparation and resolved-plan
86/// revalidation cannot drift into separate model-name switch statements.
87pub struct ProductionModelFamilyRegistry {
88    registrations: Vec<Box<dyn ModelFamilyRegistration>>,
89}
90
91impl ProductionModelFamilyRegistry {
92    pub fn new() -> ferrum_types::Result<Self> {
93        let registrations = MODEL_LOADERS
94            .iter()
95            .map(|registration| (registration.create_family_registration)())
96            .collect::<ferrum_types::Result<Vec<_>>>()?;
97        Ok(Self { registrations })
98    }
99}
100
101impl ModelFamilyRegistry for ProductionModelFamilyRegistry {
102    fn registrations(&self) -> Vec<&dyn ModelFamilyRegistration> {
103        self.registrations
104            .iter()
105            .map(|registration| registration.as_ref())
106            .collect()
107    }
108}
109
110struct LegacyModelRegistration {
111    external_metadata_id: &'static str,
112}
113
114/// Explicit migration ledger for safetensors families that still use the
115/// legacy executor registry. Moving a family to vNext requires deleting its
116/// row here and adding one `MODEL_LOADERS` row; unknown metadata never gains a
117/// legacy fallback implicitly.
118const LEGACY_MODELS: &[LegacyModelRegistration] = &[
119    LegacyModelRegistration {
120        external_metadata_id: "hf.architecture.LlamaForCausalLM",
121    },
122    LegacyModelRegistration {
123        external_metadata_id: "hf.architecture.Qwen2ForCausalLM",
124    },
125    LegacyModelRegistration {
126        external_metadata_id: "hf.architecture.Qwen3ForCausalLM",
127    },
128    LegacyModelRegistration {
129        external_metadata_id: "hf.architecture.Gemma3ForCausalLM",
130    },
131    LegacyModelRegistration {
132        external_metadata_id: "hf.architecture.Gemma3ForConditionalGeneration",
133    },
134    LegacyModelRegistration {
135        external_metadata_id: "hf.architecture.MistralForCausalLM",
136    },
137    LegacyModelRegistration {
138        external_metadata_id: "hf.architecture.PhiForCausalLM",
139    },
140    LegacyModelRegistration {
141        external_metadata_id: "hf.architecture.GPT2LMHeadModel",
142    },
143    LegacyModelRegistration {
144        external_metadata_id: "hf.architecture.BertModel",
145    },
146    LegacyModelRegistration {
147        external_metadata_id: "hf.architecture.BertForMaskedLM",
148    },
149    LegacyModelRegistration {
150        external_metadata_id: "hf.architecture.BertForSequenceClassification",
151    },
152    LegacyModelRegistration {
153        external_metadata_id: "hf.architecture.CLIPModel",
154    },
155    LegacyModelRegistration {
156        external_metadata_id: "hf.architecture.ChineseCLIPModel",
157    },
158    LegacyModelRegistration {
159        external_metadata_id: "hf.architecture.SiglipModel",
160    },
161    LegacyModelRegistration {
162        external_metadata_id: "hf.architecture.WhisperForConditionalGeneration",
163    },
164    LegacyModelRegistration {
165        external_metadata_id: "hf.architecture.Qwen3TTSForConditionalGeneration",
166    },
167];
168
169/// Product executor category selected by model semantics, never by a family
170/// name. New families using existing operations register against an existing
171/// kind without changing the engine composition root.
172#[derive(Debug, Clone, Copy, PartialEq, Eq)]
173pub enum ProductionExecutionKind {
174    CausalLanguage,
175}
176
177/// Runtime-facing language-model facts produced by a typed family package.
178///
179/// These values replace the legacy engine's second parse through
180/// `ModelDefinition`. Non-zero fields and head compatibility are checked once
181/// while the package is prepared, before an execution plan is compiled.
182#[derive(Debug, Clone, PartialEq, Eq)]
183pub struct CausalLanguageModelDescriptor {
184    architecture: String,
185    parameter_count: NonZeroU64,
186    hidden_size: NonZeroUsize,
187    layer_count: NonZeroUsize,
188    attention_head_count: NonZeroUsize,
189    kv_head_count: NonZeroUsize,
190    attention_head_dimension: NonZeroUsize,
191    vocabulary_size: NonZeroUsize,
192    maximum_sequence_tokens: NonZeroUsize,
193    execution_dtype: DataType,
194}
195
196impl CausalLanguageModelDescriptor {
197    #[allow(clippy::too_many_arguments)]
198    pub fn new(
199        architecture: impl Into<String>,
200        parameter_count: u64,
201        hidden_size: usize,
202        layer_count: usize,
203        attention_head_count: usize,
204        kv_head_count: usize,
205        attention_head_dimension: usize,
206        vocabulary_size: usize,
207        maximum_sequence_tokens: usize,
208        execution_dtype: DataType,
209    ) -> ferrum_types::Result<Self> {
210        let architecture = architecture.into();
211        if architecture.trim().is_empty() || architecture.trim() != architecture {
212            return Err(ferrum_types::FerrumError::model(
213                "architecture capability id must be non-empty and canonical",
214            ));
215        }
216        let parameter_count = NonZeroU64::new(parameter_count)
217            .ok_or_else(|| ferrum_types::FerrumError::model("parameter_count must be positive"))?;
218        let hidden_size = NonZeroUsize::new(hidden_size)
219            .ok_or_else(|| ferrum_types::FerrumError::model("hidden_size must be positive"))?;
220        let layer_count = NonZeroUsize::new(layer_count)
221            .ok_or_else(|| ferrum_types::FerrumError::model("layer_count must be positive"))?;
222        let attention_head_count = NonZeroUsize::new(attention_head_count).ok_or_else(|| {
223            ferrum_types::FerrumError::model("attention_head_count must be positive")
224        })?;
225        let kv_head_count = NonZeroUsize::new(kv_head_count)
226            .ok_or_else(|| ferrum_types::FerrumError::model("kv_head_count must be positive"))?;
227        let attention_head_dimension =
228            NonZeroUsize::new(attention_head_dimension).ok_or_else(|| {
229                ferrum_types::FerrumError::model("attention_head_dimension must be positive")
230            })?;
231        let vocabulary_size = NonZeroUsize::new(vocabulary_size)
232            .ok_or_else(|| ferrum_types::FerrumError::model("vocabulary_size must be positive"))?;
233        let maximum_sequence_tokens =
234            NonZeroUsize::new(maximum_sequence_tokens).ok_or_else(|| {
235                ferrum_types::FerrumError::model("maximum_sequence_tokens must be positive")
236            })?;
237        if kv_head_count.get() > attention_head_count.get() {
238            return Err(ferrum_types::FerrumError::model(format!(
239                "kv_head_count {} exceeds attention_head_count {}",
240                kv_head_count, attention_head_count
241            )));
242        }
243        if attention_head_count.get() % kv_head_count.get() != 0 {
244            return Err(ferrum_types::FerrumError::model(format!(
245                "attention_head_count {} is not divisible by kv_head_count {}",
246                attention_head_count, kv_head_count
247            )));
248        }
249        attention_head_count
250            .get()
251            .checked_mul(attention_head_dimension.get())
252            .ok_or_else(|| {
253                ferrum_types::FerrumError::model("attention projection width overflows usize")
254            })?;
255        if !execution_dtype.is_float() {
256            return Err(ferrum_types::FerrumError::model(format!(
257                "causal language execution dtype must be floating point, got {execution_dtype:?}"
258            )));
259        }
260        Ok(Self {
261            architecture,
262            parameter_count,
263            hidden_size,
264            layer_count,
265            attention_head_count,
266            kv_head_count,
267            attention_head_dimension,
268            vocabulary_size,
269            maximum_sequence_tokens,
270            execution_dtype,
271        })
272    }
273
274    pub fn architecture(&self) -> &str {
275        &self.architecture
276    }
277
278    pub const fn parameter_count(&self) -> u64 {
279        self.parameter_count.get()
280    }
281
282    pub const fn hidden_size(&self) -> usize {
283        self.hidden_size.get()
284    }
285
286    pub const fn layer_count(&self) -> usize {
287        self.layer_count.get()
288    }
289
290    pub const fn attention_head_count(&self) -> usize {
291        self.attention_head_count.get()
292    }
293
294    pub const fn kv_head_count(&self) -> usize {
295        self.kv_head_count.get()
296    }
297
298    pub const fn attention_head_dimension(&self) -> usize {
299        self.attention_head_dimension.get()
300    }
301
302    pub const fn vocabulary_size(&self) -> usize {
303        self.vocabulary_size.get()
304    }
305
306    pub const fn maximum_sequence_tokens(&self) -> usize {
307        self.maximum_sequence_tokens.get()
308    }
309
310    pub const fn execution_dtype(&self) -> DataType {
311        self.execution_dtype
312    }
313}
314
315/// One immutable semantic family and the exact indexed weight source used to
316/// initialize its execution plan. Keeping them together prevents product code
317/// from resolving a family from one model directory and loading bytes from
318/// another.
319pub struct PreparedProductionModel {
320    family: PreparedModelFamily,
321    weights: Arc<dyn WeightComponentSource>,
322    descriptor: CausalLanguageModelDescriptor,
323    sources: Arc<ProductionModelSourceBundle>,
324}
325
326impl std::fmt::Debug for PreparedProductionModel {
327    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
328        formatter
329            .debug_struct("PreparedProductionModel")
330            .field("family_id", self.family.family_id())
331            .field("descriptor", &self.descriptor)
332            .field("sources", &self.sources)
333            .finish_non_exhaustive()
334    }
335}
336
337impl PreparedProductionModel {
338    pub(super) fn new(
339        family: PreparedModelFamily,
340        weights: impl WeightComponentSource + 'static,
341        descriptor: CausalLanguageModelDescriptor,
342        sources: Arc<ProductionModelSourceBundle>,
343    ) -> Self {
344        Self {
345            family,
346            weights: Arc::new(weights),
347            descriptor,
348            sources,
349        }
350    }
351
352    pub fn family(&self) -> &PreparedModelFamily {
353        &self.family
354    }
355
356    pub fn weights(&self) -> &dyn WeightComponentSource {
357        self.weights.as_ref()
358    }
359
360    pub fn weight_source(&self) -> &Arc<dyn WeightComponentSource> {
361        &self.weights
362    }
363
364    pub fn descriptor(&self) -> &CausalLanguageModelDescriptor {
365        &self.descriptor
366    }
367
368    pub fn sources(&self) -> &Arc<ProductionModelSourceBundle> {
369        &self.sources
370    }
371
372    pub fn product_source_identity(
373        &self,
374        requested_model: impl Into<String>,
375        resolved_model: impl Into<String>,
376    ) -> ferrum_types::Result<ferrum_interfaces::vnext::ProductModelSourceIdentity> {
377        let template = &self.family.metadata().template;
378        self.sources.product_source_identity(
379            requested_model,
380            resolved_model,
381            &template.source_file,
382            &template.template,
383        )
384    }
385
386    pub const fn execution_kind(&self) -> ProductionExecutionKind {
387        ProductionExecutionKind::CausalLanguage
388    }
389
390    /// Projects the immutable typed package into startup auto-configuration.
391    /// Fixed sequence state comes directly from the semantic program; token-
392    /// scaled state (for example KV) remains under the plan/runtime capacity
393    /// policy and is deliberately not double-counted here.
394    pub fn model_capabilities(&self) -> ferrum_types::Result<ModelCapabilities> {
395        let estimated_weight_bytes = self.sources.weight_payload_bytes()?;
396        let recurrent_state_bytes_per_sequence = self
397            .family
398            .program()
399            .states()
400            .iter()
401            .filter(|state| {
402                state.lifetime == StateLifetime::Sequence
403                    && state.capacity_demand == StateCapacityDemand::FixedPerScope
404            })
405            .try_fold(0_u64, |total, state| {
406                let bytes = state.tensor.byte_len().map_err(|error| {
407                    ferrum_types::FerrumError::model(format!(
408                        "fixed per-sequence state {} has invalid storage size: {error}",
409                        state.id
410                    ))
411                })?;
412                total.checked_add(bytes).ok_or_else(|| {
413                    ferrum_types::FerrumError::model(
414                        "fixed per-sequence state byte size overflows u64",
415                    )
416                })
417            })?;
418        let quantization_formats = self.family.weight_schema().quantization_formats();
419        let quantization = (!quantization_formats.is_empty()).then(|| {
420            quantization_formats
421                .iter()
422                .map(ToString::to_string)
423                .collect::<Vec<_>>()
424                .join("+")
425        });
426        let mut supported_dtypes =
427            BTreeSet::from([data_type_label(self.descriptor.execution_dtype())]);
428        supported_dtypes.extend(
429            self.family
430                .program()
431                .states()
432                .iter()
433                .filter_map(|state| element_type_label(state.tensor.element_type)),
434        );
435        let moe = moe_capabilities_from_program(&self.family)?;
436
437        Ok(ModelCapabilities {
438            architecture: self.descriptor.architecture().to_owned(),
439            quantization,
440            moe,
441            max_context_len: Some(self.descriptor.maximum_sequence_tokens()),
442            num_hidden_layers: Some(self.descriptor.layer_count()),
443            head_dim: Some(self.descriptor.attention_head_dimension()),
444            kv_heads: Some(self.descriptor.kv_head_count()),
445            estimated_weight_bytes: (estimated_weight_bytes > 0).then_some(estimated_weight_bytes),
446            recurrent_state_bytes_per_sequence: (recurrent_state_bytes_per_sequence > 0)
447                .then_some(recurrent_state_bytes_per_sequence),
448            supported_dtypes: supported_dtypes.into_iter().collect(),
449            graph_safe_moe: false,
450        })
451    }
452
453    /// Projects typed package facts into the transitional `ModelExecutor`
454    /// metadata contract. The family id is descriptive only and never drives
455    /// executor selection.
456    pub fn model_info(&self, model_id: ModelId, device: Device) -> ModelInfo {
457        ModelInfo {
458            model_id,
459            model_type: ModelType::Custom(self.family.family_id().to_string()),
460            num_parameters: self.descriptor.parameter_count(),
461            hidden_size: self.descriptor.hidden_size(),
462            num_layers: self.descriptor.layer_count(),
463            num_heads: self.descriptor.attention_head_count(),
464            num_kv_heads: self.descriptor.kv_head_count(),
465            vocab_size: self.descriptor.vocabulary_size(),
466            max_sequence_length: self.descriptor.maximum_sequence_tokens(),
467            dtype: self.descriptor.execution_dtype(),
468            device,
469            version: None,
470            license: None,
471            metadata: Default::default(),
472        }
473    }
474}
475
476fn moe_capabilities_from_program(
477    family: &PreparedModelFamily,
478) -> ferrum_types::Result<Option<MoeCapabilities>> {
479    let mut capabilities = None;
480    for node in family
481        .program()
482        .blocks()
483        .iter()
484        .flat_map(|block| &block.nodes)
485        .filter(|node| {
486            matches!(
487                node.operation_id.as_str(),
488                ROUTED_SHARED_SWIGLU_MOE_OPERATION_ID | ROUTED_SWIGLU_MOE_OPERATION_ID
489            )
490        })
491    {
492        let current = MoeCapabilities {
493            num_experts: required_positive_node_attribute(node, "expert_count")?,
494            experts_per_token: required_positive_node_attribute(node, "experts_per_token")?,
495            moe_intermediate_size: Some(required_positive_node_attribute(
496                node,
497                "routed_intermediate_size",
498            )?),
499        };
500        if current.experts_per_token > current.num_experts {
501            return Err(ferrum_types::FerrumError::model(format!(
502                "MoE program node {} routes {} experts per token from only {} experts",
503                node.id, current.experts_per_token, current.num_experts
504            )));
505        }
506        if capabilities
507            .as_ref()
508            .is_some_and(|expected| expected != &current)
509        {
510            return Err(ferrum_types::FerrumError::model(format!(
511                "MoE program node {} has capabilities inconsistent with earlier layers",
512                node.id
513            )));
514        }
515        capabilities = Some(current);
516    }
517    Ok(capabilities)
518}
519
520fn required_positive_node_attribute(
521    node: &ProgramNode,
522    attribute: &str,
523) -> ferrum_types::Result<usize> {
524    let attribute_id = AttributeId::new(attribute).map_err(|error| {
525        ferrum_types::FerrumError::internal(format!(
526            "standard MoE attribute id {attribute:?} is invalid: {error}"
527        ))
528    })?;
529    let Some(SemanticValue::Unsigned(value)) = node.attributes.get(&attribute_id) else {
530        return Err(ferrum_types::FerrumError::model(format!(
531            "MoE program node {} lacks unsigned attribute {attribute:?}",
532            node.id
533        )));
534    };
535    let value = usize::try_from(*value).map_err(|_| {
536        ferrum_types::FerrumError::model(format!(
537            "MoE program node {} attribute {attribute:?} exceeds usize",
538            node.id
539        ))
540    })?;
541    if value == 0 {
542        return Err(ferrum_types::FerrumError::model(format!(
543            "MoE program node {} attribute {attribute:?} must be positive",
544            node.id
545        )));
546    }
547    Ok(value)
548}
549
550fn data_type_label(data_type: DataType) -> String {
551    data_type.to_string().to_ascii_lowercase()
552}
553
554fn element_type_label(element_type: ElementType) -> Option<String> {
555    match element_type {
556        ElementType::F16 => Some("fp16".to_owned()),
557        ElementType::Bf16 => Some("bf16".to_owned()),
558        ElementType::F32 => Some("fp32".to_owned()),
559        ElementType::Bool
560        | ElementType::U8
561        | ElementType::U32
562        | ElementType::I8
563        | ElementType::I32 => None,
564    }
565}
566
567/// A resolved static registration. Resolution reads only `config.json`; model
568/// weights are not opened until [`RegisteredProductionModel::prepare`] after
569/// the engine has selected a compatible backend composition.
570pub struct RegisteredProductionModel {
571    registration: &'static ModelLoaderRegistration,
572    external_metadata_id: ExternalModelMetadataId,
573}
574
575impl RegisteredProductionModel {
576    pub fn external_metadata_id(&self) -> &ExternalModelMetadataId {
577        &self.external_metadata_id
578    }
579
580    pub const fn execution_kind(&self) -> ProductionExecutionKind {
581        self.registration.execution_kind
582    }
583
584    pub fn validate_semantic_config(&self, raw: &[u8]) -> ferrum_types::Result<()> {
585        let current = external_metadata_id_from_bytes(raw, "config.json")?;
586        if current != self.external_metadata_id {
587            return Err(ferrum_types::FerrumError::model(format!(
588                "model metadata identity changed between registration and semantic preflight: expected {} got {}",
589                self.external_metadata_id, current
590            )));
591        }
592        (self.registration.validate_semantic_config)(&self.external_metadata_id, raw)
593    }
594
595    pub fn prepare(&self, model_dir: &Path) -> ferrum_types::Result<PreparedProductionModel> {
596        let original = OriginalModelSource {
597            kind: ModelSourceKind::LocalDirectory,
598            location: model_dir.display().to_string(),
599            requested_revision: None,
600        };
601        let sources = Arc::new(ProductionModelSourceBundle::open_with_semantic_preflight(
602            model_dir,
603            model_dir,
604            ProductionWeightArtifact::safetensors_directory(model_dir),
605            OriginalModelSources {
606                semantic: original.clone(),
607                tokenizer: original.clone(),
608                weights: original,
609            },
610            |raw| self.validate_semantic_config(raw),
611        )?);
612        self.prepare_from_sources(sources)
613    }
614
615    pub fn prepare_from_sources(
616        &self,
617        sources: Arc<ProductionModelSourceBundle>,
618    ) -> ferrum_types::Result<PreparedProductionModel> {
619        self.validate_semantic_config(sources.config_json())?;
620        let prepared = (self.registration.prepare)(sources)?;
621        if prepared.family().external_metadata_id() != &self.external_metadata_id {
622            return Err(ferrum_types::FerrumError::model(format!(
623                "registered model loader returned metadata identity {} for resolved identity {}",
624                prepared.family().external_metadata_id(),
625                self.external_metadata_id
626            )));
627        }
628        if prepared.execution_kind() != self.registration.execution_kind {
629            return Err(ferrum_types::FerrumError::model(format!(
630                "registered model loader returned execution kind {:?} for registered kind {:?}",
631                prepared.execution_kind(),
632                self.registration.execution_kind
633            )));
634        }
635        Ok(prepared)
636    }
637}
638
639/// Explicit migration result keyed only by external model metadata.
640///
641/// Product paths that require vNext must consume this through
642/// [`ProductionModelRegistration::into_required`]. A legacy result exists only
643/// for an explicit registry row; unknown metadata is rejected during
644/// resolution and can never enter the old architecture cascade implicitly.
645pub enum ProductionModelRegistration {
646    Registered(RegisteredProductionModel),
647    LegacyRegistered {
648        external_metadata_id: ExternalModelMetadataId,
649    },
650}
651
652impl ProductionModelRegistration {
653    pub fn external_metadata_id(&self) -> &ExternalModelMetadataId {
654        match self {
655            Self::Registered(registration) => registration.external_metadata_id(),
656            Self::LegacyRegistered {
657                external_metadata_id,
658                ..
659            } => external_metadata_id,
660        }
661    }
662
663    fn validate_semantic_config(&self, raw: &[u8]) -> ferrum_types::Result<()> {
664        match self {
665            Self::Registered(registration) => registration.validate_semantic_config(raw),
666            Self::LegacyRegistered {
667                external_metadata_id,
668                ..
669            } => {
670                let current = external_metadata_id_from_bytes(raw, "config.json")?;
671                if &current != external_metadata_id {
672                    return Err(ferrum_types::FerrumError::model(format!(
673                        "model metadata identity changed during semantic preflight: expected {external_metadata_id} got {current}"
674                    )));
675                }
676                Ok(())
677            }
678        }
679    }
680
681    /// Requires a registered vNext production package and fails closed instead
682    /// of allowing a product caller to fall back to a legacy executor.
683    pub fn into_required(self) -> ferrum_types::Result<RegisteredProductionModel> {
684        match self {
685            Self::Registered(registration) => Ok(registration),
686            Self::LegacyRegistered {
687                external_metadata_id,
688                ..
689            } => Err(ferrum_types::FerrumError::unsupported(format!(
690                "model family metadata {external_metadata_id} is registered for the legacy runtime only; vNext product fallback is forbidden"
691            ))),
692        }
693    }
694}
695
696/// Resolves and validates one semantic config without accessing its physical
697/// weight source. Family-specific validation is supplied by the same registry
698/// row that owns preparation, so product composition has no model-name switch.
699pub fn validate_registered_model_semantics(raw: &[u8]) -> ferrum_types::Result<()> {
700    let external_metadata_id = external_metadata_id_from_bytes(raw, "config.json")?;
701    let registration = resolve_registered_model(external_metadata_id)?;
702    registration.validate_semantic_config(raw)
703}
704
705/// Product source constructor with a hard semantic-before-weights ordering.
706/// The exact config bytes validated by the registry are retained in the
707/// returned immutable bundle.
708pub fn open_registered_product_sources(
709    semantic_root: impl AsRef<Path>,
710    tokenizer_root: impl AsRef<Path>,
711    weights: ProductionWeightArtifact,
712    original_sources: OriginalModelSources,
713) -> ferrum_types::Result<ProductionModelSourceBundle> {
714    ProductionModelSourceBundle::open_with_semantic_preflight(
715        semantic_root,
716        tokenizer_root,
717        weights,
718        original_sources,
719        validate_registered_model_semantics,
720    )
721}
722
723pub fn open_registered_colocated_safetensors(
724    model_dir: impl AsRef<Path>,
725) -> ferrum_types::Result<ProductionModelSourceBundle> {
726    let model_dir = model_dir.as_ref();
727    let original = OriginalModelSource {
728        kind: ModelSourceKind::LocalDirectory,
729        location: model_dir.display().to_string(),
730        requested_revision: None,
731    };
732    open_registered_product_sources(
733        model_dir,
734        model_dir,
735        ProductionWeightArtifact::safetensors_directory(model_dir),
736        OriginalModelSources {
737            semantic: original.clone(),
738            tokenizer: original.clone(),
739            weights: original,
740        },
741    )
742}
743
744pub fn resolve_registered_model_from_dir(
745    model_dir: &Path,
746) -> ferrum_types::Result<ProductionModelRegistration> {
747    let external_metadata_id = external_metadata_id_from_model_dir(model_dir)?;
748    resolve_registered_model(external_metadata_id)
749}
750
751pub fn resolve_registered_model_from_sources(
752    sources: &ProductionModelSourceBundle,
753) -> ferrum_types::Result<ProductionModelRegistration> {
754    let external_metadata_id =
755        external_metadata_id_from_bytes(sources.config_json(), "config.json")?;
756    resolve_registered_model(external_metadata_id)
757}
758
759fn resolve_registered_model(
760    external_metadata_id: ExternalModelMetadataId,
761) -> ferrum_types::Result<ProductionModelRegistration> {
762    let mut loaders = MODEL_LOADERS.iter().filter(|registration| {
763        registration
764            .external_metadata_ids
765            .contains(&external_metadata_id.as_str())
766    });
767    let loader = loaders.next();
768    if loaders.next().is_some() {
769        return Err(ferrum_types::FerrumError::internal(format!(
770            "model metadata {external_metadata_id} has duplicate vNext production registrations"
771        )));
772    }
773    let mut legacy_rows = LEGACY_MODELS
774        .iter()
775        .filter(|registration| registration.external_metadata_id == external_metadata_id.as_str());
776    let legacy = legacy_rows.next();
777    if legacy_rows.next().is_some() {
778        return Err(ferrum_types::FerrumError::internal(format!(
779            "model metadata {external_metadata_id} has duplicate legacy registrations"
780        )));
781    }
782
783    match (loader, legacy) {
784        (Some(_), Some(_)) => Err(ferrum_types::FerrumError::internal(format!(
785            "model metadata {external_metadata_id} is registered in both vNext and legacy registries"
786        ))),
787        (Some(registration), None) => Ok(ProductionModelRegistration::Registered(
788            RegisteredProductionModel {
789                registration,
790                external_metadata_id,
791            },
792        )),
793        (None, Some(_)) => Ok(ProductionModelRegistration::LegacyRegistered {
794            external_metadata_id,
795        }),
796        (None, None) => Err(ferrum_types::FerrumError::unsupported(format!(
797            "model metadata {external_metadata_id} is absent from both the vNext and explicit legacy registries; implicit architecture fallback is forbidden"
798        ))),
799    }
800}
801
802fn external_metadata_id_from_model_dir(
803    model_dir: &Path,
804) -> ferrum_types::Result<ExternalModelMetadataId> {
805    let path = model_dir.join("config.json");
806    let raw = fs::read(&path)
807        .map_err(|error| ferrum_types::FerrumError::model(format!("read {path:?}: {error}")))?;
808    external_metadata_id_from_bytes(&raw, &path.display().to_string())
809}
810
811fn external_metadata_id_from_bytes(
812    raw: &[u8],
813    source: &str,
814) -> ferrum_types::Result<ExternalModelMetadataId> {
815    let config: Value = serde_json::from_slice(raw)
816        .map_err(|error| ferrum_types::FerrumError::model(format!("parse {source}: {error}")))?;
817    let architectures = config
818        .get("architectures")
819        .and_then(Value::as_array)
820        .ok_or_else(|| {
821            ferrum_types::FerrumError::model(
822                "config.json must declare exactly one architectures entry for typed family resolution",
823            )
824        })?;
825    let architecture = match architectures.as_slice() {
826        [value] => value.as_str().filter(|value| !value.is_empty()),
827        _ => None,
828    }
829    .ok_or_else(|| {
830        ferrum_types::FerrumError::model(
831            "config.json must declare exactly one non-empty architecture identity",
832        )
833    })?;
834    ExternalModelMetadataId::new(format!("hf.architecture.{architecture}"))
835        .map_err(|error| ferrum_types::FerrumError::model(error.to_string()))
836}
837
838#[cfg(test)]
839mod tests {
840    use super::*;
841
842    #[test]
843    fn required_production_selection_rejects_legacy_family() {
844        let metadata = ExternalModelMetadataId::new("hf.architecture.LlamaForCausalLM").unwrap();
845        let selection = ProductionModelRegistration::LegacyRegistered {
846            external_metadata_id: metadata,
847        };
848
849        let error = match selection.into_required() {
850            Ok(_) => panic!("legacy family unexpectedly entered the vNext product path"),
851            Err(error) => error.to_string(),
852        };
853
854        assert!(
855            error.contains("hf.architecture.LlamaForCausalLM"),
856            "{error}"
857        );
858        assert!(
859            error.contains("vNext product fallback is forbidden"),
860            "{error}"
861        );
862    }
863
864    #[test]
865    fn registry_ids_are_unique_and_disjoint() {
866        let mut ids = std::collections::BTreeSet::new();
867        let mut gguf_architectures = std::collections::BTreeSet::new();
868        for registration in MODEL_LOADERS {
869            for external_metadata_id in registration.external_metadata_ids {
870                assert!(
871                    ids.insert(*external_metadata_id),
872                    "duplicate vNext registration {external_metadata_id}"
873                );
874            }
875            for architecture in registration.gguf_architectures {
876                assert!(
877                    gguf_architectures.insert(*architecture),
878                    "GGUF architecture {architecture} maps to more than one vNext registration"
879                );
880            }
881        }
882        for registration in LEGACY_MODELS {
883            assert!(
884                ids.insert(registration.external_metadata_id),
885                "registration {} appears in both or more than once",
886                registration.external_metadata_id
887            );
888        }
889    }
890
891    #[test]
892    fn migrated_gguf_architecture_requires_typed_product_sources() {
893        assert!(gguf_architecture_requires_typed_product_sources("qwen35"));
894        assert!(gguf_architecture_requires_typed_product_sources(
895            "qwen35moe"
896        ));
897        assert!(gguf_architecture_requires_typed_product_sources("qwen3moe"));
898        assert!(!gguf_architecture_requires_typed_product_sources("qwen3"));
899    }
900
901    #[test]
902    fn production_family_registry_is_derived_from_loader_rows() {
903        let registry = ProductionModelFamilyRegistry::new().unwrap();
904        assert_eq!(registry.registrations().len(), MODEL_LOADERS.len());
905        let metadata = ExternalModelMetadataId::new(qwen35::EXTERNAL_METADATA_ID).unwrap();
906        let registration = (&registry as &dyn ModelFamilyRegistry)
907            .resolve_external(&metadata)
908            .unwrap();
909        assert_eq!(registration.family_id().as_str(), qwen35::FAMILY_ID);
910        let moe_metadata = ExternalModelMetadataId::new(qwen35::MOE_EXTERNAL_METADATA_ID).unwrap();
911        let moe_registration = (&registry as &dyn ModelFamilyRegistry)
912            .resolve_external(&moe_metadata)
913            .unwrap();
914        assert_eq!(moe_registration.family_id().as_str(), qwen35::FAMILY_ID);
915        let qwen3_moe_metadata =
916            ExternalModelMetadataId::new(qwen3_moe::EXTERNAL_METADATA_ID).unwrap();
917        let qwen3_moe_registration = (&registry as &dyn ModelFamilyRegistry)
918            .resolve_external(&qwen3_moe_metadata)
919            .unwrap();
920        assert_eq!(
921            qwen3_moe_registration.family_id().as_str(),
922            qwen3_moe::FAMILY_ID
923        );
924    }
925
926    #[test]
927    fn qwen35_moe_metadata_resolves_to_vnext_product_loader() {
928        let directory = tempfile::tempdir().unwrap();
929        fs::write(
930            directory.path().join("config.json"),
931            r#"{"architectures":["Qwen3_5MoeForConditionalGeneration"]}"#,
932        )
933        .unwrap();
934
935        let registration = resolve_registered_model_from_dir(directory.path())
936            .unwrap()
937            .into_required()
938            .unwrap();
939        assert_eq!(
940            registration.external_metadata_id().as_str(),
941            qwen35::MOE_EXTERNAL_METADATA_ID
942        );
943    }
944
945    #[test]
946    fn qwen3_moe_metadata_resolves_to_vnext_product_loader() {
947        let directory = tempfile::tempdir().unwrap();
948        fs::write(
949            directory.path().join("config.json"),
950            r#"{"architectures":["Qwen3MoeForCausalLM"]}"#,
951        )
952        .unwrap();
953
954        let registration = resolve_registered_model_from_dir(directory.path())
955            .unwrap()
956            .into_required()
957            .unwrap();
958        assert_eq!(
959            registration.external_metadata_id().as_str(),
960            qwen3_moe::EXTERNAL_METADATA_ID
961        );
962    }
963
964    #[test]
965    fn unknown_metadata_cannot_enter_an_implicit_legacy_path() {
966        let directory = tempfile::tempdir().unwrap();
967        fs::write(
968            directory.path().join("config.json"),
969            r#"{"architectures":["UnregisteredQwenForConditionalGeneration"]}"#,
970        )
971        .unwrap();
972
973        let error = resolve_registered_model_from_dir(directory.path())
974            .err()
975            .expect("unknown architecture must fail closed")
976            .to_string();
977        assert!(
978            error.contains("hf.architecture.UnregisteredQwenForConditionalGeneration"),
979            "{error}"
980        );
981        assert!(
982            error.contains("implicit architecture fallback is forbidden"),
983            "{error}"
984        );
985    }
986
987    #[test]
988    fn registered_source_open_rejects_nested_layout_before_weight_resolution() {
989        let semantic = tempfile::tempdir().unwrap();
990        let tokenizer = tempfile::tempdir().unwrap();
991        fs::write(
992            semantic.path().join("config.json"),
993            r#"{
994                "architectures":["Qwen3_5MoeForConditionalGeneration"],
995                "model_type":"qwen3_5_moe",
996                "text_config":{"model_type":"unsupported_nested_layout"}
997            }"#,
998        )
999        .unwrap();
1000        fs::write(
1001            tokenizer.path().join("tokenizer.json"),
1002            br#"{"version":"1.0"}"#,
1003        )
1004        .unwrap();
1005        let original = OriginalModelSource {
1006            kind: ModelSourceKind::LocalDirectory,
1007            location: "fixture".to_owned(),
1008            requested_revision: None,
1009        };
1010
1011        let error = open_registered_product_sources(
1012            semantic.path(),
1013            tokenizer.path(),
1014            ProductionWeightArtifact::safetensors_directory(
1015                semantic.path().join("missing-weight-root"),
1016            ),
1017            OriginalModelSources {
1018                semantic: original.clone(),
1019                tokenizer: original.clone(),
1020                weights: original,
1021            },
1022        )
1023        .expect_err("invalid nested semantics must fail before resolving weights")
1024        .to_string();
1025
1026        assert!(
1027            error.contains("unsupported Qwen3.5 text model_type"),
1028            "{error}"
1029        );
1030        assert!(
1031            !error.contains("weight source is not a directory"),
1032            "{error}"
1033        );
1034    }
1035
1036    #[test]
1037    fn registered_semantic_preflight_accepts_reference_dense_and_moe_configs() {
1038        let root = Path::new(env!("CARGO_MANIFEST_DIR")).join(
1039            "../../docs/goals/model-coverage-2026-06-12/artifacts/\
1040             w3_hf_config_probe_20260617T131209Z_f97c1d6f",
1041        );
1042        for name in [
1043            "dense_min_reference.config.json",
1044            "moe_shared_expert_reference.config.json",
1045        ] {
1046            let raw = fs::read(root.join(name)).unwrap();
1047            validate_registered_model_semantics(&raw)
1048                .unwrap_or_else(|error| panic!("{name}: {error}"));
1049        }
1050    }
1051
1052    #[test]
1053    fn registration_resolution_does_not_open_weights_and_rechecks_identity() {
1054        let directory = tempfile::tempdir().unwrap();
1055        let config_path = directory.path().join("config.json");
1056        fs::write(
1057            &config_path,
1058            r#"{"architectures":["Qwen3_5ForConditionalGeneration"]}"#,
1059        )
1060        .unwrap();
1061
1062        let registration = resolve_registered_model_from_dir(directory.path())
1063            .unwrap()
1064            .into_required()
1065            .unwrap();
1066        assert_eq!(
1067            registration.external_metadata_id().as_str(),
1068            qwen35::EXTERNAL_METADATA_ID
1069        );
1070        assert_eq!(
1071            registration.execution_kind(),
1072            ProductionExecutionKind::CausalLanguage
1073        );
1074
1075        fs::write(
1076            config_path,
1077            r#"{"architectures":["DifferentForConditionalGeneration"]}"#,
1078        )
1079        .unwrap();
1080        let error = match registration.prepare(directory.path()) {
1081            Ok(_) => panic!("changed metadata unexpectedly prepared"),
1082            Err(error) => error.to_string(),
1083        };
1084        assert!(error.contains("metadata identity changed"), "{error}");
1085        assert!(error.contains(qwen35::EXTERNAL_METADATA_ID), "{error}");
1086    }
1087
1088    #[test]
1089    fn causal_language_descriptor_rejects_invalid_runtime_facts() {
1090        assert!(CausalLanguageModelDescriptor::new(
1091            "test",
1092            0,
1093            16,
1094            2,
1095            2,
1096            1,
1097            4,
1098            32,
1099            128,
1100            DataType::FP16,
1101        )
1102        .is_err());
1103        assert!(CausalLanguageModelDescriptor::new(
1104            "test",
1105            1,
1106            16,
1107            2,
1108            2,
1109            1,
1110            0,
1111            32,
1112            128,
1113            DataType::FP16,
1114        )
1115        .is_err());
1116        assert!(CausalLanguageModelDescriptor::new(
1117            "test",
1118            1,
1119            16,
1120            2,
1121            2,
1122            3,
1123            4,
1124            32,
1125            128,
1126            DataType::FP16,
1127        )
1128        .is_err());
1129        assert!(CausalLanguageModelDescriptor::new(
1130            "test",
1131            1,
1132            16,
1133            2,
1134            3,
1135            2,
1136            4,
1137            32,
1138            128,
1139            DataType::FP16,
1140        )
1141        .is_err());
1142        assert!(CausalLanguageModelDescriptor::new(
1143            "test",
1144            1,
1145            16,
1146            2,
1147            2,
1148            1,
1149            4,
1150            32,
1151            128,
1152            DataType::INT8,
1153        )
1154        .is_err());
1155        assert!(
1156            CausalLanguageModelDescriptor::new("test", 1, 15, 2, 2, 1, 4, 32, 128, DataType::FP16,)
1157                .is_ok(),
1158            "hidden width and explicit attention projection width are independent facts"
1159        );
1160    }
1161}