Compare commits

...

8 Commits

Author SHA1 Message Date
Richard Palethorpe
b81a6d01b3 perf(react-ui): code-split bundle, speed up coverage suite (#10042)
* Curate the highlight.js build to ~29 languages (lib/core + the
  common set) instead of the full ~190-grammar default: -787 KB raw /
  -230 KB gz on the base bundle.
* Code-split every route via React.lazy with a per-layout <Suspense>
  in App.jsx so the sidebar stays mounted on navigation. Initial entry
  chunk drops from 3194 KB raw / 887 KB gz to 397 KB / 122 KB (-87%).
  Warm chunks on sidebar hover/focus/touch via a preload registry so
  the click finds the chunk already in flight or cached.
* Migrate Playwright coverage from istanbul (build-time counters) to
  native Chromium V8 coverage, with per-worker accumulation +
  conversion. Suite drops from 71s to 30s at 20 workers (~58%) at the
  non-instrumented floor.
* Keep the coverage gate bundling-invariant: the coverage build inlines
  dynamic imports so every shipped source file lands in the denominator
  (otherwise untested page chunks silently drop out and inflate the
  percentage). Production builds stay code-split.
* Add UI_TEST_WORKERS=N Makefile knob; tighten coverage tolerance to
  0.8pp now that jitter sits near istanbul's ~0.5pp again.

Assisted-by: Claude:claude-opus-4-7 [Claude Code]

Signed-off-by: Richard Palethorpe <io@richiejp.com>
2026-05-28 13:43:15 +02:00
Tai An
0fd666ee6e fix(openresponses): populate Content and accept bare {role,content} items (#10039) (#10040)
* fix(openresponses): populate Content and accept bare {role,content} items (#10039)

Fixes mudler/LocalAI#10039 — `/v1/responses` silently returned empty
output on any model whose YAML doesn't include a Go-side
`template.chat_message` block.

Three cooperating bugs:

* `convertORInputToMessages` populated only `StringContent` for string
  input and for the `input.Instructions` system message, leaving the
  `Content` (any) field nil.
* `TemplateMessages` gated all fallback content-rendering branches on
  `Content != nil && StringContent != ""` — but every branch in that
  function consumes `StringContent`, not `Content`. The `&&` silently
  dropped messages that had StringContent set and Content nil, producing
  an empty prompt that the 5× empty-retry guard then turned into a
  200 OK with `output: []`.
* The array-input branch of `convertORInputToMessages` dispatched on
  `itemMap["type"]` with no default, dropping bare `{role, content}`
  items emitted by the OpenAI Python SDK helper
  `client.responses.create(input=[{...}])`.

Fix:

* Set both `Content` and `StringContent` in the two openresponses
  message-construction sites that only set one.
* Treat a bare `{role, content}` item (no `type`) as
  `type: "message"` for OpenAI-SDK compatibility.
* Gate `TemplateMessages` fallback rendering on `StringContent != ""`,
  which is what every downstream branch in that function actually
  reads.

Regression test added to `evaluator_test.go` covering the fallback
path (no `ChatMessage` template) with a StringContent-only message,
both with and without a role mapping.

* test(openresponses): guard Content population and ToProto path (#10039)

Add regression tests for the two seams the original fix touched but
left uncovered:

* convertORInputToMessages must populate both Content and StringContent
  for plain string input and for bare {role, content} array items (the
  OpenAI SDK shape that omits the type discriminator). Both are
  functional reds against the pre-fix code.
* Messages.ToProto reads Content, not StringContent — this is the path
  UseTokenizerTemplate backends (imported GGUFs) take. The cases pin
  that contract so a future regression on the producer side is caught.

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

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
2026-05-28 07:21:48 +00:00
LocalAI [bot]
7763fb23a3 chore: ⬆️ Update antirez/ds4 to 072bc0feb187be5f374c08b16d0045e1ad7bc9bc (#10036)
⬆️ Update antirez/ds4

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-05-28 08:41:03 +02:00
LocalAI [bot]
324277ccfd chore: ⬆️ Update ggml-org/whisper.cpp to 6dcdd6536456158667747f724d6bd3a2ceaa8d88 (#10032)
⬆️ Update ggml-org/whisper.cpp

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-05-28 00:25:20 +02:00
LocalAI [bot]
10d02e6c59 chore: ⬆️ Update leejet/stable-diffusion.cpp to 29ab511fc75f89fbab148665eab1a8e10a139a72 (#10033)
⬆️ Update leejet/stable-diffusion.cpp

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-05-28 00:24:59 +02:00
LocalAI [bot]
05ae06c17b chore: ⬆️ Update ggml-org/llama.cpp to aa50b2c2ae91326d5aad956ceeb015d1d48e626b (#10034)
⬆️ Update ggml-org/llama.cpp

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-05-28 00:23:40 +02:00
LocalAI [bot]
2671e0c6f7 chore(model-gallery): ⬆️ update checksum (#10038)
⬆️ Checksum updates in gallery/index.yaml

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-05-28 00:22:19 +02:00
LocalAI [bot]
81b6b94f0b chore: ⬆️ Update ikawrakow/ik_llama.cpp to 3bf7e836c2c5a895e8d12d3eb7e398ae7ab2f9ce (#10037)
⬆️ Update ikawrakow/ik_llama.cpp

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-05-28 00:21:45 +02:00
26 changed files with 498 additions and 125 deletions

View File

@@ -1313,6 +1313,13 @@ build-ui-test-server: build-mock-backend react-ui protogen-go
test-ui-e2e: build-ui-test-server
cd core/http/react-ui && npm install && npx playwright install --with-deps chromium && npx playwright test
## Optional Playwright worker count for the UI e2e targets below. Pass
## UI_TEST_WORKERS=N (e.g. `make test-ui-coverage UI_TEST_WORKERS=20`) to
## override Playwright's default (cores/2). Empty by default so Playwright
## picks its own worker count.
UI_TEST_WORKERS ?=
PLAYWRIGHT_WORKERS_FLAG = $(if $(UI_TEST_WORKERS),--workers=$(UI_TEST_WORKERS),)
## Fast Playwright e2e run used by the pre-commit hook on React UI changes.
## Force-rebuilds the (non-instrumented) dist so the suite tests the working
## tree — not a stale dist the `react-ui` skip-guard would leave — re-embeds
@@ -1322,22 +1329,24 @@ test-ui-e2e: build-ui-test-server
test-ui: build-mock-backend protogen-go
cd core/http/react-ui && bun install && bun run build
$(GOCMD) build -o tests/e2e-ui/ui-test-server ./tests/e2e-ui
cd core/http/react-ui && sh $(CURDIR)/scripts/ensure-playwright-browser.sh && bunx playwright test
cd core/http/react-ui && sh $(CURDIR)/scripts/ensure-playwright-browser.sh && bunx playwright test $(PLAYWRIGHT_WORKERS_FLAG)
## React UI code coverage from the Playwright e2e suite. Builds an
## istanbul-instrumented bundle (COVERAGE=true), re-embeds it into the
## ui-test-server (the dist is //go:embed'ed at compile time), runs the
## Playwright specs which harvest window.__coverage__ via the coverage
## fixture — and writes an nyc report to core/http/react-ui/coverage/.
## Removes the instrumented dist afterwards so normal builds aren't served
## instrumented assets.
## React UI code coverage from the Playwright e2e suite. Builds a
## NON-instrumented bundle with source maps (COVERAGE_V8=true), re-embeds it
## into the ui-test-server (the dist is //go:embed'ed at compile time), runs the
## Playwright specs which collect native Chromium V8 coverage (PW_V8_COVERAGE=1)
## — far cheaper than istanbul's build-time counters (~40% faster end-to-end) —
## convert it to istanbul via v8-to-istanbul in the coverage fixture, and write
## an nyc report to core/http/react-ui/coverage/. Removes the dist afterwards so
## normal builds aren't served source-mapped assets. (The legacy istanbul path
## still exists: `bun run build:coverage` + unset PW_V8_COVERAGE.)
test-ui-coverage: build-mock-backend protogen-go
trap 'rm -rf "$(CURDIR)/core/http/react-ui/dist"' EXIT; \
( cd core/http/react-ui && bun install && bun run build:coverage ) && \
( cd core/http/react-ui && bun install && bun run build:coverage-v8 ) && \
$(GOCMD) build -o tests/e2e-ui/ui-test-server ./tests/e2e-ui && \
( cd core/http/react-ui && rm -rf .nyc_output coverage && \
sh $(CURDIR)/scripts/ensure-playwright-browser.sh && \
bunx playwright test && bun run coverage:report )
PW_V8_COVERAGE=1 bunx playwright test $(PLAYWRIGHT_WORKERS_FLAG) && bun run coverage:report )
## UI coverage baseline (committed) and the strict gate that compares against
## it — the React mirror of test-coverage-baseline / test-coverage-check.

View File

@@ -1,10 +1,10 @@
# ds4 backend Makefile.
#
# Upstream pin lives below as DS4_VERSION?=e8e8779b261c10f36ad6270ba732c8f0be5b62e3
# Upstream pin lives below as DS4_VERSION?=072bc0feb187be5f374c08b16d0045e1ad7bc9bc
# (.github/bump_deps.sh) can find and update it - matches the
# llama-cpp / ik-llama-cpp / turboquant convention.
DS4_VERSION?=e8e8779b261c10f36ad6270ba732c8f0be5b62e3
DS4_VERSION?=072bc0feb187be5f374c08b16d0045e1ad7bc9bc
DS4_REPO?=https://github.com/antirez/ds4
CURRENT_MAKEFILE_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))

View File

@@ -1,5 +1,5 @@
IK_LLAMA_VERSION?=d2da6da05c73aeb658a3d1751f386c24e6963856
IK_LLAMA_VERSION?=3bf7e836c2c5a895e8d12d3eb7e398ae7ab2f9ce
LLAMA_REPO?=https://github.com/ikawrakow/ik_llama.cpp
CMAKE_ARGS?=

View File

@@ -1,5 +1,5 @@
LLAMA_VERSION?=0d18aaa9d1a8af3df9abccd828e22eeaac7f840b
LLAMA_VERSION?=aa50b2c2ae91326d5aad956ceeb015d1d48e626b
LLAMA_REPO?=https://github.com/ggerganov/llama.cpp
CMAKE_ARGS?=

View File

@@ -8,7 +8,7 @@ JOBS?=$(shell nproc --ignore=1)
# stablediffusion.cpp (ggml)
STABLEDIFFUSION_GGML_REPO?=https://github.com/leejet/stable-diffusion.cpp
STABLEDIFFUSION_GGML_VERSION?=92dc7268fc4ffb0c0cc0bd52dfcefea91326e797
STABLEDIFFUSION_GGML_VERSION?=29ab511fc75f89fbab148665eab1a8e10a139a72
CMAKE_ARGS+=-DGGML_MAX_NAME=128

View File

@@ -8,7 +8,7 @@ JOBS?=$(shell nproc --ignore=1)
# whisper.cpp version
WHISPER_REPO?=https://github.com/ggml-org/whisper.cpp
WHISPER_CPP_VERSION?=27101c01dcac1676e2b6422256233cd0f1f9ae28
WHISPER_CPP_VERSION?=6dcdd6536456158667747f724d6bd3a2ceaa8d88
SO_TARGET?=libgowhisper.so
CMAKE_ARGS+=-DBUILD_SHARED_LIBS=OFF

View File

@@ -95,7 +95,7 @@ func ResponsesEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, eval
// Add instructions as system message if provided
if input.Instructions != "" {
messages = append([]schema.Message{{Role: "system", StringContent: input.Instructions}}, messages...)
messages = append([]schema.Message{{Role: "system", Content: input.Instructions, StringContent: input.Instructions}}, messages...)
}
// Handle tools
@@ -299,7 +299,7 @@ func convertORInputToMessages(input any, cfg *config.ModelConfig) ([]schema.Mess
switch v := input.(type) {
case string:
// Simple string = user message
return []schema.Message{{Role: "user", StringContent: v}}, nil
return []schema.Message{{Role: "user", Content: v, StringContent: v}}, nil
case []any:
// Array of items
for _, itemRaw := range v {
@@ -309,6 +309,16 @@ func convertORInputToMessages(input any, cfg *config.ModelConfig) ([]schema.Mess
}
itemType, _ := itemMap["type"].(string)
// OpenAI SDK helpers (e.g. client.responses.create(input=[{"role":...,"content":...}]))
// send message items without a "type" discriminator. Treat a bare {role, content}
// object as type:"message" so the chat-completions and responses paths agree.
if itemType == "" {
if _, hasRole := itemMap["role"].(string); hasRole {
if _, hasContent := itemMap["content"]; hasContent {
itemType = "message"
}
}
}
switch itemType {
case "message":
msg, err := convertORMessageItem(itemMap, cfg)

View File

@@ -0,0 +1,62 @@
package openresponses
import (
"github.com/mudler/LocalAI/core/config"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
// Regression for mudler/LocalAI#10039. convertORInputToMessages must populate
// both Content and StringContent: the templating fallback path reads
// StringContent, while the UseTokenizerTemplate path serialises Content via
// Messages.ToProto(). Leaving Content nil produced an empty prompt on any model
// without a Go-side template.chat_message block (the default for imported GGUFs).
var _ = Describe("convertORInputToMessages", func() {
cfg := &config.ModelConfig{}
It("populates both Content and StringContent for plain string input", func() {
msgs, err := convertORInputToMessages("Hello", cfg)
Expect(err).NotTo(HaveOccurred())
Expect(msgs).To(HaveLen(1))
Expect(msgs[0].Role).To(Equal("user"))
Expect(msgs[0].StringContent).To(Equal("Hello"))
Expect(msgs[0].Content).To(Equal("Hello"))
})
It("accepts a bare {role, content} item without a type discriminator", func() {
// The OpenAI Python SDK helper client.responses.create(input=[{...}])
// sends message items with no "type" field. They must not be dropped.
input := []any{
map[string]any{"role": "user", "content": "Hi there"},
}
msgs, err := convertORInputToMessages(input, cfg)
Expect(err).NotTo(HaveOccurred())
Expect(msgs).To(HaveLen(1))
Expect(msgs[0].Role).To(Equal("user"))
Expect(msgs[0].StringContent).To(Equal("Hi there"))
Expect(msgs[0].Content).To(Equal("Hi there"))
})
It("still populates both fields for an explicit type:message item", func() {
input := []any{
map[string]any{"type": "message", "role": "user", "content": "Typed"},
}
msgs, err := convertORInputToMessages(input, cfg)
Expect(err).NotTo(HaveOccurred())
Expect(msgs).To(HaveLen(1))
Expect(msgs[0].StringContent).To(Equal("Typed"))
Expect(msgs[0].Content).To(Equal("Typed"))
})
It("does not treat a non-message item (no content key) as a message", func() {
// An item with neither a known type nor a {role, content} shape must
// keep falling through unchanged — no behaviour change for such inputs.
input := []any{
map[string]any{"role": "user"},
}
msgs, err := convertORInputToMessages(input, cfg)
Expect(err).NotTo(HaveOccurred())
Expect(msgs).To(BeEmpty())
})
})

View File

@@ -32,6 +32,7 @@
"yaml": "^2.8.3",
},
"devDependencies": {
"@bcoe/v8-coverage": "^1.0.2",
"@eslint/js": "^9.27.0",
"@playwright/test": "1.58.2",
"@vitejs/plugin-react": "^6.0.2",
@@ -41,6 +42,7 @@
"globals": "^16.1.0",
"i18next-parser": "^9.4.0",
"nyc": "^18.0.0",
"v8-to-istanbul": "^9.3.0",
"vite": "^8.0.14",
"vite-plugin-istanbul": "^9.0.0",
},
@@ -81,6 +83,8 @@
"@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
"@bcoe/v8-coverage": ["@bcoe/v8-coverage@1.0.2", "", {}, "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA=="],
"@codemirror/autocomplete": ["@codemirror/autocomplete@6.20.1", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0" } }, "sha512-1cvg3Vz1dSSToCNlJfRA2WSI4ht3K+WplO0UMOgmUYPivCyy2oueZY6Lx7M9wThm7SDUBViRmuT+OG/i8+ON9A=="],
"@codemirror/commands": ["@codemirror/commands@6.10.3", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.6.0", "@codemirror/view": "^6.27.0", "@lezer/common": "^1.1.0" } }, "sha512-JFRiqhKu+bvSkDLI+rUhJwSxQxYb759W5GBezE8Uc8mHLqC9aV/9aTC7yJSqCtB3F00pylrLCwnyS91Ap5ej4Q=="],
@@ -267,6 +271,8 @@
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/istanbul-lib-coverage": ["@types/istanbul-lib-coverage@2.0.6", "", {}, "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w=="],
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
"@types/minimatch": ["@types/minimatch@3.0.5", "", {}, "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ=="],
@@ -983,6 +989,8 @@
"uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="],
"v8-to-istanbul": ["v8-to-istanbul@9.3.0", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.12", "@types/istanbul-lib-coverage": "^2.0.1", "convert-source-map": "^2.0.0" } }, "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA=="],
"value-or-function": ["value-or-function@4.0.0", "", {}, "sha512-aeVK81SIuT6aMJfNo9Vte8Dw0/FZINGBV8BfCraGtqVxIeLAEhJyoWs8SmvRVmXfGss2PmmOwZCuBPbZR+IYWg=="],
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
@@ -1121,6 +1129,8 @@
"test-exclude/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
"v8-to-istanbul/convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
"vinyl-sourcemap/convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
"vite-plugin-istanbul/espree": ["espree@11.2.0", "", { "dependencies": { "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^5.0.1" } }, "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw=="],

View File

@@ -1 +1 @@
30.66
38.29

View File

@@ -15,9 +15,41 @@ import { randomUUID } from 'node:crypto'
import path from 'node:path'
const COVERAGE_DIR = path.resolve(process.cwd(), '.nyc_output')
const V8_COVERAGE = process.env.PW_V8_COVERAGE === '1'
const withCoverage = base.extend({
// Worker-scoped V8 coverage accumulator: collects every test's native
// Chromium coverage and converts it to istanbul ONCE at worker teardown
// (conversion is expensive; see e2e/v8-coverage.js). null when V8 mode is off.
_v8acc: [
async ({}, use) => {
if (!V8_COVERAGE) {
await use(null)
return
}
const { createAccumulator } = await import('./v8-coverage.js')
const acc = createAccumulator()
await use(acc)
await acc.flush()
},
{ scope: 'worker' },
],
page: async ({ page, _v8acc }, use) => {
// V8 coverage path: collect native Chromium coverage (cheap), hand it to the
// worker accumulator on teardown. Avoids running an instrumented bundle.
if (V8_COVERAGE) {
const { startV8 } = await import('./v8-coverage.js')
await startV8(page)
await use(page)
try {
_v8acc.add(await page.coverage.stopJSCoverage())
} catch {
// page already closed — nothing to collect
}
return
}
export const test = base.extend({
page: async ({ page }, use) => {
await use(page)
let coverage
@@ -37,4 +69,5 @@ export const test = base.extend({
},
})
export const test = withCoverage
export { expect }

88
core/http/react-ui/e2e/v8-coverage.js vendored Normal file
View File

@@ -0,0 +1,88 @@
// V8 -> istanbul coverage harvest for the Playwright suite.
//
// When PW_V8_COVERAGE=1 the suite runs against a NON-instrumented build (built
// with COVERAGE_V8=true, which only adds source maps). Chromium collects native
// V8 coverage with near-zero runtime overhead; we convert it back to per-source
// istanbul data via v8-to-istanbul (using the on-disk source maps), filter to
// src/**, and write the same .nyc_output/*.json the istanbul path produced — so
// `nyc report` and the strict baseline gate are unchanged.
//
// Conversion (v8-to-istanbul load() parses the large bundle source map) is the
// expensive part, so we do NOT convert per test. Instead each worker collects
// raw V8 coverage from every test, merges it with @bcoe/v8-coverage (which sums
// counts and reconciles overlapping ranges correctly — applyCoverage can't be
// called repeatedly, it pushes/overwrites), and converts ONCE at worker
// teardown. That cuts conversions from ~152 (per test) to ~1 per worker.
import v8toIstanbul from 'v8-to-istanbul'
import libCoverage from 'istanbul-lib-coverage'
import { mergeProcessCovs } from '@bcoe/v8-coverage'
import { mkdirSync, writeFileSync, existsSync } from 'node:fs'
import { randomUUID } from 'node:crypto'
import path from 'node:path'
const COVERAGE_DIR = path.resolve(process.cwd(), '.nyc_output')
const DIST_ASSETS = path.resolve(process.cwd(), 'dist', 'assets')
// Absolute app source dir. Match on this (not a bare "/src/" substring) — the
// repo itself lives under .../go/src/..., so a substring check would collide.
const SRC_DIR = path.resolve(process.cwd(), 'src') + path.sep
// Only our own bundle chunks under /assets/*.js carry app source maps.
const APP_CHUNK = /\/assets\/([^/?]+\.js)(\?|$)/
export async function startV8(page) {
// resetOnNavigation:false so hard navigations (goto) within a test accumulate.
await page.coverage.startJSCoverage({ resetOnNavigation: false })
}
// One accumulator per worker (created by the worker-scoped fixture).
export function createAccumulator() {
const processCovs = []
return {
// Called on each test teardown with that test's V8 coverage entries.
add(entries) {
const result = entries
.filter((e) => APP_CHUNK.test(e.url))
// Keep only structural fields (drop the ~1MB `source` per entry — it's
// re-read from disk at convert time — to bound per-worker memory).
.map((e) => ({ scriptId: e.scriptId || e.url, url: e.url, functions: e.functions }))
if (result.length) processCovs.push({ result })
},
// Called once at worker teardown: merge all tests' coverage, convert, write.
async flush() {
if (processCovs.length === 0) return
const merged = mergeProcessCovs(processCovs)
const map = libCoverage.createCoverageMap({})
for (const script of merged.result) {
const m = APP_CHUNK.exec(script.url)
if (!m) continue
const diskPath = path.join(DIST_ASSETS, m[1])
if (!existsSync(diskPath)) continue
// v8-to-istanbul auto-loads source + sibling .map from disk; the served
// bytes match dist, so the V8 ranges line up.
const converter = v8toIstanbul(diskPath, 0)
try {
await converter.load()
converter.applyCoverage(script.functions)
const data = converter.toIstanbul()
for (const [key, fileCov] of Object.entries(data)) {
// v8-to-istanbul keys are already absolute; keep only app sources.
if (!key.startsWith(SRC_DIR) || key.includes(`${path.sep}node_modules${path.sep}`)) continue
map.merge({ [key]: fileCov })
}
} catch {
// skip a chunk we couldn't convert
} finally {
converter.destroy()
}
}
const json = map.toJSON()
if (Object.keys(json).length === 0) return
mkdirSync(COVERAGE_DIR, { recursive: true })
writeFileSync(path.join(COVERAGE_DIR, `v8-${randomUUID()}.json`), JSON.stringify(json))
},
}
}

View File

@@ -12,6 +12,7 @@
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"build:coverage": "COVERAGE=true vite build",
"build:coverage-v8": "COVERAGE_V8=true vite build",
"coverage:report": "nyc report"
},
"dependencies": {
@@ -42,6 +43,7 @@
"yaml": "^2.8.3"
},
"devDependencies": {
"@bcoe/v8-coverage": "^1.0.2",
"@eslint/js": "^9.27.0",
"@playwright/test": "1.58.2",
"@vitejs/plugin-react": "^6.0.2",
@@ -51,6 +53,7 @@
"globals": "^16.1.0",
"i18next-parser": "^9.4.0",
"nyc": "^18.0.0",
"v8-to-istanbul": "^9.3.0",
"vite": "^8.0.14",
"vite-plugin-istanbul": "^9.0.0"
}

View File

@@ -1,4 +1,4 @@
import { useState, useEffect, useRef } from 'react'
import { useState, useEffect, useRef, Suspense } from 'react'
import { Outlet, useLocation, useNavigate } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
import Sidebar from './components/Sidebar'
@@ -122,7 +122,14 @@ export default function App() {
</header>
<div className="main-content-inner">
<div className="page-transition" key={location.pathname}>
<Outlet context={{ addToast }} />
{/* Per-route Suspense catches React.lazy chunk loads (router.jsx)
here, inside the App layout. Without it, suspension would bubble
up to main.jsx's outer boundary and unmount the sidebar/header
on every navigation. fallback={null} keeps the shell stable; the
page-content area briefly blanks while the chunk arrives. */}
<Suspense fallback={null}>
<Outlet context={{ addToast }} />
</Suspense>
</div>
</div>
{!isChatRoute && (

View File

@@ -4,7 +4,7 @@ import { getArtifactIcon } from '../utils/artifacts'
import { safeHref } from '../utils/url'
import { copyToClipboard } from '../utils/clipboard'
import DOMPurify from 'dompurify'
import hljs from 'highlight.js'
import hljs from '../utils/hljs'
export default function CanvasPanel({ artifacts, selectedId, onSelect, onClose }) {
const [showPreview, setShowPreview] = useState(true)

View File

@@ -6,6 +6,7 @@ import LanguageSwitcher from './LanguageSwitcher'
import { useAuth } from '../context/AuthContext'
import { useBranding } from '../contexts/BrandingContext'
import { apiUrl } from '../utils/basePath'
import { preloadRoute } from '../router'
const COLLAPSED_KEY = 'localai_sidebar_collapsed'
const SECTIONS_KEY = 'localai_sidebar_sections'
@@ -85,6 +86,10 @@ const sections = [
function NavItem({ item, onClose, collapsed }) {
const { t } = useTranslation('nav')
const label = t(item.labelKey)
// Warm the route's lazy chunk before the user clicks. Touch fires ~150ms
// before the synthetic click on mobile; mouseenter/focus cover desktop and
// keyboard. The underlying import() is memoised so multiple triggers are free.
const preload = () => preloadRoute(item.path)
return (
<NavLink
to={item.path}
@@ -93,6 +98,9 @@ function NavItem({ item, onClose, collapsed }) {
`nav-item ${isActive ? 'active' : ''}`
}
onClick={onClose}
onMouseEnter={preload}
onFocus={preload}
onTouchStart={preload}
title={collapsed ? label : undefined}
>
<i className={`${item.icon} nav-icon`} />
@@ -296,6 +304,9 @@ export default function Sidebar({ isOpen, onClose }) {
<button
className="sidebar-user-link"
onClick={() => { navigate('/app/account'); onClose?.() }}
onMouseEnter={() => preloadRoute('/app/account')}
onFocus={() => preloadRoute('/app/account')}
onTouchStart={() => preloadRoute('/app/account')}
title={t('accountSettings')}
>
{user.avatarUrl ? (

View File

@@ -1,54 +1,81 @@
import { lazy } from 'react'
import { createBrowserRouter, Navigate, useParams } from 'react-router-dom'
import { routerBasename } from './utils/basePath'
import App from './App'
import Home from './pages/Home'
import Chat from './pages/Chat'
import Models from './pages/Models'
import Manage from './pages/Manage'
import ImageGen from './pages/ImageGen'
import VideoGen from './pages/VideoGen'
import TTS from './pages/TTS'
import Sound from './pages/Sound'
import AudioTransform from './pages/AudioTransform'
import Talk from './pages/Talk'
import Backends from './pages/Backends'
import Settings from './pages/Settings'
import Traces from './pages/Traces'
import P2P from './pages/P2P'
import Agents from './pages/Agents'
import AgentCreate from './pages/AgentCreate'
import AgentChat from './pages/AgentChat'
import AgentStatus from './pages/AgentStatus'
import Collections from './pages/Collections'
import CollectionDetails from './pages/CollectionDetails'
import Skills from './pages/Skills'
import SkillEdit from './pages/SkillEdit'
import AgentJobs from './pages/AgentJobs'
import AgentTaskDetails from './pages/AgentTaskDetails'
import AgentJobDetails from './pages/AgentJobDetails'
import ModelEditor from './pages/ModelEditor'
// PipelineEditor removed — the Model Editor with templates handles all model types
import ImportModel from './pages/ImportModel'
import BackendLogs from './pages/BackendLogs'
import Explorer from './pages/Explorer'
import Login from './pages/Login'
import FineTune from './pages/FineTune'
import Quantize from './pages/Quantize'
import Studio from './pages/Studio'
import FaceRecognition from './pages/FaceRecognition'
import VoiceRecognition from './pages/VoiceRecognition'
import Nodes from './pages/Nodes'
import NodeBackendLogs from './pages/NodeBackendLogs'
import NotFound from './pages/NotFound'
import Usage from './pages/Usage'
import Users from './pages/Users'
import Middleware from './pages/Middleware'
import Account from './pages/Account'
import RequireAdmin from './components/RequireAdmin'
import RequireAuth from './components/RequireAuth'
import RequireAuthEnabled from './components/RequireAuthEnabled'
import RequireFeature from './components/RequireFeature'
// Pages are code-split: each becomes its own chunk loaded on demand, so a route
// no longer drags every other page (and its heavy deps — CodeMirror, the MCP
// SDK, yaml, marked) into the initial bundle. The <Suspense> boundary in
// App.jsx (around <Outlet/>) shows nothing while a chunk loads, keeping the
// sidebar/header mounted.
//
// `page(key, loader)` registers the dynamic import under a route-segment key
// (the first segment after /app/) so a NavLink can warm the chunk on hover via
// `preloadRoute('/app/chat')`. Dynamic import() is memoised by the module
// loader, so a preloaded chunk is reused — not re-fetched — when the user
// actually navigates. Pages with `key: null` aren't sidebar-reachable; they
// still code-split, they just won't be preloaded from the nav.
const preloaders = {}
function page(key, loader) {
if (key !== null) preloaders[key] = loader
return lazy(loader)
}
export function preloadRoute(path) {
if (!path) return
const m = path.match(/^\/app(?:\/([^/?#]*))?/)
if (!m) return
preloaders[m[1] ?? '']?.().catch(() => { /* network blip — real click will retry */ })
}
const Home = page('', () => import('./pages/Home'))
const Chat = page('chat', () => import('./pages/Chat'))
const Models = page('models', () => import('./pages/Models'))
const Manage = page('manage', () => import('./pages/Manage'))
const ImageGen = page('image', () => import('./pages/ImageGen'))
const VideoGen = page('video', () => import('./pages/VideoGen'))
const TTS = page('tts', () => import('./pages/TTS'))
const Sound = page('sound', () => import('./pages/Sound'))
const AudioTransform = page('transform', () => import('./pages/AudioTransform'))
const Talk = page('talk', () => import('./pages/Talk'))
const Backends = page('backends', () => import('./pages/Backends'))
const Settings = page('settings', () => import('./pages/Settings'))
const Traces = page('traces', () => import('./pages/Traces'))
const P2P = page('p2p', () => import('./pages/P2P'))
const Agents = page('agents', () => import('./pages/Agents'))
const AgentCreate = page(null, () => import('./pages/AgentCreate'))
const AgentChat = page(null, () => import('./pages/AgentChat'))
const AgentStatus = page(null, () => import('./pages/AgentStatus'))
const Collections = page('collections', () => import('./pages/Collections'))
const CollectionDetails = page(null, () => import('./pages/CollectionDetails'))
const Skills = page('skills', () => import('./pages/Skills'))
const SkillEdit = page(null, () => import('./pages/SkillEdit'))
const AgentJobs = page('agent-jobs', () => import('./pages/AgentJobs'))
const AgentTaskDetails = page(null, () => import('./pages/AgentTaskDetails'))
const AgentJobDetails = page(null, () => import('./pages/AgentJobDetails'))
const ModelEditor = page(null, () => import('./pages/ModelEditor'))
// PipelineEditor removed — the Model Editor with templates handles all model types
const ImportModel = page(null, () => import('./pages/ImportModel'))
const BackendLogs = page(null, () => import('./pages/BackendLogs'))
const Explorer = page(null, () => import('./pages/Explorer'))
const Login = page(null, () => import('./pages/Login'))
const FineTune = page('fine-tune', () => import('./pages/FineTune'))
const Quantize = page('quantize', () => import('./pages/Quantize'))
const Studio = page('studio', () => import('./pages/Studio'))
const FaceRecognition = page('face', () => import('./pages/FaceRecognition'))
const VoiceRecognition = page('voice', () => import('./pages/VoiceRecognition'))
const Nodes = page('nodes', () => import('./pages/Nodes'))
const NodeBackendLogs = page(null, () => import('./pages/NodeBackendLogs'))
const NotFound = page(null, () => import('./pages/NotFound'))
const Usage = page('usage', () => import('./pages/Usage'))
const Users = page('users', () => import('./pages/Users'))
const Middleware = page('middleware', () => import('./pages/Middleware'))
const Account = page('account', () => import('./pages/Account'))
function BrowseRedirect() {
const { '*': splat } = useParams()
return <Navigate to={`/app/${splat || ''}`} replace />

View File

@@ -1,6 +1,6 @@
import { Marked } from 'marked'
import DOMPurify from 'dompurify'
import hljs from 'highlight.js'
import hljs from './hljs'
import { apiUrl } from './basePath'
const FENCE_REGEX = /```(\w*)\n([\s\S]*?)```/g
@@ -119,12 +119,17 @@ export function getArtifactIcon(type, language) {
const artifactMarked = new Marked({
renderer: {
code({ text, lang }) {
// Will be overridden per-call
// Match markdown.js's fallback: when the language is unknown (not in
// the curated hljs set, see utils/hljs.js), use highlightAuto so the
// block still picks up theme colors — otherwise the same fenced block
// would render differently in chat (auto-highlighted) vs artifact card
// (plain text).
if (lang && hljs.getLanguage(lang)) {
const highlighted = hljs.highlight(text, { language: lang }).value
return `<pre><code class="hljs language-${lang}">${highlighted}</code></pre>`
}
return `<pre><code>${text.replace(/</g, '&lt;').replace(/>/g, '&gt;')}</code></pre>`
const highlighted = hljs.highlightAuto(text).value
return `<pre><code class="hljs">${highlighted}</code></pre>`
},
},
breaks: true,

53
core/http/react-ui/src/utils/hljs.js vendored Normal file
View File

@@ -0,0 +1,53 @@
// Curated highlight.js build.
//
// `import hljs from 'highlight.js'` pulls in the full bundle — ~190 language
// grammars, ~893 KB raw / ~294 KB gzip, the single biggest item in the app
// bundle (measured). We render code blocks from chat/markdown/canvas only, and
// only ever for a handful of common languages, so we import the lightweight
// core and register just the grammars below. `highlightAuto` still works — it
// auto-detects among the registered set, which covers what an LLM realistically
// emits. Import hljs from THIS module, never directly from 'highlight.js'.
import hljs from 'highlight.js/lib/core'
import bash from 'highlight.js/lib/languages/bash'
import c from 'highlight.js/lib/languages/c'
import cpp from 'highlight.js/lib/languages/cpp'
import csharp from 'highlight.js/lib/languages/csharp'
import css from 'highlight.js/lib/languages/css'
import diff from 'highlight.js/lib/languages/diff'
import dockerfile from 'highlight.js/lib/languages/dockerfile'
import go from 'highlight.js/lib/languages/go'
import ini from 'highlight.js/lib/languages/ini'
import java from 'highlight.js/lib/languages/java'
import javascript from 'highlight.js/lib/languages/javascript'
import json from 'highlight.js/lib/languages/json'
import kotlin from 'highlight.js/lib/languages/kotlin'
import lua from 'highlight.js/lib/languages/lua'
import makefile from 'highlight.js/lib/languages/makefile'
import markdown from 'highlight.js/lib/languages/markdown'
import php from 'highlight.js/lib/languages/php'
import plaintext from 'highlight.js/lib/languages/plaintext'
import powershell from 'highlight.js/lib/languages/powershell'
import python from 'highlight.js/lib/languages/python'
import ruby from 'highlight.js/lib/languages/ruby'
import rust from 'highlight.js/lib/languages/rust'
import scss from 'highlight.js/lib/languages/scss'
import shell from 'highlight.js/lib/languages/shell'
import sql from 'highlight.js/lib/languages/sql'
import swift from 'highlight.js/lib/languages/swift'
import typescript from 'highlight.js/lib/languages/typescript'
import xml from 'highlight.js/lib/languages/xml'
import yaml from 'highlight.js/lib/languages/yaml'
// Each grammar registers its own aliases (e.g. js→javascript, ts→typescript,
// yml→yaml, html→xml, sh→bash, py→python), so hljs.getLanguage('js') resolves.
const languages = {
bash, c, cpp, csharp, css, diff, dockerfile, go, ini, java, javascript,
json, kotlin, lua, makefile, markdown, php, plaintext, powershell, python,
ruby, rust, scss, shell, sql, swift, typescript, xml, yaml,
}
for (const [name, lang] of Object.entries(languages)) {
hljs.registerLanguage(name, lang)
}
export default hljs

View File

@@ -1,6 +1,6 @@
import { marked } from 'marked'
import DOMPurify from 'dompurify'
import hljs from 'highlight.js'
import hljs from './hljs'
marked.setOptions({
highlight(code, lang) {

View File

@@ -9,6 +9,11 @@ const backendUrl = process.env.LOCALAI_URL || 'http://localhost:8080'
// fixture (e2e/coverage-fixtures.js). Off by default so normal/dev/prod builds
// carry no instrumentation overhead.
const coverage = process.env.COVERAGE === 'true'
// COVERAGE_V8=true produces a NON-instrumented build with source maps, so the
// Playwright coverage fixture can collect Chromium V8 coverage (near-zero
// runtime overhead, unlike istanbul's build-time counters) and map it back to
// source via v8-to-istanbul. Mutually exclusive with COVERAGE.
const coverageV8 = process.env.COVERAGE_V8 === 'true'
export default defineConfig({
plugins: [
@@ -50,5 +55,20 @@ export default defineConfig({
build: {
outDir: 'dist',
assetsDir: 'assets',
// Source maps are needed only to map V8 coverage back to original sources.
sourcemap: coverageV8,
rollupOptions: {
output: {
// The coverage build inlines all dynamic imports into a single chunk.
// The app is route-code-split (router.jsx uses React.lazy), so a normal
// build emits ~50 lazy chunks. V8 coverage only sees chunks a test
// actually loaded, so untested pages would silently drop out of the
// denominator and inflate the percentage. Bundling everything into one
// chunk for the coverage build keeps the denominator complete and the
// measurement invariant to how production is split. Production builds
// (COVERAGE_V8 unset) keep code-splitting for fast first paint.
inlineDynamicImports: coverageV8,
},
},
},
})

View File

@@ -332,5 +332,41 @@ var _ = Describe("LLM tests", func() {
// Should only extract text parts
Expect(protoMessages[0].Content).To(Equal("Hello"))
})
// Regression for mudler/LocalAI#10039: ToProto is the path taken by
// UseTokenizerTemplate backends (e.g. imported GGUFs, where the backend
// applies the GGUF's jinja template to the raw messages). It reads
// Content, not StringContent — so a message that only populated
// StringContent (the shape /v1/responses produced before the fix)
// reached the backend with empty content. These two cases pin that
// contract: Content is authoritative, and producers must set it.
It("emits empty content when only StringContent is set (Content nil)", func() {
messages := Messages{
{
Role: "user",
StringContent: "Hello",
},
}
protoMessages := messages.ToProto()
Expect(protoMessages).To(HaveLen(1))
Expect(protoMessages[0].Content).To(BeEmpty())
})
It("carries Content through to proto regardless of StringContent", func() {
messages := Messages{
{
Role: "user",
Content: "Hello",
StringContent: "Hello",
},
}
protoMessages := messages.ToProto()
Expect(protoMessages).To(HaveLen(1))
Expect(protoMessages[0].Content).To(Equal("Hello"))
})
})
})

View File

@@ -111,7 +111,11 @@ func (e *Evaluator) TemplateMessages(input schema.OpenAIRequest, messages []sche
}
}
r := config.Roles[role]
contentExists := i.Content != nil && i.StringContent != ""
// Treat StringContent as the source of truth — every downstream fallback branch in this
// function reads StringContent, not Content. Gating on both with && silently drops
// messages that have StringContent set but Content nil (e.g. /v1/responses string-input
// before mudler/LocalAI#10039 fix).
contentExists := i.StringContent != ""
fcall := i.FunctionCall
if len(i.ToolCalls) > 0 {

View File

@@ -218,4 +218,41 @@ var _ = Describe("Templates", func() {
})
}
})
// Regression test for mudler/LocalAI#10039: when a model has no Go-side
// TemplateConfig.ChatMessage block (e.g. backends that rely on the GGUF's
// jinja template), TemplateMessages falls through to the role-prefix path.
// That path must still render messages whose StringContent is populated but
// Content (any) is nil — which is the shape /v1/responses produced before
// the fix to convertORInputToMessages.
Context("fallback path with StringContent-only message (no ChatMessage template)", func() {
var evaluator *Evaluator
BeforeEach(func() {
evaluator = NewEvaluator("")
})
It("renders the role prefix and content when only StringContent is set", func() {
cfg := &config.ModelConfig{
TemplateConfig: config.TemplateConfig{},
Roles: map[string]string{"user": "USER: "},
}
messages := []schema.Message{
{
Role: "user",
StringContent: "hello",
// Content intentionally left nil — reproduces /v1/responses string-input.
},
}
templated := evaluator.TemplateMessages(schema.OpenAIRequest{}, messages, cfg, []functions.Function{}, false)
Expect(templated).To(Equal("USER: hello"), templated)
})
It("renders content even with no role mapping", func() {
cfg := &config.ModelConfig{
TemplateConfig: config.TemplateConfig{},
}
messages := []schema.Message{
{Role: "user", StringContent: "hello"},
}
templated := evaluator.TemplateMessages(schema.OpenAIRequest{}, messages, cfg, []functions.Function{}, false)
Expect(templated).To(Equal("hello"), templated)
})
})
})

View File

@@ -3,35 +3,7 @@
url: "github:mudler/LocalAI/gallery/virtual.yaml@master"
urls:
- https://huggingface.co/Jackrong/Qwopus3.5-9B-Coder-MTP-GGUF
description: |
# 🌟 Qwopus3.5-9B-v3.5
## 💡 Model Overview & v3.5 Design
Qwopus3.5-9B-v3.5 is a **data-scaled continuation** of the Qwopus3.5-9B-v3 model.
The training data in v3.5 is expanded to cover a broader range of domains, including mathematics, programming, puzzle-solving, multilingual dialogue, instruction-following, multi-turn interactions, and STEM-related tasks.
Qwopus3.5-9B-v3.5 is a reasoning-enhanced model based on **Qwen3.5-9B**, designed for:
- 🧩 Structured reasoning
- 🔧 Tool-augmented workflows
- 🔁 Multi-step agentic tasks
- ⚡ Token-efficient inference
Compared with Qwopus3.5-9B-v3, **3.5 version does not introduce a new architecture, RL stage, or template redesign**.
This version is trained with approximately **2× more SFT data**.
## 🎯 Motivation & Generalization Insight
The motivation behind v3.5 comes from a simple observation:
> This work is motivated by the hypothesis that scaling high-quality SFT data may further enhance the generalization ability of large language models.
In earlier Qwopus3.5 experiments, structured reasoning was observed to improve both **accuracy and efficiency**:
...
description: "# \U0001F31F Qwopus3.5-9B-v3.5\n\n## \U0001F4A1 Model Overview & v3.5 Design\n\nQwopus3.5-9B-v3.5 is a **data-scaled continuation** of the Qwopus3.5-9B-v3 model.\n\nThe training data in v3.5 is expanded to cover a broader range of domains, including mathematics, programming, puzzle-solving, multilingual dialogue, instruction-following, multi-turn interactions, and STEM-related tasks.\n\nQwopus3.5-9B-v3.5 is a reasoning-enhanced model based on **Qwen3.5-9B**, designed for:\n\n - \U0001F9E9 Structured reasoning\n - \U0001F527 Tool-augmented workflows\n - \U0001F501 Multi-step agentic tasks\n - ⚡ Token-efficient inference\n\nCompared with Qwopus3.5-9B-v3, **3.5 version does not introduce a new architecture, RL stage, or template redesign**.\n\nThis version is trained with approximately **2× more SFT data**.\n\n## \U0001F3AF Motivation & Generalization Insight\n\nThe motivation behind v3.5 comes from a simple observation:\n\n> This work is motivated by the hypothesis that scaling high-quality SFT data may further enhance the generalization ability of large language models.\n\nIn earlier Qwopus3.5 experiments, structured reasoning was observed to improve both **accuracy and efficiency**:\n\n...\n"
license: "apache-2.0"
tags:
- llm
@@ -67,26 +39,7 @@
url: "github:mudler/LocalAI/gallery/virtual.yaml@master"
urls:
- https://huggingface.co/Jackrong/Qwopus3.6-27B-v2-MTP-GGUF
description: |
🪐 Qwopus3.6-27B-v2-MTP
MTP Release
Multi-Token Prediction reasoning model fine-tuned from Qwen3.6-27B
🧬 Trace Inversion & Negentropy
🧠 27B Parameters
⚡ Speculative Decoding
🛠️ Coding / DevOps / Math
💡 What is Qwopus3.6-27B-v2-MTP?
🪐 Qwopus3.6-27B-v2-MTP is a speed-oriented reasoning release built on top of Qwen3.6-27B. It keeps the Qwopus line's focus on reconstructed reasoning traces, coding discipline, DevOps procedures, and mathematical derivations, while adding Multi-Token Prediction for faster generation. The goal is simple: preserve the depth and structure of a 27B reasoning model while making real interactive use noticeably faster.
⚡ MTP DecodingAuxiliary future-token prediction improves throughput on long reasoning, code, math, and strict-format prompts.
🧩 Structured ReasoningInherits the Qwopus training recipe built around reconstructed step-by-step reasoning trajectories.
🧪 GB10 TestedValidated on a 30-question local benchmark across Logic, Coding, DevOps, Math, and Edge tasks.
🚀 Practical SpeedDesigned for workflows where strong answers matter, but waiting several extra minutes per task does not.
...
description: "\U0001FA90 Qwopus3.6-27B-v2-MTP\nMTP Release\n\nMulti-Token Prediction reasoning model fine-tuned from Qwen3.6-27B\n\n\U0001F9EC Trace Inversion & Negentropy\n\U0001F9E0 27B Parameters\n⚡ Speculative Decoding\n\U0001F6E0 Coding / DevOps / Math\n\n\U0001F4A1 What is Qwopus3.6-27B-v2-MTP?\n\U0001FA90 Qwopus3.6-27B-v2-MTP is a speed-oriented reasoning release built on top of Qwen3.6-27B. It keeps the Qwopus line's focus on reconstructed reasoning traces, coding discipline, DevOps procedures, and mathematical derivations, while adding Multi-Token Prediction for faster generation. The goal is simple: preserve the depth and structure of a 27B reasoning model while making real interactive use noticeably faster.\n\n⚡ MTP DecodingAuxiliary future-token prediction improves throughput on long reasoning, code, math, and strict-format prompts.\n\U0001F9E9 Structured ReasoningInherits the Qwopus training recipe built around reconstructed step-by-step reasoning trajectories.\n\U0001F9EA GB10 TestedValidated on a 30-question local benchmark across Logic, Coding, DevOps, Math, and Edge tasks.\n\U0001F680 Practical SpeedDesigned for workflows where strong answers matter, but waiting several extra minutes per task does not.\n\n...\n"
license: "apache-2.0"
tags:
- llm
@@ -6208,6 +6161,7 @@
files:
- filename: rfdetr-nano-q8_0.gguf
uri: huggingface://mudler/rfdetr-cpp-nano/rfdetr-nano-q8_0.gguf
sha256: 940084c60a780f1a19a51458ae3a601454b3b843675fa0713ff43ae5bccc0d9b
- name: rfdetr-cpp-base
url: github:mudler/LocalAI/gallery/virtual.yaml@master
urls:
@@ -6233,6 +6187,7 @@
files:
- filename: rfdetr-base-f16.gguf
uri: huggingface://mudler/rfdetr-cpp-base/rfdetr-base-f16.gguf
sha256: 8a68b21a90478564bcbb758557069a618d96e25e7c358207fd85ba45b90faf52
- name: rfdetr-cpp-small
url: github:mudler/LocalAI/gallery/virtual.yaml@master
urls:

View File

@@ -4,13 +4,16 @@
#
# Compares the total line coverage in an nyc coverage-summary.json against a
# committed baseline and fails (exit 1) if it dropped by more than
# UI_COVERAGE_TOLERANCE percentage points (default 1.0). The React UI e2e suite
# UI_COVERAGE_TOLERANCE percentage points (default 0.8). The React UI e2e suite
# drives the real app, so a removed feature or deleted spec shows up as a
# coverage drop here.
#
# UI e2e line coverage is NOT deterministic: async/debounced paths (e.g. the
# VRAM estimate's 500ms debounce) mean identical specs vary ~0.5pp run-to-run.
# The tolerance absorbs that jitter; keep it just above the observed wobble.
# VRAM estimate's 500ms debounce) mean identical specs vary run-to-run. With the
# V8 path's single-chunk coverage build (vite.config.js inlineDynamicImports)
# the observed wobble is ~0.5pp, similar to the old istanbul path. The tolerance
# absorbs that jitter — keep it just above the observed wobble so a real ~1pp
# regression still trips the gate.
# (The Go gate carries a smaller tolerance for the same reason — its e2e slice.)
#
# When coverage rises meaningfully, regenerate and commit the baseline with:
@@ -19,7 +22,7 @@ set -eu
summary="${1:?usage: ui-coverage-check.sh SUMMARY_JSON BASELINE_FILE}"
baseline_file="${2:?usage: ui-coverage-check.sh SUMMARY_JSON BASELINE_FILE}"
tolerance="${UI_COVERAGE_TOLERANCE:-1.0}"
tolerance="${UI_COVERAGE_TOLERANCE:-0.8}"
if [ ! -f "$summary" ]; then
echo "ui-coverage-check: coverage summary not found: $summary" >&2