Skip to main content

mesh_llm_config/
lib.rs

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}