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 = frink_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 {
175 arch == "glm-dsa"
176}
177
178fn is_gemma4_arch(arch: &str) -> bool {
179 crate::gemma4_engine::GEMMA4_ARCHES.contains(&arch)
180}
181
182pub fn load_glm52_engine_from_path(path: &std::path::Path) -> Result<ServedEngine, LoadError> {
184 let file = frink_gguf::ShardedGguf::open(path)?;
185 let arch = file
186 .metadata_str("general.architecture")
187 .unwrap_or("unknown");
188 if !is_glm52_arch(arch) {
189 return Err(LoadError::DedicatedArchitectureRequired(
190 arch.to_string(),
191 "not a GLM-5.2 architecture (expected glm-dsa)",
192 ));
193 }
194 match select_engine_kind(arch) {
195 Ok(SelectedEngineKind::DedicatedStack) => {}
196 Ok(other) => {
197 return Err(LoadError::DedicatedArchitectureRequired(
198 arch.to_string(),
199 match other {
200 SelectedEngineKind::GenericDecoder => "generic decoder arch, not GLM DSA",
201 SelectedEngineKind::Mla => "MLA arch, not GLM DSA",
202 SelectedEngineKind::RecurrentHybrid => "hybrid/recurrent, not GLM DSA",
203 SelectedEngineKind::EncoderDecoder => "encoder-decoder, not GLM DSA",
204 SelectedEngineKind::Gemma4 => "gemma4 dedicated, not GLM DSA",
205 SelectedEngineKind::DedicatedStack => unreachable!(),
206 },
207 ));
208 }
209 Err(EngineSelectError::Unknown(a)) => {
210 return Err(LoadError::UnsupportedArchitecture(a));
211 }
212 Err(EngineSelectError::OutOfScope(a, r)) => {
213 return Err(LoadError::UnsupportedFeature(a, r.to_string()));
214 }
215 Err(EngineSelectError::DedicatedUnavailable(a, r)) => {
216 return Err(LoadError::DedicatedArchitectureRequired(a, r));
217 }
218 }
219 Ok(ServedEngine::Glm52(glm52_gguf_loader::load_glm52_engine(
220 &file,
221 )?))
222}
223
224pub fn load_gemma4_engine_from_path(path: &std::path::Path) -> Result<ServedEngine, LoadError> {
226 let file = frink_gguf::ShardedGguf::open(path)?;
227 let arch = file
228 .metadata_str("general.architecture")
229 .unwrap_or("unknown");
230 if !is_gemma4_arch(arch) {
231 return Err(LoadError::DedicatedArchitectureRequired(
232 arch.to_string(),
233 "not a gemma4 / gemma4-assistant architecture",
234 ));
235 }
236 match select_engine_kind(arch) {
237 Ok(SelectedEngineKind::Gemma4) => {}
238 Ok(other) => {
239 return Err(LoadError::DedicatedArchitectureRequired(
240 arch.to_string(),
241 match other {
242 SelectedEngineKind::GenericDecoder => "generic decoder arch, not Gemma4",
243 SelectedEngineKind::Mla => "MLA arch, not Gemma4",
244 SelectedEngineKind::DedicatedStack => "dedicated non-Gemma4 stack",
245 SelectedEngineKind::RecurrentHybrid => "hybrid/recurrent, not Gemma4",
246 SelectedEngineKind::EncoderDecoder => "encoder-decoder, not Gemma4",
247 SelectedEngineKind::Gemma4 => unreachable!(),
248 },
249 ));
250 }
251 Err(EngineSelectError::Unknown(a)) => {
252 return Err(LoadError::UnsupportedArchitecture(a));
253 }
254 Err(EngineSelectError::OutOfScope(a, r)) => {
255 return Err(LoadError::UnsupportedFeature(a, r.to_string()));
256 }
257 Err(EngineSelectError::DedicatedUnavailable(a, r)) => {
258 return Err(LoadError::DedicatedArchitectureRequired(a, r));
259 }
260 }
261 Ok(ServedEngine::Gemma4(Box::new(
262 gemma4_gguf_loader::load_gemma4_engine(&file)?,
263 )))
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269
270 #[test]
271 fn llama_and_qwen3_select_generic_decoder() {
272 assert_eq!(
273 select_engine_kind("llama").unwrap(),
274 SelectedEngineKind::GenericDecoder
275 );
276 assert_eq!(
277 select_engine_kind("qwen3").unwrap(),
278 SelectedEngineKind::GenericDecoder
279 );
280 assert_eq!(
281 select_engine_kind("gemma3").unwrap(),
282 SelectedEngineKind::GenericDecoder
283 );
284 assert_eq!(
285 select_engine_kind("phi3").unwrap(),
286 SelectedEngineKind::GenericDecoder
287 );
288 assert_eq!(
293 select_engine_kind("olmoe").unwrap(),
294 SelectedEngineKind::GenericDecoder
295 );
296 }
297
298 #[test]
302 fn mamba_is_generic_and_rwkv_is_recurrent_fail_closed() {
303 assert!(matches!(
304 select_engine_kind("mamba2").unwrap(),
305 SelectedEngineKind::GenericDecoder
306 ));
307 assert!(ensure_generic_decoder("mamba2").is_ok());
308 assert!(matches!(
309 select_engine_kind("rwkv7").unwrap(),
310 SelectedEngineKind::RecurrentHybrid
311 ));
312 assert!(ensure_generic_decoder("rwkv7").is_err());
313 }
314
315 #[test]
316 fn deepseek2_is_mla_not_generic() {
317 assert_eq!(
318 select_engine_kind("deepseek2").unwrap(),
319 SelectedEngineKind::Mla
320 );
321 assert!(ensure_generic_decoder("deepseek2").is_err());
322 }
323
324 #[test]
325 fn glm4_is_generic_and_glm_dsa_is_the_dedicated_stack() {
326 assert_eq!(
327 select_engine_kind("glm4").unwrap(),
328 SelectedEngineKind::GenericDecoder
329 );
330 assert!(ensure_generic_decoder("glm4").is_ok());
331 assert_eq!(
332 select_engine_kind("glm-dsa").unwrap(),
333 SelectedEngineKind::DedicatedStack
334 );
335 }
336
337 #[test]
338 fn gemma4_selects_dedicated_engine() {
339 assert_eq!(
340 select_engine_kind("gemma4").unwrap(),
341 SelectedEngineKind::Gemma4
342 );
343 assert_eq!(
344 select_engine_kind("gemma4-assistant").unwrap(),
345 SelectedEngineKind::Gemma4
346 );
347 assert!(ensure_generic_decoder("gemma4").is_err());
348 }
349}