Skip to content

Language Bindings

ai-core-ffi is the C ABI layer for non-Rust SDKs. Treat it as the stable boundary for Python, Go, PHP, Swift, Kotlin, Flutter, and service-side integrations.

What This Section Covers

This page explains:

  • which C ABI functions to wrap;
  • what each function accepts and returns;
  • how to pass config and request JSON;
  • how to free returned strings;
  • how to shape Go, Python, and PHP bindings.

Result Wrapper

Every string-returning function returns an allocated UTF-8 C string containing one of these JSON envelopes:

json
{"ok":true,"data":{}}
json
{"ok":false,"error":{"code":"config_error","message":"..."}}

The host must parse the envelope, copy data into host-owned objects, and free the returned string with ai_core_string_free.

Memory Ownership

  • ai_core_handle_new returns an opaque runtime handle.
  • ai_core_handle_free releases that handle.
  • Every returned char * must be released with ai_core_string_free.
  • Do not keep raw C pointers in long-lived host objects.
  • Copy returned JSON into host-managed memory before freeing the SDK string.

Run FFI calls off the UI/main async executor thread. For parallel work, keep one long-lived handle per worker or tenant.

Multiple handles in one host process share the native local-model managers below the FFI layer. A builtin-local-llm build keeps one resident llama_model per process/model/backend key after first use. A builtin-speech-whisper build keeps a resident Whisper context pool per process/model path. Separate OS processes do not share these managers.

Use models[].local.max_concurrent_requests and speech.whisper.max_concurrent_requests to bound active native work inside one process. Use max_pending_requests only as a small in-process admission queue; durable queues, retries, and backpressure stay in the host.

Methods

ai_core_ffi_abi_version

  • Inputs: none.
  • Returns: uint32_t ABI version. Current value: 1.
  • Use when: the binding wants to reject incompatible shared libraries before making JSON calls.

ai_core_version_json

  • Inputs: none.
  • Returns: JSON with the AI Core version and compiled native-provider flags: builtin_local_llm and builtin_speech_whisper.
  • Free with: ai_core_string_free.

ai_core_handle_new

  • Inputs: none.
  • Returns: AiCoreFfiHandle *.
  • Use when: creating a runtime instance for a worker, tenant, or process.
  • Notes: the handle registers built-in core tools and built-in HTTP model providers. Builds with builtin-local-llm also register the native local provider.

FFI native-provider feature variants:

sh
# HTTP providers only.
cargo build -p ai-core-ffi --release

# Local GGUF chat only.
cargo build -p ai-core-ffi --release --features builtin-local-llm

# Local Whisper transcription only.
cargo build -p ai-core-ffi --release --features builtin-speech-whisper

# Local GGUF chat plus local Whisper transcription.
cargo build -p ai-core-ffi --release --features builtin-local-all

ai_core_handle_free

  • Inputs: AiCoreFfiHandle *.
  • Returns: nothing.
  • Use when: shutting down a worker or tenant runtime.

ai_core_apply_config_json

  • Inputs: handle and config JSON string.
  • Returns: apply report JSON with model_count, has_model_selection, and plugin_config_count.
  • Use when: the host has loaded or changed model, plugin, secret-reference, or speech settings.
  • Local LLM: config JSON may include local_llm.backend with auto, cpu, or metal. models[].local.backend can override it for one built_in_local model.
  • Local LLM concurrency: models[].local.max_concurrent_requests defaults to 1 and must be greater than zero. models[].local.max_pending_requests defaults to 0. The resident GGUF weights are shared inside the process, but each active request still creates its own llama_context and KV cache. JSON hosts can pass 4294967295 for an effectively unbounded active limit, but that is a deliberate memory-risk choice.
  • Whisper concurrency: speech.whisper.max_concurrent_requests defaults to 1 and must be greater than zero. speech.whisper.max_pending_requests defaults to 0. With defaults, concurrent transcription calls are rejected while the resident context is busy. Builds without builtin-speech-whisper still accept this config but cannot execute native transcription. JSON hosts can pass 4294967295 for an effectively unbounded active limit only when they provide external backpressure.

ai_core_settings_catalog_json

  • Inputs: handle.
  • Returns: JSON catalog of current core settings, supported model kinds, configured models, selected model slots, plugin config schema metadata, and Wasm limits.
  • Use when: a UI needs to render available runtime settings.

ai_core_add_core_license_document

  • Inputs: handle, license document bytes, byte length.
  • Returns: current license document count.
  • Availability: normal marketplace builds only.
  • Use when: the host stores a license document separately from KEM key material.

