Entropic 2.9.4
Local-first agentic inference engine
Loading...
Searching...
No Matches
inference_c_api.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
16
17#include "llama_cpp_backend.h"
18
19#include <nlohmann/json.hpp>
20
21#include <atomic>
22#include <cerrno>
23#include <chrono>
24#include <cstdlib>
25#include <cstring>
26#include <filesystem>
27#include <mutex>
28#include <optional>
29#include <string>
30#include <thread>
31
32namespace {
33
34auto logger = entropic::log::get("inference.c_api");
35
44 return reinterpret_cast<entropic::InferenceBackend*>(h);
45}
46
54char* alloc_string(const std::string& s) {
55 char* buf = static_cast<char*>(std::malloc(s.size() + 1));
56 if (buf) {
57 std::memcpy(buf, s.c_str(), s.size() + 1);
58 }
59 return buf;
60}
61
77template <typename T>
78static void set_if(const nlohmann::json& j, const char* key, T& out) {
79 if (j.contains(key)) { out = j[key].get<T>(); }
80}
81
87entropic::ModelConfig parse_config_json(const char* json_str) {
89 auto j = nlohmann::json::parse(json_str);
90
91 set_if(j, "path", config.path);
92 set_if(j, "adapter", config.adapter);
93 set_if(j, "context_length", config.context_length);
94 set_if(j, "gpu_layers", config.gpu_layers);
95 set_if(j, "keep_warm", config.keep_warm);
96 set_if(j, "use_mlock", config.use_mlock);
97 set_if(j, "n_batch", config.n_batch);
98 set_if(j, "n_threads", config.n_threads);
99 set_if(j, "flash_attn", config.flash_attn);
100
101 return config;
102}
103
113template <typename T>
114static void assign_if_present(const nlohmann::json& j,
115 const char* key, T& dst) {
116 if (j.contains(key)) { dst = j[key].get<T>(); }
117}
118
129static void parse_logit_bias_into(
130 const nlohmann::json& j,
131 std::unordered_map<int32_t, float>& dst)
132{
133 if (!j.contains("logit_bias") || !j["logit_bias"].is_object()) {
134 return;
135 }
136 for (auto it = j["logit_bias"].begin(); it != j["logit_bias"].end(); ++it) {
137 try {
138 dst[std::stoi(it.key())] = it.value().get<float>();
139 } catch (const std::exception&) {
140 // skip un-parseable keys
141 }
142 }
143}
144
152entropic::GenerationParams parse_params_json(const char* json_str) {
154 auto j = nlohmann::json::parse(json_str);
155
156 assign_if_present(j, "temperature", params.temperature);
157 assign_if_present(j, "top_p", params.top_p);
158 assign_if_present(j, "top_k", params.top_k);
159 assign_if_present(j, "min_p", params.min_p);
160 assign_if_present(j, "presence_penalty", params.presence_penalty);
161 assign_if_present(j, "frequency_penalty", params.frequency_penalty);
162 assign_if_present(j, "repeat_penalty", params.repeat_penalty);
163 assign_if_present(j, "max_tokens", params.max_tokens);
164 assign_if_present(j, "grammar", params.grammar);
165 parse_logit_bias_into(j, params.logit_bias);
166
167 return params;
168}
169
177std::string serialize_result_json(const entropic::GenerationResult& result) {
178 nlohmann::json j;
179 j["content"] = result.content;
180 j["finish_reason"] = result.finish_reason;
181 j["token_count"] = result.token_count;
182 j["generation_time_ms"] = result.generation_time_ms;
183 j["error_code"] = static_cast<int>(result.error_code);
184 j["error_message"] = result.error_message;
185 return j.dump();
186}
187
188/* parse_content_part + parse_messages_json moved to the shared
189 * utility include/entropic/types/messages_json.h (v2.1.8, gh#37) so
190 * the facade's entropic_run_messages can reuse them. Calls below
191 * dispatch to entropic::parse_messages_json directly. */
192
193} // anonymous namespace
194
195// ── C API Implementation ───────────────────────────────────
196
197extern "C" {
198
209 const char* config_json)
210{
211 // v2.3.10: null-handle guard. Pre-v2.3.10 to_backend(nullptr)
212 // returned nullptr and the next ->load() dereferenced it, taking
213 // the whole process down with SIGSEGV. The plugin ABI is the
214 // load-bearing boundary for misuse; rejecting null with a clean
215 // error code is safer than crashing.
216 if (!backend) { return ENTROPIC_ERROR_INVALID_ARGUMENT; }
217 logger->info("C API: inference_load");
218 try {
219 auto config = parse_config_json(config_json);
220 auto rc = to_backend(backend)->load(config)
222 logger->info("C API: inference_load -> {}",
223 static_cast<int>(rc));
224 return rc;
225 } catch (const std::exception& e) {
226 logger->error("inference_load exception: {}", e.what());
228 }
229}
230
240{
241 if (!backend) { return ENTROPIC_ERROR_INVALID_ARGUMENT; }
242 try {
243 return to_backend(backend)->activate() ? ENTROPIC_OK : ENTROPIC_ERROR_LOAD_FAILED;
244 } catch (const std::exception& e) {
245 logger->error("inference_activate exception: {}", e.what());
247 }
248}
249
259{
260 if (!backend) { return ENTROPIC_ERROR_INVALID_ARGUMENT; }
261 try {
262 to_backend(backend)->deactivate();
263 return ENTROPIC_OK;
264 } catch (const std::exception& e) {
265 logger->error("inference_deactivate exception: {}", e.what());
267 }
268}
269
279{
280 if (!backend) { return ENTROPIC_ERROR_INVALID_ARGUMENT; }
281 try {
282 to_backend(backend)->unload();
283 return ENTROPIC_OK;
284 } catch (const std::exception& e) {
285 logger->error("inference_unload exception: {}", e.what());
287 }
288}
289
297ENTROPIC_EXPORT int entropic_inference_state(
299{
300 // v2.3.10: null-handle returns COLD (0) — the safest "I don't
301 // own anything" answer. Callers gating activation on state should
302 // treat null + COLD identically anyway.
303 if (!backend) { return static_cast<int>(ENTROPIC_MODEL_STATE_COLD); }
304 return static_cast<int>(to_backend(backend)->state());
305}
306
319 const char* messages_json,
320 const char* params_json,
321 char** result_json)
322{
323 if (!backend || !result_json) {
325 }
326 logger->info("C API: inference_generate");
327 try {
328 auto msgs = entropic::parse_messages_json(messages_json);
329 auto params = parse_params_json(params_json);
330 auto result = to_backend(backend)->generate(msgs, params);
331 *result_json = alloc_string(serialize_result_json(result));
332 logger->info("C API: inference_generate -> {}",
333 result.ok() ? "ok" : "error");
334 return result.ok() ? ENTROPIC_OK : result.error_code;
335 } catch (const std::exception& e) {
336 logger->error("inference_generate exception: {}", e.what());
338 }
339}
340
363namespace {
364
376class CancelFlagBridge {
377public:
383 explicit CancelFlagBridge(int* cancel_flag) {
384 if (cancel_flag == nullptr) { return; }
385 poller_ = std::thread([this, cancel_flag]() {
386 while (!done_.load(std::memory_order_acquire)) {
387 if (*cancel_flag != 0) {
388 cancel_.store(true, std::memory_order_release);
389 return;
390 }
391 std::this_thread::sleep_for(std::chrono::milliseconds(10));
392 }
393 });
394 }
395
401 ~CancelFlagBridge() {
402 done_.store(true, std::memory_order_release);
403 if (poller_.joinable()) { poller_.join(); }
404 }
405
406 CancelFlagBridge(const CancelFlagBridge&) = delete;
407 CancelFlagBridge& operator=(const CancelFlagBridge&) = delete;
408
414 std::atomic<bool>& flag() { return cancel_; }
415
416private:
417 std::atomic<bool> cancel_{false};
418 std::atomic<bool> done_{false};
419 std::thread poller_;
420};
421
422} // namespace
423
439 const char* messages_json,
440 const char* params_json,
441 char** result_json,
442 int* cancel_flag)
443{
444 if (!backend || !result_json) {
446 }
447 logger->info("C API: inference_generate_with_cancel");
448 try {
449 auto msgs = entropic::parse_messages_json(messages_json);
450 auto params = parse_params_json(params_json);
451 CancelFlagBridge bridge(cancel_flag);
452 auto result = to_backend(backend)->generate(
453 msgs, params, bridge.flag());
454 *result_json = alloc_string(serialize_result_json(result));
455 logger->info("C API: inference_generate_with_cancel -> {}",
456 result.ok() ? "ok" : "error");
457 return result.ok() ? ENTROPIC_OK : result.error_code;
458 } catch (const std::exception& e) {
459 logger->error("inference_generate_with_cancel exception: {}", e.what());
461 }
462}
463
478 const char* messages_json,
479 const char* params_json,
480 void (*on_token)(const char* token, size_t len, void* user_data),
481 void* user_data,
482 int* cancel_flag)
483{
484 if (!backend || !on_token) {
486 }
487 try {
488 auto msgs = entropic::parse_messages_json(messages_json);
489 auto params = parse_params_json(params_json);
490 std::atomic<bool> cancel{false};
491
492 auto callback = [on_token, user_data, cancel_flag, &cancel]
493 (std::string_view token) {
494 on_token(token.data(), token.size(), user_data);
495 if (cancel_flag && *cancel_flag) {
496 cancel.store(true, std::memory_order_release);
497 }
498 };
499
500 auto result = to_backend(backend)->generate_streaming(
501 msgs, params, callback, cancel);
502 return result.ok() ? ENTROPIC_OK : result.error_code;
503 } catch (const std::exception& e) {
504 logger->error("inference_generate_streaming exception: {}", e.what());
506 }
507}
508
521 const char* prompt,
522 const char* params_json,
523 char** result_json)
524{
525 if (!backend || !prompt || !result_json) {
527 }
528 try {
529 auto params = parse_params_json(params_json);
530 auto result = to_backend(backend)->complete(prompt, params);
531 *result_json = alloc_string(serialize_result_json(result));
532 return result.ok() ? ENTROPIC_OK : result.error_code;
533 } catch (const std::exception& e) {
534 logger->error("inference_complete exception: {}", e.what());
536 }
537}
538
550 const char* text,
551 size_t text_len)
552{
553 // v2.3.10: null guards. text=nullptr is a real misuse (caller
554 // owes us bytes). Returning 0 keeps the contract simple (no
555 // tokens to count when there's no string).
556 if (!backend || !text) { return 0; }
557 try {
558 return to_backend(backend)->count_tokens(std::string(text, text_len));
559 } catch (...) {
560 return static_cast<int>(text_len) / 4;
561 }
562}
563
570ENTROPIC_EXPORT void entropic_inference_destroy(
572{
573 delete to_backend(backend);
574}
575
582ENTROPIC_EXPORT void entropic_inference_free(void* ptr) {
583 std::free(ptr);
584}
585
596
603ENTROPIC_EXPORT int entropic_plugin_api_version() {
604 return 1;
605}
606
607// ── Log redirect (v2.0.1) ──────────────────────────────────
608
609static FILE* s_ggml_log_fp = nullptr;
610// llama.cpp's llama_log_set is a single-slot process global. Track
611// the active path so a second handle in the same process gets a
612// predictable answer: same path → no-op (don't truncate the first
613// handle's live log), conflicting path → reject with a warning
614// (rather than clobber).
615static std::mutex s_ggml_log_mu;
616static std::optional<std::string> s_ggml_log_path;
617
623static void ggml_log_to_file(enum ggml_log_level /*level*/,
624 const char* text, void* /*ud*/) {
625 if (s_ggml_log_fp && text) {
626 fputs(text, s_ggml_log_fp);
627 fflush(s_ggml_log_fp);
628 }
629}
630
636static void ggml_log_noop(enum ggml_log_level /*level*/,
637 const char* /*text*/, void* /*ud*/) {
638}
639
652 if (s_ggml_log_fp) {
653 fclose(s_ggml_log_fp);
654 s_ggml_log_fp = nullptr;
655 }
656 s_ggml_log_path.reset();
657 llama_log_set(ggml_log_noop, nullptr);
658}
659
665static std::string canonicalize_or_passthrough(const char* path) {
666 std::error_code ec;
667 auto canonical = std::filesystem::weakly_canonical(path, ec).string();
668 return ec ? std::string(path) : canonical;
669}
670
682void entropic_inference_log_to_file(const char* path) {
683 std::lock_guard lk(s_ggml_log_mu);
684
685 if (!path || path[0] == '\0') {
687 return;
688 }
689 auto canonical = canonicalize_or_passthrough(path);
690
691 // llama_log_set has one process-global slot; first-call wins so a
692 // second handle's redirect cannot clobber the first.
693 if (s_ggml_log_path && *s_ggml_log_path != canonical) {
694 logger->warn(
695 "ggml log redirect already wired to {}; ignoring request for {}",
696 *s_ggml_log_path, canonical);
697 return;
698 }
699
700 FILE* fp = fopen(path, "w");
701 if (!fp) {
702 logger->warn("ggml log fopen failed for {}: {}",
703 path, std::strerror(errno));
704 return;
705 }
706 if (s_ggml_log_fp) { fclose(s_ggml_log_fp); }
707 s_ggml_log_fp = fp;
708 s_ggml_log_path = canonical;
709 llama_log_set(ggml_log_to_file, nullptr);
710}
711
718 std::lock_guard lk(s_ggml_log_mu);
720}
721
722} // extern "C"
Concrete base class for inference backends (80% logic).
Definition backend.h:69
LlamaCppBackend — common llama.cpp patterns (15% layer).
Symbol visibility macro for all exported symbols.
@ ENTROPIC_MODEL_STATE_COLD
On disk only, no RAM consumed.
Definition enums.h:28
entropic_error_t
Error codes returned by all C API functions.
Definition error.h:35
@ ENTROPIC_OK
Success.
Definition error.h:36
@ ENTROPIC_ERROR_INTERNAL
Unexpected internal error (bug)
Definition error.h:51
@ ENTROPIC_ERROR_INVALID_ARGUMENT
NULL pointer, empty string, out-of-range value.
Definition error.h:37
@ ENTROPIC_ERROR_GENERATE_FAILED
Generation failed (context overflow, model error)
Definition error.h:42
@ ENTROPIC_ERROR_LOAD_FAILED
Model load failed (corrupt file, OOM, unsupported format)
Definition error.h:41
Pure C interface contract for inference backends.
struct entropic_inference_backend * entropic_inference_backend_t
Opaque handle to an inference backend instance.
ENTROPIC_EXPORT entropic_error_t entropic_inference_complete(entropic_inference_backend_t backend, const char *prompt, const char *params_json, char **result_json)
Plugin C API: raw text completion without chat template.
static void ggml_log_to_file(enum ggml_log_level, const char *text, void *)
Callback that writes to the ggml log file.
ENTROPIC_EXPORT entropic_error_t entropic_inference_generate_streaming(entropic_inference_backend_t backend, const char *messages_json, const char *params_json, void(*on_token)(const char *token, size_t len, void *user_data), void *user_data, int *cancel_flag)
Plugin C API: streaming generation with token callback and cancel flag.
ENTROPIC_EXPORT entropic_error_t entropic_inference_generate(entropic_inference_backend_t backend, const char *messages_json, const char *params_json, char **result_json)
Plugin C API: blocking generation returning full result.
ENTROPIC_EXPORT int entropic_plugin_api_version()
Plugin API version.
static std::string canonicalize_or_passthrough(const char *path)
Resolve path via weakly_canonical, fall back to raw on error.
ENTROPIC_EXPORT entropic_error_t entropic_inference_unload(entropic_inference_backend_t backend)
Plugin C API: release the loaded model (transition to COLD).
ENTROPIC_EXPORT entropic_error_t entropic_inference_generate_with_cancel(entropic_inference_backend_t backend, const char *messages_json, const char *params_json, char **result_json, int *cancel_flag)
Plugin C API: batch generation with mid-decode cancel.
void entropic_inference_log_silence(void)
Silence all llama/ggml output.
ENTROPIC_EXPORT int entropic_inference_count_tokens(entropic_inference_backend_t backend, const char *text, size_t text_len)
Plugin C API: count tokens for a text span (exact when loaded, estimate when COLD).
ENTROPIC_EXPORT entropic_error_t entropic_inference_deactivate(entropic_inference_backend_t backend)
Plugin C API: demote backend from ACTIVE to WARM (release GPU).
static void ggml_log_silence_locked()
Redirect llama/ggml logs to a file or silence them.
void entropic_inference_log_to_file(const char *path)
Redirect llama/ggml logs to a file or silence them.
ENTROPIC_EXPORT entropic_error_t entropic_inference_load(entropic_inference_backend_t backend, const char *config_json)
Plugin C API: load a model into the inference backend.
ENTROPIC_EXPORT entropic_inference_backend_t entropic_create_inference_backend()
Factory: create inference backend instance.
static void ggml_log_noop(enum ggml_log_level, const char *, void *)
No-op callback.
ENTROPIC_EXPORT void entropic_inference_free(void *ptr)
Plugin C API: free memory allocated by the inference backend.
ENTROPIC_EXPORT void entropic_inference_destroy(entropic_inference_backend_t backend)
Plugin C API: destroy the backend and free its resources.
ENTROPIC_EXPORT entropic_error_t entropic_inference_activate(entropic_inference_backend_t backend)
Plugin C API: promote backend from WARM to ACTIVE (GPU load).
ENTROPIC_EXPORT int entropic_inference_state(entropic_inference_backend_t backend)
Plugin C API: query current lifecycle state (lock-free).
LlamaCppBackend — llama.cpp C API integration.
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
static char * alloc_string(const std::string &s)
Allocate a C string copy for caller-owned return.
Definition mcp_c_api.cpp:41
Shared parser: messages-JSON wire format → vector<Message>.
std::vector< Message > parse_messages_json(const char *json_str)
Parse a JSON array of messages into a vector of Message.
Generation parameters for a single inference call.
Definition config.h:302
std::string grammar
GBNF grammar string (empty = unconstrained)
Definition config.h:359
int top_k
Top-K sampling.
Definition config.h:305
std::unordered_map< int32_t, float > logit_bias
Per-token logit bias map (gh#23 MVP item 4).
Definition config.h:336
float repeat_penalty
Repetition penalty.
Definition config.h:306
float temperature
Sampling temperature.
Definition config.h:303
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
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 generation_time_ms
Wall-clock generation time.
std::string finish_reason
Finish reason: "stop", "length", "error".
std::string content
Generated text (cleaned by adapter)
std::string error_message
Error description (empty if no error)
int token_count
Generated token count.
Model configuration for a single tier.
Definition config.h:148
int gpu_layers
GPU offload layers (-1 = all)
Definition config.h:152
int context_length
Context window size (512–131072)
Definition config.h:151
std::filesystem::path path
Resolved model file path.
Definition config.h:149
int n_threads
CPU threads (0 = auto-detect)
Definition config.h:174
bool keep_warm
Pre-warm model at startup.
Definition config.h:153
int n_batch
Batch size for prompt processing.
Definition config.h:160
bool flash_attn
Enable flash attention.
Definition config.h:233
bool use_mlock
Lock model in system RAM.
Definition config.h:154
std::string adapter
Chat adapter name.
Definition config.h:150