diff --git a/.devops/intel.Dockerfile b/.devops/intel.Dockerfile
index c8839fe02..9ce80a71e 100644
--- a/.devops/intel.Dockerfile
+++ b/.devops/intel.Dockerfile
@@ -1,4 +1,4 @@
-ARG ONEAPI_VERSION=2025.0.0-0-devel-ubuntu22.04
+ARG ONEAPI_VERSION=2025.1.1-0-devel-ubuntu24.04
## Build Image
@@ -49,19 +49,23 @@ COPY --from=build /app/full /app
WORKDIR /app
-RUN apt-get update \
- && apt-get install -y \
- git \
- python3 \
- python3-pip \
- && pip install --upgrade pip setuptools wheel \
- && pip install -r requirements.txt \
- && apt autoremove -y \
- && apt clean -y \
- && rm -rf /tmp/* /var/tmp/* \
- && find /var/cache/apt/archives /var/lib/apt/lists -not -name lock -type f -delete \
- && find /var/cache -type f -delete
+RUN apt-get update && \
+ apt-get install -y \
+ git \
+ python3 \
+ python3-pip \
+ python3-venv && \
+ python3 -m venv /opt/venv && \
+ . /opt/venv/bin/activate && \
+ pip install --upgrade pip setuptools wheel && \
+ pip install -r requirements.txt && \
+ apt autoremove -y && \
+ apt clean -y && \
+ rm -rf /tmp/* /var/tmp/* && \
+ find /var/cache/apt/archives /var/lib/apt/lists -not -name lock -type f -delete && \
+ find /var/cache -type f -delete
+ENV PATH="/opt/venv/bin:$PATH"
ENTRYPOINT ["/app/tools.sh"]
diff --git a/.devops/musa.Dockerfile b/.devops/musa.Dockerfile
index e0f1ad972..87ce2393f 100644
--- a/.devops/musa.Dockerfile
+++ b/.devops/musa.Dockerfile
@@ -1,10 +1,10 @@
ARG UBUNTU_VERSION=22.04
# This needs to generally match the container host's environment.
-ARG MUSA_VERSION=rc3.1.1
+ARG MUSA_VERSION=rc4.0.1
# Target the MUSA build image
-ARG BASE_MUSA_DEV_CONTAINER=mthreads/musa:${MUSA_VERSION}-devel-ubuntu${UBUNTU_VERSION}
+ARG BASE_MUSA_DEV_CONTAINER=mthreads/musa:${MUSA_VERSION}-mudnn-devel-ubuntu${UBUNTU_VERSION}
-ARG BASE_MUSA_RUN_CONTAINER=mthreads/musa:${MUSA_VERSION}-runtime-ubuntu${UBUNTU_VERSION}
+ARG BASE_MUSA_RUN_CONTAINER=mthreads/musa:${MUSA_VERSION}-mudnn-runtime-ubuntu${UBUNTU_VERSION}
FROM ${BASE_MUSA_DEV_CONTAINER} AS build
@@ -21,21 +21,14 @@ RUN apt-get update && \
libcurl4-openssl-dev \
libgomp1
-COPY requirements.txt requirements.txt
-COPY requirements requirements
-
-RUN pip install --upgrade pip setuptools wheel \
- && pip install -r requirements.txt
-
WORKDIR /app
COPY . .
-# Use the default MUSA archs if not specified
RUN if [ "${MUSA_DOCKER_ARCH}" != "default" ]; then \
export CMAKE_ARGS="-DMUSA_ARCHITECTURES=${MUSA_DOCKER_ARCH}"; \
fi && \
- cmake -B build -DGGML_NATIVE=OFF -DGGML_MUSA=ON -DLLAMA_BUILD_TESTS=OFF -DGGML_BACKEND_DL=ON -DGGML_CPU_ALL_VARIANTS=ON ${CMAKE_ARGS} -DCMAKE_EXE_LINKER_FLAGS=-Wl,--allow-shlib-undefined . && \
+ cmake -B build -DGGML_NATIVE=OFF -DGGML_MUSA=ON -DGGML_BACKEND_DL=ON -DGGML_CPU_ALL_VARIANTS=ON -DLLAMA_BUILD_TESTS=OFF ${CMAKE_ARGS} -DCMAKE_EXE_LINKER_FLAGS=-Wl,--allow-shlib-undefined . && \
cmake --build build --config Release -j$(nproc)
RUN mkdir -p /app/lib && \
diff --git a/.editorconfig b/.editorconfig
index 1eadda334..c90b171f5 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -48,3 +48,7 @@ end_of_line = unset
charset = unset
trim_trailing_whitespace = unset
insert_final_newline = unset
+
+[vendor/miniaudio/miniaudio.h]
+trim_trailing_whitespace = unset
+insert_final_newline = unset
diff --git a/.github/actions/windows-setup-curl/action.yml b/.github/actions/windows-setup-curl/action.yml
index 5d76da3d7..446f799fa 100644
--- a/.github/actions/windows-setup-curl/action.yml
+++ b/.github/actions/windows-setup-curl/action.yml
@@ -5,6 +5,10 @@ inputs:
description: 'CURL version'
required: false
default: '8.6.0_6'
+ architecture:
+ description: 'Architecture of the libcurl to download'
+ required: false
+ default: 'win64'
outputs:
curl_path:
description: "Path to the downloaded libcurl"
@@ -18,8 +22,9 @@ runs:
shell: powershell
env:
CURL_VERSION: ${{ inputs.curl_version }}
+ ARCHITECTURE: ${{ inputs.architecture }}
run: |
- curl.exe -o $env:RUNNER_TEMP/curl.zip -L "https://curl.se/windows/dl-${env:CURL_VERSION}/curl-${env:CURL_VERSION}-win64-mingw.zip"
+ curl.exe -o $env:RUNNER_TEMP/curl.zip -L "https://curl.se/windows/dl-${env:CURL_VERSION}/curl-${env:CURL_VERSION}-${env:ARCHITECTURE}-mingw.zip"
mkdir $env:RUNNER_TEMP/libcurl
tar.exe -xvf $env:RUNNER_TEMP/curl.zip --strip-components=1 -C $env:RUNNER_TEMP/libcurl
echo "curl_path=$env:RUNNER_TEMP/libcurl" >> $env:GITHUB_OUTPUT
diff --git a/.github/labeler.yml b/.github/labeler.yml
index 278032ef2..3c2f67707 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -86,3 +86,10 @@ nix:
embedding:
- changed-files:
- any-glob-to-any-file: examples/embedding/
+
+Ascend NPU:
+ - changed-files:
+ - any-glob-to-any-file:
+ - ggml/include/ggml-cann.h
+ - ggml/src/ggml-cann/**
+ - docs/backend/CANN.md
diff --git a/.github/workflows/build-cmake-pkg.yml b/.github/workflows/build-cmake-pkg.yml
new file mode 100644
index 000000000..fee2ab96b
--- /dev/null
+++ b/.github/workflows/build-cmake-pkg.yml
@@ -0,0 +1,51 @@
+name: Build relocatable cmake package
+on:
+ workflow_dispatch:
+ workflow_call:
+
+jobs:
+ linux:
+ runs-on: ubuntu-24.04
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Install dependencies
+ run: |
+ sudo apt update
+ sudo apt install -y build-essential tcl
+
+ - name: Build
+ run: |
+ PREFIX="$(pwd)"/inst
+ cmake -S . -B build -DCMAKE_PREFIX_PATH="$PREFIX" \
+ -DLLAMA_CURL=OFF -DLLAMA_BUILD_TESTS=OFF -DLLAMA_BUILD_TOOLS=OFF \
+ -DLLAMA_BUILD_EXAMPLES=OFF -DCMAKE_BUILD_TYPE=Release
+ cmake --build build --config Release
+ cmake --install build --prefix "$PREFIX" --config Release
+
+ export LLAMA_CONFIG="$PREFIX"/lib/cmake/llama/llama-config.cmake
+ tclsh <<'EOF'
+ set build(commit) [string trim [exec git rev-parse --short HEAD]]
+ set build(number) [string trim [exec git rev-list --count HEAD]]
+ set build(version) "0.0.$build(number)"
+
+ set llamaconfig [read [open "$env(LLAMA_CONFIG)" r]]
+ set checks [list "set\\(LLAMA_VERSION \\s+$build(version)\\)" \
+ "set\\(LLAMA_BUILD_COMMIT\\s+$build(commit)\\)" \
+ "set\\(LLAMA_BUILD_NUMBER\\s+$build(number)\\)"]
+
+ puts -nonewline "Checking llama-config.cmake version... "
+ foreach check $checks {
+ if {![regexp -expanded -- $check $llamaconfig]} {
+ puts "\"$check\" failed!"
+ exit 1
+ }
+ }
+ puts "success."
+ EOF
+
+ cd examples/simple-cmake-pkg
+ cmake -S . -B build -DCMAKE_PREFIX_PATH="$PREFIX"/lib/cmake
+ cmake --build build
diff --git a/.github/workflows/build-linux-cross.yml b/.github/workflows/build-linux-cross.yml
index 1c38d7e11..7cfc82ba4 100644
--- a/.github/workflows/build-linux-cross.yml
+++ b/.github/workflows/build-linux-cross.yml
@@ -26,12 +26,12 @@ jobs:
sudo apt-get install -y --no-install-recommends \
build-essential \
gcc-14-riscv64-linux-gnu \
- g++-14-riscv64-linux-gnu \
- libcurl4-openssl-dev:riscv64
+ g++-14-riscv64-linux-gnu
- name: Build
run: |
- cmake -B build -DCMAKE_BUILD_TYPE=Release \
+ cmake -B build -DLLAMA_CURL=OFF \
+ -DCMAKE_BUILD_TYPE=Release \
-DGGML_OPENMP=OFF \
-DLLAMA_BUILD_EXAMPLES=ON \
-DLLAMA_BUILD_TOOLS=ON \
@@ -72,12 +72,12 @@ jobs:
glslc \
gcc-14-riscv64-linux-gnu \
g++-14-riscv64-linux-gnu \
- libvulkan-dev:riscv64 \
- libcurl4-openssl-dev:riscv64
+ libvulkan-dev:riscv64
- name: Build
run: |
- cmake -B build -DCMAKE_BUILD_TYPE=Release \
+ cmake -B build -DLLAMA_CURL=OFF \
+ -DCMAKE_BUILD_TYPE=Release \
-DGGML_VULKAN=ON \
-DGGML_OPENMP=OFF \
-DLLAMA_BUILD_EXAMPLES=ON \
@@ -118,12 +118,12 @@ jobs:
build-essential \
glslc \
crossbuild-essential-arm64 \
- libvulkan-dev:arm64 \
- libcurl4-openssl-dev:arm64
+ libvulkan-dev:arm64
- name: Build
run: |
- cmake -B build -DCMAKE_BUILD_TYPE=Release \
+ cmake -B build -DLLAMA_CURL=OFF \
+ -DCMAKE_BUILD_TYPE=Release \
-DGGML_VULKAN=ON \
-DGGML_OPENMP=OFF \
-DLLAMA_BUILD_EXAMPLES=ON \
@@ -140,3 +140,207 @@ jobs:
-DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=BOTH
cmake --build build --config Release -j $(nproc)
+
+ ubuntu-24-ppc64el-cpu-cross:
+ runs-on: ubuntu-24.04
+
+ steps:
+ - uses: actions/checkout@v4
+ - name: Setup PowerPC64le
+ run: |
+ sudo dpkg --add-architecture ppc64el
+
+ # Add arch-specific repositories for non-amd64 architectures
+ cat << EOF | sudo tee /etc/apt/sources.list.d/ppc64el-ports.list
+ deb [arch=ppc64el] http://ports.ubuntu.com/ubuntu-ports/ noble main universe
+ deb [arch=ppc64el] http://ports.ubuntu.com/ubuntu-ports/ noble-updates main universe
+ deb [arch=ppc64el] http://ports.ubuntu.com/ubuntu-ports/ noble-security main universe
+ deb [arch=ppc64el] http://ports.ubuntu.com/ubuntu-ports/ noble-backports main universe
+ EOF
+
+ sudo apt-get update || true ;# Prevent failure due to missing URLs.
+
+ sudo apt-get install -y --no-install-recommends \
+ build-essential \
+ gcc-14-powerpc64le-linux-gnu \
+ g++-14-powerpc64le-linux-gnu
+
+ - name: Build
+ run: |
+ cmake -B build -DLLAMA_CURL=OFF \
+ -DCMAKE_BUILD_TYPE=Release \
+ -DGGML_OPENMP=OFF \
+ -DLLAMA_BUILD_EXAMPLES=ON \
+ -DLLAMA_BUILD_TOOLS=ON \
+ -DLLAMA_BUILD_TESTS=OFF \
+ -DCMAKE_SYSTEM_NAME=Linux \
+ -DCMAKE_SYSTEM_PROCESSOR=ppc64 \
+ -DCMAKE_C_COMPILER=powerpc64le-linux-gnu-gcc-14 \
+ -DCMAKE_CXX_COMPILER=powerpc64le-linux-gnu-g++-14 \
+ -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
+ -DCMAKE_FIND_ROOT_PATH=/usr/lib/powerpc64le-linux-gnu \
+ -DCMAKE_FIND_ROOT_PATH_MODE_PROGRAM=NEVER \
+ -DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=ONLY \
+ -DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=BOTH
+
+ cmake --build build --config Release -j $(nproc)
+
+ ubuntu-24-ppc64el-vulkan-cross:
+ runs-on: ubuntu-24.04
+
+ steps:
+ - uses: actions/checkout@v4
+ - name: Setup PowerPC64le
+ run: |
+ sudo dpkg --add-architecture ppc64el
+
+ # Add arch-specific repositories for non-amd64 architectures
+ cat << EOF | sudo tee /etc/apt/sources.list.d/ppc64el-ports.list
+ deb [arch=ppc64el] http://ports.ubuntu.com/ubuntu-ports/ noble main universe
+ deb [arch=ppc64el] http://ports.ubuntu.com/ubuntu-ports/ noble-updates main universe
+ deb [arch=ppc64el] http://ports.ubuntu.com/ubuntu-ports/ noble-security main universe
+ deb [arch=ppc64el] http://ports.ubuntu.com/ubuntu-ports/ noble-backports main universe
+ EOF
+
+ sudo apt-get update || true ;# Prevent failure due to missing URLs.
+
+ sudo apt-get install -y --no-install-recommends \
+ build-essential \
+ glslc \
+ gcc-14-powerpc64le-linux-gnu \
+ g++-14-powerpc64le-linux-gnu \
+ libvulkan-dev:ppc64el
+
+ - name: Build
+ run: |
+ cmake -B build -DLLAMA_CURL=OFF \
+ -DCMAKE_BUILD_TYPE=Release \
+ -DGGML_VULKAN=ON \
+ -DGGML_OPENMP=OFF \
+ -DLLAMA_BUILD_EXAMPLES=ON \
+ -DLLAMA_BUILD_TOOLS=ON \
+ -DLLAMA_BUILD_TESTS=OFF \
+ -DCMAKE_SYSTEM_NAME=Linux \
+ -DCMAKE_SYSTEM_PROCESSOR=ppc64 \
+ -DCMAKE_C_COMPILER=powerpc64le-linux-gnu-gcc-14 \
+ -DCMAKE_CXX_COMPILER=powerpc64le-linux-gnu-g++-14 \
+ -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
+ -DCMAKE_FIND_ROOT_PATH=/usr/lib/powerpc64le-linux-gnu \
+ -DCMAKE_FIND_ROOT_PATH_MODE_PROGRAM=NEVER \
+ -DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=ONLY \
+ -DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=BOTH
+
+ cmake --build build --config Release -j $(nproc)
+
+ debian-13-loongarch64-cpu-cross:
+ runs-on: ubuntu-24.04
+ container: debian@sha256:653dfb9f86c3782e8369d5f7d29bb8faba1f4bff9025db46e807fa4c22903671
+
+ steps:
+ - uses: actions/checkout@v4
+ - name: Setup LoongArch
+ run: |
+ rm -f /etc/apt/sources.list.d/*
+ cat << EOF | tee /etc/apt/sources.list.d/debian-ports.list
+ deb http://snapshot.debian.org/archive/debian/20250515T202920Z/ trixie main
+ EOF
+ ( echo 'quiet "true";'; \
+ echo 'APT::Get::Assume-Yes "true";'; \
+ echo 'APT::Install-Recommends "false";'; \
+ echo 'Acquire::Check-Valid-Until "false";'; \
+ echo 'Acquire::Retries "5";'; \
+ ) > /etc/apt/apt.conf.d/99snapshot-repos
+
+ apt-get update
+ apt-get install -y ca-certificates debian-ports-archive-keyring cmake git zip
+ dpkg --add-architecture loong64
+
+ # Add arch-specific repositories for non-amd64 architectures
+ cat << EOF | tee /etc/apt/sources.list.d/loong64-ports.list
+ deb [arch=loong64] http://snapshot.debian.org/archive/debian-ports/20250515T194251Z/ sid main
+ EOF
+
+ apt-get update || true ;# Prevent failure due to missing URLs.
+
+ apt-get install -y --no-install-recommends \
+ build-essential \
+ gcc-14-loongarch64-linux-gnu \
+ g++-14-loongarch64-linux-gnu
+
+ - name: Build
+ run: |
+ cmake -B build -DLLAMA_CURL=OFF \
+ -DCMAKE_BUILD_TYPE=Release \
+ -DGGML_OPENMP=OFF \
+ -DLLAMA_BUILD_EXAMPLES=ON \
+ -DLLAMA_BUILD_TOOLS=ON \
+ -DLLAMA_BUILD_TESTS=OFF \
+ -DCMAKE_SYSTEM_NAME=Linux \
+ -DCMAKE_SYSTEM_PROCESSOR=loongarch64 \
+ -DCMAKE_C_COMPILER=loongarch64-linux-gnu-gcc-14 \
+ -DCMAKE_CXX_COMPILER=loongarch64-linux-gnu-g++-14 \
+ -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
+ -DCMAKE_FIND_ROOT_PATH=/usr/lib/loongarch64-linux-gnu \
+ -DCMAKE_FIND_ROOT_PATH_MODE_PROGRAM=NEVER \
+ -DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=ONLY \
+ -DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=BOTH
+
+ cmake --build build --config Release -j $(nproc)
+
+ debian-13-loongarch64-vulkan-cross:
+ runs-on: ubuntu-24.04
+ container: debian@sha256:653dfb9f86c3782e8369d5f7d29bb8faba1f4bff9025db46e807fa4c22903671
+
+ steps:
+ - uses: actions/checkout@v4
+ - name: Setup LoongArch
+ run: |
+ rm -f /etc/apt/sources.list.d/*
+ cat << EOF | tee /etc/apt/sources.list.d/debian-ports.list
+ deb http://snapshot.debian.org/archive/debian/20250515T202920Z/ trixie main
+ EOF
+ ( echo 'quiet "true";'; \
+ echo 'APT::Get::Assume-Yes "true";'; \
+ echo 'APT::Install-Recommends "false";'; \
+ echo 'Acquire::Check-Valid-Until "false";'; \
+ echo 'Acquire::Retries "5";'; \
+ ) > /etc/apt/apt.conf.d/99snapshot-repos
+
+ apt-get update
+ apt-get install -y ca-certificates debian-ports-archive-keyring cmake git zip
+ dpkg --add-architecture loong64
+
+ # Add arch-specific repositories for non-amd64 architectures
+ cat << EOF | tee /etc/apt/sources.list.d/loong64-ports.list
+ deb [arch=loong64] http://snapshot.debian.org/archive/debian-ports/20250515T194251Z/ sid main
+ EOF
+
+ apt-get update || true ;# Prevent failure due to missing URLs.
+
+ apt-get install -y --no-install-recommends \
+ build-essential \
+ glslc \
+ gcc-14-loongarch64-linux-gnu \
+ g++-14-loongarch64-linux-gnu \
+ libvulkan-dev:loong64
+
+ - name: Build
+ run: |
+ cmake -B build -DLLAMA_CURL=OFF \
+ -DCMAKE_BUILD_TYPE=Release \
+ -DGGML_VULKAN=ON \
+ -DGGML_OPENMP=OFF \
+ -DLLAMA_BUILD_EXAMPLES=ON \
+ -DLLAMA_BUILD_TOOLS=ON \
+ -DLLAMA_BUILD_TESTS=OFF \
+ -DCMAKE_SYSTEM_NAME=Linux \
+ -DCMAKE_SYSTEM_PROCESSOR=loongarch64 \
+ -DCMAKE_C_COMPILER=loongarch64-linux-gnu-gcc-14 \
+ -DCMAKE_CXX_COMPILER=loongarch64-linux-gnu-g++-14 \
+ -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
+ -DCMAKE_FIND_ROOT_PATH=/usr/lib/loongarch64-linux-gnu \
+ -DCMAKE_FIND_ROOT_PATH_MODE_PROGRAM=NEVER \
+ -DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=ONLY \
+ -DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=BOTH
+
+ cmake --build build --config Release -j $(nproc)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index ae066697d..4feccf21e 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -5,10 +5,43 @@ on:
push:
branches:
- master
- paths: ['.github/workflows/build.yml', '.github/workflows/build-linux-cross.yml', '**/CMakeLists.txt', '**/.cmake', '**/*.h', '**/*.hpp', '**/*.c', '**/*.cpp', '**/*.cu', '**/*.cuh', '**/*.swift', '**/*.m', '**/*.metal', '**/*.comp']
+ paths: [
+ '.github/workflows/build.yml',
+ '.github/workflows/build-linux-cross.yml',
+ '.github/workflows/build-cmake-pkg.yml',
+ '**/CMakeLists.txt',
+ '**/.cmake',
+ '**/*.h',
+ '**/*.hpp',
+ '**/*.c',
+ '**/*.cpp',
+ '**/*.cu',
+ '**/*.cuh',
+ '**/*.swift',
+ '**/*.m',
+ '**/*.metal',
+ '**/*.comp'
+ ]
+
pull_request:
types: [opened, synchronize, reopened]
- paths: ['.github/workflows/build.yml', '.github/workflows/build-linux-cross.yml', '**/CMakeLists.txt', '**/.cmake', '**/*.h', '**/*.hpp', '**/*.c', '**/*.cpp', '**/*.cu', '**/*.cuh', '**/*.swift', '**/*.m', '**/*.metal', '**/*.comp']
+ paths: [
+ '.github/workflows/build.yml',
+ '.github/workflows/build-linux-cross.yml',
+ '.github/workflows/build-cmake-pkg.yml',
+ '**/CMakeLists.txt',
+ '**/.cmake',
+ '**/*.h',
+ '**/*.hpp',
+ '**/*.c',
+ '**/*.cpp',
+ '**/*.cu',
+ '**/*.cuh',
+ '**/*.swift',
+ '**/*.m',
+ '**/*.metal',
+ '**/*.comp'
+ ]
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref && github.ref || github.run_id }}
@@ -306,8 +339,9 @@ jobs:
id: cmake_test
run: |
cd build
+ export GGML_VK_VISIBLE_DEVICES=0
# This is using llvmpipe and runs slower than other backends
- ctest -L main --verbose --timeout 2700
+ ctest -L main --verbose --timeout 3600
ubuntu-22-cmake-hip:
runs-on: ubuntu-22.04
@@ -351,7 +385,7 @@ jobs:
ubuntu-22-cmake-musa:
runs-on: ubuntu-22.04
- container: mthreads/musa:rc3.1.1-devel-ubuntu22.04
+ container: mthreads/musa:rc4.0.1-mudnn-devel-ubuntu22.04
steps:
- name: Clone
@@ -477,6 +511,9 @@ jobs:
build-linux-cross:
uses: ./.github/workflows/build-linux-cross.yml
+ build-cmake-pkg:
+ uses: ./.github/workflows/build-cmake-pkg.yml
+
macOS-latest-cmake-ios:
runs-on: macos-latest
@@ -682,17 +719,17 @@ jobs:
env:
OPENBLAS_VERSION: 0.3.23
SDE_VERSION: 9.33.0-2024-01-07
- VULKAN_VERSION: 1.4.309.0
+ VULKAN_VERSION: 1.4.313.2
strategy:
matrix:
include:
- - build: 'cpu-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'
+ - build: 'cpu-x64 (static)'
+ defines: '-G "Ninja Multi-Config" -D CMAKE_TOOLCHAIN_FILE=cmake/x64-windows-llvm.cmake -DGGML_NATIVE=OFF -DLLAMA_BUILD_SERVER=ON -DGGML_RPC=ON -DBUILD_SHARED_LIBS=OFF'
- build: 'openblas-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: 'vulkan-x64'
- defines: '-DGGML_NATIVE=OFF -DLLAMA_BUILD_SERVER=ON -DGGML_RPC=ON -DGGML_BACKEND_DL=ON -DGGML_CPU_ALL_VARIANTS=ON -DGGML_VULKAN=ON'
+ defines: '-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: 'llvm-arm64'
defines: '-G "Ninja Multi-Config" -D CMAKE_TOOLCHAIN_FILE=cmake/arm64-windows-llvm.cmake -DGGML_NATIVE=OFF -DLLAMA_BUILD_SERVER=ON'
- build: 'llvm-arm64-opencl-adreno'
@@ -735,7 +772,7 @@ jobs:
id: get_vulkan
if: ${{ matrix.build == 'kompute-x64' || matrix.build == 'vulkan-x64' }}
run: |
- curl.exe -o $env:RUNNER_TEMP/VulkanSDK-Installer.exe -L "https://sdk.lunarg.com/sdk/download/${env:VULKAN_VERSION}/windows/VulkanSDK-${env:VULKAN_VERSION}-Installer.exe"
+ 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"
@@ -777,6 +814,7 @@ jobs:
cmake -S . -B build ${{ matrix.defines }} `
-DCURL_LIBRARY="$env:CURL_PATH/lib/libcurl.dll.a" -DCURL_INCLUDE_DIR="$env:CURL_PATH/include"
cmake --build build --config Release -j ${env:NUMBER_OF_PROCESSORS}
+ cp $env:CURL_PATH/bin/libcurl-*.dll build/bin/Release
- name: Add libopenblas.dll
id: add_libopenblas_dll
@@ -839,12 +877,12 @@ jobs:
-DGGML_CUDA=ON
cmake --build build
- windows-2019-cmake-cuda:
- runs-on: windows-2019
+ windows-2022-cmake-cuda:
+ runs-on: windows-2022
strategy:
matrix:
- cuda: ['12.4', '11.7']
+ cuda: ['12.4']
steps:
- name: Clone
@@ -878,7 +916,7 @@ jobs:
env:
CURL_PATH: ${{ steps.get_libcurl.outputs.curl_path }}
run: |
- call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvars64.bat"
+ call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" x64
cmake -S . -B build -G "Ninja Multi-Config" ^
-DLLAMA_BUILD_SERVER=ON ^
-DGGML_NATIVE=OFF ^
@@ -899,7 +937,7 @@ jobs:
shell: bash
env:
- WINDOWS_BASEKIT_URL: https://registrationcenter-download.intel.com/akdlm/IRC_NAS/b380d914-366b-4b77-a74a-05e3c38b3514/intel-oneapi-base-toolkit-2025.0.0.882_offline.exe
+ WINDOWS_BASEKIT_URL: https://registrationcenter-download.intel.com/akdlm/IRC_NAS/7cd9bba0-7aab-4e30-b3ae-2221006a4a05/intel-oneapi-base-toolkit-2025.1.1.34_offline.exe
WINDOWS_DPCPP_MKL: intel.oneapi.win.cpp-dpcpp-common:intel.oneapi.win.mkl.devel:intel.oneapi.win.dnnl:intel.oneapi.win.tbb.devel
ONEAPI_ROOT: "C:/Program Files (x86)/Intel/oneAPI"
steps:
diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
index f8e072fb8..2067927be 100644
--- a/.github/workflows/docker.yml
+++ b/.github/workflows/docker.yml
@@ -42,8 +42,7 @@ jobs:
- { tag: "cpu", dockerfile: ".devops/cpu.Dockerfile", platforms: "linux/amd64", full: true, light: true, server: true, free_disk_space: false }
- { tag: "cuda", dockerfile: ".devops/cuda.Dockerfile", platforms: "linux/amd64", full: true, light: true, server: true, free_disk_space: false }
- { tag: "musa", dockerfile: ".devops/musa.Dockerfile", platforms: "linux/amd64", full: true, light: true, server: true, free_disk_space: true }
- # Note: the intel images are failing due to an out of disk space error
- # - { tag: "intel", dockerfile: ".devops/intel.Dockerfile", platforms: "linux/amd64", full: true, light: true, server: true, free_disk_space: false }
+ - { tag: "intel", dockerfile: ".devops/intel.Dockerfile", platforms: "linux/amd64", full: true, light: true, server: true, free_disk_space: true }
- { tag: "vulkan", dockerfile: ".devops/vulkan.Dockerfile", platforms: "linux/amd64", full: true, light: true, server: true, free_disk_space: false }
# Note: the rocm images are failing due to a compiler error and are disabled until this is fixed to allow the workflow to complete
#- {tag: "rocm", dockerfile: ".devops/rocm.Dockerfile", platforms: "linux/amd64,linux/arm64", full: true, light: true, server: true, free_disk_space: true }
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 5f54909dc..64fff175e 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -1,4 +1,4 @@
-name: Create Release
+name: Release
on:
workflow_dispatch: # allows manual triggering
@@ -131,8 +131,9 @@ jobs:
include:
- build: 'x64'
os: ubuntu-22.04
- - build: 'arm64'
- os: ubuntu-22.04-arm
+ # GGML_BACKEND_DL and GGML_CPU_ALL_VARIANTS are not currently supported on arm
+ # - build: 'arm64'
+ # os: ubuntu-22.04-arm
runs-on: ${{ matrix.os }}
@@ -159,6 +160,9 @@ jobs:
id: cmake_build
run: |
cmake -B build \
+ -DGGML_BACKEND_DL=ON \
+ -DGGML_NATIVE=OFF \
+ -DGGML_CPU_ALL_VARIANTS=ON \
-DLLAMA_FATAL_WARNINGS=ON \
${{ env.CMAKE_ARGS }}
cmake --build build --config Release -j $(nproc)
@@ -207,6 +211,9 @@ jobs:
id: cmake_build
run: |
cmake -B build \
+ -DGGML_BACKEND_DL=ON \
+ -DGGML_NATIVE=OFF \
+ -DGGML_CPU_ALL_VARIANTS=ON \
-DGGML_VULKAN=ON \
${{ env.CMAKE_ARGS }}
cmake --build build --config Release -j $(nproc)
@@ -227,30 +234,17 @@ jobs:
path: llama-${{ steps.tag.outputs.name }}-bin-ubuntu-vulkan-x64.zip
name: llama-bin-ubuntu-vulkan-x64.zip
- windows:
+ windows-cpu:
runs-on: windows-latest
- env:
- OPENBLAS_VERSION: 0.3.23
- VULKAN_VERSION: 1.4.309.0
-
strategy:
matrix:
include:
- - build: 'cpu-x64'
- defines: '-G "Ninja Multi-Config" -D CMAKE_TOOLCHAIN_FILE=cmake/x64-windows-llvm.cmake -DGGML_NATIVE=OFF -DGGML_BACKEND_DL=ON -DGGML_CPU_ALL_VARIANTS=ON -DGGML_OPENMP=OFF'
- #- build: 'openblas-x64'
- # defines: '-G "Ninja Multi-Config" -D CMAKE_TOOLCHAIN_FILE=cmake/x64-windows-llvm.cmake -DGGML_NATIVE=OFF -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: 'vulkan-x64'
- defines: '-DGGML_NATIVE=OFF -DGGML_BACKEND_DL=ON -DGGML_CPU_ALL_VARIANTS=ON -DGGML_VULKAN=ON'
- - build: 'cpu-arm64'
- defines: '-G "Ninja Multi-Config" -D CMAKE_TOOLCHAIN_FILE=cmake/arm64-windows-llvm.cmake -DGGML_NATIVE=OFF'
- - build: 'opencl-adreno-arm64'
- defines: '-G "Ninja Multi-Config" -D CMAKE_TOOLCHAIN_FILE=cmake/arm64-windows-llvm.cmake -DCMAKE_PREFIX_PATH="$env:RUNNER_TEMP/opencl-arm64-release" -DGGML_OPENCL=ON -DGGML_OPENCL_USE_ADRENO_KERNELS=ON'
+ - arch: 'x64'
+ - arch: 'arm64'
steps:
- name: Clone
- id: checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
@@ -258,28 +252,87 @@ jobs:
- name: ccache
uses: hendrikmuhs/ccache-action@v1.2.16
with:
- key: windows-latest-cmake-${{ matrix.build }}
+ key: windows-latest-cmake-cpu-${{ matrix.arch }}
variant: ccache
evict-old-files: 1d
- - name: Download OpenBLAS
- id: get_openblas
- if: ${{ matrix.build == 'openblas-x64' }}
+ - name: Install Ninja
run: |
- curl.exe -o $env:RUNNER_TEMP/openblas.zip -L "https://github.com/xianyi/OpenBLAS/releases/download/v${env:OPENBLAS_VERSION}/OpenBLAS-${env:OPENBLAS_VERSION}-x64.zip"
- curl.exe -o $env:RUNNER_TEMP/OpenBLAS.LICENSE.txt -L "https://github.com/xianyi/OpenBLAS/raw/v${env:OPENBLAS_VERSION}/LICENSE"
- mkdir $env:RUNNER_TEMP/openblas
- tar.exe -xvf $env:RUNNER_TEMP/openblas.zip -C $env:RUNNER_TEMP/openblas
- $vcdir = $(vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath)
- $msvc = $(join-path $vcdir $('VC\Tools\MSVC\'+$(gc -raw $(join-path $vcdir 'VC\Auxiliary\Build\Microsoft.VCToolsVersion.default.txt')).Trim()))
- $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
+ choco install ninja
+
+ - name: libCURL
+ id: get_libcurl
+ uses: ./.github/actions/windows-setup-curl
+ with:
+ architecture: ${{ matrix.arch == 'x64' && 'win64' || 'win64a' }}
+
+ - name: Build
+ shell: cmd
+ env:
+ CURL_PATH: ${{ steps.get_libcurl.outputs.curl_path }}
+ run: |
+ call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" ${{ matrix.arch }}
+ cmake -S . -B build -G "Ninja Multi-Config" ^
+ -D CMAKE_TOOLCHAIN_FILE=cmake/${{ matrix.arch }}-windows-llvm.cmake ^
+ -DGGML_NATIVE=OFF ^
+ -DGGML_BACKEND_DL=ON ^
+ -DGGML_CPU_ALL_VARIANTS=${{ matrix.arch == 'x64' && 'ON' || 'OFF' }} ^
+ -DGGML_OPENMP=ON ^
+ -DCURL_LIBRARY="%CURL_PATH%/lib/libcurl.dll.a" -DCURL_INCLUDE_DIR="%CURL_PATH%/include" ^
+ ${{ env.CMAKE_ARGS }}
+ cmake --build build --config Release
+
+ - name: Pack artifacts
+ id: pack_artifacts
+ env:
+ CURL_PATH: ${{ steps.get_libcurl.outputs.curl_path }}
+ run: |
+ Copy-Item $env:CURL_PATH\bin\libcurl-${{ matrix.arch }}.dll .\build\bin\Release\
+ Copy-Item "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Redist\MSVC\14.42.34433\debug_nonredist\${{ matrix.arch }}\Microsoft.VC143.OpenMP.LLVM\libomp140.${{ matrix.arch == 'x64' && 'x86_64' || 'aarch64' }}.dll" .\build\bin\Release\
+ 7z a llama-bin-win-cpu-${{ matrix.arch }}.zip .\build\bin\Release\*
+
+ - name: Upload artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ path: llama-bin-win-cpu-${{ matrix.arch }}.zip
+ name: llama-bin-win-cpu-${{ matrix.arch }}.zip
+
+ windows:
+ runs-on: windows-latest
+
+ env:
+ OPENBLAS_VERSION: 0.3.23
+ VULKAN_VERSION: 1.4.313.2
+
+ strategy:
+ matrix:
+ include:
+ - backend: 'vulkan'
+ arch: 'x64'
+ defines: '-DGGML_VULKAN=ON'
+ target: 'ggml-vulkan'
+ - backend: 'opencl-adreno'
+ arch: 'arm64'
+ defines: '-G "Ninja Multi-Config" -D CMAKE_TOOLCHAIN_FILE=cmake/arm64-windows-llvm.cmake -DCMAKE_PREFIX_PATH="$env:RUNNER_TEMP/opencl-arm64-release" -DGGML_OPENCL=ON -DGGML_OPENCL_USE_ADRENO_KERNELS=ON'
+ target: 'ggml-opencl'
+
+ steps:
+ - name: Clone
+ id: checkout
+ uses: actions/checkout@v4
+
+ - name: ccache
+ uses: hendrikmuhs/ccache-action@v1.2.16
+ with:
+ key: windows-latest-cmake-${{ matrix.backend }}-${{ matrix.arch }}
+ variant: ccache
+ evict-old-files: 1d
- name: Install Vulkan SDK
id: get_vulkan
- if: ${{ matrix.build == 'vulkan-x64' }}
+ if: ${{ matrix.backend == 'vulkan' }}
run: |
- curl.exe -o $env:RUNNER_TEMP/VulkanSDK-Installer.exe -L "https://sdk.lunarg.com/sdk/download/${env:VULKAN_VERSION}/windows/VulkanSDK-${env:VULKAN_VERSION}-Installer.exe"
+ 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"
@@ -291,7 +344,7 @@ jobs:
- name: Install OpenCL Headers and Libs
id: install_opencl
- if: ${{ matrix.build == 'opencl-adreno-arm64' }}
+ if: ${{ matrix.backend == 'opencl-adreno' && matrix.arch == 'arm64' }}
run: |
git clone https://github.com/KhronosGroup/OpenCL-Headers
cd OpenCL-Headers
@@ -309,58 +362,34 @@ jobs:
-DCMAKE_INSTALL_PREFIX="$env:RUNNER_TEMP/opencl-arm64-release"
cmake --build build-arm64-release --target install --config release
- - name: libCURL
- id: get_libcurl
- uses: ./.github/actions/windows-setup-curl
-
- name: Build
id: cmake_build
- env:
- CURL_PATH: ${{ steps.get_libcurl.outputs.curl_path }}
run: |
- cmake -S . -B build ${{ matrix.defines }} `
- -DCURL_LIBRARY="$env:CURL_PATH/lib/libcurl.dll.a" -DCURL_INCLUDE_DIR="$env:CURL_PATH/include" `
- ${{ env.CMAKE_ARGS }}
- cmake --build build --config Release -j ${env:NUMBER_OF_PROCESSORS}
-
- - name: Add libopenblas.dll
- id: add_libopenblas_dll
- if: ${{ matrix.build == 'openblas-x64' }}
- run: |
- cp $env:RUNNER_TEMP/openblas/bin/libopenblas.dll ./build/bin/Release/openblas.dll
- cp $env:RUNNER_TEMP/OpenBLAS.LICENSE.txt ./build/bin/Release/OpenBLAS-${env:OPENBLAS_VERSION}.txt
-
- - name: Determine tag name
- id: tag
- uses: ./.github/actions/get-tag-name
+ cmake -S . -B build ${{ matrix.defines }} -DGGML_NATIVE=OFF -DGGML_CPU=OFF -DGGML_BACKEND_DL=ON -DLLAMA_CURL=OFF
+ cmake --build build --config Release --target ${{ matrix.target }}
- name: Pack artifacts
id: pack_artifacts
- env:
- CURL_PATH: ${{ steps.get_libcurl.outputs.curl_path }}
run: |
- Copy-Item $env:CURL_PATH\bin\libcurl-x64.dll .\build\bin\Release\libcurl-x64.dll
- 7z a llama-${{ steps.tag.outputs.name }}-bin-win-${{ matrix.build }}.zip .\build\bin\Release\*
+ 7z a llama-bin-win-${{ matrix.backend }}-${{ matrix.arch }}.zip .\build\bin\Release\${{ matrix.target }}.dll
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
- path: llama-${{ steps.tag.outputs.name }}-bin-win-${{ matrix.build }}.zip
- name: llama-bin-win-${{ matrix.build }}.zip
+ path: llama-bin-win-${{ matrix.backend }}-${{ matrix.arch }}.zip
+ name: llama-bin-win-${{ matrix.backend }}-${{ matrix.arch }}.zip
windows-cuda:
- runs-on: windows-2019
+ runs-on: windows-2022
strategy:
matrix:
- cuda: ['12.4', '11.7']
+ cuda: ['12.4']
steps:
- name: Clone
id: checkout
uses: actions/checkout@v4
- with:
- fetch-depth: 0
- name: Install ccache
uses: hendrikmuhs/ccache-action@v1.2.16
@@ -379,45 +408,30 @@ jobs:
run: |
choco install ninja
- - name: libCURL
- id: get_libcurl
- uses: ./.github/actions/windows-setup-curl
-
- name: Build
id: cmake_build
shell: cmd
- env:
- CURL_PATH: ${{ steps.get_libcurl.outputs.curl_path }}
run: |
- call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvars64.bat"
+ call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" x64
cmake -S . -B build -G "Ninja Multi-Config" ^
- -DGGML_NATIVE=OFF ^
-DGGML_BACKEND_DL=ON ^
- -DGGML_CPU_ALL_VARIANTS=ON ^
+ -DGGML_NATIVE=OFF ^
+ -DGGML_CPU=OFF ^
-DGGML_CUDA=ON ^
- -DCURL_LIBRARY="%CURL_PATH%/lib/libcurl.dll.a" -DCURL_INCLUDE_DIR="%CURL_PATH%/include" ^
- ${{ env.CMAKE_ARGS }}
+ -DLLAMA_CURL=OFF
set /A NINJA_JOBS=%NUMBER_OF_PROCESSORS%-1
- cmake --build build --config Release -j %NINJA_JOBS% -t ggml
- cmake --build build --config Release
-
- - name: Determine tag name
- id: tag
- uses: ./.github/actions/get-tag-name
+ cmake --build build --config Release -j %NINJA_JOBS% --target ggml-cuda
- name: Pack artifacts
id: pack_artifacts
- env:
- CURL_PATH: ${{ steps.get_libcurl.outputs.curl_path }}
run: |
- cp $env:CURL_PATH\bin\libcurl-x64.dll .\build\bin\Release\libcurl-x64.dll
- 7z a llama-${{ steps.tag.outputs.name }}-bin-win-cuda${{ matrix.cuda }}-x64.zip .\build\bin\Release\*
+ 7z a llama-bin-win-cuda-${{ matrix.cuda }}-x64.zip .\build\bin\Release\ggml-cuda.dll
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
- path: llama-${{ steps.tag.outputs.name }}-bin-win-cuda${{ matrix.cuda }}-x64.zip
- name: llama-bin-win-cuda${{ matrix.cuda }}-x64.zip
+ path: llama-bin-win-cuda-${{ matrix.cuda }}-x64.zip
+ name: llama-bin-win-cuda-${{ matrix.cuda }}-x64.zip
- name: Copy and pack Cuda runtime
run: |
@@ -425,13 +439,13 @@ jobs:
$dst='.\build\bin\cudart\'
robocopy "${{env.CUDA_PATH}}\bin" $dst cudart64_*.dll cublas64_*.dll cublasLt64_*.dll
robocopy "${{env.CUDA_PATH}}\lib" $dst cudart64_*.dll cublas64_*.dll cublasLt64_*.dll
- 7z a cudart-llama-bin-win-cuda${{ matrix.cuda }}-x64.zip $dst\*
+ 7z a cudart-llama-bin-win-cuda-${{ matrix.cuda }}-x64.zip $dst\*
- name: Upload Cuda runtime
uses: actions/upload-artifact@v4
with:
- path: cudart-llama-bin-win-cuda${{ matrix.cuda }}-x64.zip
- name: cudart-llama-bin-win-cuda${{ matrix.cuda }}-x64.zip
+ path: cudart-llama-bin-win-cuda-${{ matrix.cuda }}-x64.zip
+ name: cudart-llama-bin-win-cuda-${{ matrix.cuda }}-x64.zip
windows-sycl:
runs-on: windows-latest
@@ -441,15 +455,14 @@ jobs:
shell: bash
env:
- WINDOWS_BASEKIT_URL: https://registrationcenter-download.intel.com/akdlm/IRC_NAS/b380d914-366b-4b77-a74a-05e3c38b3514/intel-oneapi-base-toolkit-2025.0.0.882_offline.exe
+ WINDOWS_BASEKIT_URL: https://registrationcenter-download.intel.com/akdlm/IRC_NAS/7cd9bba0-7aab-4e30-b3ae-2221006a4a05/intel-oneapi-base-toolkit-2025.1.1.34_offline.exe
WINDOWS_DPCPP_MKL: intel.oneapi.win.cpp-dpcpp-common:intel.oneapi.win.mkl.devel:intel.oneapi.win.dnnl:intel.oneapi.win.tbb.devel
ONEAPI_ROOT: "C:/Program Files (x86)/Intel/oneAPI"
+
steps:
- name: Clone
id: checkout
uses: actions/checkout@v4
- with:
- fetch-depth: 0
- name: ccache
uses: hendrikmuhs/ccache-action@v1.2.16
@@ -462,15 +475,18 @@ jobs:
run: |
scripts/install-oneapi.bat $WINDOWS_BASEKIT_URL $WINDOWS_DPCPP_MKL
- # TODO: add libcurl support ; we will also need to modify win-build-sycl.bat to accept user-specified args
-
- name: Build
id: cmake_build
- run: examples/sycl/win-build-sycl.bat
-
- - name: Determine tag name
- id: tag
- uses: ./.github/actions/get-tag-name
+ shell: cmd
+ run: |
+ call "C:\Program Files (x86)\Intel\oneAPI\setvars.bat" intel64 --force
+ cmake -G "Ninja" -B build ^
+ -DCMAKE_C_COMPILER=cl -DCMAKE_CXX_COMPILER=icx ^
+ -DCMAKE_BUILD_TYPE=Release ^
+ -DGGML_BACKEND_DL=ON -DBUILD_SHARED_LIBS=ON ^
+ -DGGML_CPU=OFF -DGGML_SYCL=ON ^
+ -DLLAMA_CURL=OFF
+ cmake --build build --target ggml-sycl -j
- name: Build the release package
id: pack_artifacts
@@ -495,12 +511,12 @@ jobs:
cp "${{ env.ONEAPI_ROOT }}/tbb/latest/bin/tbb12.dll" ./build/bin
echo "cp oneAPI running time dll files to ./build/bin done"
- 7z a llama-${{ steps.tag.outputs.name }}-bin-win-sycl-x64.zip ./build/bin/*
+ 7z a llama-bin-win-sycl-x64.zip ./build/bin/*
- name: Upload the release package
uses: actions/upload-artifact@v4
with:
- path: llama-${{ steps.tag.outputs.name }}-bin-win-sycl-x64.zip
+ path: llama-bin-win-sycl-x64.zip
name: llama-bin-win-sycl-x64.zip
windows-hip:
@@ -508,14 +524,14 @@ jobs:
strategy:
matrix:
- gpu_target: [gfx1100, gfx1101, gfx1030]
+ include:
+ - name: "radeon"
+ gpu_targets: "gfx1100;gfx1101;gfx1102;gfx1030;gfx1031;gfx1032"
steps:
- name: Clone
id: checkout
uses: actions/checkout@v4
- with:
- fetch-depth: 0
- name: Clone rocWMMA repository
id: clone_rocwmma
@@ -525,7 +541,7 @@ jobs:
- name: ccache
uses: hendrikmuhs/ccache-action@v1.2.16
with:
- key: windows-latest-cmake-hip-release
+ key: windows-latest-cmake-hip-${{ matrix.name }}-x64
evict-old-files: 1d
- name: Install
@@ -543,50 +559,39 @@ jobs:
run: |
& 'C:\Program Files\AMD\ROCm\*\bin\clang.exe' --version
- - name: libCURL
- id: get_libcurl
- uses: ./.github/actions/windows-setup-curl
-
- name: Build
id: cmake_build
- env:
- CURL_PATH: ${{ steps.get_libcurl.outputs.curl_path }}
run: |
$env:HIP_PATH=$(Resolve-Path 'C:\Program Files\AMD\ROCm\*\bin\clang.exe' | split-path | split-path)
$env:CMAKE_PREFIX_PATH="${env:HIP_PATH}"
cmake -G "Unix Makefiles" -B build -S . `
-DCMAKE_C_COMPILER="${env:HIP_PATH}\bin\clang.exe" `
-DCMAKE_CXX_COMPILER="${env:HIP_PATH}\bin\clang++.exe" `
- -DCMAKE_CXX_FLAGS="-I$($PWD.Path.Replace('\', '/'))/rocwmma/library/include/" `
+ -DCMAKE_CXX_FLAGS="-I$($PWD.Path.Replace('\', '/'))/rocwmma/library/include/ -Wno-ignored-attributes -Wno-nested-anon-types" `
-DCMAKE_BUILD_TYPE=Release `
- -DAMDGPU_TARGETS=${{ matrix.gpu_target }} `
+ -DGGML_BACKEND_DL=ON `
+ -DGGML_NATIVE=OFF `
+ -DGGML_CPU=OFF `
+ -DAMDGPU_TARGETS="${{ matrix.gpu_targets }}" `
-DGGML_HIP_ROCWMMA_FATTN=ON `
-DGGML_HIP=ON `
- -DCURL_LIBRARY="$env:CURL_PATH/lib/libcurl.dll.a" -DCURL_INCLUDE_DIR="$env:CURL_PATH/include" `
- ${{ env.CMAKE_ARGS }}
- cmake --build build -j ${env:NUMBER_OF_PROCESSORS}
+ -DLLAMA_CURL=OFF
+ cmake --build build --target ggml-hip -j ${env:NUMBER_OF_PROCESSORS}
md "build\bin\rocblas\library\"
cp "${env:HIP_PATH}\bin\hipblas.dll" "build\bin\"
cp "${env:HIP_PATH}\bin\rocblas.dll" "build\bin\"
cp "${env:HIP_PATH}\bin\rocblas\library\*" "build\bin\rocblas\library\"
- - name: Determine tag name
- id: tag
- uses: ./.github/actions/get-tag-name
-
- name: Pack artifacts
id: pack_artifacts
- env:
- CURL_PATH: ${{ steps.get_libcurl.outputs.curl_path }}
run: |
- cp $env:CURL_PATH\bin\libcurl-x64.dll .\build\bin\libcurl-x64.dll
- 7z a llama-${{ steps.tag.outputs.name }}-bin-win-hip-x64-${{ matrix.gpu_target }}.zip .\build\bin\*
+ 7z a llama-bin-win-hip-${{ matrix.name }}-x64.zip .\build\bin\*
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
- path: llama-${{ steps.tag.outputs.name }}-bin-win-hip-x64-${{ matrix.gpu_target }}.zip
- name: llama-bin-win-hip-x64-${{ matrix.gpu_target }}.zip
+ path: llama-bin-win-hip-${{ matrix.name }}-x64.zip
+ name: llama-bin-win-hip-${{ matrix.name }}-x64.zip
ios-xcode-build:
runs-on: macos-latest
@@ -648,14 +653,16 @@ jobs:
runs-on: ubuntu-latest
needs:
- - ubuntu-22-cpu
- - ubuntu-22-vulkan
- windows
+ - windows-cpu
- windows-cuda
- windows-sycl
- windows-hip
+ - ubuntu-22-cpu
+ - ubuntu-22-vulkan
- macOS-arm64
- macOS-x64
+ - ios-xcode-build
steps:
- name: Clone
@@ -673,10 +680,43 @@ jobs:
uses: actions/download-artifact@v4
with:
path: ./artifact
+ merge-multiple: true
- name: Move artifacts
id: move_artifacts
- run: mkdir -p ./artifact/release && mv ./artifact/*/*.zip ./artifact/release
+ run: |
+ mkdir -p release
+
+ echo "Adding CPU backend files to existing zips..."
+ for arch in x64 arm64; do
+ cpu_zip="artifact/llama-bin-win-cpu-${arch}.zip"
+ temp_dir=$(mktemp -d)
+ echo "Extracting CPU backend for $arch..."
+ unzip "$cpu_zip" -d "$temp_dir"
+
+ echo "Adding CPU files to $arch zips..."
+ for target_zip in artifact/llama-bin-win-*-${arch}.zip; do
+ if [[ "$target_zip" == "$cpu_zip" ]]; then
+ continue
+ fi
+ echo "Adding CPU backend to $(basename "$target_zip")"
+ realpath_target_zip=$(realpath "$target_zip")
+ (cd "$temp_dir" && zip -r "$realpath_target_zip" .)
+ done
+
+ rm -rf "$temp_dir"
+ done
+
+ echo "Renaming and moving zips to release..."
+ for zip_file in artifact/llama-bin-win-*.zip; do
+ base_name=$(basename "$zip_file" .zip)
+ zip_name="llama-${{ steps.tag.outputs.name }}-${base_name#llama-}.zip"
+ echo "Moving $zip_file to release/$zip_name"
+ mv "$zip_file" "release/$zip_name"
+ done
+
+ echo "Moving other artifacts..."
+ mv -v artifact/*.zip release
- name: Create release
id: create_release
@@ -695,7 +735,7 @@ jobs:
const path = require('path');
const fs = require('fs');
const release_id = '${{ steps.create_release.outputs.id }}';
- for (let file of await fs.readdirSync('./artifact/release')) {
+ for (let file of await fs.readdirSync('./release')) {
if (path.extname(file) === '.zip') {
console.log('uploadReleaseAsset', file);
await github.repos.uploadReleaseAsset({
@@ -703,7 +743,7 @@ jobs:
repo: context.repo.repo,
release_id: release_id,
name: file,
- data: await fs.readFileSync(`./artifact/release/${file}`)
+ data: await fs.readFileSync(`./release/${file}`)
});
}
}
diff --git a/.github/workflows/server.yml b/.github/workflows/server.yml
index 4baf6f6c7..f6da48857 100644
--- a/.github/workflows/server.yml
+++ b/.github/workflows/server.yml
@@ -180,7 +180,7 @@ jobs:
server-windows:
- runs-on: windows-2019
+ runs-on: windows-2022
steps:
- name: Clone
diff --git a/.github/workflows/winget.yml b/.github/workflows/winget.yml
new file mode 100644
index 000000000..5c2861559
--- /dev/null
+++ b/.github/workflows/winget.yml
@@ -0,0 +1,42 @@
+name: Update Winget Package
+
+on:
+ workflow_dispatch: # allows manual triggering
+ schedule:
+ - cron: '28 5 * * *' # Update every day at 5:28 UTC
+
+jobs:
+ update:
+ name: Update Winget Package
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Install cargo binstall
+ uses: cargo-bins/cargo-binstall@268643a6b5ea099f5718ee5cd3ff7dc89a5eb49b
+
+ - name: Install komac
+ run: |
+ cargo binstall komac@2.11.2 -y
+
+ - name: Find latest release
+ id: find_latest_release
+ uses: actions/github-script@v6
+ with:
+ script: |
+ const { data: releases } = await github.rest.repos.listReleases({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ });
+ console.log("Latest release:", releases[0].tag_name);
+ return releases[0].tag_name;
+
+ - name: Update manifest
+ env:
+ VERSION: ${{ steps.find_latest_release.outputs.result }}
+ run: |
+ echo "Updating manifest..."
+ komac update --version ${{ env.VERSION }} \
+ --urls "https://github.com/ggml-org/llama.cpp/releases/download/${{ env.VERSION }}/llama-${{ env.VERSION }}-bin-win-vulkan-x64.zip" \
+ --token ${{ secrets.WINGET_GITHUB_TOKEN }} \
+ --submit \
+ ggml.llamacpp
diff --git a/CMakeLists.txt b/CMakeLists.txt
index ac3e90903..d2becb04c 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -89,6 +89,14 @@ option(LLAMA_LLGUIDANCE "llama-common: include LLGuidance library for structured
include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/build-info.cmake)
include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/common.cmake)
+if (NOT DEFINED LLAMA_BUILD_NUMBER)
+ set(LLAMA_BUILD_NUMBER ${BUILD_NUMBER})
+endif()
+if (NOT DEFINED LLAMA_BUILD_COMMIT)
+ set(LLAMA_BUILD_COMMIT ${BUILD_COMMIT})
+endif()
+set(LLAMA_INSTALL_VERSION 0.0.${LLAMA_BUILD_NUMBER})
+
# override ggml options
set(GGML_ALL_WARNINGS ${LLAMA_ALL_WARNINGS})
set(GGML_FATAL_WARNINGS ${LLAMA_FATAL_WARNINGS})
@@ -155,10 +163,17 @@ if (LLAMA_USE_SYSTEM_GGML)
endif()
if (NOT TARGET ggml AND NOT LLAMA_USE_SYSTEM_GGML)
+ set(GGML_BUILD_NUMBER ${LLAMA_BUILD_NUMBER})
+ set(GGML_BUILD_COMMIT ${LLAMA_BUILD_COMMIT})
add_subdirectory(ggml)
# ... otherwise assume ggml is added by a parent CMakeLists.txt
endif()
+if (MINGW)
+ # Target Windows 8 for PrefetchVirtualMemory
+ add_compile_definitions(_WIN32_WINNT=${GGML_WIN_VER})
+endif()
+
#
# build the library
#
@@ -199,10 +214,6 @@ endif()
include(GNUInstallDirs)
include(CMakePackageConfigHelpers)
-set(LLAMA_BUILD_NUMBER ${BUILD_NUMBER})
-set(LLAMA_BUILD_COMMIT ${BUILD_COMMIT})
-set(LLAMA_INSTALL_VERSION 0.0.${BUILD_NUMBER})
-
set(LLAMA_INCLUDE_INSTALL_DIR ${CMAKE_INSTALL_INCLUDEDIR} CACHE PATH "Location of header files")
set(LLAMA_LIB_INSTALL_DIR ${CMAKE_INSTALL_LIBDIR} CACHE PATH "Location of library files")
set(LLAMA_BIN_INSTALL_DIR ${CMAKE_INSTALL_BINDIR} CACHE PATH "Location of binary files")
diff --git a/Makefile b/Makefile
index 958ad8f2f..ac442aec0 100644
--- a/Makefile
+++ b/Makefile
@@ -367,7 +367,7 @@ ifdef LLAMA_SERVER_SSL
endif
ifndef GGML_NO_CPU_AARCH64
- MK_CPPFLAGS += -DGGML_USE_CPU_AARCH64
+ MK_CPPFLAGS += -DGGML_USE_CPU_REPACK
endif
# warnings
@@ -970,7 +970,7 @@ OBJ_GGML = \
$(DIR_GGML)/src/ggml-threading.o \
$(DIR_GGML)/src/ggml-cpu/ggml-cpu.o \
$(DIR_GGML)/src/ggml-cpu/ggml-cpu_cpp.o \
- $(DIR_GGML)/src/ggml-cpu/ggml-cpu-aarch64.o \
+ $(DIR_GGML)/src/ggml-cpu/repack.o \
$(DIR_GGML)/src/ggml-cpu/ggml-cpu-hbm.o \
$(DIR_GGML)/src/ggml-cpu/ggml-cpu-quants.o \
$(DIR_GGML)/src/ggml-cpu/ggml-cpu-traits.o \
diff --git a/README.md b/README.md
index 0401723ff..90c7364df 100644
--- a/README.md
+++ b/README.md
@@ -3,9 +3,10 @@

[](https://opensource.org/licenses/MIT)
+[](https://github.com/ggml-org/llama.cpp/releases)
[](https://github.com/ggml-org/llama.cpp/actions/workflows/server.yml)
-[Roadmap](https://github.com/users/ggerganov/projects/7) / [Project status](https://github.com/ggml-org/llama.cpp/discussions/3471) / [Manifesto](https://github.com/ggml-org/llama.cpp/discussions/205) / [ggml](https://github.com/ggml-org/ggml)
+[Roadmap](https://github.com/users/ggerganov/projects/7) / [Manifesto](https://github.com/ggml-org/llama.cpp/discussions/205) / [ggml](https://github.com/ggml-org/ggml)
Inference of Meta's [LLaMA](https://arxiv.org/abs/2302.13971) model (and others) in pure C/C++
@@ -17,7 +18,6 @@ Inference of Meta's [LLaMA](https://arxiv.org/abs/2302.13971) model (and others)
## Hot topics
- 🔥 Multimodal support arrived in `llama-server`: [#12898](https://github.com/ggml-org/llama.cpp/pull/12898) | [documentation](./docs/multimodal.md)
-- **GGML developer experience survey (organized and reviewed by NVIDIA):** [link](https://forms.gle/Gasw3cRgyhNEnrwK9)
- A new binary `llama-mtmd-cli` is introduced to replace `llava-cli`, `minicpmv-cli`, `gemma3-cli` ([#13012](https://github.com/ggml-org/llama.cpp/pull/13012)) and `qwen2vl-cli` ([#13141](https://github.com/ggml-org/llama.cpp/pull/13141)), `libllava` will be deprecated
- VS Code extension for FIM completions: https://github.com/ggml-org/llama.vscode
- Universal [tool call support](./docs/function-calling.md) in `llama-server` https://github.com/ggml-org/llama.cpp/pull/9639
@@ -28,6 +28,30 @@ Inference of Meta's [LLaMA](https://arxiv.org/abs/2302.13971) model (and others)
----
+## Quick start
+
+Getting started with llama.cpp is straightforward. Here are several ways to install it on your machine:
+
+- Install `llama.cpp` using [brew, nix or winget](docs/install.md)
+- Run with Docker - see our [Docker documentation](docs/docker.md)
+- Download pre-built binaries from the [releases page](https://github.com/ggml-org/llama.cpp/releases)
+- Build from source by cloning this repository - check out [our build guide](docs/build.md)
+
+Once installed, you'll need a model to work with. Head to the [Obtaining and quantizing models](#obtaining-and-quantizing-models) section to learn more.
+
+Example command:
+
+```sh
+# Use a local model file
+llama-cli -m my_model.gguf
+
+# Or download and run a model directly from Hugging Face
+llama-cli -hf ggml-org/gemma-3-1b-it-GGUF
+
+# Launch OpenAI-compatible API server
+llama-server -hf ggml-org/gemma-3-1b-it-GGUF
+```
+
## Description
The main goal of `llama.cpp` is to enable LLM inference with minimal setup and state-of-the-art performance on a wide
@@ -37,7 +61,7 @@ range of hardware - locally and in the cloud.
- Apple silicon is a first-class citizen - optimized via ARM NEON, Accelerate and Metal frameworks
- AVX, AVX2, AVX512 and AMX support for x86 architectures
- 1.5-bit, 2-bit, 3-bit, 4-bit, 5-bit, 6-bit, and 8-bit integer quantization for faster inference and reduced memory use
-- Custom CUDA kernels for running LLMs on NVIDIA GPUs (support for AMD GPUs via HIP and Moore Threads MTT GPUs via MUSA)
+- Custom CUDA kernels for running LLMs on NVIDIA GPUs (support for AMD GPUs via HIP and Moore Threads GPUs via MUSA)
- Vulkan and SYCL backend support
- CPU+GPU hybrid inference to partially accelerate models larger than the total VRAM capacity
@@ -130,6 +154,7 @@ Instructions for adding support for new models: [HOWTO-add-model.md](docs/develo
Bindings
+- Python: [ddh0/easy-llama](https://github.com/ddh0/easy-llama)
- Python: [abetlen/llama-cpp-python](https://github.com/abetlen/llama-cpp-python)
- Go: [go-skynet/go-llama.cpp](https://github.com/go-skynet/go-llama.cpp)
- Node.js: [withcatai/node-llama-cpp](https://github.com/withcatai/node-llama-cpp)
@@ -229,6 +254,7 @@ Instructions for adding support for new models: [HOWTO-add-model.md](docs/develo
+
## Supported backends
| Backend | Target devices |
@@ -237,7 +263,7 @@ Instructions for adding support for new models: [HOWTO-add-model.md](docs/develo
| [BLAS](docs/build.md#blas-build) | All |
| [BLIS](docs/backend/BLIS.md) | All |
| [SYCL](docs/backend/SYCL.md) | Intel and Nvidia GPU |
-| [MUSA](docs/build.md#musa) | Moore Threads MTT GPU |
+| [MUSA](docs/build.md#musa) | Moore Threads GPU |
| [CUDA](docs/build.md#cuda) | Nvidia GPU |
| [HIP](docs/build.md#hip) | AMD GPU |
| [Vulkan](docs/build.md#vulkan) | GPU |
@@ -245,16 +271,6 @@ Instructions for adding support for new models: [HOWTO-add-model.md](docs/develo
| [OpenCL](docs/backend/OPENCL.md) | Adreno GPU |
| [RPC](https://github.com/ggml-org/llama.cpp/tree/master/tools/rpc) | All |
-## Building the project
-
-The main product of this project is the `llama` library. Its C-style interface can be found in [include/llama.h](include/llama.h).
-The project also includes many example programs and tools using the `llama` library. The examples range from simple, minimal code snippets to sophisticated sub-projects such as an OpenAI-compatible HTTP server. Possible methods for obtaining the binaries:
-
-- Clone this repository and build locally, see [how to build](docs/build.md)
-- On MacOS or Linux, install `llama.cpp` via [brew, flox or nix](docs/install.md)
-- Use a Docker image, see [documentation for Docker](docs/docker.md)
-- Download pre-built binaries from [releases](https://github.com/ggml-org/llama.cpp/releases)
-
## Obtaining and quantizing models
The [Hugging Face](https://huggingface.co) platform hosts a [number of LLMs](https://huggingface.co/models?library=gguf&sort=trending) compatible with `llama.cpp`:
@@ -262,7 +278,11 @@ The [Hugging Face](https://huggingface.co) platform hosts a [number of LLMs](htt
- [Trending](https://huggingface.co/models?library=gguf&sort=trending)
- [LLaMA](https://huggingface.co/models?sort=trending&search=llama+gguf)
-You can either manually download the GGUF file or directly use any `llama.cpp`-compatible models from [Hugging Face](https://huggingface.co/) or other model hosting sites, such as [ModelScope](https://modelscope.cn/), by using this CLI argument: `-hf /[:quant]`.
+You can either manually download the GGUF file or directly use any `llama.cpp`-compatible models from [Hugging Face](https://huggingface.co/) or other model hosting sites, such as [ModelScope](https://modelscope.cn/), by using this CLI argument: `-hf /[:quant]`. For example:
+
+```sh
+llama-cli -hf ggml-org/gemma-3-1b-it-GGUF
+```
By default, the CLI would download from Hugging Face, you can switch to other options with the environment variable `MODEL_ENDPOINT`. For example, you may opt to downloading model checkpoints from ModelScope or other model sharing communities by setting the environment variable, e.g. `MODEL_ENDPOINT=https://www.modelscope.cn/`.
@@ -572,4 +592,12 @@ automatically. For example:
$ echo "source ~/.llama-completion.bash" >> ~/.bashrc
```
-## References
+## Dependencies
+
+- [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
+- [nlohmann/json](https://github.com/nlohmann/json) - Single-header JSON library, used by various tools/examples - MIT License
+- [minja](https://github.com/google/minja) - Minimal Jinja parser in C++, used by various tools/examples - MIT License
+- [linenoise.cpp](./tools/run/linenoise.cpp/linenoise.cpp) - C++ library that provides readline-like line editing capabilities, used by `llama-run` - BSD 2-Clause License
+- [curl](https://curl.se/) - Client-side URL transfer library, used by various tools/examples - [CURL License](https://curl.se/docs/copyright.html)
+- [miniaudio.h](https://github.com/mackron/miniaudio) - Single-header audio format decoder, used by multimodal subsystem - Public domain
diff --git a/build-xcframework.sh b/build-xcframework.sh
index 3c2498b03..a08419a80 100755
--- a/build-xcframework.sh
+++ b/build-xcframework.sh
@@ -117,6 +117,7 @@ setup_framework_structure() {
# Copy all required headers (common for all platforms)
cp include/llama.h ${header_path}
cp ggml/include/ggml.h ${header_path}
+ cp ggml/include/ggml-opt.h ${header_path}
cp ggml/include/ggml-alloc.h ${header_path}
cp ggml/include/ggml-backend.h ${header_path}
cp ggml/include/ggml-metal.h ${header_path}
diff --git a/ci/README.md b/ci/README.md
index ec3f44350..6e297f1a8 100644
--- a/ci/README.md
+++ b/ci/README.md
@@ -54,7 +54,7 @@ docker run --privileged -it \
-v $HOME/llama.cpp/ci-cache:/ci-cache \
-v $HOME/llama.cpp/ci-results:/ci-results \
-v $PWD:/ws -w /ws \
- mthreads/musa:rc3.1.1-devel-ubuntu22.04
+ mthreads/musa:rc4.0.1-mudnn-devel-ubuntu22.04
```
Inside the container, execute the following commands:
diff --git a/ci/run.sh b/ci/run.sh
index b49a3a5f8..e1b777c30 100755
--- a/ci/run.sh
+++ b/ci/run.sh
@@ -39,14 +39,27 @@ sd=`dirname $0`
cd $sd/../
SRC=`pwd`
-CMAKE_EXTRA="-DLLAMA_FATAL_WARNINGS=ON -DLLAMA_CURL=OFF"
+CMAKE_EXTRA="-DLLAMA_FATAL_WARNINGS=ON -DLLAMA_CURL=ON"
if [ ! -z ${GG_BUILD_METAL} ]; then
CMAKE_EXTRA="${CMAKE_EXTRA} -DGGML_METAL=ON -DGGML_METAL_USE_BF16=ON"
fi
if [ ! -z ${GG_BUILD_CUDA} ]; then
- CMAKE_EXTRA="${CMAKE_EXTRA} -DGGML_CUDA=ON -DCMAKE_CUDA_ARCHITECTURES=native"
+ CMAKE_EXTRA="${CMAKE_EXTRA} -DGGML_CUDA=ON"
+
+ if command -v nvidia-smi >/dev/null 2>&1; then
+ CUDA_ARCH=$(nvidia-smi --query-gpu=compute_cap --format=csv,noheader,nounits 2>/dev/null | head -1 | tr -d '.')
+ if [[ -n "$CUDA_ARCH" && "$CUDA_ARCH" =~ ^[0-9]+$ ]]; then
+ CMAKE_EXTRA="${CMAKE_EXTRA} -DCMAKE_CUDA_ARCHITECTURES=${CUDA_ARCH}"
+ else
+ echo "Warning: Using fallback CUDA architectures"
+ CMAKE_EXTRA="${CMAKE_EXTRA} -DCMAKE_CUDA_ARCHITECTURES=61;70;75;80;86;89"
+ fi
+ else
+ echo "Error: nvidia-smi not found, cannot build with CUDA"
+ exit 1
+ fi
fi
if [ ! -z ${GG_BUILD_SYCL} ]; then
@@ -766,7 +779,7 @@ function gg_run_rerank_tiny {
model_f16="${path_models}/ggml-model-f16.gguf"
# for this model, the SEP token is ""
- (time ./bin/llama-embedding --model ${model_f16} -p "what is panda?hi\nwhat is panda?it's a bear\nwhat is panda?The giant panda (Ailuropoda melanoleuca), sometimes called a panda bear or simply panda, is a bear species endemic to China." -ngl 99 -c 0 --pooling rank --embd-normalize -1 --verbose-prompt) 2>&1 | tee -a $OUT/${ci}-rk-f16.log
+ (time ./bin/llama-embedding --model ${model_f16} -p "what is panda?\thi\nwhat is panda?\tit's a bear\nwhat is panda?\tThe giant panda (Ailuropoda melanoleuca), sometimes called a panda bear or simply panda, is a bear species endemic to China." -ngl 99 -c 0 --pooling rank --embd-normalize -1 --verbose-prompt) 2>&1 | tee -a $OUT/${ci}-rk-f16.log
# sample output
# rerank score 0: 0.029
diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt
index 6b0011e4d..f43a630c9 100644
--- a/common/CMakeLists.txt
+++ b/common/CMakeLists.txt
@@ -7,8 +7,8 @@ llama_add_compile_flags()
# Build info header
#
-if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/../.git")
- set(GIT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../.git")
+if(EXISTS "${PROJECT_SOURCE_DIR}/.git")
+ set(GIT_DIR "${PROJECT_SOURCE_DIR}/.git")
# Is git submodule
if(NOT IS_DIRECTORY "${GIT_DIR}")
@@ -18,36 +18,26 @@ if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/../.git")
if (SLASH_POS EQUAL 0)
set(GIT_DIR "${REAL_GIT_DIR}")
else()
- set(GIT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../${REAL_GIT_DIR}")
+ set(GIT_DIR "${PROJECT_SOURCE_DIR}/${REAL_GIT_DIR}")
endif()
endif()
if(EXISTS "${GIT_DIR}/index")
- set(GIT_INDEX "${GIT_DIR}/index")
+ # For build-info.cpp below
+ set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${GIT_DIR}/index")
else()
message(WARNING "Git index not found in git repository.")
- set(GIT_INDEX "")
endif()
else()
message(WARNING "Git repository not found; to enable automatic generation of build info, make sure Git is installed and the project is a Git repository.")
- set(GIT_INDEX "")
endif()
-# Add a custom command to rebuild build-info.cpp when .git/index changes
-add_custom_command(
- OUTPUT "${CMAKE_CURRENT_SOURCE_DIR}/build-info.cpp"
- COMMENT "Generating build details from Git"
- COMMAND ${CMAKE_COMMAND} -DMSVC=${MSVC} -DCMAKE_C_COMPILER_VERSION=${CMAKE_C_COMPILER_VERSION}
- -DCMAKE_C_COMPILER_ID=${CMAKE_C_COMPILER_ID} -DCMAKE_VS_PLATFORM_NAME=${CMAKE_VS_PLATFORM_NAME}
- -DCMAKE_C_COMPILER=${CMAKE_C_COMPILER}
- -DCMAKE_SYSTEM_NAME=${CMAKE_SYSTEM_NAME} -DCMAKE_SYSTEM_PROCESSOR=${CMAKE_SYSTEM_PROCESSOR}
- -P "${CMAKE_CURRENT_SOURCE_DIR}/cmake/build-info-gen-cpp.cmake"
- WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/.."
- DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/build-info.cpp.in" ${GIT_INDEX}
- VERBATIM
-)
+set(TEMPLATE_FILE "${CMAKE_CURRENT_SOURCE_DIR}/build-info.cpp.in")
+set(OUTPUT_FILE "${CMAKE_CURRENT_BINARY_DIR}/build-info.cpp")
+configure_file(${TEMPLATE_FILE} ${OUTPUT_FILE})
+
set(TARGET build_info)
-add_library(${TARGET} OBJECT build-info.cpp)
+add_library(${TARGET} OBJECT ${OUTPUT_FILE})
if (BUILD_SHARED_LIBS)
set_target_properties(${TARGET} PROPERTIES POSITION_INDEPENDENT_CODE ON)
endif()
@@ -58,21 +48,24 @@ add_library(${TARGET} STATIC
arg.cpp
arg.h
base64.hpp
+ chat-parser.cpp
+ chat-parser.h
chat.cpp
chat.h
common.cpp
common.h
console.cpp
console.h
+ json-partial.cpp
+ json-partial.h
json-schema-to-grammar.cpp
- json.hpp
llguidance.cpp
log.cpp
log.h
- minja/chat-template.hpp
- minja/minja.hpp
ngram-cache.cpp
ngram-cache.h
+ regex-partial.cpp
+ regex-partial.h
sampling.cpp
sampling.h
speculative.cpp
@@ -119,8 +112,8 @@ if (LLAMA_LLGUIDANCE)
ExternalProject_Add(llguidance_ext
GIT_REPOSITORY https://github.com/guidance-ai/llguidance
- # v0.7.19 (+ fancy-regex build fix):
- GIT_TAG b59f98f85269892a7de3d3641ad155366f13daa6
+ # v0.7.20 (+ fix to build on GCC 15):
+ GIT_TAG b5b8b64dba11c4e4ee6b1d1450d3a3ae279891e8
PREFIX ${CMAKE_BINARY_DIR}/llguidance
SOURCE_DIR ${LLGUIDANCE_SRC}
BUILD_IN_SOURCE TRUE
@@ -141,7 +134,7 @@ if (LLAMA_LLGUIDANCE)
set(LLAMA_COMMON_EXTRA_LIBS ${LLAMA_COMMON_EXTRA_LIBS} llguidance ${LLGUIDANCE_PLATFORM_LIBS})
endif ()
-target_include_directories(${TARGET} PUBLIC .)
+target_include_directories(${TARGET} PUBLIC . ../vendor)
target_compile_features (${TARGET} PUBLIC cxx_std_17)
target_link_libraries (${TARGET} PRIVATE ${LLAMA_COMMON_EXTRA_LIBS} PUBLIC llama Threads::Threads)
diff --git a/common/arg.cpp b/common/arg.cpp
index f67e0d96d..c4ad85c47 100644
--- a/common/arg.cpp
+++ b/common/arg.cpp
@@ -1,10 +1,11 @@
-#include "gguf.h" // for reading GGUF splits
#include "arg.h"
+#include "chat.h"
#include "common.h"
+#include "gguf.h" // for reading GGUF splits
+#include "json-schema-to-grammar.h"
#include "log.h"
#include "sampling.h"
-#include "chat.h"
// fix problem with std::min and std::max
#if defined(_WIN32)
@@ -15,6 +16,9 @@
#include
#endif
+#define JSON_ASSERT GGML_ASSERT
+#include
+
#include
#include
#include
@@ -34,12 +38,10 @@
#include
#endif
-#include "json-schema-to-grammar.h"
-
using json = nlohmann::ordered_json;
std::initializer_list mmproj_examples = {
- LLAMA_EXAMPLE_LLAVA,
+ LLAMA_EXAMPLE_MTMD,
LLAMA_EXAMPLE_SERVER,
};
@@ -242,7 +244,56 @@ static bool curl_perform_with_retry(const std::string & url, CURL * curl, int ma
}
// download one single file from remote URL to local path
-static bool common_download_file_single(const std::string & url, const std::string & path, const std::string & bearer_token) {
+static bool common_download_file_single(const std::string & url, const std::string & path, const std::string & bearer_token, bool offline) {
+ // Check if the file already exists locally
+ auto file_exists = std::filesystem::exists(path);
+
+ // If the file exists, check its JSON metadata companion file.
+ std::string metadata_path = path + ".json";
+ nlohmann::json metadata; // TODO @ngxson : get rid of this json, use regex instead
+ std::string etag;
+ std::string last_modified;
+
+ if (file_exists) {
+ if (offline) {
+ LOG_INF("%s: using cached file (offline mode): %s\n", __func__, path.c_str());
+ return true; // skip verification/downloading
+ }
+ // Try and read the JSON metadata file (note: stream autoclosed upon exiting this block).
+ std::ifstream metadata_in(metadata_path);
+ if (metadata_in.good()) {
+ try {
+ metadata_in >> metadata;
+ LOG_DBG("%s: previous metadata file found %s: %s\n", __func__, metadata_path.c_str(), metadata.dump().c_str());
+ if (metadata.contains("etag") && metadata.at("etag").is_string()) {
+ etag = metadata.at("etag");
+ }
+ if (metadata.contains("lastModified") && metadata.at("lastModified").is_string()) {
+ last_modified = metadata.at("lastModified");
+ }
+ } catch (const nlohmann::json::exception & e) {
+ LOG_ERR("%s: error reading metadata file %s: %s\n", __func__, metadata_path.c_str(), e.what());
+ }
+ }
+ // if we cannot open the metadata file, we assume that the downloaded file is not valid (etag and last-modified are left empty, so we will download it again)
+ } else {
+ if (offline) {
+ LOG_ERR("%s: required file is not available in cache (offline mode): %s\n", __func__, path.c_str());
+ return false;
+ }
+ LOG_INF("%s: no previous model file found %s\n", __func__, path.c_str());
+ }
+
+ // Send a HEAD request to retrieve the etag and last-modified headers
+ struct common_load_model_from_url_headers {
+ std::string etag;
+ std::string last_modified;
+ };
+
+ common_load_model_from_url_headers headers;
+ bool head_request_ok = false;
+ bool should_download = !file_exists; // by default, we should download if the file does not exist
+
// Initialize libcurl
curl_ptr curl(curl_easy_init(), &curl_easy_cleanup);
curl_slist_ptr http_headers;
@@ -269,91 +320,47 @@ static bool common_download_file_single(const std::string & url, const std::stri
curl_easy_setopt(curl.get(), CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA);
#endif
- // Check if the file already exists locally
- auto file_exists = std::filesystem::exists(path);
+ typedef size_t(*CURLOPT_HEADERFUNCTION_PTR)(char *, size_t, size_t, void *);
+ auto header_callback = [](char * buffer, size_t /*size*/, size_t n_items, void * userdata) -> size_t {
+ common_load_model_from_url_headers * headers = (common_load_model_from_url_headers *) userdata;
- // If the file exists, check its JSON metadata companion file.
- std::string metadata_path = path + ".json";
- nlohmann::json metadata; // TODO @ngxson : get rid of this json, use regex instead
- std::string etag;
- std::string last_modified;
+ static std::regex header_regex("([^:]+): (.*)\r\n");
+ static std::regex etag_regex("ETag", std::regex_constants::icase);
+ static std::regex last_modified_regex("Last-Modified", std::regex_constants::icase);
- if (file_exists) {
- // Try and read the JSON metadata file (note: stream autoclosed upon exiting this block).
- std::ifstream metadata_in(metadata_path);
- if (metadata_in.good()) {
- try {
- metadata_in >> metadata;
- LOG_DBG("%s: previous metadata file found %s: %s\n", __func__, metadata_path.c_str(), metadata.dump().c_str());
- if (metadata.contains("etag") && metadata.at("etag").is_string()) {
- etag = metadata.at("etag");
- }
- if (metadata.contains("lastModified") && metadata.at("lastModified").is_string()) {
- last_modified = metadata.at("lastModified");
- }
- } catch (const nlohmann::json::exception & e) {
- LOG_ERR("%s: error reading metadata file %s: %s\n", __func__, metadata_path.c_str(), e.what());
+ std::string header(buffer, n_items);
+ std::smatch match;
+ if (std::regex_match(header, match, header_regex)) {
+ const std::string & key = match[1];
+ const std::string & value = match[2];
+ if (std::regex_match(key, match, etag_regex)) {
+ headers->etag = value;
+ } else if (std::regex_match(key, match, last_modified_regex)) {
+ headers->last_modified = value;
}
}
- // if we cannot open the metadata file, we assume that the downloaded file is not valid (etag and last-modified are left empty, so we will download it again)
- } else {
- LOG_INF("%s: no previous model file found %s\n", __func__, path.c_str());
- }
-
- // Send a HEAD request to retrieve the etag and last-modified headers
- struct common_load_model_from_url_headers {
- std::string etag;
- std::string last_modified;
+ return n_items;
};
- common_load_model_from_url_headers headers;
- bool head_request_ok = false;
- bool should_download = !file_exists; // by default, we should download if the file does not exist
+ curl_easy_setopt(curl.get(), CURLOPT_NOBODY, 1L); // will trigger the HEAD verb
+ curl_easy_setopt(curl.get(), CURLOPT_NOPROGRESS, 1L); // hide head request progress
+ curl_easy_setopt(curl.get(), CURLOPT_HEADERFUNCTION, static_cast(header_callback));
+ curl_easy_setopt(curl.get(), CURLOPT_HEADERDATA, &headers);
- // get ETag to see if the remote file has changed
- {
- typedef size_t(*CURLOPT_HEADERFUNCTION_PTR)(char *, size_t, size_t, void *);
- auto header_callback = [](char * buffer, size_t /*size*/, size_t n_items, void * userdata) -> size_t {
- common_load_model_from_url_headers * headers = (common_load_model_from_url_headers *) userdata;
+ // we only allow retrying once for HEAD requests
+ // this is for the use case of using running offline (no internet), retrying can be annoying
+ bool was_perform_successful = curl_perform_with_retry(url, curl.get(), 1, 0, "HEAD");
+ if (!was_perform_successful) {
+ head_request_ok = false;
+ }
- static std::regex header_regex("([^:]+): (.*)\r\n");
- static std::regex etag_regex("ETag", std::regex_constants::icase);
- static std::regex last_modified_regex("Last-Modified", std::regex_constants::icase);
-
- std::string header(buffer, n_items);
- std::smatch match;
- if (std::regex_match(header, match, header_regex)) {
- const std::string & key = match[1];
- const std::string & value = match[2];
- if (std::regex_match(key, match, etag_regex)) {
- headers->etag = value;
- } else if (std::regex_match(key, match, last_modified_regex)) {
- headers->last_modified = value;
- }
- }
- return n_items;
- };
-
- curl_easy_setopt(curl.get(), CURLOPT_NOBODY, 1L); // will trigger the HEAD verb
- curl_easy_setopt(curl.get(), CURLOPT_NOPROGRESS, 1L); // hide head request progress
- curl_easy_setopt(curl.get(), CURLOPT_HEADERFUNCTION, static_cast(header_callback));
- curl_easy_setopt(curl.get(), CURLOPT_HEADERDATA, &headers);
-
- // we only allow retrying once for HEAD requests
- // this is for the use case of using running offline (no internet), retrying can be annoying
- bool was_perform_successful = curl_perform_with_retry(url, curl.get(), 1, 0, "HEAD");
- if (!was_perform_successful) {
- head_request_ok = false;
- }
-
- long http_code = 0;
- curl_easy_getinfo(curl.get(), CURLINFO_RESPONSE_CODE, &http_code);
- if (http_code == 200) {
- head_request_ok = true;
- } else {
- LOG_WRN("%s: HEAD invalid http status code received: %ld\n", __func__, http_code);
- head_request_ok = false;
- }
+ long http_code = 0;
+ curl_easy_getinfo(curl.get(), CURLINFO_RESPONSE_CODE, &http_code);
+ if (http_code == 200) {
+ head_request_ok = true;
+ } else {
+ LOG_WRN("%s: HEAD invalid http status code received: %ld\n", __func__, http_code);
+ head_request_ok = false;
}
// if head_request_ok is false, we don't have the etag or last-modified headers
@@ -460,12 +467,12 @@ static bool common_download_file_single(const std::string & url, const std::stri
// download multiple files from remote URLs to local paths
// the input is a vector of pairs
-static bool common_download_file_multiple(const std::vector> & urls, const std::string & bearer_token) {
+static bool common_download_file_multiple(const std::vector> & urls, const std::string & bearer_token, bool offline) {
// Prepare download in parallel
std::vector> futures_download;
for (auto const & item : urls) {
- futures_download.push_back(std::async(std::launch::async, [bearer_token](const std::pair & it) -> bool {
- return common_download_file_single(it.first, it.second, bearer_token);
+ futures_download.push_back(std::async(std::launch::async, [bearer_token, offline](const std::pair & it) -> bool {
+ return common_download_file_single(it.first, it.second, bearer_token, offline);
}, item));
}
@@ -481,14 +488,15 @@ static bool common_download_file_multiple(const std::vector> common_remote_get_content(const std::string &
*
* Note: we use the Ollama-compatible HF API, but not using the blobId. Instead, we use the special "ggufFile" field which returns the value for "hf_file". This is done to be backward-compatible with existing cache files.
*/
-static struct common_hf_file_res common_get_hf_file(const std::string & hf_repo_with_tag, const std::string & bearer_token) {
+static struct common_hf_file_res common_get_hf_file(const std::string & hf_repo_with_tag, const std::string & bearer_token, bool offline) {
auto parts = string_split(hf_repo_with_tag, ':');
std::string tag = parts.size() > 1 ? parts.back() : "latest";
std::string hf_repo = parts[0];
@@ -638,20 +646,25 @@ static struct common_hf_file_res common_get_hf_file(const std::string & hf_repo_
long res_code = 0;
std::string res_str;
bool use_cache = false;
- try {
- auto res = common_remote_get_content(url, params);
- res_code = res.first;
- res_str = std::string(res.second.data(), res.second.size());
- } catch (const std::exception & e) {
- LOG_WRN("error: failed to get manifest: %s\n", e.what());
- LOG_WRN("try reading from cache\n");
- // try to read from cache
+ if (!offline) {
try {
+ auto res = common_remote_get_content(url, params);
+ res_code = res.first;
+ res_str = std::string(res.second.data(), res.second.size());
+ } catch (const std::exception & e) {
+ LOG_WRN("error: failed to get manifest at %s: %s\n", url.c_str(), e.what());
+ }
+ }
+ if (res_code == 0) {
+ if (std::filesystem::exists(cached_response_path)) {
+ LOG_WRN("trying to read manifest from cache: %s\n", cached_response_path.c_str());
res_str = read_file(cached_response_path);
res_code = 200;
use_cache = true;
- } catch (const std::exception & e) {
- throw std::runtime_error("error: failed to get manifest (check your internet connection)");
+ } else {
+ throw std::runtime_error(
+ offline ? "error: failed to get manifest (offline mode)"
+ : "error: failed to get manifest (check your internet connection)");
}
}
std::string ggufFile;
@@ -698,24 +711,25 @@ bool common_has_curl() {
return false;
}
-static bool common_download_file_single(const std::string &, const std::string &, const std::string &) {
+static bool common_download_file_single(const std::string &, const std::string &, const std::string &, bool) {
LOG_ERR("error: built without CURL, cannot download model from internet\n");
return false;
}
-static bool common_download_file_multiple(const std::vector> &, const std::string &) {
+static bool common_download_file_multiple(const std::vector> &, const std::string &, bool) {
LOG_ERR("error: built without CURL, cannot download model from the internet\n");
return false;
}
static bool common_download_model(
const common_params_model &,
- const std::string &) {
+ const std::string &,
+ bool) {
LOG_ERR("error: built without CURL, cannot download model from the internet\n");
return false;
}
-static struct common_hf_file_res common_get_hf_file(const std::string &, const std::string &) {
+static struct common_hf_file_res common_get_hf_file(const std::string &, const std::string &, bool) {
LOG_ERR("error: built without CURL, cannot download model from the internet\n");
return {};
}
@@ -742,7 +756,8 @@ struct handle_model_result {
static handle_model_result common_params_handle_model(
struct common_params_model & model,
const std::string & bearer_token,
- const std::string & model_path_default) {
+ const std::string & model_path_default,
+ bool offline) {
handle_model_result result;
// handle pre-fill default model path and url based on hf_repo and hf_file
{
@@ -750,7 +765,7 @@ static handle_model_result common_params_handle_model(
// short-hand to avoid specifying --hf-file -> default it to --model
if (model.hf_file.empty()) {
if (model.path.empty()) {
- auto auto_detected = common_get_hf_file(model.hf_repo, bearer_token);
+ auto auto_detected = common_get_hf_file(model.hf_repo, bearer_token, offline);
if (auto_detected.repo.empty() || auto_detected.ggufFile.empty()) {
exit(1); // built without CURL, error message already printed
}
@@ -791,7 +806,7 @@ static handle_model_result common_params_handle_model(
// then, download it if needed
if (!model.url.empty()) {
- bool ok = common_download_model(model, bearer_token);
+ bool ok = common_download_model(model, bearer_token, offline);
if (!ok) {
LOG_ERR("error: failed to download model from %s\n", model.url.c_str());
exit(1);
@@ -934,7 +949,7 @@ static bool common_params_parse_ex(int argc, char ** argv, common_params_context
// handle model and download
{
- auto res = common_params_handle_model(params.model, params.hf_token, DEFAULT_MODEL_PATH);
+ auto res = common_params_handle_model(params.model, params.hf_token, DEFAULT_MODEL_PATH, params.offline);
if (params.no_mmproj) {
params.mmproj = {};
} else if (res.found_mmproj && params.mmproj.path.empty() && params.mmproj.url.empty()) {
@@ -944,12 +959,12 @@ static bool common_params_parse_ex(int argc, char ** argv, common_params_context
// only download mmproj if the current example is using it
for (auto & ex : mmproj_examples) {
if (ctx_arg.ex == ex) {
- common_params_handle_model(params.mmproj, params.hf_token, "");
+ common_params_handle_model(params.mmproj, params.hf_token, "", params.offline);
break;
}
}
- common_params_handle_model(params.speculative.model, params.hf_token, "");
- common_params_handle_model(params.vocoder.model, params.hf_token, "");
+ common_params_handle_model(params.speculative.model, params.hf_token, "", params.offline);
+ common_params_handle_model(params.vocoder.model, params.hf_token, "", params.offline);
}
if (params.escape) {
@@ -973,10 +988,6 @@ static bool common_params_parse_ex(int argc, char ** argv, common_params_context
params.tensor_buft_overrides.push_back({nullptr, nullptr});
}
- if (params.reranking && params.embedding) {
- throw std::invalid_argument("error: either --embedding or --reranking can be specified, but not both");
- }
-
if (!params.chat_template.empty() && !common_chat_verify_template(params.chat_template, params.use_jinja)) {
throw std::runtime_error(string_format(
"error: the supplied chat template is not supported: %s%s\n",
@@ -1333,9 +1344,9 @@ common_params_context common_params_parser_init(common_params & params, llama_ex
));
add_opt(common_arg(
{"--prio"}, "N",
- string_format("set process/thread priority : 0-normal, 1-medium, 2-high, 3-realtime (default: %d)\n", params.cpuparams.priority),
+ string_format("set process/thread priority : low(-1), normal(0), medium(1), high(2), realtime(3) (default: %d)\n", params.cpuparams.priority),
[](common_params & params, int prio) {
- if (prio < 0 || prio > 3) {
+ if (prio < GGML_SCHED_PRIO_LOW || prio > GGML_SCHED_PRIO_REALTIME) {
throw std::invalid_argument("invalid value");
}
params.cpuparams.priority = (enum ggml_sched_priority) prio;
@@ -1445,6 +1456,14 @@ common_params_context common_params_parser_init(common_params & params, llama_ex
params.n_keep = value;
}
));
+ add_opt(common_arg(
+ {"--swa-full"},
+ string_format("use full-size SWA cache (default: %s)\n"
+ "[(more info)](https://github.com/ggml-org/llama.cpp/pull/13194#issuecomment-2868343055)", params.swa_full ? "true" : "false"),
+ [](common_params & params) {
+ params.swa_full = true;
+ }
+ ).set_env("LLAMA_ARG_SWA_FULL"));
add_opt(common_arg(
{"--no-context-shift"},
string_format("disables context shift on infinite text generation (default: %s)", params.ctx_shift ? "disabled" : "enabled"),
@@ -1670,7 +1689,7 @@ common_params_context common_params_parser_init(common_params & params, llama_ex
[](common_params & params) {
params.warmup = false;
}
- ).set_examples({LLAMA_EXAMPLE_MAIN, LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_EMBEDDING}));
+ ).set_examples({LLAMA_EXAMPLE_MAIN, LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_EMBEDDING, LLAMA_EXAMPLE_RETRIEVAL}));
add_opt(common_arg(
{"--spm-infill"},
string_format(
@@ -2057,13 +2076,6 @@ common_params_context common_params_parser_init(common_params & params, llama_ex
params.grp_attn_w = value;
}
).set_env("LLAMA_ARG_GRP_ATTN_W").set_examples({LLAMA_EXAMPLE_MAIN}));
- add_opt(common_arg(
- {"-dkvc", "--dump-kv-cache"},
- "verbose print of the KV cache",
- [](common_params & params) {
- params.dump_kv_cache = true;
- }
- ));
add_opt(common_arg(
{"-nkvo", "--no-kv-offload"},
"disable KV offload",
@@ -2204,39 +2216,40 @@ common_params_context common_params_parser_init(common_params & params, llama_ex
).set_examples({LLAMA_EXAMPLE_SERVER}).set_env("LLAMA_ARG_NO_CONT_BATCHING"));
add_opt(common_arg(
{"--mmproj"}, "FILE",
- "path to a multimodal projector file. see tools/mtmd/README.md",
+ "path to a multimodal projector file. see tools/mtmd/README.md\n"
+ "note: if -hf is used, this argument can be omitted",
[](common_params & params, const std::string & value) {
params.mmproj.path = value;
}
- ).set_examples(mmproj_examples));
+ ).set_examples(mmproj_examples).set_env("LLAMA_ARG_MMPROJ"));
add_opt(common_arg(
{"--mmproj-url"}, "URL",
"URL to a multimodal projector file. see tools/mtmd/README.md",
[](common_params & params, const std::string & value) {
params.mmproj.url = value;
}
- ).set_examples(mmproj_examples));
+ ).set_examples(mmproj_examples).set_env("LLAMA_ARG_MMPROJ_URL"));
add_opt(common_arg(
{"--no-mmproj"},
"explicitly disable multimodal projector, useful when using -hf",
[](common_params & params) {
params.no_mmproj = true;
}
- ).set_examples(mmproj_examples));
+ ).set_examples(mmproj_examples).set_env("LLAMA_ARG_NO_MMPROJ"));
add_opt(common_arg(
{"--no-mmproj-offload"},
"do not offload multimodal projector to GPU",
[](common_params & params) {
params.mmproj_use_gpu = false;
}
- ).set_examples(mmproj_examples));
+ ).set_examples(mmproj_examples).set_env("LLAMA_ARG_NO_MMPROJ_OFFLOAD"));
add_opt(common_arg(
- {"--image"}, "FILE",
- "path to an image file. use with multimodal models. Specify multiple times for batching",
+ {"--image", "--audio"}, "FILE",
+ "path to an image or audio file. use with multimodal models, can be repeated if you have multiple files\n",
[](common_params & params, const std::string & value) {
params.image.emplace_back(value);
}
- ).set_examples({LLAMA_EXAMPLE_LLAVA}));
+ ).set_examples({LLAMA_EXAMPLE_MTMD}));
if (llama_supports_rpc()) {
add_opt(common_arg(
{"--rpc"}, "SERVERS",
@@ -2436,6 +2449,13 @@ common_params_context common_params_parser_init(common_params & params, llama_ex
}
}
));
+ add_opt(common_arg(
+ {"--no-op-offload"},
+ string_format("disable offloading host tensor operations to device (default: %s)", params.no_op_offload ? "true" : "false"),
+ [](common_params & params) {
+ params.no_op_offload = true;
+ }
+ ));
add_opt(common_arg(
{"--lora"}, "FNAME",
"path to LoRA adapter (can be repeated to use multiple adapters)",
@@ -2577,7 +2597,7 @@ common_params_context common_params_parser_init(common_params & params, llama_ex
[](common_params & params, int value) {
params.n_junk = value;
}
- ).set_examples({LLAMA_EXAMPLE_PASSKEY}));
+ ).set_examples({LLAMA_EXAMPLE_PASSKEY, LLAMA_EXAMPLE_PARALLEL}));
add_opt(common_arg(
{"--pos"}, "N",
string_format("position of the passkey in the junk text (default: %d)", params.i_pos),
@@ -2640,7 +2660,7 @@ common_params_context common_params_parser_init(common_params & params, llama_ex
[](common_params & params) {
params.is_pp_shared = true;
}
- ).set_examples({LLAMA_EXAMPLE_BENCH}));
+ ).set_examples({LLAMA_EXAMPLE_BENCH, LLAMA_EXAMPLE_PARALLEL}));
add_opt(common_arg(
{"-npp"}, "n0,n1,...",
"number of prompt tokens",
@@ -2686,6 +2706,13 @@ common_params_context common_params_parser_init(common_params & params, llama_ex
params.embd_sep = value;
}
).set_examples({LLAMA_EXAMPLE_EMBEDDING}));
+ add_opt(common_arg(
+ {"--cls-separator"}, "STRING",
+ "separator of classification sequences (default \\t) for example \"<#seq#>\"",
+ [](common_params & params, const std::string & value) {
+ params.cls_sep = value;
+ }
+ ).set_examples({LLAMA_EXAMPLE_EMBEDDING}));
add_opt(common_arg(
{"--host"}, "HOST",
string_format("ip address to listen, or bind to an UNIX socket if the address ends with .sock (default: %s)", params.hostname.c_str()),
@@ -2723,9 +2750,10 @@ common_params_context common_params_parser_init(common_params & params, llama_ex
).set_examples({LLAMA_EXAMPLE_SERVER}).set_env("LLAMA_ARG_EMBEDDINGS"));
add_opt(common_arg(
{"--reranking", "--rerank"},
- string_format("enable reranking endpoint on server (default: %s)", params.reranking ? "enabled" : "disabled"),
+ string_format("enable reranking endpoint on server (default: %s)", "disabled"),
[](common_params & params) {
- params.reranking = true;
+ params.embedding = true;
+ params.pooling_type = LLAMA_POOLING_TYPE_RANK;
}
).set_examples({LLAMA_EXAMPLE_SERVER}).set_env("LLAMA_ARG_RERANKING"));
add_opt(common_arg(
@@ -2839,15 +2867,25 @@ common_params_context common_params_parser_init(common_params & params, llama_ex
).set_examples({LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_MAIN}).set_env("LLAMA_ARG_JINJA"));
add_opt(common_arg(
{"--reasoning-format"}, "FORMAT",
- "reasoning format (default: deepseek; allowed values: deepseek, none)\n"
- "controls whether thought tags are extracted from the response, and in which format they're returned. 'none' leaves thoughts unparsed in `message.content`, 'deepseek' puts them in `message.reasoning_content` (for DeepSeek R1 & Command R7B only).\n"
- "only supported for non-streamed responses",
+ "controls whether thought tags are allowed and/or extracted from the response, and in which format they're returned; one of:\n"
+ "- none: leaves thoughts unparsed in `message.content`\n"
+ "- deepseek: puts thoughts in `message.reasoning_content` (except in streaming mode, which behaves as `none`)\n"
+ "(default: deepseek)",
[](common_params & params, const std::string & value) {
/**/ if (value == "deepseek") { params.reasoning_format = COMMON_REASONING_FORMAT_DEEPSEEK; }
+ else if (value == "deepseek-legacy") { params.reasoning_format = COMMON_REASONING_FORMAT_DEEPSEEK_LEGACY; }
else if (value == "none") { params.reasoning_format = COMMON_REASONING_FORMAT_NONE; }
- else { std::invalid_argument("invalid value"); }
+ else { throw std::invalid_argument("invalid value"); }
}
).set_examples({LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_MAIN}).set_env("LLAMA_ARG_THINK"));
+ add_opt(common_arg(
+ {"--reasoning-budget"}, "N",
+ "controls the amount of thinking allowed; currently only one of: -1 for unrestricted thinking budget, or 0 to disable thinking (default: -1)",
+ [](common_params & params, int value) {
+ if (value != 0 && value != -1) { throw std::invalid_argument("invalid value"); }
+ params.reasoning_budget = value;
+ }
+ ).set_examples({LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_MAIN}).set_env("LLAMA_ARG_THINK_BUDGET"));
add_opt(common_arg(
{"--chat-template"}, "JINJA_TEMPLATE",
string_format(
@@ -2859,7 +2897,7 @@ common_params_context common_params_parser_init(common_params & params, llama_ex
[](common_params & params, const std::string & value) {
params.chat_template = value;
}
- ).set_examples({LLAMA_EXAMPLE_MAIN, LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_LLAVA}).set_env("LLAMA_ARG_CHAT_TEMPLATE"));
+ ).set_examples({LLAMA_EXAMPLE_MAIN, LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_MTMD}).set_env("LLAMA_ARG_CHAT_TEMPLATE"));
add_opt(common_arg(
{"--chat-template-file"}, "JINJA_TEMPLATE_FILE",
string_format(
@@ -2872,6 +2910,16 @@ common_params_context common_params_parser_init(common_params & params, llama_ex
params.chat_template = read_file(value);
}
).set_examples({LLAMA_EXAMPLE_MAIN, LLAMA_EXAMPLE_SERVER}).set_env("LLAMA_ARG_CHAT_TEMPLATE_FILE"));
+ add_opt(common_arg(
+ {"--no-prefill-assistant"},
+ string_format(
+ "whether to prefill the assistant's response if the last message is an assistant message (default: prefill enabled)\n"
+ "when this flag is set, if the last message is an assistant message then it will be treated as a full message and not prefilled\n"
+ ),
+ [](common_params & params) {
+ params.prefill_assistant = false;
+ }
+ ).set_examples({LLAMA_EXAMPLE_SERVER}).set_env("LLAMA_ARG_NO_PREFILL_ASSISTANT"));
add_opt(common_arg(
{"-sps", "--slot-prompt-similarity"}, "SIMILARITY",
string_format("how much the prompt of a request must match the prompt of a slot in order to use that slot (default: %.2f, 0.0 = disabled)\n", params.slot_prompt_similarity),
@@ -2936,7 +2984,7 @@ common_params_context common_params_parser_init(common_params & params, llama_ex
[](common_params & params, const std::string & value) {
/**/ if (value == "jsonl") { params.batched_bench_output_jsonl = true; }
else if (value == "md") { params.batched_bench_output_jsonl = false; }
- else { std::invalid_argument("invalid value"); }
+ else { throw std::invalid_argument("invalid value"); }
}
).set_examples({LLAMA_EXAMPLE_BENCH}));
add_opt(common_arg(
@@ -2968,6 +3016,13 @@ common_params_context common_params_parser_init(common_params & params, llama_ex
common_log_set_verbosity_thold(INT_MAX);
}
));
+ add_opt(common_arg(
+ {"--offline"},
+ "Offline mode: forces use of cache, prevents network access",
+ [](common_params & params) {
+ params.offline = true;
+ }
+ ).set_env("LLAMA_OFFLINE"));
add_opt(common_arg(
{"-lv", "--verbosity", "--log-verbosity"}, "N",
"Set the verbosity threshold. Messages with a higher verbosity will be ignored.",
@@ -3162,6 +3217,32 @@ common_params_context common_params_parser_init(common_params & params, llama_ex
params.speculative.model.path = value;
}
).set_examples({LLAMA_EXAMPLE_SPECULATIVE, LLAMA_EXAMPLE_SERVER}).set_env("LLAMA_ARG_MODEL_DRAFT"));
+ add_opt(common_arg(
+ {"-ctkd", "--cache-type-k-draft"}, "TYPE",
+ string_format(
+ "KV cache data type for K for the draft model\n"
+ "allowed values: %s\n"
+ "(default: %s)",
+ get_all_kv_cache_types().c_str(),
+ ggml_type_name(params.speculative.cache_type_k)
+ ),
+ [](common_params & params, const std::string & value) {
+ params.speculative.cache_type_k = kv_cache_type_from_str(value);
+ }
+ ).set_env("LLAMA_ARG_CACHE_TYPE_K_DRAFT"));
+ add_opt(common_arg(
+ {"-ctvd", "--cache-type-v-draft"}, "TYPE",
+ string_format(
+ "KV cache data type for V for the draft model\n"
+ "allowed values: %s\n"
+ "(default: %s)",
+ get_all_kv_cache_types().c_str(),
+ ggml_type_name(params.speculative.cache_type_v)
+ ),
+ [](common_params & params, const std::string & value) {
+ params.speculative.cache_type_v = kv_cache_type_from_str(value);
+ }
+ ).set_env("LLAMA_ARG_CACHE_TYPE_V_DRAFT"));
add_opt(common_arg(
{"-mv", "--model-vocoder"}, "FNAME",
diff --git a/common/build-info.cpp.in b/common/build-info.cpp.in
index 0b945aa68..aee9d7eaf 100644
--- a/common/build-info.cpp.in
+++ b/common/build-info.cpp.in
@@ -1,4 +1,4 @@
-int LLAMA_BUILD_NUMBER = @BUILD_NUMBER@;
-char const *LLAMA_COMMIT = "@BUILD_COMMIT@";
+int LLAMA_BUILD_NUMBER = @LLAMA_BUILD_NUMBER@;
+char const *LLAMA_COMMIT = "@LLAMA_BUILD_COMMIT@";
char const *LLAMA_COMPILER = "@BUILD_COMPILER@";
char const *LLAMA_BUILD_TARGET = "@BUILD_TARGET@";
diff --git a/common/chat-parser.cpp b/common/chat-parser.cpp
new file mode 100644
index 000000000..18a30e49a
--- /dev/null
+++ b/common/chat-parser.cpp
@@ -0,0 +1,385 @@
+#include "chat-parser.h"
+#include "common.h"
+#include "log.h"
+#include "regex-partial.h"
+
+#include
+#include
+#include
+#include
+
+using json = nlohmann::ordered_json;
+
+common_chat_msg_parser::common_chat_msg_parser(const std::string & input, bool is_partial, const common_chat_syntax & syntax)
+ : input_(input), is_partial_(is_partial), syntax_(syntax)
+{
+ result_.role = "assistant";
+
+ while (true) {
+ std::string id = std::to_string(std::rand());
+ if (input.find(id) == std::string::npos) {
+ healing_marker_ = id;
+ break;
+ }
+ }
+}
+
+std::string common_chat_msg_parser::str(const common_string_range & rng) const {
+ GGML_ASSERT(rng.begin <= rng.end);
+ return input_.substr(rng.begin, rng.end - rng.begin);
+}
+
+void common_chat_msg_parser::add_content(const std::string &content) {
+ result_.content += content;
+}
+
+void common_chat_msg_parser::add_reasoning_content(const std::string &reasoning_content) {
+ result_.reasoning_content += reasoning_content;
+}
+
+bool common_chat_msg_parser::add_tool_call(const std::string & name, const std::string & id, const std::string & arguments) {
+ if (name.empty()) {
+ return false;
+ }
+
+ common_chat_tool_call tool_call;
+ tool_call.name = name;
+ tool_call.arguments = arguments;
+ tool_call.id = id;
+
+ // LOG_DBG("Tool call arguments:\n\traw: %s\n\tresult: %s\n", arguments.c_str(), tool_call.arguments.c_str());
+ result_.tool_calls.emplace_back(tool_call);
+
+ return true;
+}
+bool common_chat_msg_parser::add_tool_call(const json & tool_call) {
+ std::string name = tool_call.contains("name") ? tool_call.at("name") : "";
+ std::string id = tool_call.contains("id") ? tool_call.at("id") : "";
+ std::string arguments = tool_call.contains("arguments") ? tool_call.at("arguments") : "";
+ return add_tool_call(name, id, arguments);
+}
+
+bool common_chat_msg_parser::add_tool_calls(const json & arr) {
+ for (const auto & item : arr) {
+ if (!add_tool_call(item)) {
+ return false;
+ }
+ }
+ return true;
+}
+void common_chat_msg_parser::finish() {
+ if (!is_partial_ && pos_ != input_.size()) {
+ throw std::runtime_error("Unexpected content at end of input");// + input_.substr(pos_));
+ }
+}
+
+bool common_chat_msg_parser::consume_spaces() {
+ const auto length = input_.size();
+ auto consumed = false;
+ while (pos_ < length && std::isspace(input_[pos_])) {
+ ++pos_;
+ consumed = true;
+ }
+ return consumed;
+}
+
+bool common_chat_msg_parser::try_consume_literal(const std::string & literal) {
+ auto pos = pos_;
+ for (auto i = 0u; i < literal.size(); ++i) {
+ if (pos >= input_.size()) {
+ return false;
+ }
+ if (input_[pos] != literal[i]) {
+ return false;
+ }
+ ++pos;
+ }
+ pos_ = pos;
+ return true;
+}
+
+std::optional common_chat_msg_parser::try_find_literal(const std::string & literal) {
+ auto idx = input_.find(literal, pos_);
+ if (idx != std::string::npos) {
+ find_regex_result res;
+ res.prelude = input_.substr(pos_, idx - pos_);
+ auto end = idx + literal.size();
+ res.groups.emplace_back(common_string_range{idx, end});
+ move_to(end);
+ return res;
+ }
+ if (is_partial_) {
+ idx = string_find_partial_stop(input_, literal);
+ if (idx != std::string::npos && idx >= pos_) {
+ find_regex_result res;
+ res.prelude = input_.substr(pos_, idx - pos_);
+ auto end = input_.size();
+ res.groups.emplace_back(common_string_range{idx, end});
+ move_to(end);
+ return res;
+ }
+ }
+ return std::nullopt;
+}
+
+void common_chat_msg_parser::consume_literal(const std::string & literal) {
+ if (!try_consume_literal(literal)) {
+ throw common_chat_msg_partial_exception(literal);
+ }
+}
+
+bool common_chat_msg_parser::try_parse_reasoning(const std::string & start_think, const std::string & end_think) {
+ auto handle_reasoning = [&](const std::string & reasoning, bool closed) {
+ auto stripped_reasoning = string_strip(reasoning);
+ if (stripped_reasoning.empty()) {
+ return;
+ }
+ if (syntax_.reasoning_in_content) {
+ add_content(syntax_.reasoning_format == COMMON_REASONING_FORMAT_DEEPSEEK ? "" : start_think);
+ add_content(stripped_reasoning);
+ if (closed) {
+ add_content(syntax_.reasoning_format == COMMON_REASONING_FORMAT_DEEPSEEK ? "" : end_think);
+ }
+ } else {
+ add_reasoning_content(stripped_reasoning);
+ }
+ };
+ if (syntax_.reasoning_format != COMMON_REASONING_FORMAT_NONE) {
+ if (syntax_.thinking_forced_open || try_consume_literal(start_think)) {
+ if (auto res = try_find_literal(end_think)) {
+ handle_reasoning(res->prelude, /* closed */ true);
+ consume_spaces();
+ return true;
+ }
+ auto rest = consume_rest();
+ if (!rest.empty()) {
+ handle_reasoning(rest, /* closed */ !is_partial());
+ }
+ // Allow unclosed thinking tags, for now (https://github.com/ggml-org/llama.cpp/issues/13812, https://github.com/ggml-org/llama.cpp/issues/13877)
+ // if (!syntax_.thinking_forced_open) {
+ // throw common_chat_msg_partial_exception(end_think);
+ // }
+ return true;
+ }
+ }
+ return false;
+}
+
+std::string common_chat_msg_parser::consume_rest() {
+ auto rest = input_.substr(pos_);
+ pos_ = input_.size();
+ return rest;
+}
+
+// Tries to find the regex, consumes it (pos right after it) and gives the prelude (right before it) and the groups to the callback.
+std::optional common_chat_msg_parser::try_find_regex(const common_regex & regex, size_t from, bool add_prelude_to_content) {
+ auto m = regex.search(input_, from == std::string::npos ? pos_ : from);
+ if (m.type == COMMON_REGEX_MATCH_TYPE_NONE) {
+ return std::nullopt;
+ }
+ auto prelude = input_.substr(pos_, m.groups[0].begin - pos_);
+ pos_ = m.groups[0].end;
+
+ if (add_prelude_to_content) {
+ add_content(prelude);
+ }
+ if (m.type == COMMON_REGEX_MATCH_TYPE_PARTIAL) {
+ if (is_partial()) {
+ throw common_chat_msg_partial_exception(regex.str());
+ }
+ return std::nullopt;
+ }
+ return find_regex_result{prelude, m.groups};
+}
+
+common_chat_msg_parser::find_regex_result common_chat_msg_parser::consume_regex(const common_regex & regex) {
+ if (auto result = try_consume_regex(regex)) {
+ return *result;
+ }
+ throw common_chat_msg_partial_exception(regex.str());
+}
+
+std::optional common_chat_msg_parser::try_consume_regex(const common_regex & regex) {
+ auto m = regex.search(input_, pos_);
+ if (m.type == COMMON_REGEX_MATCH_TYPE_NONE) {
+ return std::nullopt;
+ }
+ if (m.type == COMMON_REGEX_MATCH_TYPE_PARTIAL) {
+ if (is_partial()) {
+ throw common_chat_msg_partial_exception(regex.str());
+ }
+ return std::nullopt;
+ }
+ if (m.groups[0].begin != pos_) {
+ // Didn't match at the current position.
+ return std::nullopt;
+ }
+ pos_ = m.groups[0].end;
+
+ return find_regex_result {
+ /* .prelude = */ "",
+ m.groups,
+ };
+}
+
+std::optional common_chat_msg_parser::try_consume_json() {
+ auto it = input_.cbegin() + pos_;
+ const auto end = input_.cend();
+ common_json result;
+ if (!common_json_parse(it, end, healing_marker_, result)) {
+ return std::nullopt;
+ }
+ pos_ = std::distance(input_.cbegin(), it);
+ if (result.healing_marker.marker.empty()) {
+ // No healing marker, just return the parsed json
+ return result;
+ }
+ if (!is_partial()) {
+ throw common_chat_msg_partial_exception("JSON");
+ }
+ return result;
+}
+
+common_json common_chat_msg_parser::consume_json() {
+ if (auto result = try_consume_json()) {
+ return *result;
+ }
+ throw common_chat_msg_partial_exception("JSON");
+}
+
+common_chat_msg_parser::consume_json_result common_chat_msg_parser::consume_json_with_dumped_args(
+ const std::vector> & args_paths,
+ const std::vector> & content_paths
+) {
+ if (auto result = try_consume_json_with_dumped_args(args_paths, content_paths)) {
+ return *result;
+ }
+ throw common_chat_msg_partial_exception("JSON");
+}
+
+std::optional common_chat_msg_parser::try_consume_json_with_dumped_args(
+ const std::vector> & args_paths,
+ const std::vector> & content_paths
+) {
+ auto partial = try_consume_json();
+ if (!partial) {
+ return std::nullopt;
+ }
+ auto is_arguments_path = [&](const std::vector & path) {
+ return std::find(args_paths.begin(), args_paths.end(), path) != args_paths.end();
+ };
+ auto is_content_path = [&](const std::vector & path) {
+ return std::find(content_paths.begin(), content_paths.end(), path) != content_paths.end();
+ };
+
+ if (partial->healing_marker.marker.empty()) {
+ if (args_paths.empty()) {
+ // No arguments to dump, and JSON was parsed fully.
+ return consume_json_result {
+ partial->json,
+ /* .is_partial = */ false,
+ };
+ }
+ if (is_arguments_path({})) {
+ // Entire JSON is the arguments and was parsed fully.
+ return consume_json_result {
+ partial->json.dump(),
+ /* .is_partial = */ false,
+ };
+ }
+ }
+
+ LOG_DBG("Parsed partial JSON: %s (json_healing_marker: %s)\n", partial->json.dump().c_str(), partial->healing_marker.json_dump_marker.c_str());
+
+ auto found_healing_marker = false;
+ std::vector path;
+ std::function remove_unsupported_healings_and_dump_args = [&](const json & j) -> json {
+ if (is_arguments_path(path)) {
+ auto arguments = j.dump();
+ if (is_partial() && !partial->healing_marker.marker.empty()) {
+ auto idx = arguments.find(partial->healing_marker.json_dump_marker);
+ if (idx != std::string::npos) {
+ arguments.resize(idx);
+ found_healing_marker = true;
+ }
+ if (arguments == "\"") {
+ // This happens because of completing `:"$magic` after `"arguments"`
+ arguments = "";
+ }
+ }
+ return arguments;
+ }
+ if (is_content_path(path)) {
+ if (!j.is_string()) {
+ throw std::runtime_error("Content path must be a string");
+ }
+ std::string str = j;
+ auto idx = str.find(partial->healing_marker.marker); // not using json_dump_marker as we're inside a string
+ if (idx != std::string::npos) {
+ str.resize(idx);
+ found_healing_marker = true;
+ }
+ return str;
+ }
+ if (j.is_object()) {
+ auto obj = json::object();
+ for (const auto & p : j.items()) {
+ const auto & key = p.key();
+ const auto & value = p.value();
+ const std::string key_str = key; // NOLINT
+ auto idx = key_str.find(healing_marker_);
+ if (idx != std::string::npos) {
+ found_healing_marker = true;
+ break;
+ }
+ path.push_back(key_str);
+ if (value.is_string()) {
+ const std::string value_str = value;
+ if (value_str.find(healing_marker_) != std::string::npos) {
+ found_healing_marker = true;
+ if (is_content_path(path)) {
+ if (partial->healing_marker.marker == partial->healing_marker.json_dump_marker) {
+ // The healing occurred inside the string: good. Otherwise we just ditch the entire key/value pair.
+ obj[key] = remove_unsupported_healings_and_dump_args(value);
+ }
+ }
+ break;
+ }
+ obj[key] = value;
+ } else {
+ obj[key] = remove_unsupported_healings_and_dump_args(value);
+ }
+ path.pop_back();
+ }
+ return obj;
+ }
+ if (j.is_array()) {
+ auto arr = json::array();
+ for (const auto & value : j) {
+ if (value.is_string()) {
+ std::string str = value;
+ auto idx = str.find(healing_marker_);
+ if (idx != std::string::npos) {
+ // Don't heal array values that aren't in the arguments.
+ found_healing_marker = true;
+ break;
+ }
+ }
+ arr.push_back(remove_unsupported_healings_and_dump_args(value));
+ }
+ return arr;
+ }
+ return j;
+ };
+
+ auto cleaned = remove_unsupported_healings_and_dump_args(partial->json);
+ LOG_DBG("Cleaned up JSON %s to %s (json_healing_marker : '%s')\n", partial->json.dump().c_str(), cleaned.dump().c_str(), partial->healing_marker.json_dump_marker.c_str());
+ return consume_json_result {
+ cleaned,
+ /* .is_partial = */ found_healing_marker,
+ };
+}
+
+void common_chat_msg_parser::clear_tools() {
+ result_.tool_calls.clear();
+}
diff --git a/common/chat-parser.h b/common/chat-parser.h
new file mode 100644
index 000000000..0e64c341a
--- /dev/null
+++ b/common/chat-parser.h
@@ -0,0 +1,120 @@
+#pragma once
+
+#include "chat.h"
+#include "json-partial.h"
+#include "regex-partial.h"
+
+#include
+
+#include
+#include
+#include
+
+class common_chat_msg_partial_exception : public std::runtime_error {
+ public:
+ common_chat_msg_partial_exception(const std::string & message) : std::runtime_error(message) {}
+};
+
+class common_chat_msg_parser {
+ std::string input_;
+ bool is_partial_;
+ common_chat_syntax syntax_;
+ std::string healing_marker_;
+
+ size_t pos_ = 0;
+ common_chat_msg result_;
+
+ public:
+ common_chat_msg_parser(const std::string & input, bool is_partial, const common_chat_syntax & syntax);
+ const std::string & input() const { return input_; }
+ size_t pos() const { return pos_; }
+ const std::string & healing_marker() const { return healing_marker_; }
+ const bool & is_partial() const { return is_partial_; }
+ const common_chat_msg & result() const { return result_; }
+ const common_chat_syntax & syntax() const { return syntax_; }
+
+ void move_to(size_t pos) {
+ if (pos > input_.size()) {
+ throw std::runtime_error("Invalid position!");
+ }
+ pos_ = pos;
+ }
+ void move_back(size_t n) {
+ if (pos_ < n) {
+ throw std::runtime_error("Can't move back that far!");
+ }
+ pos_ -= n;
+ }
+
+ // Get the substring of the input at the given range
+ std::string str(const common_string_range & rng) const;
+
+ // Appends to the result.content field
+ void add_content(const std::string & content);
+
+ // Appends to the result.reasoning_content field
+ void add_reasoning_content(const std::string & reasoning_content);
+
+ // Adds a tool call to the result. If the tool call is too incomplete (e.g. name empty), it won't add anything.
+ bool add_tool_call(const std::string & name, const std::string & id, const std::string & arguments);
+
+ // Adds a tool call using the "name", "id" and "arguments" fields of the json object
+ bool add_tool_call(const nlohmann::ordered_json & tool_call);
+
+ // Adds an array of tool calls using their "name", "id" and "arguments" fields.
+ bool add_tool_calls(const nlohmann::ordered_json & arr);
+
+ void finish();
+
+ bool consume_spaces();
+
+ void consume_literal(const std::string & literal);
+
+ bool try_parse_reasoning(const std::string & start_think, const std::string & end_think);
+
+ std::string consume_rest();
+
+ struct find_regex_result {
+ std::string prelude;
+ std::vector groups;
+ };
+
+ std::optional try_find_regex(const common_regex & regex, size_t from = std::string::npos, bool add_prelude_to_content = true);
+
+ bool try_consume_literal(const std::string & literal);
+
+ std::optional try_find_literal(const std::string & literal);
+
+ find_regex_result consume_regex(const common_regex & regex);
+
+ std::optional try_consume_regex(const common_regex & regex);
+
+ std::optional try_consume_json();
+ common_json consume_json();
+
+ struct consume_json_result {
+ nlohmann::ordered_json value;
+ bool is_partial;
+ };
+
+ /*
+ Consume (possibly partial) json and converts specific subtrees to (possibly truncated) JSON strings.
+
+ By default, object keys can't be truncated, nor can string values (their corresponding key is removed,
+ e.g. `{"foo": "bar", "baz": "b` -> `{"foo": "bar"}`
+
+ But one can allow subpaths to be kept truncated, and possibly json-dumped to truncated json strings
+ - with `content_paths={{"foo"}}` -> `{"foo": "b` -> {"foo": "b"}`
+ - with `args_paths={{"foo"}}` -> `{"foo": {"b` -> `{"foo": "{b"}`
+ */
+ consume_json_result consume_json_with_dumped_args(
+ const std::vector> & args_paths = {},
+ const std::vector> & content_paths = {}
+ );
+ std::optional try_consume_json_with_dumped_args(
+ const std::vector> & args_paths = {},
+ const std::vector> & content_paths = {}
+ );
+
+ void clear_tools();
+};
diff --git a/common/chat.cpp b/common/chat.cpp
index ad3d4aa99..7d9aaeb12 100644
--- a/common/chat.cpp
+++ b/common/chat.cpp
@@ -1,10 +1,125 @@
#include "chat.h"
+#include "chat-parser.h"
+#include "common.h"
+#include "json-partial.h"
#include "json-schema-to-grammar.h"
#include "log.h"
-#include "minja/chat-template.hpp"
-#include "minja/minja.hpp"
+#include "regex-partial.h"
+#include
+#include
+
+#include
+#include
+#include
#include
+#include
+#include
+#include
+
+static std::string format_time(const std::chrono::system_clock::time_point & now, const std::string & format) {
+ auto time = std::chrono::system_clock::to_time_t(now);
+ auto local_time = *std::localtime(&time);
+ std::ostringstream ss;
+ ss << std::put_time(&local_time, format.c_str());
+ auto res = ss.str();
+ return res;
+}
+
+static std::string string_diff(const std::string & last, const std::string & current) {
+ if (last.empty()) {
+ return current;
+ }
+ if (!string_starts_with(current, last)) {
+ if (string_starts_with(last, current)) {
+ // This happens if the last generation ended on a partial stop word (not erased),
+ // and the current ended on a stop word (erased).
+ return "";
+ }
+ throw std::runtime_error("Invalid diff: '" + last + "' not found at start of '" + current + "'");
+ }
+ return current.substr(last.size());
+}
+
+static bool has_content_or_tool_calls(const common_chat_msg & msg) {
+ return !msg.content.empty() || !msg.tool_calls.empty();
+}
+
+template <>
+json common_chat_msg::to_json_oaicompat() const
+{
+ json message {
+ {"role", "assistant"},
+ };
+ if (!reasoning_content.empty()) {
+ message["reasoning_content"] = reasoning_content;
+ }
+ if (content.empty() && !tool_calls.empty()) {
+ message["content"] = json();
+ } else {
+ message["content"] = content;
+ }
+ if (!tool_calls.empty()) {
+ auto arr = json::array();
+ for (const auto & tc : tool_calls) {
+ arr.push_back({
+ {"type", "function"},
+ {"function", {
+ {"name", tc.name},
+ {"arguments", tc.arguments},
+ }},
+ {"id", tc.id},
+ // // Some templates generate and require an id (sometimes in a very specific format, e.g. Mistral Nemo).
+ // // We only generate a random id for the ones that don't generate one by themselves
+ // // (they also won't get to see it as their template likely doesn't use it, so it's all for the client)
+ // {"id", tc.id.empty() ? gen_tool_call_id() : tc.id},
+ });
+ }
+ message["tool_calls"] = arr;
+ }
+ return message;
+}
+
+std::vector common_chat_msg_diff::compute_diffs(const common_chat_msg & previous_msg, const common_chat_msg & new_msg) {
+ std::vector diffs;
+ if (previous_msg.reasoning_content != new_msg.reasoning_content) {
+ auto & diff = diffs.emplace_back();
+ diff.reasoning_content_delta = string_diff(previous_msg.reasoning_content, new_msg.reasoning_content);
+ }
+ if (previous_msg.content != new_msg.content) {
+ auto & diff = diffs.emplace_back();
+ diff.content_delta = string_diff(previous_msg.content, new_msg.content);
+ }
+
+ if (new_msg.tool_calls.size() < previous_msg.tool_calls.size()) {
+ throw std::runtime_error("Invalid diff: now finding less tool calls!");
+ }
+
+ if (!previous_msg.tool_calls.empty()) {
+ auto idx = previous_msg.tool_calls.size() - 1;
+ const auto & pref = previous_msg.tool_calls[idx];
+ const auto & newf = new_msg.tool_calls[idx];
+ if (pref.name != newf.name) {
+ throw std::runtime_error("Invalid diff: tool call mismatch!");
+ }
+ auto args_diff = string_diff(pref.arguments, newf.arguments);
+ if (!args_diff.empty() || pref.id != newf.id) {
+ auto & diff = diffs.emplace_back();
+ diff.tool_call_index = idx;
+ if (pref.id != newf.id) {
+ diff.tool_call_delta.id = newf.id;
+ diff.tool_call_delta.name = newf.name;
+ }
+ diff.tool_call_delta.arguments = args_diff;
+ }
+ }
+ for (size_t idx = previous_msg.tool_calls.size(); idx < new_msg.tool_calls.size(); ++idx) {
+ auto & diff = diffs.emplace_back();
+ diff.tool_call_index = idx;
+ diff.tool_call_delta = new_msg.tool_calls[idx];
+ }
+ return diffs;
+}
typedef minja::chat_template common_chat_template;
@@ -23,7 +138,8 @@ struct templates_params {
bool stream;
std::string grammar;
bool add_generation_prompt = true;
- bool extract_reasoning = true;
+ bool enable_thinking = true;
+ std::chrono::system_clock::time_point now = std::chrono::system_clock::now();
};
common_chat_tool_choice common_chat_tool_choice_parse_oaicompat(const std::string & tool_choice) {
@@ -267,6 +383,32 @@ json common_chat_tools_to_json_oaicompat(const std::vector & t
return result;
}
+template <> json common_chat_msg_diff_to_json_oaicompat(const common_chat_msg_diff & diff) {
+ json delta = json::object();
+ if (!diff.reasoning_content_delta.empty()) {
+ delta["reasoning_content"] = diff.reasoning_content_delta;
+ }
+ if (!diff.content_delta.empty()) {
+ delta["content"] = diff.content_delta;
+ }
+ if (diff.tool_call_index != std::string::npos) {
+ json tool_call;
+ tool_call["index"] = diff.tool_call_index;
+ if (!diff.tool_call_delta.id.empty()) {
+ tool_call["id"] = diff.tool_call_delta.id;
+ tool_call["type"] = "function";
+ }
+ json function = json::object();
+ if (!diff.tool_call_delta.name.empty()) {
+ function["name"] = diff.tool_call_delta.name;
+ }
+ function["arguments"] = diff.tool_call_delta.arguments;
+ tool_call["function"] = function;
+ delta["tool_calls"] = json::array({tool_call});
+ }
+ return delta;
+}
+
bool common_chat_verify_template(const std::string & tmpl, bool use_jinja) {
if (use_jinja) {
try {
@@ -434,7 +576,7 @@ common_chat_templates_ptr common_chat_templates_init(
return tmpls;
}
-std::string common_chat_format_name(common_chat_format format) {
+const char * common_chat_format_name(common_chat_format format) {
switch (format) {
case COMMON_CHAT_FORMAT_CONTENT_ONLY: return "Content-only";
case COMMON_CHAT_FORMAT_GENERIC: return "Generic";
@@ -442,182 +584,128 @@ std::string common_chat_format_name(common_chat_format format) {
case COMMON_CHAT_FORMAT_LLAMA_3_X: return "Llama 3.x";
case COMMON_CHAT_FORMAT_LLAMA_3_X_WITH_BUILTIN_TOOLS: return "Llama 3.x with builtin tools";
case COMMON_CHAT_FORMAT_DEEPSEEK_R1: return "DeepSeek R1";
- case COMMON_CHAT_FORMAT_DEEPSEEK_R1_EXTRACT_REASONING: return "DeepSeek R1 (extract reasoning)";
case COMMON_CHAT_FORMAT_FIREFUNCTION_V2: return "FireFunction v2";
case COMMON_CHAT_FORMAT_FUNCTIONARY_V3_2: return "Functionary v3.2";
case COMMON_CHAT_FORMAT_FUNCTIONARY_V3_1_LLAMA_3_1: return "Functionary v3.1 Llama 3.1";
case COMMON_CHAT_FORMAT_HERMES_2_PRO: return "Hermes 2 Pro";
- case COMMON_CHAT_FORMAT_HERMES_2_PRO_EXTRACT_REASONING: return "Hermes 2 Pro (extract reasoning)";
case COMMON_CHAT_FORMAT_COMMAND_R7B: return "Command R7B";
- case COMMON_CHAT_FORMAT_COMMAND_R7B_EXTRACT_REASONING: return "Command R7B (extract reasoning)";
default:
throw std::runtime_error("Unknown chat format");
}
}
-static bool parse_json(std::string::const_iterator & it, const std::string::const_iterator & end, json & out) {
- // // https://json.nlohmann.me/features/parsing/sax_interface/
- struct json_error_locator : public nlohmann::json_sax {
- std::size_t position;
- bool found_error;
+const char * common_reasoning_format_name(common_reasoning_format format) {
+ switch (format) {
+ case COMMON_REASONING_FORMAT_NONE: return "none";
+ case COMMON_REASONING_FORMAT_DEEPSEEK: return "deepseek";
+ case COMMON_REASONING_FORMAT_DEEPSEEK_LEGACY: return "deepseek-legacy";
+ default:
+ throw std::runtime_error("Unknown reasoning format");
+ }
+}
- json_error_locator() : position(0), found_error(false) {}
-
- bool parse_error(std::size_t position, const std::string &, const json::exception &) override { // NOLINT
- this->position = position - 1;
- this->found_error = true;
- return false;
+static std::string wrap_code_as_arguments(common_chat_msg_parser & builder, const std::string & code) {
+ std::string arguments;
+ if (builder.is_partial()) {
+ arguments = (json {{"code", code + builder.healing_marker()}}).dump();
+ auto idx = arguments.find(builder.healing_marker());
+ if (idx != std::string::npos) {
+ arguments.resize(idx);
}
- bool null() override { return true; } // NOLINT
- bool boolean(bool) override { return true; } // NOLINT
- bool number_integer(number_integer_t) override { return true; } // NOLINT
- bool number_unsigned(number_unsigned_t) override { return true; } // NOLINT
- bool number_float(number_float_t, const string_t &) override { return true; } // NOLINT
- bool string(string_t &) override { return true; } // NOLINT
- bool binary(binary_t &) override { return true; } // NOLINT
- bool start_object(std::size_t) override { return true; } // NOLINT
- bool key(string_t &) override { return true; } // NOLINT
- bool end_object() override { return true; }
- bool start_array(std::size_t) override { return true; } // NOLINT
- bool end_array() override { return true; }
- };
- json_error_locator err_loc;
- json::sax_parse(it, end, &err_loc);
-
- std::string::const_iterator temptative_end;
- if (err_loc.found_error) {
- temptative_end = it + err_loc.position;
} else {
- temptative_end = end;
- }
- std::string json_sub {it, temptative_end};
- try {
- out = json::parse(json_sub);
- it = temptative_end;
- return true;
- } catch (const std::exception &) {
- return false;
- }
-}
-
-static bool parse_literal(std::string::const_iterator & it, const std::string::const_iterator & end, const std::string & expected) {
- auto expected_it = expected.begin();
- auto tmp_it = it;
- while (tmp_it != end && expected_it != expected.end() && *tmp_it == *expected_it) {
- ++tmp_it;
- ++expected_it;
- }
- if (expected_it == expected.end()) {
- it = tmp_it;
- return true;
- }
- return false;
-}
-
-static std::optional parse_pattern(std::string::const_iterator & it, const std::string::const_iterator & end, const std::regex & expected) {
- std::smatch match;
- if (std::regex_match(it, end, match, expected)) {
- it = match.suffix().first;
- return match;
- }
- return std::nullopt;
-}
-
-static void consume_spaces(std::string::const_iterator & it, const std::string::const_iterator & end) {
- while (it != end && std::isspace(*it)) {
- ++it;
+ arguments = (json {{"code", code}}).dump();
}
+ return arguments;
}
/**
* Takes a prefix regex that must have 1 group to capture the function name, a closing suffix, and expects json parameters in between.
* Aggregates the prefix, suffix and in-between text into the content.
*/
-static common_chat_msg parse_json_tool_calls(
- const std::string& input,
- const std::optional & trigger_opt,
- const std::regex & function_regex,
- const std::regex & close_regex,
- bool allow_raw_python = false) {
- std::smatch match;
+static void parse_json_tool_calls(
+ common_chat_msg_parser & builder,
+ const std::optional & block_open,
+ const std::optional & function_regex_start_only,
+ const std::optional & function_regex,
+ const common_regex & close_regex,
+ const std::optional & block_close,
+ bool allow_raw_python = false,
+ const std::function & get_function_name = nullptr) {
- common_chat_msg result;
- result.role = "assistant";
+ auto parse_tool_calls = [&]() {
+ size_t from = std::string::npos;
+ auto first = true;
+ while (true) {
+ auto res = function_regex_start_only && first
+ ? builder.try_consume_regex(*function_regex_start_only)
+ : function_regex
+ ? builder.try_find_regex(*function_regex, from)
+ : std::nullopt;
+ if (res) {
+ std::string name;
+ if (get_function_name) {
+ name = get_function_name(*res);
+ } else {
+ GGML_ASSERT(res->groups.size() == 2);
+ name = builder.str(res->groups[1]);
+ }
+ first = false;
+ if (name.empty()) {
+ // get_function_name signalled us that we should skip this match and treat it as content.
+ from = res->groups[0].begin + 1;
+ continue;
+ }
+ from = std::string::npos;
-
- auto end = input.end();
- auto it = input.begin();
-
- if (trigger_opt) {
- if (!std::regex_search(it, end, match, *trigger_opt)) {
- result.content = input;
- return result;
- }
- result.content = match.prefix().str();
- it = match.suffix().first;
- }
-
- while (it != end) {
- std::sregex_iterator rend;
- std::sregex_iterator rit(it, end, function_regex);
- if (rit == rend) {
- result.content += std::string(it, end);
+ auto maybe_raw_python = name == "python" && allow_raw_python;
+ if (builder.input()[builder.pos()] == '{' || !maybe_raw_python) {
+ if (auto arguments = builder.try_consume_json_with_dumped_args({{}})) {
+ if (!builder.add_tool_call(name, "", arguments->value) || arguments->is_partial) {
+ throw common_chat_msg_partial_exception("incomplete tool call");
+ }
+ builder.consume_regex(close_regex);
+ }
+ continue;
+ }
+ if (maybe_raw_python) {
+ auto arguments = wrap_code_as_arguments(builder, builder.consume_rest());
+ if (!builder.add_tool_call(name, "", arguments)) {
+ throw common_chat_msg_partial_exception("incomplete tool call");
+ }
+ return;
+ }
+ throw common_chat_msg_partial_exception("incomplete tool call");
+ }
break;
}
- auto name = rit->str(1);
- result.content += std::string(it, rit->prefix().second);
- it = rit->suffix().first;
-
- json arguments;
- if (parse_json(it, end, arguments)) {
- if (!std::regex_search(it, end, match, close_regex)) {
- throw std::runtime_error("Malformed input, missing closing pattern: " + input);
- }
- it = match.suffix().first;
- result.tool_calls.push_back({name, arguments.is_string() ? arguments.get() : arguments.dump(), /* id= */ ""});
- } else {
- if (allow_raw_python && name == "python") {
- result.tool_calls.push_back({name, json({{"code", std::string(it, end)}}).dump(), /* id= */ ""});
- break;
- }
- throw std::runtime_error("Failed to parse json tool call arguments: " + input);
+ if (block_close) {
+ builder.consume_regex(*block_close);
}
- }
-
- if (!result.tool_calls.empty()) {
- if (!string_strip(result.content).empty()) {
- LOG_WRN("Content found with tool calls: %s\n", result.content.c_str());
- }
- result.content = "";
- }
- return result;
-}
-
-static common_chat_tool_call process_tool_call(const json & tool_call) {
- const auto & arguments = tool_call.at("arguments");
- return {
- /* .name = */ tool_call.at("name"),
- /* .arguments = */ arguments.is_string() ? arguments.get() : arguments.dump(),
- /* .id = */ tool_call.contains("id") ? tool_call.at("id") : "",
+ builder.consume_spaces();
+ builder.add_content(builder.consume_rest());
};
-}
-static common_chat_msg parse_prefixed_json_tool_call_array(const std::string& input, const std::string & prefix, size_t rstrip_prefix = 0) {
- auto content_end = input.find(prefix);
- size_t tc_start = std::string::npos;
-
- common_chat_msg result;
- result.role = "assistant";
- if (content_end == std::string::npos) {
- result.content = input;
- } else {
- tc_start = content_end + prefix.size() - rstrip_prefix;
- result.content = input.substr(0, content_end);
- auto tool_calls = json::parse(input.substr(tc_start));
- for (const auto & tool_call : tool_calls) {
- result.tool_calls.emplace_back(process_tool_call(tool_call));
+ if (block_open) {
+ if (auto res = builder.try_find_regex(*block_open)) {
+ parse_tool_calls();
+ } else {
+ builder.add_content(builder.consume_rest());
}
+ } else {
+ parse_tool_calls();
+ }
+}
+
+static void parse_prefixed_json_tool_call_array(common_chat_msg_parser & builder, const common_regex & prefix, size_t rstrip_prefix = 0) {
+ static const std::vector> args_paths = {{"arguments"}};
+ if (auto res = builder.try_find_regex(prefix)) {
+ builder.move_back(rstrip_prefix);
+ auto tool_calls = builder.consume_json_with_dumped_args(args_paths);
+ if (!builder.add_tool_calls(tool_calls.value) || tool_calls.is_partial) {
+ throw common_chat_msg_partial_exception("incomplete tool call array");
+ }
+ } else {
+ builder.add_content(builder.consume_rest());
}
- return result;
}
static void foreach_function(const json & tools, const std::function & fn) {
@@ -744,29 +832,36 @@ static common_chat_params common_chat_params_init_generic(const common_chat_temp
data.format = COMMON_CHAT_FORMAT_GENERIC;
return data;
}
-static common_chat_msg common_chat_parse_generic(const std::string & input) {
- json data = json::parse(input);
- common_chat_msg result;
- result.role = "assistant";
- if (data.contains("tool_calls")) {
- for (const auto & tool_call : data.at("tool_calls")) {
- result.tool_calls.push_back({
- tool_call.at("name"),
- tool_call.at("arguments").dump(),
- tool_call.contains("id") ? tool_call.at("id") : "",
- });
- }
- } else if (data.contains("tool_call")) {
- result.tool_calls.push_back({
- data.at("tool_call").at("name"),
- data.at("tool_call").at("arguments").dump(),
- /* id= */ "",
- });
- } else if (data.contains("response")) {
- const auto & response = data.at("response");
- result.content = response.is_string() ? response.get() : response.dump(2);
+static void common_chat_parse_generic(common_chat_msg_parser & builder) {
+ if (!builder.syntax().parse_tool_calls) {
+ builder.add_content(builder.consume_rest());
+ return;
+ }
+ static const std::vector> content_paths = {
+ {"response"},
+ };
+ static const std::vector> args_paths = {
+ {"tool_call", "arguments"},
+ {"tool_calls", "arguments"},
+ };
+ auto data = builder.consume_json_with_dumped_args(args_paths, content_paths);
+ if (data.value.contains("tool_calls")) {
+ if (!builder.add_tool_calls(data.value.at("tool_calls")) || data.is_partial) {
+ throw common_chat_msg_partial_exception("incomplete tool calls");
+ }
+ } else if (data.value.contains("tool_call")) {
+ if (!builder.add_tool_call(data.value.at("tool_call")) || data.is_partial) {
+ throw common_chat_msg_partial_exception("incomplete tool call");
+ }
+ } else if (data.value.contains("response")) {
+ const auto & response = data.value.at("response");
+ builder.add_content(response.is_string() ? response.template get() : response.dump(2));
+ if (data.is_partial) {
+ throw common_chat_msg_partial_exception("incomplete response");
+ }
+ } else {
+ throw common_chat_msg_partial_exception("Expected 'tool_call', 'tool_calls' or 'response' in JSON");
}
- return result;
}
static common_chat_params common_chat_params_init_mistral_nemo(const common_chat_template & tmpl, const struct templates_params & inputs) {
@@ -813,12 +908,44 @@ static common_chat_params common_chat_params_init_mistral_nemo(const common_chat
data.format = COMMON_CHAT_FORMAT_MISTRAL_NEMO;
return data;
}
-static common_chat_msg common_chat_parse_mistral_nemo(const std::string & input) {
- return parse_prefixed_json_tool_call_array(input, "[TOOL_CALLS]");
+static void common_chat_parse_mistral_nemo(common_chat_msg_parser & builder) {
+ if (!builder.syntax().parse_tool_calls) {
+ builder.add_content(builder.consume_rest());
+ return;
+ }
+
+ static const common_regex prefix(regex_escape("[TOOL_CALLS]"));
+ parse_prefixed_json_tool_call_array(builder, prefix);
}
static common_chat_params common_chat_params_init_command_r7b(const common_chat_template & tmpl, const struct templates_params & inputs) {
common_chat_params data;
+
+ auto adjusted_messages = json::array();
+ for (const auto & msg : inputs.messages) {
+ auto has_reasoning_content = msg.contains("reasoning_content") && msg.at("reasoning_content").is_string();
+ auto has_tool_calls = msg.contains("tool_calls") && msg.at("tool_calls").is_array();
+ if (has_reasoning_content && has_tool_calls) {
+ auto adjusted_message = msg;
+ adjusted_message["tool_plan"] = msg.at("reasoning_content");
+ adjusted_message.erase("reasoning_content");
+ adjusted_messages.push_back(adjusted_message);
+ } else {
+ adjusted_messages.push_back(msg);
+ }
+ }
+ data.prompt = apply(tmpl, adjusted_messages, inputs.tools.empty() ? json() : inputs.tools, inputs.add_generation_prompt, {});
+ data.format = COMMON_CHAT_FORMAT_COMMAND_R7B;
+ if (string_ends_with(data.prompt, "<|START_THINKING|>")) {
+ if (!inputs.enable_thinking) {
+ data.prompt += "<|END_THINKING|>";
+ } else {
+ data.thinking_forced_open = true;
+ }
+ } else if (!inputs.enable_thinking && string_ends_with(data.prompt, "<|CHATBOT_TOKEN|>")) {
+ data.prompt += "<|START_THINKING|><|END_THINKING|>";
+ }
+
data.grammar_lazy = inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_REQUIRED;
data.grammar = build_grammar([&](const common_grammar_builder & builder) {
auto schemas = json::array();
@@ -849,11 +976,16 @@ static common_chat_params common_chat_params_init_command_r7b(const common_chat_
if (!inputs.parallel_tool_calls) {
schema["maxItems"] = 1;
}
- builder.add_rule("root", "\"<|START_ACTION|>\" " + builder.add_schema("tool_calls", schema) + " \"<|END_ACTION|>\"");
+ builder.add_rule("root",
+ std::string(data.thinking_forced_open ? "( \"<|END_THINKING|>\" space )? " : "") +
+ "\"<|START_ACTION|>\" " + builder.add_schema("tool_calls", schema) + " \"<|END_ACTION|>\"");
});
data.grammar_triggers.push_back({
- COMMON_GRAMMAR_TRIGGER_TYPE_WORD,
- "<|START_ACTION|>",
+ COMMON_GRAMMAR_TRIGGER_TYPE_PATTERN_FULL,
+ // If thinking_forced_open, then we capture the tag in the grammar,
+ // (important for required tool choice) and in the trigger's first capture (decides what is sent to the grammar)
+ std::string(data.thinking_forced_open ? "[\\s\\S]*?(<\\|END_THINKING\\|>\\s*)" : "(?:<\\|START_THINKING\\|>[\\s\\S]*?<\\|END_THINKING\\|>\\s*)?") +
+ "(<\\|START_ACTION\\|>)[\\s\\S]*"
});
data.preserved_tokens = {
"<|START_ACTION|>",
@@ -863,61 +995,40 @@ static common_chat_params common_chat_params_init_command_r7b(const common_chat_
"<|START_THINKING|>",
"<|END_THINKING|>",
};
- auto adjusted_messages = json::array();
- for (const auto & msg : inputs.messages) {
- auto has_reasoning_content = msg.contains("reasoning_content") && msg.at("reasoning_content").is_string();
- auto has_tool_calls = msg.contains("tool_calls") && msg.at("tool_calls").is_array();
- if (has_reasoning_content && has_tool_calls) {
- auto adjusted_message = msg;
- adjusted_message["tool_plan"] = msg.at("reasoning_content");
- adjusted_message.erase("reasoning_content");
- adjusted_messages.push_back(adjusted_message);
- } else {
- adjusted_messages.push_back(msg);
- }
- }
- data.prompt = apply(tmpl, adjusted_messages, inputs.tools.empty() ? json() : inputs.tools, inputs.add_generation_prompt, {});
- data.format = inputs.extract_reasoning ? COMMON_CHAT_FORMAT_COMMAND_R7B_EXTRACT_REASONING : COMMON_CHAT_FORMAT_COMMAND_R7B;
return data;
}
-static common_chat_msg common_chat_parse_command_r7b(const std::string & input, bool extract_reasoning) {
- static const std::regex thought_regex("(<\\|START_THINKING\\|>([\\s\\S]*?)<\\|END_THINKING\\|>)([\\s\\S]*)");
- static const std::regex action_regex("<\\|START_ACTION\\|>([\\s\\S]*?)<\\|END_ACTION\\|>");
- static const std::regex response_regex("(?:<\\|START_RESPONSE\\|>)?([\\s\\S]*?)<\\|END_RESPONSE\\|>");
- std::smatch match;
+static void common_chat_parse_command_r7b(common_chat_msg_parser & builder) {
+ builder.try_parse_reasoning("<|START_THINKING|>", "<|END_THINKING|>");
- common_chat_msg result;
- result.role = "assistant";
+ static const common_regex start_action_regex("<\\|START_ACTION\\|>");
+ static const common_regex end_action_regex("<\\|END_ACTION\\|>");
+ static const common_regex start_response_regex("<\\|START_RESPONSE\\|>");
+ static const common_regex end_response_regex("<\\|END_RESPONSE\\|>");
- std::string rest = input;
-
- if (std::regex_match(rest, match, thought_regex)) {
- if (extract_reasoning) {
- result.reasoning_content = match[2].str();
- } else if (!match[2].str().empty()) {
- // Let the unparsed thinking tags through in content only if their insides aren't empty.
- result.content = match[1].str();
+ if (auto res = builder.try_find_regex(start_action_regex)) {
+ // If we didn't extract thoughts, prelude includes them.
+ auto tool_calls = builder.consume_json_with_dumped_args({{"parameters"}});
+ for (const auto & tool_call : tool_calls.value) {
+ std::string name = tool_call.contains("tool_name") ? tool_call.at("tool_name") : "";
+ std::string id = tool_call.contains("tool_call_id") ? tool_call.at("tool_call_id") : "";
+ std::string arguments = tool_call.contains("parameters") ? tool_call.at("parameters") : "";
+ if (!builder.add_tool_call(name, id, arguments) || tool_calls.is_partial) {
+ throw common_chat_msg_partial_exception("incomplete tool call");
+ }
}
- rest = match[3].str();
- }
- if (std::regex_match(rest, match, action_regex)) {
- auto actions_str = match[1].str();
- auto actions = json::parse(actions_str);
- for (const auto & action : actions) {
- result.tool_calls.push_back({
- /* .name = */ action.at("tool_name"),
- /* .arguments = */ action.at("parameters").dump(),
- /* .id = */ action.at("tool_call_id"),
- });
+ if (tool_calls.is_partial) {
+ throw common_chat_msg_partial_exception("incomplete tool call");
+ }
+ builder.consume_regex(end_action_regex);
+ } else if (auto res = builder.try_find_regex(start_response_regex)) {
+ if (!builder.try_find_regex(end_response_regex)) {
+ builder.add_content(builder.consume_rest());
+ throw common_chat_msg_partial_exception(end_response_regex.str());
}
- } else if (std::regex_match(rest, match, response_regex)) {
- auto response = match[1].str();
- result.content += response;
} else {
- result.content += rest;
+ builder.add_content(builder.consume_rest());
}
- return result;
}
static void expect_tool_parameters(const std::string & name, const json & parameters, const std::vector & expected_properties) {
@@ -939,152 +1050,143 @@ static void expect_tool_parameters(const std::string & name, const json & parame
}
}
-static common_chat_params common_chat_params_init_llama_3_1_tool_calls(const common_chat_template & tmpl, const struct templates_params & inputs, bool allow_python_tag_builtin_tools) {
+static common_chat_params common_chat_params_init_llama_3_x(const common_chat_template & tmpl, const struct templates_params & inputs, bool allow_python_tag_builtin_tools) {
auto builtin_tools = json::array();
common_chat_params data;
- data.grammar_lazy = inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_REQUIRED;
- data.grammar = build_grammar([&](const common_grammar_builder & builder) {
- std::vector tool_rules;
-
- auto handle_builtin_tool = [&](const std::string & name, const json & parameters) {
- if (name == "wolfram_alpha" || name == "web_search" || name == "brave_search") {
- // https://github.com/meta-llama/llama-stack/blob/main/llama_stack/providers/remote/tool_runtime/wolfram_alpha/wolfram_alpha.py
- // https://github.com/meta-llama/llama-stack/blob/main/llama_stack/providers/remote/tool_runtime/brave_search/brave_search.py
- expect_tool_parameters(name, parameters, {"query"});
- } else if (name == "python" || name == "code_interpreter") {
- // https://github.com/meta-llama/llama-stack/blob/main/llama_stack/providers/inline/tool_runtime/code_interpreter/code_interpreter.py
- expect_tool_parameters(name, parameters, {"code"});
- } else {
- return false;
- }
-
- std::vector kvs;
- for (const auto & [key, value] : parameters.at("properties").items()) {
- kvs.push_back("\"" + key + "=\" " + builder.add_schema(name + "-args-" + key, value)); // NOLINT
- }
-
- tool_rules.push_back(
- builder.add_rule(
- name + "-call",
- "\"<|python_tag|>" + name + ".call(\" " + string_join(kvs, " \", \" ") + " \")\""));
- builtin_tools.push_back(name);
-
- return true;
- };
-
- foreach_function(inputs.tools, [&](const json & tool) {
- const auto & function = tool.at("function");
- std::string name = function.at("name");
- auto parameters = function.at("parameters");
- builder.resolve_refs(parameters);
-
- // https://github.com/meta-llama/llama-stack/tree/main/llama_stack/providers/remote/tool_runtime
- if (allow_python_tag_builtin_tools) {
- handle_builtin_tool(name, parameters);
- }
- tool_rules.push_back(
- builder.add_rule(
- name + "-call",
- "\"{\" space "
- "( \"\\\"type\\\"\" space \":\" space \"\\\"function\\\"\" space \",\" space )? "
- " \"\\\"name\\\"\" space \":\" space \"\\\"" + name + "\\\"\" space \",\" space "
- " \"\\\"parameters\\\"\" space \":\" space " + builder.add_schema(name + "-args", parameters) + " "
- "\"}\" space"));
- });
- // Small models may hallucinate function names so we match anything (*at the start*) that looks like the JSON of a function call, regardless of the name.
- data.grammar_triggers.push_back({
- COMMON_GRAMMAR_TRIGGER_TYPE_PATTERN_START,
- "\\{\\s*(?:\"type\"\\s*:\\s*\"function\"\\s*,\\s*)?\"name\"\\s*:\\s*\"", // + name + "\"[\\s\\S]*",
- });
- if (!builtin_tools.empty()) {
- data.grammar_triggers.push_back({COMMON_GRAMMAR_TRIGGER_TYPE_WORD, "<|python_tag|>"});
- data.preserved_tokens.push_back("<|python_tag|>");
- }
- // Allow a few empty lines on top of the usual constrained json schema space rule.
- builder.add_rule("root", string_join(tool_rules, " | "));
- });
- data.additional_stops.push_back("<|eom_id|>");
- data.prompt = apply(tmpl, inputs.messages, inputs.tools.empty() ? json() : inputs.tools, inputs.add_generation_prompt, {
- {"tools_in_user_message", false},
- {"builtin_tools", builtin_tools.empty() ? json() : builtin_tools},
- });
- data.format = allow_python_tag_builtin_tools && !builtin_tools.empty()
- ? COMMON_CHAT_FORMAT_LLAMA_3_X_WITH_BUILTIN_TOOLS
- : COMMON_CHAT_FORMAT_LLAMA_3_X;
- return data;
-}
-static common_chat_msg common_chat_parse_llama_3_1(const std::string & input, bool with_builtin_tools = false) {
- // TODO: tighten & simplify the parser, don't accept leading text context.
- static const std::regex function_regex(
- "\\s*\\{\\s*(?:\"type\"\\s*:\\s*\"function\"\\s*,\\s*)?\"name\"\\s*:\\s*\"([^\"]+)\"\\s*,\\s*\"parameters\"\\s*: ");
- static const std::regex close_regex("\\}\\s*");
- static const std::regex builtin_call_regex("<\\|python_tag\\|>\\s*([^.(]+)\\s*\\.\\s*call\\s*\\(\\s*([\\w]+)\\s*=\\s*([\\s\\S]*?)\\)");
-
- if (with_builtin_tools) {
- std::smatch match;
- if (std::regex_match(input, match, builtin_call_regex)) {
- try {
- auto name = match[1].str();
- auto arg_name = match[2].str();
- auto arg_value_str = match[3].str();
- auto arg_value = json::parse(arg_value_str);
-
- common_chat_msg msg;
- msg.role = "assistant";
- msg.tool_calls.push_back({
- /* .name = */ name,
- /* .arguments = */ (json {
- {arg_name, arg_value},
- }).dump(),
- /* .id = */ "",
- });
- return msg;
- } catch (const std::exception & e) {
- LOG_WRN("Failed to parse builtin tool call arguments (%s): %s", e.what(), input.c_str());
- }
- }
- }
- return parse_json_tool_calls(input, std::nullopt, function_regex, close_regex);
-}
-
-static common_chat_params common_chat_params_init_deepseek_r1(const common_chat_template & tmpl, const struct templates_params & inputs) {
- common_chat_params data;
- if (inputs.tools.is_array() && !inputs.tools.empty()) {
- data.grammar_lazy = inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_REQUIRED && inputs.json_schema.is_null();
+ if (!inputs.tools.is_null()) {
+ data.grammar_lazy = inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_REQUIRED;
data.grammar = build_grammar([&](const common_grammar_builder & builder) {
std::vector tool_rules;
+
+ auto handle_builtin_tool = [&](const std::string & name, const json & parameters) {
+ if (name == "wolfram_alpha" || name == "web_search" || name == "brave_search") {
+ // https://github.com/meta-llama/llama-stack/blob/main/llama_stack/providers/remote/tool_runtime/wolfram_alpha/wolfram_alpha.py
+ // https://github.com/meta-llama/llama-stack/blob/main/llama_stack/providers/remote/tool_runtime/brave_search/brave_search.py
+ expect_tool_parameters(name, parameters, {"query"});
+ } else if (name == "python" || name == "code_interpreter") {
+ // https://github.com/meta-llama/llama-stack/blob/main/llama_stack/providers/inline/tool_runtime/code_interpreter/code_interpreter.py
+ expect_tool_parameters(name, parameters, {"code"});
+ } else {
+ return false;
+ }
+
+ std::vector kvs;
+ for (const auto & [key, value] : parameters.at("properties").items()) {
+ kvs.push_back("\"" + key + "=\" " + builder.add_schema(name + "-args-" + key, value)); // NOLINT
+ }
+
+ tool_rules.push_back(
+ builder.add_rule(
+ name + "-call",
+ "\"<|python_tag|>" + name + ".call(\" " + string_join(kvs, " \", \" ") + " \")\""));
+ builtin_tools.push_back(name);
+
+ return true;
+ };
+
foreach_function(inputs.tools, [&](const json & tool) {
const auto & function = tool.at("function");
std::string name = function.at("name");
auto parameters = function.at("parameters");
builder.resolve_refs(parameters);
- tool_rules.push_back(builder.add_rule(name + "-call",
- "\"<|tool▁call▁begin|>function<|tool▁sep|>" + name + "\\n"
- "```json\\n\" " + builder.add_schema(name + "-args", parameters) + " "
- "\"```<|tool▁call▁end|>\""));
+
+ // https://github.com/meta-llama/llama-stack/tree/main/llama_stack/providers/remote/tool_runtime
+ if (allow_python_tag_builtin_tools) {
+ handle_builtin_tool(name, parameters);
+ }
+ tool_rules.push_back(
+ builder.add_rule(
+ name + "-call",
+ "\"{\" space "
+ "( \"\\\"type\\\"\" space \":\" space \"\\\"function\\\"\" space \",\" space )? "
+ " \"\\\"name\\\"\" space \":\" space \"\\\"" + name + "\\\"\" space \",\" space "
+ " \"\\\"parameters\\\"\" space \":\" space " + builder.add_schema(name + "-args", parameters) + " "
+ "\"}\" space"));
});
- // Distill Qwen 7B & 32B models seem confused re/ syntax of their tool call opening tag,
- // so we accept common variants (then it's all constrained)
- builder.add_rule("root",
- "( \"<|tool▁calls▁begin|>\" | \"<|tool_calls_begin|>\" | \"<|tool calls begin|>\" | \"<|tool\\\\_calls\\\\_begin|>\" ) "
- "(" + string_join(tool_rules, " | ") + ")" + (inputs.parallel_tool_calls ? "*" : "") + " "
- "\"<|tool▁calls▁end|>\""
- " space");
- data.grammar_triggers.push_back({COMMON_GRAMMAR_TRIGGER_TYPE_WORD, "<|tool▁calls▁begin|>"});
- data.grammar_triggers.push_back({COMMON_GRAMMAR_TRIGGER_TYPE_WORD, "<|tool_calls_begin|>"});
- data.grammar_triggers.push_back({COMMON_GRAMMAR_TRIGGER_TYPE_WORD, "<|tool calls begin|>"});
- data.grammar_triggers.push_back({COMMON_GRAMMAR_TRIGGER_TYPE_WORD, "<|tool\\_calls\\_begin|>"});
- data.preserved_tokens = {
- "",
- "",
- "<|tool▁calls▁begin|>",
- "<|tool▁call▁begin|>",
- "<|tool▁sep|>",
- "<|tool▁call▁end|>",
- "<|tool▁calls▁end|",
- };
+ // Small models may hallucinate function names so we match anything (*at the start*) that looks like the JSON of a function call, regardless of the name.
+ data.grammar_triggers.push_back({
+ COMMON_GRAMMAR_TRIGGER_TYPE_PATTERN_FULL,
+ "(\\{\\s*(?:\"type\"\\s*:\\s*\"function\"\\s*,\\s*)?\"name\"\\s*:\\s*\")[\\s\\S]*", // + name + "\"[\\s\\S]*",
+ });
+ if (!builtin_tools.empty()) {
+ data.grammar_triggers.push_back({COMMON_GRAMMAR_TRIGGER_TYPE_WORD, "<|python_tag|>"});
+ data.preserved_tokens.push_back("<|python_tag|>");
+ }
+ // Allow a few empty lines on top of the usual constrained json schema space rule.
+ builder.add_rule("root", string_join(tool_rules, " | "));
+ data.additional_stops.push_back("<|eom_id|>");
});
+ data.format = allow_python_tag_builtin_tools && !builtin_tools.empty()
+ ? COMMON_CHAT_FORMAT_LLAMA_3_X_WITH_BUILTIN_TOOLS
+ : COMMON_CHAT_FORMAT_LLAMA_3_X;
+ } else {
+ data.format = COMMON_CHAT_FORMAT_CONTENT_ONLY;
}
+ data.prompt = apply(tmpl, inputs.messages, inputs.tools.empty() ? json() : inputs.tools, inputs.add_generation_prompt, {
+ {"date_string", format_time(inputs.now, "%d %b %Y")},
+ {"tools_in_user_message", false},
+ {"builtin_tools", builtin_tools.empty() ? json() : builtin_tools},
+ });
+ return data;
+}
+static void common_chat_parse_llama_3_1(common_chat_msg_parser & builder, bool with_builtin_tools = false) {
+ if (!builder.syntax().parse_tool_calls) {
+ builder.add_content(builder.consume_rest());
+ return;
+ }
+
+ static const common_regex function_regex(
+ "\\s*\\{\\s*(?:\"type\"\\s*:\\s*\"function\"\\s*,\\s*)?\"name\"\\s*:\\s*\"([^\"]+)\"\\s*,\\s*\"parameters\"\\s*: ");
+ static const common_regex close_regex("\\}\\s*");
+
+ static const common_regex function_name_regex("\\s*(\\w+)\\s*\\.\\s*call\\(");
+ static const common_regex arg_name_regex("\\s*(\\w+)\\s*=\\s*");
+
+ if (with_builtin_tools) {
+ static const common_regex builtin_call_regex("<\\|python_tag\\|>");
+ if (auto res = builder.try_find_regex(builtin_call_regex)) {
+ auto fun_res = builder.consume_regex(function_name_regex);
+ auto function_name = builder.str(fun_res.groups[1]);
+
+ common_healing_marker healing_marker;
+ json args = json::object();
+ while (true) {
+ if (auto arg_res = builder.try_consume_regex(arg_name_regex)) {
+ auto arg_name = builder.str(arg_res->groups[1]);
+ auto partial = builder.consume_json();
+ args[arg_name] = partial.json;
+ healing_marker.marker = partial.healing_marker.marker;
+ healing_marker.json_dump_marker = partial.healing_marker.json_dump_marker;
+ builder.consume_spaces();
+ if (!builder.try_consume_literal(",")) {
+ break;
+ }
+ } else {
+ break;
+ }
+ }
+ builder.consume_literal(")");
+ builder.consume_spaces();
+
+ auto arguments = args.dump();
+ if (!builder.add_tool_call(function_name, "", arguments)) {
+ throw common_chat_msg_partial_exception("Incomplete tool call");
+ }
+ return;
+ }
+ }
+ parse_json_tool_calls(
+ builder,
+ /* block_open= */ std::nullopt,
+ /* function_regex_start_only= */ function_regex,
+ /* function_regex= */ std::nullopt,
+ close_regex,
+ std::nullopt);
+
+}
+
+static common_chat_params common_chat_params_init_deepseek_r1(const common_chat_template & tmpl, const struct templates_params & inputs) {
+ common_chat_params data;
auto prompt = apply(tmpl, inputs.messages, inputs.tools.empty() ? json() : inputs.tools, inputs.add_generation_prompt);
// Hacks to fix the official (broken) prompt.
@@ -1105,52 +1207,83 @@ static common_chat_params common_chat_params_init_deepseek_r1(const common_chat_
"$1<|tool▁calls▁end|><|end▁of▁sentence|>$2");
}
data.prompt = prompt;
- data.format = inputs.extract_reasoning ? COMMON_CHAT_FORMAT_DEEPSEEK_R1_EXTRACT_REASONING : COMMON_CHAT_FORMAT_DEEPSEEK_R1;
+ data.format = COMMON_CHAT_FORMAT_DEEPSEEK_R1;
+ if (string_ends_with(data.prompt, "\n")) {
+ if (!inputs.enable_thinking) {
+ data.prompt += "";
+ } else {
+ data.thinking_forced_open = true;
+ }
+ }
+
+ if (inputs.tools.is_array() && !inputs.tools.empty()) {
+ data.grammar_lazy = inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_REQUIRED && inputs.json_schema.is_null();
+ data.grammar = build_grammar([&](const common_grammar_builder & builder) {
+ std::vector tool_rules;
+ foreach_function(inputs.tools, [&](const json & tool) {
+ const auto & function = tool.at("function");
+ std::string name = function.at("name");
+ auto parameters = function.at("parameters");
+ builder.resolve_refs(parameters);
+ tool_rules.push_back(builder.add_rule(name + "-call",
+ "( \"<|tool▁call▁begin|>\" )? \"function<|tool▁sep|>" + name + "\\n"
+ "```json\\n\" " + builder.add_schema(name + "-args", parameters) + " "
+ "\"```<|tool▁call▁end|>\""));
+ });
+ // Distill Qwen 7B & 32B models seem confused re/ syntax of their tool call opening tag,
+ // so we accept common variants (then it's all constrained)
+ builder.add_rule("root",
+ std::string(data.thinking_forced_open ? "( \"\" space )? " : "") +
+ "( \"<|tool▁calls▁begin|>\" | \"<|tool_calls_begin|>\" | \"<|tool calls begin|>\" | \"<|tool\\\\_calls\\\\_begin|>\" | \"<|tool▁calls|>\" ) "
+ "(" + string_join(tool_rules, " | ") + ")" + (inputs.parallel_tool_calls ? "*" : "") + " "
+ "\"<|tool▁calls▁end|>\""
+ " space");
+ data.grammar_triggers.push_back({
+ COMMON_GRAMMAR_TRIGGER_TYPE_PATTERN_FULL,
+ // If thinking_forced_open, then we capture the tag in the grammar,
+ // (important for required tool choice) and in the trigger's first capture (decides what is sent to the grammar)
+ std::string(data.thinking_forced_open ? "[\\s\\S]*?(\\s*)" : "(?:[\\s\\S]*?\\s*)?") +
+ "(<|tool▁calls▁begin|>|<|tool_calls_begin|>|<|tool calls begin|>|<|tool\\\\_calls\\\\_begin|>|<|tool▁calls|>)[\\s\\S]*"
+ });
+ data.preserved_tokens = {
+ "",
+ "",
+ "<|tool▁calls▁begin|>",
+ "<|tool▁call▁begin|>",
+ "<|tool▁sep|>",
+ "<|tool▁call▁end|>",
+ "<|tool▁calls▁end|",
+ };
+ });
+ }
return data;
}
-static common_chat_msg handle_think_tag_prelude(const std::string & input, bool extract_reasoning, const std::function & rest_parser) {
- std::smatch match;
- static const std::regex reasoning_content_regex("((?:)?([\\s\\S\\r\\n]*?))?([\\s\\S\\r\\n]*)");
- if (std::regex_match(input, match, reasoning_content_regex)) {
- auto rest = match[3].str();
- auto msg = rest_parser(rest);
- auto reasoning_content = string_strip(match[2].str());
- if (extract_reasoning) {
- msg.reasoning_content = reasoning_content;
- } else if (!reasoning_content.empty()) {
- std::ostringstream content;
- content << "" << reasoning_content << "" << msg.content;
- msg.content = content.str();
- }
- return msg;
+static void common_chat_parse_deepseek_r1(common_chat_msg_parser & builder) {
+ builder.try_parse_reasoning("", "");
+ if (!builder.syntax().parse_tool_calls) {
+ builder.add_content(builder.consume_rest());
+ return;
}
- return rest_parser(input);
-}
-static common_chat_msg common_chat_parse_deepseek_r1(const std::string & input, bool extract_reasoning) {
- return handle_think_tag_prelude(input, extract_reasoning, [](const std::string & input) {
- static const std::regex function_regex("<|tool▁call▁begin|>function<|tool▁sep|>([^\n]+)\n```json\n");
- static const std::regex close_regex("```[\\s\\r\\n]*<|tool▁call▁end|>");
- static const std::regex tool_calls_regex("[\\s\\r\\n]*(?:<|tool▁calls▁begin|>|<|tool_calls_begin|>|<|tool calls begin|>|<|tool\\\\_calls\\\\_begin|>)([\\s\\S\\r\\n]*?)<|tool▁calls▁end|>");
- common_chat_msg msg;
- msg.role = "assistant";
- std::smatch match;
- if (std::regex_search(input, match, tool_calls_regex)) {
- auto tool_calls = match[1].str();
- auto msg2 = parse_json_tool_calls(tool_calls, std::nullopt, function_regex, close_regex);
- msg.tool_calls = std::move(msg2.tool_calls);
- } else {
- msg.content = input;
- }
- return msg;
- });
+ static const common_regex tool_calls_begin("(?:<|tool▁calls▁begin|>|<|tool_calls_begin|>|<|tool calls begin|>|<|tool\\\\_calls\\\\_begin|>|<|tool▁calls|>)");
+ static const common_regex tool_calls_end("<|tool▁calls▁end|>");
+ static const common_regex function_regex("(?:<|tool▁call▁begin|>)?function<|tool▁sep|>([^\n]+)\n```json\n");
+ static const common_regex close_regex("```[\\s\\r\\n]*<|tool▁call▁end|>");
+
+ parse_json_tool_calls(
+ builder,
+ /* block_open= */ tool_calls_begin,
+ /* function_regex_start_only= */ std::nullopt,
+ function_regex,
+ close_regex,
+ tool_calls_end);
}
static common_chat_params common_chat_params_init_firefunction_v2(const common_chat_template & tmpl, const struct templates_params & inputs) {
LOG_DBG("%s\n", __func__);
common_chat_params data;
data.prompt = apply(tmpl, inputs.messages, /* tools= */ nullptr, inputs.add_generation_prompt, {
- {"datetime", "Jan 29 2025 13:00:00 GMT"},
+ {"datetime", format_time(inputs.now, "%b %d %Y %H:%M:%S GMT")},
{"functions", json(inputs.tools.empty() ? "" : inputs.tools.dump(2))},
});
if (inputs.tools.is_array() && !inputs.tools.empty()) {
@@ -1191,13 +1324,19 @@ static common_chat_params common_chat_params_init_firefunction_v2(const common_c
}
return data;
}
-static common_chat_msg common_chat_parse_firefunction_v2(const std::string & input) {
- return parse_prefixed_json_tool_call_array(input, " functools[", /* rstrip_prefix= */ 1);
+static void common_chat_parse_firefunction_v2(common_chat_msg_parser & builder) {
+ if (!builder.syntax().parse_tool_calls) {
+ builder.add_content(builder.consume_rest());
+ return;
+ }
+ static const common_regex prefix(regex_escape(" functools["));
+ parse_prefixed_json_tool_call_array(builder, prefix, /* rstrip_prefix= */ 1);
}
static common_chat_params common_chat_params_init_functionary_v3_2(const common_chat_template & tmpl, const struct templates_params & inputs) {
// >>>all\nlet's call functions>>>fn1\n{"arg1": 1...}\n>>>fn2\n{"arg1": 1...}...
// Using ">>>f1\n", ">>>f2\n"... as trigger words for the grammar
+ // If the function is python, we also allow raw python code (if the line after `python\n` doesn't start w/ opening `{`), which the model seems to prefer for multiline code.
common_chat_params data;
data.prompt = apply(tmpl, inputs.messages, inputs.tools.empty() ? json() : inputs.tools, inputs.add_generation_prompt);
data.format = COMMON_CHAT_FORMAT_FUNCTIONARY_V3_2;
@@ -1211,24 +1350,21 @@ static common_chat_params common_chat_params_init_functionary_v3_2(const common_
std::string name = function.at("name");
auto parameters = function.at("parameters");
builder.resolve_refs(parameters);
+ std::string args_pattern = "[\\s\\S]*";
auto args_rule = builder.add_schema(name + "-args", parameters);
- first_tool_rules.push_back(builder.add_rule(name + "-call", "( \"assistant<|end_header_id|>\\n\" )? \"" + name + "\\n\" " + args_rule));
- subsequent_tool_rules.push_back(builder.add_rule(name + "-call2", "\">>>" + name + "\\n\" " + args_rule));
+ if (name == "python") {
+ args_rule = builder.add_rule(name + "-maybe-raw-args", args_rule + " | [^{] .*");
+ } else {
+ args_pattern = "\\{" + args_pattern;
+ }
+ auto call_rule = builder.add_rule(name + "-call", "\"" + name + "\\n\" " + args_rule);
+ first_tool_rules.push_back(call_rule);
+ if (inputs.parallel_tool_calls) {
+ subsequent_tool_rules.push_back(builder.add_rule(name + "-call2", "\">>>\" " + call_rule));
+ }
data.grammar_triggers.push_back({
- COMMON_GRAMMAR_TRIGGER_TYPE_PATTERN_START,
- regex_escape(name + "\n"),
- });
- data.grammar_triggers.push_back({
- COMMON_GRAMMAR_TRIGGER_TYPE_PATTERN_START,
- regex_escape("assistant<|end_header_id|>\n" + name + "\n"),
- });
- data.grammar_triggers.push_back({
- COMMON_GRAMMAR_TRIGGER_TYPE_WORD,
- regex_escape(">>>" + name + "\n"),
- });
- data.grammar_triggers.push_back({
- COMMON_GRAMMAR_TRIGGER_TYPE_WORD,
- ">>>assistant<|end_header_id|>\n" + name,
+ COMMON_GRAMMAR_TRIGGER_TYPE_PATTERN_FULL,
+ "((?:[\\s\\S]+?>>>)?" + regex_escape(name) + "\n)" + args_pattern,
});
});
data.preserved_tokens = {
@@ -1246,319 +1382,311 @@ static common_chat_params common_chat_params_init_functionary_v3_2(const common_
}
return data;
}
+static void common_chat_parse_functionary_v3_2(common_chat_msg_parser & builder) {
+ static const common_regex function_regex_start_only(R"((\w+\n\{|python\n|all\n))");
+ static const common_regex function_regex(R"(>>>(\w+\n\{|python\n|all\n))");
+ static const common_regex close_regex(R"(\s*)");
-static common_chat_msg common_chat_parse_functionary_v3_2(const std::string & input) {
- static const std::regex function_regex(R"((?:>>>)?(?:assistant<|end_header_id|>\n)?(\w+)\n)");
- static const std::regex close_regex(R"($|(?=>>>))");
-
- std::string content;
- auto it = input.begin();
- const auto end = input.end();
-
- if (parse_literal(it, end, "all\n")) {
- std::smatch match;
- if (std::regex_search(it, end, match, function_regex)) {
- auto fun_it = match.prefix().second;
- content = std::string(it, fun_it);
- it = fun_it;
- } else {
- common_chat_msg res;
- res.role = "assistant";
- res.content = std::string(it, end);
- return res;
- }
- }
- // TODO: tighten & simplify.
- try {
- auto res = parse_json_tool_calls(std::string(it, end), std::nullopt, function_regex, close_regex, /* allow_raw_python= */ true);
- res.content = content + res.content;
- return res;
- } catch (const std::exception & e) {
- LOG_ERR("Failed to parse functionary v3.2 input: %s\n", e.what());
- common_chat_msg res;
- res.role = "assistant";
- res.content = input;
- return res;
- }
+ parse_json_tool_calls(
+ builder,
+ std::nullopt,
+ function_regex_start_only,
+ function_regex,
+ close_regex,
+ std::nullopt,
+ /* allow_raw_python= */ true,
+ /* get_function_name= */ [&](const auto & res) -> std::string {
+ auto at_start = res.groups[0].begin == 0;
+ auto name = builder.str(res.groups[1]);
+ if (!name.empty() && name.back() == '{') {
+ // Unconsume the opening brace '{' to ensure the JSON parsing goes well.
+ builder.move_back(1);
+ }
+ auto idx = name.find_last_not_of("\n{");
+ name = name.substr(0, idx + 1);
+ if (at_start && name == "all") {
+ return "";
+ }
+ return name;
+ });
}
static common_chat_params common_chat_params_init_functionary_v3_1_llama_3_1(const common_chat_template & tmpl, const struct templates_params & inputs) {
// https://github.com/MeetKai/functionary/blob/main/tests/prompt_test_v3-llama3.1.txt
common_chat_params data;
- json tools = inputs.tools.is_null() ? inputs.tools : json::array();
- std::string python_code_argument_name;
- auto has_raw_python = false;
- data.grammar_lazy = inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_REQUIRED;
- data.grammar = build_grammar([&](const common_grammar_builder & builder) {
- std::vector tool_rules;
- foreach_function(inputs.tools, [&](const json & tool) {
- const auto & function = tool.at("function");
- const auto & parameters = function.at("parameters");
- std::string name = function.at("name");
- if (name == "python" || name == "ipython") {
- if (!parameters.contains("type")) {
- throw std::runtime_error("Missing type in python tool");
- }
- has_raw_python = true;
- const auto & type = parameters.at("type");
- if (type == "object") {
- auto properties = parameters.at("properties");
- for (auto it = properties.begin(); it != properties.end(); ++it) {
- if (it.value().at("type") == "string") {
- if (!python_code_argument_name.empty()) {
- throw std::runtime_error("Multiple string arguments found in python tool");
+ if (!inputs.tools.is_null()) {
+ std::string python_code_argument_name;
+ auto has_raw_python = false;
+
+ data.grammar_lazy = inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_REQUIRED;
+ data.grammar = build_grammar([&](const common_grammar_builder & builder) {
+ std::vector tool_rules;
+ foreach_function(inputs.tools, [&](const json & tool) {
+ const auto & function = tool.at("function");
+ const auto & parameters = function.at("parameters");
+ std::string name = function.at("name");
+ if (name == "python" || name == "ipython") {
+ if (!parameters.contains("type")) {
+ throw std::runtime_error("Missing type in python tool");
+ }
+ has_raw_python = true;
+ const auto & type = parameters.at("type");
+ if (type == "object") {
+ auto properties = parameters.at("properties");
+ for (auto it = properties.begin(); it != properties.end(); ++it) {
+ if (it.value().at("type") == "string") {
+ if (!python_code_argument_name.empty()) {
+ throw std::runtime_error("Multiple string arguments found in python tool");
+ }
+ python_code_argument_name = it.key();
}
- python_code_argument_name = it.key();
}
+ if (python_code_argument_name.empty()) {
+ throw std::runtime_error("No string argument found in python tool");
+ }
+ } else if (type != "string") {
+ throw std::runtime_error("Invalid type in python tool: " + type.dump());
}
- if (python_code_argument_name.empty()) {
- throw std::runtime_error("No string argument found in python tool");
- }
- } else if (type != "string") {
- throw std::runtime_error("Invalid type in python tool: " + type.dump());
}
+ tool_rules.push_back(builder.add_rule(name + "-call", "\"\" " + builder.add_schema(name + "-args", parameters) + " \"\" space"));
+ });
+ if (has_raw_python) {
+ tool_rules.push_back(builder.add_rule("python-call", "\"<|python_tag|>\" .*"));
+ data.grammar_triggers.push_back({COMMON_GRAMMAR_TRIGGER_TYPE_WORD, "<|python_tag|>"});
+ data.preserved_tokens.push_back("<|python_tag|>");
}
- tool_rules.push_back(builder.add_rule(name + "-call", "\"\" " + builder.add_schema(name + "-args", parameters) + " \"\" space"));
+ auto tool_call = builder.add_rule("tool_call", string_join(tool_rules, " | ")) + " space";
+ builder.add_rule("root", inputs.parallel_tool_calls ? "(" + tool_call + ")+" : tool_call);
+ data.grammar_triggers.push_back({COMMON_GRAMMAR_TRIGGER_TYPE_WORD, "\" .*"));
- data.grammar_triggers.push_back({COMMON_GRAMMAR_TRIGGER_TYPE_WORD, "<|python_tag|>"});
- data.preserved_tokens.push_back("<|python_tag|>");
- }
- auto tool_call = builder.add_rule("tool_call", string_join(tool_rules, " | ")) + " space";
- builder.add_rule("root", inputs.parallel_tool_calls ? "(" + tool_call + ")+" : tool_call);
- data.grammar_triggers.push_back({COMMON_GRAMMAR_TRIGGER_TYPE_WORD, "([\s\S\n]*)$)");
- std::smatch match;
- if (std::regex_search(input, match, python_tag_regex)) {
- auto code = match[1].str();
- common_chat_msg msg;
- msg.role = "assistant";
- msg.content = match.prefix().str();
- msg.tool_calls.push_back({
- /* .name = */ "python",
- /* .arguments = */ (json {{"code", code}}).dump(),
- /* .id = */ "",
- });
- return msg;
+static void common_chat_parse_functionary_v3_1_llama_3_1(common_chat_msg_parser & builder) {
+ if (!builder.syntax().parse_tool_calls) {
+ builder.add_content(builder.consume_rest());
+ return;
+ }
+ // This version of Functionary still supports the llama 3.1 tool call format for the python tool.
+ static const common_regex python_tag_regex(regex_escape("<|python_tag|>"));
+
+ static const common_regex function_regex(R"()");
+ static const common_regex close_regex(R"()");
+
+ parse_json_tool_calls(
+ builder,
+ /* block_open= */ std::nullopt,
+ /* function_regex_start_only= */ std::nullopt,
+ function_regex,
+ close_regex,
+ std::nullopt);
+
+ if (auto res = builder.try_find_regex(python_tag_regex)) {
+ auto arguments = wrap_code_as_arguments(builder, builder.consume_rest());
+ builder.add_tool_call("python", "", arguments);
+ return;
}
- static const std::regex function_regex(R"()");
- static const std::regex close_regex(R"()");
- // TODO: tighten & simplify.
- return parse_json_tool_calls(input, std::nullopt, function_regex, close_regex);
}
static common_chat_params common_chat_params_init_hermes_2_pro(const common_chat_template & tmpl, const struct templates_params & inputs) {
common_chat_params data;
- // (content)?({"name": "foo", "arguments": {"a": 1}})*
- data.grammar_lazy = inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_REQUIRED;
- data.grammar = build_grammar([&](const common_grammar_builder & builder) {
- std::vector tool_rules;
- std::vector tool_call_alts;
- foreach_function(inputs.tools, [&](const json & tool) {
- const auto & function = tool.at("function");
- std::string name = function.at("name");
- auto parameters = function.at("parameters");
- builder.resolve_refs(parameters);
- tool_rules.push_back(builder.add_schema(name + "-call", {
- {"type", "object"},
- {"properties", json {
- {"name", json {{"const", name}}},
- {"arguments", parameters},
- }},
- {"required", json::array({"name", "arguments"})},
- }));
- tool_call_alts.push_back(builder.add_rule(
- name + "-function-tag",
- "\"\" space " +
- builder.add_schema(name + "-args", parameters) + " "
- "\"\" space"));
- data.grammar_triggers.push_back({
- COMMON_GRAMMAR_TRIGGER_TYPE_WORD,
- "",
- });
- auto escaped_name = regex_escape(name);
- data.grammar_triggers.push_back({
- COMMON_GRAMMAR_TRIGGER_TYPE_PATTERN,
- " alt_tags {
- any_tool_call,
- "\"\" space " + any_tool_call + " \"\"",
- // The rest is just to accommodate common "good bad" outputs.
- "\"\" space " + any_tool_call + " \"\"",
- "\"\" space " + any_tool_call + " \"\"",
- "\"\" space " + any_tool_call + " \"\"",
- "\"\" space " + any_tool_call + " \"\"",
- "\"\" space " + any_tool_call + " \"\"",
- "\"\" space " + any_tool_call + " \"\"",
- };
- auto wrappable_tool_call = builder.add_rule("wrappable_tool_call", "( " + string_join(alt_tags, " | ") + " ) space");
- tool_call_alts.push_back(wrappable_tool_call);
- tool_call_alts.push_back(
- "( \"```\\n\" | \"```json\\n\" | \"```xml\\n\" ) space " + wrappable_tool_call + " space \"```\" space ");
- auto tool_call = builder.add_rule("tool_call", string_join(tool_call_alts, " | "));
- builder.add_rule("root", inputs.parallel_tool_calls ? "(" + tool_call + ")+" : tool_call);
- data.grammar_triggers.push_back({COMMON_GRAMMAR_TRIGGER_TYPE_WORD, ""});
- data.grammar_triggers.push_back({COMMON_GRAMMAR_TRIGGER_TYPE_WORD, "|||)?\\s*\\{\\s*\"", //name\"\\s*:\\s*\"" + escaped_name + "\"",
- });
- data.preserved_tokens = {
- "",
- "",
- "",
- "",
- "",
- "",
- "",
- "",
- "",
- "",
- "",
- "",
- "",
- "",
- "```",
- "```json",
- "```xml",
- };
- });
+ json additional_context = {
+ {"enable_thinking", inputs.enable_thinking},
+ };
+
+ data.prompt = apply(tmpl, inputs.messages, inputs.tools.empty() ? json() : inputs.tools, inputs.add_generation_prompt, additional_context);
+ data.format = COMMON_CHAT_FORMAT_HERMES_2_PRO;
+ if (string_ends_with(data.prompt, "\n")) {
+ if (!inputs.enable_thinking) {
+ data.prompt += "";
+ } else {
+ data.thinking_forced_open = true;
+ }
+ }
+
+ if (!inputs.tools.is_null()) {
+ // (content)?({"name": "foo", "arguments": {"a": 1}})*
+ data.grammar_lazy = inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_REQUIRED;
+ data.grammar = build_grammar([&](const common_grammar_builder & builder) {
+ std::vector tool_rules;
+ std::vector tool_call_alts;
+ std::vector escaped_names;
+ foreach_function(inputs.tools, [&](const json & tool) {
+ const auto & function = tool.at("function");
+ std::string name = function.at("name");
+ auto parameters = function.at("parameters");
+ builder.resolve_refs(parameters);
+ tool_rules.push_back(builder.add_schema(name + "-call", {
+ {"type", "object"},
+ {"properties", json {
+ {"name", json {{"const", name}}},
+ {"arguments", parameters},
+ }},
+ {"required", json::array({"name", "arguments"})},
+ }));
+ tool_call_alts.push_back(builder.add_rule(
+ name + "-function-tag",
+ "\"\" space " +
+ builder.add_schema(name + "-args", parameters) + " "
+ "\"\" space"));
+
+ data.grammar_triggers.push_back({
+ COMMON_GRAMMAR_TRIGGER_TYPE_WORD,
+ "",
+ });
+ auto escaped_name = regex_escape(name);
+ data.grammar_triggers.push_back({
+ COMMON_GRAMMAR_TRIGGER_TYPE_PATTERN,
+ " alt_tags {
+ any_tool_call,
+ "\"\" space " + any_tool_call + " \"\"",
+ // The rest is just to accommodate common "good bad" outputs.
+ "\"\" space " + any_tool_call + " \"\"",
+ "\"\" space " + any_tool_call + " \"\"",
+ "\"\" space " + any_tool_call + " \"\"",
+ "\"\" space " + any_tool_call + " \"\"",
+ "\"\" space " + any_tool_call + " \"\"",
+ "\"\" space " + any_tool_call + " \"\"",
+ };
+ auto wrappable_tool_call = builder.add_rule("wrappable_tool_call", "( " + string_join(alt_tags, " | ") + " ) space");
+ tool_call_alts.push_back(wrappable_tool_call);
+ tool_call_alts.push_back(
+ "( \"```\\n\" | \"```json\\n\" | \"```xml\\n\" ) space " + wrappable_tool_call + " space \"```\" space ");
+ auto tool_call = builder.add_rule("tool_call", string_join(tool_call_alts, " | "));
+ builder.add_rule("root",
+ std::string(data.thinking_forced_open ? "( \"\" space )? " : "") +
+ (inputs.parallel_tool_calls ? "(" + tool_call + ")+" : tool_call));
+ // Trigger on some common known "good bad" outputs (only from the start and with a json that's about a specific argument name to avoid false positives)
+ data.grammar_triggers.push_back({
+ COMMON_GRAMMAR_TRIGGER_TYPE_PATTERN_FULL,
+ // If thinking_forced_open, then we capture the tag in the grammar,
+ // (important for required tool choice) and in the trigger's first capture (decides what is sent to the grammar)
+ std::string(data.thinking_forced_open ? "[\\s\\S]*?(\\s*)" : "(?:[\\s\\S]*?\\s*)?") + (
+ "(\\s*"
+ "(?:"
+ "||||)?"
+ "\\s*\\{\\s*\"name\"\\s*:\\s*\"(?:" + string_join(escaped_names, "|") + ")\""
+ ")"
+ ")[\\s\\S]*"
+ ),
+ });
+ data.preserved_tokens = {
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "```",
+ "```json",
+ "```xml",
+ };
+ });
+ }
- data.prompt = apply(tmpl, inputs.messages, inputs.tools.empty() ? json() : inputs.tools, inputs.add_generation_prompt);
- data.format = inputs.extract_reasoning ? COMMON_CHAT_FORMAT_HERMES_2_PRO_EXTRACT_REASONING : COMMON_CHAT_FORMAT_HERMES_2_PRO;
return data;
}
-static common_chat_msg common_chat_parse_hermes_2_pro(const std::string& input, bool extract_reasoning) {
- return handle_think_tag_prelude(input, extract_reasoning, [](const std::string & input) {
- static const std::regex open_regex(
- "(?:"
- "(```(?:xml|json)?\\n\\s*)?" // match 1 (block_start)
- "(" // match 2 (open_tag)
- "|"
- "|"
- "|"
- "|"
- "|"
- "|"
- "|"
+static void common_chat_parse_hermes_2_pro(common_chat_msg_parser & builder) {
+ builder.try_parse_reasoning("", "");
+ if (!builder.syntax().parse_tool_calls) {
+ builder.add_content(builder.consume_rest());
+ return;
+ }
+
+ static const common_regex open_regex(
+ "(?:"
+ "(```(?:xml|json)?\\n\\s*)?" // match 1 (block_start)
+ "(" // match 2 (open_tag)
+ ""
+ "|"
+ "|"
+ "|"
+ "|"
+ "|"
+ "|"
+ "|"
")?"
- "(\\s*\\{\\s*\"name\"\\s*:[\\s\\S]*)" // match 3 (named tool call + rest)
- ")"
- "|"
- "(?:]+)>" // match 4 (function name)
- "|)" // match 5 (function name again)
- "([\\s\\S]*)" // match 6 (function arguments + rest)})"
- );
+ "(\\s*\\{\\s*\"name\")" // match 3 (named tool call)
+ ")"
+ "|]+)>" // match 4 (function name)
+ "|" // match 5 (function name again)
+ );
- try {
- common_chat_msg msg;
- msg.role = "assistant";
+ if (auto res = builder.try_find_regex(open_regex)) {
+ const auto & block_start = res->groups[1];
+ std::string block_end = block_start.empty() ? "" : "```";
- std::string::const_iterator it = input.begin();
- const std::string::const_iterator end = input.end();
- std::smatch match;
+ const auto & open_tag = res->groups[2];
+ std::string close_tag;
- while (it != end) {
- if (std::regex_search(it, end, match, open_regex)) {
- // Add content before the match
- msg.content += std::string(it, match[0].first);
+ if (!res->groups[3].empty()) {
+ builder.move_to(res->groups[3].begin);
+ close_tag = open_tag.empty() ? "" : "" + builder.str(open_tag).substr(1);
- auto block_start = match[1].str();
- std::string block_end = block_start.empty() ? "" : "```";
+ if (auto tool_call = builder.try_consume_json_with_dumped_args({{"arguments"}})) {
+ if (!builder.add_tool_call(tool_call->value) || tool_call->is_partial) {
+ throw common_chat_msg_partial_exception("incomplete tool call");
+ }
+ builder.consume_spaces();
+ builder.consume_literal(close_tag);
+ builder.consume_spaces();
+ if (!block_end.empty()) {
+ builder.consume_literal(block_end);
+ builder.consume_spaces();
+ }
+ builder.add_content(builder.consume_rest());
+ } else {
+ throw common_chat_msg_partial_exception("failed to parse tool call");
+ }
+ } else {
+ auto function_name = builder.str(res->groups[4]);
+ if (function_name.empty()) {
+ function_name = builder.str(res->groups[5]);
+ }
+ GGML_ASSERT(!function_name.empty());
- auto open_tag = match[2].str();
- std::string close_tag;
+ close_tag = "";
- if (match[3].matched) {
- close_tag = open_tag.empty() ? "" : "" + open_tag.substr(1);
- auto json_it = match[3].first;
- json tool_call;
- if (parse_json(json_it, end, tool_call) && tool_call.contains("name") && tool_call.contains("arguments")) {
-
- msg.tool_calls.emplace_back(process_tool_call(tool_call));
- it = json_it; // Move iterator past parsed JSON
-
- // Handle close tags
- consume_spaces(it, end);
- if (!close_tag.empty() && !parse_literal(it, end, close_tag)) {
- throw std::runtime_error("Failed to parse closing tag");
- }
- consume_spaces(it, end);
- if (!block_end.empty() && !parse_literal(it, end, block_end)) {
- throw std::runtime_error("Failed to parse block end");
- }
- consume_spaces(it, end);
- } else {
- // Not a valid tool call, treat as content
- msg.content += std::string(match[0].first, match[0].second);
- it = match[0].second;
- }
- } else {
- auto function_name = match[4].str();
- if (function_name.empty()) {
- function_name = match[5].str();
- }
- GGML_ASSERT(!function_name.empty());
-
- close_tag = "";
- // Start parsing from after the opening tags
- auto json_it = match[6].first;
- json arguments;
- if (parse_json(json_it, end, arguments)) {
- msg.tool_calls.emplace_back(process_tool_call({
- {"name", function_name},
- {"arguments", arguments},
- }));
- it = json_it; // Move iterator past parsed JSON
-
- // Handle close tags
- consume_spaces(it, end);
- if (!close_tag.empty() && !parse_literal(it, end, close_tag)) {
- throw std::runtime_error("Failed to parse closing tag");
- }
- consume_spaces(it, end);
- if (!block_end.empty() && !parse_literal(it, end, block_end)) {
- throw std::runtime_error("Failed to parse block end");
- }
- consume_spaces(it, end);
- } else {
- // Not a valid tool call, treat as content
- msg.content += std::string(match[0].first, match[0].second);
- it = match[0].second;
- }
- }
- } else {
- // Add remaining content
- msg.content += std::string(it, end);
- break;
+ if (auto arguments = builder.try_consume_json_with_dumped_args({{}})) {
+ if (!builder.add_tool_call(function_name, "", arguments->value) || arguments->is_partial) {
+ throw common_chat_msg_partial_exception("incomplete tool call");
+ }
+ builder.consume_spaces();
+ builder.consume_literal(close_tag);
+ builder.consume_spaces();
+ if (!block_end.empty()) {
+ builder.consume_literal(block_end);
+ builder.consume_spaces();
}
}
- return msg;
- } catch (const std::exception & e) {
- LOG_ERR("Failed to parse hermes 2 pro input: %s\n", e.what());
- common_chat_msg msg;
- msg.role = "assistant";
- msg.content = input;
- return msg;
+ builder.add_content(builder.consume_rest());
}
- });
+ } else {
+ builder.add_content(builder.consume_rest());
+ }
}
static common_chat_params common_chat_params_init_without_tools(const common_chat_template & tmpl, const struct templates_params & inputs) {
@@ -1590,9 +1718,10 @@ static common_chat_params common_chat_templates_apply_jinja(
const auto & caps = tmpl.original_caps();
params.messages = common_chat_msgs_to_json_oaicompat(inputs.messages, /* concat_text= */ !tmpl.original_caps().requires_typed_content);
params.add_generation_prompt = inputs.add_generation_prompt;
- params.extract_reasoning = inputs.extract_reasoning;
params.tool_choice = inputs.tool_choice;
+ params.enable_thinking = inputs.enable_thinking;
params.grammar = inputs.grammar;
+ params.now = inputs.now;
if (!inputs.json_schema.empty()) {
params.json_schema = json::parse(inputs.json_schema);
}
@@ -1624,7 +1753,7 @@ static common_chat_params common_chat_templates_apply_jinja(
}
// Hermes 2/3 Pro, Qwen 2.5 Instruct (w/ tools)
- if (src.find("") != std::string::npos && params.json_schema.is_null() && params.tools.is_array() && params.json_schema.is_null()) {
+ if (src.find("") != std::string::npos && params.json_schema.is_null()) {
return common_chat_params_init_hermes_2_pro(tmpl, params);
}
@@ -1644,21 +1773,21 @@ static common_chat_params common_chat_templates_apply_jinja(
return common_chat_params_init_firefunction_v2(tmpl, params);
}
- // Plain handler (no tools)
- if (params.tools.is_null() || inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_NONE) {
- return common_chat_params_init_without_tools(tmpl, params);
- }
-
// Functionary v3.1 (w/ tools)
if (src.find("<|start_header_id|>") != std::string::npos
&& src.find("ipython<|end_header_id|>") != std::string::npos) {
auto allow_python_tag_builtin_tools = src.find("<|python_tag|>") != std::string::npos;
- return common_chat_params_init_llama_3_1_tool_calls(tmpl, params, allow_python_tag_builtin_tools);
+ return common_chat_params_init_llama_3_x(tmpl, params, allow_python_tag_builtin_tools);
+ }
+
+ // Plain handler (no tools)
+ if (params.tools.is_null() || inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_NONE) {
+ return common_chat_params_init_without_tools(tmpl, params);
}
// Mistral Nemo (w/ tools)
@@ -1709,7 +1838,7 @@ static common_chat_params common_chat_templates_apply_legacy(
if (res < 0) {
// if the custom "tmpl" is not supported, we throw an error
// this is a bit redundant (for good), since we're not sure if user validated the custom template with llama_chat_verify_template()
- throw std::runtime_error("this custom template is not supported");
+ throw std::runtime_error("this custom template is not supported, try using --jinja");
}
// if it turns out that our buffer is too small, we resize it
@@ -1738,44 +1867,66 @@ common_chat_params common_chat_templates_apply(
: common_chat_templates_apply_legacy(tmpls, inputs);
}
-static common_chat_msg common_chat_parse_content_only(const std::string & input) {
- common_chat_msg msg;
- msg.role = "assistant";
- msg.content = input;
- return msg;
+static void common_chat_parse_content_only(common_chat_msg_parser & builder) {
+ builder.add_content(builder.consume_rest());
}
-common_chat_msg common_chat_parse(const std::string & input, common_chat_format format) {
- switch (format) {
+static void common_chat_parse(common_chat_msg_parser & builder) {
+ LOG_DBG("Parsing input with format %s: %s\n", common_chat_format_name(builder.syntax().format), builder.input().c_str());
+
+ switch (builder.syntax().format) {
case COMMON_CHAT_FORMAT_CONTENT_ONLY:
- return common_chat_parse_content_only(input);
+ common_chat_parse_content_only(builder);
+ break;
case COMMON_CHAT_FORMAT_GENERIC:
- return common_chat_parse_generic(input);
+ common_chat_parse_generic(builder);
+ break;
case COMMON_CHAT_FORMAT_MISTRAL_NEMO:
- return common_chat_parse_mistral_nemo(input);
+ common_chat_parse_mistral_nemo(builder);
+ break;
case COMMON_CHAT_FORMAT_LLAMA_3_X:
- return common_chat_parse_llama_3_1(input);
+ common_chat_parse_llama_3_1(builder);
+ break;
case COMMON_CHAT_FORMAT_LLAMA_3_X_WITH_BUILTIN_TOOLS:
- return common_chat_parse_llama_3_1(input, /* with_builtin_tools= */ true);
+ common_chat_parse_llama_3_1(builder, /* with_builtin_tools= */ true);
+ break;
case COMMON_CHAT_FORMAT_DEEPSEEK_R1:
- return common_chat_parse_deepseek_r1(input, /* extract_reasoning= */ false);
- case COMMON_CHAT_FORMAT_DEEPSEEK_R1_EXTRACT_REASONING:
- return common_chat_parse_deepseek_r1(input, /* extract_reasoning= */ true);
+ common_chat_parse_deepseek_r1(builder);
+ break;
case COMMON_CHAT_FORMAT_FUNCTIONARY_V3_2:
- return common_chat_parse_functionary_v3_2(input);
+ common_chat_parse_functionary_v3_2(builder);
+ break;
case COMMON_CHAT_FORMAT_FUNCTIONARY_V3_1_LLAMA_3_1:
- return common_chat_parse_functionary_v3_1_llama_3_1(input);
+ common_chat_parse_functionary_v3_1_llama_3_1(builder);
+ break;
case COMMON_CHAT_FORMAT_HERMES_2_PRO:
- return common_chat_parse_hermes_2_pro(input, /* extract_reasoning= */ false);
- case COMMON_CHAT_FORMAT_HERMES_2_PRO_EXTRACT_REASONING:
- return common_chat_parse_hermes_2_pro(input, /* extract_reasoning= */ true);
+ common_chat_parse_hermes_2_pro(builder);
+ break;
case COMMON_CHAT_FORMAT_FIREFUNCTION_V2:
- return common_chat_parse_firefunction_v2(input);
+ common_chat_parse_firefunction_v2(builder);
+ break;
case COMMON_CHAT_FORMAT_COMMAND_R7B:
- return common_chat_parse_command_r7b(input, /* extract_reasoning= */ false);
- case COMMON_CHAT_FORMAT_COMMAND_R7B_EXTRACT_REASONING:
- return common_chat_parse_command_r7b(input, /* extract_reasoning= */ true);
+ common_chat_parse_command_r7b(builder);
+ break;
default:
- throw std::runtime_error("Unsupported format: " + common_chat_format_name(format));
+ throw std::runtime_error(std::string("Unsupported format: ") + common_chat_format_name(builder.syntax().format));
}
+ builder.finish();
+}
+
+common_chat_msg common_chat_parse(const std::string & input, bool is_partial, const common_chat_syntax & syntax) {
+ common_chat_msg_parser builder(input, is_partial, syntax);
+ try {
+ common_chat_parse(builder);
+ } catch (const common_chat_msg_partial_exception & ex) {
+ LOG_DBG("Partial parse: %s\n", ex.what());
+ if (!is_partial) {
+ builder.clear_tools();
+ builder.move_to(0);
+ common_chat_parse_content_only(builder);
+ }
+ }
+ auto msg = builder.result();
+ LOG_DBG("Parsed message: %s\n", common_chat_msgs_to_json_oaicompat({msg}).at(0).dump().c_str());
+ return msg;
}
diff --git a/common/chat.h b/common/chat.h
index 9aad84e88..9f59e6b08 100644
--- a/common/chat.h
+++ b/common/chat.h
@@ -3,6 +3,8 @@
#pragma once
#include "common.h"
+#include
+#include
#include
#include
@@ -12,11 +14,19 @@ struct common_chat_tool_call {
std::string name;
std::string arguments;
std::string id;
+
+ bool operator==(const common_chat_tool_call & other) const {
+ return name == other.name && arguments == other.arguments && id == other.id;
+ }
};
struct common_chat_msg_content_part {
std::string type;
std::string text;
+
+ bool operator==(const common_chat_msg_content_part & other) const {
+ return type == other.type && text == other.text;
+ }
};
struct common_chat_msg {
@@ -27,6 +37,51 @@ struct common_chat_msg {
std::string reasoning_content;
std::string tool_name;
std::string tool_call_id;
+
+ template T to_json_oaicompat() const;
+
+ bool empty() const {
+ return content.empty() && content_parts.empty() && tool_calls.empty() && reasoning_content.empty() && tool_name.empty() && tool_call_id.empty();
+ }
+ void ensure_tool_call_ids_set(std::vector & ids_cache, const std::function & gen_tool_call_id) {
+ for (auto i = 0u; i < tool_calls.size(); i++) {
+ if (ids_cache.size() <= i) {
+ auto id = tool_calls[i].id;
+ if (id.empty()) {
+ id = gen_tool_call_id();
+ }
+ ids_cache.push_back(id);
+ }
+ tool_calls[i].id = ids_cache[i];
+ }
+ }
+ bool operator==(const common_chat_msg & other) const {
+ return role == other.role
+ && content == other.content
+ && content_parts == other.content_parts
+ && tool_calls == other.tool_calls
+ && reasoning_content == other.reasoning_content
+ && tool_name == other.tool_name
+ && tool_call_id == other.tool_call_id;
+ }
+ bool operator!=(const common_chat_msg & other) const {
+ return !(*this == other);
+ }
+};
+
+struct common_chat_msg_diff {
+ std::string reasoning_content_delta;
+ std::string content_delta;
+ size_t tool_call_index = std::string::npos;
+ common_chat_tool_call tool_call_delta;
+
+ static std::vector compute_diffs(const common_chat_msg & previous_msg, const common_chat_msg & new_msg);
+
+ bool operator==(const common_chat_msg_diff & other) const {
+ return content_delta == other.content_delta
+ && tool_call_index == other.tool_call_index
+ && tool_call_delta == other.tool_call_delta;
+ }
};
struct common_chat_tool {
@@ -48,14 +103,11 @@ enum common_chat_format {
COMMON_CHAT_FORMAT_LLAMA_3_X,
COMMON_CHAT_FORMAT_LLAMA_3_X_WITH_BUILTIN_TOOLS,
COMMON_CHAT_FORMAT_DEEPSEEK_R1,
- COMMON_CHAT_FORMAT_DEEPSEEK_R1_EXTRACT_REASONING,
COMMON_CHAT_FORMAT_FIREFUNCTION_V2,
COMMON_CHAT_FORMAT_FUNCTIONARY_V3_2,
COMMON_CHAT_FORMAT_FUNCTIONARY_V3_1_LLAMA_3_1,
COMMON_CHAT_FORMAT_HERMES_2_PRO,
- COMMON_CHAT_FORMAT_HERMES_2_PRO_EXTRACT_REASONING,
COMMON_CHAT_FORMAT_COMMAND_R7B,
- COMMON_CHAT_FORMAT_COMMAND_R7B_EXTRACT_REASONING,
COMMON_CHAT_FORMAT_COUNT, // Not a format, just the # formats
};
@@ -70,7 +122,9 @@ struct common_chat_templates_inputs {
std::vector tools;
common_chat_tool_choice tool_choice = COMMON_CHAT_TOOL_CHOICE_AUTO;
bool parallel_tool_calls = false;
- bool extract_reasoning = true;
+ common_reasoning_format reasoning_format = COMMON_REASONING_FORMAT_NONE;
+ bool enable_thinking = true;
+ std::chrono::system_clock::time_point now = std::chrono::system_clock::now();
};
struct common_chat_params {
@@ -78,11 +132,21 @@ struct common_chat_params {
std::string prompt;
std::string grammar;
bool grammar_lazy = false;
+ bool thinking_forced_open = false;
std::vector grammar_triggers;
std::vector preserved_tokens;
std::vector additional_stops;
};
+struct common_chat_syntax {
+ common_chat_format format = COMMON_CHAT_FORMAT_CONTENT_ONLY;
+ common_reasoning_format reasoning_format = COMMON_REASONING_FORMAT_NONE;
+ // Whether reasoning_content should be inlined in the content (e.g. for reasoning_format=deepseek in stream mode)
+ bool reasoning_in_content = false;
+ bool thinking_forced_open = false;
+ bool parse_tool_calls = true;
+};
+
// Check if the template supplied via "--chat-template" is supported or not. Returns true if it's valid
bool common_chat_verify_template(const std::string & tmpl, bool use_jinja);
@@ -119,8 +183,9 @@ std::string common_chat_format_example(
const struct common_chat_templates * tmpls,
bool use_jinja);
-std::string common_chat_format_name(common_chat_format format);
-common_chat_msg common_chat_parse( const std::string & input, common_chat_format format);
+const char* common_chat_format_name(common_chat_format format);
+const char* common_reasoning_format_name(common_reasoning_format format);
+common_chat_msg common_chat_parse(const std::string & input, bool is_partial, const common_chat_syntax & syntax);
common_chat_tool_choice common_chat_tool_choice_parse_oaicompat(const std::string & tool_choice);
@@ -133,3 +198,5 @@ template T common_chat_msgs_to_json_oaicompat(const std::vector std::vector common_chat_tools_parse_oaicompat(const T & tools);
template T common_chat_tools_to_json_oaicompat(const std::vector & tools);
+
+template T common_chat_msg_diff_to_json_oaicompat(const common_chat_msg_diff & diff);
diff --git a/common/cmake/build-info-gen-cpp.cmake b/common/cmake/build-info-gen-cpp.cmake
deleted file mode 100644
index fbc92b52c..000000000
--- a/common/cmake/build-info-gen-cpp.cmake
+++ /dev/null
@@ -1,24 +0,0 @@
-include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/build-info.cmake)
-
-set(TEMPLATE_FILE "${CMAKE_CURRENT_SOURCE_DIR}/common/build-info.cpp.in")
-set(OUTPUT_FILE "${CMAKE_CURRENT_SOURCE_DIR}/common/build-info.cpp")
-
-# Only write the build info if it changed
-if(EXISTS ${OUTPUT_FILE})
- file(READ ${OUTPUT_FILE} CONTENTS)
- string(REGEX MATCH "LLAMA_COMMIT = \"([^\"]*)\";" _ ${CONTENTS})
- set(OLD_COMMIT ${CMAKE_MATCH_1})
- string(REGEX MATCH "LLAMA_COMPILER = \"([^\"]*)\";" _ ${CONTENTS})
- set(OLD_COMPILER ${CMAKE_MATCH_1})
- string(REGEX MATCH "LLAMA_BUILD_TARGET = \"([^\"]*)\";" _ ${CONTENTS})
- set(OLD_TARGET ${CMAKE_MATCH_1})
- if (
- NOT OLD_COMMIT STREQUAL BUILD_COMMIT OR
- NOT OLD_COMPILER STREQUAL BUILD_COMPILER OR
- NOT OLD_TARGET STREQUAL BUILD_TARGET
- )
- configure_file(${TEMPLATE_FILE} ${OUTPUT_FILE})
- endif()
-else()
- configure_file(${TEMPLATE_FILE} ${OUTPUT_FILE})
-endif()
diff --git a/common/common.cpp b/common/common.cpp
index bd20af233..e4e71ad13 100644
--- a/common/common.cpp
+++ b/common/common.cpp
@@ -203,6 +203,7 @@ bool set_process_priority(enum ggml_sched_priority prio) {
DWORD p = NORMAL_PRIORITY_CLASS;
switch (prio) {
+ case GGML_SCHED_PRIO_LOW: p = BELOW_NORMAL_PRIORITY_CLASS; break;
case GGML_SCHED_PRIO_NORMAL: p = NORMAL_PRIORITY_CLASS; break;
case GGML_SCHED_PRIO_MEDIUM: p = ABOVE_NORMAL_PRIORITY_CLASS; break;
case GGML_SCHED_PRIO_HIGH: p = HIGH_PRIORITY_CLASS; break;
@@ -228,6 +229,7 @@ bool set_process_priority(enum ggml_sched_priority prio) {
int p = 0;
switch (prio) {
+ case GGML_SCHED_PRIO_LOW: p = 5; break;
case GGML_SCHED_PRIO_NORMAL: p = 0; break;
case GGML_SCHED_PRIO_MEDIUM: p = -5; break;
case GGML_SCHED_PRIO_HIGH: p = -10; break;
@@ -443,9 +445,28 @@ void string_replace_all(std::string & s, const std::string & search, const std::
s = std::move(builder);
}
+bool string_ends_with(const std::string_view & str, const std::string_view & suffix) {
+ return str.size() >= suffix.size() && str.compare(str.size()-suffix.size(), suffix.size(), suffix) == 0;
+}
+size_t string_find_partial_stop(const std::string_view & str, const std::string_view & stop) {
+ if (!str.empty() && !stop.empty()) {
+ const char text_last_char = str.back();
+ for (int64_t char_index = stop.size() - 1; char_index >= 0; char_index--) {
+ if (stop[char_index] == text_last_char) {
+ const auto current_partial = stop.substr(0, char_index + 1);
+ if (string_ends_with(str, current_partial)) {
+ return str.size() - char_index - 1;
+ }
+ }
+ }
+ }
+
+ return std::string::npos;
+}
+
std::string regex_escape(const std::string & s) {
static const std::regex special_chars("[.^$|()*+?\\[\\]{}\\\\]");
- return std::regex_replace(s, special_chars, "\\$0");
+ return std::regex_replace(s, special_chars, "\\$&");
}
std::string string_join(const std::vector & values, const std::string & separator) {
@@ -685,11 +706,17 @@ bool fs_validate_filename(const std::string & filename) {
// disable C++17 deprecation warning for std::codecvt_utf8
# pragma clang diagnostic push
# pragma clang diagnostic ignored "-Wdeprecated-declarations"
+#elif defined(__GNUC__)
+# pragma GCC diagnostic push
+# pragma GCC diagnostic ignored "-Wdeprecated-declarations"
#endif
+
std::wstring_convert, char32_t> converter;
#if defined(__clang__)
# pragma clang diagnostic pop
+#elif defined(__GNUC__)
+# pragma GCC diagnostic pop
#endif
filename_utf32 = converter.from_bytes(filename);
@@ -746,6 +773,9 @@ bool fs_validate_filename(const std::string & filename) {
return true;
}
+#include
+
+
// returns true if successful, false otherwise
bool fs_create_directory_with_parents(const std::string & path) {
#ifdef _WIN32
@@ -763,9 +793,16 @@ bool fs_create_directory_with_parents(const std::string & path) {
// process path from front to back, procedurally creating directories
while ((pos_slash = path.find('\\', pos_slash)) != std::string::npos) {
const std::wstring subpath = wpath.substr(0, pos_slash);
- const wchar_t * test = subpath.c_str();
- const bool success = CreateDirectoryW(test, NULL);
+ pos_slash += 1;
+
+ // skip the drive letter, in some systems it can return an access denied error
+ if (subpath.length() == 2 && subpath[1] == ':') {
+ continue;
+ }
+
+ const bool success = CreateDirectoryW(subpath.c_str(), NULL);
+
if (!success) {
const DWORD error = GetLastError();
@@ -779,8 +816,6 @@ bool fs_create_directory_with_parents(const std::string & path) {
return false;
}
}
-
- pos_slash += 1;
}
return true;
@@ -830,7 +865,7 @@ std::string fs_get_cache_directory() {
if (getenv("LLAMA_CACHE")) {
cache_directory = std::getenv("LLAMA_CACHE");
} else {
-#if defined(__linux__) || defined(__FreeBSD__) || defined(_AIX)
+#if defined(__linux__) || defined(__FreeBSD__) || defined(_AIX) || defined(__OpenBSD__)
if (std::getenv("XDG_CACHE_HOME")) {
cache_directory = std::getenv("XDG_CACHE_HOME");
} else {
@@ -876,31 +911,6 @@ struct common_init_result common_init_from_params(common_params & params) {
const llama_vocab * vocab = llama_model_get_vocab(model);
- if (params.reranking) {
- bool ok = true;
-
- if (llama_vocab_bos(vocab) == LLAMA_TOKEN_NULL) {
- LOG_WRN("%s: warning: vocab does not have a BOS token, reranking will not work\n", __func__);
- ok = false;
- }
-
- if (llama_vocab_eos(vocab) == LLAMA_TOKEN_NULL) {
- LOG_WRN("%s: warning: vocab does not have an EOS token, reranking will not work\n", __func__);
- ok = false;
- }
-
- if (llama_vocab_sep(vocab) == LLAMA_TOKEN_NULL) {
- LOG_WRN("%s: warning: vocab does not have a SEP token, reranking will not work\n", __func__);
- ok = false;
- }
-
- if (!ok) {
- llama_model_free(model);
-
- return iparams;
- }
- }
-
auto cparams = common_context_params_to_llama(params);
llama_context * lctx = llama_init_from_model(model, cparams);
@@ -910,7 +920,7 @@ struct common_init_result common_init_from_params(common_params & params) {
return iparams;
}
- if (params.ctx_shift && !llama_kv_self_can_shift(lctx)) {
+ if (params.ctx_shift && !llama_memory_can_shift(llama_get_memory(lctx))) {
LOG_WRN("%s: KV cache shifting is not supported for this context, disabling KV cache shifting\n", __func__);
params.ctx_shift = false;
}
@@ -942,6 +952,35 @@ struct common_init_result common_init_from_params(common_params & params) {
}
}
+ if (llama_pooling_type(lctx) == LLAMA_POOLING_TYPE_RANK) {
+ bool ok = true;
+
+ if (llama_vocab_bos(vocab) == LLAMA_TOKEN_NULL) {
+ LOG_WRN("%s: warning: vocab does not have a BOS token, reranking will not work\n", __func__);
+ ok = false;
+ }
+
+ bool has_eos = llama_vocab_eos(vocab) != LLAMA_TOKEN_NULL;
+ bool has_sep = llama_vocab_sep(vocab) != LLAMA_TOKEN_NULL;
+
+ if (!has_eos && !has_sep) {
+ LOG_WRN("%s: warning: vocab does not have an EOS token or SEP token, reranking will not work\n", __func__);
+ ok = false;
+ } else if (!has_eos) {
+ LOG_WRN("%s: warning: vocab does not have an EOS token, using SEP token as fallback\n", __func__);
+ } else if (!has_sep) {
+ LOG_WRN("%s: warning: vocab does not have a SEP token, reranking will not work\n", __func__);
+ ok = false;
+ }
+
+ if (!ok) {
+ llama_free(lctx);
+ llama_model_free(model);
+
+ return iparams;
+ }
+ }
+
// load and optionally apply lora adapters
for (auto & la : params.lora_adapters) {
llama_adapter_lora_ptr lora;
@@ -1017,7 +1056,7 @@ struct common_init_result common_init_from_params(common_params & params) {
if (llama_model_has_decoder(model)) {
llama_decode(lctx, llama_batch_get_one(tmp.data(), std::min(tmp.size(), (size_t) params.n_batch)));
}
- llama_kv_self_clear(lctx);
+ llama_memory_clear(llama_get_memory(lctx), true);
llama_synchronize(lctx);
llama_perf_context_reset(lctx);
llama_set_warmup(lctx, false);
@@ -1083,6 +1122,9 @@ struct llama_model_params common_model_params_to_llama(common_params & params) {
mparams.tensor_buft_overrides = params.tensor_buft_overrides.data();
}
+ mparams.progress_callback = params.load_progress_callback;
+ mparams.progress_callback_user_data = params.load_progress_callback_user_data;
+
return mparams;
}
@@ -1113,11 +1155,8 @@ struct llama_context_params common_context_params_to_llama(const common_params &
cparams.offload_kqv = !params.no_kv_offload;
cparams.flash_attn = params.flash_attn;
cparams.no_perf = params.no_perf;
-
- if (params.reranking) {
- cparams.embeddings = true;
- cparams.pooling_type = LLAMA_POOLING_TYPE_RANK;
- }
+ cparams.op_offload = !params.no_op_offload;
+ cparams.swa_full = params.swa_full;
cparams.type_k = params.cache_type_k;
cparams.type_v = params.cache_type_v;
@@ -1251,6 +1290,9 @@ std::vector common_tokenize(
int n_tokens = text.length() + 2 * add_special;
std::vector result(n_tokens);
n_tokens = llama_tokenize(vocab, text.data(), text.length(), result.data(), result.size(), add_special, parse_special);
+ if (n_tokens == std::numeric_limits::min()) {
+ throw std::runtime_error("Tokenization failed: input text too large, tokenization result exceeds int32_t limit");
+ }
if (n_tokens < 0) {
result.resize(-n_tokens);
int check = llama_tokenize(vocab, text.data(), text.length(), result.data(), result.size(), add_special, parse_special);
@@ -1305,81 +1347,6 @@ std::string common_detokenize(const struct llama_vocab * vocab, const std::vecto
return text;
}
-//
-// KV cache utils
-//
-
-void common_kv_cache_dump_view(const llama_kv_cache_view & view, int row_size) {
- static const char slot_chars[] = ".123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+";
-
- printf("=== Dumping KV cache. total cells %d, max sequences per cell %d, populated cells %d, total tokens in cache %d, largest empty slot=%d @ %d",
- view.n_cells, view.n_seq_max, view.used_cells, view.token_count, view.max_contiguous, view.max_contiguous_idx);
-
- llama_kv_cache_view_cell * c_curr = view.cells;
- llama_seq_id * cs_curr = view.cells_sequences;
-
- for (int i = 0; i < view.n_cells; i++, c_curr++, cs_curr += view.n_seq_max) {
- if (i % row_size == 0) {
- printf("\n%5d: ", i);
- }
- int seq_count = 0;
- for (int j = 0; j < view.n_seq_max; j++) {
- if (cs_curr[j] >= 0) { seq_count++; }
- }
- putchar(slot_chars[std::min(sizeof(slot_chars) - 2, size_t(seq_count))]);
- }
-
- printf("\n=== Done dumping\n");
-}
-
-void common_kv_cache_dump_view_seqs(const llama_kv_cache_view & view, int row_size) {
- static const char slot_chars[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
-
- printf("=== Dumping KV cache. total cells %d, max sequences per cell %d, populated cells %d, total tokens in cache %d, largest empty slot=%d @ %d\n",
- view.n_cells, view.n_seq_max, view.used_cells, view.token_count, view.max_contiguous, view.max_contiguous_idx);
-
- std::unordered_map seqs;
- llama_kv_cache_view_cell * c_curr = view.cells;
- llama_seq_id * cs_curr = view.cells_sequences;
-
- for (int i = 0; i < view.n_cells; i++, c_curr++, cs_curr += view.n_seq_max) {
- for (int j = 0; j < view.n_seq_max; j++) {
- if (cs_curr[j] < 0) { continue; }
- if (seqs.find(cs_curr[j]) == seqs.end()) {
- if (seqs.size() + 1 >= sizeof(slot_chars)) { break; }
- const size_t sz = seqs.size();
- seqs[cs_curr[j]] = sz;
- }
- }
- if (seqs.size() + 1 >= sizeof(slot_chars)) { break; }
- }
-
- printf("=== Sequence legend: ");
- for (const auto & it : seqs) {
- printf("%zu=%d, ", it.second, it.first);
- }
- printf("'+'=other sequence ids");
-
- c_curr = view.cells;
- cs_curr = view.cells_sequences;
- for (int i = 0; i < view.n_cells; i++, c_curr++, cs_curr += view.n_seq_max) {
- if (i % row_size == 0) {
- printf("\n%5d: ", i);
- }
- for (int j = 0; j < view.n_seq_max; j++) {
- if (cs_curr[j] >= 0) {
- const auto & it = seqs.find(cs_curr[j]);
- putchar(it != seqs.end() ? int(slot_chars[it->second]) : '+');
- } else {
- putchar('.');
- }
- }
- putchar(' ');
- }
-
- printf("\n=== Done dumping\n");
-}
-
//
// Embedding utils
//
@@ -1564,3 +1531,20 @@ common_control_vector_data common_control_vector_load(const std::vector & tokens, int64_t stride) {
+ const int64_t ne_datapoint = llama_n_ctx(ctx);
+ const int64_t ndata = (tokens.size() - ne_datapoint - 1) / stride;
+ ggml_opt_dataset_t result = ggml_opt_dataset_init(
+ GGML_TYPE_I32, GGML_TYPE_I32, ne_datapoint, ne_datapoint, ndata, /*ndata_shard =*/ 1);
+
+ llama_token * data = (llama_token *) ggml_opt_dataset_data(result)->data;
+ llama_token * labels = (llama_token *) ggml_opt_dataset_labels(result)->data;
+
+ for (int64_t idata = 0; idata < ndata; ++idata) {
+ memcpy(data + idata*ne_datapoint, tokens.data() + idata*stride + 0, ne_datapoint*sizeof(llama_token));
+ memcpy(labels + idata*ne_datapoint, tokens.data() + idata*stride + 1, ne_datapoint*sizeof(llama_token));
+ }
+
+ return result;
+}
diff --git a/common/common.h b/common/common.h
index d051d4ec9..e08a59eae 100644
--- a/common/common.h
+++ b/common/common.h
@@ -6,6 +6,7 @@
#include
#include
+#include
#include
#include
@@ -75,7 +76,7 @@ enum llama_example {
LLAMA_EXAMPLE_SERVER,
LLAMA_EXAMPLE_CVECTOR_GENERATOR,
LLAMA_EXAMPLE_EXPORT_LORA,
- LLAMA_EXAMPLE_LLAVA,
+ LLAMA_EXAMPLE_MTMD,
LLAMA_EXAMPLE_LOOKUP,
LLAMA_EXAMPLE_PARALLEL,
LLAMA_EXAMPLE_TTS,
@@ -114,7 +115,7 @@ enum common_grammar_trigger_type {
COMMON_GRAMMAR_TRIGGER_TYPE_TOKEN,
COMMON_GRAMMAR_TRIGGER_TYPE_WORD,
COMMON_GRAMMAR_TRIGGER_TYPE_PATTERN,
- COMMON_GRAMMAR_TRIGGER_TYPE_PATTERN_START,
+ COMMON_GRAMMAR_TRIGGER_TYPE_PATTERN_FULL,
};
struct common_grammar_trigger {
@@ -198,6 +199,9 @@ struct common_params_speculative {
float p_split = 0.1f; // speculative decoding split probability
float p_min = 0.75f; // minimum speculative decoding probability (greedy)
+ ggml_type cache_type_k = GGML_TYPE_F16; // KV cache data type for the K
+ ggml_type cache_type_v = GGML_TYPE_F16; // KV cache data type for the V
+
struct cpu_params cpuparams;
struct cpu_params cpuparams_batch;
@@ -214,7 +218,8 @@ struct common_params_vocoder {
enum common_reasoning_format {
COMMON_REASONING_FORMAT_NONE,
- COMMON_REASONING_FORMAT_DEEPSEEK, // Extract thinking tag contents and return as `message.reasoning_content`
+ COMMON_REASONING_FORMAT_DEEPSEEK_LEGACY, // Extract thinking tag contents and return as `message.reasoning_content`, or leave inline in tags in stream mode
+ COMMON_REASONING_FORMAT_DEEPSEEK, // Extract thinking tag contents and return as `message.reasoning_content`, including in streaming deltas.
};
struct common_params {
@@ -290,6 +295,7 @@ struct common_params {
int32_t verbosity = 0;
int32_t control_vector_layer_start = -1; // layer range for control vector
int32_t control_vector_layer_end = -1; // layer range for control vector
+ bool offline = false;
int32_t ppl_stride = 0; // stride for perplexity calculations. If left at 0, the pre-existing approach will be used.
int32_t ppl_output_type = 0; // = 0 -> ppl output is as usual, = 1 -> ppl output is num_tokens, ppl, one per line
@@ -322,16 +328,17 @@ struct common_params {
bool flash_attn = false; // flash attention
bool no_perf = false; // disable performance metrics
bool ctx_shift = true; // context shift on inifinite text generation
+ bool swa_full = false; // use full-size SWA cache (https://github.com/ggml-org/llama.cpp/pull/13194#issuecomment-2868343055)
bool input_prefix_bos = false; // prefix BOS to user inputs, preceding input_prefix
bool use_mmap = true; // use mmap for faster loads
bool use_mlock = false; // use mlock to keep model in memory
bool verbose_prompt = false; // print prompt tokens before generation
bool display_prompt = true; // print prompt before generation
- bool dump_kv_cache = false; // dump the KV cache contents for debugging purposes
bool no_kv_offload = false; // disable KV offloading
bool warmup = true; // warmup run
bool check_tensors = false; // validate tensor data
+ bool no_op_offload = false; // globally disable offload host tensor operations to device
bool single_turn = false; // single turn chat conversation
@@ -351,7 +358,7 @@ struct common_params {
int32_t embd_normalize = 2; // normalisation for embeddings (-1=none, 0=max absolute int16, 1=taxicab, 2=euclidean, >2=p-norm)
std::string embd_out = ""; // empty = default, "array" = [[],[]...], "json" = openai style, "json+" = same "json" + cosine similarity matrix
std::string embd_sep = "\n"; // separator of embeddings
- bool reranking = false; // enable reranking support on server
+ std::string cls_sep = "\t"; // separator of classification sequences
// server params
int32_t port = 8080; // server listens on this network port
@@ -366,6 +373,8 @@ struct common_params {
bool use_jinja = false; // NOLINT
bool enable_chat_template = true;
common_reasoning_format reasoning_format = COMMON_REASONING_FORMAT_DEEPSEEK;
+ int reasoning_budget = -1;
+ bool prefill_assistant = true; // if true, any trailing assistant message will be prefilled into the response
std::vector api_keys;
@@ -425,6 +434,11 @@ struct common_params {
// common params
std::string out_file; // output filename for all example programs
+ // optional callback for model loading progress and cancellation:
+ // called with a progress value between 0.0 and 1.0.
+ // return false from callback to abort model loading or true to continue
+ llama_progress_callback load_progress_callback = NULL;
+ void * load_progress_callback_user_data = NULL;
};
// call once at the start of a program if it uses libcommon
@@ -502,10 +516,9 @@ static bool string_starts_with(const std::string & str,
return str.rfind(prefix, 0) == 0;
}
-static bool string_ends_with(const std::string & str,
- const std::string & suffix) { // While we wait for C++20's std::string::ends_with...
- return str.size() >= suffix.size() && str.compare(str.size()-suffix.size(), suffix.size(), suffix) == 0;
-}
+// While we wait for C++20's std::string::ends_with...
+bool string_ends_with(const std::string_view & str, const std::string_view & suffix);
+size_t string_find_partial_stop(const std::string_view & str, const std::string_view & stop);
bool string_parse_kv_override(const char * data, std::vector & overrides);
void string_process_escapes(std::string & input);
@@ -614,16 +627,6 @@ std::string common_detokenize(
const std::vector & tokens,
bool special = true);
-//
-// KV cache utils
-//
-
-// Dump the KV cache view with the number of sequences per cell.
-void common_kv_cache_dump_view(const llama_kv_cache_view & view, int row_size = 80);
-
-// Dump the KV cache view showing individual sequences in each cell (long output).
-void common_kv_cache_dump_view_seqs(const llama_kv_cache_view & view, int row_size = 40);
-
//
// Embedding utils
//
@@ -665,3 +668,9 @@ const char * const LLM_KV_SPLIT_COUNT = "split.count";
const char * const LLM_KV_SPLIT_TENSORS_COUNT = "split.tensors.count";
}
+
+//
+// training utils
+//
+
+ggml_opt_dataset_t common_opt_dataset_init(struct llama_context * ctx, const std::vector & tokens, int64_t stride);
diff --git a/common/json-partial.cpp b/common/json-partial.cpp
new file mode 100644
index 000000000..d9d916998
--- /dev/null
+++ b/common/json-partial.cpp
@@ -0,0 +1,256 @@
+#include "json-partial.h"
+
+#include "log.h"
+
+#include
+
+#include
+
+using json = nlohmann::ordered_json;
+
+enum common_json_stack_element_type {
+ COMMON_JSON_STACK_ELEMENT_OBJECT,
+ COMMON_JSON_STACK_ELEMENT_KEY,
+ COMMON_JSON_STACK_ELEMENT_ARRAY,
+};
+
+struct common_json_stack_element {
+ common_json_stack_element_type type;
+ std::string key;
+};
+
+bool common_json_parse(
+ const std::string & input,
+ const std::string & healing_marker,
+ common_json & out)
+{
+ std::string::const_iterator it = input.begin();
+ const auto end = input.end();
+ return common_json_parse(it, end, healing_marker, out);
+}
+
+bool common_json_parse(
+ std::string::const_iterator & it,
+ const std::string::const_iterator & end,
+ const std::string & healing_marker,
+ common_json & out)
+{
+ // // https://json.nlohmann.me/features/parsing/sax_interface/
+ struct json_error_locator : public nlohmann::json_sax {
+ std::size_t position;
+ bool found_error;
+ std::string last_token;
+ std::string exception_message;
+ std::vector stack;
+
+ json_error_locator() : position(0), found_error(false) {}
+
+ bool parse_error(std::size_t position, const std::string & last_token, const json::exception & ex) override { // NOLINT
+ this->position = position - 1;
+ this->found_error = true;
+ this->last_token = last_token;
+ this->exception_message = ex.what();
+ return false;
+ }
+ void close_value() {
+ if (!stack.empty() && (stack.back().type == COMMON_JSON_STACK_ELEMENT_KEY)) {
+ stack.pop_back();
+ }
+ }
+ bool null() override { // NOLINT
+ close_value();
+ return true;
+ }
+ bool boolean(bool) override { // NOLINT
+ close_value();
+ return true;
+ }
+ bool number_integer(number_integer_t) override { // NOLINT
+ close_value();
+ return true;
+ }
+ bool number_unsigned(number_unsigned_t) override { // NOLINT
+ close_value();
+ return true;
+ }
+ bool number_float(number_float_t, const string_t &) override { // NOLINT
+ close_value();
+ return true;
+ }
+ bool string(string_t &) override { // NOLINT
+ close_value();
+ return true;
+ }
+ bool binary(binary_t &) override { // NOLINT
+ close_value();
+ return true;
+ }
+ bool start_object(std::size_t) override { // NOLINT
+ stack.push_back({COMMON_JSON_STACK_ELEMENT_OBJECT, ""});
+ return true;
+ }
+ bool end_object() override {
+ GGML_ASSERT(!stack.empty() && stack.back().type == COMMON_JSON_STACK_ELEMENT_OBJECT);
+ stack.pop_back();
+ close_value();
+ return true;
+ }
+ bool key(string_t & key) override { // NOLINT
+ stack.push_back({COMMON_JSON_STACK_ELEMENT_KEY, key});
+ return true;
+ }
+ bool start_array(std::size_t) override { // NOLINT
+ stack.push_back({COMMON_JSON_STACK_ELEMENT_ARRAY, ""});
+ return true;
+ }
+ bool end_array() override {
+ GGML_ASSERT(!stack.empty() && stack.back().type == COMMON_JSON_STACK_ELEMENT_ARRAY);
+ stack.pop_back();
+ close_value();
+ return true;
+ }
+ };
+ json_error_locator err_loc;
+ auto start = it;
+ json::sax_parse(it, end, &err_loc);
+
+ if (err_loc.found_error) {
+ it = start;
+ auto temptative_end = it + err_loc.position;
+ // LOG_DBG("Error at position %zu (is_end = %s): %s\n", err_loc.position, temptative_end == end ? "true" : "false", err_loc.exception_message.c_str());
+
+ auto input = std::string(it, temptative_end);
+ try {
+ out.json = json::parse(input);
+ // out.json = json::parse(it, temptative_end);
+ it = temptative_end;
+ return true;
+ } catch (const std::exception & ex) {
+ // No, needs healing.
+ LOG_DBG("Failed to parse up to error: %s: <<<%s>>>\n", ex.what(), std::string(it, temptative_end).c_str());
+ }
+ auto can_parse = [](const std::string & str) {
+ try {
+ auto _ = json::parse(str); // NOLINT
+ return true;
+ } catch (const std::exception &) {
+ return false;
+ }
+ };
+ if (!healing_marker.empty() && !err_loc.stack.empty()) {
+ std::string str(it, temptative_end);
+ auto last_non_sp_pos = str.find_last_not_of(" \n\r\t");
+ if (last_non_sp_pos == std::string::npos) {
+ throw std::runtime_error("Cannot heal a truncated JSON that stopped in an unknown location");
+ }
+ auto last_non_sp_char = str[last_non_sp_pos];
+ // Used to detect stops on a number, which may not be complete.
+ auto was_maybe_number = [&]() {
+ if (!str.empty() && std::isspace(str.back())) {
+ return false;
+ }
+ return std::isdigit(last_non_sp_char) ||
+ last_non_sp_char == '.' ||
+ last_non_sp_char == 'e' ||
+ last_non_sp_char == 'E' ||
+ last_non_sp_char == '-';
+ };
+
+ std::string closing;
+ for (size_t i = err_loc.stack.size(); i > 0; i--) {
+ auto & el = err_loc.stack[i - 1];
+ if (el.type == COMMON_JSON_STACK_ELEMENT_OBJECT) {
+ closing += "}";
+ } else if (el.type == COMMON_JSON_STACK_ELEMENT_ARRAY) {
+ closing += "]";
+ } else if (el.type != COMMON_JSON_STACK_ELEMENT_KEY) {
+ throw std::runtime_error("Unexpected stack element type");
+ }
+ }
+
+ const auto & magic_seed = out.healing_marker.marker = healing_marker;//"$llama.cpp.json$";
+
+ if (err_loc.stack.back().type == COMMON_JSON_STACK_ELEMENT_KEY) {
+ // We're inside an object value
+ if (last_non_sp_char == ':' && can_parse(str + "1" + closing)) {
+ // Was about to create an object value
+ str += (out.healing_marker.json_dump_marker = "\"" + magic_seed) + "\"" + closing;
+ } else if (can_parse(str + ": 1" + closing)) {
+ str += (out.healing_marker.json_dump_marker = ":\"" + magic_seed) + "\"" + closing;
+ } else if (last_non_sp_char == '{' && can_parse(str + closing)) {
+ // Was about to create an object
+ str += (out.healing_marker.json_dump_marker = "\"" + magic_seed) + "\": 1" + closing;
+ } else if (can_parse(str + "\"" + closing)) {
+ // Was inside an object value string
+ str += (out.healing_marker.json_dump_marker = magic_seed) + "\"" + closing;
+ } else if (str[str.length() - 1] == '\\' && can_parse(str + "\\\"" + closing)) {
+ // Was inside an object value string after an escape
+ str += (out.healing_marker.json_dump_marker = "\\" + magic_seed) + "\"" + closing;
+ } else {
+ // find last :
+ auto last_pos = str.find_last_of(':');
+ if (last_pos == std::string::npos) {
+ throw std::runtime_error("Cannot heal a truncated JSON that stopped in an unknown location");
+ }
+ // Cutting back to opening : for object value
+ str = str.substr(0, last_pos + 1) + (out.healing_marker.json_dump_marker = "\"" + magic_seed) + "\"" + closing;
+ }
+ } else if (err_loc.stack.back().type == COMMON_JSON_STACK_ELEMENT_ARRAY) {
+ if ((last_non_sp_char == ',' || last_non_sp_char == '[') && can_parse(str + "1" + closing)) {
+ // Was about to create an array value
+ str += (out.healing_marker.json_dump_marker = "\"" + magic_seed) + "\"" + closing;
+ } else if (can_parse(str + "\"" + closing)) {
+ // Was inside an array value string
+ str += (out.healing_marker.json_dump_marker = magic_seed) + "\"" + closing;
+ } else if (str[str.length() - 1] == '\\' && can_parse(str + "\\\"" + closing)) {
+ // Was inside an array value string after an escape
+ str += (out.healing_marker.json_dump_marker = "\\" + magic_seed) + "\"" + closing;
+ } else if (!was_maybe_number() && can_parse(str + ", 1" + closing)) {
+ // Had just finished a value
+ str += (out.healing_marker.json_dump_marker = ",\"" + magic_seed) + "\"" + closing;
+ } else {
+ auto last_pos = str.find_last_of("[,");
+ if (last_pos == std::string::npos) {
+ throw std::runtime_error("Cannot heal a truncated JSON array stopped in an unknown location");
+ }
+ // Cutting back to last [ or , for array value
+ str = str.substr(0, last_pos + 1) + (out.healing_marker.json_dump_marker = "\"" + magic_seed) + "\"" + closing;
+ }
+ } else if (err_loc.stack.back().type == COMMON_JSON_STACK_ELEMENT_OBJECT) {
+ if ((last_non_sp_char == '{' && can_parse(str + closing)) ||
+ (last_non_sp_char == ',' && can_parse(str + "\"\": 1" + closing))) {
+ // Was about to create an object key+value
+ str += (out.healing_marker.json_dump_marker = "\"" + magic_seed) + "\": 1" + closing;
+ } else if (!was_maybe_number() && can_parse(str + ",\"\": 1" + closing)) {
+ // Was about to create an object key+value
+ str += (out.healing_marker.json_dump_marker = ",\"" + magic_seed) + "\": 1" + closing;
+ } else if (can_parse(str + "\": 1" + closing)) {
+ // Was inside an object key string
+ str += (out.healing_marker.json_dump_marker = magic_seed) + "\": 1" + closing;
+ } else if (str[str.length() - 1] == '\\' && can_parse(str + "\\\": 1" + closing)) {
+ // Was inside an object key string after an escape
+ str += (out.healing_marker.json_dump_marker = "\\" + magic_seed) + "\": 1" + closing;
+ } else {
+ auto last_pos = str.find_last_of(':');
+ if (last_pos == std::string::npos) {
+ throw std::runtime_error("Cannot heal a truncated JSON object stopped in an unknown location");
+ }
+ // fprintf(stderr, "Cutting back to last : for object key+value\n");
+ str = str.substr(0, last_pos + 1) + (out.healing_marker.json_dump_marker = "\"" + magic_seed) + "\"" + closing;
+ }
+ } else {
+ throw std::runtime_error("Cannot heal a truncated JSON object stopped in an unknown location");
+ }
+ // fprintf(stderr, "HEALED:\nSTRING <<<\n%s\n>>>\n\nmagic_cut: <<<\n%s\n>>>\n\n", str.c_str(), out.healing_marker.json_dump_marker.c_str());
+ out.json = json::parse(str);
+ it = temptative_end;
+ return true;
+ }
+ // TODO: handle unclosed top-level primitive if the stack was empty but we got an error (e.g. "tru", "\"", etc...)
+ // fprintf(stderr, "Closing: TODO\n");
+ return false;
+ }
+ out.json = json::parse(it, end);
+ it = end;
+ return true;
+}
diff --git a/common/json-partial.h b/common/json-partial.h
new file mode 100644
index 000000000..f63356dc4
--- /dev/null
+++ b/common/json-partial.h
@@ -0,0 +1,38 @@
+#pragma once
+
+#include
+
+// Healing marker (empty if the JSON was fully parsed / wasn't healed).
+struct common_healing_marker {
+ // Raw marker.
+ std::string marker;
+
+ // Cutting the `common_json.json.dump()` string at the (only) occurrence of this marker should yield the original partial JSON string (modulo spaces / if it had the same dump format).
+ std::string json_dump_marker;
+};
+
+// Represents a parsed JSON object, with its optional healing marker (a JSON dump fragment that can be used to find the position of healing in the JSON dump string)
+struct common_json {
+ nlohmann::ordered_json json;
+
+ common_healing_marker healing_marker;
+};
+
+// Parse the JSON string, healing (closing) any partial JSON if `healing_marker` is not empty.
+//
+// Healing completes partial JSON strings by adding a (possibly modified) healing marker, then whatever is needed to close the JSON.
+// This allows to parse the resulting healed JSON string, yet be able to cut it again if needed at the healing marker.
+// (this is used when parsing JSON outputs from the models, then crafting partial JSONs for the partial tool calls in OAI format).
+//
+// For instance, parsing `{` with a healing marker `foo` will produce a healed JSON `{"foo":1}`, w/ json_dump_marker = `"foo"` (which can be used to break the JSON again).
+bool common_json_parse(
+ const std::string & input,
+ const std::string & healing_marker,
+ common_json & out);
+
+// Parse the JSON string (see overload above), but advancing an iterator to the end of the input when the (potentially partial) parsing succeeds.
+bool common_json_parse(
+ std::string::const_iterator & it,
+ const std::string::const_iterator & end,
+ const std::string & healing_marker,
+ common_json & out);
diff --git a/common/json-schema-to-grammar.cpp b/common/json-schema-to-grammar.cpp
index 5b3059c2f..637891f50 100644
--- a/common/json-schema-to-grammar.cpp
+++ b/common/json-schema-to-grammar.cpp
@@ -1,8 +1,9 @@
#include "json-schema-to-grammar.h"
#include "common.h"
+#include
+
#include
-#include
#include