diff --git a/tools/omnivoice/CMakeLists.txt b/tools/omnivoice/CMakeLists.txt index 2be48b753..ec56da599 100644 --- a/tools/omnivoice/CMakeLists.txt +++ b/tools/omnivoice/CMakeLists.txt @@ -369,7 +369,7 @@ if(TARGET mtmd) endif() add_dependencies(elizainference_static omnivoice-version) - # FFI tests (ABI v17). + # FFI tests (ABI v18). # eliza-inference-test-abi links the real libelizainference: ABI # string, probes, cancel/evict no-ops, and # (opt-in, ELIZA_TEST_ASR_DIR) a real-model diff --git a/tools/omnivoice/include/eliza-inference-ffi.h b/tools/omnivoice/include/eliza-inference-ffi.h index d00f0d2d7..b7b31a79d 100644 --- a/tools/omnivoice/include/eliza-inference-ffi.h +++ b/tools/omnivoice/include/eliza-inference-ffi.h @@ -1,5 +1,5 @@ /* - * libelizainference FFI ABI v17. + * libelizainference FFI ABI v18. * * (Banner tracks ELIZA_INFERENCE_ABI_VERSION below; the per-version history is * at the end of this header preamble, newest first.) @@ -134,6 +134,16 @@ extern "C" { * load and refuses to bind if they disagree. * * Changelog: + * v18: text-model context capacity resolved before stream admission. + * - `eliza_inference_llm_context_capacity(ctx, requested_context, + * n_gpu_layers, &out_capacity, &err)` loads the resident text model + * (if needed) and returns the context size `llm_stream_open` would + * use: `requested_context`, else ELIZA_LLM_N_CTX, else 8192, clamped + * to the GGUF training context. No decode context is allocated. + * - `llm_stream_open` now shares that resolution and rejects a + * non-positive capacity instead of creating a zero-sized context. + * Additive symbol; no existing signature changes. A v17 library lacks + * the symbol, so loaders must treat it as absent. * v17: Paradee-8M TTS with native misaki G2P, plus cancellation and * eviction for every TTS head and for ASR. All additions are new * symbols; no existing signature changes. @@ -254,9 +264,9 @@ extern "C" { * v7: real Silero VAD (same symbol surface as v6). * v6: fused wake-word, speaker, diarizer. */ -#define ELIZA_INFERENCE_ABI_VERSION 17 +#define ELIZA_INFERENCE_ABI_VERSION 18 -/* Returns a static, NUL-terminated string of the form "17" matching +/* Returns a static, NUL-terminated string of the form "18" matching * ELIZA_INFERENCE_ABI_VERSION at the time the library was built. The * pointer is owned by the library — do NOT free. */ const char * eliza_inference_abi_version(void); @@ -1152,6 +1162,19 @@ typedef struct { /* Opaque streaming-LLM session. One per active generation. */ typedef struct EliLlmStream EliLlmStream; +/* Resolve the streaming context capacity against the resident text model + * before token admission (ABI v18). Loads the model with n_gpu_layers when + * necessary. requested_context <=0 uses ELIZA_LLM_N_CTX or 8192; the result + * is clamped to the GGUF training context exactly as stream_open does. + * On success, pass *out_capacity explicitly to stream_open to preserve this + * decision even if the environment changes. No decode context is allocated. */ +int eliza_inference_llm_context_capacity( + EliInferenceContext * ctx, + int32_t requested_context, + int32_t n_gpu_layers, + int32_t * out_capacity, + char ** out_error); + /* Open a session anchored to `ctx`. Loads the bundle's text GGUF on first * open and reuses it across sessions. Returns NULL on failure with * `*out_error` populated. Close exactly once via `_close`. */ diff --git a/tools/omnivoice/src/eliza-inference-ffi.cpp b/tools/omnivoice/src/eliza-inference-ffi.cpp index 8ec811696..7a390126a 100644 --- a/tools/omnivoice/src/eliza-inference-ffi.cpp +++ b/tools/omnivoice/src/eliza-inference-ffi.cpp @@ -3796,6 +3796,40 @@ int eliza_inference_llm_kv_quant_supported(void) { return 1; } +static int eliza_llm_context_capacity( + const llama_model * model, int32_t requested_context) { + const int n_ctx_train = llama_model_n_ctx_train(model); + int n_ctx = requested_context > 0 + ? requested_context + : eliza_int_env_or_default("ELIZA_LLM_N_CTX", 8192); + if (n_ctx_train > 0 && n_ctx > n_ctx_train) n_ctx = n_ctx_train; + return n_ctx; +} + +int eliza_inference_llm_context_capacity( + EliInferenceContext * ctx, + int32_t requested_context, + int32_t n_gpu_layers, + int32_t * out_capacity, + char ** out_error) { + if (!ctx || !out_capacity) { + eliza_set_error(out_error, + "[libelizainference] llm_context_capacity: ctx and out_capacity are required"); + return ELIZA_ERR_INVALID_ARG; + } + std::lock_guard lock(ctx->llm_mutex); + const int rc = eliza_load_llm_model_locked(ctx, n_gpu_layers, out_error); + if (rc != ELIZA_OK) return rc; + const int capacity = eliza_llm_context_capacity(ctx->llm_model, requested_context); + if (capacity <= 0) { + eliza_set_error(out_error, + "[libelizainference] llm_context_capacity: context capacity must be positive"); + return ELIZA_ERR_INVALID_ARG; + } + *out_capacity = capacity; + return ELIZA_OK; +} + EliLlmStream * eliza_inference_llm_stream_open( EliInferenceContext * ctx, const eliza_llm_stream_config_t * cfg, @@ -3827,11 +3861,13 @@ EliLlmStream * eliza_inference_llm_stream_open( * MTP engine's target context so both decode identically (same n_ctx / * batch / threads / flash-attn / KV-quant). */ llama_context_params cparams = llama_context_default_params(); - const int n_ctx_train = llama_model_n_ctx_train(model); - int n_ctx = cfg->context_size > 0 - ? cfg->context_size - : eliza_int_env_or_default("ELIZA_LLM_N_CTX", 8192); - if (n_ctx_train > 0 && n_ctx > n_ctx_train) n_ctx = n_ctx_train; + const int n_ctx = eliza_llm_context_capacity(model, cfg->context_size); + if (n_ctx <= 0) { + eliza_set_error(out_error, + "[libelizainference] llm_stream_open: context capacity must be positive"); + delete stream; + return nullptr; + } cparams.n_ctx = (uint32_t) n_ctx; cparams.n_batch = (uint32_t) eliza_int_env_or_default("ELIZA_LLM_N_BATCH", 512); cparams.n_ubatch = cparams.n_batch; diff --git a/tools/omnivoice/tests/ffi-abi.c b/tools/omnivoice/tests/ffi-abi.c index 03c997b45..8dd147450 100644 --- a/tools/omnivoice/tests/ffi-abi.c +++ b/tools/omnivoice/tests/ffi-abi.c @@ -1,7 +1,8 @@ /* tools/omnivoice/tests/ffi-abi.c * - * C11 test against the real libelizainference (ABI v17). Always runs: - * - ABI string "17" and the v17 capability probes + * C11 test against the real libelizainference (ABI v18). Always runs: + * - ABI string "18" and the v17 capability probes + * - v18 llm_context_capacity argument validation and the no-text-GGUF error * - cancel_tts / asr_cancel are no-ops with nothing in flight * - mmap_evict / mmap_acquire("tts") on a bundle with no TTS weights * - Paradee entries before any load (or NOT_IMPLEMENTED when not built) @@ -49,7 +50,7 @@ static double now_ms(void) { static void test_probes_and_noops(void) { char * err = NULL; - CHECK(strcmp(eliza_inference_abi_version(), "17") == 0); + CHECK(strcmp(eliza_inference_abi_version(), "18") == 0); CHECK_RC(eliza_inference_asr_supported(), 1); #if defined(ELIZA_TEST_EXPECT_PARADEE) CHECK_RC(eliza_inference_paradee_supported(), 1); @@ -72,6 +73,18 @@ static void test_probes_and_noops(void) { clear_err(&err); CHECK_RC(eliza_inference_mmap_evict(ctx, "tts", &err), ELIZA_OK); CHECK_RC(eliza_inference_mmap_evict(ctx, "asr", &err), ELIZA_OK); + /* v18: capacity resolution validates its arguments and reports a bundle + * without a text GGUF instead of returning a capacity. */ + int32_t capacity = -12345; + CHECK_RC(eliza_inference_llm_context_capacity(NULL, 0, 0, &capacity, &err), ELIZA_ERR_INVALID_ARG); + CHECK(capacity == -12345); + clear_err(&err); + CHECK_RC(eliza_inference_llm_context_capacity(ctx, 0, 0, NULL, &err), ELIZA_ERR_INVALID_ARG); + clear_err(&err); + CHECK_RC(eliza_inference_llm_context_capacity(ctx, 4096, 0, &capacity, &err), ELIZA_ERR_BUNDLE_INVALID); + CHECK(capacity == -12345); + CHECK(err != NULL); + clear_err(&err); /* No TTS weights and no configured Kokoro/Paradee head. */ CHECK_RC(eliza_inference_mmap_acquire(ctx, "tts", &err), ELIZA_ERR_BUNDLE_INVALID); clear_err(&err); diff --git a/tools/omnivoice/tests/ffi-paradee-asr.cpp b/tools/omnivoice/tests/ffi-paradee-asr.cpp index 28c95c3fd..33b85834e 100644 --- a/tools/omnivoice/tests/ffi-paradee-asr.cpp +++ b/tools/omnivoice/tests/ffi-paradee-asr.cpp @@ -9,7 +9,7 @@ // - eliza_inference_asr_cancel during prefill and during decode, over a // scripted ASR engine that polls the same abort callback llama_decode // uses -// - ABI string "17" +// - ABI string "18" // // The bridge is compiled into this TU (ELIZA_INFERENCE_TEST_HOOKS) against // the Paradee / misaki doubles in tests/paradee-contract/, so no weights are @@ -86,7 +86,7 @@ static int synth_text(EliInferenceContext * ctx, const std::string & text, size_ } static void test_abi_and_probes() { - CHECK(std::string(eliza_inference_abi_version()) == "17"); + CHECK(std::string(eliza_inference_abi_version()) == "18"); CHECK_RC(eliza_inference_paradee_supported(), 1); CHECK_RC(eliza_inference_asr_supported(), 1); CHECK_RC(eliza_inference_asr_stream_supported(), 0);