Repository navigation
AbstractCore v2.15.0
On a Mac the recommended local model now follows the computer's memory, downloads report real
progress and bring a model's MTP companion along, and loading a model stays offline without
switching the whole process offline. Fetched PDFs no longer leave the machine by default.
Added
- On a Mac the recommended text model follows the computer's memory. One function,
model_catalog.recommended_text_model(), decides it for every surface: the
catalog'srecommended/starterflags, the fresh-install defaults,
config apply-recommended,models download --recommendedand, through the
runtime facade, the Gateway's guide. On Apple silicon it is an MLX build by
unified memory: below 24 GiBmlx-community/Qwen3.5-9B-MLX-4bit, 24 GiB to
below 128 GiBmlx-community/Qwen3.8-27B-4bit, 128 GiB and above
mlx-community/Qwen3.8-Flash-Next-4bit(new catalog row
qwen3.8-flash-next). MLX now leads the Apple-silicon provider order; LM
Studio and Ollama builds stay listed and downloadable. Other hosts keep LM
Studioqwen/qwen3.5-9b@4bit. Each tier also lists its MTP build
(mlx-works/Qwen3.5-9B-oQ4e-mtp,Jundot/Qwen3.8-27B-oQ4e-mtp,
Jundot/Qwen3.8-Flash-Next-oQ4e-mtp, with thespeculationoptions);
MTP_RECOMMENDED(off) switches the recommendation to them. A tier the fit
estimate says will not fit is still the tier, withfits: falseand a
one-sentencewarning;recommended_plan()carries both on the text row.
Newcapability_defaults.recommended_capability_default_routes()/
recommended_model_downloads()are the host-aware views of the portable
tables. See docs/models.md. - 8-bit builds in the catalog, every id verified upstream. 95 artifacts
added: 39 MLX-8bitrepos (plus the 5 tier builds), 24 GGUFQ8_0files
in the repos the catalog already used forQ4_K_M, and 27 Ollama-q8_0
tags whose registry config matches the tag the catalog already lists. Each
carriesupstream: {method, checked, revision?}and the size read upstream;
the seed validator (and schema) reject anupstreamartifact without that
size. LM Studio@8bitids are not added: they cannot be verified upstream
without runninglms. quant_classandquant_class_sourceon every catalog artifact:
2bit,3bit,4bit,5bit,6bit,8bit,16bit,fullor
unknown,statedwhen the reference names its quant,assumedfor a bare
Ollama tag or LM Studio id (the engine's default build, as the fit estimate
assumes),nullwithunknown. Artifacts also carryoptions,note,
companions(an MLX build's MTP drafter, read from the MLX drafter
registry,speculation.mlx_companion_repos, never hand-typed) and
companion_bytes(the drafter's verified size, recorded in the seed's new
companion_sizes); a catalog-sizeddownload_bytesincludes the
companion, and so does the fit estimate.quantandbitsare unchanged. See
docs/models.md.- Fit estimates for the hybrid Qwen3.5/3.8 tier rows use their real KV
geometry. The rows carrykv_geometryread from the upstream
config.json(full-attention layers only: 8 of 32, 16 of 64, 12 of 48),
used before and after install. Qwen3.8 Flash-Next 4-bit went from an
estimated ~199 GiB (a KV cache guessed from 180B parameters) to ~109 GiB
(103.9 GiB of upstream weights + 0.2 GiB KV + 5.2 GiB overhead). Atight
pick now says it fits tightly. - Download jobs report real progress from the first second, for every
source. Ahost_job_v1download now carriesstate(queued, resolving,
downloading, verifying, installing, done, failed, cancelled, stalled),
bytes_done/bytes_total,size_unknown+size_note,percent,
bytes_per_second(last 5 s; falls to 0 when bytes stop),eta_s,
updated_at(at least every 0.5 s),filesandcurrent_file, a
one-sentencemessage("Downloading model.safetensors (2 of 5) · 1.2 GB of
4.8 GB · 38 MB/s · 1 min left"), the tool's own line indetail, and
transitions. A download with no bytes for 15 s turnsstalled, says so,
is logged, and recovers by itself (ABSTRACTCORE_DOWNLOAD_STALL_S). Every
update is appended to<jobs dir>/<job_id>.events.jsonl, never capped.
Hugging Face / MLX / mlx-gen: file list and sizes before the first byte,
per-file bytes from the cache. Ollama: layers added up into one total (the
bar used to restart at 0 % for each layer). LM Studio: the bytes, total and
speedlms getprints (its bar reached the job only as raw text, so no
bytes or percent were ever set), or the bytes landing in the models folder
when it prints none. Supertonic:
per-file bytes, sizes read first. See docs/models.md. - Installed-model rows say what each artifact is and can do.
models_installed_v1rows gainkind(model|adapter|encoder|
embedding, ornull),tasks(for exampletext_generation,
image_to_text,text_embedding,speech_to_text,text_to_image) and
tasks_source. The values come from local evidence only: the cached model
card'spipeline_tag,config.jsonarchitectures,adapter_config.json,
sentence-transformers files, andlms ls --jsontype/vision. No card
is fetched. A LoRA is now visibly an adapter, not a model to load. Ollama
rows stay unknown (null/[]). See
docs/models.md. abstractcore models repair-refs [--dry-run] [--cache-dir DIR] [--json].
Writes the missingrefs/mainof cached Hugging Face repos left by earlier
pinned downloads, so they load by repo id offline again. A repo is repaired
only when it has norefs/mainand exactly one complete snapshot (no
.incompleteblob or unfinished download marker, no dangling file, every
shard of a*.index.jsonpresent, a config or weight file). Repos with
several complete snapshots are reported asambiguousand left alone; an
existingrefs/mainis never changed (one naming a missing snapshot is
reported asdangling_ref); quarantined caches are not scanned. See
docs/models.md.
Fixed
fetch_urlno longer sends fetched PDFs to OpenAI because an API key is set. On the defaultautoroute the PDF router uploaded every
small fetched PDF to the OpenAI-compatible endpoint wheneverOPENAI_API_KEYwas
present, with no enable flag. Local extraction (pypdf, thenpymupdfwhen
installed) is now the only default; remote extraction is an explicit operator
opt-in, the new config keyoffline.allow_remote_pdf_extraction(defaultfalse;
abstractcore --allow-remote-pdf-extraction/--disallow-remote-pdf-extraction).
It gatespreferred_backend="native_llm"too. Results say which extractor ran:
pdf_text_backend,pdf_summary_backend,pdf_backend_attempts(the remote route
is listed as skipped withremote_extraction_disabled) and the new
pdf_remote_extraction_enabled. See docs/web-tools.md.- Embedding caches are no longer emptied at interpreter exit. Every
EmbeddingManagerpickled its whole in-memory cache over the on-disk file at exit,
last writer wins: a process that loaded the cache while it was empty (a test run, a
second app on the same model) and exited later replaced a populated cache with an
empty one (this emptied the operator's OVH embedding caches on 2026-09-24). Saves now
merge with the file as it is on disk (under a lock where the OS has one), are
written to a temp file and renamed into place, never write an empty cache, and are
skipped when the process added nothing. EmbeddingManagerno longer writesHF_HOME,TRANSFORMERS_CACHEand
HF_DATASETS_CACHEinto the process environment. Those writes leaked
into every later import and child process (HF_DATASETS_CACHEwith the wrong
layout). The load location is now per call: the resolved snapshot directory, or
cache_folder=when a download is allowed.- Model lookups read every Hugging Face cache, not only
~/.cache/huggingface/hub. The GGUF search, the similar-GGUF suggestions and
list_available_modelsof the HuggingFace provider, the MLX provider's
list_available_models, the embeddings ONNX probe and the model materializer's
fallback now shareutils.model_cache.hf_hub_cache_dirs()(the cache
huggingface_hubuses, thencache.huggingface_cache_dir), so a relocated cache is
no longer reported as missing. abstractcore --download-vision-modelhonourscache.local_models_cache_dir. It always wrote to~/.abstractcore/models(the 1.3 GBgit-base
of 2026-09-24 landed there). See docs/models.md.- Embeddings tests follow the offline-first load. The 21 mocked
EmbeddingManagertests assumed the repo id reachedSentenceTransformerdirectly
and failed once loading resolved the cached snapshot first; they now run
against a fake cached snapshot (tests/embeddings/conftest.py). - An MTP-preserving MLX checkpoint never loads through mlx-lm. mlx-lm ≤ 0.31.3
qwen3_5.Model.sanitizetreats anymtp.tensor as a raw Hugging
Face checkpoint and adds +1.0 to every RMSNorm weight. Somlx-works/Qwen3.5-9B-oQ4e-mtp
andJundot/Qwen3.8-27B-oQ4e-mtpwere shifted twice and generated garbage whenever the MTP
lane was not entered: companion missing (always the case on a fresh install),
speculation=False, or no drafter. The provider now reads the local indexweight_map
(mlx_native_session.mtp_weight_keys) and loads such checkpoints through mlx-vlm in every
lane. A failed mlx-vlm load raisesProviderAPIErrornaming the model and the reason,
never an mlx-lm fallback. Fixed upstream on mlx-lmmain(ml-explore/mlx-lm#1623), not
released. See docs/speculative-decoding.md. - A model's MTP companion is downloaded, listed and deleted with it.
abstractcore models download mlx <model>, the Gateway's download jobs and--recommended
fetch the registry's MLX drafter (speculation.runtimes.mlx.drafter) in the same job. It
appears as child file rows (role: "mtp_companion"), its size is inbytes_totalfrom the
start, cancel covers it, and the job isdone(and the modelinstalled) only when both
are whole.models listshows the companion under its model (companions).models deleteoffers to remove it (--with-companion/--keep-companion,companion_offer
in JSON) and keeps it while another installed model uses it. Flash-Next (built-in head)
has no companion. Newproviders.speculation.mlx_companion_repos()and
model_materializer.companion_artifacts(). - A missing companion is said in words. The response's
speculation.messageand the
discovery capabilities'messageread: "MTP acceleration off: companion … is not
downloaded; download it withabstractcore models download mlx …". The Gateway's web
and terminal consoles show that text instead of the baremtp_head_not_cachedslug. abstractcore models verify <repo>: a fresh-install inference check. It loads an
installed model with the default configuration (never downloads), checks the answer to
a fixed question and, for a model with a companion, checksspeculation.used. The same
check runs as the opt-in slow testtests/providers/test_mlx_fresh_install_inference_slow.py.- Native MLX error texts name the checkpoint (
<model> (model_type qwen3_5)) instead of
"Native Qwen4" for every model. abstractcore models download --helplists every provider with a download verb,mlx
included. The refusal text already told users to runmodels download mlx ….tests/config/test_model_catalog.py::test_mlx_artifacts_are_not_downloadable_off_apple_silicon
failed whenever llama-cpp-python was importable:engine_inventory()
describes the running interpreter, so the synthetic CUDA host inherited an
installedllamacppengine and the documented rule (an installed engine
outranks the provider order) picked the GGUF. The test now pins "no engine
installed"; a companion test pins the installed-engine rule.- The re-read upstream size of
mlx-community/Qwen3.8-27B-4bitis
16,054,541,349 bytes (the seed had 16,081,490,933 from an older revision). - A pinned Hugging Face download now leaves
refs/main. The downloader
pins the commit it listed (snapshot_download(revision=<sha>)), and
huggingface_hub writes norefs/mainfor a commit-hash revision, so every
loader that resolves a repo by id offline (transformers with
local_files_only,mlx_lm.load("<id>"), vLLM, your own scripts) failed
with "couldn't find them in the cached files". After every planned file is
verified whole, the download writes<repo>/refs/main= that sha when the
repo has norefs/mainyet. An existingrefs/mainnaming another commit
is never overwritten; the completion message then says which commit loading
by name resolves. Existing caches: runabstractcore models repair-refs. - Embedding models apply offline-first on each load.
EmbeddingManager
calledSentenceTransformer(model_id)with nolocal_files_only; it stayed
offline only when the Hugging Face provider had already written
HF_HUB_OFFLINE=1into the process, a write that is now gone. With
offline-first on (orforce_local_files_only), the model id resolves to its
cached snapshot directory (resolve_hf_load_snapshot; a bare legacy name is
also looked up assentence-transformers/<name>) and loads with
local_files_only=True, making no network call. An uncached model raises
ModelNotFoundErrorwith the same "download it first" message as the
Hugging Face provider. --download-vision-modeland the config wizard's "download embeddings
now" say why they cannot download. These are explicit downloads, so
offline_firstdoes not apply and they carry nolocal_files_only. When
the operator setHF_HUB_OFFLINEorTRANSFORMERS_OFFLINEbefore start,
they now stop with a message naming the variable instead of a bare
OfflineModeIsEnabled. The wizard no longer says an uncached embedding
model "will download on first use", which is false under offline-first.- Importing the Hugging Face provider no longer switches the whole process to
Hugging Face offline mode. Withoffline_firston (the default), importing
abstractcore.providers.huggingface_providerwroteTRANSFORMERS_OFFLINE,
HF_DATASETS_OFFLINEandHF_HUB_OFFLINE= 1 intoos.environ. Every child
process inherited them: engine installs, app launches, the tray and tools ran
offline. The write is removed. Offline-first is now applied on each load
call: every transformers call (AutoConfig,AutoTokenizer,
AutoModelForCausalLM/AutoModel,AutoProcessor,
AutoModelForImageTextToText) gets the cached snapshot directory plus
local_files_only=True. The vision loader also no longer writes
TRANSFORMERS_VERBOSITY/DISABLE_TQDMinto the environment. - A fully cached Hugging Face model loads offline. Loads failed with "We
couldn't connect to 'https://huggingface.co' ... couldn't find them in the
cached files" even though every file was on disk. Cause: AbstractCore's
downloader pins the commit it listed (snapshot_download(revision=<sha>)).
For a revision that is already a commit hash, huggingface_hub writes no
refs/main, and transformers needsrefs/mainto find a repo id offline.
Transformers also checked the Hub foradapter_config.jsoneven with
local_files_only=True. The provider now finds the snapshot itself
(utils.model_cache.resolve_hf_load_snapshot:refs/main, else the newest
snapshot that has a config) and passes transformers the directory, so no
load makes a network call. A PEFT adapter (LoRA) whose base model is cached
loads the same way: the base comes from its snapshot and the adapter is
attached from its own directory. Before the base loads, the provider checks
that the installedpeftmeets transformers' ownMIN_PEFT_VERSION(0.19.1
for transformers 5.17). Ifpeftis missing, too old or cannot be imported,
the load raises a plainProviderErrornaming both installed versions and the
pip install -U "peft>=…"fix. It no longer surfaces as anImportError
wrapped inRuntimeError. - An uncached or incomplete Hugging Face model fails at once with a plain
message. With offline-first on, the load raisesModelNotFoundError
instead of a network timeout. The message names the model and the fix
(abstractcore models download huggingface <repo>). It also covers a
snapshot with a config but no weights, a snapshot with only a README (for
example a diffusion LoRA loaded as text), and an adapter whose base model is
not cached. - An explicitly named MLX drafter that is not cached is no longer downloaded
silently. With offline-first on,resolve_native_drafter_pathrefuses it
with the same plain message (abstractcore models download mlx <repo>).
With offline-first off, it downloads the drafter and logs a warning. - The bitsandbytes fused-kernel check no longer lifts an offline flag.
AbstractCore no longer sets that flag. The check runs once per process, and
any failure is reported without a second network attempt. - Loading an MLX model no longer switches the whole process to Hugging Face
offline mode. Withoffline_firston (the default), each MLX load used to
writeHF_HUB_OFFLINE,TRANSFORMERS_OFFLINEandHF_DATASETS_OFFLINE= 1
intoos.environfor the rest of the process. The write never affected the
loader:huggingface_hubreads the flag once, at its import, which mlx-lm
had already triggered. But every child process inherited it, so in the
gateway every download job started after the first MLX load failed with
OfflineModeIsEnabled. The write is removed. "No on-demand download while
loading" still holds: the load resolves to a local cache directory and
raisesModelNotFoundErroron a miss. Tests cover both the unchanged
environment and a cache miss that makes no network call. - An explicit Hugging Face download always reaches the Hub. Download jobs
build the child'sHF_HUB_OFFLINE,TRANSFORMERS_OFFLINEand
HF_DATASETS_OFFLINEfrom the values the operator set before the process
started (newconfig.manager.explicit_download_hf_env()). Values written
later in-process are not passed on, for example by the Hugging Face
provider's import. Detached job processes are started with the same values.
The job log says which ones were withheld. An offline
flag the operator set before start makes the download fail at once, with a
message naming the variable. See
docs/models.md. - Cancelling a download stops it within a second and leaves nothing that
reads as installed. A Hugging Face download from a job runs in a child
process that is stopped at once (it used to run on in a thread until the
hub library's next progress callback); its temporary files
are removed, and a marker keeps a download cancelled between two files from
reading as installed. A quiet Ollama pull is stopped by closing the
connection instead of waiting for the next line.lms getis stopped the way
its own Ctrl-C does, answering No to "continue in the background?", so LM
Studio stops downloading too. Supertonic removes the partial file. - Engine detection finds Ollama and LM Studio installed for one user. A
Mac app placed in~/Applications(a user-level install, no administrator
rights; what AbstractGateway's Install does on a standard account) is now
reported as installed, like one in/Applications. Ollama's CLI is found
inside the app bundle when/usr/local/bin/ollamawas never linked.
Tests
- Tests never touch your home or the network.
Two incidents on 2026-09-24: an unisolated test downloaded
microsoft/git-baseinto~/.abstractcore/modelsand rewrote
abstractcore.json;test_endpoint_profile_can_back_embedding_manager
rewrote the operator'sendpoint_ovh_provider_Qwen3_Embedding_8Bembedding
caches at interpreter exit.tests/conftest.pynow movesHOME,HF_HOME
andHF_HUB_CACHEto tmp at import and per test, clears the exported
ABSTRACT*/HF_*path settings andXDG_*, and fails loudly if
huggingface_hubfroze its cache path on the real home. A socket guard
refuses non-loopback destinations and the live local services on loopback
(8080, 1234, 11434, 18850); every refused attempt fails its test and is listed
with its host and port.@pytest.mark.network("reason")(skipped unless
pytest --allow-network) and@pytest.mark.real_home("reason")are the
opt-outs; a bare marker is a collection error. Tests that reached live
services by accident now use fakes or dead ports (LM Studio base-URL tests,
the companion-delete tests, provider inventory probe, OpenAI construction
preflight, fetch_url DNS via the newfake_public_dnsfixture, the PDF test
that uploaded to OpenAI); live-provider tests carrynetworkmarkers. See
CONTRIBUTING.md.