ai_core_add_core_license_key_material

  • Inputs: handle, license document bytes, document length, matching private KEM key bytes, key length.
  • Returns: current license key-material count.
  • Availability: normal marketplace builds only.
  • Use when: installing encrypted marketplace packages.

ai_core_clear_core_license_documents

  • Inputs: handle.
  • Returns: zeroed license document and key-material counts.
  • Availability: normal marketplace builds only.
  • Use when: signing out, changing tenants, or clearing runtime license state.

ai_core_install_package_bytes

  • Inputs: handle, package bytes, package byte length, valid_at Unix timestamp.
  • Returns: package install report with plugin id, replacement flag, verification status, and issuer.
  • Use when: loading a .aip plugin or pipeline package.

ai_core_pipeline_start_json

  • Inputs: handle and start request JSON.
  • Returns: pipeline run update JSON.
  • Use when: starting a package pipeline from fresh user input.

Start request:

json
{
  "run_id": "run-1",
  "task_id": "task-1",
  "plugin_id": "unpack_systems_pipeline",
  "user_text": "build an audit for a new product"
}

ai_core_pipeline_resume_json

  • Inputs: handle and resume request JSON.
  • Returns: pipeline run update JSON.
  • Use when: continuing or retrying from a checkpoint.

Resume request:

json
{
  "state": {
    "schema": "ai_core.pipeline_state.v1",
    "run_id": "run-1",
    "task_id": "task-1",
    "plugin_id": "unpack_systems_pipeline",
    "status": "waiting_for_approval",
    "next_step_index": 1
  },
  "user_text": "continue",
  "previous_outputs": [],
  "control": "continue"
}

control can be "continue" or "retry". For "retry", the host supplies the replacement user_text; the rerun still receives the step skill, output schema, and earlier previous_outputs.

ai_core_wasm_run_action_json

  • Inputs: handle, plugin id, fully qualified action id, action input JSON.
  • Returns: action output JSON plus emitted events and queued host calls.
  • Use when: the host already knows the exact installed Wasm action to run.

Example:

c
char *out = ai_core_wasm_run_action_json(
    core,
    "plugin_id",
    "plugin_id.action_id",
    "{\"some\":\"input\"}"
);

ai_core_model_chat_json

  • Inputs: handle and model chat request JSON.
  • Returns: chat response JSON.
  • Use when: calling a configured model directly by model_id.

Request:

json
{
  "model_id": "qwen/qwen3.5-9b",
  "messages": [
    {"role": "user", "content": "hello"}
  ],
  "response_format": {"kind": "text"},
  "max_output_tokens": 512
}

Supported roles are system, user, and assistant. Supported response formats are text, json_object, and json_schema with a required schema string. Returned content is a nested JSON value when the model produced valid JSON; otherwise it is the model's raw string.

ai_core_transcribe_audio_json

  • Inputs: handle, audio byte pointer, audio byte length, and request JSON.
  • Returns: transcription response JSON.
  • Use when: calling the built-in Whisper provider through FFI.
  • Availability: exported in every FFI build. Builds without builtin-speech-whisper return transcription_provider_unavailable.

Request:

json
{
  "format": "pcm16",
  "sample_rate_hz": 16000,
  "locale": "ru-RU"
}

Supported format values are wav, pcm16, m4a, mp3, flac, ogg_opus, webm_opus, and unknown. For predictable native behavior, hosts should decode compressed uploads to WAV PCM16 or raw PCM16 before calling FFI.

Successful response data:

json
{
  "text": "transcribed text",
  "locale": "ru"
}

Common error codes:

  • transcription_provider_unavailable: shared library was built without builtin-speech-whisper.
  • transcription_request_error: request JSON or audio format is invalid.
  • transcription_provider_error: Whisper provider failed, for example because the configured model file is missing.

Config JSON

ai_core_apply_config_json accepts this host-facing shape:

The host-owned config snapshot and model-selection fields are documented in Runtime Contracts.

Worker Pattern

The examples below show thin binding layers, not full worker orchestration. Application code should keep one long-lived handle per worker or tenant and run blocking FFI calls off the UI thread or async executor.

Example: Python Binding

python
import ctypes
import json

lib = ctypes.CDLL("./libai_core_ffi.dylib")
lib.ai_core_handle_new.restype = ctypes.c_void_p
lib.ai_core_handle_free.argtypes = [ctypes.c_void_p]
lib.ai_core_string_free.argtypes = [ctypes.c_void_p]
lib.ai_core_apply_config_json.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
lib.ai_core_apply_config_json.restype = ctypes.c_void_p

