Commit Graph
8646 Commits
Author SHA1 Message Date
localai-org-maint-botandmudler fd2697686d chore: ⬆️ Update ggml-org/whisper.cpp to 231638a0717b7822784297e5980965ffcec05810 (#12776)
⬆️ 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-10-20 08:25:30 +02:00
Stefan Walczandlocalai-org-maint-bot 9c0266e506 fix(openresponses): keep server-side MCP tool items on synchronous paths (#12711)
* fix(openresponses): keep server-side MCP tool items on synchronous paths

When a model runs MCP tools server-side, /v1/responses streams or builds
function_call and function_call_output items for each call, but the two
synchronous paths dropped them from the final response:

- non-streaming: the output items are local to each MCP iteration, which
  recurses, so only the last iteration's items were returned and stored.
- streaming: response.completed and the stored response were rebuilt from
  the last iteration (reasoning, message, unexecuted tool calls) only.

The background paths already keep them. Without the trail, a follow-up
with previous_response_id, or a client that rebuilds history from the
completed output, never shows the model which tools ran and what they
returned. Measured with gemma4-26b behind a model with mcp.auto: on 12
follow-up questions that need fresh tool data, the model called the tool
0/12 times when the history carried only text (and invented numbers), and
12/12 times when the earlier tool calls and results were included.

The non-streaming handler now passes the items of earlier iterations
into the recursive call; the streaming handler collects each finished MCP
iteration's reasoning, function_call and function_call_output items and
puts them in front of the final items in response.completed and in the
store. Requests without server-side tools are unchanged. No new state: the
items travel in the already replicated stored response.

Tests: an in-memory MCP server behind the real LocalToolExecutor and a
stubbed backend. For both synchronous paths the completed and the stored
output are function_call, function_call_output, message (with reasoning
in front when the model reasoned before the call), and a follow-up through
ResponsesEndpoint with previous_response_id hands the backend the tool
call and its result. Counter-checks: requests without tools still return
a single message, and both background paths are pinned as they were.

Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Stefan Walcz <stefan.walcz@walcz.de>

* fix(responses): preserve MCP trail associations

Keep tool result call IDs when replaying stored responses. Clear consumed
streaming state so the iteration limit cannot duplicate the final round.
Add regression coverage for replay and iteration exhaustion.

Assisted-by: Codex:GPT-6
Signed-off-by: Stefan Walcz <stefan.walcz@walcz.de>

---------

Signed-off-by: Stefan Walcz <stefan.walcz@walcz.de>
Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-10-19 23:44:59 +02:00
Stefan Walcz 95034f1147 feat(ui): offer model MCP servers to non-admin chat users (#12660)
The chat page derived MCP availability from GET
/api/models/config-json/:name, which is admin-only. With auth enabled a
regular user holding the `mcp` feature (default ON) therefore never saw
the MCP Servers/Prompts/Resources tabs, although every endpoint behind
them (/v1/mcp/servers|prompts|resources/:model) is gated only by that
feature.

/api/models/capabilities, which the chat already loads for its model
picker and which honours the per-user model allowlist, now reports
`mcp: true` for models whose config declares remote or stdio MCP servers
(never when MCP is disabled on the instance). For non-admins the chat
combines that flag with hasFeature('mcp') and clears a stale MCP
selection otherwise. Admins keep the config-json path unchanged; the
model info panel and automatic context size stay admin-only on purpose,
since they need the full model config.

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

Signed-off-by: Stefan Walcz <stefan.walcz@walcz.de>
2026-10-19 23:43:47 +02:00
Stefan Walcz b56b903c92 feat(config): let model aliases carry an opt-in profile block (system prompt, MCP, client tools) (#12661)
* feat(config): let model aliases carry an opt-in profile block

An alias can now act as a profile of its target through an explicit
`profile:` block carrying system_prompt, mcp (remote/stdio servers, plus
the new mcp.auto) and the new client_tools. ResolveAlias applies only
that block on top of a copy of the target, while Name, Model, Backend
and the persisted revision stay the target's. The loader key is
therefore unchanged and every profile runs on the target's backend
process; today a persona needs a separate model config, which loads the
weights a second time.

The block is opt-in so existing aliases never change: top-level fields
on an alias stay ignored, and an alias without a profile block resolves
to exactly the target. A profile block on a non-alias config is
rejected.

A profile with its own MCP servers caches its sessions under the alias
name (MCPSessionKey), because the cache slot keyed on the target's name
is invalidated whenever its config hash changes and two server sets
would tear each other's sessions down.

mcp.auto enables all configured servers on /v1/chat/completions when the
request does not name any in metadata.mcp_servers, which plain OpenAI
clients cannot send. client_tools: drop discards client tools and
tool_choice before the request is merged (chat completions, responses,
Anthropic messages), so only the model's own MCP tools are offered.
Both keys work on any model and inside an alias's profile block.

Refs #12548

Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Stefan Walcz <stefan.walcz@walcz.de>

* fix(openai): only auto-enable model MCP when MCP is enabled and permitted

mcp.auto turns a plain /v1/chat/completions request into a server-side
MCP request. It now applies the same controls as the dedicated MCP
routes before any server is selected: the instance switch
(LOCALAI_DISABLE_MCP) and, with auth on, the caller's "mcp" feature,
checked with auth.HasFeatureAccess against the auth database that the
chat route now passes to ChatEndpoint.

A denied request is answered as a plain chat, without contacting a
server or adding tools, rather than rejected: the client did not ask
for MCP, it only reached a model that offers it.

Endpoint specs drive ChatEndpoint against a counting MCP fixture server:
MCP disabled and a user with chat but without mcp see no tools and no
connection; MCP enabled and a user with mcp get the fixture tool. The
user specs need the auth build tag (SQLite auth database).

Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Stefan Walcz <stefan.walcz@walcz.de>

* fix(mcp): resolve profile aliases in the MCP discovery endpoints

GET /v1/mcp/servers/:model, /v1/mcp/prompts/:model and
/v1/mcp/resources/:model looked the model up with GetModelConfig, which
returns the alias stub. A profile alias carries its servers in its
profile block, so the stub has none of its own and the endpoints
reported an empty list for an alias whose chat requests do use tools.

Resolve the alias the way the request middleware does before reading the
MCP config, and open the sessions under MCPSessionKey so discovery shares
the profile's session cache slot instead of tearing down the target's.

Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Stefan Walcz <stefan.walcz@walcz.de>

* feat(config): opt-in evidence check for profile aliases

A profile with server-side MCP tools (profile.mcp, mcp.auto) runs its
tools on the server and returns only the answer text. On the next turn
the client sends the history back without those tool calls and results,
and the model then tends to skip the tool and answer from nothing. In a
measurement on a follow-up question, 0 of 12 turns called the tool and
the answers carried invented numbers (e.g. "0" where the true value was
9); with the tool trail kept in the history the tool was called 12 of
12 times. A prompt instruction does not reliably prevent this, so the
check sits at the output.

New profile keys:

  profile:
    evidence_check:
      numbers: true   # enables the check
      note: "..."     # optional, defaults to an English note

Rule, per request and stateless: when the check is on, no MCP tool call
succeeded on the server during this request (a call that returned an
error delivers no evidence and does not count), and the final answer contains a
number (\b\d+([.,]\d+)?\b) that occurs in none of the request's input
(the profile's system prompt and every message as received, before
compression), the note is appended after a blank line. "3,5" and "3.5"
count as the same number. In a streamed response the note is one extra
content delta before the finish chunk. Choices that hand tool calls to
the client are not checked. The setting is only read from a profile
block, so plain models and aliases behave as before; a note without
numbers: true is rejected at validation.

Tests (Ginkgo): the number rule (invented number, number from the input,
no numbers, comma/dot decimals, system prompt as evidence, tool ran),
the overlay into the resolved config with plain alias, target and a
top-level key as counter-checks, validation, and ChatEndpoint specs on a
stubbed backend for both paths: note on an invented number, none for a
number from the request, none for a profile without the check, none
after a successful MCP tool call against an in-process MCP server, and
the note again after a failed tool call (arguments that are not JSON).

Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Stefan Walcz <stefan.walcz@walcz.de>

---------

Signed-off-by: Stefan Walcz <stefan.walcz@walcz.de>
2026-10-19 23:42:40 +02:00
Stefan Walczandlocalai-org-maint-bot d768f1f973 feat(pii): apply PII middleware to the Open Responses API (#12708)
* feat(pii): apply PII middleware to the Open Responses API

The per-model PII filter ran on /v1/chat/completions, /v1/completions,
/v1/messages and the Ollama routes, but not on POST /v1/responses. A
model whose policy blocks or masks a secret on the chat route served the
same content unfiltered when it was sent through the Responses API.

Add a piiadapter.OpenResponses adapter and wire pii.RequestMiddleware
into the POST /v1/responses and /responses chain, after the model config
is resolved and merged and before the handler converts input items into
prompt messages. The adapter covers instructions, input as a string, and
in an input item array the text of message and reasoning items (string
content or input_text/output_text parts, including typeless
{role, content} items), function_call arguments and function_call_output
output; structured tool output is scanned as the JSON the handler
forwards. Image, file, audio and video parts are left untouched.

The Responses WebSocket mode (GET /v1/responses) is not HTTP middleware
and cannot filter, so a response.create for a model with PII filtering
enabled is now refused with pii_unsupported_transport instead of being
served unfiltered. The docs list the new route and this limit.

Tests: adapter specs for every covered field, each with a clean-text
counter-check; a route spec driving the real middleware chain with an
in-process pattern detector (block on string input, content part,
instructions and tool output; mask; clean input passes unchanged); and
WebSocket specs proving the refusal and that models without a PII
policy are still served.

Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Stefan Walcz <stefan.walcz@walcz.de>

* fix(pii): scan complete Responses tool output arrays

The Responses handler forwards non-string tool output as JSON. Scan
that full encoding so string arrays and extra fields cannot bypass PII
filtering. Cover array scans, redaction, clean values and route blocking.

Document that stored history is resolved after request filtering.

Assisted-by: Codex:gpt-6
Signed-off-by: Stefan Walcz <stefan.walcz@walcz.de>

* feat(pii): report unscanned media on the Open Responses route

Extend block_unscanned_media to /v1/responses: input_image, input_file
and input_audio parts inside message items, and the same types as bare
input items, are reported as unscanned content. Tool output stays
allowed, as it is scanned as JSON text.

Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Stefan Walcz <stefan.walcz@walcz.de>

---------

Signed-off-by: Stefan Walcz <stefan.walcz@walcz.de>
Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-10-19 23:39:56 +02:00
localai-org-maint-botandmudler e97bd0be76 chore(model-gallery): ⬆️ update checksum (#12784)
⬆️ 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-10-19 23:38:33 +02:00
mudler-agentandEttore Di Giacinto 0de4a7e0d9 fix(ui): restore per-node installed backend removal (#12785)
* 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(ui): restore confirmed per-node installed backend removal

Verify exact node inventory and protection before offering and confirming removal. Capture the node/backend target, guard duplicate requests, and preserve installed data when refresh fails. Reuse the flow for catalog target removal and document scoped retry behavior.

Assisted-by: nib:gpt-6.1-sol

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

* fix(ui): guard targeted backend mutations and refresh feedback

Distinguish ordinary refresh failures from successful deletion with stale data. Lock target installs synchronously and block conflicting install/removal actions through completion-driven refresh. Cover retry wording, deferred mutation races, obsolete inventory responses, and final installation selection cleanup.

Assisted-by: nib:gpt-6.1-sol

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

* fix(ui): block conflicting generic backend deletion

Keep generic deletion blocked during scoped removal and targeted installs.
Recheck conflicts after dependency lookup and before confirmation so
navigation cannot expose a conflicting delete action.

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>
2026-10-19 23:36:08 +02:00
localai-org-maint-botandmudler 37c773c384 chore: ⬆️ Update localai-org/privacy-filter.cpp to 8e0213a8d59d6659251b9ee68795878f1042b18c (#12775)
⬆️ Update localai-org/privacy-filter.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-10-19 23:10:32 +02:00
localai-org-maint-botandmudler 99dc20133d chore: ⬆️ Update localai-org/gem-x.cpp to 756f6b30d1e39a0e2ddb3eb13262ba0316fee8df (#12777)
⬆️ Update localai-org/gem-x.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-10-19 23:10:15 +02:00
localai-org-maint-botandmudler 42e54aeb52 chore: ⬆️ Update ikawrakow/ik_llama.cpp to 35e72560fac6eb4e0bedc99976d3a12040749230 (#12778)
⬆️ 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-10-19 23:09:58 +02:00
localai-org-maint-botandmudler ec50784474 chore: ⬆️ Update localai-org/kimodo.cpp to ed8a931aedb132f768190f6931c365855caba74c (#12780)
⬆️ Update localai-org/kimodo.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-10-19 23:09:38 +02:00
mudler-agentandEttore Di Giacinto 2f4eaf1fad fix(sound): require tool calls for expansion (#12783)
Require one expand_sound call instead of parsing assistant prose.
Reject unsupported templates and invalid arguments before audio generation.
Keep installed configuration unchanged and preserve explicit controls.

Assisted-by: nib:gpt-6.1-sol

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
2026-10-19 23:04:14 +02:00
mudler-agentandEttore Di Giacinto dce91ba7aa fix(backends): prefer installed development builds (#12782)
* 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>
2026-10-19 22:58:38 +02:00
36307005a9 feat(chat-history): server-side chat history with tombstones (follow-up to #9902) (#12663)
* 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>
2026-10-19 22:52:07 +02:00
mudler-agentandEttore Di Giacinto ae8092e1c6 feat(sound): expand Simple prompts with chat (#12772)
* 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>
2026-10-19 18:16:20 +02:00
Younes Beriane e517470554 fix(pocket-tts): honour the language option when loading the model (#12766)
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
2026-10-19 13:12:42 +02:00
localai-org-maint-botandmudler 5ec7c05ad6 chore(website): refresh the counters (#12768)
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-10-19 13:12:00 +02:00
localai-org-maint-botandmudler 5841a42e85 chore: ⬆️ Update leejet/stable-diffusion.cpp to b13a48219d5cdf408edcb93da6cd69b612ed03d3 (#12753)
⬆️ 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-10-19 12:36:54 +02:00
localai-org-maint-botandmudler 4776986feb chore: ⬆️ Update PrismML-Eng/llama.cpp to 11809184898113482c2f510fd13da669aaba1052 (#12763)
⬆️ 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-10-19 12:35:59 +02:00
localai-org-maint-botandmudler 9ba9639948 chore: ⬆️ Update 0xShug0/audio.cpp to 507495611b22dc4590e0e5609e05e8096384e33f (#12764)
⬆️ 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-10-19 12:35:27 +02:00
mudler-agentandEttore Di Giacinto f85087f992 feat(tts): stream Qwen text and add assistant preset (#12770)
* 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>
2026-10-19 12:30:52 +02:00
mudler-agentandEttore Di Giacinto 48f358dd95 fix(ci): capture and surface ginkgo errors in run-coverage.sh (#12765)
* 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>
2026-10-19 08:09:26 +02:00
localai-org-maint-botandmudler 4d11d86e09 chore: ⬆️ Update 0xShug0/audio.cpp to 03d1b469063390e6d04cd13f5df5163894994029 (#12752)
⬆️ 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-10-19 00:08:02 +02:00
localai-org-maint-botandmudler 0e77dfba00 chore(model-gallery): ⬆️ update checksum (#12762)
⬆️ 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-10-19 00:07:41 +02:00
localai-org-maint-botandmudler fbe63617d2 chore: ⬆️ Update ikawrakow/ik_llama.cpp to c446f1e089f3dc2436b0600596c23abde00d988f (#12746)
⬆️ 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-10-18 21:55:04 +02:00
localai-org-maint-botandmudler eb74d3861f chore: ⬆️ Update PrismML-Eng/llama.cpp to 0082ac24a89ae62132041b40e2c0170e278a98f4 (#12745)
⬆️ 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-10-18 21:54:54 +02:00
localai-org-maint-botandmudler 423734d252 chore: ⬆️ Update CrispStrobe/CrispASR to dd14099c35d297364092576466b12e622d399b41 (#12744)
⬆️ 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-10-18 21:54:41 +02:00
Stefan Walcz f4505dbb5d feat(pii): optionally block content the detectors cannot inspect (#12755)
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>
2026-10-18 21:54:17 +02:00
Stefan Walcz 92500b4389 feat(ui): dictate in the chat composer and read scanned PDFs as images (#12756)
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>
2026-10-18 21:52:59 +02:00
mudler-agentandEttore Di Giacinto d501447d73 feat(gallery): add MOSS-Image and wire tokenizer option (#12757)
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>
2026-10-18 21:47:23 +02:00
localai-org-maint-botandmudler 0d6e57f9f5 chore(model-gallery): ⬆️ update checksum (#12751)
⬆️ 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-10-17 23:30:06 +02:00
216c85374e fix(gallery): configure EmbeddingGemma 2 variants (#12667)
* 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>
2026-10-17 22:45:07 +02:00
e7cc993b0f feat(llama-cpp): add lazy_mode model option (#12743)
* 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>
2026-10-17 22:38:23 +02:00
e715af1def feat(llama-cpp): add slot prompt-cache save and restore API (#12730)
* 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>
2026-10-17 18:23:02 +02:00
localai-org-maint-botandlocalai-org-maint-bot 9bceeb1e5d feat(gallery): add MiniCPM-V 4.7 1B variants (#12684)
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>
2026-10-17 00:54:45 +02:00
mudler-agentandEttore Di Giacinto 9ad78381d8 [ui] Serve canvas HTML previews with their own CSP (#12715)
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>
2026-10-17 00:54:22 +02:00
localai-org-maint-botandmudler 06b2abe5ec chore: ⬆️ Update ServeurpersoCom/omnivoice.cpp to 1a468d8b9d65b3544ca1004d2d7c05dfc65d1f3e (#12738)
⬆️ 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-10-17 00:41:22 +02:00
localai-org-maint-botandmudler 4548aaae33 chore: ⬆️ Update 0xShug0/audio.cpp to de4f4c977cecb93894590f98170226af9620e603 (#12739)
⬆️ 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-10-17 00:41:07 +02:00
localai-org-maint-botandmudler 1513ba3ab3 chore: ⬆️ Update CrispStrobe/CrispASR to 604b7deca5fbc16af8c9eafd8e6162a7e34da9cf (#12740)
⬆️ 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-10-17 00:40:52 +02:00
localai-org-maint-botandmudler 756b35d532 chore(model-gallery): ⬆️ update checksum (#12742)
⬆️ 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-10-16 23:22:50 +02:00
localai-org-maint-botandmudler 0b842eaa9c feat(swagger): update swagger (#12737)
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-10-16 22:35:16 +02:00
mudler-agentandEttore Di Giacinto 13466917f0 fix(vllm-cpp): pin MLX 0.32.0, the version the engine builds against (#12734)
* 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>
2026-10-16 22:32:47 +02:00
mudler-agentandEttore Di Giacinto d82624c81e feat(gallery): add FLUX.3 Image Klein and Klein Base (#12736)
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>
2026-10-16 18:44:58 +02:00
localai-org-maint-botandlocalai-org-maint-bot eec2ec33de feat(gallery): add LFM2.5 24B variants (#12673)
Add official Q4, Q5, Q6, and Q8 GGUF builds with pinned downloads.
Document installation and automatic variant selection.

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>
2026-10-16 17:34:53 +02:00
mudler-agentandEttore Di Giacinto 39b3b31153 fix(backends): flag a llama-cpp backend that predates native decisions (#12733)
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>
2026-10-16 10:41:45 +02:00
localai-org-maint-botandlocalai-org-maint-bot 62fa19b833 fix(audio): use captions as the default YuE2 style (#12703)
* 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>
2026-10-16 08:46:16 +02:00
github-actions[bot]andmudler fdfb171c3e chore: bump inference defaults from unsloth (#12732)
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-10-16 08:45:28 +02:00
localai-org-maint-botandmudler 377e71b704 chore: ⬆️ Update OpenNMT/CTranslate2 ROCm wheel to v4.8.3 (#12675)
⬆️ Update OpenNMT/CTranslate2 ROCm 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-10-16 00:57:23 +02:00
localai-org-maint-botandmudler 6a29abc6f5 chore: ⬆️ Update CrispStrobe/CrispASR to 36ddc87accf351d689b4fba0b5030aa42f67ccc8 (#12681)
⬆️ 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-10-16 00:57:07 +02:00
localai-org-maint-botandmudler 6d322a2f6f chore: ⬆️ Update ServeurpersoCom/omnivoice.cpp to 48b2d00e7435edace09aa09b0efdbc633e0dadf4 (#12697)
⬆️ 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-10-16 00:56:51 +02:00