* fix(ci): capture and surface ginkgo errors in run-coverage.sh
The coverage script ran ginkgo per test root with `|| fail=1`, which
swallowed the exit code. Combined with `make --output-sync=target`,
which buffers all output and flushes at the end, the actual ginkgo
error was invisible in interleaved CI logs. Every test suite passed
but the script still exited 1, and the error message was buried in
22 000 lines of buffered output.
Each ginkgo invocation now captures combined stdout+stderr to a
per-root log file. On failure the last 80 lines are printed to
stderr so the error is visible in the CI log. On success the log is
replayed to stdout so test output remains visible.
The build flags (--tags, --coverpkg) are quoted correctly so values
containing spaces (e.g. --tags="debug auth") are preserved through
eval.
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* fix(ci): create output dir before writing ginkgo logs
The previous commit added per-root log files but forgot to create
the output directory before the first ginkgo invocation. On CI the
coverage/ dir did not exist yet, so the shell redirect to
cover-pkg.out.log failed with "No such file or directory" before
ginkgo even ran.
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* test(core/http): add canvas preview route to auth coverage allowlist
Commit 9ad78381d8 added GET /api/canvas/preview/:id as a public route
(no auth — the random id is the capability, an iframe cannot send API
key headers) but did not add it to expectedPublicRoutes in the route
auth coverage test. The test flagged the route as an anonymous-access
leak, failing the entire ./core ginkgo suite on every CI run since.
The POST /api/canvas/preview endpoint is auth-gated (returns 401 to
anonymous requests) and is intentionally absent from the allowlist.
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* fix(backends): prefer installed development builds
Prefer compatible runnable development builds for generic aliases when
the preference is enabled. Keep concrete names and disabled selection
unchanged.
Pass worker-local configuration into resolver state before registration.
Synchronize saved frontend settings at startup and require a restart for
resolver changes instead of switching registered paths during runtime.
Add selection, root-shadowing, configuration, and restart-boundary tests.
Document fallback behavior and worker-local activation.
Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* fix(settings): isolate backend restart notices
A development preference save must not restart the watchdog. Separate
its server restart notification from the legacy reconfiguration signal.
Preserve existing response fields and genuine watchdog updates.
Exercise the real settings endpoint for both preference values, repeated
saves, and mixed watchdog updates. Verify resolver state and registered
paths remain unchanged until startup.
Attribution correction for commit
85c46d2fac:
its Assisted-by: Codex:gpt-5 trailer is incorrect. The actual agent was
nib using model gpt-6.1-sol. Preserve that commit as task history.
Assisted-by: nib:gpt-6.1-sol
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>
* feat(schema): add Conversation schema for chat history (#9432)
Persist WebUI chat conversations server-side so browser refresh, private
windows, or device changes preserve user history (issue #9432). This
commit adds the on-disk representation:
- Conversation holds id, name, model, opaque history, MCP / sampling
settings, and timestamps.
- ConversationsFile is the per-user list serialised to JSON.
History is stored as json.RawMessage so the server stays agnostic to
React message shapes (user / assistant / thinking / tool_call /
tool_result mixed with text / image_url / audio_url / file attachments).
Assisted-by: Claude:claude-opus-4-7
Signed-off-by: TLoE419 <tloemizuchizu@gmail.com>
* feat(chathistory): add file-based per-user conversation store (#9432)
File-backed persister for chat history. Each user's conversations live
under {baseDir}/{userID}/conversations.json (anonymous/ when auth is
disabled).
Design choices:
- In-memory cache backed by sync.Mutex so concurrent saves don't
interleave writes.
- Atomic write via tmp file + os.Rename so a crash mid-save never
leaves a corrupted history.
- ID validation regex blocks path-traversal payloads at the store
boundary, in addition to the auth context constraint.
Tests cover round-trip persistence across instances, user isolation,
unsafe-ID rejection, bulk replace migration, and the anonymous fallback
path.
Assisted-by: Claude:claude-opus-4-7
Signed-off-by: TLoE419 <tloemizuchizu@gmail.com>
* feat(api): expose chat conversation CRUD endpoints (#9432)
Wire the chathistory store into the HTTP layer. New endpoints under
/api/conversations:
- GET /api/conversations list
- POST /api/conversations upsert (id in body)
- DELETE /api/conversations delete all
- PUT /api/conversations/bulk replace entire set (localStorage migration)
- GET /api/conversations/:id fetch one
- PUT /api/conversations/:id upsert (id in path; path id wins over body)
- DELETE /api/conversations/:id delete one
All endpoints scope queries to the authenticated user via getUserID(c)
- callers cannot impersonate other users by passing a user_id in the
body. The endpoints are gated behind the new chat_history feature
permission (default ON in APIFeatures), registered through the existing
RouteFeatureRegistry so the unified feature middleware picks them up
automatically.
Application.ChatHistoryStore() returns nil when DisableWebUI is set or
no persistence path is configured, in which case route registration is
skipped entirely rather than registering handlers that always 503.
Also adds the chat-history instruction entry and Swagger tag so the
endpoint surfaces in /api/instructions and /swagger.
Assisted-by: Claude:claude-opus-4-7
Signed-off-by: TLoE419 <tloemizuchizu@gmail.com>
* feat(react-ui): sync chat history to server with localStorage fallback (#9432)
useChat now probes /api/conversations on mount. When the endpoint
responds 200, the server becomes the authoritative source: the hook
merges remote conversations into local state and pushes per-chat PUT
updates on each debounced save. When the endpoint 404s - older LocalAI
deploys or the feature disabled - the hook silently keeps the old
localStorage-only behaviour, so the change is backward-compatible.
Migration: when the server is reachable but empty and the browser has
local conversations with history, the hook fires a single
PUT /api/conversations/bulk to seed the server. The atomic bulk endpoint
avoids partial-upload states where a retry would skip already-uploaded
entries.
Delete operations propagate to the server when it's available; failures
are silently swallowed so the local UI stays responsive on transient
network errors.
Assisted-by: Claude:claude-opus-4-7
Signed-off-by: TLoE419 <tloemizuchizu@gmail.com>
* test(chathistory): rewrite tests in Ginkgo/Gomega
The initial test file used the stdlib testing package (t.Fatalf /
t.Errorf), which is forbidden by the forbidigo linter rule in
.golangci.yml — LocalAI mandates Ginkgo/Gomega for all Go tests (see
.agents/coding-style.md).
Restructured the same six scenarios into Describe / Context / It blocks,
with a chathistory_suite_test.go bootstrap that registers the Ginkgo
fail handler. Test coverage is unchanged: basic CRUD round-trip, cross-
instance persistence, user isolation, unsafe ID rejection (now via
DescribeTable), bulk replace overwrite, and the anonymous fallback
path.
Assisted-by: Claude:claude-opus-4-7
Signed-off-by: TLoE419 <tloemizuchizu@gmail.com>
* refactor(chathistory): switch store to GORM-backed authDB
Addresses mudler's review on #9902. Chat history was the only per-user
state in LocalAI sitting in a separate filesystem store instead of the
shared GORM database — AgentStore and JobStore both live there. Move
it onto Application.authDB so per-user state stays in one place.
* ConversationRecord (GORM model) with composite primary key
(user_id, conv_id) — React mints IDs locally with no global
coordination so per-user uniqueness is the natural shape.
* Store.New(db *gorm.DB) replaces Store.New(baseDir string).
Save / Get / List / Delete / DeleteAll / ReplaceAll keep their
signatures so HTTP handlers and the React UI are untouched.
* ReplaceAll runs in a transaction with an Unscoped() pre-clear so a
future retention sweep cannot resurrect a stale ID on re-upload.
* gorm.DeletedAt in the schema means a future retention pruner is a
one-line query — addresses the second half of the review (unbounded
in-memory map). The map is gone: reads go straight to the DB.
Trade-off worth flagging in the PR reply: chat history persistence
now requires auth.Enabled = true (that's what initialises
Application.authDB). With auth disabled the React UI transparently
falls back to localStorage, the same path the original PR took on
404. Consistent with AgentStore / JobStore; if a "chat history
always on" mode is preferred we can pull DB init out of auth.InitDB
into a shared helper.
Assisted-by: Claude:claude-opus-4-7 [Read Edit Bash]
Signed-off-by: TLoE419 <tloemizuchizu@gmail.com>
* refactor(schema): drop now-unused ConversationsFile envelope
ConversationsFile was the JSON wrapper for the old file-based chat
history store — a {Conversations: [...], UpdatedAt: ...} pair
serialised to disk under {baseDir}/{userID}/conversations.json. The
previous commit replaced that store with a GORM-backed one that
writes rows directly as ConversationRecord, leaving the envelope
without a consumer.
Assisted-by: Claude:claude-opus-4-7 [Read Edit Bash]
Signed-off-by: TLoE419 <tloemizuchizu@gmail.com>
* test(chathistory): switch tests to testutil.SetupTestDB
Mirror the AgentStore test setup: spin up a PostgreSQL 16 container
via testutil.SetupTestDB(), construct a Store on top of it, exercise
CRUD round-trips. The previous file-based fixtures (TempDir,
filepath.Join checks) are no longer meaningful now that the store
talks GORM.
Tests are skipped on macOS (testcontainers needs Docker) and run on
CI, same as the other GORM-backed suites in the repo.
Also reframed two cases:
* "unsafe IDs" → "malformed IDs" — the DB-backed store doesn't care
about path traversal, but idRegex still rejects whitespace /
control characters so IDs stay safe in logs and HTTP responses.
* "anonymous user" no longer pins a directory path (no directory
exists anymore); instead it checks the round-trip plus the
per-user isolation guarantee.
Added a DeleteAll round-trip to cover the bulk-clear endpoint.
Assisted-by: Claude:claude-opus-4-7 [Read Edit Bash]
Signed-off-by: TLoE419 <tloemizuchizu@gmail.com>
* test(chathistory): cover the conversation HTTP handlers
The seven /api/conversations handlers had no tests. Inject the
*chathistory.Store into the handlers instead of the whole Application and
move the route table into RegisterChatHistoryRoutes, so a spec can drive
the real registration and middleware chain (auth.Middleware plus the
chat_history feature gate) against an in-memory SQLite auth database.
The new specs cover 401 without credentials on every route, 403 when an
admin revoked chat_history, the CRUD round trip, path id over body id,
400 on malformed ids and bodies, per-user isolation, bulk replace and
delete-all, and that a nil store registers nothing (the UI then falls
back to localStorage).
The handlers' nil-store branches are gone: the routes are only
registered with a store, so those branches could never run.
Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Stefan Walcz <stefan.walcz@walcz.de>
* fix(chathistory): make deletions final with tombstones and versions
A conversation deleted on one device came back as soon as a client with
a stale copy wrote it again: Delete soft-deleted the row, and the next
Save went through GORM's upsert, which cleared deleted_at and resurrected
it for every device. The lookup that recovered CreatedAt also ran outside
any transaction, so concurrent writers raced on it.
The server is now authoritative over deletions:
* Delete turns the row into a tombstone: content dropped, deleted_at
set. It is idempotent, and deleting an id the server has not seen yet
still records a tombstone, so a delete that overtakes the UI's
in-flight debounced save of the same chat still wins.
* Save runs in a transaction, refuses a tombstoned id with ErrDeleted
(HTTP 410 Gone), and bumps the version with a single
UPDATE ... SET version = version + 1 guarded by deleted_at IS NULL,
so concurrent writers each count and a delete committed between the
lookup and the write still wins. Two concurrent first writes of the
same id retry once (ON CONFLICT DO NOTHING, then update).
* Every row carries a server-assigned version (1 on the first save,
+1 per write including the delete), returned to the client. A non-zero
version tells the UI the server has seen a conversation, which is how
it can tell a never-synced draft from a server-side deletion.
* GET /api/conversations returns the tombstones under "deleted".
* DeleteAll and ReplaceAll leave tombstones too. ReplaceAll no longer
hard-deletes everything first; it upserts the payload, skips ids that
were deleted (reported in "skipped"), and tombstones the rest.
* createdAt/updatedAt are stored as the client sends them (only zero
values are stamped), so clients compare updatedAt from their own
clocks rather than against the server's.
The store specs now run against PostgreSQL (testcontainers) and, with
the auth tag, SQLite through auth.InitDB, since both back the auth
database. The API specs cover a stale client reconnecting after a remote
deletion. Swagger is regenerated with swag v1.16.6 (json.RawMessage
fields get swaggertype), and the feature is documented, including that
it needs user auth.
Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Stefan Walcz <stefan.walcz@walcz.de>
* fix(react-ui): let the server decide which chats were deleted
mergeRemoteAndLocal kept every chat that was only in the browser and
let the server copy win for every shared id. A stale browser therefore
uploaded chats deleted on another device as if they were new drafts,
and a message sent while the list request was still in flight was
replaced by the older server copy and then lost from localStorage too.
The first load now reconciles with the server's tombstones and versions:
* a chat the server lists under "deleted" is dropped locally;
* a chat on both sides keeps the copy with the newer updatedAt, so an
edit made during the load wins and is uploaded afterwards;
* a chat only in the browser is uploaded only if it was never synced
(no version). With a version the server has seen it and no longer
has it, so it is dropped instead of resurrected (this also keeps one
account's cached chats out of another account on a shared browser);
* deletes made while the list is loading are queued and sent once it
arrives.
Saves record the version the server returns, and a 410 on save drops
the chat locally instead of retrying it forever. Chats received from
the server count as already sent, so adopting a server copy no longer
echoes it straight back. The bulk localStorage migration is gone: it
replaced the whole server set from a possibly stale browser, and the
per-chat saves already upload never-synced drafts through the tombstone
check.
The Playwright spec drives each case against a stubbed
/api/conversations that follows the server contract.
Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Stefan Walcz <stefan.walcz@walcz.de>
* fix(chathistory): keep the LocalAI Assistant toggle on synced chats
master's createNewChat sets localaiAssistant, but serializeChat did not
send it and schema.Conversation had no field for it, so a chat with the
Assistant mode on came back with it off after a reload from the server
(and the local cache dropped it too). Add the field on both sides. The
other fields createNewChat sets were already covered; a handler spec now
round-trips all of them, and the UI spec checks the toggle on the way up
and in the local cache.
Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Stefan Walcz <stefan.walcz@walcz.de>
* fix(react-ui): scope chat cache per account, retry unconfirmed deletes
Two gaps in the client half of server-side chat history, both found in
review:
The browser cache was one shared localStorage key. A draft that one
account could not upload (a failed first save) stayed in it, and after
that account signed out and another signed in, the reconcile kept the
unversioned draft and the save pass uploaded it into the second
account. The cache now lives under localai_chats_data:<user id>, Chat
remounts its state when the account changes, and nothing is synced
without a signed-in account. The shared key is left for browser-only
use without auth and is not uploaded when signed in, because nothing
tells which account it belongs to.
Deletes were dropped locally and their DELETE errors swallowed; only
deletes made while the first list was loading were queued, and not
persisted. A delete made while the server was unreachable therefore
came back with the next list. Deletes (single, delete all, and deletes
on the Home page) are now kept as pending deletes in the account's
cache until the server confirms them, override the server list in the
reconcile, and are sent again on the next change, on the online event
and on the next load. While a delete-all is pending no conversation is
uploaded, so a chat started after it is not caught by it.
The Playwright specs replay both review scenarios (account switch after
a failed first save; delete and delete-all while offline) with
counter-checks that the same account keeps and uploads its own draft
and that a confirmed delete is neither re-sent nor resurrected.
Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Stefan Walcz <stefan.walcz@walcz.de>
* fix(react-ui): persist cleared chats immediately
Navigation cancels the debounced cache write and can restore unsynced
chats after Clear all. Save the cleared list with its pending deletion
before returning, and cover immediate navigation in the browser spec.
Assisted-by: Codex:GPT-6
Signed-off-by: Stefan Walcz <stefan.walcz@walcz.de>
* test(react-ui): make the chat history sync specs deterministic
- "clear all persists before navigation" opened a single chat, and the menu
offers no Clear all for one chat, so the spec timed out in every run. It
now opens two unsynced chats and checks that neither survives. Reverting
03cd9c115's save in clearAllChats turns it red again.
- "a delete the server confirmed is not sent again" reopened the page as
soon as the server had the request, before the page had the answer; the
unconfirmed delete was then rightly sent again (4 of 5 runs). It now waits
until the cache holds no pending delete.
Docs-Not-Needed: test-only change.
Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Stefan Walcz <stefan.walcz@walcz.de>
* fix(chathistory): keep the chat's reasoning effort on the server
reasoningEffort (#12700, #12716) is part of the chat the UI syncs since the
master merge, but schema.Conversation had no field for it, so the server
dropped it and a reload from the server reset the setting. Add the field.
The handler spec that round-trips every chat field now includes
reasoningEffort. A schema test reads serializeChat in useChat.js and fails
for any key the UI sends that schema.Conversation cannot hold, so the next
such drift shows up as a failing test rather than in review (it reports
reasoningEffort on the previous head). Swagger and docs updated.
Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Stefan Walcz <stefan.walcz@walcz.de>
* test(chat): use Ginkgo for the schema drift guard
The new drift guard uses testing methods forbidden by repository lint.
Keep its field coverage and fail-fast parser checks in Ginkgo/Gomega.
Assisted-by: nib:gpt-6.1-sol
---------
Signed-off-by: TLoE419 <tloemizuchizu@gmail.com>
Signed-off-by: Stefan Walcz <stefan.walcz@walcz.de>
Co-authored-by: TLoE419 <tloemizuchizu@gmail.com>
Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
Co-authored-by: mudler-agent <mudler-bot@c3os.io>
* feat(sound): expand simple prompts with chat models
Allow callers to select an installed chat model for sound prompt
expansion. Validate its JSON before audio generation and preserve
explicit instrumental and language controls. Keep backend defaults
when expansion is omitted, including native Python ACE-Step sampling.
Assisted-by: Codex:gpt-6
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* feat(sound): map expansion by loaded audio family
Map neutral expanded fields where the resolved audio family is known.
Keep unmarked requests unchanged and reject unsupported expansion before
inference. Clear YuE2 and HeartMuLa transcripts to prevent lyric fallback.
Assisted-by: Codex:gpt-6.1-sol
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* feat(sound): choose a prompt expansion model
Require an explicit Simple choice and remember it separately from chat.
Clear unavailable choices instead of silently switching language models.
Preserve the choice in history and restore it when editing a past result.
Keep Backend default and Advanced requests compatible with existing APIs.
Document family support and align the published optional request schema.
Verify selection, request fields, persistence and history in the browser.
Assisted-by: Codex:gpt-6.1-sol
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* fix(sound): authorize both generation models
Check the resolved audio model and explicit expansion model before
loading either. Permission lookup failures stop generation.
Assisted-by: Codex:gpt-6
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* fix(sound): enforce JSON during prompt expansion
Constrain expansion output with the existing JSON grammar before decoding.
Require a llama-cpp chat model so unsupported backends cannot ignore it.
Keep strict field validation and fail before generating audio.
Assisted-by: Codex:gpt-6
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>
Pocket TTS ships one model per language, but LoadModel called
TTSModel.load_model() with no arguments, so the library always fell back
to its English model whatever the model config said. Read a `language`
option and pass it to load_model, keeping the library default when it is
not set. Document the option and add unit tests that mock the loader.
Fixes#12759
Assisted-by: Claude:claude-opus-5-5
* feat(tts): stream Qwen text through audio pipelines
Feed cleaned LLM deltas into one persistent Qwen synthesis session.
Return audio before input ends and preserve the live RPC through model
cache, staging and in-flight wrappers. Stop generation on sink failures
or cancellation without replaying speech through a fallback.
Pin the incremental native fork and verify early audio, late text,
finalization and cancellation with lifecycle and real-model checks.
Assisted-by: Codex:gpt-6.1-sol
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* build(tts): use the organization Qwen fork
Fetch the reviewed incremental synthesis commit from localai-org so
backend builds use the organization-maintained fork.
Assisted-by: Codex:gpt-6.1-sol
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* feat(gallery): add a Qwen streaming assistant preset
Expose an English pipeline using existing VAD, ASR, LLM and named-voice
TTS models. Enable incremental text and audio delivery, and document
separate component installs because the gallery has no dependency
installer.
Assisted-by: Codex:gpt-6.1-sol
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* fix(gallery): use Parakeet TDT v3 for the assistant
Select the existing Parakeet TDT 0.6B v3 transcription entry and update
its prerequisite install command. Keep the preset's English voice and
incremental Qwen synthesis configuration.
Assisted-by: Codex:gpt-6.1-sol
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* fix(kokoros): reject unsupported backend RPCs
Implement the MotionStream and SlotCache methods required by the shared
protobuf service. Return Unimplemented so Kokoros compiles without
advertising support for either operation.
Assisted-by: Codex:gpt-6.1-sol
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* fix(tts): mark cleanup and logging results ignored
Explicitly ignore connection shutdown, test server shutdown and
Ginkgo diagnostic writer errors. These operations do not replace the
stream's inference or sink errors, and errcheck requires their return
values to be acknowledged.
Assisted-by: Codex:gpt-6.1-sol
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* feat(tts): stream buffered clauses in realtime pipelines
Start clause synthesis while LLM text is still arriving when both streaming flags are enabled. Keep explicit clause opt-out and native Qwen sessions, bound automatic queued work, and choose streaming support on resolved failover targets.
Assisted-by: Codex:gpt-6.1-sol
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* test(tts): bound synthetic voice clone generation
The sine-wave reference checks speaker conditioning and WAV output, but does not provide a meaningful speech EOS target. Limit that smoke request to 128 frames and verify the request limit reaches native synthesis. Leave production sampling defaults unchanged.
Assisted-by: Codex:gpt-6.1-sol
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* fix(tts): require explicit clause buffering opt-in
Keep whole-reply synthesis for nonnative engines when clause_chunking is unset or false. Use the bounded cancellable worker when explicitly enabled, with unary clauses when TTS streaming is disabled. Preserve native Qwen precedence for every clause setting.
Assisted-by: Codex:gpt-6.1-sol
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* feat(gallery): add VITS and Piper streaming assistants
Provide comparable full pipeline configurations with shared VAD, Parakeet transcription, and Qwen chat components. Enable clause buffering explicitly and document the separate prerequisite model installs and Talk selection.
Assisted-by: Codex:gpt-6.1-sol
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>
* fix(ci): capture and surface ginkgo errors in run-coverage.sh
The coverage script ran ginkgo per test root with `|| fail=1`, which
swallowed the exit code. Combined with `make --output-sync=target`,
which buffers all output and flushes at the end, the actual ginkgo
error was invisible in interleaved CI logs. Every test suite passed
but the script still exited 1, and the error message was buried in
22 000 lines of buffered output.
Each ginkgo invocation now captures combined stdout+stderr to a
per-root log file. On failure the last 80 lines are printed to
stderr so the error is visible in the CI log. On success the log is
replayed to stdout so test output remains visible.
The build flags (--tags, --coverpkg) are quoted correctly so values
containing spaces (e.g. --tags="debug auth") are preserved through
eval.
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* fix(ci): create output dir before writing ginkgo logs
The previous commit added per-root log files but forgot to create
the output directory before the first ginkgo invocation. On CI the
coverage/ dir did not exist yet, so the shell redirect to
cover-pkg.out.log failed with "No such file or directory" before
ginkgo even ran.
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* test(core/http): add canvas preview route to auth coverage allowlist
Commit 9ad78381d8 added GET /api/canvas/preview/:id as a public route
(no auth — the random id is the capability, an iframe cannot send API
key headers) but did not add it to expectedPublicRoutes in the route
auth coverage test. The test flagged the route as an anonymous-access
leak, failing the entire ./core ginkgo suite on every CI run since.
The POST /api/canvas/preview endpoint is auth-gated (returns 401 to
anonymous requests) and is intentionally absent from the allowlist.
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>
The PII middleware scans the text of every message, but images, audio,
video and files pass to the backend unchecked. For an egress policy
("nothing sensitive leaves the box") that is a hole: a photographed
invoice or an attached PDF reaches a cloud-proxy model without any check.
Add pii.block_unscanned_media. While PII is enabled for a model and the
option is set, a request that carries such content is rejected with the
usual 400 pii_blocked response and an audit event with pattern
__unscanned_media__, before any detector runs. Adapters report the
kinds of unscanned content through a new optional Adapter.Unscanned:
OpenAI chat (non-text content blocks), Anthropic messages (image and
document blocks; tool blocks stay allowed) and Ollama chat (images).
Off by default, so existing configurations behave as before.
Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Stefan Walcz <stefan.walcz@walcz.de>
Two gaps in the chat composer:
- The microphone button only opened a file dialog for audio files; there
was no way to speak a message. Add a dictation button: the first click
records from the microphone, the second stops and sends the recording
to /v1/audio/transcriptions with the first installed transcription
model; the text is appended to the message box for review. The button
is hidden without a transcription model or without a microphone API.
The audio attach button keeps its function and gets the music icon.
- A PDF without a text layer (a scan) was rejected with a toast. Render
its first ten pages to PNG with pdf.js and attach them as images, so a
vision model can read them; a toast says how many pages went along.
PDFs that cannot be parsed at all are still rejected.
Strings for all eight locales, docs in features/chat.md, Playwright
specs for both (with negative cases: no transcription model, failed
transcription, unparsable PDF).
Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Stefan Walcz <stefan.walcz@walcz.de>
stable-diffusion.cpp PR #2148 added MOSS-Image support, which needs an
external tokenizer file passed via the --tokenizer CLI arg. The C++ binding
(gosd.cpp) never parsed a tokenizer option nor set ctx_params.tokenizer, so
MOSS-Image could not load through LocalAI.
- Wire the "tokenizer" option in gosd.cpp (parse + assign to ctx_params)
- Resolve "tokenizer:..." paths against the model directory in gosd.go,
the same way *_path options are resolved
- Bump the vendored stable-diffusion.cpp to 68285df (moss-image merge)
- Add moss-image-base and moss-image-sft gallery entries (int8_convrot)
FLUX.3 Image Klein is already in the gallery (added in #12736).
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
* chore(model gallery): 🤖 add new models via gallery agent
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* fix(gallery): give the Q6 build a unique name
The Q8 entry already uses embeddinggemma-2. Include the quantization
in the new entry name so both builds remain reachable.
Assisted-by: Codex:gpt-6
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* fix(gallery): configure EmbeddingGemma 2 variants
Serve the Unsloth builds as text embeddings and remove chat settings.\nGroup Q4, Q5, and Q6 with the existing family and pin verified assets.
Assisted-by: Codex:gpt-6
---------
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: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
* feat(llama-cpp): add lazy_mode model option
Expose upstream --lazy-mode (on, auto, off) as a model option, for
example `lazy_mode:on`. It controls on-demand reading of large tensors
such as per-layer embeddings; `on` requires mmap.
The option is also accepted as `lazy-mode` or `lzm`. Invalid values log
a warning and keep the upstream default (auto). The pinned llama.cpp
already provides the lazy_mode field in common_params.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* chore(llama-cpp): bump llama.cpp to c0fd78626405
Update the default llama.cpp pin to the current upstream master. The
changes since the old pin that the backend touches are additive: a new
common_decision_type value, new mtmd audio-stream and download helper
declarations. The two server patches still apply in order, and the
common_params fields and server headers the backend uses are unchanged.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
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>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* feat(llama-cpp): add slot prompt-cache save and restore API
Add POST /v1/slots/save and POST /v1/slots/restore. They write one
llama.cpp slot's prompt cache (KV state) to a file and load it back,
so a client can warm a long system prompt in a few hundred
milliseconds instead of re-running prefill. Refs #12685.
The new SlotCache gRPC call posts the same slot save and restore
tasks the llama-server /slots endpoint uses. It is off unless the
model sets the new slot_save_path option, so existing models are not
affected. The /slots monitoring endpoint is not needed here: the
backend has no HTTP server.
The file name comes from the caller, so it is checked twice, in Go
and again in the backend. It must be one path component: no empty
name, "/", "\", "..", NUL, control characters or absolute path. The
resolved path, with symbolic links followed, must be a direct child
of slot_save_path. The routes are admin only because they write
files on the backend host.
Assisted-by: Claude:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* fix(llama-cpp): answer 501 when a backend lacks slot cache
A backend that does not implement the SlotCache rpc returns gRPC
UNIMPLEMENTED. The API passed that text through, so the client saw
"unknown method SlotCache". Map the status to a typed error in
core/backend and return 501 with a fixed message instead. Every other
error path is unchanged.
Make the SlotCache comments in backend.proto backend-neutral: slot is
the backend's state slot, filename must stay inside the save directory,
and an empty model_identity means the backend does not check it. Only
comments change, no field or name.
Add tests that the identity sent is the configured model, and for the
error mapping.
Assisted-by: Claude:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* fix(llama-cpp): issue slot cache IDs on the server
The slot save and restore API let the client pick the file name.
Restore could load any file in slot_save_path, and the symlink check
ran before llama.cpp opened the file.
The backend now issues the name. A save returns a random 128-bit ID
(32 lowercase hex characters, read from /dev/urandom) and never
overwrites an existing file. Files live in a 0700 directory per model,
named from the SHA-256 of the model identity, with a manifest.json
that records each save. The manifest is written atomically and a
corrupt one stops both save and restore.
A restore takes the ID, finds it in the manifest, refuses an entry of
another model, builds the path from the entry and checks with lstat
that it is a plain file of the recorded size. The directory mode is
the main defence against another local user, since llama.cpp still
opens the path itself.
Each model keeps at most 32 saves and the oldest is deleted first. A
file over 8 GiB is deleted after the save, because llama.cpp does not
report the size before it writes. Responses carry no host path.
The HTTP bodies change: save takes {model, slot}, restore takes
{model, slot, id}, and an unknown ID returns 404. In the proto,
SlotCacheRequest.filename and SlotCacheResponse.filename become id.
SlotCache no longer calls checkModelIdentity, which reads a field this
request does not have.
Assisted-by: Claude:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* fix(llama-cpp): copy slot_cache files in prepare.sh
grpc-server.cpp includes slot_cache.h, but prepare.sh copies the helper
headers into llama.cpp/tools/grpc-server/ one by one and skipped it. The
llama-cpp, turboquant and bonsai builds share this step, so they all
failed with "slot_cache.h: No such file or directory".
Copy slot_cache.h and slot_cache_test.cpp, which CMakeLists.txt builds
when LLAMA_GRPC_BUILD_TESTS is on.
Assisted-by: Claude:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* chore(deps): bump LocalAGI to pick up the scheduler start/stop fix
LocalAGI's scheduler could hit a nil pointer in run when Start and Stop
raced. That made the core/services/agentpool tests panic at random and
turned tests-linux red.
Move to mudler/LocalAGI@7aa0c8fea0, the merge of
mudler/LocalAGI#519 ("make Start and Stop safe to call concurrently").
The new module version also adds a few indirect requirements to go.mod
and go.sum.
Assisted-by: Claude:claude-sonnet-5-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>
Co-authored-by: Ettore Di Giacinto <mudler@users.noreply.github.com>
Add official Q4, Q5, Q6, and Q8 GGUF builds with the F16 vision
projector. Group them for automatic selection and document installation.
Pin downloads to one Hugging Face revision with verified SHA256 values.
Assisted-by: Codex:gpt-6
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
fix(ui): serve canvas HTML previews from a route with their own CSP
External scripts in canvas previews (for example three.js from unpkg)
were blocked. The preview used an iframe with srcdoc, which inherits the
global Content-Security-Policy, and a meta tag can only tighten it.
The UI now posts the HTML to POST /api/canvas/preview, gets a random id,
and loads GET /api/canvas/preview/:id in the iframe. That response sets
its own CSP that allows scripts from unpkg, jsdelivr, cdnjs, the
Tailwind CDN and jQuery. The global CSP is unchanged. The iframe keeps
sandbox="allow-scripts" without allow-same-origin.
Previews are kept in memory for 10 minutes and are never written to
disk. The GET route is public because the id is the capability and a
frame navigation cannot send API key headers.
Assisted-by: Claude:claude-haiku-5-5 [go test] [npm]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
* chore(vllm-cpp): bump MLX pin to 0.32.4
The macOS runner's pip index no longer offers mlx==0.29.4 (it lists only
0.32.4), so the darwin vllm-cpp build fails at the MLX venv step. Move the
pin to 0.32.4 and update the README default to match.
Not verified on macOS: the Makefile was only dry-run on Linux.
Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* fix(vllm-cpp): pin MLX 0.32.0, the version the engine builds against
The macOS build fails against mlx 0.32.4:
metal_mlx_provider.mm:147:5: error: no matching member function
for call to 'set_data'
The vllm.cpp Metal provider calls the five-argument
array::set_data(Buffer, size, strides, flags, Deleter). The 0.32.4
headers only declare set_data(allocator::Data) and the Data overload
with size, strides, flags and offset; 0.32.0 still has the Buffer one.
The engine's own CI installs 'mlx==0.32.0' (ci.yml line 1798,
release.yml line 360), so pin that and update the README default.
Not verified on macOS: only the Makefile dry-run and the wheel
headers were checked on Linux.
Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* fix(vllm-cpp): install the MLX 0.32.0 wheel by URL and hash
The macOS runner's pip index offers only mlx 0.32.4, so
'pip install mlx==0.32.0' fails with "Could not find a version that
satisfies the requirement". 0.32.4 cannot be used: its headers lack the
five-argument array::set_data the engine calls.
Install the 0.32.0 wheel directly by URL with a sha256 check. The wheel
is mlx-metal (py3-none-macosx_14_0_arm64) from PyPI, sha256
5b64b20ac24b0c401f489de01e8209edc4d372125201f19314e6f39e385322aa. It
holds mlx/include, mlx/lib/libmlx.dylib and mlx/lib/mlx.metallib, which
is all the build and package.sh use, and it is not tied to a CPython
version. The URL and hash are Makefile variables; the stamp stays keyed
on MLX_VERSION.
Not verified on macOS: only the Makefile dry-run, the wheel contents and
its sha256 were checked on Linux.
Assisted-by: Claude Code:claude-sonnet-5-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>
Add the distilled and base FLUX.3 Image Klein models for the
stablediffusion-ggml backend, each in Q4_K_M and Q8_0. They use the
Qwen3-VL 4B text encoder and the FLUX.2 VAE.
Klein runs with 16 steps and CFG 1.0, Klein Base with 20 steps and
CFG 3.5, both with the euler sampler. The sd.cpp pin on master already
contains FLUX.3 support.
Assisted-by: Claude Code:claude-sonnet-5-5
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
A decision request (/v1/systemone, /v1/decisions) sends Score with
question_type "systemone". A backend built before native decisions ignores
that field and runs plain scoring, so the caller saw "Score was not enabled"
(500) or "candidates must be non-empty" (400). The gallery "latest" image can
lag behind the server, and the upgrade check compares only tag, version and
digest, so /backends/upgrades did not list the backend.
Recognise those two errors for a decision request, return 501 with a message
that says the backend is older than the server and must be upgraded, and mark
the backend stale. The upgrade check now reports a marked backend with a
`reason`, and refreshes at once. Reinstalling the backend drops the mark; if
the new image is still old, the next request marks it again. Model loading is
not affected.
Refreshing the "latest" backend image is a separate publishing step and is
not part of this change.
Refs #12693
Assisted-by: Claude Code:claude-sonnet-5-5 [go vet] [go test]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
* fix(audio): use captions as the default YuE2 style
Studio sends a caption, but YuE2 requires a style request option.
Default audio-cpp style from caption while preserving explicit options
and the caller's parameter map. Document the Studio fields.
Assisted-by: Codex:gpt-6
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* fix(audio-cpp): map captions to style in backend
Move the YuE2 caption fallback into the C++ request builder as requested.
Preserve explicit style options, including empty values, and move the
regression cases from the Go layer into the backend suite.
Assisted-by: Codex:GPT-6
---------
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
Bump LLAMA_VERSION to b76b13379d3b (upstream 0.7.0 plus later
commits). This is the same one-line change as the bot PR #12725.
Add the moe_cache_mib model option. It sets upstream's
moe_cache_size, a GPU cache for MoE experts kept in host memory,
from a size in MiB. It is off by default. Invalid, negative or
non-numeric values are ignored with a log line. Forks that predate
the field ignore the option, using the same prepare.sh probe as the
other fork-skew switches.
Document the option, and cover the MiB to bytes conversion with a
standalone unit test.
Assisted-by: Claude:claude-sonnet-5-5 g++
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
fix(ui): fold the "No reasoning" chip into the effort picker
The chat composer had two controls for one idea: the effort picker
with a "None" level, and a separate "No reasoning" chip. The chip
also hid reasoning traces that a model sends despite
reasoning_effort "none", which "None" did not. The chip disabled the
picker while it was on.
Choosing "None" now hides those traces and stays selectable, so the
chip, its toasts and the /reasoning slash action are removed. A chat
saved with the old reasoningOff flag loads with effort "none", so it
keeps hiding reasoning.
Assisted-by: Claude:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>