Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot]
7e2b69e895 chore(deps): bump torch in /backend/python/vllm
Bumps torch from 2.9.1+cpu to 2.12.1+xpu.

---
updated-dependencies:
- dependency-name: torch
  dependency-version: 2.12.1+xpu
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-22 18:33:32 +00:00
48 changed files with 195 additions and 1260 deletions

View File

@@ -44,7 +44,7 @@ jobs:
has-merges-singlearch: ${{ steps.set-matrix.outputs['has-merges-singlearch'] }}
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Setup Bun
uses: oven-sh/setup-bun@v2

View File

@@ -101,7 +101,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
submodules: true

View File

@@ -57,7 +57,7 @@ jobs:
HOMEBREW_NO_ANALYTICS: '1'
steps:
- name: Clone
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
submodules: true

View File

@@ -49,7 +49,7 @@ jobs:
# Sparse checkout: the merge job needs `.github/scripts/` (for the
# keepalive cleanup script) but none of the source tree.
- name: Checkout (.github/scripts only)
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
sparse-checkout: |
.github/scripts

View File

@@ -23,7 +23,7 @@ jobs:
has-merges-singlearch: ${{ steps.set-matrix.outputs['has-merges-singlearch'] }}
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Setup Bun
uses: oven-sh/setup-bun@v2

View File

@@ -127,7 +127,7 @@ jobs:
# the original l4t matrix entry which set skip-drivers: 'true'.
skip-drivers: 'true'
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
with:
submodules: false
- name: Free disk space

View File

@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up Go
@@ -25,7 +25,7 @@ jobs:
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up Go
@@ -47,7 +47,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Configure apt mirror on runner

View File

@@ -14,7 +14,7 @@ jobs:
bump:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- uses: actions/setup-go@v5
with:

View File

@@ -92,7 +92,7 @@ jobs:
file: "backend/go/vibevoice-cpp/Makefile"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Bump dependencies 🔧
id: bump
run: |
@@ -128,7 +128,7 @@ jobs:
if: github.repository == 'mudler/LocalAI'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Bump vLLM cu130 wheel pin 🔧
id: bump
run: |

View File

@@ -13,7 +13,7 @@ jobs:
- repository: "mudler/LocalAI"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Bump dependencies 🔧
run: |
bash .github/bump_docs.sh ${{ matrix.repository }}

View File

@@ -8,7 +8,7 @@ jobs:
if: github.repository == 'mudler/LocalAI'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Configure apt mirror on runner
uses: ./.github/actions/configure-apt-mirror
- name: Install dependencies

View File

@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Clone
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
submodules: true
- uses: actions/setup-go@v5

View File

@@ -31,7 +31,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -44,7 +44,7 @@ jobs:
uses: docker/setup-buildx-action@master
- name: Checkout
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Cache Intel images
uses: docker/build-push-action@v7

View File

@@ -28,7 +28,7 @@ jobs:
HUGO_VERSION: "0.146.3"
steps:
- name: Checkout
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
fetch-depth: 0 # needed for enableGitInfo
submodules: true

View File

@@ -80,7 +80,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Configure apt mirror on runner
id: apt_mirror

View File

@@ -36,7 +36,7 @@ jobs:
# Sparse checkout: needed for .github/scripts/ (the keepalive cleanup
# script). Skips the rest of the source tree.
- name: Checkout (.github/scripts only)
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
sparse-checkout: |
.github/scripts

View File

@@ -20,7 +20,7 @@ jobs:
golangci-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
with:
# Full history so golangci-lint's new-from-merge-base can reach
# origin/master and compute the diff against it.

View File

@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up Go
@@ -28,7 +28,7 @@ jobs:
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up Go
@@ -46,7 +46,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Configure apt mirror on runner

View File

@@ -14,7 +14,7 @@ jobs:
GO111MODULE: on
steps:
- name: Checkout Source
uses: actions/checkout@v7
uses: actions/checkout@v6
if: ${{ github.actor != 'dependabot[bot]' }}
- name: Run Gosec Security Scanner
if: ${{ github.actor != 'dependabot[bot]' }}

View File

@@ -50,7 +50,7 @@ jobs:
parakeet-cpp: ${{ steps.detect.outputs.parakeet-cpp }}
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Install dependencies
@@ -67,7 +67,7 @@ jobs:
# runs-on: ubuntu-latest
# steps:
# - name: Clone
# uses: actions/checkout@v7
# uses: actions/checkout@v6
# with:
# submodules: true
# - name: Dependencies
@@ -90,7 +90,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Clone
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
submodules: true
- name: Dependencies
@@ -113,7 +113,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Clone
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
submodules: true
- name: Dependencies
@@ -137,7 +137,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Clone
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
submodules: true
- name: Dependencies
@@ -158,7 +158,7 @@ jobs:
# runs-on: ubuntu-latest
# steps:
# - name: Clone
# uses: actions/checkout@v7
# uses: actions/checkout@v6
# with:
# submodules: true
# - name: Dependencies
@@ -178,7 +178,7 @@ jobs:
# runs-on: ubuntu-latest
# steps:
# - name: Clone
# uses: actions/checkout@v7
# uses: actions/checkout@v6
# with:
# submodules: true
# - name: Dependencies
@@ -240,7 +240,7 @@ jobs:
# sudo rm -rf "$AGENT_TOOLSDIRECTORY" || true
# df -h
# - name: Clone
# uses: actions/checkout@v7
# uses: actions/checkout@v6
# with:
# submodules: true
# - name: Dependencies
@@ -265,7 +265,7 @@ jobs:
# runs-on: ubuntu-latest
# steps:
# - name: Clone
# uses: actions/checkout@v7
# uses: actions/checkout@v6
# with:
# submodules: true
# - name: Dependencies
@@ -288,7 +288,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Clone
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
submodules: true
- name: Dependencies
@@ -309,7 +309,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Clone
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
submodules: true
- name: Dependencies
@@ -330,7 +330,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Clone
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
submodules: true
- name: Dependencies
@@ -351,7 +351,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Clone
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
submodules: true
- name: Dependencies
@@ -373,7 +373,7 @@ jobs:
# timeout-minutes: 45
# steps:
# - name: Clone
# uses: actions/checkout@v7
# uses: actions/checkout@v6
# with:
# submodules: true
# - name: Dependencies
@@ -394,7 +394,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Clone
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
submodules: true
- name: Dependencies
@@ -415,7 +415,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Clone
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
submodules: true
- name: Dependencies
@@ -436,7 +436,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Clone
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
submodules: true
- name: Dependencies
@@ -462,7 +462,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Clone
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
submodules: true
- name: Dependencies
@@ -484,7 +484,7 @@ jobs:
timeout-minutes: 30
steps:
- name: Clone
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
submodules: true
- name: Dependencies
@@ -513,7 +513,7 @@ jobs:
timeout-minutes: 90
steps:
- name: Clone
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
submodules: true
- name: Setup Go
@@ -530,7 +530,7 @@ jobs:
timeout-minutes: 90
steps:
- name: Clone
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
submodules: true
- name: Setup Go
@@ -552,7 +552,7 @@ jobs:
timeout-minutes: 20
steps:
- name: Clone
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
submodules: true
- name: Setup Go
@@ -579,7 +579,7 @@ jobs:
timeout-minutes: 90
steps:
- name: Clone
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
submodules: true
- name: Setup Go
@@ -604,7 +604,7 @@ jobs:
timeout-minutes: 90
steps:
- name: Clone
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
submodules: true
- name: Setup Go
@@ -625,7 +625,7 @@ jobs:
timeout-minutes: 90
steps:
- name: Clone
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
submodules: true
- name: Setup Go
@@ -645,7 +645,7 @@ jobs:
timeout-minutes: 90
steps:
- name: Clone
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
submodules: true
- name: Setup Go
@@ -664,7 +664,7 @@ jobs:
timeout-minutes: 90
steps:
- name: Clone
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
submodules: true
- name: Setup Go
@@ -681,7 +681,7 @@ jobs:
timeout-minutes: 90
steps:
- name: Clone
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
submodules: true
- name: Setup Go
@@ -698,7 +698,7 @@ jobs:
timeout-minutes: 90
steps:
- name: Clone
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
submodules: true
- name: Setup Go
@@ -741,7 +741,7 @@ jobs:
# timeout-minutes: 90
# steps:
# - name: Clone
# uses: actions/checkout@v7
# uses: actions/checkout@v6
# with:
# submodules: true
# - name: Dependencies
@@ -783,7 +783,7 @@ jobs:
# timeout-minutes: 90
# steps:
# - name: Clone
# uses: actions/checkout@v7
# uses: actions/checkout@v6
# with:
# submodules: true
# - name: Dependencies
@@ -808,7 +808,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Clone
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
submodules: true
- name: Dependencies
@@ -840,7 +840,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Clone
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
submodules: true
- name: Dependencies
@@ -876,7 +876,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Clone
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
submodules: true
- name: Dependencies
@@ -915,7 +915,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Clone
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
submodules: true
- name: Dependencies
@@ -952,7 +952,7 @@ jobs:
timeout-minutes: 90
steps:
- name: Clone
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
submodules: true
- name: Dependencies
@@ -987,7 +987,7 @@ jobs:
timeout-minutes: 90
steps:
- name: Clone
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
submodules: true
- name: Setup Go
@@ -1013,7 +1013,7 @@ jobs:
timeout-minutes: 150
steps:
- name: Clone
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
submodules: true
- name: Dependencies
@@ -1042,7 +1042,7 @@ jobs:
timeout-minutes: 60
steps:
- name: Clone
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
submodules: true
- name: Setup Go
@@ -1058,7 +1058,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Clone
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
submodules: true
- name: Dependencies
@@ -1091,7 +1091,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Clone
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
submodules: true
- name: Dependencies
@@ -1114,7 +1114,7 @@ jobs:
timeout-minutes: 90
steps:
- name: Clone
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
submodules: true
- name: Dependencies
@@ -1140,7 +1140,7 @@ jobs:
timeout-minutes: 90
steps:
- name: Clone
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
submodules: true
- name: Dependencies

View File

@@ -21,7 +21,7 @@ jobs:
go-version: ['1.26.x']
steps:
- name: Clone
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
submodules: true
- name: Free disk space
@@ -84,7 +84,7 @@ jobs:
go-version: ['1.26.x']
steps:
- name: Clone
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
submodules: true
- name: Setup Go ${{ matrix.go-version }}

View File

@@ -62,7 +62,7 @@ jobs:
sudo rm -rfv build || true
df -h
- name: Clone
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
submodules: true
- name: Dependencies

View File

@@ -21,7 +21,7 @@ jobs:
go-version: ['1.25.x']
steps:
- name: Clone
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
submodules: true
- name: Configure apt mirror on runner

View File

@@ -57,7 +57,7 @@ jobs:
go-version: ['1.25.x']
steps:
- name: Clone
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
submodules: true
- name: Free disk space

