mirror of
https://github.com/mudler/LocalAI.git
synced 2026-08-05 12:54:39 -04:00
Compare commits
4 Commits
feat/audio
...
bot/issue-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
053e828484 | ||
|
|
c905d40d9e | ||
|
|
8089b2bf09 | ||
|
|
89ee62b2af |
@@ -122,7 +122,7 @@ The per-backend prefix match only sees files under a backend's own directory, so
|
||||
|
||||
| Changed path | Rebuilds |
|
||||
|---|---|
|
||||
| `backend/backend.proto` | everything (all languages compile or copy it) |
|
||||
| `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` | Python, Linux only |
|
||||
@@ -132,6 +132,17 @@ The per-backend prefix match only sees files under a backend's own directory, so
|
||||
|
||||
Deliberately excluded: `backend/index.yaml` (gallery metadata, never enters an image), `.github/backend-matrix.yml` (adding a backend would rebuild all of them), `backend/Dockerfile.base-grpc-builder` (owned by `base-images.yml`), and the root `Makefile` (touched in ~11% of commits, and its backend-relevant edits arrive alongside the backend directory anyway). `make test-ci-scripts` pins all of this.
|
||||
|
||||
#### `backend/backend.proto` is content-filtered, not path-filtered
|
||||
|
||||
Every language consumes the proto, so a path rule for it can only ever say "rebuild all 473 images". It changes in ~1.3% of commits, and that was enough to make it the single largest CI cost driver in the repo: on 2026-07-29 four runs totalling 935 queued jobs traced to nothing but a proto edit, one of which (#11158) was a six-line diff adding `bool cache_prompt = 8;`.
|
||||
|
||||
An additive proto edit cannot change how a backend that never references the new symbol behaves, so `filterMatrix()` suppresses the rule for one. `changed-backends.js` fetches `backend/backend.proto` at the base revision (same contents-API pattern as `.github/backend-matrix.yml`) and hands both texts to `protoChangeIsAdditive()`, which compares them structurally rather than textually:
|
||||
|
||||
- **Additive, rebuilds nothing**: a new field with an unused number, a new message, a new enum value, a new RPC. Comment, whitespace and ordering changes also land here.
|
||||
- **Breaking, rebuilds everything**: a removed, renumbered, retyped or renamed field, a dropped RPC, a changed `option` or `package`. So does an unresolvable base revision, matching the run-all posture used for a truncated diff.
|
||||
|
||||
Checked against every proto commit in the preceding six months, all nine resolvable ones classify as additive. Note the tradeoff this accepts: generated stubs do change for an additive edit, so image bytes would differ on a rebuild even though behavior does not. That is the same standard already applied when the filter declines to rebuild on unrelated `pkg/` changes, and the weekly cron remains the backstop.
|
||||
|
||||
The Sunday 06:00 UTC cron on `backend.yml` exists specifically because path filtering can leave Python backends frozen on stale wheels. `DEPS_REFRESH` (below) only fires when the build actually runs, so an untouched Python backend would never re-resolve its unpinned deps. The weekly cron is the safety net.
|
||||
|
||||
## The `DEPS_REFRESH` cache-buster (Python backends)
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(audio-cpp-grpc-server LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
|
||||
set(AUDIO_CPP_DIR "${CMAKE_CURRENT_SOURCE_DIR}/audio.cpp"
|
||||
CACHE PATH "Path to the audio.cpp source tree")
|
||||
set(LOCALAI_BACKEND_PROTO "${CMAKE_CURRENT_SOURCE_DIR}/../../backend.proto"
|
||||
CACHE FILEPATH "Path to the LocalAI backend protocol")
|
||||
|
||||
option(ENGINE_ENABLE_CUDA "Build audio.cpp with CUDA support" OFF)
|
||||
option(ENGINE_ENABLE_VULKAN "Build audio.cpp with Vulkan support" OFF)
|
||||
option(ENGINE_ENABLE_METAL "Build audio.cpp with Metal support" OFF)
|
||||
option(AUDIO_CPP_BUILD_TESTS "Build LocalAI audio.cpp unit tests" OFF)
|
||||
option(AUDIO_CPP_BUILD_GRPC "Build the LocalAI gRPC server" ON)
|
||||
|
||||
find_package(Threads REQUIRED)
|
||||
|
||||
if(NOT EXISTS "${AUDIO_CPP_DIR}/CMakeLists.txt")
|
||||
message(FATAL_ERROR
|
||||
"AUDIO_CPP_DIR does not point to an audio.cpp source tree: ${AUDIO_CPP_DIR}")
|
||||
endif()
|
||||
add_subdirectory("${AUDIO_CPP_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/audio.cpp")
|
||||
|
||||
add_library(localai_audio_cpp_runtime STATIC
|
||||
audio_cpp_runtime.cpp
|
||||
model_config.cpp)
|
||||
target_include_directories(localai_audio_cpp_runtime
|
||||
PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}")
|
||||
target_link_libraries(localai_audio_cpp_runtime PUBLIC engine_runtime)
|
||||
|
||||
if(AUDIO_CPP_BUILD_GRPC)
|
||||
find_package(Protobuf CONFIG REQUIRED)
|
||||
find_package(gRPC CONFIG REQUIRED)
|
||||
find_program(PROTOC_EXECUTABLE NAMES protoc REQUIRED)
|
||||
find_program(GRPC_CPP_PLUGIN_EXECUTABLE NAMES grpc_cpp_plugin REQUIRED)
|
||||
|
||||
get_filename_component(LOCALAI_BACKEND_PROTO_DIR
|
||||
"${LOCALAI_BACKEND_PROTO}" DIRECTORY)
|
||||
set(LOCALAI_PROTO_SOURCES
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/backend.pb.cc"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/backend.grpc.pb.cc")
|
||||
set(LOCALAI_PROTO_HEADERS
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/backend.pb.h"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/backend.grpc.pb.h")
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT ${LOCALAI_PROTO_SOURCES} ${LOCALAI_PROTO_HEADERS}
|
||||
COMMAND "${PROTOC_EXECUTABLE}"
|
||||
ARGS
|
||||
--cpp_out "${CMAKE_CURRENT_BINARY_DIR}"
|
||||
--grpc_out "${CMAKE_CURRENT_BINARY_DIR}"
|
||||
-I "${LOCALAI_BACKEND_PROTO_DIR}"
|
||||
--plugin=protoc-gen-grpc="${GRPC_CPP_PLUGIN_EXECUTABLE}"
|
||||
"${LOCALAI_BACKEND_PROTO}"
|
||||
DEPENDS "${LOCALAI_BACKEND_PROTO}"
|
||||
VERBATIM)
|
||||
|
||||
add_library(localai_backend_proto STATIC
|
||||
${LOCALAI_PROTO_SOURCES}
|
||||
${LOCALAI_PROTO_HEADERS})
|
||||
target_include_directories(localai_backend_proto
|
||||
PUBLIC "${CMAKE_CURRENT_BINARY_DIR}")
|
||||
target_link_libraries(localai_backend_proto
|
||||
PUBLIC protobuf::libprotobuf gRPC::grpc++)
|
||||
|
||||
# Task 2 replaces this generated entry point with the LocalAI service.
|
||||
set(AUDIO_CPP_SERVER_PLACEHOLDER
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/audio-cpp-grpc-server-placeholder.cpp")
|
||||
file(GENERATE OUTPUT "${AUDIO_CPP_SERVER_PLACEHOLDER}"
|
||||
CONTENT "int main() { return 0; }\n")
|
||||
|
||||
add_executable(audio-cpp-grpc-server "${AUDIO_CPP_SERVER_PLACEHOLDER}")
|
||||
target_link_libraries(audio-cpp-grpc-server PRIVATE
|
||||
localai_audio_cpp_runtime
|
||||
engine_runtime
|
||||
localai_backend_proto
|
||||
gRPC::grpc++
|
||||
gRPC::grpc++_reflection)
|
||||
endif()
|
||||
|
||||
if(AUDIO_CPP_BUILD_TESTS)
|
||||
enable_testing()
|
||||
add_executable(audio-cpp-runtime-test tests/runtime_tests.cpp)
|
||||
target_link_libraries(audio-cpp-runtime-test PRIVATE
|
||||
localai_audio_cpp_runtime
|
||||
Threads::Threads)
|
||||
add_test(
|
||||
NAME audio-cpp-runtime-test
|
||||
COMMAND audio-cpp-runtime-test "${AUDIO_CPP_DIR}")
|
||||
endif()
|
||||
@@ -1,67 +0,0 @@
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
AUDIO_CPP_VERSION?=f8fb0c19739193adfad0d9e58da99f25eda65256
|
||||
AUDIO_CPP_REPO?=https://github.com/0xShug0/audio.cpp
|
||||
AUDIO_CPP_SRC?=
|
||||
|
||||
CURRENT_MAKEFILE_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
|
||||
BUILD_DIR := build
|
||||
|
||||
BUILD_TYPE ?=
|
||||
JOBS ?= $(shell nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4)
|
||||
UNAME_S := $(shell uname -s)
|
||||
|
||||
CMAKE_ARGS ?= -DCMAKE_BUILD_TYPE=Release
|
||||
CMAKE_ARGS += -DENGINE_ENABLE_CUDA=OFF
|
||||
CMAKE_ARGS += -DENGINE_ENABLE_VULKAN=OFF
|
||||
CMAKE_ARGS += -DENGINE_ENABLE_METAL=OFF
|
||||
|
||||
ifeq ($(BUILD_TYPE),cublas)
|
||||
CMAKE_ARGS += -DENGINE_ENABLE_CUDA=ON
|
||||
else ifeq ($(BUILD_TYPE),vulkan)
|
||||
CMAKE_ARGS += -DENGINE_ENABLE_VULKAN=ON
|
||||
else ifeq ($(UNAME_S),Darwin)
|
||||
CMAKE_ARGS += -DENGINE_ENABLE_METAL=ON
|
||||
endif
|
||||
|
||||
.PHONY: all grpc-server test test-unit clean purge
|
||||
|
||||
all: grpc-server
|
||||
|
||||
audio.cpp:
|
||||
ifneq ($(AUDIO_CPP_SRC),)
|
||||
ln -sfn $(abspath $(AUDIO_CPP_SRC)) audio.cpp
|
||||
else
|
||||
mkdir -p audio.cpp
|
||||
cd audio.cpp && \
|
||||
git init -q && \
|
||||
git remote add origin $(AUDIO_CPP_REPO) && \
|
||||
git fetch --depth 1 origin $(AUDIO_CPP_VERSION) && \
|
||||
git checkout --detach FETCH_HEAD && \
|
||||
git submodule update --init --recursive --depth 1
|
||||
endif
|
||||
|
||||
grpc-server: audio.cpp
|
||||
mkdir -p $(BUILD_DIR)
|
||||
cd $(BUILD_DIR) && cmake $(CMAKE_ARGS) $(CURRENT_MAKEFILE_DIR)
|
||||
cmake --build $(BUILD_DIR) --config Release \
|
||||
--target audio-cpp-grpc-server -j $(JOBS)
|
||||
cp $(BUILD_DIR)/audio-cpp-grpc-server grpc-server
|
||||
|
||||
test:
|
||||
bash tests/build_contract_test.sh
|
||||
|
||||
test-unit: audio.cpp
|
||||
mkdir -p $(BUILD_DIR)-unit
|
||||
cd $(BUILD_DIR)-unit && cmake $(CMAKE_ARGS) \
|
||||
-DAUDIO_CPP_BUILD_TESTS=ON -DAUDIO_CPP_BUILD_GRPC=OFF \
|
||||
$(CURRENT_MAKEFILE_DIR)
|
||||
cmake --build $(BUILD_DIR)-unit --config Release \
|
||||
--target audio-cpp-runtime-test -j $(JOBS)
|
||||
ctest --test-dir $(BUILD_DIR)-unit --output-on-failure
|
||||
|
||||
clean:
|
||||
rm -rf $(BUILD_DIR) $(BUILD_DIR)-unit grpc-server
|
||||
|
||||
purge: clean
|
||||
rm -rf audio.cpp
|
||||
@@ -1,178 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "audio_cpp_runtime.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace audio_cpp {
|
||||
namespace {
|
||||
|
||||
using engine::runtime::CapabilitySet;
|
||||
using engine::runtime::RunMode;
|
||||
using engine::runtime::TaskSpec;
|
||||
|
||||
const engine::runtime::TaskCapability * find_task_capability(
|
||||
const CapabilitySet & capabilities,
|
||||
const TaskSpec & task) {
|
||||
const auto it = std::find_if(
|
||||
capabilities.supported_tasks.begin(),
|
||||
capabilities.supported_tasks.end(),
|
||||
[&](const engine::runtime::TaskCapability & capability) {
|
||||
return capability.task == task.task;
|
||||
});
|
||||
return it == capabilities.supported_tasks.end() ? nullptr : &*it;
|
||||
}
|
||||
|
||||
void validate_capability(
|
||||
const engine::runtime::ILoadedVoiceModel & model,
|
||||
const AudioCppModelConfig & config) {
|
||||
if (model.metadata().family != config.family) {
|
||||
throw std::runtime_error(
|
||||
"loaded audio.cpp model family '" + model.metadata().family +
|
||||
"' does not match requested family '" + config.family + "'");
|
||||
}
|
||||
|
||||
const auto * capability = find_task_capability(
|
||||
model.capabilities(),
|
||||
config.task);
|
||||
if (capability == nullptr) {
|
||||
throw std::runtime_error(
|
||||
"loaded audio.cpp model does not support requested task '" +
|
||||
std::string(engine::runtime::to_string(config.task.task)) + "'");
|
||||
}
|
||||
if (std::find(
|
||||
capability->modes.begin(),
|
||||
capability->modes.end(),
|
||||
config.task.mode) == capability->modes.end()) {
|
||||
throw std::runtime_error(
|
||||
"loaded audio.cpp model does not support requested mode '" +
|
||||
std::string(engine::runtime::to_string(config.task.mode)) +
|
||||
"' for task '" +
|
||||
std::string(engine::runtime::to_string(config.task.task)) + "'");
|
||||
}
|
||||
}
|
||||
|
||||
void validate_session(
|
||||
const engine::runtime::IVoiceTaskSession & session,
|
||||
const AudioCppModelConfig & config) {
|
||||
if (session.family() != config.family) {
|
||||
throw std::runtime_error("audio.cpp session returned the wrong family");
|
||||
}
|
||||
if (session.task_kind() != config.task.task) {
|
||||
throw std::runtime_error("audio.cpp session returned the wrong task");
|
||||
}
|
||||
if (session.run_mode() != config.task.mode) {
|
||||
throw std::runtime_error("audio.cpp session returned the wrong mode");
|
||||
}
|
||||
if (config.task.mode == RunMode::Offline &&
|
||||
dynamic_cast<const engine::runtime::IOfflineVoiceTaskSession *>(&session) == nullptr) {
|
||||
throw std::runtime_error("audio.cpp session does not implement offline execution");
|
||||
}
|
||||
if (config.task.mode == RunMode::Streaming &&
|
||||
dynamic_cast<const engine::runtime::IStreamingVoiceTaskSession *>(&session) == nullptr) {
|
||||
throw std::runtime_error("audio.cpp session does not implement streaming execution");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
AudioCppRuntime::AudioCppRuntime()
|
||||
: registry_(engine::runtime::make_default_registry()) {}
|
||||
|
||||
AudioCppRuntime::AudioCppRuntime(engine::runtime::ModelRegistry registry)
|
||||
: registry_(std::move(registry)) {}
|
||||
|
||||
AudioCppRuntime::~AudioCppRuntime() {
|
||||
free();
|
||||
}
|
||||
|
||||
void AudioCppRuntime::load(const AudioCppModelConfig & config) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
|
||||
auto candidate_model = registry_.load(config.load);
|
||||
if (candidate_model == nullptr) {
|
||||
throw std::runtime_error("audio.cpp registry returned a null model");
|
||||
}
|
||||
validate_capability(*candidate_model, config);
|
||||
|
||||
auto candidate_session = candidate_model->create_task_session(
|
||||
config.task,
|
||||
config.session);
|
||||
if (candidate_session == nullptr) {
|
||||
throw std::runtime_error("audio.cpp model returned a null session");
|
||||
}
|
||||
validate_session(*candidate_session, config);
|
||||
|
||||
free_locked();
|
||||
model_ = std::move(candidate_model);
|
||||
session_ = std::move(candidate_session);
|
||||
}
|
||||
|
||||
void AudioCppRuntime::free() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
free_locked();
|
||||
}
|
||||
|
||||
engine::runtime::TaskResult AudioCppRuntime::run(
|
||||
const engine::runtime::TaskRequest & request) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
auto & session = require_session_locked();
|
||||
session.prepare(engine::runtime::build_preparation_request(request));
|
||||
return require_offline_locked().run(request);
|
||||
}
|
||||
|
||||
void AudioCppRuntime::start_stream(
|
||||
const engine::runtime::TaskRequest & request) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
auto & session = require_session_locked();
|
||||
session.prepare(engine::runtime::build_preparation_request(request));
|
||||
require_streaming_locked().start_stream(request);
|
||||
}
|
||||
|
||||
engine::runtime::StreamEvent AudioCppRuntime::process_audio_chunk(
|
||||
const engine::runtime::AudioChunk & chunk) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
return require_streaming_locked().process_audio_chunk(chunk);
|
||||
}
|
||||
|
||||
engine::runtime::TaskResult AudioCppRuntime::finish_stream() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
return require_streaming_locked().finish_stream();
|
||||
}
|
||||
|
||||
engine::runtime::IVoiceTaskSession & AudioCppRuntime::require_session_locked() {
|
||||
if (session_ == nullptr) {
|
||||
throw std::runtime_error("audio.cpp runtime has no loaded session");
|
||||
}
|
||||
return *session_;
|
||||
}
|
||||
|
||||
engine::runtime::IOfflineVoiceTaskSession &
|
||||
AudioCppRuntime::require_offline_locked() {
|
||||
auto * offline = dynamic_cast<engine::runtime::IOfflineVoiceTaskSession *>(
|
||||
&require_session_locked());
|
||||
if (offline == nullptr) {
|
||||
throw std::runtime_error("loaded audio.cpp session is not offline");
|
||||
}
|
||||
return *offline;
|
||||
}
|
||||
|
||||
engine::runtime::IStreamingVoiceTaskSession &
|
||||
AudioCppRuntime::require_streaming_locked() {
|
||||
auto * streaming = dynamic_cast<engine::runtime::IStreamingVoiceTaskSession *>(
|
||||
&require_session_locked());
|
||||
if (streaming == nullptr) {
|
||||
throw std::runtime_error("loaded audio.cpp session is not streaming");
|
||||
}
|
||||
return *streaming;
|
||||
}
|
||||
|
||||
void AudioCppRuntime::free_locked() {
|
||||
session_.reset();
|
||||
model_.reset();
|
||||
}
|
||||
|
||||
} // namespace audio_cpp
|
||||
@@ -1,46 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "model_config.h"
|
||||
|
||||
#include "engine/framework/runtime/registry.h"
|
||||
#include "engine/framework/runtime/session.h"
|
||||
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
|
||||
namespace audio_cpp {
|
||||
|
||||
class AudioCppRuntime {
|
||||
public:
|
||||
AudioCppRuntime();
|
||||
explicit AudioCppRuntime(engine::runtime::ModelRegistry registry);
|
||||
~AudioCppRuntime();
|
||||
|
||||
AudioCppRuntime(const AudioCppRuntime &) = delete;
|
||||
AudioCppRuntime & operator=(const AudioCppRuntime &) = delete;
|
||||
|
||||
void load(const AudioCppModelConfig & config);
|
||||
void free();
|
||||
|
||||
engine::runtime::TaskResult run(
|
||||
const engine::runtime::TaskRequest & request);
|
||||
void start_stream(const engine::runtime::TaskRequest & request);
|
||||
engine::runtime::StreamEvent process_audio_chunk(
|
||||
const engine::runtime::AudioChunk & chunk);
|
||||
engine::runtime::TaskResult finish_stream();
|
||||
|
||||
private:
|
||||
engine::runtime::IVoiceTaskSession & require_session_locked();
|
||||
engine::runtime::IOfflineVoiceTaskSession & require_offline_locked();
|
||||
engine::runtime::IStreamingVoiceTaskSession & require_streaming_locked();
|
||||
void free_locked();
|
||||
|
||||
std::mutex mutex_;
|
||||
engine::runtime::ModelRegistry registry_;
|
||||
std::unique_ptr<engine::runtime::ILoadedVoiceModel> model_;
|
||||
std::unique_ptr<engine::runtime::IVoiceTaskSession> session_;
|
||||
};
|
||||
|
||||
} // namespace audio_cpp
|
||||
@@ -1,156 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "model_config.h"
|
||||
|
||||
#include <limits>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
namespace audio_cpp {
|
||||
namespace {
|
||||
|
||||
using engine::core::BackendType;
|
||||
using engine::runtime::RunMode;
|
||||
using engine::runtime::VoiceTaskKind;
|
||||
|
||||
std::string require_option(
|
||||
const std::unordered_map<std::string, std::string> & options,
|
||||
const std::string & name) {
|
||||
const auto it = options.find(name);
|
||||
if (it == options.end() || it->second.empty()) {
|
||||
throw std::invalid_argument("audio.cpp model config requires " + name);
|
||||
}
|
||||
return it->second;
|
||||
}
|
||||
|
||||
VoiceTaskKind parse_task(const std::string & value) {
|
||||
static const std::unordered_map<std::string, VoiceTaskKind> tasks = {
|
||||
{"vad", VoiceTaskKind::Vad},
|
||||
{"asr", VoiceTaskKind::Asr},
|
||||
{"diarization", VoiceTaskKind::Diarization},
|
||||
{"source-separation", VoiceTaskKind::SourceSeparation},
|
||||
{"audio-generation", VoiceTaskKind::AudioGeneration},
|
||||
{"tts", VoiceTaskKind::Tts},
|
||||
{"voice-cloning", VoiceTaskKind::VoiceCloning},
|
||||
{"voice-conversion", VoiceTaskKind::VoiceConversion},
|
||||
{"speech-to-speech", VoiceTaskKind::SpeechToSpeech},
|
||||
{"alignment", VoiceTaskKind::Alignment},
|
||||
{"voice-design", VoiceTaskKind::VoiceDesign},
|
||||
{"speaker-recognition", VoiceTaskKind::SpeakerRecognition},
|
||||
{"svc", VoiceTaskKind::Svc},
|
||||
};
|
||||
const auto it = tasks.find(value);
|
||||
if (it == tasks.end()) {
|
||||
throw std::invalid_argument("unsupported audio.cpp task: " + value);
|
||||
}
|
||||
return it->second;
|
||||
}
|
||||
|
||||
RunMode parse_mode(const std::string & value) {
|
||||
if (value == "offline") {
|
||||
return RunMode::Offline;
|
||||
}
|
||||
if (value == "streaming") {
|
||||
return RunMode::Streaming;
|
||||
}
|
||||
throw std::invalid_argument("unsupported audio.cpp mode: " + value);
|
||||
}
|
||||
|
||||
BackendType parse_backend(const std::string & value) {
|
||||
if (value == "cpu") {
|
||||
return BackendType::Cpu;
|
||||
}
|
||||
if (value == "cuda") {
|
||||
return BackendType::Cuda;
|
||||
}
|
||||
if (value == "vulkan") {
|
||||
return BackendType::Vulkan;
|
||||
}
|
||||
if (value == "metal") {
|
||||
return BackendType::Metal;
|
||||
}
|
||||
if (value == "best") {
|
||||
return BackendType::BestAvailable;
|
||||
}
|
||||
throw std::invalid_argument("unsupported audio.cpp backend: " + value);
|
||||
}
|
||||
|
||||
int parse_integer(
|
||||
const std::string & name,
|
||||
const std::string & value,
|
||||
int minimum) {
|
||||
size_t parsed = 0;
|
||||
long result = 0;
|
||||
try {
|
||||
result = std::stol(value, &parsed);
|
||||
} catch (const std::exception &) {
|
||||
throw std::invalid_argument("invalid audio.cpp " + name + ": " + value);
|
||||
}
|
||||
if (parsed != value.size() ||
|
||||
result < minimum ||
|
||||
result > std::numeric_limits<int>::max()) {
|
||||
throw std::invalid_argument("invalid audio.cpp " + name + ": " + value);
|
||||
}
|
||||
return static_cast<int>(result);
|
||||
}
|
||||
|
||||
void copy_namespaced_option(
|
||||
const std::string & key,
|
||||
const std::string & prefix,
|
||||
const std::string & value,
|
||||
std::unordered_map<std::string, std::string> & destination) {
|
||||
const std::string name = key.substr(prefix.size());
|
||||
if (name.empty()) {
|
||||
throw std::invalid_argument("audio.cpp option namespace requires a name: " + key);
|
||||
}
|
||||
destination[name] = value;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
AudioCppModelConfig parse_model_config(
|
||||
const std::filesystem::path & model_path,
|
||||
const std::unordered_map<std::string, std::string> & options) {
|
||||
AudioCppModelConfig config;
|
||||
config.model_path = model_path;
|
||||
config.family = require_option(options, "family");
|
||||
config.task.task = parse_task(require_option(options, "task"));
|
||||
config.task.mode = RunMode::Offline;
|
||||
config.load.model_path = model_path;
|
||||
config.load.family_hint = config.family;
|
||||
config.session.backend.type = BackendType::Cpu;
|
||||
|
||||
if (const auto it = options.find("mode"); it != options.end()) {
|
||||
config.task.mode = parse_mode(it->second);
|
||||
}
|
||||
if (const auto it = options.find("backend"); it != options.end()) {
|
||||
config.session.backend.type = parse_backend(it->second);
|
||||
}
|
||||
if (const auto it = options.find("device"); it != options.end()) {
|
||||
config.session.backend.device = parse_integer("device", it->second, 0);
|
||||
}
|
||||
if (const auto it = options.find("threads"); it != options.end()) {
|
||||
config.session.backend.threads = parse_integer("threads", it->second, 1);
|
||||
}
|
||||
if (const auto it = options.find("model_spec"); it != options.end()) {
|
||||
config.load.model_spec_override = std::filesystem::path(it->second);
|
||||
}
|
||||
if (const auto it = options.find("config_id"); it != options.end()) {
|
||||
config.load.config_id = it->second;
|
||||
}
|
||||
if (const auto it = options.find("weight_id"); it != options.end()) {
|
||||
config.load.weight_id = it->second;
|
||||
}
|
||||
|
||||
for (const auto & [key, value] : options) {
|
||||
if (key.rfind("load.", 0) == 0) {
|
||||
copy_namespaced_option(key, "load.", value, config.load.options);
|
||||
} else if (key.rfind("session.", 0) == 0) {
|
||||
copy_namespaced_option(key, "session.", value, config.session.options);
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
} // namespace audio_cpp
|
||||
@@ -1,26 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "engine/framework/runtime/model.h"
|
||||
#include "engine/framework/runtime/session.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace audio_cpp {
|
||||
|
||||
struct AudioCppModelConfig {
|
||||
std::filesystem::path model_path;
|
||||
std::string family;
|
||||
engine::runtime::TaskSpec task;
|
||||
engine::runtime::ModelLoadRequest load;
|
||||
engine::runtime::SessionOptions session;
|
||||
};
|
||||
|
||||
AudioCppModelConfig parse_model_config(
|
||||
const std::filesystem::path & model_path,
|
||||
const std::unordered_map<std::string, std::string> & options);
|
||||
|
||||
} // namespace audio_cpp
|
||||
@@ -1,164 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
backend_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
tmp_dir="$(mktemp -d)"
|
||||
trap 'rm -rf "${tmp_dir}"' EXIT
|
||||
|
||||
fixture_dir="${tmp_dir}/audio.cpp"
|
||||
prefix_dir="${tmp_dir}/prefix"
|
||||
tools_dir="${tmp_dir}/tools"
|
||||
mkdir -p "${fixture_dir}" "${prefix_dir}/lib/cmake/Protobuf" \
|
||||
"${prefix_dir}/lib/cmake/gRPC" "${tools_dir}"
|
||||
|
||||
cat >"${fixture_dir}/engine_runtime.cpp" <<'EOF'
|
||||
void audio_cpp_build_contract_fixture() {}
|
||||
EOF
|
||||
|
||||
cat >"${fixture_dir}/CMakeLists.txt" <<'EOF'
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(AudioCppBuildContractFixture LANGUAGES CXX)
|
||||
|
||||
option(ENGINE_ENABLE_CUDA "Build with CUDA" OFF)
|
||||
option(ENGINE_ENABLE_VULKAN "Build with Vulkan" OFF)
|
||||
option(ENGINE_ENABLE_METAL "Build with Metal" OFF)
|
||||
|
||||
foreach(wrong_option IN ITEMS
|
||||
AUDIO_CPP_ENABLE_CUDA
|
||||
AUDIO_CPP_ENABLE_VULKAN
|
||||
AUDIO_CPP_ENABLE_METAL
|
||||
AUDIOCPP_ENABLE_CUDA
|
||||
AUDIOCPP_ENABLE_VULKAN
|
||||
AUDIOCPP_ENABLE_METAL
|
||||
ENGINE_CUDA
|
||||
ENGINE_VULKAN
|
||||
ENGINE_METAL
|
||||
GGML_CUDA
|
||||
GGML_VULKAN
|
||||
GGML_METAL)
|
||||
if(DEFINED ${wrong_option})
|
||||
message(FATAL_ERROR "legacy or unsupported audio.cpp option: ${wrong_option}")
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
add_library(engine_runtime STATIC engine_runtime.cpp)
|
||||
EOF
|
||||
|
||||
cat >"${prefix_dir}/lib/cmake/Protobuf/ProtobufConfig.cmake" <<'EOF'
|
||||
set(Protobuf_FOUND TRUE)
|
||||
set(Protobuf_VERSION 0.0.0)
|
||||
if(NOT TARGET protobuf::libprotobuf)
|
||||
add_library(protobuf::libprotobuf INTERFACE IMPORTED)
|
||||
endif()
|
||||
EOF
|
||||
|
||||
cat >"${prefix_dir}/lib/cmake/gRPC/gRPCConfig.cmake" <<'EOF'
|
||||
set(gRPC_FOUND TRUE)
|
||||
if(NOT TARGET gRPC::grpc++)
|
||||
add_library(gRPC::grpc++ INTERFACE IMPORTED)
|
||||
endif()
|
||||
if(NOT TARGET gRPC::grpc++_reflection)
|
||||
add_library(gRPC::grpc++_reflection INTERFACE IMPORTED)
|
||||
endif()
|
||||
EOF
|
||||
|
||||
cat >"${tools_dir}/protoc" <<'EOF'
|
||||
#!/usr/bin/env sh
|
||||
exit 0
|
||||
EOF
|
||||
cat >"${tools_dir}/grpc_cpp_plugin" <<'EOF'
|
||||
#!/usr/bin/env sh
|
||||
exit 0
|
||||
EOF
|
||||
chmod +x "${tools_dir}/protoc" "${tools_dir}/grpc_cpp_plugin"
|
||||
|
||||
assert_cache_bool() {
|
||||
local cache_file="$1"
|
||||
local name="$2"
|
||||
local expected="$3"
|
||||
grep -q "^${name}:BOOL=${expected}$" "${cache_file}" || {
|
||||
echo "expected ${name}:BOOL=${expected} in ${cache_file}" >&2
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
configure_case() {
|
||||
local name="$1"
|
||||
local cuda="$2"
|
||||
local vulkan="$3"
|
||||
local metal="$4"
|
||||
local build_dir="${tmp_dir}/build-${name}"
|
||||
|
||||
PATH="${tools_dir}:${PATH}" cmake \
|
||||
-S "${backend_dir}" \
|
||||
-B "${build_dir}" \
|
||||
-DCMAKE_PREFIX_PATH="${prefix_dir}" \
|
||||
-DAUDIO_CPP_DIR="${fixture_dir}" \
|
||||
-DENGINE_ENABLE_CUDA="${cuda}" \
|
||||
-DENGINE_ENABLE_VULKAN="${vulkan}" \
|
||||
-DENGINE_ENABLE_METAL="${metal}" \
|
||||
>/dev/null
|
||||
|
||||
assert_cache_bool "${build_dir}/CMakeCache.txt" ENGINE_ENABLE_CUDA "${cuda}"
|
||||
assert_cache_bool "${build_dir}/CMakeCache.txt" ENGINE_ENABLE_VULKAN "${vulkan}"
|
||||
assert_cache_bool "${build_dir}/CMakeCache.txt" ENGINE_ENABLE_METAL "${metal}"
|
||||
|
||||
grep -q 'engine_runtime' \
|
||||
"${build_dir}/CMakeFiles/audio-cpp-grpc-server.dir/link.txt" || {
|
||||
echo "audio-cpp-grpc-server does not link engine_runtime" >&2
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
configure_case cpu OFF OFF OFF
|
||||
configure_case cuda ON OFF OFF
|
||||
configure_case vulkan OFF ON OFF
|
||||
configure_case metal OFF OFF ON
|
||||
|
||||
if PATH="${tools_dir}:${PATH}" cmake \
|
||||
-S "${backend_dir}" \
|
||||
-B "${tmp_dir}/build-wrong-option" \
|
||||
-DCMAKE_PREFIX_PATH="${prefix_dir}" \
|
||||
-DAUDIO_CPP_DIR="${fixture_dir}" \
|
||||
-DGGML_CUDA=ON \
|
||||
>/dev/null 2>&1; then
|
||||
echo "strict audio.cpp fixture accepted legacy GGML_CUDA option" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
make_database="${tmp_dir}/make-database"
|
||||
make -C "${backend_dir}" -pn >"${make_database}"
|
||||
audio_cpp_version="$(
|
||||
sed -n 's/^AUDIO_CPP_VERSION = //p' "${make_database}" | head -n 1
|
||||
)"
|
||||
[[ "${audio_cpp_version}" =~ ^[0-9a-f]{40}$ ]] || {
|
||||
echo "AUDIO_CPP_VERSION must be a pinned 40-character commit" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
fetch_plan="${tmp_dir}/fetch-plan"
|
||||
make -C "${backend_dir}" -Bn audio.cpp >"${fetch_plan}"
|
||||
grep -q 'github.com/0xShug0/audio.cpp' "${fetch_plan}"
|
||||
grep -q "${audio_cpp_version}" "${fetch_plan}"
|
||||
|
||||
cat >"${tools_dir}/uname" <<'EOF'
|
||||
#!/usr/bin/env sh
|
||||
if [ "$#" -eq 1 ] && [ "$1" = "-s" ]; then
|
||||
echo Darwin
|
||||
exit 0
|
||||
fi
|
||||
echo "build contract requires uname -s" >&2
|
||||
exit 64
|
||||
EOF
|
||||
chmod +x "${tools_dir}/uname"
|
||||
|
||||
darwin_plan="${tmp_dir}/darwin-plan"
|
||||
PATH="${tools_dir}:${PATH}" make -C "${backend_dir}" -n \
|
||||
AUDIO_CPP_SRC="${fixture_dir}" grpc-server >"${darwin_plan}"
|
||||
grep -q -- '-DENGINE_ENABLE_CUDA=OFF' "${darwin_plan}"
|
||||
grep -q -- '-DENGINE_ENABLE_VULKAN=OFF' "${darwin_plan}"
|
||||
grep -q -- '-DENGINE_ENABLE_METAL=ON' "${darwin_plan}"
|
||||
|
||||
echo "audio.cpp build contract: PASS"
|
||||
@@ -1,490 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "audio_cpp_runtime.h"
|
||||
#include "model_config.h"
|
||||
|
||||
#include "engine/framework/runtime/model.h"
|
||||
#include "engine/framework/runtime/registry.h"
|
||||
#include "engine/framework/runtime/session.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <exception>
|
||||
#include <filesystem>
|
||||
#include <future>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using engine::runtime::AudioChunk;
|
||||
using engine::runtime::CapabilitySet;
|
||||
using engine::runtime::ILoadedVoiceModel;
|
||||
using engine::runtime::IOfflineVoiceTaskSession;
|
||||
using engine::runtime::IStreamingVoiceTaskSession;
|
||||
using engine::runtime::IVoiceModelLoader;
|
||||
using engine::runtime::IVoiceTaskSession;
|
||||
using engine::runtime::ModelInspection;
|
||||
using engine::runtime::ModelLoadRequest;
|
||||
using engine::runtime::ModelMetadata;
|
||||
using engine::runtime::RunMode;
|
||||
using engine::runtime::SessionOptions;
|
||||
using engine::runtime::SessionPreparationRequest;
|
||||
using engine::runtime::StreamEvent;
|
||||
using engine::runtime::TaskCapability;
|
||||
using engine::runtime::TaskRequest;
|
||||
using engine::runtime::TaskResult;
|
||||
using engine::runtime::TaskSpec;
|
||||
using engine::runtime::VoiceTaskKind;
|
||||
|
||||
void require(bool condition, const std::string & message) {
|
||||
if (!condition) {
|
||||
throw std::runtime_error(message);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename Function>
|
||||
void require_throws(Function && function, const std::string & expected) {
|
||||
try {
|
||||
function();
|
||||
} catch (const std::exception & error) {
|
||||
require(
|
||||
std::string(error.what()).find(expected) != std::string::npos,
|
||||
"expected error containing '" + expected + "', got '" + error.what() + "'");
|
||||
return;
|
||||
}
|
||||
throw std::runtime_error("expected exception containing '" + expected + "'");
|
||||
}
|
||||
|
||||
struct SessionGate {
|
||||
std::mutex mutex;
|
||||
std::condition_variable condition;
|
||||
bool first_entered = false;
|
||||
bool release_first = false;
|
||||
std::atomic<int> entries{0};
|
||||
};
|
||||
|
||||
struct FakeState {
|
||||
std::mutex mutex;
|
||||
std::vector<std::string> events;
|
||||
CapabilitySet capabilities;
|
||||
bool fail_load = false;
|
||||
bool fail_session = false;
|
||||
int generation = 0;
|
||||
std::shared_ptr<SessionGate> gate;
|
||||
|
||||
void record(std::string event) {
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
events.push_back(std::move(event));
|
||||
}
|
||||
};
|
||||
|
||||
class FakeSession final
|
||||
: public IOfflineVoiceTaskSession,
|
||||
public IStreamingVoiceTaskSession {
|
||||
public:
|
||||
FakeSession(
|
||||
std::shared_ptr<FakeState> state,
|
||||
int generation,
|
||||
TaskSpec task,
|
||||
SessionOptions options)
|
||||
: state_(std::move(state)),
|
||||
generation_(generation),
|
||||
task_(task),
|
||||
options_(std::move(options)) {}
|
||||
|
||||
~FakeSession() override {
|
||||
state_->record("session-" + std::to_string(generation_) + "-destroyed");
|
||||
}
|
||||
|
||||
std::string family() const override { return "fake-family"; }
|
||||
VoiceTaskKind task_kind() const override { return task_.task; }
|
||||
RunMode run_mode() const override { return task_.mode; }
|
||||
|
||||
void prepare(const SessionPreparationRequest & request) override {
|
||||
prepared_ = request;
|
||||
}
|
||||
|
||||
TaskResult run(const TaskRequest &) override {
|
||||
if (state_->gate != nullptr) {
|
||||
const int entry = ++state_->gate->entries;
|
||||
if (entry == 1) {
|
||||
std::unique_lock<std::mutex> lock(state_->gate->mutex);
|
||||
state_->gate->first_entered = true;
|
||||
state_->gate->condition.notify_all();
|
||||
state_->gate->condition.wait(
|
||||
lock,
|
||||
[&] { return state_->gate->release_first; });
|
||||
}
|
||||
}
|
||||
|
||||
TaskResult result;
|
||||
result.text_output = engine::runtime::Transcript{
|
||||
"generation-" + std::to_string(generation_),
|
||||
"en",
|
||||
};
|
||||
return result;
|
||||
}
|
||||
|
||||
engine::runtime::StreamingPolicy streaming_policy() const override {
|
||||
engine::runtime::StreamingPolicy policy;
|
||||
policy.input = engine::runtime::StreamingInputKind::AudioChunks;
|
||||
policy.output = engine::runtime::StreamingOutputKind::PullEvents;
|
||||
policy.preferred_audio_chunk_samples = 160;
|
||||
return policy;
|
||||
}
|
||||
|
||||
void start_stream(const TaskRequest &) override {
|
||||
streaming_ = true;
|
||||
}
|
||||
|
||||
std::optional<StreamEvent> next_stream_event() override {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
void set_stream_event_sink(engine::runtime::StreamEventCallback sink) override {
|
||||
sink_ = std::move(sink);
|
||||
}
|
||||
|
||||
TaskResult finish_stream() override {
|
||||
streaming_ = false;
|
||||
return run({});
|
||||
}
|
||||
|
||||
void reset() override {
|
||||
streaming_ = false;
|
||||
}
|
||||
|
||||
StreamEvent process_audio_chunk(const AudioChunk & chunk) override {
|
||||
require(streaming_, "stream was not started");
|
||||
StreamEvent event;
|
||||
event.audio_output = engine::runtime::AudioBuffer{
|
||||
chunk.sample_rate,
|
||||
chunk.channels,
|
||||
chunk.samples,
|
||||
};
|
||||
if (sink_) {
|
||||
sink_(event);
|
||||
}
|
||||
return event;
|
||||
}
|
||||
|
||||
TaskResult finalize() override {
|
||||
streaming_ = false;
|
||||
return run({});
|
||||
}
|
||||
|
||||
private:
|
||||
std::shared_ptr<FakeState> state_;
|
||||
int generation_;
|
||||
TaskSpec task_;
|
||||
SessionOptions options_;
|
||||
SessionPreparationRequest prepared_;
|
||||
engine::runtime::StreamEventCallback sink_;
|
||||
bool streaming_ = false;
|
||||
};
|
||||
|
||||
class FakeLoadedModel final : public ILoadedVoiceModel {
|
||||
public:
|
||||
FakeLoadedModel(std::shared_ptr<FakeState> state, int generation)
|
||||
: state_(std::move(state)),
|
||||
generation_(generation) {
|
||||
metadata_.family = "fake-family";
|
||||
metadata_.variant = "complete-fake";
|
||||
metadata_.description = "complete test implementation";
|
||||
metadata_.config_candidates = {"config.json"};
|
||||
metadata_.weight_candidates = {"weights.gguf"};
|
||||
}
|
||||
|
||||
~FakeLoadedModel() override {
|
||||
state_->record("model-" + std::to_string(generation_) + "-destroyed");
|
||||
}
|
||||
|
||||
const ModelMetadata & metadata() const noexcept override {
|
||||
return metadata_;
|
||||
}
|
||||
|
||||
const CapabilitySet & capabilities() const noexcept override {
|
||||
return state_->capabilities;
|
||||
}
|
||||
|
||||
std::unique_ptr<IVoiceTaskSession> create_task_session(
|
||||
const TaskSpec & task,
|
||||
const SessionOptions & options) const override {
|
||||
if (state_->fail_session) {
|
||||
throw std::runtime_error("session creation failed");
|
||||
}
|
||||
return std::make_unique<FakeSession>(state_, generation_, task, options);
|
||||
}
|
||||
|
||||
private:
|
||||
std::shared_ptr<FakeState> state_;
|
||||
int generation_;
|
||||
ModelMetadata metadata_;
|
||||
};
|
||||
|
||||
class FakeLoader final : public IVoiceModelLoader {
|
||||
public:
|
||||
explicit FakeLoader(std::shared_ptr<FakeState> state)
|
||||
: state_(std::move(state)) {}
|
||||
|
||||
std::string family() const override { return "fake-family"; }
|
||||
|
||||
bool can_load(const ModelLoadRequest & request) const override {
|
||||
return request.family_hint == family();
|
||||
}
|
||||
|
||||
ModelInspection inspect(const ModelLoadRequest & request) const override {
|
||||
ModelInspection inspection;
|
||||
inspection.metadata.family = family();
|
||||
inspection.metadata.variant = "complete-fake";
|
||||
inspection.metadata.description = "complete test loader";
|
||||
inspection.metadata.config_candidates = {"config.json"};
|
||||
inspection.metadata.weight_candidates = {"weights.gguf"};
|
||||
inspection.capabilities = state_->capabilities;
|
||||
inspection.model_root = request.model_path;
|
||||
return inspection;
|
||||
}
|
||||
|
||||
std::unique_ptr<ILoadedVoiceModel> load(
|
||||
const ModelLoadRequest &) const override {
|
||||
if (state_->fail_load) {
|
||||
throw std::runtime_error("model load failed");
|
||||
}
|
||||
const int generation = ++state_->generation;
|
||||
return std::make_unique<FakeLoadedModel>(state_, generation);
|
||||
}
|
||||
|
||||
CapabilitySet advertised_capabilities() const override {
|
||||
return state_->capabilities;
|
||||
}
|
||||
|
||||
std::string advertised_instructions_policy() const override {
|
||||
return "explicit";
|
||||
}
|
||||
|
||||
std::vector<std::string> advertised_api_endpoints() const override {
|
||||
return {"/v1/audio/transcriptions"};
|
||||
}
|
||||
|
||||
private:
|
||||
std::shared_ptr<FakeState> state_;
|
||||
};
|
||||
|
||||
audio_cpp::AudioCppModelConfig offline_asr_config(
|
||||
const std::filesystem::path & model_path) {
|
||||
return audio_cpp::parse_model_config(
|
||||
model_path,
|
||||
{
|
||||
{"family", "fake-family"},
|
||||
{"task", "asr"},
|
||||
{"mode", "offline"},
|
||||
{"backend", "cpu"},
|
||||
{"device", "2"},
|
||||
{"threads", "3"},
|
||||
{"load.cache", "memory"},
|
||||
{"session.language", "en"},
|
||||
});
|
||||
}
|
||||
|
||||
std::unique_ptr<audio_cpp::AudioCppRuntime> make_runtime(
|
||||
const std::shared_ptr<FakeState> & state) {
|
||||
engine::runtime::ModelRegistry registry;
|
||||
registry.register_loader(std::make_shared<FakeLoader>(state));
|
||||
return std::make_unique<audio_cpp::AudioCppRuntime>(std::move(registry));
|
||||
}
|
||||
|
||||
void test_model_config(const std::filesystem::path & model_path) {
|
||||
const auto config = offline_asr_config(model_path);
|
||||
require(config.model_path == model_path, "model path was not preserved");
|
||||
require(config.family == "fake-family", "family was not parsed");
|
||||
require(config.task.task == VoiceTaskKind::Asr, "task was not parsed");
|
||||
require(config.task.mode == RunMode::Offline, "mode was not parsed");
|
||||
require(
|
||||
config.session.backend.type == engine::core::BackendType::Cpu,
|
||||
"backend was not parsed");
|
||||
require(config.session.backend.device == 2, "device was not parsed");
|
||||
require(config.session.backend.threads == 3, "threads were not parsed");
|
||||
require(config.load.options.at("cache") == "memory", "load option prefix was not stripped");
|
||||
require(
|
||||
config.session.options.at("language") == "en",
|
||||
"session option prefix was not stripped");
|
||||
|
||||
require_throws(
|
||||
[&] { audio_cpp::parse_model_config(model_path, {{"task", "asr"}}); },
|
||||
"family");
|
||||
require_throws(
|
||||
[&] { audio_cpp::parse_model_config(model_path, {{"family", "fake-family"}}); },
|
||||
"task");
|
||||
require_throws(
|
||||
[&] {
|
||||
audio_cpp::parse_model_config(
|
||||
model_path,
|
||||
{{"family", "fake-family"}, {"task", "asr"}, {"mode", "batch"}});
|
||||
},
|
||||
"mode");
|
||||
require_throws(
|
||||
[&] {
|
||||
audio_cpp::parse_model_config(
|
||||
model_path,
|
||||
{{"family", "fake-family"}, {"task", "asr"}, {"backend", "tpu"}});
|
||||
},
|
||||
"backend");
|
||||
}
|
||||
|
||||
void test_capability_validation(const std::filesystem::path & model_path) {
|
||||
auto state = std::make_shared<FakeState>();
|
||||
state->capabilities.supported_tasks = {
|
||||
{VoiceTaskKind::Tts, {RunMode::Offline}},
|
||||
};
|
||||
auto runtime = make_runtime(state);
|
||||
|
||||
require_throws(
|
||||
[&] { runtime->load(offline_asr_config(model_path)); },
|
||||
"task");
|
||||
|
||||
state->capabilities.supported_tasks = {
|
||||
{VoiceTaskKind::Asr, {RunMode::Streaming}},
|
||||
};
|
||||
require_throws(
|
||||
[&] { runtime->load(offline_asr_config(model_path)); },
|
||||
"mode");
|
||||
}
|
||||
|
||||
void test_atomic_replacement(const std::filesystem::path & model_path) {
|
||||
auto state = std::make_shared<FakeState>();
|
||||
state->capabilities.supported_tasks = {
|
||||
{VoiceTaskKind::Asr, {RunMode::Offline}},
|
||||
};
|
||||
auto runtime = make_runtime(state);
|
||||
runtime->load(offline_asr_config(model_path));
|
||||
|
||||
state->fail_load = true;
|
||||
require_throws(
|
||||
[&] { runtime->load(offline_asr_config(model_path)); },
|
||||
"model load failed");
|
||||
require(
|
||||
runtime->run({}).text_output->text == "generation-1",
|
||||
"old model was not retained after load failure");
|
||||
|
||||
state->fail_load = false;
|
||||
state->fail_session = true;
|
||||
require_throws(
|
||||
[&] { runtime->load(offline_asr_config(model_path)); },
|
||||
"session creation failed");
|
||||
require(
|
||||
runtime->run({}).text_output->text == "generation-1",
|
||||
"old model was not retained after session creation failure");
|
||||
}
|
||||
|
||||
void test_teardown_order(const std::filesystem::path & model_path) {
|
||||
auto state = std::make_shared<FakeState>();
|
||||
state->capabilities.supported_tasks = {
|
||||
{VoiceTaskKind::Asr, {RunMode::Offline}},
|
||||
};
|
||||
auto runtime = make_runtime(state);
|
||||
runtime->load(offline_asr_config(model_path));
|
||||
runtime->free();
|
||||
|
||||
std::lock_guard<std::mutex> lock(state->mutex);
|
||||
require(state->events.size() == 2, "expected one session and one model teardown");
|
||||
require(
|
||||
state->events[0] == "session-1-destroyed",
|
||||
"session was not destroyed before model");
|
||||
require(
|
||||
state->events[1] == "model-1-destroyed",
|
||||
"model teardown event was not second");
|
||||
}
|
||||
|
||||
void test_runtime_serializes_calls(const std::filesystem::path & model_path) {
|
||||
auto state = std::make_shared<FakeState>();
|
||||
state->capabilities.supported_tasks = {
|
||||
{VoiceTaskKind::Asr, {RunMode::Offline}},
|
||||
};
|
||||
state->gate = std::make_shared<SessionGate>();
|
||||
auto runtime = make_runtime(state);
|
||||
runtime->load(offline_asr_config(model_path));
|
||||
|
||||
auto first = std::async(std::launch::async, [&] { return runtime->run({}); });
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(state->gate->mutex);
|
||||
state->gate->condition.wait(
|
||||
lock,
|
||||
[&] { return state->gate->first_entered; });
|
||||
}
|
||||
|
||||
std::promise<void> release_second;
|
||||
std::shared_future<void> second_barrier = release_second.get_future().share();
|
||||
std::promise<void> second_attempted_promise;
|
||||
auto second_attempted = second_attempted_promise.get_future();
|
||||
auto second = std::async(std::launch::async, [&] {
|
||||
second_barrier.wait();
|
||||
second_attempted_promise.set_value();
|
||||
return runtime->run({});
|
||||
});
|
||||
|
||||
release_second.set_value();
|
||||
second_attempted.wait();
|
||||
require(
|
||||
second.wait_for(std::chrono::milliseconds(50)) == std::future_status::timeout,
|
||||
"second call completed while first call held the runtime");
|
||||
require(
|
||||
state->gate->entries.load() == 1,
|
||||
"second call entered the upstream session concurrently");
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state->gate->mutex);
|
||||
state->gate->release_first = true;
|
||||
}
|
||||
state->gate->condition.notify_all();
|
||||
first.get();
|
||||
second.get();
|
||||
require(state->gate->entries.load() == 2, "second call never reached the session");
|
||||
}
|
||||
|
||||
void test_streaming_surface(const std::filesystem::path & model_path) {
|
||||
auto state = std::make_shared<FakeState>();
|
||||
state->capabilities.supported_tasks = {
|
||||
{VoiceTaskKind::Asr, {RunMode::Streaming}},
|
||||
};
|
||||
auto runtime = make_runtime(state);
|
||||
auto config = offline_asr_config(model_path);
|
||||
config.task.mode = RunMode::Streaming;
|
||||
runtime->load(config);
|
||||
runtime->start_stream({});
|
||||
const auto event = runtime->process_audio_chunk({16000, 1, 0, {0.25f}});
|
||||
require(event.audio_output.has_value(), "streaming chunk result was lost");
|
||||
require(
|
||||
event.audio_output->samples == std::vector<float>{0.25f},
|
||||
"streaming chunk samples changed");
|
||||
require(
|
||||
runtime->finish_stream().text_output->text == "generation-1",
|
||||
"streaming final result was lost");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char ** argv) {
|
||||
try {
|
||||
require(argc == 2, "runtime_test requires an existing model path argument");
|
||||
const std::filesystem::path model_path(argv[1]);
|
||||
test_model_config(model_path);
|
||||
test_capability_validation(model_path);
|
||||
test_atomic_replacement(model_path);
|
||||
test_teardown_order(model_path);
|
||||
test_runtime_serializes_calls(model_path);
|
||||
test_streaming_surface(model_path);
|
||||
std::cout << "audio.cpp runtime unit tests: PASS\n";
|
||||
return 0;
|
||||
} catch (const std::exception & error) {
|
||||
std::cerr << "audio.cpp runtime unit tests: FAIL: " << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -696,6 +696,9 @@ func ChatEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, evaluator
|
||||
template = predInput
|
||||
}
|
||||
thinkingStartToken := reason.DetectThinkingStartToken(template, &config.ReasoningConfig)
|
||||
if config.TemplateConfig.UseTokenizerTemplate {
|
||||
thinkingStartToken = reason.DetectThinkingStartTokenInTemplate(template, &config.ReasoningConfig)
|
||||
}
|
||||
|
||||
xlog.Debug("Thinking start token", "thinkingStartToken", thinkingStartToken, "template", template)
|
||||
|
||||
|
||||
@@ -150,6 +150,9 @@ func processStream(
|
||||
template = s
|
||||
}
|
||||
thinkingStartToken := reason.DetectThinkingStartToken(template, &cfg.ReasoningConfig)
|
||||
if cfg.TemplateConfig.UseTokenizerTemplate {
|
||||
thinkingStartToken = reason.DetectThinkingStartTokenInTemplate(template, &cfg.ReasoningConfig)
|
||||
}
|
||||
extractor := reason.NewReasoningExtractor(thinkingStartToken, cfg.ReasoningConfig)
|
||||
|
||||
// preferAutoparser is sticky: once the C++ autoparser has ever classified
|
||||
@@ -248,6 +251,9 @@ func processStreamWithTools(
|
||||
template = prompt
|
||||
}
|
||||
thinkingStartToken := reason.DetectThinkingStartToken(template, &cfg.ReasoningConfig)
|
||||
if cfg.TemplateConfig.UseTokenizerTemplate {
|
||||
thinkingStartToken = reason.DetectThinkingStartTokenInTemplate(template, &cfg.ReasoningConfig)
|
||||
}
|
||||
extractor := reason.NewReasoningExtractor(thinkingStartToken, cfg.ReasoningConfig)
|
||||
|
||||
result := ""
|
||||
|
||||
@@ -2480,6 +2480,9 @@ func triggerResponseAtTurn(ctx context.Context, session *Session, conv *Conversa
|
||||
template = config.TemplateConfig.Chat
|
||||
}
|
||||
thinkingStartToken := reasoning.DetectThinkingStartToken(template, &config.ReasoningConfig)
|
||||
if config.TemplateConfig.UseTokenizerTemplate {
|
||||
thinkingStartToken = reasoning.DetectThinkingStartTokenInTemplate(template, &config.ReasoningConfig)
|
||||
}
|
||||
|
||||
// When the C++ autoparser emitted ChatDeltas with actionable data,
|
||||
// prefer them — the backend clears Reply.Message in that path and
|
||||
|
||||
@@ -145,6 +145,9 @@ func streamLLMResponse(ctx context.Context, session *Session, conv *Conversation
|
||||
template = llmCfg.TemplateConfig.Chat
|
||||
}
|
||||
thinkingStartToken := reasoning.DetectThinkingStartToken(template, &llmCfg.ReasoningConfig)
|
||||
if llmCfg.TemplateConfig.UseTokenizerTemplate {
|
||||
thinkingStartToken = reasoning.DetectThinkingStartTokenInTemplate(template, &llmCfg.ReasoningConfig)
|
||||
}
|
||||
|
||||
// The autoparser (tokenizer-template path) already delivers reasoning-free
|
||||
// content. Prefilling the thinking start token here would re-tag that clean
|
||||
|
||||
@@ -1359,6 +1359,9 @@ func handleOpenResponsesNonStream(c echo.Context, responseID string, createdAt i
|
||||
template = predInput
|
||||
}
|
||||
thinkingStartToken := reason.DetectThinkingStartToken(template, &cfg.ReasoningConfig)
|
||||
if cfg.TemplateConfig.UseTokenizerTemplate {
|
||||
thinkingStartToken = reason.DetectThinkingStartTokenInTemplate(template, &cfg.ReasoningConfig)
|
||||
}
|
||||
|
||||
// Extract reasoning from result before cleaning
|
||||
reasoningContent, cleanedResult := reason.ExtractReasoningComplete(result, thinkingStartToken, cfg.ReasoningConfig)
|
||||
@@ -1640,6 +1643,9 @@ func handleOpenResponsesStream(c echo.Context, responseID string, createdAt int6
|
||||
template = predInput
|
||||
}
|
||||
thinkingStartToken := reason.DetectThinkingStartToken(template, &cfg.ReasoningConfig)
|
||||
if cfg.TemplateConfig.UseTokenizerTemplate {
|
||||
thinkingStartToken = reason.DetectThinkingStartTokenInTemplate(template, &cfg.ReasoningConfig)
|
||||
}
|
||||
|
||||
// Track state for streaming
|
||||
var currentMessageID string
|
||||
|
||||
@@ -10,33 +10,5 @@ config_file: |
|
||||
- <end_of_turn>
|
||||
- <start_of_turn>
|
||||
template:
|
||||
chat: |
|
||||
{{.Input }}
|
||||
<start_of_turn>model
|
||||
chat_message: |-
|
||||
<start_of_turn>{{if eq .RoleName "assistant" }}model{{else}}{{ .RoleName }}{{end}}
|
||||
{{ if .FunctionCall -}}
|
||||
{{ else if eq .RoleName "tool" -}}
|
||||
{{ end -}}
|
||||
{{ if .Content -}}
|
||||
{{.Content -}}
|
||||
{{ end -}}
|
||||
{{ if .FunctionCall -}}
|
||||
{{toJson .FunctionCall}}
|
||||
{{ end -}}<end_of_turn>
|
||||
completion: |
|
||||
{{.Input}}
|
||||
function: |
|
||||
<start_of_turn>system
|
||||
You have access to functions. If you decide to invoke any of the function(s),
|
||||
you MUST put it in the format of
|
||||
{"name": function name, "parameters": dictionary of argument name and its value}
|
||||
|
||||
You SHOULD NOT include any other text in the response if you call a function
|
||||
{{range .Functions}}
|
||||
{'type': 'function', 'function': {'name': '{{.Name}}', 'description': '{{.Description}}', 'parameters': {{toJson .Parameters}} }}
|
||||
{{end}}
|
||||
<end_of_turn>
|
||||
{{.Input -}}
|
||||
<start_of_turn>model
|
||||
use_tokenizer_template: true
|
||||
name: gemma
|
||||
|
||||
@@ -1,4 +1,230 @@
|
||||
---
|
||||
- &kat-coder-v2-5-dev
|
||||
name: "kat-coder-v2.5-dev"
|
||||
variants:
|
||||
- model: kat-coder-v2.5-dev-q8
|
||||
- model: kat-coder-v2.5-dev-apex-i-quality
|
||||
- model: kat-coder-v2.5-dev-apex-i-balanced
|
||||
- model: kat-coder-v2.5-dev-apex-i-compact
|
||||
- model: kat-coder-v2.5-dev-apex-i-mini
|
||||
- model: kat-coder-v2.5-dev-apex-quality
|
||||
- model: kat-coder-v2.5-dev-apex-balanced
|
||||
- model: kat-coder-v2.5-dev-apex-compact
|
||||
url: "github:mudler/LocalAI/gallery/virtual.yaml@master"
|
||||
urls:
|
||||
- https://huggingface.co/Kwaipilot/KAT-Coder-V2.5-Dev
|
||||
- https://huggingface.co/bartowski/Kwaipilot_KAT-Coder-V2.5-Dev-GGUF
|
||||
- https://huggingface.co/mudler/KAT-Coder-V2.5-Dev-APEX-GGUF
|
||||
description: |
|
||||
KAT-Coder-V2.5-Dev is an Apache-2.0 agentic coding model from Kwaipilot,
|
||||
post-trained from Qwen3.6-35B-A3B. It has 35 billion total parameters with
|
||||
3 billion activated per token, a 262K-token context window, and text-only
|
||||
weights tuned for repository-level coding and tool use.
|
||||
|
||||
This entry offers standard Q4_K_M and Q8_0 GGUF quantizations alongside
|
||||
APEX mixed-precision variants with quality, balanced, compact, and mini
|
||||
profiles. The APEX I-profiles use importance-matrix calibration.
|
||||
license: "apache-2.0"
|
||||
icon: https://huggingface.co/Kwaipilot/KAT-Coder-V2.5-Dev/resolve/main/kat_logo_hd.png
|
||||
tags:
|
||||
- llm
|
||||
- gguf
|
||||
- qwen
|
||||
- coding
|
||||
last_checked: "2026-07-29"
|
||||
overrides:
|
||||
backend: llama-cpp
|
||||
function:
|
||||
automatic_tool_parsing_fallback: true
|
||||
grammar:
|
||||
disable: true
|
||||
known_usecases:
|
||||
- chat
|
||||
options:
|
||||
- use_jinja:true
|
||||
parameters:
|
||||
model: llama-cpp/models/Kwaipilot_KAT-Coder-V2.5-Dev-GGUF/Kwaipilot_KAT-Coder-V2.5-Dev-Q4_K_M.gguf
|
||||
template:
|
||||
use_tokenizer_template: true
|
||||
files:
|
||||
- filename: llama-cpp/models/Kwaipilot_KAT-Coder-V2.5-Dev-GGUF/Kwaipilot_KAT-Coder-V2.5-Dev-Q4_K_M.gguf
|
||||
sha256: 4221c26e5663502d1c96fc901c9967d0e70ce2dcfaa5a9fb9280a46bd19e3c07
|
||||
uri: huggingface://bartowski/Kwaipilot_KAT-Coder-V2.5-Dev-GGUF/Kwaipilot_KAT-Coder-V2.5-Dev-Q4_K_M.gguf
|
||||
- !!merge <<: *kat-coder-v2-5-dev
|
||||
name: "kat-coder-v2.5-dev-q8"
|
||||
variants: []
|
||||
description: |
|
||||
KAT-Coder-V2.5-Dev is an Apache-2.0 agentic coding model from Kwaipilot,
|
||||
post-trained from Qwen3.6-35B-A3B. It has 35 billion total parameters with
|
||||
3 billion activated per token, a 262K-token context window, and text-only
|
||||
weights tuned for repository-level coding and tool use.
|
||||
|
||||
This entry uses the higher-quality Q8_0 GGUF quantization.
|
||||
overrides:
|
||||
backend: llama-cpp
|
||||
function:
|
||||
automatic_tool_parsing_fallback: true
|
||||
grammar:
|
||||
disable: true
|
||||
known_usecases:
|
||||
- chat
|
||||
options:
|
||||
- use_jinja:true
|
||||
parameters:
|
||||
model: llama-cpp/models/Kwaipilot_KAT-Coder-V2.5-Dev-GGUF/Kwaipilot_KAT-Coder-V2.5-Dev-Q8_0.gguf
|
||||
template:
|
||||
use_tokenizer_template: true
|
||||
files:
|
||||
- filename: llama-cpp/models/Kwaipilot_KAT-Coder-V2.5-Dev-GGUF/Kwaipilot_KAT-Coder-V2.5-Dev-Q8_0.gguf
|
||||
sha256: 5fa510f44779b0e3d38a6678985f417a1c65e3000405ca5d6dcf7fd065e47a15
|
||||
uri: huggingface://bartowski/Kwaipilot_KAT-Coder-V2.5-Dev-GGUF/Kwaipilot_KAT-Coder-V2.5-Dev-Q8_0.gguf
|
||||
- !!merge <<: *kat-coder-v2-5-dev
|
||||
name: "kat-coder-v2.5-dev-apex-i-quality"
|
||||
variants: []
|
||||
overrides:
|
||||
backend: llama-cpp
|
||||
function:
|
||||
automatic_tool_parsing_fallback: true
|
||||
grammar:
|
||||
disable: true
|
||||
known_usecases:
|
||||
- chat
|
||||
options:
|
||||
- use_jinja:true
|
||||
parameters:
|
||||
model: llama-cpp/models/KAT-Coder-V2.5-Dev-APEX-GGUF/KAT-Coder-V2.5-Dev-APEX-I-Quality.gguf
|
||||
template:
|
||||
use_tokenizer_template: true
|
||||
files:
|
||||
- filename: llama-cpp/models/KAT-Coder-V2.5-Dev-APEX-GGUF/KAT-Coder-V2.5-Dev-APEX-I-Quality.gguf
|
||||
sha256: e1cf7f33e13ee787a8557effee41eef5261b24696e283ef9d320830ba39f6784
|
||||
uri: huggingface://mudler/KAT-Coder-V2.5-Dev-APEX-GGUF/KAT-Coder-V2.5-Dev-APEX-I-Quality.gguf
|
||||
- !!merge <<: *kat-coder-v2-5-dev
|
||||
name: "kat-coder-v2.5-dev-apex-i-balanced"
|
||||
variants: []
|
||||
overrides:
|
||||
backend: llama-cpp
|
||||
function:
|
||||
automatic_tool_parsing_fallback: true
|
||||
grammar:
|
||||
disable: true
|
||||
known_usecases:
|
||||
- chat
|
||||
options:
|
||||
- use_jinja:true
|
||||
parameters:
|
||||
model: llama-cpp/models/KAT-Coder-V2.5-Dev-APEX-GGUF/KAT-Coder-V2.5-Dev-APEX-I-Balanced.gguf
|
||||
template:
|
||||
use_tokenizer_template: true
|
||||
files:
|
||||
- filename: llama-cpp/models/KAT-Coder-V2.5-Dev-APEX-GGUF/KAT-Coder-V2.5-Dev-APEX-I-Balanced.gguf
|
||||
sha256: ee6e0ec15964c42ba91831d13e9709239f1b74da3dc49dd3edec4ad6aed8029f
|
||||
uri: huggingface://mudler/KAT-Coder-V2.5-Dev-APEX-GGUF/KAT-Coder-V2.5-Dev-APEX-I-Balanced.gguf
|
||||
- !!merge <<: *kat-coder-v2-5-dev
|
||||
name: "kat-coder-v2.5-dev-apex-i-compact"
|
||||
variants: []
|
||||
overrides:
|
||||
backend: llama-cpp
|
||||
function:
|
||||
automatic_tool_parsing_fallback: true
|
||||
grammar:
|
||||
disable: true
|
||||
known_usecases:
|
||||
- chat
|
||||
options:
|
||||
- use_jinja:true
|
||||
parameters:
|
||||
model: llama-cpp/models/KAT-Coder-V2.5-Dev-APEX-GGUF/KAT-Coder-V2.5-Dev-APEX-I-Compact.gguf
|
||||
template:
|
||||
use_tokenizer_template: true
|
||||
files:
|
||||
- filename: llama-cpp/models/KAT-Coder-V2.5-Dev-APEX-GGUF/KAT-Coder-V2.5-Dev-APEX-I-Compact.gguf
|
||||
sha256: 5235ac39e7989d9fcf08078acfa755611d42a1f80060d226e4aba04a3595d0d8
|
||||
uri: huggingface://mudler/KAT-Coder-V2.5-Dev-APEX-GGUF/KAT-Coder-V2.5-Dev-APEX-I-Compact.gguf
|
||||
- !!merge <<: *kat-coder-v2-5-dev
|
||||
name: "kat-coder-v2.5-dev-apex-i-mini"
|
||||
variants: []
|
||||
overrides:
|
||||
backend: llama-cpp
|
||||
function:
|
||||
automatic_tool_parsing_fallback: true
|
||||
grammar:
|
||||
disable: true
|
||||
known_usecases:
|
||||
- chat
|
||||
options:
|
||||
- use_jinja:true
|
||||
parameters:
|
||||
model: llama-cpp/models/KAT-Coder-V2.5-Dev-APEX-GGUF/KAT-Coder-V2.5-Dev-APEX-I-Mini.gguf
|
||||
template:
|
||||
use_tokenizer_template: true
|
||||
files:
|
||||
- filename: llama-cpp/models/KAT-Coder-V2.5-Dev-APEX-GGUF/KAT-Coder-V2.5-Dev-APEX-I-Mini.gguf
|
||||
sha256: 9d901bf1c44946840e15e6f73a66782f56ae2f1a41d6508995ddcd976e9af878
|
||||
uri: huggingface://mudler/KAT-Coder-V2.5-Dev-APEX-GGUF/KAT-Coder-V2.5-Dev-APEX-I-Mini.gguf
|
||||
- !!merge <<: *kat-coder-v2-5-dev
|
||||
name: "kat-coder-v2.5-dev-apex-quality"
|
||||
variants: []
|
||||
overrides:
|
||||
backend: llama-cpp
|
||||
function:
|
||||
automatic_tool_parsing_fallback: true
|
||||
grammar:
|
||||
disable: true
|
||||
known_usecases:
|
||||
- chat
|
||||
options:
|
||||
- use_jinja:true
|
||||
parameters:
|
||||
model: llama-cpp/models/KAT-Coder-V2.5-Dev-APEX-GGUF/KAT-Coder-V2.5-Dev-APEX-Quality.gguf
|
||||
template:
|
||||
use_tokenizer_template: true
|
||||
files:
|
||||
- filename: llama-cpp/models/KAT-Coder-V2.5-Dev-APEX-GGUF/KAT-Coder-V2.5-Dev-APEX-Quality.gguf
|
||||
sha256: 849737baf59183ed518d34f2460f0dcd86190953ccc3213072bdcb1d7f8d2882
|
||||
uri: huggingface://mudler/KAT-Coder-V2.5-Dev-APEX-GGUF/KAT-Coder-V2.5-Dev-APEX-Quality.gguf
|
||||
- !!merge <<: *kat-coder-v2-5-dev
|
||||
name: "kat-coder-v2.5-dev-apex-balanced"
|
||||
variants: []
|
||||
overrides:
|
||||
backend: llama-cpp
|
||||
function:
|
||||
automatic_tool_parsing_fallback: true
|
||||
grammar:
|
||||
disable: true
|
||||
known_usecases:
|
||||
- chat
|
||||
options:
|
||||
- use_jinja:true
|
||||
parameters:
|
||||
model: llama-cpp/models/KAT-Coder-V2.5-Dev-APEX-GGUF/KAT-Coder-V2.5-Dev-APEX-Balanced.gguf
|
||||
template:
|
||||
use_tokenizer_template: true
|
||||
files:
|
||||
- filename: llama-cpp/models/KAT-Coder-V2.5-Dev-APEX-GGUF/KAT-Coder-V2.5-Dev-APEX-Balanced.gguf
|
||||
sha256: 9a5d31b110a95bc085d9851c420dd7a1ffeaec7c9263073c8850d8fb81c4f8ba
|
||||
uri: huggingface://mudler/KAT-Coder-V2.5-Dev-APEX-GGUF/KAT-Coder-V2.5-Dev-APEX-Balanced.gguf
|
||||
- !!merge <<: *kat-coder-v2-5-dev
|
||||
name: "kat-coder-v2.5-dev-apex-compact"
|
||||
variants: []
|
||||
overrides:
|
||||
backend: llama-cpp
|
||||
function:
|
||||
automatic_tool_parsing_fallback: true
|
||||
grammar:
|
||||
disable: true
|
||||
known_usecases:
|
||||
- chat
|
||||
options:
|
||||
- use_jinja:true
|
||||
parameters:
|
||||
model: llama-cpp/models/KAT-Coder-V2.5-Dev-APEX-GGUF/KAT-Coder-V2.5-Dev-APEX-Compact.gguf
|
||||
template:
|
||||
use_tokenizer_template: true
|
||||
files:
|
||||
- filename: llama-cpp/models/KAT-Coder-V2.5-Dev-APEX-GGUF/KAT-Coder-V2.5-Dev-APEX-Compact.gguf
|
||||
sha256: 98dfee53102bbf01e67a768066543a707b40f365ff71e35132ac432c58ad99fd
|
||||
uri: huggingface://mudler/KAT-Coder-V2.5-Dev-APEX-GGUF/KAT-Coder-V2.5-Dev-APEX-Compact.gguf
|
||||
- name: "inkling"
|
||||
url: "github:mudler/LocalAI/gallery/virtual.yaml@master"
|
||||
urls:
|
||||
|
||||
@@ -21,6 +21,17 @@ import (
|
||||
// - [THINK] (Magistral models)
|
||||
// Custom tokens from config are checked first, then default tokens.
|
||||
func DetectThinkingStartToken(prompt string, config *Config) string {
|
||||
return detectThinkingStartToken(prompt, config, true)
|
||||
}
|
||||
|
||||
// DetectThinkingStartTokenInTemplate detects a possible prefill in an
|
||||
// unrendered tokenizer template. Marker ordering cannot reveal which Jinja
|
||||
// branch will render, so matching closing markers are intentionally ignored.
|
||||
func DetectThinkingStartTokenInTemplate(template string, config *Config) string {
|
||||
return detectThinkingStartToken(template, config, false)
|
||||
}
|
||||
|
||||
func detectThinkingStartToken(prompt string, config *Config, honorClosingToken bool) string {
|
||||
// Common thinking start tokens (in order of specificity - longer first)
|
||||
// Based on llama.cpp's chat-parser.cpp implementations
|
||||
defaultTokens := []string{
|
||||
@@ -44,7 +55,8 @@ func DetectThinkingStartToken(prompt string, config *Config) string {
|
||||
// Check if prompt ends with any of these tokens (allowing for trailing whitespace/newlines)
|
||||
trimmedPrompt := strings.TrimRight(prompt, " \t\n\r")
|
||||
for _, token := range thinkingStartTokens {
|
||||
if strings.Contains(trimmedPrompt, token) {
|
||||
if strings.Contains(trimmedPrompt, token) &&
|
||||
(!honorClosingToken || !thinkingTokenClosedAfterLastStart(trimmedPrompt, token, config)) {
|
||||
return token
|
||||
}
|
||||
}
|
||||
@@ -67,6 +79,15 @@ func DetectThinkingStartToken(prompt string, config *Config) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func thinkingTokenClosedAfterLastStart(prompt, startToken string, config *Config) bool {
|
||||
endToken := ClosingTokenForStart(startToken, config)
|
||||
if endToken == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
return strings.LastIndex(prompt, endToken) > strings.LastIndex(prompt, startToken)
|
||||
}
|
||||
|
||||
// ExtractReasoningWithConfig extracts reasoning from content with the given config.
|
||||
// If reasoning is disabled, it returns the original content.
|
||||
// If thinking start token prefill is enabled, it prepends the thinking start token to the content.
|
||||
|
||||
@@ -410,6 +410,29 @@ var _ = Describe("DetectThinkingStartToken", func() {
|
||||
token := DetectThinkingStartToken(prompt, nil)
|
||||
Expect(token).To(Equal("<think>"))
|
||||
})
|
||||
|
||||
It("should ignore a Gemma thinking token that is already closed in the prompt", func() {
|
||||
prompt := "<|turn>model\n<|channel>thought\n<channel|>\n"
|
||||
token := DetectThinkingStartToken(prompt, nil)
|
||||
Expect(token).To(BeEmpty())
|
||||
|
||||
extractor := NewReasoningExtractor(token, Config{})
|
||||
reasoningDelta, contentDelta := extractor.ProcessToken("READY.")
|
||||
Expect(reasoningDelta).To(BeEmpty())
|
||||
Expect(contentDelta).To(Equal("READY."))
|
||||
})
|
||||
|
||||
It("should preserve prefill detection for unrendered conditional templates", func() {
|
||||
template := "{% if enable_thinking %}<think>{% else %}<think></think>{% endif %}"
|
||||
token := DetectThinkingStartTokenInTemplate(template, nil)
|
||||
Expect(token).To(Equal("<think>"))
|
||||
})
|
||||
|
||||
It("should ignore user Jinja text before a preclosed Gemma prompt suffix", func() {
|
||||
prompt := "Explain {{ variable }}\n<|turn>model\n<|channel>thought\n<channel|>\n"
|
||||
token := DetectThinkingStartToken(prompt, nil)
|
||||
Expect(token).To(BeEmpty())
|
||||
})
|
||||
})
|
||||
|
||||
Context("when prompt does not contain thinking tokens", func() {
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
getAllBackendPaths,
|
||||
filterMatrix,
|
||||
BACKEND_MATRIX_FILE,
|
||||
BACKEND_PROTO_FILE,
|
||||
} from "./lib/backend-filter.mjs";
|
||||
|
||||
// Matrix data lives in a small data-only YAML so both backend.yml (master push)
|
||||
@@ -116,6 +117,42 @@ async function getPreviousMatrix(event) {
|
||||
}
|
||||
}
|
||||
|
||||
// backend.proto at the base revision plus the checked-out copy, so filterMatrix
|
||||
// can tell an additive edit (a new field, message or RPC, which invalidates no
|
||||
// existing image) from a breaking one. Returning null means "rebuild
|
||||
// everything", the same posture as an unresolvable matrix diff.
|
||||
//
|
||||
// Only called when the changed-file list names the proto, so the common path
|
||||
// costs no extra API request.
|
||||
async function getProtoRevisions(event) {
|
||||
const ref = event.pull_request ? event.pull_request.base.sha : event.before;
|
||||
if (!ref || /^0+$/.test(ref)) return null;
|
||||
const owner = event.repository.owner.login;
|
||||
const repo = event.repository.name;
|
||||
try {
|
||||
const res = await octokit.request('GET /repos/{owner}/{repo}/contents/{path}', {
|
||||
owner,
|
||||
repo,
|
||||
path: BACKEND_PROTO_FILE,
|
||||
ref,
|
||||
mediaType: { format: 'raw' },
|
||||
});
|
||||
const previous = typeof res.data === 'string'
|
||||
? res.data
|
||||
: Buffer.from(res.data.content, 'base64').toString('utf8');
|
||||
return {
|
||||
previous,
|
||||
current: fs.readFileSync(BACKEND_PROTO_FILE, "utf8"),
|
||||
};
|
||||
} catch (err) {
|
||||
console.log(
|
||||
`could not read ${BACKEND_PROTO_FILE} at ${ref}, falling back to run-all:`,
|
||||
err.message
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Group matrix entries by tag-suffix and emit a merge-matrix entry per group.
|
||||
// Both multi-leg groups (per-arch fan-out) and singletons get one entry each:
|
||||
// the build job pushes by digest only with no tags applied, so every backend
|
||||
@@ -240,7 +277,7 @@ function emitFullMatrix() {
|
||||
}
|
||||
}
|
||||
|
||||
function emitFilteredMatrix(changedFiles, previousMatrix) {
|
||||
function emitFilteredMatrix(changedFiles, previousMatrix, protoRevisions) {
|
||||
console.log("Changed files:", changedFiles);
|
||||
|
||||
const { filtered, filteredDarwin, changedBackends } = filterMatrix({
|
||||
@@ -248,6 +285,7 @@ function emitFilteredMatrix(changedFiles, previousMatrix) {
|
||||
includesDarwin,
|
||||
changedFiles,
|
||||
previousMatrix,
|
||||
protoRevisions,
|
||||
});
|
||||
|
||||
console.log("Filtered files:", filtered);
|
||||
@@ -306,5 +344,9 @@ function emitFilteredMatrix(changedFiles, previousMatrix) {
|
||||
? await getPreviousMatrix(event)
|
||||
: null;
|
||||
|
||||
emitFilteredMatrix(changedFiles, previousMatrix);
|
||||
const protoRevisions = changedFiles.includes(BACKEND_PROTO_FILE)
|
||||
? await getProtoRevisions(event)
|
||||
: null;
|
||||
|
||||
emitFilteredMatrix(changedFiles, previousMatrix, protoRevisions);
|
||||
})();
|
||||
|
||||
@@ -178,6 +178,126 @@ const GO_BACKEND_PKG_PREFIXES = [
|
||||
"pkg/utils/",
|
||||
];
|
||||
|
||||
export const BACKEND_PROTO_FILE = "backend/backend.proto";
|
||||
const PROTO_RULE_ID = "backend-proto";
|
||||
|
||||
// Split a .proto into a map of symbol -> fingerprint so two revisions can be
|
||||
// compared structurally instead of textually. A comment reflow, a reindent or a
|
||||
// reordered field must not read as a change, and a renumbered or retyped field
|
||||
// must.
|
||||
//
|
||||
// The scanner is deliberately syntax-light: it tracks brace depth to build a
|
||||
// container path and records one entry per declaration (`message`, `enum`,
|
||||
// `service`, `oneof`, `rpc`) and one per statement (fields, enum values,
|
||||
// `option`, `reserved`). It never needs to understand types, only to notice
|
||||
// when the text describing one stops being identical.
|
||||
function protoSymbols(text) {
|
||||
const symbols = new Map();
|
||||
const stack = [];
|
||||
const norm = s => s.trim().replace(/\s+/g, " ");
|
||||
|
||||
// A declaration is identified by its kind and name, so that changing its body
|
||||
// shows up as changed members rather than as a wholesale replacement.
|
||||
const declKey = header => {
|
||||
const rpc = header.match(/^rpc\s+([A-Za-z_]\w*)/);
|
||||
if (rpc) return `rpc ${rpc[1]}`;
|
||||
const decl = header.match(/^(message|enum|service|oneof|extend)\s+([A-Za-z_]\w*)/);
|
||||
if (decl) return `${decl[1]} ${decl[2]}`;
|
||||
return header;
|
||||
};
|
||||
|
||||
// `bool cache_prompt = 8` is identified by `cache_prompt`, so renumbering or
|
||||
// retyping it changes the fingerprint under a stable key, while renaming it
|
||||
// reads as a removal plus an addition. Statements with no `=` (`reserved 4;`)
|
||||
// are their own identity.
|
||||
const stmtKey = stmt => {
|
||||
const eq = stmt.indexOf("=");
|
||||
if (eq === -1) return stmt;
|
||||
const lhs = norm(stmt.slice(0, eq)).split(" ");
|
||||
return lhs[lhs.length - 1] || stmt;
|
||||
};
|
||||
|
||||
let buf = "";
|
||||
let i = 0;
|
||||
while (i < text.length) {
|
||||
const c = text[i];
|
||||
|
||||
// String literals first: `option go_package = "github.com/..."` contains a
|
||||
// `//` that is not a comment.
|
||||
if (c === '"' || c === "'") {
|
||||
buf += c;
|
||||
i++;
|
||||
while (i < text.length) {
|
||||
if (text[i] === "\\") {
|
||||
buf += text.slice(i, i + 2);
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
buf += text[i];
|
||||
i++;
|
||||
if (text[i - 1] === c) break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (c === "/" && text[i + 1] === "/") {
|
||||
while (i < text.length && text[i] !== "\n") i++;
|
||||
continue;
|
||||
}
|
||||
if (c === "/" && text[i + 1] === "*") {
|
||||
i += 2;
|
||||
while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++;
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
if (c === "{") {
|
||||
const header = norm(buf);
|
||||
buf = "";
|
||||
i++;
|
||||
const key = header ? declKey(header) : "";
|
||||
if (header) symbols.set(`${stack.join("/")}|${key}`, header);
|
||||
stack.push(key);
|
||||
continue;
|
||||
}
|
||||
if (c === "}") {
|
||||
stack.pop();
|
||||
buf = "";
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (c === ";") {
|
||||
const stmt = norm(buf);
|
||||
buf = "";
|
||||
i++;
|
||||
if (stmt) symbols.set(`${stack.join("/")}|${stmtKey(stmt)}`, stmt);
|
||||
continue;
|
||||
}
|
||||
buf += c;
|
||||
i++;
|
||||
}
|
||||
return symbols;
|
||||
}
|
||||
|
||||
// True when every symbol the previous revision declared survives unchanged into
|
||||
// the current one. New symbols are free: a backend that never references a new
|
||||
// field, message or RPC produces the same behavior with or without it, so its
|
||||
// image does not need rebuilding.
|
||||
//
|
||||
// Anything else (a removed, renumbered, retyped or renamed field, a dropped
|
||||
// RPC, a changed option) is treated as breaking and rebuilds the full matrix.
|
||||
// Either text being unresolvable is breaking too, matching the run-all posture
|
||||
// changed-backends.js takes for a diff it cannot compute.
|
||||
export function protoChangeIsAdditive(previousText, currentText) {
|
||||
if (typeof previousText !== "string" || typeof currentText !== "string") {
|
||||
return false;
|
||||
}
|
||||
const before = protoSymbols(previousText);
|
||||
const after = protoSymbols(currentText);
|
||||
for (const [key, fingerprint] of before) {
|
||||
if (after.get(key) !== fingerprint) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Shared build inputs: files that end up in, or decide the contents of, images
|
||||
// belonging to backends whose own directory they do not live under. The
|
||||
// per-backend prefix match in filterMatrix() structurally cannot see these, so
|
||||
@@ -200,7 +320,14 @@ export const SHARED_BUILD_INPUTS = [
|
||||
// backends regenerate their stubs from it via `make protogen-go`, the C++
|
||||
// CMakeLists compile backend.pb.cc from it, and the Rust crate Makefile
|
||||
// copies it in before build.rs runs.
|
||||
matches: file => file === "backend/backend.proto",
|
||||
//
|
||||
// Which is why this rule is always/always, and why it is the single most
|
||||
// expensive entry in the table: 417 Linux plus 56 Darwin builds. It fires
|
||||
// on 1.3% of commits (10 of 767 over six months), and every one of those
|
||||
// ten was purely additive. filterMatrix() suppresses this rule for an
|
||||
// additive-only diff, so `id` exists to identify it there.
|
||||
id: PROTO_RULE_ID,
|
||||
matches: file => file === BACKEND_PROTO_FILE,
|
||||
linux: always,
|
||||
darwin: always,
|
||||
},
|
||||
@@ -362,8 +489,19 @@ export function filterMatrix({
|
||||
includesDarwin,
|
||||
changedFiles,
|
||||
previousMatrix,
|
||||
protoRevisions,
|
||||
}) {
|
||||
const sharedRules = matchedSharedRules(changedFiles);
|
||||
// An additive-only backend.proto edit invalidates no existing image, so drop
|
||||
// its always/always rule. Every other matched rule still applies: a PR that
|
||||
// touches the proto and scripts/build/ is still a full rebuild.
|
||||
const protoAdditiveOnly =
|
||||
changedFiles.includes(BACKEND_PROTO_FILE) &&
|
||||
!!protoRevisions &&
|
||||
protoChangeIsAdditive(protoRevisions.previous, protoRevisions.current);
|
||||
|
||||
const sharedRules = matchedSharedRules(changedFiles).filter(
|
||||
rule => !(rule.id === PROTO_RULE_ID && protoAdditiveOnly)
|
||||
);
|
||||
|
||||
const matrixFileChanged = changedFiles.includes(BACKEND_MATRIX_FILE);
|
||||
// The matrix file changed but we could not resolve what it used to say (API
|
||||
|
||||
@@ -385,3 +385,146 @@ test("an unavailable previous matrix conservatively rebuilds everything", () =>
|
||||
assert.equal(filtered.length, includes.length);
|
||||
assert.equal(filteredDarwin.length, includesDarwin.length);
|
||||
});
|
||||
|
||||
// --- backend.proto: additive changes must not rebuild the world -------------
|
||||
//
|
||||
// backend/backend.proto is consumed by every language, so the SHARED_BUILD_INPUTS
|
||||
// rule for it is always/always: a full 417-entry Linux matrix plus all 56 Darwin
|
||||
// entries. It fires on 1.3% of commits, and in practice every one of those has
|
||||
// been purely additive (a new field with an unused number, a new message, a new
|
||||
// RPC). Adding `bool cache_prompt = 8;` (PR #11158) cannot change the behavior of
|
||||
// a backend that never reads it, yet it rebuilt all 473 images.
|
||||
//
|
||||
// So the rule becomes content-aware rather than path-aware, using the same shape
|
||||
// as previousMatrix above: changed-backends.js resolves the base revision and
|
||||
// hands the two texts in, and everything here stays pure.
|
||||
|
||||
const protoWith = body => `
|
||||
syntax = "proto3";
|
||||
|
||||
package backend;
|
||||
|
||||
service Backend {
|
||||
rpc Health(HealthMessage) returns (Reply) {}
|
||||
rpc Predict(PredictOptions) returns (Reply) {}
|
||||
}
|
||||
|
||||
message HealthMessage {}
|
||||
|
||||
message PredictOptions {
|
||||
${body}
|
||||
}
|
||||
`;
|
||||
|
||||
const BASE_FIELDS = ` string Prompt = 1;
|
||||
int32 Tokens = 2;
|
||||
bool UseTokenizerTemplate = 3;`;
|
||||
|
||||
const runProto = (previous, current) =>
|
||||
filterMatrix({
|
||||
includes,
|
||||
includesDarwin,
|
||||
changedFiles: ["backend/backend.proto"],
|
||||
protoRevisions: { previous, current },
|
||||
});
|
||||
|
||||
test("an added proto field rebuilds nothing", () => {
|
||||
// The real PR #11158 diff: six added lines, one new field, unused number.
|
||||
const { filtered, filteredDarwin, changedBackends } = runProto(
|
||||
protoWith(BASE_FIELDS),
|
||||
protoWith(`${BASE_FIELDS}\n bool cache_prompt = 8;`)
|
||||
);
|
||||
|
||||
assert.deepEqual(filtered, []);
|
||||
assert.deepEqual(filteredDarwin, []);
|
||||
assert.equal(changedBackends.size, 0);
|
||||
});
|
||||
|
||||
test("an added proto message and RPC rebuild nothing", () => {
|
||||
const current = protoWith(BASE_FIELDS).replace(
|
||||
"message HealthMessage {}",
|
||||
"message HealthMessage {}\n\nmessage ScoreRequest {\n string Text = 1;\n}"
|
||||
).replace(
|
||||
" rpc Predict(PredictOptions) returns (Reply) {}",
|
||||
" rpc Predict(PredictOptions) returns (Reply) {}\n rpc Score(ScoreRequest) returns (Reply) {}"
|
||||
);
|
||||
|
||||
const { filtered, filteredDarwin } = runProto(protoWith(BASE_FIELDS), current);
|
||||
|
||||
assert.deepEqual(filtered, []);
|
||||
assert.deepEqual(filteredDarwin, []);
|
||||
});
|
||||
|
||||
test("a removed proto field rebuilds every backend on every OS", () => {
|
||||
const { filtered, filteredDarwin } = runProto(
|
||||
protoWith(BASE_FIELDS),
|
||||
protoWith(` string Prompt = 1;\n bool UseTokenizerTemplate = 3;`)
|
||||
);
|
||||
|
||||
assert.equal(filtered.length, includes.length);
|
||||
assert.equal(filteredDarwin.length, includesDarwin.length);
|
||||
});
|
||||
|
||||
test("a renumbered proto field rebuilds every backend on every OS", () => {
|
||||
// Wire-incompatible: an old backend reading field 2 gets nothing.
|
||||
const { filtered, filteredDarwin } = runProto(
|
||||
protoWith(BASE_FIELDS),
|
||||
protoWith(` string Prompt = 1;\n int32 Tokens = 9;\n bool UseTokenizerTemplate = 3;`)
|
||||
);
|
||||
|
||||
assert.equal(filtered.length, includes.length);
|
||||
assert.equal(filteredDarwin.length, includesDarwin.length);
|
||||
});
|
||||
|
||||
test("a retyped proto field rebuilds every backend on every OS", () => {
|
||||
const { filtered, filteredDarwin } = runProto(
|
||||
protoWith(BASE_FIELDS),
|
||||
protoWith(` string Prompt = 1;\n int64 Tokens = 2;\n bool UseTokenizerTemplate = 3;`)
|
||||
);
|
||||
|
||||
assert.equal(filtered.length, includes.length);
|
||||
assert.equal(filteredDarwin.length, includesDarwin.length);
|
||||
});
|
||||
|
||||
test("a renamed proto field rebuilds every backend on every OS", () => {
|
||||
// Same number and type, but every generated accessor changes name.
|
||||
const { filtered, filteredDarwin } = runProto(
|
||||
protoWith(BASE_FIELDS),
|
||||
protoWith(` string Prompt = 1;\n int32 MaxTokens = 2;\n bool UseTokenizerTemplate = 3;`)
|
||||
);
|
||||
|
||||
assert.equal(filtered.length, includes.length);
|
||||
assert.equal(filteredDarwin.length, includesDarwin.length);
|
||||
});
|
||||
|
||||
test("a removed proto RPC rebuilds every backend on every OS", () => {
|
||||
const { filtered, filteredDarwin } = runProto(
|
||||
protoWith(BASE_FIELDS),
|
||||
protoWith(BASE_FIELDS).replace(
|
||||
" rpc Predict(PredictOptions) returns (Reply) {}\n",
|
||||
""
|
||||
)
|
||||
);
|
||||
|
||||
assert.equal(filtered.length, includes.length);
|
||||
assert.equal(filteredDarwin.length, includesDarwin.length);
|
||||
});
|
||||
|
||||
test("a comment-only proto change rebuilds nothing", () => {
|
||||
const { filtered, filteredDarwin } = runProto(
|
||||
protoWith(BASE_FIELDS),
|
||||
protoWith(` string Prompt = 1;\n // how many tokens to emit\n int32 Tokens = 2;\n bool UseTokenizerTemplate = 3;`)
|
||||
);
|
||||
|
||||
assert.deepEqual(filtered, []);
|
||||
assert.deepEqual(filteredDarwin, []);
|
||||
});
|
||||
|
||||
test("unresolvable proto revisions conservatively rebuild everything", () => {
|
||||
// Same posture as the previousMatrix fallback: if we cannot resolve what the
|
||||
// proto used to say, we must not claim the change was additive.
|
||||
const { filtered, filteredDarwin } = runProto(null, protoWith(BASE_FIELDS));
|
||||
|
||||
assert.equal(filtered.length, includes.length);
|
||||
assert.equal(filteredDarwin.length, includesDarwin.length);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user