Compare commits

..
Author SHA1 Message Date
localai-org-maint-bot 359c0ce881 fix(ui): omit GPU recommendations that do not fit
When no sampled candidate fits GPU memory, ranking falls back to the
oversized pool and labels its first model Best fit. Keep GPU picks within
the existing 95% budget and hide the section when no candidate qualifies.
Remove static GPU starter picks so Home cannot reintroduce the same error.

Add browser regressions for both sections and document the empty result.
CPU fallback behavior stays unchanged.

Assisted-by: Codex:gpt-6 [Codex]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-09 03:14:22 +00:00
13 changed files with 110 additions and 117 deletions

No files matched your search

+1 -1
View File
@@ -9,7 +9,7 @@
# recipe is a make target (not a prepare.sh) so 'make purge && make' is a clean
# rebuild and so the bump bot can see the pin.
AUDIO_CPP_VERSION?=05e508a70e3600b01454c647cdb122133ba8e64c
AUDIO_CPP_VERSION?=9c6a282337cc83f227cc10428867a478947706ad
AUDIO_CPP_REPO?=https://github.com/0xShug0/audio.cpp
CURRENT_MAKEFILE_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
+2 -2
View File
@@ -1,10 +1,10 @@
# ds4 backend Makefile.
#
# Upstream pin lives below as DS4_VERSION?=6289c516273979173abbc062209a81dd3706b804
# Upstream pin lives below as DS4_VERSION?=f62ca29a308724cde5bc99134ede19104b2a3260
# (.github/bump_deps.sh) can find and update it - matches the
# llama-cpp / ik-llama-cpp / turboquant convention.
DS4_VERSION?=6289c516273979173abbc062209a81dd3706b804
DS4_VERSION?=f62ca29a308724cde5bc99134ede19104b2a3260
DS4_REPO?=https://github.com/antirez/ds4
CURRENT_MAKEFILE_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
+1 -1
View File
@@ -1,5 +1,5 @@
IK_LLAMA_VERSION?=1a2a8604a6c6c6413c06bf9adfc2f64329af4366
IK_LLAMA_VERSION?=fe215a8ccdce6b844d2a3a3bbde08ae76a6284bf
LLAMA_REPO?=https://github.com/ikawrakow/ik_llama.cpp
CMAKE_ARGS?=
+1 -1
View File
@@ -12,7 +12,7 @@
# runs 'make -C backend/go/$(BACKEND) build' and then copies package/), so it
# has to produce the binary and the package, not just the shared libraries.
NEMO_SPEECH_VERSION?=a5b6953c4a579a2bbd1c0913ad8a85c2a4d99953
NEMO_SPEECH_VERSION?=ffa38cb2408f1e832a36d46fef5e3e1e80d07e6c
NEMO_SPEECH_REPO?=https://github.com/NVIDIA/NeMo-Speech.cpp
GOCMD?=go
+1 -1
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?=c44b60b8053bbf2a5c1e014f11323fb3f2485177
WHISPER_CPP_VERSION?=52a939a2a762224e255d366c1182b2af4dd1a032
SO_TARGET?=libgowhisper.so
CMAKE_ARGS+=-DBUILD_SHARED_LIBS=OFF
@@ -103,3 +103,48 @@ test.describe("Models gallery - recommended panel prominence", () => {
await expect(grid(page).locator(".lane__tag--evidence")).toHaveCount(1);
});
});
// Start with a fitting model so absence assertions cannot pass during loading.
// Then change the polled hardware budget while keeping the same gallery.
for (const view of ["models", "home"]) {
test(`${view} removes GPU recommendations when no candidate fits`, async ({ page }) => {
await mockGallery(page, 0);
await page.route("**/v1/models", (route) =>
route.fulfill({ json: { data: [] } }),
);
const gib = 1024 ** 3;
let budget = 24 * gib;
await page.route("**/api/resources", (route) =>
route.fulfill({ json: {
type: "gpu",
aggregate: { total_memory: budget, gpu_count: 1 },
gpus: [{ vendor: "nvidia", total_memory: budget }],
} }),
);
await page.route("**/api/models/estimate/*", (route) =>
route.fulfill({ json: {
sizeBytes: 17.4 * gib,
sizeDisplay: "17.4 GB",
estimates: { 4096: { vramBytes: 18.4 * gib, vramDisplay: "18.4 GB" } },
} }),
);
await page.goto(view === "models" ? "/app/models" : "/app/");
const section = view === "models" ? panel(page) : page.locator(".home-starters");
await expect(section).toBeVisible();
await expect(section).toContainText("tiny-chat");
// Wait for BOTH recommendation estimates, not the hook's loading render
// or the gallery rail's separate context-size requests.
const estimatesFinished = REC_MODELS.map(model => page.waitForResponse(response => {
const url = new URL(response.url());
return url.pathname.endsWith('/api/models/estimate/' + model.name) &&
url.searchParams.get('contexts') === '4096' && response.status() === 200;
}).then(response => response.finished()));
budget = 12 * gib;
await Promise.all(estimatesFinished);
await page.evaluate(() => new Promise(resolve =>
requestAnimationFrame(() => requestAnimationFrame(resolve)),
));
await expect(section).toHaveCount(0, { timeout: 15_000 });
});
}
@@ -3,63 +3,17 @@ import { useTranslation } from 'react-i18next'
import { modelsApi } from '../utils/api'
import { useRecommendedModels, isNvfp4Name } from '../hooks/useRecommendedModels'
// Static fallback used only when the live gallery / estimates can't be reached
// (offline, trimmed gallery). The hook is the primary, data-driven path; these
// are real gallery names kept as a safety net so onboarding never shows nothing.
// Gemma picks use the QAT (quantization-aware-trained) Q4 builds. NVIDIA boxes
// get NVFP4 + MTP variants at the mid/large tiers (see NVIDIA below).
const BASE = {
cpu: [
{ name: 'gemma-4-e2b-it-qat-q4_0', size: '~1.5 GB' },
{ name: 'qwen3.5-4b-claude-4.6-opus-reasoning-distilled', size: '~2.5 GB' },
{ name: 'gemma-4-e4b-it-qat-q4_0', size: '~3 GB' },
{ name: 'lfm2.5-1.2b-instruct', size: '~0.8 GB' },
],
'gpu-small': [
{ name: 'gemma-4-e4b-it-qat-q4_0', size: '~3 GB' },
{ name: 'lfm2.5-8b-a1b', size: '~5 GB' },
{ name: 'qwen3.5-9b', size: '~5.5 GB' },
{ name: 'gemma-4-12b-it-qat-q4_0', size: '~7 GB' },
],
'gpu-mid': [
{ name: 'qwen3.6-27b', size: '~16 GB' },
{ name: 'qwen3.6-27b-mtp-pi-tune', size: '~16 GB' },
{ name: 'gemma-4-26b-a4b-it-qat-q4_0', size: '~16 GB' },
{ name: 'qwen3.5-27b', size: '~16 GB' },
],
'gpu-large': [
{ name: 'qwen3.6-35b-a3b-apex', size: '~20 GB' },
{ name: 'qwen3.6-35b-a3b-claude-4.6-opus-reasoning-distilled', size: '~20 GB' },
{ name: 'gemma-4-31b-it-qat-q4_0', size: '~18 GB' },
{ name: 'qwen3.5-35b-a3b-apex', size: '~20 GB' },
],
}
// NVIDIA-only overrides: NVFP4 is a Blackwell-optimised 4-bit format paired with
// MTP (multi-token prediction) for speed. Only the mid/large tiers have these.
const NVIDIA = {
'gpu-mid': [
{ name: 'qwen3.6-27b-nvfp4-mtp', size: '~14 GB' },
{ name: 'qwen3.6-27b-mtp-pi-tune', size: '~16 GB' },
{ name: 'gemma-4-26b-a4b-it-qat-q4_0', size: '~16 GB' },
{ name: 'qwen3.6-27b', size: '~16 GB' },
],
'gpu-large': [
{ name: 'qwen3.6-35b-a3b-nvfp4-mtp', size: '~18 GB' },
{ name: 'qwen3.6-27b-nvfp4-mtp', size: '~14 GB' },
{ name: 'qwen3.6-35b-a3b-apex', size: '~20 GB' },
{ name: 'gemma-4-31b-it-qat-q4_0', size: '~18 GB' },
],
}
function fallbackFor(tierId, isNvidia) {
if (isNvidia && NVIDIA[tierId]) return NVIDIA[tierId]
return BASE[tierId] || BASE.cpu
}
// Offline CPU suggestions do not claim a measured GPU fit.
const CPU_FALLBACK = [
{ name: 'gemma-4-e2b-it-qat-q4_0', size: '~1.5 GB' },
{ name: 'qwen3.5-4b-claude-4.6-opus-reasoning-distilled', size: '~2.5 GB' },
{ name: 'gemma-4-e4b-it-qat-q4_0', size: '~3 GB' },
{ name: 'lfm2.5-1.2b-instruct', size: '~0.8 GB' },
]
export default function StarterModels({ addToast, onInstallStarted }) {
const { t } = useTranslation('home')
const { recommended, tier, isNvidia, loading } = useRecommendedModels({ count: 4 })
const { recommended, tier, loading } = useRecommendedModels({ count: 4 })
const [installing, setInstalling] = useState(() => new Set())
// While the hardware probe + gallery query are in flight, render nothing
@@ -67,10 +21,11 @@ export default function StarterModels({ addToast, onInstallStarted }) {
if (loading) return null
// Prefer live recommendations; fall back to the static list only when the
// gallery yielded nothing.
// gallery yielded nothing on a CPU host. Static GPU picks have no measured
// fit and must not replace an empty set of fitting recommendations.
const items = (recommended && recommended.length > 0)
? recommended.map(r => ({ name: r.name, size: r.sizeDisplay }))
: fallbackFor(tier.id, isNvidia)
: tier.id === 'cpu' ? CPU_FALLBACK : []
if (items.length === 0) return null
+3 -3
View File
@@ -53,16 +53,16 @@ function rank(candidates, tier, count, isNvidia) {
}
const limit = tier.vram * 0.95
const fits = pool.filter(c => c.vramBytes != null && c.vramBytes <= limit)
const base = fits.length > 0 ? fits : pool // tiny GPU where nothing fits → fall through to smallest
const byPreference = (a, b) => {
// On NVIDIA, surface NVFP4 first; then largest-that-fits (best quality).
if (isNvidia) {
const an = isNvfp4Name(a.name), bn = isNvfp4Name(b.name)
if (an !== bn) return an ? -1 : 1
}
return fits.length > 0 ? b.sizeBytes - a.sizeBytes : a.sizeBytes - b.sizeBytes
return b.sizeBytes - a.sizeBytes
}
return [...base].sort(byPreference).slice(0, count)
// An oversized or unestimated model cannot be labelled a hardware fit.
return [...fits].sort(byPreference).slice(0, count)
}
export function useRecommendedModels({ count = 4, candidatePool = 10 } = {}) {
+23 -23
View File
@@ -43,7 +43,7 @@ func (capacityShortWriter) Write(p []byte) (int, error) {
var _ = Describe("EphemeralCapacityGuard", func() {
It("derives bounded defaults and preserves positive overrides", func() {
root := canonicalWorkerTempDir()
root := GinkgoT().TempDir()
limit, headroom, err := effectiveEphemeralCapacity([]string{root}, 0, -1)
Expect(err).NotTo(HaveOccurred())
Expect(limit).To(BeNumerically(">", 0))
@@ -57,8 +57,8 @@ var _ = Describe("EphemeralCapacityGuard", func() {
})
It("accounts existing regular files without following symlinks", func() {
root := canonicalWorkerTempDir()
outside := filepath.Join(canonicalWorkerTempDir(), "outside.bin")
root := GinkgoT().TempDir()
outside := filepath.Join(GinkgoT().TempDir(), "outside.bin")
Expect(os.WriteFile(filepath.Join(root, "existing.bin"), make([]byte, 6), 0o600)).To(Succeed())
Expect(os.WriteFile(outside, make([]byte, 100), 0o600)).To(Succeed())
Expect(os.Symlink(outside, filepath.Join(root, "outside-link"))).To(Succeed())
@@ -77,7 +77,7 @@ var _ = Describe("EphemeralCapacityGuard", func() {
})
It("serializes competing reservations", func() {
root := canonicalWorkerTempDir()
root := GinkgoT().TempDir()
guard, err := NewEphemeralCapacityGuard([]string{root}, 1, 0)
Expect(err).NotTo(HaveOccurred())
@@ -109,7 +109,7 @@ var _ = Describe("EphemeralCapacityGuard", func() {
})
It("makes only an equal active reservation idempotent", func() {
root := canonicalWorkerTempDir()
root := GinkgoT().TempDir()
guard, err := NewEphemeralCapacityGuard([]string{root}, 10, 0)
Expect(err).NotTo(HaveOccurred())
path := filepath.Join(root, "nested", "payload.bin")
@@ -128,7 +128,7 @@ var _ = Describe("EphemeralCapacityGuard", func() {
})
It("retains committed bytes when the same path starts another reservation", func() {
root := canonicalWorkerTempDir()
root := GinkgoT().TempDir()
guard, err := NewEphemeralCapacityGuard([]string{root}, 10, 0)
Expect(err).NotTo(HaveOccurred())
path := filepath.Join(root, "payload.bin")
@@ -147,7 +147,7 @@ var _ = Describe("EphemeralCapacityGuard", func() {
})
It("retains startup-accounted bytes when the path is reserved", func() {
root := canonicalWorkerTempDir()
root := GinkgoT().TempDir()
path := filepath.Join(root, "payload.bin")
Expect(os.WriteFile(path, make([]byte, 4), 0o600)).To(Succeed())
guard, err := NewEphemeralCapacityGuard([]string{root}, 10, 0)
@@ -163,7 +163,7 @@ var _ = Describe("EphemeralCapacityGuard", func() {
})
It("commits the regular file's actual size", func() {
root := canonicalWorkerTempDir()
root := GinkgoT().TempDir()
guard, err := NewEphemeralCapacityGuard([]string{root}, 10, 0)
Expect(err).NotTo(HaveOccurred())
path := filepath.Join(root, "payload.bin")
@@ -176,7 +176,7 @@ var _ = Describe("EphemeralCapacityGuard", func() {
})
It("preserves configured filesystem headroom", func() {
root := canonicalWorkerTempDir()
root := GinkgoT().TempDir()
guard, err := NewEphemeralCapacityGuard([]string{root}, 1<<30, 1<<62)
Expect(err).NotTo(HaveOccurred())
@@ -189,7 +189,7 @@ var _ = Describe("EphemeralCapacityGuard", func() {
})
It("reserves bounded chunks before forwarding unknown-length input", func() {
root := canonicalWorkerTempDir()
root := GinkgoT().TempDir()
guard, err := NewEphemeralCapacityGuard([]string{root}, ephemeralCapacityWriteChunk+1, 0)
Expect(err).NotTo(HaveOccurred())
path := filepath.Join(root, "payload.bin")
@@ -210,7 +210,7 @@ var _ = Describe("EphemeralCapacityGuard", func() {
})
It("waits for an open bounded writer before committing", func() {
root := canonicalWorkerTempDir()
root := GinkgoT().TempDir()
guard, err := NewEphemeralCapacityGuard([]string{root}, 10, 0)
Expect(err).NotTo(HaveOccurred())
path := filepath.Join(root, "payload.bin")
@@ -254,7 +254,7 @@ var _ = Describe("EphemeralCapacityGuard", func() {
})
It("does not share pending capacity between concurrent writers", func() {
root := canonicalWorkerTempDir()
root := GinkgoT().TempDir()
guard, err := NewEphemeralCapacityGuard([]string{root}, 10, 0)
Expect(err).NotTo(HaveOccurred())
path := filepath.Join(root, "payload.bin")
@@ -290,7 +290,7 @@ var _ = Describe("EphemeralCapacityGuard", func() {
})
It("rolls back bytes the destination writer does not accept", func() {
root := canonicalWorkerTempDir()
root := GinkgoT().TempDir()
guard, err := NewEphemeralCapacityGuard([]string{root}, 5, 0)
Expect(err).NotTo(HaveOccurred())
writer, err := guard.NewWriter(filepath.Join(root, "payload.bin"), capacityShortWriter{})
@@ -304,8 +304,8 @@ var _ = Describe("EphemeralCapacityGuard", func() {
})
It("rejects paths outside roots and through symlinks", func() {
root := canonicalWorkerTempDir()
outside := canonicalWorkerTempDir()
root := GinkgoT().TempDir()
outside := GinkgoT().TempDir()
guard, err := NewEphemeralCapacityGuard([]string{root}, 100, 0)
Expect(err).NotTo(HaveOccurred())
@@ -319,7 +319,7 @@ var _ = Describe("EphemeralCapacityGuard", func() {
})
It("supports recovery tree accounting without dropping active reservations", func() {
root := canonicalWorkerTempDir()
root := GinkgoT().TempDir()
guard, err := NewEphemeralCapacityGuard([]string{root}, 10, 0)
Expect(err).NotTo(HaveOccurred())
active := filepath.Join(root, "active", "payload.bin")
@@ -335,7 +335,7 @@ var _ = Describe("EphemeralCapacityGuard", func() {
})
It("waits for pre-release reservations before request cleanup scans", func() {
root := canonicalWorkerTempDir()
root := GinkgoT().TempDir()
guard, err := NewEphemeralCapacityGuard([]string{root}, 10, 0)
Expect(err).NotTo(HaveOccurred())
path := filepath.Join(root, "audio", "request-1", "input.wav")
@@ -357,7 +357,7 @@ var _ = Describe("EphemeralCapacityGuard", func() {
})
It("rejects staging after request cleanup begins", func() {
root := canonicalWorkerTempDir()
root := GinkgoT().TempDir()
guard, err := NewEphemeralCapacityGuard([]string{root}, 10, 0)
Expect(err).NotTo(HaveOccurred())
Expect(guard.BeginRequestRelease(context.Background(), "request-1")).To(Succeed())
@@ -370,7 +370,7 @@ var _ = Describe("EphemeralCapacityGuard", func() {
})
It("leaves a late commit recoverable when release times out", func() {
root := canonicalWorkerTempDir()
root := GinkgoT().TempDir()
guard, err := NewEphemeralCapacityGuard([]string{root}, 10, 0)
Expect(err).NotTo(HaveOccurred())
path := filepath.Join(root, "audio", "request-1", "late.wav")
@@ -386,7 +386,7 @@ var _ = Describe("EphemeralCapacityGuard", func() {
})
It("bounds release markers without reopening registered work", func() {
root := canonicalWorkerTempDir()
root := GinkgoT().TempDir()
guard, err := NewEphemeralCapacityGuard([]string{root}, 10, 0)
Expect(err).NotTo(HaveOccurred())
Expect(guard.BeginRequestOperation("request-pinned")).To(Succeed())
@@ -411,7 +411,7 @@ var _ = Describe("EphemeralCapacityGuard", func() {
})
It("applies backpressure at the release-pin cap and clears ownership", func() {
root := canonicalWorkerTempDir()
root := GinkgoT().TempDir()
guard, err := NewEphemeralCapacityGuard([]string{root}, 10, 0)
Expect(err).NotTo(HaveOccurred())
path := filepath.Join(root, "audio", "request-target", "input.wav")
@@ -438,7 +438,7 @@ var _ = Describe("EphemeralCapacityGuard", func() {
})
It("makes committed files recoverable when pin backpressure expires", func() {
root := canonicalWorkerTempDir()
root := GinkgoT().TempDir()
guard, err := NewEphemeralCapacityGuard([]string{root}, 10, 0)
Expect(err).NotTo(HaveOccurred())
path := filepath.Join(root, "audio", "request-target", "input.wav")
@@ -459,7 +459,7 @@ var _ = Describe("EphemeralCapacityGuard", func() {
})
It("rejects a registered cache-hit claim after pin backpressure expires", func() {
root := canonicalWorkerTempDir()
root := GinkgoT().TempDir()
path := filepath.Join(root, "audio", "request-target", "input.wav")
Expect(os.MkdirAll(filepath.Dir(path), 0o750)).To(Succeed())
Expect(os.WriteFile(path, []byte("data"), 0o600)).To(Succeed())
@@ -24,7 +24,7 @@ var _ = Describe("Worker ephemeral staging cleanup", func() {
return dir
}
BeforeEach(func() { stagingDir = canonicalWorkerTempDir() })
BeforeEach(func() { stagingDir = GinkgoT().TempDir() })
It("removes staged request directories older than the TTL", func() {
old := mkEphemeral("aaaa1111", 48*time.Hour)
@@ -57,7 +57,7 @@ var _ = Describe("Worker ephemeral staging cleanup", func() {
})
It("sweeps both transport roots by newest descendant and skips active requests", func() {
cacheDir := canonicalWorkerTempDir()
cacheDir := GinkgoT().TempDir()
httpRoot := filepath.Join(stagingDir, "ephemeral")
s3Root := filepath.Join(cacheDir, "ephemeral")
guard, err := NewEphemeralCapacityGuard([]string{httpRoot, s3Root}, 8, 0)
@@ -87,7 +87,7 @@ func (m *releaseMessagingClient) Close() {}
var _ = Describe("Worker exact-key staging release", func() {
It("protects a startup-accounted HTTP cache hit through authenticated repeated probes", func() {
stagingDir := canonicalWorkerTempDir()
stagingDir := GinkgoT().TempDir()
root := filepath.Join(stagingDir, "ephemeral")
key := "ephemeral/audio/request-id/input.wav"
remotePath := filepath.Join(stagingDir, filepath.FromSlash(key))
@@ -107,11 +107,11 @@ var _ = Describe("Worker exact-key staging release", func() {
Expect(err).NotTo(HaveOccurred())
addr := listener.Addr().String()
Expect(listener.Close()).To(Succeed())
server, err := nodes.StartFileTransferServerWithCapacity(addr, stagingDir, canonicalWorkerTempDir(), canonicalWorkerTempDir(), "secret", 0, nil, guard)
server, err := nodes.StartFileTransferServerWithCapacity(addr, stagingDir, GinkgoT().TempDir(), GinkgoT().TempDir(), "secret", 0, nil, guard)
Expect(err).NotTo(HaveOccurred())
DeferCleanup(nodes.ShutdownFileTransferServer, server)
localPath := filepath.Join(canonicalWorkerTempDir(), "input.wav")
localPath := filepath.Join(GinkgoT().TempDir(), "input.wav")
Expect(os.WriteFile(localPath, content, 0o600)).To(Succeed())
stager := nodes.NewHTTPFileStager(func(string) (string, error) { return addr, nil }, "secret")
for range 2 {
@@ -129,7 +129,7 @@ var _ = Describe("Worker exact-key staging release", func() {
})
It("claims a startup-scanned cache hit against stale recovery until release", func() {
cacheDir := canonicalWorkerTempDir()
cacheDir := GinkgoT().TempDir()
root := filepath.Join(cacheDir, "ephemeral")
key := "ephemeral/audio/request-id/input.wav"
cachePath := filepath.Join(cacheDir, filepath.FromSlash(key))
@@ -158,7 +158,7 @@ var _ = Describe("Worker exact-key staging release", func() {
})
It("downloads again when a cache file disappears while being claimed", func() {
cacheDir := canonicalWorkerTempDir()
cacheDir := GinkgoT().TempDir()
key := "ephemeral/audio/request-id/input.wav"
cachePath := filepath.Join(cacheDir, filepath.FromSlash(key))
Expect(os.MkdirAll(filepath.Dir(cachePath), 0o750)).To(Succeed())
@@ -176,7 +176,7 @@ var _ = Describe("Worker exact-key staging release", func() {
})
It("makes repeated cache-hit claims idempotent", func() {
cacheDir := canonicalWorkerTempDir()
cacheDir := GinkgoT().TempDir()
root := filepath.Join(cacheDir, "ephemeral")
key := "ephemeral/audio/request-id/input.wav"
cachePath := filepath.Join(cacheDir, filepath.FromSlash(key))
@@ -199,7 +199,7 @@ var _ = Describe("Worker exact-key staging release", func() {
})
It("capacity-checks growth of a startup-scanned cache file", func() {
cacheDir := canonicalWorkerTempDir()
cacheDir := GinkgoT().TempDir()
root := filepath.Join(cacheDir, "ephemeral")
key := "ephemeral/audio/request-id/input.wav"
cachePath := filepath.Join(cacheDir, filepath.FromSlash(key))
@@ -220,7 +220,7 @@ var _ = Describe("Worker exact-key staging release", func() {
})
It("reserves S3 object size before download and releases it with the exact key", func() {
cacheDir := canonicalWorkerTempDir()
cacheDir := GinkgoT().TempDir()
root := filepath.Join(cacheDir, "ephemeral")
store := &stagingObjectStore{payload: []byte("data")}
fm, err := storage.NewFileManager(store, cacheDir)
@@ -240,7 +240,7 @@ var _ = Describe("Worker exact-key staging release", func() {
})
It("rejects an oversized S3 object before starting its download", func() {
cacheDir := canonicalWorkerTempDir()
cacheDir := GinkgoT().TempDir()
store := &stagingObjectStore{payload: []byte("oversized")}
fm, err := storage.NewFileManager(store, cacheDir)
Expect(err).NotTo(HaveOccurred())
@@ -253,7 +253,7 @@ var _ = Describe("Worker exact-key staging release", func() {
})
It("rolls back an S3 reservation when the download fails", func() {
cacheDir := canonicalWorkerTempDir()
cacheDir := GinkgoT().TempDir()
root := filepath.Join(cacheDir, "ephemeral")
store := &stagingObjectStore{payload: []byte("data"), getErr: errors.New("download failed")}
fm, err := storage.NewFileManager(store, cacheDir)
@@ -267,7 +267,7 @@ var _ = Describe("Worker exact-key staging release", func() {
})
It("removes only the exact cache file and upload sidecars", func() {
cacheDir := canonicalWorkerTempDir()
cacheDir := GinkgoT().TempDir()
categoryDir := filepath.Join(cacheDir, "ephemeral", "request-id", "audio")
Expect(os.MkdirAll(categoryDir, 0750)).To(Succeed())
target := filepath.Join(categoryDir, "input.wav")
@@ -285,7 +285,7 @@ var _ = Describe("Worker exact-key staging release", func() {
})
It("succeeds for a missing file and prunes empty category and request directories", func() {
cacheDir := canonicalWorkerTempDir()
cacheDir := GinkgoT().TempDir()
categoryDir := filepath.Join(cacheDir, "ephemeral", "request-id", "audio")
Expect(os.MkdirAll(categoryDir, 0750)).To(Succeed())
@@ -298,8 +298,8 @@ var _ = Describe("Worker exact-key staging release", func() {
})
It("rejects traversal and symlink escapes", func() {
cacheDir := canonicalWorkerTempDir()
outsideDir := canonicalWorkerTempDir()
cacheDir := GinkgoT().TempDir()
outsideDir := GinkgoT().TempDir()
outsidePath := filepath.Join(outsideDir, "input.wav")
Expect(os.WriteFile(outsidePath, []byte("keep"), 0640)).To(Succeed())
requestDir := filepath.Join(cacheDir, "ephemeral", "request-id")
@@ -319,7 +319,7 @@ var _ = Describe("Worker exact-key staging release", func() {
It("rejects symlinked files and sidecars without deleting their targets", func() {
for _, linkedName := range []string{"input.wav", "input.wav.sha256", "input.wav.sha256.target"} {
cacheDir := canonicalWorkerTempDir()
cacheDir := GinkgoT().TempDir()
categoryDir := filepath.Join(cacheDir, "ephemeral", "request-id", "audio")
Expect(os.MkdirAll(categoryDir, 0750)).To(Succeed())
target := filepath.Join(categoryDir, "input.wav")
@@ -336,7 +336,7 @@ var _ = Describe("Worker exact-key staging release", func() {
})
It("registers an exact release handler", func() {
cacheDir := canonicalWorkerTempDir()
cacheDir := GinkgoT().TempDir()
path := filepath.Join(cacheDir, "ephemeral", "request-id", "audio", "input.wav")
Expect(os.MkdirAll(filepath.Dir(path), 0750)).To(Succeed())
Expect(os.WriteFile(path, []byte("data"), 0640)).To(Succeed())
@@ -358,7 +358,7 @@ var _ = Describe("Worker exact-key staging release", func() {
})
It("releases a request batch through one worker message", func() {
cacheDir := canonicalWorkerTempDir()
cacheDir := GinkgoT().TempDir()
keys := []string{
"ephemeral/audio/request-id/input.wav",
"ephemeral/images/request-id/frame.jpg",
@@ -387,7 +387,7 @@ var _ = Describe("Worker exact-key staging release", func() {
})
It("returns validation errors through the release handler", func() {
cacheDir := canonicalWorkerTempDir()
cacheDir := GinkgoT().TempDir()
fm, err := storage.NewFileManager(nil, cacheDir)
Expect(err).NotTo(HaveOccurred())
client := &releaseMessagingClient{}
@@ -1,7 +1,6 @@
package worker
import (
"path/filepath"
"testing"
. "github.com/onsi/ginkgo/v2"
@@ -12,11 +11,3 @@ func TestWorker(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Worker Suite")
}
// Capacity guards reject symlink components, including macOS /var -> /private/var.
func canonicalWorkerTempDir() string {
GinkgoHelper()
dir, err := filepath.EvalSymlinks(GinkgoT().TempDir())
Expect(err).NotTo(HaveOccurred())
return dir
}
+2
View File
@@ -19,6 +19,8 @@ This section covers everything you need to know about installing and configuring
The Model Gallery is the simplest way to install models. It provides pre-configured models ready to use.
GPU recommendations require a memory estimate within 95% of the detected model memory budget at a 4096-token context. If none of the sampled candidates fit, the recommendation section is hidden. You can still browse the gallery and check individual models at your intended context size. The Home page also omits static GPU suggestions when no fitting recommendation is available.
### Via WebUI
1. Open the LocalAI WebUI at `http://localhost:8080`