Entropic 2.9.4
Local-first agentic inference engine
Loading...
Searching...
No Matches
orchestrator.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
17
18#include "llama_cpp_backend.h"
20#include <entropic/inference/adapters/adapter_base.h> // gh#88 recovery
21
22#include <llama.h>
23#include <nlohmann/json.hpp>
24
25#include <cstdlib>
26#include <filesystem>
27
28namespace entropic {
29
30namespace {
31auto logger = entropic::log::get("inference.orchestrator");
34
42std::string extract_latest_user_message(const std::vector<Message>& messages) {
43 for (auto it = messages.rbegin(); it != messages.rend(); ++it) {
44 if (it->role == "user") {
45 return it->content;
46 }
47 }
48 return "";
49}
50
51} // anonymous namespace
52
53// ── Initialization ─────────────────────────────────────────
54
68bool ModelOrchestrator::create_tier_backends(const ParsedConfig& config) {
69 for (const auto& [name, tier_config] : config.models.tiers) {
70 std::string path_key = tier_config.path.string();
71 if (!std::filesystem::exists(tier_config.path)) {
72 logger->error("Model file not found for tier '{}': {}",
73 name, path_key);
74 logger->error("Place a GGUF file at the path above, or set "
75 "ENTROPIC_MODEL_DIR to a directory containing "
76 "it. Run `entropic download --list` to see "
77 "bundled model keys, then "
78 "`entropic download <key>` to fetch one.");
79 return false;
80 }
81 if (model_pool_.find(path_key) == model_pool_.end()) {
82 model_pool_[path_key] = std::make_shared<LlamaCppBackend>();
83 }
84 tiers_[name] = model_pool_[path_key];
85 adapters_[name] = create_adapter(
86 tier_config.adapter, name, "" /* prompt resolved later */);
87 }
88 // Router backend instantiation moved to SecondaryModelLoader
89 // (gh#27, v2.1.11). The loader allocates the role slot lazily on
90 // first ensure_loaded() call from activate_router().
91 logger->info("Created {} unique backend(s) for {} tier(s)",
92 model_pool_.size(), tiers_.size());
93 return true;
94}
95
102void ModelOrchestrator::build_routing_tables(const ParsedConfig& config) {
103 for (const auto& [digit, tier_name] : config.routing.tier_map) {
104 tier_map_[digit] = tier_name;
105 }
106 for (const auto& [src, targets] : config.routing.handoff_rules) {
107 handoff_rules_[src] = std::unordered_set<std::string>(
108 targets.begin(), targets.end());
109 }
110}
111
119bool ModelOrchestrator::activate_default_tier(const ParsedConfig& config) {
120 if (tiers_.find(default_tier_) == tiers_.end()) { return true; }
121 auto& backend = tiers_[default_tier_];
122 auto& tier_cfg = config.models.tiers.at(default_tier_);
123 if (!backend->load_and_activate(tier_cfg)) {
124 logger->error("Failed to activate default tier: {}", default_tier_);
125 return false;
126 }
127 loaded_main_tier_ = default_tier_;
128 logger->info("Activated default tier: {}", default_tier_);
129 return true;
130}
131
143void ModelOrchestrator::activate_router(const ParsedConfig& config) {
144 if (!config.models.router) { return; }
145 // Lifecycle now lives on SecondaryModelLoader (gh#27, v2.1.11).
146 // Diagnostic-level logging is emitted by the loader itself.
147 secondary_loader_.ensure_loaded("router", *config.models.router);
148}
149
163void ModelOrchestrator::activate_draft(const ParsedConfig& config) {
164 const auto& spec = config.inference.speculative;
165 if (!spec.enabled || spec.draft.path.empty()) { return; }
166 // gh#106 (v2.9.0): under MTP the target owns the head (lazily, via
167 // generate_mtp → setup_mtp_draft). Loading draft.path as a standalone
168 // secondary backend here would double-load the head GGUF and never use
169 // it — skip the gh#36 separate-draft activation entirely.
170 if (spec.mtp) {
171 logger->info("Speculative MTP: head '{}' is target-owned; skipping "
172 "separate draft activation", spec.draft.path.string());
173 return;
174 }
175 // Full ModelConfig comes from the YAML's
176 // `inference.speculative.draft:` block — every llama.cpp knob is
177 // consumer-tunable. Defaults come from
178 // `make_default_draft_model_config()` (gpu_layers=0,
179 // flash_attn=false, context_length=8192, n_threads=4).
180 secondary_loader_.ensure_loaded("draft", spec.draft);
181}
182
198 config_ = config;
199 default_tier_ = config.models.default_tier;
200 vram_budget_bytes_ = resolve_vram_budget_bytes();
201 if (vram_budget_bytes_ > 0) {
202 logger->info("[residency] VRAM budget: {} bytes "
203 "(ENTROPIC_VRAM_BUDGET_BYTES)",
204 vram_budget_bytes_);
205 }
206
207 // Route ggml/llama logs before any model loading.
208 // gh#23 v2.3.24: `llama_log_path` overrides the hardcoded
209 // `<log_dir>/llama_ggml.log` when non-empty. The non-empty-and-no-log-dir
210 // case is also supported so consumers that want llama logs but
211 // no session.log can opt in.
212 if (config.ggml_logging) {
213 std::string path;
214 if (!config.llama_log_path.empty()) {
215 path = config.llama_log_path.string();
216 } else if (!config.log_dir.empty()) {
217 path = (config.log_dir / "llama_ggml.log").string();
218 }
219 if (!path.empty()) {
220 entropic_inference_log_to_file(path.c_str());
221 logger->info("ggml logging: {}", path);
222 }
223 }
224
225 logger->info("Initializing model orchestrator");
226
227 if (!create_tier_backends(config)) { return false; }
228 build_routing_tables(config);
229 if (!activate_default_tier(config)) { return false; }
230 activate_router(config);
231 activate_draft(config); // Speculative draft slot (v2.1.11)
232
233 preload_adapters(); // LoRA adapters → WARM (v1.9.2)
234 load_bundled_grammars(); // Bundled grammars (v1.9.3)
235 return true;
236}
237
248 logger->info("Shutting down model orchestrator");
249
250 for (auto& [path, backend] : model_pool_) {
251 if (backend->is_loaded()) {
252 backend->unload();
253 }
254 }
255
256 secondary_loader_.shutdown();
257}
258
265 // Order matters (gh#58 close-out, v2.3.0):
266 // 1. Backends first → frees llama_contexts.
267 // 2. LoRA adapter handles after → safe because the contexts
268 // that may have held HOT adapter references are gone.
269 shutdown();
270 lora_manager_.unload_all();
271}
272
280bool ModelOrchestrator::resolve_mtp_effective(const std::string& tier_name) const {
281 auto it = config_.models.tiers.find(tier_name);
282 if (it != config_.models.tiers.end() && it->second.speculative_mtp) {
283 return *it->second.speculative_mtp;
284 }
285 return config_.inference.speculative.mtp;
286}
287
294GenerationResult ModelOrchestrator::run_generate_dispatch(
295 InferenceBackend* model,
296 const std::vector<Message>& messages,
297 const GenerationParams& params,
298 const std::string& tier_name) {
299 GenerationResult result;
300 bool kernel_ran = config_.inference.speculative.enabled
301 && try_speculative_route(model, messages, params, tier_name, result);
302 if (!kernel_ran) {
303 result = model->generate(messages, params);
304 }
305 return result;
306}
307
322bool ModelOrchestrator::try_mtp_route(
323 InferenceBackend* model,
324 const std::vector<Message>& messages,
325 const GenerationParams& params,
326 std::function<void(std::string_view)> on_token,
327 std::atomic<bool>& cancel,
328 GenerationResult& result)
329{
330 auto* llama_target = dynamic_cast<LlamaCppBackend*>(model);
331 if (llama_target == nullptr) {
332 // Fail loud — no silent plain-decode fallback (gh#108).
333 result = GenerationResult{};
334 result.error_code = ENTROPIC_ERROR_NOT_SUPPORTED;
335 result.error_message = "speculative.mtp enabled but the target backend "
336 "is not llama.cpp; disable speculative.mtp";
337 result.finish_reason = "error";
338 logger->error("{}", result.error_message);
339 } else {
340 result = llama_target->generate_mtp(
341 messages, params, on_token, cancel,
342 config_.inference.speculative.draft.path.string(),
344 }
345 return true; // MTP owns the outcome — never fall back to plain decode
346}
347
372bool ModelOrchestrator::try_speculative_route_streaming(
373 InferenceBackend* model,
374 const std::vector<Message>& messages,
375 const GenerationParams& params,
376 const std::string& tier_name,
377 std::function<void(std::string_view)> on_token,
378 std::atomic<bool>& cancel,
379 GenerationResult& result)
380{
381 // gh#106 (v2.9.0): MTP routes BEFORE the gh#36 compat/pair path — the
382 // target owns the head (no separate draft backend), and MTP tolerates
383 // shared-KV gemma4 archs the gh#36 compat gate rejects.
384 if (resolve_mtp_effective(tier_name) && params.grammar.empty()) {
385 return try_mtp_route(model, messages, params, on_token, cancel,
386 result);
387 }
388 auto compat = check_speculative_compat();
389 bool kernel_ran = false;
390 if (!compat.compatible) {
391 logger->info("Speculative requested but pair incompatible "
392 "({}); using plain decode", compat.diagnostic);
393 } else {
394 auto* llama_target = dynamic_cast<LlamaCppBackend*>(model);
395 auto* draft_be = secondary_loader_.get("draft");
396 auto* llama_draft = dynamic_cast<LlamaCppBackend*>(draft_be);
397 if (llama_target == nullptr || llama_draft == nullptr) {
398 logger->info("Speculative compat passed but target/draft "
399 "is not llama.cpp; using plain decode");
400 } else {
401 auto spec = llama_target->generate_speculative_with_draft(
402 messages, params, on_token, cancel, *llama_draft,
404 config_.inference.speculative.draft.path.string());
405 if (spec.error_code == ENTROPIC_ERROR_NOT_SUPPORTED) {
406 logger->info("Speculative kernel returned NOT_SUPPORTED "
407 "({}); falling back", spec.error_message);
408 } else {
409 result = std::move(spec);
410 kernel_ran = true;
411 }
412 }
413 }
414 return kernel_ran;
415}
416
426bool ModelOrchestrator::try_speculative_route(
427 InferenceBackend* model,
428 const std::vector<Message>& messages,
429 const GenerationParams& params,
430 const std::string& tier_name,
431 GenerationResult& result)
432{
433 std::atomic<bool> local_cancel{false};
434 // gh#108: pass an EMPTY std::function (not a bound no-op lambda) so the MTP
435 // path can distinguish non-streaming from streaming via the callback's
436 // bound-ness. gh#36's emit guards `if (on_token)`, so empty is equivalent.
437 return try_speculative_route_streaming(
438 model, messages, params, tier_name,
439 std::function<void(std::string_view)>{}, local_cancel, result);
440}
441
442// ── Generation ─────────────────────────────────────────────
443
457 const GenerationParams& params) {
458 if (auto* llama = dynamic_cast<LlamaCppBackend*>(model)) {
459 llama->set_active_tools(params.tools);
460 }
461}
462
480 ChatAdapter* adapter,
481 GenerationResult& result) {
482 if (result.content.empty()) { return; }
483 auto* llama = dynamic_cast<LlamaCppBackend*>(model);
484 result.raw_content = result.content;
485 if (llama != nullptr && llama->common_chat_parse_reliable()) {
486 auto parsed = llama->parse_response(result.content);
488 parsed.tool_calls, result.raw_content);
489 result.content = parsed.content;
490 result.tool_calls = std::move(parsed.tool_calls);
491 } else if (adapter != nullptr) {
492 auto parsed = adapter->parse_tool_calls(result.content);
493 result.content = parsed.cleaned_content;
494 result.tool_calls = std::move(parsed.tool_calls);
495 }
496}
497
514GenerationParams ModelOrchestrator::resolve_and_stage(
515 InferenceBackend* model,
516 const GenerationParams& params,
517 const std::string& tier_name) {
518 GenerationParams resolved = params;
519 resolve_grammar_key(resolved, tier_name); // v1.9.3
520 apply_tier_sampler_defaults(resolved, tier_name); // gh#82
521 stage_active_tools(model, resolved); // gh#87 3b
522 return resolved;
523}
524
536static void log_orchestration(const GenerationResult& result,
537 const std::string& selected,
538 const std::string& adapter_name,
539 const GenerationParams& params,
540 double routing_ms, double swap_ms) {
541 logger->info("Orchestration: tier={}, adapter={}, grammar={}",
542 selected, adapter_name,
543 params.grammar.empty() ? "unconstrained"
544 : params.grammar_key);
545 logger->info("Total: {:.0f}ms (route={:.0f}ms, swap={:.0f}ms, "
546 "gen={:.0f}ms)",
547 result.total_ms, routing_ms, swap_ms,
548 result.generation_time_ms);
549}
550
571 const std::vector<Message>& messages,
572 const GenerationParams& params,
573 const std::string& tier_name)
574{
575 auto t_start = now();
576
577 // Route if no explicit tier
578 std::string selected = tier_name;
579 double routing_ms = 0.0;
580 if (selected.empty()) {
581 auto t_route = now();
582 selected = route(messages);
583 routing_ms = elapsed_ms(t_route, now());
584 }
585
586 // Get model (may trigger swap)
587 auto t_swap = now();
588 InferenceBackend* model = get_model(selected);
589 double swap_ms = elapsed_ms(t_swap, now());
590
591 if (!model) { return build_no_model_error(selected); }
592
593 GenerationParams resolved_params =
594 resolve_and_stage(model, params, selected); // gh#87 3b
595
596 // Generate — speculative routing applies here too (v2.1.11, gh#36)
597 GenerationResult result = run_generate_dispatch(
598 model, messages, resolved_params, selected);
599
600 apply_adapter_parse(model, get_adapter(selected), result);
601
602 result.routing_ms = routing_ms;
603 result.swap_ms = swap_ms;
604 result.total_ms = elapsed_ms(t_start, now());
605 log_orchestration(result, selected, last_routing_result_.adapter_name,
606 resolved_params, routing_ms, swap_ms);
607 return result;
608}
609
622 const std::vector<Message>& messages,
623 const GenerationParams& params,
624 std::atomic<bool>& cancel,
625 const std::string& tier_name)
626{
627 auto t_start = now();
628
629 std::string selected = tier_name;
630 double routing_ms = 0.0;
631 if (selected.empty()) {
632 auto t_route = now();
633 selected = route(messages);
634 routing_ms = elapsed_ms(t_route, now());
635 }
636
637 auto t_swap = now();
638 InferenceBackend* model = get_model(selected);
639 double swap_ms = elapsed_ms(t_swap, now());
640
641 if (!model) { return build_no_model_error(selected); }
642
643 GenerationParams resolved_params =
644 resolve_and_stage(model, params, selected); // gh#87 3b
645
646 GenerationResult result = model->generate(
647 messages, resolved_params, cancel);
648
649 apply_adapter_parse(model, get_adapter(selected), result);
650
651 result.routing_ms = routing_ms;
652 result.swap_ms = swap_ms;
653 result.total_ms = elapsed_ms(t_start, now());
654 log_orchestration(result, selected, last_routing_result_.adapter_name,
655 resolved_params, routing_ms, swap_ms);
656 return result;
657}
658
672std::vector<GenerationResult> ModelOrchestrator::generate_batch(
673 const std::vector<std::vector<Message>>& messages_list,
674 const std::vector<GenerationParams>& params_list,
675 const std::vector<std::string>& tiers,
676 std::atomic<bool>& cancel)
677{
678 const std::size_t n = messages_list.size();
679 const std::string lead =
680 (tiers.empty() || tiers[0].empty()) ? "default" : tiers[0];
681 InferenceBackend* model = get_model(lead);
682 if (model == nullptr) {
683 return std::vector<GenerationResult>(n, build_no_model_error(lead));
684 }
685
686 std::vector<GenerationParams> resolved;
687 resolved.reserve(n);
688 for (std::size_t i = 0; i < n; ++i) {
689 const std::string& t = tiers[i].empty() ? lead : tiers[i];
690 resolved.push_back(resolve_and_stage(model, params_list[i], t));
691 }
692
693 auto results = model->generate_batch(messages_list, resolved, cancel);
694 for (std::size_t i = 0; i < results.size() && i < tiers.size(); ++i) {
695 const std::string& t = tiers[i].empty() ? lead : tiers[i];
696 apply_adapter_parse(model, get_adapter(t), results[i]);
697 }
698 return results;
699}
700
715 const std::vector<Message>& messages,
716 const GenerationParams& params,
717 std::function<void(std::string_view)> on_token,
718 std::atomic<bool>& cancel,
719 const std::string& tier_name)
720{
721 std::string selected = tier_name.empty() ? route(messages) : tier_name;
722 InferenceBackend* model = get_model(selected);
723
724 if (!model) {
727 err.error_message = "No model for tier: " + selected;
728 err.finish_reason = "error";
729 return err;
730 }
731
732 GenerationParams resolved_params =
733 resolve_and_stage(model, params, selected); // gh#87 3b
734
735 // Speculative routing (v2.1.11, gh#36): when speculative is
736 // enabled in config AND target/draft pair is compatible, attempt
737 // the speculative kernel. On NOT_SUPPORTED (kernel staged), fall
738 // back to plain streaming. This keeps the v2.1.11 ship-without-
739 // kernel state observable as "plain decode, speculative
740 // requested but deferred."
741 GenerationResult spec_streaming;
742 if (config_.inference.speculative.enabled
743 && try_speculative_route_streaming(
744 model, messages, resolved_params, selected, on_token, cancel,
745 spec_streaming)) {
746 return spec_streaming;
747 }
748
749 return model->generate_streaming(messages, resolved_params, on_token, cancel);
750}
751
752// ── Routing ────────────────────────────────────────────────
753
766std::string ModelOrchestrator::route(const std::vector<Message>& messages) {
767 if (!config_.routing.enabled
768 || !config_.models.router.has_value()) {
769 logger->info("Route: routing disabled, using default '{}'",
770 default_tier_);
771 last_routing_result_ = {default_tier_, "", "", "none", 0.0};
772 return default_tier_;
773 }
774
775 auto [tier, raw] = classify_task(messages);
776 last_routing_result_ = {tier, loaded_main_tier_, raw, "none", 0.0};
777
778 // Track history
779 tier_history_.push_back(tier);
780 if (tier_history_.size() > 5) {
781 tier_history_.erase(tier_history_.begin());
782 }
783
784 logger->info("[ROUTER] {} | raw='{}'", tier, raw);
785 return tier;
786}
787
809std::pair<std::string, std::string> ModelOrchestrator::classify_task(
810 const std::vector<Message>& messages)
811{
812 std::string user_msg = extract_latest_user_message(messages);
813
814 GenerationParams router_params;
815 router_params.max_tokens = 1;
816 router_params.temperature = 0.0f;
817
818 auto* router_backend = secondary_loader_.get("router");
819 if (router_backend == nullptr) {
820 logger->warn("classify_task: router not loaded; returning empty");
821 return {"", ""};
822 }
823 // audit task #71: a non-fine-tuned router fed the bare "<msg> ->" just
824 // CONTINUES the text and never emits a routing digit, so classify_task
825 // silently always fell back to the default tier. When the deployment
826 // configures routing.classification_prompt, prepend it so a general
827 // instruct model is actually told the digit scheme. (The trailing " ->"
828 // still constrains it to a single digit, per build_classification_prompt.)
829 std::string router_prompt = user_msg + " ->";
830 const auto& cprompt = config_.routing.classification_prompt;
831 if (cprompt.has_value() && !cprompt->empty()) {
832 router_prompt = *cprompt + "\n" + user_msg + " ->";
833 // A general instruct model emits a leading space before the digit;
834 // max_tokens=1 would cut it off. 4 captures "<space>1"; the digit scan
835 // below takes the first tier_map char. Only widened on the prompt path
836 // so unconfigured deployments keep the original 1-token behavior.
837 router_params.max_tokens = 4;
838 // v2.8.1 (review #3): classification_prompt was parsed-but-never-read
839 // before the v2.8.0 fix. Log when the active (prompt) path is taken so
840 // a deployment carrying a stale prompt sees the inert->active switch +
841 // the widened token budget instead of a silent behavior change.
842 logger->info("classify_task: using configured classification_prompt "
843 "(router instructed; max_tokens widened to 4)");
844 }
845 auto result = router_backend->complete(router_prompt, router_params);
846 std::string raw = result.content;
847
848 // Trim whitespace
849 auto start = raw.find_first_not_of(" \t\n\r");
850 if (start != std::string::npos) {
851 raw = raw.substr(start);
852 }
853
854 // Find matching tier
855 for (char c : raw) {
856 std::string digit(1, c);
857 auto it = tier_map_.find(digit);
858 if (it != tier_map_.end()) {
859 logger->info("Route: digit='{}' -> tier='{}'",
860 digit, it->second);
861 return {it->second, digit};
862 }
863 }
864
865 logger->warn("Route: no valid digit in '{}', defaulting to {}",
866 raw, default_tier_);
867 return {default_tier_, ""};
868}
869
870// ── Model access ───────────────────────────────────────────
871
890void ModelOrchestrator::record_activation_reuse(
891 const std::string& tier_name) {
892 auto now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
893 std::chrono::steady_clock::now() - start_time_).count();
894 bool tier_changed = (loaded_main_tier_ != tier_name);
895 tier_last_activation_ms_[tier_name] = now_ms;
896 if (!tier_changed) { return; }
897 auto tier_it = config_.models.tiers.find(tier_name);
898 std::string path = tier_it != config_.models.tiers.end()
899 ? tier_it->second.path.string() : "";
900 size_t footprint = tier_footprint_bytes_.count(tier_name)
901 ? tier_footprint_bytes_[tier_name]
902 : estimate_footprint_bytes(tier_name);
903 tier_footprint_bytes_[tier_name] = footprint;
904 loaded_main_tier_ = tier_name;
905 fire_residency_observer(ResidencyEvent::ActivationSwap,
906 tier_name, path, footprint);
907}
908
919bool ModelOrchestrator::residency_admits(const std::string& tier_name) {
920 size_t footprint = estimate_footprint_bytes(tier_name);
921 if (footprint > 0) {
922 tier_footprint_bytes_[tier_name] = footprint;
923 }
924 if (vram_budget_bytes_ > 0 && footprint > vram_budget_bytes_) {
925 logger->error("[residency] tier '{}' footprint {} bytes "
926 "exceeds VRAM budget {} bytes — "
927 "TIER_MODEL_TOO_LARGE (gh#57)",
928 tier_name, footprint, vram_budget_bytes_);
929 last_residency_error_ = ENTROPIC_ERROR_TIER_MODEL_TOO_LARGE;
930 return false;
931 }
932 return true;
933}
934
954GenerationResult ModelOrchestrator::build_no_model_error(
955 const std::string& tier_name) {
956 GenerationResult err;
957 err.finish_reason = "error";
958 if (last_residency_error_ != ENTROPIC_OK) {
959 err.error_code = last_residency_error_;
960 err.error_message = "Tier '" + tier_name + "' model exceeds the "
961 "engine's VRAM budget (gh#57)";
962 last_residency_error_ = ENTROPIC_OK;
963 } else {
964 err.error_code = ENTROPIC_ERROR_GENERATE_FAILED;
965 err.error_message = "No model available for tier: " + tier_name;
966 }
967 return err;
968}
969
983InferenceBackend* ModelOrchestrator::activate_and_track(
984 const std::string& tier_name,
985 const std::shared_ptr<InferenceBackend>& backend) {
986 auto tier_it = config_.models.tiers.find(tier_name);
987 bool activated = tier_it != config_.models.tiers.end()
988 && backend->load_and_activate(tier_it->second);
989 if (!activated) {
990 logger->error("Failed to activate tier: {}", tier_name);
991 return nullptr;
992 }
993 loaded_main_tier_ = tier_name;
994 last_routing_result_.swap_action = "loaded";
995 auto now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
996 std::chrono::steady_clock::now() - start_time_).count();
997 tier_last_activation_ms_[tier_name] = now_ms;
998 size_t footprint = tier_footprint_bytes_.count(tier_name)
999 ? tier_footprint_bytes_[tier_name] : 0;
1000 fire_residency_observer(ResidencyEvent::Loaded,
1001 tier_name, tier_it->second.path.string(),
1002 footprint);
1003 return backend.get();
1004}
1005
1019InferenceBackend* ModelOrchestrator::get_model(const std::string& tier_name) {
1020 std::lock_guard<std::mutex> lock(swap_mutex_);
1021
1022 auto it = tiers_.find(tier_name);
1023 std::string effective_tier = tier_name;
1024 if (it == tiers_.end()) {
1025 it = tiers_.find(config_.routing.fallback_tier);
1026 if (it != tiers_.end()) {
1027 effective_tier = config_.routing.fallback_tier;
1028 }
1029 }
1030
1031 InferenceBackend* result = nullptr;
1032 if (it != tiers_.end() && it->second->is_active()) {
1033 last_routing_result_.swap_action = "reused";
1034 record_activation_reuse(effective_tier);
1035 result = it->second.get();
1036 } else if (it != tiers_.end() && residency_admits(effective_tier)) {
1037 deactivate_current_if_needed(it->second.get());
1038 result = activate_and_track(effective_tier, it->second);
1039 }
1040
1041 // Ensure correct LoRA adapter for this tier (v1.9.2)
1042 if (result) {
1043 ensure_tier_lora(tier_name, result);
1044 }
1045
1046 return result;
1047}
1048
1056void ModelOrchestrator::ensure_tier_lora(const std::string& tier_name,
1057 InferenceBackend* result) {
1058 auto* llama_backend = dynamic_cast<LlamaCppBackend*>(result);
1059 llama_context* ctx = llama_backend
1060 ? llama_backend->llama_context_ptr() : nullptr;
1061 double adapter_ms = ensure_adapter_for_tier(tier_name, ctx);
1062 last_routing_result_.adapter_swap_ms = adapter_ms;
1063 last_routing_result_.adapter_name = lora_manager_.active_adapter();
1064}
1065
1077void ModelOrchestrator::deactivate_current_if_needed(InferenceBackend* incoming) {
1078 auto it = loaded_main_tier_.empty()
1079 ? tiers_.end() : tiers_.find(loaded_main_tier_);
1080
1081 bool should_swap = it != tiers_.end()
1082 && it->second.get() != incoming
1083 && it->second->is_loaded();
1084
1085 if (!should_swap) {
1086 return;
1087 }
1088
1089 // Cascade: unload adapters for this base model (v1.9.2)
1090 auto* llama_backend = dynamic_cast<LlamaCppBackend*>(it->second.get());
1091 if (llama_backend) {
1092 lora_manager_.unload_all_for_model(
1093 llama_backend->llama_model_ptr(),
1094 llama_backend->llama_context_ptr());
1095 }
1096
1097 unload_or_warm_current(it->second.get());
1098}
1099
1106void ModelOrchestrator::unload_or_warm_current(InferenceBackend* current) {
1107 auto cfg_it = config_.models.tiers.find(loaded_main_tier_);
1108 bool keep_warm = cfg_it != config_.models.tiers.end()
1109 && cfg_it->second.keep_warm;
1110
1111 if (keep_warm) {
1112 logger->info("Deactivating {} (keep_warm=true)", loaded_main_tier_);
1113 current->deactivate();
1114 return;
1115 }
1116 logger->info("Unloading {} (keep_warm=false)", loaded_main_tier_);
1117 std::string path = cfg_it != config_.models.tiers.end()
1118 ? cfg_it->second.path.string() : "";
1119 size_t footprint = tier_footprint_bytes_.count(loaded_main_tier_)
1120 ? tier_footprint_bytes_[loaded_main_tier_] : 0;
1121 std::string evicted_tier = loaded_main_tier_;
1122 current->unload();
1123 fire_residency_observer(ResidencyEvent::Evicted,
1124 evicted_tier, path, footprint);
1125}
1126
1127// ── Queries ────────────────────────────────────────────────
1128
1135 return last_routing_result_;
1136}
1137
1144 return loaded_main_tier_;
1145}
1146
1156std::vector<std::string> ModelOrchestrator::loaded_models() const {
1157 std::vector<std::string> result;
1158 for (const auto& [name, backend] : tiers_) {
1159 if (backend->is_loaded()) {
1160 result.push_back(name);
1161 }
1162 }
1163 if (secondary_loader_.is_loaded("router")) {
1164 result.push_back("router");
1165 }
1166 return result;
1167}
1168
1174std::vector<std::string> ModelOrchestrator::available_models() const {
1175 std::vector<std::string> result;
1176 for (const auto& [name, _] : tiers_) {
1177 result.push_back(name);
1178 }
1179 if (config_.models.router.has_value()) {
1180 result.push_back("router");
1181 }
1182 return result;
1183}
1184
1193 const std::string& tier_name) const {
1194 auto it = tiers_.find(tier_name);
1195 if (it == tiers_.end()) { return nullptr; }
1196 return it->second.get();
1197}
1198
1205 const std::string& from, const std::string& to) const
1206{
1207 auto it = handoff_rules_.find(from);
1208 if (it == handoff_rules_.end()) {
1209 return false;
1210 }
1211 return it->second.count(to) > 0;
1212}
1213
1219ChatAdapter* ModelOrchestrator::get_adapter(const std::string& tier_name) const {
1220 auto it = adapters_.find(tier_name);
1221 if (it != adapters_.end()) {
1222 return it->second.get();
1223 }
1224 return nullptr;
1225}
1226
1227// ── LoRA adapter management (v1.9.2) ──────────────────────
1228
1249bool ModelOrchestrator::deactivate_if_active(llama_context* ctx) {
1250 if (lora_manager_.active_adapter().empty()) {
1251 return false;
1252 }
1253 lora_manager_.deactivate(ctx);
1254 return true;
1255}
1256
1265double ModelOrchestrator::ensure_adapter_for_tier(
1266 const std::string& tier_name, llama_context* ctx)
1267{
1268 auto tier_it = config_.models.tiers.find(tier_name);
1269 if (tier_it == config_.models.tiers.end()) {
1270 return 0.0;
1271 }
1272
1273 const auto& tier_cfg = tier_it->second;
1274 auto t_start = now();
1275 bool needs_kv_clear = false;
1276
1277 if (!tier_cfg.adapter_path) {
1278 needs_kv_clear = deactivate_if_active(ctx);
1279 } else if (lora_manager_.active_adapter() != tier_name) {
1280 needs_kv_clear = lora_manager_.swap(tier_name, ctx);
1281 if (!needs_kv_clear) {
1282 logger->warn("Adapter swap to '{}' failed", tier_name);
1283 }
1284 }
1285
1286 if (needs_kv_clear && ctx) {
1287 llama_memory_clear(llama_get_memory(ctx), true);
1288 logger->info("Adapter swap for tier '{}' in {:.1f}ms",
1289 tier_name, elapsed_ms(t_start, now()));
1290 }
1291
1292 return elapsed_ms(t_start, now());
1293}
1294
1304void ModelOrchestrator::preload_adapters() {
1305 int loaded = 0;
1306
1307 for (const auto& [name, tier_cfg] : config_.models.tiers) {
1308 if (!tier_cfg.adapter_path) {
1309 continue;
1310 }
1311
1312 auto tier_it = tiers_.find(name);
1313 if (tier_it == tiers_.end()) {
1314 continue;
1315 }
1316
1317 auto* llama_backend = dynamic_cast<LlamaCppBackend*>(
1318 tier_it->second.get());
1319 if (!llama_backend || !llama_backend->llama_model_ptr()) {
1320 logger->warn("Cannot preload adapter for '{}' — model not loaded",
1321 name);
1322 continue;
1323 }
1324
1325 bool ok = lora_manager_.load(
1326 name,
1327 *tier_cfg.adapter_path,
1328 llama_backend->llama_model_ptr(),
1329 tier_cfg.adapter_scale);
1330
1331 if (ok) {
1332 ++loaded;
1333 }
1334 }
1335
1336 if (loaded > 0) {
1337 logger->info("Preloaded {} LoRA adapter(s) to WARM", loaded);
1338 }
1339}
1340
1341// ── Grammar registry (v1.9.3) ──────────────────────────────
1342
1352void ModelOrchestrator::load_bundled_grammars() {
1353 std::filesystem::path grammar_dir;
1354 if (!config_.config_dir.empty()) {
1355 grammar_dir = config_.config_dir / "grammars";
1356 }
1357 if (grammar_dir.empty() || !std::filesystem::is_directory(grammar_dir)) {
1358 // Fallback set by facade via load_grammars_from() if config_dir
1359 // doesn't have a grammars subdir. Check if already loaded.
1360 logger->info("No bundled grammar directory found, skipping");
1361 return;
1362 }
1363
1364 size_t count = grammar_registry_.load_bundled(grammar_dir);
1365 logger->info("Grammar registry: {} grammar(s) loaded from {}",
1366 count, grammar_dir.string());
1367}
1368
1382 const std::filesystem::path& grammar_dir) {
1383 if (!std::filesystem::is_directory(grammar_dir)) {
1384 return 0;
1385 }
1386 auto count = grammar_registry_.load_bundled(grammar_dir);
1387 logger->info("Grammar registry: {} grammar(s) loaded from {}",
1388 count, grammar_dir.string());
1389 return count;
1390}
1391
1403 for (auto& [_, backend] : model_pool_) {
1404 if (backend) { backend->clear_prompt_cache(); }
1405 }
1406 secondary_loader_.clear_all_prompt_caches();
1407 logger->info("Prompt caches invalidated across all backends "
1408 "(identity change)");
1409}
1410
1418 for (const auto& [_, tier] : config_.models.tiers) {
1419 if (tier.has_capability("vision")) { return true; }
1420 }
1421 return false;
1422}
1423
1431 for (const auto& [name, tier] : config_.models.tiers) {
1432 if (tier.has_capability("vision")) { return name; }
1433 }
1434 return "";
1435}
1436
1445static llama_model* resolve_target_model(
1446 const std::shared_ptr<InferenceBackend>& tier_backend) {
1447 if (!tier_backend || !tier_backend->is_loaded()) {
1448 return nullptr;
1449 }
1450 auto* llama_be = dynamic_cast<LlamaCppBackend*>(tier_backend.get());
1451 return (llama_be == nullptr) ? nullptr : llama_be->llama_model_ptr();
1452}
1453
1464std::string ModelOrchestrator::resolve_speculative_pair(
1465 llama_model*& target_out, llama_model*& draft_out) const {
1466 target_out = nullptr;
1467 draft_out = nullptr;
1468 std::string err;
1469
1470 auto tier_it = tiers_.find(loaded_main_tier_);
1471 if (tier_it == tiers_.end()) {
1472 err = "no main tier loaded";
1473 } else {
1474 target_out = resolve_target_model(tier_it->second);
1475 if (target_out == nullptr) {
1476 err = "main tier backend is not a llama.cpp backend or "
1477 "is not loaded";
1478 } else {
1479 auto* draft_backend = secondary_loader_.get("draft");
1480 if (draft_backend == nullptr || !draft_backend->is_loaded()) {
1481 err = "no draft model configured for speculative "
1482 "decoding "
1483 "(set inference.speculative.draft_model)";
1484 } else {
1485 auto* d = dynamic_cast<LlamaCppBackend*>(draft_backend);
1486 draft_out = (d == nullptr) ? nullptr : d->llama_model_ptr();
1487 if (draft_out == nullptr) {
1488 err = "draft backend is not a llama.cpp backend";
1489 }
1490 }
1491 }
1492 }
1493 return err;
1494}
1495
1507ModelOrchestrator::SpeculativeCompatInfo
1510 llama_model* target_model = nullptr;
1511 llama_model* draft_model = nullptr;
1512 info.diagnostic = resolve_speculative_pair(target_model, draft_model);
1513 if (info.diagnostic.empty()) {
1514 auto result = entropic::speculative::check_compat(
1515 target_model, draft_model);
1516 info.compatible = result.compatible;
1517 info.diagnostic = std::move(result.diagnostic);
1518 }
1519 return info;
1520}
1521
1533static std::string normalize_grammar_key(const std::string& grammar_value) {
1534 std::filesystem::path p(grammar_value);
1535 if (p.extension() == ".gbnf") {
1536 return p.stem().string();
1537 }
1538 return grammar_value;
1539}
1540
1555void ModelOrchestrator::resolve_grammar_key(
1556 GenerationParams& params, const std::string& tier_name)
1557{
1558 if (!params.grammar.empty()) {
1559 return;
1560 }
1561
1562 // Try explicit grammar_key
1563 std::string key = params.grammar_key;
1564
1565 // Fall back to tier config grammar field (frontmatter)
1566 if (key.empty()) {
1567 auto it = config_.models.tiers.find(tier_name);
1568 if (it != config_.models.tiers.end() && it->second.grammar) {
1569 key = normalize_grammar_key(it->second.grammar->string());
1570 }
1571 }
1572
1573 if (key.empty()) {
1574 return;
1575 }
1576
1577 std::string content = grammar_registry_.get(key);
1578 if (content.empty()) {
1579 logger->warn("Grammar key '{}' not found in registry", key);
1580 return;
1581 }
1582
1583 logger->info("Grammar resolved: key='{}', {} bytes",
1584 key, content.size());
1585 params.grammar = std::move(content);
1586}
1587
1588namespace {
1595template <typename T>
1596inline void apply_if_default(T& field, const std::optional<T>& ov, T dflt) {
1597 if (ov.has_value() && field == dflt) { field = *ov; }
1598}
1599} // namespace
1600
1607 GenerationParams& params, const TierSamplerOverrides& ov)
1608{
1609 // GenerationParams struct defaults (see types/config.h).
1610 apply_if_default(params.temperature, ov.temperature, 0.7f);
1611 apply_if_default(params.max_tokens, ov.max_output_tokens, 4096);
1612 apply_if_default(params.top_p, ov.top_p, 0.9f);
1613 apply_if_default(params.top_k, ov.top_k, 40);
1614 apply_if_default(params.min_p, ov.min_p, 0.0f);
1615 apply_if_default(params.presence_penalty, ov.presence_penalty, 0.0f);
1616 apply_if_default(params.frequency_penalty, ov.frequency_penalty, 0.0f);
1617 apply_if_default(params.repeat_penalty, ov.repeat_penalty, 1.1f); // gh#86
1618 apply_if_default(params.enable_thinking, ov.enable_thinking, true); // gh#86
1619 apply_if_default(params.tool_call_mode, ov.tool_call_mode, std::string{}); // gh#103
1620}
1621
1631void ModelOrchestrator::apply_tier_sampler_defaults(
1632 GenerationParams& params, const std::string& tier_name)
1633{
1634 auto it = config_.models.tiers.find(tier_name);
1635 if (it == config_.models.tiers.end()) { return; }
1636 const auto& tier = it->second;
1637 TierSamplerOverrides ov;
1638 ov.temperature = tier.temperature;
1639 ov.max_output_tokens = tier.max_output_tokens;
1640 ov.top_p = tier.top_p; // gh#85
1641 ov.top_k = tier.top_k; // gh#85
1642 ov.min_p = tier.min_p; // gh#85
1643 ov.presence_penalty = tier.presence_penalty; // gh#85
1644 ov.frequency_penalty = tier.frequency_penalty; // gh#85
1645 ov.repeat_penalty = tier.repeat_penalty; // gh#86
1646 ov.enable_thinking = tier.enable_thinking; // gh#86
1647 ov.tool_call_mode = tier.tool_call_mode; // gh#103
1648 float before_temp = params.temperature;
1649 int before_max = params.max_tokens;
1650 apply_tier_sampler_overrides(params, ov);
1651 if (params.temperature != before_temp) {
1652 logger->info("Tier '{}' temperature applied: {}",
1653 tier_name, params.temperature);
1654 }
1655 if (params.max_tokens != before_max) {
1656 logger->info("Tier '{}' max_output_tokens applied: {}",
1657 tier_name, params.max_tokens);
1658 }
1659}
1660
1661// ── VRAM-aware tier residency (v2.2.4, gh#57) ──────────────
1662
1673size_t ModelOrchestrator::resolve_vram_budget_bytes() {
1674 const char* env = std::getenv("ENTROPIC_VRAM_BUDGET_BYTES");
1675 if (env == nullptr || *env == '\0') { return 0; }
1676 try {
1677 long long v = std::stoll(env);
1678 return (v < 0) ? 0 : static_cast<size_t>(v);
1679 } catch (...) {
1680 return 0;
1681 }
1682}
1683
1694size_t ModelOrchestrator::estimate_footprint_bytes(
1695 const std::string& tier_name) const {
1696 auto tier_it = config_.models.tiers.find(tier_name);
1697 if (tier_it == config_.models.tiers.end()) { return 0; }
1698 const auto& tier_cfg = tier_it->second;
1699 std::error_code ec;
1700 auto weights = std::filesystem::file_size(tier_cfg.path, ec);
1701 if (ec) { return 0; }
1702 const size_t kv_per_token = 16ull * 1024ull;
1703 size_t kv = static_cast<size_t>(tier_cfg.context_length) * kv_per_token;
1704 size_t headroom = static_cast<size_t>(config_.vram_reserve_mb)
1705 * 1024ull * 1024ull;
1706 return static_cast<size_t>(weights) + kv + headroom;
1707}
1708
1715 const std::string& tier_name) const {
1716 std::lock_guard<std::mutex> lock(swap_mutex_);
1717 auto it = tier_footprint_bytes_.find(tier_name);
1718 if (it != tier_footprint_bytes_.end()) { return it->second; }
1719 size_t v = estimate_footprint_bytes(tier_name);
1720 if (v > 0) {
1721 tier_footprint_bytes_[tier_name] = v;
1722 }
1723 return v;
1724}
1725
1732 std::lock_guard<std::mutex> lock(swap_mutex_);
1733 residency_observer_ = std::move(cb);
1734}
1735
1741void ModelOrchestrator::fire_residency_observer(
1742 ResidencyEvent event,
1743 const std::string& tier_name,
1744 const std::string& model_path,
1745 size_t footprint) {
1746 const char* event_name = "unknown";
1747 switch (event) {
1748 case ResidencyEvent::Loaded: event_name = "loaded"; break;
1749 case ResidencyEvent::Evicted: event_name = "evicted"; break;
1750 case ResidencyEvent::ActivationSwap: event_name = "activation_swap"; break;
1751 }
1752 logger->info("[residency] {} tier='{}' path='{}' footprint={} bytes",
1753 event_name, tier_name, model_path, footprint);
1754 if (residency_observer_) {
1755 residency_observer_(event, tier_name, model_path, footprint);
1756 }
1757}
1758
1776static nlohmann::json make_residency_entry(
1777 const std::string& name, const std::filesystem::path& path,
1778 int context_length, size_t footprint, int vram_reserve_mb,
1779 long long last_ms) {
1780 std::error_code ec;
1781 auto weights = std::filesystem::file_size(path, ec);
1782 size_t weights_b = ec ? 0u : static_cast<size_t>(weights);
1783 size_t kv = static_cast<size_t>(context_length) * 16ull * 1024ull;
1784 size_t headroom = static_cast<size_t>(vram_reserve_mb)
1785 * 1024ull * 1024ull;
1786 return {
1787 {"tier", name},
1788 {"model_path", path.string()},
1789 {"footprint_bytes", footprint},
1790 {"weights_bytes", weights_b},
1791 {"kv_cache_bytes", kv},
1792 {"headroom_bytes", headroom},
1793 {"last_activation_ms", last_ms}
1794 };
1795}
1796
1803 std::lock_guard<std::mutex> lock(swap_mutex_);
1804 nlohmann::json j;
1805 j["vram_total_bytes"] = vram_budget_bytes_;
1806 j["vram_budget_bytes"] = vram_budget_bytes_;
1807 size_t in_use = 0;
1808 nlohmann::json arr = nlohmann::json::array();
1809 for (const auto& [name, backend] : tiers_) {
1810 if (!backend || !backend->is_loaded()) { continue; }
1811 auto tier_it = config_.models.tiers.find(name);
1812 if (tier_it == config_.models.tiers.end()) { continue; }
1813 auto fp_it = tier_footprint_bytes_.find(name);
1814 size_t footprint = (fp_it != tier_footprint_bytes_.end())
1815 ? fp_it->second : estimate_footprint_bytes(name);
1816 in_use += footprint;
1817 auto la = tier_last_activation_ms_.find(name);
1818 long long last_ms = (la != tier_last_activation_ms_.end())
1819 ? la->second : 0;
1820 arr.push_back(make_residency_entry(
1821 name, tier_it->second.path, tier_it->second.context_length,
1822 footprint, config_.vram_reserve_mb, last_ms));
1823 }
1824 j["residency"] = std::move(arr);
1825 j["vram_headroom_bytes"] = vram_budget_bytes_ > in_use
1826 ? vram_budget_bytes_ - in_use
1827 : 0u;
1828 j["backend"] = vram_budget_bytes_ > 0 ? "configured" : "unknown";
1829 return j.dump();
1830}
1831
1832} // namespace entropic
ChatAdapter concrete base class.
Adapter factory — create adapters by name.
bool swap(const std::string &name, llama_context *ctx)
Swap to a different adapter atomically.
std::string active_adapter() const
Get the currently HOT adapter name.
void unload_all_for_model(llama_model *model, llama_context *ctx)
Unload all adapters for a given base model.
void deactivate(llama_context *ctx)
Deactivate current HOT adapter (HOT -> WARM).
bool load(const std::string &name, const std::filesystem::path &adapter_path, llama_model *model, float scale=1.0f)
Load a LoRA adapter into RAM (COLD -> WARM).
void unload_all()
Free every loaded adapter handle (gh#58 close-out, v2.3.0).
Concrete base class for chat format adapters (80% logic).
virtual ParseResult parse_tool_calls(const std::string &content) const =0
Parse tool calls from model output.
size_t load_bundled(const std::filesystem::path &grammar_dir)
Load all bundled grammars from a directory.
std::string get(const std::string &key) const
Get GBNF content string for a grammar key.
Concrete base class for inference backends (80% logic).
Definition backend.h:69
BackendInfo info() const
Get backend metadata.
Definition backend.cpp:540
std::vector< GenerationResult > generate_batch(const std::vector< std::vector< Message > > &requests, const std::vector< GenerationParams > &params, std::atomic< bool > &cancel)
Generate N independent same-prefix requests together.
Definition backend.cpp:235
GenerationResult generate(const std::vector< Message > &messages, const GenerationParams &params)
Generate a complete response.
Definition backend.cpp:182
GenerationResult generate_streaming(const std::vector< Message > &messages, const GenerationParams &params, std::function< void(std::string_view token)> on_token, std::atomic< bool > &cancel)
Generate with per-token streaming callback.
Definition backend.cpp:265
LlamaCppBackend — common llama.cpp patterns (15% layer).
llama_model * llama_model_ptr()
Get the loaded llama_model pointer.
SpeculativeCompatInfo check_speculative_compat() const
Check whether the currently-configured target/draft pair is compatible for speculative decoding.
std::vector< std::string > available_models() const
All configured tier names.
size_t load_grammars_from(const std::filesystem::path &grammar_dir)
Load grammars from an explicit directory path.
GenerationResult generate_streaming(const std::vector< Message > &messages, const GenerationParams &params, std::function< void(std::string_view)> on_token, std::atomic< bool > &cancel, const std::string &tier_name="")
Streaming generation.
std::vector< std::string > loaded_models() const
Currently loaded model tier names.
bool initialize(const ParsedConfig &config)
Initialize from parsed config.
bool has_vision_capable_tier() const
Return true if any configured tier declares the "vision" capability (gh#41, v2.1.8).
size_t tier_footprint_bytes(const std::string &tier_name) const
Estimated VRAM footprint for a given tier in bytes.
void shutdown()
Shutdown — unload all models.
RoutingResult last_routing_result() const
Last routing result.
std::function< void(ResidencyEvent event, const std::string &tier_name, const std::string &model_path, size_t footprint)> ResidencyObserverFn
Residency observer callback type (internal C++ form).
GenerationResult generate(const std::vector< Message > &messages, const GenerationParams &params, const std::string &tier_name="")
Generate using routed or explicit tier.
void clear_all_prompt_caches()
Invalidate prompt/KV caches across every pooled backend.
std::string route(const std::vector< Message > &messages)
Route to tier using router model.
ChatAdapter * get_adapter(const std::string &tier_name) const
Get adapter for a tier.
void set_residency_observer(ResidencyObserverFn cb)
Register a residency observer.
std::string last_used_tier() const
Last used tier name.
~ModelOrchestrator()
Destructor — invokes shutdown() and AdapterManager::unload_all().
std::vector< GenerationResult > generate_batch(const std::vector< std::vector< Message > > &messages_list, const std::vector< GenerationParams > &params_list, const std::vector< std::string > &tiers, std::atomic< bool > &cancel)
Same-prefix batch generation on a shared resident model (gh#98).
std::string select_vision_tier() const
Pick the canonical vision-capable tier name (gh#41).
bool can_handoff(const std::string &from, const std::string &to) const
Check if handoff is permitted.
std::string residency_snapshot_json() const
Serialize the current residency set as a JSON string.
InferenceBackend * get_backend(const std::string &tier_name) const
Get the inference backend for a tier (for evaluation APIs).
void clear_all_prompt_caches()
Fanout: clear prompt/KV cache on every loaded backend.
bool is_loaded(const std::string &role) const
Check whether a role is currently loaded and active.
InferenceBackend * get(const std::string &role) const
Get the backend for a role.
bool ensure_loaded(const std::string &role, const ModelConfig &config)
Lazily load and activate a model for a role.
@ ENTROPIC_OK
Success.
Definition error.h:36
@ ENTROPIC_ERROR_TIER_MODEL_TOO_LARGE
A single tier's model weights+KV exceed the engine's VRAM budget; eviction cannot help (v2....
Definition error.h:89
@ ENTROPIC_ERROR_NOT_SUPPORTED
Capability not supported by this backend (v1.9.13)
Definition error.h:84
@ ENTROPIC_ERROR_GENERATE_FAILED
Generation failed (context overflow, model error)
Definition error.h:42
Pure C interface contract for inference backends.
void entropic_inference_log_to_file(const char *path)
Redirect llama/ggml logs to a file.
LlamaCppBackend — llama.cpp C API integration.
spdlog initialization and logger access.
auto now()
Get current time for timing measurements.
Definition logging.h:193
ENTROPIC_EXPORT std::shared_ptr< spdlog::logger > get(const std::string &name)
Get or create a named logger.
Definition logging.cpp:211
double elapsed_ms(std::chrono::steady_clock::time_point start, std::chrono::steady_clock::time_point end)
Compute elapsed milliseconds between two time points.
Definition logging.h:203
Activate model on GPU (WARM → ACTIVE).
static void stage_active_tools(InferenceBackend *model, const GenerationParams &params)
Stage the turn's tool defs on the backend for common_chat (gh#87).
static nlohmann::json make_residency_entry(const std::string &name, const std::filesystem::path &path, int context_length, size_t footprint, int vram_reserve_mb, long long last_ms)
JSON serialization of the current residency set.
void apply_action_envelope_recovery(std::vector< ToolCall > &calls, const std::string &raw)
gh#88: substitute recovered bare-JSON calls when a reliable (PEG_GEMMA4 / gemma) parse produced none;...
@ ok
Tool dispatched, returned non-empty content.
static void log_orchestration(const GenerationResult &result, const std::string &selected, const std::string &adapter_name, const GenerationParams &params, double routing_ms, double swap_ms)
Log the per-orchestration tier/adapter/timing summary.
static llama_model * resolve_target_model(const std::shared_ptr< InferenceBackend > &tier_backend)
Resolve the active main-tier llama_model* for compat lookup.
std::unique_ptr< ChatAdapter > create_adapter(const std::string &name, const std::string &tier_name, const std::string &identity_prompt)
Create adapter by name (gh#87 Phase D hybrid).
ENTROPIC_EXPORT void apply_tier_sampler_overrides(GenerationParams &params, const TierSamplerOverrides &ov)
Apply per-tier sampler overrides to params.
static void apply_adapter_parse(InferenceBackend *model, ChatAdapter *adapter, GenerationResult &result)
Split tool calls out of a result (gh#87: common_chat or adapter).
static std::string normalize_grammar_key(const std::string &grammar_value)
Normalize a frontmatter grammar value to a registry key.
ModelOrchestrator — multi-model lifecycle and routing.
Tokenizer/architecture compatibility check for speculative decoding draft pairing.
Generation parameters for a single inference call.
Definition config.h:302
std::string grammar
GBNF grammar string (empty = unconstrained)
Definition config.h:359
std::string tool_call_mode
Per-call tool-call generation mode (gh#103).
Definition config.h:372
int top_k
Top-K sampling.
Definition config.h:305
float repeat_penalty
Repetition penalty.
Definition config.h:306
std::string tools
Active tool definitions for this turn, as an MCP tool-list JSON array ([{name, description,...
Definition config.h:411
float temperature
Sampling temperature.
Definition config.h:303
std::string grammar_key
Grammar registry key.
Definition config.h:364
float frequency_penalty
Frequency-penalty term in llama.cpp's penalties sampler (gh#23 MVP item 3).
Definition config.h:349
float presence_penalty
Presence-penalty term in llama.cpp's penalties sampler (gh#23 MVP item 2).
Definition config.h:322
bool enable_thinking
Enable <think> blocks (false if reasoning_budget == 0)
Definition config.h:358
float min_p
Min-p nucleus sampling threshold (gh#23 MVP item 1).
Definition config.h:315
int max_tokens
Maximum tokens to generate.
Definition config.h:351
float top_p
Nucleus sampling threshold.
Definition config.h:304
Result of a single generation call.
entropic_error_t error_code
Error code (ENTROPIC_OK if no error)
double swap_ms
Model swap time.
double routing_ms
Router classification time.
double generation_time_ms
Wall-clock generation time.
std::string raw_content
Raw model output before adapter processing.
std::string finish_reason
Finish reason: "stop", "length", "error".
std::string content
Generated text (cleaned by adapter)
std::vector< ToolCall > tool_calls
Tool calls parsed from content.
std::string error_message
Error description (empty if no error)
double total_ms
Total end-to-end time.
SpeculativeConfig speculative
Speculative decoding (gh#36)
Definition config.h:918
std::filesystem::path path
Resolved model file path.
Definition config.h:149
Result of a speculative-decoding compatibility check.
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
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
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
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
std::filesystem::path config_dir
Config dir — base for bundled data discovery.
Definition config.h:953
std::string fallback_tier
Fallback when routing fails.
Definition config.h:579
bool enabled
Enable routing.
Definition config.h:578
std::optional< std::string > classification_prompt
Custom prompt (nullopt = auto)
Definition config.h:580
Result metadata from a routing decision.
std::string adapter_name
Active adapter (empty = base model) (v1.9.2)
std::string swap_action
"none", "reused", "loaded"
double adapter_swap_ms
Adapter swap latency (v1.9.2)
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
Per-tier sampler overrides parsed from identity frontmatter.
std::optional< float > top_p
gh#85
std::optional< float > temperature
gh#82
std::optional< float > min_p
gh#85
std::optional< float > presence_penalty
gh#85
std::optional< std::string > tool_call_mode
gh#103
std::optional< float > frequency_penalty
gh#85
std::optional< int > top_k
gh#85
std::optional< bool > enable_thinking
gh#86
std::optional< float > repeat_penalty
gh#86
std::optional< int > max_output_tokens
gh#82