Compare commits

..
Author SHA1 Message Date
Georgi Gerganov 1511ce3bc3 sync : ggml 2026-08-18 11:30:03 +03:00
Georgi Gerganov da786dc23e ggml : bump version to 0.20.2 (ggml/1589) 2026-08-18 11:30:03 +03:00
Georgi GerganovandGitHub 27e345b574 build : fix xcframework + cmake clean-up (#27304)
* xcframework : fix build

* mtmd : remove unused include path

* vendor : use vendor::hash alias target in cmake

CMake reserves "::" in target names for imported/alias targets, so the real
target keeps the name vendor-hash and a vendor::hash ALIAS target is added.
Consumers (mtmd, llama-gguf-hash) now link against the namespaced alias.

Assisted-by: pi:llama.cpp/Qwen3.8-27B

* vendor : add cmake targets for all vendored libs with vendor:: aliases

Add INTERFACE targets for the header-only vendor libs (miniaudio, nlohmann,
sheredom, stb) and ALIAS targets named vendor::<lib> for all of them,
including cpp-httplib and hash. Each exposes the vendor/ root so includes
are namespaced, e.g. <nlohmann/json.hpp>.

Consolidate the per-lib add_subdirectory calls into a single
add_subdirectory(vendor), keeping the cpp-httplib gate on LLAMA_BUILD_COMMON.
Consumers (llama-common, mtmd) now link the aliases instead of relying on
raw vendor/ include paths.

hash: consumers now include via "hash/hash.h"; the vendor/hash dir is kept
as a PRIVATE include so the synced upstream sources compile unmodified.

Assisted-by: pi:llama.cpp/Qwen3.8-27B

* readme : use foo/bar names in acknowledgements

Assisted-by: pi:llama.cpp/Qwen3.8-27B

* ocd : fix valign
2026-08-18 11:16:51 +03:00
Georgi GerganovandGitHub 8b86400975 ci : create pre-release with change log and nightly link in make-release (#27302)
* ci : create pre-release with change log and nightly link in make-release

After pushing the tag, create a pre-release using
ggml-org/action-create-release. The release description is generated by
scripts/make-release-desc.sh: the change log between the current and
previous version (one line per commit), a link to the corresponding
nightly build when it exists, and a note that semantic versioning is
still work in progress.

Assisted-by: pi:llama.cpp/Qwen3.8-27B

* cmake : bump version to 0.1.2

Assisted-by: pi:llama.cpp/Qwen3.8-27B

* ci : find the nightly tag by commit in make-release-desc.sh

The nightly release is guaranteed by the release checks to point at HEAD,
so instead of reconstructing its name (commit count, branch, hash) just
pick the b* tag pointing at HEAD. This also drops the RELEASE_BRANCH env
var from the workflow.

Assisted-by: pi:llama.cpp/Qwen3.8-27B

* ci : resolve the release commit from the version tag in make-release-desc.sh

The change log and nightly lookup now use the commit the version tag
points at (HEAD when the tag does not exist), instead of always HEAD.
This makes the script usable locally for older versions, e.g.
./scripts/make-release-desc.sh v0.1.1. The tag is resolved to a SHA
first, since --points-at does not peel annotated tags.

Assisted-by: pi:llama.cpp/Qwen3.8-27B

* ci : normalize the version argument in make-release-desc.sh

Accept the version with or without the leading v (0.1.1 == v0.1.1) and
reject anything else, instead of silently treating a bare version as a
non-existent tag name.

Assisted-by: pi:llama.cpp/Qwen3.8-27B

* cont : clean-up
2026-08-18 10:14:41 +03:00
ynankaniandGitHub 25ae3a9b33 CUDA: MMVQ nwarps=8 for bs=1 for dense models on DGX Spark (#26843)
* CUDA: MMVQ nwarps=8 for bs=1 for dense models on DGX Spark

Signed-off-by: ynankani <ynankani@nvidia.com>

* skip moe experts and allow others based on k geometry (allow only small idle tail)

Signed-off-by: ynankani <ynankani@nvidia.com>

* rename MMVQ DGX Spark params to GB10 and fix MSVC constexpr lambda capture

Signed-off-by: ynankani <ynankani@nvidia.com>

---------

Signed-off-by: ynankani <ynankani@nvidia.com>
2026-08-18 09:45:53 +05:30
Aleksander GrygierandGitHub 01818e4956 ui: enforce alphabetical enum member ordering (#27272) 2026-08-17 23:52:00 +02:00
Aleksander GrygierandGitHub 0021a77de0 ui: Refactor Built-In Tools naming (Server/Browser) (#27271)
* server: rename built-in tools to server tools

* ui: rename built-in tools to server/browser tools
2026-08-17 22:23:22 +02:00
EveandGitHub 058df671b2 ci: more optimizations (#26983)
* replace rpc job with cpu

* remove vulkan cache

* move windows to build vulkan
2026-08-17 19:59:40 +00:00
102 changed files with 891 additions and 1857 deletions
@@ -1,20 +0,0 @@
name: "Linux - Setup Vulkan SDK"
description: "Setup Vulkan SDK for Linux"
inputs:
path:
description: "Installation path"
required: true
version:
description: "Vulkan SDK version"
required: true
runs:
using: "composite"
steps:
- name: Setup Vulkan SDK
id: setup
uses: ./.github/actions/unarchive-tar
with:
url: https://sdk.lunarg.com/sdk/download/${{ inputs.version }}/linux/vulkan_sdk.tar.xz
path: ${{ inputs.path }}
strip: 1
-27
View File
@@ -10,33 +10,6 @@ concurrency:
cancel-in-progress: true
jobs:
ubuntu-24-vulkan-cache:
runs-on: ubuntu-24.04
steps:
- name: Clone
id: checkout
uses: actions/checkout@v6
- name: Get latest Vulkan SDK version
id: vulkan_sdk_version
run: |
echo "VULKAN_SDK_VERSION=$(curl https://vulkan.lunarg.com/sdk/latest/linux.txt)" >> "$GITHUB_ENV"
- name: Setup Cache
uses: actions/cache@v5
id: cache-sdk
with:
path: ./vulkan_sdk
key: cache-gha-vulkan-sdk-${{ env.VULKAN_SDK_VERSION }}-${{ runner.os }}
- name: Setup Vulkan SDK
if: steps.cache-sdk.outputs.cache-hit != 'true'
uses: ./.github/actions/linux-setup-vulkan
with:
path: ./vulkan_sdk
version: ${{ env.VULKAN_SDK_VERSION }}
#ubuntu-24-spacemit-cache:
# runs-on: ubuntu-24.04
+1 -13
View File
@@ -21,6 +21,7 @@ on:
paths: [
'.github/workflows/build-cpu.yml',
'.github/workflows/build-cmake-pkg.yml',
'ggml/src/ggml-rpc/**',
'**/CMakeLists.txt',
'**/.cmake',
'**/*.h',
@@ -123,7 +124,6 @@ jobs:
env:
OPENBLAS_VERSION: 0.3.23
SDE_VERSION: 9.33.0-2024-01-07
VULKAN_VERSION: 1.4.357.0
strategy:
matrix:
@@ -134,9 +134,6 @@ jobs:
- build: 'x64-openblas'
arch: 'x64'
defines: '-G "Ninja Multi-Config" -D CMAKE_TOOLCHAIN_FILE=cmake/x64-windows-llvm.cmake -DGGML_NATIVE=OFF -DLLAMA_BUILD_SERVER=ON -DGGML_RPC=ON -DGGML_BACKEND_DL=ON -DGGML_CPU_ALL_VARIANTS=ON -DGGML_OPENMP=OFF -DGGML_BLAS=ON -DGGML_BLAS_VENDOR=OpenBLAS -DBLAS_INCLUDE_DIRS="$env:RUNNER_TEMP/openblas/include" -DBLAS_LIBRARIES="$env:RUNNER_TEMP/openblas/lib/openblas.lib"'
- build: 'x64-vulkan'
arch: 'x64'
defines: '-G "Ninja Multi-Config" -D CMAKE_TOOLCHAIN_FILE=cmake/x64-windows-llvm.cmake -DCMAKE_BUILD_TYPE=Release -DGGML_NATIVE=OFF -DLLAMA_BUILD_SERVER=ON -DGGML_RPC=ON -DGGML_BACKEND_DL=ON -DGGML_CPU_ALL_VARIANTS=ON -DGGML_VULKAN=ON'
- build: 'arm64'
arch: 'arm64'
defines: '-G "Ninja Multi-Config" -D CMAKE_TOOLCHAIN_FILE=cmake/arm64-windows-llvm.cmake -DGGML_NATIVE=OFF -DLLAMA_BUILD_SERVER=ON'
@@ -167,15 +164,6 @@ jobs:
$lib = $(join-path $msvc 'bin\Hostx64\x64\lib.exe')
& $lib /machine:x64 "/def:${env:RUNNER_TEMP}/openblas/lib/libopenblas.def" "/out:${env:RUNNER_TEMP}/openblas/lib/openblas.lib" /name:openblas.dll
- name: Install Vulkan SDK
id: get_vulkan
if: ${{ matrix.build == 'x64-vulkan' }}
run: |
curl.exe -o $env:RUNNER_TEMP/VulkanSDK-Installer.exe -L "https://sdk.lunarg.com/sdk/download/${env:VULKAN_VERSION}/windows/vulkansdk-windows-X64-${env:VULKAN_VERSION}.exe"
& "$env:RUNNER_TEMP\VulkanSDK-Installer.exe" --accept-licenses --default-answer --confirm-command install
Add-Content $env:GITHUB_ENV "VULKAN_SDK=C:\VulkanSDK\${env:VULKAN_VERSION}"
Add-Content $env:GITHUB_PATH "C:\VulkanSDK\${env:VULKAN_VERSION}\bin"
- name: Install Ninja
id: install_ninja
run: |
-66
View File
@@ -1,66 +0,0 @@
name: CI (rpc)
on:
workflow_dispatch: # allows manual triggering
push:
branches:
- master
paths: [
'.github/workflows/build-rpc.yml',
'**/CMakeLists.txt',
'**/.cmake',
'**/*.h',
'**/*.hpp',
'**/*.c',
'**/*.cpp'
]
pull_request:
types: [opened, synchronize, reopened]
paths: [
'.github/workflows/build-rpc.yml',
'ggml/src/ggml-rpc/**'
]
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref && github.ref || github.run_id }}
cancel-in-progress: true
env:
GGML_NLOOP: 3
GGML_N_THREADS: 1
LLAMA_ARG_LOG_COLORS: 1
LLAMA_ARG_LOG_PREFIX: 1
LLAMA_ARG_LOG_TIMESTAMPS: 1
jobs:
ubuntu-24-rpc:
runs-on: ${{ 'ubuntu-24.04-arm' || 'ubuntu-24.04' }}
continue-on-error: true
steps:
- name: Clone
id: checkout
uses: actions/checkout@v6
- name: Dependencies
id: depends
run: |
sudo apt-get update
sudo apt-get install build-essential libssl-dev ninja-build
- name: Build
id: cmake_build
run: |
cmake -B build \
-G "Ninja" \
-DCMAKE_BUILD_TYPE=Release \
-DGGML_RPC=ON
time cmake --build build --config Release -j $(nproc)
- name: Test
id: cmake_test
run: |
cd build
ctest -L main --verbose
+57 -10
View File
@@ -93,19 +93,13 @@ jobs:
run: |
echo "VULKAN_SDK_VERSION=$(curl https://vulkan.lunarg.com/sdk/latest/linux.txt)" >> "$GITHUB_ENV"
- name: Use Vulkan SDK Cache
uses: actions/cache@v5
id: cache-sdk
with:
path: ./vulkan_sdk
key: cache-gha-vulkan-sdk-${{ env.VULKAN_SDK_VERSION }}-${{ runner.os }}
- name: Setup Vulkan SDK
if: steps.cache-sdk.outputs.cache-hit != 'true'
uses: ./.github/actions/linux-setup-vulkan
id: setup
uses: ./.github/actions/unarchive-tar
with:
url: https://sdk.lunarg.com/sdk/download/${{ env.VULKAN_SDK_VERSION }}/linux/vulkan_sdk.tar.xz
path: ./vulkan_sdk
version: ${{ env.VULKAN_SDK_VERSION }}
strip: 1
- name: ccache
uses: ggml-org/ccache-action@v1.2.21
@@ -133,3 +127,56 @@ jobs:
# This is using llvmpipe and runs slower than other backends
# test-backend-ops is too slow on llvmpipe, skip it
ctest -L main -E test-backend-ops --verbose --timeout 900
windows:
runs-on: windows-2025
env:
VULKAN_VERSION: 1.4.357.0
steps:
- name: Clone
id: checkout
uses: actions/checkout@v6
- name: ccache
uses: ggml-org/ccache-action@v1.2.21
with:
key: cpu-windows-2025-x64-vulkan
variant: ccache
evict-old-files: 1d
save: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }}
- name: Install Vulkan SDK
id: get_vulkan
run: |
curl.exe -o $env:RUNNER_TEMP/VulkanSDK-Installer.exe -L "https://sdk.lunarg.com/sdk/download/${env:VULKAN_VERSION}/windows/vulkansdk-windows-X64-${env:VULKAN_VERSION}.exe"
& "$env:RUNNER_TEMP\VulkanSDK-Installer.exe" --accept-licenses --default-answer --confirm-command install
Add-Content $env:GITHUB_ENV "VULKAN_SDK=C:\VulkanSDK\${env:VULKAN_VERSION}"
Add-Content $env:GITHUB_PATH "C:\VulkanSDK\${env:VULKAN_VERSION}\bin"
- name: Install Ninja
id: install_ninja
run: |
choco install ninja
- name: Build
id: cmake_build
run: |
cmake -S . -B build -G "Ninja Multi-Config" `
-D CMAKE_TOOLCHAIN_FILE=cmake/x64-windows-llvm.cmake `
-DCMAKE_BUILD_TYPE=Release `
-DGGML_NATIVE=OFF `
-DLLAMA_BUILD_SERVER=ON `
-DGGML_RPC=ON `
-DGGML_BACKEND_DL=ON `
-DGGML_CPU_ALL_VARIANTS=ON `
-DGGML_VULKAN=ON `
-DLLAMA_BUILD_BORINGSSL=ON
cmake --build build --config Release -j ${env:NUMBER_OF_PROCESSORS}
- name: Test
id: cmake_test
run: |
cd build
ctest -L main -C Release --verbose --timeout 900
+27
View File
@@ -49,6 +49,33 @@ jobs:
git push origin "${VERSION}"
echo "Created and pushed tag ${VERSION}"
- name: Generate release description
id: desc
run: bash scripts/make-release-desc.sh "${{ steps.checks.outputs.version }}"
env:
GITHUB_REPOSITORY: ${{ github.repository }}
- name: Create release
if: ${{ github.event.inputs.dry_run == 'false' }}
uses: ggml-org/action-create-release@v1
env:
GITHUB_TOKEN: ${{ github.token }}
with:
tag_name: ${{ steps.checks.outputs.version }}
# TODO: remove the prerelease flag once the semantic versioning workflow is ready
# ref: https://github.com/ggml-org/ggml/discussions/1579
prerelease: true
body: |
> [!NOTE]
> Semantic versioning is still work in progress.
> More info can be found in https://github.com/ggml-org/ggml/discussions/1579
${{ steps.desc.outputs.nightly }}
## ${{ steps.desc.outputs.changelog_title }}
${{ steps.desc.outputs.changelog }}
- name: Dry run summary
if: ${{ github.event.inputs.dry_run == 'true' }}
run: |
+2 -4
View File
@@ -5,7 +5,7 @@ include(CheckIncludeFileCXX)
### llama.cpp version
set(LLAMA_VERSION_MAJOR 0)
set(LLAMA_VERSION_MINOR 1)
set(LLAMA_VERSION_PATCH 1)
set(LLAMA_VERSION_PATCH 2)
set(LLAMA_VERSION_BASE "${LLAMA_VERSION_MAJOR}.${LLAMA_VERSION_MINOR}.${LLAMA_VERSION_PATCH}")
# whether this is a development/nightly build
@@ -224,12 +224,10 @@ add_subdirectory(src)
# utils, programs, examples and tests
#
# mtmd needs this even when common is not built
add_subdirectory(vendor/hash)
add_subdirectory(vendor)
if (LLAMA_BUILD_COMMON)
add_subdirectory(common)
add_subdirectory(vendor/cpp-httplib)
endif()
if (LLAMA_BUILD_COMMON AND LLAMA_BUILD_TESTS AND NOT CMAKE_JS_VERSION)
+3 -3
View File
@@ -119,7 +119,7 @@ The `llama.cpp` project is build on top of the [ggml](https://github.com/ggml-or
## Acknowledgements
- [yhirose/cpp-httplib](https://github.com/yhirose/cpp-httplib) - Single-header HTTP server, used by `llama-server` - MIT license
- [stb-image](https://github.com/nothings/stb) - Single-header image format decoder, used by multimodal subsystem - Public domain
- [nothings/stb](https://github.com/nothings/stb) - Single-header image format decoder, used by multimodal subsystem - Public domain
- [nlohmann/json](https://github.com/nlohmann/json) - Single-header JSON library, used by various tools/examples - MIT License
- [miniaudio.h](https://github.com/mackron/miniaudio) - Single-header audio format decoder, used by multimodal subsystem - Public domain
- [subprocess.h](https://github.com/sheredom/subprocess.h) - Single-header process launching solution for C and C++ - Public domain
- [mackron/miniaudio](https://github.com/mackron/miniaudio) - Single-header audio format decoder, used by multimodal subsystem - Public domain
- [sheredom/subprocess.h](https://github.com/sheredom/subprocess.h) - Single-header process launching solution for C and C++ - Public domain
+1
View File
@@ -290,6 +290,7 @@ combine_static_libraries() {
"${base_dir}/${build_dir}/ggml/src/ggml-metal/${release_dir}/libggml-metal.a"
"${base_dir}/${build_dir}/ggml/src/ggml-blas/${release_dir}/libggml-blas.a"
"${base_dir}/${build_dir}/tools/mtmd/${release_dir}/libmtmd.a"
"${base_dir}/${build_dir}/vendor/hash/${release_dir}/libvendor-hash.a"
)
# Create temporary directory for processing
+2 -1
View File
@@ -126,7 +126,8 @@ set_target_properties(${TARGET} PROPERTIES
MACHO_CURRENT_VERSION 0 # keep macOS linker from seeing oversized version number
)
target_include_directories(${TARGET} PUBLIC . ../vendor)
target_include_directories(${TARGET} PUBLIC .)
target_link_libraries (${TARGET} PUBLIC vendor::nlohmann vendor::sheredom)
target_compile_features (${TARGET} PUBLIC cxx_std_17)
if (LLAMA_SUBPROCESS)
-32
View File
@@ -1710,38 +1710,6 @@ common_params_context common_params_parser_init(common_params & params, llama_ex
params.cache_ram_mib = value;
}
).set_env("LLAMA_ARG_CACHE_RAM").set_examples({LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_CLI}));
add_opt(common_arg(
{"-cdisk", "--cache-disk"}, "PATH",
"directory for the disk prompt cache; prompts evicted from the RAM cache are saved here and restored on later requests, including across restarts (default: disabled, requires cache-ram)",
[](common_params & params, const std::string & value) {
params.cache_disk_path = value;
if (!fs_is_directory(params.cache_disk_path)) {
throw std::invalid_argument("not a directory: " + value);
}
// if doesn't end with DIRECTORY_SEPARATOR, add it
if (params.cache_disk_path[params.cache_disk_path.size() - 1] != DIRECTORY_SEPARATOR) {
params.cache_disk_path += DIRECTORY_SEPARATOR;
}
}
).set_env("LLAMA_ARG_CACHE_DISK").set_examples({LLAMA_EXAMPLE_SERVER}));
add_opt(common_arg(
{"--cache-disk-limit"}, "N",
string_format("total size budget of the disk prompt cache directory in MiB; oldest entries are deleted when exceeded (default: %d, -1 - no limit)", params.cache_disk_limit_mib),
[](common_params & params, int value) {
if (value == 0 || value < -1) {
throw std::invalid_argument("cache-disk-limit must be positive or -1 (no limit)");
}
params.cache_disk_limit_mib = value;
}
).set_env("LLAMA_ARG_CACHE_DISK_LIMIT").set_examples({LLAMA_EXAMPLE_SERVER}));
add_opt(common_arg(
{"--cache-disk-write-through"},
{"--no-cache-disk-write-through"},
"write prompts to the disk cache every time they are saved to the RAM cache, instead of only when evicted from it (default: disabled)",
[](common_params & params, bool value) {
params.cache_disk_write_through = value;
}
).set_env("LLAMA_ARG_CACHE_DISK_WRITE_THROUGH").set_examples({LLAMA_EXAMPLE_SERVER}));
add_opt(common_arg(
{"-kvu", "--kv-unified"},
{"-no-kvu", "--no-kv-unified"},
-4
View File
@@ -614,10 +614,6 @@ struct common_params {
int32_t checkpoint_min_step = 8192; // minimum spacing between context checkpoints
int32_t cache_ram_mib = 8192; // -1 = no limit, 0 - disable, 1 = 1 MiB, etc.
std::string cache_disk_path; // disk prompt cache directory, empty = disabled
int32_t cache_disk_limit_mib = -1; // total size budget for the disk prompt cache dir, -1 = no limit
bool cache_disk_write_through = false; // also write to disk whenever a prompt is saved to the RAM cache
std::string hostname = "127.0.0.1";
std::string public_path = ""; // NOLINT
std::string api_prefix = ""; // NOLINT
+1 -1
View File
@@ -2,5 +2,5 @@ set(TARGET llama-gguf-hash)
add_executable(${TARGET} gguf-hash.cpp)
install(TARGETS ${TARGET} RUNTIME)
target_link_libraries(${TARGET} PRIVATE vendor-hash ggml ${CMAKE_THREAD_LIBS_INIT})
target_link_libraries(${TARGET} PRIVATE vendor::hash ggml ${CMAKE_THREAD_LIBS_INIT})
target_compile_features(${TARGET} PRIVATE cxx_std_17)
+3 -3
View File
@@ -17,15 +17,15 @@
extern "C" {
#endif
#include "xxhash/xxhash.h"
#include "sha256/sha256.h"
#include "hash/xxhash/xxhash.h"
#include "hash/sha256/sha256.h"
#ifdef __cplusplus
}
#endif
// sha1 is compiled as C++ and lives in a namespace, see scripts/sync_vendor.py
#include "sha1/sha1.h"
#include "hash/sha1/sha1.h"
using namespace vendor_hash;
+1 -1
View File
@@ -5,7 +5,7 @@ project("ggml" C CXX ASM)
### GGML Version
set(GGML_VERSION_MAJOR 0)
set(GGML_VERSION_MINOR 20)
set(GGML_VERSION_PATCH 1)
set(GGML_VERSION_PATCH 2)
set(GGML_VERSION_BASE "${GGML_VERSION_MAJOR}.${GGML_VERSION_MINOR}.${GGML_VERSION_PATCH}")
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake/")
+91 -31
View File
@@ -4,6 +4,7 @@
#include "vecdotq.cuh"
#include <cstdint>
#include <type_traits>
typedef float (*vec_dot_q_cuda_t)(const void * __restrict__ vbq, const block_q8_1 * __restrict__ bq8_1, const int & kbx, const int & iqs);
@@ -69,7 +70,8 @@ enum mmvq_parameter_table_id {
MMVQ_PARAMETERS_GCN,
MMVQ_PARAMETERS_RDNA2,
MMVQ_PARAMETERS_RDNA3_0,
MMVQ_PARAMETERS_RDNA4
MMVQ_PARAMETERS_RDNA4,
MMVQ_PARAMETERS_GB10
};
static constexpr __device__ mmvq_parameter_table_id get_device_table_id() {
@@ -83,6 +85,8 @@ static constexpr __device__ mmvq_parameter_table_id get_device_table_id() {
return MMVQ_PARAMETERS_GCN;
#elif defined(__CUDA_ARCH__) && __CUDA_ARCH__ >= GGML_CUDA_CC_TURING && __CUDA_ARCH__ < GGML_CUDA_CC_AMPERE
return MMVQ_PARAMETERS_TURING;
#elif defined(__CUDA_ARCH__) && __CUDA_ARCH__ == GGML_CUDA_CC_DGX_SPARK
return MMVQ_PARAMETERS_GB10;
#else
return MMVQ_PARAMETERS_GENERIC;
#endif
@@ -104,6 +108,9 @@ static __host__ mmvq_parameter_table_id get_device_table_id(int cc) {
if (GGML_CUDA_CC_IS_NVIDIA(cc) && ggml_cuda_highest_compiled_arch(cc) >= GGML_CUDA_CC_TURING && ggml_cuda_highest_compiled_arch(cc) < GGML_CUDA_CC_AMPERE) {
return MMVQ_PARAMETERS_TURING;
}
if (GGML_CUDA_CC_IS_NVIDIA(cc) && ggml_cuda_highest_compiled_arch(cc) == GGML_CUDA_CC_DGX_SPARK) {
return MMVQ_PARAMETERS_GB10;
}
return MMVQ_PARAMETERS_GENERIC;
}
@@ -351,7 +358,7 @@ static constexpr __device__ int get_mmvq_mmid_max_batch_for_device() {
#endif
}
static constexpr __host__ __device__ int calc_nwarps(ggml_type type, int ncols_dst, mmvq_parameter_table_id table_id) {
static constexpr __host__ __device__ int calc_nwarps(ggml_type type, int ncols_dst, mmvq_parameter_table_id table_id, bool small_k = false, bool halve_iters = false) {
if (table_id == MMVQ_PARAMETERS_GENERIC) {
switch (ncols_dst) {
case 1:
@@ -454,11 +461,32 @@ static constexpr __host__ __device__ int calc_nwarps(ggml_type type, int ncols_d
return 1;
}
}
if (table_id == MMVQ_PARAMETERS_GB10) {
const int generic = calc_nwarps(type, ncols_dst, MMVQ_PARAMETERS_GENERIC);
// Only worth the wider block when it actually retires the K loop in half the trips (Observation)
if (ncols_dst == 1 && !small_k && halve_iters) {
switch (type) {
case GGML_TYPE_Q4_0:
case GGML_TYPE_Q4_1:
case GGML_TYPE_Q5_0:
case GGML_TYPE_Q5_1:
case GGML_TYPE_Q8_0:
case GGML_TYPE_Q4_K:
case GGML_TYPE_Q5_K:
case GGML_TYPE_Q6_K:
case GGML_TYPE_IQ4_NL:
return 2 * generic;
default:
break;
}
}
return generic;
}
return 1;
}
static constexpr __host__ __device__ int calc_rows_per_block(int ncols_dst, int table_id, bool small_k = false, int nwarps = 1) {
if (table_id == MMVQ_PARAMETERS_GENERIC || table_id == MMVQ_PARAMETERS_GCN || table_id == MMVQ_PARAMETERS_TURING) {
if (table_id == MMVQ_PARAMETERS_GENERIC || table_id == MMVQ_PARAMETERS_GCN || table_id == MMVQ_PARAMETERS_TURING || table_id == MMVQ_PARAMETERS_GB10) {
switch (ncols_dst) {
case 1:
return small_k ? nwarps : 1;
@@ -477,8 +505,8 @@ static constexpr __host__ __device__ int calc_rows_per_block(int ncols_dst, int
return 1;
}
template <ggml_type type, int ncols_dst, bool has_fusion, bool small_k = false>
__launch_bounds__(calc_nwarps(type, ncols_dst, get_device_table_id())*ggml_cuda_get_physical_warp_size(), 1)
template <ggml_type type, int ncols_dst, bool has_fusion, bool small_k = false, bool halve_iters = false>
__launch_bounds__(calc_nwarps(type, ncols_dst, get_device_table_id(), small_k, halve_iters)*ggml_cuda_get_physical_warp_size(), 1)
static __global__ void mul_mat_vec_q(
const void * vx_ptr, const void * vy_ptr, const int32_t * ids_ptr, const ggml_cuda_mm_fusion_args_device fusion, float * dst_ptr,
const uint32_t ncols_x, const uint3 nchannels_y, const uint32_t stride_row_x, const uint32_t stride_col_y,
@@ -495,7 +523,7 @@ static __global__ void mul_mat_vec_q(
constexpr int qi = ggml_cuda_type_traits<type>::qi;
constexpr int vdr = get_vdr_mmvq(type);
constexpr mmvq_parameter_table_id table_id = get_device_table_id();
constexpr int nwarps = calc_nwarps(type, ncols_dst, table_id);
constexpr int nwarps = calc_nwarps(type, ncols_dst, table_id, small_k, halve_iters);
constexpr int rows_per_cuda_block = calc_rows_per_block(ncols_dst, table_id, small_k, nwarps);
constexpr int warp_size = ggml_cuda_get_physical_warp_size();
@@ -773,8 +801,8 @@ static __global__ void mul_mat_vec_q_moe(
template<ggml_type type>
static std::pair<dim3, dim3> calc_launch_params(
const int ncols_dst, const int nrows_x, const int nchannels_dst, const int nsamples_or_ntokens,
const int warp_size, const mmvq_parameter_table_id table_id, const bool small_k = false) {
const int nwarps = calc_nwarps(type, ncols_dst, table_id);
const int warp_size, const mmvq_parameter_table_id table_id, const bool small_k = false, const bool halve_iters = false) {
const int nwarps = calc_nwarps(type, ncols_dst, table_id, small_k, halve_iters);
const int rpb = calc_rows_per_block(ncols_dst, table_id, small_k, nwarps);
const int64_t nblocks = (nrows_x + rpb - 1) / rpb;
const dim3 block_nums(nblocks, nchannels_dst, nsamples_or_ntokens);
@@ -782,7 +810,7 @@ static std::pair<dim3, dim3> calc_launch_params(
return {block_nums, block_dims};
}
template<ggml_type type, int c_ncols_dst, bool small_k = false>
template<ggml_type type, int c_ncols_dst, bool small_k = false, bool halve_iters = false>
static void mul_mat_vec_q_switch_fusion(
const void * vx, const void * vy, const int32_t * ids, const ggml_cuda_mm_fusion_args_device fusion, float * dst,
const uint32_t ncols_x, const uint3 nchannels_y, const uint32_t stride_row_x, const uint32_t stride_col_y,
@@ -797,7 +825,7 @@ static void mul_mat_vec_q_switch_fusion(
if constexpr (c_ncols_dst == 1) {
if (has_fusion) {
const ggml_cuda_kernel_launch_params launch_params = ggml_cuda_kernel_launch_params(block_nums, block_dims, nbytes_shared, stream);
ggml_cuda_kernel_launch(mul_mat_vec_q<type, c_ncols_dst, true, small_k>, launch_params,
ggml_cuda_kernel_launch(mul_mat_vec_q<type, c_ncols_dst, true, small_k, halve_iters>, launch_params,
vx, vy, ids, fusion, dst, ncols_x, nchannels_y, stride_row_x, stride_col_y, stride_col_dst,
channel_ratio, stride_channel_x, stride_channel_y, stride_channel_dst,
sample_ratio, stride_sample_x, stride_sample_y, stride_sample_dst, ids_stride);
@@ -808,7 +836,7 @@ static void mul_mat_vec_q_switch_fusion(
GGML_ASSERT(!has_fusion && "fusion only supported for ncols_dst=1");
const ggml_cuda_kernel_launch_params launch_params = ggml_cuda_kernel_launch_params(block_nums, block_dims, nbytes_shared, stream);
ggml_cuda_kernel_launch(mul_mat_vec_q<type, c_ncols_dst, false, small_k>, launch_params,
ggml_cuda_kernel_launch(mul_mat_vec_q<type, c_ncols_dst, false, small_k, halve_iters>, launch_params,
vx, vy, ids, fusion, dst, ncols_x, nchannels_y, stride_row_x, stride_col_y, stride_col_dst,
channel_ratio, stride_channel_x, stride_channel_y, stride_channel_dst,
sample_ratio, stride_sample_x, stride_sample_y, stride_sample_dst, ids_stride);
@@ -860,16 +888,18 @@ static void mul_mat_vec_q_switch_ncols_dst(
const bool has_ids = ids != nullptr;
// How the K loop divides up at the baseline block width, both decisions below use these.
constexpr int qk = ggml_cuda_type_traits<type>::qk;
constexpr int qi = ggml_cuda_type_traits<type>::qi;
constexpr int vdr = get_vdr_mmvq(type);
const int blocks_per_row_x = ncols_x / qk;
const int blocks_per_iter_1warp = vdr * warp_size / qi;
const auto should_use_small_k = [&](int c_ncols_dst) {
// When K is small, increase rows_per_block to match nwarps so each warp has more work to do
// Trigger when the full thread block covers all K blocks in a single loop iteration and few threads remain idle.
constexpr int qk = ggml_cuda_type_traits<type>::qk;
constexpr int qi = ggml_cuda_type_traits<type>::qi;
constexpr int vdr = get_vdr_mmvq(type);
const int blocks_per_row_x = ncols_x / qk;
const int blocks_per_iter_1warp = vdr * warp_size / qi;
const int nwarps = calc_nwarps(type, c_ncols_dst, table_id);
bool use = nwarps > 1 && blocks_per_row_x < nwarps * blocks_per_iter_1warp;
const int nwarps = calc_nwarps(type, c_ncols_dst, table_id);
bool use = nwarps > 1 && blocks_per_row_x < nwarps * blocks_per_iter_1warp;
constexpr std::array<ggml_type, 2> iq_slow_turing = {
GGML_TYPE_IQ3_XXS,
@@ -902,6 +932,28 @@ static void mul_mat_vec_q_switch_ncols_dst(
return use;
};
// Whether doubling nwarps pays off on the ncols_dst == 1 path, where K sets the K loop trip count.
const auto should_halve_iters = [&] {
if (table_id != MMVQ_PARAMETERS_GB10) {
return false;
}
// Expert rows are gathered per token, so a wider block adds reduction work without reuse.
if (has_ids) {
return false;
}
const int blocks_per_iter = calc_nwarps(type, 1, table_id) * blocks_per_iter_1warp;
const int iters = (blocks_per_row_x + blocks_per_iter - 1) / blocks_per_iter;
const int iters_wide = (blocks_per_row_x + blocks_per_iter * 2 - 1) / (blocks_per_iter * 2);
// An odd trip count leaves half the wider block idle for its last iteration, that tail is
// only affordable once the loop is long enough to dilute it to an eighth of the work (observation).
const int idle = iters_wide * 2 - iters;
return idle * 8 <= iters_wide * 2;
};
if (has_ids && ncols_dst > 1) {
// Multi-token MUL_MAT_ID path - dedicated MoE kernel
mul_mat_vec_q_moe_launch<type>(
@@ -914,26 +966,34 @@ static void mul_mat_vec_q_switch_ncols_dst(
switch (ncols_dst) {
case 1: {
constexpr int c_ncols_dst = 1;
// static, else MSVC lambda capture breaks the constexpr uses below
static constexpr int c_ncols_dst = 1;
bool use_small_k = should_use_small_k(c_ncols_dst);
// Tag types keep the flags compile-time, so __launch_bounds__ matches what is launched.
const auto launch = [&](auto small_k_tag, auto halve_iters_tag) {
constexpr bool c_small_k = decltype(small_k_tag)::value;
// Types the table does not promote would compile a second, identical kernel.
constexpr bool c_promoted =
calc_nwarps(type, c_ncols_dst, MMVQ_PARAMETERS_GB10, false, true) !=
calc_nwarps(type, c_ncols_dst, MMVQ_PARAMETERS_GB10, false, false);
if (use_small_k) {
std::pair<dim3, dim3> dims = calc_launch_params<type>(c_ncols_dst, nrows_x, nchannels_dst,
nsamples_dst, warp_size, table_id, true);
mul_mat_vec_q_switch_fusion<type, c_ncols_dst, true>(
constexpr bool c_halve_iters = decltype(halve_iters_tag)::value && c_promoted;
const std::pair<dim3, dim3> dims = calc_launch_params<type>(c_ncols_dst, nrows_x, nchannels_dst,
nsamples_dst, warp_size, table_id, c_small_k, c_halve_iters);
mul_mat_vec_q_switch_fusion<type, c_ncols_dst, c_small_k, c_halve_iters>(
vx, vy, ids, fusion, dst, ncols_x, nchannels_y_fd, stride_row_x, stride_col_y, stride_col_dst,
channel_ratio_fd, stride_channel_x, stride_channel_y, stride_channel_dst, sample_ratio_fd,
stride_sample_x, stride_sample_y, stride_sample_dst, dims.first, dims.second, 0, ids_stride,
stream);
};
if (should_use_small_k(c_ncols_dst)) {
launch(std::true_type{}, std::false_type{});
} else if (should_halve_iters()) {
launch(std::false_type{}, std::true_type{});
} else {
std::pair<dim3, dim3> dims = calc_launch_params<type>(c_ncols_dst, nrows_x, nchannels_dst,
nsamples_dst, warp_size, table_id);
mul_mat_vec_q_switch_fusion<type, c_ncols_dst>(
vx, vy, ids, fusion, dst, ncols_x, nchannels_y_fd, stride_row_x, stride_col_y, stride_col_dst,
channel_ratio_fd, stride_channel_x, stride_channel_y, stride_channel_dst, sample_ratio_fd,
stride_sample_x, stride_sample_y, stride_sample_dst, dims.first, dims.second, 0, ids_stride,
stream);
launch(std::false_type{}, std::false_type{});
}
} break;
case 2: {
+87
View File
@@ -0,0 +1,87 @@
#!/bin/bash
# Generate the description of a release: the previous release version, the
# change log and the link to the nightly release corresponding to the commit being released.
#
# Usage: make-release-desc.sh <version>
# <version>: current release version (v<maj>.<min>.<pat>, the leading v is optional)
#
# The previous version is the highest plain semver tag (v<maj>.<min>.<pat>)
# strictly below <version>. The change log lists all commits between the
# previous version tag and the release commit, one line per commit.
#
# The release commit is the commit <version> points at when the tag exists,
# HEAD otherwise. The nightly release is the b* tag pointing at that commit
# (release.yml tags the same commit); the link is only generated when that
# tag exists.
#
# Env (when running in GitHub Actions):
# GITHUB_OUTPUT: previous_tag, changelog_title, changelog and nightly are written here
# GITHUB_REPOSITORY: owner/repo, used to build the nightly release URL (skipped when unset)
set -euo pipefail
if [[ $# -ne 1 ]]; then
echo "Usage: $(basename "$0") <version>"
exit 1
fi
VERSION="$1"
# Accept the version with or without the leading v, reject anything else
if [[ "${VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
VERSION="v${VERSION}"
elif [[ ! "${VERSION}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Error: invalid version '${VERSION}' (expected v<maj>.<min>.<pat>)"
exit 1
fi
# Make sure all remote tags are available locally (skipped on local runs without origin)
if ! git fetch --tags origin 2>/dev/null; then
echo "Warning: could not fetch tags from origin (local run?)"
fi
# Release commit: the commit <version> points at when the tag exists, HEAD otherwise.
if ! RELEASE_COMMIT="$(git rev-parse -q --verify "refs/tags/${VERSION}^{commit}" 2>/dev/null)"; then
RELEASE_COMMIT="$(git rev-parse HEAD)"
fi
echo "Release commit: $(git rev-parse --short "${RELEASE_COMMIT}")"
PREV="$( { git tag --list; echo "${VERSION}"; } \
| grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' \
| sort -V \
| awk -v cur="${VERSION}" '$0 == cur { exit } { prev = $0 } END { print prev }')"
if [[ -n "${PREV}" ]]; then
CHANGELOG="$(git log --oneline "${PREV}..${RELEASE_COMMIT}")"
CHANGELOG_TITLE="Change log since ${PREV}"
else
CHANGELOG="(no previous release tag found)"
CHANGELOG_TITLE="Change log"
fi
# Nightly release: the b* tag pointing at the release commit (|| true: no match is not an error)
NIGHTLY_TAG="$(git tag --points-at "${RELEASE_COMMIT}" | grep -E '(^|-)b[0-9]+(-[0-9a-f]{7})?$' | head -n 1 || true)"
NIGHTLY=""
if [[ -n "${NIGHTLY_TAG}" ]]; then
if [[ -n "${GITHUB_REPOSITORY:-}" ]]; then
NIGHTLY_URL="https://github.com/${GITHUB_REPOSITORY}/releases/tag/${NIGHTLY_TAG}"
NIGHTLY="**Nightly build:** [${NIGHTLY_TAG}](${NIGHTLY_URL})"
echo "Nightly release: ${NIGHTLY_URL}"
fi
else
echo "No nightly release found for commit $(git rev-parse --short "${RELEASE_COMMIT}")"
fi
echo "Previous version: ${PREV:-none}"
echo "${CHANGELOG}"
if [[ -n "${GITHUB_OUTPUT:-}" ]]; then
{
echo "previous_tag=${PREV}"
echo "changelog_title=${CHANGELOG_TITLE}"
echo "nightly=${NIGHTLY}"
echo "changelog<<CHANGELOG_EOF"
echo "${CHANGELOG}"
echo "CHANGELOG_EOF"
} >> "${GITHUB_OUTPUT}"
fi
+1 -1
View File
@@ -1 +1 @@
3834fd814e74e8af277939dabd69ecc780affd21
8c63e70982c95ceb862e3a1073a2c1beef75d60a
+1 -3
View File
@@ -78,10 +78,8 @@ set_target_properties(mtmd PROPERTIES
)
target_link_libraries (mtmd PUBLIC ggml llama)
target_link_libraries (mtmd PRIVATE Threads::Threads vendor-hash)
target_link_libraries (mtmd PRIVATE Threads::Threads vendor::hash vendor::miniaudio vendor::stb vendor::sheredom)
target_include_directories(mtmd PUBLIC .)
target_include_directories(mtmd PRIVATE ../..)
target_include_directories(mtmd PRIVATE ../../vendor)
target_compile_features (mtmd PRIVATE cxx_std_17)
if (MTMD_VIDEO)
+1 -1
View File
@@ -12,7 +12,7 @@
#include "mtmd-helper-common.h"
#include "llama.h"
#include "hash.h"
#include "hash/hash.h"
#include <algorithm>
#include <cinttypes>
+1 -3
View File
@@ -5,8 +5,6 @@ include_directories(${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR})
set(TARGET server-context)
add_library(${TARGET} STATIC
server-cache-disk.cpp
server-cache-disk.h
server-chat.cpp
server-chat.h
server-task.cpp
@@ -33,7 +31,7 @@ endif()
target_include_directories(${TARGET} PRIVATE ../mtmd)
target_include_directories(${TARGET} PRIVATE ${CMAKE_SOURCE_DIR})
target_link_libraries(${TARGET} PUBLIC llama-common mtmd vendor-hash ${CMAKE_THREAD_LIBS_INIT})
target_link_libraries(${TARGET} PUBLIC llama-common mtmd ${CMAKE_THREAD_LIBS_INIT})
# llama-server-impl: server logic, reusable by app
+1 -1
View File
@@ -189,7 +189,7 @@ This endpoint is intended to be used internally by the Web UI and subject to cha
Get a list of tools, each tool has these fields:
- `tool` (string): the ID name of the tool, to be used in POST call. Example: `read_file`
- `display_name` (string): the name to be displayed on UI. Example: `Read file`
- `type` (string): `"builtin"` for a built-in tool, or `"mcp"` for a tool exposed by an MCP server
- `type` (string): `"server"` for a server tool, or `"mcp"` for a tool exposed by an MCP server
- `permissions` (object): a mapping string --> boolean that indicates the permission required by this tool. This is useful for the UI to ask the user before calling the tool. For now, the only permission supported is `"write"`
- `definition` (object): the OAI-compat definition of this tool
+6 -25
View File
@@ -164,9 +164,6 @@ For the full list of features, please refer to [server's changelog](https://gith
| `-ctxcp, --ctx-checkpoints, --swa-checkpoints N` | max number of context checkpoints to create per slot (default: 32)[(more info)](https://github.com/ggml-org/llama.cpp/pull/15293)<br/>(env: LLAMA_ARG_CTX_CHECKPOINTS) |
| `-cms, --checkpoint-min-step N` | minimum spacing between context checkpoints in tokens (default: 8192, 0 = no minimum)<br/>(env: LLAMA_ARG_CHECKPOINT_MIN_SPACING_NT) |
| `-cram, --cache-ram N` | set the maximum cache size in MiB (default: 8192, -1 - no limit, 0 - disable)[(more info)](https://github.com/ggml-org/llama.cpp/pull/16391)<br/>(env: LLAMA_ARG_CACHE_RAM) |
| `-cdisk, --cache-disk PATH` | directory for the disk prompt cache; prompts evicted from the RAM cache are saved here and restored on later requests, including across restarts (default: disabled, requires cache-ram)<br/>(env: LLAMA_ARG_CACHE_DISK) |
| `--cache-disk-limit N` | total size budget of the disk prompt cache directory in MiB; oldest entries are deleted when exceeded (default: -1, -1 - no limit)<br/>(env: LLAMA_ARG_CACHE_DISK_LIMIT) |
| `--cache-disk-write-through, --no-cache-disk-write-through` | write prompts to the disk cache every time they are saved to the RAM cache, instead of only when evicted from it (default: disabled)<br/>(env: LLAMA_ARG_CACHE_DISK_WRITE_THROUGH) |
| `-kvu, --kv-unified, -no-kvu, --no-kv-unified` | use single unified KV buffer shared across all sequences (default: enabled if number of slots is auto)<br/>(env: LLAMA_ARG_KV_UNIFIED) |
| `--cache-idle-slots, --no-cache-idle-slots` | save idle slots to the prompt cache on new task, and clear them when using unified KV (default: enabled, requires cache-ram)<br/>(env: LLAMA_ARG_CACHE_IDLE_SLOTS) |
| `--context-shift, --no-context-shift` | whether to use context shift on infinite text generation (default: disabled)<br/>(env: LLAMA_ARG_CONTEXT_SHIFT) |
@@ -199,11 +196,11 @@ For the full list of features, please refer to [server's changelog](https://gith
| `--ui-config, --webui-config JSON` | JSON that provides default UI settings (overrides UI defaults)<br/>(env: LLAMA_ARG_UI_CONFIG) |
| `--ui-config-file, --webui-config-file PATH` | JSON file that provides default UI settings (overrides UI defaults)<br/>(env: LLAMA_ARG_UI_CONFIG_FILE) |
| `--ui-mcp-proxy, --webui-mcp-proxy, --no-ui-mcp-proxy, --no-webui-mcp-proxy` | experimental: whether to enable MCP CORS proxy - do not enable in untrusted environments (default: disabled)<br/>(env: LLAMA_ARG_UI_MCP_PROXY) |
| `--tools TOOL1,TOOL2,...` | experimental: whether to enable built-in tools for AI agents - do not enable in untrusted environments (default: no tools)<br/>specify "all" to enable all tools<br/>available tools: read_file, file_glob_search, grep_search, exec_shell_command, write_file, edit_file, get_info<br/>note: for security reasons, this will limit --cors-origins to localhost by default<br/>(env: LLAMA_ARG_TOOLS) |
| `--tools TOOL1,TOOL2,...` | experimental: whether to enable server tools for AI agents - do not enable in untrusted environments (default: no tools)<br/>specify "all" to enable all tools<br/>available tools: read_file, file_glob_search, grep_search, exec_shell_command, write_file, edit_file, get_info<br/>note: for security reasons, this will limit --cors-origins to localhost by default<br/>(env: LLAMA_ARG_TOOLS) |
| `--tools-runtime OPTION` | experimental: run tools in a separate runtime environment (default: none, use host environment)<br/>available options:<br/> 'docker:<image>', 'podman:<image>': spin up a new container and reuse it for all invocations, clean up on server exit<br/> 'docker-container:<id>', 'podman-container:<id>': use an existing container by ID, won't stop on server exit<br/> 'ssh:<target>': run tools on a remote POSIX host over SSH, key-based auth and a trusted host key are required<br/><br/>(env: LLAMA_ARG_TOOLS_RUNTIME) |
| `--mcp-servers-config PATH` | experimental: path to JSON file with MCP server definitions (Cursor-compatible format) - do not enable in untrusted environments (default: none)<br/>note: for security reasons, this will limit --cors-origins to localhost by default<br/>(env: LLAMA_ARG_MCP_SERVERS_CONFIG) |
| `--mcp-servers-json JSON` | experimental: inline JSON with MCP server definitions (Cursor-compatible format) - do not enable in untrusted environments (default: none)<br/>note: for security reasons, this will limit --cors-origins to localhost by default<br/>(env: LLAMA_ARG_MCP_SERVERS_JSON) |
| `-ag, --agent, -no-ag, --no-agent` | whether to enable CORS proxy and all built-in tools - do not enable in untrusted environments (default: disabled)<br/>note: for security reasons, this will limit --cors-origins to localhost by default<br/>(env: LLAMA_ARG_AGENT) |
| `-ag, --agent, -no-ag, --no-agent` | whether to enable CORS proxy and all server tools - do not enable in untrusted environments (default: disabled)<br/>note: for security reasons, this will limit --cors-origins to localhost by default<br/>(env: LLAMA_ARG_AGENT) |
| `--ui, --webui, --no-ui, --no-webui` | whether to enable the Web UI (default: enabled)<br/>(env: LLAMA_ARG_UI) |
| `--embedding, --embeddings` | restrict to only support embedding use case; use only with dedicated embedding models (default: disabled)<br/>(env: LLAMA_ARG_EMBEDDINGS) |
| `--rerank, --reranking` | enable reranking endpoint on server (default: disabled)<br/>(env: LLAMA_ARG_RERANKING) |
@@ -330,22 +327,6 @@ services:
LLAMA_ARG_PORT: 8080
```
### Prompt disk cache
The server keeps recently used prompts (their processed KV cache state) in RAM, controlled by `--cache-ram`. With `--cache-disk PATH`, a disk tier is added below the RAM cache: entries evicted from RAM are written to the given directory, and all RAM entries are flushed there on graceful shutdown. On later requests - including after a server restart - the longest cached prefix of the incoming prompt is restored from disk instead of being re-processed.
```sh
llama-server -m model.gguf --cache-disk /path/to/cache --cache-disk-limit 32768
```
Details:
- Files are named `{compat_hash}-{n_tokens}-{chain_hash}.kvc`, where the hashes identify the server configuration and the exact token prefix the file contains. Lookup is a single directory scan at startup plus one hash pass per prompt - no database is used.
- The cache is invalidated automatically when the model file, mmproj, LoRA adapters, KV cache types, or rope parameters change (stale files are ignored, and deleted once the size budget is exceeded).
- `--cache-disk-limit` bounds the total size of the directory in MiB; the oldest files (by modification time) are deleted first, including files left over from other models or configurations. The same directory can be shared by multiple servers.
- By default, files are only written when an entry is evicted from the RAM cache (or on shutdown). With `--cache-disk-write-through`, every prompt saved to the RAM cache is also written to disk immediately, which is more crash-resilient at the cost of extra I/O.
- Note that KV cache states can be large (potentially multiple GiB per prompt, depending on the model and prompt length), so make sure the disk budget is sized accordingly.
### Multimodal support
Multimodal support was added in [#12898](https://github.com/ggml-org/llama.cpp/pull/12898) and is currently an experimental feature.
@@ -356,9 +337,9 @@ It is currently available in the following endpoints:
For more details, please refer to [multimodal documentation](../../docs/multimodal.md)
### Built-in tools support
### Server tools support
The server includes a set of built-in tools that enable the LLM to access the local file system directly from the Web UI.
The server includes a set of server tools that enable the LLM to access the local file system directly from the Web UI.
To use this feature, start the server with `--tools all`. You can also enable only specific tools by passing a comma-separated list: `--tools name1,name2,...`. Run `--help` for the full list of available tool names.
@@ -1650,9 +1631,9 @@ curl http://localhost:8080/v1/messages/count_tokens \
{"input_tokens": 10}
```
## Server built-in tools
## Server tools
The server exposes a REST API under `/tools` that allows the Web UI to call built-in tools. This endpoint is intended to be used internally by the Web UI and subject to change or to be removed in the future.
The server exposes a REST API under `/tools` that allows the Web UI to call server tools. This endpoint is intended to be used internally by the Web UI and subject to change or to be removed in the future.
**Please do NOT use this endpoint in a downstream application**
-580
View File
@@ -1,580 +0,0 @@
#include "server-cache-disk.h"
#include "common.h"
#include "llama.h"
#include "xxhash/xxhash.h"
#include <algorithm>
#include <cinttypes>
#include <cstdio>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <functional>
namespace {
constexpr uint32_t SERVER_CACHE_DISK_MAGIC = 0x3143564B; // "KVC1"
constexpr uint32_t SERVER_CACHE_DISK_VERSION = 1;
// seed for the chained prefix hash - changing it invalidates all filenames
constexpr uint64_t SERVER_CACHE_DISK_CHAIN_SEED = 0x6b7663636861696eULL;
struct server_cache_disk_file_header {
uint32_t magic = SERVER_CACHE_DISK_MAGIC;
uint32_t version = SERVER_CACHE_DISK_VERSION;
uint64_t compat_hash = 0; // full 64-bit value (the filename only carries the low 32 bits)
uint64_t chain_hash = 0;
uint32_t n_tokens = 0;
uint32_t pad = 0;
uint64_t tokens_size = 0; // bytes of the server_tokens::serialize() section
uint64_t state_size = 0; // bytes of the llama_state_seq_get_data section
};
static_assert(sizeof(server_cache_disk_file_header) == 48, "unexpected header size");
std::string make_filename(uint64_t compat_hash, uint32_t n_tokens, uint64_t chain_hash) {
char buf[64];
snprintf(buf, sizeof(buf), "%08x-%u-%016" PRIx64 ".kvc", (uint32_t) compat_hash, n_tokens, chain_hash);
return buf;
}
bool parse_filename(const std::string & name, uint32_t & compat32, uint32_t & n_tokens, uint64_t & chain_hash) {
if (sscanf(name.c_str(), "%8x-%u-%16" SCNx64 ".kvc", &compat32, &n_tokens, &chain_hash) != 3) {
return false;
}
// reject padding/case/suffix variations by requiring the canonical spelling
return name == make_filename(compat32, n_tokens, chain_hash);
}
int64_t file_mtime(const std::filesystem::path & path) {
std::error_code ec;
const auto t = std::filesystem::last_write_time(path, ec);
return ec ? 0 : (int64_t) t.time_since_epoch().count();
}
uint64_t covered_key(uint32_t n_tokens, uint64_t chain_hash) {
const uint64_t buf[2] = { n_tokens, chain_hash };
return XXH64(buf, sizeof(buf), 0);
}
// walk the chained hash over the token list, invoking cb(n, h) at every valid prefix boundary:
// after each text token and after each complete media chunk (never mid-chunk)
// returns true if the walk reached n_max
bool tokens_chain_hash_walk(const server_tokens & tokens, size_t n_max, const std::function<bool(size_t, uint64_t)> & cb) {
uint64_t h = SERVER_CACHE_DISK_CHAIN_SEED;
size_t i = 0;
try {
while (i < n_max) {
const llama_token tok = tokens[i];
if (tok == LLAMA_TOKEN_NULL) {
// media chunk - fold in its content id instead of the placeholder token ids,
// otherwise different images would hash identically
const auto & chunk = tokens.find_chunk(i);
const char * id = mtmd_input_chunk_get_id(chunk.get());
const size_t n_tok = mtmd_input_chunk_get_n_tokens(chunk.get());
if (id == nullptr || id[0] == '\0' || n_tok == 0 || i + n_tok > n_max) {
return false;
}
std::vector<uint8_t> buf;
buf.reserve(5 + strlen(id));
buf.push_back(0x01);
for (int b = 0; b < 4; ++b) {
buf.push_back((uint8_t) (n_tok >> (8*b)));
}
buf.insert(buf.end(), id, id + strlen(id));
h = XXH64(buf.data(), buf.size(), h);
i += n_tok;
} else {
uint8_t buf[5] = { 0x00 };
memcpy(buf + 1, &tok, sizeof(tok));
h = XXH64(buf, sizeof(buf), h);
i += 1;
}
if (!cb(i, h)) {
return false;
}
}
} catch (const std::exception & e) {
SRV_WRN("failed to hash token list: %s\n", e.what());
return false;
}
return true;
}
} // namespace
server_prompt_cache_disk::server_prompt_cache_disk(const std::string & dir_, uint64_t compat_hash, bool has_mtmd, int32_t limit_mib, bool write_through) :
write_through(write_through),
dir(dir_.empty() || dir_.back() == DIRECTORY_SEPARATOR ? dir_ : dir_ + DIRECTORY_SEPARATOR),
compat_hash(compat_hash),
has_mtmd(has_mtmd),
limit_bytes(limit_mib < 0 ? 0 : 1024ull*1024ull*limit_mib) {
scan_dir();
}
void server_prompt_cache_disk::scan_dir() {
namespace fs = std::filesystem;
std::error_code ec;
for (const auto & ent : fs::directory_iterator(dir, ec)) {
if (!ent.is_regular_file(ec)) {
continue;
}
const std::string name = ent.path().filename().string();
// leftover temporary files from a previous crash
if (name.size() > 4 && name.compare(name.size() - 4, 4, ".tmp") == 0 && name[0] == '.') {
fs::remove(ent.path(), ec);
continue;
}
uint32_t compat32 = 0;
uint32_t n_tokens = 0;
uint64_t chain = 0;
if (!parse_filename(name, compat32, n_tokens, chain)) {
continue;
}
server_cache_disk_file file;
file.name = name;
file.chain_hash = chain;
file.n_tokens = n_tokens;
file.n_bytes = ent.file_size(ec);
file.mtime = file_mtime(ent.path());
total_bytes += file.n_bytes;
if (compat32 == (uint32_t) compat_hash) {
index[n_tokens][chain] = std::move(file);
} else {
foreign.push_back(std::move(file));
}
}
SRV_INF("disk prompt cache '%s': %zu usable entries, %zu from other configurations, %.3f MiB total (budget: %.3f MiB)\n",
dir.c_str(), n_files(), foreign.size(), total_bytes / (1024.0 * 1024.0), limit_bytes / (1024.0 * 1024.0));
}
size_t server_prompt_cache_disk::n_files() const {
size_t res = 0;
for (const auto & [n, files] : index) {
res += files.size();
}
return res;
}
server_cache_disk_file * server_prompt_cache_disk::find_file(uint32_t n_tokens, uint64_t chain_hash) {
const auto it = index.find(n_tokens);
if (it == index.end()) {
return nullptr;
}
const auto it_file = it->second.find(chain_hash);
return it_file == it->second.end() ? nullptr : &it_file->second;
}
const server_cache_disk_file * server_prompt_cache_disk::lookup(const server_tokens & tokens, size_t n_max) const {
if (index.empty()) {
return nullptr;
}
// no file can be longer than the largest indexed length - cap the walk
n_max = std::min<size_t>(n_max, index.rbegin()->first);
const server_cache_disk_file * best = nullptr;
tokens_chain_hash_walk(tokens, n_max, [&](size_t n, uint64_t h) {
const auto it = index.find((uint32_t) n);
if (it != index.end()) {
const auto it_file = it->second.find(h);
if (it_file != it->second.end()) {
best = &it_file->second;
}
}
return true;
});
return best;
}
void server_prompt_cache_disk::touch(const server_cache_disk_file & file) {
std::error_code ec;
std::filesystem::last_write_time(dir + file.name, std::filesystem::file_time_type::clock::now(), ec);
if (auto * f = find_file(file.n_tokens, file.chain_hash)) {
f->mtime = file_mtime(dir + file.name);
}
}
void server_prompt_cache_disk::forget(const server_cache_disk_file & file) {
// copy the fields first - the reference may point into the index entry being erased
const uint32_t n_tokens = file.n_tokens;
const uint64_t chain = file.chain_hash;
const uint64_t n_bytes = file.n_bytes;
const auto it = index.find(n_tokens);
if (it == index.end()) {
return;
}
if (it->second.erase(chain) > 0) {
total_bytes -= std::min<size_t>(total_bytes, n_bytes);
}
if (it->second.empty()) {
index.erase(it);
}
}
void server_prompt_cache_disk::remove_file(const server_cache_disk_file & file) {
SRV_WRN("disk prompt cache: removing '%s'\n", file.name.c_str());
std::error_code ec;
std::filesystem::remove(dir + file.name, ec);
forget(file);
}
void server_prompt_cache_disk::enforce_budget(const std::string & name_protected) {
if (limit_bytes == 0) {
return;
}
while (total_bytes > limit_bytes) {
// find the oldest file, ours and foreign alike
const server_cache_disk_file * oldest = nullptr;
bool oldest_foreign = false;
for (const auto & [n, files] : index) {
for (const auto & [h, file] : files) {
if (file.name != name_protected && (!oldest || file.mtime < oldest->mtime)) {
oldest = &file;
oldest_foreign = false;
}
}
}
for (const auto & file : foreign) {
if (file.name != name_protected && (!oldest || file.mtime < oldest->mtime)) {
oldest = &file;
oldest_foreign = true;
}
}
if (!oldest) {
break;
}
SRV_INF("disk prompt cache: size %.3f MiB over budget %.3f MiB, evicting oldest entry '%s'\n",
total_bytes / (1024.0 * 1024.0), limit_bytes / (1024.0 * 1024.0), oldest->name.c_str());
if (oldest_foreign) {
std::error_code ec;
std::filesystem::remove(dir + oldest->name, ec);
total_bytes -= std::min<size_t>(total_bytes, oldest->n_bytes);
foreign.erase(foreign.begin() + (oldest - foreign.data()));
} else {
remove_file(*oldest);
}
}
}
bool server_prompt_cache_disk::store(const server_tokens & tokens, const std::vector<uint8_t> & state_main) {
if (tokens.empty() || state_main.empty()) {
return false;
}
std::vector<std::pair<size_t, uint64_t>> bounds;
if (!tokens_chain_hash_walk(tokens, tokens.size(), [&](size_t n, uint64_t h) { bounds.emplace_back(n, h); return true; }) ||
bounds.empty() || bounds.back().first != tokens.size()) {
SRV_WRN("%s", "disk prompt cache: token list cannot be hashed, skipping\n");
return false;
}
const uint32_t n_tokens = (uint32_t) tokens.size();
const uint64_t chain = bounds.back().second;
if (auto * existing = find_file(n_tokens, chain)) {
SRV_TRC("disk prompt cache: '%s' already exists, refreshing\n", existing->name.c_str());
touch(*existing);
return true;
}
if (covered.count(covered_key(n_tokens, chain)) > 0) {
SRV_TRC(" - prompt with %u tokens is a prefix of an already persisted entry, skipping\n", n_tokens);
return true;
}
std::vector<char> tok_data;
try {
tok_data = tokens.serialize();
} catch (const std::exception & e) {
SRV_WRN("disk prompt cache: failed to serialize tokens: %s\n", e.what());
return false;
}
server_cache_disk_file_header header;
header.compat_hash = compat_hash;
header.chain_hash = chain;
header.n_tokens = n_tokens;
header.tokens_size = tok_data.size();
header.state_size = state_main.size();
const std::string name = make_filename(compat_hash, n_tokens, chain);
char tmp_buf[64];
snprintf(tmp_buf, sizeof(tmp_buf), ".%08x-%u.tmp", (uint32_t) (uintptr_t) this, tmp_counter++);
const std::string path_tmp = dir + tmp_buf;
const std::string path = dir + name;
{
std::ofstream out(path_tmp, std::ios::binary | std::ios::trunc);
out.write((const char *) &header, sizeof(header));
out.write(tok_data.data(), tok_data.size());
out.write((const char *) state_main.data(), state_main.size());
if (!out.good()) {
SRV_ERR("disk prompt cache: failed to write '%s'\n", path_tmp.c_str());
out.close();
std::error_code ec;
std::filesystem::remove(path_tmp, ec);
return false;
}
}
std::error_code ec;
std::filesystem::rename(path_tmp, path, ec);
if (ec) {
SRV_ERR("disk prompt cache: failed to rename '%s' to '%s': %s\n", path_tmp.c_str(), path.c_str(), ec.message().c_str());
std::filesystem::remove(path_tmp, ec);
return false;
}
server_cache_disk_file file;
file.name = name;
file.chain_hash = chain;
file.n_tokens = n_tokens;
file.n_bytes = sizeof(header) + tok_data.size() + state_main.size();
file.mtime = file_mtime(path);
total_bytes += file.n_bytes;
index[n_tokens][chain] = std::move(file);
for (const auto & [n, h] : bounds) {
covered.insert(covered_key((uint32_t) n, h));
}
SRV_INF("disk prompt cache: saved prompt with %u tokens, %.3f MiB to '%s'\n",
n_tokens, (sizeof(header) + tok_data.size() + state_main.size()) / (1024.0 * 1024.0), name.c_str());
SRV_DBG("%s", "__TEST_TAG_CACHE_DISK_STORE__\n");
enforce_budget(name);
return true;
}
server_prompt_cache_disk::load_status server_prompt_cache_disk::load(
server_cache_disk_file file, const server_tokens & tokens_new, llama_context * ctx, int32_t id_slot, server_tokens & tokens_out) {
const std::string path = dir + file.name;
std::error_code ec;
const uint64_t n_bytes = std::filesystem::file_size(path, ec);
if (ec) {
// deleted by another process - not an error, just a miss
forget(file);
return LOAD_MISS;
}
std::ifstream in(path, std::ios::binary);
if (!in.good()) {
forget(file);
return LOAD_MISS;
}
server_cache_disk_file_header header;
in.read((char *) &header, sizeof(header));
if (!in.good() ||
header.magic != SERVER_CACHE_DISK_MAGIC ||
header.version != SERVER_CACHE_DISK_VERSION ||
header.chain_hash != file.chain_hash ||
header.n_tokens != file.n_tokens ||
header.tokens_size % sizeof(llama_token) != 0 ||
sizeof(header) + header.tokens_size + header.state_size != n_bytes) {
SRV_WRN("disk prompt cache: '%s' is corrupt\n", file.name.c_str());
remove_file(file);
return LOAD_MISS;
}
if (header.compat_hash != compat_hash) {
// same low 32 bits, different configuration - leave the file for its owner
SRV_WRN("disk prompt cache: '%s' belongs to a different configuration, ignoring\n", file.name.c_str());
forget(file);
return LOAD_MISS;
}
llama_tokens packed(header.tokens_size / sizeof(llama_token));
in.read((char *) packed.data(), header.tokens_size);
if (!in.good()) {
SRV_WRN("disk prompt cache: '%s' is truncated\n", file.name.c_str());
remove_file(file);
return LOAD_MISS;
}
server_tokens loaded;
try {
loaded = server_tokens::deserialize(packed, has_mtmd);
} catch (const std::exception & e) {
SRV_WRN("disk prompt cache: failed to deserialize tokens from '%s': %s\n", file.name.c_str(), e.what());
remove_file(file);
return LOAD_MISS;
}
// the filename hash only proves an exact prefix probabilistically - verify against the actual tokens
if (loaded.size() != file.n_tokens ||
loaded.get_common_prefix(tokens_new) != file.n_tokens ||
!loaded.validate(ctx)) {
SRV_WRN("disk prompt cache: token mismatch in '%s' (hash collision?)\n", file.name.c_str());
remove_file(file);
return LOAD_MISS;
}
std::vector<uint8_t> state;
try {
state.resize(header.state_size);
} catch (const std::bad_alloc &) {
SRV_ERR("disk prompt cache: failed to allocate %" PRIu64 " bytes for '%s'\n", header.state_size, file.name.c_str());
return LOAD_MISS;
}
in.read((char *) state.data(), state.size());
if (!in.good()) {
SRV_WRN("disk prompt cache: '%s' is truncated\n", file.name.c_str());
remove_file(file);
return LOAD_MISS;
}
const size_t n = llama_state_seq_set_data_ext(ctx, state.data(), state.size(), id_slot, 0);
if (n != state.size()) {
SRV_WRN("disk prompt cache: failed to restore state from '%s' (%zu / %zu bytes)\n", file.name.c_str(), n, state.size());
// the sequence may hold a partial state now - clear it and let the caller recover
llama_memory_seq_rm(llama_get_memory(ctx), id_slot, -1, -1);
return LOAD_FAIL_SEQ_DIRTY;
}
tokens_out = std::move(loaded);
covered.insert(covered_key(file.n_tokens, file.chain_hash));
touch(file);
SRV_INF("disk prompt cache: restored prompt with %u tokens, %.3f MiB from '%s'\n",
file.n_tokens, state.size() / (1024.0 * 1024.0), file.name.c_str());
SRV_DBG("%s", "__TEST_TAG_CACHE_DISK_HIT__\n");
return LOAD_OK;
}
//
// compat hash
//
namespace {
template <typename T>
void hash_pod(std::string & blob, const T & value) {
static_assert(std::is_trivially_copyable<T>::value, "hash_pod requires a POD type");
blob.append((const char *) &value, sizeof(value));
}
void hash_str(std::string & blob, const std::string & value) {
blob += value;
blob += '\0';
}
// path + size + mtime: conservative, but never misses a changed file
void hash_file_meta(std::string & blob, const std::string & path) {
hash_str(blob, path);
std::error_code ec;
const uint64_t size = path.empty() ? 0 : (uint64_t) std::filesystem::file_size(path, ec);
hash_pod(blob, ec ? (uint64_t) 0 : size);
hash_pod(blob, path.empty() ? (int64_t) 0 : file_mtime(path));
}
} // namespace
uint64_t server_cache_disk_compat_hash(const common_params & params) {
std::string blob;
// format versions
hash_pod(blob, (uint32_t) SERVER_CACHE_DISK_VERSION);
hash_pod(blob, (uint32_t) LLAMA_STATE_SEQ_VERSION);
hash_pod(blob, (uint32_t) server_tokens::SERVER_TOKENS_STATE_VERSION);
// model identity
hash_file_meta(blob, params.model.path);
hash_file_meta(blob, params.mmproj.path);
for (const auto & la : params.lora_adapters) {
hash_file_meta(blob, la.path);
hash_pod(blob, la.scale);
}
// KV cache layout
hash_pod(blob, (int32_t) params.cache_type_k);
hash_pod(blob, (int32_t) params.cache_type_v);
hash_pod(blob, (uint8_t) params.swa_full);
// rope params change the KV content for the same tokens
hash_pod(blob, params.rope_freq_base);
hash_pod(blob, params.rope_freq_scale);
hash_pod(blob, (int32_t) params.rope_scaling_type);
hash_pod(blob, params.yarn_ext_factor);
hash_pod(blob, params.yarn_attn_factor);
hash_pod(blob, params.yarn_beta_fast);
hash_pod(blob, params.yarn_beta_slow);
hash_pod(blob, params.yarn_orig_ctx);
return XXH64(blob.data(), blob.size(), 0);
}
-96
View File
@@ -1,96 +0,0 @@
#pragma once
#include "server-common.h"
#include <cstdint>
#include <map>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
struct common_params;
struct llama_context;
// disk-backed prompt cache: a cold tier below the in-RAM server_prompt_cache
//
// each entry is one file in a flat directory, named after the exact token prefix it contains:
//
// {compat_hash8}-{n_tokens}-{chain_hash16}.kvc
//
// - compat_hash: hash of everything that invalidates a KV state (model file, mmproj, loras,
// cache types, rope params, ...) - see server_cache_disk_compat_hash()
// - chain_hash: chained hash over the first n_tokens tokens, so a filename identifies an exact
// prefix and lookup is a single rolling-hash pass over the incoming prompt plus an index probe
//
// file contents mirror what the RAM cache holds for the target context:
//
// header | server_tokens::serialize() bytes | llama_state_seq_get_data (FLAGS_NONE) bytes
struct server_cache_disk_file {
std::string name; // filename inside the cache directory
uint64_t chain_hash = 0;
uint32_t n_tokens = 0;
uint64_t n_bytes = 0;
int64_t mtime = 0; // only used for relative ordering during eviction
};
struct server_prompt_cache_disk {
server_prompt_cache_disk(const std::string & dir, uint64_t compat_hash, bool has_mtmd, int32_t limit_mib, bool write_through);
enum load_status {
LOAD_OK, // state restored into the sequence
LOAD_MISS, // file unusable (corrupt, collision, ...) - sequence untouched
LOAD_FAIL_SEQ_DIRTY, // restore failed mid-way - the sequence was cleared and must be re-filled
};
// largest exact-prefix hit for the first n_max tokens, or nullptr on miss
const server_cache_disk_file * lookup(const server_tokens & tokens, size_t n_max) const;
// restore the state from a file into sequence id_slot of ctx
// on LOAD_OK, tokens_out receives the cached token list (an exact prefix of tokens_new)
load_status load(server_cache_disk_file file, const server_tokens & tokens_new, llama_context * ctx, int32_t id_slot, server_tokens & tokens_out);
// write one entry; deduplicates against existing files and enforces the size budget
bool store(const server_tokens & tokens, const std::vector<uint8_t> & state_main);
size_t n_files() const;
size_t n_bytes_total() const { return total_bytes; }
const bool write_through;
private:
void scan_dir();
server_cache_disk_file * find_file(uint32_t n_tokens, uint64_t chain_hash);
void touch (const server_cache_disk_file & file); // bump mtime so eviction treats it as fresh
void forget(const server_cache_disk_file & file); // drop from the index without touching the filesystem
void remove_file(const server_cache_disk_file & file); // delete from disk and drop from the index
// delete oldest-mtime files (ours and foreign alike) while over the size budget
void enforce_budget(const std::string & name_protected);
const std::string dir;
const uint64_t compat_hash;
const bool has_mtmd;
const size_t limit_bytes; // 0 = no limit
// n_tokens -> chain_hash -> file, for our compat hash only
std::map<uint32_t, std::unordered_map<uint64_t, server_cache_disk_file>> index;
// .kvc files with a different compat hash prefix - never opened, but counted toward the budget
std::vector<server_cache_disk_file> foreign;
size_t total_bytes = 0; // ours + foreign
// (n_tokens, chain_hash) prefixes known to be covered by a file written or loaded this
// session - lets store() skip prefixes of already-persisted prompts
std::unordered_set<uint64_t> covered;
uint32_t tmp_counter = 0;
};
// hash of everything that invalidates a saved KV state for the current server configuration
uint64_t server_cache_disk_compat_hash(const common_params & params);
+2
View File
@@ -266,6 +266,8 @@ static inline raw_buffer base64_decode(const std::string & encoded_string) {
namespace {
constexpr uint32_t SERVER_TOKENS_STATE_VERSION = 1;
uint32_t server_tokens_state_u32(size_t value) {
if (value > std::numeric_limits<uint32_t>::max()) {
throw std::runtime_error("Server tokens state is too large");
-3
View File
@@ -156,9 +156,6 @@ private: // disallow accessing these members directly, risking out-of-sync
// map_idx_to_media will contain: {5, img0}, {8, img1}
public:
// version of the serialize()/deserialize() format below
static constexpr uint32_t SERVER_TOKENS_STATE_VERSION = 1;
server_tokens() = default;
~server_tokens() = default;
+1 -27
View File
@@ -275,13 +275,11 @@ struct server_slot {
llama_state_seq_get_data_ext(ctx_dft, cur->data.drft.data(), cur_size_dft, id, LLAMA_STATE_SEQ_FLAGS_NONE);
}
prompt_cache.disk_store_write_through(*cur);
return true;
}
bool prompt_load(server_prompt_cache & prompt_cache, const server_tokens & tokens) {
bool res = prompt_cache.load(prompt, tokens, ctx_tgt, ctx_dft, id, n_ctx);
bool res = prompt_cache.load(prompt, tokens, ctx_tgt, ctx_dft, id);
if (!res) {
SLT_WRN(*this, "%s", "failed to load prompt from cache\n");
}
@@ -1310,26 +1308,7 @@ private:
SRV_TRC("%s", "use `--cache-ram 0` to disable the prompt cache\n");
prompt_cache = std::make_unique<server_prompt_cache>(params_base.cache_ram_mib, n_ctx);
if (!params_base.cache_disk_path.empty()) {
const uint64_t compat_hash = server_cache_disk_compat_hash(params_base);
SRV_INF("disk prompt cache is enabled, dir: '%s', compat hash: %08x\n",
params_base.cache_disk_path.c_str(), (uint32_t) compat_hash);
prompt_cache->disk = std::make_unique<server_prompt_cache_disk>(
params_base.cache_disk_path,
compat_hash,
mctx != nullptr,
params_base.cache_disk_limit_mib,
params_base.cache_disk_write_through);
}
} else {
if (!params_base.cache_disk_path.empty()) {
SRV_ERR("%s", "--cache-disk requires the RAM prompt cache - remove `--cache-ram 0`\n");
return false;
}
SRV_TRC("%s", "prompt cache is disabled - use `--cache-ram N` to enable it\n");
}
SRV_TRC("%s", "for more info see https://github.com/ggml-org/llama.cpp/pull/16391\n");
@@ -4079,11 +4058,6 @@ bool server_context::load_model(common_params & params) {
void server_context::start_loop() {
auto & params = impl->params_base;
impl->queue_tasks.start_loop(params.sleep_idle_seconds * 1000);
// on graceful shutdown, give the RAM prompt cache entries a chance to survive the restart
if (impl->prompt_cache) {
impl->prompt_cache->disk_flush();
}
}
void server_context::terminate() {
+2 -80
View File
@@ -1750,8 +1750,6 @@ server_prompt_cache_state * server_prompt_cache::alloc(const server_prompt & pro
SRV_WRN(" - making room for prompt cache entry, removing oldest entry (size = %.3f MiB)\n",
states.front().size() / (1024.0 * 1024.0));
spill_front();
states.pop_front();
}
}
@@ -1789,7 +1787,7 @@ server_prompt_cache_state * server_prompt_cache::alloc(const server_prompt & pro
return &states.back();
}
bool server_prompt_cache::load(server_prompt & prompt, const server_tokens & tokens_new, llama_context * ctx_tgt, llama_context * ctx_dft, int32_t id_slot, int32_t n_ctx_slot) {
bool server_prompt_cache::load(server_prompt & prompt, const server_tokens & tokens_new, llama_context * ctx_tgt, llama_context * ctx_dft, int32_t id_slot) {
const int lcp_best = prompt.tokens.get_common_prefix(tokens_new);
float f_keep_best = prompt.tokens.size() > 0 ? float(lcp_best) / prompt.tokens.size() : -1.0f; // empty slot: any cache entry wins
@@ -1799,8 +1797,6 @@ bool server_prompt_cache::load(server_prompt & prompt, const server_tokens & tok
auto it_best = states.end();
int lcp_it_best = 0;
// find the most similar cached prompt, that would also preserve the most context
for (auto it = states.begin(); it != states.end(); ++it) {
const int lcp_cur = it->prompt.tokens.get_common_prefix(tokens_new);
@@ -1819,41 +1815,7 @@ bool server_prompt_cache::load(server_prompt & prompt, const server_tokens & tok
f_keep_best = f_keep_cur;
f_sim_best = f_sim_cur;
it_best = it;
lcp_it_best = lcp_cur;
}
}
// check the disk tier for an exact-prefix match longer than what RAM (or the slot itself) offers
if (disk) {
const int lcp_sel = std::max(lcp_best, lcp_it_best);
const size_t n_max = std::min<size_t>(tokens_new.size(), std::max(0, n_ctx_slot));
const auto * file = disk->lookup(tokens_new, n_max);
if (file && (int64_t) file->n_tokens > (int64_t) lcp_sel) {
server_tokens tokens_disk;
const auto status = disk->load(*file, tokens_new, ctx_tgt, id_slot, tokens_disk);
if (status == server_prompt_cache_disk::LOAD_OK) {
// disk entries carry no draft state - clear the draft sequence so it re-prefills
if (ctx_dft) {
llama_memory_seq_rm(llama_get_memory(ctx_dft), id_slot, -1, -1);
}
prompt.tokens = std::move(tokens_disk);
prompt.checkpoints.clear();
return true;
}
if (status == server_prompt_cache_disk::LOAD_FAIL_SEQ_DIRTY && it_best == states.end()) {
// the slot's sequence was cleared during the failed restore and there is no RAM
// candidate to restore over it - the caller has to clear the slot
return false;
}
it_best = it;
}
}
@@ -1907,8 +1869,6 @@ void server_prompt_cache::update() {
while (!states.empty() && size() > limit_size) {
SRV_WRN(" - cache size limit reached, removing oldest entry (size = %.3f MiB)\n", states.front().size() / (1024.0 * 1024.0));
spill_front();
states.pop_front();
}
}
@@ -1924,8 +1884,6 @@ void server_prompt_cache::update() {
SRV_WRN(" - cache token limit (%zu, est: %zu) reached, removing oldest entry (size = %.3f MiB)\n",
limit_tokens, limit_tokens_cur, states.front().size() / (1024.0 * 1024.0));
spill_front();
states.pop_front();
}
}
@@ -1938,39 +1896,3 @@ void server_prompt_cache::update() {
(const void *)&state, state.prompt.n_tokens(), state.prompt.checkpoints.size(), state.size() / (1024.0 * 1024.0));
}
}
void server_prompt_cache::disk_store(const server_prompt_cache_state & state) const {
if (!disk || state.data.main.empty()) {
return;
}
disk->store(state.prompt.tokens, state.data.main);
}
void server_prompt_cache::disk_store_write_through(const server_prompt_cache_state & state) const {
if (!disk || !disk->write_through) {
return;
}
disk_store(state);
}
void server_prompt_cache::disk_flush() const {
if (!disk) {
return;
}
SRV_INF("flushing %zu prompt cache entries to disk\n", states.size());
for (const auto & state : states) {
disk_store(state);
}
}
void server_prompt_cache::spill_front() const {
if (!disk || states.empty()) {
return;
}
disk_store(states.front());
}
+1 -20
View File
@@ -7,10 +7,8 @@
#include <unordered_set>
#include <list>
#include <map>
#include <memory>
// TODO: prevent including the whole server-common.h as we only use server_tokens
#include "server-cache-disk.h"
#include "server-common.h"
using json = nlohmann::ordered_json;
@@ -614,10 +612,6 @@ struct server_prompt_cache {
std::list<server_prompt_cache_state> states;
// optional cold tier - entries evicted from RAM are spilled here and can be restored later,
// including across server restarts
std::unique_ptr<server_prompt_cache_disk> disk;
// in bytes, 0 = no limit
size_t limit_size = 0;
@@ -630,22 +624,9 @@ struct server_prompt_cache {
server_prompt_cache_state * alloc(const server_prompt & prompt, size_t state_size_main, size_t state_size_drft);
bool load(server_prompt & prompt, const server_tokens & tokens_new, llama_context * ctx_tgt, llama_context * ctx_dft, int32_t id_slot, int32_t n_ctx_slot);
bool load(server_prompt & prompt, const server_tokens & tokens_new, llama_context * ctx_tgt, llama_context * ctx_dft, int32_t id_slot);
void update();
// write one RAM cache entry to the disk tier (no-op when the disk tier is disabled)
void disk_store(const server_prompt_cache_state & state) const;
// disk_store, but only when write-through mode is enabled
void disk_store_write_through(const server_prompt_cache_state & state) const;
// spill all RAM entries to the disk tier (e.g. on graceful shutdown)
void disk_flush() const;
private:
// spill the entry that is about to be evicted
void spill_front() const;
};
// used exclusively by router mode
+1 -1
View File
@@ -2035,7 +2035,7 @@ void server_tools::setup(const std::vector<std::string> & enabled_tools,
}
}
// append MCP tools, skipping any that collide with a built-in or another MCP tool of the same "<server>_<tool>" name
// append MCP tools, skipping any that collide with a server tool or another MCP tool of the same "<server>_<tool>" name
if (!mcp_mgr.empty()) {
std::unordered_set<std::string> seen_names;
for (auto & t : tools) {
+1 -1
View File
@@ -18,7 +18,7 @@ struct server_tool {
virtual ~server_tool() = default;
virtual json get_definition() const = 0;
virtual std::string type() const { return "builtin"; }
virtual std::string type() const { return "server"; }
struct stream {
server_response & qr;
+1 -1
View File
@@ -346,7 +346,7 @@ int llama_server(common_params & params, int argc, char ** argv) {
ctx_http.get ("/tools", ex_wrapper(tools.handle_get));
ctx_http.post("/tools", ex_wrapper(tools.handle_post));
if (!params.server_tools.empty()) {
warn_names.push_back("built-in tools (experimental)");
warn_names.push_back("server tools (experimental)");
}
if (!params.server_tools_runtime.empty()) {
warn_names.push_back("tools runtime (experimental)");
-268
View File
@@ -1,268 +0,0 @@
import base64
import glob
import os
import shutil
import tempfile
import time
import pytest
import requests
from utils import *
server = ServerPreset.tinyllama2()
cache_dir: str = ""
class LogReader:
def __init__(self, path):
self.path = path
self.pos = 0
def drain(self):
with open(self.path) as f:
f.seek(self.pos)
content = f.read()
self.pos = f.tell()
return content
def wait_for(self, tag, timeout=10) -> bool:
# the server log is pumped to the file asynchronously - poll for the tag
deadline = time.time() + timeout
while time.time() < deadline:
if tag in self.drain():
return True
time.sleep(0.25)
return False
def kvc_files() -> list[str]:
return sorted(glob.glob(os.path.join(cache_dir, "*.kvc")))
@pytest.fixture(autouse=True)
def create_server():
global server, cache_dir
cache_dir = tempfile.mkdtemp(prefix="llama_cache_disk_")
server = ServerPreset.tinyllama2()
server.n_slots = 1
server.temperature = 0.0
server.debug = True
server.cache_disk = cache_dir
fd, server.log_path = tempfile.mkstemp(suffix='.log')
os.close(fd)
yield
shutil.rmtree(cache_dir, ignore_errors=True)
PROMPT_A = (
"Once upon a time in a land far away, there lived a brave knight "
"who traveled across mountains and rivers to find the legendary "
"golden sword hidden deep within the enchanted forest of whispers."
)
PROMPT_B = "The quick brown fox jumps over the lazy dog."
def make_prompt_request(prompt, n_predict=0):
global server
res = server.make_request("POST", "/completion", data={
"prompt": prompt,
"n_predict": n_predict, # 0 = evaluate the prompt into the KV cache only
"cache_prompt": True,
})
assert res.status_code == 200
return res
def test_write_through_and_restart_hit():
global server
server.cache_disk_write_through = True
server.start()
log = LogReader(server.log_path)
res = make_prompt_request(PROMPT_A)
prompt_n_full = res.body["timings"]["prompt_n"]
assert prompt_n_full > 0
# nothing is written while the prompt is still live in the slot
assert len(kvc_files()) == 0
# a different prompt takes over the only slot - the previous one is saved
# to the RAM cache and, in write-through mode, to disk immediately
make_prompt_request(PROMPT_B)
assert log.wait_for("__TEST_TAG_CACHE_DISK_STORE__")
assert len(kvc_files()) == 1
# the state must survive a full server restart
server.stop()
server.start()
log = LogReader(server.log_path)
res = make_prompt_request(PROMPT_A)
assert log.wait_for("__TEST_TAG_CACHE_DISK_HIT__")
assert res.body["timings"]["prompt_n"] == 1 # only the last token is re-evaluated
assert res.body["timings"]["cache_n"] == prompt_n_full - 1
def test_spill_on_shutdown_flush():
global server
server.start()
log = LogReader(server.log_path)
make_prompt_request(PROMPT_A)
make_prompt_request(PROMPT_B) # forces PROMPT_A into the RAM cache
# without write-through, nothing reaches the disk while running
time.sleep(0.5)
assert "__TEST_TAG_CACHE_DISK_STORE__" not in log.drain()
assert len(kvc_files()) == 0
# a graceful shutdown flushes the RAM cache entries to disk
server.stop()
assert len(kvc_files()) == 1
server.start()
log = LogReader(server.log_path)
res = make_prompt_request(PROMPT_A)
assert log.wait_for("__TEST_TAG_CACHE_DISK_HIT__")
assert res.body["timings"]["prompt_n"] == 1
def test_ram_cache_hit_takes_priority():
global server
server.cache_disk_write_through = True
server.start()
log = LogReader(server.log_path)
make_prompt_request(PROMPT_A)
make_prompt_request(PROMPT_B)
assert len(kvc_files()) == 1
# PROMPT_A is in both the RAM cache and on disk - the RAM copy must win
# (the disk entry is never longer than the RAM one here)
res = make_prompt_request(PROMPT_A)
time.sleep(0.5)
assert "__TEST_TAG_CACHE_DISK_HIT__" not in log.drain()
assert res.body["timings"]["cache_n"] > 0
def test_budget_eviction():
global server
server.n_ctx = 2048
server.n_batch = 512
server.cache_disk_write_through = True
server.cache_disk_limit = 1 # MiB
server.start()
# three long, distinct token-array prompts; each state is close to 1 MiB
n_len = 1500
for i in range(3):
make_prompt_request([100 + i] * n_len)
# one final small prompt to force the last long prompt out of the slot
make_prompt_request(PROMPT_B)
files = kvc_files()
assert len(files) >= 1
assert len(files) < 3 # the oldest entries were evicted
# the budget is respected (a single over-budget file is allowed to remain)
if len(files) > 1:
assert sum(os.path.getsize(f) for f in files) <= 1024 * 1024
def test_corrupt_file_is_removed():
global server
server.cache_disk_write_through = True
server.start()
make_prompt_request(PROMPT_A)
make_prompt_request(PROMPT_B)
files = kvc_files()
assert len(files) == 1
server.stop()
# corrupt the serialized token section (starts right after the 48-byte header)
with open(files[0], "r+b") as f:
f.seek(48 + 4)
f.write(b"\xff\xff\xff\xff")
server.start()
log = LogReader(server.log_path)
# the request must still succeed, with the prompt fully re-processed
res = make_prompt_request(PROMPT_A)
time.sleep(0.5)
assert "__TEST_TAG_CACHE_DISK_HIT__" not in log.drain()
assert res.body["timings"]["prompt_n"] > 1
# the corrupt file was deleted
assert len(kvc_files()) == 0
IMG_URL_CAT = "https://huggingface.co/ggml-org/tinygemma3-GGUF/resolve/main/test/91_cat.png"
def _get_img_base64(url: str) -> str:
response = requests.get(url)
response.raise_for_status()
return base64.b64encode(response.content).decode("utf-8")
@pytest.fixture
def mmproj_server():
global cache_dir
os.environ['LLAMA_MEDIA_MARKER'] = '<__media__>'
mm_server = ServerPreset.tinygemma3()
mm_server.n_slots = 1
mm_server.temperature = 0.0
mm_server.debug = True
# use the full SWA cache so the restored image prefix can be reused
mm_server.swa_full = True
mm_server.cache_disk = cache_dir
mm_server.cache_disk_write_through = True
fd, mm_server.log_path = tempfile.mkstemp(suffix='.log')
os.close(fd)
return mm_server
def test_image_prompt_across_restart(mmproj_server):
server = mmproj_server
server.start()
prompt_cat = {
"prompt_string": "What is this: <__media__>\n",
"multimodal_data": [_get_img_base64(IMG_URL_CAT)],
}
res = server.make_request("POST", "/completions", data={
"n_predict": 0,
"cache_prompt": True,
"prompt": prompt_cat,
})
assert res.status_code == 200
prompt_n_full = res.body["timings"]["prompt_n"]
res = server.make_request("POST", "/completions", data={
"n_predict": 0,
"cache_prompt": True,
"prompt": "The quick brown fox",
})
assert res.status_code == 200
assert len(kvc_files()) == 1
server.stop()
server.start()
log = LogReader(server.log_path)
# the image KV must be restored from disk in the new process
res = server.make_request("POST", "/completions", data={
"n_predict": 0,
"cache_prompt": True,
"prompt": prompt_cat,
})
assert res.status_code == 200
assert log.wait_for("__TEST_TAG_CACHE_DISK_HIT__")
assert res.body["timings"]["prompt_n"] == 1
assert res.body["timings"]["cache_n"] == prompt_n_full - 1
-9
View File
@@ -111,9 +111,6 @@ class ServerProcess:
media_path: str | None = None
sleep_idle_seconds: int | None = None
cache_ram: int | None = None
cache_disk: str | None = None
cache_disk_limit: int | None = None
cache_disk_write_through: bool = False
no_cache_idle_slots: bool = False
log_path: str | None = None
ui_mcp_proxy: bool = False
@@ -274,12 +271,6 @@ class ServerProcess:
server_args.extend(["--sleep-idle-seconds", self.sleep_idle_seconds])
if self.cache_ram is not None:
server_args.extend(["--cache-ram", self.cache_ram])
if self.cache_disk is not None:
server_args.extend(["--cache-disk", self.cache_disk])
if self.cache_disk_limit is not None:
server_args.extend(["--cache-disk-limit", self.cache_disk_limit])
if self.cache_disk_write_through:
server_args.append("--cache-disk-write-through")
if self.no_cache_idle_slots:
server_args.append("--no-cache-idle-slots")
if self.ui_mcp_proxy:
+3
View File
@@ -61,6 +61,9 @@ export default ts.config(
{ blankLine: 'always', next: ['return', 'throw', 'break', 'continue'], prev: '*' }
],
// Alphabetical order for enum members
'perfectionist/sort-enums': ['error', { type: 'natural' }],
'perfectionist/sort-objects': ['error', { type: 'natural' }],
// Alphabetical order for variable declarations and object keys
@@ -35,7 +35,7 @@
<span>
Run llama-server with <code>{CLI_FLAGS.TOOLS}</code> flag to enable
<strong>Built-in Tools</strong>.
<strong>Server Tools</strong>.
</span>
</span>
@@ -62,7 +62,7 @@
// it, the picker still opens for manual entry but explains why search is
// unavailable instead of firing searches that would only fail. Browse is
// hidden too: it resolves the picked folder name through the same tool.
const fileSearchKey = $derived(toolsStore.getPermissionKey(BuiltInTool.FILE_GLOB_SEARCH));
const fileSearchKey = $derived(toolsStore.getPermissionKey(BuiltInTool.SERVER_FILE_GLOB_SEARCH));
const fileSearchEnabled = $derived(
fileSearchKey !== null && toolsStore.isToolEnabled(fileSearchKey)
);
@@ -212,7 +212,7 @@
// so the caller fails visibly instead of committing a bare leaf name.
async function resolveNativeName(name: string): Promise<string | null> {
try {
const res = await ToolsService.executeToolRaw(BuiltInTool.FILE_GLOB_SEARCH, {
const res = await ToolsService.executeToolRaw(BuiltInTool.SERVER_FILE_GLOB_SEARCH, {
include: buildCaseInsensitiveGlob(name),
limit: SEARCH.NATIVE_LIMIT,
max_depth: SEARCH.NATIVE_MAX_DEPTH,
@@ -51,7 +51,7 @@
// When the server does not expose file_glob_search (started without
// --tools) or the user disabled it, the picker still opens but explains
// why instead of firing searches that would only fail.
const fileSearchKey = $derived(toolsStore.getPermissionKey(BuiltInTool.FILE_GLOB_SEARCH));
const fileSearchKey = $derived(toolsStore.getPermissionKey(BuiltInTool.SERVER_FILE_GLOB_SEARCH));
const fileSearchEnabled = $derived(
fileSearchKey !== null && toolsStore.isToolEnabled(fileSearchKey)
);
@@ -35,19 +35,19 @@
{#if isSearchCall}
<ChatMessageToolCallBlockSearchResults {section} {open} {isStreaming} {onToggle} />
{:else if section.toolName === BuiltInTool.GET_DATETIME}
{:else if section.toolName === BuiltInTool.BROWSER_GET_DATETIME}
<ChatMessageToolCallBlockGetDatetime {section} {isStreaming} />
{:else if section.toolName === BuiltInTool.GET_INFO}
{:else if section.toolName === BuiltInTool.SERVER_GET_INFO}
<ChatMessageToolCallBlockGetInfo {section} {isStreaming} />
{:else if section.toolName === BuiltInTool.READ_FILE}
{:else if section.toolName === BuiltInTool.SERVER_READ_FILE}
<ChatMessageToolCallBlockReadFile {section} {open} {isStreaming} {onToggle} />
{:else if section.toolName === BuiltInTool.READ_MEDIA}
{:else if section.toolName === BuiltInTool.BROWSER_READ_MEDIA}
<ChatMessageToolCallBlockReadMedia {section} {open} {isStreaming} {onToggle} />
{:else if section.toolName === BuiltInTool.EDIT_FILE}
{:else if section.toolName === BuiltInTool.SERVER_EDIT_FILE}
<ChatMessageToolCallBlockEditFile {section} {open} {isStreaming} {onToggle} />
{:else if section.toolName === BuiltInTool.WRITE_FILE}
{:else if section.toolName === BuiltInTool.SERVER_WRITE_FILE}
<ChatMessageToolCallBlockWriteFile {section} {open} {isStreaming} {onToggle} />
{:else if section.toolName === BuiltInTool.EXEC_SHELL_COMMAND}
{:else if section.toolName === BuiltInTool.SERVER_EXEC_SHELL_COMMAND}
<ChatMessageToolCallBlockExecShellCommand
{section}
{open}
@@ -56,11 +56,11 @@
{attachments}
{onToggle}
/>
{:else if section.toolName === BuiltInTool.FILE_GLOB_SEARCH}
{:else if section.toolName === BuiltInTool.SERVER_FILE_GLOB_SEARCH}
<ChatMessageToolCallBlockFileGlobSearch {section} {open} {isStreaming} {onToggle} />
{:else if section.toolName === BuiltInTool.GREP_SEARCH}
{:else if section.toolName === BuiltInTool.SERVER_GREP_SEARCH}
<ChatMessageToolCallBlockGrepSearch {section} {open} {isStreaming} {onToggle} />
{:else if section.toolName === BuiltInTool.RUN_JAVASCRIPT}
{:else if section.toolName === BuiltInTool.BROWSER_RUN_JAVASCRIPT}
<ChatMessageToolCallBlockRunJavascript {section} {open} {isStreaming} {onToggle} />
{:else}
<ChatMessageToolCallBlockDefault {section} {open} {isStreaming} {attachments} {onToggle} />
@@ -12,7 +12,7 @@
import {
classifyToolResult,
formatJsonPretty,
getBuiltinToolUi,
getToolUi,
parseToolResultWithMedia
} from '$lib/utils';
import { createBase64DataUrl } from '$lib/utils/data-url';
@@ -27,7 +27,7 @@
let { attachments, isStreaming, onToggle, open, section }: Props = $props();
const title = $derived(getBuiltinToolUi(section.toolName)?.label ?? section.toolName ?? '');
const title = $derived(getToolUi(section.toolName)?.label ?? section.toolName ?? '');
const outputKind = $derived(classifyToolResult(section.toolResult));
const parsedLines: ToolResultLine[] = $derived(
section.toolResult ? parseToolResultWithMedia(section.toolResult, attachments) : []
@@ -6,7 +6,7 @@
import { MAX_HEIGHT_CODE_BLOCK } from '$lib/constants';
import { FileTypeText } from '$lib/enums';
import type { AgenticSection } from '$lib/types';
import { getBuiltinToolUi } from '$lib/utils';
import { getToolUi } from '$lib/utils';
interface Props {
section: AgenticSection;
@@ -18,7 +18,7 @@
let { isStreaming, onToggle, open, section }: Props = $props();
const runJsMeta = $derived(parseRunJavascriptMeta(section));
const title = $derived(getBuiltinToolUi(section.toolName)?.label ?? section.toolName ?? '');
const title = $derived(getToolUi(section.toolName)?.label ?? section.toolName ?? '');
</script>
<ToolCallBlock {section} {open} {isStreaming} meta={runJsMeta} {title} {onToggle}>
@@ -14,8 +14,8 @@
import { ICON_CLASS_DEFAULT, ICON_CLASS_SPIN } from '$lib/constants';
import { AgenticSectionType } from '$lib/enums';
import { mcpStore } from '$lib/stores';
import type { AgenticSection, BuiltinToolUiEntry } from '$lib/types';
import { getBuiltinToolUi } from '$lib/utils';
import type { AgenticSection, ToolUiEntry } from '$lib/types';
import { getToolUi } from '$lib/utils';
import type { Component, Snippet } from 'svelte';
type ToolCallBlockMetaWithError = TMeta & { errorMessage?: string };
@@ -82,7 +82,7 @@
const showSpinner = $derived(isPending || (isStreamingCall && isStreaming) || extraLiveStreaming);
const isCodeStreaming = $derived(isStreaming && (isPending || isStreamingCall));
const toolUi: BuiltinToolUiEntry | null = $derived(getBuiltinToolUi(section.toolName));
const toolUi: ToolUiEntry | null = $derived(getToolUi(section.toolName));
const toolIcon: Component = $derived(
spinIconWhenActive && showSpinner ? Loader2 : (toolUi?.icon ?? Wrench)
);
@@ -24,7 +24,7 @@ export type EditFileMeta = {
};
export function parseEditFileMeta(section: AgenticSection): EditFileMeta | null {
const args = parseToolArgs(BuiltInTool.EDIT_FILE, section, { partial: true });
const args = parseToolArgs(BuiltInTool.SERVER_EDIT_FILE, section, { partial: true });
if (!args) return null;
@@ -14,7 +14,7 @@ export type ExecShellCommandMeta = {
};
export function parseExecShellCommandMeta(section: AgenticSection): ExecShellCommandMeta | null {
const args = parseToolArgs(BuiltInTool.EXEC_SHELL_COMMAND, section);
const args = parseToolArgs(BuiltInTool.SERVER_EXEC_SHELL_COMMAND, section);
if (!args) return null;
@@ -19,7 +19,7 @@ export type FileGlobSearchMeta = {
};
export function parseFileGlobSearchMeta(section: AgenticSection): FileGlobSearchMeta | null {
const args = parseToolArgs(BuiltInTool.FILE_GLOB_SEARCH, section);
const args = parseToolArgs(BuiltInTool.SERVER_FILE_GLOB_SEARCH, section);
if (!args) return null;
@@ -28,7 +28,7 @@ export type GrepSearchMeta = {
};
export function parseGrepSearchMeta(section: AgenticSection): GrepSearchMeta | null {
const args = parseToolArgs(BuiltInTool.GREP_SEARCH, section);
const args = parseToolArgs(BuiltInTool.SERVER_GREP_SEARCH, section);
if (!args) return null;
@@ -16,7 +16,7 @@ export type ReadFileMeta = {
};
export function parseReadFileMeta(section: AgenticSection): ReadFileMeta | null {
const args = parseToolArgs(BuiltInTool.READ_FILE, section, { partial: true });
const args = parseToolArgs(BuiltInTool.SERVER_READ_FILE, section, { partial: true });
if (!args) return null;
@@ -16,7 +16,7 @@ export type RunJavascriptMeta = {
};
export function parseRunJavascriptMeta(section: AgenticSection): RunJavascriptMeta | null {
const args = parseToolArgs(BuiltInTool.RUN_JAVASCRIPT, section);
const args = parseToolArgs(BuiltInTool.BROWSER_RUN_JAVASCRIPT, section);
if (!args) return null;
@@ -20,7 +20,7 @@ export type WriteFileMeta = {
};
export function parseWriteFileMeta(section: AgenticSection): WriteFileMeta | null {
const args = parseToolArgs(BuiltInTool.WRITE_FILE, section, { partial: true });
const args = parseToolArgs(BuiltInTool.SERVER_WRITE_FILE, section, { partial: true });
if (!args) return null;
@@ -61,8 +61,8 @@
{:else}
{@const source = toolsStore.getToolSource(toolName)}
{@const providerName =
source === ToolSource.BUILTIN
? TOOL_SERVER_LABELS[ToolSource.BUILTIN]
source === ToolSource.SERVER
? TOOL_SERVER_LABELS[ToolSource.SERVER]
: source === ToolSource.CUSTOM
? TOOL_SERVER_LABELS[ToolSource.CUSTOM]
: 'MCP Tools'}
@@ -278,7 +278,7 @@ export { default as ChatFormInput } from './ChatForm/ChatFormInput/ChatFormInput
/**
* Working directory selector for agent mode. Renders a chip below the chat
* form; clicking it opens a popover with a directory picker backed by the
* server's `file_glob_search` built-in tool (POST /tools). The picked
* server's `file_glob_search` server tool (POST /tools). The picked
* directory is exposed via `bind:directory`; changing it records a
* synthetic "Set working directory to ..." user message into chat history
* and is enforced on tool calls via the `x-tool-cwd` request header.
@@ -380,7 +380,7 @@ export { default as ChatFormPickerListItemSkeleton } from './ChatForm/ChatFormPi
/**
* `@`-triggered file/folder mention picker. Resolves `@<query>` in the chat
* input to a filesystem match via the server's `file_glob_search` built-in
* input to a filesystem match via the server's `file_glob_search` server tool
* tool, scoped to the conversation cwd (or server home when unset).
* Selection splices a `[name](file:///<abs path>)` link into the input.
*/
@@ -6,7 +6,7 @@
import { ICON_CLASS_DEFAULT } from '$lib/constants';
import { ToolSource } from '$lib/enums/tools.enums';
import { mcpStore, permissionsStore, toolsStore } from '$lib/stores';
import { getBuiltinToolUi } from '$lib/utils';
import { getToolUi } from '$lib/utils';
import { SvelteSet } from 'svelte/reactivity';
let expandedGroups = new SvelteSet<string>();
@@ -69,12 +69,12 @@
{#each group.tools as entry (entry.key)}
{@const toolName = entry.definition.function.name}
{@const builtinUi =
entry.source === ToolSource.BUILTIN || entry.source === ToolSource.FRONTEND
? getBuiltinToolUi(toolName)
{@const toolUi =
entry.source === ToolSource.SERVER || entry.source === ToolSource.BROWSER
? getToolUi(toolName)
: null}
{@const displayLabel = builtinUi?.label ?? toolName}
{@const IconComponent = builtinUi?.icon ?? null}
{@const displayLabel = toolUi?.label ?? toolName}
{@const IconComponent = toolUi?.icon ?? null}
{@const isEnabled = toolsStore.isToolEnabled(entry.key)}
{@const permissionKey = entry.key}
{@const isAlwaysAllowed = permissionsStore.hasTool(permissionKey)}
@@ -69,7 +69,7 @@ export { default as SettingsChatFields } from './SettingsChat/SettingsChatFields
/**
* **SettingsChatToolsTab** - Tools configuration tab for chat settings
*
* Displays available tools grouped by source (built-in, MCP, custom) with
* Displays available tools grouped by source (server, browser, MCP, custom) with
* toggles to enable/disable individual tools and tool groups. Shows MCP
* server favicons and permission management controls.
*/
+3 -1
View File
@@ -2,7 +2,9 @@ import { CLI_FLAGS } from './cli-flags.constants';
import { BuiltInTool, JsonSchemaType, ToolCallType } from '$lib/enums';
import type { OpenAIToolDefinition } from '$lib/types';
export const BROWSER_INFO_TOOL_NAME = BuiltInTool.GET_INFO;
// get_info is served by the server, but the browser falls back to this
// implementation when the server does not provide it - same wire name.
export const BROWSER_INFO_TOOL_NAME = BuiltInTool.SERVER_GET_INFO;
/** UA token to OS name, first match wins - Android and iOS UAs also carry the Linux / Mac OS X tokens */
export const BROWSER_INFO_OS_UA_PATTERNS: readonly [RegExp, string][] = [
@@ -1,52 +0,0 @@
// Registry of built-in and frontend (browser) tools whose renderer
// shows a recognizable icon and friendly label inline in the chat UI.
//
// To add a new built-in tool, add an entry to BUILTIN_TOOL_UI. To give a
// tool a custom title or body renderer, add a dedicated component under
// ChatMessageToolCall/ and route it in ChatMessageToolCallBlock.svelte
// (see ChatMessageToolCallBlockGetDatetime and
// ChatMessageToolCallBlockSearchResults for prior art).
import {
Braces,
Clock,
Eye,
FilePen,
FilePlus,
FileSearch,
FileText,
Info,
SearchCode,
Terminal
} from '@lucide/svelte';
import { BuiltInTool, ToolSource } from '$lib/enums';
import type { BuiltinToolUiEntry } from '$lib/types';
export const BUILTIN_TOOL_UI: Readonly<Record<BuiltInTool, BuiltinToolUiEntry>> = {
[BuiltInTool.EDIT_FILE]: { icon: FilePen, label: 'Edit file', source: ToolSource.BUILTIN },
[BuiltInTool.EXEC_SHELL_COMMAND]: {
icon: Terminal,
label: 'Run command',
source: ToolSource.BUILTIN
},
[BuiltInTool.FILE_GLOB_SEARCH]: {
icon: FileSearch,
label: 'Search files',
source: ToolSource.BUILTIN
},
[BuiltInTool.GET_DATETIME]: { icon: Clock, label: 'Current time', source: ToolSource.FRONTEND },
[BuiltInTool.GET_INFO]: { icon: Info, label: 'Runtime info', source: ToolSource.BUILTIN },
[BuiltInTool.GREP_SEARCH]: {
icon: SearchCode,
label: 'Search in files',
source: ToolSource.BUILTIN
},
[BuiltInTool.READ_FILE]: { icon: FileText, label: 'Read file', source: ToolSource.BUILTIN },
[BuiltInTool.READ_MEDIA]: { icon: Eye, label: 'Read media', source: ToolSource.FRONTEND },
[BuiltInTool.RUN_JAVASCRIPT]: {
icon: Braces,
label: 'Run JavaScript',
source: ToolSource.FRONTEND
},
[BuiltInTool.WRITE_FILE]: { icon: FilePlus, label: 'Write file', source: ToolSource.BUILTIN }
} as const;
+1 -1
View File
@@ -1,7 +1,7 @@
import { BuiltInTool, JsonSchemaType, ToolCallType } from '$lib/enums';
import type { OpenAIToolDefinition } from '$lib/types';
export const GET_DATETIME_TOOL_NAME = BuiltInTool.GET_DATETIME;
export const GET_DATETIME_TOOL_NAME = BuiltInTool.BROWSER_GET_DATETIME;
export function buildGetDatetimeToolDefinition(): OpenAIToolDefinition {
return {
+1 -1
View File
@@ -15,7 +15,7 @@ export * from './context-gauge-popup.constants';
export * from './conversation-import.constants';
export * from './binary-detection.constants';
export * from './content-detection.constants';
export * from './built-in-tools.constants';
export * from './tool-ui.constants';
export * from './cache.constants';
export * from './chat-form.constants';
export * from './cli-flags.constants';
+1 -1
View File
@@ -7,7 +7,7 @@ import {
} from '$lib/enums';
import type { OpenAIToolDefinition } from '$lib/types';
export const READ_MEDIA_TOOL_NAME = BuiltInTool.READ_MEDIA;
export const READ_MEDIA_TOOL_NAME = BuiltInTool.BROWSER_READ_MEDIA;
// header lines of the tool result, parsed back by the read_media renderer
export const PREFIX_FILE = 'File: ';
@@ -1,6 +1,6 @@
import { BuiltInTool } from '$lib/enums';
export const SANDBOX_TOOL_NAME = BuiltInTool.RUN_JAVASCRIPT;
export const SANDBOX_TOOL_NAME = BuiltInTool.BROWSER_RUN_JAVASCRIPT;
export const SANDBOX_TIMEOUT_MS_DEFAULT = 10000;
@@ -0,0 +1,60 @@
// Registry of server and browser tools whose renderer
// shows a recognizable icon and friendly label inline in the chat UI.
//
// To add a new tool, add an entry to TOOL_UI. To give a
// tool a custom title or body renderer, add a dedicated component under
// ChatMessageToolCall/ and route it in ChatMessageToolCallBlock.svelte
// (see ChatMessageToolCallBlockGetDatetime and
// ChatMessageToolCallBlockSearchResults for prior art).
import {
Braces,
Clock,
Eye,
FilePen,
FilePlus,
FileSearch,
FileText,
Info,
SearchCode,
Terminal
} from '@lucide/svelte';
import { BuiltInTool, ToolSource } from '$lib/enums';
import type { ToolUiEntry } from '$lib/types';
export const TOOL_UI: Readonly<Record<BuiltInTool, ToolUiEntry>> = {
[BuiltInTool.BROWSER_GET_DATETIME]: {
icon: Clock,
label: 'Current time',
source: ToolSource.BROWSER
},
[BuiltInTool.BROWSER_READ_MEDIA]: { icon: Eye, label: 'Read media', source: ToolSource.BROWSER },
[BuiltInTool.BROWSER_RUN_JAVASCRIPT]: {
icon: Braces,
label: 'Run JavaScript',
source: ToolSource.BROWSER
},
[BuiltInTool.SERVER_EDIT_FILE]: { icon: FilePen, label: 'Edit file', source: ToolSource.SERVER },
[BuiltInTool.SERVER_EXEC_SHELL_COMMAND]: {
icon: Terminal,
label: 'Run command',
source: ToolSource.SERVER
},
[BuiltInTool.SERVER_FILE_GLOB_SEARCH]: {
icon: FileSearch,
label: 'Search files',
source: ToolSource.SERVER
},
[BuiltInTool.SERVER_GET_INFO]: { icon: Info, label: 'Runtime info', source: ToolSource.SERVER },
[BuiltInTool.SERVER_GREP_SEARCH]: {
icon: SearchCode,
label: 'Search in files',
source: ToolSource.SERVER
},
[BuiltInTool.SERVER_READ_FILE]: { icon: FileText, label: 'Read file', source: ToolSource.SERVER },
[BuiltInTool.SERVER_WRITE_FILE]: {
icon: FilePlus,
label: 'Write file',
source: ToolSource.SERVER
}
} as const;
+4 -4
View File
@@ -18,15 +18,15 @@ export const UI_DATA_ATTRS = {
} as const;
export const TOOL_GROUP_LABELS = {
[ToolSource.BUILTIN]: 'Built-in',
[ToolSource.BROWSER]: 'Browser',
[ToolSource.CUSTOM]: 'JSON Schema',
[ToolSource.FRONTEND]: 'Browser'
[ToolSource.SERVER]: 'Server'
} as const;
export const TOOL_SERVER_LABELS = {
[ToolSource.BUILTIN]: 'Built-in Tools',
[ToolSource.BROWSER]: 'Browser Tools',
[ToolSource.CUSTOM]: 'Custom Tools',
[ToolSource.FRONTEND]: 'Browser Tools'
[ToolSource.SERVER]: 'Server Tools'
} as const;
export const TOOLTIP_DELAY_DURATION = 500;
+6 -6
View File
@@ -9,12 +9,12 @@ export enum ToolCallType {
* Types of sections in agentic content display.
*/
export enum AgenticSectionType {
REASONING = 'reasoning',
REASONING_PENDING = 'reasoning_pending',
TEXT = 'text',
TOOL_CALL = 'tool_call',
TOOL_CALL_PENDING = 'tool_call_pending',
TOOL_CALL_STREAMING = 'tool_call_streaming',
REASONING = 'reasoning',
REASONING_PENDING = 'reasoning_pending'
TOOL_CALL_STREAMING = 'tool_call_streaming'
}
/**
@@ -22,8 +22,8 @@ export enum AgenticSectionType {
*/
export enum ContinueIntentKind {
APPEND_TEXT = 'append_text',
RERUN_TURN = 'rerun_turn',
NEXT_TURN = 'next_turn'
NEXT_TURN = 'next_turn',
RERUN_TURN = 'rerun_turn'
}
/**
@@ -39,7 +39,7 @@ export enum ToolResultKind {
* Line classification for the unified-diff renderer of `edit_file` results.
*/
export enum DiffLineKind {
CONTEXT = 'context',
ADD = 'add',
CONTEXT = 'context',
REMOVE = 'remove'
}
+13 -13
View File
@@ -4,12 +4,12 @@
export enum AttachmentType {
AUDIO = 'AUDIO',
IMAGE = 'IMAGE',
VIDEO = 'VIDEO',
LEGACY_CONTEXT = 'context', // Legacy attachment type for backward compatibility
MCP_PROMPT = 'MCP_PROMPT',
MCP_RESOURCE = 'MCP_RESOURCE',
PDF = 'PDF',
TEXT = 'TEXT',
LEGACY_CONTEXT = 'context' // Legacy attachment type for backward compatibility
VIDEO = 'VIDEO'
}
/**
@@ -17,14 +17,14 @@ export enum AttachmentType {
* Used to select which file upload or attachment action is triggered.
*/
export enum AttachmentMenuItemId {
IMAGES = 'images',
AUDIO = 'audio',
VIDEO = 'video',
TEXT = 'text',
IMAGES = 'images',
MCP_PROMPT = 'mcp-prompt',
MCP_RESOURCES = 'mcp-resources',
PDF = 'pdf',
SYSTEM_MESSAGE = 'system-message',
MCP_PROMPT = 'mcp-prompt',
MCP_RESOURCES = 'mcp-resources'
TEXT = 'text',
VIDEO = 'video'
}
/**
@@ -32,9 +32,9 @@ export enum AttachmentMenuItemId {
*/
export enum AttachmentItemEnabledWhen {
ALWAYS = 'always',
HAS_VISION_MODALITY = 'hasVisionModality',
HAS_AUDIO_MODALITY = 'hasAudioModality',
HAS_VIDEO_MODALITY = 'hasVideoModality'
HAS_VIDEO_MODALITY = 'hasVideoModality',
HAS_VISION_MODALITY = 'hasVisionModality'
}
/**
@@ -42,9 +42,9 @@ export enum AttachmentItemEnabledWhen {
*/
export enum AttachmentAction {
FILE_UPLOAD = 'onFileUpload',
SYSTEM_PROMPT_CLICK = 'onSystemPromptClick',
MCP_PROMPT_CLICK = 'onMcpPromptClick',
MCP_RESOURCES_CLICK = 'onMcpResourcesClick'
MCP_RESOURCES_CLICK = 'onMcpResourcesClick',
SYSTEM_PROMPT_CLICK = 'onSystemPromptClick'
}
/**
@@ -52,9 +52,9 @@ export enum AttachmentAction {
*/
export enum AttachmentLabel {
FILE = 'File',
PDF_FILE = 'PDF File',
MCP_PROMPT = 'MCP Prompt',
MCP_RESOURCE = 'MCP Resource'
MCP_RESOURCE = 'MCP Resource',
PDF_FILE = 'PDF File'
}
/**
@@ -1,5 +1,5 @@
/** String representation of a boolean used in data attributes and persisted values. */
export enum BooleanString {
TRUE = 'true',
FALSE = 'false'
FALSE = 'false',
TRUE = 'true'
}
+24 -24
View File
@@ -1,41 +1,41 @@
export enum ChatMessageStatsView {
GENERATION = 'generation',
READING = 'reading',
TOOLS = 'tools',
SUMMARY = 'summary'
SUMMARY = 'summary',
TOOLS = 'tools'
}
export enum ChatMessageStatisticsMode {
SWITCHABLE = 'switchable',
GENERATION = 'generation',
READING = 'reading',
GENERATION = 'generation'
SWITCHABLE = 'switchable'
}
/**
* Connection state of a streamed completion, drives the resume status indicator.
*/
export enum StreamConnectionState {
STREAMING = 'streaming',
LOST = 'lost',
RESUMING = 'resuming',
LOST = 'lost'
STREAMING = 'streaming'
}
/**
* Reasoning format options for API requests.
*/
export enum ReasoningFormat {
NONE = 'none',
AUTO = 'auto'
AUTO = 'auto',
NONE = 'none'
}
/**
* Message roles for chat messages.
*/
export enum MessageRole {
USER = 'user',
ASSISTANT = 'assistant',
SYSTEM = 'system',
TOOL = 'tool'
TOOL = 'tool',
USER = 'user'
}
/**
@@ -43,27 +43,27 @@ export enum MessageRole {
*/
export enum MessageType {
ROOT = 'root',
SYSTEM = 'system',
TEXT = 'text',
THINK = 'think',
SYSTEM = 'system'
THINK = 'think'
}
/**
* Content part types for API chat message content.
*/
export enum ContentPartType {
TEXT = 'text',
IMAGE_URL = 'image_url',
INPUT_AUDIO = 'input_audio',
INPUT_VIDEO = 'input_video'
INPUT_VIDEO = 'input_video',
TEXT = 'text'
}
/**
* Error dialog types for displaying server/timeout errors.
*/
export enum ErrorDialogType {
TIMEOUT = 'timeout',
SERVER = 'server'
SERVER = 'server',
TIMEOUT = 'timeout'
}
export enum ConversationSelectionMode {
@@ -75,27 +75,27 @@ export enum ConversationSelectionMode {
* PDF view mode options for previewing PDF attachments.
*/
export enum PdfViewMode {
TEXT = 'text',
PAGES = 'pages'
PAGES = 'pages',
TEXT = 'text'
}
export enum ChatFormCommandAction {
PROMPT = 'prompt',
CWD = 'cwd',
MODEL = 'model'
MODEL = 'model',
PROMPT = 'prompt'
}
export enum FileMentionEntryType {
FILE = 'file',
DIRECTORY = 'directory'
DIRECTORY = 'directory',
FILE = 'file'
}
/**
* Kinds of tokens the chat-form-input-rich produces.
*/
export enum ChatFormInputRichTokenKind {
TEXT = 'text',
BADGE = 'badge',
CODE_BLOCK = 'code_block',
CODE_INLINE = 'code_inline',
CODE_BLOCK = 'code_block'
TEXT = 'text'
}
@@ -4,6 +4,6 @@
* message record belongs to it.
*/
export enum SessionRecordType {
SESSION = 'session',
MESSAGE = 'message'
MESSAGE = 'message',
SESSION = 'session'
}
+132 -132
View File
@@ -5,11 +5,11 @@
// File type category enum
export enum FileTypeCategory {
IMAGE = 'image',
AUDIO = 'audio',
VIDEO = 'video',
IMAGE = 'image',
PDF = 'pdf',
TEXT = 'text'
TEXT = 'text',
VIDEO = 'video'
}
/**
@@ -21,13 +21,13 @@ export enum SpecialFileType {
// Specific file type enums for each category
export enum FileTypeImage {
GIF = 'gif',
HEIC = 'heic',
HEIF = 'heif',
JPEG = 'jpeg',
PNG = 'png',
GIF = 'gif',
WEBP = 'webp',
SVG = 'svg',
HEIC = 'heic',
HEIF = 'heif'
WEBP = 'webp'
}
export enum FileTypeAudio {
@@ -46,55 +46,55 @@ export enum FileTypePdf {
}
export enum FileTypeText {
PLAIN_TEXT = 'plainText',
MARKDOWN = 'md',
ASCIIDOC = 'asciidoc',
JAVASCRIPT = 'js',
TYPESCRIPT = 'ts',
JSX = 'jsx',
TSX = 'tsx',
CSS = 'css',
HTML = 'html',
JSON = 'json',
XML = 'xml',
YAML = 'yaml',
CSV = 'csv',
LOG = 'log',
PYTHON = 'python',
JAVA = 'java',
BIBTEX = 'bibtex',
CPP = 'cpp',
PHP = 'php',
RUBY = 'ruby',
CSHARP = 'csharp',
CSS = 'css',
CSV = 'csv',
CUDA = 'cuda',
DART = 'dart',
GO = 'go',
HASKELL = 'haskell',
HTML = 'html',
JAVA = 'java',
JAVASCRIPT = 'js',
JSON = 'json',
JSX = 'jsx',
KOTLIN = 'kotlin',
LATEX = 'latex',
LOG = 'log',
MARKDOWN = 'md',
PHP = 'php',
PLAIN_TEXT = 'plainText',
PROPERTIES = 'properties',
PYTHON = 'python',
R = 'r',
RUBY = 'ruby',
RUST = 'rust',
SCALA = 'scala',
SHELL = 'shell',
SQL = 'sql',
R = 'r',
SCALA = 'scala',
KOTLIN = 'kotlin',
SWIFT = 'swift',
DART = 'dart',
VUE = 'vue',
SVELTE = 'svelte',
LATEX = 'latex',
BIBTEX = 'bibtex',
CUDA = 'cuda',
SWIFT = 'swift',
TSX = 'tsx',
TYPESCRIPT = 'ts',
VUE = 'vue',
VULKAN = 'vulkan',
HASKELL = 'haskell',
CSHARP = 'csharp',
PROPERTIES = 'properties'
XML = 'xml',
YAML = 'yaml'
}
// File extension enums
export enum FileExtensionImage {
JPG = '.jpg',
JPEG = '.jpeg',
PNG = '.png',
GIF = '.gif',
WEBP = '.webp',
SVG = '.svg',
HEIC = '.heic',
HEIF = '.heif'
HEIF = '.heif',
JPEG = '.jpeg',
JPG = '.jpg',
PNG = '.png',
SVG = '.svg',
WEBP = '.webp'
}
export enum FileExtensionAudio {
@@ -112,64 +112,64 @@ export enum FileExtensionPdf {
}
export enum FileExtensionText {
TXT = '.txt',
MD = '.md',
ADOC = '.adoc',
JS = '.js',
TS = '.ts',
JSX = '.jsx',
TSX = '.tsx',
BAT = '.bat',
BIB = '.bib',
C = '.c',
COMP = '.comp',
CPP = '.cpp',
CS = '.cs',
CSS = '.css',
HTML = '.html',
CSV = '.csv',
CU = '.cu',
CUH = '.cuh',
DART = '.dart',
GO = '.go',
H = '.h',
HPP = '.hpp',
HS = '.hs',
HTM = '.htm',
HTML = '.html',
JAVA = '.java',
JS = '.js',
JSON = '.json',
JSONL = '.jsonl',
ZIP = '.zip',
JSX = '.jsx',
KT = '.kt',
LOG = '.log',
MD = '.md',
PHP = '.php',
PROPERTIES = '.properties',
PY = '.py',
R = '.r',
RB = '.rb',
RS = '.rs',
SCALA = '.scala',
SH = '.sh',
SQL = '.sql',
SVELTE = '.svelte',
SWIFT = '.swift',
TEX = '.tex',
TS = '.ts',
TSX = '.tsx',
TXT = '.txt',
VUE = '.vue',
XML = '.xml',
YAML = '.yaml',
YML = '.yml',
CSV = '.csv',
LOG = '.log',
PY = '.py',
JAVA = '.java',
CPP = '.cpp',
C = '.c',
H = '.h',
PHP = '.php',
RB = '.rb',
GO = '.go',
RS = '.rs',
SH = '.sh',
BAT = '.bat',
SQL = '.sql',
R = '.r',
SCALA = '.scala',
KT = '.kt',
SWIFT = '.swift',
DART = '.dart',
VUE = '.vue',
SVELTE = '.svelte',
TEX = '.tex',
BIB = '.bib',
CU = '.cu',
CUH = '.cuh',
COMP = '.comp',
HPP = '.hpp',
HS = '.hs',
PROPERTIES = '.properties',
CS = '.cs'
ZIP = '.zip'
}
// MIME type prefixes and includes for content detection
export enum MimeTypePrefix {
IMAGE = 'image/',
AUDIO = 'audio/',
IMAGE = 'image/',
TEXT = 'text'
}
export enum MimeTypeIncludes {
JSON = 'json',
JAVASCRIPT = 'javascript',
JSON = 'json',
TYPESCRIPT = 'typescript'
}
@@ -182,23 +182,23 @@ export enum UriPattern {
// MIME type enums
export enum MimeTypeApplication {
JSON = 'application/json',
PDF = 'application/pdf',
OCTET_STREAM = 'application/octet-stream',
PDF = 'application/pdf',
ZIP = 'application/zip'
}
export enum MimeTypeAudio {
MP3_MPEG = 'audio/mpeg',
MP3 = 'audio/mp3',
MP3_MPEG = 'audio/mpeg',
MP4 = 'audio/mp4',
VND_WAVE = 'audio/vnd.wave',
WAV = 'audio/wav',
WAVE = 'audio/wave',
X_WAV = 'audio/x-wav',
X_WAVE = 'audio/x-wave',
VND_WAVE = 'audio/vnd.wave',
X_PN_WAV = 'audio/x-pn-wav',
WEBM = 'audio/webm',
WEBM_OPUS = 'audio/webm;codecs=opus'
WEBM_OPUS = 'audio/webm;codecs=opus',
X_PN_WAV = 'audio/x-pn-wav',
X_WAV = 'audio/x-wav',
X_WAVE = 'audio/x-wave'
}
export enum MimeTypeVideo {
@@ -207,62 +207,62 @@ export enum MimeTypeVideo {
}
export enum MimeTypeImage {
GIF = 'image/gif',
HEIC = 'image/heic',
HEIF = 'image/heif',
ICO = 'image/x-icon',
ICO_MICROSOFT = 'image/vnd.microsoft.icon',
JPEG = 'image/jpeg',
JPG = 'image/jpg',
PNG = 'image/png',
GIF = 'image/gif',
WEBP = 'image/webp',
SVG = 'image/svg+xml',
ICO = 'image/x-icon',
ICO_MICROSOFT = 'image/vnd.microsoft.icon',
HEIC = 'image/heic',
HEIF = 'image/heif'
WEBP = 'image/webp'
}
export enum MimeTypeText {
PLAIN = 'text/plain',
MARKDOWN = 'text/markdown',
ASCIIDOC = 'text/asciidoc',
JAVASCRIPT = 'text/javascript',
JAVASCRIPT_APP = 'application/javascript',
TYPESCRIPT = 'text/typescript',
JSX = 'text/jsx',
TSX = 'text/tsx',
CSS = 'text/css',
HTML = 'text/html',
JSON = 'application/json',
JSONL = 'application/jsonl',
XML_TEXT = 'text/xml',
XML_APP = 'application/xml',
YAML_TEXT = 'text/yaml',
YAML_APP = 'application/yaml',
CSV = 'text/csv',
PYTHON = 'text/x-python',
JAVA = 'text/x-java-source',
BAT = 'application/x-bat',
BIBTEX = 'text/x-bibtex',
C_HDR = 'text/x-chdr',
C_SRC = 'text/x-csrc',
CPP_HDR = 'text/x-c++hdr',
CPP_SRC = 'text/x-c++src',
CSHARP = 'text/x-csharp',
HASKELL = 'text/x-haskell',
C_SRC = 'text/x-csrc',
C_HDR = 'text/x-chdr',
PHP = 'text/x-php',
RUBY = 'text/x-ruby',
GO = 'text/x-go',
RUST = 'text/x-rust',
SHELL = 'text/x-shellscript',
BAT = 'application/x-bat',
SQL = 'text/x-sql',
R = 'text/x-r',
SCALA = 'text/x-scala',
KOTLIN = 'text/x-kotlin',
SWIFT = 'text/x-swift',
CSS = 'text/css',
CSV = 'text/csv',
CUDA = 'text/x-cuda',
DART = 'text/x-dart',
VUE = 'text/x-vue',
GO = 'text/x-go',
HASKELL = 'text/x-haskell',
HTML = 'text/html',
JAVA = 'text/x-java-source',
JAVASCRIPT = 'text/javascript',
JAVASCRIPT_APP = 'application/javascript',
JSON = 'application/json',
JSONL = 'application/jsonl',
JSX = 'text/jsx',
KOTLIN = 'text/x-kotlin',
LATEX = 'application/x-latex',
MARKDOWN = 'text/markdown',
PHP = 'text/x-php',
PLAIN = 'text/plain',
PROPERTIES = 'text/properties',
PYTHON = 'text/x-python',
R = 'text/x-r',
RUBY = 'text/x-ruby',
RUST = 'text/x-rust',
SCALA = 'text/x-scala',
SHELL = 'text/x-shellscript',
SQL = 'text/x-sql',
SVELTE = 'text/x-svelte',
SWIFT = 'text/x-swift',
TEX = 'text/x-tex',
TEX_APP = 'application/x-tex',
LATEX = 'application/x-latex',
BIBTEX = 'text/x-bibtex',
CUDA = 'text/x-cuda',
PROPERTIES = 'text/properties'
TSX = 'text/tsx',
TYPESCRIPT = 'text/typescript',
VUE = 'text/x-vue',
XML_APP = 'application/xml',
XML_TEXT = 'text/xml',
YAML_APP = 'application/yaml',
YAML_TEXT = 'text/yaml'
}
+5 -5
View File
@@ -2,19 +2,19 @@
* Keyboard key names for event handling
*/
export enum KeyboardKey {
ENTER = 'Enter',
ESCAPE = 'Escape',
ARROW_UP = 'ArrowUp',
ARROW_DOWN = 'ArrowDown',
ARROW_LEFT = 'ArrowLeft',
ARROW_RIGHT = 'ArrowRight',
TAB = 'Tab',
ARROW_UP = 'ArrowUp',
B_LOWER = 'b',
D_LOWER = 'd',
D_UPPER = 'D',
E_UPPER = 'E',
ENTER = 'Enter',
ESCAPE = 'Escape',
K_LOWER = 'k',
O_LOWER = 'o',
O_UPPER = 'O',
SPACE = ' '
SPACE = ' ',
TAB = 'Tab'
}
+17 -17
View File
@@ -2,61 +2,61 @@
* Connection lifecycle phases for MCP protocol
*/
export enum MCPConnectionPhase {
IDLE = 'idle',
TRANSPORT_CREATING = 'transport_creating',
TRANSPORT_READY = 'transport_ready',
INITIALIZING = 'initializing',
CAPABILITIES_EXCHANGED = 'capabilities_exchanged',
LISTING_TOOLS = 'listing_tools',
CONNECTED = 'connected',
DISCONNECTED = 'disconnected',
ERROR = 'error',
DISCONNECTED = 'disconnected'
IDLE = 'idle',
INITIALIZING = 'initializing',
LISTING_TOOLS = 'listing_tools',
TRANSPORT_CREATING = 'transport_creating',
TRANSPORT_READY = 'transport_ready'
}
/**
* Log level for connection events
*/
export enum MCPLogLevel {
ERROR = 'error',
INFO = 'info',
WARN = 'warn',
ERROR = 'error'
WARN = 'warn'
}
/**
* Transport types for MCP connections
*/
export enum MCPTransportType {
WEBSOCKET = 'websocket',
SSE = 'sse',
STREAMABLE_HTTP = 'streamable_http',
SSE = 'sse'
WEBSOCKET = 'websocket'
}
/**
* Health check status for MCP servers
*/
export enum HealthCheckStatus {
IDLE = 'idle',
CONNECTING = 'connecting',
SUCCESS = 'success',
ERROR = 'error'
ERROR = 'error',
IDLE = 'idle',
SUCCESS = 'success'
}
/**
* Content types for MCP tool results
*/
export enum MCPContentType {
TEXT = 'text',
IMAGE = 'image',
RESOURCE = 'resource'
RESOURCE = 'resource',
TEXT = 'text'
}
/**
* JSON Schema types used in MCP tool definitions
*/
export enum JsonSchemaType {
NUMBER = 'number',
OBJECT = 'object',
STRING = 'string',
NUMBER = 'number'
STRING = 'string'
}
/**
+3 -3
View File
@@ -1,6 +1,6 @@
export enum ModelModality {
TEXT = 'TEXT',
AUDIO = 'AUDIO',
VISION = 'VISION',
VIDEO = 'VIDEO'
TEXT = 'TEXT',
VIDEO = 'VIDEO',
VISION = 'VISION'
}
@@ -4,9 +4,9 @@
*/
export enum ReasoningEffort {
DEFAULT = 'default',
OFF = 'off',
LOW = 'low',
MEDIUM = 'medium',
HIGH = 'high',
MAX = 'max'
LOW = 'low',
MAX = 'max',
MEDIUM = 'medium',
OFF = 'off'
}
+8 -8
View File
@@ -13,11 +13,11 @@ export enum ServerRole {
* Used as the `value` field in the status object from /models endpoint
*/
export enum ServerModelStatus {
UNLOADED = 'unloaded',
LOADING = 'loading',
FAILED = 'failed',
LOADED = 'loaded',
LOADING = 'loading',
SLEEPING = 'sleeping',
FAILED = 'failed'
UNLOADED = 'unloaded'
}
/**
@@ -26,10 +26,10 @@ export enum ServerModelStatus {
* tools/server/server-models.cpp from the C++ server.
*/
export enum ServerModelsSseEventType {
STATUS_CHANGE = 'status_change',
MODEL_STATUS = 'model_status',
STATUS_UPDATE = 'status_update',
MODELS_RELOAD = 'models_reload',
DOWNLOAD_PROGRESS = 'download_progress',
MODEL_REMOVE = 'model_remove',
DOWNLOAD_PROGRESS = 'download_progress'
MODEL_STATUS = 'model_status',
MODELS_RELOAD = 'models_reload',
STATUS_CHANGE = 'status_change',
STATUS_UPDATE = 'status_update'
}
+7 -7
View File
@@ -2,26 +2,26 @@
* Parameter source - indicates whether a parameter uses default or custom value
*/
export enum ParameterSource {
DEFAULT = 'default',
CUSTOM = 'custom'
CUSTOM = 'custom',
DEFAULT = 'default'
}
/**
* Syncable parameter type - data types for parameters that can be synced with server
*/
export enum SyncableParameterType {
BOOLEAN = 'boolean',
NUMBER = 'number',
STRING = 'string',
BOOLEAN = 'boolean'
STRING = 'string'
}
/**
* Settings field type - defines the input type for settings fields
*/
export enum SettingsFieldType {
INPUT = 'input',
TEXTAREA = 'textarea',
CHECKBOX = 'checkbox',
INPUT = 'input',
RADIO = 'radio',
SELECT = 'select',
RADIO = 'radio'
TEXTAREA = 'textarea'
}
+2 -2
View File
@@ -2,6 +2,6 @@
* Splash screen orientation for iOS apple-touch-startup-image
*/
export enum SplashOrientation {
PORTRAIT = 'portrait',
LANDSCAPE = 'landscape'
LANDSCAPE = 'landscape',
PORTRAIT = 'portrait'
}
+30 -24
View File
@@ -1,20 +1,20 @@
export enum ToolSource {
BUILTIN = 'builtin',
MCP = 'mcp',
BROWSER = 'browser',
CUSTOM = 'custom',
FRONTEND = 'frontend'
MCP = 'mcp',
SERVER = 'server'
}
export enum ToolPermissionDecision {
ALWAYS = 'always',
ALWAYS_SERVER = 'always_server',
ONCE = 'once',
DENY = 'deny'
DENY = 'deny',
ONCE = 'once'
}
export enum ToolResponseField {
PLAIN_TEXT = 'plain_text_response',
ERROR = 'error'
ERROR = 'error',
PLAIN_TEXT = 'plain_text_response'
}
/**
@@ -22,28 +22,34 @@ export enum ToolResponseField {
* Mirrors the server-side validation in server-tools.cpp.
*/
export enum GlobSearchType {
FILE = 'file',
ALL = 'all',
DIR = 'dir',
ALL = 'all'
FILE = 'file'
}
/**
* Wire-format identifiers for built-in and frontend tools. The string
* Wire-format identifiers for server and browser tools. The string
* value matches what the model emits in tool call names, so comparing
* against `BuiltInTool.READ_FILE` is equivalent to comparing against the
* raw `'read_file'` literal - the enum just keeps the two in lock-step
* and gives TypeScript a single source of truth for autocomplete / rename
* support.
* against `BuiltInTool.SERVER_READ_FILE` is equivalent to comparing
* against the raw `'read_file'` literal - the enum just keeps the two in
* lock-step and gives TypeScript a single source of truth for autocomplete
* / rename support.
*
* The `SERVER_` / `BROWSER_` prefixes mirror the tool's primary source
* (llama-server vs llama-ui). `get_info` is the exception: it is served by
* the server, but llama-ui falls back to a browser implementation when the
* server does not provide it, so it can surface under both categories in
* the UI while keeping a single wire name.
*/
export enum BuiltInTool {
READ_FILE = 'read_file',
READ_MEDIA = 'read_media',
EDIT_FILE = 'edit_file',
WRITE_FILE = 'write_file',
GET_DATETIME = 'get_datetime',
GET_INFO = 'get_info',
FILE_GLOB_SEARCH = 'file_glob_search',
GREP_SEARCH = 'grep_search',
EXEC_SHELL_COMMAND = 'exec_shell_command',
RUN_JAVASCRIPT = 'run_javascript'
BROWSER_GET_DATETIME = 'get_datetime',
BROWSER_READ_MEDIA = 'read_media',
BROWSER_RUN_JAVASCRIPT = 'run_javascript',
SERVER_EDIT_FILE = 'edit_file',
SERVER_EXEC_SHELL_COMMAND = 'exec_shell_command',
SERVER_FILE_GLOB_SEARCH = 'file_glob_search',
SERVER_GET_INFO = 'get_info',
SERVER_GREP_SEARCH = 'grep_search',
SERVER_READ_FILE = 'read_file',
SERVER_WRITE_FILE = 'write_file'
}
+9 -9
View File
@@ -1,22 +1,22 @@
export enum ColorMode {
LIGHT = 'light',
DARK = 'dark',
LIGHT = 'light',
SYSTEM = 'system'
}
export enum TooltipSide {
TOP = 'top',
RIGHT = 'right',
BOTTOM = 'bottom',
LEFT = 'left'
LEFT = 'left',
RIGHT = 'right',
TOP = 'top'
}
/**
* MCP prompt display variant
*/
export enum McpPromptVariant {
MESSAGE = 'message',
ATTACHMENT = 'attachment'
ATTACHMENT = 'attachment',
MESSAGE = 'message'
}
/**
@@ -39,8 +39,8 @@ export enum HtmlInputType {
* Alert level that drives the context gauge dial color.
*/
export enum ColorLevel {
OK = 'ok',
WARNING = 'warning',
CRITICAL = 'critical',
NEUTRAL = 'neutral'
NEUTRAL = 'neutral',
OK = 'ok',
WARNING = 'warning'
}
@@ -46,13 +46,13 @@ export function useToolsPanel(): UseToolsPanelReturn {
// Tools endpoint is unreachable (404) — server started without --tools
if (toolsStore.isToolsEndpointUnreachable) {
return `To enable Built-In Tools you need to run llama-server with ${CLI_FLAGS.TOOLS} all or ${CLI_FLAGS.TOOLS} <name> flag. To see MCP Tools you need to add / enable MCP Server(s).`;
return `To enable Server Tools you need to run llama-server with ${CLI_FLAGS.TOOLS} all or ${CLI_FLAGS.TOOLS} <name> flag. To see MCP Tools you need to add / enable MCP Server(s).`;
}
// Other errors — return null so UI shows "Failed to load tools"
if (toolsStore.error) return null;
return `To enable Built-In Tools you need to run llama-server with ${CLI_FLAGS.TOOLS} all or ${CLI_FLAGS.TOOLS} <name> flag. To see MCP Tools you need to add / enable MCP Server(s).`;
return `To enable Server Tools you need to run llama-server with ${CLI_FLAGS.TOOLS} all or ${CLI_FLAGS.TOOLS} <name> flag. To see MCP Tools you need to add / enable MCP Server(s).`;
});
function isGroupChecked(group: ToolGroup): boolean {
@@ -95,8 +95,8 @@ export function useToolsPanel(): UseToolsPanelReturn {
}
function handleOpen(): void {
if (toolsStore.builtinTools.length === 0 && !toolsStore.loading) {
toolsStore.fetchBuiltinTools();
if (toolsStore.serverTools.length === 0 && !toolsStore.loading) {
toolsStore.fetchServerTools();
}
mcpStore.runHealthChecksForServers(mcpStore.getServers().filter((s) => s.enabled));
+3 -3
View File
@@ -262,9 +262,9 @@ export { ParameterSyncService } from './parameter-sync.service';
export { MCPService } from './mcp.service';
/**
* **SandboxService** - Frontend JavaScript execution in a browser sandbox
* **SandboxService** - Browser JavaScript execution in a browser sandbox
*
* Stateless executor for the run_javascript frontend tool. Model generated
* Stateless executor for the run_javascript browser tool. Model generated
* code runs in a Web Worker spawned inside a sandboxed iframe with an opaque
* origin: no access to the app origin, its storage or its API, and outgoing
* requests carry a null origin. The code never touches a main thread, so the
@@ -274,7 +274,7 @@ export { MCPService } from './mcp.service';
* **Architecture & Relationships:**
* - **SandboxService** (this class): Stateless sandbox execution
* - **toolsStore**: Exposes the tool definition when the sandbox is enabled
* - **agenticStore**: Dispatches ToolSource.FRONTEND calls here
* - **agenticStore**: Dispatches ToolSource.BROWSER calls here
*
* @see buildSandboxToolDefinition in utils/sandbox-tool - tool schema sent to the LLM
* @see agenticStore in stores/agentic.svelte.ts - tool dispatch
@@ -28,15 +28,15 @@ function fileExtension(path: string): string {
}
/**
* **ReadMediaService** - frontend executor for the `read_media` tool
* **ReadMediaService** - browser executor for the `read_media` tool
*
* The tool is synthetic: no such tool exists on the server. It reads the file
* through the built-in `read_file` tool with the `base64` response type, then
* through the server `read_file` tool with the `base64` response type, then
* turns the bytes into a data URI line. The agentic store lifts that line into
* an image or audio attachment on the tool result message, which is what makes
* the model perceive the file instead of reading a wall of base64.
*
* Living in the frontend is what lets it exist only for models that can
* Living in the browser is what lets it exist only for models that can
* actually use the result - the server has no idea which model is selected.
*
* @see buildReadMediaToolDefinition in constants/read-media.ts - tool schema sent to the LLM
@@ -82,7 +82,7 @@ export class ReadMediaService {
}
const raw = await ToolsService.executeToolRaw(
BuiltInTool.READ_FILE,
BuiltInTool.SERVER_READ_FILE,
{ path },
signal,
cwd,
+2 -2
View File
@@ -68,7 +68,7 @@ function formatReply(reply: SandboxReply): ToolExecutionResult {
export class SandboxService {
/**
* Execute a frontend sandbox tool call and return its output.
* Execute a browser sandbox tool call and return its output.
* One disposable iframe per execution, removed on completion,
* timeout or abort. Removing the iframe terminates the worker
* at the browser level, so runaway code cannot outlive it.
@@ -79,7 +79,7 @@ export class SandboxService {
signal?: AbortSignal
): Promise<ToolExecutionResult> {
if (toolName !== SANDBOX_TOOL_NAME) {
return { content: `Unknown frontend tool: ${toolName}`, isError: true };
return { content: `Unknown browser tool: ${toolName}`, isError: true };
}
const code = typeof params.code === 'string' ? params.code : '';
+7 -7
View File
@@ -1,23 +1,23 @@
import { base } from '$app/paths';
import { API_TOOLS, HEADERS } from '$lib/constants';
import { ToolResponseField } from '$lib/enums';
import type { ServerBuiltinToolInfo, ToolExecutionResult } from '$lib/types';
import type { ServerToolInfo, ToolExecutionResult } from '$lib/types';
import { apiFetch } from '$lib/utils';
import { getJsonHeaders } from '$lib/utils/api-headers';
import { parseSseJsonStream, type SseJsonEvent } from '$lib/utils/sse';
export class ToolsService {
/**
* Fetch the list of built-in tools from the server.
* Fetch the list of server tools from the server.
*
* @returns Array of tool definitions in OpenAI-compatible format
*/
static async list(): Promise<ServerBuiltinToolInfo[]> {
return apiFetch<ServerBuiltinToolInfo[]>(API_TOOLS.LIST);
static async list(): Promise<ServerToolInfo[]> {
return apiFetch<ServerToolInfo[]>(API_TOOLS.LIST);
}
/**
* Execute a built-in tool on the server.
* Execute a server tool on the server.
*
* @param cwd - Working directory for the tool call, sent as the
* x-tool-cwd request header. The server resolves relative paths
@@ -48,7 +48,7 @@ export class ToolsService {
}
/**
* Execute a built-in tool and return the raw JSON response. Unlike
* Execute a server tool and return the raw JSON response. Unlike
* executeTool, this preserves structured fields (e.g. file_glob_search's
* `entries` and `base`) that the flattened ToolExecutionResult drops.
*
@@ -77,7 +77,7 @@ export class ToolsService {
}
/**
* Stream a built-in tool's output chunks from the server. The server
* Stream a server tool's output chunks from the server. The server
* `POST /tools` endpoint with `{stream: true}` emits `data: {"chunk": "..."}`
* events followed by a terminal `data: {"done": true}` (optionally with
* `error`). Yields the chunk string for each partial event.
+12 -12
View File
@@ -326,8 +326,8 @@ class AgenticStore {
const maxTurns = Number(settings.agenticMaxTurns) || DEFAULT_AGENTIC_CONFIG.maxTurns;
const hasTools =
mcpStore.hasEnabledServers(perChatOverrides) ||
toolsStore.builtinTools.length > 0 ||
toolsStore.frontendTools.length > 0 ||
toolsStore.serverTools.length > 0 ||
toolsStore.browserTools.length > 0 ||
toolsStore.customTools.length > 0;
return {
@@ -455,9 +455,9 @@ class AgenticStore {
this._continueResolvers.delete(conversationId);
this._steeringMessages.delete(conversationId);
// Ensure built-in tools are fetched before checking if agentic is enabled
if (toolsStore.builtinTools.length === 0 && !toolsStore.loading) {
await toolsStore.fetchBuiltinTools();
// Ensure server tools are fetched before checking if agentic is enabled
if (toolsStore.serverTools.length === 0 && !toolsStore.loading) {
await toolsStore.fetchServerTools();
}
const agenticConfig = this.getConfig(settingsStore.config, perChatOverrides);
@@ -906,8 +906,8 @@ class AgenticStore {
} else {
try {
if (
toolSource === ToolSource.BUILTIN &&
toolName === BuiltInTool.EXEC_SHELL_COMMAND &&
toolSource === ToolSource.SERVER &&
toolName === BuiltInTool.SERVER_EXEC_SHELL_COMMAND &&
createToolResultMessage &&
updateToolResultMessage
) {
@@ -938,7 +938,7 @@ class AgenticStore {
}
}
result = accumulated;
} else if (toolSource === ToolSource.BUILTIN) {
} else if (toolSource === ToolSource.SERVER) {
const args = this.parseToolArguments(toolCall.function.arguments);
const cwd = conversationsStore.activeConversation?.cwd;
const executionResult = await ToolsService.executeTool(toolName, args, signal, cwd);
@@ -946,16 +946,16 @@ class AgenticStore {
result = executionResult.content;
if (executionResult.isError) toolSuccess = false;
} else if (toolSource === ToolSource.FRONTEND) {
} else if (toolSource === ToolSource.BROWSER) {
const args = this.parseToolArguments(toolCall.function.arguments);
let executionResult: ToolExecutionResult;
if (toolName === BuiltInTool.GET_DATETIME) {
if (toolName === BuiltInTool.BROWSER_GET_DATETIME) {
executionResult = executeGetDatetimeTool();
} else if (toolName === BuiltInTool.GET_INFO) {
} else if (toolName === BuiltInTool.SERVER_GET_INFO) {
executionResult = executeBrowserInfoTool();
} else if (toolName === BuiltInTool.READ_MEDIA) {
} else if (toolName === BuiltInTool.BROWSER_READ_MEDIA) {
executionResult = await ReadMediaService.executeTool(
args,
{
+37 -37
View File
@@ -28,11 +28,11 @@ import { SvelteMap, SvelteSet } from 'svelte/reactivity';
/** Stable selection identity for a tool, shared by the disabled set and the permission store */
class ToolsStore {
private _builtinTools = $state<OpenAIToolDefinition[]>([]);
private _serverTools = $state<OpenAIToolDefinition[]>([]);
private _loading = $state(false);
private _error = $state<string | null>(null);
private _disabledTools = $state(new SvelteSet<string>());
// builtin tools that resolve their paths against the working directory,
// server tools that resolve their paths against the working directory,
// as declared by the server in its `/tools` listing
private _cwdAwareTools = $state(new SvelteSet<string>());
private _toolsEndpointUnreachable = $state(false);
@@ -58,7 +58,7 @@ class ToolsStore {
console.error('[ToolsStore] Failed to load disabled tools from localStorage:', err);
}
this.fetchBuiltinTools();
this.fetchServerTools();
}
private persistDisabledTools(): void {
@@ -78,10 +78,10 @@ class ToolsStore {
return serverId ? `mcp-${serverId}:${name}` : `mcp:${name}`;
case ToolSource.CUSTOM:
return `custom:${name}`;
case ToolSource.FRONTEND:
return `frontend:${name}`;
case ToolSource.BROWSER:
return `browser:${name}`;
default:
return `builtin:${name}`;
return `server:${name}`;
}
}
@@ -164,8 +164,8 @@ class ToolsStore {
};
}
get builtinTools(): OpenAIToolDefinition[] {
return this._builtinTools;
get serverTools(): OpenAIToolDefinition[] {
return this._serverTools;
}
get serverHome(): string | null {
@@ -176,7 +176,7 @@ class ToolsStore {
return this.mcpEntries().map((e) => e.definition);
}
get frontendTools(): OpenAIToolDefinition[] {
get browserTools(): OpenAIToolDefinition[] {
const tools: OpenAIToolDefinition[] = [buildGetDatetimeToolDefinition()];
if (settingsStore.config.jsSandboxEnabled) {
@@ -188,25 +188,25 @@ class ToolsStore {
if (readMedia) tools.push(readMedia);
// provide browser's get_info tool if server doesn't provide one
if (!this.hasBuiltinTool(BuiltInTool.GET_INFO)) {
if (!this.hasServerTool(BuiltInTool.SERVER_GET_INFO)) {
tools.push(buildBrowserInfoToolDefinition());
}
return tools;
}
private hasBuiltinTool(name: BuiltInTool): boolean {
return this._builtinTools.some((def) => def.function.name === name);
private hasServerTool(name: BuiltInTool): boolean {
return this._serverTools.some((def) => def.function.name === name);
}
/**
* `read_media` runs in the frontend on top of the server's `read_file`, so it
* `read_media` runs in the browser on top of the server's `read_file`, so it
* exists only when that tool is served and the active model can perceive the
* bytes. The server cannot make this call - it does not know which model the
* conversation uses.
*/
private readMediaTool(): OpenAIToolDefinition | null {
if (!this.hasBuiltinTool(BuiltInTool.READ_FILE)) return null;
if (!this.hasServerTool(BuiltInTool.SERVER_READ_FILE)) return null;
const model = modelsStore.selectedModelName ?? modelsStore.models[0]?.model ?? '';
@@ -304,23 +304,23 @@ class ToolsStore {
entries.push(entry);
};
for (const def of this._builtinTools) {
for (const def of this._serverTools) {
const name = def.function.name;
push({
definition: def,
key: this.toolKey(ToolSource.BUILTIN, name),
source: ToolSource.BUILTIN
key: this.toolKey(ToolSource.SERVER, name),
source: ToolSource.SERVER
});
}
for (const def of this.frontendTools) {
for (const def of this.browserTools) {
const name = def.function.name;
push({
definition: def,
key: this.toolKey(ToolSource.FRONTEND, name),
source: ToolSource.FRONTEND
key: this.toolKey(ToolSource.BROWSER, name),
source: ToolSource.BROWSER
});
}
@@ -384,17 +384,17 @@ class ToolsStore {
return entry.serverName ?? '';
case ToolSource.CUSTOM:
return TOOL_GROUP_LABELS[ToolSource.CUSTOM];
case ToolSource.FRONTEND:
return TOOL_GROUP_LABELS[ToolSource.FRONTEND];
case ToolSource.BROWSER:
return TOOL_GROUP_LABELS[ToolSource.BROWSER];
default:
return TOOL_GROUP_LABELS[ToolSource.BUILTIN];
return TOOL_GROUP_LABELS[ToolSource.SERVER];
}
}
/**
* Enabled tool definitions for sending to the LLM.
* MCP tool schemas are normalized here so the wire payload is consistent
* across all four sources (built-in, frontend/sandbox, MCP, custom JSON).
* across all four sources (server, browser/sandbox, MCP, custom JSON).
* The API identifies tools by name, so a name is sent at most once.
*/
getEnabledToolsForLLM(): OpenAIToolDefinition[] {
@@ -417,8 +417,8 @@ class ToolsStore {
result.push(def);
};
for (const def of this._builtinTools) take(def);
for (const def of this.frontendTools) take(def);
for (const def of this._serverTools) take(def);
for (const def of this.browserTools) take(def);
// mcpEntries() over mcpStore directly so wire shape stays normalized and aligned with the tools UI.
for (const entry of this.mcpEntries()) take(entry.definition);
for (const def of this.customTools) take(def);
@@ -542,11 +542,11 @@ class ToolsStore {
if (entry.serverName) return mcpStore.getServerDisplayName(entry.serverName);
if (entry.source === ToolSource.BUILTIN) return TOOL_SERVER_LABELS[ToolSource.BUILTIN];
if (entry.source === ToolSource.SERVER) return TOOL_SERVER_LABELS[ToolSource.SERVER];
if (entry.source === ToolSource.CUSTOM) return TOOL_SERVER_LABELS[ToolSource.CUSTOM];
if (entry.source === ToolSource.FRONTEND) return TOOL_SERVER_LABELS[ToolSource.FRONTEND];
if (entry.source === ToolSource.BROWSER) return TOOL_SERVER_LABELS[ToolSource.BROWSER];
return '';
}
@@ -556,27 +556,27 @@ class ToolsStore {
return this.findEntryByName(toolName)?.key ?? null;
}
/** Check if there are any enabled tools available (builtin, MCP, or custom) */
/** Check if there are any enabled tools available (server, MCP, or custom) */
get hasEnabledTools(): boolean {
return this.getEnabledToolsForLLM().length > 0;
}
/**
* Check if a working directory is worth setting: at least one builtin tool
* Check if a working directory is worth setting: at least one server tool
* that reads it is both served and left enabled by the user.
*/
get hasEnabledCwdTools(): boolean {
return this._builtinTools.some((def) => {
return this._serverTools.some((def) => {
const name = def.function.name;
return (
this._cwdAwareTools.has(name) &&
!this._disabledTools.has(this.toolKey(ToolSource.BUILTIN, name))
!this._disabledTools.has(this.toolKey(ToolSource.SERVER, name))
);
});
}
async fetchBuiltinTools(): Promise<void> {
async fetchServerTools(): Promise<void> {
if (this._loading) return;
this._loading = true;
@@ -586,7 +586,7 @@ class ToolsStore {
try {
const toolInfos = await ToolsService.list();
this._builtinTools = toolInfos.map((info) => info.definition);
this._serverTools = toolInfos.map((info) => info.definition);
this._cwdAwareTools = new SvelteSet(
toolInfos.filter((info) => info.uses_cwd).map((info) => info.tool)
);
@@ -599,9 +599,9 @@ class ToolsStore {
// TODO: check status code instead of relying on message
if (errorMessage.includes('this feature is disabled')) {
this._toolsEndpointUnreachable = true;
console.info('[ToolsStore] Built-in tools are disabled on the server');
console.info('[ToolsStore] Server tools are disabled on the server');
} else {
console.error('[ToolsStore] Failed to fetch built-in tools:', err);
console.error('[ToolsStore] Failed to fetch server tools:', err);
}
} finally {
this._loading = false;
@@ -618,7 +618,7 @@ class ToolsStore {
if (this._serverHome !== undefined) return this._serverHome;
try {
const res = await ToolsService.executeToolRaw(BuiltInTool.FILE_GLOB_SEARCH, {
const res = await ToolsService.executeToolRaw(BuiltInTool.SERVER_FILE_GLOB_SEARCH, {
limit: 1,
max_depth: 1,
path: HOME_TILDE,
+2 -2
View File
@@ -147,7 +147,7 @@ export type {
ServerStatus,
ToolCallParams,
ToolExecutionResult,
ServerBuiltinToolInfo,
ServerToolInfo,
Tool,
Prompt,
GetPromptResult,
@@ -208,7 +208,7 @@ export type {
export type { DesktopIconStripItem } from './navigation';
// Tools types
export type { ToolEntry, ToolGroup, BuiltinToolUiEntry } from './tools';
export type { ToolEntry, ToolGroup, ToolUiEntry } from './tools';
// Reasoning
export type { ReasoningEffortLevel } from './reasoning';
+2 -2
View File
@@ -285,10 +285,10 @@ export interface ToolExecutionResult {
isError: boolean;
}
export interface ServerBuiltinToolInfo {
export interface ServerToolInfo {
display_name: string;
tool: string;
type: ToolSource.BUILTIN;
type: ToolSource.SERVER;
permissions: {
write: boolean;
};
+4 -4
View File
@@ -3,12 +3,12 @@ import type { ToolSource } from '$lib/enums';
import type { Component } from 'svelte';
/**
* UI metadata for a built-in or frontend tool, keyed by its `BuiltInTool` id.
* UI metadata for a server or browser tool, keyed by its `BuiltInTool` id.
*/
export interface BuiltinToolUiEntry {
export interface ToolUiEntry {
icon: Component;
label: string;
source: ToolSource.BUILTIN | ToolSource.FRONTEND;
source: ToolSource.SERVER | ToolSource.BROWSER;
}
export interface ToolEntry {
@@ -17,7 +17,7 @@ export interface ToolEntry {
serverName?: string;
/** For MCP tools, the server ID (used for permission keys) */
serverId?: string;
/** Stable selection identity: builtin:name, mcp-<serverId>:name, mcp:name, custom:name */
/** Stable selection identity: server:name, mcp-<serverId>:name, mcp:name, custom:name */
key: string;
definition: OpenAIToolDefinition;
}
-13
View File
@@ -1,13 +0,0 @@
import { BUILTIN_TOOL_UI } from '$lib/constants';
import type { BuiltinToolUiEntry } from '$lib/types';
/**
* Resolve the UI metadata (label + icon) for a built-in tool by its name.
* Falls back to null for unknown or non-built-in tools so callers can render
* a generic chrome instead.
*/
export function getBuiltinToolUi(toolName: string | undefined): BuiltinToolUiEntry | null {
if (!toolName) return null;
return (BUILTIN_TOOL_UI as Record<string, BuiltinToolUiEntry>)[toolName] ?? null;
}
+1 -1
View File
@@ -1,5 +1,5 @@
/**
* Frontend executor for the `get_datetime` tool. It runs in the browser, so it
* Browser executor for the `get_datetime` tool. It runs in the browser, so it
* reports the user's own clock and time zone instead of the server's UTC time -
* a chat about "tomorrow" means the user's tomorrow, not the host's.
*
+1 -1
View File
@@ -42,7 +42,7 @@ export async function runGlobSearch(
}
const res = await ToolsService.executeToolRaw(
BuiltInTool.FILE_GLOB_SEARCH,
BuiltInTool.SERVER_FILE_GLOB_SEARCH,
{ include: args.include, limit, max_depth: args.maxDepth, path: args.path, type },
signal
);
+4 -4
View File
@@ -127,7 +127,7 @@ export { sanitizeKeyValuePairKey, sanitizeKeyValuePairValue } from './sanitize';
// Image error fallback utilities
export { getImageErrorFallbackHtml } from './image-error-fallback';
// SSE-with-JSON stream iterator (used by built-in tool streaming, decoupled
// SSE-with-JSON stream iterator (used by server tool streaming, decoupled
// from chat.service.ts which embeds its own SSE parser for resume support)
export { parseSseJsonStream } from './sse';
@@ -310,7 +310,7 @@ export {
withAbortSignal
} from './abort';
// Tool-call meta utilities. Parsers for each built-in tool live next to
// Tool-call meta utilities. Parsers for each server tool live next to
// their renderer family under
// `src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/`.
// This module only carries the helpers that genuinely cross tool
@@ -321,7 +321,7 @@ export { tryParseToolResultObject } from './tool-call-meta';
// Per-tool UI metadata (label + icon) used by the tool-call chrome.
// Re-exported through $lib/utils so renderer components can read the
// label without depending on $lib/constants directly.
export { getBuiltinToolUi } from './built-in-tools';
export { getToolUi } from './tool-ui';
// Chat command picker
@@ -331,7 +331,7 @@ export { getChatCommands } from './chat-commands';
// SANDBOX_TOOL_DEFINITION is deprecated; kept for backward compatibility.
export { buildSandboxToolDefinition, SANDBOX_TOOL_DEFINITION } from './sandbox-tool';
// Frontend `get_datetime` executor (the browser clock, not the server's)
// Browser `get_datetime` executor (the browser clock, not the server's)
export { executeGetDatetimeTool } from './get-datetime';
// Browser fallback for the server's get_info tool
+3 -3
View File
@@ -50,10 +50,10 @@ const FAVICON_PATH = '/favicon.ico';
// (and that callers read off `SearchResult`), so `FieldKey.TITLE` is a
// drop-in for the literal `'title'`.
enum FieldKey {
TITLE = 'title',
URL = 'url',
AUTHOR = 'author',
PUBLISHED = 'published',
AUTHOR = 'author'
TITLE = 'title',
URL = 'url'
}
const FIELD_PREFIXES: ReadonlyArray<{ key: FieldKey; prefix: string }> = [
{ key: FieldKey.TITLE, prefix: 'Title:' },
+13
View File
@@ -0,0 +1,13 @@
import { TOOL_UI } from '$lib/constants';
import type { ToolUiEntry } from '$lib/types';
/**
* Resolve the UI metadata (label + icon) for a server or browser tool by its
* name. Falls back to null for unknown tools so callers can render a generic
* chrome instead.
*/
export function getToolUi(toolName: string | undefined): ToolUiEntry | null {
if (!toolName) return null;
return (TOOL_UI as Record<string, ToolUiEntry>)[toolName] ?? null;
}
@@ -13,15 +13,15 @@ import { afterEach, describe, expect, it } from 'vitest';
import { render } from 'vitest-browser-svelte';
const FILE_SEARCH_DEF: OpenAIToolDefinition = {
function: { description: '', name: BuiltInTool.FILE_GLOB_SEARCH, parameters: {} },
function: { description: '', name: BuiltInTool.SERVER_FILE_GLOB_SEARCH, parameters: {} },
type: 'function'
};
const FILE_SEARCH_KEY = `builtin:${BuiltInTool.FILE_GLOB_SEARCH}`;
const FILE_SEARCH_KEY = `server:${BuiltInTool.SERVER_FILE_GLOB_SEARCH}`;
// The store keeps its builtin tool list private; tests inject it through
// The store keeps its server tool list private; tests inject it through
// the reactive field so the derived gates recompute.
function setBuiltinTools(defs: OpenAIToolDefinition[]) {
(toolsStore as unknown as { _builtinTools: OpenAIToolDefinition[] })._builtinTools = defs;
function setServerTools(defs: OpenAIToolDefinition[]) {
(toolsStore as unknown as { _serverTools: OpenAIToolDefinition[] })._serverTools = defs;
}
function renderPicker() {
@@ -34,14 +34,14 @@ function renderPicker() {
}
afterEach(() => {
setBuiltinTools([]);
setServerTools([]);
toolsStore.setToolEnabled(FILE_SEARCH_KEY, true);
localStorage.removeItem(DISABLED_TOOL_KEYS_LOCALSTORAGE_KEY);
});
describe('ChatFormPickerMention file_glob_search gate', () => {
it('explains that file search is unavailable when the server has no tools', async () => {
setBuiltinTools([]);
setServerTools([]);
renderPicker();
await tick();
@@ -51,7 +51,7 @@ describe('ChatFormPickerMention file_glob_search gate', () => {
});
it('explains that file search must be enabled when the user disabled it', async () => {
setBuiltinTools([FILE_SEARCH_DEF]);
setServerTools([FILE_SEARCH_DEF]);
toolsStore.setToolEnabled(FILE_SEARCH_KEY, false);
renderPicker();
await tick();
+67 -54
View File
@@ -16,7 +16,7 @@ import { describe, expect, it } from 'vitest';
function makeSection(
overrides: Partial<AgenticSection> = {},
toolName = BuiltInTool.READ_FILE
toolName = BuiltInTool.SERVER_READ_FILE
): AgenticSection {
return {
content: '',
@@ -115,15 +115,18 @@ describe('formatCwdMessage / parseCwdMessage', () => {
describe('parseToolArgs (shared)', () => {
it('returns null when the section has no toolArgs', () => {
const result = parseToolArgs(BuiltInTool.READ_FILE, makeSection({ toolArgs: undefined }));
const result = parseToolArgs(
BuiltInTool.SERVER_READ_FILE,
makeSection({ toolArgs: undefined })
);
expect(result).toBeNull();
});
it('returns null when the tool name does not match', () => {
const result = parseToolArgs(
BuiltInTool.READ_FILE,
makeSection({ toolArgs: '{"path":"/x"}' }, BuiltInTool.WRITE_FILE)
BuiltInTool.SERVER_READ_FILE,
makeSection({ toolArgs: '{"path":"/x"}' }, BuiltInTool.SERVER_WRITE_FILE)
);
expect(result).toBeNull();
@@ -131,7 +134,7 @@ describe('parseToolArgs (shared)', () => {
it('returns null when args are not valid final JSON (partial: false)', () => {
const result = parseToolArgs(
BuiltInTool.READ_FILE,
BuiltInTool.SERVER_READ_FILE,
makeSection({ toolArgs: '{"path": "/foo.tx' })
);
@@ -140,7 +143,7 @@ describe('parseToolArgs (shared)', () => {
it('returns parsed args when valid final JSON', () => {
const result = parseToolArgs(
BuiltInTool.READ_FILE,
BuiltInTool.SERVER_READ_FILE,
makeSection({ toolArgs: '{"path":"/foo.txt"}' })
);
@@ -149,7 +152,7 @@ describe('parseToolArgs (shared)', () => {
it('accepts partial JSON when partial: true', () => {
const result = parseToolArgs(
BuiltInTool.READ_FILE,
BuiltInTool.SERVER_READ_FILE,
makeSection({ toolArgs: '{"path": "/foo.tx' }),
{ partial: true }
);
@@ -162,7 +165,10 @@ describe('parseWriteFileMeta', () => {
it('returns null for sections with a different tool name', () => {
expect(
parseWriteFileMeta(
makeSection({ toolArgs: '{"path":"/x","content":"y"}', toolName: BuiltInTool.READ_FILE })
makeSection({
toolArgs: '{"path":"/x","content":"y"}',
toolName: BuiltInTool.SERVER_READ_FILE
})
)
).toBeNull();
});
@@ -170,14 +176,14 @@ describe('parseWriteFileMeta', () => {
it('returns null when args have no path-like field', () => {
expect(
parseWriteFileMeta(
makeSection({ toolArgs: '{"content":"x"}', toolName: BuiltInTool.WRITE_FILE })
makeSection({ toolArgs: '{"content":"x"}', toolName: BuiltInTool.SERVER_WRITE_FILE })
)
).toBeNull();
});
it('accepts partial args (renders incrementally as content streams in)', () => {
const meta = parseWriteFileMeta(
makeSection({ toolArgs: '{"path":"/foo.t', toolName: BuiltInTool.WRITE_FILE })
makeSection({ toolArgs: '{"path":"/foo.t', toolName: BuiltInTool.SERVER_WRITE_FILE })
);
expect(meta?.filePath).toBe('/foo.t');
@@ -188,10 +194,10 @@ describe('parseWriteFileMeta', () => {
makeSection(
{
toolArgs: '{"path":"/foo.ts","content":"x"}',
toolName: BuiltInTool.WRITE_FILE,
toolName: BuiltInTool.SERVER_WRITE_FILE,
toolResult: '{"result":"wrote","bytes":42}'
},
BuiltInTool.WRITE_FILE
BuiltInTool.SERVER_WRITE_FILE
)
);
@@ -208,7 +214,7 @@ describe('parseWriteFileMeta', () => {
const meta = parseWriteFileMeta(
makeSection({
toolArgs: '{"path":"/foo","content":"x"}',
toolName: BuiltInTool.WRITE_FILE,
toolName: BuiltInTool.SERVER_WRITE_FILE,
toolResult: '{"error":"permission denied"}'
})
);
@@ -223,10 +229,10 @@ describe('parseEditFileMeta', () => {
{
toolArgs:
'{"path":"/foo.ts","edits":[{"old_text":"a","new_text":"b"},{"old_text":"c","new_text":"d"}]}',
toolName: BuiltInTool.EDIT_FILE,
toolName: BuiltInTool.SERVER_EDIT_FILE,
toolResult: '{"result":"ok","edits_applied":2}'
},
BuiltInTool.EDIT_FILE
BuiltInTool.SERVER_EDIT_FILE
);
const meta = parseEditFileMeta(section);
@@ -242,9 +248,9 @@ describe('parseEditFileMeta', () => {
const section = makeSection(
{
toolArgs: '{"path":"/foo","edits":[{"old_text":""},{"old_text":"a","new_text":""}]}',
toolName: BuiltInTool.EDIT_FILE
toolName: BuiltInTool.SERVER_EDIT_FILE
},
BuiltInTool.EDIT_FILE
BuiltInTool.SERVER_EDIT_FILE
);
const meta = parseEditFileMeta(section);
@@ -257,10 +263,10 @@ describe('parseEditFileMeta', () => {
const section = makeSection(
{
toolArgs: '{"path":"/foo"}',
toolName: BuiltInTool.EDIT_FILE,
toolName: BuiltInTool.SERVER_EDIT_FILE,
toolResult: '{"error":"bad path","result":"ok"}'
},
BuiltInTool.EDIT_FILE
BuiltInTool.SERVER_EDIT_FILE
);
const meta = parseEditFileMeta(section);
@@ -272,7 +278,7 @@ describe('parseEditFileMeta', () => {
describe('parseReadFileMeta', () => {
it('parses file name alone (no range)', () => {
const meta = parseReadFileMeta(
makeSection({ toolArgs: '{"path":"/foo.txt"}' }, BuiltInTool.READ_FILE)
makeSection({ toolArgs: '{"path":"/foo.txt"}' }, BuiltInTool.SERVER_READ_FILE)
);
expect(meta?.fileName).toBe('foo.txt');
@@ -283,7 +289,7 @@ describe('parseReadFileMeta', () => {
const meta = parseReadFileMeta(
makeSection(
{ toolArgs: '{"path":"/foo.ts","start_line":10,"end_line":20}' },
BuiltInTool.READ_FILE
BuiltInTool.SERVER_READ_FILE
)
);
@@ -294,7 +300,7 @@ describe('parseReadFileMeta', () => {
const meta = parseReadFileMeta(
makeSection(
{ toolArgs: '{"path":"/foo.ts","start_line":10,"line_count":5}' },
BuiltInTool.READ_FILE
BuiltInTool.SERVER_READ_FILE
)
);
@@ -302,7 +308,9 @@ describe('parseReadFileMeta', () => {
});
it('returns null when args cannot be parsed', () => {
expect(parseReadFileMeta(makeSection({ toolArgs: '{bad' }, BuiltInTool.READ_FILE))).toBeNull();
expect(
parseReadFileMeta(makeSection({ toolArgs: '{bad' }, BuiltInTool.SERVER_READ_FILE))
).toBeNull();
});
});
@@ -310,12 +318,12 @@ describe('parseGrepSearchMeta', () => {
it('returns null when path or pattern is missing', () => {
expect(
parseGrepSearchMeta(
makeSection({ toolArgs: '{"pattern":"foo"}', toolName: BuiltInTool.GREP_SEARCH })
makeSection({ toolArgs: '{"pattern":"foo"}', toolName: BuiltInTool.SERVER_GREP_SEARCH })
)
).toBeNull();
expect(
parseGrepSearchMeta(
makeSection({ toolArgs: '{"path":"/x"}', toolName: BuiltInTool.GREP_SEARCH })
makeSection({ toolArgs: '{"path":"/x"}', toolName: BuiltInTool.SERVER_GREP_SEARCH })
)
).toBeNull();
});
@@ -325,10 +333,10 @@ describe('parseGrepSearchMeta', () => {
makeSection(
{
toolArgs: '{"path":"/x","pattern":"foo"}',
toolName: BuiltInTool.GREP_SEARCH,
toolName: BuiltInTool.SERVER_GREP_SEARCH,
toolResult: JSON.stringify({ plain_text_response: 'a.ts:hello\nb.ts:world' })
},
BuiltInTool.GREP_SEARCH
BuiltInTool.SERVER_GREP_SEARCH
)
);
@@ -341,10 +349,10 @@ describe('parseGrepSearchMeta', () => {
makeSection(
{
toolArgs: '{"path":"/x","pattern":"foo"}',
toolName: BuiltInTool.GREP_SEARCH,
toolName: BuiltInTool.SERVER_GREP_SEARCH,
toolResult: 'a.ts:hello\nb.ts:world'
},
BuiltInTool.GREP_SEARCH
BuiltInTool.SERVER_GREP_SEARCH
)
);
@@ -356,10 +364,10 @@ describe('parseGrepSearchMeta', () => {
makeSection(
{
toolArgs: '{"path":"/x","pattern":"foo","return_line_numbers":true}',
toolName: BuiltInTool.GREP_SEARCH,
toolName: BuiltInTool.SERVER_GREP_SEARCH,
toolResult: 'a.ts:12:hello'
},
BuiltInTool.GREP_SEARCH
BuiltInTool.SERVER_GREP_SEARCH
)
);
@@ -374,10 +382,10 @@ describe('parseFileGlobSearchMeta', () => {
makeSection(
{
toolArgs: '{"path":"/x"}',
toolName: BuiltInTool.FILE_GLOB_SEARCH,
toolName: BuiltInTool.SERVER_FILE_GLOB_SEARCH,
toolResult: 'a.ts\nb.ts'
},
BuiltInTool.FILE_GLOB_SEARCH
BuiltInTool.SERVER_FILE_GLOB_SEARCH
)
);
@@ -389,10 +397,10 @@ describe('parseFileGlobSearchMeta', () => {
makeSection(
{
toolArgs: '{"path":"/x"}',
toolName: BuiltInTool.FILE_GLOB_SEARCH,
toolName: BuiltInTool.SERVER_FILE_GLOB_SEARCH,
toolResult: JSON.stringify({ plain_text_response: 'a.ts\nb.ts' })
},
BuiltInTool.FILE_GLOB_SEARCH
BuiltInTool.SERVER_FILE_GLOB_SEARCH
)
);
@@ -404,10 +412,10 @@ describe('parseFileGlobSearchMeta', () => {
makeSection(
{
toolArgs: '{"path":"/x"}',
toolName: BuiltInTool.FILE_GLOB_SEARCH,
toolName: BuiltInTool.SERVER_FILE_GLOB_SEARCH,
toolResult: JSON.stringify({ error: 'permission denied' })
},
BuiltInTool.FILE_GLOB_SEARCH
BuiltInTool.SERVER_FILE_GLOB_SEARCH
)
);
@@ -418,15 +426,20 @@ describe('parseFileGlobSearchMeta', () => {
describe('parseRunJavascriptMeta', () => {
it('returns null when code is missing', () => {
expect(
parseRunJavascriptMeta(makeSection({ toolArgs: '{}', toolName: BuiltInTool.RUN_JAVASCRIPT }))
parseRunJavascriptMeta(
makeSection({ toolArgs: '{}', toolName: BuiltInTool.BROWSER_RUN_JAVASCRIPT })
)
).toBeNull();
});
it('reads code and timeout', () => {
const meta = parseRunJavascriptMeta(
makeSection(
{ toolArgs: '{"code":"Math.PI","timeout_ms":5000}', toolName: BuiltInTool.RUN_JAVASCRIPT },
BuiltInTool.RUN_JAVASCRIPT
{
toolArgs: '{"code":"Math.PI","timeout_ms":5000}',
toolName: BuiltInTool.BROWSER_RUN_JAVASCRIPT
},
BuiltInTool.BROWSER_RUN_JAVASCRIPT
)
);
@@ -439,10 +452,10 @@ describe('parseRunJavascriptMeta', () => {
makeSection(
{
toolArgs: '{"code":"throw new Error()"}',
toolName: BuiltInTool.RUN_JAVASCRIPT,
toolName: BuiltInTool.BROWSER_RUN_JAVASCRIPT,
toolResult: JSON.stringify({ error: 'undefined is not a function' })
},
BuiltInTool.RUN_JAVASCRIPT
BuiltInTool.BROWSER_RUN_JAVASCRIPT
)
);
@@ -457,10 +470,10 @@ describe('parseRunJavascriptMeta', () => {
makeSection(
{
toolArgs: '{"code":"[1,2,3]"}',
toolName: BuiltInTool.RUN_JAVASCRIPT,
toolName: BuiltInTool.BROWSER_RUN_JAVASCRIPT,
toolResult: '[1,2,3]'
},
BuiltInTool.RUN_JAVASCRIPT
BuiltInTool.BROWSER_RUN_JAVASCRIPT
)
);
@@ -472,10 +485,10 @@ describe('parseRunJavascriptMeta', () => {
makeSection(
{
toolArgs: '{"code":"foo"}',
toolName: BuiltInTool.RUN_JAVASCRIPT,
toolName: BuiltInTool.BROWSER_RUN_JAVASCRIPT,
toolResult: 'Error: undefined is not a function\n at <anonymous>:1:1'
},
BuiltInTool.RUN_JAVASCRIPT
BuiltInTool.BROWSER_RUN_JAVASCRIPT
)
);
@@ -487,8 +500,8 @@ describe('parseExecShellCommandMeta', () => {
it('reads command from the args', () => {
const meta = parseExecShellCommandMeta(
makeSection(
{ toolArgs: '{"command":"ls -la"}', toolName: BuiltInTool.EXEC_SHELL_COMMAND },
BuiltInTool.EXEC_SHELL_COMMAND
{ toolArgs: '{"command":"ls -la"}', toolName: BuiltInTool.SERVER_EXEC_SHELL_COMMAND },
BuiltInTool.SERVER_EXEC_SHELL_COMMAND
)
);
@@ -499,16 +512,16 @@ describe('parseExecShellCommandMeta', () => {
expect(
parseExecShellCommandMeta(
makeSection(
{ toolArgs: '{"cmd":"ls"}', toolName: BuiltInTool.EXEC_SHELL_COMMAND },
BuiltInTool.EXEC_SHELL_COMMAND
{ toolArgs: '{"cmd":"ls"}', toolName: BuiltInTool.SERVER_EXEC_SHELL_COMMAND },
BuiltInTool.SERVER_EXEC_SHELL_COMMAND
)
)?.command
).toBe('ls');
expect(
parseExecShellCommandMeta(
makeSection(
{ toolArgs: '{"shell_command":"ls"}', toolName: BuiltInTool.EXEC_SHELL_COMMAND },
BuiltInTool.EXEC_SHELL_COMMAND
{ toolArgs: '{"shell_command":"ls"}', toolName: BuiltInTool.SERVER_EXEC_SHELL_COMMAND },
BuiltInTool.SERVER_EXEC_SHELL_COMMAND
)
)?.command
).toBe('ls');
@@ -518,8 +531,8 @@ describe('parseExecShellCommandMeta', () => {
expect(
parseExecShellCommandMeta(
makeSection(
{ toolArgs: '{"cwd":"/x"}', toolName: BuiltInTool.EXEC_SHELL_COMMAND },
BuiltInTool.EXEC_SHELL_COMMAND
{ toolArgs: '{"cwd":"/x"}', toolName: BuiltInTool.SERVER_EXEC_SHELL_COMMAND },
BuiltInTool.SERVER_EXEC_SHELL_COMMAND
)
)
).toBeNull();
+11
View File
@@ -0,0 +1,11 @@
# mtmd needs these even when common is not built
add_subdirectory(hash)
add_subdirectory(miniaudio)
add_subdirectory(nlohmann)
add_subdirectory(sheredom)
add_subdirectory(stb)
# only used by common
if (LLAMA_BUILD_COMMON)
add_subdirectory(cpp-httplib)
endif()
+2
View File
@@ -9,6 +9,8 @@ set(CMAKE_POSITION_INDEPENDENT_CODE ON)
add_library(${TARGET} STATIC httplib.cpp httplib.h)
add_library(vendor::cpp-httplib ALIAS ${TARGET})
# disable warnings in 3rd party code
if (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
target_compile_options(${TARGET} PRIVATE /w)
+7 -2
View File
@@ -16,6 +16,8 @@ add_library(${TARGET} STATIC
${VENDOR_SRCS}
)
add_library(vendor::hash ALIAS ${TARGET})
target_compile_features(${TARGET} PRIVATE cxx_std_17)
# disable warnings in 3rd party code, but keep them for hash.cpp
@@ -29,5 +31,8 @@ set_source_files_properties(${VENDOR_SRCS} PROPERTIES COMPILE_OPTIONS ${NO_WARN_
# sha1 lives in a namespace to avoid a clash with boringssl, see scripts/sync_vendor.py
set_source_files_properties(sha1/sha1.c PROPERTIES LANGUAGE CXX)
# sha256.c includes "rotate-bits/rotate-bits.h", so consumers get this dir too
target_include_directories(${TARGET} PUBLIC .)
# expose the vendor/ root so consumers can include via "hash/hash.h"
target_include_directories(${TARGET} PUBLIC ..)
# internal includes of the vendored sources, e.g. sha256.c -> "rotate-bits/rotate-bits.h"
target_include_directories(${TARGET} PRIVATE .)
+6
View File
@@ -0,0 +1,6 @@
# header-only: interface target exposing the vendor/ root so consumers
# can include via <miniaudio/miniaudio.h>
add_library(miniaudio INTERFACE)
add_library(vendor::miniaudio ALIAS miniaudio)
target_include_directories(miniaudio INTERFACE ..)
+6
View File
@@ -0,0 +1,6 @@
# header-only: interface target exposing the vendor/ root so consumers
# can include via <nlohmann/json.hpp>
add_library(nlohmann INTERFACE)
add_library(vendor::nlohmann ALIAS nlohmann)
target_include_directories(nlohmann INTERFACE ..)

Some files were not shown because too many files have changed in this diff Show More