1use crate::capability::{resolve_profile, ArchPath, DecoderFamily};
9use crate::decoder::Decoder;
10use crate::engine::{Engine, Glm52Engine, KimiEngine, MlaEngine};
11use crate::gemma4_engine::Gemma4Engine;
12use crate::gemma4_gguf_loader;
13use crate::glm52_gguf_loader;
14use crate::loader::LoadError;
15use crate::mla_gguf_loader;
16use thiserror::Error;
17
18#[derive(Debug, Error)]
20pub enum EngineSelectError {
21 #[error("architecture {0:?} is outside the text-generation scope ({1})")]
22 OutOfScope(String, &'static str),
23 #[error("architecture {0:?} requires a dedicated engine that is not wired for serve yet: {1}")]
24 DedicatedUnavailable(String, &'static str),
25 #[error("unknown or unsupported architecture {0:?}")]
26 Unknown(String),
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum SelectedEngineKind {
32 GenericDecoder,
34 DedicatedStack,
36 Mla,
38 Gemma4,
40 RecurrentHybrid,
42 EncoderDecoder,
44}
45
46pub fn select_engine_kind(arch: &str) -> Result<SelectedEngineKind, EngineSelectError> {
48 let profile =
49 resolve_profile(arch).ok_or_else(|| EngineSelectError::Unknown(arch.to_string()))?;
50 match profile.path {
51 ArchPath::GenericGqa { .. } | ArchPath::TestFixture { .. } => {
52 Ok(SelectedEngineKind::GenericDecoder)
53 }
54 ArchPath::Deferred { reason } => {
55 Err(EngineSelectError::OutOfScope(arch.to_string(), reason))
56 }
57 ArchPath::DedicatedOnly { reason } => match profile.family {
58 DecoderFamily::Dedicated => Ok(SelectedEngineKind::DedicatedStack),
59 DecoderFamily::Mla => Ok(SelectedEngineKind::Mla),
60 DecoderFamily::GemmaFamily if is_gemma4_arch(arch) => Ok(SelectedEngineKind::Gemma4),
61 DecoderFamily::Hybrid | DecoderFamily::Recurrent => {
62 Ok(SelectedEngineKind::RecurrentHybrid)
63 }
64 DecoderFamily::EncoderDecoder => Ok(SelectedEngineKind::EncoderDecoder),
65 _ => Err(EngineSelectError::DedicatedUnavailable(
66 arch.to_string(),
67 reason,
68 )),
69 },
70 }
71}
72
73pub fn ensure_generic_decoder(arch: &str) -> Result<(), EngineSelectError> {
76 match select_engine_kind(arch)? {
77 SelectedEngineKind::GenericDecoder => Ok(()),
78 SelectedEngineKind::DedicatedStack => Err(EngineSelectError::DedicatedUnavailable(
79 arch.to_string(),
80 if is_glm52_arch(arch) {
81 "use load_glm52_engine_from_path / ServedEngine::Glm52 — not generic Decoder"
82 } else {
83 "use the dedicated Kimi/GLM/DeepSeek loader, not generic Decoder"
84 },
85 )),
86 SelectedEngineKind::Mla => Err(EngineSelectError::DedicatedUnavailable(
87 arch.to_string(),
88 "use load_mla_engine_from_path / ServedEngine::Mla — not generic Decoder",
89 )),
90 SelectedEngineKind::Gemma4 => Err(EngineSelectError::DedicatedUnavailable(
91 arch.to_string(),
92 "use load_gemma4_engine_from_path / ServedEngine::Gemma4 — not generic Decoder",
93 )),
94 SelectedEngineKind::RecurrentHybrid => {
95 let _ = crate::recurrent_engine::RecurrentEngine::reject(arch);
96 let _ = crate::hybrid_engine::HybridEngine::reject(arch);
97 Err(EngineSelectError::DedicatedUnavailable(
98 arch.to_string(),
99 "recurrent/hybrid SSM engine stub present — not yet on the serve path",
100 ))
101 }
102 SelectedEngineKind::EncoderDecoder => {
103 let _ = crate::t5_engine::T5Engine::reject(arch);
104 Err(EngineSelectError::DedicatedUnavailable(
105 arch.to_string(),
106 "T5 encoder-decoder engine stub present — not yet on the serve path",
107 ))
108 }
109 }
110}
111
112pub enum ServedEngine {
115 Decoder(Box<Decoder>),
116 Kimi(KimiEngine),
117 Glm52(Glm52Engine),
118 Mla(MlaEngine),
119 Gemma4(Box<Gemma4Engine>),
122}
123
124impl ServedEngine {
125 pub fn vocab_size(&self) -> usize {
126 match self {
127 Self::Decoder(d) => Engine::vocab_size(d.as_ref()),
128 Self::Kimi(k) => Engine::vocab_size(k),
129 Self::Glm52(g) => Engine::vocab_size(g),
130 Self::Mla(m) => Engine::vocab_size(m),
131 Self::Gemma4(g) => Engine::vocab_size(g.as_ref()),
132 }
133 }
134}
135
136pub fn load_mla_engine_from_path(path: &std::path::Path) -> Result<ServedEngine, LoadError> {
138 let file = ferrox_gguf::ShardedGguf::open(path)?;
139 let arch = file
140 .metadata_str("general.architecture")
141 .unwrap_or("unknown");
142 match select_engine_kind(arch) {
143 Ok(SelectedEngineKind::Mla) => {}
144 Ok(other) => {
145 return Err(LoadError::DedicatedArchitectureRequired(
146 arch.to_string(),
147 match other {
148 SelectedEngineKind::GenericDecoder => "generic decoder arch, not MLA",
149 SelectedEngineKind::DedicatedStack => "dedicated non-MLA stack",
150 SelectedEngineKind::RecurrentHybrid => "hybrid/recurrent, not MLA",
151 SelectedEngineKind::EncoderDecoder => "encoder-decoder, not MLA",
152 SelectedEngineKind::Gemma4 => "gemma4 dedicated, not MLA",
153 SelectedEngineKind::Mla => unreachable!(),
154 },
155 ));
156 }
157 Err(EngineSelectError::Unknown(a)) => {
158 return Err(LoadError::UnsupportedArchitecture(a));
159 }
160 Err(EngineSelectError::OutOfScope(a, r)) => {
161 return Err(LoadError::UnsupportedFeature(a, r.to_string()));
162 }
163 Err(EngineSelectError::DedicatedUnavailable(a, r)) => {
164 return Err(LoadError::DedicatedArchitectureRequired(a, r));
165 }
166 }
167 Ok(ServedEngine::Mla(mla_gguf_loader::load_mla_engine(&file)?))
168}
169
170fn is_glm52_arch(arch: &str) -> bool {
171 matches!(arch, "glm-dsa" | "glm4" | "glm4moe")
172}
173
174fn is_gemma4_arch(arch: &str) -> bool {
175 crate::gemma4_engine::GEMMA4_ARCHES.contains(&arch)
176}
177
178pub fn load_glm52_engine_from_path(path: &std::path::Path) -> Result<ServedEngine, LoadError> {
180 let file = ferrox_gguf::ShardedGguf::open(path)?;
181 let arch = file
182 .metadata_str("general.architecture")
183 .unwrap_or("unknown");
184 if !is_glm52_arch(arch) {
185 return Err(LoadError::DedicatedArchitectureRequired(
186 arch.to_string(),
187 "not a GLM-5.2 / GLM4-family architecture (expected glm-dsa/glm4/glm4moe)",
188 ));
189 }
190 match select_engine_kind(arch) {
191 Ok(SelectedEngineKind::DedicatedStack) => {}
192 Ok(other) => {
193 return Err(LoadError::DedicatedArchitectureRequired(
194 arch.to_string(),
195 match other {
196 SelectedEngineKind::GenericDecoder => "generic decoder arch, not GLM DSA",
197 SelectedEngineKind::Mla => "MLA arch, not GLM DSA",
198 SelectedEngineKind::RecurrentHybrid => "hybrid/recurrent, not GLM DSA",
199 SelectedEngineKind::EncoderDecoder => "encoder-decoder, not GLM DSA",
200 SelectedEngineKind::Gemma4 => "gemma4 dedicated, not GLM DSA",
201 SelectedEngineKind::DedicatedStack => unreachable!(),
202 },
203 ));
204 }
205 Err(EngineSelectError::Unknown(a)) => {
206 return Err(LoadError::UnsupportedArchitecture(a));
207 }
208 Err(EngineSelectError::OutOfScope(a, r)) => {
209 return Err(LoadError::UnsupportedFeature(a, r.to_string()));
210 }
211 Err(EngineSelectError::DedicatedUnavailable(a, r)) => {
212 return Err(LoadError::DedicatedArchitectureRequired(a, r));
213 }
214 }
215 Ok(ServedEngine::Glm52(glm52_gguf_loader::load_glm52_engine(
216 &file,
217 )?))
218}
219
220pub fn load_gemma4_engine_from_path(path: &std::path::Path) -> Result<ServedEngine, LoadError> {
222 let file = ferrox_gguf::ShardedGguf::open(path)?;
223 let arch = file
224 .metadata_str("general.architecture")
225 .unwrap_or("unknown");
226 if !is_gemma4_arch(arch) {
227 return Err(LoadError::DedicatedArchitectureRequired(
228 arch.to_string(),
229 "not a gemma4 / gemma4-assistant architecture",
230 ));
231 }
232 match select_engine_kind(arch) {
233 Ok(SelectedEngineKind::Gemma4) => {}
234 Ok(other) => {
235 return Err(LoadError::DedicatedArchitectureRequired(
236 arch.to_string(),
237 match other {
238 SelectedEngineKind::GenericDecoder => "generic decoder arch, not Gemma4",
239 SelectedEngineKind::Mla => "MLA arch, not Gemma4",
240 SelectedEngineKind::DedicatedStack => "dedicated non-Gemma4 stack",
241 SelectedEngineKind::RecurrentHybrid => "hybrid/recurrent, not Gemma4",
242 SelectedEngineKind::EncoderDecoder => "encoder-decoder, not Gemma4",
243 SelectedEngineKind::Gemma4 => unreachable!(),
244 },
245 ));
246 }
247 Err(EngineSelectError::Unknown(a)) => {
248 return Err(LoadError::UnsupportedArchitecture(a));
249 }
250 Err(EngineSelectError::OutOfScope(a, r)) => {
251 return Err(LoadError::UnsupportedFeature(a, r.to_string()));
252 }
253 Err(EngineSelectError::DedicatedUnavailable(a, r)) => {
254 return Err(LoadError::DedicatedArchitectureRequired(a, r));
255 }
256 }
257 Ok(ServedEngine::Gemma4(Box::new(
258 gemma4_gguf_loader::load_gemma4_engine(&file)?,
259 )))
260}
261
262#[cfg(test)]
263mod tests {
264 use super::*;
265
266 #[test]
267 fn llama_and_qwen3_select_generic_decoder() {
268 assert_eq!(
269 select_engine_kind("llama").unwrap(),
270 SelectedEngineKind::GenericDecoder
271 );
272 assert_eq!(
273 select_engine_kind("qwen3").unwrap(),
274 SelectedEngineKind::GenericDecoder
275 );
276 assert_eq!(
277 select_engine_kind("gemma3").unwrap(),
278 SelectedEngineKind::GenericDecoder
279 );
280 assert_eq!(
281 select_engine_kind("phi3").unwrap(),
282 SelectedEngineKind::GenericDecoder
283 );
284 assert_eq!(
285 select_engine_kind("mixtral").unwrap(),
286 SelectedEngineKind::GenericDecoder
287 );
288 }
289
290 #[test]
291 fn mamba_is_recurrent_fail_closed_for_generic() {
292 assert!(matches!(
293 select_engine_kind("mamba2").unwrap(),
294 SelectedEngineKind::RecurrentHybrid
295 ));
296 assert!(ensure_generic_decoder("mamba2").is_err());
297 }
298
299 #[test]
300 fn deepseek2_is_mla_not_generic() {
301 assert_eq!(
302 select_engine_kind("deepseek2").unwrap(),
303 SelectedEngineKind::Mla
304 );
305 assert!(ensure_generic_decoder("deepseek2").is_err());
306 }
307
308 #[test]
309 fn glm4_is_dedicated_stack_not_generic() {
310 assert_eq!(
311 select_engine_kind("glm4").unwrap(),
312 SelectedEngineKind::DedicatedStack
313 );
314 assert!(ensure_generic_decoder("glm4").is_err());
315 }
316
317 #[test]
318 fn gemma4_selects_dedicated_engine() {
319 assert_eq!(
320 select_engine_kind("gemma4").unwrap(),
321 SelectedEngineKind::Gemma4
322 );
323 assert_eq!(
324 select_engine_kind("gemma4-assistant").unwrap(),
325 SelectedEngineKind::Gemma4
326 );
327 assert!(ensure_generic_decoder("gemma4").is_err());
328 }
329}