class AiCore:
    def __init__(self):
        self.handle = lib.ai_core_handle_new()

    def _call_json(self, fn, *args):
        ptr = fn(*args)
        try:
            raw = ctypes.cast(ptr, ctypes.c_char_p).value.decode("utf-8")
            envelope = json.loads(raw)
            if not envelope["ok"]:
                raise RuntimeError(envelope["error"]["message"])
            return envelope["data"]
        finally:
            lib.ai_core_string_free(ptr)

    def apply_config(self, config):
        payload = json.dumps(config).encode("utf-8")
        return self._call_json(
            lib.ai_core_apply_config_json,
            self.handle,
            ctypes.c_char_p(payload),
        )

    def close(self):
        lib.ai_core_handle_free(self.handle)

Call the binding from a background thread or process worker. Do not run FFI calls directly on an event-loop thread.

Example: Go Binding

go
package aicore

/*
#include "ai_core_ffi.h"
#include <stdlib.h>
*/
import "C"
import (
    "encoding/json"
    "errors"
    "unsafe"
)

type Core struct {
    handle *C.AiCoreFfiHandle
}

func New() *Core {
    return &Core{handle: C.ai_core_handle_new()}
}

func (c *Core) Close() {
    C.ai_core_handle_free(c.handle)
}

func decode(ptr *C.char, out any) error {
    defer C.ai_core_string_free(ptr)
    var env struct {
        Ok    bool            `json:"ok"`
        Data  json.RawMessage `json:"data"`
        Error struct {
            Code    string `json:"code"`
            Message string `json:"message"`
        } `json:"error"`
    }
    if err := json.Unmarshal([]byte(C.GoString(ptr)), &env); err != nil {
        return err
    }
    if !env.Ok {
        return errors.New(env.Error.Message)
    }
    return json.Unmarshal(env.Data, out)
}

func (c *Core) ApplyConfig(config []byte, out any) error {
    input := C.CString(string(config))
    defer C.free(unsafe.Pointer(input))
    return decode(C.ai_core_apply_config_json(c.handle, input), out)
}

Use the binding from a worker goroutine or service worker pool and keep the handle alive across requests instead of creating one per call.

Example: PHP Binding

php
final class AiCore
{
    private FFI $ffi;
    private FFI\CData $handle;

    public function __construct(string $library)
    {
        $this->ffi = FFI::cdef('
            typedef struct AiCoreFfiHandle AiCoreFfiHandle;
            AiCoreFfiHandle *ai_core_handle_new(void);
            void ai_core_handle_free(AiCoreFfiHandle *handle);
            void ai_core_string_free(char *ptr);
            char *ai_core_apply_config_json(
                AiCoreFfiHandle *handle,
                const char *config_json
            );
        ', $library);
        $this->handle = $this->ffi->ai_core_handle_new();
    }

    public function applyConfig(array $config): array
    {
        $ptr = $this->ffi->ai_core_apply_config_json(
            $this->handle,
            json_encode($config, JSON_THROW_ON_ERROR)
        );

        try {
            $env = json_decode(FFI::string($ptr), true, flags: JSON_THROW_ON_ERROR);
            if (!$env['ok']) {
                throw new RuntimeException($env['error']['message']);
            }
            return $env['data'];
        } finally {
            $this->ffi->ai_core_string_free($ptr);
        }
    }

    public function close(): void
    {
        $this->ffi->ai_core_handle_free($this->handle);
    }
}

Use the binding from a long-lived PHP worker process or another background execution layer, not directly from latency-sensitive request glue that also owns durable retries or queueing.

Example: Rust Worker Wrapper

rust
use std::sync::Arc;
use tokio::task;

// Assume AiCoreBinding is the thin synchronous wrapper around ai-core-ffi.
struct AiCoreWorker {
    core: Arc<AiCoreBinding>,
}

impl AiCoreWorker {
    fn new() -> Self {
        Self {
            core: Arc::new(AiCoreBinding::new()),
        }
    }

    async fn apply_config(&self, config: Vec<u8>) -> anyhow::Result<serde_json::Value> {
        let core = Arc::clone(&self.core);
        task::spawn_blocking(move || {
            let mut out = serde_json::Value::Null;
            core.apply_config(&config, &mut out)?;
            Ok(out)
        })
        .await??
    }
}

This keeps one long-lived handle inside a worker object and moves the blocking FFI call onto tokio::task::spawn_blocking instead of running it on a Tokio worker thread.

Binding Shape

Expose idiomatic host-language methods over the C ABI:

  • Python: core.apply_config(config: dict) -> dict
  • Go: core.ApplyConfig(config []byte, out any) error
  • PHP: $core->applyConfig(array $config): array

Application code should never manage C pointers directly. Keep pointer copying and ai_core_string_free inside the binding layer.

AI Core documentation site.