Compare commits

..

1 Commits

Author SHA1 Message Date
localai-org-maint-bot
bc8113a8d6 fix(gallery): persist inference defaults under parameters
Gallery installs merged family defaults at the YAML root and only re-marshaled them on the artifact path. Persist the defaults in the loader-visible parameters map for every install path while preserving authored overrides.

Assisted-by: Codex:gpt-5
2026-07-31 18:06:29 +00:00
57 changed files with 480 additions and 1696 deletions

View File

@@ -125,7 +125,7 @@ The per-backend prefix match only sees files under a backend's own directory, so
| `backend/backend.proto` | nothing if the edit is additive-only, otherwise everything (see below) |
| `backend/Dockerfile.<x>` | the Linux entries whose `dockerfile:` names it |
| `backend/python/common/` | Python, Linux + Darwin |
| `scripts/build/package-gpu-libs.sh` | every Linux entry (Python, Go and C++ all run it) |
| `scripts/build/package-gpu-libs.sh` | Python, Linux only |
| `scripts/build/<lang>-darwin.sh` | the Darwin entries that build target routes to |
| `.github/workflows/backend_build[_darwin].yml` | everything on that OS |
| anything else under `scripts/build/` (except `*_test.sh`) | everything — conservative default for unclassified packaging inputs |

View File

@@ -113,54 +113,6 @@ if [ "${BUILD_TYPE:-}" = "vulkan" ] && [ "${SKIP_DRIVERS:-false}" = "false" ]; t
rm -rf /var/lib/apt/lists/*
fi
# --- 2b. Intel graphics driver (BUILD_TYPE=sycl*) ---
# The Intel oneAPI base image brings the compilers and the oneAPI libraries, but
# not the driver that talks to the graphics card. The packaging step copies that
# driver into the backend, so that the backend works on a machine which has no
# Intel graphics packages of its own, for the same reason the Vulkan section
# above installs the Mesa drivers. Install it here so there is something to copy.
#
# Only the sycl builds are covered, because those are the ones whose packaging
# copies the driver. See package_intel_libs in scripts/build/package-gpu-libs.sh.
#
# The driver comes from Intel's own package repository, not from the Ubuntu
# archive. The archive has 23.43 from late 2023, which does not know any card
# released since, so a machine with a recent Intel GPU would end up carrying a
# driver that cannot drive it. Intel's repository has 25.18 for the same Ubuntu
# release.
#
# Anything that goes wrong here fails the build, on purpose. An unreachable
# repository is a passing problem that a retry fixes, whereas carrying a
# different driver than intended, or none, is a difference nobody would notice
# until a user reports an idle GPU.
if case "${BUILD_TYPE:-}" in sycl*) true;; *) false;; esac \
&& [ "${SKIP_DRIVERS:-false}" = "false" ]; then
# Ubuntu release name, which is what the repository is indexed by.
ubuntu_codename=$(. /etc/os-release && echo "${VERSION_CODENAME:-}")
if [ -z "$ubuntu_codename" ]; then
echo "ERROR: cannot tell which Ubuntu release this image is, so cannot pick the Intel driver repository" >&2
exit 1
fi
# The key is armored text, which apt reads directly from a .asc file, so
# there is no need for gnupg here. "unified" is the component Intel ships
# its current driver in.
mkdir -p /usr/share/keyrings
curl -fsSL https://repositories.intel.com/gpu/intel-graphics.key \
-o /usr/share/keyrings/intel-graphics.asc
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/intel-graphics.asc] https://repositories.intel.com/gpu/ubuntu ${ubuntu_codename} unified" \
> /etc/apt/sources.list.d/intel-graphics.list
apt-get update
# The first package holds the driver OpenCL talks to, the second the driver
# Level Zero talks to. Between them they pull in the compiler and the memory
# manager that both need.
apt-get install -y --no-install-recommends \
intel-opencl-icd \
libze-intel-gpu1
apt-get clean
rm -rf /var/lib/apt/lists/*
fi
# --- 3. CUDA toolkit (BUILD_TYPE=cublas|l4t) ---
if { [ "${BUILD_TYPE:-}" = "cublas" ] || [ "${BUILD_TYPE:-}" = "l4t" ]; } && [ "${SKIP_DRIVERS:-false}" = "false" ]; then
apt-get update

View File

@@ -1,14 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
arch=${1:?target architecture is required}
build_type=${2-}
# GPU arm64 base images do not consistently provide the gcc-14 toolchain needed
# to compile ggml's armv9.2 CPU variants. Keep their portable fallback until the
# builder images can supply that compiler.
if [ "$arch" = "arm64" ] && [ -n "$build_type" ]; then
echo llama-cpp-fallback
else
echo llama-cpp-cpu-all
fi

View File

@@ -18,12 +18,10 @@ if [[ -n "${CUDA_DOCKER_ARCH:-}" ]]; then
fi
cd /LocalAI/backend/cpp/llama-cpp
BUILD_TARGET=$(/LocalAI/.docker/llama-cpp-build-target.sh "${TARGETARCH}" "${BUILD_TYPE:-}")
if [ "$BUILD_TARGET" = "llama-cpp-cpu-all" ]; then
# One build with ggml CPU_ALL_VARIANTS replaces the per-microarch binaries (x86:
# avx/avx2/avx512/fallback; arm64: armv8.x/armv9.x). BUILD_TYPE remains in the
# environment, so GPU builds retain their accelerator backend while ggml dlopens the
# best CPU library when work is offloaded to the host.
if [ -z "${BUILD_TYPE:-}" ]; then
# Pure CPU image (BUILD_TYPE empty): one build with ggml CPU_ALL_VARIANTS replaces the
# per-microarch binaries (x86: avx/avx2/avx512/fallback; arm64: armv8.x/armv9.x). ggml
# dlopens the best libggml-cpu-*.so at runtime by probing host CPU features.
#
# arm64: the CPU_ALL_VARIANTS table includes armv9.2 SME variants whose -march=...+sme is
# rejected by the Ubuntu 24.04 default gcc-13. gcc-14 accepts it, so build the arm64
@@ -37,8 +35,14 @@ if [ "$BUILD_TARGET" = "llama-cpp-cpu-all" ]; then
apt-get update -qq && apt-get install -y -qq gcc-14 g++-14
export CC=gcc-14 CXX=g++-14
fi
make llama-cpp-cpu-all
else
# GPU build (cublas/hipblas/sycl/vulkan/...): the accelerator does the compute, so a
# single fallback CPU build is enough - no per-microarch CPU variants needed. (This also
# keeps the heavy GPU backend compile from also building the whole CPU variant matrix,
# and avoids the gcc-14 apt step on GPU base images such as nvidia l4t.)
make llama-cpp-fallback
fi
make "$BUILD_TARGET"
make llama-cpp-grpc
make llama-cpp-rpc-server

View File

@@ -1,14 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
arch=${1:?target architecture is required}
build_type=${2-}
# GPU arm64 base images do not consistently provide the gcc-14 toolchain needed
# to compile ggml's armv9.2 CPU variants. Keep their portable fallback until the
# builder images can supply that compiler.
if [ "$arch" = "arm64" ] && [ -n "$build_type" ]; then
echo turboquant-fallback
else
echo turboquant-cpu-all
fi

View File

@@ -19,18 +19,20 @@ fi
cd /LocalAI/backend/cpp/turboquant
BUILD_TARGET=$(/LocalAI/.docker/turboquant-build-target.sh "${TARGETARCH}" "${BUILD_TYPE:-}")
if [ "$BUILD_TARGET" = "turboquant-cpu-all" ]; then
# BUILD_TYPE remains in the environment, so GPU builds retain their accelerator while
# ggml selects the best CPU library when model work is offloaded to the host.
if [ -z "${BUILD_TYPE:-}" ]; then
# Pure CPU image: one ggml CPU_ALL_VARIANTS build replaces the per-microarch binaries.
# arm64: the armv9.2 SME variants need gcc-14 (gcc-13 rejects +sme).
if [ "${TARGETARCH}" = "arm64" ]; then
sh /LocalAI/.docker/apt-mirror.sh || true
apt-get update -qq && apt-get install -y -qq gcc-14 g++-14
export CC=gcc-14 CXX=g++-14
fi
make turboquant-cpu-all
else
# GPU build (cublas/hipblas/sycl/vulkan/...): single fallback CPU build, the accelerator
# does the compute. Keeps the GPU compile from also building the CPU variant matrix and
# avoids the gcc-14 apt step on GPU base images such as nvidia l4t.
make turboquant-fallback
fi
make "$BUILD_TARGET"
make turboquant-grpc
make turboquant-rpc-server

View File

@@ -1,64 +0,0 @@
#!/usr/bin/env bash
# Refreshes the counters shown on the landing page from the GitHub API.
#
# The numbers used to be typed into the templates by hand, which meant they
# only moved when somebody remembered, and a stale star count on the front
# page is worse than no star count. Everything the API can answer for lives
# in website/data/stats.yaml and is rewritten wholesale by this script.
#
# Anything the API cannot answer for (the Discord member count) is read back
# out of the existing file and carried through untouched.
set -euo pipefail
REPO="${REPO:-mudler/LocalAI}"
OUT="${OUT:-website/data/stats.yaml}"
# The contributors and releases endpoints are paginated and never report a
# total. Asking for one item per page makes the last page number equal to the
# item count, which the Link header hands over.
count_via_link_header() {
local path="$1" link last
link=$(gh api -i "${path}?per_page=1" 2>/dev/null | tr -d '\r' | grep -i '^link:' || true)
if [ -z "$link" ]; then
# No Link header means a single page, so count that page directly.
gh api "${path}?per_page=100" --jq 'length'
return
fi
last=$(sed -n 's/.*[?&]page=\([0-9]*\)>; rel="last".*/\1/p' <<<"$link")
[ -n "$last" ] || { gh api "${path}?per_page=100" --jq 'length'; return; }
printf '%s\n' "$last"
}
read -r stars forks < <(gh api "repos/${REPO}" --jq '"\(.stargazers_count) \(.forks_count)"')
contributors=$(count_via_link_header "repos/${REPO}/contributors")
releases=$(count_via_link_header "repos/${REPO}/releases")
# Not derivable from the GitHub API, so keep whatever is already on disk.
discord=$(sed -n 's/^discord: *\([0-9]*\).*/\1/p' "$OUT" 2>/dev/null | head -1)
discord="${discord:-0}"
for n in stars forks contributors releases; do
v="${!n}"
[[ "$v" =~ ^[0-9]+$ ]] && [ "$v" -gt 0 ] || {
echo "refusing to write: ${n} came back as '${v}'" >&2
exit 1
}
done
cat > "$OUT" <<YAML
# Counters shown on the landing page.
#
# The four GitHub fields are rewritten by .github/ci/refresh-site-counters.sh,
# which runs weekly from .github/workflows/refresh-site-counters.yml. Editing
# them by hand works but will be overwritten on the next run.
stars: ${stars}
forks: ${forks}
contributors: ${contributors}
releases: ${releases}
# The GitHub API cannot answer for this one, so it is maintained by hand and
# the refresh script carries it through untouched.
discord: ${discord}
YAML
echo "stars=${stars} forks=${forks} contributors=${contributors} releases=${releases} discord=${discord}"

View File

@@ -1,44 +0,0 @@
name: Refresh site counters
# The landing page shows a star count, a contributor count and a release
# count. They were typed in by hand, so they drifted the moment somebody
# forgot. This pulls the real numbers once a week and commits them only when
# they have actually moved, which in turn triggers the usual Pages deploy.
on:
schedule:
# Mondays, 06:17 UTC. Off the hour on purpose, since the scheduler queues
# everything that asks for :00 and drops what it cannot run.
- cron: '17 6 * * 1'
workflow_dispatch:
permissions:
contents: write
concurrency:
group: refresh-site-counters
cancel-in-progress: false
jobs:
refresh:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Read the counts off the GitHub API
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: ./.github/ci/refresh-site-counters.sh
- name: Commit only if something moved
run: |
if git diff --quiet -- website/data/stats.yaml; then
echo "counters unchanged, nothing to commit"
exit 0
fi
git diff --unified=0 -- website/data/stats.yaml
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add website/data/stats.yaml
git commit -m "chore(website): refresh the counters"
git push

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?=f78227c52736a4792a50aa3f82ead7e7385c891b
AUDIO_CPP_VERSION?=f32876cfb45732dd4f43264e9104d229e95b0bc3
AUDIO_CPP_REPO?=https://github.com/0xShug0/audio.cpp
CURRENT_MAKEFILE_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))

View File

@@ -1,7 +1,7 @@
# Pinned to the HEAD of the `prism` branch on https://github.com/PrismML-Eng/llama.cpp.
# Auto-bumped nightly by .github/workflows/bump_deps.yaml.
BONSAI_VERSION?=4dd165625bb6c020285eec8b342af25cf60233dd
BONSAI_VERSION?=7529fdaaf99ffdc5ca71ace9c7409a56b27ad92f
LLAMA_REPO?=https://github.com/PrismML-Eng/llama.cpp
CMAKE_ARGS?=

View File

@@ -40,27 +40,6 @@ else
if [ -d "$CURDIR/lib/hipblaslt/library" ]; then
export HIPBLASLT_TENSILE_LIBPATH="$CURDIR"/lib/hipblaslt/library
fi
# Backends built for Intel GPUs carry a copy of the Intel graphics driver,
# and libze_loader is only there in those builds. Level Zero looks for a
# driver on its own, so point it at the copy that came with this backend: it
# was built against the same C library, while the machine's own driver may
# not have been, and loading that one can crash on start.
#
# Anything the user set is left alone, so a machine with a graphics card
# newer than the driver carried here can still be told to use its own.
# Nothing is said about OpenCL: no OpenCL driver is carried, so anything we
# set there would leave OpenCL worse off than the machine's own setup.
if [ -e "$CURDIR/lib/libze_loader.so.1" ]; then
if [ -e "$CURDIR/lib/libze_intel_gpu.so.1" ] && [ -z "${ZE_ENABLE_ALT_DRIVERS:-}" ]; then
export ZE_ENABLE_ALT_DRIVERS="$CURDIR"/lib/libze_intel_gpu.so.1
fi
# Ask the driver how much graphics memory is free. Without this, the
# backend reads zero on an integrated graphics chip, because such a chip
# shares the system memory instead of having its own.
if [ -z "${ZES_ENABLE_SYSMAN:-}" ]; then
export ZES_ENABLE_SYSMAN=1
fi
fi
fi
# If there is a lib/ld.so, use it

View File

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

View File

@@ -1,5 +1,5 @@
LLAMA_VERSION?=876a4321163249c43ca4e986818fab5ab081f282
LLAMA_VERSION?=1cbfd1988311775425d36c0ce066590f7d3049cf
LLAMA_REPO?=https://github.com/ggerganov/llama.cpp
CMAKE_ARGS?=

View File

