Entropic 2.14.3
Local-first agentic inference engine
Loading...
Searching...
No Matches
validate.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
10
11// gh#144 (v2.12.0): pure session-pool geometry + conflict rules.
12#include "../inference/session_pool_util.h"
13// gh#153 #42(iii) (v2.13.0): pure expert-offload conflict rules. Vendor-free
14// on purpose — entropic-config links no llama.cpp.
15#include "../inference/expert_offload.h"
16
17static auto s_log = entropic::log::get("config");
18
19namespace entropic::config {
20
28std::string validate_allowed_tools(const std::vector<std::string>& tools)
29{
30 for (const auto& entry : tools) {
31 if (entry.find('.') == std::string::npos) {
32 return "allowed_tools entry '" + entry
33 + "' must use '{server_name}.{tool_name}' format";
34 }
35 }
36 return "";
37}
38
47std::string validate(const ModelConfig& config)
48{
49 std::string err;
50
51 if (config.context_length < 512 || config.context_length > 131072) {
52 err = "context_length must be in [512, 131072], got "
53 + std::to_string(config.context_length);
54 } else if (config.adapter.empty()) {
55 err = "adapter must not be empty";
56 } else if (config.n_batch < 1) {
57 err = "n_batch must be >= 1, got "
58 + std::to_string(config.n_batch);
59 } else if (config.allowed_tools.has_value()) {
60 err = validate_allowed_tools(*config.allowed_tools);
61 }
62
63 // gh#144 (v2.12.0): a session pool and the gh#98 batch fan-out want
64 // opposite KV buffers. Refuse the combination by name at CONFIGURE time
65 // rather than picking one silently — either choice would fail later and
66 // far from its cause (a pool without private streams, or seq_cp
67 // asserting at decode).
68 if (err.empty()) {
70 }
71
72 // gh#153 #42(iii) (v2.13.0): expert offload is EXPERIMENTAL and its
73 // impossible combinations are refused by name here rather than
74 // discovered as "the knob did nothing" after a multi-second load.
75 if (err.empty()) {
77 }
78
79 return err;
80}
81
89std::string validate(const ModelsConfig& config)
90{
91 if (!config.tiers.empty()
92 && config.tiers.count(config.default_tier) == 0) {
93 return "default tier '" + config.default_tier
94 + "' not in tiers";
95 }
96 return "";
97}
98
106std::string validate(const CompactionConfig& config)
107{
108 std::string err;
109
110 if (config.threshold_percent < 0.5f
111 || config.threshold_percent > 0.99f) {
112 err = "compaction.threshold_percent must be in [0.5, 0.99], got "
113 + std::to_string(config.threshold_percent);
114 } else if (config.warning_threshold_percent
115 >= config.threshold_percent) {
116 err = "compaction.warning_threshold_percent ("
117 + std::to_string(config.warning_threshold_percent)
118 + ") must be less than threshold_percent ("
119 + std::to_string(config.threshold_percent) + ")";
120 } else if (config.preserve_recent_turns < 1
121 || config.preserve_recent_turns > 10) {
122 err = "compaction.preserve_recent_turns must be in [1, 10], got "
123 + std::to_string(config.preserve_recent_turns);
124 } else if (config.tool_result_ttl < 1) {
125 // v2.1.3 #6 / TTL clamp removal: tool_result_ttl was previously
126 // documented as "1–20" but never actually validated. The upper
127 // bound was always advisory — consumers can pick whatever
128 // makes sense for their workload (long delegations on large
129 // context windows benefit from large TTLs). Lower bound IS
130 // enforced because TTL=0 would prune every result on the next
131 // iteration, which is nonsensical and almost certainly a
132 // misconfiguration rather than an intent.
133 err = "compaction.tool_result_ttl must be >= 1, got "
134 + std::to_string(config.tool_result_ttl);
135 }
136
137 return err;
138}
139
149 const std::string& fallback,
150 const std::unordered_map<std::string, TierConfig>& tiers)
151{
152 if (tiers.count(fallback) == 0) {
153 return "routing.fallback_tier '" + fallback + "' not in tiers";
154 }
155 return "";
156}
157
167 const std::unordered_map<std::string, std::string>& tier_map,
168 const std::unordered_map<std::string, TierConfig>& tiers)
169{
170 for (const auto& [key, tier] : tier_map) {
171 if (tiers.count(tier) == 0) {
172 return "routing.tier_map['" + key + "'] = '" + tier
173 + "' not in tiers";
174 }
175 }
176 return "";
177}
178
188 const std::unordered_map<std::string, std::vector<std::string>>& rules,
189 const std::unordered_map<std::string, TierConfig>& tiers)
190{
191 for (const auto& [source, targets] : rules) {
192 if (tiers.count(source) == 0) {
193 return "routing.handoff_rules key '" + source
194 + "' not in tiers";
195 }
196 for (const auto& target : targets) {
197 if (tiers.count(target) == 0) {
198 return "routing.handoff_rules['" + source
199 + "'] contains '" + target + "' not in tiers";
200 }
201 }
202 }
203 return "";
204}
205
223 bool has_prompt = routing.classification_prompt.has_value()
224 && !routing.classification_prompt->empty();
225 if (routing.enabled && has_prompt && routing.tier_map.empty()) {
226 return "routing.classification_prompt is set but routing.tier_map is "
227 "empty — the router emits a digit that maps to no tier, so every "
228 "route falls back to the default (configure routing.tier_map)";
229 }
230 return "";
231}
232
242 const RoutingConfig& routing,
243 const ModelsConfig& models)
244{
245 std::string err;
246
247 if (models.tiers.empty()) {
248 /* no tiers — nothing to validate */
249 } else if (routing.enabled && !models.router.has_value()) {
250 err = "routing.enabled is true but models.router is not configured";
251 } else {
253
254 if (err.empty()) {
255 err = validate_tier_map(routing.tier_map, models.tiers);
256 }
257 if (err.empty()) {
259 }
260 if (err.empty()) {
262 }
263 }
264
265 return err;
266}
267
277 const std::unordered_map<std::string, TierConfig>& tiers,
278 const std::unordered_map<std::string, std::vector<std::string>>&
279 handoff_rules)
280{
281 std::string warnings;
282 for (const auto& [name, tier] : tiers) {
283 if (tier.auto_chain.has_value() && !tier.auto_chain->empty()
284 && handoff_rules.count(name) == 0) {
285 if (!warnings.empty()) {
286 warnings += "; ";
287 }
288 warnings += "tier '" + name
289 + "' has auto_chain but no handoff_rules entry";
290 }
291 }
292 return warnings;
293}
294
302std::string validate(const PromptCacheConfig& config)
303{
304 if (config.enabled && config.max_bytes == 0) {
305 return "inference.prompt_cache: enabled=true but max_bytes=0";
306 }
307 return "";
308}
309
317static std::string validate_model_tiers(const ModelsConfig& models)
318{
319 std::string err = validate(models);
320
321 if (err.empty()) {
322 for (const auto& [name, tier] : models.tiers) {
323 err = validate(static_cast<const ModelConfig&>(tier));
324 if (!err.empty()) {
325 err = "models." + name + ": " + err;
326 break;
327 }
328 }
329 }
330
331 if (err.empty() && models.router.has_value()) {
332 err = validate(*models.router);
333 if (!err.empty()) {
334 err = "models.router: " + err;
335 }
336 }
337
338 return err;
339}
340
348static bool holds_grammars(const std::filesystem::path& dir)
349{
350 std::error_code ec;
351 if (!std::filesystem::is_directory(dir, ec)) { return false; }
352 bool found = false;
353 for (const auto& entry : std::filesystem::directory_iterator(dir, ec)) {
354 if (entry.path().extension() == ".gbnf") {
355 found = true;
356 break;
357 }
358 }
359 return found;
360}
361
370std::vector<std::filesystem::path> grammar_search_paths(
371 const ParsedConfig& config,
372 const std::filesystem::path& data_dir)
373{
374 std::vector<std::filesystem::path> dirs;
375 if (!config.config_dir.empty()) {
376 dirs.push_back(config.config_dir / "grammars");
377 }
378 // The facade's data_dir fallback runs only when the config_dir load
379 // registered nothing, so include it on exactly that condition —
380 // otherwise this would accept a stem the registry will not hold.
381 if (!data_dir.empty() && (dirs.empty() || !holds_grammars(dirs.front()))) {
382 dirs.push_back(data_dir / "grammars");
383 }
384 return dirs;
385}
386
396 const std::string& stem,
397 const std::vector<std::filesystem::path>& search_dirs)
398{
399 bool found = false;
400 for (const auto& dir : search_dirs) {
401 std::error_code ec;
402 if (std::filesystem::exists(dir / (stem + ".gbnf"), ec)) {
403 found = true;
404 break;
405 }
406 }
407 return found;
408}
409
417static std::string describe_search_dirs(
418 const std::vector<std::filesystem::path>& search_dirs)
419{
420 if (search_dirs.empty()) {
421 return "no grammar search path is configured "
422 "(set config_dir, or install the bundled data directory)";
423 }
424 std::string list;
425 for (const auto& dir : search_dirs) {
426 if (!list.empty()) { list += ", "; }
427 list += dir.string();
428 }
429 return list;
430}
431
444 const ParsedConfig& config,
445 const std::vector<std::filesystem::path>& search_dirs)
446{
447 std::vector<std::string> unresolved;
448 for (const auto& [name, tier] : config.models.tiers) {
449 if (!tier.grammar.has_value()) { continue; }
450 // The registry keys on the file stem: "compactor.gbnf" and
451 // "compactor" name the same grammar (normalize_grammar_key).
452 const auto stem = tier.grammar->stem().string();
453 if (stem.empty() || grammar_stem_resolves(stem, search_dirs)) {
454 continue;
455 }
456 unresolved.push_back("models." + name + " -> '" + stem + "'");
457 }
458 if (unresolved.empty()) { return ""; }
459
460 std::string list;
461 for (const auto& entry : unresolved) {
462 if (!list.empty()) { list += ", "; }
463 list += entry;
464 }
465 return "grammar not resolvable yet for " + list
466 + " — searched " + describe_search_dirs(search_dirs)
467 + ". Register each key with entropic_grammar_register_file() or "
468 "entropic_grammar_register() before the first run on that tier, "
469 "or place <stem>.gbnf in a grammar search path. A run on such a "
470 "tier before then fails with ENTROPIC_ERROR_GRAMMAR_NOT_FOUND "
471 "rather than decoding unconstrained (gh#154).";
472}
473
482std::string validate_config(
483 const ParsedConfig& config,
484 std::vector<std::string>& warnings)
485{
486 s_log->info("Config validation start");
487 std::string err = validate_model_tiers(config.models);
488
489 if (err.empty()) {
490 err = validate_routing(config.routing, config.models);
491 }
492 if (err.empty()) {
493 err = validate(config.compaction);
494 }
495 if (err.empty()) {
496 err = validate(config.prompt_cache);
497 }
498 if (err.empty()) {
500 config.models.tiers, config.routing.handoff_rules);
501 if (!w.empty()) {
502 warnings.push_back(w);
503 }
504 }
505
506 if (err.empty()) {
507 s_log->info("Config validation passed ({} warning(s))",
508 warnings.size());
509 } else {
510 s_log->error("Config validation failed: {}", err);
511 }
512 return err;
513}
514
515} // namespace entropic::config
std::string err
size_exceeded, when set
const ReadWindow & w
Requested window.
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
@ optional
optional: every escape asks the host's path approver
@ tier
The TIER's identity frontmatter grammar: stem (gh#154).
std::string session_pool_conflict_reason(const ModelConfig &cfg)
Why this tier's session-pool request cannot be honoured.
std::string expert_offload_conflict_reason(const ModelConfig &cfg)
Configuration-time refusal for a combination that cannot mean what it says.
std::string config
–config FILE
Definition serve.cpp:38
Auto-compaction configuration.
Definition config.h:1059
Model configuration for a single tier.
Definition config.h:154
Configuration for all models (tiers + router).
Definition config.h:689
std::optional< ModelConfig > router
Router model (separate from tiers)
Definition config.h:691
std::unordered_map< std::string, TierConfig > tiers
Tier name → config.
Definition config.h:690
Full parsed configuration.
Definition config.h:1338
Prompt caching configuration.
Definition config.h:348
Configuration for model routing.
Definition config.h:742
std::string fallback_tier
Fallback when routing fails.
Definition config.h:744
std::unordered_map< std::string, std::vector< std::string > > handoff_rules
Tier handoff rules.
Definition config.h:747
bool enabled
Enable routing.
Definition config.h:743
std::optional< std::string > classification_prompt
Custom prompt (nullopt = auto)
Definition config.h:745
std::unordered_map< std::string, std::string > tier_map
Classification → tier mapping.
Definition config.h:746
static bool grammar_stem_resolves(const std::string &stem, const std::vector< std::filesystem::path > &search_dirs)
Whether <stem>.gbnf exists in any search directory.
Definition validate.cpp:395
static std::string describe_search_dirs(const std::vector< std::filesystem::path > &search_dirs)
Render the search-path list for an error message.
Definition validate.cpp:417
std::string validate_classification_routable(const RoutingConfig &routing)
A configured classification_prompt is inert without a tier_map.
Definition validate.cpp:222
static std::string validate_model_tiers(const ModelsConfig &models)
Validate model tiers and router.
Definition validate.cpp:317
static bool holds_grammars(const std::filesystem::path &dir)
Whether a directory holds at least one .gbnf file.
Definition validate.cpp:348
Config validation functions.
ENTROPIC_EXPORT std::string validate_handoff_rules(const std::unordered_map< std::string, std::vector< std::string > > &rules, const std::unordered_map< std::string, TierConfig > &tiers)
Check all handoff_rules keys and values exist in tiers.
Definition validate.cpp:187
ENTROPIC_EXPORT std::string validate_routing(const RoutingConfig &routing, const ModelsConfig &models)
Validate RoutingConfig against defined tiers.
Definition validate.cpp:241
ENTROPIC_EXPORT std::string validate_tier_map(const std::unordered_map< std::string, std::string > &tier_map, const std::unordered_map< std::string, TierConfig > &tiers)
Check all tier_map values exist in tiers.
Definition validate.cpp:166
ENTROPIC_EXPORT std::string validate_config(const ParsedConfig &config, std::vector< std::string > &warnings)
Validate the full ParsedConfig.
Definition validate.cpp:482
ENTROPIC_EXPORT std::string warn_unresolved_tier_grammars(const ParsedConfig &config, const std::vector< std::filesystem::path > &search_dirs)
Warn about a tier whose grammar: stem resolves to no .gbnf (gh#154).
Definition validate.cpp:443
ENTROPIC_EXPORT std::string validate_fallback_tier(const std::string &fallback, const std::unordered_map< std::string, TierConfig > &tiers)
Check fallback_tier exists in tiers.
Definition validate.cpp:148
ENTROPIC_EXPORT std::string warn_auto_chain_without_targets(const std::unordered_map< std::string, TierConfig > &tiers, const std::unordered_map< std::string, std::vector< std::string > > &handoff_rules)
Warn if tier has auto_chain but no handoff_rules entry.
Definition validate.cpp:276
ENTROPIC_EXPORT std::string validate(const ModelConfig &config)
Validate a ModelConfig.
Definition validate.cpp:47
ENTROPIC_EXPORT std::string validate_allowed_tools(const std::vector< std::string > &tools)
Validate allowed_tools entries use "server.tool" format.
Definition validate.cpp:28
ENTROPIC_EXPORT std::vector< std::filesystem::path > grammar_search_paths(const ParsedConfig &config, const std::filesystem::path &data_dir)
The directories a bare grammar stem is looked up in (gh#154).
Definition validate.cpp:370