View File

@@ -23,7 +23,7 @@ jobs:
go-version: ['1.26.x']
steps:
- name: Clone
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
submodules: true
- name: Configure apt mirror on runner

View File

@@ -10,7 +10,7 @@ jobs:
fail-fast: false
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Configure apt mirror on runner
uses: ./.github/actions/configure-apt-mirror
- uses: actions/setup-go@v5

View File

@@ -1,5 +1,5 @@
LLAMA_VERSION?=73618f27a801c0b8614ceaf3547d3c2a99baae14
LLAMA_VERSION?=7c082bc417bbe53210a83df4ba5b49e18ce6193c
LLAMA_REPO?=https://github.com/ggerganov/llama.cpp
CMAKE_ARGS?=

View File

@@ -8,7 +8,7 @@ JOBS?=$(shell nproc --ignore=1)
# CrispASR version (release tag)
CRISPASR_REPO?=https://github.com/CrispStrobe/CrispASR
CRISPASR_VERSION?=63b57289255267edf66e43e33bc3911e04a2e92d
CRISPASR_VERSION?=7a8cb80907341c0204bd0488c1244764f4163883
SO_TARGET?=libgocrispasr.so
CMAKE_ARGS+=-DBUILD_SHARED_LIBS=OFF

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?=f440ad9c29dd8bc34e5d1f4b863832b96d6ea05f
STABLEDIFFUSION_GGML_VERSION?=b12098f5d09fc83da36e65c784f7bdb16a5a5ebf
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?=bae6bc02b1940bbfb87b6a0299c565e563b916d1
WHISPER_CPP_VERSION?=5ed76e9a079962f1c85cfce44edd325c27ef1f97
SO_TARGET?=libgowhisper.so
CMAKE_ARGS+=-DBUILD_SHARED_LIBS=OFF

View File

@@ -1,6 +1,6 @@
--extra-index-url https://download.pytorch.org/whl/cpu
accelerate
torch==2.9.1+cpu
torch==2.12.1+xpu
torchvision
torchaudio
transformers

View File

