Entropic 2.9.4
Local-first agentic inference engine
Loading...
Searching...
No Matches
loader.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
11#include "yaml_util.h"
12
13#include <nlohmann/json.hpp>
14
15#include <fstream>
16#include <optional>
17#include <sstream>
18
19#include <dlfcn.h>
20
21static auto s_log = entropic::log::get("config");
22
23namespace entropic::config {
24
35 ryml::ConstNodeRef node, ModelConfig& config)
36{
37 extract(node, "cache_type_k", config.cache_type_k);
38 extract(node, "cache_type_v", config.cache_type_v);
39 extract(node, "n_batch", config.n_batch);
40 extract(node, "n_ubatch", config.n_ubatch); // gh#23 v2.3.17
41 extract(node, "n_threads", config.n_threads);
42 extract(node, "tensor_split", config.tensor_split);
43 extract(node, "split_mode", config.split_mode); // gh#23 v2.3.18
44 extract(node, "main_gpu", config.main_gpu); // gh#23 v2.3.19
45 extract(node, "offload_kqv", config.offload_kqv); // gh#23 v2.3.20
46 extract(node, "rope_freq_base", config.rope_freq_base); // gh#23 v2.3.21
47 extract(node, "rope_freq_scale", config.rope_freq_scale); // gh#23 v2.3.22
48 extract(node, "n_parallel", config.n_parallel); // gh#23 v2.3.23
49 extract(node, "flash_attn", config.flash_attn);
50}
51
67 ryml::ConstNodeRef node, TierConfig& config)
68{
69 float f = 0.0f;
70 int i = 0;
71 bool b = false;
72 std::string s;
73 if (extract(node, "temperature", f)) { config.temperature = f; }
74 if (extract(node, "top_p", f)) { config.top_p = f; }
75 if (extract(node, "min_p", f)) { config.min_p = f; }
76 if (extract(node, "presence_penalty", f)) { config.presence_penalty = f; }
77 if (extract(node, "frequency_penalty", f)) { config.frequency_penalty = f; }
78 if (extract(node, "repeat_penalty", f)) { config.repeat_penalty = f; }
79 if (extract(node, "top_k", i)) { config.top_k = i; }
80 if (extract(node, "max_output_tokens", i)) { config.max_output_tokens = i; }
81 if (extract(node, "enable_thinking", b)) { config.enable_thinking = b; }
82 if (extract(node, "tool_call_mode", s)) { config.tool_call_mode = s; } // gh#103
83}
84
98 ryml::ConstNodeRef node, TierConfig& config)
99{
100 if (!node.has_child("speculative")) { return; }
101 auto spec = node["speculative"];
102 bool mtp = false;
103 if (extract(spec, "mtp", mtp)) { config.speculative_mtp = mtp; }
104}
105
118static std::string resolve_model_path(
119 ryml::ConstNodeRef node,
120 const BundledModels& registry,
121 ModelConfig& config)
122{
123 std::string path_str;
124 if (extract(node, "path", path_str)) {
125 config.path = registry.resolve(path_str);
126 return "";
127 }
128
129 std::string family;
130 std::string size;
131 std::string quant;
132 const bool hf = extract(node, "family", family);
133 const bool hs = extract(node, "size", size);
134 const bool hq = extract(node, "quant", quant);
135 if (!hf && !hs && !hq) { return ""; } // no path, no selector
136
137 // Single-exit accumulator (knots returns gate ≤ 3).
138 std::string err;
139 if (!(hf && hs && hq)) {
140 err = "model selector requires all of family/size/quant "
141 "(or use path:)";
142 } else if (const std::string key = registry.find_by(family, size, quant);
143 key.empty()) {
144 err = "no bundled model matches family=" + family
145 + " size=" + size + " quant=" + quant;
146 } else {
147 config.path = registry.resolve(key);
148 }
149 return err;
150}
151
161static std::string parse_model_config(
162 ryml::ConstNodeRef node,
163 const BundledModels& registry,
164 ModelConfig& config)
165{
166 const std::string err = resolve_model_path(node, registry, config);
167 if (!err.empty()) { return err; }
168
169 extract(node, "adapter", config.adapter);
170 extract(node, "context_length", config.context_length);
171 extract(node, "gpu_layers", config.gpu_layers);
172 extract(node, "keep_warm", config.keep_warm);
173 extract(node, "use_mlock", config.use_mlock);
174 extract(node, "reasoning_budget", config.reasoning_budget);
175 parse_model_runtime_knobs(node, config);
176 extract_string_list_opt(node, "allowed_tools", config.allowed_tools);
177
178 /* v1.9.11 mmproj wiring at the loader (gh#42, gh#41): read mmproj
179 * here so the YAML key matches the bundled_models.yaml registry
180 * shape and the orchestrator can declare a tier vision-capable. */
181 std::string mmproj_str;
182 if (extract(node, "mmproj", mmproj_str)) {
183 config.mmproj_path = registry.resolve(mmproj_str);
184 }
185
186 return "";
187}
188
204static std::string parse_tier_config(
205 ryml::ConstNodeRef node,
206 const BundledModels& registry,
207 TierConfig& config)
208{
209 auto err = parse_model_config(node, registry, config);
210 if (!err.empty()) {
211 return err;
212 }
213
214 // gh#94 (v2.7.3): per-tier sampler overrides live on TierConfig.
215 parse_sampler_overrides(node, config);
216
217 // gh#108 (v2.9.4): per-tier speculative.mtp override.
219
220 extract_tri_state_path(node, "identity",
221 config.identity, config.identity_disabled);
222
223 std::string grammar_str;
224 if (extract(node, "grammar", grammar_str)) {
225 config.grammar = expand_home(std::filesystem::path(grammar_str));
226 }
227
228 // gh#108 (v2.9.4): a tier that statically opts into MTP while also
229 // statically configuring a grammar would fail every request (MTP does
230 // not enforce GBNF) — catch that at load time instead of per-call.
231 if (config.speculative_mtp && *config.speculative_mtp
232 && config.grammar.has_value()) {
233 return "speculative.mtp=true is incompatible with a static grammar "
234 "on this tier (MTP does not enforce GBNF constraints); "
235 "remove speculative.mtp or the grammar for this tier";
236 }
237
238 std::string auto_chain_str;
239 if (extract(node, "auto_chain", auto_chain_str)) {
240 config.auto_chain = auto_chain_str;
241 }
242
243 bool routable_val = false;
244 if (extract(node, "routable", routable_val)) {
245 config.routable = routable_val;
246 }
247
248 /* gh#41 v2.1.8: tier capabilities. Missing key → ["text"] so
249 * every pre-v2.1.8 tier config remains valid. Configs that
250 * declare capabilities explicitly must include "text" themselves
251 * if the tier still serves text — we don't auto-inject. */
252 if (!extract_string_list(node, "capabilities", config.capabilities)) {
253 config.capabilities = {"text"};
254 }
255
256 return "";
257}
258
268static std::string parse_models_config(
269 ryml::ConstNodeRef node,
270 const BundledModels& registry,
271 ModelsConfig& config)
272{
273 extract(node, "default", config.default_tier);
274
275 if (node.has_child("router")) {
276 config.router.emplace();
277 auto err = parse_model_config(node["router"], registry,
278 *config.router);
279 if (!err.empty()) {
280 return "models.router: " + err;
281 }
282 }
283
284 for (auto child : node) {
285 std::string key = to_string(child.key());
286 if (key == "default" || key == "router") {
287 continue;
288 }
289 if (!child.is_map()) {
290 continue;
291 }
292
293 TierConfig tier;
294 if (config.tiers.count(key) > 0) {
295 tier = config.tiers[key];
296 }
297 auto err = parse_tier_config(child, registry, tier);
298 if (!err.empty()) {
299 return "models." + key + ": " + err;
300 }
301 config.tiers[key] = std::move(tier);
302 }
303
304 return "";
305}
306
315static std::string parse_routing_config(
316 ryml::ConstNodeRef node,
317 RoutingConfig& config)
318{
319 extract(node, "enabled", config.enabled);
320 extract(node, "fallback_tier", config.fallback_tier);
321
322 std::string class_prompt;
323 if (extract(node, "classification_prompt", class_prompt)) {
324 config.classification_prompt = class_prompt;
325 }
326
327 extract_string_map(node, "tier_map", config.tier_map);
328 extract_string_list_map(node, "handoff_rules", config.handoff_rules);
329
330 return "";
331}
332
341static std::string parse_compaction_config(
342 ryml::ConstNodeRef node,
343 CompactionConfig& config)
344{
345 extract(node, "enabled", config.enabled);
346 extract(node, "threshold_percent", config.threshold_percent);
347 extract(node, "preserve_recent_turns", config.preserve_recent_turns);
348 extract(node, "summary_max_tokens", config.summary_max_tokens);
349 extract(node, "notify_user", config.notify_user);
350 extract(node, "save_full_history", config.save_full_history);
351 extract(node, "tool_result_ttl", config.tool_result_ttl);
352 extract(node, "warning_threshold_percent",
354 return "";
355}
356
365static std::string parse_permissions_config(
366 ryml::ConstNodeRef node,
367 PermissionsConfig& config)
368{
369 extract_string_list(node, "allow", config.allow);
370 extract_string_list(node, "deny", config.deny);
371 extract(node, "auto_approve", config.auto_approve);
372 return "";
373}
374
383static std::string parse_filesystem_config(
384 ryml::ConstNodeRef node,
385 FilesystemConfig& config)
386{
387 extract(node, "diagnostics_on_edit", config.diagnostics_on_edit);
388 extract(node, "fail_on_errors", config.fail_on_errors);
389 extract(node, "diagnostics_timeout", config.diagnostics_timeout);
390 extract(node, "allow_outside_root", config.allow_outside_root);
391 extract(node, "max_read_context_pct", config.max_read_context_pct);
392
393 int max_read = 0;
394 if (extract(node, "max_read_bytes", max_read)) {
395 config.max_read_bytes = max_read;
396 }
397
398 return "";
399}
400
409static std::string parse_external_mcp_config(
410 ryml::ConstNodeRef node,
411 ExternalMCPConfig& config)
412{
413 extract(node, "enabled", config.enabled);
414 extract(node, "rate_limit", config.rate_limit);
415
416 if (node.is_map() && node.has_child("socket_path")
417 && !node["socket_path"].val_is_null()) {
418 std::filesystem::path tmp;
419 extract_path(node, "socket_path", tmp);
420 config.socket_path = tmp;
421 }
422
423 return "";
424}
425
434static std::string parse_mcp_config(
435 ryml::ConstNodeRef node,
436 MCPConfig& config)
437{
438 extract(node, "enable_entropic", config.enable_entropic);
439 extract(node, "enable_filesystem", config.enable_filesystem);
440 extract(node, "enable_bash", config.enable_bash);
441 extract(node, "enable_git", config.enable_git);
442 extract(node, "enable_diagnostics", config.enable_diagnostics);
443 extract(node, "enable_web", config.enable_web);
444 extract(node, "server_timeout_seconds", config.server_timeout_seconds);
445 extract(node, "working_dir", config.working_dir);
446
447 if (node.has_child("filesystem")) {
448 parse_filesystem_config(node["filesystem"], config.filesystem);
449 }
450 if (node.has_child("external")) {
451 parse_external_mcp_config(node["external"], config.external);
452 }
453
454 return "";
455}
456
465static std::string parse_generation_config(
466 ryml::ConstNodeRef node,
467 GenerationConfig& config)
468{
469 extract(node, "max_tokens", config.max_tokens);
470 extract(node, "default_temperature", config.default_temperature);
471 extract(node, "default_top_p", config.default_top_p);
472 // gh#80 (v2.5.0): thinking-budget knobs (validated in build_loop_config).
473 extract(node, "budget_mode", config.budget_mode);
474 extract(node, "budget_limit", config.budget_limit);
475 return "";
476}
477
486static std::string parse_lsp_config(
487 ryml::ConstNodeRef node,
488 LSPConfig& config)
489{
490 extract(node, "enabled", config.enabled);
491 extract(node, "python_enabled", config.python_enabled);
492 extract(node, "c_enabled", config.c_enabled);
493 return "";
494}
495
504static std::string parse_prompt_cache_config(
505 ryml::ConstNodeRef node,
506 PromptCacheConfig& config)
507{
508 extract(node, "enabled", config.enabled);
509 extract(node, "log_hits", config.log_hits);
510
511 int max_bytes_int = 0;
512 if (extract(node, "max_bytes", max_bytes_int)) {
513 config.max_bytes = static_cast<size_t>(max_bytes_int);
514 }
515
516 return "";
517}
518
527 ryml::ConstNodeRef node,
529{
530 extract(node, "enabled", config.enabled);
531 extract(node, "max_revisions", config.max_revisions);
532 extract(node, "max_critique_tokens", config.max_critique_tokens);
533 extract(node, "temperature", config.temperature);
534 extract(node, "enable_thinking", config.enable_thinking);
535 extract(node, "priority", config.priority);
536 extract(node, "grammar_key", config.grammar_key);
537 extract_string_list(node, "skip_tiers", config.skip_tiers);
538}
539
558 ryml::ConstNodeRef node,
559 const BundledModels& registry,
560 SpeculativeConfig& config)
561{
562 extract(node, "enabled", config.enabled);
563 extract(node, "n_draft", config.n_draft);
564 extract(node, "mtp", config.mtp); // gh#106: drive the MTP head path
565 // Nested ModelConfig — every llama.cpp knob is consumer-tunable
566 // via `inference.speculative.draft.<field>`. Defaults come from
567 // `make_default_draft_model_config()` (gpu_layers=0, flash_attn=
568 // false, context_length=8192, n_threads=4). (v2.1.11)
569 if (node.has_child("draft")) {
570 auto err = parse_model_config(
571 node["draft"], registry, config.draft);
572 if (!err.empty()) {
573 s_log->warn("inference.speculative.draft parse: {}", err);
574 }
575 }
576}
577
595 ryml::ConstNodeRef root,
596 const BundledModels& registry,
597 ParsedConfig& config)
598{
599 if (!root.has_child("inference")) { return; }
600 auto inf = root["inference"];
601 if (inf.has_child("prompt_cache"))
602 parse_prompt_cache_config(inf["prompt_cache"], config.prompt_cache);
603 if (inf.has_child("speculative"))
604 parse_speculative_config(inf["speculative"], registry,
605 config.inference.speculative);
606}
607
617 ryml::ConstNodeRef root,
618 const BundledModels& registry,
619 ParsedConfig& config)
620{
621 if (root.has_child("generation"))
622 parse_generation_config(root["generation"], config.generation);
623 if (root.has_child("permissions"))
624 parse_permissions_config(root["permissions"], config.permissions);
625 if (root.has_child("mcp"))
626 parse_mcp_config(root["mcp"], config.mcp);
627 if (root.has_child("compaction"))
628 parse_compaction_config(root["compaction"], config.compaction);
629 if (root.has_child("lsp"))
630 parse_lsp_config(root["lsp"], config.lsp);
631 parse_inference_subsections(root, registry, config);
632 if (root.has_child("constitutional_validation"))
634 root["constitutional_validation"],
636}
637
645static void extract_scalar_fields(ryml::ConstNodeRef root,
646 ParsedConfig& config)
647{
648 extract(root, "log_level", config.log_level);
649 extract(root, "inject_model_context", config.inject_model_context);
650 extract(root, "vram_reserve_mb", config.vram_reserve_mb);
651 extract_path(root, "config_dir", config.config_dir);
652 extract_path(root, "log_dir", config.log_dir);
653 extract(root, "ggml_logging", config.ggml_logging);
654 extract_path(root, "llama_log_path", config.llama_log_path); // gh#23 v2.3.24
655 extract(root, "console_logging", config.console_logging);
656
657 extract_tri_state_path(root, "constitution",
658 config.constitution, config.constitution_disabled);
659 extract_tri_state_path(root, "app_context",
660 config.app_context, config.app_context_disabled);
661}
662
671 ryml::ConstNodeRef root,
672 const BundledModels& registry,
673 ParsedConfig& config)
674{
675 parse_optional_subsections(root, registry, config);
676 extract_scalar_fields(root, config);
677}
678
688static std::string parse_top_sections(
689 ryml::ConstNodeRef root,
690 const BundledModels& registry,
691 ParsedConfig& config)
692{
693 std::string err;
694 if (root.has_child("models")) {
695 err = parse_models_config(root["models"], registry, config.models);
696 }
697 if (err.empty() && root.has_child("routing")) {
698 err = parse_routing_config(root["routing"], config.routing);
699 }
700 if (err.empty()) {
701 parse_optional_sections(root, registry, config);
702 }
703 return err;
704}
705
721 const std::filesystem::path& path,
722 const BundledModels& registry,
723 ParsedConfig& config)
724{
725 auto content = read_file(path);
726 if (content.empty()) {
727 return "cannot read config file: " + path.string();
728 }
729
730 ryml::Tree tree = ryml::parse_in_arena(
731 ryml::to_csubstr(path.string()),
732 ryml::to_csubstr(content));
733 ryml::ConstNodeRef root = tree.rootref();
734 if (!root.is_map()) {
735 return "config file root is not a YAML mapping: " + path.string();
736 }
737
738 return parse_top_sections(root, registry, config);
739}
740
750static std::string load_bundled_default(
751 const std::filesystem::path& global_path,
752 const BundledModels& registry,
753 ParsedConfig& config)
754{
755 auto data_dir = resolve_data_dir(config);
756 auto bundled = data_dir / "default_config.yaml";
757 if (!std::filesystem::exists(bundled)) {
758 s_log->warn("No config found (checked global, project, bundled)");
759 return "";
760 }
761 s_log->info("No user config — loading bundled default: {}",
762 bundled.string());
763 auto err = parse_config_file(bundled, registry, config);
764 if (!err.empty()) { return "bundled default: " + err; }
765
766 // Auto-create global config so this only happens once
767 if (!std::filesystem::exists(global_path)) {
768 auto parent = global_path.parent_path();
769 std::filesystem::create_directories(parent);
770 std::filesystem::copy_file(bundled, global_path);
771 s_log->info("Created {}", global_path.string());
772 }
773 return "";
774}
775
786static std::string load_config_layers(
787 const std::filesystem::path& global_path,
788 const std::filesystem::path& project_path,
789 const BundledModels& registry,
790 ParsedConfig& config)
791{
792 bool have_config = false;
793 std::string err;
794
795 if (std::filesystem::exists(global_path)) {
796 s_log->info("Loading global config: {}", global_path.string());
797 err = parse_config_file(global_path, registry, config);
798 if (!err.empty()) { return "global config: " + err; }
799 have_config = true;
800 }
801
802 if (err.empty() && std::filesystem::exists(project_path)) {
803 s_log->info("Loading project config: {}", project_path.string());
804 err = parse_config_file(project_path, registry, config);
805 if (!err.empty()) { return "project config: " + err; }
806 have_config = true;
807 }
808
809 return have_config ? ""
810 : load_bundled_default(global_path, registry, config);
811}
812
823std::string load_config(
824 const std::filesystem::path& global_path,
825 const std::filesystem::path& project_path,
826 const BundledModels& registry,
827 ParsedConfig& config)
828{
829 auto err = load_config_layers(global_path, project_path, registry, config);
830 if (!err.empty()) { return err; }
831
832 apply_env_overrides(config);
833
834 std::vector<std::string> warnings;
835 err = validate_config(config, warnings);
836 for (const auto& w : warnings) { s_log->warn("{}", w); }
837 return err;
838}
839
850 const std::filesystem::path& path,
851 const BundledModels& registry,
852 ParsedConfig& config)
853{
854 auto err = parse_config_file(path, registry, config);
855 if (!err.empty()) {
856 return err;
857 }
858
859 apply_env_overrides(config);
860
861 std::vector<std::string> warnings;
862 err = validate_config(config, warnings);
863 for (const auto& w : warnings) {
864 s_log->warn("{}", w);
865 }
866 return err;
867}
868
878 const std::string& name,
879 const nlohmann::json& entry) {
880 ExternalServerEntry result;
881 std::string type = entry.value("type", std::string("stdio"));
882 if (type == "sse") {
883 result.url = entry.value("url", std::string{});
884 } else {
885 result.command = entry.value("command", std::string{});
886 if (entry.contains("args") && entry["args"].is_array()) {
887 for (const auto& a : entry["args"]) {
888 result.args.push_back(a.get<std::string>());
889 }
890 }
891 if (entry.contains("env") && entry["env"].is_object()) {
892 for (auto it = entry["env"].begin();
893 it != entry["env"].end(); ++it) {
894 result.env[it.key()] = it.value().get<std::string>();
895 }
896 }
897 }
898 s_log->info("Discovered external MCP server: {} (type={})",
899 name, type);
900 return result;
901}
902
925static std::optional<nlohmann::json> read_mcp_servers(
926 const std::filesystem::path& path) {
927 std::ifstream f(path);
928 nlohmann::json j;
929 if (f.is_open()) {
930 std::stringstream ss;
931 ss << f.rdbuf();
932 j = nlohmann::json::parse(ss.str(), nullptr, false);
933 }
934 bool valid = f.is_open() && !j.is_discarded() && j.is_object()
935 && j.contains("mcpServers")
936 && j["mcpServers"].is_object();
937 if (!valid) {
938 s_log->warn(".mcp.json missing/malformed: {}", path.string());
939 return std::nullopt;
940 }
941 return j["mcpServers"];
942}
943
950 const std::filesystem::path& project_dir,
951 ParsedConfig& config) {
952 if (project_dir.empty()) { return; }
953 auto path = project_dir / ".mcp.json";
954 if (!std::filesystem::exists(path)) { return; }
955 auto servers = read_mcp_servers(path);
956 if (!servers) { return; }
957
958 int added = 0;
959 for (auto it = servers->begin(); it != servers->end(); ++it) {
960 const std::string& name = it.key();
961 if (config.mcp.external_servers.count(name)) {
962 s_log->info(".mcp.json: {} already in config, skipping", name);
963 continue;
964 }
966 name, it.value());
967 added++;
968 }
969 s_log->info("Loaded {} external MCP server(s) from {}",
970 added, path.string());
971}
972
979static std::string load_global_layer(
980 const BundledModels& registry, ParsedConfig& config)
981{
982 const char* home = getenv("HOME");
983 std::string err;
984 if (home) {
985 auto path = std::filesystem::path(home) / ".entropic" / "config.yaml";
986 if (std::filesystem::exists(path)) {
987 s_log->info("Loading global config: {}", path.string());
988 err = parse_config_file(path, registry, config);
989 if (!err.empty()) {
990 err = "global config: " + err;
991 }
992 }
993 }
994 return err;
995}
996
1015static std::filesystem::path resolve_consumer_defaults(
1016 const std::filesystem::path& consumer_defaults)
1017{
1018 // Input already usable (empty, absolute, or found at CWD) → passthrough.
1019 if (consumer_defaults.empty()
1020 || consumer_defaults.is_absolute()
1021 || std::filesystem::exists(consumer_defaults)) {
1022 return consumer_defaults;
1023 }
1024 // Fall back to <install-prefix>/share/entropic/<filename>. Uses
1025 // the same librentropic.so location trick — dladdr on any address
1026 // in this translation unit resolves to the .so's on-disk path.
1027 std::filesystem::path result = consumer_defaults;
1028 Dl_info info = {};
1029 if (dladdr(reinterpret_cast<void*>(&resolve_consumer_defaults), &info) != 0
1030 && info.dli_fname != nullptr) {
1031 std::error_code ec;
1032 auto lib_path = std::filesystem::absolute(info.dli_fname, ec);
1033 if (!ec) {
1034 auto candidate = lib_path.parent_path().parent_path()
1035 / "share" / "entropic" / consumer_defaults.filename();
1036 if (std::filesystem::exists(candidate)) {
1037 result = candidate;
1038 }
1039 }
1040 }
1041 return result;
1042}
1043
1057static bool yaml_has_key(const std::filesystem::path& path,
1058 const char* key) {
1059 auto content = read_file(path);
1060 if (content.empty()) { return false; }
1061 auto tree = ryml::parse_in_arena(
1062 ryml::to_csubstr(path.string()),
1063 ryml::to_csubstr(content));
1064 auto root = tree.rootref();
1065 return root.is_map() && root.has_child(ryml::to_csubstr(key));
1066}
1067
1083 const std::filesystem::path& consumer_defaults_in,
1084 const BundledModels& registry, ParsedConfig& config)
1085{
1086 auto consumer_defaults = resolve_consumer_defaults(consumer_defaults_in);
1087 if (consumer_defaults.empty() || !std::filesystem::exists(consumer_defaults)) {
1088 return;
1089 }
1090 s_log->info("Loading consumer defaults: {}", consumer_defaults.string());
1091
1092 // Replace semantics: if consumer defines models or routing,
1093 // clear existing tiers/routing so global ones don't leak through.
1094 if (yaml_has_key(consumer_defaults, "models")) {
1095 s_log->info("Consumer defines models: — replacing global tiers");
1096 config.models.tiers.clear();
1097 config.models.router.reset();
1098 config.models.default_tier.clear();
1099 }
1100 if (yaml_has_key(consumer_defaults, "routing")) {
1101 config.routing = RoutingConfig{};
1102 }
1103
1104 auto err = parse_config_file(consumer_defaults, registry, config);
1105 if (!err.empty()) {
1106 s_log->warn("Consumer defaults failed: {}", err);
1107 }
1108}
1109
1127static std::string load_project_layer(
1128 const std::filesystem::path& project_dir,
1129 const BundledModels& registry, ParsedConfig& config)
1130{
1131 std::string err;
1132 if (!project_dir.empty()) {
1133 auto path = project_dir / "config.local.yaml";
1134 if (std::filesystem::exists(path)) {
1135 s_log->info("Loading project config: {}", path.string());
1136 if (yaml_has_key(path, "models")) {
1137 s_log->info("Project defines models: — replacing prior tiers");
1138 config.models.tiers.clear();
1139 config.models.router.reset();
1140 config.models.default_tier.clear();
1141 }
1142 if (yaml_has_key(path, "routing")) {
1143 config.routing = RoutingConfig{};
1144 }
1145 err = parse_config_file(path, registry, config);
1146 if (!err.empty()) {
1147 err = "project config: " + err;
1148 }
1149 }
1150 }
1151 return err;
1152}
1153
1180std::string load_layered(
1181 const std::filesystem::path& project_dir,
1182 const std::filesystem::path& consumer_defaults,
1183 const BundledModels& registry,
1184 ParsedConfig& config)
1185{
1186 auto err = load_global_layer(registry, config);
1187 if (err.empty()) {
1188 load_consumer_layer(consumer_defaults, registry, config);
1189 if (!project_dir.empty() && config.log_dir.empty()) {
1190 config.log_dir = project_dir;
1191 }
1192 err = load_project_layer(project_dir, registry, config);
1193 }
1194 if (err.empty() && config.models.tiers.empty()) {
1195 err = load_bundled_default(std::filesystem::path{}, registry, config);
1196 }
1197 if (err.empty()) {
1198 apply_env_overrides(config);
1199 discover_mcp_json(project_dir, config);
1200 }
1201 return err;
1202}
1203
1217static std::string parse_config_string(
1218 const std::string& content,
1219 const BundledModels& registry,
1220 ParsedConfig& config)
1221{
1222 if (content.empty()) {
1223 return "config string is empty";
1224 }
1225
1226 ryml::Tree tree = ryml::parse_in_arena(
1227 ryml::to_csubstr("<json>"),
1228 ryml::to_csubstr(content));
1229 auto root = tree.rootref();
1230 if (!root.is_map()) {
1231 return "config string root is not a mapping";
1232 }
1233
1234 std::string err;
1235 if (root.has_child("models")) {
1236 err = parse_models_config(root["models"], registry, config.models);
1237 }
1238 if (err.empty() && root.has_child("routing")) {
1239 err = parse_routing_config(root["routing"], config.routing);
1240 }
1241 if (err.empty()) {
1242 parse_optional_sections(root, registry, config);
1243 }
1244 return err;
1245}
1246
1257 const std::string& content,
1258 const BundledModels& registry,
1259 ParsedConfig& config)
1260{
1261 auto err = parse_config_string(content, registry, config);
1262 if (!err.empty()) {
1263 return err;
1264 }
1265
1266 apply_env_overrides(config);
1267
1268 std::vector<std::string> warnings;
1269 err = validate_config(config, warnings);
1270 for (const auto& w : warnings) {
1271 s_log->warn("{}", w);
1272 }
1273 return err;
1274}
1275
1276} // namespace entropic::config
Bundled model registry loaded from bundled_models.yaml.
std::filesystem::path resolve(const std::string &value) const
Resolve a model reference to a filesystem path.
std::string find_by(const std::string &family, const std::string &size_label, const std::string &quant) const
Look up a registry key by (family, size, quant) (gh#62).
static void extract_scalar_fields(ryml::ConstNodeRef root, ParsedConfig &config)
Extract the top-level scalar/path config fields.
Definition loader.cpp:645
static std::string parse_config_string(const std::string &content, const BundledModels &registry, ParsedConfig &config)
Parse a config string (YAML or JSON) and overlay onto config.
Definition loader.cpp:1217
static std::string load_project_layer(const std::filesystem::path &project_dir, const BundledModels &registry, ParsedConfig &config)
Load the project-local layer if present.
Definition loader.cpp:1127
static std::string parse_external_mcp_config(ryml::ConstNodeRef node, ExternalMCPConfig &config)
Parse the external MCP section from a YAML node.
Definition loader.cpp:409
static bool yaml_has_key(const std::filesystem::path &path, const char *key)
Load the consumer/app defaults layer if present (non-fatal).
Definition loader.cpp:1057
static std::string load_global_layer(const BundledModels &registry, ParsedConfig &config)
Load the global user config layer if present.
Definition loader.cpp:979
static std::string parse_generation_config(ryml::ConstNodeRef node, GenerationConfig &config)
Parse the generation section from a YAML node.
Definition loader.cpp:465
static std::string parse_permissions_config(ryml::ConstNodeRef node, PermissionsConfig &config)
Parse the permissions section from a YAML node.
Definition loader.cpp:365
static void parse_inference_subsections(ryml::ConstNodeRef root, const BundledModels &registry, ParsedConfig &config)
Parse the nested optional config sub-sections.
Definition loader.cpp:594
static void load_consumer_layer(const std::filesystem::path &consumer_defaults_in, const BundledModels &registry, ParsedConfig &config)
Load the consumer-defaults layer.
Definition loader.cpp:1082
static std::filesystem::path resolve_consumer_defaults(const std::filesystem::path &consumer_defaults)
Resolve a relative consumer_defaults path against install prefix.
Definition loader.cpp:1015
static std::optional< nlohmann::json > read_mcp_servers(const std::filesystem::path &path)
Discover and parse .mcp.json from the project directory.
Definition loader.cpp:925
static void parse_optional_sections(ryml::ConstNodeRef root, const BundledModels &registry, ParsedConfig &config)
Parse optional config sections that don't return errors.
Definition loader.cpp:670
static void parse_speculative_config(ryml::ConstNodeRef node, const BundledModels &registry, SpeculativeConfig &config)
Parse inference.speculative YAML node into SpeculativeConfig.
Definition loader.cpp:557
static void parse_constitutional_validation_config(ryml::ConstNodeRef node, ConstitutionalValidationConfig &config)
Parse constitutional_validation section.
Definition loader.cpp:526
static void parse_optional_subsections(ryml::ConstNodeRef root, const BundledModels &registry, ParsedConfig &config)
Parse the nested optional config sub-sections.
Definition loader.cpp:616
static std::string parse_mcp_config(ryml::ConstNodeRef node, MCPConfig &config)
Parse the MCP section from a YAML node.
Definition loader.cpp:434
static std::string load_bundled_default(const std::filesystem::path &global_path, const BundledModels &registry, ParsedConfig &config)
Load bundled default config when no user config exists.
Definition loader.cpp:750
static void parse_tier_speculative_override(ryml::ConstNodeRef node, TierConfig &config)
Parse the per-tier speculative.mtp override (gh#108, v2.9.4).
Definition loader.cpp:97
static std::string parse_filesystem_config(ryml::ConstNodeRef node, FilesystemConfig &config)
Parse the filesystem section from a YAML node.
Definition loader.cpp:383
static std::string parse_compaction_config(ryml::ConstNodeRef node, CompactionConfig &config)
Parse the compaction section from a YAML node.
Definition loader.cpp:341
static void discover_mcp_json(const std::filesystem::path &project_dir, ParsedConfig &config)
Discover + merge external MCP servers from <dir>/.mcp.json.
Definition loader.cpp:949
static std::string parse_top_sections(ryml::ConstNodeRef root, const BundledModels &registry, ParsedConfig &config)
Parse the top-level models/routing/optional sections.
Definition loader.cpp:688
static std::string parse_routing_config(ryml::ConstNodeRef node, RoutingConfig &config)
Parse the routing section from a YAML node.
Definition loader.cpp:315
static void parse_sampler_overrides(ryml::ConstNodeRef node, TierConfig &config)
Read per-tier sampler overrides from a YAML model/tier node.
Definition loader.cpp:66
static void parse_model_runtime_knobs(ryml::ConstNodeRef node, ModelConfig &config)
Read the model-runtime knobs (KV cache, batching, threads).
Definition loader.cpp:34
static std::string parse_prompt_cache_config(ryml::ConstNodeRef node, PromptCacheConfig &config)
Parse prompt_cache config from inference YAML section.
Definition loader.cpp:504
static std::string resolve_model_path(ryml::ConstNodeRef node, const BundledModels &registry, ModelConfig &config)
Resolve config.path from an explicit path: or a selector (gh#62).
Definition loader.cpp:118
static std::string parse_model_config(ryml::ConstNodeRef node, const BundledModels &registry, ModelConfig &config)
Parse a ModelConfig from a YAML node.
Definition loader.cpp:161
static ExternalServerEntry parse_mcp_json_entry(const std::string &name, const nlohmann::json &entry)
Parse a single mcpServers entry from .mcp.json.
Definition loader.cpp:877
static std::string load_config_layers(const std::filesystem::path &global_path, const std::filesystem::path &project_path, const BundledModels &registry, ParsedConfig &config)
Load global, project, and bundled config layers in order.
Definition loader.cpp:786
static std::string parse_tier_config(ryml::ConstNodeRef node, const BundledModels &registry, TierConfig &config)
Parse a TierConfig from a YAML node.
Definition loader.cpp:204
static std::string parse_models_config(ryml::ConstNodeRef node, const BundledModels &registry, ModelsConfig &config)
Parse the models section from a YAML node.
Definition loader.cpp:268
static std::string parse_lsp_config(ryml::ConstNodeRef node, LSPConfig &config)
Parse the LSP section from a YAML node.
Definition loader.cpp:486
Config loader — YAML to C++ structs with validation.
ENTROPIC_EXPORT std::string load_config(const std::filesystem::path &global_path, const std::filesystem::path &project_path, const BundledModels &registry, ParsedConfig &config)
Load config using layered resolution.
Definition loader.cpp:823
ENTROPIC_EXPORT std::string load_config_from_string(const std::string &content, const BundledModels &registry, ParsedConfig &config)
Load config from a YAML/JSON string (no layering).
Definition loader.cpp:1256
ENTROPIC_EXPORT std::filesystem::path resolve_data_dir(const ParsedConfig &config)
Resolve the bundled data directory.
Definition data_dir.cpp:81
ENTROPIC_EXPORT std::string parse_config_file(const std::filesystem::path &path, const BundledModels &registry, ParsedConfig &config)
Parse a config YAML file and overlay onto existing config.
Definition loader.cpp:720
ENTROPIC_EXPORT void apply_env_overrides(ParsedConfig &config)
Apply ENTROPIC_* environment variable overrides.
ENTROPIC_EXPORT std::string load_config_from_file(const std::filesystem::path &path, const BundledModels &registry, ParsedConfig &config)
Load config from a single YAML file (no layering).
Definition loader.cpp:849
ENTROPIC_EXPORT std::string load_layered(const std::filesystem::path &project_dir, const std::filesystem::path &consumer_defaults, const BundledModels &registry, ParsedConfig &config)
Load config with consumer defaults + global + project layers.
Definition loader.cpp:1180
spdlog initialization and logger access.
ENTROPIC_EXPORT std::shared_ptr< spdlog::logger > get(const std::string &name)
Get or create a named logger.
Definition logging.cpp:211
Auto-compaction configuration.
Definition config.h:697
bool save_full_history
Save full history before compaction.
Definition config.h:703
bool notify_user
Notify user on compaction.
Definition config.h:702
float warning_threshold_percent
Warning trigger (0.3–0.9)
Definition config.h:705
int preserve_recent_turns
Turns to preserve (1–10)
Definition config.h:700
int summary_max_tokens
Summary max tokens (500–4000)
Definition config.h:701
int tool_result_ttl
Tool result TTL in turns (>= 1; v2.1.3 #6: gated on fill, no upper bound)
Definition config.h:704
float threshold_percent
Compaction trigger (0.5–0.99)
Definition config.h:699
bool enabled
Enable auto-compaction.
Definition config.h:698
Constitutional validation pipeline configuration.
Definition config.h:769
int max_revisions
Max re-generation attempts (0 = critique only)
Definition config.h:771
int priority
Hook priority (higher = later)
Definition config.h:775
bool enable_thinking
Enable think-blocks for critique (default OFF)
Definition config.h:774
float temperature
Critique generation temperature.
Definition config.h:773
bool enabled
Global enable/disable (default OFF)
Definition config.h:770
int max_critique_tokens
Token budget for critique generation.
Definition config.h:772
std::string grammar_key
Grammar registry key.
Definition config.h:776
std::vector< std::string > skip_tiers
Tiers exempt from validation (default: lead — streams before hook fires)
Definition config.h:778
External MCP server configuration (Entropic-as-server).
Definition config.h:612
std::optional< std::filesystem::path > socket_path
Socket path (nullopt = derived)
Definition config.h:614
int rate_limit
Requests per minute (1–100)
Definition config.h:615
bool enabled
Enable external MCP.
Definition config.h:613
Configuration for a single external MCP server entry.
Definition config.h:633
std::string command
Stdio command (empty for SSE)
Definition config.h:634
std::vector< std::string > args
Stdio command arguments.
Definition config.h:635
std::string url
SSE endpoint URL (empty for stdio)
Definition config.h:637
std::unordered_map< std::string, std::string > env
Stdio environment variables.
Definition config.h:636
Filesystem MCP server configuration.
Definition config.h:599
bool diagnostics_on_edit
Proactive diagnostics on edit/write.
Definition config.h:600
bool allow_outside_root
Allow file ops outside workspace root.
Definition config.h:603
float diagnostics_timeout
Diagnostics timeout (0.1–5.0)
Definition config.h:602
std::optional< int > max_read_bytes
Max file read size (nullopt = derive from context)
Definition config.h:604
float max_read_context_pct
Max context % for single file read.
Definition config.h:605
bool fail_on_errors
Rollback edit if it introduces errors.
Definition config.h:601
Generation parameters configuration (top-level defaults).
Definition config.h:712
int max_tokens
Default max tokens (64–32768)
Definition config.h:713
float default_top_p
Default top_p (0.0–1.0)
Definition config.h:715
int budget_limit
gh#80 (v2.5.0) budget ceiling: generated tokens (budget_mode "tokens") or wall-clock seconds (budget_...
Definition config.h:730
std::string budget_mode
gh#80 (v2.5.0) thinking-budget mode: "off" (default), "tokens", or "wall_clock".
Definition config.h:723
float default_temperature
Default temperature (0.0–2.0)
Definition config.h:714
SpeculativeConfig speculative
Speculative decoding (gh#36)
Definition config.h:918
LSP integration configuration.
Definition config.h:747
bool python_enabled
Enable Python LSP.
Definition config.h:749
bool enabled
Enable LSP integration.
Definition config.h:748
bool c_enabled
Enable C/C++ LSP.
Definition config.h:750
MCP server configuration.
Definition config.h:644
bool enable_entropic
Enable entropic internal server (handoff, delegate, pipeline)
Definition config.h:645
FilesystemConfig filesystem
Filesystem server config.
Definition config.h:651
bool enable_filesystem
Enable filesystem server.
Definition config.h:646
std::unordered_map< std::string, ExternalServerEntry > external_servers
Named external servers.
Definition config.h:657
bool enable_git
Enable git server.
Definition config.h:648
bool enable_diagnostics
Enable diagnostics server.
Definition config.h:649
int server_timeout_seconds
Server timeout (5–300)
Definition config.h:653
bool enable_bash
Enable bash server.
Definition config.h:647
ExternalMCPConfig external
External MCP server config (Entropic-as-server)
Definition config.h:652
std::string working_dir
Server working directory (empty = CWD) (v2.0.4)
Definition config.h:654
bool enable_web
Enable web server.
Definition config.h:650
Model configuration for a single tier.
Definition config.h:148
std::filesystem::path mmproj_path
Vision projector GGUF path.
Definition config.h:244
int gpu_layers
GPU offload layers (-1 = all)
Definition config.h:152
int reasoning_budget
Think token budget (-1 = unlimited)
Definition config.h:157
int n_ubatch
Physical micro-batch size for prompt processing (gh#23 MVP item 5).
Definition config.h:172
int context_length
Context window size (512–131072)
Definition config.h:151
std::filesystem::path path
Resolved model file path.
Definition config.h:149
float rope_freq_scale
RoPE frequency scaling factor (gh#23 MVP item 10).
Definition config.h:222
int main_gpu
Primary GPU index for model load (gh#23 MVP item 7).
Definition config.h:194
int n_threads
CPU threads (0 = auto-detect)
Definition config.h:174
bool offload_kqv
Offload KQV ops (incl.
Definition config.h:202
int n_parallel
Max parallel sequences per context (gh#23 MVP item 11).
Definition config.h:232
std::string tensor_split
Multi-GPU tensor split ratios (empty = single GPU)
Definition config.h:175
std::string cache_type_k
KV cache key quantization type.
Definition config.h:158
bool keep_warm
Pre-warm model at startup.
Definition config.h:153
std::string cache_type_v
KV cache value quantization type.
Definition config.h:159
std::string split_mode
Multi-GPU split mode for model load (gh#23 MVP item 6).
Definition config.h:186
int n_batch
Batch size for prompt processing.
Definition config.h:160
bool flash_attn
Enable flash attention.
Definition config.h:233
bool use_mlock
Lock model in system RAM.
Definition config.h:154
std::optional< std::vector< std::string > > allowed_tools
Tool whitelist (nullopt = all)
Definition config.h:236
std::string adapter
Chat adapter name.
Definition config.h:150
float rope_freq_base
RoPE base frequency override (gh#23 MVP item 9).
Definition config.h:212
Configuration for all models (tiers + router).
Definition config.h:542
std::optional< ModelConfig > router
Router model (separate from tiers)
Definition config.h:544
std::unordered_map< std::string, TierConfig > tiers
Tier name → config.
Definition config.h:543
std::string default_tier
Default tier name.
Definition config.h:545
Full parsed configuration.
Definition config.h:929
int vram_reserve_mb
Reserved VRAM headroom (MB, 0–65536)
Definition config.h:950
PermissionsConfig permissions
Tool permissions.
Definition config.h:933
PromptCacheConfig prompt_cache
Prompt KV cache settings.
Definition config.h:937
std::optional< std::filesystem::path > app_context
App context: nullopt = disabled by default.
Definition config.h:946
CompactionConfig compaction
Auto-compaction settings.
Definition config.h:935
RoutingConfig routing
Routing rules.
Definition config.h:931
InferenceConfig inference
Inference-side knobs (currently speculative decoding only).
Definition config.h:985
ModelsConfig models
Tiers + router.
Definition config.h:930
LSPConfig lsp
LSP integration.
Definition config.h:936
ConstitutionalValidationConfig constitutional_validation
Constitutional validation pipeline settings.
Definition config.h:981
std::filesystem::path log_dir
Session log directory (session.log + session_model.log).
Definition config.h:957
bool ggml_logging
Enable ggml/llama.cpp logging to llama_ggml.log in log_dir.
Definition config.h:961
GenerationConfig generation
Default generation params.
Definition config.h:932
std::string log_level
Log level string.
Definition config.h:939
MCPConfig mcp
MCP server settings.
Definition config.h:934
bool console_logging
Emit engine spdlog output to the stderr console sink.
Definition config.h:978
bool inject_model_context
Auto-inject model context into system prompt.
Definition config.h:949
std::filesystem::path llama_log_path
Override path for ggml/llama log when ggml_logging == true (gh#23 MVP item 12, v2....
Definition config.h:971
bool app_context_disabled
true if app_context explicitly disabled
Definition config.h:947
std::optional< std::filesystem::path > constitution
Constitution: nullopt = bundled default, disabled = explicit false.
Definition config.h:942
bool constitution_disabled
true if constitution explicitly disabled
Definition config.h:943
std::filesystem::path config_dir
Config dir — base for bundled data discovery.
Definition config.h:953
Tool permission configuration.
Definition config.h:589
std::vector< std::string > deny
Denied tool patterns (glob)
Definition config.h:591
std::vector< std::string > allow
Allowed tool patterns (glob)
Definition config.h:590
bool auto_approve
Skip confirmation prompts.
Definition config.h:592
Prompt caching configuration.
Definition config.h:266
size_t max_bytes
Maximum cache RAM (512 MB default)
Definition config.h:267
bool log_hits
Log cache hit/miss at INFO level.
Definition config.h:269
bool enabled
Master switch (false = no caching)
Definition config.h:268
Configuration for model routing.
Definition config.h:577
std::string fallback_tier
Fallback when routing fails.
Definition config.h:579
std::unordered_map< std::string, std::vector< std::string > > handoff_rules
Tier handoff rules.
Definition config.h:582
bool enabled
Enable routing.
Definition config.h:578
std::optional< std::string > classification_prompt
Custom prompt (nullopt = auto)
Definition config.h:580
std::unordered_map< std::string, std::string > tier_map
Classification → tier mapping.
Definition config.h:581
Speculative-decoding configuration (inference.speculative.
Definition config.h:876
bool enabled
Master switch (off by default)
Definition config.h:877
bool mtp
gh#106 (v2.9.0): drive MTP (the draft is a trunk-sharing head via ctx_other) instead of the gh#36 sep...
Definition config.h:884
int n_draft
Window size (proposed tokens).
Definition config.h:878
ModelConfig draft
Full ModelConfig for the draft model.
Definition config.h:905
Tier-specific model configuration.
Definition config.h:425
std::optional< float > frequency_penalty
gh#85
Definition config.h:478
std::optional< bool > routable
None = defer to identity frontmatter.
Definition config.h:430
std::optional< std::filesystem::path > identity
Identity prompt path (nullopt = bundled)
Definition config.h:426
std::optional< float > temperature
Per-tier sampler temperature from identity frontmatter (gh#82).
Definition config.h:462
std::optional< float > top_p
Per-tier sampler knobs from identity frontmatter (gh#85).
Definition config.h:474
std::optional< float > repeat_penalty
Per-tier repeat_penalty + enable_thinking from identity frontmatter (gh#86).
Definition config.h:485
std::optional< std::string > auto_chain
Target tier name (nullopt = defer to identity)
Definition config.h:429
bool identity_disabled
true if identity explicitly disabled
Definition config.h:427
std::optional< float > min_p
gh#85
Definition config.h:476
std::optional< std::string > tool_call_mode
Per-tier tool-call generation mode (gh#103).
Definition config.h:493
std::optional< float > presence_penalty
gh#85
Definition config.h:477
std::optional< int > max_output_tokens
Per-tier max output tokens from identity frontmatter (gh#82).
Definition config.h:467
std::optional< int > top_k
gh#85
Definition config.h:475
std::optional< bool > enable_thinking
gh#86
Definition config.h:486
std::vector< std::string > capabilities
Declared tier capabilities (gh#41).
Definition config.h:455
std::optional< std::filesystem::path > grammar
Grammar file path.
Definition config.h:428
std::optional< bool > speculative_mtp
Per-tier MTP speculative-decode override (gh#108).
Definition config.h:502
Config validation functions.
ENTROPIC_EXPORT std::string validate_config(const ParsedConfig &config, std::vector< std::string > &warnings)
Validate the full ParsedConfig.
Definition validate.cpp:326
std::filesystem::path expand_home(const std::filesystem::path &p)
Expand ~ to home directory in a path.
Definition yaml_util.cpp:61
bool extract_string_list_map(ryml::ConstNodeRef node, c4::csubstr key, std::unordered_map< std::string, std::vector< std::string > > &out)
Extract a map of string to list of strings from a YAML mapping.
std::string read_file(const std::filesystem::path &path)
Read a file into a string.
Definition yaml_util.cpp:39
bool extract_path(ryml::ConstNodeRef node, c4::csubstr key, std::filesystem::path &out)
Extract a filesystem path with ~ expansion.
bool extract_string_list(ryml::ConstNodeRef node, c4::csubstr key, std::vector< std::string > &out)
Extract a vector of strings from a YAML sequence node.
bool extract_string_map(ryml::ConstNodeRef node, c4::csubstr key, std::unordered_map< std::string, std::string > &out)
Extract a map of string to string from a YAML mapping node.
bool extract_string_list_opt(ryml::ConstNodeRef node, c4::csubstr key, std::optional< std::vector< std::string > > &out)
Extract an optional vector of strings.
bool extract_tri_state_path(ryml::ConstNodeRef node, c4::csubstr key, std::optional< std::filesystem::path > &out, bool &disabled)
Extract a tri-state path (null = default, false = disabled, string = path).
std::string to_string(c4::csubstr s)
Convert ryml csubstr to std::string.
Definition yaml_util.cpp:27
bool extract(ryml::ConstNodeRef node, c4::csubstr key, std::string &out)
Extract a string value from a YAML node.
Definition yaml_util.cpp:85
ryml extraction helpers for config parsing.