Skip to content

AbstractCore v2.16.0

Choose a tag to compare

@github-actions github-actions released this 26 Sep 09:20
· 463 commits to main since this release

Ejecting a model now frees it from the whole process for every in-process backend (MLX,
HuggingFace transformers and GGUF, embedding models), and the memory report gives one figure for
what the process holds on the accelerator.

Fixed

  • Tool calls a local model writes inside its thinking block (for example Qwen3-Coder style <tool_call><function=…> blocks before </think>) are recovered and executed when the reply has no visible answer; a call naming a tool that was not offered, or one cut off mid-parameter, is reported instead of silently dropped or run with missing arguments.
  • A thinking model now streams its reasoning while it thinks. When the chat template opens the
    thinking block itself (Qwen3.x with thinking on), a stream used to show nothing until the model
    closed it: minutes for a long reasoning, with the first text arriving only with the answer. The
    reasoning now arrives as metadata["reasoning_delta"] from the first token (inline <think>
    blocks stream the same way), the answer follows as text, and the final record, including
    metadata["reasoning"], equals the non-streamed one. This covers every lane that renders the chat
    template in-process: MLX, HuggingFace transformers and GGUF (llama.cpp). OpenAI-compatible
    servers render it themselves; there the reasoning streams only when the server sends it
    separately. A streamed answer no longer starts with the
    blank lines the model writes after </think>; whitespace inside the answer is kept.
  • Non-streamed calls on MLX, HuggingFace transformers and GGUF now also know when the chat template
    opened the thinking block: a reply cut off while thinking is returned as reasoning marked
    truncated ( (...)) with an empty answer, instead of the thinking as the answer, and a reply with
    a later thinking block no longer leaves </think> in the text.
  • POST /acore/models/unload and unload_after now free the model from every holder in the server
    process, not only from the server's own runtime, once no other managed runtime serves it. The
    response includes a process_eject report and is ok: false when weights remain in memory.
    Before, a model shared with another instance, or a HuggingFace copy held elsewhere, stayed in
    memory while the unload reported success.
  • An embedding model could never leave memory: every EmbeddingManager stayed referenced for the
    life of the process. It is now freed when dropped, EmbeddingManager.unload() frees it on
    request, and the next embedding after an unload loads the model again. unload() leaves embedders
    served by Ollama, LM Studio or another server untouched (in_process: false).
  • An unload never ejects a model that something else in the process still uses. Before, an
    unload for another spelling of a model name (for example MLX-Community/qwen3-4b-4bit) or for
    its hub-cache path ejected a managed runtime's model, even a locked one. Such an unload is now
    refused with 409 (model_locked or model_in_use). Every owner of models in the process (the
    server's managed runtimes, runtime clients) registers what it uses, and an eject checks all of
    them with the same name matching the eject itself uses.
  • Unloading an embedding model while an embedding is running no longer breaks that call. The
    unload waits for it to finish; if it is still running after drain_timeout_s, nothing is freed
    and the report says the call is in flight.
  • device.process_held_bytes on NVIDIA GPUs now counts what torch has reserved on the GPU
    (device.torch_cuda_reserved_bytes, basis cuda_device_counter), plus the llama.cpp figure when
    a GGUF model is loaded. Before, it read 0 while torch held the model.
  • MLXProvider.load_model() accepted ttl_s and keep_alive and ignored them without saying so.
    MLX has no idle or time-based unload; the response now lists them under unsupported_options
    with a warning.
  • A streamed MLX call now reports what a non-streamed one does. The stream ends with one chunk
    carrying finish_reason, usage and, when the call has a prompt_cache_key,
    metadata["prompt_cache"], identical to the non-streamed response for the same prompt and key.
    Before, MLX streams carried no usage or finish reason at all (except on the native runtime), and
    the prompt-cache record was only attached to non-streamed calls, so hosts that need it, such as
    AbstractRuntime, did not stream on MLX.
  • When a streamed model opens a tool call and never closes it, or writes one no parser accepts,
    the text is no longer printed as the answer (it was, together with a second copy of the text
    before it). It is reported on the final chunk as metadata["unparsed_tool_call"]
    (format, text, reason) with a warning.
  • A tool call written as a ```json block ({"name": ..., "arguments": ...},
    {"tool_calls": [...]}, a list of calls, or the OpenAI function shape) is now recognised while
    streaming when it names a tool the request offered: it is kept out of the text and returned in
    tool_calls. Any other ```json block stays text, including one that names a tool the
    request did not offer, and every one when the request has no tools.
  • Streaming a GPT-OSS model whose backend returns the raw Harmony transcript no longer holds the
    whole answer back until the end. The final channel streams as text, analysis as reasoning
    (metadata["reasoning_delta"], joined into metadata["reasoning"] at the end), commentary
    without a recipient as text, and a to= message as a tool call. The <|channel|> and other
    framing tokens never appear in the text.
  • A GPT-OSS reply cut off before its final message (typically by max_output_tokens) no longer
    returns the model's unfinished thinking as the answer. The reply is empty, the thinking is in
    metadata["reasoning"] marked truncated ( (...)), and finish_reason is "length" when the
    output limit was the cause; streamed and non-streamed calls now agree. On MLX, a Harmony tool
    call in a non-streamed reply is no longer lost when the reply starts with analysis.
  • <|return|> and <|call|> never appear in a GPT-OSS answer, streamed or not; a non-streamed
    answer used to end with them when a backend emitted those tokens.
  • Two scheduled requests running at the same time on one MLX native-runtime provider can no longer
    report each other's cached and fed token counts.
  • The last chunk of a stream is the one carrying the provider's finish_reason and usage. Text
    held back because it might have started a tool call is now sent before it; before, it came after
    with finish_reason: "stop", which hid a "length" stop.

Added

  • GET /acore/models/loaded lists models the server process holds outside its managed runtimes,
    including embedding models (runtime_id process:<task>:<provider>:<model>), and such rows can
    be unloaded. task: "embedding" is accepted by the listing and the unload.
  • abstractcore.providers.process_residency: resident_rows() and eject() for the MLX,
    HuggingFace and embeddings backends in one place; abstractcore.providers.hf_residency and
    abstractcore.embeddings.manager.eject_embedding_models() for each backend.
  • The memory snapshot reports device.process_held_bytes, the accelerator memory this process
    holds across MLX, torch and llama.cpp, with device.process_held_basis saying how it was obtained
    (the Metal device counter when torch is loaded, else a sum), plus
    torch_mps_allocated_bytes, torch_mps_driver_bytes, llama_cpp_bytes,
    metal_process_allocated_bytes, and a resident block listing every in-process model across
    backends. GGUF rows whose cache figure includes the f16 KV-cache estimate carry
    kv_bytes_estimated: true.
  • abstractcore.utils.identity: app_identity(), about_fields(), about_lines(),
    about_html() and gateway_version_rows() give every AbstractFramework application the same
    About facts (version, framework, author, licence, source, documentation, issues, feedback) from a
    descriptor shipped with the package.