1use std::path::Path;
24
25use serde::{Deserialize, Serialize};
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
31#[serde(rename_all = "snake_case")]
32pub enum Backend {
33 Vllm,
35 Ollama,
37 LlamaCpp,
39}
40
41impl Backend {
42 #[must_use]
44 pub fn as_str(self) -> &'static str {
45 match self {
46 Self::Vllm => "vllm",
47 Self::Ollama => "ollama",
48 Self::LlamaCpp => "llama_cpp",
49 }
50 }
51}
52
53impl std::str::FromStr for Backend {
54 type Err = String;
55 fn from_str(s: &str) -> Result<Self, String> {
56 match s.trim().to_ascii_lowercase().as_str() {
57 "vllm" => Ok(Self::Vllm),
58 "ollama" => Ok(Self::Ollama),
59 "llama_cpp" | "llama.cpp" | "llamacpp" => Ok(Self::LlamaCpp),
60 other => Err(format!(
61 "unknown backend `{other}` (expected vllm | ollama | llama_cpp)"
62 )),
63 }
64 }
65}
66
67#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
70#[serde(deny_unknown_fields)]
71pub struct VllmProfile {
72 #[serde(default, skip_serializing_if = "Option::is_none")]
74 pub served_name: Option<String>,
75 #[serde(default, skip_serializing_if = "Option::is_none")]
77 pub max_model_len: Option<u32>,
78 #[serde(default, skip_serializing_if = "Option::is_none")]
80 pub tensor_parallel: Option<u8>,
81 #[serde(default, skip_serializing_if = "Option::is_none")]
83 pub gpu_mem: Option<f64>,
84 #[serde(default, skip_serializing_if = "Option::is_none")]
86 pub reasoning_parser: Option<String>,
87 #[serde(default, skip_serializing_if = "Option::is_none")]
89 pub tool_call_parser: Option<String>,
90 #[serde(default, skip_serializing_if = "Option::is_none")]
92 pub enable_auto_tool_choice: Option<bool>,
93 #[serde(default, skip_serializing_if = "Vec::is_empty")]
95 pub extra: Vec<String>,
96}
97
98impl VllmProfile {
99 #[must_use]
100 pub(crate) fn merge(self, o: Self) -> Self {
101 Self {
102 served_name: o.served_name.or(self.served_name),
103 max_model_len: o.max_model_len.or(self.max_model_len),
104 tensor_parallel: o.tensor_parallel.or(self.tensor_parallel),
105 gpu_mem: o.gpu_mem.or(self.gpu_mem),
106 reasoning_parser: o.reasoning_parser.or(self.reasoning_parser),
107 tool_call_parser: o.tool_call_parser.or(self.tool_call_parser),
108 enable_auto_tool_choice: o.enable_auto_tool_choice.or(self.enable_auto_tool_choice),
109 extra: if o.extra.is_empty() {
110 self.extra
111 } else {
112 o.extra
113 },
114 }
115 }
116}
117
118#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
125#[serde(deny_unknown_fields)]
126pub struct FamilyDefaults {
127 #[serde(default, skip_serializing_if = "Option::is_none")]
128 pub vllm: Option<VllmProfile>,
129}
130
131#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
133#[serde(deny_unknown_fields)]
134pub struct OllamaProfile {
135 #[serde(default, skip_serializing_if = "Option::is_none")]
137 pub tag: Option<String>,
138 #[serde(default, skip_serializing_if = "Option::is_none")]
140 pub num_ctx: Option<u32>,
141 #[serde(default, skip_serializing_if = "Option::is_none")]
143 pub template: Option<String>,
144}
145
146impl OllamaProfile {
147 #[must_use]
148 fn merge(self, o: Self) -> Self {
149 Self {
150 tag: o.tag.or(self.tag),
151 num_ctx: o.num_ctx.or(self.num_ctx),
152 template: o.template.or(self.template),
153 }
154 }
155}
156
157#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
159#[serde(deny_unknown_fields)]
160pub struct Tuning {
161 #[serde(default, skip_serializing_if = "Option::is_none")]
162 pub temperature: Option<f64>,
163 #[serde(default, skip_serializing_if = "Option::is_none")]
164 pub top_p: Option<f64>,
165 #[serde(default, skip_serializing_if = "Option::is_none")]
166 pub top_k: Option<u32>,
167 #[serde(default, skip_serializing_if = "Option::is_none")]
169 pub context_tokens: Option<u32>,
170}
171
172impl Tuning {
173 #[must_use]
174 fn merge(self, o: Self) -> Self {
175 Self {
176 temperature: o.temperature.or(self.temperature),
177 top_p: o.top_p.or(self.top_p),
178 top_k: o.top_k.or(self.top_k),
179 context_tokens: o.context_tokens.or(self.context_tokens),
180 }
181 }
182}
183
184#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
187#[serde(deny_unknown_fields)]
188pub struct Capability {
189 #[serde(default, skip_serializing_if = "Option::is_none")]
192 pub emits_leading_reasoning: Option<bool>,
193 #[serde(default, skip_serializing_if = "Option::is_none")]
195 pub thinking_default: Option<bool>,
196 #[serde(default, skip_serializing_if = "Option::is_none")]
199 pub reasoning_content_field: Option<String>,
200}
201
202impl Capability {
203 #[must_use]
204 fn merge(self, o: Self) -> Self {
205 Self {
206 emits_leading_reasoning: o.emits_leading_reasoning.or(self.emits_leading_reasoning),
207 thinking_default: o.thinking_default.or(self.thinking_default),
208 reasoning_content_field: o.reasoning_content_field.or(self.reasoning_content_field),
209 }
210 }
211}
212
213#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
217#[serde(deny_unknown_fields)]
218pub struct ModelCard {
219 pub name: String,
221 #[serde(default, skip_serializing_if = "Option::is_none")]
223 pub backend: Option<Backend>,
224 #[serde(default, skip_serializing_if = "Option::is_none")]
227 pub footprint_gib: Option<f64>,
228 #[serde(default, skip_serializing_if = "Option::is_none")]
231 pub gated: Option<bool>,
232 #[serde(default, skip_serializing_if = "Option::is_none")]
239 pub family: Option<String>,
240 #[serde(default, skip_serializing_if = "Option::is_none")]
241 pub vllm: Option<VllmProfile>,
242 #[serde(default, skip_serializing_if = "Option::is_none")]
243 pub ollama: Option<OllamaProfile>,
244 #[serde(default, skip_serializing_if = "Option::is_none")]
245 pub tuning: Option<Tuning>,
246 #[serde(default, skip_serializing_if = "Option::is_none")]
247 pub capability: Option<Capability>,
248}
249
250fn merge_opt<T>(base: Option<T>, overlay: Option<T>, f: impl FnOnce(T, T) -> T) -> Option<T> {
251 match (base, overlay) {
252 (Some(b), Some(o)) => Some(f(b, o)),
253 (b, None) => b,
254 (None, o) => o,
255 }
256}
257
258impl ModelCard {
259 #[must_use]
263 pub fn merge(self, overlay: Self) -> Self {
264 Self {
265 name: if overlay.name.trim().is_empty() {
266 self.name
267 } else {
268 overlay.name
269 },
270 backend: overlay.backend.or(self.backend),
271 footprint_gib: overlay.footprint_gib.or(self.footprint_gib),
272 gated: overlay.gated.or(self.gated),
273 family: overlay.family.or(self.family),
274 vllm: merge_opt(self.vllm, overlay.vllm, VllmProfile::merge),
275 ollama: merge_opt(self.ollama, overlay.ollama, OllamaProfile::merge),
276 tuning: merge_opt(self.tuning, overlay.tuning, Tuning::merge),
277 capability: merge_opt(self.capability, overlay.capability, Capability::merge),
278 }
279 }
280
281 pub fn validate(&self) -> Result<(), String> {
290 if self.name.trim().is_empty() {
291 return Err("model card: `name` is empty".to_string());
292 }
293 if let Some(family) = self.family.as_deref() {
294 if family_defaults(family).is_none() {
295 return Err(format!(
296 "model card `{}`: family `{family}` has no known defaults \
297 (check for a typo, or add cards/families/{family}.toml)",
298 self.name
299 ));
300 }
301 }
302 match self.backend {
303 None => Err(format!(
304 "model card `{}`: no `backend` set (vllm | ollama | llama_cpp)",
305 self.name
306 )),
307 Some(Backend::Vllm) if self.vllm.is_none() => Err(format!(
308 "model card `{}`: backend=vllm but no [vllm] serving block",
309 self.name
310 )),
311 Some(Backend::Ollama) if self.ollama.is_none() => Err(format!(
312 "model card `{}`: backend=ollama but no [ollama] serving block",
313 self.name
314 )),
315 Some(_) => Ok(()),
316 }
317 }
318
319 pub fn to_toml(&self) -> Result<String, String> {
324 toml::to_string_pretty(self).map_err(|e| format!("card serialize: {e}"))
325 }
326}
327
328pub fn parse_card(contents: &str, ext: &str) -> Result<ModelCard, String> {
335 match ext.trim_start_matches('.').to_ascii_lowercase().as_str() {
336 "toml" => toml::from_str(contents).map_err(|e| format!("card TOML: {e}")),
337 "yaml" | "yml" => serde_yaml::from_str(contents).map_err(|e| format!("card YAML: {e}")),
338 other => Err(format!(
339 "card: unknown extension `.{other}` (expected .toml / .yaml / .yml)"
340 )),
341 }
342}
343
344pub fn load_card_file(path: &Path) -> Result<ModelCard, String> {
349 let contents =
350 std::fs::read_to_string(path).map_err(|e| format!("read {}: {e}", path.display()))?;
351 let ext = path.extension().and_then(|s| s.to_str()).unwrap_or("");
352 parse_card(&contents, ext)
353}
354
355#[must_use]
359pub fn load_dropin_dir(dir: &Path) -> Vec<ModelCard> {
360 let mut out = Vec::new();
361 let Ok(rd) = std::fs::read_dir(dir) else {
362 return out;
363 };
364 for entry in rd.flatten() {
365 let p = entry.path();
366 let ext = p.extension().and_then(|s| s.to_str()).unwrap_or("");
367 if matches!(ext, "toml" | "yaml" | "yml") {
368 if let Ok(card) = load_card_file(&p) {
369 out.push(card);
370 }
371 }
372 }
373 out
374}
375
376#[must_use]
383pub fn resolve(builtin: ModelCard, dropins: &[ModelCard], one_off: Option<ModelCard>) -> ModelCard {
384 let mut card = builtin;
385 if let Some(family) = card.family.clone() {
386 if let Some(defaults) = family_defaults(&family) {
387 card.vllm = merge_opt(defaults.vllm, card.vllm, VllmProfile::merge);
388 }
389 }
390 let name = card.name.clone();
391 for d in dropins.iter().filter(|d| d.name == name) {
392 card = card.merge(d.clone());
393 }
394 if let Some(o) = one_off {
395 card = card.merge(o);
396 }
397 card
398}
399
400#[must_use]
406pub fn family_defaults(family: &str) -> Option<FamilyDefaults> {
407 const EMBEDDED: &[(&str, &str)] = &[("qwen3", include_str!("cards/families/qwen3.toml"))];
408 let want = family.trim().to_ascii_lowercase();
409 EMBEDDED
410 .iter()
411 .find(|(name, _)| *name == want)
412 .map(|(_, toml)| toml::from_str(toml).expect("built-in family-defaults file is valid TOML"))
413}
414
415#[must_use]
420pub fn builtin_cards() -> Vec<ModelCard> {
421 const EMBEDDED: &[&str] = &[
422 include_str!("cards/ornith-1.0-35b.toml"),
423 include_str!("cards/ornith-1.0-397b.toml"),
424 ];
425 EMBEDDED
426 .iter()
427 .map(|s| parse_card(s, "toml").expect("built-in card is valid TOML"))
428 .collect()
429}
430
431#[must_use]
433pub fn builtin_card(name: &str) -> Option<ModelCard> {
434 let want = name.trim().to_ascii_lowercase();
435 builtin_cards()
436 .into_iter()
437 .find(|c| c.name.to_ascii_lowercase() == want)
438}
439
440#[must_use]
447pub fn no_hardware_leak(card: &ModelCard) -> Vec<String> {
448 let text = card.to_toml().unwrap_or_default();
450 let mut hits = Vec::new();
451 for tok in text.split(|c: char| !(c.is_ascii_digit() || c == '.')) {
452 if is_private_ipv4(tok) {
453 hits.push(tok.to_string());
454 }
455 }
456 hits
457}
458
459fn is_private_ipv4(s: &str) -> bool {
462 let octets: Vec<&str> = s.split('.').collect();
463 if octets.len() != 4 {
464 return false;
465 }
466 let mut o = [0u16; 4];
467 for (i, part) in octets.iter().enumerate() {
468 match part.parse::<u16>() {
469 Ok(v) if v <= 255 && (part.len() == 1 || !part.starts_with('0')) => o[i] = v,
470 _ => return false,
471 }
472 }
473 o[0] == 10
474 || (o[0] == 172 && (16..=31).contains(&o[1]))
475 || (o[0] == 192 && o[1] == 168)
476 || (o[0] == 100 && (64..=127).contains(&o[1]))
477}
478
479#[cfg(test)]
480mod tests {
481 use super::*;
482 use std::str::FromStr;
483
484 fn ornith_toml() -> &'static str {
485 r#"
486name = "Ornith-1.0-35B"
487backend = "vllm"
488footprint_gib = 35.0
489
490[vllm]
491served_name = "Ornith-1.0-35B"
492max_model_len = 262144
493reasoning_parser = "qwen3"
494tool_call_parser = "qwen3_xml"
495enable_auto_tool_choice = true
496
497[tuning]
498temperature = 0.6
499top_p = 0.95
500top_k = 20
501context_tokens = 262144
502
503[capability]
504emits_leading_reasoning = true
505thinking_default = true
506reasoning_content_field = "reasoning_content"
507"#
508 }
509
510 #[test]
512 fn backend_tokens_round_trip() {
513 for (tok, b) in [
514 ("vllm", Backend::Vllm),
515 ("ollama", Backend::Ollama),
516 ("llama_cpp", Backend::LlamaCpp),
517 ] {
518 assert_eq!(Backend::from_str(tok).unwrap(), b);
519 assert_eq!(b.as_str(), tok);
520 }
521 assert_eq!(Backend::from_str(" VLLM ").unwrap(), Backend::Vllm);
523 assert_eq!(Backend::from_str("llama.cpp").unwrap(), Backend::LlamaCpp);
524 assert!(Backend::from_str("tgi").is_err());
526 }
527
528 #[test]
530 fn parses_from_toml_and_yaml_identically() {
531 let from_toml = parse_card(ornith_toml(), "toml").expect("toml");
532 let yaml = r#"
534name: Ornith-1.0-35B
535backend: vllm
536footprint_gib: 35.0
537vllm:
538 served_name: Ornith-1.0-35B
539 max_model_len: 262144
540 reasoning_parser: qwen3
541 tool_call_parser: qwen3_xml
542 enable_auto_tool_choice: true
543tuning:
544 temperature: 0.6
545 top_p: 0.95
546 top_k: 20
547 context_tokens: 262144
548capability:
549 emits_leading_reasoning: true
550 thinking_default: true
551 reasoning_content_field: reasoning_content
552"#;
553 let from_yaml = parse_card(yaml, "yml").expect("yaml");
554 assert_eq!(from_toml, from_yaml, "TOML and YAML must parse identically");
555 let reparsed = parse_card(&from_toml.to_toml().unwrap(), "toml").unwrap();
557 assert_eq!(reparsed, from_toml);
558 }
559
560 #[test]
561 fn parse_rejects_unknown_extension_and_bad_syntax() {
562 assert!(parse_card(ornith_toml(), "json").is_err());
563 assert!(parse_card("name = \"x\"\nbogus_field = 1", "toml").is_err()); }
565
566 #[test]
568 fn overlay_wins_field_by_field_and_deep_merges() {
569 let base = parse_card(ornith_toml(), "toml").unwrap();
570 let overlay: ModelCard = toml::from_str(
572 "name = \"Ornith-1.0-35B\"\n[vllm]\nmax_model_len = 65536\n[tuning]\ntemperature = 1.0",
573 )
574 .unwrap();
575 let merged = base.clone().merge(overlay);
576 let v = merged.vllm.as_ref().unwrap();
577 assert_eq!(v.max_model_len, Some(65536));
579 assert_eq!(merged.tuning.as_ref().unwrap().temperature, Some(1.0));
580 assert_eq!(v.reasoning_parser.as_deref(), Some("qwen3"));
582 assert_eq!(v.tool_call_parser.as_deref(), Some("qwen3_xml"));
583 assert_eq!(merged.tuning.as_ref().unwrap().top_p, Some(0.95));
584 assert_eq!(
585 merged.capability.as_ref().unwrap().thinking_default,
586 Some(true)
587 );
588 }
589
590 #[test]
591 fn resolve_applies_precedence_builtin_dropin_oneoff() {
592 let builtin = parse_card(ornith_toml(), "toml").unwrap();
593 let dropin: ModelCard =
595 toml::from_str("name = \"Ornith-1.0-35B\"\n[vllm]\ngpu_mem = 0.9").unwrap();
596 let other: ModelCard =
598 toml::from_str("name = \"Other\"\n[vllm]\nmax_model_len = 1").unwrap();
599 let one_off: ModelCard =
601 toml::from_str("name = \"Ornith-1.0-35B\"\n[vllm]\nmax_model_len = 131072").unwrap();
602 let card = resolve(builtin, &[dropin, other], Some(one_off));
603 let v = card.vllm.unwrap();
604 assert_eq!(v.gpu_mem, Some(0.9), "drop-in applied");
605 assert_eq!(v.max_model_len, Some(131072), "one-off wins over built-in");
606 assert_eq!(
607 v.reasoning_parser.as_deref(),
608 Some("qwen3"),
609 "base inherited"
610 );
611 }
612
613 #[test]
615 fn family_defaults_returns_known_family_case_insensitively() {
616 for name in ["qwen3", "QWEN3", "Qwen3"] {
617 let d = family_defaults(name).unwrap_or_else(|| panic!("{name} should be known"));
618 let v = d.vllm.unwrap();
619 assert_eq!(v.reasoning_parser.as_deref(), Some("qwen3"));
620 assert_eq!(v.tool_call_parser.as_deref(), Some("qwen3_xml"));
621 assert_eq!(v.enable_auto_tool_choice, Some(true));
622 }
623 }
624
625 #[test]
626 fn family_defaults_is_none_for_an_unknown_family() {
627 assert!(family_defaults("nemotron").is_none());
628 assert!(family_defaults("bogus").is_none());
629 }
630
631 #[test]
632 fn resolve_fills_vllm_gaps_from_family_defaults() {
633 let card: ModelCard = toml::from_str(
634 "name = \"test\"\nbackend = \"vllm\"\nfamily = \"qwen3\"\n\
635 [vllm]\nserved_name = \"test\"\nmax_model_len = 8192\n",
636 )
637 .unwrap();
638 let resolved = resolve(card, &[], None);
639 let v = resolved.vllm.unwrap();
640 assert_eq!(v.served_name.as_deref(), Some("test"));
642 assert_eq!(v.max_model_len, Some(8192));
643 assert_eq!(v.reasoning_parser.as_deref(), Some("qwen3"));
645 assert_eq!(v.tool_call_parser.as_deref(), Some("qwen3_xml"));
646 assert_eq!(v.enable_auto_tool_choice, Some(true));
647 }
648
649 #[test]
650 fn resolve_cards_own_vllm_field_wins_over_family_default() {
651 let card: ModelCard = toml::from_str(
652 "name = \"test\"\nbackend = \"vllm\"\nfamily = \"qwen3\"\n\
653 [vllm]\ntool_call_parser = \"custom_xml\"\n",
654 )
655 .unwrap();
656 let resolved = resolve(card, &[], None);
657 let v = resolved.vllm.unwrap();
658 assert_eq!(
659 v.tool_call_parser.as_deref(),
660 Some("custom_xml"),
661 "the card's own declaration overrides the family default, never the reverse"
662 );
663 assert_eq!(
664 v.reasoning_parser.as_deref(),
665 Some("qwen3"),
666 "a field the card didn't set still inherits from the family"
667 );
668 }
669
670 #[test]
671 fn resolve_with_no_family_is_unaffected_by_family_defaults() {
672 let card: ModelCard =
675 toml::from_str("name = \"test\"\nbackend = \"vllm\"\n[vllm]\nserved_name = \"test\"\n")
676 .unwrap();
677 let resolved = resolve(card, &[], None);
678 assert_eq!(resolved.vllm.unwrap().reasoning_parser, None);
679 }
680
681 #[test]
682 fn resolve_with_an_unknown_family_leaves_vllm_unchanged() {
683 let card: ModelCard = toml::from_str(
687 "name = \"test\"\nbackend = \"vllm\"\nfamily = \"bogus\"\n\
688 [vllm]\nserved_name = \"test\"\n",
689 )
690 .unwrap();
691 let resolved = resolve(card, &[], None);
692 assert_eq!(resolved.vllm.unwrap().served_name.as_deref(), Some("test"));
693 }
694
695 #[test]
696 fn validate_rejects_an_unknown_family_name() {
697 let card: ModelCard = toml::from_str(
698 "name = \"test\"\nbackend = \"vllm\"\nfamily = \"qwenn3\"\n\
699 [vllm]\nserved_name = \"test\"\n",
700 )
701 .unwrap();
702 let err = card
703 .validate()
704 .expect_err("unknown family must be rejected");
705 assert!(err.contains("qwenn3"), "{err}");
706 }
707
708 #[test]
709 fn validate_accepts_a_known_family_name() {
710 let card: ModelCard = toml::from_str(
711 "name = \"test\"\nbackend = \"vllm\"\nfamily = \"qwen3\"\n\
712 [vllm]\nserved_name = \"test\"\n",
713 )
714 .unwrap();
715 assert!(card.validate().is_ok());
716 }
717
718 #[test]
720 fn validate_accepts_a_well_formed_card() {
721 assert!(parse_card(ornith_toml(), "toml")
722 .unwrap()
723 .validate()
724 .is_ok());
725 }
726
727 #[test]
728 fn validate_rejects_empty_name_missing_backend_and_missing_block() {
729 let mut c = parse_card(ornith_toml(), "toml").unwrap();
730 c.name = " ".into();
731 assert!(c.validate().is_err(), "empty name");
732
733 let no_backend: ModelCard = toml::from_str("name = \"X\"").unwrap();
734 assert!(no_backend.validate().is_err(), "no backend");
735
736 let vllm_no_block: ModelCard = toml::from_str("name = \"X\"\nbackend = \"vllm\"").unwrap();
737 assert!(vllm_no_block.validate().is_err(), "backend=vllm, no [vllm]");
738
739 let ollama_no_block: ModelCard =
740 toml::from_str("name = \"X\"\nbackend = \"ollama\"").unwrap();
741 assert!(
742 ollama_no_block.validate().is_err(),
743 "backend=ollama, no [ollama]"
744 );
745 }
746
747 #[test]
749 fn no_hardware_leak_flags_a_private_ip_and_passes_clean_cards() {
750 let clean = parse_card(ornith_toml(), "toml").unwrap();
752 assert!(
753 no_hardware_leak(&clean).is_empty(),
754 "identities-only card is clean"
755 );
756
757 let ip = format!("{}.{}.{}.{}", 192, 168, 1, 100);
761 let leaky: ModelCard = toml::from_str(&format!(
762 "name = \"X\"\nbackend = \"vllm\"\n[vllm]\nserved_name = \"http://{ip}:8000\""
763 ))
764 .unwrap();
765 assert_eq!(no_hardware_leak(&leaky), vec![ip]);
766 }
767
768 #[test]
769 fn is_private_ipv4_ranges() {
770 for o in [
773 [10, 0, 0, 1],
774 [172, 16, 0, 1],
775 [172, 31, 255, 255],
776 [192, 168, 1, 1],
777 [100, 64, 0, 1],
778 ] {
779 let ip = format!("{}.{}.{}.{}", o[0], o[1], o[2], o[3]);
780 assert!(is_private_ipv4(&ip), "{ip} is private");
781 }
782 for ip in [
783 "8.8.8.8",
784 "172.15.0.1",
785 "172.32.0.1",
786 "1.2.3",
787 "256.1.1.1",
788 "01.2.3.4",
789 ] {
790 assert!(!is_private_ipv4(ip), "{ip} is NOT flagged");
791 }
792 }
793
794 #[test]
796 fn builtin_cards_parse_validate_and_are_leak_free() {
797 let cards = builtin_cards();
798 assert!(!cards.is_empty(), "built-in cards present");
799 for c in &cards {
800 c.validate()
801 .unwrap_or_else(|e| panic!("built-in `{}` invalid: {e}", c.name));
802 assert!(
803 no_hardware_leak(c).is_empty(),
804 "built-in `{}` leaks a private IP",
805 c.name
806 );
807 if let Some(v) = c.vllm.as_ref() {
809 assert!(v.gpu_mem.is_none(), "{}: gpu_mem is node-specific", c.name);
810 assert!(
811 v.tensor_parallel.is_none(),
812 "{}: tensor_parallel is node-specific",
813 c.name
814 );
815 }
816 }
817 }
818
819 #[test]
820 fn ornith_35b_card_has_the_expected_settings() {
821 let raw = builtin_card("Ornith-1.0-35B").expect("Ornith-1.0-35B present");
828 let c = resolve(raw, &[], None);
829 assert_eq!(c.backend, Some(Backend::Vllm));
830 assert_eq!(c.gated, None, "the 35B is runnable, not gated");
831 let v = c.vllm.unwrap();
832 assert_eq!(v.max_model_len, Some(262_144));
833 assert_eq!(v.reasoning_parser.as_deref(), Some("qwen3"));
834 assert_eq!(v.tool_call_parser.as_deref(), Some("qwen3_xml"));
835 assert_eq!(v.enable_auto_tool_choice, Some(true));
836 assert!(v.extra.iter().any(|a| a == "--enable-prefix-caching"));
837 assert!(v.extra.iter().any(|a| a == "--trust-remote-code"));
838 let t = c.tuning.unwrap();
839 assert_eq!(t.temperature, Some(0.6));
840 assert_eq!(t.top_p, Some(0.95));
841 assert_eq!(t.top_k, Some(20));
842 assert_eq!(t.context_tokens, Some(262_144));
843 let cap = c.capability.unwrap();
844 assert_eq!(cap.emits_leading_reasoning, Some(true));
845 assert_eq!(cap.thinking_default, Some(true));
846 assert_eq!(
847 cap.reasoning_content_field.as_deref(),
848 Some("reasoning_content")
849 );
850 let o = c.ollama.unwrap();
851 assert_eq!(o.tag.as_deref(), Some("ornith:35b"));
852 assert!(
853 o.num_ctx.unwrap() < 262_144,
854 "ollama num_ctx is capped below the native window (OOM guard)"
855 );
856 }
857
858 #[test]
859 fn ornith_397b_is_gated() {
860 let c = builtin_card("Ornith-1.0-397B").expect("397B present as a gated card");
861 assert_eq!(c.gated, Some(true), "the 397B must be hardware-gated");
862 }
863
864 #[test]
865 fn builtin_card_is_overridable_by_name() {
866 let base = builtin_card("Ornith-1.0-35B").unwrap();
868 let dropin: ModelCard =
869 toml::from_str("name = \"Ornith-1.0-35B\"\n[ollama]\nnum_ctx = 65536").unwrap();
870 let resolved = resolve(base, std::slice::from_ref(&dropin), None);
871 assert_eq!(
872 resolved.ollama.unwrap().num_ctx,
873 Some(65536),
874 "drop-in wins"
875 );
876 assert_eq!(
877 resolved.vllm.unwrap().reasoning_parser.as_deref(),
878 Some("qwen3"),
879 "un-set fields inherit from the built-in"
880 );
881 }
882}