Repository navigation
AbstractCore v2.16.0
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 asmetadata["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/unloadandunload_afternow 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 aprocess_ejectreport and isok: falsewhen 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
EmbeddingManagerstayed 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 exampleMLX-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 with409(model_lockedormodel_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 afterdrain_timeout_s, nothing is freed
and the report says the call is in flight. device.process_held_byteson NVIDIA GPUs now counts what torch has reserved on the GPU
(device.torch_cuda_reserved_bytes, basiscuda_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()acceptedttl_sandkeep_aliveand ignored them without saying so.
MLX has no idle or time-based unload; the response now lists them underunsupported_options
with a warning.- A streamed MLX call now reports what a non-streamed one does. The stream ends with one chunk
carryingfinish_reason,usageand, when the call has aprompt_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 asmetadata["unparsed_tool_call"]
(format,text,reason) with a warning. - A tool call written as a
```jsonblock ({"name": ..., "arguments": ...},
{"tool_calls": [...]}, a list of calls, or the OpenAIfunctionshape) 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```jsonblock 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. Thefinalchannel streams as text,analysisas reasoning
(metadata["reasoning_delta"], joined intometadata["reasoning"]at the end),commentary
without a recipient as text, and ato=message as a tool call. The<|channel|>and other
framing tokens never appear in the text. - A GPT-OSS reply cut off before its
finalmessage (typically bymax_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 ((...)), andfinish_reasonis"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 withanalysis. <|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_reasonandusage. Text
held back because it might have started a tool call is now sent before it; before, it came after
withfinish_reason: "stop", which hid a"length"stop.
Added
GET /acore/models/loadedlists models the server process holds outside its managed runtimes,
including embedding models (runtime_idprocess:<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()andeject()for the MLX,
HuggingFace and embeddings backends in one place;abstractcore.providers.hf_residencyand
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, withdevice.process_held_basissaying 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 aresidentblock 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()andgateway_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.