Compare commits

...
387 Commits
Author SHA1 Message Date
localai-org-maint-botandmudler dc353aecb6 chore: ⬆️ Update ikawrakow/ik_llama.cpp to 1a2a8604a6c6c6413c06bf9adfc2f64329af4366 (#11932)
⬆️ 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-09-09 09:25:33 +02:00
localai-org-maint-botandmudler 14b1796cdd chore: ⬆️ Update 0xShug0/audio.cpp to 05e508a70e3600b01454c647cdb122133ba8e64c (#11933)
⬆️ 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-09-09 08:52:33 +02:00
localai-org-maint-botandmudler 89dcdea0a0 chore: ⬆️ Update NVIDIA/NeMo-Speech.cpp to a5b6953c4a579a2bbd1c0913ad8a85c2a4d99953 (#11935)
⬆️ Update NVIDIA/NeMo-Speech.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-09-09 08:52:12 +02:00
localai-org-maint-botandmudler afb9bfd183 chore: ⬆️ Update antirez/ds4 to 6289c516273979173abbc062209a81dd3706b804 (#11936)
⬆️ 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-09-09 08:51:36 +02:00
localai-org-maint-botandmudler 36cbe294b2 chore: ⬆️ Update ggml-org/whisper.cpp to c44b60b8053bbf2a5c1e014f11323fb3f2485177 (#11937)
⬆️ 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-09-09 08:51:23 +02:00
localai-org-maint-botandlocalai-org-maint-bot be5342ef05 fix(worker): resolve temporary paths in tests (#11944)
Capacity guards reject symlink components. On macOS, temporary paths
start with /var, which links to /private/var, so the new staging tests
fail before exercising cleanup or capacity accounting.

Resolve the fixture directories before building guarded paths. Keep
explicit symlinks within the fixtures for containment tests.

Assisted-by: Codex:gpt-6

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-09-09 08:51:09 +02:00
localai-org-maint-botandEttore Di Giacinto bf93008ef3 fix(backends): bound temporary scratch files (#11941)
Backend processes shared the host temporary directory, so crashes could leave request images and audio behind until the filesystem filled. Give each process a locked LocalAI-owned runtime, remove scratch on exit, and sweep only marked abandoned runtimes at the next start.

Also close known request error-path leaks in the Python media backends, CrispASR, LongCat Video, and stable-diffusion.cpp.

Assisted-by: Codex:gpt-5

Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-08 23:44:22 +02:00
localai-org-maint-botandEttore Di Giacinto 8c718441f6 fix(realtime): resolve pipeline voice profiles (#11942)
* fix(realtime): resolve saved voice profiles

Realtime pipelines now validate saved voices against the selected TTS model and retain leased audio until session teardown. Each synthesis request receives its own transcript parameter map.

Assisted-by: Codex:GPT-5

* docs(tts): document realtime voice defaults

Show how a realtime pipeline selects a saved Voice Library profile at session start. Clarify which session voice updates remain supported.

Assisted-by: Codex:GPT-5

---------

Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-08 23:42:31 +02:00
localai-org-maint-botandmudler 80872e5e8d chore(model-gallery): ⬆️ update checksum (#11939)
⬆️ 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-09-08 23:13:20 +02:00
localai-org-maint-botandEttore Di Giacinto 780e458f11 fix(realtime): skip responses for empty transcripts (#11940)
Realtime turns could invoke the LLM and TTS even when speech transcription returned only whitespace. This let ambient noise produce unsolicited assistant output and polluted conversation history with an empty user turn.

Require non-blank transcript text before automatic response generation while preserving the completed transcription event.

Assisted-by: Codex:gpt-5 golangci-lint

Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-08 23:12:57 +02:00
localai-org-maint-botandEttore Di Giacinto 2ba6600440 fix(detection): avoid temporary image files (#11938)
RF-DETR and Locate Anything wrote each decoded request image to the OS
temporary directory. A full temporary filesystem then disabled detection,
even though both native libraries already accept encoded image buffers.

Pass decoded images directly to the native buffer APIs. This removes the
request-time disk dependency and prevents crash-orphaned image files.

Assisted-by: Codex:gpt-5

Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-08 22:59:57 +02:00
localai-org-maint-botandEttore Di Giacinto 8b5f62cc02 fix(distributed): bound ephemeral staging (#11924)
* docs: design ephemeral staging retention

High-frequency camera and audio inputs can fill a worker before the current six-hour cleanup window expires.

Define a one-hour retention policy that preserves recently modified request payloads.

Assisted-by: Codex:gpt-6

* docs: make ephemeral staging request-owned

Time-based retention can still fill a worker under bursty or high-rate input. Define request-lifecycle cleanup with capacity reservation and crash recovery.

Assisted-by: Codex:gpt-6

* feat(distributed): release exact staged keys

Request inputs need transport-neutral cleanup after backend calls. Add authenticated exact-key deletion for HTTP and coordinated cache eviction before shared-object deletion for S3/NATS.

Preserve URL metacharacters as filename data, reject unsafe keys, and remove upload sidecars while pruning empty request directories.

Assisted-by: Codex:gpt-6

* fix(distributed): release staged request inputs

Ephemeral inputs remained on workers after inference completed. Release each exact key after synchronous and streaming calls, including partial staging failures.

Use a bounded cleanup context so caller cancellation cannot suppress release. Preserve caller requests and backend results when cleanup fails.

Assisted-by: Codex:gpt-6

* feat(worker): bound ephemeral staging capacity

Concurrent staging can otherwise exceed its byte limit or consume reserved filesystem headroom. Explicit states keep bytes charged through each reservation, write, and commit transition.

Use a synchronized waiter count to prove Commit blocks until bounded writers close, and retain committed baselines across re-reservation.

Assisted-by: Codex:gpt-6

* feat(worker): enforce ephemeral staging bounds

Share capacity accounting across HTTP and S3 request inputs so workers
reject uploads before exhausting their filesystem. Reconcile exact release
and crash recovery with the same guard.

Assisted-by: Codex:gpt-6

* fix(distributed): make staged release race-safe

Pin each release path component before removing request-owned inputs and sidecars. Stop pruning when a directory identity changes.

Assisted-by: Codex:gpt-6

* fix(worker): retain staged input ownership

Keep committed request inputs protected from age recovery until exact release ends their ownership. Startup-scanned files remain reclaimable and can acquire ownership through reservation.

Assisted-by: Codex:gpt-6

* fix(worker): claim cached ephemeral inputs

Keep startup-scanned cache hits owned while inference uses them and reconcile their actual size against capacity.

Assisted-by: Codex:gpt-6

* fix(distributed): enforce staging admission

Propagate multimodal staging failures before inference and claim matching ephemeral HTTP cache entries. Fall back to PUT when an older worker does not support claims.

Assisted-by: Codex:gpt-6

* fix(distributed): close staging accounting gaps

Keep unknown-length reservations charged until bytes reach disk and bound NATS release waits by the lifecycle cleanup deadline.

Assisted-by: Codex:gpt-6

* fix(distributed): restage swept cache hits

Treat files removed between cache probing and ownership claims as misses so HTTP and S3 workers can stage them again.

Assisted-by: Codex:gpt-6

* fix(distributed): release staged inputs by request

Release every input from one inference with one fixed-size worker coordination request. Fence request ingress against cleanup, bound staging capacity and cleanup state, and retain exact-key release for rolling upgrades.

Assisted-by: Codex:gpt-6

---------

Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-08 18:13:40 +02:00
Plamen K. Kosseffandlocalai-org-maint-bot e8546965c7 fix(gallery): default audio-cpp models to backend:best (#11892)
* fix(gallery): default audio-cpp models to backend:best

The audio-cpp engine creates its session on the CPU backend when no
backend option is given, so every gallery model ran CPU-only even on
machines where a CUDA/Vulkan/Metal device was registered. backend:best
selects the best available backend and falls back to CPU.

Assisted-by: Claude:claude-fable-5
Signed-off-by: Plamen K. Kosseff <p.kosseff@gmail.com>

* docs(audio-cpp): explain gallery device selection

Document automatic compute backend selection and the CPU override.

Assisted-by: Codex:gpt-6
Signed-off-by: Plamen K. Kosseff <p.kosseff@gmail.com>

---------

Signed-off-by: Plamen K. Kosseff <p.kosseff@gmail.com>
Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-09-08 08:45:17 +02:00
Plamen K. Kosseff ba88fb13ce fix(diffusers): auto-detect CUDA instead of defaulting to CPU (#11891)
The device fell back to CPU unless the model config set cuda: true,
while MPS right below was auto-detected — GPU hosts silently rendered
on CPU for any gallery entry missing the flag. Use CUDA whenever torch
reports it available (ROCm builds included), keep cuda: true as an
explicit force, and allow pinning with the device: model option (e.g.
options: ["device:cpu"]). Gallery entries stay untouched.

Assisted-by: Claude:claude-fable-5

Signed-off-by: Plamen K. Kosseff <p.kosseff@gmail.com>
2026-09-08 08:44:35 +02:00
localai-org-maint-botandmudler e0179a1d3b chore(website): refresh the counters (#11902)
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-09-08 08:43:10 +02:00
localai-org-maint-botandmudler caa92a7af4 feat(swagger): update swagger (#11914)
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-09-08 08:17:13 +02:00
localai-org-maint-botandmudler c718937900 chore: ⬆️ Update 0xShug0/audio.cpp to 9c6a282337cc83f227cc10428867a478947706ad (#11915)
⬆️ 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-09-08 08:16:51 +02:00
localai-org-maint-botandmudler 20825e92c5 chore: ⬆️ Update CrispStrobe/CrispASR to 301acd87b036764973b8bfba71e0a21818036d33 (#11916)
⬆️ 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-09-08 08:16:37 +02:00
localai-org-maint-botandmudler 3f9c0612ed chore: ⬆️ Update antirez/ds4 to f62ca29a308724cde5bc99134ede19104b2a3260 (#11917)
⬆️ 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-09-08 08:16:18 +02:00
localai-org-maint-botandmudler 630948683a chore: ⬆️ Update mudler/depth-anything.cpp to 14f7461d1f704761a038ac9f50dbde8fdb7275e2 (#11918)
⬆️ Update mudler/depth-anything.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-09-08 08:16:03 +02:00
localai-org-maint-botandmudler d3eaef2192 chore: ⬆️ Update leejet/stable-diffusion.cpp to d04e8950c1ec8d30248cbe996682b3182fb1adf6 (#11919)
⬆️ 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-09-08 08:15:48 +02:00
localai-org-maint-botandmudler 7e04ab9d87 chore: ⬆️ Update ggml-org/llama.cpp to 67672dc5b76f8bc17785a19d3dc6d1463fc2902c (#11920)
⬆️ Update ggml-org/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-09-08 08:15:31 +02:00
localai-org-maint-botandEttore Di Giacinto 561d623976 feat(gallery): add EXL3 vllm.cpp models (#11912)
* docs: design EXL3 gallery entries

Define the pinned Qwen3.8 and DeepSeek V4 Flash entries before changing the gallery.

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

* feat(gallery): add EXL3 vllm.cpp models

Add pinned Qwen3.8 and DeepSeek V4 Flash configurations. Bind the staged DFlash companion path so vllm.cpp opens LocalAI's managed snapshot.

Assisted-by: Codex:gpt-5

---------

Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-07 20:55:42 +02:00
localai-org-maint-botandEttore Di Giacinto aff9db9758 fix(distributed): resolve paths for virtual models (#11911)
Virtual model names have no primary file to anchor the worker path.
Companion assets still stage successfully, but relative options retain
an incorrect model directory and fail to load.

Derive the worker root from successfully staged option assets when the
primary path is absent. Cover Buffalo packs, files, directories,
overrides, and failed transfers. Document the frontend upgrade.

Assisted-by: Codex:gpt-6 golangci-lint

Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-07 19:32:17 +02:00
localai-org-maint-botandEttore Di Giacinto e494033607 fix(distributed): finalize stalled model uploads (#11910)
A worker can retain all model bytes with an unfinished-upload marker.
Retries then start at zero and repeatedly fail with HTTP 416.

Verify the existing bytes and finalize same-file retries at full size.
Reuse the normal integrity checks so corrupt content cannot be accepted.
Add regression coverage and document worker recovery.

Assisted-by: Codex:gpt-6 golangci-lint

Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-07 18:52:29 +02:00
localai-org-maint-botandEttore Di Giacinto 88d19567b8 feat(faces): replay saved face enrollments (#11908)
Accept original embeddings and timestamps so clients can restore faces
when the in-memory store restarts. Derive stable IDs from exact vectors
to make registration retries preserve identity without duplicate entries.

Assisted-by: Codex:GPT-6 golangci-lint

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-07 17:40:27 +02:00
localai-org-maint-botandEttore Di Giacinto fb8b7a359a fix(distributed): stage sound detection audio (#11907)
* fix(distributed): stage sound detection audio

Sound detection passes frontend temporary paths directly to remote
workers, unlike transcription. Stage the WAV before classification so
CED can read it without a shared temporary directory.

Preserve the original request for retries and propagate staging errors
without calling the backend. Cover staging, request preservation, and
error handling with regression tests.

Assisted-by: Codex:GPT-6 golangci-lint

* test(distributed): verify routed sound staging

Call sound detection through the client returned by SmartRouter.Route.
This checks interface dispatch through both routing wrappers, rather
than constructing FileStagingClient directly.

The test fails without the sound-staging override and passes with it.

Assisted-by: Codex:GPT-6 golangci-lint

---------

Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-07 17:12:37 +02:00
Plamen K. Kosseff 77b8241c51 docs(integrations): add Distribution Packages section (#11904)
Community-maintained packagings that currently track releases —
Homebrew, ALT Sisyphus and the Gentoo local-ai overlay — with a note
that versions may lag. Placement and scope as discussed in the issue.

Assisted-by: Claude:claude-fable-5

Signed-off-by: Plamen K. Kosseff <p.kosseff@gmail.com>
2026-09-07 16:36:58 +02:00
57f802aa7a chore: ⬆️ Update leejet/stable-diffusion.cpp to d8fb10c02977c8ca999f3fb4e02df9ecf10f7ba6 (#11898)
* ⬆️ Update leejet/stable-diffusion.cpp

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* fix(stablediffusion): adapt streaming options

Upstream now selects segmented weight streaming automatically and removes the stream_layers field. Keep the old LocalAI option as a no-op for existing model configurations.

Assisted-by: Codex:gpt-5

---------

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-07 12:56:55 +02:00
f94ce520ce chore: ⬆️ Update antirez/ds4 to 9ab705347c1775e7599ede7eb81a6255ec7dccb5 (#11808)
* ⬆️ Update antirez/ds4

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* fix(ds4): link upstream image helpers

The ds4 bump adds vision calls to the engine object. Link the new image preprocessing object into every backend target.

Assisted-by: Codex:gpt-5

---------

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-07 12:56:38 +02:00
localai-org-maint-botandmudler 5728f78f2d chore: ⬆️ Update ggml-org/llama.cpp to 465e49b9cea78a68b9c244ffb48d0ee24a82873d (#11897)
⬆️ Update ggml-org/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-09-07 08:22:41 +02:00
localai-org-maint-botandmudler 763bf6fbab chore: ⬆️ Update 0xShug0/audio.cpp to f6277c1695a83cf388a8282c1c1a8757cf626f18 (#11899)
⬆️ 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-09-07 08:22:16 +02:00
Alex Mazzariolandlocalai-org-maint-bot 4d854f96a8 Update containers.md to fix podman image qualification (#11749)
* Update containers.md to fix podman image qualification

Signed-off-by: Alex Mazzariol <alex@alex-maz.info>

* docs(containers): clarify Podman image names

Podman can reject short image names when no registry is configured. Explain why the examples use fully qualified Docker Hub names.

Assisted-by: Codex:gpt-5.6

---------

Signed-off-by: Alex Mazzariol <alex@alex-maz.info>
Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-09-06 12:45:56 +02:00
localai-org-maint-botandmudler c3f9e1a3a2 chore: ⬆️ Update ggml-org/llama.cpp to 74a7c897f049c17e7080423aa2111776eff6ebbf (#11889)
⬆️ Update ggml-org/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-09-06 09:11:54 +02:00
Ettore Di Giacinto 9d58fb34cc feat(gallery): add S1-mini variants
Squashed merge of #11642.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-06 07:04:35 +00:00
Ettore Di Giacinto b5b4633595 feat(gallery): add nl2sh 1.5B
Squashed merge of #11538.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-06 07:04:35 +00:00
Ettore Di Giacinto 24b9362038 feat(gallery): add GLM-5.3 Flash variants
Squashed merge of #11785.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-06 07:04:35 +00:00
Ettore Di Giacinto 4aae0009a2 feat(gallery): add Apodex 1.1 mini variants
Squashed merge of #11731.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-06 07:04:35 +00:00
mudler 287d77911b chore(model gallery): 🤖 add new models via gallery agent
Squashed merge of #11871.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-06 07:04:35 +00:00
mudler 5dc16c6f52 chore(model gallery): 🤖 add new models via gallery agent
Squashed merge of #11848.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-06 07:04:35 +00:00
mudler 648fbafd93 chore(model gallery): 🤖 add new models via gallery agent
Squashed merge of #11847.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-06 07:04:35 +00:00
mudler ae8dc199d5 chore(model gallery): 🤖 add new models via gallery agent
Squashed merge of #11820.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-06 07:04:34 +00:00
mudler 8dda90e60a chore(model gallery): 🤖 add new models via gallery agent
Squashed merge of #11813.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-06 07:04:34 +00:00
mudler 4f8efd51be chore(model gallery): 🤖 add new models via gallery agent
Squashed merge of #11805.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-06 07:04:34 +00:00
localai-org-maint-botandmudler e170b2d615 chore: ⬆️ Update 0xShug0/audio.cpp to c41ffefb44c16345c0ac57a0030f4cd3b5fbc4b8 (#11888)
⬆️ 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-09-06 08:55:28 +02:00
localai-org-maint-botandmudler b1dbc0686a chore: ⬆️ Update ServeurpersoCom/omnivoice.cpp to 040c8b344d8c670ce1475194751d119b5ef82c78 (#11890)
⬆️ Update ServeurpersoCom/omnivoice.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-09-06 08:55:10 +02:00
localai-org-maint-botandmudler cdcb724523 chore: ⬆️ Update NVIDIA/NeMo-Speech.cpp to ffa38cb2408f1e832a36d46fef5e3e1e80d07e6c (#11887)
⬆️ Update NVIDIA/NeMo-Speech.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-09-06 08:54:52 +02:00
Ettore Di Giacinto 18d20239df fix(ui): keep trace expansion stable during refresh
Squashed merge of #11278.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-05 22:09:16 +00:00
pengmin bfd85ac1dc fix(diffusers): pass text prompts by keyword
Squashed merge of #11836.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-05 22:09:16 +00:00
pengmin 718357219b fix(ui): send collection intervals as numbers
Squashed merge of #11819.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-05 22:09:16 +00:00
Anupam Mediratta 1479aef8cd fix(deps): upgrade path-to-regexp to 8.4.0 (CVE-2026-4926)
Squashed merge of #11633.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-05 22:09:16 +00:00
pos-ei-don 4894056380 fix(sglang): force reasoning when the template prefills the think tag
Qwen3-style chat templates append the opening <think> tag to the *prompt*
when thinking is enabled. The model therefore never generates it and emits
only the reasoning text plus the closing </think>.

sglang's ReasoningParser keys off the opening tag:

    in_reasoning = self._in_reasoning or self.think_start_token in text
    if not in_reasoning:
        return StreamingParseResult(normal_text=text)

so with such a template the entire completion — reasoning and answer, the
raw </think> in between — is returned as content and reasoning_content
stays empty, no matter how reasoning_parser is configured.

sglang's own OpenAI server handles this via

    force_reasoning = (self.template_manager.force_reasoning
                       or self._get_reasoning_from_request(request))

This backend has no template manager, so derive the same signal from the
rendered prompt: if it ends with the detector's think_start_token, the tag
was prefilled and the parser is constructed with force_reasoning=True.

Structured decoding is the exception, and it matters: a grammar applies
from the first token, so the model cannot emit the closing tag even though
the template opened the block. The whole completion is schema output and
belongs in content — forcing there files it as reasoning and returns an
empty answer. Measured against a JSON-schema code audit: 10107 characters
of "reasoning", zero content. sglang's own server keeps the two apart for
the same reason; its grammar backend owns the reasoning prefix when a
reasoning parser is configured.

force_reasoning is only passed when it is meant to be True, so detector
defaults (DeepSeek-R1 already defaults to True) are untouched, and a
prompt without a prefilled tag behaves exactly as before — which matters,
because forcing unconditionally makes an answer generated with thinking
off disappear into reasoning_content.

The construction is factored into _new_reasoning_parser() so the streaming
and non-streaming paths, which previously built the parser separately,
cannot drift apart.

Signed-off-by: pos-ei-don <1822533+pos-ei-don@users.noreply.github.com>
2026-09-05 22:09:16 +00:00
Tai An 9319450aa6 fix(python-backends): re-attach media markers under use_tokenizer_template (#11621)
With `template.use_tokenizer_template: true` the sglang and vllm backends
render the prompt themselves via `tokenizer.apply_chat_template()`, and they
hand it plain string content. A chat template only emits the model's own media
tokens when the content is a list of parts, so the rendered prompt carries no
`<|vision_start|><|image_pad|><|vision_end|>`. The pixels do reach the engine
(`image_data` / `multi_modal_data`), but both engines locate them by scanning
the prompt for that token, so they are discarded silently: HTTP 200, no
warning, and the model answers as if no image had been attached.

Add `attach_media_parts()` to the shared `python_utils` helper and call it in
both backends: the last user turn is rebuilt as
`[{"type": "image"} * n, {"type": "video"} * n, {"type": "text", ...}]` before
templating, which makes the template emit the placeholders. The pixels keep
travelling out of band exactly as before.

Text-only requests are untouched - with no media the helper returns None and
the original string-content path runs unchanged. If a template cannot iterate
content parts (a text-only model), the parts render is caught and the request
falls back to the previous string-content prompt instead of failing.

Signed-off-by: Tai An <antai12232931@outlook.com>
2026-09-05 22:09:16 +00:00
pos-ei-don c4fe77a369 fix(vllm): tell the reasoning parser whether thinking was enabled
vLLM's engine-based reasoning parsers derive their initial state from the
chat template kwargs. Qwen3Parser:

    chat_kwargs = kwargs.get("chat_template_kwargs", {}) or {}
    self.thinking_enabled = chat_kwargs.get("enable_thinking", True)

Constructed as ReasoningParser(tokenizer) the flag defaults to True, so the
parser starts in the REASONING state. A completion produced with thinking
disabled contains no tags at all, and every reasoning parser shape then
reports the whole answer as reasoning:

  - engine-based parsers classify it by initial state;
  - BaseThinkingReasoningParser hits its documented "may not generate start
    token" fallback and returns (model_output, None).

Either way `content = c if c is not None else generated_text` turns that
into a duplicate: a Qwen3 model answering "391" with thinking off comes back
as reasoning_content="391" AND content="391".

Measured against Qwen3.5-MoE on vLLM 0.28, non-streaming:

    before   thinking on   reasoning=202  content="391"
             thinking off  reasoning="391" content="391"   <- duplicated
    after    thinking on   reasoning=192  content="391"
             thinking off  reasoning=""    content="391"

Forward the kwargs the prompt was rendered with, which is what vLLM's own
OpenAI server does; parsers that do not accept the argument keep the plain
constructor.

_split_reasoning() covers the older parser shape, which has no initial state
to set. It only reclassifies when the parser exposes a start/end token pair
and neither the completion nor the prompt ever opened a reasoning block.
Truncated reasoning (block open, end token never arrived) stays reasoning,
and parsers without that token pair are left untouched.

Signed-off-by: pos-ei-don <1822533+pos-ei-don@users.noreply.github.com>
2026-09-05 22:09:16 +00:00
pos-ei-don 84db1e6430 fix(backends): preserve an explicit seed of 0 in sglang and vllm
#11772 exempted Temperature from the zero-filter in both backend adapters,
because proto3 has no field presence and an explicit 0 is indistinguishable
from "unset". Seed has exactly the same property and is still filtered:

    if proto_field != "Temperature" and value in (None, 0, 0.0, [], False, ""):
        continue

A caller pinning `"seed": 0` for a reproducible run therefore gets a random
seed instead, with no error and no log line — the one case where the failure
is invisible precisely because the request looked deliberate.

Both adapters now share a named tuple of fields whose zero is meaningful, so
the next one is added in one place rather than as a second special case.

Deliberately left filtered: top_k, top_p, min_p and the penalties. Their zero
is not a value a caller means — sglang disables top_k with -1, not 0, so
forwarding 0 there would turn a default into an invalid argument.

Verified on the sglang backend (Qwen3.5-MoE, arm64): with the temperature fix
alone, two identical requests at temperature 0 are byte-identical, but pinning
seed 0 has no effect until this change.

Signed-off-by: pos-ei-don <1822533+pos-ei-don@users.noreply.github.com>
2026-09-05 22:09:16 +00:00
Tai An 37e18a4bf3 fix(quantization): record job progress without an attached listener (#11874)
A quantization job that runs with no client on its progress stream stays
"queued" forever, in the API and in state.json, while the finished artifact
sits on disk. state.json was written once by StartJob, and the only code that
advanced a job afterwards lived inside the stream callback of StreamProgress,
so job state depended on somebody watching it.

The backend's progress stream cannot simply gain a second reader: each job owns
one queue.Queue and QuantizationProgress pops from it, so two consumers split
the updates rather than both seeing them. The stream has to be opened exactly
once per job.

StartJob now starts watchProgress on the application context (the request
context is done as soon as the handler returns). That goroutine is the single
reader: it applies each update to the job -- in the cross-replica store and in
state.json, terminal statuses still winning over late updates -- and republishes
it in-process. StreamProgress becomes a pure reader over that fan-out and no
longer loads a backend or opens a stream. A client attaching to a job that has
already finished, including one hydrated from disk after a restart, gets a final
event built from the stored job instead of blocking.

Two paths used to end a client's stream by breaking the gRPC connection and now
release it explicitly: StopJob kills the backend, so it publishes the stopped
event itself; and a stream that ends without a terminal update means the backend
is gone, so the job is recorded as failed rather than left running forever.

Signed-off-by: Tai An <antai12232931@outlook.com>
2026-09-05 22:09:16 +00:00
Ettore Di Giacinto ab3f7fc904 fix(qwen-asr): select Intel XPU devices
The Intel backend installs PyTorch XPU wheels, but Qwen ASR only
checked CUDA and MPS. Every Intel model therefore loaded on the CPU.

Select XPU when available and place the model on xpu:0. Keep the
existing CUDA, MPS, and CPU placement behavior.

Assisted-by: Codex:GPT-5 [apply_patch] [gh]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-05 22:09:16 +00:00
Ettore Di Giacinto 8744de44d4 fix(whisperx): reject unconfigured diarization
WhisperX silently returned a plain transcript when diarization lacked
the Hugging Face token required to load pyannote. Reject that request
clearly so callers do not mistake missing speaker labels for a
successful diarization.

Convert WhisperX seconds to the nanosecond duration unit used by the
transcription API.

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-05 22:09:16 +00:00
lei_lei 9ca516d6ef fix(nix): use legacy-peer-deps for react-ui
importNpmLock turns the same-version hono override into a file: tarball
that conflicts with the direct dependency (EOVERRIDE). Pass
--legacy-peer-deps so the flake build can proceed without
touching package.json (open #11633).

Fixes #11804

Signed-off-by: lei_lei <96427312+leilei3167@users.noreply.github.com>
2026-09-05 22:09:16 +00:00
Ettore Di Giacinto b2fdbdd52a feat(gallery): add LLM-jp 4 thinking
LLM-jp 4 provides a recent Japanese and English reasoning model on the supported Llama architecture. Add its official Q4 and BF16 GGUF builds so hosts can select the fidelity that fits.

Assisted-by: Codex:gpt-5

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-05 22:09:16 +00:00
Ettore Di Giacinto f4bbc4a725 feat(gallery): add Dirk Qwen3.8 variants
Add Q4_K_XL and Q8_K_XL llama.cpp builds with the shared vision projector. Enable the preserved MTP head for speculative decoding.

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-05 22:09:16 +00:00
Ettore Di Giacinto 5f783f97a3 feat(gallery): add Security-SLM Gemma 4
Add the text-only Q4_K_M build for private red-team, blue-team, and security operations workloads. Configure the supported Gemma 4 model for llama.cpp with its verified Hugging Face checksum.

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-05 22:09:16 +00:00
Ettore Di Giacinto 4c1002cc78 feat(gallery): add mxbai embed large variants
Add Q4_K_M and Q8_0 llama.cpp builds plus the official F16 GGUF for Mixedbread mxbai-embed-large-v1.

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-05 22:09:16 +00:00
Ettore Di Giacinto ca3b5dad8b feat(gallery): add Laguna S APEX variants
The new importance-matrix builds reduce Laguna S 2.1 from the existing 96 GB default to 73.9 GB or 54.4 GB. Add both as selectable llama.cpp variants.

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-05 22:09:16 +00:00
Ettore Di Giacinto c96904ddf4 feat(gallery): add DFM Mimir for vLLM
DFM Mimir is a new permissively licensed Danish and English HRM-Text model. LocalAI already ships a vLLM version with native support for its architecture.

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-05 22:09:16 +00:00
Ettore Di Giacinto 6324ee1dd9 feat(gallery): add VoxCPM2
Add the artifact-backed VoxCPM2 model for multilingual speech synthesis, voice design, and controllable voice cloning.

Assisted-by: Codex:gpt-5.6 [Codex]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-05 22:09:16 +00:00
Ettore Di Giacinto e2ef7d050d feat(gallery): add Qwen3.8 9B Distill variants
Add the Q4_K_M and Q8_0 GGUF builds for a compact Qwen3.5-based reasoning and tool-use distillation. The variant pair lets LocalAI choose higher fidelity when the host has enough memory while keeping the Q4 build as the installable fallback.

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-05 22:09:16 +00:00
Ettore Di Giacinto cfafd6cd36 feat(gallery): add Qwen3.8 NVFP4 MTP variant
Add a compact NVFP4 build with its embedded MTP head and vision
projector. This gives Blackwell-capable llama.cpp hosts a speculative
option alongside the existing Q4 and Q8 builds.

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-05 22:09:16 +00:00
Ettore Di Giacinto bb4432d2f2 feat(gallery): add IndexTTS 2.5
Add the original-dtype GGUF for LocalAI\x27s audio.cpp backend. The entry exposes multilingual voice cloning and records the model license.\n\nAssisted-by: Codex:gpt-5

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-05 22:09:16 +00:00
Ettore Di Giacinto 6a5895cd7f feat(gallery): add LightOnOCR 2 1B variants
Add the compact LightOnOCR model to the OCR section with Q8_0 and F16 builds. Each build uses its matching vision projector.

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-05 22:09:16 +00:00
Ettore Di Giacinto 7ec55db327 feat(gallery): add Supra2 100M Instruct
Add the official F16 GGUF build of the compact Apache-2.0 Qwen3-family chat model with its native 2K context limit.

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-05 22:09:16 +00:00
陈志谦 dd1776a91f docs: correct documented env var and CLI flag names (#11886)
- api-errors.md documented LOCALAI_SUBTLEKEY_COMPARISON (missing the
  KEY underscore); the code defines LOCALAI_SUBTLE_KEY_COMPARISON, so
  the documented variable silently did nothing
- cli-reference.md documented a --csrf flag / $LOCALAI_CSRF env that
  do not exist, with inverted semantics; the actual flag is
  --disable-csrf (LOCALAI_DISABLE_CSRF), 'Disable CSRF middleware
  (enabled by default)'
2026-09-05 23:49:10 +02:00
陈志谦 d9d68c595e docs: fix dead links in the PR template and backends docs (#11884)
- the PR template's 'Signed commits' anchor pointed at a CONTRIBUTING
  section that does not exist; repointed at the Commit messages
  section
- the longcat-video backend README linked a docs page that was never
  committed; replaced the dead link with plain text
- formal-verification/README.md used ../../../ for five in-repo
  packages (escaping the repo root); fixed to ../
2026-09-05 23:48:01 +02:00
陈志谦 24f897cd09 docs: fix dead anchors and a dead section link (#11885)
- middleware.md: the 'default detector' link used #instance-wide-defaults;
  the heading is 'Instance-wide default detector'
- the advanced/reference landing pages linked an ../installation/
  directory that does not exist in docs/content; dropped the dead
  bullets (deployment content lives under getting-started)
2026-09-05 23:47:13 +02:00
Abdullah Mansour | عبد الله منصور a98501d6ce docs(llama-cpp): clarify multimodal speculative decoding (#11700)
* docs(llama-cpp): clarify multimodal speculative decoding

Update the speculative decoding guidance now that modern llama.cpp backends can combine mmproj-based vision with speculative decoding, including MTP. Document compatibility checks, draft acceptance statistics, VRAM tradeoffs, and a combined configuration example.

Assisted-by: Codex:GPT-5.6-Sol [gh] [OpenStack] [Docker]
Signed-off-by: Abdullah Mansour <abdullahmansour.marketing@gmail.com>

* docs(llama-cpp): clarify multimodal MTP references

Distinguish the upstream change that removed the general multimodal speculative restriction from the later change that added MTP with explicit vision compatibility.

Assisted-by: Codex:GPT-5.6-Sol [gh] [Docker]
Signed-off-by: Abdullah Mansour <abdullahmansour.marketing@gmail.com>

---------

Signed-off-by: Abdullah Mansour <abdullahmansour.marketing@gmail.com>
2026-09-05 23:46:24 +02:00
dependabot[bot] b217148bbc chore(deps): bump torch from 2.13.0+xpu to 2.14.0+xpu in /backend/python/common/template (#11859)
chore(deps): bump torch in /backend/python/common/template

Bumps torch from 2.13.0+xpu to 2.14.0+xpu.

---
updated-dependencies:
- dependency-name: torch
  dependency-version: 2.14.0+xpu
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-05 11:27:01 +02:00
localai-org-maint-botandmudler 32875a7089 chore: ⬆️ Update CrispStrobe/CrispASR to c6d4265ae2ee2b8931b09d7d25d5c65c75c36a41 (#11862)
⬆️ 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-09-05 11:26:30 +02:00
localai-org-maint-botandmudler 7ad4384cbe chore: ⬆️ Update ggml-org/llama.cpp to 427291b5b34cd914a31b3fd3b61a68f6184f4b9f (#11866)
⬆️ Update ggml-org/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-09-05 11:26:11 +02:00
dependabot[bot] ee2920d619 chore(deps): bump the npm_and_yarn group across 1 directory with 3 updates (#11869)
Bumps the npm_and_yarn group with 3 updates in the /core/http/react-ui directory: [@humanfs/node](https://github.com/humanwhocodes/humanfs/tree/HEAD/packages/node), [fast-uri](https://github.com/fastify/fast-uri) and [qs](https://github.com/ljharb/qs).


Updates `@humanfs/node` from 0.16.7 to 0.16.8
- [Release notes](https://github.com/humanwhocodes/humanfs/releases)
- [Changelog](https://github.com/humanwhocodes/humanfs/blob/main/packages/node/CHANGELOG.md)
- [Commits](https://github.com/humanwhocodes/humanfs/commits/node-v0.16.8/packages/node)

Updates `fast-uri` from 3.1.5 to 3.1.7
- [Release notes](https://github.com/fastify/fast-uri/releases)
- [Commits](https://github.com/fastify/fast-uri/compare/v3.1.5...v3.1.7)

Updates `qs` from 6.15.3 to 6.16.0
- [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ljharb/qs/compare/v6.15.3...v6.16.0)

---
updated-dependencies:
- dependency-name: "@humanfs/node"
  dependency-version: 0.16.8
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: fast-uri
  dependency-version: 3.1.7
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: qs
  dependency-version: 6.16.0
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-05 11:25:54 +02:00
localai-org-maint-botandmudler 7ab7a3136b chore(model gallery): 🤖 add 1 new models via gallery agent (#11880)
chore(model gallery): 🤖 add new models via gallery agent

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-09-05 11:25:24 +02:00
localai-org-maint-botandmudler f09d6092ed chore: ⬆️ Update 0xShug0/audio.cpp to cd98afb3264044aeff33618df486c81de1bee7e7 (#11878)
⬆️ 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-09-05 11:24:25 +02:00
dependabot[bot] 3e98ec935a chore(deps): bump grpcio from 1.82.1 to 1.83.1 in /backend/python/common/template (#11858)
chore(deps): bump grpcio in /backend/python/common/template

Bumps [grpcio](https://github.com/grpc/grpc) from 1.82.1 to 1.83.1.
- [Release notes](https://github.com/grpc/grpc/releases)
- [Commits](https://github.com/grpc/grpc/compare/v1.82.1...v1.83.1)

---
updated-dependencies:
- dependency-name: grpcio
  dependency-version: 1.83.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-05 01:03:02 +02:00
localai-org-maint-botandmudler 583992a76f chore: ⬆️ Update ggml-org/whisper.cpp to 52a939a2a762224e255d366c1182b2af4dd1a032 (#11877)
⬆️ 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-09-05 01:02:43 +02:00
localai-org-maint-botandmudler 87d15e7108 chore: ⬆️ Update NVIDIA/NeMo-Speech.cpp to 69d7fd46b253043ed6649bf35d68407448912a8e (#11863)
⬆️ Update NVIDIA/NeMo-Speech.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-09-04 16:57:26 +02:00
localai-org-maint-botandmudler a742c87f76 chore: ⬆️ Update ServeurpersoCom/omnivoice.cpp to 78ce63c90d36b94a765073e8cbe0d241fc114590 (#11865)
⬆️ Update ServeurpersoCom/omnivoice.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-09-04 16:53:28 +02:00
localai-org-maint-botandmudler 1f2f931f53 chore: ⬆️ Update 0xShug0/audio.cpp to 8fe1ab805555c72aaf06d83828b523dfc6ed796d (#11861)
⬆️ 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-09-04 09:16:30 +02:00
localai-org-maint-botandmudler 5e6a83694d chore(model-gallery): ⬆️ update checksum (#11868)
⬆️ 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-09-04 09:00:41 +02:00
dependabot[bot] 5485a2e62b chore(deps): bump grpcio from 1.83.0 to 1.83.1 in /backend/python/vllm (#11853)
Bumps [grpcio](https://github.com/grpc/grpc) from 1.83.0 to 1.83.1.
- [Release notes](https://github.com/grpc/grpc/releases)
- [Commits](https://github.com/grpc/grpc/compare/v1.83.0...v1.83.1)

---
updated-dependencies:
- dependency-name: grpcio
  dependency-version: 1.83.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-04 09:00:22 +02:00
dependabot[bot] 072e367df8 chore(deps): bump grpcio from 1.82.1 to 1.83.1 in /backend/python/rerankers (#11852)
chore(deps): bump grpcio in /backend/python/rerankers

Bumps [grpcio](https://github.com/grpc/grpc) from 1.82.1 to 1.83.1.
- [Release notes](https://github.com/grpc/grpc/releases)
- [Commits](https://github.com/grpc/grpc/compare/v1.82.1...v1.83.1)

---
updated-dependencies:
- dependency-name: grpcio
  dependency-version: 1.83.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-04 09:00:00 +02:00
dependabot[bot] 8b31f5d6ad chore(deps): bump grpcio from 1.83.0 to 1.83.1 in /backend/python/coqui (#11856)
Bumps [grpcio](https://github.com/grpc/grpc) from 1.83.0 to 1.83.1.
- [Release notes](https://github.com/grpc/grpc/releases)
- [Commits](https://github.com/grpc/grpc/compare/v1.83.0...v1.83.1)

---
updated-dependencies:
- dependency-name: grpcio
  dependency-version: 1.83.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-04 08:51:51 +02:00
localai-org-maint-botandmudler b35fdf0142 chore: ⬆️ Update ikawrakow/ik_llama.cpp to fe215a8ccdce6b844d2a3a3bbde08ae76a6284bf (#11864)
⬆️ 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-09-04 08:49:55 +02:00
dependabot[bot] 99a2e855a8 chore(deps): bump the npm_and_yarn group across 1 directory with 2 updates (#11835)
Bumps the npm_and_yarn group with 2 updates in the /core/http/react-ui directory: [react-router](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router) and [browserslist](https://github.com/browserslist/browserslist).


Updates `react-router` from 7.18.1 to 7.18.2
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/react-router@7.18.2/packages/react-router/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router@7.18.2/packages/react-router)

Updates `browserslist` from 4.28.2 to 4.28.8
- [Release notes](https://github.com/browserslist/browserslist/releases)
- [Changelog](https://github.com/browserslist/browserslist/blob/main/CHANGELOG.md)
- [Commits](https://github.com/browserslist/browserslist/compare/4.28.2...4.28.8)

---
updated-dependencies:
- dependency-name: react-router
  dependency-version: 7.18.2
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: browserslist
  dependency-version: 4.28.8
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-03 23:38:23 +02:00
localai-org-maint-botandEttore Di Giacinto 44de82e7c7 docs(dco): let maintainer-operated automation sign off (#11850)
The AI-assistant policy says an AI agent must never add a Signed-off-by
trailer, because only a human can certify the DCO. That is right for the
case it was written for: an assistant helping a contributor who then
signs off themselves.

It does not fit automation a maintainer runs. Those pull requests have no
human submitter, so nothing ever signs and the DCO check blocks them
permanently. Sixty-one open pull requests from the maintenance bot are in
exactly that state, every one of them correctly following the documented
rule.

Carve out the case: automation a maintainer operates signs off with that
maintainer's identity. The maintainer certifies the DCO, as they do for a
commit they typed by hand, because they configured the automation, own
its output, and take responsibility on merge. The Assisted-by trailer
still records that a model wrote the code, so provenance is unchanged.

Keep the exception narrow. An assistant helping an outside contributor
still must not sign off, and a bot must not sign for anyone but its
operator, including on a contributor's branch it pushes to.

Assisted-by: Claude:claude-opus-5

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-03 23:08:38 +02:00
localai-org-maint-botandmudler 211487c7f3 chore: ⬆️ Update ggml-org/llama.cpp to 9cffdcc801582616250520966699cb5b25d28243 (#11845)
⬆️ Update ggml-org/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-09-03 23:06:10 +02:00
localai-org-maint-botandEttore Di Giacinto 7a234473e8 fix(ci): unbreak the e2e build and the darwin vllm-metal pin (#11849)
Two independent breakages on master make every open pull request red,
for reasons unrelated to the changes under review.

The e2e backend suite stopped compiling. Reply.message is `bytes` in
backend.proto, so res.GetMessage() returns []byte, and strings.ToUpper
wants a string. Every other call site in the file already converts.
tests/e2e-backends sits behind a build tag, so `go build ./...` never
compiled it and the breakage reached master unnoticed.

The darwin vllm build stopped resolving. Upstream vllm-metal deleted
its old dev tags and re-versioned to track the vLLM release it targets,
so the pinned wheel 404s. The coupled vLLM release also moved out of
upstream's install.sh into .github/vllm-release-tag.commit, and the
wheel's platform tag moved from macosx_11_0 to macosx_15_0.

Read the wheel name from the release's own asset listing rather than
composing it from a hardcoded platform segment, so a platform-tag
change cannot silently 404 again, and resolve the vLLM version from
the new metadata file with a fallback to the legacy installer. The
bump script and the extractor learn the same two-source lookup, so the
next nightly run converges on the pin checked in here instead of
reintroducing the break.

Assisted-by: Claude:claude-opus-5

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-03 18:32:02 +02:00
Tai An 9901103aac fix(downloader): make file:// installs reachable again (#11701) (#11734)
fix(downloader): make file:// installs reachable again

DownloadFileWithContext already has a branch that copies from a local
file, but it could never run. Before reaching it the function decides
whether the destination is fetchable with

    } else if !os.IsNotExist(err) || !URI(url).LooksLikeHTTPURL() {

and LooksLikeHTTPURL is http(s) only, so any URI resolving to a local
path is rejected there. Falling through requires the destination to be
missing AND the source to be an HTTP URL, which a file:// source never
is -- leaving the local-source branch below unreachable.

A first import always has a missing destination, so importing
file:///path/to/model.gguf always failed, with an error that listed
file:// among the supported schemes (#11701).

Name the local-source condition once as URI.hasLocalSource and use it
both to admit the destination and to pick the source, so the two cannot
drift apart again.

Signed-off-by: Tai An <antai12232931@outlook.com>
2026-09-03 18:30:31 +02:00
Dimitris Karakasilis 8aeea4cdde fix(gallery): persist inference defaults where the loader reads them (#11232)
The recommended sampling parameters for a model family were applied at
install and then never took effect. Two things went wrong on the way to
disk.

They were written as top level keys. ModelConfig embeds PredictionOptions
under the "parameters" yaml key, so temperature, top_p, top_k, min_p,
repeat_penalty and presence_penalty are only read from there. At the top
level they parse without error and are then ignored for the life of the
model.

They were also merged in after the YAML had already been marshalled. The
only re-marshal sat behind the artifact binding, which an entry carrying
files: never reaches, so for those entries the defaults were computed and
then dropped before anything was written.

Neither failure was visible in normal use. ApplyInferenceDefaults runs
again at load time and fills the same values from the same table, so the
model ends up tuned correctly while the file on disk pins nothing. It
surfaces when someone edits one of those values expecting it to win, or
when a family is absent from inference_defaults.json and there is nothing
to refill from.

Both install paths are covered: an entry carrying files:, and one that
binds a primary artifact instead.

The empty base spec asserted that the authored parameters block landed
verbatim. It now checks the authored keys individually, because the family
defaults are merged into that same block.

Assisted-by: Claude:claude-opus-5

Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
2026-09-03 18:30:26 +02:00
localai-org-maint-botandmudler 49945fdd75 chore: ⬆️ Update 0xShug0/audio.cpp to c18b7f737aac0a2855e9f963a427498739ad40fe (#11843)
⬆️ 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-09-03 18:03:20 +02:00
Claudio Maradonna 9e831d7709 fix(ds4): build CUDA kernels for the target architecture (#11840)
* fix(ds4): build CUDA kernels for the target architecture

The ds4 backend compiled its CUDA objects with no -arch. Upstream's Makefile
leaves CUDA_ARCH empty and its `cuda` target refuses to build without one,
offering `cuda-spark` (sm_121) and `cuda-generic` (native) instead. We invoke
its object targets directly, which bypasses that guard, so nvcc fell back to
its default architecture and the kernels ran as JIT'd PTX on the real GPU.

On GB10 (sm_121) that silently corrupted inference: any prompt over roughly 128
tokens produced text unrelated to the input and never closed its thinking
block, so content came back empty and the chat showed only reasoning; longer
prompts failed with "cuda decode failed". It also cost close to two orders of
magnitude of prefill throughput. Measured on one box, same model, same prompt,
same GPU, upstream ds4 at the pinned commit, differing only in the nvcc flags:

  make -B ds4      (archless, as we build it)   garbage output    4.21 t/s
  make cuda-spark  (compute_121a/sm_121a)       correct output  325.70 t/s

Select an architecture list from CUDA_MAJOR_VERSION, which the backend matrix
already declares for both ds4 cublas entries but Dockerfile.ds4 never forwarded.
Upstream's CUDA_ARCH takes a single value, so it cannot express the fat binary
these images need; NVCC_ARCH_FLAGS is overridden instead, since a command-line
assignment wins over its `:=`. The lists are copied from vllm-cpp rather than
invented so the two CUDA images cover the same GPUs, with l4t/arm64 covering
Orin, Thor and GB10. An empty CUDA_MAJOR_VERSION keeps upstream's `native`
behaviour for local developer builds, and no CI runner has a GPU to enumerate.

DS4_CUDA_HAVE_MXF4 is deliberately left unset: upstream defines it only for
single-arch sm_120/sm_121 builds and guards it with a plain #ifdef rather than
__CUDA_ARCH__, so it cannot be combined with older archs. It gates an optional
MXFP4 indexer fast path whose #ifndef branch returns 0 and falls back cleanly,
so omitting it costs speed on GB10, not correctness.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Claudio Maradonna <git@codeshifter.xyz>

* test(ds4): cover the multi-batch prefill regression

The architecture fix has no automated guard: every existing e2e spec uses a
short prompt, and the miscompiled backend answered short prompts correctly.
The corruption only appears once a prompt spans more than one prefill batch,
so the whole suite passed against a backend that produced garbage in normal
use.

Add an opt-in "long_prefill" capability to the backend e2e suite that sends a
prompt well past one batch with a known needle and asserts the answer still
reflects it, and document in the ds4 guide why the build must never omit an
nvcc architecture, how to check which flags a configuration resolves to
without compiling, and how to run the new spec.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Claudio Maradonna <git@codeshifter.xyz>

---------

Signed-off-by: Claudio Maradonna <git@codeshifter.xyz>
2026-09-03 13:03:44 +02:00
Claudio Maradonna 335acce21f fix(ds4): cancel abandoned inference (#11822)
Propagate gRPC cancellation into DS4 prompt synchronization and poll it at decode boundaries.

Stop on failed stream writes and skip parser finalization and KV persistence for abandoned partial requests.

Assisted-by: Codex:gpt-5.6-sol

Signed-off-by: Claudio Maradonna <git@codeshifter.xyz>
2026-09-03 13:02:33 +02:00
localai-org-maint-botandmudler e9ba60ba57 chore: ⬆️ Update CrispStrobe/CrispASR to ff3945c94cab9191199a5d531a32c4e9535c094b (#11829)
⬆️ 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-09-03 13:02:00 +02:00
localai-org-maint-botandmudler 09f42db913 chore: ⬆️ Update NVIDIA/NeMo-Speech.cpp to 56b60d432f1731d6d5b28a4c5a31cbaf871daba1 (#11846)
⬆️ Update NVIDIA/NeMo-Speech.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-09-03 13:01:45 +02:00
localai-org-maint-botandmudler 1f4de9c809 chore: ⬆️ Update ikawrakow/ik_llama.cpp to caf7eae5282d840d77e9f91a56df7d2ef28fa612 (#11842)
⬆️ 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-09-03 13:01:32 +02:00
lei_lei 7c921aa020 fix(ui): omit empty system prompt so model default applies (#11838)
Web Chat Settings left the System Prompt field empty but still treated a
blank/whitespace value as an explicit system turn. That satisfied
tokenizer chat templates' messages[0].role == system check and suppressed
the model YAML system_prompt on fresh chats.

Omit empty/whitespace system messages in the React and Alpine UIs, strip
them server-side, and inject config.SystemPrompt for tokenizer-template
models when the request has no real system turn.

Fixes #11834

Signed-off-by: lei_lei <96427312+leilei3167@users.noreply.github.com>
2026-09-02 11:58:23 +00:00
localai-org-maint-botandEttore Di Giacinto 9afe10ba21 fix(distributed): survive a slow control-plane database (#11837)
* fix(distributed): evict only when a node is known to be full

scheduleNewModel asked the registry for a free replica slot and treated
every error as "this node is full", so a control-plane database slow
enough to time out the lookup evicted a healthy loaded model. The
evicted process died, a peer frontend still holding its address dialled
the dead port and retried, and the model thrashed between nodes. The
comment on the branch already said it meant a full node; the code never
tested for it.

Evict only on ErrNoFreeSlot. Any other error now returns and names the
lookup that failed, so a slow database degrades into a diagnosable
load failure instead of into lost work.

An audit of the rest of the router found one branch of the same shape:
node selection discarded the error from its last-resort finder, so a
database timeout there also produced a nil node and evicted for it.
That path now returns unless the finder said gorm.ErrRecordNotFound,
which is the only answer that means the cluster had no node to give.
No other destructive branch in router.go fires on a generic error.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* fix(distributed): checkpoint heartbeat writes instead of writing every beat

Every heartbeat UPDATEd backend_nodes. Six nodes at a ten second beat is
roughly 52,000 writes a day against a six-row table, and that churn is
what turned a blocked autovacuum into a 460 MB table whose six-row scan
cost 867 ms and timed out the queries that place models.

A beat carrying only a fresher timestamp now waits for the checkpoint
interval. Each reported field is compared against the value last
persisted rather than tested for presence, because a worker sends its
disk figures on every beat and presence alone would suppress nothing.
A node's first beat, a changed total VRAM, total disk or GPU vendor,
and a free VRAM, RAM or disk reading that has moved more than 256 MiB
from the persisted value all still write at once. A node that is not
active is never suppressed, because it recovers only when the health
monitor sees a fresh timestamp.

The persisted column is up to one interval stale by design, so the
stale-node threshold moves from 60s to 5m to cover it.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* fix(distributed): fail worker readiness when a held backend is unreachable

The readiness gate tracked only the NATS link, so a worker whose backend
processes had died still answered /readyz with 200 and kept receiving
loads. One node did exactly that during an incident: it reported healthy
while its backend port refused connections, and every load routed to it
failed.

Readiness is now the NATS link and, for each backend process the worker
believes it is running, a short dial of its recorded address. A worker
holding no backends stays ready, because idle is a healthy state.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* fix(distributed): keep a starting backend out of the readiness dial set

A backend process is inserted into the supervisor map with its gRPC
address already recorded, but the address refuses connections until the
gRPC server binds, which the startup poll allows up to 30 seconds for and
which takes 10 to 15 seconds on a slow node. The new data-path readiness
probe dialled that address straight away, so a worker answered /readyz
with 503 for the whole of every cold backend start. The container
HEALTHCHECK absorbs that, but a Kubernetes readinessProbe at 10s does
not, and the worker would leave rotation each time it loaded a model.

The skip for a stopping process had no counterpart at the other end of
the lifecycle. Backend processes now carry a serving flag, set where the
startup health-check gate succeeds, and the probe dials only processes
that are serving and not yet stopping. backendStartStillValid becomes
markBackendServing: the check and the mark must share one lock hold, so
the flag can only ever land on the entry the key currently owns.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* feat(distributed): export control-plane database health gauges

Four transactions wedged on a corrupt index held the vacuum horizon open
for 42 days. Nothing measured it, so the first symptom anyone saw was
models failing to load six weeks later, by which time a six-row table
had grown to 460 MB.

Export the oldest xmin age, the longest open transaction, and the dead
tuple ratio on the registry tables. The first is the number that would
have caught it: it sits near zero in health and was 21,002,291.

Sampling is scrape-driven behind a cache, and a failed sample reports
the last good values rather than failing the scrape, because these
gauges matter most when the database is already struggling.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* fix(distributed): rate-limit failed control-plane database samples

The cache advanced its clock only on a successful sample, so once the
database started failing every scrape retried the query immediately.
That turned the cache off in the one regime it exists for: a retry
storm at scrape cadence aimed at a database already in trouble. A
catalog read that consistently exceeds the 5 second timeout also paid
that cost on every scrape, with all scrapes serialised behind the
sampler mutex.

Time every attempt rather than every success, so failures and timeouts
cost the same interval as good samples. Whether a good sample exists
moves to its own field, keeping the gauges absent until the first
success and holding the last good values through later failures.

Also note in the runbook that pg_stat_activity cannot see prepared
transactions or replication slot xmins, so a healthy-looking xmin age
does not by itself rule out a blocked horizon.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* test(distributed): pin that a failing database evicts nothing

Exercises the real distributed stack against a control-plane database that
refuses the router's slot lookup, and asserts the scheduler reports the
lookup it could not answer instead of falling through to eviction.

The failure is injected with privileges rather than a statement timeout. A
timeout set with ALTER DATABASE also breaks AutoMigrate, and it leaks into
every later spec in the suite unless it is reset, so the spec would end up
testing the migration rather than the scheduler. Instead the spec creates a
dedicated login role, points a second gorm handle at it, and revokes that
role's SELECT on node_models.replica_index. This has to be a separate role:
the test container's owner is a PostgreSQL superuser, and superusers bypass
every privilege check, so revoking from CURRENT_USER is recorded and then
ignored.

The revoke is scoped to one column on purpose. Revoking the whole table
would also blind node selection, which runs first and has a guard of its
own, so the scheduler would never reach the slot lookup this spec is about.
Leaving every other column readable lets selection succeed and lands the
refusal exactly on NextFreeReplicaIndex, which plucks replica_index. The
grant is restored from BeforeEach via DeferCleanup, so a failing assertion
or a panic cannot hand the next spec a role that cannot read.

Reverting the eviction guard fails this spec, which is the point of it: the
router then reports "no replica slot on keeper and eviction failed" for an
error that was never evidence the node was full. The surviving-row
assertions are secondary under this injection, because the eviction path
reads whole node_models rows and the same revoke blinds it too; a comment
in the spec says so, so nobody mistakes them for the load-bearing ones.

Also documents why the vector store and the control plane must not share a
database: the removable-tuple cutoff is per database, not per table, so one
transaction left open anywhere stops autovacuum reclaiming the node
registry, and a six-row table bloats into hundreds of megabytes. The note
names LOCALAI_AUTH_DATABASE_URL and LOCALAI_AGENT_POOL_DATABASE_URL as the
two knobs that must differ, and the localai_control_plane_oldest_xmin_age
gauge as the way to see it coming.

grep for StaleNodeThreshold and HealthCheckInterval in
core/config/runtime_settings_registry.go returns no matches: the
distributed duration knobs are not exposed as runtime settings, so the new
heartbeat checkpoint interval follows them and needs no registry entry.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* fix(distributed): close the review gaps in the heartbeat and health path

The stale-node threshold moved from 60 seconds to 5 minutes in this branch
because checkpointing makes last_heartbeat up to one checkpoint interval
behind by design. Two things were left inconsistent with that. NewHealthMonitor
still fell back to a hardcoded 60 seconds when handed a zero threshold, so any
future caller that stopped passing the configured value would mark every
healthy, beating node offline on every cycle. And the threshold itself had a
flag-name constant but no AppOption, no CLI field and no env binding, so an
operator who widened --node-heartbeat-checkpoint had no way to widen the
threshold to match. The fallback now tracks config.DefaultStaleNodeThreshold,
and --stale-node-threshold / LOCALAI_STALE_NODE_THRESHOLD is wired the same
way its sibling is.

Heartbeat suppression compared the RAW reported free VRAM against the
snapshot, but the column persists capAvailable(raw, ceiling). On any node with
a VRAM budget set, whose actual free VRAM oscillates above that ceiling, every
beat looked material while the persisted value never moved: suppression was
defeated on exactly the nodes an operator had configured, and the write
amplification this branch exists to remove came straight back there. The
comparison and the snapshot now both hold the capped figure, so they measure
the same quantity as the column.

Fixing that needs the ceiling, and reading it cost a SELECT on every beat,
including suppressed ones. The skip decision therefore moved ahead of the
updates map and now reuses the ceiling cached on the last durable write, while
the write path still re-reads it before capping anything. A ceiling that
changed inside the checkpoint window can cost one extra or one late write; it
cannot persist a wrong figure. A suppressed beat now costs no query at all.

Also: the operations section now says to grant pg_read_all_stats to the
LocalAI role, because PostgreSQL blanks backend_xmin and xact_start for
sessions owned by other roles, and the transaction that wedged the horizon in
the incident was a co-located vector store connecting as a different role, so
without the grant the new gauge sees only our own sessions. The compose
healthcheck comment now describes readiness covering the backend data path,
and the control-plane gauge registration records the otel.SetMeterProvider
ordering it depends on.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* fix(distributed): resolve the gauge's table names through gorm

The dead-tuple gauge queried pg_stat_user_tables against a hardcoded list
of three table names. Those three do not agree on where their name comes
from: BackendNode and NodeModel take gorm's default pluralisation, while
GalleryOperationRecord overrides TableName, and gallery_operations
already had a constant of its own that the list duplicated.

A literal list keeps compiling after any of that moves, and the query
then matches nothing. The failure is silent and it points the wrong way:
a dead-tuple ratio that matched no rows reports the same numbers as a
cluster with no bloat, so the gauge would look healthiest exactly when it
had stopped working.

Ask gorm what each model is stored as instead, which follows a TableName
override and the default pluralisation alike. A spec pins that the
override really is consulted: naive pluralisation of the type would give
gallery_operation_records, so the resolution cannot quietly stop asking
the model.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-02 12:37:12 +02:00
Claudio Maradonna 30e53f8d9f fix(ds4): enforce generation boundaries (#11821)
Clamp requested generation to the usable context after prompt sync while preserving the legacy 256-token fallback for omitted limits.

Constrain each speculative MTP cycle to the remaining request budget so accepted tokens cannot advance beyond the visible output limit.

Assisted-by: Codex:gpt-5.6-sol

Signed-off-by: Claudio Maradonna <git@codeshifter.xyz>
2026-09-02 12:36:10 +02:00
localai-org-maint-botandmudler 38d12f1ba4 chore: ⬆️ Update mudler/vllm.cpp to 6bf3abb580982f4fd2e4525ef37802ee0ce28981 (#11828)
⬆️ 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-09-02 10:54:51 +02:00
localai-org-maint-botandmudler 35cfb4f433 chore: ⬆️ Update 0xShug0/audio.cpp to f334cff70a68ea3d2e40d6638733e8c1ec434164 (#11830)
⬆️ 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-09-02 10:54:38 +02:00
localai-org-maint-botandmudler 2bbcfb3ec6 chore(model-gallery): ⬆️ update checksum (#11831)
⬆️ 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-09-01 23:13:48 +02:00
e760eb236f chore: ⬆️ Update ggml-org/llama.cpp to 3466812d1f06728effe7c0f3c0671117f461672d (#11798)
* ⬆️ Update ggml-org/llama.cpp

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* fix(llama-cpp): link librdma from the static ggml-rpc build

ggml-rpc gained an Apple RDMA transport in this llama.cpp range and
declares its librdma dependency with target_link_options(ggml-rpc
PRIVATE "LINKER:-weak_library,..."). Link options are not a usage
requirement of a static library, so the llama-cpp-grpc variant, which
builds with BUILD_SHARED_LIBS=OFF, dropped the flag and left every
ibv_* symbol of transport-apple.cpp undefined when grpc-server linked
on darwin.

prepare.sh now re-declares the same weak link as INTERFACE on the
ggml-rpc target, so the flag reaches whoever links the static library.
The append is guarded on a marker for repeat runs, and on
GGML_RPC_RDMA_APPLE, which the turboquant and bonsai forks lack.

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

---------

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-01 23:13:11 +02:00
localai-org-maint-botandmudler aa1f0f8599 chore: ⬆️ Update mudler/vllm.cpp to 839ea1ceddb787778b6bd86a38a917a1aab74d8f (#11817)
⬆️ 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-09-01 13:00:02 +02:00
localai-org-maint-botandmudler 1da0dd7de2 chore: ⬆️ Update 0xShug0/audio.cpp to 3497b7cc44753e2c141d8fe60ac42cec433e3281 (#11818)
⬆️ 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-09-01 12:43:04 +02:00
localai-org-maint-botandmudler 34e9ad0cec chore: ⬆️ Update CrispStrobe/CrispASR to 78c545eb80409b91291642ddb23b3a6dc044fd34 (#11811)
⬆️ 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-09-01 12:38:24 +02:00
localai-org-maint-botandmudler 06ebc28ae6 chore: ⬆️ Update mudler/depth-anything.cpp to 02ba082274e001a63e50de5a1eb0ccc50c6af4b1 (#11810)
⬆️ Update mudler/depth-anything.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-09-01 12:37:10 +02:00
localai-org-maint-botandEttore Di Giacinto b4e16b963f fix(ci): stop the e2e teardown from failing a green suite (#11816)
* fix(ci): remove the e2e container before removing its image

`docker stop` returns as soon as the container exits, but the daemon
reaps a `--rm` container asynchronously after that. The `docker rmi
localai-tests` that follows teardown-e2e then loses the race against the
reaper and fails with "conflict: ... is using its referenced image", so
make exits 1 and the job goes red after every spec has passed.

This is why the E2E Backend Tests job fails at random across pull
requests. Runs 33435319093, 33435332991, 33412165884 and 33444669207 all
report "SUCCESS! -- 235 Passed | 0 Failed" and then die in teardown.

`docker rm -f` is synchronous, so the image reference is gone before
teardown-e2e returns. It also covers the case where no container is
running, which `docker stop` could not because it rejects an empty
argument list.

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

* fix(ci): open a tmate session only when a PR asks for one

The tmate step runs on every failure and then holds the runner until
GitHub cancels the job at the 6 hour limit. A one second cleanup race in
the e2e teardown therefore costs a whole ubuntu-latest slot. The recent
run list is full of 6h, 7h and 12h cancelled runs for that reason.

The step now needs the `ci-debug` label on the pull request, so a
session opens when somebody wants to debug and never otherwise. The
30 minute step timeout caps the cost when the label is left behind.

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

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-01 08:55:35 +02:00
localai-org-maint-botandmudler 834473a46d chore: ⬆️ Update 0xShug0/audio.cpp to bf3315fe4aaa16dc1125f580c29aff90a8900b36 (#11794)
⬆️ 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-09-01 08:34:36 +02:00
localai-org-maint-botandmudler 2dcd853a2b chore: ⬆️ Update mudler/vllm.cpp to 6a544bdb89eb5a3512ac922241439e45f24d74d4 (#11797)
⬆️ 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-09-01 00:06:49 +02:00
Claudio Maradonna 2ad4238416 fix(ds4): separate prefilled reasoning from content (#11802)
DS4 appends the opening thinking marker to tokenizer-templated prompts, so generated text begins directly with reasoning bytes. Starting DsmlParser in TEXT therefore puts the reasoning and closing marker in visible content.

Start the parser in THINK for structured chat requests with thinking enabled in both Predict and PredictStream. Keep the default TEXT state for raw prompts and reasoning-off requests, and add incremental regression coverage.

Assisted-by: Codex:gpt-5

Signed-off-by: Claudio Maradonna <git@codeshifter.xyz>
2026-09-01 00:06:23 +02:00
localai-org-maint-botandmudler 475dc254be chore: ⬆️ Update ikawrakow/ik_llama.cpp to 3c58ae373a0081c884099f435fb16ca720852bf7 (#11809)
⬆️ 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-09-01 00:06:05 +02:00
Ettore Di GiacintoandClaude Opus 5 5555a3c569 fix(nodes): skip checksum sidecars when staging option dirs
stageDirectory and countStageableFiles already skip them, but
stageOptionDir did not - and it is the path sherpa-onnx voices take for
espeak-ng-data. The receiver writes "<file>.sha256" for every file it
accepts, so staging the sidecars made it write sidecars for those in
turn, one level deeper on every load.

Observed on a live node: "<file>.sha256" repeated eleven times, 5077
junk files out of 7832 in the models dir, and still growing. Staging
never finished, so vits-piper-it_IT-paola-sherpa stayed permanently
"staging on node" and every realtime warmup needing that voice failed
with the session then going silent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0142UfUh8HWxdim5JZqf8Tr6
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-01 00:03:04 +02:00
localai-org-maint-botandlocalai-org-maint-bot 357eabf382 feat(gallery): add Ornith 1.5 9B OBLITERATED (#11803)
Add Q4 and Q8 GGUF builds with their shared vision projector. The
model is a recent refusal-removed Ornith derivative for alignment and
red-team research.

Assisted-by: Codex:gpt-5

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-31 18:06:07 +02:00
ginavalent 04bca750d0 refactor(worker): iterate node label pairs with SplitSeq (#11806)
Signed-off-by: ginavalent <ginavalent@outlook.com>
2026-08-31 18:05:44 +02:00
localai-org-maint-botandmudler 69a5b54c0a chore: ⬆️ Update CrispStrobe/CrispASR to 18b3e3f8456748a6380dc4c13817df244b695d39 (#11799)
⬆️ 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-31 13:11:15 +02:00
localai-org-maint-botandmudler bdf600ea2a chore: ⬆️ Update ggml-org/whisper.cpp to eacbd8234c6654cdbf2c377f72b2106875479bdc (#11796)
⬆️ 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-31 13:11:05 +02:00
lei_lei b8316f2a2a fix(gallery): use published F16 mmproj for qwythos-9b (#11792)
Install 404s because the gallery still points at mmproj-...-f16.gguf.
HF only ships ...-F16.gguf now, with a different sha256.

Signed-off-by: lei_lei <96427312+leilei3167@users.noreply.github.com>
2026-08-31 10:17:11 +02:00
localai-org-maint-botandmudler 1ab3db4bb7 chore: ⬆️ Update leejet/stable-diffusion.cpp to 6b3edaaf32cc19e5bb2d819c788bd557eddc8eba (#11793)
⬆️ 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-31 09:39:08 +02:00
Ettore Di Giacinto f829059e3d Rename branches from 'update/' to 'bump/'
Signed-off-by: Ettore Di Giacinto <mudler@users.noreply.github.com>
2026-08-31 09:38:28 +02:00
Ettore Di Giacinto 7aeb47cbf3 fix(launcher): auto-start the server so launching the app actually serves
Fixes #11673: on macOS the DMG launcher appeared to launch nothing. After
installing, the app sat in the menu bar with no window, nothing listening
on localhost:8080, and empty log files, because nothing ever started the
server unless the unrelated 'start on system boot' option was enabled.

- Start the LocalAI server automatically when the launcher opens and right
  after a fresh install. The new auto_start_server config key defaults to
  enabled and gets a settings checkbox; the legacy auto_start key was never
  honored nor exposed, so every existing launcher.json carries an
  unintentional false and is deliberately left behind.
- Fix the welcome window suppressing itself: its 'don't show this again'
  checkbox was initialized with the inverted value, and SetChecked fired
  the change callback which persisted ShowWelcome=false on the very first
  showing.
- Surface auto-start failures through the systray startup-error dialog,
  since there is no visible window during auto-start.
- Pass --app-version to fyne package so the app stops reporting itself as
  version 0.0.0 in the About box.
- Document the first-launch flow (menu bar app, auto-start, WebUI URL) in
  the macOS getting-started page.
- Repair two launcher specs that never ran in CI: a *bool matched against
  BeTrue and a /tmp assertion that trips on Linux where the test tempdir
  itself lives under /tmp.

Assisted-by: Claude Code:claude-fable-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-08-30 21:45:59 +00:00
localai-org-maint-botandlocalai-org-maint-bot dd4e75983d feat(gallery): add Qwen3.8 GSQ-RCO variants (#11787)
Add three llama.cpp-compatible mixed quantizations from ISTA DASLab. These builds give Qwen3.8-27B users an 8.4 to 10.1 GB weight tier with the shared vision projector.

Assisted-by: Codex:gpt-5

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-30 20:10:22 +02:00
localai-org-maint-botandlocalai-org-maint-bot 9c7c9974eb feat(gallery): add LFM2.5 8B DSpark variants (#11751)
Add Q4 and Q8 target builds with LiquidAI DSpark draft sidecars.
The variants expose mainline llama.cpp speculative decoding for the
existing LFM2.5 8B family.

Assisted-by: Codex:gpt-5

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-30 09:33:39 +02:00
localai-org-maint-botandlocalai-org-maint-bot 2a13b6e1f2 feat(gallery): add Qwen3.8 Cold Fusion (#11754)
Add Q4_K_M and Q8_0 MTP variants with the shared vision projector.
The publisher recommends these builds for faster Qwen3.8 generation.

Assisted-by: Codex:gpt-5

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-30 09:33:24 +02:00
localai-org-maint-botandlocalai-org-maint-bot 287ef12bf0 feat(gallery): add Granite 4.2 variants (#11779)
Add the 3B, 8B, and 30B safetensors checkpoints as one vLLM variant family so LocalAI can select the largest build that fits. Configure the parsers and sampling defaults recommended for Granite reasoning and tool calls.

Assisted-by: Codex:gpt-5

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-30 09:01:05 +02:00
mudler's LocalAI [bot]andmudler a7cc5873ef chore(model gallery): 🤖 add 1 new models via gallery agent (#11777)
chore(model gallery): 🤖 add new models via gallery agent

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-29 21:29:32 +02:00
mudler's LocalAI [bot]andEttore Di Giacinto 572a127682 feat(stablediffusion-ggml): build a ROCm variant (#11774)
The Makefile already had a hipblas branch, but no CI row built it and
the gallery's `amd:` mapping stayed commented out. On an AMD host the
capability lookup found no `amd` key and fell back to `default`, so
these users silently ran the CPU build.

Add the hipblas row to the backend matrix and the two gallery entries
it publishes, then point `amd:` at them.

Drop `-DGGML_HIPBLAS=ON` while here. `SD_HIPBLAS` sets `GGML_HIP`
itself, and `GGML_HIPBLAS` is the name ggml used before the rename, so
the flag only produced an unused-variable warning. Add gfx1151 to the
local target list to match the value the workflows pass in.


Assisted-by: Claude Code:claude-opus-5[1m]

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
2026-08-29 21:29:16 +02:00
localai-org-maint-botandlocalai-org-maint-bot 27bcf86a5a feat(gallery): add WeMM embedding variants (#11775)
Tencent released three WeMM sizes with direct Sentence Transformers support. Add each safetensor repository so users can select the quality and resource tradeoff.

Assisted-by: Codex:gpt-5

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-29 21:29:00 +02:00
localai-org-maint-botandlocalai-org-maint-bot 893a45141c fix(realtime): accept GA WebRTC signaling (#11778)
OpenAI GA clients send multipart or raw SDP requests. They expect a bare
SDP answer. LocalAI only accepted its legacy JSON envelope, so signaling
failed before media setup.

Keep the JSON contract for existing clients. Accept both GA request
shapes and choose the matching response format.

Assisted-by: Codex:gpt-5

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-29 21:28:37 +02:00
mudler's LocalAI [bot]andmudler 1db8db762d chore: ⬆️ Update mudler/vllm.cpp to 150b37852c123f7855fb219b37347572ca9427e7 (#11745)
⬆️ 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-29 21:20:09 +02:00
localai-org-maint-botandlocalai-org-maint-bot 16aa8ca004 feat(gallery): add Qwen3.8 Flash Next (#11763)
Add the Q4 and Q8 GGUF builds with the shared vision projector.\nThe variant pair lets LocalAI select the build that fits available memory.\n\nAssisted-by: Codex:gpt-5

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-29 10:34:53 +02:00
localai-org-maint-botandlocalai-org-maint-bot 62f1c0ca7f feat(gallery): add PhoneLLM variants (#11772)
Add complete vLLM and SGLang entries with their exact tool parsers. Preserve an explicit zero temperature in both backend adapters.

Assisted-by: Codex:gpt-5

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-29 10:03:54 +02:00
mudler's LocalAI [bot]andmudler 14c13ca1ef chore: ⬆️ Update 0xShug0/audio.cpp to 89a0e9803380880305e9e1b83c93614f9df2c893 (#11769)
⬆️ 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-29 09:43:27 +02:00
github-actions[bot]andmudler 176683dbe6 chore: bump inference defaults from unsloth (#11773)
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-08-29 09:17:12 +02:00
mudler's LocalAI [bot]andEttore Di Giacinto 80e3240f2d feat(distributed): key scheduling rules by a model alias (#11771)
Node placement and replica rules could only name a model, so an operator
who pinned "llama3" to the GPU tier had to rewrite the rule whenever a
different model took over that job. An alias already gives a stable name
for whichever model serves it, and a rule on that name makes it a
deployment slot: repoint the alias and the placement follows.

A rule keeps the name the operator chose. Reads resolve that name through
the config loader to the model the rule governs, so the reconciler counts,
schedules and trims replicas of the target, and the router finds an
alias-keyed rule from the target it is already routing. An alias that
resolves to nothing governs nothing loadable, so the reconciler skips it
and the write paths refuse it.

A replica is shared by every name that resolves to it, so only one rule
can decide where it runs. The REST and MCP write paths reject a rule whose
target another rule already governs. A pair that arrives some other way,
such as a seed file or an alias repointed onto a model that already has a
rule, resolves in favour of the rule named after the model itself and then
the oldest, and the rest are listed as shadowed.

The eviction guard is the exception: it matches rules to replicas in raw
SQL inside a locking transaction and cannot resolve an alias. It reads a
stored target that the reconciler refreshes each tick, and falls back to
the rule's own name when that target is empty.


Assisted-by: Claude:claude-opus-5 golangci-lint eslint

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
2026-08-29 09:16:50 +02:00
mudler's LocalAI [bot]andmudler f9f4d2751f chore: ⬆️ Update ggml-org/llama.cpp to d7bd3bfcad3e29c7e49fd26f38c79ee3e9a3fd6b (#11770)
⬆️ Update ggml-org/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-29 09:16:37 +02:00
mudler's LocalAI [bot]andmudler a5a8338eeb chore: ⬆️ Update antirez/ds4 to 8db89fe083ae4d17c9a2428ccd29803d3ae8f577 (#11768)
⬆️ 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-29 01:04:06 +02:00
mudler's LocalAI [bot]andmudler 8c2c3c5777 chore: ⬆️ Update ikawrakow/ik_llama.cpp to 15dddc60b3fc937a9e2a210359ecce392ccdf446 (#11767)
⬆️ 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-29 01:03:53 +02:00
localai-org-maint-botandlocalai-org-maint-bot 9db6caf3fd feat(gallery): add Thomson 1.0 Small variants (#11766)
Add Q4_K_M and Q8_0 GGUF builds with the shared BF16 vision
projector.

Assisted-by: Codex:gpt-5

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-28 22:57:53 +02:00
mudler's LocalAI [bot]andEttore Di Giacinto 29899cd1e0 fix(ui): size model fit against the cluster and move node labels into the selector (#11765)
* fix(ui): move node labels into the scheduling selector field

The scheduling page kept a node-label browser open above the rules
whether or not anyone was writing one, while the field that actually
needs labels, the rule's node selector, was two bare text inputs with no
hint of what the cluster reports.

The browser is gone. The selector's key input now completes against the
label keys the cluster uses, and the value input offers only the values
that key takes. The roster already loads for the page, so the
suggestions cost no request, and a roster that fails to load costs the
admin the hints and nothing else.

Suggestions stay suggestions: a key no node reports yet still commits as
typed, which is how an admin writes a rule before labelling the nodes
for it.

Assisted-by: Claude:claude-opus-5 golangci-lint eslint playwright
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* fix(distributed): size model fit against the cluster, not the frontend

The models page asked the frontend how much memory a model may occupy.
In distributed mode the frontend is usually a GPU-less pod while every
model runs on a worker, so a fleet of GPU nodes was told it could only
run the smallest CPU build. The variant picker's fits flag and its
auto-selection came from the same place, as did the hardware
recommendations.

The registry now reports the largest single healthy backend node. The
largest node, not the fleet total: a model loads into one node, so four
16GB workers are not a home for a 40GB model. An operator-set VRAM
budget caps a node's contribution, because the scheduler refuses a load
above that ceiling anyway, and a GPU node beats a CPU node holding more
system RAM.

GET /api/resources and GET /api/models carry this as an additional
cluster object. Their aggregate and ram fields keep reporting the
frontend's own hardware, which is what the resource monitor shows.
Variant selection judges backends against the union of the capabilities
present in the cluster, the way backend discovery already did.

Every path degrades to the local host: no cluster object in single-node
mode, and none when the registry cannot be read, so a hiccup narrows the
answer back to single-node behaviour rather than marking the whole
catalog too large.

The verdicts now name the node they belong to, since a model fits
somewhere or nowhere.

Assisted-by: Claude:claude-opus-5 golangci-lint eslint playwright
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
2026-08-28 22:57:35 +02:00
0cdc31dcb3 chore: ⬆️ Update ggml-org/llama.cpp to e70802a01f03f0ed31a26338a5664796f3824371 (#11755)
* ⬆️ Update ggml-org/llama.cpp

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* fix(llama-cpp): follow upstream MTMD APIs

The dependency update adds MTMD initialization options to prompt and
bitmap helpers. The gRPC adapter now passes the server options through
each affected path.

The update also replaces the per-layer MoE regex helper. Preparation
probes both APIs because older forks still reuse this adapter.

Assisted-by: Codex:gpt-5

---------

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-28 14:06:35 +02:00
mudler's LocalAI [bot]andmudler 51f906f4e0 chore: ⬆️ Update 0xShug0/audio.cpp to 17751c0e8c48a3d56dcf05eeb60464409ecc69ce (#11759)
⬆️ 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-28 08:52:13 +02:00
localai-org-maint-botandlocalai-org-maint-bot d85577ff5c docs: add Apache APISIX reverse proxy example (#11294)
docs: add APISIX reverse proxy example

Document the route settings needed for forwarded headers, streaming responses, and long-running inference behind Apache APISIX.

Closes #11215

Assisted-by: Codex:gpt-5

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-28 08:51:38 +02:00
mudler's LocalAI [bot]andmudler 81a54573ed chore: ⬆️ Update leejet/stable-diffusion.cpp to be0e34480dada95f8ce9a021bbb95c5de85d67c7 (#11760)
⬆️ 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-28 08:49:26 +02:00
mudler's LocalAI [bot]andmudler 83972b593f chore: ⬆️ Update ikawrakow/ik_llama.cpp to 7cff686d3732bfef5ce18bc4a6115fbceda29c14 (#11757)
⬆️ 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-28 08:48:31 +02:00
mudler's LocalAI [bot]andmudler f2814b9b83 chore: ⬆️ Update mudler/depth-anything.cpp to 739992d10bf9472c46dcd4622b14d2b20766c58d (#11758)
⬆️ Update mudler/depth-anything.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-28 08:48:15 +02:00
dependabot[bot] 92bcbaea87 chore(deps): bump vllm from 0.26.0 to 0.28.0 in /backend/python/vllm (#11752)
Bumps [vllm](https://github.com/vllm-project/vllm) from 0.26.0 to 0.28.0.
- [Release notes](https://github.com/vllm-project/vllm/releases)
- [Changelog](https://github.com/vllm-project/vllm/blob/main/RELEASE.md)
- [Commits](https://github.com/vllm-project/vllm/compare/v0.26.0...v0.28.0)

---
updated-dependencies:
- dependency-name: vllm
  dependency-version: 0.28.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-27 22:32:53 +02:00
Plamen K. Kosseff e58dabf75f feat(ui): add 'Focus mode' option in chat settings to persistently toggle the sidebar auto-collapse (#11750)
Assisted-by: Claude:claude-fable-5

Signed-off-by: Plamen K. Kosseff <p.kosseff@gmail.com>
2026-08-27 22:32:32 +02:00
mudler's LocalAI [bot]andmudler 1070cb1245 chore: ⬆️ Update 0xShug0/audio.cpp to db21cbdd60f3d2ff62114bc863781ff8073ac39b (#11746)
⬆️ 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-27 09:54:02 +02:00
mudler's LocalAI [bot]andmudler 1b4c4853fb chore: ⬆️ Update ggml-org/llama.cpp to 925e1179947ea0c0ebfb0032df18af3a729822be (#11744)
⬆️ Update ggml-org/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-27 09:53:49 +02:00
mudler's LocalAI [bot]andmudler 460c22bff6 chore: ⬆️ Update ikawrakow/ik_llama.cpp to ef40550042973817ac391ca95a2ff041f512257b (#11743)
⬆️ 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-27 09:53:36 +02:00
mudler's LocalAI [bot]andmudler 8712d37e2e chore: ⬆️ Update vllm-project/vllm cu130 wheel to 0.28.0 (#11741)
⬆️ Update vllm-project/vllm cu130 wheel

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-27 09:53:25 +02:00
mudler's LocalAI [bot]andmudler 74b885c31a chore: ⬆️ Update PrismML-Eng/llama.cpp to 312bb2a93ea2bf798333fa859614fbf913ecb9e2 (#11740)
⬆️ Update PrismML-Eng/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-27 09:53:12 +02:00
mudler's LocalAI [bot]andmudler 0a89fdb1d0 chore(model-gallery): ⬆️ update checksum (#11742)
⬆️ 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-27 00:18:18 +02:00
Szymon Podeszwa 6f6ddba746 fix(deps): bump go-m1cpu to v0.2.2 to fix SIGSEGV on Apple M5 (#11736)
go-m1cpu v0.1.6 runs its cgo initialiser from a package init(), where
getFrequency() dereferences the CFTypeRef returned by
IORegistryEntryCreateCFProperty without a NULL check. On Apple M5 the
pmgr IORegistry node does not expose voltage-states5-sram /
voltage-states1-sram in the shape v0.1.6 expects, so the call returns
NULL and CFDataGetLength(NULL) faults before main() runs. Every command
dies, including local-ai --version.

The package is linked indirectly: cmd/local-ai reaches
gopsutil/v3/{process,disk}, which pull in gopsutil/v3/cpu on darwin,
which calls m1cpu.IsAppleSilicon() and m1cpu.PCoreHz().

v0.2.2 adds the missing NULL guard and moves the IORegistry probe out of
init() behind a lazy sync.Once. The exported Go API is unchanged and the
non-darwin stub is byte-identical, so gopsutil/v3 compiles against it
untouched and no other platform is affected.

Bumping gopsutil/v3 is not an alternative: v3.24.5 is the final v3
release, so the v3 line will never carry this fix.

Fixes #11735

Assisted-by: Claude:claude-opus-5

Signed-off-by: Szymon Podeszwa <2962046+sz-po@users.noreply.github.com>
2026-08-26 21:33:37 +02:00
fa19b08f35 chore: ⬆️ Update mudler/vllm.cpp to 6738e0b4639199f3ff0998815e4d32bfa7fe5be2 (#11647)
* ⬆️ Update mudler/vllm.cpp

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* fix(vllm-cpp): mirror ABI v23

The new engine pin reports ABI v23 and appends mmproj_path to
vllm_model_params. LocalAI still declares v21, so the build-time ABI
guard rejects every backend build.

Grow the Go mirror by the appended pointer and update its offset checks.
ABI v23 adds a video function but does not change the mirrored text
structs.

Assisted-by: Codex:gpt-5.6 [systematic-debugging]

---------

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
Co-authored-by: localai-org-maint-bot <bot-opensource@localaisrl.com>
2026-08-26 16:16:49 +02:00
localai-org-maint-botandlocalai-org-maint-bot bbd3ab5a14 feat(gallery): add Tiel-Coder 35B variants (#11723)
Add Q4_K_XL, MTP Q4_K_XL, and Q8_K_XL builds with their BF16 vision projectors.

Assisted-by: Codex:gpt-5

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-26 09:02:31 +02:00
localai-org-maint-botandlocalai-org-maint-bot 5dab4fcde9 feat(gallery): add Granite 4.2 variants (#11719)
Add the official IBM Q4_K_M and Q8_0 GGUF builds for the 3B, 8B, and 30B Granite 4.2 models.

Assisted-by: Codex:gpt-5.6-sol

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-26 09:01:55 +02:00
mudler's LocalAI [bot]andmudler 15f12074ca chore: ⬆️ Update leejet/stable-diffusion.cpp to 50d640568388f876b0d63ee6ddb6bc86d997ec64 (#11725)
⬆️ 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-26 08:59:12 +02:00
mudler's LocalAI [bot]andmudler 5c08ec0382 chore: ⬆️ Update ggml-org/llama.cpp to eab8ee41f889ef7823af517e8098fb8a9b3cf601 (#11724)
⬆️ Update ggml-org/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-26 08:58:37 +02:00
Copilotandmudler e7b83ef7c0 Fix flaky "tests-apple" CI job in modeladmin test suite (#11717)
* Initial plan

* tests: raise default Eventually timeout for modeladmin suite to fix flaky macOS CI

Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-08-26 08:58:17 +02:00
mudler's LocalAI [bot]andmudler aea477932d chore(model-gallery): ⬆️ update checksum (#11730)
⬆️ 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-26 08:57:58 +02:00
lei_lei f28e8b24e6 fix(ollama): accept :latest tag on model lookup (#11732)
/api/tags appends :latest to untagged names, but chat and the other
model endpoints looked the tagged name up as-is and 404'd.

Signed-off-by: lei_lei <96427312+leilei3167@users.noreply.github.com>
2026-08-26 08:57:36 +02:00
mudler's LocalAI [bot]andmudler 5755898e57 chore: ⬆️ Update ggml-org/whisper.cpp to 978113305b2ead22249b881deafa131dc8884911 (#11711)
⬆️ 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-26 01:01:32 +02:00
mudler's LocalAI [bot]andmudler f63f11eb86 chore: ⬆️ Update 0xShug0/audio.cpp to c79e58899bf13db4d78fd06372da23cc13f55b28 (#11722)
⬆️ 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-26 01:01:19 +02:00
mudler's LocalAI [bot]andmudler 33dafe37ab chore: ⬆️ Update ikawrakow/ik_llama.cpp to 08b500b958a3f1102e6500e5c425e65517d6fb7e (#11726)
⬆️ 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-26 01:01:06 +02:00
localai-org-maint-botandlocalai-org-maint-bot edabdf9501 feat(gallery): add Ornith 1.5 397B variants (#11716)
* feat(gallery): add Ornith 1.5 397B variants

Add the official Q4_K_M and Q8_0 GGUF builds with their shared BF16 vision projector.

Assisted-by: Codex:gpt-5

* feat(gallery): resolve Ornith variant ordering\n\nKeep the 35B entries from master next to the 397B variants.\n\nAssisted-by: Codex:gpt-5

---------

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-25 17:36:02 +02:00
fa9ffc181c chore: ⬆️ Update ggml-org/llama.cpp to f280b26983ad0fdb705a0d9ebf0503e76f2899b0 (#11646)
* ⬆️ Update ggml-org/llama.cpp

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* fix(llama-cpp): adapt to the common JSON API

The llama.cpp bump replaces its nlohmann JSON alias with common_json. Update the gRPC adapter for the new exception, iterator, conversion, and container APIs.

Assisted-by: Codex:gpt-5.6 [systematic-debugging]

* fix(turboquant): adapt the JSON exception type

The shared gRPC source now follows the upstream common_json API. The
TurboQuant fork still exposes nlohmann JSON and cannot compile the new
exception type.

Translate that exception in the fork-specific source patch so both
llama.cpp variants compile from the shared adapter.

Assisted-by: Codex:gpt-5.6 [systematic-debugging]

* fix(bonsai): adapt the JSON exception type

The shared gRPC source uses upstream's common_json wrapper. The Bonsai fork still exposes nlohmann JSON and cannot compile that exception type.\n\nTranslate the exception in the fork-specific preparation step and verify that repeated preparation stays idempotent.\n\nAssisted-by: Codex:gpt-5.6 [systematic-debugging]

* fix(llama-cpp): let prepare register gRPC

The score patch duplicated the gRPC CMake registration that prepare.sh already owns. Its stale context rejects the current upstream tools file on Darwin before compilation starts.

Assisted-by: Codex:gpt-5

---------

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-25 12:57:12 +02:00
mudler's LocalAI [bot]andmudler ccb9a0a088 chore: ⬆️ Update 0xShug0/audio.cpp to d25ffac094a9d5a240940b4955ea79ad9b7b4c78 (#11710)
⬆️ 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-25 12:55:43 +02:00
localai-org-maint-botandlocalai-org-maint-bot f7c55788c7 feat(gallery): add Ornith 1.5 35B variants (#11714)
Add the official Q4_K_M and Q8_0 GGUF builds with their shared BF16 vision projector.

Assisted-by: Codex:gpt-5

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-25 12:54:00 +02:00
lei_lei a760a7ab4b fix(backends): honor enable_thinking=false in sglang and vllm (#11715)
Those backends only forwarded the flag when it was "true", so "false"
never reached apply_chat_template and Qwen3 kept thinking on.

Signed-off-by: lei_lei <96427312+leilei3167@users.noreply.github.com>
2026-08-25 12:52:57 +02:00
mudler's LocalAI [bot]andmudler 964be3bceb chore: ⬆️ Update ikawrakow/ik_llama.cpp to 0ed847d3140baead542abe3e5e6fe841013e7340 (#11708)
⬆️ 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-25 08:54:29 +02:00
mudler's LocalAI [bot]andmudler 496921f73a chore(model-gallery): ⬆️ update checksum (#11707)
⬆️ 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-24 23:41:38 +02:00
Ettore Di Giacinto f7ded96b1e fix(distributed): probe liveness on a subject every worker answers
The scheduler's liveness probe asks a worker a question over NATS and
reads "no responders" as proof the worker is gone. That is only sound
when every worker in the fleet subscribes to the subject asked.

It asked models.running, which arrived in 4.6. A 4.5 worker is alive and
serving, answers backend.list, and never subscribes to models.running,
so the probe condemned it on every scheduling attempt and marked it
unhealthy. A model pinned to such a node by its selector could then
never be placed at all: on this cluster an embedding model pinned to the
one Apple node was unschedulable for exactly this reason, while that
node's log showed it handling backend.list throughout.

Ask backend.list, which has been in the worker protocol far longer, and
treat a worker that answers anything as alive. Only a node that reports
no responders on every subject is absent, so adding a newer subject here
can never condemn an older worker.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude Code:claude-opus-5 [golangci-lint]
2026-08-24 19:58:49 +00:00
Ettore Di Giacinto 1dc3aeef87 fix(distributed): resolve config revisions through one entry point
A model's revision is published by administration and checked against on
every inference request. Those were computed by separate code: the
request path resolves through the loader, while each publisher hashed
whatever ModelConfig it happened to hold. By then SetDefaults had folded
in the GGUF guess and app-level options, so the published value was one
no request would ever carry and the model became unroutable until the
row was deleted by hand.

Fixing the publishers one at a time did not hold. Three rounds each
found another: the startup resync, then a saved edit and a toggle, then
a rename and the peer-change path.

ModelConfigLoader.RevisionFor is now the only way to obtain a revision,
and the raw hash is unexported, so a caller outside this package cannot
hash a config it holds. A publisher and a request agree by construction
rather than by two implementations happening to match.

The request path no longer falls back to hashing its merged config
either: an unstamped config is routed without a revision rather than
with a wrong one.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude Code:claude-opus-5 [golangci-lint]
2026-08-24 19:11:16 +00:00
Ettore Di Giacinto 2c68fa1eb6 fix(distributed): keep eviction inside the model's node selector
When no node the selector allows has a free slot, scheduling falls back
to evicting the least-recently-used idle model. That eviction searched
every healthy node, so it freed a slot on a node the selector forbids
and the model was then placed there: pinned to one class of hardware and
running on another.

An unrelated model pays for it. On this cluster an embedding model
pinned to Apple hardware could not reach its only matching node, so each
attempt evicted a large language model from an Nvidia node, failed to
start there anyway, and left the evicted model to reload. Repeated, that
reads as one replica bouncing between nodes.

Eviction is now restricted to the candidate set the selector produced.
With no selector the candidate set is nil and eviction stays global.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude Code:claude-opus-5 [golangci-lint]
2026-08-24 13:18:35 +00:00
Ettore Di Giacinto 38ba3fec63 fix(distributed): stop reclaiming healthy reconciler-driven loads
The abandoned-load sweeper treated a replica row with no load job as
abandoned. Only the request path creates load jobs; the reconciler's own
scale-up loads a replica without one. So any scale-up that ran past the
five-minute grace period was deleted mid-transfer, which for a
multi-gigabyte checkpoint is every time. The replica never finished
anywhere, and the reconciler kept re-placing it, so it looked like one
replica hopping between nodes instead of a model reaching its replica
count.

A row with no job is now reclaimed only once its node stops being
healthy, which is the case the sweeper was written for: a worker that
dropped out mid-transfer. A job that failed or stopped heartbeating
still proves abandonment on its own. Every uncertain case leaves the
slot held.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude Code:claude-opus-5 [golangci-lint]
2026-08-24 13:00:34 +00:00
Ettore Di Giacinto bebd812e7d fix(distributed): stop flapping agent nodes on backend listing
Only backend workers subscribe to backend.list. ListBackends asked every
node that was not pending, offline or draining, so an agent worker could
only answer "no responders", which the error handling reads as a node
that has gone away. Every poll of the backends view therefore marked
each agent node unhealthy, and its next heartbeat marked it healthy
again.

While unhealthy the node is not schedulable, so this also cost agent
capacity for as long as each flap lasted.

Skip non-backend workers, as the backend-op fan-out already does for the
same reason. A backend worker that does not answer is still marked
unhealthy: that one really is gone.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude Code:claude-opus-5 [golangci-lint]
2026-08-24 12:48:23 +00:00
Ettore Di Giacinto df1a40f9c0 fix(distributed): hash the config as persisted, not as defaulted
The revision was computed after SetDefaults, which folds in things that
are not persisted configuration: the GGUF guess, the hardware defaults,
and app-level options such as threads.

The GGUF guess is the damaging one. It parses the model file to fill in
values like context size, and when that parse fails it falls back to a
different default. Whether a multi-gigabyte file on network storage
parses at a given moment is not a property of the configuration, so one
unchanged YAML produced two different revisions depending on when it was
read. The controller rejected every request carrying the other one, and
the model stayed unroutable until the stored value happened to match
again. This is why it never reproduced against a model directory with no
weights in it: the guess is skipped there and both values agree.

The app-level defaults are the same class of bug with a slower fuse:
changing threads in the settings UI changed every model's revision and
made every model unroutable.

The revision is now stamped when the file is parsed, before any defaults
are applied, so it is a function of the file alone.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude Code:claude-opus-5 [golangci-lint]
2026-08-24 11:45:32 +00:00
Ettore Di Giacinto 505a6d040b fix(distributed): publish the revision a request actually carries
Two code paths computed a model's revision. Inference resolves the
config through the loader, which applies SetDefaults a second time.
Everything that publishes a revision hashed the stored config instead,
with SetDefaults applied once.

SetDefaults is not idempotent for every model: it re-runs the GGUF guess
and the hardware defaults, both of which read state the stored config
does not carry. Where the two disagree, a publisher wrote a revision no
request would ever carry, and the model became unroutable the moment it
was published. On this cluster the startup resync republished one such
value and every request for that model was then rejected against it.

The publishers now resolve the revision through the loader, exactly as a
request does, so there is one definition rather than two that agree only
when SetDefaults happens to be idempotent. This covers the startup
resync, a saved config edit, and enabling or disabling a model.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude Code:claude-opus-5 [golangci-lint]
2026-08-24 09:05:52 +00:00
mudler's LocalAI [bot]andmudler 98649d775e chore(model gallery): 🤖 add 1 new models via gallery agent (#11692)
chore(model gallery): 🤖 add new models via gallery agent

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-24 09:45:32 +02:00
mudler's LocalAI [bot]andmudler dc303aa96c feat(swagger): update swagger (#11682)
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-24 09:44:51 +02:00
DanielSwift1992 336b97fcfe chore(deps): remove 16 dependabot entries for directories that no longer exist (#11686)
Remove 16 dependabot entries for directories that no longer exist

Signed-off-by: Daniil S <daniel.swift.1992@gmail.com>
2026-08-24 09:44:36 +02:00
mudler's LocalAI [bot]andmudler dc0961f962 chore: ⬆️ Update 0xShug0/audio.cpp to 288a2712316470847a730e55db9ac9e5062a2b03 (#11683)
⬆️ 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-24 09:34:09 +02:00
mudler's LocalAI [bot]andmudler 1bee6b14b7 chore: ⬆️ Update CrispStrobe/CrispASR to ae4474dd8306384a0e697183d863dfc52e69a2fb (#11684)
⬆️ 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-24 09:33:56 +02:00
mudler's LocalAI [bot]andEttore Di Giacinto d7ff43781d fix(oci): resume interrupted layer downloads (#11688)
quay.io redirects blob downloads to pre-signed S3/Akamai URLs that
expire after about 10 minutes. On a slow connection a multi-GiB
backend layer cannot finish inside that window, so the connection
drops mid-stream on every attempt. The retry added for #10577
restarted each attempt from byte zero, which replayed the same
failure until the budget ran out and the install failed with
"unexpected EOF".

A retry now keeps the bytes already on disk and re-requests the
blob with "Range: bytes=N-". Each request goes back to the
registry, so it gets a fresh redirect URL and auth token. The
retry budget only counts attempts that made no forward progress,
so a slow link that keeps advancing keeps downloading. A resumed
file is spliced from separate responses and bypasses the digest
check in layer.Compressed(), so the assembled file is re-verified
against the layer digest before it is trusted; on a mismatch the
download starts over through the verified reader.

Fixes #10577


Assisted-by: Claude Code:claude-fable-5

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
2026-08-24 09:33:44 +02:00
localai-org-maint-botandlocalai-org-maint-bot e470d4b625 feat(gallery): add Qwen3.8 OBLITERATED variants (#11691)
* feat(gallery): add Qwen3.8 OBLITERATED variants

Add Q4_K_M and Q8_0 llama.cpp builds with the shared BF16 vision projector.

Assisted-by: Codex:gpt-5

* fix(tests): implement node liveness stub

NodeCommandSender now requires PingNode. The endpoint test stub must
implement it before the package can compile.

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

* fix(distributed): restore node liveness tests

The router now probes models.running before it schedules work. The E2E
workers only mocked backend.install, so every test node appeared offline.

The endpoint test double also missed the new PingNode method and stopped
the Linux, Apple, and lint jobs during compilation.

Mock the existing worker reply in both distributed fixtures and keep the
endpoint test double aligned with NodeCommandSender.

Assisted-by: Codex:gpt-5 [golangci-lint]

* fix(tests): check node liveness replies

The liveness test subscriptions ignored setup and reply errors.

Errcheck rejected each branch that carried them.

Assisted-by: Codex:gpt-5 [golangci-lint]

---------

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-24 09:33:10 +02:00
localai-org-maint-botandlocalai-org-maint-bot 7ff9d9942b fix(distributed): restore node liveness tests (#11694)
* fix(distributed): restore node liveness tests

The router now probes models.running before it schedules work. The E2E
workers only mocked backend.install, so every test node appeared offline.

The endpoint test double also missed the new PingNode method and stopped
the Linux, Apple, and lint jobs during compilation.

Mock the existing worker reply in both distributed fixtures and keep the
endpoint test double aligned with NodeCommandSender.

Assisted-by: Codex:gpt-5 [golangci-lint]

* fix(tests): check node liveness replies

The liveness test subscriptions ignored setup and reply errors.

Errcheck rejected each branch that carried them.

Assisted-by: Codex:gpt-5 [golangci-lint]

---------

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-24 09:32:48 +02:00
localai-org-maint-botandlocalai-org-maint-bot a8bc64cd09 fix(ci): bound Discord release summaries (#11695)
* fix(ci): bound Discord release summaries

The release model can return more than Discord's 2,000-character
message limit. Discord then rejects the entire release notification.

Ask the model for a smaller response and truncate extracted content to
1,800 characters before the notification step. The smaller bound leaves
room below Discord's hard limit when model output varies.

Assisted-by: Codex:gpt-5

* fix(tests): implement node liveness stub

NodeCommandSender now requires PingNode. The endpoint test stub must
implement it before the package can compile.

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

---------

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-24 09:32:26 +02:00
mudler's LocalAI [bot]andmudler 2f625becf6 chore(website): refresh the counters (#11697)
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-08-24 09:31:36 +02:00
Ettore Di Giacinto 5c9d8190d9 fix(distributed): resync revisions after the configs are loaded
The resync added in 3953448f6 ran before LoadModelConfigsFromPath, so it
read an empty loader, reconciled nothing and reported success. The
symptom was a stored revision that stayed stale across restarts while
the log showed no complaint, which is exactly what the resync was meant
to prevent.

Move the call after the configs are loaded, and refuse to treat an empty
loader as a clean run: reconciling zero models is indistinguishable from
reconciling correctly, and that is what hid the mis-ordered call.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude Code:claude-opus-5 [golangci-lint]
2026-08-24 06:40:30 +00:00
Ettore Di Giacinto 3953448f60 fix(distributed): resync stored config revisions at startup
The controller pins a model's replicas to a stored revision and rejects
any request carrying a different one. Nothing ever re-derived that value
from the configuration on disk: it moved only on an edit, a gallery
install, or a peer's change broadcast. An inference request may only
establish a revision, never replace one.

So any other way for the two to diverge left the model permanently
unroutable. A configuration edited while a frontend was down lands
there, and so does a change in what the revision is computed over: an
upgrade that alters the hashed form leaves every stored revision
describing a configuration that no longer exists. The only recovery was
deleting the row by hand, which is not something a cluster should need.

Each frontend now reconciles the stored revisions against the loaded
configurations at startup and republishes the ones that disagree. Only
those: republishing quarantines every replica loaded under the old
revision, so doing it for a model that did not drift would unload a
healthy replica for nothing. A model with no stored revision has never
been served and is left for its first request to establish.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude Code:claude-opus-5 [golangci-lint]
2026-08-23 22:17:59 +00:00
mudler's LocalAI [bot]andmudler eadc005b86 chore: ⬆️ Update antirez/ds4 to c1d4597a80e300b803dc642519718f2c999589da (#11685)
⬆️ 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-23 23:49:32 +02:00
mudler's LocalAI [bot]andmudler 7a78ec82eb chore(model-gallery): ⬆️ update checksum (#11690)
⬆️ 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-23 23:46:34 +02:00
Ettore Di Giacinto e6269e3cdd fix(distributed): reclaim replica slots held by abandoned loads
A replica row in staging or loading holds its slot, because slot
allocation counts every state except unloading. Nothing ever reclaimed
such a row: every reconciler pass and the router's eviction query filter
state = "loaded", and the per-model probe skips rows without an address,
which is exactly what a row that never finished loading has.

So a worker that dropped out mid-transfer left a row that pinned the
only replica slot for that model on that node. Scheduling then found no
free slot and eviction found nothing it was allowed to evict, and the
request failed with "no replica slot on <node> and eviction failed: all
models busy". The state persisted until an operator intervened.

The reconciler now reclaims a row stuck before serving when no load job
is driving it. Ownership is decided by the job's LastProgress heartbeat,
not by elapsed time: staging a large checkpoint legitimately runs for a
long while without touching the replica row, so a deadline would either
be a model-size cliff or reclaim a healthy transfer. That heartbeat is
the same signal job takeover already trusts. Any error reading the job
leaves the slot held, because holding one for another pass costs a
scheduling opportunity while a wrong reclaim restarts a multi-gigabyte
transfer.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude Code:claude-opus-5 [golangci-lint]
2026-08-23 21:07:29 +00:00
Ettore Di Giacinto c541dbeef4 fix(distributed): check a node answers before scheduling onto it
A node's status comes from its HTTP heartbeat. Backend installs travel
over NATS. The two are independent, so a worker that dies stops
answering on the bus at once but stays healthy in the database until its
heartbeat ages out. Inside that window the scheduler picked a node it
could not reach, and the request failed with "no responders available"
rather than moving to a node that was up.

The scheduler now probes the node it selected and, when nothing answers,
marks it unhealthy and selects again. The demotion is what makes the
retry terminate: the next selection reads only healthy nodes. It also
tells the other frontends what this one learned, so the cluster does not
rediscover a dead worker one failed request at a time.

Only nats.ErrNoResponders counts as absent. A worker that answers slowly
stays eligible, because dropping it would cost capacity that is really
there. The probe reuses the models.running subject: a new subject would
go unanswered by workers that have not been upgraded, and every one of
them would then look dead.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude Code:claude-opus-5 [golangci-lint]
2026-08-23 20:44:43 +00:00
Ettore Di Giacinto ac9969ef4d chore: bump go-processmanager to dfa0ed8
Picks up "surface option errors instead of discarding them". New records
the error from applying its options and Run returns it, so a process
whose state directory cannot be created reports the real cause instead
of failing later inside os.MkdirAll("") with an empty path.

LocalAI already resolves that directory itself, so this covers the other
process.New call sites rather than changing behaviour on the backend
start path.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude Code:claude-opus-5
2026-08-23 20:26:47 +00:00
Ettore Di Giacinto d40662cfd5 fix(model): report why a backend state directory fails
process.New applies its options and discards the error they return. When
WithTemporaryStateDir could not create a directory, StateDir stayed
empty and every later option went unapplied, so the failure surfaced
from Run as "mkdir : no such file or directory" naming no path.

That message cost a full day of diagnosis on a worker whose volume was
full: the real errno was ENOSPC and nothing reported it.

The loader now creates the directory itself and returns the underlying
error with the path attached.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude Code:claude-opus-5 [golangci-lint]
2026-08-23 20:20:02 +00:00
Ettore Di Giacinto cee87d1608 fix(distributed): expire staged request files on the worker
A request that carries a file stages it to the worker, which writes it
under its staging directory. Nothing removed it afterwards. The frontend
expires ephemeral keys from object storage, but that sweep never covered
a worker's local disk, so every image, audio clip and video a worker
ever served stayed on it.

One worker had accumulated 175 request directories over three months.
The volume reached 100 percent, and from that point every backend start
failed because the process manager could not create a state directory.

The worker now sweeps its ephemeral staging directory on a timer and
once at startup, so files left by a crash are reclaimed too. Staged
model files live beside that directory and are not touched.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude Code:claude-opus-5 [golangci-lint]
2026-08-23 20:20:02 +00:00
Ettore Di Giacinto 4bad644498 fix(distributed): name both revisions in the stale error
"stale model config revision" reported only that two hashes differed.
It named neither, so an operator could not tell an edited configuration
from a revision that is not reproducible for one unchanged file, and the
failing value appears in no table.

The error now carries the revision the request brought and the one the
controller holds. It still wraps ErrStaleModelConfigRevision, so callers
that classify the error keep working.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude Code:claude-opus-5 [golangci-lint]
2026-08-23 19:53:42 +00:00
localai-org-maint-botandlocalai-org-maint-bot eafc7fda27 feat(gallery): add Homura 30B (#11680)
Add the publisher\047s Q4_K_M build for the recent Muse Glimmer agent fine-tune.

Assisted-by: Codex:gpt-5

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-23 21:38:10 +02:00
localai-org-maint-botandlocalai-org-maint-bot b9914b56fb feat(gallery): add UI-Mate 27B variants (#11672)
Add Q4_K_M and Q8_0 builds for Tencent's Qwen3.6-based computer-use model.

Assisted-by: Codex:gpt-5.6 [Codex]

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-23 21:37:58 +02:00
localai-org-maint-botandlocalai-org-maint-bot 031eb5fc7c feat(gallery): add Qwen3.8 small distills (#11675)
Add the 2B and 4B distilled models alongside the 9B model. Their compact GGUF builds make the Qwen3.8 reasoning distill practical on smaller hosts.

Assisted-by: Codex:gpt-5

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-23 21:37:45 +02:00
localai-org-maint-botandlocalai-org-maint-bot 1313a4a5a2 feat(gallery): add LFM2.5 DSpark variants (#11676)
LiquidAI now publishes official DSpark draft sidecars for its 2.6B target. Pair Q4 and Q8 targets with matching draft choices so LocalAI can use speculative decoding across different memory budgets.

Assisted-by: Codex:gpt-5 [systematic-debugging]

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-23 21:37:28 +02:00
Ettore Di Giacinto f3fabe8c5c fix(distributed): order derived usecases deterministically
syncKnownUsecasesFromString rebuilds KnownUsecaseStrings by ranging
GetAllModelConfigUsecases, which is a map. Go randomizes that order per
call, and the field is part of the serialized config, so one unchanged
YAML hashed to a different config revision on every load.

A model that derives a single usecase hid the problem. One that derives
several, such as a chat model with an mmproj, alternated between as many
revisions as there are orderings. The router treats a revision it did
not establish as a config change, so requests failed with "stale model
config revision" until the stored value happened to match again.

Sorting the list makes the revision a function of the file alone.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude Code:claude-opus-5 [golangci-lint]
2026-08-23 17:20:20 +00:00
Ettore Di Giacinto 04735cd1f6 fix(distributed): stamp config revision at load time
The request middleware merges the caller's prediction parameters into
its copy of the model config. core/backend.ModelOptions then hashed
that copy, so the revision identified the request body rather than the
persisted configuration.

EstablishModelConfigRevision stores the first revision it sees and
requires an exact match afterwards. The first request after a restart
therefore pinned the model to its own temperature, top_p and stop
values, and every later request that sent different ones failed with
"stale model config revision". No config edit was involved.

The loader now stamps the revision when it materializes a config,
before any request override reaches it, and ModelOptions reads that
stamp. Model administration keeps hashing the same persisted config, so
both paths agree on one revision per configuration.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude Code:claude-opus-5 [golangci-lint]
2026-08-23 14:35:44 +00:00
localai-org-maint-botandlocalai-org-maint-bot 8f56e4e042 fix(vram): persist remote probe metadata (#11487)
* fix(vram): persist remote probe metadata

The startup warmer repeated remote size and GGUF metadata probes after every restart because both caches lived only in memory. Store successful HTTP probes for 24 hours so frequent restarts reuse the prior results.

Bound the cache, reject invalid records, and purge it when gallery data changes. Local model files continue to bypass persistence.

Assisted-by: Codex:gpt-5

* fix(vram): check temporary file cleanup

The lint gate rejects the unchecked cleanup call in the persistent cache writer.

Assisted-by: Codex:gpt-5.6 [golangci-lint]

* fix(vram): make persistent cache optional

Remote metadata probes can transfer enough data that operators need
control over disk reuse and startup warming. Gallery autoload now gates
both behaviors, and the runtime setting applies changes immediately.

Assisted-by: Codex:gpt-5

* fix(ui): expose gallery startup pre-warm

The existing gallery autoload setting also gates the startup metadata warmer. Name both effects in Settings so operators can find the requested boot control.

Assisted-by: Codex:gpt-5

---------

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-23 08:55:13 +02:00
mudler's LocalAI [bot]andmudler ffef539866 chore: ⬆️ Update ggml-org/whisper.cpp to 233fe1fc9b48a09e361d3594520838ca266537fe (#11648)
⬆️ 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-23 08:54:33 +02:00
mudler's LocalAI [bot]andmudler 1205f37457 chore: ⬆️ Update 0xShug0/audio.cpp to 4d383be1bff107e823ffc19120dcb6c78d493c0f (#11666)
⬆️ 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-23 08:54:13 +02:00
mudler's LocalAI [bot]andmudler 3f5467b4eb chore: ⬆️ Update CrispStrobe/CrispASR to 74bb374a8cc74284348d76a0a6e944180fbe6b07 (#11650)
⬆️ 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-23 08:53:57 +02:00
mudler's LocalAI [bot]andEttore Di Giacinto 82c191afad fix(distributed): keep model replicas config-consistent (#11664)
* docs: design configurable copy buffering

Document the context-aware copy buffer option and its validation plan.

Assisted-by: Codex:gpt-5

* docs: design durable distributed staging operations

Assisted-by: Codex:gpt-5

* docs: design distributed model config revisions

Assisted-by: Codex:GPT-5 [apply_patch] [exec_command]

* feat(config): add stable model revisions

Hash typed model configuration and effective protobuf options deterministically for distributed revision comparisons.

Assisted-by: Codex:GPT-5 [apply_patch] [exec_command]

* feat(worker): acknowledge exact model stops

Assisted-by: Codex:GPT-5 [apply_patch] [exec_command]

* feat(nodes): track model config revisions

Assisted-by: Codex:GPT-5 [apply_patch]

* fix(distributed): retry quarantined model cleanup

Stop quarantined replicas by exact process identity, retain failed cleanup as durable capped retries, and compare-and-delete only the claimed registry row. Process one sufficiently leased row at a time so multiple frontends cannot duplicate slow cleanup work.

Assisted-by: Codex:gpt-5

* fix(distributed): bind loads to config revisions

Assisted-by: Codex: GPT-5 [OpenAI Codex]

* fix(modeladmin): apply config revisions consistently

Route model edits, patches, state changes, deletion, and peer refreshes through the same revision lifecycle. Quarantine stale replicas before exact cleanup and report durable pending cleanup without failing successful config writes.

Assisted-by: Codex: GPT-5 [OpenAI Codex]

* feat(distributed): expose model config revision state

Document replica revision observability and durable cleanup behavior. Keep pending cleanup explicit in model mutation responses and verify endpoint contracts expose revision state without serialized load options.

Assisted-by: Codex:GPT-5 [OpenAI Codex]

* test(distributed): cover model revision convergence

Exercise cross-frontend quarantine, stale replay rejection, exact cleanup retry, worker re-registration, and current-generation replica convergence against the distributed PostgreSQL harness.

Assisted-by: Codex:gpt-5

* fix(distributed): pass config revision CI checks

Keep configured gallery sources out of authoritative runtime snapshots only after validating their real schema, and harden rollback snapshots against symlink races and non-regular files.

Assisted-by: Codex: GPT-5 [OpenAI Codex]

---------

Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
2026-08-22 22:44:03 +02:00
localai-org-maint-botandlocalai-org-maint-bot 6c7b45b32d fix(gallery): remove duplicate Qwopus entries (#11670)
The Qwopus variant pair was added twice, which makes the later pair unreachable and fails the gallery invariant on master.

Keep the first pair that LocalAI already resolves and remove the dead duplicate declarations.

Assisted-by: Codex:gpt-5

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-22 08:47:58 +02:00
mudler's LocalAI [bot]andmudler b013edab35 chore(model-gallery): ⬆️ update checksum (#11665)
⬆️ 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-22 00:51:59 +02:00
mudler's LocalAI [bot]andEttore Di Giacinto 9d92139de4 feat(ui): edit scheduling rules in place (#11667)
* docs(ui): design scheduling rule editing

Document the approved in-place rule editing flow and scalable node-label reference for the scheduling view.

Assisted-by: Codex:gpt-5

* feat(ui): improve scheduling rule management

Add scalable node-label discovery and editable scheduling rules with responsive, accessible controls.

Assisted-by: Codex:gpt-5

* chore(ui): ratchet inline style baseline

Record the static inline style removed by the scheduling view enhancement.

Assisted-by: Codex:gpt-5

---------

Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
2026-08-22 00:51:48 +02:00
localai-org-maint-botandlocalai-org-maint-bot 9feaa817f2 feat(gallery): add OpenResearcher variants (#11658)
Add the Q4_K_M and Q8_0 GGUF builds for the agentic deep-research model.

Assisted-by: Codex:gpt-5

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-21 21:21:13 +02:00
localai-org-botandlocalai-org-bot 3684a534bb docs(website): simplify installation paths (#11631)
Keep the homepage focused on runtime capabilities and move engine details to their canonical directory. Make installation choices stable and explicit for users across supported hardware.

Assisted-by: Codex:gpt-5

Co-authored-by: localai-org-bot <306113404+localai-org-bot@users.noreply.github.com>
2026-08-21 21:20:54 +02:00
mudler's LocalAI [bot]andEttore Di Giacinto a0252ad6a1 fix(distributed): keep staging operations stable (#11663)
* docs: design configurable copy buffering

Document the context-aware copy buffer option and its validation plan.

Assisted-by: Codex:gpt-5

* docs: design durable distributed staging operations

Assisted-by: Codex:gpt-5

* fix(distributed): merge durable staging operations

Use active model load jobs as the durable operations baseline and overlay replica-local staging progress without duplication. Preserve tracker-only operations when the registry cannot be read.

Assisted-by: Codex:gpt-5

---------

Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
2026-08-21 20:08:47 +02:00
mudler's LocalAI [bot]andEttore Di Giacinto 5429f569e0 fix(progress): stop status updates throttling downloads (#11661)
* feat(progress): aggregate and coalesce gallery downloads

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* feat(ui): show rolling transfer speed

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* fix(ui): preserve legacy import byte labels

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
2026-08-21 18:54:28 +02:00
localai-org-maint-botandlocalai-org-maint-bot 36ad21d1f5 feat(gallery): add Qwopus 3.6 variants (#11662)
Add the trending 27B Fusion merge in Q4_K_M and Q8_0 formats with its shared vision projector.

Assisted-by: Codex:gpt-5

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-21 18:53:07 +02:00
mudler's LocalAI [bot]andEttore Di Giacinto 5072219829 feat(xio): make copy buffer size configurable (#11660)
* docs: design configurable copy buffering

Document the context-aware copy buffer option and its validation plan.

Assisted-by: Codex:gpt-5

* feat(xio): configure context copy buffer size

---------

Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
2026-08-21 16:14:26 +02:00
mudler's LocalAI [bot]andmudler 5797ccb442 chore(model-gallery): ⬆️ update checksum (#11645)
⬆️ 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-21 12:50:01 +02:00
Anupam Mediratta 387db04a6e fix: upgrade github.com/containerd/containerd to 1.7.33 (CVE-2026-53488) (#11655)
fix: CVE-2026-53488 security vulnerability

Automated dependency upgrade by OrbisAI Security

Signed-off-by: anupamme <mediratta@gmail.com>
2026-08-21 10:20:55 +02:00
mudler's LocalAI [bot]andmudler 22ae9f5ba7 chore: ⬆️ Update 0xShug0/audio.cpp to 43001a7e0f452d80f4588e613f13332940dd4d3a (#11649)
⬆️ 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-21 08:47:02 +02:00
localai-org-maint-botandlocalai-org-maint-bot 5a890bf661 fix(gallery): tag MTP-enabled models (#11653)
The gallery ranker reads serving features from tags instead of backend options. Mark two entries that enable draft MTP so ranking can identify them correctly.

Assisted-by: Codex:gpt-5

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-21 08:46:47 +02:00
github-actions[bot]andmudler 7f2c599f4a chore: bump inference defaults from unsloth (#11654)
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-08-21 08:46:35 +02:00
Anupam Mediratta ff6043b811 fix: upgrade react-router to 7.18.2, 8.3.0 (GHSA-qwww-vcr4-c8h2) (#11644)
* fix: GHSA-qwww-vcr4-c8h2 security vulnerability

Automated dependency upgrade by OrbisAI Security

Signed-off-by: anupamme <mediratta@gmail.com>

* fix: upgrade react-router-dom to 7.18.2 to fully remediate GHSA-qwww-vcr4-c8h2

The prior fix pinned react-router@7.18.2 directly but left react-router-dom
at ^7.18.1, which bun resolved to 7.18.1. That package bundles its own
react-router@7.18.1 sub-dep, leaving the vulnerable version in bun.lock
via the react-router-dom/react-router scoped resolution. Pinning
react-router-dom to 7.18.2 and regenerating the lockfile removes all
7.18.1 resolutions.

Assisted-by: Claude Code:claude-sonnet-4-6
Signed-off-by: Anupam Mediratta <mediratta@gmail.com>

---------

Signed-off-by: anupamme <mediratta@gmail.com>
Signed-off-by: Anupam Mediratta <mediratta@gmail.com>
2026-08-21 08:41:23 +02:00
mudler's LocalAI [bot]andmudler 6d8667f93a chore: ⬆️ Update ggml-org/llama.cpp to d59d455fd8ea09e5a2e87ce2a9d668267ffb5ccd (#11618)
⬆️ Update ggml-org/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-20 22:59:48 +02:00
dependabot[bot] 3c899961e9 chore(deps): update charset-normalizer requirement from >=3.4.9 to >=3.5.1 in /backend/python/vllm (#11637)
chore(deps): update charset-normalizer requirement

Updates the requirements on [charset-normalizer](https://github.com/jawah/charset_normalizer) to permit the latest version.
- [Release notes](https://github.com/jawah/charset_normalizer/releases)
- [Changelog](https://github.com/jawah/charset_normalizer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/jawah/charset_normalizer/compare/3.4.9...3.5.1)

---
updated-dependencies:
- dependency-name: charset-normalizer
  dependency-version: 3.5.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-20 22:59:19 +02:00
Anupam Mediratta 9236be96bd fix: upgrade github.com/containerd/containerd to 1.7.32 (CVE-2026-46680) (#11634)
fix: CVE-2026-46680 security vulnerability

Automated dependency upgrade by OrbisAI Security

Signed-off-by: anupamme <mediratta@gmail.com>
2026-08-20 22:56:33 +02:00
mudler's LocalAI [bot]andmudler 80dd0fd076 docs: ⬆️ update docs version mudler/LocalAI (#11643)
⬆️ Update docs version mudler/LocalAI

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-20 22:55:36 +02:00
mudler's LocalAI [bot]andEttore Di Giacinto d2588b9177 docs(blog): add the 4.9 release post and its demo clips (#11629)
* docs(blog): add the 4.9 release post and its demo clips

The 4.9 cycle changed how you authenticate, how chat handles a history
that no longer fits, and where models and backends live in the UI. The
release notes list every pull request; this post covers the three
changes that alter day-to-day use, and leads with the auth one because
it needs an action before upgrading.

Two clips are recorded from a real session against a local-ai built
from master with the live gallery loaded: model-lifecycle.mp4 walks
the unified models and backends pages, import-model.mp4 shows the
rebuilt import form. Both follow the clip conventions in
.agents/preparing-a-release.md: h264, no audio track, 1000x562, under
30 seconds, and named after the feature so they stay reusable.

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

* docs(blog): anti-slop pass over the 4.9 post

Ran the post through the humanizer and no-ai-slop rules, calibrated
against what-landed-in-localai-4-8.md. That post is the one #11324
left unchanged, so it sets the rate for this series.

The draft ran denser than it on two constructions: "rather than" at
5.1 per thousand words against 3.5, and "instead of" at 3.1 against
1.6. Both are now at or below the 4.8 rate, 2.7 and 1.5, by rewriting
seven of them as plain statements.

Also cut: "keeping both cost a mode switch", a ledger metaphor for
something that is not money, which is the tell #11324 removed eight
times from the APEX post. "A follow-up fixed the thing that made that
awkward", an unearned framing plus a colon reveal. "This release adds
a different one: compress them", a second colon reveal. And
"byte-structurally identical", a second exactness idiom in a post that
already uses "byte-identical" where the precision carries weight.

Five paragraphs opened with "Two things" or "Two details", so three of
them start differently now. The summary listed three items, which is
the rule of three; it lists four, like the 4.8 summary.

Every figure, PR number, link and media reference is unchanged, checked
by diffing them out of both revisions. Hugo builds clean and the
rendered HTML has no em dashes.

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

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
2026-08-20 22:55:13 +02:00
Anupam Mediratta 9ba4bbf9bb fix: upgrade ip-address to 10.3.1 (CVE-2026-69192) (#11632)
fix: CVE-2026-69192 security vulnerability

Automated dependency upgrade by OrbisAI Security

Signed-off-by: anupamme <mediratta@gmail.com>
2026-08-20 20:04:11 +02:00
localai-org-maint-botandlocalai-org-maint-bot f7ad3f70eb feat(gallery): add Gemma 4 agentic variants (#11622)
Expose the upstream Q8_0 build and the MTP drafter for the existing Gemma 4 12B agentic entry. The variants let LocalAI select higher fidelity or faster speculative decoding from the same model family.

Assisted-by: Codex:gpt-5

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-20 12:53:54 +02:00
0ef6a5d7f1 chore: ⬆️ Update NVIDIA/NeMo-Speech.cpp to 4f9676226f667d14608487df744f375db87127f8 (#11417)
* ⬆️ Update NVIDIA/NeMo-Speech.cpp

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* fix(nemo-speech-cpp): build required sentencepiece

The updated runtime uses SentencePiece for RNNT context biasing even when Flashlight and text normalization are disabled. Build upstream's pinned static archive before configuring the engine so every backend variant has the newly required library and headers.

Assisted-by: Codex:gpt-5

* fix(nemo-speech-cpp): support BSD install

The pinned SentencePiece script uses GNU install -D for license files. macOS provides BSD install, which rejects that flag after the library compiles.

Create the license directory and use the portable install form on Darwin.

Assisted-by: Codex:gpt-5

* fix(nemo-speech-cpp): support CMake 4

Homebrew CMake 4 rejects the old policy floor in the pinned SentencePiece helper. Select the oldest supported policy floor for the Darwin dependency build.

Assisted-by: Codex:gpt-5.6 [Codex CLI]

---------

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-20 08:16:55 +00:00
mudler's LocalAI [bot]andmudler 98fd29c1e3 chore: ⬆️ Update CrispStrobe/CrispASR to a153b09b37c90cd55cd9336fccbdf3ba7a289596 (#11611)
⬆️ 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-20 09:16:17 +02:00
mudler's LocalAI [bot]andmudler 5b87f4adf7 chore(model-gallery): ⬆️ update checksum (#11615)
⬆️ 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-20 08:17:43 +02:00
mudler's LocalAI [bot]andmudler ea1d19064d chore: ⬆️ Update leejet/stable-diffusion.cpp to 97d2990807fe6d558e395f8764198d7c7e7b411c (#11616)
⬆️ 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-20 08:17:28 +02:00
mudler's LocalAI [bot]andmudler ea1a876b46 chore: ⬆️ Update 0xShug0/audio.cpp to a61da671b6a81c79071500954eea3c91c1a383dd (#11613)
⬆️ 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-20 08:17:13 +02:00
mudler's LocalAI [bot]andmudler 63d6993e0a chore: ⬆️ Update mudler/vllm.cpp to 438305e1577768ec0f75729456a4c8b9f425e2ee (#11612)
⬆️ 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-20 08:17:00 +02:00
localai-org-maint-botandlocalai-org-maint-bot 4ee667343c feat(gallery): add Ornith 1.5 9B variants (#11603)
Add the official Q4_K_M and Q8_0 GGUF builds with the shared BF16 vision projector.

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

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-19 20:49:55 +02:00
mudler's LocalAI [bot]andmudler 6fae75cb21 chore(model gallery): 🤖 add 1 new models via gallery agent (#11605)
chore(model gallery): 🤖 add new models via gallery agent

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-19 20:49:37 +02:00
mudler's LocalAI [bot]andEttore Di Giacinto 0ab632b6bd fix(auth): protect HTTP routes by default (#11602)
* fix(auth): default to protected HTTP routes

Use a method-aware registry for the small anonymous bootstrap surface.
Unknown routes now require credentials instead of inheriting fail-open
path classification.

Keep node self-service routes behind their registration-token middleware.
Global auth no longer rejects valid worker credentials first.

Assisted-by: Codex:gpt-5

* docs(auth): document public HTTP surface

Assisted-by: Codex:gpt-5

* test(auth): align route coverage with default denial

Assisted-by: Codex:gpt-5

---------

Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
2026-08-19 20:49:19 +02:00
localai-org-maint-botandlocalai-org-maint-bot 34e986de0a feat(gallery): add Carbon genomics variants (#11594)
Add the Carbon 3B and 8B genomic foundation models with Q4_K_M and Q8_0 GGUF builds.

Assisted-by: Codex:gpt-5

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-19 16:40:28 +02:00
localai-org-maint-botandlocalai-org-maint-bot 0df8de35e2 feat(gallery): add Hy-MT2 1.8B variants (#11599)
Add Tencent’s compact translation model in Q4 and Q8 GGUF formats. The linked variants let LocalAI select the best build that fits each host.

Assisted-by: Codex:gpt-5

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-19 16:40:10 +02:00
Ettore Di Giacinto 2383726d6d Revert "chore(tests): Avoid network, sleep and more during tests" (#11601)
Revert "chore(tests): Avoid network, sleep and more during tests (#11050)"

This reverts commit cb3bf7af3f.
2026-08-19 16:39:39 +02:00
localai-org-maint-botandlocalai-org-maint-bot 6d27f3b210 feat(gallery): add Ling 3.0 Flash variants (#11598)
Add IQ1_M and IQ2_XS GGUF builds for the 124B hybrid reasoning model.

Assisted-by: Codex:GPT-5 [Codex CLI] [curl]

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-19 12:43:15 +02:00
Richard Palethorpeandlocalai-org-maint-bot cb3bf7af3f chore(tests): Avoid network, sleep and more during tests (#11050)
* test: make coverage failures observable

Keep per-root logs, reject concurrent coverage runs, and avoid relying on /bin/sleep in the worker timeout test.

Assisted-by: Codex:gpt-5
Signed-off-by: Richard Palethorpe <io@richiejp.com>

* test: parallelize coverage without remote fixtures

Assisted-by: Codex:gpt-5 [apply_patch] [exec_command]
Signed-off-by: Richard Palethorpe <io@richiejp.com>

* test: add offline resource infrastructure

Introduce versioned resource manifests, a checksum-verified CAS preparer, offline test wrappers, and a guarded network transport. Replace live Hugging Face, GitHub, and OCI cases with deterministic fixtures and inject fixture metadata into importer discovery.

Assisted-by: Codex:gpt-5
Signed-off-by: Richard Palethorpe <io@richiejp.com>

* test: enforce offline resource replay

Assisted-by: Codex:gpt-5
Signed-off-by: Richard Palethorpe <io@richiejp.com>

* test: harden offline resource refresh

Assisted-by: Codex:gpt-5
Signed-off-by: Richard Palethorpe <io@richiejp.com>

* test: expose slow coverage waits

Assisted-by: Codex:gpt-5
Signed-off-by: Richard Palethorpe <io@richiejp.com>

* test: eliminate avoidable wall-clock waits

Inject a clock into Hugging Face retry handling, reuse a process-scoped PostgreSQL container with per-spec schemas in the nodes suite, and poll local import jobs promptly.

Assisted-by: Codex:gpt-5
Signed-off-by: Richard Palethorpe <io@richiejp.com>

* test: remove repeated fixture startup waits

Share PostgreSQL fixtures across parallel endpoint and agent suite workers, and make the worker Free deadline injectable so the wedged-backend test does not spend five seconds on wall-clock time.

Assisted-by: Codex:gpt-5
Signed-off-by: Richard Palethorpe <io@richiejp.com>

* test: fix offline resource CI portability

Normalize Docker archive metadata before content addressing, derive archive checksums during explicit refreshes, make network lint portable to macOS, and prepare distributed images before running their offline suite.

Assisted-by: Codex:gpt-5
Signed-off-by: Richard Palethorpe <io@richiejp.com>

* ci: cache Go modules before offline tests

Warm the complete module graph before the Linux and macOS test jobs enter offline replay mode, so tool dependencies such as Ginkgo are not fetched through the guarded proxy.

Assisted-by: Codex:gpt-5
Signed-off-by: Richard Palethorpe <io@richiejp.com>

* test: drop the static network lint in favour of real isolation

The offline test suite already prevents tests from reaching the network
twice over: run-test-linux-offline.sh puts the test process in a cgroup
and REJECTs egress outside the private ranges, and HardenedTransport
installs testnetwork.LocalGuard to refuse dials that resolve to a public
address. Both fail the test with a precise error at the moment of the
dial.

test-network-lint.sh added neither. Its diff stage defaulted to a HEAD
base, so on a clean checkout it compared the tree against itself and
inspected nothing; the branch's own commits were never examined. It only
produced output when an earlier job step dirtied the tree, and then it
matched a bare https?:// against whatever changed. make react-ui runs
npm install rather than npm ci, so CI rewrote
core/http/react-ui/package-lock.json and the lint reported an npm
registry URL as forbidden test network access:

  +      "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.25.tgz",

Its fingerprint stage was self-defeating in a quieter way: hashing the
whole tree's network-mechanism inventory meant every rebase onto a master
that touched any _test.go needed a manual baseline bump, so the check
mostly caught its own staleness.

Remove the script, its make target and the two prerequisite edges, along
with the test-network: fixture markers that existed only to suppress it.
The isolation itself is untouched.

Assisted-by: Claude:claude-opus-5 [go vet]
Signed-off-by: Richard Palethorpe <io@richiejp.com>

* ci: keep hidden files in the offline test bundle artifact

Cherry-picked from 15a37b0ac on the remote branch. The offline bundle lives
under .cache/, which actions/upload-artifact skips by default, so the Linux
job packed an artifact missing the very file the next step restores.

The other half of 15a37b0ac moved test-network-lint out of the `test` and
`test-coverage` prerequisite lists into a recipe line, so parallel make could
not fingerprint the tree while generated fixtures were still changing. That
is dropped: the preceding commit removes the lint entirely, and the race it
worked around is one more reason a whole-tree fingerprint was the wrong
mechanism.

Assisted-by: Codex:gpt-5
Signed-off-by: Richard Palethorpe <io@richiejp.com>

* refactor: share bounded exponential backoff

Use overflow-safe saturating arithmetic for retry delays across model import polling, downloads, registration, node operations, and model loading. Keep model import status checks responsive initially while capping their interval at 500ms.

Assisted-by: Codex:gpt-5
Signed-off-by: Richard Palethorpe <io@richiejp.com>

* ci: mirror Jetson Python wheels

Keep the CUDA aarch64 wheel subset in GHCR and serve it as a local PEP 503 index during L4T backend builds, preserving last-known-good packages through upstream outages.

Assisted-by: Codex:gpt-5
Signed-off-by: Richard Palethorpe <io@richiejp.com>

* docs(agents): index the Jetson wheels mirror

Mention the GHCR-hosted L4T wheel mirror in the CI caching guide summary so maintainers can find its outage and cache documentation.

Assisted-by: Codex:gpt-5
Signed-off-by: Richard Palethorpe <io@richiejp.com>

* ci: add defensive build network proxy

Record build destinations and byte counts, retry observable idempotent HTTP downloads, and isolate explorer database tests that race under coverage.

Assisted-by: Codex:gpt-5
Signed-off-by: Richard Palethorpe <io@richiejp.com>

* fix(kokoros): implement updated backend trait

Return unimplemented for image upscaling, matching the backend's other unsupported modalities after the protobuf API update.

Assisted-by: Codex:gpt-5
Signed-off-by: Richard Palethorpe <io@richiejp.com>

* fix(ci): clear recovered proxy errors

Do not mark a request failed when a later safe retry succeeds.

Assisted-by: Codex:gpt-5
Signed-off-by: Richard Palethorpe <io@richiejp.com>

* ci: require HTTPS build interception

Inject a short-lived proxy CA into BuildKit and Dockerfile RUN steps, reject plain HTTP and opaque tunnels, and retain method/status/byte telemetry for verified HTTPS traffic.

Assisted-by: Codex:gpt-5
Signed-off-by: Richard Palethorpe <io@richiejp.com>

* fix(ci): preserve system trust in unproxied builds

Mount the generated interception CA at a dedicated secret path and add it to the trust bundle only in proxy-aware dependency stages. This prevents optional secret mounts from masking the system CA bundle in ordinary backend test builds.

Install the requested Go toolchain before starting the proxy and satisfy cleanup error checks found by CI lint.

Assisted-by: Codex:gpt-5
Signed-off-by: Richard Palethorpe <io@richiejp.com>

* fix(ci): persist build proxy trust

Install the generated proxy CA through the system-managed local certificate directory so ca-certificates upgrades retain it. Avoid turning canceled matrix jobs into proxy cleanup failures.

Assisted-by: Codex:gpt-5

Signed-off-by: Richard Palethorpe <io@richiejp.com>

* fix(ci): trust proxy in nested build scripts

Install the build proxy CA before nested source fetches, route the DS4 package setup through the HTTPS mirror helper, and avoid repeated OCI setup in gallery behavior tests.

Assisted-by: Codex:gpt-5

Signed-off-by: Richard Palethorpe <io@richiejp.com>

* fix(ci): use HTTPS apt sources for Bonsai

Rewrite ARM64 package sources before installing GCC and check gallery fixture cleanup errors so the optimized tests satisfy errcheck.

Assisted-by: Codex:gpt-5

Signed-off-by: Richard Palethorpe <io@richiejp.com>

* fix(privacy-filter): trust build proxy CA

Install the mounted build proxy certificate before privacy-filter's make target fetches its HTTPS sources, for both source and prebuilt builder paths.\n\nAssisted-by: Codex:gpt-5

Signed-off-by: Richard Palethorpe <io@richiejp.com>

* test: fail on hidden offline egress

Count cgroup-scoped firewall rejects and fail the offline test harness with bounded aggregate diagnostics. Inject the gen-audio GGUF probe so fixture-backed importer tests do not attempt real network access.

Assisted-by: Codex:gpt-5
Signed-off-by: Richard Palethorpe <io@richiejp.com>

* fix(ci): preserve system CA trust

Build a combined runner certificate bundle instead of replacing public roots with the generated proxy CA. Centralize additive container installation in the shared proxy CA helper.

Assisted-by: Codex:gpt-5
Signed-off-by: Richard Palethorpe <io@richiejp.com>

---------

Signed-off-by: Richard Palethorpe <io@richiejp.com>
Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-19 10:59:31 +02:00
localai-org-maint-botandlocalai-org-maint-bot de3329e332 feat(gallery): add UI-Mate 9B variants (#11584)
Add Q4_K_M and Q8_0 GGUF builds for Tencent UI-Mate-9B. Include the f16 vision projector so llama.cpp can process desktop screenshots.

Assisted-by: Codex:gpt-5

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-19 08:48:35 +02:00
localai-org-maint-botandlocalai-org-maint-bot aa7d016a48 feat(gallery): add North Mini Code variants (#11559)
Add the supported Q4_K_M and Q8_0 GGUF builds for Cohere Labs' 30B-A3B coding model.

Assisted-by: Codex:gpt-5

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-19 08:48:24 +02:00
mudler's LocalAI [bot]andmudler 89d1cda0fc chore(model gallery): 🤖 add 1 new models via gallery agent (#11596)
chore(model gallery): 🤖 add new models via gallery agent

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-19 08:46:12 +02:00
localai-org-maint-botandlocalai-org-maint-bot 4cad809003 fix(ci): test stale chunks in split bundle (#11595)
The V8 coverage build inlines every dynamic import, so the stale chunk tests cannot intercept a page chunk. Run those tests against the normal code-split bundle and exclude them from the inlined coverage pass.

Assisted-by: Codex:gpt-5

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-19 08:45:59 +02:00
mudler's LocalAI [bot]andmudler ea60f62d66 chore: ⬆️ Update mudler/parakeet.cpp to e75de9b6b9b688fd293aa22f7e27aa724ea286f8 (#11592)
⬆️ Update mudler/parakeet.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-19 08:45:27 +02:00
mudler's LocalAI [bot]andmudler 25cfc7c3c9 chore: ⬆️ Update CrispStrobe/CrispASR to a117d433855ecc36b0cb20bd1abddd852902ed13 (#11591)
⬆️ 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-19 08:45:07 +02:00
mudler's LocalAI [bot]andmudler a57fce10c5 chore: ⬆️ Update ggml-org/llama.cpp to 60addddf3c567c43ec3caf70fc953fba3572d96f (#11590)
⬆️ Update ggml-org/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-19 08:44:55 +02:00
mudler's LocalAI [bot]andmudler 2ba3265c67 chore: ⬆️ Update mudler/vllm.cpp to 6e99e4540b8d0811542a9d7690dc9aa0bda5812f (#11589)
⬆️ 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-19 08:44:44 +02:00
mudler's LocalAI [bot]andmudler 61f02f7504 chore: ⬆️ Update ggml-org/whisper.cpp to 4834a2327d008ace3ec5a9ed00f51454bcabbc1c (#11588)
⬆️ 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-19 08:44:32 +02:00
mudler's LocalAI [bot]andmudler 59001291e1 chore: ⬆️ Update 0xShug0/audio.cpp to 92816fc5c48d33a58171b49678ccf884d70142fb (#11587)
⬆️ 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-19 08:44:13 +02:00
mudler's LocalAI [bot]andmudler 77ff6889ec chore: ⬆️ Update vllm-metal (darwin) to v0.3.0.dev20260818075955 (#11586)
⬆️ Update vllm-project/vllm-metal (darwin)

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-19 08:39:14 +02:00
mudler's LocalAI [bot]andmudler 08786395be feat(swagger): update swagger (#11585)
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-19 08:37:21 +02:00
Ettore Di Giacinto b806b1fec3 fix(ui): reload once when a page chunk 404s
A deploy replaces the whole content-hashed asset set at once. A tab
holding an older index.html, or one whose request lands on a replica
that the rollout has not swapped yet, asks for a page chunk the server
no longer has. The dynamic import rejects and React Router's default
error boundary replaces the app with "Unexpected Application Error!"
until someone reloads by hand.

The router now reloads the page itself when a chunk fails to load.
index.html is served no-cache, so the reload lands on a self-consistent
asset set. A timestamp in sessionStorage bounds this to one reload per
10 seconds, so a chunk that is genuinely gone reaches the error boundary
instead of looping forever.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude Code:claude-opus-5[1m]
2026-08-18 19:51:15 +00:00
Stefan Walcz 178ac66d77 docs(ecosystem): add Walcz to contributors (#11582)
Nine commits in this repository, all merged: native Prometheus metrics for agent
chat runs (#10689), PII/audit events as a Prometheus counter (#10641), optional
Anthropic prompt-cache breakpoints (#11158), plus fixes to cloud-proxy, the agent
UI and the Python backend.

The author's public GitHub profile names the employer, which is the evidence this
file asks for. Counts verified with the GitHub commit search API on 2026-08-18.

Placed by commit count, between Spectro Cloud and VictoriaMetrics.

Signed-off-by: Stefan Walcz <stefan.walcz@walcz.de>
2026-08-18 17:55:58 +02:00
Stefan Walcz 906868992e docs(adopters): add walcz.de (#11581)
We run LocalAI as the inference layer of a self-hosted appliance for a German
B2B consultancy — local-only, on AMD Strix Halo (gfx1151) with ROCm.

In daily production use for agents with MCP tools, RAG over an internal
knowledge base, and a document/bookkeeping pipeline.

Signed-off-by: Stefan Walcz <stefan.walcz@walcz.de>
2026-08-18 17:55:35 +02:00
localai-org-maint-botandlocalai-org-maint-bot 0761bd02c7 feat(chat): add end-to-end context compression (#11556)
* feat(config): add context compression policy

Define the opt-in model configuration contract before the chat middleware consumes it. Document each policy field so later request handling does not invent a second schema.\n\nRefs #9534\n\nAssisted-by: Codex:gpt-5

* fix(config): register compression fields

The model editor metadata gate rejects new config fields without descriptions and suitable controls. Register the compression policy so operators can edit its six fields safely.

Assisted-by: Codex:gpt-5 [monitoring-prs]

* feat(chat): compress long contexts

Long conversations currently fail once they reach the model context window. The opt-in policy now summarizes complete older turns before primary inference and preserves the newest tool chains.

Both OpenAI and MCP chat routes share the same transformation. Usage metadata and metrics expose each compression event.

Refs #9534

Assisted-by: Codex:gpt-5

---------

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-18 11:31:03 +00:00
Nicholas Ciechanowski cf93c04878 fix: tts text wrap (#11576)
Signed-off-by: Nicholas Ciechanowski <nicholas@ciech.anow.ski>
2026-08-18 12:57:55 +02:00
4058194b18 chore: ⬆️ Update 0xShug0/audio.cpp to 3407559d7c83b543bd60ca8fa2b630374e71083f (#11474)
* ⬆️ Update 0xShug0/audio.cpp

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* fix(audio-cpp): map the MIDI task

audio.cpp now appends MIDI to its task enum. Keep the LocalAI mirror and conversion switches aligned so the backend builds against the new pin.

Assisted-by: Codex:gpt-5

---------

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-18 09:46:56 +02:00
6d4c64c6c3 chore: ⬆️ Update mudler/vllm.cpp to 76f2a6d84e41cc9a593146a9a958f63645542731 (#11555)
* ⬆️ Update mudler/vllm.cpp

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* fix(vllm-cpp): track ABI v21

The updated engine exposes ABI v21 after extending its speech API. The Go
backend does not bind that API, so its existing mirrors remain valid.

The qwen3.5 warning fix is now present upstream, so the old patch no
longer applies.

Assisted-by: Codex:gpt-5

---------

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-18 09:46:15 +02:00
mudler's LocalAI [bot]andlocalai-org-maint-bot 4c4911fe2c chore: ⬆️ Update ggml-org/llama.cpp to 0021a77de0a8966059dc94548fb3b96654e0bb12 (#11508)
* chore(llama-cpp): update upstream revision

Assisted-by: Codex:gpt-5.6

* fix(llama-cpp): refresh server patch contexts

The new llama.cpp pin changed the slot reset and prompt batch code. GNU patch accepted stale hunks with fuzz, which left the L4T build with invalid source.

Refresh both server patches against the pinned source so each hunk applies at its intended location.

Assisted-by: Codex:gpt-5

* fix(llama-cpp): adapt metrics result fields

The updated llama.cpp groups cumulative counters under server_metrics. Probe the result layout so the shared adapter also compiles against older forks.

Assisted-by: Codex:gpt-5

* fix(llama-cpp): refresh TTS patch offsets

GNU patch rejects the stale pre-decode hunk after the score patch changes the same file. Anchor the TTS hunks to the pinned llama.cpp source so the full series applies without fuzz.

Assisted-by: Codex:gpt-5.4

* fix(llama-cpp): normalize batch threads

The updated llama.cpp creates its batch threadpool during model initialization, before the context-level fallback can replace the -1 sentinel. Resolve that sentinel from the inference thread count so model loading does not overflow the threadpool allocation.\n\nAssisted-by: Codex:gpt-5.4

---------

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-18 09:45:56 +02:00
Richard Palethorpe d10374f849 feat(router): make KNN a first-class classifier with a persisted, curated corpus (#10652)
* feat(router): make KNN a first-class classifier with a persisted, curated corpus

Add `classifier: knn` — similarity-weighted voting over labelled
example prompts. Unlike score/colbert it needs no classifier model:
label knowledge lives in a corpus seeded and curated through the
admin API, so routing decisions are deterministic, auditable, and
grounded in graded experience rather than a model's opinion.

Epistemic gate: corpus entries below knn.similarity_threshold cannot
vote; when none clears it the classifier activates no labels and the
router uses the fallback — a prompt unlike all labelled experience is
treated as undecidable, not guessed. Decisions record
nearest_similarity (also on fallback rows) so admins can see how far
the nearest labelled experience was; the Routing tab explains
out-of-corpus fallbacks and shows per-label corpus counts.

Persistence: one JSONL file per router under
<data path>/router-corpus (text, labels, vector, embedder
fingerprint). The file is the source of truth; the local-store index
is rebuilt from it at classifier build time and stays a pure
in-memory index. Entries recorded under a different embedding model
re-embed on load. Also corrects the docs' false claim that
local-store collections persist — the embedding cache never survived
restarts (and still doesn't); the corpus does.

Corpus input is API-only by design (entries may contain example user
content): POST /api/router/{name}/corpus seeds (labels validated
against declared policies, embedded server-side, indexed
immediately), GET .../corpus/stats inspects — label counts only,
entry texts are never returned by any surface — DELETE .../corpus
wipes. Admin-gated like the sibling router endpoints, and exposed as
MCP tools (seed_router_corpus / get_router_corpus_stats /
clear_router_corpus) in both the httpapi and inproc clients with
coverage-test route mappings.

Plumbing: VectorStore gains SearchK (top-K was hardcoded to 1);
local-store gets InsertBatch/Delete as optional fast paths;
RouterConfig gains a knn block (embedding_model, k,
similarity_threshold, vote_threshold, store_name) with meta-registry
fields; the classifier dropdown now offers knn and the
previously-missing colbert; embedding_cache is ignored (with a
warning) for knn — it IS an embedding-KNN lookup; the stale
/api/instructions intelligent-routing entry is rewritten (it
described a classifier that no longer exists); swagger regenerated.

Tests: KNN vote/gate specs with hand-computed vote shares, corpus
manager suite (restart reload without re-embedding, fingerprint
re-embed, dedupe, hostile store names), middleware specs (corpus
routing, gate fallback, config validation, cache-wrap refusal),
corpus endpoint specs pinning the texts-never-returned contract, MCP
catalog + route-mapping gates, and a Playwright spec for corpus
stats and the out-of-corpus decision detail.

Assisted-by: Claude:claude-fable-5 [Claude Code]
Signed-off-by: Richard Palethorpe <io@richiejp.com>

* feat(router): name consulted corpus neighbours in knn decisions

Every knn decision (decision log rows and the /api/router/decide
response) now carries neighbors: the K retrieved corpus entries by
descending similarity - including ones below the epistemic gate, which
is what makes fallback decisions diagnosable - each as {id, similarity,
labels}. The id is the entry's content hash (first 8 bytes of the
SHA-256 of its text, hex): stable across reseeds and re-embeds, and
text-free, so an external platform that seeded the corpus can recompute
text->id on its own copy and bucket decisions by corpus region (per-
region reliability accounting) without corpus text ever leaving the
server. A corrupt index payload surfaces as an id-less neighbour at a
real similarity instead of disappearing.

Assisted-by: Claude:claude-fable-5 [Claude Code]
Signed-off-by: Richard Palethorpe <io@richiejp.com>

* refactor(router): deduplicate knn plumbing and cut corpus hot-path waste

Post-review cleanup of the knn-first-class-router branch; no behaviour
changes on the API surface.

Reuse/altitude:
- RouterKNNConfig.ResolvedStoreName is now the single source of the
  router-corpus-<name> default (was hand-derived in four files).
- corpus.ResolveKNNRouter + corpus.Seed carry the shared model
  resolution and seed validation; the REST endpoints and the assistant
  MCP client are thin transport adapters over them, with sentinel
  errors mapped to HTTP statuses at the echo boundary.
- middleware.NewClassifierDeps assembles the classifier dependency set
  once for all five entry points (OpenAI, Anthropic, realtime, decide,
  corpus) instead of five hand-copied literals.
- router.AllClassifiers feeds both the status endpoint and the
  unknown-classifier error, ending the classifier-list drift.
- Per-classifier requirements moved out of validateRouterPolicies into
  their buildClassifier arms; the knn arm owns its embedding_cache
  opt-out instead of a name-check in the shared wrap tail.
- adminOnly replaces four inline copies of the admin gate in the
  middleware routes.
- localVectorStore.Search delegates to SearchK (identical traces).

Efficiency:
- Manager.Add embeds outside the manager mutex and appends to the
  JSONL file (O(new) instead of O(corpus) rewrite); a torn tail from a
  crash mid-append is tolerated on read and repaired on next write.
- Stats memoises per store keyed on the file's stat fingerprint and no
  longer takes the manager mutex, so the 5s status poll stops parsing
  vector-laden JSONL and stops blocking behind seeds.
- KNN Classify decodes each neighbour payload once (was twice) and
  builds refs and votes in a single pass with one fallback return.
- Corpus file writes fsync before rename/close.
- The corpus manager is built eagerly in newApplication (sync.Once
  dropped); test helper dead branch removed.

Assisted-by: Claude:claude-fable-5 [Claude Code]
Signed-off-by: Richard Palethorpe <io@richiejp.com>

* feat(router): bind knn corpus vectors to an embedder fingerprint and fail closed on mismatch

Assisted-by: Claude:claude-fable-5 [Claude Code]
Signed-off-by: Richard Palethorpe <io@richiejp.com>

* chore(mcp): align corpus tool prompts and the mutating-tool safety list

Assisted-by: Claude:claude-fable-5 [Claude Code]
Signed-off-by: Richard Palethorpe <io@richiejp.com>

* feat(proto,backend): report embedding shape from the llama-cpp backend

Assisted-by: Claude:claude-fable-5 [Claude Code]
Signed-off-by: Richard Palethorpe <io@richiejp.com>

* feat(embeddings): Go-side pooling — mean/last/decayed_mean with half-life

Assisted-by: Claude:claude-fable-5 [Claude Code]
Signed-off-by: Richard Palethorpe <io@richiejp.com>

* feat(embeddings): accept chat messages[] and per-request pooling on /v1/embeddings

Assisted-by: Claude:claude-fable-5 [Claude Code]
Signed-off-by: Richard Palethorpe <io@richiejp.com>

* chore(middleware): name the failing fields when post-merge validation 400s

An intermittent post-merge validation failure surfaced as an opaque 400
during integration (pooling scheme mismatch that no client had sent).
Log the model, the request's pooling override, and the merged config's
pooling fields at the failure point so the next occurrence identifies
whether the request or the stored config carried the bad value.

Assisted-by: Claude:claude-fable-5 [Claude Code]
Signed-off-by: Richard Palethorpe <io@richiejp.com>

* fix(embeddings): scheme override must not inherit the config's half-life

A model config defaulting to decayed_mean pooling carries
pooling_half_life_tokens; a request overriding the scheme to mean/last
without its own half-life inherited that value, and post-merge
validation rejected the pair the server itself had assembled. Zero the
inherited half-life when the overridden scheme is not decayed_mean; a
request that explicitly pairs a half-life with a non-decayed scheme
still 400s.

Assisted-by: Claude:claude-fable-5 [Claude Code]
Signed-off-by: Richard Palethorpe <io@richiejp.com>

* fix embedding pooling validation and router bounds

Declare backend embedding layouts and reject incompatible pooling modes. Reset local-store dimensions after a full clear, validate KNN thresholds, and add real backend and store integration coverage.

Assisted-by: Codex:gpt-5
Signed-off-by: Richard Palethorpe <io@richiejp.com>

* ci: run local-store integration tests

Build and install the local-store backend in the Linux test job, then run the existing store integration suite so new specs are discovered automatically.

Assisted-by: Codex:gpt-5
Signed-off-by: Richard Palethorpe <io@richiejp.com>

---------

Signed-off-by: Richard Palethorpe <io@richiejp.com>
2026-08-18 09:37:43 +02:00
localai-org-maint-botandlocalai-org-maint-bot 10b4a8a5ae feat(gallery): add MiniCPM5 1B Q8 variant (#11561)
The official GGUF repository publishes both Q4_K_M and Q8_0 builds, but the gallery only exposed Q4_K_M. Link the higher-quality Q8_0 build so capable hosts can select it automatically.

Assisted-by: Codex:gpt-5

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-18 09:04:03 +02:00
localai-org-maint-botandlocalai-org-maint-bot a7bce6a128 fix(audio): reject incompatible transform streams (#11565)
The transform WebSocket accepted any model and opened its frame-based RPC. Any-to-any models use a different stream contract, so liquid-audio failed with an unimplemented RPC after the handshake.

Reject incompatible model use cases before loading the backend. Direct realtime-audio callers to the OpenAI Realtime API.

Assisted-by: Codex:gpt-5

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-18 09:03:16 +02:00
localai-org-maint-botandlocalai-org-maint-bot 411fdf10c3 fix(fish-speech): preserve ROCm PyTorch (#11568)
The upstream editable install pins generic PyTorch packages. It
replaces the HIP wheels with CUDA wheels in ROCm images.

Remove those pins only for hipBLAS builds before the editable install.
Keep the existing CPU and CUDA dependency behavior unchanged.

Assisted-by: Codex:gpt-5

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-18 08:58:38 +02:00
Richard Palethorpe 799cc9f211 feat: bound global admission and expose running backend traces (#11560)
feat: bound backend admission and expose running traces

Add process-wide backend execution admission without blocking UI or administrative HTTP work. Represent backend operations while they are in flight, surface running traces with immediate log links, and tie streaming admission leases to the gRPC receive lifecycle.

Assisted-by: OpenAI Codex: GPT-5

Signed-off-by: Richard Palethorpe <io@richiejp.com>
2026-08-18 08:56:59 +02:00
mudler's LocalAI [bot]andmudler 2ae7b45940 chore: ⬆️ Update CrispStrobe/CrispASR to 007fda47128aaba7e2ce2279b0947a1c13800d68 (#11569)
⬆️ 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-18 08:55:42 +02:00
mudler's LocalAI [bot]andmudler a755608cff chore: ⬆️ Update vllm-metal (darwin) to v0.3.0.dev20260817081527 (#11570)
⬆️ Update vllm-project/vllm-metal (darwin)

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-18 08:54:51 +02:00
localai-org-maint-botandlocalai-org-maint-bot b858f334fa feat(gallery): add Qwen3.8 small variants (#11571)
Add the 2B and 4B reasoning distillations in Q4_K_M and Q8_0 formats. These sizes extend the existing Qwen3.8 family to compact and edge hosts.

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

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-18 08:54:41 +02:00
localai-org-maint-botandlocalai-org-maint-bot b5258f5d46 fix(ci): refresh site counters through a PR (#11572)
Branch protection rejects the weekly workflow's direct push to master. Reuse the repository's create-pull-request automation so counter updates go through the protected-branch review and CI path.

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

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-18 08:54:30 +02:00
localai-org-maint-botandlocalai-org-maint-bot 4c740a0c65 feat(gallery): add Qwen3.8 Ridge variant (#11573)
Add the smaller 3.69-bit mixed quantization to the existing Qwen3.8 27B variant group. Enable its embedded MTP head so compatible hosts can prefer speculative decoding.

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

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-18 08:54:12 +02:00
localai-org-maint-botandlocalai-org-maint-bot c29c99e120 feat(gallery): add Ornith 1.0 9B variants (#11552)
Add Q4_K_M and Q8_0 llama.cpp builds with the shared vision projector. The MIT-licensed agentic coding model is absent from the current gallery.

Assisted-by: Codex:gpt-5

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-17 08:57:29 +02:00
localai-org-maint-botandlocalai-org-maint-bot 2fa431bf6f feat(gallery): add Qwen3.8 9B variants (#11557)
Add Q4_K_M and Q8_0 llama.cpp builds for the text-only Qwen3.8 9B distillation.

Assisted-by: Codex:gpt-5

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-17 08:57:13 +02:00
mudler's LocalAI [bot]andmudler 8d8ea91fbf chore: ⬆️ Update CrispStrobe/CrispASR to 3721d402f7bcc911dd4143a58e3da1cc67f09cc2 (#11554)
⬆️ 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-17 08:57:01 +02:00
mudler's LocalAI [bot]andmudler b6d7046a51 chore: ⬆️ Update vllm-metal (darwin) to v0.3.0.dev20260816085229 (#11553)
⬆️ Update vllm-project/vllm-metal (darwin)

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-17 08:51:04 +02:00
869b30d18f chore: ⬆️ Update mudler/vllm.cpp to 4880c5715f36445a30bd39d3349a06dc96085a11 (#11515)
* ⬆️ Update mudler/vllm.cpp

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* fix(vllm-cpp): mirror ABI v20 layouts

The dependency bump advances the engine ABI from v17 to v20. The old
binding stops every backend build at the ABI guard and undersizes
structures used at runtime.

Mirror the appended model and video fields so every platform uses the
pinned engine layout.

Assisted-by: Codex:gpt-5

* fix(vllm-cpp): satisfy Apple Clang

The new engine pin captures a namespace-scope help string in a lambda. Apple Clang rejects the redundant capture because upstream enables -Werror.

Carry the one-line source patch until the engine pin includes the fix.

Assisted-by: Codex:gpt-5

* fix(vllm-cpp): align the carry patch

The Apple Clang patch used context from another source revision.
Source preparation rejected it before every backend build.

Align the patch with the pinned engine revision.

Assisted-by: Codex:gpt-5 [monitoring-prs]

---------

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-16 21:45:49 +02:00
localai-org-maint-botandlocalai-org-maint-bot dcb8b278a5 feat(gallery): add OvisOCR2 variants (#11549)
Add Q4_K_M and Q8_0 llama.cpp builds with the required F16 vision projector.

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

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-16 21:45:05 +02:00
localai-org-maint-botandlocalai-org-maint-bot 069204e0e7 feat(gallery): add AREX Turbo variants (#11551)
Add Q4_K_M and Q8_0 llama.cpp builds for BAAI AREX-Turbo. The compact research agent is absent from the current gallery.

Assisted-by: Codex:gpt-5

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-16 21:44:53 +02:00
localai-org-maint-botandlocalai-org-maint-bot 804dc10968 feat(gallery): add Tess 4 27B variants (#11547)
Add Q4_K_M and Q8_0 multimodal builds. Include an MTP-enabled Q4_K_M build for speculative decoding.

Assisted-by: Codex:gpt-5

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-16 21:44:22 +02:00
localai-org-maint-botandlocalai-org-maint-bot d666f1a0f0 feat(gallery): add HunyuanOCR variants (#11540)
Add the official Q8 and BF16 llama.cpp builds for the archived HunyuanOCR 1.0 checkpoint.

Assisted-by: Codex:gpt-5

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-16 21:44:09 +02:00
localai-org-maint-botandlocalai-org-maint-bot 1e3e72ecb8 chore(deps): bump golang.org/x/net to v0.55.0 (#11544)
Assisted-by: Codex:gpt-5.6

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-16 11:57:55 +02:00
mudler's LocalAI [bot]andEttore Di Giacinto 0aaff91ebd feat(ui): unify model and backend lifecycle (#11548)
* feat(ui): add installed model lifecycle

Models now owns catalog exploration and installed runtime controls under one canonical route. URL-owned state keeps lifecycle context recoverable through links and browser history.

Assisted-by: Codex:gpt-5 Playwright

* feat(ui): add installed backend lifecycle

Backends split discovery from backend-binary management. The canonical
page now keeps both lifecycle views under one URL-backed shell while it
preserves target-node placement.

Assisted-by: Codex:gpt-5 Playwright

* fix(ui): repair lifecycle state updates

Installed models lost distributed refreshes and kept a deleted selection. Backend searches also stopped tracking URL changes, while batch upgrades stopped after their first error.

Preserve background refreshes and finish each requested batch action. Drive catalog results from URL-backed state without losing full metadata.

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

* feat(ui): make resource pages canonical

Replace Host navigation with canonical Models and Backends lifecycle routes, preserve legacy management URLs, and surface shared host capacity on the Operate overview.

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

* feat(ui): complete canonical resource lifecycle

Finish the responsive list-to-detail behavior, remove the retired Host implementation, and keep Explore focused on discovery while Installed owns destructive actions. Update regression coverage, localization, documentation, and development binding for the canonical resource pages.

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

* docs(ui): record the UI design context

Record the approved users, brand character, and design principles so
future interface work uses the same product direction. Index the context
from the repository's agent instructions.

Assisted-by: Codex:gpt-5

---------

Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
2026-08-16 11:57:31 +02:00
mudler's LocalAI [bot]andEttore Di Giacinto 6fb9ab38aa feat(gallery): add vllm.cpp text-generation models (#11511)
Adds eight curated vllm-cpp entries to the model gallery. Until now the
backend had gallery coverage only for MiniMax-H3 video, so serving text on
it meant hand-writing engine_args.

The flagship tier is what vllm.cpp gates its correctness and speed claims
on: Qwen3.6-27B and Qwen3.6-35B-A3B in NVFP4, each with a speculative
sibling (MTP on both, DFlash on the 27B). Qwen3-Coder-30B-A3B covers
agentic tool use, and Qwen3-4B / Qwen3-0.6B in bf16 are the entries that
run where NVFP4 cannot, CPU included.

Three details are load-bearing rather than incidental:

- The 27B entries pin revision 890bdef7. That repository was later
  re-quantized in place from NVFP4 to FP8 W8A8 under the same name, so an
  unpinned entry resolves to different weights and reports nothing.
- Qwen3-Coder names tool_parser: qwen3_coder explicitly. Its dialect is
  byte-identical on the wire to step3p5's, so chat-template sniffing
  cannot separate them and auto-detection picks wrong.
- enable_prefix_caching is deliberately left unset everywhere. It defaults
  on for dense models and off for the GDN hybrids, and that per-model
  default is the right answer.

num_blocks is sized per model from its real KV footprint rather than
copied between entries, which ranges from 20 KiB/token on the 35B to
144 KiB/token on the 4B.

Docs: adds features/vllm-cpp.md covering installation, the model table,
the pinning rationale and how to choose between the speculative variants,
and cross-links it from the existing engine_args reference. It also
records that the CUDA images are built for Blackwell only, which is
narrower than vllm.cpp's own ten-architecture release and makes an
otherwise cryptic "no kernel image is available" failure legible.

Verified: gallery suite green; all eight decode and validate as a
ModelConfig. qwen3-0.6b-vllm-cpp confirmed end to end on a real cluster,
chat plus engine-parsed tool_calls. The NVFP4 entries are not yet
runtime-verified: no available node has kernels for them.


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

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
2026-08-16 00:32:37 +02:00
mudler's LocalAI [bot]andmudler 7a2a624424 chore: ⬆️ Update vllm-metal (darwin) to v0.3.0.dev20260815085651 (#11541)
⬆️ Update vllm-project/vllm-metal (darwin)

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-16 00:32:04 +02:00
mudler's LocalAI [bot]andmudler a556b1a10d chore: ⬆️ Update ikawrakow/ik_llama.cpp to 8337e4cd3861406fc04e0854b1409cd1b027fbc9 (#11542)
⬆️ 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-16 00:31:54 +02:00
localai-org-maint-botandlocalai-org-maint-bot db21c47a76 fix(downloader): retry checksum mismatches (#11536)
A remote can serve stale or corrupted bytes for one request. Mark the
integrity failure as transient so the bounded download planner retries it.

Assisted-by: Codex:gpt-5.6

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-15 18:07:17 +02:00
dependabot[bot]andlocalai-org-maint-bot 4f807eaa06 chore(deps): bump vllm from 0.25.1 to 0.26.0 in /backend/python/vllm (#11402)
* chore(deps): bump vllm from 0.25.1 to 0.26.0 in /backend/python/vllm

Bumps [vllm](https://github.com/vllm-project/vllm) from 0.25.1 to 0.26.0.
- [Release notes](https://github.com/vllm-project/vllm/releases)
- [Changelog](https://github.com/vllm-project/vllm/blob/main/RELEASE.md)
- [Commits](https://github.com/vllm-project/vllm/compare/v0.25.1...v0.26.0)

---
updated-dependencies:
- dependency-name: vllm
  dependency-version: 0.26.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* fix(vllm): pin Intel source build to release

Build the Intel XPU backend from vLLM 0.26.0 instead of the moving main branch, and use the Triton XPU version required by that release's torch 2.12 dependency.

Assisted-by: Codex:gpt-5 [systematic-debugging]

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-15 14:38:08 +02:00
88edd7fc7f fix(distributed): run cold model loads as durable jobs instead of holding the advisory lock (#11514)
* fix(advisorylock): set statement_timeout alongside lock_timeout

WithLockCtx already overrides a deployment-wide lock_timeout on its
dedicated connection so a blocking pg_advisory_lock() waits its turn
instead of failing with 55P03. statement_timeout aborts that exact same
statement independently, with SQLSTATE 57014, and was not overridden.

Production roles commonly carry statement_timeout=60s. Any guarded
section longer than that (a cold model load stages for tens of minutes)
therefore killed every concurrent waiter:

  advisorylock: acquiring lock 9003261067483446873: ERROR: canceling
  statement due to statement timeout (SQLSTATE 57014)

Derive it from the same context budget as lock_timeout, with a matching
RESET so the pooled connection is returned clean.

Assisted-by: Claude Opus 5 [claude-code]

* feat(distributed): add ModelLoadJob, the durable cold-load record

A cold load in distributed mode is a long-running background job, but it
was modelled as a synchronous side effect of an inference request: the
whole of it (backend install, multi-GB staging, checkpoint load) ran
inside the per-model advisory lock. Loading a 35.7 GB GGUF held that lock
for ~20 minutes, so every concurrent request for the same model blocked
on pg_advisory_lock and died at the role's 60s statement_timeout.

Introduce the row that lets the lock shrink to a decision. Exactly one
ModelLoadJob may be active per tracking key; that uniqueness — not the
lifetime of a lock — is what de-duplicates concurrent loaders across
replicas. ClaimLoadJob does its read-then-write under the advisory lock
and nothing else: no network, file or gRPC I/O inside the guarded
section, so a claim costs milliseconds no matter how long the resulting
load takes.

LastProgress is a heartbeat rather than a byte counter. A checkpoint load
legitimately moves zero bytes for many minutes, so a reaper keyed on byte
movement would reclaim a healthy job mid-load; byte progress stays the
concern of load_deadline.go. A job whose heartbeat stops for longer than
the orphan window is reclaimable, so a replica killed mid-load cannot
wedge a model permanently.

Failed jobs keep their row for a short grace so an immediately-following
request reports the real cause instead of silently starting a fresh load
of a model that just failed.

No caller yet — the router moves onto this in the next commit.

Assisted-by: Claude Opus 5 [claude-code]

* refactor(distributed): run cold loads as jobs, outside the advisory lock

Route wrapped the entire cold load — node selection, backend install,
multi-GB staging and the remote LoadModel — in the per-model advisory
lock. The lock's job is to de-duplicate concurrent loaders, a decision
that takes milliseconds; holding it for the tens of minutes the resulting
work takes is what turned a dedup mechanism into a cluster-wide outage
for that model.

Split it into a claim and a run. The claim is the only thing left inside
the lock. The run is a background job owned by the claiming replica and
bounded by the same progress-extended deadline as before; every other
request for that model — local or on another replica — attaches as a
waiter and is served the moment the model is ready, with no duplicate
load and no lock contention.

Waiters share one broadcast rather than an ordered queue: they all want
the identical outcome, so ordering them would add fairness machinery that
changes no result. The local channel wakes same-replica waiters instantly
and a 2s DB poll is the authority, because a waiter on another replica
has no channel to close. On wake a waiter re-runs the warm path rather
than trusting the signal — the model may have been evicted in between.

A waiter whose client disconnects returns immediately and the job keeps
running; it belongs to the job record, not to the request. A failure is
recorded on the row so every waiter reports the real cause, and the row
survives briefly so the next request does not read "no job" as "not
loading" and start a duplicate load of a model that just failed.

The runner heartbeats the row on a fixed interval whether or not bytes
are moving, which is what keeps a legitimately silent checkpoint load
from being reclaimed as an orphan. Phase (installing/staging/loading) and
placement ride to the heartbeat on the context, the same seam
load_deadline.go already uses, so single-host paths are untouched.

Non-distributed mode (no DB) keeps the inline load exactly as it was.

Assisted-by: Claude Opus 5 [claude-code]

* feat(distributed): bound the wait for a loading model and answer with progress

A request whose model is cold-loading now attaches to the running job and
is served the moment the model is ready. That wait has to be bounded: a
held HTTP request cannot survive real infrastructure, and an ingress or LB
idle timeout kills a twenty-minute request regardless of what LocalAI
does.

New LOCALAI_MODEL_LOAD_WAIT (default 60s) bounds the CALLER, never the
load — the job keeps running either way. On expiry the request gets 503
with Retry-After and a structured body naming the model, the node, the
phase, byte progress and an ETA. The `error` envelope keeps OpenAI
clients working; `loading` is additive so they ignore it.

The ETA comes from the job's own observed rate and is omitted rather than
guessed until enough bytes have moved for that rate to mean anything: a
confidently wrong ETA on a twenty-minute wait is worse than none.
Retry-After is that ETA when known, clamped to [5s, 300s], and the wait
budget otherwise.

LOCALAI_MODEL_LOAD_WAIT=0 waits unbounded, for deployments with no proxy
in front. Zero in the config struct still means "unset, use the default",
so the CLI records the operator's zero as ModelLoadWaitUnbounded rather
than losing the distinction.

The distributed branch of ModelLoader.loadModel wrapped the router's
error with %s, which flattened it to a string. Use %w: the typed error is
what the HTTP layer keys the 503 off.

Assisted-by: Claude Opus 5 [claude-code]

* feat(api): add GET /api/models/{id}/load-status

A client that receives 503 while a model stages onto a worker needs
somewhere to poll. This returns the same `loading` object the 503 carries
— phase, node, byte progress and ETA — or 404 when no load is running.

Read-only and observability-shaped, so it is deliberately neither
admin-gated nor feature-gated: it explains a 503 the caller just
received, and hiding that behind a per-modality feature would make the
explanation for a failed image request depend on chat permissions. It
also gets no MCP tool, since there is nothing here an admin would manage
conversationally.

Registered on the surfaces from .agents/api-endpoints-and-auth.md: the
swagger block (existing `models` tag, so /api/instructions needs no new
area), the endpoint discovery maps in RegisterLocalAIRoutes, regenerated
swagger, and the distributed-mode docs page. No FLAG_* usecase is
involved, so capabilities.js is unchanged.

Assisted-by: Claude Opus 5 [claude-code]

* feat(ui): show cold-load progress in Chat and retry when the model is ready

A chat request for a model that is still staging onto a worker now gets a
503 carrying live progress instead of an error. Render it: the composer
shows the phase (installing / staging / loading), the node, the percent
and the ETA, then polls load-status and re-sends the request the moment
the model is ready.

Reuses the staging progress idiom the page already had rather than
inventing a second one — the two sources are folded into one
loadProgress, with the load job winning because it is authoritative
across frontend replicas and knows the phase, where the staging operation
only knows about a byte transfer this replica happens to be performing.

Waiting is bounded (three send attempts, ~30 min of polling each), so a
load that never finishes still surfaces as an error rather than as a
spinner nobody questions. An aborted generation stops the polling too.

Assisted-by: Claude Opus 5 [claude-code]

* fix(distributed): check warm-path cleanup errors

The router moved legacy cleanup calls onto newly linted lines. Report
cleanup failures while preserving the fallback to a cold load.

Assisted-by: Codex:gpt-5 [golangci-lint]

---------

Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-15 13:20:11 +02:00
localai-org-maint-botandlocalai-org-maint-bot 08563942b5 feat(gallery): add LFM2.5 230M variants (#11526)
Add LiquidAI’s compact edge model in Q4_K_M and Q8_0 builds. The
variant pair lets LocalAI choose the highest-quality build that fits.

Assisted-by: Codex:gpt-5.4

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-15 13:17:52 +02:00
localai-org-maint-botandlocalai-org-maint-bot 342c3d0e17 fix(model): report backend crash diagnostics (#11532)
Unexpected runtime exits only reported an exit code, which hid the backend diagnostic. Include the final non-empty stderr line when one exists.

Assisted-by: Codex:gpt-5

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-15 13:17:24 +02:00
localai-org-maint-botandlocalai-org-maint-bot ce8a04fe46 fix(stablediffusion): embed Metal library (#11531)
The Darwin workflow passes BUILD_TYPE=metal but does not define OS=Darwin. The backend therefore omitted its Metal CMake flags and shipped the runtime source path instead of the embedded library.

Map the requested build type directly to the Metal flags and guard the build contract with a dry-run regression test.

Assisted-by: Codex:gpt-5

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-15 13:15:55 +02:00
dependabot[bot] 9f17f5e76c chore(deps): bump sentence-transformers from 5.6.1 to 5.7.0 in /backend/python/transformers (#11499)
chore(deps): bump sentence-transformers in /backend/python/transformers

Bumps [sentence-transformers](https://github.com/huggingface/sentence-transformers) from 5.6.1 to 5.7.0.
- [Release notes](https://github.com/huggingface/sentence-transformers/releases)
- [Commits](https://github.com/huggingface/sentence-transformers/compare/v5.6.1...v5.7.0)

---
updated-dependencies:
- dependency-name: sentence-transformers
  dependency-version: 5.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-15 13:14:13 +02:00
localai-org-maint-botandlocalai-org-maint-bot c82eec3bbd feat(gallery): add DeepSeek V4 Pro 0813 (#11533)
Add the UD-Q4_K_XL GGUF build as a 20-shard llama.cpp entry for the latest DeepSeek V4 Pro release.

Assisted-by: Codex:gpt-5

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-15 13:12:18 +02:00
mudler's LocalAI [bot]andmudler 342f9bc9d2 chore: ⬆️ Update ggml-org/whisper.cpp to 1fe009caeda75f69bc864d6370b10674e45a92bd (#11524)
⬆️ 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-15 13:11:59 +02:00
mudler's LocalAI [bot]andmudler ac468f743c chore: ⬆️ Update CrispStrobe/CrispASR to cc498701f1a68d88dd489803ebad10053a924322 (#11523)
⬆️ 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-15 12:41:56 +02:00
mudler's LocalAI [bot]andmudler ee3604b294 chore: ⬆️ Update vllm-metal (darwin) to v0.3.0.dev20260814013332 (#11522)
⬆️ Update vllm-project/vllm-metal (darwin)

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-15 12:41:10 +02:00
mudler's LocalAI [bot]andmudler f4a8dd73fd chore(model-gallery): ⬆️ update checksum (#11525)
⬆️ 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-15 12:40:37 +02:00
mudler's LocalAI [bot]andmudler 81d8507a26 chore: ⬆️ Update ikawrakow/ik_llama.cpp to 43afea46c25a12aae6db1e3105643267164898b4 (#11527)
⬆️ 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-15 12:40:10 +02:00
localai-org-maint-botandlocalai-org-maint-bot 44413a9d06 feat(gallery): add Qwen3.8 27B variants (#11519)
Add the official Q4_K_M and Q8_0 GGUF files with the shared vision projector. Include an MTP variant for speculative decoding.

Assisted-by: Codex:gpt-5

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-14 19:02:32 +02:00
Ettore Di Giacinto 3a3c31114b Remove Star history from README
Removed the Star history section from the README.

Signed-off-by: Ettore Di Giacinto <mudler@users.noreply.github.com>
2026-08-14 15:07:40 +02:00
localai-org-maint-botandlocalai-org-maint-bot 95653f221e fix(ui): keep agent import action visible (#11488)
* fix(ui): keep agent import action visible

The header hid its full import label after the agent list became non-empty. Hide only the nested file input so users can import more agents.

Assisted-by: Codex:gpt-5

* test(ui): match the agent import label

The Agents page renders the action as Import.

The test searched for Import Agent, so it failed before checking visibility.

Mock the observables request to remove backend timing from the fixture.

Assisted-by: Codex:gpt-5

---------

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-14 15:06:51 +02:00
mudler's LocalAI [bot]andmudler 58a37fa2c9 chore: ⬆️ Update CrispStrobe/CrispASR to cb082743c456ac77aec0947de36e6420a933da04 (#11510)
⬆️ 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-14 15:05:08 +02:00
mudler's LocalAI [bot]andmudler 1dd6af0977 chore: ⬆️ Update ikawrakow/ik_llama.cpp to 981e5ea0d7579b4803c86afbb09a7cd7d7bf3bb8 (#11509)
⬆️ 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-14 15:04:53 +02:00
tom-mi 9e8a4b5f34 fix (gallery): Parse harmony output of gpt-oss-* models correctly (#8037) (#11518)
fix: Parse harmony output of gpt-oss-* models correctly (#8037)

* Delegate templating to llama.cpp's jinja runtime

Assisted-by: opencode:GLM-5.2

Signed-off-by: Thomas Reifenberger <tom-mi@users.noreply.github.com>
2026-08-14 15:04:12 +02:00
mudler's LocalAI [bot]andEttore Di Giacinto 0c9d4bf9cc fix(vllm-cpp): build every CUDA architecture the platform can host (#11512)
The vllm-cpp CUDA images were built for Blackwell only: 120a;121a on
amd64 and 121a alone on arm64. vllm.cpp's own release archive builds ten
architectures, so LocalAI shipped one or two of them.

The failure mode is the problem. An unlisted card is not slower, it dies
at the first request with "no kernel image is available for execution on
the device", long after `backends install` reported success. That covers
A100, A10/3090, L4/4090/RTX 6000 Ada, H100/H200, B200, B300, Jetson Orin
and Jetson Thor, and it is how a Jetson Thor node was found serving
nothing at all.

amd64 now builds 80;86;89;90a;100a;103a;120a;121a and arm64 builds
87;90a;100a;110;121a, split by where the silicon exists: Jetson is
arm64-only, desktop 120a is amd64-only, and 90a/100a are on both because
of GH200/GB200.

Triton-AOT stays ON for both, which the old comment said was impossible.
It is not, at the version we pin: only maintainer REGEN needs a single
arch, while the BUILDER path embeds every vendored cubin tree and selects
by exact SM, so 87/103a/110/120a take the portable CUDA kernels and can
never load a neighbouring cubin. Upstream ships its ten-SM archive that
way.

The CUDA 13 guard now covers both branches rather than amd64 alone. arm64
needs compute_121a just as much, and CI already builds it with 13.

Cost is smaller than the arch count suggests, because gencode is
per-source: fp4-mma still resolves to 120a;121a, and the CUTLASS
scaled-mm kernels to one arch each, so the added architectures do not
multiply the expensive translation units.

Verified: flag generation checked for both branches, CUDA 12 still
refused, CPU build untouched; both arch lists expanded through vllm.cpp's
own vt_cuda_gencode_options and per-feature arch gating, and all six
vendored Triton trees confirmed intact, at the exact pinned commit. A
real compile is CI-only: there is no CUDA toolchain on the dev box.


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

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
2026-08-14 08:53:27 +02:00
mudler's LocalAI [bot]andmudler f86df43415 chore: ⬆️ Update vllm-metal (darwin) to v0.3.0.dev20260813121949 (#11507)
⬆️ Update vllm-project/vllm-metal (darwin)

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-14 08:42:23 +02:00
dependabot[bot] 90b3585080 chore(deps): update transformers requirement from >=5.14.1 to >=5.15.0 in /backend/python/transformers (#11500)
chore(deps): update transformers requirement

Updates the requirements on [transformers](https://github.com/huggingface/transformers) to permit the latest version.
- [Release notes](https://github.com/huggingface/transformers/releases)
- [Commits](https://github.com/huggingface/transformers/compare/v5.14.1...v5.15.0)

---
updated-dependencies:
- dependency-name: transformers
  dependency-version: 5.15.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-13 22:28:05 +02:00
071952a964 chore: ⬆️ Update ggml-org/llama.cpp to 84e908c625fb60992b4cdef8180fb12fa9b4c4bf (#11473)
* ⬆️ Update ggml-org/llama.cpp

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* fix(llama-cpp): refresh the TTS patch

The llama.cpp update moved and changed the generated-audio pipeline. Refresh the carried patch so backend builds can apply it to the new revision.

Assisted-by: Codex:gpt-5.4

---------

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-13 22:27:45 +02:00
Richard Palethorpe 5c63969760 fix: Show MCP connection errors in the UI (#11495)
* fix(mcp): surface configured server failures

Keep model-configured MCP servers visible when discovery or connection setup fails, propagate status through distributed discovery, and let the Chat UI show actionable errors while retrying unavailable servers.

Add model-editor metadata for remote and stdio configuration and document the expected format, deployment networking boundary, and alternate MCP scopes.

Assisted-by: Codex:gpt-5 Ordino golangci-lint
Signed-off-by: Richard Palethorpe <io@richiejp.com>

* build(compose): match CUDA development image

Configure the API image with the cublas, CUDA 13, auth-tagged build settings used by the local development Makefile invocation, including the 24-way Docker build.

Assisted-by: Codex:gpt-5 Ordino
Signed-off-by: Richard Palethorpe <io@richiejp.com>

* revert: keep host build settings out of compose

The CUDA development deployment is managed from ~/docker/localai, not the repository example Compose file. Restore the generic example and keep machine-specific build settings in the host deployment.

Assisted-by: Codex:gpt-5 Ordino
Signed-off-by: Richard Palethorpe <io@richiejp.com>

* fix(docker): exclude local agent artifacts

Keep Claude worktrees and locally installed verification tools out of the Docker build context. These host-only directories added roughly 1.9 GB to every root image build.

Assisted-by: Codex:gpt-5 Ordino
Signed-off-by: Richard Palethorpe <io@richiejp.com>

---------

Signed-off-by: Richard Palethorpe <io@richiejp.com>
2026-08-13 22:25:58 +02:00
dependabot[bot] b2ff2b5477 chore(deps): bump packaging from 26.2 to 26.3 in /backend/python/coqui (#11497)
Bumps [packaging](https://github.com/pypa/packaging) from 26.2 to 26.3.
- [Release notes](https://github.com/pypa/packaging/releases)
- [Changelog](https://github.com/pypa/packaging/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pypa/packaging/compare/26.2...26.3)

---
updated-dependencies:
- dependency-name: packaging
  dependency-version: '26.3'
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-13 22:25:41 +02:00
dependabot[bot] 8c69a5f77a chore(deps): bump backend/rust/kokoros/sources/Kokoros from 7089168 to 29e99ad (#11496)
chore(deps): bump backend/rust/kokoros/sources/Kokoros

Bumps [backend/rust/kokoros/sources/Kokoros](https://github.com/lucasjinreal/Kokoros) from `7089168` to `29e99ad`.
- [Commits](https://github.com/lucasjinreal/Kokoros/compare/7089168f0ca2d8e1fcd8e523c9d75d915c6afdff...29e99ad5a5aa64b97e1e8e963e6d73b0267d796a)

---
updated-dependencies:
- dependency-name: backend/rust/kokoros/sources/Kokoros
  dependency-version: 29e99ad5a5aa64b97e1e8e963e6d73b0267d796a
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-13 22:25:22 +02:00
Dedy F. Setyawan 9fd7ea7e93 i18n(id): translate admin, media, and nav UI strings to Indonesian (#11493)
Signed-off-by: Dedy F. Setyawan <dedyfajars@gmail.com>
2026-08-13 16:27:45 +02:00
localai-org-maint-botandlocalai-org-maint-bot 8b01ac2d4e feat(gallery): add LFM2.5 VL 1.6B variants (#11490)
Add the official Q4_K_M and Q8_0 GGUF builds with the F16 vision projector.

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

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-13 10:47:56 +02:00
fieryWaters 1ef10721c8 fix(parakeet-cpp): enable Metal in macOS builds (#11492)
Forward BUILD_TYPE=metal to PARAKEET_GGML_METAL so macOS backend artifacts include Metal support.

Assisted-by: Codex:gpt-5.6-sol

Signed-off-by: fierywaters <fierywaters13@gmail.com>
2026-08-13 10:47:27 +02:00
mudler's LocalAI [bot]andmudler 9ab62cbb38 chore: ⬆️ Update leejet/stable-diffusion.cpp to de298c225bed97c3f9026b73cd7b71e7879bd41b (#11469)
⬆️ 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-13 08:25:21 +02:00
mudler's LocalAI [bot]andmudler e9b94e54a3 chore: ⬆️ Update CrispStrobe/CrispASR to ce521ee178867ceaa5fdc11803616578c8936c19 (#11470)
⬆️ 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-13 08:25:05 +02:00
mudler's LocalAI [bot]andmudler 6fc46e83e1 chore: ⬆️ Update vllm-project/vllm cu130 wheel to 0.27.1 (#11468)
⬆️ Update vllm-project/vllm cu130 wheel

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-13 08:24:45 +02:00
mudler's LocalAI [bot]andmudler 1fa9f2969d chore: ⬆️ Update vllm-metal (darwin) to v0.3.0.dev20260812005333 (#11467)
⬆️ Update vllm-project/vllm-metal (darwin)

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-13 08:24:32 +02:00
mudler's LocalAI [bot]andmudler 2b1f6d27e9 chore: ⬆️ Update ikawrakow/ik_llama.cpp to c46ffaa5665cfb2d6cf372c9a054dbab896e14fe (#11482)
⬆️ 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-13 00:47:11 +02:00
mudler's LocalAI [bot]andmudler 3aa097af44 chore: ⬆️ Update mudler/vllm.cpp to 9fd9e8f34408d5dd21d7f9385e96fc755708950b (#11472)
⬆️ 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-13 00:46:58 +02:00
localai-org-maint-botandlocalai-org-maint-bot c52808c38b feat(gallery): add Fara1.5 4B variants (#11479)
Add the smaller Fara1.5 computer-use model alongside the existing 9B and 27B gallery entries. Offer Q4_K_M and Q8_0 builds so LocalAI can select for memory or quality.

Assisted-by: Codex:gpt-5

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-12 22:43:26 +02:00
localai-org-maint-botandlocalai-org-maint-bot 0647939953 fix(gallery): repair DeepSeek V4 fallback (#11480)
The DeepSeek V4 Flash base entry points at a Hugging Face repository page instead of a GGUF object. When variant probing cannot rank a concrete build, the base fallback downloads no usable model weights.

Use the validated IQ2XXS object and checksum already shipped by the q2 variant. Pin that payload in the gallery resolution test.

Assisted-by: Codex:gpt-5.6

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-12 22:42:55 +02:00
mudler's LocalAI [bot]andmudler b295c936fb chore: ⬆️ Update mudler/depth-anything.cpp to 54abd5c0abfd1f394e01cb3c38f2e3af4daedf85 (#11481)
⬆️ Update mudler/depth-anything.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-12 22:42:26 +02:00
localai-org-maint-botandlocalai-org-maint-bot d54067d4c6 feat(gallery): add TwIL-LM3 variants (#11478)
Add the publisher-recommended Q4_K_M build and a near-lossless Q8_0 variant for the 3B formal-logic reasoning model.

Assisted-by: Codex:gpt-5

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-12 18:54:15 +02:00
localai-org-maint-botandlocalai-org-maint-bot d50a1b9c98 feat(gallery): add Nemotron 3.5 Lightning 30B (#11477)
Add the official llama.cpp GGUF builds for NVIDIA Nemotron 3.5 Lightning 30B-A3B, linking Q4_K_M, NVFP4, and Q8_0 as installable variants.

Assisted-by: Codex:gpt-5

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-12 15:53:12 +02:00
localai-org-maint-botandlocalai-org-maint-bot ecdb8e768a fix(gallery): align Higgs Audio checksum test (#11476)
The validated gallery checksum changed in #11459, but its dedicated regression assertion kept the previous value and now fails the master test suite on Linux and macOS.

Assisted-by: Codex:gpt-5.4

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-12 15:52:59 +02:00
mudler's LocalAI [bot]andmudler 4916e44c07 chore(model-gallery): ⬆️ update checksum (#11471)
⬆️ 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-12 09:12:26 +02:00
github-actions[bot]andmudler b7a0646587 chore: bump inference defaults from unsloth (#11270)
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-08-12 09:12:12 +02:00
3ec466b61b chore: ⬆️ Update ikawrakow/ik_llama.cpp to 26ceed9d4091a1696cf50e2ed87e5767d5811d81 (#11475)
* ⬆️ Update ikawrakow/ik_llama.cpp

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* fix(gallery): align Higgs Audio checksum test

The validated gallery checksum changed in #11459, but its dedicated regression assertion kept the previous value and now fails the master test suite on Linux and macOS.

Assisted-by: Codex:gpt-5.6

---------

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-12 08:46:16 +02:00
mudler's LocalAI [bot]andmudler f92e4e1303 chore: ⬆️ Update mudler/vllm.cpp to 24306364ab8beaed9197604a042a56aaccfde493 (#11385)
⬆️ 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-11 23:05:57 +02:00
Dimitris Karakasilis 3636fcbd38 fix(realtime): keep the ICE interface allow-list working with a fixed UDP port (#11466)
LOCALAI_WEBRTC_ICE_INTERFACES was silently ignored whenever
LOCALAI_WEBRTC_UDP_PORT was set. Every interface was gathered regardless
of the allow-list, so a browser was handed the docker0/veth addresses the
setting exists to suppress, and the connection established on a good pair
and then dropped when consent checks failed on the unreachable ones.

Two things combine to cause it. A mux built over a wildcard socket makes
pion derive its host candidates by enumerating interfaces itself, with a
nil filter and loopback included. Independently, the muxed gathering path
in pion/ice never consults SetInterfaceFilter, so setting it has no effect
there either.

Bind one socket per admitted interface address via NewMultiUDPMuxFromPort,
which takes the filter, instead of one wildcard socket. All the sockets
share the same port, so the firewall requirement is still a single rule.
Networks are pinned to UDP4 to match the socket family this replaces.

An allow-list that matches no address on the host now reports the
misconfiguration rather than binding nothing and leaving signaling to
succeed while no candidate is ever advertised.

Two tests: one asserts an unmatched allow-list is an error, and one gathers
against a real peer connection and asserts no address outside the allowed
interface appears (skipped on single-interface hosts).

Assisted-by: Claude:claude-opus-5 go vet gofmt

Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
2026-08-11 18:37:49 +02:00
localai-org-maint-botandlocalai-org-maint-bot 22076774f0 fix(gallery): correct Higgs Audio v3 checksum (#11459)
The gallery checksum did not match the Q8_0 object currently published by audio-cpp. Use the SHA256 reported by both Hugging Face LFS metadata and the live x-linked-etag so model installation can verify the download.

Assisted-by: Codex:gpt-5

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-11 12:07:55 +02:00
mudler's LocalAI [bot]andEttore Di Giacinto 45cb3983ee fix(ui): unmerge the class strings that left buttons in browser chrome (#11462)
Eight header controls across seven pages had two or three elements' classes
collapsed into one string. The wrapper or the icon ended up wearing the
button classes, and the buttons themselves were left with no class at all,
so they rendered in the browser's own chrome. Reported on Agent Jobs; the
grep found the rest.

`fas` does not draw anything by itself: it sets
`font-family: "Font Awesome 6 Free"` and weight 900 on whatever carries it,
and the `fa-*` class supplies the glyph via ::before. So

    <button className="btn btn-primary fas fa-plus">

renders its own label "New Task" in the icon font, and

    <div className="hstack btn btn-primary btn-sm fas fa-edit btn-secondary fa-arrow-left">
      <button>Edit</button>
      <button>Back</button>
    </div>

styles the flex wrapper as a button that is both primary and secondary,
points two glyphs at one ::before, and leaves both real buttons bare.

Fixed, all of them keeping the correct `<i>` child they already had:

  - AgentJobs, AgentTaskDetails (x2), AgentCreate - icon classes off the
    button.
  - AgentTaskDetails, AgentJobDetails - wrapper back to plain `hstack`, and
    the two buttons inside each get the variants the wrapper had been
    holding. Back is secondary and leads, Edit/Cancel is the emphatic one
    and trails, matching every other detail header.
  - VoiceLibrary, VoiceProfileCreate - the title `<i>` had swallowed the
    action link's classes, so "Create voice" and "Back to library" were
    unstyled anchors. Back was also drawing a "+" because it had inherited
    fa-plus while its own fa-arrow-left sat up in the title.
  - P2P - a stray fa-circle-info on the title icon.

The ninth instance was ImportModel, where this class of bug was first
found. #11461 rewrote that file and landed first, so nothing is left to fix
there.

Guarded by e2e/class-hygiene.spec.js, which reads the source rather than
walking routes: several of these pages need agent or voice data before they
render a header, so a route walk would skip exactly the pages that had the
bug. It fails on an icon-font class outside an `<i>`/`<span>`, on two glyphs
or two button variants on one element, and on a layout wrapper that is also
a button. Font Awesome modifiers (fa-spin, fa-fw, sizes) are excluded, so
the `fa-spinner fa-spin` idiom stays legal.

e2e: 428 passed.


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

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
2026-08-11 12:07:42 +02:00
mudler's LocalAI [bot]andEttore Di Giacinto 7a7fb00730 feat(ui): rebuild the import form on the restyled design language (#11461)
The import page took the new palette in #11305 but kept its old layout, so
it stayed a 760px column with the primary action detached from the form it
submits. Two of the problems were outright bugs.

The Import button carried no className at all, so the page's single most
important control fell through to the user-agent button: system chrome,
wrong radius, no design-system focus ring. The YAML button carried
`fas fa-save fa-upload`, which sets Font Awesome as the button's own font
family (its label text inherits it) and points two glyph classes at one
::before.

On the layout: `page--narrow` is documented for "forms / single-record edit
views", and in Advanced mode this page held a URI field, a six-section
format guide, ten modality chips, nine preference fields, a key-value
repeater and a YAML editor at `calc(100vh - 400px)`. The width was the
symptom; one column was the disease.

  - `page--medium` with a work column and a format reference beside it.
    The reference answers the only question a first-time admin has and used
    to sit behind a chevron, closed by default. Below 1024px it becomes a
    disclosure rather than disappearing.
  - The source field is the hero: monospace, because it holds something you
    paste, and it carries its own Import button. That removes the hidden
    aria-hidden submit button that existed only because the real action sat
    outside the form.
  - Simple and Advanced are gone. They were ~80% the same surface, and the
    overlap cost a mode switch, a localStorage key and a three-button
    Keep/Discard/Cancel dialog whose only job was protecting state that
    switching modes would hide. One form with a collapsible options panel
    hides nothing, so none of it is needed. What genuinely differs is the
    kind of input, which is now the two tabs: a source, or YAML.
  - The size/VRAM estimate reports under the field that produced it instead
    of as a banner above the page header, and an import in flight gets the
    progress, phase and byte counts the poller already returned and the old
    status card threw away.
  - ModalityChips resolves its labels through the same `modality.*` keys as
    the dropdown it filters. It hardcoded English shorthand, so one modality
    carried two names on one screen ("Speech" on the chip, "Speech
    recognition" on the group it scrolled to) and seven locales had neither.
    Its inline styles and its pill radius move onto the design system.
  - Three inline styles go, including both conditional-padding hacks; the
    only one left is the progress bar's runtime width. Baseline 538 -> 535.

Docs updated in the same change: the WebUI section described a Simple and an
Advanced mode and told the reader to "Toggle to Advanced Mode".

e2e: 426 passed. The mode-switch suite is replaced by one covering the tabs
and the disclosure, and a new layout suite pins the width, the styled
primary action, the absence of an icon-font button, the reference column at
both widths, and the estimate's position.


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

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
2026-08-11 12:00:59 +02:00
localai-org-maint-botandlocalai-org-maint-bot f2cdc06781 fix(model): surface backend startup exits (#11447)
* fix(model): surface backend startup exits

Preserve the local backend process exit code and bounded stderr diagnostic when the process dies before its gRPC service becomes ready.

Fixes #9050

Assisted-by: Codex:gpt-5

* fix(model): satisfy startup diagnostic checks

Assisted-by: Codex:gpt-5.6 [Codex]

---------

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-11 09:54:29 +02:00
localai-org-maint-botandlocalai-org-maint-bot 16dd81ecd2 fix(ci): parse current vllm-metal version pins (#11457)
* fix(ci): parse current vllm-metal version pins

vllm-metal renamed its installer pin from vllm_v to VLLM_VERSION, breaking both the nightly bumper and the Darwin backend installer after a bump. Share a strict parser that accepts both formats and cover the transition with shell regressions.

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

* fix(ci): parse scoped vllm-metal pins

The pinned vllm-metal installer declares its version as a local shell variable. Accept that optional declaration while retaining strict validation of the assignment and semantic version.

Assisted-by: Codex:gpt-5.4

---------

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-11 09:53:50 +02:00
localai-org-maint-botandlocalai-org-maint-bot f3ea275c05 feat(gallery): add XYZ Aquila mini variants (#11455)
Add Q4_K_M and Q6_K llama.cpp builds with the matching vision projector and verified Hugging Face artifact hashes.

Assisted-by: Codex:gpt-5

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-11 09:53:24 +02:00
localai-org-maint-botandlocalai-org-maint-bot b0d731288f feat(gallery): add BTL-4 Compact (#11449)
Add the text-only IQ2_XXS build with the model card's required Jinja, reasoning parser, KV cache, and sampling defaults.

Assisted-by: Codex:gpt-5

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-11 09:53:13 +02:00
localai-org-maint-botandlocalai-org-maint-bot 16193e1982 feat(gallery): add Higgs Audio v3 TTS (#11456)
Expose the existing audio.cpp Higgs support as an installable Q8 gallery model and document voice cloning and licensing constraints.

Assisted-by: Codex:gpt-5

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-11 09:52:38 +02:00
7cfccdc2bf chore: ⬆️ Update ggml-org/llama.cpp to 030ebb558a5820b444a8f836ed5cdd46c9b4bd7a (#11454)
* ⬆️ Update ggml-org/llama.cpp

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* fix(llama-cpp): rebase server patches

Adapt score output limits and TTS backend sampling to the updated llama.cpp server APIs.

Assisted-by: Codex:gpt-5.4

---------

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-11 09:52:24 +02:00
localai-org-maint-botandlocalai-org-maint-bot 129f834039 feat(gallery): add Muse Glimmer 30B variants (#11446)
Add the official dynamic and 17 GB K-quant GGUF builds with the shared multimodal projector and optional DFlash acceleration.

Assisted-by: Codex:gpt-5

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-11 09:51:58 +02:00
mudler's LocalAI [bot]andmudler 0df63de1ed chore(model-gallery): ⬆️ update checksum (#11453)
⬆️ 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-11 09:51:36 +02:00
mudler's LocalAI [bot]andmudler b08eaeeec0 chore: ⬆️ Update 0xShug0/audio.cpp to 9d6e7b39236e0151ad28a70fab0d538b84ce8718 (#11451)
⬆️ 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-11 09:51:22 +02:00
mudler's LocalAI [bot]andmudler c045a57e96 chore: ⬆️ Update CrispStrobe/CrispASR to 8b91f4c5490b88ae6193673c35644d91d8695821 (#11450)
⬆️ 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-11 09:48:07 +02:00
mudler's LocalAI [bot]andmudler 48118678ef chore: ⬆️ Update ikawrakow/ik_llama.cpp to 5763a901de3f525b65ed5574fd57623905685755 (#11452)
⬆️ 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-11 09:47:56 +02:00
localai-org-maint-botandlocalai-org-maint-bot fbe1d5036d feat(gallery): add Gemma 4 Scotoma 2 variants (#11458)
Add Q4_K_M and Q8_0 multimodal builds with the matching vision projector.

Assisted-by: Codex:gpt-5 [HF API]

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-11 09:47:40 +02:00
Richard Palethorpe 8ad22eeaef docs: remove broken stars counter (#11448)
Assisted-by: Codex:gpt-5

Signed-off-by: Richard Palethorpe <io@richiejp.com>
2026-08-10 21:58:55 +02:00
localai-org-maint-botandlocalai-org-maint-bot f7db51bdf5 feat(realtime): add shared WebRTC UDP port (#11436)
* feat(realtime): add shared WebRTC UDP port

Allow realtime WebRTC peer connections to reuse one configurable UDP mux, and surface listener bind failures through signaling.

Assisted-by: Codex:gpt-5

* test(realtime): keep UDP mux alive during bind check

The returned SettingEngine owns the UDP listener. Retain it through the duplicate-bind assertion so macOS cannot finalize the listener early and make the exclusivity check spuriously pass.

Assisted-by: Codex:gpt-5 [systematic-debugging]

* test(realtime): use IPv4 for UDP mux checks

Match the socket family used by the WebRTC UDP mux so macOS does not allocate an IPv6 probe that can coexist with the IPv4 listener.\n\nAssisted-by: Codex:gpt-5

---------

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-10 17:57:58 +02:00
localai-org-maint-botandlocalai-org-maint-bot e1b1a2564a feat(gallery): add BigBang v1 variants (#11443)
Add official multimodal GGUF builds for the new BigBang-v1 scientific and agentic reasoning model, including Q4_K_M and Q6_K variants with MTP enabled.\n\nAssisted-by: Codex:gpt-5 [Hugging Face API]

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-10 15:12:09 +02:00
mudler's LocalAI [bot]andEttore Di Giacinto da6fc8f14a feat(gallery): add minimax-h3-ref2va-q4 - the Ref2VA partition, verified coherent (#11439)
MiniMax-H3 ships two independently-served DiT partitions and each refuses the
other's tasks. The gallery had only the FL2VA one (t2va + fl2va); this adds the
Ref2VA checkpoint, which is what serves REFERENCE conditioning - a reference
image, a reference clip, or reference audio prepended as their own blocks.

It is a genuinely separate 19.9 GB download, not a mode flag, which is why it
needs its own entry. Everything except the DiT is shared with the FL2VA entry.

Q4_K_M specifically, and the description says why: the NVFP4 Ref2VA weights
render a multicolour patch grid. Three upstream investigations attributed that to
the ref2va reference-row path before the checkpoint was varied; on Q4_K_M the
identical code renders coherently (period-16 seam 1.13 against 2.28 broken,
VAE-input latent adjacent-cell cosine 0.8526 against 0.06 white). Pointing users
at NVFP4 here would hand them that dead end.

known_input_modalities carries image, video AND audio, unlike the FL2VA entry:
the ABI's ref2va slots are ref_image (PPM), ref_video (a directory of
frame_%06d.ppm) and ref_audio (16-bit PCM WAV), and the backend's
checkPartitionConditioning already routes all three.

Checksum verified two ways: sha256 computed over the downloaded 19,864,208,064
byte file, and HuggingFace's x-linked-etag for the same URI - both
17925612821ea3037ffaf5f7f9789f5460e87025385bd45e9ec6c7d536684d56.

Not verified here: an end-to-end render through LocalAI itself. The render was
proven with vllm.cpp's own driver on this exact checkpoint; the backend path is
the same library seam but has not been exercised with these weights.

Assisted-by: Claude Code:claude-opus-5 [ClaudeCode]

Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
2026-08-10 10:33:58 +02:00
dependabot[bot] 6cf2e97868 chore(deps): bump dompurify from 3.4.12 to 3.4.13 in /core/http/react-ui in the npm_and_yarn group across 1 directory (#11425)
chore(deps): bump dompurify

Bumps the npm_and_yarn group with 1 update in the /core/http/react-ui directory: [dompurify](https://github.com/cure53/DOMPurify).


Updates `dompurify` from 3.4.12 to 3.4.13
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/3.4.12...3.4.13)

---
updated-dependencies:
- dependency-name: dompurify
  dependency-version: 3.4.13
  dependency-type: direct:production
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-10 10:21:41 +02:00
localai-org-maint-botandlocalai-org-maint-bot 32bf33f7d7 feat(gallery): add Genesis Hermes V7 variants (#11435)
Add the recommended APEX, compact, MTP-enabled, and Q8_K_P builds of Qwen3.6 35B A3B Genesis Hermes V7 with its shared multimodal projector.

Assisted-by: Codex:gpt-5

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-10 10:21:25 +02:00
mudler's LocalAI [bot]andEttore Di Giacinto 7b9167eaad feat(llama-cpp): serve Qwen3-TTS through the llama.cpp backend (#11392)
* fix(config): do not read a TTS speaker-encoder mmproj as vision support

Qwen3-TTS on llama-cpp ships an mmproj holding the speaker encoder and
code predictor. VisionSupported() treated any non-empty MMProj as proof
of image input, so every such model would be advertised as vision-capable.

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

* feat(llama-cpp): add TTS request option parsing helper

Validates text and speaker reference presence and strictly parses the
top_k / top_p per-request params, in a header with no llama.cpp or gRPC
dependencies so the standalone C++ unit test gate picks it up.

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

* fix(llama-cpp): range-check the TTS top_k and top_p request params

Format validation alone let NaN, infinity and out-of-range values through.
The consumer copies both values into the audio generation input
unconditionally and only guards its separate sampler assignment with
"> 0", a test NaN also fails, so a NaN reached llama.cpp with the guard
never firing. top_k must now be >= 0 and top_p must fall within 0.0 to 1.0
inclusive, with the bound written as a negated in-range test so NaN is
rejected rather than silently accepted.

Also cover the two checks the suite could not previously kill: the
whole-string check in the float parser and the int32 range check.

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

* chore(llama-cpp): bump pin to f9e832c10 and carry the TTS server task

Picks up ggml-org/llama.cpp#26254 (Qwen3-TTS via mtmd) and #26536 (the
short-input audio chunk fix). Adds 0002-add-server-task-type-tts.patch,
the server-side half of the still-draft #26603, so TTS runs through the
slot scheduler instead of racing it. Remove that patch when #26603 merges.

The patch is rebased on top of the score patch: its tokenize-switch hunk
collided with the SERVER_TASK_TYPE_SCORE case, and its lone SRV_WRN call
passes no variadic argument, which the macro cannot expand. The score
patch itself needed no refresh.

Also fixes fallout from the bump in grpc-server.cpp: upstream dropped the
per-slot n_ctx argument from server_schema::eval_llama_cmpl_schema. Only
the schema branch loses it, since forks predating the server-schema split
still expect the old argument list.

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

* feat(llama-cpp): implement the TTS and TTSStream RPCs

Both were declared in backend.proto but unimplemented. They now submit a
SERVER_TASK_TYPE_TTS task and drain the response reader, the same shape
PredictStream uses.

The streaming path emits a leading sample_rate message and then raw PCM,
because ModelTTSStream builds the WAV header itself; the non-streaming
path emits a complete WAV to the requested dst.

The streamed samples are converted from the pipeline's float32 to signed
16-bit first. MTMD_HELPER_GEN_AUDIO_OUTTYPE_PCM hands back floats, while
the header ModelTTSStream writes announces 16-bit samples, so shipping
the floats verbatim would decode as noise.

prepare.sh and CMakeLists.txt now stage tts_request_options.h alongside
the other grpc-server helpers, and register its standalone test with
ctest the way passthrough_options_test is registered.

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

* fix(llama-cpp): mask non-codec tokens for Qwen3-TTS generation

The Qwen3-TTS gen-audio pipeline maps a sampled backbone token to a
codebook row with an unchecked subtraction, in mtmd-helper-gen.cpp:

    inp.code0 = sampled - codec_0;

For ggml-org/Qwen3-TTS-12Hz-1.7B-Base-GGUF the vocab is 155008 tokens,
<|codec_0|> is 151936 and the codec codes end at 153983. The model's own
tokenizer.ggml.suppress_tokens holds 1023 ids covering 153984..155007,
every special above the codec range except <|codec_eos_token|> (154086)
which stays reachable as the stop token. Nothing masks the text range
0..151935, so the backbone can sample a text token at any step, the
subtraction goes negative, and ggml_compute_forward_get_rows aborts the
whole backend process on GGML_ASSERT(i01 >= 0 && i01 < ne01).

Complete the mask upstream started: bias every token below <|codec_0|>
to -INFINITY for TTS tasks so only codec codes and the codec EOS remain
reachable. The biases are appended to task.params.sampling.logit_bias,
which common_sampler_init already merges with the model's suppress
tokens into one llama_sampler_init_logit_bias, so no sampler is added to
the chain. Measured cost is 0.082 ms per sampled token and 1.16 MB, set
against a forward pass in the multi-millisecond range.

It lands in launch_slot_with_task rather than in a route handler so that
llama.cpp's own POST /tts and LocalAI's TTS/TTSStream RPCs are both
covered, and <|codec_0|> is resolved from the vocab rather than
hardcoded so a model without it is left alone.

This is reproducible with upstream's own llama-tts and no LocalAI code
loaded, aborting at frame 55 on Q4_K_M and frame 71 on Q8_0, so it is
neither a quantization artifact nor an artifact of the gRPC adapter.
Two further defects in the same draft pipeline still prevent end-to-end
audio; they are independent of this one and are recorded in the task
report for an upstream bug report.

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

* chore(llama-cpp): bump pin to 9de0fcf2b and drop the TTS codec mask

Upstream fixed the Qwen3-TTS abort in ggml-org/llama.cpp c8e03ce81
("mtmd/ggml: add ggml_build_forward_order", #26649), landed one hour
after the previous pin. ggml_build_forward_expand marks a tensor and all
its ancestors for compute, so using it as a pure ordering hint defeated
ggml_build_forward_select and made GEN_WAV calls execute the GEN_CODE
branch against a stale inp_code0, hitting the get_rows bound assert in
ggml_compute_forward_get_rows.

That single defect accounts for every abort seen on this model, so
0003-mask-non-codec-tokens-for-tts.patch is removed rather than rebased.
The mask changed the observed behavior, but it was perturbing a graph
ordering bug rather than fixing a sampling one: at the new pin the whole
path works without it. Keeping it would have meant carrying a 152k-entry
logit bias, and rebasing it on every pin bump, for no benefit.

Verified at 9de0fcf2b with only 0001 and 0002 applied, which both apply
clean with no fuzz and needed no rebase:

  non-streaming  HTTP 200, 410924 bytes, 8.56 s
                 RIFF (little-endian) data, WAVE audio, Microsoft PCM,
                 16 bit, mono 24000 Hz
  streaming      HTTP 200, 560684 bytes, 11.68 s, exactly one RIFF at
                 byte 0, same format, which also exercises the
                 float32-to-s16 conversion at runtime for the first time

Pristine unpatched llama-tts at the same pin now also completes, 130
frames to a valid WAV, where it aborted at frame 55 before.

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

* fix(llama-cpp): clear the TTS slot sequence between requests

Only the first TTS request in a backend process succeeded. Every later
one failed instantly, in about 0.13 s, with "TTS prompt processing
failed" from step_prompt, regardless of streaming or non-streaming and
regardless of the text. With LOCALAI_SINGLE_ACTIVE_BACKEND=true the
process is kept alive between requests, so a deployment would have
served exactly one utterance per backend start.

The cause is missing KV hygiene, not anything in the gRPC adapter. TTS
slots never enter the shared batch: pre_decode() returns early for them
and process_tts_slots() drives them instead, so they skip the
prompt-cache bookkeeping that clears a slot's sequence between requests.
Nothing in the gen-audio path makes up for it: mtmd_helper_gen_audio_reset
only clears host-side buffers, and the pipeline always decodes from
position 0 into the sequence identified by slot.id. So the second task
on a slot writes positions 0..N over the first task's tokens and
llama_decode fails.

Fix is one call to slot.prompt_clear(), the same helper the normal path
uses, in the SERVER_TASK_TYPE_TTS branch of launch_slot_with_task before
set_input. It goes into 0002 rather than a new patch file because it is
a defect in the code that patch introduces, and the header now records
it as ours so we know whether it still needs carrying if #26603 merges
without it.

Verified in one backend process, different text on every request:
three consecutive non-streaming requests, three consecutive streaming
requests, and an interleaved non-streaming, streaming, non-streaming,
streaming run. All ten returned HTTP 200 with
RIFF ... WAVE audio, Microsoft PCM, 16 bit, mono 24000 Hz, the streamed
ones carrying exactly one RIFF header at byte 0, and every output
measured as real speech rather than silence or a truncated fragment.

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

* feat(llama-cpp): expose max_frames for TTS requests

The Qwen3-TTS backbone does not always emit <|codec_eos_token|>, and
when it does not, generation runs to upstream's 512-frame n_predict
default. At the model's 12.5 Hz frame rate that is 40.96 s of audio,
which a short input can trigger: one request in this session produced
40.96 s for a ten-word sentence. prepareTTSTask hardcoded n_predict to
-1, so callers had no way to bound it.

Add a max_frames key alongside top_k and top_p, parsed with the same
strict whole-string parsing so a typo is an error rather than a silently
truncated value, and rejected with a field-naming message when negative.
0 keeps the existing sentinel convention and means unset, so a request
that omits it behaves exactly as before.

Named max_frames rather than n_predict because frames are what the
parameter means at a TTS endpoint: one frame is 0.08 s of audio.

The 512-frame default is deliberately unchanged. Lowering it would
truncate legitimately long inputs, which is a worse failure than an
occasionally overlong one.

Verified end to end on one text of thirty words:

  max_frames=25    HTTP 200,  96044 bytes,  2.00 s, exactly 25 frames
  max_frames=50    HTTP 200, 192044 bytes,  4.00 s, exactly 50 frames
  no max_frames    HTTP 200, 572204 bytes, 11.92 s, stopped at its own
                   codec EOS after 149 frames, unchanged behavior

  max_frames=-1    InvalidArgument "max_frames must be >= 0, got \"-1\""
  max_frames=many  InvalidArgument "max_frames must be an integer, got \"many\""

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

* fix(llama-cpp): send the TTS sample rate up front, and tidy three review items

Four items from the Task 4 review.

Streaming first-byte latency. TTSStream sent the sample-rate reply only
once the first audio result arrived, and a chunk needs a whole 72-frame
window, roughly 5.8 s of audio and far longer in wall time on CPU. The
Go side blocks on that reply before it can emit the WAV header, so a
streaming client sat at zero bytes for the whole stretch. The rate is a
property of the loaded model and is available synchronously from
mtmd_gen_audio_get_info, so it now goes out immediately after post_task
and the rate_sent bookkeeping is gone. Measured on a warm model, first
byte drops from 30.48 s to 0.014 s, and the output is still a valid WAV
with exactly one RIFF header at byte 0.

Unchecked close. The non-streaming path ignored ofstream::close(), so a
failure that only surfaces on flush was reported as success while
leaving a truncated file at dst. It now returns INTERNAL like the other
write failures.

Wrong comment on set_lang. gen_audio::inp::get() already maps a stored
blank to nullptr, so our guard is behavior-preserving, not
behavior-fixing. The comment claimed otherwise; the code was right.

Repetition penalty. penalty_last_n = -1 is inert at this pin, because
llama_sampler_init_penalties clamps it with std::max(penalty_last_n, 0)
and then builds a disabled sampler, so the 1.05 penalty never applies.
Upstream's README attributes looping to a missing repeat_penalty, so it
was worth testing as a root-cause fix for the model running to the frame
cap. Dropping the line lets the sampling default of 64 apply, which was
confirmed in the sampler chain trace as penalty_last_n = 64 with
repeat_penalty = 1.050. Over 15 uncapped short requests each way it did
not help: 0 of 15 ran to the cap with the penalty inert, 1 of 15 with it
active. Both lines are therefore kept for parity with upstream's draft,
and a comment now records that the pair is inert and why, so the next
reader does not believe a penalty is applied. max_frames remains the way
to bound output.

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

* build(llama-cpp): let unpatched forks opt out of the TTS task

turboquant and bonsai copy grpc-server.cpp into llama.cpp forks that do
not carry our patches. disable-tts-task.sh injects the same kind of
preprocessor switch disable-score-task.sh already uses, so those builds
answer UNIMPLEMENTED rather than failing to compile.

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

* fix(config): keep a TTS speaker-encoder projector out of vision detection

Task 1 exempted a declared-TTS model's mmproj from VisionSupported, but the
first real gallery entry with an mmproj still came back vision-capable through
two paths the earlier fix did not close.

GuessUsecases has no FLAG_VISION branch, so it falls through to true for any
chat-ish model. That is not just a wrong answer at the call site:
syncKnownUsecasesFromString rewrites KnownUsecaseStrings from HasUsecases, and
the loader calls it more than once per config file, so the guessed FLAG_VISION
is written out and parsed back into KnownUsecases as if the operator had
declared it. Give GuessUsecases a FLAG_VISION branch that defers to the same
explicit signals VisionSupported uses.

Second, llama.cpp builds an mtmd context for the speaker-encoder projector and
reports its media marker on the first chat probe, which resurrected vision
after the model had been used once. Apply the same declared-TTS exemption to
MediaMarker that the mmproj check already had.

Verified against the qwen3-tts-llamacpp-q4 gallery entry: no vision capability
and no image input modality, before load, after a TTS request, and after a chat
probe.

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

* feat(gallery): add Qwen3-TTS entries for the llama-cpp backend

Two entries over upstream's own GGUF conversion, Q8_0 and Q4_K_M, each
pairing a backbone with the Q8_0 projector. Named to sit alongside the
existing qwen3-tts-cpp entries rather than replace them.

Also tags the llama-cpp backend text-to-speech / TTS so the backend browser
surfaces the capability.

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

* docs: cover Qwen3-TTS on the llama-cpp backend

Adds the gallery variants, the two-file mmproj configuration, the
required voice reference, and the language and sampling knobs. Also
corrects the streaming-support list, which named only voxcpm.

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

* fix(config): register llama-cpp as a TTS and voice-cloning backend

The branch taught the llama-cpp backend to serve Qwen3-TTS and shipped two
gallery entries for it, but never told the capability table. llama-cpp still
declared only the text RPCs and usecases, so:

- VoiceCloningForModel returned nil at the capability check, before it ever
  reached the model's own tts.voice_cloning override, and /tts answered 400
  "selected model does not support reference-audio voice cloning" for any
  localai://voice-profiles/... voice. No model YAML could opt back in.
- GET /api/backends/usecases did not list tts for llama-cpp, so the gallery
  greyed out the TTS filter for the entries this branch adds.
- The React TTS page saw voice_cloning: null and kept both models out of the
  Voice Library.

Add the TTS RPCs and usecase, and the reference-audio contract.

The contract needs narrowing, because the per-backend switch in
VoiceCloningForModel ends in a permissive default: an unnarrowed entry would
have advertised reference-audio cloning on every GGUF chat model in the
gallery. Narrow on the declared TTS usecase rather than the model name. The
TTS checkpoints are the only llama-cpp models carrying known_usecases: [tts];
name matching would have to guess at third-party repacks, and "base", the
substring the neighbouring Qwen and vLLM cases key on, is a routine word in
text-model names. The check reads the declared bit directly instead of going
through HasUsecases, which falls through to GuessUsecases and would hand the
decision to a heuristic that never had a llama.cpp TTS model in mind.

DefaultUsecases stays [chat]: a bare GGUF served by llama.cpp is a chat model,
and both the gallery filter and the importer read that field.

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

* fix(gallery): declare what nemotron-3-nano-omni actually accepts

The entry is backend: vllm-omni with known_usecases: [chat, completion], no
mmproj and no media marker, so it used to report vision only through the
blanket GuessUsecases fallthrough that the vision branch in this branch
removed. Nemotron 3 Nano Omni is a multimodal understanding model: image,
video and audio in, text out. Declaring that is what the sibling
vllm-omni-qwen3-omni-30b already does.

known_usecases gains vision only. FLAG_VIDEO is video GENERATION, an output
modality, and this model generates none; video and audio input belong in
known_input_modalities, which is where AudioInputSupported and
VideoInputSupported read them from.

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

* fix(importers): import a Qwen3-TTS GGUF repo as TTS, not chat

The llama-cpp importer hardcodes known_usecases: [chat] and assigns any
mmproj-matching file as a vision projector, so ggml-org/Qwen3-TTS-12Hz-1.7B-
Base-GGUF imported as a chat model with vision. Both fields were wrong, and
the model was unreachable from /tts and from the Voice Library.

Filenames cannot fix this. A Qwen3-TTS repo has the exact shape of a vision
repo, one backbone GGUF plus one mmproj-*.gguf, so the projector's own header
is the only honest signal: mtmd writes clip.has_gen_audio_encoder for the
projectors it can drive as a speech pipeline and refuses to build one without
it. Probe the selected mmproj for that flag, reusing the range-fetch the MTP
detection already does, and declare tts when it is set. The mmproj assignment
then stops reading as vision on its own, since a declared-TTS model already
exempts its projector from vision detection.

The probe is best-effort like the MTP one: a network blip leaves the chat
default in place rather than failing the import.

Verified against the real artifacts on disk: the Qwen3-TTS projector reports
gen-audio, its backbone does not.

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

* fix(llama-cpp): stop non-TTS models crashing on the new pin

Two regressions, both hit every ordinary llama-cpp model and neither was
caught locally because every test on this branch loaded a TTS model.

The first is a null dereference. server_slot::tts_ctx::reset() called
mtmd_helper_gen_audio_reset() unconditionally, but the gen-audio pipeline
is only allocated for models carrying a gen-audio mmproj, and upstream's
implementation reads ctx->pipeline before null-checking anything. Since
server_slot::reset() runs during slot initialization for every model, any
non-TTS model segfaulted the backend the moment it loaded. Guard the call
on the is_supported() predicate already defined beside it, and keep the
plain field resets unconditional.

The second is unrelated to TTS and came in with the pin bump.
PredictOptions.Penalty is a bare proto float, so a caller that names no
repetition penalty sends 0 rather than omitting the field. Since
9de0fcf2b, common_sampler_init() rejects a non-positive penalty_repeat
outright because it would divide logits by zero, turning every such
request into "Failed to initialize samplers". Treat 0 as unset and leave
llama.cpp's own neutral default in place.

Verified with the same suite CI runs, which is what caught both:
tests/e2e-backends passes 6 of 6 including the load and predict specs
that were red. Qwen3-TTS still synthesises on both paths, 24 kHz mono
16-bit WAV with exactly one RIFF header on the streamed output.

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

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
2026-08-10 10:18:47 +02:00
localai-org-maint-botandlocalai-org-maint-bot 7a22586399 feat(gallery): add Shieldstral 1.0 3B variants (#11441)
Add Q4_K_M and Q8_0 llama.cpp builds of Mistral AI’s multimodal safety classifier, including the shared Pixtral projector.

Assisted-by: Codex:gpt-5

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-10 10:17:44 +02:00
localai-org-maint-botandlocalai-org-maint-bot 7ef3c6f786 fix(vllm): align Intel basekit runtime (#11437)
* fix(vllm): align Intel basekit runtime

The latest vLLM XPU requirements install oneAPI 2026 runtime packages. The 2025.3.0 base image ships an older libsycl/UR loader pair and fails while importing torch with an undefined urDeviceWaitExp symbol. Use the current repository-wide 2025.3.2 Intel basekit patch level, which carries the compatible loader.

Assisted-by: Codex:gpt-5 [systematic-debugging]

* fix(vllm): pin Intel source build to release

Build the Intel XPU backend from vLLM 0.26.0 instead of the moving main branch, and use the Triton XPU version required by that release's torch 2.12 dependency.

Assisted-by: Codex:gpt-5 [systematic-debugging]

---------

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-10 09:44:52 +02:00
localai-org-maint-botandlocalai-org-maint-bot 2b62f1815f feat(gallery): add Grug 12B variants (#11438)
Add Q4_K_M and Q8_0 llama.cpp builds of the compact-reasoning Gemma 4 fine-tune, including its multimodal projector.

Assisted-by: Codex:gpt-5

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-10 09:44:38 +02:00
mudler's LocalAI [bot]andmudler 68e2b127b1 chore: ⬆️ Update CrispStrobe/CrispASR to 8898b0fee199b1765321a5d0d8314aee51a34583 (#11422)
⬆️ 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-10 08:13:19 +02:00
mudler's LocalAI [bot]andmudler f129a76cc4 chore: ⬆️ Update 0xShug0/audio.cpp to 748c5e28f6a7228b8f38ad7142ca97d29584544b (#11401)
⬆️ 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-10 00:48:23 +02:00
localai-org-maint-botandlocalai-org-maint-bot daa8d2adbd fix(gallery): identify invalid preload JSON (#11434)
Wrap PRELOAD_MODELS decoding failures with the setting name and expected top-level shape so startup errors point directly to the invalid configuration. Document the required array format and cover scalar and empty-array inputs.

Assisted-by: Codex:gpt-5

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-10 00:18:01 +02:00
mudler's LocalAI [bot]andmudler e9cfc2d284 chore(model-gallery): ⬆️ update checksum (#11433)
⬆️ 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-09 23:26:55 +02:00
mudler's LocalAI [bot]andmudler 1f5dbe8ffc chore: ⬆️ Update ikawrakow/ik_llama.cpp to a7c81affa48c6800d63111bdb33469a01d062daa (#11431)
⬆️ 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-09 22:56:28 +02:00
mudler's LocalAI [bot]andmudler 1ffc68a153 chore: ⬆️ Update antirez/ds4 to 84cc882352757baf628a1776badf7cc54d584e28 (#11432)
⬆️ 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-09 22:56:12 +02:00
localai-org-maint-botandlocalai-org-maint-bot 06ff56e674 feat(pii): restore request-scoped pseudonyms (#11272)
* feat(pii): restore request-scoped pseudonyms

Replace masked request values with unique per-request tokens when response restoration is enabled, then restore them across JSON and SSE write boundaries. Document the opt-in model setting and expose it in config metadata.\n\nAssisted-by: Codex:gpt-5

* fix(pii): wrap reversible redaction tokens

Use configurable token delimiters to avoid restoring ordinary model text that happens to match an internal identifier. Rename the option and document the confidentiality tradeoff.

Assisted-by: Codex:gpt-5

---------

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-08-09 22:37:13 +02:00
mudler's LocalAI [bot]andEttore Di Giacinto a0f50b2af2 feat(vllm-cpp): serve MiniMax-H3 video+audio generation (#11424)
* feat(vllm-cpp): serve MiniMax-H3 video+audio generation

vllm.cpp's C ABI grew a video slice (ABI v12): a second engine handle
loaded from the MiniMax-H3 checkpoint SET, one blocking generate, and a
composed ffmpeg argv the caller execs. This wires that into LocalAI's
existing /video endpoint, so `vllm-cpp` now serves both text and video
and a clip comes back as an MP4 with a real audio track rather than a
silent render.

The video engine is a separate handle rather than a mode of the text
one because H3 is not a model directory: the DiT, the text encoder and
two VAEs are separate artifacts, and vllm.cpp has the two loaders refuse
each other's checkpoints. `Load` takes the video branch when the config
declares any of the video options; `parameters.model` is the DiT and the
rest of the set is named in `options:`.

Three details are worth calling out because getting them wrong is
expensive:

- The partition is DECLARED, not detected. The community quantisations
  strip the release metadata and the FL2VA and Ref2VA DiTs are
  byte-structurally identical, so the engine refuses to generate until
  it is told which it has. Worse, a mismatch does not fail cleanly: a
  reference passed to an FL2VA DiT renders for hours and returns a
  coloured lattice over the frame. The backend refuses that combination
  up front instead.
- ffmpeg comes from the host. libvllm writes frames plus a WAV and
  composes the mux argv, then spawns nothing - that process boundary is
  upstream's decision. The backend execs it, the same arrangement
  vibevoice-cpp uses for transcoding, and ffmpeg also converts a
  start_image upload into the binary PPM at the exact output canvas the
  engine requires.
- It is slow. Roughly 176 s per denoise step at the default 1344x768
  canvas on a 20-SM device, so the 50-step default is a multi-hour job.
  Nothing on this path imposes a deadline.

The /video endpoint no longer forces 512x512 when the request omits the
geometry. Every video backend already supplies its own default for a
zero (512x512 for stablediffusion-ggml, 1280x720 for diffusers, 832x480
for longcat-video, 1344x768 for H3), so the hardcoded value only ever
overrode the model's trained canvas with one three of the four were
never trained at.

Moving the engine pin from ABI v10 to v16 also grows the text
vllm_model_params mirror by the v14 device field and the v16 KV-sizing
knobs. LocalAI sets none of them - 0 is the pre-v14 engine byte for byte
- but the struct SIZE is part of the layout contract, so leaving them
out would have vllm_engine_load read past the allocation.

Gallery: `minimax-h3-fl2va-q4` installs the Q4_K_M FL2VA set (~40 GB
across five weight files plus the two VAE configs that carry the latent
statistics).

Assisted-by: Claude:claude-opus-5 golangci-lint yamllint go-vet

* fix(vllm-cpp): unbreak the Darwin build at the new engine pin

src/capi/vllm_c.cpp opens one `extern "C" {` for the whole ABI surface,
so file-local helpers declared inside it inherit C linkage. The video
slice added one that returns std::string, which Apple Clang reports as
-Wreturn-type-c-linkage and vllm.cpp's target-local -Werror turns into a
build failure. GCC and upstream Clang do not diagnose it, so only the
metal-darwin-arm64 job saw it.

Suppress it the same way this Makefile already suppresses Apple Clang's
-Wgnu-folding-constant on the Metal build. The helper is never called
across the boundary so the warning describes no hazard here, but it is a
real upstream wart: the fix belongs in vllm.cpp, hoisting the helper
above the extern "C" block, and this flag should go when a pin carrying
that fix lands.

Assisted-by: Claude:claude-opus-5

* fix(vllm-cpp): patch the engine clone instead of the warning flag

The -Wno-return-type-c-linkage added in the previous commit does nothing.
vllm_cpp_set_warnings adds `-Wall -Wextra -Werror` as PRIVATE target
options, so they land after anything CMAKE_CXX_FLAGS contributes, and
-Wall re-enables the -Wreturn-type group that -Wreturn-type-c-linkage
belongs to. The darwin job failed again on the same line, which is the
evidence: a consumer cannot wave this off from outside the engine.

Position is the only fix, so carry it as a patch against the pinned SHA,
the way longcat-video patches its own upstream. It hoists the helper
above the `extern "C" {` that gives it C linkage; it is file-local and
never called across the boundary, so nothing else moves.

`git apply` is unguarded on purpose: a patch that stops applying must
fail the clone loudly, because the alternative is a pin that silently
ships without a fix it is documented to carry. The patch header names
what retires it - a pin carrying the fix upstream, where it belongs.

Verified by applying the patch with `git apply` to the exact blob at the
pinned SHA and diffing the result against the intended file.

Assisted-by: Claude:claude-opus-5

* chore(vllm-cpp): bump the engine pin to ABI v17 and drop the vendored OrEmpty patch

The OrEmpty linkage fix this backend carried as patches/0001-* landed upstream
(mudler/vllm.cpp#195, 7534da65), so the patch has done its job. It is deleted
rather than left in place: the Makefile applies patches/*.patch unguarded and
documents that "a patch that no longer applies must FAIL the clone", so keeping
it against fixed source would break the build the moment the pin moved. Bumping
the pin and deleting the patch therefore have to be the SAME change.

Pin f921062b -> 776c56f1 (current vllm.cpp main).

That range also carries the engine's ABI v17 (vllm_server_main: the OpenAI server
published on the public surface). registerLib compares the library's
vllm_abi_version against `abiVersion` for EXACT equality, so the constant moves
16 -> 17 in the same commit or every load fails with an ABI mismatch.

The bump is safe for the layout assertions in video_test.go: diffing include/vllm.h
across the two pins shows zero struct-field changes -- v17 adds one function
declaration, the version macro and a doc comment, nothing else -- so every
unsafe.Offsetof in the video params test still holds.

Assisted-by: Claude Code:claude-opus-5 [ClaudeCode]

* chore(vllm-cpp): re-pin to pick up the VLLM_CPP_SERVER=OFF link fix

The previous pin carried vllm.cpp's ABI v17 (vllm_server_main) but not the guard
that makes it link when the server is compiled out. This backend builds libvllm
with VLLM_CPP_SERVER off, so the darwin lane failed at the dylib link with
vllm::entrypoints::openai::VllmServerMain undefined.

Fixed upstream in mudler/vllm.cpp#202: the C entry point is now guarded, so the
symbol is still exported (ABI v17 stays resolvable for dlopen) while the
no-server arm reports the missing capability instead of dragging in a translation
unit that was never compiled.

Verified upstream in BOTH arms before re-pinning: SERVER=ON builds and runs, and
SERVER=OFF configures, links, produces libvllm.so, and `nm -D` shows
vllm_server_main exported next to vllm_video_generate and vllm_transcribe.

Assisted-by: Claude Code:claude-opus-5 [ClaudeCode]

---------

Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
2026-08-09 22:34:51 +02:00
Matheus C. França f31c3bbf1b feat(i18n): add pt-BR translation (#11427)
Adds a complete Brazilian Portuguese (pt-BR) translation for the
LocalAI WebUI across 14 namespaces with full key parity against the
English locale, including modelEditor.json. Registers pt-BR in
SUPPORTED_LANGUAGES with the code 'pt-BR', name 'Português (Brasil)'
and flag 'BR'. Brand/model/product names and technical identifiers are
kept untranslated, matching the existing locale conventions.

Assisted-by: opencode:deepseek-v4-flash-free python3

Signed-off-by: Matheus C. França <matheus-catarino@hotmail.com>
2026-08-09 22:34:19 +02:00
mudler's LocalAI [bot]andmudler 1f30ecc398 chore(model-gallery): ⬆️ update checksum (#11423)
⬆️ 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-08 23:04:23 +02:00
mudler's LocalAI [bot]andmudler 6c4acece2a chore: ⬆️ Update ikawrakow/ik_llama.cpp to f2328aa0c19954d0ab31a3de60fbf50e47c2429f (#11421)
⬆️ 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-08 23:04:08 +02:00
mudler's LocalAI [bot]andmudler ea9f4f5bc5 chore: ⬆️ Update CrispStrobe/CrispASR to 17a6cc99422bfafadf7161e96dd7294c89da9c36 (#11404)
⬆️ 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-08 08:23:40 +02:00
mudler's LocalAI [bot]andmudler 53637e5397 docs: ⬆️ update docs version mudler/LocalAI (#11415)
⬆️ Update docs version mudler/LocalAI

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-08 08:23:24 +02:00
Copilotandmudler c5645795ba fix(kokoros): add missing upscale_image stub to Backend trait impl (#11414)
* Initial plan

* fix(kokoros): add missing upscale_image stub to Backend impl

Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-08-08 08:23:05 +02:00
mudler's LocalAI [bot]andmudler 7047ae7210 chore: ⬆️ Update ikawrakow/ik_llama.cpp to 40dffce6857b4fe051f096379dc464764c718458 (#11403)
⬆️ 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-08 08:22:34 +02:00
mudler's LocalAI [bot]andmudler 40343ebeed chore(model-gallery): ⬆️ update checksum (#11418)
⬆️ 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-08 08:22:16 +02:00
mudler's LocalAI [bot]andmudler 18041b615e chore: ⬆️ Update ggml-org/whisper.cpp to 592feef04a1802b18cbeffd0fd0eb5d02570c2ec (#11416)
⬆️ 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-08 08:21:59 +02:00
Adiraandlocalai-org-maint-bot ab52813342 feat(modelartifacts): support bounded parallel Hugging Face file downloads (#11162)
* feat(modelartifacts): support bounded parallel Hugging Face file downloads

Closes #11114.

Snapshot materialization fetched every file through the sequential
executor in DownloadFilesWithContext, so a repository split into many
shards spent most of its wall clock in per-file request latency rather
than moving bytes.

Add DownloadFilesWithConcurrency, an errgroup with SetLimit, and keep
DownloadFilesWithContext as a wrapper that passes a limit of 1. That
leaves the two non-artifact callers (core/gallery and the model config
loader) on exactly the path they had: tasks still run in slice order,
and the first failure still returns before any later task starts.

Only whole files run in parallel. A single file is never split, so the
.partial resume machinery and the per-file SHA check in
downloadTaskWithRetry are untouched.

Two details the parallel path forced:

- completedBytes becomes an atomic.Int64. Several AfterDownload hooks
  add to it while other files' progress callbacks read it; without this
  the race detector reports three races on the new specs.
- The caller's status callback is serialized. The sequential path gave
  it an implicit guarantee of never being entered twice at once, and it
  belongs to the caller, so the executor keeps that promise rather than
  pushing locking onto every caller. AfterDownload is deliberately not
  serialized -- it does the verify-and-promote work that parallelism
  exists to overlap.

Manifest order needed no work: each hook already writes its own
manifest.Files slot by snapshot index, so entries stay in snapshot
order whatever the completion order. A spec now pins that.

The default is 1, unchanged behaviour. A shared models volume is often
the bottleneck rather than the link, so raising it is a deployment
decision; --artifact-download-concurrency and
LOCALAI_ARTIFACT_DOWNLOAD_CONCURRENCY expose it on both `run` and
`models install`.

Not done here, per the issue: no chunk-level parallelism within a single
file, and no throughput measurements across concurrency 1/2/4/8 -- that
needs a representative sharded repo and a real link.

Assisted-by: Claude:claude-opus-5 go-test gofmt
Signed-off-by: Adira Denis Muhando <dennisadira@gmail.com>

* feat(modelartifacts): expose download concurrency in settings

Follow-up to review feedback on #11162:

- The CLI flag and docs no longer describe the limit as Hugging Face
  specific. It applies to any artifact source, as @mudler pointed out.
- artifact_download_concurrency is now a persisted runtime setting and
  is editable from the WebUI, so it can be changed without a restart.

The manager's limit becomes an atomic.Int64 behind
SetDownloadConcurrency, because a live runtime setting can be updated
while a materialization is already in flight. Injected materializers
stay compatible through an optional setter interface, so a manager that
does not implement it is simply left alone.

Verified before taking this on: go build, go vet and go test -race all
pass for pkg/modelartifacts, pkg/downloader and core/config. The React
UI builds with vite, artifact_download_concurrency is present in the
built Settings chunk, and eslint reports the same 8 pre-existing
warnings on Settings.jsx as it does without the change.

Implementation contributed by localai-org-maint-bot on the review
thread; reviewed, verified and signed off by me.

Assisted-by: Codex:gpt-5
Assisted-by: Claude:claude-opus-5 go-test vite eslint
Signed-off-by: Adira Denis Muhando <dennisadira@gmail.com>

---------

Signed-off-by: Adira Denis Muhando <dennisadira@gmail.com>
Co-authored-by: localai-org-maint-bot <bot-opensource@localaisrl.com>
2026-08-07 18:00:45 +02:00
702 changed files with 56494 additions and 6403 deletions

No files matched your search

+34
View File
@@ -49,6 +49,40 @@ AI agents MUST NOT add `Co-Authored-By` trailers for themselves either.
A human reviewer owns the contribution; the AI's involvement is recorded
via `Assisted-by` (see below).
### Exception: automation operated by a maintainer
The rule above addresses the common case, an AI assistant helping a human
contributor who then signs off. It does not fit automation that a
maintainer runs themselves, which opens pull requests with no human
submitter to sign. Applied literally there, nothing ever signs and the
DCO check blocks the pull request permanently.
A maintainer-operated bot MUST therefore add a `Signed-off-by` trailer
naming **the maintainer who operates it**, not the bot and not the model:
```
Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
```
This is not the AI certifying the DCO. The maintainer is, exactly as they
do for a commit they typed by hand: they configured the automation, they
own its output, and they take responsibility for it when they merge it.
The `Assisted-by` trailer still records that a model produced the code, so
the provenance trail is unchanged.
The exception is narrow and does not widen the rule for anyone else:
- It applies only to automation a LocalAI maintainer operates and whose
output that maintainer reviews before merge.
- The sign-off names a real person who accepts DCO responsibility.
- An AI assistant helping an outside contributor still MUST NOT sign off.
That contributor adds their own trailer.
- A bot MUST NOT sign off on behalf of anyone other than its operator, and
MUST NOT add a trailer for a contributor whose branch it pushes to. If
automation contributes to someone else's branch, it leaves the sign-off
to that contributor.
## Attribution
When AI tools contribute to LocalAI development, proper attribution helps
+52
View File
@@ -236,6 +236,58 @@ Use these HTTP status codes:
If your endpoint should be tracked for usage (token counts, request counts), add the `usageMiddleware` to its middleware chain. See `core/http/middleware/usage.go` and how it's applied in `routes/openai.go`.
## Control-plane database health metrics
In distributed mode the frontend registers three OpenTelemetry gauges over the
PostgreSQL control-plane database (`core/services/monitoring/control_plane_db.go`,
wired in `core/application/distributed.go`). They reach `/metrics` through the
same Prometheus exporter as the rest of the API metrics.
| Metric | Meaning | Page when |
|--------|---------|-----------|
| `localai_control_plane_oldest_xmin_age` | Transactions elapsed since the oldest snapshot any backend still holds | above a few million, and rising |
| `localai_control_plane_longest_transaction_seconds` | Age of the longest open transaction | above 3600 |
| `localai_control_plane_dead_tuple_ratio` | Dead tuples per live tuple, labelled by `table`, on `backend_nodes`, `node_models` and `gallery_operations` | sustained above ~10 on a small table |
A sustained high `localai_control_plane_oldest_xmin_age` is the one to page on.
While it grows, autovacuum can reclaim nothing anywhere in the database no
matter how often it runs, so the dead tuple ratio keeps climbing and a six-row
registry table can reach hundreds of megabytes. Tuning autovacuum does not help.
The fix is to find the transaction holding the horizon open and clear it:
```sql
SELECT pid, state, age(backend_xmin) AS xmin_age, now() - xact_start AS xact_age, query
FROM pg_stat_activity
WHERE backend_xmin IS NOT NULL
ORDER BY age(backend_xmin) DESC;
```
Then `pg_terminate_backend(pid)` on the offenders, and `VACUUM (VERBOSE)` the
bloated tables once the horizon has moved.
**A healthy-looking xmin age does not on its own prove the horizon is free.**
The gauge reads `pg_stat_activity`, which only sees live backends. Two other
things pin the very same horizon and are invisible there, so either one can hold
vacuum back while the gauge reads 0:
```sql
SELECT gid, prepared, database, transaction FROM pg_prepared_xacts;
SELECT slot_name, active, xmin, catalog_xmin FROM pg_replication_slots;
```
An orphaned prepared transaction is cleared with `ROLLBACK PREPARED '<gid>'`,
and a stale slot with `pg_drop_replication_slot('<slot_name>')`. Check both
before concluding that a bloated table has some other cause.
Sampling is scrape-driven behind a 30 second cache, so scrape frequency does not
translate into database load. Failed and timed-out samples cost the same interval
as successful ones, so a database that is already struggling is not retried on
every scrape. A failed sample reports the last good values rather than failing the
scrape, because these gauges matter most when the database is struggling. Before
the first successful sample the gauges are absent rather than zero, since a zero
xmin age would read as a healthy horizon: alert on `absent()` too if you need to
distinguish "healthy" from "never sampled".
## Advertising surfaces — where to register a new capability
Beyond routing and auth, LocalAI publishes its capability surface in **four independent places**. When you add an endpoint — especially one introducing a net-new capability like a new media type or a new auth-gated feature — you must update every relevant surface. These aren't optional: missing them means the endpoint works but is invisible to clients, admins, and the UI.
+50
View File
@@ -77,6 +77,56 @@ spectrum. **Metal (Darwin) only** - it is a no-op on CUDA/CPU. Enable with
budget). Gallery entries built on this: `deepseek-v4-flash-q4-ssd` (153 GB Flash
on a 128 GB Mac) and `deepseek-v4-pro-q2-ssd` (433 GB Pro, experimental).
## CUDA architecture (do not build without one)
`backend/cpp/ds4/Makefile` drives upstream's **object targets** directly
(`$(MAKE) -C ds4 ds4.o ds4_cuda.o ...`), which bypasses upstream's own guard:
its `cuda` target refuses to build unless `CUDA_ARCH` is set, and offers
`cuda-spark` (sm_121, DGX Spark / GB10) and `cuda-generic` (native) instead.
Built with no `-arch`, nvcc targets its default architecture and the kernels run
as JIT'd PTX. On GB10 that silently corrupted every prefill batch of >=128
tokens - the model emitted text unrelated to the prompt and never closed its
thinking block, so `content` came back empty - and cost close to two orders of
magnitude of prefill throughput (4.21 t/s vs 325.70 t/s, same box, same model).
Short prompts stayed correct, which is why it went unnoticed.
The Makefile therefore picks a gencode list from `CUDA_MAJOR_VERSION` (a build
arg the backend matrix already declares, forwarded by `Dockerfile.ds4`) and
`uname -m`, and passes it as `NVCC_ARCH_FLAGS` to the sub-make. Upstream's
`CUDA_ARCH` accepts a single value, so it cannot express the fat binary the
shipped images need; a command-line assignment beats its `:=`. An empty
`CUDA_MAJOR_VERSION` falls back to upstream's `native` for local developer
builds, and an unrecognised one is a hard error - no CI runner has a GPU, so a
silent `native` there is exactly the failure mode this guards against.
`DS4_CUDA_HAVE_MXF4` is deliberately unset: upstream defines it only for
single-arch sm_120/sm_121 builds and guards it with a plain `#ifdef` rather than
`__CUDA_ARCH__`, so it cannot be combined with older archs. It gates an optional
MXFP4 indexer fast path whose `#ifndef` branch returns 0, so omitting it costs
speed, not correctness.
### Verifying a build
Check which flags a configuration resolves to, without compiling anything:
```
make -C backend/cpp/ds4 BUILD_TYPE=cublas CUDA_MAJOR_VERSION=13 NATIVE=false \
--eval='show: ; @echo [$(DS4_ARCH_MAKEVARS)]' show
```
Do not use `make -n` for this: the recipe is `+$(MAKE) ...`, and the `+` prefix
makes it run even under `-n`.
Then exercise the failure mode itself against a built backend. It only appears
above one prefill batch, so the ordinary `predict` spec cannot catch it:
```
BACKEND_BINARY=$(pwd)/backend/cpp/ds4/package/run.sh \
BACKEND_TEST_MODEL_FILE=/path/to/ds4flash.gguf \
BACKEND_TEST_CAPS=health,load,predict,long_prefill \
go test -count=1 -timeout=30m -v ./tests/e2e-backends/...
```
## Build matrix
| Build | Where | Notes |
+2
View File
@@ -59,7 +59,9 @@ backend/rust/*/target
backend-images
local-backends
local-ai
.claude
.crush
.tools
protoc
tests
+1 -1
View File
@@ -5,7 +5,7 @@ This PR fixes #
**Notes for Reviewers**
**[Signed commits](../CONTRIBUTING.md#signing-off-on-commits-developer-certificate-of-origin)**
**[Signed commits](../CONTRIBUTING.md#commit-messages)**
- [ ] Yes, I signed my commits.
- [ ] Documentation updated (docs/content/) for user-facing changes, or not applicable
+14 -1
View File
@@ -2584,7 +2584,7 @@ include:
tag-latest: 'auto'
tag-suffix: '-gpu-intel-vllm'
runs-on: 'ubuntu-latest'
base-image: "intel/oneapi-basekit:2025.3.0-0-devel-ubuntu24.04"
base-image: "intel/oneapi-basekit:2025.3.2-0-devel-ubuntu24.04"
skip-drivers: 'false'
backend: "vllm"
dockerfile: "./backend/Dockerfile.python"
@@ -3754,6 +3754,19 @@ include:
dockerfile: "./backend/Dockerfile.golang"
context: "./"
ubuntu-version: '2404'
- build-type: 'hipblas'
cuda-major-version: ""
cuda-minor-version: ""
platforms: 'linux/amd64'
tag-latest: 'auto'
tag-suffix: '-gpu-rocm-hipblas-stablediffusion-ggml'
runs-on: 'ubuntu-latest'
base-image: "rocm/dev-ubuntu-24.04:7.2.1"
skip-drivers: 'false'
backend: "stablediffusion-ggml"
dockerfile: "./backend/Dockerfile.golang"
context: "./"
ubuntu-version: '2404'
- build-type: 'sycl_f16'
cuda-major-version: ""
cuda-minor-version: ""
+15 -10
View File
@@ -3,9 +3,9 @@
# darwin (Apple Silicon) install path. The macOS/Metal build
# (backend/python/vllm/install.sh, Darwin branch) installs vllm-metal, which is
# version-locked to a specific vLLM source release. install.sh derives that vLLM
# version at build time from vllm-metal's own installer (`vllm_v=`) at the pinned
# tag, so there is only ONE value to bump here -- mirroring bump_vllm_wheel.sh,
# which bumps the Linux cu130 wheel pin.
# version, and the wheel asset name, at build time from the pinned tag, so there
# is only ONE value to bump here -- mirroring bump_vllm_wheel.sh, which bumps the
# Linux cu130 wheel pin.
#
# This deliberately tracks vllm-project/vllm-metal, NOT vllm-project/vllm: the
# darwin build can only use the exact vLLM version vllm-metal supports, so it may
@@ -23,19 +23,24 @@ if [ -z "$FILE" ] || [ -z "$REPO" ] || [ -z "$VAR" ]; then
exit 1
fi
# vllm-metal ships frequent dev releases, all flagged as non-prerelease, so
# /releases/latest returns the newest one (with its cp312 wheel asset).
# vllm-metal ships frequent .dev releases, flagged as prereleases, alongside the
# stable ones. /releases/latest skips the prereleases and returns the newest
# stable tag, which is what darwin should pin: upstream deletes and re-cuts .dev
# tags, and a pin to a deleted tag 404s the whole build.
LATEST_TAG=$(gh_curl -H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/$REPO/releases/latest" \
| python3 -c "import json,sys; print(json.load(sys.stdin)['tag_name'])")
# The coupled vLLM source version lives in vllm-metal's installer at that tag.
NEW_VLLM_VERSION=$(gh_curl \
"https://raw.githubusercontent.com/$REPO/$LATEST_TAG/install.sh" \
| grep -oE 'vllm_v="[0-9]+\.[0-9]+\.[0-9]+"' | head -1 | cut -d'"' -f2)
# The coupled vLLM release lives in .github/vllm-release-tag.commit at that tag
# (since vllm-metal 0.28); releases predating that file pinned it inline in their
# own install.sh. The extractor reads both forms.
NEW_VLLM_VERSION=$( { gh_curl \
"https://raw.githubusercontent.com/$REPO/$LATEST_TAG/.github/vllm-release-tag.commit" \
|| gh_curl "https://raw.githubusercontent.com/$REPO/$LATEST_TAG/install.sh"; } \
| "$(dirname "${BASH_SOURCE[0]}")/../scripts/lib/extract-vllm-metal-version.sh")
if [ -z "$LATEST_TAG" ] || [ -z "$NEW_VLLM_VERSION" ]; then
echo "Could not resolve vllm-metal tag ($LATEST_TAG) or its vllm_v ($NEW_VLLM_VERSION)." >&2
echo "Could not resolve vllm-metal tag ($LATEST_TAG) or its vLLM version ($NEW_VLLM_VERSION)." >&2
exit 1
fi
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
home = Path("website/layouts/index.html").read_text()
css = Path("website/static/css/site.css").read_text()
install = Path("docs/content/getting-started/install.md").read_text()
containers = Path("docs/content/getting-started/containers.md").read_text()
def require(condition, message):
if not condition:
raise SystemExit(f"FAIL: {message}")
require("Drop-in replacement for most upstream APIs." in home,
"homepage must use the requested drop-in API heading")
require("Everything else plugs into LocalAI." not in home,
"old runtime heading must be removed")
require("When the engine we need" not in home,
"hero must describe user outcomes instead of team implementation")
require('href="mailto:contact@localai.io"' in home and "business" in home.lower(),
"homepage must provide a direct business contact action")
require(home.index('id="localai"') < home.index('id="proof-quotes"') < home.index('id="mission"'),
"headline testimonials must directly follow the runtime section")
require(home.count('id="proof-quotes"') == 1,
"headline testimonials must appear exactly once")
require('id="engines"' not in home and "Engines we build" not in home,
"homepage engine showcase must be removed")
require('href="/docs/installation/index.html"' in home,
"installation guide action must use the direct installation URL")
require('<iframe' in install and "youtube.com/embed/cMVNnlqwfw4" in install,
"installation page must embed the walkthrough video")
require("## Quick Start" not in install,
"installation landing page must not duplicate Quick Start")
for text in ("CUDA 12", "CUDA 13", "ROCm", "Intel", "Jetson", "Vulkan", "fallback"):
require(text.lower() in containers.lower(), f"GPU chooser must explain {text}")
require('class="sn__e"><a href="https://github.com/mudler/parakeet.cpp">parakeet.cpp</a>' in home,
"capability engine names must link to their repositories")
require(".pane{min-height:" in css.replace(" ", ""),
"all installation panes must have a fixed minimum height")
print("website review 143 source checks passed")
PY
+1 -65
View File
@@ -29,10 +29,6 @@ updates:
schedule:
# Check for updates to GitHub Actions every weekday
interval: "weekly"
- package-ecosystem: "pip"
directory: "/backend/python/bark"
schedule:
interval: "weekly"
- package-ecosystem: "pip"
directory: "/backend/python/common/template"
schedule:
@@ -55,30 +51,10 @@ updates:
ignore:
- dependency-name: "torch"
- dependency-name: "transformers"
- package-ecosystem: "pip"
directory: "/backend/python/exllama"
schedule:
interval: "weekly"
- package-ecosystem: "pip"
directory: "/backend/python/exllama2"
schedule:
interval: "weekly"
- package-ecosystem: "pip"
directory: "/backend/python/mamba"
schedule:
interval: "weekly"
- package-ecosystem: "pip"
directory: "/backend/python/openvoice"
schedule:
interval: "weekly"
- package-ecosystem: "pip"
directory: "/backend/python/rerankers"
schedule:
interval: "weekly"
- package-ecosystem: "pip"
directory: "/backend/python/sentencetransformers"
schedule:
interval: "weekly"
- package-ecosystem: "pip"
directory: "/backend/python/transformers"
schedule:
@@ -86,44 +62,4 @@ updates:
- package-ecosystem: "pip"
directory: "/backend/python/vllm"
schedule:
interval: "weekly"
- package-ecosystem: "pip"
directory: "/examples/chainlit"
schedule:
interval: "weekly"
- package-ecosystem: "pip"
directory: "/examples/functions"
schedule:
interval: "weekly"
- package-ecosystem: "pip"
directory: "/examples/langchain/langchainpy-localai-example"
schedule:
interval: "weekly"
- package-ecosystem: "pip"
directory: "/examples/langchain-chroma"
schedule:
interval: "weekly"
- package-ecosystem: "pip"
directory: "/examples/streamlit-bot"
schedule:
interval: "weekly"
- package-ecosystem: "docker"
directory: "/examples/k8sgpt"
schedule:
interval: "weekly"
- package-ecosystem: "docker"
directory: "/examples/kubernetes"
schedule:
interval: "weekly"
- package-ecosystem: "docker"
directory: "/examples/langchain"
schedule:
interval: "weekly"
- package-ecosystem: "gomod"
directory: "/examples/semantic-todo"
schedule:
interval: "weekly"
- package-ecosystem: "docker"
directory: "/examples/telegram-bot"
schedule:
interval: "weekly"
interval: "weekly"
+3 -3
View File
@@ -166,7 +166,7 @@ jobs:
push-to-fork: ci-forks/LocalAI
commit-message: ':arrow_up: Update ${{ matrix.repository }}'
title: 'chore: :arrow_up: Update ${{ matrix.repository }} to `${{ steps.bump.outputs.commit }}`'
branch: "update/${{ matrix.variable }}"
branch: "bump/${{ matrix.variable }}"
body: ${{ steps.bump.outputs.message }}
signoff: true
@@ -203,7 +203,7 @@ jobs:
push-to-fork: ci-forks/LocalAI
commit-message: ':arrow_up: Update vllm-project/vllm cu130 wheel'
title: 'chore: :arrow_up: Update vllm-project/vllm cu130 wheel to `${{ steps.bump.outputs.commit }}`'
branch: "update/VLLM_VERSION"
branch: "bump/VLLM_VERSION"
body: ${{ steps.bump.outputs.message }}
signoff: true
@@ -241,6 +241,6 @@ jobs:
push-to-fork: ci-forks/LocalAI
commit-message: ':arrow_up: Update vllm-project/vllm-metal (darwin)'
title: 'chore: :arrow_up: Update vllm-metal (darwin) to `${{ steps.bump.outputs.commit }}`'
branch: "update/VLLM_METAL_VERSION"
branch: "bump/VLLM_METAL_VERSION"
body: ${{ steps.bump.outputs.message }}
signoff: true
+4 -3
View File
@@ -31,13 +31,14 @@ jobs:
messages: [
{
role: "system",
content: "Write a discord message with a bullet point summary of the release notes."
content: "Write a Discord message with a bullet point summary of the release notes. Keep the complete message under 1800 characters."
},
{
role: "user",
content: $input
}
]
],
max_tokens: 450
}')
# Send the request to LocalAI API
@@ -46,7 +47,7 @@ jobs:
-d "$json_payload")
# Extract the summary from the response
summary=$(echo $response | jq -r '.choices[0].message.content')
summary=$(printf '%s' "$response" | jq -er '.choices[0].message.content | strings | .[0:1800]')
# Print the summary
# -H "Authorization: Bearer $API_KEY" \
+20 -9
View File
@@ -14,6 +14,7 @@ on:
permissions:
contents: write
pull-requests: write
concurrency:
group: refresh-site-counters
@@ -30,15 +31,25 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: ./.github/ci/refresh-site-counters.sh
- name: Commit only if something moved
- name: Show changes
run: |
if git diff --quiet -- website/data/stats.yaml; then
echo "counters unchanged, nothing to commit"
exit 0
echo "counters unchanged"
else
git diff --unified=0 -- website/data/stats.yaml
fi
git diff --unified=0 -- website/data/stats.yaml
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add website/data/stats.yaml
git commit -m "chore(website): refresh the counters"
git push
- name: Create pull request when counters moved
uses: peter-evans/create-pull-request@v8
with:
token: ${{ secrets.UPDATE_BOT_TOKEN }}
push-to-fork: ci-forks/LocalAI
commit-message: "chore(website): refresh the counters"
title: "chore(website): refresh the counters"
body: |
Weekly refresh of the landing-page counters from the GitHub API.
This PR was created automatically by the `refresh-site-counters` workflow.
branch: update/site-counters
delete-branch: true
labels: automated
+1
View File
@@ -526,6 +526,7 @@ jobs:
- name: Build llama-cpp backend image and run gRPC e2e tests
run: |
make test-extra-backend-llama-cpp
make test-extra-backend-llama-cpp-embeddings
tests-llama-cpp-grpc-transcription:
needs: detect-changes
if: needs.detect-changes.outputs.llama-cpp == 'true' || needs.detect-changes.outputs.run-all == 'true'
+18 -2
View File
@@ -65,6 +65,12 @@ jobs:
- name: Test (with coverage gate)
run: |
PATH="$PATH:/root/go/bin" make --jobs 5 --output-sync=target test-coverage-check
# tests/integration is outside the coverage roots because its store specs
# need a live backend. test-stores builds and installs local-store before
# running the complete suite, so new local-store specs are collected
# automatically without adding another workflow entry.
- name: Test local-store integration
run: PATH="$PATH:$HOME/go/bin" make test-stores
- name: Upload coverage report
if: ${{ always() }}
uses: actions/upload-artifact@v4
@@ -74,8 +80,13 @@ jobs:
coverage/coverage.out
coverage/coverage.html
if-no-files-found: ignore
# tmate keeps the runner busy until the 6 hour job limit, so a single
# failure costs a whole runner slot. Only open a session when someone
# asked for one by labelling the pull request `ci-debug`, and cap the
# session so a forgotten label cannot idle a runner either.
- name: Setup tmate session if tests fail
if: ${{ failure() }}
if: ${{ failure() && contains(github.event.pull_request.labels.*.name, 'ci-debug') }}
timeout-minutes: 30
uses: mxschmitt/action-tmate@v3.23
with:
detached: true
@@ -119,8 +130,13 @@ jobs:
export PATH="/opt/homebrew/opt/make/libexec/gnubin:$PATH"
PATH="$PATH:$HOME/go/bin" make protogen-go
PATH="$PATH:$HOME/go/bin" BUILD_TYPE="GITHUB_CI_HAS_BROKEN_METAL" CMAKE_ARGS="-DGGML_F16C=OFF -DGGML_AVX512=OFF -DGGML_AVX2=OFF -DGGML_FMA=OFF" make --jobs 4 --output-sync=target test
# tmate keeps the runner busy until the 6 hour job limit, so a single
# failure costs a whole runner slot. Only open a session when someone
# asked for one by labelling the pull request `ci-debug`, and cap the
# session so a forgotten label cannot idle a runner either.
- name: Setup tmate session if tests fail
if: ${{ failure() }}
if: ${{ failure() && contains(github.event.pull_request.labels.*.name, 'ci-debug') }}
timeout-minutes: 30
uses: mxschmitt/action-tmate@v3.23
with:
detached: true
+6 -1
View File
@@ -77,8 +77,13 @@ jobs:
- name: Test
run: |
PATH="$PATH:$HOME/go/bin" make backends/local-store backends/silero-vad backends/llama-cpp backends/whisper backends/piper backends/stablediffusion-ggml docker-build-e2e e2e-aio
# tmate keeps the runner busy until the 6 hour job limit, so a single
# failure costs a whole runner slot. Only open a session when someone
# asked for one by labelling the pull request `ci-debug`, and cap the
# session so a forgotten label cannot idle a runner either.
- name: Setup tmate session if tests fail
if: ${{ failure() }}
if: ${{ failure() && contains(github.event.pull_request.labels.*.name, 'ci-debug') }}
timeout-minutes: 30
uses: mxschmitt/action-tmate@v3.23
with:
detached: true
+6 -1
View File
@@ -63,8 +63,13 @@ jobs:
- name: Test Backend E2E
run: |
PATH="$PATH:$HOME/go/bin" make build-mock-backend test-e2e
# tmate keeps the runner busy until the 6 hour job limit, so a single
# failure costs a whole runner slot. Only open a session when someone
# asked for one by labelling the pull request `ci-debug`, and cap the
# session so a forgotten label cannot idle a runner either.
- name: Setup tmate session if tests fail
if: ${{ failure() }}
if: ${{ failure() && contains(github.event.pull_request.labels.*.name, 'ci-debug') }}
timeout-minutes: 30
uses: mxschmitt/action-tmate@v3.23
with:
detached: true
+6 -1
View File
@@ -88,8 +88,13 @@ jobs:
# CPU and runs the token_classify capability spec (byte-offset contract).
- name: Run live PII NER backend E2E
run: PATH="$PATH:$HOME/go/bin" make test-extra-backend-privacy-filter
# tmate keeps the runner busy until the 6 hour job limit, so a single
# failure costs a whole runner slot. Only open a session when someone
# asked for one by labelling the pull request `ci-debug`, and cap the
# session so a forgotten label cannot idle a runner either.
- name: Setup tmate session if tests fail
if: ${{ failure() }}
if: ${{ failure() && contains(github.event.pull_request.labels.*.name, 'ci-debug') }}
timeout-minutes: 30
uses: mxschmitt/action-tmate@v3.23
with:
detached: true
+8 -1
View File
@@ -52,6 +52,8 @@ jobs:
run: |
sudo apt-get update
sudo apt-get install -y build-essential libopus-dev
- name: Run stale chunk recovery tests
run: PATH="$PATH:$HOME/go/bin" make test-ui-stale-chunk
# Builds an instrumented UI bundle, runs the Playwright specs, and fails
# if line coverage regressed beyond the jitter tolerance (the gate is
# in `make test-ui-coverage-check`). PLAYWRIGHT_CHROMIUM_PATH is unset
@@ -73,8 +75,13 @@ jobs:
path: core/http/react-ui/coverage/
if-no-files-found: ignore
retention-days: 7
# tmate keeps the runner busy until the 6 hour job limit, so a single
# failure costs a whole runner slot. Only open a session when someone
# asked for one by labelling the pull request `ci-debug`, and cap the
# session so a forgotten label cannot idle a runner either.
- name: Setup tmate session if tests fail
if: ${{ failure() }}
if: ${{ failure() && contains(github.event.pull_request.labels.*.name, 'ci-debug') }}
timeout-minutes: 30
uses: mxschmitt/action-tmate@v3.23
with:
detached: true
+21
View File
@@ -0,0 +1,21 @@
## Design Context
### Users
LocalAI serves both single-host users who want to install and try models quickly and experienced developers, ML engineers, system administrators, and DevOps operators who manage production hosts or distributed clusters. The interface must support first-time discovery without hiding the runtime state, configuration, and control that returning operators need.
### Brand Personality
Capable, easy to use, and trustworthy. The interface should make sophisticated local-AI infrastructure feel understandable and under control. It should be direct and calm rather than playful, ornamental, or intimidating.
### Aesthetic Direction
Use LocalAI's established technical, editorial design language: Geist typography, compact information density, sharp geometry, deep blue-black surfaces, action blue, mint for healthy/local/live state, and amber only for decisions requiring attention. Support both dark and light themes. Avoid generic card dashboards, decorative gradients, glass effects, and visual noise.
### Design Principles
1. Use progressive disclosure to serve newcomers and operators in the same workflow: make the common path obvious, then reveal operational depth in context.
2. Organize navigation around user intent and lifecycle state, not implementation concepts or nested containers.
3. Give each resource one canonical home; expose discovery, installed state, and runtime state as clear views of that resource instead of duplicating management surfaces.
4. Keep operational status visible and trustworthy through precise labels, explicit scope, and actionable state—not decoration.
5. Preserve information density for expert use while flattening navigation and reducing repeated summaries, tabs, rails, and panels.
+1
View File
@@ -27,6 +27,7 @@ To be removed, open a pull request deleting your row, or email
| Organisation | What they use it for | Status |
|---|---|---|
| [walcz.de](https://walcz.de) | Self-hosted appliance for a German B2B consultancy: local-only inference on AMD Strix Halo (gfx1151/ROCm), agents with MCP tools, RAG over an internal knowledge base, and a document/bookkeeping pipeline. | Production |
| _Your organisation here_ | | |
## What this list is not
+2 -1
View File
@@ -8,7 +8,7 @@ Human contributors: see [CONTRIBUTING.md](CONTRIBUTING.md) for the development w
LocalAI follows the Linux kernel project's [guidelines for AI coding assistants](https://docs.kernel.org/process/coding-assistants.html). Before submitting AI-assisted code, read [.agents/ai-coding-assistants.md](.agents/ai-coding-assistants.md). Key rules:
- **No `Signed-off-by` from AI.** Only the human submitter may sign off on the Developer Certificate of Origin.
- **No `Signed-off-by` from AI.** Only the human submitter may sign off on the Developer Certificate of Origin. One exception: automation a maintainer operates signs off with *that maintainer's* identity, since no other human submitter exists to certify it. See [.agents/ai-coding-assistants.md](.agents/ai-coding-assistants.md).
- **No `Co-Authored-By: <AI>` trailers.** The human contributor owns the change.
- **Use an `Assisted-by:` trailer** to attribute AI involvement. Format: `Assisted-by: AGENT_NAME:MODEL_VERSION [TOOL1] [TOOL2]`.
- **The human submitter is responsible** for reviewing, testing, and understanding every line of generated code.
@@ -33,6 +33,7 @@ LocalAI follows the Linux kernel project's [guidelines for AI coding assistants]
| [.agents/localai-assistant-mcp.md](.agents/localai-assistant-mcp.md) | LocalAI Assistant chat modality — adding admin tools to the in-process MCP server, editing skill prompts, keeping REST + MCP + skills in sync |
| [.agents/backend-signing.md](.agents/backend-signing.md) | Backend OCI image signing (keyless cosign + sigstore-go) — producer-side CI setup, consumer-side gallery `verification:` block, strict mode (`LOCALAI_REQUIRE_BACKEND_INTEGRITY`), revocation via `not_before` |
| [.agents/preparing-a-release.md](.agents/preparing-a-release.md) | Cutting a release: PR labels, `RELEASE_NOTES_vX.Y.Z.md`, the blog post under `website/content/blog/`, and the demo clips under `website/static/media/` |
| [.impeccable.md](.impeccable.md) | Design context for UI/UX work — users, brand personality, aesthetic direction, and design principles |
## Quick Reference
+1 -1
View File
@@ -218,7 +218,7 @@ LocalAI follows the **same guidelines as the Linux kernel project** for AI-assis
The full policy for this repository lives in [`.agents/ai-coding-assistants.md`](.agents/ai-coding-assistants.md). Summary:
- **AI agents MUST NOT add `Signed-off-by` tags.** Only humans can certify the Developer Certificate of Origin.
- **AI agents MUST NOT add `Signed-off-by` tags.** Only humans can certify the Developer Certificate of Origin. Automation operated by a maintainer is the one exception: it signs off with that maintainer's identity, because there is no other human submitter to certify it.
- **AI agents MUST NOT add `Co-Authored-By` trailers** attributing themselves as co-authors.
- **Attribute AI involvement with an `Assisted-by` trailer** in the commit message:
+38 -6
View File
@@ -34,6 +34,11 @@ TEST_FLAKES?=5
RANDOM := $(shell bash -c 'echo $$RANDOM')
VERSION?=$(shell git describe --always --tags || echo "dev" )
# fyne package only accepts numeric x[.y[.z]] app versions, so reduce git
# describe output (v4.9.0, v4.9.0-14-gabc1234, or a bare sha on untagged
# checkouts) to its numeric core; anything non-numeric falls back to 0.0.0.
# Without this the packaged launcher reports itself as version 0.0.0 (#11673).
LAUNCHER_APP_VERSION?=$(shell v=$$(echo "$(VERSION)" | sed -E 's/^v//; s/[+-].*$$//'); echo "$$v" | grep -qE '^[0-9]+(\.[0-9]+){0,2}$$' && echo "$$v" || echo "0.0.0")
# go tool nm ./local-ai | grep Commit
LD_FLAGS?=-s -w
override LD_FLAGS += -X "github.com/mudler/LocalAI/internal.Version=$(VERSION)"
@@ -103,7 +108,7 @@ COVERAGE_E2E_LABELS?=!real-models
COVERAGE_EXCLUDE_RE?=grpc/proto/.*[.]pb[.]go
.PHONY: all test test-coverage test-coverage-baseline test-coverage-check test-backend-cpp test-build-scripts test-ui test-ui-coverage-baseline test-ui-coverage-check build vendor lint lint-all
.PHONY: all test test-coverage test-coverage-baseline test-coverage-check test-backend-cpp test-build-scripts test-ui test-ui-stale-chunk test-ui-coverage-baseline test-ui-coverage-check build vendor lint lint-all
all: help
@@ -235,7 +240,7 @@ test-ci-scripts:
## pure stdlib on purpose so they run without any backend venv; the list is
## explicit because their siblings (model_identity_test) import grpc and the
## generated protobufs, which only exist inside a built backend.
PYTHON_HELPER_TESTS?=python_utils_test vllm_utils_test model_utils_test mlx_utils_test parent_watch_test
PYTHON_HELPER_TESTS?=python_utils_test vllm_utils_test model_utils_test mlx_utils_test parent_watch_test temp_utils_test
test-python-helpers:
cd backend/python/common && python3 -m unittest $(PYTHON_HELPER_TESTS)
@@ -388,9 +393,17 @@ test-e2e: build-mock-backend build-cloud-proxy-backend prepare-e2e run-e2e-image
$(MAKE) teardown-e2e
docker rmi localai-tests
# `docker stop` returns as soon as the container exits, but Docker reaps a
# `--rm` container asynchronously after that. The `docker rmi localai-tests` in
# test-e2e then loses the race against the reaper and fails on a still
# referenced image, turning a green suite red. Removing the container ourselves
# is synchronous, so the image reference is gone before we return. It also
# covers the case where nothing is running, which `docker stop` could not
# because it rejects an empty argument list.
teardown-e2e:
rm -rf $(TEST_DIR) || true
docker stop $$(docker ps -q --filter ancestor=localai-tests)
@CONTAINERS=$$(docker ps -aq --filter ancestor=localai-tests 2>/dev/null); \
if [ -n "$$CONTAINERS" ]; then docker rm -f $$CONTAINERS || true; fi
########################################################
## Integration and unit tests
@@ -676,6 +689,7 @@ test-extra: prepare-test-extra
## BACKEND_TEST_PROMPT Override the prompt used in predict/stream specs.
## BACKEND_TEST_OPTIONS Comma-separated Options[] entries forwarded to LoadModel,
## e.g. "tool_parser:hermes,reasoning_parser:qwen3".
## BACKEND_TEST_EMBEDDING_LAYOUT Expected EmbeddingResult layout: "final" or "per_token".
##
## Direct usage (image already built, no docker-build-* dependency):
##
@@ -705,6 +719,7 @@ test-extra-backend: protogen-go
BACKEND_TEST_CAPS="$$BACKEND_TEST_CAPS" \
BACKEND_TEST_PROMPT="$$BACKEND_TEST_PROMPT" \
BACKEND_TEST_OPTIONS="$$BACKEND_TEST_OPTIONS" \
BACKEND_TEST_EMBEDDING_LAYOUT="$$BACKEND_TEST_EMBEDDING_LAYOUT" \
BACKEND_TEST_TOOL_PROMPT="$$BACKEND_TEST_TOOL_PROMPT" \
BACKEND_TEST_TOOL_NAME="$$BACKEND_TEST_TOOL_NAME" \
BACKEND_TEST_CACHE_TYPE_K="$$BACKEND_TEST_CACHE_TYPE_K" \
@@ -724,6 +739,15 @@ test-extra-backend-llama-cpp: docker-build-llama-cpp
BACKEND_TEST_CAPS=health,load,predict,stream,logprobs,logit_bias \
$(MAKE) test-extra-backend
## Raw llama.cpp embeddings are required by Go-side pooling. This exercises the
## real C++ backend and verifies that it marks the flattened matrix per-token.
test-extra-backend-llama-cpp-embeddings: docker-build-llama-cpp
BACKEND_IMAGE=local-ai-backend:llama-cpp \
BACKEND_TEST_CAPS=health,load,embeddings \
BACKEND_TEST_OPTIONS=pooling:none \
BACKEND_TEST_EMBEDDING_LAYOUT=per_token \
$(MAKE) test-extra-backend
test-extra-backend-ik-llama-cpp: docker-build-ik-llama-cpp
BACKEND_IMAGE=local-ai-backend:ik-llama-cpp $(MAKE) test-extra-backend
@@ -813,6 +837,7 @@ test-extra-backend-tinygrad-embeddings: docker-build-tinygrad
BACKEND_IMAGE=local-ai-backend:tinygrad \
BACKEND_TEST_MODEL_NAME=Qwen/Qwen3-0.6B \
BACKEND_TEST_CAPS=health,load,embeddings \
BACKEND_TEST_EMBEDDING_LAYOUT=final \
$(MAKE) test-extra-backend
## tinygrad — Stable Diffusion 1.5. The original CompVis/runwayml repos have
@@ -1505,6 +1530,13 @@ test-ui: build-mock-backend protogen-go
$(GOCMD) build -o tests/e2e-ui/ui-test-server ./tests/e2e-ui
cd core/http/react-ui && sh $(CURDIR)/scripts/ensure-playwright-browser.sh && bunx playwright test $(PLAYWRIGHT_WORKERS_FLAG)
## The stale-chunk specs need the production code-split bundle. The V8 coverage
## bundle below inlines dynamic imports to keep every page in its denominator.
test-ui-stale-chunk: build-mock-backend protogen-go
cd core/http/react-ui && bun install && bun run build
$(GOCMD) build -o tests/e2e-ui/ui-test-server ./tests/e2e-ui
cd core/http/react-ui && sh $(CURDIR)/scripts/ensure-playwright-browser.sh && bunx playwright test --grep @production-chunks --workers=1
## React UI code coverage from the Playwright e2e suite. Builds a
## NON-instrumented bundle with source maps (COVERAGE_V8=true), re-embeds it
## into the ui-test-server (the dist is //go:embed'ed at compile time), runs the
@@ -1520,7 +1552,7 @@ test-ui-coverage: build-mock-backend protogen-go
$(GOCMD) build -o tests/e2e-ui/ui-test-server ./tests/e2e-ui && \
( cd core/http/react-ui && rm -rf .nyc_output coverage && \
sh $(CURDIR)/scripts/ensure-playwright-browser.sh && \
PW_V8_COVERAGE=1 bunx playwright test $(PLAYWRIGHT_WORKERS_FLAG) && bun run coverage:report )
PW_V8_COVERAGE=1 bunx playwright test --grep-invert @production-chunks $(PLAYWRIGHT_WORKERS_FLAG) && bun run coverage:report )
## UI coverage baseline (committed) and the strict gate that compares against
## it — the React mirror of test-coverage-baseline / test-coverage-check.
@@ -1603,7 +1635,7 @@ site-serve: site
build-launcher-darwin:
rm -rf dist/LocalAI.app cmd/launcher/LocalAI.app
mkdir -p dist
cd cmd/launcher && go run fyne.io/tools/cmd/fyne@latest package -os darwin -icon ../../core/http/static/logo.png --executable $(LAUNCHER_BINARY_NAME)
cd cmd/launcher && go run fyne.io/tools/cmd/fyne@latest package -os darwin -icon ../../core/http/static/logo.png --executable $(LAUNCHER_BINARY_NAME) --app-version $(LAUNCHER_APP_VERSION)
mv cmd/launcher/LocalAI.app dist/LocalAI.app
bash contrib/macos/sign-and-notarize.sh sign dist/LocalAI.app
@@ -1630,4 +1662,4 @@ release-launcher-darwin: notarize-launcher-darwin
@echo "dist/LocalAI.dmg is ready"
build-launcher-linux:
cd cmd/launcher && go run fyne.io/tools/cmd/fyne@latest package -os linux -icon ../../core/http/static/logo.png --executable $(LAUNCHER_BINARY_NAME)-linux && mv LocalAI.tar.xz ../../$(LAUNCHER_BINARY_NAME)-linux.tar.xz
cd cmd/launcher && go run fyne.io/tools/cmd/fyne@latest package -os linux -icon ../../core/http/static/logo.png --executable $(LAUNCHER_BINARY_NAME)-linux --app-version $(LAUNCHER_APP_VERSION) && mv LocalAI.tar.xz ../../$(LAUNCHER_BINARY_NAME)-linux.tar.xz
+1 -8
View File
@@ -5,9 +5,6 @@
</h1>
<p align="center">
<a href="https://github.com/go-skynet/LocalAI/stargazers" target="blank">
<img src="https://img.shields.io/github/stars/go-skynet/LocalAI?style=for-the-badge" alt="LocalAI stars"/>
</a>
<a href='https://github.com/go-skynet/LocalAI/releases'>
<img src='https://img.shields.io/github/release/go-skynet/LocalAI?&label=Latest&style=for-the-badge'>
</a>
@@ -231,7 +228,7 @@ Most backends wrap a best-in-class upstream engine. A handful of them are native
| Backend | What it does |
|---------|-------------|
| [vllm.cpp](https://github.com/mudler/vllm.cpp) | From-scratch C++20 port of vLLM for text generation: paged KV cache, continuous batching, prefix caching, safetensors + GGUF loading, engine-enforced structured output, on CPU, CUDA, Metal and Vulkan |
| [vllm.cpp](https://github.com/mudler/vllm.cpp) | From-scratch C++20 port of vLLM for text generation: paged KV cache, continuous batching, prefix caching, safetensors + GGUF loading, engine-enforced structured output, on CPU, CUDA, Metal and Vulkan. Also serves MiniMax-H3 joint video+audio generation |
| [parakeet.cpp](https://github.com/mudler/parakeet.cpp) | C++/GGML port of NVIDIA NeMo Parakeet ASR (tdt/ctc/rnnt/hybrid), with cache-aware streaming transcription |
| [moss-transcribe.cpp](https://github.com/localai-org/moss-transcribe.cpp) | C++/GGML port of OpenMOSS MOSS-Transcribe-Diarize: joint long-form transcription, speaker diarization and timestamping in a single pass |
| [moss-tts.cpp](https://github.com/mudler/moss-tts.cpp) | C++/GGML port of the OpenMOSS MOSS-TTS family: text-to-speech (MOSS-TTS-Local v1.5, 48 kHz stereo) with reference-audio voice cloning, through the MOSS-Audio-Tokenizer neural codec |
@@ -318,10 +315,6 @@ Past sponsors
A special thanks to individual sponsors, a full list is on [GitHub](https://github.com/sponsors/mudler) and [buymeacoffee](https://buymeacoffee.com/mudler). Special shout out to [drikster80](https://github.com/drikster80) for being generous. Thank you everyone!
## Star history
[![LocalAI Star history Chart](https://api.star-history.com/svg?repos=go-skynet/LocalAI&type=Date)](https://star-history.com/#go-skynet/LocalAI&Date)
## License
LocalAI is a community-driven project created by [Ettore Di Giacinto](https://github.com/mudler/) and maintained by the [LocalAI team](#team).
+3 -1
View File
@@ -10,6 +10,7 @@ FROM ${BASE_IMAGE} AS builder
ARG BUILD_TYPE
ARG TARGETARCH
ARG TARGETVARIANT
ARG CUDA_MAJOR_VERSION
ENV BUILD_TYPE=${BUILD_TYPE} \
DEBIAN_FRONTEND=noninteractive \
@@ -35,7 +36,8 @@ RUN apt-get update && \
COPY . /LocalAI
RUN --mount=type=cache,target=/root/.ccache,id=ds4-ccache-${TARGETARCH}-${BUILD_TYPE},sharing=locked \
make -C /LocalAI/backend/cpp/ds4 BUILD_TYPE=${BUILD_TYPE} NATIVE=false grpc-server package
make -C /LocalAI/backend/cpp/ds4 BUILD_TYPE=${BUILD_TYPE} \
CUDA_MAJOR_VERSION=${CUDA_MAJOR_VERSION} NATIVE=false grpc-server package
FROM scratch
COPY --from=builder /LocalAI/backend/cpp/ds4/package/. ./
+20
View File
@@ -536,8 +536,28 @@ message Result {
bool success = 2;
}
// EmbeddingLayout describes whether embeddings contains one final vector or
// a matrix of per-token vectors. Go-side pooling must never infer this from
// tokens/dim alone: a one-token raw matrix and a final vector have the same
// shape.
enum EmbeddingLayout {
EMBEDDING_LAYOUT_UNSPECIFIED = 0;
EMBEDDING_LAYOUT_FINAL = 1;
EMBEDDING_LAYOUT_PER_TOKEN = 2;
}
message EmbeddingResult {
repeated float embeddings = 1;
// Shape of the payload above: dim is the embedding width, tokens is the
// number of vectors packed into `embeddings` (1 when the backend pooled
// server-side, N with pooling:none; total across prompts if a request
// carried several). tokens=0/dim=0 means the backend predates shape
// reporting. prompt_tokens is the number of prompt tokens evaluated, for
// usage accounting.
int32 tokens = 2;
int32 dim = 3;
int32 prompt_tokens = 4;
EmbeddingLayout layout = 5;
}
message TranscriptRequest {
+1 -1
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?=7efbb58def443722ea540d931dd3debee3e4d5e8
AUDIO_CPP_VERSION?=05e508a70e3600b01454c647cdb122133ba8e64c
AUDIO_CPP_REPO?=https://github.com/0xShug0/audio.cpp
CURRENT_MAKEFILE_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
@@ -29,6 +29,7 @@ const NamedTask kTaskNames[] = {
{Task::VoiceDesign, "vdes"},
{Task::SpeakerRecognition, "spk"},
{Task::Svc, "svc"},
{Task::Midi, "midi"},
};
// Accepted on input but never emitted. "spkrec" was this backend's own earlier
@@ -25,6 +25,7 @@ enum class Task {
VoiceDesign,
SpeakerRecognition,
Svc,
Midi,
};
// Mirrors engine::runtime::RunMode.
@@ -361,7 +361,7 @@ static void test_names_round_trip() {
Task::SourceSeparation, Task::AudioGeneration, Task::Tts,
Task::VoiceCloning, Task::VoiceConversion,
Task::SpeechToSpeech, Task::Alignment, Task::VoiceDesign,
Task::SpeakerRecognition, Task::Svc};
Task::SpeakerRecognition, Task::Svc, Task::Midi};
for (const Task t : all) {
Task parsed = Task::Vad;
const bool ok = parse_task_name(task_name(t), parsed);
+5 -1
View File
@@ -69,7 +69,8 @@ static_assert(kEngine(engine::runtime::VoiceTaskKind::VoiceDesign) == 10, "Voice
static_assert(kEngine(engine::runtime::VoiceTaskKind::SpeakerRecognition) == 11, "VoiceTaskKind drifted");
// The last member. Pinning it pins the member count too, as long as the
// enumerators stay contiguous and unassigned, which upstream's declaration is.
static_assert(kEngine(engine::runtime::VoiceTaskKind::Svc) == 12,
static_assert(kEngine(engine::runtime::VoiceTaskKind::Svc) == 12, "VoiceTaskKind drifted");
static_assert(kEngine(engine::runtime::VoiceTaskKind::Midi) == 13,
"engine::runtime::VoiceTaskKind gained, lost or reordered a member. "
"audiocpp_backend::Task mirrors it positionally: update capability_routing.h, "
"to_engine_task and from_engine_task together, then move this pin.");
@@ -87,6 +88,7 @@ static_assert(kMirror(Task::Alignment) == 9, "Task drifted from VoiceTaskKind");
static_assert(kMirror(Task::VoiceDesign) == 10, "Task drifted from VoiceTaskKind");
static_assert(kMirror(Task::SpeakerRecognition) == 11, "Task drifted from VoiceTaskKind");
static_assert(kMirror(Task::Svc) == 12, "Task drifted from VoiceTaskKind");
static_assert(kMirror(Task::Midi) == 13, "Task drifted from VoiceTaskKind");
static_assert(static_cast<int>(engine::runtime::RunMode::Offline) == 0, "RunMode drifted");
static_assert(static_cast<int>(engine::runtime::RunMode::Streaming) == 1,
@@ -241,6 +243,7 @@ engine::runtime::VoiceTaskKind to_engine_task(Task task) {
case Task::VoiceDesign: return K::VoiceDesign;
case Task::SpeakerRecognition: return K::SpeakerRecognition;
case Task::Svc: return K::Svc;
case Task::Midi: return K::Midi;
}
// Unreachable for any valid enumerator. No `default:` label, so -Wswitch
// still reports a member this switch stops covering.
@@ -263,6 +266,7 @@ Task from_engine_task(engine::runtime::VoiceTaskKind kind) {
case K::VoiceDesign: return Task::VoiceDesign;
case K::SpeakerRecognition: return Task::SpeakerRecognition;
case K::Svc: return Task::Svc;
case K::Midi: return Task::Midi;
}
return Task::Vad;
}
+5 -1
View File
@@ -1,7 +1,7 @@
# Pinned to the HEAD of the `prism` branch on https://github.com/PrismML-Eng/llama.cpp.
# Auto-bumped nightly by .github/workflows/bump_deps.yaml.
BONSAI_VERSION?=9ca265a57f85f2117942490f421f64a226dd9847
BONSAI_VERSION?=312bb2a93ea2bf798333fa859614fbf913ecb9e2
LLAMA_REPO?=https://github.com/PrismML-Eng/llama.cpp
CMAKE_ARGS?=
@@ -41,7 +41,9 @@ define bonsai-build
# and are applied by apply-patches.sh below.
rm -rf $(CURRENT_MAKEFILE_DIR)/../bonsai-$(1)-build/patches
$(MAKE) -C $(CURRENT_MAKEFILE_DIR)/../bonsai-$(1)-build purge
bash $(CURRENT_MAKEFILE_DIR)/patch-grpc-server.sh $(CURRENT_MAKEFILE_DIR)/../bonsai-$(1)-build/grpc-server.cpp
bash $(LLAMA_CPP_DIR)/disable-score-task.sh $(CURRENT_MAKEFILE_DIR)/../bonsai-$(1)-build/grpc-server.cpp
bash $(LLAMA_CPP_DIR)/disable-tts-task.sh $(CURRENT_MAKEFILE_DIR)/../bonsai-$(1)-build/grpc-server.cpp
$(info $(GREEN)I bonsai build info:$(1)$(RESET))
LLAMA_REPO=$(LLAMA_REPO) LLAMA_VERSION=$(BONSAI_VERSION) \
$(MAKE) -C $(CURRENT_MAKEFILE_DIR)/../bonsai-$(1)-build llama.cpp
@@ -78,7 +80,9 @@ bonsai-cpu-all:
# and are applied by apply-patches.sh below.
rm -rf $(CURRENT_MAKEFILE_DIR)/../bonsai-cpu-all-build/patches
$(MAKE) -C $(CURRENT_MAKEFILE_DIR)/../bonsai-cpu-all-build purge
bash $(CURRENT_MAKEFILE_DIR)/patch-grpc-server.sh $(CURRENT_MAKEFILE_DIR)/../bonsai-cpu-all-build/grpc-server.cpp
bash $(LLAMA_CPP_DIR)/disable-score-task.sh $(CURRENT_MAKEFILE_DIR)/../bonsai-cpu-all-build/grpc-server.cpp
bash $(LLAMA_CPP_DIR)/disable-tts-task.sh $(CURRENT_MAKEFILE_DIR)/../bonsai-cpu-all-build/grpc-server.cpp
$(info $(GREEN)I bonsai build info:cpu-all-variants$(RESET))
LLAMA_REPO=$(LLAMA_REPO) LLAMA_VERSION=$(BONSAI_VERSION) \
$(MAKE) -C $(CURRENT_MAKEFILE_DIR)/../bonsai-cpu-all-build llama.cpp
+24
View File
@@ -0,0 +1,24 @@
#!/bin/bash
# Adapt the shared llama.cpp gRPC source to the older JSON API in Bonsai.
set -euo pipefail
if [[ $# -ne 1 ]]; then
echo "usage: $0 <grpc-server.cpp>" >&2
exit 2
fi
SRC=$1
if [[ ! -f "$SRC" ]]; then
echo "grpc-server.cpp not found at $SRC" >&2
exit 2
fi
if grep -q 'common_json_error' "$SRC"; then
echo "==> patching $SRC to use the Bonsai JSON exception type"
awk '{ gsub(/common_json_error/, "json::parse_error"); print }' "$SRC" > "$SRC.tmp"
mv "$SRC.tmp" "$SRC"
echo "==> Bonsai JSON exception patch OK"
else
echo "==> $SRC already uses a Bonsai-compatible JSON exception type, skipping"
fi
+4 -3
View File
@@ -84,9 +84,10 @@ elseif(DS4_GPU STREQUAL "cpu")
set(DS4_OBJS "${DS4_DIR}/ds4_cpu.o")
endif()
# Upstream splits distributed inference, tensor-parallel transport, the SSD
# expert cache, and layer placement into GPU-agnostic translation units. Link
# them regardless of DS4_GPU.
# Upstream splits image preprocessing, distributed inference, tensor-parallel
# transport, the SSD expert cache, and layer placement into GPU-agnostic
# translation units. Link them regardless of DS4_GPU.
list(APPEND DS4_OBJS "${DS4_DIR}/ds4_image.o")
list(APPEND DS4_OBJS "${DS4_DIR}/ds4_distributed.o")
list(APPEND DS4_OBJS "${DS4_DIR}/ds4_tp.o")
list(APPEND DS4_OBJS "${DS4_DIR}/ds4_ssd.o")
+73 -11
View File
@@ -1,10 +1,10 @@
# ds4 backend Makefile.
#
# Upstream pin lives below as DS4_VERSION?=b0309611041655f4e45671cfd9c9886aff161406
# Upstream pin lives below as DS4_VERSION?=6289c516273979173abbc062209a81dd3706b804
# (.github/bump_deps.sh) can find and update it - matches the
# llama-cpp / ik-llama-cpp / turboquant convention.
DS4_VERSION?=b0309611041655f4e45671cfd9c9886aff161406
DS4_VERSION?=6289c516273979173abbc062209a81dd3706b804
DS4_REPO?=https://github.com/antirez/ds4
CURRENT_MAKEFILE_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
@@ -18,21 +18,83 @@ UNAME_S := $(shell uname -s)
CMAKE_ARGS ?= -DCMAKE_BUILD_TYPE=Release
# Upstream splits distributed inference, tensor-parallel transport, the SSD
# expert cache, and layer placement into GPU-agnostic translation units. They
# are shared by every GPU mode, so append them unconditionally below.
# nvcc must be told the target architecture explicitly for a cublas build, and
# this is not a tuning knob. Upstream's Makefile leaves CUDA_ARCH empty and its
# `cuda` target REFUSES to build without one, offering `cuda-spark`
# (CUDA_ARCH=sm_121) and `cuda-generic` (CUDA_ARCH=native) instead. We drive its
# object targets directly, which bypasses that guard: nvcc then compiles with no
# -arch at all, and the kernels run as JIT'd PTX for its default architecture.
# On GB10 (sm_121) that silently produced corrupt inference output above a
# ~128-token prefill batch and ~77x slower prefill (4.21 t/s vs 325.70 t/s,
# measured on the same box with the same model). No CI runner has a GPU, so
# `native` has nothing to enumerate there.
#
# Upstream's CUDA_ARCH takes a SINGLE value (see its sm_120/sm_121 special cases
# and the `-arch=$(CUDA_ARCH)` fallback), so it cannot express the fat binary
# these images need. NVCC_ARCH_FLAGS is overridden instead: a command-line
# assignment wins over the `:=` in upstream's Makefile, and its NVCCFLAGS
# expands whatever we pass.
#
# The architecture lists are copied from backend/go/vllm-cpp/Makefile rather
# than invented, so the two CUDA images cover the same GPUs: amd64 datacenter +
# consumer, and l4t/arm64 covering Orin (87), Thor (110) and GB10 (121a).
#
# -DDS4_CUDA_HAVE_MXF4=1 is deliberately NOT set. Upstream only defines it for
# single-arch sm_120/sm_121 builds and guards the code with a plain #ifdef
# rather than __CUDA_ARCH__, so it cannot be combined with older archs in one
# fat binary. It gates an optional MXFP4 indexer fast path whose #ifndef branch
# returns 0 and falls back to the generic path, so omitting it costs some speed
# on GB10, not correctness. Revisit if upstream adds __CUDA_ARCH__ guards.
#
# An EMPTY CUDA_MAJOR_VERSION means a local developer build, not CI: fall back
# to upstream's own `native` handling, which needs a GPU present but is what a
# developer building on their own machine wants. Both variables are `?=` so an
# explicit value on the command line always wins.
UNAME_M := $(shell uname -m)
CUDA_MAJOR_VERSION ?=
ifeq ($(BUILD_TYPE),cublas)
ifeq ($(CUDA_MAJOR_VERSION),13)
ifeq ($(UNAME_M),aarch64)
DS4_NVCC_ARCH_FLAGS ?= -gencode arch=compute_87,code=sm_87 \
-gencode arch=compute_90a,code=sm_90a \
-gencode arch=compute_100a,code=sm_100a \
-gencode arch=compute_110,code=sm_110 \
-gencode arch=compute_121a,code=sm_121a
else
DS4_NVCC_ARCH_FLAGS ?= -gencode arch=compute_80,code=sm_80 \
-gencode arch=compute_86,code=sm_86 \
-gencode arch=compute_89,code=sm_89 \
-gencode arch=compute_90a,code=sm_90a \
-gencode arch=compute_100a,code=sm_100a \
-gencode arch=compute_103a,code=sm_103a \
-gencode arch=compute_120a,code=sm_120a \
-gencode arch=compute_121a,code=sm_121a
endif
DS4_ARCH_MAKEVARS := NVCC_ARCH_FLAGS="$(DS4_NVCC_ARCH_FLAGS)"
else ifeq ($(CUDA_MAJOR_VERSION),)
# Local build: let upstream resolve the host GPU.
DS4_ARCH_MAKEVARS := CUDA_ARCH=native
else
$(error CUDA_MAJOR_VERSION=$(CUDA_MAJOR_VERSION) has no architecture list here (13 does). Leave it empty for a native build, or pass DS4_NVCC_ARCH_FLAGS explicitly.)
endif
endif
# Upstream splits image preprocessing, distributed inference, tensor-parallel
# transport, the SSD expert cache, and layer placement into GPU-agnostic
# translation units. They are shared by every GPU mode, so append them
# unconditionally below.
ifeq ($(BUILD_TYPE),cublas)
CMAKE_ARGS += -DDS4_GPU=cuda
DS4_OBJ_TARGET := ds4.o ds4_cuda.o ds4_distributed.o ds4_tp.o ds4_ssd.o ds4_layer_pack.o \
DS4_OBJ_TARGET := ds4.o ds4_image.o ds4_cuda.o ds4_distributed.o ds4_tp.o ds4_ssd.o ds4_layer_pack.o \
cuda/mmq/ds4_ggml_stubs.o cuda/mmq/ds4_mmq.o cuda/mmq/ds4_mmq_d2r.o \
cuda/mmq/quantize.o cuda/mmq/mmid.o cuda/mmq/mmvq.o cuda/mmq/ds4_repack.o
else ifeq ($(UNAME_S),Darwin)
CMAKE_ARGS += -DDS4_GPU=metal
DS4_OBJ_TARGET := ds4.o ds4_metal.o ds4_distributed.o ds4_tp.o ds4_ssd.o ds4_layer_pack.o
DS4_OBJ_TARGET := ds4.o ds4_image.o ds4_metal.o ds4_distributed.o ds4_tp.o ds4_ssd.o ds4_layer_pack.o
else
# CPU reference path (Linux only - macOS CPU path is broken by VM bug per ds4 README).
CMAKE_ARGS += -DDS4_GPU=cpu
DS4_OBJ_TARGET := ds4_cpu.o ds4_distributed.o ds4_tp.o ds4_ssd.o ds4_layer_pack.o
DS4_OBJ_TARGET := ds4_cpu.o ds4_image.o ds4_distributed.o ds4_tp.o ds4_ssd.o ds4_layer_pack.o
endif
ifneq ($(NATIVE),true)
@@ -57,11 +119,11 @@ ds4:
# the right per-platform compile flags (Objective-C/Metal on Darwin, nvcc on Linux+CUDA).
ds4/ds4.o: ds4
ifeq ($(BUILD_TYPE),cublas)
+$(MAKE) -C ds4 $(DS4_OBJ_TARGET)
+$(MAKE) -C ds4 $(DS4_ARCH_MAKEVARS) $(DS4_OBJ_TARGET)
else ifeq ($(UNAME_S),Darwin)
+$(MAKE) -C ds4 ds4.o ds4_metal.o ds4_distributed.o ds4_tp.o ds4_ssd.o ds4_layer_pack.o
+$(MAKE) -C ds4 ds4.o ds4_image.o ds4_metal.o ds4_distributed.o ds4_tp.o ds4_ssd.o ds4_layer_pack.o
else
+$(MAKE) -C ds4 ds4_cpu.o ds4_distributed.o ds4_tp.o ds4_ssd.o ds4_layer_pack.o
+$(MAKE) -C ds4 ds4_cpu.o ds4_image.o ds4_distributed.o ds4_tp.o ds4_ssd.o ds4_layer_pack.o
endif
grpc-server: ds4/ds4.o
+2 -1
View File
@@ -92,7 +92,8 @@ std::string json_escape(const std::string &in) {
} // namespace
DsmlParser::DsmlParser() = default;
DsmlParser::DsmlParser(bool starts_in_thinking)
: state_(starts_in_thinking ? State::THINK : State::TEXT) {}
bool DsmlParser::IsInDsmlStructural() const {
switch (state_) {
+4 -2
View File
@@ -17,7 +17,9 @@ struct ParserEvent {
// Streaming parser. Stateless across instances; one per Predict call.
class DsmlParser {
public:
DsmlParser();
// The chat prompt may already contain the opening thinking marker, so the
// generated text can begin directly with reasoning bytes.
explicit DsmlParser(bool starts_in_thinking = false);
// Feed a chunk of raw model-emitted text. Appends classified events to
// `out`. May buffer the tail of `chunk` internally if it looks like a
@@ -43,7 +45,7 @@ public:
private:
enum class State { TEXT, THINK, TOOL_CALLS, INVOKE, PARAM_VALUE };
State state_ = State::TEXT;
State state_;
std::string buf_;
std::string current_tool_name_;
int tool_index_ = -1;
+133
View File
@@ -0,0 +1,133 @@
// SPDX-License-Identifier: MIT
// Standalone regression tests for the DSML streaming parser.
//
// The repository's backend/cpp/run-unit-tests.sh harness compiles each
// *_test.cpp as a single translation unit, so include the implementation here.
#include "dsml_parser.cpp"
#include <cstdio>
#include <string>
#include <type_traits>
#include <vector>
namespace {
struct ParsedText {
std::string content;
std::string reasoning;
};
int failures = 0;
void check_equal(const std::string &got, const std::string &want,
const char *name) {
if (got == want) return;
std::fprintf(stderr, "FAIL %s: got \"%s\", want \"%s\"\n",
name, got.c_str(), want.c_str());
failures++;
}
void collect_text(const std::vector<ds4cpp::ParserEvent> &events,
ParsedText *parsed) {
for (const auto &event : events) {
if (event.type == ds4cpp::ParserEvent::CONTENT) {
parsed->content += event.text;
} else if (event.type == ds4cpp::ParserEvent::REASONING) {
parsed->reasoning += event.text;
}
}
}
ParsedText parse_chunks(ds4cpp::DsmlParser *parser,
const std::vector<std::string> &chunks) {
ParsedText parsed;
for (const auto &chunk : chunks) {
std::vector<ds4cpp::ParserEvent> events;
parser->Feed(chunk, events);
collect_text(events, &parsed);
}
std::vector<ds4cpp::ParserEvent> events;
parser->Flush(events);
collect_text(events, &parsed);
return parsed;
}
template <typename Parser>
void test_reasoning_opened_by_prompt() {
if constexpr (!std::is_constructible_v<Parser, bool>) {
std::fprintf(stderr,
"FAIL reasoning_opened_by_prompt: parser cannot start in thinking state\n");
failures++;
} else {
Parser parser(true);
ParsedText parsed = parse_chunks(
&parser,
{"We need to calculate factorial recursively.</think>Here is the answer."});
check_equal(parsed.reasoning,
"We need to calculate factorial recursively.",
"reasoning_opened_by_prompt:reasoning");
check_equal(parsed.content, "Here is the answer.",
"reasoning_opened_by_prompt:content");
}
}
template <typename Parser>
Parser text_parser() {
if constexpr (std::is_constructible_v<Parser, bool>) {
return Parser(false);
} else {
return Parser();
}
}
void test_reasoning_disabled() {
auto parser = text_parser<ds4cpp::DsmlParser>();
ParsedText parsed = parse_chunks(&parser, {"Here is the answer."});
check_equal(parsed.reasoning, "", "reasoning_disabled:reasoning");
check_equal(parsed.content, "Here is the answer.",
"reasoning_disabled:content");
}
void test_explicit_think_tag() {
auto parser = text_parser<ds4cpp::DsmlParser>();
ParsedText parsed = parse_chunks(
&parser, {"<think>reasoning</think>answer"});
check_equal(parsed.reasoning, "reasoning", "explicit_think_tag:reasoning");
check_equal(parsed.content, "answer", "explicit_think_tag:content");
}
template <typename Parser>
void test_split_think_close_marker() {
if constexpr (!std::is_constructible_v<Parser, bool>) {
std::fprintf(stderr,
"FAIL split_think_close_marker: parser cannot start in thinking state\n");
failures++;
} else {
Parser parser(true);
ParsedText parsed = parse_chunks(
&parser,
{"We need ", "to calculate ", "factorial", "</thi", "nk>",
"Here is ", "the answer."});
check_equal(parsed.reasoning, "We need to calculate factorial",
"split_think_close_marker:reasoning");
check_equal(parsed.content, "Here is the answer.",
"split_think_close_marker:content");
}
}
} // namespace
int main() {
test_reasoning_opened_by_prompt<ds4cpp::DsmlParser>();
test_reasoning_disabled();
test_explicit_think_tag();
test_split_think_close_marker<ds4cpp::DsmlParser>();
if (failures == 0) {
std::fprintf(stderr, "all dsml_parser checks passed\n");
return 0;
}
std::fprintf(stderr, "%d check(s) failed\n", failures);
return 1;
}
+27
View File
@@ -0,0 +1,27 @@
// SPDX-License-Identifier: MIT
#pragma once
#include <algorithm>
namespace ds4cpp {
inline int EffectiveGenerationLimit(int requested, int context_size,
int session_position) {
const int limit = requested > 0 ? requested : 256;
const int room = context_size - session_position;
if (room <= 1) return 0;
return std::min(limit, room - 1);
}
inline int RemainingGenerationBudget(int effective_limit, int produced) {
if (effective_limit <= produced) return 0;
return effective_limit - produced;
}
inline int SpeculativeAcceptedCapacity(int remaining, int draft_allowance,
int buffer_capacity) {
if (remaining <= 0 || draft_allowance < 0 || buffer_capacity <= 0) return 0;
return std::min({remaining, draft_allowance + 1, buffer_capacity});
}
} // namespace ds4cpp
@@ -0,0 +1,92 @@
// SPDX-License-Identifier: MIT
#include "generation_limits.h"
#include <cstdio>
namespace {
int failures = 0;
void check_equal(int got, int want, const char *name) {
if (got == want) return;
std::fprintf(stderr, "FAIL %s: got %d, want %d\n", name, got, want);
failures++;
}
// Mutation caught: treating omitted or negative max_tokens as unlimited instead
// of preserving DS4's legacy 256-token default.
void test_nonpositive_uses_legacy_default_when_space_permits() {
check_equal(ds4cpp::EffectiveGenerationLimit(0, 4096, 100), 256,
"zero max_tokens uses legacy default");
check_equal(ds4cpp::EffectiveGenerationLimit(-1, 4096, 100), 256,
"negative max_tokens uses legacy default");
}
// Mutation caught: applying the legacy default without clamping it to the
// post-prefill context room and reserved slot.
void test_legacy_default_is_clamped_by_context() {
check_equal(ds4cpp::EffectiveGenerationLimit(0, 300, 100), 199,
"legacy default is context-clamped");
}
// Mutation caught: allowing an explicitly large request to overrun the
// post-prefill context boundary.
void test_large_positive_limit_is_clamped_to_context() {
check_equal(ds4cpp::EffectiveGenerationLimit(32768, 32768, 100), 32667,
"large positive is context-clamped");
}
// Mutation caught: replacing every positive request with the legacy default
// rather than preserving a smaller configured limit.
void test_smaller_positive_limit_is_preserved() {
check_equal(ds4cpp::EffectiveGenerationLimit(64, 4096, 100), 64,
"smaller positive is preserved");
}
// Mutation caught: consuming the final context slot instead of reserving it as
// required by DS4's generation loop.
void test_no_usable_room_returns_zero() {
check_equal(ds4cpp::EffectiveGenerationLimit(32, 100, 99), 0,
"one remaining context slot is not usable");
}
// Mutation caught: sending the original generation limit to a later
// speculative cycle instead of subtracting tokens already produced.
void test_remaining_budget_accounts_for_produced_tokens() {
check_equal(ds4cpp::RemainingGenerationBudget(10, 4), 6,
"remaining budget subtracts produced tokens");
check_equal(ds4cpp::RemainingGenerationBudget(10, 12), 0,
"remaining budget never becomes negative");
}
// Mutation caught: giving speculative evaluation capacity beyond either the
// output budget, the draft allowance plus its first target token, or the fixed
// accepted-token buffer.
void test_speculative_capacity_obeys_all_bounds() {
check_equal(ds4cpp::SpeculativeAcceptedCapacity(3, 8, 8), 3,
"capacity respects remaining output budget");
check_equal(ds4cpp::SpeculativeAcceptedCapacity(20, 4, 8), 5,
"capacity includes one target token beyond draft allowance");
check_equal(ds4cpp::SpeculativeAcceptedCapacity(20, 8, 6), 6,
"capacity respects fixed buffer");
}
} // namespace
int main() {
test_nonpositive_uses_legacy_default_when_space_permits();
test_legacy_default_is_clamped_by_context();
test_large_positive_limit_is_clamped_to_context();
test_smaller_positive_limit_is_preserved();
test_no_usable_room_returns_zero();
test_remaining_budget_accounts_for_produced_tokens();
test_speculative_capacity_obeys_all_bounds();
if (failures == 0) {
std::fprintf(stderr, "all generation limit checks passed\n");
return 0;
}
std::fprintf(stderr, "%d check(s) failed\n", failures);
return 1;
}
+184 -55
View File
@@ -10,7 +10,9 @@
#include "dsml_parser.h" // populated in Task 12
#include "dsml_renderer.h" // populated in Task 16
#include "generation_limits.h"
#include "kv_cache.h" // populated in Task 17
#include "request_lifecycle.h"
extern "C" {
#include "ds4.h"
@@ -35,6 +37,7 @@ extern "C" {
#include <mutex>
#include <string>
#include <thread>
#include <utility>
#include <vector>
using grpc::Server;
@@ -69,6 +72,21 @@ int g_route_timeout_sec = 60;
std::atomic<Server *> g_server{nullptr};
static bool server_context_cancelled(void *ud) {
return static_cast<ServerContext *>(ud)->IsCancelled();
}
static void set_session_cancel(void *target, ds4cpp::CancelCallback callback,
void *userdata) noexcept {
ds4_session_set_cancel(static_cast<ds4_session *>(target), callback, userdata);
}
static bool request_should_continue(ds4cpp::RequestLifecycle *request,
ServerContext *context) {
request->ObserveContextCancellation(context->IsCancelled());
return request->ShouldContinue();
}
// Parse a "key:value" option string. Returns empty when no colon.
static std::pair<std::string, std::string> split_option(const std::string &opt) {
auto colon = opt.find(':');
@@ -238,37 +256,58 @@ static bool apply_engine_option(ds4_engine_options *opt, const std::string &key,
// When acting as a distributed coordinator, block until the worker route
// covers all layers (ds4_session_distributed_route_ready == 1) or the timeout
// elapses. Returns an empty string on success, or an error message to return
// to the client. No-op when not distributed.
// elapses. No-op when not distributed.
//
// Takes the g_engine_mu lock by reference and RELEASES it during each poll
// sleep. The wait can span up to g_route_timeout_sec seconds while workers
// connect; holding g_engine_mu the whole time would block the Status/Health
// readiness probes (they also lock g_engine_mu), making LocalAI's loader treat
// a still-starting worker as hung.
static std::string wait_route_ready(std::unique_lock<std::mutex> &lock) {
if (!g_distributed) return "";
struct RouteWaitResult {
ds4cpp::RouteWaitDecision decision;
std::string error;
};
static RouteWaitResult wait_route_ready(std::unique_lock<std::mutex> &lock,
ServerContext *context) {
if (!g_distributed) return {ds4cpp::RouteWaitDecision::Ready, ""};
char err[256] = {0};
const int deadline_polls = g_route_timeout_sec * 10; // 100ms per poll
for (int i = 0; i <= deadline_polls; ++i) {
int ready = ds4_session_distributed_route_ready(g_session, err, sizeof(err));
if (ready == 1) return "";
if (ready < 0) {
return std::string("ds4 distributed route error: ") +
(err[0] ? err : "unknown");
switch (ds4cpp::DecideRouteWait(ready, context->IsCancelled())) {
case ds4cpp::RouteWaitDecision::Ready:
return {ds4cpp::RouteWaitDecision::Ready, ""};
case ds4cpp::RouteWaitDecision::Error:
return {ds4cpp::RouteWaitDecision::Error,
std::string("ds4 distributed route error: ") +
(err[0] ? err : "unknown")};
case ds4cpp::RouteWaitDecision::Cancelled:
return {ds4cpp::RouteWaitDecision::Cancelled, ""};
case ds4cpp::RouteWaitDecision::Pending:
break;
}
if (i == deadline_polls) break;
// Release the lock while sleeping so Status/Health and other RPCs can
// interleave during worker startup.
lock.unlock();
struct timespec ts = {0, 100L * 1000L * 1000L}; // 100ms
nanosleep(&ts, nullptr);
lock.lock();
if (context->IsCancelled()) {
return {ds4cpp::RouteWaitDecision::Cancelled, ""};
}
// A concurrent Free() may have torn down the engine while we slept.
if (!g_engine || !g_session) {
return "ds4: model unloaded while waiting for distributed route";
return {ds4cpp::RouteWaitDecision::Error,
"ds4: model unloaded while waiting for distributed route"};
}
}
return "ds4 distributed route incomplete: workers not connected (layers uncovered)";
if (context->IsCancelled()) {
return {ds4cpp::RouteWaitDecision::Cancelled, ""};
}
return {ds4cpp::RouteWaitDecision::Error,
"ds4 distributed route incomplete: workers not connected (layers uncovered)"};
}
static void append_token_text(ds4_engine *engine, int token, std::string &out) {
@@ -341,9 +380,9 @@ static void collect_done(void *) {}
struct StreamCtx {
ds4_engine *engine;
ServerWriter<backend::Reply> *writer;
ds4cpp::RequestLifecycle *request;
ds4cpp::DsmlParser parser;
int tokens;
bool aborted;
// Track which tool indices we've seen TOOL_START for, so subsequent
// ARGS deltas can elide the redundant id/name fields.
std::vector<bool> tool_started;
@@ -351,7 +390,7 @@ struct StreamCtx {
static void stream_emit(void *ud, int token) {
auto *s = static_cast<StreamCtx *>(ud);
if (s->aborted) return;
if (!s->request->ShouldContinue()) return;
if (token == ds4_token_eos(s->engine)) return;
size_t len = 0;
const char *text = ds4_token_text(s->engine, token, &len);
@@ -401,7 +440,7 @@ static void stream_emit(void *ud, int token) {
reply.set_message(chunk);
reply.set_tokens(1);
if (any_field) {
if (!s->writer->Write(reply)) s->aborted = true;
s->request->ObserveStreamWrite(s->writer->Write(reply));
}
s->tokens++;
}
@@ -757,21 +796,30 @@ public:
return GStatus::OK;
}
GStatus Predict(ServerContext *, const backend::PredictOptions *request,
GStatus Predict(ServerContext *context, const backend::PredictOptions *request,
backend::Reply *reply) override {
std::unique_lock<std::mutex> lock(g_engine_mu);
if (!g_engine || !g_session) {
return GStatus(StatusCode::FAILED_PRECONDITION, "ds4: model not loaded");
}
if (GStatus id = check_model_identity(request); !id.ok()) return id;
if (std::string route_err = wait_route_ready(lock); !route_err.empty()) {
return GStatus(StatusCode::UNAVAILABLE, route_err);
RouteWaitResult route = wait_route_ready(lock, context);
if (route.decision == ds4cpp::RouteWaitDecision::Cancelled) {
return GStatus(StatusCode::CANCELLED, "ds4 request cancelled");
}
if (route.decision == ds4cpp::RouteWaitDecision::Error) {
return GStatus(StatusCode::UNAVAILABLE, route.error);
}
ds4_tokens prompt = {};
build_prompt(g_engine, request, &prompt);
int n_predict = request->tokens() > 0 ? request->tokens() : 256;
CollectCtx collect = {g_engine, "", {}, reply, 0, {}, "", ""};
const bool think_enabled = ds4_think_mode_enabled(parse_think_mode(request));
const bool starts_in_thinking = think_enabled &&
request->usetokenizertemplate() && request->messages_size() > 0;
CollectCtx collect = {
g_engine, "", ds4cpp::DsmlParser(starts_in_thinking),
reply, 0, {}, "", ""};
ds4cpp::RequestLifecycle lifecycle;
std::string cache_key = render_prompt_text(request);
size_t cache_hit = maybe_load_cache(cache_key);
(void)cache_hit; // future: skip prompt prefix if hit covers full prompt
@@ -783,15 +831,27 @@ public:
// Either way g_session advances so the disk KV cache picks up a
// real checkpoint after the call (see maybe_save_cache below).
char err[256] = {0};
int rc = ds4_session_sync(g_session, &prompt, err, sizeof(err));
int rc;
{
ds4cpp::CancelCallbackScope cancel_scope(
g_session, set_session_cancel, server_context_cancelled, context);
rc = ds4_session_sync(g_session, &prompt, err, sizeof(err));
}
int prompt_len = prompt.len;
ds4_tokens_free(&prompt);
if (rc == 0) {
if (rc == DS4_SESSION_SYNC_INTERRUPTED) {
lifecycle.ObserveContextCancellation(true);
}
const bool generation_started = rc == 0;
if (generation_started) {
const int n_predict = ds4cpp::EffectiveGenerationLimit(
request->tokens(), ds4_session_ctx(g_session),
ds4_session_pos(g_session));
const int eos = ds4_token_eos(g_engine);
const int draft_max = ds4_engine_mtp_draft_tokens(g_engine);
const bool think_enabled = ds4_think_mode_enabled(parse_think_mode(request));
int produced = 0;
while (produced < n_predict) {
if (!request_should_continue(&lifecycle, context)) break;
SampleParams sp = compute_sample_params(request, collect.parser, think_enabled);
int first;
if (sp.temperature <= 0.0f) {
@@ -806,13 +866,20 @@ public:
if (draft_max > 0 && sp.temperature <= 0.0f) {
constexpr int kAcceptedMax = 8;
int accepted[kAcceptedMax];
int cap = std::min(kAcceptedMax, draft_max + 1);
const int remaining = ds4cpp::RemainingGenerationBudget(
n_predict, produced);
const int cap = ds4cpp::SpeculativeAcceptedCapacity(
remaining, draft_max, kAcceptedMax);
int n = ds4_session_eval_speculative_argmax(
g_session, first, draft_max, eos,
g_session, first, remaining, eos,
accepted, cap, err, sizeof(err));
if (n < 0) { rc = -1; break; }
bool stop = false;
for (int j = 0; j < n; ++j) {
if (!request_should_continue(&lifecycle, context)) {
stop = true;
break;
}
if (accepted[j] == eos) { stop = true; break; }
collect_emit(&collect, accepted[j]);
if (++produced >= n_predict) { stop = true; break; }
@@ -821,12 +888,26 @@ public:
} else {
collect_emit(&collect, first);
if (++produced >= n_predict) break;
if (!request_should_continue(&lifecycle, context)) break;
rc = ds4_session_eval(g_session, first, err, sizeof(err));
if (rc != 0) break;
}
}
collect_done(&collect);
}
request_should_continue(&lifecycle, context);
ds4cpp::TerminalDecision terminal = ds4cpp::ResolveTerminalDecision(
rc == DS4_SESSION_SYNC_INTERRUPTED, rc != 0,
!lifecycle.ShouldFinalize());
if (!terminal.should_finalize) {
if (terminal.cause == ds4cpp::TerminalCause::EngineError) {
return GStatus(StatusCode::INTERNAL,
std::string("ds4 generation failed: ") + err);
}
return GStatus(StatusCode::CANCELLED,
"ds4 request cancelled");
}
if (generation_started) collect_done(&collect);
maybe_save_cache(cache_key);
// Flush any buffered parser state.
@@ -834,7 +915,7 @@ public:
collect.parser.Flush(events);
apply_events(&collect, events);
if (rc != 0) {
if (terminal.cause == ds4cpp::TerminalCause::EngineError) {
return GStatus(StatusCode::INTERNAL,
std::string("ds4 generation failed: ") + err);
}
@@ -857,21 +938,30 @@ public:
return GStatus::OK;
}
GStatus PredictStream(ServerContext *, const backend::PredictOptions *request,
GStatus PredictStream(ServerContext *context, const backend::PredictOptions *request,
ServerWriter<backend::Reply> *writer) override {
std::unique_lock<std::mutex> lock(g_engine_mu);
if (!g_engine || !g_session) {
return GStatus(StatusCode::FAILED_PRECONDITION, "ds4: model not loaded");
}
if (GStatus id = check_model_identity(request); !id.ok()) return id;
if (std::string route_err = wait_route_ready(lock); !route_err.empty()) {
return GStatus(StatusCode::UNAVAILABLE, route_err);
RouteWaitResult route = wait_route_ready(lock, context);
if (route.decision == ds4cpp::RouteWaitDecision::Cancelled) {
return GStatus(StatusCode::CANCELLED, "ds4 request cancelled");
}
if (route.decision == ds4cpp::RouteWaitDecision::Error) {
return GStatus(StatusCode::UNAVAILABLE, route.error);
}
ds4_tokens prompt = {};
build_prompt(g_engine, request, &prompt);
int n_predict = request->tokens() > 0 ? request->tokens() : 256;
StreamCtx s = {g_engine, writer, {}, 0, false, {}};
const bool think_enabled = ds4_think_mode_enabled(parse_think_mode(request));
const bool starts_in_thinking = think_enabled &&
request->usetokenizertemplate() && request->messages_size() > 0;
ds4cpp::RequestLifecycle lifecycle;
StreamCtx s = {
g_engine, writer, &lifecycle,
ds4cpp::DsmlParser(starts_in_thinking), 0, {}};
std::string cache_key = render_prompt_text(request);
size_t cache_hit = maybe_load_cache(cache_key);
(void)cache_hit;
@@ -879,14 +969,26 @@ public:
// Manual loop on g_session - see Predict() above for the rationale.
// MTP speculative path used when ds4_engine_mtp_draft_tokens > 0.
char err[256] = {0};
int rc = ds4_session_sync(g_session, &prompt, err, sizeof(err));
int rc;
{
ds4cpp::CancelCallbackScope cancel_scope(
g_session, set_session_cancel, server_context_cancelled, context);
rc = ds4_session_sync(g_session, &prompt, err, sizeof(err));
}
ds4_tokens_free(&prompt);
if (rc == 0) {
if (rc == DS4_SESSION_SYNC_INTERRUPTED) {
lifecycle.ObserveContextCancellation(true);
}
const bool generation_started = rc == 0;
if (generation_started) {
const int n_predict = ds4cpp::EffectiveGenerationLimit(
request->tokens(), ds4_session_ctx(g_session),
ds4_session_pos(g_session));
const int eos = ds4_token_eos(g_engine);
const int draft_max = ds4_engine_mtp_draft_tokens(g_engine);
const bool think_enabled = ds4_think_mode_enabled(parse_think_mode(request));
int produced = 0;
while (produced < n_predict && !s.aborted) {
while (produced < n_predict) {
if (!request_should_continue(&lifecycle, context)) break;
SampleParams sp = compute_sample_params(request, s.parser, think_enabled);
int first;
if (sp.temperature <= 0.0f) {
@@ -900,50 +1002,77 @@ public:
if (draft_max > 0 && sp.temperature <= 0.0f) {
constexpr int kAcceptedMax = 8;
int accepted[kAcceptedMax];
int cap = std::min(kAcceptedMax, draft_max + 1);
const int remaining = ds4cpp::RemainingGenerationBudget(
n_predict, produced);
const int cap = ds4cpp::SpeculativeAcceptedCapacity(
remaining, draft_max, kAcceptedMax);
int n = ds4_session_eval_speculative_argmax(
g_session, first, draft_max, eos,
g_session, first, remaining, eos,
accepted, cap, err, sizeof(err));
if (n < 0) { rc = -1; break; }
bool stop = false;
for (int j = 0; j < n; ++j) {
if (!request_should_continue(&lifecycle, context)) {
stop = true;
break;
}
if (accepted[j] == eos) { stop = true; break; }
stream_emit(&s, accepted[j]);
if (s.aborted) { stop = true; break; }
if (!lifecycle.ShouldContinue()) { stop = true; break; }
if (++produced >= n_predict) { stop = true; break; }
}
if (stop) break;
} else {
stream_emit(&s, first);
if (s.aborted || ++produced >= n_predict) break;
if (!lifecycle.ShouldContinue() || ++produced >= n_predict) break;
if (!request_should_continue(&lifecycle, context)) break;
rc = ds4_session_eval(g_session, first, err, sizeof(err));
if (rc != 0) break;
}
}
stream_done(&s);
}
maybe_save_cache(cache_key);
// Flush parser state.
std::vector<ds4cpp::ParserEvent> events;
s.parser.Flush(events);
if (!events.empty() && !s.aborted) {
backend::Reply reply;
auto *delta = reply.add_chat_deltas();
for (const auto &e : events) {
if (e.type == ds4cpp::ParserEvent::CONTENT) {
delta->set_content(delta->content() + e.text);
} else if (e.type == ds4cpp::ParserEvent::REASONING) {
delta->set_reasoning_content(delta->reasoning_content() + e.text);
request_should_continue(&lifecycle, context);
ds4cpp::TerminalDecision terminal = ds4cpp::ResolveTerminalDecision(
rc == DS4_SESSION_SYNC_INTERRUPTED, rc != 0,
!lifecycle.ShouldFinalize());
terminal = ds4cpp::RunPostlude(
terminal,
[&]() {
ds4cpp::DsmlParser staged_parser = s.parser;
std::vector<ds4cpp::ParserEvent> events;
staged_parser.Flush(events);
bool write_succeeded = true;
if (!events.empty()) {
backend::Reply reply;
auto *delta = reply.add_chat_deltas();
for (const auto &e : events) {
if (e.type == ds4cpp::ParserEvent::CONTENT) {
delta->set_content(delta->content() + e.text);
} else if (e.type == ds4cpp::ParserEvent::REASONING) {
delta->set_reasoning_content(
delta->reasoning_content() + e.text);
}
}
write_succeeded = s.writer->Write(reply);
}
}
s.writer->Write(reply);
}
lifecycle.ObserveStreamWrite(write_succeeded);
request_should_continue(&lifecycle, context);
if (!lifecycle.ShouldFinalize()) return false;
s.parser = std::move(staged_parser);
if (generation_started) stream_done(&s);
return true;
},
[&]() { maybe_save_cache(cache_key); });
if (rc != 0 && !s.aborted) {
if (terminal.cause == ds4cpp::TerminalCause::EngineError) {
return GStatus(StatusCode::INTERNAL,
std::string("ds4 generation failed: ") + err);
}
if (terminal.cause == ds4cpp::TerminalCause::Cancelled) {
return GStatus(StatusCode::CANCELLED,
"ds4 request cancelled");
}
return GStatus::OK;
}
+111
View File
@@ -0,0 +1,111 @@
// SPDX-License-Identifier: MIT
#pragma once
namespace ds4cpp {
using CancelCallback = bool (*)(void *);
using CancelSetter = void (*)(void *, CancelCallback, void *) noexcept;
class CancelCallbackScope {
public:
CancelCallbackScope(void *target, CancelSetter setter,
CancelCallback callback, void *userdata) noexcept
: target_(target), setter_(setter) {
setter_(target_, callback, userdata);
}
~CancelCallbackScope() noexcept {
setter_(target_, nullptr, nullptr);
}
CancelCallbackScope(const CancelCallbackScope &) = delete;
CancelCallbackScope &operator=(const CancelCallbackScope &) = delete;
private:
void *target_;
CancelSetter setter_;
};
enum class RouteWaitDecision {
Pending,
Ready,
Error,
Cancelled,
};
inline RouteWaitDecision DecideRouteWait(int route_status, bool cancelled) {
if (cancelled) return RouteWaitDecision::Cancelled;
if (route_status > 0) return RouteWaitDecision::Ready;
if (route_status < 0) return RouteWaitDecision::Error;
return RouteWaitDecision::Pending;
}
enum class TerminalCause {
Success,
Cancelled,
EngineError,
};
inline TerminalCause DecideTerminalCause(bool sync_interrupted,
bool engine_error,
bool abandoned) {
if (sync_interrupted) return TerminalCause::Cancelled;
if (engine_error) return TerminalCause::EngineError;
if (abandoned) return TerminalCause::Cancelled;
return TerminalCause::Success;
}
struct TerminalDecision {
TerminalCause cause;
bool should_finalize;
};
inline TerminalDecision ResolveTerminalDecision(bool sync_interrupted,
bool engine_error,
bool abandoned) {
return {
DecideTerminalCause(sync_interrupted, engine_error, abandoned),
!sync_interrupted && !abandoned,
};
}
template <typename Finalize, typename Persist>
TerminalDecision RunPostlude(TerminalDecision terminal,
Finalize transactional_finalize,
Persist persist) {
if (!terminal.should_finalize) return terminal;
if (!transactional_finalize()) {
terminal.should_finalize = false;
if (terminal.cause != TerminalCause::EngineError) {
terminal.cause = TerminalCause::Cancelled;
}
return terminal;
}
persist();
return terminal;
}
class RequestLifecycle {
public:
void ObserveContextCancellation(bool cancelled) {
context_cancelled_ = context_cancelled_ || cancelled;
}
void ObserveStreamWrite(bool succeeded) {
stream_write_aborted_ = stream_write_aborted_ || !succeeded;
}
bool ShouldContinue() const {
return !context_cancelled_ && !stream_write_aborted_;
}
bool ShouldFinalize() const {
return ShouldContinue();
}
private:
bool context_cancelled_ = false;
bool stream_write_aborted_ = false;
};
} // namespace ds4cpp
+414
View File
@@ -0,0 +1,414 @@
// SPDX-License-Identifier: MIT
// Standalone regression tests for DS4 request cancellation policy.
#include "request_lifecycle.h"
#include <cstdio>
namespace {
int failures = 0;
struct FakeCancelTarget {
ds4cpp::CancelCallback callback = nullptr;
void *userdata = nullptr;
int installs = 0;
int clears = 0;
};
struct PostludeCounts {
int finalize_attempts = 0;
int finalize_commits = 0;
int cache_persists = 0;
bool cache_followed_commit = true;
};
ds4cpp::TerminalDecision run_fake_postlude(
ds4cpp::TerminalDecision terminal, bool finalize_succeeds,
PostludeCounts *counts) {
return ds4cpp::RunPostlude(
terminal,
[=]() {
counts->finalize_attempts++;
if (!finalize_succeeds) return false;
counts->finalize_commits++;
return true;
},
[=]() {
counts->cache_followed_commit = counts->finalize_commits == 1;
counts->cache_persists++;
});
}
bool fake_cancel(void *) {
return false;
}
void fake_set_cancel(void *target, ds4cpp::CancelCallback callback,
void *userdata) noexcept {
auto *fake = static_cast<FakeCancelTarget *>(target);
fake->callback = callback;
fake->userdata = userdata;
if (callback) {
fake->installs++;
} else {
fake->clears++;
}
}
void check(bool condition, const char *name) {
if (condition) return;
std::fprintf(stderr, "FAIL %s\n", name);
failures++;
}
// Production mutation caught: treating an active request as abandoned would
// skip its parser finalization and cache save.
void test_active_request_continues_and_finalizes() {
ds4cpp::RequestLifecycle request;
check(request.ShouldContinue(), "active:continue");
check(request.ShouldFinalize(), "active:finalize");
}
// Production mutation caught: omitting the ServerContext cancellation branch
// would continue decoding and finalize a partial response.
void test_context_cancellation_stops_without_finalizing() {
ds4cpp::RequestLifecycle request;
request.ObserveContextCancellation(true);
check(!request.ShouldContinue(), "context_cancelled:stop");
check(!request.ShouldFinalize(), "context_cancelled:no_finalize");
}
// Production mutation caught: ignoring ServerWriter::Write failure would keep
// streaming and finalize a response whose client has gone away.
void test_stream_write_abort_stops_without_finalizing() {
ds4cpp::RequestLifecycle request;
request.ObserveStreamWrite(false);
check(!request.ShouldContinue(), "write_abort:stop");
check(!request.ShouldFinalize(), "write_abort:no_finalize");
}
// Production mutation caught: combining cancellation and write failure with
// AND would fail to stop when either signal occurs on its own.
void test_cancellation_and_write_abort_are_independent_or_conditions() {
ds4cpp::RequestLifecycle cancelled;
cancelled.ObserveContextCancellation(true);
cancelled.ObserveStreamWrite(true);
ds4cpp::RequestLifecycle write_aborted;
write_aborted.ObserveContextCancellation(false);
write_aborted.ObserveStreamWrite(false);
check(!cancelled.ShouldContinue(), "or:context_only");
check(!write_aborted.ShouldContinue(), "or:write_only");
}
// Production mutation caught: treating an incomplete distributed route as an
// error would return before workers have time to connect.
void test_route_wait_pending() {
check(ds4cpp::DecideRouteWait(0, false) ==
ds4cpp::RouteWaitDecision::Pending,
"route_wait:pending");
}
// Production mutation caught: failing to recognize a complete route would
// keep a ready inference request in the polling loop.
void test_route_wait_ready() {
check(ds4cpp::DecideRouteWait(1, false) ==
ds4cpp::RouteWaitDecision::Ready,
"route_wait:ready");
}
// Production mutation caught: ignoring a route probe error would poll until a
// misleading timeout instead of returning UNAVAILABLE promptly.
void test_route_wait_error() {
check(ds4cpp::DecideRouteWait(-1, false) ==
ds4cpp::RouteWaitDecision::Error,
"route_wait:error");
}
// Production mutation caught: omitting cancellation from route waiting would
// leave an abandoned request blocked until the distributed timeout.
void test_route_wait_cancellation() {
check(ds4cpp::DecideRouteWait(0, true) ==
ds4cpp::RouteWaitDecision::Cancelled,
"route_wait:cancelled");
}
// Production mutation caught: checking route errors before cancellation would
// report UNAVAILABLE for a request the client already abandoned.
void test_route_wait_cancellation_precedes_error() {
check(ds4cpp::DecideRouteWait(-1, true) ==
ds4cpp::RouteWaitDecision::Cancelled,
"route_wait:cancellation_precedence");
}
// Production mutation caught: classifying a successful active request as a
// terminal failure would suppress its normal response finalization.
void test_terminal_success() {
check(ds4cpp::DecideTerminalCause(false, false, false) ==
ds4cpp::TerminalCause::Success,
"terminal:success");
}
// Production mutation caught: treating DS4's cooperative sync interruption
// as an ordinary engine error would return INTERNAL instead of CANCELLED.
void test_terminal_sync_interruption_is_cancelled() {
check(ds4cpp::DecideTerminalCause(true, true, true) ==
ds4cpp::TerminalCause::Cancelled,
"terminal:sync_interrupted");
}
// Production mutation caught: treating every nonzero engine result as client
// abandonment would hide genuine DS4 failures behind CANCELLED.
void test_terminal_engine_error() {
check(ds4cpp::DecideTerminalCause(false, true, false) ==
ds4cpp::TerminalCause::EngineError,
"terminal:engine_error");
}
// Production mutation caught: ignoring an rc==0 context cancellation would
// finalize and cache an abandoned request.
void test_terminal_context_abandonment() {
ds4cpp::RequestLifecycle request;
request.ObserveContextCancellation(true);
check(ds4cpp::DecideTerminalCause(
false, false, !request.ShouldFinalize()) ==
ds4cpp::TerminalCause::Cancelled,
"terminal:context_abandonment");
}
// Production mutation caught: ignoring an rc==0 stream write failure would
// finalize and cache an abandoned streaming request.
void test_terminal_write_abandonment() {
ds4cpp::RequestLifecycle request;
request.ObserveStreamWrite(false);
check(ds4cpp::DecideTerminalCause(
false, false, !request.ShouldFinalize()) ==
ds4cpp::TerminalCause::Cancelled,
"terminal:write_abandonment");
}
// Production mutation caught: checking late cancellation or write failure
// before a determined ordinary DS4 error would replace INTERNAL with CANCELLED.
void test_terminal_engine_error_precedes_late_abandonment() {
ds4cpp::RequestLifecycle cancelled;
cancelled.ObserveContextCancellation(true);
ds4cpp::RequestLifecycle write_aborted;
write_aborted.ObserveStreamWrite(false);
check(ds4cpp::DecideTerminalCause(
false, true, !cancelled.ShouldFinalize()) ==
ds4cpp::TerminalCause::EngineError,
"terminal:engine_error_precedes_cancellation");
check(ds4cpp::DecideTerminalCause(
false, true, !write_aborted.ShouldFinalize()) ==
ds4cpp::TerminalCause::EngineError,
"terminal:engine_error_precedes_write_abort");
}
// Production mutation caught: using status precedence alone to gate side
// effects would finalize and persist an engine-error request abandoned later.
void test_abandoned_engine_error_keeps_internal_without_finalizing() {
ds4cpp::RequestLifecycle request;
request.ObserveContextCancellation(true);
ds4cpp::TerminalDecision terminal = ds4cpp::ResolveTerminalDecision(
false, true, !request.ShouldFinalize());
check(terminal.cause == ds4cpp::TerminalCause::EngineError,
"terminal_decision:abandoned_engine_error_status");
check(!terminal.should_finalize,
"terminal_decision:abandoned_engine_error_no_finalize");
}
// Production mutation caught: suppressing side effects for every engine error
// would change the existing finalization and cache behavior of active failures.
void test_active_engine_error_still_finalizes() {
ds4cpp::RequestLifecycle request;
ds4cpp::TerminalDecision terminal = ds4cpp::ResolveTerminalDecision(
false, true, !request.ShouldFinalize());
check(terminal.cause == ds4cpp::TerminalCause::EngineError,
"terminal_decision:active_engine_error_status");
check(terminal.should_finalize,
"terminal_decision:active_engine_error_finalize");
}
// Production mutation caught: persisting before committed finalization would
// cache a state whose final buffered stream reply was never completed.
void test_postlude_active_success_commits_then_persists() {
PostludeCounts counts;
ds4cpp::TerminalDecision terminal = run_fake_postlude(
{ds4cpp::TerminalCause::Success, true}, true, &counts);
check(terminal.cause == ds4cpp::TerminalCause::Success,
"postlude:success_outcome");
check(terminal.should_finalize, "postlude:success_committed");
check(counts.finalize_attempts == 1, "postlude:success_attempts");
check(counts.finalize_commits == 1, "postlude:success_commits");
check(counts.cache_persists == 1, "postlude:success_cache");
check(counts.cache_followed_commit, "postlude:success_cache_order");
}
// Production mutation caught: starting the postlude for an already-cancelled
// request would flush buffered parser state or persist an abandoned session.
void test_postlude_cancellation_skips_all_side_effects() {
PostludeCounts counts;
ds4cpp::TerminalDecision terminal = run_fake_postlude(
{ds4cpp::TerminalCause::Cancelled, false}, true, &counts);
check(terminal.cause == ds4cpp::TerminalCause::Cancelled,
"postlude:cancelled_outcome");
check(counts.finalize_attempts == 0, "postlude:cancelled_attempts");
check(counts.finalize_commits == 0, "postlude:cancelled_commits");
check(counts.cache_persists == 0, "postlude:cancelled_cache");
}
// Production mutation caught: committing the live parser or cache after a
// failed final Write would publish an abandoned streaming postlude.
void test_postlude_finalize_failure_cancels_without_commit_or_cache() {
PostludeCounts counts;
ds4cpp::TerminalDecision terminal = run_fake_postlude(
{ds4cpp::TerminalCause::Success, true}, false, &counts);
check(terminal.cause == ds4cpp::TerminalCause::Cancelled,
"postlude:write_failure_outcome");
check(!terminal.should_finalize, "postlude:write_failure_not_committed");
check(counts.finalize_attempts == 1, "postlude:write_failure_attempts");
check(counts.finalize_commits == 0, "postlude:write_failure_commits");
check(counts.cache_persists == 0, "postlude:write_failure_cache");
}
// Production mutation caught: skipping the postlude for every engine error
// would change active internal-error finalization and cache behavior.
void test_postlude_active_engine_error_finalizes_and_persists() {
PostludeCounts counts;
ds4cpp::TerminalDecision terminal = run_fake_postlude(
{ds4cpp::TerminalCause::EngineError, true}, true, &counts);
check(terminal.cause == ds4cpp::TerminalCause::EngineError,
"postlude:engine_error_outcome");
check(counts.finalize_attempts == 1, "postlude:engine_error_attempts");
check(counts.finalize_commits == 1, "postlude:engine_error_commits");
check(counts.cache_persists == 1, "postlude:engine_error_cache");
check(counts.cache_followed_commit, "postlude:engine_error_cache_order");
}
// Production mutation caught: replacing every failed transactional finalize
// with cancellation would hide an already-determined engine error.
void test_postlude_engine_error_finalize_failure_preserves_internal() {
PostludeCounts counts;
ds4cpp::TerminalDecision terminal = run_fake_postlude(
{ds4cpp::TerminalCause::EngineError, true}, false, &counts);
check(terminal.cause == ds4cpp::TerminalCause::EngineError,
"postlude:engine_error_write_failure_outcome");
check(!terminal.should_finalize,
"postlude:engine_error_write_failure_not_committed");
check(counts.finalize_attempts == 1,
"postlude:engine_error_write_failure_attempts");
check(counts.finalize_commits == 0,
"postlude:engine_error_write_failure_commits");
check(counts.cache_persists == 0,
"postlude:engine_error_write_failure_cache");
}
// Production mutation caught: status precedence must not grant side-effect
// permission to an engine-error request that was also abandoned.
void test_postlude_abandoned_engine_error_skips_all_side_effects() {
PostludeCounts counts;
ds4cpp::TerminalDecision terminal = run_fake_postlude(
{ds4cpp::TerminalCause::EngineError, false}, true, &counts);
check(terminal.cause == ds4cpp::TerminalCause::EngineError,
"postlude:abandoned_engine_error_outcome");
check(counts.finalize_attempts == 0,
"postlude:abandoned_engine_error_attempts");
check(counts.finalize_commits == 0,
"postlude:abandoned_engine_error_commits");
check(counts.cache_persists == 0,
"postlude:abandoned_engine_error_cache");
}
// Production mutation caught: failing to install the request callback would
// make DS4 prompt synchronization unable to observe client cancellation.
void test_cancel_callback_scope_installs_callback() {
FakeCancelTarget target;
int request_context = 42;
{
ds4cpp::CancelCallbackScope scope(
&target, fake_set_cancel, fake_cancel, &request_context);
check(target.callback == fake_cancel, "cancel_scope:callback_installed");
check(target.userdata == &request_context, "cancel_scope:userdata_installed");
check(target.installs == 1, "cancel_scope:installed_once");
}
}
// Production mutation caught: failing to clear the callback at every scope
// exit would leave DS4 pointing at a destroyed stack-owned ServerContext.
void test_cancel_callback_scope_clears_callback() {
FakeCancelTarget target;
int request_context = 42;
{
ds4cpp::CancelCallbackScope scope(
&target, fake_set_cancel, fake_cancel, &request_context);
}
check(target.callback == nullptr, "cancel_scope:callback_cleared");
check(target.userdata == nullptr, "cancel_scope:userdata_cleared");
check(target.clears == 1, "cancel_scope:cleared_once");
}
} // namespace
int main() {
test_active_request_continues_and_finalizes();
test_context_cancellation_stops_without_finalizing();
test_stream_write_abort_stops_without_finalizing();
test_cancellation_and_write_abort_are_independent_or_conditions();
test_route_wait_pending();
test_route_wait_ready();
test_route_wait_error();
test_route_wait_cancellation();
test_route_wait_cancellation_precedes_error();
test_terminal_success();
test_terminal_sync_interruption_is_cancelled();
test_terminal_engine_error();
test_terminal_context_abandonment();
test_terminal_write_abandonment();
test_terminal_engine_error_precedes_late_abandonment();
test_abandoned_engine_error_keeps_internal_without_finalizing();
test_active_engine_error_still_finalizes();
test_postlude_active_success_commits_then_persists();
test_postlude_cancellation_skips_all_side_effects();
test_postlude_finalize_failure_cancels_without_commit_or_cache();
test_postlude_active_engine_error_finalizes_and_persists();
test_postlude_engine_error_finalize_failure_preserves_internal();
test_postlude_abandoned_engine_error_skips_all_side_effects();
test_cancel_callback_scope_installs_callback();
test_cancel_callback_scope_clears_callback();
if (failures == 0) {
std::fprintf(stderr, "all request_lifecycle checks passed\n");
return 0;
}
std::fprintf(stderr, "%d check(s) failed\n", failures);
return 1;
}
+1 -1
View File
@@ -1,5 +1,5 @@
IK_LLAMA_VERSION?=cf1aa57e1a0fabfd015831718fc99d1aec01ada5
IK_LLAMA_VERSION?=1a2a8604a6c6c6413c06bf9adfc2f64329af4366
LLAMA_REPO?=https://github.com/ikawrakow/ik_llama.cpp
CMAKE_ARGS?=
+1
View File
@@ -2565,6 +2565,7 @@ public:
grpc::Status Embedding(ServerContext* context, const backend::PredictOptions* request, backend::EmbeddingResult* embeddingResult) {
auto identity = checkModelIdentity(request);
if (!identity.ok()) return identity;
embeddingResult->set_layout(backend::EMBEDDING_LAYOUT_FINAL);
json data = parse_options(false, request, llama);
const int task_id = llama.queue_tasks.get_new_id();
llama.queue_results.add_waiting_task_id(task_id);
+10
View File
@@ -115,4 +115,14 @@ if(LLAMA_GRPC_BUILD_TESTS)
target_include_directories(passthrough_options_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_compile_features(passthrough_options_test PRIVATE cxx_std_17)
add_test(NAME passthrough_options_test COMMAND passthrough_options_test)
add_executable(tts_request_options_test tts_request_options_test.cpp tts_request_options.h)
target_include_directories(tts_request_options_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_compile_features(tts_request_options_test PRIVATE cxx_std_17)
add_test(NAME tts_request_options_test COMMAND tts_request_options_test)
add_executable(thread_params_test thread_params_test.cpp thread_params.h)
target_include_directories(thread_params_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_compile_features(thread_params_test PRIVATE cxx_std_17)
add_test(NAME thread_params_test COMMAND thread_params_test)
endif()
+1 -1
View File
@@ -1,5 +1,5 @@
LLAMA_VERSION?=221f0f6356efe2260023208365705ec5d5a7c8f5
LLAMA_VERSION?=67672dc5b76f8bc17785a19d3dc6d1463fc2902c
LLAMA_REPO?=https://github.com/ggerganov/llama.cpp
CMAKE_ARGS?=
+43
View File
@@ -0,0 +1,43 @@
#!/bin/bash
# Mark a copied gRPC server as targeting a llama.cpp fork that does not carry
# LocalAI's SERVER_TASK_TYPE_TTS patch. The RPCs remain present in the shared
# protobuf service, but respond with UNIMPLEMENTED instead of referencing
# server task types and mtmd gen-audio APIs absent from those forks.
set -euo pipefail
if [[ $# -ne 1 ]]; then
echo "usage: $0 <grpc-server.cpp>" >&2
exit 2
fi
SRC=$1
if [[ ! -f "$SRC" ]]; then
echo "grpc-server.cpp not found at $SRC" >&2
exit 2
fi
if grep -q '^#define LOCALAI_LLAMA_CPP_NO_TTS_TASK' "$SRC"; then
echo "==> $SRC already disables the LocalAI TTS task, skipping"
exit 0
fi
awk '
!done && /^#include/ {
print "#define LOCALAI_LLAMA_CPP_NO_TTS_TASK 1"
print "// ^ injected by disable-tts-task.sh for an unpatched llama.cpp fork"
print ""
done = 1
}
{ print }
END {
if (!done) {
print "disable-tts-task.sh: no #include anchor found" > "/dev/stderr"
exit 1
}
}
' "$SRC" > "$SRC.tmp"
mv "$SRC.tmp" "$SRC"
echo "==> LocalAI TTS task disabled in $SRC"
+359 -72
View File
@@ -53,8 +53,10 @@
#include "arg.h"
#include "chat-auto-parser.h"
#include "llama_compat.h" // fork-skew switches, generated by prepare.sh
#include "thread_params.h"
#include "message_content.h"
#include "passthrough_options.h"
#include "tts_request_options.h"
#include <getopt.h>
#include <grpcpp/ext/proto_server_reflection_plugin.h>
#include <grpcpp/grpcpp.h>
@@ -65,6 +67,7 @@
#include <atomic>
#include <cmath>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <iterator>
#include <list>
@@ -85,6 +88,12 @@ using grpc::ServerBuilder;
using grpc::ServerContext;
using grpc::Status;
#if LOCALAI_HAS_MTMD_INIT_OPT
#define LOCALAI_MTMD_INIT_OPT_ARG(value) , value
#else
#define LOCALAI_MTMD_INIT_OPT_ARG(value)
#endif
// gRPC bearer token auth for distributed mode.
// Reads LOCALAI_GRPC_AUTH_TOKEN from the environment. When set, rejects
// requests without a matching "authorization: Bearer <token>" metadata header.
@@ -233,7 +242,15 @@ json parse_options(bool streaming, const backend::PredictOptions* predict, const
data["typical_p"] = predict->typicalp();
data["temperature"] = predict->temperature();
data["repeat_last_n"] = predict->repeat();
data["repeat_penalty"] = predict->penalty();
// PredictOptions.Penalty is a bare proto float, so a caller that names no
// repetition penalty sends 0 rather than omitting the field. Since
// llama.cpp 9de0fcf2b, common_sampler_init() rejects a non-positive
// penalty_repeat outright (it would divide logits by zero), which turned
// every such request into "Failed to initialize samplers". Treat 0 as
// "unset" and leave llama.cpp's own neutral default in place.
if (predict->penalty() > 0.0f) {
data["repeat_penalty"] = predict->penalty();
}
data["frequency_penalty"] = predict->frequencypenalty();
data["presence_penalty"] = predict->presencepenalty();
data["mirostat"] = predict->mirostat();
@@ -283,7 +300,7 @@ json parse_options(bool streaming, const backend::PredictOptions* predict, const
} else {
SRV_WRN("[TOOLS DEBUG] parse_options: Parsed tools JSON is not an array: %s\n", tools_json.dump().c_str());
}
} catch (const json::parse_error& e) {
} catch (const common_json_error& e) {
SRV_WRN("Failed to parse tools JSON from proto: %s\n", e.what());
SRV_WRN("[TOOLS DEBUG] parse_options: Tools string that failed to parse: %s\n", predict->tools().c_str());
}
@@ -313,7 +330,7 @@ json parse_options(bool streaming, const backend::PredictOptions* predict, const
SRV_DBG("[TOOLS DEBUG] Received tool_choice object from Go layer: %s\n", tool_choice_json.dump().c_str());
}
SRV_INF("Extracted tool_choice from proto: %s\n", predict->toolchoice().c_str());
} catch (const json::parse_error& e) {
} catch (const common_json_error& e) {
// If parsing fails, treat as string
data["tool_choice"] = predict->toolchoice();
SRV_INF("Extracted tool_choice as string: %s\n", predict->toolchoice().c_str());
@@ -342,7 +359,7 @@ json parse_options(bool streaming, const backend::PredictOptions* predict, const
// Add to data - llama.cpp server expects it as an object (map)
data["logit_bias"] = logit_bias_json;
SRV_INF("Using logit_bias: %s\n", predict->logitbias().c_str());
} catch (const json::parse_error& e) {
} catch (const common_json_error& e) {
SRV_ERR("Failed to parse logit_bias JSON from proto: %s\n", e.what());
}
}
@@ -387,7 +404,10 @@ json parse_options(bool streaming, const backend::PredictOptions* predict, const
});
}
data["stop"] = predict->stopprompts();
data["stop"] = json::array();
for (const auto & stop : predict->stopprompts()) {
data["stop"].push_back(stop);
}
// data["n_probs"] = predict->nprobs();
//TODO: images,
@@ -1105,14 +1125,16 @@ static void params_parse(server_context& /*ctx_server*/, const backend::ModelOpt
try {
int n = std::stoi(optval_str);
if (n < 0) n = 0;
// Keep override-name storage alive for the lifetime of the params struct
// (mirrors upstream arg.cpp behavior with a function-local static).
#if LOCALAI_HAS_N_CPU_FFN_HELPER
llm_add_n_cpu_ffn_overrides(n, LLM_FFN_EXPS_REGEX, params.speculative.draft.tensor_buft_overrides);
#else
static std::list<std::string> buft_overrides_draft;
for (int i = 0; i < n; ++i) {
buft_overrides_draft.push_back(llm_ffn_exps_block_regex(i));
params.speculative.draft.tensor_buft_overrides.push_back(
{buft_overrides_draft.back().c_str(), ggml_backend_cpu_buffer_type()});
}
#endif
} catch (...) {}
}
@@ -1130,14 +1152,16 @@ static void params_parse(server_context& /*ctx_server*/, const backend::ModelOpt
try {
int n = std::stoi(optval_str);
if (n < 0) n = 0;
// Keep override-name storage alive for the lifetime of the
// params struct (mirrors upstream arg.cpp's function-local static).
#if LOCALAI_HAS_N_CPU_FFN_HELPER
llm_add_n_cpu_ffn_overrides(n, LLM_FFN_EXPS_REGEX, params.tensor_buft_overrides);
#else
static std::list<std::string> buft_overrides_main;
for (int i = 0; i < n; ++i) {
buft_overrides_main.push_back(llm_ffn_exps_block_regex(i));
params.tensor_buft_overrides.push_back(
{buft_overrides_main.back().c_str(), ggml_backend_cpu_buffer_type()});
}
#endif
} catch (...) {}
}
@@ -1402,6 +1426,12 @@ static void params_parse(server_context& /*ctx_server*/, const backend::ModelOpt
passthrough_draft_gpu_layers);
}
// The library initializer now creates both threadpools before the server
// can apply llama_context's fallback for the -1 batch-thread sentinel.
params.cpuparams_batch.n_threads = llama_grpc::resolve_batch_threads(
params.cpuparams_batch.n_threads,
params.cpuparams.n_threads);
#ifndef LOCALAI_LLAMA_CPP_NO_SCORE_TASK
// Score-task suffix forking: reserve seq ids (and recurrent-state cells)
// beyond the slots so one scoring call decodes all candidate tails in a
@@ -1445,6 +1475,26 @@ static void params_parse(server_context& /*ctx_server*/, const backend::ModelOpt
}
#ifndef LOCALAI_LLAMA_CPP_NO_TTS_TASK
// MTMD_HELPER_GEN_AUDIO_OUTTYPE_PCM hands back raw float32 samples, but the
// WAV header core/backend/tts.go builds around the streamed chunks announces
// 16-bit samples, so the wire has to carry s16 or the client decodes floats as
// integers and hears noise. The scaling matches write_wav16() in
// tools/mtmd/mtmd-helper-gen.cpp, which is what the non-streaming path writes.
static std::string tts_pcm_f32_to_s16(const std::string & samples) {
const size_t n = samples.size() / sizeof(float);
std::string out;
out.resize(n * sizeof(int16_t));
for (size_t i = 0; i < n; i++) {
float v = 0.0f;
std::memcpy(&v, samples.data() + i * sizeof(float), sizeof(float));
const int16_t s = (int16_t) (std::max(-1.0f, std::min(1.0f, v)) * 32767.0f);
std::memcpy(&out[i * sizeof(int16_t)], &s, sizeof(int16_t));
}
return out;
}
#endif
// GRPC Server start
class BackendServiceImpl final : public backend::Backend::Service {
private:
@@ -1758,7 +1808,7 @@ public:
for (int j = 0; j < request->audios_size(); j++) rin.audios.push_back(request->audios(j));
for (int j = 0; j < request->videos_size(); j++) rin.videos.push_back(request->videos(j));
}
messages_json.push_back(llama_grpc::build_reconstructed_message(rin));
messages_json.push_back(json::parse(llama_grpc::build_reconstructed_message(rin).dump()));
}
// Final safety check: Ensure no message has null content (Jinja templates require strings)
@@ -1951,7 +2001,7 @@ public:
if (!body_json.contains("chat_template_kwargs")) {
body_json["chat_template_kwargs"] = json::object();
}
for (auto& el : ctk.items()) {
for (auto el : ctk.items()) {
body_json["chat_template_kwargs"][el.key()] = el.value();
}
}
@@ -2037,30 +2087,27 @@ public:
// If not using chat templates, extract files from image_data/audio_data fields
// (If using chat templates, files were already extracted by oaicompat_chat_params_parse)
if (!request->usetokenizertemplate() || request->messages_size() == 0 || ctx_server.impl->chat_params.tmpls == nullptr) {
const auto &images_data = data.find("image_data");
if (images_data != data.end() && images_data->is_array())
if (data.contains("image_data") && data.at("image_data").is_array())
{
for (const auto &img : *images_data)
for (const auto &img : data.at("image_data"))
{
auto decoded_data = base64_decode(img["data"].get<std::string>());
files.push_back(decoded_data);
}
}
const auto &audio_data = data.find("audio_data");
if (audio_data != data.end() && audio_data->is_array())
if (data.contains("audio_data") && data.at("audio_data").is_array())
{
for (const auto &audio : *audio_data)
for (const auto &audio : data.at("audio_data"))
{
auto decoded_data = base64_decode(audio["data"].get<std::string>());
files.push_back(decoded_data);
}
}
const auto &video_data = data.find("video_data");
if (video_data != data.end() && video_data->is_array())
if (data.contains("video_data") && data.at("video_data").is_array())
{
for (const auto &video : *video_data)
for (const auto &video : data.at("video_data"))
{
auto decoded_data = base64_decode(video["data"].get<std::string>());
files.push_back(decoded_data);
@@ -2074,10 +2121,10 @@ public:
std::vector<server_tokens> inputs;
if (has_mtmd) {
// multimodal
inputs.push_back(process_mtmd_prompt(ctx_server.impl->mctx, prompt_str, files));
inputs.push_back(process_mtmd_prompt(ctx_server.impl->mctx, prompt_str, files LOCALAI_MTMD_INIT_OPT_ARG(ctx_server.impl->init_opt)));
} else {
// Everything else, including multimodal completions.
inputs = tokenize_input_prompts(ctx_server.impl->vocab, ctx_server.impl->mctx, prompt_str, true, true);
inputs = tokenize_input_prompts(ctx_server.impl->vocab, ctx_server.impl->mctx, prompt_str, true, true LOCALAI_MTMD_INIT_OPT_ARG(ctx_server.impl->init_opt));
}
tasks.reserve(inputs.size());
@@ -2089,15 +2136,23 @@ public:
task.tokens = std::move(inputs[i]);
#ifdef LOCALAI_HAS_SERVER_SCHEMA
// The schema evaluator no longer takes the per-slot n_ctx: upstream
// dropped the parameter and server-schema stopped consulting n_ctx at
// all, leaving the context bound to the slot. Forks that predate the
// server-schema split still expect it, so only this branch loses it.
task.params = server_schema::eval_llama_cmpl_schema(
ctx_server.impl->vocab,
params_base,
ctx_server.get_meta().logit_bias_eog,
data);
#else
task.params = server_task::params_from_json_cmpl(
#endif
ctx_server.impl->vocab,
params_base,
ctx_server.get_meta().slot_n_ctx,
ctx_server.get_meta().logit_bias_eog,
data);
#endif
task.id_slot = json_value(data, "id_slot", -1);
// OAI-compat: enable autoparser (PEG-based chat parsing) so that
@@ -2325,7 +2380,7 @@ public:
for (int j = 0; j < request->audios_size(); j++) rin.audios.push_back(request->audios(j));
for (int j = 0; j < request->videos_size(); j++) rin.videos.push_back(request->videos(j));
}
messages_json.push_back(llama_grpc::build_reconstructed_message(rin));
messages_json.push_back(json::parse(llama_grpc::build_reconstructed_message(rin).dump()));
}
// Final safety check: Ensure no message has null content (Jinja templates require strings)
@@ -2518,7 +2573,7 @@ public:
if (!body_json.contains("chat_template_kwargs")) {
body_json["chat_template_kwargs"] = json::object();
}
for (auto& el : ctk.items()) {
for (auto el : ctk.items()) {
body_json["chat_template_kwargs"][el.key()] = el.value();
}
}
@@ -2604,11 +2659,10 @@ public:
// If not using chat templates, extract files from image_data/audio_data fields
// (If using chat templates, files were already extracted by oaicompat_chat_params_parse)
if (!request->usetokenizertemplate() || request->messages_size() == 0 || ctx_server.impl->chat_params.tmpls == nullptr) {
const auto &images_data = data.find("image_data");
if (images_data != data.end() && images_data->is_array())
if (data.contains("image_data") && data.at("image_data").is_array())
{
std::cout << "[PREDICT] Processing " << images_data->size() << " images" << std::endl;
for (const auto &img : *images_data)
std::cout << "[PREDICT] Processing " << data.at("image_data").size() << " images" << std::endl;
for (const auto &img : data.at("image_data"))
{
std::cout << "[PREDICT] Processing image" << std::endl;
auto decoded_data = base64_decode(img["data"].get<std::string>());
@@ -2616,20 +2670,18 @@ public:
}
}
const auto &audio_data = data.find("audio_data");
if (audio_data != data.end() && audio_data->is_array())
if (data.contains("audio_data") && data.at("audio_data").is_array())
{
for (const auto &audio : *audio_data)
for (const auto &audio : data.at("audio_data"))
{
auto decoded_data = base64_decode(audio["data"].get<std::string>());
files.push_back(decoded_data);
}
}
const auto &video_data = data.find("video_data");
if (video_data != data.end() && video_data->is_array())
if (data.contains("video_data") && data.at("video_data").is_array())
{
for (const auto &video : *video_data)
for (const auto &video : data.at("video_data"))
{
auto decoded_data = base64_decode(video["data"].get<std::string>());
files.push_back(decoded_data);
@@ -2644,10 +2696,10 @@ public:
std::vector<server_tokens> inputs;
if (has_mtmd) {
// multimodal
inputs.push_back(process_mtmd_prompt(ctx_server.impl->mctx, prompt_str, files));
inputs.push_back(process_mtmd_prompt(ctx_server.impl->mctx, prompt_str, files LOCALAI_MTMD_INIT_OPT_ARG(ctx_server.impl->init_opt)));
} else {
// Everything else, including multimodal completions.
inputs = tokenize_input_prompts(ctx_server.impl->vocab, ctx_server.impl->mctx, prompt_str, true, true);
inputs = tokenize_input_prompts(ctx_server.impl->vocab, ctx_server.impl->mctx, prompt_str, true, true LOCALAI_MTMD_INIT_OPT_ARG(ctx_server.impl->init_opt));
}
tasks.reserve(inputs.size());
@@ -2659,15 +2711,23 @@ public:
task.tokens = std::move(inputs[i]);
#ifdef LOCALAI_HAS_SERVER_SCHEMA
// The schema evaluator no longer takes the per-slot n_ctx: upstream
// dropped the parameter and server-schema stopped consulting n_ctx at
// all, leaving the context bound to the slot. Forks that predate the
// server-schema split still expect it, so only this branch loses it.
task.params = server_schema::eval_llama_cmpl_schema(
ctx_server.impl->vocab,
params_base,
ctx_server.get_meta().logit_bias_eog,
data);
#else
task.params = server_task::params_from_json_cmpl(
#endif
ctx_server.impl->vocab,
params_base,
ctx_server.get_meta().slot_n_ctx,
ctx_server.get_meta().logit_bias_eog,
data);
#endif
task.id_slot = json_value(data, "id_slot", -1);
// OAI-compat: enable autoparser (PEG-based chat parsing) so that
@@ -2826,7 +2886,7 @@ public:
json prompt = body.at("embeddings");
auto tokenized_prompts = tokenize_input_prompts(ctx_server.impl->vocab, ctx_server.impl->mctx, prompt, true, true);
auto tokenized_prompts = tokenize_input_prompts(ctx_server.impl->vocab, ctx_server.impl->mctx, prompt, true, true LOCALAI_MTMD_INIT_OPT_ARG(ctx_server.impl->init_opt));
for (const auto & tokens : tokenized_prompts) {
// this check is necessary for models that do not add BOS token to the input
if (tokens.empty()) {
@@ -2865,42 +2925,40 @@ public:
return grpc::Status(grpc::StatusCode::INTERNAL, all_results.error->to_json().value("message", "Error in receiving results"));
}
// Collect responses
json responses = json::array();
// Extract the embeddings typed, straight from the task results (no
// JSON round-trip), and report the payload shape alongside the same
// flat float array as before: dim is the embedding width, tokens the
// number of vectors packed into `embeddings` (1 per prompt when the
// server pooled, one per token with pooling:none; summed across
// prompts if the request carried several), prompt_tokens the prompt
// tokens evaluated, for usage accounting. Consumers seeing 0/0 know
// the backend predates shape reporting.
int32_t n_vectors = 0;
int32_t dim = 0;
int32_t prompt_tokens = 0;
for (auto & res : all_results.results) {
GGML_ASSERT(dynamic_cast<server_task_result_embd*>(res.get()) != nullptr);
responses.push_back(res->to_json());
}
std::cout << "[DEBUG] Responses size: " << responses.size() << std::endl;
// Process the responses and extract embeddings
for (const auto & response_elem : responses) {
// Check if the response has an "embedding" field
if (response_elem.contains("embedding")) {
json embedding_data = json_value(response_elem, "embedding", json::array());
if (embedding_data.is_array() && !embedding_data.empty()) {
for (const auto & embedding_vector : embedding_data) {
if (embedding_vector.is_array()) {
for (const auto & embedding_value : embedding_vector) {
embeddingResult->add_embeddings(embedding_value.get<float>());
}
}
}
auto * embd_res = dynamic_cast<server_task_result_embd*>(res.get());
GGML_ASSERT(embd_res != nullptr);
prompt_tokens += embd_res->n_tokens;
for (const auto & vec : embd_res->embedding) {
for (const float value : vec) {
embeddingResult->add_embeddings(value);
}
} else {
// Check if the response itself contains the embedding data directly
if (response_elem.is_array()) {
for (const auto & embedding_value : response_elem) {
embeddingResult->add_embeddings(embedding_value.get<float>());
}
if (!vec.empty()) {
n_vectors++;
dim = (int32_t) vec.size();
}
}
}
embeddingResult->set_tokens(n_vectors);
embeddingResult->set_dim(dim);
embeddingResult->set_prompt_tokens(prompt_tokens);
embeddingResult->set_layout(
llama_pooling_type(ctx_server.get_llama_context()) == LLAMA_POOLING_TYPE_NONE
? backend::EMBEDDING_LAYOUT_PER_TOKEN
: backend::EMBEDDING_LAYOUT_FINAL);
std::cout << "[DEBUG] Embedding vectors: " << n_vectors << " x " << dim << std::endl;
return grpc::Status::OK;
}
@@ -2933,7 +2991,7 @@ public:
tasks.reserve(documents.size());
for (size_t i = 0; i < documents.size(); i++) {
auto tmp = format_prompt_rerank(ctx_server.impl->model_tgt, ctx_server.impl->vocab, ctx_server.impl->mctx, request->query(), documents[i]);
auto tmp = format_prompt_rerank(ctx_server.impl->model_tgt, ctx_server.impl->vocab, ctx_server.impl->mctx, request->query(), documents[i] LOCALAI_MTMD_INIT_OPT_ARG(ctx_server.impl->init_opt));
server_task task = server_task(SERVER_TASK_TYPE_RERANK);
task.id = rd.queue_tasks.get_new_id();
task.index = i;
@@ -2954,7 +3012,7 @@ public:
}
// Collect responses
json responses = json::array();
std::vector<json> responses;
for (auto & res : all_results.results) {
GGML_ASSERT(dynamic_cast<server_task_result_rerank*>(res.get()) != nullptr);
responses.push_back(res->to_json());
@@ -2967,7 +3025,7 @@ public:
// Crop results by request.top_n if specified
int top_n = request->top_n();
if (top_n > 0 && top_n < static_cast<int>(responses.size())) {
responses = json(responses.begin(), responses.begin() + top_n);
responses.resize(top_n);
}
// Set usage information
backend::Usage* usage = rerankResult->mutable_usage();
@@ -2994,6 +3052,229 @@ public:
return grpc::Status::OK;
}
#ifndef LOCALAI_LLAMA_CPP_NO_TTS_TASK
// Builds the shared TTS task from a request. Returns a non-OK status and
// leaves `task` untouched when the request is malformed or the loaded model
// cannot synthesise audio.
grpc::Status prepareTTSTask(const backend::TTSRequest* request, bool stream, server_task & task) {
if (!ctx_server.get_meta().has_cap_tts) {
return grpc::Status(grpc::StatusCode::UNIMPLEMENTED,
"the loaded model does not support audio generation (no gen-audio mmproj)");
}
std::map<std::string, std::string> params(request->params().begin(), request->params().end());
const auto opts = llama_grpc::parse_tts_request_options(
request->text(),
request->voice(),
request->has_language() ? request->language() : std::string(),
params);
if (!opts.ok) {
return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT, opts.error);
}
auto wrapper = mtmd_helper_bitmap_init_from_file(ctx_server.impl->mctx, opts.voice_path.c_str(), false LOCALAI_MTMD_INIT_OPT_ARG(ctx_server.impl->init_opt));
if (!wrapper.bitmap) {
return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT,
"failed to read speaker reference audio: " + opts.voice_path);
}
task.tts_inp.set_prompt(opts.text);
// core/backend/tts.go always sets TTSRequest.language, so has_language()
// is true even when the caller named no language and the string is empty.
// gen_audio::inp::get() already maps a stored blank to nullptr, so this
// guard is behavior-preserving rather than behavior-fixing. It is kept
// so the "unset" intent is visible at the call site instead of resting
// on a detail of the helper.
if (!opts.language.empty()) {
task.tts_inp.set_lang(opts.language);
}
task.tts_inp.set_speaker_ref(mtmd::bitmap_ptr(wrapper.bitmap));
task.tts_inp.data.top_k = opts.top_k;
task.tts_inp.data.top_p = opts.top_p;
task.tts_inp.data.stream = stream;
task.tts_inp.data.out_type = stream
? MTMD_HELPER_GEN_AUDIO_OUTTYPE_PCM // Go prepends its own WAV header, see core/backend/tts.go
: MTMD_HELPER_GEN_AUDIO_OUTTYPE_WAV;
task.params.stream = stream;
// -1 keeps upstream's 512-frame default. The model does not always emit
// its codec EOS, so a short input can otherwise generate the full cap.
task.params.n_predict = opts.max_frames > 0 ? opts.max_frames : -1;
task.params.sampling = params_base.sampling;
// Both values mirror upstream's draft POST /tts handler. Note that the
// pair is INERT at this pin: llama_sampler_init_penalties() clamps
// penalty_last_n with std::max(penalty_last_n, 0), so -1 means "off",
// not "the whole generation", and the penalty sampler is then built
// disabled. No repetition penalty is actually applied.
//
// That is deliberate. Dropping the second line lets the sampling
// default of 64 apply and genuinely engages the 1.05 penalty, which was
// measured here against the model's habit of never emitting its codec
// EOS and running to the frame cap: 0 of 15 short requests ran away
// with the penalty inert, 1 of 15 with it active over the last 64
// tokens. It does not fix the runaway, so the line stays for parity
// with the draft. Use max_frames to bound the output instead.
task.params.sampling.penalty_repeat = 1.05f;
task.params.sampling.penalty_last_n = -1;
if (opts.top_k > 0) {
task.params.sampling.top_k = opts.top_k;
}
if (opts.top_p > 0) {
task.params.sampling.top_p = opts.top_p;
}
return grpc::Status::OK;
}
grpc::Status TTS(ServerContext* context, const backend::TTSRequest* request, backend::Result* result) override {
auto auth = checkAuth(context);
if (!auth.ok()) return auth;
auto identity = checkModelIdentity(request);
if (!identity.ok()) return identity;
if (params_base.model.path.empty()) {
return grpc::Status(grpc::StatusCode::FAILED_PRECONDITION, "Model not loaded");
}
if (request->dst().empty()) {
return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT, "dst must name an output file path");
}
server_task task(SERVER_TASK_TYPE_TTS);
auto prepared = prepareTTSTask(request, /* stream= */ false, task);
if (!prepared.ok()) return prepared;
auto rd = ctx_server.get_response_reader();
task.id = rd.get_new_id();
rd.post_task(std::move(task));
auto should_stop = [context]() { return context->IsCancelled(); };
std::string audio;
while (true) {
auto res = rd.next(should_stop);
if (!res) {
return grpc::Status(grpc::StatusCode::CANCELLED, "TTS request cancelled");
}
if (res->is_error()) {
return grpc::Status(grpc::StatusCode::INTERNAL, res->to_json().dump());
}
auto * tts_res = dynamic_cast<server_task_result_tts *>(res.get());
if (tts_res == nullptr) {
return grpc::Status(grpc::StatusCode::INTERNAL, "unexpected result type for a TTS task");
}
audio.append(tts_res->audio);
if (tts_res->final) {
break;
}
}
std::ofstream out(request->dst(), std::ios::binary | std::ios::trunc);
if (!out) {
return grpc::Status(grpc::StatusCode::INTERNAL, "failed to open output file: " + request->dst());
}
out.write(audio.data(), (std::streamsize) audio.size());
if (!out) {
return grpc::Status(grpc::StatusCode::INTERNAL, "failed to write output file: " + request->dst());
}
// Buffered data is flushed here, so a full disk or a failing device can
// surface for the first time on close. Reporting success then would
// leave a truncated file behind under the name the caller will read.
out.close();
if (!out) {
return grpc::Status(grpc::StatusCode::INTERNAL, "failed to close output file: " + request->dst());
}
result->set_success(true);
result->set_message("TTS audio generated");
return grpc::Status::OK;
}
grpc::Status TTSStream(ServerContext* context, const backend::TTSRequest* request, grpc::ServerWriter<backend::Reply>* writer) override {
auto auth = checkAuth(context);
if (!auth.ok()) return auth;
auto identity = checkModelIdentity(request);
if (!identity.ok()) return identity;
if (params_base.model.path.empty()) {
return grpc::Status(grpc::StatusCode::FAILED_PRECONDITION, "Model not loaded");
}
server_task task(SERVER_TASK_TYPE_TTS);
auto prepared = prepareTTSTask(request, /* stream= */ true, task);
if (!prepared.ok()) return prepared;
auto rd = ctx_server.get_response_reader();
task.id = rd.get_new_id();
rd.post_task(std::move(task));
auto should_stop = [context]() { return context->IsCancelled(); };
// core/backend/tts.go:ModelTTSStream builds the WAV header itself from
// the sample rate in the first reply's Message, then concatenates every
// Reply.Audio verbatim. So the rate goes out once, up front, and the
// chunks stay raw PCM.
//
// Send it before draining rather than off the first audio result: a
// chunk needs a whole 72-frame window, about 5.8 s of audio and far
// longer in wall time on CPU, and the Go side cannot emit the WAV
// header until this reply lands. Waiting would hold the client at zero
// bytes for that entire stretch. The rate is a property of the loaded
// model, available synchronously, so there is nothing to wait for.
{
backend::Reply header;
const json info = { {"sample_rate", mtmd_gen_audio_get_info(ctx_server.impl->mctx).sample_rate} };
header.set_message(info.dump());
if (!writer->Write(header)) {
return grpc::Status(grpc::StatusCode::CANCELLED, "client closed the TTS stream");
}
}
while (true) {
auto res = rd.next(should_stop);
if (!res) {
return grpc::Status(grpc::StatusCode::CANCELLED, "TTS request cancelled");
}
if (res->is_error()) {
return grpc::Status(grpc::StatusCode::INTERNAL, res->to_json().dump());
}
auto * tts_res = dynamic_cast<server_task_result_tts *>(res.get());
if (tts_res == nullptr) {
return grpc::Status(grpc::StatusCode::INTERNAL, "unexpected result type for a TTS task");
}
if (!tts_res->audio.empty()) {
backend::Reply chunk;
chunk.set_audio(tts_pcm_f32_to_s16(tts_res->audio));
if (!writer->Write(chunk)) {
return grpc::Status(grpc::StatusCode::CANCELLED, "client closed the TTS stream");
}
}
if (tts_res->final) {
break;
}
}
return grpc::Status::OK;
}
#else
grpc::Status TTS(ServerContext* context, const backend::TTSRequest* request, backend::Result* result) override {
auto auth = checkAuth(context);
if (!auth.ok()) return auth;
(void) request;
(void) result;
return grpc::Status(grpc::StatusCode::UNIMPLEMENTED,
"TTS is unavailable in this llama.cpp fork backend");
}
grpc::Status TTSStream(ServerContext* context, const backend::TTSRequest* request, grpc::ServerWriter<backend::Reply>* writer) override {
auto auth = checkAuth(context);
if (!auth.ok()) return auth;
(void) request;
(void) writer;
return grpc::Status(grpc::StatusCode::UNIMPLEMENTED,
"TTSStream is unavailable in this llama.cpp fork backend");
}
#endif
// Score returns the model's joint log-probability of each candidate
// continuation given a shared prompt.
//
@@ -3328,9 +3609,15 @@ public:
// Populate the response with metrics
response->set_slot_id(0);
response->set_prompt_json_for_slot("");
#if LOCALAI_HAS_SERVER_METRICS
response->set_tokens_per_second(res_metrics->metrics.prompt_bucket.n_per_second());
response->set_tokens_generated(res_metrics->metrics.predict.count);
response->set_prompt_tokens_processed(res_metrics->metrics.prompt.count);
#else
response->set_tokens_per_second(res_metrics->n_prompt_tokens_processed ? 1.e3 / res_metrics->t_prompt_processing * res_metrics->n_prompt_tokens_processed : 0.);
response->set_tokens_generated(res_metrics->n_tokens_predicted_total);
response->set_prompt_tokens_processed(res_metrics->n_prompt_tokens_processed_total);
#endif
return grpc::Status::OK;
+4 -3
View File
@@ -52,14 +52,15 @@ inline nlohmann::ordered_json normalize_message_content(const std::string& role,
// (#7528). A multimodal user message legitimately carries a typed-part array
// ({type:text}, {type:image_url}, ...), which must be left intact. Shared by the
// streaming and non-streaming paths so this invariant cannot drift between them.
inline void normalize_template_message(nlohmann::ordered_json& msg) {
template <typename Json>
inline void normalize_template_message(Json& msg) {
if (!msg.contains("content")) {
msg["content"] = ""; // templates expect the field to exist
return;
}
nlohmann::ordered_json& content = msg["content"];
auto& content = msg["content"];
const std::string role = (msg.contains("role") && msg["role"].is_string())
? msg["role"].get<std::string>()
? msg["role"].template get<std::string>()
: std::string();
if (content.is_null()) {
content = ""; // #7324: null would crash content[:N] slicing
@@ -1,8 +1,20 @@
From 75220a0d74892e3315f4042274b1efa6195868d8 Mon Sep 17 00:00:00 2001
From: Codex <codex@local>
Date: Mon, 10 Aug 2026 23:05:52 +0000
Subject: [PATCH 1/2] score-patch
---
common/common.cpp | 6 +-
common/common.h | 3 +
tools/server/server-context.cpp | 358 +++++++++++++++++++++++++++++++-
tools/server/server-task.h | 47 +++++
4 files changed, 405 insertions(+), 9 deletions(-)
diff --git a/common/common.cpp b/common/common.cpp
index 8f13217..fc584e1 100644
index 2e3f14c..0cec0dc 100644
--- a/common/common.cpp
+++ b/common/common.cpp
@@ -1591,8 +1591,10 @@ struct llama_context_params common_context_params_to_llama(const common_params &
@@ -1636,8 +1636,10 @@ struct llama_context_params common_context_params_to_llama(const common_params &
auto cparams = llama_context_default_params();
cparams.n_ctx = params.n_ctx;
@@ -13,13 +25,13 @@ index 8f13217..fc584e1 100644
+ cparams.n_seq_max = params.n_parallel + params.n_seq_score_forks;
+ cparams.n_rs_seq = std::max(params.speculative.need_n_rs_seq(), (uint32_t) std::max(0, params.n_rs_seq));
cparams.n_outputs_max = std::max(params.n_outputs_max, 0);
cparams.n_outputs_max_per_seq = std::max(params.n_outputs_max_per_seq, 0);
cparams.n_batch = params.n_batch;
cparams.n_ubatch = params.n_ubatch;
diff --git a/common/common.h b/common/common.h
index bffc176..e313bd6 100644
index 878534d..4001df2 100644
--- a/common/common.h
+++ b/common/common.h
@@ -455,6 +455,9 @@ struct common_params {
@@ -445,6 +445,9 @@ struct common_params {
int32_t n_keep = 0; // number of tokens to keep from initial prompt
int32_t n_chunks = -1; // max number of chunks to process (-1 = unlimited)
int32_t n_parallel = 1; // number of parallel sequences to decode
@@ -28,39 +40,26 @@ index bffc176..e313bd6 100644
+ bool score_enabled = false; // reserve server resources for the Score task type
int32_t n_sequences = 1; // number of sequences to decode
int32_t n_outputs_max = 0; // max outputs in a batch (0 = n_batch)
int32_t grp_attn_n = 1; // group-attention factor
diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt
index 780df32..1d2fe8f 100644
--- a/tools/CMakeLists.txt
+++ b/tools/CMakeLists.txt
@@ -41,3 +41,4 @@ else()
add_subdirectory(fit-params)
add_subdirectory(results)
endif()
+add_subdirectory(grpc-server)
int32_t n_outputs_max_per_seq = 1; // max outputs per sequence
diff --git a/tools/server/server-context.cpp b/tools/server/server-context.cpp
index 715477e..de5bed8 100644
index 3b5f6a1..d0e18e6 100644
--- a/tools/server/server-context.cpp
+++ b/tools/server/server-context.cpp
@@ -49,7 +49,16 @@ static uint32_t server_n_outputs_max(const common_params & params) {
@@ -48,6 +48,13 @@ static common_speculative_output_limits server_output_limits(const common_params
auto result = common_speculative_get_output_limits(
params.n_batch, params.n_parallel, common_speculative_n_max(&params.speculative));
const uint32_t n_outputs_per_seq = 1 + common_speculative_n_max(&params.speculative);
- const uint64_t n_outputs = (uint64_t) params.n_parallel * n_outputs_per_seq;
+ // score tasks (SERVER_TASK_TYPE_SCORE) output logits for every candidate
+ // token, so reserve room for a bounded candidate tail per parallel slot
+ if (!params.score_enabled) {
+ return std::max<uint32_t>(1, std::min<uint64_t>(n_batch,
+ (uint64_t) params.n_parallel * n_outputs_per_seq));
+ // Score tasks output logits for every candidate token, so reserve room
+ // for a bounded candidate tail per parallel slot.
+ if (params.score_enabled) {
+ result.per_seq = std::max<int32_t>(result.per_seq, 1 + SERVER_SCORE_MAX_CAND_TOKENS);
+ result.total = std::min<int32_t>(params.n_batch, params.n_parallel * result.per_seq);
+ }
+
+ const uint32_t n_outputs_score_seq = 1 + SERVER_SCORE_MAX_CAND_TOKENS;
+
+ const uint64_t n_outputs = (uint64_t) params.n_parallel * std::max(n_outputs_per_seq, n_outputs_score_seq);
return std::max<uint32_t>(1, std::min<uint64_t>(n_batch, n_outputs));
}
@@ -202,6 +211,26 @@ struct server_slot {
result.total = std::max<int32_t>(1, result.total);
result.per_seq = std::max<int32_t>(1, result.per_seq);
return result;
@@ -239,6 +246,26 @@ struct server_slot {
std::vector<completion_token_output> generated_token_probs;
@@ -87,7 +86,7 @@ index 715477e..de5bed8 100644
bool has_next_token = true;
bool has_new_line = false;
bool truncated = false;
@@ -311,6 +340,10 @@ struct server_slot {
@@ -341,6 +368,10 @@ struct server_slot {
}
generated_tokens.clear();
generated_token_probs.clear();
@@ -97,8 +96,8 @@ index 715477e..de5bed8 100644
+ score_divergence = -1;
json_schema = json();
// clear speculative decoding stats
@@ -2205,6 +2238,229 @@ private:
task_prev = std::move(task);
@@ -2271,6 +2302,229 @@ private:
queue_results.send(std::move(res));
}
@@ -328,7 +327,7 @@ index 715477e..de5bed8 100644
//
// Functions to process the task
//
@@ -2341,6 +2597,7 @@ private:
@@ -2407,6 +2661,7 @@ private:
case SERVER_TASK_TYPE_INFILL:
case SERVER_TASK_TYPE_EMBEDDING:
case SERVER_TASK_TYPE_RERANK:
@@ -336,7 +335,7 @@ index 715477e..de5bed8 100644
{
// special case: if input is provided via CLI, tokenize it first
// otherwise, no need to tokenize as it's already done inside the HTTP thread
@@ -2832,6 +3089,13 @@ private:
@@ -2903,6 +3158,13 @@ private:
break; // stop any further processing
}
}
@@ -350,7 +349,7 @@ index 715477e..de5bed8 100644
}
void pre_decode() {
@@ -3154,6 +3418,16 @@ private:
@@ -3222,6 +3484,16 @@ private:
n_past = std::min(n_past, slot.alora_invocation_start - 1);
}
@@ -367,7 +366,7 @@ index 715477e..de5bed8 100644
const auto n_cache_reuse = slot.task->params.n_cache_reuse;
const bool can_cache_reuse =
@@ -3395,8 +3669,12 @@ private:
@@ -3455,8 +3727,12 @@ private:
bool do_checkpoint = params_base.n_ctx_checkpoints > 0;
@@ -382,7 +381,7 @@ index 715477e..de5bed8 100644
// make a checkpoint of the parts of the memory that cannot be rolled back.
// checkpoints are created only if:
@@ -3463,10 +3741,17 @@ private:
@@ -3444,9 +3720,16 @@ private:
// embedding requires all tokens in the batch to be output;
// MTP also wants logits at every prompt position so the
// streaming hook can mirror t_h_nextn into ctx_dft.
@@ -395,16 +394,12 @@ index 715477e..de5bed8 100644
+ slot.prompt.n_tokens() + 1 < slot.task->n_tokens();
add_ok &= batch.add(slot.id,
cur_tok,
slot.prompt.tokens.pos_next(),
- slot.need_embd());
+ slot.need_embd() || need_score_logit);
/* pos = */ slot.prompt.tokens.pos_next(),
- /* output = */ slot.need_embd(),
+ /* output = */ slot.need_embd() || need_score_logit,
/* is_prompt = */ true);
slot.prompt.tokens.push_back(cur_tok);
slot.n_prompt_tokens_processed++;
@@ -3481,6 +3766,32 @@ private:
}
}
@@ -3454,2 +3737,28 @@ private:
+ // score tasks: break at the shared-prompt boundary so the checkpoint
+ // below lands exactly there — the other candidates of the same
+ // scoring call re-process only their own tokens. Also break at the
@@ -431,10 +426,9 @@ index 715477e..de5bed8 100644
+ }
+ }
+
// process the last few tokens of the prompt separately in order to allow for a checkpoint to be created.
// create checkpoints that many tokens before the end of the prompt:
// - 4 + n_ubatch
@@ -3513,6 +3824,15 @@ private:
// break at the last user message, or at user messages at least min step past the last checkpoint
if (do_checkpoint && spans.is_user_start(slot.prompt.n_tokens())) {
@@ -3573,6 +3882,15 @@ private:
const bool is_user_start = spans.is_user_start(n_tokens_start);
const bool is_last_user_message = n_tokens_start == last_user_pos;
@@ -450,7 +444,7 @@ index 715477e..de5bed8 100644
// entire prompt has been processed
if (slot.prompt.n_tokens() == slot.task->n_tokens()) {
slot.state = SLOT_STATE_DONE_PROMPT;
@@ -3528,8 +3848,8 @@ private:
@@ -3588,8 +3906,8 @@ private:
slot.init_sampler();
} else {
// skip ordinary mid-prompt checkpoints, unless the batch starts a user
@@ -461,7 +455,7 @@ index 715477e..de5bed8 100644
do_checkpoint = false;
}
}
@@ -3546,10 +3866,10 @@ private:
@@ -3606,10 +3924,10 @@ private:
// do not checkpoint after mtmd chunks
do_checkpoint = do_checkpoint && !has_mtmd;
@@ -474,7 +468,7 @@ index 715477e..de5bed8 100644
n_tokens_start > slot.prompt.checkpoints.back().n_tokens + params_base.checkpoint_min_step);
SLT_DBG(slot, "main/do_checkpoint = %s, pos_min = %d, pos_max = %d\n", do_checkpoint ? "yes" : "no", pos_min, pos_max);
@@ -3703,6 +4023,13 @@ private:
@@ -3772,6 +4090,13 @@ private:
}
}
@@ -488,7 +482,7 @@ index 715477e..de5bed8 100644
if (!is_inside_view(slot.i_batch)) {
// the required token not in this sub-batch, skip
return;
@@ -3724,6 +4051,25 @@ private:
@@ -3793,6 +4118,25 @@ private:
return;
}
@@ -515,7 +509,7 @@ index 715477e..de5bed8 100644
// prompt evaluated for next-token prediction
diff --git a/tools/server/server-task.h b/tools/server/server-task.h
index c3eea2e..fb3c178 100644
index 6275ec7..5bedf19 100644
--- a/tools/server/server-task.h
+++ b/tools/server/server-task.h
@@ -13,10 +13,25 @@
@@ -597,3 +591,5 @@ index c3eea2e..fb3c178 100644
struct server_task_result_error : server_task_result {
error_type err_type = ERROR_TYPE_SERVER;
std::string err_msg;
--
2.39.5
@@ -0,0 +1,845 @@
diff --git a/tools/mtmd/mtmd-helper-gen.cpp b/tools/mtmd/mtmd-helper-gen.cpp
index 1c58d3ae1..196cbd433 100644
--- a/tools/mtmd/mtmd-helper-gen.cpp
+++ b/tools/mtmd/mtmd-helper-gen.cpp
@@ -50,29 +50,38 @@ static llama_token find_special_token(const llama_vocab * vocab, const std::stri
return LLAMA_TOKEN_NULL;
}
+static void put_bytes(std::vector<char> & buf, const void * p, size_t n) {
+ const char * c = (const char *) p;
+ buf.insert(buf.end(), c, c + n);
+}
+
+// data_sz == UINT32_MAX writes the "unknown length" sentinel (streaming), same as ffmpeg does on a pipe
+static void write_wav16_header(std::vector<char> & buf, uint32_t data_sz, int32_t rate) {
+ const uint32_t riff_sz = data_sz == UINT32_MAX ? UINT32_MAX : 36 + data_sz;
+ const uint32_t fmt_sz = 16, byte_rate = (uint32_t) rate * 2;
+ const uint16_t fmt = 1, ch = 1, align = 2, bits = 16;
+ const uint32_t rate32 = (uint32_t) rate;
+ put_bytes(buf, "RIFF", 4); put_bytes(buf, &riff_sz, 4); put_bytes(buf, "WAVE", 4);
+ put_bytes(buf, "fmt ", 4); put_bytes(buf, &fmt_sz, 4);
+ put_bytes(buf, &fmt, 2); put_bytes(buf, &ch, 2); put_bytes(buf, &rate32, 4);
+ put_bytes(buf, &byte_rate, 4); put_bytes(buf, &align, 2); put_bytes(buf, &bits, 2);
+ put_bytes(buf, "data", 4); put_bytes(buf, &data_sz, 4);
+}
+
+static void append_wav16_pcm(std::vector<char> & buf, const float * pcm, size_t n) {
+ for (size_t i = 0; i < n; i++) {
+ int16_t s = (int16_t) (std::max(-1.0f, std::min(1.0f, pcm[i])) * 32767.0f);
+ put_bytes(buf, &s, 2);
+ }
+}
+
static bool write_wav16(std::vector<char> & buf, const std::vector<float> & pcm, int32_t rate) {
// RIFF chunk sizes are 32-bit; refuse to emit a file with a truncated header
if (pcm.size() > ((size_t) UINT32_MAX - 36) / 2) {
return false;
}
- const uint32_t data_sz = (uint32_t) (pcm.size() * 2);
- const uint32_t riff_sz = 36 + data_sz;
- const uint32_t fmt_sz = 16, byte_rate = (uint32_t) rate * 2;
- const uint16_t fmt = 1, ch = 1, align = 2, bits = 16;
- const uint32_t rate32 = (uint32_t) rate;
- auto put = [&](const void * p, size_t n) {
- const char * c = (const char *) p;
- buf.insert(buf.end(), c, c + n);
- };
- put("RIFF", 4); put(&riff_sz, 4); put("WAVE", 4);
- put("fmt ", 4); put(&fmt_sz, 4);
- put(&fmt, 2); put(&ch, 2); put(&rate32, 4);
- put(&byte_rate, 4); put(&align, 2); put(&bits, 2);
- put("data", 4); put(&data_sz, 4);
- for (float v : pcm) {
- int16_t s = (int16_t) (std::max(-1.0f, std::min(1.0f, v)) * 32767.0f);
- put(&s, 2);
- }
+ write_wav16_header(buf, (uint32_t) (pcm.size() * 2), rate);
+ append_wav16_pcm(buf, pcm.data(), pcm.size());
return true;
}
@@ -92,6 +101,8 @@ public:
// set out_stop on end-of-speech, h_state_out must be null if no frame is generated
virtual int32_t step_gen(llama_token sampled, const float * h_state_in, const float ** h_state_out, bool * out_stop) = 0;
virtual int32_t get_output(int32_t * out_sample_rate, const char ** out_data, size_t * out_data_len, int64_t * out_n_samples) = 0;
+ // forces any buffered codes through code2wav now, regardless of window_frames
+ virtual int32_t flush() { return 0; }
protected:
llama_context * lctx;
@@ -121,6 +132,9 @@ public:
prompt_batch.reset();
n_prompt = 0;
prompt_pos = 0;
+ stream = false;
+ pcm_sent = 0;
+ wav_header_sent = false;
}
int32_t set_input(const mtmd_helper_gen_audio_inp * inp) override {
@@ -208,6 +222,7 @@ public:
top_p = inp->top_p > 0 ? inp->top_p : def.top_p;
seed = inp->seed;
out_type = inp->out_type;
+ stream = inp->stream;
// the prompt above holds the whole text stream up to tts_eos, so every generated
// frame adds tts_pad on top of the codes embedding
@@ -302,31 +317,60 @@ public:
}
int32_t get_output(int32_t * out_sample_rate, const char ** out_data, size_t * out_data_len, int64_t * out_n_samples) override {
- if (!flush_gen_wav()) {
- return 1;
+ *out_sample_rate = info.sample_rate;
+
+ if (!stream) {
+ // one-shot call: force out whatever's left, regardless of window_frames
+ if (!flush_gen_wav()) {
+ return 1;
+ }
+ if (out_n_samples) {
+ *out_n_samples = (int64_t) audio_pcm.size();
+ }
+ if (out_type == MTMD_HELPER_GEN_AUDIO_OUTTYPE_PCM) {
+ *out_data = (const char *) audio_pcm.data();
+ *out_data_len = audio_pcm.size() * sizeof(float);
+ return 0;
+ }
+ out_buf.clear();
+ if (!write_wav16(out_buf, audio_pcm, info.sample_rate)) {
+ LOG_ERR("mtmd_helper_gen_audio: output too large for WAV\n");
+ return 1;
+ }
+ *out_data = out_buf.data();
+ *out_data_len = out_buf.size();
+ return 0;
}
- *out_sample_rate = info.sample_rate;
+ // streaming: only return audio produced since the previous call
+ const size_t n_new = audio_pcm.size() - pcm_sent;
if (out_n_samples) {
- *out_n_samples = (int64_t) audio_pcm.size();
+ *out_n_samples = (int64_t) n_new;
}
if (out_type == MTMD_HELPER_GEN_AUDIO_OUTTYPE_PCM) {
- *out_data = (const char *) audio_pcm.data();
- *out_data_len = audio_pcm.size() * sizeof(float);
+ *out_data = (const char *) (audio_pcm.data() + pcm_sent);
+ *out_data_len = n_new * sizeof(float);
+ pcm_sent = audio_pcm.size();
return 0;
}
out_buf.clear();
- if (!write_wav16(out_buf, audio_pcm, info.sample_rate)) {
- LOG_ERR("mtmd_helper_gen_audio: output too large for WAV\n");
- return 1;
+ if (!wav_header_sent) {
+ write_wav16_header(out_buf, UINT32_MAX, info.sample_rate);
+ wav_header_sent = true;
}
+ append_wav16_pcm(out_buf, audio_pcm.data() + pcm_sent, n_new);
+ pcm_sent = audio_pcm.size();
*out_data = out_buf.data();
*out_data_len = out_buf.size();
return 0;
}
+ int32_t flush() override {
+ return flush_gen_wav() ? 0 : 1;
+ }
+
private:
bool ensure_cache() {
if (specials_ok) {
@@ -370,7 +414,7 @@ private:
LOG_ERR("mtmd_helper_gen_audio: mmproj has no speaker/audio encoder\n");
return false;
}
- const std::string marker = mtmd_default_marker();
+ const std::string marker = mtmd_get_marker(mctx);
mtmd_input_text text{ marker.c_str(), marker.size(), false, true };
mtmd_input_chunks * chunks = mtmd_input_chunks_init();
const mtmd_bitmap * bptr = bitmap;
@@ -456,6 +500,9 @@ private:
std::vector<float> h_state_buf;
mtmd_helper_gen_audio_outtype out_type = MTMD_HELPER_GEN_AUDIO_OUTTYPE_WAV;
std::vector<char> out_buf;
+ bool stream = false;
+ size_t pcm_sent = 0; // samples already returned by get_output()
+ bool wav_header_sent = false;
};
// settings that only live in the reference's per-pack yaml, not in the checkpoint
@@ -1024,6 +1071,14 @@ void mtmd_helper_gen_audio_reset(mtmd_helper_gen_audio * ctx) {
}
}
+struct mtmd_helper_gen_audio_inp mtmd_helper_gen_audio_inp_default(void) {
+ mtmd_helper_gen_audio_inp inp{};
+ inp.top_k = 50;
+ inp.top_p = 1.0f;
+ inp.out_type = MTMD_HELPER_GEN_AUDIO_OUTTYPE_WAV;
+ return inp;
+}
+
int32_t mtmd_helper_gen_audio_set_input(mtmd_helper_gen_audio * ctx, const mtmd_helper_gen_audio_inp * inp) {
if (!ctx->pipeline) {
LOG_ERR("mtmd_helper_gen_audio: unsupported or missing gen-audio pipeline\n");
@@ -1060,3 +1115,10 @@ int32_t mtmd_helper_gen_audio_get_output(mtmd_helper_gen_audio * ctx, int32_t *
}
return ctx->pipeline->get_output(out_sample_rate, out_data, out_data_len, out_n_samples);
}
+
+int32_t mtmd_helper_gen_audio_flush(mtmd_helper_gen_audio * ctx) {
+ if (!ctx->pipeline) {
+ return 1;
+ }
+ return ctx->pipeline->flush();
+}
diff --git a/tools/mtmd/mtmd-helper.h b/tools/mtmd/mtmd-helper.h
index 832f7171a..3eaa01aab 100644
--- a/tools/mtmd/mtmd-helper.h
+++ b/tools/mtmd/mtmd-helper.h
@@ -175,6 +175,7 @@ enum mtmd_helper_gen_audio_outtype {
MTMD_HELPER_GEN_AUDIO_OUTTYPE_WAV, // WAV PCM 16-bit LE, mono
};
struct mtmd_helper_gen_audio_inp {
+ bool stream; // if true, output() must be called after each step_gen()
llama_seq_id seq_id;
const char * prompt;
@@ -190,6 +191,8 @@ struct mtmd_helper_gen_audio_inp {
enum mtmd_helper_gen_audio_outtype out_type;
};
+MTMD_API struct mtmd_helper_gen_audio_inp mtmd_helper_gen_audio_inp_default(void);
+
MTMD_API mtmd_helper_gen_audio * mtmd_helper_gen_audio_init(
struct llama_context * lctx,
struct mtmd_context * mctx);
@@ -221,6 +224,8 @@ MTMD_API int32_t mtmd_helper_gen_audio_step_gen(
// out_data valid until next get_output() or reset() call
// out_n_samples (optional, can be NULL) receives the number of generated PCM samples
+// if inp->stream is true: returns only audio produced since the previous call, and
+// *out_data_len == 0 whenever a full window_frames batch hasn't accumulated yet
MTMD_API int32_t mtmd_helper_gen_audio_get_output(
mtmd_helper_gen_audio * ctx,
int32_t * out_sample_rate,
@@ -228,6 +233,10 @@ MTMD_API int32_t mtmd_helper_gen_audio_get_output(
size_t * out_data_len,
int64_t * out_n_samples);
+// forces any buffered codes through code2wav now, regardless of window_frames;
+// call once when generation has ended, before the last get_output() in stream mode
+MTMD_API int32_t mtmd_helper_gen_audio_flush(mtmd_helper_gen_audio * ctx);
+
#ifdef __cplusplus
} // extern "C"
#endif
@@ -254,8 +263,41 @@ struct mtmd_helper_gen_audio_deleter {
};
using gen_audio_ptr = std::unique_ptr<mtmd_helper_gen_audio, mtmd_helper_gen_audio_deleter>;
struct gen_audio {
+
+ // sub-struct, RAII wrapper for mtmd_helper_gen_audio_inp
+ struct inp {
+ mtmd_helper_gen_audio_inp data = mtmd_helper_gen_audio_inp_default();
+ std::string prompt_str;
+ std::string lang_str;
+ mtmd::bitmap_ptr speaker_ref_ptr;
+
+ inp() = default;
+ inp(inp &&) = default;
+ inp & operator=(inp &&) = default;
+ inp(const inp &) = delete;
+ inp & operator=(const inp &) = delete;
+
+ void set_prompt (std::string p) { prompt_str = std::move(p); }
+ void set_lang (std::string l) { lang_str = std::move(l); }
+ void set_speaker_ref(mtmd::bitmap_ptr bmp) { speaker_ref_ptr = std::move(bmp); }
+
+ // pointers are only valid as long as *this is alive
+ const mtmd_helper_gen_audio_inp * get() {
+ data.prompt = prompt_str.c_str();
+ data.prompt_len = prompt_str.size();
+ data.lang = lang_str.empty() ? nullptr : lang_str.c_str();
+ data.speaker_ref = speaker_ref_ptr.get();
+ return &data;
+ }
+ };
+
gen_audio_ptr ctx;
- gen_audio(struct llama_context * lctx, struct mtmd_context * mctx) : ctx(mtmd_helper_gen_audio_init(lctx, mctx)) {}
+ void init(struct llama_context * lctx, struct mtmd_context * mctx) {
+ ctx.reset(mtmd_helper_gen_audio_init(lctx, mctx));
+ }
+ bool valid() const {
+ return ctx.get() != nullptr;
+ }
void reset() {
mtmd_helper_gen_audio_reset(ctx.get());
}
@@ -271,6 +313,9 @@ struct gen_audio {
int32_t get_output(int32_t * out_sample_rate, const char ** out_data, size_t * out_data_len, int64_t * out_n_samples = nullptr) {
return mtmd_helper_gen_audio_get_output(ctx.get(), out_sample_rate, out_data, out_data_len, out_n_samples);
}
+ int32_t flush() {
+ return mtmd_helper_gen_audio_flush(ctx.get());
+ }
};
} // namespace mtmd_helper
diff --git a/tools/server/server-context.cpp b/tools/server/server-context.cpp
index 9069463fe..b7fa1e534 100644
--- a/tools/server/server-context.cpp
+++ b/tools/server/server-context.cpp
@@ -16,6 +16,7 @@
#include "speculative.h"
#include "mtmd.h"
#include "mtmd-helper.h"
+#include "base64.hpp"
#include <algorithm>
#include <cstddef>
@@ -41,8 +42,9 @@ constexpr int HTTP_POLLING_SECONDS = 1;
static common_speculative_output_limits server_output_limits(const common_params & params) {
if (params.embedding ||
- (params.pooling_type != LLAMA_POOLING_TYPE_UNSPECIFIED && params.pooling_type != LLAMA_POOLING_TYPE_NONE)) {
- return { params.n_batch, 1 };
+ (params.pooling_type != LLAMA_POOLING_TYPE_UNSPECIFIED && params.pooling_type != LLAMA_POOLING_TYPE_NONE) ||
+ !params.mmproj.path.empty()) { // gen-audio (TTS) capability isn't known until the mmproj loads, size generously
+ return { params.n_batch, params.n_batch };
}
auto result = common_speculative_get_output_limits(
@@ -212,6 +214,30 @@ struct server_slot {
mtmd_context * mctx = nullptr;
mtmd::batch_ptr mbatch = nullptr;
+ struct tts_ctx {
+ mtmd_helper::gen_audio ctx;
+ const float * h_state;
+ llama_token sampled;
+ int32_t n_decoded;
+ bool is_supported() const {
+ return ctx.valid();
+ }
+ void reset() {
+ // mtmd_helper_gen_audio_reset() dereferences its argument before it
+ // null-checks the pipeline, and the pipeline is only allocated for
+ // models that actually carry a gen-audio mmproj. server_slot::reset()
+ // runs for every slot of every model, so without this guard any
+ // non-TTS model segfaults during slot initialization.
+ if (is_supported()) {
+ ctx.reset();
+ }
+ h_state = nullptr;
+ sampled = LLAMA_TOKEN_NULL;
+ n_decoded = 0;
+ }
+ };
+ tts_ctx tts;
+
// speculative decoding
common_speculative * spec;
@@ -391,6 +417,8 @@ struct server_slot {
// clear multimodal state
mbatch.reset();
+
+ tts.reset();
}
void init_sampler() const {
@@ -829,6 +857,14 @@ public:
mtmd_context * mctx = nullptr;
const llama_vocab * vocab = nullptr;
+ bool has_cap_tts() const {
+ return mctx != nullptr && mtmd_gen_audio_get_info(mctx).type != MTMD_GEN_AUDIO_TYPE_NONE;
+ }
+
+ bool has_cap_chat() const {
+ return mctx == nullptr || mtmd_helper_model_can_chat(ctx_tgt, mctx);
+ }
+
server_queue queue_tasks;
server_response queue_results;
@@ -1288,6 +1324,10 @@ private:
slot.mctx = mctx;
slot.prompt.tokens.has_mtmd = mctx != nullptr;
+ if (has_cap_tts()) {
+ slot.tts.ctx.init(ctx_tgt, mctx);
+ }
+
SLT_TRC(slot, "new slot, n_ctx = %d\n", slot.n_ctx);
slot.callback_on_release = [this](int id_slot) {
@@ -1748,6 +1788,28 @@ private:
SLT_DBG(slot, "launching slot : %s\n", safe_json_to_str(slot.to_json()).c_str());
+ if (task.type == SERVER_TASK_TYPE_TTS) {
+ GGML_ASSERT(has_cap_tts()); // should already checked in route handler
+ if (!slot.tts.is_supported()) {
+ slot.tts.ctx.init(ctx_tgt, slot.mctx);
+ }
+
+ // TTS slots never enter the shared batch: pre_decode() returns early for
+ // them and process_tts_slots() drives them instead, so they skip the
+ // prompt-cache bookkeeping that clears this sequence between requests.
+ // The gen-audio pipeline always decodes from position 0, and its own
+ // reset() only clears host-side buffers, so without this the second and
+ // later tasks on a slot decode over the previous request's tokens and
+ // step_prompt() fails immediately.
+ slot.prompt_clear();
+
+ task.tts_inp.data.seq_id = slot.id;
+ if (slot.tts.ctx.set_input(task.tts_inp.get()) != 0) {
+ send_error(task, "failed to process TTS prompt", ERROR_TYPE_SERVER);
+ return false;
+ }
+ }
+
// initialize samplers
if (task.need_sampling()) {
try {
@@ -1765,6 +1827,9 @@ private:
// TODO: getting pre sampling logits is not yet supported with backend sampling
use_backend_sampling &= !need_pre_sample_logits;
+ // TODO: check verify if this actually works with TTS
+ use_backend_sampling &= task.type != SERVER_TASK_TYPE_TTS;
+
// TODO: tmp until backend sampling is fully implemented
if (use_backend_sampling) {
llama_set_sampler(ctx_tgt, slot.id, common_sampler_get(slot.smpl.get()));
@@ -1783,9 +1848,13 @@ private:
slot.task = std::make_unique<const server_task>(std::move(task));
- slot.state = slot.task->is_child()
- ? SLOT_STATE_WAIT_OTHER // wait for the parent to process prompt
- : SLOT_STATE_STARTED;
+ if (slot.task->type == SERVER_TASK_TYPE_TTS) {
+ slot.state = SLOT_STATE_PROCESSING_PROMPT;
+ } else {
+ slot.state = slot.task->is_child()
+ ? SLOT_STATE_WAIT_OTHER // wait for the parent to process prompt
+ : SLOT_STATE_STARTED;
+ }
// reset server kill-switch counter
n_empty_consecutive = 0;
@@ -2050,6 +2119,18 @@ private:
queue_results.send(std::move(res));
}
+ void send_tts_result(server_slot & slot, int32_t sample_rate, const char * data, size_t data_len, bool final) {
+ auto res = std::make_unique<server_task_result_tts>();
+
+ res->id = slot.task->id;
+ res->index = slot.task->index;
+ res->sample_rate = sample_rate;
+ res->audio.assign(data, data_len);
+ res->final = final;
+
+ queue_results.send(std::move(res));
+ }
+
void send_final_response(server_slot & slot) {
auto res = std::make_unique<server_task_result_cmpl_final>();
@@ -2556,6 +2637,7 @@ private:
case SERVER_TASK_TYPE_EMBEDDING:
case SERVER_TASK_TYPE_RERANK:
case SERVER_TASK_TYPE_SCORE:
+ case SERVER_TASK_TYPE_TTS:
{
// special case: if input is provided via CLI, tokenize it first
// otherwise, no need to tokenize as it's already done inside the HTTP thread
@@ -3007,1 +3089,9 @@ private:
+ // note: TTS slots bypass the shared batch entirely
+ try {
+ process_tts_slots();
+ } catch (const std::exception & e) {
+ SRV_ERR("process_tts_slots() failed: %s\n", e.what());
+ abort_all_slots("process_tts_slots() failed: " + std::string(e.what()));
+ }
+
GGML_ASSERT(batch.slot_batched || batch.size() == 0);
@@ -3074,10 +3164,77 @@ private:
}
}
+ void process_tts_slots() {
+ iterate(slots, [&](server_slot & slot) {
+ if (!slot.is_processing() || slot.task->type != SERVER_TASK_TYPE_TTS) {
+ return;
+ }
+
+ llama_set_embeddings(ctx_tgt, true);
+
+ if (slot.state == SLOT_STATE_PROCESSING_PROMPT) {
+ const int32_t ret = slot.tts.ctx.step_prompt(llama_n_batch(ctx_tgt));
+ if (ret < 0) {
+ send_error(slot, "TTS prompt processing failed", ERROR_TYPE_SERVER);
+ slot.release();
+ } else if (ret == 0) {
+ slot.tts.sampled = common_sampler_sample(slot.smpl.get(), ctx_tgt, -1);
+ common_sampler_accept(slot.smpl.get(), slot.tts.sampled, true);
+ slot.tts.h_state = llama_get_embeddings_ith(ctx_tgt, -1);
+ slot.state = SLOT_STATE_GENERATING;
+ }
+ return;
+ }
+
+ const int32_t n_predict = slot.task->params.n_predict > 0 ? slot.task->params.n_predict : 512;
+ if (slot.tts.n_decoded >= n_predict || llama_vocab_is_eog(vocab, slot.tts.sampled)) {
+ int32_t sample_rate = 0;
+ const char * data = nullptr;
+ size_t data_len = 0;
+ // generation truly ends here: force out any sub-window remainder still buffered
+ if (slot.tts.ctx.flush() != 0 || slot.tts.ctx.get_output(&sample_rate, &data, &data_len) != 0) {
+ send_error(slot, "failed to finalize TTS output", ERROR_TYPE_SERVER);
+ } else {
+ send_tts_result(slot, sample_rate, data, data_len, true);
+ }
+ slot.release();
+ return;
+ }
+
+ const float * h_state_next = nullptr;
+ if (slot.tts.ctx.step_gen(slot.tts.sampled, slot.tts.h_state, &h_state_next) != 0) {
+ send_error(slot, "TTS generation failed", ERROR_TYPE_SERVER);
+ slot.release();
+ return;
+ }
+ slot.tts.h_state = h_state_next;
+ slot.tts.n_decoded++;
+
+ slot.tts.sampled = common_sampler_sample(slot.smpl.get(), ctx_tgt, -1);
+ common_sampler_accept(slot.smpl.get(), slot.tts.sampled, true);
+
+ if (slot.task->params.stream) {
+ int32_t sample_rate = 0;
+ const char * data = nullptr;
+ size_t data_len = 0;
+ if (slot.tts.ctx.get_output(&sample_rate, &data, &data_len) != 0) {
+ send_error(slot, "TTS streaming output failed", ERROR_TYPE_SERVER);
+ slot.release();
+ } else if (data_len > 0) {
+ send_tts_result(slot, sample_rate, data, data_len, false);
+ }
+ }
+ });
+ }
+
void pre_decode() {
// apply context-shift if needed
// TODO: simplify and improve
iterate(slots, [&](server_slot & slot) {
+ if (slot.task && slot.task->type == SERVER_TASK_TYPE_TTS) {
+ // TTS slots drive their own decode loop in process_tts_slots(), never enter the shared batch
+ return;
+ }
if (slot.state == SLOT_STATE_GENERATING && slot.prompt.n_tokens() + 1 >= slot.n_ctx) {
if (!params_base.ctx_shift) {
// this check is redundant (for good)
@@ -3150,7 +3307,7 @@ private:
// determine which slots are generating and drafting
iterate(slots, [&](server_slot & slot) {
- if (slot.state != SLOT_STATE_GENERATING) {
+ if (slot.state != SLOT_STATE_GENERATING || slot.task->type == SERVER_TASK_TYPE_TTS) {
return;
}
@@ -3284,7 +3441,7 @@ private:
return; // batch is full, skip remaining slots
}
- if (!slot.is_processing()) {
+ if (!slot.is_processing() || slot.task->type == SERVER_TASK_TYPE_TTS) {
return;
}
@@ -4433,6 +4590,8 @@ server_context_meta server_context::get_meta() const {
/* has_inp_image */ impl->chat_params.allow_image,
/* has_inp_audio */ impl->chat_params.allow_audio,
/* has_inp_video */ impl->chat_params.allow_video,
+ /* has_cap_chat */ impl->has_cap_chat(),
+ /* has_cap_tts */ impl->has_cap_tts(),
/* json_ui_settings */ impl->json_ui_settings,
/* slot_n_ctx */ impl->get_slot_n_ctx(),
/* pooling_type */ llama_pooling_type(impl->ctx_tgt),
@@ -4512,6 +4671,11 @@ std::unique_ptr<server_res_generator> server_routes::handle_completions_impl(
res->set_req(&req); // will also set spipe if needed
+ if (!ctx_server.has_cap_chat()) {
+ res->error(format_error_response("this server does not support chat/completions", ERROR_TYPE_NOT_SUPPORTED));
+ return res;
+ }
+
int32_t sse_ping_interval = params.sse_ping_interval;
try {
@@ -5399,6 +5563,150 @@ void server_routes::init_routes() {
return res;
};
+ this->post_tts = [this](const server_http_req & req) {
+ auto res = create_response();
+ res->set_req(&req); // will also set spipe if needed
+
+ if (!ctx_server.has_cap_tts()) {
+ res->error(format_error_response("this server does not support audio generation", ERROR_TYPE_NOT_SUPPORTED));
+ return res;
+ }
+
+ const json body = json::parse(req.body);
+
+ std::string prompt = json_value(body, "input", json_value(body, "prompt", std::string()));
+ if (prompt.empty()) {
+ res->error(format_error_response("\"input\" must be a non-empty string", ERROR_TYPE_INVALID_REQUEST));
+ return res;
+ }
+
+ const std::string response_format = json_value(body, "response_format", std::string("wav"));
+ const bool stream = json_value(body, "stream", false);
+
+ server_task task(SERVER_TASK_TYPE_TTS);
+ task.tts_inp.set_prompt(prompt);
+ task.tts_inp.set_lang(json_value(body, "lang", std::string()));
+ task.tts_inp.data.top_k = json_value(body, "top_k", 0);
+ task.tts_inp.data.top_p = json_value(body, "top_p", 0.0f);
+ task.tts_inp.data.stream = stream;
+ task.tts_inp.data.out_type = response_format == "pcm"
+ ? MTMD_HELPER_GEN_AUDIO_OUTTYPE_PCM
+ : MTMD_HELPER_GEN_AUDIO_OUTTYPE_WAV;
+ task.params.stream = stream;
+ task.params.n_predict = json_value(body, "n_predict", -1);
+ task.params.sampling = params.sampling; // baseline defaults, then apply overrides below
+ task.params.sampling.penalty_repeat = json_value(body, "repeat_penalty", 1.05f);
+ task.params.sampling.penalty_last_n = -1;
+ if (task.tts_inp.data.top_k > 0) {
+ task.params.sampling.top_k = task.tts_inp.data.top_k;
+ }
+ if (task.tts_inp.data.top_p > 0) {
+ task.params.sampling.top_p = task.tts_inp.data.top_p;
+ }
+
+ // speaker reference: either an uploaded form file ("speaker_ref") or a base64 JSON field ("speaker_ref_b64")
+ const unsigned char * speaker_ref_data = nullptr;
+ size_t speaker_ref_len = 0;
+ std::string speaker_ref_b64_decoded;
+
+ auto speaker_ref_file = req.files.find("speaker_ref");
+ if (speaker_ref_file != req.files.end()) {
+ speaker_ref_data = speaker_ref_file->second.data.data();
+ speaker_ref_len = speaker_ref_file->second.data.size();
+ } else {
+ std::string speaker_ref_b64 = json_value(body, "speaker_ref_b64", std::string());
+ if (!speaker_ref_b64.empty()) {
+ speaker_ref_b64_decoded = base64::decode(speaker_ref_b64);
+ speaker_ref_data = (const unsigned char *) speaker_ref_b64_decoded.data();
+ speaker_ref_len = speaker_ref_b64_decoded.size();
+ }
+ }
+
+ if (speaker_ref_len > 0) {
+ auto wrapper = mtmd_helper_bitmap_init_from_buf(ctx_server.mctx, speaker_ref_data, speaker_ref_len, false, ctx_server.init_opt);
+ if (!wrapper.bitmap) {
+ res->error(format_error_response("failed to decode \"speaker_ref\"", ERROR_TYPE_INVALID_REQUEST));
+ return res;
+ }
+ task.tts_inp.set_speaker_ref(mtmd::bitmap_ptr(wrapper.bitmap));
+ } else {
+ // SRV_WRN expands __VA_ARGS__ without the GNU comma-elision extension,
+ // so a bare format string leaves a trailing comma and will not compile
+ SRV_WRN("%s", "no speaker reference provided, the model may behave randomly\n");
+ }
+
+ auto & rd = res->rd;
+ task.id = rd.get_new_id();
+ rd.post_task(std::move(task));
+
+ const std::string content_type = response_format == "pcm" ? "audio/L16" : "audio/wav";
+
+ if (!stream) {
+ auto result = rd.next(req.should_stop);
+ if (!result) {
+ GGML_ASSERT(req.should_stop());
+ return res; // connection is closed
+ }
+ if (result->is_error()) {
+ res->error(result->to_json());
+ return res;
+ }
+ auto * tts_res = dynamic_cast<server_task_result_tts *>(result.get());
+ GGML_ASSERT(tts_res != nullptr);
+ res->status = 200;
+ res->content_type = content_type;
+ res->data = std::move(tts_res->audio);
+ return res;
+ } else {
+ auto first_result = rd.next(req.should_stop);
+ if (!first_result) {
+ GGML_ASSERT(req.should_stop());
+ return res; // connection is closed
+ }
+ if (first_result->is_error()) {
+ res->error(first_result->to_json());
+ return res;
+ }
+ auto * first_tts_res = dynamic_cast<server_task_result_tts *>(first_result.get());
+ GGML_ASSERT(first_tts_res != nullptr);
+
+ res->status = 200;
+ res->content_type = content_type;
+ res->data = std::move(first_tts_res->audio);
+ bool is_done = first_tts_res->final;
+
+ res->set_next([res_this = res.get(), is_done](std::string & output) mutable -> bool {
+ if (is_done) {
+ return false;
+ }
+ if (res_this->should_stop()) {
+ return false;
+ }
+ if (!res_this->data.empty()) {
+ output = std::move(res_this->data);
+ res_this->data.clear();
+ return true;
+ }
+
+ server_response_reader & rd = res_this->rd;
+ if (!rd.has_next()) {
+ return false;
+ }
+ auto result = rd.next([&res_this]() { return res_this->should_stop(); });
+ if (!result || result->is_error()) {
+ return false;
+ }
+ auto * tts_res = dynamic_cast<server_task_result_tts *>(result.get());
+ GGML_ASSERT(tts_res != nullptr);
+ output = std::move(tts_res->audio);
+ is_done = tts_res->final;
+ return true;
+ });
+ }
+
+ return res;
+ };
+
this->get_lora_adapters = [this](const server_http_req & req) {
auto res = create_response();
diff --git a/tools/server/server-context.h b/tools/server/server-context.h
index f9ab1132b..610512678 100644
--- a/tools/server/server-context.h
+++ b/tools/server/server-context.h
@@ -22,6 +22,8 @@ struct server_context_meta {
bool has_inp_image;
bool has_inp_audio;
bool has_inp_video;
+ bool has_cap_chat;
+ bool has_cap_tts;
json json_ui_settings;
int slot_n_ctx;
enum llama_pooling_type pooling_type;
@@ -151,6 +153,7 @@ struct server_routes {
server_http_context::handler_t post_embeddings;
server_http_context::handler_t post_embeddings_oai;
server_http_context::handler_t post_rerank;
+ server_http_context::handler_t post_tts;
server_http_context::handler_t get_lora_adapters;
server_http_context::handler_t post_lora_adapters;
diff --git a/tools/server/server-task.cpp b/tools/server/server-task.cpp
index 1ee677553..939630b8b 100644
--- a/tools/server/server-task.cpp
+++ b/tools/server/server-task.cpp
@@ -1497,6 +1497,17 @@ json server_task_result_rerank::to_json() {
};
}
+//
+// server_task_result_tts
+//
+json server_task_result_tts::to_json() {
+ return json {
+ {"sample_rate", sample_rate},
+ {"n_bytes", audio.size()},
+ {"final", final},
+ };
+}
+
//
// server_task_result_error
//
diff --git a/tools/server/server-task.h b/tools/server/server-task.h
index 5bedf1987..e6ca67a65 100644
--- a/tools/server/server-task.h
+++ b/tools/server/server-task.h
@@ -10,6 +10,7 @@
// TODO: prevent including the whole server-common.h as we only use server_tokens
#include "server-common.h"
+#include "mtmd-helper.h"
using json = nlohmann::ordered_json;
@@ -42,6 +43,7 @@ enum server_task_type {
SERVER_TASK_TYPE_SLOT_ERASE,
SERVER_TASK_TYPE_GET_LORA,
SERVER_TASK_TYPE_SET_LORA,
+ SERVER_TASK_TYPE_TTS,
};
// TODO: change this to more generic "response_format" to replace the "format_response_*" in server-common
@@ -202,6 +204,9 @@ struct server_task {
// used by SERVER_TASK_TYPE_SET_LORA
std::map<int, float> set_lora; // mapping adapter ID -> scale
+ // used by SERVER_TASK_TYPE_TTS
+ mtmd_helper::gen_audio::inp tts_inp;
+
server_task() = default;
server_task(server_task_type type) : type(type) {}
@@ -235,6 +240,7 @@ struct server_task {
switch (type) {
case SERVER_TASK_TYPE_COMPLETION:
case SERVER_TASK_TYPE_INFILL:
+ case SERVER_TASK_TYPE_TTS:
return true;
default:
return false;
@@ -494,5 +500,15 @@ struct server_task_result_embd : server_task_result {
json to_json_oaicompat();
};
+struct server_task_result_tts : server_task_result {
+ std::string audio; // raw bytes for this chunk (WAV or PCM, per request's out_type)
+ int32_t sample_rate = 0;
+ bool final = false; // true for the last chunk of a request
+
+ virtual bool is_stop() override { return final; }
+
+ virtual json to_json() override;
+};
+
struct server_task_result_rerank : server_task_result {
float score = -1e6;
+49
View File
@@ -15,6 +15,30 @@ if [ -d "patches" ]; then
done
fi
## Apple RDMA link fixup.
## ggml-rpc hands Apple's librdma to the linker with
## target_link_options(ggml-rpc PRIVATE "LINKER:-weak_library,..."). Link options are not
## a usage requirement of a static library, so in our BUILD_SHARED_LIBS=OFF build the flag
## dies with libggml-rpc.a and every ibv_* symbol transport-apple.cpp reaches for comes out
## undefined when grpc-server and ggml-rpc-server link. Re-declare the same weak link as
## INTERFACE so it travels to whoever links the static library.
##
## Guarded on the marker so a second prepare.sh over the same checkout is a no-op, and on
## GGML_RPC_RDMA_APPLE so forks that branched before the Apple RDMA transport (turboquant,
## bonsai) are left alone.
RPC_CMAKE=llama.cpp/ggml/src/ggml-rpc/CMakeLists.txt
if [ -f "$RPC_CMAKE" ] && grep -q "GGML_RPC_RDMA_APPLE" "$RPC_CMAKE" && ! grep -q "LOCALAI_RDMA_IFACE" "$RPC_CMAKE"; then
echo "==> ggml-rpc carries the Apple RDMA transport, re-declaring its weak librdma link as INTERFACE"
cat >> "$RPC_CMAKE" <<'EOF'
# LOCALAI_RDMA_IFACE: added by backend/cpp/llama-cpp/prepare.sh
if (GGML_RPC_RDMA AND APPLE AND NOT BUILD_SHARED_LIBS)
target_link_options(ggml-rpc INTERFACE "LINKER:-weak_library,${RDMA_LIB}")
endif()
EOF
fi
for file in $(ls llama.cpp/tools/server/); do
cp -rfv llama.cpp/tools/server/$file llama.cpp/tools/grpc-server/
done
@@ -28,6 +52,13 @@ cp -r message_content_test.cpp llama.cpp/tools/grpc-server/
# Generic passthrough parser staging and its standalone regression test.
cp -r passthrough_options.h llama.cpp/tools/grpc-server/
cp -r passthrough_options_test.cpp llama.cpp/tools/grpc-server/
# TTS request validation (included by grpc-server.cpp) and its standalone
# regression test.
cp -r tts_request_options.h llama.cpp/tools/grpc-server/
cp -r tts_request_options_test.cpp llama.cpp/tools/grpc-server/
# Thread-count default normalization and its standalone regression test.
cp -r thread_params.h llama.cpp/tools/grpc-server/
cp -r thread_params_test.cpp llama.cpp/tools/grpc-server/
# Parent-death watcher (included by grpc-server.cpp) and its standalone unit
# test (run via backend/cpp/run-unit-tests.sh; also buildable under ctest).
cp -r parent_watch.h llama.cpp/tools/grpc-server/
@@ -49,10 +80,28 @@ else
echo "==> llama.cpp predates the load-mode enum, using the legacy mmap/mlock/direct-io booleans"
LEGACY_LOAD_MODE=1
fi
if grep -q "server_metrics metrics;" llama.cpp/tools/server/server-task.h; then
HAS_SERVER_METRICS=1
else
HAS_SERVER_METRICS=0
fi
if grep -q "mtmd_helper_init_opt" llama.cpp/tools/mtmd/mtmd-helper.h; then
HAS_MTMD_INIT_OPT=1
else
HAS_MTMD_INIT_OPT=0
fi
if grep -q "llm_add_n_cpu_ffn_overrides" llama.cpp/common/common.h; then
HAS_N_CPU_FFN_HELPER=1
else
HAS_N_CPU_FFN_HELPER=0
fi
cat > llama.cpp/tools/grpc-server/llama_compat.h <<EOF
// Generated by backend/cpp/llama-cpp/prepare.sh. Do not edit.
#pragma once
#define LOCALAI_LEGACY_LOAD_MODE ${LEGACY_LOAD_MODE}
#define LOCALAI_HAS_SERVER_METRICS ${HAS_SERVER_METRICS}
#define LOCALAI_HAS_MTMD_INIT_OPT ${HAS_MTMD_INIT_OPT}
#define LOCALAI_HAS_N_CPU_FFN_HELPER ${HAS_N_CPU_FFN_HELPER}
EOF
set +e
+11
View File
@@ -0,0 +1,11 @@
#pragma once
#include <cstdint>
namespace llama_grpc {
inline int32_t resolve_batch_threads(int32_t batch_threads, int32_t inference_threads) {
return batch_threads < 0 ? inference_threads : batch_threads;
}
} // namespace llama_grpc
@@ -0,0 +1,15 @@
#include "thread_params.h"
#include <cstdio>
int main() {
if (llama_grpc::resolve_batch_threads(-1, 4) != 4) {
std::fprintf(stderr, "default batch threads did not inherit inference threads\n");
return 1;
}
if (llama_grpc::resolve_batch_threads(2, 4) != 2) {
std::fprintf(stderr, "explicit batch threads were overwritten\n");
return 1;
}
return 0;
}
+149
View File
@@ -0,0 +1,149 @@
// SPDX-License-Identifier: MIT
#pragma once
#include <cstdint>
#include <exception>
#include <map>
#include <string>
namespace llama_grpc {
// Validated, parsed form of a backend::TTSRequest, kept free of llama.cpp,
// mtmd and gRPC headers so backend/cpp/run-unit-tests.sh can compile it as a
// standalone translation unit. grpc-server.cpp turns this into a
// mtmd_helper::gen_audio::inp.
struct tts_request_options {
bool ok = false;
std::string error;
std::string text;
std::string voice_path;
std::string language;
// 0 / 0.0f mean "unset": upstream only overrides the sampler defaults when
// the value is strictly positive.
int32_t top_k = 0;
float top_p = 0.0f;
// Upper bound on generated audio frames, exposed because the model does not
// always emit its codec EOS and will otherwise run to the 512-frame default,
// which is roughly 41 s at the 12.5 Hz frame rate. 0 means unset, leaving
// that default in place.
int32_t max_frames = 0;
};
namespace detail {
// Strict whole-string numeric parsing. std::stoi/stof accept trailing garbage
// ("40abc" -> 40), which would silently honour a typo'd request.
inline bool parse_whole_int32(const std::string & value, int32_t & out) {
if (value.empty()) {
return false;
}
try {
size_t consumed = 0;
const long parsed = std::stol(value, &consumed);
if (consumed != value.size()) {
return false;
}
if (parsed < INT32_MIN || parsed > INT32_MAX) {
return false;
}
out = static_cast<int32_t>(parsed);
return true;
} catch (const std::exception &) {
return false;
}
}
inline bool parse_whole_float(const std::string & value, float & out) {
if (value.empty()) {
return false;
}
try {
size_t consumed = 0;
const float parsed = std::stof(value, &consumed);
if (consumed != value.size()) {
return false;
}
out = parsed;
return true;
} catch (const std::exception &) {
return false;
}
}
inline tts_request_options reject(const std::string & message) {
tts_request_options opts;
opts.ok = false;
opts.error = message;
return opts;
}
} // namespace detail
inline tts_request_options parse_tts_request_options(
const std::string & text,
const std::string & voice,
const std::string & language,
const std::map<std::string, std::string> & params) {
if (text.empty()) {
return detail::reject("text must be a non-empty string");
}
// The Qwen3-TTS Base checkpoints have no built-in speaker. Without a
// reference clip the model picks an arbitrary voice, so an unset voice is
// a request error rather than a defaulted one.
if (voice.empty()) {
return detail::reject("voice must name a speaker reference audio file");
}
tts_request_options opts;
opts.text = text;
opts.voice_path = voice;
opts.language = language;
// Both values are range-checked here rather than left to the caller: the
// consumer copies them straight into mtmd_helper::gen_audio::inp, and only
// its separate sampler assignment is guarded by "> 0". An out-of-range or
// non-finite value would slip past that guard and reach llama.cpp.
const auto top_k_it = params.find("top_k");
if (top_k_it != params.end()) {
if (!detail::parse_whole_int32(top_k_it->second, opts.top_k)) {
return detail::reject("top_k must be an integer, got \"" + top_k_it->second + "\"");
}
if (opts.top_k < 0) {
return detail::reject("top_k must be >= 0, got \"" + top_k_it->second + "\"");
}
}
const auto top_p_it = params.find("top_p");
if (top_p_it != params.end()) {
if (!detail::parse_whole_float(top_p_it->second, opts.top_p)) {
return detail::reject("top_p must be a number, got \"" + top_p_it->second + "\"");
}
// Phrased as a negated in-range test, not "p < 0.0f || p > 1.0f",
// because every comparison against NaN is false: the obvious form
// would accept NaN, and NaN then defeats the consumer's "> 0" guard
// too, since that comparison is false as well.
if (!(opts.top_p >= 0.0f && opts.top_p <= 1.0f)) {
return detail::reject("top_p must be between 0.0 and 1.0, got \"" + top_p_it->second + "\"");
}
}
const auto max_frames_it = params.find("max_frames");
if (max_frames_it != params.end()) {
if (!detail::parse_whole_int32(max_frames_it->second, opts.max_frames)) {
return detail::reject("max_frames must be an integer, got \"" + max_frames_it->second + "\"");
}
if (opts.max_frames < 0) {
return detail::reject("max_frames must be >= 0, got \"" + max_frames_it->second + "\"");
}
}
opts.ok = true;
return opts;
}
} // namespace llama_grpc
@@ -0,0 +1,209 @@
// SPDX-License-Identifier: MIT
#include <cstdio>
#include <map>
#include <string>
#include "tts_request_options.h"
static int failures = 0;
static void check(bool ok, const char * name) {
if (!ok) {
++failures;
std::fprintf(stderr, "FAIL: %s\n", name);
}
}
static void test_accepts_a_minimal_valid_request() {
const auto opts = llama_grpc::parse_tts_request_options(
"Hello world", "/models/voices/ref.wav", "en", {});
check(opts.ok, "minimal request is accepted");
check(opts.error.empty(), "minimal request has no error");
check(opts.text == "Hello world", "text passes through");
check(opts.voice_path == "/models/voices/ref.wav", "voice path passes through");
check(opts.language == "en", "language passes through");
check(opts.top_k == 0, "top_k defaults to the unset sentinel");
check(opts.top_p == 0.0f, "top_p defaults to the unset sentinel");
check(opts.max_frames == 0, "max_frames defaults to the unset sentinel");
}
static void test_rejects_empty_text() {
const auto opts = llama_grpc::parse_tts_request_options(
"", "/models/voices/ref.wav", "en", {});
check(!opts.ok, "empty text is rejected");
check(opts.error.find("text") != std::string::npos, "empty-text error names the field");
}
static void test_rejects_missing_speaker_reference() {
// Qwen3-TTS Base has no built-in speaker; without a reference it produces
// an arbitrary voice, so this must be a hard error rather than a surprise.
const auto opts = llama_grpc::parse_tts_request_options(
"Hello world", "", "en", {});
check(!opts.ok, "missing voice is rejected");
check(opts.error.find("voice") != std::string::npos, "missing-voice error names the field");
}
static void test_parses_sampling_params() {
const std::map<std::string, std::string> params{
{"top_k", "40"},
{"top_p", "0.85"},
};
const auto opts = llama_grpc::parse_tts_request_options(
"Hello world", "/models/voices/ref.wav", "", params);
check(opts.ok, "sampling params are accepted");
check(opts.top_k == 40, "top_k is parsed");
check(opts.top_p > 0.849f && opts.top_p < 0.851f, "top_p is parsed");
check(opts.language.empty(), "absent language stays empty");
}
static void test_rejects_malformed_sampling_params() {
const auto bad_top_k = llama_grpc::parse_tts_request_options(
"Hello world", "/models/voices/ref.wav", "", {{"top_k", "forty"}});
check(!bad_top_k.ok, "non-numeric top_k is rejected");
check(bad_top_k.error.find("top_k") != std::string::npos, "top_k error names the field");
const auto bad_top_p = llama_grpc::parse_tts_request_options(
"Hello world", "/models/voices/ref.wav", "", {{"top_p", ""}});
check(!bad_top_p.ok, "empty top_p is rejected");
const auto trailing = llama_grpc::parse_tts_request_options(
"Hello world", "/models/voices/ref.wav", "", {{"top_k", "40abc"}});
check(!trailing.ok, "top_k with trailing garbage is rejected");
const auto trailing_float = llama_grpc::parse_tts_request_options(
"Hello world", "/models/voices/ref.wav", "", {{"top_p", "0.8abc"}});
check(!trailing_float.ok, "top_p with trailing garbage is rejected");
// std::stol returns a long, which is wider than int32_t on 64-bit hosts, so
// an in-range-for-long value still has to be caught before the narrowing.
const auto overflow_top_k = llama_grpc::parse_tts_request_options(
"Hello world", "/models/voices/ref.wav", "", {{"top_k", "99999999999"}});
check(!overflow_top_k.ok, "top_k beyond int32 range is rejected");
check(overflow_top_k.error.find("top_k") != std::string::npos,
"top_k overflow error names the field");
}
static void test_rejects_out_of_range_sampling_params() {
// These reach mtmd_helper::gen_audio::inp unconditionally downstream, where
// the "> 0" sampler guard does not screen them, so they must die here.
const auto negative_top_k = llama_grpc::parse_tts_request_options(
"Hello world", "/models/voices/ref.wav", "", {{"top_k", "-5"}});
check(!negative_top_k.ok, "negative top_k is rejected");
check(negative_top_k.error.find("top_k") != std::string::npos,
"negative top_k error names the field");
const auto negative_top_p = llama_grpc::parse_tts_request_options(
"Hello world", "/models/voices/ref.wav", "", {{"top_p", "-0.1"}});
check(!negative_top_p.ok, "negative top_p is rejected");
check(negative_top_p.error.find("top_p") != std::string::npos,
"negative top_p error names the field");
const auto large_top_p = llama_grpc::parse_tts_request_options(
"Hello world", "/models/voices/ref.wav", "", {{"top_p", "1.5"}});
check(!large_top_p.ok, "top_p above 1.0 is rejected");
// NaN survives a naive "p < 0.0f || p > 1.0f" range test because every
// comparison against NaN is false. This case pins the correct form.
const auto nan_top_p = llama_grpc::parse_tts_request_options(
"Hello world", "/models/voices/ref.wav", "", {{"top_p", "nan"}});
check(!nan_top_p.ok, "NaN top_p is rejected");
const auto inf_top_p = llama_grpc::parse_tts_request_options(
"Hello world", "/models/voices/ref.wav", "", {{"top_p", "inf"}});
check(!inf_top_p.ok, "infinite top_p is rejected");
}
static void test_accepts_sampling_param_boundaries() {
const auto zero_top_p = llama_grpc::parse_tts_request_options(
"Hello world", "/models/voices/ref.wav", "", {{"top_p", "0.0"}});
check(zero_top_p.ok, "top_p of 0.0 is accepted");
check(zero_top_p.top_p == 0.0f, "top_p of 0.0 round-trips");
const auto one_top_p = llama_grpc::parse_tts_request_options(
"Hello world", "/models/voices/ref.wav", "", {{"top_p", "1.0"}});
check(one_top_p.ok, "top_p of 1.0 is accepted");
check(one_top_p.top_p == 1.0f, "top_p of 1.0 round-trips");
const auto zero_top_k = llama_grpc::parse_tts_request_options(
"Hello world", "/models/voices/ref.wav", "", {{"top_k", "0"}});
check(zero_top_k.ok, "top_k of 0 is accepted");
}
static void test_parses_max_frames() {
// The consumer maps a positive value onto n_predict and leaves upstream's
// 512-frame default in place when it is unset, so the sentinel matters as
// much as the parsed value.
const auto opts = llama_grpc::parse_tts_request_options(
"Hello world", "/models/voices/ref.wav", "", {{"max_frames", "120"}});
check(opts.ok, "max_frames is accepted");
check(opts.max_frames == 120, "max_frames is parsed");
const auto absent = llama_grpc::parse_tts_request_options(
"Hello world", "/models/voices/ref.wav", "", {{"top_k", "40"}});
check(absent.ok, "a request without max_frames is accepted");
check(absent.max_frames == 0, "absent max_frames leaves the unset sentinel");
const auto zero = llama_grpc::parse_tts_request_options(
"Hello world", "/models/voices/ref.wav", "", {{"max_frames", "0"}});
check(zero.ok, "max_frames of 0 is accepted");
check(zero.max_frames == 0, "max_frames of 0 means unset");
}
static void test_rejects_malformed_max_frames() {
const auto negative = llama_grpc::parse_tts_request_options(
"Hello world", "/models/voices/ref.wav", "", {{"max_frames", "-1"}});
check(!negative.ok, "negative max_frames is rejected");
check(negative.error.find("max_frames") != std::string::npos,
"negative max_frames error names the field");
const auto non_numeric = llama_grpc::parse_tts_request_options(
"Hello world", "/models/voices/ref.wav", "", {{"max_frames", "many"}});
check(!non_numeric.ok, "non-numeric max_frames is rejected");
check(non_numeric.error.find("max_frames") != std::string::npos,
"non-numeric max_frames error names the field");
const auto trailing = llama_grpc::parse_tts_request_options(
"Hello world", "/models/voices/ref.wav", "", {{"max_frames", "120abc"}});
check(!trailing.ok, "max_frames with trailing garbage is rejected");
const auto empty = llama_grpc::parse_tts_request_options(
"Hello world", "/models/voices/ref.wav", "", {{"max_frames", ""}});
check(!empty.ok, "empty max_frames is rejected");
const auto overflow = llama_grpc::parse_tts_request_options(
"Hello world", "/models/voices/ref.wav", "", {{"max_frames", "99999999999"}});
check(!overflow.ok, "max_frames beyond int32 range is rejected");
}
static void test_ignores_unknown_params() {
// Unknown keys are backend-specific knobs meant for other TTS engines. A
// request routed here must not fail just because it carries them.
const auto opts = llama_grpc::parse_tts_request_options(
"Hello world", "/models/voices/ref.wav", "", {{"exaggeration", "0.7"}});
check(opts.ok, "unknown params are ignored, not rejected");
}
int main() {
test_accepts_a_minimal_valid_request();
test_rejects_empty_text();
test_rejects_missing_speaker_reference();
test_parses_sampling_params();
test_rejects_malformed_sampling_params();
test_rejects_out_of_range_sampling_params();
test_accepts_sampling_param_boundaries();
test_parses_max_frames();
test_rejects_malformed_max_frames();
test_ignores_unknown_params();
if (failures == 0) {
std::printf("tts_request_options_test: all checks passed\n");
}
return failures;
}
+2
View File
@@ -48,6 +48,7 @@ define turboquant-build
# stays compiling against vanilla upstream.
bash $(CURRENT_MAKEFILE_DIR)/patch-grpc-server.sh $(CURRENT_MAKEFILE_DIR)/../turboquant-$(1)-build/grpc-server.cpp
bash $(LLAMA_CPP_DIR)/disable-score-task.sh $(CURRENT_MAKEFILE_DIR)/../turboquant-$(1)-build/grpc-server.cpp
bash $(LLAMA_CPP_DIR)/disable-tts-task.sh $(CURRENT_MAKEFILE_DIR)/../turboquant-$(1)-build/grpc-server.cpp
$(info $(GREEN)I turboquant build info:$(1)$(RESET))
LLAMA_REPO=$(LLAMA_REPO) LLAMA_VERSION=$(TURBOQUANT_VERSION) \
$(MAKE) -C $(CURRENT_MAKEFILE_DIR)/../turboquant-$(1)-build llama.cpp
@@ -86,6 +87,7 @@ turboquant-cpu-all:
$(MAKE) -C $(CURRENT_MAKEFILE_DIR)/../turboquant-cpu-all-build purge
bash $(CURRENT_MAKEFILE_DIR)/patch-grpc-server.sh $(CURRENT_MAKEFILE_DIR)/../turboquant-cpu-all-build/grpc-server.cpp
bash $(LLAMA_CPP_DIR)/disable-score-task.sh $(CURRENT_MAKEFILE_DIR)/../turboquant-cpu-all-build/grpc-server.cpp
bash $(LLAMA_CPP_DIR)/disable-tts-task.sh $(CURRENT_MAKEFILE_DIR)/../turboquant-cpu-all-build/grpc-server.cpp
$(info $(GREEN)I turboquant build info:cpu-all-variants$(RESET))
LLAMA_REPO=$(LLAMA_REPO) LLAMA_VERSION=$(TURBOQUANT_VERSION) \
$(MAKE) -C $(CURRENT_MAKEFILE_DIR)/../turboquant-cpu-all-build llama.cpp
@@ -8,6 +8,8 @@
# so the grpc-server option parser skips the two references to
# common_params::checkpoint_min_step (the default and the option handler).
# That field does not exist in the fork yet; drop this once it does.
# 3. Use nlohmann's parse_error type in JSON catch clauses because the fork
# predates upstream's common_json_error wrapper.
#
# The fork used to lag upstream on the whole common_params_speculative refactor
# (ggml-org/llama.cpp#22397/#22838/#22964), the model_tgt rename (#22838) and
@@ -100,4 +102,16 @@ else
echo "==> LOCALAI_TURBOQUANT_NO_CHECKPOINT_MIN_STEP define OK"
fi
# 3. The shared source follows current upstream and catches common_json_error.
# TurboQuant still exposes nlohmann::json directly, so its equivalent parse
# failures use json::parse_error instead.
if grep -q 'common_json_error' "$SRC"; then
echo "==> patching $SRC to use the TurboQuant JSON exception type"
awk '{ gsub(/common_json_error/, "json::parse_error"); print }' "$SRC" > "$SRC.tmp"
mv "$SRC.tmp" "$SRC"
echo "==> TurboQuant JSON exception patch OK"
else
echo "==> $SRC already uses a TurboQuant-compatible JSON exception type, skipping"
fi
echo "==> all patches applied"
+1 -1
View File
@@ -8,7 +8,7 @@ JOBS?=$(shell nproc --ignore=1)
# CrispASR version (release tag)
CRISPASR_REPO?=https://github.com/CrispStrobe/CrispASR
CRISPASR_VERSION?=21901d3f7c23554f072964828363e49ddbc2dc68
CRISPASR_VERSION?=301acd87b036764973b8bfba71e0a21818036d33
SO_TARGET?=libgocrispasr.so
CMAKE_ARGS+=-DBUILD_SHARED_LIBS=OFF
+1 -1
View File
@@ -615,10 +615,10 @@ func (w *CrispASR) TTSStream(req *pb.TTSRequest, results chan []byte) error {
return fmt.Errorf("crispasr: tempfile: %w", err)
}
dst := tmp.Name()
defer func() { _ = os.Remove(dst) }()
if err := tmp.Close(); err != nil {
return fmt.Errorf("crispasr: close tempfile: %w", err)
}
defer func() { _ = os.Remove(dst) }()
if err := writeWAV(dst, pcm, w.sampleRate); err != nil {
return err
+1 -1
View File
@@ -14,7 +14,7 @@ JOBS?=$(shell nproc --ignore=1)
# It is kept alive by the upstream tag da2-support (survives a squash-merge);
# repoint to the master merge commit once mudler/depth-anything.cpp PR #1 lands.
DEPTHANYTHING_REPO?=https://github.com/mudler/depth-anything.cpp.git
DEPTHANYTHING_VERSION?=2028b47ac75a8659c6a9aa617baf09be193eb55f
DEPTHANYTHING_VERSION?=14f7461d1f704761a038ac9f50dbde8fdb7275e2
ifeq ($(NATIVE),false)
CMAKE_ARGS+=-DGGML_NATIVE=OFF
+7 -2
View File
@@ -38,8 +38,9 @@ type Store struct {
// keysAreNormalized stays true until any non-unit-magnitude key
// is added; once false, the magnitude-aware fallback path is
// used by Find. Re-evaluated only at Set time, never again on
// its own — a deletion of the offending key does NOT flip it
// back to true (the bookkeeping cost would dominate the gain).
// its own — a partial deletion of the offending key does NOT flip
// it back to true (the bookkeeping cost would dominate the gain).
// An empty store returns to its initial state.
keysAreNormalized bool
// keyLen is the dimension of every stored key. -1 means "no
@@ -142,6 +143,10 @@ func (s *Store) StoresDelete(opts *pb.StoresDeleteOptions) error {
mergedV = append(mergedV, tailV...)
s.keys = mergedK
s.values = mergedV
if len(s.keys) == 0 {
s.keyLen = -1
s.keysAreNormalized = true
}
assert(slices.IsSortedFunc(s.keys, slices.Compare[[]float32]), "Delete: s.keys not sorted post-merge")
assert(len(s.keys) == len(s.values), "Delete: keys/values length skew")
return nil
+40
View File
@@ -105,6 +105,46 @@ var _ = Describe("StoresDelete", func() {
})).To(Succeed(), "delete of missing key should succeed")
Expect(s.keys).To(HaveLen(1))
})
It("reopens the dimension after deleting every key", func() {
s := NewStore()
oldKey := []float32{2, 0, 0}
mustSet(s, [][]float32{oldKey}, [][]byte{[]byte("3d")})
Expect(s.keysAreNormalized).To(BeFalse())
Expect(s.StoresDelete(&pb.StoresDeleteOptions{
Keys: wrapKeys([][]float32{oldKey}),
})).To(Succeed())
Expect(s.keys).To(BeEmpty())
Expect(s.keyLen).To(Equal(-1))
Expect(s.keysAreNormalized).To(BeTrue())
newKey := normalizeVec([]float32{1, 1})
mustSet(s, [][]float32{newKey}, [][]byte{[]byte("2d")})
res, err := s.StoresFind(&pb.StoresFindOptions{
Key: &pb.StoresKey{Floats: newKey},
TopK: 1,
})
Expect(err).NotTo(HaveOccurred())
Expect(res.Values).To(HaveLen(1))
Expect(string(res.Values[0].Bytes)).To(Equal("2d"))
})
It("retains the dimension after a partial delete", func() {
s := NewStore()
mustSet(s,
[][]float32{{1, 0, 0}, {0, 1, 0}},
[][]byte{[]byte("x"), []byte("y")},
)
Expect(s.StoresDelete(&pb.StoresDeleteOptions{
Keys: wrapKeys([][]float32{{1, 0, 0}}),
})).To(Succeed())
Expect(s.keyLen).To(Equal(3))
Expect(s.StoresSet(&pb.StoresSetOptions{
Keys: wrapKeys([][]float32{{1, 0}}),
Values: wrapValues([][]byte{[]byte("2d")}),
})).NotTo(Succeed())
})
})
var _ = Describe("StoresFind", func() {
@@ -11,6 +11,7 @@ import (
"fmt"
"os"
"path/filepath"
"runtime"
"unsafe"
"github.com/mudler/LocalAI/pkg/grpc/base"
@@ -109,30 +110,25 @@ func (r *LocateAnythingCpp) Detect(opts *pb.DetectOptions) (pb.DetectResponse, e
return pb.DetectResponse{}, fmt.Errorf("locate-anything-cpp: a text prompt is required (open-vocabulary detection)")
}
// Decode base64 image and write to temp file.
imgData, err := base64.StdEncoding.DecodeString(opts.Src)
if err != nil {
return pb.DetectResponse{}, fmt.Errorf("locate-anything-cpp: failed to decode base64 image: %w", err)
}
tmpFile, err := os.CreateTemp("", "locate-anything-*.img")
if err != nil {
return pb.DetectResponse{}, fmt.Errorf("locate-anything-cpp: failed to create temp file: %w", err)
}
defer func() { _ = os.Remove(tmpFile.Name()) }()
if _, err := tmpFile.Write(imgData); err != nil {
_ = tmpFile.Close()
return pb.DetectResponse{}, fmt.Errorf("locate-anything-cpp: failed to write temp file: %w", err)
}
if err := tmpFile.Close(); err != nil {
return pb.DetectResponse{}, fmt.Errorf("locate-anything-cpp: failed to close temp file: %w", err)
if len(imgData) == 0 {
return pb.DetectResponse{}, fmt.Errorf("locate-anything-cpp: decoded image is empty")
}
// mode 0 = hybrid (Parallel Box Decoding). The JSON return value is unused:
// structured detections are read via the accessor functions. Still must
// free the returned string.
jsonPtr := CapiLocatePath(r.handle, tmpFile.Name(), prompt, 0)
jsonPtr := CapiLocateBuffer(
r.handle,
uintptr(unsafe.Pointer(unsafe.SliceData(imgData))),
uintptr(len(imgData)),
prompt,
0,
)
runtime.KeepAlive(imgData)
if jsonPtr != 0 {
CapiFreeString(jsonPtr)
}
@@ -0,0 +1,54 @@
package main
import (
"encoding/base64"
"path/filepath"
pb "github.com/mudler/LocalAI/pkg/grpc/proto"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("LocateAnythingCpp detection input", func() {
It("detects from memory when the temporary directory is unavailable", func() {
originalLocateBuffer := CapiLocateBuffer
originalLocatePath := CapiLocatePath
originalGetNDetections := CapiGetNDetections
defer func() {
CapiLocateBuffer = originalLocateBuffer
CapiLocatePath = originalLocatePath
CapiGetNDetections = originalGetNDetections
}()
image := []byte("encoded-image")
var receivedData uintptr
var receivedLength uintptr
CapiLocateBuffer = func(_ uintptr, data uintptr, length uintptr, _ string, _ int32) uintptr {
receivedData = data
receivedLength = length
return 0
}
CapiLocatePath = func(_ uintptr, _ string, _ string, _ int32) uintptr {
Fail("path-based detection must not be called")
return 0
}
CapiGetNDetections = func(uintptr) int32 { return 0 }
GinkgoT().Setenv("TMPDIR", filepath.Join(GinkgoT().TempDir(), "missing"))
result, err := (&LocateAnythingCpp{handle: 1}).Detect(&pb.DetectOptions{
Src: base64.StdEncoding.EncodeToString(image),
Prompt: "the object",
})
Expect(err).NotTo(HaveOccurred())
Expect(result.Detections).To(BeEmpty())
Expect(receivedData).NotTo(BeZero())
Expect(receivedLength).To(Equal(uintptr(len(image))))
})
It("rejects an empty decoded image", func() {
_, err := (&LocateAnythingCpp{handle: 1}).Detect(&pb.DetectOptions{Prompt: "the object"})
Expect(err).To(MatchError("locate-anything-cpp: decoded image is empty"))
})
})
+32 -3
View File
@@ -12,7 +12,7 @@
# runs 'make -C backend/go/$(BACKEND) build' and then copies package/), so it
# has to produce the binary and the package, not just the shared libraries.
NEMO_SPEECH_VERSION?=2e12e2def8a98ed06666f7ee3ca94e7193e04be4
NEMO_SPEECH_VERSION?=a5b6953c4a579a2bbd1c0913ad8a85c2a4d99953
NEMO_SPEECH_REPO?=https://github.com/NVIDIA/NeMo-Speech.cpp
GOCMD?=go
@@ -88,6 +88,18 @@ ITN_LIB_DIR=$(ITN_PREFIX)/lib
ITN_MARKER=$(ITN_LIB_DIR)/libsparrowhawk.so
ITN_FST_HEADER=$(ITN_PREFIX)/include/fst/fst.h
# SentencePiece became a core ASR dependency in 5be7bfb: RNNT context biasing
# uses it even when Flashlight and text normalization are disabled. Build the
# pinned static archive provided by upstream so every platform gets the same
# dependency instead of relying on an undeclared system package.
SENTENCEPIECE_PREFIX=sources/NeMo-Speech.cpp/.deps/sentencepiece
SENTENCEPIECE_MARKER=$(SENTENCEPIECE_PREFIX)/lib/libsentencepiece.a
# Linux's ASR CMake block looks in NEMO_SPEECH_DEPENDENCY_PREFIX directly, but
# the Apple branch uses generic find_library()/find_path(). Put the same private
# prefix on CMake's search path so Darwin consumes the archive built above too.
CMAKE_ARGS+=-DCMAKE_PREFIX_PATH=$(abspath $(SENTENCEPIECE_PREFIX))
ITN_CC?=gcc-12
ITN_CXX?=g++-12
@@ -152,7 +164,7 @@ else
endif
CMAKE_ARGS+=-DNEMO_SPEECH_GGML_PATCHED=$(GGML_PATCHED)
.PHONY: nemo-speech-cpp-grpc package build clean purge test all stage-libs patch-ggml engine itn patch-itn-headers
.PHONY: nemo-speech-cpp-grpc package build clean purge test all stage-libs patch-ggml engine itn sentencepiece patch-itn-headers
all: nemo-speech-cpp-grpc package
@@ -266,11 +278,28 @@ patch-itn-headers:
itn: $(ITN_MARKER)
$(SENTENCEPIECE_MARKER): | sources/NeMo-Speech.cpp
# Upstream's license copies use GNU install's -D flag, which BSD install
# does not support. Homebrew CMake 4 also rejects SentencePiece's old policy
# floor. Patch both incompatibilities before running the helper on Darwin.
@if [ "$(shell uname -s)" = Darwin ]; then \
cd sources/NeMo-Speech.cpp && \
mkdir -p .deps/sentencepiece/share/licenses/nemo-speech/third_party/sentencepiece && \
perl -pi \
-e 's/install -Dm0644/install -m 0644/g;' \
-e 's/-DCMAKE_BUILD_TYPE=Release /-DCMAKE_BUILD_TYPE=Release -DCMAKE_POLICY_VERSION_MINIMUM=3.5 /;' \
scripts/build_sentencepiece_static.sh; \
fi
cd sources/NeMo-Speech.cpp && JOBS=$(JOBS) scripts/build_sentencepiece_static.sh
sentencepiece: $(SENTENCEPIECE_MARKER)
# Only a WITH_NORM=ON build needs the ITN stack, and it must exist before cmake
# configures, since the WITH_NORM cmake block find_library()s into the prefix
# with REQUIRED.
NEMO_RUNTIME_PREREQS=$(SENTENCEPIECE_MARKER)
ifeq ($(WITH_NORM),ON)
NEMO_RUNTIME_PREREQS=$(ITN_MARKER)
NEMO_RUNTIME_PREREQS+=$(ITN_MARKER)
endif
# Upstream sets CMAKE_LIBRARY_OUTPUT_DIRECTORY to ${CMAKE_BINARY_DIR}/bin, so the
+1 -1
View File
@@ -8,7 +8,7 @@ JOBS?=$(shell nproc --ignore=1)
# omnivoice.cpp version
OMNIVOICE_REPO?=https://github.com/ServeurpersoCom/omnivoice.cpp
OMNIVOICE_VERSION?=4f33af825d66e6ef1cb185e87b4589cacf747291
OMNIVOICE_VERSION?=040c8b344d8c670ce1475194751d119b5ef82c78
SO_TARGET?=libgomnivoicecpp.so
CMAKE_ARGS+=-DBUILD_SHARED_LIBS=OFF
+4 -2
View File
@@ -1,6 +1,6 @@
# parakeet-cpp backend Makefile.
#
# Upstream pin lives below as PARAKEET_VERSION?=1bfbebfaaf493866f49597cd3b7901959d395c60
# Upstream pin lives below as PARAKEET_VERSION?=e75de9b6b9b688fd293aa22f7e27aa724ea286f8
# (.github/bump_deps.sh) can find and update it - matches the
# whisper.cpp / ds4 / vibevoice-cpp convention.
#
@@ -15,7 +15,7 @@
# That's what the L0 smoke test uses. The default target below does the
# proper clone-at-pin + cmake build so CI doesn't need a side-checkout.
PARAKEET_VERSION?=1bfbebfaaf493866f49597cd3b7901959d395c60
PARAKEET_VERSION?=e75de9b6b9b688fd293aa22f7e27aa724ea286f8
PARAKEET_REPO?=https://github.com/mudler/parakeet.cpp
GOCMD?=go
@@ -49,6 +49,8 @@ else ifeq ($(BUILD_TYPE),hipblas)
CMAKE_ARGS+=-DPARAKEET_GGML_HIP=ON
else ifeq ($(BUILD_TYPE),vulkan)
CMAKE_ARGS+=-DPARAKEET_GGML_VULKAN=ON
else ifeq ($(BUILD_TYPE),metal)
CMAKE_ARGS+=-DPARAKEET_GGML_METAL=ON
endif
.PHONY: parakeet-cpp-grpc package build clean purge test all
+13 -16
View File
@@ -10,6 +10,7 @@ import (
"fmt"
"os"
"path/filepath"
"runtime"
"strconv"
"unsafe"
@@ -102,24 +103,12 @@ func (r *RFDetrCpp) Detect(opts *pb.DetectOptions) (pb.DetectResponse, error) {
return pb.DetectResponse{}, fmt.Errorf("rfdetr-cpp: model not loaded")
}
// Decode base64 image and write to temp file.
imgData, err := base64.StdEncoding.DecodeString(opts.Src)
if err != nil {
return pb.DetectResponse{}, fmt.Errorf("rfdetr-cpp: failed to decode base64 image: %w", err)
}
tmpFile, err := os.CreateTemp("", "rfdetr-*.img")
if err != nil {
return pb.DetectResponse{}, fmt.Errorf("rfdetr-cpp: failed to create temp file: %w", err)
}
defer func() { _ = os.Remove(tmpFile.Name()) }()
if _, err := tmpFile.Write(imgData); err != nil {
_ = tmpFile.Close()
return pb.DetectResponse{}, fmt.Errorf("rfdetr-cpp: failed to write temp file: %w", err)
}
if err := tmpFile.Close(); err != nil {
return pb.DetectResponse{}, fmt.Errorf("rfdetr-cpp: failed to close temp file: %w", err)
if len(imgData) == 0 {
return pb.DetectResponse{}, fmt.Errorf("rfdetr-cpp: decoded image is empty")
}
threshold := opts.Threshold
@@ -127,10 +116,18 @@ func (r *RFDetrCpp) Detect(opts *pb.DetectOptions) (pb.DetectResponse, error) {
threshold = 0.5
}
// JSON output from detect_path is unused: we read structured detections via
// JSON output from the detection ABI is unused: we read structured detections via
// the accessor functions. Still must free the returned string.
var jsonPtr uintptr
rc := CapiDetectPath(r.handle, tmpFile.Name(), threshold, uint32(defaultTopK), &jsonPtr)
rc := CapiDetectBuffer(
r.handle,
uintptr(unsafe.Pointer(unsafe.SliceData(imgData))),
uintptr(len(imgData)),
threshold,
uint32(defaultTopK),
&jsonPtr,
)
runtime.KeepAlive(imgData)
if jsonPtr != 0 {
CapiFreeString(jsonPtr)
}
@@ -0,0 +1,56 @@
package main
import (
"encoding/base64"
"path/filepath"
pb "github.com/mudler/LocalAI/pkg/grpc/proto"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("RFDetrCpp detection input", func() {
It("detects from memory when the temporary directory is unavailable", func() {
originalDetectBuffer := CapiDetectBuffer
originalDetectPath := CapiDetectPath
originalFreeString := CapiFreeString
originalGetNDetections := CapiGetNDetections
defer func() {
CapiDetectBuffer = originalDetectBuffer
CapiDetectPath = originalDetectPath
CapiFreeString = originalFreeString
CapiGetNDetections = originalGetNDetections
}()
image := []byte("encoded-image")
var receivedData uintptr
var receivedLength uintptr
CapiDetectBuffer = func(_ uintptr, data uintptr, length uintptr, _ float32, _ uint32, _ *uintptr) int32 {
receivedData = data
receivedLength = length
return 0
}
CapiDetectPath = func(_ uintptr, _ string, _ float32, _ uint32, _ *uintptr) int32 {
Fail("path-based detection must not be called")
return -1
}
CapiFreeString = func(uintptr) {}
CapiGetNDetections = func(uintptr) int32 { return 0 }
GinkgoT().Setenv("TMPDIR", filepath.Join(GinkgoT().TempDir(), "missing"))
result, err := (&RFDetrCpp{handle: 1}).Detect(&pb.DetectOptions{
Src: base64.StdEncoding.EncodeToString(image),
})
Expect(err).NotTo(HaveOccurred())
Expect(result.Detections).To(BeEmpty())
Expect(receivedData).NotTo(BeZero())
Expect(receivedLength).To(Equal(uintptr(len(image))))
})
It("rejects an empty decoded image", func() {
_, err := (&RFDetrCpp{handle: 1}).Detect(&pb.DetectOptions{})
Expect(err).To(MatchError("rfdetr-cpp: decoded image is empty"))
})
})
+10 -12
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?=c6beeef35526c6dc94b74a7fb69f9d2e6a2a7a12
STABLEDIFFUSION_GGML_VERSION?=d04e8950c1ec8d30248cbe996682b3182fb1adf6
CMAKE_ARGS+=-DGGML_MAX_NAME=128
@@ -38,17 +38,16 @@ else ifeq ($(BUILD_TYPE),hipblas)
ROCM_PATH ?= /opt/rocm
export CXX=$(ROCM_HOME)/llvm/bin/clang++
export CC=$(ROCM_HOME)/llvm/bin/clang
AMDGPU_TARGETS?=gfx908,gfx90a,gfx942,gfx950,gfx1030,gfx1100,gfx1101,gfx1102,gfx1200,gfx1201
CMAKE_ARGS+=-DSD_HIPBLAS=ON -DGGML_HIPBLAS=ON -DAMDGPU_TARGETS=$(AMDGPU_TARGETS)
AMDGPU_TARGETS?=gfx908,gfx90a,gfx942,gfx950,gfx1030,gfx1100,gfx1101,gfx1102,gfx1151,gfx1200,gfx1201
# SD_HIPBLAS turns on ggml's HIP backend itself; GGML_HIPBLAS is the name ggml
# used before it was renamed to GGML_HIP, so passing it here only produced an
# unused-variable warning.
CMAKE_ARGS+=-DSD_HIPBLAS=ON -DAMDGPU_TARGETS=$(AMDGPU_TARGETS)
else ifeq ($(BUILD_TYPE),vulkan)
CMAKE_ARGS+=-DSD_VULKAN=ON -DGGML_VULKAN=ON
else ifeq ($(OS),Darwin)
ifneq ($(BUILD_TYPE),metal)
CMAKE_ARGS+=-DSD_METAL=OFF -DGGML_METAL=OFF
else
CMAKE_ARGS+=-DSD_METAL=ON -DGGML_METAL=ON
CMAKE_ARGS+=-DGGML_METAL_EMBED_LIBRARY=ON
endif
else ifeq ($(BUILD_TYPE),metal)
CMAKE_ARGS+=-DSD_METAL=ON -DGGML_METAL=ON
CMAKE_ARGS+=-DGGML_METAL_EMBED_LIBRARY=ON
endif
ifeq ($(BUILD_TYPE),sycl_f16)
@@ -72,7 +71,6 @@ sources/stablediffusion-ggml.cpp:
git checkout $(STABLEDIFFUSION_GGML_VERSION) && \
git submodule update --init --recursive --depth 1 --single-branch
# Detect OS
UNAME_S := $(shell uname -s)
# Only build CPU variants on Linux
@@ -134,4 +132,4 @@ libgosd-custom: CMakeLists.txt cpp/gosd.cpp cpp/gosd.h
(mv build-$(SO_TARGET)/libgosd.so ./$(SO_TARGET) 2>/dev/null || \
mv build-$(SO_TARGET)/libgosd.dylib ./$(SO_TARGET) 2>/dev/null)
all: stablediffusion-ggml package
all: stablediffusion-ggml package
+18 -11
View File
@@ -401,7 +401,6 @@ int load_model(const char *model, char *model_path, char* options[], int threads
const char *params_backend_arg = "";
const char *rpc_servers_arg = "";
const char *max_vram_arg = "";
bool stream_layers = false;
int n_threads = threads;
enum sd_type_t wtype = SD_TYPE_COUNT;
@@ -510,7 +509,10 @@ int load_model(const char *model, char *model_path, char* options[], int threads
if (!strcmp(optname, "params_backend")) params_backend_arg = strdup(optval);
if (!strcmp(optname, "rpc_servers")) rpc_servers_arg = strdup(optval);
if (!strcmp(optname, "max_vram")) max_vram_arg = strdup(optval);
if (!strcmp(optname, "stream_layers")) stream_layers = (strcmp(optval, "true") == 0 || strcmp(optval, "1") == 0);
if (!strcmp(optname, "stream_layers")) {
// Retained as a no-op for existing configurations. Upstream now
// selects segmented weight streaming automatically.
}
// vae_decode_only is still accepted for backwards compatibility with
// existing gallery configs, but upstream dropped the option (the model
@@ -650,11 +652,9 @@ int load_model(const char *model, char *model_path, char* options[], int threads
ctx_params.rpc_servers = env_rpc_servers;
}
}
// max_vram: GiB budget or per-backend spec for graph-cut segmented param
// offload ("0" = disabled, "-1" = auto). stream_layers only has effect when
// max_vram is set.
// max_vram is an optional GiB budget or per-backend spec for automatic
// graph-cut execution. A zero value uses the live free-VRAM budget.
if (strlen(max_vram_arg) > 0) ctx_params.max_vram = max_vram_arg;
ctx_params.stream_layers = stream_layers;
ctx_params.diffusion_flash_attn = diffusion_flash_attn;
ctx_params.tae_preview_only = tae_preview_only;
ctx_params.diffusion_conv_direct = diffusion_conv_direct;
@@ -1144,17 +1144,25 @@ static uint8_t* load_and_resize_image(const char* path, int target_width, int ta
// Write sd.cpp's audio buffer to a temp WAV file (IEEE float, interleaved).
// sd_audio_t.data is planar (all channel 0 samples, then channel 1, etc.) — we
// interleave on the fly so ffmpeg's standard wav demuxer can read it directly.
// Returns 0 on success and fills wav_path (must be at least 64 bytes).
// Returns 0 on success and fills wav_path.
static int write_planar_float_wav(const sd_audio_t* a, char* wav_path, size_t wav_path_sz) {
if (!a || !a->data || a->sample_count == 0 || a->channels == 0 || a->sample_rate == 0) {
return -1;
}
snprintf(wav_path, wav_path_sz, "/tmp/gosd-audio-XXXXXX.wav");
const char* temp_dir = getenv("TMPDIR");
if (!temp_dir || temp_dir[0] == '\0') {
temp_dir = "/tmp";
}
int path_len = snprintf(wav_path, wav_path_sz, "%s/gosd-audio-XXXXXX.wav", temp_dir);
if (path_len < 0 || (size_t)path_len >= wav_path_sz) {
fprintf(stderr, "temporary directory path is too long\n");
return -1;
}
int fd = mkstemps(wav_path, 4);
if (fd < 0) { perror("mkstemps wav"); return -1; }
FILE* f = fdopen(fd, "wb");
if (!f) { perror("fdopen wav"); close(fd); return -1; }
if (!f) { perror("fdopen wav"); close(fd); unlink(wav_path); return -1; }
uint64_t frames = a->sample_count;
uint32_t channels = a->channels;
@@ -1221,7 +1229,7 @@ static int ffmpeg_mux_raw_to_mp4(sd_image_t* frames, int num_frames, int fps,
snprintf(fps_str, sizeof(fps_str), "%d", fps);
// Optional audio: write a temp WAV file if the model produced audio.
char wav_path[64] = {0};
char wav_path[4096] = {0};
bool have_audio = false;
if (audio && audio->data && audio->sample_count > 0 && audio->channels > 0 && audio->sample_rate > 0) {
if (write_planar_float_wav(audio, wav_path, sizeof(wav_path)) == 0) {
@@ -1438,4 +1446,3 @@ int unload() {
free_sd_ctx(sd_c);
return 0;
}
+42 -22
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?=0757cac231ecd571a83c4fd2f50805c9251fc225
VLLM_CPP_VERSION?=6bf3abb580982f4fd2e4525ef37802ee0ce28981
# 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
@@ -47,26 +47,35 @@ CMAKE_ARGS+=-DCMAKE_BUILD_TYPE=Release
UNAME_M := $(shell uname -m)
ifeq ($(BUILD_TYPE),cublas)
# Blackwell-family targets only: other CUDA arches are build-supported
# upstream but have no runtime-proven fast path. amd64 gets the consumer
# (120a) + GB10 (121a) fat binary; arm64 CUDA (l4t-style images, DGX
# Spark) is GB10 only. Triton-AOT GDN cubins are vendored per-arch, no
# Python needed to consume them.
# Every CUDA architecture upstream builds that the platform can actually
# host, split by where the silicon exists: Jetson (87 Orin, 110 Thor) is
# arm64-only, desktop 120a is amd64-only, and 90a/100a appear on both
# because of the SBSA parts (GH200, GB200).
#
# This deliberately matches vllm.cpp's own release archive rather than
# narrowing to the boxes we benchmark on. A narrower list does not degrade
# on an unlisted card, it dies at the first request with "no kernel image
# is available for execution on the device", long after `backends install`
# reported success -- so an arch we merely lack numbers for still belongs
# in the binary.
#
# Triton-AOT stays ON for both. A fat build is supported on the BUILDER
# path: it embeds every vendored cubin tree (sm_80/86/89/90a/100a/121a) and
# selects by exact SM at runtime, so the arches with no tree (87, 103a,
# 110, 120a) take the portable CUDA kernels and can never load a
# neighbouring cubin. Only maintainer REGEN needs a single pinned arch.
# See vllm.cpp cmake/TritonAOT.cmake `_triton_aot_arch_names`.
#
# CUDA builds REQUIRE the CUDA 13 toolchain: 12.x nvcc lacks compute_121a
# (GB10) and its ptxas rejects the sm_120a NVFP4 MMA kernels ("Vector type
# too large"), so no cuda-12 variant is shipped.
ifeq ($(CUDA_MAJOR_VERSION),12)
$(error vllm.cpp needs the CUDA 13 toolchain: CUDA 12.x cannot compile the Blackwell fp4 kernels)
endif
ifeq ($(UNAME_M),x86_64)
# NO -DVLLM_CPP_TRITON on fat builds: the vendored Triton-AOT cubin
# trees are per-arch and the engine refuses a multi-arch build unless
# pinned to one tree (unsound for the other arch). The non-AOT GDN
# path serves the fat binary; single-arch builds keep the cubins.
#
# CUDA builds REQUIRE the CUDA 13 toolchain: 12.x nvcc lacks
# compute_121a (GB10) and its ptxas rejects the sm_120a NVFP4 MMA
# kernels ("Vector type too large"), so no cuda-12 variant is shipped.
ifeq ($(CUDA_MAJOR_VERSION),12)
$(error vllm.cpp needs the CUDA 13 toolchain: CUDA 12.x cannot compile the Blackwell fp4 kernels)
endif
CMAKE_ARGS+=-DVLLM_CPP_CUDA=ON "-DVLLM_CPP_CUDA_ARCHITECTURES=120a;121a"
CMAKE_ARGS+=-DVLLM_CPP_CUDA=ON "-DVLLM_CPP_CUDA_ARCHITECTURES=80;86;89;90a;100a;103a;120a;121a" -DVLLM_CPP_TRITON=ON
else
CMAKE_ARGS+=-DVLLM_CPP_CUDA=ON -DVLLM_CPP_CUDA_ARCHITECTURES=121a -DVLLM_CPP_TRITON=ON
CMAKE_ARGS+=-DVLLM_CPP_CUDA=ON "-DVLLM_CPP_CUDA_ARCHITECTURES=87;90a;100a;110;121a" -DVLLM_CPP_TRITON=ON
endif
else ifeq ($(BUILD_TYPE),vulkan)
CMAKE_ARGS+=-DVLLM_CPP_VULKAN=ON -DVLLM_CPP_CUDA=OFF
@@ -106,13 +115,24 @@ else
LIB=libvllm.so
endif
sources/vllm.cpp:
# patches/ carries fixes the pinned engine SHA does not have yet. `git apply`
# is deliberately unguarded: a patch that no longer applies must FAIL the clone
# loudly, because the alternative is a pin that silently ships without a fix it
# is documented to carry. Each patch header says which pin retires it.
VLLM_CPP_PATCHES=$(wildcard patches/*.patch)
sources/vllm.cpp: $(VLLM_CPP_PATCHES)
rm -rf sources/vllm.cpp
mkdir -p sources/vllm.cpp
cd sources/vllm.cpp && \
git init && \
git remote add origin $(VLLM_CPP_REPO) && \
git fetch --depth 1 origin $(VLLM_CPP_VERSION) && \
git checkout FETCH_HEAD
git checkout FETCH_HEAD && \
for p in $(VLLM_CPP_PATCHES); do \
echo "==> applying $$p"; \
git apply ../../$$p || exit 1; \
done
ifeq ($(MLX_ENABLED),1)
# A stamp FILE, not a phony target: a phony prerequisite is always "newer" than
@@ -165,7 +185,7 @@ $(LIB): sources/vllm.cpp $(MLX_STAMP)
cmake --build . --config Release -j$(JOBS) --target vllm_shared
cp -fL build/$(LIB) ./$(LIB)
vllm-cpp: main.go govllmcpp.go backend.go options.go $(LIB)
vllm-cpp: main.go govllmcpp.go backend.go chat.go options.go video.go $(LIB)
CGO_ENABLED=0 $(GOCMD) build -tags "$(GO_TAGS)" -o vllm-cpp ./
package: vllm-cpp
+73 -2
View File
@@ -1,12 +1,15 @@
# vllm-cpp backend
LocalAI text-generation backend for [vllm.cpp](https://github.com/mudler/vllm.cpp),
LocalAI backend for [vllm.cpp](https://github.com/mudler/vllm.cpp),
the LocalAI-team C++20 port of vLLM (paged KV cache, continuous batching,
safetensors + GGUF loading, CUDA / CPU / Metal / Vulkan) with no Python at
inference time.
It serves two things: text generation, and MiniMax-H3 joint video+audio
generation.
The backend dlopens the engine's stable C ABI (`libvllm`, `include/vllm.h`,
ABI v10) through purego:
ABI v20) through purego:
- `Load` -> `vllm_engine_load`: accepts a `.gguf` file or a HF-style model
directory (`config.json` + safetensors). `context_size` maps to
@@ -29,6 +32,12 @@ ABI v10) through purego:
LocalAI's Go-side grammar-constrained tool calling; JSON-schema / regex /
choice constraints are also exposed by the ABI.
`patches/` carries fixes the pinned engine SHA does not have yet, applied to
the clone the same way `longcat-video` patches its upstream. `git apply` is
unguarded on purpose: a patch that stops applying must fail the clone loudly
rather than leave a pin silently missing a fix it is documented to carry. Each
patch header says what retires it.
The struct mirrors in `govllmcpp.go` are hand-written against one ABI version,
and the engine refuses to load against any other. Moving `VLLM_CPP_VERSION` in
the Makefile therefore means updating `abiVersion` plus the mirrors (and their
@@ -47,6 +56,68 @@ options:
- max_num_seqs:16
```
## MiniMax-H3 video+audio generation
`GenerateVideo` -> `vllm_video_generate` (ABI v12). H3 renders picture and sound
together, so the output MP4 carries a real AAC track.
The video engine is a SECOND handle (`vllm_video_engine`), not a mode of the
text one, because H3 is a checkpoint SET rather than a model directory: the DiT,
the text encoder and two VAEs are separate artifacts, and vllm.cpp has the two
loaders refuse each other's checkpoints. `Load` takes the video branch when the
model config carries any of the video options below; `parameters.model` is the
DiT and everything else is named in `options:`.
```yaml
name: minimax-h3-fl2va-q4
backend: vllm-cpp
cuda: true
known_usecases: [video]
parameters:
model: minimax-h3/MiniMax-H3-FL2VA-Q4_K_M.gguf
options:
- video_encoder:minimax-h3/qwen3vl-32B-MiniMax-H3-Q4_K_M.gguf
- video_tokenizer:minimax-h3/tokenizer.json
- video_vae:minimax-h3/video_vae.safetensors
- video_vae_config:minimax-h3/video_vae_config.json
- audio_vae:minimax-h3/audio_vae.safetensors
- audio_vae_config:minimax-h3/audio_vae_config.json
- video_partition:fl2va
- video_device:cuda
- video_dequant_bf16:true
- video_width:1344
- video_height:768
- video_num_frames:124
```
Three things are worth knowing before touching this path.
**The partition is declared, not detected, and a mismatch does not fail
cleanly.** The FL2VA DiT serves `t2va` and `fl2va`; `ref2va` is a different
checkpoint. The community GGUF/NVFP4 quantisations strip the release metadata
and the two DiTs are byte-structurally identical, so the engine refuses every
generate until `video_partition` says which one it has. Handing reference
conditioning to an FL2VA DiT renders for hours and returns a coloured lattice
over the frame, so `checkPartitionConditioning` refuses that combination here,
before the engine is called.
**ffmpeg comes from the host.** libvllm writes the frames and the WAV and
COMPOSES the mux argv, then spawns nothing — that process boundary is upstream's
decision. `muxVideo` takes the composed argv, substitutes `argv[0]` with the
resolved binary and execs it; the backend image is `FROM scratch` and carries no
ffmpeg, the same arrangement `vibevoice-cpp` uses for transcoding. ffmpeg also
converts a `start_image`/`end_image` upload into the binary PPM at the exact
output canvas the engine requires, since libvllm vendors neither an image codec
nor a resampler.
**It is slow.** Roughly 176 s per denoise step at 1344x768 on a 20-SM device, so
the 50-step default is hours. Nothing here imposes a deadline.
Geometry mirrors the engine so the two agree: the canvas is truncated onto a
32-pixel grid, the frame count sits on the 17n+5 grid, and an unspecified canvas
with a keyframe is derived from that image's aspect on a 768-pixel short edge
(`MiniMaxH3ResolveShape`, `minimax_h3_planner.cpp`).
## Apple Silicon: the MLX GEMM provider (ON by default, gated to prefill)
`BUILD_TYPE=metal` builds vllm.cpp's MLX provider for the dense GEMM
+18 -1
View File
@@ -28,7 +28,12 @@ type VllmCpp struct {
base.Base
engine uintptr
opts loadOptions
// videoEngine is the MiniMax-H3 handle (ABI v12). It is deliberately a
// SECOND handle, not a mode of the first: H3 is a checkpoint set rather
// than a model directory, and vllm.cpp has the two loaders refuse each
// other's checkpoints. Exactly one of the two is ever non-zero.
videoEngine uintptr
opts loadOptions
}
// Stream registry: the per-request bridge between the C token callback and
@@ -109,6 +114,14 @@ func (v *VllmCpp) Load(opts *pb.ModelOptions) error {
v.opts = parseOptions(opts)
// MiniMax-H3 is a checkpoint SET behind its own engine handle, so the
// branch is taken before any text-engine knob is resolved. The two loaders
// refuse each other's checkpoints, which is why this is decided from the
// config rather than probed.
if v.opts.video.engaged() {
return v.loadVideo(opts, model)
}
// A DFlash draft is a second checkpoint the engine opens by path, and the
// engine never downloads one. Resolve it against LocalAI's models directory
// now so a repo-id spelling works, and so a missing draft fails here with an
@@ -194,6 +207,10 @@ func (v *VllmCpp) Free() error {
vllmEngineFree(v.engine)
v.engine = 0
}
if v.videoEngine != 0 {
vllmVideoEngineFree(v.videoEngine)
v.videoEngine = 0
}
return nil
}
+128 -3
View File
@@ -1,6 +1,6 @@
package main
// purego bindings for the vllm.cpp stable C ABI (include/vllm.h, ABI v10).
// purego bindings for the vllm.cpp stable C ABI (include/vllm.h, ABI v23).
//
// The structs below are hand-mirrored PODs of the C declarations, with
// explicit padding so the Go layout matches the C layout on linux/darwin
@@ -21,7 +21,7 @@ import (
// the header of the VLLM_CPP_VERSION pinned in the Makefile: the build checks
// the two against each other, because a mismatch is only caught at runtime by
// registerLib, where it takes the backend down on every load (issue #11379).
const abiVersion = 10
const abiVersion = 23
// The ABI's tri-state toggles (enable_prefix_caching ABI v7,
// enable_jump_forward ABI v10) share one encoding: 0 is NOT "off", it is
@@ -69,8 +69,21 @@ type cModelParams struct {
MaxNumBatchedTokens int32 // <= 0 = per-arch default (ABI v9)
SchedulingPolicy uintptr // const char*; NULL = "fcfs" (ABI v9)
KVTransferConfig uintptr // const char* JSON; NULL = no connector (ABI v9)
OffloadConfig uintptr // const char* JSON; NULL = no weight offload
EnableJumpForward int32 // tri-state 0/1/2 (ABI v10)
_ [4]byte // trailing pad to the struct's 8-byte alignment
// v14/v16 tail. LocalAI sets none of these (0 is "auto" for the device and
// "unset" for both sizing knobs, i.e. the pre-v14 engine byte for byte), but
// the fields MUST be mirrored: the C side reads sizeof(vllm_model_params)
// bytes off the pointer we hand it, so a Go struct that stopped at
// EnableJumpForward would have vllm_engine_load read 24 bytes past our
// allocation and size the KV pool from whatever sat there.
Device int32 // 0 auto, 1 cpu, 2 cuda (ABI v14)
GPUMemoryUtil float64 // 0 => 0.92 (ABI v16)
KVCacheMemoryBytes int64 // 0 => unset (ABI v16)
LanguageModelOnly int32 // 0 = multimodal inputs enabled (ABI v19)
_ [4]byte
LimitMMPerPrompt uintptr // const char* JSON; NULL = default limits (ABI v19)
MMProjPath uintptr // const char*; NULL = no GGUF projector (ABI v22)
}
// cSamplingParams mirrors vllm_sampling_params (structured fields included).
@@ -117,6 +130,88 @@ type cCompletion struct {
CompletionTokens int32
}
// ── Video+audio generation (ABI v12, MiniMax-H3) ────────────────────────────
//
// A video engine is a SEPARATE handle from vllm_engine: H3 is a checkpoint SET
// (DiT + text encoder + two VAEs), not one model directory, and the two loaders
// refuse each other's checkpoints on purpose. Offsets are asserted in
// video_test.go the same way the text PODs are in vllmcpp_test.go.
// cVideoModelParams mirrors vllm_video_model_params. Nine pointers then three
// int32s, so only the trailing pad is implicit.
type cVideoModelParams struct {
DitPath uintptr // const char*
EncoderPath uintptr // const char*
TokenizerPath uintptr // const char*
VideoVaePath uintptr // const char*
VideoVaeConfigPath uintptr // const char*
AudioVaePath uintptr // const char*
AudioVaeConfigPath uintptr // const char*
PromptEmbedsPath uintptr // const char*
Partition uintptr // const char*; "fl2va" | "ref2va", REQUIRED
Device int32 // 0 cpu, 1 cuda
DequantBf16 int32 // 0 keep-quant, 1 dequant/stream bf16
Fp4Resident int32 // NVFP4+cuda: keep FP4 packed, Marlin W4A16
_ [4]byte
Family uintptr // const char*; NULL = detect (ABI v18)
ExtraKeys uintptr // const char* const* (ABI v18)
ExtraValues uintptr // const char* const* (ABI v18)
NExtras int32 // 0 = none (ABI v18)
_ [4]byte // trailing pad to the struct's 8-byte alignment
}
// cVideoParams mirrors vllm_video_params. `width`/`height` and `num_frames`/
// `steps` pair up into 8-byte slots; the uint64 seed forces the alignment after
// them, and the float noise_aug leaves a pad before output_dir.
type cVideoParams struct {
Prompt uintptr // const char*
Width int32
Height int32
NumFrames int32 // <= 1 => per-task default (124 for t2va/fl2va)
Steps int32 // <= 0 => the H3 default (50)
Seed uint64
HasSeed int32
_ [4]byte
FirstFrame uintptr // const char*; fl2va keyframe, binary PPM (P6)
LastFrame uintptr // const char*
RefImage uintptr // const char*; ref2va only
RefVideo uintptr // const char*; ref2va only, a frame_%06d.ppm DIRECTORY
RefAudio uintptr // const char*; ref2va only, 16-bit PCM WAV
NoiseAug float32 // <= 0 => 1.0
_ [4]byte
OutputDir uintptr // const char*; REQUIRED
ExtraKeys uintptr // const char* const* (ABI v18)
ExtraValues uintptr // const char* const* (ABI v18)
NExtras int32 // 0 = none (ABI v18)
_ [4]byte
}
// cVideoResult mirrors vllm_video_result. Every member is library-allocated and
// released together by vllm_video_result_free.
type cVideoResult struct {
FrameDir uintptr // char*, holds frame_%06d.ppm
AudioPath uintptr // char*, 16-bit PCM WAV
FrameCount int32
Width int32
Height int32
Fps int32
SampleRate int32
_ [4]byte
MuxArgv uintptr // char**, NULL-terminated at MuxArgc
MuxArgc int32
_ [4]byte
}
// cVideoMuxParams mirrors vllm_video_mux_params. The library composes the argv;
// spawning it is the CALLER's job, which is why no ffmpeg lives in libvllm.
type cVideoMuxParams struct {
Frames uintptr // const char*; printf pattern, dir/frame_%06d.ppm
AudioPath uintptr // const char*; NULL/empty => a silent clip
OutputPath uintptr // const char*; the .mp4 to write
Fps int32 // <= 0 => the H3 default (24)
Crf int32 // <= 0 => the library default (18)
}
// defaultSamplingParams mirrors vllm_sampling_params_default().
func defaultSamplingParams() cSamplingParams {
return cSamplingParams{
@@ -148,6 +243,14 @@ var (
vllmLastError func() string
vllmVersion func() string
vllmABIVersion func() int32
// Video+audio generation (ABI v12).
vllmVideoEngineLoad func(params, out unsafe.Pointer) int32
vllmVideoEngineFree func(engine uintptr)
vllmVideoGenerate func(engine uintptr, params, out unsafe.Pointer) int32
vllmVideoResultFree func(out unsafe.Pointer)
vllmVideoMuxArgv func(params, outArgv, outArgc unsafe.Pointer) int32
vllmVideoMuxArgvFre func(argv uintptr, argc int32)
)
type libFunc struct {
@@ -175,6 +278,12 @@ func registerLib(libName string) error {
{&vllmLastError, "vllm_last_error"},
{&vllmVersion, "vllm_version"},
{&vllmABIVersion, "vllm_abi_version"},
{&vllmVideoEngineLoad, "vllm_video_engine_load"},
{&vllmVideoEngineFree, "vllm_video_engine_free"},
{&vllmVideoGenerate, "vllm_video_generate"},
{&vllmVideoResultFree, "vllm_video_result_free"},
{&vllmVideoMuxArgv, "vllm_video_mux_argv"},
{&vllmVideoMuxArgvFre, "vllm_video_mux_argv_free"},
} {
purego.RegisterLibFunc(lf.ptr, lib, lf.name)
}
@@ -222,3 +331,19 @@ func goString(p uintptr) string {
}
return string(unsafe.Slice((*byte)(base), n))
}
// goStringSlice copies a C `char*` array of n entries. Used for the ffmpeg argv
// the library composes: it is copied out immediately so the caller can free the
// C allocation before ever spawning the process.
func goStringSlice(p uintptr, n int32) []string {
if p == 0 || n <= 0 {
return nil
}
//nolint:govet // C-owned pointer handed over by purego, valid for this call
entries := unsafe.Slice((**byte)(unsafe.Pointer(p)), int(n)) // #nosec G103 -- C-owned, copied out immediately
out := make([]string, 0, n)
for _, e := range entries {
out = append(out, goString(uintptr(unsafe.Pointer(e)))) // #nosec G103 -- ditto
}
return out
}
+178
View File
@@ -62,15 +62,106 @@ type loadOptions struct {
// Override for the tokenizer_config.json the chat template is read from
// (ABI v9). Empty = <model_dir>/tokenizer_config.json.
tokenizerConfigPath string
// MiniMax-H3 video+audio generation (ABI v12). Present only when the config
// carries at least one of its keys; see videoOptions.engaged.
video videoOptions
}
// videoOptions is the MiniMax-H3 checkpoint SET plus its generation defaults.
//
// H3 is not one model directory: the DiT, the text encoder and the two VAEs are
// separate artifacts, which is why vllm.cpp gives video its own engine handle
// (vllm_video_engine, ABI v12) rather than another vllm_engine. The DiT is the
// model config's `parameters.model`; everything else arrives through these
// options, so one gallery entry can name five files.
//
// The geometry/frame defaults exist because H3's trained canvas is nothing like
// the generic /video defaults: 1344x768 at 124 frames is a ~5.2 s clip, and the
// frame count must sit on the 17n+5 grid. A request that leaves a field unset
// gets the model's own default from here instead of a canvas the checkpoint was
// never trained at.
type videoOptions struct {
encoderPath string // H3-Encoder GGUF or bf16 shard dir
tokenizerPath string // tokenizer.json, needed with an encoder
videoVaePath string
videoVaeConfig string
audioVaePath string
audioVaeConfig string
promptEmbedsPath string // fallback conditioning when there is no encoder
// The served checkpoint PARTITION. Community GGUF/NVFP4 files strip the
// release metadata and the FL2VA/Ref2VA DiTs are byte-structurally
// identical, so the engine refuses every generate until it is DECLARED.
// "fl2va" serves t2va + fl2va; "ref2va" serves reference conditioning.
partition string
device int32 // 0 cpu, 1 cuda (the ABI's own encoding, no auto slot)
deviceSet bool
dequantBf16 int32
fp4Resident int32
// Per-model generation defaults, applied when the request leaves the field
// at 0.
width int32
height int32
numFrames int32
steps int32
// Where frames + WAV are written. Empty = a temporary directory beside the
// requested output, removed once the mux succeeds. Set it to keep the
// frame_%06d.ppm runs around (they are what ref2va's ref_video consumes).
workdir string
// The ffmpeg binary the composed mux argv is exec'd with. Empty = "ffmpeg"
// from PATH. libvllm composes the argv and spawns nothing, by design.
ffmpeg string
crf int32
}
// engaged reports whether this config describes an H3 video engine. Load uses
// it to choose which of the two mutually exclusive engine handles to open: the
// checkpoints refuse each other, so guessing is not an option, and every key
// below is meaningless to the text engine.
func (v videoOptions) engaged() bool {
return v.encoderPath != "" || v.tokenizerPath != "" ||
v.videoVaePath != "" || v.videoVaeConfig != "" ||
v.audioVaePath != "" || v.audioVaeConfig != "" ||
v.promptEmbedsPath != "" || v.partition != ""
}
func parseOptions(opts *pb.ModelOptions) loadOptions {
lo := loadOptions{}
applyOptionsList(&lo, opts.GetOptions())
applyEngineArgs(&lo, opts.GetEngineArgs())
applyDraftModelOption(&lo, opts.GetOptions())
return lo
}
// applyDraftModelOption binds a managed companion snapshot after engine_args
// has supplied the speculative document. Companion paths do not exist until
// LocalAI materializes the artifact, so they must replace the gallery's static
// repository reference without disturbing the method or token budget.
func applyDraftModelOption(lo *loadOptions, options []string) {
if strings.TrimSpace(lo.speculativeConfig) == "" {
return
}
var draftModel string
for _, option := range options {
key, value, found := strings.Cut(option, ":")
if found && strings.TrimSpace(key) == "draft_model" {
draftModel = strings.TrimSpace(value)
}
}
if draftModel == "" {
return
}
var spec map[string]any
if err := json.Unmarshal([]byte(lo.speculativeConfig), &spec); err != nil {
return
}
spec["model"] = draftModel
encoded, err := json.Marshal(spec)
if err == nil {
lo.speculativeConfig = string(encoded)
}
}
// applyOptionsList reads the legacy free-form "key:value" list. strings.Cut
// splits on the FIRST colon only, so a JSON object value survives intact.
func applyOptionsList(lo *loadOptions, options []string) {
@@ -110,10 +201,94 @@ func applyOptionsList(lo *loadOptions, options []string) {
if b, err := strconv.ParseBool(strings.TrimSpace(v)); err == nil {
lo.enableJumpForward = boolTriState(b)
}
default:
applyVideoOption(&lo.video, strings.TrimSpace(k), v)
}
}
}
// applyVideoOption reads one MiniMax-H3 key. Split out of applyOptionsList so
// the video surface stays legible next to the videoOptions it fills, and so
// video_test.go can exercise it directly.
func applyVideoOption(vo *videoOptions, key, value string) bool {
v := strings.TrimSpace(value)
switch key {
case "video_encoder":
vo.encoderPath = v
case "video_tokenizer":
vo.tokenizerPath = v
case "video_vae":
vo.videoVaePath = v
case "video_vae_config":
vo.videoVaeConfig = v
case "audio_vae":
vo.audioVaePath = v
case "audio_vae_config":
vo.audioVaeConfig = v
case "video_prompt_embeds":
vo.promptEmbedsPath = v
case "video_partition":
vo.partition = strings.ToLower(v)
case "video_device":
switch strings.ToLower(v) {
case "cpu":
vo.device, vo.deviceSet = videoDeviceCPU, true
case "cuda", "gpu":
vo.device, vo.deviceSet = videoDeviceCUDA, true
default:
xlog.Warn("[vllm-cpp] ignoring unknown video_device", "value", v)
}
case "video_dequant_bf16":
if b, err := strconv.ParseBool(v); err == nil {
vo.dequantBf16 = boolInt32(b)
}
case "video_fp4_resident":
if b, err := strconv.ParseBool(v); err == nil {
vo.fp4Resident = boolInt32(b)
}
case "video_width":
vo.width = parseInt32(v, vo.width)
case "video_height":
vo.height = parseInt32(v, vo.height)
case "video_num_frames":
vo.numFrames = parseInt32(v, vo.numFrames)
case "video_steps":
vo.steps = parseInt32(v, vo.steps)
case "video_workdir":
vo.workdir = v
case "video_crf":
vo.crf = parseInt32(v, vo.crf)
case "ffmpeg", "ffmpeg_path":
vo.ffmpeg = v
default:
return false
}
return true
}
// videoScalarString renders an engine_args scalar so the video keys can share
// one parser with the "key:value" list. Objects and arrays have no video
// meaning and are left to the caller's unknown-key path.
func videoScalarString(v any) (string, bool) {
switch t := v.(type) {
case string:
return t, true
case bool:
return strconv.FormatBool(t), true
case float64:
return strconv.FormatFloat(t, 'f', -1, 64), true
default:
return "", false
}
}
func boolInt32(b bool) int32 {
if b {
return 1
}
return 0
}
// applyEngineArgs overlays the `engine_args:` JSON object. A document that does
// not parse is logged and skipped: engine_args is shared with the other engines
// (the vLLM and SGLang backends read the same field), so a stray key must not
@@ -160,6 +335,9 @@ func applyEngineArgs(lo *loadOptions, engineArgs string) {
lo.enableJumpForward = boolTriState(b)
}
default:
if s, ok := videoScalarString(v); ok && applyVideoOption(&lo.video, k, s) {
continue
}
xlog.Debug("[vllm-cpp] ignoring unknown engine_args key", "key", k)
}
}
+38
View File
@@ -0,0 +1,38 @@
package main
import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
pb "github.com/mudler/LocalAI/pkg/grpc/proto"
)
var _ = Describe("managed DFlash companion options", func() {
It("replaces only the draft model in an existing speculative configuration", func() {
managedPath := ".artifacts/huggingface/0123456789abcdef/snapshot"
lo := parseOptions(&pb.ModelOptions{
Options: []string{"draft_model:" + managedPath},
EngineArgs: `{
"speculative_config": {
"method": "dflash",
"model": "Mia-AiLab/Qwen3.8-27B-DFlash2-EXL3-5.0bpw",
"num_speculative_tokens": 7
}
}`,
})
Expect(lo.speculativeConfig).To(MatchJSON(`{
"method": "dflash",
"model": ".artifacts/huggingface/0123456789abcdef/snapshot",
"num_speculative_tokens": 7
}`))
})
It("ignores a draft companion when speculative decoding is not configured", func() {
lo := parseOptions(&pb.ModelOptions{
Options: []string{"draft_model:.artifacts/huggingface/0123456789abcdef/snapshot"},
})
Expect(lo.speculativeConfig).To(BeEmpty())
})
})
+634
View File
@@ -0,0 +1,634 @@
package main
// MiniMax-H3 video+audio generation over the vllm.cpp C ABI (v12).
//
// Two things make this different from the text path, and both come from the
// engine's own shape rather than from LocalAI:
//
// 1. A video engine is loaded from a checkpoint SET - the DiT, the text
// encoder and two VAEs are separate artifacts - so it is its own handle
// (vllm_video_engine) and its own Load branch. The two loaders refuse each
// other's checkpoints on purpose.
// 2. libvllm writes frames + a WAV and COMPOSES the ffmpeg argv, but spawns
// nothing. That process boundary is deliberate upstream, so the mux lives
// here: we take the composed argv, substitute argv[0], and exec it. ffmpeg
// comes from PATH the same way the vibevoice-cpp backend takes it.
//
// Generation is SLOW - roughly 176 s per denoise step at 1344x768 on a 20-SM
// device, so a default 50-step render is hours, not seconds. Nothing here
// imposes a deadline: GenerateVideo blocks for as long as the engine needs and
// the gRPC call carries LocalAI's application context.
import (
"fmt"
"image"
"math"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
"unsafe"
// Registered for image.DecodeConfig only: a staged keyframe arrives as
// whatever the caller uploaded, and we need its geometry to size the canvas.
_ "image/gif"
_ "image/jpeg"
_ "image/png"
pb "github.com/mudler/LocalAI/pkg/grpc/proto"
"github.com/mudler/xlog"
)
// vllm_video_model_params.device (vllm.h): no auto slot, unlike the text
// engine's v14 device field.
const (
videoDeviceCPU int32 = 0
videoDeviceCUDA int32 = 1
)
// H3's shipped geometry. The canvas is truncated onto a 32-pixel grid and the
// frame count onto the 17n+5 grid by the engine itself
// (MiniMaxH3ResolveShape / MiniMaxH3AlignFrameCount in
// src/vllm/model_executor/models/minimax_h3_planner.cpp); mirrored here only so
// a keyframe can be resampled to the exact canvas the engine will render at.
const (
h3CanvasMultiple int32 = 32
h3FrameGrid int32 = 17
h3FrameOffset int32 = 5
h3ShortEdge int32 = 768
)
// videoPartitions are the two DECLARED partitions of the H3 release. The FL2VA
// checkpoint serves t2va and fl2va; ref2va is a different checkpoint. Passing
// reference conditioning against an fl2va DiT is a partition mismatch that
// renders a coloured lattice over the frame rather than failing cleanly, which
// is why it is refused here before the engine is ever called.
const (
partitionFL2VA = "fl2va"
partitionRef2VA = "ref2va"
)
// videoRequestParams are the per-request `params` keys this backend accepts.
// Unknown keys are an error rather than a silent drop: a misspelled reference
// path would otherwise produce a perfectly successful render of the wrong
// thing, hours later.
var videoRequestParams = []string{"noise_aug", "ref_image", "ref_video", "crf"}
// loadVideo opens the H3 checkpoint set. `dit` is the model config's
// parameters.model; every other artifact comes from the options.
func (v *VllmCpp) loadVideo(opts *pb.ModelOptions, dit string) error {
vo := &v.opts.video
// Relative option paths resolve against LocalAI's models directory, which
// is where the gallery lands the five H3 files.
resolve := func(p string) string {
if p == "" || filepath.IsAbs(p) || opts.ModelPath == "" {
return p
}
return filepath.Join(opts.ModelPath, p)
}
vo.encoderPath = resolve(vo.encoderPath)
vo.tokenizerPath = resolve(vo.tokenizerPath)
vo.videoVaePath = resolve(vo.videoVaePath)
vo.videoVaeConfig = resolve(vo.videoVaeConfig)
vo.audioVaePath = resolve(vo.audioVaePath)
vo.audioVaeConfig = resolve(vo.audioVaeConfig)
vo.promptEmbedsPath = resolve(vo.promptEmbedsPath)
vo.workdir = resolve(vo.workdir)
// A VAE config carries the per-channel latents_mean/latents_std and the
// temporal clip_length/token_drop; decode is wrong without it. The release
// ships it beside the weights, so default to that rather than making every
// config repeat it.
if vo.videoVaeConfig == "" && vo.videoVaePath != "" {
vo.videoVaeConfig = siblingConfigJSON(vo.videoVaePath)
}
if vo.audioVaeConfig == "" && vo.audioVaePath != "" {
vo.audioVaeConfig = siblingConfigJSON(vo.audioVaePath)
}
if vo.partition == "" {
// The community GGUF/NVFP4 quantisations strip the release metadata and
// the two DiTs are byte-structurally identical, so the engine cannot
// infer this and refuses every generate until it is declared. The
// shipped FL2VA checkpoint is the one the gallery entry installs.
vo.partition = partitionFL2VA
xlog.Warn("[vllm-cpp] video partition not declared, assuming the FL2VA checkpoint",
"hint", "set options: [video_partition:fl2va] or [video_partition:ref2va] to match the DiT you installed")
}
if vo.partition != partitionFL2VA && vo.partition != partitionRef2VA {
return fmt.Errorf("vllm-cpp: video_partition must be %q or %q, got %q",
partitionFL2VA, partitionRef2VA, vo.partition)
}
if vo.videoVaePath == "" || vo.audioVaePath == "" {
return fmt.Errorf("vllm-cpp: MiniMax-H3 needs both VAEs: set options: " +
"[video_vae:<video vae .safetensors>, audio_vae:<audio vae .safetensors>]")
}
if vo.encoderPath == "" && vo.promptEmbedsPath == "" {
return fmt.Errorf("vllm-cpp: MiniMax-H3 needs text conditioning: set options: " +
"[video_encoder:<encoder .gguf>, video_tokenizer:<tokenizer.json>] " +
"or [video_prompt_embeds:<f32 embeddings>]")
}
if !vo.deviceSet && opts.GetCUDA() {
vo.device = videoDeviceCUDA
}
mp := cVideoModelParams{
Device: vo.device,
DequantBf16: vo.dequantBf16,
Fp4Resident: vo.fp4Resident,
}
var keep [][]byte
setStr := func(dst *uintptr, s string) {
if s == "" {
return
}
b := cString(s)
keep = append(keep, b)
*dst = uintptr(unsafe.Pointer(&b[0])) // #nosec G103 -- borrowed by C for the load call only
}
setStr(&mp.DitPath, dit)
setStr(&mp.EncoderPath, vo.encoderPath)
setStr(&mp.TokenizerPath, vo.tokenizerPath)
setStr(&mp.VideoVaePath, vo.videoVaePath)
setStr(&mp.VideoVaeConfigPath, vo.videoVaeConfig)
setStr(&mp.AudioVaePath, vo.audioVaePath)
setStr(&mp.AudioVaeConfigPath, vo.audioVaeConfig)
setStr(&mp.PromptEmbedsPath, vo.promptEmbedsPath)
setStr(&mp.Partition, vo.partition)
xlog.Info("[vllm-cpp] Load (MiniMax-H3 video)", "dit", dit, "engine", vllmVersion(),
"encoder", vo.encoderPath, "tokenizer", vo.tokenizerPath,
"videoVae", vo.videoVaePath, "audioVae", vo.audioVaePath,
"partition", vo.partition, "device", videoDeviceName(vo.device),
"dequantBf16", vo.dequantBf16 == 1, "fp4Resident", vo.fp4Resident == 1)
var engine uintptr
rc := vllmVideoEngineLoad(unsafe.Pointer(&mp), unsafe.Pointer(&engine)) // #nosec G103 -- POD out-params
runtime.KeepAlive(keep)
if rc != vllmOK {
return fmt.Errorf("vllm-cpp: video engine load failed: %s", vllmLastError())
}
v.videoEngine = engine
return nil
}
// GenerateVideo renders one clip and muxes it to opts.Dst as an MP4 carrying
// H3's jointly generated AAC audio track. It blocks for the whole render.
func (v *VllmCpp) GenerateVideo(opts *pb.GenerateVideoRequest) error {
if v.videoEngine == 0 {
return fmt.Errorf("vllm-cpp: this model is not a MiniMax-H3 video engine " +
"(load it with the video_vae / audio_vae / video_encoder options)")
}
if strings.TrimSpace(opts.GetPrompt()) == "" {
return fmt.Errorf("vllm-cpp: video generation needs a prompt")
}
dst := opts.GetDst()
if dst == "" {
return fmt.Errorf("vllm-cpp: video generation needs an output path")
}
vo := v.opts.video
extra, err := parseVideoRequestParams(opts.GetParams())
if err != nil {
return err
}
if err := checkPartitionConditioning(vo.partition, opts, extra); err != nil {
return err
}
if opts.GetNegativePrompt() != "" {
xlog.Warn("[vllm-cpp] MiniMax-H3 has no negative prompt; ignoring it")
}
if opts.GetCfgScale() != 0 {
xlog.Warn("[vllm-cpp] MiniMax-H3 has no classifier-free guidance scale; ignoring cfg_scale")
}
workdir, cleanup, err := v.videoWorkdir(dst)
if err != nil {
return err
}
defer cleanup()
width, height := firstPositive(opts.GetWidth(), vo.width), firstPositive(opts.GetHeight(), vo.height)
frames := firstPositive(opts.GetNumFrames(), vo.numFrames)
steps := firstPositive(opts.GetStep(), vo.steps)
vp := cVideoParams{
NumFrames: frames,
Steps: steps,
NoiseAug: extra.noiseAug,
}
if opts.GetSeed() > 0 {
vp.Seed = uint64(opts.GetSeed())
vp.HasSeed = 1
}
if aligned := alignFrameCount(frames); aligned != frames {
xlog.Warn("[vllm-cpp] frame count is not on H3's 17n+5 grid; the engine rounds up",
"requested", frames, "rendered", aligned)
}
// Keyframes must be binary PPM (P6) at the exact output canvas: no image
// codec and no resampler is vendored in libvllm. Resolve the canvas first,
// then stage the frames through ffmpeg into it.
//
// The REQUEST's geometry is what is honoured here, not the model-level
// default: that default is a t2va canvas, and applying it to a keyframe
// would stretch a portrait photo into a 1344x768 letterbox. With no
// requested geometry the canvas comes from the keyframe's own aspect, which
// is the rule the engine itself applies (MiniMaxH3ResolveShape).
first, last := opts.GetStartImage(), opts.GetEndImage()
if first != "" || last != "" {
width, height, err = resolveCanvas(opts.GetWidth(), opts.GetHeight(), first, last)
if err != nil {
return err
}
if first, err = stageKeyframe(vo.ffmpeg, first, width, height, workdir, "first"); err != nil {
return err
}
if last, err = stageKeyframe(vo.ffmpeg, last, width, height, workdir, "last"); err != nil {
return err
}
}
vp.Width, vp.Height = truncateToGrid(width), truncateToGrid(height)
var keep [][]byte
setStr := func(dst *uintptr, s string) {
if s == "" {
return
}
b := cString(s)
keep = append(keep, b)
*dst = uintptr(unsafe.Pointer(&b[0])) // #nosec G103 -- borrowed by C for the call only
}
setStr(&vp.Prompt, opts.GetPrompt())
setStr(&vp.OutputDir, workdir)
setStr(&vp.FirstFrame, first)
setStr(&vp.LastFrame, last)
setStr(&vp.RefImage, extra.refImage)
setStr(&vp.RefVideo, extra.refVideo)
setStr(&vp.RefAudio, opts.GetAudio())
xlog.Info("[vllm-cpp] GenerateVideo", "dst", dst, "workdir", workdir,
"width", vp.Width, "height", vp.Height, "frames", vp.NumFrames,
"steps", vp.Steps, "seeded", vp.HasSeed == 1, "partition", vo.partition)
var out cVideoResult
rc := vllmVideoGenerate(v.videoEngine, unsafe.Pointer(&vp), unsafe.Pointer(&out)) // #nosec G103 -- POD in/out params
runtime.KeepAlive(keep)
if rc != vllmOK {
return fmt.Errorf("vllm-cpp: video generation failed: %s", vllmLastError())
}
defer vllmVideoResultFree(unsafe.Pointer(&out)) // #nosec G103 -- frees the library-owned members
frameDir, audioPath := goString(out.FrameDir), goString(out.AudioPath)
xlog.Info("[vllm-cpp] rendered", "frames", out.FrameCount,
"width", out.Width, "height", out.Height, "fps", out.Fps,
"audio", audioPath, "sampleRate", out.SampleRate)
if opts.GetFps() > 0 && opts.GetFps() != out.Fps {
// Muxing at any other rate desynchronises the jointly generated audio.
xlog.Warn("[vllm-cpp] MiniMax-H3 renders at a fixed frame rate; ignoring the requested fps",
"requested", opts.GetFps(), "rendered", out.Fps)
}
return v.muxVideo(frameDir, audioPath, dst, out.Fps, extra.crf)
}
// muxVideo execs the argv libvllm composed. The encoding contract (h264 /
// yuv420p + AAC, -shortest, +faststart) belongs to the library; only the spawn
// is ours.
func (v *VllmCpp) muxVideo(frameDir, audioPath, dst string, fps, crf int32) error {
mx := cVideoMuxParams{Fps: fps, Crf: crf}
var keep [][]byte
setStr := func(dst *uintptr, s string) {
if s == "" {
return
}
b := cString(s)
keep = append(keep, b)
*dst = uintptr(unsafe.Pointer(&b[0])) // #nosec G103 -- borrowed by C for the call only
}
setStr(&mx.Frames, filepath.Join(frameDir, "frame_%06d.ppm"))
setStr(&mx.AudioPath, audioPath)
setStr(&mx.OutputPath, dst)
var argvPtr uintptr
var argc int32
rc := vllmVideoMuxArgv(unsafe.Pointer(&mx), unsafe.Pointer(&argvPtr), unsafe.Pointer(&argc)) // #nosec G103 -- POD out-params
runtime.KeepAlive(keep)
if rc != vllmOK {
return fmt.Errorf("vllm-cpp: composing the mux command failed: %s", vllmLastError())
}
argv := goStringSlice(argvPtr, argc)
vllmVideoMuxArgvFre(argvPtr, argc)
if len(argv) == 0 {
return fmt.Errorf("vllm-cpp: the library composed an empty mux command")
}
ffmpegBin, err := resolveFfmpeg(v.opts.video.ffmpeg)
if err != nil {
return err
}
argv[0] = ffmpegBin
xlog.Debug("[vllm-cpp] muxing", "argv", argv)
output, err := exec.Command(argv[0], argv[1:]...).CombinedOutput() // #nosec G204 -- argv is composed by libvllm, argv[0] is a resolved binary
if err != nil {
return fmt.Errorf("vllm-cpp: ffmpeg mux failed: %w (output: %s)", err, strings.TrimSpace(string(output)))
}
return nil
}
// resolveFfmpeg locates the mux binary. The backend image is FROM scratch and
// carries no ffmpeg, exactly like vibevoice-cpp's transcode path: the host must
// provide one, and saying so plainly beats a bare "exec: not found" after an
// hours-long render.
func resolveFfmpeg(configured string) (string, error) {
name := configured
if name == "" {
name = "ffmpeg"
}
path, err := exec.LookPath(name)
if err != nil {
return "", fmt.Errorf("vllm-cpp: %q not found: MiniMax-H3 output is muxed with ffmpeg, "+
"install it on the host or point options: [ffmpeg:<path>] at a binary: %w", name, err)
}
return path, nil
}
// videoWorkdir returns the directory the engine writes frame_%06d.ppm and
// audio.wav into, plus its cleanup.
//
// It is ALWAYS a fresh directory. Reusing one would leave a longer previous
// run's trailing frames in place for the mux to pick up, silently splicing two
// renders together. With video_workdir set the run is kept (its frames are what
// ref2va's ref_video consumes); otherwise it is removed once the mux succeeds.
func (v *VllmCpp) videoWorkdir(dst string) (string, func(), error) {
parent := v.opts.video.workdir
keep := parent != ""
if parent == "" {
parent = filepath.Dir(dst)
}
if err := os.MkdirAll(parent, 0o750); err != nil {
return "", nil, fmt.Errorf("vllm-cpp: creating the video work directory: %w", err)
}
dir, err := os.MkdirTemp(parent, "vllm-cpp-h3-")
if err != nil {
return "", nil, fmt.Errorf("vllm-cpp: creating the video work directory: %w", err)
}
if keep {
return dir, func() {}, nil
}
return dir, func() {
if err := os.RemoveAll(dir); err != nil {
xlog.Warn("[vllm-cpp] could not remove the video work directory", "dir", dir, "error", err)
}
}, nil
}
// videoExtraParams holds the per-request knobs that have no proto field.
type videoExtraParams struct {
noiseAug float32
refImage string
refVideo string
crf int32
}
func parseVideoRequestParams(params map[string]string) (videoExtraParams, error) {
var extra videoExtraParams
for k, raw := range params {
v := strings.TrimSpace(raw)
switch k {
case "noise_aug":
f, err := strconv.ParseFloat(v, 32)
if err != nil {
return extra, fmt.Errorf("vllm-cpp: params.noise_aug must be a number, got %q", raw)
}
extra.noiseAug = float32(f)
case "ref_image":
extra.refImage = v
case "ref_video":
extra.refVideo = v
case "crf":
n, err := strconv.ParseInt(v, 10, 32)
if err != nil {
return extra, fmt.Errorf("vllm-cpp: params.crf must be an integer, got %q", raw)
}
extra.crf = int32(n)
default:
return extra, fmt.Errorf("vllm-cpp: unknown params key %q (accepted: %s)",
k, strings.Join(videoRequestParams, ", "))
}
}
return extra, nil
}
// checkPartitionConditioning refuses conditioning the loaded checkpoint cannot
// serve.
//
// This is the failure this backend most needs to catch early. The FL2VA
// partition serves t2va and fl2va; handing it a reference image or audio is a
// partition mismatch, and H3 does not fail cleanly on one - it renders, for
// hours, and returns a coloured lattice over the frame. The engine's own #77
// guard covers a missing declaration; this covers a declaration that does not
// match the request.
func checkPartitionConditioning(partition string, opts *pb.GenerateVideoRequest, extra videoExtraParams) error {
hasKeyframe := opts.GetStartImage() != "" || opts.GetEndImage() != ""
hasReference := extra.refImage != "" || extra.refVideo != "" || opts.GetAudio() != ""
if hasKeyframe && hasReference {
return fmt.Errorf("vllm-cpp: fl2va keyframes (start_image/end_image) and ref2va reference " +
"conditioning (params.ref_image/params.ref_video/audio) are exclusive in the H3 pipeline")
}
switch partition {
case partitionFL2VA:
if hasReference {
return fmt.Errorf("vllm-cpp: the FL2VA checkpoint serves t2va and fl2va only - " +
"reference conditioning (params.ref_image/params.ref_video/audio) needs a ref2va DiT. " +
"Use start_image for first-frame conditioning instead")
}
case partitionRef2VA:
if hasKeyframe {
return fmt.Errorf("vllm-cpp: the Ref2VA checkpoint does not serve fl2va keyframes - " +
"pass the image as params.ref_image, or install the FL2VA checkpoint")
}
}
return nil
}
// resolveCanvas settles the output geometry BEFORE a keyframe is resampled,
// because the two have to agree exactly: the engine refuses a keyframe that is
// not already at the output resolution, and when no geometry is requested it
// derives one from the keyframe's own aspect. Mirrors _resolve_shape
// (src/vllm/model_executor/models/minimax_h3_planner.cpp:264-308).
func resolveCanvas(width, height int32, keyframes ...string) (int32, int32, error) {
if width > 0 && height > 0 {
return width, height, nil
}
for _, k := range keyframes {
if k == "" {
continue
}
w, h, err := imageDimensions(k)
if err != nil {
return 0, 0, err
}
if w <= 0 || h <= 0 {
continue
}
// A 768 short edge, the long edge snapped onto the 32 grid.
if w >= h {
return alignMultiple(float64(h3ShortEdge)*float64(w)/float64(h), h3CanvasMultiple), h3ShortEdge, nil
}
return h3ShortEdge, alignMultiple(float64(h3ShortEdge)*float64(h)/float64(w), h3CanvasMultiple), nil
}
// The shipped canvas.
return 1344, h3ShortEdge, nil
}
// stageKeyframe converts a staged upload into the binary PPM (P6) at exactly
// width x height that the engine requires. libvllm vendors no image codec and
// no resampler, so ffmpeg does both; a P6 already at the canvas passes through
// untouched.
func stageKeyframe(ffmpegPath, src string, width, height int32, workdir, name string) (string, error) {
if src == "" {
return "", nil
}
if w, h, err := ppmDimensions(src); err == nil && w == width && h == height {
return src, nil
}
ffmpegBin, err := resolveFfmpeg(ffmpegPath)
if err != nil {
return "", fmt.Errorf("converting the %s keyframe to PPM: %w", name, err)
}
out := filepath.Join(workdir, name+"_frame.ppm")
// -frames:v 1 because an animated upload (GIF) would otherwise write a
// sequence; -pix_fmt rgb24 is what the image2/ppm muxer needs for P6.
cmd := exec.Command(ffmpegBin, "-y", "-loglevel", "error", "-i", src, // #nosec G204 -- the binary is resolved, the rest are literals and staged paths
"-frames:v", "1",
"-vf", fmt.Sprintf("scale=%d:%d", width, height),
"-pix_fmt", "rgb24", "-f", "image2", out)
if output, err := cmd.CombinedOutput(); err != nil {
return "", fmt.Errorf("vllm-cpp: converting the %s keyframe to PPM failed: %w (output: %s)",
name, err, strings.TrimSpace(string(output)))
}
return out, nil
}
// imageDimensions reads geometry from a staged upload, PPM included (the Go
// standard library has no netpbm decoder).
func imageDimensions(path string) (int32, int32, error) {
if w, h, err := ppmDimensions(path); err == nil {
return w, h, nil
}
f, err := os.Open(path) // #nosec G304 -- a path staged by LocalAI for this request
if err != nil {
return 0, 0, fmt.Errorf("vllm-cpp: reading the keyframe %q: %w", path, err)
}
defer func() { _ = f.Close() }()
cfg, _, err := image.DecodeConfig(f)
if err != nil {
return 0, 0, fmt.Errorf("vllm-cpp: the keyframe %q is not a PNG, JPEG, GIF or binary PPM: %w", path, err)
}
return int32(cfg.Width), int32(cfg.Height), nil
}
// ppmDimensions parses a binary PPM (P6) header: magic, then width, height and
// maxval as ASCII decimals separated by whitespace, with # comments allowed.
func ppmDimensions(path string) (int32, int32, error) {
f, err := os.Open(path) // #nosec G304 -- a path staged by LocalAI for this request
if err != nil {
return 0, 0, err
}
defer func() { _ = f.Close() }()
// A P6 header is a handful of bytes; 512 covers any sane comment run.
buf := make([]byte, 512)
n, err := f.Read(buf)
if n < 2 || (err != nil && n == 0) {
return 0, 0, fmt.Errorf("not a PPM")
}
if buf[0] != 'P' || buf[1] != '6' {
return 0, 0, fmt.Errorf("not a binary PPM (P6)")
}
fields := make([]int32, 0, 2)
for i := 2; i < n && len(fields) < 2; {
switch {
case buf[i] == '#':
for i < n && buf[i] != '\n' {
i++
}
case buf[i] >= '0' && buf[i] <= '9':
value := int32(0)
for i < n && buf[i] >= '0' && buf[i] <= '9' {
value = value*10 + int32(buf[i]-'0')
i++
}
fields = append(fields, value)
default:
i++
}
}
if len(fields) < 2 {
return 0, 0, fmt.Errorf("truncated PPM header")
}
return fields[0], fields[1], nil
}
// alignMultiple mirrors MiniMaxH3AlignMultiple: round-half-to-even onto the
// multiple, floored at one multiple. Half-to-even, not half-away-from-zero,
// because the reference pipeline uses Python's round().
func alignMultiple(value float64, multiple int32) int32 {
snapped := int32(math.RoundToEven(value/float64(multiple))) * multiple
if snapped < multiple {
return multiple
}
return snapped
}
// truncateToGrid mirrors the engine's canvas snap: truncation, not rounding.
func truncateToGrid(v int32) int32 {
if v <= 0 {
return 0
}
return v / h3CanvasMultiple * h3CanvasMultiple
}
// alignFrameCount mirrors MiniMaxH3AlignFrameCount: the next value on the
// 17n+5 grid. Used only to warn - the engine does the real alignment.
func alignFrameCount(frames int32) int32 {
if frames <= 0 {
return frames
}
for frames%h3FrameGrid != h3FrameOffset {
frames++
}
return frames
}
func firstPositive(values ...int32) int32 {
for _, v := range values {
if v > 0 {
return v
}
}
return 0
}
func videoDeviceName(device int32) string {
if device == videoDeviceCUDA {
return "cuda"
}
return "cpu"
}
// siblingConfigJSON is the release layout: each VAE ships its config.json in
// the directory holding its weights.
func siblingConfigJSON(weights string) string {
candidate := filepath.Join(filepath.Dir(weights), "config.json")
if _, err := os.Stat(candidate); err != nil {
return ""
}
return candidate
}
+305
View File
@@ -0,0 +1,305 @@
package main
import (
"os"
"path/filepath"
"unsafe"
pb "github.com/mudler/LocalAI/pkg/grpc/proto"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
// The video PODs carry the same contract as the text ones in vllmcpp_test.go:
// these are the C offsets of vllm.h on LP64, and a drift here is silent memory
// corruption rather than a compile error.
var _ = Describe("C ABI video struct mirrors", func() {
It("cVideoModelParams matches vllm_video_model_params", func() {
var p cVideoModelParams
Expect(unsafe.Offsetof(p.DitPath)).To(Equal(uintptr(0)))
Expect(unsafe.Offsetof(p.EncoderPath)).To(Equal(uintptr(8)))
Expect(unsafe.Offsetof(p.TokenizerPath)).To(Equal(uintptr(16)))
Expect(unsafe.Offsetof(p.VideoVaePath)).To(Equal(uintptr(24)))
Expect(unsafe.Offsetof(p.VideoVaeConfigPath)).To(Equal(uintptr(32)))
Expect(unsafe.Offsetof(p.AudioVaePath)).To(Equal(uintptr(40)))
Expect(unsafe.Offsetof(p.AudioVaeConfigPath)).To(Equal(uintptr(48)))
Expect(unsafe.Offsetof(p.PromptEmbedsPath)).To(Equal(uintptr(56)))
Expect(unsafe.Offsetof(p.Partition)).To(Equal(uintptr(64)))
Expect(unsafe.Offsetof(p.Device)).To(Equal(uintptr(72)))
Expect(unsafe.Offsetof(p.DequantBf16)).To(Equal(uintptr(76)))
Expect(unsafe.Offsetof(p.Fp4Resident)).To(Equal(uintptr(80)))
Expect(unsafe.Offsetof(p.Family)).To(Equal(uintptr(88)))
Expect(unsafe.Offsetof(p.ExtraKeys)).To(Equal(uintptr(96)))
Expect(unsafe.Offsetof(p.ExtraValues)).To(Equal(uintptr(104)))
Expect(unsafe.Offsetof(p.NExtras)).To(Equal(uintptr(112)))
Expect(unsafe.Sizeof(p)).To(Equal(uintptr(120)))
})
It("cVideoParams matches vllm_video_params", func() {
var p cVideoParams
Expect(unsafe.Offsetof(p.Prompt)).To(Equal(uintptr(0)))
Expect(unsafe.Offsetof(p.Width)).To(Equal(uintptr(8)))
Expect(unsafe.Offsetof(p.Height)).To(Equal(uintptr(12)))
Expect(unsafe.Offsetof(p.NumFrames)).To(Equal(uintptr(16)))
Expect(unsafe.Offsetof(p.Steps)).To(Equal(uintptr(20)))
Expect(unsafe.Offsetof(p.Seed)).To(Equal(uintptr(24)))
Expect(unsafe.Offsetof(p.HasSeed)).To(Equal(uintptr(32)))
Expect(unsafe.Offsetof(p.FirstFrame)).To(Equal(uintptr(40)))
Expect(unsafe.Offsetof(p.LastFrame)).To(Equal(uintptr(48)))
Expect(unsafe.Offsetof(p.RefImage)).To(Equal(uintptr(56)))
Expect(unsafe.Offsetof(p.RefVideo)).To(Equal(uintptr(64)))
Expect(unsafe.Offsetof(p.RefAudio)).To(Equal(uintptr(72)))
Expect(unsafe.Offsetof(p.NoiseAug)).To(Equal(uintptr(80)))
Expect(unsafe.Offsetof(p.OutputDir)).To(Equal(uintptr(88)))
Expect(unsafe.Offsetof(p.ExtraKeys)).To(Equal(uintptr(96)))
Expect(unsafe.Offsetof(p.ExtraValues)).To(Equal(uintptr(104)))
Expect(unsafe.Offsetof(p.NExtras)).To(Equal(uintptr(112)))
Expect(unsafe.Sizeof(p)).To(Equal(uintptr(120)))
})
It("cVideoResult matches vllm_video_result", func() {
var r cVideoResult
Expect(unsafe.Offsetof(r.FrameDir)).To(Equal(uintptr(0)))
Expect(unsafe.Offsetof(r.AudioPath)).To(Equal(uintptr(8)))
Expect(unsafe.Offsetof(r.FrameCount)).To(Equal(uintptr(16)))
Expect(unsafe.Offsetof(r.Width)).To(Equal(uintptr(20)))
Expect(unsafe.Offsetof(r.Height)).To(Equal(uintptr(24)))
Expect(unsafe.Offsetof(r.Fps)).To(Equal(uintptr(28)))
Expect(unsafe.Offsetof(r.SampleRate)).To(Equal(uintptr(32)))
Expect(unsafe.Offsetof(r.MuxArgv)).To(Equal(uintptr(40)))
Expect(unsafe.Offsetof(r.MuxArgc)).To(Equal(uintptr(48)))
Expect(unsafe.Sizeof(r)).To(Equal(uintptr(56)))
})
It("cVideoMuxParams matches vllm_video_mux_params", func() {
var p cVideoMuxParams
Expect(unsafe.Offsetof(p.Frames)).To(Equal(uintptr(0)))
Expect(unsafe.Offsetof(p.AudioPath)).To(Equal(uintptr(8)))
Expect(unsafe.Offsetof(p.OutputPath)).To(Equal(uintptr(16)))
Expect(unsafe.Offsetof(p.Fps)).To(Equal(uintptr(24)))
Expect(unsafe.Offsetof(p.Crf)).To(Equal(uintptr(28)))
Expect(unsafe.Sizeof(p)).To(Equal(uintptr(32)))
})
})
var _ = Describe("video load options", func() {
It("stays disengaged for a plain text config", func() {
lo := parseOptions(&pb.ModelOptions{Options: []string{"max_num_seqs:16"}})
Expect(lo.video.engaged()).To(BeFalse())
})
It("reads the H3 checkpoint set from the options list", func() {
lo := parseOptions(&pb.ModelOptions{Options: []string{
"video_encoder:qwen3vl-32B-MiniMax-H3-Q4_K_M.gguf",
"video_tokenizer:tokenizer.json",
"video_vae:vae/diffusion_pytorch_model.safetensors",
"audio_vae:audio_vae/model.safetensors",
"video_partition:fl2va",
"video_device:cuda",
"video_dequant_bf16:true",
"video_width:1344",
"video_height:768",
"video_num_frames:124",
"video_steps:50",
}})
Expect(lo.video.engaged()).To(BeTrue())
Expect(lo.video.encoderPath).To(Equal("qwen3vl-32B-MiniMax-H3-Q4_K_M.gguf"))
Expect(lo.video.tokenizerPath).To(Equal("tokenizer.json"))
Expect(lo.video.videoVaePath).To(Equal("vae/diffusion_pytorch_model.safetensors"))
Expect(lo.video.audioVaePath).To(Equal("audio_vae/model.safetensors"))
Expect(lo.video.partition).To(Equal(partitionFL2VA))
Expect(lo.video.device).To(Equal(videoDeviceCUDA))
Expect(lo.video.deviceSet).To(BeTrue())
Expect(lo.video.dequantBf16).To(Equal(int32(1)))
Expect(lo.video.width).To(Equal(int32(1344)))
Expect(lo.video.height).To(Equal(int32(768)))
Expect(lo.video.numFrames).To(Equal(int32(124)))
Expect(lo.video.steps).To(Equal(int32(50)))
})
It("reads the same keys from engine_args", func() {
lo := parseOptions(&pb.ModelOptions{
EngineArgs: `{"video_vae":"vae/v.safetensors","audio_vae":"a.safetensors","video_num_frames":124,"video_dequant_bf16":true}`,
})
Expect(lo.video.engaged()).To(BeTrue())
Expect(lo.video.videoVaePath).To(Equal("vae/v.safetensors"))
Expect(lo.video.audioVaePath).To(Equal("a.safetensors"))
Expect(lo.video.numFrames).To(Equal(int32(124)))
Expect(lo.video.dequantBf16).To(Equal(int32(1)))
})
It("ignores an unknown video_device rather than guessing", func() {
lo := parseOptions(&pb.ModelOptions{Options: []string{"video_vae:v", "video_device:tpu"}})
Expect(lo.video.deviceSet).To(BeFalse())
Expect(lo.video.device).To(Equal(videoDeviceCPU))
})
})
var _ = Describe("per-request params", func() {
It("maps the accepted keys", func() {
extra, err := parseVideoRequestParams(map[string]string{
"noise_aug": "0.5", "ref_image": "/tmp/ref.ppm", "crf": "20",
})
Expect(err).ToNot(HaveOccurred())
Expect(extra.noiseAug).To(BeNumerically("~", 0.5, 1e-6))
Expect(extra.refImage).To(Equal("/tmp/ref.ppm"))
Expect(extra.crf).To(Equal(int32(20)))
})
It("refuses an unknown key instead of dropping it", func() {
_, err := parseVideoRequestParams(map[string]string{"resolution": "480p"})
Expect(err).To(MatchError(ContainSubstring("unknown params key")))
})
It("refuses a non-numeric noise_aug", func() {
_, err := parseVideoRequestParams(map[string]string{"noise_aug": "high"})
Expect(err).To(HaveOccurred())
})
})
// The partition guard is the correctness rule this backend exists to enforce:
// the FL2VA DiT serves t2va and fl2va, and handing it reference conditioning
// renders a broken lattice over the frame after a multi-hour generation rather
// than failing.
var _ = Describe("partition conditioning guard", func() {
It("accepts a plain t2va request on fl2va", func() {
Expect(checkPartitionConditioning(partitionFL2VA,
&pb.GenerateVideoRequest{Prompt: "a llama"}, videoExtraParams{})).To(Succeed())
})
It("accepts fl2va keyframes on fl2va", func() {
Expect(checkPartitionConditioning(partitionFL2VA,
&pb.GenerateVideoRequest{StartImage: "/tmp/a.png"}, videoExtraParams{})).To(Succeed())
})
It("refuses a reference image on fl2va", func() {
err := checkPartitionConditioning(partitionFL2VA,
&pb.GenerateVideoRequest{}, videoExtraParams{refImage: "/tmp/ref.ppm"})
Expect(err).To(MatchError(ContainSubstring("ref2va")))
})
It("refuses reference audio on fl2va", func() {
err := checkPartitionConditioning(partitionFL2VA,
&pb.GenerateVideoRequest{Audio: "/tmp/voice.wav"}, videoExtraParams{})
Expect(err).To(HaveOccurred())
})
It("refuses fl2va keyframes on ref2va", func() {
err := checkPartitionConditioning(partitionRef2VA,
&pb.GenerateVideoRequest{StartImage: "/tmp/a.png"}, videoExtraParams{})
Expect(err).To(HaveOccurred())
})
It("refuses keyframes and references together on either partition", func() {
err := checkPartitionConditioning(partitionRef2VA,
&pb.GenerateVideoRequest{StartImage: "/tmp/a.png"}, videoExtraParams{refVideo: "/tmp/clip"})
Expect(err).To(MatchError(ContainSubstring("exclusive")))
})
})
var _ = Describe("H3 geometry", func() {
It("keeps an explicitly requested canvas", func() {
w, h, err := resolveCanvas(1280, 720)
Expect(err).ToNot(HaveOccurred())
Expect(w).To(Equal(int32(1280)))
Expect(h).To(Equal(int32(720)))
})
It("falls back to the shipped 1344x768 canvas", func() {
w, h, err := resolveCanvas(0, 0)
Expect(err).ToNot(HaveOccurred())
Expect(w).To(Equal(int32(1344)))
Expect(h).To(Equal(int32(768)))
})
It("derives a landscape canvas from a keyframe's aspect", func() {
path := writePPM(1920, 1080)
w, h, err := resolveCanvas(0, 0, path)
Expect(err).ToNot(HaveOccurred())
Expect(h).To(Equal(int32(768)))
// 768 * 16/9 = 1365.33; /32 = 42.67, round-half-to-even to 43, x32.
Expect(w).To(Equal(int32(1376)))
})
It("derives a portrait canvas from a keyframe's aspect", func() {
path := writePPM(1080, 1920)
w, h, err := resolveCanvas(0, 0, path)
Expect(err).ToNot(HaveOccurred())
Expect(w).To(Equal(int32(768)))
Expect(h).To(Equal(int32(1376)))
})
It("truncates onto the 32 grid the way the engine does", func() {
Expect(truncateToGrid(1000)).To(Equal(int32(992)))
Expect(truncateToGrid(768)).To(Equal(int32(768)))
})
It("reports the 17n+5 frame grid", func() {
Expect(alignFrameCount(124)).To(Equal(int32(124)))
Expect(alignFrameCount(120)).To(Equal(int32(124)))
Expect(alignFrameCount(100)).To(Equal(int32(107)))
})
})
var _ = Describe("keyframe staging", func() {
It("parses a binary PPM header, comments included", func() {
dir := GinkgoT().TempDir()
path := filepath.Join(dir, "commented.ppm")
Expect(os.WriteFile(path, []byte("P6\n# made by a test\n64 32\n255\n"), 0o600)).To(Succeed())
w, h, err := ppmDimensions(path)
Expect(err).ToNot(HaveOccurred())
Expect(w).To(Equal(int32(64)))
Expect(h).To(Equal(int32(32)))
})
It("refuses an ASCII PPM (P3): the engine reads P6 only", func() {
dir := GinkgoT().TempDir()
path := filepath.Join(dir, "ascii.ppm")
Expect(os.WriteFile(path, []byte("P3\n64 32\n255\n"), 0o600)).To(Succeed())
_, _, err := ppmDimensions(path)
Expect(err).To(HaveOccurred())
})
It("passes a P6 already at the canvas straight through, without ffmpeg", func() {
path := writePPM(64, 32)
out, err := stageKeyframe("", path, 64, 32, GinkgoT().TempDir(), "first")
Expect(err).ToNot(HaveOccurred())
Expect(out).To(Equal(path))
})
It("is a no-op for an absent keyframe", func() {
out, err := stageKeyframe("", "", 64, 32, GinkgoT().TempDir(), "first")
Expect(err).ToNot(HaveOccurred())
Expect(out).To(BeEmpty())
})
})
var _ = Describe("GenerateVideo preconditions", func() {
It("refuses when the model is not a video engine", func() {
v := &VllmCpp{}
Expect(v.GenerateVideo(&pb.GenerateVideoRequest{Prompt: "x", Dst: "/tmp/o.mp4"})).
To(MatchError(ContainSubstring("not a MiniMax-H3 video engine")))
})
})
// writePPM writes a valid P6 header of the given geometry. Only the header is
// read by anything under test, so the pixel payload is left off.
func writePPM(width, height int) string {
dir := GinkgoT().TempDir()
path := filepath.Join(dir, "frame.ppm")
header := []byte("P6\n" + itoa(width) + " " + itoa(height) + "\n255\n")
Expect(os.WriteFile(path, header, 0o600)).To(Succeed())
return path
}
func itoa(v int) string {
if v == 0 {
return "0"
}
digits := ""
for v > 0 {
digits = string(rune('0'+v%10)) + digits
v /= 10
}
return digits
}
+13 -6
View File
@@ -16,7 +16,7 @@ func TestVllmCpp(t *testing.T) {
RunSpecs(t, "vllm-cpp suite")
}
// The Go POD mirrors must match the C struct layout of vllm.h (ABI v10)
// The Go POD mirrors must match the C struct layout of vllm.h (ABI v23)
// byte-for-byte: these offsets are the C offsets on LP64 (linux/darwin
// amd64+arm64). A failure here means govllmcpp.go drifted from vllm.h.
var _ = Describe("C ABI struct mirrors", func() {
@@ -24,7 +24,7 @@ var _ = Describe("C ABI struct mirrors", func() {
// VLLM_ABI_VERSION in the vllm.h of VLLM_CPP_VERSION (Makefile).
// Moving the pin past this without growing the mirrors below ships a
// backend that refuses every load at startup (issue #11379).
Expect(abiVersion).To(Equal(10))
Expect(abiVersion).To(Equal(23))
})
It("cModelParams matches vllm_model_params", func() {
@@ -42,10 +42,17 @@ var _ = Describe("C ABI struct mirrors", func() {
Expect(unsafe.Offsetof(p.MaxNumBatchedTokens)).To(Equal(uintptr(60)))
Expect(unsafe.Offsetof(p.SchedulingPolicy)).To(Equal(uintptr(64)))
Expect(unsafe.Offsetof(p.KVTransferConfig)).To(Equal(uintptr(72)))
Expect(unsafe.Offsetof(p.EnableJumpForward)).To(Equal(uintptr(80)))
// 88, not 84: the struct is 8-aligned (it holds pointers), so the
// trailing int32 is padded out. Go pads identically.
Expect(unsafe.Sizeof(p)).To(Equal(uintptr(88)))
Expect(unsafe.Offsetof(p.OffloadConfig)).To(Equal(uintptr(80)))
Expect(unsafe.Offsetof(p.EnableJumpForward)).To(Equal(uintptr(88)))
Expect(unsafe.Offsetof(p.Device)).To(Equal(uintptr(92)))
// 96: gpu_memory_utilization is a double, so it takes the next
// 8-aligned slot after the int32 pair. Go pads identically.
Expect(unsafe.Offsetof(p.GPUMemoryUtil)).To(Equal(uintptr(96)))
Expect(unsafe.Offsetof(p.KVCacheMemoryBytes)).To(Equal(uintptr(104)))
Expect(unsafe.Offsetof(p.LanguageModelOnly)).To(Equal(uintptr(112)))
Expect(unsafe.Offsetof(p.LimitMMPerPrompt)).To(Equal(uintptr(120)))
Expect(unsafe.Offsetof(p.MMProjPath)).To(Equal(uintptr(128)))
Expect(unsafe.Sizeof(p)).To(Equal(uintptr(136)))
})
It("cSamplingParams matches vllm_sampling_params (ABI v8)", func() {
+1 -1
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?=306c88f4d1286aec1bf96e544632897886af5501
WHISPER_CPP_VERSION?=c44b60b8053bbf2a5c1e014f11323fb3f2485177
SO_TARGET?=libgowhisper.so
CMAKE_ARGS+=-DBUILD_SHARED_LIBS=OFF
+14 -2
View File
@@ -11,6 +11,8 @@
- https://github.com/ggerganov/llama.cpp
tags:
- text-to-text
- text-to-speech
- TTS
- LLM
- CPU
- GPU
@@ -508,7 +510,7 @@
default: "cpu-stablediffusion-ggml"
nvidia: "cuda12-stablediffusion-ggml"
intel: "intel-sycl-f16-stablediffusion-ggml"
# amd: "rocm-stablediffusion-ggml"
amd: "rocm-stablediffusion-ggml"
vulkan: "vulkan-stablediffusion-ggml"
nvidia-l4t: "nvidia-l4t-arm64-stablediffusion-ggml"
metal: "metal-stablediffusion-ggml"
@@ -2107,7 +2109,7 @@
default: "cpu-stablediffusion-ggml-development"
nvidia: "cuda12-stablediffusion-ggml-development"
intel: "intel-sycl-f16-stablediffusion-ggml-development"
# amd: "rocm-stablediffusion-ggml-development"
amd: "rocm-stablediffusion-ggml-development"
vulkan: "vulkan-stablediffusion-ggml-development"
nvidia-l4t: "nvidia-l4t-arm64-stablediffusion-ggml-development"
metal: "metal-stablediffusion-ggml-development"
@@ -3902,6 +3904,11 @@
uri: "quay.io/go-skynet/local-ai-backends:latest-gpu-nvidia-cuda-12-stablediffusion-ggml"
mirrors:
- localai/localai-backends:latest-gpu-nvidia-cuda-12-stablediffusion-ggml
- !!merge <<: *stablediffusionggml
name: "rocm-stablediffusion-ggml"
uri: "quay.io/go-skynet/local-ai-backends:latest-gpu-rocm-hipblas-stablediffusion-ggml"
mirrors:
- localai/localai-backends:latest-gpu-rocm-hipblas-stablediffusion-ggml
- !!merge <<: *stablediffusionggml
name: "intel-sycl-f32-stablediffusion-ggml"
uri: "quay.io/go-skynet/local-ai-backends:latest-gpu-intel-sycl-f32-stablediffusion-ggml"
@@ -3915,6 +3922,11 @@
uri: "quay.io/go-skynet/local-ai-backends:master-gpu-nvidia-cuda-12-stablediffusion-ggml"
mirrors:
- localai/localai-backends:master-gpu-nvidia-cuda-12-stablediffusion-ggml
- !!merge <<: *stablediffusionggml
name: "rocm-stablediffusion-ggml-development"
uri: "quay.io/go-skynet/local-ai-backends:master-gpu-rocm-hipblas-stablediffusion-ggml"
mirrors:
- localai/localai-backends:master-gpu-rocm-hipblas-stablediffusion-ggml
- !!merge <<: *stablediffusionggml
name: "intel-sycl-f32-stablediffusion-ggml-development"
uri: "quay.io/go-skynet/local-ai-backends:master-gpu-intel-sycl-f32-stablediffusion-ggml"
+15 -18
View File
@@ -19,6 +19,7 @@ import grpc
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'common'))
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'common'))
from grpc_auth import get_auth_interceptors
from temp_utils import cleanup_paths
import tempfile
@@ -115,11 +116,6 @@ def merge_audio_files(audio_files, output_path, sample_rate):
# Save the merged audio
ta.save(output_path, merged_waveform, sample_rate)
# Clean up temporary files
for audio_file in audio_files:
if os.path.exists(audio_file):
os.remove(audio_file)
_ONE_DAY_IN_SECONDS = 60 * 60 * 24
# If MAX_WORKERS are specified in the environment use it, otherwise default to 1
@@ -226,19 +222,20 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
text_chunks = split_text_at_word_boundary(request.text, max_length=250)
print(f"Splitting text into chunks of 250 characters: {len(text_chunks)}", file=sys.stderr)
# Generate audio for each chunk
temp_audio_files = []
for i, chunk in enumerate(text_chunks):
# Generate audio for this chunk
wav = self.model.generate(chunk, **kwargs)
# Create temporary file for this chunk
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.wav')
temp_file.close()
ta.save(temp_file.name, wav, self.model.sr)
temp_audio_files.append(temp_file.name)
# Merge all audio files
merge_audio_files(temp_audio_files, request.dst, self.model.sr)
with cleanup_paths() as temp_audio_files:
for i, chunk in enumerate(text_chunks):
# Generate audio for this chunk
wav = self.model.generate(chunk, **kwargs)
# Register ownership before saving so a partial write is
# removed too when generation or encoding fails.
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.wav')
temp_file.close()
temp_audio_files.append(temp_file.name)
ta.save(temp_file.name, wav, self.model.sr)
# Merge all audio files
merge_audio_files(temp_audio_files, request.dst, self.model.sr)
else:
# Generate audio using ChatterboxTTS for short text
wav = self.model.generate(request.text, **kwargs)
+40
View File
@@ -37,6 +37,46 @@ def parse_options(options_list):
return opts
def attach_media_parts(messages_dicts, n_images=0, n_videos=0):
"""Rebuild the last user message as content *parts* carrying media markers.
Backends that let the tokenizer do the templating hand plain string content
to ``apply_chat_template``, but a chat template only emits the model's own
media tokens (``<|vision_start|><|image_pad|><|vision_end|>`` for the
Qwen-VL family, and the equivalents elsewhere) when the content is a list
of parts. Without those markers the engine's multimodal processor finds
nothing to substitute and silently discards the pixels, even though they
were forwarded correctly out of band.
Returns a new list whose last user message has
``[{"type": "image"} * n_images, {"type": "video"} * n_videos, text]`` as
its content, or ``None`` when there is nothing to attach - no media, no
user turn, or content that is already a list of parts - so the caller can
keep using the original string-content list.
"""
if not n_images and not n_videos:
return None
idx = next(
(
i
for i in reversed(range(len(messages_dicts)))
if messages_dicts[i].get("role") == "user"
),
None,
)
if idx is None:
return None
text = messages_dicts[idx].get("content") or ""
if not isinstance(text, str):
return None
parts = [{"type": "image"}] * n_images + [{"type": "video"}] * n_videos
if text:
parts.append({"type": "text", "text": text})
patched = list(messages_dicts)
patched[idx] = dict(patched[idx], content=parts)
return patched
def messages_to_dicts(proto_messages):
"""Convert proto ``Message`` objects to dicts suitable for ``apply_chat_template``.
+59 -1
View File
@@ -14,7 +14,7 @@ import json
import types
import unittest
from python_utils import messages_to_dicts, parse_options
from python_utils import attach_media_parts, messages_to_dicts, parse_options
def _msg(**fields):
@@ -118,5 +118,63 @@ class TestMessagesToDicts(unittest.TestCase):
self.assertNotIn("tool_calls", out[0])
class TestAttachMediaParts(unittest.TestCase):
def test_image_marker_added_to_last_user_turn(self):
messages = [
{"role": "system", "content": "be brief"},
{"role": "user", "content": "first"},
{"role": "assistant", "content": "ok"},
{"role": "user", "content": "how high is the water?"},
]
out = attach_media_parts(messages, n_images=1)
self.assertEqual(
out[3]["content"],
[{"type": "image"}, {"type": "text", "text": "how high is the water?"}],
)
# Earlier turns and the input list itself are untouched.
self.assertEqual(out[:3], messages[:3])
self.assertEqual(messages[3]["content"], "how high is the water?")
def test_counts_and_order_images_then_videos(self):
out = attach_media_parts(
[{"role": "user", "content": "describe"}], n_images=2, n_videos=1
)
self.assertEqual(
out[0]["content"],
[
{"type": "image"},
{"type": "image"},
{"type": "video"},
{"type": "text", "text": "describe"},
],
)
def test_empty_text_yields_media_only_parts(self):
out = attach_media_parts([{"role": "user", "content": ""}], n_images=1)
self.assertEqual(out[0]["content"], [{"type": "image"}])
def test_other_message_keys_are_preserved(self):
out = attach_media_parts(
[{"role": "user", "content": "hi", "name": "bob"}], n_images=1
)
self.assertEqual(out[0]["name"], "bob")
def test_no_media_is_a_no_op(self):
self.assertIsNone(attach_media_parts([{"role": "user", "content": "hi"}]))
def test_no_user_turn_is_a_no_op(self):
self.assertIsNone(
attach_media_parts([{"role": "system", "content": "hi"}], n_images=1)
)
def test_content_already_parts_is_a_no_op(self):
self.assertIsNone(
attach_media_parts(
[{"role": "user", "content": [{"type": "text", "text": "hi"}]}],
n_images=1,
)
)
if __name__ == "__main__":
unittest.main()
+36
View File
@@ -0,0 +1,36 @@
import base64
import contextlib
import os
import tempfile
@contextlib.contextmanager
def materialize_base64(data, suffix=""):
"""Materialize base64 data for a path-only library and always remove it."""
descriptor, path = tempfile.mkstemp(prefix="localai-media-", suffix=suffix)
try:
with os.fdopen(descriptor, "wb") as output:
descriptor = None
output.write(base64.b64decode(data))
yield path
finally:
if descriptor is not None:
os.close(descriptor)
try:
os.remove(path)
except OSError:
pass
@contextlib.contextmanager
def cleanup_paths():
"""Collect temporary paths and remove them on success or failure."""
paths = []
try:
yield paths
finally:
for path in paths:
try:
os.remove(path)
except OSError:
pass
+41
View File
@@ -0,0 +1,41 @@
import os
import tempfile
import unittest
from unittest import mock
from temp_utils import cleanup_paths, materialize_base64
class MaterializeBase64Test(unittest.TestCase):
def test_removes_materialized_file_after_success(self):
with tempfile.TemporaryDirectory() as directory:
with mock.patch.object(tempfile, "tempdir", directory):
with materialize_base64("aGVsbG8=", suffix=".data") as path:
with open(path, "rb") as materialized:
self.assertEqual(materialized.read(), b"hello")
self.assertFalse(os.path.exists(path))
def test_removes_materialized_file_when_consumer_fails(self):
with tempfile.TemporaryDirectory() as directory:
with mock.patch.object(tempfile, "tempdir", directory):
with self.assertRaisesRegex(RuntimeError, "decode failed"):
with materialize_base64("aGVsbG8="):
raise RuntimeError("decode failed")
self.assertEqual(os.listdir(directory), [])
class CleanupPathsTest(unittest.TestCase):
def test_removes_every_registered_path_after_failure(self):
with tempfile.TemporaryDirectory() as directory:
paths = [os.path.join(directory, name) for name in ("one.wav", "two.wav")]
with self.assertRaisesRegex(RuntimeError, "merge failed"):
with cleanup_paths() as registered:
for path in paths:
open(path, "wb").close()
registered.append(path)
raise RuntimeError("merge failed")
self.assertEqual(os.listdir(directory), [])
if __name__ == "__main__":
unittest.main()
@@ -1,4 +1,4 @@
--extra-index-url https://download.pytorch.org/whl/xpu
torch==2.13.0+xpu
torch==2.14.0+xpu
oneccl_bind_pt==2.8.0+xpu
optimum[openvino]
@@ -1,3 +1,3 @@
grpcio==1.82.1
grpcio==1.83.1
protobuf
grpcio-tools
+2 -2
View File
@@ -1,4 +1,4 @@
grpcio==1.83.0
grpcio==1.83.1
protobuf
certifi
packaging==26.2
packaging==26.3
+25 -9
View File
@@ -122,6 +122,21 @@ from diffusers.schedulers import (
UniPCMultistepScheduler,
)
def select_device(request_cuda, device_option, cuda_available, xpu, mps_available):
"""Pick the pipeline device. An explicit `device:` model option wins;
otherwise CUDA is used whenever torch reports it available (ROCm
builds included) or the model config forces it with `cuda: true`,
keeping the pre-existing XPU/MPS overrides. CPU is the fallback, not
the default."""
if device_option:
return device_option
device = "cuda" if (request_cuda or cuda_available) else "cpu"
if xpu:
device = "xpu"
if mps_available:
device = "mps"
return device
def is_float(s):
"""Check if a string can be converted to float."""
try:
@@ -627,12 +642,13 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
# modify LoraAdapter to be relative to modelFileBase
request.LoraAdapter = os.path.join(request.ModelPath, request.LoraAdapter)
device = "cpu" if not request.CUDA else "cuda"
if XPU:
device = "xpu"
mps_available = hasattr(torch.backends, "mps") and torch.backends.mps.is_available()
if mps_available:
device = "mps"
device = select_device(
request.CUDA,
self.options.pop("device", None),
torch.cuda.is_available(),
XPU,
hasattr(torch.backends, "mps") and torch.backends.mps.is_available(),
)
self.device = device
if request.LoraAdapter:
# Check if its a local file and not a directory ( we load lora differently for a safetensor file )
@@ -800,12 +816,12 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
image = image.resize((1024, 576))
generator = torch.manual_seed(request.seed)
frames = self.pipe(image, guidance_scale=self.cfg_scale, decode_chunk_size=CHUNK_SIZE, generator=generator).frames[0]
frames = self.pipe(image=image, guidance_scale=self.cfg_scale, decode_chunk_size=CHUNK_SIZE, generator=generator).frames[0]
export_to_video(frames, request.dst, fps=FPS)
return backend_pb2.Result(message="Media generated successfully", success=True)
if self.txt2vid:
video_frames = self.pipe(prompt, guidance_scale=self.cfg_scale, num_inference_steps=steps, num_frames=int(FRAMES)).frames
video_frames = self.pipe(prompt=prompt, guidance_scale=self.cfg_scale, num_inference_steps=steps, num_frames=int(FRAMES)).frames
export_to_video(video_frames, request.dst)
return backend_pb2.Result(message="Media generated successfully", success=True)
@@ -868,7 +884,7 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
else:
# pass the kwargs dictionary to the self.pipe method
image = self.pipe(
prompt,
prompt=prompt,
guidance_scale=self.cfg_scale,
**kwargs
).images[0]
+72
View File
@@ -7,6 +7,7 @@ import time
from unittest.mock import patch, MagicMock
# Import dynamic loader for testing (these don't need gRPC)
import backend
import diffusers_dynamic_loader as loader
from diffusers import DiffusionPipeline, StableDiffusionPipeline
@@ -373,3 +374,74 @@ class TestGenerateImageOptionsKwargsMerge(unittest.TestCase):
finally:
os.unlink(src_file.name)
os.unlink(dst_file.name)
def test_text_to_image_prompt_is_passed_by_keyword(self):
"""Test compatibility with pipelines that take image before prompt."""
import os
import tempfile
from PIL import Image
from backend import BackendServicer
class Flux2CompatiblePipeline:
"""Model the FLUX.2 call signature: image is before prompt."""
def __call__(self, image=None, prompt=None, **kwargs):
if prompt is None:
raise ValueError("prompt was not passed by keyword")
self.prompt = prompt
self.kwargs = kwargs
return MagicMock(images=[Image.new("RGB", (4, 4))])
pipeline = Flux2CompatiblePipeline()
svc = BackendServicer.__new__(BackendServicer)
svc.pipe = pipeline
svc.cfg_scale = 7.5
svc.controlnet = None
svc.img2vid = False
svc.txt2vid = False
svc.clip_skip = 0
svc.PipelineType = "Flux2KleinPipeline"
svc.options = {}
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as dst_file:
dst_path = dst_file.name
try:
request = MagicMock()
request.positive_prompt = "a red apple on a wooden table"
request.negative_prompt = ""
request.step = 4
request.seed = 0
request.width = 0
request.height = 0
request.src = ""
request.ref_images = []
request.dst = dst_path
svc.GenerateImage(request, context=None)
self.assertEqual(pipeline.prompt, request.positive_prompt)
self.assertEqual(pipeline.kwargs["num_inference_steps"], 4)
finally:
os.unlink(dst_path)
class TestDeviceSelection(unittest.TestCase):
"""Unit tests for backend.select_device (no GPU required)."""
def test_autodetect_cuda(self):
self.assertEqual(backend.select_device(False, None, True, False, False), "cuda")
def test_cpu_fallback(self):
self.assertEqual(backend.select_device(False, None, False, False, False), "cpu")
def test_forced_cuda(self):
self.assertEqual(backend.select_device(True, None, False, False, False), "cuda")
def test_device_option_wins(self):
self.assertEqual(backend.select_device(True, "cpu", True, True, True), "cpu")
def test_mps_overrides(self):
self.assertEqual(backend.select_device(False, None, True, False, True), "mps")
+5 -2
View File
@@ -1,7 +1,10 @@
.PHONY: fish-speech
fish-speech:
.PHONY: fish-speech test-source-preparation
fish-speech: test-source-preparation
bash install.sh
test-source-preparation:
bash prepare-source_test.sh
.PHONY: run
run: fish-speech
@echo "Running fish-speech..."
+4 -4
View File
@@ -39,10 +39,10 @@ else
cd "${FISH_SPEECH_DIR}" && git pull && cd -
fi
# Remove pyaudio from fish-speech deps — it's only used by the upstream client tool
# (tools/api_client.py) for speaker playback, not by our gRPC backend server.
# It requires native portaudio libs which aren't available on all build environments.
sed -i.bak '/"pyaudio"/d' "${FISH_SPEECH_DIR}/pyproject.toml"
# Keep the platform-specific PyTorch installed above. Upstream pins the generic
# PyPI torch wheel, which replaces ROCm builds with a CUDA wheel during the
# editable install. pyaudio is only used by the upstream playback client.
bash "${backend_dir}/prepare-source.sh" "${BUILD_TYPE:-}" "${FISH_SPEECH_DIR}/pyproject.toml"
# Install fish-speech deps from source (without the package itself since we use PYTHONPATH)
ensureVenv
+18
View File
@@ -0,0 +1,18 @@
#!/bin/bash
set -euo pipefail
build_type=${1:-}
pyproject=${2:?usage: prepare-source.sh BUILD_TYPE PYPROJECT}
prepared=$(mktemp "${pyproject}.XXXXXX")
trap 'rm -f "$prepared"' EXIT
awk -v build_type="$build_type" '
/^dependencies = \[$/ { in_project_dependencies = 1 }
build_type == "hipblas" && in_project_dependencies && /^[[:space:]]*"(torch|torchaudio)[^"]*",?[[:space:]]*$/ { next }
in_project_dependencies && /^[[:space:]]*"pyaudio",?[[:space:]]*$/ { next }
{ print }
in_project_dependencies && /^\]$/ { in_project_dependencies = 0 }
' "$pyproject" > "$prepared"
mv "$prepared" "$pyproject"
trap - EXIT
+66
View File
@@ -0,0 +1,66 @@
#!/bin/bash
set -euo pipefail
SCRIPT_DIR=$(dirname "$(realpath "$0")")
WORK_DIR=$(mktemp -d)
trap 'rm -rf "$WORK_DIR"' EXIT
write_fixture() {
cat > "$1" <<'EOF'
[project]
dependencies = [
"numpy",
"torch==2.8.0",
"torchaudio==2.8.0",
"pyaudio",
]
[project.optional-dependencies]
stable = [
"torch==2.8.0",
"torchaudio",
]
EOF
}
write_fixture "$WORK_DIR/rocm.toml"
write_fixture "$WORK_DIR/cuda.toml"
write_fixture "$WORK_DIR/cpu.toml"
bash "$SCRIPT_DIR/prepare-source.sh" hipblas "$WORK_DIR/rocm.toml"
bash "$SCRIPT_DIR/prepare-source.sh" cublas "$WORK_DIR/cuda.toml"
bash "$SCRIPT_DIR/prepare-source.sh" "" "$WORK_DIR/cpu.toml"
cat > "$WORK_DIR/expected-rocm.toml" <<'EOF'
[project]
dependencies = [
"numpy",
]
[project.optional-dependencies]
stable = [
"torch==2.8.0",
"torchaudio",
]
EOF
cat > "$WORK_DIR/expected-default.toml" <<'EOF'
[project]
dependencies = [
"numpy",
"torch==2.8.0",
"torchaudio==2.8.0",
]
[project.optional-dependencies]
stable = [
"torch==2.8.0",
"torchaudio",
]
EOF
diff -u "$WORK_DIR/expected-rocm.toml" "$WORK_DIR/rocm.toml"
diff -u "$WORK_DIR/expected-default.toml" "$WORK_DIR/cuda.toml"
diff -u "$WORK_DIR/expected-default.toml" "$WORK_DIR/cpu.toml"
echo "PASS: source preparation preserves each platform's PyTorch dependencies"
+1
View File
@@ -8,4 +8,5 @@ else
source $backend_dir/../common/libbackend.sh
fi
bash "${backend_dir}/prepare-source_test.sh"
runUnittests
+4 -1
View File
@@ -127,7 +127,10 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
context.set_code(grpc.StatusCode.NOT_FOUND)
context.set_details("no face detected")
return backend_pb2.EmbeddingResult()
return backend_pb2.EmbeddingResult(embeddings=[float(x) for x in vec])
return backend_pb2.EmbeddingResult(
embeddings=[float(x) for x in vec],
layout=backend_pb2.EMBEDDING_LAYOUT_FINAL,
)
def Detect(self, request, context):
if self.engine is None:
Loaded 100 of 702 files, more files were not shown because too many files have changed in this diff. Show more