mirror of
https://github.com/mudler/LocalAI.git
synced 2026-05-28 10:38:05 -04:00
Compare commits
2 Commits
master
...
update/RFD
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca4222e3e3 | ||
|
|
36bf088f34 |
29
Makefile
29
Makefile
@@ -1313,13 +1313,6 @@ 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
|
||||
@@ -1329,24 +1322,22 @@ PLAYWRIGHT_WORKERS_FLAG = $(if $(UI_TEST_WORKERS),--workers=$(UI_TEST_WORKERS),)
|
||||
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 $(PLAYWRIGHT_WORKERS_FLAG)
|
||||
cd core/http/react-ui && sh $(CURDIR)/scripts/ensure-playwright-browser.sh && bunx playwright test
|
||||
|
||||
## 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.)
|
||||
## 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.
|
||||
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-v8 ) && \
|
||||
( cd core/http/react-ui && bun install && bun run build:coverage ) && \
|
||||
$(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 && \
|
||||
PW_V8_COVERAGE=1 bunx playwright test $(PLAYWRIGHT_WORKERS_FLAG) && bun run coverage:report )
|
||||
bunx playwright test && 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.
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# ds4 backend Makefile.
|
||||
#
|
||||
# Upstream pin lives below as DS4_VERSION?=072bc0feb187be5f374c08b16d0045e1ad7bc9bc
|
||||
# Upstream pin lives below as DS4_VERSION?=e8e8779b261c10f36ad6270ba732c8f0be5b62e3
|
||||
# (.github/bump_deps.sh) can find and update it - matches the
|
||||
# llama-cpp / ik-llama-cpp / turboquant convention.
|
||||
|
||||
DS4_VERSION?=072bc0feb187be5f374c08b16d0045e1ad7bc9bc
|
||||
DS4_VERSION?=e8e8779b261c10f36ad6270ba732c8f0be5b62e3
|
||||
DS4_REPO?=https://github.com/antirez/ds4
|
||||
|
||||
CURRENT_MAKEFILE_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
IK_LLAMA_VERSION?=3bf7e836c2c5a895e8d12d3eb7e398ae7ab2f9ce
|
||||
IK_LLAMA_VERSION?=d2da6da05c73aeb658a3d1751f386c24e6963856
|
||||
LLAMA_REPO?=https://github.com/ikawrakow/ik_llama.cpp
|
||||
|
||||
CMAKE_ARGS?=
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
LLAMA_VERSION?=aa50b2c2ae91326d5aad956ceeb015d1d48e626b
|
||||
LLAMA_VERSION?=0d18aaa9d1a8af3df9abccd828e22eeaac7f840b
|
||||
LLAMA_REPO?=https://github.com/ggerganov/llama.cpp
|
||||
|
||||
CMAKE_ARGS?=
|
||||
|
||||
@@ -11,7 +11,7 @@ JOBS?=$(shell nproc --ignore=1)
|
||||
# build; leaving this on `master` always picks up the latest C-API surface
|
||||
# (incl. the per-detection accessor functions used by gorfdetrcpp.go).
|
||||
RFDETR_REPO?=https://github.com/mudler/rf-detr.cpp.git
|
||||
RFDETR_VERSION?=main
|
||||
RFDETR_VERSION?=ecf64d77b09013e7e90af6a17b9ce884e7daa86c
|
||||
|
||||
ifeq ($(NATIVE),false)
|
||||
CMAKE_ARGS+=-DGGML_NATIVE=OFF
|
||||
|
||||
@@ -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?=29ab511fc75f89fbab148665eab1a8e10a139a72
|
||||
STABLEDIFFUSION_GGML_VERSION?=92dc7268fc4ffb0c0cc0bd52dfcefea91326e797
|
||||
|
||||
CMAKE_ARGS+=-DGGML_MAX_NAME=128
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ JOBS?=$(shell nproc --ignore=1)
|
||||
|
||||
# whisper.cpp version
|
||||
WHISPER_REPO?=https://github.com/ggml-org/whisper.cpp
|
||||
WHISPER_CPP_VERSION?=6dcdd6536456158667747f724d6bd3a2ceaa8d88
|
||||
WHISPER_CPP_VERSION?=27101c01dcac1676e2b6422256233cd0f1f9ae28
|
||||
SO_TARGET?=libgowhisper.so
|
||||
|
||||
CMAKE_ARGS+=-DBUILD_SHARED_LIBS=OFF
|
||||
|
||||
@@ -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", Content: input.Instructions, StringContent: input.Instructions}}, messages...)
|
||||
messages = append([]schema.Message{{Role: "system", 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", Content: v, StringContent: v}}, nil
|
||||
return []schema.Message{{Role: "user", StringContent: v}}, nil
|
||||
case []any:
|
||||
// Array of items
|
||||
for _, itemRaw := range v {
|
||||
@@ -309,16 +309,6 @@ 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)
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
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())
|
||||
})
|
||||
})
|
||||
@@ -32,7 +32,6 @@
|
||||
"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",
|
||||
@@ -42,7 +41,6 @@
|
||||
"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",
|
||||
},
|
||||
@@ -83,8 +81,6 @@
|
||||
|
||||
"@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=="],
|
||||
@@ -271,8 +267,6 @@
|
||||
|
||||
"@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=="],
|
||||
@@ -989,8 +983,6 @@
|
||||
|
||||
"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=="],
|
||||
@@ -1129,8 +1121,6 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -1 +1 @@
|
||||
38.29
|
||||
30.66
|
||||
37
core/http/react-ui/e2e/coverage-fixtures.js
vendored
37
core/http/react-ui/e2e/coverage-fixtures.js
vendored
@@ -15,41 +15,9 @@ 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
|
||||
@@ -69,5 +37,4 @@ const withCoverage = base.extend({
|
||||
},
|
||||
})
|
||||
|
||||
export const test = withCoverage
|
||||
export { expect }
|
||||
|
||||
88
core/http/react-ui/e2e/v8-coverage.js
vendored
88
core/http/react-ui/e2e/v8-coverage.js
vendored
@@ -1,88 +0,0 @@
|
||||
// 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))
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,6 @@
|
||||
"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": {
|
||||
@@ -43,7 +42,6 @@
|
||||
"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",
|
||||
@@ -53,7 +51,6 @@
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useRef, Suspense } from 'react'
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { Outlet, useLocation, useNavigate } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Sidebar from './components/Sidebar'
|
||||
@@ -122,14 +122,7 @@ export default function App() {
|
||||
</header>
|
||||
<div className="main-content-inner">
|
||||
<div className="page-transition" key={location.pathname}>
|
||||
{/* 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>
|
||||
<Outlet context={{ addToast }} />
|
||||
</div>
|
||||
</div>
|
||||
{!isChatRoute && (
|
||||
|
||||
@@ -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 '../utils/hljs'
|
||||
import hljs from 'highlight.js'
|
||||
|
||||
export default function CanvasPanel({ artifacts, selectedId, onSelect, onClose }) {
|
||||
const [showPreview, setShowPreview] = useState(true)
|
||||
|
||||
@@ -6,7 +6,6 @@ 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'
|
||||
@@ -86,10 +85,6 @@ 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}
|
||||
@@ -98,9 +93,6 @@ 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`} />
|
||||
@@ -304,9 +296,6 @@ 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 ? (
|
||||
|
||||
@@ -1,81 +1,54 @@
|
||||
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 />
|
||||
|
||||
11
core/http/react-ui/src/utils/artifacts.js
vendored
11
core/http/react-ui/src/utils/artifacts.js
vendored
@@ -1,6 +1,6 @@
|
||||
import { Marked } from 'marked'
|
||||
import DOMPurify from 'dompurify'
|
||||
import hljs from './hljs'
|
||||
import hljs from 'highlight.js'
|
||||
import { apiUrl } from './basePath'
|
||||
|
||||
const FENCE_REGEX = /```(\w*)\n([\s\S]*?)```/g
|
||||
@@ -119,17 +119,12 @@ export function getArtifactIcon(type, language) {
|
||||
const artifactMarked = new Marked({
|
||||
renderer: {
|
||||
code({ text, lang }) {
|
||||
// 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).
|
||||
// Will be overridden per-call
|
||||
if (lang && hljs.getLanguage(lang)) {
|
||||
const highlighted = hljs.highlight(text, { language: lang }).value
|
||||
return `<pre><code class="hljs language-${lang}">${highlighted}</code></pre>`
|
||||
}
|
||||
const highlighted = hljs.highlightAuto(text).value
|
||||
return `<pre><code class="hljs">${highlighted}</code></pre>`
|
||||
return `<pre><code>${text.replace(/</g, '<').replace(/>/g, '>')}</code></pre>`
|
||||
},
|
||||
},
|
||||
breaks: true,
|
||||
|
||||
53
core/http/react-ui/src/utils/hljs.js
vendored
53
core/http/react-ui/src/utils/hljs.js
vendored
@@ -1,53 +0,0 @@
|
||||
// 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
|
||||
2
core/http/react-ui/src/utils/markdown.js
vendored
2
core/http/react-ui/src/utils/markdown.js
vendored
@@ -1,6 +1,6 @@
|
||||
import { marked } from 'marked'
|
||||
import DOMPurify from 'dompurify'
|
||||
import hljs from './hljs'
|
||||
import hljs from 'highlight.js'
|
||||
|
||||
marked.setOptions({
|
||||
highlight(code, lang) {
|
||||
|
||||
@@ -9,11 +9,6 @@ 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: [
|
||||
@@ -55,20 +50,5 @@ 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,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -332,41 +332,5 @@ 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"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -111,11 +111,7 @@ func (e *Evaluator) TemplateMessages(input schema.OpenAIRequest, messages []sche
|
||||
}
|
||||
}
|
||||
r := config.Roles[role]
|
||||
// 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 != ""
|
||||
contentExists := i.Content != nil && i.StringContent != ""
|
||||
|
||||
fcall := i.FunctionCall
|
||||
if len(i.ToolCalls) > 0 {
|
||||
|
||||
@@ -218,41 +218,4 @@ 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,7 +3,35 @@
|
||||
url: "github:mudler/LocalAI/gallery/virtual.yaml@master"
|
||||
urls:
|
||||
- https://huggingface.co/Jackrong/Qwopus3.5-9B-Coder-MTP-GGUF
|
||||
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"
|
||||
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**:
|
||||
|
||||
...
|
||||
license: "apache-2.0"
|
||||
tags:
|
||||
- llm
|
||||
@@ -39,7 +67,26 @@
|
||||
url: "github:mudler/LocalAI/gallery/virtual.yaml@master"
|
||||
urls:
|
||||
- https://huggingface.co/Jackrong/Qwopus3.6-27B-v2-MTP-GGUF
|
||||
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"
|
||||
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.
|
||||
|
||||
...
|
||||
license: "apache-2.0"
|
||||
tags:
|
||||
- llm
|
||||
@@ -6161,7 +6208,6 @@
|
||||
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:
|
||||
@@ -6187,7 +6233,6 @@
|
||||
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:
|
||||
|
||||
@@ -4,16 +4,13 @@
|
||||
#
|
||||
# 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 0.8). The React UI e2e suite
|
||||
# UI_COVERAGE_TOLERANCE percentage points (default 1.0). 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 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.
|
||||
# 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.
|
||||
# (The Go gate carries a smaller tolerance for the same reason — its e2e slice.)
|
||||
#
|
||||
# When coverage rises meaningfully, regenerate and commit the baseline with:
|
||||
@@ -22,7 +19,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:-0.8}"
|
||||
tolerance="${UI_COVERAGE_TOLERANCE:-1.0}"
|
||||
|
||||
if [ ! -f "$summary" ]; then
|
||||
echo "ui-coverage-check: coverage summary not found: $summary" >&2
|
||||
|
||||
Reference in New Issue
Block a user