mirror of
https://github.com/mudler/LocalAI.git
synced 2026-09-18 10:53:22 -04:00
8bc2fcdf1fa3cf393dbd998b5cc54f2b09365f56
217
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
15de88d361 |
fix(ui): test backend actions through their menu (#12069)
The node detail redesign moved backend operations into an action menu. Four existing specs still search for the removed direct buttons, so the UI E2E workflow fails consistently on master. Open the backend action menu before checking or activating its items. Assisted-by: Codex:gpt-5 Co-authored-by: Ettore Di Giacinto <mudler@localai.io> |
||
|
|
d5256a5584 |
fix(ui): restore node operation controls (#12068)
The node restructure hid backend logs and split related controls across inconsistent layouts. Restore contextual log actions and align the detail page with the fleet dashboard. Make multi-node selection clear and accessible. Assisted-by: Codex:gpt-5 Playwright ESLint Co-authored-by: Ettore Di Giacinto <mudler@localai.io> |
||
|
|
997d403de4 |
feat(nodes): add fleet operations dashboard (#12046)
* feat(nodes): report CPU telemetry Assisted-by: Codex:gpt-6 * feat(nodes): add fleet view utilities Assisted-by: Codex:gpt-6 * feat(nodes): add fleet operations dashboard Replace the panel roster with aggregate capacity gauges, fleet filtering and selection, bounded bulk actions, and an on-demand node inspector. Extend node details and distributed-mode documentation with CPU and models-disk telemetry. Assisted-by: Codex:gpt-6 * fix(nodes): harden fleet lifecycle actions Assisted-by: Codex:gpt-6 * fix(nodes): restore compact fleet composition Keep fleet health, capacity, and attention in one compact overview at ordinary desktop widths. The inspector now overlays the roster until the workbench can preserve a useful table beside it. Assisted-by: Codex:gpt-6 * feat(nodes): add accessible running models workbench Assisted-by: Codex:gpt-6 * fix(nodes): correct model view ARIA links Keep each tab panel available for its controlling tab while native hidden state removes inactive content from accessibility navigation. Model controls now expose only supported state and valid inspector relationships. Assisted-by: Codex:gpt-6 * fix(nodes): align lifecycle and capacity states Pending nodes now expose approval wherever node actions appear, while other lifecycle controls follow the server transition rules. Capacity totals exclude incomplete readings so missing availability remains unknown. Assisted-by: Codex:gpt-6 * fix(nodes): restore approved dashboard composition Assisted-by: Codex:gpt-6 * fix(nodes): integrate operate navigation Assisted-by: Codex:gpt-6 * fix(nodes): restore low density fleet view Assisted-by: Codex:gpt-6 * fix(nodes): preserve complete operate menu Assisted-by: Codex:gpt-6 * fix(nodes): preserve inspector workspace height Assisted-by: Codex:gpt-6 * fix(nodes): restore standard operate navigation Assisted-by: Codex:gpt-6 * feat(ui): add collapsible console rail Assisted-by: Codex:gpt-6 * feat(nodes): stop models from fleet view Assisted-by: Codex:gpt-6 * fix(nodes): make inspector a full height drawer Assisted-by: Codex:gpt-6 * fix(model): stop mixed local and remote placements Assisted-by: Codex:gpt-6 * fix(ui): announce action menu navigation Assisted-by: Codex:gpt-6 * fix(nodes): keep inspector within viewport Assisted-by: Codex:gpt-6 * fix(ui): preserve focus across model actions Assisted-by: Codex:gpt-6 --------- Co-authored-by: Ettore Di Giacinto <mudler@localai.io> |
||
|
|
6983477a71 |
fix(agent-ui): keep chat open for status
Agent Status replaced the chat route and unmounted its EventSource. Any response still in flight could then disappear from the conversation.\n\nOpen status in a separate tab so the chat keeps its live connection until the response completes.\n\nAssisted-by: Codex:gpt-5 [eslint] Signed-off-by: Ettore Di Giacinto <mudler@localai.io> |
||
|
|
db09452d54 |
feat(tts): support multi-reference personalities
Saved profiles previously resolved to one audio path and transcript, so cloning backends could not use several examples of one personality. Store ordered audio and transcript pairs while preserving the legacy first-reference fields. Fish Speech and audio.cpp receive all pairs, including on distributed workers. Other backends retain their single-reference behavior. Assisted-by: Codex:gpt-5 Signed-off-by: Ettore Di Giacinto <mudler@localai.io> |
||
|
|
ddda0436a3 |
fix(ui): exclude navigation from the TTS mock
The API mock also matched navigation to /app/tts and returned a WAV download instead of the React page. Let non-POST requests reach the test server. Assisted-by: Codex:gpt-5 [Playwright] Signed-off-by: Ettore Di Giacinto <mudler@localai.io> |
||
|
|
1bcbff586b |
fix(ui): target the TTS input in history tests
TTS instructions add a second textarea to the page. Target the speech input by its placeholder so the history test does not depend on the page having one textarea. Assisted-by: Codex:gpt-5 Signed-off-by: Ettore Di Giacinto <mudler@localai.io> |
||
|
|
919b5c96fa |
feat(ui): add per-request TTS instructions
Let studio users guide speech delivery for backends that support request instructions. Blank guidance stays out of requests and media history. Assisted-by: Codex:gpt-5 Signed-off-by: Ettore Di Giacinto <mudler@localai.io> |
||
|
|
e588d463dc |
fix(ui): preserve percent signs in route parameters (#11883)
fix(ui): preserve decoded route parameters React Router already decodes dynamic path segments before exposing them through useParams. Decoding those values again crashes pages for names containing a literal percent sign and mutates escape-like substrings. Use route parameters as-is, encode the model editor API path at the outbound boundary, and cover all affected pages with Playwright. Fixes #11882 Assisted-by: Codex:gpt-5 eslint playwright Signed-off-by: QiuLG <l237455523@outlook.com> |
||
|
|
428898373a |
chore(deps): bump the npm_and_yarn group across 1 directory with 2 updates (#11957)
Bumps the npm_and_yarn group with 2 updates in the /core/http/react-ui directory: [hono](https://github.com/honojs/hono) and [js-yaml](https://github.com/nodeca/js-yaml). Updates `hono` from 4.12.34 to 4.13.5 - [Release notes](https://github.com/honojs/hono/releases) - [Commits](https://github.com/honojs/hono/compare/v4.12.34...v4.13.5) Updates `js-yaml` from 3.14.2 to 3.15.2 - [Changelog](https://github.com/nodeca/js-yaml/blob/3.15.2/CHANGELOG.md) - [Commits](https://github.com/nodeca/js-yaml/compare/3.14.2...3.15.2) --- updated-dependencies: - dependency-name: hono dependency-version: 4.13.5 dependency-type: direct:production dependency-group: npm_and_yarn - dependency-name: js-yaml dependency-version: 3.15.2 dependency-type: indirect dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
de563f17b5 |
fix(ui): omit GPU recommendations that do not fit (#11945)
When no sampled candidate fits GPU memory, ranking falls back to the oversized pool and labels its first model Best fit. Keep GPU picks within the existing 95% budget and hide the section when no candidate qualifies. Remove static GPU starter picks so Home cannot reintroduce the same error. Add browser regressions for both sections and document the empty result. CPU fallback behavior stays unchanged. Assisted-by: Codex:gpt-6 [Codex] 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> |
||
|
|
18d20239df |
fix(ui): keep trace expansion stable during refresh
Squashed merge of #11278. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> |
||
|
|
718357219b |
fix(ui): send collection intervals as numbers
Squashed merge of #11819. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> |
||
|
|
1479aef8cd |
fix(deps): upgrade path-to-regexp to 8.4.0 (CVE-2026-4926)
Squashed merge of #11633. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> |
||
|
|
ee2920d619 |
chore(deps): bump the npm_and_yarn group across 1 directory with 3 updates (#11869)
Bumps the npm_and_yarn group with 3 updates in the /core/http/react-ui directory: [@humanfs/node](https://github.com/humanwhocodes/humanfs/tree/HEAD/packages/node), [fast-uri](https://github.com/fastify/fast-uri) and [qs](https://github.com/ljharb/qs). Updates `@humanfs/node` from 0.16.7 to 0.16.8 - [Release notes](https://github.com/humanwhocodes/humanfs/releases) - [Changelog](https://github.com/humanwhocodes/humanfs/blob/main/packages/node/CHANGELOG.md) - [Commits](https://github.com/humanwhocodes/humanfs/commits/node-v0.16.8/packages/node) Updates `fast-uri` from 3.1.5 to 3.1.7 - [Release notes](https://github.com/fastify/fast-uri/releases) - [Commits](https://github.com/fastify/fast-uri/compare/v3.1.5...v3.1.7) Updates `qs` from 6.15.3 to 6.16.0 - [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md) - [Commits](https://github.com/ljharb/qs/compare/v6.15.3...v6.16.0) --- updated-dependencies: - dependency-name: "@humanfs/node" dependency-version: 0.16.8 dependency-type: indirect dependency-group: npm_and_yarn - dependency-name: fast-uri dependency-version: 3.1.7 dependency-type: indirect dependency-group: npm_and_yarn - dependency-name: qs dependency-version: 6.16.0 dependency-type: indirect dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
99a2e855a8 |
chore(deps): bump the npm_and_yarn group across 1 directory with 2 updates (#11835)
Bumps the npm_and_yarn group with 2 updates in the /core/http/react-ui directory: [react-router](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router) and [browserslist](https://github.com/browserslist/browserslist). Updates `react-router` from 7.18.1 to 7.18.2 - [Release notes](https://github.com/remix-run/react-router/releases) - [Changelog](https://github.com/remix-run/react-router/blob/react-router@7.18.2/packages/react-router/CHANGELOG.md) - [Commits](https://github.com/remix-run/react-router/commits/react-router@7.18.2/packages/react-router) Updates `browserslist` from 4.28.2 to 4.28.8 - [Release notes](https://github.com/browserslist/browserslist/releases) - [Changelog](https://github.com/browserslist/browserslist/blob/main/CHANGELOG.md) - [Commits](https://github.com/browserslist/browserslist/compare/4.28.2...4.28.8) --- updated-dependencies: - dependency-name: react-router dependency-version: 7.18.2 dependency-type: direct:production dependency-group: npm_and_yarn - dependency-name: browserslist dependency-version: 4.28.8 dependency-type: indirect dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
7c921aa020 |
fix(ui): omit empty system prompt so model default applies (#11838)
Web Chat Settings left the System Prompt field empty but still treated a blank/whitespace value as an explicit system turn. That satisfied tokenizer chat templates' messages[0].role == system check and suppressed the model YAML system_prompt on fresh chats. Omit empty/whitespace system messages in the React and Alpine UIs, strip them server-side, and inject config.SystemPrompt for tokenizer-template models when the request has no real system turn. Fixes #11834 Signed-off-by: lei_lei <96427312+leilei3167@users.noreply.github.com> |
||
|
|
80e3240f2d |
feat(distributed): key scheduling rules by a model alias (#11771)
Node placement and replica rules could only name a model, so an operator who pinned "llama3" to the GPU tier had to rewrite the rule whenever a different model took over that job. An alias already gives a stable name for whichever model serves it, and a rule on that name makes it a deployment slot: repoint the alias and the placement follows. A rule keeps the name the operator chose. Reads resolve that name through the config loader to the model the rule governs, so the reconciler counts, schedules and trims replicas of the target, and the router finds an alias-keyed rule from the target it is already routing. An alias that resolves to nothing governs nothing loadable, so the reconciler skips it and the write paths refuse it. A replica is shared by every name that resolves to it, so only one rule can decide where it runs. The REST and MCP write paths reject a rule whose target another rule already governs. A pair that arrives some other way, such as a seed file or an alias repointed onto a model that already has a rule, resolves in favour of the rule named after the model itself and then the oldest, and the rest are listed as shadowed. The eviction guard is the exception: it matches rules to replicas in raw SQL inside a locking transaction and cannot resolve an alias. It reads a stored target that the reconciler refreshes each tick, and falls back to the rule's own name when that target is empty. Assisted-by: Claude:claude-opus-5 golangci-lint eslint Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Co-authored-by: Ettore Di Giacinto <mudler@localai.io> |
||
|
|
29899cd1e0 |
fix(ui): size model fit against the cluster and move node labels into the selector (#11765)
* fix(ui): move node labels into the scheduling selector field The scheduling page kept a node-label browser open above the rules whether or not anyone was writing one, while the field that actually needs labels, the rule's node selector, was two bare text inputs with no hint of what the cluster reports. The browser is gone. The selector's key input now completes against the label keys the cluster uses, and the value input offers only the values that key takes. The roster already loads for the page, so the suggestions cost no request, and a roster that fails to load costs the admin the hints and nothing else. Suggestions stay suggestions: a key no node reports yet still commits as typed, which is how an admin writes a rule before labelling the nodes for it. Assisted-by: Claude:claude-opus-5 golangci-lint eslint playwright Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix(distributed): size model fit against the cluster, not the frontend The models page asked the frontend how much memory a model may occupy. In distributed mode the frontend is usually a GPU-less pod while every model runs on a worker, so a fleet of GPU nodes was told it could only run the smallest CPU build. The variant picker's fits flag and its auto-selection came from the same place, as did the hardware recommendations. The registry now reports the largest single healthy backend node. The largest node, not the fleet total: a model loads into one node, so four 16GB workers are not a home for a 40GB model. An operator-set VRAM budget caps a node's contribution, because the scheduler refuses a load above that ceiling anyway, and a GPU node beats a CPU node holding more system RAM. GET /api/resources and GET /api/models carry this as an additional cluster object. Their aggregate and ram fields keep reporting the frontend's own hardware, which is what the resource monitor shows. Variant selection judges backends against the union of the capabilities present in the cluster, the way backend discovery already did. Every path degrades to the local host: no cluster object in single-node mode, and none when the registry cannot be read, so a hiccup narrows the answer back to single-node behaviour rather than marking the whole catalog too large. The verdicts now name the node they belong to, since a model fits somewhere or nowhere. Assisted-by: Claude:claude-opus-5 golangci-lint eslint playwright Signed-off-by: Ettore Di Giacinto <mudler@localai.io> --------- Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Co-authored-by: Ettore Di Giacinto <mudler@localai.io> |
||
|
|
e58dabf75f |
feat(ui): add 'Focus mode' option in chat settings to persistently toggle the sidebar auto-collapse (#11750)
Assisted-by: Claude:claude-fable-5 Signed-off-by: Plamen K. Kosseff <p.kosseff@gmail.com> |
||
|
|
8f56e4e042 |
fix(vram): persist remote probe metadata (#11487)
* fix(vram): persist remote probe metadata The startup warmer repeated remote size and GGUF metadata probes after every restart because both caches lived only in memory. Store successful HTTP probes for 24 hours so frequent restarts reuse the prior results. Bound the cache, reject invalid records, and purge it when gallery data changes. Local model files continue to bypass persistence. Assisted-by: Codex:gpt-5 * fix(vram): check temporary file cleanup The lint gate rejects the unchecked cleanup call in the persistent cache writer. Assisted-by: Codex:gpt-5.6 [golangci-lint] * fix(vram): make persistent cache optional Remote metadata probes can transfer enough data that operators need control over disk reuse and startup warming. Gallery autoload now gates both behaviors, and the runtime setting applies changes immediately. Assisted-by: Codex:gpt-5 * fix(ui): expose gallery startup pre-warm The existing gallery autoload setting also gates the startup metadata warmer. Name both effects in Settings so operators can find the requested boot control. Assisted-by: Codex:gpt-5 --------- Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com> |
||
|
|
9d92139de4 |
feat(ui): edit scheduling rules in place (#11667)
* docs(ui): design scheduling rule editing Document the approved in-place rule editing flow and scalable node-label reference for the scheduling view. Assisted-by: Codex:gpt-5 * feat(ui): improve scheduling rule management Add scalable node-label discovery and editable scheduling rules with responsive, accessible controls. Assisted-by: Codex:gpt-5 * chore(ui): ratchet inline style baseline Record the static inline style removed by the scheduling view enhancement. Assisted-by: Codex:gpt-5 --------- Co-authored-by: Ettore Di Giacinto <mudler@localai.io> |
||
|
|
5429f569e0 |
fix(progress): stop status updates throttling downloads (#11661)
* feat(progress): aggregate and coalesce gallery downloads Assisted-by: Codex:gpt-5 Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(ui): show rolling transfer speed Assisted-by: Codex:gpt-5 Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix(ui): preserve legacy import byte labels Assisted-by: Codex:gpt-5 Signed-off-by: Ettore Di Giacinto <mudler@localai.io> --------- Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Co-authored-by: Ettore Di Giacinto <mudler@localai.io> |
||
|
|
ff6043b811 |
fix: upgrade react-router to 7.18.2, 8.3.0 (GHSA-qwww-vcr4-c8h2) (#11644)
* fix: GHSA-qwww-vcr4-c8h2 security vulnerability Automated dependency upgrade by OrbisAI Security Signed-off-by: anupamme <mediratta@gmail.com> * fix: upgrade react-router-dom to 7.18.2 to fully remediate GHSA-qwww-vcr4-c8h2 The prior fix pinned react-router@7.18.2 directly but left react-router-dom at ^7.18.1, which bun resolved to 7.18.1. That package bundles its own react-router@7.18.1 sub-dep, leaving the vulnerable version in bun.lock via the react-router-dom/react-router scoped resolution. Pinning react-router-dom to 7.18.2 and regenerating the lockfile removes all 7.18.1 resolutions. Assisted-by: Claude Code:claude-sonnet-4-6 Signed-off-by: Anupam Mediratta <mediratta@gmail.com> --------- Signed-off-by: anupamme <mediratta@gmail.com> Signed-off-by: Anupam Mediratta <mediratta@gmail.com> |
||
|
|
9ba4bbf9bb |
fix: upgrade ip-address to 10.3.1 (CVE-2026-69192) (#11632)
fix: CVE-2026-69192 security vulnerability Automated dependency upgrade by OrbisAI Security Signed-off-by: anupamme <mediratta@gmail.com> |
||
|
|
4cad809003 |
fix(ci): test stale chunks in split bundle (#11595)
The V8 coverage build inlines every dynamic import, so the stale chunk tests cannot intercept a page chunk. Run those tests against the normal code-split bundle and exclude them from the inlined coverage pass. Assisted-by: Codex:gpt-5 Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com> |
||
|
|
b806b1fec3 |
fix(ui): reload once when a page chunk 404s
A deploy replaces the whole content-hashed asset set at once. A tab holding an older index.html, or one whose request lands on a replica that the rollout has not swapped yet, asks for a page chunk the server no longer has. The dynamic import rejects and React Router's default error boundary replaces the app with "Unexpected Application Error!" until someone reloads by hand. The router now reloads the page itself when a chunk fails to load. index.html is served no-cache, so the reload lands on a self-consistent asset set. A timestamp in sessionStorage bounds this to one reload per 10 seconds, so a chunk that is genuinely gone reaches the error boundary instead of looping forever. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: Claude Code:claude-opus-5[1m] |
||
|
|
cf93c04878 |
fix: tts text wrap (#11576)
Signed-off-by: Nicholas Ciechanowski <nicholas@ciech.anow.ski> |
||
|
|
d10374f849 |
feat(router): make KNN a first-class classifier with a persisted, curated corpus (#10652)
* feat(router): make KNN a first-class classifier with a persisted, curated corpus
Add `classifier: knn` — similarity-weighted voting over labelled
example prompts. Unlike score/colbert it needs no classifier model:
label knowledge lives in a corpus seeded and curated through the
admin API, so routing decisions are deterministic, auditable, and
grounded in graded experience rather than a model's opinion.
Epistemic gate: corpus entries below knn.similarity_threshold cannot
vote; when none clears it the classifier activates no labels and the
router uses the fallback — a prompt unlike all labelled experience is
treated as undecidable, not guessed. Decisions record
nearest_similarity (also on fallback rows) so admins can see how far
the nearest labelled experience was; the Routing tab explains
out-of-corpus fallbacks and shows per-label corpus counts.
Persistence: one JSONL file per router under
<data path>/router-corpus (text, labels, vector, embedder
fingerprint). The file is the source of truth; the local-store index
is rebuilt from it at classifier build time and stays a pure
in-memory index. Entries recorded under a different embedding model
re-embed on load. Also corrects the docs' false claim that
local-store collections persist — the embedding cache never survived
restarts (and still doesn't); the corpus does.
Corpus input is API-only by design (entries may contain example user
content): POST /api/router/{name}/corpus seeds (labels validated
against declared policies, embedded server-side, indexed
immediately), GET .../corpus/stats inspects — label counts only,
entry texts are never returned by any surface — DELETE .../corpus
wipes. Admin-gated like the sibling router endpoints, and exposed as
MCP tools (seed_router_corpus / get_router_corpus_stats /
clear_router_corpus) in both the httpapi and inproc clients with
coverage-test route mappings.
Plumbing: VectorStore gains SearchK (top-K was hardcoded to 1);
local-store gets InsertBatch/Delete as optional fast paths;
RouterConfig gains a knn block (embedding_model, k,
similarity_threshold, vote_threshold, store_name) with meta-registry
fields; the classifier dropdown now offers knn and the
previously-missing colbert; embedding_cache is ignored (with a
warning) for knn — it IS an embedding-KNN lookup; the stale
/api/instructions intelligent-routing entry is rewritten (it
described a classifier that no longer exists); swagger regenerated.
Tests: KNN vote/gate specs with hand-computed vote shares, corpus
manager suite (restart reload without re-embedding, fingerprint
re-embed, dedupe, hostile store names), middleware specs (corpus
routing, gate fallback, config validation, cache-wrap refusal),
corpus endpoint specs pinning the texts-never-returned contract, MCP
catalog + route-mapping gates, and a Playwright spec for corpus
stats and the out-of-corpus decision detail.
Assisted-by: Claude:claude-fable-5 [Claude Code]
Signed-off-by: Richard Palethorpe <io@richiejp.com>
* feat(router): name consulted corpus neighbours in knn decisions
Every knn decision (decision log rows and the /api/router/decide
response) now carries neighbors: the K retrieved corpus entries by
descending similarity - including ones below the epistemic gate, which
is what makes fallback decisions diagnosable - each as {id, similarity,
labels}. The id is the entry's content hash (first 8 bytes of the
SHA-256 of its text, hex): stable across reseeds and re-embeds, and
text-free, so an external platform that seeded the corpus can recompute
text->id on its own copy and bucket decisions by corpus region (per-
region reliability accounting) without corpus text ever leaving the
server. A corrupt index payload surfaces as an id-less neighbour at a
real similarity instead of disappearing.
Assisted-by: Claude:claude-fable-5 [Claude Code]
Signed-off-by: Richard Palethorpe <io@richiejp.com>
* refactor(router): deduplicate knn plumbing and cut corpus hot-path waste
Post-review cleanup of the knn-first-class-router branch; no behaviour
changes on the API surface.
Reuse/altitude:
- RouterKNNConfig.ResolvedStoreName is now the single source of the
router-corpus-<name> default (was hand-derived in four files).
- corpus.ResolveKNNRouter + corpus.Seed carry the shared model
resolution and seed validation; the REST endpoints and the assistant
MCP client are thin transport adapters over them, with sentinel
errors mapped to HTTP statuses at the echo boundary.
- middleware.NewClassifierDeps assembles the classifier dependency set
once for all five entry points (OpenAI, Anthropic, realtime, decide,
corpus) instead of five hand-copied literals.
- router.AllClassifiers feeds both the status endpoint and the
unknown-classifier error, ending the classifier-list drift.
- Per-classifier requirements moved out of validateRouterPolicies into
their buildClassifier arms; the knn arm owns its embedding_cache
opt-out instead of a name-check in the shared wrap tail.
- adminOnly replaces four inline copies of the admin gate in the
middleware routes.
- localVectorStore.Search delegates to SearchK (identical traces).
Efficiency:
- Manager.Add embeds outside the manager mutex and appends to the
JSONL file (O(new) instead of O(corpus) rewrite); a torn tail from a
crash mid-append is tolerated on read and repaired on next write.
- Stats memoises per store keyed on the file's stat fingerprint and no
longer takes the manager mutex, so the 5s status poll stops parsing
vector-laden JSONL and stops blocking behind seeds.
- KNN Classify decodes each neighbour payload once (was twice) and
builds refs and votes in a single pass with one fallback return.
- Corpus file writes fsync before rename/close.
- The corpus manager is built eagerly in newApplication (sync.Once
dropped); test helper dead branch removed.
Assisted-by: Claude:claude-fable-5 [Claude Code]
Signed-off-by: Richard Palethorpe <io@richiejp.com>
* feat(router): bind knn corpus vectors to an embedder fingerprint and fail closed on mismatch
Assisted-by: Claude:claude-fable-5 [Claude Code]
Signed-off-by: Richard Palethorpe <io@richiejp.com>
* chore(mcp): align corpus tool prompts and the mutating-tool safety list
Assisted-by: Claude:claude-fable-5 [Claude Code]
Signed-off-by: Richard Palethorpe <io@richiejp.com>
* feat(proto,backend): report embedding shape from the llama-cpp backend
Assisted-by: Claude:claude-fable-5 [Claude Code]
Signed-off-by: Richard Palethorpe <io@richiejp.com>
* feat(embeddings): Go-side pooling — mean/last/decayed_mean with half-life
Assisted-by: Claude:claude-fable-5 [Claude Code]
Signed-off-by: Richard Palethorpe <io@richiejp.com>
* feat(embeddings): accept chat messages[] and per-request pooling on /v1/embeddings
Assisted-by: Claude:claude-fable-5 [Claude Code]
Signed-off-by: Richard Palethorpe <io@richiejp.com>
* chore(middleware): name the failing fields when post-merge validation 400s
An intermittent post-merge validation failure surfaced as an opaque 400
during integration (pooling scheme mismatch that no client had sent).
Log the model, the request's pooling override, and the merged config's
pooling fields at the failure point so the next occurrence identifies
whether the request or the stored config carried the bad value.
Assisted-by: Claude:claude-fable-5 [Claude Code]
Signed-off-by: Richard Palethorpe <io@richiejp.com>
* fix(embeddings): scheme override must not inherit the config's half-life
A model config defaulting to decayed_mean pooling carries
pooling_half_life_tokens; a request overriding the scheme to mean/last
without its own half-life inherited that value, and post-merge
validation rejected the pair the server itself had assembled. Zero the
inherited half-life when the overridden scheme is not decayed_mean; a
request that explicitly pairs a half-life with a non-decayed scheme
still 400s.
Assisted-by: Claude:claude-fable-5 [Claude Code]
Signed-off-by: Richard Palethorpe <io@richiejp.com>
* fix embedding pooling validation and router bounds
Declare backend embedding layouts and reject incompatible pooling modes. Reset local-store dimensions after a full clear, validate KNN thresholds, and add real backend and store integration coverage.
Assisted-by: Codex:gpt-5
Signed-off-by: Richard Palethorpe <io@richiejp.com>
* ci: run local-store integration tests
Build and install the local-store backend in the Linux test job, then run the existing store integration suite so new specs are discovered automatically.
Assisted-by: Codex:gpt-5
Signed-off-by: Richard Palethorpe <io@richiejp.com>
---------
Signed-off-by: Richard Palethorpe <io@richiejp.com>
|
||
|
|
799cc9f211 |
feat: bound global admission and expose running backend traces (#11560)
feat: bound backend admission and expose running traces Add process-wide backend execution admission without blocking UI or administrative HTTP work. Represent backend operations while they are in flight, surface running traces with immediate log links, and tie streaming admission leases to the gRPC receive lifecycle. Assisted-by: OpenAI Codex: GPT-5 Signed-off-by: Richard Palethorpe <io@richiejp.com> |
||
|
|
0aaff91ebd |
feat(ui): unify model and backend lifecycle (#11548)
* feat(ui): add installed model lifecycle Models now owns catalog exploration and installed runtime controls under one canonical route. URL-owned state keeps lifecycle context recoverable through links and browser history. Assisted-by: Codex:gpt-5 Playwright * feat(ui): add installed backend lifecycle Backends split discovery from backend-binary management. The canonical page now keeps both lifecycle views under one URL-backed shell while it preserves target-node placement. Assisted-by: Codex:gpt-5 Playwright * fix(ui): repair lifecycle state updates Installed models lost distributed refreshes and kept a deleted selection. Backend searches also stopped tracking URL changes, while batch upgrades stopped after their first error. Preserve background refreshes and finish each requested batch action. Drive catalog results from URL-backed state without losing full metadata. Assisted-by: Codex:gpt-5 [Playwright] * feat(ui): make resource pages canonical Replace Host navigation with canonical Models and Backends lifecycle routes, preserve legacy management URLs, and surface shared host capacity on the Operate overview. Assisted-by: Codex:gpt-5 [Playwright] * feat(ui): complete canonical resource lifecycle Finish the responsive list-to-detail behavior, remove the retired Host implementation, and keep Explore focused on discovery while Installed owns destructive actions. Update regression coverage, localization, documentation, and development binding for the canonical resource pages. Assisted-by: Codex:gpt-5 [Playwright] * docs(ui): record the UI design context Record the approved users, brand character, and design principles so future interface work uses the same product direction. Index the context from the repository's agent instructions. Assisted-by: Codex:gpt-5 --------- Co-authored-by: Ettore Di Giacinto <mudler@localai.io> |
||
|
|
88edd7fc7f |
fix(distributed): run cold model loads as durable jobs instead of holding the advisory lock (#11514)
* fix(advisorylock): set statement_timeout alongside lock_timeout
WithLockCtx already overrides a deployment-wide lock_timeout on its
dedicated connection so a blocking pg_advisory_lock() waits its turn
instead of failing with 55P03. statement_timeout aborts that exact same
statement independently, with SQLSTATE 57014, and was not overridden.
Production roles commonly carry statement_timeout=60s. Any guarded
section longer than that (a cold model load stages for tens of minutes)
therefore killed every concurrent waiter:
advisorylock: acquiring lock 9003261067483446873: ERROR: canceling
statement due to statement timeout (SQLSTATE 57014)
Derive it from the same context budget as lock_timeout, with a matching
RESET so the pooled connection is returned clean.
Assisted-by: Claude Opus 5 [claude-code]
* feat(distributed): add ModelLoadJob, the durable cold-load record
A cold load in distributed mode is a long-running background job, but it
was modelled as a synchronous side effect of an inference request: the
whole of it (backend install, multi-GB staging, checkpoint load) ran
inside the per-model advisory lock. Loading a 35.7 GB GGUF held that lock
for ~20 minutes, so every concurrent request for the same model blocked
on pg_advisory_lock and died at the role's 60s statement_timeout.
Introduce the row that lets the lock shrink to a decision. Exactly one
ModelLoadJob may be active per tracking key; that uniqueness — not the
lifetime of a lock — is what de-duplicates concurrent loaders across
replicas. ClaimLoadJob does its read-then-write under the advisory lock
and nothing else: no network, file or gRPC I/O inside the guarded
section, so a claim costs milliseconds no matter how long the resulting
load takes.
LastProgress is a heartbeat rather than a byte counter. A checkpoint load
legitimately moves zero bytes for many minutes, so a reaper keyed on byte
movement would reclaim a healthy job mid-load; byte progress stays the
concern of load_deadline.go. A job whose heartbeat stops for longer than
the orphan window is reclaimable, so a replica killed mid-load cannot
wedge a model permanently.
Failed jobs keep their row for a short grace so an immediately-following
request reports the real cause instead of silently starting a fresh load
of a model that just failed.
No caller yet — the router moves onto this in the next commit.
Assisted-by: Claude Opus 5 [claude-code]
* refactor(distributed): run cold loads as jobs, outside the advisory lock
Route wrapped the entire cold load — node selection, backend install,
multi-GB staging and the remote LoadModel — in the per-model advisory
lock. The lock's job is to de-duplicate concurrent loaders, a decision
that takes milliseconds; holding it for the tens of minutes the resulting
work takes is what turned a dedup mechanism into a cluster-wide outage
for that model.
Split it into a claim and a run. The claim is the only thing left inside
the lock. The run is a background job owned by the claiming replica and
bounded by the same progress-extended deadline as before; every other
request for that model — local or on another replica — attaches as a
waiter and is served the moment the model is ready, with no duplicate
load and no lock contention.
Waiters share one broadcast rather than an ordered queue: they all want
the identical outcome, so ordering them would add fairness machinery that
changes no result. The local channel wakes same-replica waiters instantly
and a 2s DB poll is the authority, because a waiter on another replica
has no channel to close. On wake a waiter re-runs the warm path rather
than trusting the signal — the model may have been evicted in between.
A waiter whose client disconnects returns immediately and the job keeps
running; it belongs to the job record, not to the request. A failure is
recorded on the row so every waiter reports the real cause, and the row
survives briefly so the next request does not read "no job" as "not
loading" and start a duplicate load of a model that just failed.
The runner heartbeats the row on a fixed interval whether or not bytes
are moving, which is what keeps a legitimately silent checkpoint load
from being reclaimed as an orphan. Phase (installing/staging/loading) and
placement ride to the heartbeat on the context, the same seam
load_deadline.go already uses, so single-host paths are untouched.
Non-distributed mode (no DB) keeps the inline load exactly as it was.
Assisted-by: Claude Opus 5 [claude-code]
* feat(distributed): bound the wait for a loading model and answer with progress
A request whose model is cold-loading now attaches to the running job and
is served the moment the model is ready. That wait has to be bounded: a
held HTTP request cannot survive real infrastructure, and an ingress or LB
idle timeout kills a twenty-minute request regardless of what LocalAI
does.
New LOCALAI_MODEL_LOAD_WAIT (default 60s) bounds the CALLER, never the
load — the job keeps running either way. On expiry the request gets 503
with Retry-After and a structured body naming the model, the node, the
phase, byte progress and an ETA. The `error` envelope keeps OpenAI
clients working; `loading` is additive so they ignore it.
The ETA comes from the job's own observed rate and is omitted rather than
guessed until enough bytes have moved for that rate to mean anything: a
confidently wrong ETA on a twenty-minute wait is worse than none.
Retry-After is that ETA when known, clamped to [5s, 300s], and the wait
budget otherwise.
LOCALAI_MODEL_LOAD_WAIT=0 waits unbounded, for deployments with no proxy
in front. Zero in the config struct still means "unset, use the default",
so the CLI records the operator's zero as ModelLoadWaitUnbounded rather
than losing the distinction.
The distributed branch of ModelLoader.loadModel wrapped the router's
error with %s, which flattened it to a string. Use %w: the typed error is
what the HTTP layer keys the 503 off.
Assisted-by: Claude Opus 5 [claude-code]
* feat(api): add GET /api/models/{id}/load-status
A client that receives 503 while a model stages onto a worker needs
somewhere to poll. This returns the same `loading` object the 503 carries
— phase, node, byte progress and ETA — or 404 when no load is running.
Read-only and observability-shaped, so it is deliberately neither
admin-gated nor feature-gated: it explains a 503 the caller just
received, and hiding that behind a per-modality feature would make the
explanation for a failed image request depend on chat permissions. It
also gets no MCP tool, since there is nothing here an admin would manage
conversationally.
Registered on the surfaces from .agents/api-endpoints-and-auth.md: the
swagger block (existing `models` tag, so /api/instructions needs no new
area), the endpoint discovery maps in RegisterLocalAIRoutes, regenerated
swagger, and the distributed-mode docs page. No FLAG_* usecase is
involved, so capabilities.js is unchanged.
Assisted-by: Claude Opus 5 [claude-code]
* feat(ui): show cold-load progress in Chat and retry when the model is ready
A chat request for a model that is still staging onto a worker now gets a
503 carrying live progress instead of an error. Render it: the composer
shows the phase (installing / staging / loading), the node, the percent
and the ETA, then polls load-status and re-sends the request the moment
the model is ready.
Reuses the staging progress idiom the page already had rather than
inventing a second one — the two sources are folded into one
loadProgress, with the load job winning because it is authoritative
across frontend replicas and knows the phase, where the staging operation
only knows about a byte transfer this replica happens to be performing.
Waiting is bounded (three send attempts, ~30 min of polling each), so a
load that never finishes still surfaces as an error rather than as a
spinner nobody questions. An aborted generation stops the polling too.
Assisted-by: Claude Opus 5 [claude-code]
* fix(distributed): check warm-path cleanup errors
The router moved legacy cleanup calls onto newly linted lines. Report
cleanup failures while preserving the fallback to a cold load.
Assisted-by: Codex:gpt-5 [golangci-lint]
---------
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
|
||
|
|
95653f221e |
fix(ui): keep agent import action visible (#11488)
* fix(ui): keep agent import action visible The header hid its full import label after the agent list became non-empty. Hide only the nested file input so users can import more agents. Assisted-by: Codex:gpt-5 * test(ui): match the agent import label The Agents page renders the action as Import. The test searched for Import Agent, so it failed before checking visibility. Mock the observables request to remove backend timing from the fixture. Assisted-by: Codex:gpt-5 --------- Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com> |
||
|
|
5c63969760 |
fix: Show MCP connection errors in the UI (#11495)
* fix(mcp): surface configured server failures Keep model-configured MCP servers visible when discovery or connection setup fails, propagate status through distributed discovery, and let the Chat UI show actionable errors while retrying unavailable servers. Add model-editor metadata for remote and stdio configuration and document the expected format, deployment networking boundary, and alternate MCP scopes. Assisted-by: Codex:gpt-5 Ordino golangci-lint Signed-off-by: Richard Palethorpe <io@richiejp.com> * build(compose): match CUDA development image Configure the API image with the cublas, CUDA 13, auth-tagged build settings used by the local development Makefile invocation, including the 24-way Docker build. Assisted-by: Codex:gpt-5 Ordino Signed-off-by: Richard Palethorpe <io@richiejp.com> * revert: keep host build settings out of compose The CUDA development deployment is managed from ~/docker/localai, not the repository example Compose file. Restore the generic example and keep machine-specific build settings in the host deployment. Assisted-by: Codex:gpt-5 Ordino Signed-off-by: Richard Palethorpe <io@richiejp.com> * fix(docker): exclude local agent artifacts Keep Claude worktrees and locally installed verification tools out of the Docker build context. These host-only directories added roughly 1.9 GB to every root image build. Assisted-by: Codex:gpt-5 Ordino Signed-off-by: Richard Palethorpe <io@richiejp.com> --------- Signed-off-by: Richard Palethorpe <io@richiejp.com> |
||
|
|
9fd7ea7e93 |
i18n(id): translate admin, media, and nav UI strings to Indonesian (#11493)
Signed-off-by: Dedy F. Setyawan <dedyfajars@gmail.com> |
||
|
|
45cb3983ee |
fix(ui): unmerge the class strings that left buttons in browser chrome (#11462)
Eight header controls across seven pages had two or three elements' classes
collapsed into one string. The wrapper or the icon ended up wearing the
button classes, and the buttons themselves were left with no class at all,
so they rendered in the browser's own chrome. Reported on Agent Jobs; the
grep found the rest.
`fas` does not draw anything by itself: it sets
`font-family: "Font Awesome 6 Free"` and weight 900 on whatever carries it,
and the `fa-*` class supplies the glyph via ::before. So
<button className="btn btn-primary fas fa-plus">
renders its own label "New Task" in the icon font, and
<div className="hstack btn btn-primary btn-sm fas fa-edit btn-secondary fa-arrow-left">
<button>Edit</button>
<button>Back</button>
</div>
styles the flex wrapper as a button that is both primary and secondary,
points two glyphs at one ::before, and leaves both real buttons bare.
Fixed, all of them keeping the correct `<i>` child they already had:
- AgentJobs, AgentTaskDetails (x2), AgentCreate - icon classes off the
button.
- AgentTaskDetails, AgentJobDetails - wrapper back to plain `hstack`, and
the two buttons inside each get the variants the wrapper had been
holding. Back is secondary and leads, Edit/Cancel is the emphatic one
and trails, matching every other detail header.
- VoiceLibrary, VoiceProfileCreate - the title `<i>` had swallowed the
action link's classes, so "Create voice" and "Back to library" were
unstyled anchors. Back was also drawing a "+" because it had inherited
fa-plus while its own fa-arrow-left sat up in the title.
- P2P - a stray fa-circle-info on the title icon.
The ninth instance was ImportModel, where this class of bug was first
found. #11461 rewrote that file and landed first, so nothing is left to fix
there.
Guarded by e2e/class-hygiene.spec.js, which reads the source rather than
walking routes: several of these pages need agent or voice data before they
render a header, so a route walk would skip exactly the pages that had the
bug. It fails on an icon-font class outside an `<i>`/`<span>`, on two glyphs
or two button variants on one element, and on a layout wrapper that is also
a button. Font Awesome modifiers (fa-spin, fa-fw, sizes) are excluded, so
the `fa-spinner fa-spin` idiom stays legal.
e2e: 428 passed.
Assisted-by: Claude Code:claude-opus-5[1m] [Read] [Edit] [Bash] [Playwright]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
|
||
|
|
7a7fb00730 |
feat(ui): rebuild the import form on the restyled design language (#11461)
The import page took the new palette in #11305 but kept its old layout, so it stayed a 760px column with the primary action detached from the form it submits. Two of the problems were outright bugs. The Import button carried no className at all, so the page's single most important control fell through to the user-agent button: system chrome, wrong radius, no design-system focus ring. The YAML button carried `fas fa-save fa-upload`, which sets Font Awesome as the button's own font family (its label text inherits it) and points two glyph classes at one ::before. On the layout: `page--narrow` is documented for "forms / single-record edit views", and in Advanced mode this page held a URI field, a six-section format guide, ten modality chips, nine preference fields, a key-value repeater and a YAML editor at `calc(100vh - 400px)`. The width was the symptom; one column was the disease. - `page--medium` with a work column and a format reference beside it. The reference answers the only question a first-time admin has and used to sit behind a chevron, closed by default. Below 1024px it becomes a disclosure rather than disappearing. - The source field is the hero: monospace, because it holds something you paste, and it carries its own Import button. That removes the hidden aria-hidden submit button that existed only because the real action sat outside the form. - Simple and Advanced are gone. They were ~80% the same surface, and the overlap cost a mode switch, a localStorage key and a three-button Keep/Discard/Cancel dialog whose only job was protecting state that switching modes would hide. One form with a collapsible options panel hides nothing, so none of it is needed. What genuinely differs is the kind of input, which is now the two tabs: a source, or YAML. - The size/VRAM estimate reports under the field that produced it instead of as a banner above the page header, and an import in flight gets the progress, phase and byte counts the poller already returned and the old status card threw away. - ModalityChips resolves its labels through the same `modality.*` keys as the dropdown it filters. It hardcoded English shorthand, so one modality carried two names on one screen ("Speech" on the chip, "Speech recognition" on the group it scrolled to) and seven locales had neither. Its inline styles and its pill radius move onto the design system. - Three inline styles go, including both conditional-padding hacks; the only one left is the progress bar's runtime width. Baseline 538 -> 535. Docs updated in the same change: the WebUI section described a Simple and an Advanced mode and told the reader to "Toggle to Advanced Mode". e2e: 426 passed. The mode-switch suite is replaced by one covering the tabs and the disclosure, and a new layout suite pins the width, the styled primary action, the absence of an icon-font button, the reference column at both widths, and the estimate's position. Assisted-by: Claude Code:claude-opus-5[1m] [Read] [Edit] [Bash] [Playwright] Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Co-authored-by: Ettore Di Giacinto <mudler@localai.io> |
||
|
|
6cf2e97868 |
chore(deps): bump dompurify from 3.4.12 to 3.4.13 in /core/http/react-ui in the npm_and_yarn group across 1 directory (#11425)
chore(deps): bump dompurify Bumps the npm_and_yarn group with 1 update in the /core/http/react-ui directory: [dompurify](https://github.com/cure53/DOMPurify). Updates `dompurify` from 3.4.12 to 3.4.13 - [Release notes](https://github.com/cure53/DOMPurify/releases) - [Commits](https://github.com/cure53/DOMPurify/compare/3.4.12...3.4.13) --- updated-dependencies: - dependency-name: dompurify dependency-version: 3.4.13 dependency-type: direct:production dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
f31c3bbf1b |
feat(i18n): add pt-BR translation (#11427)
Adds a complete Brazilian Portuguese (pt-BR) translation for the LocalAI WebUI across 14 namespaces with full key parity against the English locale, including modelEditor.json. Registers pt-BR in SUPPORTED_LANGUAGES with the code 'pt-BR', name 'Português (Brasil)' and flag 'BR'. Brand/model/product names and technical identifiers are kept untranslated, matching the existing locale conventions. Assisted-by: opencode:deepseek-v4-flash-free python3 Signed-off-by: Matheus C. França <matheus-catarino@hotmail.com> |
||
|
|
ab52813342 |
feat(modelartifacts): support bounded parallel Hugging Face file downloads (#11162)
* feat(modelartifacts): support bounded parallel Hugging Face file downloads Closes #11114. Snapshot materialization fetched every file through the sequential executor in DownloadFilesWithContext, so a repository split into many shards spent most of its wall clock in per-file request latency rather than moving bytes. Add DownloadFilesWithConcurrency, an errgroup with SetLimit, and keep DownloadFilesWithContext as a wrapper that passes a limit of 1. That leaves the two non-artifact callers (core/gallery and the model config loader) on exactly the path they had: tasks still run in slice order, and the first failure still returns before any later task starts. Only whole files run in parallel. A single file is never split, so the .partial resume machinery and the per-file SHA check in downloadTaskWithRetry are untouched. Two details the parallel path forced: - completedBytes becomes an atomic.Int64. Several AfterDownload hooks add to it while other files' progress callbacks read it; without this the race detector reports three races on the new specs. - The caller's status callback is serialized. The sequential path gave it an implicit guarantee of never being entered twice at once, and it belongs to the caller, so the executor keeps that promise rather than pushing locking onto every caller. AfterDownload is deliberately not serialized -- it does the verify-and-promote work that parallelism exists to overlap. Manifest order needed no work: each hook already writes its own manifest.Files slot by snapshot index, so entries stay in snapshot order whatever the completion order. A spec now pins that. The default is 1, unchanged behaviour. A shared models volume is often the bottleneck rather than the link, so raising it is a deployment decision; --artifact-download-concurrency and LOCALAI_ARTIFACT_DOWNLOAD_CONCURRENCY expose it on both `run` and `models install`. Not done here, per the issue: no chunk-level parallelism within a single file, and no throughput measurements across concurrency 1/2/4/8 -- that needs a representative sharded repo and a real link. Assisted-by: Claude:claude-opus-5 go-test gofmt Signed-off-by: Adira Denis Muhando <dennisadira@gmail.com> * feat(modelartifacts): expose download concurrency in settings Follow-up to review feedback on #11162: - The CLI flag and docs no longer describe the limit as Hugging Face specific. It applies to any artifact source, as @mudler pointed out. - artifact_download_concurrency is now a persisted runtime setting and is editable from the WebUI, so it can be changed without a restart. The manager's limit becomes an atomic.Int64 behind SetDownloadConcurrency, because a live runtime setting can be updated while a materialization is already in flight. Injected materializers stay compatible through an optional setter interface, so a manager that does not implement it is simply left alone. Verified before taking this on: go build, go vet and go test -race all pass for pkg/modelartifacts, pkg/downloader and core/config. The React UI builds with vite, artifact_download_concurrency is present in the built Settings chunk, and eslint reports the same 8 pre-existing warnings on Settings.jsx as it does without the change. Implementation contributed by localai-org-maint-bot on the review thread; reviewed, verified and signed off by me. Assisted-by: Codex:gpt-5 Assisted-by: Claude:claude-opus-5 go-test vite eslint Signed-off-by: Adira Denis Muhando <dennisadira@gmail.com> --------- Signed-off-by: Adira Denis Muhando <dennisadira@gmail.com> Co-authored-by: localai-org-maint-bot <bot-opensource@localaisrl.com> |
||
|
|
a77780ad14 |
feat(gallery): fall back to mirrors and a cached index when the primary source fails (#11389)
* feat(version): include OS and arch in the outbound User-Agent
Registries and galleries already receive LocalAI/<version>; adding the
platform follows ordinary client convention and discloses nothing a
registry cannot infer from the manifest it is asked for.
Updates the User-Agent note in docs/content/getting-started/models.md,
which documented the old format.
Assisted-by: Claude:claude-opus-5 [go vet] [go test]
* feat(downloader): identify LocalAI on outbound requests
pkg/oci has always sent a User-Agent; the downloader sent none, so gallery
reads, model-file downloads, resume probes, content-length probes and the
HuggingFace safety scan all went out as a bare Go HTTP client, unattributable
to LocalAI by the hosts serving them.
HuggingFaceScan moves off the client's Get shorthand to an explicit request
for the same reason — the shorthand gives no place to hang a header.
Extends the User-Agent note in docs/content/getting-started/models.md, which
claimed the header was sent only to Ollama and OCI registries.
Assisted-by: Claude:claude-opus-5 [go vet] [go test]
* feat(gallery): add a mirrors list to gallery configuration
Mirrors are an availability fallback, tried in order only after the primary
URL fails. omitempty keeps existing configurations byte-identical.
The slice makes config.Gallery non-comparable with ==, which broke the two
slices.Equal callers in the runtime settings registry. Replace them with an
explicit Gallery.Equal / GalleriesEqual so a gallery list that differs from
the baseline only by its mirrors still counts as env/CLI-set. Equal compares
the Verification block by value; == compared it by pointer identity, which
called two structurally identical policies different.
Assisted-by: Claude:claude-opus-5 [go vet] [go test]
* fix(downloader): treat an HTTP error status as a failed read
ReadWithCallback handed the response body to its callback whatever the
status was, so a 404 page or a 502 from a CDN arrived as if it were a
gallery index or a model config: it parsed to nothing, got cached for an
hour, and no caller could tell the source had been down. DownloadFile has
always checked the status; this path never did.
Mirror fallback depends on it — a source that answers with an error page
has to count as unreachable, or the next candidate is never tried.
Assisted-by: Claude:claude-opus-5 [go vet] [go test]
* feat(gallery): fall back to mirrors when the primary source fails
Candidates are tried primary-first with a bounded timeout each, and a
source that just failed is skipped for a cooldown so a dead host is not
re-dialled on every listing. When every candidate is in cooldown they are
all tried anyway: refusing to serve a gallery we might be able to reach is
worse than one slow request.
The one-hour index cache is untouched and stays keyed on the gallery's own
identity, so a mirror-served fetch fills the entry the primary would have.
No SSRF validation is applied to the candidates. validateGalleryConfigURL
guards GetGalleryConfigFromURL because that URL arrives in a request body;
mirrors come from the operator's gallery configuration, the same place the
primary has always come from, and the index fetch has never validated the
primary. Validating mirrors while the primary goes unchecked would buy
nothing and would break the deployment mirrors exist for — an index served
from a host on the LAN.
Assisted-by: Claude:claude-opus-5 [go vet] [go test]
* fix(gallery): loosen the mirror fetch timeout and stop blaming the caller
The downloader only ever bounded response headers, never the body, so the
per-attempt deadline added with mirror fallback was the first whole-transfer
timeout this path has had. At 30s the default 2.2 MB index demanded ~75 KB/s
sustained: a rural-DSL, mobile or satellite user who used to wait 60s and
succeed would now fail, and then eat a 10-minute cooldown on a source that
was perfectly healthy. Raised to 120s (~19 KB/s), which no link that could
go on to download a model will miss, and made it a var so a test can shorten
it and prove a hanging candidate is actually abandoned.
Caller cancellation is no longer recorded as a failure of the source.
Unreachable today since getGalleryElements passes context.Background(), but
once a request context is wired through, a browser disconnect would have
blackholed every candidate for ten minutes over something the sources had
no part in.
Also document that mirrors do not cover a .ref gallery URL: the reference is
resolved before mirrors are considered, so a .ref that cannot be fetched
fails the gallery outright. Routing .ref resolution through the candidate
list needs a per-candidate resolve-and-fetch and a decision about cache
identity, which is more than this change should carry.
Assisted-by: Claude:claude-opus-5 [go vet] [go test]
* feat(gallery): serve the last known good index when everything is offline
A successful fetch is cached alongside the models directory and served when
no source is reachable, so an offline or airgapped machine can still list
its gallery. Entries may be stale in that state, and the fallback is logged.
The copy is deliberately kept out of the models directory, where a <name>.yaml
file is read as an installed model's configuration, and is named after a digest
of the gallery URL so the model and backend galleries cannot collide. Writing
it is best effort: a read-only or full disk must not fail a fetch that
otherwise succeeded.
Also corrects the mirror scheme list in the docs: the HuggingFace prefixes are
huggingface://, hf:// and hf.co/, not huggingface:.
Assisted-by: Claude:claude-opus-5 [go vet] [go test]
* fix(gallery): only cache a response that is really a gallery index
The last known good copy was written on any 2xx, before anything looked
at the bytes: the parse only happens later, in getGalleryElements. A
captive portal, a corporate proxy or a CDN error page all answer HTTP 200
with HTML, so any of them could overwrite a good copy. The listing fails
then and there, and the next offline start — the one case this cache
exists for — serves the interception page instead of the gallery it
already had.
Probe the body before persisting it: unmarshal into a []any and keep the
older copy unless the result is a non-empty sequence. An empty document
is rejected too. It parses fine, so a parse-only check would still let a
blank response replace a populated index with one that lists nothing,
which from the user's side is the same outage; and an empty index is
worth nothing offline, so there is no case where caching it beats keeping
what came before. The live body is still returned to the caller — the
probe gates persistence only, and getGalleryElements remains the thing
that reports a real parse failure.
Also in this pass:
- The empty-basePath guard only caught exact "". galleryCachePath(".")
and galleryCachePath("models") still resolved the cache sibling against
the process working directory, which is what the guard was written to
prevent. Reject any non-absolute base.
- The docs claimed the offline cache "applies to every gallery, with or
without mirrors". Not true for a .ref URL: the reference is resolved
before the cache is consulted, so a .ref gallery fails offline even
after a successful earlier fetch, and the cache file it writes can
never be read. Extend the .ref warning and qualify the sentence.
- pkg/oci's UserAgent comment never mentioned the platform component
added earlier on this branch.
- resetGalleryFailures and expireGalleryFailure had no non-test callers;
move them into the test file.
- The all-candidates-failed error reported len(attempt), so a three
mirror gallery with two sources in cooldown said "all 1 source(s)
failed" — which reads as a misconfiguration. Report how many were
configured and how many were skipped.
- Give the package's tests their own TMPDIR. The cache is a sibling of
the models directory, which is right in production, but specs that
build a models directory directly under /tmp made the sibling resolve
to /tmp/cache and left it behind after every run.
Assisted-by: Claude:claude-opus-5 [go vet] [go test]
* fix(gallery): convert the new tests to Ginkgo and clear the lint gate
.agents/coding-style.md requires Ginkgo v2 + Gomega for every Go test and
has forbidigo enforce it; the stdlib-style tests still in the tree are tech
debt, not a pattern. Every test file this branch added was written in the
forbidden style, which is what turned CI red.
Convert all five of them. internal had no suite bootstrap, so add one;
core/config, core/gallery and pkg/downloader already have theirs and are
reused, so no package mixes styles. pkg/downloader/useragent_test.go and
read_status_test.go were not in CI's forbidigo list but used the same
forbidden calls, so they are converted too.
The one conversion with a trap in it is core/gallery. Go's t.TempDir()
yields $TMPDIR/<TestName>NNNN/001, so the gallery cache — a sibling of the
models directory — was isolated per test. GinkgoT().TempDir() yields a flat
$TMPDIR/ginkgoNNNN, which would put every spec's cache in one shared
directory and break the specs that count files in it. tempModelsDir()
restores the original isolation.
Also make the deliberate cleanup-path ignores explicit with `_ =`, drop the
gallery cache directory to 0750 (nothing outside the server's own user and
group reads it), and justify the cache read with a #nosec G304 comment in
the form already used elsewhere in the tree: the path is a hex sha256 under
a fixed directory with a non-absolute base already rejected, so no
caller-supplied text reaches it.
Re-ran the mutations these specs were verified against — dropping the
platform suffix from UserAgent, making Gallery.Equal ignore Mirrors and
ignore Name, removing persistGalleryIndex's validity probe, removing the
!filepath.IsAbs guard, not skipping a cooled-down candidate, and dropping
the per-attempt timeout. All seven still fail the converted specs.
Assisted-by: Claude:claude-opus-5 [go vet] [go test] [golangci-lint] [gosec]
---------
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
|
||
|
|
5ac445e1d4 |
fix(react-ui): restore 3D Studio results and history (#11393)
* fix(react-ui): restore 3D Studio results and history Keep large conditioning-image payloads out of the rendered request panel so the generated viewer can mount reliably. Accept clipboard images and synchronize 3D history consumers so new results appear in Studio without a reload. Cover clipboard input, bounded request rendering, result display, and cross-view history synchronization with Playwright. Assisted-by: Codex:gpt-5 Playwright * perf(react-ui): idle the 3D viewport when still Limit auto-rotate rendering to 30 FPS and stop scheduling frames when rotation is disabled. Resize, view controls, and pointer input invalidate the still frame on demand. Assisted-by: Codex:gpt-5 Playwright |
||
|
|
147a5ee783 |
fix(react-ui): stop traces page crash when switching trace tabs (#11387)
Switching from Backend Traces back to API Traces crashed the page with "can't access property status, e.response is undefined" (#11376). The API table briefly renders the previous tab's backend rows while the refetch effect is still pending, and those rows carry no `response` envelope. The status column dereferenced it unguarded. Render a neutral placeholder instead of throwing, and cover the tab-switch scenario with a regression spec. Assisted-by: opencode:big-pickle Signed-off-by: Nandana Dileep <110280757+nandanadileep@users.noreply.github.com> |
||
|
|
9f62401fca |
feat(traces): show in-flight API requests (#11368)
Register JSON API exchanges before their handlers run so the traces dashboard can surface active work. Replace the live entry with the completed persisted record under the same ID, and clean it up if a handler panics. Assisted-by: Codex:gpt-5 Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com> |
||
|
|
08598a8611 |
chore(deps): bump the npm_and_yarn group across 1 directory with 5 updates (#11341)
Bumps the npm_and_yarn group with 5 updates in the /core/http/react-ui directory: | Package | From | To | | --- | --- | --- | | [hono](https://github.com/honojs/hono) | `4.12.25` | `4.12.34` | | [@hono/node-server](https://github.com/honojs/node-server) | `1.19.14` | `2.1.0` | | [fast-uri](https://github.com/fastify/fast-uri) | `3.1.4` | `3.1.5` | | [ip-address](https://github.com/beaugunderson/ip-address) | `10.2.0` | `10.4.0` | | [undici](https://github.com/nodejs/undici) | `7.28.0` | `7.29.0` | Updates `hono` from 4.12.25 to 4.12.34 - [Release notes](https://github.com/honojs/hono/releases) - [Commits](https://github.com/honojs/hono/compare/v4.12.25...v4.12.34) Updates `@hono/node-server` from 1.19.14 to 2.1.0 - [Release notes](https://github.com/honojs/node-server/releases) - [Commits](https://github.com/honojs/node-server/compare/v1.19.14...v2.1.0) Updates `fast-uri` from 3.1.4 to 3.1.5 - [Release notes](https://github.com/fastify/fast-uri/releases) - [Commits](https://github.com/fastify/fast-uri/compare/v3.1.4...v3.1.5) Updates `ip-address` from 10.2.0 to 10.4.0 - [Release notes](https://github.com/beaugunderson/ip-address/releases) - [Commits](https://github.com/beaugunderson/ip-address/compare/v10.2.0...v10.4.0) Updates `undici` from 7.28.0 to 7.29.0 - [Release notes](https://github.com/nodejs/undici/releases) - [Commits](https://github.com/nodejs/undici/compare/v7.28.0...v7.29.0) --- updated-dependencies: - dependency-name: hono dependency-version: 4.12.34 dependency-type: direct:production dependency-group: npm_and_yarn - dependency-name: "@hono/node-server" dependency-version: 2.1.0 dependency-type: indirect dependency-group: npm_and_yarn - dependency-name: fast-uri dependency-version: 3.1.5 dependency-type: indirect dependency-group: npm_and_yarn - dependency-name: ip-address dependency-version: 10.4.0 dependency-type: indirect dependency-group: npm_and_yarn - dependency-name: undici dependency-version: 7.29.0 dependency-type: indirect dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
93fe086798 |
chore(deps): bump the npm_and_yarn group across 1 directory with 2 updates (#11338)
Bumps the npm_and_yarn group with 2 updates in the /core/http/react-ui directory: [@hono/node-server](https://github.com/honojs/node-server) and [brace-expansion](https://github.com/juliangruber/brace-expansion). Updates `@hono/node-server` from 1.19.14 to 2.0.12 - [Release notes](https://github.com/honojs/node-server/releases) - [Commits](https://github.com/honojs/node-server/compare/v1.19.14...v2.0.12) Updates `brace-expansion` from 1.1.12 to 1.1.18 - [Release notes](https://github.com/juliangruber/brace-expansion/releases) - [Commits](https://github.com/juliangruber/brace-expansion/compare/v1.1.12...v1.1.18) --- updated-dependencies: - dependency-name: "@hono/node-server" dependency-version: 2.0.12 dependency-type: indirect dependency-group: npm_and_yarn - dependency-name: brace-expansion dependency-version: 1.1.18 dependency-type: indirect dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
1741df0bf1 |
fix(ui): scale the chrome audit's timeout to the number of routes it walks (#11319)
chrome-audit.spec.js walks 25 routes in a single test, and has been the
UI E2E suite's failure on 5 of the last 6 master runs. It always dies the
same way, at the 30s per-test default:
Test timeout of 30000ms exceeded.
Error: page.waitForTimeout: Test timeout of 30000ms exceeded.
19 | await page.goto(route)
> 20 | await page.waitForTimeout(400)
The spec is new in 5cb0c1a8; the commit before it was green, and every
run since has been red on this file.
The failure is cumulative rather than one bad route. Across those runs
the clock runs out at line 19, 20 or 21 depending on where the loop
happens to be, and the timeout lands on waitForTimeout rather than on
goto, which is what running out of budget looks like as opposed to a
navigation that hangs. 30s over 25 routes is ~1.2s each, including a
deliberate 400ms settle, so there is very little headroom to begin with.
Give the test a budget proportional to its work: six seconds a route.
That absorbs a slow runner and still fails promptly if a route genuinely
hangs.
Verified: the spec passes on the current UI in 12.2s solo, and the full
suite passes 418 at 8 workers locally. What I could NOT do is reproduce
the CI timeout on this machine, which has 20 cores against the runner's
2 to 4; under synthetic CPU load it still finished in 13.5s. So the fix
is argued from the CI signature and the arithmetic, not from a local
repro, and the proof is this spec going green on the hosted runner.
Note test.setTimeout() has to be called inside the test body. At module
scope Playwright rejects it with "test.setTimeout() can only be called
from a test".
Assisted-by: Claude Code:claude-opus-5 [Read] [Edit] [Bash]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
|
||
|
|
fd4ec083b9 |
feat(downloads): add resume-safe pause action (#11222)
Give gallery operations distinct pause and cancel paths. Pause preserves partial download data so reinstalling the same model or backend resumes through HTTP Range, while cancel keeps its destructive semantics. Surface the action in the Activity UI and document the API behavior. Assisted-by: Codex:gpt-5 Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com> |
||
|
|
5cb0c1a872 |
feat(ui): close the gap between the shipped UI and the design mocks (#11307)
* feat(ui): give the Operate overview real numbers and traces a latency shape First two items from a component-by-component comparison against the mocks. The pattern that audit found: everything newly built matched, everything pre-existing got the palette but not the layout, and an "absent rather than empty" rule hid most of the overview exactly when someone was looking at an idle installation. **The headline grid is always rendered**, including at zero, with a fourth cell for host memory. Hiding it removed the page's structure precisely when it was most likely to be read, and "0 failed" is information — an absent panel is not. The quiet case is now said in a line underneath instead of by showing nothing. **The sections state counts** rather than listing their destinations: backends, models, updates and running operations instead of the words "Usage and traces". That needed installed backend and model counts in the summary context, which are two more cheap reads on the poll that was already running. **Traces rows carry latency as a bar as well as a figure**, scaled against the slowest request currently in view and turning amber past two seconds. The table had no latency column at all — the number was buried in the expanded detail, so the shape of the tail was invisible while scanning. Scaling against the view rather than an absolute ceiling is deliberate: what matters when reading a page of traces is which of these are the outliers, and an absolute scale flattens every row on a fast installation into nothing. Full e2e suite: 409 passed, 4 skipped. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: Claude Code:claude-opus-5[1m] [Read] [Edit] [Bash] * feat(ui): name the engine on Home's resident models, and add jump-back-in Third item from the mock comparison. The mock showed each resident model with the engine serving it. /system carried only the id, so the audit recorded this as blocked on a server field — but the config loader is already in scope where that response is built, so it is one lookup. SysInfoModel gains an optional `backend`, resolved from the model's config and omitted rather than guessed when there is none (a loose file, or a config since removed). Home renders the column blank in that case; the test pins both halves of that. Memory per model stays out. It is not one lookup — it would mean asking each backend process — and inventing a number beside a real one is worse than leaving the column off. "Jump back in" is the block the mock had and Home did not. The quick-links row above it is a set of first-run actions; these are the three places someone returns to, each stated with what it currently holds rather than as a bare label. Go: routes suite passes. Full e2e suite: 412 passed, 4 skipped. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: Claude Code:claude-opus-5[1m] [Read] [Edit] [Bash] * feat(ui): rank the recommended models as lanes instead of equal cards The hardware recommendations were a grid of equally-weighted cards. The list is already sorted by fit, and a grid throws that order away: three cards side by side say "pick one", when the page has actually formed an opinion about which one. They are lanes now, read top to bottom in fit order, with the leader carrying the single amber "Best fit" label and the rest marked "Also fits". One opinion per page — the alternatives are alternatives, not runners-up each worth their own colour, which is how a strip of coloured badges ends up meaning nothing. Below 720px the size and VRAM columns drop and the lane keeps the name and the install action, which are the two things a narrow screen needs. The existing panel spec moves off .rec-models-item onto .lane rather than being deleted; dismissal, collapse, keyboard operation and install all still pass unchanged, and there is a new assertion that exactly one row is called out. Full e2e suite: 413 passed, 4 skipped. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: Claude Code:claude-opus-5[1m] [Read] [Edit] [Bash] * fix(ui): drop capsule chips app-wide, and un-break the empty voice library **Pills are gone.** A capsule radius reads as a tag floating on the surface, which fights a system whose structure is hairlines and square corners — and with chips on Discover, Host, Activity and the biometrics pages, "some pages have pills" was the real inconsistency rather than any one page. Sixteen selectors move to the small radius: filter buttons, tab pills, activity and biometrics chips, file and count badges, the jump-to-latest control, the nav badge. Round *buttons* keep their circle — .lightbox__nav and .home-send-btn are circles, not capsules — as do every progress track, status dot and avatar, which are round because they are round, not because they are tags. **The empty voice library was unusable.** `.voice-library-empty` sets min-height: 430px, border: 0 and background: transparent — a description of the empty PANEL — and it had been attached to the action instead. The create button was therefore a 430px transparent box that pushed itself out of the panel and could not be seen. Moved onto the container it describes, which now centres its action rather than letting it fall off the bottom. Same class-mangling shape as the Agents header fixed earlier. Full e2e suite: 416 passed, 4 skipped. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: Claude Code:claude-opus-5[1m] [Read] [Edit] [Bash] * feat(ui): put Host's headline figures on the shared hairline strip Host had shadowed, clickable StatCards above a page that already has a rail, a pane and a tab bar — a second dashboard language on one screen, and a different one again from the figures inside its own detail pane. The Operate overview's figure grid is generalised into a shared `.stat-strip` and Host adopts it, so the two pages read as one system: same cell, same figure scale, same tone vocabulary, and the same hairline grid the split-view StatGrid already uses. The cells stay clickable and still route into the tab and filter they describe, because a count is worth more when it is also the way to the thing counted. Tone is spent only where the number means something — running and updates when non-zero — since a strip where every cell is coloured has no emphasis left. Two bugs made on the way, both now covered: - The first version put `<button>` elements inside a `<dl>` with `<dt>`/`<dd>` inside the buttons. Neither is valid, the browser re-parents both, and the cells collapsed. These cells are a set of controls, so a plain container of buttons is also the honest markup. - Even correct, the strip rendered 2px tall: `.page--app` is a flex column whose split view takes flex:1, so a child with no intrinsic minimum is shrunk away. The old cards survived only because `.stat-card` carried min-height:96px. The strip now declines to shrink, with a test pinning it. The stat-card specs are retargeted rather than deleted: they were written to guard a class collision on a page that no longer uses cards, so they now guard the strip's labels and its height. Full e2e suite: 417 passed, 4 skipped. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: Claude Code:claude-opus-5[1m] [Read] [Edit] [Bash] * feat(ui): make Backends notices an edge rather than a filled card The install and upgrade banners were tinted cards with a full border. A filled panel makes every notice shout at the weight of an error, which is how notices stop being read — and Backends shows one on most visits, so it was shouting routinely. They are now a hairline with a coloured left edge, the same treatment the Operate overview gives rows that want a decision, so "this needs you" looks the same wherever it appears. Counts in the notice take the monospace tabular figures the rest of the console uses. Also drops the last inline style on the page, and refreshes the inline-style baseline, which has read 624 against a real count since #11288 landed. The gate exits 0 either way, so nothing was failing — but a baseline 86 above the truth would have let that many inline styles back in unnoticed. Now at 538, which tightens the ratchet rather than loosening it. The spec creates the upgrade it asserts on rather than skipping when the mock has no notice: a test that skips is a test that proves nothing. Full e2e suite: 418 passed, 4 skipped. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: Claude Code:claude-opus-5[1m] [Read] [Edit] [Bash] * feat(ui): finish the mock parity list, and stop hiding the recommendations The last two items from the audit, plus a correction. **Discover's use-case shelf is lanes.** These are a list of ways in, read in order; a grid of equal cards asks the reader to compare them, which is not the choice on offer. **The request panel reaches every generator.** Video, 3D, Sound and Audio FX join Images and Speech, so each one teaches its own endpoint rather than two of six doing it. Audio FX records the fields that shape the request rather than the bytes, since its payload is multipart. **Recommendations no longer collapse themselves.** They were folded away by default once anything was installed. That is the page's one opinion about this host, and an opinion hidden by default is one the reader never gets. Someone who disagrees can still collapse it and that choice is remembered — the difference is that we no longer make it for them. Three specs asserted the old default and now assert the new one. The use-case heading also sat a line's width from the text it introduces, so the two read as one paragraph. It has air under it now, and the shelf is separated from the recommendations above it. Two tests removed rather than kept: a generator loop whose only real assertion was `expect(endpoint.length).toBeGreaterThan(0)`, and an earlier card-gap guard that could only skip. A test that cannot fail is worse than no test, because it reads as coverage. Full e2e suite: 418 passed, 4 skipped. Inline styles at baseline. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: Claude Code:claude-opus-5[1m] [Read] [Edit] [Bash] * fix(ui): make the Host figures legible and give the strip its spacing back Three defects introduced by the Host redesign, all found by looking at the running app rather than by the suite. **The figures were invisible.** "Running now" and "Updates available" rendered pure black on the dark ground. Two causes compounding: the `--muted` tone alias never landed, because the source rule has extra spaces before its brace and the exact-match edit missed it silently; and a `<button>` does not inherit colour, so with no tone rule the value fell back to the user agent's `buttontext`. Both fixed, and a test now fails on any figure computing to pure black. **The strip sat flush against the resources panel.** `.stat-strip` declares `margin: 0 0 ...` and is declared later in the file than `.manage-summary`, so the shorthand quietly won and the top margin became zero. Raised to `.stat-strip.manage-summary` so it beats the shorthand on specificity rather than on declaration order, which is the kind of thing that breaks again the next time a rule moves. **Discover's use-case heading had a doubled gap.** `.zero-pane` is a flex column that already separates its children; adding a margin on top of the gap stacked the two. The margin is gone and the heading keeps only its own breathing room. Full e2e suite: 420 passed, 4 skipped. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: Claude Code:claude-opus-5[1m] [Read] [Edit] [Bash] * feat(ui): make Studio's tabs path segments rather than a query parameter `/app/studio?tab=images` reads like a filter applied to a page. It is navigation: a different generator, with its own state and its own deep link. It is now `/app/studio/images`, with the overview at `/app/studio`. Legacy `?tab=` links are redirected once to the path form, replacing the history entry so Back does not bounce between two spellings of the same place. Bookmarks and older links keep working and land on the canonical URL rather than a second version of it, which is the part worth having a test for. The nine `?tab=` references were all in specs, none in docs, so the migration is contained. They move to paths, and a new spec pins the redirect. Full e2e suite: 421 passed, 4 skipped. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: Claude Code:claude-opus-5[1m] [Read] [Edit] [Bash] * feat(ui): make the hardware recommendation a section, not a dismissable card It was a bordered card with a collapse control and a close button, sitting inside a pane that is otherwise hairline sections. Two problems: it read as something bolted onto the page rather than part of it, and treating it as an interruption to be shut is the wrong frame for the one thing the page has to say about the machine it is running on. It is now a plain section with the same heading treatment as the shelves below it. The collapse state, the dismissal, their storage keys and the legacy key read for backwards compatibility all go with it, along with the installedCount prop that existed only to pick a default collapse. Five specs described behaviour that no longer exists and are removed rather than adjusted — collapsing, dismissing, persistence of both, and the toggle's keyboard handling. One new spec asserts the replacement contract: no control with aria-expanded, no dismiss, and no card border. Full e2e suite: 416 passed, 4 skipped. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: Claude Code:claude-opus-5[1m] [Read] [Edit] [Bash] * fix(ui): restore every stripped icon and every default-chrome control You reported two broken icons. They were not two: an earlier automated edit had stripped the `fa-*` class from twenty `<i>` elements across eleven pages, and an `<i>` with no icon class renders nothing at all. Settings' save button, the voice-profile back link, and eighteen others — agent row actions, task and job buttons, import and create actions — were all drawing empty space. Each is restored from its own context rather than a blanket icon: the agent row gets pause/play, pen, comments, file-export and trash; the fine-tune toggle swaps plus for xmark as it opens; the P2P documentation link gets the external-link glyph. The same edit left controls without their classes. Fine-tune's "Import config" was rendering in the browser's own chrome, and `.p2p-cmd__copy` set a border but no background, so it fell back to `buttonface` — a pale grey chip on a dark command block. FineTune's "New job" also had its icon classes folded into the button's className, the same mangling already fixed on the Agents header. Rather than fix the reported two and wait for the next report, this adds a standing audit: twenty-five routes are walked and the test fails on any visible control rendering with user-agent chrome, or any `<i>` without an `fa-*` class. It found the three remaining cases after the first sweep, and it is the reason the next one cannot ship quietly. Full e2e suite: 417 passed, 4 skipped. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: Claude Code:claude-opus-5[1m] [Read] [Edit] [Bash] --------- Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Co-authored-by: Ettore Di Giacinto <mudler@localai.io> |
||
|
|
58ea2f5d79 |
feat(ui): give Operate and Studio a front door, and fix two layout regressions (#11305)
* feat(ui): give Operate a front door and fold six rail groups into four Opening Operate ran firstVisiblePath() and landed on Backends, because Backends happens to be written first in operateConsole.groups. The section that should answer "is anything wrong" opened on a package manager, and nothing was reported until you visited it. Adds /app/operate. Its one irreplaceable block is "Needs attention", which is empty when nothing is wrong and says so in a line rather than rendering a reassuring green panel. It collects stale backends, failed operations and unhealthy nodes. Everything else on the page is a summary you could already assemble by visiting four others. The rail regroups from six headings to four: Inference and Activity were both "the runtime right now", Access and System were both administration. No destination is removed and no gate changes, so isConsoleItemVisible and consolePaths are untouched. Overview leads the first group, which is what makes firstVisiblePath() return it without knowing it exists. Rail entries now carry a signal beside the label. This does not replace the sidebar badge and is not built as if it does: the badge stays on the always-visible sidebar entry for the reason recorded in Sidebar.jsx, that the rail exists only on Operate routes and can be collapsed. The signals are orientation while inside Operate, so they are aria-hidden and nothing urgent depends on them alone. OperateSummaryContext polls once for the whole console, following OperationsContext, which exists because per-consumer setInterval against one endpoint was the defect it fixed. It is mounted by ConsoleLayout for the Operate console only, so "poll only while in Operate" needs no route check. Built on usePolling, so it pauses on a hidden tab. Operations are read from OperationsContext rather than polled a second time, and each source degrades to no-signal on its own so one dead endpoint cannot blank the rest. It reads the cached GET /api/backends/upgrades and never the POST that forces a real registry check. Traces and Usage get no signal yet: /api/traces returns the list, so a count would mean fetching every trace to render one number. A counts endpoint is the honest fix and is scoped separately. Full e2e suite green (369 passed, 4 skipped), including a render-smoke entry for the new route. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: Claude Code:claude-opus-5[1m] [Read] [Edit] [Bash] * feat(ui): open Studio on what this machine can actually make Studio was a tab strip over six generators that opened on Images, which was never a decision, only the first entry in BASE_TABS. Nothing said which modalities this installation could run, so the way to learn that video had no model was to pick the tab and find an empty select. Adds an overview tab and makes it the fallback. Explicit tabs still win, so existing deep links keep working; anything unrecognised or gated now lands on the overview rather than Images. Each tab carries a dot: filled when an installed model advertises that modality, hollow when nothing serves it. That is the feature in one detail, turning the strip from navigation into a report of what the machine can do before anything is clicked. The dot is aria-hidden because the overview states the same facts in words and the dots change as models load. Two kinds of unavailable, which had to stop looking alike: - switched off, via a permission: no tab and no lane, unchanged - available with no model: a lane, and a route to installing one Studio now owns one MODALITIES table so the tab strip and the overview cannot disagree about what exists, and calls useModels() once, unfiltered, grouping in the browser. useModels(capability) fetches the whole list and filters locally, so a hook per modality would have been six identical requests to /api/models/capabilities on every mount. There is a test for that. Recent outputs read every localStorage store through a new readAllMediaHistory(), which avoids mounting five hooks that carry save timers the overview has no use for. 3D is read separately through use3DHistory rather than folded in: its entries are GLB blobs in IndexedDB, so they cannot come from the same synchronous read. Typical cost is the median of this machine's own history, not a guess, and renders as a dash when there is nothing to go on. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: Claude Code:claude-opus-5[1m] [Read] [Edit] [Bash] * fix(ui): stop the stat cards and the console rail breaking on small screens Two unrelated causes behind one report that /app/manage looks wrong when the window is narrow. The stat cards were being laid out by the wrong rule at every width. Two different components both claimed `.stat-grid`: the dashboard card strip that holds .stat-card children, and the detail-pane StatGrid the split views introduced further down App.css. Being later, the second won every shared property, so the cards got its 120px columns and its 1px hairline gap in place of their own 180px columns and spacing-md. Four cards were packed onto a row that fits two, labels wrapped to three lines and clipped, and the icon crowded the value. Renamed the strip to `.stat-cards`, after the children it actually holds, which also removes the mismatch of a `.stat-grid` container full of `.stat-card`s. The split-view component keeps `.stat-grid` and its BEM parts. The expanded console rail had no bounded height. Thirteen destinations stacked in one column is taller than a phone, so opening the menu pushed the page's own heading past the fold: the menu replaced the page rather than annotating it. Capped at 55vh with internal scrolling below 768px, so the content behind stays reachable. Both are asserted on behaviour rather than markup: no stat-card label may be clipped, the card gap must not be the detail pane's hairline, and expanding the rail must leave the page heading on screen. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: Claude Code:claude-opus-5[1m] [Read] [Edit] [Bash] * feat(ui): retemper the palette to localai.io and add the lane primitive The token half of the style transfer, plus the shared list idiom the two overviews had each grown their own copy of. theme.css moves from Nord to the website's palette, variable names preserved so every consumer moves with it: ground #13171f -> #0d1117, accent frost cyan #88c0d0 -> action blue #4f8cff, success sage -> mint #56d6a4, warning -> the amber #f1b95d the site spends only on the thing asking for a decision. Eyebrows go mint. Dividers become an opaque #29384a hairline rather than alpha over a varying surface, which is what makes stacked surfaces read crisply on the site. Light is derived, not inverted. The site ships one theme and never had to answer this, but the app does: blue darkens to #2f62d8, mint to #0d8b60 and amber to #8a5d0b, all clearing 4.5:1 on a cool paper ground, where the dark-mode values sit near 2:1. Same three roles, different values. Three files restate the palette because CSS variables cannot reach them: cmTheme.js (the whole CodeMirror theme), VoiceVisualizer and WaveformPlayer (canvas). Left alone they would have quietly kept the app half-Nord. The `.lane` primitive replaces the near-identical row CSS that OperateOverview and StudioOverview had each written: a full-bleed row on a hairline that insets on hover, with no card and no shadow. Callers supply only the column template. Both pages now use it, along with `.lane-head` for section rhythm and a `.page-pad` container for top-level pages outside a console shell — without which Studio sat flush against the sidebar with its eyebrow clipped. Studio's tab strip wraps rather than running off the edge at narrow widths. Full e2e suite: 386 passed, 4 skipped. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: Claude Code:claude-opus-5[1m] [Read] [Edit] [Bash] * feat(ui): put Home's resident models on lanes and give the footer one line Home's status line was three chips saying a thing was true. It now reports figures: how many models are resident, how many nodes are healthy, what share of memory is in use, set in tabular monospace so the digits line up. A chip answers whether; a figure answers how much, which is what someone opening the page at a glance is after. Resident models move from status chips to lanes, with the id set in a new `.lane__name--id` because an id is something you might type or paste and the UI face makes it read as a label. /api/system-information carries only the id, so there is deliberately no backend or memory column: inventing one would mean a server change this does not make. The footer was three centred rows and cost the bottom sixth of every page for chrome. It is one line now, version left and links right, wrapping to centred when the viewport is too narrow to hold both. Every link it had, it keeps. Full e2e suite: 392 passed, 4 skipped. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: Claude Code:claude-opus-5[1m] [Read] [Edit] [Bash] * fix(ui): correct three contrast failures and stop a guaranteed-404 poll A contrast audit of the new palette found three values below WCAG AA, one of which the previous commit message claimed was fine: - White on the #4f8cff button is 3.22:1, which is large-text only. The website does exactly this, but a button label in an app is not large text, so the label goes to dark ink at 5.88:1. Light mode keeps white, which is 5.44:1 on its darker blue. - Light-mode success was 4.08:1 on paper, not the 4.5 claimed. Darkened to #0a734f, 5.56:1. - Nord red was already 4.28:1 on raised surfaces, a pre-existing miss carried over unexamined. Lifted to #c96f78, 5.02:1. Lanes gain the two states they were missing: a 44px target on coarse pointers, matching what EntityRail already does so the two list idioms feel the same under a thumb, and a reduced-motion variant that keeps the background feedback while dropping the hover inset, which is a position change. The Operate summary no longer asks for /api/nodes on a single-node install. The cluster API answers 503 when distributed mode is off, so it was a guaranteed miss every fifteen seconds; it is now gated on useDistributedMode, the same condition the rail already uses for the Nodes entry. Full e2e suite: 392 passed, 4 skipped. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: Claude Code:claude-opus-5[1m] [Read] [Edit] [Bash] * fix(ui): restore the gap between overview blocks, and stop claiming zero nodes Two defects a design review surfaced. `.lane-head:first-child { margin-top: 0 }` was meant to stop the first block on a page carrying a top margin. But every <section> makes its lane-head a first child, so the reset applied to all of them and the gap between blocks vanished: "Sections" sat flush against the attention row above it. The header supplies its own bottom margin, so a uniform top margin is correct everywhere. The Cluster summary read "0 nodes" on a single-node install, which looks like a fault when the cluster API is simply switched off. It now says "Single node". Full e2e suite: 392 passed, 4 skipped. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: Claude Code:claude-opus-5[1m] [Read] [Edit] [Bash] * feat(ui): open dark by default, and stop clipping the collapsed sidebar footer Dark is the identity rather than a preference: localai.io ships one theme and it is this one, so an install should look like LocalAI before anyone has chosen anything. The OS setting no longer selects light on first load. The toggle still does, and a stored choice wins forever after, which the tests assert both ways. The collapsed sidebar footer stacked its controls but kept the expanded row's inline padding, so their edges were clipped against the 51px rail. Full e2e suite: 394 passed, 4 skipped. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: Claude Code:claude-opus-5[1m] [Read] [Edit] [Bash] * feat(api): count traces server-side and give the Operate overview real totals The overview's headline block had no source. /api/traces returns the trace list, so "37 errors in 24h" meant fetching every buffered exchange to count it in the browser — waste that grows with the buffer, to produce three integers. Adds GET /api/traces/summary: totals, failures, p95 and a bucketed series for sparklines, over a window that defaults to 24 hours and is capped at a week. Deliberate calls, each with a spec: - A 4xx is the caller getting it wrong, not the installation being unhealthy, so only 5xx and transport errors count as failures. - p95 is a nearest-rank percentile rather than the slowest request, which is what a max would report and what makes latency panels lie. - Buckets are oldest-first so a sparkline reads left to right, and the slice is never nil: nil serialises as null and breaks .map() on the other side, which is a silent runtime error rather than an empty chart. - Exchanges outside the window are not counted at all. The route is registered before /api/traces/:id so "summary" is not captured as a trace ID. On the client, Traces and Usage gain the rail signals they were shipped without, the Observability section summary now states counts instead of listing its destinations, and an installation that has served nothing says so rather than showing three zeroes dressed as telemetry. Sparkline is a bare stroke with an emphasised endpoint and no axes: the figure above it already states the value, so its only job is the shape. Go: 185 middleware specs pass. Full e2e suite: 396 passed, 4 skipped. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: Claude Code:claude-opus-5[1m] [Read] [Edit] [Bash] * fix(ui): stop the memory chart calling a trade-off an error The VRAM-by-context chart rendered any build over the limit in error red, and escalated the verdict to the error tone as soon as two context sizes crossed it. But an over-limit build still installs — #11288 keeps a test on exactly that — so red overstates what is happening. A model that fits at 32k and not 64k is a trade-off, not a fault. Over-limit bars and the limit line now use the warning tone, which is the constraint colour used everywhere else in this branch: know what you are doing, not you may not. The error tone is reserved for "fits nowhere", where the model genuinely cannot run on this host. Full e2e suite: 397 passed, 4 skipped. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: Claude Code:claude-opus-5[1m] [Read] [Edit] [Bash] * feat(ui): give the new surfaces orchestrated motion Uses the reveal system already in the codebase rather than adding a library: pageReveal, .reveal-stagger and staggerStyle() were built for exactly this, and anime.js would be ~17KB duplicating four lines of CSS for list reveals. The overview's headline figures, attention rows and section lanes stagger in, as do Studio's modality lanes and recent outputs, so a page assembles in the order it is read instead of appearing all at once. Two additions beyond stagger. Rail signals transition on opacity when a poll lands, so a number changing reads as an update rather than a jump cut, and it stays on the compositor so it cannot reflow the rail. The attention block animates its left edge in — the one thing on the page that should announce itself, and on the border rather than the text so nothing moves under a reader. Both are dropped entirely under prefers-reduced-motion, alongside the lane hover inset already handled. Full e2e suite: 397 passed, 4 skipped. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: Claude Code:claude-opus-5[1m] [Read] [Edit] [Bash] * feat(ui): put the generators on a hairline field stack and record the request The workbench treatment from the mocks, applied where it costs least: both changes land on shared surfaces, so all six generators get them at once rather than drifting apart page by page. The control column stops being a shadowed card of boxed groups and becomes a hairline field stack — the panel is the page's left half, not an object floating on it — with uppercase micro-labels matching the eyebrow treatment used elsewhere. Because .media-controls is shared, Images, Video, 3D, Speech, Sound and Audio FX all move together. RequestPanel shows the request the form actually built, with a copy-as-curl. LocalAI is API-first and Studio is the best place in the app to teach its own endpoints: the form stops being a black box, and a result worth keeping can be reproduced from a shell without reverse-engineering which fields the page sent. It records what was sent rather than what the form currently holds, and renders nothing until a request has been made — a panel describing a request nobody made is a tutorial, not a record. Wired into Images and Speech. Full e2e suite: 401 passed, 4 skipped. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: Claude Code:claude-opus-5[1m] [Read] [Edit] [Bash] * feat(ui): make Chat a transcript instead of a bubble thread Rounded, filled, asymmetric bubbles fight a system built on hairlines, and they carry the speaker in shape and side rather than in words. The assistant side had already given up its bubble; this finishes the job. Both roles now run full width down one column, separated by a rule, each with a mono role label. The user turn keeps a left edge in the action tone so the two are still told apart at a glance, without a fill or a corner radius. The avatars go: the accent and the label carry the speaker, so the glyph was decoration once neither side had a bubble. Saying who is speaking in words rather than in geometry is also what survives being read aloud, printed, or looked at by someone who cannot pick the sides apart by colour. Full e2e suite: 404 passed, 4 skipped. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: Claude Code:claude-opus-5[1m] [Read] [Edit] [Bash] * feat(ui): dress the API reference in LocalAI's palette The Swagger page was the last surface still shipping in someone else's colours, which is conspicuous now that everything it links from is dark. Swagger UI has no theming hook, so rather than fork it we serve our own index ahead of the library's wildcard and restate the palette over its stylesheet. The library's own bundle and assets are still what load, so a swagger-ui upgrade cannot silently break the page — this is a skin, not a fork. Two things needed real care. Swagger tints the entire operation row per method via .opblock.opblock-post and friends, so the palette had to match that specificity rather than reach for !important; the method now lives on one edge instead of washing across the row, because a page where every row is a status colour has no status colour left. And the filled method chip put white on pale green, which was the least readable thing on the page — it is an outlined mono chip now, carrying the method in its border and text. Palette values are copied from theme.css rather than referenced: this page is served by Go and never sees the app's CSS. The comment says so, and says to keep them in step. Go: routes and middleware suites pass. Full e2e suite: 405 passed, 4 skipped. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: Claude Code:claude-opus-5[1m] [Read] [Edit] [Bash] * fix(ui): make tall split-view pages reachable, repair the Agents header, scale titles Three things found by actually using the app rather than measuring it. **Host was unusable.** The shell above a split view is overflow:hidden so the document cannot grow, which left anything taller than the viewport simply unreachable — and Host stacks a resources card, four stat cards and a tab bar above its split, so the bottom of the pane fell off at every window height with nothing to scroll. Every sweep I ran for this was horizontal, which is why it kept coming back clean. The page now scrolls inside the pinned shell. The pane keeps its own scroller: letting it grow instead pushes the document taller and stretches the rail to match, which is the regression e2e/discover-height.spec.js exists to catch, and which the first version of this fix duly caused. **The Agents header controls were unstyled** — "Create Agent" was rendering with the browser's default chrome. The markup had been mangled at some point: six unrelated classes merged into one string on the link, and the label and button left with none at all and empty icons. Repaired, with the inline flex replaced by a shared .header-actions class. **Page titles take the editorial scale from the site**: larger, tracked at -0.04em, on a line height near 1, so a two-word title reads as a statement rather than a label. The typeface is unchanged — DESIGN.md keeps the existing type system — so the whole difference is scale, tracking and leading, which is where the site gets its voice from. This was the biggest reason the running app still did not look like the mocks. Full e2e suite: 404 passed, 4 skipped. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: Claude Code:claude-opus-5[1m] [Read] [Edit] [Bash] --------- Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Co-authored-by: Ettore Di Giacinto <mudler@localai.io> |