@@ -537,36 +537,6 @@ func DefaultRegistry() map[string]FieldMetaOverride {
Component: "number",
Order: 79,
},
"pipeline.compaction.enabled": {
Section: "pipeline",
Label: "Compaction Enabled",
Description: "Fold conversation items that age out of the live window (Max History Items) into a rolling summary instead of dropping them, so long realtime sessions stay cheap without losing earlier context. Off by default.",
Component: "toggle",
Order: 80,
},
"pipeline.compaction.trigger_items": {
Section: "pipeline",
Label: "Compaction Trigger Items",
Description: "High-water mark: once the live conversation exceeds this many items, the overflow above Max History Items is summarized and evicted. Must be greater than Max History Items; defaults to twice it. The gap controls how often summarization runs.",
Component: "number",
Order: 81,
},
"pipeline.compaction.summary_model": {
Section: "pipeline",
Label: "Compaction Summary Model",
Description: "Optional smaller/cheaper model used to produce the rolling summary. Empty reuses the pipeline's own LLM. On CPU, a tiny model here keeps compaction from competing with the conversation LLM.",
Component: "input",
Advanced: true,
Order: 82,
},
"pipeline.compaction.max_summary_tokens": {
Section: "pipeline",
Label: "Compaction Max Summary Tokens",
Description: "Advisory cap on the rolling summary length (fed to the summarizer prompt). Defaults to 512.",
Component: "number",
Advanced: true,
Order: 83,
},
// --- Functions ---
"function.grammar.parallel_calls": {

View File

@@ -641,32 +641,11 @@ type Pipeline struct {
// context fills.
MaxHistoryItems *int `yaml:"max_history_items,omitempty" json:"max_history_items,omitempty"`
// Compaction folds conversation items that age out of the live window
// (max_history_items) into a rolling summary instead of dropping them, so
// long realtime sessions stay cheap without losing earlier context. Nil
// (block absent) means disabled, preserving existing behavior.
Compaction *PipelineCompaction `yaml:"compaction,omitempty" json:"compaction,omitempty"`
// VoiceRecognition gates the pipeline behind speaker verification. Nil
// (block absent) means no gate, preserving existing behavior.
VoiceRecognition *PipelineVoiceRecognition `yaml:"voice_recognition,omitempty" json:"voice_recognition,omitempty"`
}
// PipelineCompaction configures summarize-then-drop for a realtime pipeline.
type PipelineCompaction struct {
// Enabled turns summarize-then-drop on. Default false.
Enabled bool `yaml:"enabled,omitempty" json:"enabled,omitempty"`
// TriggerItems is the high-water mark: once live items exceed it, overflow
// above max_history_items is summarized and evicted. Must exceed
// max_history_items; clamped up if not. Default: 2x max_history_items.
TriggerItems int `yaml:"trigger_items,omitempty" json:"trigger_items,omitempty"`
// SummaryModel optionally names a smaller/cheaper model for the summary
// call. Empty uses the pipeline's own LLM.
SummaryModel string `yaml:"summary_model,omitempty" json:"summary_model,omitempty"`
// MaxSummaryTokens advises the summary length (fed to the prompt). Default 512.
MaxSummaryTokens int `yaml:"max_summary_tokens,omitempty" json:"max_summary_tokens,omitempty"`
}
// ApplyReasoningEffort resolves the effective reasoning effort — a per-request
// value (requestEffort) overrides the config's own ReasoningEffort default —
// stores it on the config so gRPCPredictOpts forwards it to the backend as the

View File

@@ -5,7 +5,6 @@ import (
"errors"
"os"
"path/filepath"
"reflect"
)
// runtimeSettingsFile is the on-disk filename inside DynamicConfigsDir.
@@ -34,35 +33,6 @@ func (o *ApplicationConfig) ReadPersistedSettings() (RuntimeSettings, error) {
return settings, nil
}
// MergeNonNil overlays every set (non-nil) field of overlay onto the
// receiver, leaving the receiver's value untouched wherever overlay left a
// field unset. Every RuntimeSettings field is a pointer precisely so "set"
// can be told apart from "absent" (see the type doc), which makes this a
// faithful partial update: a caller that submits only the field it owns
// changes exactly that field and never clobbers unrelated settings.
//
// This is the read-modify-write contract the persistence helpers exist for.
// UpdateSettingsEndpoint reads the on-disk settings, merges the request body
// on top, and writes the result — so a focused admin page that POSTs only its
// own field (the Middleware page sends only mitm_listen; the detector table
// only pii_default_detectors) no longer nulls every other setting.
//
// Reflection keeps the merge total over the struct: a field added to
// RuntimeSettings later is merged automatically, so the persistence path can
// never silently drop a new setting the way a hand-maintained field list
// would. Non-pointer fields (none today) are skipped — they cannot express
// "absent", so the receiver wins.
func (s *RuntimeSettings) MergeNonNil(overlay RuntimeSettings) {
dst := reflect.ValueOf(s).Elem()
src := reflect.ValueOf(overlay)
for i := 0; i < src.NumField(); i++ {
f := src.Field(i)
if f.Kind() == reflect.Pointer && !f.IsNil() {
dst.Field(i).Set(f)
}
}
}
// WritePersistedSettings serialises the given RuntimeSettings to
// runtime_settings.json with restricted permissions (it may carry API
// keys and P2P tokens).

View File

@@ -12,7 +12,6 @@ import (
)
func strPtr(s string) *string { return &s }
func boolPtr(b bool) *bool { return &b }
var _ = Describe("RuntimeSettings persistence helpers", func() {
var (
@@ -52,47 +51,6 @@ var _ = Describe("RuntimeSettings persistence helpers", func() {
})
})
// MergeNonNil is the partial-update primitive UpdateSettingsEndpoint
// relies on: a focused admin page POSTs only the field it owns, and the
// handler reads the on-disk settings and overlays the request on top.
// Without it, the body would be written verbatim and every field the
// caller omitted would be nulled (the reported regression: changing
// mitm_listen wiped the galleries, api keys, watchdog config, etc.).
Describe("MergeNonNil partial update", func() {
It("overlays set fields and preserves unset ones", func() {
base := config.RuntimeSettings{
MITMListen: strPtr(":9000"),
Galleries: &[]config.Gallery{{Name: "g1", URL: "http://example/g1"}},
WatchdogIdleEnabled: boolPtr(true),
ApiKeys: &[]string{"persisted-key"},
PIIDefaultDetectors: &[]string{"det-a"},
}
// Simulate the Middleware proxy tab: only mitm_listen is sent.
overlay := config.RuntimeSettings{MITMListen: strPtr(":8443")}
base.MergeNonNil(overlay)
Expect(base.MITMListen).ToNot(BeNil())
Expect(*base.MITMListen).To(Equal(":8443"), "set field should be overlaid")
// Everything the overlay left unset must survive untouched.
Expect(base.Galleries).ToNot(BeNil(), "galleries were clobbered")
Expect(*base.Galleries).To(HaveLen(1))
Expect(base.WatchdogIdleEnabled).ToNot(BeNil())
Expect(*base.WatchdogIdleEnabled).To(BeTrue())
Expect(base.ApiKeys).ToNot(BeNil(), "api_keys were clobbered")
Expect(*base.ApiKeys).To(Equal([]string{"persisted-key"}))
Expect(base.PIIDefaultDetectors).ToNot(BeNil(), "pii_default_detectors were clobbered")
Expect(*base.PIIDefaultDetectors).To(Equal([]string{"det-a"}))
})
It("lets an explicit empty slice clear a field", func() {
base := config.RuntimeSettings{PIIDefaultDetectors: &[]string{"det-a"}}
base.MergeNonNil(config.RuntimeSettings{PIIDefaultDetectors: &[]string{}})
Expect(base.PIIDefaultDetectors).ToNot(BeNil())
Expect(*base.PIIDefaultDetectors).To(BeEmpty(), "an explicit empty slice should clear, not preserve")
})
})
// MITM round trip pins the contract that loadRuntimeSettingsFromFile
// MITM listener address must survive a write/read round trip so the
// next process restart can bring the listener back up. (Intercept

View File

@@ -4,6 +4,8 @@ import (
"encoding/json"
"io"
"net/http"
"os"
"path/filepath"
"time"
"github.com/labstack/echo/v4"
@@ -108,18 +110,6 @@ func UpdateSettingsEndpoint(app *application.Application) echo.HandlerFunc {
})
}
// Read whatever is already persisted: it is both the source of truth
// for branding asset filenames (below) and the base we merge this
// request onto before writing. A read failure must not let a Save
// silently discard the existing settings — surface it instead.
persisted, err := appConfig.ReadPersistedSettings()
if err != nil {
return c.JSON(http.StatusInternalServerError, schema.SettingsResponse{
Success: false,
Error: "Failed to read existing settings: " + err.Error(),
})
}
// Branding asset filenames are owned exclusively by
// /api/branding/asset/{kind} (upload/delete). The Settings page also
// round-trips them via GET /api/settings, but its local state is stale
@@ -128,9 +118,11 @@ func UpdateSettingsEndpoint(app *application.Application) echo.HandlerFunc {
// at page open. Replace whatever the body sent for these three fields
// with the values currently on disk so /api/settings can never
// regress them.
settings.LogoFile = persisted.LogoFile
settings.LogoHorizontalFile = persisted.LogoHorizontalFile
settings.FaviconFile = persisted.FaviconFile
if existing, err := appConfig.ReadPersistedSettings(); err == nil {
settings.LogoFile = existing.LogoFile
settings.LogoHorizontalFile = existing.LogoHorizontalFile
settings.FaviconFile = existing.FaviconFile
}
// The UI reads ApiKeys from GET /api/settings, which already returns the
// merged env+runtime list. When the user clicks Save, the same merged
@@ -153,17 +145,16 @@ func UpdateSettingsEndpoint(app *application.Application) echo.HandlerFunc {
settings.ApiKeys = &runtimeOnly
}
// Persist as a partial update: overlay only the fields this request set
// onto the settings already on disk. Focused admin pages POST just the
// keys they own (the Middleware proxy tab sends only mitm_listen; the
// detector table only pii_default_detectors), so writing the request
// body verbatim would null every unrelated setting (the no-omitempty
// api_keys / pii_default_detectors fields even round-trip as JSON
// null). The full Settings page still round-trips every field, so its
// Save is unchanged.
toPersist := persisted
toPersist.MergeNonNil(settings)
if err := appConfig.WritePersistedSettings(toPersist); err != nil {
settingsFile := filepath.Join(appConfig.DynamicConfigsDir, "runtime_settings.json")
settingsJSON, err := json.MarshalIndent(settings, "", " ")
if err != nil {
return c.JSON(http.StatusInternalServerError, schema.SettingsResponse{
Success: false,
Error: "Failed to marshal settings: " + err.Error(),
})
}
if err := os.WriteFile(settingsFile, settingsJSON, 0600); err != nil {
return c.JSON(http.StatusInternalServerError, schema.SettingsResponse{
Success: false,
Error: "Failed to write settings file: " + err.Error(),

View File

@@ -52,10 +52,6 @@ var _ = Describe("Settings endpoints", func() {
// Settings are persisted here; set after construction since there's no
// dedicated AppOption for it.
app.ApplicationConfig().DynamicConfigsDir = tmp
// Contain the MITM CA inside tmp too. The partial-save spec flips
// mitm_listen, which starts the listener and writes a CA; without this
// it defaults to ./mitm-ca and litters the package source tree.
app.ApplicationConfig().MITMCADir = filepath.Join(tmp, "mitm-ca")
e = echo.New()
e.GET("/api/settings", GetSettingsEndpoint(app))
@@ -113,39 +109,6 @@ var _ = Describe("Settings endpoints", func() {
Expect(err).ToNot(HaveOccurred())
})
// Regression: a focused admin page (the Middleware proxy tab) POSTs only
// the one field it owns — mitm_listen. The old handler wrote the request
// body verbatim, so every other persisted setting was dropped (and
// api_keys / pii_default_detectors, which lack omitempty, were written as
// null). A partial POST must now merge onto what is already on disk.
It("preserves unrelated persisted settings when a partial POST sets only mitm_listen", func() {
// First save establishes a fuller settings file (as the full Settings
// page would): galleries, an API key, and the MITM listener. The
// listener restart binds a real socket, so use 127.0.0.1:0 for an
// ephemeral free port rather than a fixed one that may be in use.
rec := post(`{"mitm_listen":"127.0.0.1:0","galleries":[{"name":"g1","url":"http://example/g1"}],"api_keys":["k1"],"pii_default_detectors":["det-a"]}`)
Expect(rec.Code).To(Equal(http.StatusOK), rec.Body.String())
// The Middleware proxy tab then changes only the listen address — the
// exact partial body that nulled everything else before the fix.
rec = post(`{"mitm_listen":"127.0.0.1:0"}`)
Expect(rec.Code).To(Equal(http.StatusOK), rec.Body.String())
raw, err := os.ReadFile(filepath.Join(tmp, "runtime_settings.json"))
Expect(err).ToNot(HaveOccurred())
var ondisk config.RuntimeSettings
Expect(json.Unmarshal(raw, &ondisk)).To(Succeed())
Expect(ondisk.MITMListen).ToNot(BeNil())
Expect(*ondisk.MITMListen).To(Equal("127.0.0.1:0"), "the changed field should be saved")
Expect(ondisk.Galleries).ToNot(BeNil(), "galleries were clobbered by the partial save")
Expect(*ondisk.Galleries).To(HaveLen(1))
Expect(ondisk.ApiKeys).ToNot(BeNil(), "api_keys were nulled by the partial save")
Expect(*ondisk.ApiKeys).To(Equal([]string{"k1"}))
Expect(ondisk.PIIDefaultDetectors).ToNot(BeNil(), "pii_default_detectors were nulled by the partial save")
Expect(*ondisk.PIIDefaultDetectors).To(Equal([]string{"det-a"}))
})
// Residual #9125: enabling the watchdog from a cold (off) state via the
// React master toggle must start the live watchdog immediately, without a
// restart. The toggle posts watchdog_idle_enabled/busy_enabled=true while

View File

@@ -12,7 +12,6 @@ import (
"os"
"strconv"
"sync"
"sync/atomic"
"time"
"net/http"
@@ -135,18 +134,6 @@ type Session struct {
// pairs are kept together so we never feed an orphaned tool result.
MaxHistoryItems int
// Compaction settings resolved from pipeline.compaction (see resolveCompaction).
CompactionEnabled bool
CompactionTrigger int
SummaryModel string
MaxSummaryTokens int
// summarizerFactory lazily builds the model used for compaction summaries
// when summary_model is configured; nil means reuse the pipeline LLM.
summarizerFactory func() (Model, error)
summarizerOnce sync.Once
summarizerCached Model
// AssistantExecutor is non-nil when the session opted into the in-process
// LocalAI Assistant tool surface. Tool calls whose name matches this
// executor's catalog are run inproc and their output is fed back to the
@@ -254,12 +241,6 @@ type Conversation struct {
ID string
Items []*types.MessageItemUnion
Lock sync.Mutex
// Memory is the rolling summary of items already evicted by compaction. It
// is kept out of Items (so trimRealtimeItems never drops it) and rendered
// as a system message right after the session instructions.
Memory string
// compacting ensures at most one background compaction runs per conversation.
compacting atomic.Bool
}
func (c *Conversation) ToServer() types.Conversation {
@@ -559,12 +540,13 @@ func runRealtimeSession(application *application.Application, t Transport, model
SoundDetectionWindowMs: cfg.Pipeline.SoundDetectionWindowMs,
SoundDetectionHopMs: cfg.Pipeline.SoundDetectionHopMs,
}
session.CompactionEnabled, session.CompactionTrigger, session.MaxSummaryTokens, session.SummaryModel = resolveCompaction(cfg, session.MaxHistoryItems)
// Create a default conversation
conversationID := generateConversationID()
conversation := &Conversation{
ID: conversationID,
ID: conversationID,
// TODO: We need to truncate the conversation items when a new item is added and we have run out of space. There are multiple places where items
// can be added so we could use a datastructure here that enforces truncation upon addition
Items: []*types.MessageItemUnion{},
}
session.Conversations[conversationID] = conversation
@@ -595,18 +577,6 @@ func runRealtimeSession(application *application.Application, t Transport, model
}
session.ModelInterface = m
if session.SummaryModel != "" {
summaryModelName := session.SummaryModel
sid := sessionID
session.summarizerFactory = func() (Model, error) {
summaryCfg, lerr := application.ModelConfigLoader().LoadModelConfigFileByNameDefaultOptions(summaryModelName, application.ApplicationConfig())
if lerr != nil {
return nil, fmt.Errorf("load summary model config %q: %w", summaryModelName, lerr)
}
return newModel(&summaryCfg.Pipeline, application.ModelConfigLoader(), application.ModelLoader(), application.ApplicationConfig(), evaluator, buildRealtimeRoutingContext(application, sid))
}
}
if cfg.Pipeline.VoiceGateEnabled() {
gate, gerr := newVoiceGate(
*cfg.Pipeline.VoiceRecognition,
@@ -837,15 +807,6 @@ func runRealtimeSession(application *application.Application, t Transport, model
commitUtterance(respCtx, allAudio, session, conversation, t)
}()
case types.InputAudioBufferClearEvent:
xlog.Debug("recv", "message", string(msg))
// Discard a partially-captured utterance so the client can restart
// input cleanly without the stale buffer leaking into the next commit.
clearInputAudio(session)
sendEvent(t, types.InputAudioBufferClearedEvent{
ServerEventBase: types.ServerEventBase{EventID: e.EventID},
})
case types.ConversationItemCreateEvent:
xlog.Debug("recv", "message", string(msg))
// Add the item to the conversation
@@ -880,39 +841,7 @@ func runRealtimeSession(application *application.Application, t Transport, model
})
case types.ConversationItemDeleteEvent:
xlog.Debug("recv", "message", string(msg))
if e.ItemID == "" {
sendError(t, "invalid_item_id", "Need item_id, but none specified", "", "event_TODO")
continue
}
conversation.Lock.Lock()
updated, ok := deleteItem(conversation.Items, e.ItemID)
conversation.Items = updated
conversation.Lock.Unlock()
if !ok {
sendError(t, "invalid_item_id", "Item to delete not found", "", "event_TODO")
continue
}
sendEvent(t, types.ConversationItemDeletedEvent{
ServerEventBase: types.ServerEventBase{EventID: e.EventID},
ItemID: e.ItemID,
})
case types.ConversationItemTruncateEvent:
xlog.Debug("recv", "message", string(msg))
conversation.Lock.Lock()
ok := truncateAssistantText(conversation.Items, e.ItemID, e.ContentIndex)
conversation.Lock.Unlock()
if !ok {
sendError(t, "invalid_item_id", "Item to truncate not found", "", "event_TODO")
continue
}
sendEvent(t, types.ConversationItemTruncatedEvent{
ServerEventBase: types.ServerEventBase{EventID: e.EventID},
ItemID: e.ItemID,
ContentIndex: e.ContentIndex,
AudioEndMs: e.AudioEndMs,
})
sendError(t, "not_implemented", "Deleting items not implemented", "", "event_TODO")
case types.ConversationItemRetrieveEvent:
xlog.Debug("recv", "message", string(msg))
@@ -925,7 +854,21 @@ func runRealtimeSession(application *application.Application, t Transport, model
conversation.Lock.Lock()
var retrievedItem types.MessageItemUnion
for _, item := range conversation.Items {
if itemID(item) == e.ItemID {
// We need to check ID in the union
var id string
if item.System != nil {
id = item.System.ID
} else if item.User != nil {
id = item.User.ID
} else if item.Assistant != nil {
id = item.Assistant.ID
} else if item.FunctionCall != nil {
id = item.FunctionCall.ID
} else if item.FunctionCallOutput != nil {
id = item.FunctionCallOutput.ID
}
if id == e.ItemID {
retrievedItem = *item
break
}
@@ -1723,9 +1666,6 @@ const maxAssistantToolTurns = 10
func triggerResponse(ctx context.Context, session *Session, conv *Conversation, t Transport, overrides *types.ResponseCreateParams) {
triggerResponseAtTurn(ctx, session, conv, t, overrides, 0)
// Fold aged-out turns into the rolling memory off the critical path; the
// next turn reaps the smaller buffer.
session.maybeCompact(conv)
}
func triggerResponseAtTurn(ctx context.Context, session *Session, conv *Conversation, t Transport, overrides *types.ResponseCreateParams, toolTurn int) {
@@ -1781,7 +1721,6 @@ func triggerResponseAtTurn(ctx context.Context, session *Session, conv *Conversa
var lastUserSpeaker *types.Speaker
personalize := session.voiceGate != nil && session.voiceGate.cfg.PersonalizeEnabled()
conv.Lock.Lock()
conversationHistory = withMemory(conversationHistory, conv.Memory)
items := trimRealtimeItems(conv.Items, session.MaxHistoryItems)
for _, item := range items {
if item.User != nil {

View File

@@ -1,326 +0,0 @@
package openai
import (
"context"
"fmt"
"strings"
"time"
"github.com/mudler/LocalAI/core/config"
"github.com/mudler/LocalAI/core/http/endpoints/openai/types"
"github.com/mudler/LocalAI/core/schema"
"github.com/mudler/LocalAI/pkg/reasoning"
"github.com/mudler/xlog"
)
const (
defaultMaxSummaryTokens = 512
memoryPrefix = "Summary of earlier conversation:\n"
// compactionTimeout bounds the summarizer call so a stuck model can't pin the
// compacting flag (and thus block all further compaction) forever.
compactionTimeout = 60 * time.Second
)
// withMemory inserts the rolling summary as a system message after the existing
// (instructions) history. No-op when memory is empty.
func withMemory(history schema.Messages, memory string) schema.Messages {
if memory == "" {
return history
}
content := memoryPrefix + memory
return append(history, schema.Message{
Role: string(types.MessageRoleSystem),
StringContent: content,
Content: content,
})
}
// renderItemsTranscript renders conversation items as a plain "role: text"
// transcript for summarization. Non-text items (bare tool calls) are labelled
// so the summarizer keeps track of actions taken.
func renderItemsTranscript(items []*types.MessageItemUnion) string {
var b strings.Builder
for _, item := range items {
switch {
case item.User != nil:
b.WriteString("user: ")
for _, c := range item.User.Content {
if c.Text != "" {
b.WriteString(c.Text)
}
if c.Transcript != "" {
b.WriteString(c.Transcript)
}
}
b.WriteString("\n")
case item.Assistant != nil:
b.WriteString("assistant: ")
// Realtime assistant *audio* turns store the spoken words in
// .Transcript (not .Text), so emit both or spoken turns are dropped.
for _, c := range item.Assistant.Content {
if c.Text != "" {
b.WriteString(c.Text)
}
if c.Transcript != "" {
b.WriteString(c.Transcript)
}
}
b.WriteString("\n")
case item.FunctionCall != nil:
b.WriteString(fmt.Sprintf("assistant called tool %s(%s)\n", item.FunctionCall.Name, item.FunctionCall.Arguments))
case item.FunctionCallOutput != nil:
b.WriteString(fmt.Sprintf("tool result: %s\n", item.FunctionCallOutput.Output))
}
}
return strings.TrimSpace(b.String())
}
// buildSummaryMessages builds the chat messages for the summarizer LLM: a system
// instruction plus prior memory and the new transcript to fold in. maxTokens is
// advisory (fed to the prompt; not hard-enforced in v1).
func buildSummaryMessages(priorMemory, transcript string, maxTokens int) schema.Messages {
system := fmt.Sprintf("You maintain a running memory of a live voice conversation. "+
"Merge the prior memory with the new exchanges into an updated memory. "+
"Keep names, decisions, facts, preferences, and open threads. Be concise "+
"(under ~%d tokens). Output only the updated memory, with no reasoning or tags.", maxTokens)
var user strings.Builder
if priorMemory != "" {
user.WriteString("Prior memory:\n")
user.WriteString(priorMemory)
user.WriteString("\n\n")
}
user.WriteString("New exchanges to fold in:\n")
user.WriteString(transcript)
return schema.Messages{
{Role: string(types.MessageRoleSystem), StringContent: system, Content: system},
{Role: string(types.MessageRoleUser), StringContent: user.String(), Content: user.String()},
}
}
// clearInputAudio resets the session's pending input audio buffer (the raw
// PCM and any buffered Opus frames). Used by the input_audio_buffer.clear
// realtime event so a client can discard a partially-captured utterance.
func clearInputAudio(s *Session) {
s.AudioBufferLock.Lock()
s.InputAudioBuffer = nil
s.AudioBufferLock.Unlock()
s.OpusFramesLock.Lock()
s.OpusFrames = nil
s.OpusFramesLock.Unlock()
}
// itemID extracts the id from any MessageItemUnion variant ("" if none).
func itemID(item *types.MessageItemUnion) string {
switch {
case item == nil:
return ""
case item.System != nil:
return item.System.ID
case item.User != nil:
return item.User.ID
case item.Assistant != nil:
return item.Assistant.ID
case item.FunctionCall != nil:
return item.FunctionCall.ID
case item.FunctionCallOutput != nil:
return item.FunctionCallOutput.ID
default:
return ""
}
}
// deleteItem removes the item with id from items, returning the new slice and
// whether it was found.
func deleteItem(items []*types.MessageItemUnion, id string) ([]*types.MessageItemUnion, bool) {
for i, item := range items {
if itemID(item) == id {
return append(items[:i:i], items[i+1:]...), true
}
}
return items, false
}
// truncateAssistantText clears the text of the assistant item's content part at
// contentIndex. Minimal truncate: used to discard an interrupted/barge-in
// response tail. Both .Text and .Transcript are cleared because realtime audio
// turns store the spoken words in .Transcript (clearing only .Text would no-op).
func truncateAssistantText(items []*types.MessageItemUnion, id string, contentIndex int) bool {
for _, item := range items {
if itemID(item) != id || item.Assistant == nil {
continue
}
if contentIndex >= 0 && contentIndex < len(item.Assistant.Content) {
item.Assistant.Content[contentIndex].Text = ""
item.Assistant.Content[contentIndex].Transcript = ""
}
return true
}
return false
}
// compactionCut returns the index splitting items into overflow (items[:cut],
// to be summarized+evicted) and the kept live tail (items[cut:]), keeping the
// last `keep` items. It mirrors trimRealtimeItems' pair-safety: the cut is
// pulled left so a function_call and its function_call_output are never split
// across the boundary (the whole pair lands in the kept tail). Returns 0 when
// there is nothing to cut.
func compactionCut(items []*types.MessageItemUnion, keep int) int {
// keep <= 0 means no live-window cap (the "unlimited history" sentinel, as
// in trimRealtimeItems): there is nothing to evict, so cut nothing. This
// also avoids indexing items[len(items)] in the pair-safety loop below.
if keep <= 0 {
return 0
}
cut := len(items) - keep
if cut <= 0 {
return 0
}
for cut > 0 && items[cut] != nil && items[cut].FunctionCallOutput != nil {
cut--
}
return cut
}
// resolveCompaction reads the pipeline.compaction block, applying defaults and
// the trigger>max_history invariant. maxHistory is the already-resolved live
// window size. Returns enabled=false (and zero values) when compaction is off.
func resolveCompaction(cfg *config.ModelConfig, maxHistory int) (enabled bool, trigger, maxSummaryTokens int, summaryModel string) {
if cfg == nil || cfg.Pipeline.Compaction == nil || !cfg.Pipeline.Compaction.Enabled {
return false, 0, 0, ""
}
c := cfg.Pipeline.Compaction
trigger = c.TriggerItems
if trigger <= 0 {
trigger = maxHistory * 2
}
if trigger <= maxHistory {
trigger = maxHistory + 1
}
maxSummaryTokens = c.MaxSummaryTokens
if maxSummaryTokens <= 0 {
maxSummaryTokens = defaultMaxSummaryTokens
}
return true, trigger, maxSummaryTokens, c.SummaryModel
}
// prefixMatches reports whether items begins with the same ids, in order, as
// snapshot — i.e. the overflow we summarized is still at the head (no concurrent
// client delete reshuffled it).
func prefixMatches(items, snapshot []*types.MessageItemUnion) bool {
if len(items) < len(snapshot) {
return false
}
for i := range snapshot {
if itemID(items[i]) != itemID(snapshot[i]) {
return false
}
}
return true
}
// compact folds overflow items into conv.Memory and evicts them. It never holds
// conv.Lock across the summarizer call: snapshot under lock, summarize unlocked,
// commit under lock (re-validating the head is unchanged). On any error it
// leaves the conversation untouched — items are never dropped without a summary.
func (s *Session) compact(conv *Conversation, model Model) {
if model == nil {
return
}
// Snapshot.
conv.Lock.Lock()
if len(conv.Items) <= s.CompactionTrigger {
conv.Lock.Unlock()
return
}
cut := compactionCut(conv.Items, s.MaxHistoryItems)
if cut <= 0 {
conv.Lock.Unlock()
return
}
overflow := append([]*types.MessageItemUnion(nil), conv.Items[:cut]...)
prior := conv.Memory
conv.Lock.Unlock()
// Summarize (unlocked).
msgs := buildSummaryMessages(prior, renderItemsTranscript(overflow), s.MaxSummaryTokens)
ctx, cancel := context.WithTimeout(context.Background(), compactionTimeout)
defer cancel()
predFunc, err := model.Predict(ctx, msgs, nil, nil, nil, nil, nil, nil, nil, nil, nil)
if err != nil {
xlog.Warn("realtime compaction: summarizer predict failed", "error", err)
return
}
pred, err := predFunc()
if err != nil {
xlog.Warn("realtime compaction: summarizer inference failed", "error", err)
return
}
// Strip any leaked reasoning/thinking spans using the same extractor the
// rest of the realtime path uses, rather than a bespoke regex.
rcfg := reasoning.Config{}
if mc := model.PredictConfig(); mc != nil {
rcfg = spokenReasoningConfig(mc.ReasoningConfig)
}
_, summary := reasoning.ExtractReasoningComplete(pred.Response, "", rcfg)
summary = strings.TrimSpace(summary)
if summary == "" {
xlog.Warn("realtime compaction: empty summary, skipping eviction")
return
}
// Commit.
conv.Lock.Lock()
defer conv.Lock.Unlock()
if !prefixMatches(conv.Items, overflow) {
xlog.Debug("realtime compaction: head changed during summary, skipping")
return
}
conv.Memory = summary
conv.Items = conv.Items[len(overflow):]
xlog.Debug("realtime compaction: evicted items into memory", "evicted", len(overflow), "remaining", len(conv.Items))
}
// summarizerModel resolves the model used to produce compaction summaries.
// Without a configured summary_model (or factory) it reuses the pipeline LLM.
func (s *Session) summarizerModel() Model {
if s.SummaryModel == "" || s.summarizerFactory == nil {
return s.ModelInterface
}
s.summarizerOnce.Do(func() {
m, err := s.summarizerFactory()
if err != nil {
xlog.Warn("realtime compaction: summary_model load failed, falling back to pipeline LLM", "model", s.SummaryModel, "error", err)
m = s.ModelInterface
}
s.summarizerCached = m
})
return s.summarizerCached
}
// maybeCompact schedules a background compaction when the live buffer has grown
// past the trigger and none is already running. Returns immediately.
func (s *Session) maybeCompact(conv *Conversation) {
if !s.CompactionEnabled {
return
}
conv.Lock.Lock()
over := len(conv.Items) > s.CompactionTrigger
conv.Lock.Unlock()
if !over {
return
}
if !conv.compacting.CompareAndSwap(false, true) {
return
}
go func() {
defer conv.compacting.Store(false)
// Resolve (and, for a configured summary_model, lazily load) the
// summarizer only when a compaction actually runs, off the response
// path — so the model load never blocks a user turn.
model := s.summarizerModel()
if model == nil {
return
}
s.compact(conv, model)
}()
}

View File

@@ -1,308 +0,0 @@
package openai
import (
"errors"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/mudler/LocalAI/core/backend"
"github.com/mudler/LocalAI/core/config"
"github.com/mudler/LocalAI/core/http/endpoints/openai/types"
"github.com/mudler/LocalAI/core/schema"
)
var _ = Describe("resolveCompaction", func() {
It("disables when the block is absent", func() {
enabled, _, _, _ := resolveCompaction(&config.ModelConfig{}, 6)
Expect(enabled).To(BeFalse())
})
It("defaults trigger to 2x max history and tokens to 512", func() {
cfg := &config.ModelConfig{Pipeline: config.Pipeline{Compaction: &config.PipelineCompaction{Enabled: true}}}
enabled, trigger, maxTok, _ := resolveCompaction(cfg, 6)
Expect(enabled).To(BeTrue())
Expect(trigger).To(Equal(12))
Expect(maxTok).To(Equal(512))
})
It("clamps trigger to max history + 1 when misconfigured", func() {
cfg := &config.ModelConfig{Pipeline: config.Pipeline{Compaction: &config.PipelineCompaction{Enabled: true, TriggerItems: 4}}}
_, trigger, _, _ := resolveCompaction(cfg, 6)
Expect(trigger).To(Equal(7))
})
It("honors explicit values", func() {
cfg := &config.ModelConfig{Pipeline: config.Pipeline{Compaction: &config.PipelineCompaction{
Enabled: true, TriggerItems: 20, MaxSummaryTokens: 256, SummaryModel: "tiny"}}}
enabled, trigger, maxTok, model := resolveCompaction(cfg, 6)
Expect(enabled).To(BeTrue())
Expect(trigger).To(Equal(20))
Expect(maxTok).To(Equal(256))
Expect(model).To(Equal("tiny"))
})
})
var _ = Describe("deleteItem", func() {
mk := func(ids ...string) []*types.MessageItemUnion {
out := make([]*types.MessageItemUnion, len(ids))
for i, id := range ids {
out[i] = &types.MessageItemUnion{User: &types.MessageItemUser{ID: id}}
}
return out
}
It("removes the item with the given id", func() {
items, ok := deleteItem(mk("a", "b", "c"), "b")
Expect(ok).To(BeTrue())
Expect(len(items)).To(Equal(2))
Expect(itemID(items[0])).To(Equal("a"))
Expect(itemID(items[1])).To(Equal("c"))
})
It("reports not found for an unknown id", func() {
_, ok := deleteItem(mk("a"), "zzz")
Expect(ok).To(BeFalse())
})
})
var _ = Describe("clearInputAudio", func() {
It("resets the pending PCM and buffered Opus frames", func() {
s := &Session{InputAudioBuffer: []byte{1, 2, 3}, OpusFrames: [][]byte{{9}}}
clearInputAudio(s)
Expect(s.InputAudioBuffer).To(BeNil())
Expect(s.OpusFrames).To(BeNil())
})
})
var _ = Describe("truncateAssistantText", func() {
It("clears the text of the assistant content part at the index", func() {
items := []*types.MessageItemUnion{{Assistant: &types.MessageItemAssistant{
ID: "a1",
Content: []types.MessageContentOutput{{Type: types.MessageContentTypeText, Text: "hello world"}},
}}}
ok := truncateAssistantText(items, "a1", 0)
Expect(ok).To(BeTrue())
Expect(items[0].Assistant.Content[0].Text).To(Equal(""))
})
// Realtime assistant *audio* turns store the spoken words in .Transcript, not
// .Text, so a barge-in truncate must clear .Transcript too or it would no-op.
It("clears the transcript of an assistant audio content part", func() {
items := []*types.MessageItemUnion{{Assistant: &types.MessageItemAssistant{
ID: "a1",
Content: []types.MessageContentOutput{{Type: types.MessageContentTypeAudio, Transcript: "hello world"}},
}}}
ok := truncateAssistantText(items, "a1", 0)
Expect(ok).To(BeTrue())
Expect(items[0].Assistant.Content[0].Transcript).To(Equal(""))
})
It("returns false for an unknown id", func() {
Expect(truncateAssistantText(nil, "nope", 0)).To(BeFalse())
})
})
var _ = Describe("compactionCut", func() {
user := func(id string) *types.MessageItemUnion {
return &types.MessageItemUnion{User: &types.MessageItemUser{ID: id}}
}
call := func(id string) *types.MessageItemUnion {
return &types.MessageItemUnion{FunctionCall: &types.MessageItemFunctionCall{ID: id}}
}
out := func(id string) *types.MessageItemUnion {
return &types.MessageItemUnion{FunctionCallOutput: &types.MessageItemFunctionCallOutput{ID: id}}
}
It("cuts exactly len-keep when no pairs straddle the boundary", func() {
items := []*types.MessageItemUnion{user("1"), user("2"), user("3"), user("4")}
Expect(compactionCut(items, 2)).To(Equal(2))
})
It("returns 0 when nothing to cut", func() {
Expect(compactionCut([]*types.MessageItemUnion{user("1")}, 2)).To(Equal(0))
})
It("returns 0 (cuts nothing) when keep is 0 — the unlimited-window sentinel", func() {
items := []*types.MessageItemUnion{user("1"), user("2"), user("3")}
Expect(compactionCut(items, 0)).To(Equal(0))
})
It("moves the boundary so a call/output pair is not split", func() {
// keep=2 -> naive cut=2, but items[2] is the output of items[1]'s call;
// pull the cut right so the whole pair stays in the kept tail.
items := []*types.MessageItemUnion{user("1"), call("c"), out("c"), user("4")}
Expect(compactionCut(items, 2)).To(Equal(1))
})
})
var _ = Describe("withMemory", func() {
It("inserts a memory system message when memory is non-empty", func() {
base := schema.Messages{{Role: "system", StringContent: "instructions"}}
out := withMemory(base, "user is Bob; wants pizza")
Expect(len(out)).To(Equal(2))
Expect(out[1].Role).To(Equal("system"))
Expect(out[1].StringContent).To(ContainSubstring("user is Bob"))
Expect(out[1].StringContent).To(ContainSubstring("Summary of earlier conversation"))
})
It("is a no-op when memory is empty", func() {
base := schema.Messages{{Role: "system", StringContent: "instructions"}}
Expect(withMemory(base, "")).To(HaveLen(1))
})
})
var _ = Describe("renderItemsTranscript", func() {
It("renders user and assistant text turns", func() {
items := []*types.MessageItemUnion{
{User: &types.MessageItemUser{Content: []types.MessageContentInput{{Type: types.MessageContentTypeInputText, Text: "hi"}}}},
{Assistant: &types.MessageItemAssistant{Content: []types.MessageContentOutput{{Type: types.MessageContentTypeText, Text: "hello"}}}},
}
out := renderItemsTranscript(items)
Expect(out).To(ContainSubstring("user: hi"))
Expect(out).To(ContainSubstring("assistant: hello"))
})
// Realtime assistant *audio* turns store the spoken words in .Transcript, not
// .Text, so the transcript builder must emit .Transcript too or spoken turns
// would be dropped from the summary.
It("renders an assistant audio turn from its transcript", func() {
items := []*types.MessageItemUnion{
{Assistant: &types.MessageItemAssistant{Content: []types.MessageContentOutput{{Type: types.MessageContentTypeAudio, Transcript: "spoken words"}}}},
}
Expect(renderItemsTranscript(items)).To(ContainSubstring("assistant: spoken words"))
})
})
var _ = Describe("buildSummaryMessages", func() {
It("includes prior memory and the new transcript", func() {
msgs := buildSummaryMessages("prior facts", "user: hi", 512)
Expect(len(msgs)).To(Equal(2))
Expect(msgs[0].Role).To(Equal("system"))
Expect(msgs[1].StringContent).To(ContainSubstring("prior facts"))
Expect(msgs[1].StringContent).To(ContainSubstring("user: hi"))
})
})
var _ = Describe("compact", func() {
user := func(id, text string) *types.MessageItemUnion {
return &types.MessageItemUnion{User: &types.MessageItemUser{ID: id,
Content: []types.MessageContentInput{{Type: types.MessageContentTypeInputText, Text: text}}}}
}
It("summarizes overflow into Memory and evicts it, keeping the live tail", func() {
conv := &Conversation{Items: []*types.MessageItemUnion{
user("1", "a"), user("2", "b"), user("3", "c"), user("4", "d"),
user("5", "e"), user("6", "f"), user("7", "g"), user("8", "h"),
}}
s := &Session{CompactionEnabled: true, CompactionTrigger: 7, MaxHistoryItems: 4, MaxSummaryTokens: 512}
m := &fakeModel{predictResp: backend.LLMResponse{Response: "ROLLED UP"}}
s.compact(conv, m)
Expect(conv.Memory).To(Equal("ROLLED UP"))
Expect(len(conv.Items)).To(Equal(4))
Expect(itemID(conv.Items[0])).To(Equal("5"))
// The summarizer saw the evicted turns.
Expect(m.lastMessages[1].StringContent).To(ContainSubstring("a"))
})
It("leaves Items and Memory untouched when the summarizer errors", func() {
items := []*types.MessageItemUnion{user("1", "a"), user("2", "b"), user("3", "c")}
conv := &Conversation{Items: items}
s := &Session{CompactionEnabled: true, CompactionTrigger: 2, MaxHistoryItems: 1, MaxSummaryTokens: 512}
m := &fakeModel{predictErr: errors.New("boom")}
s.compact(conv, m)
Expect(conv.Memory).To(Equal(""))
Expect(len(conv.Items)).To(Equal(3))
})
It("strips leaked reasoning tags from the summary via the shared extractor", func() {
conv := &Conversation{Items: []*types.MessageItemUnion{
user("1", "a"), user("2", "b"), user("3", "c"), user("4", "d"),
user("5", "e"), user("6", "f"), user("7", "g"), user("8", "h"),
}}
s := &Session{CompactionEnabled: true, CompactionTrigger: 7, MaxHistoryItems: 4, MaxSummaryTokens: 512}
m := &fakeModel{predictResp: backend.LLMResponse{Response: "<think>planning the summary</think>CLEAN SUMMARY"}}
s.compact(conv, m)
Expect(conv.Memory).To(Equal("CLEAN SUMMARY"))
Expect(conv.Memory).ToNot(ContainSubstring("planning"))
})
It("does nothing when items are at or below the trigger", func() {
conv := &Conversation{Items: []*types.MessageItemUnion{user("1", "a")}}
s := &Session{CompactionEnabled: true, CompactionTrigger: 7, MaxHistoryItems: 4}
s.compact(conv, &fakeModel{predictResp: backend.LLMResponse{Response: "x"}})
Expect(conv.Memory).To(Equal(""))
Expect(len(conv.Items)).To(Equal(1))
})
})
var _ = Describe("prefixMatches", func() {
user := func(id string) *types.MessageItemUnion {
return &types.MessageItemUnion{User: &types.MessageItemUser{ID: id}}
}
It("matches when items begins with the snapshot ids in order", func() {
items := []*types.MessageItemUnion{user("1"), user("2"), user("3")}
snap := []*types.MessageItemUnion{user("1"), user("2")}
Expect(prefixMatches(items, snap)).To(BeTrue())
})
It("matches an empty snapshot", func() {
Expect(prefixMatches([]*types.MessageItemUnion{user("1")}, nil)).To(BeTrue())
})
It("fails when items is shorter than the snapshot (a concurrent delete shrank the head)", func() {
items := []*types.MessageItemUnion{user("1")}
snap := []*types.MessageItemUnion{user("1"), user("2")}
Expect(prefixMatches(items, snap)).To(BeFalse())
})
It("fails when the head ids differ (a concurrent delete reordered the head)", func() {
items := []*types.MessageItemUnion{user("2"), user("3")}
snap := []*types.MessageItemUnion{user("1"), user("2")}
Expect(prefixMatches(items, snap)).To(BeFalse())
})
})
var _ = Describe("summarizerModel", func() {
It("returns the pipeline model when no summary_model is set", func() {
m := &fakeModel{}
s := &Session{ModelInterface: m}
Expect(s.summarizerModel()).To(Equal(m))
})
It("uses the factory (once) when summary_model is set", func() {
pipeline := &fakeModel{}
small := &fakeModel{}
calls := 0
s := &Session{ModelInterface: pipeline, SummaryModel: "tiny",
summarizerFactory: func() (Model, error) { calls++; return small, nil }}
Expect(s.summarizerModel()).To(Equal(small))
Expect(s.summarizerModel()).To(Equal(small))
Expect(calls).To(Equal(1))
})
It("falls back to the pipeline model when the factory errors", func() {
pipeline := &fakeModel{}
s := &Session{ModelInterface: pipeline, SummaryModel: "tiny",
summarizerFactory: func() (Model, error) { return nil, errors.New("nope") }}
Expect(s.summarizerModel()).To(Equal(pipeline))
})
})
var _ = Describe("itemID", func() {
It("returns the id for each variant and empty for nil", func() {
Expect(itemID(nil)).To(Equal(""))
Expect(itemID(&types.MessageItemUnion{User: &types.MessageItemUser{ID: "u1"}})).To(Equal("u1"))
Expect(itemID(&types.MessageItemUnion{Assistant: &types.MessageItemAssistant{ID: "a1"}})).To(Equal("a1"))
Expect(itemID(&types.MessageItemUnion{System: &types.MessageItemSystem{ID: "s1"}})).To(Equal("s1"))
Expect(itemID(&types.MessageItemUnion{FunctionCall: &types.MessageItemFunctionCall{ID: "f1"}})).To(Equal("f1"))
Expect(itemID(&types.MessageItemUnion{FunctionCallOutput: &types.MessageItemFunctionCallOutput{ID: "o1"}})).To(Equal("o1"))
})
})

View File

@@ -79,29 +79,21 @@ func (s *GalleryStore) Create(op *GalleryOperationRecord) error {
}).Create(op).Error
}
// UpdateProgress updates progress for an operation. The cancellable flag is
// persisted on every tick so a replica that restarts mid-install rehydrates the
// op as still cancellable — otherwise the column keeps its Create-time zero
// value (false), the UI hides the cancel button, and the orphaned op can only
// be dismissed by waiting for the 30-minute stale reaper.
func (s *GalleryStore) UpdateProgress(id string, progress float64, message, downloadedSize string, cancellable bool) error {
// UpdateProgress updates progress for an operation.
func (s *GalleryStore) UpdateProgress(id string, progress float64, message, downloadedSize string) error {
return s.db.Model(&GalleryOperationRecord{}).Where("id = ?", id).Updates(map[string]any{
"progress": progress,
"message": message,
"downloaded_file_size": downloadedSize,
"cancellable": cancellable,
"updated_at": time.Now(),
}).Error
}
// UpdateStatus updates the status of an operation. A terminal status is never
// cancellable, so the flag is cleared here to keep the persisted row consistent
// with what the UI should offer.
// UpdateStatus updates the status of an operation.
func (s *GalleryStore) UpdateStatus(id, status, errMsg string) error {
updates := map[string]any{
"status": status,
"cancellable": false,
"updated_at": time.Now(),
"status": status,
"updated_at": time.Now(),
}
if errMsg != "" {
updates["error"] = errMsg

View File

@@ -1,56 +0,0 @@
package galleryop_test
import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/mudler/LocalAI/core/config"
"github.com/mudler/LocalAI/core/services/distributed"
"github.com/mudler/LocalAI/core/services/galleryop"
"github.com/mudler/LocalAI/core/services/testutil"
)
// Reproduces "an in-flight install can't be cancelled after a restart". The
// live install path marks OpStatus.Cancellable=true on every progress tick, but
// UpdateStatus persisted progress/status to the gallery store WITHOUT the
// cancellable flag, and Create defaulted it to false. So after a replica
// restart Hydrate rebuilt the op with Cancellable=false, /api/operations
// reported cancellable:false, and the UI hid the cancel button — the orphaned
// op lingered until the 30-minute stale reaper expired it. The cancellable
// state must be persisted so a rehydrated in-flight op stays cancellable.
var _ = Describe("GalleryService cancellable persistence across restart", func() {
It("rehydrates an in-flight op as still cancellable", func() {
db := testutil.SetupTestDB()
store, err := distributed.NewGalleryStore(db)
Expect(err).ToNot(HaveOccurred())
svc := galleryop.NewGalleryService(&config.ApplicationConfig{}, nil)
svc.SetGalleryStore(store)
// Seed the in-flight op row as the worker goroutine does on admission.
Expect(store.Create(&distributed.GalleryOperationRecord{
ID: "op-inflight",
GalleryElementName: "llama-cpp-development",
OpType: "backend_install",
Status: "pending",
})).To(Succeed())
// Simulate a progress tick: the live path always marks installs
// cancellable while they are downloading/processing.
svc.UpdateStatus("op-inflight", &galleryop.OpStatus{
Message: "downloading",
Progress: 25,
Cancellable: true,
})
// A fresh replica boots and hydrates from the store.
fresh := galleryop.NewGalleryService(&config.ApplicationConfig{}, nil)
fresh.SetGalleryStore(store)
Expect(fresh.Hydrate()).To(Succeed())
st := fresh.GetStatus("op-inflight")
Expect(st).ToNot(BeNil(), "the in-flight op must hydrate after a restart")
Expect(st.Cancellable).To(BeTrue(),
"a still-active install must rehydrate as cancellable so the admin can dismiss it")
})
})

View File

@@ -167,7 +167,7 @@ func (g *GalleryService) UpdateStatus(s string, op *OpStatus) {
xlog.Warn("Failed to persist gallery operation status", "op_id", s, "error", err)
}
} else {
if err := store.UpdateProgress(s, op.Progress, op.Message, op.DownloadedFileSize, op.Cancellable); err != nil {
if err := store.UpdateProgress(s, op.Progress, op.Message, op.DownloadedFileSize); err != nil {
xlog.Warn("Failed to persist gallery operation progress", "op_id", s, "error", err)
}
}
@@ -467,7 +467,6 @@ func (g *GalleryService) Start(c context.Context, cl *config.ModelConfigLoader,
GalleryElementName: op.GalleryElementName,
OpType: "backend_install",
Status: "pending",
Cancellable: true,
})
}
err := g.backendHandler(&op, systemState)
@@ -500,8 +499,6 @@ func (g *GalleryService) Start(c context.Context, cl *config.ModelConfigLoader,
GalleryElementName: op.GalleryElementName,
OpType: opType,
Status: "pending",
// A delete is not cancellable; an install is.
Cancellable: !op.Delete,
})
}
err := g.modelHandler(&op, cl, systemState)

View File

@@ -68,33 +68,6 @@ pipeline:
This is applied only to the realtime session's copy of the LLM config, so it does not affect other users of the same model. Leave it unset to use the LLM model config's own reasoning settings.
### Conversation compaction (long sessions on CPU)
By default a realtime session feeds only the last `max_history_items` turns to the LLM; older turns are dropped and forgotten. On CPU, long calls also grow expensive as the prompt fills with verbatim history. Enable `compaction` to instead fold older turns into a rolling summary, so long calls stay cheap without losing earlier context.
Compaction works with two numbers:
- **`max_history_items`** is the *live window* — the recent turns kept verbatim in the prompt.
- **`compaction.trigger_items`** is the *high-water mark* — let the buffer grow to here, then summarize the overflow (everything above `max_history_items`) into a rolling memory and evict it. It must be greater than `max_history_items`; if it is not, it is clamped up.
The gap between the two controls how often summarization runs: a summary call fires roughly every `(trigger_items - max_history_items)` turns (here, about every 6 turns).
```yaml
pipeline:
max_history_items: 6 # live window — recent turns kept verbatim
compaction:
enabled: true
trigger_items: 12 # summarize overflow back down to max_history_items
summary_model: "" # optional: a small model for the summary (CPU); default = pipeline LLM
max_summary_tokens: 512
```
{{% notice tip %}}
On CPU, set `summary_model` to a small, fast model so compaction never competes with the conversation LLM for compute. Left empty, the pipeline's own LLM produces the summary.
{{% /notice %}}
Clients can also manage history directly via the now-supported `conversation.item.delete`, `conversation.item.truncate`, and `input_audio_buffer.clear` realtime events.
## Transports
The Realtime API supports two transports: **WebSocket** and **WebRTC**.

View File

@@ -1,175 +1,4 @@
---
- name: "qwopus3.6-27b-coder-compat-mtp"
url: "github:mudler/LocalAI/gallery/virtual.yaml@master"
urls:
- https://huggingface.co/Jackrong/Qwopus3.6-27B-Coder-Compat-MTP-GGUF
description: "\U0001FA90 Qwopus-3.6-27B-Coder\nCoder SFT Release\n\nAgentic Coding &amp; Tool-Use Reasoning Model Fine-Tuned on Qwopus3.6-27B-v2\n\n\U0001F9EC Trace Inversion & Negentropy\n\U0001F9E0 27B Dense Model\n⚡ Agentic Coding\n\U0001F6E0 Tool Calling & Agent\n\U0001F3C6 SWE-bench Verified: 67.0% (off-thinking)\n\n\U0001F4A1 What is Qwopus-3.6-27B-Coder?\n\U0001FA90 Qwopus-3.6-27B-Coder is a reasoning-enhanced agentic coding model built on top of Qwopus3.6-27B-v2. It inherits the powerful reasoning foundation of the v2 base — which achieved 87.43% MMLU-Pro and 75.25% SWE-bench Verified — and further specializes it for agentic code generation, structured tool calling, debugging, and instruction-following in developer workflows. The model is designed to excel at repository-level coding tasks, multi-turn tool orchestration, and complex logical reasoning under realistic agent environments.\n\n\U0001F9E9 Agentic Coding\nOptimized for repository-level coding, debugging, patch generation, and structured multi-step development workflows.\n\n\U0001F6E0 Tool Calling\nLearns from real agent trajectories with tool definitions, tool calls, and environment feedback for robust multi-turn execution.\n\n...\n"
license: "apache-2.0"
tags:
- llm
- gguf
- vision
- multimodal
- reasoning
icon: https://cdn-uploads.huggingface.co/production/uploads/66309bd090589b7c65950665/sGQKmrMc6L6guMoaB5_Y2.png
overrides:
backend: llama-cpp
function:
automatic_tool_parsing_fallback: true
grammar:
disable: true
known_usecases:
- chat
mmproj: llama-cpp/mmproj/Qwopus3.6-27B-Coder-Compat-MTP-GGUF/mmproj-F32.gguf
options:
- use_jinja:true
- spec_type:draft-mtp
- spec_n_max:6
- spec_p_min:0.75
parameters:
model: llama-cpp/models/Qwopus3.6-27B-Coder-Compat-MTP-GGUF/Qwopus3.6-27B-Coder-Compat-MTP-Q4_K_M.gguf
template:
use_tokenizer_template: true
files:
- filename: llama-cpp/models/Qwopus3.6-27B-Coder-Compat-MTP-GGUF/Qwopus3.6-27B-Coder-Compat-MTP-Q4_K_M.gguf
sha256: f893632170124da60e159b7bcc9d91e1cda3014b2c6b8ad9c6cde38a1fcd2f6f
uri: https://huggingface.co/Jackrong/Qwopus3.6-27B-Coder-Compat-MTP-GGUF/resolve/main/Qwopus3.6-27B-Coder-Compat-MTP-Q4_K_M.gguf
- filename: llama-cpp/mmproj/Qwopus3.6-27B-Coder-Compat-MTP-GGUF/mmproj-F32.gguf
sha256: 32f7ea0600c07272547da401d460f8abbd980f3a57b69d6df87be0e2505e0b9c
uri: https://huggingface.co/Jackrong/Qwopus3.6-27B-Coder-Compat-MTP-GGUF/resolve/main/mmproj-F32.gguf
- name: "kimi-k2.7-code"
url: "github:mudler/LocalAI/gallery/virtual.yaml@master"
urls:
- https://huggingface.co/unsloth/Kimi-K2.7-Code-GGUF
description: |
## 1. Model Introduction
Kimi K2.7 Code is a coding-focused agentic model built upon Kimi K2.6. With substantial improvements on real-world long-horizon coding tasks, it strengthens end-to-end task completion across complex software engineering workflows while improving token efficiency, reducing thinking-token usage by approximately 30% compared with Kimi K2.6.
## 2. Model Summary
## 3. Evaluation Results
Benchmark
Kimi K2.6
Kimi K2.7 Code
GPT-5.5
Claude Opus 4.8
Coding
Kimi Code Bench v2
50.9
62.0
69.0
67.4
Program Bench
48.3
53.6
69.1
63.8
MLS Bench Lite
26.7
35.1
35.5
42.8
Agentic
Kimi Claw 24/7 Bench
42.9
46.9
52.8
50.4
MCP Atlas
69.4
76.0
79.4
81.3
MCP Mark Verified
72.8
81.1
92.9
76.4
Footnotes
...
license: "other"
tags:
- llm
- gguf
icon: https://huggingface.co/moonshotai/Kimi-K2.7-Code/resolve/main/figures/kimi-logo.png
overrides:
backend: llama-cpp
function:
automatic_tool_parsing_fallback: true
grammar:
disable: true
known_usecases:
- chat
mmproj: llama-cpp/mmproj/Kimi-K2.7-Code-GGUF/mmproj-F32.gguf
options:
- use_jinja:true
parameters:
min_p: 0.01
model: llama-cpp/models/Kimi-K2.7-Code-GGUF/Kimi-K2.7-Code-UD-Q8_K_XL-00001-of-00014.gguf
repeat_penalty: 1
temperature: 0.6
top_k: -1
top_p: 0.95
template:
use_tokenizer_template: true
files:
- filename: llama-cpp/models/Kimi-K2.7-Code-GGUF/Kimi-K2.7-Code-UD-Q8_K_XL-00001-of-00014.gguf
sha256: 65f0aca336f876902323a90e2aff32cac76d071b2cdd818c6a8d78be8fc2c680
uri: https://huggingface.co/unsloth/Kimi-K2.7-Code-GGUF/resolve/main/UD-Q8_K_XL/Kimi-K2.7-Code-UD-Q8_K_XL-00001-of-00014.gguf
- filename: llama-cpp/models/Kimi-K2.7-Code-GGUF/Kimi-K2.7-Code-UD-Q8_K_XL-00002-of-00014.gguf
sha256: 40f4416c130827a11502778891f4ef95b2144db90f51d63aa3548d0952a39683
uri: https://huggingface.co/unsloth/Kimi-K2.7-Code-GGUF/resolve/main/UD-Q8_K_XL/Kimi-K2.7-Code-UD-Q8_K_XL-00002-of-00014.gguf
- filename: llama-cpp/models/Kimi-K2.7-Code-GGUF/Kimi-K2.7-Code-UD-Q8_K_XL-00003-of-00014.gguf
sha256: ba2ba0b5168784ace7c752ecadfc3631279b2bb023824cb0fe9e2dab3dd28f22
uri: https://huggingface.co/unsloth/Kimi-K2.7-Code-GGUF/resolve/main/UD-Q8_K_XL/Kimi-K2.7-Code-UD-Q8_K_XL-00003-of-00014.gguf
- filename: llama-cpp/models/Kimi-K2.7-Code-GGUF/Kimi-K2.7-Code-UD-Q8_K_XL-00004-of-00014.gguf
sha256: 10298a6c98b13ef49be286fefbea8663e16473fb69bbeabe153bc80c60ae116e
uri: https://huggingface.co/unsloth/Kimi-K2.7-Code-GGUF/resolve/main/UD-Q8_K_XL/Kimi-K2.7-Code-UD-Q8_K_XL-00004-of-00014.gguf
- filename: llama-cpp/models/Kimi-K2.7-Code-GGUF/Kimi-K2.7-Code-UD-Q8_K_XL-00005-of-00014.gguf
sha256: 8e9e4c8e35d34fc4fef6bfb65a715ad7defbd196970d833c1df6924d701c88b3
uri: https://huggingface.co/unsloth/Kimi-K2.7-Code-GGUF/resolve/main/UD-Q8_K_XL/Kimi-K2.7-Code-UD-Q8_K_XL-00005-of-00014.gguf
- filename: llama-cpp/models/Kimi-K2.7-Code-GGUF/Kimi-K2.7-Code-UD-Q8_K_XL-00006-of-00014.gguf
sha256: ccff6e7f299742f82cf6f51a871e3eb3167511efaee967477cc8387f54d16442
uri: https://huggingface.co/unsloth/Kimi-K2.7-Code-GGUF/resolve/main/UD-Q8_K_XL/Kimi-K2.7-Code-UD-Q8_K_XL-00006-of-00014.gguf
- filename: llama-cpp/models/Kimi-K2.7-Code-GGUF/Kimi-K2.7-Code-UD-Q8_K_XL-00007-of-00014.gguf
sha256: 1a3b639633a2d22f71156a9f643ded2329cdd969cc21177b644b5741bac1af8e
uri: https://huggingface.co/unsloth/Kimi-K2.7-Code-GGUF/resolve/main/UD-Q8_K_XL/Kimi-K2.7-Code-UD-Q8_K_XL-00007-of-00014.gguf
- filename: llama-cpp/models/Kimi-K2.7-Code-GGUF/Kimi-K2.7-Code-UD-Q8_K_XL-00008-of-00014.gguf
sha256: bde28f682a1eab973538b2102007d952f37a13c1f7d55e2ed99177445ddc4282
uri: https://huggingface.co/unsloth/Kimi-K2.7-Code-GGUF/resolve/main/UD-Q8_K_XL/Kimi-K2.7-Code-UD-Q8_K_XL-00008-of-00014.gguf
- filename: llama-cpp/models/Kimi-K2.7-Code-GGUF/Kimi-K2.7-Code-UD-Q8_K_XL-00009-of-00014.gguf
sha256: b6a23a95b61e100f7593fa75e2363966323fa767b7e4fdf45d963b59e8fdc69f
uri: https://huggingface.co/unsloth/Kimi-K2.7-Code-GGUF/resolve/main/UD-Q8_K_XL/Kimi-K2.7-Code-UD-Q8_K_XL-00009-of-00014.gguf
- filename: llama-cpp/models/Kimi-K2.7-Code-GGUF/Kimi-K2.7-Code-UD-Q8_K_XL-00010-of-00014.gguf
sha256: fb10231c2e6d76921d40f22690f4aa08a8090c708edeaf7e581abafc24d3b25c
uri: https://huggingface.co/unsloth/Kimi-K2.7-Code-GGUF/resolve/main/UD-Q8_K_XL/Kimi-K2.7-Code-UD-Q8_K_XL-00010-of-00014.gguf
- filename: llama-cpp/models/Kimi-K2.7-Code-GGUF/Kimi-K2.7-Code-UD-Q8_K_XL-00011-of-00014.gguf
sha256: d2290be7ed1a22ac1f9f8a4813389689e075ce2ab8abc3aaaa1157a3cb1462d8
uri: https://huggingface.co/unsloth/Kimi-K2.7-Code-GGUF/resolve/main/UD-Q8_K_XL/Kimi-K2.7-Code-UD-Q8_K_XL-00011-of-00014.gguf
- filename: llama-cpp/models/Kimi-K2.7-Code-GGUF/Kimi-K2.7-Code-UD-Q8_K_XL-00012-of-00014.gguf
sha256: ce0d028314aa3fc783082dbca097e1055d69686a17ab8306574e2949568f26a5
uri: https://huggingface.co/unsloth/Kimi-K2.7-Code-GGUF/resolve/main/UD-Q8_K_XL/Kimi-K2.7-Code-UD-Q8_K_XL-00012-of-00014.gguf
- filename: llama-cpp/models/Kimi-K2.7-Code-GGUF/Kimi-K2.7-Code-UD-Q8_K_XL-00013-of-00014.gguf
sha256: 217864ce63a1d130ab39dcb0996b6097e1aa78eb896e38efaefdbbac3a00b7ec
uri: https://huggingface.co/unsloth/Kimi-K2.7-Code-GGUF/resolve/main/UD-Q8_K_XL/Kimi-K2.7-Code-UD-Q8_K_XL-00013-of-00014.gguf
- filename: llama-cpp/models/Kimi-K2.7-Code-GGUF/Kimi-K2.7-Code-UD-Q8_K_XL-00014-of-00014.gguf
sha256: eb7582ad7066c5eaa01bde95acb00b4ad9cd7b07cd50a6cf5c9ee427258bc9dd
uri: https://huggingface.co/unsloth/Kimi-K2.7-Code-GGUF/resolve/main/UD-Q8_K_XL/Kimi-K2.7-Code-UD-Q8_K_XL-00014-of-00014.gguf
- filename: llama-cpp/mmproj/Kimi-K2.7-Code-GGUF/mmproj-F32.gguf
sha256: b2cc50c8c13fe70fc4968a83332f31e9007ea09ebb9ae91d46a4e4cd2a3053cd
uri: https://huggingface.co/unsloth/Kimi-K2.7-Code-GGUF/resolve/main/mmproj-F32.gguf
- name: "qwythos-9b-claude-mythos-5-1m"
url: "github:mudler/LocalAI/gallery/virtual.yaml@master"
urls:
@@ -220,7 +49,33 @@
url: "github:mudler/LocalAI/gallery/virtual.yaml@master"
urls:
- https://huggingface.co/unsloth/GLM-5.2-GGUF
description: "# GLM-5.2\n\n\U0001F44B Join our WeChat or Discord community.\n\n\U0001F4D6 Check out the GLM-5.2 blog and GLM-5 Technical report.\n\n\U0001F4CD Use GLM-5.2 API services on Z.ai API Platform.\n\n\U0001F51C Try GLM-5.2 here.\n\n[Paper]\n[GitHub]\n\n## Introduction\n\nWe're introducing GLM-5.2, our latest flagship model for long-horizon tasks. It marks a substantial leap in long-horizon task capability over its predecessor GLM-5.1 and, for the first time, delivers that capability on a **solid 1M-token context**. GLM-5.2's new capabilities include:\n - **Solid 1M Context:** A solid 1M-token context that stably sustains long-horizon work\n - **Advanced Coding with Flexible Effort**: Stronger coding capabilities with multiple thinking effort levels to balance performance and latency\n - **Improved Architecture**: We propose IndexShare, which reuses the same indexer across every four sparse attention layers, reducing per-token FLOPs by 2.9× at a 1M context length. We also improve GLM-5.2s MTP layer for speculative decoding, increasing the acceptance length by up to 20%\n - **Pure Open**: An MIT open-source license — no regional limits, technical access without borders\n\n## Benchmark\n\n## Serve GLM-5.2 Locally\n\n...\n"
description: |
# GLM-5.2
👋 Join our WeChat or Discord community.
📖 Check out the GLM-5.2 blog and GLM-5 Technical report.
📍 Use GLM-5.2 API services on Z.ai API Platform.
🔜 Try GLM-5.2 here.
[Paper]
[GitHub]
## Introduction
We're introducing GLM-5.2, our latest flagship model for long-horizon tasks. It marks a substantial leap in long-horizon task capability over its predecessor GLM-5.1 and, for the first time, delivers that capability on a **solid 1M-token context**. GLM-5.2's new capabilities include:
- **Solid 1M Context:** A solid 1M-token context that stably sustains long-horizon work
- **Advanced Coding with Flexible Effort**: Stronger coding capabilities with multiple thinking effort levels to balance performance and latency
- **Improved Architecture**: We propose IndexShare, which reuses the same indexer across every four sparse attention layers, reducing per-token FLOPs by 2.9× at a 1M context length. We also improve GLM-5.2s MTP layer for speculative decoding, increasing the acceptance length by up to 20%
- **Pure Open**: An MIT open-source license — no regional limits, technical access without borders
## Benchmark
## Serve GLM-5.2 Locally
...
license: "mit"
tags:
- llm
@@ -343,7 +198,26 @@
url: "github:mudler/LocalAI/gallery/virtual.yaml@master"
urls:
- https://huggingface.co/michaelw9999/Qwopus3.6-27B-v2-MTP-NVFP4-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.
...
tags:
- llm
- gguf
@@ -369,7 +243,28 @@
url: "github:mudler/LocalAI/gallery/virtual.yaml@master"
urls:
- https://huggingface.co/michaelw9999/Qwopus3.6-27B-Coder-MTP-NVFP4-GGUF
description: "\U0001FA90 Qwopus-3.6-27B-Coder\nCoder SFT Release\n\nAgentic Coding &amp; Tool-Use Reasoning Model Fine-Tuned on Qwopus3.6-27B-v2\n\n\U0001F9EC Trace Inversion & Negentropy\n\U0001F9E0 27B Dense Model\n⚡ Agentic Coding\n\U0001F6E0 Tool Calling & Agent\n\U0001F3C6 SWE-bench Verified: 67.0% (off-thinking)\n\n\U0001F4A1 What is Qwopus-3.6-27B-Coder?\n\U0001FA90 Qwopus-3.6-27B-Coder is a reasoning-enhanced agentic coding model built on top of Qwopus3.6-27B-v2. It inherits the powerful reasoning foundation of the v2 base — which achieved 87.43% MMLU-Pro (300ex) and 75.25% SWE-bench Verified — and further specializes it for agentic code generation, structured tool calling, debugging, and instruction-following in developer workflows. The model is designed to excel at repository-level coding tasks, multi-turn tool orchestration, and complex logical reasoning under realistic agent environments.\n\n\U0001F9E9 Agentic Coding\nOptimized for repository-level coding, debugging, patch generation, and structured multi-step development workflows.\n\n\U0001F6E0 Tool Calling\nLearns from real agent trajectories with tool definitions, tool calls, and environment feedback for robust multi-turn execution.\n\n...\n"
description: |
🪐 Qwopus-3.6-27B-Coder
Coder SFT Release
Agentic Coding &amp; Tool-Use Reasoning Model Fine-Tuned on Qwopus3.6-27B-v2
🧬 Trace Inversion & Negentropy
🧠 27B Dense Model
⚡ Agentic Coding
🛠️ Tool Calling & Agent
🏆 SWE-bench Verified: 67.0% (off-thinking)
💡 What is Qwopus-3.6-27B-Coder?
🪐 Qwopus-3.6-27B-Coder is a reasoning-enhanced agentic coding model built on top of Qwopus3.6-27B-v2. It inherits the powerful reasoning foundation of the v2 base — which achieved 87.43% MMLU-Pro (300ex) and 75.25% SWE-bench Verified — and further specializes it for agentic code generation, structured tool calling, debugging, and instruction-following in developer workflows. The model is designed to excel at repository-level coding tasks, multi-turn tool orchestration, and complex logical reasoning under realistic agent environments.
🧩 Agentic Coding
Optimized for repository-level coding, debugging, patch generation, and structured multi-step development workflows.
🛠️ Tool Calling
Learns from real agent trajectories with tool definitions, tool calls, and environment feedback for robust multi-turn execution.
...
tags:
- llm
- gguf
@@ -1589,8 +1484,8 @@
use_tokenizer_template: true
files:
- filename: llama-cpp/models/Qwopus3.6-27B-v2-MTP-GGUF/Qwopus3.6-27B-v2-MTP-Q4_K_M.gguf
sha256: 818d68223be4d8518dac0b3b5604dde633cbbcbae1f491d842a3e26711c6606d
uri: https://huggingface.co/Jackrong/Qwopus3.6-27B-v2-MTP-GGUF/resolve/main/Qwopus3.6-27B-v2-MTP-Q4_K_M.gguf
sha256: 31cf5fc2406a0c7aaebcc26d440bf0df94e215d0589d5205bf319649c052b50a
- name: "qwen3.6-40b-claude-4.6-opus-deckard-heretic-uncensored-thinking-neo-code-di-imatrix-max"
url: "github:mudler/LocalAI/gallery/virtual.yaml@master"
urls:

View File

@@ -53,13 +53,12 @@ var _ = Describe("Gallery Distributed", Label("Distributed"), func() {
Expect(retrieved.Status).To(Equal("downloading"))
Expect(retrieved.FrontendID).To(Equal("f1"))
// Update progress (cancellable: a downloading install can be cancelled)
Expect(galleryStore.UpdateProgress(op.ID, 0.75, "75% complete", "6GB", true)).To(Succeed())
// Update progress
Expect(galleryStore.UpdateProgress(op.ID, 0.75, "75% complete", "6GB")).To(Succeed())
updated, _ := galleryStore.Get(op.ID)
Expect(updated.Progress).To(BeNumerically("~", 0.75, 0.01))
Expect(updated.Message).To(Equal("75% complete"))
Expect(updated.Cancellable).To(BeTrue())
// Complete
Expect(galleryStore.UpdateStatus(op.ID, "completed", "")).To(Succeed())

View File

@@ -104,12 +104,11 @@ var _ = Describe("Phase 4: MCP, Skills, Gallery, Fine-Tuning", Label("Distribute
}
stores.Gallery.Create(op)
Expect(stores.Gallery.UpdateProgress(op.ID, 0.5, "50% complete", "2GB", true)).To(Succeed())
Expect(stores.Gallery.UpdateProgress(op.ID, 0.5, "50% complete", "2GB")).To(Succeed())
updated, _ := stores.Gallery.Get(op.ID)
Expect(updated.Progress).To(BeNumerically("~", 0.5, 0.01))
Expect(updated.Message).To(Equal("50% complete"))
Expect(updated.Cancellable).To(BeTrue())
})
It("should deduplicate concurrent downloads", func() {