Skip to content

Языковые привязки

ai-core-ffi — это слой C ABI для SDK не на Rust. Рассматривайте его как стабильную границу для Python, Go, PHP, Swift, Kotlin, Flutter и интеграций на стороне сервера.

Что покрывает этот раздел

Эта страница объясняет:

  • какие функции C ABI нужно оборачивать;
  • что каждая функция принимает и возвращает;
  • как передавать config и request JSON;
  • как освобождать возвращаемые строки;
  • как строить bindings для Go, Python и PHP.

Обёртка результата

Каждая функция, возвращающая строку, отдаёт выделенную UTF-8 C string с одним из этих JSON envelopes:

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

Host должен распарсить envelope, скопировать данные в host-owned objects и освободить возвращённую строку через ai_core_string_free.

Владение памятью

  • ai_core_handle_new возвращает opaque runtime handle.
  • ai_core_handle_free освобождает этот handle.
  • Каждый возвращённый char * должен быть освобождён через ai_core_string_free.
  • Не храните raw C pointers в long-lived host objects.
  • Копируйте возвращённый JSON в host-managed memory до освобождения SDK string.

Вызывайте FFI calls вне UI/main async executor thread. Для parallel work держите один long-lived handle на worker или tenant.

Несколько handles в одном host process разделяют native local-model managers под слоем FFI. Сборка builtin-local-llm хранит один resident llama_model на каждый ключ process/model/backend после первого использования. Сборка builtin-speech-whisper хранит resident Whisper context pool на каждый путь process/model. Отдельные OS processes не разделяют эти managers.

Используйте models[].local.max_concurrent_requests и speech.whisper.max_concurrent_requests, чтобы ограничить активную native работу внутри одного process. Используйте max_pending_requests только как небольшую in-process admission queue; durable queues, retries и backpressure остаются в host.

Методы

ai_core_ffi_abi_version

  • Inputs: none.
  • Returns: uint32_t ABI version. Current value: 1.
  • Use when: binding wants to reject incompatible shared libraries before 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 runtime instance for worker, tenant or process.
  • Notes: handle registers built-in core tools and built-in HTTP model providers. Builds with builtin-local-llm also register 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 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: host 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: 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: 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 .aip plugin or pipeline package.

ai_core_pipeline_start_json

  • Inputs: handle and start request JSON.
  • Returns: pipeline run update JSON.
  • Use when: starting 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 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 может быть "continue" или "retry". Для "retry" host передаёт заменяющий user_text; повторный запуск всё так же получает skill шага, output schema и предыдущие 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: host already knows 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 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
}

Поддерживаемые роли: system, user и assistant. Поддерживаемые форматы ответа: text, json_object и json_schema с обязательной строкой schema. Если модель вернула валидный JSON, content содержит вложенное JSON-значение; любой другой ответ сохраняется исходной строкой.

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"
}

Поддерживаемые значения format: wav, pcm16, m4a, mp3, flac, ogg_opus, webm_opus и unknown. Для предсказуемого native behavior host' ы должны декодировать сжатые uploads в WAV PCM16 или raw PCM16 до вызова 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:

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

Паттерн worker'а

Примеры ниже показывают тонкие binding layers, а не полную orchestration worker'ов. Application code должна хранить один long-lived handle на worker или tenant и выносить blocking FFI calls с UI thread или async executor thread.

Пример: 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)

Вызывайте binding из background thread или process worker. Не запускайте FFI calls напрямую на event-loop thread.

Пример: 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)
}

Вызывайте binding из worker goroutine или service worker pool и держите handle живым между requests вместо создания нового handle на каждый call.

Пример: 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);
    }
}

Вызывайте binding из long-lived PHP worker process или другого background execution layer, а не напрямую из latency-sensitive request glue, которая ещё и владеет durable retries или queueing.

Пример: 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??
    }
}

Это удерживает один long-lived handle внутри worker object и переносит blocking FFI call в tokio::task::spawn_blocking, а не на Tokio worker thread.

Форма binding'ов

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.