Entropic 2.11.1
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
115static GenerationParams parse_params(const char* json_str) {
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 assign_if_present(j, "enable_thinking", p.enable_thinking);
124 assign_if_present(j, "top_p", p.top_p);
125 assign_if_present(j, "top_k", p.top_k);
126 assign_if_present(j, "min_p", p.min_p);
127 assign_if_present(j, "presence_penalty", p.presence_penalty);
128 assign_if_present(j, "frequency_penalty",p.frequency_penalty);
129 assign_if_present(j, "repeat_penalty", p.repeat_penalty);
130 assign_if_present(j, "seed", p.seed);
131 assign_if_present(j, "tools", p.tools); // gh#87 3b
133 return p;
134}
135
144static std::string extract_tier(const char* json_str,
145 const std::string& default_tier) {
146 if (!json_str) { return default_tier; }
147 auto j = nlohmann::json::parse(json_str, nullptr, false);
148 if (j.is_object() && j.contains("tier")) {
149 return j["tier"].get<std::string>();
150 }
151 return default_tier;
152}
153
161static char* dup(const std::string& s) {
162 return strdup(s.c_str());
163}
164
165// ── C-callable wrappers ────────────────────────────────────
166
172static int iface_generate(const char* msgs_json,
173 const char* params_json,
174 char** result_json,
175 void* user_data) {
176 auto* ctx = static_cast<InterfaceContext*>(user_data);
177 auto messages = parse_msgs(msgs_json);
178 auto params = parse_params(params_json);
179 auto tier = extract_tier(params_json, ctx->default_tier);
180 auto result = ctx->orchestrator->generate(
181 messages, params, tier);
182 auto& out = result.raw_content.empty()
183 ? result.content : result.raw_content;
184 *result_json = dup(out);
185 return 0;
186}
187
204 const char* msgs_json, const char* params_json,
205 void (*on_token)(const char*, size_t, void*),
206 void* token_ud, int* cancel, void* user_data) {
207 auto* ctx = static_cast<InterfaceContext*>(user_data);
208 auto messages = parse_msgs(msgs_json);
209 auto params = parse_params(params_json);
210 std::atomic<bool> cancel_flag(false);
211 auto cb = [on_token, token_ud, cancel, &cancel_flag]
212 (std::string_view tok) {
213 on_token(tok.data(), tok.size(), token_ud);
214 if (cancel != nullptr && *cancel != 0) {
215 cancel_flag.store(true, std::memory_order_release);
216 }
217 };
218 auto tier = extract_tier(params_json, ctx->default_tier);
219 ctx->orchestrator->generate_streaming(
220 messages, params, cb, cancel_flag, tier);
221 return 0;
222}
223
236 const char* msgs_json, const char* params_json,
237 char** result_json, int* cancel, void* user_data) {
238 auto* ctx = static_cast<InterfaceContext*>(user_data);
239 auto messages = parse_msgs(msgs_json);
240 auto params = parse_params(params_json);
241 auto tier = extract_tier(params_json, ctx->default_tier);
242
243 std::atomic<bool> cancel_flag(false);
244 std::atomic<bool> done(false);
245 std::thread poller;
246 if (cancel != nullptr) {
247 poller = std::thread([cancel, &cancel_flag, &done]() {
248 while (!done.load(std::memory_order_acquire)) {
249 if (*cancel != 0) {
250 cancel_flag.store(true, std::memory_order_release);
251 return;
252 }
253 std::this_thread::sleep_for(
254 std::chrono::milliseconds(10));
255 }
256 });
257 }
258
259 auto result = ctx->orchestrator->generate(
260 messages, params, cancel_flag, tier);
261
262 done.store(true, std::memory_order_release);
263 if (poller.joinable()) { poller.join(); }
264
265 auto& out = result.raw_content.empty()
266 ? result.content : result.raw_content;
267 *result_json = dup(out);
268 return 0;
269}
270
276static int iface_route(const char* msgs_json,
277 char** result_json, void* user_data) {
278 auto* ctx = static_cast<InterfaceContext*>(user_data);
279 auto messages = parse_msgs(msgs_json);
280 auto tier = ctx->orchestrator->route(messages);
281 *result_json = dup(tier);
282 return 0;
283}
284
290static int iface_complete(const char* prompt,
291 const char* params_json,
292 char** result_json, void* user_data) {
293 auto* ctx = static_cast<InterfaceContext*>(user_data);
294 auto tier = extract_tier(params_json, ctx->default_tier);
295 Message msg;
296 msg.role = "user";
297 msg.content = prompt;
298 GenerationParams params{};
299 params.max_tokens = 1;
300 auto result = ctx->orchestrator->generate(
301 {msg}, params, tier);
302 *result_json = dup(result.content);
303 return 0;
304}
305
312static std::vector<ToolCall> tc_from_json_obj(const nlohmann::json& j) {
313 // j.value() returns default when key absent or wrong type — fewer branches.
314 std::string name = j.value("name", std::string{});
315 if (name.empty()) { name = j.value("tool", std::string{}); }
316
317 auto it = j.find("arguments");
318 if (it == j.end() || !it->is_object()) { it = j.find("parameters"); }
319 const nlohmann::json* args =
320 (it != j.end() && it->is_object()) ? &(*it) : nullptr;
321
322 std::vector<ToolCall> result;
323 if (!name.empty() && args != nullptr) {
324 ToolCall tc;
325 tc.name = std::move(name);
326 for (const auto& [k, v] : args->items()) {
327 tc.arguments[k] = v.is_string() ? v.get<std::string>() : v.dump();
328 }
329 result.push_back(std::move(tc));
330 }
331 return result;
332}
333
347static std::vector<ToolCall> try_fenced_json_call(const std::string& raw) {
348 const std::string kOpen = "```json";
349 const std::string kClose = "```";
350 auto ob = raw.find(kOpen);
351 auto bs = (ob != std::string::npos) ? (ob + kOpen.size()) : std::string::npos;
352 auto cb = (bs != std::string::npos) ? raw.find(kClose, bs) : std::string::npos;
353 bool single = (ob != std::string::npos) && (cb != std::string::npos)
354 && (raw.find(kOpen, ob + 1) == std::string::npos
355 || raw.find(kOpen, ob + 1) > cb);
356 if (!single) { return {}; }
357 auto j = nlohmann::json::parse(raw.substr(bs, cb - bs), nullptr, false);
358 if (!j.is_object()) { return {}; }
359 return tc_from_json_obj(j);
360}
361
375static std::string find_unique_args_match(
376 const nlohmann::json& obj,
377 const nlohmann::json& tools) {
378 std::string matched;
379 for (const auto& t : tools) {
380 auto name = t.value("name", std::string{});
381 if (name.empty()) { continue; }
382 auto req = t.value("inputSchema", nlohmann::json{})
383 .value("required", nlohmann::json::array());
384 bool ok = true;
385 for (const auto& r : req) {
386 if (!r.is_string()
387 || !obj.contains(r.get<std::string>())) {
388 ok = false; break;
389 }
390 }
391 if (!ok) { continue; }
392 if (!matched.empty()) { return {}; }
393 matched = name;
394 }
395 return matched;
396}
397
411static std::optional<nlohmann::json> extract_args_only_fence(
412 const std::string& raw) {
413 const std::string kOpen = "```json";
414 const std::string kClose = "```";
415 auto ob = raw.find(kOpen);
416 auto bs = ob != std::string::npos ? ob + kOpen.size() : std::string::npos;
417 auto cb = bs != std::string::npos ? raw.find(kClose, bs) : std::string::npos;
418 bool single = ob != std::string::npos && cb != std::string::npos
419 && (raw.find(kOpen, ob + 1) == std::string::npos
420 || raw.find(kOpen, ob + 1) > cb);
421 if (!single) { return std::nullopt; }
422 auto j = nlohmann::json::parse(raw.substr(bs, cb - bs), nullptr, false);
423 if (!j.is_object() || j.contains("name") || j.contains("tool")) {
424 return std::nullopt;
425 }
426 return j;
427}
428
444static std::vector<ToolCall> try_fenced_args_object_call(
445 const std::string& raw,
446 const std::string& tools_json) {
447 auto j_opt = extract_args_only_fence(raw);
448 if (!j_opt) { return {}; }
449 auto tools = nlohmann::json::parse(tools_json, nullptr, false);
450 auto name = tools.is_array()
451 ? find_unique_args_match(*j_opt, tools) : std::string{};
452 if (name.empty()) { return {}; }
453 ToolCall tc;
454 tc.name = std::move(name);
455 for (const auto& [k, v] : j_opt->items()) {
456 tc.arguments[k] = v.is_string() ? v.get<std::string>() : v.dump();
457 }
458 return {std::move(tc)};
459}
460
474static void apply_fenced_fallbacks(const std::string& raw,
475 LlamaCppBackend* llama,
476 std::vector<ToolCall>& calls) {
477 if (!calls.empty()) { return; }
478 auto fb = try_fenced_json_call(raw);
479 if (fb.empty() && llama != nullptr) {
481 }
482 if (!fb.empty()) { calls = std::move(fb); }
483}
484
501 const std::string& raw,
502 const std::string& tier,
503 std::string& out_cleaned,
504 std::vector<ToolCall>& out_calls) {
505 auto* adapter = orch->get_adapter(tier);
506 if (adapter == nullptr) { out_cleaned = raw; return; }
507 auto p = adapter->parse_tool_calls(raw);
508 out_cleaned = std::move(p.cleaned_content);
509 out_calls = std::move(p.tool_calls);
510}
511
541static int iface_parse_tool_calls(const char* raw,
542 char** cleaned,
543 char** tool_calls_json,
544 void* user_data) {
545 auto* ctx = static_cast<InterfaceContext*>(user_data);
546 std::string raw_str = raw ? raw : "";
547 auto tier = ctx->orchestrator->last_used_tier();
548 if (tier.empty()) { tier = ctx->default_tier; }
549 auto* llama = dynamic_cast<LlamaCppBackend*>(
550 ctx->orchestrator->get_backend(tier));
551
552 // gh#108 (v2.10.3): same rule as the orchestrator's buffered parse —
553 // this used to be a byte-for-byte duplicate of that branch, which is how
554 // a fix could land on one path and miss the other.
555 auto parsed = parse_model_response(
556 llama, ctx->orchestrator->get_adapter(tier), raw_str);
557 std::string cleaned_str = std::move(parsed.content);
558 std::vector<ToolCall> calls = std::move(parsed.tool_calls);
559
560 apply_fenced_fallbacks(raw_str, llama, calls); // gh#122, gh#127
561
562 *cleaned = dup(cleaned_str);
563 *tool_calls_json = dup(serialize_tool_calls(calls));
564 return 0;
565}
566
572static int iface_is_complete(const char* /*content*/,
573 const char* tool_calls_json,
574 void* /*user_data*/) {
575 if (!tool_calls_json) { return 1; }
576 auto tc = nlohmann::json::parse(tool_calls_json, nullptr, false);
577 return (tc.is_array() && !tc.empty()) ? 0 : 1;
578}
579
580// ── Factory ────────────────────────────────────────────────
581
591 ModelOrchestrator* orchestrator,
592 const std::string& default_tier,
593 InterfaceContext** out_context) {
594 auto* ctx = new InterfaceContext{orchestrator, default_tier};
595 if (out_context) { *out_context = ctx; }
596
597 InferenceInterface iface;
598 iface.generate = iface_generate;
599 iface.generate_cancellable = iface_generate_with_cancel; // gh#81, v2.4.2
600 iface.generate_stream = iface_generate_stream;
601 iface.route = iface_route;
602 iface.complete = iface_complete;
603 iface.parse_tool_calls = iface_parse_tool_calls;
604 iface.is_response_complete = iface_is_complete;
605 iface.free_fn = free;
606 iface.backend_data = ctx;
607 iface.orchestrator_data = ctx;
608 iface.adapter_data = ctx;
609 return iface;
610}
611
618 delete context;
619}
620
621} // 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.
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.
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).
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:313
int top_k
Top-K sampling.
Definition config.h:316
std::unordered_map< int32_t, float > logit_bias
Per-token logit bias map (gh#23 MVP item 4).
Definition config.h:347
float repeat_penalty
Repetition penalty.
Definition config.h:317
std::string tools
Active tool definitions for this turn, as an MCP tool-list JSON array ([{name, description,...
Definition config.h:422
float temperature
Sampling temperature.
Definition config.h:314
std::string grammar_key
Grammar registry key.
Definition config.h:375
float frequency_penalty
Frequency-penalty term in llama.cpp's penalties sampler (gh#23 MVP item 3).
Definition config.h:360
float presence_penalty
Presence-penalty term in llama.cpp's penalties sampler (gh#23 MVP item 2).
Definition config.h:333
bool enable_thinking
Enable <think> blocks (false if reasoning_budget == 0)
Definition config.h:369
float min_p
Min-p nucleus sampling threshold (gh#23 MVP item 1).
Definition config.h:326
int max_tokens
Maximum tokens to generate.
Definition config.h:362
float top_p
Nucleus sampling threshold.
Definition config.h:315
int seed
RNG seed for reproducible sampling.
Definition config.h:367
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.