@@ -0,0 +1,225 @@
# MiniMax-M3 chat-template parser, vendored from upstream llama.cpp PR #24523.
#
# Upstream has since merged the *model* half of #24523 (LLM_ARCH_MINIMAX_M3,
# src/models/minimax-m3.cpp, the gguf-py constants and conversion/minimax.py), so
# only the chat half is carried here: M3's namespace token "]<]minimax[>[" collides
# with the autoparser's markup delimiters, so common/chat.cpp needs a dedicated
# template detection + PEG parser that upstream does not have yet.
#
# Rebased against LLAMA_VERSION 0d47ea7427463093e69128bf2c2f9cd06b3ee5b3, which also
# renamed common_chat_params::thinking_end_tag to thinking_end_tags (a vector).
# LLAMA_VERSION is auto-bumped nightly; if a bump rejects this patch, re-vendor from
# #24523 — or, once the chat half merges upstream, delete this file.
# See https://github.com/mudler/LocalAI/issues/10820 and PR #10837.
diff --git a/common/chat.cpp b/common/chat.cpp
index 7a6e7238c..2dd015a2e 100644
--- a/common/chat.cpp
+++ b/common/chat.cpp
@@ -2121,6 +2121,191 @@ static common_chat_params common_chat_params_init_deepseek_v3_2(const common_cha
return data;
}
+static common_chat_params common_chat_params_init_minimax_m3(const common_chat_template & tmpl,
+ const autoparser::generation_params & inputs) {
+ common_chat_params data;
+
+ data.prompt = common_chat_template_direct_apply_impl(tmpl, inputs);
+ data.generation_prompt = common_chat_template_generation_prompt_impl(tmpl, inputs);
+ data.format = COMMON_CHAT_FORMAT_PEG_NATIVE;
+ data.supports_thinking = true;
+ data.thinking_start_tag = "<mm:think>";
+ data.thinking_end_tags = {"</mm:think>"};
+
+ // M3 prefixes every tool tag with the namespace token "]<]minimax[>[";
+ // params use the parameter name as the tag (<file_path>...</file_path>).
+ const std::string NS = "]<]minimax[>[";
+ const std::string THINK_START = "<mm:think>";
+ const std::string THINK_END = "</mm:think>";
+ const std::string FC_START = NS + "<tool_call>";
+ const std::string FC_END = NS + "</tool_call>";
+ const std::string INVOKE_END = NS + "</invoke>";
+
+ data.preserved_tokens = {
+ NS,
+ "<tool_call>",
+ "</tool_call>",
+ THINK_START,
+ THINK_END,
+ };
+
+ auto has_tools = inputs.tools.is_array() && !inputs.tools.empty();
+ auto has_response_format = !inputs.json_schema.is_null() && inputs.json_schema.is_object();
+ auto extract_reasoning = inputs.reasoning_format != COMMON_REASONING_FORMAT_NONE;
+ auto include_grammar = has_response_format || (has_tools && inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE);
+
+ const std::string GEN_PROMPT = data.generation_prompt;
+
+ if (inputs.has_continuation()) {
+ const auto & msg = inputs.continue_msg;
+
+ data.generation_prompt = GEN_PROMPT + THINK_START + msg.reasoning_content;
+ if (inputs.continue_final_message == COMMON_CHAT_CONTINUATION_CONTENT) {
+ data.generation_prompt += THINK_END + msg.render_content();
+ }
+
+ data.prompt += data.generation_prompt;
+ }
+
+ auto parser = build_chat_peg_parser([&](common_chat_peg_builder & p) {
+ auto generation_prompt = p.literal(GEN_PROMPT);
+ auto end = p.end();
+
+ auto reasoning = p.eps();
+ // M3 can emit a bare </mm:think> (no opener) after tool results; keep the opener optional.
+ if (extract_reasoning && inputs.enable_thinking) {
+ reasoning = p.optional(p.optional(p.literal(THINK_START)) + p.reasoning(p.until(THINK_END)) + THINK_END);
+ } else if (extract_reasoning) {
+ reasoning = p.optional(p.optional(p.literal(THINK_START)) + p.until(THINK_END) + p.literal(THINK_END));
+ }
+
+ if (has_response_format) {
+ auto response_format = p.rule("response-format",
+ p.literal("```json") + p.space() +
+ p.content(p.schema(p.json(), "response-format-schema", inputs.json_schema)) +
+ p.space() + p.literal("```"));
+ return generation_prompt + reasoning + response_format + end;
+ }
+
+ if (!has_tools || inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_NONE) {
+ return generation_prompt + reasoning + p.content(p.rest()) + end;
+ }
+
+ auto tool_choice = p.choice();
+ foreach_function(inputs.tools, [&](const json & tool) {
+ const auto & function = tool.at("function");
+ std::string name = function.at("name");
+ auto params = function.contains("parameters") ? function.at("parameters") : json::object();
+ const auto & props = params.contains("properties") ? params.at("properties") : json::object();
+
+ std::set<std::string> required;
+ if (params.contains("required")) {
+ params.at("required").get_to(required);
+ }
+
+ auto schema_info = common_schema_info();
+ schema_info.resolve_refs(params);
+
+ std::vector<common_peg_parser> required_parsers;
+ std::vector<common_peg_parser> optional_parsers;
+ for (const auto & [param_name, param_schema] : props.items()) {
+ bool is_required = required.find(param_name) != required.end();
+ bool is_string = schema_info.resolves_to_string(param_schema);
+
+ const std::string p_close = NS + "</" + param_name + ">";
+
+ auto arg = p.tool_arg(
+ p.tool_arg_open(
+ p.literal(NS + "<") +
+ p.tool_arg_name(p.literal(param_name)) +
+ p.literal(">")) +
+ (is_string
+ ? p.ac(p.tool_arg_string_value(p.until(p_close)) +
+ p.tool_arg_close(p.literal(p_close)), p_close)
+ : p.tool_arg_json_value(p.schema(p.json(),
+ "tool-" + name + "-arg-" + param_name + "-schema",
+ param_schema, false)) +
+ p.tool_arg_close(p.literal(p_close))));
+
+ auto named_arg = p.rule("tool-" + name + "-arg-" + param_name, arg);
+ if (is_required) {
+ required_parsers.push_back(named_arg);
+ } else {
+ optional_parsers.push_back(named_arg);
+ }
+ }
+
+ common_peg_parser args_seq = p.eps();
+ for (size_t i = 0; i < required_parsers.size(); i++) {
+ if (i > 0) {
+ args_seq = args_seq + p.space();
+ }
+ args_seq = args_seq + required_parsers[i];
+ }
+
+ if (!optional_parsers.empty()) {
+ common_peg_parser any_opt = p.choice();
+ for (const auto & opt : optional_parsers) {
+ any_opt |= opt;
+ }
+ args_seq = args_seq + p.repeat(p.space() + any_opt, 0, -1);
+ }
+
+ common_peg_parser invoke_body = args_seq;
+ auto func_parser = p.tool(
+ p.tool_open(p.literal(NS + "<invoke name=\"") +
+ p.tool_name(p.literal(name)) + p.literal("\">")) +
+ p.space() + invoke_body + p.space() +
+ p.tool_close(p.literal(INVOKE_END)));
+
+ tool_choice |= p.rule("tool-" + name, func_parser);
+ });
+
+ auto require_tools = inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED;
+
+ common_peg_parser tool_calls = p.eps();
+ if (inputs.parallel_tool_calls) {
+ tool_calls = p.trigger_rule("tool-call",
+ p.literal(FC_START) + p.space() + tool_choice +
+ p.zero_or_more(p.space() + tool_choice) + p.space() + p.literal(FC_END));
+ } else {
+ tool_calls = p.trigger_rule("tool-call",
+ p.literal(FC_START) + p.space() + tool_choice + p.space() + p.literal(FC_END));
+ }
+
+ if (!require_tools) {
+ tool_calls = p.optional(tool_calls);
+ }
+
+ auto content_before_tools = p.content(p.until(FC_START));
+ return generation_prompt + reasoning + content_before_tools + tool_calls + end;
+ });
+
+ data.parser = parser.save();
+
+ if (include_grammar) {
+ data.grammar_lazy = !(has_response_format || (has_tools && inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED));
+ data.grammar = build_grammar([&](const common_grammar_builder & builder) {
+ foreach_function(inputs.tools, [&](const json & tool) {
+ const auto & function = tool.at("function");
+ auto schema = function.contains("parameters") ? function.at("parameters") : json::object();
+ builder.resolve_refs(schema);
+ });
+ if (has_response_format) {
+ auto schema = inputs.json_schema;
+ builder.resolve_refs(schema);
+ }
+ parser.build_grammar(builder, data.grammar_lazy);
+ });
+
+ data.grammar_triggers = {
+ { COMMON_GRAMMAR_TRIGGER_TYPE_WORD, FC_START },
+ };
+ }
+
+ return data;
+}
+
// Cohere2 MoE (a.k.a. "North Code") parser.
//
// The assistant turn is fully marker-wrapped:
@@ -2707,6 +2892,15 @@ std::optional<common_chat_params> common_chat_try_specialized_template(
return common_chat_params_init_gigachat_v3(tmpl, params);
}
+ // MiniMax-M3: the namespace token "]<]minimax[>[" collides with the autoparser's
+ // markup delimiters, so detect the template and use a dedicated parser.
+ if (src.find("]<]minimax[>[") != std::string::npos &&
+ src.find("<tool_call>") != std::string::npos &&
+ src.find("<invoke name=") != std::string::npos) {
+ LOG_DBG("Using specialized template: MiniMax-M3\n");
+ return common_chat_params_init_minimax_m3(tmpl, params);
+ }
+
// DeepSeek V3.2/V4 format detection: template defines dsml_token and uses it for tool calls.
// The template source contains the token as a variable assignment, not as a literal in markup.
// V3.2 names the tool call block "function_calls", V4 names it "tool_calls".

View File

@@ -12,10 +12,10 @@ grep -e "flags" /proc/cpuinfo | head -1
BINARY=llama-cpp-fallback
# CPU images and x86 GPU images ship a single llama-cpp-cpu-all built with ggml
# CPU images (x86, arm64, darwin) ship a single llama-cpp-cpu-all built with ggml
# CPU_ALL_VARIANTS: ggml's backend registry dlopens the best libggml-cpu-*.so for this
# host, so no shell-side AVX probing. GPU arm64 images still ship llama-cpp-fallback
# until their builder toolchains support ggml's complete arm variant matrix.
# host, so no shell-side AVX probing. GPU images (cublas/sycl/vulkan/hipblas) ship only
# llama-cpp-fallback (the accelerator does the compute), so fall back to it when absent.
if [ -e "$CURDIR"/llama-cpp-cpu-all ]; then
BINARY=llama-cpp-cpu-all
fi
@@ -42,27 +42,6 @@ else
if [ -d "$CURDIR/lib/hipblaslt/library" ]; then
export HIPBLASLT_TENSILE_LIBPATH="$CURDIR"/lib/hipblaslt/library
fi
# Backends built for Intel GPUs carry a copy of the Intel graphics driver,
# and libze_loader is only there in those builds. Level Zero looks for a
# driver on its own, so point it at the copy that came with this backend: it
# was built against the same C library, while the machine's own driver may
# not have been, and loading that one can crash on start.
#
# Anything the user set is left alone, so a machine with a graphics card
# newer than the driver carried here can still be told to use its own.
# Nothing is said about OpenCL: no OpenCL driver is carried, so anything we
# set there would leave OpenCL worse off than the machine's own setup.
if [ -e "$CURDIR/lib/libze_loader.so.1" ]; then
if [ -e "$CURDIR/lib/libze_intel_gpu.so.1" ] && [ -z "${ZE_ENABLE_ALT_DRIVERS:-}" ]; then
export ZE_ENABLE_ALT_DRIVERS="$CURDIR"/lib/libze_intel_gpu.so.1
fi
# Ask the driver how much graphics memory is free. Without this,
# llama.cpp reads zero on an integrated graphics chip, because such a
# chip shares the system memory instead of having its own.
if [ -z "${ZES_ENABLE_SYSMAN:-}" ]; then
export ZES_ENABLE_SYSMAN=1
fi
fi
fi
# If there is a lib/ld.so, use it
@@ -76,4 +55,4 @@ echo "Using binary: $BINARY"
exec "$CURDIR"/$BINARY "$@"
# We should never reach this point, however just in case we do, run fallback
exec "$CURDIR"/llama-cpp-fallback "$@"
exec "$CURDIR"/llama-cpp-fallback "$@"

View File

@@ -1,7 +1,7 @@
# Pinned to the HEAD of feature/turboquant-kv-cache on https://github.com/TheTom/llama-cpp-turboquant.
# Auto-bumped nightly by .github/workflows/bump_deps.yaml.
TURBOQUANT_VERSION?=8a891f4b566efdbd3cea92fafee3227a0a267683
TURBOQUANT_VERSION?=c26cbdffcf6fc9b7430cd6b117757e9a3f70b7ea
LLAMA_REPO?=https://github.com/TheTom/llama-cpp-turboquant
CMAKE_ARGS?=

View File

@@ -12,11 +12,9 @@ grep -e "flags" /proc/cpuinfo | head -1
BINARY=turboquant-fallback
# CPU images and x86 GPU images ship a single turboquant-cpu-all built with ggml
# CPU_ALL_VARIANTS: ggml's
# x86/arm64 ship a single turboquant-cpu-all built with ggml CPU_ALL_VARIANTS: ggml's
# backend registry dlopens the best libggml-cpu-*.so for this host, so no shell-side
# probing. GPU arm64 images still ship turboquant-fallback until their builder toolchains
# support ggml's complete arm variant matrix.
# probing. ROCm ships only turboquant-fallback, so fall back to it when cpu-all is absent.
if [ -e "$CURDIR"/turboquant-cpu-all ]; then
BINARY=turboquant-cpu-all
fi
@@ -42,27 +40,6 @@ else
if [ -d "$CURDIR/lib/hipblaslt/library" ]; then
export HIPBLASLT_TENSILE_LIBPATH="$CURDIR"/lib/hipblaslt/library
fi
# Backends built for Intel GPUs carry a copy of the Intel graphics driver,
# and libze_loader is only there in those builds. Level Zero looks for a
# driver on its own, so point it at the copy that came with this backend: it
# was built against the same C library, while the machine's own driver may
# not have been, and loading that one can crash on start.
#
# Anything the user set is left alone, so a machine with a graphics card
# newer than the driver carried here can still be told to use its own.
# Nothing is said about OpenCL: no OpenCL driver is carried, so anything we
# set there would leave OpenCL worse off than the machine's own setup.
if [ -e "$CURDIR/lib/libze_loader.so.1" ]; then
if [ -e "$CURDIR/lib/libze_intel_gpu.so.1" ] && [ -z "${ZE_ENABLE_ALT_DRIVERS:-}" ]; then
export ZE_ENABLE_ALT_DRIVERS="$CURDIR"/lib/libze_intel_gpu.so.1
fi
# Ask the driver how much graphics memory is free. Without this, the
# backend reads zero on an integrated graphics chip, because such a chip
# shares the system memory instead of having its own.
if [ -z "${ZES_ENABLE_SYSMAN:-}" ]; then
export ZES_ENABLE_SYSMAN=1
fi
fi
fi
# If there is a lib/ld.so, use it

View File

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

View File

@@ -11,7 +11,7 @@ JOBS?=$(shell nproc --ignore=1)
# build; leaving this on `master` always picks up the latest C-API surface
# (incl. the per-detection accessor functions used by gorfdetrcpp.go).
RFDETR_REPO?=https://github.com/localai-org/rf-detr.cpp.git
RFDETR_VERSION?=98d0f381b832ef08a608b65c7dd78db066ed8b9a
RFDETR_VERSION?=65c0ffcc9a9bc9dae38252f63d0417c9845a6cf7
ifeq ($(NATIVE),false)
CMAKE_ARGS+=-DGGML_NATIVE=OFF

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?=2ca53bb45e38748d07b310eeb36245a7157ac882
WHISPER_CPP_VERSION?=4523d0ce373ee4b2176b3251fff29fd4864fcf38
SO_TARGET?=libgowhisper.so
CMAKE_ARGS+=-DBUILD_SHARED_LIBS=OFF

View File

@@ -236,8 +236,8 @@ var _ = Describe("InstallModelFromGallery with an empty base config", func() {
Expect(install(e.Name, gallery.GalleryModel{})).To(Succeed())
cfg := installedConfig(e.Name)
Expect(cfg["name"]).To(Equal(e.Name))
// The catalog's own overrides, verbatim, laid over the empty base.
Expect(cfg["parameters"]).To(Equal(e.Overrides["parameters"]))
// The catalog's model override survives inference-default enrichment.
Expect(cfg["parameters"]).To(HaveKeyWithValue("model", "LiquidAI_LFM2-1.2B-RAG-Q4_K_M.gguf"))
Expect(cfg["known_usecases"]).To(Equal(e.Overrides["known_usecases"]))
})
})

View File

