The chat could not set reasoning_effort or turn reasoning traces off.
Add an effort menu and a "No reasoning" toggle to the composer chips,
and the matching /effort and /reasoning slash actions. Both are stored
per chat. The effort menu defaults to the model default, which sends no
reasoning_effort, so existing chats behave as before.
Chosen effort goes out as the per-request reasoning_effort field. The
toggle sends "none", which the server maps to enable_thinking=false, and
also drops any reasoning the model still returns, so the thread shows
only the answer.
Assisted-by: Claude:claude-sonnet-5-5 eslint playwright
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
* fix(http): stop listing the models directory on embeddings and bearer requests
Every embeddings request read the whole models directory before the body
was parsed, even when the body named its model. On a network-backed
models directory this cost seconds per request.
Embeddings now binds the body first and looks up a default model only
when the body has none, like chat. The pre-bind default lookup and the
gpt-4o constant default are gone from that route.
The first-available default lookup skips the directory when the filter
cannot accept an unconfigured file. Every usecase filter rejects files
without a config, so the scan never added results for them. Match-all,
nil and name filters keep the scan.
The bearer token check is now an exact test: a configured name, or one
stat of that name in the models directory. The value is a literal, not a
regular expression, and a name that is not a local relative path never
matches a file.
Assisted-by: Claude Code:claude-haiku-5-5
* perf(config): cache GGUF header reads for model defaults
Model defaults re-parsed the GGUF header on every SetDefaults call, which
runs per request and at startup reload. Put a bounded LRU in front of the
parser, keyed by absolute path and validated by size and modification
time. The cached value is a snapshot of the fields the defaults use, not
the parser output.
A changed size or mtime is a miss. Missing files and parse errors are not
cached, so the existing fallback is unchanged. LOCALAI_DISABLE_GUESSING,
the empty-path guard, the alias early return, the panic recovery and the
context-size default are all kept in front of the cache.
Assisted-by: Claude:claude-haiku-5-5
* test(http): cover listing-free default lookup on every usecase route
Assisted-by: Claude Code:claude-haiku-5-5
* feat(config): persist the GGUF header cache per instance
The header cache kept parsed GGUF results only in memory, so every restart
parsed each model file again before its defaults were known. Write the cache
to a per-instance file in the data path and load it at startup. A restarted
frontend with a fixed instance ID starts warm.
The file is versioned and records the estimator inputs (gguf-parser version,
candidate contexts, auto context size, offload count). A mismatch on any of
them makes the file ignored. Each entry keeps path, size and mtime, so a
changed model file is a miss on load. Writes run on one background saver,
debounced, through a temp file and rename. The entry bound applies to the
file as well. Without a data path the cache stays in memory only.
Documented as a per-instance exception in the model configuration docs.
Assisted-by: Claude:claude-haiku-5-5
* docs(embeddings): document model selection and the no-model error
Assisted-by: Claude Code:claude-haiku-5-5
* refactor(config): split the header cache into a generic cache
The GGUF header cache mixed two things: the mechanics (bounded LRU keyed
by path and identity, per-instance file, debounced atomic save, tolerant
load) and the GGUF fields. Move the mechanics into headerCache[S], driven
by a headerCacheSpec[S] that names the file prefix, schema, fingerprint,
parser and the snapshot encode/decode pair. The GGUF cache becomes one
instance of it.
File names, the identity check, LRU size, debounce, atomic write and the
corruption handling are unchanged. The on-disk JSON keeps the "estimator"
key so existing files still load.
Assisted-by: Claude Code:claude-haiku-5-5
* feat(config): share one GGUF header cache file between frontends
Every frontend used to write its own header cache file, named after its
instance ID, so a header read by one frontend was never reused by another.
The cache now uses one file, header-cache.json, in the data path, for all
frontends and for standalone mode. The instance ID no longer changes the
file name.
Frontends share the file in three ways. A save first merges the file's
entries in, so one frontend's write does not drop another's entries. A
miss reloads the file when its modification time has changed, and a hit
never reads it. Writes go to a temp file that is renamed into place, so a
reader never sees a partial file.
Two frontends saving at the same moment can still drop each other's last
entries. That costs one re-parse, never a wrong value, because every
entry is checked against its model file before use.
The shared file is valid only when every frontend mounts the same data
volume. The distributed-mode and model-configuration docs state this mode
and its volume limit.
Assisted-by: Claude:claude-haiku-5-5
* feat(config): give an instance ID its own header cache file
Assisted-by: Claude Code:claude-haiku-5-5
* style(config): gofmt the distributed config struct
Assisted-by: Claude Code:claude-haiku-5-5
---------
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
Fluid Speech Mini is a compact English dictation model by FluidVoice,
built on NVIDIA Parakeet TDT 0.6B v3 and Moondream Parakeet Ultra. It
matches Parakeet Ultra accuracy (4.53% WER on the Open ASR Leaderboard's
8 public English sets) at about a quarter of the download size (347 MB),
and is trained to keep transcribing the right speaker when another voice
is in the room.
The GGUF is a stock parakeet.cpp TDT checkpoint (Q2_K/Q4_K mixed
precision) that loads in the existing runtime on CPU and CUDA without
engine changes. Verified locally: parakeet-cli loads the model and
transcribes a test clip correctly.
Gallery entry uses the parakeet-cpp backend with the model file sourced
from huggingface://FluidVoice-Labs/fluid-speech-mini-gguf.
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
Warn before the standalone pool starts root agents with user auth enabled.
Count root agents, collections, skills, and configured skill repositories.
Document the auth transition, bootstrap rules, and agent key behavior.
Addresses the warning and documentation step of #12665.
Admin visibility and data migration remain separate work.
Assisted-by: Codex:GPT-6
Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
* feat(stablediffusion-ggml): run MiniMax-H3 video with reference conditioning
Bump stable-diffusion.cpp to 2988060 and follow its tiling rename
(tile_size_x/y -> tile_size_w/h, rel_size_x/y -> rel_size_w/h).
GenerateVideo now passes params["ref_image"] and the request audio to
the MiniMax-H3 Ref2VA inputs (ref_images, ref_audios). The audio must
be a WAV file. params["ref_video"] is refused, since the proto cannot
carry a frame sequence yet.
stable-diffusion.cpp cannot tell the FL2VA and REF2VA DiTs apart, and a
reference input on FL2VA renders a wrong video without an error. The
backend now reads the video_partition option, or the model file name,
and fails early on that mix. Keyframes plus references are refused too.
Add gallery entries for the FL2VA and REF2VA Q4_K_M sets. They do not
enable any bf16 dequantisation.
Assisted-by: Claude:claude-sonnet-5-5 go vet gofmt
* feat(stablediffusion-ggml): take several reference images, videos and audio clips
Phase 1 passed one reference image and one reference audio clip to
stable-diffusion.cpp and refused ref_video. Add repeated reference fields
to GenerateVideoRequest (ref_images, ref_videos, ref_audios) so the
MiniMax-H3 Ref2VA path can use as many references as upstream accepts. A
reference video is a directory of frames with an fps and an optional
paired audio clip.
The /video endpoint stages the new inputs through the same path as
start_image, end_image and audio. Frames of one reference video are
written to a private directory under zero-padded names so the backend can
read them in order. The input count and frame count per request are
bounded.
The backend merges the new fields with params.ref_image, params.ref_video
and audio, which keep working. The FL2VA partition refuses any reference
input, REF2VA accepts all of them, and keyframes stay exclusive with
references. Reference audio is still WAV only.
Assisted-by: Claude:claude-sonnet-5-5 [Bash]
* feat(gallery): offer MiniMax-H3 by partition, DiT layout and format
Name each MiniMax-H3 entry after its partition (fl2va or ref2va), DiT
layout (unpruned or pruned) and format. Add the vllm.cpp builds that its
model page lists: pruned Q8_0 (renders), pruned Q6_K, Q5_0, Q4_K, Q3_K and
Q2_K (load only), and the pruned FL2VA NVFP4 file (loads only). The
earlier names minimax-h3-fl2va-q4 and minimax-h3-ref2va-q4 stay as
aliases. The two stable-diffusion.cpp entries become
minimax-h3-fl2va-unpruned-q4-ggml and minimax-h3-ref2va-pruned-q4-ggml,
and the latter now lists video input.
Fix two bad sources. The stable-diffusion.cpp entries took the VAEs from
a path that does not exist in the GGUF repository; they now use the
Comfy-Org copies. The vllm.cpp entries paired the fp16 video VAE weights
with a config from a different directory; they now use the official
weights and config from FL2VA/video_vae/source/.
Drop video_dequant_bf16 from every vllm.cpp entry. Expanding a quantised
DiT to bf16 is the suspected cause of a host out-of-memory error, so the
quantised weights stay resident. The model page records its renders with
the expansion on, and each description says so.
Assisted-by: Claude:claude-sonnet-5-5 [Bash]
* feat(gallery): add pruned FL2VA Q4 MiniMax-H3 for sd.cpp
Add minimax-h3-fl2va-pruned-q4-ggml, the 11.4 GB pruned FL2VA
counterpart of the existing pruned REF2VA entry.
Assisted-by: Claude Code:claude-sonnet-5-5 [rc]
---------
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
Document the failed and cancelled outcomes of a response.create that
waits for the preceding input_audio_buffer.commit.
Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
A client response.create waits for the in-flight commit before the LLM
starts. Two exits left the client without a usable outcome:
- When the wait was cancelled (response.cancel, a newer response or
teardown) the response returned with no event at all.
- When the commit produced no user item (transcription error, empty
transcript, voice gate rejection) the LLM ran on a history that
lacked the turn.
Send response.created and response.done with status cancelled for the
first case. For the second, do not run the LLM: send response.done with
status failed and an input_not_committed error. The commit still
reports its own cause. A response.create with no commit in flight is
unchanged. Add specs for STT error, empty transcript, cancel and
supersede during the wait.
Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
A client response.create waited on a separate mutex-guarded channel
that only the last client commit recorded, so it ignored in-flight VAD
commits. The commit chain already ends in session.commitTail, whose
done channel closes after every earlier commit has finished.
Read the tail under commitOrderMu when the response is issued and wait
on it. This drops the extra mutex, the clientCommitDone field and the
slotSink callback of issueCommit, and the response is now ordered
against both commit producers. Fix the commitSlot.done comment: it
closes when the commit function returns, not when the user item is
appended. Add a spec for commit, commit, response ordering.
Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Poll the event count during the held transcription instead of checking\na captured value. Document the client commit and response ordering.
Assisted-by: Codex:gpt-6
Signed-off-by: nexxtmobile.de <kai@nexxtmobile.de>
(cherry picked from commit 034086d3b0)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
With client-driven turns (turn_detection null, e.g. a client VAD / speaker
gate) the client sends input_audio_buffer.commit and response.create
back-to-back. The response.create triggered the LLM immediately while the
commit's transcription was still in flight, so the LLM (fast) won the race
and answered an empty transcript; the response on the real transcript was
then discarded ("skipping response: turn context cancelled during
transcription").
Record the in-flight client commit's slot done channel (closed once the
commit has transcribed and appended its user item) and make the client
response.wait for it before the LLM starts, so the LLM only runs after the
STT is complete.
- issueCommit gains a slotSink callback (slot claim + issue stay under one
lock); the VAD commit path passes nil.
- The client commit / response.create handler cores are extracted to
issueClientCommit / issueClientResponse so the real path is unit-testable.
- Regression specs drive the real issue path with a held transcription: the
LLM must not start until the STT completes, and it must see the user's
transcript.
Signed-off-by: nexxtmobile.de <kai@nexxtmobile.de>
(cherry picked from commit 10555084bf)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
A VAD commit that is skipped because interrupt_response is false runs
with an already cancelled response context. Sound detection used that
context, so the model call failed and the turn never got its
sound-detection event. Run it under the session context, like the
transcription and the voice gate, so overlapping turns keep their
sound events.
Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Keep active responses running when VAD interruption is disabled. Skip
an overlapping automatic response while preserving its transcript and
conversation history. Default interruption and explicit client actions
keep their existing behavior.
Add regression coverage for both VAD modes and document skipped turns.
Assisted-by: Codex:gpt-6
(cherry picked from commit f54023a2f9)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Remove a stray blank line in backend.proto and group the imports in
pkg/motion/validation.go. Decide the motion usecase guess from the
backend capability registry instead of a hardcoded backend name, so
variant builds of the backend resolve too. Declare the weights licence
in the gallery entry, as stated on the model card.
Assisted-by: Claude Code:claude-sonnet-5-5
A model serves one motion session at a time, so a single user could hold
several models for as long as the session stays active. Limit each user
to four sessions and free the slot on close. The one-session-per-model
rule and the global limit are unchanged.
Assisted-by: Claude Code:claude-sonnet-5-5
CSRFMiddlewareWithCORS changed CSRF handling for every route and, with a
wildcard allowlist, disabled the Sec-Fetch-Site check. Restore the
original CSRFMiddleware: clients that authenticate with validated header
credentials, or with a single-use WebSocket ticket, are already exempt,
so the motion routes need no relaxation.
The motion WebSocket upgrade no longer reads the CORS allowlist to
decide whether a cross-origin upgrade is acceptable. It accepts one only
with a valid ticket or validated header credentials; everything else
gets the same-origin default.
Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
FLAG_MOTION and FLAG_UPSCALE were both declared as 1 << 26 on separate
branches, and a merge combined the two const blocks without a conflict.
Add a spec that checks every flag from GetAllModelConfigUsecases() is a
single bit that no other flag uses, so a collision fails the tests.
FLAG_MOTION itself moves to 1 << 27.
Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Add the GEM-X native backend with live pose and root displacement output, bounded duplex WebSocket streaming, session authorization, metrics and usage accounting. Share the embedded bidirectional RPC channel adapter across streaming backends.
Include backend build and gallery integration, API documentation, schemas and tests.
Assisted-by: Codex:gpt-6
Signed-off-by: Richard Palethorpe <io@richiejp.com>
(cherry picked from commit 016dc3d71a)
A queued import goes through the gallery installer, so it persists
recommended inference defaults and installs the backend when automatic
backend installation is on, while the synchronous write stores the body
as posted. Say so in the docs and the endpoint description, list the
reference forms that are queued (URLs, hf:// and owner/repo, artifacts),
and regenerate the swagger files.
Assisted-by: Claude:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
The model editor polled a queued import forever: every status error was
swallowed and retried, and nothing stopped the loop when the editor
closed. A job the server had already dropped left the editor stuck on
"saving".
Move the job-status reading into a shared helper used by the import page
and the editor. It classifies cancelled, failed (error field or an
"error:" message) and finished jobs once, gives up after 10 consecutive
status failures or 2 hours of waiting, and stops on unmount. The editor
now shows the failure instead of looping.
Assisted-by: Claude:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Cancelled gallery jobs also set processed. Handle cancellation before
completion so imports do not announce success or open a missing model.
Assisted-by: Codex:gpt-6
Signed-off-by: Plamen K. Kosseff <p.kosseff@gmail.com>
(cherry picked from commit 0f54bdcb38)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Review round at aec658a99:
- [P1] acquisition is decided by PrimaryArtifactSpec: managed artifacts
(hf:// repos, bare owner/repo, explicit artifacts:) enqueue with no
file entries so the worker's InstallModel runs the artifact binding
and repository materializer — flattening them into file entries routed
repositories through the single-file downloader and skipped the
materializer. Plain remote files keep the legacy file path; malformed
references fail at admission. Covered by endpoint specs and a
worker-level materializer spec.
- [P2] both ModelEditor create paths wait on the returned job before
announcing success and navigating; download failures surface as save
errors instead of after a success toast.
Assisted-by: Claude:claude-fable-5
Signed-off-by: Plamen K. Kosseff <p.kosseff@gmail.com>
(cherry picked from commit 742971c2c4)
- swagger: annotate POST /models/import (untyped body: documenting it
as config.ModelConfig would inline the whole type graph, ~5.5k
generated lines; the description spells out the dual response) and
regenerate the spec
- tests: pin the admission contract of the queued branch — job id +
status URL, job queryable before the worker runs, config not written
at admission, preload-matching filenames, checksum passthrough, and
the untouched synchronous path
- docs: the Writing YAML section explains when Create queues and what
the API answers in each case
Assisted-by: Claude:claude-fable-5
Signed-off-by: Plamen K. Kosseff <p.kosseff@gmail.com>
(cherry picked from commit aec658a998)
Manual /models/import with downloadable assets ran synchronously; it now
enqueues like a gallery install and the import UI polls the job progress.
Assisted-by: Claude:claude-fable-5
Signed-off-by: Plamen K. Kosseff <p.kosseff@gmail.com>
(cherry picked from commit 7420e2e71b)
The Qdrant client requires a newer lufia/plan9stats, so go.mod already
pins it. Remove the entries for the superseded version.
Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
With DOT, EUCLID or MANHATTAN the backend returns Qdrant's raw scores,
not similarity-like values. Router, face and voice stores compare
against cosine thresholds, so log a warning at load when another
metric is configured, and document the behaviour on the stores page.
Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
StoresSet sent the whole batch in one Upsert under a single
request_timeout_ms, so a large batch could hit the timeout even though
every point was fine. Send at most 500 points per request and apply
the timeout to each request.
Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
A TLS 1.3 floor rejects Qdrant endpoints that only speak TLS 1.2, such
as some managed deployments and proxies. Use TLS 1.2 as the minimum.
When Load fails after the client was created, it closed the client but
left it set, so Free closed it a second time. Clear it after closing.
Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Store loading picked the backend and the loader's process slot in one
function, which let an explicit `backend` argument collide with the
store's own config and made traces report a fixed "local-store".
This changes /stores/* behaviour for every store:
- An explicit `backend` that differs from the one named in the store's
model config now gets its own loader slot, `<store>@<backend>`, so it
no longer reuses (or evicts) the process of the configured backend.
Naming the configured backend, or one of its aliases, keeps the
store's own slot.
- When the config names a different backend than the explicit one, the
config's `options` are not passed on, since they are meant for the
configured backend. A config without `backend:` still passes its
options to the explicit backend.
- Backend names are compared and reported in canonical form (aliases
resolved, lower case) through the new model.CanonicalBackend, and
traces record the backend that was actually used instead of always
"local-store".
Backend loading itself uses CanonicalBackend for alias handling; type
aliases keep their existing effect.
Signed-off-by: Anush008 <mail@anush.sh>
Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Titles and prepositions in front of a name are taken as well, and using
both directions on one name group swallows a following number.
Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
It shared order 215 with extend_to_next_word, so the settings UI had no
defined order between the two. Move it to 216 and shift the protected
terms fields. Pin that a number in front of a hit is never taken.
Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
The mirror of extend_to_next_word. A name model often tags the surname with
high confidence and scores the first name in front of it below min_score,
so "Gast Greta Waldmeister" became "Gast Greta [REDACTED:ner:LASTNAME]".
extend_to_previous_word lists entity groups whose accepted detections also
cover the word directly before them on the same line. The preceding word
must consist of letters (plus '-' and the apostrophe): a word with digits -
an amount, a date, a room number - is never taken, nor is a protected term,
and the search never crosses a line break.
Docs: operations/middleware.md and advanced/model-configuration.md.
Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Stefan Walcz <stefan.walcz@walcz.de>
(cherry picked from commit b1845b8bed)
Protected terms apply to the NER tier only and not to a pattern
detector; extend_to_next_word also takes numbers and dates after a hit.
Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
A protected term that is one half of "Anna-Lena" or "O'Brien" is not a
whole word: shielding it left the term in the clear next to a masked
rest. Hyphen and apostrophe joiners between letters now count as
in-word, as they do for the word extensions.
When a detection spanned the placeholder and the name after it, the
space beside the protected term stayed inside the mask. Trim whitespace
from the surviving pieces.
Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Look up the compiled matcher by comparing the raw term list instead of
normalising, sorting and joining it on every call, compile outside the
lock, and evict the least recently used entry instead of the oldest.
Whitespace inside a term now also matches no-break, narrow no-break and
other Unicode spaces, so "Hotel Seeblick" written with a no-break space
is still protected. Close the term file with the repo's errcheck-clean
idiom, and note that the multi-segment path scans the joined document.
Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Term-file reloads retain each compiled matcher indefinitely. Limit the
cache to 32 lists and verify that evicted lists still match on reload.
Assisted-by: Codex:GPT-6
Signed-off-by: Stefan Walcz <stefan.walcz@walcz.de>
(cherry picked from commit dce0eacfcf)
A name model reads many business names as surnames. Measured with
privacy-filter-multilingual on 24 synthetic German receipts and
invoices: the supplier name ("Hotel Seeblick", "Café Lindenhof", ...)
was masked as LASTNAME (0.78-0.98) or FIRSTNAME in 9 of 12 receipts
that carried one. The supplier is exactly what the upstream model needs
to categorise a receipt, and no score floor helps at those confidences.
pii_detection.protected_terms (inline) and
pii_detection.protected_terms_files (files inside the models path, one
term per line, '#' comments, re-read when size or mtime change) list
values that must pass unchanged. Each whole-word, case-insensitive
occurrence is replaced by a neutral placeholder before the detector
runs; every detection is mapped back onto the original text with the
placeholder parts cut out, so a protected term is never masked or
blocked, the detector never sees it as context, and event offsets keep
addressing the original text. A word extension (extend_to_next_word)
is dropped rather than run into a protected term.
Terms shorter than three characters are ignored. A missing or
unreadable file is logged and skipped: fewer protected terms means more
masking, never less. Paths are verified to stay inside the models path.
Opt-in, empty by default. Covered by Ginkgo specs in
core/services/routing/pii/protected_test.go (detector-visible text,
exact restoration, straddling detections, whole-word/case/line-wrap
matching, the segment path, file loading and reload), a YAML round-trip
spec, registry entries and the middleware/model-configuration docs.
Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Stefan Walcz <stefan.walcz@walcz.de>
(cherry picked from commit 01f71772a5)
The client secret is sent to token_url, so a plain http endpoint would
expose it. Reject non-https URLs except for loopback hosts, which keeps
local identity providers and tests working. Document the restriction,
recommend client_secret_env over a literal secret, and note that rotating
a *_env secret needs a restart because sessions and cached tokens are
keyed by configuration, not by the environment value.
Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
JSON log handlers bypass String and serialize exported credentials.
Implement LogValue so remote server entries use the same redacted
representation in text and JSON logs. Extend the regression test
to cover both handlers.
Assisted-by: Codex:GPT-6
(cherry picked from commit f93781c307)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
A slow failed refresh can cross the token's safety margin.
Check the current time before reusing the token and start retry
backoff when the failed fetch finishes. Apply backoff before the
first successful fetch too.
Add regression tests for these cases and document per-process caches.
Assisted-by: Codex:GPT-6
(cherry picked from commit a849e872ec)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Remote MCP servers could only be authenticated with a static bearer
token. Deployments that put their MCP servers behind an OIDC provider
(authentik, Keycloak, Entra ID, ...) issue short-lived service tokens
instead, so LocalAI had to be given a long-lived key or could not
connect at all.
A remote server entry now accepts an optional "oauth2" block
(token_url, client_id/client_id_env, client_secret/client_secret_env,
scopes, endpoint_params). The token is fetched with the
client_credentials grant by an http.RoundTripper installed on the MCP
client's HTTP client, so streamable HTTP and SSE transports both use it.
The token is cached and refreshed at half of its lifetime; if a refresh
fails the current token is kept until 30s before expiry, after which
requests fail with an error instead of going out unauthenticated.
"token" and "oauth2" are mutually exclusive and inconsistent blocks are
rejected when the model config is loaded. Static tokens behave as
before. MCPRemoteServer now implements fmt.Stringer so the existing
debug logs that print whole server entries no longer write the token
or client secret in clear text.
Signed-off-by: Stefan Walcz <stefan.walcz@walcz.de>
Assisted-by: Claude Code:claude-opus-5-5
(cherry picked from commit 954b9d25f7)