1mod authoring;
2mod diagnostic;
3mod hardware_validation;
4mod model;
5mod model_validation;
6mod plugin_validation;
7mod store;
8mod validate;
9mod validation_support;
10
11#[cfg(test)]
12mod validate_schema_contract;
13
14pub use authoring::{
15 ConfigEditor, ConfigSchemaBuilder, ConfigSettingSchemaBuilder, LocalServingNodeConfig,
16 ModelConfigEditor, ModelDefaultsEditor, PluginConfigEditor, built_in_config_schema,
17};
18pub use model::*;
19pub use plugin_validation::control_behavior::{
20 PluginConditionOperator, PluginConditionValue, PluginConditionalDisable, PluginConflictRule,
21 PluginControlAvailability, PluginControlAvailabilitySource, PluginControlBehavior,
22 PluginControlCondition, PluginDisabledWritePolicy, PluginNumericControl, PluginOptionsSource,
23 PluginTextFormat,
24};
25pub use plugin_validation::{
26 PluginConfigSchema, PluginObjectPropertySchema, PluginSchemaAvailability,
27 PluginSettingConstraint, PluginSettingSchema, PluginValueKind, PluginValueSchema,
28 SUPPORTED_PLUGIN_CONFIG_SCHEMA_VERSION,
29};
30pub use store::{ConfigStore, config_path, config_to_toml, load_config, parse_config_toml};
31pub use validate::{
32 ConfigDiagnostic, ConfigDiagnosticCode, ConfigDiagnosticSchemaSource, ConfigDiagnosticSeverity,
33 ConfigDiagnosticSource, alias_diagnostic, built_in_support_diagnostic,
34 canonical_builtin_diagnostic_path, invalid_value_diagnostic, legacy_validation_error_text,
35 rejected_field_diagnostic, unsupported_field_diagnostic, validate_config,
36 validate_config_diagnostics, validate_config_diagnostics_with_plugin_schemas,
37 validate_config_with_plugin_schemas,
38};
39
40#[cfg(test)]
41mod tests {
42 use super::{
43 ConfigStore, GpuAssignment, LocalServingNodeConfig, MeshConfig, ModelRuntimeKind,
44 SpeculativeConfig, built_in_config_schema, canonicalize_built_in_config_identifier,
45 parse_config_toml, validate_config,
46 };
47 use std::collections::{BTreeMap, BTreeSet};
48 use std::fs;
49 use tempfile::TempDir;
50
51 #[test]
52 fn config_store_loads_missing_file_as_default() {
53 let temp_dir = TempDir::new().unwrap();
54 let store = ConfigStore::open(temp_dir.path().join("config.toml"));
55
56 let config = store.load().unwrap();
57
58 assert!(config.models.is_empty());
59 }
60
61 #[test]
62 fn speculative_config_precedence_keeps_lower_layer_fields() {
63 let defaults = SpeculativeConfig {
64 strategy: Some("mtp-cache".to_string()),
65 verify_window_pipeline_depth: Some(2),
66 ..Default::default()
67 };
68 let model = SpeculativeConfig {
69 ngram_max_proposal_tokens: Some(6),
70 ..Default::default()
71 };
72 let overrides = SpeculativeConfig {
73 strategy: Some("mtp".to_string()),
74 verify_window_pipeline_depth: Some(3),
75 ..Default::default()
76 };
77
78 let resolved =
79 SpeculativeConfig::with_precedence(Some(&overrides), Some(&model), Some(&defaults));
80
81 assert_eq!(resolved.strategy.as_deref(), Some("mtp"));
82 assert_eq!(resolved.ngram_max_proposal_tokens, Some(6));
83 assert_eq!(resolved.verify_window_pipeline_depth, Some(3));
84 }
85
86 #[test]
87 fn plugin_startup_config_round_trips_from_toml() {
88 let config: MeshConfig = toml::from_str(
89 r#"
90version = 1
91
92[[plugin]]
93name = "metrics"
94command = "mesh-llm-plugin-metrics"
95
96[plugin.startup]
97connect_timeout_secs = 75
98init_timeout_secs = 90
99optional = true
100lazy_start = true
101"#,
102 )
103 .expect("plugin startup config should parse");
104
105 let startup = &config.plugins[0].startup;
106 assert_eq!(startup.connect_timeout_secs, Some(75));
107 assert_eq!(startup.init_timeout_secs, Some(90));
108 assert!(startup.optional);
109 assert!(startup.lazy_start);
110 validate_config(&config).expect("positive startup timeouts should validate");
111 }
112
113 #[test]
114 fn plugin_startup_config_rejects_zero_timeouts() {
115 let config: MeshConfig = toml::from_str(
116 r#"
117version = 1
118
119[[plugin]]
120name = "metrics"
121command = "mesh-llm-plugin-metrics"
122
123[plugin.startup]
124connect_timeout_secs = 0
125"#,
126 )
127 .expect("plugin startup config should parse before validation");
128
129 let err = validate_config(&config).expect_err("zero connect timeout must be rejected");
130
131 assert!(
132 err.to_string()
133 .contains("plugin[0].startup.connect_timeout_secs must be at least 1"),
134 "unexpected validation error: {err}"
135 );
136 }
137
138 #[test]
139 fn native_runtime_override_accepts_mesh_version_with_optional_abi_and_selection() {
140 let config = parse_config_toml(
141 r#"
142[runtime.native_runtime]
143mesh_version = "0.68.0"
144skippy_abi = "0.1.25"
145selection = "exact:meshllm-native-runtime-linux-x86_64-cuda12"
146"#,
147 )
148 .expect("native runtime selector should parse");
149
150 assert_eq!(
151 config.runtime.native_runtime.mesh_version.as_deref(),
152 Some("0.68.0")
153 );
154 assert_eq!(
155 config.runtime.native_runtime.skippy_abi.as_deref(),
156 Some("0.1.25")
157 );
158 assert_eq!(
159 config.runtime.native_runtime.selection.as_deref(),
160 Some("exact:meshllm-native-runtime-linux-x86_64-cuda12")
161 );
162
163 parse_config_toml(
164 r#"
165[runtime.native_runtime]
166mesh_version = "0.68.0"
167"#,
168 )
169 .expect("mesh-version-only native runtime selector should parse");
170
171 let err = parse_config_toml(
172 r#"
173[runtime.native_runtime]
174selection = "cuda12"
175"#,
176 )
177 .expect_err("partial native runtime selector should fail validation");
178
179 assert!(
180 err.to_string().contains(
181 "runtime.native_runtime override must set mesh_version when skippy_abi or selection is set"
182 ),
183 "unexpected validation error: {err}"
184 );
185 }
186
187 #[test]
188 fn config_store_add_model_preserves_existing_fields() {
189 let temp_dir = TempDir::new().unwrap();
190 let path = temp_dir.path().join("config.toml");
191 fs::write(
192 &path,
193 r#"
194version = 1
195
196[defaults.model_fit]
197ctx_size = 8192
198
199[[models]]
200model = "Qwen3-4B-Q4_K_M"
201ctx_size = 4096
202"#,
203 )
204 .unwrap();
205 let store = ConfigStore::open(&path);
206
207 let models = store.add_model_ref(" org/model-GGUF:Q5_K_M ").unwrap();
208
209 assert_eq!(
210 models,
211 vec![
212 "Qwen3-4B-Q4_K_M".to_string(),
213 "org/model-GGUF:Q5_K_M".to_string()
214 ]
215 );
216 let raw = fs::read_to_string(&path).unwrap();
217 assert!(raw.contains("[defaults.model_fit]"));
218 assert!(raw.contains("ctx_size = 4096"));
219 assert_eq!(raw.matches("org/model-GGUF:Q5_K_M").count(), 1);
220 }
221
222 #[test]
223 fn config_store_save_validates_before_writing() {
224 let temp_dir = TempDir::new().unwrap();
225 let path = temp_dir.path().join("config.toml");
226 let store = ConfigStore::open(&path);
227 let config = MeshConfig {
228 version: Some(2),
229 ..MeshConfig::default()
230 };
231
232 let err = store.save(&config).unwrap_err().to_string();
233
234 assert!(err.contains("unsupported config version"));
235 assert!(!path.exists());
236 }
237
238 #[test]
239 fn config_store_update_writes_local_serving_node_without_callers_writing_toml() {
240 let temp_dir = TempDir::new().unwrap();
241 let path = temp_dir.path().join("config.toml");
242 let store = ConfigStore::open(&path);
243
244 let config = store
245 .update(|config| {
246 config.configure_local_serving_node(LocalServingNodeConfig {
247 model: "Qwen/Qwen3-8B-GGUF:Q4_K_M".into(),
248 runtime: Some(ModelRuntimeKind::Metal),
249 device: Some("metal:0".into()),
250 context_size: Some(8192),
251 parallel: Some(2),
252 owner_control_bind: Some("127.0.0.1:0".parse().unwrap()),
253 gpu_assignment: Some(GpuAssignment::Pinned),
254 ..LocalServingNodeConfig::default()
255 })?;
256 let derived_profile = {
257 let entry = config
258 .config()
259 .models
260 .iter()
261 .find(|m| m.model == "Qwen/Qwen3-8B-GGUF:Q4_K_M")
262 .expect("model entry exists after configure_local_serving_node");
263 entry.derived_profile()
264 };
265 config
266 .upsert_model("Qwen/Qwen3-8B-GGUF:Q4_K_M", derived_profile)?
267 .max_tokens(1024)
268 .temperature(0.2);
269 Ok(())
270 })
271 .unwrap();
272
273 assert_eq!(config.models.len(), 1);
274 assert_eq!(
275 config.models[0]
276 .hardware
277 .as_ref()
278 .and_then(|hardware| hardware.model_runtime),
279 Some(ModelRuntimeKind::Metal)
280 );
281 let raw = fs::read_to_string(path).unwrap();
282 assert!(raw.contains("model_runtime = \"metal\""));
283 assert!(raw.contains("ctx_size = 8192"));
284 assert!(raw.contains("temperature = 0.2"));
285 }
286
287 #[test]
288 fn config_editor_updates_plugins_without_callers_writing_toml() {
289 let temp_dir = TempDir::new().unwrap();
290 let path = temp_dir.path().join("config.toml");
291 let store = ConfigStore::open(&path);
292
293 let config = store
294 .update(|config| {
295 config.enable_builtin_plugin("telemetry")?;
296 config
297 .upsert_plugin("endpoint-plugin")?
298 .enabled(true)
299 .url("http://localhost:8000/v1");
300 config.upsert_external_plugin("custom-tool", "mesh-tool", ["--serve"])?;
301 Ok(())
302 })
303 .unwrap();
304
305 assert_eq!(config.plugins.len(), 3);
306 assert_eq!(
307 config
308 .plugins
309 .iter()
310 .find(|plugin| plugin.name == "endpoint-plugin")
311 .and_then(|plugin| plugin.url.as_deref()),
312 Some("http://localhost:8000/v1")
313 );
314 assert!(fs::read_to_string(path).unwrap().contains("[[plugin]]"));
315 }
316
317 #[test]
318 fn parse_config_toml_rejects_unknown_runtime_kind() {
319 let err = parse_config_toml(
320 r#"
321version = 1
322
323[[models]]
324model = "Qwen3-8B-Q4_K_M"
325
326[models.hardware]
327model_runtime = "bogus"
328"#,
329 )
330 .unwrap_err();
331
332 assert!(format!("{err:#}").contains("unknown variant"));
333 }
334
335 #[test]
336 fn parse_config_toml_accepts_mixed_case_runtime_kind() {
337 let config = parse_config_toml(
338 r#"
339version = 1
340
341[[models]]
342model = "Qwen3-8B-Q4_K_M"
343
344[models.hardware]
345model_runtime = "Metal"
346"#,
347 )
348 .unwrap();
349
350 assert_eq!(
351 config.models[0]
352 .hardware
353 .as_ref()
354 .and_then(|hardware| hardware.model_runtime),
355 Some(ModelRuntimeKind::Metal)
356 );
357 }
358
359 #[test]
360 fn runtime_model_target_reconciliation_deserializes_from_toml() {
361 let config = parse_config_toml(
362 r#"
363version = 1
364
365[runtime]
366debug = true
367listen_all = true
368reconcile_model_targets = true
369reconcile_model_target_demand_upgrades = true
370model_target_demand_upgrade_min_requests = 4
371model_target_demand_upgrade_max_age_secs = 900
372"#,
373 )
374 .unwrap();
375
376 assert!(config.runtime.debug);
377 assert!(config.runtime.listen_all);
378 assert!(config.runtime.reconcile_model_targets);
379 assert!(config.runtime.reconcile_model_target_demand_upgrades);
380 assert_eq!(config.runtime.model_target_demand_upgrade_min_requests, 4);
381 assert_eq!(config.runtime.model_target_demand_upgrade_max_age_secs, 900);
382 }
383
384 #[test]
385 fn nested_hardware_device_does_not_serialize_as_legacy_gpu_id() {
386 let config = parse_config_toml(
387 r#"
388version = 1
389
390[gpu]
391assignment = "pinned"
392
393[[models]]
394model = "Qwen3-8B-Q4_K_M"
395
396[models.hardware]
397device = "cuda:0"
398"#,
399 )
400 .unwrap();
401
402 let toml = super::config_to_toml(&config).unwrap();
403
404 assert!(toml.contains("device = \"cuda:0\""));
405 assert!(!toml.contains("gpu_id"));
406 parse_config_toml(&toml).unwrap();
407 }
408
409 #[test]
410 fn explicit_legacy_gpu_id_still_serializes_for_legacy_round_trip() {
411 let config = parse_config_toml(
412 r#"
413version = 1
414
415[gpu]
416assignment = "pinned"
417
418[[models]]
419model = "Qwen3-8B-Q4_K_M"
420gpu_id = "pci:0000:65:00.0"
421"#,
422 )
423 .unwrap();
424
425 let toml = super::config_to_toml(&config).unwrap();
426
427 assert!(toml.contains("gpu_id = \"pci:0000:65:00.0\""));
428 parse_config_toml(&toml).unwrap();
429 }
430
431 #[test]
432 fn built_in_schema_exhaustiveness() {
433 let schema = built_in_config_schema();
434 let canonical_paths: BTreeSet<_> = schema
435 .settings
436 .iter()
437 .map(|setting| setting.path.render())
438 .collect();
439 assert_eq!(
440 canonical_paths.len(),
441 schema.settings.len(),
442 "duplicate canonical paths in built-in schema"
443 );
444
445 assert_eq!(
446 schema.settings.len(),
447 canonical_public_field_count(),
448 "built-in schema count drifted from model-owned config leaf inventory"
449 );
450
451 for required in [
452 "version",
453 "gpu.assignment",
454 "owner_control.bind",
455 "runtime.debug",
456 "runtime.listen_all",
457 "telemetry.prompt_shape_metrics",
458 "defaults.model_fit.ctx_size",
459 "defaults.hardware.rpc_backend",
460 "models.<model-ref>.hardware.device",
461 "models.<model-ref>.throughput.sleep_idle_seconds",
462 "models.<model-ref>.request_defaults.json_schema",
463 "plugin.<plugin-name>.startup.connect_timeout_secs",
464 ] {
465 assert!(
466 canonical_paths.contains(required),
467 "missing built-in schema descriptor for {required}"
468 );
469 }
470 }
471
472 #[test]
473 fn canonical_path_aliases() {
474 let cases = [
475 ("models[0].gpu_id", "models.<model-ref>.hardware.device"),
476 (
477 "models[0].ctx_size",
478 "models.<model-ref>.model_fit.ctx_size",
479 ),
480 (
481 "models[0].parallel",
482 "models.<model-ref>.throughput.parallel",
483 ),
484 ("models[0].mmproj", "models.<model-ref>.multimodal.mmproj"),
485 ("defaults.gpu_id", "defaults.hardware.device"),
486 ("defaults.ctx_size", "defaults.model_fit.ctx_size"),
487 ("defaults.parallel", "defaults.throughput.parallel"),
488 ("defaults.mmproj", "defaults.multimodal.mmproj"),
489 (
490 "plugin[0].startup.connect_timeout_secs",
491 "plugin.<plugin-name>.startup.connect_timeout_secs",
492 ),
493 ];
494
495 for (alias, canonical) in cases {
496 assert_eq!(
497 canonicalize_built_in_config_identifier(alias).as_deref(),
498 Some(canonical),
499 "alias `{alias}` should resolve to canonical `{canonical}`"
500 );
501 }
502 }
503
504 #[test]
505 fn authoring_mutators_remain_schema_classified() {
506 let canonical_paths: BTreeSet<_> = built_in_config_schema()
507 .settings
508 .into_iter()
509 .map(|setting| setting.path.render())
510 .collect();
511 let tracked = BTreeMap::from([
512 ("ConfigEditor::set_version", vec!["version"]),
513 ("ConfigEditor::set_gpu_assignment", vec!["gpu.assignment"]),
514 ("ConfigEditor::set_gpu_parallel", vec!["gpu.parallel"]),
515 (
516 "ConfigEditor::set_owner_control_bind",
517 vec!["owner_control.bind"],
518 ),
519 (
520 "ConfigEditor::set_owner_control_advertise_addr",
521 vec!["owner_control.advertise_addr"],
522 ),
523 (
524 "ConfigEditor::set_default_runtime",
525 vec!["defaults.hardware.model_runtime"],
526 ),
527 (
528 "ConfigEditor::clear_default_runtime",
529 vec!["defaults.hardware.model_runtime"],
530 ),
531 (
532 "ConfigEditor::set_default_device",
533 vec!["defaults.hardware.device"],
534 ),
535 (
536 "ConfigEditor::clear_default_device",
537 vec!["defaults.hardware.device"],
538 ),
539 (
540 "ConfigEditor::set_default_context_size",
541 vec!["defaults.model_fit.ctx_size"],
542 ),
543 (
544 "ConfigEditor::configure_local_serving_node",
545 vec![
546 "version",
547 "gpu.assignment",
548 "owner_control.bind",
549 "owner_control.advertise_addr",
550 "models.<model-ref>.hardware.model_runtime",
551 "models.<model-ref>.hardware.device",
552 "models.<model-ref>.model_fit.ctx_size",
553 "models.<model-ref>.throughput.parallel",
554 "models.<model-ref>.multimodal.mmproj",
555 ],
556 ),
557 (
558 "ConfigEditor::enable_builtin_plugin",
559 vec!["plugin.<plugin-name>.enabled"],
560 ),
561 (
562 "ConfigEditor::disable_plugin",
563 vec!["plugin.<plugin-name>.enabled"],
564 ),
565 (
566 "ConfigEditor::upsert_external_plugin",
567 vec![
568 "plugin.<plugin-name>.enabled",
569 "plugin.<plugin-name>.command",
570 "plugin.<plugin-name>.args",
571 ],
572 ),
573 (
574 "ModelDefaultsEditor::runtime",
575 vec!["defaults.hardware.model_runtime"],
576 ),
577 (
578 "ModelDefaultsEditor::clear_runtime",
579 vec!["defaults.hardware.model_runtime"],
580 ),
581 (
582 "ModelDefaultsEditor::device",
583 vec!["defaults.hardware.device"],
584 ),
585 (
586 "ModelDefaultsEditor::clear_device",
587 vec!["defaults.hardware.device"],
588 ),
589 (
590 "ModelDefaultsEditor::context_size",
591 vec!["defaults.model_fit.ctx_size"],
592 ),
593 (
594 "ModelDefaultsEditor::parallel",
595 vec!["defaults.throughput.parallel"],
596 ),
597 (
598 "ModelConfigEditor::runtime",
599 vec!["models.<model-ref>.hardware.model_runtime"],
600 ),
601 (
602 "ModelConfigEditor::clear_runtime",
603 vec!["models.<model-ref>.hardware.model_runtime"],
604 ),
605 (
606 "ModelConfigEditor::device",
607 vec!["models.<model-ref>.hardware.device"],
608 ),
609 (
610 "ModelConfigEditor::clear_device",
611 vec!["models.<model-ref>.hardware.device"],
612 ),
613 (
614 "ModelConfigEditor::context_size",
615 vec!["models.<model-ref>.model_fit.ctx_size"],
616 ),
617 (
618 "ModelConfigEditor::parallel",
619 vec!["models.<model-ref>.throughput.parallel"],
620 ),
621 (
622 "ModelConfigEditor::cache_types",
623 vec![
624 "models.<model-ref>.model_fit.cache_type_k",
625 "models.<model-ref>.model_fit.cache_type_v",
626 ],
627 ),
628 (
629 "ModelConfigEditor::max_tokens",
630 vec!["models.<model-ref>.request_defaults.max_tokens"],
631 ),
632 (
633 "ModelConfigEditor::temperature",
634 vec!["models.<model-ref>.request_defaults.temperature"],
635 ),
636 (
637 "ModelConfigEditor::mmproj",
638 vec!["models.<model-ref>.multimodal.mmproj"],
639 ),
640 (
641 "PluginConfigEditor::enabled",
642 vec!["plugin.<plugin-name>.enabled"],
643 ),
644 (
645 "PluginConfigEditor::web_ui_enabled",
646 vec!["plugin.<plugin-name>.web_ui_enabled"],
647 ),
648 (
649 "PluginConfigEditor::command",
650 vec!["plugin.<plugin-name>.command"],
651 ),
652 (
653 "PluginConfigEditor::args",
654 vec!["plugin.<plugin-name>.args"],
655 ),
656 ("PluginConfigEditor::url", vec!["plugin.<plugin-name>.url"]),
657 (
658 "PluginConfigEditor::connect_timeout_secs",
659 vec!["plugin.<plugin-name>.startup.connect_timeout_secs"],
660 ),
661 (
662 "PluginConfigEditor::init_timeout_secs",
663 vec!["plugin.<plugin-name>.startup.init_timeout_secs"],
664 ),
665 (
666 "PluginConfigEditor::optional",
667 vec!["plugin.<plugin-name>.startup.optional"],
668 ),
669 (
670 "PluginConfigEditor::lazy_start",
671 vec!["plugin.<plugin-name>.startup.lazy_start"],
672 ),
673 ]);
674 let ignored = BTreeSet::from([
675 "ConfigEditor::new",
676 "ConfigEditor::into_config",
677 "ConfigEditor::config",
678 "ConfigEditor::defaults",
679 "ConfigEditor::upsert_model",
680 "ConfigEditor::remove_model",
681 "ConfigEditor::model_refs",
682 "ConfigEditor::upsert_plugin",
683 "ModelConfigEditor::model_ref",
684 "ModelConfigEditor::derived_profile",
685 "PluginConfigEditor::name",
686 ]);
687 let actual = authoring_public_methods();
688 let expected = tracked
689 .keys()
690 .map(|name| (*name).to_string())
691 .chain(ignored.iter().map(|name| (*name).to_string()))
692 .collect::<BTreeSet<_>>();
693
694 assert_eq!(
695 actual, expected,
696 "authoring public method inventory drifted; classify new mutators against the schema registry"
697 );
698
699 for (method, paths) in tracked {
700 for path in paths {
701 assert!(
702 canonical_paths.contains(path),
703 "authoring method {method} references unclassified canonical path {path}"
704 );
705 }
706 }
707 }
708
709 fn canonical_public_field_count() -> usize {
710 let source_model = include_str!("model.rs");
711 let source_runtime = include_str!("model/runtime.rs");
712 let sources = [source_model, source_runtime];
713 let occurrences = [
714 ("MeshConfig", 1usize),
715 ("OwnerControlConfig", 1),
716 ("GpuConfig", 1),
717 ("RuntimeConfig", 1),
718 ("NativeRuntimeConfig", 1),
719 ("MeshRequirementsConfig", 1),
720 ("ModelConfigEntry", 1),
721 ("ModelFitConfig", 2),
722 ("PrefixCacheConfig", 2),
723 ("HardwareConfig", 2),
724 ("ThroughputConfig", 2),
725 ("SkippyConfig", 2),
726 ("SpeculativeConfig", 2),
727 ("RequestDefaultsConfig", 2),
728 ("MultimodalConfig", 2),
729 ("AdvancedServerConfig", 2),
730 ("TelemetryConfig", 1),
731 ("TelemetryMetricsConfig", 1),
732 ("PluginConfigEntry", 1),
733 ("PluginStartupConfig", 1),
734 ("RuntimeActivityConfig", 1),
735 ];
736 let nested = [
737 "GpuConfig",
738 "MeshRequirementsConfig",
739 "OwnerControlConfig",
740 "RuntimeConfig",
741 "NativeRuntimeConfig",
742 "TelemetryConfig",
743 "TelemetryMetricsConfig",
744 "ModelConfigDefaults",
745 "ModelConfigEntry",
746 "ModelFitConfig",
747 "PrefixCacheConfig",
748 "HardwareConfig",
749 "ThroughputConfig",
750 "SkippyConfig",
751 "SpeculativeConfig",
752 "RequestDefaultsConfig",
753 "MultimodalConfig",
754 "AdvancedConfig",
755 "AdvancedServerConfig",
756 "PluginConfigEntry",
757 "PluginStartupConfig",
758 "RuntimeActivityConfig",
759 ];
760 let ignored = [
761 "extra",
762 "gpu_id_from_legacy_shim",
763 "models",
764 "plugins",
765 "settings",
766 ];
767
768 let mut total = 0usize;
769 for (name, multiplier) in occurrences.iter() {
770 let leafs = extract_struct_fields(&sources, name)
771 .into_iter()
772 .filter(|(field, ty)| {
773 !ignored.contains(&field.as_str())
774 && !is_legacy_flat_model_field(name, field)
775 && !nested
776 .iter()
777 .any(|nested_ty| contains_nested_type(ty, nested_ty))
778 })
779 .count();
780 let contribution = leafs * multiplier;
781 total += contribution;
782 }
783 total
784 }
785
786 fn extract_struct_fields(sources: &[&str], struct_name: &str) -> Vec<(String, String)> {
787 let marker = format!("pub struct {struct_name} {{");
788 let source = sources
789 .iter()
790 .find(|s| s.contains(&marker))
791 .unwrap_or_else(|| panic!("struct {} not found in config model sources", struct_name));
792 let start = source
793 .find(&marker)
794 .expect("marker was just confirmed present");
795 let body = &source[start + marker.len()..];
796 let end = body.find("\n}").expect("struct body terminator");
797
798 body[..end]
799 .lines()
800 .filter_map(|line| {
801 let line = line.trim();
802 line.strip_prefix("pub ")
803 .and_then(|line| line.split_once(':'))
804 .map(|(field, ty)| {
805 (
806 field.trim().to_string(),
807 ty.trim().trim_end_matches(',').to_string(),
808 )
809 })
810 })
811 .collect()
812 }
813
814 fn contains_nested_type(type_name: &str, nested: &str) -> bool {
815 if type_name == nested {
816 return true;
817 }
818 let option = format!("Option<{nested}>");
819 let vec = format!("Vec<{nested}>");
820 if type_name == option || type_name == vec {
821 return true;
822 }
823 if type_name.ends_with(&format!("::{nested}")) {
824 return true;
825 }
826 if (type_name.starts_with("Option<") || type_name.starts_with("Vec<"))
827 && type_name.ends_with(&format!("::{nested}>"))
828 {
829 return true;
830 }
831 false
832 }
833
834 fn authoring_public_methods() -> BTreeSet<String> {
835 let source = include_str!("authoring.rs");
836 let mut methods = BTreeSet::new();
837
838 for (impl_name, marker) in [
839 ("ConfigEditor", "impl ConfigEditor {"),
840 ("ModelDefaultsEditor", "impl ModelDefaultsEditor<'_> {"),
841 ("ModelConfigEditor", "impl ModelConfigEditor<'_> {"),
842 ("PluginConfigEditor", "impl PluginConfigEditor<'_> {"),
843 ] {
844 let body = impl_body(source, marker);
845 for line in body.lines() {
846 let line = line.trim_start();
847 if let Some(signature) = line.strip_prefix("pub fn ") {
848 let name = signature
849 .split_once('(')
850 .map(|(name, _)| name)
851 .expect("public function signature should contain '('");
852 methods.insert(format!("{impl_name}::{name}"));
853 }
854 }
855 }
856
857 methods
858 }
859
860 fn impl_body<'a>(source: &'a str, marker: &str) -> &'a str {
861 let start = source
862 .find(marker)
863 .unwrap_or_else(|| panic!("impl marker `{marker}` not found in authoring.rs"));
864 let body_start = start + marker.len();
865 let mut depth = 1usize;
866
867 for (offset, ch) in source[body_start..].char_indices() {
868 match ch {
869 '{' => depth += 1,
870 '}' => {
871 depth -= 1;
872 if depth == 0 {
873 return &source[body_start..body_start + offset];
874 }
875 }
876 _ => {}
877 }
878 }
879
880 panic!("impl marker `{marker}` did not terminate");
881 }
882
883 fn is_legacy_flat_model_field(struct_name: &str, field: &str) -> bool {
884 struct_name == "ModelConfigEntry"
885 && matches!(
886 field,
887 "mmproj"
888 | "ctx_size"
889 | "gpu_id"
890 | "parallel"
891 | "cache_type_k"
892 | "cache_type_v"
893 | "batch"
894 | "ubatch"
895 | "flash_attention"
896 )
897 }
898}