mirror of
https://github.com/mudler/LocalAI.git
synced 2026-07-31 18:38:23 -04:00
backend(audio-cpp): gate model loading on the audio.cpp family
Refuses any GGUF without an audiocpp.model_spec.family key and any non-GGUF path without an explicit family option, so the model loader's greedy backend probe cannot bind an unrelated llama.cpp GGUF to this backend (#9287). Assisted-by: Claude:claude-opus-5 [Claude Code] Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
60
backend/cpp/audio-cpp/family_gate.cpp
Normal file
60
backend/cpp/audio-cpp/family_gate.cpp
Normal file
@@ -0,0 +1,60 @@
|
||||
#include "family_gate.h"
|
||||
|
||||
#include <cctype>
|
||||
|
||||
namespace audiocpp_backend {
|
||||
namespace {
|
||||
|
||||
bool ends_with_ci(const std::string &value, const std::string &suffix) {
|
||||
if (value.size() <= suffix.size()) {
|
||||
return false; // a bare ".gguf" is an extension, not a model file
|
||||
}
|
||||
const size_t offset = value.size() - suffix.size();
|
||||
for (size_t i = 0; i < suffix.size(); ++i) {
|
||||
const auto lhs = static_cast<unsigned char>(value[offset + i]);
|
||||
const auto rhs = static_cast<unsigned char>(suffix[i]);
|
||||
if (std::tolower(lhs) != std::tolower(rhs)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool path_looks_like_gguf(const std::string &path) {
|
||||
return ends_with_ci(path, ".gguf");
|
||||
}
|
||||
|
||||
FamilyDecision decide_family(bool path_is_gguf, const std::string &embedded_family,
|
||||
const std::string &configured_family) {
|
||||
FamilyDecision decision;
|
||||
|
||||
if (!configured_family.empty()) {
|
||||
decision.ok = true;
|
||||
decision.family = configured_family;
|
||||
return decision;
|
||||
}
|
||||
|
||||
if (path_is_gguf) {
|
||||
if (!embedded_family.empty()) {
|
||||
decision.ok = true;
|
||||
decision.family = embedded_family;
|
||||
return decision;
|
||||
}
|
||||
decision.error =
|
||||
"audio-cpp: this GGUF carries no 'audiocpp.model_spec.family' "
|
||||
"metadata key, so it is not an audio.cpp model. Convert it with "
|
||||
"audiocpp_gguf, or name the family explicitly with the model option "
|
||||
"'family:<name>'";
|
||||
return decision;
|
||||
}
|
||||
|
||||
decision.error =
|
||||
"audio-cpp: a model path that is not a standalone audio.cpp GGUF needs "
|
||||
"an explicit 'family:<name>' model option, because the audio.cpp family "
|
||||
"cannot be inferred from a safetensors or package directory";
|
||||
return decision;
|
||||
}
|
||||
|
||||
} // namespace audiocpp_backend
|
||||
35
backend/cpp/audio-cpp/family_gate.h
Normal file
35
backend/cpp/audio-cpp/family_gate.h
Normal file
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
// Decides which audio.cpp family a model path belongs to, and refuses paths
|
||||
// this backend must not claim. Standard library only.
|
||||
//
|
||||
// This is the guard against issue #9287. A model config with no explicit
|
||||
// backend makes LocalAI probe every installed backend and bind to the first
|
||||
// Load that succeeds, so accepting an arbitrary GGUF here would capture
|
||||
// unrelated LLMs. audio.cpp GGUFs carry an audiocpp.model_spec.family metadata
|
||||
// key; llama.cpp GGUFs do not.
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace audiocpp_backend {
|
||||
|
||||
// True when the path ends in ".gguf", case insensitively, and has a stem.
|
||||
bool path_looks_like_gguf(const std::string &path);
|
||||
|
||||
struct FamilyDecision {
|
||||
bool ok = false;
|
||||
std::string family;
|
||||
// Set when ok is false. Suitable verbatim as an INVALID_ARGUMENT message.
|
||||
std::string error;
|
||||
};
|
||||
|
||||
// Precedence:
|
||||
// 1. an explicit `family:` option, so a user can override wrong metadata;
|
||||
// 2. for a GGUF, the family embedded in audiocpp.model_spec.family;
|
||||
// 3. otherwise refuse.
|
||||
// A directory path never consults embedded metadata: there is no single GGUF
|
||||
// to read it from.
|
||||
FamilyDecision decide_family(bool path_is_gguf, const std::string &embedded_family,
|
||||
const std::string &configured_family);
|
||||
|
||||
} // namespace audiocpp_backend
|
||||
118
backend/cpp/audio-cpp/family_gate_test.cpp
Normal file
118
backend/cpp/audio-cpp/family_gate_test.cpp
Normal file
@@ -0,0 +1,118 @@
|
||||
// Unit tests for family_gate. Standard library only. The harness compiles this
|
||||
// as a single translation unit, so the implementation is included directly.
|
||||
//
|
||||
// This unit is the guard against issue #9287: when a model config has no
|
||||
// explicit backend, LocalAI probes every installed backend and binds to the
|
||||
// first Load that succeeds. Accepting an arbitrary GGUF here would capture
|
||||
// unrelated LLMs.
|
||||
|
||||
#include "family_gate.cpp"
|
||||
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
|
||||
static int failures = 0;
|
||||
|
||||
static void check(bool ok, const std::string &name) {
|
||||
if (!ok) {
|
||||
failures++;
|
||||
fprintf(stderr, "FAIL: %s\n", name.c_str());
|
||||
} else {
|
||||
fprintf(stderr, "ok: %s\n", name.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
using namespace audiocpp_backend;
|
||||
|
||||
static void test_gguf_suffix_detection() {
|
||||
check(path_looks_like_gguf("/models/chatterbox-q8_0.gguf"), "plain .gguf");
|
||||
check(path_looks_like_gguf("/models/CHATTERBOX.GGUF"), "uppercase .GGUF");
|
||||
check(path_looks_like_gguf("/models/x.GgUf"), "mixed case .GgUf");
|
||||
check(path_looks_like_gguf("a.gguf"), "a one character stem is still a stem");
|
||||
check(!path_looks_like_gguf("/models/chatterbox"), "extensionless directory");
|
||||
check(!path_looks_like_gguf("/models/model.safetensors"), "safetensors");
|
||||
check(!path_looks_like_gguf("/models/gguf"), "a name that is merely 'gguf'");
|
||||
check(!path_looks_like_gguf("/models/GGUF"), "an uppercase name that is merely 'GGUF'");
|
||||
check(!path_looks_like_gguf(""), "empty path");
|
||||
check(!path_looks_like_gguf(".gguf"), "a bare extension is not a model file");
|
||||
// The suffix has to be at the end. A prefix or infix match would let
|
||||
// ".gguf.tmp" download artefacts and ".ggufx" siblings through.
|
||||
check(!path_looks_like_gguf("/models/model.gguf.tmp"), ".gguf in the middle");
|
||||
check(!path_looks_like_gguf("/models/model.ggufx"), "a longer extension");
|
||||
// Every character of the suffix has to match, including the last one.
|
||||
check(!path_looks_like_gguf("/models/model.ggug"), "a near miss in the final character");
|
||||
check(!path_looks_like_gguf("/models/model_gguf"), "a near miss in the first character");
|
||||
check(!path_looks_like_gguf("/models/.gguf-notes"), "a leading .gguf");
|
||||
}
|
||||
|
||||
static void test_explicit_family_always_wins() {
|
||||
// Explicit configuration beats metadata, so a user can force a family when
|
||||
// upstream metadata is wrong or absent.
|
||||
const auto gguf = decide_family(true, "chatterbox", "omnivoice");
|
||||
check(gguf.ok && gguf.family == "omnivoice", "explicit family overrides GGUF metadata");
|
||||
check(gguf.error.empty(), "an accepted decision carries no error text");
|
||||
|
||||
const auto dir = decide_family(false, "", "qwen3_tts");
|
||||
check(dir.ok && dir.family == "qwen3_tts", "explicit family satisfies a directory path");
|
||||
|
||||
// A GGUF with no embedded spec is still loadable when the user names the
|
||||
// family: the option is an override, not a tie-break that needs metadata to
|
||||
// break against.
|
||||
const auto bare = decide_family(true, "", "supertonic");
|
||||
check(bare.ok && bare.family == "supertonic",
|
||||
"explicit family rescues a GGUF that carries no spec");
|
||||
}
|
||||
|
||||
static void test_gguf_metadata_supplies_the_family() {
|
||||
const auto d = decide_family(true, "nemotron_asr", "");
|
||||
check(d.ok, "an audio.cpp GGUF loads with no family option");
|
||||
check(d.family == "nemotron_asr", "family comes from the embedded spec");
|
||||
check(d.error.empty(), "an accepted GGUF carries no error text");
|
||||
}
|
||||
|
||||
// THE GATE. A llama.cpp GGUF has no audiocpp.model_spec.family key.
|
||||
static void test_foreign_gguf_is_refused() {
|
||||
const auto d = decide_family(true, "", "");
|
||||
check(!d.ok, "a GGUF with no audio.cpp spec is refused");
|
||||
check(d.family.empty(), "no family is guessed");
|
||||
check(!d.error.empty(), "a refusal always says why");
|
||||
check(d.error.find("audiocpp.model_spec.family") != std::string::npos,
|
||||
"error names the missing metadata key so the cause is diagnosable");
|
||||
check(d.error.find("family:") != std::string::npos,
|
||||
"error names the option that would override it");
|
||||
}
|
||||
|
||||
static void test_directory_without_family_is_refused() {
|
||||
const auto d = decide_family(false, "", "");
|
||||
check(!d.ok, "a non-GGUF path with no family option is refused");
|
||||
check(d.family.empty(), "a refused directory guesses no family");
|
||||
check(d.error.find("family:") != std::string::npos,
|
||||
"error names the required option");
|
||||
}
|
||||
|
||||
// A directory path never consults embedded metadata, because there is no single
|
||||
// GGUF to read it from.
|
||||
static void test_directory_ignores_embedded_family() {
|
||||
const auto d = decide_family(false, "chatterbox", "");
|
||||
check(!d.ok, "a directory is refused even when an embedded family is supplied");
|
||||
check(d.family.empty(), "a refused directory does not adopt the embedded family");
|
||||
// If the GGUF branch ever leaked into the directory branch this message
|
||||
// would start blaming a metadata key that a directory has no place to carry.
|
||||
check(d.error.find("audiocpp.model_spec.family") == std::string::npos,
|
||||
"a directory refusal does not blame GGUF metadata it could not have");
|
||||
}
|
||||
|
||||
int main() {
|
||||
test_gguf_suffix_detection();
|
||||
test_explicit_family_always_wins();
|
||||
test_gguf_metadata_supplies_the_family();
|
||||
test_foreign_gguf_is_refused();
|
||||
test_directory_without_family_is_refused();
|
||||
test_directory_ignores_embedded_family();
|
||||
if (failures) {
|
||||
fprintf(stderr, "%d check(s) failed\n", failures);
|
||||
return 1;
|
||||
}
|
||||
fprintf(stderr, "all family_gate checks passed\n");
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user