@@ -3,7 +3,6 @@ package importers
import (
"encoding/json"
"path/filepath"
"slices"
"strings"
"github.com/mudler/LocalAI/core/config"
@@ -32,7 +31,7 @@ func (i *MLXImporter) Match(details Details) bool {
}
b, ok := preferencesMap["backend"].(string)
if ok && slices.Contains([]string{"mlx", "mlx-vlm", "mlx-audio"}, b) {
if ok && b == "mlx" || b == "mlx-vlm" {
return true
}
@@ -72,32 +71,19 @@ func (i *MLXImporter) Import(details Details) (gallery.ModelConfig, error) {
// (issue #10269). Send them to the mlx-vlm backend, which applies the
// processor-aware chat template.
backend := "mlx"
usecases := []string{config.UsecaseChat}
useTokenizerTemplate := true
if details.HuggingFace != nil {
switch details.HuggingFace.PipelineTag {
case "image-text-to-text":
backend = "mlx-vlm"
case "text-to-speech":
backend = "mlx-audio"
usecases = []string{config.UsecaseTTS}
useTokenizerTemplate = false
}
if details.HuggingFace != nil && details.HuggingFace.PipelineTag == "image-text-to-text" {
backend = "mlx-vlm"
}
// An explicit backend preference always wins.
b, ok := preferencesMap["backend"].(string)
if ok {
backend = b
if backend == "mlx-audio" {
usecases = []string{config.UsecaseTTS}
useTokenizerTemplate = false
}
}
modelConfig := config.ModelConfig{
Name: name,
Description: description,
KnownUsecaseStrings: usecases,
KnownUsecaseStrings: []string{config.UsecaseChat},
Backend: backend,
PredictionOptions: schema.PredictionOptions{
BasicModelRequest: schema.BasicModelRequest{
@@ -105,7 +91,7 @@ func (i *MLXImporter) Import(details Details) (gallery.ModelConfig, error) {
},
},
TemplateConfig: config.TemplateConfig{
UseTokenizerTemplate: useTokenizerTemplate,
UseTokenizerTemplate: true,
},
}

View File

@@ -48,16 +48,6 @@ var _ = Describe("MLXImporter", func() {
Expect(result).To(BeTrue())
})
It("should match when backend preference is mlx-audio", func() {
preferences := json.RawMessage(`{"backend": "mlx-audio"}`)
details := importers.Details{
URI: "https://example.com/model",
Preferences: preferences,
}
Expect(importer.Match(details)).To(BeTrue())
})
It("should not match when URI does not contain mlx-community/ and no backend preference", func() {
details := importers.Details{
URI: "https://huggingface.co/other-org/test-model",
@@ -133,21 +123,6 @@ var _ = Describe("MLXImporter", func() {
Expect(modelConfig.ConfigFile).To(ContainSubstring("backend: mlx-vlm"))
})
It("should configure explicit mlx-audio imports for text-to-speech", func() {
preferences := json.RawMessage(`{"backend": "mlx-audio"}`)
details := importers.Details{
URI: "https://huggingface.co/mlx-community/Kokoro-82M-4bit",
Preferences: preferences,
}
modelConfig, err := importer.Import(details)
Expect(err).ToNot(HaveOccurred())
Expect(modelConfig.ConfigFile).To(ContainSubstring("backend: mlx-audio"))
Expect(modelConfig.ConfigFile).To(ContainSubstring("- tts"))
Expect(modelConfig.ConfigFile).ToNot(ContainSubstring("use_tokenizer_template: true"))
})
It("should auto-route vision-language models to the mlx-vlm backend", func() {
// gemma-4 E4B and similar VLMs declare pipeline_tag
// "image-text-to-text" on HuggingFace. The text-only mlx-lm
@@ -168,23 +143,6 @@ var _ = Describe("MLXImporter", func() {
Expect(modelConfig.ConfigFile).To(ContainSubstring("backend: mlx-vlm"))
})
It("should auto-route text-to-speech models to the mlx-audio backend", func() {
details := importers.Details{
URI: "https://huggingface.co/mlx-community/Kokoro-82M-4bit",
HuggingFace: &hfapi.ModelDetails{
ModelID: "mlx-community/Kokoro-82M-4bit",
PipelineTag: "text-to-speech",
},
}
modelConfig, err := importer.Import(details)
Expect(err).ToNot(HaveOccurred())
Expect(modelConfig.ConfigFile).To(ContainSubstring("backend: mlx-audio"))
Expect(modelConfig.ConfigFile).To(ContainSubstring("- tts"))
Expect(modelConfig.ConfigFile).ToNot(ContainSubstring("use_tokenizer_template: true"))
})
It("should keep text-only models on the plain mlx backend", func() {
details := importers.Details{
URI: "https://huggingface.co/mlx-community/Llama-3.2-1B-Instruct-4bit",

View File

@@ -0,0 +1,90 @@
package gallery_test
import (
"context"
"os"
"path/filepath"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"gopkg.in/yaml.v3"
"github.com/mudler/LocalAI/core/gallery"
"github.com/mudler/LocalAI/pkg/modelartifacts"
"github.com/mudler/LocalAI/pkg/system"
)
var _ = Describe("gallery inference defaults", func() {
readPersistedConfig := func(modelsPath, name string) map[string]any {
data, err := os.ReadFile(filepath.Join(modelsPath, name+".yaml"))
Expect(err).NotTo(HaveOccurred())
persisted := map[string]any{}
Expect(yaml.Unmarshal(data, &persisted)).To(Succeed())
return persisted
}
expectNestedDefaults := func(persisted map[string]any) {
Expect(persisted).NotTo(HaveKey("temperature"))
Expect(persisted).NotTo(HaveKey("top_p"))
parameters, ok := persisted["parameters"].(map[string]any)
Expect(ok).To(BeTrue())
Expect(parameters).To(HaveKeyWithValue("temperature", 0.7))
Expect(parameters).To(HaveKeyWithValue("top_p", 0.42))
Expect(parameters).To(HaveKeyWithValue("top_k", 20))
Expect(parameters).To(HaveKeyWithValue("min_p", 0))
Expect(parameters).To(HaveKeyWithValue("repeat_penalty", 1))
Expect(parameters).To(HaveKeyWithValue("presence_penalty", 1.5))
}
It("persists defaults under parameters after artifact binding", func() {
modelsPath := GinkgoT().TempDir()
state, err := system.GetSystemState(system.WithModelPath(modelsPath))
Expect(err).NotTo(HaveOccurred())
resolved := modelartifacts.Spec{
Name: "model", Target: "model",
Source: modelartifacts.Source{Type: "huggingface", Repo: "owner/qwen3.5-model", Revision: "main"},
Resolved: &modelartifacts.Resolved{
Endpoint: "https://huggingface.co",
Revision: "0123456789abcdef0123456789abcdef01234567",
CacheKey: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
},
}
fake := &fakeArtifactMaterializer{result: modelartifacts.Result{Spec: resolved}}
definition := &gallery.ModelConfig{Name: "qwen3.5-artifact", ConfigFile: `
backend: transformers
artifacts:
- name: model
target: model
source: {type: huggingface, repo: owner/qwen3.5-model}
parameters:
model: owner/qwen3.5-model
top_p: 0.42
`}
_, err = gallery.InstallModel(context.Background(), state, "", definition, nil, nil, false,
gallery.WithArtifactMaterializer(fake))
Expect(err).NotTo(HaveOccurred())
expectNestedDefaults(readPersistedConfig(modelsPath, definition.Name))
})
It("persists defaults under parameters when the entry declares files", func() {
modelsPath := GinkgoT().TempDir()
state, err := system.GetSystemState(system.WithModelPath(modelsPath))
Expect(err).NotTo(HaveOccurred())
Expect(os.WriteFile(filepath.Join(modelsPath, "weights.gguf"), []byte("weights"), 0644)).To(Succeed())
definition := &gallery.ModelConfig{
Name: "qwen3.5-files",
ConfigFile: `
backend: llama-cpp
parameters:
model: weights.gguf
top_p: 0.42
`,
Files: []gallery.File{{Filename: "weights.gguf", URI: "https://example.com/weights.gguf"}},
}
_, err = gallery.InstallModel(context.Background(), state, "", definition, nil, nil, false)
Expect(err).NotTo(HaveOccurred())
expectNestedDefaults(readPersistedConfig(modelsPath, definition.Name))
})
})

View File

@@ -622,35 +622,41 @@ func InstallModel(ctx context.Context, systemState *system.SystemState, nameOver
lconfig.ApplyInferenceDefaults(&modelConfig, name, modelConfig.Model)
// Merge inference defaults into configMap so they are persisted without losing unknown fields.
defaults := make(map[string]any)
if modelConfig.Temperature != nil {
if _, exists := configMap["temperature"]; !exists {
configMap["temperature"] = *modelConfig.Temperature
}
defaults["temperature"] = *modelConfig.Temperature
}
if modelConfig.TopP != nil {
if _, exists := configMap["top_p"]; !exists {
configMap["top_p"] = *modelConfig.TopP
}
defaults["top_p"] = *modelConfig.TopP
}
if modelConfig.TopK != nil {
if _, exists := configMap["top_k"]; !exists {
configMap["top_k"] = *modelConfig.TopK
}
defaults["top_k"] = *modelConfig.TopK
}
if modelConfig.MinP != nil {
if _, exists := configMap["min_p"]; !exists {
configMap["min_p"] = *modelConfig.MinP
}
defaults["min_p"] = *modelConfig.MinP
}
if modelConfig.RepeatPenalty != 0 {
if _, exists := configMap["repeat_penalty"]; !exists {
configMap["repeat_penalty"] = modelConfig.RepeatPenalty
}
defaults["repeat_penalty"] = modelConfig.RepeatPenalty
}
if modelConfig.PresencePenalty != 0 {
if _, exists := configMap["presence_penalty"]; !exists {
configMap["presence_penalty"] = modelConfig.PresencePenalty
defaults["presence_penalty"] = modelConfig.PresencePenalty
}
if len(defaults) > 0 {
parameters, ok := configMap["parameters"].(map[string]any)
if !ok {
parameters = make(map[string]any)
configMap["parameters"] = parameters
}
for key, value := range defaults {
if _, exists := parameters[key]; !exists {
parameters[key] = value
}
}
}
updatedConfigYAML, err = yaml.Marshal(configMap)
if err != nil {
return nil, fmt.Errorf("failed to marshal config with inference defaults: %v", err)
}
if valid, err := modelConfig.Validate(); !valid {

View File

@@ -38,7 +38,6 @@ var knownPrefOnlyBackends = []schema.KnownBackend{
{Name: "whisperx", Modality: "asr", AutoDetect: false, Description: "WhisperX transcription (preference-only)"},
{Name: "crispasr", Modality: "asr", AutoDetect: false, Description: "CrispASR multi-architecture transcription (preference-only)"},
// TTS
{Name: "mlx-audio", Modality: "tts", AutoDetect: false, Description: "MLX-Audio text-to-speech models (auto-detected; pref-only fallback)"},
{Name: "kokoros", Modality: "tts", AutoDetect: false, Description: "Kokoros TTS (preference-only)"},
{Name: "qwen-tts", Modality: "tts", AutoDetect: false, Description: "Qwen TTS (preference-only)"},
{Name: "qwen3-tts-cpp", Modality: "tts", AutoDetect: false, Description: "Qwen3 TTS C++ (preference-only)"},

View File

@@ -152,7 +152,6 @@ var _ = Describe("Backend Endpoints", func() {
expectPrefOnly("tinygrad", "text")
expectPrefOnly("trl", "text")
expectPrefOnly("mlx-vlm", "text")
expectPrefOnly("mlx-audio", "tts")
expectPrefOnly("whisperx", "asr")
expectPrefOnly("crispasr", "asr")
expectPrefOnly("kokoros", "tts")

View File

@@ -3,7 +3,6 @@ import { test, expect } from './coverage-fixtures.js'
// Seeds two-message chat into localStorage so we don't need a live model.
async function seedChat(page, history) {
await page.addInitScript((h) => {
if (localStorage.getItem('localai_chats_data')) return
const chat = {
id: 'seed1', name: 'Seeded Chat', model: 'test-model',
history: h, systemPrompt: '', mcpMode: false, mcpServers: [],
@@ -34,56 +33,6 @@ const TWO_TURNS = [
{ role: 'assistant', content: 'second answer' },
]
test('saved message edits persist without sending a completion request', async ({ page }) => {
await mockModels(page)
let completionRequests = 0
await page.route('**/v1/chat/completions', (route) => {
completionRequests++
route.abort()
})
await seedChat(page, TWO_TURNS)
await page.goto('/app/chat')
const firstUser = page.locator('.chat-message-user').first()
await firstUser.hover()
await firstUser.getByTitle('Edit').click()
await firstUser.getByRole('textbox').fill('edited first question')
await firstUser.getByRole('button', { name: 'Save' }).click()
const firstAssistant = page.locator('.chat-message-assistant').first()
await firstAssistant.hover()
await firstAssistant.getByTitle('Edit').click()
await firstAssistant.getByRole('textbox').fill('edited first answer')
await firstAssistant.getByRole('button', { name: 'Save' }).click()
await expect(firstUser).toContainText('edited first question')
await expect(firstAssistant).toContainText('edited first answer')
await expect.poll(() => page.evaluate(() => {
const data = JSON.parse(localStorage.getItem('localai_chats_data'))
return data.chats[0].history.slice(0, 2).map(message => message.content)
})).toEqual(['edited first question', 'edited first answer'])
await page.reload()
await expect(page.locator('.chat-message-user').first()).toContainText('edited first question')
await expect(page.locator('.chat-message-assistant').first()).toContainText('edited first answer')
expect(completionRequests).toBe(0)
})
test('cancelling a message edit leaves the original content unchanged', async ({ page }) => {
await mockModels(page)
await seedChat(page, TWO_TURNS)
await page.goto('/app/chat')
const firstUser = page.locator('.chat-message-user').first()
await firstUser.hover()
await firstUser.getByTitle('Edit').click()
await firstUser.getByRole('textbox').fill('discard this draft')
await firstUser.getByRole('button', { name: 'Cancel' }).click()
await expect(firstUser).toContainText('first question')
await expect(firstUser).not.toContainText('discard this draft')
})
test('duplicate creates an independent copy and switches to it', async ({ page }) => {
await mockModels(page)
await seedChat(page, TWO_TURNS)
@@ -163,29 +112,6 @@ const FILE_TURNS = [
{ role: 'assistant', content: 'nope, that is it' },
]
test('editing a file prompt preserves its content blocks and attachment metadata', async ({ page }) => {
await mockModels(page)
await seedChat(page, FILE_TURNS)
await page.goto('/app/chat')
const firstUser = page.locator('.chat-message-user').first()
await firstUser.hover()
await firstUser.getByTitle('Edit').click()
await firstUser.getByRole('textbox').fill('edited file question')
await firstUser.getByRole('button', { name: 'Save' }).click()
await expect.poll(() => page.evaluate(() => {
const data = JSON.parse(localStorage.getItem('localai_chats_data'))
return data.chats[0].history[0]
})).toEqual({
...FILE_TURNS[0],
content: [
{ type: 'text', text: 'edited file question' },
FILE_TURNS[0].content[1],
],
})
})
test('regenerating a non-last answer in a fork still sends the uploaded file content', async ({ page }) => {
await mockModels(page)
let sentMessages = null

View File

@@ -79,30 +79,4 @@ test.describe('Traces - bounded list and on-demand detail', () => {
await expect(page.locator('text=hello from the response body')).toBeVisible()
await expect(page.locator('text=203.0.113.9').first()).toBeVisible()
})
test('keeps the expanded trace open when a refresh prepends a new row', async ({ page }) => {
await page.locator('tr', { hasText: '/v1/chat/completions' }).first().click()
await expect(page.locator('text=hello from the request body')).toBeVisible()
await page.route('**/api/traces?*', (route) => {
route.fulfill({
contentType: 'application/json',
headers: { 'X-Total-Count': '843' },
body: JSON.stringify([
{
id: '8',
request: { method: 'GET', path: '/v1/models', body: null },
response: { status: 200, body: null },
},
...LIST_BODY,
]),
})
})
await page.getByRole('button', { name: 'Refresh' }).click()
await expect(page.locator('text=hello from the request body')).toBeVisible()
const originalRow = page.locator('tr', { hasText: '/v1/chat/completions' }).first()
await expect(originalRow.locator('i.fa-chevron-down')).toBeVisible()
})
})

View File

@@ -71,10 +71,6 @@
},
"actions": {
"copy": "Kopieren",
"edit": "Bearbeiten",
"editMessage": "Nachricht bearbeiten",
"save": "Speichern",
"cancel": "Abbrechen",
"regenerate": "Neu generieren",
"jumpToLatest": "Jump to latest"
},

View File

@@ -71,10 +71,6 @@
},
"actions": {
"copy": "Copy",
"edit": "Edit",
"editMessage": "Edit message",
"save": "Save",
"cancel": "Cancel",
"regenerate": "Regenerate",
"branch": "Branch from here",
"jumpToLatest": "Jump to latest"

View File

@@ -71,10 +71,6 @@
},
"actions": {
"copy": "Copiar",
"edit": "Editar",
"editMessage": "Editar mensaje",
"save": "Guardar",
"cancel": "Cancelar",
"regenerate": "Regenerar",
"jumpToLatest": "Jump to latest"
},

View File

@@ -71,10 +71,6 @@
},
"actions": {
"copy": "Salin",
"edit": "Edit",
"editMessage": "Edit pesan",
"save": "Simpan",
"cancel": "Batal",
"regenerate": "Hasilkan ulang",
"jumpToLatest": "Lompat ke terbaru"
},

View File

@@ -71,10 +71,6 @@
},
"actions": {
"copy": "Copia",
"edit": "Modifica",
"editMessage": "Modifica messaggio",
"save": "Salva",
"cancel": "Annulla",
"regenerate": "Rigenera",
"jumpToLatest": "Torna in fondo"
},

View File

@@ -71,10 +71,6 @@
},
"actions": {
"copy": "복사",
"edit": "편집",
"editMessage": "메시지 편집",
"save": "저장",
"cancel": "취소",
"regenerate": "다시 생성",
"jumpToLatest": "Jump to latest"
},

View File

@@ -71,10 +71,6 @@
},
"actions": {
"copy": "复制",
"edit": "编辑",
"editMessage": "编辑消息",
"save": "保存",
"cancel": "取消",
"regenerate": "重新生成",
"jumpToLatest": "Jump to latest"
},

View File

@@ -3540,37 +3540,6 @@ button.collapsible-header:focus-visible {
background: var(--color-primary-light);
}
.chat-message-edit {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
min-width: min(32rem, 60vw);
}
.chat-message-edit-input {
width: 100%;
min-height: 6rem;
resize: vertical;
border: 1px solid var(--color-primary-border);
border-radius: var(--radius-md);
background: var(--color-bg-primary);
color: var(--color-text-primary);
font: inherit;
line-height: 1.5;
padding: var(--spacing-sm);
}
.chat-message-edit-input:focus {
outline: 2px solid var(--color-primary-light);
outline-offset: 1px;
}
.chat-message-edit-actions {
display: flex;
justify-content: flex-end;
gap: var(--spacing-xs);
}
.chat-message-system {
align-self: center;
max-width: 90%;

View File

@@ -287,24 +287,6 @@ function UserMessageContent({ content, files }) {
)
}
function editableMessageText(message) {
if (typeof message.content === 'string') return message.content
if (!Array.isArray(message.content)) return null
const textBlock = message.content.find(block => block?.type === 'text')
return typeof textBlock?.text === 'string' ? textBlock.text : null
}
function withEditedMessageText(message, text) {
if (typeof message.content === 'string') return { ...message, content: text }
const textIndex = message.content.findIndex(block => block?.type === 'text')
return {
...message,
content: message.content.map((block, index) =>
index === textIndex ? { ...block, text } : block
),
}
}
export default function Chat() {
const { model: urlModel } = useParams()
const { addToast } = useOutletContext()
@@ -347,8 +329,6 @@ export default function Chat() {
const [clientMCPServers, setClientMCPServers] = useState(() => loadClientMCPServers())
const [confirmDialog, setConfirmDialog] = useState(null)
const [completionGlowIdx, setCompletionGlowIdx] = useState(-1)
const [editingMessageIndex, setEditingMessageIndex] = useState(null)
const [messageEditDraft, setMessageEditDraft] = useState('')
const prevStreamingRef = useRef(false)
const {
connect: mcpConnect, disconnect: mcpDisconnect, disconnectAll: mcpDisconnectAll,
@@ -565,33 +545,6 @@ export default function Chat() {
updateChatSettings(activeChat.id, { clientMCPServers: next })
}, [activeChat, updateChatSettings])
const startMessageEdit = useCallback((index, message) => {
const text = editableMessageText(message)
if (text === null) return
setEditingMessageIndex(index)
setMessageEditDraft(text)
}, [])
const cancelMessageEdit = useCallback(() => {
setEditingMessageIndex(null)
setMessageEditDraft('')
}, [])
const saveMessageEdit = useCallback(() => {
if (!activeChat || isStreaming || editingMessageIndex === null || !messageEditDraft.trim()) return
const message = activeChat.history[editingMessageIndex]
if (!message || editableMessageText(message) === null) return
const history = activeChat.history.map((item, index) =>
index === editingMessageIndex ? withEditedMessageText(item, messageEditDraft) : item
)
updateChatSettings(activeChat.id, { history })
cancelMessageEdit()
}, [activeChat, isStreaming, editingMessageIndex, messageEditDraft, updateChatSettings, cancelMessageEdit])
useEffect(() => {
cancelMessageEdit()
}, [activeChat?.id, isStreaming, cancelMessageEdit])
// Load initial message from home page
const homeDataProcessed = useRef(false)
useEffect(() => {
@@ -1217,80 +1170,40 @@ export default function Chat() {
{msg.role === 'assistant' && activeChat.model && (
<span className="chat-message-model">{activeChat.model}</span>
)}
{editingMessageIndex === i ? (
<div className="chat-message-edit">
<textarea
autoFocus
className="chat-message-edit-input"
value={messageEditDraft}
onChange={(event) => setMessageEditDraft(event.target.value)}
onKeyDown={(event) => {
if (event.key === 'Escape') cancelMessageEdit()
}}
aria-label={t('actions.editMessage')}
/>
<div className="chat-message-edit-actions">
<button
type="button"
className="btn btn-primary btn-sm"
onClick={saveMessageEdit}
disabled={!messageEditDraft.trim()}
>
{t('actions.save')}
</button>
<button
type="button"
className="btn btn-secondary btn-sm"
onClick={cancelMessageEdit}
>
{t('actions.cancel')}
</button>
</div>
</div>
) : (
<div className="chat-message-content">
{msg.role === 'user' ? (
<UserMessageContent content={msg.content} files={msg.files} />
) : (
<div dangerouslySetInnerHTML={{
__html: canvasMode
? renderMarkdownWithArtifacts(typeof msg.content === 'string' ? msg.content : '', i)
: renderMarkdown(typeof msg.content === 'string' ? msg.content : '')
}} />
)}
</div>
)}
<div className="chat-message-content">
{msg.role === 'user' ? (
<UserMessageContent content={msg.content} files={msg.files} />
) : (
<div dangerouslySetInnerHTML={{
__html: canvasMode
? renderMarkdownWithArtifacts(typeof msg.content === 'string' ? msg.content : '', i)
: renderMarkdown(typeof msg.content === 'string' ? msg.content : '')
}} />
)}
</div>
{msg.role === 'assistant' && typeof msg.content === 'string' && msg.content.includes('Error:') && (
<a href="/app/traces?tab=backend" className="chat-error-trace-link">
<i className="fas fa-wave-square" /> {t('errors.viewTraces')}
</a>
)}
{editingMessageIndex !== i && (
<div className="chat-message-actions">
<button onClick={() => copyMessage(msg.content)} title={t('actions.copy')}>
<i className="fas fa-copy" />
<div className="chat-message-actions">
<button onClick={() => copyMessage(msg.content)} title={t('actions.copy')}>
<i className="fas fa-copy" />
</button>
{msg.role === 'assistant' && !isStreaming && (
<button onClick={() => handleRegenerate(i)} title={t('actions.regenerate')}>
<i className="fas fa-rotate" />
</button>
{(msg.role === 'user' || msg.role === 'assistant') &&
editableMessageText(msg) !== null && !isStreaming && (
<button onClick={() => startMessageEdit(i, msg)} title={t('actions.edit')}>
<i className="fas fa-pen" />
</button>
)}
{msg.role === 'assistant' && !isStreaming && (
<button onClick={() => handleRegenerate(i)} title={t('actions.regenerate')}>
<i className="fas fa-rotate" />
</button>
)}
{msg.role === 'assistant' && !isStreaming && (
<button
onClick={() => { forkChat(activeChat.id, i + 1); addToast(t('toasts.forked'), 'success', 2000) }}
title={t('actions.branch')}
>
<i className="fas fa-code-branch" />
</button>
)}
</div>
)}
)}
{msg.role === 'assistant' && !isStreaming && (
<button
onClick={() => { forkChat(activeChat.id, i + 1); addToast(t('toasts.forked'), 'success', 2000) }}
title={t('actions.branch')}
>
<i className="fas fa-code-branch" />
</button>
)}
</div>
</div>
</div>
)

View File

@@ -342,7 +342,7 @@ export default function Traces() {
const [apiCount, setApiCount] = useState(0)
const [backendCount, setBackendCount] = useState(0)
const [loading, setLoading] = useState(true)
const [expandedTraceId, setExpandedTraceId] = useState(null)
const [expandedRow, setExpandedRow] = useState(null)
// detail holds the full record for the currently expanded row, fetched on
// demand from /api/traces/:id (the list response omits the bodies).
const [detail, setDetail] = useState(null)
@@ -360,8 +360,7 @@ export default function Traces() {
duration: (a, b) => (a.duration || 0) - (b.duration || 0),
}
const toggleSort = (key) => {
setExpandedTraceId(null)
setDetail(null)
setExpandedRow(null)
setSort(s => s.key === key ? { key, dir: s.dir === 'asc' ? 'desc' : 'asc' } : { key, dir: 'asc' })
}
const sortableTh = (key, label, props = {}) => (
@@ -434,21 +433,20 @@ export default function Traces() {
useEffect(() => {
setLoading(true)
setExpandedTraceId(null)
setExpandedRow(null)
setDetail(null)
fetchTraces()
}, [fetchTraces])
// Expanding a row pulls the full record (bodies, data fields, audio
// snippets) that the list response deliberately omits.
const toggleRow = useCallback(async (row, index) => {
const traceKey = row?.id ?? index
if (expandedTraceId === traceKey) {
setExpandedTraceId(null)
const toggleRow = useCallback(async (index, row) => {
if (expandedRow === index) {
setExpandedRow(null)
setDetail(null)
return
}
setExpandedTraceId(traceKey)
setExpandedRow(index)
setDetail(null)
if (!row?.id) return
try {
@@ -459,7 +457,7 @@ export default function Traces() {
} catch {
// Fall back to the summary view; the row still renders what it has.
}
}, [expandedTraceId, activeTab])
}, [expandedRow, activeTab])
// Auto-refresh every 5 seconds
useEffect(() => {
@@ -472,7 +470,7 @@ export default function Traces() {
if (activeTab === 'api') await tracesApi.clear()
else await tracesApi.clearBackend()
setTraces([])
setExpandedTraceId(null)
setExpandedRow(null)
setDetail(null)
addToast('Traces cleared', 'success')
} catch (err) {
@@ -502,7 +500,7 @@ export default function Traces() {
}
// Reset sort + expansion when switching trace tabs (columns differ).
useEffect(() => { setSort({ key: null, dir: 'asc' }); setExpandedTraceId(null); setDetail(null) }, [activeTab])
useEffect(() => { setSort({ key: null, dir: 'asc' }); setExpandedRow(null); setDetail(null) }, [activeTab])
const sortedTraces = sort.key && TRACE_SORT[sort.key]
? [...traces].sort((a, b) => sort.dir === 'asc' ? TRACE_SORT[sort.key](a, b) : TRACE_SORT[sort.key](b, a))
@@ -637,9 +635,9 @@ export default function Traces() {
</thead>
<tbody>
{sortedTraces.map((trace, i) => (
<React.Fragment key={trace.id ?? i}>
<tr onClick={() => toggleRow(trace, i)} className="clickable">
<td><i className={`fas fa-chevron-${expandedTraceId === (trace.id ?? i) ? 'down' : 'right'} text-xs`} /></td>
<React.Fragment key={i}>
<tr onClick={() => toggleRow(i, trace)} className="clickable">
<td><i className={`fas fa-chevron-${expandedRow === i ? 'down' : 'right'} text-xs`} /></td>
<td><span className="badge badge-info">{trace.request?.method || '-'}</span></td>
<td className="text-mono text-sm">{trace.request?.path || '-'}</td>
<td className="text-sub cell-clip" title={trace.user_name || trace.user_id || ''}>{trace.user_name || trace.user_id || '-'}</td>
@@ -650,7 +648,7 @@ export default function Traces() {
: <i className="fas fa-check-circle text-success" />}
</td>
</tr>
{expandedTraceId === (trace.id ?? i) && (
{expandedRow === i && (
<tr>
<td colSpan="6" className="p-0">
<ApiTraceDetail trace={detail && detail.id === trace.id ? detail : trace} />
@@ -676,9 +674,9 @@ export default function Traces() {
</thead>
<tbody>
{sortedTraces.map((trace, i) => (
<React.Fragment key={trace.id ?? i}>
<tr onClick={() => toggleRow(trace, i)} className="clickable">
<td><i className={`fas fa-chevron-${expandedTraceId === (trace.id ?? i) ? 'down' : 'right'} text-xs`} /></td>
<React.Fragment key={i}>
<tr onClick={() => toggleRow(i, trace)} className="clickable">
<td><i className={`fas fa-chevron-${expandedRow === i ? 'down' : 'right'} text-xs`} /></td>
<td><span style={typeBadgeStyle(trace.type)}>{trace.type || '-'}</span></td>
<td className="text-sub nowrap">{formatDateTime(trace.timestamp)}</td>
<td className="text-mono text-sm">{trace.model_name || '-'}</td>
@@ -692,7 +690,7 @@ export default function Traces() {
: <i className="fas fa-check-circle text-success" />}
</td>
</tr>
{expandedTraceId === (trace.id ?? i) && (
{expandedRow === i && (
<tr>
<td colSpan="7" className="p-0">
<BackendTraceDetail trace={detail && detail.id === trace.id ? detail : trace} />

View File

@@ -27,17 +27,9 @@ Building and running the site locally requires a recent `extended` version of [H
You can find out more about how to install Hugo for your environment in our
[Getting started](https://www.docsy.dev/docs/getting-started/#prerequisites-and-installation) guide.
From the LocalAI repository root, run:
Once you've made your working copy of the site repo, from the repo root folder, run:
```bash
make docs
```
The Hugo configuration lives in the `docs` directory. To invoke Hugo
directly instead, run:
```bash
cd docs
hugo server
```

View File

@@ -329,23 +329,7 @@ This configuration has been tested on a 'custom' cluster managed by SUSE Rancher
### Requirements
You need a machine with an Intel GPU and a kernel that drives it, which every current Linux kernel does. You do not need to install any Intel graphics packages: the backends carry their own copy of the Intel graphics driver, so they work on a machine that has none installed, and on a machine whose own driver was built against a newer C library than the backend.
If you build from source instead of using the images, you need the [Intel oneAPI Base Toolkit](https://software.intel.com/content/www/us/en/develop/tools/oneapi/base-toolkit/download.html).
#### Using your own Intel driver instead
The carried driver comes from Intel's own package repository, so it knows the cards released up to the point the image was built. If your GPU is newer than that, or you would rather use the driver your distribution ships, point the backend at it:
```bash
docker run --rm -ti --device /dev/dri -p 8080:8080 \
-e ZE_ENABLE_ALT_DRIVERS=/usr/lib/x86_64-linux-gnu/libze_intel_gpu.so.1 \
-v $PWD/models:/models quay.io/go-skynet/local-ai:{{< version >}}-gpu-intel
```
Set the path to wherever your distribution keeps that file. Whatever you set is used as is, and the carried driver is left alone.
The backends carry only the driver Level Zero uses, which is how llama.cpp reaches an Intel GPU. They do not carry an OpenCL driver, so OpenCL inside a container continues to use whatever the image itself provides.
If building from source, you need to install [Intel oneAPI Base Toolkit](https://software.intel.com/content/www/us/en/develop/tools/oneapi/base-toolkit/download.html) and have the Intel drivers available in the system.
### Container images
@@ -371,8 +355,6 @@ docker run --rm -ti --device /dev/dri -p 8080:8080 -e DEBUG=true -e MODELS_PATH=
Note also that sycl does have a known issue to hang with `mmap: true`. You have to disable it in the model configuration if explicitly enabled.
On an integrated Intel GPU, the amount of free graphics memory can only be read if the driver is asked to report it. The backends do that for you by setting `ZES_ENABLE_SYSMAN=1`. If you set that variable yourself, your value is kept, and setting it to `0` makes the backend read zero free memory, because an integrated GPU has no memory of its own and shares the system's.
## Vulkan acceleration
### Requirements
@@ -474,7 +456,7 @@ sycl-ls
- **NVIDIA**: Ensure `nvidia-container-toolkit` is installed and the Docker runtime is configured. Test with `docker run --rm --gpus all nvidia/cuda:12.8.0-base-ubuntu24.04 nvidia-smi`.
- **AMD**: Ensure `/dev/dri` and `/dev/kfd` are passed to the container and that `amdgpu-dkms` is installed on the host.
- **Intel**: Ensure `/dev/dri` is passed to the container. No Intel graphics packages are needed on the host, since the backends bring their own driver. If the GPU is a recent model that the carried driver does not know, point the backend at the host's own driver as shown in [Intel acceleration](#intel-acceleration-sycl).
- **Intel**: Ensure `/dev/dri` is passed to the container and Intel GPU drivers are installed on the host.
### Model loads on CPU instead of GPU

View File

@@ -80,12 +80,6 @@ The WebUI provides a powerful model import interface that supports both simple a
- Custom preferences
5. Click "Import Model" to start the import process
Repositories under `mlx-community` are imported with the native MLX backend.
LocalAI uses Hugging Face's pipeline metadata to select `mlx-vlm` for
vision-language models and `mlx-audio` for text-to-speech models; other MLX
repositories use `mlx`. An explicit backend selection in the import form always
overrides this automatic routing.
### Advanced Import Mode
For full control over model configuration:

View File

@@ -63,10 +63,6 @@ To get your first chat working:
1. Open the **Models** page and search for `qwen3-4b`. Click **Install** on the `qwen3-4b` entry and wait for the download to finish. (`qwen3-4b` is a small, CPU-friendly Qwen3 model that also supports tool calling, so you can reuse it later in the [Build your first agent]({{% relref "getting-started/first-agent" %}}) walkthrough.)
2. Open the **Chat** page, select `qwen3-4b` from the model dropdown, type a message, and send it. You should get a reply within a few seconds.
To correct an earlier prompt or response without running the model again, hover
over the saved message and select **Edit**. **Save** updates that conversation's
local history; **Cancel** discards the draft.
### Downloading models from the CLI
When starting LocalAI (either via Docker or via CLI) you can specify as argument a list of models to install automatically before starting the API, for example:

View File

@@ -1,48 +1,4 @@
---
- name: "deepseek-v4-flash-0731"
url: "github:mudler/LocalAI/gallery/virtual.yaml@master"
urls:
- https://huggingface.co/unsloth/DeepSeek-V4-Flash-0731-GGUF
description: |
# DeepSeek-V4-Flash-0731
Technical Report👁
## Introduction
**DeepSeek-V4-Flash-0731** is the official release of **DeepSeek-V4-Flash**, superseding the preview version, with substantially enhanced agentic capabilities. It has the same model structure as DeepSeek-V4-Flash-DSpark, i.e. it comes with a speculative decoding module attached.
DeepSeek-V4-Flash-0731 outperforms DeepSeek-V4-Pro (Preview) on benchmarks listed below despite its far smaller activated parameter count, and is broadly competitive with the strongest proprietary models available.
Notes:
1. For the Code Agent tasks among the public benchmarks above, DeepSeek-V4-Flash-0731 is evaluated with the minimal mode of DeepSeek Harness (to be released) as the agent framework, using the `max` reasoning effort level with `temperature = 1.0, top_p = 0.95`.
2. † DSBench-FullStack is an internal full-stack development test set; DSBench-Hard is an internal test set of difficult coding-agent problems.
## Chat Template
...
license: "mit"
tags:
- llm
- gguf
- deepseek
icon: https://github.com/deepseek-ai/DeepSeek-V2/blob/main/figures/logo.svg
overrides:
backend: ds4
function:
grammar:
disable: true
known_usecases:
- chat
parameters:
model: ds4flash.gguf
template:
use_tokenizer_template: true
files:
- filename: ds4flash.gguf
sha256: ""
uri: https://huggingface.co/unsloth/DeepSeek-V4-Flash-0731-GGUF
- name: "parable-granite-4.1-3b-claude-fable-5"
url: "github:mudler/LocalAI/gallery/virtual.yaml@master"
urls:
@@ -114,7 +70,7 @@
files:
- filename: llama-cpp/models/Parable-Qwen3-4B-Claude-Fable-5-Q4_K_M/Parable-Qwen3-4B-Claude-Fable-5-GGUF-Q4_K_M.gguf
uri: https://huggingface.co/AnkitAI/Parable-Qwen3-4B-Claude-Fable-5-GGUF/resolve/main/Parable-Qwen3-4B-Claude-Fable-5-GGUF-Q4_K_M.gguf
sha256: c94b06a912aa901f3da5689754577ad534415efafc50dcee3f389594a153bf38
sha256: c6f991bd243fd1449d50a58a8de7d26bcad35d908bc0193e5a2c2c56ac6b8d5f
- name: "parable-granite-4.1-8b-claude-fable-5"
url: "github:mudler/LocalAI/gallery/virtual.yaml@master"
urls:
@@ -941,90 +897,6 @@
- filename: llama-cpp/mmproj/inkling-UD-Q4_K_XL/mmproj-BF16.gguf
sha256: 662c925e1df293cfba16ffd6bd53dac31d3c73160ba65dff7270d7a70f351e91
uri: https://huggingface.co/unsloth/inkling-GGUF/resolve/main/mmproj-BF16.gguf
- &inkling-small
name: "inkling-small"
url: "github:mudler/LocalAI/gallery/virtual.yaml@master"
urls:
- https://huggingface.co/thinkingmachines/Inkling-Small
- https://huggingface.co/unsloth/Inkling-Small-GGUF
description: |
Inkling Small is a 276B-parameter mixture-of-experts multimodal model with 12B active parameters for text, image, and audio understanding, instruction following, coding, and tool use. This entry uses the Q4_K_M GGUF quantization, whose five language-model shards total approximately 162.5 GB.
license: "apache-2.0"
tags:
- llm
- gguf
- vision
- audio
- multimodal
overrides:
backend: llama-cpp
function:
automatic_tool_parsing_fallback: true
grammar:
disable: true
known_usecases:
- chat
mmproj: llama-cpp/mmproj/Inkling-Small-UD-Q4_K_M/mmproj-BF16.gguf
options:
- use_jinja:true
parameters:
model: llama-cpp/models/Inkling-Small-UD-Q4_K_M/Inkling-Small-UD-Q4_K_M-00001-of-00005.gguf
template:
use_tokenizer_template: true
files:
- filename: llama-cpp/models/Inkling-Small-UD-Q4_K_M/Inkling-Small-UD-Q4_K_M-00001-of-00005.gguf
sha256: a51ac3f439198f2817219edd582be4b600c273be24e78cbd58ebff982d9f007e
uri: https://huggingface.co/unsloth/Inkling-Small-GGUF/resolve/main/UD-Q4_K_M/Inkling-Small-UD-Q4_K_M-00001-of-00005.gguf
- filename: llama-cpp/models/Inkling-Small-UD-Q4_K_M/Inkling-Small-UD-Q4_K_M-00002-of-00005.gguf
sha256: 3dccdd473cc3a191e6028f6105b01831ed3a8dc30ec4e02679f9e0c2ccb59671
uri: https://huggingface.co/unsloth/Inkling-Small-GGUF/resolve/main/UD-Q4_K_M/Inkling-Small-UD-Q4_K_M-00002-of-00005.gguf
- filename: llama-cpp/models/Inkling-Small-UD-Q4_K_M/Inkling-Small-UD-Q4_K_M-00003-of-00005.gguf
sha256: 1a7edf29bda1d278b4668e1a082d7db634b845a53ea58c918a8cea1f9006c21c
uri: https://huggingface.co/unsloth/Inkling-Small-GGUF/resolve/main/UD-Q4_K_M/Inkling-Small-UD-Q4_K_M-00003-of-00005.gguf
- filename: llama-cpp/models/Inkling-Small-UD-Q4_K_M/Inkling-Small-UD-Q4_K_M-00004-of-00005.gguf
sha256: 376f67568438da96b10566730e8a9e17e3f665ab1b9d82eba48ec33708b172f7
uri: https://huggingface.co/unsloth/Inkling-Small-GGUF/resolve/main/UD-Q4_K_M/Inkling-Small-UD-Q4_K_M-00004-of-00005.gguf
- filename: llama-cpp/models/Inkling-Small-UD-Q4_K_M/Inkling-Small-UD-Q4_K_M-00005-of-00005.gguf
sha256: e34364af0d04d2d295bc374f1a4fa80002e4277db1bcf4b56240dbd443ac21d3
uri: https://huggingface.co/unsloth/Inkling-Small-GGUF/resolve/main/UD-Q4_K_M/Inkling-Small-UD-Q4_K_M-00005-of-00005.gguf
- filename: llama-cpp/mmproj/Inkling-Small-UD-Q4_K_M/mmproj-BF16.gguf
sha256: 05d4475a956030be87b099865d6552a541a476db8cc3e266fcfa7c5a24846248
uri: https://huggingface.co/unsloth/Inkling-Small-GGUF/resolve/main/mmproj-BF16.gguf
variants:
- model: inkling-small-iq2-m
- !!merge <<: *inkling-small
name: "inkling-small-iq2-m"
description: |
Inkling Small is a 276B-parameter mixture-of-experts multimodal model with 12B active parameters for text, image, and audio understanding, instruction following, coding, and tool use. This entry uses the IQ2_M GGUF quantization, whose three language-model shards total approximately 82.4 GB.
overrides:
backend: llama-cpp
function:
automatic_tool_parsing_fallback: true
grammar:
disable: true
known_usecases:
- chat
mmproj: llama-cpp/mmproj/Inkling-Small-UD-IQ2_M/mmproj-BF16.gguf
options:
- use_jinja:true
parameters:
model: llama-cpp/models/Inkling-Small-UD-IQ2_M/Inkling-Small-UD-IQ2_M-00001-of-00003.gguf
template:
use_tokenizer_template: true
files:
- filename: llama-cpp/models/Inkling-Small-UD-IQ2_M/Inkling-Small-UD-IQ2_M-00001-of-00003.gguf
sha256: 3b6ace30e488ad26e816cdba4e42714f40110a3142a210bd5c2e48f69e27cb31
uri: https://huggingface.co/unsloth/Inkling-Small-GGUF/resolve/main/UD-IQ2_M/Inkling-Small-UD-IQ2_M-00001-of-00003.gguf
- filename: llama-cpp/models/Inkling-Small-UD-IQ2_M/Inkling-Small-UD-IQ2_M-00002-of-00003.gguf
sha256: 5ca94e858ae116eb513a2af1facd35844d42ef4a209e1d85cc4ecc73cd21b894
uri: https://huggingface.co/unsloth/Inkling-Small-GGUF/resolve/main/UD-IQ2_M/Inkling-Small-UD-IQ2_M-00002-of-00003.gguf
- filename: llama-cpp/models/Inkling-Small-UD-IQ2_M/Inkling-Small-UD-IQ2_M-00003-of-00003.gguf
sha256: 8a84e00d4625d52491969f88f24f0999bc77527b7fa1d15cbe44ef88235bb377
uri: https://huggingface.co/unsloth/Inkling-Small-GGUF/resolve/main/UD-IQ2_M/Inkling-Small-UD-IQ2_M-00003-of-00003.gguf
- filename: llama-cpp/mmproj/Inkling-Small-UD-IQ2_M/mmproj-BF16.gguf
sha256: 05d4475a956030be87b099865d6552a541a476db8cc3e266fcfa7c5a24846248
uri: https://huggingface.co/unsloth/Inkling-Small-GGUF/resolve/main/mmproj-BF16.gguf
variants: []
- name: "qwythos-9b-v2"
url: "github:mudler/LocalAI/gallery/virtual.yaml@master"
urls:
@@ -1124,74 +996,6 @@
- filename: llama-cpp/mmproj/Qwen3.6-27B-Fable-Fus-711-UnHeretic-NM-DAU-NEO-MAX-NEO-MTP-Q4_K_M/mmproj-F32.gguf
sha256: fdc443e974cad1f61c45af1cfd5580855855ddce0d6c14cc500a5714c486ac1d
uri: https://huggingface.co/DavidAU/Qwen3.6-27B-Fable-Fusion-711-Uncensored-Heretic-NM-DAU-NEO-MAX-MTP-GGUF/resolve/main/mmproj-F32.gguf
- &qwopus3-6-27b-fusion
name: "qwopus3.6-27b-fusion"
variants:
- model: qwopus3.6-27b-fusion-q8
url: "github:mudler/LocalAI/gallery/virtual.yaml@master"
urls:
- https://huggingface.co/Qwen/Qwen3.6-27B
- https://huggingface.co/KyleHessling1/Qwopus3.6-27B-Fusion-GGUF
description: |
Qwopus3.6-27B Fusion is an experimental 27B Qwen3.6 merge that combines the
Qwopus reasoning and coding fine-tunes. It targets agentic coding and
general reasoning, supports the Qwen3.6 262K context window, and retains
the base model's MTP head. This default entry uses the 16.8 GB Q4_K_M GGUF.
license: qwen
tags:
- llm
- gguf
- reasoning
- thinking
- mtp
overrides:
backend: llama-cpp
function:
automatic_tool_parsing_fallback: true
grammar:
disable: true
known_usecases:
- chat
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-Fusion-Q4_K_M/Qwopus3.6-27B-Fusion-Q4_K_M.gguf
template:
use_tokenizer_template: true
files:
- filename: llama-cpp/models/Qwopus3.6-27B-Fusion-Q4_K_M/Qwopus3.6-27B-Fusion-Q4_K_M.gguf
sha256: 6d4e6e9ffba84ffe5166969303d4918f3a31682d75862416827412cee8f20d5d
uri: huggingface://KyleHessling1/Qwopus3.6-27B-Fusion-GGUF/Qwopus3.6-27B-Fusion-Q4_K_M.gguf
- !!merge <<: *qwopus3-6-27b-fusion
name: "qwopus3.6-27b-fusion-q8"
variants: null
description: |
Qwopus3.6-27B Fusion in the higher-fidelity 29.0 GB Q8_0 GGUF format.
It retains the base model's MTP head for speculative decoding.
overrides:
backend: llama-cpp
function:
automatic_tool_parsing_fallback: true
grammar:
disable: true
known_usecases:
- chat
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-Fusion-Q8_0/Qwopus3.6-27B-Fusion-Q8_0.gguf
template:
use_tokenizer_template: true
files:
- filename: llama-cpp/models/Qwopus3.6-27B-Fusion-Q8_0/Qwopus3.6-27B-Fusion-Q8_0.gguf
sha256: 5594e1776b75beedf4a54b933bba386dc83a0883417e3bcd9ef53fdfd120d5b6
uri: huggingface://KyleHessling1/Qwopus3.6-27B-Fusion-GGUF/Qwopus3.6-27B-Fusion-Q8_0.gguf
- name: "minicpm5-1b-claude-opus-fable5-v2-thinking"
url: "github:mudler/LocalAI/gallery/virtual.yaml@master"
urls:
@@ -1605,7 +1409,7 @@
files:
- filename: ds4flash.gguf
uri: https://huggingface.co/unsloth/DeepSeek-V4-Flash-GGUF
sha256: 7d6d1691bc2d02c5a8194afb8bd9b57519343afebf020231c61d9236504b4a5c
sha256: ea2be54e4e989cb8cc2a88c0791b15eaa50eb97fb46a89843f6415d7e1a73e33
- name: "qwopus3.6-35b-a3b-coder-mtp"
url: "github:mudler/LocalAI/gallery/virtual.yaml@master"
urls:
@@ -5390,82 +5194,6 @@
- filename: llama-cpp/models/Qwen3.5-35B-A3B-APEX-GGUF/Qwen3.5-35B-A3B-APEX-Quality.gguf
sha256: 50887b60c77ee5c95bc3657814ae993abcab7b2d71868b9af1e84d6badd09a57
uri: https://huggingface.co/mudler/Qwen3.5-35B-A3B-APEX-GGUF/resolve/main/Qwen3.5-35B-A3B-APEX-Quality.gguf
- &fara1-5-9b
name: fara1.5-9b
url: github:mudler/LocalAI/gallery/virtual.yaml@master
variants:
- model: fara1.5-9b-q8
urls:
- https://huggingface.co/microsoft/Fara1.5-9B
- https://huggingface.co/bartowski/Fara1.5-9B-GGUF
description: |
Fara1.5-9B is Microsoft's 9B-parameter multimodal computer-use agent for web browsers, fine-tuned from Qwen3.5-9B. It accepts screenshots and text, emits structured browser actions, supports a 262K-token context, and should be deployed with appropriate sandboxing and user-confirmation controls. This entry uses the recommended Q4_K_M GGUF quantization.
license: mit
tags:
- fara
- qwen
- qwen3.5
- 9b
- llm
- gguf
- quantized
- chat
- vision
- multimodal
- agent
- computer-use
- gpu
- cpu
last_checked: "2026-08-01"
overrides:
backend: llama-cpp
function:
grammar:
disable: true
known_usecases:
- chat
- vision
mmproj: llama-cpp/mmproj/Fara1.5-9B-GGUF/mmproj-Fara1.5-9B-f16.gguf
options:
- use_jinja:true
parameters:
model: llama-cpp/models/Fara1.5-9B-GGUF/Fara1.5-9B-Q4_K_M.gguf
template:
use_tokenizer_template: true
files:
- filename: llama-cpp/models/Fara1.5-9B-GGUF/Fara1.5-9B-Q4_K_M.gguf
sha256: a02e7220337b87290bca7ef7225ba4afa7104efa05c0851de6c29ec9c5d04c7d
uri: huggingface://bartowski/Fara1.5-9B-GGUF/Fara1.5-9B-Q4_K_M.gguf
- filename: llama-cpp/mmproj/Fara1.5-9B-GGUF/mmproj-Fara1.5-9B-f16.gguf
sha256: 97b423c81719ffc367124a9739d6feb6f62d62f60869a6d385a701b963ce1906
uri: huggingface://bartowski/Fara1.5-9B-GGUF/mmproj-Fara1.5-9B-f16.gguf
- !!merge <<: *fara1-5-9b
name: fara1.5-9b-q8
variants: []
description: |
Fara1.5-9B is Microsoft's 9B-parameter multimodal computer-use agent for web browsers, fine-tuned from Qwen3.5-9B. It accepts screenshots and text, emits structured browser actions, supports a 262K-token context, and should be deployed with appropriate sandboxing and user-confirmation controls. This entry uses the higher-quality Q8_0 GGUF quantization.
overrides:
backend: llama-cpp
function:
grammar:
disable: true
known_usecases:
- chat
- vision
mmproj: llama-cpp/mmproj/Fara1.5-9B-GGUF/mmproj-Fara1.5-9B-f16.gguf
options:
- use_jinja:true
parameters:
model: llama-cpp/models/Fara1.5-9B-GGUF/Fara1.5-9B-Q8_0.gguf
template:
use_tokenizer_template: true
files:
- filename: llama-cpp/models/Fara1.5-9B-GGUF/Fara1.5-9B-Q8_0.gguf
sha256: a2e30cca7aec006266308153ae781347505af16baa514bbd4e0e3f4a79ea3a22
uri: huggingface://bartowski/Fara1.5-9B-GGUF/Fara1.5-9B-Q8_0.gguf
- filename: llama-cpp/mmproj/Fara1.5-9B-GGUF/mmproj-Fara1.5-9B-f16.gguf
sha256: 97b423c81719ffc367124a9739d6feb6f62d62f60869a6d385a701b963ce1906
uri: huggingface://bartowski/Fara1.5-9B-GGUF/mmproj-Fara1.5-9B-f16.gguf
- name: fara1.5-27b
url: github:mudler/LocalAI/gallery/virtual.yaml@master
variants:
@@ -6214,14 +5942,14 @@
- instruction-tuned
- code
- math
last_checked: "2026-08-01"
last_checked: "2026-07-28"
overrides:
parameters:
model: Nanbeige4.2-3B-Q8_0.gguf
model: nanbeige4.2-3b-Q8_0.gguf
files:
- filename: Nanbeige4.2-3B-Q8_0.gguf
sha256: 4f8bd17cdf58bea2a94aef03457e0b8f019c26fe4daee7ae49b61bfa935a9126
uri: huggingface://owao/Nanbeige4.2-3B-GGUF/Nanbeige4.2-3B-Q8_0.gguf
- filename: nanbeige4.2-3b-Q8_0.gguf
sha256: 44707bb25e7ba3f2b0b5f3c2311da95ee3676986dd2014ce2aaeb14113590e33
uri: huggingface://owao/Nanbeige4.2-3B-GGUF/nanbeige4.2-3b-Q8_0.gguf
- name: nanbeige4.2-3b
url: github:mudler/LocalAI/gallery/nanbeige4.2.yaml@master
urls:
@@ -6244,16 +5972,16 @@
- instruction-tuned
- code
- math
last_checked: "2026-08-01"
last_checked: "2026-07-28"
variants:
- model: nanbeige4.2-3b-q8
overrides:
parameters:
model: Nanbeige4.2-3B-Q4_K_M.gguf
model: nanbeige4.2-3b-Q4_K_M.gguf
files:
- filename: Nanbeige4.2-3B-Q4_K_M.gguf
sha256: ffe1b9b8ee95ec4b962c379905aa8be6f72ae9c4645c6c70e3b6ff7b197e6ef4
uri: huggingface://owao/Nanbeige4.2-3B-GGUF/Nanbeige4.2-3B-Q4_K_M.gguf
- filename: nanbeige4.2-3b-Q4_K_M.gguf
sha256: 9ffd17d14472ff208409b3f51a6d87a5e5ec1b878b9a6f4dfe15c2a883366104
uri: huggingface://owao/Nanbeige4.2-3B-GGUF/nanbeige4.2-3b-Q4_K_M.gguf
- name: nemo-parakeet-tdt-0.6b
url: github:mudler/LocalAI/gallery/virtual.yaml@master
urls:
@@ -13368,8 +13096,8 @@
model: rfdetr-seg-medium-f16.gguf
files:
- filename: rfdetr-seg-medium-f16.gguf
sha256: 885d85ed6935495fc50ff464e06b6ea3bd8e8386865852d68a8be0f649d65afe
uri: huggingface://mudler/rfdetr-cpp-seg-medium/rfdetr-seg-medium-f16.gguf
sha256: dd7c8da7cf0a2e64a1002f5ff66d7fede45b00e612457f249bcd9d4a0c122566
- name: rfdetr-cpp-seg-large
url: github:mudler/LocalAI/gallery/virtual.yaml@master
urls:
@@ -13397,8 +13125,8 @@
model: rfdetr-seg-large-f16.gguf
files:
- filename: rfdetr-seg-large-f16.gguf
sha256: 90423066d0791b4ae249f3986cce1f095a1e4090bf46800bf7f9e371ea80d559
uri: huggingface://mudler/rfdetr-cpp-seg-large/rfdetr-seg-large-f16.gguf
sha256: ffc631b8e6115b11bdbb8e876c77aaa3e3e5d8c41c00ce8417ebbf183b1e6404
- name: rfdetr-cpp-seg-xlarge
url: github:mudler/LocalAI/gallery/virtual.yaml@master
urls:
@@ -13426,8 +13154,8 @@
model: rfdetr-seg-xlarge-f16.gguf
files:
- filename: rfdetr-seg-xlarge-f16.gguf
sha256: 0b82de4a6e65a40bc930979a1a4281cb24de35203d30eeefd797c858101a7bec
uri: huggingface://mudler/rfdetr-cpp-seg-xlarge/rfdetr-seg-xlarge-f16.gguf
sha256: 72b6210e255ebb89a8d471c06a6f4b5307205b1a34b9e4292de90d1488da4e26
- name: rfdetr-cpp-seg-2xlarge
url: github:mudler/LocalAI/gallery/virtual.yaml@master
urls:
@@ -13456,8 +13184,8 @@
model: rfdetr-seg-2xlarge-f16.gguf
files:
- filename: rfdetr-seg-2xlarge-f16.gguf
sha256: 7f957997db23e844194ea8266a95b4adc3deb6d0b71c0924922b20fbdeafa299
uri: huggingface://mudler/rfdetr-cpp-seg-2xlarge/rfdetr-seg-2xlarge-f16.gguf
sha256: 00f3988bdf9a382b06610c200b3938b65a7731d14eafa84b73f6c3b5be4af8d9
- name: edgetam
url: github:mudler/LocalAI/gallery/virtual.yaml@master
urls:

View File

@@ -70,11 +70,6 @@ func ExtractArchive(archive, dst string) error {
if f.FileInfo.Mode()&os.ModeSymlink != 0 {
return fmt.Errorf("archive contains a symlink")
}
if linkname, ok := archiveMemberLinkname(f); ok {
if err := validateArchiveMemberPath(extractRoot, linkname); err != nil {
return err
}
}
return nil
})
@@ -100,18 +95,6 @@ func archiveMemberName(f archiver.File) string {
}
}
// archiveMemberLinkname reports the target of a tar hardlink member, which carries a regular file mode and so is not caught by the symlink check.
func archiveMemberLinkname(f archiver.File) (string, bool) {
switch h := f.Header.(type) {
case tar.Header:
return h.Linkname, h.Typeflag == tar.TypeLink
case *tar.Header:
return h.Linkname, h.Typeflag == tar.TypeLink
default:
return "", false
}
}
func validateArchiveMemberPath(root, name string) error {
if name == "" {
return fmt.Errorf("archive contains an empty path")

View File

@@ -3,7 +3,6 @@ package utils_test
import (
"archive/tar"
"archive/zip"
"compress/gzip"
"os"
"path/filepath"
@@ -60,53 +59,6 @@ var _ = Describe("utils/archive tests", func() {
Expect(err.Error()).To(ContainSubstring("unsafe path"))
Expect(filepath.Join(tmpDir, "escaped.txt")).ToNot(BeAnExistingFile())
})
It("rejects tar hardlinks that overwrite a file outside the destination", func() {
tmpDir := GinkgoT().TempDir()
archivePath := filepath.Join(tmpDir, "model.tar.gz")
extractPath := filepath.Join(tmpDir, "models")
outsidePath := filepath.Join(tmpDir, "outside.txt")
Expect(os.WriteFile(outsidePath, []byte("original"), 0o600)).To(Succeed())
Expect(writeTarGzArchiveWithHardlinkedFile(archivePath, "payload.bin", "../outside.txt", "overwritten")).To(Succeed())
err := ExtractArchive(archivePath, extractPath)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("unsafe path"))
contents, readErr := os.ReadFile(outsidePath)
Expect(readErr).ToNot(HaveOccurred())
Expect(string(contents)).To(Equal("original"))
})
It("extracts tar hardlinks that stay inside the destination", func() {
tmpDir := GinkgoT().TempDir()
archivePath := filepath.Join(tmpDir, "model.tar.gz")
extractPath := filepath.Join(tmpDir, "models")
Expect(writeTarGzArchiveWithInternalHardlink(archivePath, "model.bin", "alias.bin", "weights")).To(Succeed())
Expect(ExtractArchive(archivePath, extractPath)).To(Succeed())
extracted, err := os.ReadFile(filepath.Join(extractPath, "alias.bin"))
Expect(err).ToNot(HaveOccurred())
Expect(string(extracted)).To(Equal("weights"))
})
It("rejects tar hardlinks that point outside the destination", func() {
tmpDir := GinkgoT().TempDir()
archivePath := filepath.Join(tmpDir, "model.tar")
extractPath := filepath.Join(tmpDir, "models")
Expect(writeTarArchiveWithHardlink(archivePath, "payload.bin", "../escaped.txt")).To(Succeed())
err := ExtractArchive(archivePath, extractPath)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("unsafe path"))
Expect(filepath.Join(tmpDir, "escaped.txt")).ToNot(BeAnExistingFile())
})
})
func writeZipArchive(path string, files map[string]string) (err error) {
@@ -174,121 +126,3 @@ func writeTarArchive(path string, files map[string]string) (err error) {
return nil
}
func writeTarArchiveWithHardlink(path, name, linkname string) (err error) {
out, err := os.Create(path)
if err != nil {
return err
}
defer func() {
if closeErr := out.Close(); err == nil {
err = closeErr
}
}()
writer := tar.NewWriter(out)
defer func() {
if closeErr := writer.Close(); err == nil {
err = closeErr
}
}()
return writer.WriteHeader(&tar.Header{
Name: name,
Linkname: linkname,
Typeflag: tar.TypeLink,
Mode: 0o600,
})
}
func writeTarGzArchiveWithHardlinkedFile(path, name, linkname, contents string) (err error) {
out, err := os.Create(path)
if err != nil {
return err
}
defer func() {
if closeErr := out.Close(); err == nil {
err = closeErr
}
}()
compressor := gzip.NewWriter(out)
defer func() {
if closeErr := compressor.Close(); err == nil {
err = closeErr
}
}()
writer := tar.NewWriter(compressor)
defer func() {
if closeErr := writer.Close(); err == nil {
err = closeErr
}
}()
if err := writer.WriteHeader(&tar.Header{
Name: name,
Linkname: linkname,
Typeflag: tar.TypeLink,
Mode: 0o600,
}); err != nil {
return err
}
data := []byte(contents)
if err := writer.WriteHeader(&tar.Header{
Name: name,
Mode: 0o600,
Size: int64(len(data)),
}); err != nil {
return err
}
_, err = writer.Write(data)
return err
}
func writeTarGzArchiveWithInternalHardlink(path, targetName, linkName, contents string) (err error) {
out, err := os.Create(path)
if err != nil {
return err
}
defer func() {
if closeErr := out.Close(); err == nil {
err = closeErr
}
}()
compressor := gzip.NewWriter(out)
defer func() {
if closeErr := compressor.Close(); err == nil {
err = closeErr
}
}()
writer := tar.NewWriter(compressor)
defer func() {
if closeErr := writer.Close(); err == nil {
err = closeErr
}
}()
data := []byte(contents)
if err := writer.WriteHeader(&tar.Header{
Name: targetName,
Mode: 0o600,
Size: int64(len(data)),
}); err != nil {
return err
}
if _, err := writer.Write(data); err != nil {
return err
}
return writer.WriteHeader(&tar.Header{
Name: linkName,
Linkname: targetName,
Typeflag: tar.TypeLink,
Mode: 0o600,
})
}

View File

@@ -1,139 +0,0 @@
#!/bin/bash
# Checks how the run.sh of each C++ backend sets up the Intel graphics driver.
#
# A backend built for Intel GPUs carries its own copy of the Intel graphics
# driver. run.sh has to tell Level Zero, which is how llama.cpp reaches the
# card, to use that copy. Three things must hold, and all three have broken in
# the past:
#
# 1. If the user already chose a driver, keep the user's choice. Otherwise a
# machine with a graphics card too new for the carried driver stops
# working, with no way to get back to the driver that did work.
# 2. Say nothing about OpenCL. No OpenCL driver is carried, so pointing
# OpenCL at the backend's own directory would leave it with no driver at
# all, where saying nothing leaves it the machine's own.
# 3. Ask the driver for the amount of free memory. Without this, llama.cpp
# reads zero free memory on an integrated graphics chip, because such a
# chip has no memory of its own and shares the system's.
#
# The test builds a fake backend directory for each run.sh, runs it, and reads
# back the variables it exported.
set -euo pipefail
WORK=$(mktemp -d)
trap 'rm -rf "$WORK"' EXIT
REPO_ROOT=$(dirname "$(dirname "$(dirname "$(realpath "$0")")")")
RUN_SCRIPTS=(
"backend/cpp/llama-cpp/run.sh llama-cpp"
"backend/cpp/turboquant/run.sh turboquant"
"backend/cpp/bonsai/run.sh bonsai"
)
failures=0
fail() {
echo "FAIL: $*"
failures=$((failures + 1))
}
# Builds a fake backend directory: the real run.sh, a stand-in for the backend
# program that prints the variables we care about, and whichever libraries the
# caller asked for.
#
# Usage: make_backend <dir> <program-prefix> [library ...]
make_backend() {
local dir="$1" prefix="$2"
shift 2
mkdir -p "$dir/lib"
cp "$RUN_SH" "$dir/run.sh"
chmod +x "$dir/run.sh"
local lib
for lib in "$@"; do
: > "$dir/lib/$lib"
done
cat > "$dir/${prefix}-fallback" <<'PROGRAM'
#!/bin/bash
echo "level_zero_driver=${ZE_ENABLE_ALT_DRIVERS:-}"
echo "opencl_driver_list=${OCL_ICD_VENDORS:-}"
echo "report_free_memory=${ZES_ENABLE_SYSMAN:-}"
PROGRAM
chmod +x "$dir/${prefix}-fallback"
}
# Runs a fake backend and prints the one variable asked for.
# Usage: read_variable <dir> <name>
read_variable() {
local dir="$1" name="$2"
bash "$dir/run.sh" 2>/dev/null | sed -n "s/^${name}=//p"
}
for entry in "${RUN_SCRIPTS[@]}"; do
read -r script prefix <<< "$entry"
RUN_SH="$REPO_ROOT/$script"
if [ ! -f "$RUN_SH" ]; then
fail "$script does not exist"
continue
fi
# An Intel build with its own graphics driver: point Level Zero and OpenCL
# at the bundled copies and ask for the free memory reading.
bundled="$WORK/$prefix-bundled"
make_backend "$bundled" "$prefix" \
libze_loader.so.1 libze_intel_gpu.so.1 libigdrcl.so
mkdir -p "$bundled/etc/OpenCL/vendors"
echo "libigdrcl.so" > "$bundled/etc/OpenCL/vendors/intel.icd"
got=$(read_variable "$bundled" level_zero_driver)
if [ "$got" != "$bundled/lib/libze_intel_gpu.so.1" ]; then
fail "$script: expected Level Zero to use the bundled driver, got '$got'"
fi
# Even with an OpenCL driver and a driver list sitting in the backend, which
# is what an older packaging left behind, OpenCL must be left alone.
got=$(read_variable "$bundled" opencl_driver_list)
if [ -n "$got" ]; then
fail "$script: OpenCL was pointed at the backend's own directory ('$got')"
fi
got=$(read_variable "$bundled" report_free_memory)
if [ "$got" != "1" ]; then
fail "$script: expected the free memory reading to be turned on, got '$got'"
fi
# The user picked a driver already. Both choices must survive.
got=$(ZE_ENABLE_ALT_DRIVERS=/usr/lib/host-driver.so \
read_variable "$bundled" level_zero_driver)
if [ "$got" != "/usr/lib/host-driver.so" ]; then
fail "$script: the user's Level Zero driver was overwritten with '$got'"
fi
got=$(ZES_ENABLE_SYSMAN=0 read_variable "$bundled" report_free_memory)
if [ "$got" != "0" ]; then
fail "$script: the user's free memory setting was overwritten with '$got'"
fi
# A build for some other kind of graphics card. None of the Intel
# variables belong here.
other="$WORK/$prefix-other"
make_backend "$other" "$prefix" libcublas.so.12
for name in level_zero_driver opencl_driver_list report_free_memory; do
got=$(read_variable "$other" "$name")
if [ -n "$got" ]; then
fail "$script: $name was set on a build with no Intel libraries ('$got')"
fi
done
done
if [ "$failures" -gt 0 ]; then
echo "$failures check(s) failed"
exit 1
fi
echo "PASS: every run.sh sets up the Intel graphics driver correctly"

View File

@@ -1,26 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
CURDIR=$(dirname "$(realpath "$0")")
SELECTOR="$CURDIR/../../.docker/llama-cpp-build-target.sh"
assert_target() {
local arch=$1
local build_type=$2
local expected=$3
local actual
actual=$("$SELECTOR" "$arch" "$build_type")
if [ "$actual" != "$expected" ]; then
echo "FAIL: $arch/$build_type selected $actual, expected $expected"
exit 1
fi
}
assert_target amd64 cublas llama-cpp-cpu-all
assert_target amd64 vulkan llama-cpp-cpu-all
assert_target amd64 "" llama-cpp-cpu-all
assert_target arm64 cublas llama-cpp-fallback
assert_target arm64 "" llama-cpp-cpu-all
echo "PASS: llama.cpp build target preserves CPU variants where supported"

View File

@@ -1,171 +0,0 @@
#!/bin/bash
# Checks what package_intel_libs puts in a backend built for Intel GPUs.
#
# The packager copies the libraries a backend needs next to the backend itself,
# so it can run on a machine that has none of them installed. Four things have
# to happen, and each one has been missing at some point:
#
# 1. Copy the libraries the backend program is linked against. Some of them
# are only reachable from the program, not from any other copied library,
# so looking at the copied libraries alone is not enough.
# 2. Copy the libraries that are opened by name while the program runs. Those
# are invisible to any tool that reads the list of libraries a file is
# linked against, so they have to be named one by one.
# 3. Copy the Intel graphics driver, which is also opened by name at run
# time.
# 4. Copy only the driver Level Zero talks to, and leave the OpenCL one out.
# llama.cpp reaches an Intel GPU through Level Zero; the OpenCL driver
# brings a second copy of the graphics compiler with it, which is about
# 139 MB for a path nothing here uses.
#
# The test builds a stand-in for an oneAPI installation, a stand-in for a
# driver installation and two fake backend programs, runs the real packager and
# checks the result.
set -euo pipefail
CURDIR=$(dirname "$(realpath "$0")")
SCRIPT="$CURDIR/package-gpu-libs.sh"
if ! command -v gcc >/dev/null 2>&1 || ! command -v ldd >/dev/null 2>&1; then
echo "SKIP: gcc/ldd not available"
exit 0
fi
WORK=$(mktemp -d)
trap 'rm -rf "$WORK"' EXIT
# Stand-in for /opt/intel/oneapi/*/lib.
ONEAPI="$WORK/oneapi/lib"
mkdir -p "$ONEAPI"
# Two libraries the backend programs are linked against, one each. Nothing else
# refers to them, so they can only be found by looking at the programs.
echo 'int first_fn(void){return 1;}' > "$WORK/first.c"
gcc -shared -fPIC -o "$ONEAPI/libfakeoneapifirst.so.2" "$WORK/first.c"
echo 'int second_fn(void){return 2;}' > "$WORK/second.c"
gcc -shared -fPIC -o "$ONEAPI/libfakeoneapisecond.so.2" "$WORK/second.c"
# A library that is opened by name while the program runs. Nothing is linked
# against it, so only the list of names in the packager can find it.
echo 'int adapter_fn(void){return 3;}' > "$WORK/adapter.c"
gcc -shared -fPIC -o "$ONEAPI/libur_adapter_level_zero.so.0" "$WORK/adapter.c"
# Two fake backend programs, in the directory the real packaging script uses:
# package/, one level above package/lib. One is named after llama.cpp, the
# other is not, because the same packager serves several backends.
PKG="$WORK/package"
TARGET="$PKG/lib"
mkdir -p "$TARGET"
echo 'int first_fn(void); int main(void){return first_fn();}' > "$WORK/main1.c"
gcc -o "$PKG/llama-cpp-grpc" "$WORK/main1.c" \
-L"$ONEAPI" -l:libfakeoneapifirst.so.2 -Wl,-rpath,"$ONEAPI"
echo 'int second_fn(void); int main(void){return second_fn();}' > "$WORK/main2.c"
gcc -o "$PKG/bonsai-grpc" "$WORK/main2.c" \
-L"$ONEAPI" -l:libfakeoneapisecond.so.2 -Wl,-rpath,"$ONEAPI"
# The real directory also holds the script that starts the backend. Looking at a
# shell script for libraries has to be harmless.
printf '#!/bin/bash\necho started\n' > "$PKG/run.sh"
chmod +x "$PKG/run.sh"
# Stand-in for the Intel graphics driver installation. These files are opened by
# name at run time rather than linked, so the packager has to name the ones it
# wants.
DRV="$WORK/driver"
mkdir -p "$DRV/intel-opencl"
echo 'int ze_drv(void){return 4;}' > "$WORK/zedrv.c"
gcc -shared -fPIC -o "$DRV/libze_intel_gpu.so.1" "$WORK/zedrv.c"
echo 'int cl_drv(void){return 5;}' > "$WORK/cldrv.c"
gcc -shared -fPIC -o "$DRV/intel-opencl/libigdrcl.so" "$WORK/cldrv.c"
# The compiler front end the OpenCL driver needs, and the large library it is
# linked against. The link is what makes the big one arrive on its own if the
# front end is ever copied again, so the fake mirrors it.
echo 'int clang_fn(void){return 6;}' > "$WORK/clang.c"
gcc -shared -fPIC -o "$DRV/libopencl-clang.so.15" "$WORK/clang.c"
echo 'int clang_fn(void); int fcl_fn(void){return clang_fn();}' > "$WORK/fcl.c"
gcc -shared -fPIC -o "$DRV/libigdfcl.so.2" "$WORK/fcl.c" \
-L"$DRV" -l:libopencl-clang.so.15 -Wl,-rpath,"$DRV"
# Let the fake oneAPI libraries be found the way the real ones are on the build
# machine.
export LD_LIBRARY_PATH="$ONEAPI:${LD_LIBRARY_PATH:-}"
# shellcheck source=/dev/null
source "$SCRIPT" "$TARGET"
export BUILD_TYPE=sycl_f16
export INTEL_ONEAPI_LIB_DIRS="$ONEAPI"
export INTEL_DRIVER_LIB_DIRS="$DRV $DRV/intel-opencl"
package_intel_libs
fail=false
for lib in libfakeoneapifirst.so.2 libfakeoneapisecond.so.2; do
if [ ! -e "$TARGET/$lib" ]; then
echo "FAIL: $lib is missing; the backend programs' own libraries were not copied"
fail=true
fi
done
if [ ! -e "$TARGET/libur_adapter_level_zero.so.0" ]; then
echo "FAIL: the Level Zero adapter, which is opened by name, was not copied"
fail=true
fi
if [ ! -e "$TARGET/libze_intel_gpu.so.1" ]; then
echo "FAIL: the Level Zero graphics driver was not copied"
fail=true
fi
# The OpenCL driver and the compiler front end that hangs off it are left out,
# and so is the driver list that would name them.
for lib in libigdrcl.so libigdfcl.so.2 libopencl-clang.so.15; do
if [ -e "$TARGET/$lib" ]; then
echo "FAIL: $lib was copied, but nothing here uses the OpenCL path"
fail=true
fi
done
if [ -e "$TARGET/../etc/OpenCL" ]; then
echo "FAIL: an OpenCL driver list was created for a path nothing uses"
fail=true
fi
# The Python backends for Intel GPUs, built as BUILD_TYPE=intel, start without
# run.sh and so never load a copied driver. Copying one for them would add
# several hundred megabytes that nothing reads.
PYTHON_STYLE="$WORK/python-backend/lib"
mkdir -p "$PYTHON_STYLE"
(
BUILD_TYPE=intel \
INTEL_ONEAPI_LIB_DIRS="$ONEAPI" \
INTEL_DRIVER_LIB_DIRS="$DRV $DRV/intel-opencl" \
bash -c 'source "$0" "$1"; package_intel_libs' "$SCRIPT" "$PYTHON_STYLE"
) >/dev/null 2>&1
if [ -e "$PYTHON_STYLE/libze_intel_gpu.so.1" ]; then
echo "FAIL: the graphics driver was copied into a backend that cannot load it"
fail=true
fi
# A build for Intel GPUs that ends up with no driver still works, but only on a
# machine that has its own. That is easy to cause by accident and impossible to
# see afterwards, so the packager has to say so.
warning=$(
BUILD_TYPE=sycl_f16 \
INTEL_ONEAPI_LIB_DIRS="$ONEAPI" \
INTEL_DRIVER_LIB_DIRS="$WORK/empty" \
bash -c 'source "$0" "$1"; package_intel_libs' \
"$SCRIPT" "$WORK/nodriver/lib" 2>&1 >/dev/null || true
)
if ! grep -qi "no intel graphics driver" <<< "$warning"; then
echo "FAIL: no warning when the graphics driver could not be copied"
fail=true
fi
if [ "$fail" = true ]; then
ls -la "$TARGET" || true
exit 1
fi
echo "PASS: the oneAPI libraries, the adapter and the Level Zero graphics driver were all handled"
exit 0

View File

@@ -675,43 +675,21 @@ package_rocm_libs() {
package_intel_libs() {
echo "Packaging Intel oneAPI/SYCL libraries for BUILD_TYPE=${BUILD_TYPE}..."
# Where to look for the oneAPI libraries. The default is the standard install
# layout. The list can be overridden with a space-separated one, which lets
# the tests run without a real oneAPI install, the same way ROCM_BASE_DIRS
# works. Both the current and the older math library layouts are listed, and
# the check below skips whichever of them is absent.
local intel_lib_paths
if [ -n "${INTEL_ONEAPI_LIB_DIRS:-}" ]; then
# shellcheck disable=SC2206 # intentional word-split of the override
intel_lib_paths=(${INTEL_ONEAPI_LIB_DIRS})
else
intel_lib_paths=(
"/opt/intel/oneapi/compiler/latest/lib"
"/opt/intel/oneapi/mkl/latest/lib"
"/opt/intel/oneapi/mkl/latest/lib/intel64"
"/opt/intel/oneapi/dnnl/latest/lib"
"/opt/intel/oneapi/tbb/latest/lib/intel64/gcc4.8"
)
fi
local intel_lib_paths=(
"/opt/intel/oneapi/compiler/latest/lib"
"/opt/intel/oneapi/mkl/latest/lib/intel64"
"/opt/intel/oneapi/tbb/latest/lib/intel64/gcc4.8"
)
# The oneAPI libraries a backend needs at run time. The math library entries
# cover both of its number formats and both of its threading layers, because
# the llama.cpp build for Intel GPUs uses a different combination than the
# rest. The libur_adapter_* entries have to be named here even though nothing
# is linked against them: oneAPI opens them by name while the program runs,
# so the dependency scan later in this function cannot see them.
# Core Intel oneAPI runtime libraries
local intel_libs=(
"libsycl.so*"
"libOpenCL.so*"
"libmkl_core.so*"
"libmkl_intel_lp64.so*"
"libmkl_intel_ilp64.so*"
"libmkl_intel_thread.so*"
"libmkl_tbb_thread.so*"
"libmkl_sequential.so*"
"libmkl_sycl.so*"
"libmkl_sycl_blas.so*"
"libdnnl.so*"
"libiomp5.so*"
"libsvml.so*"
"libirng.so*"
@@ -719,10 +697,6 @@ package_intel_libs() {
"libintlc.so*"
"libtbb.so*"
"libtbbmalloc.so*"
"libur_loader.so*"
"libur_adapter_level_zero.so*"
"libur_adapter_level_zero_v2.so*"
"libur_adapter_opencl.so*"
"libpi_level_zero.so*"
"libpi_opencl.so*"
"libze_loader.so*"
@@ -736,92 +710,10 @@ package_intel_libs() {
fi
done
# Copy the libraries the backend programs themselves are linked against. The
# list above is not enough on its own: the programs are linked directly
# against several oneAPI libraries that no copied library refers to, so
# without this step the backend only ran inside the build image, where oneAPI
# happens to be on the library path.
#
# The programs sit one level above the target directory, in package/, next to
# the run.sh that starts them. Every backend that builds for Intel GPUs is
# covered by looking at all of them, rather than at one set of names, because
# llama.cpp, turboquant and bonsai all come through here.
local pkg_dir="$TARGET_LIB_DIR/.."
local bin
for bin in "$pkg_dir"/*; do
if [ -f "$bin" ] && [ -x "$bin" ]; then
copy_elf_deps "$bin"
fi
done
# Copy the Intel graphics driver itself, the way the Vulkan packaging copies
# the Mesa driver. Level Zero opens the driver by name while the program
# runs, so no dependency scan can find it and it has to be named here.
#
# This is what lets the backend run on a machine with no Intel graphics
# packages installed, and also on a machine whose own driver was built
# against a newer C library than the one this backend carries, where loading
# the host's driver crashes. Carrying the driver is safe across kernel
# versions because it reaches the graphics hardware through an interface the
# kernel keeps stable. The NVIDIA driver has no such interface, which is why
# that one is never copied.
#
# Only the builds that start through run.sh get a driver: run.sh is what
# tells Level Zero to use it. The Python backends built for Intel GPUs start
# differently and keep using the host's driver, so copying one for them would
# add several hundred megabytes that nothing would ever load.
#
# Only the Level Zero side is copied. llama.cpp reaches an Intel GPU through
# Level Zero, which hands the driver ready-compiled programs and so needs
# only the compiler's back end. The OpenCL driver can be handed source code
# instead, so it also needs the compiler's front end, and that pulls in a
# copy of clang: around 139 MB for a path nothing here takes. A user who
# wants OpenCL has their machine's own.
case "${BUILD_TYPE:-}" in
sycl*)
local intel_driver_lib_dirs
if [ -n "${INTEL_DRIVER_LIB_DIRS:-}" ]; then
# shellcheck disable=SC2206 # split the override into words on purpose
intel_driver_lib_dirs=(${INTEL_DRIVER_LIB_DIRS})
else
intel_driver_lib_dirs=(
"/usr/lib/x86_64-linux-gnu"
"/usr/lib"
)
fi
local driver_libs=(
"libze_intel_gpu.so*" # the driver Level Zero talks to
"libigc.so*" # turns compute programs into instructions for the card
"libigdgmm.so*" # manages graphics memory
)
local drv_dir pat
for drv_dir in "${intel_driver_lib_dirs[@]}"; do
[ -d "$drv_dir" ] || continue
for pat in "${driver_libs[@]}"; do
copy_libs_glob "${drv_dir}/${pat}"
done
done
;;
esac
# Copy whatever the steps above still missed. Each library copied so far can
# need further libraries of its own, and a missing one stops the backend from
# starting at all (issue #10537).
# Pull in transitive deps the allowlist misses so the backend is
# self-contained (same class of failure as #10537).
sweep_transitive_deps "$TARGET_LIB_DIR"
# Say so when a build meant for Intel GPUs ends up without a driver. It still
# works on a machine that has its own, so nothing fails here, and the only
# other way to notice is a user reporting that their GPU is not used. The
# usual cause is a build image that predates the driver being installed in
# .docker/install-base-deps.sh.
case "${BUILD_TYPE:-}" in
sycl*)
if [ ! -e "$TARGET_LIB_DIR/libze_intel_gpu.so.1" ]; then
echo "WARNING: no Intel graphics driver was found to copy. This backend will only use a GPU on a machine that has its own Intel driver installed." >&2
fi
;;
esac
echo "Intel oneAPI libraries packaged successfully"
}

View File

@@ -1,26 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
CURDIR=$(dirname "$(realpath "$0")")
SELECTOR="$CURDIR/../../.docker/turboquant-build-target.sh"
assert_target() {
local arch=$1
local build_type=$2
local expected=$3
local actual
actual=$("$SELECTOR" "$arch" "$build_type")
if [ "$actual" != "$expected" ]; then
echo "FAIL: $arch/$build_type selected $actual, expected $expected"
exit 1
fi
}
assert_target amd64 cublas turboquant-cpu-all
assert_target amd64 vulkan turboquant-cpu-all
assert_target amd64 "" turboquant-cpu-all
assert_target arm64 cublas turboquant-fallback
assert_target arm64 "" turboquant-cpu-all
echo "PASS: turboquant build target preserves CPU variants where supported"

View File

@@ -389,14 +389,10 @@ export const SHARED_BUILD_INPUTS = [
darwin: always,
},
{
// Decides which GPU libraries end up inside an image. Every Linux image
// runs it: Dockerfile.python calls it directly, and the Go and C++ backends
// call it from their own package.sh. Naming only the Python images here is
// how a packaging fix for the Intel llama.cpp backend could merge and reach
// no image, which is the #10946 case all over again. The Darwin builds have
// their own packaging scripts and never call this one.
// Stages the CUDA/ROCm runtime libraries into every Python image's lib/.
// COPY'd and run by Dockerfile.python only. This is the #10946 case.
matches: file => file === "scripts/build/package-gpu-libs.sh",
linux: always,
linux: isLinuxPython,
darwin: never,
},
{

View File

@@ -86,25 +86,20 @@ const run = (changedFiles, previousMatrix) =>
const names = entries => entries.map(e => e.backend).sort();
test("a change to only package-gpu-libs.sh rebuilds every Linux image", () => {
// The PR #10946 regression: this script decides which GPU libraries end up
// inside an image, but lives under scripts/, so the per-backend prefix match
// produced an empty matrix and the packaging fix shipped to nothing.
//
// Every Linux image runs it, not only the Python ones: Dockerfile.python
// calls it directly, and the Go and C++ backends call it from their own
// package.sh (see backend/cpp/llama-cpp/package.sh and backend/go/*/
// package.sh). Leaving those out is how a fix aimed at the Intel llama.cpp
// backend could merge and reach no image.
test("a change to only package-gpu-libs.sh rebuilds every Python image", () => {
// The PR #10946 regression: this script is COPY'd and run by
// Dockerfile.python for every Python backend, but lives under scripts/, so
// the per-backend prefix match produced an empty matrix and the cuDNN
// packaging fix shipped to nothing.
const { filtered, filteredDarwin, changedBackends } = run([
"scripts/build/package-gpu-libs.sh",
]);
assert.equal(filtered.length, includes.length);
assert.notEqual(filtered.length, 0, "expected a non-empty Linux matrix");
assert.deepEqual(names(filtered), ["diffusers", "vllm"]);
assert.ok(changedBackends.has("vllm"));
assert.ok(changedBackends.has("llama-cpp"));
// The Darwin builds have their own packaging scripts and never call this one.
// Darwin Python builds never invoke it (see scripts/build/python-darwin.sh).
assert.deepEqual(filteredDarwin, []);
});

View File

@@ -1,13 +0,0 @@
# Counters shown on the landing page.
#
# The four GitHub fields are rewritten by .github/ci/refresh-site-counters.sh,
# which runs weekly from .github/workflows/refresh-site-counters.yml. Editing
# them by hand works but will be overwritten on the next run.
stars: 48067
forks: 4320
contributors: 225
releases: 133
# The GitHub API cannot answer for this one, so it is maintained by hand and
# the refresh script carries it through untouched.
discord: 3187

View File

@@ -16,8 +16,9 @@ enableEmoji = true
discord = 'https://discord.gg/uJAeKSAGDy'
x = 'https://twitter.com/LocalAI_API'
huggingface = 'https://huggingface.co/mudler'
# Counters live in data/stats.yaml, which .github/ci/refresh-site-counters.sh
# rewrites weekly. They used to sit here as hand-typed strings and drifted.
# Refreshed by hand or by CI; shown in the nav and the traction band.
stars = '48,042'
contributors = '224'
[markup.goldmark.renderer]
unsafe = true

View File

@@ -16,9 +16,9 @@
<a class="btn btn--o" href="{{ .Site.Params.github }}">&#9733; Star on GitHub</a>
</div>
<div class="figures fd">
<div><b class="tnum" data-count="{{ .Site.Data.stats.stars }}">0</b><span>GitHub stars</span></div>
<div><b class="tnum" data-count="48042">0</b><span>GitHub stars</span></div>
<div><b class="tnum" data-count="73">0</b><span>Backends</span></div>
<div><b class="tnum" data-count="{{ len .Site.Data.engines.engines }}">0</b><span>Engines we wrote</span></div>
<div><b class="tnum" data-count="18">0</b><span>Engines we wrote</span></div>
<div><b class="tnum" data-count="1585">0</b><span>Models, one click</span></div>
</div>
</div>
@@ -164,11 +164,8 @@
<div class="shell">
<div class="bars rv" aria-hidden="true"><i></i><i></i><i></i><i></i></div>
<p class="kicker rv">Engines we build</p>
<h2 class="rv mt1" style="max-width:22ch">{{ len .Site.Data.engines.engines }} engines, written from scratch.</h2>
{{/* Names the link back to the runtime section explicitly. Readers were
arriving here and taking these for unrelated side projects, because
nothing on the page said they are the backends the core loads. */}}
<p class="lede rv mt2">Those backends the core pulls in on demand are mostly ours. Most projects wrap somebody else's engine. We wrote ours, because the thing we needed came as a 9 GB Python install, or was closed, or did not exist. Each one is a binary and a GGUF file, checked against the reference implementation in CI.</p>
<h2 class="rv mt1" style="max-width:22ch">Eighteen engines, written from scratch.</h2>
<p class="lede rv mt2">Most backends wrap somebody else's engine. These do not. They exist because the thing we needed was a 9 GB Python install, or closed, or nobody had built it yet. Each one is a binary and a GGUF file, checked against the reference implementation in CI.</p>
<div class="spot rv">
<div>
@@ -250,7 +247,7 @@
<video src="/media/vllm-race.mp4" muted loop playsinline preload="none" data-lazy aria-label="vllm.cpp ahead of vLLM at every concurrency level"></video>
</div>
<p class="rv mt2"><a class="btn btn--o" href="/engines/">All {{ len .Site.Data.engines.engines }} engines →</a></p>
<p class="rv mt2"><a class="btn btn--o" href="/engines/">All eighteen engines →</a></p>
</div>
</section>
@@ -260,10 +257,7 @@
<div class="bars rv" aria-hidden="true"><i></i><i></i><i></i><i></i></div>
<p class="kicker rv">APEX quantization</p>
<h2 class="rv mt1" style="max-width:19ch">The model you could not fit, on the card you already own.</h2>
{{/* APEX was being used as a known term on first appearance, in a section
that opened straight onto a benchmark table. The first two sentences
say what it is and why it follows the engines. */}}
<p class="lede rv mt2">The engine decides how fast a model runs. The weights decide whether it runs at all, so we build those too. APEX assigns a different precision to every tensor and every layer: a 35B mixture-of-experts model goes from 64.6 GB, out of reach of any consumer GPU, to 12.2 GB at 74 tokens a second. That is more than twice the speed of the original, and quality barely moves. The file is an ordinary GGUF, so stock llama.cpp opens it with no patches, and 201 of them are already sitting in the LocalAI gallery.</p>
<p class="lede rv mt2">A 35B mixture-of-experts model is 64.6 GB at full precision, which puts it out of reach of every consumer GPU. APEX gets it to 12.2 GB, and it runs at 74 tokens a second, more than twice the speed of the original. Quality barely moves. The file is an ordinary GGUF, so stock llama.cpp opens it with no patches, and 201 of them are already sitting in the LocalAI gallery.</p>
<div class="sizes rv">
<div class="sz"><span>F16 · 64.6 GB</span><i style="width:100%"></i><u>30.4 t/s</u></div>
<div class="sz"><span>Q8_0 · 34.4 GB</span><i style="width:53%"></i><u>52.5 t/s</u></div>
@@ -369,14 +363,14 @@
<h2 class="rv mt1" style="max-width:22ch">Forty-eight thousand stars, and still shipping every week.</h2>
<div class="trend rv">
<img src="/img/trendshift.svg" alt="LocalAI on Trendshift">
<p>LocalAI has been <b>trending on GitHub</b> repeatedly since it launched, and it is one of the most starred self-hosted AI projects there is. <b>{{ lang.FormatNumberCustom 0 .Site.Data.stats.contributors }} people</b> have contributed code, <b>{{ lang.FormatNumberCustom 0 .Site.Data.stats.discord }}</b> are in the Discord, and the README is kept translated into <b>eight languages</b> because the users are everywhere.</p>
<p>LocalAI has been <b>trending on GitHub</b> repeatedly since it launched, and it is one of the most starred self-hosted AI projects there is. <b>224 people</b> have contributed code, <b>3,187</b> are in the Discord, and the README is kept translated into <b>eight languages</b> because the users are everywhere.</p>
</div>
<div class="big rv">
<div><b class="tnum" data-count="{{ .Site.Data.stats.stars }}">0</b><span>Stars</span></div>
<div><b class="tnum" data-count="{{ .Site.Data.stats.forks }}">0</b><span>Forks</span></div>
<div><b class="tnum" data-count="{{ .Site.Data.stats.contributors }}">0</b><span>Contributors</span></div>
<div><b class="tnum" data-count="{{ .Site.Data.stats.releases }}">0</b><span>Releases</span></div>
<div><b class="tnum" data-count="{{ .Site.Data.stats.discord }}">0</b><span>In Discord</span></div>
<div><b class="tnum" data-count="48042">0</b><span>Stars</span></div>
<div><b class="tnum" data-count="4314">0</b><span>Forks</span></div>
<div><b class="tnum" data-count="224">0</b><span>Contributors</span></div>
<div><b class="tnum" data-count="133">0</b><span>Releases</span></div>
<div><b class="tnum" data-count="3187">0</b><span>In Discord</span></div>
<div><b class="tnum" data-count="0" data-text="3 yrs">0</b><span>Shipping since</span></div>
</div>
<div class="tl rv">
@@ -427,7 +421,7 @@
employers reads as a customer logo wall, which is a claim we are not
making; a sentence keeps it about the people, which is the true one. */}}
<p class="kicker rv mt3">Who shows up</p>
<h3 class="eco__h rv">{{ lang.FormatNumberCustom 0 .Site.Data.stats.contributors }} people have put code in this repository.</h3>
<h3 class="eco__h rv">{{ .Site.Params.contributors }} people have put code in this repository.</h3>
{{- $co := slice }}
{{- range .Site.Data.ecosystem.contributors.companies }}{{ $co = $co | append (printf "<b>%s</b>" .name) }}{{ end }}
{{- $ac := slice }}

View File

@@ -13,7 +13,7 @@
<a href="{{ .Site.Params.docsURL }}">Docs</a>
</nav>
<div class="topright">
<a class="pill" href="{{ .Site.Params.github }}">&#9733; {{ lang.FormatNumberCustom 0 .Site.Data.stats.stars }}</a>
<a class="pill" href="{{ .Site.Params.github }}">&#9733; {{ .Site.Params.stars }}</a>
<a class="go-btn" href="{{ $home }}#start">Install</a>
</div>
</header>