Entropic 2.14.3
Local-first agentic inference engine
Loading...
Searching...
No Matches
interface_factory.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
12#include <entropic/inference/adapters/adapter_base.h> // gh#88 recovery
13
14#include "llama_cpp_backend.h"
15#include "response_parse.h" // gh#87 3b: common_chat parse routing
16#include "tool_call_serialize.h" // gh#93: shared (typed) tool-call serialization
17
18#include <nlohmann/json.hpp>
19
20#include <atomic>
21#include <chrono>
22#include <cstdlib>
23#include <cstring>
24#include <string>
25#include <thread>
26#include <vector>
27
28namespace entropic {
29
30// ── Context struct for callbacks ───────────────────────────
31
46
47// ── JSON helpers ───────────────────────────────────────────
48
56static std::vector<Message> parse_msgs(const char* json_str) {
57 std::vector<Message> msgs;
58 if (!json_str) { return msgs; }
59 auto arr = nlohmann::json::parse(json_str, nullptr, false);
60 if (!arr.is_array()) { return msgs; }
61 for (const auto& obj : arr) {
62 Message m;
63 m.role = obj.value("role", "");
64 m.content = obj.value("content", "");
65 msgs.push_back(std::move(m));
66 }
67 return msgs;
68}
69
78template <typename T>
79static void assign_if_present(const nlohmann::json& j,
80 const char* key, T& dst) {
81 if (j.contains(key)) { dst = j[key].get<T>(); }
82}
83
93 const nlohmann::json& j,
94 std::unordered_map<int32_t, float>& dst)
95{
96 if (!j.contains("logit_bias") || !j["logit_bias"].is_object()) {
97 return;
98 }
99 for (auto it = j["logit_bias"].begin(); it != j["logit_bias"].end(); ++it) {
100 try {
101 dst[std::stoi(it.key())] = it.value().get<float>();
102 } catch (const std::exception&) {
103 // skip un-parseable keys
104 }
105 }
106}
107
117 if (!json_str) { return p; }
118 auto j = nlohmann::json::parse(json_str, nullptr, false);
119 if (!j.is_object()) { return p; }
120 assign_if_present(j, "max_tokens", p.max_tokens);
121 assign_if_present(j, "temperature", p.temperature);
122 assign_if_present(j, "grammar_key", p.grammar_key);
123 // gh#144 (v2.12.0): caller-scoped session; absent means the default.
124 assign_if_present(j, "session", p.session_key);
125 assign_if_present(j, "enable_thinking", p.enable_thinking);
126 assign_if_present(j, "top_p", p.top_p);
127 assign_if_present(j, "top_k", p.top_k);
128 assign_if_present(j, "min_p", p.min_p);
129 assign_if_present(j, "presence_penalty", p.presence_penalty);
130 assign_if_present(j, "frequency_penalty",p.frequency_penalty);
131 assign_if_present(j, "repeat_penalty", p.repeat_penalty);
132 assign_if_present(j, "seed", p.seed);
133 assign_if_present(j, "tools", p.tools); // gh#87 3b
134 parse_logit_bias_into(j, p.logit_bias);
135 return p;
136}
137
146static std::string extract_tier(const char* json_str,
147 const std::string& default_tier) {
148 if (!json_str) { return default_tier; }
149 auto j = nlohmann::json::parse(json_str, nullptr, false);
150 if (j.is_object() && j.contains("tier")) {
151 return j["tier"].get<std::string>();
152 }
153 return default_tier;
154}
155
163static char* dup(const std::string& s) {
164 return strdup(s.c_str());
165}
166
167// ── C-callable wrappers ────────────────────────────────────
168
174static int iface_generate(const char* msgs_json,
175 const char* params_json,
176 char** result_json,
177 void* user_data) {
178 auto* ctx = static_cast<InterfaceContext*>(user_data);
179 auto messages = parse_msgs(msgs_json);
180 auto params = parse_params(params_json);
181 auto tier = extract_tier(params_json, ctx->default_tier);
182 auto result = ctx->orchestrator->generate(
183 messages, params, tier);
184 auto& out = result.raw_content.empty()
185 ? result.content : result.raw_content;
186 *result_json = dup(out);
187 return 0;
188}
189
206 const char* msgs_json, const char* params_json,
207 void (*on_token)(const char*, size_t, void*),
208 void* token_ud, int* cancel, void* user_data) {
209 auto* ctx = static_cast<InterfaceContext*>(user_data);
210 auto messages = parse_msgs(msgs_json);
211 auto params = parse_params(params_json);
212 std::atomic<bool> cancel_flag(false);
213 auto cb = [on_token, token_ud, cancel, &cancel_flag]
214 (std::string_view tok) {
215 on_token(tok.data(), tok.size(), token_ud);
216 if (cancel != nullptr && *cancel != 0) {
217 cancel_flag.store(true, std::memory_order_release);
218 }
219 };
220 auto tier = extract_tier(params_json, ctx->default_tier);
221 ctx->orchestrator->generate_streaming(
222 messages, params, cb, cancel_flag, tier);
223 return 0;
224}
225
238 const char* msgs_json, const char* params_json,
239 char** result_json, int* cancel, void* user_data) {
240 auto* ctx = static_cast<InterfaceContext*>(user_data);
241 auto messages = parse_msgs(msgs_json);
242 auto params = parse_params(params_json);
243 auto tier = extract_tier(params_json, ctx->default_tier);
244
245 std::atomic<bool> cancel_flag(false);
246 std::atomic<bool> done(false);
247 std::thread poller;
248 if (cancel != nullptr) {
249 poller = std::thread([cancel, &cancel_flag, &done]() {
250 while (!done.load(std::memory_order_acquire)) {
251 if (*cancel != 0) {
252 cancel_flag.store(true, std::memory_order_release);
253 return;
254 }
255 std::this_thread::sleep_for(
256 std::chrono::milliseconds(10));
257 }
258 });
259 }
260
261 auto result = ctx->orchestrator->generate(
262 messages, params, cancel_flag, tier);
263
264 done.store(true, std::memory_order_release);
265 if (poller.joinable()) { poller.join(); }
266
267 auto& out = result.raw_content.empty()
268 ? result.content : result.raw_content;
269 *result_json = dup(out);
270 return 0;
271}
272
278static int iface_route(const char* msgs_json,
279 char** result_json, void* user_data) {
280 auto* ctx = static_cast<InterfaceContext*>(user_data);
281 auto messages = parse_msgs(msgs_json);
282 auto tier = ctx->orchestrator->route(messages);
283 *result_json = dup(tier);
284 return 0;
285}
286
292static int iface_complete(const char* prompt,
293 const char* params_json,
294 char** result_json, void* user_data) {
295 auto* ctx = static_cast<InterfaceContext*>(user_data);
296 auto tier = extract_tier(params_json, ctx->default_tier);
297 Message msg;
298 msg.role = "user";
299 msg.content = prompt;
300 GenerationParams params{};
301 params.max_tokens = 1;
302 auto result = ctx->orchestrator->generate(
303 {msg}, params, tier);
304 *result_json = dup(result.content);
305 return 0;
306}
307
314static std::vector<ToolCall> tc_from_json_obj(const nlohmann::json& j) {
315 // j.value() returns default when key absent or wrong type — fewer branches.
316 std::string name = j.value("name", std::string{});
317 if (name.empty()) { name = j.value("tool", std::string{}); }
318
319 auto it = j.find("arguments");
320 if (it == j.end() || !it->is_object()) { it = j.find("parameters"); }
321 const nlohmann::json* args =
322 (it != j.end() && it->is_object()) ? &(*it) : nullptr;
323
324 std::vector<ToolCall> result;
325 if (!name.empty() && args != nullptr) {
326 ToolCall tc;
327 tc.name = std::move(name);
328 for (const auto& [k, v] : args->items()) {
329 tc.arguments[k] = v.is_string() ? v.get<std::string>() : v.dump();
330 }
331 result.push_back(std::move(tc));
332 }
333 return result;
334}
335
349static std::vector<ToolCall> try_fenced_json_call(const std::string& raw) {
350 const std::string kOpen = "```json";
351 const std::string kClose = "```";
352 auto ob = raw.find(kOpen);
353 auto bs = (ob != std::string::npos) ? (ob + kOpen.size()) : std::string::npos;
354 auto cb = (bs != std::string::npos) ? raw.find(kClose, bs) : std::string::npos;
355 bool single = (ob != std::string::npos) && (cb != std::string::npos)
356 && (raw.find(kOpen, ob + 1) == std::string::npos
357 || raw.find(kOpen, ob + 1) > cb);
358 if (!single) { return {}; }
359 auto j = nlohmann::json::parse(raw.substr(bs, cb - bs), nullptr, false);
360 if (!j.is_object()) { return {}; }
361 return tc_from_json_obj(j);
362}
363
377static std::string find_unique_args_match(
378 const nlohmann::json& obj,
379 const nlohmann::json& tools) {
380 std::string matched;
381 for (const auto& t : tools) {
382 auto name = t.value("name", std::string{});
383 if (name.empty()) { continue; }
384 auto req = t.value("inputSchema", nlohmann::json{})
385 .value("required", nlohmann::json::array());
386 bool ok = true;
387 for (const auto& r : req) {
388 if (!r.is_string()
389 || !obj.contains(r.get<std::string>())) {
390 ok = false; break;
391 }
392 }
393 if (!ok) { continue; }
394 if (!matched.empty()) { return {}; }
395 matched = name;
396 }
397 return matched;
398}
399
413static std::optional<nlohmann::json> extract_args_only_fence(
414 const std::string& raw) {
415 const std::string kOpen = "```json";
416 const std::string kClose = "```";
417 auto ob = raw.find(kOpen);
418 auto bs = ob != std::string::npos ? ob + kOpen.size() : std::string::npos;
419 auto cb = bs != std::string::npos ? raw.find(kClose, bs) : std::string::npos;
420 bool single = ob != std::string::npos && cb != std::string::npos
421 && (raw.find(kOpen, ob + 1) == std::string::npos
422 || raw.find(kOpen, ob + 1) > cb);
423 if (!single) { return std::nullopt; }
424 auto j = nlohmann::json::parse(raw.substr(bs, cb - bs), nullptr, false);
425 if (!j.is_object() || j.contains("name") || j.contains("tool")) {
426 return std::nullopt;
427 }
428 return j;
429}
430
446static std::vector<ToolCall> try_fenced_args_object_call(
447 const std::string& raw,
448 const std::string& tools_json) {
449 auto j_opt = extract_args_only_fence(raw);
450 if (!j_opt) { return {}; }
451 auto tools = nlohmann::json::parse(tools_json, nullptr, false);
452 auto name = tools.is_array()
453 ? find_unique_args_match(*j_opt, tools) : std::string{};
454 if (name.empty()) { return {}; }
455 ToolCall tc;
456 tc.name = std::move(name);
457 for (const auto& [k, v] : j_opt->items()) {
458 tc.arguments[k] = v.is_string() ? v.get<std::string>() : v.dump();
459 }
460 return {std::move(tc)};
461}
462
476static void apply_fenced_fallbacks(const std::string& raw,
477 LlamaCppBackend* llama,
478 std::vector<ToolCall>& calls) {
479 if (!calls.empty()) { return; }
480 auto fb = try_fenced_json_call(raw);
481 if (fb.empty() && llama != nullptr) {
483 }
484 if (!fb.empty()) { calls = std::move(fb); }
485}
486
503 const std::string& raw,
504 const std::string& tier,
505 std::string& out_cleaned,
506 std::vector<ToolCall>& out_calls) {
507 auto* adapter = orch->get_adapter(tier);
508 if (adapter == nullptr) { out_cleaned = raw; return; }
509 auto p = adapter->parse_tool_calls(raw);
510 out_cleaned = std::move(p.cleaned_content);
511 out_calls = std::move(p.tool_calls);
512}
513
543static int iface_parse_tool_calls(const char* raw,
544 char** cleaned,
545 char** tool_calls_json,
546 void* user_data) {
547 auto* ctx = static_cast<InterfaceContext*>(user_data);
548 std::string raw_str = raw ? raw : "";
549 auto tier = ctx->orchestrator->last_used_tier();
550 if (tier.empty()) { tier = ctx->default_tier; }
551 auto* llama = dynamic_cast<LlamaCppBackend*>(
552 ctx->orchestrator->get_backend(tier));
553
554 // gh#108 (v2.10.3): same rule as the orchestrator's buffered parse —
555 // this used to be a byte-for-byte duplicate of that branch, which is how
556 // a fix could land on one path and miss the other.
557 auto parsed = parse_model_response(
558 llama, ctx->orchestrator->get_adapter(tier), raw_str);
559 std::string cleaned_str = std::move(parsed.content);
560 std::vector<ToolCall> calls = std::move(parsed.tool_calls);
561
562 apply_fenced_fallbacks(raw_str, llama, calls); // gh#122, gh#127
563
564 *cleaned = dup(cleaned_str);
565 *tool_calls_json = dup(serialize_tool_calls(calls));
566 return 0;
567}
568
574static int iface_is_complete(const char* /*content*/,
575 const char* tool_calls_json,
576 void* /*user_data*/) {
577 if (!tool_calls_json) { return 1; }
578 auto tc = nlohmann::json::parse(tool_calls_json, nullptr, false);
579 return (tc.is_array() && !tc.empty()) ? 0 : 1;
580}
581
582// ── Factory ────────────────────────────────────────────────
583
593 ModelOrchestrator* orchestrator,
594 const std::string& default_tier,
595 InterfaceContext** out_context) {
596 auto* ctx = new InterfaceContext{orchestrator, default_tier};
597 if (out_context) { *out_context = ctx; }
598
599 InferenceInterface iface;
600 iface.generate = iface_generate;
601 iface.generate_cancellable = iface_generate_with_cancel; // gh#81, v2.4.2
602 iface.generate_stream = iface_generate_stream;
603 iface.route = iface_route;
604 iface.complete = iface_complete;
605 iface.parse_tool_calls = iface_parse_tool_calls;
606 iface.is_response_complete = iface_is_complete;
607 iface.free_fn = free;
608 iface.backend_data = ctx;
609 iface.orchestrator_data = ctx;
610 iface.adapter_data = ctx;
611 return iface;
612}
613
620 delete context;
621}
622
623} // namespace entropic
ChatAdapter concrete base class.
LlamaCppBackend — common llama.cpp patterns (15% layer).
const std::string & active_tools_json() const
Return the tool definitions staged for the current turn.
Multi-model lifecycle and routing orchestrator.
ChatAdapter * get_adapter(const std::string &tier_name) const
Get adapter for a tier.
Configuration structs with defaults.
int * cancel
Consumer's cancel flag, nullable.
Definition engine.cpp:4074
Factory for building InferenceInterface from a ModelOrchestrator.
LlamaCppBackend — llama.cpp C API integration.
Message struct for conversation history.
Activate model on GPU (WARM → ACTIVE).
static void assign_if_present(const nlohmann::json &j, const char *key, T &dst)
Conditionally assign a typed JSON field into a destination.
static int iface_generate_with_cancel(const char *msgs_json, const char *params_json, char **result_json, int *cancel, void *user_data)
Batch generate with cancel via orchestrator (gh#81, v2.4.2).
static int iface_parse_tool_calls(const char *raw, char **cleaned, char **tool_calls_json, void *user_data)
Parse tool calls from raw model output (gh#87 3b).
@ ok
Tool dispatched, returned non-empty content.
std::string serialize_tool_calls(const std::vector< ToolCall > &calls)
Serialize parsed tool calls to the C-ABI JSON array form.
static std::vector< Message > parse_msgs(const char *json_str)
Parse JSON message array into Message vector.
@ optional
optional: every escape asks the host's path approver
static GenerationParams parse_params(const char *json_str)
Parse generation params from JSON string.
static int iface_is_complete(const char *, const char *tool_calls_json, void *)
Check if response is complete (no pending tool calls).
static void apply_fenced_fallbacks(const std::string &raw, LlamaCppBackend *llama, std::vector< ToolCall > &calls)
Apply fenced-json and fenced-args-only fallback parsers (gh#122, gh#127).
@ tier
The TIER's identity frontmatter grammar: stem (gh#154).
static int iface_generate(const char *msgs_json, const char *params_json, char **result_json, void *user_data)
Generate via orchestrator.
static void parse_logit_bias_into(const nlohmann::json &j, std::unordered_map< int32_t, float > &dst)
Populate logit_bias map from a JSON object of token→bias.
static int iface_route(const char *msgs_json, char **result_json, void *user_data)
Route messages to tier via orchestrator.
static std::string extract_tier(const char *json_str, const std::string &default_tier)
Extract tier name from params JSON, falling back to default.
static void parse_via_adapter(ModelOrchestrator *orch, const std::string &raw, const std::string &tier, std::string &out_cleaned, std::vector< ToolCall > &out_calls)
Parse tool calls via the adapter for the given tier (gh#89).
static std::vector< ToolCall > try_fenced_json_call(const std::string &raw)
Extract a tool call from a single fenced json block in raw model output (gh#122).
void destroy_orchestrator_interface(InterfaceContext *context)
Free a context returned by build_orchestrator_interface().
static int iface_generate_stream(const char *msgs_json, const char *params_json, void(*on_token)(const char *, size_t, void *), void *token_ud, int *cancel, void *user_data)
Streaming generate via orchestrator.
static std::optional< nlohmann::json > extract_args_only_fence(const std::string &raw)
Extract and validate the fenced JSON block for args-only inference.
static std::vector< ToolCall > tc_from_json_obj(const nlohmann::json &j)
Build a ToolCall from a parsed JSON object with name/tool + arguments/parameters.
static int iface_complete(const char *prompt, const char *params_json, char **result_json, void *user_data)
Raw text completion via orchestrator.
static char * dup(const std::string &s)
Heap-allocate a C string copy.
ParsedModelResponse parse_model_response(LlamaCppBackend *llama, ChatAdapter *adapter, const std::string &raw)
Parse a raw emission: template first, adapter second.
static std::vector< ToolCall > try_fenced_args_object_call(const std::string &raw, const std::string &tools_json)
Try to synthesise a tool call from a fenced args-only JSON object (gh#127).
InferenceInterface build_orchestrator_interface(ModelOrchestrator *orchestrator, const std::string &default_tier, InterfaceContext **out_context)
Build an InferenceInterface wired to an orchestrator.
static std::string find_unique_args_match(const nlohmann::json &obj, const nlohmann::json &tools)
Find the unique tool whose required params are all present in obj.
ModelOrchestrator — multi-model lifecycle and routing.
One template-first / adapter-second parse rule for raw model output.
Generation parameters for a single inference call.
Definition config.h:389
int max_tokens
Maximum tokens to generate.
Definition config.h:447
Holds orchestrator + tier for C callback user_data.
ModelOrchestrator * orchestrator
Orchestrator pointer.
std::string default_tier
Default tier name.
A message in a conversation.
Definition message.h:36
std::string content
Message text content (always populated)
Definition message.h:38
std::string role
Message role.
Definition message.h:37
A tool call request parsed from model output.
Definition tool_call.h:31
std::unordered_map< std::string, std::string > arguments
Tool arguments as string key-value pairs.
Definition tool_call.h:34
std::string name
Tool name (e.g. "filesystem.read_file")
Definition tool_call.h:33
Shared serialization of parsed tool calls to the C-ABI JSON array form.