Compare commits

...

19 Commits

Author SHA1 Message Date
Ettore Di Giacinto
1f995e2a16 docs(blog): cover 4.7 as well, since it never got a post
4.7 shipped on 14 July and never got a blog post or an announcement.
Everything in it has reached only the people who read release notes,
which is a release worth of work nobody has seen.

Added a section near the end covering it: managed voice cloning as a UI
workflow with reusable localai://voice-profiles/<id> URIs, the
longcat-video backend with audio-driven avatars and talking-head
continuation, streaming TTS in vibevoice-cpp taking time to first audio
from 39.96s to 2.38s, one-pass diarized transcription in
moss-transcribe-cpp, F5-TTS in the CrispASR build, interleaved thinking
that survives the tool-result loop, context_size: -1, and per-GPU
offload selection.

Retitled to "What landed in LocalAI 4.7 and 4.8" and said so in the
intro, since a post that silently covers two releases is worse than one
that admits it. The filename is unchanged, so the published URL still
resolves.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude:claude-opus-5 [Claude Code]
2026-08-05 14:52:56 +00:00
Ettore Di Giacinto
42b8130e94 docs(blog): cover the terminal agent, and fix the counts in the intro
The 4.8 post never mentions that `local-ai chat` stopped being a REPL
and became an agent (#11291): the nib harness compiled into the binary,
with tool use behind an approval gate, sub-agents, MCP servers, plugins
and skills, auto-configured against the local instance. It also ships a
shell integration script for zsh, bash and fish that binds Ctrl+Space.

That is one of the larger user-facing changes in the release and it was
missing from both the post and the release-notes highlights. Added a
section after 3D generation, including the breaking changes for anyone
who had habits around the old REPL: `/clear` is gone in favour of
`/compact`, and a model switch now keeps the conversation.

While in the intro, corrected the counts. The post said 374 pull
requests in twenty-one days, which was accurate when it was drafted on
the 4th but not once v4.8.0 was tagged on the 5th. The published release
notes say 386 in twenty-two days, and the intro now matches them rather
than contradicting them.

For the record, neither figure is exactly right: `git log --format=%s
v4.7.1..v4.8.0 | grep -cE '\(#[0-9]+\)$'` counts 388 squash-merged pull
requests, and 389 from v4.7.0. The notes were cut before the last few
landed. Matching the published notes was the priority here, since that
is the artifact everyone else quotes, and 386 is the number already in
circulation.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude:claude-opus-5 [Claude Code]
2026-08-05 14:43:36 +00:00
Ettore Di Giacinto
2c0e7c584d website: re-record the hero and gallery clips for the 4.8 UI
The two landing-page clips predated the v4.8.0 interface work (#11288,
#11305, #11307): the gallery clip showed the retired light-theme Install
Models table, and the hero clip toured the Nodes pages in a full browser
window while its caption promised a chat completion on CPU.

Both are re-recorded from a real local-ai built from v4.8.0, dark theme,
app chrome only:

- hero-ui.mp4: a chat completion on lfm2.5-1.2b-instruct streaming on
  CPU with the live tok/s meter, so the caption now matches the footage.
  The poster frame is regenerated from the new clip.
- gallery.mp4: the Discover rail and detail pane, the hardware
  recommendation lanes, the VRAM-by-context chart, and a real install
  with the live progress banner.

The hand-typed model count moves from 1,585 to 1,255 in the three places
it appears, matching the distinct-model count the recorded UI shows on
screen. The 3d-generation clip is untouched: the post-capture UI changes
do not show in its footage.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude Code:claude-fable-5
2026-08-05 12:46:44 +00:00
localai-org-maint-bot
fb444f917f gallery: add Agents-A1 4B variants (#11365)
Add the official Q4_K_M and Q8_0 GGUF builds with their matching vision projectors so the compact agentic model can be installed through LocalAI.

Assisted-by: Codex:gpt-5 [web]

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-05 09:40:37 +02:00
localai-org-maint-bot
a05a790021 fix(ci): emit verifiable backend signature bundles (#11366)
Cosign v2.4.1 does not select the Sigstore bundle format by default, while LocalAI's verifier only consumes OCI bundle referrers. Request the format explicitly for both registries and guard the producer contract with a shell regression test.

Document strict backend integrity configuration and release-tag identities for operators.

Assisted-by: Codex:gpt-5

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-05 09:39:35 +02:00
localai-org-maint-bot
9f62401fca feat(traces): show in-flight API requests (#11368)
Register JSON API exchanges before their handlers run so the traces dashboard can surface active work. Replace the live entry with the completed persisted record under the same ID, and clean it up if a handler panics.

Assisted-by: Codex:gpt-5

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-05 09:37:05 +02:00
Ettore Di Giacinto
4a5c5e51b7 website: say plainly that engines are swappable behind the same API
The runtime section described the small core and on-demand backends but
never stated the simple fact readers look for: one model can run on
llama.cpp while the next loads on vLLM, SGLang or MLX, behind the same
endpoint, and switching is one line in the model's config.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude Code:claude-fable-5
2026-08-05 07:33:08 +00:00
mudler's LocalAI [bot]
c61b6f2286 docs(blog): new DeepSeek and Laguna numbers, visuals, humanizer pass (#11369)
* docs(blog): new DeepSeek and Laguna numbers, visuals, humanizer pass

vllm.cpp master moved 26 commits past what the post was written against,
and two results changed enough to matter. Both came from the same lever:
staging weights device-resident at load instead of reading them from the
GGUF mmap over unified memory, which the GB10 reads about 20% slower per
GEMV than device memory.

- DeepSeek-V4-Flash against DwarfStar: 0.997x parity becomes 1.144x
  ahead, 18.69 vs 16.33 tok/s decode, same generated tokens.
- Laguna-XS-2.1 against vLLM: 87% becomes 1.03x, 44.46 vs 43.10 tok/s.
  New row in the scoreboard.

Adds three visuals. A chart of throughput against every reference engine,
which is worth having now that the spread is 0.976 to 1.144 rather than a
flat line at parity. The Activity page with four installs running, and the
model detail pane with all four pocket-35b variants. Both screenshots were
recaptured on 2026-08-04 because #11288, #11305, #11307 and #11222 had all
changed those pages since the earlier set.

llama.cpp is deliberately absent from the chart: its 1.18x is a prefill
ratio, and putting it on the same axis as throughput ratios would be
comparing two different measurements.

Also carries the media the release notes embed, since a GitHub release
body needs URLs that survive publishing and drag-and-drop has no CLI.
Supersedes #11364.

Humanizer pass on the prose. The post had collected five exactness idioms
in one section (token-for-token, byte-exact twice, byte-identical,
token-identical). One is precision, five is a tic, so the 27B row keeps
its "token-for-token identical" where identical output is the actual
claim and the rest say what they mean. That also fixed a hyphen in
predicate position ("is token-identical").

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude Code:claude-opus-5 [Read] [Edit] [Bash]

* docs(blog): redraw the benchmark chart as a branded card

The Flint bar chart was generic: default palette, no brand, and drawn
from zero, which made five ratios between 0.976 and 1.144 look like five
bars of roughly equal length.

Redrawn in the style of recorder-for-agents' render-card.sh cards, the
same shape as the vllm.cpp README GIF. Palette taken from the two logos
rather than invented (LocalAI navy #0E2632 and teal #469AAF, vllm.cpp
teal #3AB4CA), SVG generated by a small JS loop so the geometry is exact
at any scale, headless Chrome to PNG at 2x.

The substantive change is that bars now run from the 1.00 parity line
instead of from zero. Deviation is what the data is about, so DeepSeek's
+14.4% and MLX-LM's -2.4% are both legible, and the one row that is
behind is the one row in amber. Each bar carries its ratio and the raw
measurement under it.

Keeps the .html source next to the .png so the chart is editable later:
change a number, re-run render-card.sh.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude Code:claude-opus-5 [Read] [Edit] [Bash]

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
2026-08-05 09:14:51 +02:00
localai-org-maint-bot
0332e9729f gallery: add LFM2.5 2.6B variants (#11351)
Add LiquidAI official Q4_K_M and Q8_0 GGUF builds with linked variant selection and documented generation defaults.

Assisted-by: Codex:gpt-5 [Hugging Face]

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-05 01:37:33 +02:00
mudler's LocalAI [bot]
a8d310573e chore: ⬆️ Update mudler/vllm.cpp to 0757cac231ecd571a83c4fd2f50805c9251fc225 (#11352)
⬆️ Update mudler/vllm.cpp

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-08-05 01:37:09 +02:00
mudler's LocalAI [bot]
144baaa809 chore: ⬆️ Update ggml-org/whisper.cpp to 306c88f4d1286aec1bf96e544632897886af5501 (#11353)
⬆️ Update ggml-org/whisper.cpp

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-08-05 01:36:56 +02:00
mudler's LocalAI [bot]
86c2e9a273 chore: ⬆️ Update leejet/stable-diffusion.cpp to ea7f0c87cfe4c673263b4c201c596c7f1cbe2528 (#11354)
⬆️ Update leejet/stable-diffusion.cpp

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-08-05 01:36:41 +02:00
mudler's LocalAI [bot]
89995d7535 chore: ⬆️ Update 0xShug0/audio.cpp to 238ab6a9e321c17de8e120559f57efeedaeb1345 (#11355)
⬆️ Update 0xShug0/audio.cpp

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-08-05 01:36:26 +02:00
mudler's LocalAI [bot]
1f4ec3bdf8 chore: ⬆️ Update CrispStrobe/CrispASR to ec730908a418b6032f9e69ded6186d3f042a7747 (#11356)
⬆️ Update CrispStrobe/CrispASR

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-08-05 01:36:13 +02:00
mudler's LocalAI [bot]
1466aaa9f7 chore: ⬆️ Update antirez/ds4 to 6747e7718dd08f00b680d0c16231f2d59ec3747e (#11357)
⬆️ Update antirez/ds4

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-08-05 01:36:01 +02:00
mudler's LocalAI [bot]
e6712844ee chore: ⬆️ Update ikawrakow/ik_llama.cpp to 6b55d2c7504f482e7c8ec6cbf22a19f3778c522b (#11358)
⬆️ Update ikawrakow/ik_llama.cpp

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-08-05 01:35:49 +02:00
mudler's LocalAI [bot]
b1d964ef7b chore(model-gallery): ⬆️ update checksum (#11359)
⬆️ Checksum updates in gallery/index.yaml

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-08-05 01:35:37 +02:00
mudler's LocalAI [bot]
0d342c61d8 docs(backends): correct the vllm-cpp description in the gallery (#11363)
This is the text users read in the backends list and the gallery, and it
was the last place still describing vllm.cpp as "a from-scratch C++20
port of vLLM created and maintained by the LocalAI team" with no
indication of maturity.

Three corrections, matching the v4.8 release notes and blog post:

- It leads with ALPHA. These are alpha development builds and llama-cpp
  stays the recommendation for production, which is the single most
  useful thing to know before clicking install.
- It is maintained by the LocalAI team but developed in its own
  repository and usable without LocalAI. vLLM is named for what it
  actually is, the reference implementation that output is checked
  against and benchmarked against, rather than just the thing that was
  ported.
- It records the featureset that has grown past vLLM: GGUF loading,
  speculative decoding and KV offload, alongside the architecture and
  hardware coverage that were already listed.

Also notes that the project is expected to be renamed, with the new name
still to be decided, so anyone who installs it now is not surprised
later.

vllm-cpp-development inherits all of this through the YAML anchor, so
both entries are covered by the one edit. Verified the file still parses
and that both entries carry the new text.


Assisted-by: Claude Code:claude-opus-5 [Read] [Edit] [Bash]

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
2026-08-05 01:35:21 +02:00
mudler's LocalAI [bot]
4fec33966a docs(blog): final figures for the 4.8 post, and the MLX provider (#11362)
* docs(blog): final figures for the 4.8 post, and the MLX provider

The cycle closed at 374 PRs over twenty-one days, not the 321 over
eighteen the post was written against. Corrects the summary, the opening
line, the contributor count and the gallery total, and moves the date to
the day the release is cut.

Adds the MLX GEMM provider (#11137), which merged after the post was
written and is the one number an Apple Silicon reader wants: 1.54x to
2.19x on an M4 with time to first token roughly halving, both arms
toggled on one binary. The +/-10% caveat travels with the table rather
than being left in the PR.

Two lines edited against the no-ai-slop skill while I was in the file,
the same pass #11324 ran over the engines post:

- The opener balanced two clauses across a colon and closed on "without
  lying to you", which is the built-to-be-quoted shape readers picked
  out of the HN thread. It is a flat statement now.
- "This is a new modality rather than a new backend under an existing
  one" is a binary contrast that says nothing the next clause does not.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude Code:claude-opus-5 [Read] [Edit] [Bash]

* docs(blog): call vllm.cpp alpha, and finish the no-ai-slop pass

vllm.cpp is not a released backend and the post read like it was. The
old wording buried the caveat in a block quote at the end of the section
and still said "first release of a young engine". It now says plainly,
before the caveat can be skipped, that these are alpha development
builds, that shipping them in 4.8 is about letting people try the thing
rather than recommending it, and that llama-cpp stays the default.

Also completes the no-ai-slop pass I had only half run. Counting the
lines built to be quoted, headings and section endings included, the post
is in reasonable shape: long flat informational stretches, tables
followed by a plain finding, headings that are labels rather than
epigram-verdicts. Three patterns survived, each one an item in eval.md:

- "and inverts that:" set the usual shape against ours across a colon.
  The sentence works without the frame.
- "Two things were conflated there: a signal, which needs one line, and
  the detail, which needs somewhere to put it" is a role-assignment pair.
  Says what happens instead.
- "The maturity statement from the release notes is worth repeating in
  full" is throat-clearing in front of a quote, and the quote is gone.

Left the rest alone. Minimum effective edit, not a rewrite.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude Code:claude-opus-5 [Read] [Edit] [Bash]

* docs(blog): present vllm.cpp as a community project, with its own numbers

The post described vllm.cpp as "a from-scratch port of vLLM, written and
maintained by the LocalAI team". Two things wrong with that. It is a
community project, and it has stopped being only a port: it loads GGUF,
runs on CPU, Metal and Vulkan, ships speculative decoding and KV offload,
and its benchmark page measures against llama.cpp, MLX-LM and DwarfStar
as well as vLLM, because those are the engines it competes with on that
hardware.

vLLM's role is now stated for what it is, the reference implementation.
Correctness is checked against it and the scoreboard is kept against it.
Also flags that the name will probably change, since it is drifting far
enough that vllm.cpp will eventually mislead.

Adds real numbers from the project's own docs/BENCHMARKS.md rather than
adjectives: 1.045x vLLM at concurrency 1 on Qwen3.6-27B NVFP4 with
token-for-token identical output, 1.010x and 1.013x at c16 and c32 on the
35B MoE and behind below that, prefill 1.18x over llama.cpp on CPU
aarch64, 97.6% of MLX-LM warm total on an M4. Upstream's own caution
travels with them: it treats c2 through c32 as ties because its noise
band is 0.5% and those margins are 0.7% to 1.7%.

Every figure was checked against ~/_git/vllm.cpp/docs/BENCHMARKS.md
rather than restated from memory. The heading is marked alpha to match
the section body.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude Code:claude-opus-5 [Read] [Edit] [Bash]

* docs(blog): say who maintains vllm.cpp, and add the DeepSeek Flash result

Two corrections to the previous commit.

"A community project" says nothing and was not quite true either. The
LocalAI team maintains vllm.cpp. Community-first is the intent, not a
description, so it now says that and says what backs it: its own
repository, its own docs, benchmark record and issue tracker, and it runs
without LocalAI anywhere in the picture.

Adds the DeepSeek-V4-Flash result, which makes the divergence point
better than any of the prose around it. That model does not run on vLLM
on a single GB10: every vLLM-loadable checkpoint is 156 GB or more
against a 119 GiB unified pool, and the only quant that fits is an
extreme-low-bit GGUF that vLLM cannot load. vllm.cpp reads GGUF and runs
it at 16.28 tok/s against ds4's 16.33, a parity result. Also notes MTP
speculative decoding, token-identical to vLLM's and about 4% faster at
concurrency 1.

Both figures checked against ~/_git/vllm.cpp/docs/BENCHMARKS.md.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude Code:claude-opus-5 [Read] [Edit] [Bash]

* docs(blog): lead the DeepSeek result with what we run, not with what vLLM cannot

The previous version opened on "that model does not run on vLLM on a
single GB10 at all". Wrong emphasis twice over: it makes a strong
negative claim about another project the headline, and it buries the
actual result, which is that vllm.cpp runs DeepSeek-V4-Flash at roughly
2-bit (IQ2_XXS mixed, about 80 GB) on a single DGX Spark and decodes at
16.28 tok/s against DwarfStar's 16.33.

The size constraint is still there, stated as the reason the quant is
what it is rather than as a point about vLLM: at 300B+ total parameters
even a 4-bit checkpoint is 156 GB or more, so a 2-bit GGUF is what fits
the Spark's 119 GiB unified pool.

The table row now names the quant and the box (IQ2_XXS, one DGX Spark)
instead of just "GGUF, GB10", since that is the part a reader with a
Spark wants.

Figures unchanged and still from ~/_git/vllm.cpp/docs/BENCHMARKS.md.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude Code:claude-opus-5 [Read] [Edit] [Bash]

* docs(blog): say the new name is undecided

"The name will probably change at some point" invited the obvious
question. It now says the rename is expected and the name is still to be
decided, which is the actual state.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude Code:claude-opus-5 [Read] [Edit] [Bash]

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
2026-08-05 01:23:49 +02:00
29 changed files with 635 additions and 74 deletions

View File

@@ -16,8 +16,7 @@ side (`pkg/oci/cosignverify` plus the gallery YAML).
per-arch manifest before checking signatures.
- **Storage:** Signatures are written as OCI 1.1 referrers
(`--registry-referrers-mode=oci-1-1`) in the new Sigstore bundle format
(current cosign releases do this by default; no `--new-bundle-format`
flag). No `:sha256-<hex>.sig` tag clutter.
(`--new-bundle-format`). No `:sha256-<hex>.sig` tag clutter.
- **Consumer:** `pkg/oci/cosignverify` discovers the bundle via the
referrers API, hands it to `sigstore-go`, and verifies it against the
policy declared in the gallery YAML (`Gallery.Verification`).
@@ -34,14 +33,15 @@ to sign. The job needs:
- `permissions: { id-token: write, contents: read }` at the job level so
the runner can exchange its GitHub OIDC token for a Fulcio cert.
- `sigstore/cosign-installer@v3` step (current cosign releases already
default to the new bundle format).
- `sigstore/cosign-installer@v3` step (the pinned cosign v2 release needs
`--new-bundle-format` explicitly).
- After each `docker buildx imagetools create`, resolve the resulting
list digest with `docker buildx imagetools inspect <tag> --format
'{{.Manifest.Digest}}'` and sign:
```sh
cosign sign --yes --recursive \
--new-bundle-format \
--registry-referrers-mode=oci-1-1 \
"${REGISTRY_REPO}@${DIGEST}"
```
@@ -70,7 +70,7 @@ entry (`backend/index.yaml`):
url: github:mudler/LocalAI/backend/index.yaml@master
verification:
issuer: "https://token.actions.githubusercontent.com"
identity_regex: "^https://github\\.com/mudler/LocalAI/\\.github/workflows/backend_merge\\.yml@refs/heads/master$"
identity_regex: "^https://github\\.com/mudler/LocalAI/\\.github/workflows/backend_merge\\.yml@refs/(heads/master|tags/.+)$"
# Optional revocation cutoff; advance during incident response.
# not_before: "2026-06-01T00:00:00Z"
```

View File

@@ -71,8 +71,8 @@ jobs:
# cosign signs each pushed manifest list with --recursive so the
# index and every per-arch entry get an attached Sigstore bundle.
# Recent cosign releases always emit the new bundle format, so
# there's no extra CLI flag to opt into it.
# The pinned cosign v2 release needs --new-bundle-format explicitly;
# the verifier only consumes OCI 1.1 Sigstore bundle referrers.
- name: Install cosign
if: github.event_name != 'pull_request'
uses: sigstore/cosign-installer@v3
@@ -159,6 +159,7 @@ jobs:
# manifest before checking signatures need the per-arch
# signatures, not just the list-level one.
cosign sign --yes --recursive \
--new-bundle-format \
--registry-referrers-mode=oci-1-1 \
"quay.io/go-skynet/local-ai-backends@${digest}"
@@ -185,6 +186,7 @@ jobs:
' <<< "$DOCKER_METADATA_OUTPUT_JSON")
digest=$(docker buildx imagetools inspect "$first_tag" --format '{{.Manifest.Digest}}')
cosign sign --yes --recursive \
--new-bundle-format \
--registry-referrers-mode=oci-1-1 \
"localai/localai-backends@${digest}"

View File

@@ -9,7 +9,7 @@
# recipe is a make target (not a prepare.sh) so 'make purge && make' is a clean
# rebuild and so the bump bot can see the pin.
AUDIO_CPP_VERSION?=4e3aea2fd99aeaa5924e71c51eb2793846045332
AUDIO_CPP_VERSION?=238ab6a9e321c17de8e120559f57efeedaeb1345
AUDIO_CPP_REPO?=https://github.com/0xShug0/audio.cpp
CURRENT_MAKEFILE_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))

View File

@@ -1,10 +1,10 @@
# ds4 backend Makefile.
#
# Upstream pin lives below as DS4_VERSION?=b7e9f0091139999b6c070a57590c447c5741da5c
# Upstream pin lives below as DS4_VERSION?=6747e7718dd08f00b680d0c16231f2d59ec3747e
# (.github/bump_deps.sh) can find and update it - matches the
# llama-cpp / ik-llama-cpp / turboquant convention.
DS4_VERSION?=b7e9f0091139999b6c070a57590c447c5741da5c
DS4_VERSION?=6747e7718dd08f00b680d0c16231f2d59ec3747e
DS4_REPO?=https://github.com/antirez/ds4
CURRENT_MAKEFILE_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))

View File

@@ -1,5 +1,5 @@
IK_LLAMA_VERSION?=60389410a1ff01f9d37dcc6261db33b3183bdea2
IK_LLAMA_VERSION?=6b55d2c7504f482e7c8ec6cbf22a19f3778c522b
LLAMA_REPO?=https://github.com/ikawrakow/ik_llama.cpp
CMAKE_ARGS?=

View File

@@ -8,7 +8,7 @@ JOBS?=$(shell nproc --ignore=1)
# CrispASR version (release tag)
CRISPASR_REPO?=https://github.com/CrispStrobe/CrispASR
CRISPASR_VERSION?=fe3caf8e363b27572dbdd1a9d37083f25e6decda
CRISPASR_VERSION?=ec730908a418b6032f9e69ded6186d3f042a7747
SO_TARGET?=libgocrispasr.so
CMAKE_ARGS+=-DBUILD_SHARED_LIBS=OFF

View File

@@ -8,7 +8,7 @@ JOBS?=$(shell nproc --ignore=1)
# stablediffusion.cpp (ggml)
STABLEDIFFUSION_GGML_REPO?=https://github.com/leejet/stable-diffusion.cpp
STABLEDIFFUSION_GGML_VERSION?=db99efdd6d2a43c7937fd55b3359206c680a75b0
STABLEDIFFUSION_GGML_VERSION?=ea7f0c87cfe4c673263b4c201c596c7f1cbe2528
CMAKE_ARGS+=-DGGML_MAX_NAME=128

View File

@@ -11,7 +11,7 @@ JOBS?=$(shell nproc --ignore=1 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || e
# vllm.cpp version
VLLM_CPP_REPO?=https://github.com/mudler/vllm.cpp
VLLM_CPP_VERSION?=9d1fad3cde0acb95eb0bb0a1025f40a0eb614147
VLLM_CPP_VERSION?=0757cac231ecd571a83c4fd2f50805c9251fc225
# MLX GEMM provider (darwin/metal only; see the metal branch below for why).
# Consumed as the prebuilt pip wheel: building MLX from source needs `xcrun

View File

@@ -8,7 +8,7 @@ JOBS?=$(shell nproc --ignore=1)
# whisper.cpp version
WHISPER_REPO?=https://github.com/ggml-org/whisper.cpp
WHISPER_CPP_VERSION?=64d57d3df5c8dacee098577257edcaa154bf5ef3
WHISPER_CPP_VERSION?=306c88f4d1286aec1bf96e544632897886af5501
SO_TARGET?=libgowhisper.so
CMAKE_ARGS+=-DBUILD_SHARED_LIBS=OFF

View File

@@ -193,12 +193,22 @@
alias: "vllm-cpp"
license: apache-2.0
description: |
vllm.cpp is a from-scratch C++20 port of vLLM created and maintained by the LocalAI team.
It mirrors vLLM's V1 architecture (paged KV cache, continuous batching, prefix caching,
scheduler, sampler) on a portable tensor runtime with no Python, PyTorch or ggml at
inference time. It loads Hugging Face safetensors and GGUF checkpoints, supports
structured output (JSON schema / regex / choice / GBNF grammar) enforced in-engine,
and runs on CPU, NVIDIA CUDA (Blackwell-family), Apple Metal and Vulkan.
ALPHA development builds. Try it, but llama-cpp stays the recommendation for
production use.
vllm.cpp is an Apache-2.0 C++20 inference engine maintained by the LocalAI team,
developed in its own repository and usable without LocalAI. It began as a port of
vLLM and keeps vLLM as its reference implementation, checking output against it and
benchmarking against it, while growing a featureset of its own. It implements vLLM's
V1 architecture (paged KV cache, continuous batching, prefix caching, scheduler,
sampler) on a portable tensor runtime with no Python, PyTorch or ggml at inference
time. It loads GGUF as well as Hugging Face safetensors, supports structured output
(JSON schema / regex / choice / GBNF grammar) enforced in-engine, ships speculative
decoding and KV offload, and runs on CPU, NVIDIA CUDA (Blackwell-family), Apple
Metal and Vulkan.
The project is expected to be renamed as it diverges further from vLLM; the new
name is still to be decided.
urls:
- https://github.com/mudler/vllm.cpp
tags:

View File

@@ -60,6 +60,7 @@ type APIExchange struct {
}
var traceBuffer *circularbuffer.Queue[APIExchange]
var inFlightTraces = make(map[string]APIExchange)
var mu sync.Mutex
var logChan = make(chan traceCommand, 100)
var traceIDSeq atomic.Uint64
@@ -126,16 +127,17 @@ func initializeTracing(dataPath string, maxItems int) {
continue
}
exchange := *command.exchange
mu.Lock()
delete(inFlightTraces, exchange.ID)
if traceBuffer != nil {
traceBuffer.Enqueue(exchange)
}
mu.Unlock()
if command.store != nil {
if err := command.store.Append(exchange.ID, exchange); err != nil {
xlog.Warn("Failed to persist API trace", "error", err)
}
}
mu.Lock()
if traceBuffer != nil {
traceBuffer.Enqueue(exchange)
}
mu.Unlock()
}
}()
})
@@ -261,6 +263,38 @@ func TraceMiddleware(app *application.Application) echo.MiddlewareFunc {
// tens of MB, which then locks the admin Traces UI fetching the
// JSON dump faster than the 5s auto-refresh.
maxBodyBytes := app.ApplicationConfig().TracingMaxBodyBytes
requestHeaders := redactSensitiveHeaders(c.Request().Header)
requestBody, requestTruncated := truncateForTrace(body, maxBodyBytes)
exchange := APIExchange{
ID: nextTraceID(),
Timestamp: startTime,
ClientIP: c.RealIP(),
UserAgent: c.Request().UserAgent(),
Request: APIExchangeRequest{
Method: c.Request().Method,
Path: c.Path(),
Headers: &requestHeaders,
Body: &requestBody,
BodyTruncated: requestTruncated,
BodyBytes: len(body),
},
}
if user := auth.GetUser(c); user != nil {
exchange.UserID = user.ID
exchange.UserName = user.Name
}
mu.Lock()
inFlightTraces[exchange.ID] = exchange
mu.Unlock()
queued := false
defer func() {
if queued {
return
}
mu.Lock()
delete(inFlightTraces, exchange.ID)
mu.Unlock()
}()
// Wrap response writer to capture body
resBody := new(bytes.Buffer)
@@ -287,47 +321,27 @@ func TraceMiddleware(app *application.Application) echo.MiddlewareFunc {
// the trace endpoint is admin-only but the buffer is also reachable
// via any heap-dump-style introspection, and tokens shouldn't
// outlive the request that carried them.
requestHeaders := redactSensitiveHeaders(c.Request().Header)
requestBody, requestTruncated := truncateForTrace(body, maxBodyBytes)
responseHeaders := redactSensitiveHeaders(c.Response().Header())
responseBody := make([]byte, resBody.Len())
copy(responseBody, resBody.Bytes())
exchange := APIExchange{
ID: nextTraceID(),
Timestamp: startTime,
Duration: time.Since(startTime),
ClientIP: c.RealIP(),
UserAgent: c.Request().UserAgent(),
Request: APIExchangeRequest{
Method: c.Request().Method,
Path: c.Path(),
Headers: &requestHeaders,
Body: &requestBody,
BodyTruncated: requestTruncated,
BodyBytes: len(body),
},
Response: APIExchangeResponse{
Status: status,
Headers: &responseHeaders,
Body: &responseBody,
BodyTruncated: mw.truncated,
BodyBytes: mw.totalBytes,
},
exchange.Duration = time.Since(startTime)
exchange.Response = APIExchangeResponse{
Status: status,
Headers: &responseHeaders,
Body: &responseBody,
BodyTruncated: mw.truncated,
BodyBytes: mw.totalBytes,
}
if handlerErr != nil {
exchange.Error = handlerErr.Error()
}
if user := auth.GetUser(c); user != nil {
exchange.UserID = user.ID
exchange.UserName = user.Name
}
mu.Lock()
store := traceStore
mu.Unlock()
select {
case logChan <- traceCommand{exchange: &exchange, store: store}:
queued = true
default:
xlog.Warn("Trace channel full, dropping trace")
}
@@ -345,6 +359,10 @@ func GetTraces() []APIExchange {
return []APIExchange{}
}
traces := traceBuffer.Values()
for _, exchange := range inFlightTraces {
exchange.Duration = time.Since(exchange.Timestamp)
traces = append(traces, exchange)
}
mu.Unlock()
slices.SortFunc(traces, func(a, b APIExchange) int {

View File

@@ -0,0 +1,108 @@
// SPDX-License-Identifier: MIT
package middleware
import (
"net/http"
"net/http/httptest"
"time"
"github.com/labstack/echo/v4"
"github.com/mudler/LocalAI/core/application"
"github.com/mudler/LocalAI/core/config"
"github.com/mudler/LocalAI/pkg/system"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("live API traces", func() {
newApp := func(root string) *application.Application {
app, err := application.New(
config.EnableTracing,
config.WithDataPath(root),
config.WithDisableLocalAIAssistant(true),
config.WithDisableStats(true),
config.WithSystemState(&system.SystemState{
Model: system.Model{ModelsPath: root},
Backend: system.Backend{BackendsPath: root},
}),
)
Expect(err).NotTo(HaveOccurred())
DeferCleanup(func() { Expect(app.Shutdown()).To(Succeed()) })
ClearTraces()
return app
}
It("lists a request while its handler is still running", func() {
root := GinkgoT().TempDir()
app := newApp(root)
started := make(chan struct{})
release := make(chan struct{})
DeferCleanup(func() {
select {
case <-release:
default:
close(release)
}
})
handler := TraceMiddleware(app)(func(c echo.Context) error {
close(started)
<-release
return c.NoContent(http.StatusNoContent)
})
e := echo.New()
req := httptest.NewRequest(http.MethodPost, "/slow", http.NoBody)
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
rec := httptest.NewRecorder()
ctx := e.NewContext(req, rec)
ctx.SetPath("/slow")
done := make(chan error, 1)
go func() {
done <- handler(ctx)
}()
<-started
var running APIExchange
Eventually(func() bool {
traces := GetTraces()
if len(traces) != 1 {
return false
}
running = traces[0]
return running.Request.Path == "/slow"
}).Should(BeTrue())
Expect(running.Response.Status).To(Equal(0))
Expect(running.Duration).To(BeNumerically(">", 0))
close(release)
Expect(<-done).To(Succeed())
Eventually(func() []APIExchange { return GetTraces() }).Should(ConsistOf(
And(
HaveField("ID", running.ID),
HaveField("Response.Status", http.StatusNoContent),
HaveField("Duration", BeNumerically(">", time.Duration(0))),
),
))
})
It("removes an in-flight trace when the handler panics", func() {
app := newApp(GinkgoT().TempDir())
handler := TraceMiddleware(app)(func(echo.Context) error {
panic("handler panic")
})
e := echo.New()
req := httptest.NewRequest(http.MethodPost, "/panic", http.NoBody)
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
ctx := e.NewContext(req, httptest.NewRecorder())
ctx.SetPath("/panic")
func() {
defer func() { _ = recover() }()
_ = handler(ctx)
}()
Expect(GetTraces()).To(BeEmpty())
})
})

View File

@@ -0,0 +1,22 @@
import { test, expect } from './coverage-fixtures.js'
test('marks an API trace with no response status as in progress', async ({ page }) => {
await page.route('**/api/traces?*', route => route.fulfill({
json: [{
id: 'running-1',
timestamp: '2026-08-05T02:00:00Z',
duration: 2_000_000_000,
request: { method: 'POST', path: '/v1/chat/completions' },
response: { status: 0 },
}],
headers: { 'X-Total-Count': '1' },
}))
await page.route('**/api/backend-traces?*', route => route.fulfill({ json: [] }))
await page.goto('/app/traces')
const row = page.locator('tbody tr').filter({ hasText: '/v1/chat/completions' })
await expect(row.getByText('Running', { exact: true })).toBeVisible()
await expect(row.locator('[title="In progress"]')).toBeVisible()
await expect(row.locator('.fa-check-circle')).toHaveCount(0)
})

View File

@@ -664,10 +664,16 @@ export default function Traces() {
<td><span className="badge badge-info">{trace.request?.method || '-'}</span></td>
<td className="text-mono text-sm">{trace.request?.path || '-'}</td>
<td className="text-sub cell-clip" title={trace.user_name || trace.user_id || ''}>{trace.user_name || trace.user_id || '-'}</td>
<td><span className={`badge ${(trace.response?.status || 0) < 400 ? 'badge-success' : 'badge-error'}`}>{trace.response?.status || '-'}</span></td>
<td>
{trace.response?.status === 0
? <span className="badge badge-info">Running</span>
: <span className={`badge ${trace.response.status < 400 ? 'badge-success' : 'badge-error'}`}>{trace.response.status}</span>}
</td>
<td><LatencyCell ns={trace.duration} max={slowestTrace} /></td>
<td className="text-center">
{trace.error
{trace.response?.status === 0
? <i className="fas fa-spinner fa-spin text-primary" title="In progress" />
: trace.error
? <i className="fas fa-times-circle text-error" title={trace.error} />
: <i className="fas fa-check-circle text-success" />}
</td>

View File

@@ -72,6 +72,44 @@ tags:
- "text-generation"
```
### Verifying OCI Backends
Backend galleries can require keyless Sigstore signatures for every OCI image
they provide. Add a `verification` policy to the gallery configuration, then
enable strict integrity mode:
```bash
export LOCALAI_BACKEND_GALLERIES='[{"name":"localai","url":"github:mudler/LocalAI/backend/index.yaml@master","verification":{"issuer":"https://token.actions.githubusercontent.com","identity_regex":"^https://github\\.com/mudler/LocalAI/\\.github/workflows/backend_merge\\.yml@refs/(heads/master|tags/.+)$"}}]'
export LOCALAI_REQUIRE_BACKEND_INTEGRITY=1
local-ai run
```
The policy pins the Fulcio issuer and the GitHub Actions workflow identity that
signed the image. The identity expression covers development images produced
from `master` and release images produced from tags. Use a narrower expression
if your deployment only accepts one release channel.
Without strict mode, an OCI gallery without a verification policy installs
with a warning. With strict mode, LocalAI refuses galleries without a policy,
images without a compatible Sigstore bundle, and signatures that do not match
the configured identity. Existing images published before bundle signing was
enabled must be rebuilt or re-signed before strict deployments can install
them.
An optional `not_before` RFC3339 value revokes signatures logged before that
time. Advance it after a signing-workflow compromise, then rebuild or re-sign
the trusted images:
```json
{
"verification": {
"issuer": "https://token.actions.githubusercontent.com",
"identity_regex": "^https://github\\.com/mudler/LocalAI/\\.github/workflows/backend_merge\\.yml@refs/(heads/master|tags/.+)$",
"not_before": "2026-08-05T00:00:00Z"
}
}
```
## Pre-installing Backends
You can pre-install backends when starting LocalAI using the `LOCALAI_EXTERNAL_BACKENDS` environment variable:

View File

@@ -9,6 +9,11 @@ LocalAI can retain recent API exchanges and backend operations for inspection
on the **Traces** page in the management interface. Enable tracing in runtime
settings or with the existing tracing configuration.
API requests appear while they are still running. Their elapsed duration
updates when the page refreshes, and the result column marks them as in
progress until the response completes. In-flight requests live only in memory;
the completed exchange is what LocalAI adds to the bounded, persistent history.
API and backend trace histories are persisted in separate directories below
the configured data path. They are restored after a clean service restart,
whether or not authentication is enabled.

View File

@@ -1992,7 +1992,7 @@
files:
- filename: ds4flash.gguf
uri: https://huggingface.co/unsloth/DeepSeek-V4-Flash-GGUF
sha256: 1bfdafd1c288eb1b2bcb629ee9e1b7567dcf0abbe4d20995905a3c3465e9bd1e
sha256: ba1d64ad8d77038124839956b614db2e889daa1a4ddc83060bb06ccb5a1d7461
- name: "qwopus3.6-35b-a3b-coder-mtp"
url: "github:mudler/LocalAI/gallery/virtual.yaml@master"
urls:
@@ -2091,6 +2091,83 @@
- filename: llama-cpp/models/Qwen-AgentWorld-35B-A3B-GGUF/Qwen-AgentWorld-35B-A3B-UD-Q4_K_M.gguf
sha256: e7a8eafdd8013443b6bcc4b6fb47b2d2025f772d359650b9ceb7d75971e22cad
uri: https://huggingface.co/unsloth/Qwen-AgentWorld-35B-A3B-GGUF/resolve/main/Qwen-AgentWorld-35B-A3B-UD-Q4_K_M.gguf
- &agents-a1-4b
name: "agents-a1-4b"
variants:
- model: agents-a1-4b-q8
url: "github:mudler/LocalAI/gallery/virtual.yaml@master"
urls:
- https://huggingface.co/InternScience/Agents-A1-4B
- https://huggingface.co/InternScience/Agents-A1-4B-Q4_K_M-GGUF
description: |
Agents-A1-4B is InternScience's Apache-2.0 dense 4B agentic model, based on
Qwen3.5. It is trained for long-horizon search, engineering and scientific
research, instruction following, tool use, and multimodal tasks. This entry
uses the official Q4_K_M GGUF quantization and vision projector.
license: "apache-2.0"
tags:
- llm
- gguf
- vision
- multimodal
- gpu
- cpu
icon: https://huggingface.co/InternScience/Agents-A1-4B/resolve/main/figures/logo_nobg.png
overrides:
backend: llama-cpp
function:
automatic_tool_parsing_fallback: true
grammar:
disable: true
known_usecases:
- chat
mmproj: llama-cpp/mmproj/Agents-A1-4B-Q4_K_M/Agents-A1-4B-mmproj.gguf
options:
- use_jinja:true
parameters:
model: llama-cpp/models/Agents-A1-4B-Q4_K_M/Agents-A1-4B-Q4_K_M.gguf
template:
use_tokenizer_template: true
files:
- filename: llama-cpp/models/Agents-A1-4B-Q4_K_M/Agents-A1-4B-Q4_K_M.gguf
sha256: d93c393a9bd5139a4b5cfe24d31ef553c5a497bfb8afec178a354ecbf508f062
uri: huggingface://InternScience/Agents-A1-4B-Q4_K_M-GGUF/Agents-A1-4B-Q4_K_M.gguf
- filename: llama-cpp/mmproj/Agents-A1-4B-Q4_K_M/Agents-A1-4B-mmproj.gguf
sha256: 254145e7e03e9e8d3120813fac8033ffa04e411eb6d70a198833504935681084
uri: huggingface://InternScience/Agents-A1-4B-Q4_K_M-GGUF/Agents-A1-4B-mmproj.gguf
- !!merge <<: *agents-a1-4b
name: "agents-a1-4b-q8"
variants: []
urls:
- https://huggingface.co/InternScience/Agents-A1-4B
- https://huggingface.co/InternScience/Agents-A1-4B-Q8_0-GGUF
description: |
Agents-A1-4B is InternScience's Apache-2.0 dense 4B agentic model, based on
Qwen3.5. It is trained for long-horizon search, engineering and scientific
research, instruction following, tool use, and multimodal tasks. This entry
uses the official Q8_0 GGUF quantization and vision projector.
overrides:
backend: llama-cpp
function:
automatic_tool_parsing_fallback: true
grammar:
disable: true
known_usecases:
- chat
mmproj: llama-cpp/mmproj/Agents-A1-4B-Q8_0/Agents-A1-4B-mmproj.gguf
options:
- use_jinja:true
parameters:
model: llama-cpp/models/Agents-A1-4B-Q8_0/Agents-A1-4B-Q8_0.gguf
template:
use_tokenizer_template: true
files:
- filename: llama-cpp/models/Agents-A1-4B-Q8_0/Agents-A1-4B-Q8_0.gguf
sha256: c327f66e820dae550bd230394595071c79f48c88d411b452d013ee4b5999fcea
uri: huggingface://InternScience/Agents-A1-4B-Q8_0-GGUF/Agents-A1-4B-Q8_0.gguf
- filename: llama-cpp/mmproj/Agents-A1-4B-Q8_0/Agents-A1-4B-mmproj.gguf
sha256: 254145e7e03e9e8d3120813fac8033ffa04e411eb6d70a198833504935681084
uri: huggingface://InternScience/Agents-A1-4B-Q8_0-GGUF/Agents-A1-4B-mmproj.gguf
- name: "ornith-1.0-9b"
variants:
- model: ornith-1.0-9b-mtp
@@ -2614,6 +2691,83 @@
- filename: llama-cpp/models/LFM2.5-1.2B-Instruct-GGUF/LFM2.5-1.2B-Instruct-Q4_K_M.gguf
sha256: b1b3de114215d9507409a662a501a631095a479a419584e8a2ded6304b19b4f5
uri: https://huggingface.co/LiquidAI/LFM2.5-1.2B-Instruct-GGUF/resolve/main/LFM2.5-1.2B-Instruct-Q4_K_M.gguf
- &lfm2-5-2-6b
name: "lfm2.5-2.6b"
url: "github:mudler/LocalAI/gallery/virtual.yaml@master"
urls:
- https://huggingface.co/LiquidAI/LFM2.5-2.6B
- https://huggingface.co/LiquidAI/LFM2.5-2.6B-GGUF
description: |
LFM2.5-2.6B is LiquidAI's compact, text-only reasoning model for on-device
agentic workloads. It has 2.69B parameters, a 128K-token context window,
multilingual support, and post-training for tool use, instruction following,
data extraction, RAG, and multi-step agents. This entry uses the recommended
Q4_K_M GGUF quantization from LiquidAI's official repository.
license: "other"
tags:
- llm
- gguf
- reasoning
- cpu
- gpu
icon: https://cdn-uploads.huggingface.co/production/uploads/61b8e2ba285851687028d395/2b08LKpev0DNEk6DlnWkY.png
variants:
- model: lfm2.5-2.6b-q8
overrides:
backend: llama-cpp
context_size: 131072
function:
automatic_tool_parsing_fallback: true
grammar:
disable: true
known_usecases:
- chat
- completion
options:
- use_jinja:true
parameters:
model: llama-cpp/models/LFM2.5-2.6B-GGUF/LFM2.5-2.6B-Q4_K_M.gguf
repeat_penalty: 1.1
temperature: 0.1
top_k: 50
template:
use_tokenizer_template: true
files:
- filename: llama-cpp/models/LFM2.5-2.6B-GGUF/LFM2.5-2.6B-Q4_K_M.gguf
sha256: 79fdf00351b46cf26f020aead28d01889886be87c55fa0eb907e6f9b00bfee14
uri: https://huggingface.co/LiquidAI/LFM2.5-2.6B-GGUF/resolve/main/LFM2.5-2.6B-Q4_K_M.gguf
- !!merge <<: *lfm2-5-2-6b
name: "lfm2.5-2.6b-q8"
description: |
LFM2.5-2.6B is LiquidAI's compact, text-only reasoning model for on-device
agentic workloads. It has 2.69B parameters, a 128K-token context window,
multilingual support, and post-training for tool use, instruction following,
data extraction, RAG, and multi-step agents. This entry uses the higher-quality
Q8_0 GGUF quantization from LiquidAI's official repository.
variants: null
overrides:
backend: llama-cpp
context_size: 131072
function:
automatic_tool_parsing_fallback: true
grammar:
disable: true
known_usecases:
- chat
- completion
options:
- use_jinja:true
parameters:
model: llama-cpp/models/LFM2.5-2.6B-GGUF/LFM2.5-2.6B-Q8_0.gguf
repeat_penalty: 1.1
temperature: 0.1
top_k: 50
template:
use_tokenizer_template: true
files:
- filename: llama-cpp/models/LFM2.5-2.6B-GGUF/LFM2.5-2.6B-Q8_0.gguf
sha256: 36587fdf27bdfc69caf2637273679a0870ec155162161bde6fd16e8c70bdb757
uri: https://huggingface.co/LiquidAI/LFM2.5-2.6B-GGUF/resolve/main/LFM2.5-2.6B-Q8_0.gguf
- name: "qwopus3.6-27b-coder-compat-mtp"
url: "github:mudler/LocalAI/gallery/virtual.yaml@master"
urls:

View File

@@ -0,0 +1,13 @@
#!/usr/bin/env bash
set -euo pipefail
WORKFLOW="$(dirname "$(realpath "$0")")/../../.github/workflows/backend_merge.yml"
sign_commands=$(grep -Ec -- '^[[:space:]]+cosign sign([[:space:]]|$)' "$WORKFLOW" || true)
bundle_flags=$(grep -Ec -- '^[[:space:]]+--new-bundle-format([[:space:]]|$)' "$WORKFLOW" || true)
if [ "$sign_commands" -ne 2 ] || [ "$bundle_flags" -ne "$sign_commands" ]; then
echo "FAIL: every backend signing command must request the new bundle format (commands=$sign_commands flags=$bundle_flags)"
exit 1
fi
echo "PASS: backend signing emits Sigstore bundles for both registries"

View File

@@ -1,14 +1,16 @@
---
title: "What landed in LocalAI 4.8"
date: 2026-08-01
title: "What landed in LocalAI 4.7 and 4.8"
date: 2026-08-04
author: "Ettore Di Giacinto"
category: "Release"
tags: ["release", "vllm.cpp", "audio.cpp", "3d", "gallery", "distributed", "performance"]
summary: "A new inference engine, 3D generation, one backend that serves six audio endpoints, and a web interface 3.48x lighter. 321 pull requests in eighteen days."
tags: ["release", "vllm.cpp", "audio.cpp", "3d", "agent", "gallery", "distributed", "performance"]
summary: "A new inference engine, a terminal agent in the CLI, 3D generation, and a web interface 3.48x lighter. Plus the 4.7 release we never wrote up."
extracss: ["blog.css"]
---
LocalAI 4.8.0 is out. It took eighteen days and 321 merged pull requests, and it pulls in two directions at once: three new things LocalAI can do that it could not do before, and a long list of places where it now does the old things without lying to you.
LocalAI 4.8.0 is out, after twenty-two days and 386 merged pull requests. There are four new things LocalAI can do, and a lot of repair work on things it already did.
We also never got around to writing this post for 4.7, so it is in here too, near the bottom. If you do not read release notes, that release has not reached you at all.
The full notes list everything. This post covers the parts that change what you do day to day, with the pull request numbers so you can read the diffs.
@@ -36,6 +38,11 @@ The third one was `/api/traces` returning a 21 MB unpaginated blob that the UI p
## One gallery entry, several builds
<figure>
<img src="/media/v4-8-0-ui-model-variants.png" alt="The model detail pane listing every variant">
<figcaption>One entry, four builds. LocalAI picks the largest that fits and marks it auto-selected.</figcaption>
</figure>
Installing a model no longer means reading a list of quantizations and guessing which one your card will hold. A gallery entry can now declare `variants:`, a list of references to other entries that are alternative builds of the same weights:
```yaml
@@ -55,12 +62,43 @@ Every surface can override the choice: `variant` on `POST /models/apply`, `local
One gap worth knowing about: in distributed mode `InstallModel` resolves against the frontend rather than the worker that will serve the model, so a cluster with a small frontend and large workers selects conservatively. PRs [#10943](https://github.com/mudler/LocalAI/pull/10943), [#10983](https://github.com/mudler/LocalAI/pull/10983), [#10992](https://github.com/mudler/LocalAI/pull/10992), [#11027](https://github.com/mudler/LocalAI/pull/11027) and [#11139](https://github.com/mudler/LocalAI/pull/11139).
## A new engine: vllm.cpp
## A new engine: vllm.cpp (alpha)
[vllm.cpp](https://github.com/mudler/vllm.cpp) is a from-scratch C++20 port of vLLM, written and maintained by the LocalAI team under Apache-2.0, and it ships here as the `vllm-cpp` backend ([#11100](https://github.com/mudler/LocalAI/pull/11100)). It mirrors vLLM's V1 architecture, so paged KV cache, continuous batching, prefix caching, scheduler and sampler, on a portable tensor runtime with no Python, no PyTorch and no ggml at inference. It loads Hugging Face safetensors and GGUF, enforces structured output inside the engine (JSON schema, regex, choice, GBNF), and builds for CPU amd64 and arm64, CUDA 12 and 13 including Blackwell, L4T for GB10, Vulkan and Darwin Metal.
[vllm.cpp](https://github.com/mudler/vllm.cpp) is Apache-2.0 and maintained by the LocalAI team. We want it community-first rather than a LocalAI-only engine, so it lives in its own repository with its own docs, benchmark record and issue tracker, and it runs without LocalAI anywhere in the picture. It began as a C++20 port of vLLM. It ships here as the `vllm-cpp` backend ([#11100](https://github.com/mudler/LocalAI/pull/11100)). It implements vLLM's V1 architecture, so paged KV cache, continuous batching, prefix caching, scheduler and sampler, on a portable tensor runtime with no Python, no PyTorch and no ggml at inference. vLLM stays its reference implementation: correctness is checked by comparing output against it, and the benchmark scoreboard is kept against it.
It has grown features vLLM does not have, which is most of the reason the port exists. It loads GGUF as well as safetensors, runs on CPU, Apple Metal and Vulkan alongside CUDA 12 and 13 and L4T for GB10, and ships speculative decoding and KV offload. Its benchmark page now measures against llama.cpp, MLX-LM and DwarfStar as well as vLLM, because on that hardware those are the engines it competes with. The project is expected to be renamed, with the new name still to be decided; it is drifting far enough that vllm.cpp will eventually mislead.
Tool calling is at llama.cpp parity by construction, because chat deliberately reuses the same autoparser path: full minja chat templates, `tool_choice: auto` lowered to a lazy structural-tag decode constraint, 30 tool dialects, 7 reasoning parsers, and streamed `ChatDelta` and `ToolCallDelta`.
<figure>
<img src="/media/v4-8-0-vllm-cpp-scoreboard.png" alt="Throughput of vllm.cpp relative to each reference engine, drawn as deviation from parity">
<figcaption>llama.cpp is left out because its 1.18x is a prefill ratio, and putting that on the same axis as throughput would compare two different measurements.</figcaption>
</figure>
Numbers from the project's own [scoreboard](https://github.com/mudler/vllm.cpp/blob/master/docs/BENCHMARKS.md), which calls ties ties and losses losses. Above 1.0 means vllm.cpp is ahead:
<div class="tw">
<table>
<thead><tr><th>Reference</th><th>Workload</th><th>Result</th></tr></thead>
<tbody>
<tr><td>vLLM</td><td>Qwen3.6-27B NVFP4, GB10</td><td>1.045x at concurrency 1, 1.007x to 1.017x from c2 to c32, output token-for-token identical</td></tr>
<tr><td>vLLM</td><td>Qwen3.6-35B-A3B NVFP4, GB10</td><td>1.010x at c16 and 1.013x at c32, behind from c1 to c8 (0.817x at c1)</td></tr>
<tr><td>llama.cpp</td><td>Qwen3.5-2B GGUF, CPU aarch64</td><td>prefill 1.18x, decode a tie, memory parity</td></tr>
<tr><td>MLX-LM</td><td>Qwen3-0.6B, Apple M4</td><td>97.6% of warm total, prefill ahead</td></tr>
<tr><td>DwarfStar (ds4)</td><td>DeepSeek-V4-Flash IQ2_XXS, one DGX Spark</td><td>18.69 vs 16.33 tok/s decode, <b>1.144x</b>, same output</td></tr>
<tr><td>vLLM</td><td>Laguna-XS-2.1 NVFP4, GB10</td><td>44.46 vs 43.10 tok/s, <b>1.03x</b>, same output</td></tr>
</tbody>
</table>
</div>
The upstream page is careful about its own noise: on the 27B grid the run-to-run spread is 0.5% and c2 through c32 land between 0.7% and 1.7%, so it calls those five ties rather than wins. The concurrency-1 result is the one it stands behind.
The DeepSeek-V4-Flash row is the one that shows how far this has moved from being a vLLM port. It runs DeepSeek-V4-Flash at roughly 2-bit (IQ2_XXS mixed, about 80 GB) on a single DGX Spark, decoding at 18.69 tok/s against DwarfStar's 16.33. At 300B+ total parameters even a 4-bit checkpoint is 156 GB or more, so a 2-bit GGUF is what fits inside the Spark's 119 GiB unified pool, and reading GGUF is what makes that possible.
That number moved twice in a week, and the second move came from one lever. The dense Q8_0 projection tower was being read from the GGUF mmap over unified memory, which the GB10 reads about 20% slower per-GEMV than device memory. Staging that 6 GiB tower device-resident once at load, same bytes and same kernels, took decode from 16.23 to 18.69, generating the same tokens and using no more peak memory. The same change took Laguna-XS-2.1 from 87% of vLLM to 1.03x ahead of it.
Speculative decoding is in similar shape: MTP on Qwen3.6-27B NVFP4 generates the same tokens as vLLM's MTP and runs about 4% faster at concurrency 1.
Configuration is a normal backend install:
```yaml
@@ -73,9 +111,24 @@ options:
- max_num_seqs:16 # also: block_size:<n>, num_blocks:<n>
```
The CPU path is verified end to end against `Qwen3.5-2B-UD-Q8_K_XL.gguf` with the full Ginkgo suite, covering blocking and streaming byte-parity, greedy determinism, stop words, GBNF-constrained generation, concurrent streams, reasoning split and both `required` and `auto` tool calls. The maturity statement from the release notes is worth repeating in full:
**Treat these as alpha development builds, not a released backend.** vllm.cpp is early, and shipping it in 4.8 is about getting it in front of people who want to try it, not about recommending it for anything you care about. `llama-cpp` stays the default for real use.
> The GPU images build and ship, but their runtime behavior has not been through the same e2e gate yet. This is a first release of a young engine: no throughput comparison against upstream vLLM is claimed here, and `llama-cpp` remains the default recommendation for general use. Try it, and please report what breaks.
The CPU path is verified end to end against `Qwen3.5-2B-UD-Q8_K_XL.gguf` with the full Ginkgo suite, covering blocking and streaming byte-parity, greedy determinism, stop words, GBNF-constrained generation, concurrent streams, reasoning split and both `required` and `auto` tool calls. The GPU images build and ship, but their runtime behavior has not been through that gate. No throughput comparison against upstream vLLM is claimed. Expect rough edges, and please report what breaks.
On Apple Silicon the image now ships vllm.cpp's MLX GEMM provider ([#11137](https://github.com/mudler/LocalAI/pull/11137)). Upstream keeps it off by default because it adds about 124 MB, so we measured before turning it on. Qwen3-1.7B-bf16 on an M4, p=512 g=128, both arms toggled on one binary so a build difference cannot explain the gap:
<div class="tw">
<table>
<thead><tr><th>Batch</th><th>MLX tok/s</th><th>native tok/s</th><th>speedup</th><th>MLX TTFT</th><th>native TTFT</th></tr></thead>
<tbody>
<tr><td>1</td><td>5.79</td><td>3.08</td><td><b>1.88x</b></td><td>3.32 s</td><td>7.68 s</td></tr>
<tr><td>4</td><td>15.75</td><td>10.24</td><td><b>1.54x</b></td><td>9.63 s</td><td>18.77 s</td></tr>
<tr><td>16</td><td>38.65</td><td>17.69</td><td><b>2.19x</b></td><td>18.33 s</td><td>54.48 s</td></tr>
</tbody>
</table>
</div>
Two reps, with rep spread reaching 9.4%, so treat the multipliers as +/-10%. Time to first token roughly halves across the range.
<figure>
<video src="/media/vllm-race.mp4" muted loop playsinline preload="none" data-lazy aria-label="vllm.cpp generating tokens"></video>
@@ -84,7 +137,7 @@ The CPU path is verified end to end against `Qwen3.5-2B-UD-Q8_K_XL.gguf` with th
## LocalAI generates 3D models now
This is a new modality rather than a new backend under an existing one, so it goes through the whole stack: a `Generate3D` RPC in `backend.proto`, a `FLAG_3D` capability so the loader knows which backends can serve it, and `POST /v1/3d/generations`.
3D generation is a new modality, so it had to be wired through the whole stack: a `Generate3D` RPC in `backend.proto`, a `FLAG_3D` capability so the loader knows which backends can serve it, and `POST /v1/3d/generations`.
The first engine behind it is `trellis2cpp`, an image-to-3D backend over TRELLIS.2. You give it an image, you get a GLB back. The web UI has a page for it with a native GLB viewer, so you can turn the result around in the browser instead of downloading it to find out whether it worked, history kept in IndexedDB so a reload does not lose your generations, and previewable print remeshing for output you actually intend to send to a printer ([#10979](https://github.com/mudler/LocalAI/pull/10979)).
@@ -93,9 +146,23 @@ The first engine behind it is `trellis2cpp`, an image-to-3D backend over TRELLIS
<figcaption>trellis2-4b, 2,502,928 vertices and 5,012,118 triangles, turning in the browser. The remesh slider below it is the print path.</figcaption>
</figure>
## `local-ai chat` stopped being a REPL
`local-ai chat` used to be a chat prompt in a terminal. It is now an agent, and it is the [nib](https://github.com/mudler/nib) harness compiled straight into the binary: tool use behind an approval gate, sub-agents, MCP servers, plugins and skills, auto-configured against your own instance. Nothing extra to install.
```bash
local-ai chat # the agent, pointed at your models
echo "what is 2+2" | local-ai chat --cli
local-ai chat --init zsh # Ctrl+Space from any shell prompt
```
That last one prints a shell integration script (zsh, bash or fish), so you can pull the agent up from wherever you already are instead of opening something else.
It runs shell commands now, so every tool call goes through an approval prompt you control, and read-only ones like `ls` and `cat` run without asking. If you had habits around the old REPL, a few things moved: `/clear` is gone and `/compact` is the closest thing, `/models` and `/model <name>` mean what they always meant, and switching model keeps the conversation instead of starting over ([#11291](https://github.com/mudler/LocalAI/pull/11291)).
## One backend, six audio endpoints
The usual shape for audio is one backend per model family, which means a process per capability and a config file for each. `audio-cpp` wraps [audio.cpp](https://github.com/0xShug0/audio.cpp), a multi-family ggml audio engine, and inverts that: one backend process serves several unrelated families through a single runtime vocabulary, and works out which family a checkpoint belongs to from the GGUF's own `audiocpp.model_spec.family` metadata key. There is nothing backend-specific to write in the model config.
The usual shape for audio is one backend per model family, which means a process per capability and a config file for each. `audio-cpp` wraps [audio.cpp](https://github.com/0xShug0/audio.cpp), a multi-family ggml audio engine. One backend process serves several unrelated families through a single runtime vocabulary, and works out which family a checkpoint belongs to from the GGUF's own `audiocpp.model_spec.family` metadata key. There is nothing backend-specific to write in the model config.
<div class="tw">
<table>
@@ -130,7 +197,12 @@ The `bonsai` backend serves the 1-bit (Q1_0) and ternary (Q2_0) Bonsai quantizat
## The operations bar became a page
The old operations bar rendered one row per in-flight operation above every page. Queue four model installs and a backend and it took most of the viewport, on every route, until the last one finished. Two things were conflated there: a global "something is happening" signal, which needs one line, and the detail of what is happening, which needs somewhere to put it.
<figure>
<img src="/media/v4-8-0-ui-activity.png" alt="The Activity page with four installs running">
<figcaption>Four backend installs in flight, and the record of what already finished.</figcaption>
</figure>
The old operations bar rendered one row per in-flight operation above every page. Queue four model installs and a backend and it took most of the viewport, on every route, until the last one finished. It was doing two jobs at once. A global "something is happening" signal only needs one line, and the detail of what is happening needs a page of its own.
The strip is now one line, permanently, showing a failure first and otherwise the least-advanced running operation, with a `+N more` pill. Its `✕` hides the strip and no longer cancels anything. That is a deliberate behavior change worth knowing about before you click it out of habit: the same glyph used to cancel a 17 GB download in one row and dismiss a message in the next. Cancelling moved to the new page, behind a button that says so.
@@ -167,6 +239,18 @@ Inline reward code is now refused unless the operator sets `LOCALAI_TRL_ALLOW_IN
Two more hardening fixes landed in the same cycle. Tar hardlinks that escape the extraction root are now rejected: the archive extractor pre-scanned members and rejected symlinks, but a tar hardlink entry carries a regular file mode and passed that check, and `Header.Linkname` was never validated, so an archive could link to a path outside the destination ([#11266](https://github.com/mudler/LocalAI/pull/11266)). And a cyclic `$ref` in a JSON-schema grammar is now rejected rather than recursing into a stack overflow ([#11041](https://github.com/mudler/LocalAI/pull/11041)). This release also picks up hono 4.12.25 for CVE-2026-54290 ([#11023](https://github.com/mudler/LocalAI/pull/11023)).
## The 4.7 release nobody heard about
We skipped this post for 4.7. If you do not read release notes, none of what follows has reached you, and a couple of them are things you would have wanted a month ago.
Voice cloning became a UI workflow instead of a YAML exercise. You record or upload a consented reference in the web UI, normalize it, preview it, and save it as a named profile, then reference it as `localai://voice-profiles/<id>` from the TTS page or from `/v1/audio/speech`. Nothing gets copied into model directories by hand. Which backends can do it is discovered from their declared capabilities rather than a hardcoded list, so every cloning-capable backend picks it up, and the UI offers gallery models to install if you have none ([#10799](https://github.com/mudler/LocalAI/pull/10799)).
The `longcat-video` backend brought text-to-video, image-to-video and audio-driven avatar generation, including talking-head continuation across segments, with audio and reference-image controls in the Studio UI ([#10792](https://github.com/mudler/LocalAI/pull/10792)).
`vibevoice-cpp` got real streaming TTS, replacing whole-clip-then-chunk synthesis with incremental output through a new callback ABI. Time to first audio went from 39.96s to 2.38s on a CPU-only box for `VibeVoice-Realtime-0.5B` ([#10764](https://github.com/mudler/LocalAI/pull/10764)). `moss-transcribe-cpp` does multi-speaker transcription, diarization and timestamps in one offline pass, byte-exact against the reference PyTorch and 1.6 to 2.2x faster on CPU ([#10756](https://github.com/mudler/LocalAI/pull/10756)). And F5-TTS is linked into the CrispASR build with its own gallery model ([#10753](https://github.com/mudler/LocalAI/pull/10753)).
An assistant turn can also carry `reasoning` and `tool_calls` together now, and keep the reasoning across the tool-result loop ([#10744](https://github.com/mudler/LocalAI/pull/10744)). `context_size: -1` runs a model at its full trained context window, read per-model from GGUF metadata, with a warning when it will not fit ([#10752](https://github.com/mudler/LocalAI/pull/10752)). And `devices:` picks which GPUs llama.cpp offloads to ([#10724](https://github.com/mudler/LocalAI/pull/10724)).
## The rest, briefly
Hugging Face model artifacts are now a managed snapshot flow: immutable snapshot resolution, authenticated downloads with real progress, materialization on gallery install and preload, runtime binding to staged artifacts, and per-file resume of an interrupted download rather than starting over. Python backends reuse the Go download path instead of fetching on their own.
@@ -179,6 +263,6 @@ Valkey Search joins the vector store options as the `valkey-store` backend ([#11
This is also the release where localai.io split in two: the project site at the root, and the documentation under `/docs/`. Every URL that was published before still resolves, through 214 generated redirect stubs, because GitHub Pages has no server-side rewrites to do it properly ([#11243](https://github.com/mudler/LocalAI/pull/11243)).
Twenty-four people contributed to this release, eleven of them for the first time. The gallery went from 1,221 entries to 1,505.
Twenty-five people contributed to this release, eleven of them for the first time. The gallery went from 1,221 entries to 1,515.
To upgrade, pull `localai/localai:latest` or re-run the install script. The [full changelog](https://github.com/mudler/LocalAI/compare/v4.7.1...v4.8.0) has everything this post left out.

View File

@@ -19,7 +19,7 @@
<div><b class="tnum" data-count="{{ .Site.Data.stats.stars }}">0</b><span>GitHub stars</span></div>
<div><b class="tnum" data-count="73">0</b><span>Backends</span></div>
<div><b class="tnum" data-count="{{ len .Site.Data.engines.engines }}">0</b><span>Engines we wrote</span></div>
<div><b class="tnum" data-count="1585">0</b><span>Models, one click</span></div>
<div><b class="tnum" data-count="1255">0</b><span>Models, one click</span></div>
</div>
</div>
<div class="fd">
@@ -39,7 +39,8 @@
<p class="kicker rv">The runtime</p>
<h2 class="rv mt1" style="max-width:21ch">Everything else plugs into LocalAI.</h2>
<p class="lede rv mt2">One binary with an OpenAI-compatible API in front of it. Point an existing client at it and the calls keep working, except now the model is on your machine. It also speaks the Anthropic, Ollama and ElevenLabs APIs, so most tools need a URL change and nothing else.</p>
<p class="lede rv mt2">Underneath, a small core pulls each engine in as a separate backend, only when a model asks for it. That is why one install covers this much ground without becoming a 9 GB download.</p>
<p class="lede rv mt2">The engine behind that API is swappable. One model can run on llama.cpp while the next loads on vLLM, SGLang or MLX, and the client never notices: same endpoint, same request, different engine underneath. Switching is one line in the model's config.</p>
<p class="lede rv mt2">A small core pulls each engine in as a separate backend, only when a model asks for it. That is why one install covers this much ground without becoming a 9 GB download.</p>
<div class="apis rv">
<span>OpenAI API</span><span>Anthropic API</span><span>Ollama API</span><span>ElevenLabs API</span><span>Realtime over WebRTC</span>
</div>
@@ -57,7 +58,7 @@
</div>
<div class="duo__m rv">
<figure class="screen" style="margin:0">
<figcaption class="screen__bar"><i></i> localai · model gallery <b>1,585 models</b></figcaption>
<figcaption class="screen__bar"><i></i> localai · model gallery <b>1,255 models</b></figcaption>
<video src="/media/gallery.mp4" muted loop playsinline preload="none" data-lazy aria-label="Installing a model from the LocalAI gallery"></video>
</figure>
</div>
@@ -327,7 +328,7 @@
<div class="shell">
<div class="bars rv" aria-hidden="true"><i></i><i></i><i></i><i></i></div>
<p class="kicker rv">The gallery</p>
<h2 class="rv mt1" style="max-width:20ch">1,585 models. No notebook, no conversion script.</h2>
<h2 class="rv mt1" style="max-width:20ch">1,255 models. No notebook, no conversion script.</h2>
<div class="cards">
<a class="cd rv" href="/docs/getting-started/models/"><p class="cd__k">Quantizations</p><h3>201 APEX builds</h3>
<p>Every tier of every model we quantize, ranked against the hardware you actually have and installed with one click.</p><span class="cd__go">Browse the gallery →</span></a>

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 75 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 646 KiB

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

View File

@@ -0,0 +1,100 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<style>
/* palette lifted from the two logos:
LocalAI #0E2632 navy, #385360 slate, #469AAF teal, #90A8AE haze
vllm.cpp #3AB4CA teal, #95C4D1 light */
:root{
--bg:#0b1c25; --ink:#e8f1f4; --dim:#90a8ae; --faint:#5d757f;
--teal:#3ab4ca; --teal-hi:#7fd4e2; --amber:#e0a944; --rule:#1d3440;
}
*{margin:0;padding:0;box-sizing:border-box}
html,body{width:1600px;height:900px}
body{
background:radial-gradient(1250px 720px at 80% -12%, #143140 0%, var(--bg) 62%);
color:var(--ink);
font-family:-apple-system,"SF Pro Display","Segoe UI",Helvetica,Arial,sans-serif;
-webkit-font-smoothing:antialiased; padding:58px 84px; position:relative;
}
.eyebrow{display:flex;align-items:center;gap:14px;color:var(--teal);
font-weight:600;font-size:23px;letter-spacing:.14em;text-transform:uppercase}
.eyebrow .dot{width:11px;height:11px;border-radius:50%;background:var(--teal);
box-shadow:0 0 16px 2px var(--teal)}
h1{font-size:56px;line-height:1.06;font-weight:760;margin:16px 0 6px;letter-spacing:-.02em}
h1 .grad{background:linear-gradient(92deg,var(--teal),var(--teal-hi));
-webkit-background-clip:text;background-clip:text;color:transparent}
.sub{color:var(--dim);font-size:23px;margin-bottom:14px}
svg{width:100%;height:auto;display:block}
.foot{position:absolute;left:84px;right:84px;bottom:40px;display:flex;
justify-content:space-between;align-items:center;color:var(--faint);
font-size:21px;border-top:1px solid var(--rule);padding-top:16px}
.foot .link{color:var(--ink);font-weight:600}
</style>
</head>
<body>
<div class="eyebrow"><span class="dot"></span>vllm.cpp &middot; throughput vs the reference engine</div>
<h1>Measured against <span class="grad">what each workload actually runs on</span></h1>
<div class="sub">Throughput relative to the reference. 1.00 is parity, bars run from it. Higher is faster.</div>
<svg id="c" viewBox="0 0 1432 585"></svg>
<div class="foot">
<span class="link">github.com/mudler/vllm.cpp</span>
<span>GB10 unless noted &middot; greedy, reference in its own production config &middot; docs/BENCHMARKS.md</span>
</div>
<script>
const rows = [
{ref:'DwarfStar (ds4)', work:'DeepSeek-V4-Flash IQ2_XXS', v:1.144, note:'18.69 vs 16.33 tok/s'},
{ref:'vLLM', work:'Qwen3.6-27B NVFP4, c1', v:1.045, note:'86.05 vs 82.32 tok/s'},
{ref:'vLLM', work:'Laguna-XS-2.1 NVFP4', v:1.030, note:'44.46 vs 43.10 tok/s'},
{ref:'vLLM', work:'Qwen3.6-35B-A3B, c32', v:1.013, note:'3030.5 vs 2993.0 tok/s'},
{ref:'MLX-LM', work:'Qwen3-0.6B, Apple M4', v:0.976, note:'97.6% of warm total'},
];
const W=1432, H=585;
const AX=64; // axis strip reserved at the bottom
const LBL=470; // left label gutter
const R=150; // right gutter for the value
const lo=-0.055, hi=0.165; // deviation domain around parity
const pw=W-LBL-R;
const x = d => LBL + pw*((d-lo)/(hi-lo));
const zero = x(0);
const rowH = (H-AX)/rows.length;
const barH = 46;
let g='';
// faint engineering grid at 2% steps
for(let d=-0.04; d<=0.16001; d+=0.02){
const gx=x(d), on0=Math.abs(d)<1e-9;
g+=`<line x1="${gx}" y1="4" x2="${gx}" y2="${H-AX+10}" stroke="${on0?'#4a6b78':'#16303c'}" stroke-width="${on0?2:1}"/>`;
g+=`<text x="${gx}" y="${H-22}" fill="${on0?'#90a8ae':'#4d6570'}" font-size="17" text-anchor="middle"
font-weight="${on0?'700':'400'}">${(1+d).toFixed(2)}</text>`;
}
rows.forEach((r,i)=>{
const cy = i*rowH + rowH/2;
const d = r.v-1;
const ahead = d>=0;
const col = ahead ? '#3ab4ca' : '#e0a944';
const x0 = ahead ? zero : x(d);
const w = Math.abs(x(d)-zero);
// reference + workload, two weights on one line
g+=`<text x="${LBL-26}" y="${cy-4}" fill="#e8f1f4" font-size="25" font-weight="670" text-anchor="end">${r.ref}</text>`;
g+=`<text x="${LBL-26}" y="${cy+22}" fill="#5d757f" font-size="19" text-anchor="end">${r.work}</text>`;
g+=`<rect x="${x0}" y="${cy-barH/2}" width="${Math.max(w,2)}" height="${barH}" rx="4" fill="${col}" opacity="0.92"/>`;
// value, then the raw measurement under it
const vx = ahead ? x(d)+18 : zero+18;
g+=`<text x="${vx}" y="${cy+1}" fill="${col}" font-size="27" font-weight="700"
font-variant-numeric="tabular-nums">${r.v.toFixed(3)}&times;</text>`;
g+=`<text x="${vx}" y="${cy+23}" fill="#5d757f" font-size="17">${r.note}</text>`;
});
document.getElementById('c').innerHTML=g;
</script>
</body>
</html>

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 689 KiB