From d5fa587adcda26982fb6bf8b0987b4406e6c0707 Mon Sep 17 00:00:00 2001 From: Sanchit Monga Date: Sun, 23 Aug 2026 17:36:58 -0700 Subject: [PATCH 01/19] feat(kit): publish a C++ desktop find_package prefix for RCLI Ship RunAnywhere-cpp-desktop-{macos-arm64,windows-x64} tarballs from a dedicated workflow and from release.yml. In-tree rcli bottles are no longer a release gate; the product CLI lives in RunanywhereAI/RCLI and consumes the kit (proto headers + SCHEMA_LOCK, never a second protoc). Co-authored-by: Cursor --- .github/workflows/cpp-desktop-kit.yml | 117 +++++++++++ .github/workflows/pr-build.yml | 11 +- .github/workflows/release.yml | 110 +++++++--- CMakeLists.txt | 17 +- CMakePresets.json | 54 +++++ cmake/CppDesktopKit.cmake | 182 ++++++++++++++++ cmake/PackageCppDesktop.cmake | 229 +++++++++++++++++++++ cmake/RunAnywhereConfig.cmake.in | 120 +++++++++++ docs/reference/cpp-desktop-kit.md | 65 ++++++ docs/reference/generated-code-contract.md | 16 ++ rcli/README.md | 8 + scripts/build/fetch-private-engine-pack.sh | 29 +++ scripts/build/package-cpp-desktop.sh | 21 ++ tests/kit/test_cpp_desktop_kit.c | 16 ++ 14 files changed, 960 insertions(+), 35 deletions(-) create mode 100644 .github/workflows/cpp-desktop-kit.yml create mode 100644 cmake/CppDesktopKit.cmake create mode 100644 cmake/PackageCppDesktop.cmake create mode 100644 cmake/RunAnywhereConfig.cmake.in create mode 100644 docs/reference/cpp-desktop-kit.md create mode 100755 scripts/build/fetch-private-engine-pack.sh create mode 100755 scripts/build/package-cpp-desktop.sh create mode 100644 tests/kit/test_cpp_desktop_kit.c diff --git a/.github/workflows/cpp-desktop-kit.yml b/.github/workflows/cpp-desktop-kit.yml new file mode 100644 index 0000000000..46e0d04ea7 --- /dev/null +++ b/.github/workflows/cpp-desktop-kit.yml @@ -0,0 +1,117 @@ +name: C++ desktop kit + +# Builds the find_package(RunAnywhere) prefix RCLI consumes. Does not build rcli. +# Dispatch this to refresh kits without a full SDK release train. release.yml +# also builds the same presets and attaches the tarballs to the GitHub Release. + +on: + workflow_dispatch: + inputs: + attach_tag: + description: 'GitHub release tag to attach kits to (e.g. v0.20.25). Empty = artifacts only.' + required: false + default: '' + push: + paths: + - 'cmake/CppDesktopKit.cmake' + - 'cmake/PackageCppDesktop.cmake' + - 'cmake/RunAnywhereConfig.cmake.in' + - 'CMakePresets.json' + - 'core/**' + - 'engines/**' + - 'idl/**' + - '.github/workflows/cpp-desktop-kit.yml' + branches: [main] + pull_request: + paths: + - 'cmake/CppDesktopKit.cmake' + - 'cmake/PackageCppDesktop.cmake' + - 'cmake/RunAnywhereConfig.cmake.in' + - 'CMakePresets.json' + - '.github/workflows/cpp-desktop-kit.yml' + +permissions: + contents: write + +concurrency: + group: cpp-desktop-kit-${{ github.ref }} + cancel-in-progress: true + +jobs: + macos-arm64: + runs-on: macos-14 + timeout-minutes: 90 + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/setup-toolchain + with: + platform: macos + - name: Configure + package + run: | + set -euo pipefail + cmake --preset cpp-desktop-macos-arm64 + cmake --build --preset cpp-desktop-macos-arm64 --target package-cpp-desktop-tarball \ + -j "$(sysctl -n hw.logicalcpu)" + ls -la dist/RunAnywhere-cpp-desktop-macos-arm64-v*.tar.gz + shasum -a 256 dist/RunAnywhere-cpp-desktop-macos-arm64-v*.tar.gz \ + | tee dist/RunAnywhere-cpp-desktop-macos-arm64-v$(tr -d '[:space:]' < core/VERSION).tar.gz.sha256 + - uses: actions/upload-artifact@v4 + with: + name: cpp-desktop-macos-arm64 + path: | + dist/RunAnywhere-cpp-desktop-macos-arm64-v*.tar.gz + dist/RunAnywhere-cpp-desktop-macos-arm64-v*.tar.gz.sha256 + if-no-files-found: error + + windows-x64: + runs-on: windows-2022 + timeout-minutes: 120 + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/setup-toolchain + with: + platform: windows + - uses: ilammy/msvc-dev-cmd@v1 + - name: vcpkg curl (static) + shell: pwsh + run: | + & "$env:VCPKG_INSTALLATION_ROOT\vcpkg.exe" install curl:x64-windows-static zlib:x64-windows-static + if ($LASTEXITCODE -ne 0) { throw "vcpkg install failed" } + - name: Configure + package + shell: pwsh + run: | + cmake --preset cpp-desktop-windows-x64 + if ($LASTEXITCODE -ne 0) { throw "cmake configure failed" } + cmake --build --preset cpp-desktop-windows-x64 --config Release --target package-cpp-desktop-tarball + if ($LASTEXITCODE -ne 0) { throw "cmake build failed" } + $ver = (Get-Content core/VERSION -Raw).Trim() + $tar = "dist/RunAnywhere-cpp-desktop-windows-x64-v$ver.tar.gz" + if (-not (Test-Path $tar)) { throw "missing $tar" } + Get-FileHash $tar -Algorithm SHA256 | + ForEach-Object { "$($_.Hash.ToLower()) $(Split-Path $_.Path -Leaf)" } | + Set-Content "$tar.sha256" + - uses: actions/upload-artifact@v4 + with: + name: cpp-desktop-windows-x64 + path: | + dist/RunAnywhere-cpp-desktop-windows-x64-v*.tar.gz + dist/RunAnywhere-cpp-desktop-windows-x64-v*.tar.gz.sha256 + if-no-files-found: error + + attach-release: + if: github.event_name == 'workflow_dispatch' && inputs.attach_tag != '' + needs: [macos-arm64, windows-x64] + runs-on: ubuntu-24.04 + steps: + - uses: actions/download-artifact@v4 + with: + path: artifacts + merge-multiple: true + - name: Upload kits to ${{ inputs.attach_tag }} + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + ls -la artifacts + gh release upload "${{ inputs.attach_tag }}" artifacts/RunAnywhere-cpp-desktop-*.tar.gz* \ + --repo "${{ github.repository }}" --clobber diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index 6b941a5c4a..9c2a220646 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yml @@ -171,9 +171,9 @@ jobs: - run: ctest --preset linux-asan rcli-macos: - # rcli desktop CLI — full-backend release build + modelless smoke. - # This is the one macOS lane that enables GGML_METAL, so it retains the - # older compatible runner while CPU-only native and SDK lanes use Xcode 26. + # Product CLI moved to RunanywhereAI/RCLI (kit consumer). In-tree RAC_BUILD_CLI + # is a hard error. Keep the job name so required checks do not 404; skip the work. + if: false runs-on: macos-14 steps: - uses: actions/checkout@v7 @@ -197,7 +197,7 @@ jobs: run: bash rcli/scripts/package-rcli.sh build/rcli-macos-release macos-arm64 rcli-linux: - # rcli desktop CLI on Linux — release build, unit tests, modelless smoke. + if: false runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v7 @@ -222,8 +222,7 @@ jobs: run: bash rcli/scripts/package-rcli.sh build/rcli-linux-release linux-x86_64 rcli-windows: - # Windows is compiled and exercised on the native runner. MLX remains - # Apple-only; the portable CLI ships llama.cpp + Sherpa/ONNX on Windows. + if: false runs-on: windows-2022 timeout-minutes: 60 steps: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cdb44ce950..5557ec49a2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -399,11 +399,9 @@ jobs: native_rcli_macos: needs: [validate, native_ios] - # The production rcli is the Swift host that registers the MLX callbacks - # before entering the shared C++ CLI. It consumes the exact Apple release - # candidates produced above and requires Swift 6.2+ for mlx-audio-swift. - # Pull-request smoke keeps the faster CMake-only macos-14 lane. - if: ${{ !inputs.publish_from_run_id }} + # Product CLI moved to RunanywhereAI/RCLI (kit consumer). Keep the job + # id so publish `needs:` does not 404; skip the in-tree bottle. + if: false runs-on: macos-26 timeout-minutes: 120 steps: @@ -649,7 +647,7 @@ jobs: native_rcli_linux: needs: validate - if: ${{ !inputs.publish_from_run_id }} + if: false # Product CLI is RunanywhereAI/RCLI. runs-on: ubuntu-latest timeout-minutes: 60 steps: @@ -681,7 +679,7 @@ jobs: native_rcli_windows: needs: validate - if: ${{ !inputs.publish_from_run_id }} + if: false # Product CLI is RunanywhereAI/RCLI. runs-on: windows-2022 timeout-minutes: 90 steps: @@ -772,6 +770,74 @@ jobs: retention-days: 7 if-no-files-found: error + native_cpp_desktop_macos: + needs: validate + if: ${{ !inputs.publish_from_run_id }} + runs-on: macos-14 + timeout-minutes: 90 + steps: + - uses: actions/checkout@v7 + - uses: ./.github/actions/setup-toolchain + with: + platform: macos + - name: Configure + package C++ desktop kit + run: | + set -euo pipefail + cmake --preset cpp-desktop-macos-arm64 + cmake --build --preset cpp-desktop-macos-arm64 --target package-cpp-desktop-tarball \ + -j "$(sysctl -n hw.logicalcpu)" + ls -la dist/RunAnywhere-cpp-desktop-macos-arm64-v*.tar.gz + shasum -a 256 dist/RunAnywhere-cpp-desktop-macos-arm64-v*.tar.gz \ + | tee dist/RunAnywhere-cpp-desktop-macos-arm64-v$(tr -d '[:space:]' < core/VERSION).tar.gz.sha256 + - uses: actions/upload-artifact@v7 + with: + name: cpp-desktop-macos + path: | + dist/RunAnywhere-cpp-desktop-macos-arm64-v*.tar.gz + dist/RunAnywhere-cpp-desktop-macos-arm64-v*.tar.gz.sha256 + retention-days: 7 + if-no-files-found: error + + native_cpp_desktop_windows: + needs: validate + if: ${{ !inputs.publish_from_run_id }} + runs-on: windows-2022 + timeout-minutes: 120 + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - uses: ./.github/actions/setup-toolchain + with: + platform: windows + - uses: ilammy/msvc-dev-cmd@v1 + - name: vcpkg curl (static) + shell: pwsh + run: | + & "$env:VCPKG_INSTALLATION_ROOT\vcpkg.exe" install curl:x64-windows-static zlib:x64-windows-static + if ($LASTEXITCODE -ne 0) { throw "vcpkg install failed" } + - name: Configure + package C++ desktop kit + shell: pwsh + run: | + cmake --preset cpp-desktop-windows-x64 + if ($LASTEXITCODE -ne 0) { throw "cmake configure failed" } + cmake --build --preset cpp-desktop-windows-x64 --config Release --target package-cpp-desktop-tarball + if ($LASTEXITCODE -ne 0) { throw "cmake build failed" } + $ver = (Get-Content core/VERSION -Raw).Trim() + $tar = "dist/RunAnywhere-cpp-desktop-windows-x64-v$ver.tar.gz" + if (-not (Test-Path $tar)) { throw "missing $tar" } + Get-FileHash $tar -Algorithm SHA256 | + ForEach-Object { "$($_.Hash.ToLower()) $(Split-Path $_.Path -Leaf)" } | + Set-Content "$tar.sha256" + - uses: actions/upload-artifact@v7 + with: + name: cpp-desktop-windows + path: | + dist/RunAnywhere-cpp-desktop-windows-x64-v*.tar.gz + dist/RunAnywhere-cpp-desktop-windows-x64-v*.tar.gz.sha256 + retention-days: 7 + if-no-files-found: error + native_windows: needs: validate # Pin windows-2022: build-windows.bat hardcodes the "Visual Studio 17 2022" @@ -2086,6 +2152,8 @@ jobs: - native_rcli_macos - native_rcli_linux - native_rcli_windows + - native_cpp_desktop_macos + - native_cpp_desktop_windows - native_windows - native_web - native_electron @@ -2131,11 +2199,10 @@ jobs: needs.native_ios.result == 'success' && needs.native_android.result == 'success' && needs.native_linux.result == 'success' && - needs.native_rcli_macos.result == 'success' && - needs.native_rcli_linux.result == 'success' && - (needs.native_rcli_windows.result == 'success' || - needs.native_rcli_windows.result == 'failure' || - needs.native_rcli_windows.result == 'skipped') && + needs.native_cpp_desktop_macos.result == 'success' && + (needs.native_cpp_desktop_windows.result == 'success' || + needs.native_cpp_desktop_windows.result == 'failure' || + needs.native_cpp_desktop_windows.result == 'skipped') && (needs.native_web.result == 'success' || (inputs.reuse_native_web_run_id != '' && needs.native_web.result == 'skipped')) && needs.sdk_kotlin.result == 'success' && @@ -2268,21 +2335,14 @@ jobs: echo " SKIP: Windows x64 advisory artifact absent (native_windows failed/skipped)" fi assert_pair "Web WASM" "RACommons-web-v${VERSION}.tar.gz" - assert_pair "rcli macOS arm64" "rcli-macos-arm64-v${VERSION}.tar.gz" - # The DMG exists only when the Developer ID + notary secrets are set; - # without them the packaging step soft-skips it by design and ships the - # ad-hoc tarball alone. Asserting it unconditionally made publish - # unsatisfiable, which is what blocked every release after v0.20.10. - if [ -s "release-flat/rcli-macos-arm64-v${VERSION}.dmg" ]; then - assert_pair "rcli macOS notarized disk image" "rcli-macos-arm64-v${VERSION}.dmg" + assert_pair "C++ desktop kit macOS arm64" "RunAnywhere-cpp-desktop-macos-arm64-v${VERSION}.tar.gz" + if [ -s "release-flat/RunAnywhere-cpp-desktop-windows-x64-v${VERSION}.tar.gz" ]; then + assert_pair "C++ desktop kit Windows x64" "RunAnywhere-cpp-desktop-windows-x64-v${VERSION}.tar.gz" else - echo "::warning::no notarized rcli DMG in this release — Developer ID/notary secrets are not configured" + echo " SKIP: C++ desktop kit Windows x64 advisory artifact absent" fi - assert_pair "rcli Linux x86_64" "rcli-linux-x86_64-v${VERSION}.tar.gz" - if [ -s "release-flat/rcli-windows-x86_64-v${VERSION}.zip" ] || [ -s "release-flat/rcli-windows-x86_64-v${VERSION}.zip.sha256" ]; then - assert_pair "rcli Windows x86_64 (advisory)" "rcli-windows-x86_64-v${VERSION}.zip" - else - echo " SKIP: rcli Windows x86_64 advisory artifact absent (job failed/skipped)" + if [ -s "release-flat/rcli-macos-arm64-v${VERSION}.tar.gz" ]; then + echo " NOTE: in-tree rcli bottle present (legacy); official CLI is RunanywhereAI/RCLI" fi assert_pair "Kotlin Maven repository" "runanywhere-kotlin-maven-v${VERSION}.zip" assert_pair "Web proto package" "runanywhere-proto-ts-${VERSION}.tgz" diff --git a/CMakeLists.txt b/CMakeLists.txt index 5c1a1d7078..3b55d4e67a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -276,10 +276,14 @@ if(RAC_BUILD_TESTS) enable_testing() endif() -# rcli desktop CLI. Added after engines/ because it links the -# rac_backend_* targets that only exist once the engine subdirs configured. +# In-tree rcli is retired. The product lives in RunanywhereAI/RCLI and +# consumes the C++ desktop kit (find_package(RunAnywhere)). if(RAC_BUILD_CLI) - add_subdirectory(rcli) + message(FATAL_ERROR + "RAC_BUILD_CLI is retired. Build EXTERNAL/RCLI (RunanywhereAI/RCLI) against " + "the C++ desktop kit:\n" + " cmake --preset cpp-desktop-macos-arm64 && cmake --build --preset cpp-desktop-macos-arm64\n" + " cmake -S EXTERNAL/RCLI -B EXTERNAL/RCLI/build -DCMAKE_PREFIX_PATH=") endif() # runanywhere-electron M0 inference harness. Added after engines/ because it @@ -772,6 +776,10 @@ else() set(_rac_plugin_mode "SHARED (dlopen-loaded)") endif() +# C++ desktop kit (find_package(RunAnywhere) prefix for RCLI). After engines +# so rac_backend_* targets exist for the kit's optional-engine flags. +include(CppDesktopKit) + message(STATUS "") message(STATUS "============================================") message(STATUS "RunAnywhere SDKs — root CMake configured") @@ -785,7 +793,8 @@ message(STATUS "Server (HTTP) : ${RAC_BUILD_SERVER}") message(STATUS "Platform backend : ${RAC_BUILD_PLATFORM}") message(STATUS "JNI bridge : ${RAC_BUILD_JNI}") message(STATUS "Plugin smoke CLI : ${RAC_BUILD_PLUGIN_SMOKE}") -message(STATUS "CLI (rcli) : ${RAC_BUILD_CLI}") +message(STATUS "CLI (in-tree, deprecated) : ${RAC_BUILD_CLI}") +message(STATUS "C++ desktop kit : ${RAC_PACKAGE_CPP_DESKTOP}") message(STATUS "Desktop adapter : ${RAC_DESKTOP_ADAPTER}") message(STATUS "============================================") message(STATUS "") diff --git a/CMakePresets.json b/CMakePresets.json index 12ef820295..07f2584070 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -85,6 +85,58 @@ } }, + { + "name": "cpp-desktop-macos-arm64", + "displayName": "C++ desktop kit — macOS arm64 (no CLI)", + "inherits": "base-release", + "cacheVariables": { + "RAC_BUILD_CLI": "OFF", + "RAC_PACKAGE_CPP_DESKTOP": "ON", + "RAC_DESKTOP_ADAPTER": "ON", + "RAC_BUILD_BACKENDS": "ON", + "RAC_BACKEND_LLAMACPP": "ON", + "RAC_BACKEND_MLX": "ON", + "RAC_BACKEND_SHERPA": "ON", + "RAC_BACKEND_ONNX": "ON", + "RAC_BACKEND_NEURT": "OFF", + "RAC_BACKEND_QHEXRT": "OFF", + "RAC_RUNTIME_ONNXRT": "ON", + "RAC_RUNTIME_COREML": "ON", + "RAC_BACKEND_RAG": "ON", + "RAC_BUILD_SERVER": "OFF", + "RAC_STATIC_PLUGINS": "ON", + "RAC_BUILD_SHARED": "OFF", + "RAC_BUILD_PLATFORM": "ON", + "CMAKE_OSX_ARCHITECTURES": "arm64" + }, + "condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Darwin" } + }, + { + "name": "cpp-desktop-windows-x64", + "displayName": "C++ desktop kit — Windows x64 (no CLI)", + "inherits": "base-release", + "cacheVariables": { + "CMAKE_TOOLCHAIN_FILE": "$env{VCPKG_INSTALLATION_ROOT}/scripts/buildsystems/vcpkg.cmake", + "VCPKG_TARGET_TRIPLET": "x64-windows-static", + "RAC_BUILD_CLI": "OFF", + "RAC_PACKAGE_CPP_DESKTOP": "ON", + "RAC_DESKTOP_ADAPTER": "ON", + "RAC_BUILD_BACKENDS": "ON", + "RAC_BACKEND_LLAMACPP": "ON", + "RAC_BACKEND_SHERPA": "ON", + "RAC_BACKEND_ONNX": "ON", + "RAC_BACKEND_MLX": "OFF", + "RAC_BACKEND_NEURT": "OFF", + "RAC_BACKEND_QHEXRT": "OFF", + "RAC_RUNTIME_ONNXRT": "ON", + "RAC_BACKEND_RAG": "OFF", + "RAC_BUILD_SERVER": "OFF", + "RAC_STATIC_PLUGINS": "ON", + "RAC_BUILD_SHARED": "OFF", + "RAC_BUILD_PLATFORM": "OFF" + }, + "condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Windows" } + }, { "name": "rcli-macos-release", "displayName": "rcli — macOS arm64 Release (Metal)", @@ -405,6 +457,8 @@ { "name": "linux-debug", "configurePreset": "linux-debug" }, { "name": "linux-release", "configurePreset": "linux-release" }, { "name": "linux-asan", "configurePreset": "linux-asan" }, + { "name": "cpp-desktop-macos-arm64", "configurePreset": "cpp-desktop-macos-arm64", "targets": ["package-cpp-desktop-tarball"] }, + { "name": "cpp-desktop-windows-x64", "configurePreset": "cpp-desktop-windows-x64", "targets": ["package-cpp-desktop-tarball"], "configuration": "Release" }, { "name": "rcli-macos-release", "configurePreset": "rcli-macos-release" }, { "name": "rcli-linux-release", "configurePreset": "rcli-linux-release" }, { "name": "rcli-windows-release", "configurePreset": "rcli-windows-release" }, diff --git a/cmake/CppDesktopKit.cmake b/cmake/CppDesktopKit.cmake new file mode 100644 index 0000000000..9faea36a76 --- /dev/null +++ b/cmake/CppDesktopKit.cmake @@ -0,0 +1,182 @@ +# C++ desktop kit: a find_package(RunAnywhere) prefix RCLI can consume +# without compiling this monorepo. Included from the root CMakeLists after +# rac_commons and the engines exist. + +if(NOT TARGET rac_commons) + return() +endif() + +if(NOT RAC_SOURCE_DIR) + set(RAC_SOURCE_DIR "${RAC_ROOT_DIR}") +endif() + +set(RAC_PACKAGE_CPP_DESKTOP OFF CACHE BOOL + "Generate the package-cpp-desktop target (static commons + public headers + IDL)") + +# Kit smoke: a tiny C program that only includes public headers. Always +# available when tests are on, so commons stays proven without rcli. +if(RAC_BUILD_TESTS AND EXISTS "${RAC_SOURCE_DIR}/tests/kit/test_cpp_desktop_kit.c") + add_executable(test_cpp_desktop_kit "${RAC_SOURCE_DIR}/tests/kit/test_cpp_desktop_kit.c") + target_link_libraries(test_cpp_desktop_kit PRIVATE rac_commons) + target_compile_features(test_cpp_desktop_kit PRIVATE c_std_11) + add_test(NAME cpp_desktop_kit_smoke COMMAND test_cpp_desktop_kit) +endif() + +if(NOT RAC_PACKAGE_CPP_DESKTOP) + return() +endif() + +set(_kit_genex) +foreach(_t IN ITEMS + rac_commons + llama llama-common llama-common-base ggml ggml-base ggml-cpu ggml-metal ggml-cuda ggml-vulkan ggml-blas + rac_runtime_onnxrt rac_runtime_coreml + rac_backend_llamacpp rac_backend_onnx rac_backend_sherpa + rac_backend_mlx rac_backend_neurt rac_backend_cloud rac_backend_qhexrt + rac_server) + if(TARGET ${_t}) + get_target_property(_type ${_t} TYPE) + if(_type STREQUAL "STATIC_LIBRARY" OR _type STREQUAL "SHARED_LIBRARY") + string(APPEND _kit_genex "$\n") + endif() + endif() +endforeach() +file(GENERATE OUTPUT "${CMAKE_BINARY_DIR}/cpp-desktop-libs.txt" CONTENT "${_kit_genex}") + +set(_kit_os "${CMAKE_SYSTEM_NAME}") +string(TOLOWER "${_kit_os}" _kit_os) +if(_kit_os STREQUAL "darwin") + set(_kit_os "macos") +endif() +set(_kit_arch "${CMAKE_SYSTEM_PROCESSOR}") +if(_kit_arch MATCHES "arm64|aarch64") + set(_kit_arch "arm64") +elseif(_kit_arch MATCHES "x86_64|amd64|AMD64") + set(_kit_arch "x64") +endif() +set(_kit_out "${RAC_SOURCE_DIR}/dist/cpp-desktop-${_kit_os}-${_kit_arch}") + +# Values substituted into RunAnywhereConfig.cmake.in via PackageCppDesktop.cmake. +set(RUNANYWHERE_KIT_COMMONS_ARCHIVE "$") +set(RUNANYWHERE_KIT_HAS_LLAMACPP FALSE) +set(RUNANYWHERE_KIT_HAS_ONNX FALSE) +set(RUNANYWHERE_KIT_HAS_SHERPA FALSE) +set(RUNANYWHERE_KIT_HAS_MLX FALSE) +set(RUNANYWHERE_KIT_HAS_NEURT FALSE) +set(RUNANYWHERE_KIT_HAS_CLOUD FALSE) +set(RUNANYWHERE_KIT_HAS_QHEXRT FALSE) +set(RUNANYWHERE_KIT_HAS_RAG FALSE) +set(RUNANYWHERE_KIT_HAS_SERVER FALSE) +if(TARGET rac_backend_llamacpp) + set(RUNANYWHERE_KIT_HAS_LLAMACPP TRUE) +endif() +if(TARGET rac_backend_onnx) + set(RUNANYWHERE_KIT_HAS_ONNX TRUE) +endif() +if(TARGET rac_backend_sherpa) + set(RUNANYWHERE_KIT_HAS_SHERPA TRUE) +endif() +if(TARGET rac_backend_mlx) + set(RUNANYWHERE_KIT_HAS_MLX TRUE) +endif() +if(TARGET rac_backend_neurt) + set(RUNANYWHERE_KIT_HAS_NEURT TRUE) +endif() +if(TARGET rac_backend_cloud) + set(RUNANYWHERE_KIT_HAS_CLOUD TRUE) +endif() +if(TARGET rac_backend_qhexrt) + set(RUNANYWHERE_KIT_HAS_QHEXRT TRUE) +endif() +if(RAC_BACKEND_RAG) + set(RUNANYWHERE_KIT_HAS_RAG TRUE) +endif() +if(TARGET rac_server) + set(RUNANYWHERE_KIT_HAS_SERVER TRUE) +endif() +set(RUNANYWHERE_KIT_HAS_DESKTOP_ADAPTER ${RAC_DESKTOP_ADAPTER}) +set(RUNANYWHERE_KIT_EXTRA_LIBS "") +set(RUNANYWHERE_KIT_SYSTEM_LIBS "") +if(RAC_PROTOBUF_NAMESPACE_ISOLATED) + set(RUNANYWHERE_KIT_PROTOBUF_ISOLATED TRUE) +else() + set(RUNANYWHERE_KIT_PROTOBUF_ISOLATED FALSE) +endif() +if(DEFINED protobuf_fetched_SOURCE_DIR) + set(RAC_PROTOBUF_INCLUDE_DIR "${protobuf_fetched_SOURCE_DIR}/src") +elseif(Protobuf_INCLUDE_DIRS) + list(GET Protobuf_INCLUDE_DIRS 0 RAC_PROTOBUF_INCLUDE_DIR) +endif() +if(EXISTS "${CMAKE_BINARY_DIR}/_deps/absl-src/absl/strings/string_view.h") + set(RAC_ABSL_INCLUDE_DIR "${CMAKE_BINARY_DIR}/_deps/absl-src") +endif() +if(EXISTS "${CMAKE_BINARY_DIR}/_deps/utf8_range-src/utf8_range.h") + set(RAC_UTF8_RANGE_INCLUDE_DIR "${CMAKE_BINARY_DIR}/_deps/utf8_range-src") +elseif(DEFINED protobuf_fetched_SOURCE_DIR + AND EXISTS "${protobuf_fetched_SOURCE_DIR}/third_party/utf8_range/utf8_range.h") + set(RAC_UTF8_RANGE_INCLUDE_DIR "${protobuf_fetched_SOURCE_DIR}/third_party/utf8_range") +endif() + +# Plugin ABI from the public header, not a stale comment. +set(_abi_header "${RAC_SOURCE_DIR}/core/include/rac/plugin/rac_plugin_entry.h") +file(STRINGS "${_abi_header}" _abi_line REGEX "#define[ \t]+RAC_PLUGIN_API_VERSION") +string(REGEX REPLACE ".*[^0-9]([0-9]+)u?.*" "\\1" RAC_PLUGIN_API_VERSION "${_abi_line}") +if(NOT RAC_PLUGIN_API_VERSION) + set(RAC_PLUGIN_API_VERSION "9") +endif() + +set(_kit_depends rac_commons) +foreach(_t IN ITEMS + llama llama-common llama-common-base ggml ggml-base ggml-cpu ggml-metal ggml-cuda ggml-vulkan ggml-blas + rac_runtime_onnxrt rac_runtime_coreml + rac_backend_llamacpp rac_backend_onnx rac_backend_sherpa + rac_backend_mlx rac_backend_neurt rac_backend_cloud rac_backend_qhexrt + rac_server archive_static) + if(TARGET ${_t}) + list(APPEND _kit_depends ${_t}) + endif() +endforeach() + +add_custom_target(package-cpp-desktop + COMMAND ${CMAKE_COMMAND} + -DRAC_SOURCE_DIR=${RAC_SOURCE_DIR} + -DRAC_BINARY_DIR=${CMAKE_BINARY_DIR} + -DRAC_VERSION=${RAC_VERSION} + -DRAC_PLUGIN_API_VERSION=${RAC_PLUGIN_API_VERSION} + -DRAC_COMMONS_FILE=$ + -DRAC_KIT_LIBS_FILE=${CMAKE_BINARY_DIR}/cpp-desktop-libs.txt + -DRAC_KIT_OUT=${_kit_out} + -DRAC_KIT_OS=${_kit_os} + -DRAC_KIT_ARCH=${_kit_arch} + -DRUNANYWHERE_KIT_COMMONS_ARCHIVE=$ + -DRUNANYWHERE_KIT_HAS_LLAMACPP=${RUNANYWHERE_KIT_HAS_LLAMACPP} + -DRUNANYWHERE_KIT_HAS_ONNX=${RUNANYWHERE_KIT_HAS_ONNX} + -DRUNANYWHERE_KIT_HAS_SHERPA=${RUNANYWHERE_KIT_HAS_SHERPA} + -DRUNANYWHERE_KIT_HAS_MLX=${RUNANYWHERE_KIT_HAS_MLX} + -DRUNANYWHERE_KIT_HAS_NEURT=${RUNANYWHERE_KIT_HAS_NEURT} + -DRUNANYWHERE_KIT_HAS_CLOUD=${RUNANYWHERE_KIT_HAS_CLOUD} + -DRUNANYWHERE_KIT_HAS_QHEXRT=${RUNANYWHERE_KIT_HAS_QHEXRT} + -DRUNANYWHERE_KIT_HAS_RAG=${RUNANYWHERE_KIT_HAS_RAG} + -DRUNANYWHERE_KIT_HAS_SERVER=${RUNANYWHERE_KIT_HAS_SERVER} + -DRUNANYWHERE_KIT_HAS_DESKTOP_ADAPTER=${RUNANYWHERE_KIT_HAS_DESKTOP_ADAPTER} + -DRUNANYWHERE_KIT_PROTOBUF_ISOLATED=${RUNANYWHERE_KIT_PROTOBUF_ISOLATED} + -DRAC_PROTOBUF_INCLUDE_DIR=${RAC_PROTOBUF_INCLUDE_DIR} + -DRAC_ABSL_INCLUDE_DIR=${RAC_ABSL_INCLUDE_DIR} + -DRAC_UTF8_RANGE_INCLUDE_DIR=${RAC_UTF8_RANGE_INCLUDE_DIR} + -P ${RAC_SOURCE_DIR}/cmake/PackageCppDesktop.cmake + DEPENDS ${_kit_depends} + VERBATIM + COMMENT "Staging C++ desktop kit at ${_kit_out}" +) + +# Tarball next to the prefix for release.yml to pick up. +add_custom_target(package-cpp-desktop-tarball + COMMAND ${CMAKE_COMMAND} -E make_directory "${RAC_SOURCE_DIR}/dist" + COMMAND ${CMAKE_COMMAND} -E tar czf + "${RAC_SOURCE_DIR}/dist/RunAnywhere-cpp-desktop-${_kit_os}-${_kit_arch}-v${RAC_VERSION}.tar.gz" + --format=gnutar + "cpp-desktop-${_kit_os}-${_kit_arch}" + DEPENDS package-cpp-desktop + WORKING_DIRECTORY "${RAC_SOURCE_DIR}/dist" + COMMENT "Tarring C++ desktop kit" +) diff --git a/cmake/PackageCppDesktop.cmake b/cmake/PackageCppDesktop.cmake new file mode 100644 index 0000000000..2cd1f96e07 --- /dev/null +++ b/cmake/PackageCppDesktop.cmake @@ -0,0 +1,229 @@ +# Script-mode packager for the C++ desktop kit. +# Invoked from the package-cpp-desktop custom target. +# +# Required -D (see cmake/CppDesktopKit.cmake): +# RAC_SOURCE_DIR RAC_BINARY_DIR RAC_VERSION RAC_PLUGIN_API_VERSION +# RAC_COMMONS_FILE RAC_KIT_LIBS_FILE RAC_KIT_OUT + +if(NOT RAC_SOURCE_DIR OR NOT RAC_COMMONS_FILE OR NOT RAC_KIT_OUT) + message(FATAL_ERROR "PackageCppDesktop: RAC_SOURCE_DIR, RAC_COMMONS_FILE, RAC_KIT_OUT required") +endif() + +file(REMOVE_RECURSE "${RAC_KIT_OUT}") +file(MAKE_DIRECTORY "${RAC_KIT_OUT}/include") +file(MAKE_DIRECTORY "${RAC_KIT_OUT}/lib/cmake/RunAnywhere") +file(MAKE_DIRECTORY "${RAC_KIT_OUT}/share/runanywhere/idl") +file(MAKE_DIRECTORY "${RAC_KIT_OUT}/third_party") + +file(COPY "${RAC_SOURCE_DIR}/core/include/rac" DESTINATION "${RAC_KIT_OUT}/include") + +file(GLOB _protos "${RAC_SOURCE_DIR}/idl/*.proto") +if(_protos) + file(COPY ${_protos} DESTINATION "${RAC_KIT_OUT}/share/runanywhere/idl") +endif() + +# Generated C++ proto headers (same protoc that built commons). RCLI includes +# these; it must not run a second protoc. .pb.cc stays inside librac_commons.a. +set(_proto_gen "${RAC_SOURCE_DIR}/core/src/generated/proto") +if(NOT EXISTS "${_proto_gen}/model_types.pb.h") + message(FATAL_ERROR + "PackageCppDesktop: missing ${_proto_gen}/model_types.pb.h. " + "Configure/build commons with protobuf before packaging the kit.") +endif() +file(MAKE_DIRECTORY "${RAC_KIT_OUT}/include/runanywhere/proto") +file(GLOB _pb_headers "${_proto_gen}/*.pb.h") +file(COPY ${_pb_headers} DESTINATION "${RAC_KIT_OUT}/include/runanywhere/proto") + +# SCHEMA_LOCK is the cross-repo proto fingerprint. RCLI pins the SHA and +# refuses a kit whose lock does not match — neither side runs a second protoc. +set(_schema_lock "${RAC_SOURCE_DIR}/idl/SCHEMA_LOCK") +if(NOT EXISTS "${_schema_lock}") + message(FATAL_ERROR "PackageCppDesktop: missing ${_schema_lock}") +endif() +file(COPY "${_schema_lock}" DESTINATION "${RAC_KIT_OUT}/share/runanywhere") +if(EXISTS "${RAC_SOURCE_DIR}/idl/VERSION") + file(COPY "${RAC_SOURCE_DIR}/idl/VERSION" DESTINATION "${RAC_KIT_OUT}/share/runanywhere") +endif() + +macro(_kit_lock_key key var) + set(${var} "") + file(STRINGS "${_schema_lock}" _hit REGEX "^${key}=") + if(_hit) + list(GET _hit 0 _line) + string(REGEX REPLACE "^${key}=" "" ${var} "${_line}") + string(STRIP "${${var}}" ${var}) + endif() +endmacro() +_kit_lock_key(IDL_VERSION RUNANYWHERE_KIT_IDL_VERSION) +_kit_lock_key(IDL_SCHEMA_SHA256 RUNANYWHERE_KIT_IDL_SCHEMA_SHA256) +_kit_lock_key(IDL_PROTOC_VERSION RUNANYWHERE_KIT_IDL_PROTOC_VERSION) +_kit_lock_key(IDL_PROTO_COUNT RUNANYWHERE_KIT_IDL_PROTO_COUNT) +if(NOT RUNANYWHERE_KIT_IDL_SCHEMA_SHA256 OR NOT RUNANYWHERE_KIT_IDL_VERSION) + message(FATAL_ERROR "PackageCppDesktop: idl/SCHEMA_LOCK is missing IDL_SCHEMA_SHA256 or IDL_VERSION") +endif() +file(WRITE "${RAC_KIT_OUT}/include/runanywhere/proto/schema_lock.h" +"#pragma once +// Generated from idl/SCHEMA_LOCK. Do not edit. +#define RUNANYWHERE_IDL_VERSION \"${RUNANYWHERE_KIT_IDL_VERSION}\" +#define RUNANYWHERE_IDL_SCHEMA_SHA256 \"${RUNANYWHERE_KIT_IDL_SCHEMA_SHA256}\" +#define RUNANYWHERE_IDL_PROTOC_VERSION \"${RUNANYWHERE_KIT_IDL_PROTOC_VERSION}\" +#define RUNANYWHERE_IDL_PROTO_COUNT ${RUNANYWHERE_KIT_IDL_PROTO_COUNT} +") + +# Vendored protobuf + absl headers so PROTOBUF_VERSION in the generated +# gencode matches the runtime linked from the kit (not Homebrew). +if(NOT RAC_PROTOBUF_INCLUDE_DIR) + set(RAC_PROTOBUF_INCLUDE_DIR "${RAC_BINARY_DIR}/_deps/protobuf_fetched-src/src") +endif() +if(NOT RAC_ABSL_INCLUDE_DIR) + set(RAC_ABSL_INCLUDE_DIR "${RAC_BINARY_DIR}/_deps/absl-src") +endif() +if(NOT RAC_UTF8_RANGE_INCLUDE_DIR) + if(EXISTS "${RAC_BINARY_DIR}/_deps/utf8_range-src/utf8_range.h") + set(RAC_UTF8_RANGE_INCLUDE_DIR "${RAC_BINARY_DIR}/_deps/utf8_range-src") + elseif(EXISTS "${RAC_PROTOBUF_INCLUDE_DIR}/../third_party/utf8_range/utf8_range.h") + set(RAC_UTF8_RANGE_INCLUDE_DIR "${RAC_PROTOBUF_INCLUDE_DIR}/../third_party/utf8_range") + endif() +endif() +if(RAC_PROTOBUF_INCLUDE_DIR AND EXISTS "${RAC_PROTOBUF_INCLUDE_DIR}/google/protobuf/message.h") + file(COPY "${RAC_PROTOBUF_INCLUDE_DIR}/google" DESTINATION "${RAC_KIT_OUT}/include" + FILES_MATCHING PATTERN "*.h" PATTERN "*.inc" + PATTERN "compiler" EXCLUDE PATTERN "testdata" EXCLUDE) +endif() +if(RAC_ABSL_INCLUDE_DIR AND EXISTS "${RAC_ABSL_INCLUDE_DIR}/absl/strings/string_view.h") + file(COPY "${RAC_ABSL_INCLUDE_DIR}/absl" DESTINATION "${RAC_KIT_OUT}/include" + FILES_MATCHING PATTERN "*.h" PATTERN "*.inc" + PATTERN "testdata" EXCLUDE) +endif() +if(RAC_UTF8_RANGE_INCLUDE_DIR AND EXISTS "${RAC_UTF8_RANGE_INCLUDE_DIR}/utf8_range.h") + file(COPY "${RAC_UTF8_RANGE_INCLUDE_DIR}/utf8_range.h" DESTINATION "${RAC_KIT_OUT}/include") + if(EXISTS "${RAC_UTF8_RANGE_INCLUDE_DIR}/utf8_validity.h") + file(COPY "${RAC_UTF8_RANGE_INCLUDE_DIR}/utf8_validity.h" DESTINATION "${RAC_KIT_OUT}/include") + endif() +endif() +if(RUNANYWHERE_KIT_PROTOBUF_ISOLATED AND NOT EXISTS "${RAC_KIT_OUT}/include/google/protobuf/message.h") + message(FATAL_ERROR + "PackageCppDesktop: isolated protobuf headers missing at " + "${RAC_KIT_OUT}/include/google/protobuf/message.h " + "(RAC_PROTOBUF_INCLUDE_DIR=${RAC_PROTOBUF_INCLUDE_DIR})") +endif() +if(RUNANYWHERE_KIT_PROTOBUF_ISOLATED AND NOT EXISTS "${RAC_KIT_OUT}/include/absl/strings/string_view.h") + message(FATAL_ERROR + "PackageCppDesktop: absl headers missing at " + "${RAC_KIT_OUT}/include/absl/strings/string_view.h " + "(RAC_ABSL_INCLUDE_DIR=${RAC_ABSL_INCLUDE_DIR})") +endif() + +file(COPY "${RAC_COMMONS_FILE}" DESTINATION "${RAC_KIT_OUT}/lib") +get_filename_component(_commons_name "${RAC_COMMONS_FILE}" NAME) + +set(_extra_link "") +if(EXISTS "${RAC_KIT_LIBS_FILE}") + file(STRINGS "${RAC_KIT_LIBS_FILE}" _kit_libs) + foreach(_lib IN LISTS _kit_libs) + if(EXISTS "${_lib}" AND NOT "${_lib}" STREQUAL "${RAC_COMMONS_FILE}") + file(COPY "${_lib}" DESTINATION "${RAC_KIT_OUT}/lib") + get_filename_component(_n "${_lib}" NAME) + string(APPEND _extra_link "\${RunAnywhere_LIBRARY_DIR}/${_n};") + endif() + endforeach() +endif() + +if(RAC_BINARY_DIR) + foreach(_pattern + "libonnxruntime*" "onnxruntime*" "libsherpa-onnx*" "sherpa-onnx*" + "libomp*" "libggml*" "ggml*") + file(GLOB_RECURSE _hits + "${RAC_BINARY_DIR}/${_pattern}.dylib" + "${RAC_BINARY_DIR}/${_pattern}.so" + "${RAC_BINARY_DIR}/${_pattern}.so.*" + "${RAC_BINARY_DIR}/${_pattern}.dll") + foreach(_hit IN LISTS _hits) + # ONNX Runtime ships a versioned *dSYM companion* named + # libonnxruntime.1.x.y.dylib (Mach-O type MH_DSYM). Copying it + # and letting the real dylib's install name point at it makes + # dyld abort with "unloadable mach-o file type 10". + execute_process(COMMAND file --brief "${_hit}" OUTPUT_VARIABLE _ft + OUTPUT_STRIP_TRAILING_WHITESPACE) + if(NOT _ft MATCHES "dynamically linked shared library|shared object|DLL") + continue() + endif() + file(COPY "${_hit}" DESTINATION "${RAC_KIT_OUT}/third_party") + endforeach() + endforeach() + if(APPLE AND EXISTS "${RAC_KIT_OUT}/third_party/libonnxruntime.dylib") + # The real dylib advertises LC_ID_DYLIB @rpath/libonnxruntime.1.dylib. + # There is no loadable file by that name in the kit, so rewrite the id + # to the file we actually ship. + execute_process(COMMAND install_name_tool -id + "@rpath/libonnxruntime.dylib" + "${RAC_KIT_OUT}/third_party/libonnxruntime.dylib") + endif() + # Transitive static runtimes the CLI must link: isolated protobuf, libarchive. + file(GLOB_RECURSE _static_hits + "${RAC_BINARY_DIR}/*.a" + "${RAC_BINARY_DIR}/*.lib") + foreach(_hit IN LISTS _static_hits) + get_filename_component(_n "${_hit}" NAME) + if(_n MATCHES "^(libprotobuf|libprotobuf-lite)\\.(a|lib)$") + # Isolated commons protobuf (runanywhere_internal::). Rename so + # find_package(Protobuf) in consumers cannot pick this archive as + # vanilla google::protobuf. + if(_n MATCHES "\\.lib$") + set(_dst "rac_protobuf_isolated.lib") + if(_n MATCHES "protobuf-lite") + set(_dst "rac_protobuf_lite_isolated.lib") + endif() + else() + set(_dst "librac_protobuf_isolated.a") + if(_n STREQUAL "libprotobuf-lite.a") + set(_dst "librac_protobuf_lite_isolated.a") + endif() + endif() + file(COPY "${_hit}" DESTINATION "${RAC_KIT_OUT}/lib") + file(RENAME "${RAC_KIT_OUT}/lib/${_n}" "${RAC_KIT_OUT}/lib/${_dst}") + string(APPEND _extra_link "\${RunAnywhere_LIBRARY_DIR}/${_dst};") + elseif(_n MATCHES "^(libarchive|archive|libutf8_range|utf8_range|libutf8_validity|utf8_validity|libllama-common.*|llama-common.*)\\.(a|lib)$" + OR _n MATCHES "^libabsl_.*\\.a$" + OR _n MATCHES "^absl_.*\\.lib$") + file(COPY "${_hit}" DESTINATION "${RAC_KIT_OUT}/lib") + string(APPEND _extra_link "\${RunAnywhere_LIBRARY_DIR}/${_n};") + endif() + endforeach() +endif() + +file(WRITE "${RAC_KIT_OUT}/share/runanywhere/VERSION" "${RAC_VERSION}\n") +file(WRITE "${RAC_KIT_OUT}/share/runanywhere/PLUGIN_API_VERSION" "${RAC_PLUGIN_API_VERSION}\n") + +if(APPLE) + set(RUNANYWHERE_KIT_SYSTEM_LIBS "Threads::Threads;ZLIB::ZLIB;CURL::libcurl;dl;bz2") +elseif(WIN32) + set(RUNANYWHERE_KIT_SYSTEM_LIBS "ws2_32;crypt32;bcrypt;secur32") +else() + set(RUNANYWHERE_KIT_SYSTEM_LIBS "Threads::Threads;ZLIB::ZLIB;CURL::libcurl;dl;m") +endif() +set(RUNANYWHERE_KIT_EXTRA_LIBS "${_extra_link}") +if(NOT RUNANYWHERE_KIT_COMMONS_ARCHIVE) + set(RUNANYWHERE_KIT_COMMONS_ARCHIVE "${_commons_name}") +endif() + +configure_file( + "${RAC_SOURCE_DIR}/cmake/RunAnywhereConfig.cmake.in" + "${RAC_KIT_OUT}/lib/cmake/RunAnywhere/RunAnywhereConfig.cmake" + @ONLY) + +file(WRITE "${RAC_KIT_OUT}/lib/cmake/RunAnywhere/RunAnywhereConfigVersion.cmake" +"set(PACKAGE_VERSION \"${RAC_VERSION}\") +set(PACKAGE_VERSION_COMPATIBLE FALSE) +set(PACKAGE_VERSION_EXACT FALSE) +if(PACKAGE_FIND_VERSION STREQUAL PACKAGE_VERSION) + set(PACKAGE_VERSION_COMPATIBLE TRUE) + set(PACKAGE_VERSION_EXACT TRUE) +elseif(PACKAGE_FIND_VERSION_MAJOR EQUAL ${RAC_VERSION}) + if(PACKAGE_FIND_VERSION VERSION_LESS_EQUAL PACKAGE_VERSION) + set(PACKAGE_VERSION_COMPATIBLE TRUE) + endif() +endif() +") + +message(STATUS "C++ desktop kit staged at ${RAC_KIT_OUT}") diff --git a/cmake/RunAnywhereConfig.cmake.in b/cmake/RunAnywhereConfig.cmake.in new file mode 100644 index 0000000000..395c79bfc3 --- /dev/null +++ b/cmake/RunAnywhereConfig.cmake.in @@ -0,0 +1,120 @@ +# RunAnywhere C++ desktop kit — imported from a packaged prefix. +# +# find_package(RunAnywhere @RAC_VERSION@ EXACT REQUIRED) +# target_link_libraries(app PRIVATE RunAnywhere::commons) +# +# Generated. Do not edit the copy inside a tarball. + +get_filename_component(PACKAGE_PREFIX_DIR "${CMAKE_CURRENT_LIST_DIR}/../../../" ABSOLUTE) + +set(RunAnywhere_VERSION "@RAC_VERSION@") +set(RunAnywhere_PLUGIN_API_VERSION "@RAC_PLUGIN_API_VERSION@") +set(RunAnywhere_IDL_DIR "${PACKAGE_PREFIX_DIR}/share/runanywhere/idl") +set(RunAnywhere_IDL_VERSION "@RUNANYWHERE_KIT_IDL_VERSION@") +set(RunAnywhere_IDL_SCHEMA_SHA256 "@RUNANYWHERE_KIT_IDL_SCHEMA_SHA256@") +set(RunAnywhere_IDL_PROTOC_VERSION "@RUNANYWHERE_KIT_IDL_PROTOC_VERSION@") +set(RunAnywhere_SCHEMA_LOCK "${PACKAGE_PREFIX_DIR}/share/runanywhere/SCHEMA_LOCK") +set(RunAnywhere_PROTO_INCLUDE_DIR "${PACKAGE_PREFIX_DIR}/include/runanywhere/proto") +set(RunAnywhere_INCLUDE_DIR "${PACKAGE_PREFIX_DIR}/include") +set(RunAnywhere_LIBRARY_DIR "${PACKAGE_PREFIX_DIR}/lib") +set(RunAnywhere_THIRD_PARTY_DIR "${PACKAGE_PREFIX_DIR}/third_party") +set(RunAnywhere_PROTOBUF_ISOLATED @RUNANYWHERE_KIT_PROTOBUF_ISOLATED@) + +set(RunAnywhere_HAS_LLAMACPP @RUNANYWHERE_KIT_HAS_LLAMACPP@) +set(RunAnywhere_HAS_ONNX @RUNANYWHERE_KIT_HAS_ONNX@) +set(RunAnywhere_HAS_SHERPA @RUNANYWHERE_KIT_HAS_SHERPA@) +set(RunAnywhere_HAS_MLX @RUNANYWHERE_KIT_HAS_MLX@) +set(RunAnywhere_HAS_NEURT @RUNANYWHERE_KIT_HAS_NEURT@) +set(RunAnywhere_HAS_CLOUD @RUNANYWHERE_KIT_HAS_CLOUD@) +set(RunAnywhere_HAS_QHEXRT @RUNANYWHERE_KIT_HAS_QHEXRT@) +set(RunAnywhere_HAS_RAG @RUNANYWHERE_KIT_HAS_RAG@) +set(RunAnywhere_HAS_SERVER @RUNANYWHERE_KIT_HAS_SERVER@) +set(RunAnywhere_HAS_DESKTOP_ADAPTER @RUNANYWHERE_KIT_HAS_DESKTOP_ADAPTER@) + +include(CMakeFindDependencyMacro) +if(NOT WIN32) + find_dependency(Threads) + find_dependency(ZLIB) + find_dependency(CURL) +endif() + +if(NOT TARGET RunAnywhere::commons) + add_library(RunAnywhere::commons STATIC IMPORTED) + set(_runanywhere_inc + "${RunAnywhere_INCLUDE_DIR}" + "${RunAnywhere_PROTO_INCLUDE_DIR}") + set(_runanywhere_defs "RAC_HAVE_PROTOBUF=1") + if(RunAnywhere_PROTOBUF_ISOLATED) + # Same token rewrite commons used to compile *.pb.cc into this archive. + list(APPEND _runanywhere_defs "google=runanywhere_internal") + endif() + set_target_properties(RunAnywhere::commons PROPERTIES + IMPORTED_LOCATION "${RunAnywhere_LIBRARY_DIR}/@RUNANYWHERE_KIT_COMMONS_ARCHIVE@" + INTERFACE_INCLUDE_DIRECTORIES "${_runanywhere_inc}" + INTERFACE_COMPILE_FEATURES "cxx_std_20" + INTERFACE_COMPILE_DEFINITIONS "${_runanywhere_defs}" + ) + set(_runanywhere_kit_extra "@RUNANYWHERE_KIT_EXTRA_LIBS@") + file(GLOB _kit_archives + "${RunAnywhere_LIBRARY_DIR}/*.a" + "${RunAnywhere_LIBRARY_DIR}/*.lib") + foreach(_archive IN LISTS _kit_archives) + get_filename_component(_an "${_archive}" NAME) + if(NOT _an STREQUAL "@RUNANYWHERE_KIT_COMMONS_ARCHIVE@") + # Static plugin archives register via ctor; the linker otherwise + # drops them because rcli never references a symbol inside. + if(_an MATCHES "^(lib)?rac_backend_") + if(APPLE) + list(APPEND _runanywhere_kit_extra "-Wl,-force_load,${_archive}") + elseif(MSVC) + list(APPEND _runanywhere_kit_extra "/WHOLEARCHIVE:${_archive}") + else() + list(APPEND _runanywhere_kit_extra + "-Wl,--whole-archive" "${_archive}" "-Wl,--no-whole-archive") + endif() + else() + list(APPEND _runanywhere_kit_extra "${_archive}") + endif() + endif() + endforeach() + # Link only the unversioned ONNX Runtime dylib. Versioned copies + # (libonnxruntime.1.dylib, libonnxruntime.1.*.dylib) are Mach-O stubs / + # compatibility names and fail with "unsupported mach-o filetype". + file(GLOB _kit_dylibs + "${RunAnywhere_THIRD_PARTY_DIR}/libonnxruntime.dylib" + "${RunAnywhere_THIRD_PARTY_DIR}/libonnxruntime.so" + "${RunAnywhere_THIRD_PARTY_DIR}/onnxruntime.dll") + list(APPEND _runanywhere_kit_extra ${_kit_dylibs}) + if(_runanywhere_kit_extra) + list(REMOVE_DUPLICATES _runanywhere_kit_extra) + set_property(TARGET RunAnywhere::commons APPEND PROPERTY + INTERFACE_LINK_LIBRARIES ${_runanywhere_kit_extra}) + endif() + set(_runanywhere_kit_sys "@RUNANYWHERE_KIT_SYSTEM_LIBS@") + if(_runanywhere_kit_sys) + set_property(TARGET RunAnywhere::commons APPEND PROPERTY + INTERFACE_LINK_LIBRARIES ${_runanywhere_kit_sys}) + endif() + if(APPLE) + set_property(TARGET RunAnywhere::commons APPEND PROPERTY + INTERFACE_LINK_LIBRARIES + "-framework Foundation" "-framework Accelerate" "-framework Metal" + "-framework MetalKit" "-framework CoreML") + endif() +endif() + +foreach(_opt mlx neurt qhexrt) + string(TOUPPER ${_opt} _OPT) + if(RunAnywhere_HAS_${_OPT} + AND EXISTS "${RunAnywhere_LIBRARY_DIR}/librac_backend_${_opt}${CMAKE_STATIC_LIBRARY_SUFFIX}") + if(NOT TARGET RunAnywhere::${_opt}) + add_library(RunAnywhere::${_opt} STATIC IMPORTED) + set_target_properties(RunAnywhere::${_opt} PROPERTIES + IMPORTED_LOCATION + "${RunAnywhere_LIBRARY_DIR}/librac_backend_${_opt}${CMAKE_STATIC_LIBRARY_SUFFIX}" + INTERFACE_LINK_LIBRARIES RunAnywhere::commons) + endif() + endif() +endforeach() + +set(RunAnywhere_FOUND TRUE) diff --git a/docs/reference/cpp-desktop-kit.md b/docs/reference/cpp-desktop-kit.md new file mode 100644 index 0000000000..85677d9d5d --- /dev/null +++ b/docs/reference/cpp-desktop-kit.md @@ -0,0 +1,65 @@ +# C++ desktop kit + +The kit is the **only** supported way for [RCLI](https://github.com/RunanywhereAI/RCLI) +to consume this SDK. It is not an rcli binary. + +## Layout + +``` +include/rac/** public C ABI +include/runanywhere/proto/*.pb.h generated messages (same protoc as commons) +include/google/protobuf/** vendored runtime headers (PROTOBUF_VERSION pin) +include/absl/** vendored absl headers the runtime needs +lib/librac_commons.a STATIC_PLUGINS=ON, DESKTOP_ADAPTER=ON +lib/cmake/RunAnywhere/RunAnywhereConfig.cmake +share/runanywhere/idl/*.proto wire schema +share/runanywhere/SCHEMA_LOCK proto fingerprint (idl/SCHEMA_LOCK) +share/runanywhere/VERSION +third_party/ onnxruntime / sherpa shared libs if needed +``` + +## Protobuf contract (SOT across SDK and RCLI) + +`idl/*.proto` is the schema. There is **one** C++ compilation of that schema: +commons, packed into `librac_commons.a`. The kit also ships the matching +`*.pb.h` plus the exact protobuf/absl headers commons was built against. + +RCLI includes those headers and uses `runanywhere::v1::*` at the `rac_*` byte +boundary (`ParseFromArray` / `SerializeToString`). It must not run `protoc`, +compile `*.pb.cc`, or `find_package(Protobuf)` against Homebrew. + +Cross-repo consistency is `idl/SCHEMA_LOCK` (digest of every `.proto` + +`IDL_VERSION` + pinned protoc). The kit copies that lock into +`share/runanywhere/SCHEMA_LOCK` and `find_package(RunAnywhere)` exports +`RunAnywhere_IDL_SCHEMA_SHA256`. RCLI pins the same values in +`cmake/sdk-pin.cmake` and configure-fails on mismatch. Bump the pin when you +consume a new kit — never regenerate headers in RCLI. + +`find_package(RunAnywhere)` puts the proto include path and, when the kit was +built with namespace isolation, `google=runanywhere_internal` on +`RunAnywhere::commons`. + +## Build locally + +```bash +./scripts/build/package-cpp-desktop.sh # macOS arm64 default +cmake --preset cpp-desktop-windows-x64 && cmake --build --preset cpp-desktop-windows-x64 +``` + +Then in RCLI: + +```bash +cmake -B build -DCMAKE_PREFIX_PATH=/path/to/dist/cpp-desktop-macos-arm64 +``` + +`find_package(RunAnywhere ${PIN} EXACT REQUIRED)` links `RunAnywhere::commons`. + +## Private packs + +NeuRT (Apple ANE) and QHexRT (Windows ARM64 NPU) are **not** in the public kit. + +```bash +NEURUN_TOKEN=... ./scripts/build/fetch-private-engine-pack.sh neurt macos-arm64 +``` + +RCLI defines `RCLI_HAS_NEURT` only when `RunAnywhere::neurt` (or the pack) is present. diff --git a/docs/reference/generated-code-contract.md b/docs/reference/generated-code-contract.md index b88a202aee..0efcd8b175 100644 --- a/docs/reference/generated-code-contract.md +++ b/docs/reference/generated-code-contract.md @@ -79,3 +79,19 @@ bumping `idl/VERSION` also fails. CI `idl-drift-check.yml` is **generate, then verify** — not "regenerate and diff", which cannot fail for an ignored file. + +## Downstream kits (RCLI) + +The C++ desktop kit (`package-cpp-desktop`) is how a second repo stays on this +schema without running `protoc`: + +1. `idl/*.proto` is the only schema. +2. Commons compiles it **once**; `.pb.cc` lives inside `librac_commons.a`. +3. The kit ships the matching `include/runanywhere/proto/*.pb.h`, vendored + protobuf/absl headers, and a copy of `idl/SCHEMA_LOCK`. +4. RCLI `find_package(RunAnywhere)` consumes those headers and pins + `IDL_SCHEMA_SHA256` in `cmake/sdk-pin.cmake`. A kit whose lock does not + match is a configure error. + +Do not add a second codegen path in RCLI. When the schema moves, cut a new +SDK kit and bump the RCLI pin. diff --git a/rcli/README.md b/rcli/README.md index 77f7c3a4ff..0027e1432a 100644 --- a/rcli/README.md +++ b/rcli/README.md @@ -1,3 +1,11 @@ +# RunAnywhere CLI (`rcli`) — in-tree copy, retired + +**Do not build this tree.** The official CLI is [RunanywhereAI/RCLI](https://github.com/RunanywhereAI/RCLI) (`EXTERNAL/RCLI` in this checkout). It links a published C++ desktop kit (`find_package(RunAnywhere)`), not this subdirectory. + +This directory remains only as a reference for command coverage until the next SDK release drops it. + +--- + # RunAnywhere CLI (`rcli`) Run, manage, and serve on-device AI models from the terminal. One binary, multi-modal: LLM chat, VLM image understanding, speech-to-text, text-to-speech, voice activity detection, and a full voice pipeline — all running locally on the RunAnywhere C++ core. diff --git a/scripts/build/fetch-private-engine-pack.sh b/scripts/build/fetch-private-engine-pack.sh new file mode 100755 index 0000000000..1e430fda5c --- /dev/null +++ b/scripts/build/fetch-private-engine-pack.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# Fetch a private engine pack (NeuRT / QHexRT) from the neurun GitHub release +# that core/VERSIONS pins. Public C++ desktop kits never include these. +# +# NEURUN_TOKEN=... scripts/build/fetch-private-engine-pack.sh neurt macos-arm64 +# NEURUN_TOKEN=... scripts/build/fetch-private-engine-pack.sh qhexrt windows-arm64 +set -euo pipefail +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +ENGINE="${1:?engine: neurt|qhexrt}" +SLICE="${2:?slice e.g. macos-arm64|windows-arm64}" +# shellcheck source=/dev/null +source "$ROOT/core/scripts/load-versions.sh" +TOKEN="${NEURUN_TOKEN:-${GH_TOKEN:-}}" +if [[ -z "$TOKEN" ]]; then + echo "NEURUN_TOKEN (or GH_TOKEN) is required to fetch private engine packs." >&2 + exit 3 +fi +case "$ENGINE" in + neurt) + exec "$ROOT/scripts/build/download-neurt.sh" --slice "$SLICE" + ;; + qhexrt) + exec "$ROOT/scripts/build/download-qhexrt.sh" --abi "$SLICE" + ;; + *) + echo "unknown engine $ENGINE" >&2 + exit 2 + ;; +esac diff --git a/scripts/build/package-cpp-desktop.sh b/scripts/build/package-cpp-desktop.sh new file mode 100755 index 0000000000..abf3ac179c --- /dev/null +++ b/scripts/build/package-cpp-desktop.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# Build and stage the C++ desktop kit (headers + static commons + IDL). +# Does not produce an rcli binary. +# +# scripts/build/package-cpp-desktop.sh [preset] +# default preset: cpp-desktop-macos-arm64 on Darwin, cpp-desktop-windows-x64 else +set -euo pipefail +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +cd "$ROOT" +PRESET="${1:-}" +if [[ -z "$PRESET" ]]; then + case "$(uname -s)" in + Darwin) PRESET=cpp-desktop-macos-arm64 ;; + Linux) PRESET=cpp-desktop-macos-arm64; echo "WARN: using macOS preset name; override for linux" >&2 ;; + *) PRESET=cpp-desktop-windows-x64 ;; + esac +fi +cmake --preset "$PRESET" +cmake --build --preset "$PRESET" --target package-cpp-desktop-tarball +echo "Kit under $ROOT/dist/" +ls -la "$ROOT/dist"/RunAnywhere-cpp-desktop-* 2>/dev/null || ls -la "$ROOT/dist"/cpp-desktop-* diff --git a/tests/kit/test_cpp_desktop_kit.c b/tests/kit/test_cpp_desktop_kit.c new file mode 100644 index 0000000000..99b65350e7 --- /dev/null +++ b/tests/kit/test_cpp_desktop_kit.c @@ -0,0 +1,16 @@ +/* Public-ABI smoke for the C++ desktop kit. No protobuf, no CLI. */ +#include +#include + +#include "rac/core/rac_core.h" + +int main(void) { + const rac_version_t version = rac_get_version(); + if (version.string == NULL || version.string[0] == '\0') { + fprintf(stderr, "rac_get_version returned an empty string\n"); + return 1; + } + printf("RunAnywhere %s (%u.%u.%u)\n", version.string, (unsigned)version.major, + (unsigned)version.minor, (unsigned)version.patch); + return 0; +} From 3db4c20603b11ac2b67d842004deba49dedb3e3f Mon Sep 17 00:00:00 2001 From: Sanchit Monga Date: Sun, 23 Aug 2026 17:46:55 -0700 Subject: [PATCH 02/19] fix(ci): do not use setup-toolchain on the macOS C++ kit job macos-14 does not ship Xcode 26.6, which setup-toolchain requires. Install ninja from Homebrew and use the image compiler instead. Co-authored-by: Cursor --- .github/workflows/cpp-desktop-kit.yml | 5 ++--- .github/workflows/release.yml | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/cpp-desktop-kit.yml b/.github/workflows/cpp-desktop-kit.yml index 46e0d04ea7..5452a96fbb 100644 --- a/.github/workflows/cpp-desktop-kit.yml +++ b/.github/workflows/cpp-desktop-kit.yml @@ -43,9 +43,8 @@ jobs: timeout-minutes: 90 steps: - uses: actions/checkout@v4 - - uses: ./.github/actions/setup-toolchain - with: - platform: macos + - name: Install ninja + run: brew install ninja - name: Configure + package run: | set -euo pipefail diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5557ec49a2..8ede527ab5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -777,9 +777,8 @@ jobs: timeout-minutes: 90 steps: - uses: actions/checkout@v7 - - uses: ./.github/actions/setup-toolchain - with: - platform: macos + - name: Install ninja + run: brew install ninja - name: Configure + package C++ desktop kit run: | set -euo pipefail From 90597da043c4dc98a59f4167221212012b3a54a9 Mon Sep 17 00:00:00 2001 From: Sanchit Monga Date: Sun, 23 Aug 2026 18:01:57 -0700 Subject: [PATCH 03/19] fix(windows): target AMD64 when building the x64 desktop kit on WoA CMake reports CMAKE_SYSTEM_PROCESSOR=ARM64 on Snapdragon hosts even when vcvarsall amd64 has selected Hostx64/x64/cl.exe. ggml then takes its ARM MSVC path and fatals ("use clang"). Pin the x64 kit preset to AMD64 and detect the same mismatch in the llama.cpp engine so configure matches the compiler we actually invoked. --- CMakePresets.json | 1 + engines/llamacpp/CMakeLists.txt | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/CMakePresets.json b/CMakePresets.json index 07f2584070..f7612997ec 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -118,6 +118,7 @@ "cacheVariables": { "CMAKE_TOOLCHAIN_FILE": "$env{VCPKG_INSTALLATION_ROOT}/scripts/buildsystems/vcpkg.cmake", "VCPKG_TARGET_TRIPLET": "x64-windows-static", + "CMAKE_SYSTEM_PROCESSOR": "AMD64", "RAC_BUILD_CLI": "OFF", "RAC_PACKAGE_CPP_DESKTOP": "ON", "RAC_DESKTOP_ADAPTER": "ON", diff --git a/engines/llamacpp/CMakeLists.txt b/engines/llamacpp/CMakeLists.txt index 54ae1c37b4..30d99f09da 100644 --- a/engines/llamacpp/CMakeLists.txt +++ b/engines/llamacpp/CMakeLists.txt @@ -182,6 +182,25 @@ elseif(RAC_PLATFORM_WINDOWS) set(GGML_CUDA OFF CACHE BOOL "" FORCE) message(STATUS "Configuring llama.cpp for Windows (no CUDA; Vulkan=${GGML_VULKAN})") endif() + + # ggml's MSVC ARM backend is an explicit FATAL_ERROR ("use clang"). + # Windows-on-ARM hosts still report CMAKE_SYSTEM_PROCESSOR=ARM64 even + # when vcvarsall amd64 has selected Hostx64/x64/cl.exe, which is how + # the cpp-desktop-windows-x64 kit is built on Snapdragon boxes under + # Prism. Force AMD64 so ggml takes the AVX path, not ARM. + if(MSVC AND CMAKE_SYSTEM_PROCESSOR MATCHES "[Aa][Rr][Mm]|aarch64") + if(CMAKE_CXX_COMPILER MATCHES "[/\\\\]Hostx64[/\\\\]x64[/\\\\]" + OR CMAKE_CXX_COMPILER MATCHES "[/\\\\]x64[/\\\\]cl\\.exe$" + OR CMAKE_CXX_COMPILER_ARCHITECTURE_ID STREQUAL "x64") + set(CMAKE_SYSTEM_PROCESSOR AMD64) + message(STATUS "llama.cpp: x64 MSVC on ARM host — CMAKE_SYSTEM_PROCESSOR=AMD64") + else() + message(FATAL_ERROR + "llama.cpp cannot be built with MSVC targeting ARM. " + "Use vcvarsall amd64 (x64 kit under Prism) or clang, " + "or set RAC_BACKEND_LLAMACPP=OFF.") + endif() + endif() endif() set(BUILD_SHARED_LIBS OFF CACHE BOOL "Force static libraries for llama.cpp" FORCE) From 0899a75b67aff444e78f4bd2f7fc2bd407273ddf Mon Sep 17 00:00:00 2001 From: Sanchit Monga Date: Sun, 23 Aug 2026 18:18:34 -0700 Subject: [PATCH 04/19] ci: reject C++ desktop kit tarballs that omit proto headers The v0.20.25 macOS kit originally uploaded to GitHub was a 32MB archive without include/runanywhere/proto or SCHEMA_LOCK, so RCLI CI failed on `test -f` with no useful log. GHA already staged a complete prefix; assert the tarball contains those files before uploading the artifact. --- .github/workflows/cpp-desktop-kit.yml | 28 +++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/.github/workflows/cpp-desktop-kit.yml b/.github/workflows/cpp-desktop-kit.yml index 5452a96fbb..d53b8cd23a 100644 --- a/.github/workflows/cpp-desktop-kit.yml +++ b/.github/workflows/cpp-desktop-kit.yml @@ -52,6 +52,21 @@ jobs: cmake --build --preset cpp-desktop-macos-arm64 --target package-cpp-desktop-tarball \ -j "$(sysctl -n hw.logicalcpu)" ls -la dist/RunAnywhere-cpp-desktop-macos-arm64-v*.tar.gz + python3 - <<'PY' + import glob, sys, tarfile + tars = glob.glob("dist/RunAnywhere-cpp-desktop-macos-arm64-v*.tar.gz") + if not tars: + sys.exit("no macOS kit tarball") + names = tarfile.open(tars[0]).getnames() + need = ( + "include/runanywhere/proto/model_types.pb.h", + "share/runanywhere/SCHEMA_LOCK", + ) + missing = [n for n in need if not any(x.endswith(n) for x in names)] + if missing: + sys.exit("kit tarball missing: " + ", ".join(missing)) + print(f"kit ok: {len(names)} entries") + PY shasum -a 256 dist/RunAnywhere-cpp-desktop-macos-arm64-v*.tar.gz \ | tee dist/RunAnywhere-cpp-desktop-macos-arm64-v$(tr -d '[:space:]' < core/VERSION).tar.gz.sha256 - uses: actions/upload-artifact@v4 @@ -86,6 +101,19 @@ jobs: $ver = (Get-Content core/VERSION -Raw).Trim() $tar = "dist/RunAnywhere-cpp-desktop-windows-x64-v$ver.tar.gz" if (-not (Test-Path $tar)) { throw "missing $tar" } + python -c @" + import sys, tarfile + names = tarfile.open(r'$tar').getnames() + need = ( + 'include/runanywhere/proto/model_types.pb.h', + 'share/runanywhere/SCHEMA_LOCK', + ) + missing = [n for n in need if not any(x.endswith(n) for x in names)] + if missing: + sys.exit('kit tarball missing: ' + ', '.join(missing)) + print(f'kit ok: {len(names)} entries') + "@ + if ($LASTEXITCODE -ne 0) { throw "kit tarball contents check failed" } Get-FileHash $tar -Algorithm SHA256 | ForEach-Object { "$($_.Hash.ToLower()) $(Split-Path $_.Path -Leaf)" } | Set-Content "$tar.sha256" From 724b21ca5d2dbfb7556a5c76288af24b802da64c Mon Sep 17 00:00:00 2001 From: Sanchit Monga Date: Sun, 23 Aug 2026 18:53:12 -0700 Subject: [PATCH 05/19] fix(kit): use CMake WHOLE_ARCHIVE genex on MSVC Putting /WHOLEARCHIVE:C:/... in INTERFACE_LINK_LIBRARIES makes Ninja treat the flag as a path and fail FindFirstFileExA when RCLI links the kit. Use $ instead. --- cmake/RunAnywhereConfig.cmake.in | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/cmake/RunAnywhereConfig.cmake.in b/cmake/RunAnywhereConfig.cmake.in index 395c79bfc3..72b041e0da 100644 --- a/cmake/RunAnywhereConfig.cmake.in +++ b/cmake/RunAnywhereConfig.cmake.in @@ -64,10 +64,15 @@ if(NOT TARGET RunAnywhere::commons) # Static plugin archives register via ctor; the linker otherwise # drops them because rcli never references a symbol inside. if(_an MATCHES "^(lib)?rac_backend_") + # Must be a link feature/flag, not INTERFACE_LINK_LIBRARIES + # with a leading slash — Ninja then stats `/WHOLEARCHIVE:C:/...` + # as a path and fails FindFirstFileExA. if(APPLE) - list(APPEND _runanywhere_kit_extra "-Wl,-force_load,${_archive}") + list(APPEND _runanywhere_kit_extra + "-Wl,-force_load,${_archive}") elseif(MSVC) - list(APPEND _runanywhere_kit_extra "/WHOLEARCHIVE:${_archive}") + list(APPEND _runanywhere_kit_extra + "$") else() list(APPEND _runanywhere_kit_extra "-Wl,--whole-archive" "${_archive}" "-Wl,--no-whole-archive") From 09e18f89bf4a3a77160fcbf0acdf7fa4f7af6816 Mon Sep 17 00:00:00 2001 From: Sanchit Monga Date: Sun, 23 Aug 2026 20:06:56 -0700 Subject: [PATCH 06/19] fix(kit): address CodeRabbit review of the C++ desktop kit Tighten workflow permissions, keep in-flight Windows kit jobs, quote the checksum path, and fail closed when Linux has no kit preset. Packaging now guards IDL_PROTO_COUNT, Apple-only file(1) filtering, Windows libcurl/zlib export, and whole-archive backend linking without flattening flag pairs. Co-authored-by: Cursor --- .github/workflows/cpp-desktop-kit.yml | 11 ++++-- .github/workflows/release.yml | 2 +- CMakePresets.json | 3 +- cmake/CppDesktopKit.cmake | 4 +++ cmake/PackageCppDesktop.cmake | 40 ++++++++++++++++------ cmake/RunAnywhereConfig.cmake.in | 13 ++++--- docs/reference/cpp-desktop-kit.md | 2 +- scripts/build/fetch-private-engine-pack.sh | 8 ++++- scripts/build/package-cpp-desktop.sh | 5 ++- 9 files changed, 63 insertions(+), 25 deletions(-) diff --git a/.github/workflows/cpp-desktop-kit.yml b/.github/workflows/cpp-desktop-kit.yml index d53b8cd23a..f1003efbaa 100644 --- a/.github/workflows/cpp-desktop-kit.yml +++ b/.github/workflows/cpp-desktop-kit.yml @@ -28,14 +28,17 @@ on: - 'cmake/PackageCppDesktop.cmake' - 'cmake/RunAnywhereConfig.cmake.in' - 'CMakePresets.json' + - 'core/**' + - 'engines/**' + - 'idl/**' - '.github/workflows/cpp-desktop-kit.yml' permissions: - contents: write + contents: read concurrency: group: cpp-desktop-kit-${{ github.ref }} - cancel-in-progress: true + cancel-in-progress: false jobs: macos-arm64: @@ -68,7 +71,7 @@ jobs: print(f"kit ok: {len(names)} entries") PY shasum -a 256 dist/RunAnywhere-cpp-desktop-macos-arm64-v*.tar.gz \ - | tee dist/RunAnywhere-cpp-desktop-macos-arm64-v$(tr -d '[:space:]' < core/VERSION).tar.gz.sha256 + | tee "dist/RunAnywhere-cpp-desktop-macos-arm64-v$(tr -d '[:space:]' < core/VERSION).tar.gz.sha256" - uses: actions/upload-artifact@v4 with: name: cpp-desktop-macos-arm64 @@ -129,6 +132,8 @@ jobs: if: github.event_name == 'workflow_dispatch' && inputs.attach_tag != '' needs: [macos-arm64, windows-x64] runs-on: ubuntu-24.04 + permissions: + contents: write steps: - uses: actions/download-artifact@v4 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8ede527ab5..3ab53ef68d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -787,7 +787,7 @@ jobs: -j "$(sysctl -n hw.logicalcpu)" ls -la dist/RunAnywhere-cpp-desktop-macos-arm64-v*.tar.gz shasum -a 256 dist/RunAnywhere-cpp-desktop-macos-arm64-v*.tar.gz \ - | tee dist/RunAnywhere-cpp-desktop-macos-arm64-v$(tr -d '[:space:]' < core/VERSION).tar.gz.sha256 + | tee "dist/RunAnywhere-cpp-desktop-macos-arm64-v$(tr -d '[:space:]' < core/VERSION).tar.gz.sha256" - uses: actions/upload-artifact@v7 with: name: cpp-desktop-macos diff --git a/CMakePresets.json b/CMakePresets.json index f7612997ec..27b6ddf14c 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -107,7 +107,8 @@ "RAC_STATIC_PLUGINS": "ON", "RAC_BUILD_SHARED": "OFF", "RAC_BUILD_PLATFORM": "ON", - "CMAKE_OSX_ARCHITECTURES": "arm64" + "CMAKE_OSX_ARCHITECTURES": "arm64", + "GGML_METAL": "ON" }, "condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Darwin" } }, diff --git a/cmake/CppDesktopKit.cmake b/cmake/CppDesktopKit.cmake index 9faea36a76..1d06dc550c 100644 --- a/cmake/CppDesktopKit.cmake +++ b/cmake/CppDesktopKit.cmake @@ -168,6 +168,9 @@ add_custom_target(package-cpp-desktop VERBATIM COMMENT "Staging C++ desktop kit at ${_kit_out}" ) +if(_kit_depends) + add_dependencies(package-cpp-desktop ${_kit_depends}) +endif() # Tarball next to the prefix for release.yml to pick up. add_custom_target(package-cpp-desktop-tarball @@ -180,3 +183,4 @@ add_custom_target(package-cpp-desktop-tarball WORKING_DIRECTORY "${RAC_SOURCE_DIR}/dist" COMMENT "Tarring C++ desktop kit" ) +add_dependencies(package-cpp-desktop-tarball package-cpp-desktop) diff --git a/cmake/PackageCppDesktop.cmake b/cmake/PackageCppDesktop.cmake index 2cd1f96e07..6da0db1678 100644 --- a/cmake/PackageCppDesktop.cmake +++ b/cmake/PackageCppDesktop.cmake @@ -61,6 +61,9 @@ _kit_lock_key(IDL_PROTO_COUNT RUNANYWHERE_KIT_IDL_PROTO_COUNT) if(NOT RUNANYWHERE_KIT_IDL_SCHEMA_SHA256 OR NOT RUNANYWHERE_KIT_IDL_VERSION) message(FATAL_ERROR "PackageCppDesktop: idl/SCHEMA_LOCK is missing IDL_SCHEMA_SHA256 or IDL_VERSION") endif() +if(NOT RUNANYWHERE_KIT_IDL_PROTO_COUNT MATCHES "^[0-9]+$") + message(FATAL_ERROR "PackageCppDesktop: idl/SCHEMA_LOCK is missing a numeric IDL_PROTO_COUNT") +endif() file(WRITE "${RAC_KIT_OUT}/include/runanywhere/proto/schema_lock.h" "#pragma once // Generated from idl/SCHEMA_LOCK. Do not edit. @@ -124,7 +127,12 @@ if(EXISTS "${RAC_KIT_LIBS_FILE}") if(EXISTS "${_lib}" AND NOT "${_lib}" STREQUAL "${RAC_COMMONS_FILE}") file(COPY "${_lib}" DESTINATION "${RAC_KIT_OUT}/lib") get_filename_component(_n "${_lib}" NAME) - string(APPEND _extra_link "\${RunAnywhere_LIBRARY_DIR}/${_n};") + # Backends are whole-archived from a GLOB in RunAnywhereConfig.cmake. + # Listing them here as well would duplicate the archive as a lazy + # extra and then REMOVE_DUPLICATES would strip --whole-archive flags. + if(NOT _n MATCHES "^(lib)?rac_backend_") + string(APPEND _extra_link "\${RunAnywhere_LIBRARY_DIR}/${_n};") + endif() endif() endforeach() endif() @@ -143,10 +151,13 @@ if(RAC_BINARY_DIR) # libonnxruntime.1.x.y.dylib (Mach-O type MH_DSYM). Copying it # and letting the real dylib's install name point at it makes # dyld abort with "unloadable mach-o file type 10". - execute_process(COMMAND file --brief "${_hit}" OUTPUT_VARIABLE _ft - OUTPUT_STRIP_TRAILING_WHITESPACE) - if(NOT _ft MATCHES "dynamically linked shared library|shared object|DLL") - continue() + if(APPLE) + execute_process(COMMAND file --brief "${_hit}" OUTPUT_VARIABLE _ft + OUTPUT_STRIP_TRAILING_WHITESPACE + RESULT_VARIABLE _ft_rc) + if(NOT _ft_rc EQUAL 0 OR NOT _ft MATCHES "dynamically linked shared library") + continue() + endif() endif() file(COPY "${_hit}" DESTINATION "${RAC_KIT_OUT}/third_party") endforeach() @@ -198,7 +209,20 @@ file(WRITE "${RAC_KIT_OUT}/share/runanywhere/PLUGIN_API_VERSION" "${RAC_PLUGIN_A if(APPLE) set(RUNANYWHERE_KIT_SYSTEM_LIBS "Threads::Threads;ZLIB::ZLIB;CURL::libcurl;dl;bz2") elseif(WIN32) - set(RUNANYWHERE_KIT_SYSTEM_LIBS "ws2_32;crypt32;bcrypt;secur32") + # libcurl (static vcpkg) needs these plus the curl/zlib archives themselves. + set(RUNANYWHERE_KIT_SYSTEM_LIBS "ws2_32;crypt32;bcrypt;secur32;wldap32;normaliz;advapi32") + foreach(_root IN ITEMS "$ENV{VCPKG_INSTALLATION_ROOT}" "$ENV{VCPKG_ROOT}") + if(_root AND EXISTS "${_root}/installed/x64-windows-static/lib") + set(_vlib "${_root}/installed/x64-windows-static/lib") + foreach(_n IN ITEMS libcurl.lib zlib.lib) + if(EXISTS "${_vlib}/${_n}") + file(COPY "${_vlib}/${_n}" DESTINATION "${RAC_KIT_OUT}/lib") + string(APPEND _extra_link "\${RunAnywhere_LIBRARY_DIR}/${_n};") + endif() + endforeach() + break() + endif() + endforeach() else() set(RUNANYWHERE_KIT_SYSTEM_LIBS "Threads::Threads;ZLIB::ZLIB;CURL::libcurl;dl;m") endif() @@ -219,10 +243,6 @@ set(PACKAGE_VERSION_EXACT FALSE) if(PACKAGE_FIND_VERSION STREQUAL PACKAGE_VERSION) set(PACKAGE_VERSION_COMPATIBLE TRUE) set(PACKAGE_VERSION_EXACT TRUE) -elseif(PACKAGE_FIND_VERSION_MAJOR EQUAL ${RAC_VERSION}) - if(PACKAGE_FIND_VERSION VERSION_LESS_EQUAL PACKAGE_VERSION) - set(PACKAGE_VERSION_COMPATIBLE TRUE) - endif() endif() ") diff --git a/cmake/RunAnywhereConfig.cmake.in b/cmake/RunAnywhereConfig.cmake.in index 72b041e0da..fd1447b117 100644 --- a/cmake/RunAnywhereConfig.cmake.in +++ b/cmake/RunAnywhereConfig.cmake.in @@ -67,6 +67,8 @@ if(NOT TARGET RunAnywhere::commons) # Must be a link feature/flag, not INTERFACE_LINK_LIBRARIES # with a leading slash — Ninja then stats `/WHOLEARCHIVE:C:/...` # as a path and fails FindFirstFileExA. + # Do not REMOVE_DUPLICATES this list: GNU ld's + # --whole-archive / --no-whole-archive flags repeat per archive. if(APPLE) list(APPEND _runanywhere_kit_extra "-Wl,-force_load,${_archive}") @@ -77,8 +79,6 @@ if(NOT TARGET RunAnywhere::commons) list(APPEND _runanywhere_kit_extra "-Wl,--whole-archive" "${_archive}" "-Wl,--no-whole-archive") endif() - else() - list(APPEND _runanywhere_kit_extra "${_archive}") endif() endif() endforeach() @@ -91,7 +91,6 @@ if(NOT TARGET RunAnywhere::commons) "${RunAnywhere_THIRD_PARTY_DIR}/onnxruntime.dll") list(APPEND _runanywhere_kit_extra ${_kit_dylibs}) if(_runanywhere_kit_extra) - list(REMOVE_DUPLICATES _runanywhere_kit_extra) set_property(TARGET RunAnywhere::commons APPEND PROPERTY INTERFACE_LINK_LIBRARIES ${_runanywhere_kit_extra}) endif() @@ -110,13 +109,13 @@ endif() foreach(_opt mlx neurt qhexrt) string(TOUPPER ${_opt} _OPT) - if(RunAnywhere_HAS_${_OPT} - AND EXISTS "${RunAnywhere_LIBRARY_DIR}/librac_backend_${_opt}${CMAKE_STATIC_LIBRARY_SUFFIX}") + set(_opt_lib + "${RunAnywhere_LIBRARY_DIR}/${CMAKE_STATIC_LIBRARY_PREFIX}rac_backend_${_opt}${CMAKE_STATIC_LIBRARY_SUFFIX}") + if(RunAnywhere_HAS_${_OPT} AND EXISTS "${_opt_lib}") if(NOT TARGET RunAnywhere::${_opt}) add_library(RunAnywhere::${_opt} STATIC IMPORTED) set_target_properties(RunAnywhere::${_opt} PROPERTIES - IMPORTED_LOCATION - "${RunAnywhere_LIBRARY_DIR}/librac_backend_${_opt}${CMAKE_STATIC_LIBRARY_SUFFIX}" + IMPORTED_LOCATION "${_opt_lib}" INTERFACE_LINK_LIBRARIES RunAnywhere::commons) endif() endif() diff --git a/docs/reference/cpp-desktop-kit.md b/docs/reference/cpp-desktop-kit.md index 85677d9d5d..d0445ba02c 100644 --- a/docs/reference/cpp-desktop-kit.md +++ b/docs/reference/cpp-desktop-kit.md @@ -5,7 +5,7 @@ to consume this SDK. It is not an rcli binary. ## Layout -``` +```text include/rac/** public C ABI include/runanywhere/proto/*.pb.h generated messages (same protoc as commons) include/google/protobuf/** vendored runtime headers (PROTOBUF_VERSION pin) diff --git a/scripts/build/fetch-private-engine-pack.sh b/scripts/build/fetch-private-engine-pack.sh index 1e430fda5c..867fa4e402 100755 --- a/scripts/build/fetch-private-engine-pack.sh +++ b/scripts/build/fetch-private-engine-pack.sh @@ -15,12 +15,18 @@ if [[ -z "$TOKEN" ]]; then echo "NEURUN_TOKEN (or GH_TOKEN) is required to fetch private engine packs." >&2 exit 3 fi +export NEURUN_TOKEN="$TOKEN" +export GH_TOKEN="$TOKEN" case "$ENGINE" in neurt) exec "$ROOT/scripts/build/download-neurt.sh" --slice "$SLICE" ;; qhexrt) - exec "$ROOT/scripts/build/download-qhexrt.sh" --abi "$SLICE" + ABI="$SLICE" + case "$ABI" in + windows-arm64) ABI=win-arm64 ;; + esac + exec "$ROOT/scripts/build/download-qhexrt.sh" --abi "$ABI" ;; *) echo "unknown engine $ENGINE" >&2 diff --git a/scripts/build/package-cpp-desktop.sh b/scripts/build/package-cpp-desktop.sh index abf3ac179c..384ebe73f3 100755 --- a/scripts/build/package-cpp-desktop.sh +++ b/scripts/build/package-cpp-desktop.sh @@ -11,7 +11,10 @@ PRESET="${1:-}" if [[ -z "$PRESET" ]]; then case "$(uname -s)" in Darwin) PRESET=cpp-desktop-macos-arm64 ;; - Linux) PRESET=cpp-desktop-macos-arm64; echo "WARN: using macOS preset name; override for linux" >&2 ;; + Linux) + echo "error: no cpp-desktop Linux kit preset; pass one explicitly" >&2 + exit 2 + ;; *) PRESET=cpp-desktop-windows-x64 ;; esac fi From ab0d67c8b407b78abd2bc0a157e518efc7055886 Mon Sep 17 00:00:00 2001 From: Sanchit Monga Date: Sun, 23 Aug 2026 20:40:30 -0700 Subject: [PATCH 07/19] chore: drop in-tree rcli; public SDKs ship the C++ kit only The desktop CLI, its tests, installers, and bottles live in RunanywhereAI/RCLI and consume a released find_package(RunAnywhere) kit. Keep rac_server in this repo (any consumer can embed the OpenAI-compatible HTTP API); RCLI only wraps it. - git rm rcli/ and the SPM MLX CLI host that compiled it - remove rcli CMake presets and native_rcli_* / rcli-* CI jobs - stage zlibstatic.lib and bz2_bundled.lib in the Windows kit - verify every generated proto header + SCHEMA_LOCK in kit tarballs - point docs and ./run rcli at the RCLI repository Co-authored-by: Cursor --- .github/actions/generate-idl/action.yml | 6 +- .github/workflows/cpp-desktop-kit.yml | 30 +- .github/workflows/legacy-files-blocklist.yml | 2 + .github/workflows/oss-keyless-telemetry.yml | 55 - .github/workflows/pr-build.yml | 103 +- .github/workflows/release.yml | 390 +- AGENTS.md | 2 +- CMakeLists.txt | 4 +- CMakePresets.json | 87 +- Package.swift | 152 - README.md | 11 +- bindings/kotlin/example/AGENTS.md | 2 +- bindings/kotlin/example/README.md | 2 +- bindings/python/README.md | 2 +- .../Sources/RunAnywhereMLXCLI/main.swift | 53 - bindings/swift/example/AGENTS.md | 2 +- bindings/swift/example/README.md | 4 +- cmake/PackageCppDesktop.cmake | 21 +- core/tests/scripts/run-cli-e2e-linux.sh | 205 +- rcli/.gitignore | 1 - rcli/AGENTS.md | 81 - rcli/CLAUDE.md | 1 - rcli/CMakeLists.txt | 219 - rcli/README.md | 209 - rcli/docs/RELEASING.md | 96 - rcli/include/rcli_host.h | 14 - rcli/packaging/homebrew/rcli.rb.in | 52 - rcli/scripts/build-mlx-cli.sh | 79 - rcli/scripts/install.ps1 | 64 - rcli/scripts/install.sh | 82 - rcli/scripts/package-rcli-windows.ps1 | 118 - rcli/scripts/package-rcli.sh | 378 - rcli/scripts/smoke-mlx-cli.sh | 88 - rcli/scripts/test-e2e.sh | 95 - rcli/scripts/update-tap.sh | 69 - rcli/src/app.cpp | 112 - rcli/src/app.h | 20 - rcli/src/bootstrap.cpp | 665 - rcli/src/bootstrap.h | 95 - rcli/src/catalog/catalog.cpp | 2007 --- rcli/src/catalog/catalog.h | 70 - rcli/src/catalog/model_ref.cpp | 316 - rcli/src/catalog/model_ref.h | 53 - rcli/src/commands/bench_metrics.h | 80 - rcli/src/commands/cmd_auth.cpp | 123 - rcli/src/commands/cmd_backends.cpp | 103 - rcli/src/commands/cmd_bench.cpp | 780 -- rcli/src/commands/cmd_diarize.cpp | 191 - rcli/src/commands/cmd_embed.cpp | 274 - rcli/src/commands/cmd_image.cpp | 313 - rcli/src/commands/cmd_info.cpp | 83 - rcli/src/commands/cmd_list.cpp | 132 - rcli/src/commands/cmd_lora.cpp | 336 - rcli/src/commands/cmd_models.cpp | 427 - rcli/src/commands/cmd_pull.cpp | 291 - rcli/src/commands/cmd_rag.cpp | 419 - rcli/src/commands/cmd_rerank.cpp | 179 - rcli/src/commands/cmd_rm.cpp | 188 - rcli/src/commands/cmd_run.cpp | 767 - rcli/src/commands/cmd_segment.cpp | 203 - rcli/src/commands/cmd_serve.cpp | 140 - rcli/src/commands/cmd_show.cpp | 138 - rcli/src/commands/cmd_stt.cpp | 164 - rcli/src/commands/cmd_telemetry.cpp | 498 - rcli/src/commands/cmd_tool.cpp | 286 - rcli/src/commands/cmd_tts.cpp | 162 - rcli/src/commands/cmd_vad.cpp | 179 - rcli/src/commands/cmd_version.cpp | 40 - rcli/src/commands/cmd_voice.cpp | 173 - rcli/src/commands/commands.h | 105 - rcli/src/commands/engine_options.cpp | 73 - rcli/src/commands/engine_options.h | 31 - rcli/src/commands/model_labels.h | 84 - rcli/src/commands/model_setup.cpp | 120 - rcli/src/commands/model_setup.h | 42 - rcli/src/config/cli_paths.cpp | 86 - rcli/src/config/cli_paths.h | 36 - rcli/src/device_info.cpp | 691 - rcli/src/device_info.h | 15 - rcli/src/io/image_io.cpp | 257 - rcli/src/io/image_io.h | 45 - rcli/src/io/output.cpp | 221 - rcli/src/io/output.h | 78 - rcli/src/io/proto.h | 61 - rcli/src/io/wav_io.cpp | 220 - rcli/src/io/wav_io.h | 43 - rcli/src/main.cpp | 15 - rcli/src/net/control_plane.cpp | 248 - rcli/src/net/control_plane.h | 88 - rcli/src/progress/progress_bar.cpp | 186 - rcli/src/progress/progress_bar.h | 61 - rcli/src/repl/repl.cpp | 73 - rcli/src/repl/repl.h | 31 - rcli/src/util/term.cpp | 62 - rcli/src/util/term.h | 28 - rcli/src/windows_proto_compat.h | 28 - rcli/tests/CMakeLists.txt | 37 - rcli/tests/test_rcli_mlx_e2e.cpp | 1129 -- rcli/tests/test_rcli_segment.cpp | 504 - rcli/tests/test_rcli_telemetry_live.cpp | 294 - rcli/tests/test_rcli_unit.cpp | 2149 --- rcli/third_party/CLI11/CLI11.hpp | 11527 ---------------- rcli/third_party/CLI11/LICENSE | 25 - rcli/third_party/linenoise/LICENSE | 25 - rcli/third_party/linenoise/linenoise.c | 2380 ---- rcli/third_party/linenoise/linenoise.h | 120 - run | 39 +- scripts/AGENTS.md | 2 +- scripts/ci/oss_keyless_telemetry_blast.sh | 106 +- scripts/ci/verify_cpp_desktop_kit.py | 64 + .../gates/check_engine_prebuilt_pins.sh | 7 +- 111 files changed, 137 insertions(+), 34085 deletions(-) delete mode 100644 .github/workflows/oss-keyless-telemetry.yml delete mode 100644 bindings/swift/Sources/RunAnywhereMLXCLI/main.swift delete mode 100644 rcli/.gitignore delete mode 100644 rcli/AGENTS.md delete mode 120000 rcli/CLAUDE.md delete mode 100644 rcli/CMakeLists.txt delete mode 100644 rcli/README.md delete mode 100644 rcli/docs/RELEASING.md delete mode 100644 rcli/include/rcli_host.h delete mode 100644 rcli/packaging/homebrew/rcli.rb.in delete mode 100755 rcli/scripts/build-mlx-cli.sh delete mode 100644 rcli/scripts/install.ps1 delete mode 100755 rcli/scripts/install.sh delete mode 100644 rcli/scripts/package-rcli-windows.ps1 delete mode 100755 rcli/scripts/package-rcli.sh delete mode 100755 rcli/scripts/smoke-mlx-cli.sh delete mode 100755 rcli/scripts/test-e2e.sh delete mode 100755 rcli/scripts/update-tap.sh delete mode 100644 rcli/src/app.cpp delete mode 100644 rcli/src/app.h delete mode 100644 rcli/src/bootstrap.cpp delete mode 100644 rcli/src/bootstrap.h delete mode 100644 rcli/src/catalog/catalog.cpp delete mode 100644 rcli/src/catalog/catalog.h delete mode 100644 rcli/src/catalog/model_ref.cpp delete mode 100644 rcli/src/catalog/model_ref.h delete mode 100644 rcli/src/commands/bench_metrics.h delete mode 100644 rcli/src/commands/cmd_auth.cpp delete mode 100644 rcli/src/commands/cmd_backends.cpp delete mode 100644 rcli/src/commands/cmd_bench.cpp delete mode 100644 rcli/src/commands/cmd_diarize.cpp delete mode 100644 rcli/src/commands/cmd_embed.cpp delete mode 100644 rcli/src/commands/cmd_image.cpp delete mode 100644 rcli/src/commands/cmd_info.cpp delete mode 100644 rcli/src/commands/cmd_list.cpp delete mode 100644 rcli/src/commands/cmd_lora.cpp delete mode 100644 rcli/src/commands/cmd_models.cpp delete mode 100644 rcli/src/commands/cmd_pull.cpp delete mode 100644 rcli/src/commands/cmd_rag.cpp delete mode 100644 rcli/src/commands/cmd_rerank.cpp delete mode 100644 rcli/src/commands/cmd_rm.cpp delete mode 100644 rcli/src/commands/cmd_run.cpp delete mode 100644 rcli/src/commands/cmd_segment.cpp delete mode 100644 rcli/src/commands/cmd_serve.cpp delete mode 100644 rcli/src/commands/cmd_show.cpp delete mode 100644 rcli/src/commands/cmd_stt.cpp delete mode 100644 rcli/src/commands/cmd_telemetry.cpp delete mode 100644 rcli/src/commands/cmd_tool.cpp delete mode 100644 rcli/src/commands/cmd_tts.cpp delete mode 100644 rcli/src/commands/cmd_vad.cpp delete mode 100644 rcli/src/commands/cmd_version.cpp delete mode 100644 rcli/src/commands/cmd_voice.cpp delete mode 100644 rcli/src/commands/commands.h delete mode 100644 rcli/src/commands/engine_options.cpp delete mode 100644 rcli/src/commands/engine_options.h delete mode 100644 rcli/src/commands/model_labels.h delete mode 100644 rcli/src/commands/model_setup.cpp delete mode 100644 rcli/src/commands/model_setup.h delete mode 100644 rcli/src/config/cli_paths.cpp delete mode 100644 rcli/src/config/cli_paths.h delete mode 100644 rcli/src/device_info.cpp delete mode 100644 rcli/src/device_info.h delete mode 100644 rcli/src/io/image_io.cpp delete mode 100644 rcli/src/io/image_io.h delete mode 100644 rcli/src/io/output.cpp delete mode 100644 rcli/src/io/output.h delete mode 100644 rcli/src/io/proto.h delete mode 100644 rcli/src/io/wav_io.cpp delete mode 100644 rcli/src/io/wav_io.h delete mode 100644 rcli/src/main.cpp delete mode 100644 rcli/src/net/control_plane.cpp delete mode 100644 rcli/src/net/control_plane.h delete mode 100644 rcli/src/progress/progress_bar.cpp delete mode 100644 rcli/src/progress/progress_bar.h delete mode 100644 rcli/src/repl/repl.cpp delete mode 100644 rcli/src/repl/repl.h delete mode 100644 rcli/src/util/term.cpp delete mode 100644 rcli/src/util/term.h delete mode 100644 rcli/src/windows_proto_compat.h delete mode 100644 rcli/tests/CMakeLists.txt delete mode 100644 rcli/tests/test_rcli_mlx_e2e.cpp delete mode 100644 rcli/tests/test_rcli_segment.cpp delete mode 100644 rcli/tests/test_rcli_telemetry_live.cpp delete mode 100644 rcli/tests/test_rcli_unit.cpp delete mode 100644 rcli/third_party/CLI11/CLI11.hpp delete mode 100644 rcli/third_party/CLI11/LICENSE delete mode 100644 rcli/third_party/linenoise/LICENSE delete mode 100644 rcli/third_party/linenoise/linenoise.c delete mode 100644 rcli/third_party/linenoise/linenoise.h create mode 100755 scripts/ci/verify_cpp_desktop_kit.py diff --git a/.github/actions/generate-idl/action.yml b/.github/actions/generate-idl/action.yml index bccf6aa7a8..c855905a21 100644 --- a/.github/actions/generate-idl/action.yml +++ b/.github/actions/generate-idl/action.yml @@ -17,7 +17,7 @@ description: | and keeps each job's install to what it actually needs. WHAT DOES *NOT* NEED THIS - Jobs that configure core/CMakeLists.txt (native_*, wasm, rcli, ios-device, + Jobs that configure core/CMakeLists.txt (native_*, wasm, ios-device, android-arm64, macos-*, linux-*, the Electron addon, the Python wheel) get the C/C++ bindings generated for them at CMake configure time, and jobs that run Gradle on bindings/kotlin get the Kotlin bindings from the @@ -25,8 +25,8 @@ description: | so ~29 native runner instances do not each need a codegen step here. A job that reads core/include WITHOUT configuring CMake does still need - this with `cpp` — rn-typecheck (-fsyntax-only against core/include) and - native_rcli_macos (`swift build` over root Package.swift) are the two. + this with `cpp` — rn-typecheck (-fsyntax-only against core/include) is + the remaining case. All versions come from core/VERSIONS. Nothing here is installed from a package manager where the exact patch level matters: idl/codegen/bootstrap_protoc.sh diff --git a/.github/workflows/cpp-desktop-kit.yml b/.github/workflows/cpp-desktop-kit.yml index f1003efbaa..577f3a02af 100644 --- a/.github/workflows/cpp-desktop-kit.yml +++ b/.github/workflows/cpp-desktop-kit.yml @@ -55,21 +55,8 @@ jobs: cmake --build --preset cpp-desktop-macos-arm64 --target package-cpp-desktop-tarball \ -j "$(sysctl -n hw.logicalcpu)" ls -la dist/RunAnywhere-cpp-desktop-macos-arm64-v*.tar.gz - python3 - <<'PY' - import glob, sys, tarfile - tars = glob.glob("dist/RunAnywhere-cpp-desktop-macos-arm64-v*.tar.gz") - if not tars: - sys.exit("no macOS kit tarball") - names = tarfile.open(tars[0]).getnames() - need = ( - "include/runanywhere/proto/model_types.pb.h", - "share/runanywhere/SCHEMA_LOCK", - ) - missing = [n for n in need if not any(x.endswith(n) for x in names)] - if missing: - sys.exit("kit tarball missing: " + ", ".join(missing)) - print(f"kit ok: {len(names)} entries") - PY + tar=$(echo dist/RunAnywhere-cpp-desktop-macos-arm64-v*.tar.gz) + python3 scripts/ci/verify_cpp_desktop_kit.py "$tar" --source-root . shasum -a 256 dist/RunAnywhere-cpp-desktop-macos-arm64-v*.tar.gz \ | tee "dist/RunAnywhere-cpp-desktop-macos-arm64-v$(tr -d '[:space:]' < core/VERSION).tar.gz.sha256" - uses: actions/upload-artifact@v4 @@ -104,18 +91,7 @@ jobs: $ver = (Get-Content core/VERSION -Raw).Trim() $tar = "dist/RunAnywhere-cpp-desktop-windows-x64-v$ver.tar.gz" if (-not (Test-Path $tar)) { throw "missing $tar" } - python -c @" - import sys, tarfile - names = tarfile.open(r'$tar').getnames() - need = ( - 'include/runanywhere/proto/model_types.pb.h', - 'share/runanywhere/SCHEMA_LOCK', - ) - missing = [n for n in need if not any(x.endswith(n) for x in names)] - if missing: - sys.exit('kit tarball missing: ' + ', '.join(missing)) - print(f'kit ok: {len(names)} entries') - "@ + python scripts/ci/verify_cpp_desktop_kit.py $tar --source-root . --windows if ($LASTEXITCODE -ne 0) { throw "kit tarball contents check failed" } Get-FileHash $tar -Algorithm SHA256 | ForEach-Object { "$($_.Hash.ToLower()) $(Split-Path $_.Path -Leaf)" } | diff --git a/.github/workflows/legacy-files-blocklist.yml b/.github/workflows/legacy-files-blocklist.yml index 193807bfc9..90639d4b86 100644 --- a/.github/workflows/legacy-files-blocklist.yml +++ b/.github/workflows/legacy-files-blocklist.yml @@ -21,6 +21,8 @@ jobs: run: | set -euo pipefail blocked=( + "rcli" + "rcli/" "bindings/react-native/packages/core/src/Features/VoiceSession/VoiceSessionHandle.ts" "bindings/react-native/packages/core/src/Public/Extensions/RunAnywhere+VoiceSession.ts" "bindings/react-native/scripts/build-react-native.sh" diff --git a/.github/workflows/oss-keyless-telemetry.yml b/.github/workflows/oss-keyless-telemetry.yml deleted file mode 100644 index b282940588..0000000000 --- a/.github/workflows/oss-keyless-telemetry.yml +++ /dev/null @@ -1,55 +0,0 @@ -# ============================================================================= -# OSS keyless telemetry gate — rcli development → public staging backend -# ============================================================================= -# Primary CI for the open-source contract: build rcli in this public repo and -# keyless-blast all 12 modalities at the staging backend (PUBLIC org). No API -# key. Staging origin comes from repo secrets/vars — not hardcoded hosts. -# -# Intentionally NOT on pull_request — each run writes real events into staging -# and would dirty the DB under PR load. Daily + manual only. -# ============================================================================= - -name: OSS keyless telemetry - -on: - schedule: - # Daily 08:00 UTC — light cadence for Staging drift. - - cron: "0 8 * * *" - workflow_dispatch: {} - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -permissions: - contents: read - -jobs: - keyless-staging-blast: - name: rcli keyless → staging backend - runs-on: macos-14 - timeout-minutes: 60 - steps: - - uses: actions/checkout@v7 - with: - persist-credentials: false - - - name: Install ninja + protobuf - run: brew install ninja protobuf - - - name: Require staging backend origin - env: - STAGING_BASE_URL: ${{ secrets.STAGING_BASE_URL }} - RA_OSS_BASE_URL: ${{ vars.RA_OSS_BASE_URL }} - run: | - set -euo pipefail - if [[ -z "${STAGING_BASE_URL:-}" && -z "${RA_OSS_BASE_URL:-}" ]]; then - echo "::error::Set repository secret STAGING_BASE_URL (or variable RA_OSS_BASE_URL) to the public staging backend origin." - exit 1 - fi - - - name: Build rcli + keyless blast (12 modalities) - env: - STAGING_BASE_URL: ${{ secrets.STAGING_BASE_URL }} - RA_OSS_BASE_URL: ${{ vars.RA_OSS_BASE_URL || secrets.STAGING_BASE_URL }} - run: bash scripts/ci/oss_keyless_telemetry_blast.sh diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index 9c2a220646..4902020f2f 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yml @@ -133,7 +133,8 @@ jobs: macos-release: # Keep the release compile on the canonical Apple toolchain too. Metal is - # disabled in this preset; the dedicated rcli lane covers Metal separately. + # disabled in this preset (Apple GPU coverage lives in the C++ desktop kit + # and in RunanywhereAI/RCLI). runs-on: macos-26 steps: - uses: actions/checkout@v7 @@ -170,103 +171,6 @@ jobs: - run: cmake --build --preset linux-asan - run: ctest --preset linux-asan - rcli-macos: - # Product CLI moved to RunanywhereAI/RCLI (kit consumer). In-tree RAC_BUILD_CLI - # is a hard error. Keep the job name so required checks do not 404; skip the work. - if: false - runs-on: macos-14 - steps: - - uses: actions/checkout@v7 - - run: brew install ninja protobuf - - run: cmake --preset rcli-macos-release - - run: cmake --build --preset rcli-macos-release - - name: Smoke (modelless) - run: | - set -euo pipefail - RCLI=build/rcli-macos-release/rcli/rcli - "$RCLI" version - BACKENDS="$("$RCLI" backends --json)" - echo "$BACKENDS" - # Release CLI must ship llama.cpp. MLX is linked (RCLI_HAS_MLX) but - # only registers once Swift callbacks are present (RunAnywhereMLXCLI). - echo "$BACKENDS" | grep -q '"name":"llamacpp"' - "$RCLI" list --all | grep -q 'bonsai-27b-q1_0' - "$RCLI" list --all | grep -q 'mlx-bonsai-27b-1bit' - "$RCLI" info --json - - name: Package smoke - run: bash rcli/scripts/package-rcli.sh build/rcli-macos-release macos-arm64 - - rcli-linux: - if: false - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@v7 - - run: sudo apt-get update && sudo apt-get install -y ninja-build libcurl4-openssl-dev patchelf - - name: Sherpa-ONNX prebuilts - run: ./core/scripts/linux/download-sherpa-onnx.sh - - run: cmake --preset rcli-linux-release -DRAC_BUILD_TESTS=ON - - run: cmake --build --preset rcli-linux-release - - name: Unit tests - run: ctest --test-dir build/rcli-linux-release -R "rcli_unit_tests|desktop_adapter_tests" --output-on-failure - - name: Smoke (modelless) - run: | - set -euo pipefail - RCLI=build/rcli-linux-release/rcli/rcli - "$RCLI" version - BACKENDS="$("$RCLI" backends --json)" - echo "$BACKENDS" - echo "$BACKENDS" | grep -q '"name":"llamacpp"' - "$RCLI" list --all | grep -q 'bonsai-27b-q1_0' - "$RCLI" info --json - - name: Package smoke - run: bash rcli/scripts/package-rcli.sh build/rcli-linux-release linux-x86_64 - - rcli-windows: - if: false - runs-on: windows-2022 - timeout-minutes: 60 - steps: - - uses: actions/checkout@v7 - with: - persist-credentials: false - - uses: ./.github/actions/setup-toolchain - with: - platform: windows - - name: Install static libcurl - shell: pwsh - run: | - & "$env:VCPKG_INSTALLATION_ROOT\vcpkg.exe" install curl:x64-mingw-static --host-triplet=x64-mingw-static - if ($LASTEXITCODE -ne 0) { throw "vcpkg curl install failed" } - - name: Sherpa-ONNX prebuilts - shell: cmd - run: core\scripts\windows\download-sherpa-onnx.bat - - name: Configure - shell: pwsh - run: cmake --preset rcli-windows-release -DRAC_BUILD_TESTS=ON - - name: Build - shell: pwsh - run: cmake --build --preset rcli-windows-release - - name: Add native runtimes to PATH - shell: pwsh - run: | - $Onnx = Get-ChildItem build/rcli-windows-release -Filter onnxruntime.dll -File -Recurse | - Where-Object { $_.FullName -match 'onnxruntime-src[\\/]lib' } | - Select-Object -First 1 - if (-not $Onnx) { throw "onnxruntime.dll not found" } - $Onnx.DirectoryName | Out-File $env:GITHUB_PATH -Encoding utf8 -Append - "$env:GITHUB_WORKSPACE\core\third_party\sherpa-onnx-windows\lib" | - Out-File $env:GITHUB_PATH -Encoding utf8 -Append - - name: Unit tests - shell: pwsh - run: >- - ctest --test-dir build/rcli-windows-release - -R "rcli_unit_tests|desktop_adapter_tests" --output-on-failure - - name: Package and smoke - shell: pwsh - run: >- - ./rcli/scripts/package-rcli-windows.ps1 - -BuildDir build/rcli-windows-release - windows-commons-release: # PR-time coverage for core/scripts/build-windows.bat, i.e. exactly what # release.yml's native_windows job ships as RACommons-windows-x64-v*.zip. @@ -466,8 +370,7 @@ jobs: ios-device: # MLX audio is linked through mlx-audio-swift, which requires Swift 6.2+. - # Use the macOS 26 runner line for Xcode 26; only the Metal rcli lane keeps - # its explicit older-runner compatibility exception. + # Use the macOS 26 runner line for Xcode 26. runs-on: macos-26 steps: - uses: actions/checkout@v7 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3ab53ef68d..40eb740467 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -371,7 +371,7 @@ jobs: # libsherpa-onnx-c-api.so beside it, and STT/TTS/VAD cannot work on # Linux at all. Shipped that way through at least 0.20.24 and 0.20.25 # (byte-identical carriers) because nothing asserted routability here — - # native_rcli_linux already prefetched it, native_linux never did. + # this job used to rely on a sibling rcli job to prefetch sherpa; it now fetches itself. - name: Sherpa-ONNX prebuilts (required for a ROUTABLE sherpa backend) run: ./core/scripts/linux/download-sherpa-onnx.sh - name: Build + package commons for Linux x86_64 @@ -397,379 +397,6 @@ jobs: # publish gate (pass3-syn-007). A missing tarball blocks publish. if-no-files-found: error - native_rcli_macos: - needs: [validate, native_ios] - # Product CLI moved to RunanywhereAI/RCLI (kit consumer). Keep the job - # id so publish `needs:` does not 404; skip the in-tree bottle. - if: false - runs-on: macos-26 - timeout-minutes: 120 - steps: - - uses: actions/checkout@v7 - with: - persist-credentials: false - # protobuf here is the RUNTIME: Package.swift adds - # -I$(brew --prefix)/opt/protobuf/include for RCLIHost. Its protoc is - # whatever Homebrew ships and is not the pin; bootstrap_protoc.sh in the - # step below resolves the pinned one regardless of what is on PATH. - - run: brew install ninja protobuf - # `swift build` over the root Package.swift compiles rcli's C++ against - # core/include + core/src/generated/proto without ever configuring CMake, - # so the configure-time codegen hook does not cover this job. Both are - # protoc output and neither is tracked — generate them here. - # cpp for the C headers this job reads without configuring CMake, AND - # swift because the step below runs `swift build` over the root - # Package.swift, which compiles the generated *Swift* bindings - # (RALLMStreamEvent, RAVoiceEvent, RADownloadPlanResult, ...). Generating - # only cpp left those absent and the build failed with a wall of - # "cannot find type 'RA...' in scope", plus cascading "'Any' cannot be - # constructed" noise from the resulting bad inference. - - uses: ./.github/actions/generate-idl - with: - languages: cpp,swift - - uses: actions/download-artifact@v8 - with: - name: native-ios-macos - path: native-ios-staging - - name: Stage exact Apple release candidates - run: | - set -euo pipefail - BINARIES="$GITHUB_WORKSPACE/bindings/swift/Binaries" - rm -rf "$BINARIES" - mkdir -p "$BINARIES" - for archive in "$GITHUB_WORKSPACE"/native-ios-staging/*-ios-v*.zip; do - [ -f "$archive" ] || continue - unzip -qo "$archive" -d "$BINARIES" - done - for framework in \ - RACommons \ - RABackendLLAMACPP \ - RABackendONNX \ - RABackendSherpa \ - RABackendNeuRT \ - RABackendMLX \ - RunAnywhereMLXRuntime \ - RunAnywhereMLXMetal; do - [ -d "$BINARIES/${framework}.xcframework" ] || { - echo "::error::Missing staged ${framework}.xcframework" - exit 1 - } - done - [ -d "$BINARIES/RunAnywhereMLXRuntimeResources" ] || { - echo "::error::Missing staged RunAnywhereMLXRuntimeResources" - exit 1 - } - - name: Build combined rcli Swift/MLX host (macOS arm64) - id: mlx-host - env: - CONFIGURATION: release - SWIFT_BUILD_JOBS: 2 - run: | - set -euo pipefail - bash rcli/scripts/build-mlx-cli.sh - export RUNANYWHERE_USE_LOCAL_NATIVES=1 - BIN_DIR="$(swift build -c release --show-bin-path)" - [ -x "$BIN_DIR/RunAnywhereMLXCLI" ] - [ -s "$BIN_DIR/mlx.metallib" ] - echo "bin-dir=$BIN_DIR" >> "$GITHUB_OUTPUT" - - name: Smoke combined host (modelless — llama.cpp + MLX + Apple secondary engines) - env: - RCLI_BIN_DIR: ${{ steps.mlx-host.outputs.bin-dir }} - run: | - set -euo pipefail - RCLI="$RCLI_BIN_DIR/RunAnywhereMLXCLI" - BACKENDS="$("$RCLI" backends --json)" - echo "$BACKENDS" - echo "$BACKENDS" | grep -q '"name":"llamacpp"' - echo "$BACKENDS" | grep -q '"name":"mlx"' - echo "$BACKENDS" | grep -q '"name":"sherpa"' - echo "$BACKENDS" | grep -q '"name":"neurt"' - "$RCLI" list --all | grep -q 'bonsai-27b-q1_0' - "$RCLI" list --all | grep -q 'mlx-bonsai-27b-1bit' - - name: Configure ephemeral Developer ID credentials - id: signing - env: - CERTIFICATE_BASE64: ${{ secrets.RCLI_DEVELOPER_ID_CERT_P12_BASE64 }} - CERTIFICATE_PASSWORD: ${{ secrets.RCLI_DEVELOPER_ID_CERT_PASSWORD }} - NOTARY_KEY_BASE64: ${{ secrets.RCLI_NOTARY_API_KEY_P8_BASE64 }} - NOTARY_KEY_ID: ${{ secrets.RCLI_NOTARY_KEY_ID }} - NOTARY_ISSUER_ID: ${{ secrets.RCLI_NOTARY_ISSUER_ID }} - NOTARY_APPLE_ID: ${{ secrets.RCLI_NOTARY_APPLE_ID }} - NOTARY_APP_PASSWORD: ${{ secrets.RCLI_NOTARY_APP_SPECIFIC_PASSWORD }} - NOTARY_TEAM_ID: ${{ secrets.RCLI_NOTARY_TEAM_ID }} - run: | - set -euo pipefail - set +x - # Soft-skip when org secrets are absent so Release publish is not - # blocked on credentials alone. Ship an ad-hoc-signed tarball; restore - # RCLI_DEVELOPER_ID_* / notary secrets for Developer ID + notarized DMG. - if [ -z "${CERTIFICATE_BASE64:-}" ] || [ -z "${CERTIFICATE_PASSWORD:-}" ]; then - echo "::warning::RCLI Developer ID secrets not configured — packaging unsigned (ad-hoc) macOS rcli tarball" - { - echo "signed=false" - echo "identity=" - echo "keychain=" - echo "certificate=" - echo "notary-mode=" - echo "notary-key=" - echo "notary-profile=" - } >> "$GITHUB_OUTPUT" - exit 0 - fi - - KEYCHAIN="$RUNNER_TEMP/rcli-signing.keychain-db" - CERTIFICATE="$RUNNER_TEMP/rcli-developer-id.p12" - KEYCHAIN_PASSWORD="$(openssl rand -hex 32)" - NOTARY_KEY="" - NOTARY_PROFILE="" - NOTARY_MODE="" - - api_key_complete=1 - for variable in NOTARY_KEY_BASE64 NOTARY_KEY_ID NOTARY_ISSUER_ID; do - [ -n "${!variable:-}" ] || api_key_complete=0 - done - apple_id_complete=1 - for variable in NOTARY_APPLE_ID NOTARY_APP_PASSWORD NOTARY_TEAM_ID; do - [ -n "${!variable:-}" ] || apple_id_complete=0 - done - - if [ "$api_key_complete" -eq 1 ]; then - # Preserve the existing App Store Connect API-key authentication - # path as the preferred release mode when all three values exist. - NOTARY_MODE="api-key" - NOTARY_KEY="$RUNNER_TEMP/AuthKey_${NOTARY_KEY_ID}.p8" - elif [ "$apple_id_complete" -eq 1 ]; then - # Fallback for teams that already notarize with an Apple ID and an - # app-specific password. The run-scoped name and explicit keychain - # prevent accidental selection of a profile from the login keychain. - NOTARY_MODE="apple-id" - NOTARY_PROFILE="rcli-notary-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" - else - echo "::error::Configure either the complete rcli App Store Connect API-key secret set or the complete Apple ID/app-specific-password secret set." - exit 1 - fi - - cleanup_failed_setup() { - status=$? - trap - ERR - security delete-keychain "$KEYCHAIN" >/dev/null 2>&1 || true - rm -f "$CERTIFICATE" "$NOTARY_KEY" - exit "$status" - } - trap cleanup_failed_setup ERR - - printf '%s' "$CERTIFICATE_BASE64" | base64 --decode > "$CERTIFICATE" - chmod 600 "$CERTIFICATE" - if [ "$NOTARY_MODE" = "api-key" ]; then - printf '%s' "$NOTARY_KEY_BASE64" | base64 --decode > "$NOTARY_KEY" - chmod 600 "$NOTARY_KEY" - fi - - security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN" - security set-keychain-settings -lut 21600 "$KEYCHAIN" - security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN" - security import "$CERTIFICATE" \ - -k "$KEYCHAIN" \ - -P "$CERTIFICATE_PASSWORD" \ - -A \ - -t cert \ - -f pkcs12 - security set-key-partition-list \ - -S apple-tool:,apple:,codesign: \ - -s \ - -k "$KEYCHAIN_PASSWORD" \ - "$KEYCHAIN" - - IDENTITY="$(security find-identity -v -p codesigning "$KEYCHAIN" \ - | awk -F'"' '/Developer ID Application:/ { print $2; exit }')" - [ -n "$IDENTITY" ] || { - echo "::error::The imported certificate is not a Developer ID Application identity" - exit 1 - } - - if [ "$NOTARY_MODE" = "apple-id" ]; then - xcrun notarytool store-credentials "$NOTARY_PROFILE" \ - --apple-id "$NOTARY_APPLE_ID" \ - --password "$NOTARY_APP_PASSWORD" \ - --team-id "$NOTARY_TEAM_ID" \ - --keychain "$KEYCHAIN" \ - --validate >/dev/null - fi - - { - echo "signed=true" - echo "identity=$IDENTITY" - echo "keychain=$KEYCHAIN" - echo "certificate=$CERTIFICATE" - echo "notary-mode=$NOTARY_MODE" - echo "notary-key=$NOTARY_KEY" - echo "notary-profile=$NOTARY_PROFILE" - } >> "$GITHUB_OUTPUT" - trap - ERR - - name: Package, sign, notarize, and staple rcli - env: - RAC_RELEASE_VERSION: ${{ needs.validate.outputs.version }} - RCLI_MACOS_FULL_RELEASE: 1 - RCLI_MACOS_SWIFT_BIN_DIR: ${{ steps.mlx-host.outputs.bin-dir }} - RCLI_CODESIGN_IDENTITY: ${{ steps.signing.outputs.identity }} - RCLI_CODESIGN_KEYCHAIN: ${{ steps.signing.outputs.keychain }} - # Notarize only when Developer ID secrets were loaded above. - RCLI_MACOS_NOTARIZE: ${{ steps.signing.outputs.signed == 'true' && '1' || '0' }} - RCLI_NOTARYTOOL_PROFILE: ${{ steps.signing.outputs.notary-profile }} - RCLI_NOTARYTOOL_KEYCHAIN: ${{ steps.signing.outputs.keychain }} - RCLI_NOTARY_KEY_PATH: ${{ steps.signing.outputs.notary-key }} - RCLI_NOTARY_KEY_ID: ${{ secrets.RCLI_NOTARY_KEY_ID }} - RCLI_NOTARY_ISSUER_ID: ${{ secrets.RCLI_NOTARY_ISSUER_ID }} - run: bash rcli/scripts/package-rcli.sh .build macos-arm64 - - name: Remove ephemeral signing credentials - if: always() - env: - SIGNING_KEYCHAIN: ${{ steps.signing.outputs.keychain }} - SIGNING_CERTIFICATE: ${{ steps.signing.outputs.certificate }} - SIGNING_NOTARY_KEY: ${{ steps.signing.outputs.notary-key }} - run: | - set -euo pipefail - if [ -n "${SIGNING_KEYCHAIN:-}" ] && [ -f "$SIGNING_KEYCHAIN" ]; then - security delete-keychain "$SIGNING_KEYCHAIN" - fi - rm -f "${SIGNING_CERTIFICATE:-}" "${SIGNING_NOTARY_KEY:-}" - - name: Upload rcli macOS artifact - uses: actions/upload-artifact@v7 - with: - name: rcli-macos - path: | - rcli/dist/rcli-macos-arm64-v*.tar.gz - rcli/dist/rcli-macos-arm64-v*.tar.gz.sha256 - rcli/dist/rcli-macos-arm64-v*.dmg - rcli/dist/rcli-macos-arm64-v*.dmg.sha256 - retention-days: 7 - if-no-files-found: error - - native_rcli_linux: - needs: validate - if: false # Product CLI is RunanywhereAI/RCLI. - runs-on: ubuntu-latest - timeout-minutes: 60 - steps: - - uses: actions/checkout@v7 - - uses: ./.github/actions/setup-toolchain - with: - platform: linux - - run: sudo apt-get update && sudo apt-get install -y libcurl4-openssl-dev patchelf - - name: Sherpa-ONNX prebuilts - run: ./core/scripts/linux/download-sherpa-onnx.sh - - name: Build rcli (Linux x86_64) - run: | - set -euo pipefail - cmake --preset rcli-linux-release - cmake --build --preset rcli-linux-release - - name: Package rcli tarball - env: - RAC_RELEASE_VERSION: ${{ needs.validate.outputs.version }} - run: bash rcli/scripts/package-rcli.sh build/rcli-linux-release linux-x86_64 - - name: Upload rcli Linux artifact - uses: actions/upload-artifact@v7 - with: - name: rcli-linux - path: | - rcli/dist/rcli-linux-x86_64-v*.tar.gz - rcli/dist/rcli-linux-x86_64-v*.tar.gz.sha256 - retention-days: 7 - if-no-files-found: error - - native_rcli_windows: - needs: validate - if: false # Product CLI is RunanywhereAI/RCLI. - runs-on: windows-2022 - timeout-minutes: 90 - steps: - - uses: actions/checkout@v7 - with: - persist-credentials: false - - uses: ./.github/actions/setup-toolchain - with: - platform: windows - - name: Install static libcurl - shell: pwsh - run: | - & "$env:VCPKG_INSTALLATION_ROOT\vcpkg.exe" install curl:x64-mingw-static --host-triplet=x64-mingw-static - if ($LASTEXITCODE -ne 0) { throw "vcpkg curl install failed" } - - name: Sherpa-ONNX prebuilts - shell: cmd - run: core\scripts\windows\download-sherpa-onnx.bat - - name: Build rcli (Windows x86_64) - shell: pwsh - run: | - cmake --preset rcli-windows-release -DRAC_BUILD_TESTS=ON - cmake --build --preset rcli-windows-release - - name: Add native runtimes to PATH - shell: pwsh - run: | - $Onnx = Get-ChildItem build/rcli-windows-release -Filter onnxruntime.dll -File -Recurse | - Where-Object { $_.FullName -match 'onnxruntime-src[\\/]lib' } | - Select-Object -First 1 - if (-not $Onnx) { throw "onnxruntime.dll not found" } - $Onnx.DirectoryName | Out-File $env:GITHUB_PATH -Encoding utf8 -Append - "$env:GITHUB_WORKSPACE\core\third_party\sherpa-onnx-windows\lib" | - Out-File $env:GITHUB_PATH -Encoding utf8 -Append - - name: Unit tests - shell: pwsh - run: >- - ctest --test-dir build/rcli-windows-release - -R "rcli_unit_tests|desktop_adapter_tests" --output-on-failure - - name: Authenticode sign rcli.exe - shell: pwsh - env: - WINDOWS_CERTIFICATE_BASE64: ${{ secrets.RCLI_WINDOWS_CODESIGN_PFX_BASE64 }} - WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.RCLI_WINDOWS_CODESIGN_PFX_PASSWORD }} - run: | - # Soft-skip when org secrets are absent so Release publish is not - # blocked on credentials alone. Restore RCLI_WINDOWS_CODESIGN_PFX_* - # for Authenticode-signed Windows rcli. - if ([string]::IsNullOrWhiteSpace($env:WINDOWS_CERTIFICATE_BASE64) -or - [string]::IsNullOrWhiteSpace($env:WINDOWS_CERTIFICATE_PASSWORD)) { - Write-Host "::warning::RCLI Windows codesign secrets not configured — packaging unsigned rcli.exe" - exit 0 - } - $Binary = Get-ChildItem build/rcli-windows-release -Filter rcli.exe -File -Recurse | - Where-Object { $_.FullName -match 'runanywhere-cli' } | - Select-Object -ExpandProperty FullName -First 1 - if (-not $Binary) { throw "rcli.exe not found" } - $Pfx = Join-Path $env:RUNNER_TEMP "rcli-windows-codesign.pfx" - [IO.File]::WriteAllBytes($Pfx, [Convert]::FromBase64String($env:WINDOWS_CERTIFICATE_BASE64)) - try { - $SignTool = Get-ChildItem "${env:ProgramFiles(x86)}\Windows Kits\10\bin" ` - -Filter signtool.exe -File -Recurse | - Where-Object { $_.DirectoryName -match '\\x64$' } | - Sort-Object FullName -Descending | - Select-Object -ExpandProperty FullName -First 1 - if (-not $SignTool) { throw "signtool.exe not found" } - & $SignTool sign /f $Pfx /p $env:WINDOWS_CERTIFICATE_PASSWORD ` - /fd SHA256 /tr http://timestamp.digicert.com /td SHA256 $Binary - if ($LASTEXITCODE -ne 0) { throw "Authenticode signing failed" } - $Signature = Get-AuthenticodeSignature $Binary - if ($Signature.Status -ne 'Valid') { - throw "Authenticode verification failed: $($Signature.Status)" - } - } finally { - Remove-Item $Pfx -Force -ErrorAction SilentlyContinue - } - - name: Package rcli ZIP - shell: pwsh - run: >- - ./rcli/scripts/package-rcli-windows.ps1 - -BuildDir build/rcli-windows-release - -Version ${{ needs.validate.outputs.version }} - - name: Upload rcli Windows artifact - uses: actions/upload-artifact@v7 - with: - name: rcli-windows - path: | - rcli/dist/rcli-windows-x86_64-v*.zip - rcli/dist/rcli-windows-x86_64-v*.zip.sha256 - retention-days: 7 - if-no-files-found: error - native_cpp_desktop_macos: needs: validate if: ${{ !inputs.publish_from_run_id }} @@ -786,6 +413,8 @@ jobs: cmake --build --preset cpp-desktop-macos-arm64 --target package-cpp-desktop-tarball \ -j "$(sysctl -n hw.logicalcpu)" ls -la dist/RunAnywhere-cpp-desktop-macos-arm64-v*.tar.gz + tar=$(echo dist/RunAnywhere-cpp-desktop-macos-arm64-v*.tar.gz) + python3 scripts/ci/verify_cpp_desktop_kit.py "$tar" --source-root . shasum -a 256 dist/RunAnywhere-cpp-desktop-macos-arm64-v*.tar.gz \ | tee "dist/RunAnywhere-cpp-desktop-macos-arm64-v$(tr -d '[:space:]' < core/VERSION).tar.gz.sha256" - uses: actions/upload-artifact@v7 @@ -825,6 +454,8 @@ jobs: $ver = (Get-Content core/VERSION -Raw).Trim() $tar = "dist/RunAnywhere-cpp-desktop-windows-x64-v$ver.tar.gz" if (-not (Test-Path $tar)) { throw "missing $tar" } + python scripts/ci/verify_cpp_desktop_kit.py $tar --source-root . --windows + if ($LASTEXITCODE -ne 0) { throw "kit tarball contents check failed" } Get-FileHash $tar -Algorithm SHA256 | ForEach-Object { "$($_.Hash.ToLower()) $(Split-Path $_.Path -Leaf)" } | Set-Content "$tar.sha256" @@ -853,7 +484,7 @@ jobs: # Sherpa on Windows is a SHA-pinned prebuilt, not a source build. Without # this, sherpa still "builds" but as a non-routable stub # (RAC_SHERPA_ROUTABLE=0), and build-windows.bat now hard-fails instead of - # quietly shipping it. native_rcli_windows already did this; this job never + # quietly shipping it. this job must prefetch sherpa itself; it previously assumed a sibling rcli job # did, which is part of why the bundle shipped hollow. - name: Sherpa-ONNX prebuilts (required for a ROUTABLE sherpa backend) shell: cmd @@ -2148,9 +1779,6 @@ jobs: - native_ios - native_android - native_linux - - native_rcli_macos - - native_rcli_linux - - native_rcli_windows - native_cpp_desktop_macos - native_cpp_desktop_windows - native_windows @@ -2168,7 +1796,7 @@ jobs: # ADVISORY jobs (kept in `needs` so publish waits + still includes their # artifacts on success, but deliberately omitted from the success checks # below; `!cancelled()` lets publish proceed when only these fail): - # - native_windows / native_rcli_windows: no non-Windows package consumes + # - native_windows: no non-Windows package consumes # these artifacts. Keep both jobs running so their failures remain # visible, but do not block an otherwise complete non-Windows train. # - native_electron: its own `report` job already makes the one part @@ -2340,9 +1968,7 @@ jobs: else echo " SKIP: C++ desktop kit Windows x64 advisory artifact absent" fi - if [ -s "release-flat/rcli-macos-arm64-v${VERSION}.tar.gz" ]; then - echo " NOTE: in-tree rcli bottle present (legacy); official CLI is RunanywhereAI/RCLI" - fi + # Official CLI bottles ship from RunanywhereAI/RCLI, not this repo. assert_pair "Kotlin Maven repository" "runanywhere-kotlin-maven-v${VERSION}.zip" assert_pair "Web proto package" "runanywhere-proto-ts-${VERSION}.tgz" assert_pair "Web core package" "runanywhere-web-${VERSION}.tgz" diff --git a/AGENTS.md b/AGENTS.md index ae3a775625..e12e41fdd9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -183,7 +183,7 @@ per SDK — useful when porting a fix across SDKs — lives in The root `CMakeLists.txt` (version from `core/VERSION`) is the single entry point for native builds; `CMakePresets.json` defines `macos-{debug,release}`, `linux-{debug,release,asan}`, -`ios-{device,simulator}`, `android-arm64`, `wasm`, and the `rcli-*`/`windows-*` presets. +`ios-{device,simulator}`, `android-arm64`, `wasm`, `cpp-desktop-*`, and the `windows-*` presets. ```bash cmake --preset macos-debug && cmake --build build/macos-debug && ctest --preset macos-debug diff --git a/CMakeLists.txt b/CMakeLists.txt index 3b55d4e67a..7667c5410a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -76,7 +76,7 @@ option(RAC_BUILD_SERVER "Build the OpenAI-compatible HTTP server (runanywh option(RAC_BUILD_PLATFORM "Build the platform backend (Apple Foundation Models, System TTS) on Apple hosts" ON) option(RAC_BUILD_JNI "Build the JNI bridge for Android / JVM hosts" OFF) option(RAC_BUILD_PLUGIN_SMOKE "Build tools/plugin-loader-smoke" OFF) -option(RAC_BUILD_CLI "Build the rcli desktop CLI (rcli)" OFF) +option(RAC_BUILD_CLI "Retired: in-tree rcli was moved to RunanywhereAI/RCLI. Leaving ON is a hard error." OFF) option(RAC_BUILD_ELECTRON_HARNESS "Build the runanywhere-electron M0 Win32 inference harness" OFF) option(RAC_BUILD_ELECTRON_ADDON "Build the runanywhere-electron N-API .node addon" OFF) option(RAC_BUILD_PYTHON_MODULE "Build the runanywhere-python pybind11 _core module" OFF) @@ -793,7 +793,7 @@ message(STATUS "Server (HTTP) : ${RAC_BUILD_SERVER}") message(STATUS "Platform backend : ${RAC_BUILD_PLATFORM}") message(STATUS "JNI bridge : ${RAC_BUILD_JNI}") message(STATUS "Plugin smoke CLI : ${RAC_BUILD_PLUGIN_SMOKE}") -message(STATUS "CLI (in-tree, deprecated) : ${RAC_BUILD_CLI}") +message(STATUS "CLI (in-tree) : retired (RunanywhereAI/RCLI)") message(STATUS "C++ desktop kit : ${RAC_PACKAGE_CPP_DESKTOP}") message(STATUS "Desktop adapter : ${RAC_DESKTOP_ADAPTER}") message(STATUS "============================================") diff --git a/CMakePresets.json b/CMakePresets.json index 27b6ddf14c..98d991e41c 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -139,87 +139,6 @@ }, "condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Windows" } }, - { - "name": "rcli-macos-release", - "displayName": "rcli — macOS arm64 Release (Metal)", - "inherits": "base-release", - "cacheVariables": { - "RAC_BUILD_CLI": "ON", - "RAC_DESKTOP_ADAPTER": "ON", - "RAC_BUILD_BACKENDS": "ON", - "RAC_BACKEND_LLAMACPP": "ON", - "RAC_BACKEND_MLX": "ON", - "RAC_BACKEND_SHERPA": "ON", - "RAC_BACKEND_ONNX": "ON", - "RAC_BACKEND_NEURT": "ON", - "RAC_RUNTIME_ONNXRT": "ON", - "RAC_RUNTIME_COREML": "ON", - "RAC_BACKEND_RAG": "ON", - "RAC_BUILD_SERVER": "ON", - "RAC_STATIC_PLUGINS": "ON", - "RAC_BUILD_SHARED": "OFF", - "RAC_BUILD_PLATFORM": "ON", - "RAC_INCLUDE_LOCAL_DEV_CONFIG": "OFF", - "GGML_METAL": "ON", - "CMAKE_OSX_ARCHITECTURES": "arm64", - "CMAKE_C_FLAGS": "-ffile-prefix-map=${sourceDir}=/runanywhere-sdks -fmacro-prefix-map=${sourceDir}=/runanywhere-sdks -fdebug-prefix-map=${sourceDir}=/runanywhere-sdks", - "CMAKE_CXX_FLAGS": "-ffile-prefix-map=${sourceDir}=/runanywhere-sdks -fmacro-prefix-map=${sourceDir}=/runanywhere-sdks -fdebug-prefix-map=${sourceDir}=/runanywhere-sdks", - "CMAKE_OBJC_FLAGS": "-ffile-prefix-map=${sourceDir}=/runanywhere-sdks -fmacro-prefix-map=${sourceDir}=/runanywhere-sdks -fdebug-prefix-map=${sourceDir}=/runanywhere-sdks", - "CMAKE_OBJCXX_FLAGS": "-ffile-prefix-map=${sourceDir}=/runanywhere-sdks -fmacro-prefix-map=${sourceDir}=/runanywhere-sdks -fdebug-prefix-map=${sourceDir}=/runanywhere-sdks" - }, - "condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Darwin" } - }, - { - "name": "rcli-linux-release", - "displayName": "rcli — Linux x86_64 Release", - "inherits": "base-release", - "cacheVariables": { - "RAC_BUILD_CLI": "ON", - "RAC_DESKTOP_ADAPTER": "ON", - "RAC_BUILD_BACKENDS": "ON", - "RAC_BACKEND_LLAMACPP": "ON", - "RAC_BACKEND_SHERPA": "ON", - "RAC_BACKEND_ONNX": "ON", - "RAC_RUNTIME_ONNXRT": "ON", - "RAC_BACKEND_RAG": "ON", - "RAC_BUILD_SERVER": "ON", - "RAC_STATIC_PLUGINS": "ON", - "RAC_BUILD_SHARED": "OFF", - "RAC_BUILD_PLATFORM": "OFF", - "RAC_INCLUDE_LOCAL_DEV_CONFIG": "OFF", - "CMAKE_C_FLAGS": "-ffile-prefix-map=${sourceDir}=/runanywhere-sdks -fmacro-prefix-map=${sourceDir}=/runanywhere-sdks -fdebug-prefix-map=${sourceDir}=/runanywhere-sdks", - "CMAKE_CXX_FLAGS": "-ffile-prefix-map=${sourceDir}=/runanywhere-sdks -fmacro-prefix-map=${sourceDir}=/runanywhere-sdks -fdebug-prefix-map=${sourceDir}=/runanywhere-sdks" - }, - "condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Linux" } - }, - { - "name": "rcli-windows-release", - "displayName": "rcli — Windows x86_64 Release", - "inherits": "base-release", - "cacheVariables": { - "CMAKE_TOOLCHAIN_FILE": "$env{VCPKG_INSTALLATION_ROOT}/scripts/buildsystems/vcpkg.cmake", - "VCPKG_TARGET_TRIPLET": "x64-mingw-static", - "VCPKG_HOST_TRIPLET": "x64-mingw-static", - "RAC_BUILD_CLI": "ON", - "RAC_DESKTOP_ADAPTER": "ON", - "RAC_BUILD_BACKENDS": "ON", - "RAC_BACKEND_LLAMACPP": "ON", - "RAC_BACKEND_SHERPA": "ON", - "RAC_BACKEND_ONNX": "ON", - "RAC_BACKEND_MLX": "OFF", - "RAC_BACKEND_NEURT": "OFF", - "RAC_RUNTIME_ONNXRT": "ON", - "RAC_BACKEND_RAG": "OFF", - "RAC_BUILD_SERVER": "OFF", - "RAC_STATIC_PLUGINS": "ON", - "RAC_BUILD_SHARED": "OFF", - "RAC_BUILD_PLATFORM": "OFF", - "RAC_INCLUDE_LOCAL_DEV_CONFIG": "OFF", - "GGML_METAL": "OFF" - }, - "condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Windows" } - }, - { "name": "android-arm64", "displayName": "Android arm64-v8a (cross-compile)", @@ -461,9 +380,6 @@ { "name": "linux-asan", "configurePreset": "linux-asan" }, { "name": "cpp-desktop-macos-arm64", "configurePreset": "cpp-desktop-macos-arm64", "targets": ["package-cpp-desktop-tarball"] }, { "name": "cpp-desktop-windows-x64", "configurePreset": "cpp-desktop-windows-x64", "targets": ["package-cpp-desktop-tarball"], "configuration": "Release" }, - { "name": "rcli-macos-release", "configurePreset": "rcli-macos-release" }, - { "name": "rcli-linux-release", "configurePreset": "rcli-linux-release" }, - { "name": "rcli-windows-release", "configurePreset": "rcli-windows-release" }, { "name": "android-arm64", "configurePreset": "android-arm64" }, { "name": "android-armv7", "configurePreset": "android-armv7" }, { "name": "android-x86_64", "configurePreset": "android-x86_64" }, @@ -484,7 +400,6 @@ "testPresets": [ { "name": "macos-debug", "configurePreset": "macos-debug", "output": { "outputOnFailure": true } }, { "name": "linux-debug", "configurePreset": "linux-debug", "output": { "outputOnFailure": true } }, - { "name": "linux-asan", "configurePreset": "linux-asan", "output": { "outputOnFailure": true } }, - { "name": "rcli-windows-release", "configurePreset": "rcli-windows-release", "output": { "outputOnFailure": true } } + { "name": "linux-asan", "configurePreset": "linux-asan", "output": { "outputOnFailure": true } } ] } diff --git a/Package.swift b/Package.swift index 5d8e8bae62..2149a7d210 100644 --- a/Package.swift +++ b/Package.swift @@ -197,15 +197,6 @@ let package = Package( targets: ["NeuRTRuntime"] ), - // ================================================================= - // macOS CLI host — registers real mlx-swift callbacks, then - // delegates to the C++ rcli stack with llama.cpp + MLX both enabled. - // ================================================================= - .executable( - name: "RunAnywhereMLXCLI", - targets: ["RunAnywhereMLXCLI"] - ), - ] + mlxDistributionProducts, dependencies: [ // SPM deps use `.upToNextMinor` (not open-ended `from:`) so a @@ -473,149 +464,6 @@ let package = Package( ] ), - // ================================================================= - // rcli host bridge for the macOS CLI executable (RunAnywhereMLXCLI). - // - // Release builds keep BOTH llama.cpp (GGUF) and MLX enabled. MLX - // needs Swift runtime callbacks from MLXRuntime; llama.cpp registers - // from C++ bootstrap via RCLI_HAS_LLAMACPP. Linux/Windows keep using - // the CMake-built pure C++ rcli (llama.cpp; MLX is Apple-only). - // ================================================================= - .target( - name: "RADesktopHostAdapter", - dependencies: [ - "CRACommons", - ], - // Path is src/ (not src/desktop/) so we can compile the desktop - // device-manager TU that lives under infrastructure/device/. CMake - // already groups both under RAC_DESKTOP_SOURCES; SPM must match or - // RunAnywhereMLXCLI fails to link install_device_manager_provider / - // rac_desktop_{platform_name,device_model,os_version}. - path: "core/src", - sources: [ - "desktop/desktop_adapter.cpp", - "desktop/desktop_secure_store.cpp", - "desktop/http_transport_curl.cpp", - "infrastructure/device/rac_device_manager_desktop.cpp", - ], - publicHeadersPath: "desktop", - cxxSettings: [ - .headerSearchPath("."), - .headerSearchPath("../include"), - ], - linkerSettings: [ - .linkedLibrary("curl"), - .linkedLibrary("z"), - ] - ), - - .target( - name: "RCLIHost", - dependencies: [ - "CRACommons", - "RADesktopHostAdapter", - "RABackendLlamaCPPBinary", - "RABackendMLXBinary", - "RABackendNeuRTBinary", - ], - path: "rcli", - exclude: [ - "dist", - ], - sources: [ - "src/app.cpp", - "src/bootstrap.cpp", - "src/net/control_plane.cpp", - "src/catalog/catalog.cpp", - "src/catalog/model_ref.cpp", - "src/commands/cmd_version.cpp", - "src/commands/cmd_info.cpp", - "src/commands/cmd_backends.cpp", - "src/commands/cmd_list.cpp", - "src/commands/cmd_lora.cpp", - "src/commands/cmd_models.cpp", - "src/commands/cmd_pull.cpp", - "src/commands/cmd_rm.cpp", - "src/commands/cmd_run.cpp", - "src/commands/cmd_tool.cpp", - "src/commands/cmd_serve.cpp", - "src/commands/cmd_show.cpp", - "src/commands/cmd_stt.cpp", - "src/commands/cmd_embed.cpp", - "src/commands/cmd_tts.cpp", - "src/commands/cmd_vad.cpp", - "src/commands/cmd_voice.cpp", - "src/commands/cmd_image.cpp", - "src/commands/cmd_segment.cpp", - "src/commands/cmd_diarize.cpp", - "src/commands/cmd_rag.cpp", - "src/commands/cmd_rerank.cpp", - "src/commands/cmd_bench.cpp", - "src/commands/cmd_auth.cpp", - "src/commands/cmd_telemetry.cpp", - "src/commands/engine_options.cpp", - "src/commands/model_setup.cpp", - "src/config/cli_paths.cpp", - "src/device_info.cpp", - "src/io/wav_io.cpp", - "src/io/image_io.cpp", - "src/io/output.cpp", - "src/progress/progress_bar.cpp", - "src/repl/repl.cpp", - "src/util/term.cpp", - "third_party/linenoise/linenoise.c", - ], - publicHeadersPath: "include", - cxxSettings: [ - .define("RAC_HAVE_PROTOBUF", to: "1"), - // RACommons statically bundles its pinned protobuf runtime in - // a private namespace. Every generated-proto consumer must - // compile with the identical token rewrite. - .define("google", to: "runanywhere_internal"), - // CLI11's C++20 codecvt path uses APIs deprecated since C++17. - // Select its current locale-conversion implementation. - .define("CLI11_HAS_CODECVT", to: "0"), - .define("RCLI_HAS_LLAMACPP", to: "1"), - .define("RCLI_HAS_MLX", to: "1"), - .define("RCLI_HAS_NEURT", to: "1"), - .define("RCLI_VERSION", to: "\"\(sdkVersion)\""), - .headerSearchPath("include"), - .headerSearchPath("src"), - .headerSearchPath("third_party/CLI11"), - .headerSearchPath("third_party/linenoise"), - .headerSearchPath("../core/include"), - .headerSearchPath("../core/src"), - .headerSearchPath("../core/src/generated"), - .headerSearchPath("../core/src/generated/proto"), - .unsafeFlags([ - "-I\(homebrewPrefix)/opt/protobuf/include", - "-I\(homebrewPrefix)/opt/abseil/include", - ]), - ], - linkerSettings: [ - .linkedLibrary("c++"), - .linkedLibrary("curl"), - .linkedLibrary("archive"), - .linkedLibrary("bz2"), - .linkedLibrary("z"), - .linkedFramework("Accelerate"), - .linkedFramework("CoreFoundation"), - .linkedFramework("Metal"), - .linkedFramework("MetalKit"), - .linkedFramework("Security"), - ] - ), - - .executableTarget( - name: "RunAnywhereMLXCLI", - dependencies: [ - "MLXRuntime", - "ONNXRuntime", - "RCLIHost", - ], - path: "bindings/swift/Sources/RunAnywhereMLXCLI" - ), - // ================================================================= // RunAnywhere unit tests (e.g. AudioCaptureManager – Issue #198) // ================================================================= diff --git a/README.md b/README.md index ce5249b531..1cf79faa8a 100644 --- a/README.md +++ b/README.md @@ -108,10 +108,12 @@ print(ra.llm.generate("Explain on-device AI in one sentence.", LlmOptions(model="qwen2.5-0.5b")).text) ``` -Prefer a terminal? The same core ships as a CLI: +Prefer a terminal? Install the CLI from [RunanywhereAI/RCLI](https://github.com/RunanywhereAI/RCLI) — it consumes the C++ desktop kit this repo publishes: ```bash brew install runanywhereai/tap/rcli +# or +curl -fsSL https://raw.githubusercontent.com/RunanywhereAI/RCLI/main/install.sh | sh rcli run qwen3 "Explain on-device AI in one sentence." ``` @@ -401,10 +403,10 @@ Install (macOS Apple Silicon, Linux x86_64/aarch64, Windows x86_64): ```bash brew install runanywhereai/tap/rcli # or -curl -fsSL https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/main/rcli/scripts/install.sh | sh +curl -fsSL https://raw.githubusercontent.com/RunanywhereAI/RCLI/main/install.sh | sh ``` -[CLI README](rcli/) +[CLI README](https://github.com/RunanywhereAI/RCLI) @@ -421,7 +423,7 @@ curl -fsSL https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/main | **Web** | Chromium, Safari, Firefox | Beta | npm (`@runanywhere/web`) | [SDK README](bindings/web/) | | **Electron** | Windows x64 desktop | Preview | [Build from source](bindings/electron/) | [SDK README](bindings/electron/) | | **Python** | Windows, macOS, Linux | Alpha | pip (`runanywhere`) | [SDK README](bindings/python/) | -| **rcli** | macOS, Linux, Windows | Stable | Homebrew / install script | [CLI README](rcli/) | +| **rcli** | macOS, Linux, Windows | Stable | Homebrew / install script | [RCLI](https://github.com/RunanywhereAI/RCLI) | All SDKs ship on one version line, currently **0.20.11**, from a single C++ core. Pin the same version across the core package and its backends. See [Releases](https://github.com/RunanywhereAI/runanywhere-sdks/releases) for what is published today. @@ -659,7 +661,6 @@ runanywhere-sdks/ │ └── shared-apple/ # Apple transport shared by the RN + Flutter bindings │ # (iOS/Android/Web/Electron consumer apps live in their own repos) │ -├── rcli/ # rcli, the terminal app built on core/ ├── engines/ # llamacpp, mlx, sherpa, onnx, neurt, qhexrt, cloud ├── runtimes/ # cpu, coreml, onnxrt compute adapters ├── idl/ # Protobuf schemas, generated bindings per language diff --git a/bindings/kotlin/example/AGENTS.md b/bindings/kotlin/example/AGENTS.md index 1b5d36c31a..c13d509a45 100644 --- a/bindings/kotlin/example/AGENTS.md +++ b/bindings/kotlin/example/AGENTS.md @@ -83,7 +83,7 @@ auto-seeded, though: an unknown id is rejected before generation, so the one `models.register` call in `bootstrap` (in `MainActivity.onCreate`) is required. To try a different model, change `MODEL_ID`/`MODEL_URL` at the top of `app/src/main/java/com/runanywhere/minimal/MainActivity.kt` (the canonical ids and -URLs live in `rcli/src/catalog/catalog.cpp` at the repo root). +URLs live in the [RCLI](https://github.com/RunanywhereAI/RCLI) catalog (`src/catalog/catalog.cpp`). ## Source layout diff --git a/bindings/kotlin/example/README.md b/bindings/kotlin/example/README.md index 88386c4156..2c5aca034b 100644 --- a/bindings/kotlin/example/README.md +++ b/bindings/kotlin/example/README.md @@ -77,7 +77,7 @@ call to make. **Download and load are automatic** — passing `LlmOptions.model` is enough. The catalog is *not* auto-seeded, though: an unknown id is rejected before generation, so the one `models.register` call is required. To try a different model, change `MODEL_ID`/`MODEL_URL` in `MainActivity.kt` (the -canonical ids and URLs live in `rcli/src/catalog/catalog.cpp`). +canonical ids and URLs live in the [RCLI](https://github.com/RunanywhereAI/RCLI) catalog (`src/catalog/catalog.cpp`). Deliberately absent: Compose, a design system, model catalogs, navigation. diff --git a/bindings/python/README.md b/bindings/python/README.md index e4ef9687d4..25ee9b81cf 100644 --- a/bindings/python/README.md +++ b/bindings/python/README.md @@ -171,7 +171,7 @@ runanywhere list --all runanywhere serve ``` -Use `--json` for machine-readable output. For the standalone binary, see the [RunAnywhere CLI](../../rcli/README.md). +Use `--json` for machine-readable output. For the standalone binary, see the [RunAnywhere CLI](https://github.com/RunanywhereAI/RCLI). ## Errors diff --git a/bindings/swift/Sources/RunAnywhereMLXCLI/main.swift b/bindings/swift/Sources/RunAnywhereMLXCLI/main.swift deleted file mode 100644 index f406d110a2..0000000000 --- a/bindings/swift/Sources/RunAnywhereMLXCLI/main.swift +++ /dev/null @@ -1,53 +0,0 @@ -import Darwin -import Foundation -import MLXRuntime -import ONNXRuntime -import RCLIHost - -/// macOS CLI entry: registers MLX Swift callbacks, then hands off to the -/// shared C++ rcli host. RCLIHost is built with both `RCLI_HAS_LLAMACPP` and -/// `RCLI_HAS_MLX`, so GGUF (llama.cpp) and MLX catalog models are available. -@main -struct RunAnywhereMLXCLI { - static func main() { - guard registerAppleBackends() else { - stderrWrite("error: failed to register RunAnywhere Apple backend callbacks (MLX + ONNX)\n") - Darwin.exit(1) - } - - var argv = CommandLine.arguments.map { strdup($0) } - defer { - for pointer in argv { - free(pointer) - } - } - - let exitCode = argv.withUnsafeMutableBufferPointer { buffer -> Int32 in - rcli_run_main(Int32(buffer.count), buffer.baseAddress) - } - Darwin.exit(exitCode) - } - - private static func registerAppleBackends() -> Bool { - if Thread.isMainThread { - return MainActor.assumeIsolated { - ONNX.register() - return MLX.register() - } - } - - var registered = false - DispatchQueue.main.sync { - registered = MainActor.assumeIsolated { - ONNX.register() - return MLX.register() - } - } - return registered - } - - private static func stderrWrite(_ text: String) { - guard let data = text.data(using: .utf8) else { return } - FileHandle.standardError.write(data) - } -} diff --git a/bindings/swift/example/AGENTS.md b/bindings/swift/example/AGENTS.md index c1e3edf662..58ff0213af 100644 --- a/bindings/swift/example/AGENTS.md +++ b/bindings/swift/example/AGENTS.md @@ -63,7 +63,7 @@ unknown id with `Model '…' is not registered`, so the one `models.register` ca `main.swift` is required before generation. To try a different model, change `modelId`/`modelURL` in `Sources/main.swift`; canonical -model ids and URLs live in `rcli/src/catalog/catalog.cpp`. +model ids and URLs live in the [RCLI](https://github.com/RunanywhereAI/RCLI) catalog (`src/catalog/catalog.cpp`). Deliberately absent from this example: model catalogs, download/load UI, theming. diff --git a/bindings/swift/example/README.md b/bindings/swift/example/README.md index bb15919002..b90b88bf7f 100644 --- a/bindings/swift/example/README.md +++ b/bindings/swift/example/README.md @@ -52,8 +52,8 @@ call to make. **Download and load are automatic** — passing `options.model` is enough. The catalog is *not* auto-seeded, though: `ensureLoaded` rejects an unknown id with `Model '…' is not registered`, so the one `models.register` call above is required. To try a different model, change `modelId`/`modelURL` -in `Sources/main.swift` (the canonical ids and URLs live in -`rcli/src/catalog/catalog.cpp`). +in `Sources/main.swift` (canonical ids and URLs live in +[RCLI](https://github.com/RunanywhereAI/RCLI) `src/catalog/catalog.cpp`). Deliberately absent: model catalogs, download/load calls, theming, UI. Those either live in the SDK already or belong in a full example app. diff --git a/cmake/PackageCppDesktop.cmake b/cmake/PackageCppDesktop.cmake index 6da0db1678..6f1a529e3f 100644 --- a/cmake/PackageCppDesktop.cmake +++ b/cmake/PackageCppDesktop.cmake @@ -196,7 +196,8 @@ if(RAC_BINARY_DIR) string(APPEND _extra_link "\${RunAnywhere_LIBRARY_DIR}/${_dst};") elseif(_n MATCHES "^(libarchive|archive|libutf8_range|utf8_range|libutf8_validity|utf8_validity|libllama-common.*|llama-common.*)\\.(a|lib)$" OR _n MATCHES "^libabsl_.*\\.a$" - OR _n MATCHES "^absl_.*\\.lib$") + OR _n MATCHES "^absl_.*\\.lib$" + OR _n MATCHES "^(lib)?(zlibstatic|bz2_bundled)\\.(a|lib)$") file(COPY "${_hit}" DESTINATION "${RAC_KIT_OUT}/lib") string(APPEND _extra_link "\${RunAnywhere_LIBRARY_DIR}/${_n};") endif() @@ -223,6 +224,24 @@ elseif(WIN32) break() endif() endforeach() + # rac_commons PUBLIC-links these by bare filename on MSVC. vcpkg zlib.lib + # does not satisfy a zlibstatic.lib DEFAULTLIB / unresolved reference. + foreach(_need IN ITEMS zlibstatic.lib bz2_bundled.lib) + if(NOT EXISTS "${RAC_KIT_OUT}/lib/${_need}") + file(GLOB_RECURSE _hits "${RAC_BINARY_DIR}/${_need}") + if(_hits) + list(GET _hits 0 _found) + file(COPY "${_found}" DESTINATION "${RAC_KIT_OUT}/lib") + string(APPEND _extra_link "\${RunAnywhere_LIBRARY_DIR}/${_need};") + endif() + endif() + if(NOT EXISTS "${RAC_KIT_OUT}/lib/${_need}") + message(FATAL_ERROR + "PackageCppDesktop: Windows kit missing ${_need} " + "(rac_commons PUBLIC-links zlibstatic and bz2_bundled). " + "Searched ${RAC_BINARY_DIR}.") + endif() + endforeach() else() set(RUNANYWHERE_KIT_SYSTEM_LIBS "Threads::Threads;ZLIB::ZLIB;CURL::libcurl;dl;m") endif() diff --git a/core/tests/scripts/run-cli-e2e-linux.sh b/core/tests/scripts/run-cli-e2e-linux.sh index ef8d6bfe5f..5947fef516 100755 --- a/core/tests/scripts/run-cli-e2e-linux.sh +++ b/core/tests/scripts/run-cli-e2e-linux.sh @@ -1,202 +1,5 @@ #!/usr/bin/env bash -# ============================================================================= -# run-cli-e2e-linux.sh -# -# Fail-closed Linux/Docker e2e suite for the rcli desktop CLI. -# Builds the same image as run-real-inference-linux.sh (now with -# RAC_BUILD_CLI=ON) and exercises: -# - modelless smoke: version / backends / list --all / info -# - hermetic pull/rm: python3 -m http.server inside the container serves a -# mounted model file; pull exercises the curl transport, -# download orchestrator, registry and rm — no network -# - real inference: tts -> wav -> stt roundtrip, vad segments, -# run (LLM one-shot), voice turn, serve /health -# -# Model expectations match download-test-models.sh layout, mounted at /models. -# rcli's own storage uses a scratch home INSIDE the container; the canonical -# models (qwen3, whisper, piper, silero) are pulled hermetically or copied from -# the mount where layouts match. -# ============================================================================= - -set -euo pipefail - -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' - -print_header() { - echo "" - echo -e "${BLUE}==========================================${NC}" - echo -e "${BLUE} $1${NC}" - echo -e "${BLUE}==========================================${NC}" - echo "" -} - -print_step() { echo -e "${YELLOW}-> $1${NC}"; } -print_error() { echo -e "${RED}[ERROR] $1${NC}"; } - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -COMMONS_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" -REPO_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)" -DOCKER_IMAGE="${RAC_REAL_INFERENCE_IMAGE:-rac-real-inference-linux}" -MODEL_DIR="${RAC_TEST_MODEL_DIR:-${HOME}/.local/share/runanywhere/Models}" -LOG_DIR="${RAC_TEST_LOG_DIR:-${COMMONS_ROOT}/build/cli-e2e-logs}" -RCLI="/build/rcli/rcli" -BUILD_ONLY=false -SKIP_BUILD=false - -while [[ "$#" -gt 0 ]]; do - case "$1" in - --build-only) BUILD_ONLY=true; shift ;; - --skip-build) SKIP_BUILD=true; shift ;; - --logs) LOG_DIR="$2"; shift 2 ;; - --help|-h) - echo "Usage: $0 [--build-only] [--skip-build] [--logs ]" - echo "" - echo "Environment:" - echo " RAC_TEST_MODEL_DIR Host model dir mounted at /models (download-test-models.sh layout)" - echo " RAC_REAL_INFERENCE_IMAGE Docker image name (shared with run-real-inference-linux.sh)" - echo " RAC_TEST_LOG_DIR Host log directory" - exit 0 - ;; - *) print_error "Unknown option: $1"; exit 1 ;; - esac -done - -print_header "rcli CLI e2e (Linux Docker)" -echo "Repo root: ${REPO_ROOT}" -echo "Model dir: ${MODEL_DIR}" -echo "Log dir: ${LOG_DIR}" -echo "Docker img: ${DOCKER_IMAGE}" - -if ! command -v docker >/dev/null 2>&1; then - print_error "Docker not found." - exit 1 -fi -mkdir -p "${LOG_DIR}" - -if [[ "${SKIP_BUILD}" != true ]]; then - print_header "Building Docker Image" - docker build -f "${COMMONS_ROOT}/tests/Dockerfile.linux-tests" -t "${DOCKER_IMAGE}" "${REPO_ROOT}" -fi -if [[ "${BUILD_ONLY}" == true ]]; then - echo -e "${GREEN}[OK] Build-only verification succeeded${NC}" - exit 0 -fi - -run_case() { - local name="$1" - local script="$2" - local log_file="${LOG_DIR}/${name}.log" - echo -n " ${name}... " - if docker run --rm \ - -v "${MODEL_DIR}:/models:ro" \ - -e RUNANYWHERE_HOME=/root/.local/share/runanywhere \ - "${DOCKER_IMAGE}" \ - bash -lc "set -euo pipefail; RCLI='${RCLI}'; ${script}" >"${log_file}" 2>&1; then - echo -e "${GREEN}PASS${NC} (${log_file})" - return 0 - fi - echo -e "${RED}FAIL${NC} (${log_file})" - return 1 -} - -failed=0 -failed_names=() -check() { - if ! run_case "$1" "$2"; then - failed=$((failed + 1)) - failed_names+=("$1") - fi -} - -print_header "Modelless Smoke" -check "smoke_version" '"$RCLI" version | grep -E "^rcli [0-9]+\."' -check "smoke_backends" '"$RCLI" backends | grep -q llamacpp && "$RCLI" backends | grep -q sherpa' -check "smoke_list_all" '"$RCLI" list --all | grep -q qwen3-0.6b && "$RCLI" list --all | grep -q sherpa-onnx-whisper-tiny.en' -check "smoke_info_json" '"$RCLI" info --json | python3 -c "import json,sys; d=json.load(sys.stdin); assert d[\"rcli\"] and d[\"backends\"] >= 1"' -check "smoke_usage_error" '! "$RCLI" bogus-command; [ $? -eq 0 ]' - -print_header "Hermetic Pull / rm (no network)" -# Serves the mounted silero model over loopback HTTP and pulls it through the -# full curl-transport + orchestrator + registry path into a scratch home. -# Background daemons must fully detach stdio (incl. stdin) or the container -# outlives the test command and `docker run` never returns. -check "hermetic_pull_rm" ' -test -f /models/ONNX/silero-vad/silero_vad.onnx -(cd /models/ONNX/silero-vad && nohup python3 -m http.server 8077 >/dev/null 2>&1 /dev/null 2>&1 && break; sleep 1; done -"$RCLI" pull http://127.0.0.1:8077/silero_vad.onnx --no-progress -"$RCLI" list | grep -q silero_vad -"$RCLI" rm silero_vad --force -! "$RCLI" list | grep -q silero_vad -pkill -f "http.server 8077" || true -' - -print_header "Real Inference (canonical-layout models)" -# Stage canonical copies from the mount where the rig layout already matches -# commons conventions (LlamaCpp/, ONNX/silero-vad). -STAGE='mkdir -p /root/.local/share/runanywhere/Models/LlamaCpp /root/.local/share/runanywhere/Models/ONNX -cp -r /models/LlamaCpp/qwen3-0.6b /root/.local/share/runanywhere/Models/LlamaCpp/ 2>/dev/null || true -cp -r /models/ONNX/silero-vad /root/.local/share/runanywhere/Models/ONNX/ 2>/dev/null || true' - -check "llm_one_shot" "${STAGE} -test -f /root/.local/share/runanywhere/Models/LlamaCpp/qwen3-0.6b/Qwen3-0.6B-Q8_0.gguf -output=\$(\"\$RCLI\" run qwen3-0.6b 'Reply with exactly: OK' --no-think --max-tokens 32 2>/dev/null) -echo \"LLM said: \$output\" -[ -n \"\$output\" ]" - -check "tts_stt_roundtrip" "${STAGE} -\"\$RCLI\" pull piper --no-progress -\"\$RCLI\" tts --text 'RunAnywhere runs models on device.' --output /tmp/tts.wav -test -s /tmp/tts.wav -\"\$RCLI\" pull whisper-tiny --no-progress -transcript=\$(\"\$RCLI\" stt --input /tmp/tts.wav 2>/dev/null) -echo \"Transcript: \$transcript\" -echo \"\$transcript\" | grep -iE 'run|anywhere|models|device'" - -check "vad_segments" "${STAGE} -\"\$RCLI\" pull piper --no-progress -\"\$RCLI\" tts --text 'Testing voice activity detection.' --output /tmp/vad.wav -\"\$RCLI\" vad --input /tmp/vad.wav --json | python3 -c 'import json,sys; d=json.load(sys.stdin); assert len(d[\"segments\"]) >= 1'" - -check "voice_turn" "${STAGE} -\"\$RCLI\" pull piper --no-progress -\"\$RCLI\" pull whisper-tiny --no-progress -\"\$RCLI\" tts --text 'Hello there.' --output /tmp/turn.wav -\"\$RCLI\" voice --input /tmp/turn.wav --output /tmp/reply.wav --json | python3 -c 'import json,sys; d=json.load(sys.stdin); assert d[\"transcription\"] and d[\"response\"]' -test -s /tmp/reply.wav" - -check "serve_health" "${STAGE} -test -f /root/.local/share/runanywhere/Models/LlamaCpp/qwen3-0.6b/Qwen3-0.6B-Q8_0.gguf -\"\$RCLI\" serve qwen3-0.6b --port 8090 >/tmp/serve.log 2>&1 /dev/null 2>&1 && break - sleep 1 -done -curl -sf http://127.0.0.1:8090/health -curl -sf http://127.0.0.1:8090/v1/models | grep -q qwen3 -kill \$SERVER_PID -# Bounded clean-shutdown check (SIGTERM must exit; see cmd_serve handler). -for i in \$(seq 1 15); do - kill -0 \$SERVER_PID 2>/dev/null || break - sleep 1 -done -if kill -0 \$SERVER_PID 2>/dev/null; then - echo 'server did not exit after SIGTERM' - kill -9 \$SERVER_PID - exit 1 -fi -cat /tmp/serve.log" - -print_header "CLI e2e Summary" -if [[ "${failed}" -gt 0 ]]; then - print_error "Failed cases: ${failed_names[*]}" - echo "Logs: ${LOG_DIR}" - exit 1 -fi -echo -e "${GREEN}[OK] All rcli e2e cases passed${NC}" -echo "Logs: ${LOG_DIR}" +# Retired: in-tree rcli e2e. CLI tests live in RunanywhereAI/RCLI +# (scripts/e2e.sh, scripts/smoke.sh, tests/). +echo "run-cli-e2e-linux.sh was for in-tree rcli, which has moved to https://github.com/RunanywhereAI/RCLI" >&2 +exit 2 diff --git a/rcli/.gitignore b/rcli/.gitignore deleted file mode 100644 index 849ddff3b7..0000000000 --- a/rcli/.gitignore +++ /dev/null @@ -1 +0,0 @@ -dist/ diff --git a/rcli/AGENTS.md b/rcli/AGENTS.md deleted file mode 100644 index 199ce08e8d..0000000000 --- a/rcli/AGENTS.md +++ /dev/null @@ -1,81 +0,0 @@ -# AGENTS.md — rcli - -Rules for AI assistants working in this package. The repo-root AGENTS.md applies in full; -these are the CLI-specific additions. - -## What this package is - -`rcli` is the RunAnywhere desktop CLI (macOS/Linux): Ollama-style model lifecycle -management plus multi-modal inference (LLM/VLM/STT/TTS/VAD/voice) on top of the -`rac_*` C ABI. It is a **6th consumer** of `core` — the same -role the Swift/Kotlin/Flutter/RN/Web SDKs play. - -Plan / design doc: `thoughts/shared/plans/rcli_desktop_cli.md`. - -## Command surface (follow the spec, not your taste) - -`thoughts/shared/plans/public_api_spec.md` defines the public surface of all SDKs, and -the CLI is one of them. Concretely: - -- One subcommand per spec namespace, the spec's verb under it: `rcli llm generate`, - `rcli models download`, `rcli lora apply`. New capability means a new verb in the - right namespace, never a new top-level word. -- Flags are the spec's option fields in kebab-case: `--max-output-tokens`, - `--top-p`, `--system-prompt`, `--reasoning on|off`, `--speed`, `--guidance-scale`, - `--top-n`. Do not invent a shorter name for a field the spec already names. -- Terminal-friendly spellings (`run`, `chat`, `list`, `pull`, `rm`, `show`, and the - flat `stt`/`tts`/`vad` forms) are aliases. One `configure_*` function wires the - options and callback and is attached at both places; two implementations of the - same verb is a bug. See `src/commands/commands.h`. -- Retired flag spellings ride along as extra names on the same option - (`--max-output-tokens,--max-tokens`) for one release, then go. -- Help strings are one imperative line that says what the command or flag does - without restating its name. -- If the spec asks for something the C ABI cannot do, leave the command out and say - so in README "Known limitations". Never wire a flag that the commons call ignores. - -## Layering (the only rule that really matters here) - -- Command files (`src/commands/cmd_*.cpp`) are THIN: parse flags → bootstrap() → - ONE commons entry point → render. No inference logic, no multi-step model - orchestration, no SDK-internal knowledge (path patterns, framework dirs). -- If a command needs a sequence commons doesn't offer as one call, **fix commons** - (add/extend a `rac_*` API), don't compose it here. -- The desktop platform adapter + curl transport live in commons - (`core/src/desktop/`, `include/rac/desktop/rac_desktop.h`), - NOT here — they're shared with runanywhere-server, tests, and Playground. -- CLI-only concerns that DO belong here: argv parsing (CLI11), terminal - rendering (tables, progress bars), the REPL (linenoise), WAV file I/O, the - built-in model catalog, and directory resolution (`RUNANYWHERE_HOME`). - -## Output discipline (enforced; tested in tests/test_rcli_unit.cpp) - -- Results → stdout. Logs / progress / banners / prompts → stderr. -- `--json` prints exactly ONE JSON document on stdout (built with - `rcli::out::JsonWriter`; no JSON library). -- Progress bars only when stderr is a TTY and neither `--json` nor - `--no-progress` is set; otherwise plain percentage lines. -- Exit codes: 0 success, 1 runtime/SDK error, 2 usage error. - -## Build - -```bash -# Lean dev loop (no backends, fast): -cmake --preset macos-debug -DRAC_DESKTOP_ADAPTER=ON -DRAC_BUILD_CLI=ON -cmake --build build/macos-debug -j 2 --target rcli test_rcli_unit - -# Full release build (llama.cpp + MLX + Metal on macOS): -cmake --preset rcli-macos-release && cmake --build build/rcli-macos-release -j 2 -``` - -Always `-j 2` (repo resource discipline). One heavy build at a time. - -macOS release CLI keeps **both** `RAC_BACKEND_LLAMACPP=ON` and `RAC_BACKEND_MLX=ON`. -MLX inference needs the Swift host (`RunAnywhereMLXCLI` / `build-mlx-cli.sh`); -the CMake `rcli` binary still links the MLX bridge and ships both catalogs. - -## Vendored third_party - -`third_party/CLI11/CLI11.hpp` (BSD-3) and `third_party/linenoise/` (BSD-2) are -vendored verbatim — never edit them; update by replacing the file from upstream -and noting the version in the PR description. diff --git a/rcli/CLAUDE.md b/rcli/CLAUDE.md deleted file mode 120000 index 47dc3e3d86..0000000000 --- a/rcli/CLAUDE.md +++ /dev/null @@ -1 +0,0 @@ -AGENTS.md \ No newline at end of file diff --git a/rcli/CMakeLists.txt b/rcli/CMakeLists.txt deleted file mode 100644 index 4bee737755..0000000000 --- a/rcli/CMakeLists.txt +++ /dev/null @@ -1,219 +0,0 @@ -# ============================================================================= -# rcli — RunAnywhere desktop CLI -# ============================================================================= -# Built from the root CMakeLists via -DRAC_BUILD_CLI=ON (see the rcli-* presets). -# Links rac_commons + statically-folded backends; the reusable desktop platform -# adapter lives in commons (RAC_DESKTOP_ADAPTER) per the repo layering rule. -# -# Targets: -# rcli_core — OBJECT library with every TU except main.cpp (shared with tests) -# rcli — the CLI binary -# ============================================================================= - -if(NOT TARGET rac_commons) - message(FATAL_ERROR "rcli must be built from the repository root CMakeLists (rac_commons target missing)") -endif() -if(NOT RAC_DESKTOP_ADAPTER) - message(FATAL_ERROR "rcli requires -DRAC_DESKTOP_ADAPTER=ON (desktop platform adapter + curl transport)") -endif() - -# winnt.h (via windows.h, pulled in by Abseil/protobuf) defines -# ERROR_SEVERITY_WARNING/ERROR_SEVERITY_ERROR as macros that clobber the proto -# enum runanywhere::v1::ErrorSeverity, breaking errors.pb.h in any TU that sees -# windows.h before the proto header (e.g. cmd_run.cpp). Force-include a prefix -# header that includes windows.h once and undefs those two collisions. Directory -# scope so it covers rcli_core, the rcli binary, and the tests subdirectory. -if(WIN32) - if(MSVC) - add_compile_options("/FI${CMAKE_CURRENT_SOURCE_DIR}/src/windows_proto_compat.h") - else() - add_compile_options(-include "${CMAKE_CURRENT_SOURCE_DIR}/src/windows_proto_compat.h") - endif() -endif() - -set(RCLI_SOURCES - src/app.cpp - src/bootstrap.cpp - src/catalog/catalog.cpp - src/catalog/model_ref.cpp - src/commands/cmd_version.cpp - src/commands/cmd_info.cpp - src/commands/cmd_auth.cpp - src/commands/cmd_backends.cpp - src/commands/cmd_list.cpp - src/commands/cmd_lora.cpp - src/commands/cmd_models.cpp - src/commands/cmd_pull.cpp - src/commands/cmd_rerank.cpp - src/commands/cmd_rm.cpp - src/commands/cmd_run.cpp - src/commands/cmd_tool.cpp - src/commands/cmd_image.cpp - src/commands/cmd_segment.cpp - src/commands/cmd_serve.cpp - src/commands/cmd_show.cpp - src/commands/cmd_stt.cpp - src/commands/cmd_diarize.cpp - src/commands/cmd_embed.cpp - src/commands/cmd_telemetry.cpp - src/commands/cmd_tts.cpp - src/commands/cmd_vad.cpp - src/commands/cmd_voice.cpp - src/commands/cmd_rag.cpp - src/commands/cmd_bench.cpp - src/commands/engine_options.cpp - src/commands/model_setup.cpp - src/config/cli_paths.cpp - src/device_info.cpp - src/net/control_plane.cpp - src/io/wav_io.cpp - src/io/image_io.cpp - src/io/output.cpp - src/progress/progress_bar.cpp - src/repl/repl.cpp - src/util/term.cpp -) - -# Vendored single-file deps. linenoise is C; compiled into the core object lib. -if(WIN32) - # The vendored linenoise implementation is POSIX-only. Windows consoles - # provide native line editing; repl.cpp supplies the small getline/history - # fallback so the command surface remains identical. - set(RCLI_THIRD_PARTY_SOURCES "") -else() - set(RCLI_THIRD_PARTY_SOURCES - third_party/linenoise/linenoise.c - ) -endif() - -add_library(rcli_core OBJECT ${RCLI_SOURCES} ${RCLI_THIRD_PARTY_SOURCES}) - -target_include_directories(rcli_core PUBLIC - ${CMAKE_CURRENT_SOURCE_DIR}/include - ${CMAKE_CURRENT_SOURCE_DIR}/src - ${CMAKE_CURRENT_SOURCE_DIR}/third_party/CLI11 - ${CMAKE_CURRENT_SOURCE_DIR}/third_party/linenoise -) - -target_link_libraries(rcli_core PUBLIC rac_commons) -target_compile_features(rcli_core PUBLIC cxx_std_20) - -# macOS battery / memory sampling in device_info.cpp (IOKit + CoreFoundation). -if(APPLE) - target_link_libraries(rcli_core PUBLIC "-framework IOKit" "-framework CoreFoundation") -endif() - -# Version string: single source of truth is core/VERSION, -# already read into PROJECT_VERSION by the root CMakeLists. -target_compile_definitions(rcli_core PUBLIC - RCLI_VERSION="${PROJECT_VERSION}" - # CLI11's current C++20 default still selects std::wstring_convert / - # , which the standard library deprecated in C++17. Use CLI11's - # maintained locale-conversion path instead. - CLI11_HAS_CODECVT=0) -if(WIN32) - target_compile_definitions(rcli_core PUBLIC RCLI_NO_LINENOISE=1) -endif() - -# Proto-byte ABI access (DownloadProgress, ModelInfo, Register* requests). -# Same internal-consumer pattern as commons tests' rac_test_define_have_protobuf: -# rac_commons keeps RAC_HAVE_PROTOBUF/include paths PRIVATE, so re-attach here. -set(_RCLI_COMMONS_DIR ${CMAKE_SOURCE_DIR}/core) -if(NOT RAC_PROTOBUF_RUNTIME_ENABLED - OR NOT EXISTS ${_RCLI_COMMONS_DIR}/src/generated/proto/model_types.pb.cc) - message(FATAL_ERROR "rcli requires the commons protobuf runtime (RAC_PROTOBUF_RUNTIME_ENABLED) " - "for the model-lifecycle proto ABI") -endif() -target_compile_definitions(rcli_core PUBLIC RAC_HAVE_PROTOBUF=1) -# The RAG pipeline is folded into rac_commons only when RAC_BACKEND_RAG is ON -# (it is OFF on the Windows CLI preset). rac_commons keeps RAC_HAVE_RAG PRIVATE, -# so re-attach it here to gate the `rag` command's rac_rag_*_proto references. -if(RAC_BACKEND_RAG) - target_compile_definitions(rcli_core PUBLIC RAC_HAVE_RAG=1) -endif() -if(RAC_PROTOBUF_NAMESPACE_ISOLATED) - # Generated messages and the statically bundled runtime must use the same - # private namespace token rewrite. rac_commons keeps this definition - # private, so attach it explicitly to the CLI and its test consumers. - target_compile_definitions(rcli_core PUBLIC google=runanywhere_internal) -endif() -target_include_directories(rcli_core PUBLIC - ${_RCLI_COMMONS_DIR}/src/generated/proto - ${_RCLI_COMMONS_DIR}/src/generated - ${Protobuf_INCLUDE_DIRS} -) -if(TARGET protobuf::libprotobuf) - target_link_libraries(rcli_core PUBLIC protobuf::libprotobuf) -elseif(Protobuf_LIBRARIES) - target_link_libraries(rcli_core PUBLIC ${Protobuf_LIBRARIES}) -endif() -if(RAC_ABSL_LIBS) - target_link_libraries(rcli_core PUBLIC ${RAC_ABSL_LIBS}) -endif() -if(RAC_PROTOBUF_COMPILE_OPTIONS) - target_compile_options(rcli_core PRIVATE ${RAC_PROTOBUF_COMPILE_OPTIONS}) -endif() - -# Per-backend availability defines, mirroring tools/CMakeLists.txt's -# if(TARGET ...) pattern so the CLI builds with any backend subset. -# neurt is Apple-only (ANE LLM + CoreML diffusion); its target only exists on Apple, so -# the if(TARGET ...) guard keeps this list cross-platform. Unlike the others it -# has no rac_backend_neurt_register() fn — bootstrap.cpp registers its plugin -# entry directly under RCLI_HAS_NEURT. -foreach(_rcli_backend llamacpp onnx sherpa mlx neurt) - if(TARGET rac_backend_${_rcli_backend}) - string(TOUPPER ${_rcli_backend} _rcli_backend_upper) - target_compile_definitions(rcli_core PUBLIC RCLI_HAS_${_rcli_backend_upper}=1) - target_link_libraries(rcli_core PUBLIC rac_backend_${_rcli_backend}) - endif() -endforeach() - -# Optional OpenAI-compatible server behind `rcli serve` (RAC_BUILD_SERVER=ON). -# On rcli_core (PUBLIC) so every consumer — the binary and the test -# executable — links rac_server whenever cmd_serve was compiled with it. -if(TARGET rac_server) - target_link_libraries(rcli_core PUBLIC rac_server) - target_compile_definitions(rcli_core PUBLIC RCLI_HAS_SERVER=1) -endif() - -if(CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU") - target_compile_options(rcli_core PRIVATE -Wall -Wextra) - # Vendored C/C++ compiles with the repo's warning set; CLI11/linenoise - # are not ours to fix. - set_source_files_properties(third_party/linenoise/linenoise.c - PROPERTIES COMPILE_OPTIONS "-w") -endif() - -# ============================================================================= -# rcli binary -# ============================================================================= - -add_executable(rcli src/main.cpp) -target_link_libraries(rcli PRIVATE rcli_core) - -find_package(Threads REQUIRED) -target_link_libraries(rcli PRIVATE Threads::Threads) - - -# Relocatable layout: tarballs ship bin/rcli + lib/*.{so,dylib}. -if(APPLE) - set_target_properties(rcli PROPERTIES - BUILD_RPATH "${CMAKE_BINARY_DIR}" - INSTALL_RPATH "@loader_path/../lib") -else() - set_target_properties(rcli PROPERTIES - BUILD_RPATH "${CMAKE_BINARY_DIR}" - INSTALL_RPATH "$ORIGIN/../lib") -endif() - -install(TARGETS rcli RUNTIME DESTINATION bin) - -# ============================================================================= -# Tests -# ============================================================================= - -if(RAC_BUILD_TESTS) - add_subdirectory(tests) -endif() - -message(STATUS " rcli configured (version ${PROJECT_VERSION})") diff --git a/rcli/README.md b/rcli/README.md deleted file mode 100644 index 0027e1432a..0000000000 --- a/rcli/README.md +++ /dev/null @@ -1,209 +0,0 @@ -# RunAnywhere CLI (`rcli`) — in-tree copy, retired - -**Do not build this tree.** The official CLI is [RunanywhereAI/RCLI](https://github.com/RunanywhereAI/RCLI) (`EXTERNAL/RCLI` in this checkout). It links a published C++ desktop kit (`find_package(RunAnywhere)`), not this subdirectory. - -This directory remains only as a reference for command coverage until the next SDK release drops it. - ---- - -# RunAnywhere CLI (`rcli`) - -Run, manage, and serve on-device AI models from the terminal. One binary, multi-modal: LLM chat, VLM image understanding, speech-to-text, text-to-speech, voice activity detection, and a full voice pipeline — all running locally on the RunAnywhere C++ core. - -```console -$ rcli models download qwen3 -pulling qwen3-0.6b ▕████████████▏ 100% 639 MB/639 MB 32 MB/s -$ rcli llm generate --model qwen3 "Reply with exactly: RCLI WORKS" --reasoning off -RCLI WORKS -$ rcli tts synthesize "RunAnywhere runs models on device." --output hello.wav -$ rcli stt transcribe hello.wav -$ rcli serve qwen3 # OpenAI-compatible API on :8080 -``` - -## Install - -**Homebrew** (macOS Apple Silicon or Linux x86_64): - -```bash -brew install runanywhereai/tap/rcli -``` - -**Install script** (macOS Apple Silicon or Linux x86_64): - -```bash -curl -fsSL https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/main/rcli/scripts/install.sh | sh -``` - -**PowerShell** (Windows x86_64): - -```powershell -irm https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/main/rcli/scripts/install.ps1 | iex -``` - -The Windows installer verifies the release checksum, installs `rcli.exe` and pinned ONNX Runtime/Sherpa DLLs under `%LOCALAPPDATA%\Programs\rcli\bin`, and adds that directory to the user `PATH`. - -| Platform | Engines | Notes | -|---|---|---| -| macOS Apple Silicon | llama.cpp, MLX, Sherpa-ONNX, ONNX Runtime, CoreML | Signed/notarized DMG in GitHub Releases | -| Linux x86_64 | llama.cpp, Sherpa-ONNX, ONNX Runtime | | -| Windows x86_64 | llama.cpp, Sherpa-ONNX, ONNX Runtime | `rcli serve` is macOS/Linux-only | - -MLX is Apple Silicon only. Commands that require an unavailable engine return a clear unsupported-backend error. - -**From source:** see [Building from source](#building-from-source). - -## Commands - -The command surface is the [public API spec](../thoughts/shared/plans/public_api_spec.md) spelled in kebab-case: one namespace per modality, the spec's verb under it, and option names that match the spec's option fields (`--max-output-tokens`, `--top-p`, `--reasoning`, `--speed`, `--guidance-scale`). If you know the surface in one SDK you know it here. - -| Command | Description | -|---|---| -| `rcli llm generate [prompt]` | Complete a prompt and print the result | -| `rcli llm stream [prompt]` | Complete a prompt, printing tokens as they arrive | -| `rcli vlm generate --image f.png [prompt]` | Answer a prompt about an image | -| `rcli stt transcribe a.wav` | Transcribe an audio file (default: whisper-tiny) | -| `rcli tts synthesize "…" -o o.wav` | Write spoken audio to a WAV file (default: Piper Lessac) | -| `rcli vad detect a.wav` | Report speech segments with timestamps (default: silero) | -| `rcli embed [text]` | Turn text into embedding vectors | -| `rcli rerank -m -d "…"` | Score documents against a query, best first | -| `rcli image generate -p "…" -o o.png` | Render an image from a prompt (Core ML, Apple only) | -| `rcli diarize a.wav -m ` | Label who spoke when | -| `rcli segment image.ppm -m ` | Label every pixel of an image by class | -| `rcli rag query ` | Answer a question over `--doc` / `--file` documents | -| `rcli voice a.wav [-o reply.wav]` | Hold one spoken turn: STT → LLM → TTS | -| `rcli models list` | List models, downloaded ones by default (`--all` for the catalog) | -| `rcli models get ` | Show one model's registry entry | -| `rcli models register ` | Add a model from a URL or `hf.co` ref | -| `rcli models download ` | Fetch a model, resuming a partial download | -| `rcli models delete ` | Remove a model's files and registration (`-f` skips the prompt) | -| `rcli models load ` | Load a model now instead of on first use | -| `rcli models unload [category]` | Free loaded models, all of them by default | -| `rcli models state` | Report resident models and disk usage | -| `rcli lora {apply,remove,list,catalog}` | Attach LoRA adapters to a language model | -| `rcli serve [model]` | OpenAI-compatible HTTP server (`/v1/chat/completions`, `/v1/models`, `/health`) | -| `rcli bench [model]` | Benchmark downloaded LLM/STT/TTS/VLM models | -| `rcli telemetry {emit,blast}` | Drive the control-plane telemetry pipeline (no model needed) | -| `rcli backends` | Registered inference backends per primitive | -| `rcli info` (`doctor`) / `rcli version` | Environment and version info | -| `rcli auth login` | Authenticated control-plane login (production) | - -Generation, transcription and synthesis load what they need: name a model with `--model` and rcli downloads it if it is missing, then loads it before the first token. `rcli models load` is for paying that cost when you choose to. - -### Aliases - -The shorter spellings are the same commands, not separate ones: - -| Alias | Namespaced form | -|---|---| -| `rcli run [prompt]`, `rcli chat ` | `rcli llm stream` (REPL when no prompt is given) | -| `rcli list`, `rcli ls` | `rcli models list` | -| `rcli show ` | `rcli models get` | -| `rcli pull ` | `rcli models download` | -| `rcli rm`, `rcli remove` | `rcli models delete` | -| `rcli stt --input a.wav` | `rcli stt transcribe a.wav` | -| `rcli tts --text "…"` | `rcli tts synthesize "…"` | -| `rcli vad --input a.wav` | `rcli vad detect a.wav` | -| `rcli run --image f.png` | `rcli vlm generate --image f.png` | - -Older flag spellings keep working next to the spec names: `--max-tokens` for `--max-output-tokens`, `--temp` for `--temperature`, `--system` for `--system-prompt`, `--no-think` for `--reasoning off`, `--negative` for `--negative-prompt`, `--guidance` for `--guidance-scale`, `--min-duration` for `--minimum-duration-ms`, `--merge-gap` for `--merge-gap-ms`. - -Global flags: `--json` (one machine-readable document on stdout), `--home `, `-v/--verbose`, `-q/--quiet`, `--no-progress`, plus the control-plane trio `--environment `, `--base-url `, and `--api-key ` (see [docs/RELEASING.md](./docs/RELEASING.md)). - -Exit codes: `0` ok · `1` runtime error · `2` usage error · `130` cancelled. - -## Interactive REPL - -Launch with no prompt when stdin is a TTY: - -```bash -rcli chat qwen3 -``` - -Features line editing and history (`~/.local/state/runanywhere/history`; disable with `RUNANYWHERE_NOHISTORY=1`). - -Slash commands: `/set system `, `/set temperature `, `/set max-output-tokens `, `/show`, `/bye` (or Ctrl-D). One Ctrl-C cancels the current generation. - -Thinking models (qwen3 family): thought tokens stream dimmed to **stderr**, answers to **stdout**. `--hide-thinking` keeps the thoughts off your terminal while the model still thinks; `--reasoning off` stops it thinking at all. - -## Model catalog - -`rcli models list --all` shows the built-in catalog — Qwen3, Llama 3.2, SmolLM2, SmolVLM2, Whisper, Piper voices, Silero VAD, MiniLM embeddings, and more. Short aliases work everywhere: `qwen3`, `whisper-tiny`, `piper`, `smolvlm2`, … - -Fetch models outside the catalog: - -```bash -rcli models download hf.co/Qwen/Qwen3-0.6B-GGUF/Qwen3-0.6B-Q8_0.gguf -rcli models download https://example.com/model.gguf -``` - -`rcli models register ` does the registration step alone, when you want the entry now and the bytes later. - -URL registrations persist under `/RunAnywhere/Registry/`. - -## Storage layout - -One knob: the RunAnywhere home (`--home`, `$RUNANYWHERE_HOME`, default `~/.local/share/runanywhere`). - -``` -~/.local/share/runanywhere/Models/{LlamaCpp,Sherpa,ONNX,...}//… -~/.local/share/runanywhere/Registry/ # persisted URL registrations -~/.config/runanywhere/secure/ # secure store (0600 files) -~/.local/state/runanywhere/history # REPL history -``` - -Models pulled by `rcli` are shared with other RunAnywhere desktop apps using the same home directory. - -## Building from source - -Requires CMake ≥ 3.24, a C++20 compiler, and libcurl dev headers on Linux (`apt install libcurl4-openssl-dev`). - -```bash -# macOS (full MLX host): -CONFIGURATION=release ./rcli/scripts/build-mlx-cli.sh - -# Linux x86_64: -./core/scripts/linux/download-sherpa-onnx.sh -cmake --preset rcli-linux-release -cmake --build build/rcli-linux-release -j 2 - -# Windows x86_64 (PowerShell): -core\scripts\windows\download-sherpa-onnx.bat -cmake --preset rcli-windows-release -cmake --build --preset rcli-windows-release -``` - -See [docs/RELEASING.md](./docs/RELEASING.md) for signing, notarization, control-plane validation, and release workflow details. - -## Testing - -```bash -ctest --test-dir build/macos-debug -R "rcli_unit_tests|desktop_adapter_tests" -bash core/tests/scripts/run-cli-e2e-linux.sh -``` - -## Architecture - -`rcli` is a consumer of the `rac_*` C ABI — the same core used by the mobile, web, and desktop SDKs. Commands are thin wrappers: lifecycle-owned model loading, proto-byte streaming generation, the download orchestrator, and the voice-agent pipeline. The reusable desktop platform layer lives in commons (`include/rac/desktop/rac_desktop.h`). - -## Known limitations - -- `serve` is LLM-only and single-model. -- REPL turns are independent (no conversation memory yet). -- `rcli voice` with thinking models may speak reasoning text — use a non-thinking LLM (`--llm lfm2`) until voice-agent thinking control lands. -- macOS x86_64, Linux ARM64, and Windows ARM64 binaries are not published yet. -- Spec verbs with no standalone command yet: `tts speak` (commons synthesizes to a buffer and has no playback path), and `rag open` / `rag ingest` (RAG indexes are in-memory per process). `rcli rag query` and `rcli rag search` open, ingest, then ask or retrieve in one invocation instead. -- `VLMGenerationOptions` was deleted; `vlm generate` (and `run --image`) now share the exact `LLMGenerationOptions` the text path uses, so `--seed`, `--frequency-penalty`, and `--presence-penalty` all apply to VLM generation too. -- `rcli lora import` was removed: `idl/lora_options.proto` deleted `LoraAdapterImportRequest`/`Result` outright, and commons permanently stubs `rac_lora_adapter_import_proto` to `RAC_ERROR_NOT_IMPLEMENTED`. No replacement verb exists in this namespace yet. -- `models load` takes `--engine` and `--category` only. `ModelLoadRequest` carries no context length, thread count or GPU switch; `rcli serve` has `--context`, `--threads` and `--gpu-layers` for the server it runs. -- `vad detect` exposes `--activation-threshold`. The spec's `minSpeechMs`, `minSilenceMs` and `prefixPaddingMs` are stream-level knobs that the per-frame `rac_vad_component_process` call does not accept. -- `stt transcribe` has no `--translate-to-english`: `rac_stt_options_t` has no field for it. - -## Support - -- Documentation: [docs.runanywhere.ai](https://docs.runanywhere.ai) -- Discord: [discord.gg/N359FBbDVd](https://discord.gg/N359FBbDVd) -- Email: [founders@runanywhere.ai](mailto:founders@runanywhere.ai) - -## License - -See the repository [LICENSE](../LICENSE). diff --git a/rcli/docs/RELEASING.md b/rcli/docs/RELEASING.md deleted file mode 100644 index ce7caab0c1..0000000000 --- a/rcli/docs/RELEASING.md +++ /dev/null @@ -1,96 +0,0 @@ -# RunAnywhere CLI — Releasing - -Contributor and release-engineering guide for shipping `rcli` binaries. - -## Control plane - -Two SDK environments: - -| Who | Flags | Auth | Backend | -|---|---|---|---| -| OSS / no key | `--environment development` (default) | Keyless | Baked staging backend → PUBLIC org | -| Team testing | `--environment production --base-url --api-key $KEY` | JWT | Your team backend | -| Customers | `--environment production --api-key $KEY` | JWT | Production backend | - -| Flag | Env var | Meaning | -|---|---|---| -| `--environment ` | `RUNANYWHERE_ENVIRONMENT` | `development` (default) = keyless OSS telemetry. `production` = API key + https. | -| `--base-url ` | `RUNANYWHERE_BASE_URL` | Optional in development (baked staging URL in release builds). Required https for production. | -| `--api-key ` | `RUNANYWHERE_API_KEY` | Required for production (≥ 10 chars). Omit for keyless development. | - -```console -# OSS keyless blast → staging backend (PUBLIC org) -# Unset ambient RUNANYWHERE_API_KEY or an invalid key will force a failed JWT login. -$ unset RUNANYWHERE_API_KEY RUNANYWHERE_BASE_URL -$ rcli --environment development \ - --base-url "$STAGING_BASE_URL" \ - telemetry blast --processing-ms 42.5 -# Release builds can omit --base-url (baked STAGING_BASE_URL). -# CI gate (path is relative to the repo root): -# STAGING_BASE_URL=… ./scripts/ci/oss_keyless_telemetry_blast.sh - -# Team / customer authed path -$ rcli --environment production \ - --base-url https://api.example.com \ - --api-key $KEY auth login - -$ rcli --environment production \ - --base-url https://api.example.com \ - --api-key $KEY telemetry blast -MODALITY RESULT STATUS RECEIVED STORED SKIPPED -llm ok HTTP 200 1 1 0 -… (one row per modality, 12 total) -``` - -- `auth login` runs the authenticated handshake (`/api/v1/auth/sdk/authenticate` → `/api/v1/devices/register` → model assignments). Production only. -- `telemetry emit|blast` drive the real commons telemetry pipeline to `/api/v2/sdk/telemetry/{modality}`. Development is keyless (no JWT). Production logs in first. Modalities: `llm stt tts vlm rag imagegen embeddings vad voice lora model system`. Exit is non-zero when any POST fails or any tracked event never reached the backend. - -## macOS distribution signing - -`release.yml` builds the combined Swift/C++ host from the same Apple artifacts as the SDK release, imports a Developer ID Application certificate into an ephemeral keychain, signs the executable and compatibility libraries with the hardened runtime and secure timestamp, notarizes a DMG, staples and validates its ticket, then deletes the temporary keychain and credential files. - -The repository stores no signing material. Configure the Developer ID secrets and one complete notarization credential set before creating a release tag: - -- `RCLI_DEVELOPER_ID_CERT_P12_BASE64` -- `RCLI_DEVELOPER_ID_CERT_PASSWORD` - -Preferred App Store Connect API-key notarization: - -- `RCLI_NOTARY_API_KEY_P8_BASE64` -- `RCLI_NOTARY_KEY_ID` -- `RCLI_NOTARY_ISSUER_ID` - -Apple ID fallback notarization: - -- `RCLI_NOTARY_APPLE_ID` -- `RCLI_NOTARY_APP_SPECIFIC_PASSWORD` -- `RCLI_NOTARY_TEAM_ID` - -When both notarization sets are complete, the workflow uses the App Store Connect API key. The Apple ID fallback stores its run-scoped notarytool profile only in the same ephemeral keychain as the imported Developer ID identity, passes that keychain explicitly during submission, and deletes it after the package step. - -For a local or external release runner, `scripts/package-rcli.sh` accepts an already-available identity through `RCLI_CODESIGN_IDENTITY` (and optionally `RCLI_CODESIGN_KEYCHAIN`). Set `RCLI_MACOS_NOTARIZE=1` and authenticate notarytool either with `RCLI_NOTARYTOOL_PROFILE` (plus `RCLI_NOTARYTOOL_KEYCHAIN` for a profile in a non-default keychain) or the API-key path, key ID, and issuer ID variables documented at the top of that script. The normal credential-free packaging path remains ad-hoc signed for pull-request smoke. - -## Windows distribution signing - -The release workflow Authenticode-signs `rcli.exe`, validates the resulting signature, and only then creates the Windows ZIP. Configure these repository secrets before creating a release tag: - -- `RCLI_WINDOWS_CODESIGN_PFX_BASE64` -- `RCLI_WINDOWS_CODESIGN_PFX_PASSWORD` - -Pull-request builds remain credential-free and validate the same unsigned binary/package layout before the protected release job performs signing. - -## CI and release workflow - -- `pr-build.yml` builds macOS, Linux, and Windows rcli targets and runs unit, backend-registration, relocatable-package, and modelless smoke checks. -- `release.yml` requires all three rcli packages before publishing the GitHub Release. -- macOS GitHub Release assets include a Developer ID signed, notarized, and stapled disk image alongside the tarball. - -Published release assets are platform-specific: - -| Platform | Asset | Included engines | -|---|---|---| -| macOS Apple Silicon | `rcli-macos-arm64-vX.Y.Z.tar.gz` and signed/notarized DMG | llama.cpp + MLX + Sherpa-ONNX + ONNX Runtime + CoreML | -| Linux x86_64 | `rcli-linux-x86_64-vX.Y.Z.tar.gz` | llama.cpp + Sherpa-ONNX + ONNX Runtime | -| Windows x86_64 | `rcli-windows-x86_64-vX.Y.Z.zip` | llama.cpp + Sherpa-ONNX + ONNX Runtime | - -The tagged macOS release packages the `RunAnywhereMLXCLI` product as `bin/rcli` together with `mlx.metallib`, its SwiftPM resource bundles, and any deployment-target Swift compatibility libraries. The CMake `rcli` binary remains the fast, credential-free pull-request smoke target; it registers llama.cpp and exposes the dual catalog but cannot execute MLX without the Swift callbacks. diff --git a/rcli/include/rcli_host.h b/rcli/include/rcli_host.h deleted file mode 100644 index c761752748..0000000000 --- a/rcli/include/rcli_host.h +++ /dev/null @@ -1,14 +0,0 @@ -#ifndef RCLI_HOST_H -#define RCLI_HOST_H - -#ifdef __cplusplus -extern "C" { -#endif - -int rcli_run_main(int argc, char** argv); - -#ifdef __cplusplus -} -#endif - -#endif // RCLI_HOST_H diff --git a/rcli/packaging/homebrew/rcli.rb.in b/rcli/packaging/homebrew/rcli.rb.in deleted file mode 100644 index a5ffff5ec9..0000000000 --- a/rcli/packaging/homebrew/rcli.rb.in +++ /dev/null @@ -1,52 +0,0 @@ -# rcli Homebrew formula template. -# -# Rendered by rcli/scripts/update-tap.sh after a GitHub release -# publishes the platform tarballs: @VERSION@, @SHA256_MACOS_ARM64@ and -# @SHA256_LINUX_X86_64@ are substituted from the release's .sha256 sidecars, -# then the result is committed to the RunanywhereAI/homebrew-tap repository as -# Formula/rcli.rb (`brew install runanywhereai/tap/rcli`). -class Rcli < Formula - desc "RunAnywhere on-device AI CLI — run, manage and serve local models" - homepage "https://github.com/RunanywhereAI/runanywhere-sdks" - version "@VERSION@" - license "MIT" - - on_macos do - on_arm do - url "https://github.com/RunanywhereAI/runanywhere-sdks/releases/download/v@VERSION@/rcli-macos-arm64-v@VERSION@.tar.gz" - sha256 "@SHA256_MACOS_ARM64@" - end - end - - on_linux do - on_intel do - url "https://github.com/RunanywhereAI/runanywhere-sdks/releases/download/v@VERSION@/rcli-linux-x86_64-v@VERSION@.tar.gz" - sha256 "@SHA256_LINUX_X86_64@" - end - depends_on "curl" - end - - def install - # Keep the executable colocated with its MLX metallib and SwiftPM resource - # bundles. The Linux archive contains only rcli here, so the same layout is - # harmless across both platforms. - libexec.install Dir["bin/*"] - bin.install_symlink libexec/"rcli" - lib.install Dir["lib/*"] unless Dir["lib/*"].empty? - doc.install "README.md" - end - - def caveats - <<~EOS - Models are stored under ~/.local/share/runanywhere (override with - RUNANYWHERE_HOME). Get started: - rcli list --all - rcli run qwen3 - EOS - end - - test do - assert_match version.to_s, shell_output("#{bin}/rcli version") - system bin/"rcli", "backends" - end -end diff --git a/rcli/scripts/build-mlx-cli.sh b/rcli/scripts/build-mlx-cli.sh deleted file mode 100755 index c6c8926648..0000000000 --- a/rcli/scripts/build-mlx-cli.sh +++ /dev/null @@ -1,79 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" -cd "$REPO_ROOT" - -# The CLI is a local developer target and consumes the staged XCFrameworks. -export RUNANYWHERE_USE_LOCAL_NATIVES=1 - -SWIFT_BUILD_JOBS="${SWIFT_BUILD_JOBS:-2}" -CONFIGURATION="${CONFIGURATION:-debug}" -PRODUCT="RunAnywhereMLXCLI" - -if [[ "$CONFIGURATION" != "debug" && "$CONFIGURATION" != "release" ]]; then - echo "CONFIGURATION must be debug or release" >&2 - exit 2 -fi - -swift_args=(--product "$PRODUCT" --jobs "$SWIFT_BUILD_JOBS") -if [[ "$CONFIGURATION" == "release" ]]; then - swift_args=(-c release "${swift_args[@]}") -fi - -echo "building $PRODUCT ($CONFIGURATION, jobs=$SWIFT_BUILD_JOBS)" -"$REPO_ROOT/idl/codegen/generate_all.sh" --only swift -swift build "${swift_args[@]}" - -if [[ "$CONFIGURATION" == "release" ]]; then - BIN_DIR="$(swift build -c release --show-bin-path)" -else - BIN_DIR="$(swift build --show-bin-path)" -fi -EXE="$BIN_DIR/$PRODUCT" -if [[ ! -x "$EXE" ]]; then - echo "expected executable not found: $EXE" >&2 - exit 1 -fi - -MLX_KERNEL_DIR="$REPO_ROOT/.build/checkouts/mlx-swift/Source/Cmlx/mlx/mlx/backend/metal/kernels" -MLX_CPP_ROOT="$REPO_ROOT/.build/checkouts/mlx-swift/Source/Cmlx/mlx" -if [[ ! -d "$MLX_KERNEL_DIR" ]]; then - echo "mlx-swift checkout missing; run swift package resolve first" >&2 - exit 1 -fi - -AIR_DIR="${RUNANYWHERE_MLX_AIR_DIR:-$REPO_ROOT/.build/mlx-metallib/$CONFIGURATION}" -rm -rf "$AIR_DIR" -mkdir -p "$AIR_DIR" - -metal_flags=( - -x metal - -Wall - -Wextra - -fno-fast-math - -Wno-c++17-extensions - -Wno-c++20-extensions -) - -airs=() -while IFS= read -r metal_file; do - rel="${metal_file#"$MLX_KERNEL_DIR"/}" - air_file="$AIR_DIR/${rel%.metal}.air" - mkdir -p "$(dirname "$air_file")" - echo "metal $rel" - xcrun -sdk macosx metal "${metal_flags[@]}" -c "$metal_file" -I"$MLX_CPP_ROOT" -o "$air_file" - airs+=("$air_file") -done < <(find "$MLX_KERNEL_DIR" -name '*.metal' -type f | sort) - -if [[ "${#airs[@]}" -eq 0 ]]; then - echo "no MLX metal kernels found under $MLX_KERNEL_DIR" >&2 - exit 1 -fi - -echo "linking mlx.metallib (${#airs[@]} kernels)" -xcrun -sdk macosx metallib "${airs[@]}" -o "$BIN_DIR/mlx.metallib" - -echo "ready: $EXE" -echo "metallib: $BIN_DIR/mlx.metallib" diff --git a/rcli/scripts/install.ps1 b/rcli/scripts/install.ps1 deleted file mode 100644 index 5c5e22afa2..0000000000 --- a/rcli/scripts/install.ps1 +++ /dev/null @@ -1,64 +0,0 @@ -param( - [Parameter(Mandatory = $false)] - [string]$Version = "", - - [Parameter(Mandatory = $false)] - [string]$InstallDir = "$env:LOCALAPPDATA\Programs\rcli" -) - -$ErrorActionPreference = "Stop" -Set-StrictMode -Version Latest - -$Repo = "RunanywhereAI/runanywhere-sdks" -if ([string]::IsNullOrWhiteSpace($Version)) { - $Latest = Invoke-RestMethod "https://api.github.com/repos/$Repo/releases/latest" - $Version = ([string]$Latest.tag_name).TrimStart("v") -} -$Version = $Version.TrimStart("v") - -$Asset = "rcli-windows-x86_64-v$Version.zip" -$BaseUrl = "https://github.com/$Repo/releases/download/v$Version" -$TempDir = Join-Path ([IO.Path]::GetTempPath()) "rcli-install-$([Guid]::NewGuid())" -New-Item $TempDir -ItemType Directory -Force | Out-Null - -try { - $Zip = Join-Path $TempDir $Asset - $Checksum = "$Zip.sha256" - Invoke-WebRequest "$BaseUrl/$Asset" -OutFile $Zip - Invoke-WebRequest "$BaseUrl/$Asset.sha256" -OutFile $Checksum - - $Expected = ((Get-Content $Checksum -Raw).Trim() -split '\s+')[0].ToLowerInvariant() - $Actual = (Get-FileHash $Zip -Algorithm SHA256).Hash.ToLowerInvariant() - if ($Actual -ne $Expected) { - throw "SHA-256 verification failed for $Asset" - } - - $Expanded = Join-Path $TempDir "expanded" - Expand-Archive $Zip -DestinationPath $Expanded - $Payload = Join-Path $Expanded "rcli-windows-x86_64" - if (-not (Test-Path (Join-Path $Payload "bin\rcli.exe"))) { - throw "release archive does not contain rcli-windows-x86_64/bin/rcli.exe" - } - - $BinDir = Join-Path $InstallDir "bin" - Remove-Item $BinDir -Recurse -Force -ErrorAction SilentlyContinue - New-Item $BinDir -ItemType Directory -Force | Out-Null - Copy-Item (Join-Path $Payload "bin\*") $BinDir -Recurse - Copy-Item (Join-Path $Payload "README.md") (Join-Path $InstallDir "README.md") -Force - - $UserPath = [Environment]::GetEnvironmentVariable("Path", "User") - $Entries = @($UserPath -split ';' | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) - if ($Entries -notcontains $BinDir) { - $NewPath = (@($Entries) + $BinDir) -join ';' - [Environment]::SetEnvironmentVariable("Path", $NewPath, "User") - Write-Host "Added $BinDir to your user PATH. Open a new terminal to use it." - } - $env:PATH = "$BinDir;$env:PATH" - - & (Join-Path $BinDir "rcli.exe") version - if ($LASTEXITCODE -ne 0) { throw "installed rcli smoke test failed" } - Write-Host "Installed rcli $Version to $BinDir" - Write-Host "Try: rcli list --all; rcli run qwen3" -} finally { - Remove-Item $TempDir -Recurse -Force -ErrorAction SilentlyContinue -} diff --git a/rcli/scripts/install.sh b/rcli/scripts/install.sh deleted file mode 100755 index 43c0f68c7b..0000000000 --- a/rcli/scripts/install.sh +++ /dev/null @@ -1,82 +0,0 @@ -#!/bin/sh -# ============================================================================= -# RunAnywhere rcli installer -# -# curl -fsSL https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/main/rcli/scripts/install.sh | sh -# -# Downloads the rcli release tarball for this platform, verifies its sha256, -# installs to ~/.local/share/rcli/ and symlinks ~/.local/bin/rcli. -# -# Environment: -# RCLI_VERSION Pin a version (default: latest GitHub release) -# RCLI_INSTALL_DIR Install root (default: ~/.local/share/rcli) -# RCLI_BIN_DIR Symlink dir (default: ~/.local/bin) -# ============================================================================= - -set -eu - -REPO="RunanywhereAI/runanywhere-sdks" -INSTALL_DIR="${RCLI_INSTALL_DIR:-${HOME}/.local/share/rcli}" -BIN_DIR="${RCLI_BIN_DIR:-${HOME}/.local/bin}" - -error() { printf 'install.sh: %s\n' "$1" >&2; exit 1; } - -# --- platform detection ------------------------------------------------------ -OS="$(uname -s)" -ARCH="$(uname -m)" -case "${OS}-${ARCH}" in - Darwin-arm64) PLATFORM="macos-arm64" ;; - Linux-x86_64) PLATFORM="linux-x86_64" ;; - Darwin-x86_64) error "Intel macOS builds are not published yet (Apple Silicon only). Build from source: see rcli/README.md" ;; - *) error "unsupported platform ${OS}/${ARCH}. Build from source: see rcli/README.md" ;; -esac - -command -v curl >/dev/null 2>&1 || error "curl is required" -command -v tar >/dev/null 2>&1 || error "tar is required" - -# --- resolve version --------------------------------------------------------- -VERSION="${RCLI_VERSION:-}" -if [ -z "${VERSION}" ]; then - VERSION="$(curl -fsSL "https://api.github.com/repos/${REPO}/releases/latest" \ - | grep '"tag_name"' | head -1 | sed -E 's/.*"v?([^"]+)".*/\1/')" - [ -n "${VERSION}" ] || error "could not resolve the latest release (set RCLI_VERSION=x.y.z)" -fi -VERSION="${VERSION#v}" - -TARBALL="rcli-${PLATFORM}-v${VERSION}.tar.gz" -URL="https://github.com/${REPO}/releases/download/v${VERSION}/${TARBALL}" - -printf 'Installing rcli %s (%s)\n' "${VERSION}" "${PLATFORM}" - -# --- download + verify ------------------------------------------------------- -TMP="$(mktemp -d)" -trap 'rm -rf "${TMP}"' EXIT - -curl -fL --progress-bar -o "${TMP}/${TARBALL}" "${URL}" \ - || error "download failed: ${URL}" -curl -fsSL -o "${TMP}/${TARBALL}.sha256" "${URL}.sha256" \ - || error "checksum download failed: ${URL}.sha256" - -( - cd "${TMP}" - if command -v shasum >/dev/null 2>&1; then - shasum -a 256 -c "${TARBALL}.sha256" >/dev/null - else - sha256sum -c "${TARBALL}.sha256" >/dev/null - fi -) || error "sha256 verification failed" - -# --- install ----------------------------------------------------------------- -DEST="${INSTALL_DIR}/${VERSION}" -rm -rf "${DEST}" -mkdir -p "${DEST}" "${BIN_DIR}" -tar -xzf "${TMP}/${TARBALL}" -C "${DEST}" --strip-components 1 - -ln -sf "${DEST}/bin/rcli" "${BIN_DIR}/rcli" - -printf 'Installed: %s/rcli -> %s/bin/rcli\n' "${BIN_DIR}" "${DEST}" -case ":${PATH}:" in - *":${BIN_DIR}:"*) ;; - *) printf 'NOTE: add %s to your PATH (e.g. export PATH="%s:$PATH")\n' "${BIN_DIR}" "${BIN_DIR}" ;; -esac -printf 'Try: rcli list --all && rcli run qwen3\n' diff --git a/rcli/scripts/package-rcli-windows.ps1 b/rcli/scripts/package-rcli-windows.ps1 deleted file mode 100644 index 1de6100122..0000000000 --- a/rcli/scripts/package-rcli-windows.ps1 +++ /dev/null @@ -1,118 +0,0 @@ -param( - [Parameter(Mandatory = $false)] - [string]$BuildDir = "build/rcli-windows-release", - - [Parameter(Mandatory = $false)] - [string]$Version = "" -) - -$ErrorActionPreference = "Stop" -Set-StrictMode -Version Latest - -$CliRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path -# $CliRoot is the rcli/ directory, which sits directly under the repo root. It -# used to be sdk/runanywhere-cli/, two levels down, which is why this walked up -# twice. The sibling .sh scripts derive their root from rcli/scripts instead, so -# they legitimately still use "../..". -$RepoRoot = (Resolve-Path (Join-Path $CliRoot "..")).Path -if (-not [IO.Path]::IsPathRooted($BuildDir)) { - $BuildDir = Join-Path $RepoRoot $BuildDir -} -$BuildDir = (Resolve-Path $BuildDir).Path - -if ([string]::IsNullOrWhiteSpace($Version)) { - $Version = (Get-Content (Join-Path $RepoRoot "core\VERSION") -Raw).Trim() -} -$Version = $Version.TrimStart("v") - -$BinaryCandidates = @( - (Join-Path $BuildDir "rcli\rcli.exe"), - (Join-Path $BuildDir "rcli\Release\rcli.exe") -) -$Binary = $BinaryCandidates | Where-Object { Test-Path $_ } | Select-Object -First 1 -if (-not $Binary) { - $Binary = Get-ChildItem -Path $BuildDir -Filter "rcli.exe" -File -Recurse | - Where-Object { $_.FullName -match "runanywhere-cli" } | - Select-Object -ExpandProperty FullName -First 1 -} -if (-not $Binary) { - throw "rcli.exe was not found under $BuildDir" -} - -$Platform = "windows-x86_64" -$DistDir = Join-Path $CliRoot "dist" -$StageRoot = Join-Path $DistDir "stage" -$Stage = Join-Path $StageRoot "rcli-$Platform" -$BinDir = Join-Path $Stage "bin" -$Zip = Join-Path $DistDir "rcli-$Platform-v$Version.zip" - -Remove-Item $Stage -Recurse -Force -ErrorAction SilentlyContinue -New-Item $BinDir -ItemType Directory -Force | Out-Null -Copy-Item $Binary (Join-Path $BinDir "rcli.exe") -Copy-Item (Join-Path $CliRoot "README.md") (Join-Path $Stage "README.md") - -function Copy-RuntimeDlls([string]$Source, [string[]]$Exclude = @()) { - if (-not (Test-Path $Source)) { return } - Get-ChildItem -Path $Source -Filter "*.dll" -File | ForEach-Object { - if (-not ($Exclude -contains $_.Name)) { - $Destination = Join-Path $BinDir $_.Name - if (-not (Test-Path $Destination)) { - Copy-Item $_.FullName $Destination - } - } - } -} - -# The CLI links the pinned standalone ONNX Runtime. Sherpa's upstream bundle -# may carry an older onnxruntime.dll, so stage the repository pin first and -# deliberately exclude that duplicate while collecting Sherpa's other DLLs. -$OnnxDll = Get-ChildItem -Path $BuildDir -Filter "onnxruntime.dll" -File -Recurse | - Where-Object { $_.FullName -match "onnxruntime-src[\\/]lib" } | - Select-Object -ExpandProperty FullName -First 1 -if (-not $OnnxDll) { - throw "the pinned ONNX Runtime DLL was not found under $BuildDir" -} -Copy-Item $OnnxDll (Join-Path $BinDir "onnxruntime.dll") - -# Sherpa ships sherpa-onnx-c-api.dll and its siblings in lib/ (bin/ holds only -# the example executables and a duplicate onnxruntime.dll). -$SherpaLib = Join-Path $RepoRoot "core\third_party\sherpa-onnx-windows\lib" -Copy-RuntimeDlls $SherpaLib @("onnxruntime.dll") -if (-not (Test-Path (Join-Path $BinDir "sherpa-onnx-c-api.dll"))) { - throw "sherpa-onnx-c-api.dll was not staged" -} - -# Validate from the exact relocatable directory that users receive. -$OldPath = $env:PATH -try { - $env:PATH = "$BinDir;$OldPath" - & (Join-Path $BinDir "rcli.exe") version - if ($LASTEXITCODE -ne 0) { throw "packaged rcli version smoke failed" } - $Backends = (& (Join-Path $BinDir "rcli.exe") backends --json) -join "`n" - Write-Host $Backends - if ($LASTEXITCODE -ne 0) { throw "packaged rcli backends smoke failed" } - if ($Backends -notmatch '"name":"llamacpp"') { throw "llama.cpp backend is missing" } - if ($Backends -notmatch '"name":"sherpa"') { throw "Sherpa backend is missing" } -} finally { - $env:PATH = $OldPath -} - -Get-ChildItem -Path $BinDir -File | ForEach-Object { - $Bytes = [IO.File]::ReadAllBytes($_.FullName) - $Text = [Text.Encoding]::ASCII.GetString($Bytes) - if ($Text.Contains($RepoRoot) -or $Text -match '[A-Za-z]:\\Users\\[^\\]+\\') { - throw "packaged artifact embeds a developer checkout path: $($_.Name)" - } -} - -New-Item $DistDir -ItemType Directory -Force | Out-Null -Remove-Item $Zip, "$Zip.sha256" -Force -ErrorAction SilentlyContinue -Compress-Archive -Path $Stage -DestinationPath $Zip -CompressionLevel Optimal -$Hash = (Get-FileHash $Zip -Algorithm SHA256).Hash.ToLowerInvariant() -"$Hash $([IO.Path]::GetFileName($Zip))" | - Set-Content -Path "$Zip.sha256" -Encoding ascii -NoNewline - -Write-Host "Packaged: $Zip" -Get-ChildItem -Path $Stage -Recurse -File | ForEach-Object { - Write-Host $_.FullName.Substring($StageRoot.Length + 1) -} diff --git a/rcli/scripts/package-rcli.sh b/rcli/scripts/package-rcli.sh deleted file mode 100755 index 4075b69e2b..0000000000 --- a/rcli/scripts/package-rcli.sh +++ /dev/null @@ -1,378 +0,0 @@ -#!/usr/bin/env bash -# ============================================================================= -# package-rcli.sh -# -# Stages bin/rcli + the shared libraries it actually links (discovered via -# otool/ldd, fail-closed) into a relocatable layout, sanity-runs the staged -# binary, and packs rcli--v.tar.gz + .sha256 under -# rcli/dist/. -# -# A tagged macOS release sets RCLI_MACOS_FULL_RELEASE=1 and points -# RCLI_MACOS_SWIFT_BIN_DIR at the release output of build-mlx-cli.sh. That -# product is the combined Swift/C++ host: it registers the MLX callbacks and -# then runs the same rcli command stack with llama.cpp and MLX both enabled. -# The package additionally stages mlx.metallib and the SwiftPM resource -# bundles beside the executable, where Bundle.module resolves them. -# -# Developer ID distribution is opt-in so pull-request smoke packaging remains -# credential-free. Set RCLI_CODESIGN_IDENTITY plus either a notarytool keychain -# profile (and RCLI_NOTARYTOOL_KEYCHAIN when the profile is in a non-default -# keychain) or App Store Connect API-key inputs, and RCLI_MACOS_NOTARIZE=1. -# The notarized, stapled DMG is emitted alongside the Homebrew-compatible -# tarball. -# -# platform-tag: macos-arm64 | linux-x86_64 -# version: $RAC_RELEASE_VERSION, else core/VERSION -# -# Layout inside the tarball (matches the binary's INSTALL_RPATH -# @loader_path/../lib | $ORIGIN/../lib): -# rcli-/bin/rcli -# rcli-macos-arm64/bin/mlx.metallib + *.bundle (full macOS release) -# rcli-/lib/*.dylib|*.so* -# rcli-/README.md -# ============================================================================= - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -CLI_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" -REPO_ROOT="$(cd "${CLI_ROOT}/.." && pwd)" - -BUILD_DIR="${1:?usage: package-rcli.sh }" -PLATFORM="${2:?usage: package-rcli.sh }" -[[ "${BUILD_DIR}" = /* ]] || BUILD_DIR="${REPO_ROOT}/${BUILD_DIR}" - -VERSION="${RAC_RELEASE_VERSION:-$(tr -d '[:space:]' < "${REPO_ROOT}/core/VERSION")}" -MACOS_FULL_RELEASE="${RCLI_MACOS_FULL_RELEASE:-0}" -MACOS_NOTARIZE="${RCLI_MACOS_NOTARIZE:-0}" -SWIFT_BIN_DIR="${RCLI_MACOS_SWIFT_BIN_DIR:-}" -CODESIGN_IDENTITY="${RCLI_CODESIGN_IDENTITY:-}" -CODESIGN_KEYCHAIN="${RCLI_CODESIGN_KEYCHAIN:-}" -BINARY="${BUILD_DIR}/rcli/rcli" -DIST_DIR="${CLI_ROOT}/dist" -STAGE_ROOT="${DIST_DIR}/stage" -STAGE="${STAGE_ROOT}/rcli-${PLATFORM}" -TARBALL="${DIST_DIR}/rcli-${PLATFORM}-v${VERSION}.tar.gz" -DMG="${DIST_DIR}/rcli-${PLATFORM}-v${VERSION}.dmg" - -case "${MACOS_FULL_RELEASE}:${MACOS_NOTARIZE}" in - 0:0|0:1|1:0|1:1) ;; - *) echo "ERROR: RCLI_MACOS_FULL_RELEASE and RCLI_MACOS_NOTARIZE must be 0 or 1" >&2; exit 2 ;; -esac - -if [[ "${PLATFORM}" == macos-* && "${MACOS_FULL_RELEASE}" == "1" ]]; then - [ -n "${SWIFT_BIN_DIR}" ] || { - echo "ERROR: RCLI_MACOS_SWIFT_BIN_DIR is required for a full macOS release" >&2 - exit 1 - } - BINARY="${SWIFT_BIN_DIR}/RunAnywhereMLXCLI" -fi - -if [[ "${MACOS_NOTARIZE}" == "1" ]]; then - [[ "${PLATFORM}" == macos-* && "${MACOS_FULL_RELEASE}" == "1" ]] || { - echo "ERROR: notarization is supported only for a full macOS release" >&2 - exit 1 - } - [ -n "${CODESIGN_IDENTITY}" ] || { - echo "ERROR: RCLI_CODESIGN_IDENTITY is required for notarization" >&2 - exit 1 - } -fi - -[ -x "${BINARY}" ] || { echo "ERROR: rcli binary not found at ${BINARY}" >&2; exit 1; } - -sanitize_macho_host_prefix() { - local artifact="$1" - local source_prefix="$2" - local stable_prefix="$3" - local label="$4" - - python3 - "${artifact}" "${source_prefix}" "${stable_prefix}" "${label}" <<'PY' -from pathlib import Path -import sys - -artifact = Path(sys.argv[1]) -source = sys.argv[2].encode() -stable_prefix = sys.argv[3].encode() -label = sys.argv[4] -payload = artifact.read_bytes() -count = payload.count(source) -if count == 0: - raise SystemExit(f"ERROR: full macOS host contains no reviewed {label} prefix") - -if len(stable_prefix) > len(source): - stable_prefix = b"/" -replacement = stable_prefix + (b"_" * (len(source) - len(stable_prefix))) -payload = payload.replace(source, replacement) -if source in payload or len(payload) != artifact.stat().st_size: - raise SystemExit(f"ERROR: {label} sanitization was incomplete") -artifact.write_bytes(payload) -PY -} - -rm -rf "${STAGE}" -mkdir -p "${STAGE}/bin" "${STAGE}/lib" -cp "${BINARY}" "${STAGE}/bin/rcli" -cp "${CLI_ROOT}/README.md" "${STAGE}/README.md" - -if [[ "${PLATFORM}" == macos-* && "${MACOS_FULL_RELEASE}" == "1" ]]; then - [ -s "${SWIFT_BIN_DIR}/mlx.metallib" ] || { - echo "ERROR: full macOS release is missing ${SWIFT_BIN_DIR}/mlx.metallib" >&2 - exit 1 - } - cp "${SWIFT_BIN_DIR}/mlx.metallib" "${STAGE}/bin/mlx.metallib" - - resource_count=0 - while IFS= read -r -d '' bundle; do - cp -R "${bundle}" "${STAGE}/bin/$(basename "${bundle}")" - resource_count=$((resource_count + 1)) - done < <(find "${SWIFT_BIN_DIR}" -maxdepth 1 -type d -name '*.bundle' \ - ! -name '*-tool.bundle' -print0) - [ "${resource_count}" -gt 0 ] || { - echo "ERROR: full macOS release contains no SwiftPM resource bundles" >&2 - exit 1 - } - [ -d "${STAGE}/bin/swift-transformers_Hub.bundle" ] || { - echo "ERROR: full macOS release is missing swift-transformers_Hub.bundle" >&2 - exit 1 - } - - # SwiftPM resource accessors contain a build-tree fallback after their - # relocatable Bundle.main lookup. Replace only that known prefix, keeping - # Mach-O offsets stable, before the package privacy scan and final signing. - sanitize_macho_host_prefix \ - "${STAGE}/bin/rcli" \ - "${SWIFT_BIN_DIR}/" \ - "/runanywhere/swiftpm-resources/" \ - "SwiftPM resource path" - sanitize_macho_host_prefix \ - "${STAGE}/bin/rcli" \ - "${REPO_ROOT}/" \ - "/runanywhere/source/" \ - "source checkout path" - - # Copy any Swift compatibility runtime required by the deployment target. - # Current macOS provides the standard Swift runtime; this normally stages - # only compatibility shims such as libswiftCompatibilitySpan.dylib. - # Xcode 26.6's swift-stdlib-tool resolves --unsigned-destination to `/` - # for this standalone executable layout. --destination preserves the - # requested directory; every copied dylib is signed explicitly below. - xcrun swift-stdlib-tool --copy \ - --scan-executable "${STAGE}/bin/rcli" \ - --platform macosx \ - --destination "${STAGE}/lib" -fi - -# ---------------------------------------------------------------------------- -# Bundle every non-system shared library the binary links. Discovering from -# the binary (instead of hardcoding libonnxruntime/sherpa names) keeps the -# package correct when backend link sets change. -# ---------------------------------------------------------------------------- -case "${PLATFORM}" in - macos-*) - deps=$(otool -L "${STAGE}/bin/rcli" | awk 'NR>1 {print $1}' \ - | grep -vE '^(/usr/lib|/System)' || true) - for dep in ${deps}; do - # @rpath/libfoo.dylib → find the real file in the build tree. - local_name="$(basename "${dep}")" - src="${dep}" - if [[ "${dep}" == @rpath/* || ! -f "${dep}" ]]; then - if [ -f "${STAGE}/lib/${local_name}" ]; then - src="${STAGE}/lib/${local_name}" - else - # Release archives may contain a dSYM DWARF file with the same - # basename as the linked dylib. Search only library entries - # so filesystem traversal order cannot select debug symbols. - # Versioned runtime archives use symlink chains such as - # libonnxruntime.1.dylib -> libonnxruntime.1.28.0.dylib. - # -type f drops the linked name that Mach-O records, so - # accept both regular files and symlinks here. The -f - # validation below still rejects broken links. - src="$(find "${BUILD_DIR}" -path "*/lib/${local_name}" \ - \( -type f -o -type l \) \ - ! -path "*/.dSYM/*" 2>/dev/null | LC_ALL=C sort | head -1)" - fi - fi - if [ -z "${src}" ] || [ ! -f "${src}" ]; then - echo "ERROR: cannot locate linked library ${dep}" >&2 - exit 1 - fi - if [ "${src}" != "${STAGE}/lib/${local_name}" ]; then - cp "${src}" "${STAGE}/lib/${local_name}" - fi - install_name_tool -change "${dep}" "@rpath/${local_name}" "${STAGE}/bin/rcli" - done - - # A copied Homebrew dylib may retain an absolute install ID or refer - # to another copied dylib through its Cellar path. Make the complete - # staged set self-contained before validating the executable. - for library in "${STAGE}"/lib/*.dylib; do - [ -e "${library}" ] || continue - library_name="$(basename "${library}")" - library_id="$(otool -D "${library}" | tail -1)" - if [[ "${library_id}" != @rpath/* && "${library_id}" != @loader_path/* ]]; then - install_name_tool -id "@rpath/${library_name}" "${library}" - fi - library_deps=$(otool -L "${library}" | awk 'NR>1 {print $1}') - for library_dep in ${library_deps}; do - dep_name="$(basename "${library_dep}")" - if [ "${dep_name}" != "${library_name}" ] \ - && [ "${library_dep}" != "@loader_path/${dep_name}" ] \ - && [ -f "${STAGE}/lib/${dep_name}" ]; then - install_name_tool -change "${library_dep}" "@loader_path/${dep_name}" "${library}" - fi - done - while IFS= read -r rpath; do - if [[ "${rpath}" != @loader_path* && "${rpath}" != @rpath* ]]; then - install_name_tool -delete_rpath "${rpath}" "${library}" - fi - done < <(otool -l "${library}" | awk ' - $1 == "cmd" && $2 == "LC_RPATH" { in_rpath = 1; next } - in_rpath && $1 == "path" { print $2; in_rpath = 0 } - ') - done - - # The build-tree executable carries absolute LC_RPATH entries so it - # can locate fetched dylibs before packaging. Retire every non-package - # entry and install exactly one relocatable package rpath before the - # privacy scan and ad-hoc signature. - has_package_rpath=0 - while IFS= read -r rpath; do - if [ "${rpath}" = "@loader_path/../lib" ]; then - has_package_rpath=1 - else - install_name_tool -delete_rpath "${rpath}" "${STAGE}/bin/rcli" - fi - done < <(otool -l "${STAGE}/bin/rcli" | awk ' - $1 == "cmd" && $2 == "LC_RPATH" { in_rpath = 1; next } - in_rpath && $1 == "path" { print $2; in_rpath = 0 } - ') - if [ "${has_package_rpath}" -eq 0 ]; then - install_name_tool -add_rpath "@loader_path/../lib" "${STAGE}/bin/rcli" - fi - - if [ -n "${CODESIGN_IDENTITY}" ]; then - codesign_args=(--force --sign "${CODESIGN_IDENTITY}" --options runtime --timestamp) - if [ -n "${CODESIGN_KEYCHAIN}" ]; then - codesign_args+=(--keychain "${CODESIGN_KEYCHAIN}") - fi - while IFS= read -r -d '' library; do - codesign "${codesign_args[@]}" "${library}" - done < <(find "${STAGE}/lib" -name '*.dylib' -type f -print0) - codesign "${codesign_args[@]}" "${STAGE}/bin/rcli" - codesign --verify --strict --verbose=2 "${STAGE}/bin/rcli" - codesign_metadata="$(codesign -dvvv "${STAGE}/bin/rcli" 2>&1)" - [[ "${codesign_metadata}" == *'Authority=Developer ID Application:'* ]] || { - echo "ERROR: rcli is not signed by a Developer ID Application certificate" >&2 - exit 1 - } - [[ "${codesign_metadata}" =~ flags=.*runtime ]] || { - echo "ERROR: rcli signature does not enable the hardened runtime" >&2 - exit 1 - } - else - # Credential-free smoke packages remain ad-hoc signed. Tagged - # production releases set RCLI_CODESIGN_IDENTITY and notarize. - codesign --force -s - "${STAGE}/bin/rcli" - find "${STAGE}/lib" -name "*.dylib" -exec codesign --force -s - {} \; - fi - ;; - linux-*) - deps=$(ldd "${STAGE}/bin/rcli" | awk '/=>/ {print $3}' \ - | grep -vE '^(/lib|/usr/lib|/lib64)' || true) - for src in ${deps}; do - [ -f "${src}" ] && cp -L "${src}" "${STAGE}/lib/$(basename "${src}")" - done - command -v patchelf >/dev/null 2>&1 || { - echo "ERROR: patchelf is required to make the Linux package relocatable" >&2 - exit 1 - } - patchelf --set-rpath "\$ORIGIN/../lib" "${STAGE}/bin/rcli" - while IFS= read -r -d '' library; do - patchelf --set-rpath "\$ORIGIN" "${library}" - done < <(find "${STAGE}/lib" -type f -print0) - ;; - *) - echo "ERROR: unknown platform tag '${PLATFORM}'" >&2 - exit 1 - ;; -esac - -# ---------------------------------------------------------------------------- -# Fail-closed sanity run from the staged layout. -# ---------------------------------------------------------------------------- -case "${PLATFORM}" in - macos-*|linux-*) "${STAGE}/bin/rcli" version >/dev/null ;; -esac - -# Release artifacts must not disclose the packager's checkout, home, or temp -# locations. Check the current host's concrete prefixes instead of rejecting -# every /Users/ or /home/ string: official third-party binaries -# (including ONNX Runtime) can legitimately retain their upstream producer's -# source paths. -while IFS= read -r -d '' artifact; do - if LC_ALL=C grep -aF -q -- "${REPO_ROOT}" "${artifact}"; then - echo "ERROR: packaged artifact embeds the local checkout path: ${artifact#"${STAGE}/"}" >&2 - exit 1 - fi - if { [ -n "${HOME:-}" ] && LC_ALL=C grep -aF -q -- "${HOME%/}/" "${artifact}"; } \ - || { [ -n "${TMPDIR:-}" ] && LC_ALL=C grep -aF -q -- "${TMPDIR%/}/" "${artifact}"; }; then - echo "ERROR: packaged artifact embeds a packager host path: ${artifact#"${STAGE}/"}" >&2 - exit 1 - fi -done < <(find "${STAGE}/bin" "${STAGE}/lib" -type f -print0) - -mkdir -p "${DIST_DIR}" -rm -f "${TARBALL}" "${TARBALL}.sha256" "${DMG}" "${DMG}.sha256" -tar -czf "${TARBALL}" -C "${STAGE_ROOT}" "rcli-${PLATFORM}" -(cd "${DIST_DIR}" && shasum -a 256 "$(basename "${TARBALL}")" > "$(basename "${TARBALL}").sha256") - -if [[ "${PLATFORM}" == macos-* && "${MACOS_NOTARIZE}" == "1" ]]; then - notary_args=() - if [ -n "${RCLI_NOTARYTOOL_PROFILE:-}" ]; then - notary_args+=(--keychain-profile "${RCLI_NOTARYTOOL_PROFILE}") - if [ -n "${RCLI_NOTARYTOOL_KEYCHAIN:-}" ]; then - [ -f "${RCLI_NOTARYTOOL_KEYCHAIN}" ] || { - echo "ERROR: RCLI_NOTARYTOOL_KEYCHAIN does not exist" >&2 - exit 1 - } - notary_args+=(--keychain "${RCLI_NOTARYTOOL_KEYCHAIN}") - fi - elif [ -n "${RCLI_NOTARY_KEY_PATH:-}" ] \ - && [ -n "${RCLI_NOTARY_KEY_ID:-}" ] \ - && [ -n "${RCLI_NOTARY_ISSUER_ID:-}" ]; then - [ -f "${RCLI_NOTARY_KEY_PATH}" ] || { - echo "ERROR: RCLI_NOTARY_KEY_PATH does not exist" >&2 - exit 1 - } - notary_args+=( - --key "${RCLI_NOTARY_KEY_PATH}" - --key-id "${RCLI_NOTARY_KEY_ID}" - --issuer "${RCLI_NOTARY_ISSUER_ID}" - ) - else - echo "ERROR: provide RCLI_NOTARYTOOL_PROFILE or the complete App Store Connect API-key inputs" >&2 - exit 1 - fi - - hdiutil create -quiet -fs HFS+ -format UDZO \ - -volname "rcli ${VERSION}" \ - -srcfolder "${STAGE}" \ - "${DMG}" - dmg_codesign_args=(--force --sign "${CODESIGN_IDENTITY}" --timestamp) - if [ -n "${CODESIGN_KEYCHAIN}" ]; then - dmg_codesign_args+=(--keychain "${CODESIGN_KEYCHAIN}") - fi - codesign "${dmg_codesign_args[@]}" "${DMG}" - xcrun notarytool submit "${DMG}" "${notary_args[@]}" --wait - xcrun stapler staple "${DMG}" - xcrun stapler validate "${DMG}" - (cd "${DIST_DIR}" && shasum -a 256 "$(basename "${DMG}")" > "$(basename "${DMG}").sha256") -fi - -echo "Packaged: ${TARBALL}" -if [ -f "${DMG}" ]; then - echo "Notarized + stapled: ${DMG}" -fi -echo "Contents:" -tar -tzf "${TARBALL}" | head -20 diff --git a/rcli/scripts/smoke-mlx-cli.sh b/rcli/scripts/smoke-mlx-cli.sh deleted file mode 100755 index f8dd9c887e..0000000000 --- a/rcli/scripts/smoke-mlx-cli.sh +++ /dev/null @@ -1,88 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" -cd "$REPO_ROOT" - -CLI="${RUNANYWHERE_MLX_CLI:-$REPO_ROOT/.build/debug/RunAnywhereMLXCLI}" -HOME_DIR="${RUNANYWHERE_MLX_SMOKE_HOME:-/tmp/runanywhere-mlx-cli-smoke}" -PULL_MODE="${RUNANYWHERE_MLX_SMOKE_PULL:-1}" - -LLM_MODEL="${RUNANYWHERE_MLX_SMOKE_LLM:-mlx-qwen3-0.6b-4bit}" -VLM_MODEL="${RUNANYWHERE_MLX_SMOKE_VLM:-mlx-fastvlm-0.5b-bf16}" -STT_MODEL="${RUNANYWHERE_MLX_SMOKE_STT:-mlx-qwen3-asr-0.6b-8bit}" -TTS_MODEL="${RUNANYWHERE_MLX_SMOKE_TTS:-mlx-soprano-1.1-80m-5bit}" - -if [[ ! -x "$CLI" || ! -f "$(dirname "$CLI")/mlx.metallib" ]]; then - "$SCRIPT_DIR/build-mlx-cli.sh" -fi - -mkdir -p "$HOME_DIR" - -pull_if_enabled() { - local model="$1" - if [[ "$PULL_MODE" == "1" ]]; then - "$CLI" --home "$HOME_DIR" pull "$model" - fi -} - -require_nonempty_file() { - local path="$1" - if [[ ! -s "$path" ]]; then - echo "expected non-empty file: $path" >&2 - exit 1 - fi -} - -require_nonempty_text() { - local label="$1" - local value="$2" - if [[ -z "${value//[[:space:]]/}" ]]; then - echo "$label produced empty output" >&2 - exit 1 - fi -} - -make_default_image() { - local out="$1" - base64 --decode >"$out" <<'PNG' -iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAAiklEQVR4nO3QQQ3AIADAQMDwMPrvQJkQxZbB7iTn9cw7wF2mB6wH7AbYDbAbYDfAbgDdALsBdgPsBtgNsBtgN8BugN0AuwF2A+wG2A2wG2A3wG6A3QC7AXYD7AbYDbAbYDfAboDdALsBdgPsBtgNsBtgN8BugN0AuwF2A+wG2A2wG2A3wG6A3QC7AXYDXBuUBgGkrp6WAAAAAElFTkSuQmCC -PNG -} - -echo "MLX CLI backend smoke" -"$CLI" --home "$HOME_DIR" backends --json | grep -q '"name":"mlx"' -"$CLI" --home "$HOME_DIR" backends --json | grep -q '"name":"llamacpp"' - -echo "LLM: $LLM_MODEL" -pull_if_enabled "$LLM_MODEL" -llm_output="$("$CLI" --home "$HOME_DIR" run "$LLM_MODEL" "Say OK in one short sentence." --max-tokens 16 --temp 0.1)" -require_nonempty_text "LLM" "$llm_output" -printf '%s\n' "$llm_output" - -echo "TTS: $TTS_MODEL" -pull_if_enabled "$TTS_MODEL" -tts_wav="$HOME_DIR/mlx-smoke-tts.wav" -rm -f "$tts_wav" -"$CLI" --home "$HOME_DIR" tts "$TTS_MODEL" --text "Hello from MLX text to speech." --output "$tts_wav" -require_nonempty_file "$tts_wav" - -echo "STT: $STT_MODEL" -pull_if_enabled "$STT_MODEL" -stt_output="$("$CLI" --home "$HOME_DIR" stt "$STT_MODEL" --input "$tts_wav")" -require_nonempty_text "STT" "$stt_output" -printf '%s\n' "$stt_output" - -echo "VLM: $VLM_MODEL" -pull_if_enabled "$VLM_MODEL" -image_path="${RUNANYWHERE_MLX_SMOKE_IMAGE:-$HOME_DIR/mlx-smoke-image.png}" -if [[ -z "${RUNANYWHERE_MLX_SMOKE_IMAGE:-}" ]]; then - make_default_image "$image_path" -fi -vlm_output="$("$CLI" --home "$HOME_DIR" run "$VLM_MODEL" --image "$image_path" \ - "Describe the image in one short sentence." --max-tokens 32 --temp 0.1)" -require_nonempty_text "VLM" "$vlm_output" -printf '%s\n' "$vlm_output" - -echo "MLX CLI smoke passed" diff --git a/rcli/scripts/test-e2e.sh b/rcli/scripts/test-e2e.sh deleted file mode 100755 index 07498c2753..0000000000 --- a/rcli/scripts/test-e2e.sh +++ /dev/null @@ -1,95 +0,0 @@ -#!/usr/bin/env bash -# -# End-to-end test for rcli (the RunAnywhere desktop CLI). -# -# Phases: -# 1. Configure + build rcli and its offline test binaries from commons source. -# 2. Run the offline unit/segment tests. -# 3. Exercise the real CLI: version, help, JSON listing, exit-code contract. -# 4. (optional) Pull a model and run one generation, when RCLI_E2E_MODEL is set. -# -# rcli is a C++ consumer of runanywhere-commons; it builds via the repo-root -# CMake with -DRAC_BUILD_CLI=ON. Per repo resource discipline the native build -# uses -j2 (one heavy build at a time). -# -# Usage: -# rcli/scripts/test-e2e.sh -# RCLI_E2E_MODEL=smollm2-135m-instruct-q4_k_m rcli/scripts/test-e2e.sh -# RCLI_E2E_KEEP_BUILD=1 ... # reuse an existing build dir -# -set -euo pipefail - -REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -cd "$REPO_ROOT" - -case "$(uname -s)" in - Darwin) PRESET="macos-debug" ;; - Linux) PRESET="linux-debug" ;; - *) echo "rcli e2e: unsupported OS $(uname -s)"; exit 2 ;; -esac -BUILD_DIR="build/${PRESET}" - -pass=0; fail=0 -step() { printf '\n\033[1m==> %s\033[0m\n' "$1"; } -ok() { printf ' \033[32mPASS\033[0m %s\n' "$1"; pass=$((pass+1)); } -bad() { printf ' \033[31mFAIL\033[0m %s\n' "$1"; fail=$((fail+1)); } - -# 1. Build ------------------------------------------------------------------ -step "Configuring ($PRESET, CLI + tests)" -cmake --preset "$PRESET" \ - -DRAC_DESKTOP_ADAPTER=ON -DRAC_BUILD_CLI=ON -DRAC_BUILD_TESTS=ON - -step "Building rcli + offline tests (-j2)" -cmake --build "$BUILD_DIR" -j2 --target rcli test_rcli_unit test_rcli_segment - -RCLI="$(find "$BUILD_DIR" -name rcli -type f -perm -u+x | head -1)" -[ -x "$RCLI" ] || { echo "rcli binary not found under $BUILD_DIR"; exit 1; } -echo "rcli: $RCLI" - -# 2. Offline unit/segment tests -------------------------------------------- -step "Offline test binaries" -for t in test_rcli_unit test_rcli_segment; do - bin="$(find "$BUILD_DIR" -name "$t" -type f -perm -u+x | head -1)" - # The rcli test harness lists its subtests when run bare; --run-all executes them. - if [ -x "$bin" ] && "$bin" --run-all; then ok "$t"; else bad "$t"; fi -done - -# 3. CLI contract smoke ----------------------------------------------------- -# Isolate state so the run never touches a developer's real model store. -export RUNANYWHERE_HOME="$(mktemp -d)" -trap 'rm -rf "$RUNANYWHERE_HOME"' EXIT - -step "CLI contract" -"$RCLI" --version >/dev/null 2>&1 && ok "rcli --version (exit 0)" || bad "rcli --version" -"$RCLI" --help >/dev/null 2>&1 && ok "rcli --help (exit 0)" || bad "rcli --help" - -# models list must emit valid JSON when asked for it. -if "$RCLI" models list --json > "$RUNANYWHERE_HOME/list.json" 2>/dev/null; then - if command -v python3 >/dev/null && python3 -c 'import json,sys;json.load(open(sys.argv[1]))' "$RUNANYWHERE_HOME/list.json" 2>/dev/null; then - ok "rcli models list --json (valid JSON)" - else - ok "rcli models list --json (exit 0)" - fi -else - bad "rcli models list --json" -fi - -# Usage errors must exit 2 (0 success / 1 runtime / 2 usage). -set +e -"$RCLI" definitely-not-a-command >/dev/null 2>&1; code=$? -set -e -[ "$code" -eq 2 ] && ok "unknown subcommand exits 2 (got $code)" || bad "unknown subcommand exit code ($code, want 2)" - -# 4. Optional real model round-trip ---------------------------------------- -if [ -n "${RCLI_E2E_MODEL:-}" ]; then - step "Model round-trip: $RCLI_E2E_MODEL" - if "$RCLI" pull "$RCLI_E2E_MODEL"; then ok "pull $RCLI_E2E_MODEL"; else bad "pull $RCLI_E2E_MODEL"; fi - out="$("$RCLI" run "$RCLI_E2E_MODEL" "Reply with the single word: ok" 2>/dev/null || true)" - [ -n "$out" ] && ok "run produced output: $(printf '%s' "$out" | head -c 60)" || bad "run produced no output" -else - echo " (skipping model round-trip; set RCLI_E2E_MODEL= to enable)" -fi - -# Summary ------------------------------------------------------------------- -step "Summary: $pass passed, $fail failed" -[ "$fail" -eq 0 ] diff --git a/rcli/scripts/update-tap.sh b/rcli/scripts/update-tap.sh deleted file mode 100755 index ceca735f5e..0000000000 --- a/rcli/scripts/update-tap.sh +++ /dev/null @@ -1,69 +0,0 @@ -#!/usr/bin/env bash -# ============================================================================= -# update-tap.sh -# -# Renders packaging/homebrew/rcli.rb.in against a PUBLISHED GitHub release -# (downloads the .sha256 sidecars) and pushes Formula/rcli.rb to the Homebrew -# tap repository. -# -# Run manually after the release workflow's draft Release is published: -# ./rcli/scripts/update-tap.sh 0.20.0 -# -# Environment: -# RCLI_TAP_REPO Tap git remote (default git@github.com:RunanywhereAI/homebrew-tap.git) -# RCLI_TAP_DIR Existing tap checkout to reuse (default: fresh temp clone) -# DRY_RUN=1 Render + print, do not commit/push -# ============================================================================= - -set -euo pipefail - -VERSION="${1:?usage: update-tap.sh }" -VERSION="${VERSION#v}" - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -CLI_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" -TEMPLATE="${CLI_ROOT}/packaging/homebrew/rcli.rb.in" -RELEASE_BASE="https://github.com/RunanywhereAI/runanywhere-sdks/releases/download/v${VERSION}" -TAP_REPO="${RCLI_TAP_REPO:-git@github.com:RunanywhereAI/homebrew-tap.git}" - -fetch_sha() { - local asset="$1" - local line - line="$(curl -fsSL "${RELEASE_BASE}/${asset}.sha256")" || - { echo "ERROR: missing release asset ${asset}.sha256 — is v${VERSION} published?" >&2; exit 1; } - echo "${line}" | awk '{print $1}' -} - -echo "Fetching release checksums for v${VERSION}..." -SHA_MAC_ARM="$(fetch_sha "rcli-macos-arm64-v${VERSION}.tar.gz")" -SHA_LINUX_X64="$(fetch_sha "rcli-linux-x86_64-v${VERSION}.tar.gz")" - -RENDERED="$(mktemp)" -sed -e "s/@VERSION@/${VERSION}/g" \ - -e "s/@SHA256_MACOS_ARM64@/${SHA_MAC_ARM}/g" \ - -e "s/@SHA256_LINUX_X86_64@/${SHA_LINUX_X64}/g" \ - "${TEMPLATE}" > "${RENDERED}" - -echo "Rendered formula:" -echo "----------------------------------------" -cat "${RENDERED}" -echo "----------------------------------------" - -if [[ "${DRY_RUN:-0}" == "1" ]]; then - echo "DRY_RUN=1 — not pushing to the tap." - exit 0 -fi - -TAP_DIR="${RCLI_TAP_DIR:-}" -if [[ -z "${TAP_DIR}" ]]; then - TAP_DIR="$(mktemp -d)/homebrew-tap" - git clone --depth 1 "${TAP_REPO}" "${TAP_DIR}" -fi - -mkdir -p "${TAP_DIR}/Formula" -cp "${RENDERED}" "${TAP_DIR}/Formula/rcli.rb" -git -C "${TAP_DIR}" add Formula/rcli.rb -git -C "${TAP_DIR}" commit -m "rcli ${VERSION}" -git -C "${TAP_DIR}" push - -echo "Tap updated: brew install runanywhereai/tap/rcli" diff --git a/rcli/src/app.cpp b/rcli/src/app.cpp deleted file mode 100644 index 9e2ccf1148..0000000000 --- a/rcli/src/app.cpp +++ /dev/null @@ -1,112 +0,0 @@ -#include "app.h" - -#include -#include - -#include - -#include "bootstrap.h" -#include "commands/commands.h" -#include "io/output.h" - -#ifndef RCLI_VERSION -#define RCLI_VERSION "0.0.0-dev" -#endif - -namespace rcli { - -void configure_app(CLI::App& app, GlobalOptions& options) { - app.set_version_flag("--version,-V", std::string("rcli ") + RCLI_VERSION); - app.require_subcommand(0, 1); - app.fallthrough(true); - - app.add_flag("--json", options.json, "Machine-readable JSON output on stdout"); - app.add_flag("-v,--verbose", options.verbose, "Debug logging on stderr"); - app.add_flag("-q,--quiet", options.quiet, "Errors only on stderr"); - app.add_flag("--no-progress", options.no_progress, "Disable progress rendering"); - app.add_option("--home", options.home_override, - "RunAnywhere home directory (default: $RUNANYWHERE_HOME or " - "~/.local/share/runanywhere; models live under /Models)"); - - // Control-plane connection. validation happens in resolve_connection(). - app.add_option("--environment", options.environment, - "SDK environment: development (default, keyless OSS → baked staging " - "backend) or production (API key + https URL).") - ->envname("RUNANYWHERE_ENVIRONMENT") - ->check(CLI::IsMember({"dev", "development", "prod", "production"})); - app.add_option("--base-url", options.base_url, - "Backend base URL. Optional in development (baked staging URL). " - "Required https for production.") - ->envname("RUNANYWHERE_BASE_URL"); - app.add_option("--api-key", options.api_key, - "Control-plane API key (required for production; omit for " - "keyless development)") - ->envname("RUNANYWHERE_API_KEY"); - - // Namespaces first (the spec grammar), then the terminal aliases, then the - // infrastructure commands — that is the order `--help` lists them in. - commands::register_llm(app, options); - commands::register_vlm(app, options); - commands::register_tool(app, options); // must follow register_llm (extends the `llm` group) - commands::register_stt(app, options); - commands::register_tts(app, options); - commands::register_vad(app, options); - commands::register_embed(app, options); - commands::register_rerank(app, options); - commands::register_image(app, options); - commands::register_diarize(app, options); - commands::register_segment(app, options); - commands::register_voice(app, options); - commands::register_rag(app, options); - commands::register_models(app, options); - commands::register_lora(app, options); - - commands::register_llm_aliases(app, options); - commands::register_models_aliases(app, options); - - commands::register_serve(app, options); - commands::register_bench(app, options); - commands::register_backends(app, options); - commands::register_info(app, options); - commands::register_version(app, options); - commands::register_auth(app, options); - commands::register_telemetry(app, options); -} - -int run(int argc, char** argv) { - GlobalOptions options; - - CLI::App app{"RunAnywhere on-device AI CLI — llm, vlm, stt, tts, vad, embed, rerank, " - "image, rag, voice and the models that back them"}; - configure_app(app, options); - - int exit_code = 0; - try { - app.parse(argc, argv); - if (app.get_subcommands().empty()) { - // Bare `rcli` prints help like `ollama` does. - out::status_line(app.help()); - } - } catch (const CLI::CallForHelp& e) { - exit_code = app.exit(e); - } catch (const CLI::CallForVersion& e) { - exit_code = app.exit(e); - } catch (const CLI::RuntimeError& e) { - exit_code = (e.get_exit_code() != 0) ? e.get_exit_code() : 1; - } catch (const CLI::ParseError& e) { - app.exit(e); // prints the usage message to stderr - exit_code = 2; - } catch (const std::exception& e) { - out::error_line(e.what()); - exit_code = 1; - } - - shutdown(); - return exit_code; -} - -} // namespace rcli - -extern "C" int rcli_run_main(int argc, char** argv) { - return rcli::run(argc, argv); -} diff --git a/rcli/src/app.h b/rcli/src/app.h deleted file mode 100644 index 5cc804c3cf..0000000000 --- a/rcli/src/app.h +++ /dev/null @@ -1,20 +0,0 @@ -/** - * @file app.h - * @brief Shared rcli app wiring for the binary and in-process tests. - */ - -#ifndef RCLI_APP_H -#define RCLI_APP_H - -#include - -#include "bootstrap.h" - -namespace rcli { - -void configure_app(CLI::App& app, GlobalOptions& options); -int run(int argc, char** argv); - -} // namespace rcli - -#endif // RCLI_APP_H diff --git a/rcli/src/bootstrap.cpp b/rcli/src/bootstrap.cpp deleted file mode 100644 index 3b16934325..0000000000 --- a/rcli/src/bootstrap.cpp +++ /dev/null @@ -1,665 +0,0 @@ -#include "bootstrap.h" - -#include -#include -#include -#include -#include -#if !defined(_WIN32) -#include -#endif - -#include "rac/core/rac_core.h" -#include "rac/core/rac_logger.h" -#include "rac/core/rac_platform_adapter.h" -#include "rac/core/rac_sdk_state.h" -#include "rac/desktop/rac_desktop.h" -#include "rac/infrastructure/device/rac_device_identity.h" -#include "rac/infrastructure/model_management/rac_model_paths.h" -#include "rac/infrastructure/network/rac_dev_config.h" -#include "rac/infrastructure/network/rac_environment.h" -#include "rac/infrastructure/network/rac_auth_manager.h" -#include "rac/infrastructure/network/rac_endpoints.h" -#include "rac/infrastructure/http/rac_http_client.h" -#include "rac/infrastructure/http/rac_http_transport.h" -#include "rac/infrastructure/telemetry/rac_telemetry_manager.h" -#include "rac/infrastructure/events/rac_sdk_event_stream.h" -#include "rac/lifecycle/rac_sdk_init.h" -#include "rac/foundation/rac_proto_buffer.h" - -#include "model_types.pb.h" -#include "sdk_init.pb.h" - -#include "catalog/catalog.h" -#include "config/cli_paths.h" -#include "device_info.h" -#include "io/output.h" - -#if defined(RCLI_HAS_LLAMACPP) -#include "rac/backends/rac_llm_llamacpp.h" -#endif -#if defined(RCLI_HAS_ONNX) -#include "rac/plugin/rac_plugin_entry_onnx.h" -#endif -#if defined(RCLI_HAS_SHERPA) -#include "rac/plugin/rac_plugin_entry_sherpa.h" -#endif -#if defined(RCLI_HAS_MLX) -#include "rac/backends/rac_mlx.h" -#endif -#if defined(RCLI_HAS_NEURT) -// The neurt engine (Apple-only: ANE LLM + CoreML diffusion) has no dedicated -// rac_backend_neurt_register() fn; register its plugin entry directly. This -// call also keeps the static rac_backend_neurt archive linked (references -// rac_plugin_entry_neurt), mirroring how the other backends stay alive. -#include "rac/plugin/rac_plugin_entry.h" -#include "rac/plugin/rac_plugin_entry_neurt.h" -#endif - -namespace rcli { - -namespace { - -// rac_init requires the adapter pointer to stay valid until rac_shutdown. -rac_platform_adapter_t g_adapter{}; -bool g_bootstrapped = false; - -// Owns the telemetry manager for the process lifetime so the terminal flush in -// rac_shutdown() can deliver through our HTTP callback before teardown. -rac_telemetry_manager_t *g_telemetry_manager = nullptr; - -rac_log_level_t log_level_for(const GlobalOptions &options) { - if (options.verbose) { - return RAC_LOG_DEBUG; - } - // Quiet by default (like ollama): SDK internals only surface at ERROR. - // rcli prints its own user-facing status/progress lines on stderr. - return RAC_LOG_ERROR; -} - -std::string first_env_value(const char *first, const char *second, - const char *third) { - const char *keys[] = {first, second, third}; - for (const char *key : keys) { - if (!key) { - continue; - } - const char *value = std::getenv(key); - if (value && value[0] != '\0') { - return value; - } - } - return {}; -} - -std::string normalize_locale(std::string locale) { - const std::size_t encoding = locale.find('.'); - if (encoding != std::string::npos) { - locale.resize(encoding); - } - const std::size_t modifier = locale.find('@'); - if (modifier != std::string::npos) { - locale.resize(modifier); - } - if (locale.empty() || locale == "C" || locale == "POSIX") { - return {}; - } - for (char &ch : locale) { - if (ch == '_') { - ch = '-'; - } - } - return locale; -} - -std::string detect_locale() { - return normalize_locale(first_env_value("LC_ALL", "LC_MESSAGES", "LANG")); -} - -std::string strip_timezone_prefix(const std::string &path) { - const char *prefixes[] = {"/usr/share/zoneinfo/", - "/var/db/timezone/zoneinfo/", - "/usr/share/lib/zoneinfo/"}; - for (const char *prefix : prefixes) { - const std::size_t len = std::strlen(prefix); - if (path.compare(0, len, prefix) == 0) { - return path.substr(len); - } - } - - const std::string marker = "zoneinfo/"; - const std::size_t marker_pos = path.find(marker); - if (marker_pos != std::string::npos) { - return path.substr(marker_pos + marker.size()); - } - - return {}; -} - -std::string detect_timezone() { - std::string tz = first_env_value("TZ", nullptr, nullptr); - if (!tz.empty()) { - if (tz[0] == ':') { - tz.erase(0, 1); - } - return tz; - } - -#if !defined(_WIN32) - char link_target[1024] = {}; - const ssize_t len = - readlink("/etc/localtime", link_target, sizeof(link_target) - 1); - if (len > 0) { - link_target[len] = '\0'; - return strip_timezone_prefix(link_target); - } -#endif - - return {}; -} - -const char *desktop_platform() { -#if defined(__APPLE__) - return "macos"; -#elif defined(__linux__) - return "linux"; -#elif defined(_WIN32) - return "windows"; -#else - return "desktop"; -#endif -} - -bool parse_environment_name(const std::string &name, rac_environment_t *out) { - if (name.empty() || name == "dev" || name == "development") { - *out = RAC_ENV_DEVELOPMENT; - return true; - } - if (name == "prod" || name == "production") { - *out = RAC_ENV_PRODUCTION; - return true; - } - return false; -} - -// SdkInitEnvironment is gone: SdkInitPhase1Request.environment now takes -// model_types.proto's SDKEnvironment directly (the single environment -// vocabulary across the whole IDL). -::runanywhere::v1::SDKEnvironment -proto_environment_from_rac(rac_environment_t env) { - switch (rac_env_normalize(env)) { - case RAC_ENV_PRODUCTION: - return ::runanywhere::v1::SDK_ENVIRONMENT_PRODUCTION; - default: - return ::runanywhere::v1::SDK_ENVIRONMENT_DEVELOPMENT; - } -} - -void initialize_sdk_metadata(const Connection &connection) { - char device_id[RAC_DEVICE_ID_BUFFER_MIN_SIZE] = {}; - const rac_result_t device_rc = - rac_device_get_or_create_persistent_id(device_id, sizeof(device_id)); - if (device_rc != RAC_SUCCESS) { - out::status_line("warning: device identity unavailable: " + - out::describe_result(device_rc)); - device_id[0] = '\0'; - } - - const std::string locale = detect_locale(); - const std::string timezone = detect_timezone(); - - // Mirror rac_sdk_init_phase1_proto's step order: runtime state first (the - // auth / device-registration / telemetry paths read env + credentials from - // rac_state), then the copied SDK configuration + client info. - // Development fills the baked staging backend URL when base_url is empty. - std::string effective_base_url = connection.base_url; - if (connection.environment == RAC_ENV_DEVELOPMENT && - effective_base_url.empty()) { - const char *baked = rac_dev_config_get_staging_base_url(); - if (rac_dev_config_is_usable_http_url(baked)) { - effective_base_url = baked; - } - } - - const rac_result_t state_rc = rac_state_initialize( - connection.environment, connection.api_key.c_str(), - effective_base_url.c_str(), device_id[0] != '\0' ? device_id : ""); - if (state_rc != RAC_SUCCESS) { - out::status_line("warning: SDK state init failed: " + - out::describe_result(state_rc)); - } - - rac_sdk_config_t sdk_config = {}; - sdk_config.environment = connection.environment; - sdk_config.api_key = connection.api_key.c_str(); - sdk_config.base_url = effective_base_url.c_str(); - sdk_config.device_id = device_id[0] != '\0' ? device_id : ""; - sdk_config.platform = desktop_platform(); - sdk_config.sdk_version = RCLI_VERSION; - sdk_config.client_info.sdk_binding = "cli"; - sdk_config.client_info.app_identifier = "ai.runanywhere.rcli"; - sdk_config.client_info.app_name = "RunAnywhere CLI"; - sdk_config.client_info.app_version = RCLI_VERSION; - sdk_config.client_info.app_build = nullptr; - sdk_config.client_info.locale = locale.empty() ? nullptr : locale.c_str(); - sdk_config.client_info.timezone = timezone.empty() ? nullptr : timezone.c_str(); - - const rac_validation_result_t config_rc = rac_sdk_init(&sdk_config); - if (config_rc != RAC_VALIDATION_OK) { - out::status_line(std::string("warning: SDK metadata init failed: ") + - rac_validation_error_message(config_rc)); - } -} - -// Delivers a queued telemetry batch over the desktop HTTP transport. Wired via -// rac_telemetry_manager_set_http_callback (user_data = the manager) so the -// outcome is reported back through rac_telemetry_manager_http_complete. Mirrors -// the control-plane POST performed by commons' auth path. -void rcli_telemetry_http_callback(void *user_data, const char *endpoint, - const char *json_body, size_t json_length, - rac_bool_t requires_auth) { - auto *manager = static_cast(user_data); - const char *base_url = rac_state_get_base_url(); - if (base_url == nullptr || base_url[0] == '\0' || - rac_http_transport_is_registered() != RAC_TRUE) { - if (manager != nullptr) { - rac_telemetry_manager_http_complete(manager, RAC_FALSE, nullptr, - "telemetry transport unavailable"); - } - return; - } - - char url[2048] = {}; - if (rac_build_url(base_url, endpoint, url, sizeof(url)) < 0) { - if (manager != nullptr) { - rac_telemetry_manager_http_complete(manager, RAC_FALSE, nullptr, - "telemetry URL build failed"); - } - return; - } - - std::vector headers; - const rac_http_header_kv_t *defaults = nullptr; - size_t default_count = 0; - if (rac_http_default_headers(&defaults, &default_count) == RAC_SUCCESS && - defaults != nullptr) { - headers.assign(defaults, defaults + default_count); - } - std::string auth_value; - if (requires_auth == RAC_TRUE) { - const char *token = rac_auth_get_access_token(); - if (token != nullptr && token[0] != '\0') { - auth_value = std::string("Bearer ") + token; - headers.push_back({"Authorization", auth_value.c_str()}); - } - } - - rac_http_client_t *client = nullptr; - if (rac_http_client_create(&client) != RAC_SUCCESS) { - if (manager != nullptr) { - rac_telemetry_manager_http_complete(manager, RAC_FALSE, nullptr, - "telemetry client create failed"); - } - return; - } - - rac_http_request_t request = {}; - request.method = "POST"; - request.url = url; - request.headers = headers.empty() ? nullptr : headers.data(); - request.header_count = headers.size(); - request.body_bytes = reinterpret_cast(json_body); - request.body_len = json_length; - request.timeout_ms = rac_env_default_http_timeout_ms(rac_state_get_environment()); - request.follow_redirects = RAC_FALSE; - - rac_http_response_t response = {}; - const rac_result_t rc = rac_http_request_send(client, &request, &response); - rac_http_client_destroy(client); - - const bool ok = - rc == RAC_SUCCESS && response.status >= 200 && response.status < 300; - std::string body; - if (response.body_bytes != nullptr && response.body_len > 0) { - body.assign(reinterpret_cast(response.body_bytes), - response.body_len); - } - if (!ok) { - // Surface the exact backend rejection (status + response body) so schema - // mismatches (e.g. strict extra_forbidden 422s) are diagnosable from rcli. - out::status_line(std::string("telemetry POST ") + (endpoint ? endpoint : "?") + - " -> rc=" + out::describe_result(rc) + - " http=" + std::to_string(response.status) + - " body=" + (body.empty() ? "(empty)" : body)); - // DEBUG: dump the exact request JSON so a malformed offset can be inspected. - if (const char *dump = std::getenv("RCLI_TELEMETRY_DUMP"); - dump != nullptr && dump[0] != '\0' && json_body != nullptr) { - if (FILE *fp = std::fopen(dump, "ab")) { - std::fwrite(json_body, 1, json_length, fp); - std::fputc('\n', fp); - std::fclose(fp); - } - } - } - if (manager != nullptr) { - rac_telemetry_manager_http_complete(manager, ok ? RAC_TRUE : RAC_FALSE, - body.empty() ? nullptr : body.c_str(), - ok ? nullptr : "telemetry POST failed"); - } - rac_http_response_free(&response); -} - -// Runs the canonical two-phase SDK init so telemetry can flush. -// Development (keyless OSS): Phase 1 fills baked staging backend URL when -// needed; Phase 2 skips JWT/register; telemetry POSTs anonymously → PUBLIC org. -// Production: Phase 2 authenticates + registers with the API key. -void initialize_telemetry_auth(const Connection &connection) { - const bool keyless_dev = connection.environment == RAC_ENV_DEVELOPMENT; - std::string effective_base_url = connection.base_url; - if (keyless_dev && effective_base_url.empty()) { - const char *baked = rac_dev_config_get_staging_base_url(); - if (rac_dev_config_is_usable_http_url(baked)) { - effective_base_url = baked; - } - } - - // No remote telemetry without a base URL (baked or explicit). - if (effective_base_url.empty()) { - return; - } - // Authenticated environments still need an API key. - if (!keyless_dev && connection.api_key.empty()) { - return; - } - - // Enable the auth manager. NULL secure storage: tokens are not persisted - // across runs (fine for a CLI session); authentication still runs per run - // when Phase 2 expects a key. - rac_auth_init(nullptr); - - char device_id[RAC_DEVICE_ID_BUFFER_MIN_SIZE] = {}; - if (rac_device_get_or_create_persistent_id(device_id, sizeof(device_id)) != - RAC_SUCCESS) { - device_id[0] = '\0'; - } - - // Create + register the telemetry sink BEFORE Phase 2 so its flush has a sink - // and events emitted during subsequent commands are tracked. Delivery runs - // through rcli_telemetry_http_callback over the desktop HTTP transport; the - // terminal batch flushes in rac_shutdown() during teardown. - g_telemetry_manager = rac_telemetry_manager_create( - connection.environment, device_id[0] != '\0' ? device_id : "", - desktop_platform(), RCLI_VERSION); - if (g_telemetry_manager != nullptr) { - rac_telemetry_manager_set_http_callback( - g_telemetry_manager, rcli_telemetry_http_callback, g_telemetry_manager); - rac_events_set_telemetry_sink(g_telemetry_manager); - } - - ::runanywhere::v1::SdkInitPhase1Request phase1; - phase1.set_environment(proto_environment_from_rac(connection.environment)); - phase1.set_api_key(connection.api_key); - phase1.set_base_url(effective_base_url); - if (device_id[0] != '\0') { - phase1.set_device_id(device_id); - } - phase1.set_platform(desktop_platform()); - phase1.set_sdk_version(RCLI_VERSION); - - std::string phase1_bytes; - if (!phase1.SerializeToString(&phase1_bytes)) { - out::status_line("warning: telemetry phase 1 serialize failed"); - return; - } - - rac_proto_buffer_t phase1_out; - rac_proto_buffer_init(&phase1_out); - rac_result_t rc = rac_sdk_init_phase1_proto( - reinterpret_cast(phase1_bytes.data()), - phase1_bytes.size(), &phase1_out); - rac_proto_buffer_free(&phase1_out); - if (rc != RAC_SUCCESS) { - out::status_line("warning: telemetry phase 1 failed: " + - out::describe_result(rc)); - return; - } - - // flush_telemetry/discover_downloaded_models/rescan_local_models were - // deleted from SdkInitPhase2Request outright: telemetry flushing and - // registry/local-file reconciliation are now unconditional commons - // behavior on every Phase 2 call, not per-call hints. - ::runanywhere::v1::SdkInitPhase2Request phase2; - - std::string phase2_bytes; - if (!phase2.SerializeToString(&phase2_bytes)) { - out::status_line("warning: telemetry phase 2 serialize failed"); - return; - } - - rac_proto_buffer_t phase2_out; - rac_proto_buffer_init(&phase2_out); - rc = rac_sdk_init_phase2_proto( - reinterpret_cast(phase2_bytes.data()), - phase2_bytes.size(), &phase2_out); - - ::runanywhere::v1::SdkInitResult result; - const bool parsed = phase2_out.status == RAC_SUCCESS && - phase2_out.data != nullptr && - result.ParseFromArray(phase2_out.data, - static_cast(phase2_out.size)); - rac_proto_buffer_free(&phase2_out); - - if (rc != RAC_SUCCESS) { - out::status_line("warning: telemetry phase 2 failed: " + - out::describe_result(rc)); - return; - } - - if (parsed) { - // http_configured/device_registered were deleted outright from - // SdkInitResult; has_completed_http_setup is the cross-phase latched bit - // that survives (the per-call http_configured signal has no replacement). - std::string note = - std::string("telemetry ready | has_completed_http_setup=") + - (result.has_completed_http_setup() ? "yes" : "no"); - if (!result.warning().empty()) { - note += " | " + result.warning(); - } - out::status_line(note); - } -} - -} // namespace - -rac_result_t resolve_connection(const GlobalOptions &options, Connection *out, - std::string *error) { - // Prefer explicit --environment; otherwise CLI11 / getenv via - // RUNANYWHERE_ENVIRONMENT (the single canonical name on the cross-fit line). - std::string environment_name = options.environment; - if (environment_name.empty()) { - environment_name = - first_env_value("RUNANYWHERE_ENVIRONMENT", nullptr, nullptr); - } - std::string base_url = options.base_url; - if (base_url.empty()) { - base_url = first_env_value("RUNANYWHERE_BASE_URL", nullptr, nullptr); - } - std::string api_key = options.api_key; - if (api_key.empty()) { - api_key = first_env_value("RUNANYWHERE_API_KEY", nullptr, nullptr); - } - - Connection connection; - if (!parse_environment_name(environment_name, &connection.environment)) { - if (error) { - *error = "invalid --environment '" + environment_name + - "' (expected development or production)"; - } - return RAC_ERROR_INVALID_CONFIGURATION; - } - connection.base_url = std::move(base_url); - connection.api_key = std::move(api_key); - - // Development (keyless OSS): optional --base-url (else baked staging backend - // URL). API key is optional and usually omitted. - // Production: API key + https base URL required (validators enforce). - const rac_validation_result_t key_rc = rac_validate_api_key( - connection.api_key.empty() ? nullptr : connection.api_key.c_str(), - connection.environment); - if (key_rc != RAC_VALIDATION_OK) { - if (error) { - *error = std::string(rac_validation_error_message(key_rc)) + - " (--api-key / RUNANYWHERE_API_KEY)"; - } - return RAC_ERROR_INVALID_CONFIGURATION; - } - const rac_validation_result_t url_rc = rac_validate_base_url( - connection.base_url.empty() ? nullptr : connection.base_url.c_str(), - connection.environment); - if (url_rc != RAC_VALIDATION_OK) { - if (error) { - *error = std::string(rac_validation_error_message(url_rc)) + - " (--base-url / RUNANYWHERE_BASE_URL)"; - } - return RAC_ERROR_INVALID_CONFIGURATION; - } - - if (out) { - *out = connection; - } - return RAC_SUCCESS; -} - -rac_result_t bootstrap(const GlobalOptions &options, Bootstrapped *out) { - const std::string home = paths::resolve_home(options.home_override); - if (home.empty()) { - out::error_line("cannot resolve RunAnywhere home ($HOME unset?)"); - return RAC_ERROR_NOT_INITIALIZED; - } - - Connection connection; - std::string connection_error; - if (resolve_connection(options, &connection, &connection_error) != - RAC_SUCCESS) { - out::error_line(connection_error); - return RAC_ERROR_INVALID_CONFIGURATION; - } - - if (!g_bootstrapped) { - rac_result_t rc = rac_desktop_adapter_init(nullptr, &g_adapter); - if (rc != RAC_SUCCESS) { - out::error_line("desktop adapter init failed: " + - out::describe_result(rc)); - return rc; - } - - rc = rac_model_paths_set_base_dir(home.c_str()); - if (rc != RAC_SUCCESS) { - out::error_line("model paths init failed: " + out::describe_result(rc)); - return rc; - } - - // Configure the logger BEFORE rac_init so init-time logs obey the CLI - // level too. Two distinct knobs: stderr_always off makes the adapter - // the single sink (commons' own stderr mirror would double every - // line); the logger min level is a separate gate from - // rac_config_t.log_level. - const rac_log_level_t log_level = log_level_for(options); - rac_logger_set_stderr_always(RAC_FALSE); - rac_logger_set_min_level(log_level); - - rac_config_t config = {}; - config.platform_adapter = &g_adapter; - config.log_level = log_level; - config.log_tag = "rcli"; - rc = rac_init(&config); - if (rc != RAC_SUCCESS) { - out::error_line("rac_init failed: " + out::describe_result(rc)); - return rc; - } - - rc = rac_desktop_http_transport_register(); - if (rc != RAC_SUCCESS) { - out::error_line("HTTP transport registration failed: " + - out::describe_result(rc)); - return rc; - } - - initialize_sdk_metadata(connection); - - // Prefer the richer desktop device-info callbacks from device_info.cpp - // (battery/RAM/CPU/fingerprint). control_plane.cpp still owns login() / - // control_plane_post() for the explicit auth/telemetry commands. - if (install_device_callbacks() != RAC_SUCCESS) { - out::status_line("warning: device info callbacks failed to register"); - } - - initialize_telemetry_auth(connection); - -#if defined(RCLI_HAS_LLAMACPP) - if (rac_backend_llamacpp_register() != RAC_SUCCESS) { - out::status_line("warning: llamacpp backend failed to register"); - } -#endif -#if defined(RCLI_HAS_ONNX) - if (rac_backend_onnx_register() != RAC_SUCCESS) { - out::status_line("warning: onnx backend failed to register"); - } -#endif -#if defined(RCLI_HAS_SHERPA) - if (rac_backend_sherpa_register() != RAC_SUCCESS) { - out::status_line("warning: sherpa backend failed to register"); - } -#endif -#if defined(RCLI_HAS_MLX) - if (rac_mlx_is_available() != RAC_TRUE) { - out::status_line( - "warning: mlx backend requires MLX runtime callbacks; skipping registration"); - } else if (rac_backend_mlx_register() != RAC_SUCCESS) { - out::status_line( - "warning: mlx backend requires MLX runtime callbacks; backend failed to register"); - } -#endif -#if defined(RCLI_HAS_NEURT) - if (rac_plugin_register(rac_plugin_entry_neurt()) != RAC_SUCCESS) { - out::status_line("warning: neurt (Apple Neural Engine) backend failed to register"); - } -#endif - - // Built-in catalog — same per-launch registration pattern as the - // example apps (the registry is in-memory). Ad-hoc URL/HF pulls from - // previous runs come back via the commons model-folder manifest - // restore inside the registry refresh/discover paths. - catalog::register_all(); - - g_bootstrapped = true; - } - - if (out) { - out->home = home; - char models[1024] = {}; - if (rac_model_paths_get_models_directory(models, sizeof(models)) == - RAC_SUCCESS) { - out->models_dir = models; - } - } - return RAC_SUCCESS; -} - -void shutdown() { - if (g_bootstrapped) { - // rac_shutdown() flushes the terminal telemetry batch through the - // registered sink (our HTTP callback) before clearing lifetime state. - rac_shutdown(); - rac_events_set_telemetry_sink(nullptr); - if (g_telemetry_manager != nullptr) { - rac_telemetry_manager_destroy(g_telemetry_manager); - g_telemetry_manager = nullptr; - } - g_bootstrapped = false; - } -} - -rac_telemetry_manager_t *active_telemetry_manager() { return g_telemetry_manager; } - -} // namespace rcli diff --git a/rcli/src/bootstrap.h b/rcli/src/bootstrap.h deleted file mode 100644 index fc6202cc18..0000000000 --- a/rcli/src/bootstrap.h +++ /dev/null @@ -1,95 +0,0 @@ -/** - * @file bootstrap.h - * @brief One-call SDK bring-up for every rcli command. - * - * Mirrors the canonical bootstrap proven by the commons real-inference tests - * (tests/test_voice_agent.cpp) with real desktop I/O: - * - * desktop adapter → rac_model_paths_set_base_dir → rac_init → - * curl HTTP transport → backend registration → (PR3: catalog + discovery) - * - * Commands call bootstrap() exactly once; it is idempotent within a process. - */ - -#ifndef RCLI_BOOTSTRAP_H -#define RCLI_BOOTSTRAP_H - -#include - -#include "rac/core/rac_types.h" -#include "rac/infrastructure/network/rac_environment.h" - -typedef struct rac_telemetry_manager rac_telemetry_manager_t; - -namespace rcli { - -/** Global flags shared by all subcommands (parsed in main.cpp). */ -struct GlobalOptions { - bool json = false; - bool verbose = false; - bool quiet = false; - bool no_progress = false; - std::string home_override; // --home flag - - // Control-plane connection. CLI11 fills these from - // --base-url/--api-key/--environment with RUNANYWHERE_BASE_URL / - // RUNANYWHERE_API_KEY / RUNANYWHERE_ENVIRONMENT env-var fallbacks (app.cpp). - // development: keyless OSS → staging backend (baked URL or --base-url). - // production: API key + https URL. - std::string environment; // dev|development|prod|production ("" → dev) - std::string base_url; // development may omit (baked Staging URL) - std::string api_key; // required for production; omit for keyless development -}; - -/** - * Validated control-plane connection resolved from GlobalOptions. - * bootstrap() threads these values into rac_state / rac_sdk_config so the - * commons auth, device-registration, and telemetry paths can read them. - */ -struct Connection { - rac_environment_t environment = RAC_ENV_DEVELOPMENT; - std::string base_url; - std::string api_key; -}; - -/** - * Resolve + validate the connection flags client-side (before any network - * call). On failure fills `error` with an actionable message and returns - * RAC_ERROR_INVALID_CONFIGURATION. - * - * Rules (mirrors commons rac_validate_api_key / rac_validate_base_url): - * - development (default): keyless PUBLIC-org telemetry; optional --base-url - * (else baked staging backend URL). No JWT. - * - production: API key + https base URL required; localhost rejected. - * - * Env: RUNANYWHERE_ENVIRONMENT (also --environment). - */ -rac_result_t resolve_connection(const GlobalOptions& options, Connection* out, std::string* error); - -/** Resolved environment after bootstrap. */ -struct Bootstrapped { - std::string home; // RunAnywhere home (storage base dir) - std::string models_dir; // commons-derived models directory -}; - -/** - * Initialize the SDK for CLI use. Logs go to stderr at WARNING by default - * (DEBUG with --verbose, ERROR with --quiet). - * - * @return RAC_SUCCESS or the first failing step's error code. - */ -rac_result_t bootstrap(const GlobalOptions& options, Bootstrapped* out); - -/** rac_shutdown() wrapper; safe to call when bootstrap never ran. */ -void shutdown(); - -/** - * The process telemetry manager created by bootstrap() (NULL if telemetry was - * not initialized, e.g. no creds). Exposed for the live telemetry integration - * test, which overrides its HTTP callback to observe the backend's response. - */ -rac_telemetry_manager_t* active_telemetry_manager(); - -} // namespace rcli - -#endif // RCLI_BOOTSTRAP_H diff --git a/rcli/src/catalog/catalog.cpp b/rcli/src/catalog/catalog.cpp deleted file mode 100644 index e0fac9d206..0000000000 --- a/rcli/src/catalog/catalog.cpp +++ /dev/null @@ -1,2007 +0,0 @@ -#include "catalog/catalog.h" - -#include - -#include "rac/core/rac_core.h" -#include "rac/infrastructure/model_management/rac_model_registry.h" - -#include "io/output.h" -#include "io/proto.h" - -namespace rcli::catalog { - -namespace { - -namespace v1 = runanywhere::v1; - -// VLM pairs / multi-file artifacts. Filenames are the URL basenames so the -// llamacpp loader finds the mmproj companion next to the primary gguf. -constexpr CatalogFile kSmolVlm2Files[] = { - {"https://huggingface.co/ggml-org/SmolVLM2-256M-Video-Instruct-GGUF/" - "resolve/main/" - "SmolVLM2-256M-Video-Instruct-Q8_0.gguf", - "SmolVLM2-256M-Video-Instruct-Q8_0.gguf", true}, - {"https://huggingface.co/ggml-org/SmolVLM2-256M-Video-Instruct-GGUF/" - "resolve/main/" - "mmproj-SmolVLM2-256M-Video-Instruct-Q8_0.gguf", - "mmproj-SmolVLM2-256M-Video-Instruct-Q8_0.gguf", true}, -}; - -constexpr CatalogFile kLfm2VlFiles[] = { - {"https://huggingface.co/runanywhere/LFM2-VL-450M-GGUF/resolve/main/" - "LFM2-VL-450M-Q8_0.gguf", - "LFM2-VL-450M-Q8_0.gguf", true}, - {"https://huggingface.co/runanywhere/LFM2-VL-450M-GGUF/resolve/main/" - "mmproj-LFM2-VL-450M-Q8_0.gguf", - "mmproj-LFM2-VL-450M-Q8_0.gguf", true}, -}; - -// LiquidAI's own GGUF export. general.architecture is "lfm2" (same as the -// LFM2-VL 450M row above) and the mmproj is a standard clip/mmproj projector, -// verified by reading both GGUF headers off HF. -constexpr CatalogFile kLfm2_5Vl3BFiles[] = { - {"https://huggingface.co/LiquidAI/LFM2.5-VL-3B-GGUF/resolve/main/" - "LFM2.5-VL-3B-Q4_K_M.gguf", - "LFM2.5-VL-3B-Q4_K_M.gguf", true, 1674454240LL}, - {"https://huggingface.co/LiquidAI/LFM2.5-VL-3B-GGUF/resolve/main/" - "mmproj-LFM2.5-VL-3B-Q8_0.gguf", - "mmproj-LFM2.5-VL-3B-Q8_0.gguf", true, 583109120LL}, -}; - -constexpr CatalogFile kQwen2VlFiles[] = { - {"https://huggingface.co/ggml-org/Qwen2-VL-2B-Instruct-GGUF/resolve/main/" - "Qwen2-VL-2B-Instruct-Q4_K_M.gguf", - "Qwen2-VL-2B-Instruct-Q4_K_M.gguf", true}, - {"https://huggingface.co/ggml-org/Qwen2-VL-2B-Instruct-GGUF/resolve/main/" - "mmproj-Qwen2-VL-2B-Instruct-Q8_0.gguf", - "mmproj-Qwen2-VL-2B-Instruct-Q8_0.gguf", true}, -}; - -constexpr CatalogFile kFara15GgufFiles[] = { - {"https://huggingface.co/runanywhere/Fara1.5-4B-GGUF/resolve/main/" - "Fara1.5-4B-Q4_K_M.gguf", - "Fara1.5-4B-Q4_K_M.gguf", true}, - {"https://huggingface.co/runanywhere/Fara1.5-4B-GGUF/resolve/main/" - "mmproj-Fara1.5-4B-f16.gguf", - "mmproj-Fara1.5-4B-f16.gguf", true}, -}; - -constexpr CatalogFile kMiniLmFiles[] = { - {"https://huggingface.co/Xenova/all-MiniLM-L6-v2/resolve/main/onnx/" - "model.onnx", - "model.onnx", true}, - {"https://huggingface.co/Xenova/all-MiniLM-L6-v2/resolve/main/vocab.txt", - "vocab.txt", true}, -}; - -constexpr CatalogFile kSherpaParakeetTdtV2Files[] = { - {"https://huggingface.co/csukuangfj/" - "sherpa-onnx-nemo-parakeet-tdt-0.6b-v2-int8/resolve/" - "1ab9323565ddb038682214b292f588070a538ce2/encoder.int8.onnx", - "encoder.int8.onnx", true, 652184296LL}, - {"https://huggingface.co/csukuangfj/" - "sherpa-onnx-nemo-parakeet-tdt-0.6b-v2-int8/resolve/" - "1ab9323565ddb038682214b292f588070a538ce2/decoder.int8.onnx", - "decoder.int8.onnx", true, 7257753LL}, - {"https://huggingface.co/csukuangfj/" - "sherpa-onnx-nemo-parakeet-tdt-0.6b-v2-int8/resolve/" - "1ab9323565ddb038682214b292f588070a538ce2/joiner.int8.onnx", - "joiner.int8.onnx", true, 1739080LL}, - {"https://huggingface.co/csukuangfj/" - "sherpa-onnx-nemo-parakeet-tdt-0.6b-v2-int8/resolve/" - "1ab9323565ddb038682214b292f588070a538ce2/tokens.txt", - "tokens.txt", true, 9384LL}, -}; - -constexpr CatalogFile kSherpaParakeetTdtV3Files[] = { - {"https://huggingface.co/csukuangfj/" - "sherpa-onnx-nemo-parakeet-tdt-0.6b-v3-int8/resolve/" - "2bda32ec70b097a55adaa07d9a7173915b43cc78/encoder.int8.onnx", - "encoder.int8.onnx", true, 652184281LL}, - {"https://huggingface.co/csukuangfj/" - "sherpa-onnx-nemo-parakeet-tdt-0.6b-v3-int8/resolve/" - "2bda32ec70b097a55adaa07d9a7173915b43cc78/decoder.int8.onnx", - "decoder.int8.onnx", true, 11845275LL}, - {"https://huggingface.co/csukuangfj/" - "sherpa-onnx-nemo-parakeet-tdt-0.6b-v3-int8/resolve/" - "2bda32ec70b097a55adaa07d9a7173915b43cc78/joiner.int8.onnx", - "joiner.int8.onnx", true, 6355277LL}, - {"https://huggingface.co/csukuangfj/" - "sherpa-onnx-nemo-parakeet-tdt-0.6b-v3-int8/resolve/" - "2bda32ec70b097a55adaa07d9a7173915b43cc78/tokens.txt", - "tokens.txt", true, 93939LL}, -}; - -constexpr CatalogFile kSherpaCanary180MFiles[] = { - {"https://huggingface.co/csukuangfj/" - "sherpa-onnx-nemo-canary-180m-flash-en-es-de-fr-int8/resolve/" - "9077164e0d3dd1d5353743e89ceaa1d3a770838c/encoder.int8.onnx", - "encoder.int8.onnx", true, 132678643LL}, - {"https://huggingface.co/csukuangfj/" - "sherpa-onnx-nemo-canary-180m-flash-en-es-de-fr-int8/resolve/" - "9077164e0d3dd1d5353743e89ceaa1d3a770838c/decoder.int8.onnx", - "decoder.int8.onnx", true, 74437848LL}, - {"https://huggingface.co/csukuangfj/" - "sherpa-onnx-nemo-canary-180m-flash-en-es-de-fr-int8/resolve/" - "9077164e0d3dd1d5353743e89ceaa1d3a770838c/tokens.txt", - "tokens.txt", true, 53555LL}, -}; - -// Sherpa 1.13.5 is required for the corrected NeMo streaming-transducer -// decoder. Keep every URL on the immutable HF revision and verify each large -// artifact independently so a mutable model update cannot enter a release. -constexpr CatalogFile kSherpaNemotronStreamingAsrFiles[] = { - {"https://huggingface.co/csukuangfj2/" - "sherpa-onnx-nemotron-3.5-asr-streaming-0.6b-320ms-int8-2026-06-11/" - "resolve/424ce58898995b713f84341f2e1492f9207a26aa/encoder.int8.onnx", - "encoder.int8.onnx", true, 657601518LL, - "f79c3fcc149f268b54b7d5754bdc2ba5c47c16b1fc70d15728a56f6efbf60ca5"}, - {"https://huggingface.co/csukuangfj2/" - "sherpa-onnx-nemotron-3.5-asr-streaming-0.6b-320ms-int8-2026-06-11/" - "resolve/424ce58898995b713f84341f2e1492f9207a26aa/decoder.int8.onnx", - "decoder.int8.onnx", true, 14978075LL, - "19f9c98fc6d0a2c33a65a43b36fdb2e914c26c0aa9764be3aebc502a1e982fb0"}, - {"https://huggingface.co/csukuangfj2/" - "sherpa-onnx-nemotron-3.5-asr-streaming-0.6b-320ms-int8-2026-06-11/" - "resolve/424ce58898995b713f84341f2e1492f9207a26aa/joiner.int8.onnx", - "joiner.int8.onnx", true, 9504438LL, - "4101c7c679a0bc30483794b27a059e34e79232aa2068d78d51231a22c8b0d7ce"}, - {"https://huggingface.co/csukuangfj2/" - "sherpa-onnx-nemotron-3.5-asr-streaming-0.6b-320ms-int8-2026-06-11/" - "resolve/424ce58898995b713f84341f2e1492f9207a26aa/tokens.txt", - "tokens.txt", true, 131440LL, - "729cc103155bafa785f9cd45746cd41cabe97eab7182fc04d594129587958f8a"}, -}; - -// The upstream OpenVoiceOS export omits three metadata_props entries Sherpa -// requires, so it cannot be loaded as published. This repo is that export with -// the entries added; provenance and a reproduction script live in its model -// card. -constexpr CatalogFile kSherpaParakeetCtcFiles[] = { - {"https://huggingface.co/runanywhere/" - "sherpa-onnx-nemo-parakeet-ctc-1.1b-int8/resolve/" - "48a549f552774db3cd09dd1548f3d1a2b37bc7c5/model.int8.onnx", - "model.int8.onnx", true, 1110014145LL, - "62f73c17a5301c048c7273cf24ef1cd0c3621d3625c5415fbafe5633d7bf2f98"}, - {"https://huggingface.co/runanywhere/" - "sherpa-onnx-nemo-parakeet-ctc-1.1b-int8/resolve/" - "48a549f552774db3cd09dd1548f3d1a2b37bc7c5/tokens.txt", - "tokens.txt", true, 10374LL, - "ed16e1a4e3a3aa379138c0b1888e5d49f993c9d512b2be4d46e90a87afd54921"}, -}; - -constexpr CatalogFile kMlxQwen3_06BFiles[] = { - {"https://huggingface.co/mlx-community/Qwen3-0.6B-4bit/resolve/main/" - "added_tokens.json", - "added_tokens.json", true}, - {"https://huggingface.co/mlx-community/Qwen3-0.6B-4bit/resolve/main/" - "config.json", - "config.json", true}, - {"https://huggingface.co/mlx-community/Qwen3-0.6B-4bit/resolve/main/" - "merges.txt", - "merges.txt", true}, - {"https://huggingface.co/mlx-community/Qwen3-0.6B-4bit/resolve/main/" - "model.safetensors", - "model.safetensors", true}, - {"https://huggingface.co/mlx-community/Qwen3-0.6B-4bit/resolve/main/" - "model.safetensors.index.json", - "model.safetensors.index.json", true}, - {"https://huggingface.co/mlx-community/Qwen3-0.6B-4bit/resolve/main/" - "special_tokens_map.json", - "special_tokens_map.json", true}, - {"https://huggingface.co/mlx-community/Qwen3-0.6B-4bit/resolve/main/" - "tokenizer.json", - "tokenizer.json", true}, - {"https://huggingface.co/mlx-community/Qwen3-0.6B-4bit/resolve/main/" - "tokenizer_config.json", - "tokenizer_config.json", true}, - {"https://huggingface.co/mlx-community/Qwen3-0.6B-4bit/resolve/main/" - "vocab.json", - "vocab.json", true}, -}; - -constexpr CatalogFile kMlxMaplePreviewFiles[] = { - {"https://huggingface.co/deepgrove/maple-preview-2bit-mlx/resolve/" - "d0a7314d6bf14c880201b599d7a701cfbc8717e6/added_tokens.json", - "added_tokens.json", true, 707LL}, - {"https://huggingface.co/deepgrove/maple-preview-2bit-mlx/resolve/" - "d0a7314d6bf14c880201b599d7a701cfbc8717e6/chat_template.jinja", - "chat_template.jinja", true, 3292LL}, - {"https://huggingface.co/deepgrove/maple-preview-2bit-mlx/resolve/" - "d0a7314d6bf14c880201b599d7a701cfbc8717e6/config.json", - "config.json", true, 2710LL}, - {"https://huggingface.co/deepgrove/maple-preview-2bit-mlx/resolve/" - "d0a7314d6bf14c880201b599d7a701cfbc8717e6/merges.txt", - "merges.txt", true, 1671853LL}, - {"https://huggingface.co/deepgrove/maple-preview-2bit-mlx/resolve/" - "d0a7314d6bf14c880201b599d7a701cfbc8717e6/" - "model-00001-of-00003.safetensors", - "model-00001-of-00003.safetensors", true, 2162084350LL}, - {"https://huggingface.co/deepgrove/maple-preview-2bit-mlx/resolve/" - "d0a7314d6bf14c880201b599d7a701cfbc8717e6/" - "model-00002-of-00003.safetensors", - "model-00002-of-00003.safetensors", true, 2187444586LL}, - {"https://huggingface.co/deepgrove/maple-preview-2bit-mlx/resolve/" - "d0a7314d6bf14c880201b599d7a701cfbc8717e6/" - "model-00003-of-00003.safetensors", - "model-00003-of-00003.safetensors", true, 958711742LL}, - {"https://huggingface.co/deepgrove/maple-preview-2bit-mlx/resolve/" - "d0a7314d6bf14c880201b599d7a701cfbc8717e6/" - "model-flashhead.safetensors", - "model-flashhead.safetensors", true, 6087456LL}, - {"https://huggingface.co/deepgrove/maple-preview-2bit-mlx/resolve/" - "d0a7314d6bf14c880201b599d7a701cfbc8717e6/" - "model.safetensors.index.json", - "model.safetensors.index.json", true, 40054LL}, - {"https://huggingface.co/deepgrove/maple-preview-2bit-mlx/resolve/" - "d0a7314d6bf14c880201b599d7a701cfbc8717e6/special_tokens_map.json", - "special_tokens_map.json", true, 613LL}, - {"https://huggingface.co/deepgrove/maple-preview-2bit-mlx/resolve/" - "d0a7314d6bf14c880201b599d7a701cfbc8717e6/tokenizer.json", - "tokenizer.json", true, 11422654LL}, - {"https://huggingface.co/deepgrove/maple-preview-2bit-mlx/resolve/" - "d0a7314d6bf14c880201b599d7a701cfbc8717e6/tokenizer_config.json", - "tokenizer_config.json", true, 5432LL}, - {"https://huggingface.co/deepgrove/maple-preview-2bit-mlx/resolve/" - "d0a7314d6bf14c880201b599d7a701cfbc8717e6/vocab.json", - "vocab.json", true, 2776833LL}, -}; - -constexpr CatalogFile kMlxNemotronNano8BFiles[] = { - {"https://huggingface.co/bourn23/" - "nvidia-llama-3.1-nemotron-nano-8b-v1-mlx-4bit/resolve/" - "00378e66048eadf358aad0f66c09e5c3750f8243/chat_template.jinja", - "chat_template.jinja", true}, - {"https://huggingface.co/bourn23/" - "nvidia-llama-3.1-nemotron-nano-8b-v1-mlx-4bit/resolve/" - "00378e66048eadf358aad0f66c09e5c3750f8243/config.json", - "config.json", true}, - {"https://huggingface.co/bourn23/" - "nvidia-llama-3.1-nemotron-nano-8b-v1-mlx-4bit/resolve/" - "00378e66048eadf358aad0f66c09e5c3750f8243/generation_config.json", - "generation_config.json", true}, - {"https://huggingface.co/bourn23/" - "nvidia-llama-3.1-nemotron-nano-8b-v1-mlx-4bit/resolve/" - "00378e66048eadf358aad0f66c09e5c3750f8243/model.safetensors", - "model.safetensors", true}, - {"https://huggingface.co/bourn23/" - "nvidia-llama-3.1-nemotron-nano-8b-v1-mlx-4bit/resolve/" - "00378e66048eadf358aad0f66c09e5c3750f8243/" - "model.safetensors.index.json", - "model.safetensors.index.json", true}, - {"https://huggingface.co/bourn23/" - "nvidia-llama-3.1-nemotron-nano-8b-v1-mlx-4bit/resolve/" - "00378e66048eadf358aad0f66c09e5c3750f8243/special_tokens_map.json", - "special_tokens_map.json", true}, - {"https://huggingface.co/bourn23/" - "nvidia-llama-3.1-nemotron-nano-8b-v1-mlx-4bit/resolve/" - "00378e66048eadf358aad0f66c09e5c3750f8243/tokenizer.json", - "tokenizer.json", true}, - {"https://huggingface.co/bourn23/" - "nvidia-llama-3.1-nemotron-nano-8b-v1-mlx-4bit/resolve/" - "00378e66048eadf358aad0f66c09e5c3750f8243/tokenizer_config.json", - "tokenizer_config.json", true}, -}; - -constexpr CatalogFile kMlxNemotronMini4BFiles[] = { - {"https://huggingface.co/mlx-community/" - "Nemotron-Mini-4B-Instruct-4bit-mlx/resolve/" - "b5784198153d2d71afcc97d4cc38c049abced8cd/chat_template.jinja", - "chat_template.jinja", true}, - {"https://huggingface.co/mlx-community/" - "Nemotron-Mini-4B-Instruct-4bit-mlx/resolve/" - "b5784198153d2d71afcc97d4cc38c049abced8cd/config.json", - "config.json", true}, - {"https://huggingface.co/mlx-community/" - "Nemotron-Mini-4B-Instruct-4bit-mlx/resolve/" - "b5784198153d2d71afcc97d4cc38c049abced8cd/model.safetensors", - "model.safetensors", true}, - {"https://huggingface.co/mlx-community/" - "Nemotron-Mini-4B-Instruct-4bit-mlx/resolve/" - "b5784198153d2d71afcc97d4cc38c049abced8cd/" - "model.safetensors.index.json", - "model.safetensors.index.json", true}, - {"https://huggingface.co/mlx-community/" - "Nemotron-Mini-4B-Instruct-4bit-mlx/resolve/" - "b5784198153d2d71afcc97d4cc38c049abced8cd/tokenizer.json", - "tokenizer.json", true}, - {"https://huggingface.co/mlx-community/" - "Nemotron-Mini-4B-Instruct-4bit-mlx/resolve/" - "b5784198153d2d71afcc97d4cc38c049abced8cd/tokenizer_config.json", - "tokenizer_config.json", true}, -}; - -constexpr CatalogFile kMlxLlama32_1BFiles[] = { - {"https://huggingface.co/mlx-community/Llama-3.2-1B-Instruct-4bit/resolve/" - "main/" - "config.json", - "config.json", true}, - {"https://huggingface.co/mlx-community/Llama-3.2-1B-Instruct-4bit/resolve/" - "main/" - "model.safetensors", - "model.safetensors", true}, - {"https://huggingface.co/mlx-community/Llama-3.2-1B-Instruct-4bit/resolve/" - "main/" - "model.safetensors.index.json", - "model.safetensors.index.json", true}, - {"https://huggingface.co/mlx-community/Llama-3.2-1B-Instruct-4bit/resolve/" - "main/" - "special_tokens_map.json", - "special_tokens_map.json", true}, - {"https://huggingface.co/mlx-community/Llama-3.2-1B-Instruct-4bit/resolve/" - "main/" - "tokenizer.json", - "tokenizer.json", true}, - {"https://huggingface.co/mlx-community/Llama-3.2-1B-Instruct-4bit/resolve/" - "main/" - "tokenizer_config.json", - "tokenizer_config.json", true}, -}; - -constexpr CatalogFile kMlxQwen2Vl2BFiles[] = { - {"https://huggingface.co/mlx-community/Qwen2-VL-2B-Instruct-4bit/resolve/" - "main/" - "added_tokens.json", - "added_tokens.json", true}, - {"https://huggingface.co/mlx-community/Qwen2-VL-2B-Instruct-4bit/resolve/" - "main/" - "chat_template.json", - "chat_template.json", true}, - {"https://huggingface.co/mlx-community/Qwen2-VL-2B-Instruct-4bit/resolve/" - "main/" - "config.json", - "config.json", true}, - {"https://huggingface.co/mlx-community/Qwen2-VL-2B-Instruct-4bit/resolve/" - "main/" - "merges.txt", - "merges.txt", true}, - {"https://huggingface.co/mlx-community/Qwen2-VL-2B-Instruct-4bit/resolve/" - "main/" - "model.safetensors", - "model.safetensors", true}, - {"https://huggingface.co/mlx-community/Qwen2-VL-2B-Instruct-4bit/resolve/" - "main/" - "model.safetensors.index.json", - "model.safetensors.index.json", true}, - {"https://huggingface.co/mlx-community/Qwen2-VL-2B-Instruct-4bit/resolve/" - "main/" - "preprocessor_config.json", - "preprocessor_config.json", true}, - {"https://huggingface.co/mlx-community/Qwen2-VL-2B-Instruct-4bit/resolve/" - "main/" - "special_tokens_map.json", - "special_tokens_map.json", true}, - {"https://huggingface.co/mlx-community/Qwen2-VL-2B-Instruct-4bit/resolve/" - "main/" - "tokenizer.json", - "tokenizer.json", true}, - {"https://huggingface.co/mlx-community/Qwen2-VL-2B-Instruct-4bit/resolve/" - "main/" - "tokenizer_config.json", - "tokenizer_config.json", true}, - {"https://huggingface.co/mlx-community/Qwen2-VL-2B-Instruct-4bit/resolve/" - "main/" - "vocab.json", - "vocab.json", true}, -}; - -constexpr CatalogFile kMlxFastVlm05BFiles[] = { - {"https://huggingface.co/mlx-community/FastVLM-0.5B-bf16/resolve/main/" - "added_tokens.json", - "added_tokens.json", true}, - {"https://huggingface.co/mlx-community/FastVLM-0.5B-bf16/resolve/main/" - "chat_template.jinja", - "chat_template.jinja", true}, - {"https://huggingface.co/mlx-community/FastVLM-0.5B-bf16/resolve/main/" - "config.json", - "config.json", true}, - {"https://huggingface.co/mlx-community/FastVLM-0.5B-bf16/resolve/main/" - "llava_qwen.py", - "llava_qwen.py", false}, - {"https://huggingface.co/mlx-community/FastVLM-0.5B-bf16/resolve/main/" - "merges.txt", - "merges.txt", true}, - {"https://huggingface.co/mlx-community/FastVLM-0.5B-bf16/resolve/main/" - "model.safetensors", - "model.safetensors", true}, - {"https://huggingface.co/mlx-community/FastVLM-0.5B-bf16/resolve/main/" - "model.safetensors.index.json", - "model.safetensors.index.json", true}, - {"https://huggingface.co/mlx-community/FastVLM-0.5B-bf16/resolve/main/" - "preprocessor_config.json", - "preprocessor_config.json", true}, - {"https://huggingface.co/mlx-community/FastVLM-0.5B-bf16/resolve/main/" - "processing_fastvlm.py", - "processing_fastvlm.py", false}, - {"https://huggingface.co/mlx-community/FastVLM-0.5B-bf16/resolve/main/" - "processor_config.json", - "processor_config.json", true}, - {"https://huggingface.co/mlx-community/FastVLM-0.5B-bf16/resolve/main/" - "special_tokens_map.json", - "special_tokens_map.json", true}, - {"https://huggingface.co/mlx-community/FastVLM-0.5B-bf16/resolve/main/" - "tokenizer.json", - "tokenizer.json", true}, - {"https://huggingface.co/mlx-community/FastVLM-0.5B-bf16/resolve/main/" - "tokenizer_config.json", - "tokenizer_config.json", true}, - {"https://huggingface.co/mlx-community/FastVLM-0.5B-bf16/resolve/main/" - "vocab.json", - "vocab.json", true}, -}; - -// LiquidAI's own MLX 4-bit export. config.json model_type is "lfm2_vl", which -// the pinned mlx-swift-lm 3.31.5 registers in VLMModelFactory (together with -// the "Lfm2VlProcessor" processor class this repo declares). It ships no -// merges.txt/vocab.json — tokenizer.json is the self-contained fast-tokenizer -// format — and its processor lives in processor_config.json rather than -// preprocessor_config.json, which the factory also accepts. Verified via the HF -// API file listing this session; do not add filenames that are not below. -constexpr CatalogFile kMlxLfm2_5Vl3BFiles[] = { - {"https://huggingface.co/LiquidAI/LFM2.5-VL-3B-MLX-4bit/resolve/main/" - "chat_template.jinja", - "chat_template.jinja", true}, - {"https://huggingface.co/LiquidAI/LFM2.5-VL-3B-MLX-4bit/resolve/main/" - "config.json", - "config.json", true}, - {"https://huggingface.co/LiquidAI/LFM2.5-VL-3B-MLX-4bit/resolve/main/" - "generation_config.json", - "generation_config.json", true}, - {"https://huggingface.co/LiquidAI/LFM2.5-VL-3B-MLX-4bit/resolve/main/" - "model.safetensors", - "model.safetensors", true}, - {"https://huggingface.co/LiquidAI/LFM2.5-VL-3B-MLX-4bit/resolve/main/" - "model.safetensors.index.json", - "model.safetensors.index.json", true}, - {"https://huggingface.co/LiquidAI/LFM2.5-VL-3B-MLX-4bit/resolve/main/" - "processor_config.json", - "processor_config.json", true}, - {"https://huggingface.co/LiquidAI/LFM2.5-VL-3B-MLX-4bit/resolve/main/" - "special_tokens_map.json", - "special_tokens_map.json", true}, - {"https://huggingface.co/LiquidAI/LFM2.5-VL-3B-MLX-4bit/resolve/main/" - "tokenizer.json", - "tokenizer.json", true}, - {"https://huggingface.co/LiquidAI/LFM2.5-VL-3B-MLX-4bit/resolve/main/" - "tokenizer_config.json", - "tokenizer_config.json", true}, -}; - -constexpr CatalogFile kMlxQwen3Embedding06BFiles[] = { - {"https://huggingface.co/mlx-community/Qwen3-Embedding-0.6B-4bit-DWQ/" - "resolve/main/" - "added_tokens.json", - "added_tokens.json", true}, - {"https://huggingface.co/mlx-community/Qwen3-Embedding-0.6B-4bit-DWQ/" - "resolve/main/" - "chat_template.jinja", - "chat_template.jinja", true}, - {"https://huggingface.co/mlx-community/Qwen3-Embedding-0.6B-4bit-DWQ/" - "resolve/main/" - "config.json", - "config.json", true}, - {"https://huggingface.co/mlx-community/Qwen3-Embedding-0.6B-4bit-DWQ/" - "resolve/main/" - "generation_config.json", - "generation_config.json", true}, - {"https://huggingface.co/mlx-community/Qwen3-Embedding-0.6B-4bit-DWQ/" - "resolve/main/" - "merges.txt", - "merges.txt", true}, - {"https://huggingface.co/mlx-community/Qwen3-Embedding-0.6B-4bit-DWQ/" - "resolve/main/" - "model.safetensors", - "model.safetensors", true}, - {"https://huggingface.co/mlx-community/Qwen3-Embedding-0.6B-4bit-DWQ/" - "resolve/main/" - "model.safetensors.index.json", - "model.safetensors.index.json", true}, - {"https://huggingface.co/mlx-community/Qwen3-Embedding-0.6B-4bit-DWQ/" - "resolve/main/" - "special_tokens_map.json", - "special_tokens_map.json", true}, - {"https://huggingface.co/mlx-community/Qwen3-Embedding-0.6B-4bit-DWQ/" - "resolve/main/" - "tokenizer.json", - "tokenizer.json", true}, - {"https://huggingface.co/mlx-community/Qwen3-Embedding-0.6B-4bit-DWQ/" - "resolve/main/" - "tokenizer_config.json", - "tokenizer_config.json", true}, - {"https://huggingface.co/mlx-community/Qwen3-Embedding-0.6B-4bit-DWQ/" - "resolve/main/" - "vocab.json", - "vocab.json", true}, -}; - -constexpr CatalogFile kMlxQwen3Asr06BFiles[] = { - {"https://huggingface.co/mlx-community/Qwen3-ASR-0.6B-8bit/resolve/main/" - "chat_template.json", - "chat_template.json", true}, - {"https://huggingface.co/mlx-community/Qwen3-ASR-0.6B-8bit/resolve/main/" - "config.json", - "config.json", true}, - {"https://huggingface.co/mlx-community/Qwen3-ASR-0.6B-8bit/resolve/main/" - "generation_config.json", - "generation_config.json", true}, - {"https://huggingface.co/mlx-community/Qwen3-ASR-0.6B-8bit/resolve/main/" - "merges.txt", - "merges.txt", true}, - {"https://huggingface.co/mlx-community/Qwen3-ASR-0.6B-8bit/resolve/main/" - "model.safetensors", - "model.safetensors", true}, - {"https://huggingface.co/mlx-community/Qwen3-ASR-0.6B-8bit/resolve/main/" - "model.safetensors.index.json", - "model.safetensors.index.json", true}, - {"https://huggingface.co/mlx-community/Qwen3-ASR-0.6B-8bit/resolve/main/" - "preprocessor_config.json", - "preprocessor_config.json", true}, - {"https://huggingface.co/mlx-community/Qwen3-ASR-0.6B-8bit/resolve/main/" - "tokenizer_config.json", - "tokenizer_config.json", true}, - {"https://huggingface.co/mlx-community/Qwen3-ASR-0.6B-8bit/resolve/main/" - "vocab.json", - "vocab.json", true}, -}; - -constexpr CatalogFile kMlxGlmAsrNano2512Files[] = { - {"https://huggingface.co/mlx-community/GLM-ASR-Nano-2512-4bit/resolve/main/" - "config.json", - "config.json", true}, - {"https://huggingface.co/mlx-community/GLM-ASR-Nano-2512-4bit/resolve/main/" - "configuration_glmasr.py", - "configuration_glmasr.py", false}, - {"https://huggingface.co/mlx-community/GLM-ASR-Nano-2512-4bit/resolve/main/" - "inference.py", - "inference.py", false}, - {"https://huggingface.co/mlx-community/GLM-ASR-Nano-2512-4bit/resolve/main/" - "model.safetensors", - "model.safetensors", true}, - {"https://huggingface.co/mlx-community/GLM-ASR-Nano-2512-4bit/resolve/main/" - "model.safetensors.index.json", - "model.safetensors.index.json", true}, - {"https://huggingface.co/mlx-community/GLM-ASR-Nano-2512-4bit/resolve/main/" - "modeling_audio.py", - "modeling_audio.py", false}, - {"https://huggingface.co/mlx-community/GLM-ASR-Nano-2512-4bit/resolve/main/" - "modeling_glmasr.py", - "modeling_glmasr.py", false}, - {"https://huggingface.co/mlx-community/GLM-ASR-Nano-2512-4bit/resolve/main/" - "tokenizer.json", - "tokenizer.json", true}, - {"https://huggingface.co/mlx-community/GLM-ASR-Nano-2512-4bit/resolve/main/" - "tokenizer_config.json", - "tokenizer_config.json", true}, -}; - -constexpr CatalogFile kMlxParakeetCtc11BFiles[] = { - {"https://huggingface.co/mlx-community/parakeet-ctc-1.1b/resolve/" - "295d0c0557aef0c445db79b3d09c9a94a69ffeaf/config.json", - "config.json", true}, - {"https://huggingface.co/mlx-community/parakeet-ctc-1.1b/resolve/" - "295d0c0557aef0c445db79b3d09c9a94a69ffeaf/model.safetensors", - "model.safetensors", true}, -}; - -constexpr CatalogFile kMlxParakeetTdtV2Files[] = { - {"https://huggingface.co/mlx-community/parakeet-tdt-0.6b-v2/resolve/" - "8ae155301e23d820d82aa60d24817c900e69e487/config.json", - "config.json", true}, - {"https://huggingface.co/mlx-community/parakeet-tdt-0.6b-v2/resolve/" - "8ae155301e23d820d82aa60d24817c900e69e487/model.safetensors", - "model.safetensors", true}, -}; - -constexpr CatalogFile kMlxParakeetTdtV3Files[] = { - {"https://huggingface.co/mlx-community/parakeet-tdt-0.6b-v3/resolve/" - "ed2b7e8c15f9aaa0b5772e2efb986255eaef7e15/config.json", - "config.json", true}, - {"https://huggingface.co/mlx-community/parakeet-tdt-0.6b-v3/resolve/" - "ed2b7e8c15f9aaa0b5772e2efb986255eaef7e15/model.safetensors", - "model.safetensors", true}, -}; - -constexpr CatalogFile kMlxParakeetRnnt11BFiles[] = { - {"https://huggingface.co/mlx-community/parakeet-rnnt-1.1b/resolve/" - "7f399a0d3442123deae9194e71f5c984b2879efa/config.json", - "config.json", true}, - {"https://huggingface.co/mlx-community/parakeet-rnnt-1.1b/resolve/" - "7f399a0d3442123deae9194e71f5c984b2879efa/model.safetensors", - "model.safetensors", true}, -}; - -constexpr CatalogFile kMlxNemotronStreamingAsrFiles[] = { - {"https://huggingface.co/mlx-community/" - "nemotron-3.5-asr-streaming-0.6b-8bit/resolve/" - "7279359e4481b5e9e185a318bd618e429c6d86cd/config.json", - "config.json", true}, - {"https://huggingface.co/mlx-community/" - "nemotron-3.5-asr-streaming-0.6b-8bit/resolve/" - "7279359e4481b5e9e185a318bd618e429c6d86cd/model.safetensors", - "model.safetensors", true}, -}; - -constexpr CatalogFile kMlxQwen3Tts06BBaseFiles[] = { - {"https://huggingface.co/mlx-community/Qwen3-TTS-12Hz-0.6B-Base-8bit/" - "resolve/main/" - "config.json", - "config.json", true}, - {"https://huggingface.co/mlx-community/Qwen3-TTS-12Hz-0.6B-Base-8bit/" - "resolve/main/" - "generation_config.json", - "generation_config.json", true}, - {"https://huggingface.co/mlx-community/Qwen3-TTS-12Hz-0.6B-Base-8bit/" - "resolve/main/" - "merges.txt", - "merges.txt", true}, - {"https://huggingface.co/mlx-community/Qwen3-TTS-12Hz-0.6B-Base-8bit/" - "resolve/main/" - "model.safetensors", - "model.safetensors", true}, - {"https://huggingface.co/mlx-community/Qwen3-TTS-12Hz-0.6B-Base-8bit/" - "resolve/main/" - "model.safetensors.index.json", - "model.safetensors.index.json", true}, - {"https://huggingface.co/mlx-community/Qwen3-TTS-12Hz-0.6B-Base-8bit/" - "resolve/main/" - "preprocessor_config.json", - "preprocessor_config.json", true}, - {"https://huggingface.co/mlx-community/Qwen3-TTS-12Hz-0.6B-Base-8bit/" - "resolve/main/" - "speech_tokenizer/config.json", - "speech_tokenizer/config.json", true}, - {"https://huggingface.co/mlx-community/Qwen3-TTS-12Hz-0.6B-Base-8bit/" - "resolve/main/" - "speech_tokenizer/configuration.json", - "speech_tokenizer/configuration.json", true}, - {"https://huggingface.co/mlx-community/Qwen3-TTS-12Hz-0.6B-Base-8bit/" - "resolve/main/" - "speech_tokenizer/model.safetensors", - "speech_tokenizer/model.safetensors", true}, - {"https://huggingface.co/mlx-community/Qwen3-TTS-12Hz-0.6B-Base-8bit/" - "resolve/main/" - "speech_tokenizer/preprocessor_config.json", - "speech_tokenizer/preprocessor_config.json", true}, - {"https://huggingface.co/mlx-community/Qwen3-TTS-12Hz-0.6B-Base-8bit/" - "resolve/main/" - "tokenizer_config.json", - "tokenizer_config.json", true}, - {"https://huggingface.co/mlx-community/Qwen3-TTS-12Hz-0.6B-Base-8bit/" - "resolve/main/" - "vocab.json", - "vocab.json", true}, -}; - -constexpr CatalogFile kMlxSoprano1180M5BitFiles[] = { - {"https://huggingface.co/mlx-community/Soprano-1.1-80M-5bit/resolve/main/" - "config.json", - "config.json", true}, - {"https://huggingface.co/mlx-community/Soprano-1.1-80M-5bit/resolve/main/" - "generation_config.json", - "generation_config.json", true}, - {"https://huggingface.co/mlx-community/Soprano-1.1-80M-5bit/resolve/main/" - "model.safetensors", - "model.safetensors", true}, - {"https://huggingface.co/mlx-community/Soprano-1.1-80M-5bit/resolve/main/" - "model.safetensors.index.json", - "model.safetensors.index.json", true}, - {"https://huggingface.co/mlx-community/Soprano-1.1-80M-5bit/resolve/main/" - "special_tokens_map.json", - "special_tokens_map.json", true}, - {"https://huggingface.co/mlx-community/Soprano-1.1-80M-5bit/resolve/main/" - "tokenizer.json", - "tokenizer.json", true}, - {"https://huggingface.co/mlx-community/Soprano-1.1-80M-5bit/resolve/main/" - "tokenizer_config.json", - "tokenizer_config.json", true}, -}; - -// PrismML Bonsai-27B 1-bit MLX (qwen3_5). Files match the HF repo siblings -// needed for mlx-swift-lm load (weights + tokenizer + config). Vision -// preprocessor stubs are present on HF but not required for text-only LLM use. -constexpr CatalogFile kMlxBonsai27B1BitFiles[] = { - {"https://huggingface.co/prism-ml/Bonsai-27B-mlx-1bit/resolve/main/" - "chat_template.jinja", - "chat_template.jinja", true}, - {"https://huggingface.co/prism-ml/Bonsai-27B-mlx-1bit/resolve/main/" - "config.json", - "config.json", true}, - {"https://huggingface.co/prism-ml/Bonsai-27B-mlx-1bit/resolve/main/" - "merges.txt", - "merges.txt", true}, - {"https://huggingface.co/prism-ml/Bonsai-27B-mlx-1bit/resolve/main/" - "model.safetensors", - "model.safetensors", true}, - {"https://huggingface.co/prism-ml/Bonsai-27B-mlx-1bit/resolve/main/" - "model.safetensors.index.json", - "model.safetensors.index.json", true}, - {"https://huggingface.co/prism-ml/Bonsai-27B-mlx-1bit/resolve/main/" - "tokenizer.json", - "tokenizer.json", true}, - {"https://huggingface.co/prism-ml/Bonsai-27B-mlx-1bit/resolve/main/" - "tokenizer_config.json", - "tokenizer_config.json", true}, - {"https://huggingface.co/prism-ml/Bonsai-27B-mlx-1bit/resolve/main/" - "vocab.json", - "vocab.json", true}, -}; - -// PrismML Bonsai 1-bit MLX at 1.7B/4B/8B — same 8-file set as the 27B above -// (mlx-swift-lm needs weights + tokenizer + config; vision preprocessor stubs -// on some repos are not required for text-only LLM use). -#define BONSAI_MLX_FILES(repo) \ - {"https://huggingface.co/prism-ml/" repo \ - "/resolve/main/chat_template.jinja", \ - "chat_template.jinja", true}, \ - {"https://huggingface.co/prism-ml/" repo "/resolve/main/config.json", \ - "config.json", true}, \ - {"https://huggingface.co/prism-ml/" repo "/resolve/main/merges.txt", \ - "merges.txt", true}, \ - {"https://huggingface.co/prism-ml/" repo \ - "/resolve/main/model.safetensors", \ - "model.safetensors", true}, \ - {"https://huggingface.co/prism-ml/" repo \ - "/resolve/main/model.safetensors.index.json", \ - "model.safetensors.index.json", true}, \ - {"https://huggingface.co/prism-ml/" repo "/resolve/main/tokenizer.json", \ - "tokenizer.json", true}, \ - {"https://huggingface.co/prism-ml/" repo \ - "/resolve/main/tokenizer_config.json", \ - "tokenizer_config.json", true}, \ - {"https://huggingface.co/prism-ml/" repo "/resolve/main/vocab.json", \ - "vocab.json", true}, - -constexpr CatalogFile kMlxBonsai1_7B1BitFiles[] = { - BONSAI_MLX_FILES("Bonsai-1.7B-mlx-1bit")}; -constexpr CatalogFile kMlxBonsai4B1BitFiles[] = { - BONSAI_MLX_FILES("Bonsai-4B-mlx-1bit")}; -constexpr CatalogFile kMlxBonsai8B1BitFiles[] = { - BONSAI_MLX_FILES("Bonsai-8B-mlx-1bit")}; - -// PrismML Ternary-Bonsai 2-bit MLX at 1.7B/4B/8B — these repos do NOT ship -// merges.txt/vocab.json (tokenizer.json is the self-contained fast-tokenizer -// format here), unlike the plain-Bonsai repos above. Verified via HF API file -// listing this session — do not add those two filenames or the download 404s. -#define TERNARY_BONSAI_MLX_FILES_SMALL(repo) \ - {"https://huggingface.co/prism-ml/" repo \ - "/resolve/main/chat_template.jinja", \ - "chat_template.jinja", true}, \ - {"https://huggingface.co/prism-ml/" repo "/resolve/main/config.json", \ - "config.json", true}, \ - {"https://huggingface.co/prism-ml/" repo \ - "/resolve/main/model.safetensors", \ - "model.safetensors", true}, \ - {"https://huggingface.co/prism-ml/" repo \ - "/resolve/main/model.safetensors.index.json", \ - "model.safetensors.index.json", true}, \ - {"https://huggingface.co/prism-ml/" repo "/resolve/main/tokenizer.json", \ - "tokenizer.json", true}, \ - {"https://huggingface.co/prism-ml/" repo \ - "/resolve/main/tokenizer_config.json", \ - "tokenizer_config.json", true}, - -constexpr CatalogFile kMlxTernaryBonsai1_7B2BitFiles[] = { - TERNARY_BONSAI_MLX_FILES_SMALL("Ternary-Bonsai-1.7B-mlx-2bit")}; -constexpr CatalogFile kMlxTernaryBonsai4B2BitFiles[] = { - TERNARY_BONSAI_MLX_FILES_SMALL("Ternary-Bonsai-4B-mlx-2bit")}; -constexpr CatalogFile kMlxTernaryBonsai8B2BitFiles[] = { - TERNARY_BONSAI_MLX_FILES_SMALL("Ternary-Bonsai-8B-mlx-2bit")}; - -// Ternary-Bonsai-27B-mlx-2bit DOES ship merges.txt/vocab.json (matches the -// plain-Bonsai 8-file pattern) — verified via HF API file listing this -// session; the smaller Ternary sizes above do not. -constexpr CatalogFile kMlxTernaryBonsai27B2BitFiles[] = { - BONSAI_MLX_FILES("Ternary-Bonsai-27B-mlx-2bit")}; - -#undef BONSAI_MLX_FILES -#undef TERNARY_BONSAI_MLX_FILES_SMALL - -// Meta Muse Glimmer 30B GGUF + mmproj (image-text-to-text; llama.cpp mmproj is -// image-only, so this is vision-capable, not the checkpoint's full "omni" -// audio/video marketing claim). Sizes verified via HF API blobs this session. -constexpr CatalogFile kMuseGlimmer30BFiles[] = { - {"https://huggingface.co/unsloth/Muse-Glimmer-30B-GGUF/resolve/" - "faa5b025c584459c13febfa5c59883516710ae39/" - "Muse-Glimmer-30B-UD-Q4_K_XL.gguf", - "Muse-Glimmer-30B-UD-Q4_K_XL.gguf", true, 15878222368LL}, - {"https://huggingface.co/unsloth/Muse-Glimmer-30B-GGUF/resolve/" - "faa5b025c584459c13febfa5c59883516710ae39/" - "mmproj-Muse-Glimmer-30B-Q8_0.gguf", - "mmproj-Muse-Glimmer-30B-Q8_0.gguf", true, 2051685088LL}, -}; - -// NVIDIA Nemotron-3-Nano-Omni-30B-A3B-Reasoning GGUF + mmproj. Same -// image-only mmproj caveat as Muse Glimmer above: this is vision-capable via -// llama.cpp, not the model's full audio/video "omni" surface. -constexpr CatalogFile kNemotronOmniReasoningFiles[] = { - {"https://huggingface.co/unsloth/" - "NVIDIA-Nemotron-3-Nano-Omni-30B-A3B-Reasoning-GGUF/resolve/" - "571758804835f56154718683f5c0e388b7d0fef9/" - "NVIDIA-Nemotron-3-Nano-Omni-30B-A3B-Reasoning-UD-Q4_K_M.gguf", - "NVIDIA-Nemotron-3-Nano-Omni-30B-A3B-Reasoning-UD-Q4_K_M.gguf", true, - 23887023552LL}, - {"https://huggingface.co/unsloth/" - "NVIDIA-Nemotron-3-Nano-Omni-30B-A3B-Reasoning-GGUF/resolve/" - "571758804835f56154718683f5c0e388b7d0fef9/" - "mmproj-F16.gguf", - "mmproj-F16.gguf", true, 1587540224LL}, -}; - -// mlx-community/gemma-4-e2b-it-4bit — config.json model_type "gemma4", -// registered in the pinned mlx-swift-lm 3.31.5 LLMTypeRegistry/VLMTypeRegistry -// alike. File list + sizes verified via HF API blobs this session. -constexpr CatalogFile kMlxGemma4E2BFiles[] = { - {"https://huggingface.co/mlx-community/gemma-4-e2b-it-4bit/resolve/" - "238767527555cb75a05732a84dff5d6ba0dd6809/" - "chat_template.jinja", - "chat_template.jinja", true}, - {"https://huggingface.co/mlx-community/gemma-4-e2b-it-4bit/resolve/" - "238767527555cb75a05732a84dff5d6ba0dd6809/" - "config.json", - "config.json", true}, - {"https://huggingface.co/mlx-community/gemma-4-e2b-it-4bit/resolve/" - "238767527555cb75a05732a84dff5d6ba0dd6809/" - "generation_config.json", - "generation_config.json", true}, - {"https://huggingface.co/mlx-community/gemma-4-e2b-it-4bit/resolve/" - "238767527555cb75a05732a84dff5d6ba0dd6809/" - "model.safetensors", - "model.safetensors", true, 3550670554LL}, - {"https://huggingface.co/mlx-community/gemma-4-e2b-it-4bit/resolve/" - "238767527555cb75a05732a84dff5d6ba0dd6809/" - "model.safetensors.index.json", - "model.safetensors.index.json", true}, - {"https://huggingface.co/mlx-community/gemma-4-e2b-it-4bit/resolve/" - "238767527555cb75a05732a84dff5d6ba0dd6809/" - "processor_config.json", - "processor_config.json", true}, - {"https://huggingface.co/mlx-community/gemma-4-e2b-it-4bit/resolve/" - "238767527555cb75a05732a84dff5d6ba0dd6809/" - "tokenizer.json", - "tokenizer.json", true}, - {"https://huggingface.co/mlx-community/gemma-4-e2b-it-4bit/resolve/" - "238767527555cb75a05732a84dff5d6ba0dd6809/" - "tokenizer_config.json", - "tokenizer_config.json", true}, -}; - -// mlx-community/gemma-4-E4B-it-qat-4bit — model_type "gemma4" (registered). -constexpr CatalogFile kMlxGemma4E4BFiles[] = { - {"https://huggingface.co/mlx-community/gemma-4-E4B-it-qat-4bit/resolve/" - "0f35c6f6d386f7f74e628bd7c6526ce531212300/" - "chat_template.jinja", - "chat_template.jinja", true}, - {"https://huggingface.co/mlx-community/gemma-4-E4B-it-qat-4bit/resolve/" - "0f35c6f6d386f7f74e628bd7c6526ce531212300/" - "config.json", - "config.json", true}, - {"https://huggingface.co/mlx-community/gemma-4-E4B-it-qat-4bit/resolve/" - "0f35c6f6d386f7f74e628bd7c6526ce531212300/" - "generation_config.json", - "generation_config.json", true}, - {"https://huggingface.co/mlx-community/gemma-4-E4B-it-qat-4bit/resolve/" - "0f35c6f6d386f7f74e628bd7c6526ce531212300/" - "model-00001-of-00002.safetensors", - "model-00001-of-00002.safetensors", true, 4249502053LL}, - {"https://huggingface.co/mlx-community/gemma-4-E4B-it-qat-4bit/resolve/" - "0f35c6f6d386f7f74e628bd7c6526ce531212300/" - "model-00002-of-00002.safetensors", - "model-00002-of-00002.safetensors", true, 2548805689LL}, - {"https://huggingface.co/mlx-community/gemma-4-E4B-it-qat-4bit/resolve/" - "0f35c6f6d386f7f74e628bd7c6526ce531212300/" - "model.safetensors.index.json", - "model.safetensors.index.json", true}, - {"https://huggingface.co/mlx-community/gemma-4-E4B-it-qat-4bit/resolve/" - "0f35c6f6d386f7f74e628bd7c6526ce531212300/" - "processor_config.json", - "processor_config.json", true}, - {"https://huggingface.co/mlx-community/gemma-4-E4B-it-qat-4bit/resolve/" - "0f35c6f6d386f7f74e628bd7c6526ce531212300/" - "tokenizer.json", - "tokenizer.json", true}, - {"https://huggingface.co/mlx-community/gemma-4-E4B-it-qat-4bit/resolve/" - "0f35c6f6d386f7f74e628bd7c6526ce531212300/" - "tokenizer_config.json", - "tokenizer_config.json", true}, -}; - -// mlx-community/gemma-4-12B-it-qat-4bit — model_type "gemma4_unified" -// (registered in both LLMTypeRegistry and VLMTypeRegistry). -constexpr CatalogFile kMlxGemma4_12BFiles[] = { - {"https://huggingface.co/mlx-community/gemma-4-12B-it-qat-4bit/resolve/" - "e70c6b3ba0979b3357dcd2f223ad8bde7787a6b6/" - "chat_template.jinja", - "chat_template.jinja", true}, - {"https://huggingface.co/mlx-community/gemma-4-12B-it-qat-4bit/resolve/" - "e70c6b3ba0979b3357dcd2f223ad8bde7787a6b6/" - "config.json", - "config.json", true}, - {"https://huggingface.co/mlx-community/gemma-4-12B-it-qat-4bit/resolve/" - "e70c6b3ba0979b3357dcd2f223ad8bde7787a6b6/" - "generation_config.json", - "generation_config.json", true}, - {"https://huggingface.co/mlx-community/gemma-4-12B-it-qat-4bit/resolve/" - "e70c6b3ba0979b3357dcd2f223ad8bde7787a6b6/" - "model-00001-of-00003.safetensors", - "model-00001-of-00003.safetensors", true, 5343482357LL}, - {"https://huggingface.co/mlx-community/gemma-4-12B-it-qat-4bit/resolve/" - "e70c6b3ba0979b3357dcd2f223ad8bde7787a6b6/" - "model-00002-of-00003.safetensors", - "model-00002-of-00003.safetensors", true, 5315166254LL}, - {"https://huggingface.co/mlx-community/gemma-4-12B-it-qat-4bit/resolve/" - "e70c6b3ba0979b3357dcd2f223ad8bde7787a6b6/" - "model-00003-of-00003.safetensors", - "model-00003-of-00003.safetensors", true, 329123819LL}, - {"https://huggingface.co/mlx-community/gemma-4-12B-it-qat-4bit/resolve/" - "e70c6b3ba0979b3357dcd2f223ad8bde7787a6b6/" - "model.safetensors.index.json", - "model.safetensors.index.json", true}, - {"https://huggingface.co/mlx-community/gemma-4-12B-it-qat-4bit/resolve/" - "e70c6b3ba0979b3357dcd2f223ad8bde7787a6b6/" - "processor_config.json", - "processor_config.json", true}, - {"https://huggingface.co/mlx-community/gemma-4-12B-it-qat-4bit/resolve/" - "e70c6b3ba0979b3357dcd2f223ad8bde7787a6b6/" - "tokenizer.json", - "tokenizer.json", true}, - {"https://huggingface.co/mlx-community/gemma-4-12B-it-qat-4bit/resolve/" - "e70c6b3ba0979b3357dcd2f223ad8bde7787a6b6/" - "tokenizer_config.json", - "tokenizer_config.json", true}, -}; - -// mlx-community/gemma-4-26b-a4b-it-4bit (MoE) — model_type "gemma4" -// (registered). -constexpr CatalogFile kMlxGemma4_26BA4BFiles[] = { - {"https://huggingface.co/mlx-community/gemma-4-26b-a4b-it-4bit/resolve/" - "0d77464eeb233a2da68ebf9d7dc4edaac7db956d/" - "chat_template.jinja", - "chat_template.jinja", true}, - {"https://huggingface.co/mlx-community/gemma-4-26b-a4b-it-4bit/resolve/" - "0d77464eeb233a2da68ebf9d7dc4edaac7db956d/" - "config.json", - "config.json", true}, - {"https://huggingface.co/mlx-community/gemma-4-26b-a4b-it-4bit/resolve/" - "0d77464eeb233a2da68ebf9d7dc4edaac7db956d/" - "generation_config.json", - "generation_config.json", true}, - {"https://huggingface.co/mlx-community/gemma-4-26b-a4b-it-4bit/resolve/" - "0d77464eeb233a2da68ebf9d7dc4edaac7db956d/" - "model-00001-of-00003.safetensors", - "model-00001-of-00003.safetensors", true, 5320218487LL}, - {"https://huggingface.co/mlx-community/gemma-4-26b-a4b-it-4bit/resolve/" - "0d77464eeb233a2da68ebf9d7dc4edaac7db956d/" - "model-00002-of-00003.safetensors", - "model-00002-of-00003.safetensors", true, 5363328422LL}, - {"https://huggingface.co/mlx-community/gemma-4-26b-a4b-it-4bit/resolve/" - "0d77464eeb233a2da68ebf9d7dc4edaac7db956d/" - "model-00003-of-00003.safetensors", - "model-00003-of-00003.safetensors", true, 4657658867LL}, - {"https://huggingface.co/mlx-community/gemma-4-26b-a4b-it-4bit/resolve/" - "0d77464eeb233a2da68ebf9d7dc4edaac7db956d/" - "model.safetensors.index.json", - "model.safetensors.index.json", true}, - {"https://huggingface.co/mlx-community/gemma-4-26b-a4b-it-4bit/resolve/" - "0d77464eeb233a2da68ebf9d7dc4edaac7db956d/" - "processor_config.json", - "processor_config.json", true}, - {"https://huggingface.co/mlx-community/gemma-4-26b-a4b-it-4bit/resolve/" - "0d77464eeb233a2da68ebf9d7dc4edaac7db956d/" - "tokenizer.json", - "tokenizer.json", true}, - {"https://huggingface.co/mlx-community/gemma-4-26b-a4b-it-4bit/resolve/" - "0d77464eeb233a2da68ebf9d7dc4edaac7db956d/" - "tokenizer_config.json", - "tokenizer_config.json", true}, -}; - -// mlx-community/gemma-4-31b-it-4bit — the plain 4bit variant, NOT -// "-qat-4bit" (that name 404s / does not exist as a clean repo; verified this -// session). model_type "gemma4" (registered). -constexpr CatalogFile kMlxGemma4_31BFiles[] = { - {"https://huggingface.co/mlx-community/gemma-4-31b-it-4bit/resolve/" - "696d436c404745a59f30e4939a658162b0a9e57f/" - "chat_template.jinja", - "chat_template.jinja", true}, - {"https://huggingface.co/mlx-community/gemma-4-31b-it-4bit/resolve/" - "696d436c404745a59f30e4939a658162b0a9e57f/" - "config.json", - "config.json", true}, - {"https://huggingface.co/mlx-community/gemma-4-31b-it-4bit/resolve/" - "696d436c404745a59f30e4939a658162b0a9e57f/" - "generation_config.json", - "generation_config.json", true}, - {"https://huggingface.co/mlx-community/gemma-4-31b-it-4bit/resolve/" - "696d436c404745a59f30e4939a658162b0a9e57f/" - "model-00001-of-00004.safetensors", - "model-00001-of-00004.safetensors", true, 5366617512LL}, - {"https://huggingface.co/mlx-community/gemma-4-31b-it-4bit/resolve/" - "696d436c404745a59f30e4939a658162b0a9e57f/" - "model-00002-of-00004.safetensors", - "model-00002-of-00004.safetensors", true, 5361642573LL}, - {"https://huggingface.co/mlx-community/gemma-4-31b-it-4bit/resolve/" - "696d436c404745a59f30e4939a658162b0a9e57f/" - "model-00003-of-00004.safetensors", - "model-00003-of-00004.safetensors", true, 5367276094LL}, - {"https://huggingface.co/mlx-community/gemma-4-31b-it-4bit/resolve/" - "696d436c404745a59f30e4939a658162b0a9e57f/" - "model-00004-of-00004.safetensors", - "model-00004-of-00004.safetensors", true, 2316480497LL}, - {"https://huggingface.co/mlx-community/gemma-4-31b-it-4bit/resolve/" - "696d436c404745a59f30e4939a658162b0a9e57f/" - "model.safetensors.index.json", - "model.safetensors.index.json", true}, - {"https://huggingface.co/mlx-community/gemma-4-31b-it-4bit/resolve/" - "696d436c404745a59f30e4939a658162b0a9e57f/" - "processor_config.json", - "processor_config.json", true}, - {"https://huggingface.co/mlx-community/gemma-4-31b-it-4bit/resolve/" - "696d436c404745a59f30e4939a658162b0a9e57f/" - "tokenizer.json", - "tokenizer.json", true}, - {"https://huggingface.co/mlx-community/gemma-4-31b-it-4bit/resolve/" - "696d436c404745a59f30e4939a658162b0a9e57f/" - "tokenizer_config.json", - "tokenizer_config.json", true}, -}; - -// mlx-community/Qwen3.6-35B-A3B-4bit (MoE) — config.json model_type -// "qwen3_5_moe", registered in mlx-swift-lm 3.31.5's LLMTypeRegistry. -constexpr CatalogFile kMlxQwen3_6_35BA3BFiles[] = { - {"https://huggingface.co/mlx-community/Qwen3.6-35B-A3B-4bit/resolve/" - "38740b847e4cb78f352aba30aa41c76e08e6eb46/" - "chat_template.jinja", - "chat_template.jinja", true}, - {"https://huggingface.co/mlx-community/Qwen3.6-35B-A3B-4bit/resolve/" - "38740b847e4cb78f352aba30aa41c76e08e6eb46/" - "config.json", - "config.json", true}, - {"https://huggingface.co/mlx-community/Qwen3.6-35B-A3B-4bit/resolve/" - "38740b847e4cb78f352aba30aa41c76e08e6eb46/" - "configuration.json", - "configuration.json", true}, - {"https://huggingface.co/mlx-community/Qwen3.6-35B-A3B-4bit/resolve/" - "38740b847e4cb78f352aba30aa41c76e08e6eb46/" - "generation_config.json", - "generation_config.json", true}, - {"https://huggingface.co/mlx-community/Qwen3.6-35B-A3B-4bit/resolve/" - "38740b847e4cb78f352aba30aa41c76e08e6eb46/" - "model-00001-of-00004.safetensors", - "model-00001-of-00004.safetensors", true, 5288196018LL}, - {"https://huggingface.co/mlx-community/Qwen3.6-35B-A3B-4bit/resolve/" - "38740b847e4cb78f352aba30aa41c76e08e6eb46/" - "model-00002-of-00004.safetensors", - "model-00002-of-00004.safetensors", true, 5368472749LL}, - {"https://huggingface.co/mlx-community/Qwen3.6-35B-A3B-4bit/resolve/" - "38740b847e4cb78f352aba30aa41c76e08e6eb46/" - "model-00003-of-00004.safetensors", - "model-00003-of-00004.safetensors", true, 5368324139LL}, - {"https://huggingface.co/mlx-community/Qwen3.6-35B-A3B-4bit/resolve/" - "38740b847e4cb78f352aba30aa41c76e08e6eb46/" - "model-00004-of-00004.safetensors", - "model-00004-of-00004.safetensors", true, 4377211365LL}, - {"https://huggingface.co/mlx-community/Qwen3.6-35B-A3B-4bit/resolve/" - "38740b847e4cb78f352aba30aa41c76e08e6eb46/" - "model.safetensors.index.json", - "model.safetensors.index.json", true}, - {"https://huggingface.co/mlx-community/Qwen3.6-35B-A3B-4bit/resolve/" - "38740b847e4cb78f352aba30aa41c76e08e6eb46/" - "preprocessor_config.json", - "preprocessor_config.json", true}, - {"https://huggingface.co/mlx-community/Qwen3.6-35B-A3B-4bit/resolve/" - "38740b847e4cb78f352aba30aa41c76e08e6eb46/" - "processor_config.json", - "processor_config.json", true}, - {"https://huggingface.co/mlx-community/Qwen3.6-35B-A3B-4bit/resolve/" - "38740b847e4cb78f352aba30aa41c76e08e6eb46/" - "tokenizer.json", - "tokenizer.json", true}, - {"https://huggingface.co/mlx-community/Qwen3.6-35B-A3B-4bit/resolve/" - "38740b847e4cb78f352aba30aa41c76e08e6eb46/" - "tokenizer_config.json", - "tokenizer_config.json", true}, - {"https://huggingface.co/mlx-community/Qwen3.6-35B-A3B-4bit/resolve/" - "38740b847e4cb78f352aba30aa41c76e08e6eb46/" - "video_preprocessor_config.json", - "video_preprocessor_config.json", true}, - {"https://huggingface.co/mlx-community/Qwen3.6-35B-A3B-4bit/resolve/" - "38740b847e4cb78f352aba30aa41c76e08e6eb46/" - "vocab.json", - "vocab.json", true}, -}; - -// mlx-community/Qwen3.8-27B-4bit (dense) — config.json model_type "qwen3_5", -// registered in mlx-swift-lm 3.31.5's LLMTypeRegistry. -constexpr CatalogFile kMlxQwen3_8_27BFiles[] = { - {"https://huggingface.co/mlx-community/Qwen3.8-27B-4bit/resolve/" - "3e6447f082e89cc7f0bc6e5441afd38dfce760ff/" - "chat_template.jinja", - "chat_template.jinja", true}, - {"https://huggingface.co/mlx-community/Qwen3.8-27B-4bit/resolve/" - "3e6447f082e89cc7f0bc6e5441afd38dfce760ff/" - "config.json", - "config.json", true}, - {"https://huggingface.co/mlx-community/Qwen3.8-27B-4bit/resolve/" - "3e6447f082e89cc7f0bc6e5441afd38dfce760ff/" - "generation_config.json", - "generation_config.json", true}, - {"https://huggingface.co/mlx-community/Qwen3.8-27B-4bit/resolve/" - "3e6447f082e89cc7f0bc6e5441afd38dfce760ff/" - "model-00001-of-00003.safetensors", - "model-00001-of-00003.safetensors", true, 5343268662LL}, - {"https://huggingface.co/mlx-community/Qwen3.8-27B-4bit/resolve/" - "3e6447f082e89cc7f0bc6e5441afd38dfce760ff/" - "model-00002-of-00003.safetensors", - "model-00002-of-00003.safetensors", true, 5354185130LL}, - {"https://huggingface.co/mlx-community/Qwen3.8-27B-4bit/resolve/" - "3e6447f082e89cc7f0bc6e5441afd38dfce760ff/" - "model-00003-of-00003.safetensors", - "model-00003-of-00003.safetensors", true, 5357087557LL}, - {"https://huggingface.co/mlx-community/Qwen3.8-27B-4bit/resolve/" - "3e6447f082e89cc7f0bc6e5441afd38dfce760ff/" - "model.safetensors.index.json", - "model.safetensors.index.json", true}, - {"https://huggingface.co/mlx-community/Qwen3.8-27B-4bit/resolve/" - "3e6447f082e89cc7f0bc6e5441afd38dfce760ff/" - "preprocessor_config.json", - "preprocessor_config.json", true}, - {"https://huggingface.co/mlx-community/Qwen3.8-27B-4bit/resolve/" - "3e6447f082e89cc7f0bc6e5441afd38dfce760ff/" - "processor_config.json", - "processor_config.json", true}, - {"https://huggingface.co/mlx-community/Qwen3.8-27B-4bit/resolve/" - "3e6447f082e89cc7f0bc6e5441afd38dfce760ff/" - "tokenizer.json", - "tokenizer.json", true}, - {"https://huggingface.co/mlx-community/Qwen3.8-27B-4bit/resolve/" - "3e6447f082e89cc7f0bc6e5441afd38dfce760ff/" - "tokenizer_config.json", - "tokenizer_config.json", true}, - {"https://huggingface.co/mlx-community/Qwen3.8-27B-4bit/resolve/" - "3e6447f082e89cc7f0bc6e5441afd38dfce760ff/" - "video_preprocessor_config.json", - "video_preprocessor_config.json", true}, - {"https://huggingface.co/mlx-community/Qwen3.8-27B-4bit/resolve/" - "3e6447f082e89cc7f0bc6e5441afd38dfce760ff/" - "vocab.json", - "vocab.json", true}, -}; - -// IBM Granite 4.1 MLX — config.json model_type "granite" (registered in -// mlx-swift-lm 3.31.5's LLMTypeRegistry). File set verified via HF API. -constexpr CatalogFile kMlxGranite4_1_3BFiles[] = { - {"https://huggingface.co/mlx-community/granite-4.1-3b-4bit/resolve/" - "b1b476b5a17c46b7d6cd663b4a8ed44b66720aef/" - "chat_template.jinja", - "chat_template.jinja", true}, - {"https://huggingface.co/mlx-community/granite-4.1-3b-4bit/resolve/" - "b1b476b5a17c46b7d6cd663b4a8ed44b66720aef/" - "config.json", - "config.json", true}, - {"https://huggingface.co/mlx-community/granite-4.1-3b-4bit/resolve/" - "b1b476b5a17c46b7d6cd663b4a8ed44b66720aef/" - "generation_config.json", - "generation_config.json", true}, - {"https://huggingface.co/mlx-community/granite-4.1-3b-4bit/resolve/" - "b1b476b5a17c46b7d6cd663b4a8ed44b66720aef/" - "model.safetensors", - "model.safetensors", true, 2127162429LL}, - {"https://huggingface.co/mlx-community/granite-4.1-3b-4bit/resolve/" - "b1b476b5a17c46b7d6cd663b4a8ed44b66720aef/" - "model.safetensors.index.json", - "model.safetensors.index.json", true}, - {"https://huggingface.co/mlx-community/granite-4.1-3b-4bit/resolve/" - "b1b476b5a17c46b7d6cd663b4a8ed44b66720aef/" - "tokenizer.json", - "tokenizer.json", true}, - {"https://huggingface.co/mlx-community/granite-4.1-3b-4bit/resolve/" - "b1b476b5a17c46b7d6cd663b4a8ed44b66720aef/" - "tokenizer_config.json", - "tokenizer_config.json", true}, -}; - -// mlx-community/granite-4.1-8b-4bit — NOT in the original ask (which assumed -// no clean official MLX 8B quant existed), but a real, official mlx-community -// repo does exist: Apache-2.0, base_model ibm-granite/granite-4.1-8b, -// model_type "granite" (registered). Verified via HF API this session; added -// for parity with the 3B/30B MLX rows below. -constexpr CatalogFile kMlxGranite4_1_8BFiles[] = { - {"https://huggingface.co/mlx-community/granite-4.1-8b-4bit/resolve/" - "08fb1e272f7bd49fa83ce279bbdc496c980380ac/" - "chat_template.jinja", - "chat_template.jinja", true}, - {"https://huggingface.co/mlx-community/granite-4.1-8b-4bit/resolve/" - "08fb1e272f7bd49fa83ce279bbdc496c980380ac/" - "config.json", - "config.json", true}, - {"https://huggingface.co/mlx-community/granite-4.1-8b-4bit/resolve/" - "08fb1e272f7bd49fa83ce279bbdc496c980380ac/" - "generation_config.json", - "generation_config.json", true}, - {"https://huggingface.co/mlx-community/granite-4.1-8b-4bit/resolve/" - "08fb1e272f7bd49fa83ce279bbdc496c980380ac/" - "model.safetensors", - "model.safetensors", true, 5238406779LL}, - {"https://huggingface.co/mlx-community/granite-4.1-8b-4bit/resolve/" - "08fb1e272f7bd49fa83ce279bbdc496c980380ac/" - "model.safetensors.index.json", - "model.safetensors.index.json", true}, - {"https://huggingface.co/mlx-community/granite-4.1-8b-4bit/resolve/" - "08fb1e272f7bd49fa83ce279bbdc496c980380ac/" - "tokenizer.json", - "tokenizer.json", true}, - {"https://huggingface.co/mlx-community/granite-4.1-8b-4bit/resolve/" - "08fb1e272f7bd49fa83ce279bbdc496c980380ac/" - "tokenizer_config.json", - "tokenizer_config.json", true}, -}; - -constexpr CatalogFile kMlxGranite4_1_30BFiles[] = { - {"https://huggingface.co/mlx-community/granite-4.1-30b-4bit/resolve/" - "03e8065d3219e525aa27fc4aaa9b375fe2cd6cb8/" - "chat_template.jinja", - "chat_template.jinja", true}, - {"https://huggingface.co/mlx-community/granite-4.1-30b-4bit/resolve/" - "03e8065d3219e525aa27fc4aaa9b375fe2cd6cb8/" - "config.json", - "config.json", true}, - {"https://huggingface.co/mlx-community/granite-4.1-30b-4bit/resolve/" - "03e8065d3219e525aa27fc4aaa9b375fe2cd6cb8/" - "generation_config.json", - "generation_config.json", true}, - {"https://huggingface.co/mlx-community/granite-4.1-30b-4bit/resolve/" - "03e8065d3219e525aa27fc4aaa9b375fe2cd6cb8/" - "model-00001-of-00004.safetensors", - "model-00001-of-00004.safetensors", true, 5360664833LL}, - {"https://huggingface.co/mlx-community/granite-4.1-30b-4bit/resolve/" - "03e8065d3219e525aa27fc4aaa9b375fe2cd6cb8/" - "model-00002-of-00004.safetensors", - "model-00002-of-00004.safetensors", true, 5363828231LL}, - {"https://huggingface.co/mlx-community/granite-4.1-30b-4bit/resolve/" - "03e8065d3219e525aa27fc4aaa9b375fe2cd6cb8/" - "model-00003-of-00004.safetensors", - "model-00003-of-00004.safetensors", true, 5363828281LL}, - {"https://huggingface.co/mlx-community/granite-4.1-30b-4bit/resolve/" - "03e8065d3219e525aa27fc4aaa9b375fe2cd6cb8/" - "model-00004-of-00004.safetensors", - "model-00004-of-00004.safetensors", true, 1953655228LL}, - {"https://huggingface.co/mlx-community/granite-4.1-30b-4bit/resolve/" - "03e8065d3219e525aa27fc4aaa9b375fe2cd6cb8/" - "model.safetensors.index.json", - "model.safetensors.index.json", true}, - {"https://huggingface.co/mlx-community/granite-4.1-30b-4bit/resolve/" - "03e8065d3219e525aa27fc4aaa9b375fe2cd6cb8/" - "tokenizer.json", - "tokenizer.json", true}, - {"https://huggingface.co/mlx-community/granite-4.1-30b-4bit/resolve/" - "03e8065d3219e525aa27fc4aaa9b375fe2cd6cb8/" - "tokenizer_config.json", - "tokenizer_config.json", true}, -}; - -// Supertone Supertonic v3 TTS via Sherpa-ONNX. NOT the raw Supertone/ -// supertonic-3 HF repo — its JSON voice styles / unicode indexer do not match -// what sherpa-onnx's OfflineTtsSupertonicModelConfig loader expects (a binary -// voice.bin + unicode_indexer.bin). This is the pre-converted bundle whose -// 7 filenames match that loader's fields 1:1 (see -// sherpa-onnx/csrc/offline-tts-supertonic-model-config.h). MIT license. -// Requires sherpa-onnx >= 1.13.2 (pinned SHERPA_ONNX_VERSION_* in -// core/VERSIONS already satisfies this). -constexpr CatalogFile kSherpaSupertonicV3Files[] = { - {"https://huggingface.co/csukuangfj2/" - "sherpa-onnx-supertonic-3-tts-int8-2026-05-11/resolve/" - "cca5a0e6c96e1d2c720986bf7e75fcc81dee3ae4/" - "duration_predictor.int8.onnx", - "duration_predictor.int8.onnx", true, 3700147LL}, - {"https://huggingface.co/csukuangfj2/" - "sherpa-onnx-supertonic-3-tts-int8-2026-05-11/resolve/" - "cca5a0e6c96e1d2c720986bf7e75fcc81dee3ae4/" - "text_encoder.int8.onnx", - "text_encoder.int8.onnx", true, 36416150LL}, - {"https://huggingface.co/csukuangfj2/" - "sherpa-onnx-supertonic-3-tts-int8-2026-05-11/resolve/" - "cca5a0e6c96e1d2c720986bf7e75fcc81dee3ae4/tts.json", - "tts.json", true, 8253LL}, - {"https://huggingface.co/csukuangfj2/" - "sherpa-onnx-supertonic-3-tts-int8-2026-05-11/resolve/" - "cca5a0e6c96e1d2c720986bf7e75fcc81dee3ae4/" - "unicode_indexer.bin", - "unicode_indexer.bin", true, 262144LL}, - {"https://huggingface.co/csukuangfj2/" - "sherpa-onnx-supertonic-3-tts-int8-2026-05-11/resolve/" - "cca5a0e6c96e1d2c720986bf7e75fcc81dee3ae4/" - "vector_estimator.int8.onnx", - "vector_estimator.int8.onnx", true, 78400833LL}, - {"https://huggingface.co/csukuangfj2/" - "sherpa-onnx-supertonic-3-tts-int8-2026-05-11/resolve/" - "cca5a0e6c96e1d2c720986bf7e75fcc81dee3ae4/" - "vocoder.int8.onnx", - "vocoder.int8.onnx", true, 25991073LL}, - {"https://huggingface.co/csukuangfj2/" - "sherpa-onnx-supertonic-3-tts-int8-2026-05-11/resolve/" - "cca5a0e6c96e1d2c720986bf7e75fcc81dee3ae4/voice.bin", - "voice.bin", true, 517168LL}, -}; - -constexpr int64_t MB = 1024LL * 1024LL; - -// ids/URLs verbatim from the consumer apps (RunanywhereAI/runanywhere-{ios, -// android,web}: ModelCatalogBootstrap.swift, ModelCatalog.kt, model-catalog.ts) -// and tests/scripts/download-test-models.sh (qwen3-0.6b Q8_0 matches the Linux -// test rig's LlamaCpp/qwen3-0.6b layout). -constexpr CatalogEntry kCatalog[] = { - // --- LLM (LlamaCpp / GGUF) --- - {"qwen3-0.6b", "qwen3", "Qwen3 0.6B Q8_0", v1::MODEL_CATEGORY_LANGUAGE, - v1::INFERENCE_FRAMEWORK_LLAMA_CPP, v1::MODEL_FORMAT_GGUF, - "https://huggingface.co/Qwen/Qwen3-0.6B-GGUF/resolve/main/" - "Qwen3-0.6B-Q8_0.gguf", - nullptr, 0, 639 * MB, 4096, true}, - {"qwen3-1.7b-q4_k_m", "qwen3-1.7b", "Qwen3 1.7B Q4_K_M", - v1::MODEL_CATEGORY_LANGUAGE, v1::INFERENCE_FRAMEWORK_LLAMA_CPP, - v1::MODEL_FORMAT_GGUF, - "https://huggingface.co/unsloth/Qwen3-1.7B-GGUF/resolve/main/" - "Qwen3-1.7B-Q4_K_M.gguf", - nullptr, 0, 1230 * MB, 4096, true}, - {"qwen3-4b-q4_k_m", "qwen3-4b", "Qwen3 4B Q4_K_M", - v1::MODEL_CATEGORY_LANGUAGE, v1::INFERENCE_FRAMEWORK_LLAMA_CPP, - v1::MODEL_FORMAT_GGUF, - "https://huggingface.co/unsloth/Qwen3-4B-GGUF/resolve/main/" - "Qwen3-4B-Q4_K_M.gguf", - nullptr, 0, 2560 * MB, 4096, true}, - // RunAnywhere's canonical-based llama.cpp fork supports PrismML's Q1_0 - // Bonsai artifacts. Ternary-Bonsai uses the explicitly canonical - // Q2_0_g64 artifacts below; legacy 128-value Q2_0 remains unsupported. - // Exact artifact byte sizes. - {"bonsai-1.7b-q1_0", "bonsai-1.7b", "Bonsai-1.7B 1-bit Q1_0 (CPU)", - v1::MODEL_CATEGORY_LANGUAGE, v1::INFERENCE_FRAMEWORK_LLAMA_CPP, - v1::MODEL_FORMAT_GGUF, - "https://huggingface.co/prism-ml/Bonsai-1.7B-gguf/resolve/main/" - "Bonsai-1.7B-Q1_0.gguf", - nullptr, 0, 248302272LL, 4096, true}, - {"bonsai-4b-q1_0", "bonsai-4b", "Bonsai-4B 1-bit Q1_0 (CPU)", - v1::MODEL_CATEGORY_LANGUAGE, v1::INFERENCE_FRAMEWORK_LLAMA_CPP, - v1::MODEL_FORMAT_GGUF, - "https://huggingface.co/prism-ml/Bonsai-4B-gguf/resolve/main/" - "Bonsai-4B-Q1_0.gguf", - nullptr, 0, 572270624LL, 4096, true}, - {"bonsai-8b-q1_0", "bonsai-8b", "Bonsai-8B 1-bit Q1_0 (CPU)", - v1::MODEL_CATEGORY_LANGUAGE, v1::INFERENCE_FRAMEWORK_LLAMA_CPP, - v1::MODEL_FORMAT_GGUF, - "https://huggingface.co/prism-ml/Bonsai-8B-gguf/resolve/main/" - "Bonsai-8B-Q1_0.gguf", - nullptr, 0, 1158654496LL, 4096, true}, - {"bonsai-27b-q1_0", "bonsai-27b", "Bonsai-27B 1-bit Q1_0 (CPU)", - v1::MODEL_CATEGORY_LANGUAGE, v1::INFERENCE_FRAMEWORK_LLAMA_CPP, - v1::MODEL_FORMAT_GGUF, - "https://huggingface.co/prism-ml/Bonsai-27B-gguf/resolve/main/" - "Bonsai-27B-Q1_0.gguf", - nullptr, 0, 3803452480LL, 4096, true}, - {"ternary-bonsai-1.7b-q2_0-g64", "ternary-bonsai-1.7b", - "Ternary-Bonsai-1.7B Q2_0 g64", v1::MODEL_CATEGORY_LANGUAGE, - v1::INFERENCE_FRAMEWORK_LLAMA_CPP, v1::MODEL_FORMAT_GGUF, - "https://huggingface.co/prism-ml/Ternary-Bonsai-1.7B-gguf/resolve/" - "983b5dec2ff16aab79990711ba0f828a499a7e6a/" - "Ternary-Bonsai-1.7B-Q2_0_g64.gguf", - nullptr, 0, 490163968LL, 4096, true}, - {"ternary-bonsai-4b-q2_0-g64", "ternary-bonsai-4b", - "Ternary-Bonsai-4B Q2_0 g64", v1::MODEL_CATEGORY_LANGUAGE, - v1::INFERENCE_FRAMEWORK_LLAMA_CPP, v1::MODEL_FORMAT_GGUF, - "https://huggingface.co/prism-ml/Ternary-Bonsai-4B-gguf/resolve/" - "a3eb42bafe873f9686bc97486c43b72ef7d75ec8/" - "Ternary-Bonsai-4B-Q2_0_g64.gguf", - nullptr, 0, 1137806656LL, 4096, true}, - {"ternary-bonsai-8b-q2_0-g64", "ternary-bonsai-8b", - "Ternary-Bonsai-8B Q2_0 g64", v1::MODEL_CATEGORY_LANGUAGE, - v1::INFERENCE_FRAMEWORK_LLAMA_CPP, v1::MODEL_FORMAT_GGUF, - "https://huggingface.co/prism-ml/Ternary-Bonsai-8B-gguf/resolve/" - "c2aefbeb4b24469cd11579c3384b990404c17a30/" - "Ternary-Bonsai-8B-Q2_0_g64.gguf", - nullptr, 0, 2310125920LL, 4096, true}, - {"maple-preview-tq1_0-q4_k", "maple-preview", - "DeepGrove Maple Preview TQ1_0 + Q4_K head (CPU)", - v1::MODEL_CATEGORY_LANGUAGE, v1::INFERENCE_FRAMEWORK_LLAMA_CPP, - v1::MODEL_FORMAT_GGUF, - "https://huggingface.co/deepgrove/maple-preview-GGUF/resolve/" - "f5466f918e0c50cdb9d4d47a6f35813509a42a30/" - "maple-preview-TQ1_0-head-Q4_K.gguf", - nullptr, 0, 4984016416LL, 4096, true}, - {"llama-3.2-3b", "llama3.2", "Llama 3.2 3B Instruct Q4_K_M", - v1::MODEL_CATEGORY_LANGUAGE, v1::INFERENCE_FRAMEWORK_LLAMA_CPP, - v1::MODEL_FORMAT_GGUF, - "https://huggingface.co/bartowski/Llama-3.2-3B-Instruct-GGUF/resolve/main/" - "Llama-3.2-3B-Instruct-Q4_K_M.gguf", - nullptr, 0, 2020 * MB, 0, false}, - {"lfm2-350m-q8_0", "lfm2", "LiquidAI LFM2 350M Q8_0", - v1::MODEL_CATEGORY_LANGUAGE, v1::INFERENCE_FRAMEWORK_LLAMA_CPP, - v1::MODEL_FORMAT_GGUF, - "https://huggingface.co/LiquidAI/LFM2-350M-GGUF/resolve/main/" - "LFM2-350M-Q8_0.gguf", - nullptr, 0, 400 * MB, 2048, false}, - {"smollm2-360m-q8_0", "smollm2", "SmolLM2 360M Q8_0", - v1::MODEL_CATEGORY_LANGUAGE, v1::INFERENCE_FRAMEWORK_LLAMA_CPP, - v1::MODEL_FORMAT_GGUF, - "https://huggingface.co/prithivMLmods/SmolLM2-360M-GGUF/resolve/main/" - "SmolLM2-360M.Q8_0.gguf", - nullptr, 0, 386 * MB, 2048, false}, - - // Google Gemma 4 family (GGUF). Licensed under Apache 2.0; preserve the - // upstream license and attribution notices when redistributing. - {"gemma-4-e2b-it-q4_k_m", "gemma4-e2b", "Gemma 4 E2B IT Q4_K_M", - v1::MODEL_CATEGORY_LANGUAGE, v1::INFERENCE_FRAMEWORK_LLAMA_CPP, - v1::MODEL_FORMAT_GGUF, - "https://huggingface.co/unsloth/gemma-4-E2B-it-GGUF/resolve/" - "0314792d7f1f7e229411f620751375812bb9faf2/" - "gemma-4-E2B-it-Q4_K_M.gguf", - nullptr, 0, 3106738272LL, 4096, false}, - {"gemma-4-e4b-it-q4_k_m", "gemma4-e4b", "Gemma 4 E4B IT Q4_K_M", - v1::MODEL_CATEGORY_LANGUAGE, v1::INFERENCE_FRAMEWORK_LLAMA_CPP, - v1::MODEL_FORMAT_GGUF, - "https://huggingface.co/unsloth/gemma-4-E4B-it-GGUF/resolve/" - "bfc15c382204943c3a8fff0c750b94ae2364d7a3/" - "gemma-4-E4B-it-Q4_K_M.gguf", - nullptr, 0, 4977171584LL, 4096, false}, - {"gemma-4-12b-it-q4_k_m", "gemma4-12b", "Gemma 4 12B IT Q4_K_M", - v1::MODEL_CATEGORY_LANGUAGE, v1::INFERENCE_FRAMEWORK_LLAMA_CPP, - v1::MODEL_FORMAT_GGUF, - "https://huggingface.co/unsloth/gemma-4-12b-it-GGUF/resolve/" - "fc034cfff751157913579611efad8462ac1be606/" - "gemma-4-12b-it-Q4_K_M.gguf", - nullptr, 0, 7121861440LL, 4096, false}, - {"gemma-4-26b-a4b-it-q4_k_xl", "gemma4-26b-a4b", - "Gemma 4 26B-A4B IT UD-Q4_K_XL (MoE)", v1::MODEL_CATEGORY_LANGUAGE, - v1::INFERENCE_FRAMEWORK_LLAMA_CPP, v1::MODEL_FORMAT_GGUF, - "https://huggingface.co/unsloth/gemma-4-26B-A4B-it-GGUF/resolve/" - "c099eb48e663fd284577b04978a94ffccb261841/" - "gemma-4-26B-A4B-it-UD-Q4_K_XL.gguf", - nullptr, 0, 17010980576LL, 4096, false}, - {"gemma-4-31b-it-q4_k_m", "gemma4-31b", "Gemma 4 31B IT Q4_K_M", - v1::MODEL_CATEGORY_LANGUAGE, v1::INFERENCE_FRAMEWORK_LLAMA_CPP, - v1::MODEL_FORMAT_GGUF, - "https://huggingface.co/unsloth/gemma-4-31B-it-GGUF/resolve/" - "c1ac76e99d5513b141e8adde7288b85c3f9c32ec/" - "gemma-4-31B-it-Q4_K_M.gguf", - nullptr, 0, 18323733440LL, 4096, false}, - // Smaller quant of the same 31B model for tighter RAM budgets. - {"gemma-4-31b-it-ud-q2_k_xl", "gemma4-31b-q2", "Gemma 4 31B IT UD-Q2_K_XL", - v1::MODEL_CATEGORY_LANGUAGE, v1::INFERENCE_FRAMEWORK_LLAMA_CPP, - v1::MODEL_FORMAT_GGUF, - "https://huggingface.co/unsloth/gemma-4-31B-it-GGUF/resolve/" - "c1ac76e99d5513b141e8adde7288b85c3f9c32ec/" - "gemma-4-31B-it-UD-Q2_K_XL.gguf", - nullptr, 0, 11774991296LL, 4096, false}, - - // Qwen3.6-35B-A3B (MoE, agentic-coding, Apache 2.0). - {"qwen3.6-35b-a3b-q4_k_m", "qwen3.6-35b", "Qwen3.6 35B-A3B UD-Q4_K_M (MoE)", - v1::MODEL_CATEGORY_LANGUAGE, v1::INFERENCE_FRAMEWORK_LLAMA_CPP, - v1::MODEL_FORMAT_GGUF, - "https://huggingface.co/unsloth/Qwen3.6-35B-A3B-GGUF/resolve/" - "a483e9e6cbd595906af30beda3187c2663a1118c/" - "Qwen3.6-35B-A3B-UD-Q4_K_M.gguf", - nullptr, 0, 22134528992LL, 4096, true}, - // Qwen3.8-27B (dense, newest Qwen, Apache 2.0). - {"qwen3.8-27b-q4_k_m", "qwen3.8-27b", "Qwen3.8 27B Q4_K_M", - v1::MODEL_CATEGORY_LANGUAGE, v1::INFERENCE_FRAMEWORK_LLAMA_CPP, - v1::MODEL_FORMAT_GGUF, - "https://huggingface.co/unsloth/Qwen3.8-27B-GGUF/resolve/" - "f1bfb127c64f7072bdd2cad55f258b9c8b2910fe/" - "Qwen3.8-27B-Q4_K_M.gguf", - nullptr, 0, 17106775008LL, 4096, true}, - - // IBM Granite 4.1 family (Apache 2.0). - {"granite-4.1-3b-q4_k_m", "granite4.1-3b", "IBM Granite 4.1 3B Q4_K_M", - v1::MODEL_CATEGORY_LANGUAGE, v1::INFERENCE_FRAMEWORK_LLAMA_CPP, - v1::MODEL_FORMAT_GGUF, - "https://huggingface.co/unsloth/granite-4.1-3b-GGUF/resolve/" - "5b88826e4b80789548180f8faab39c5cf68772c9/" - "granite-4.1-3b-Q4_K_M.gguf", - nullptr, 0, 2099502400LL, 4096, false}, - {"granite-4.1-8b-q4_k_m", "granite4.1-8b", "IBM Granite 4.1 8B Q4_K_M", - v1::MODEL_CATEGORY_LANGUAGE, v1::INFERENCE_FRAMEWORK_LLAMA_CPP, - v1::MODEL_FORMAT_GGUF, - "https://huggingface.co/unsloth/granite-4.1-8b-GGUF/resolve/" - "6f9671f73eb03273bc09319194b8a4e810e03a8f/" - "granite-4.1-8b-Q4_K_M.gguf", - nullptr, 0, 5347915136LL, 4096, false}, - {"granite-4.1-30b-q4_k_m", "granite4.1-30b", "IBM Granite 4.1 30B Q4_K_M", - v1::MODEL_CATEGORY_LANGUAGE, v1::INFERENCE_FRAMEWORK_LLAMA_CPP, - v1::MODEL_FORMAT_GGUF, - "https://huggingface.co/unsloth/granite-4.1-30b-GGUF/resolve/" - "6cb34f31b11ca4c1433de1af7391dac46de4e666/" - "granite-4.1-30b-Q4_K_M.gguf", - nullptr, 0, 17490241472LL, 4096, false}, - - // --- VLM (gguf + mmproj pairs) --- - {"smolvlm2-256m-video-instruct-q8_0", "smolvlm2", - "SmolVLM2 256M Video Instruct Q8_0", v1::MODEL_CATEGORY_MULTIMODAL, - v1::INFERENCE_FRAMEWORK_LLAMA_CPP, v1::MODEL_FORMAT_GGUF, nullptr, - kSmolVlm2Files, 2, 420 * MB, 2048, false}, - {"lfm2-vl-450m-q8_0", "lfm2-vl", "LFM2-VL 450M Q8_0", - v1::MODEL_CATEGORY_MULTIMODAL, v1::INFERENCE_FRAMEWORK_LLAMA_CPP, - v1::MODEL_FORMAT_GGUF, nullptr, kLfm2VlFiles, 2, 600 * MB, 0, false}, - // Native window is 128k (lfm2.context_length in the GGUF); 4096 is the - // on-device working context, matching the other multi-GB VLM row. - {"lfm2.5-vl-3b-q4_k_m", "lfm2.5-vl", "LFM2.5-VL 3B Q4_K_M", - v1::MODEL_CATEGORY_MULTIMODAL, v1::INFERENCE_FRAMEWORK_LLAMA_CPP, - v1::MODEL_FORMAT_GGUF, nullptr, kLfm2_5Vl3BFiles, 2, 2257563360LL, 4096, - false}, - {"qwen2-vl-2b-instruct-q4_k_m", "qwen2-vl", "Qwen2-VL 2B Instruct Q4_K_M", - v1::MODEL_CATEGORY_MULTIMODAL, v1::INFERENCE_FRAMEWORK_LLAMA_CPP, - v1::MODEL_FORMAT_GGUF, nullptr, kQwen2VlFiles, 2, 1800 * MB, 2048, false}, - {"fara1.5-4b-q4_k_m", "fara", "Fara1.5 4B Computer-Use Agent Q4_K_M", - v1::MODEL_CATEGORY_MULTIMODAL, v1::INFERENCE_FRAMEWORK_LLAMA_CPP, - v1::MODEL_FORMAT_GGUF, nullptr, kFara15GgufFiles, 2, 3300 * MB, 4096, - false, - /*memory_required_bytes*/ 0, /*cua_profile*/ "fara"}, - // Meta Muse Glimmer 30B (Apache 2.0). llama.cpp's mmproj is image-only — - // vision-capable, not the checkpoint's marketed audio/video "omni" surface. - {"muse-glimmer-30b-q4_k_xl", "muse-glimmer", "Muse Glimmer 30B UD-Q4_K_XL", - v1::MODEL_CATEGORY_MULTIMODAL, v1::INFERENCE_FRAMEWORK_LLAMA_CPP, - v1::MODEL_FORMAT_GGUF, nullptr, kMuseGlimmer30BFiles, 2, 17929907456LL, - 4096, false}, - // NVIDIA Nemotron-3-Nano-Omni-30B-A3B-Reasoning (MoE, NVIDIA Open Model - // License). Same image-only mmproj caveat as Muse Glimmer above. - {"nemotron-3-nano-omni-30b-a3b-reasoning-q4_k_m", "nemotron-omni", - "NVIDIA Nemotron-3-Nano-Omni 30B-A3B Reasoning UD-Q4_K_M (vision, MoE)", - v1::MODEL_CATEGORY_MULTIMODAL, v1::INFERENCE_FRAMEWORK_LLAMA_CPP, - v1::MODEL_FORMAT_GGUF, nullptr, kNemotronOmniReasoningFiles, 2, - 25474563776LL, 4096, true}, - - // --- Speech (Sherpa-ONNX archives; orchestrator extracts in-core) --- - {"sherpa-onnx-whisper-tiny.en", "whisper-tiny", - "Whisper Tiny English (Sherpa-ONNX)", - v1::MODEL_CATEGORY_SPEECH_RECOGNITION, v1::INFERENCE_FRAMEWORK_SHERPA, - v1::MODEL_FORMAT_ONNX, - "https://github.com/RunanywhereAI/sherpa-onnx/releases/download/" - "runanywhere-models-v1/" - "sherpa-onnx-whisper-tiny.en.tar.gz", - nullptr, 0, 75 * MB, 0, false}, - {"sherpa-nemo-parakeet-tdt-0.6b-v2-int8", "parakeet-tdt-v2", - "NVIDIA Parakeet TDT 0.6B v2 INT8 (Sherpa-ONNX)", - v1::MODEL_CATEGORY_SPEECH_RECOGNITION, v1::INFERENCE_FRAMEWORK_SHERPA, - v1::MODEL_FORMAT_ONNX, nullptr, kSherpaParakeetTdtV2Files, 4, 661190513LL, - 0, false}, - {"sherpa-nemo-parakeet-tdt-0.6b-v3-int8", "parakeet-tdt-v3", - "NVIDIA Parakeet TDT 0.6B v3 INT8 (Sherpa-ONNX)", - v1::MODEL_CATEGORY_SPEECH_RECOGNITION, v1::INFERENCE_FRAMEWORK_SHERPA, - v1::MODEL_FORMAT_ONNX, nullptr, kSherpaParakeetTdtV3Files, 4, 670478772LL, - 0, false}, - {"sherpa-nemo-parakeet-ctc-1.1b-int8", "parakeet-ctc", - "NVIDIA Parakeet CTC 1.1B INT8 (Sherpa-ONNX)", - v1::MODEL_CATEGORY_SPEECH_RECOGNITION, v1::INFERENCE_FRAMEWORK_SHERPA, - v1::MODEL_FORMAT_ONNX, nullptr, kSherpaParakeetCtcFiles, 2, 1110024519LL, - 0, false, 2147483648LL}, - {"sherpa-nemo-canary-180m-flash-int8", "canary-180m", - "NVIDIA Canary 180M Flash INT8 (Sherpa-ONNX)", - v1::MODEL_CATEGORY_SPEECH_RECOGNITION, v1::INFERENCE_FRAMEWORK_SHERPA, - v1::MODEL_FORMAT_ONNX, nullptr, kSherpaCanary180MFiles, 3, 207170046LL, 0, - false}, - {"sherpa-nemotron-3.5-asr-streaming-0.6b-320ms-int8", - "nemotron-asr-streaming", - "NVIDIA Nemotron 3.5 Streaming ASR 0.6B 320ms INT8 (Sherpa-ONNX)", - v1::MODEL_CATEGORY_SPEECH_RECOGNITION, v1::INFERENCE_FRAMEWORK_SHERPA, - v1::MODEL_FORMAT_ONNX, nullptr, kSherpaNemotronStreamingAsrFiles, 4, - 682215471LL, 0, false}, - {"vits-piper-en_US-lessac-medium", "piper", - "Piper TTS US English (Lessac Medium)", - v1::MODEL_CATEGORY_SPEECH_SYNTHESIS, v1::INFERENCE_FRAMEWORK_SHERPA, - v1::MODEL_FORMAT_ONNX, - "https://github.com/RunanywhereAI/sherpa-onnx/releases/download/" - "runanywhere-models-v1/" - "vits-piper-en_US-lessac-medium.tar.gz", - nullptr, 0, 65 * MB, 0, false}, - // Supertone Supertonic v3 (MIT). Not the raw Supertone/supertonic-3 repo — - // see kSherpaSupertonicV3Files for why. Needs sherpa-onnx >= 1.13.2. - {"sherpa-supertonic-3-tts-int8", "supertonic", - "Supertone Supertonic v3 TTS INT8 (Sherpa-ONNX)", - v1::MODEL_CATEGORY_SPEECH_SYNTHESIS, v1::INFERENCE_FRAMEWORK_SHERPA, - v1::MODEL_FORMAT_ONNX, nullptr, kSherpaSupertonicV3Files, 7, 145295768LL, - 0, false}, - - // --- VAD --- - // Exact artifact size (matches iOS ModelCatalogBootstrap.swift): the - // post-finalize size guard treats download_size_bytes as authoritative, - // and an over-stated 3 MB estimate tripped it on the valid ~2.3 MB file. - {"silero-vad", "silero", "Silero VAD", - v1::MODEL_CATEGORY_VOICE_ACTIVITY_DETECTION, v1::INFERENCE_FRAMEWORK_ONNX, - v1::MODEL_FORMAT_ONNX, - "https://github.com/snakers4/silero-vad/raw/master/src/silero_vad/data/" - "silero_vad.onnx", - nullptr, 0, 2327524, 0, false}, - - // --- Speaker diarization (ONNX Runtime) --- - {"diar-streaming-sortformer-4spk-v2.1", "sortformer", - "NVIDIA Streaming Sortformer 4-Speaker v2.1", - v1::MODEL_CATEGORY_SPEAKER_DIARIZATION, v1::INFERENCE_FRAMEWORK_ONNX, - v1::MODEL_FORMAT_ONNX, - "https://huggingface.co/cgus/diar_streaming_sortformer_4spk-v2.1-onnx/" - "resolve/main/diar_streaming_sortformer_4spk-v2.1.onnx", - nullptr, 0, 492242946LL, 0, false}, - - // --- Semantic segmentation (ONNX Runtime) --- - {"segformer-b0-ade-512", "segformer", - "SegFormer B0 ADE20K 512 (Semantic Segmentation)", - v1::MODEL_CATEGORY_SEMANTIC_SEGMENTATION, v1::INFERENCE_FRAMEWORK_ONNX, - v1::MODEL_FORMAT_ONNX, - "https://huggingface.co/Xenova/segformer-b0-finetuned-ade-512-512/" - "resolve/main/onnx/model.onnx", - nullptr, 0, 15335446LL, 0, false}, - - // --- Embeddings --- - {"nemotron-3-embed-1b-q4_k_m", "nemotron-3-embed", - "NVIDIA Nemotron 3 Embed 1B Q4_K_M", v1::MODEL_CATEGORY_EMBEDDING, - v1::INFERENCE_FRAMEWORK_LLAMA_CPP, v1::MODEL_FORMAT_GGUF, - "https://huggingface.co/zenmagnets/" - "Nemotron-3-Embed-1B-Q4_K_M-GGUF/resolve/" - "06df1fde6f7009c91f6cc3cd520081921929a678/" - "nemotron-3-embed-1b-q4_k_m.gguf", - nullptr, 0, 749352096LL, 0, false}, - {"llama-nemotron-embed-1b-v2-q4_k_m", "llama-nemotron-embed", - "NVIDIA Llama Nemotron Embed 1B v2 Q4_K_M", v1::MODEL_CATEGORY_EMBEDDING, - v1::INFERENCE_FRAMEWORK_LLAMA_CPP, v1::MODEL_FORMAT_GGUF, - "https://huggingface.co/mykor/llama-nemotron-embed-1b-v2-GGUF/" - "resolve/bf7c9832b1d76f86777379e58b7b74805ee58006/" - "llama-nemotron-embed-1B-v2-Q4_K_M.gguf", - nullptr, 0, 807690624LL, 0, false}, - // NVIDIA Llama Embed Nemotron 8B — portable GGUF previously HNPU-only. - {"llama-embed-nemotron-8b-q4_k_m", "llama-embed-nemotron", - "NVIDIA Llama Embed Nemotron 8B Q4_K_M", v1::MODEL_CATEGORY_EMBEDDING, - v1::INFERENCE_FRAMEWORK_LLAMA_CPP, v1::MODEL_FORMAT_GGUF, - "https://huggingface.co/mradermacher/llama-embed-nemotron-8b-GGUF/" - "resolve/e7ae3cbae4f7693bbd75ec959bf293f39e1f2e25/" - "llama-embed-nemotron-8b.Q4_K_M.gguf", - nullptr, 0, 4625233184LL, 0, false}, - {"all-minilm-l6-v2", "minilm", "All-MiniLM-L6-v2 (Embeddings)", - v1::MODEL_CATEGORY_EMBEDDING, v1::INFERENCE_FRAMEWORK_ONNX, - v1::MODEL_FORMAT_ONNX, nullptr, kMiniLmFiles, 2, 90 * MB, 0, false}, - - // --- Reranking (llama.cpp cross-encoder; `rcli rerank -m `) --- - {"bge-reranker-v2-m3-q4_k_m", "bge-reranker", - "BGE Reranker v2-m3 Q4_K_M (Reranking)", v1::MODEL_CATEGORY_EMBEDDING, - v1::INFERENCE_FRAMEWORK_LLAMA_CPP, v1::MODEL_FORMAT_GGUF, - "https://huggingface.co/gpustack/bge-reranker-v2-m3-GGUF/resolve/main/" - "bge-reranker-v2-m3-Q4_K_M.gguf", - nullptr, 0, 438376864LL, 0, false}, - - // --- Image generation (CoreML diffusion; Apple only) --- - // Apple-optimized Stable Diffusion 1.5. Id matches the built-in diffusion - // model registry (diffusion_model_registry.cpp) and the Swift facade's - // canonical `.imageGeneration` model, so `rcli image generate` resolves it - // and `rcli list` shows it. The palettized CoreML bundle is a directory of - // compiled .mlmodelc sub-models served by the `coreml` engine; a - // pre-fetched bundle can also be passed to `--model` as a local path. - {"stable-diffusion-v1-5-coreml", "sd15", "Stable Diffusion 1.5 (CoreML)", - v1::MODEL_CATEGORY_IMAGE_GENERATION, v1::INFERENCE_FRAMEWORK_COREML, - v1::MODEL_FORMAT_MLPACKAGE, - "https://huggingface.co/apple/coreml-stable-diffusion-v1-5-palettized", - nullptr, 0, 1200 * MB, 0, false}, - - // --- MLX (Apple Silicon / Apple GPU via mlx-swift-lm) --- - {"mlx-qwen3-0.6b-4bit", "mlx-qwen3", "Qwen3 0.6B 4-bit (MLX)", - v1::MODEL_CATEGORY_LANGUAGE, v1::INFERENCE_FRAMEWORK_MLX, - v1::MODEL_FORMAT_SAFETENSORS, nullptr, kMlxQwen3_06BFiles, 9, 351383618, - 4096, true}, - {"mlx-maple-preview-2bit", "mlx-maple-preview", - "DeepGrove Maple Preview 2-bit (MLX)", v1::MODEL_CATEGORY_LANGUAGE, - v1::INFERENCE_FRAMEWORK_MLX, v1::MODEL_FORMAT_SAFETENSORS, nullptr, - kMlxMaplePreviewFiles, 13, 5330252282LL, 128000, true}, - {"mlx-llama-3.1-nemotron-nano-8b-v1-4bit", "mlx-nemotron-nano", - "NVIDIA Llama 3.1 Nemotron Nano 8B 4-bit (MLX)", - v1::MODEL_CATEGORY_LANGUAGE, v1::INFERENCE_FRAMEWORK_MLX, - v1::MODEL_FORMAT_SAFETENSORS, nullptr, kMlxNemotronNano8BFiles, 8, - 4534806075LL, 131072, false}, - {"mlx-nemotron-mini-4b-instruct-4bit", "mlx-nemotron-mini", - "NVIDIA Nemotron Mini 4B Instruct 4-bit (MLX)", - v1::MODEL_CATEGORY_LANGUAGE, v1::INFERENCE_FRAMEWORK_MLX, - v1::MODEL_FORMAT_SAFETENSORS, nullptr, kMlxNemotronMini4BFiles, 6, - 2392679103LL, 4096, false}, - // PrismML Bonsai family 1-bit MLX. Needs the narrow Prism kernels carried - // by the canonical-first RunAnywhere MLX/mlx-swift forks pinned in the - // Swift manifests and resolved files. - {"mlx-bonsai-1.7b-1bit", "mlx-bonsai-1.7b", "MLX Bonsai-1.7B 1-bit", - v1::MODEL_CATEGORY_LANGUAGE, v1::INFERENCE_FRAMEWORK_MLX, - v1::MODEL_FORMAT_SAFETENSORS, nullptr, kMlxBonsai1_7B1BitFiles, 8, - 269060904LL, 4096, true}, - {"mlx-bonsai-4b-1bit", "mlx-bonsai-4b", "MLX Bonsai-4B 1-bit", - v1::MODEL_CATEGORY_LANGUAGE, v1::INFERENCE_FRAMEWORK_MLX, - v1::MODEL_FORMAT_SAFETENSORS, nullptr, kMlxBonsai4B1BitFiles, 8, - 628865840LL, 4096, true}, - {"mlx-bonsai-8b-1bit", "mlx-bonsai-8b", "MLX Bonsai-8B 1-bit", - v1::MODEL_CATEGORY_LANGUAGE, v1::INFERENCE_FRAMEWORK_MLX, - v1::MODEL_FORMAT_SAFETENSORS, nullptr, kMlxBonsai8B1BitFiles, 8, - 1280131424LL, 4096, true}, - // PrismML Bonsai-27B 1-bit MLX (~5.1 GB safetensors). Experimental — - // requires mlx-swift-lm support for qwen3_5 / 1-bit Bonsai. - {"mlx-bonsai-27b-1bit", "mlx-bonsai", "MLX Bonsai-27B 1-bit", - v1::MODEL_CATEGORY_LANGUAGE, v1::INFERENCE_FRAMEWORK_MLX, - v1::MODEL_FORMAT_SAFETENSORS, nullptr, kMlxBonsai27B1BitFiles, 8, - 5129115752LL, 4096, true}, - // PrismML Ternary-Bonsai family at ternary/2-bit MLX. bits=2 was already - // supported by upstream MLX 0.31.6 before the Prism 1-bit patch, so this - // needs no additional fork support beyond what Bonsai (above) needs. - // Verified this session: loaded + generated correctly via the app's - // Add-from-URL flow (Ternary-Bonsai-1.7B, 64 tok/s, no crash). - {"mlx-ternary-bonsai-1.7b-2bit", "mlx-ternary-bonsai-1.7b", - "MLX Ternary-Bonsai-1.7B 2-bit", v1::MODEL_CATEGORY_LANGUAGE, - v1::INFERENCE_FRAMEWORK_MLX, v1::MODEL_FORMAT_SAFETENSORS, nullptr, - kMlxTernaryBonsai1_7B2BitFiles, 6, 484049216LL, 4096, true}, - {"mlx-ternary-bonsai-4b-2bit", "mlx-ternary-bonsai-4b", - "MLX Ternary-Bonsai-4B 2-bit", v1::MODEL_CATEGORY_LANGUAGE, - v1::INFERENCE_FRAMEWORK_MLX, v1::MODEL_FORMAT_SAFETENSORS, nullptr, - kMlxTernaryBonsai4B2BitFiles, 6, 1131565944LL, 4096, true}, - {"mlx-ternary-bonsai-8b-2bit", "mlx-ternary-bonsai-8b", - "MLX Ternary-Bonsai-8B 2-bit", v1::MODEL_CATEGORY_LANGUAGE, - v1::INFERENCE_FRAMEWORK_MLX, v1::MODEL_FORMAT_SAFETENSORS, nullptr, - kMlxTernaryBonsai8B2BitFiles, 6, 2303661704LL, 4096, true}, - {"mlx-ternary-bonsai-27b-2bit", "mlx-ternary-bonsai-27b", - "MLX Ternary-Bonsai-27B 2-bit", v1::MODEL_CATEGORY_LANGUAGE, - v1::INFERENCE_FRAMEWORK_MLX, v1::MODEL_FORMAT_SAFETENSORS, nullptr, - kMlxTernaryBonsai27B2BitFiles, 8, 8490785104LL, 4096, true}, - {"mlx-llama-3.2-1b-instruct-4bit", "mlx-llama3.2", - "Llama 3.2 1B Instruct 4-bit (MLX)", v1::MODEL_CATEGORY_LANGUAGE, - v1::INFERENCE_FRAMEWORK_MLX, v1::MODEL_FORMAT_SAFETENSORS, nullptr, - kMlxLlama32_1BFiles, 6, 712575975, 0, false}, - {"mlx-qwen2-vl-2b-instruct-4bit", "mlx-qwen2-vl", - "Qwen2-VL 2B Instruct 4-bit (MLX)", v1::MODEL_CATEGORY_MULTIMODAL, - v1::INFERENCE_FRAMEWORK_MLX, v1::MODEL_FORMAT_SAFETENSORS, nullptr, - kMlxQwen2Vl2BFiles, 11, 1261853827, 2048, false}, - {"mlx-fastvlm-0.5b-bf16", "mlx-fastvlm", "FastVLM 0.5B bf16 (MLX)", - v1::MODEL_CATEGORY_MULTIMODAL, v1::INFERENCE_FRAMEWORK_MLX, - v1::MODEL_FORMAT_SAFETENSORS, nullptr, kMlxFastVlm05BFiles, 14, 1256926974, - 2048, false}, - {"mlx-lfm2.5-vl-3b-4bit", "mlx-lfm2.5-vl", "LFM2.5-VL 3B 4-bit (MLX)", - v1::MODEL_CATEGORY_MULTIMODAL, v1::INFERENCE_FRAMEWORK_MLX, - v1::MODEL_FORMAT_SAFETENSORS, nullptr, kMlxLfm2_5Vl3BFiles, 9, - 2388258432LL, 4096, false}, - {"mlx-qwen3-embedding-0.6b-4bit-dwq", "mlx-qwen3-embed", - "Qwen3 Embedding 0.6B 4-bit DWQ (MLX)", v1::MODEL_CATEGORY_EMBEDDING, - v1::INFERENCE_FRAMEWORK_MLX, v1::MODEL_FORMAT_SAFETENSORS, nullptr, - kMlxQwen3Embedding06BFiles, 11, 351230811, 0, false}, - {"mlx-qwen3-asr-0.6b-8bit", "mlx-qwen3-asr", "Qwen3-ASR 0.6B 8-bit (MLX)", - v1::MODEL_CATEGORY_SPEECH_RECOGNITION, v1::INFERENCE_FRAMEWORK_MLX, - v1::MODEL_FORMAT_SAFETENSORS, nullptr, kMlxQwen3Asr06BFiles, 9, 1010773761, - 0, false}, - {"mlx-glm-asr-nano-2512-4bit", "mlx-glm-asr", - "GLM-ASR Nano 2512 4-bit (MLX)", v1::MODEL_CATEGORY_SPEECH_RECOGNITION, - v1::INFERENCE_FRAMEWORK_MLX, v1::MODEL_FORMAT_SAFETENSORS, nullptr, - kMlxGlmAsrNano2512Files, 9, 1288437789, 0, false}, - {"mlx-parakeet-ctc-1.1b", "mlx-parakeet-ctc", - "NVIDIA Parakeet CTC 1.1B (MLX)", v1::MODEL_CATEGORY_SPEECH_RECOGNITION, - v1::INFERENCE_FRAMEWORK_MLX, v1::MODEL_FORMAT_SAFETENSORS, nullptr, - kMlxParakeetCtc11BFiles, 2, 4250718357LL, 0, false}, - {"mlx-parakeet-tdt-0.6b-v2", "mlx-parakeet-tdt-v2", - "NVIDIA Parakeet TDT 0.6B v2 (MLX)", v1::MODEL_CATEGORY_SPEECH_RECOGNITION, - v1::INFERENCE_FRAMEWORK_MLX, v1::MODEL_FORMAT_SAFETENSORS, nullptr, - kMlxParakeetTdtV2Files, 2, 2471596080LL, 0, false}, - {"mlx-parakeet-tdt-0.6b-v3", "mlx-parakeet-tdt-v3", - "NVIDIA Parakeet TDT 0.6B v3 (MLX)", v1::MODEL_CATEGORY_SPEECH_RECOGNITION, - v1::INFERENCE_FRAMEWORK_MLX, v1::MODEL_FORMAT_SAFETENSORS, nullptr, - kMlxParakeetTdtV3Files, 2, 2508532829LL, 0, false}, - {"mlx-parakeet-rnnt-1.1b", "mlx-parakeet-rnnt", - "NVIDIA Parakeet RNNT 1.1B (MLX)", v1::MODEL_CATEGORY_SPEECH_RECOGNITION, - v1::INFERENCE_FRAMEWORK_MLX, v1::MODEL_FORMAT_SAFETENSORS, nullptr, - kMlxParakeetRnnt11BFiles, 2, 4282283914LL, 0, false}, - {"mlx-nemotron-3.5-asr-streaming-0.6b-8bit", "mlx-nemotron-asr", - "NVIDIA Nemotron 3.5 Streaming ASR 0.6B 8-bit (MLX)", - v1::MODEL_CATEGORY_SPEECH_RECOGNITION, v1::INFERENCE_FRAMEWORK_MLX, - v1::MODEL_FORMAT_SAFETENSORS, nullptr, kMlxNemotronStreamingAsrFiles, 2, - 755758528LL, 0, false}, - {"mlx-qwen3-tts-12hz-0.6b-base-8bit", "mlx-qwen3-tts", - "Qwen3-TTS 12Hz 0.6B Base 8-bit (MLX)", - v1::MODEL_CATEGORY_SPEECH_SYNTHESIS, v1::INFERENCE_FRAMEWORK_MLX, - v1::MODEL_FORMAT_SAFETENSORS, nullptr, kMlxQwen3Tts06BBaseFiles, 12, - 1991299138, 0, false}, - {"mlx-soprano-1.1-80m-5bit", "mlx-soprano", "Soprano 1.1 80M 5-bit (MLX)", - v1::MODEL_CATEGORY_SPEECH_SYNTHESIS, v1::INFERENCE_FRAMEWORK_MLX, - v1::MODEL_FORMAT_SAFETENSORS, nullptr, kMlxSoprano1180M5BitFiles, 7, - 82220814, 0, false}, - - // Google Gemma 4 family (MLX). config.json model_type "gemma4" / - // "gemma4_unified" (12B), both registered in the pinned mlx-swift-lm - // 3.31.5 LLMTypeRegistry/VLMTypeRegistry — verified by reading the - // checked-out package source this session (not assumed). Licensed under - // Apache 2.0; preserve the upstream license and attribution notices. - {"mlx-gemma-4-e2b-it-4bit", "mlx-gemma4-e2b", "Gemma 4 E2B IT 4-bit (MLX)", - v1::MODEL_CATEGORY_LANGUAGE, v1::INFERENCE_FRAMEWORK_MLX, - v1::MODEL_FORMAT_SAFETENSORS, nullptr, kMlxGemma4E2BFiles, 8, 3550670554LL, - 4096, false}, - {"mlx-gemma-4-e4b-it-qat-4bit", "mlx-gemma4-e4b", - "Gemma 4 E4B IT QAT 4-bit (MLX)", v1::MODEL_CATEGORY_LANGUAGE, - v1::INFERENCE_FRAMEWORK_MLX, v1::MODEL_FORMAT_SAFETENSORS, nullptr, - kMlxGemma4E4BFiles, 9, 6798307742LL, 4096, false}, - {"mlx-gemma-4-12b-it-qat-4bit", "mlx-gemma4-12b", - "Gemma 4 12B IT QAT 4-bit (MLX)", v1::MODEL_CATEGORY_LANGUAGE, - v1::INFERENCE_FRAMEWORK_MLX, v1::MODEL_FORMAT_SAFETENSORS, nullptr, - kMlxGemma4_12BFiles, 10, 10987772430LL, 4096, false}, - {"mlx-gemma-4-26b-a4b-it-4bit", "mlx-gemma4-26b-a4b", - "Gemma 4 26B-A4B IT 4-bit (MLX, MoE)", v1::MODEL_CATEGORY_LANGUAGE, - v1::INFERENCE_FRAMEWORK_MLX, v1::MODEL_FORMAT_SAFETENSORS, nullptr, - kMlxGemma4_26BA4BFiles, 10, 15341205776LL, 4096, false}, - // The plain 4bit variant, NOT "-qat-4bit" — that name does not resolve to - // a clean repo (verified this session); this is the largest dense Gemma 4. - {"mlx-gemma-4-31b-it-4bit", "mlx-gemma4-31b", "Gemma 4 31B IT 4-bit (MLX)", - v1::MODEL_CATEGORY_LANGUAGE, v1::INFERENCE_FRAMEWORK_MLX, - v1::MODEL_FORMAT_SAFETENSORS, nullptr, kMlxGemma4_31BFiles, 11, - 18412016676LL, 4096, false}, - - // Qwen3.6-35B-A3B (MoE) — config.json model_type "qwen3_5_moe", - // registered in mlx-swift-lm 3.31.5's LLMTypeRegistry. - {"mlx-qwen3.6-35b-a3b-4bit", "mlx-qwen3.6-35b", - "Qwen3.6 35B-A3B 4-bit (MLX, MoE)", v1::MODEL_CATEGORY_LANGUAGE, - v1::INFERENCE_FRAMEWORK_MLX, v1::MODEL_FORMAT_SAFETENSORS, nullptr, - kMlxQwen3_6_35BA3BFiles, 15, 20402204271LL, 4096, true}, - // Qwen3.8-27B (dense) — config.json model_type "qwen3_5", registered. - {"mlx-qwen3.8-27b-4bit", "mlx-qwen3.8-27b", "Qwen3.8 27B 4-bit (MLX)", - v1::MODEL_CATEGORY_LANGUAGE, v1::INFERENCE_FRAMEWORK_MLX, - v1::MODEL_FORMAT_SAFETENSORS, nullptr, kMlxQwen3_8_27BFiles, 13, - 16054541349LL, 4096, true}, - - // IBM Granite 4.1 family (MLX). config.json model_type "granite", - // registered in mlx-swift-lm 3.31.5's LLMTypeRegistry. - {"mlx-granite-4.1-3b-4bit", "mlx-granite4.1-3b", - "IBM Granite 4.1 3B 4-bit (MLX)", v1::MODEL_CATEGORY_LANGUAGE, - v1::INFERENCE_FRAMEWORK_MLX, v1::MODEL_FORMAT_SAFETENSORS, nullptr, - kMlxGranite4_1_3BFiles, 7, 2127162429LL, 4096, false}, - // A real, official mlx-community 8B 4-bit quant does exist (Apache-2.0, - // model_type "granite") — verified via HF API this session, despite the - // original assumption that none did; added for parity with 3B/30B. - {"mlx-granite-4.1-8b-4bit", "mlx-granite4.1-8b", - "IBM Granite 4.1 8B 4-bit (MLX)", v1::MODEL_CATEGORY_LANGUAGE, - v1::INFERENCE_FRAMEWORK_MLX, v1::MODEL_FORMAT_SAFETENSORS, nullptr, - kMlxGranite4_1_8BFiles, 7, 5238406779LL, 4096, false}, - {"mlx-granite-4.1-30b-4bit", "mlx-granite4.1-30b", - "IBM Granite 4.1 30B 4-bit (MLX)", v1::MODEL_CATEGORY_LANGUAGE, - v1::INFERENCE_FRAMEWORK_MLX, v1::MODEL_FORMAT_SAFETENSORS, nullptr, - kMlxGranite4_1_30BFiles, 10, 18041976573LL, 4096, false}, - - // Muse Glimmer 30B (MLX) and Nemotron-3-Nano-Omni-30B-A3B-Reasoning (MLX) - // are deliberately NOT registered here. Their config.json model_types - // ("muse_glimmer" and "NemotronH_Nano_Omni_Reasoning_V3" respectively) are - // NOT present in the pinned mlx-swift-lm 3.31.5 LLMTypeRegistry / - // VLMTypeRegistry (checked the checked-out package source directly: - // .build/checkouts/mlx-swift-lm/Libraries/{MLXLLM,MLXVLM}/*Factory.swift — - // only "nemotron_h" exists, a different string). Loading either would fail - // with ModelFactoryError.unsupportedModelType. The GGUF+mmproj rows above - // (llama.cpp) remain the way to run these two on rcli. -}; - -constexpr size_t kCatalogCount = sizeof(kCatalog) / sizeof(kCatalog[0]); - -rac_result_t register_entry(const CatalogEntry &entry) { - // CoreML bundles (a directory of compiled .mlmodelc sub-models) don't fit the - // URL / multi-file download-factory grammar, which rejects a bare repo ref. - // Register the ModelInfo directly so the id resolves in the general registry - // (and `rcli list` shows it); the bundle itself is fetched by the diffusion - // pipeline or supplied to `rcli image --model `. - if (entry.framework == v1::INFERENCE_FRAMEWORK_COREML) { - v1::ModelInfo model; - model.set_id(entry.id); - model.set_name(entry.name); - model.set_category(entry.category); - model.set_framework(entry.framework); - model.set_format(entry.format); - if (entry.url != nullptr) { - model.set_download_url(entry.url); - } - model.set_download_size_bytes(entry.download_size_bytes); - model.set_source(v1::MODEL_SOURCE_REMOTE); - const std::string bytes = proto::serialize(model); - return rac_model_registry_register_proto( - rac_get_model_registry(), - reinterpret_cast(bytes.data()), bytes.size()); - } - - rac_proto_buffer_t out; - rac_proto_buffer_init(&out); - rac_result_t rc = RAC_SUCCESS; - - if (entry.files != nullptr) { - runanywhere::v1::RegisterMultiFileModelRequest request; - request.set_id(entry.id); - request.set_name(entry.name); - request.set_framework(entry.framework); - request.set_category(entry.category); - request.set_format(entry.format); - request.set_download_size_bytes(entry.download_size_bytes); - if (entry.memory_required_bytes > 0) { - request.set_memory_required_bytes(entry.memory_required_bytes); - } - if (entry.context_length > 0) { - request.set_context_length(entry.context_length); - } - if (entry.supports_thinking) { - request.set_supports_thinking(true); - } - if (entry.cua_profile != nullptr && entry.cua_profile[0] != '\0') { - request.set_cua_profile(entry.cua_profile); - } - for (size_t i = 0; i < entry.file_count; ++i) { - runanywhere::v1::ModelFileDescriptor *file = request.add_files(); - file->set_url(entry.files[i].url); - file->set_filename(entry.files[i].filename); - file->set_is_optional(!entry.files[i].required); - if (entry.files[i].size_bytes > 0) { - file->set_size_bytes(entry.files[i].size_bytes); - } - if (entry.files[i].checksum_sha256 != nullptr) { - file->set_checksum_sha256(entry.files[i].checksum_sha256); - } - } - const std::string bytes = proto::serialize(request); - rc = rac_register_multi_file_model_proto( - reinterpret_cast(bytes.data()), bytes.size(), &out); - } else { - runanywhere::v1::RegisterModelFromUrlRequest request; - request.set_url(entry.url); - request.set_name(entry.name); - request.set_id(entry.id); - request.set_framework(entry.framework); - request.set_category(entry.category); - request.set_download_size_bytes(entry.download_size_bytes); - if (entry.context_length > 0) { - request.set_context_length(entry.context_length); - } - if (entry.supports_thinking) { - request.set_supports_thinking(true); - } - const std::string bytes = proto::serialize(request); - rc = rac_register_model_from_url_proto( - reinterpret_cast(bytes.data()), bytes.size(), &out); - } - - // The saved ModelInfo bytes are not needed here — only the status envelope. - const rac_result_t status = (rc == RAC_SUCCESS) ? out.status : rc; - rac_proto_buffer_free(&out); - return status; -} - -} // namespace - -const CatalogEntry *all(size_t *count) { - if (count) { - *count = kCatalogCount; - } - return kCatalog; -} - -const CatalogEntry *find(const std::string &id_or_alias) { - for (const CatalogEntry &entry : kCatalog) { - if (id_or_alias == entry.id || - (entry.alias && id_or_alias == entry.alias)) { - return &entry; - } - } - return nullptr; -} - -std::vector suggestions(const std::string &input, size_t max) { - std::vector matches; - for (const CatalogEntry &entry : kCatalog) { - if (matches.size() >= max) { - break; - } - if (std::string(entry.id).find(input) != std::string::npos || - (entry.alias && - std::string(entry.alias).find(input) != std::string::npos)) { - matches.emplace_back(entry.id); - } - } - return matches; -} - -rac_result_t register_all() { - rac_result_t first_error = RAC_SUCCESS; - for (const CatalogEntry &entry : kCatalog) { - const rac_result_t rc = register_entry(entry); - if (rc != RAC_SUCCESS) { - out::status_line( - std::string("warning: catalog registration failed for ") + entry.id + - ": " + out::describe_result(rc)); - if (first_error == RAC_SUCCESS) { - first_error = rc; - } - } - } - return first_error; -} - -} // namespace rcli::catalog diff --git a/rcli/src/catalog/catalog.h b/rcli/src/catalog/catalog.h deleted file mode 100644 index 115086d3fb..0000000000 --- a/rcli/src/catalog/catalog.h +++ /dev/null @@ -1,70 +0,0 @@ -/** - * @file catalog.h - * @brief Built-in model catalog — the CLI's curated equivalent of the example - * apps' ModelCatalog (iOS ModelCatalogBootstrap.swift is the canonical - * reference; ids/URLs are copied verbatim from the app catalogs and the - * commons test tooling). - * - * Entries use the proto-generated enums (structured-types rule) and register - * through the same single-call commons entry points the SDKs use: - * rac_register_model_from_url_proto / rac_register_multi_file_model_proto. - * Registration is idempotent per process (the registry is in-memory; apps - * re-register their catalogs on every launch the same way). - */ - -#ifndef RCLI_CATALOG_CATALOG_H -#define RCLI_CATALOG_CATALOG_H - -#include -#include -#include -#include - -#include "model_types.pb.h" -#include "rac/core/rac_types.h" - -namespace rcli::catalog { - -struct CatalogFile { - const char *url; - const char *filename; - bool required; - int64_t size_bytes = 0; - const char *checksum_sha256 = nullptr; -}; - -struct CatalogEntry { - const char *id; - const char *alias; // short name accepted by `pull/run/...` (nullptr = none) - const char *name; - runanywhere::v1::ModelCategory category; - runanywhere::v1::InferenceFramework framework; - runanywhere::v1::ModelFormat format; - const char *url; // single-file / archive primary (nullptr → multi-file) - const CatalogFile *files; // multi-file artifacts (VLM pairs, embeddings) - size_t file_count; - int64_t download_size_bytes; // approximate, for display/planning - int32_t context_length; // 0 = unknown/not applicable - bool supports_thinking; - int64_t memory_required_bytes = 0; // 0 = unknown/not applicable - const char *cua_profile = ""; // Computer-Use-Agent profile id ("" = none) -}; - -/** All built-in entries. */ -const CatalogEntry *all(size_t *count); - -/** Exact id or alias lookup (nullptr when unknown). */ -const CatalogEntry *find(const std::string &id_or_alias); - -/** Closest-match candidates for error messages (substring match, ≤ max). */ -std::vector suggestions(const std::string &input, size_t max); - -/** - * Register every entry with the global model registry. Logs (does not fail - * on) individual rejections so one bad entry can't take the CLI down. - */ -rac_result_t register_all(); - -} // namespace rcli::catalog - -#endif // RCLI_CATALOG_CATALOG_H diff --git a/rcli/src/catalog/model_ref.cpp b/rcli/src/catalog/model_ref.cpp deleted file mode 100644 index c05c525f5b..0000000000 --- a/rcli/src/catalog/model_ref.cpp +++ /dev/null @@ -1,316 +0,0 @@ -#include "catalog/model_ref.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "model_types.pb.h" -#include "rac/core/rac_core.h" -#include "rac/infrastructure/model_management/rac_model_registry.h" - -#include "catalog/catalog.h" -#include "io/output.h" -#include "io/proto.h" - -namespace rcli::model_ref { - -namespace { - -bool is_http_url(const std::string &ref) { - return ref.starts_with("http://") || ref.starts_with("https://"); -} - -// Thin gate only — the full HF ref grammar (repo refs, quant tags, explicit -// file paths, shard/mmproj resolution) is owned by commons inside -// rac_register_model_from_url_proto; the CLI just decides whether `ref` is -// worth handing over versus reporting "unknown model". -bool looks_like_hf_ref(const std::string &ref) { - for (const char *prefix : {"hf://", "hf.co/", "huggingface.co/"}) { - if (ref.starts_with(prefix)) { - return true; - } - } - return false; -} - -// A ref that names an existing directory or file on disk. Checked BEFORE the -// HF/URL branch but AFTER the catalog and the registry, so a local path can -// never shadow a real model id. -bool is_local_path(const std::string &ref) { - if (ref.empty() || is_http_url(ref) || looks_like_hf_ref(ref)) { - return false; - } - // Only treat something as a path when it looks like one. A bare word is a - // model id; requiring a separator or an explicit `.`/`~` prefix keeps - // `rcli run qwen3` from stat()ing the cwd and finding a stray directory. - if (ref.find('/') == std::string::npos) { - return false; - } - struct stat st {}; - return ::stat(ref.c_str(), &st) == 0; -} - -// A trailing `/` carries no meaning but would break both the basename split -// and the `.mlpackage` suffix tests below, so strip it once, here. -std::string without_trailing_slashes(const std::string &path) { - std::string out = path; - while (out.size() > 1 && out.back() == '/') { - out.pop_back(); - } - return out; -} - -// Derive a stable, collision-free registry id from a bundle path. Using one -// hardcoded id (as the diffusion path does) is fine for a single model and -// wrong the moment two bundles are registered in one process — the second -// silently overwrites the first, and every later load resolves to whichever -// won. -std::string id_for_local_path(const std::string &path) { - std::string full = without_trailing_slashes(path); - // Canonicalize first so the same bundle spelled differently (relative, `..`, - // a symlink) keeps ONE id; the raw path is the fallback when it cannot be - // resolved. `std::filesystem` rather than `realpath` because rcli builds on - // Windows too and `realpath` is POSIX-only; `weakly_canonical` also tolerates - // a path that does not fully exist instead of failing outright. - std::error_code ec; - const std::filesystem::path canonical = std::filesystem::weakly_canonical(full, ec); - if (!ec && !canonical.empty()) { - full = canonical.string(); - } - - // filename() handles BOTH separators; a manual find_last_of('/') would return - // the whole path as the basename on a Windows-style path. - std::string base = std::filesystem::path(full).filename().string(); - if (base.empty()) { - base = full; - } - for (char &c : base) { - c = static_cast(std::tolower(static_cast(c))); - if (!std::isalnum(static_cast(c)) && c != '-' && c != '.') { - c = '-'; - } - } - - // The basename alone collides — every `.../model.mlpackage` sanitizes to the - // same string — so pin the id to the whole path with an FNV-1a digest. - uint64_t hash = 14695981039346656037ULL; // FNV-1a 64-bit offset basis - for (const unsigned char c : full) { - hash ^= c; - hash *= 1099511628211ULL; - } - static constexpr char kHex[] = "0123456789abcdef"; - std::string digest; - // 16 nibbles: emit the whole 64-bit hash. Stopping at shift 28 kept only the - // low 32 bits, so paths differing above that bit shared an id. - for (int shift = 60; shift >= 0; shift -= 4) { - digest.push_back(kHex[(hash >> shift) & 0xF]); - } - return "local-" + base + "-" + digest; -} - -// Best-effort format/framework inference from the bundle layout. Only used when -// the caller did not pin one with --engine, and deliberately narrow: it answers -// "which of the shapes the CLI can actually load is this", not "what model is -// this". Unknown layouts are left UNSPECIFIED so commons falls back to its own -// resolution rather than acting on a CLI guess. -void infer_local_kind(const std::string &path, - runanywhere::v1::InferenceFramework *framework, - runanywhere::v1::ModelFormat *format) { - *framework = runanywhere::v1::INFERENCE_FRAMEWORK_UNSPECIFIED; - *format = runanywhere::v1::MODEL_FORMAT_UNSPECIFIED; - - struct stat st {}; - if (::stat(path.c_str(), &st) != 0) { - return; - } - if (!S_ISDIR(st.st_mode)) { - if (path.ends_with(".gguf")) { - *framework = runanywhere::v1::INFERENCE_FRAMEWORK_LLAMA_CPP; - *format = runanywhere::v1::MODEL_FORMAT_GGUF; - } - return; - } - // A Core ML package IS a directory, so a ref naming the package itself lands - // here too — and its children (Manifest.json, Data/) match nothing in the - // scan below. Check the path's own name before descending into it. - const std::string self = without_trailing_slashes(path); - if (self.ends_with(".mlpackage") || self.ends_with(".mlmodelc")) { - *framework = runanywhere::v1::INFERENCE_FRAMEWORK_COREML; - *format = runanywhere::v1::MODEL_FORMAT_MLPACKAGE; - return; - } - // A directory holding at least one .mlpackage is an Apple bundle — the shape - // every runanywhere/*_ANE repo ships. - if (DIR *dir = ::opendir(path.c_str())) { - while (dirent *ent = ::readdir(dir)) { - const std::string name = ent->d_name; - if (name.ends_with(".mlpackage") || name.ends_with(".mlmodelc")) { - *framework = runanywhere::v1::INFERENCE_FRAMEWORK_COREML; - *format = runanywhere::v1::MODEL_FORMAT_MLPACKAGE; - break; - } - } - ::closedir(dir); - } -} - -// Register an already-present local bundle so the lifecycle loader can resolve -// it by id, with no download. Mirrors what `rcli image` has always done for a -// local CoreML diffusion bundle (cmd_image.cpp::register_local_bundle) — this -// is that capability moved down to the shared resolver, so every command that -// takes a model ref gets it instead of just one. -rac_result_t register_local_path(const std::string &path, - const ResolveOptions *options, - std::string *out_id, std::string *error) { - runanywhere::v1::InferenceFramework framework; - runanywhere::v1::ModelFormat format; - infer_local_kind(path, &framework, &format); - // An explicit --engine always wins over inference. - if (options && options->has_framework) { - framework = options->framework; - } - - runanywhere::v1::ModelInfo model; - model.set_id(id_for_local_path(path)); - model.set_name(path); - model.set_local_path(path); - model.set_source(runanywhere::v1::MODEL_SOURCE_LOCAL); - if (framework != runanywhere::v1::INFERENCE_FRAMEWORK_UNSPECIFIED) { - model.set_framework(framework); - } - if (format != runanywhere::v1::MODEL_FORMAT_UNSPECIFIED) { - model.set_format(format); - } - if (options && options->has_category) { - model.set_category(options->category); - } else { - model.set_category(runanywhere::v1::MODEL_CATEGORY_LANGUAGE); - } - - const std::string bytes = proto::serialize(model); - const rac_result_t rc = rac_model_registry_register_proto( - rac_get_model_registry(), - reinterpret_cast(bytes.data()), bytes.size()); - if (rc != RAC_SUCCESS) { - if (error) { - *error = "failed to register local bundle '" + path + - "': " + out::describe_result(rc); - } - return rc; - } - *out_id = model.id(); - return RAC_SUCCESS; -} - -// Registers a URL or Hugging Face ref through the commons factory and returns -// the saved id. Durable persistence is commons-owned: once the model -// downloads, the model-folder manifest sidecar restores the entry on the next -// launch (no CLI-side registry needed). -rac_result_t register_url(const std::string &url, const ResolveOptions *options, - std::string *out_id, std::string *error) { - runanywhere::v1::RegisterModelFromUrlRequest request; - request.set_url(url); - if (options && options->has_framework) { - request.set_framework(options->framework); - } - if (options && options->has_category) { - request.set_category(options->category); - } - - const std::string bytes = proto::serialize(request); - rac_proto_buffer_t out; - rac_proto_buffer_init(&out); - const rac_result_t rc = rac_register_model_from_url_proto( - reinterpret_cast(bytes.data()), bytes.size(), &out); - if (rc != RAC_SUCCESS) { - std::string detail = - out.error_message ? out.error_message : out::describe_result(rc); - rac_proto_buffer_free(&out); - if (error) { - *error = "failed to register " + url + ": " + detail; - } - return rc; - } - - runanywhere::v1::ModelInfo saved; - std::string parse_error; - if (!proto::parse_proto_buffer(&out, &saved, &parse_error)) { - if (error) { - *error = "failed to register " + url + ": " + parse_error; - } - return RAC_ERROR_INVALID_ARGUMENT; - } - *out_id = saved.id(); - return RAC_SUCCESS; -} - -} // namespace - -rac_result_t resolve(const std::string &ref, Resolved *out, std::string *error, - const ResolveOptions *options) { - if (ref.empty()) { - if (error) { - *error = "empty model reference"; - } - return RAC_ERROR_INVALID_ARGUMENT; - } - - if (const catalog::CatalogEntry *entry = catalog::find(ref)) { - out->model_id = entry->id; - out->from_catalog = true; - return RAC_SUCCESS; - } - - // Registered but non-catalog ids: manifest-restored URL/HF pulls, - // discovered models. - if (!is_http_url(ref)) { - rac_proto_buffer_t found; - rac_proto_buffer_init(&found); - if (rac_model_registry_get_proto_buffer( - rac_get_model_registry(), ref.c_str(), &found) == RAC_SUCCESS && - found.status == RAC_SUCCESS) { - rac_proto_buffer_free(&found); - out->model_id = ref; - out->from_catalog = false; - return RAC_SUCCESS; - } - rac_proto_buffer_free(&found); - } - - // A path on disk. Checked after the catalog + registry so it can never - // shadow a real id, and before the "unknown model" arm so an ANE bundle - // sitting in a directory is reachable at all — it previously was not, which - // made every local Core ML LLM unloadable through the CLI. - if (is_local_path(ref)) { - out->from_catalog = false; - return register_local_path(ref, options, &out->model_id, error); - } - - if (is_http_url(ref) || looks_like_hf_ref(ref)) { - out->from_catalog = false; - return register_url(ref, options, &out->model_id, error); - } - - if (error) { - *error = "unknown model '" + ref + "'"; - const std::vector close = catalog::suggestions(ref, 3); - if (!close.empty()) { - *error += " — did you mean: "; - for (size_t i = 0; i < close.size(); ++i) { - *error += (i ? ", " : "") + close[i]; - } - *error += "?"; - } else { - *error += " (try `rcli list --all`, an hf.co/org/repo[:quant] ref, a " - "direct URL, or a path to a local bundle directory)"; - } - } - return RAC_ERROR_NOT_FOUND; -} - -} // namespace rcli::model_ref diff --git a/rcli/src/catalog/model_ref.h b/rcli/src/catalog/model_ref.h deleted file mode 100644 index b418cb496f..0000000000 --- a/rcli/src/catalog/model_ref.h +++ /dev/null @@ -1,53 +0,0 @@ -/** - * @file model_ref.h - * @brief Model reference resolution for pull/run/show/rm arguments. - * - * Accepted forms, resolved in order: - * 1. catalog id qwen3-0.6b - * 2. catalog alias qwen3 - * 3. registered id (manifest-restored URL/HF pulls, discovered - * models) - * 4. hf.co//[:quant|/file], hf://..., huggingface.co/... - * 5. http(s)://... - * - * URL and HF forms go through rac_register_model_from_url_proto so the whole - * grammar (quant selection, mmproj pairing, shards, id/name/format inference) - * lives in commons — the CLI never guesses. - */ - -#ifndef RCLI_CATALOG_MODEL_REF_H -#define RCLI_CATALOG_MODEL_REF_H - -#include - -#include "model_types.pb.h" -#include "rac/core/rac_types.h" - -namespace rcli::model_ref { - -struct Resolved { - std::string model_id; // registry id to operate on - bool from_catalog = false; -}; - -struct ResolveOptions { - bool has_framework = false; - runanywhere::v1::InferenceFramework framework = - runanywhere::v1::INFERENCE_FRAMEWORK_UNSPECIFIED; - bool has_category = false; - runanywhere::v1::ModelCategory category = - runanywhere::v1::MODEL_CATEGORY_UNSPECIFIED; -}; - -/** - * Resolve `ref` to a registered model id. Catalog entries are assumed already - * registered (bootstrap runs catalog::register_all()). URL refs register a new - * entry on the fly. Returns RAC_SUCCESS or an error; `error` (non-null) - * receives a user-facing message including did-you-mean suggestions. - */ -rac_result_t resolve(const std::string &ref, Resolved *out, std::string *error, - const ResolveOptions *options = nullptr); - -} // namespace rcli::model_ref - -#endif // RCLI_CATALOG_MODEL_REF_H diff --git a/rcli/src/commands/bench_metrics.h b/rcli/src/commands/bench_metrics.h deleted file mode 100644 index 919104c5ca..0000000000 --- a/rcli/src/commands/bench_metrics.h +++ /dev/null @@ -1,80 +0,0 @@ -/** - * @file bench_metrics.h - * @brief Pure consume-only mapping from commons result protos → bench fields. - * - * No tok/s, decode_ms, RTF, or chars/s reconstruction. Missing commons values - * stay 0 / absent. Harness wall clocks (load/warmup/measured e2e) are separate. - */ - -#ifndef RCLI_COMMANDS_BENCH_METRICS_H -#define RCLI_COMMANDS_BENCH_METRICS_H - -#include "llm_options.pb.h" -#include "vlm_options.pb.h" - -#include - -namespace rcli::commands::bench_metrics { - -struct LlmVlmMetrics { - double end_to_end_ms = 0.0; - double tokens_per_second = 0.0; - double prompt_eval_ms = 0.0; - double decode_ms = 0.0; - int32_t output_tokens = 0; -}; - -/** Prefill from prompt_eval_time_ms, else TokenUsage.prefill_ms — never TTFT. - */ -inline double measured_prefill_ms(int64_t prompt_eval_time_ms, int64_t usage_prefill_ms) { - if (prompt_eval_time_ms > 0) { - return static_cast(prompt_eval_time_ms); - } - if (usage_prefill_ms > 0) { - return static_cast(usage_prefill_ms); - } - return 0.0; -} - -/** - * Map LLMGenerationResult → LLM bench metrics. - * @param measured_e2e_ms harness stopwatch; used only when generation_time_ms - * is 0. - * @return false when output_tokens <= 0 (not a successful LLM trial). - */ -inline bool fill_llm(const runanywhere::v1::LLMGenerationResult& r, double measured_e2e_ms, - LlmVlmMetrics* out) { - const int32_t out_tokens = r.usage().output_tokens(); - if (out_tokens <= 0) { - return false; - } - out->output_tokens = out_tokens; - out->end_to_end_ms = r.generation_time_ms() > 0.0 ? r.generation_time_ms() : measured_e2e_ms; - out->tokens_per_second = r.usage().decode_tokens_per_second(); - out->decode_ms = r.decode_time_ms() > 0 ? static_cast(r.decode_time_ms()) : 0.0; - out->prompt_eval_ms = measured_prefill_ms(r.prompt_eval_time_ms(), r.usage().prefill_ms()); - return true; -} - -/** - * Map VLMResult → VLM bench metrics. - * No decode window on VLMResult — decode_ms stays 0. - */ -inline bool fill_vlm(const runanywhere::v1::VLMResult& r, double measured_e2e_ms, - LlmVlmMetrics* out) { - const int32_t out_tokens = r.usage().output_tokens(); - if (out_tokens <= 0) { - return false; - } - out->output_tokens = out_tokens; - out->end_to_end_ms = - r.total_time_ms() > 0 ? static_cast(r.total_time_ms()) : measured_e2e_ms; - out->tokens_per_second = r.usage().decode_tokens_per_second(); - out->prompt_eval_ms = measured_prefill_ms(/*prompt_eval_time_ms=*/0, r.usage().prefill_ms()); - out->decode_ms = 0.0; - return true; -} - -} // namespace rcli::commands::bench_metrics - -#endif // RCLI_COMMANDS_BENCH_METRICS_H diff --git a/rcli/src/commands/cmd_auth.cpp b/rcli/src/commands/cmd_auth.cpp deleted file mode 100644 index a0ba1058e8..0000000000 --- a/rcli/src/commands/cmd_auth.cpp +++ /dev/null @@ -1,123 +0,0 @@ -/** - * @file cmd_auth.cpp - * @brief `rcli auth login` — real control-plane handshake. - * - * Runs the canonical staging/production auth sequence against the configured - * backend (--base-url/--api-key/--environment or their RUNANYWHERE_* env - * vars): authenticate (API key → JWT + refresh token), device registration, - * and model-assignment fetch — all through commons entry points - * (net::login → rac_auth_* + rac_sdk_init_phase2_proto). - */ - -#include "commands/commands.h" - -#include -#include -#include - -#include "net/control_plane.h" - -#include "io/output.h" - -namespace rcli::commands { - -namespace { - -std::string format_epoch_seconds(int64_t seconds) { - if (seconds <= 0) { - return "-"; - } - const time_t secs = static_cast(seconds); - struct tm tm_info{}; -#if defined(_WIN32) - if (gmtime_s(&tm_info, &secs) != 0) { - return std::to_string(seconds); - } -#else - if (gmtime_r(&secs, &tm_info) == nullptr) { - return std::to_string(seconds); - } -#endif - char buffer[32] = {}; - strftime(buffer, sizeof(buffer), "%Y-%m-%dT%H:%M:%SZ", &tm_info); - return buffer; -} - -int run_auth_login(const GlobalOptions& options) { - Bootstrapped env; - if (bootstrap(options, &env) != RAC_SUCCESS) { - return 1; - } - - net::LoginSummary summary; - std::string error; - if (net::login(&summary, &error) != RAC_SUCCESS) { - out::error_line(error); - return 1; - } - - // A staging/production login that never completes HTTP/auth setup is a - // broken control plane — surface it as a failure, not a footnote. - const bool ok = summary.has_completed_http_setup; - - if (options.json) { - out::JsonWriter json; - json.begin_object() - .field("success", ok) - .field("organization_id", summary.organization_id) - .field("user_id", summary.user_id) - .field("device_id", summary.backend_device_id) - .field("device_uuid", summary.persistent_device_id) - .field("token_expires_at", format_epoch_seconds(summary.token_expires_at)) - .field("has_completed_http_setup", summary.has_completed_http_setup) - .field("assignments", static_cast(summary.assignment_count)); - if (!summary.warning.empty()) { - json.field("warning", summary.warning); - } - json.end_object(); - out::result_line(json.str()); - } else { - out::result_line("organization " + summary.organization_id); - out::result_line("user " + - (summary.user_id.empty() ? std::string("-") : summary.user_id)); - out::result_line("device " + summary.backend_device_id); - out::result_line("device-uuid " + summary.persistent_device_id); - out::result_line("token expires " + format_epoch_seconds(summary.token_expires_at)); - out::result_line(std::string("http setup ") + - (summary.has_completed_http_setup ? "completed" : "NOT completed")); - out::result_line("assignments " + std::to_string(summary.assignment_count) + - " model(s)"); - if (!summary.warning.empty()) { - out::status_line("warning: " + summary.warning); - } - } - - if (!ok) { - out::error_line("HTTP/auth setup did not complete" + - (summary.warning.empty() ? "" : ": " + summary.warning)); - return 1; - } - return 0; -} - -} // namespace - -void register_auth(CLI::App& app, GlobalOptions& options) { - CLI::App* cmd = app.add_subcommand("auth", "Sign this device in to the control plane"); - cmd->require_subcommand(1); - - CLI::App* login_cmd = cmd->add_subcommand( - "login", - "Exchange the API key for a JWT, register this device and fetch model " - "assignments. Requires --environment production with --base-url and " - "--api-key (or RUNANYWHERE_* env vars). Keyless development has no login " - "path."); - login_cmd->callback([&options]() { - const int exit_code = run_auth_login(options); - if (exit_code != 0) { - throw CLI::RuntimeError(exit_code); - } - }); -} - -} // namespace rcli::commands diff --git a/rcli/src/commands/cmd_backends.cpp b/rcli/src/commands/cmd_backends.cpp deleted file mode 100644 index d24af66cf1..0000000000 --- a/rcli/src/commands/cmd_backends.cpp +++ /dev/null @@ -1,103 +0,0 @@ -/** - * @file cmd_backends.cpp - * @brief `rcli backends` — registered engine plugins per primitive. - */ - -#include "commands/commands.h" - -#include -#include -#include -#include - -#include "rac/plugin/rac_engine_vtable.h" -#include "rac/plugin/rac_plugin_entry.h" -#include "rac/plugin/rac_primitive.h" - -#include "io/output.h" - -namespace rcli::commands { - -namespace { - -constexpr rac_primitive_t kPrimitives[] = { - RAC_PRIMITIVE_GENERATE_TEXT, RAC_PRIMITIVE_TRANSCRIBE, RAC_PRIMITIVE_SYNTHESIZE, - RAC_PRIMITIVE_DETECT_VOICE, RAC_PRIMITIVE_EMBED, RAC_PRIMITIVE_VLM, - RAC_PRIMITIVE_DIFFUSION, -}; - -struct EngineRow { - std::string display_name; - std::string version; - int32_t priority = 0; - std::set primitives; -}; - -} // namespace - -void register_backends(CLI::App& app, GlobalOptions& options) { - CLI::App* cmd = app.add_subcommand("backends", "List registered inference backends"); - cmd->callback([&options]() { - Bootstrapped env; - if (bootstrap(options, &env) != RAC_SUCCESS) { - throw CLI::RuntimeError(1); - } - - std::map engines; - for (const rac_primitive_t primitive : kPrimitives) { - const rac_engine_vtable_t* plugins[16] = {}; - size_t count = 0; - if (rac_plugin_list(primitive, plugins, 16, &count) != RAC_SUCCESS) { - continue; - } - for (size_t i = 0; i < count; ++i) { - const rac_engine_metadata_t& meta = plugins[i]->metadata; - EngineRow& row = engines[meta.name ? meta.name : "?"]; - if (meta.display_name) { - row.display_name = meta.display_name; - } - if (meta.engine_version) { - row.version = meta.engine_version; - } - row.priority = meta.priority; - row.primitives.insert(rac_primitive_name(primitive)); - } - } - - if (options.json) { - out::JsonWriter json; - json.begin_object().begin_array("backends"); - for (const auto& [name, row] : engines) { - json.begin_array_object() - .field("name", name) - .field("display_name", row.display_name) - .field("version", row.version) - .field("priority", static_cast(row.priority)); - json.begin_array("primitives"); - for (const auto& primitive : row.primitives) { - json.begin_array_object().field("name", primitive).end_object(); - } - json.end_array().end_object(); - } - json.end_array().end_object(); - out::result_line(json.str()); - return; - } - - if (engines.empty()) { - out::result_line("no backends registered"); - return; - } - std::vector> rows; - for (const auto& [name, row] : engines) { - std::string primitives; - for (const auto& primitive : row.primitives) { - primitives += primitives.empty() ? primitive : ", " + primitive; - } - rows.push_back({name, std::to_string(row.priority), primitives}); - } - out::table({"NAME", "PRIORITY", "PRIMITIVES"}, rows); - }); -} - -} // namespace rcli::commands diff --git a/rcli/src/commands/cmd_bench.cpp b/rcli/src/commands/cmd_bench.cpp deleted file mode 100644 index f2cbf2818d..0000000000 --- a/rcli/src/commands/cmd_bench.cpp +++ /dev/null @@ -1,780 +0,0 @@ -/** - * @file cmd_bench.cpp - * @brief `rcli bench [model]` — auto-benchmark installed models, like the - * Android app's benchmark screen. - * - * With no model argument it enumerates every downloaded, non-built-in model - * from the registry and benchmarks each in its category (LLM / STT / TTS / - * VLM). Faithful port of the Android BenchmarkRunner / BenchmarkMetricPolicy - * flow: per (model, scenario), repeat `trials` times { - * unload → sample avail RAM → load (timed) → 1 warmup (discarded) - * → 1 measured pass → sample avail RAM → per-trial metrics } - * → aggregate trials by MEDIAN, report [min,max] where useful. - * - * Metrics come from commons result protos (TokenUsage + measured phase times). - * Missing values stay zero — no tok/s, decode_ms, RTF, or chars/s - * reconstruction. Harness wall clocks cover load / warmup / measured e2e only. - * No telemetry. - */ - -#include "chat.pb.h" -#include "llm_options.pb.h" -#include "llm_service.pb.h" -#include "model_types.pb.h" -#include "stt_options.pb.h" -#include "tts_options.pb.h" -#include "vlm_options.pb.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "catalog/model_ref.h" -#include "commands/bench_metrics.h" -#include "commands/commands.h" -#include "commands/engine_options.h" -#include "io/output.h" -#include "io/proto.h" -#include "rac/core/rac_benchmark.h" -#include "rac/core/rac_core.h" -#include "rac/core/rac_model_lifecycle.h" -#include "rac/features/llm/rac_llm_service.h" -#include "rac/features/stt/rac_stt_service.h" -#include "rac/features/tts/rac_tts_service.h" -#include "rac/features/vlm/rac_vlm_service.h" -#include "rac/infrastructure/model_management/rac_model_registry.h" - -namespace rcli::commands { - -namespace { - -namespace v1 = runanywhere::v1; - -// Prompts / text mirror the Android BenchmarkRunner constants so numbers are -// comparable across the CLI and the app. -constexpr const char* kLlmSystemPrompt = - "You are a helpful assistant. Always give extremely detailed, thorough " - "responses. Never stop " - "early. Use the full response length available to you. Elaborate on every " - "point with examples " - "and explanations."; -constexpr const char* kLlmPrompt = - "Write a very long and detailed explanation of how neural networks work, " - "covering perceptrons, " - "activation functions, backpropagation, gradient descent, loss functions, " - "convolutional " - "layers, recurrent layers, transformers, attention mechanisms, and " - "training procedures. Be as " - "thorough as possible."; -constexpr const char* kVlmPrompt = "Describe this image in detail."; -constexpr const char* kTtsShort = "Hello, this is a test."; -constexpr const char* kTtsMedium = - "The quick brown fox jumps over the lazy dog. Machine learning models can " - "generate speech from " - "text with remarkable quality and natural intonation."; -constexpr double kPi = 3.14159265358979323846; - -enum class Modality { kLlm, kStt, kTts, kVlm }; - -const char* modality_label(Modality m) { - switch (m) { - case Modality::kLlm: - return "llm"; - case Modality::kStt: - return "stt"; - case Modality::kTts: - return "tts"; - case Modality::kVlm: - return "vlm"; - } - return "?"; -} - -bool modality_of(v1::ModelCategory category, Modality* out) { - switch (category) { - case v1::MODEL_CATEGORY_LANGUAGE: - *out = Modality::kLlm; - return true; - case v1::MODEL_CATEGORY_SPEECH_RECOGNITION: - *out = Modality::kStt; - return true; - case v1::MODEL_CATEGORY_SPEECH_SYNTHESIS: - *out = Modality::kTts; - return true; - case v1::MODEL_CATEGORY_MULTIMODAL: - case v1::MODEL_CATEGORY_VISION: - *out = Modality::kVlm; - return true; - default: - return false; // vad, embedding, image-generation are not benchmarked - } -} - -struct Scenario { - const char* label; - int32_t max_tokens; // LLM/VLM - double seconds; // STT audio length - bool sine; // STT: 440 Hz tone vs silence - const char* text; // TTS input -}; - -const std::vector& scenarios_for(Modality m) { - static const std::vector llm = {{"Short (50)", 50, 0, false, nullptr}, - {"Medium (256)", 256, 0, false, nullptr}, - {"Long (512)", 512, 0, false, nullptr}}; - static const std::vector stt = {{"Silent 2s", 0, 2.0, false, nullptr}, - {"Sine Tone 3s", 0, 3.0, true, nullptr}}; - static const std::vector tts = {{"Short Text", 0, 0, false, kTtsShort}, - {"Medium Text", 0, 0, false, kTtsMedium}}; - static const std::vector vlm = {{"Image Description", 128, 0, false, nullptr}}; - switch (m) { - case Modality::kLlm: - return llm; - case Modality::kStt: - return stt; - case Modality::kTts: - return tts; - case Modality::kVlm: - return vlm; - } - return llm; -} - -// Per-trial metrics; aggregated to medians across trials. -struct Metrics { - double load_ms = 0.0; - double warmup_ms = 0.0; - double end_to_end_ms = 0.0; - double tokens_per_second = 0.0; // LLM/VLM - double prompt_eval_ms = 0.0; // LLM/VLM prefill - double decode_ms = 0.0; // LLM/VLM - int32_t output_tokens = 0; // LLM/VLM - double real_time_factor = 0.0; // STT - double chars_per_second = 0.0; // TTS - double audio_duration_ms = 0.0; // TTS - int64_t memory_delta_bytes = 0; -}; - -// --- small utilities ------------------------------------------------------- - -int64_t available_ram_bytes() { - std::FILE* f = std::fopen("/proc/meminfo", "r"); - if (!f) { - return 0; - } - char line[256]; - int64_t kb = 0; - while (std::fgets(line, sizeof(line), f)) { - if (std::sscanf(line, "MemAvailable: %lld kB", reinterpret_cast(&kb)) == 1) { - break; - } - } - std::fclose(f); - return kb * 1024; -} - -double median(std::vector values) { - std::vector v; - for (double x : values) { - if (std::isfinite(x)) { - v.push_back(x); - } - } - if (v.empty()) { - return 0.0; - } - std::sort(v.begin(), v.end()); - const size_t mid = v.size() / 2; - return (v.size() % 2 == 1) ? v[mid] : (v[mid - 1] + v[mid]) / 2.0; -} - -std::string human_bytes(int64_t bytes) { - if (bytes <= 0) { - return "-"; - } - const double b = static_cast(bytes); - char buf[32]; - if (b >= 1e9) { - std::snprintf(buf, sizeof(buf), "%.2f GB", b / 1e9); - } else if (b >= 1e6) { - std::snprintf(buf, sizeof(buf), "%.0f MB", b / 1e6); - } else { - std::snprintf(buf, sizeof(buf), "%.0f KB", b / 1e3); - } - return buf; -} - -// 16 kHz, 16-bit mono PCM: silence or a 440 Hz sine at 60% amplitude (matches -// Android SyntheticInput.silentPcm / sinePcm). -std::string make_pcm16(double seconds, bool sine) { - constexpr int kSampleRate = 16000; - const int n = static_cast(kSampleRate * seconds); - std::string out; - out.resize(static_cast(n) * 2); - auto* samples = reinterpret_cast(out.data()); - for (int i = 0; i < n; ++i) { - double v = sine ? std::sin(2.0 * kPi * 440.0 * i / kSampleRate) * 32767.0 * 0.6 : 0.0; - samples[i] = static_cast(v); - } - return out; -} - -// --- lifecycle helpers ----------------------------------------------------- - -void unload_category(v1::ModelCategory category) { - v1::ModelUnloadRequest request; - request.set_category(category); - const std::string bytes = proto::serialize(request); - rac_proto_buffer_t out; - rac_proto_buffer_init(&out); - rac_model_lifecycle_unload_proto(reinterpret_cast(bytes.data()), bytes.size(), - &out); - rac_proto_buffer_free(&out); -} - -double load_model_timed(const std::string& model_id, v1::ModelCategory category, - v1::InferenceFramework framework, std::string* out_error) { - v1::ModelLoadRequest request; - request.set_model_id(model_id); - request.set_category(category); - request.set_validate_availability(true); - // An explicit --engine is honoured whatever the ref resolved to (catalog - // entries included); absent the flag this stays UNSPECIFIED and the model's - // own declared framework is used, exactly as before. Mirrors cmd_run.cpp. - if (framework != v1::INFERENCE_FRAMEWORK_UNSPECIFIED) { - request.set_framework(framework); - } - const std::string bytes = proto::serialize(request); - rac_proto_buffer_t out; - rac_proto_buffer_init(&out); - const int64_t t0 = rac_monotonic_now_ms(); - const rac_result_t rc = rac_model_lifecycle_load_proto( - rac_get_model_registry(), reinterpret_cast(bytes.data()), bytes.size(), - &out); - const int64_t t1 = rac_monotonic_now_ms(); - v1::ModelLoadResult result; - std::string parse_err; - if (rc != RAC_SUCCESS || !proto::parse_proto_buffer(&out, &result, &parse_err)) { - *out_error = parse_err.empty() ? "load failed" : parse_err; - return -1.0; - } - if (!result.has_error() == false) { - *out_error = result.error().message().empty() ? "load failed" : result.error().message(); - return -1.0; - } - return static_cast(t1 - t0); -} - -// --- per-modality inference calls ------------------------------------------ - -bool llm_generate(int32_t max_tokens, bool system_prompt, v1::LLMGenerationResult* out, - std::string* err) { - v1::LLMGenerateRequest request; - v1::ChatMessage* message = request.add_messages(); - message->set_role(v1::MESSAGE_ROLE_USER); - message->set_content(kLlmPrompt); - v1::LLMGenerationOptions* gen = request.mutable_options(); - gen->set_max_output_tokens(max_tokens); - gen->set_temperature(0.0f); - if (system_prompt) { - gen->set_system_prompt(kLlmSystemPrompt); - } - const std::string bytes = proto::serialize(request); - rac_proto_buffer_t buf; - rac_proto_buffer_init(&buf); - const rac_result_t rc = - rac_llm_generate_proto(reinterpret_cast(bytes.data()), bytes.size(), &buf); - if (rc != RAC_SUCCESS || !proto::parse_proto_buffer(&buf, out, err)) { - if (err->empty()) { - *err = rac_error_message(rc); - } - return false; - } - return true; -} - -bool stt_transcribe(const std::string& pcm, v1::STTOutput* out, std::string* err) { - v1::STTTranscriptionRequest request; - v1::STTAudioSource* audio = request.mutable_audio(); - audio->set_audio_data(pcm); - audio->set_encoding(v1::AUDIO_ENCODING_PCM_S16_LE); - audio->set_sample_rate(16000); - audio->set_channels(1); - // bits_per_sample deleted: sample width is determined by `encoding` - // (AUDIO_ENCODING_PCM_S16_LE above already says 16-bit). - v1::STTOptions* opts = request.mutable_options(); - opts->set_language("en"); - const std::string bytes = proto::serialize(request); - rac_proto_buffer_t buf; - rac_proto_buffer_init(&buf); - const rac_result_t rc = rac_stt_transcribe_lifecycle_proto( - reinterpret_cast(bytes.data()), bytes.size(), &buf); - if (rc != RAC_SUCCESS || !proto::parse_proto_buffer(&buf, out, err)) { - if (err->empty()) { - *err = rac_error_message(rc); - } - return false; - } - return true; -} - -bool tts_synthesize(const std::string& text, v1::TTSOutput* out, std::string* err) { - v1::TTSSynthesisRequest request; - request.set_text(text); - v1::TTSOptions* opts = request.mutable_options(); - opts->set_sample_rate(22050); - const std::string bytes = proto::serialize(request); - rac_proto_buffer_t buf; - rac_proto_buffer_init(&buf); - const rac_result_t rc = rac_tts_synthesize_lifecycle_proto( - reinterpret_cast(bytes.data()), bytes.size(), &buf); - if (rc != RAC_SUCCESS || !proto::parse_proto_buffer(&buf, out, err)) { - if (err->empty()) { - *err = rac_error_message(rc); - } - return false; - } - return true; -} - -bool vlm_process(const std::string& image_path, int32_t max_tokens, v1::VLMResult* out, - std::string* err) { - v1::VLMGenerationRequest request; - v1::VLMImage* image = request.add_images(); - image->set_file_path(image_path); - request.set_prompt(kVlmPrompt); - v1::LLMGenerationOptions* gen = request.mutable_options(); - gen->set_max_output_tokens(max_tokens); - gen->set_temperature(0.0f); - const std::string bytes = proto::serialize(request); - rac_proto_buffer_t buf; - rac_proto_buffer_init(&buf); - const rac_result_t rc = - rac_vlm_generate_proto(reinterpret_cast(bytes.data()), bytes.size(), &buf); - if (rc != RAC_SUCCESS || !proto::parse_proto_buffer(&buf, out, err)) { - if (err->empty()) { - *err = rac_error_message(rc); - } - return false; - } - return true; -} - -// --- per-trial runners (one load → warmup → measured pass) ----------------- - -struct TrialCtx { - std::string model_id; - v1::ModelCategory category; - Scenario scenario; - std::string vlm_image; - v1::InferenceFramework framework = v1::INFERENCE_FRAMEWORK_UNSPECIFIED; -}; - -bool llm_trial(const TrialCtx& c, Metrics* m, std::string* err) { - unload_category(c.category); - const int64_t mem_before = available_ram_bytes(); - m->load_ms = load_model_timed(c.model_id, c.category, c.framework, err); - if (m->load_ms < 0.0) { - return false; - } - const int64_t w0 = rac_monotonic_now_ms(); - v1::LLMGenerationResult warm; - if (!llm_generate(5, false, &warm, err)) { - unload_category(c.category); - return false; - } - m->warmup_ms = static_cast(rac_monotonic_now_ms() - w0); - - const int64_t t0 = rac_monotonic_now_ms(); - v1::LLMGenerationResult r; - if (!llm_generate(c.scenario.max_tokens, true, &r, err)) { - unload_category(c.category); - return false; - } - const double measured_e2e = static_cast(rac_monotonic_now_ms() - t0); - m->memory_delta_bytes = mem_before - available_ram_bytes(); - unload_category(c.category); - - bench_metrics::LlmVlmMetrics filled; - if (!bench_metrics::fill_llm(r, measured_e2e, &filled)) { - *err = "no output tokens"; - return false; - } - m->end_to_end_ms = filled.end_to_end_ms; - m->tokens_per_second = filled.tokens_per_second; - m->decode_ms = filled.decode_ms; - m->prompt_eval_ms = filled.prompt_eval_ms; - m->output_tokens = filled.output_tokens; - return true; -} - -bool stt_trial(const TrialCtx& c, Metrics* m, std::string* err) { - unload_category(c.category); - const int64_t mem_before = available_ram_bytes(); - m->load_ms = load_model_timed(c.model_id, c.category, c.framework, err); - if (m->load_ms < 0.0) { - return false; - } - v1::STTOutput warm; - (void)stt_transcribe(make_pcm16(0.5, false), &warm, - err); // warmup, errors ignored - - const int64_t t0 = rac_monotonic_now_ms(); - v1::STTOutput r; - if (!stt_transcribe(make_pcm16(c.scenario.seconds, c.scenario.sine), &r, err)) { - unload_category(c.category); - return false; - } - m->end_to_end_ms = static_cast(rac_monotonic_now_ms() - t0); - m->memory_delta_bytes = mem_before - available_ram_bytes(); - unload_category(c.category); - - if (r.text().empty()) { - *err = "no transcript"; - return false; - } - // RTF is commons-owned; do not derive from wall / scenario / duration. - m->real_time_factor = 0.0; - return true; -} - -bool tts_trial(const TrialCtx& c, Metrics* m, std::string* err) { - unload_category(c.category); - const int64_t mem_before = available_ram_bytes(); - m->load_ms = load_model_timed(c.model_id, c.category, c.framework, err); - if (m->load_ms < 0.0) { - return false; - } - v1::TTSOutput warm; - (void)tts_synthesize("Hi.", &warm, err); // warmup, errors ignored - - const std::string text = c.scenario.text ? c.scenario.text : ""; - const int64_t t0 = rac_monotonic_now_ms(); - v1::TTSOutput r; - if (!tts_synthesize(text, &r, err)) { - unload_category(c.category); - return false; - } - m->end_to_end_ms = static_cast(rac_monotonic_now_ms() - t0); - m->memory_delta_bytes = mem_before - available_ram_bytes(); - unload_category(c.category); - - m->audio_duration_ms = static_cast(r.duration_ms()); - // chars/s is commons-owned; do not derive from wall / input bytes. - m->chars_per_second = 0.0; - return true; -} - -bool vlm_trial(const TrialCtx& c, Metrics* m, std::string* err) { - unload_category(v1::MODEL_CATEGORY_MULTIMODAL); - unload_category(v1::MODEL_CATEGORY_LANGUAGE); - const int64_t mem_before = available_ram_bytes(); - m->load_ms = load_model_timed(c.model_id, c.category, c.framework, err); - if (m->load_ms < 0.0) { - return false; - } - v1::VLMResult warm; - (void)vlm_process(c.vlm_image, 1, &warm, err); // warmup, errors ignored - - const int64_t t0 = rac_monotonic_now_ms(); - v1::VLMResult r; - if (!vlm_process(c.vlm_image, c.scenario.max_tokens, &r, err)) { - unload_category(c.category); - return false; - } - const double measured_e2e = static_cast(rac_monotonic_now_ms() - t0); - m->memory_delta_bytes = mem_before - available_ram_bytes(); - unload_category(c.category); - - bench_metrics::LlmVlmMetrics filled; - if (!bench_metrics::fill_vlm(r, measured_e2e, &filled)) { - *err = "no output tokens"; - return false; - } - m->end_to_end_ms = filled.end_to_end_ms; - m->tokens_per_second = filled.tokens_per_second; - m->decode_ms = filled.decode_ms; - m->prompt_eval_ms = filled.prompt_eval_ms; - m->output_tokens = filled.output_tokens; - return true; -} - -// --- aggregation + report -------------------------------------------------- - -struct BenchRow { - std::string model_id; - Modality modality; - std::string scenario; - bool success = false; - std::string error; - int trials = 0; - Metrics med; -}; - -using TrialFn = std::function; - -BenchRow aggregate(const GlobalOptions& options, const TrialCtx& ctx, Modality modality, int trials, - const TrialFn& trial) { - BenchRow row; - row.model_id = ctx.model_id; - row.modality = modality; - row.scenario = ctx.scenario.label; - row.trials = trials; - - std::vector load, warmup, e2e, tps, prefill, decode, mem, rtf, cps, adur; - std::vector out_tok; - for (int t = 0; t < trials; ++t) { - Metrics m; - std::string err; - if (!trial(ctx, &m, &err)) { - row.error = err; - return row; - } - load.push_back(m.load_ms); - warmup.push_back(m.warmup_ms); - e2e.push_back(m.end_to_end_ms); - tps.push_back(m.tokens_per_second); - prefill.push_back(m.prompt_eval_ms); - decode.push_back(m.decode_ms); - mem.push_back(static_cast(m.memory_delta_bytes)); - rtf.push_back(m.real_time_factor); - cps.push_back(m.chars_per_second); - adur.push_back(m.audio_duration_ms); - out_tok.push_back(m.output_tokens); - if (options.verbose) { - out::status_line(" trial " + std::to_string(t + 1) + "/" + std::to_string(trials) + - " ok"); - } - } - row.success = true; - row.med.load_ms = median(load); - row.med.warmup_ms = median(warmup); - row.med.end_to_end_ms = median(e2e); - row.med.tokens_per_second = median(tps); - row.med.prompt_eval_ms = median(prefill); - row.med.decode_ms = median(decode); - row.med.memory_delta_bytes = static_cast(median(mem)); - row.med.real_time_factor = median(rtf); - row.med.chars_per_second = median(cps); - row.med.audio_duration_ms = median(adur); - row.med.output_tokens = out_tok.empty() ? 0 : out_tok[out_tok.size() / 2]; - return row; -} - -// Modality-specific "primary" throughput/latency string for the report. -std::string primary_metric(const BenchRow& r) { - char buf[64]; - switch (r.modality) { - case Modality::kLlm: - case Modality::kVlm: - std::snprintf(buf, sizeof(buf), "%.1f tok/s %.0fms pf", r.med.tokens_per_second, - r.med.prompt_eval_ms); - break; - case Modality::kStt: - std::snprintf(buf, sizeof(buf), "RTF %.3f (%.0fx rt)", r.med.real_time_factor, - r.med.real_time_factor > 0.0 ? 1.0 / r.med.real_time_factor : 0.0); - break; - case Modality::kTts: - std::snprintf(buf, sizeof(buf), "%.0f chars/s", r.med.chars_per_second); - break; - } - return buf; -} - -// --- enumeration + driver -------------------------------------------------- - -struct BenchModel { - std::string id; - v1::ModelCategory category; - Modality modality; -}; - -bool collect_models(const std::string& only_model, std::vector* out, - std::string* out_error) { - rac_proto_buffer_t buf; - rac_proto_buffer_init(&buf); - if (rac_model_registry_list_downloaded_proto_buffer(rac_get_model_registry(), &buf) != - RAC_SUCCESS) { - *out_error = "failed to list downloaded models"; - return false; - } - v1::ModelInfoList list; - if (!proto::parse_proto_buffer(&buf, &list, out_error)) { - return false; - } - for (const v1::ModelInfo& m : list.models()) { - if (!only_model.empty() && m.id() != only_model) { - continue; - } - const bool builtin = m.framework() == v1::INFERENCE_FRAMEWORK_FOUNDATION_MODELS || - m.framework() == v1::INFERENCE_FRAMEWORK_SYSTEM_TTS; - if (builtin) { - continue; - } - Modality modality; - if (!modality_of(m.category(), &modality)) { - continue; - } - out->push_back({m.id(), m.category(), modality}); - } - return true; -} - -int run_bench(const GlobalOptions& options, const std::string& model_ref_arg, int trials, - const std::string& vlm_image, const std::string& engine) { - Bootstrapped env; - if (bootstrap(options, &env) != RAC_SUCCESS) { - return 1; - } - if (trials < 1) { - trials = 1; - } - - // Parsed once, up front: an explicit --engine both narrows ref resolution and - // pins the framework every trial loads with, whether one model was named or - // the whole registry is being benchmarked. - commands::EngineHintResolution engine_hint; - std::string engine_error; - if (!commands::resolve_engine_hint(engine, &engine_hint, &engine_error)) { - out::error_line(engine_error); - return 2; - } - - // Resolve the argument the same way every other command does, so a local - // bundle directory, an HF ref or a URL all work here too. collect_models - // only ever scans the registry, so without this an unregistered ref — which - // is what a freshly staged bundle on disk is — reported "not a downloaded - // benchmarkable model" even though `rcli run` could load it fine. - std::string only_model = model_ref_arg; - if (!model_ref_arg.empty()) { - model_ref::Resolved resolved; - std::string resolve_error; - if (model_ref::resolve(model_ref_arg, &resolved, &resolve_error, - &engine_hint.resolve_options) != RAC_SUCCESS) { - out::error_line(resolve_error); - return 1; - } - only_model = resolved.model_id; - } - - std::vector models; - std::string error; - if (!collect_models(only_model, &models, &error)) { - out::error_line(error); - return 1; - } - if (models.empty()) { - out::error_line(only_model.empty() - ? "no downloaded models to benchmark (pull one with `rcli pull`)" - : "model '" + only_model + "' is not a downloaded benchmarkable model"); - return 1; - } - - std::vector rows; - for (const BenchModel& model : models) { - for (const Scenario& scenario : scenarios_for(model.modality)) { - out::status_line(std::string("benchmarking ") + modality_label(model.modality) + " " + - model.id + " — " + scenario.label + " (" + std::to_string(trials) + - " trials)"); - TrialCtx ctx{model.id, model.category, scenario, vlm_image, engine_hint.framework}; - TrialFn fn; - switch (model.modality) { - case Modality::kLlm: - fn = llm_trial; - break; - case Modality::kStt: - fn = stt_trial; - break; - case Modality::kTts: - fn = tts_trial; - break; - case Modality::kVlm: - fn = vlm_trial; - break; - } - rows.push_back(aggregate(options, ctx, model.modality, trials, fn)); - } - } - - if (options.json) { - out::JsonWriter json; - json.begin_object().begin_array("results"); - for (const BenchRow& r : rows) { - json.begin_array_object() - .field("model", r.model_id) - .field("modality", modality_label(r.modality)) - .field("scenario", r.scenario) - .field("success", r.success) - .field("trials", static_cast(r.trials)); - if (r.success) { - json.field("tokens_per_second", r.med.tokens_per_second) - .field("prompt_eval_ms", r.med.prompt_eval_ms) - .field("decode_ms", r.med.decode_ms) - .field("end_to_end_ms", r.med.end_to_end_ms) - .field("real_time_factor", r.med.real_time_factor) - .field("chars_per_second", r.med.chars_per_second) - .field("output_tokens", static_cast(r.med.output_tokens)) - .field("load_ms", r.med.load_ms) - .field("memory_delta_bytes", r.med.memory_delta_bytes); - } else { - json.field("error", r.error); - } - json.end_object(); - } - json.end_array().end_object(); - out::result_line(json.str()); - return 0; - } - - out::result_line(""); - out::result_line( - "MODEL MOD SCENARIO " - "PRIMARY LOAD MEMΔ"); - for (const BenchRow& r : rows) { - char line[256]; - if (r.success) { - std::snprintf(line, sizeof(line), "%-30.30s %-4.4s %-15.15s %-22.22s %6.0fms %s", - r.model_id.c_str(), modality_label(r.modality), r.scenario.c_str(), - primary_metric(r).c_str(), r.med.load_ms, - human_bytes(r.med.memory_delta_bytes).c_str()); - } else { - std::snprintf(line, sizeof(line), "%-30.30s %-4.4s %-15.15s FAILED: %s", - r.model_id.c_str(), modality_label(r.modality), r.scenario.c_str(), - r.error.c_str()); - } - out::result_line(line); - } - return 0; -} - -} // namespace - -void register_bench(CLI::App& app, GlobalOptions& options) { - CLI::App* cmd = - app.add_subcommand("bench", "Measure throughput and load time of downloaded models"); - auto model = std::make_shared(); - auto trials = std::make_shared(3); - auto vlm_image = std::make_shared("docs/gifs/npu-model-tag-screenshot.png"); - auto engine = std::make_shared(); - cmd->add_option("model", *model, - "Model id, local bundle path, hf.co/... or URL (default: all " - "downloaded)"); - cmd->add_option("--engine", *engine, - "Engine hint (neurt|coreml|ane, mlx, llamacpp, onnx, sherpa)"); - cmd->add_option("--trials,-n", *trials, "Measured trials per scenario (median reported)") - ->default_val(3); - cmd->add_option("--vlm-image", *vlm_image, "Image file for VLM benchmarking"); - cmd->callback([&options, model, trials, vlm_image, engine]() { - const int exit_code = run_bench(options, *model, *trials, *vlm_image, *engine); - if (exit_code != 0) { - throw CLI::RuntimeError(exit_code); - } - }); -} - -} // namespace rcli::commands diff --git a/rcli/src/commands/cmd_diarize.cpp b/rcli/src/commands/cmd_diarize.cpp deleted file mode 100644 index 8ad021d4c3..0000000000 --- a/rcli/src/commands/cmd_diarize.cpp +++ /dev/null @@ -1,191 +0,0 @@ -/** - * @file cmd_diarize.cpp - * @brief `rcli diarize --model ` — offline speaker - * diarization via the commons diarization service (audio-in → typed - * speaker segments out). - * - * Audio loading mirrors cmd_stt (16-bit PCM WAV → mono 16 kHz float). The model - * lifecycle is the standard service handle sequence the C ABI exposes: - * rac_diarization_create(model) → route to the ONNX Sortformer provider - * → rac_diarization_initialize(model_path) → load the ONNX graph - * → rac_diarization_diarize(samples, …, &result) → typed segments - * → rac_diarization_result_free / rac_diarization_destroy - * A `--model` naming an on-disk path is used verbatim (the provider resolves - * the .onnx inside a directory); otherwise it is treated as a catalog id and - * pulled with the shared ensure-downloaded flow. All heavy lifting (mel - * frontend, streaming state, segmentation) lives in commons/the engine, per - * repo layering. - */ - -#include -#include -#include -#include -#include - -#include "commands/commands.h" -#include "commands/model_setup.h" -#include "io/output.h" -#include "io/wav_io.h" -#include "rac/features/diarization/rac_diarization_service.h" -#include "rac/features/diarization/rac_diarization_types.h" - -namespace rcli::commands { - -namespace { - -constexpr int kDiarizationSampleRate = 16000; - -// Resolve `ref` to a local model path: an existing on-disk path is used as-is; -// otherwise it is a catalog/registry id resolved + auto-pulled through commons -// (same flow as the speech commands). Returns 0 on success, else an exit code. -int resolve_model_path(const GlobalOptions& options, const std::string& ref, - std::string* out_path) { - std::error_code ec; - if (std::filesystem::exists(ref, ec)) { - *out_path = ref; - return 0; - } - ResolvedModelPaths model; - const int setup = ensure_model_ready(options, ref, &model); - if (setup != 0) { - return setup; - } - *out_path = model.primary_path; - return 0; -} - -void print_result(const GlobalOptions& options, const std::string& model_ref, - const rac_diarization_result_t& result) { - if (options.json) { - out::JsonWriter json; - json.begin_object() - .field("model", result.model_id ? result.model_id : model_ref) - .field("speaker_count", static_cast(result.speaker_count)) - .field("segment_count", static_cast(result.segment_count)) - .field("audio_duration_ms", static_cast(result.audio_duration_ms)) - .field("processing_time_ms", static_cast(result.processing_time_ms)); - json.begin_array("segments"); - for (size_t i = 0; i < result.segment_count; ++i) { - const rac_diarization_segment_t& seg = result.segments[i]; - json.begin_array_object() - .field("speaker", seg.speaker_id ? seg.speaker_id : "") - .field("speaker_index", static_cast(seg.speaker_index)) - .field("start_ms", static_cast(seg.start_ms)) - .field("end_ms", static_cast(seg.end_ms)) - .end_object(); - } - json.end_array().end_object(); - out::result_line(json.str()); - return; - } - - if (result.segment_count == 0) { - out::result_line("(no speech segments detected)"); - } else { - std::vector> rows; - rows.reserve(result.segment_count); - for (size_t i = 0; i < result.segment_count; ++i) { - const rac_diarization_segment_t& seg = result.segments[i]; - rows.push_back({seg.speaker_id ? seg.speaker_id : std::to_string(seg.speaker_index), - std::to_string(seg.start_ms) + " ms", - std::to_string(seg.end_ms) + " ms", - std::to_string(seg.end_ms - seg.start_ms) + " ms"}); - } - out::table({"speaker", "start", "end", "duration"}, rows); - } - if (options.verbose) { - out::status_line("(" + std::to_string(result.speaker_count) + " speakers, " + - std::to_string(result.processing_time_ms) + " ms)"); - } -} - -int run_diarize(const GlobalOptions& options, const std::string& audio_path, - const std::string& model_ref, const rac_diarization_options_t& diar_options) { - Bootstrapped env; - if (bootstrap(options, &env) != RAC_SUCCESS) { - return 1; - } - - if (model_ref.empty()) { - out::error_line("--model is required (a diarization model id or on-disk path)"); - return 2; - } - - std::string model_path; - const int resolve = resolve_model_path(options, model_ref, &model_path); - if (resolve != 0) { - return resolve; - } - - // Load audio → mono 16 kHz float, mirroring cmd_stt. - wav::WavData audio; - std::string error; - if (!wav::read_wav(audio_path, &audio, &error)) { - out::error_line(error); - return 1; - } - const std::vector pcm16 = - wav::resample(audio.samples, audio.sample_rate, kDiarizationSampleRate); - const std::vector pcm = wav::to_float(pcm16); - if (pcm.empty()) { - out::error_line("no audio samples in " + audio_path); - return 1; - } - - rac_handle_t handle = nullptr; - rac_result_t rc = rac_diarization_create(model_path.c_str(), &handle); - if (rc != RAC_SUCCESS || handle == nullptr) { - out::error_line("failed to create diarization service: " + out::describe_result(rc)); - return 1; - } - - rc = rac_diarization_initialize(handle, model_path.c_str()); - if (rc != RAC_SUCCESS) { - out::error_line("failed to load diarization model: " + out::describe_result(rc)); - rac_diarization_destroy(handle); - return 1; - } - - rac_diarization_result_t result = {}; - rc = rac_diarization_diarize(handle, pcm.data(), pcm.size(), &diar_options, &result); - if (rc != RAC_SUCCESS) { - out::error_line("diarization failed: " + out::describe_result(rc)); - rac_diarization_result_free(&result); - rac_diarization_cleanup(handle); - rac_diarization_destroy(handle); - return 1; - } - - print_result(options, model_ref, result); - - rac_diarization_result_free(&result); - rac_diarization_cleanup(handle); - rac_diarization_destroy(handle); - return 0; -} - -} // namespace - -void register_diarize(CLI::App& app, GlobalOptions& options) { - CLI::App* cmd = app.add_subcommand("diarize", "Label who spoke when in an audio file"); - auto audio = std::make_shared(); - auto model = std::make_shared(); - auto diar = std::make_shared(RAC_DIARIZATION_OPTIONS_DEFAULT); - cmd->add_option("audio", *audio, "16-bit PCM WAV file")->required()->check(CLI::ExistingFile); - cmd->add_option("--model,-m", *model, "Diarization model id or on-disk path")->required(); - cmd->add_option("--threshold", diar->threshold, - "Speaker activity needed to open a segment, in [0,1] (default 0.5)"); - cmd->add_option("--minimum-duration-ms,--min-duration", diar->minimum_duration_ms, - "Drop segments shorter than this many ms (default 0)"); - cmd->add_option("--merge-gap-ms,--merge-gap", diar->merge_gap_ms, - "Merge same-speaker segments closer than this many ms (default 0)"); - cmd->callback([&options, audio, model, diar]() { - const int exit_code = run_diarize(options, *audio, *model, *diar); - if (exit_code != 0) { - throw CLI::RuntimeError(exit_code); - } - }); -} - -} // namespace rcli::commands diff --git a/rcli/src/commands/cmd_embed.cpp b/rcli/src/commands/cmd_embed.cpp deleted file mode 100644 index 3554f0313f..0000000000 --- a/rcli/src/commands/cmd_embed.cpp +++ /dev/null @@ -1,274 +0,0 @@ -/** - * @file cmd_embed.cpp - * @brief `rcli embed [input]` — text embeddings via the commons lifecycle path. - */ - -#include "commands/commands.h" - -#include -#include -#include -#include -#include -#include - -#include "embeddings_options.pb.h" -#include "model_types.pb.h" -#include "rac/core/rac_core.h" -#include "rac/core/rac_model_lifecycle.h" -#include "rac/features/embeddings/rac_embeddings_service.h" - -#include "catalog/model_ref.h" -#include "commands/engine_options.h" -#include "io/output.h" -#include "io/proto.h" -#include "progress/progress_bar.h" - -namespace rcli::commands { - -namespace { - -constexpr const char* kDefaultEmbeddingModel = "minilm"; - -namespace v1 = runanywhere::v1; - -std::string preview_values(const v1::EmbeddingVector& vector) { - std::ostringstream out; - out << std::fixed << std::setprecision(5); - const int count = std::min(vector.values_size(), 8); - for (int i = 0; i < count; ++i) { - if (i > 0) { - out << ','; - } - out << vector.values(i); - } - return out.str(); -} - -void print_json_result(const std::string& model_id, const std::vector& texts, - const v1::EmbeddingsResult& result) { - out::JsonWriter json; - json.begin_object() - .field("model", result.has_model_id() && !result.model_id().empty() ? result.model_id() - : model_id) - .field("dimension", static_cast(result.dimension())) - .field("count", static_cast(result.vectors_size())) - .field("tokens_used", static_cast(result.tokens_used())) - .field("total_ms", static_cast(result.processing_time_ms())); - json.begin_array("vectors"); - // EmbeddingVector.text/dimension are gone: text is looked up by - // input_index (the batch position this vector answers, always set) and - // dimension is the one shared EmbeddingsResult.dimension above. - for (int i = 0; i < result.vectors_size(); ++i) { - const auto& vector = result.vectors(i); - const size_t index = static_cast(vector.input_index()); - const std::string text = index < texts.size() ? texts[index] : std::string(); - json.begin_array_object() - .field("text", text) - .field("dimension", static_cast(result.dimension())); - json.begin_array("values"); - for (const float value : vector.values()) { - json.value(static_cast(value)); - } - json.end_array().end_object(); - } - json.end_array().end_object(); - out::result_line(json.str()); -} - -void print_text_result(const std::string& model_id, const v1::EmbeddingsResult& result, - bool verbose) { - out::result_line("model\t" + model_id); - out::result_line("dimension\t" + std::to_string(result.dimension())); - out::result_line("count\t" + std::to_string(result.vectors_size())); - for (int i = 0; i < result.vectors_size(); ++i) { - out::result_line("vector[" + std::to_string(i) + "]\t" + preview_values(result.vectors(i))); - } - if (verbose) { - out::status_line("(" + std::to_string(result.processing_time_ms()) + " ms)"); - } -} - -bool load_embeddings_model(const GlobalOptions& options, const std::string& model_id, - v1::InferenceFramework framework) { - progress::DownloadProgressScope progress_scope(model_id, !options.no_progress && !options.json); - v1::ModelLoadRequest request; - request.set_model_id(model_id); - request.set_category(v1::MODEL_CATEGORY_EMBEDDING); - request.set_validate_availability(true); - if (framework != v1::INFERENCE_FRAMEWORK_UNSPECIFIED) { - request.set_framework(framework); - } - - const std::string bytes = proto::serialize(request); - rac_proto_buffer_t out_buffer; - rac_proto_buffer_init(&out_buffer); - std::string error; - v1::ModelLoadResult result; - if (rac_model_lifecycle_load_proto(rac_get_model_registry(), - reinterpret_cast(bytes.data()), - bytes.size(), &out_buffer) != RAC_SUCCESS || - !proto::parse_proto_buffer(&out_buffer, &result, &error)) { - out::error_line("embedding model load failed: " + error); - return false; - } - if (!result.has_error() == false) { - out::error_line("embedding model load failed: " + - (result.error().message().empty() ? "unknown error" - : result.error().message())); - return false; - } - if (options.verbose) { - out::status_line("loaded " + result.resolved_path()); - } - return true; -} - -bool parse_normalize(const std::string& mode, bool* out, bool* has_value) { - *has_value = !mode.empty(); - if (mode.empty()) { - *out = false; - } else if (mode == "l2") { - *out = true; - } else if (mode == "none") { - *out = false; - } else { - return false; - } - return true; -} - -bool parse_pooling(const std::string& mode, v1::EmbeddingsPoolingStrategy* out) { - if (mode.empty()) { - *out = v1::EMBEDDINGS_POOLING_STRATEGY_UNSPECIFIED; - } else if (mode == "mean") { - *out = v1::EMBEDDINGS_POOLING_STRATEGY_MEAN; - } else if (mode == "cls") { - *out = v1::EMBEDDINGS_POOLING_STRATEGY_CLS; - } else if (mode == "last") { - *out = v1::EMBEDDINGS_POOLING_STRATEGY_LAST; - } else { - return false; - } - return true; -} - -int run_embed(const GlobalOptions& options, const std::string& ref, const std::string& engine, - const std::vector& texts, const std::string& normalize, - const std::string& pooling) { - Bootstrapped env; - if (bootstrap(options, &env) != RAC_SUCCESS) { - return 1; - } - - if (texts.empty()) { - out::error_line("at least one text input is required"); - return 2; - } - - EngineHintResolution engine_hint; - std::string engine_error; - if (!resolve_engine_hint(engine, &engine_hint, &engine_error)) { - out::error_line(engine_error); - return 2; - } - engine_hint.resolve_options.has_category = true; - engine_hint.resolve_options.category = v1::MODEL_CATEGORY_EMBEDDING; - - model_ref::Resolved resolved; - std::string error; - const std::string selected_ref = ref.empty() ? kDefaultEmbeddingModel : ref; - if (model_ref::resolve(selected_ref, &resolved, &error, &engine_hint.resolve_options) != - RAC_SUCCESS) { - out::error_line(error); - return 1; - } - - // An explicit --engine is honoured whatever the ref resolved to. This used to - // read `resolved.from_catalog ? UNSPECIFIED : engine_hint.framework`, which - // silently DISCARDED the flag for built-in catalog entries — contradicting the - // `--engine` help text. When the flag is absent engine_hint.framework is - // UNSPECIFIED, so catalog entries still fall back to their own declared - // framework exactly as before. Mirrors cmd_run.cpp. - if (!load_embeddings_model(options, resolved.model_id, engine_hint.framework)) { - return 1; - } - - v1::EmbeddingsRequest request; - request.set_model_id(resolved.model_id); - for (const auto& text : texts) { - request.add_texts(text); - } - bool normalize_value = false; - bool normalize_set = false; - v1::EmbeddingsPoolingStrategy pooling_strategy; - if (!parse_normalize(normalize, &normalize_value, &normalize_set) || - !parse_pooling(pooling, &pooling_strategy)) { - out::error_line("--normalize expects l2|none and --pooling expects mean|cls|last"); - return 2; - } - if (normalize_set) { - request.mutable_options()->set_normalize(normalize_value); - } - if (pooling_strategy != v1::EMBEDDINGS_POOLING_STRATEGY_UNSPECIFIED) { - request.mutable_options()->set_pooling(pooling_strategy); - } - - const std::string bytes = proto::serialize(request); - rac_proto_buffer_t out_buffer; - rac_proto_buffer_init(&out_buffer); - v1::EmbeddingsResult result; - // EmbeddingsResult carries no error field: failures travel out-of-band on - // the rac_proto_buffer_t status envelope, already checked here. - if (rac_embeddings_embed_batch_lifecycle_proto(reinterpret_cast(bytes.data()), - bytes.size(), &out_buffer) != RAC_SUCCESS || - !proto::parse_proto_buffer(&out_buffer, &result, &error)) { - out::error_line("embedding failed: " + error); - return 1; - } - if (options.json) { - print_json_result(resolved.model_id, texts, result); - } else { - print_text_result(resolved.model_id, result, options.verbose); - } - return 0; -} - -} // namespace - -void register_embed(CLI::App& app, GlobalOptions& options) { - CLI::App* cmd = app.add_subcommand("embed", "Turn text into embedding vectors"); - auto model = std::make_shared(kDefaultEmbeddingModel); - auto engine = std::make_shared(); - auto positional_text = std::make_shared(); - auto option_texts = std::make_shared>(); - auto normalize = std::make_shared(); - auto pooling = std::make_shared(); - cmd->add_option("input", *positional_text, "Text to embed"); - cmd->add_option("--model,-m", *model, - "Embedding model to use (default: " + std::string(kDefaultEmbeddingModel) + ")") - ->default_val(kDefaultEmbeddingModel); - cmd->add_option("--engine", *engine, - "Engine hint (neurt|coreml|ane, mlx, llamacpp, onnx, sherpa). Honoured for " - "catalog models too, not just URL/HF refs."); - cmd->add_option("--text,-t", *option_texts, - "Embed this text too; repeat to batch several"); - cmd->add_option("--normalize", *normalize, "Scale vectors to unit length or leave them raw") - ->check(CLI::IsMember({"l2", "none"})); - cmd->add_option("--pooling", *pooling, "Collapse token vectors with this strategy") - ->check(CLI::IsMember({"mean", "cls", "last"})); - cmd->callback([&options, model, engine, positional_text, option_texts, normalize, pooling]() { - std::vector texts; - if (!positional_text->empty()) { - texts.push_back(*positional_text); - } - texts.insert(texts.end(), option_texts->begin(), option_texts->end()); - const int exit_code = - run_embed(options, *model, *engine, texts, *normalize, *pooling); - if (exit_code != 0) { - throw CLI::RuntimeError(exit_code); - } - }); -} - -} // namespace rcli::commands diff --git a/rcli/src/commands/cmd_image.cpp b/rcli/src/commands/cmd_image.cpp deleted file mode 100644 index 7dfd3c1fa4..0000000000 --- a/rcli/src/commands/cmd_image.cpp +++ /dev/null @@ -1,313 +0,0 @@ -/** - * @file cmd_image.cpp - * @brief `rcli image generate` — text-to-image via the CoreML diffusion engine. - * - * Canonical SDK flow, all heavy lifting in commons (mirrors cmd_run/cmd_embed): - * rac_model_lifecycle_load_proto(category=IMAGE_GENERATION, validate=true) - * → auto-pulls / resolves the diffusion bundle and loads the coreml engine. - * rac_diffusion_generate_lifecycle_proto(DiffusionGenerationRequest) - * → resolves the lifecycle-loaded model internally and returns a - * DiffusionResult (raw RGBA image_data + width/height). - * The command only translates argv ↔ proto bytes and writes the decoded image - * to --out as PNG. Diffusion is Apple/CoreML-only; on other platforms the - * command returns a clear error rather than crashing. - * - * A `--model` that names an existing on-disk path is registered as a local - * CoreML bundle (directory of compiled .mlmodelc sub-models) and loaded without - * a download — the reliable path for a pre-fetched Apple SD bundle. - */ - -#include "commands/commands.h" - -#include -#include - -#include "io/output.h" - -#if defined(RCLI_HAS_NEURT) -#include -#include -#include - -#include "diffusion_options.pb.h" -#include "model_types.pb.h" -#include "rac/core/rac_core.h" -#include "rac/core/rac_model_lifecycle.h" -#include "rac/features/diffusion/rac_diffusion_service.h" -#include "rac/infrastructure/model_management/rac_model_registry.h" - -#include "catalog/model_ref.h" -#include "io/image_io.h" -#include "io/proto.h" -#include "progress/progress_bar.h" -#endif - -namespace rcli::commands { - -namespace { - -// Default diffusion model — the built-in CoreML Stable Diffusion 1.5 catalog id -// (see catalog.cpp). Overridable with --model (catalog id / registered id / a -// local path to a compiled CoreML bundle). -constexpr const char* kDefaultDiffusionModel = "stable-diffusion-v1-5-coreml"; - -struct ImageParams { - std::string model = kDefaultDiffusionModel; - std::string prompt; - std::string negative_prompt; - std::string out_path; - int32_t steps = 0; // 0 = model/variant default - float guidance = 0.0f; // 0 = model/variant default - int64_t seed = -1; // -1 = random -}; - -#if defined(RCLI_HAS_NEURT) - -namespace v1 = runanywhere::v1; - -bool path_exists(const std::string& path) { - struct stat st {}; - return ::stat(path.c_str(), &st) == 0; -} - -// Register an already-present local CoreML bundle so the lifecycle loader can -// resolve it by id (no download). Directory resolution is a CLI concern. -bool register_local_bundle(const std::string& path, std::string* out_id, std::string* error) { - const std::string model_id = "local-diffusion-coreml"; - v1::ModelInfo model; - model.set_id(model_id); - model.set_name("Local CoreML Diffusion"); - model.set_category(v1::MODEL_CATEGORY_IMAGE_GENERATION); - model.set_framework(v1::INFERENCE_FRAMEWORK_COREML); - model.set_format(v1::MODEL_FORMAT_MLPACKAGE); - model.set_local_path(path); - model.set_source(v1::MODEL_SOURCE_LOCAL); - - const std::string bytes = proto::serialize(model); - const rac_result_t rc = rac_model_registry_register_proto( - rac_get_model_registry(), reinterpret_cast(bytes.data()), bytes.size()); - if (rc != RAC_SUCCESS) { - if (error) { - *error = "failed to register local model: " + out::describe_result(rc); - } - return false; - } - *out_id = model_id; - return true; -} - -bool load_diffusion_model(const GlobalOptions& options, const std::string& model_id, - bool validate_availability) { - progress::DownloadProgressScope progress_scope(model_id, !options.no_progress && !options.json); - v1::ModelLoadRequest request; - request.set_model_id(model_id); - request.set_category(v1::MODEL_CATEGORY_IMAGE_GENERATION); - request.set_validate_availability(validate_availability); - - const std::string bytes = proto::serialize(request); - rac_proto_buffer_t out_buffer; - rac_proto_buffer_init(&out_buffer); - std::string error; - v1::ModelLoadResult result; - if (rac_model_lifecycle_load_proto(rac_get_model_registry(), - reinterpret_cast(bytes.data()), bytes.size(), - &out_buffer) != RAC_SUCCESS || - !proto::parse_proto_buffer(&out_buffer, &result, &error)) { - out::error_line("diffusion model load failed: " + error); - return false; - } - if (result.has_error()) { - out::error_line("diffusion model load failed: " + - (result.error().message().empty() ? "unknown error" - : result.error().message())); - return false; - } - if (options.verbose) { - out::status_line("loaded " + result.resolved_path()); - } - return true; -} - -// image_data/image_media_type/width/height/seed_used/error moved off -// DiffusionResult onto DiffusionResult.images[0] (a DiffusionImage) -- -// commons emits exactly one entry until the C ABI grows a list. Errors now -// travel out-of-band via the rac_proto_buffer_t status envelope (checked by -// proto::parse_proto_buffer before this function is ever called). -bool write_image(const v1::DiffusionImage& image_result, const std::string& out_path, - std::string* error) { - const std::string& image = image_result.data(); - if (image.empty()) { - if (error) { - *error = "engine returned no image data"; - } - return false; - } - // Every shipped C-ABI diffusion engine emits raw RGBA (media_type - // "image/raw-rgba"); encode it to PNG. A future backend returning an - // encoded container writes through verbatim. - const bool is_raw_rgba = - image_result.media_type() == "image/raw-rgba" || - (image_result.width() > 0 && image_result.height() > 0 && - image.size() == static_cast(image_result.width()) * image_result.height() * 4); - if (is_raw_rgba) { - return image::write_png(out_path, reinterpret_cast(image.data()), - image_result.width(), image_result.height(), error); - } - std::ofstream file(out_path, std::ios::binary); - file.write(image.data(), static_cast(image.size())); - if (!file.good()) { - if (error) { - *error = "cannot write " + out_path; - } - return false; - } - return true; -} - -#endif // RCLI_HAS_NEURT - -int run_image_generate(const GlobalOptions& options, const ImageParams& params) { - Bootstrapped env; - if (bootstrap(options, &env) != RAC_SUCCESS) { - return 1; - } - -#if !defined(RCLI_HAS_NEURT) - (void)params; - out::error_line("image generation (diffusion) is only supported on Apple/CoreML platforms"); - return 1; -#else - if (params.prompt.empty()) { - out::error_line("--prompt is required"); - return 2; - } - if (params.out_path.empty()) { - out::error_line("--out is required"); - return 2; - } - - // Resolve the model: an existing on-disk path is a local CoreML bundle; - // otherwise fall through to the catalog / registry / URL resolver. - std::string model_id; - bool validate_availability = true; - std::string error; - if (path_exists(params.model)) { - if (!register_local_bundle(params.model, &model_id, &error)) { - out::error_line(error); - return 1; - } - validate_availability = false; // already on disk - } else { - model_ref::ResolveOptions resolve_options; - resolve_options.has_category = true; - resolve_options.category = v1::MODEL_CATEGORY_IMAGE_GENERATION; - resolve_options.has_framework = true; - resolve_options.framework = v1::INFERENCE_FRAMEWORK_COREML; - model_ref::Resolved resolved; - if (model_ref::resolve(params.model, &resolved, &error, &resolve_options) != RAC_SUCCESS) { - out::error_line(error); - return 1; - } - model_id = resolved.model_id; - } - - if (!load_diffusion_model(options, model_id, validate_availability)) { - return 1; - } - - v1::DiffusionGenerationRequest request; - request.set_model_id(model_id); - v1::DiffusionGenerationOptions* gen = request.mutable_options(); - gen->set_prompt(params.prompt); - if (!params.negative_prompt.empty()) { - gen->set_negative_prompt(params.negative_prompt); - } - if (params.steps > 0) { - gen->set_steps(params.steps); - } - if (params.guidance > 0.0f) { - gen->set_guidance_scale(params.guidance); - } - gen->set_seed(params.seed); - - const std::string bytes = proto::serialize(request); - rac_proto_buffer_t out_buffer; - rac_proto_buffer_init(&out_buffer); - v1::DiffusionResult result; - if (rac_diffusion_generate_lifecycle_proto(reinterpret_cast(bytes.data()), - bytes.size(), &out_buffer) != RAC_SUCCESS || - !proto::parse_proto_buffer(&out_buffer, &result, &error)) { - out::error_line("image generation failed: " + error); - return 1; - } - // DiffusionResult carries no error field of its own any more -- failures - // travel out-of-band on the rac_proto_buffer_t status envelope, already - // checked above by parse_proto_buffer. commons emits exactly one image. - if (result.images_size() == 0) { - out::error_line("image generation failed: engine returned no image"); - return 1; - } - const v1::DiffusionImage& image_result = result.images(0); - - if (!write_image(image_result, params.out_path, &error)) { - out::error_line("failed to write image: " + error); - return 1; - } - - if (options.json) { - out::JsonWriter json; - json.begin_object() - .field("model", model_id) - .field("output", params.out_path) - .field("width", static_cast(image_result.width())) - .field("height", static_cast(image_result.height())) - .field("seed", static_cast(image_result.seed_used())) - .field("total_ms", static_cast(result.total_time_ms())) - .end_object(); - out::result_line(json.str()); - } else { - out::result_line(params.out_path); - if (options.verbose) { - out::status_line("(" + std::to_string(image_result.width()) + "x" + - std::to_string(image_result.height()) + ", seed " + - std::to_string(image_result.seed_used()) + ", " + - std::to_string(result.total_time_ms()) + " ms)"); - } - } - return 0; -#endif -} - -} // namespace - -void register_image(CLI::App& app, GlobalOptions& options) { - CLI::App* cmd = - app.add_subcommand("image", "Make images from text (CoreML diffusion, Apple only)"); - cmd->require_subcommand(1); - - CLI::App* generate = cmd->add_subcommand("generate", "Render an image from a prompt"); - auto params = std::make_shared(); - generate - ->add_option("--model,-m", params->model, - "Diffusion model id, a registered id, or a local CoreML bundle path " - "(default: " + - std::string(kDefaultDiffusionModel) + ")") - ->default_val(kDefaultDiffusionModel); - generate->add_option("--prompt,-p", params->prompt, "What to draw")->required(); - generate->add_option("--negative-prompt,--negative", params->negative_prompt, - "What to keep out of the image"); - generate->add_option("--steps", params->steps, "Denoising steps to run (0 = model default)"); - generate->add_option("--guidance-scale,--guidance", params->guidance, - "How closely to follow the prompt (0 = model default)"); - generate->add_option("--seed", params->seed, - "Fix the RNG for a repeatable image (-1 = random)"); - generate->add_option("--output,-o,--out", params->out_path, "PNG file to write")->required(); - generate->callback([&options, params]() { - const int exit_code = run_image_generate(options, *params); - if (exit_code != 0) { - throw CLI::RuntimeError(exit_code); - } - }); -} - -} // namespace rcli::commands diff --git a/rcli/src/commands/cmd_info.cpp b/rcli/src/commands/cmd_info.cpp deleted file mode 100644 index 6e52cb2c13..0000000000 --- a/rcli/src/commands/cmd_info.cpp +++ /dev/null @@ -1,83 +0,0 @@ -/** - * @file cmd_info.cpp - * @brief `rcli info` — environment summary (versions, paths, memory, plugins). - */ - -#include "commands/commands.h" - -#include - -#include "rac/core/rac_core.h" -#include "rac/core/rac_platform_adapter.h" -#include "rac/plugin/rac_plugin_entry.h" - -#include "config/cli_paths.h" -#include "io/output.h" - -#ifndef RCLI_VERSION -#define RCLI_VERSION "0.0.0-dev" -#endif - -namespace rcli::commands { - -void register_info(CLI::App& app, GlobalOptions& options) { - CLI::App* cmd = app.add_subcommand("info", "Report versions, paths, memory and backends"); - cmd->alias("doctor"); - cmd->callback([&options]() { - Bootstrapped env; - if (bootstrap(options, &env) != RAC_SUCCESS) { - throw CLI::RuntimeError(1); - } - - const rac_version_t commons = rac_get_version(); - const std::string commons_version = commons.string ? commons.string : "unknown"; - - rac_memory_info_t memory{}; - bool memory_ok = false; - if (const rac_platform_adapter_t* adapter = rac_get_platform_adapter()) { - memory_ok = adapter->get_memory_info && - adapter->get_memory_info(&memory, adapter->user_data) == RAC_SUCCESS; - } - -#if defined(__APPLE__) - const char* platform = "macos"; -#elif defined(__linux__) - const char* platform = "linux"; -#else - const char* platform = "unknown"; -#endif - - if (options.json) { - out::JsonWriter json; - json.begin_object() - .field("rcli", RCLI_VERSION) - .field("commons", commons_version) - .field("platform", platform) - .field("home", env.home) - .field("models_dir", env.models_dir) - .field("state_dir", paths::state_dir()) - .field("backends", static_cast(rac_plugin_count())); - if (memory_ok) { - json.field("memory_total_bytes", static_cast(memory.total_bytes)) - .field("memory_available_bytes", - static_cast(memory.available_bytes)); - } - json.end_object(); - out::result_line(json.str()); - return; - } - - out::result_line("rcli " RCLI_VERSION); - out::result_line("commons " + commons_version); - out::result_line("platform " + std::string(platform)); - out::result_line("home " + env.home); - out::result_line("models " + env.models_dir); - out::result_line("backends " + std::to_string(rac_plugin_count())); - if (memory_ok) { - out::result_line("memory " + out::human_bytes(memory.available_bytes) + - " available of " + out::human_bytes(memory.total_bytes)); - } - }); -} - -} // namespace rcli::commands diff --git a/rcli/src/commands/cmd_list.cpp b/rcli/src/commands/cmd_list.cpp deleted file mode 100644 index dc1ba1d906..0000000000 --- a/rcli/src/commands/cmd_list.cpp +++ /dev/null @@ -1,132 +0,0 @@ -/** - * @file cmd_list.cpp - * @brief `rcli models list` (alias `rcli list`) — downloaded models by - * default, the whole catalog with --all. - * - * The registry is refreshed with rescan_local so on-disk artifacts pulled by - * previous runs (or by the test rig / playground tooling) are linked before - * listing. - */ - -#include "commands/commands.h" - -#include -#include -#include -#include - -#include "model_types.pb.h" -#include "rac/core/rac_core.h" -#include "rac/infrastructure/model_management/rac_model_registry.h" - -#include "commands/model_setup.h" -#include "commands/model_labels.h" -#include "io/output.h" -#include "io/proto.h" - -namespace rcli::commands { - -namespace { - -namespace v1 = runanywhere::v1; - -int run_list(const GlobalOptions& options, bool show_all) { - Bootstrapped env; - if (bootstrap(options, &env) != RAC_SUCCESS) { - return 1; - } - - std::string error; - if (!refresh_registry(&error)) { - out::status_line("warning: registry refresh failed: " + error); - } - - // Full list + downloaded list; membership marks the DOWNLOADED column. - rac_proto_buffer_t all_out; - rac_proto_buffer_init(&all_out); - v1::ModelInfoList all_models; - if (rac_model_registry_list_proto_buffer(rac_get_model_registry(), &all_out) != RAC_SUCCESS || - !proto::parse_proto_buffer(&all_out, &all_models, &error)) { - out::error_line("failed to list models: " + error); - return 1; - } - - std::set downloaded_ids; - { - rac_proto_buffer_t downloaded_out; - rac_proto_buffer_init(&downloaded_out); - v1::ModelInfoList downloaded; - if (rac_model_registry_list_downloaded_proto_buffer(rac_get_model_registry(), - &downloaded_out) == RAC_SUCCESS && - proto::parse_proto_buffer(&downloaded_out, &downloaded, nullptr)) { - for (const v1::ModelInfo& model : downloaded.models()) { - downloaded_ids.insert(model.id()); - } - } - } - - if (options.json) { - out::JsonWriter json; - json.begin_object().begin_array("models"); - for (const v1::ModelInfo& model : all_models.models()) { - const bool is_downloaded = - downloaded_ids.count(model.id()) > 0 || - model.registry_status() == v1::MODEL_REGISTRY_STATUS_DOWNLOADED; - if (!show_all && !is_downloaded) { - continue; - } - json.begin_array_object() - .field("id", model.id()) - .field("name", model.name()) - .field("modality", model_labels::category(model.category())) - .field("backend", model_labels::backend(model.framework())) - .field("size_bytes", static_cast(model.download_size_bytes())) - .field("downloaded", is_downloaded) - .field("local_path", model.local_path()) - .end_object(); - } - json.end_array().end_object(); - out::result_line(json.str()); - return 0; - } - - std::vector> rows; - for (const v1::ModelInfo& model : all_models.models()) { - const bool is_downloaded = - downloaded_ids.count(model.id()) > 0 || - model.registry_status() == v1::MODEL_REGISTRY_STATUS_DOWNLOADED; - if (!show_all && !is_downloaded) { - continue; - } - rows.push_back({model.id(), model_labels::category(model.category()), - model_labels::backend(model.framework()), - model.download_size_bytes() > 0 - ? out::human_bytes(static_cast(model.download_size_bytes())) - : "-", - is_downloaded ? "yes" : "no"}); - } - - if (rows.empty()) { - out::result_line(show_all ? "no models registered" - : "no models downloaded — try `rcli list --all` then " - "`rcli pull `"); - return 0; - } - out::table({"ID", "MODALITY", "BACKEND", "SIZE", "DOWNLOADED"}, rows); - return 0; -} - -} // namespace - -void configure_models_list(CLI::App* cmd, GlobalOptions& options) { - auto show_all = std::make_shared(false); - cmd->add_flag("--all,-a", *show_all, "Include catalog models that are not downloaded"); - cmd->callback([&options, show_all]() { - const int exit_code = run_list(options, *show_all); - if (exit_code != 0) { - throw CLI::RuntimeError(exit_code); - } - }); -} - -} // namespace rcli::commands diff --git a/rcli/src/commands/cmd_lora.cpp b/rcli/src/commands/cmd_lora.cpp deleted file mode 100644 index c2eb7fd225..0000000000 --- a/rcli/src/commands/cmd_lora.cpp +++ /dev/null @@ -1,336 +0,0 @@ -/** - * @file cmd_lora.cpp - * @brief `rcli lora apply|remove|list|catalog` — LoRA adapters on the loaded - * LLM. - * - * `list` reports the adapters currently attached (rac_lora_state_proto) — the - * spec's LoraState — while `catalog` lists registered adapter metadata. This - * file only translates argv <-> proto bytes, per the repo layering rule. - * - * `import` is retired: idl/lora_options.proto deleted - * LoraAdapterImportRequest/Result outright, and commons permanently stubs - * rac_lora_adapter_import_proto to RAC_ERROR_NOT_IMPLEMENTED (see - * src/infrastructure/model_management/lora_import.cpp). No replacement verb - * exists yet. - */ - -#include "commands/commands.h" - -#include -#include -#include - -#include "lora_options.pb.h" -#include "model_types.pb.h" -#include "rac/core/rac_core.h" -#include "rac/core/rac_model_lifecycle.h" -#include "rac/features/lora/rac_lora_service.h" - -#include "io/output.h" -#include "io/proto.h" - -namespace rcli::commands { - -namespace { - -namespace v1 = runanywhere::v1; - -rac_lora_registry_handle_t require_lora_registry() { - rac_lora_registry_handle_t registry = rac_get_lora_registry(); - if (!registry) { - out::error_line("LoRA registry unavailable (SDK not initialized)"); - } - return registry; -} - -int run_lora_catalog(const GlobalOptions &options) { - Bootstrapped env; - if (bootstrap(options, &env) != RAC_SUCCESS) { - return 1; - } - rac_lora_registry_handle_t registry = require_lora_registry(); - if (!registry) { - return 1; - } - - v1::LoraAdapterCatalogListRequest request; - const std::string request_bytes = proto::serialize(request); - - rac_proto_buffer_t out_buffer; - rac_proto_buffer_init(&out_buffer); - const rac_result_t rc = rac_lora_catalog_list_proto( - registry, reinterpret_cast(request_bytes.data()), - request_bytes.size(), &out_buffer); - v1::LoraAdapterCatalogListResult result; - std::string error; - if (!proto::parse_proto_buffer(&out_buffer, &result, &error) || - rc != RAC_SUCCESS || !result.has_error() == false) { - out::error_line("list failed: " + - (error.empty() ? result.error().message() : error)); - return 1; - } - - if (options.json) { - out::JsonWriter json; - json.begin_object() - .field("count", static_cast(result.entries_size())) - .begin_array("entries"); - for (const v1::LoraAdapterCatalogEntry &entry : result.entries()) { - const bool downloaded = !entry.local_path().empty(); - json.begin_array_object() - .field("id", entry.id()) - .field("name", entry.name()) - .field("downloaded", downloaded) - .field("local_path", entry.local_path()) - .end_object(); - } - json.end_array().end_object(); - out::result_line(json.str()); - return 0; - } - if (result.entries_size() == 0) { - out::result_line("no LoRA adapters registered"); - return 0; - } - for (const v1::LoraAdapterCatalogEntry &entry : result.entries()) { - const bool downloaded = !entry.local_path().empty(); - out::result_line(entry.id() + " " + entry.name() + - (downloaded ? " [downloaded]" : "")); - } - return 0; -} - -void print_lora_state(const GlobalOptions &options, const v1::LoraState &state) { - if (options.json) { - out::JsonWriter json; - json.begin_object() - .field("base_model_id", state.base_model_id()) - .begin_array("applied"); - for (const v1::LoraAdapterInfo &adapter : state.loaded_adapters()) { - json.begin_array_object() - .field("id", adapter.adapter_id()) - .field("path", adapter.adapter_path()) - .field("scale", static_cast(adapter.scale())) - .field("applied", adapter.applied()) - .end_object(); - } - json.end_array().end_object(); - out::result_line(json.str()); - return; - } - if (state.loaded_adapters().empty()) { - out::result_line("no adapters applied"); - return; - } - std::vector> rows; - for (const v1::LoraAdapterInfo &adapter : state.loaded_adapters()) { - rows.push_back({adapter.adapter_id().empty() ? adapter.adapter_path() - : adapter.adapter_id(), - std::to_string(adapter.scale()), - adapter.applied() ? "yes" : "no"}); - } - out::table({"ADAPTER", "SCALE", "APPLIED"}, rows); -} - -int run_lora_list(const GlobalOptions &options) { - Bootstrapped env; - if (bootstrap(options, &env) != RAC_SUCCESS) { - return 1; - } - - // rac_lora_state_proto ignores its input payload (state is a pure read); - // an empty serialized LoraState is the canonical "no request" shape. - const v1::LoraState empty_request; - const std::string request_bytes = proto::serialize(empty_request); - rac_proto_buffer_t out_buffer; - rac_proto_buffer_init(&out_buffer); - const rac_result_t rc = rac_lora_state_proto( - reinterpret_cast(request_bytes.data()), request_bytes.size(), &out_buffer); - v1::LoraState state; - std::string error; - if (!proto::parse_proto_buffer(&out_buffer, &state, &error) || - rc != RAC_SUCCESS) { - out::error_line("cannot read LoRA state: " + error); - return 1; - } - print_lora_state(options, state); - return 0; -} - -int run_lora_remove(const GlobalOptions &options, const std::string &adapter) { - Bootstrapped env; - if (bootstrap(options, &env) != RAC_SUCCESS) { - return 1; - } - - // LoraRemoveRequest.adapter_paths was deleted: adapter_ids is the only - // identity path now besides clear_all, so a bare path can no longer be - // named directly here. - v1::LoraRemoveRequest request; - if (adapter.empty()) { - request.set_clear_all(true); - } else { - request.add_adapter_ids(adapter); - } - - const std::string request_bytes = proto::serialize(request); - rac_proto_buffer_t out_buffer; - rac_proto_buffer_init(&out_buffer); - const rac_result_t rc = rac_lora_remove_proto( - reinterpret_cast(request_bytes.data()), - request_bytes.size(), &out_buffer); - v1::LoraState state; - std::string error; - if (!proto::parse_proto_buffer(&out_buffer, &state, &error) || - rc != RAC_SUCCESS) { - out::error_line("remove failed: " + error); - return 1; - } - if (state.has_error()) { - out::error_line("remove failed: " + state.error().message()); - return 1; - } - print_lora_state(options, state); - return 0; -} - -// Load an LLM through the model-lifecycle service so rac_lora_apply_proto can -// acquire it. validate_availability=true auto-pulls the model if missing. -bool load_llm_for_lora(const GlobalOptions &options, const std::string &model_id) { - v1::ModelLoadRequest request; - request.set_model_id(model_id); - request.set_category(v1::MODEL_CATEGORY_LANGUAGE); - request.set_validate_availability(true); - - const std::string bytes = proto::serialize(request); - rac_proto_buffer_t out_buffer; - rac_proto_buffer_init(&out_buffer); - std::string error; - v1::ModelLoadResult result; - if (rac_model_lifecycle_load_proto(rac_get_model_registry(), - reinterpret_cast(bytes.data()), bytes.size(), - &out_buffer) != RAC_SUCCESS || - !proto::parse_proto_buffer(&out_buffer, &result, &error)) { - out::error_line("LLM load failed: " + error); - return false; - } - if (!result.has_error() == false) { - out::error_line("LLM load failed: " + - (result.error().message().empty() ? "unknown error" : result.error().message())); - return false; - } - return true; -} - -int run_lora_apply(const GlobalOptions &options, const std::string &model_id, - const std::string &adapter_path, float scale) { - Bootstrapped env; - if (bootstrap(options, &env) != RAC_SUCCESS) { - return 1; - } - if (!load_llm_for_lora(options, model_id)) { - return 1; - } - - // keep_existing left unset (false): SET semantics -- `adapters` becomes the - // complete active set, matching the removed explicit replace_existing(true). - v1::LoraApplyRequest request; - v1::LoraAdapterConfig *adapter = request.add_adapters(); - adapter->set_adapter_path(adapter_path); - adapter->set_scale(scale); - - const std::string request_bytes = proto::serialize(request); - rac_proto_buffer_t out_buffer; - rac_proto_buffer_init(&out_buffer); - const rac_result_t rc = rac_lora_apply_proto( - reinterpret_cast(request_bytes.data()), request_bytes.size(), &out_buffer); - v1::LoraApplyResult result; - std::string error; - if (!proto::parse_proto_buffer(&out_buffer, &result, &error) || rc != RAC_SUCCESS) { - out::error_line("apply failed: " + error); - return 1; - } - if (!result.has_error() == false) { - out::error_line("apply failed: " + - (result.error().message().empty() ? std::to_string(result.error().c_abi_code()) - : result.error().message())); - return 1; - } - - if (options.json) { - out::JsonWriter json; - json.begin_object() - .field("success", result.has_error() == false) - .field("adapters", static_cast(result.adapters_size())) - .end_object(); - out::result_line(json.str()); - } else { - out::result_line("applied " + std::to_string(result.adapters_size()) + " adapter(s) to " + - model_id); - } - return 0; -} - -} // namespace - -void register_lora(CLI::App &app, GlobalOptions &options) { - CLI::App *cmd = app.add_subcommand("lora", "Attach LoRA adapters to a language model"); - cmd->require_subcommand(1); - - CLI::App *apply_cmd = - cmd->add_subcommand("apply", "Attach an adapter to a model, loading both"); - auto apply_model = std::make_shared(); - auto adapter_path = std::make_shared(); - auto scale = std::make_shared(1.0f); - apply_cmd->add_option("adapter", *adapter_path, "Path to the adapter file (.gguf)") - ->required(); - apply_cmd->add_option("--model,-m", *apply_model, "LLM to attach the adapter to") - ->required(); - apply_cmd->add_option("--scale", *scale, "How strongly the adapter applies (default 1.0)") - ->default_val(1.0f); - apply_cmd->callback([&options, apply_model, adapter_path, scale]() { - const int exit_code = run_lora_apply(options, *apply_model, *adapter_path, *scale); - if (exit_code != 0) { - throw CLI::RuntimeError(exit_code); - } - }); - - CLI::App *remove_cmd = - cmd->add_subcommand("remove", "Detach one adapter, or every adapter"); - auto remove_ref = std::make_shared(); - remove_cmd->add_option("adapter", *remove_ref, - "Adapter id or path (omit to detach all)"); - remove_cmd->callback([&options, remove_ref]() { - const int exit_code = run_lora_remove(options, *remove_ref); - if (exit_code != 0) { - throw CLI::RuntimeError(exit_code); - } - }); - - CLI::App *list_cmd = cmd->add_subcommand("list", "Show the adapters currently attached"); - list_cmd->callback([&options]() { - const int exit_code = run_lora_list(options); - if (exit_code != 0) { - throw CLI::RuntimeError(exit_code); - } - }); - - CLI::App *catalog_cmd = - cmd->add_subcommand("catalog", "List adapters registered with the SDK"); - catalog_cmd->callback([&options]() { - const int exit_code = run_lora_catalog(options); - if (exit_code != 0) { - throw CLI::RuntimeError(exit_code); - } - }); - - // `import` is retired: idl/lora_options.proto deleted - // LoraAdapterImportRequest/Result outright (adapter files are acquired - // through the models domain's download/import verbs now), and - // rac_lora_adapter_import_proto in commons is a permanent stub returning - // RAC_ERROR_NOT_IMPLEMENTED (see lora_import.cpp). No replacement verb - // exists on this namespace yet, so the command is left out per this - // package's own rule: never wire a flag/verb the C ABI cannot serve. -} - -} // namespace rcli::commands diff --git a/rcli/src/commands/cmd_models.cpp b/rcli/src/commands/cmd_models.cpp deleted file mode 100644 index afb6c22b4a..0000000000 --- a/rcli/src/commands/cmd_models.cpp +++ /dev/null @@ -1,427 +0,0 @@ -/** - * @file cmd_models.cpp - * @brief `rcli models …` — the model-lifecycle namespace from the public API - * spec (list, get, register, download, delete, load, unload, state). - * - * list / get / download / delete reuse the configure_* functions owned by - * cmd_list, cmd_show, cmd_pull and cmd_rm, so the namespaced verbs and the - * top-level aliases (`list`, `show`, `pull`, `rm`) are the same command. - * register / load / unload / state are thin translations of the commons - * lifecycle ABI: - * register → model_ref::resolve (rac_register_model_from_url_proto inside) - * load → rac_model_lifecycle_load_proto(validate_availability=true) - * unload → rac_model_lifecycle_unload_proto - * state → rac_model_lifecycle_current_model_proto per category - */ - -#include "commands/commands.h" - -#include -#include -#include -#include -#include - -#include "model_types.pb.h" -#include "rac/core/rac_core.h" -#include "rac/core/rac_model_lifecycle.h" -#include "rac/infrastructure/model_management/rac_model_registry.h" - -#include "catalog/model_ref.h" -#include "commands/engine_options.h" -#include "commands/model_labels.h" -#include "io/output.h" -#include "io/proto.h" -#include "progress/progress_bar.h" - -namespace rcli::commands { - -namespace { - -namespace v1 = runanywhere::v1; -namespace fs = std::filesystem; - -// Categories `models state` reports, in the order they are printed. -constexpr v1::ModelCategory kStateCategories[] = { - v1::MODEL_CATEGORY_LANGUAGE, - v1::MODEL_CATEGORY_MULTIMODAL, - v1::MODEL_CATEGORY_SPEECH_RECOGNITION, - v1::MODEL_CATEGORY_SPEECH_SYNTHESIS, - v1::MODEL_CATEGORY_VOICE_ACTIVITY_DETECTION, - v1::MODEL_CATEGORY_EMBEDDING, - v1::MODEL_CATEGORY_IMAGE_GENERATION, -}; - -bool parse_category(const std::string& name, v1::ModelCategory* out) { - for (const v1::ModelCategory category : kStateCategories) { - if (name == model_labels::category(category)) { - *out = category; - return true; - } - } - return false; -} - -std::string category_choices() { - std::string choices; - for (const v1::ModelCategory category : kStateCategories) { - choices += choices.empty() ? "" : ", "; - choices += model_labels::category(category); - } - return choices; -} - -int run_register(const GlobalOptions& options, const std::string& ref, const std::string& engine) { - Bootstrapped env; - if (bootstrap(options, &env) != RAC_SUCCESS) { - return 1; - } - - EngineHintResolution engine_hint; - std::string error; - if (!resolve_engine_hint(engine, &engine_hint, &error)) { - out::error_line(error); - return 2; - } - - model_ref::Resolved resolved; - if (model_ref::resolve(ref, &resolved, &error, &engine_hint.resolve_options) != RAC_SUCCESS) { - out::error_line(error); - return 1; - } - - rac_proto_buffer_t info_out; - rac_proto_buffer_init(&info_out); - v1::ModelInfo model; - const rac_result_t get_rc = rac_model_registry_get_proto_buffer( - rac_get_model_registry(), resolved.model_id.c_str(), &info_out); - const bool parsed = proto::parse_proto_buffer(&info_out, &model, &error); - if (get_rc != RAC_SUCCESS || !parsed) { - out::error_line("registration failed: " + error); - return 1; - } - - if (options.json) { - out::JsonWriter json; - json.begin_object() - .field("id", model.id()) - .field("name", model.name()) - .field("modality", model_labels::category(model.category())) - .field("backend", model_labels::backend(model.framework())) - .field("download_url", model.download_url()) - .field("downloaded", - model.registry_status() == v1::MODEL_REGISTRY_STATUS_DOWNLOADED) - .end_object(); - out::result_line(json.str()); - } else { - out::result_line("registered " + model.id()); - } - return 0; -} - -int run_load(const GlobalOptions& options, const std::string& ref, const std::string& engine, - const std::string& category_name) { - Bootstrapped env; - if (bootstrap(options, &env) != RAC_SUCCESS) { - return 1; - } - - EngineHintResolution engine_hint; - std::string error; - if (!resolve_engine_hint(engine, &engine_hint, &error)) { - out::error_line(error); - return 2; - } - - v1::ModelCategory category = v1::MODEL_CATEGORY_UNSPECIFIED; - if (!category_name.empty()) { - if (!parse_category(category_name, &category)) { - out::error_line("unknown category '" + category_name + "' (" + category_choices() + - ")"); - return 2; - } - engine_hint.resolve_options.has_category = true; - engine_hint.resolve_options.category = category; - } - - model_ref::Resolved resolved; - if (model_ref::resolve(ref, &resolved, &error, &engine_hint.resolve_options) != RAC_SUCCESS) { - out::error_line(error); - return 1; - } - - progress::DownloadProgressScope progress_scope(resolved.model_id, - !options.no_progress && !options.json); - v1::ModelLoadRequest request; - request.set_model_id(resolved.model_id); - request.set_validate_availability(true); - if (category != v1::MODEL_CATEGORY_UNSPECIFIED) { - request.set_category(category); - } - if (!resolved.from_catalog && - engine_hint.framework != v1::INFERENCE_FRAMEWORK_UNSPECIFIED) { - request.set_framework(engine_hint.framework); - } - - const std::string bytes = proto::serialize(request); - rac_proto_buffer_t out_buffer; - rac_proto_buffer_init(&out_buffer); - v1::ModelLoadResult result; - if (rac_model_lifecycle_load_proto(rac_get_model_registry(), - reinterpret_cast(bytes.data()), - bytes.size(), &out_buffer) != RAC_SUCCESS || - !proto::parse_proto_buffer(&out_buffer, &result, &error)) { - out::error_line("model load failed: " + error); - return 1; - } - if (result.has_error()) { - out::error_line("model load failed: " + (result.error().message().empty() - ? "unknown error" - : result.error().message())); - return 1; - } - - if (options.json) { - out::JsonWriter json; - json.begin_object() - .field("id", result.model_id()) - .field("modality", model_labels::category(result.category())) - .field("backend", model_labels::backend(result.framework())) - .field("path", result.resolved_path()) - .field("already_loaded", result.already_loaded()) - .end_object(); - out::result_line(json.str()); - } else { - out::result_line("loaded " + result.model_id() + " → " + result.resolved_path()); - } - return 0; -} - -int run_unload(const GlobalOptions& options, const std::string& category_name) { - Bootstrapped env; - if (bootstrap(options, &env) != RAC_SUCCESS) { - return 1; - } - - v1::ModelUnloadRequest request; - if (category_name.empty()) { - request.set_unload_all(true); - } else { - v1::ModelCategory category = v1::MODEL_CATEGORY_UNSPECIFIED; - if (!parse_category(category_name, &category)) { - out::error_line("unknown category '" + category_name + "' (" + category_choices() + - ")"); - return 2; - } - request.set_category(category); - } - - const std::string bytes = proto::serialize(request); - rac_proto_buffer_t out_buffer; - rac_proto_buffer_init(&out_buffer); - std::string error; - v1::ModelUnloadResult result; - if (rac_model_lifecycle_unload_proto(reinterpret_cast(bytes.data()), - bytes.size(), &out_buffer) != RAC_SUCCESS || - !proto::parse_proto_buffer(&out_buffer, &result, &error)) { - out::error_line("unload failed: " + error); - return 1; - } - // Freeing everything when nothing is resident is not a failure, even though - // the lifecycle service reports "no loaded model matched". - if (result.has_error() && !(request.unload_all() && result.unloaded_model_ids().empty())) { - out::error_line("unload failed: " + (result.error().message().empty() - ? "unknown error" - : result.error().message())); - return 1; - } - - if (options.json) { - out::JsonWriter json; - json.begin_object().begin_array("unloaded"); - for (const std::string& id : result.unloaded_model_ids()) { - json.begin_array_object().field("id", id).end_object(); - } - json.end_array().end_object(); - out::result_line(json.str()); - } else if (result.unloaded_model_ids().empty()) { - out::result_line("nothing was loaded"); - } else { - for (const std::string& id : result.unloaded_model_ids()) { - out::result_line("unloaded " + id); - } - } - return 0; -} - -// Loaded model for one category, or an empty id when nothing is resident. -std::string loaded_model_id(v1::ModelCategory category) { - v1::CurrentModelRequest request; - request.set_category(category); - const std::string bytes = proto::serialize(request); - - rac_proto_buffer_t out_buffer; - rac_proto_buffer_init(&out_buffer); - v1::CurrentModelResult result; - if (rac_model_lifecycle_current_model_proto(reinterpret_cast(bytes.data()), - bytes.size(), &out_buffer) != RAC_SUCCESS || - !proto::parse_proto_buffer(&out_buffer, &result, nullptr) || !result.found()) { - return {}; - } - return result.model_id(); -} - -struct Storage { - uint64_t used_bytes = 0; - uint64_t free_bytes = 0; -}; - -Storage models_storage(const std::string& models_dir) { - Storage storage; - std::error_code ec; - for (const auto& entry : fs::recursive_directory_iterator( - models_dir, fs::directory_options::skip_permission_denied, ec)) { - if (entry.is_regular_file(ec)) { - storage.used_bytes += entry.file_size(ec); - } - } - // The models directory may not exist yet; free space comes from the nearest - // ancestor that does. - for (fs::path path = models_dir; !path.empty(); path = path.parent_path()) { - const fs::space_info space = fs::space(path, ec); - if (!ec) { - storage.free_bytes = space.available; - break; - } - if (!path.has_relative_path()) { - break; - } - } - return storage; -} - -int run_state(const GlobalOptions& options) { - Bootstrapped env; - if (bootstrap(options, &env) != RAC_SUCCESS) { - return 1; - } - - std::vector> loaded; - for (const v1::ModelCategory category : kStateCategories) { - const std::string id = loaded_model_id(category); - if (!id.empty()) { - loaded.emplace_back(model_labels::category(category), id); - } - } - const Storage storage = models_storage(env.models_dir); - - if (options.json) { - out::JsonWriter json; - json.begin_object().begin_array("loaded"); - for (const auto& [modality, id] : loaded) { - json.begin_array_object().field("modality", modality).field("id", id).end_object(); - } - json.end_array() - .field("storage_used_bytes", static_cast(storage.used_bytes)) - .field("storage_free_bytes", static_cast(storage.free_bytes)) - .end_object(); - out::result_line(json.str()); - return 0; - } - - if (loaded.empty()) { - out::result_line("no models loaded"); - } else { - std::vector> rows; - for (const auto& [modality, id] : loaded) { - rows.push_back({modality, id}); - } - out::table({"MODALITY", "LOADED"}, rows); - } - out::result_line("storage " + out::human_bytes(storage.used_bytes) + " used, " + - out::human_bytes(storage.free_bytes) + " free"); - return 0; -} - -} // namespace - -void register_models(CLI::App& app, GlobalOptions& options) { - CLI::App* ns = app.add_subcommand("models", "Manage the local model catalog"); - ns->require_subcommand(1); - - configure_models_list(ns->add_subcommand("list", "List models, downloaded ones by default"), - options); - configure_models_get(ns->add_subcommand("get", "Show one model's registry entry"), options); - configure_models_download( - ns->add_subcommand("download", "Fetch a model with resumable progress"), options); - configure_models_delete(ns->add_subcommand("delete", "Remove a model's files and registration"), - options); - - CLI::App* register_cmd = - ns->add_subcommand("register", "Add a model from a URL or hf.co ref to the registry"); - auto register_ref = std::make_shared(); - auto register_engine = std::make_shared(); - register_cmd->add_option("model", *register_ref, "hf.co/org/repo/file, hf:// or http(s) URL") - ->required(); - register_cmd->add_option("--engine", *register_engine, - "Pin the inference engine (mlx, llamacpp, onnx, sherpa)"); - register_cmd->callback([&options, register_ref, register_engine]() { - const int exit_code = run_register(options, *register_ref, *register_engine); - if (exit_code != 0) { - throw CLI::RuntimeError(exit_code); - } - }); - - CLI::App* load_cmd = ns->add_subcommand("load", "Load a model now instead of on first use"); - auto load_ref = std::make_shared(); - auto load_engine = std::make_shared(); - auto load_category = std::make_shared(); - load_cmd->add_option("model", *load_ref, "Model id, alias, hf.co/... ref or URL")->required(); - load_cmd->add_option("--engine,--framework", *load_engine, - "Pin the inference engine (mlx, llamacpp, onnx, sherpa)"); - load_cmd->add_option("--category", *load_category, - "Load it as this modality (" + category_choices() + ")"); - load_cmd->callback([&options, load_ref, load_engine, load_category]() { - const int exit_code = run_load(options, *load_ref, *load_engine, *load_category); - if (exit_code != 0) { - throw CLI::RuntimeError(exit_code); - } - }); - - CLI::App* unload_cmd = - ns->add_subcommand("unload", "Free loaded models, all of them by default"); - auto unload_category = std::make_shared(); - unload_cmd->add_option("category", *unload_category, - "Only free this modality (" + category_choices() + ")"); - unload_cmd->callback([&options, unload_category]() { - const int exit_code = run_unload(options, *unload_category); - if (exit_code != 0) { - throw CLI::RuntimeError(exit_code); - } - }); - - CLI::App* state_cmd = ns->add_subcommand("state", "Report resident models and disk usage"); - state_cmd->callback([&options]() { - const int exit_code = run_state(options); - if (exit_code != 0) { - throw CLI::RuntimeError(exit_code); - } - }); -} - -void register_models_aliases(CLI::App& app, GlobalOptions& options) { - CLI::App* list = app.add_subcommand("list", "List models (alias of `models list`)"); - list->alias("ls"); - configure_models_list(list, options); - - configure_models_get(app.add_subcommand("show", "Show model details (alias of `models get`)"), - options); - configure_models_download( - app.add_subcommand("pull", "Download a model (alias of `models download`)"), options); - - CLI::App* remove = app.add_subcommand("rm", "Delete a model (alias of `models delete`)"); - remove->alias("remove"); - configure_models_delete(remove, options); -} - -} // namespace rcli::commands diff --git a/rcli/src/commands/cmd_pull.cpp b/rcli/src/commands/cmd_pull.cpp deleted file mode 100644 index a72e70cf40..0000000000 --- a/rcli/src/commands/cmd_pull.cpp +++ /dev/null @@ -1,291 +0,0 @@ -/** - * @file cmd_pull.cpp - * @brief `rcli models download ` (alias `rcli pull`) — - * download via the commons orchestrator: plan → start → progress - * callback → terminal state. - * - * SIGINT cancels the task (partial bytes preserved → re-pull resumes via the - * plan's can_resume path). Exit codes: 0 done, 1 failure, 130 user cancel. - */ - -#include "commands/commands.h" - -#include -#include -#include -#include -#include -#include - -#include "download_service.pb.h" -#include "model_types.pb.h" -#include "rac/core/rac_core.h" -#include "rac/infrastructure/download/rac_download_orchestrator.h" -#include "rac/infrastructure/model_management/rac_model_registry.h" - -#include "commands/engine_options.h" -#include "catalog/model_ref.h" -#include "io/output.h" -#include "io/proto.h" -#include "progress/progress_bar.h" - -namespace rcli::commands { - -namespace { - -namespace v1 = runanywhere::v1; - -volatile std::sig_atomic_t g_interrupted = 0; - -void on_sigint(int /*signum*/) { g_interrupted = 1; } - -struct PullState { - std::mutex mutex; - std::condition_variable cv; - v1::DownloadProgress last; - bool terminal = false; - bool got_progress = false; - std::string model_id; // filter: only this model's updates -}; - -PullState *g_state = nullptr; - -void progress_callback(const uint8_t *proto_bytes, size_t proto_size, - void * /*user_data*/) { - if (!g_state) { - return; - } - v1::DownloadProgress progress; - if (!progress.ParseFromArray(proto_bytes, static_cast(proto_size))) { - return; - } - std::lock_guard lock(g_state->mutex); - if (!g_state->model_id.empty() && progress.model_id() != g_state->model_id) { - return; - } - g_state->last = progress; - g_state->got_progress = true; - switch (progress.state()) { - case v1::DOWNLOAD_STATE_COMPLETED: - case v1::DOWNLOAD_STATE_FAILED: - case v1::DOWNLOAD_STATE_CANCELLED: - g_state->terminal = true; - break; - default: - break; - } - g_state->cv.notify_all(); -} - -} // namespace - -int pull_model_flow(const GlobalOptions &options, const std::string &model_id) { - const model_ref::Resolved resolved{model_id, false}; - std::string error; - - // The orchestrator plans from embedded metadata (it does not consult the - // registry), so fetch the saved ModelInfo first. - v1::ModelInfo model_info; - { - rac_proto_buffer_t info_out; - rac_proto_buffer_init(&info_out); - const rac_result_t get_rc = rac_model_registry_get_proto_buffer( - rac_get_model_registry(), resolved.model_id.c_str(), &info_out); - // parse unconditionally: it interprets the {status,error_message} - // envelope and frees the buffer on every path (no leak on get failure). - if (!proto::parse_proto_buffer(&info_out, &model_info, &error) || - get_rc != RAC_SUCCESS) { - out::error_line("model not found in registry: " + resolved.model_id + - (error.empty() ? "" : " (" + error + ")")); - return 1; - } - } - - // Plan - v1::DownloadPlanRequest plan_request; - plan_request.set_model_id(resolved.model_id); - *plan_request.mutable_model() = model_info; - const std::string plan_bytes = proto::serialize(plan_request); - - rac_proto_buffer_t plan_out; - rac_proto_buffer_init(&plan_out); - rac_result_t rc = rac_download_plan_proto( - reinterpret_cast(plan_bytes.data()), plan_bytes.size(), - &plan_out); - if (rc != RAC_SUCCESS) { - rac_proto_buffer_free(&plan_out); - out::error_line("download plan failed: " + out::describe_result(rc)); - return 1; - } - v1::DownloadPlanResult plan; - if (!proto::parse_proto_buffer(&plan_out, &plan, &error)) { - out::error_line("download plan failed: " + error); - return 1; - } - if (!plan.can_start()) { - const std::string reason = - plan.error().message().empty() ? "plan rejected" : plan.error().message(); - out::error_line("cannot pull " + resolved.model_id + ": " + reason); - return 1; - } - if (plan.total_bytes() == 0 && plan.can_resume()) { - out::status_line("resuming partial download"); - } - - // Progress wiring before start so no early events are missed. - PullState state; - state.model_id = resolved.model_id; - g_state = &state; - rac_download_set_progress_proto_callback(progress_callback, nullptr); - - progress::ProgressRenderer renderer(!options.no_progress && !options.json); - - // Start - // skip_registry_update left unset: default (false) means "do NOT skip" — - // the registry is updated on completion, same behavior the deleted - // explicit set_update_registry_on_completion(true) call used to request. - v1::DownloadStartRequest start_request; - start_request.set_model_id(resolved.model_id); - *start_request.mutable_plan() = plan; - const std::string start_bytes = proto::serialize(start_request); - - rac_proto_buffer_t start_out; - rac_proto_buffer_init(&start_out); - rc = rac_download_start_proto( - reinterpret_cast(start_bytes.data()), start_bytes.size(), - &start_out); - v1::DownloadStartResult start; - if (rc != RAC_SUCCESS || - !proto::parse_proto_buffer(&start_out, &start, &error)) { - rac_download_set_progress_proto_callback(nullptr, nullptr); - g_state = nullptr; - out::error_line("download start failed: " + - (rc != RAC_SUCCESS ? out::describe_result(rc) : error)); - return 1; - } - if (!start.accepted()) { - rac_download_set_progress_proto_callback(nullptr, nullptr); - g_state = nullptr; - out::error_line("download rejected: " + start.error().message()); - return 1; - } - - // Wait for terminal state; SIGINT cancels once (partial bytes preserved). - g_interrupted = 0; - auto *previous_handler = std::signal(SIGINT, on_sigint); - bool cancel_sent = false; - v1::DownloadProgress final_progress; - { - std::unique_lock lock(state.mutex); - while (!state.terminal) { - state.cv.wait_for(lock, std::chrono::milliseconds(200)); - if (state.got_progress && !state.terminal) { - renderer.update(state.last); - } - if (g_interrupted && !cancel_sent) { - cancel_sent = true; - lock.unlock(); - renderer.finish(); - out::status_line( - "cancelling (partial bytes kept — re-pull resumes)..."); - v1::DownloadCancelRequest cancel_request; - cancel_request.set_task_id(start.task_id()); - cancel_request.set_model_id(resolved.model_id); - cancel_request.set_delete_partial_bytes(false); - const std::string cancel_bytes = proto::serialize(cancel_request); - rac_proto_buffer_t cancel_out; - rac_proto_buffer_init(&cancel_out); - rac_download_cancel_proto( - reinterpret_cast(cancel_bytes.data()), - cancel_bytes.size(), &cancel_out); - rac_proto_buffer_free(&cancel_out); - lock.lock(); - } - } - final_progress = state.last; - if (!cancel_sent) { - renderer.update(final_progress); - } - } - renderer.finish(); - std::signal(SIGINT, previous_handler); - rac_download_set_progress_proto_callback(nullptr, nullptr); - g_state = nullptr; - - switch (final_progress.state()) { - case v1::DOWNLOAD_STATE_COMPLETED: - break; - case v1::DOWNLOAD_STATE_CANCELLED: - out::error_line("pull cancelled"); - return 130; - default: - out::error_line("pull failed: " + (final_progress.error().message().empty() - ? "download error" - : final_progress.error().message())); - return 1; - } - - // Report the saved entry. parse runs unconditionally: it interprets the - // {status,error_message} envelope and frees the buffer on every path. - rac_proto_buffer_t model_out; - rac_proto_buffer_init(&model_out); - v1::ModelInfo model; - const rac_result_t report_rc = rac_model_registry_get_proto_buffer( - rac_get_model_registry(), resolved.model_id.c_str(), &model_out); - const bool report_parsed = - proto::parse_proto_buffer(&model_out, &model, nullptr); - if (report_rc == RAC_SUCCESS && report_parsed) { - if (options.json) { - out::JsonWriter json; - json.begin_object() - .field("id", model.id()) - .field("name", model.name()) - .field("local_path", model.local_path()) - .field("bytes", - static_cast(final_progress.bytes_downloaded())) - .end_object(); - out::result_line(json.str()); - } else { - out::result_line( - "pulled " + model.id() + - (model.local_path().empty() ? "" : " → " + model.local_path())); - } - } else { - out::result_line("pulled " + resolved.model_id); - } - return 0; -} - -void configure_models_download(CLI::App *cmd, GlobalOptions &options) { - auto ref = std::make_shared(); - auto engine = std::make_shared(); - cmd->add_option("model", *ref, "Model id, alias, hf.co/org/repo/file or URL") - ->required(); - cmd->add_option("--engine", *engine, - "Engine hint (neurt|coreml|ane, mlx, llamacpp, onnx, sherpa). Honoured for " - "catalog models too, not just URL/HF refs."); - cmd->callback([&options, ref, engine]() { - Bootstrapped env; - if (bootstrap(options, &env) != RAC_SUCCESS) { - throw CLI::RuntimeError(1); - } - EngineHintResolution engine_hint; - std::string engine_error; - if (!resolve_engine_hint(*engine, &engine_hint, &engine_error)) { - out::error_line(engine_error); - throw CLI::RuntimeError(2); - } - model_ref::Resolved resolved; - std::string error; - if (model_ref::resolve(*ref, &resolved, &error, &engine_hint.resolve_options) != RAC_SUCCESS) { - out::error_line(error); - throw CLI::RuntimeError(1); - } - const int exit_code = pull_model_flow(options, resolved.model_id); - if (exit_code != 0) { - throw CLI::RuntimeError(exit_code); - } - }); -} - -} // namespace rcli::commands diff --git a/rcli/src/commands/cmd_rag.cpp b/rcli/src/commands/cmd_rag.cpp deleted file mode 100644 index 0a5389547d..0000000000 --- a/rcli/src/commands/cmd_rag.cpp +++ /dev/null @@ -1,419 +0,0 @@ -/** - * @file cmd_rag.cpp - * @brief `rcli rag query` / `rcli rag search` — retrieval-augmented generation - * via the commons RAG session ABI. - * - * Single-shot flow in one process (the CLI is stateless across invocations and - * RAG indexes are in-memory only): - * rac_rag_session_create_proto(RAGConfiguration) → session handle - * → rac_rag_ingest_proto(RAGDocument) per --doc / --file - * → rac_rag_query_proto(RAGQueryOptions) → RAGResult - * or rac_rag_search_proto(RAGSearchRequest) → RAGSearchResponse - * rac_rag_session_destroy_proto(session) - * - * Commons resolves the embedding (+ optional LLM) model ids to filesystem paths - * via the model registry and owns the pipeline; this command only translates - * argv to the rac_rag_* C ABI and renders the result. - */ - -#include "commands/commands.h" - -#if !defined(RAC_HAVE_RAG) - -// The RAG pipeline is not folded into this binary (RAC_BACKEND_RAG=OFF, e.g. the -// Windows CLI preset), so the rac_rag_*_proto symbols are unavailable. Register -// no `rag` subcommand rather than fail to link. -namespace rcli::commands { - -void register_rag(CLI::App& app, GlobalOptions& options) { - (void)app; - (void)options; -} - -} // namespace rcli::commands - -#else - -#include -#include -#include -#include -#include - -#include "rag.pb.h" -#include "rac/core/rac_core.h" -#include "rac/features/rag/rac_rag.h" - -#include "io/output.h" -#include "io/proto.h" - -namespace rcli::commands { - -namespace { - -namespace v1 = runanywhere::v1; - -constexpr const char* kDefaultRagLlm = "smollm2-360m-q8_0"; -constexpr const char* kDefaultRagEmbed = "all-minilm-l6-v2"; - -bool read_text_file(const std::string& path, std::string* out, std::string* error) { - std::ifstream file(path, std::ios::binary); - if (!file) { - *error = "cannot open file: " + path; - return false; - } - std::ostringstream buffer; - buffer << file.rdbuf(); - *out = buffer.str(); - return true; -} - -struct RagParams { - std::string llm_model = kDefaultRagLlm; - std::string embed_model = kDefaultRagEmbed; - std::vector docs; - std::vector files; - std::string system_prompt; - int top_k = 0; - int chunk_size = 0; - int chunk_overlap = 0; - int max_output_tokens = 0; - float temperature = -1.0f; - float similarity_threshold = -1.0f; - bool require_llm = true; -}; - -// One session covers the whole invocation: open → ingest every document → ask -// or search. The CLI cannot split those verbs apart the way the SDK spec does -// because commons keeps RAG indexes in memory only -// (RAGConfiguration.index_path / persist_index are not honored). -bool collect_documents(const RagParams& params, std::vector* documents) { - *documents = params.docs; - for (const auto& path : params.files) { - std::string content; - std::string error; - if (!read_text_file(path, &content, &error)) { - out::error_line(error); - return false; - } - documents->push_back(content); - } - if (documents->empty()) { - out::error_line("at least one document is required (--doc or --file)"); - return false; - } - return true; -} - -bool open_and_ingest(const GlobalOptions& options, const RagParams& params, - const std::vector& documents, rac_handle_t* session) { - // Models must already be downloaded — the session resolves them from the - // registry. (Pull them first with `rcli models download `.) - v1::RAGConfiguration config; - config.set_embedding_model_id(params.embed_model); - if (params.require_llm || !params.llm_model.empty()) { - config.set_llm_model_id(params.llm_model); - } - if (params.top_k > 0) { - config.set_top_k(params.top_k); - } - if (params.chunk_size > 0) { - config.set_chunk_size(params.chunk_size); - } - if (params.chunk_overlap > 0) { - config.set_chunk_overlap(params.chunk_overlap); - } - if (params.similarity_threshold >= 0.0f) { - config.set_score_threshold(params.similarity_threshold); - } - - const std::string config_bytes = proto::serialize(config); - if (rac_rag_session_create_proto(reinterpret_cast(config_bytes.data()), - config_bytes.size(), session) != RAC_SUCCESS || - *session == nullptr) { - if (params.require_llm || !params.llm_model.empty()) { - out::error_line("RAG session create failed (check that '" + params.embed_model + - "' and '" + params.llm_model + "' are downloaded)"); - } else { - out::error_line("RAG session create failed (check that '" + params.embed_model + - "' is downloaded)"); - } - return false; - } - - std::string error; - for (size_t i = 0; i < documents.size(); ++i) { - v1::RAGDocument document; - document.set_id("doc-" + std::to_string(i)); - document.set_text(documents[i]); - const std::string doc_bytes = proto::serialize(document); - rac_proto_buffer_t stats_buffer; - rac_proto_buffer_init(&stats_buffer); - v1::RAGStatistics stats; - if (rac_rag_ingest_proto(*session, reinterpret_cast(doc_bytes.data()), - doc_bytes.size(), &stats_buffer) != RAC_SUCCESS || - !proto::parse_proto_buffer(&stats_buffer, &stats, &error)) { - out::error_line("RAG ingest failed: " + error); - rac_rag_session_destroy_proto(*session); - *session = nullptr; - return false; - } - if (options.verbose) { - out::status_line("ingested doc-" + std::to_string(i) + " (" + - std::to_string(documents[i].size()) + " bytes)"); - } - } - return true; -} - -int run_rag_query(const GlobalOptions& options, const RagParams& params, - const std::string& question) { - Bootstrapped env; - if (bootstrap(options, &env) != RAC_SUCCESS) { - return 1; - } - - if (question.empty()) { - out::error_line("a question is required (positional argument)"); - return 2; - } - - std::vector documents; - if (!collect_documents(params, &documents)) { - return 2; - } - - rac_handle_t session = nullptr; - if (!open_and_ingest(options, params, documents, &session)) { - return 1; - } - - v1::RAGQueryOptions query; - query.set_query(question); - if (params.max_output_tokens > 0) { - query.mutable_generation()->set_max_output_tokens(params.max_output_tokens); - } - if (params.temperature >= 0.0f) { - query.mutable_generation()->set_temperature(params.temperature); - } - if (!params.system_prompt.empty()) { - query.mutable_generation()->set_system_prompt(params.system_prompt); - } - if (params.similarity_threshold >= 0.0f) { - query.mutable_retrieval()->set_score_threshold(params.similarity_threshold); - } - if (params.top_k > 0) { - query.mutable_retrieval()->set_top_k(params.top_k); - } - - const std::string query_bytes = proto::serialize(query); - rac_proto_buffer_t result_buffer; - rac_proto_buffer_init(&result_buffer); - v1::RAGResult result; - std::string error; - if (rac_rag_query_proto(session, reinterpret_cast(query_bytes.data()), - query_bytes.size(), &result_buffer) != RAC_SUCCESS || - !proto::parse_proto_buffer(&result_buffer, &result, &error)) { - out::error_line("RAG query failed: " + error); - rac_rag_session_destroy_proto(session); - return 1; - } - - if (result.has_error()) { - out::error_line("RAG query failed: " + (result.error().message().empty() - ? std::to_string(result.error().c_abi_code()) - : result.error().message())); - rac_rag_session_destroy_proto(session); - return 1; - } - - if (options.json) { - out::JsonWriter json; - // RAGResult carries no total_time_ms of its own (deleted in the API - // realignment pass) -- retrieval_time_ms + generation_time_ms is the - // whole measured wall-clock, so sum them rather than drop the field. - json.begin_object() - .field("answer", result.answer()) - .field("retrieval_time_ms", static_cast(result.retrieval_time_ms())) - .field("generation_time_ms", static_cast(result.generation_time_ms())) - .field("total_time_ms", static_cast(result.retrieval_time_ms() + - result.generation_time_ms())) - .field("prompt_tokens", static_cast(result.usage().input_tokens())) - .field("completion_tokens", static_cast(result.usage().output_tokens())); - json.begin_array("matches"); - for (const v1::RAGSearchResult& match : result.retrieved_chunks()) { - json.begin_array_object() - .field("text", match.text()) - .field("score", static_cast(match.score())) - .field("source", match.source_document()) - .end_object(); - } - json.end_array(); - out::result_line(json.end_object().str()); - } else { - out::result_line(result.answer()); - if (options.verbose) { - out::status_line("chunks=" + std::to_string(result.retrieved_chunks_size()) + - " retrieval=" + std::to_string(result.retrieval_time_ms()) + "ms" + - " generation=" + std::to_string(result.generation_time_ms()) + "ms"); - } - } - - rac_rag_session_destroy_proto(session); - return 0; -} - -int run_rag_search(const GlobalOptions& options, const RagParams& params, - const std::string& question) { - Bootstrapped env; - if (bootstrap(options, &env) != RAC_SUCCESS) { - return 1; - } - - if (question.empty()) { - out::error_line("a question is required (positional argument)"); - return 2; - } - - std::vector documents; - if (!collect_documents(params, &documents)) { - return 2; - } - - rac_handle_t session = nullptr; - if (!open_and_ingest(options, params, documents, &session)) { - return 1; - } - - v1::RAGSearchRequest request; - request.set_query(question); - if (params.top_k > 0) { - request.mutable_retrieval()->set_top_k(params.top_k); - } - if (params.similarity_threshold >= 0.0f) { - request.mutable_retrieval()->set_score_threshold(params.similarity_threshold); - } - - const std::string request_bytes = proto::serialize(request); - rac_proto_buffer_t response_buffer; - rac_proto_buffer_init(&response_buffer); - v1::RAGSearchResponse response; - std::string error; - if (rac_rag_search_proto(session, reinterpret_cast(request_bytes.data()), - request_bytes.size(), &response_buffer) != RAC_SUCCESS || - !proto::parse_proto_buffer(&response_buffer, &response, &error)) { - out::error_line("RAG search failed: " + error); - rac_rag_session_destroy_proto(session); - return 1; - } - - if (response.has_error()) { - out::error_line("RAG search failed: " + (response.error().message().empty() - ? std::to_string(response.error().c_abi_code()) - : response.error().message())); - rac_rag_session_destroy_proto(session); - return 1; - } - - if (options.json) { - out::JsonWriter json; - json.begin_object() - .field("retrieval_time_ms", static_cast(response.retrieval_time_ms())) - .field("request_id", response.request_id()); - json.begin_array("matches"); - for (const v1::RAGSearchResult& match : response.chunks()) { - json.begin_array_object() - .field("text", match.text()) - .field("score", static_cast(match.score())) - .field("source", match.source_document()) - .end_object(); - } - json.end_array(); - out::result_line(json.end_object().str()); - } else { - for (const v1::RAGSearchResult& match : response.chunks()) { - out::result_line(match.text()); - } - if (options.verbose) { - out::status_line("chunks=" + std::to_string(response.chunks_size()) + - " retrieval=" + std::to_string(response.retrieval_time_ms()) + "ms"); - } - } - - rac_rag_session_destroy_proto(session); - return 0; -} - -} // namespace - -namespace { - -// Corpus and retrieval flags are the same for search and query; only `query` -// generates an answer, so only it takes the generation knobs. -void add_corpus_options(CLI::App* cmd, const std::shared_ptr& params) { - cmd->add_option("--doc,-d", params->docs, "Document text to index; repeat for several"); - cmd->add_option("--file,-f", params->files, "Text file to index; repeat for several"); - cmd->add_option("--embedding-model,--embed", params->embed_model, - "Embedding model to index with (default: " + std::string(kDefaultRagEmbed) + - ")"); - cmd->add_option("--top-k", params->top_k, "Retrieve this many chunks per question"); - cmd->add_option("--chunk-size", params->chunk_size, - "Tokens per chunk when splitting documents"); - cmd->add_option("--chunk-overlap", params->chunk_overlap, - "Tokens shared between neighbouring chunks"); - cmd->add_option("--similarity-threshold", params->similarity_threshold, - "Discard chunks scoring below this"); -} - -} // namespace - -void register_rag(CLI::App& app, GlobalOptions& options) { - CLI::App* cmd = app.add_subcommand("rag", "Answer questions over your own documents"); - cmd->require_subcommand(1); - - CLI::App* query_cmd = cmd->add_subcommand("query", "Answer a question from the documents"); - auto question = std::make_shared(); - auto params = std::make_shared(); - query_cmd->add_option("question", *question, "Question to answer over the documents") - ->required(); - add_corpus_options(query_cmd, params); - query_cmd->add_option("--model,--llm", params->llm_model, - "LLM that writes the answer (default: " + std::string(kDefaultRagLlm) + - ")"); - query_cmd->add_option("--system-prompt", params->system_prompt, - "Steer the answer with a system instruction"); - query_cmd->add_option("--max-output-tokens,--max-tokens", params->max_output_tokens, - "Cap the answer length in tokens"); - query_cmd->add_option("--temperature", params->temperature, - "Raise for more random sampling"); - - query_cmd->callback([&options, question, params]() { - const int exit_code = run_rag_query(options, *params, *question); - if (exit_code != 0) { - throw CLI::RuntimeError(exit_code); - } - }); - - CLI::App* search_cmd = - cmd->add_subcommand("search", "Retrieve matching chunks without generating an answer"); - auto search_question = std::make_shared(); - auto search_params = std::make_shared(); - search_params->llm_model.clear(); // retrieval-only unless --model/--llm is passed - search_params->require_llm = false; - search_cmd->add_option("question", *search_question, "Query to retrieve chunks for") - ->required(); - add_corpus_options(search_cmd, search_params); - search_cmd->add_option("--model,--llm", search_params->llm_model, - "Optional LLM (needed only for multi-query / session rerank)"); - - search_cmd->callback([&options, search_question, search_params]() { - const int exit_code = run_rag_search(options, *search_params, *search_question); - if (exit_code != 0) { - throw CLI::RuntimeError(exit_code); - } - }); -} - -} // namespace rcli::commands - -#endif // RAC_HAVE_RAG diff --git a/rcli/src/commands/cmd_rerank.cpp b/rcli/src/commands/cmd_rerank.cpp deleted file mode 100644 index 0a2ee8311d..0000000000 --- a/rcli/src/commands/cmd_rerank.cpp +++ /dev/null @@ -1,179 +0,0 @@ -/** - * @file cmd_rerank.cpp - * @brief `rcli rerank --doc … ` — cross-encoder relevance scoring. - * - * Same component sequence the other model-backed commands use: - * ensure_model_ready (resolve + auto-pull + resolve paths) - * → rac_rerank_component_create / load_model - * → rac_rerank_component_rerank_proto(RerankRequest) → RerankResult - * → rac_rerank_component_destroy - * Scoring and ordering belong to the engine; this file only translates argv to - * proto bytes and renders the ranked list. - */ - -#include "commands/commands.h" - -#include -#include -#include -#include -#include -#include - -#include "rac/features/rerank/rac_rerank_component.h" -#include "rerank.pb.h" - -#include "commands/model_setup.h" -#include "io/output.h" -#include "io/proto.h" - -namespace rcli::commands { - -namespace { - -namespace v1 = runanywhere::v1; - -bool read_text_file(const std::string& path, std::string* out, std::string* error) { - std::ifstream file(path, std::ios::binary); - if (!file) { - *error = "cannot open file: " + path; - return false; - } - std::ostringstream buffer; - buffer << file.rdbuf(); - *out = buffer.str(); - return true; -} - -int run_rerank(const GlobalOptions& options, const std::string& model_ref, - const std::string& query, const std::vector& docs, - const std::vector& files, int top_n) { - Bootstrapped env; - if (bootstrap(options, &env) != RAC_SUCCESS) { - return 1; - } - if (model_ref.empty()) { - out::error_line("--model is required (a reranker model id or on-disk path)"); - return 2; - } - - std::vector documents = docs; - for (const std::string& path : files) { - std::string content; - std::string error; - if (!read_text_file(path, &content, &error)) { - out::error_line(error); - return 2; - } - documents.push_back(content); - } - if (documents.empty()) { - out::error_line("at least one document is required (--doc or --file)"); - return 2; - } - - ResolvedModelPaths model; - const int setup = ensure_model_ready(options, model_ref, &model); - if (setup != 0) { - return setup; - } - - rac_handle_t reranker = nullptr; - if (rac_rerank_component_create(&reranker) != RAC_SUCCESS) { - out::error_line("failed to create rerank component"); - return 1; - } - rac_result_t rc = rac_rerank_component_load_model(reranker, model.primary_path.c_str(), - model.model_id.c_str(), - model.display_name.c_str()); - if (rc != RAC_SUCCESS) { - out::error_line("failed to load reranker: " + out::describe_result(rc)); - rac_rerank_component_destroy(reranker); - return 1; - } - - v1::RerankRequest request; - request.set_query(query); - for (const std::string& document : documents) { - request.add_documents(document); - } - if (top_n > 0) { - request.mutable_options()->set_top_n(static_cast(top_n)); - } - - const std::string bytes = proto::serialize(request); - rac_proto_buffer_t out_buffer; - rac_proto_buffer_init(&out_buffer); - std::string error; - v1::RerankResult result; - if (rac_rerank_component_rerank_proto(reranker, reinterpret_cast(bytes.data()), - bytes.size(), &out_buffer) != RAC_SUCCESS || - !proto::parse_proto_buffer(&out_buffer, &result, &error)) { - out::error_line("rerank failed: " + error); - rac_rerank_component_destroy(reranker); - return 1; - } - rac_rerank_component_destroy(reranker); - - if (options.json) { - out::JsonWriter json; - json.begin_object() - .field("model", result.model_id().empty() ? model.model_id : result.model_id()) - .field("processing_time_ms", static_cast(result.processing_time_ms())) - .begin_array("results"); - // RerankScoredItem.rank is gone -- items() is already sorted by score - // descending, so the loop position IS the rank (1-based for display). - int rank = 0; - for (const v1::RerankScoredItem& item : result.items()) { - ++rank; - json.begin_array_object() - .field("index", static_cast(item.index())) - .field("rank", static_cast(rank)) - .field("relevance_score", static_cast(item.relevance_score())) - .end_object(); - } - json.end_array().end_object(); - out::result_line(json.str()); - return 0; - } - - std::vector> rows; - int rank = 0; - for (const v1::RerankScoredItem& item : result.items()) { - ++rank; - char score[32]; - std::snprintf(score, sizeof(score), "%.4f", static_cast(item.relevance_score())); - const size_t index = item.index(); - std::string preview = index < documents.size() ? documents[index] : std::string(); - if (preview.size() > 60) { - preview = preview.substr(0, 57) + "..."; - } - rows.push_back({std::to_string(rank), std::to_string(index), score, preview}); - } - out::table({"RANK", "INDEX", "SCORE", "DOCUMENT"}, rows); - return 0; -} - -} // namespace - -void register_rerank(CLI::App& app, GlobalOptions& options) { - CLI::App* cmd = app.add_subcommand("rerank", "Score documents against a query, best first"); - auto query = std::make_shared(); - auto model = std::make_shared(); - auto docs = std::make_shared>(); - auto files = std::make_shared>(); - auto top_n = std::make_shared(0); - cmd->add_option("query", *query, "Query the documents are scored against")->required(); - cmd->add_option("--model,-m", *model, "Reranker model id or on-disk path")->required(); - cmd->add_option("--doc,-d", *docs, "Document text to score; repeat for several"); - cmd->add_option("--file,-f", *files, "Text file to score; repeat for several"); - cmd->add_option("--top-n", *top_n, "Return only this many best matches"); - cmd->callback([&options, query, model, docs, files, top_n]() { - const int exit_code = run_rerank(options, *model, *query, *docs, *files, *top_n); - if (exit_code != 0) { - throw CLI::RuntimeError(exit_code); - } - }); -} - -} // namespace rcli::commands diff --git a/rcli/src/commands/cmd_rm.cpp b/rcli/src/commands/cmd_rm.cpp deleted file mode 100644 index 3b638be965..0000000000 --- a/rcli/src/commands/cmd_rm.cpp +++ /dev/null @@ -1,188 +0,0 @@ -/** - * @file cmd_rm.cpp - * @brief `rcli models delete ` (alias `rcli rm`) — delete downloaded - * files + unregister. - * - * File deletion is CLI-owned (registry remove only unregisters, per the - * rac_model_registry_remove contract). Deletion targets come from the - * registry's local_path and are confined to the models directory before - * anything is removed. - */ - -#include "commands/commands.h" -#include "commands/model_setup.h" - -#include -#include -#include -#include - -#include "model_types.pb.h" -#include "rac/core/rac_core.h" -#include "rac/infrastructure/model_management/rac_model_registry.h" - -#include "catalog/model_ref.h" -#include "io/output.h" -#include "io/proto.h" -#include "util/term.h" - -namespace rcli::commands { - -namespace { - -namespace v1 = runanywhere::v1; -namespace fs = std::filesystem; - -// Resolve the directory to delete for a model. Single-file artifacts live in -// a per-model folder ({models}/{framework}/{id}/file) — delete the folder when -// its name matches the model id, otherwise just the file itself. -fs::path deletion_target(const v1::ModelInfo &model) { - const fs::path local(model.local_path()); - std::error_code ec; - if (fs::is_directory(local, ec)) { - return local; - } - const fs::path parent = local.parent_path(); - if (parent.filename() == model.id()) { - return parent; - } - return local; -} - -// The target must live strictly inside the models root. -bool confined_to(const fs::path &target, const fs::path &models_root) { - std::error_code ec; - const fs::path canonical_target = fs::weakly_canonical(target, ec); - if (ec) { - return false; - } - const fs::path canonical_root = fs::weakly_canonical(models_root, ec); - if (ec) { - return false; - } - const std::string target_str = canonical_target.string(); - const std::string root_str = canonical_root.string(); - return target_str.size() > root_str.size() + 1 && - target_str.starts_with(root_str + "/"); -} - -bool confirm_on_tty(const std::string &prompt) { - std::fprintf(stderr, "%s [y/N] ", prompt.c_str()); - std::fflush(stderr); - char buffer[16] = {}; - if (!std::fgets(buffer, sizeof(buffer), stdin)) { - return false; - } - return buffer[0] == 'y' || buffer[0] == 'Y'; -} - -int run_rm(const GlobalOptions &options, const std::string &ref, bool force) { - Bootstrapped env; - if (bootstrap(options, &env) != RAC_SUCCESS) { - return 1; - } - - model_ref::Resolved resolved; - std::string error; - if (model_ref::resolve(ref, &resolved, &error) != RAC_SUCCESS) { - out::error_line(error); - return 1; - } - - // Link on-disk artifacts before deciding what to delete — mirrors - // list/ensure_model_ready so rm sees the same downloaded state. - if (!refresh_registry(&error)) { - out::status_line("warning: registry refresh failed: " + error); - } - - rac_proto_buffer_t model_out; - rac_proto_buffer_init(&model_out); - v1::ModelInfo model; - const rac_result_t get_rc = rac_model_registry_get_proto_buffer( - rac_get_model_registry(), resolved.model_id.c_str(), &model_out); - // parse unconditionally: it interprets the {status,error_message} envelope - // and frees the buffer on every path (no leak on get failure). - if (!proto::parse_proto_buffer(&model_out, &model, &error) || - get_rc != RAC_SUCCESS) { - out::error_line("model not found: " + resolved.model_id); - return 1; - } - - uint64_t freed_bytes = 0; - if (!model.local_path().empty()) { - const fs::path target = deletion_target(model); - if (!confined_to(target, env.models_dir)) { - out::error_line("refusing to delete " + target.string() + - " (outside models directory " + env.models_dir + ")"); - return 1; - } - std::error_code ec; - if (fs::exists(target, ec)) { - if (!force && term::stdin_is_tty() && - !confirm_on_tty("delete " + target.string() + "?")) { - out::status_line("aborted"); - return 1; - } - // Best-effort size accounting before removal. - if (fs::is_directory(target, ec)) { - for (const auto &entry : fs::recursive_directory_iterator( - target, fs::directory_options::skip_permission_denied, ec)) { - if (entry.is_regular_file(ec)) { - freed_bytes += entry.file_size(ec); - } - } - } else if (fs::is_regular_file(target, ec)) { - freed_bytes = fs::file_size(target, ec); - } - fs::remove_all(target, ec); - if (ec) { - out::error_line("failed to delete " + target.string() + ": " + - ec.message()); - return 1; - } - } - } else { - out::status_line(resolved.model_id + " has no downloaded files"); - } - - rac_proto_buffer_t remove_out; - rac_proto_buffer_init(&remove_out); - v1::ModelDeleteResult remove_result; - if (rac_model_registry_remove_proto_buffer(rac_get_model_registry(), - resolved.model_id.c_str(), - &remove_out) != RAC_SUCCESS || - !proto::parse_proto_buffer(&remove_out, &remove_result, &error)) { - out::status_line("warning: registry unregister failed: " + error); - } - - if (options.json) { - out::JsonWriter json; - json.begin_object() - .field("id", resolved.model_id) - .field("freed_bytes", static_cast(freed_bytes)) - .end_object(); - out::result_line(json.str()); - } else { - out::result_line( - "deleted " + resolved.model_id + - (freed_bytes ? " (freed " + out::human_bytes(freed_bytes) + ")" : "")); - } - return 0; -} - -} // namespace - -void configure_models_delete(CLI::App *cmd, GlobalOptions &options) { - auto ref = std::make_shared(); - auto force = std::make_shared(false); - cmd->add_option("model", *ref, "Model id or alias")->required(); - cmd->add_flag("-f,--force", *force, "Do not ask for confirmation"); - cmd->callback([&options, ref, force]() { - const int exit_code = run_rm(options, *ref, *force); - if (exit_code != 0) { - throw CLI::RuntimeError(exit_code); - } - }); -} - -} // namespace rcli::commands diff --git a/rcli/src/commands/cmd_run.cpp b/rcli/src/commands/cmd_run.cpp deleted file mode 100644 index 46652fc6c0..0000000000 --- a/rcli/src/commands/cmd_run.cpp +++ /dev/null @@ -1,767 +0,0 @@ -/** - * @file cmd_run.cpp - * @brief `rcli llm generate|stream`, `rcli vlm generate`, and the terminal - * aliases `rcli run` / `rcli chat`. - * - * Canonical SDK flow, all heavy lifting in commons: - * rac_model_lifecycle_load_proto(validate_availability=true) → auto-pulls - * missing models through the download orchestrator (progress rendered via - * DownloadProgressScope), resolves artifact paths (incl. VLM mmproj) and - * loads the engine once. - * llm generate: rac_llm_generate_proto returns one LLMGenerationResult. - * llm stream: rac_llm_generate_stream_proto streams LLMStreamEvent protos; - * ANSWER tokens go to stdout, THOUGHT tokens to stderr (dimmed, hidden with - * -q or --hide-thinking). - * VLM: rac_vlm_generate_proto (unary) returns a VLMResult. - * Ctrl-C: rac_llm_cancel_proto from the token callback thread. - * - * REPL turns are independent generations (no cross-turn memory yet — that - * needs a commons chat-session API; tracked in the rcli plan doc). - */ - -#include "commands/commands.h" - -#include -#include -#include -#include -#include -#include -#include - -#include "chat.pb.h" -#include "llm_service.pb.h" -#include "model_types.pb.h" -#include "rac/core/rac_core.h" -#include "rac/core/rac_model_lifecycle.h" -#include "rac/features/llm/rac_llm_service.h" -#include "rac/features/llm/rac_llm_stream.h" -#include "rac/features/lora/rac_lora_service.h" -#include "rac/features/vlm/rac_vlm_service.h" -#include "lora_options.pb.h" -#include "vlm_options.pb.h" - -#include "catalog/model_ref.h" -#include "commands/engine_options.h" -#include "config/cli_paths.h" -#include "io/output.h" -#include "io/proto.h" -#include "progress/progress_bar.h" -#include "repl/repl.h" -#include "util/term.h" - -namespace rcli::commands { - -namespace { - -namespace v1 = runanywhere::v1; - -// --------------------------------------------------------------------------- -// Generation parameters shared by one-shot, streaming and REPL turns. Field -// names follow LlmOptions in the public API spec. -// --------------------------------------------------------------------------- -struct RunParams { - std::string model; - std::string image; - std::string system_prompt; - std::string engine; - std::string lora; // optional LoRA adapter (.gguf) to attach before generating - float lora_scale = 1.0f; // how strongly the adapter applies - std::string reasoning = "on"; // on | off - bool show_thinking = true; // reasoning.include_in_output - float temperature = 0.0f; // 0 = engine default - float top_p = 0.0f; - float min_p = 0.0f; - float repetition_penalty = 0.0f; - float frequency_penalty = 0.0f; - float presence_penalty = 0.0f; - int32_t top_k = 0; - int32_t max_output_tokens = 1024; - int64_t seed = -1; // -1 = unset; LLMGenerationOptions.seed default is 0 - std::vector stop_sequences; -}; - -volatile std::sig_atomic_t g_interrupted = 0; - -void on_sigint(int /*signum*/) { - g_interrupted = 1; -} - -bool reasoning_off(const RunParams& params) { - return params.reasoning == "off"; -} - -// Fill LLMGenerationOptions from the parsed flags. Zero means "leave it to the -// engine default" for every sampling knob the proto declares as non-optional. -void apply_options(const RunParams& params, v1::LLMGenerationOptions* gen) { - gen->set_max_output_tokens(params.max_output_tokens); - if (params.temperature > 0.0f) { - gen->set_temperature(params.temperature); - } - if (params.top_p > 0.0f) { - gen->set_top_p(params.top_p); - } - if (params.top_k > 0) { - gen->set_top_k(params.top_k); - } - if (params.min_p > 0.0f) { - gen->set_min_p(params.min_p); - } - if (params.repetition_penalty > 0.0f) { - gen->set_repeat_penalty(params.repetition_penalty); - } - if (params.frequency_penalty != 0.0f) { - gen->set_frequency_penalty(params.frequency_penalty); - } - if (params.presence_penalty != 0.0f) { - gen->set_presence_penalty(params.presence_penalty); - } - if (params.seed >= 0) { - gen->set_seed(params.seed); - } - for (const std::string& stop : params.stop_sequences) { - gen->add_stop_sequences(stop); - } - if (!params.system_prompt.empty()) { - gen->set_system_prompt(params.system_prompt); - } - v1::ReasoningOptions* reasoning = gen->mutable_reasoning(); - if (reasoning_off(params)) { - reasoning->set_mode(v1::REASONING_MODE_OFF); - } else { - reasoning->set_include_in_output(params.show_thinking); - } -} - -// Streaming state shared with the LLM proto callback. -struct GenState { - std::mutex mutex; - std::condition_variable cv; - bool done = false; - bool cancelled = false; - std::string answer; - std::string finish_reason; - std::string error; - bool show_thoughts = false; - bool in_thought_block = false; - bool stream_to_stdout = true; // false in --json mode (accumulate only) -}; - -GenState* g_gen = nullptr; - -void llm_stream_callback(const uint8_t* event_bytes, size_t event_size, void* /*user_data*/) { - GenState* state = g_gen; - if (!state) { - return; - } - v1::LLMStreamEvent event; - if (!event.ParseFromArray(event_bytes, static_cast(event_size))) { - return; - } - - // Ctrl-C: cancel from this (normal) thread — signal handlers must not. - if (g_interrupted) { - std::lock_guard lock(state->mutex); - if (!state->cancelled) { - state->cancelled = true; - rac_proto_buffer_t cancel_event; - rac_proto_buffer_init(&cancel_event); - rac_llm_cancel_proto(&cancel_event); - rac_proto_buffer_free(&cancel_event); - } - } - - if (!event.token().empty()) { - std::lock_guard lock(state->mutex); - if (event.event_kind() == v1::LLM_STREAM_EVENT_KIND_THINKING) { - if (state->show_thoughts) { - if (!state->in_thought_block) { - std::fprintf(stderr, "%s", term::color_enabled() ? "\033[2m" : ""); - state->in_thought_block = true; - } - std::fprintf(stderr, "%s", event.token().c_str()); - std::fflush(stderr); - } - } else { - if (state->in_thought_block) { - std::fprintf(stderr, "%s\n", term::color_enabled() ? "\033[0m" : ""); - state->in_thought_block = false; - } - // Swallow the leading-whitespace artifact left by think-tag - // stripping (qwen3 emits "\n\n" before the first answer token). - std::string token = event.token(); - if (state->answer.empty()) { - const size_t first = token.find_first_not_of(" \t\r\n"); - if (first == std::string::npos) { - token.clear(); - } else { - token.erase(0, first); - } - } - if (!token.empty()) { - if (state->stream_to_stdout) { - std::fprintf(stdout, "%s", token.c_str()); - std::fflush(stdout); - } - state->answer += token; - } - } - } - - if (event.event_kind() == v1::LLM_STREAM_EVENT_KIND_COMPLETED || - event.event_kind() == v1::LLM_STREAM_EVENT_KIND_ERROR) { - std::lock_guard lock(state->mutex); - if (state->in_thought_block) { - std::fprintf(stderr, "%s\n", term::color_enabled() ? "\033[0m" : ""); - state->in_thought_block = false; - } - state->finish_reason = v1::FinishReason_Name(event.finish_reason()); - if (!event.error().message().empty()) { - state->error = event.error().message(); - } - state->done = true; - state->cv.notify_all(); - } -} - -// One blocking streaming generation; returns 0 ok, 1 error, 130 user-cancel. -int stream_once(const GlobalOptions& options, const std::string& model_id, - const std::string& prompt, const RunParams& params) { - v1::LLMGenerateRequest request; - v1::ChatMessage* message = request.add_messages(); - message->set_role(v1::MESSAGE_ROLE_USER); - message->set_content(prompt); - apply_options(params, request.mutable_options()); - (void)model_id; // lifecycle-owned state knows the loaded model - - GenState state; - state.show_thoughts = params.show_thinking && !reasoning_off(params) && !options.quiet && - !options.json; - state.stream_to_stdout = !options.json; - g_gen = &state; - g_interrupted = 0; - auto* previous_handler = std::signal(SIGINT, on_sigint); - - const auto started = std::chrono::steady_clock::now(); - const std::string bytes = proto::serialize(request); - const rac_result_t rc = rac_llm_generate_stream_proto( - reinterpret_cast(bytes.data()), bytes.size(), llm_stream_callback, - nullptr); - - int exit_code = 0; - if (rc != RAC_SUCCESS) { - out::error_line("generation failed: " + out::describe_result(rc)); - exit_code = 1; - } else { - std::unique_lock lock(state.mutex); - state.cv.wait(lock, [&state] { return state.done; }); - if (!state.answer.empty() && state.answer.back() != '\n' && !options.json) { - std::fprintf(stdout, "\n"); - } - const auto elapsed = std::chrono::duration_cast( - std::chrono::steady_clock::now() - started) - .count(); - if (!state.error.empty()) { - out::error_line("generation failed: " + state.error); - exit_code = 1; - } else if (state.cancelled) { - out::status_line("(cancelled)"); - exit_code = 130; - } else if (options.json) { - out::JsonWriter json; - json.begin_object() - .field("model", model_id) - .field("response", state.answer) - .field("finish_reason", state.finish_reason) - .field("total_ms", static_cast(elapsed)) - .end_object(); - out::result_line(json.str()); - } else if (options.verbose) { - out::status_line("(" + std::to_string(elapsed) + " ms)"); - } - } - - std::signal(SIGINT, previous_handler); - g_gen = nullptr; - return exit_code; -} - -// One unary generation (`llm generate`): the whole result lands at once. -int generate_once(const GlobalOptions& options, const std::string& model_id, - const std::string& prompt, const RunParams& params) { - v1::LLMGenerateRequest request; - v1::ChatMessage* message = request.add_messages(); - message->set_role(v1::MESSAGE_ROLE_USER); - message->set_content(prompt); - apply_options(params, request.mutable_options()); - - const std::string bytes = proto::serialize(request); - rac_proto_buffer_t out_buffer; - rac_proto_buffer_init(&out_buffer); - std::string error; - v1::LLMGenerationResult result; - if (rac_llm_generate_proto(reinterpret_cast(bytes.data()), bytes.size(), - &out_buffer) != RAC_SUCCESS || - !proto::parse_proto_buffer(&out_buffer, &result, &error)) { - out::error_line("generation failed: " + error); - return 1; - } - - if (options.json) { - out::JsonWriter json; - json.begin_object() - .field("model", result.model_used().empty() ? model_id : result.model_used()) - .field("response", result.text()) - .field("thinking", result.thinking_content()) - .field("finish_reason", v1::FinishReason_Name(result.finish_reason())) - .field("input_tokens", static_cast(result.usage().input_tokens())) - .field("output_tokens", static_cast(result.usage().output_tokens())) - .field("tokens_per_second", result.usage().decode_tokens_per_second()) - .field("total_ms", static_cast(result.generation_time_ms())) - .end_object(); - out::result_line(json.str()); - return 0; - } - if (params.show_thinking && !reasoning_off(params) && !options.quiet && - !result.thinking_content().empty()) { - std::fprintf(stderr, "%s%s%s\n", term::color_enabled() ? "\033[2m" : "", - result.thinking_content().c_str(), term::color_enabled() ? "\033[0m" : ""); - } - out::result_line(result.text()); - if (options.verbose) { - out::status_line("(" + std::to_string(static_cast(result.generation_time_ms())) + - " ms, " + std::to_string(result.usage().decode_tokens_per_second()) + - " tok/s)"); - } - return 0; -} - -bool load_model(const GlobalOptions& options, const std::string& model_id, - v1::InferenceFramework framework, bool is_vlm) { - // Auto-pull (validate_availability) + resolve + engine load, one call. - progress::DownloadProgressScope progress_scope(model_id, - !options.no_progress && !options.json); - v1::ModelLoadRequest request; - request.set_model_id(model_id); - request.set_validate_availability(true); - if (framework != v1::INFERENCE_FRAMEWORK_UNSPECIFIED) { - request.set_framework(framework); - } - if (is_vlm) { - request.set_category(v1::MODEL_CATEGORY_MULTIMODAL); - } - const std::string bytes = proto::serialize(request); - - rac_proto_buffer_t out_buffer; - rac_proto_buffer_init(&out_buffer); - std::string error; - v1::ModelLoadResult result; - if (rac_model_lifecycle_load_proto(rac_get_model_registry(), - reinterpret_cast(bytes.data()), - bytes.size(), &out_buffer) != RAC_SUCCESS || - !proto::parse_proto_buffer(&out_buffer, &result, &error)) { - out::error_line("model load failed: " + error); - return false; - } - if (!result.has_error() == false) { - out::error_line("model load failed: " + (result.error().message().empty() - ? "unknown error" - : result.error().message())); - return false; - } - if (options.verbose) { - out::status_line("loaded " + result.resolved_path()); - } - return true; -} - -int run_vlm(const GlobalOptions& options, const std::string& model_id, - const std::string& image_path, const std::string& prompt, const RunParams& params) { - v1::VLMGenerationRequest request; - request.set_model_id(model_id); - v1::VLMImage* image = request.add_images(); - image->set_file_path(image_path); - request.set_prompt(prompt.empty() ? "Describe this image." : prompt); - v1::LLMGenerationOptions* gen = request.mutable_options(); - gen->set_max_output_tokens(params.max_output_tokens); - if (params.temperature > 0.0f) { - gen->set_temperature(params.temperature); - } - if (params.top_p > 0.0f) { - gen->set_top_p(params.top_p); - } - if (params.top_k > 0) { - gen->set_top_k(params.top_k); - } - if (params.min_p > 0.0f) { - gen->set_min_p(params.min_p); - } - if (params.repetition_penalty > 0.0f) { - gen->set_repeat_penalty(params.repetition_penalty); - } - if (params.seed >= 0) { - gen->set_seed(params.seed); - } - for (const std::string& stop : params.stop_sequences) { - gen->add_stop_sequences(stop); - } - if (!params.system_prompt.empty()) { - gen->set_system_prompt(params.system_prompt); - } - v1::ReasoningOptions* reasoning = gen->mutable_reasoning(); - if (reasoning_off(params)) { - reasoning->set_mode(v1::REASONING_MODE_OFF); - } else { - reasoning->set_include_in_output(params.show_thinking); - } - // VLMGenerationRequest.options is now the shared LLMGenerationOptions, so - // frequency/presence penalty apply to VLM generation too. - if (params.frequency_penalty != 0.0f) { - gen->set_frequency_penalty(params.frequency_penalty); - } - if (params.presence_penalty != 0.0f) { - gen->set_presence_penalty(params.presence_penalty); - } - - const std::string bytes = proto::serialize(request); - rac_proto_buffer_t out_buffer; - rac_proto_buffer_init(&out_buffer); - std::string error; - v1::VLMResult result; - if (rac_vlm_generate_proto(reinterpret_cast(bytes.data()), bytes.size(), - &out_buffer) != RAC_SUCCESS || - !proto::parse_proto_buffer(&out_buffer, &result, &error)) { - out::error_line("vlm generation failed: " + error); - return 1; - } - if (!result.error().message().empty()) { - out::error_line("vlm generation failed: " + result.error().message()); - return 1; - } - - if (options.json) { - out::JsonWriter json; - json.begin_object() - .field("model", model_id) - .field("response", result.text()) - .field("total_ms", static_cast(result.total_time_ms())) - .field("tokens_per_second", - static_cast(result.usage().decode_tokens_per_second())) - .end_object(); - out::result_line(json.str()); - } else { - out::result_line(result.text()); - if (options.verbose) { - out::status_line("(" + std::to_string(result.total_time_ms()) + " ms, " + - std::to_string(result.usage().decode_tokens_per_second()) + - " tok/s)"); - } - } - return 0; -} - -void print_repl_help() { - out::status_line("commands:"); - out::status_line(" /set system set the system prompt"); - out::status_line(" /set temperature set sampling temperature"); - out::status_line(" /set max-output-tokens set the generation budget"); - out::status_line(" /show show current settings"); - out::status_line(" /bye exit (also Ctrl-D)"); - out::status_line("note: turns are independent — no conversation memory yet"); -} - -int run_repl(const GlobalOptions& options, const std::string& model_id, RunParams params) { - out::status_line("loaded " + model_id + " — type a prompt, /? for help, /bye to exit"); - repl::LineEditor editor(std::getenv("RUNANYWHERE_NOHISTORY") - ? std::string() - : paths::state_dir() + "/history"); - - std::string line; - while (editor.read_line("» ", &line)) { - if (line.empty()) { - continue; - } - editor.add_history(line); - - if (line == "/bye" || line == "/exit" || line == "/quit") { - break; - } - if (line == "/?" || line == "/help") { - print_repl_help(); - continue; - } - if (line == "/show") { - out::status_line("model " + model_id); - out::status_line("system-prompt " + - (params.system_prompt.empty() ? "(none)" : params.system_prompt)); - out::status_line("temperature " + (params.temperature > 0 - ? std::to_string(params.temperature) - : "(engine default)")); - out::status_line("max-output-tokens " + std::to_string(params.max_output_tokens)); - out::status_line("reasoning " + params.reasoning); - continue; - } - if (line.starts_with("/set ")) { - const std::string rest = line.substr(5); - if (rest.starts_with("system ")) { - params.system_prompt = rest.substr(7); - out::status_line("system prompt set"); - } else if (rest.starts_with("temperature ")) { - params.temperature = std::strtof(rest.substr(12).c_str(), nullptr); - out::status_line("temperature set"); - } else if (rest.starts_with("temp ")) { - params.temperature = std::strtof(rest.substr(5).c_str(), nullptr); - out::status_line("temperature set"); - } else if (rest.starts_with("max-output-tokens ")) { - params.max_output_tokens = - static_cast(std::strtol(rest.substr(18).c_str(), nullptr, 10)); - out::status_line("max-output-tokens set"); - } else if (rest.starts_with("max-tokens ")) { - params.max_output_tokens = - static_cast(std::strtol(rest.substr(11).c_str(), nullptr, 10)); - out::status_line("max-output-tokens set"); - } else { - out::status_line( - "unknown /set option (system | temperature | max-output-tokens)"); - } - continue; - } - if (line.starts_with("/")) { - out::status_line("unknown command — /? for help"); - continue; - } - - const int code = stream_once(options, model_id, line, params); - if (code == 1) { - return 1; // hard error; cancel (130) just returns to the prompt - } - } - return 0; -} - -std::string read_piped_prompt() { - std::string piped; - char buffer[4096]; - size_t n = 0; - while ((n = fread(buffer, 1, sizeof(buffer), stdin)) > 0) { - piped.append(buffer, n); - } - while (!piped.empty() && (piped.back() == '\n' || piped.back() == '\r')) { - piped.pop_back(); - } - return piped; -} - -// Attach a LoRA adapter to the already-loaded LLM in this same process, so the -// following generation actually uses it (adapter state is session-scoped). -bool apply_lora_adapter(const std::string& adapter_path, float scale) { - // keep_existing left unset (false): SET semantics, which is what the - // former explicit replace_existing(true) meant. LoraApplyRequest has no - // replace_existing field to set. - v1::LoraApplyRequest request; - v1::LoraAdapterConfig* adapter = request.add_adapters(); - adapter->set_adapter_path(adapter_path); - adapter->set_scale(scale); - - const std::string request_bytes = proto::serialize(request); - rac_proto_buffer_t out_buffer; - rac_proto_buffer_init(&out_buffer); - const rac_result_t rc = rac_lora_apply_proto( - reinterpret_cast(request_bytes.data()), request_bytes.size(), &out_buffer); - v1::LoraApplyResult result; - std::string error; - const bool parsed = proto::parse_proto_buffer(&out_buffer, &result, &error); - if (!parsed || rc != RAC_SUCCESS || result.has_error()) { - out::error_line("lora apply failed: " + - (result.has_error() && !result.error().message().empty() - ? result.error().message() - : (error.empty() ? std::to_string(rc) : error))); - return false; - } - return true; -} - -int run_llm(const GlobalOptions& options, LlmVerb verb, const std::string& prompt, - const RunParams& params) { - Bootstrapped env; - if (bootstrap(options, &env) != RAC_SUCCESS) { - return 1; - } - if (params.model.empty()) { - out::error_line("--model is required (a catalog id, alias, hf.co/... ref or URL)"); - return 2; - } - - EngineHintResolution engine_hint; - std::string engine_error; - if (!resolve_engine_hint(params.engine, &engine_hint, &engine_error)) { - out::error_line(engine_error); - return 2; - } - - const bool is_vlm = !params.image.empty(); - if (is_vlm) { - engine_hint.resolve_options.has_category = true; - engine_hint.resolve_options.category = v1::MODEL_CATEGORY_MULTIMODAL; - } - - model_ref::Resolved resolved; - std::string error; - if (model_ref::resolve(params.model, &resolved, &error, &engine_hint.resolve_options) != - RAC_SUCCESS) { - out::error_line(error); - return 1; - } - - // An explicit --engine is honoured whatever the ref resolved to. This used to - // read `resolved.from_catalog ? UNSPECIFIED : engine_hint.framework`, which - // silently DISCARDED the flag for anything that came out of the built-in - // catalog — `--engine ` on a catalog model did nothing at all, with no - // warning. When the flag is absent engine_hint.framework is UNSPECIFIED, so - // catalog entries still fall back to their own declared framework exactly as - // before; the only behaviour that changes is that asking now works. Mirrors - // cmd_embed.cpp. - if (!load_model(options, resolved.model_id, engine_hint.framework, is_vlm)) { - return 1; - } - if (!params.lora.empty() && !apply_lora_adapter(params.lora, params.lora_scale)) { - return 1; - } - - std::string effective_prompt = prompt; - if (effective_prompt.empty() && !term::stdin_is_tty()) { - // Piped stdin is the prompt: echo "..." | rcli llm generate -m qwen3 - effective_prompt = read_piped_prompt(); - } - - if (is_vlm) { - return run_vlm(options, resolved.model_id, params.image, effective_prompt, params); - } - if (!effective_prompt.empty()) { - return verb == LlmVerb::Generate ? generate_once(options, resolved.model_id, - effective_prompt, params) - : stream_once(options, resolved.model_id, - effective_prompt, params); - } - if (verb == LlmVerb::Chat) { - return run_repl(options, resolved.model_id, params); - } - out::error_line("no prompt given"); - return 2; -} - -// The sampling / reasoning / model flags shared by every llm and vlm command. -// VLMGenerationRequest.options is now the same LLMGenerationOptions the LLM -// path uses (VLMGenerationOptions was deleted), so llm and vlm expose an -// identical sampling surface -- the `vlm` parameter only controls whether -// --seed / --frequency-penalty / --presence-penalty are offered at all -// (kept for CLI-surface stability; both option sets now support them). -void add_generation_options(CLI::App* cmd, const std::shared_ptr& params, - ModelArg model_arg, bool vlm) { - (void)vlm; - if (model_arg == ModelArg::Option) { - cmd->add_option("--model,-m", params->model, - "Model to generate with; downloads and loads it when absent"); - } else { - cmd->add_option("model", params->model, "Model id, alias, hf.co/... ref or URL") - ->required(); - } - cmd->add_option("--system-prompt,--system", params->system_prompt, - "Steer the model with a system instruction"); - cmd->add_option("--lora", params->lora, - "Attach a LoRA adapter (.gguf) before generating"); - cmd->add_option("--lora-scale", params->lora_scale, - "How strongly the LoRA applies (default 1.0)"); - cmd->add_option("--engine", params->engine, - "Engine hint (neurt|coreml|ane, mlx, llamacpp, onnx, sherpa). Honoured for " - "catalog models too, not just URL/HF refs."); - cmd->add_option("--temperature,--temp", params->temperature, - "Raise for more random sampling (0 = engine default)"); - cmd->add_option("--top-p", params->top_p, "Keep the smallest token set above this probability"); - cmd->add_option("--top-k", params->top_k, "Sample from this many highest-probability tokens"); - cmd->add_option("--min-p", params->min_p, "Drop tokens below this share of the top token"); - cmd->add_option("--repetition-penalty", params->repetition_penalty, - "Penalize tokens already present in the context"); - cmd->add_option("--seed", params->seed, "Fix the RNG for a repeatable answer"); - cmd->add_option("--frequency-penalty", params->frequency_penalty, - "Penalize tokens by how often they have appeared"); - cmd->add_option("--presence-penalty", params->presence_penalty, - "Penalize tokens that appeared at all"); - cmd->add_option("--stop", params->stop_sequences, - "Stop as soon as this text is produced (repeat for several)"); - cmd->add_option("--max-output-tokens,--max-tokens", params->max_output_tokens, - "Cap the generated tokens (default 1024)"); - cmd->add_option("--reasoning", params->reasoning, - "Turn the model's thinking phase on or off (default on)") - ->check(CLI::IsMember({"on", "off"})); - cmd->add_flag("--show-thinking,!--hide-thinking", params->show_thinking, - "Stream thought tokens to stderr (default on)"); - cmd->add_flag_callback( - "--no-think", [params]() { params->reasoning = "off"; }, - "Older spelling of `--reasoning off`"); -} - -} // namespace - -void configure_llm(CLI::App* cmd, GlobalOptions& options, LlmVerb verb, ModelArg model_arg) { - auto params = std::make_shared(); - auto prompt = std::make_shared(); - add_generation_options(cmd, params, model_arg, false); - cmd->add_option("prompt", *prompt, - verb == LlmVerb::Chat ? "First prompt (omit for the interactive REPL)" - : "Prompt to complete (omit to read stdin)"); - if (verb == LlmVerb::Chat) { - // The REPL and VLM paths share one implementation; `run --image` stays - // the documented alias of `vlm generate`. - cmd->add_option("--image", params->image, "Describe this image instead (VLM models)") - ->check(CLI::ExistingFile); - } - cmd->callback([&options, verb, params, prompt]() { - const int exit_code = run_llm(options, verb, *prompt, *params); - if (exit_code != 0) { - throw CLI::RuntimeError(exit_code); - } - }); -} - -void configure_vlm_generate(CLI::App* cmd, GlobalOptions& options) { - auto params = std::make_shared(); - auto prompt = std::make_shared(); - add_generation_options(cmd, params, ModelArg::Option, true); - cmd->add_option("prompt", *prompt, "Question about the image (default: describe it)"); - cmd->add_option("--image,-i", params->image, "Image to look at") - ->required() - ->check(CLI::ExistingFile); - cmd->callback([&options, params, prompt]() { - const int exit_code = run_llm(options, LlmVerb::Generate, *prompt, *params); - if (exit_code != 0) { - throw CLI::RuntimeError(exit_code); - } - }); -} - -void register_llm(CLI::App& app, GlobalOptions& options) { - CLI::App* ns = app.add_subcommand("llm", "Generate text with a language model"); - ns->require_subcommand(1); - configure_llm(ns->add_subcommand("generate", "Complete a prompt and print the result"), - options, LlmVerb::Generate, ModelArg::Option); - configure_llm(ns->add_subcommand("stream", "Complete a prompt, printing tokens as they arrive"), - options, LlmVerb::Stream, ModelArg::Option); -} - -void register_vlm(CLI::App& app, GlobalOptions& options) { - CLI::App* ns = app.add_subcommand("vlm", "Ask a vision-language model about an image"); - ns->require_subcommand(1); - configure_vlm_generate(ns->add_subcommand("generate", "Answer a prompt about an image"), - options); -} - -void register_llm_aliases(CLI::App& app, GlobalOptions& options) { - configure_llm(app.add_subcommand("run", "Chat with a model (alias of `llm stream`)"), options, - LlmVerb::Chat, ModelArg::Positional); - configure_llm( - app.add_subcommand("chat", "Start an interactive session (alias of `llm stream`)"), - options, LlmVerb::Chat, ModelArg::Positional); -} - -} // namespace rcli::commands diff --git a/rcli/src/commands/cmd_segment.cpp b/rcli/src/commands/cmd_segment.cpp deleted file mode 100644 index 19c18e52fd..0000000000 --- a/rcli/src/commands/cmd_segment.cpp +++ /dev/null @@ -1,203 +0,0 @@ -/** - * @file cmd_segment.cpp - * @brief `rcli segment --model ` — segmentation via - * the commons segmentation service (image-in → per-class mask summary). - * - * Mirrors cmd_image's structure (bootstrap → resolve model → one commons path → - * render) but consumes an image instead of producing one. The model lifecycle - * is the standard service handle sequence the C ABI exposes: - * rac_segmentation_create(model) → route to the ONNX SegFormer provider - * → rac_segmentation_initialize(model_path) → load the ONNX bundle - * → rac_segmentation_segment(image, …, &result) → source-sized class mask - * + per-class summaries - * → rac_segmentation_result_free / rac_segmentation_destroy - * A `--model` naming an on-disk path is used verbatim; otherwise it is a - * catalog id resolved + auto-pulled through commons. Input is a binary PPM - * (P6); it is the dependency-free image format the CLI can decode without - * libpng/libjpeg - * (`magick in.png out.ppm`). All preprocessing/inference lives in the engine. - */ - -#include -#include -#include -#include -#include -#include - -#include "commands/commands.h" -#include "commands/model_setup.h" -#include "io/image_io.h" -#include "io/output.h" -#include "rac/features/segmentation/rac_segmentation_service.h" -#include "rac/features/segmentation/rac_segmentation_types.h" - -namespace rcli::commands { - -namespace { - -// Resolve `ref` to a local model path: an existing on-disk path is used as-is; -// otherwise it is a catalog/registry id resolved + auto-pulled through commons. -int resolve_model_path(const GlobalOptions& options, const std::string& ref, - std::string* out_path) { - std::error_code ec; - if (std::filesystem::exists(ref, ec)) { - *out_path = ref; - return 0; - } - ResolvedModelPaths model; - const int setup = ensure_model_ready(options, ref, &model); - if (setup != 0) { - return setup; - } - *out_path = model.primary_path; - return 0; -} - -void print_result(const GlobalOptions& options, const std::string& model_ref, - const rac_segmentation_result_t& result) { - if (options.json) { - out::JsonWriter json; - json.begin_object() - .field("model", result.model_id ? result.model_id : model_ref) - .field("width", static_cast(result.width)) - .field("height", static_cast(result.height)) - .field("class_count", static_cast(result.class_summary_count)) - .field("processing_time_ms", static_cast(result.processing_time_ms)); - json.begin_array("classes"); - for (size_t i = 0; i < result.class_summary_count; ++i) { - const rac_segmentation_class_summary_t& cls = result.class_summaries[i]; - json.begin_array_object() - .field("class_id", static_cast(cls.class_id)) - .field("label", cls.label ? cls.label : "") - .field("pixel_count", static_cast(cls.pixel_count)) - .field("fraction", static_cast(cls.fraction)) - .end_object(); - } - json.end_array().end_object(); - out::result_line(json.str()); - return; - } - - out::result_line("size\t" + std::to_string(result.width) + "x" + std::to_string(result.height)); - if (result.class_summary_count == 0) { - out::result_line("(no classes)"); - } else { - std::vector> rows; - rows.reserve(result.class_summary_count); - for (size_t i = 0; i < result.class_summary_count; ++i) { - const rac_segmentation_class_summary_t& cls = result.class_summaries[i]; - char pct[16]; - std::snprintf(pct, sizeof(pct), "%.1f%%", static_cast(cls.fraction) * 100.0); - rows.push_back({std::to_string(cls.class_id), cls.label ? cls.label : "", - std::to_string(cls.pixel_count), pct}); - } - out::table({"class_id", "label", "pixels", "coverage"}, rows); - } - if (options.verbose) { - out::status_line("(" + std::to_string(result.processing_time_ms) + " ms)"); - } -} - -int run_segment(const GlobalOptions& options, const std::string& image_path, - const std::string& model_ref, const std::string& diagnostic_path) { - Bootstrapped env; - if (bootstrap(options, &env) != RAC_SUCCESS) { - return 1; - } - - if (model_ref.empty()) { - out::error_line("--model is required (a segmentation model id or on-disk path)"); - return 2; - } - - std::string model_path; - const int resolve = resolve_model_path(options, model_ref, &model_path); - if (resolve != 0) { - return resolve; - } - - image::RgbImage decoded; - std::string error; - if (!image::read_ppm(image_path, &decoded, &error)) { - out::error_line(error); - return 1; - } - - rac_handle_t handle = nullptr; - rac_result_t rc = rac_segmentation_create(model_path.c_str(), &handle); - if (rc != RAC_SUCCESS || handle == nullptr) { - out::error_line("failed to create segmentation service: " + out::describe_result(rc)); - return 1; - } - - rc = rac_segmentation_initialize(handle, model_path.c_str()); - if (rc != RAC_SUCCESS) { - out::error_line("failed to load segmentation model: " + out::describe_result(rc)); - rac_segmentation_destroy(handle); - return 1; - } - - rac_segmentation_image_t image = {}; - image.data = decoded.rgb.data(); - image.data_size = decoded.rgb.size(); - image.width = decoded.width; - image.height = decoded.height; - image.stride_bytes = static_cast(decoded.width) * 3; - image.pixel_format = RAC_SEGMENTATION_PIXEL_FORMAT_RGB8; - - rac_segmentation_options_t seg_options = RAC_SEGMENTATION_OPTIONS_DEFAULT; - seg_options.include_diagnostic_rgba = diagnostic_path.empty() ? RAC_FALSE : RAC_TRUE; - rac_segmentation_result_t result = {}; - rc = rac_segmentation_segment(handle, &image, &seg_options, &result); - if (rc != RAC_SUCCESS) { - out::error_line("segmentation failed: " + out::describe_result(rc)); - rac_segmentation_result_free(&result); - rac_segmentation_cleanup(handle); - rac_segmentation_destroy(handle); - return 1; - } - - print_result(options, model_ref, result); - if (!diagnostic_path.empty()) { - if (result.diagnostic_rgba == nullptr || result.diagnostic_rgba_size == 0) { - out::status_line("warning: engine returned no diagnostic image"); - } else if (!image::write_png(diagnostic_path, result.diagnostic_rgba, - static_cast(result.width), - static_cast(result.height), &error)) { - out::error_line("failed to write " + diagnostic_path + ": " + error); - rac_segmentation_result_free(&result); - rac_segmentation_cleanup(handle); - rac_segmentation_destroy(handle); - return 1; - } - } - - rac_segmentation_result_free(&result); - rac_segmentation_cleanup(handle); - rac_segmentation_destroy(handle); - return 0; -} - -} // namespace - -void register_segment(CLI::App& app, GlobalOptions& options) { - CLI::App* cmd = app.add_subcommand("segment", "Label every pixel of an image by class"); - auto image_path = std::make_shared(); - auto model = std::make_shared(); - auto diagnostic = std::make_shared(); - cmd->add_option("image", *image_path, "Input image (binary PPM / P6)") - ->required() - ->check(CLI::ExistingFile); - cmd->add_option("--model,-m", *model, "Segmentation model id or on-disk path")->required(); - cmd->add_option("--diagnostic-image", *diagnostic, - "Also write the colored mask overlay to this PNG"); - cmd->callback([&options, image_path, model, diagnostic]() { - const int exit_code = run_segment(options, *image_path, *model, *diagnostic); - if (exit_code != 0) { - throw CLI::RuntimeError(exit_code); - } - }); -} - -} // namespace rcli::commands diff --git a/rcli/src/commands/cmd_serve.cpp b/rcli/src/commands/cmd_serve.cpp deleted file mode 100644 index 51e7f1dd4c..0000000000 --- a/rcli/src/commands/cmd_serve.cpp +++ /dev/null @@ -1,140 +0,0 @@ -/** - * @file cmd_serve.cpp - * @brief `rcli serve [model]` — OpenAI-compatible local HTTP server. - * - * Wraps the existing commons rac_server (include/rac/server/rac_server.h — - * same engine behind tools/runanywhere-server.cpp). Scope inherited from - * rac_server: LLM only, ONE model per server process. Model refs resolve - * through the same catalog/registry path as every other command, with - * auto-pull when missing. - */ - -#include "commands/commands.h" - -#include -#include -#include -#include -#include - -#if defined(RCLI_HAS_SERVER) -#include "rac/server/rac_server.h" -#endif - -#include "commands/model_setup.h" -#include "io/output.h" - -namespace rcli::commands { - -namespace { - -constexpr const char* kDefaultServeModel = "qwen3-0.6b"; - -#if defined(RCLI_HAS_SERVER) - -// Async-signal-safe shutdown: the handler only sets a flag; the main thread -// polls and performs the actual stop. Calling rac_server_stop() from signal -// context (the tools/runanywhere-server.cpp pattern) deadlocks — the handler -// interrupts rac_server_wait() on the same thread and re-enters its mutex. -volatile std::sig_atomic_t g_serve_stop = 0; - -void serve_signal_handler(int /*signum*/) { - g_serve_stop = 1; -} - -int run_serve(const GlobalOptions& options, const std::string& ref, const std::string& host, - uint16_t port, int32_t context_size, int32_t threads, int32_t gpu_layers, - bool no_cors) { - Bootstrapped env; - if (bootstrap(options, &env) != RAC_SUCCESS) { - return 1; - } - - ResolvedModelPaths model; - const int setup = - ensure_model_ready(options, ref.empty() ? kDefaultServeModel : ref, &model); - if (setup != 0) { - return setup; - } - - rac_server_config_t config = RAC_SERVER_CONFIG_DEFAULT; - config.host = host.c_str(); - config.port = port; - config.model_path = model.primary_path.c_str(); - config.model_id = model.model_id.c_str(); - config.context_size = context_size; - config.threads = threads; - config.gpu_layers = gpu_layers; - config.enable_cors = no_cors ? RAC_FALSE : RAC_TRUE; - config.verbose = options.verbose ? RAC_TRUE : RAC_FALSE; - - const rac_result_t rc = rac_server_start(&config); - if (RAC_FAILED(rc)) { - out::error_line("server start failed: " + out::describe_result(rc)); - return 1; - } - - out::status_line("serving " + model.model_id + " (LLM-only, single model)"); - out::status_line("OpenAI API: http://" + host + ":" + std::to_string(port) + - "/v1/chat/completions"); - out::status_line("health: http://" + host + ":" + std::to_string(port) + "/health"); - out::status_line("Ctrl-C to stop"); - - g_serve_stop = 0; - std::signal(SIGINT, serve_signal_handler); - std::signal(SIGTERM, serve_signal_handler); - while (!g_serve_stop && rac_server_is_running() == RAC_TRUE) { - std::this_thread::sleep_for(std::chrono::milliseconds(200)); - } - rac_server_stop(); - const int exit_code = rac_server_wait(); - - rac_server_status_t status = {}; - if (RAC_SUCCEEDED(rac_server_get_status(&status)) && options.verbose) { - out::status_line("requests: " + std::to_string(status.total_requests) + - ", tokens: " + std::to_string(status.total_tokens_generated) + - ", uptime: " + std::to_string(status.uptime_seconds) + "s"); - } - return exit_code; -} - -#endif // RCLI_HAS_SERVER - -} // namespace - -void register_serve(CLI::App& app, GlobalOptions& options) { - CLI::App* cmd = - app.add_subcommand("serve", "Serve a model over an OpenAI-compatible HTTP API"); -#if defined(RCLI_HAS_SERVER) - auto ref = std::make_shared(); - auto host = std::make_shared("127.0.0.1"); - auto port = std::make_shared(8080); - auto context = std::make_shared(8192); - auto threads = std::make_shared(4); - auto gpu_layers = std::make_shared(0); - auto no_cors = std::make_shared(false); - cmd->add_option("model", *ref, - "LLM to serve (default: " + std::string(kDefaultServeModel) + ")"); - cmd->add_option("--host,-H", *host, "Bind to this address (default 127.0.0.1)"); - cmd->add_option("--port,-p", *port, "Listen on this port (default 8080)"); - cmd->add_option("--context-length,--context,-c", *context, - "Size the context window in tokens (default 8192)"); - cmd->add_option("--threads,-t", *threads, "Run inference on this many threads (default 4)"); - cmd->add_option("--gpu-layers,--ngl", *gpu_layers, "Offload this many layers to the GPU"); - cmd->add_flag("--no-cors", *no_cors, "Refuse cross-origin browser requests"); - cmd->callback([&options, ref, host, port, context, threads, gpu_layers, no_cors]() { - const int exit_code = run_serve(options, *ref, *host, *port, *context, *threads, - *gpu_layers, *no_cors); - if (exit_code != 0) { - throw CLI::RuntimeError(exit_code); - } - }); -#else - cmd->callback([]() { - out::error_line("this rcli build does not include the server (RAC_BUILD_SERVER=OFF)"); - throw CLI::RuntimeError(1); - }); -#endif -} - -} // namespace rcli::commands diff --git a/rcli/src/commands/cmd_show.cpp b/rcli/src/commands/cmd_show.cpp deleted file mode 100644 index fec397e440..0000000000 --- a/rcli/src/commands/cmd_show.cpp +++ /dev/null @@ -1,138 +0,0 @@ -/** - * @file cmd_show.cpp - * @brief `rcli models get ` (alias `rcli show`) — registry entry - * details. - */ - -#include "commands/commands.h" -#include "commands/model_labels.h" -#include "commands/model_setup.h" - -#include -#include - -#include "model_types.pb.h" -#include "rac/core/rac_core.h" -#include "rac/infrastructure/model_management/rac_model_registry.h" - -#include "catalog/model_ref.h" -#include "io/output.h" -#include "io/proto.h" - -namespace rcli::commands { - -namespace { - -namespace v1 = runanywhere::v1; - -int run_show(const GlobalOptions &options, const std::string &ref) { - Bootstrapped env; - if (bootstrap(options, &env) != RAC_SUCCESS) { - return 1; - } - - model_ref::Resolved resolved; - std::string error; - if (model_ref::resolve(ref, &resolved, &error) != RAC_SUCCESS) { - out::error_line(error); - return 1; - } - - // Link on-disk artifacts (incl. sidecar-less rig-placed files) before - // reading state — mirrors list/ensure_model_ready so show reports the - // same downloaded state. - if (!refresh_registry(&error)) { - out::status_line("warning: registry refresh failed: " + error); - } - - rac_proto_buffer_t out_buffer; - rac_proto_buffer_init(&out_buffer); - v1::ModelInfo model; - const rac_result_t get_rc = rac_model_registry_get_proto_buffer( - rac_get_model_registry(), resolved.model_id.c_str(), &out_buffer); - // parse unconditionally: it interprets the {status,error_message} - // envelope and frees the buffer on every path (no leak on get failure). - if (!proto::parse_proto_buffer(&out_buffer, &model, &error) || - get_rc != RAC_SUCCESS) { - out::error_line("model not found: " + resolved.model_id + - (error.empty() ? "" : " (" + error + ")")); - return 1; - } - - const bool downloaded = model.registry_status() == v1::MODEL_REGISTRY_STATUS_DOWNLOADED || - !model.local_path().empty(); - - if (options.json) { - out::JsonWriter json; - json.begin_object() - .field("id", model.id()) - .field("name", model.name()) - .field("category", static_cast(model.category())) - .field("framework", static_cast(model.framework())) - .field("backend", model_labels::backend(model.framework())) - .field("format", static_cast(model.format())) - .field("download_url", model.download_url()) - .field("local_path", model.local_path()) - .field("size_bytes", static_cast(model.download_size_bytes())) - .field("context_length", static_cast(model.context_length())) - .field("supports_thinking", model.supports_thinking()) - .field("downloaded", downloaded); - if (model.has_multi_file()) { - json.begin_array("files"); - for (const v1::ModelFileDescriptor &file : model.multi_file().files()) { - json.begin_array_object() - .field("filename", file.filename()) - .field("url", file.url()) - .end_object(); - } - json.end_array(); - } - json.end_object(); - out::result_line(json.str()); - return 0; - } - - out::result_line("id " + model.id()); - out::result_line("name " + model.name()); - out::result_line("backend " + - std::string(model_labels::backend(model.framework()))); - if (model.download_size_bytes() > 0) { - out::result_line("size " + out::human_bytes(static_cast( - model.download_size_bytes()))); - } - if (model.context_length() > 0) { - out::result_line("context " + std::to_string(model.context_length())); - } - if (!model.download_url().empty()) { - out::result_line("url " + model.download_url()); - } - if (model.has_multi_file()) { - for (const v1::ModelFileDescriptor &file : model.multi_file().files()) { - out::result_line("file " + file.filename() + " (" + file.url() + - ")"); - } - } - out::result_line("downloaded " + std::string(downloaded ? "yes" : "no")); - if (!model.local_path().empty()) { - out::result_line("path " + model.local_path()); - } - if (model.supports_thinking()) { - out::result_line("thinking yes"); - } - return 0; -} - -} // namespace - -void configure_models_get(CLI::App *cmd, GlobalOptions &options) { - auto ref = std::make_shared(); - cmd->add_option("model", *ref, "Model id, alias or URL")->required(); - cmd->callback([&options, ref]() { - const int exit_code = run_show(options, *ref); - if (exit_code != 0) { - throw CLI::RuntimeError(exit_code); - } - }); -} - -} // namespace rcli::commands diff --git a/rcli/src/commands/cmd_stt.cpp b/rcli/src/commands/cmd_stt.cpp deleted file mode 100644 index 1a13e2e13c..0000000000 --- a/rcli/src/commands/cmd_stt.cpp +++ /dev/null @@ -1,164 +0,0 @@ -/** - * @file cmd_stt.cpp - * @brief `rcli stt transcribe ` — file transcription via the STT - * component (same call sequence as the commons real-inference tests). - * - * `rcli stt --input a.wav` is the same command: the options live on the `stt` - * namespace and `transcribe` is a CLI11 fallthrough alias, so both spellings - * reach one callback. - */ - -#include "commands/commands.h" - -#include -#include - -#include "rac/features/stt/rac_stt_component.h" -#include "rac/features/stt/rac_stt_service.h" - -#include "commands/model_setup.h" -#include "io/output.h" -#include "io/wav_io.h" - -namespace rcli::commands { - -namespace { - -constexpr const char* kDefaultSttModel = "sherpa-onnx-whisper-tiny.en"; -constexpr int kSttSampleRate = 16000; - -struct SttParams { - std::string model; - std::string audio; - std::string input; // --input/-i spelling of the same file - std::string language; - bool punctuation = true; - bool word_timestamps = true; - bool diarization = false; - int32_t max_speakers = 0; -}; - -int run_stt(const GlobalOptions& options, const SttParams& params) { - Bootstrapped env; - if (bootstrap(options, &env) != RAC_SUCCESS) { - return 1; - } - - const std::string& audio_path = params.audio.empty() ? params.input : params.audio; - if (audio_path.empty()) { - out::error_line("an audio file is required (positional or --input)"); - return 2; - } - - ResolvedModelPaths model; - const int setup = ensure_model_ready( - options, params.model.empty() ? kDefaultSttModel : params.model, &model); - if (setup != 0) { - return setup; - } - - wav::WavData audio; - std::string error; - if (!wav::read_wav(audio_path, &audio, &error)) { - out::error_line(error); - return 1; - } - const std::vector pcm16 = wav::resample(audio.samples, audio.sample_rate, - kSttSampleRate); - - rac_handle_t stt = nullptr; - if (rac_stt_component_create(&stt) != RAC_SUCCESS) { - out::error_line("failed to create STT component"); - return 1; - } - rac_result_t rc = rac_stt_component_load_model(stt, model.primary_path.c_str(), - model.model_id.c_str(), - model.display_name.c_str()); - if (rc != RAC_SUCCESS) { - out::error_line("failed to load STT model: " + out::describe_result(rc)); - rac_stt_component_destroy(stt); - return 1; - } - - rac_stt_options_t stt_options = RAC_STT_OPTIONS_DEFAULT; - stt_options.language = params.language.empty() ? nullptr : params.language.c_str(); - stt_options.detect_language = params.language.empty() ? RAC_TRUE : RAC_FALSE; - stt_options.enable_punctuation = params.punctuation ? RAC_TRUE : RAC_FALSE; - stt_options.enable_timestamps = params.word_timestamps ? RAC_TRUE : RAC_FALSE; - stt_options.enable_diarization = params.diarization ? RAC_TRUE : RAC_FALSE; - stt_options.max_speakers = params.max_speakers; - stt_options.sample_rate = kSttSampleRate; - - rac_stt_result_t result = {}; - rc = rac_stt_component_transcribe(stt, pcm16.data(), pcm16.size() * sizeof(int16_t), - &stt_options, &result); - - int exit_code = 0; - if (rc != RAC_SUCCESS) { - out::error_line("transcription failed: " + out::describe_result(rc)); - exit_code = 1; - } else { - const std::string text = result.text ? result.text : ""; - if (options.json) { - out::JsonWriter json; - json.begin_object() - .field("model", model.model_id) - .field("text", text) - .field("language", result.detected_language ? result.detected_language : "") - .field("confidence", static_cast(result.confidence)) - .field("total_ms", result.processing_time_ms); - json.begin_array("words"); - for (size_t i = 0; i < result.num_words; ++i) { - const rac_stt_word_t& word = result.words[i]; - json.begin_array_object() - .field("text", word.text ? word.text : "") - .field("start_ms", static_cast(word.start_ms)) - .field("end_ms", static_cast(word.end_ms)) - .field("confidence", static_cast(word.confidence)) - .end_object(); - } - json.end_array().end_object(); - out::result_line(json.str()); - } else { - out::result_line(text); - if (options.verbose) { - out::status_line("(" + std::to_string(result.processing_time_ms) + " ms)"); - } - } - rac_stt_result_free(&result); - } - - rac_stt_component_destroy(stt); - return exit_code; -} - -} // namespace - -void register_stt(CLI::App& app, GlobalOptions& options) { - CLI::App* cmd = app.add_subcommand("stt", "Turn recorded speech into text"); - cmd->require_subcommand(0, 1); - add_verb_alias(cmd, "transcribe", "Transcribe an audio file"); - - auto params = std::make_shared(); - cmd->add_option("audio", params->audio, "16-bit PCM WAV file")->check(CLI::ExistingFile); - cmd->add_option("--input,-i", params->input, "16-bit PCM WAV file")->check(CLI::ExistingFile); - cmd->add_option("--model,-m", params->model, - "STT model to use (default: " + std::string(kDefaultSttModel) + ")"); - cmd->add_option("--language", params->language, - "BCP-47 language of the speech (omit to auto-detect)"); - cmd->add_flag("--punctuation,!--no-punctuation", params->punctuation, - "Punctuate the transcript (default on)"); - cmd->add_flag("--word-timestamps,!--no-word-timestamps", params->word_timestamps, - "Report per-word timings (default on)"); - cmd->add_flag("--diarization", params->diarization, "Attribute words to speakers"); - cmd->add_option("--max-speakers", params->max_speakers, - "Cap the speakers diarization may find"); - cmd->callback([&options, params]() { - const int exit_code = run_stt(options, *params); - if (exit_code != 0) { - throw CLI::RuntimeError(exit_code); - } - }); -} - -} // namespace rcli::commands diff --git a/rcli/src/commands/cmd_telemetry.cpp b/rcli/src/commands/cmd_telemetry.cpp deleted file mode 100644 index 66342f3634..0000000000 --- a/rcli/src/commands/cmd_telemetry.cpp +++ /dev/null @@ -1,498 +0,0 @@ -/** - * @file cmd_telemetry.cpp - * @brief `rcli telemetry emit|blast` — model-free control-plane telemetry. - * - * Drives the real commons telemetry pipeline end-to-end: payloads are queued - * with rac_telemetry_manager_track, batched + serialized by commons - * (one POST per modality to /api/v2/sdk/telemetry/{modality}), and delivered - * through the CLI's HTTP callback over the registered curl transport. - * - * Development (keyless): no JWT — anonymous POST → staging backend PUBLIC org. - * Production: login handshake first (API key → JWT), then flush. - * Exits non-zero when any POST fails or any tracked event never reached the - * backend. - */ - -#include "commands/commands.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "rac/core/rac_platform_adapter.h" -#include "rac/core/rac_sdk_state.h" -#include "rac/infrastructure/network/rac_environment.h" -#include "rac/infrastructure/telemetry/rac_telemetry_manager.h" -#include "rac/infrastructure/telemetry/rac_telemetry_types.h" - -#include "io/output.h" -#include "net/control_plane.h" - -#ifndef RCLI_VERSION -#define RCLI_VERSION "0.0.0-dev" -#endif - -namespace rcli::commands { - -namespace { - -// The 12 modalities recognized by the V2 telemetry pipeline (one backend -// endpoint each), paired with a realistic terminal event type drawn from the -// canonical names the SDK emits (telemetry_manager.cpp / the backend's -// normalizer treats *.completed as terminal). -struct ModalitySpec { - const char* name; - const char* default_event_type; - // Synthetic probe identity — blast/emit always stamp these so quality - // gates never see null model_id/framework on the control-plane path. - const char* probe_model_id; - const char* probe_framework; -}; - -constexpr ModalitySpec kModalities[] = { - {"llm", "llm.generation.completed", "probe-llm-qwen2.5-0.5b", "llamacpp"}, - {"stt", "stt.transcription.completed", "probe-stt-whisper-tiny", "sherpa"}, - {"tts", "tts.synthesis.completed", "probe-tts-piper", "sherpa"}, - {"vlm", "vlm.process.completed", "probe-vlm-llava-1.5", "llamacpp"}, - {"rag", "rag.query.completed", "probe-rag-minilm", "llamacpp"}, - // `neurt` is the ENGINE identity; the framework dimension the SDK stamps for - // the Apple engine is still `coreml` (RAC_FRAMEWORK_COREML's analytics key). - {"imagegen", "imagegen.generate.completed", "probe-imagegen-sd-turbo", "coreml"}, - {"embeddings", "embeddings.embed.completed", "probe-embed-minilm", "onnx"}, - {"vad", "vad.stopped", "probe-vad-silero", "onnx"}, - {"voice", "voice.turn.metrics", "probe-voice-pipeline", "llamacpp"}, - {"lora", "lora.attach.completed", "probe-lora-base", "llamacpp"}, - {"model", "model.download.completed", "probe-model-qwen2.5-0.5b", "llamacpp"}, - {"system", "sdk.init.completed", "probe-sdk-system", "llamacpp"}, -}; - -const ModalitySpec* find_modality(const std::string& name) { - for (const ModalitySpec& spec : kModalities) { - if (name == spec.name) { - return &spec; - } - } - return nullptr; -} - -std::vector modality_names() { - std::vector names; - for (const ModalitySpec& spec : kModalities) { - names.emplace_back(spec.name); - } - return names; -} - -std::string uuid4() { - static thread_local std::mt19937_64 rng{std::random_device{}()}; - std::uniform_int_distribution dist; - uint64_t hi = dist(rng); - uint64_t lo = dist(rng); - hi = (hi & 0xFFFFFFFFFFFF0FFFull) | 0x0000000000004000ull; // version 4 - lo = (lo & 0x3FFFFFFFFFFFFFFFull) | 0x8000000000000000ull; // RFC-4122 variant - char buffer[37] = {}; - std::snprintf(buffer, sizeof(buffer), "%08" PRIx64 "-%04" PRIx64 "-%04" PRIx64 "-%04" PRIx64 - "-%012" PRIx64, - hi >> 32, (hi >> 16) & 0xFFFFull, hi & 0xFFFFull, lo >> 48, - lo & 0xFFFFFFFFFFFFull); - return buffer; -} - -// Minimal field extraction from the backend's SDKTelemetryBatchResponse JSON -// ({"success":true,"events_received":N,"events_stored":N,"events_skipped":N, -// "storage_version":"V2"}). The CLI deliberately carries no JSON parser. -int extract_int_field(const std::string& json, const std::string& key) { - const std::string needle = "\"" + key + "\":"; - const size_t pos = json.find(needle); - if (pos == std::string::npos) { - return -1; - } - return std::atoi(json.c_str() + pos + needle.size()); -} - -bool extract_bool_field(const std::string& json, const std::string& key) { - const std::string needle = "\"" + key + "\":"; - const size_t pos = json.find(needle); - return pos != std::string::npos && json.compare(pos + needle.size(), 4, "true") == 0; -} - -// Per-endpoint accounting accumulated inside the telemetry HTTP callback. -struct EndpointStats { - int posts = 0; - int failures = 0; - int last_status = 0; - int received = 0; - int stored = 0; - int skipped = 0; - std::string last_error; -}; - -struct TelemetryHttpContext { - std::map endpoints; // key: endpoint path -}; - -void telemetry_http_callback(void* user_data, const char* endpoint, const char* json_body, - size_t json_length, rac_bool_t requires_auth) { - auto* context = static_cast(user_data); - if (context == nullptr || endpoint == nullptr) { - return; - } - const net::HttpResult result = net::control_plane_post( - endpoint, std::string(json_body != nullptr ? json_body : "", json_length), - requires_auth == RAC_TRUE); - - EndpointStats& stats = context->endpoints[endpoint]; - stats.posts += 1; - stats.last_status = result.status; - if (!result.ok()) { - stats.failures += 1; - stats.last_error = result.describe(); - return; - } - if (!extract_bool_field(result.body, "success")) { - stats.failures += 1; - stats.last_error = "backend reported success=false: " + result.body; - } - const int received = extract_int_field(result.body, "events_received"); - const int stored = extract_int_field(result.body, "events_stored"); - const int skipped = extract_int_field(result.body, "events_skipped"); - stats.received += received > 0 ? received : 0; - stats.stored += stored > 0 ? stored : 0; - stats.skipped += skipped > 0 ? skipped : 0; -} - -/** Optional metric flags shared by emit and blast. Negative = unset. */ -struct MetricOptions { - double processing_ms = -1.0; - int32_t input_tokens = -1; - int32_t output_tokens = -1; - double audio_duration_ms = -1.0; -}; - -void track_events(rac_telemetry_manager_t* manager, const ModalitySpec& spec, - const std::string& event_type, const std::string& session_id, int count, - const MetricOptions& metrics) { - for (int i = 0; i < count; ++i) { - const std::string event_id = uuid4(); - rac_telemetry_payload_t payload = rac_telemetry_payload_default(); - payload.id = event_id.c_str(); - payload.event_type = event_type.c_str(); - payload.modality = spec.name; - payload.session_id = session_id.c_str(); - payload.model_id = spec.probe_model_id; - payload.model_name = spec.probe_model_id; - payload.framework = spec.probe_framework; - const int64_t now_ms = rac_get_current_time_ms(); - payload.timestamp_ms = now_ms; - payload.created_at_ms = now_ms; - payload.success = RAC_TRUE; - payload.has_success = RAC_TRUE; - if (metrics.processing_ms >= 0) { - payload.processing_time_ms = metrics.processing_ms; - payload.has_processing_time_ms = RAC_TRUE; - } - if (metrics.input_tokens >= 0) { - payload.input_tokens = metrics.input_tokens; - } - if (metrics.output_tokens >= 0) { - payload.output_tokens = metrics.output_tokens; - payload.total_tokens = (metrics.input_tokens > 0 ? metrics.input_tokens : 0) + - metrics.output_tokens; - } - // Emit only caller-supplied metrics. Do not fabricate TTFT/TPS/context - // or STT audio-length/RTF/word-count from processing_ms. - if (metrics.audio_duration_ms >= 0) { - payload.audio_duration_ms = metrics.audio_duration_ms; - } - rac_telemetry_manager_track(manager, &payload); - } -} - -struct FlushReport { - TelemetryHttpContext context; - int tracked = 0; -}; - -/** - * Login (JWT), create a manager wired to the real transport, run `track_fn`, - * flush, and account per-endpoint results. Returns false on login failure. - */ -template -bool run_telemetry_session(const GlobalOptions& options, FlushReport* report, TrackFn&& track_fn) { - Bootstrapped env; - if (bootstrap(options, &env) != RAC_SUCCESS) { - return false; - } - - // Authenticated environments need a JWT before flush. Keyless development - // posts anonymously to staging backend (PUBLIC org) — skip login. - const rac_environment_t sdk_env = rac_state_get_environment(); - if (rac_env_auth_expected(sdk_env, rac_state_get_api_key())) { - std::string error; - if (net::login(nullptr, &error) != RAC_SUCCESS) { - out::error_line(error); - return false; - } - } - - const char* device_id = rac_state_get_device_id(); - rac_telemetry_manager_t* manager = rac_telemetry_manager_create( - rac_state_get_environment(), device_id != nullptr ? device_id : "", net::platform_name(), - RCLI_VERSION); - if (manager == nullptr) { - out::error_line("telemetry manager creation failed"); - return false; - } - rac_telemetry_manager_set_device_info(manager, net::device_model().c_str(), - net::os_version_string().c_str()); - rac_telemetry_manager_set_http_callback(manager, telemetry_http_callback, &report->context); - - report->tracked = track_fn(manager); - rac_telemetry_manager_flush(manager); - rac_telemetry_manager_set_http_callback(manager, nullptr, nullptr); - rac_telemetry_manager_destroy(manager); - return true; -} - -int total_received(const FlushReport& report) { - int received = 0; - for (const auto& [endpoint, stats] : report.context.endpoints) { - received += stats.received; - } - return received; -} - -bool report_failed(const FlushReport& report) { - if (report.context.endpoints.empty()) { - return true; // nothing was POSTed — flush deferred or dropped - } - for (const auto& [endpoint, stats] : report.context.endpoints) { - if (stats.failures > 0) { - return true; - } - } - return total_received(report) != report.tracked; -} - -void render_endpoint_results(const GlobalOptions& options, const FlushReport& report) { - if (options.json) { - out::JsonWriter json; - json.begin_object() - .field("tracked", static_cast(report.tracked)) - .field("success", !report_failed(report)) - .begin_array("endpoints"); - for (const auto& [endpoint, stats] : report.context.endpoints) { - json.begin_array_object() - .field("endpoint", endpoint) - .field("posts", static_cast(stats.posts)) - .field("http_status", static_cast(stats.last_status)) - .field("events_received", static_cast(stats.received)) - .field("events_stored", static_cast(stats.stored)) - .field("events_skipped", static_cast(stats.skipped)); - if (!stats.last_error.empty()) { - json.field("error", stats.last_error); - } - json.end_object(); - } - json.end_array().end_object(); - out::result_line(json.str()); - return; - } - - if (report.context.endpoints.empty()) { - out::error_line("no telemetry batch was sent (flush deferred?)"); - return; - } - for (const auto& [endpoint, stats] : report.context.endpoints) { - std::string line = endpoint + " HTTP " + std::to_string(stats.last_status) + - " received=" + std::to_string(stats.received) + - " stored=" + std::to_string(stats.stored) + - " skipped=" + std::to_string(stats.skipped); - if (!stats.last_error.empty()) { - line += " error: " + stats.last_error; - } - out::result_line(line); - } -} - -int run_telemetry_emit(const GlobalOptions& options, const std::string& modality, - const std::string& event_type, int count, const std::string& session_id, - const MetricOptions& metrics) { - const ModalitySpec* spec = find_modality(modality); - if (spec == nullptr) { - out::error_line("unknown modality '" + modality + "'"); - return 2; - } - const std::string resolved_event_type = - event_type.empty() ? spec->default_event_type : event_type; - const std::string resolved_session = session_id.empty() ? uuid4() : session_id; - - FlushReport report; - const bool session_ok = run_telemetry_session( - options, &report, [&](rac_telemetry_manager_t* manager) { - track_events(manager, *spec, resolved_event_type, resolved_session, count, metrics); - return count; - }); - if (!session_ok) { - return 1; - } - - if (!options.json) { - out::status_line("emitted " + std::to_string(count) + " × " + resolved_event_type + - " (modality " + modality + ", session " + resolved_session + ")"); - } - render_endpoint_results(options, report); - return report_failed(report) ? 1 : 0; -} - -int run_telemetry_blast(const GlobalOptions& options, int count, const std::string& session_id, - const MetricOptions& metrics) { - const std::string resolved_session = session_id.empty() ? uuid4() : session_id; - - FlushReport report; - const bool session_ok = run_telemetry_session( - options, &report, [&](rac_telemetry_manager_t* manager) { - for (const ModalitySpec& spec : kModalities) { - track_events(manager, spec, spec.default_event_type, resolved_session, count, - metrics); - } - return count * static_cast(std::size(kModalities)); - }); - if (!session_ok) { - return 1; - } - - bool all_ok = true; - std::vector> rows; - for (const ModalitySpec& spec : kModalities) { - const std::string endpoint = std::string("/api/v2/sdk/telemetry/") + spec.name; - const auto it = report.context.endpoints.find(endpoint); - std::string status = "NO POST"; - int received = 0; - int stored = 0; - int skipped = 0; - bool row_ok = false; - if (it != report.context.endpoints.end()) { - const EndpointStats& stats = it->second; - received = stats.received; - stored = stats.stored; - skipped = stats.skipped; - row_ok = stats.failures == 0 && stats.last_status == 200 && - stats.received >= count && stats.stored >= count; - status = row_ok ? ("HTTP " + std::to_string(stats.last_status)) - : (stats.last_error.empty() - ? "HTTP " + std::to_string(stats.last_status) - : stats.last_error); - } - all_ok = all_ok && row_ok; - rows.push_back({spec.name, row_ok ? "ok" : "FAILED", status, std::to_string(received), - std::to_string(stored), std::to_string(skipped)}); - } - - if (options.json) { - out::JsonWriter json; - json.begin_object() - .field("tracked", static_cast(report.tracked)) - .field("success", all_ok) - .field("session_id", resolved_session) - .begin_array("modalities"); - for (const auto& row : rows) { - json.begin_array_object() - .field("modality", row[0]) - .field("ok", row[1] == "ok") - .field("status", row[2]) - .field("events_received", static_cast(std::atoi(row[3].c_str()))) - .field("events_stored", static_cast(std::atoi(row[4].c_str()))) - .field("events_skipped", static_cast(std::atoi(row[5].c_str()))) - .end_object(); - } - json.end_array().end_object(); - out::result_line(json.str()); - } else { - out::status_line("blast session " + resolved_session + " — " + - std::to_string(report.tracked) + " event(s) across " + - std::to_string(std::size(kModalities)) + " modalities"); - out::table({"MODALITY", "RESULT", "STATUS", "RECEIVED", "STORED", "SKIPPED"}, rows); - } - return all_ok ? 0 : 1; -} - -} // namespace - -void register_telemetry(CLI::App& app, GlobalOptions& options) { - CLI::App* cmd = app.add_subcommand( - "telemetry", "Emit model-free telemetry through the real control-plane pipeline"); - cmd->require_subcommand(1); - - // ---- telemetry emit ---------------------------------------------------- - CLI::App* emit_cmd = cmd->add_subcommand( - "emit", - "Track N events of one modality, flush to /api/v2/sdk/telemetry/{modality} " - "and report the backend's accounting. Production runs the auth handshake " - "first; development is keyless. Exits non-zero when any POST fails."); - auto modality = std::make_shared(); - auto event_type = std::make_shared(); - auto count = std::make_shared(1); - auto session_id = std::make_shared(); - auto metrics = std::make_shared(); - emit_cmd->add_option("--modality", *modality, "Telemetry modality") - ->required() - ->check(CLI::IsMember(modality_names())); - emit_cmd->add_option("--event-type", *event_type, - "Event type string (default: the modality's terminal event, e.g. " - "llm.generation.completed)"); - emit_cmd->add_option("--count", *count, "Number of events to emit (default 1)") - ->check(CLI::PositiveNumber); - emit_cmd->add_option("--session-id", *session_id, - "Session id attached to every event (default: fresh UUID)"); - emit_cmd->add_option("--processing-ms", metrics->processing_ms, - "processing_time_ms metric for every event"); - emit_cmd->add_option("--input-tokens", metrics->input_tokens, - "input_tokens metric (llm/vlm modalities)"); - emit_cmd->add_option("--output-tokens", metrics->output_tokens, - "output_tokens metric (llm/vlm modalities)"); - emit_cmd->add_option("--audio-duration-ms", metrics->audio_duration_ms, - "audio_duration_ms metric (stt modality)"); - emit_cmd->callback([&options, modality, event_type, count, session_id, metrics]() { - const int exit_code = run_telemetry_emit(options, *modality, *event_type, *count, - *session_id, *metrics); - if (exit_code != 0) { - throw CLI::RuntimeError(exit_code); - } - }); - - // ---- telemetry blast --------------------------------------------------- - CLI::App* blast_cmd = cmd->add_subcommand( - "blast", - "Emit --count events of EVERY modality (all 12) in one run, flush, and " - "print a per-modality result table parsed from the backend's batch " - "responses. Emits one event of every modality."); - auto blast_count = std::make_shared(1); - auto blast_session = std::make_shared(); - auto blast_metrics = std::make_shared(); - blast_cmd->add_option("--count", *blast_count, "Events per modality (default 1)") - ->check(CLI::PositiveNumber); - blast_cmd->add_option("--session-id", *blast_session, - "Session id attached to every event (default: fresh UUID)"); - blast_cmd->add_option("--processing-ms", blast_metrics->processing_ms, - "processing_time_ms metric for every event"); - blast_cmd->add_option("--input-tokens", blast_metrics->input_tokens, - "input_tokens metric (llm/vlm modalities)"); - blast_cmd->add_option("--output-tokens", blast_metrics->output_tokens, - "output_tokens metric (llm/vlm modalities)"); - blast_cmd->callback([&options, blast_count, blast_session, blast_metrics]() { - const int exit_code = - run_telemetry_blast(options, *blast_count, *blast_session, *blast_metrics); - if (exit_code != 0) { - throw CLI::RuntimeError(exit_code); - } - }); -} - -} // namespace rcli::commands diff --git a/rcli/src/commands/cmd_tool.cpp b/rcli/src/commands/cmd_tool.cpp deleted file mode 100644 index 1d4cb420a7..0000000000 --- a/rcli/src/commands/cmd_tool.cpp +++ /dev/null @@ -1,286 +0,0 @@ -/** - * @file cmd_tool.cpp - * @brief `rcli llm tool-call` — exercise the tool-calling loop end to end. - * - * Thin wrapper over rac_tool_calling_run_loop_proto: load an LLM, hand commons - * a prompt plus two built-in demo tools (get_weather, calculate), and let - * commons drive the whole decide → call → execute → synthesize loop. The host - * executor here returns canned JSON so the loop can complete offline; the point - * is to see whether a given model actually emits a well-formed tool call and - * whether commons parses it and produces a grounded final answer. - */ - -#include "commands/commands.h" - -#include -#include -#include - -#include "model_types.pb.h" -#include "tool_calling.pb.h" - -#include "rac/core/rac_core.h" -#include "rac/core/rac_model_lifecycle.h" -#include "rac/features/llm/rac_tool_calling.h" -#include "rac/foundation/rac_proto_buffer.h" - -#include "bootstrap.h" -#include "catalog/model_ref.h" -#include "commands/engine_options.h" -#include "io/output.h" -#include "io/proto.h" -#include "progress/progress_bar.h" - -namespace rcli::commands { -namespace { - -namespace v1 = runanywhere::v1; - -// Small instruct GGUF that ships in the built-in catalog. Override with --model -// (e.g. `lfm2-350m-q8_0` to exercise the LFM2 tool-call format path). -constexpr const char* kDefaultToolModel = "qwen3-0.6b"; - -struct ToolCallParams { - std::string prompt; - std::string model; - std::string engine; - std::string tool_choice; // auto | required | none | specific (default auto) - std::string force_tool; // name for tool_choice=specific - int max_tool_calls = 3; -}; - -bool parse_tool_choice(const std::string& mode, v1::ToolChoiceMode* out) { - if (mode.empty() || mode == "auto") { - *out = v1::TOOL_CHOICE_MODE_AUTO; - } else if (mode == "required") { - *out = v1::TOOL_CHOICE_MODE_REQUIRED; - } else if (mode == "none") { - *out = v1::TOOL_CHOICE_MODE_NONE; - } else if (mode == "specific") { - *out = v1::TOOL_CHOICE_MODE_SPECIFIC; - } else { - return false; - } - return true; -} - -// rac_tool_calling_run_loop_proto invokes this synchronously and unconditionally -// (it is not null-checked), so a real no-op is required even when the CLI has no -// use for the cancellable handle. -void ignore_published_handle(uint64_t /*handle*/, void* /*user_data*/) {} - -// ToolParameter is gone: ToolDefinition.parameters is now one OpenAI-style -// JSON Schema object describing all of a tool's arguments (the same shape -// solutions.proto's ToolSpec already carries). -void set_single_string_param_schema(v1::ToolDefinition* tool, const char* name, - const char* description) { - tool->set_parameters(std::string(R"({"type":"object","properties":{")") + name + - R"(":{"type":"string","description":")" + description + - R"("}},"required":[")" + name + R"("]})"); -} - -// Synchronous host executor: commons hands us a serialized ToolCall and expects -// an owned serialized ToolResult back. We echo the call to stderr and return a -// canned result per tool so the loop can synthesize a final answer offline. -rac_result_t demo_executor(const uint8_t* in_bytes, size_t in_size, rac_proto_buffer_t* out_result, - void* user_data) { - (void)user_data; - v1::ToolCall call; - if (in_size > 0) { - (void)call.ParseFromArray(in_bytes, static_cast(in_size)); - } - out::status_line(" executing " + call.name() + "(" + call.arguments_json() + ")"); - - v1::ToolResult result; - result.set_tool_call_id(call.id()); - result.set_name(call.name()); - result.set_is_error(false); - if (call.name() == "get_weather") { - result.set_result_json(R"({"temperature_c":18,"condition":"cloudy"})"); - } else if (call.name() == "calculate") { - result.set_result_json(R"({"note":"demo executor does not evaluate expressions"})"); - } else { - result.set_result_json(R"({"ok":true})"); - } - - const std::string bytes = proto::serialize(result); - rac_proto_buffer_init(out_result); - return rac_proto_buffer_copy( - bytes.empty() ? nullptr : reinterpret_cast(bytes.data()), bytes.size(), - out_result); -} - -bool load_model(const GlobalOptions& options, const std::string& model_id, - v1::InferenceFramework framework) { - progress::DownloadProgressScope progress_scope(model_id, !options.no_progress && !options.json); - v1::ModelLoadRequest request; - request.set_model_id(model_id); - request.set_validate_availability(true); - if (framework != v1::INFERENCE_FRAMEWORK_UNSPECIFIED) { - request.set_framework(framework); - } - const std::string bytes = proto::serialize(request); - - rac_proto_buffer_t out_buffer; - rac_proto_buffer_init(&out_buffer); - std::string error; - v1::ModelLoadResult result; - if (rac_model_lifecycle_load_proto(rac_get_model_registry(), - reinterpret_cast(bytes.data()), bytes.size(), - &out_buffer) != RAC_SUCCESS || - !proto::parse_proto_buffer(&out_buffer, &result, &error)) { - out::error_line("model load failed: " + error); - return false; - } - if (result.has_error()) { - out::error_line("model load failed: " + (result.error().message().empty() - ? "unknown error" - : result.error().message())); - return false; - } - return true; -} - -int run_tool_call(const GlobalOptions& options, const ToolCallParams& params) { - Bootstrapped env; - if (bootstrap(options, &env) != RAC_SUCCESS) { - return 1; - } - - EngineHintResolution engine_hint; - std::string engine_error; - if (!resolve_engine_hint(params.engine, &engine_hint, &engine_error)) { - out::error_line(engine_error); - return 2; - } - engine_hint.resolve_options.has_category = true; - engine_hint.resolve_options.category = v1::MODEL_CATEGORY_LANGUAGE; - - model_ref::Resolved resolved; - std::string error; - if (model_ref::resolve(params.model, &resolved, &error, &engine_hint.resolve_options) != - RAC_SUCCESS) { - out::error_line(error); - return 1; - } - - const v1::InferenceFramework load_framework = - resolved.from_catalog ? v1::INFERENCE_FRAMEWORK_UNSPECIFIED : engine_hint.framework; - if (!load_model(options, resolved.model_id, load_framework)) { - return 1; - } - - // ToolCallingSessionCreateRequest collapsed to {prompt, history, options}: - // max_tokens/auto_execute/max_tool_calls/tools/tool_choice/forced_tool_name - // all now live on the nested ToolCallingOptions (there is no standalone - // max_tokens/temperature slot anywhere on this request any more -- - // sampling for the tool loop comes from the enclosing generation state's - // own defaults). - v1::ToolCallingSessionCreateRequest request; - request.set_prompt(params.prompt); - v1::ToolCallingOptions* options_pb = request.mutable_options(); - options_pb->set_auto_execute(true); - if (params.max_tool_calls > 0) { - options_pb->set_max_tool_calls(params.max_tool_calls); - } - - v1::ToolDefinition* weather = options_pb->add_tools(); - weather->set_name("get_weather"); - weather->set_description("Get the current weather for a city"); - set_single_string_param_schema(weather, "location", "City name, e.g. Tokyo"); - - v1::ToolDefinition* calc = options_pb->add_tools(); - calc->set_name("calculate"); - calc->set_description("Evaluate an arithmetic expression"); - set_single_string_param_schema(calc, "expression", "Expression such as 45 * 12"); - - v1::ToolChoiceMode choice = v1::TOOL_CHOICE_MODE_AUTO; - if (!parse_tool_choice(params.tool_choice, &choice)) { - out::error_line("--tool-choice expects auto|required|none|specific"); - return 2; - } - if (!params.force_tool.empty()) { - options_pb->set_tool_choice(v1::TOOL_CHOICE_MODE_SPECIFIC); - options_pb->set_forced_tool_name(params.force_tool); - } else if (choice != v1::TOOL_CHOICE_MODE_AUTO) { - options_pb->set_tool_choice(choice); - } - - const std::string bytes = proto::serialize(request); - rac_proto_buffer_t out_buffer; - rac_proto_buffer_init(&out_buffer); - const rac_result_t rc = rac_tool_calling_run_loop_proto( - reinterpret_cast(bytes.data()), bytes.size(), demo_executor, nullptr, - ignore_published_handle, nullptr, &out_buffer); - - // The run loop writes a structured ToolCallingResult even when it returns a - // non-success rc (e.g. a generation failure lands in error_code/error_message), - // so parse the envelope first and report from it rather than the bare rc. - std::string parse_error; - v1::ToolCallingResult result; - if (!proto::parse_proto_buffer(&out_buffer, &result, &parse_error)) { - out::error_line("tool-calling failed: " + - (parse_error.empty() ? std::to_string(rc) : parse_error)); - return 1; - } - - if (options.json) { - out::JsonWriter json; - json.begin_object() - .field("text", result.text()) - .field("is_complete", result.is_complete()) - .field("iterations", static_cast(result.iterations_used())) - .field("tool_calls", static_cast(result.tool_calls_size())) - .end_object(); - out::result_line(json.str()); - return result.is_complete() ? 0 : 1; - } - - for (int i = 0; i < result.tool_calls_size(); ++i) { - const v1::ToolCall& call = result.tool_calls(i); - out::status_line("tool call " + std::to_string(i + 1) + ": " + call.name() + "(" + - call.arguments_json() + ")"); - } - if (result.error_code() != 0) { - out::error_line("tool-calling error: " + result.error_message()); - } - out::status_line("iterations: " + std::to_string(result.iterations_used()) + - ", tool calls: " + std::to_string(result.tool_calls_size())); - out::result_line(result.text()); - return result.is_complete() ? 0 : 1; -} - -void configure_tool_call(CLI::App* cmd, GlobalOptions& options) { - auto params = std::make_shared(); - cmd->add_option("prompt", params->prompt, "What to ask the model")->required(); - cmd->add_option("--model,-m", params->model, "Model to use for the tool-calling loop") - ->default_val(kDefaultToolModel); - cmd->add_option("--engine", params->engine, "Pin a specific inference engine"); - cmd->add_option("--tool-choice", params->tool_choice, - "How the model may call tools: auto|required|none|specific"); - cmd->add_option("--force-tool", params->force_tool, - "Force one tool by name (implies --tool-choice specific)"); - cmd->add_option("--max-tool-calls", params->max_tool_calls, - "Maximum host tool executions per turn"); - cmd->callback([&options, params]() { - const int exit_code = run_tool_call(options, *params); - if (exit_code != 0) { - throw CLI::RuntimeError(exit_code); - } - }); -} - -} // namespace - -void register_tool(CLI::App& app, GlobalOptions& options) { - // Tool calling is an LLM capability, so it lives under the `llm` namespace - // that register_llm() already created (app.cpp registers llm first). - CLI::App* ns = app.get_subcommand("llm"); - configure_tool_call( - ns->add_subcommand("tool-call", - "Run the tool-calling loop with built-in demo tools (get_weather, " - "calculate)"), - options); -} - -} // namespace rcli::commands diff --git a/rcli/src/commands/cmd_tts.cpp b/rcli/src/commands/cmd_tts.cpp deleted file mode 100644 index 0a574382ca..0000000000 --- a/rcli/src/commands/cmd_tts.cpp +++ /dev/null @@ -1,162 +0,0 @@ -/** - * @file cmd_tts.cpp - * @brief `rcli tts synthesize "text" --output o.wav` — speech synthesis. - * - * `rcli tts --text "…" --output o.wav` is the same command: the options live on - * the `tts` namespace and `synthesize` is a CLI11 fallthrough alias. - * - * The sherpa TTS engine returns float PCM at the voice's native sample rate - * (see tests/test_voice_agent.cpp fixture synthesis); converted to int16 WAV. - */ - -#include -#include -#include - -#include "commands/commands.h" -#include "commands/model_setup.h" -#include "io/output.h" -#include "io/wav_io.h" -#include "rac/features/tts/rac_tts_component.h" -#include "rac/features/tts/rac_tts_types.h" - -namespace rcli::commands { - -namespace { - -constexpr const char* kDefaultVoice = "vits-piper-en_US-lessac-medium"; - -struct TtsParams { - std::string model; - std::string voice; - std::string positional_text; - std::string text; // --text/-t spelling of the same string - std::string output; - std::string language; - float speed = 1.0f; - float pitch = 1.0f; - int32_t sample_rate = 0; // 0 = the voice's native rate -}; - -int run_tts(const GlobalOptions& options, const TtsParams& params) { - Bootstrapped env; - if (bootstrap(options, &env) != RAC_SUCCESS) { - return 1; - } - - const std::string& text = params.positional_text.empty() ? params.text : params.positional_text; - if (text.empty()) { - out::error_line("text to speak is required (positional or --text)"); - return 2; - } - if (params.output.empty()) { - out::error_line("--output is required"); - return 2; - } - - const std::string& ref = !params.model.empty() ? params.model : params.voice; - ResolvedModelPaths voice; - const int setup = ensure_model_ready(options, ref.empty() ? kDefaultVoice : ref, &voice); - if (setup != 0) { - return setup; - } - - rac_handle_t tts = nullptr; - if (rac_tts_component_create(&tts) != RAC_SUCCESS) { - out::error_line("failed to create TTS component"); - return 1; - } - rac_result_t rc = rac_tts_component_load_voice( - tts, voice.primary_path.c_str(), voice.model_id.c_str(), voice.display_name.c_str()); - if (rc != RAC_SUCCESS) { - out::error_line("failed to load voice: " + out::describe_result(rc)); - rac_tts_component_destroy(tts); - return 1; - } - - rac_tts_options_t tts_options = RAC_TTS_OPTIONS_DEFAULT; - tts_options.voice = params.voice.empty() ? nullptr : params.voice.c_str(); - if (!params.language.empty()) { - tts_options.language = params.language.c_str(); - } - tts_options.rate = params.speed; - tts_options.pitch = params.pitch; - if (params.sample_rate > 0) { - tts_options.sample_rate = params.sample_rate; - } - - const auto started = std::chrono::steady_clock::now(); - rac_tts_result_t result = {}; - rc = rac_tts_component_synthesize(tts, text.c_str(), &tts_options, &result); - const auto elapsed = std::chrono::duration_cast( - std::chrono::steady_clock::now() - started) - .count(); - - int exit_code = 0; - if (rc != RAC_SUCCESS || !result.audio_data || result.audio_size == 0) { - out::error_line("synthesis failed: " + out::describe_result(rc)); - exit_code = 1; - } else { - // Engine emits float PCM at the voice's native rate; commons builds the WAV. - const auto* float_samples = static_cast(result.audio_data); - const size_t sample_count = result.audio_size / sizeof(float); - - std::string error; - if (!wav::write_wav_f32(params.output, float_samples, sample_count, result.sample_rate, - &error)) { - out::error_line(error); - exit_code = 1; - } else if (options.json) { - out::JsonWriter json; - json.begin_object() - .field("voice", voice.model_id) - .field("path", params.output) - .field("sample_rate", static_cast(result.sample_rate)) - .field("duration_ms", static_cast(result.duration_ms)) - .field("total_ms", static_cast(elapsed)) - .end_object(); - out::result_line(json.str()); - } else { - out::result_line(params.output); - if (options.verbose) { - out::status_line("(" + std::to_string(elapsed) + " ms, " + - std::to_string(result.sample_rate) + " Hz)"); - } - } - rac_tts_result_free(&result); - } - - rac_tts_component_destroy(tts); - return exit_code; -} - -} // namespace - -void register_tts(CLI::App& app, GlobalOptions& options) { - CLI::App* cmd = app.add_subcommand("tts", "Speak text with an on-device voice"); - cmd->require_subcommand(0, 1); - add_verb_alias(cmd, "synthesize", "Write spoken audio to a WAV file"); - - auto params = std::make_shared(); - // CLI11 matches option names without their dashes, so the positional - // cannot also be called "text" while `--text` exists. - cmd->add_option("TEXT", params->positional_text, "Text to speak"); - cmd->add_option("--text,-t", params->text, "Text to speak"); - cmd->add_option("--output,-o", params->output, "WAV file to write"); - cmd->add_option("--model,-m", params->model, - "Voice model to load (default: " + std::string(kDefaultVoice) + ")"); - cmd->add_option("--voice", params->voice, "Voice inside the model to speak with"); - cmd->add_option("--language", params->language, "BCP-47 language to speak (default en-US)"); - cmd->add_option("--speed", params->speed, "Speak faster or slower than 1.0"); - cmd->add_option("--pitch", params->pitch, "Raise or lower the pitch from 1.0"); - cmd->add_option("--sample-rate", params->sample_rate, - "Output sample rate in Hz (0 = the voice's own)"); - cmd->callback([&options, params]() { - const int exit_code = run_tts(options, *params); - if (exit_code != 0) { - throw CLI::RuntimeError(exit_code); - } - }); -} - -} // namespace rcli::commands diff --git a/rcli/src/commands/cmd_vad.cpp b/rcli/src/commands/cmd_vad.cpp deleted file mode 100644 index 096ad71a31..0000000000 --- a/rcli/src/commands/cmd_vad.cpp +++ /dev/null @@ -1,179 +0,0 @@ -/** - * @file cmd_vad.cpp - * @brief `rcli vad detect ` — speech segment detection. - * - * `rcli vad --input a.wav` is the same command: the options live on the `vad` - * namespace and `detect` is a CLI11 fallthrough alias. - * - * Feeds 16 kHz float frames through the VAD component (Silero when the model - * is loaded, energy-based otherwise) and derives segments from - * is_speech_active transitions. - */ - -#include "commands/commands.h" - -#include -#include -#include - -#include "rac/features/vad/rac_vad_component.h" - -#include "commands/model_setup.h" -#include "io/output.h" -#include "io/wav_io.h" - -namespace rcli::commands { - -namespace { - -constexpr const char* kDefaultVadModel = "silero-vad"; -constexpr int kVadSampleRate = 16000; -constexpr size_t kVadFrameSamples = 512; // Silero's native frame size @16 kHz - -struct Segment { - double start_s; - double end_s; -}; - -struct VadParams { - std::string model; - std::string audio; - std::string input; // --input/-i spelling of the same file - float activation_threshold = 0.0f; -}; - -int run_vad(const GlobalOptions& options, const VadParams& params) { - Bootstrapped env; - if (bootstrap(options, &env) != RAC_SUCCESS) { - return 1; - } - - const std::string& input = params.audio.empty() ? params.input : params.audio; - if (input.empty()) { - out::error_line("an audio file is required (positional or --input)"); - return 2; - } - - ResolvedModelPaths model; - const int setup = ensure_model_ready( - options, params.model.empty() ? kDefaultVadModel : params.model, &model); - if (setup != 0) { - return setup; - } - - wav::WavData audio; - std::string error; - if (!wav::read_wav(input, &audio, &error)) { - out::error_line(error); - return 1; - } - const std::vector pcm16 = wav::resample(audio.samples, audio.sample_rate, - kVadSampleRate); - const std::vector samples = wav::to_float(pcm16); - - rac_handle_t vad = nullptr; - if (rac_vad_component_create(&vad) != RAC_SUCCESS) { - out::error_line("failed to create VAD component"); - return 1; - } - rac_result_t rc = rac_vad_component_load_model(vad, model.primary_path.c_str(), - model.model_id.c_str(), - model.display_name.c_str()); - if (rc != RAC_SUCCESS) { - out::error_line("failed to load VAD model: " + out::describe_result(rc)); - rac_vad_component_destroy(vad); - return 1; - } - if (params.activation_threshold > 0.0f && - rac_vad_component_set_energy_threshold(vad, params.activation_threshold) != RAC_SUCCESS) { - out::error_line("invalid --activation-threshold (expected 0.0-1.0)"); - rac_vad_component_destroy(vad); - return 2; - } - if (rac_vad_component_initialize(vad) != RAC_SUCCESS || - rac_vad_component_start(vad) != RAC_SUCCESS) { - out::error_line("failed to start VAD"); - rac_vad_component_destroy(vad); - return 1; - } - - std::vector segments; - bool in_speech = false; - double segment_start = 0.0; - for (size_t offset = 0; offset + kVadFrameSamples <= samples.size(); - offset += kVadFrameSamples) { - rac_bool_t frame_is_speech = RAC_FALSE; - rac_vad_component_process(vad, samples.data() + offset, kVadFrameSamples, - &frame_is_speech); - const bool active = frame_is_speech == RAC_TRUE; - const double t = static_cast(offset + kVadFrameSamples) / kVadSampleRate; - if (active && !in_speech) { - in_speech = true; - segment_start = static_cast(offset) / kVadSampleRate; - } else if (!active && in_speech) { - in_speech = false; - segments.push_back({segment_start, t}); - } - } - if (in_speech) { - segments.push_back( - {segment_start, static_cast(samples.size()) / kVadSampleRate}); - } - rac_vad_component_stop(vad); - rac_vad_component_destroy(vad); - - if (options.json) { - out::JsonWriter json; - json.begin_object().field("model", model.model_id).begin_array("segments"); - for (const Segment& segment : segments) { - json.begin_array_object() - .field("start_s", segment.start_s) - .field("end_s", segment.end_s) - .end_object(); - } - json.end_array().end_object(); - out::result_line(json.str()); - return 0; - } - - if (segments.empty()) { - out::result_line("no speech detected"); - return 0; - } - std::vector> rows; - char buffer[64]; - for (const Segment& segment : segments) { - std::snprintf(buffer, sizeof(buffer), "%.2fs", segment.start_s); - std::string start = buffer; - std::snprintf(buffer, sizeof(buffer), "%.2fs", segment.end_s); - std::string end = buffer; - std::snprintf(buffer, sizeof(buffer), "%.2fs", segment.end_s - segment.start_s); - rows.push_back({start, end, buffer}); - } - out::table({"START", "END", "DURATION"}, rows); - return 0; -} - -} // namespace - -void register_vad(CLI::App& app, GlobalOptions& options) { - CLI::App* cmd = app.add_subcommand("vad", "Find the speech in an audio file"); - cmd->require_subcommand(0, 1); - add_verb_alias(cmd, "detect", "Report speech segments with timestamps"); - - auto params = std::make_shared(); - cmd->add_option("audio", params->audio, "16-bit PCM WAV file")->check(CLI::ExistingFile); - cmd->add_option("--input,-i", params->input, "16-bit PCM WAV file")->check(CLI::ExistingFile); - cmd->add_option("--model,-m", params->model, - "VAD model to use (default: " + std::string(kDefaultVadModel) + ")"); - cmd->add_option("--activation-threshold", params->activation_threshold, - "Speech probability needed to open a segment (0 = model default)"); - cmd->callback([&options, params]() { - const int exit_code = run_vad(options, *params); - if (exit_code != 0) { - throw CLI::RuntimeError(exit_code); - } - }); -} - -} // namespace rcli::commands diff --git a/rcli/src/commands/cmd_version.cpp b/rcli/src/commands/cmd_version.cpp deleted file mode 100644 index f5626d3b70..0000000000 --- a/rcli/src/commands/cmd_version.cpp +++ /dev/null @@ -1,40 +0,0 @@ -/** - * @file cmd_version.cpp - * @brief `rcli version` — CLI + commons versions. No bootstrap needed. - */ - -#include "commands/commands.h" - -#include - -#include "rac/core/rac_core.h" - -#include "io/output.h" - -#ifndef RCLI_VERSION -#define RCLI_VERSION "0.0.0-dev" -#endif - -namespace rcli::commands { - -void register_version(CLI::App& app, GlobalOptions& options) { - CLI::App* cmd = app.add_subcommand("version", "Show rcli and commons versions"); - cmd->callback([&options]() { - const rac_version_t commons = rac_get_version(); - const std::string commons_version = - commons.string ? commons.string : "unknown"; - if (options.json) { - out::JsonWriter json; - json.begin_object() - .field("rcli", RCLI_VERSION) - .field("commons", commons_version) - .end_object(); - out::result_line(json.str()); - } else { - out::result_line(std::string("rcli ") + RCLI_VERSION + " (commons " + - commons_version + ")"); - } - }); -} - -} // namespace rcli::commands diff --git a/rcli/src/commands/cmd_voice.cpp b/rcli/src/commands/cmd_voice.cpp deleted file mode 100644 index 388e14bb66..0000000000 --- a/rcli/src/commands/cmd_voice.cpp +++ /dev/null @@ -1,173 +0,0 @@ -/** - * @file cmd_voice.cpp - * @brief `rcli voice --input a.wav` — one-shot voice turn (STT → LLM → TTS) - * via the commons voice agent, mirroring tests/test_voice_agent.cpp. - */ - -#include "commands/commands.h" - -#include -#include -#include -#include - -#include "rac/features/voice_agent/rac_voice_agent.h" -#include "rac/foundation/rac_proto_buffer.h" - -#if defined(RAC_HAVE_PROTOBUF) -#include "voice_agent_service.pb.h" -#endif - -#include "commands/model_setup.h" -#include "io/output.h" -#include "io/proto.h" -#include "io/wav_io.h" - -namespace rcli::commands { - -namespace { - -constexpr const char* kDefaultStt = "sherpa-onnx-whisper-tiny.en"; -constexpr const char* kDefaultLlm = "qwen3-0.6b"; -constexpr const char* kDefaultTts = "vits-piper-en_US-lessac-medium"; -constexpr int kTurnSampleRate = 16000; - -int run_voice(const GlobalOptions& options, const std::string& input, const std::string& stt_ref, - const std::string& llm_ref, const std::string& tts_ref, - const std::string& output) { - Bootstrapped env; - if (bootstrap(options, &env) != RAC_SUCCESS) { - return 1; - } - if (input.empty()) { - out::error_line("an audio file is required (positional or --input)"); - return 2; - } - - ResolvedModelPaths stt; - ResolvedModelPaths llm; - ResolvedModelPaths tts; - for (const auto& [ref, paths] : - {std::pair{stt_ref.empty() ? kDefaultStt : stt_ref.c_str(), &stt}, - std::pair{llm_ref.empty() ? kDefaultLlm : llm_ref.c_str(), &llm}, - std::pair{tts_ref.empty() ? kDefaultTts : tts_ref.c_str(), &tts}}) { - const int setup = ensure_model_ready(options, ref, paths); - if (setup != 0) { - return setup; - } - } - - wav::WavData audio; - std::string error; - if (!wav::read_wav(input, &audio, &error)) { - out::error_line(error); - return 1; - } - const std::vector pcm16 = wav::resample(audio.samples, audio.sample_rate, - kTurnSampleRate); - - rac_voice_agent_handle_t agent = nullptr; - rac_result_t rc = rac_voice_agent_create_standalone(&agent); - if (rc != RAC_SUCCESS || !agent) { - out::error_line("failed to create voice agent: " + out::describe_result(rc)); - return 1; - } - - rac_voice_agent_config_t config = RAC_VOICE_AGENT_CONFIG_DEFAULT; - config.stt_config.model_path = stt.primary_path.c_str(); - config.stt_config.model_id = stt.model_id.c_str(); - config.stt_config.model_name = stt.display_name.c_str(); - config.llm_config.model_path = llm.primary_path.c_str(); - config.llm_config.model_id = llm.model_id.c_str(); - config.llm_config.model_name = llm.display_name.c_str(); - config.tts_config.voice_path = tts.primary_path.c_str(); - config.tts_config.voice_id = tts.model_id.c_str(); - config.tts_config.voice_name = tts.display_name.c_str(); - - rc = rac_voice_agent_initialize(agent, &config); - if (rc != RAC_SUCCESS) { - out::error_line("voice agent init failed: " + out::describe_result(rc)); - rac_voice_agent_destroy(agent); - return 1; - } - - out::status_line("processing voice turn (stt=" + stt.model_id + ", llm=" + llm.model_id + - ", tts=" + tts.model_id + ")"); - rac_proto_buffer_t out_buffer; - rac_proto_buffer_init(&out_buffer); - rc = rac_voice_agent_process_voice_turn_proto( - agent, pcm16.data(), pcm16.size() * sizeof(int16_t), &out_buffer); - - int exit_code = 0; - runanywhere::v1::VoiceAgentResult result; - if (rc != RAC_SUCCESS || !proto::parse_proto_buffer(&out_buffer, &result, &error)) { - out::error_line("voice turn failed: " + - (rc != RAC_SUCCESS ? out::describe_result(rc) : error)); - exit_code = 1; - } else { - std::string reply_path; - // synthesized_audio_sample_rate_hz/channels/encoding were deleted from - // VoiceAgentResult outright: commons (voice_agent_proto_abi.cpp) already - // wraps the TTS float32 PCM into a complete WAV container via - // rac_audio_float32_to_wav before setting synthesized_audio, so this is - // a ready-to-write WAV file, not raw PCM -- no reinterpret/resample here. - if (!output.empty() && !result.synthesized_audio().empty()) { - std::ofstream file(output, std::ios::binary); - file.write(result.synthesized_audio().data(), - static_cast(result.synthesized_audio().size())); - if (file.good()) { - reply_path = output; - } else { - out::status_line("warning: cannot write " + output); - } - } - - if (options.json) { - out::JsonWriter json; - json.begin_object() - .field("transcription", result.transcription()) - .field("response", result.assistant_response()) - .field("reply_audio", reply_path) - .end_object(); - out::result_line(json.str()); - } else { - out::result_line("you " + result.transcription()); - out::result_line("agent " + result.assistant_response()); - if (!reply_path.empty()) { - out::result_line("audio " + reply_path); - } - } - } - - rac_voice_agent_destroy(agent); - return exit_code; -} - -} // namespace - -void register_voice(CLI::App& app, GlobalOptions& options) { - CLI::App* cmd = app.add_subcommand("voice", "Hold one spoken turn: listen, answer, speak"); - auto input = std::make_shared(); - auto stt = std::make_shared(); - auto llm = std::make_shared(); - auto tts = std::make_shared(); - auto output = std::make_shared(); - cmd->add_option("audio", *input, "16-bit PCM WAV file with the user's speech") - ->check(CLI::ExistingFile); - cmd->add_option("--input,-i", *input, "16-bit PCM WAV file with the user's speech") - ->check(CLI::ExistingFile); - cmd->add_option("--stt", *stt, - "Transcription model (default: " + std::string(kDefaultStt) + ")"); - cmd->add_option("--llm", *llm, "Answering model (default: " + std::string(kDefaultLlm) + ")"); - cmd->add_option("--tts", *tts, "Voice that speaks the reply (default: " + - std::string(kDefaultTts) + ")"); - cmd->add_option("--output,-o", *output, "WAV file for the spoken reply"); - cmd->callback([&options, input, stt, llm, tts, output]() { - const int exit_code = run_voice(options, *input, *stt, *llm, *tts, *output); - if (exit_code != 0) { - throw CLI::RuntimeError(exit_code); - } - }); -} - -} // namespace rcli::commands diff --git a/rcli/src/commands/commands.h b/rcli/src/commands/commands.h deleted file mode 100644 index 5a1549ae40..0000000000 --- a/rcli/src/commands/commands.h +++ /dev/null @@ -1,105 +0,0 @@ -/** - * @file commands.h - * @brief Subcommand registration — one function per command file. - * - * The command surface mirrors the SDK public API spec - * (thoughts/shared/plans/public_api_spec.md): a namespace per modality and the - * spec's verb under it (`rcli llm generate`, `rcli models download`, …), with - * option names in kebab-case (`--max-output-tokens`, `--top-p`). - * - * Each register_* attaches a CLI11 subcommand whose callback performs: - * parse → bootstrap() → ONE commons entry point → render. Inference and - * lifecycle logic stay in commons per the repo layering rule; command files - * only translate between argv and the rac_* C ABI. - * - * The terminal-friendly top-level names (`run`, `chat`, `list`, `pull`, `rm`, - * `show`) are aliases: one configure_* function wires the options and callback, - * and it is attached both under the namespace and at the top level, so there is - * never a second implementation to keep in sync. - * - * Callbacks throw CLI::RuntimeError(exit_code) on failure; main.cpp maps that - * to the process exit code (0 ok, 1 runtime error, 2 usage error). - */ - -#ifndef RCLI_COMMANDS_COMMANDS_H -#define RCLI_COMMANDS_COMMANDS_H - -#include - -#include "bootstrap.h" - -namespace rcli::commands { - -// --- Feature namespaces (spec verb grammar) -------------------------------- -void register_llm(CLI::App& app, GlobalOptions& options); -void register_vlm(CLI::App& app, GlobalOptions& options); -void register_tool(CLI::App& app, GlobalOptions& options); // llm tool-call (attaches to `llm`) -void register_stt(CLI::App& app, GlobalOptions& options); -void register_tts(CLI::App& app, GlobalOptions& options); -void register_vad(CLI::App& app, GlobalOptions& options); -void register_embed(CLI::App& app, GlobalOptions& options); -void register_rerank(CLI::App& app, GlobalOptions& options); -void register_image(CLI::App& app, GlobalOptions& options); -void register_diarize(CLI::App& app, GlobalOptions& options); -void register_segment(CLI::App& app, GlobalOptions& options); -void register_voice(CLI::App& app, GlobalOptions& options); -void register_rag(CLI::App& app, GlobalOptions& options); -void register_models(CLI::App& app, GlobalOptions& options); -void register_lora(CLI::App& app, GlobalOptions& options); - -// --- Top-level aliases of namespaced verbs --------------------------------- -void register_llm_aliases(CLI::App& app, GlobalOptions& options); // run, chat -void register_models_aliases(CLI::App& app, GlobalOptions& options); // list, pull, rm, show - -// --- Infrastructure -------------------------------------------------------- -void register_version(CLI::App& app, GlobalOptions& options); -void register_info(CLI::App& app, GlobalOptions& options); -void register_backends(CLI::App& app, GlobalOptions& options); -void register_serve(CLI::App& app, GlobalOptions& options); -void register_bench(CLI::App& app, GlobalOptions& options); -void register_auth(CLI::App& app, GlobalOptions& options); -void register_telemetry(CLI::App& app, GlobalOptions& options); - -/** - * Which llm/vlm entry point a configured command drives. - * Generate — one unary result, rendered once it completes. - * Stream — tokens printed as they arrive. - * Chat — Stream, falling back to the REPL when no prompt is given. - */ -enum class LlmVerb { Generate, Stream, Chat }; - -/** Where the model comes from: a `--model` option or the first positional. */ -enum class ModelArg { Option, Positional }; - -void configure_llm(CLI::App* cmd, GlobalOptions& options, LlmVerb verb, ModelArg model_arg); -void configure_vlm_generate(CLI::App* cmd, GlobalOptions& options); - -// Model-lifecycle verbs, shared by the `models` namespace and its aliases. -void configure_models_list(CLI::App* cmd, GlobalOptions& options); -void configure_models_get(CLI::App* cmd, GlobalOptions& options); -void configure_models_download(CLI::App* cmd, GlobalOptions& options); -void configure_models_delete(CLI::App* cmd, GlobalOptions& options); - -/** - * Shared pull flow (plan → start → progress → terminal state) for an - * already-registered model id. Used by cmd_pull and by commands that need an - * ensure-downloaded step (stt/tts/vad/voice). Returns 0 / 1 / 130 (cancel). - */ -int pull_model_flow(const GlobalOptions& options, const std::string& model_id); - -/** - * Attach the spec verb name to a namespace whose options live on the namespace - * itself (`rcli stt transcribe --input a.wav` and `rcli stt --input a.wav` are - * the same command). The verb is a grammar marker: CLI11 fallthrough hands its - * options to the parent, and the parent owns the single callback. - */ -inline CLI::App* add_verb_alias(CLI::App* ns, const std::string& verb, - const std::string& description) { - CLI::App* verb_app = ns->add_subcommand(verb, description); - verb_app->fallthrough(true); - return verb_app; -} - -} // namespace rcli::commands - -#endif // RCLI_COMMANDS_COMMANDS_H diff --git a/rcli/src/commands/engine_options.cpp b/rcli/src/commands/engine_options.cpp deleted file mode 100644 index 353e38408e..0000000000 --- a/rcli/src/commands/engine_options.cpp +++ /dev/null @@ -1,73 +0,0 @@ -#include "commands/engine_options.h" - -#include -#include - -namespace rcli::commands { - -bool parse_engine_hint(const std::string& engine, - runanywhere::v1::InferenceFramework* out_framework, - std::string* error) { - if (!out_framework) { - return false; - } - *out_framework = runanywhere::v1::INFERENCE_FRAMEWORK_UNSPECIFIED; - std::string normalized = engine; - std::transform(normalized.begin(), normalized.end(), normalized.begin(), - [](unsigned char c) { return static_cast(std::tolower(c)); }); - if (normalized.empty()) { - return true; - } - if (normalized == "mlx") { - *out_framework = runanywhere::v1::INFERENCE_FRAMEWORK_MLX; - return true; - } - // The Apple engine. Its identity is `neurt` (the runtime that implements it); - // the FRAMEWORK it maps onto is still COREML, because that is what the model - // files are and there is no NEURT value in InferenceFramework. `coreml` is - // accepted as an alias: it is the engine's former name and remains the honest - // name of the framework, so a user typing either means the same thing. - if (normalized == "neurt" || normalized == "coreml" || normalized == "core-ml" || - normalized == "ane") { - *out_framework = runanywhere::v1::INFERENCE_FRAMEWORK_COREML; - return true; - } - if (normalized == "llamacpp" || normalized == "llama.cpp" || normalized == "llama_cpp" || - normalized == "llama-cpp") { - *out_framework = runanywhere::v1::INFERENCE_FRAMEWORK_LLAMA_CPP; - return true; - } - if (normalized == "onnx") { - *out_framework = runanywhere::v1::INFERENCE_FRAMEWORK_ONNX; - return true; - } - if (normalized == "sherpa") { - *out_framework = runanywhere::v1::INFERENCE_FRAMEWORK_SHERPA; - return true; - } - if (error) { - *error = "unsupported engine '" + engine + "'"; - } - return false; -} - -bool resolve_engine_hint(const std::string& engine, EngineHintResolution* out_resolution, - std::string* error) { - if (!out_resolution) { - if (error) { - *error = "engine resolution output is required"; - } - return false; - } - *out_resolution = EngineHintResolution{}; - if (!parse_engine_hint(engine, &out_resolution->framework, error)) { - return false; - } - if (out_resolution->framework != runanywhere::v1::INFERENCE_FRAMEWORK_UNSPECIFIED) { - out_resolution->resolve_options.has_framework = true; - out_resolution->resolve_options.framework = out_resolution->framework; - } - return true; -} - -} // namespace rcli::commands diff --git a/rcli/src/commands/engine_options.h b/rcli/src/commands/engine_options.h deleted file mode 100644 index b129c8048e..0000000000 --- a/rcli/src/commands/engine_options.h +++ /dev/null @@ -1,31 +0,0 @@ -/** - * @file engine_options.h - * @brief Shared parsing for rcli engine/framework hints. - */ - -#ifndef RCLI_COMMANDS_ENGINE_OPTIONS_H -#define RCLI_COMMANDS_ENGINE_OPTIONS_H - -#include - -#include "model_types.pb.h" -#include "catalog/model_ref.h" - -namespace rcli::commands { - -struct EngineHintResolution { - runanywhere::v1::InferenceFramework framework = - runanywhere::v1::INFERENCE_FRAMEWORK_UNSPECIFIED; - model_ref::ResolveOptions resolve_options; -}; - -bool parse_engine_hint(const std::string& engine, - runanywhere::v1::InferenceFramework* out_framework, - std::string* error); - -bool resolve_engine_hint(const std::string& engine, EngineHintResolution* out_resolution, - std::string* error); - -} // namespace rcli::commands - -#endif // RCLI_COMMANDS_ENGINE_OPTIONS_H diff --git a/rcli/src/commands/model_labels.h b/rcli/src/commands/model_labels.h deleted file mode 100644 index 1138658772..0000000000 --- a/rcli/src/commands/model_labels.h +++ /dev/null @@ -1,84 +0,0 @@ -#pragma once - -#include "model_types.pb.h" - -namespace rcli::commands::model_labels { - -namespace v1 = runanywhere::v1; - -inline const char* category(v1::ModelCategory category) { - switch (category) { - case v1::MODEL_CATEGORY_LANGUAGE: - return "llm"; - case v1::MODEL_CATEGORY_MULTIMODAL: - case v1::MODEL_CATEGORY_VISION: - return "vlm"; - case v1::MODEL_CATEGORY_SPEECH_RECOGNITION: - return "stt"; - case v1::MODEL_CATEGORY_SPEECH_SYNTHESIS: - return "tts"; - case v1::MODEL_CATEGORY_VOICE_ACTIVITY_DETECTION: - return "vad"; - case v1::MODEL_CATEGORY_EMBEDDING: - return "embedding"; - case v1::MODEL_CATEGORY_SPEAKER_DIARIZATION: - return "diarize"; - case v1::MODEL_CATEGORY_SEMANTIC_SEGMENTATION: - return "segment"; - case v1::MODEL_CATEGORY_IMAGE_GENERATION: - return "diffusion"; - case v1::MODEL_CATEGORY_AUDIO: - return "audio"; - default: - return "?"; - } -} - -inline const char* backend(v1::InferenceFramework framework) { - switch (framework) { - case v1::INFERENCE_FRAMEWORK_ONNX: - return "ONNX Runtime"; - case v1::INFERENCE_FRAMEWORK_LLAMA_CPP: - return "llama.cpp"; - case v1::INFERENCE_FRAMEWORK_FOUNDATION_MODELS: - return "Apple Foundation"; - case v1::INFERENCE_FRAMEWORK_SYSTEM_TTS: - return "System TTS"; - case v1::INFERENCE_FRAMEWORK_FLUID_AUDIO: - return "Fluid Audio"; - case v1::INFERENCE_FRAMEWORK_COREML: - return "Core ML"; - case v1::INFERENCE_FRAMEWORK_MLX: - return "MLX"; - case v1::INFERENCE_FRAMEWORK_TFLITE: - return "TensorFlow Lite"; - case v1::INFERENCE_FRAMEWORK_EXECUTORCH: - return "ExecuTorch"; - case v1::INFERENCE_FRAMEWORK_MEDIAPIPE: - return "MediaPipe"; - case v1::INFERENCE_FRAMEWORK_MLC: - return "MLC"; - case v1::INFERENCE_FRAMEWORK_PICO_LLM: - return "Pico LLM"; - case v1::INFERENCE_FRAMEWORK_PIPER_TTS: - return "Piper TTS"; - case v1::INFERENCE_FRAMEWORK_SWIFT_TRANSFORMERS: - return "Swift Transformers"; - case v1::INFERENCE_FRAMEWORK_BUILT_IN: - return "Built-in"; - case v1::INFERENCE_FRAMEWORK_NONE: - return "None"; - case v1::INFERENCE_FRAMEWORK_UNKNOWN: - return "Unknown"; - case v1::INFERENCE_FRAMEWORK_SHERPA: - return "Sherpa-ONNX"; - case v1::INFERENCE_FRAMEWORK_QHEXRT: - return "QHexRT"; - case v1::INFERENCE_FRAMEWORK_UNSPECIFIED: - return "Unspecified"; - default: - return "?"; - } -} - -} // namespace rcli::commands::model_labels diff --git a/rcli/src/commands/model_setup.cpp b/rcli/src/commands/model_setup.cpp deleted file mode 100644 index 8ba1fb3b94..0000000000 --- a/rcli/src/commands/model_setup.cpp +++ /dev/null @@ -1,120 +0,0 @@ -#include "commands/model_setup.h" - -#include "model_types.pb.h" -#include "rac/core/rac_core.h" -#include "rac/core/rac_model_lifecycle.h" -#include "rac/infrastructure/model_management/rac_model_registry.h" - -#include "catalog/model_ref.h" -#include "commands/commands.h" -#include "io/output.h" -#include "io/proto.h" - -namespace rcli::commands { - -namespace { - -namespace v1 = runanywhere::v1; - -bool resolve_paths(const std::string &model_id, ResolvedModelPaths *out, - std::string *error) { - v1::ModelLoadRequest request; - request.set_model_id(model_id); - const std::string bytes = proto::serialize(request); - - rac_proto_buffer_t out_buffer; - rac_proto_buffer_init(&out_buffer); - v1::ModelLoadResult result; - if (rac_model_lifecycle_resolve_paths_proto( - rac_get_model_registry(), - reinterpret_cast(bytes.data()), bytes.size(), - &out_buffer) != RAC_SUCCESS || - !proto::parse_proto_buffer(&out_buffer, &result, error)) { - return false; - } - if (result.has_error()) { - if (error) { - *error = result.error().message(); - } - return false; - } - out->primary_path = result.resolved_path(); - return true; -} - -} // namespace - -bool refresh_registry(std::string *error) { - v1::ModelRegistryRefreshRequest request; - request.set_rescan_local(true); - request.set_include_downloaded_state(true); - const std::string bytes = proto::serialize(request); - - rac_proto_buffer_t out_buffer; - rac_proto_buffer_init(&out_buffer); - if (rac_model_registry_refresh_proto( - rac_get_model_registry(), - reinterpret_cast(bytes.data()), bytes.size(), - &out_buffer) != RAC_SUCCESS) { - rac_proto_buffer_free(&out_buffer); - if (error) { - *error = "registry refresh call failed"; - } - return false; - } - v1::ModelRegistryRefreshResult result; - return proto::parse_proto_buffer(&out_buffer, &result, error); -} - -int ensure_model_ready(const GlobalOptions &options, const std::string &ref, - ResolvedModelPaths *out) { - model_ref::Resolved resolved; - std::string error; - if (model_ref::resolve(ref, &resolved, &error) != RAC_SUCCESS) { - out::error_line(error); - return 1; - } - out->model_id = resolved.model_id; - - // Link any on-disk artifacts before deciding whether to pull. - if (!refresh_registry(&error)) { - out::status_line("warning: registry refresh failed: " + error); - } - - // Display name (best effort) + downloaded check. - bool downloaded = false; - { - rac_proto_buffer_t info_out; - rac_proto_buffer_init(&info_out); - v1::ModelInfo info; - const rac_result_t get_rc = rac_model_registry_get_proto_buffer( - rac_get_model_registry(), resolved.model_id.c_str(), &info_out); - // parse unconditionally: it interprets the {status,error_message} - // envelope and frees the buffer on every path (no leak on get failure). - const bool parsed = proto::parse_proto_buffer(&info_out, &info, nullptr); - if (get_rc == RAC_SUCCESS && parsed) { - out->display_name = info.name(); - // Commons reconciliation (refresh above) already validated folder - // completeness; registry_status is the single authority. - downloaded = info.registry_status() == v1::MODEL_REGISTRY_STATUS_DOWNLOADED; - } - } - - if (!downloaded) { - out::status_line("model " + resolved.model_id + - " not downloaded — pulling"); - const int pull_code = pull_model_flow(options, resolved.model_id); - if (pull_code != 0) { - return pull_code; - } - } - - if (!resolve_paths(resolved.model_id, out, &error)) { - out::error_line("cannot resolve model files for " + resolved.model_id + - ": " + error); - return 1; - } - return 0; -} - -} // namespace rcli::commands diff --git a/rcli/src/commands/model_setup.h b/rcli/src/commands/model_setup.h deleted file mode 100644 index 157b59d84e..0000000000 --- a/rcli/src/commands/model_setup.h +++ /dev/null @@ -1,42 +0,0 @@ -/** - * @file model_setup.h - * @brief Shared ensure-downloaded + resolve-paths step for speech commands. - * - * Resolves a model ref, pulls it when missing (same flow as `rcli pull`), and - * resolves the on-disk artifact paths through commons' - * rac_model_lifecycle_resolve_paths_proto — no engine load, no path guessing - * in the CLI. - */ - -#ifndef RCLI_COMMANDS_MODEL_SETUP_H -#define RCLI_COMMANDS_MODEL_SETUP_H - -#include - -#include "bootstrap.h" - -namespace rcli::commands { - -struct ResolvedModelPaths { - std::string model_id; - std::string display_name; - std::string primary_path; // resolved artifact (file or inner directory) -}; - -/** - * Resolve ref → ensure downloaded (auto-pull with progress) → resolve paths. - * Returns 0 on success, 1 on failure, 130 when the user cancelled the pull. - */ -int ensure_model_ready(const GlobalOptions& options, const std::string& ref, - ResolvedModelPaths* out); - -/** - * Refresh the registry (rescan_local + downloaded-state reconciliation) so - * on-disk artifacts — including ones placed by the test rig or playground — - * are linked to their entries. Used by list and by ensure_model_ready. - */ -bool refresh_registry(std::string* error); - -} // namespace rcli::commands - -#endif // RCLI_COMMANDS_MODEL_SETUP_H diff --git a/rcli/src/config/cli_paths.cpp b/rcli/src/config/cli_paths.cpp deleted file mode 100644 index 7634cebf31..0000000000 --- a/rcli/src/config/cli_paths.cpp +++ /dev/null @@ -1,86 +0,0 @@ -#include "config/cli_paths.h" - -#include -#include - -#if defined(_WIN32) -#ifndef NOMINMAX -#define NOMINMAX -#endif -#include - -#include -#endif - -#include "rac/desktop/rac_desktop.h" - -namespace rcli::paths { - -namespace { - -// Read an environment variable as UTF-8. On Windows std::getenv decodes the -// value through the process ANSI code page, corrupting Unicode paths (e.g. an -// international LOCALAPPDATA/USERPROFILE/RUNANYWHERE_HOME); read the wide value -// and convert with CP_UTF8 instead. Returns empty when unset or empty. -std::string getenv_utf8(const char* name) { -#if defined(_WIN32) - const std::wstring wname(name, name + std::strlen(name)); // name is ASCII - const wchar_t* wvalue = _wgetenv(wname.c_str()); - if (!wvalue || wvalue[0] == L'\0') { - return {}; - } - const int len = WideCharToMultiByte(CP_UTF8, 0, wvalue, -1, nullptr, 0, nullptr, nullptr); - if (len <= 1) { - return {}; - } - std::string out(static_cast(len - 1), '\0'); // len counts the NUL - WideCharToMultiByte(CP_UTF8, 0, wvalue, -1, out.data(), len, nullptr, nullptr); - return out; -#else - const char* value = std::getenv(name); - return (value && value[0] != '\0') ? std::string(value) : std::string(); -#endif -} - -} // namespace - -std::string normalize_dir(std::string dir) { - while (dir.size() > 1 && (dir.back() == '/' || dir.back() == '\\')) { - dir.pop_back(); - } - return dir; -} - -std::string resolve_home(const std::string& override_dir) { - if (!override_dir.empty()) { - return normalize_dir(override_dir); - } - if (std::string env = getenv_utf8("RUNANYWHERE_HOME"); !env.empty()) { - return normalize_dir(std::move(env)); - } - char buffer[1024] = {}; - if (rac_desktop_default_base_dir(buffer, sizeof(buffer)) == RAC_SUCCESS) { - return buffer; - } - return {}; -} - -std::string state_dir() { - if (std::string env = getenv_utf8("XDG_STATE_HOME"); !env.empty()) { - return normalize_dir(std::move(env)) + "/runanywhere"; - } - if (std::string home = getenv_utf8("HOME"); !home.empty()) { - return normalize_dir(std::move(home)) + "/.local/state/runanywhere"; - } -#if defined(_WIN32) - if (std::string local = getenv_utf8("LOCALAPPDATA"); !local.empty()) { - return normalize_dir(std::move(local)) + "/RunAnywhere/state"; - } - if (std::string profile = getenv_utf8("USERPROFILE"); !profile.empty()) { - return normalize_dir(std::move(profile)) + "/AppData/Local/RunAnywhere/state"; - } -#endif - return {}; -} - -} // namespace rcli::paths diff --git a/rcli/src/config/cli_paths.h b/rcli/src/config/cli_paths.h deleted file mode 100644 index 73e2854840..0000000000 --- a/rcli/src/config/cli_paths.h +++ /dev/null @@ -1,36 +0,0 @@ -/** - * @file cli_paths.h - * @brief rcli directory resolution. - * - * One knob controls where models live: the RunAnywhere HOME directory. - * resolution: --home flag → $RUNANYWHERE_HOME → ${XDG_DATA_HOME:-~/.local/share}/runanywhere - * Models are derived BY COMMONS from that home via rac_model_paths_* - * (home named "runanywhere" → /Models/{framework}/, the same layout - * the Linux test rig and Playground tooling use). - * - * Config (secure store) stays under ${XDG_CONFIG_HOME:-~/.config}/runanywhere; - * REPL history under ${XDG_STATE_HOME:-~/.local/state}/runanywhere. - */ - -#ifndef RCLI_CONFIG_CLI_PATHS_H -#define RCLI_CONFIG_CLI_PATHS_H - -#include - -namespace rcli::paths { - -/** - * Resolve the RunAnywhere home (storage base dir) — see file header for the - * precedence. Returns empty string only when $HOME is unresolvable. - */ -std::string resolve_home(const std::string& override_dir); - -/** ${XDG_STATE_HOME:-~/.local/state}/runanywhere (not created). */ -std::string state_dir(); - -/** Strip one trailing '/' (keeps root "/"). */ -std::string normalize_dir(std::string dir); - -} // namespace rcli::paths - -#endif // RCLI_CONFIG_CLI_PATHS_H diff --git a/rcli/src/device_info.cpp b/rcli/src/device_info.cpp deleted file mode 100644 index a827681d26..0000000000 --- a/rcli/src/device_info.cpp +++ /dev/null @@ -1,691 +0,0 @@ -#include "device_info.h" - -#include -#include -#include -#include - -#include "rac/core/rac_sdk_state.h" -#include "rac/foundation/rac_sha256.h" -#include "rac/infrastructure/device/rac_device_identity.h" -#include "rac/infrastructure/device/rac_device_manager.h" -#include "rac/infrastructure/http/rac_http_client.h" -#include "rac/infrastructure/http/rac_http_transport.h" -#include "rac/infrastructure/network/rac_auth_manager.h" -#include "rac/infrastructure/network/rac_endpoints.h" -#include "rac/infrastructure/network/rac_environment.h" - -#if defined(_WIN32) -#include -#elif defined(__APPLE__) -#include -#include -#include -#include -#include -#include -#include -#else -#include -#include - -#include -#include -#endif - -namespace rcli { - -namespace { - -struct DeviceInfoState { - std::string device_id; - std::string model; - std::string name; - std::string platform; - std::string os_version; - std::string form_factor; - std::string architecture; - std::string chip; - std::string gpu_family; - std::string battery_state; - std::string fingerprint; - double battery_level = -1.0; - int64_t total_memory = 0; - int64_t available_memory = 0; - int32_t core_count = 0; - int32_t performance_cores = 0; - int32_t efficiency_cores = 0; - bool registered = false; - std::string http_body; - std::string http_error; -}; - -DeviceInfoState &state() { - static DeviceInfoState s; - return s; -} - -std::string trim(const std::string &value) { - const char *ws = " \t\r\n"; - const std::size_t begin = value.find_first_not_of(ws); - if (begin == std::string::npos) { - return {}; - } - const std::size_t end = value.find_last_not_of(ws); - return value.substr(begin, end - begin + 1); -} - -#if !defined(_WIN32) && !defined(__APPLE__) - -std::string read_first_line(const std::string &path) { - std::ifstream file(path); - std::string line; - if (file.is_open() && std::getline(file, line)) { - return trim(line); - } - return {}; -} - -std::string os_release_pretty_name() { - std::ifstream file("/etc/os-release"); - std::string line; - while (file.is_open() && std::getline(file, line)) { - const std::string key = "PRETTY_NAME="; - if (line.compare(0, key.size(), key) == 0) { - std::string value = trim(line.substr(key.size())); - if (value.size() >= 2 && value.front() == '"' && value.back() == '"') { - value = value.substr(1, value.size() - 2); - } - return value; - } - } - return {}; -} - -std::string cpuinfo_model_name() { - std::ifstream file("/proc/cpuinfo"); - std::string line; - while (file.is_open() && std::getline(file, line)) { - if (line.compare(0, 10, "model name") == 0 || - line.compare(0, 8, "Hardware") == 0) { - const std::size_t colon = line.find(':'); - if (colon != std::string::npos) { - return trim(line.substr(colon + 1)); - } - } - } - return {}; -} - -int64_t meminfo_bytes(const char *key) { - std::ifstream file("/proc/meminfo"); - std::string line; - const std::string prefix = std::string(key) + ":"; - while (file.is_open() && std::getline(file, line)) { - if (line.compare(0, prefix.size(), prefix) == 0) { - const int64_t kib = std::strtoll(line.c_str() + prefix.size(), nullptr, 10); - return kib > 0 ? kib * 1024 : 0; - } - } - return 0; -} - -bool has_battery_dir(std::string *battery_path) { - namespace fs = std::filesystem; - std::error_code ec; - for (const auto &entry : fs::directory_iterator("/sys/class/power_supply", ec)) { - const std::string name = entry.path().filename().string(); - if (name.compare(0, 3, "BAT") == 0) { - if (battery_path) { - *battery_path = entry.path().string(); - } - return true; - } - } - return false; -} - -std::string linux_gpu_family() { - std::error_code ec; - if (std::filesystem::exists("/proc/driver/nvidia/version", ec)) { - return "nvidia"; - } - namespace fs = std::filesystem; - for (const auto &entry : fs::directory_iterator("/sys/class/drm", ec)) { - const std::string card = entry.path().filename().string(); - if (card.compare(0, 4, "card") != 0) { - continue; - } - std::ifstream uevent(entry.path() / "device/uevent"); - std::string line; - while (uevent.is_open() && std::getline(uevent, line)) { - if (line.compare(0, 7, "DRIVER=") != 0) { - continue; - } - const std::string driver = trim(line.substr(7)); - if (driver == "amdgpu" || driver == "radeon") { - return "amd"; - } - if (driver == "i915" || driver == "xe") { - return "intel"; - } - if (driver == "nvidia" || driver == "nouveau") { - return "nvidia"; - } - } - } - return "unknown"; -} - -void linux_core_topology(int32_t core_count, int32_t *perf, int32_t *eff) { - std::vector max_freqs; - max_freqs.reserve(static_cast(core_count)); - int64_t highest = 0; - for (int32_t cpu = 0; cpu < core_count; ++cpu) { - const std::string path = "/sys/devices/system/cpu/cpu" + std::to_string(cpu) + - "/cpufreq/cpuinfo_max_freq"; - const std::string value = read_first_line(path); - const int64_t freq = value.empty() ? 0 : std::strtoll(value.c_str(), nullptr, 10); - if (freq <= 0) { - *perf = core_count; - *eff = 0; - return; - } - max_freqs.push_back(freq); - highest = freq > highest ? freq : highest; - } - int32_t performance = 0; - for (const int64_t freq : max_freqs) { - if (freq == highest) { - ++performance; - } - } - if (performance == 0 || performance == core_count) { - *perf = core_count; - *eff = 0; - return; - } - *perf = performance; - *eff = core_count - performance; -} - -void collect_device_info(DeviceInfoState &info) { - info.platform = "linux"; - - info.model = read_first_line("/sys/devices/virtual/dmi/id/product_name"); - if (info.model.empty()) { - info.model = "Linux Desktop"; - } - - info.name = read_first_line("/etc/hostname"); - if (info.name.empty()) { - char hostname[256] = {}; - if (gethostname(hostname, sizeof(hostname) - 1) == 0 && hostname[0] != '\0') { - info.name = hostname; - } - } - if (info.name.empty()) { - info.name = info.model; - } - - info.os_version = os_release_pretty_name(); - if (info.os_version.empty()) { - info.os_version = "Linux"; - } - - info.chip = cpuinfo_model_name(); - if (info.chip.empty()) { - info.chip = "unknown"; - } - - info.total_memory = meminfo_bytes("MemTotal"); - info.available_memory = meminfo_bytes("MemAvailable"); - - const long online = sysconf(_SC_NPROCESSORS_ONLN); - info.core_count = online > 0 ? static_cast(online) : 1; - linux_core_topology(info.core_count, &info.performance_cores, - &info.efficiency_cores); - - struct utsname uts = {}; - info.architecture = (uname(&uts) == 0 && uts.machine[0] != '\0') - ? uts.machine - : "unknown"; - - std::string battery_path; - if (has_battery_dir(&battery_path)) { - info.form_factor = "laptop"; - const std::string capacity = read_first_line(battery_path + "/capacity"); - if (!capacity.empty()) { - const long percent = std::strtol(capacity.c_str(), nullptr, 10); - if (percent >= 0 && percent <= 100) { - info.battery_level = static_cast(percent) / 100.0; - } - } - const std::string status = read_first_line(battery_path + "/status"); - if (info.battery_level >= 0.0 && !status.empty()) { - if (status == "Full") { - info.battery_state = "full"; - } else if (status == "Charging") { - info.battery_state = "charging"; - } else { - info.battery_state = "unplugged"; - } - } - } else { - info.form_factor = "desktop"; - } - - info.gpu_family = linux_gpu_family(); -} - -#elif defined(__APPLE__) - -std::string sysctl_string(const char *key) { - std::size_t size = 0; - if (sysctlbyname(key, nullptr, &size, nullptr, 0) != 0 || size == 0) { - return {}; - } - std::string value(size, '\0'); - if (sysctlbyname(key, value.data(), &size, nullptr, 0) != 0) { - return {}; - } - value.resize(value.find('\0') != std::string::npos ? value.find('\0') - : value.size()); - return trim(value); -} - -int64_t sysctl_i64(const char *key) { - int64_t value = 0; - std::size_t size = sizeof(value); - if (sysctlbyname(key, &value, &size, nullptr, 0) != 0) { - return 0; - } - return value; -} - -int64_t macos_available_memory_bytes() { - mach_port_t host = mach_host_self(); - vm_statistics64_data_t stats = {}; - mach_msg_type_number_t count = HOST_VM_INFO64_COUNT; - if (host_statistics64(host, HOST_VM_INFO64, - reinterpret_cast(&stats), - &count) != KERN_SUCCESS) { - return 0; - } - const int64_t page_size = static_cast(sysctl_i64("hw.pagesize")); - if (page_size <= 0) { - return 0; - } - // Free + speculative pages ≈ "available" for telemetry (not wired/compressed). - const int64_t free_pages = - static_cast(stats.free_count) + - static_cast(stats.purgeable_count); - return free_pages * page_size; -} - -void macos_sample_battery(DeviceInfoState &info) { - // Desktop Macs (Studio/Mini/iMac) have no battery → leave level=-1 (null). - // Laptops expose IOPowerSources; sample real capacity rather than inventing 0. - CFTypeRef blob = IOPSCopyPowerSourcesInfo(); - if (blob == nullptr) { - return; - } - CFArrayRef list = IOPSCopyPowerSourcesList(blob); - if (list == nullptr) { - CFRelease(blob); - return; - } - - const CFIndex count = CFArrayGetCount(list); - for (CFIndex i = 0; i < count; ++i) { - CFTypeRef ps = CFArrayGetValueAtIndex(list, i); - CFDictionaryRef desc = IOPSGetPowerSourceDescription(blob, ps); - if (desc == nullptr) { - continue; - } - auto number_for = [&](CFStringRef key) -> double { - const auto *num = - static_cast(CFDictionaryGetValue(desc, key)); - if (num == nullptr) { - return -1.0; - } - double value = -1.0; - return CFNumberGetValue(num, kCFNumberDoubleType, &value) ? value : -1.0; - }; - - const double current = number_for(CFSTR(kIOPSCurrentCapacityKey)); - const double max_cap = number_for(CFSTR(kIOPSMaxCapacityKey)); - if (current < 0.0) { - continue; - } - // Capacity is usually already a percent (0–100); normalize to 0–1. - double level = current; - if (max_cap > 0.0 && max_cap != 100.0) { - level = (current / max_cap) * 100.0; - } - if (level > 1.0) { - level /= 100.0; - } - if (level < 0.0 || level > 1.0) { - continue; - } - - info.battery_level = level; - info.form_factor = "laptop"; - - const auto *state = static_cast( - CFDictionaryGetValue(desc, CFSTR(kIOPSPowerSourceStateKey))); - const auto *charging = static_cast( - CFDictionaryGetValue(desc, CFSTR(kIOPSIsChargingKey))); - if (charging != nullptr && CFBooleanGetValue(charging)) { - info.battery_state = level >= 0.999 ? "full" : "charging"; - } else if (state != nullptr && - CFStringCompare(state, CFSTR(kIOPSACPowerValue), 0) == - kCFCompareEqualTo) { - info.battery_state = level >= 0.999 ? "full" : "charging"; - } else { - info.battery_state = "unplugged"; - } - break; - } - - CFRelease(list); - CFRelease(blob); -} - -void collect_device_info(DeviceInfoState &info) { - info.platform = "macos"; - - info.model = sysctl_string("hw.model"); - if (info.model.empty()) { - info.model = "Mac"; - } - - char hostname[256] = {}; - info.name = (gethostname(hostname, sizeof(hostname) - 1) == 0 && - hostname[0] != '\0') - ? hostname - : info.model; - - const std::string product_version = sysctl_string("kern.osproductversion"); - info.os_version = - product_version.empty() ? "macOS" : "macOS " + product_version; - - info.chip = sysctl_string("machdep.cpu.brand_string"); - if (info.chip.empty()) { - info.chip = "unknown"; - } - - info.total_memory = sysctl_i64("hw.memsize"); - info.available_memory = macos_available_memory_bytes(); - - const int64_t ncpu = sysctl_i64("hw.ncpu"); - info.core_count = ncpu > 0 ? static_cast(ncpu) : 1; - const int64_t perf = sysctl_i64("hw.perflevel0.logicalcpu"); - const int64_t eff = sysctl_i64("hw.perflevel1.logicalcpu"); - if (perf > 0) { - info.performance_cores = static_cast(perf); - info.efficiency_cores = eff > 0 ? static_cast(eff) : 0; - } else { - info.performance_cores = info.core_count; - info.efficiency_cores = 0; - } - - struct utsname uts = {}; - info.architecture = (uname(&uts) == 0 && uts.machine[0] != '\0') - ? uts.machine - : "unknown"; - - info.form_factor = - info.model.find("Book") != std::string::npos ? "laptop" : "desktop"; -#if defined(__arm64__) || defined(__aarch64__) - info.gpu_family = "apple"; -#else - info.gpu_family = "unknown"; -#endif - - macos_sample_battery(info); -} - -#else // _WIN32 - -void collect_device_info(DeviceInfoState &info) { - info.platform = "windows"; - - char computer_name[MAX_COMPUTERNAME_LENGTH + 1] = {}; - DWORD name_len = sizeof(computer_name); - info.name = GetComputerNameA(computer_name, &name_len) ? computer_name - : "Windows PC"; - info.model = "Windows PC"; - info.os_version = "Windows"; - - char cpu_name[256] = {}; - DWORD cpu_name_size = sizeof(cpu_name); - if (RegGetValueA(HKEY_LOCAL_MACHINE, - "HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0", - "ProcessorNameString", RRF_RT_REG_SZ, nullptr, cpu_name, - &cpu_name_size) == ERROR_SUCCESS && - cpu_name[0] != '\0') { - info.chip = trim(cpu_name); - } else { - info.chip = "unknown"; - } - - MEMORYSTATUSEX mem = {}; - mem.dwLength = sizeof(mem); - if (GlobalMemoryStatusEx(&mem)) { - info.total_memory = static_cast(mem.ullTotalPhys); - info.available_memory = static_cast(mem.ullAvailPhys); - } - - SYSTEM_INFO sys = {}; - GetNativeSystemInfo(&sys); - info.core_count = sys.dwNumberOfProcessors > 0 - ? static_cast(sys.dwNumberOfProcessors) - : 1; - info.performance_cores = info.core_count; - info.efficiency_cores = 0; - switch (sys.wProcessorArchitecture) { - case PROCESSOR_ARCHITECTURE_AMD64: - info.architecture = "x86_64"; - break; - case PROCESSOR_ARCHITECTURE_ARM64: - info.architecture = "arm64"; - break; - case PROCESSOR_ARCHITECTURE_INTEL: - info.architecture = "x86"; - break; - default: - info.architecture = "unknown"; - break; - } - - SYSTEM_POWER_STATUS power = {}; - if (GetSystemPowerStatus(&power) && power.BatteryFlag != 128 && - power.BatteryFlag != 255) { - info.form_factor = "laptop"; - if (power.BatteryLifePercent <= 100) { - info.battery_level = - static_cast(power.BatteryLifePercent) / 100.0; - if (power.ACLineStatus == 1) { - info.battery_state = power.BatteryLifePercent == 100 ? "full" : "charging"; - } else { - info.battery_state = "unplugged"; - } - } - } else { - info.form_factor = "desktop"; - } - info.gpu_family = "unknown"; -} - -#endif - -void device_get_info(rac_device_registration_info_t *out_info, - void * /*user_data*/) { - if (out_info == nullptr) { - return; - } - auto &info = state(); - info.battery_level = -1.0; - info.battery_state.clear(); - collect_device_info(info); - info.fingerprint = runanywhere::sha256_hex( - info.model + "|" + info.chip + "|" + std::to_string(info.total_memory) + - "|" + std::to_string(info.core_count)); - - *out_info = {}; - out_info->device_id = info.device_id.c_str(); - out_info->device_model = info.model.c_str(); - out_info->device_name = info.name.c_str(); - out_info->platform = info.platform.c_str(); - out_info->os_version = info.os_version.c_str(); - out_info->form_factor = info.form_factor.c_str(); - out_info->architecture = info.architecture.c_str(); - out_info->chip_name = info.chip.c_str(); - out_info->total_memory = info.total_memory; - out_info->available_memory = info.available_memory; - out_info->has_neural_engine = RAC_FALSE; - out_info->neural_engine_cores = 0; - out_info->gpu_family = info.gpu_family.c_str(); - out_info->battery_level = info.battery_level; - out_info->battery_state = - info.battery_state.empty() ? nullptr : info.battery_state.c_str(); - out_info->is_low_power_mode = RAC_FALSE; - out_info->core_count = info.core_count; - out_info->performance_cores = info.performance_cores; - out_info->efficiency_cores = info.efficiency_cores; - out_info->device_fingerprint = info.fingerprint.c_str(); -} - -const char *device_get_id(void * /*user_data*/) { - return state().device_id.c_str(); -} - -rac_bool_t device_is_registered(void * /*user_data*/) { - return state().registered ? RAC_TRUE : RAC_FALSE; -} - -void device_set_registered(rac_bool_t registered, void * /*user_data*/) { - state().registered = registered == RAC_TRUE; -} - -// Same control-plane POST shape as rcli_telemetry_http_callback: commons base -// URL + relative endpoint over the registered desktop HTTP transport, bearer -// token attached when the auth manager holds one. -rac_result_t device_http_post(const char *endpoint, const char *json_body, - rac_bool_t requires_auth, - rac_device_http_response_t *out_response, - void * /*user_data*/) { - auto &info = state(); - info.http_body.clear(); - info.http_error.clear(); - - auto fail = [&](rac_result_t rc, const char *message) { - info.http_error = message; - if (out_response != nullptr) { - out_response->result = rc; - out_response->status_code = 0; - out_response->response_body = nullptr; - out_response->error_message = info.http_error.c_str(); - } - return rc; - }; - - if (endpoint == nullptr || json_body == nullptr) { - return fail(RAC_ERROR_INVALID_ARGUMENT, "invalid registration request"); - } - - const char *base_url = rac_state_get_base_url(); - if (base_url == nullptr || base_url[0] == '\0' || - rac_http_transport_is_registered() != RAC_TRUE) { - return fail(RAC_ERROR_NETWORK_ERROR, - "device registration transport unavailable"); - } - - char url[2048] = {}; - if (rac_build_url(base_url, endpoint, url, sizeof(url)) < 0) { - return fail(RAC_ERROR_NETWORK_ERROR, "device registration URL build failed"); - } - - std::vector headers; - const rac_http_header_kv_t *defaults = nullptr; - size_t default_count = 0; - if (rac_http_default_headers(&defaults, &default_count) == RAC_SUCCESS && - defaults != nullptr) { - headers.assign(defaults, defaults + default_count); - } - std::string auth_value; - if (requires_auth == RAC_TRUE) { - const char *token = rac_auth_get_access_token(); - if (token != nullptr && token[0] != '\0') { - auth_value = std::string("Bearer ") + token; - headers.push_back({"Authorization", auth_value.c_str()}); - } - } - - rac_http_client_t *client = nullptr; - if (rac_http_client_create(&client) != RAC_SUCCESS) { - return fail(RAC_ERROR_NETWORK_ERROR, - "device registration client create failed"); - } - - rac_http_request_t request = {}; - request.method = "POST"; - request.url = url; - request.headers = headers.empty() ? nullptr : headers.data(); - request.header_count = headers.size(); - request.body_bytes = reinterpret_cast(json_body); - request.body_len = std::char_traits::length(json_body); - request.timeout_ms = - rac_env_default_http_timeout_ms(rac_state_get_environment()); - request.follow_redirects = RAC_FALSE; - - rac_http_response_t response = {}; - const rac_result_t rc = rac_http_request_send(client, &request, &response); - rac_http_client_destroy(client); - - if (response.body_bytes != nullptr && response.body_len > 0) { - info.http_body.assign(reinterpret_cast(response.body_bytes), - response.body_len); - } - const int32_t status = response.status; - rac_http_response_free(&response); - - const bool ok = rc == RAC_SUCCESS && status >= 200 && status < 300; - if (!ok) { - info.http_error = "device registration POST failed (http " + - std::to_string(status) + ")"; - } - if (out_response != nullptr) { - out_response->result = ok ? RAC_SUCCESS : RAC_ERROR_NETWORK_ERROR; - out_response->status_code = status; - out_response->response_body = - info.http_body.empty() ? nullptr : info.http_body.c_str(); - out_response->error_message = ok ? nullptr : info.http_error.c_str(); - } - return ok ? RAC_SUCCESS : RAC_ERROR_NETWORK_ERROR; -} - -} // namespace - -rac_result_t install_device_callbacks() { - auto &info = state(); - char device_id[RAC_DEVICE_ID_BUFFER_MIN_SIZE] = {}; - if (rac_device_get_or_create_persistent_id(device_id, sizeof(device_id)) == - RAC_SUCCESS && - device_id[0] != '\0') { - info.device_id = device_id; - } - - rac_device_callbacks_t callbacks = {}; - callbacks.get_device_info = device_get_info; - callbacks.get_device_id = device_get_id; - callbacks.is_registered = device_is_registered; - callbacks.set_registered = device_set_registered; - callbacks.http_post = device_http_post; - callbacks.user_data = nullptr; - return rac_device_manager_set_callbacks(&callbacks); -} - -} // namespace rcli diff --git a/rcli/src/device_info.h b/rcli/src/device_info.h deleted file mode 100644 index d18262b86e..0000000000 --- a/rcli/src/device_info.h +++ /dev/null @@ -1,15 +0,0 @@ -#ifndef RCLI_DEVICE_INFO_H -#define RCLI_DEVICE_INFO_H - -#include "rac/core/rac_types.h" - -namespace rcli { - -// Installs the desktop device-registration callbacks on the commons device -// manager. Must run before SDK phase 2 so registration carries real hardware -// info instead of being skipped for missing callbacks. -rac_result_t install_device_callbacks(); - -} // namespace rcli - -#endif // RCLI_DEVICE_INFO_H diff --git a/rcli/src/io/image_io.cpp b/rcli/src/io/image_io.cpp deleted file mode 100644 index 0cca55ba99..0000000000 --- a/rcli/src/io/image_io.cpp +++ /dev/null @@ -1,257 +0,0 @@ -/** - * @file image_io.cpp - * @brief RGBA → PNG encoder (stored/uncompressed DEFLATE, zero dependencies). - * - * The commons CoreML diffusion engine returns raw RGBA pixel data. To honour - * `rcli image ... --out foo.png` we wrap those pixels in a valid PNG container. - * We avoid pulling libpng/zlib into the CLI by emitting the IDAT as a zlib - * stream built from *stored* DEFLATE blocks (BTYPE=00). The file is larger than - * a compressed PNG but is byte-for-byte valid per the PNG/zlib/DEFLATE specs. - */ - -#include "io/image_io.h" - -#include -#include -#include -#include -#include -#include - -namespace rcli::image { - -namespace { - -void put_u32_be(std::vector& out, uint32_t v) { - out.push_back(static_cast((v >> 24) & 0xFF)); - out.push_back(static_cast((v >> 16) & 0xFF)); - out.push_back(static_cast((v >> 8) & 0xFF)); - out.push_back(static_cast(v & 0xFF)); -} - -uint32_t crc32(const uint8_t* data, size_t len) { - static uint32_t table[256]; - static bool ready = false; - if (!ready) { - for (uint32_t n = 0; n < 256; ++n) { - uint32_t c = n; - for (int k = 0; k < 8; ++k) { - c = (c & 1U) ? (0xEDB88320U ^ (c >> 1)) : (c >> 1); - } - table[n] = c; - } - ready = true; - } - uint32_t crc = 0xFFFFFFFFU; - for (size_t i = 0; i < len; ++i) { - crc = table[(crc ^ data[i]) & 0xFF] ^ (crc >> 8); - } - return crc ^ 0xFFFFFFFFU; -} - -uint32_t adler32(const uint8_t* data, size_t len) { - constexpr uint32_t kMod = 65521; - uint32_t a = 1; - uint32_t b = 0; - size_t i = 0; - while (i < len) { - const size_t block = std::min(len - i, 5552); - for (size_t j = 0; j < block; ++j) { - a += data[i + j]; - b += a; - } - a %= kMod; - b %= kMod; - i += block; - } - return (b << 16) | a; -} - -void write_chunk(std::vector& png, const char type[4], const std::vector& data) { - put_u32_be(png, static_cast(data.size())); - const size_t crc_start = png.size(); - png.insert(png.end(), type, type + 4); - png.insert(png.end(), data.begin(), data.end()); - const uint32_t crc = crc32(png.data() + crc_start, png.size() - crc_start); - put_u32_be(png, crc); -} - -} // namespace - -bool write_png(const std::string& path, const uint8_t* rgba, int width, int height, - std::string* error) { - if (!rgba || width <= 0 || height <= 0) { - if (error) { - *error = "invalid image dimensions or data"; - } - return false; - } - - // Filtered scanlines: PNG requires a per-row filter byte (0 = None). - const size_t row_bytes = static_cast(width) * 4; - std::vector raw; - raw.reserve(static_cast(height) * (1 + row_bytes)); - for (int y = 0; y < height; ++y) { - raw.push_back(0); // filter type: none - const uint8_t* row = rgba + static_cast(y) * row_bytes; - raw.insert(raw.end(), row, row + row_bytes); - } - - // zlib stream: 2-byte header, stored DEFLATE blocks, 4-byte Adler-32. - std::vector zlib; - zlib.push_back(0x78); // CMF: deflate, 32K window - zlib.push_back(0x01); // FLG: no dict, fastest - size_t pos = 0; - do { - const size_t block = std::min(raw.size() - pos, 0xFFFF); - const bool final_block = (pos + block >= raw.size()); - zlib.push_back(final_block ? 1 : 0); // BFINAL + BTYPE=00 (stored) - const uint16_t len = static_cast(block); - const uint16_t nlen = static_cast(~len); - zlib.push_back(static_cast(len & 0xFF)); - zlib.push_back(static_cast((len >> 8) & 0xFF)); - zlib.push_back(static_cast(nlen & 0xFF)); - zlib.push_back(static_cast((nlen >> 8) & 0xFF)); - zlib.insert(zlib.end(), raw.begin() + static_cast(pos), - raw.begin() + static_cast(pos + block)); - pos += block; - } while (pos < raw.size()); - const uint32_t adler = adler32(raw.data(), raw.size()); - put_u32_be(zlib, adler); - - std::vector png; - const uint8_t signature[8] = {137, 80, 78, 71, 13, 10, 26, 10}; - png.insert(png.end(), signature, signature + 8); - - std::vector ihdr; - put_u32_be(ihdr, static_cast(width)); - put_u32_be(ihdr, static_cast(height)); - ihdr.push_back(8); // bit depth - ihdr.push_back(6); // color type: RGBA - ihdr.push_back(0); // compression: deflate - ihdr.push_back(0); // filter: adaptive - ihdr.push_back(0); // interlace: none - write_chunk(png, "IHDR", ihdr); - write_chunk(png, "IDAT", zlib); - write_chunk(png, "IEND", {}); - - FILE* file = std::fopen(path.c_str(), "wb"); - if (!file) { - if (error) { - *error = "cannot open " + path + " for writing"; - } - return false; - } - const size_t written = std::fwrite(png.data(), 1, png.size(), file); - std::fclose(file); - if (written != png.size()) { - if (error) { - *error = "short write to " + path; - } - return false; - } - return true; -} - -namespace { - -// Parse the next whitespace-delimited unsigned integer from a PPM header, -// skipping '#'-to-EOL comments. Returns false at EOF / on non-numeric input. -bool next_ppm_uint(const std::vector& bytes, size_t* pos, uint32_t* out) { - size_t i = *pos; - for (;;) { - while (i < bytes.size() && std::isspace(bytes[i])) { - ++i; - } - if (i < bytes.size() && bytes[i] == '#') { // comment to end of line - while (i < bytes.size() && bytes[i] != '\n') { - ++i; - } - continue; - } - break; - } - if (i >= bytes.size() || !std::isdigit(bytes[i])) { - return false; - } - uint64_t value = 0; - while (i < bytes.size() && std::isdigit(bytes[i])) { - value = value * 10 + static_cast(bytes[i] - '0'); - if (value > 0xFFFFFFFFULL) { - return false; - } - ++i; - } - *out = static_cast(value); - *pos = i; - return true; -} - -} // namespace - -bool read_ppm(const std::string& path, RgbImage* out, std::string* error) { - FILE* file = std::fopen(path.c_str(), "rb"); - if (!file) { - if (error) { - *error = "cannot open " + path; - } - return false; - } - std::vector bytes; - uint8_t chunk[4096]; - size_t n = 0; - while ((n = std::fread(chunk, 1, sizeof(chunk), file)) > 0) { - bytes.insert(bytes.end(), chunk, chunk + n); - } - std::fclose(file); - - if (bytes.size() < 2 || bytes[0] != 'P' || bytes[1] != '6') { - if (error) { - *error = path + " is not a binary PPM (P6); convert with e.g. `magick in.png out.ppm`"; - } - return false; - } - size_t pos = 2; - uint32_t width = 0; - uint32_t height = 0; - uint32_t maxval = 0; - if (!next_ppm_uint(bytes, &pos, &width) || !next_ppm_uint(bytes, &pos, &height) || - !next_ppm_uint(bytes, &pos, &maxval)) { - if (error) { - *error = "malformed PPM header in " + path; - } - return false; - } - if (width == 0 || height == 0 || maxval != 255) { - if (error) { - *error = "unsupported PPM (need non-empty dimensions and maxval 255)"; - } - return false; - } - // Bound dimensions (4096 mirrors the SDK's segmentation source cap) so the - // `width * height * 3` below cannot overflow size_t and a pathological header - // cannot mint a huge RgbImage backed by a tiny buffer. - constexpr uint32_t kMaxPpmDimension = 4096; - if (width > kMaxPpmDimension || height > kMaxPpmDimension) { - if (error) { - *error = "PPM dimensions exceed the supported maximum (4096x4096) in " + path; - } - return false; - } - // Exactly one whitespace byte separates the header from the pixel payload. - ++pos; - const size_t expected = static_cast(width) * height * 3; - if (pos + expected > bytes.size()) { - if (error) { - *error = "truncated PPM pixel data in " + path; - } - return false; - } - out->width = width; - out->height = height; - out->rgb.assign(bytes.begin() + static_cast(pos), - bytes.begin() + static_cast(pos + expected)); - return true; -} - -} // namespace rcli::image diff --git a/rcli/src/io/image_io.h b/rcli/src/io/image_io.h deleted file mode 100644 index 471b6b3bda..0000000000 --- a/rcli/src/io/image_io.h +++ /dev/null @@ -1,45 +0,0 @@ -/** - * @file image_io.h - * @brief Minimal RGBA → PNG encoder for `rcli image` output. - * - * CLI-owned file I/O, mirroring io/wav_io: commons diffusion engines return - * raw RGBA pixels (image_media_type "image/raw-rgba"); rcli renders them to a - * real PNG so `--out foo.png` is a valid image. Self-contained (no libpng / - * zlib dependency): emits a PNG whose IDAT is a zlib stream of *stored* - * (uncompressed) DEFLATE blocks, which every decoder accepts. - */ - -#ifndef RCLI_IO_IMAGE_IO_H -#define RCLI_IO_IMAGE_IO_H - -#include -#include -#include - -namespace rcli::image { - -/** - * Write 8-bit RGBA pixels (row-major, width*height*4 bytes) as a PNG file. - * Returns false and fills `error` (when non-null) on any failure. - */ -bool write_png(const std::string& path, const uint8_t* rgba, int width, int height, - std::string* error); - -/** Decoded 8-bit RGB image (row-major, width*height*3 bytes, tightly packed). */ -struct RgbImage { - std::vector rgb; - uint32_t width = 0; - uint32_t height = 0; -}; - -/** - * Read a binary PPM (P6, maxval 255) image into tightly-packed RGB8. PPM is the - * dependency-free counterpart to write_png: `magick in.png out.ppm` (or - * `ffmpeg -i in.png out.ppm`) produces one. Returns false + fills `error` on any - * malformed input. - */ -bool read_ppm(const std::string& path, RgbImage* out, std::string* error); - -} // namespace rcli::image - -#endif // RCLI_IO_IMAGE_IO_H diff --git a/rcli/src/io/output.cpp b/rcli/src/io/output.cpp deleted file mode 100644 index 4dfbbc55da..0000000000 --- a/rcli/src/io/output.cpp +++ /dev/null @@ -1,221 +0,0 @@ -#include "io/output.h" - -#include -#include - -#include "rac/core/rac_error.h" - -namespace rcli::out { - -std::string json_escape(const std::string& value) { - std::string escaped; - escaped.reserve(value.size() + 8); - for (const char c : value) { - switch (c) { - case '"': - escaped += "\\\""; - break; - case '\\': - escaped += "\\\\"; - break; - case '\n': - escaped += "\\n"; - break; - case '\r': - escaped += "\\r"; - break; - case '\t': - escaped += "\\t"; - break; - default: - if (static_cast(c) < 0x20) { - char buf[8]; - std::snprintf(buf, sizeof(buf), "\\u%04x", c); - escaped += buf; - } else { - escaped += c; - } - } - } - return escaped; -} - -void JsonWriter::comma() { - if (!first_in_scope_.empty()) { - if (!first_in_scope_.back()) { - buffer_ += ','; - } - first_in_scope_.back() = false; - } -} - -JsonWriter& JsonWriter::begin_object() { - comma(); - buffer_ += '{'; - first_in_scope_.push_back(true); - return *this; -} - -JsonWriter& JsonWriter::end_object() { - buffer_ += '}'; - if (!first_in_scope_.empty()) { - first_in_scope_.pop_back(); - } - return *this; -} - -JsonWriter& JsonWriter::begin_array(const std::string& key) { - comma(); - if (!key.empty()) { - buffer_ += '"' + json_escape(key) + "\":"; - } - buffer_ += '['; - first_in_scope_.push_back(true); - return *this; -} - -JsonWriter& JsonWriter::end_array() { - buffer_ += ']'; - if (!first_in_scope_.empty()) { - first_in_scope_.pop_back(); - } - return *this; -} - -JsonWriter& JsonWriter::begin_array_object() { - return begin_object(); -} - -JsonWriter& JsonWriter::field(const std::string& key, const std::string& value) { - comma(); - buffer_ += '"' + json_escape(key) + "\":\"" + json_escape(value) + '"'; - return *this; -} - -JsonWriter& JsonWriter::field(const std::string& key, const char* value) { - return field(key, std::string(value ? value : "")); -} - -JsonWriter& JsonWriter::field(const std::string& key, int64_t value) { - comma(); - char buf[32]; - std::snprintf(buf, sizeof(buf), "%" PRId64, value); - buffer_ += '"' + json_escape(key) + "\":" + buf; - return *this; -} - -JsonWriter& JsonWriter::field(const std::string& key, double value) { - comma(); - char buf[48]; - std::snprintf(buf, sizeof(buf), "%g", value); - buffer_ += '"' + json_escape(key) + "\":" + buf; - return *this; -} - -JsonWriter& JsonWriter::field(const std::string& key, bool value) { - comma(); - buffer_ += '"' + json_escape(key) + "\":" + (value ? "true" : "false"); - return *this; -} - -JsonWriter& JsonWriter::value(const std::string& value) { - comma(); - buffer_ += '"' + json_escape(value) + '"'; - return *this; -} - -JsonWriter& JsonWriter::value(const char* value) { - return this->value(std::string(value ? value : "")); -} - -JsonWriter& JsonWriter::value(int64_t value) { - comma(); - char buf[32]; - std::snprintf(buf, sizeof(buf), "%" PRId64, value); - buffer_ += buf; - return *this; -} - -JsonWriter& JsonWriter::value(double value) { - comma(); - char buf[48]; - std::snprintf(buf, sizeof(buf), "%g", value); - buffer_ += buf; - return *this; -} - -JsonWriter& JsonWriter::value(bool value) { - comma(); - buffer_ += value ? "true" : "false"; - return *this; -} - -void result_line(const std::string& line) { - std::fprintf(stdout, "%s\n", line.c_str()); - std::fflush(stdout); -} - -void status_line(const std::string& line) { - std::fprintf(stderr, "%s\n", line.c_str()); -} - -void error_line(const std::string& message) { - std::fprintf(stderr, "error: %s\n", message.c_str()); -} - -std::string describe_result(rac_result_t result) { - const char* message = rac_error_message(result); - if (message && message[0] != '\0') { - return std::string(message) + " (" + std::to_string(result) + ")"; - } - return "rac error " + std::to_string(result); -} - -std::string human_bytes(uint64_t bytes) { - constexpr const char* kUnits[] = {"B", "KB", "MB", "GB", "TB"}; - double value = static_cast(bytes); - size_t unit = 0; - while (value >= 1024.0 && unit < 4) { - value /= 1024.0; - ++unit; - } - char buf[32]; - if (unit == 0) { - std::snprintf(buf, sizeof(buf), "%" PRIu64 " B", bytes); - } else { - std::snprintf(buf, sizeof(buf), "%.1f %s", value, kUnits[unit]); - } - return buf; -} - -void table(const std::vector& header, - const std::vector>& rows) { - std::vector widths(header.size(), 0); - for (size_t c = 0; c < header.size(); ++c) { - widths[c] = header[c].size(); - } - for (const auto& row : rows) { - for (size_t c = 0; c < row.size() && c < widths.size(); ++c) { - widths[c] = std::max(widths[c], row[c].size()); - } - } - - const auto print_row = [&](const std::vector& row) { - std::string line; - for (size_t c = 0; c < widths.size(); ++c) { - const std::string& cell = (c < row.size()) ? row[c] : std::string(); - line += cell; - if (c + 1 < widths.size()) { - line.append(widths[c] - cell.size() + 4, ' '); - } - } - result_line(line); - }; - - print_row(header); - for (const auto& row : rows) { - print_row(row); - } -} - -} // namespace rcli::out diff --git a/rcli/src/io/output.h b/rcli/src/io/output.h deleted file mode 100644 index f8b8acf74b..0000000000 --- a/rcli/src/io/output.h +++ /dev/null @@ -1,78 +0,0 @@ -/** - * @file output.h - * @brief Output discipline helpers + minimal JSON emission. - * - * Contract (see rcli/AGENTS.md): - * - command RESULTS go to stdout; - * - logs, progress, banners, prompts go to stderr; - * - --json mode prints exactly ONE JSON document on stdout. - */ - -#ifndef RCLI_IO_OUTPUT_H -#define RCLI_IO_OUTPUT_H - -#include -#include -#include - -#include "rac/core/rac_types.h" - -namespace rcli::out { - -/** JSON-escape a UTF-8 string (quotes not included). */ -std::string json_escape(const std::string& value); - -/** - * Minimal JSON document builder — enough for rcli's flat objects/arrays - * without pulling a JSON dependency into the CLI. - */ -class JsonWriter { - public: - JsonWriter& begin_object(); - JsonWriter& end_object(); - JsonWriter& begin_array(const std::string& key = ""); - JsonWriter& end_array(); - JsonWriter& field(const std::string& key, const std::string& value); - JsonWriter& field(const std::string& key, const char* value); - JsonWriter& field(const std::string& key, int64_t value); - JsonWriter& field(const std::string& key, double value); - JsonWriter& field(const std::string& key, bool value); - JsonWriter& value(const std::string& value); - JsonWriter& value(const char* value); - JsonWriter& value(int64_t value); - JsonWriter& value(double value); - JsonWriter& value(bool value); - /** Object element inside an array. */ - JsonWriter& begin_array_object(); - - [[nodiscard]] const std::string& str() const { return buffer_; } - - private: - void comma(); - - std::string buffer_; - std::vector first_in_scope_; -}; - -/** Print a result line to stdout (newline appended). */ -void result_line(const std::string& line); - -/** Print a status/notice line to stderr (newline appended). */ -void status_line(const std::string& line); - -/** Print an error to stderr as "error: ". */ -void error_line(const std::string& message); - -/** Human message for a rac_result_t (falls back to the numeric code). */ -std::string describe_result(rac_result_t result); - -/** "1.4 GB" / "532 MB" style size formatting. */ -std::string human_bytes(uint64_t bytes); - -/** Simple left-aligned column table rendered to stdout. */ -void table(const std::vector& header, - const std::vector>& rows); - -} // namespace rcli::out - -#endif // RCLI_IO_OUTPUT_H diff --git a/rcli/src/io/proto.h b/rcli/src/io/proto.h deleted file mode 100644 index 425bff15c8..0000000000 --- a/rcli/src/io/proto.h +++ /dev/null @@ -1,61 +0,0 @@ -/** - * @file proto.h - * @brief rac_proto_buffer_t ⇄ protobuf message glue. - * - * Every lifecycle C ABI call returns serialized proto bytes in a - * rac_proto_buffer_t with the canonical {data, size, status} convention. - * parse_proto_buffer() checks the status envelope, parses, and ALWAYS frees - * the buffer. - */ - -#ifndef RCLI_IO_PROTO_H -#define RCLI_IO_PROTO_H - -#include - -#include "rac/foundation/rac_proto_buffer.h" - -#include "io/output.h" - -namespace rcli::proto { - -/** - * Parse an out-buffer into `message`, freeing the buffer in all paths. - * On failure returns false and fills `error` (when non-null) with the buffer's - * error envelope or a parse diagnostic. - */ -template -bool parse_proto_buffer(rac_proto_buffer_t* buffer, Message* message, std::string* error) { - bool ok = false; - if (buffer->status != RAC_SUCCESS) { - if (error) { - *error = (buffer->error_message && buffer->error_message[0] != '\0') - ? buffer->error_message - : rcli::out::describe_result(buffer->status); - } - } else if (!message->ParseFromArray(buffer->data, static_cast(buffer->size))) { - if (error) { - *error = "failed to parse " + std::string(Message::descriptor()->name()) + " bytes"; - } - } else { - ok = true; - } - rac_proto_buffer_free(buffer); - return ok; -} - -/** Serialize a request message into a byte string (proto3 never fails here). */ -template -std::string serialize(const Message& message) { - std::string bytes; - // protobuf 35.x marks SerializeToString [[nodiscard]]; consume the result - // (proto3 serialization does not fail in practice — guard anyway). - if (!message.SerializeToString(&bytes)) { - bytes.clear(); - } - return bytes; -} - -} // namespace rcli::proto - -#endif // RCLI_IO_PROTO_H diff --git a/rcli/src/io/wav_io.cpp b/rcli/src/io/wav_io.cpp deleted file mode 100644 index 68a92be660..0000000000 --- a/rcli/src/io/wav_io.cpp +++ /dev/null @@ -1,220 +0,0 @@ -#include "io/wav_io.h" - -#include -#include -#include - -#include "rac/core/rac_audio_utils.h" -#include "rac/core/rac_types.h" - -namespace rcli::wav { - -namespace { - -uint32_t read_u32(const uint8_t* p) { - return static_cast(p[0]) | (static_cast(p[1]) << 8) | - (static_cast(p[2]) << 16) | (static_cast(p[3]) << 24); -} - -uint16_t read_u16(const uint8_t* p) { - return static_cast(p[0] | (p[1] << 8)); -} - -bool write_bytes(const std::string& path, const void* data, size_t size, std::string* error) { - FILE* f = std::fopen(path.c_str(), "wb"); - if (!f) { - if (error) { - *error = "cannot create " + path; - } - return false; - } - const bool ok = size == 0 || std::fwrite(data, 1, size, f) == size; - std::fclose(f); - if (!ok && error) { - *error = "failed writing " + path; - } - return ok; -} - -} // namespace - -bool read_wav(const std::string& path, WavData* out, std::string* error) { - FILE* f = std::fopen(path.c_str(), "rb"); - if (!f) { - if (error) { - *error = "cannot open " + path; - } - return false; - } - - bool ok = false; - uint16_t channels = 0; - uint16_t bits = 0; - uint32_t rate = 0; - std::vector data; - - do { - uint8_t riff[12]; - if (std::fread(riff, 1, sizeof(riff), f) != sizeof(riff) || - std::memcmp(riff, "RIFF", 4) != 0 || std::memcmp(riff + 8, "WAVE", 4) != 0) { - if (error) { - *error = path + " is not a RIFF/WAVE file"; - } - break; - } - - bool have_fmt = false; - bool have_data = false; - while (!have_fmt || !have_data) { - uint8_t header[8]; - if (std::fread(header, 1, sizeof(header), f) != sizeof(header)) { - break; - } - const uint32_t chunk_size = read_u32(header + 4); - if (std::memcmp(header, "fmt ", 4) == 0) { - std::vector fmt(chunk_size); - if (std::fread(fmt.data(), 1, chunk_size, f) != chunk_size || chunk_size < 16) { - break; - } - const uint16_t format = read_u16(fmt.data()); - channels = read_u16(fmt.data() + 2); - rate = read_u32(fmt.data() + 4); - bits = read_u16(fmt.data() + 14); - if (format != 1 /*PCM*/ || bits != 16 || channels == 0) { - if (error) { - *error = "only 16-bit PCM WAV is supported"; - } - have_fmt = false; - break; - } - have_fmt = true; - } else if (std::memcmp(header, "data", 4) == 0) { - data.resize(chunk_size); - if (std::fread(data.data(), 1, chunk_size, f) != chunk_size) { - break; - } - have_data = true; - } else { - // Skip unknown chunk (padded to even size). - std::fseek(f, static_cast(chunk_size + (chunk_size & 1)), SEEK_CUR); - } - } - if (!have_fmt || !have_data) { - if (error && error->empty()) { - *error = path + " is missing fmt/data chunks"; - } - break; - } - - const size_t frame_count = data.size() / (2 * channels); - out->samples.resize(frame_count); - const auto* pcm = reinterpret_cast(data.data()); - if (channels == 1) { - std::memcpy(out->samples.data(), pcm, frame_count * sizeof(int16_t)); - } else { - for (size_t i = 0; i < frame_count; ++i) { - int32_t acc = 0; - for (uint16_t c = 0; c < channels; ++c) { - acc += pcm[i * channels + c]; - } - out->samples[i] = static_cast(acc / channels); - } - } - out->sample_rate = static_cast(rate); - ok = true; - } while (false); - - std::fclose(f); - return ok; -} - -bool write_wav(const std::string& path, const int16_t* samples, size_t count, int sample_rate, - std::string* error) { - if (samples == nullptr || count == 0 || sample_rate <= 0) { - if (error) { - *error = "invalid PCM16 input for " + path; - } - return false; - } - - void* wav_data = nullptr; - size_t wav_size = 0; - const rac_result_t rc = - rac_audio_int16_to_wav(samples, count * sizeof(int16_t), sample_rate, &wav_data, &wav_size); - if (rc != RAC_SUCCESS || wav_data == nullptr || wav_size == 0) { - if (error) { - *error = "rac_audio_int16_to_wav failed for " + path; - } - return false; - } - - const bool ok = write_bytes(path, wav_data, wav_size, error); - rac_free(wav_data); - return ok; -} - -bool write_wav_f32(const std::string& path, const float* samples, size_t count, int sample_rate, - std::string* error) { - if (samples == nullptr || count == 0 || sample_rate <= 0) { - if (error) { - *error = "invalid float32 PCM input for " + path; - } - return false; - } - - void* wav_data = nullptr; - size_t wav_size = 0; - const rac_result_t rc = - rac_audio_float32_to_wav(samples, count * sizeof(float), sample_rate, &wav_data, &wav_size); - if (rc != RAC_SUCCESS || wav_data == nullptr || wav_size == 0) { - if (error) { - *error = "rac_audio_float32_to_wav failed for " + path; - } - return false; - } - - const bool ok = write_bytes(path, wav_data, wav_size, error); - rac_free(wav_data); - return ok; -} - -std::vector resample(const std::vector& samples, int from_rate, int to_rate) { - if (samples.empty() || from_rate == to_rate) { - return samples; - } - - std::vector in_f(samples.size()); - if (rac_audio_pcm16_to_float32(samples.data(), samples.size(), in_f.data()) != RAC_SUCCESS) { - return {}; - } - - float* out_f = nullptr; - size_t out_frames = 0; - const rac_result_t rc = - rac_audio_resample_f32(in_f.data(), in_f.size(), from_rate, to_rate, &out_f, &out_frames); - if (rc != RAC_SUCCESS) { - return {}; - } - if (out_frames == 0) { - rac_free(out_f); - return {}; - } - - std::vector out(out_frames); - const rac_result_t qrc = rac_audio_float32_to_pcm16(out_f, out_frames, out.data()); - rac_free(out_f); - if (qrc != RAC_SUCCESS) { - return {}; - } - return out; -} - -std::vector to_float(const std::vector& samples) { - std::vector out(samples.size()); - if (!samples.empty()) { - (void)rac_audio_pcm16_to_float32(samples.data(), samples.size(), out.data()); - } - return out; -} - -} // namespace rcli::wav diff --git a/rcli/src/io/wav_io.h b/rcli/src/io/wav_io.h deleted file mode 100644 index 95d83128f9..0000000000 --- a/rcli/src/io/wav_io.h +++ /dev/null @@ -1,43 +0,0 @@ -/** - * @file wav_io.h - * @brief CLI-owned WAV file I/O and RIFF format parsing. - * - * PCM conversion, linear resampling, and RIFF/WAV container synthesis live in - * commons (`rac_audio_*`). This header only exposes path-based read/write and - * thin wrappers that forward to those primitives. - */ - -#ifndef RCLI_IO_WAV_IO_H -#define RCLI_IO_WAV_IO_H - -#include -#include -#include - -namespace rcli::wav { - -struct WavData { - std::vector samples; // mono (channels collapsed by averaging) - int sample_rate = 0; -}; - -/** Read a RIFF/WAVE file (16-bit PCM only). Returns false + error message. */ -bool read_wav(const std::string& path, WavData* out, std::string* error); - -/** Write mono 16-bit PCM samples as a WAV file via rac_audio_int16_to_wav. */ -bool write_wav(const std::string& path, const int16_t* samples, size_t count, int sample_rate, - std::string* error); - -/** Write mono float32 PCM samples as a WAV file via rac_audio_float32_to_wav. */ -bool write_wav_f32(const std::string& path, const float* samples, size_t count, int sample_rate, - std::string* error); - -/** Linear resample via rac_audio_resample_f32 (PCM16 ↔ float around the call). */ -std::vector resample(const std::vector& samples, int from_rate, int to_rate); - -/** int16 → float [-1, 1] via rac_audio_pcm16_to_float32. */ -std::vector to_float(const std::vector& samples); - -} // namespace rcli::wav - -#endif // RCLI_IO_WAV_IO_H diff --git a/rcli/src/main.cpp b/rcli/src/main.cpp deleted file mode 100644 index c92091fc2f..0000000000 --- a/rcli/src/main.cpp +++ /dev/null @@ -1,15 +0,0 @@ -/** - * @file main.cpp - * @brief rcli — RunAnywhere desktop CLI entry point. - * - * Thin dispatch layer: global flags + CLI11 subcommands. All real work - * happens in commons behind the rac_* C ABI (see AGENTS.md layering rule). - * - * Exit codes: 0 success, 1 runtime/SDK error, 2 usage error. - */ - -#include "app.h" - -int main(int argc, char** argv) { - return rcli::run(argc, argv); -} diff --git a/rcli/src/net/control_plane.cpp b/rcli/src/net/control_plane.cpp deleted file mode 100644 index 4d61990b80..0000000000 --- a/rcli/src/net/control_plane.cpp +++ /dev/null @@ -1,248 +0,0 @@ -/** - * @file control_plane.cpp - * @brief Control-plane network wiring for rcli — see control_plane.h. - * - * The CLI drives the canonical commons entry points and adds only what is - * genuinely CLI-shaped: a buffered POST helper the telemetry commands reuse and - * the login flow's user-facing error text. Device callbacks are installed by - * bootstrap.cpp through the ordinary rac_device_manager surface; request - * building and response parsing stay in commons, per the repo layering rule. - */ - -#include "net/control_plane.h" - -#include -#include - -#include "rac/core/rac_sdk_state.h" -#include "rac/desktop/rac_desktop.h" -#include "rac/infrastructure/http/rac_http_client.h" -#include "rac/infrastructure/network/rac_auth_manager.h" -#include "rac/infrastructure/network/rac_endpoints.h" -#include "rac/infrastructure/network/rac_environment.h" -#include "rac/lifecycle/rac_sdk_init.h" - -#include "sdk_init.pb.h" - -#include "io/output.h" -#include "io/proto.h" - -namespace rcli::net { - -namespace { - -namespace v1 = runanywhere::v1; - -constexpr size_t kErrorBodyPreview = 500; - -std::string single_line_preview(const std::string& body) { - std::string preview = body.substr(0, kErrorBodyPreview); - for (char& ch : preview) { - if (ch == '\n' || ch == '\r' || ch == '\t') { - ch = ' '; - } - } - if (body.size() > kErrorBodyPreview) { - preview += "…"; - } - return preview; -} - -} // namespace - -const char* platform_name() { - return rac_desktop_platform_name(); -} - -const std::string& device_model() { - static const std::string model = rac_desktop_device_model(); - return model; -} - -const std::string& os_version_string() { - static const std::string version = rac_desktop_os_version(); - return version; -} - -std::string HttpResult::describe() const { - if (transport != RAC_SUCCESS) { - std::string message = "network error: " + out::describe_result(transport); - if (!body.empty()) { - message += " (" + single_line_preview(body) + ")"; - } - return message; - } - std::string message = "HTTP " + std::to_string(status); - if (!body.empty()) { - message += ": " + single_line_preview(body); - } - return message; -} - -HttpResult control_plane_post(const std::string& endpoint, const std::string& json_body, - bool bearer_auth) { - HttpResult result; - - const char* base_url = rac_state_get_base_url(); - if (base_url == nullptr || base_url[0] == '\0') { - result.transport = RAC_ERROR_INVALID_CONFIGURATION; - result.body = "control-plane base URL is not configured"; - return result; - } - - char url[2048] = {}; - if (rac_build_url(base_url, endpoint.c_str(), url, sizeof(url)) < 0) { - result.transport = RAC_ERROR_INVALID_CONFIGURATION; - result.body = "failed to build control-plane URL"; - return result; - } - - // Canonical control-plane header set — mirrors commons' phase-2 pattern: - // defaults (Content-Type/Accept/X-SDK-*) + X-Platform + apikey [+ Bearer]. - const rac_http_header_kv_t* defaults = nullptr; - size_t default_count = 0; - std::vector headers; - if (rac_http_default_headers(&defaults, &default_count) == RAC_SUCCESS && - defaults != nullptr) { - headers.assign(defaults, defaults + default_count); - } - headers.push_back({"X-Platform", platform_name()}); - const char* api_key = rac_state_get_api_key(); - if (api_key != nullptr && api_key[0] != '\0') { - headers.push_back({"apikey", api_key}); - } - std::string bearer; - if (bearer_auth) { - const char* token = rac_auth_get_access_token(); - if (token != nullptr && token[0] != '\0') { - bearer = std::string("Bearer ") + token; - headers.push_back({"Authorization", bearer.c_str()}); - } - } - - rac_http_client_t* client = nullptr; - rac_result_t rc = rac_http_client_create(&client); - if (rc != RAC_SUCCESS) { - result.transport = rc; - return result; - } - - rac_http_request_t request = {}; - request.method = "POST"; - request.url = url; - request.headers = headers.data(); - request.header_count = headers.size(); - request.body_bytes = reinterpret_cast(json_body.data()); - request.body_len = json_body.size(); - request.timeout_ms = rac_env_default_http_timeout_ms(rac_state_get_environment()); - // Credential-bearing control-plane requests never replay across redirects. - request.follow_redirects = RAC_FALSE; - - rac_http_response_t response = {}; - rc = rac_http_request_send(client, &request, &response); - rac_http_client_destroy(client); - - result.transport = rc; - if (rc == RAC_SUCCESS) { - result.status = response.status; - if (response.body_bytes != nullptr && response.body_len > 0) { - result.body.assign(reinterpret_cast(response.body_bytes), - response.body_len); - } - } - rac_http_response_free(&response); - return result; -} - -rac_result_t login(LoginSummary* out, std::string* error) { - const rac_environment_t env = rac_state_get_environment(); - if (!rac_env_auth_expected(env, rac_state_get_api_key())) { - if (error != nullptr) { - *error = - "keyless development has no JWT login; use --environment production " - "with --base-url and --api-key"; - } - return RAC_ERROR_INVALID_CONFIGURATION; - } - - // Step 1: API key → JWT. Idempotent within a process; a valid token - // short-circuits (phase 2 below then takes its authenticated fast path). - if (!rac_auth_is_authenticated() || rac_auth_needs_refresh()) { - const rac_sdk_config_t* config = rac_sdk_get_config(); - if (config == nullptr) { - if (error != nullptr) { - *error = "SDK configuration unavailable (bootstrap did not run?)"; - } - return RAC_ERROR_NOT_INITIALIZED; - } - char* request_json = rac_auth_build_authenticate_request(config); - if (request_json == nullptr) { - if (error != nullptr) { - *error = "failed to build authenticate request"; - } - return RAC_ERROR_INVALID_CONFIGURATION; - } - const HttpResult response = - control_plane_post(RAC_ENDPOINT_AUTHENTICATE, request_json, false); - std::free(request_json); - if (!response.ok()) { - if (error != nullptr) { - *error = "authentication failed: " + response.describe(); - } - return response.transport != RAC_SUCCESS ? response.transport : RAC_ERROR_HTTP_ERROR; - } - const int auth_rc = rac_auth_handle_authenticate_response(response.body.c_str()); - if (auth_rc != RAC_SUCCESS && auth_rc != RAC_ERROR_SECURE_STORAGE_FAILED) { - if (error != nullptr) { - *error = "authentication response rejected: " + single_line_preview(response.body); - } - return RAC_ERROR_INVALID_RESPONSE; - } - } - - // Step 2: canonical phase-2 orchestration — device registration + - // model-assignment fetch (telemetry flush / local rescans stay off; the - // CLI runs those flows through their own commands). - v1::SdkInitPhase2Request request; - const std::string request_bytes = proto::serialize(request); - rac_proto_buffer_t out_buffer; - rac_proto_buffer_init(&out_buffer); - const rac_result_t phase2_rc = rac_sdk_init_phase2_proto( - request_bytes.empty() ? nullptr - : reinterpret_cast(request_bytes.data()), - request_bytes.size(), &out_buffer); - v1::SdkInitResult result; - std::string parse_error; - if (!proto::parse_proto_buffer(&out_buffer, &result, &parse_error) || - phase2_rc != RAC_SUCCESS) { - if (error != nullptr) { - *error = "services init failed: " + - (parse_error.empty() ? out::describe_result(phase2_rc) : parse_error); - } - return phase2_rc != RAC_SUCCESS ? phase2_rc : RAC_ERROR_INVALID_RESPONSE; - } - if (!result.has_error() == false) { - if (error != nullptr) { - *error = "services init failed: " + result.error().message(); - } - return RAC_ERROR_INVALID_STATE; - } - - if (out != nullptr) { - const char* organization_id = rac_auth_get_organization_id(); - const char* user_id = rac_auth_get_user_id(); - const char* backend_device_id = rac_auth_get_device_id(); - const char* persistent_device_id = rac_state_get_device_id(); - out->organization_id = organization_id != nullptr ? organization_id : ""; - out->user_id = user_id != nullptr ? user_id : ""; - out->backend_device_id = backend_device_id != nullptr ? backend_device_id : ""; - out->persistent_device_id = persistent_device_id != nullptr ? persistent_device_id : ""; - out->token_expires_at = rac_auth_get_token_expires_at(); - out->has_completed_http_setup = result.has_completed_http_setup(); - out->assignment_count = result.linked_models_count(); - out->warning = result.warning(); - } - return RAC_SUCCESS; -} - -} // namespace rcli::net diff --git a/rcli/src/net/control_plane.h b/rcli/src/net/control_plane.h deleted file mode 100644 index 8397d8827e..0000000000 --- a/rcli/src/net/control_plane.h +++ /dev/null @@ -1,88 +0,0 @@ -/** - * @file control_plane.h - * @brief Control-plane network wiring for rcli (auth, device, telemetry HTTP). - * - * rcli is the 6th consumer of runanywhere-commons and plays the same role the - * Swift/Kotlin/Flutter/RN/Web bridges play for the control plane. This module - * drives the canonical commons entry points (rac_auth_* + - * rac_sdk_init_phase2_proto); bootstrap.cpp installs platform callbacks through - * the ordinary device manager. All handshake sequencing, JSON request building, - * and response parsing stay in commons. - * - * Requires bootstrap() (rac_init + curl transport + rac_state) to have run. - */ - -#ifndef RCLI_NET_CONTROL_PLANE_H -#define RCLI_NET_CONTROL_PLANE_H - -#include -#include - -#include "rac/core/rac_types.h" - -namespace rcli::net { - -/** "macos" / "linux" / "windows" — the X-Platform header + auth payload value. */ -const char* platform_name(); - -/** Best-effort local hardware model (e.g. "Mac16,8"); empty when unknown. */ -const std::string& device_model(); - -/** Best-effort OS version string (kernel release); empty when unknown. */ -const std::string& os_version_string(); - -/** One buffered control-plane HTTP exchange. */ -struct HttpResult { - rac_result_t transport = RAC_SUCCESS; ///< send-level result (network/TLS/timeout) - int32_t status = 0; ///< HTTP status (0 when transport failed) - std::string body; ///< response body (server error JSON on 4xx/5xx) - - [[nodiscard]] bool ok() const { - return transport == RAC_SUCCESS && status >= 200 && status < 300; - } - /** "HTTP 401: {...}" / "network error" — for user-facing error lines. */ - [[nodiscard]] std::string describe() const; -}; - -/** - * POST `endpoint` (path, e.g. "/api/v2/sdk/telemetry/llm") against the - * configured base URL with the canonical control-plane headers - * (commons defaults + X-Platform + apikey). When `bearer_auth` is true the - * current JWT access token is attached as `Authorization: Bearer `. - */ -HttpResult control_plane_post(const std::string& endpoint, const std::string& json_body, - bool bearer_auth); - -/** Result of the real auth handshake (authenticate → device → assignments). */ -struct LoginSummary { - std::string organization_id; - std::string user_id; // may be empty (org-scoped keys) - std::string backend_device_id; // control-plane device row id (auth response) - std::string persistent_device_id; // SDK persistent UUID (device fingerprint) - int64_t token_expires_at = 0; // unix seconds - // SdkInitResult.device_registered was deleted outright (idl API - // realignment); has_completed_http_setup is the closest surviving - // cross-phase signal ("HTTP/auth setup completed", not literally "a - // device row exists"), so it is what this now reports. - bool has_completed_http_setup = false; - uint32_t assignment_count = 0; - std::string warning; // non-fatal phase-2 notes -}; - -/** - * Run the real control-plane handshake against the configured backend: - * 1. POST /api/v1/auth/sdk/authenticate (API key → JWT + refresh token), - * 2. rac_sdk_init_phase2_proto (device registration + model-assignment - * fetch through the commons lifecycle orchestrator). - * - * Requires production (or deprecated staging alias) with an API key. - * Keyless development has no JWT path — use telemetry emit/blast instead. - * Idempotent within a process — a valid token short-circuits step 1. - * On failure returns a non-SUCCESS code and fills `error` with the - * server-surfaced message (HTTP status + response body). - */ -rac_result_t login(LoginSummary* out, std::string* error); - -} // namespace rcli::net - -#endif // RCLI_NET_CONTROL_PLANE_H diff --git a/rcli/src/progress/progress_bar.cpp b/rcli/src/progress/progress_bar.cpp deleted file mode 100644 index 4bd490ecf5..0000000000 --- a/rcli/src/progress/progress_bar.cpp +++ /dev/null @@ -1,186 +0,0 @@ -#include "progress/progress_bar.h" - -#include -#include - -#include "rac/infrastructure/download/rac_download_orchestrator.h" - -#include "io/output.h" -#include "util/term.h" - -namespace rcli::progress { - -namespace { - -const char* stage_label(runanywhere::v1::DownloadState state) { - switch (state) { - case runanywhere::v1::DOWNLOAD_STATE_DOWNLOADING: - return "pulling"; - case runanywhere::v1::DOWNLOAD_STATE_EXTRACTING: - return "extracting"; - case runanywhere::v1::DOWNLOAD_STATE_VALIDATING: - return "verifying"; - case runanywhere::v1::DOWNLOAD_STATE_COMPLETED: - return "done"; - default: - return "preparing"; - } -} - -std::string speed_text(float bps) { - if (bps <= 0) { - return ""; - } - return out::human_bytes(static_cast(bps)) + "/s"; -} - -std::string eta_text(int64_t eta_seconds) { - if (eta_seconds < 0) { - return ""; - } - char buf[32]; - if (eta_seconds >= 3600) { - std::snprintf(buf, sizeof(buf), "%lldh%lldm", static_cast(eta_seconds / 3600), - static_cast((eta_seconds % 3600) / 60)); - } else if (eta_seconds >= 60) { - std::snprintf(buf, sizeof(buf), "%lldm%llds", static_cast(eta_seconds / 60), - static_cast(eta_seconds % 60)); - } else { - std::snprintf(buf, sizeof(buf), "%llds", static_cast(eta_seconds)); - } - return buf; -} - -float fraction_of(const runanywhere::v1::DownloadProgress& p) { - if (p.overall_progress() > 0.0f) { - return std::min(1.0f, p.overall_progress()); - } - if (p.total_bytes() > 0) { - return std::min(1.0f, static_cast(p.bytes_downloaded()) / - static_cast(p.total_bytes())); - } - return 0.0f; -} - -} // namespace - -ProgressRenderer::ProgressRenderer(bool interactive) - : interactive_(interactive && term::stderr_is_tty()) {} - -std::string ProgressRenderer::render_bar(float fraction, int width) const { - const int filled = static_cast(fraction * static_cast(width)); - std::string bar = "▕"; - for (int i = 0; i < width; ++i) { - bar += (i < filled) ? "█" : " "; - } - bar += "▏"; - return bar; -} - -void ProgressRenderer::update(const runanywhere::v1::DownloadProgress& progress) { - const float fraction = fraction_of(progress); - const int percent = static_cast(fraction * 100.0f); - const std::string stage = stage_label(progress.state()); - - if (!interactive_) { - // Plain mode: line per stage change or 10%-step. - const int step = percent / 10; - if (stage != last_stage_ || step != last_step_) { - last_stage_ = stage; - last_step_ = step; - std::string line = stage + " " + progress.model_id() + " " + - std::to_string(percent) + "%"; - // bytes_downloaded is cumulative across a multi-file plan while - // total_bytes is per-file — only show the pair when coherent. - if (progress.total_bytes() > 0 && - progress.bytes_downloaded() <= progress.total_bytes()) { - line += " (" + out::human_bytes(progress.bytes_downloaded()) + "/" + - out::human_bytes(progress.total_bytes()) + ")"; - } - out::status_line(line); - } - return; - } - - // Interactive: redraw one line. - std::string line = stage + " " + progress.model_id() + " "; - const int width = term::terminal_width(); - const int bar_width = std::clamp(width - static_cast(line.size()) - 40, 10, 40); - line += render_bar(fraction, bar_width); - char pct[8]; - std::snprintf(pct, sizeof(pct), " %3d%%", percent); - line += pct; - if (progress.total_bytes() > 0 && progress.bytes_downloaded() <= progress.total_bytes()) { - line += " " + out::human_bytes(progress.bytes_downloaded()) + "/" + - out::human_bytes(progress.total_bytes()); - } - const std::string speed = speed_text(progress.bytes_per_second()); - if (!speed.empty()) { - line += " " + speed; - } - const std::string eta = eta_text(progress.eta_seconds()); - if (!eta.empty()) { - line += " ETA " + eta; - } - if (progress.total_files() > 1) { - line += " [" + std::to_string(progress.current_file_index() + 1) + "/" + - std::to_string(progress.total_files()) + "]"; - } - - std::fprintf(stderr, "\r\033[2K%s", line.c_str()); - std::fflush(stderr); - line_open_ = true; -} - -void ProgressRenderer::finish() { - if (line_open_) { - std::fprintf(stderr, "\n"); - std::fflush(stderr); - line_open_ = false; - } -} - -// ----------------------------------------------------------------------------- -// DownloadProgressScope -// ----------------------------------------------------------------------------- - -namespace { -DownloadProgressScope* g_active_scope = nullptr; -} - -DownloadProgressScope::DownloadProgressScope(std::string model_id, bool interactive) - : renderer_(interactive), model_id_(std::move(model_id)) { - g_active_scope = this; - rac_download_set_progress_proto_callback(&DownloadProgressScope::callback, nullptr); -} - -DownloadProgressScope::~DownloadProgressScope() { - rac_download_set_progress_proto_callback(nullptr, nullptr); - g_active_scope = nullptr; - std::lock_guard lock(mutex_); - renderer_.finish(); -} - -void DownloadProgressScope::callback(const uint8_t* proto_bytes, size_t proto_size, - void* /*user_data*/) { - DownloadProgressScope* scope = g_active_scope; - if (!scope) { - return; - } - runanywhere::v1::DownloadProgress progress; - if (!progress.ParseFromArray(proto_bytes, static_cast(proto_size))) { - return; - } - std::lock_guard lock(scope->mutex_); - if (!scope->model_id_.empty() && progress.model_id() != scope->model_id_) { - return; - } - scope->renderer_.update(progress); - if (progress.state() == runanywhere::v1::DOWNLOAD_STATE_COMPLETED || - progress.state() == runanywhere::v1::DOWNLOAD_STATE_FAILED || - progress.state() == runanywhere::v1::DOWNLOAD_STATE_CANCELLED) { - scope->renderer_.finish(); - } -} - -} // namespace rcli::progress diff --git a/rcli/src/progress/progress_bar.h b/rcli/src/progress/progress_bar.h deleted file mode 100644 index 13fa26dd55..0000000000 --- a/rcli/src/progress/progress_bar.h +++ /dev/null @@ -1,61 +0,0 @@ -/** - * @file progress_bar.h - * @brief Renders runanywhere.v1.DownloadProgress updates on stderr. - * - * TTY: single re-drawn line — stage, bar, bytes, speed, ETA. - * Non-TTY / --no-progress: one plain line per 10% step (and per stage change) - * so CI logs stay readable. - */ - -#ifndef RCLI_PROGRESS_PROGRESS_BAR_H -#define RCLI_PROGRESS_PROGRESS_BAR_H - -#include -#include - -#include "download_service.pb.h" - -namespace rcli::progress { - -class ProgressRenderer { - public: - /** interactive=false forces plain-line mode. */ - explicit ProgressRenderer(bool interactive); - - void update(const runanywhere::v1::DownloadProgress& progress); - - /** Erase/terminate the in-place line (call before printing results). */ - void finish(); - - private: - std::string render_bar(float fraction, int width) const; - - bool interactive_; - bool line_open_ = false; - int last_step_ = -1; - std::string last_stage_; -}; - -/** - * RAII wrapper: registers the process-wide download progress callback and - * renders updates for one model while alive (used by commands whose commons - * call may auto-download, e.g. lifecycle load in `rcli run`). Only one scope - * may be active per process at a time. Thread-safe — events arrive on - * orchestrator worker threads. - */ -class DownloadProgressScope { - public: - DownloadProgressScope(std::string model_id, bool interactive); - ~DownloadProgressScope(); - - private: - static void callback(const uint8_t* proto_bytes, size_t proto_size, void* user_data); - - std::mutex mutex_; - ProgressRenderer renderer_; - std::string model_id_; -}; - -} // namespace rcli::progress - -#endif // RCLI_PROGRESS_PROGRESS_BAR_H diff --git a/rcli/src/repl/repl.cpp b/rcli/src/repl/repl.cpp deleted file mode 100644 index 28b8ad6ca9..0000000000 --- a/rcli/src/repl/repl.cpp +++ /dev/null @@ -1,73 +0,0 @@ -#include "repl/repl.h" - -#include -#include -#include -#include - -#if !defined(RCLI_NO_LINENOISE) -extern "C" { -#include -} -#endif - -namespace rcli::repl { - -namespace { -// MSVC's / decode a narrow std::string through the ANSI -// code page; build the path from UTF-8 so non-ASCII history paths (e.g. an -// international Windows username) resolve correctly. POSIX is unaffected. -std::filesystem::path utf8_path(const std::string& s) { - return std::filesystem::path(reinterpret_cast(s.c_str())); -} -} // namespace - -LineEditor::LineEditor(std::string history_path) : history_path_(std::move(history_path)) { - if (!history_path_.empty()) { - std::error_code ec; - std::filesystem::create_directories(utf8_path(history_path_).parent_path(), ec); -#if !defined(RCLI_NO_LINENOISE) - linenoiseHistoryLoad(history_path_.c_str()); - linenoiseHistorySetMaxLen(512); -#endif - } -} - -LineEditor::~LineEditor() { -#if !defined(RCLI_NO_LINENOISE) - if (!history_path_.empty()) { - linenoiseHistorySave(history_path_.c_str()); - } -#endif -} - -bool LineEditor::read_line(const std::string& prompt, std::string* out_line) { -#if defined(RCLI_NO_LINENOISE) - std::cerr << prompt; - std::cerr.flush(); - return static_cast(std::getline(std::cin, *out_line)); -#else - char* raw = linenoise(prompt.c_str()); - if (raw == nullptr) { - return false; // EOF / Ctrl-D (Ctrl-C inside linenoise returns NULL too) - } - *out_line = raw; - linenoiseFree(raw); - return true; -#endif -} - -void LineEditor::add_history(const std::string& line) { - if (!line.empty()) { -#if defined(RCLI_NO_LINENOISE) - if (!history_path_.empty()) { - std::ofstream history(utf8_path(history_path_), std::ios::app); - history << line << '\n'; - } -#else - linenoiseHistoryAdd(line.c_str()); -#endif - } -} - -} // namespace rcli::repl diff --git a/rcli/src/repl/repl.h b/rcli/src/repl/repl.h deleted file mode 100644 index 32b6522d23..0000000000 --- a/rcli/src/repl/repl.h +++ /dev/null @@ -1,31 +0,0 @@ -/** - * @file repl.h - * @brief Thin RAII wrapper over vendored linenoise (history + line input). - */ - -#ifndef RCLI_REPL_REPL_H -#define RCLI_REPL_REPL_H - -#include - -namespace rcli::repl { - -class LineEditor { - public: - /** history_path may be empty (no persistence, e.g. RUNANYWHERE_NOHISTORY). */ - explicit LineEditor(std::string history_path); - ~LineEditor(); - - /** False on EOF (Ctrl-D). Empty lines are returned as empty strings. */ - bool read_line(const std::string& prompt, std::string* out_line); - - /** Record a line in history (skips empties/duplicates of last entry). */ - void add_history(const std::string& line); - - private: - std::string history_path_; -}; - -} // namespace rcli::repl - -#endif // RCLI_REPL_REPL_H diff --git a/rcli/src/util/term.cpp b/rcli/src/util/term.cpp deleted file mode 100644 index 3aaf011174..0000000000 --- a/rcli/src/util/term.cpp +++ /dev/null @@ -1,62 +0,0 @@ -#include "util/term.h" - -#include // stdout/stderr/stdin, _fileno (Windows TTY checks below) -#include - -#if defined(_WIN32) -#ifndef NOMINMAX -#define NOMINMAX -#endif -#include -#include -#else -#include -#include -#endif - -namespace rcli::term { - -bool stdout_is_tty() { -#if defined(_WIN32) - return _isatty(_fileno(stdout)) != 0; -#else - return isatty(STDOUT_FILENO) == 1; -#endif -} - -bool stderr_is_tty() { -#if defined(_WIN32) - return _isatty(_fileno(stderr)) != 0; -#else - return isatty(STDERR_FILENO) == 1; -#endif -} - -bool stdin_is_tty() { -#if defined(_WIN32) - return _isatty(_fileno(stdin)) != 0; -#else - return isatty(STDIN_FILENO) == 1; -#endif -} - -int terminal_width() { -#if defined(_WIN32) - CONSOLE_SCREEN_BUFFER_INFO info{}; - if (GetConsoleScreenBufferInfo(GetStdHandle(STD_ERROR_HANDLE), &info)) { - return static_cast(info.srWindow.Right - info.srWindow.Left + 1); - } -#else - winsize ws{}; - if (ioctl(STDERR_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col > 0) { - return ws.ws_col; - } -#endif - return 80; -} - -bool color_enabled() { - return stderr_is_tty() && std::getenv("NO_COLOR") == nullptr; -} - -} // namespace rcli::term diff --git a/rcli/src/util/term.h b/rcli/src/util/term.h deleted file mode 100644 index ac24dbed1f..0000000000 --- a/rcli/src/util/term.h +++ /dev/null @@ -1,28 +0,0 @@ -/** - * @file term.h - * @brief Terminal capabilities: TTY detection, width, color policy. - */ - -#ifndef RCLI_UTIL_TERM_H -#define RCLI_UTIL_TERM_H - -namespace rcli::term { - -/** True when stdout is an interactive terminal. */ -bool stdout_is_tty(); - -/** True when stderr is an interactive terminal. */ -bool stderr_is_tty(); - -/** True when stdin is an interactive terminal (REPL gate). */ -bool stdin_is_tty(); - -/** Columns of the controlling terminal (fallback 80). */ -int terminal_width(); - -/** ANSI color allowed on stderr: TTY and NO_COLOR unset. */ -bool color_enabled(); - -} // namespace rcli::term - -#endif // RCLI_UTIL_TERM_H diff --git a/rcli/src/windows_proto_compat.h b/rcli/src/windows_proto_compat.h deleted file mode 100644 index 8dff488c0f..0000000000 --- a/rcli/src/windows_proto_compat.h +++ /dev/null @@ -1,28 +0,0 @@ -#ifndef RCLI_WINDOWS_PROTO_COMPAT_H -#define RCLI_WINDOWS_PROTO_COMPAT_H - -// Force-included ahead of every rcli translation unit on Windows (see the CLI -// CMakeLists). — pulled in via by Abseil/protobuf headers -// the CLI transitively includes — defines ERROR_SEVERITY_WARNING and -// ERROR_SEVERITY_ERROR as preprocessor macros. Those clobber the identically -// named values of the generated proto enum runanywhere::v1::ErrorSeverity, -// breaking errors.pb.h with "expected '}' before numeric constant" wherever a -// TU includes windows.h before the proto header (e.g. cmd_run.cpp). -// -// Include windows.h once up front and undef only the two colliding macros, so -// later proto headers parse cleanly. Subsequent includes are no-ops -// (its own include guard), so the undefs stick. - -#if defined(_WIN32) -#ifndef NOMINMAX -#define NOMINMAX -#endif -#ifndef WIN32_LEAN_AND_MEAN -#define WIN32_LEAN_AND_MEAN -#endif -#include -#undef ERROR_SEVERITY_WARNING -#undef ERROR_SEVERITY_ERROR -#endif // _WIN32 - -#endif // RCLI_WINDOWS_PROTO_COMPAT_H diff --git a/rcli/tests/CMakeLists.txt b/rcli/tests/CMakeLists.txt deleted file mode 100644 index 9aa460f7f4..0000000000 --- a/rcli/tests/CMakeLists.txt +++ /dev/null @@ -1,37 +0,0 @@ -# rcli unit tests — reuse the commons TestSuite harness (tests/test_common.h). -add_executable(test_rcli_unit test_rcli_unit.cpp) -target_include_directories(test_rcli_unit PRIVATE - ${CMAKE_SOURCE_DIR}/core/tests -) -target_link_libraries(test_rcli_unit PRIVATE rcli_core) -add_test(NAME rcli_unit_tests COMMAND test_rcli_unit --run-all) - -# rcli segment-command tests — parse/validation/render coverage, no backend. -add_executable(test_rcli_segment test_rcli_segment.cpp) -target_include_directories(test_rcli_segment PRIVATE - ${CMAKE_SOURCE_DIR}/core/tests -) -target_link_libraries(test_rcli_segment PRIVATE rcli_core) -add_test(NAME rcli_segment_tests COMMAND test_rcli_segment --run-all) - -# Live telemetry integration — real authenticated POST per modality against the -# configured backend. Opt-in: no-ops without `--live` + creds, so it stays safe -# in ctest/CI. Links rcli_core for bootstrap() + the HTTP transport + auth. -add_executable(test_rcli_telemetry_live test_rcli_telemetry_live.cpp) -target_include_directories(test_rcli_telemetry_live PRIVATE - ${CMAKE_SOURCE_DIR}/core/tests -) -target_link_libraries(test_rcli_telemetry_live PRIVATE rcli_core) -add_test(NAME rcli_telemetry_live_tests COMMAND test_rcli_telemetry_live) - -if(TARGET rac_backend_mlx) - add_executable(test_rcli_mlx_e2e test_rcli_mlx_e2e.cpp) - target_include_directories(test_rcli_mlx_e2e PRIVATE - ${CMAKE_SOURCE_DIR}/core/tests - ) - target_link_libraries(test_rcli_mlx_e2e PRIVATE rcli_core) - if(TARGET nlohmann_json::nlohmann_json) - target_link_libraries(test_rcli_mlx_e2e PRIVATE nlohmann_json::nlohmann_json) - endif() - add_test(NAME rcli_mlx_e2e_tests COMMAND test_rcli_mlx_e2e --run-all) -endif() diff --git a/rcli/tests/test_rcli_mlx_e2e.cpp b/rcli/tests/test_rcli_mlx_e2e.cpp deleted file mode 100644 index 7565b0029d..0000000000 --- a/rcli/tests/test_rcli_mlx_e2e.cpp +++ /dev/null @@ -1,1129 +0,0 @@ -/** - * @file test_rcli_mlx_e2e.cpp - * @brief In-process rcli E2E coverage for the MLX backend contract. - * - * The production MLX runtime is Swift/MLX. This test installs the same C - * callback table that the Swift runtime installs, then invokes the actual rcli - * command stack against a local MLX-style folder. That keeps the test offline - * and fast while exercising rcli parsing, bootstrap, backend registration, - * commons lifecycle loading, MLX callback dispatch, and streaming output. - */ - -#include "test_common.h" - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -#include "app.h" -#include "bootstrap.h" -#include "io/wav_io.h" -#include "llm_service.pb.h" -#include "model_types.pb.h" -#include "rac/backends/rac_mlx.h" -#include "rac/core/rac_core.h" -#include "rac/core/rac_model_lifecycle.h" -#include "rac/features/embeddings/rac_embeddings_service.h" -#include "rac/features/llm/rac_llm_service.h" -#include "rac/features/stt/rac_stt_service.h" -#include "rac/features/tts/rac_tts_service.h" -#include "rac/features/vlm/rac_vlm_service.h" -#include "rac/foundation/rac_proto_buffer.h" -#include "rac/infrastructure/model_management/rac_model_registry.h" -#include "rac/plugin/rac_plugin_entry.h" - -namespace { - -namespace v1 = runanywhere::v1; - -struct FakeMlxSession { - rac_mlx_session_kind_t kind = RAC_MLX_SESSION_KIND_LLM; - std::string model_id; - std::string model_path; -}; - -struct FakeMlxState { - int create_count = 0; - int initialize_count = 0; - int llm_generate_count = 0; - int stream_count = 0; - int vlm_process_count = 0; - int vlm_stream_count = 0; - int embed_batch_count = 0; - int embedding_info_count = 0; - int stt_transcribe_count = 0; - int stt_stream_count = 0; - int stt_info_count = 0; - int tts_synthesize_count = 0; - int tts_stream_count = 0; - int tts_stop_count = 0; - int tts_info_count = 0; - rac_mlx_session_kind_t last_kind = RAC_MLX_SESSION_KIND_LLM; - std::string last_model_path; - size_t last_embed_batch_size = 0; - size_t last_audio_size = 0; - std::string last_tts_text; -}; - -FakeMlxState g_mlx_state; - -std::filesystem::path make_temp_dir(const std::string &name) { - const auto stamp = - std::chrono::steady_clock::now().time_since_epoch().count(); - std::filesystem::path dir = std::filesystem::temp_directory_path() / - (name + "-" + std::to_string(stamp)); - std::filesystem::create_directories(dir); - return dir; -} - -bool write_file(const std::filesystem::path &path, - const std::string &contents) { - std::ofstream out(path, std::ios::binary); - if (!out.is_open()) { - return false; - } - out << contents; - return out.good(); -} - -bool serialize(const google::protobuf::MessageLite &message, - std::vector *out) { - out->resize(message.ByteSizeLong()); - if (out->empty()) { - return true; - } - return message.SerializeToArray(out->data(), static_cast(out->size())); -} - -rac_result_t fake_create(rac_mlx_session_kind_t kind, const char *model_id, - rac_handle_t *out_handle, void *) { - if (!out_handle) { - return RAC_ERROR_NULL_POINTER; - } - auto *session = new FakeMlxSession(); - session->kind = kind; - session->model_id = model_id ? model_id : ""; - *out_handle = session; - g_mlx_state.create_count++; - g_mlx_state.last_kind = kind; - return RAC_SUCCESS; -} - -rac_result_t fake_initialize(rac_handle_t handle, const char *model_path, - void *) { - if (!handle || !model_path) { - return RAC_ERROR_NULL_POINTER; - } - auto *session = static_cast(handle); - session->model_path = model_path; - g_mlx_state.initialize_count++; - g_mlx_state.last_model_path = model_path; - return RAC_SUCCESS; -} - -rac_result_t fake_llm_generate(rac_handle_t, const char *prompt, - const rac_llm_options_t *, - rac_llm_result_t *out_result, void *) { - if (!prompt || !out_result) { - return RAC_ERROR_NULL_POINTER; - } - std::memset(out_result, 0, sizeof(*out_result)); - const std::string text = "mlx-stub: " + std::string(prompt); - out_result->text = strdup(text.c_str()); - out_result->prompt_tokens = 2; - out_result->completion_tokens = 3; - out_result->total_tokens = 5; - out_result->total_time_ms = 7; - out_result->tokens_per_second = 100.0f; - g_mlx_state.llm_generate_count++; - return out_result->text ? RAC_SUCCESS : RAC_ERROR_OUT_OF_MEMORY; -} - -rac_result_t fake_llm_generate_stream(rac_handle_t, const char *prompt, - const rac_llm_options_t *, - rac_llm_stream_callback_fn callback, - void *callback_user_data, void *) { - if (!prompt || !callback) { - return RAC_ERROR_NULL_POINTER; - } - g_mlx_state.stream_count++; - const std::string token = "mlx-stub: " + std::string(prompt); - if (callback(token.c_str(), RAC_FALSE, nullptr, /*tokens_in_delta*/ 1, callback_user_data) != - RAC_TRUE) { - return RAC_ERROR_STREAM_CANCELLED; - } - return RAC_SUCCESS; -} - -rac_result_t fake_vlm_process(rac_handle_t, const rac_vlm_image_t *, - const char *prompt, const rac_vlm_options_t *, - rac_vlm_result_t *out_result, void *) { - if (!prompt || !out_result) { - return RAC_ERROR_NULL_POINTER; - } - std::memset(out_result, 0, sizeof(*out_result)); - const std::string text = "mlx-vlm-stub: " + std::string(prompt); - out_result->text = strdup(text.c_str()); - out_result->completion_tokens = 3; - out_result->total_tokens = 8; - out_result->tokens_per_second = 50.0f; - g_mlx_state.vlm_process_count++; - return out_result->text ? RAC_SUCCESS : RAC_ERROR_OUT_OF_MEMORY; -} - -rac_result_t fake_vlm_process_stream(rac_handle_t, const rac_vlm_image_t *, - const char *prompt, - const rac_vlm_options_t *, - rac_vlm_stream_callback_fn callback, - void *callback_user_data, void *) { - if (!prompt || !callback) { - return RAC_ERROR_NULL_POINTER; - } - g_mlx_state.vlm_stream_count++; - return callback(prompt, callback_user_data) == RAC_TRUE ? RAC_SUCCESS - : RAC_ERROR_CANCELLED; -} - -rac_result_t fake_embed_batch(rac_handle_t, const char *const *texts, - size_t num_texts, - const rac_embeddings_options_t *, - rac_embeddings_result_t *out_result, void *) { - if (!texts || !out_result) { - return RAC_ERROR_NULL_POINTER; - } - std::memset(out_result, 0, sizeof(*out_result)); - out_result->num_embeddings = num_texts; - out_result->dimension = 2; - out_result->embeddings = static_cast( - std::calloc(num_texts, sizeof(rac_embedding_vector_t))); - if (!out_result->embeddings) { - return RAC_ERROR_OUT_OF_MEMORY; - } - for (size_t i = 0; i < num_texts; ++i) { - out_result->embeddings[i].dimension = 2; - out_result->embeddings[i].data = - static_cast(std::calloc(2, sizeof(float))); - if (!out_result->embeddings[i].data) { - rac_embeddings_result_free(out_result); - return RAC_ERROR_OUT_OF_MEMORY; - } - out_result->embeddings[i].data[0] = texts[i] && texts[i][0] ? 1.0f : 0.0f; - out_result->embeddings[i].data[1] = 0.5f; - } - out_result->total_tokens = static_cast(num_texts); - g_mlx_state.embed_batch_count++; - g_mlx_state.last_embed_batch_size = num_texts; - return RAC_SUCCESS; -} - -rac_result_t fake_embedding_info(rac_handle_t, rac_embeddings_info_t *out_info, - void *) { - if (!out_info) { - return RAC_ERROR_NULL_POINTER; - } - std::memset(out_info, 0, sizeof(*out_info)); - out_info->is_ready = RAC_TRUE; - out_info->dimension = 2; - out_info->max_tokens = 512; - g_mlx_state.embedding_info_count++; - return RAC_SUCCESS; -} - -rac_result_t fake_stt_transcribe(rac_handle_t, const void *audio_data, - size_t audio_size, const rac_stt_options_t *, - rac_stt_result_t *out_result, void *) { - if (!audio_data || audio_size == 0 || !out_result) { - return RAC_ERROR_NULL_POINTER; - } - std::memset(out_result, 0, sizeof(*out_result)); - const std::string text = - "mlx-stt-stub: " + std::to_string(audio_size) + " bytes"; - out_result->text = strdup(text.c_str()); - out_result->detected_language = strdup("en"); - out_result->confidence = 0.95f; - out_result->processing_time_ms = 11; - g_mlx_state.stt_transcribe_count++; - g_mlx_state.last_audio_size = audio_size; - if (!out_result->text || !out_result->detected_language) { - return RAC_ERROR_OUT_OF_MEMORY; - } - return RAC_SUCCESS; -} - -rac_result_t fake_stt_transcribe_stream(rac_handle_t, const void *audio_data, - size_t audio_size, - const rac_stt_options_t *, - rac_stt_stream_callback_t callback, - void *callback_user_data, void *) { - if (!audio_data || audio_size == 0 || !callback) { - return RAC_ERROR_NULL_POINTER; - } - g_mlx_state.stt_stream_count++; - callback("mlx-stt-partial", RAC_FALSE, callback_user_data); - callback("mlx-stt-final", RAC_TRUE, callback_user_data); - return RAC_SUCCESS; -} - -rac_result_t fake_stt_info(rac_handle_t, rac_stt_info_t *out_info, void *) { - if (!out_info) { - return RAC_ERROR_NULL_POINTER; - } - std::memset(out_info, 0, sizeof(*out_info)); - out_info->is_ready = RAC_TRUE; - out_info->current_model = "mlx.fake.stt"; - out_info->supports_streaming = RAC_TRUE; - g_mlx_state.stt_info_count++; - return RAC_SUCCESS; -} - -rac_result_t fake_tts_synthesize(rac_handle_t, const char *text, - const rac_tts_options_t *, - rac_tts_result_t *out_result, void *) { - if (!text || !out_result) { - return RAC_ERROR_NULL_POINTER; - } - std::memset(out_result, 0, sizeof(*out_result)); - constexpr size_t kSampleCount = 8; - auto *samples = - static_cast(std::calloc(kSampleCount, sizeof(float))); - if (!samples) { - return RAC_ERROR_OUT_OF_MEMORY; - } - for (size_t i = 0; i < kSampleCount; ++i) { - samples[i] = (i % 2 == 0) ? 0.25f : -0.25f; - } - out_result->audio_data = samples; - out_result->audio_size = kSampleCount * sizeof(float); - out_result->audio_format = RAC_AUDIO_FORMAT_PCM; - out_result->sample_rate = 22050; - out_result->duration_ms = 1; - out_result->processing_time_ms = 13; - g_mlx_state.tts_synthesize_count++; - g_mlx_state.last_tts_text = text; - return RAC_SUCCESS; -} - -rac_result_t fake_tts_synthesize_stream(rac_handle_t, const char *text, - const rac_tts_options_t *, - rac_tts_stream_callback_t callback, - void *callback_user_data, void *) { - if (!text || !callback) { - return RAC_ERROR_NULL_POINTER; - } - g_mlx_state.tts_stream_count++; - const float samples[2] = {0.1f, -0.1f}; - callback(samples, sizeof(samples), callback_user_data); - return RAC_SUCCESS; -} - -rac_result_t fake_tts_stop(rac_handle_t, void *) { - g_mlx_state.tts_stop_count++; - return RAC_SUCCESS; -} - -rac_result_t fake_tts_info(rac_handle_t, rac_tts_info_t *out_info, void *) { - if (!out_info) { - return RAC_ERROR_NULL_POINTER; - } - std::memset(out_info, 0, sizeof(*out_info)); - out_info->is_ready = RAC_TRUE; - out_info->is_synthesizing = RAC_FALSE; - g_mlx_state.tts_info_count++; - return RAC_SUCCESS; -} - -rac_result_t fake_cancel(rac_handle_t, void *) { return RAC_SUCCESS; } -rac_result_t fake_cleanup(rac_handle_t, void *) { return RAC_SUCCESS; } -void fake_destroy(rac_handle_t handle, void *) { - delete static_cast(handle); -} - -bool install_fake_mlx_callbacks() { - rac_mlx_callbacks_t callbacks{}; - callbacks.struct_size = sizeof(callbacks); - callbacks.create = fake_create; - callbacks.initialize = fake_initialize; - callbacks.llm_generate = fake_llm_generate; - callbacks.llm_generate_stream = fake_llm_generate_stream; - callbacks.vlm_process = fake_vlm_process; - callbacks.vlm_process_stream = fake_vlm_process_stream; - callbacks.embed_batch = fake_embed_batch; - callbacks.embedding_info = fake_embedding_info; - callbacks.stt_transcribe = fake_stt_transcribe; - callbacks.stt_transcribe_stream = fake_stt_transcribe_stream; - callbacks.stt_info = fake_stt_info; - callbacks.tts_synthesize = fake_tts_synthesize; - callbacks.tts_synthesize_stream = fake_tts_synthesize_stream; - callbacks.tts_stop = fake_tts_stop; - callbacks.tts_info = fake_tts_info; - callbacks.cancel = fake_cancel; - callbacks.cleanup = fake_cleanup; - callbacks.destroy = fake_destroy; - return rac_mlx_set_callbacks(&callbacks) == RAC_SUCCESS; -} - -bool register_local_mlx_model(const std::filesystem::path &model_dir, - const char *id, const char *name, - v1::ModelCategory category) { - v1::ModelInfo model; - model.set_id(id); - model.set_name(name); - model.set_category(category); - model.set_format(v1::MODEL_FORMAT_SAFETENSORS); - model.set_framework(v1::INFERENCE_FRAMEWORK_MLX); - model.set_local_path(model_dir.string()); - model.set_is_available(true); - model.set_registry_status(v1::MODEL_REGISTRY_STATUS_DOWNLOADED); - - auto *config = model.mutable_multi_file()->add_files(); - config->set_filename("config.json"); - config->set_destination_path("config.json"); - config->set_is_optional(false); - config->set_role(v1::MODEL_FILE_ROLE_COMPANION); - - auto *weights = model.mutable_multi_file()->add_files(); - weights->set_filename("model.safetensors"); - weights->set_destination_path("model.safetensors"); - weights->set_is_optional(false); - weights->set_role(v1::MODEL_FILE_ROLE_PRIMARY_MODEL); - - auto *tokenizer = model.mutable_multi_file()->add_files(); - tokenizer->set_filename("tokenizer.json"); - tokenizer->set_destination_path("tokenizer.json"); - tokenizer->set_is_optional(false); - tokenizer->set_role(v1::MODEL_FILE_ROLE_TOKENIZER); - - std::vector bytes; - return serialize(model, &bytes) && rac_model_registry_register_proto( - rac_get_model_registry(), bytes.data(), - bytes.size()) == RAC_SUCCESS; -} - -class StdoutCapture { -public: - bool start() { - // Flush any C-stdio-buffered stdout (e.g. the runner's own "--- Running: - // ... ---" banner) BEFORE swapping the raw fd. Without this, buffered - // text not yet written to fd 1 gets flushed later into whichever pipe - // fd 1 has been dup2'd to at that point -- silently corrupting a - // different capture with a stray "---" prefix. - std::fflush(stdout); - if (pipe(pipe_fds_) != 0) { - return false; - } - saved_stdout_ = dup(STDOUT_FILENO); - if (saved_stdout_ < 0) { - return false; - } - return dup2(pipe_fds_[1], STDOUT_FILENO) >= 0; - } - - std::string finish() { - std::fflush(stdout); - dup2(saved_stdout_, STDOUT_FILENO); - close(saved_stdout_); - saved_stdout_ = -1; - close(pipe_fds_[1]); - pipe_fds_[1] = -1; - - std::string output; - char buffer[4096]; - ssize_t n = 0; - while ((n = read(pipe_fds_[0], buffer, sizeof(buffer))) > 0) { - output.append(buffer, static_cast(n)); - } - close(pipe_fds_[0]); - pipe_fds_[0] = -1; - return output; - } - -private: - int pipe_fds_[2] = {-1, -1}; - int saved_stdout_ = -1; -}; - -int run_cli_capture(const std::vector &args, - std::string *stdout_text) { - rcli::GlobalOptions options; - CLI::App app{ - "RunAnywhere on-device AI CLI — run, manage and serve local models"}; - rcli::configure_app(app, options); - - std::vector mutable_args = args; - std::vector argv; - argv.reserve(mutable_args.size()); - for (std::string &arg : mutable_args) { - argv.push_back(arg.data()); - } - - StdoutCapture capture; - if (!capture.start()) { - return 1; - } - - int exit_code = 0; - try { - app.parse(static_cast(argv.size()), argv.data()); - } catch (const CLI::RuntimeError &e) { - exit_code = e.get_exit_code() != 0 ? e.get_exit_code() : 1; - } catch (const CLI::ParseError &e) { - exit_code = app.exit(e); - } catch (const std::exception &) { - exit_code = 1; - } - *stdout_text = capture.finish(); - return exit_code; -} - -bool backend_has_primitives(const std::string &json_text, const std::string &backend_name, - std::initializer_list expected, - std::string *error) { - try { - const auto document = nlohmann::json::parse(json_text); - for (const auto &backend : document.value("backends", nlohmann::json::array())) { - if (backend.value("name", "") != backend_name) { - continue; - } - std::set primitives; - for (const auto &primitive : backend.value("primitives", nlohmann::json::array())) { - primitives.insert(primitive.value("name", "")); - } - for (const auto &name : expected) { - if (!primitives.contains(name)) { - if (error) { - *error = "missing primitive " + name; - } - return false; - } - } - return true; - } - if (error) { - *error = "backend not found: " + backend_name; - } - return false; - } catch (const std::exception &ex) { - if (error) { - *error = ex.what(); - } - return false; - } -} - -bool run_cli_or_fail(const std::vector &args, - const std::string &expected_label, - std::string *stdout_text, TestResult *result) { - const int code = run_cli_capture(args, stdout_text); - if (code == 0) { - return true; - } - result->expected = expected_label + " exit 0"; - result->actual = "exit " + std::to_string(code); - result->details = *stdout_text; - return false; -} - -bool register_mlx_backend_or_fail(TestResult *result) { - const rac_result_t rc = rac_backend_mlx_register(); - if (rc == RAC_SUCCESS || rc == RAC_ERROR_MODULE_ALREADY_REGISTERED) { - return true; - } - result->expected = "rac_backend_mlx_register success"; - result->actual = std::to_string(rc); - return false; -} - -rac_bool_t append_llm_token_callback(const char *token, rac_bool_t is_final, - const char * /*finish_reason*/, - int32_t /*tokens_in_delta*/, void *user_data) { - if (is_final) { - return RAC_TRUE; - } - auto *out = static_cast(user_data); - if (token && out) { - out->append(token); - } - return RAC_TRUE; -} - -rac_bool_t append_token_callback(const char *token, void *user_data) { - auto *out = static_cast(user_data); - if (token && out) { - out->append(token); - } - return RAC_TRUE; -} - -void append_stt_callback(const char *text, rac_bool_t is_final, - void *user_data) { - auto *out = static_cast(user_data); - if (text && out) { - if (!out->empty()) { - out->append("|"); - } - out->append(is_final == RAC_TRUE ? "final:" : "partial:"); - out->append(text); - } -} - -void count_tts_chunk_callback(const void *, size_t audio_size, - void *user_data) { - auto *total = static_cast(user_data); - if (total) { - *total += audio_size; - } -} - -TestResult test_mlx_callback_bridge_all_slots() { - TestResult result; - result.test_name = "mlx_callback_bridge_all_slots"; - - g_mlx_state = {}; - if (!install_fake_mlx_callbacks() || !register_mlx_backend_or_fail(&result)) { - if (result.details.empty()) { - result.details = "failed to install/register MLX callbacks"; - } - return result; - } - - const rac_engine_vtable_t *llm_vt = - rac_plugin_find_for_engine(RAC_PRIMITIVE_GENERATE_TEXT, "mlx"); - const rac_engine_vtable_t *vlm_vt = - rac_plugin_find_for_engine(RAC_PRIMITIVE_VLM, "mlx"); - const rac_engine_vtable_t *embed_vt = - rac_plugin_find_for_engine(RAC_PRIMITIVE_EMBED, "mlx"); - const rac_engine_vtable_t *stt_vt = - rac_plugin_find_for_engine(RAC_PRIMITIVE_TRANSCRIBE, "mlx"); - const rac_engine_vtable_t *tts_vt = - rac_plugin_find_for_engine(RAC_PRIMITIVE_SYNTHESIZE, "mlx"); - if (!llm_vt || !vlm_vt || !embed_vt || !stt_vt || !tts_vt || - !llm_vt->llm_ops || !vlm_vt->vlm_ops || !embed_vt->embedding_ops || - !stt_vt->stt_ops || !tts_vt->tts_ops) { - result.expected = "registered MLX vtable with all modality op slots"; - result.actual = "one or more MLX op slots missing"; - rac_backend_mlx_unregister(); - return result; - } - - void *llm = nullptr; - rac_llm_result_t llm_result{}; - std::string llm_stream; - rac_llm_info_t llm_info{}; - if (llm_vt->llm_ops->create("mlx.direct.llm", nullptr, &llm) != RAC_SUCCESS || - llm_vt->llm_ops->initialize(llm, "/tmp/mlx-direct-llm") != RAC_SUCCESS || - llm_vt->llm_ops->generate(llm, "direct", nullptr, &llm_result) != RAC_SUCCESS || - llm_vt->llm_ops->generate_stream(llm, "stream", nullptr, append_llm_token_callback, - &llm_stream) != RAC_SUCCESS || - llm_vt->llm_ops->get_info(llm, &llm_info) != RAC_SUCCESS || - llm_vt->llm_ops->cancel(llm) != RAC_SUCCESS || - llm_vt->llm_ops->cleanup(llm) != RAC_SUCCESS) { - result.details = "MLX LLM direct ops failed"; - if (llm_result.text) { - rac_llm_result_free(&llm_result); - } - if (llm) { - llm_vt->llm_ops->destroy(llm); - } - rac_backend_mlx_unregister(); - return result; - } - const bool llm_ok = llm_result.text && - std::string(llm_result.text) == "mlx-stub: direct" && - llm_stream == "mlx-stub: stream" && - llm_info.is_ready == RAC_TRUE && - g_mlx_state.llm_generate_count == 1 && - g_mlx_state.stream_count == 1 && - g_mlx_state.last_kind == RAC_MLX_SESSION_KIND_LLM; - rac_llm_result_free(&llm_result); - llm_vt->llm_ops->destroy(llm); - if (!llm_ok) { - result.details = "MLX LLM direct callbacks were not all exercised"; - rac_backend_mlx_unregister(); - return result; - } - - void *vlm = nullptr; - rac_vlm_result_t vlm_result{}; - std::string vlm_stream; - rac_vlm_info_t vlm_info{}; - rac_vlm_image_t image{}; - image.format = RAC_VLM_IMAGE_FORMAT_FILE_PATH; - image.file_path = "/tmp/mlx-direct-image.jpg"; - if (vlm_vt->vlm_ops->create("mlx.direct.vlm", nullptr, &vlm) != RAC_SUCCESS || - vlm_vt->vlm_ops->initialize(vlm, "/tmp/mlx-direct-vlm", nullptr) != RAC_SUCCESS || - vlm_vt->vlm_ops->process(vlm, &image, "look", nullptr, &vlm_result) != RAC_SUCCESS || - vlm_vt->vlm_ops->process_stream(vlm, &image, "watch", nullptr, append_token_callback, - &vlm_stream) != RAC_SUCCESS || - vlm_vt->vlm_ops->get_info(vlm, &vlm_info) != RAC_SUCCESS || - vlm_vt->vlm_ops->cancel(vlm) != RAC_SUCCESS || - vlm_vt->vlm_ops->cleanup(vlm) != RAC_SUCCESS) { - result.details = "MLX VLM direct ops failed"; - if (vlm_result.text) { - rac_vlm_result_free(&vlm_result); - } - if (vlm) { - vlm_vt->vlm_ops->destroy(vlm); - } - rac_backend_mlx_unregister(); - return result; - } - const bool vlm_ok = vlm_result.text && - std::string(vlm_result.text) == "mlx-vlm-stub: look" && - vlm_stream == "watch" && - vlm_info.is_ready == RAC_TRUE && - g_mlx_state.vlm_process_count == 1 && - g_mlx_state.vlm_stream_count == 1 && - g_mlx_state.last_kind == RAC_MLX_SESSION_KIND_VLM; - rac_vlm_result_free(&vlm_result); - vlm_vt->vlm_ops->destroy(vlm); - if (!vlm_ok) { - result.details = "MLX VLM direct callbacks were not all exercised"; - rac_backend_mlx_unregister(); - return result; - } - - void *embed = nullptr; - rac_embeddings_result_t embed_result{}; - rac_embeddings_info_t embed_info{}; - const char *embed_texts[] = {"one", "two"}; - if (embed_vt->embedding_ops->create("mlx.direct.embed", nullptr, &embed) != - RAC_SUCCESS || - embed_vt->embedding_ops->initialize(embed, "/tmp/mlx-direct-embed") != - RAC_SUCCESS || - embed_vt->embedding_ops->embed(embed, "single", nullptr, &embed_result) != - RAC_SUCCESS || - embed_vt->embedding_ops->get_info(embed, &embed_info) != RAC_SUCCESS) { - result.details = "MLX embedding direct single ops failed"; - rac_embeddings_result_free(&embed_result); - if (embed) { - embed_vt->embedding_ops->destroy(embed); - } - rac_backend_mlx_unregister(); - return result; - } - rac_embeddings_result_free(&embed_result); - if (embed_vt->embedding_ops->embed_batch(embed, embed_texts, 2, nullptr, - &embed_result) != RAC_SUCCESS || - embed_vt->embedding_ops->cleanup(embed) != RAC_SUCCESS) { - result.details = "MLX embedding direct batch ops failed"; - rac_embeddings_result_free(&embed_result); - embed_vt->embedding_ops->destroy(embed); - rac_backend_mlx_unregister(); - return result; - } - const bool embed_ok = embed_result.num_embeddings == 2 && - embed_info.is_ready == RAC_TRUE && - g_mlx_state.embed_batch_count == 2 && - g_mlx_state.embedding_info_count == 1 && - g_mlx_state.last_kind == - RAC_MLX_SESSION_KIND_EMBEDDINGS; - rac_embeddings_result_free(&embed_result); - embed_vt->embedding_ops->destroy(embed); - if (!embed_ok) { - result.details = "MLX embedding direct callbacks were not all exercised"; - rac_backend_mlx_unregister(); - return result; - } - - void *stt = nullptr; - const int16_t samples[] = {0, 128, -128, 256}; - rac_stt_result_t stt_result{}; - std::string stt_stream; - rac_stt_info_t stt_info{}; - if (stt_vt->stt_ops->create("mlx.direct.stt", nullptr, &stt) != RAC_SUCCESS || - stt_vt->stt_ops->initialize(stt, "/tmp/mlx-direct-stt") != RAC_SUCCESS || - stt_vt->stt_ops->transcribe(stt, samples, sizeof(samples), nullptr, - &stt_result) != RAC_SUCCESS || - stt_vt->stt_ops->transcribe_stream(stt, samples, sizeof(samples), nullptr, - append_stt_callback, - &stt_stream) != RAC_SUCCESS || - stt_vt->stt_ops->get_info(stt, &stt_info) != RAC_SUCCESS || - stt_vt->stt_ops->cleanup(stt) != RAC_SUCCESS) { - result.details = "MLX STT direct ops failed"; - if (stt_result.text) { - rac_stt_result_free(&stt_result); - } - if (stt) { - stt_vt->stt_ops->destroy(stt); - } - rac_backend_mlx_unregister(); - return result; - } - const bool stt_ok = stt_result.text && - std::string(stt_result.text).find("mlx-stt-stub") != - std::string::npos && - stt_stream == "partial:mlx-stt-partial|final:mlx-stt-final" && - stt_info.is_ready == RAC_TRUE && - g_mlx_state.stt_transcribe_count == 1 && - g_mlx_state.stt_stream_count == 1 && - g_mlx_state.stt_info_count == 1 && - g_mlx_state.last_kind == RAC_MLX_SESSION_KIND_STT; - rac_stt_result_free(&stt_result); - stt_vt->stt_ops->destroy(stt); - if (!stt_ok) { - result.details = "MLX STT direct callbacks were not all exercised"; - rac_backend_mlx_unregister(); - return result; - } - - void *tts = nullptr; - rac_tts_result_t tts_result{}; - size_t streamed_tts_bytes = 0; - rac_tts_info_t tts_info{}; - if (tts_vt->tts_ops->create("/tmp/mlx-direct-tts", nullptr, &tts) != RAC_SUCCESS || - tts_vt->tts_ops->initialize(tts) != RAC_SUCCESS || - tts_vt->tts_ops->synthesize(tts, "say it", nullptr, &tts_result) != - RAC_SUCCESS || - tts_vt->tts_ops->synthesize_stream(tts, "stream it", nullptr, - count_tts_chunk_callback, - &streamed_tts_bytes) != RAC_SUCCESS || - tts_vt->tts_ops->stop(tts) != RAC_SUCCESS || - tts_vt->tts_ops->get_info(tts, &tts_info) != RAC_SUCCESS || - tts_vt->tts_ops->cleanup(tts) != RAC_SUCCESS) { - result.details = "MLX TTS direct ops failed"; - rac_tts_result_free(&tts_result); - if (tts) { - tts_vt->tts_ops->destroy(tts); - } - rac_backend_mlx_unregister(); - return result; - } - // tts_stop_count is intentionally NOT asserted here: dispatch_interrupt() - // (rac_mlx_engine.cpp) only forwards stop/cancel to Swift while the - // originating operation is still active -- a stop() called after - // synthesize_stream() has already returned is a late interrupt and is - // deliberately dropped (see the "late interrupt" comment at its call - // site) so it cannot poison the next inference on this session. The - // LLM/VLM sections above hold cancel() to the same bar: they only check - // for RAC_SUCCESS, not that the fake callback fired. - const bool tts_ok = tts_result.audio_data && - tts_result.audio_size == 8 * sizeof(float) && - streamed_tts_bytes == 2 * sizeof(float) && - tts_info.is_ready == RAC_TRUE && - g_mlx_state.tts_synthesize_count == 1 && - g_mlx_state.tts_stream_count == 1 && - g_mlx_state.tts_info_count == 1 && - g_mlx_state.last_kind == RAC_MLX_SESSION_KIND_TTS; - rac_tts_result_free(&tts_result); - tts_vt->tts_ops->destroy(tts); - if (!tts_ok) { - result.details = "MLX TTS direct callbacks were not all exercised"; - rac_backend_mlx_unregister(); - return result; - } - - if (g_mlx_state.create_count != 5 || g_mlx_state.initialize_count != 5) { - result.details = - "MLX direct bridge should create/initialize one session per modality"; - rac_backend_mlx_unregister(); - return result; - } - - rac_backend_mlx_unregister(); - result.passed = true; - return result; -} - -TestResult test_rcli_mlx_run_end_to_end() { - TestResult result; - result.test_name = "rcli_mlx_run_end_to_end"; - - g_mlx_state = {}; - if (!install_fake_mlx_callbacks()) { - result.details = "failed to install MLX callbacks"; - return result; - } - - const std::filesystem::path home = make_temp_dir("rcli-mlx-home"); - const std::filesystem::path llm_dir = make_temp_dir("rcli-mlx-llm"); - const std::filesystem::path vlm_dir = make_temp_dir("rcli-mlx-vlm"); - const std::filesystem::path embedding_dir = make_temp_dir("rcli-mlx-embed"); - const std::filesystem::path stt_dir = make_temp_dir("rcli-mlx-stt"); - const std::filesystem::path tts_dir = make_temp_dir("rcli-mlx-tts"); - for (const auto &dir : {llm_dir, vlm_dir, embedding_dir, stt_dir, tts_dir}) { - if (!write_file(dir / "config.json", R"({"model_type":"qwen3"})") || - !write_file(dir / "model.safetensors", "fake-weights") || - !write_file(dir / "tokenizer.json", "{}")) { - result.details = "failed to create local MLX model folder"; - return result; - } - } - - const std::filesystem::path input_wav = home / "input.wav"; - const std::filesystem::path output_wav = home / "output.wav"; - const std::filesystem::path input_image = home / "image.rgb"; - if (!write_file(input_image, "fake image")) { - result.details = "failed to create fake VLM image"; - return result; - } - const std::vector pcm_samples = {0, 1024, -1024, 2048, - -2048, 1024, -1024, 0}; - std::string wav_error; - if (!rcli::wav::write_wav(input_wav.string(), pcm_samples.data(), - pcm_samples.size(), 16000, &wav_error)) { - result.details = wav_error; - return result; - } - - rcli::GlobalOptions options; - options.home_override = home.string(); - options.json = true; - options.no_progress = true; - rcli::Bootstrapped bootstrapped; - if (rcli::bootstrap(options, &bootstrapped) != RAC_SUCCESS) { - result.details = "bootstrap failed"; - return result; - } - if (!register_local_mlx_model(llm_dir, "mlx.fake.llm", "Fake MLX LLM", - v1::MODEL_CATEGORY_LANGUAGE) || - !register_local_mlx_model(vlm_dir, "mlx.fake.vlm", "Fake MLX VLM", - v1::MODEL_CATEGORY_MULTIMODAL) || - !register_local_mlx_model(embedding_dir, "mlx.fake.embed", - "Fake MLX Embeddings", - v1::MODEL_CATEGORY_EMBEDDING) || - !register_local_mlx_model(stt_dir, "mlx.fake.stt", "Fake MLX STT", - v1::MODEL_CATEGORY_SPEECH_RECOGNITION) || - !register_local_mlx_model(tts_dir, "mlx.fake.tts", "Fake MLX TTS", - v1::MODEL_CATEGORY_SPEECH_SYNTHESIS)) { - result.details = "failed to register local MLX model"; - rcli::shutdown(); - return result; - } - - std::string backends_json; - if (!run_cli_or_fail({"rcli", "--json", "--no-progress", "--home", - home.string(), "backends"}, - "backends", &backends_json, &result)) { - rcli::shutdown(); - return result; - } - std::string backend_error; - if (!backend_has_primitives(backends_json, "mlx", - {"generate_text", "vlm", "embed", "transcribe", - "synthesize"}, - &backend_error)) { - result.expected = - "mlx backend with generate_text/vlm/embed/transcribe/synthesize " - "primitives"; - result.actual = backend_error.empty() ? backends_json : backend_error + ": " + backends_json; - rcli::shutdown(); - return result; - } - - std::string list_json; - if (!run_cli_or_fail({"rcli", "--json", "--no-progress", "--home", - home.string(), "list", "--all"}, - "list", &list_json, &result)) { - rcli::shutdown(); - return result; - } - if (list_json.find("\"id\":\"mlx.fake.vlm\"") == std::string::npos || - list_json.find("\"modality\":\"vlm\"") == std::string::npos || - list_json.find("\"id\":\"mlx.fake.embed\"") == std::string::npos || - list_json.find("\"modality\":\"embedding\"") == std::string::npos || - list_json.find("\"id\":\"mlx.fake.stt\"") == std::string::npos || - list_json.find("\"modality\":\"stt\"") == std::string::npos || - list_json.find("\"backend\":\"MLX\"") == std::string::npos || - list_json.find("\"id\":\"mlx.fake.tts\"") == std::string::npos || - list_json.find("\"modality\":\"tts\"") == std::string::npos) { - result.expected = "MLX VLM/embedding/STT/TTS rows from rcli list --all"; - result.actual = list_json; - rcli::shutdown(); - return result; - } - - std::string run_json; - if (!run_cli_or_fail({"rcli", "--json", "--no-progress", "--home", - home.string(), "run", "mlx.fake.llm", "Hello MLX", - "--engine", "mlx", "--max-tokens", "4"}, - "LLM", &run_json, &result)) { - rcli::shutdown(); - return result; - } - if (run_json.find("\"model\":\"mlx.fake.llm\"") == std::string::npos || - run_json.find("\"response\":\"mlx-stub: Hello MLX\"") == - std::string::npos) { - result.expected = "JSON response from MLX stream callback"; - result.actual = run_json; - rcli::shutdown(); - return result; - } - if (g_mlx_state.create_count != 1 || g_mlx_state.initialize_count != 1 || - g_mlx_state.stream_count != 1 || - g_mlx_state.last_kind != RAC_MLX_SESSION_KIND_LLM) { - result.details = - "MLX LLM callback counts/kind were not exercised as expected"; - rcli::shutdown(); - return result; - } - if (g_mlx_state.last_model_path != llm_dir.string()) { - result.expected = llm_dir.string(); - result.actual = g_mlx_state.last_model_path; - result.details = "MLX LLM runtime should receive the model folder, not " - "model.safetensors"; - rcli::shutdown(); - return result; - } - - std::string vlm_json; - if (!run_cli_or_fail({"rcli", "--json", "--no-progress", "--home", - home.string(), "run", "mlx.fake.vlm", - "What is in the image?", "--image", - input_image.string(), "--engine", "mlx", "--max-tokens", - "4"}, - "VLM", &vlm_json, &result)) { - rcli::shutdown(); - return result; - } - if (vlm_json.find("\"model\":\"mlx.fake.vlm\"") == std::string::npos || - vlm_json.find("\"response\":\"mlx-vlm-stub: What is in the image?\"") == - std::string::npos) { - result.expected = "JSON VLM response from MLX callback"; - result.actual = vlm_json; - rcli::shutdown(); - return result; - } - if (g_mlx_state.vlm_process_count != 1 || - g_mlx_state.last_kind != RAC_MLX_SESSION_KIND_VLM) { - result.details = - "MLX VLM callback count/kind was not exercised as expected"; - rcli::shutdown(); - return result; - } - if (g_mlx_state.last_model_path != vlm_dir.string()) { - result.expected = vlm_dir.string(); - result.actual = g_mlx_state.last_model_path; - result.details = "MLX VLM runtime should receive the model folder, not " - "model.safetensors"; - rcli::shutdown(); - return result; - } - - std::string embed_json; - if (!run_cli_or_fail({"rcli", "--json", "--no-progress", "--home", - home.string(), "embed", "Hello MLX embeddings", - "--model", "mlx.fake.embed", "--text", "Batch item"}, - "embedding", &embed_json, &result)) { - rcli::shutdown(); - return result; - } - if (embed_json.find("\"model\":\"mlx.fake.embed\"") == std::string::npos || - embed_json.find("\"dimension\":2") == std::string::npos || - embed_json.find("\"count\":2") == std::string::npos || - embed_json.find("\"values\":[1,0.5]") == std::string::npos) { - result.expected = "JSON embedding vectors from MLX callback"; - result.actual = embed_json; - rcli::shutdown(); - return result; - } - if (g_mlx_state.embed_batch_count != 1 || - g_mlx_state.last_kind != RAC_MLX_SESSION_KIND_EMBEDDINGS || - g_mlx_state.last_embed_batch_size != 2) { - result.details = - "MLX embedding callback count/kind/batch size was not exercised as expected"; - rcli::shutdown(); - return result; - } - if (g_mlx_state.last_model_path != embedding_dir.string()) { - result.expected = embedding_dir.string(); - result.actual = g_mlx_state.last_model_path; - result.details = "MLX embedding runtime should receive the model folder, not " - "model.safetensors"; - rcli::shutdown(); - return result; - } - - std::string stt_json; - if (!run_cli_or_fail({"rcli", "--json", "--no-progress", "--home", - home.string(), "stt", "--model", "mlx.fake.stt", "--input", - input_wav.string()}, - "STT", &stt_json, &result)) { - rcli::shutdown(); - return result; - } - if (stt_json.find("\"model\":\"mlx.fake.stt\"") == std::string::npos || - stt_json.find("\"text\":\"mlx-stt-stub: ") == std::string::npos) { - result.expected = "JSON STT response from MLX callback"; - result.actual = stt_json; - rcli::shutdown(); - return result; - } - if (g_mlx_state.stt_transcribe_count != 1 || - g_mlx_state.last_kind != RAC_MLX_SESSION_KIND_STT || - g_mlx_state.last_audio_size != pcm_samples.size() * sizeof(int16_t)) { - result.details = "MLX STT callback counts/kind/audio size were not " - "exercised as expected"; - rcli::shutdown(); - return result; - } - if (g_mlx_state.last_model_path != stt_dir.string()) { - result.expected = stt_dir.string(); - result.actual = g_mlx_state.last_model_path; - result.details = "MLX STT runtime should receive the model folder, not " - "model.safetensors"; - rcli::shutdown(); - return result; - } - - std::string tts_json; - const bool tts_ok = run_cli_or_fail( - {"rcli", "--json", "--no-progress", "--home", home.string(), "tts", - "--model", "mlx.fake.tts", "--text", "Hello MLX audio", "--output", - output_wav.string()}, - "TTS", &tts_json, &result); - rcli::shutdown(); - if (!tts_ok) { - return result; - } - if (tts_json.find("\"voice\":\"mlx.fake.tts\"") == std::string::npos || - tts_json.find("\"sample_rate\":22050") == std::string::npos || - !std::filesystem::exists(output_wav) || - std::filesystem::file_size(output_wav) <= 44) { - result.expected = "JSON TTS response and written WAV from MLX callback"; - result.actual = tts_json; - return result; - } - if (g_mlx_state.tts_synthesize_count != 1 || - g_mlx_state.last_kind != RAC_MLX_SESSION_KIND_TTS || - g_mlx_state.last_tts_text != "Hello MLX audio") { - result.details = - "MLX TTS callback counts/kind/text were not exercised as expected"; - return result; - } - if (g_mlx_state.last_model_path != tts_dir.string()) { - result.expected = tts_dir.string(); - result.actual = g_mlx_state.last_model_path; - result.details = "MLX TTS runtime should receive the model folder, not " - "model.safetensors"; - return result; - } - if (g_mlx_state.create_count != 5 || g_mlx_state.initialize_count != 5) { - result.details = - "MLX create/initialize should run once per LLM/VLM/embedding/STT/TTS model"; - return result; - } - - result.passed = true; - return result; -} - -} // namespace - -int main(int argc, char **argv) { - TestSuite suite("rcli_mlx_e2e"); - suite.add("mlx_callback_bridge_all_slots", test_mlx_callback_bridge_all_slots); - suite.add("rcli_mlx_run_end_to_end", test_rcli_mlx_run_end_to_end); - return suite.run(argc, argv); -} diff --git a/rcli/tests/test_rcli_segment.cpp b/rcli/tests/test_rcli_segment.cpp deleted file mode 100644 index d543dfc277..0000000000 --- a/rcli/tests/test_rcli_segment.cpp +++ /dev/null @@ -1,504 +0,0 @@ -/** - * @file test_rcli_segment.cpp - * @brief rcli `segment` command tests — pure suite: no backend, no models, no - * network, no stream redirection. - * - * Covers exactly what is reachable for the `segment` command WITHOUT a - * segmentation backend: - * - io/image_io.cpp read_ppm(): the CLI-owned input gate for `segment`. - * - io/image_io.cpp write_png(): the sibling encoder (smoke + guards). - * - The CLI11 parse/validation layer of `rcli segment` (fires before the - * callback), asserting the documented 0/1/2 exit-code contract for usage - * errors via src/app.cpp's exact catch ladder. - * - Structural wiring of register_segment (arg/flag spec) via introspection. - * - The documented --json output shape (JsonWriter sequence reproduced from - * cmd_segment.cpp's print_result; see the note on that test). - * - * run_segment(), resolve_model_path() and print_result() live in an anonymous - * namespace in cmd_segment.cpp and are unreachable from here; anything that - * needs to *execute* them requires a SEGMENT backend and belongs in a separate - * backend-gated e2e test (cf. test_rcli_mlx_e2e.cpp). - * - * Uses the commons TestSuite harness so ctest drives it the same way as the - * other rcli suites (--run-all / --test-). - */ - -#include "test_common.h" - -#include -#include -#include -#include -#include -#include -#include - -#include - -#include "app.h" -#include "bootstrap.h" -#include "commands/commands.h" -#include "io/image_io.h" -#include "io/output.h" -#include "rac/features/segmentation/rac_segmentation_types.h" - -namespace { - -// Unique temp path (steady_clock stamp + a per-process counter), mirroring the -// uniqueness scheme in test_rcli_mlx_e2e.cpp. Does not create the file. -std::filesystem::path unique_temp_path(const std::string& stem, const std::string& ext) { - static int counter = 0; - const auto stamp = std::chrono::steady_clock::now().time_since_epoch().count(); - return std::filesystem::temp_directory_path() / - (stem + "-" + std::to_string(stamp) + "-" + std::to_string(counter++) + ext); -} - -// PPM/PNG payloads are binary — write raw bytes with no text translation. -bool write_binary_file(const std::filesystem::path& path, const std::string& bytes) { - std::ofstream out(path, std::ios::binary); - if (!out.is_open()) { - return false; - } - if (!bytes.empty()) { - out.write(bytes.data(), static_cast(bytes.size())); - } - return out.good(); -} - -// Remove a temp file on scope exit, even on an early return from a test. -struct TempFileGuard { - std::filesystem::path path; - explicit TempFileGuard(std::filesystem::path p) : path(std::move(p)) {} - ~TempFileGuard() { - std::error_code ec; - std::filesystem::remove(path, ec); - } - TempFileGuard(const TempFileGuard&) = delete; - TempFileGuard& operator=(const TempFileGuard&) = delete; -}; - -// ----------------------------------------------------------------------------- -// read_ppm(): the CLI's dependency-free image decoder and the `segment` input -// gate. Highest-value coverage available in the pure suite (public, pure, -// model-free). -// ----------------------------------------------------------------------------- -TestResult test_read_ppm() { - TestResult result; - result.test_name = "read_ppm"; - - // (a) Valid P6 round-trips width/height and the exact RGB bytes. - { - const unsigned char pixels[6] = {10, 20, 30, 40, 50, 60}; - std::string content = "P6\n2 1\n255\n"; - content.append(reinterpret_cast(pixels), sizeof(pixels)); - const auto path = unique_temp_path("rcli-seg-valid", ".ppm"); - TempFileGuard guard(path); - if (!write_binary_file(path, content)) { - result.details = "could not write valid PPM fixture"; - return result; - } - rcli::image::RgbImage img; - std::string err; - if (!rcli::image::read_ppm(path.string(), &img, &err)) { - result.details = "valid P6 was rejected: " + err; - return result; - } - if (img.width != 2 || img.height != 1 || img.rgb.size() != 6) { - result.expected = "2x1 with 6 rgb bytes"; - result.actual = std::to_string(img.width) + "x" + std::to_string(img.height) + " with " + - std::to_string(img.rgb.size()) + " bytes"; - return result; - } - for (size_t i = 0; i < sizeof(pixels); ++i) { - if (img.rgb[i] != pixels[i]) { - result.details = "RGB payload mismatch at byte " + std::to_string(i); - result.expected = std::to_string(pixels[i]); - result.actual = std::to_string(img.rgb[i]); - return result; - } - } - } - - // (g) '#'-comment lines in the header are skipped (next_ppm_uint comment - // branch) and parsing still succeeds. - { - const unsigned char pixels[6] = {1, 2, 3, 4, 5, 6}; - std::string content = "P6\n# rcli comment before dims\n2 1\n# and a trailing one\n255\n"; - content.append(reinterpret_cast(pixels), sizeof(pixels)); - const auto path = unique_temp_path("rcli-seg-comment", ".ppm"); - TempFileGuard guard(path); - if (!write_binary_file(path, content)) { - result.details = "could not write commented PPM fixture"; - return result; - } - rcli::image::RgbImage img; - std::string err; - if (!rcli::image::read_ppm(path.string(), &img, &err)) { - result.details = "commented P6 header was rejected: " + err; - return result; - } - if (img.width != 2 || img.height != 1 || img.rgb.size() != 6 || img.rgb[0] != 1 || - img.rgb[5] != 6) { - result.details = "comment-header parse produced the wrong image"; - return result; - } - } - - // (b)-(f) Malformed/format-violating inputs must fail with an actionable - // error. Non-empty payloads keep the P3 case a real (if mis-tagged) pixmap. - const std::string three(3, '\x01'); - const std::string six(6, '\x01'); - const std::string five(5, '\x01'); - struct Neg { - std::string content; - std::string want_substr; - std::string note; - }; - const Neg negatives[] = { - {"P3\n1 1\n255\n" + three, "not a binary PPM (P6)", "wrong magic P3"}, - {"", "not a binary PPM (P6)", "empty file"}, - {"P6\n64\n", "malformed PPM header", "header ends before all three ints"}, - {"P6\nWxH\n255\n", "malformed PPM header", "non-numeric dimension"}, - {"P6\n2 1\n254\n" + six, "unsupported PPM", "maxval != 255"}, - {"P6\n0 1\n255\n", "unsupported PPM", "zero width"}, - {"P6\n1 0\n255\n", "unsupported PPM", "zero height"}, - {"P6\n4 4\n255\n" + five, "truncated PPM pixel data", "payload shorter than header promises"}, - }; - for (const Neg& n : negatives) { - const auto path = unique_temp_path("rcli-seg-neg", ".ppm"); - TempFileGuard guard(path); - if (!write_binary_file(path, n.content)) { - result.details = "could not write negative fixture: " + n.note; - return result; - } - rcli::image::RgbImage img; - std::string err; - if (rcli::image::read_ppm(path.string(), &img, &err)) { - result.details = "expected failure but read_ppm succeeded: " + n.note; - return result; - } - if (err.find(n.want_substr) == std::string::npos) { - result.details = "wrong error for: " + n.note; - result.expected = "error containing \"" + n.want_substr + "\""; - result.actual = err; - return result; - } - } - - // (h) Nonexistent path → false with "cannot open" (file never created). - { - const std::string missing = unique_temp_path("rcli-seg-missing", ".ppm").string(); - rcli::image::RgbImage img; - std::string err; - if (rcli::image::read_ppm(missing, &img, &err)) { - result.details = "read_ppm succeeded on a nonexistent path"; - return result; - } - if (err.find("cannot open") == std::string::npos) { - result.expected = "error containing \"cannot open\""; - result.actual = err; - return result; - } - } - - result.passed = true; - return result; -} - -// ----------------------------------------------------------------------------- -// write_png(): public sibling encoder in io/image_io.h. Smoke + input guards. -// ----------------------------------------------------------------------------- -TestResult test_write_png_smoke() { - TestResult result; - result.test_name = "write_png_smoke"; - - const int width = 2; - const int height = 2; - std::vector rgba(static_cast(width) * height * 4); - for (size_t i = 0; i < rgba.size(); ++i) { - rgba[i] = static_cast(i * 7 + 3); - } - - const auto path = unique_temp_path("rcli-seg-png", ".png"); - TempFileGuard guard(path); - std::string err; - if (!rcli::image::write_png(path.string(), rgba.data(), width, height, &err)) { - result.details = "write_png failed on a valid RGBA buffer: " + err; - return result; - } - - std::ifstream in(path, std::ios::binary); - if (!in.is_open()) { - result.details = "written PNG could not be reopened"; - return result; - } - unsigned char header[8] = {0}; - in.read(reinterpret_cast(header), static_cast(sizeof(header))); - if (in.gcount() != static_cast(sizeof(header))) { - result.details = "PNG shorter than its 8-byte signature"; - return result; - } - const unsigned char signature[8] = {137, 80, 78, 71, 13, 10, 26, 10}; - for (size_t i = 0; i < sizeof(signature); ++i) { - if (header[i] != signature[i]) { - result.details = "PNG signature mismatch at byte " + std::to_string(i); - result.expected = std::to_string(signature[i]); - result.actual = std::to_string(header[i]); - return result; - } - } - - // width <= 0 and null data are rejected before any file is opened. - const std::string throwaway = unique_temp_path("rcli-seg-png-bad", ".png").string(); - std::string err_dim; - if (rcli::image::write_png(throwaway, rgba.data(), 0, height, &err_dim) || - err_dim.find("invalid image dimensions or data") == std::string::npos) { - result.details = "width<=0 must fail with 'invalid image dimensions or data'"; - result.actual = err_dim; - return result; - } - std::string err_null; - if (rcli::image::write_png(throwaway, nullptr, width, height, &err_null) || - err_null.find("invalid image dimensions or data") == std::string::npos) { - result.details = "null data must fail with 'invalid image dimensions or data'"; - result.actual = err_null; - return result; - } - - result.passed = true; - return result; -} - -// ----------------------------------------------------------------------------- -// Exit-code contract for `segment` usage errors. -// -// Builds the real app via rcli::configure_app (which adds the global flags AND -// every subcommand, including segment) and maps the thrown CLI11 exception to a -// process exit code using the EXACT catch ladder from src/app.cpp: -// RuntimeError -> get_exit_code() or 1 -// ParseError -> app.exit(e) (usage to stderr), then 2 -// std::exception -> 1 -// No process is exec'd/forked — the pure suite never redirects streams. -// -// All cases below throw during parse (RequiredError / ValidationError / -// ExtrasError, all ParseError subclasses) BEFORE the top-level run_callback() -// fires the segment callback, so run_segment/bootstrap/backend are never -// entered. -// ----------------------------------------------------------------------------- -int segment_exit_code(const std::vector& args) { - rcli::GlobalOptions options; - CLI::App app{"RunAnywhere on-device AI CLI — run, manage and serve local models"}; - rcli::configure_app(app, options); - - std::vector mutable_args = args; - std::vector argv; - argv.reserve(mutable_args.size()); - for (std::string& arg : mutable_args) { - argv.push_back(arg.data()); - } - - try { - app.parse(static_cast(argv.size()), argv.data()); - return 0; - } catch (const CLI::RuntimeError& e) { - return (e.get_exit_code() != 0) ? e.get_exit_code() : 1; - } catch (const CLI::ParseError& e) { - app.exit(e); // prints the usage message to stderr, mirroring src/app.cpp - return 2; - } catch (const std::exception&) { - return 1; - } -} - -TestResult test_segment_usage_errors() { - TestResult result; - result.test_name = "segment_usage_errors"; - - // A real existing PPM isolates the "missing --model" and "extra flag" cases - // from the positional's ->check(CLI::ExistingFile) validation. - const unsigned char pixels[3] = {9, 9, 9}; - std::string ppm_content = "P6\n1 1\n255\n"; - ppm_content.append(reinterpret_cast(pixels), sizeof(pixels)); - const auto ppm = unique_temp_path("rcli-seg-usage", ".ppm"); - TempFileGuard guard(ppm); - if (!write_binary_file(ppm, ppm_content)) { - result.details = "could not write usage-test PPM fixture"; - return result; - } - - const std::string existing = ppm.string(); - const std::string missing = unique_temp_path("rcli-seg-usage-missing", ".ppm").string(); - - struct Case { - std::vector args; - int expected; - std::string note; - }; - const Case cases[] = { - {{"rcli", "segment"}, 2, "image + --model both missing (RequiredError -> 2)"}, - {{"rcli", "segment", "--model", "seg-model"}, 2, "image missing (RequiredError -> 2)"}, - {{"rcli", "segment", existing}, 2, "--model missing (RequiredError -> 2)"}, - {{"rcli", "segment", missing, "--model", "seg-model"}, 2, - "image not on disk (ExistingFile -> ValidationError -> 2)"}, - {{"rcli", "segment", existing, "--model", "seg-model", "--bogus"}, 2, - "unknown flag (ExtrasError -> 2)"}, - }; - for (const Case& c : cases) { - const int code = segment_exit_code(c.args); - if (code != c.expected) { - result.details = "wrong exit code for: " + c.note; - result.expected = std::to_string(c.expected); - result.actual = std::to_string(code); - return result; - } - } - - result.passed = true; - return result; -} - -// ----------------------------------------------------------------------------- -// Structural wiring of register_segment — positive spec assertion with ZERO -// callback execution (the only way to verify the happy-path option spec without -// a segmentation model). Uses CLI11 introspection on the registered subcommand. -// ----------------------------------------------------------------------------- -TestResult test_segment_option_spec() { - TestResult result; - result.test_name = "segment_option_spec"; - - rcli::GlobalOptions options; - CLI::App app{"RunAnywhere on-device AI CLI — run, manage and serve local models"}; - rcli::configure_app(app, options); - - CLI::App* seg = app.get_subcommand_no_throw("segment"); - if (seg == nullptr) { - result.details = "segment subcommand not registered by configure_app"; - return result; - } - const std::string want_desc = "Label every pixel of an image by class"; - if (seg->get_description() != want_desc) { - result.expected = want_desc; - result.actual = seg->get_description(); - return result; - } - - CLI::Option* image = seg->get_option_no_throw("image"); - if (image == nullptr) { - result.details = "positional 'image' option missing"; - return result; - } - if (!image->get_required()) { - result.details = "'image' positional must be required"; - return result; - } - - CLI::Option* model_long = seg->get_option_no_throw("--model"); - if (model_long == nullptr) { - result.details = "'--model' option missing"; - return result; - } - if (!model_long->get_required()) { - result.details = "'--model' must be required"; - return result; - } - - CLI::Option* model_short = seg->get_option_no_throw("-m"); - if (model_short != model_long) { - result.details = "'-m' must resolve to the same Option as '--model'"; - return result; - } - - result.passed = true; - return result; -} - -// ----------------------------------------------------------------------------- -// --json output-shape guard (mirrors test_json_writer_shape). -// -// print_result() lives in an anonymous namespace in cmd_segment.cpp and cannot -// be called here, so this reproduces the EXACT JsonWriter sequence it emits for -// the --json branch and asserts the serialized string. It is fed real -// rac_segmentation_result_t / rac_segmentation_class_summary_t structs so the -// test stays coupled to the actual field names/types. -// -// NOTE: this LOCKS the documented --json contract (one flat JSON document) but -// does NOT execute print_result(). Genuine coverage of print_result / -// --no-progress / the human table + "(no classes)" + verbose "(N ms)" rendering -// requires driving run_segment(), which needs a SEGMENT backend and belongs in -// a separate backend-gated e2e test. -// ----------------------------------------------------------------------------- -TestResult test_segment_json_shape() { - TestResult result; - result.test_name = "segment_json_shape"; - - char model_id_buf[] = "segformer-b0-ade20k"; - char label_background[] = "background"; - char label_person[] = "person"; - - rac_segmentation_class_summary_t classes[2] = {}; - classes[0].class_id = 0; - classes[0].pixel_count = 200000; - classes[0].fraction = 0.75f; // exactly representable -> %g emits "0.75" - classes[0].label = label_background; - classes[1].class_id = 15; - classes[1].pixel_count = 67000; - classes[1].fraction = 0.25f; // exactly representable -> %g emits "0.25" - classes[1].label = label_person; - - rac_segmentation_result_t seg_result = {}; - seg_result.width = 640; - seg_result.height = 480; - seg_result.class_summaries = classes; - seg_result.class_summary_count = 2; - seg_result.processing_time_ms = 12; - seg_result.model_id = model_id_buf; - // Not freed: every pointer above is stack/literal, not malloc-owned. - - const std::string model_ref = "unused-fallback-ref"; // model_id is non-null - - // Reproduced verbatim from cmd_segment.cpp print_result()'s --json branch. - rcli::out::JsonWriter json; - json.begin_object() - .field("model", seg_result.model_id ? seg_result.model_id : model_ref) - .field("width", static_cast(seg_result.width)) - .field("height", static_cast(seg_result.height)) - .field("class_count", static_cast(seg_result.class_summary_count)) - .field("processing_time_ms", static_cast(seg_result.processing_time_ms)); - json.begin_array("classes"); - for (size_t i = 0; i < seg_result.class_summary_count; ++i) { - const rac_segmentation_class_summary_t& cls = seg_result.class_summaries[i]; - json.begin_array_object() - .field("class_id", static_cast(cls.class_id)) - .field("label", cls.label ? cls.label : "") - .field("pixel_count", static_cast(cls.pixel_count)) - .field("fraction", static_cast(cls.fraction)) - .end_object(); - } - json.end_array().end_object(); - - const std::string expected = - R"({"model":"segformer-b0-ade20k","width":640,"height":480,"class_count":2,)" - R"("processing_time_ms":12,"classes":[)" - R"({"class_id":0,"label":"background","pixel_count":200000,"fraction":0.75},)" - R"({"class_id":15,"label":"person","pixel_count":67000,"fraction":0.25}]})"; - if (json.str() != expected) { - result.expected = expected; - result.actual = json.str(); - return result; - } - - result.passed = true; - return result; -} - -} // namespace - -int main(int argc, char** argv) { - TestSuite suite("rcli_segment"); - suite.add("read_ppm", test_read_ppm); - suite.add("write_png_smoke", test_write_png_smoke); - suite.add("segment_usage_errors", test_segment_usage_errors); - suite.add("segment_option_spec", test_segment_option_spec); - suite.add("segment_json_shape", test_segment_json_shape); - return suite.run(argc, argv); -} diff --git a/rcli/tests/test_rcli_telemetry_live.cpp b/rcli/tests/test_rcli_telemetry_live.cpp deleted file mode 100644 index 035445a6f1..0000000000 --- a/rcli/tests/test_rcli_telemetry_live.cpp +++ /dev/null @@ -1,294 +0,0 @@ -/** - * @file test_rcli_telemetry_live.cpp - * @brief Live telemetry integration test — sends real, authenticated per-modality - * telemetry to the configured backend and asserts each is accepted (2xx). - * - * This complements the hermetic commons unit test (test_telemetry_extraction), - * which validates JSON shape offline against a mock sink. Here we exercise the - * full wire path: rcli bootstrap() registers the desktop adapter + HTTP - * transport and authenticates (API key -> device register -> JWT); we then - * override the process telemetry manager's HTTP callback with a status-recording - * POST (the same recipe as bootstrap's rcli_telemetry_http_callback) so we can - * assert the backend's response. A strict-schema rejection (422 extra_forbidden) - * fails the test — catching field drift against the real V2 endpoints. - * - * Opt-in: runs ONLY when invoked with `--live` AND the creds are in the - * environment (RUNANYWHERE_BASE_URL + RUNANYWHERE_API_KEY, optional - * RUNANYWHERE_ENVIRONMENT). Without those it prints a skip and exits 0, so it is - * safe to leave registered in ctest / CI (which run it with no args). - * - * RUNANYWHERE_BASE_URL=... RUNANYWHERE_API_KEY=... \ - * ./test_rcli_telemetry_live --live - */ - -#include -#include -#include -#include -#include - -#include "bootstrap.h" - -#include "rac/core/rac_sdk_state.h" -#include "rac/infrastructure/http/rac_http_client.h" -#include "rac/infrastructure/http/rac_http_transport.h" -#include "rac/infrastructure/network/rac_auth_manager.h" -#include "rac/infrastructure/network/rac_endpoints.h" -#include "rac/infrastructure/network/rac_environment.h" -#include "rac/infrastructure/telemetry/rac_telemetry_manager.h" - -#if defined(RAC_HAVE_PROTOBUF) -#include "sdk_events.pb.h" -namespace v1 = runanywhere::v1; -#endif - -static int g_checks = 0; -static int g_failures = 0; - -#define CHECK(cond, msg) \ - do { \ - ++g_checks; \ - if (!(cond)) { \ - ++g_failures; \ - std::fprintf(stderr, " FAIL: %s\n", (msg)); \ - } \ - } while (0) - -#if defined(RAC_HAVE_PROTOBUF) - -namespace { - -// Records the backend's response for the most recent flushed batch, then hands -// the result back to the manager. POST recipe mirrors bootstrap's -// rcli_telemetry_http_callback (all-public rac_http_* / rac_auth_* APIs). -struct LiveState { - rac_telemetry_manager_t* manager = nullptr; - bool called = false; - int status = 0; - bool ok = false; - std::string endpoint; - std::string body; -}; - -void live_post_cb(void* user_data, const char* endpoint, const char* json_body, size_t json_length, - rac_bool_t requires_auth) { - auto* st = static_cast(user_data); - st->called = true; - st->endpoint = endpoint != nullptr ? endpoint : ""; - st->status = 0; - st->ok = false; - st->body.clear(); - - const char* base_url = rac_state_get_base_url(); - if (base_url == nullptr || base_url[0] == '\0' || - rac_http_transport_is_registered() != RAC_TRUE) { - rac_telemetry_manager_http_complete(st->manager, RAC_FALSE, nullptr, "transport unavailable"); - return; - } - char url[2048] = {}; - if (rac_build_url(base_url, endpoint, url, sizeof(url)) < 0) { - rac_telemetry_manager_http_complete(st->manager, RAC_FALSE, nullptr, "url build failed"); - return; - } - - std::vector headers; - const rac_http_header_kv_t* defaults = nullptr; - size_t default_count = 0; - if (rac_http_default_headers(&defaults, &default_count) == RAC_SUCCESS && defaults != nullptr) { - headers.assign(defaults, defaults + default_count); - } - std::string auth_value; - if (requires_auth == RAC_TRUE) { - const char* token = rac_auth_get_access_token(); - if (token != nullptr && token[0] != '\0') { - auth_value = std::string("Bearer ") + token; - headers.push_back({"Authorization", auth_value.c_str()}); - } - } - - rac_http_client_t* client = nullptr; - if (rac_http_client_create(&client) != RAC_SUCCESS) { - rac_telemetry_manager_http_complete(st->manager, RAC_FALSE, nullptr, "client create failed"); - return; - } - rac_http_request_t request = {}; - request.method = "POST"; - request.url = url; - request.headers = headers.empty() ? nullptr : headers.data(); - request.header_count = headers.size(); - request.body_bytes = reinterpret_cast(json_body); - request.body_len = json_length; - request.timeout_ms = rac_env_default_http_timeout_ms(rac_state_get_environment()); - request.follow_redirects = RAC_FALSE; - - rac_http_response_t response = {}; - const rac_result_t rc = rac_http_request_send(client, &request, &response); - rac_http_client_destroy(client); - - st->status = response.status; - st->ok = rc == RAC_SUCCESS && response.status >= 200 && response.status < 300; - if (response.body_bytes != nullptr && response.body_len > 0) { - st->body.assign(reinterpret_cast(response.body_bytes), response.body_len); - } - rac_telemetry_manager_http_complete(st->manager, st->ok ? RAC_TRUE : RAC_FALSE, - st->body.empty() ? nullptr : st->body.c_str(), - st->ok ? nullptr : "POST failed"); - rac_http_response_free(&response); -} - -void envelope(v1::SDKEvent* ev, v1::SDKComponent component) { - ev->set_id("rcli-live-test"); - ev->set_timestamp_ms(1); - ev->set_component(component); - ev->set_source("cpp"); -} - -// Send one event and assert the backend accepted it (2xx). -void send_and_assert(rac_telemetry_manager_t* mgr, LiveState* st, const v1::SDKEvent& ev, - const char* label) { - st->called = false; - const std::string bytes = ev.SerializeAsString(); - rac_telemetry_manager_track_proto(mgr, reinterpret_cast(bytes.data()), - bytes.size()); - if (!st->called) { - // No completion flush fired (unexpected for a completion event). - ++g_checks; - ++g_failures; - std::fprintf(stderr, " FAIL: %s: no POST was made\n", label); - return; - } - if (!st->ok) { - std::fprintf(stderr, " %s: http=%d body=%s\n", label, st->status, - st->body.empty() ? "(empty)" : st->body.c_str()); - } else { - std::fprintf(stdout, " %s: accepted (http=%d, %s)\n", label, st->status, - st->endpoint.c_str()); - } - CHECK(st->ok, label); -} - -} // namespace - -#endif // RAC_HAVE_PROTOBUF - -int main(int argc, char** argv) { - std::fprintf(stdout, "test_rcli_telemetry_live\n"); - - bool live = false; - for (int i = 1; i < argc; ++i) { - if (std::strcmp(argv[i], "--live") == 0) { - live = true; - } - } - const char* base = std::getenv("RUNANYWHERE_BASE_URL"); - const char* key = std::getenv("RUNANYWHERE_API_KEY"); - const bool have_creds = base != nullptr && base[0] != '\0' && key != nullptr && key[0] != '\0'; - - if (!live || !have_creds) { - std::fprintf(stdout, - " skip: live telemetry test (needs --live and " - "RUNANYWHERE_BASE_URL + RUNANYWHERE_API_KEY)\n"); - return 0; - } - -#if !defined(RAC_HAVE_PROTOBUF) - std::fprintf(stdout, " skip: no protobuf\n"); - return 0; -#else - rcli::GlobalOptions opts; - opts.quiet = true; - rcli::Bootstrapped env; - const rac_result_t brc = rcli::bootstrap(opts, &env); - CHECK(brc == RAC_SUCCESS, "bootstrap succeeded"); - if (brc != RAC_SUCCESS) { - return 1; - } - - rac_telemetry_manager_t* mgr = rcli::active_telemetry_manager(); - CHECK(mgr != nullptr, "telemetry manager initialized (creds + auth)"); - if (mgr == nullptr) { - rcli::shutdown(); - return 1; - } - - LiveState state; - state.manager = mgr; - rac_telemetry_manager_set_http_callback(mgr, live_post_cb, &state); - - // LLM - { - v1::SDKEvent ev; - envelope(&ev, v1::SDK_COMPONENT_LLM); - auto* g = ev.mutable_generation(); - g->set_kind(v1::GENERATION_EVENT_KIND_COMPLETED); - g->set_model_id("rcli-live-test"); - g->set_input_tokens(10); - g->set_output_tokens(20); - g->set_tokens_per_second(40.0); - g->set_prefill_duration_ms(100); - send_and_assert(mgr, &state, ev, "llm"); - } - // Embeddings - { - v1::SDKEvent ev; - envelope(&ev, v1::SDK_COMPONENT_EMBEDDINGS); - (*ev.mutable_properties())["embedding_dimension"] = "384"; - (*ev.mutable_properties())["total_tokens"] = "8"; - (*ev.mutable_properties())["batch_size"] = "1"; - auto* c = ev.mutable_capability(); - c->set_kind(v1::CAPABILITY_OPERATION_EVENT_KIND_EMBEDDINGS_COMPLETED); - c->set_component(v1::SDK_COMPONENT_EMBEDDINGS); - c->set_model_id("rcli-live-test"); - c->set_input_count(1); - c->set_output_count(1); - send_and_assert(mgr, &state, ev, "embeddings"); - } - // RAG - { - v1::SDKEvent ev; - envelope(&ev, v1::SDK_COMPONENT_RAG); - (*ev.mutable_properties())["top_k"] = "5"; - (*ev.mutable_properties())["retrieval_time_ms"] = "1"; - (*ev.mutable_properties())["embedding_model"] = "rcli-live-test"; - (*ev.mutable_properties())["query_token_count"] = "10"; - (*ev.mutable_properties())["context_tokens"] = "49"; - auto* c = ev.mutable_capability(); - c->set_kind(v1::CAPABILITY_OPERATION_EVENT_KIND_RAG_QUERY_COMPLETED); - c->set_component(v1::SDK_COMPONENT_RAG); - c->set_model_id("rcli-live-test"); - c->set_output_count(2); - send_and_assert(mgr, &state, ev, "rag"); - } - // VLM - { - v1::SDKEvent ev; - envelope(&ev, v1::SDK_COMPONENT_VLM); - (*ev.mutable_properties())["total_tokens"] = "120"; - (*ev.mutable_properties())["tokens_per_second"] = "100.0"; - (*ev.mutable_properties())["prompt_eval_time_ms"] = "800"; - auto* c = ev.mutable_capability(); - c->set_kind(v1::CAPABILITY_OPERATION_EVENT_KIND_VLM_COMPLETED); - c->set_component(v1::SDK_COMPONENT_VLM); - c->set_model_id("rcli-live-test"); - c->set_input_count(1); - c->set_output_count(120); - send_and_assert(mgr, &state, ev, "vlm"); - } - // LoRA (failure path — rides the LLM component, modality overridden to lora) - { - v1::SDKEvent ev; - envelope(&ev, v1::SDK_COMPONENT_LLM); - (*ev.mutable_properties())["adapter_id"] = "rcli-live-test"; - (*ev.mutable_properties())["adapter_size_bytes"] = "4096"; - auto* c = ev.mutable_capability(); - c->set_kind(v1::CAPABILITY_OPERATION_EVENT_KIND_LORA_FAILED); - c->set_component(v1::SDK_COMPONENT_LLM); - c->set_model_id("rcli-live-test"); - send_and_assert(mgr, &state, ev, "lora"); - } - - rcli::shutdown(); - std::fprintf(stdout, " %d checks, %d failures\n", g_checks, g_failures); - return g_failures == 0 ? 0 : 1; -#endif // RAC_HAVE_PROTOBUF -} diff --git a/rcli/tests/test_rcli_unit.cpp b/rcli/tests/test_rcli_unit.cpp deleted file mode 100644 index 23840719c1..0000000000 --- a/rcli/tests/test_rcli_unit.cpp +++ /dev/null @@ -1,2149 +0,0 @@ -/** - * @file test_rcli_unit.cpp - * @brief rcli unit tests — pure helpers, no models, no network. - * - * Uses the commons TestSuite harness so the Docker rig and ctest drive every - * suite the same way (--run-all / --test-). - */ - -#include "test_common.h" - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "llm_options.pb.h" -#include "model_types.pb.h" -#include "vlm_options.pb.h" -#include "rac/core/rac_core.h" -#include "rac/foundation/rac_proto_buffer.h" -#include "rac/infrastructure/model_management/rac_model_registry.h" - -#include "app.h" -#include "catalog/catalog.h" -#include "catalog/model_ref.h" -#include "commands/bench_metrics.h" -#include "commands/engine_options.h" -#include "config/cli_paths.h" -#include "io/image_io.h" -#include "io/output.h" -#include "io/proto.h" - -namespace { - -// setenv/unsetenv helper that restores prior state on scope exit. -class EnvVar { -public: - EnvVar(const char *name, const char *value) : name_(name) { - if (const char *prev = std::getenv(name)) { - had_prev_ = true; - prev_ = prev; - } - if (value) { -#if defined(_WIN32) - _putenv_s(name, value); -#else - setenv(name, value, 1); -#endif - } else { -#if defined(_WIN32) - _putenv_s(name, ""); -#else - unsetenv(name); -#endif - } - } - ~EnvVar() { - if (had_prev_) { -#if defined(_WIN32) - _putenv_s(name_.c_str(), prev_.c_str()); -#else - setenv(name_.c_str(), prev_.c_str(), 1); -#endif - } else { -#if defined(_WIN32) - _putenv_s(name_.c_str(), ""); -#else - unsetenv(name_.c_str()); -#endif - } - } - -private: - std::string name_; - std::string prev_; - bool had_prev_ = false; -}; - -TestResult test_json_escape() { - TestResult result; - result.test_name = "json_escape"; - - struct Case { - std::string in; - std::string expected; - }; - const Case cases[] = { - {"plain", "plain"}, - {"quote\"backslash\\", "quote\\\"backslash\\\\"}, - {"line\nbreak\ttab", "line\\nbreak\\ttab"}, - {std::string("ctl\x01", 4), "ctl\\u0001"}, - }; - for (const Case &c : cases) { - const std::string actual = rcli::out::json_escape(c.in); - if (actual != c.expected) { - result.expected = c.expected; - result.actual = actual; - return result; - } - } - result.passed = true; - return result; -} - -TestResult test_json_writer_shape() { - TestResult result; - result.test_name = "json_writer_shape"; - - rcli::out::JsonWriter json; - json.begin_object() - .field("name", "qwen3-0.6b") - .field("size", static_cast(640)) - .field("downloaded", true); - json.begin_array("files"); - json.begin_array_object().field("path", "a.gguf").end_object(); - json.begin_array_object().field("path", "b.gguf").end_object(); - json.end_array(); - json.begin_array("scores").value(1.0).value(0.5).end_array(); - json.end_object(); - - const std::string expected = - R"({"name":"qwen3-0.6b","size":640,"downloaded":true,)" - R"("files":[{"path":"a.gguf"},{"path":"b.gguf"}],)" - R"("scores":[1,0.5]})"; - if (json.str() != expected) { - result.expected = expected; - result.actual = json.str(); - return result; - } - result.passed = true; - return result; -} - -TestResult test_human_bytes() { - TestResult result; - result.test_name = "human_bytes"; - - struct Case { - uint64_t in; - std::string expected; - }; - const Case cases[] = { - {512, "512 B"}, - {2048, "2.0 KB"}, - {640ull * 1024 * 1024, "640.0 MB"}, - {3ull * 1024 * 1024 * 1024, "3.0 GB"}, - }; - for (const Case &c : cases) { - const std::string actual = rcli::out::human_bytes(c.in); - if (actual != c.expected) { - result.expected = c.expected; - result.actual = actual; - return result; - } - } - result.passed = true; - return result; -} - -TestResult test_normalize_dir() { - TestResult result; - result.test_name = "normalize_dir"; - - if (rcli::paths::normalize_dir("/a/b/") != "/a/b" || - rcli::paths::normalize_dir("/a/b///") != "/a/b" || - rcli::paths::normalize_dir("/") != "/" || - !rcli::paths::normalize_dir("").empty()) { - result.details = "trailing-slash handling broken"; - return result; - } - result.passed = true; - return result; -} - -TestResult test_resolve_home_precedence() { - TestResult result; - result.test_name = "resolve_home_precedence"; - - { - // Flag override wins over env. - EnvVar env("RUNANYWHERE_HOME", "/from-env/runanywhere"); - if (rcli::paths::resolve_home("/from-flag/runanywhere/") != - "/from-flag/runanywhere") { - result.details = "flag override should win and be normalized"; - return result; - } - if (rcli::paths::resolve_home("") != "/from-env/runanywhere") { - result.details = "env should win when no flag given"; - return result; - } - } - { - // Default: XDG data dir under runanywhere. - EnvVar env("RUNANYWHERE_HOME", nullptr); -#if defined(_WIN32) - EnvVar local("LOCALAPPDATA", "C:/rcli-local"); - const std::string home = rcli::paths::resolve_home(""); - if (home != "C:/rcli-local/RunAnywhere") { - result.details = "expected C:/rcli-local/RunAnywhere, got " + home; - return result; - } -#else - EnvVar xdg("XDG_DATA_HOME", "/xdg-data"); - const std::string home = rcli::paths::resolve_home(""); - if (home != "/xdg-data/runanywhere") { - result.details = "expected /xdg-data/runanywhere, got " + home; - return result; - } -#endif - } - result.passed = true; - return result; -} - -TestResult test_state_dir() { - TestResult result; - result.test_name = "state_dir"; - - EnvVar xdg("XDG_STATE_HOME", "/xdg-state"); - if (rcli::paths::state_dir() != "/xdg-state/runanywhere") { - result.details = "XDG_STATE_HOME not honored"; - return result; - } - result.passed = true; - return result; -} - -TestResult test_catalog_lookup() { - TestResult result; - result.test_name = "catalog_lookup"; - - size_t count = 0; - const rcli::catalog::CatalogEntry *entries = rcli::catalog::all(&count); - if (!entries || count < 10) { - result.details = "catalog unexpectedly small"; - return result; - } - - const rcli::catalog::CatalogEntry *by_id = rcli::catalog::find("qwen3-0.6b"); - const rcli::catalog::CatalogEntry *by_alias = rcli::catalog::find("qwen3"); - if (!by_id || by_id != by_alias) { - result.details = "alias lookup should resolve to the same entry"; - return result; - } - if (rcli::catalog::find("definitely-not-a-model") != nullptr) { - result.details = "unknown id should return nullptr"; - return result; - } - if (rcli::catalog::suggestions("qwen", 3).empty()) { - result.details = "expected suggestions for 'qwen'"; - return result; - } - - // Multi-file entries (VLM pairs, embeddings) must carry ≥2 required files. - const rcli::catalog::CatalogEntry *vlm = rcli::catalog::find("smolvlm2"); - if (!vlm || vlm->files == nullptr || vlm->file_count != 2) { - result.details = "smolvlm2 should be a two-file artifact"; - return result; - } - - const rcli::catalog::CatalogEntry *mlx_llm = rcli::catalog::find("mlx-qwen3"); - if (!mlx_llm || - mlx_llm->framework != runanywhere::v1::INFERENCE_FRAMEWORK_MLX || - mlx_llm->format != runanywhere::v1::MODEL_FORMAT_SAFETENSORS || - mlx_llm->category != runanywhere::v1::MODEL_CATEGORY_LANGUAGE || - mlx_llm->files == nullptr || mlx_llm->file_count != 9 || - !mlx_llm->supports_thinking) { - result.details = "mlx-qwen3 should be a complete MLX language bundle"; - return result; - } - - const rcli::catalog::CatalogEntry *maple_gguf = - rcli::catalog::find("maple-preview"); - if (!maple_gguf || - maple_gguf->framework != runanywhere::v1::INFERENCE_FRAMEWORK_LLAMA_CPP || - maple_gguf->format != runanywhere::v1::MODEL_FORMAT_GGUF || - maple_gguf->download_size_bytes != 4984016416LL || - maple_gguf->context_length != 4096 || !maple_gguf->supports_thinking) { - result.details = "maple-preview should resolve to the pinned GGUF bundle"; - return result; - } - - const rcli::catalog::CatalogEntry *mlx_maple = - rcli::catalog::find("mlx-maple-preview"); - if (!mlx_maple || - mlx_maple->framework != runanywhere::v1::INFERENCE_FRAMEWORK_MLX || - mlx_maple->format != runanywhere::v1::MODEL_FORMAT_SAFETENSORS || - mlx_maple->category != runanywhere::v1::MODEL_CATEGORY_LANGUAGE || - mlx_maple->files == nullptr || mlx_maple->file_count != 13 || - mlx_maple->download_size_bytes != 5330252282LL || - mlx_maple->context_length != 128000) { - result.details = "mlx-maple-preview should be a complete pinned MLX bundle"; - return result; - } - int64_t mlx_maple_file_total = 0; - for (size_t i = 0; i < mlx_maple->file_count; ++i) { - const rcli::catalog::CatalogFile &file = mlx_maple->files[i]; - if (file.size_bytes <= 0 || - std::string(file.url).find( - "/resolve/d0a7314d6bf14c880201b599d7a701cfbc8717e6/") == - std::string::npos) { - result.details = "mlx-maple-preview files must use the pinned revision"; - return result; - } - mlx_maple_file_total += file.size_bytes; - } - if (mlx_maple_file_total != mlx_maple->download_size_bytes) { - result.details = "mlx-maple-preview file sizes must sum to the bundle size"; - return result; - } - - const rcli::catalog::CatalogEntry *mlx_vlm = - rcli::catalog::find("mlx-qwen2-vl"); - if (!mlx_vlm || - mlx_vlm->category != runanywhere::v1::MODEL_CATEGORY_MULTIMODAL || - mlx_vlm->framework != runanywhere::v1::INFERENCE_FRAMEWORK_MLX || - mlx_vlm->files == nullptr || mlx_vlm->file_count != 11) { - result.details = "mlx-qwen2-vl should be a complete MLX VLM bundle"; - return result; - } - bool has_preprocessor = false; - for (size_t i = 0; i < mlx_vlm->file_count; ++i) { - has_preprocessor = - has_preprocessor || - std::string(mlx_vlm->files[i].filename) == "preprocessor_config.json"; - } - if (!has_preprocessor) { - result.details = - "MLX VLM catalog entry must include preprocessor_config.json"; - return result; - } - - const rcli::catalog::CatalogEntry *mlx_fastvlm = - rcli::catalog::find("mlx-fastvlm"); - if (!mlx_fastvlm || - mlx_fastvlm->category != runanywhere::v1::MODEL_CATEGORY_MULTIMODAL || - mlx_fastvlm->framework != runanywhere::v1::INFERENCE_FRAMEWORK_MLX || - mlx_fastvlm->files == nullptr || mlx_fastvlm->file_count != 14) { - result.details = "mlx-fastvlm should be a complete MLX VLM bundle"; - return result; - } - bool has_processor_config = false; - bool has_fastvlm_companion = false; - for (size_t i = 0; i < mlx_fastvlm->file_count; ++i) { - const std::string filename = mlx_fastvlm->files[i].filename; - has_processor_config = - has_processor_config || filename == "processor_config.json"; - has_fastvlm_companion = - has_fastvlm_companion || - (!mlx_fastvlm->files[i].required && - (filename == "processing_fastvlm.py" || filename == "llava_qwen.py")); - } - if (!has_processor_config || !has_fastvlm_companion) { - result.details = "MLX FastVLM catalog entry must include processor config " - "and companions"; - return result; - } - - const rcli::catalog::CatalogEntry *mlx_embed = - rcli::catalog::find("mlx-qwen3-embed"); - if (!mlx_embed || - mlx_embed->category != runanywhere::v1::MODEL_CATEGORY_EMBEDDING || - mlx_embed->framework != runanywhere::v1::INFERENCE_FRAMEWORK_MLX || - mlx_embed->files == nullptr || mlx_embed->file_count != 11) { - result.details = - "mlx-qwen3-embed should be a complete MLX embedding bundle"; - return result; - } - - struct PortableNvidiaEmbeddingCase { - const char *id; - const char *alias; - const char *revision; - int64_t download_size_bytes; - }; - const PortableNvidiaEmbeddingCase portable_nvidia_embeddings[] = { - {"nemotron-3-embed-1b-q4_k_m", "nemotron-3-embed", - "06df1fde6f7009c91f6cc3cd520081921929a678", 749352096LL}, - {"llama-nemotron-embed-1b-v2-q4_k_m", "llama-nemotron-embed", - "bf7c9832b1d76f86777379e58b7b74805ee58006", 807690624LL}, - {"llama-embed-nemotron-8b-q4_k_m", "llama-embed-nemotron", - "e7ae3cbae4f7693bbd75ec959bf293f39e1f2e25", 4625233184LL}, - }; - for (const PortableNvidiaEmbeddingCase &test_case : - portable_nvidia_embeddings) { - const rcli::catalog::CatalogEntry *entry = - rcli::catalog::find(test_case.id); - if (!entry || entry != rcli::catalog::find(test_case.alias) || - entry->category != runanywhere::v1::MODEL_CATEGORY_EMBEDDING || - entry->framework != runanywhere::v1::INFERENCE_FRAMEWORK_LLAMA_CPP || - entry->format != runanywhere::v1::MODEL_FORMAT_GGUF || - entry->files != nullptr || entry->url == nullptr || - entry->download_size_bytes != test_case.download_size_bytes || - std::string(entry->url).find(test_case.revision) == std::string::npos) { - result.details = std::string(test_case.id) + - " should be an exact pinned llama.cpp embedding"; - return result; - } - } - - const rcli::catalog::CatalogEntry *nemotron_nano = - rcli::catalog::find("mlx-nemotron-nano"); - if (!nemotron_nano || - nemotron_nano->category != runanywhere::v1::MODEL_CATEGORY_LANGUAGE || - nemotron_nano->framework != runanywhere::v1::INFERENCE_FRAMEWORK_MLX || - nemotron_nano->files == nullptr || nemotron_nano->file_count != 8 || - nemotron_nano->download_size_bytes != 4534806075LL || - nemotron_nano->context_length != 131072) { - result.details = "mlx-nemotron-nano should be a complete pinned MLX bundle"; - return result; - } - - const rcli::catalog::CatalogEntry *nemotron_mini = - rcli::catalog::find("mlx-nemotron-mini"); - if (!nemotron_mini || - nemotron_mini->category != runanywhere::v1::MODEL_CATEGORY_LANGUAGE || - nemotron_mini->framework != runanywhere::v1::INFERENCE_FRAMEWORK_MLX || - nemotron_mini->format != runanywhere::v1::MODEL_FORMAT_SAFETENSORS || - nemotron_mini->files == nullptr || nemotron_mini->file_count != 6 || - nemotron_mini->download_size_bytes != 2392679103LL || - nemotron_mini->context_length != 4096) { - result.details = "mlx-nemotron-mini should be a complete pinned MLX bundle"; - return result; - } - for (size_t i = 0; i < nemotron_mini->file_count; ++i) { - if (std::string(nemotron_mini->files[i].url) - .find("/resolve/b5784198153d2d71afcc97d4cc38c049abced8cd/") == - std::string::npos) { - result.details = "mlx-nemotron-mini files must use the pinned revision"; - return result; - } - } - - struct NvidiaSpeechCase { - const char *alias; - int64_t download_size_bytes; - }; - const NvidiaSpeechCase nvidia_speech_cases[] = { - {"mlx-parakeet-ctc", 4250718357LL}, - {"mlx-parakeet-tdt-v2", 2471596080LL}, - {"mlx-parakeet-tdt-v3", 2508532829LL}, - {"mlx-parakeet-rnnt", 4282283914LL}, - {"mlx-nemotron-asr", 755758528LL}, - }; - for (const NvidiaSpeechCase &test_case : nvidia_speech_cases) { - const rcli::catalog::CatalogEntry *entry = - rcli::catalog::find(test_case.alias); - if (!entry || - entry->category != runanywhere::v1::MODEL_CATEGORY_SPEECH_RECOGNITION || - entry->framework != runanywhere::v1::INFERENCE_FRAMEWORK_MLX || - entry->format != runanywhere::v1::MODEL_FORMAT_SAFETENSORS || - entry->files == nullptr || entry->file_count != 2 || - entry->download_size_bytes != test_case.download_size_bytes || - std::string(entry->files[0].url).find("/resolve/") == - std::string::npos) { - result.details = std::string(test_case.alias) + - " should be a complete pinned MLX speech bundle"; - return result; - } - } - - result.passed = true; - return result; -} - -TestResult test_nvidia_sherpa_catalog() { - TestResult result; - result.test_name = "nvidia_sherpa_catalog"; - - struct ExpectedFile { - const char *filename; - int64_t size_bytes; - }; - constexpr ExpectedFile parakeet_v2_files[] = { - {"encoder.int8.onnx", 652184296LL}, - {"decoder.int8.onnx", 7257753LL}, - {"joiner.int8.onnx", 1739080LL}, - {"tokens.txt", 9384LL}, - }; - constexpr ExpectedFile parakeet_v3_files[] = { - {"encoder.int8.onnx", 652184281LL}, - {"decoder.int8.onnx", 11845275LL}, - {"joiner.int8.onnx", 6355277LL}, - {"tokens.txt", 93939LL}, - }; - constexpr ExpectedFile canary_files[] = { - {"encoder.int8.onnx", 132678643LL}, - {"decoder.int8.onnx", 74437848LL}, - {"tokens.txt", 53555LL}, - }; - - struct Case { - const char *id; - const char *alias; - const char *repo; - const char *revision; - const ExpectedFile *files; - size_t file_count; - int64_t total_size_bytes; - }; - const Case cases[] = { - {"sherpa-nemo-parakeet-tdt-0.6b-v2-int8", "parakeet-tdt-v2", - "csukuangfj/sherpa-onnx-nemo-parakeet-tdt-0.6b-v2-int8", - "1ab9323565ddb038682214b292f588070a538ce2", parakeet_v2_files, 4, - 661190513LL}, - {"sherpa-nemo-parakeet-tdt-0.6b-v3-int8", "parakeet-tdt-v3", - "csukuangfj/sherpa-onnx-nemo-parakeet-tdt-0.6b-v3-int8", - "2bda32ec70b097a55adaa07d9a7173915b43cc78", parakeet_v3_files, 4, - 670478772LL}, - {"sherpa-nemo-canary-180m-flash-int8", "canary-180m", - "csukuangfj/sherpa-onnx-nemo-canary-180m-flash-en-es-de-fr-int8", - "9077164e0d3dd1d5353743e89ceaa1d3a770838c", canary_files, 3, - 207170046LL}, - }; - - for (const Case &test_case : cases) { - const rcli::catalog::CatalogEntry *entry = - rcli::catalog::find(test_case.id); - if (!entry || entry != rcli::catalog::find(test_case.alias)) { - result.details = - std::string(test_case.id) + " should resolve by exact id and alias"; - return result; - } - if (entry->category != runanywhere::v1::MODEL_CATEGORY_SPEECH_RECOGNITION || - entry->framework != runanywhere::v1::INFERENCE_FRAMEWORK_SHERPA || - entry->format != runanywhere::v1::MODEL_FORMAT_ONNX || - entry->url != nullptr || entry->files == nullptr || - entry->file_count != test_case.file_count || - entry->download_size_bytes != test_case.total_size_bytes) { - result.details = std::string(test_case.id) + - " should be an exact Sherpa-ONNX STT bundle"; - return result; - } - - const std::string base_url = std::string("https://huggingface.co/") + - test_case.repo + "/resolve/" + - test_case.revision + "/"; - int64_t manifest_total = 0; - for (size_t i = 0; i < test_case.file_count; ++i) { - const rcli::catalog::CatalogFile &actual = entry->files[i]; - const ExpectedFile &expected = test_case.files[i]; - const std::string expected_url = base_url + expected.filename; - if (actual.url == nullptr || actual.filename == nullptr || - std::string(actual.url) != expected_url || - std::string(actual.filename) != expected.filename || - !actual.required || actual.size_bytes != expected.size_bytes) { - result.details = std::string(test_case.id) + - " has a mismatched pinned file manifest at index " + - std::to_string(i); - return result; - } - manifest_total += actual.size_bytes; - } - if (manifest_total != test_case.total_size_bytes) { - result.details = std::string(test_case.id) + - " per-file sizes should sum to the exact bundle total"; - return result; - } - } - - const rcli::catalog::CatalogEntry *parakeet_ctc = - rcli::catalog::find("sherpa-nemo-parakeet-ctc-1.1b-int8"); - if (!parakeet_ctc || parakeet_ctc != rcli::catalog::find("parakeet-ctc") || - parakeet_ctc->category != - runanywhere::v1::MODEL_CATEGORY_SPEECH_RECOGNITION || - parakeet_ctc->framework != runanywhere::v1::INFERENCE_FRAMEWORK_SHERPA || - parakeet_ctc->format != runanywhere::v1::MODEL_FORMAT_ONNX || - parakeet_ctc->url != nullptr || parakeet_ctc->files == nullptr || - parakeet_ctc->file_count != 2 || - parakeet_ctc->download_size_bytes != 1110024519LL || - parakeet_ctc->memory_required_bytes != 2147483648LL) { - result.details = "Parakeet CTC should be an exact Sherpa-ONNX bundle"; - return result; - } - - const std::string base_url = - "https://huggingface.co/runanywhere/" - "sherpa-onnx-nemo-parakeet-ctc-1.1b-int8/resolve/" - "48a549f552774db3cd09dd1548f3d1a2b37bc7c5/"; - const rcli::catalog::CatalogFile &model = parakeet_ctc->files[0]; - const rcli::catalog::CatalogFile &tokens = parakeet_ctc->files[1]; - if (std::string(model.url) != base_url + "model.int8.onnx" || - std::string(model.filename) != "model.int8.onnx" || !model.required || - model.size_bytes != 1110014145LL || model.checksum_sha256 == nullptr || - std::string(model.checksum_sha256) != - "62f73c17a5301c048c7273cf24ef1cd0c3621d3625c5415fbafe5633d7bf2f98") { - result.details = "Parakeet CTC model descriptor is not exact"; - return result; - } - if (std::string(tokens.url) != base_url + "tokens.txt" || - std::string(tokens.filename) != "tokens.txt" || !tokens.required || - tokens.size_bytes != 10374LL || tokens.checksum_sha256 == nullptr || - std::string(tokens.checksum_sha256) != - "ed16e1a4e3a3aa379138c0b1888e5d49f993c9d512b2be4d46e90a87afd54921") { - result.details = "Parakeet CTC tokens descriptor is not exact"; - return result; - } - - result.passed = true; - return result; -} - -TestResult test_engine_hint_parsing() { - TestResult result; - result.test_name = "engine_hint_parsing"; - - struct Case { - std::string in; - runanywhere::v1::InferenceFramework expected; - }; - const Case cases[] = { - {"", runanywhere::v1::INFERENCE_FRAMEWORK_UNSPECIFIED}, - {"mlx", runanywhere::v1::INFERENCE_FRAMEWORK_MLX}, - {"llama.cpp", runanywhere::v1::INFERENCE_FRAMEWORK_LLAMA_CPP}, - {"llama-cpp", runanywhere::v1::INFERENCE_FRAMEWORK_LLAMA_CPP}, - {"onnx", runanywhere::v1::INFERENCE_FRAMEWORK_ONNX}, - {"sherpa", runanywhere::v1::INFERENCE_FRAMEWORK_SHERPA}, - }; - for (const Case &c : cases) { - runanywhere::v1::InferenceFramework actual = - runanywhere::v1::INFERENCE_FRAMEWORK_UNSPECIFIED; - std::string error; - if (!rcli::commands::parse_engine_hint(c.in, &actual, &error) || - actual != c.expected) { - result.expected = std::to_string(static_cast(c.expected)); - result.actual = std::to_string(static_cast(actual)); - result.details = "input: " + c.in + " error: " + error; - return result; - } - } - - runanywhere::v1::InferenceFramework actual = - runanywhere::v1::INFERENCE_FRAMEWORK_UNSPECIFIED; - std::string error; - if (rcli::commands::parse_engine_hint("banana", &actual, &error) || - error.find("unsupported engine") == std::string::npos) { - result.details = "unsupported engine should fail with an actionable error"; - return result; - } - - result.passed = true; - return result; -} - -void remove_registered_model(const std::string &id) { - if (auto *registry = rac_get_model_registry()) { - (void)rac_model_registry_remove_proto(registry, id.c_str()); - } -} - -class RegisteredModelCleanup { -public: - RegisteredModelCleanup(std::initializer_list ids) { - ids_.reserve(ids.size()); - for (const char *id : ids) { - ids_.emplace_back(id); - } - } - - ~RegisteredModelCleanup() { - for (const auto &id : ids_) { - remove_registered_model(id); - } - } - -private: - std::vector ids_; -}; - -bool get_registered_model(const std::string &id, - runanywhere::v1::ModelInfo *out, std::string *error) { - rac_proto_buffer_t found; - rac_proto_buffer_init(&found); - const rac_result_t rc = rac_model_registry_get_proto_buffer( - rac_get_model_registry(), id.c_str(), &found); - const bool parsed = rcli::proto::parse_proto_buffer(&found, out, error); - if (!parsed && error && error->empty()) { - *error = "registry get failed rc=" + std::to_string(rc); - } - return rc == RAC_SUCCESS && parsed; -} - -TestResult test_mlx_catalog_registration() { - TestResult result; - result.test_name = "mlx_catalog_registration"; - - const rac_result_t rc = rcli::catalog::register_all(); - if (rc != RAC_SUCCESS) { - result.details = "catalog registration failed rc=" + std::to_string(rc); - return result; - } - RegisteredModelCleanup cleanup({ - "mlx-qwen3-0.6b-4bit", - "mlx-maple-preview-2bit", - "mlx-llama-3.2-1b-instruct-4bit", - "mlx-qwen2-vl-2b-instruct-4bit", - "mlx-fastvlm-0.5b-bf16", - "mlx-qwen3-embedding-0.6b-4bit-dwq", - "mlx-qwen3-asr-0.6b-8bit", - "mlx-glm-asr-nano-2512-4bit", - "mlx-llama-3.1-nemotron-nano-8b-v1-4bit", - "mlx-nemotron-mini-4b-instruct-4bit", - "mlx-parakeet-ctc-1.1b", - "mlx-parakeet-tdt-0.6b-v2", - "mlx-parakeet-tdt-0.6b-v3", - "mlx-parakeet-rnnt-1.1b", - "mlx-nemotron-3.5-asr-streaming-0.6b-8bit", - "mlx-qwen3-tts-12hz-0.6b-base-8bit", - "mlx-soprano-1.1-80m-5bit", - "sherpa-nemo-parakeet-tdt-0.6b-v2-int8", - "sherpa-nemo-parakeet-tdt-0.6b-v3-int8", - "sherpa-nemo-parakeet-ctc-1.1b-int8", - "sherpa-nemo-canary-180m-flash-int8", - "sherpa-nemotron-3.5-asr-streaming-0.6b-320ms-int8", - }); - - runanywhere::v1::ModelInfo qwen; - std::string error; - if (!get_registered_model("mlx-qwen3-0.6b-4bit", &qwen, &error)) { - result.details = error; - return result; - } - if (qwen.framework() != runanywhere::v1::INFERENCE_FRAMEWORK_MLX || - qwen.format() != runanywhere::v1::MODEL_FORMAT_SAFETENSORS || - qwen.category() != runanywhere::v1::MODEL_CATEGORY_LANGUAGE || - !qwen.has_multi_file() || qwen.multi_file().files_size() != 9 || - qwen.download_size_bytes() != 351383618 || !qwen.supports_thinking()) { - result.details = "registered MLX Qwen3 metadata is incomplete"; - return result; - } - - runanywhere::v1::ModelInfo maple; - if (!get_registered_model("mlx-maple-preview-2bit", &maple, &error) || - maple.framework() != runanywhere::v1::INFERENCE_FRAMEWORK_MLX || - maple.format() != runanywhere::v1::MODEL_FORMAT_SAFETENSORS || - maple.category() != runanywhere::v1::MODEL_CATEGORY_LANGUAGE || - !maple.has_multi_file() || maple.multi_file().files_size() != 13 || - maple.download_size_bytes() != 5330252282LL || - maple.context_length() != 128000) { - result.details = "registered MLX Maple metadata is incomplete"; - return result; - } - - runanywhere::v1::ModelInfo vlm; - if (!get_registered_model("mlx-qwen2-vl-2b-instruct-4bit", &vlm, &error)) { - result.details = error; - return result; - } - bool preprocessor_registered = false; - for (const auto &file : vlm.multi_file().files()) { - preprocessor_registered = preprocessor_registered || - file.filename() == "preprocessor_config.json"; - } - if (vlm.category() != runanywhere::v1::MODEL_CATEGORY_MULTIMODAL || - vlm.framework() != runanywhere::v1::INFERENCE_FRAMEWORK_MLX || - !vlm.has_multi_file() || vlm.multi_file().files_size() != 11 || - !preprocessor_registered) { - result.details = "registered MLX VLM metadata is incomplete"; - return result; - } - - runanywhere::v1::ModelInfo fastvlm; - if (!get_registered_model("mlx-fastvlm-0.5b-bf16", &fastvlm, &error)) { - result.details = error; - return result; - } - bool processor_registered = false; - bool companion_registered = false; - for (const auto &file : fastvlm.multi_file().files()) { - processor_registered = - processor_registered || file.filename() == "processor_config.json"; - companion_registered = - companion_registered || - (file.is_optional() && (file.filename() == "processing_fastvlm.py" || - file.filename() == "llava_qwen.py")); - } - if (fastvlm.category() != runanywhere::v1::MODEL_CATEGORY_MULTIMODAL || - fastvlm.framework() != runanywhere::v1::INFERENCE_FRAMEWORK_MLX || - !fastvlm.has_multi_file() || fastvlm.multi_file().files_size() != 14 || - !processor_registered || !companion_registered) { - result.details = "registered MLX FastVLM metadata is incomplete"; - return result; - } - - runanywhere::v1::ModelInfo embedding; - if (!get_registered_model("mlx-qwen3-embedding-0.6b-4bit-dwq", &embedding, - &error)) { - result.details = error; - return result; - } - if (embedding.category() != runanywhere::v1::MODEL_CATEGORY_EMBEDDING || - embedding.framework() != runanywhere::v1::INFERENCE_FRAMEWORK_MLX || - embedding.format() != runanywhere::v1::MODEL_FORMAT_SAFETENSORS || - !embedding.has_multi_file() || - embedding.multi_file().files_size() != 11) { - result.details = "registered MLX embedding metadata is incomplete"; - return result; - } - - runanywhere::v1::ModelInfo qwen_asr; - if (!get_registered_model("mlx-qwen3-asr-0.6b-8bit", &qwen_asr, &error) || - qwen_asr.category() != - runanywhere::v1::MODEL_CATEGORY_SPEECH_RECOGNITION || - qwen_asr.framework() != runanywhere::v1::INFERENCE_FRAMEWORK_MLX || - !qwen_asr.has_multi_file() || qwen_asr.multi_file().files_size() != 9) { - result.details = "registered MLX Qwen3-ASR metadata is incomplete"; - return result; - } - - runanywhere::v1::ModelInfo glm_asr; - if (!get_registered_model("mlx-glm-asr-nano-2512-4bit", &glm_asr, &error) || - glm_asr.category() != - runanywhere::v1::MODEL_CATEGORY_SPEECH_RECOGNITION || - glm_asr.framework() != runanywhere::v1::INFERENCE_FRAMEWORK_MLX || - !glm_asr.has_multi_file() || glm_asr.multi_file().files_size() != 9) { - result.details = "registered MLX GLM-ASR metadata is incomplete"; - return result; - } - - struct RegisteredNvidiaCase { - const char *id; - int expected_files; - int64_t expected_size; - }; - const RegisteredNvidiaCase registered_nvidia_cases[] = { - {"mlx-llama-3.1-nemotron-nano-8b-v1-4bit", 8, 4534806075LL}, - {"mlx-nemotron-mini-4b-instruct-4bit", 6, 2392679103LL}, - {"mlx-parakeet-ctc-1.1b", 2, 4250718357LL}, - {"mlx-parakeet-tdt-0.6b-v2", 2, 2471596080LL}, - {"mlx-parakeet-tdt-0.6b-v3", 2, 2508532829LL}, - {"mlx-parakeet-rnnt-1.1b", 2, 4282283914LL}, - {"mlx-nemotron-3.5-asr-streaming-0.6b-8bit", 2, 755758528LL}, - }; - for (const RegisteredNvidiaCase &test_case : registered_nvidia_cases) { - runanywhere::v1::ModelInfo model; - if (!get_registered_model(test_case.id, &model, &error) || - model.framework() != runanywhere::v1::INFERENCE_FRAMEWORK_MLX || - model.format() != runanywhere::v1::MODEL_FORMAT_SAFETENSORS || - !model.has_multi_file() || - model.multi_file().files_size() != test_case.expected_files || - model.download_size_bytes() != test_case.expected_size) { - result.details = - std::string("registered NVIDIA MLX metadata is incomplete: ") + - test_case.id; - return result; - } - } - - struct RegisteredSherpaCase { - const char *id; - int expected_files; - int64_t expected_size; - }; - const RegisteredSherpaCase registered_sherpa_cases[] = { - {"sherpa-nemo-parakeet-tdt-0.6b-v2-int8", 4, 661190513LL}, - {"sherpa-nemo-parakeet-tdt-0.6b-v3-int8", 4, 670478772LL}, - {"sherpa-nemo-parakeet-ctc-1.1b-int8", 2, 1110024519LL}, - {"sherpa-nemo-canary-180m-flash-int8", 3, 207170046LL}, - {"sherpa-nemotron-3.5-asr-streaming-0.6b-320ms-int8", 4, - 682215471LL}, - }; - for (const RegisteredSherpaCase &test_case : registered_sherpa_cases) { - runanywhere::v1::ModelInfo model; - if (!get_registered_model(test_case.id, &model, &error) || - model.category() != - runanywhere::v1::MODEL_CATEGORY_SPEECH_RECOGNITION || - model.framework() != runanywhere::v1::INFERENCE_FRAMEWORK_SHERPA || - model.format() != runanywhere::v1::MODEL_FORMAT_ONNX || - !model.has_multi_file() || - model.multi_file().files_size() != test_case.expected_files || - model.download_size_bytes() != test_case.expected_size) { - result.details = - std::string("registered NVIDIA Sherpa metadata is incomplete: ") + - test_case.id; - return result; - } - int64_t registered_file_total = 0; - for (const auto &file : model.multi_file().files()) { - if (!file.has_size_bytes() || file.size_bytes() <= 0) { - result.details = - std::string("registered NVIDIA Sherpa file size is missing: ") + - test_case.id; - return result; - } - registered_file_total += file.size_bytes(); - } - if (registered_file_total != test_case.expected_size) { - result.details = - std::string("registered NVIDIA Sherpa file sizes do not sum: ") + - test_case.id; - return result; - } - } - - runanywhere::v1::ModelInfo parakeet_ctc; - if (!get_registered_model("sherpa-nemo-parakeet-ctc-1.1b-int8", ¶keet_ctc, - &error)) { - result.details = "registered Parakeet CTC metadata is missing: " + error; - return result; - } - const runanywhere::v1::ModelFileDescriptor *registered_model = nullptr; - const runanywhere::v1::ModelFileDescriptor *registered_tokens = nullptr; - for (const auto &file : parakeet_ctc.multi_file().files()) { - if (file.filename() == "model.int8.onnx") { - registered_model = &file; - } else if (file.filename() == "tokens.txt") { - registered_tokens = &file; - } - } - if (registered_model == nullptr || registered_tokens == nullptr || - registered_model->url() != - "https://huggingface.co/runanywhere/" - "sherpa-onnx-nemo-parakeet-ctc-1.1b-int8/resolve/" - "48a549f552774db3cd09dd1548f3d1a2b37bc7c5/model.int8.onnx" || - !registered_model->has_size_bytes() || - registered_model->size_bytes() != 1110014145LL || - !registered_model->has_checksum_sha256() || - registered_model->checksum_sha256() != - "62f73c17a5301c048c7273cf24ef1cd0c3621d3625c5415fbafe5633d7bf2f98" || - !parakeet_ctc.has_memory_required_bytes() || - parakeet_ctc.memory_required_bytes() != 2147483648LL) { - result.details = - "registered Parakeet CTC model descriptor tuple is incomplete"; - return result; - } - if (registered_tokens->url() != - "https://huggingface.co/runanywhere/" - "sherpa-onnx-nemo-parakeet-ctc-1.1b-int8/resolve/" - "48a549f552774db3cd09dd1548f3d1a2b37bc7c5/tokens.txt" || - !registered_tokens->has_size_bytes() || - registered_tokens->size_bytes() != 10374LL || - !registered_tokens->has_checksum_sha256() || - registered_tokens->checksum_sha256() != - "ed16e1a4e3a3aa379138c0b1888e5d49f993c9d512b2be4d46e90a87afd54921") { - result.details = - "registered Parakeet CTC tokens descriptor tuple is incomplete"; - return result; - } - - runanywhere::v1::ModelInfo qwen_tts; - if (!get_registered_model("mlx-qwen3-tts-12hz-0.6b-base-8bit", &qwen_tts, - &error) || - qwen_tts.category() != runanywhere::v1::MODEL_CATEGORY_SPEECH_SYNTHESIS || - qwen_tts.framework() != runanywhere::v1::INFERENCE_FRAMEWORK_MLX || - !qwen_tts.has_multi_file() || qwen_tts.multi_file().files_size() != 12) { - result.details = "registered MLX Qwen3-TTS metadata is incomplete"; - return result; - } - - runanywhere::v1::ModelInfo soprano; - if (!get_registered_model("mlx-soprano-1.1-80m-5bit", &soprano, &error) || - soprano.category() != runanywhere::v1::MODEL_CATEGORY_SPEECH_SYNTHESIS || - soprano.framework() != runanywhere::v1::INFERENCE_FRAMEWORK_MLX || - !soprano.has_multi_file() || soprano.multi_file().files_size() != 7) { - result.details = "registered MLX Soprano metadata is incomplete"; - return result; - } - - result.passed = true; - return result; -} - -// HF explicit-file refs normalize inside commons -// (rac_register_model_from_url_proto) now — verify through the production ABI -// that the saved entry carries the expected resolve/main download URL. -// Explicit-file refs never hit the network (only repo-level refs do, and none -// appear here). -TestResult test_hf_ref_registration() { - TestResult result; - result.test_name = "hf_ref_registration"; - - struct Case { - std::string in; - std::string expected_download_url; - }; - const Case cases[] = { - {"hf.co/Qwen/Qwen3-0.6B-GGUF/Qwen3-0.6B-Q8_0.gguf", - "https://huggingface.co/Qwen/Qwen3-0.6B-GGUF/resolve/main/" - "Qwen3-0.6B-Q8_0.gguf"}, - {"huggingface.co/org/repo/sub/dir/file.gguf", - "https://huggingface.co/org/repo/resolve/main/sub/dir/file.gguf"}, - {"https://huggingface.co/org/repo/resolve/main/f.gguf", - "https://huggingface.co/org/repo/resolve/main/f.gguf"}, - {"https://huggingface.co/org/repo/blob/main/sub/f.gguf", - "https://huggingface.co/org/repo/resolve/main/sub/f.gguf"}, - {"https://example.com/m.gguf", "https://example.com/m.gguf"}, - }; - for (const Case &c : cases) { - runanywhere::v1::RegisterModelFromUrlRequest request; - request.set_url(c.in); - const std::string bytes = rcli::proto::serialize(request); - - rac_proto_buffer_t out; - rac_proto_buffer_init(&out); - const rac_result_t rc = rac_register_model_from_url_proto( - reinterpret_cast(bytes.data()), bytes.size(), &out); - runanywhere::v1::ModelInfo saved; - std::string parse_error; - const bool parsed = rc == RAC_SUCCESS && rcli::proto::parse_proto_buffer( - &out, &saved, &parse_error); - if (!parsed) { - result.expected = c.expected_download_url; - result.actual = ""; - result.details = "input: " + c.in + " " + parse_error; - return result; - } - if (saved.download_url() != c.expected_download_url) { - result.expected = c.expected_download_url; - result.actual = - saved.download_url().empty() ? "" : saved.download_url(); - result.details = "input: " + c.in; - return result; - } - } - result.passed = true; - return result; -} - -// --------------------------------------------------------------------------- -// diarize command coverage -// -// These tests exercise register_diarize()'s argv surface WITHOUT reaching the -// CLI11 callback (which would fire run_diarize -> bootstrap + a real ONNX -// Sortformer model). Two inference-free strategies are used: -// 1. Pure introspection: configure_app() then query the CLI11 App/Option -// model -- never parse, never run a callback. -// 2. Parse-FAILURE paths via rcli::run(): a usage error makes CLI11 throw a -// ParseError inside parse(), before any callback, and src/app.cpp maps -// every ParseError to the production exit code 2 (0 ok, 1 runtime, 2 -// usage). -// The --json / table render path (print_result) has internal linkage and needs -// a real model, so it is intentionally not covered here. -// --------------------------------------------------------------------------- - -// RAII zero-byte temp file. A zero-byte regular file satisfies -// CLI::ExistingFile (WAV validity is only checked later, inside run_diarize, -// which a parse-error path never reaches). -class TempWavFile { -public: - TempWavFile() { - namespace fs = std::filesystem; - static int counter = 0; - path_ = (fs::temp_directory_path() / - ("rcli_diarize_test_" + std::to_string(++counter) + ".wav")) - .string(); - std::ofstream(path_).close(); - } - ~TempWavFile() { - std::error_code ec; - std::filesystem::remove(path_, ec); - } - const std::string &path() const { return path_; } - -private: - std::string path_; -}; - -// Drive the production entry point rcli::run() with an argv vector. run() builds -// its own App + GlobalOptions, so a usage error returns the true production exit -// code (2) without any bootstrap or inference. -int run_rcli(const std::vector &args) { - std::vector mutable_args = args; - std::vector argv; - argv.reserve(mutable_args.size()); - for (std::string &arg : mutable_args) { - argv.push_back(arg.data()); - } - return rcli::run(static_cast(argv.size()), argv.data()); -} - -TestResult test_diarize_arg_surface() { - TestResult result; - result.test_name = "diarize_arg_surface"; - - rcli::GlobalOptions options; - CLI::App app{"rcli test app"}; - rcli::configure_app(app, options); - - const CLI::App *cmd = app.get_subcommand_no_throw("diarize"); - if (cmd == nullptr) { - result.details = "diarize subcommand not registered"; - return result; - } - if (cmd->get_description() != - "Label who spoke when in an audio file") { - result.expected = "Label who spoke when in an audio file"; - result.actual = cmd->get_description(); - return result; - } - - const CLI::Option *audio = cmd->get_option_no_throw("audio"); - if (audio == nullptr || !audio->get_required()) { - result.details = "positional 'audio' must exist and be required"; - return result; - } - - const CLI::Option *model = cmd->get_option_no_throw("--model"); - if (model == nullptr || !model->get_required() || !model->check_name("-m")) { - result.details = "--model must exist, be required, and carry the -m alias"; - return result; - } - - const char *optional_flags[] = {"--threshold", "--min-duration", - "--merge-gap"}; - for (const char *name : optional_flags) { - const CLI::Option *opt = cmd->get_option_no_throw(name); - if (opt == nullptr) { - result.details = std::string("missing option ") + name; - return result; - } - if (opt->get_required()) { - result.details = std::string(name) + " must not be required"; - return result; - } - } - - result.passed = true; - return result; -} - -TestResult test_diarize_missing_model_exit2() { - TestResult result; - result.test_name = "diarize_missing_model_exit2"; - - // audio positional satisfied by an existing temp file -> the only failure is - // the missing required --model (RequiredError -> ParseError -> exit 2). - TempWavFile audio; - const int code = run_rcli({"rcli", "diarize", audio.path()}); - if (code != 2) { - result.expected = "2"; - result.actual = std::to_string(code); - result.details = "missing required --model should be a usage error"; - return result; - } - result.passed = true; - return result; -} - -TestResult test_diarize_missing_audio_exit2() { - TestResult result; - result.test_name = "diarize_missing_audio_exit2"; - - // --model consumes "x"; the required audio positional is left unsatisfied - // (RequiredError -> ParseError -> exit 2). - const int code = run_rcli({"rcli", "diarize", "--model", "x"}); - if (code != 2) { - result.expected = "2"; - result.actual = std::to_string(code); - result.details = - "missing required audio positional should be a usage error"; - return result; - } - result.passed = true; - return result; -} - -TestResult test_diarize_audio_not_found_exit2() { - TestResult result; - result.test_name = "diarize_audio_not_found_exit2"; - - // --model is supplied so the sole failure is the audio ->check(ExistingFile) - // validator (ValidationError -> ParseError -> exit 2), a distinct path from a - // plain RequiredError. - const int code = run_rcli( - {"rcli", "diarize", "/no/such/rcli-diarize-input.wav", "--model", "x"}); - if (code != 2) { - result.expected = "2"; - result.actual = std::to_string(code); - result.details = - "non-existent audio should fail CLI::ExistingFile (usage error)"; - return result; - } - result.passed = true; - return result; -} - -TestResult test_diarize_numeric_option_typing_exit2() { - TestResult result; - result.test_name = "diarize_numeric_option_typing_exit2"; - - // A non-numeric value for a typed numeric option raises CLI11 ConversionError - // (a ParseError) during parse, before the callback -> exit 2. This is the - // only inference-free way to prove --threshold binds to a float and - // --min-duration/--merge-gap bind to integers (a *valid* value would run the - // callback and load a model). Required args are satisfied so the conversion - // is the only failure. - TempWavFile audio; - const char *numeric_flags[] = {"--threshold", "--min-duration", - "--merge-gap"}; - for (const char *flag : numeric_flags) { - const int code = run_rcli( - {"rcli", "diarize", audio.path(), "--model", "x", flag, "notanumber"}); - if (code != 2) { - result.expected = "2"; - result.actual = std::to_string(code); - result.details = - std::string("non-numeric ") + flag + " should be a usage error"; - return result; - } - } - result.passed = true; - return result; -} - -TestResult test_diarize_unknown_flag_exit2() { - TestResult result; - result.test_name = "diarize_unknown_flag_exit2"; - - // An unrecognized option is not consumed by the subcommand or (via - // fallthrough) the parent, so parse ends with an ExtrasError (ParseError) -> - // exit 2. Guards against silently-ignored typos. - TempWavFile audio; - const int code = - run_rcli({"rcli", "diarize", audio.path(), "--model", "x", "--bogus"}); - if (code != 2) { - result.expected = "2"; - result.actual = std::to_string(code); - result.details = "unrecognized flag should be a usage error (ExtrasError)"; - return result; - } - result.passed = true; - return result; -} - -// =========================================================================== -// image_io helpers (write_png / read_ppm) — the segment command's PNG encoder -// and PPM decoder. Pure file-path helpers, exercised via temp files (write_png -// and read_ppm operate on paths via fopen, not injectable streams). Offline, -// model-free, deterministic. See src/io/image_io.{h,cpp}. -// =========================================================================== - -// Unique path under the system temp dir; RAII removes it recursively on scope -// exit (recursive so it also covers the never-created parent dirs used by the -// unwritable-path case). Mirrors make_temp_dir() in test_rcli_mlx_e2e.cpp. -std::string unique_temp_path(const std::string &name) { - static uint64_t counter = 0; - const auto stamp = - std::chrono::steady_clock::now().time_since_epoch().count(); - return (std::filesystem::temp_directory_path() / - (name + "-" + std::to_string(stamp) + "-" + - std::to_string(counter++))) - .string(); -} - -class TempFile { -public: - explicit TempFile(const std::string &name) : path_(unique_temp_path(name)) {} - ~TempFile() { - std::error_code ec; - std::filesystem::remove_all(path_, ec); - } - TempFile(const TempFile &) = delete; - TempFile &operator=(const TempFile &) = delete; - const std::string &path() const { return path_; } - -private: - std::string path_; -}; - -std::vector bytes_of(const std::string &s) { - return std::vector(s.begin(), s.end()); -} - -bool write_bytes(const std::string &path, const std::vector &bytes) { - std::ofstream out(path, std::ios::binary); - if (!out.is_open()) { - return false; - } - if (!bytes.empty()) { - out.write(reinterpret_cast(bytes.data()), - static_cast(bytes.size())); - } - return out.good(); -} - -bool read_bytes(const std::string &path, std::vector *bytes) { - std::ifstream in(path, std::ios::binary); - if (!in.is_open()) { - return false; - } - in.seekg(0, std::ios::end); - const std::streamoff size = in.tellg(); - if (size < 0) { - return false; - } - in.seekg(0, std::ios::beg); - bytes->resize(static_cast(size)); - if (size > 0) { - in.read(reinterpret_cast(bytes->data()), - static_cast(size)); - } - return in.good() || in.eof(); -} - -// Build a valid P6 header ("P6\n \n255\n") followed by the raw pixels. -std::vector make_ppm(uint32_t w, uint32_t h, - const std::vector &pixels) { - const std::string header = - "P6\n" + std::to_string(w) + " " + std::to_string(h) + "\n255\n"; - std::vector v(header.begin(), header.end()); - v.insert(v.end(), pixels.begin(), pixels.end()); - return v; -} - -uint32_t read_u32_be(const std::vector &b, size_t off) { - return (static_cast(b[off]) << 24) | - (static_cast(b[off + 1]) << 16) | - (static_cast(b[off + 2]) << 8) | - static_cast(b[off + 3]); -} - -// Independent CRC-32 (PNG polynomial) — deliberately separate from the encoder's -// own implementation so a regression there cannot mask a regression here. -uint32_t test_crc32(const uint8_t *data, size_t len) { - static uint32_t table[256]; - static bool ready = false; - if (!ready) { - for (uint32_t n = 0; n < 256u; ++n) { - uint32_t c = n; - for (int k = 0; k < 8; ++k) { - c = (c & 1u) ? (0xEDB88320u ^ (c >> 1)) : (c >> 1); - } - table[n] = c; - } - ready = true; - } - uint32_t crc = 0xFFFFFFFFu; - for (size_t i = 0; i < len; ++i) { - crc = table[(crc ^ data[i]) & 0xFF] ^ (crc >> 8); - } - return crc ^ 0xFFFFFFFFu; -} - -// Independent Adler-32 (per-byte modulo form). -uint32_t test_adler32(const uint8_t *data, size_t len) { - uint32_t a = 1; - uint32_t b = 0; - for (size_t i = 0; i < len; ++i) { - a = (a + data[i]) % 65521u; - b = (b + a) % 65521u; - } - return (b << 16) | a; -} - -// Reconstruct the PNG filtered scanlines the encoder feeds into DEFLATE: a 0x00 -// filter byte per row followed by that row's RGBA bytes. -std::vector filtered_raw(const std::vector &rgba, int width, - int height) { - const size_t row_bytes = static_cast(width) * 4; - std::vector raw; - raw.reserve(static_cast(height) * (1 + row_bytes)); - for (int y = 0; y < height; ++y) { - raw.push_back(0); - const uint8_t *row = rgba.data() + static_cast(y) * row_bytes; - raw.insert(raw.end(), row, row + row_bytes); - } - return raw; -} - -struct PngChunk { - std::string type; - std::vector data; - uint32_t stored_crc = 0; - uint32_t computed_crc = 0; -}; - -// Parse the 8-byte signature + length/type/data/CRC chunk stream. Records both -// the stored CRC and an independently computed CRC over type+data per chunk. -bool parse_png(const std::vector &png, std::vector *out) { - static const uint8_t sig[8] = {137, 80, 78, 71, 13, 10, 26, 10}; - if (png.size() < 8u || std::memcmp(png.data(), sig, 8) != 0) { - return false; - } - size_t pos = 8; - while (pos + 8 <= png.size()) { - const uint32_t len = read_u32_be(png, pos); - const size_t data_off = pos + 8; - if (data_off + len + 4 > png.size()) { - return false; - } - PngChunk c; - c.type.assign(png.begin() + pos + 4, png.begin() + pos + 8); - c.data.assign(png.begin() + data_off, png.begin() + data_off + len); - c.stored_crc = read_u32_be(png, data_off + len); - const std::vector crc_input(png.begin() + pos + 4, - png.begin() + data_off + len); - c.computed_crc = test_crc32(crc_input.data(), crc_input.size()); - out->push_back(c); - pos = data_off + len + 4; - } - return pos == png.size(); -} - -const PngChunk *find_chunk(const std::vector &chunks, - const std::string &type) { - for (const PngChunk &c : chunks) { - if (c.type == type) { - return &c; - } - } - return nullptr; -} - -// Parse a zlib stream (0x78 0x01 + stored DEFLATE blocks + 4-byte Adler-32). -struct ZlibParse { - bool ok = false; - std::vector payload; - int block_count = 0; - bool bfinal_ok = false; // BFINAL set on exactly the last block, none earlier - bool lennlen_ok = true; // NLEN == ~LEN for every block - uint32_t adler = 0; -}; - -ZlibParse parse_stored_zlib(const std::vector &z) { - ZlibParse r; - if (z.size() < 6u || z[0] != 0x78 || z[1] != 0x01) { - return r; - } - const size_t adler_off = z.size() - 4; - size_t pos = 2; - std::vector finals; - while (pos < adler_off) { - if (adler_off - pos < 5u) { // 1 header byte + LEN + NLEN - return r; - } - const uint8_t hdr = z[pos]; - const uint8_t btype = static_cast((hdr >> 1) & 0x03); - if (btype != 0) { // only stored (uncompressed) blocks are emitted - return r; - } - const uint16_t len = static_cast(z[pos + 1] | (z[pos + 2] << 8)); - const uint16_t nlen = static_cast(z[pos + 3] | (z[pos + 4] << 8)); - if (static_cast(~len) != nlen) { - r.lennlen_ok = false; - } - const size_t data_off = pos + 5; - if (data_off + len > adler_off) { - return r; - } - r.payload.insert(r.payload.end(), z.begin() + data_off, - z.begin() + data_off + len); - finals.push_back((hdr & 0x01) != 0); - pos = data_off + len; - ++r.block_count; - } - if (pos != adler_off) { - return r; - } - r.bfinal_ok = !finals.empty() && finals.back(); - for (size_t i = 0; i + 1 < finals.size(); ++i) { - if (finals[i]) { - r.bfinal_ok = false; - } - } - r.adler = (static_cast(z[adler_off]) << 24) | - (static_cast(z[adler_off + 1]) << 16) | - (static_cast(z[adler_off + 2]) << 8) | - static_cast(z[adler_off + 3]); - r.ok = true; - return r; -} - -TestResult test_read_ppm_errors() { - TestResult result; - result.test_name = "read_ppm_errors"; - - auto with_pixels = [](const std::string &header, size_t n) { - std::vector v(header.begin(), header.end()); - for (size_t i = 0; i < n; ++i) { - v.push_back(static_cast(i)); - } - return v; - }; - - struct Case { - const char *label; - bool create; // write `bytes` to a temp file first - std::vector bytes; // file contents when create == true - const char *expect_substr; - }; - - const std::vector cases = { - {"missing file", false, {}, "cannot open"}, - {"ascii P3 magic", true, with_pixels("P3\n2 1\n255\n", 6), - "is not a binary PPM (P6)"}, - {"one byte file", true, bytes_of("P"), "is not a binary PPM (P6)"}, - {"non-numeric dimension", true, bytes_of("P6\nxx 1\n255\n"), - "malformed PPM header"}, - {"eof before maxval", true, bytes_of("P6\n2 1\n"), - "malformed PPM header"}, - {"zero width", true, bytes_of("P6\n0 1\n255\n"), "unsupported PPM"}, - {"zero height", true, bytes_of("P6\n2 0\n255\n"), "unsupported PPM"}, - {"maxval 254", true, with_pixels("P6\n2 1\n254\n", 6), - "unsupported PPM"}, - {"maxval 65535", true, with_pixels("P6\n2 1\n65535\n", 6), - "unsupported PPM"}, - {"truncated payload", true, with_pixels("P6\n2 2\n255\n", 6), - "truncated PPM pixel data"}, - }; - - for (const Case &c : cases) { - TempFile tf("rcli-ppm-err"); - if (c.create && !write_bytes(tf.path(), c.bytes)) { - result.details = std::string("setup failed for case: ") + c.label; - return result; - } - - // Seed `out` with sentinels: a failed read must leave it untouched. - rcli::image::RgbImage out; - out.width = 12345u; - out.height = 67890u; - out.rgb = {9, 9, 9}; - - std::string error; - const bool ok = rcli::image::read_ppm(tf.path(), &out, &error); - if (ok) { - result.details = std::string("expected failure for case: ") + c.label; - return result; - } - if (error.find(c.expect_substr) == std::string::npos) { - result.expected = c.expect_substr; - result.actual = error; - result.details = std::string("wrong error for case: ") + c.label; - return result; - } - if (out.width != 12345u || out.height != 67890u || out.rgb.size() != 3u || - out.rgb[0] != 9 || out.rgb[1] != 9 || out.rgb[2] != 9) { - result.details = - std::string("out mutated on failure for case: ") + c.label; - return result; - } - } - - result.passed = true; - return result; -} - -TestResult test_read_ppm_happy_path() { - TestResult result; - result.test_name = "read_ppm_happy_path"; - - // Minimal 2x1 image: exact tight RGB8 packing (what cmd_segment feeds as - // stride = width*3, RAC_SEGMENTATION_PIXEL_FORMAT_RGB8). - const std::vector pixels = {10, 20, 30, 200, 210, 220}; - { - TempFile tf("rcli-ppm-2x1"); - if (!write_bytes(tf.path(), make_ppm(2, 1, pixels))) { - result.details = "setup: cannot write 2x1 ppm"; - return result; - } - rcli::image::RgbImage out; - std::string error; - if (!rcli::image::read_ppm(tf.path(), &out, &error)) { - result.details = "read_ppm failed on valid 2x1: " + error; - return result; - } - if (out.width != 2u || out.height != 1u) { - result.expected = "2x1"; - result.actual = - std::to_string(out.width) + "x" + std::to_string(out.height); - result.details = "wrong dimensions"; - return result; - } - if (out.rgb.size() != pixels.size() || out.rgb != pixels) { - result.details = "pixel payload mismatch (tight RGB8 packing)"; - return result; - } - } - - // Larger buffer with a trailing byte beyond the declared payload: exactly - // width*height*3 bytes are captured and the extra byte is ignored (no - // off-by-one at the payload boundary). - { - const uint32_t w = 4; - const uint32_t h = 3; - std::vector pixels2(static_cast(w) * h * 3); - for (size_t i = 0; i < pixels2.size(); ++i) { - pixels2[i] = static_cast(i * 7 + 1); - } - std::vector file = make_ppm(w, h, pixels2); - file.push_back(0xAB); // trailing byte past the payload - - TempFile tf("rcli-ppm-4x3"); - if (!write_bytes(tf.path(), file)) { - result.details = "setup: cannot write 4x3 ppm"; - return result; - } - rcli::image::RgbImage out; - std::string error; - if (!rcli::image::read_ppm(tf.path(), &out, &error)) { - result.details = "read_ppm failed on valid 4x3: " + error; - return result; - } - if (out.width != w || out.height != h || - out.rgb.size() != static_cast(w) * h * 3 || - out.rgb != pixels2) { - result.details = "4x3 payload/boundary mismatch"; - return result; - } - } - - result.passed = true; - return result; -} - -TestResult test_read_ppm_header_lexing() { - TestResult result; - result.test_name = "read_ppm_header_lexing"; - - const std::vector pixels = {1, 2, 3, 4, 5, 6}; - - // (a) '#'-to-EOL comments are skipped and (b) arbitrary/mixed whitespace - // (spaces, tabs, newlines) between the magic and the three integers is - // tolerated. - { - const std::string header = - "P6\n" - "# a comment line\n" - "\t 2 \t 1\n" - "# another comment\n" - "255\n"; - std::vector file(header.begin(), header.end()); - file.insert(file.end(), pixels.begin(), pixels.end()); - - TempFile tf("rcli-ppm-comments"); - if (!write_bytes(tf.path(), file)) { - result.details = "setup: cannot write commented ppm"; - return result; - } - rcli::image::RgbImage out; - std::string error; - if (!rcli::image::read_ppm(tf.path(), &out, &error)) { - result.details = "comments/whitespace not tolerated: " + error; - return result; - } - if (out.width != 2u || out.height != 1u || out.rgb != pixels) { - result.details = "commented header parsed to the wrong image"; - return result; - } - } - - // (c) exactly ONE whitespace byte is consumed between maxval and the pixel - // payload (the `++pos` contract); a single space separator must work. - { - const std::string header = "P6\n2 1\n255 "; // one space, then pixels - std::vector file(header.begin(), header.end()); - file.insert(file.end(), pixels.begin(), pixels.end()); - - TempFile tf("rcli-ppm-space-sep"); - if (!write_bytes(tf.path(), file)) { - result.details = "setup: cannot write space-separator ppm"; - return result; - } - rcli::image::RgbImage out; - std::string error; - if (!rcli::image::read_ppm(tf.path(), &out, &error)) { - result.details = "single-space separator not accepted: " + error; - return result; - } - if (out.width != 2u || out.height != 1u || out.rgb != pixels) { - result.details = "space-separated header parsed to the wrong image"; - return result; - } - } - - // (d) uint overflow guard: a dimension token > 0xFFFFFFFF is malformed. - { - const std::string header = "P6\n4294967296 1\n255\n"; // 2^32 width - std::vector file(header.begin(), header.end()); - file.insert(file.end(), pixels.begin(), pixels.end()); - - TempFile tf("rcli-ppm-overflow"); - if (!write_bytes(tf.path(), file)) { - result.details = "setup: cannot write overflow ppm"; - return result; - } - rcli::image::RgbImage out; - std::string error; - if (rcli::image::read_ppm(tf.path(), &out, &error)) { - result.details = "overflowing dimension should be rejected"; - return result; - } - if (error.find("malformed PPM header") == std::string::npos) { - result.expected = "malformed PPM header"; - result.actual = error; - return result; - } - } - - result.passed = true; - return result; -} - -TestResult test_write_png_invalid_args() { - TestResult result; - result.test_name = "write_png_invalid_args"; - - const std::vector px(2 * 2 * 4, 0x33); - - struct Case { - const char *label; - const uint8_t *data; - int width; - int height; - }; - const Case cases[] = { - {"null data", nullptr, 2, 2}, - {"zero width", px.data(), 0, 2}, - {"negative width", px.data(), -1, 2}, - {"zero height", px.data(), 2, 0}, - {"negative height", px.data(), 2, -3}, - }; - - for (const Case &c : cases) { - TempFile tf("rcli-png-badarg"); - std::string error; - const bool ok = - rcli::image::write_png(tf.path(), c.data, c.width, c.height, &error); - if (ok) { - result.details = std::string("expected failure for case: ") + c.label; - return result; - } - if (error != "invalid image dimensions or data") { - result.expected = "invalid image dimensions or data"; - result.actual = error; - result.details = std::string("wrong error for case: ") + c.label; - return result; - } - if (std::filesystem::exists(tf.path())) { - result.details = - std::string("no file should be created for case: ") + c.label; - return result; - } - } - - result.passed = true; - return result; -} - -TestResult test_write_png_container() { - TestResult result; - result.test_name = "write_png_container"; - - const int w = 2; - const int h = 2; - std::vector rgba(static_cast(w) * h * 4); - for (size_t i = 0; i < rgba.size(); ++i) { - rgba[i] = static_cast(i * 11 + 3); - } - - TempFile tf("rcli-png-container"); - std::string error; - if (!rcli::image::write_png(tf.path(), rgba.data(), w, h, &error)) { - result.details = "write_png failed: " + error; - return result; - } - - std::vector png; - if (!read_bytes(tf.path(), &png)) { - result.details = "cannot read back written png"; - return result; - } - - const uint8_t sig[8] = {137, 80, 78, 71, 13, 10, 26, 10}; - if (png.size() < 8u || std::memcmp(png.data(), sig, 8) != 0) { - result.details = "missing/incorrect 8-byte PNG signature"; - return result; - } - - std::vector chunks; - if (!parse_png(png, &chunks) || chunks.size() < 3u) { - result.details = "PNG chunk structure did not parse"; - return result; - } - if (chunks.front().type != "IHDR") { - result.actual = chunks.front().type; - result.details = "first chunk must be IHDR"; - return result; - } - if (chunks.back().type != "IEND" || !chunks.back().data.empty()) { - result.details = "final chunk must be a zero-length IEND"; - return result; - } - - const PngChunk *ihdr = &chunks.front(); - if (ihdr->data.size() != 13u) { - result.details = "IHDR must be 13 bytes"; - return result; - } - if (read_u32_be(ihdr->data, 0) != static_cast(w) || - read_u32_be(ihdr->data, 4) != static_cast(h)) { - result.details = "IHDR width/height mismatch"; - return result; - } - if (ihdr->data[8] != 8 || ihdr->data[9] != 6) { - result.expected = "8/6"; - result.actual = std::to_string(static_cast(ihdr->data[8])) + "/" + - std::to_string(static_cast(ihdr->data[9])); - result.details = "IHDR bit-depth/color-type must be 8/6 (RGBA)"; - return result; - } - - const PngChunk *idat = find_chunk(chunks, "IDAT"); - if (idat == nullptr) { - result.details = "no IDAT chunk"; - return result; - } - if (idat->data.size() < 2u || idat->data[0] != 0x78 || - idat->data[1] != 0x01) { - result.details = "IDAT zlib header must be 0x78 0x01"; - return result; - } - - result.passed = true; - return result; -} - -TestResult test_write_png_byte_exact() { - TestResult result; - result.test_name = "write_png_byte_exact"; - - const int w = 3; - const int h = 2; - std::vector rgba(static_cast(w) * h * 4); - for (size_t i = 0; i < rgba.size(); ++i) { - rgba[i] = static_cast(i * 13 + 5); - } - - TempFile tf("rcli-png-exact"); - std::string error; - if (!rcli::image::write_png(tf.path(), rgba.data(), w, h, &error)) { - result.details = "write_png failed: " + error; - return result; - } - std::vector png; - if (!read_bytes(tf.path(), &png)) { - result.details = "cannot read back written png"; - return result; - } - - std::vector chunks; - if (!parse_png(png, &chunks)) { - result.details = "PNG did not parse"; - return result; - } - - // (c) every chunk's stored CRC-32 matches an independent computation. - for (const PngChunk &c : chunks) { - if (c.stored_crc != c.computed_crc) { - result.details = "CRC-32 mismatch on chunk " + c.type; - return result; - } - } - - const PngChunk *idat = find_chunk(chunks, "IDAT"); - if (idat == nullptr) { - result.details = "no IDAT chunk"; - return result; - } - const ZlibParse z = parse_stored_zlib(idat->data); - if (!z.ok) { - result.details = "IDAT zlib stored-block stream did not parse"; - return result; - } - - // (a) the stored block payload equals the independently reconstructed - // filtered scanlines. - const std::vector raw = filtered_raw(rgba, w, h); - if (z.payload != raw) { - result.details = "stored DEFLATE payload != filtered scanlines"; - return result; - } - if (z.block_count != 1) { - result.expected = "1"; - result.actual = std::to_string(z.block_count); - result.details = "small image should be a single stored block"; - return result; - } - if (!z.bfinal_ok) { - result.details = "single block must have BFINAL=1"; - return result; - } - if (!z.lennlen_ok) { - result.details = "stored block LEN/NLEN are not one's-complement"; - return result; - } - - // (b) trailing big-endian Adler-32 matches adler32(raw). - if (z.adler != test_adler32(raw.data(), raw.size())) { - result.details = "Adler-32 checksum mismatch"; - return result; - } - - result.passed = true; - return result; -} - -TestResult test_write_png_multi_block() { - TestResult result; - result.test_name = "write_png_multi_block"; - - // Filtered raw = height*(1 + width*4) must exceed 0xFFFF to force >1 stored - // DEFLATE block. 200 * (1 + 400) = 80200 bytes => two blocks. - const int w = 100; - const int h = 200; - std::vector rgba(static_cast(w) * h * 4); - for (size_t i = 0; i < rgba.size(); ++i) { - rgba[i] = static_cast((i * 31 + 17) & 0xFF); - } - - TempFile tf("rcli-png-multiblock"); - std::string error; - if (!rcli::image::write_png(tf.path(), rgba.data(), w, h, &error)) { - result.details = "write_png failed: " + error; - return result; - } - std::vector png; - if (!read_bytes(tf.path(), &png)) { - result.details = "cannot read back written png"; - return result; - } - - std::vector chunks; - if (!parse_png(png, &chunks)) { - result.details = "PNG did not parse"; - return result; - } - const PngChunk *idat = find_chunk(chunks, "IDAT"); - if (idat == nullptr) { - result.details = "no IDAT chunk"; - return result; - } - const ZlibParse z = parse_stored_zlib(idat->data); - if (!z.ok) { - result.details = "IDAT zlib stored-block stream did not parse"; - return result; - } - if (z.block_count <= 1) { - result.expected = ">1"; - result.actual = std::to_string(z.block_count); - result.details = "expected more than one stored block"; - return result; - } - if (!z.bfinal_ok) { - result.details = "only the final stored block may set BFINAL=1"; - return result; - } - if (!z.lennlen_ok) { - result.details = "each block's LEN/NLEN must be one's-complement"; - return result; - } - - const std::vector raw = filtered_raw(rgba, w, h); - if (z.payload != raw) { - result.details = "reassembled multi-block payload != filtered scanlines"; - return result; - } - if (z.adler != test_adler32(raw.data(), raw.size())) { - result.details = "Adler-32 checksum mismatch across blocks"; - return result; - } - - result.passed = true; - return result; -} - -TestResult test_write_png_unwritable_path() { - TestResult result; - result.test_name = "write_png_unwritable_path"; - - TempFile base("rcli-png-nodir"); - // A path under a directory that was never created -> fopen("wb") fails. - const std::string path = - (std::filesystem::path(base.path()) / "no_such_subdir" / "x.png") - .string(); - - const std::vector rgba(2 * 2 * 4, 0x40); - std::string error; - const bool ok = rcli::image::write_png(path, rgba.data(), 2, 2, &error); - if (ok) { - result.details = "write_png should fail into a non-existent directory"; - return result; - } - if (error.find("cannot open") == std::string::npos || - error.find("for writing") == std::string::npos) { - result.expected = "cannot open for writing"; - result.actual = error; - return result; - } - if (std::filesystem::exists(path)) { - result.details = "no file should be produced on open failure"; - return result; - } - - result.passed = true; - return result; -} - -TestResult test_bench_metrics_consume_only() { - TestResult result; - result.test_name = "bench_metrics_consume_only"; - - // LLM: consume TokenUsage + measured phase fields; no tok/s or decode_ms invent. - { - runanywhere::v1::LLMGenerationResult r; - r.set_generation_time_ms(1500.0); - r.set_prompt_eval_time_ms(200); - r.set_decode_time_ms(800); - auto* usage = r.mutable_usage(); - usage->set_output_tokens(40); - usage->set_decode_tokens_per_second(50.0); - usage->set_prefill_ms(180); - usage->set_ttft_ms(210); // must not alias into prompt_eval_ms - - rcli::commands::bench_metrics::LlmVlmMetrics m; - if (!rcli::commands::bench_metrics::fill_llm(r, /*measured_e2e_ms=*/9999.0, &m)) { - result.details = "fill_llm rejected a valid result"; - return result; - } - if (m.output_tokens != 40 || m.end_to_end_ms != 1500.0 || m.tokens_per_second != 50.0 || - m.decode_ms != 800.0 || m.prompt_eval_ms != 200.0) { - result.expected = "tokens=40 e2e=1500 tps=50 decode=800 prefill=200"; - result.actual = "tokens=" + std::to_string(m.output_tokens) + - " e2e=" + std::to_string(m.end_to_end_ms) + - " tps=" + std::to_string(m.tokens_per_second) + - " decode=" + std::to_string(m.decode_ms) + - " prefill=" + std::to_string(m.prompt_eval_ms); - return result; - } - } - - // Missing decode throughput / phase times stay zero — no wall or tokens÷rate invent. - { - runanywhere::v1::LLMGenerationResult r; - r.mutable_usage()->set_output_tokens(256); - r.mutable_usage()->set_ttft_ms(500); // still must not become prefill - - rcli::commands::bench_metrics::LlmVlmMetrics m; - if (!rcli::commands::bench_metrics::fill_llm(r, /*measured_e2e_ms=*/20000.0, &m)) { - result.details = "fill_llm should accept tokens with missing rates"; - return result; - } - if (m.tokens_per_second != 0.0 || m.decode_ms != 0.0 || m.prompt_eval_ms != 0.0 || - m.end_to_end_ms != 20000.0 || m.output_tokens != 256) { - result.expected = "tps=0 decode=0 prefill=0 e2e=20000 tokens=256"; - result.actual = "tps=" + std::to_string(m.tokens_per_second) + - " decode=" + std::to_string(m.decode_ms) + - " prefill=" + std::to_string(m.prompt_eval_ms) + - " e2e=" + std::to_string(m.end_to_end_ms) + - " tokens=" + std::to_string(m.output_tokens); - return result; - } - } - - // Zero output tokens fail rather than inventing metrics. - { - runanywhere::v1::LLMGenerationResult r; - r.mutable_usage()->set_decode_tokens_per_second(99.0); - rcli::commands::bench_metrics::LlmVlmMetrics m; - if (rcli::commands::bench_metrics::fill_llm(r, 1000.0, &m)) { - result.details = "fill_llm must reject zero output tokens"; - return result; - } - } - - // VLM: never invent tok/s from e2e; decode_ms always absent (no carrier). - { - runanywhere::v1::VLMResult r; - r.set_total_time_ms(3000); - auto* usage = r.mutable_usage(); - usage->set_output_tokens(64); - usage->set_prefill_ms(120); - usage->set_ttft_ms(130); - - rcli::commands::bench_metrics::LlmVlmMetrics m; - if (!rcli::commands::bench_metrics::fill_vlm(r, /*measured_e2e_ms=*/9999.0, &m)) { - result.details = "fill_vlm rejected a valid result"; - return result; - } - if (m.tokens_per_second != 0.0 || m.decode_ms != 0.0 || m.prompt_eval_ms != 120.0 || - m.end_to_end_ms != 3000.0 || m.output_tokens != 64) { - result.expected = "tps=0 decode=0 prefill=120 e2e=3000 tokens=64"; - result.actual = "tps=" + std::to_string(m.tokens_per_second) + - " decode=" + std::to_string(m.decode_ms) + - " prefill=" + std::to_string(m.prompt_eval_ms) + - " e2e=" + std::to_string(m.end_to_end_ms) + - " tokens=" + std::to_string(m.output_tokens); - return result; - } - } - - // Prefill falls back to TokenUsage.prefill_ms when prompt_eval_time_ms is absent. - { - runanywhere::v1::LLMGenerationResult r; - r.mutable_usage()->set_output_tokens(8); - r.mutable_usage()->set_prefill_ms(77); - rcli::commands::bench_metrics::LlmVlmMetrics m; - if (!rcli::commands::bench_metrics::fill_llm(r, 100.0, &m) || m.prompt_eval_ms != 77.0) { - result.expected = "prefill_ms=77 from TokenUsage"; - result.actual = "prefill=" + std::to_string(m.prompt_eval_ms); - return result; - } - } - - result.passed = true; - return result; -} - -} // namespace - -int main(int argc, char **argv) { - TestSuite suite("rcli_unit"); - suite.add("json_escape", test_json_escape); - suite.add("json_writer_shape", test_json_writer_shape); - suite.add("human_bytes", test_human_bytes); - suite.add("normalize_dir", test_normalize_dir); - suite.add("resolve_home_precedence", test_resolve_home_precedence); - suite.add("state_dir", test_state_dir); - suite.add("catalog_lookup", test_catalog_lookup); - suite.add("nvidia_sherpa_catalog", test_nvidia_sherpa_catalog); - suite.add("engine_hint_parsing", test_engine_hint_parsing); - suite.add("mlx_catalog_registration", test_mlx_catalog_registration); - suite.add("hf_ref_registration", test_hf_ref_registration); - suite.add("diarize_arg_surface", test_diarize_arg_surface); - suite.add("diarize_missing_model_exit2", test_diarize_missing_model_exit2); - suite.add("diarize_missing_audio_exit2", test_diarize_missing_audio_exit2); - suite.add("diarize_audio_not_found_exit2", test_diarize_audio_not_found_exit2); - suite.add("diarize_numeric_option_typing_exit2", - test_diarize_numeric_option_typing_exit2); - suite.add("diarize_unknown_flag_exit2", test_diarize_unknown_flag_exit2); - suite.add("read_ppm_errors", test_read_ppm_errors); - suite.add("read_ppm_happy_path", test_read_ppm_happy_path); - suite.add("read_ppm_header_lexing", test_read_ppm_header_lexing); - suite.add("write_png_invalid_args", test_write_png_invalid_args); - suite.add("write_png_container", test_write_png_container); - suite.add("write_png_byte_exact", test_write_png_byte_exact); - suite.add("write_png_multi_block", test_write_png_multi_block); - suite.add("write_png_unwritable_path", test_write_png_unwritable_path); - suite.add("bench_metrics_consume_only", test_bench_metrics_consume_only); - return suite.run(argc, argv); -} diff --git a/rcli/third_party/CLI11/CLI11.hpp b/rcli/third_party/CLI11/CLI11.hpp deleted file mode 100644 index 9fa9cc0266..0000000000 --- a/rcli/third_party/CLI11/CLI11.hpp +++ /dev/null @@ -1,11527 +0,0 @@ -// CLI11: Version 2.5.0 -// Originally designed by Henry Schreiner -// https://github.com/CLIUtils/CLI11 -// -// This is a standalone header file generated by MakeSingleHeader.py in CLI11/scripts -// from: v2.5.0 -// -// CLI11 2.5.0 Copyright (c) 2017-2025 University of Cincinnati, developed by Henry -// Schreiner under NSF AWARD 1414736. All rights reserved. -// -// Redistribution and use in source and binary forms of CLI11, with or without -// modification, are permitted provided that the following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, this -// list of conditions and the following disclaimer. -// 2. Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// 3. Neither the name of the copyright holder nor the names of its contributors -// may be used to endorse or promote products derived from this software without -// specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -// ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -#pragma once - -// Standard combined includes: -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - - -#define CLI11_VERSION_MAJOR 2 -#define CLI11_VERSION_MINOR 5 -#define CLI11_VERSION_PATCH 0 -#define CLI11_VERSION "2.5.0" - - - - -// The following version macro is very similar to the one in pybind11 -#if !(defined(_MSC_VER) && __cplusplus == 199711L) && !defined(__INTEL_COMPILER) -#if __cplusplus >= 201402L -#define CLI11_CPP14 -#if __cplusplus >= 201703L -#define CLI11_CPP17 -#if __cplusplus > 201703L -#define CLI11_CPP20 -#if __cplusplus > 202002L -#define CLI11_CPP23 -#if __cplusplus > 202302L -#define CLI11_CPP26 -#endif -#endif -#endif -#endif -#endif -#elif defined(_MSC_VER) && __cplusplus == 199711L -// MSVC sets _MSVC_LANG rather than __cplusplus (supposedly until the standard was fully implemented) -// Unless you use the /Zc:__cplusplus flag on Visual Studio 2017 15.7 Preview 3 or newer -#if _MSVC_LANG >= 201402L -#define CLI11_CPP14 -#if _MSVC_LANG > 201402L && _MSC_VER >= 1910 -#define CLI11_CPP17 -#if _MSVC_LANG > 201703L && _MSC_VER >= 1910 -#define CLI11_CPP20 -#if _MSVC_LANG > 202002L && _MSC_VER >= 1922 -#define CLI11_CPP23 -#endif -#endif -#endif -#endif -#endif - -#if defined(CLI11_CPP14) -#define CLI11_DEPRECATED(reason) [[deprecated(reason)]] -#elif defined(_MSC_VER) -#define CLI11_DEPRECATED(reason) __declspec(deprecated(reason)) -#else -#define CLI11_DEPRECATED(reason) __attribute__((deprecated(reason))) -#endif - -// GCC < 10 doesn't ignore this in unevaluated contexts -#if !defined(CLI11_CPP17) || \ - (defined(__GNUC__) && !defined(__llvm__) && !defined(__INTEL_COMPILER) && __GNUC__ < 10 && __GNUC__ > 4) -#define CLI11_NODISCARD -#else -#define CLI11_NODISCARD [[nodiscard]] -#endif - -/** detection of rtti */ -#ifndef CLI11_USE_STATIC_RTTI -#if (defined(_HAS_STATIC_RTTI) && _HAS_STATIC_RTTI) -#define CLI11_USE_STATIC_RTTI 1 -#elif defined(__cpp_rtti) -#if (defined(_CPPRTTI) && _CPPRTTI == 0) -#define CLI11_USE_STATIC_RTTI 1 -#else -#define CLI11_USE_STATIC_RTTI 0 -#endif -#elif (defined(__GCC_RTTI) && __GXX_RTTI) -#define CLI11_USE_STATIC_RTTI 0 -#else -#define CLI11_USE_STATIC_RTTI 1 -#endif -#endif - -/** availability */ -#if defined CLI11_CPP17 && defined __has_include && !defined CLI11_HAS_FILESYSTEM -#if __has_include() -// Filesystem cannot be used if targeting macOS < 10.15 -#if defined __MAC_OS_X_VERSION_MIN_REQUIRED && __MAC_OS_X_VERSION_MIN_REQUIRED < 101500 -#define CLI11_HAS_FILESYSTEM 0 -#elif defined(__wasi__) -// As of wasi-sdk-14, filesystem is not implemented -#define CLI11_HAS_FILESYSTEM 0 -#else -#include -#if defined __cpp_lib_filesystem && __cpp_lib_filesystem >= 201703 -#if defined _GLIBCXX_RELEASE && _GLIBCXX_RELEASE >= 9 -#define CLI11_HAS_FILESYSTEM 1 -#elif defined(__GLIBCXX__) -// if we are using gcc and Version <9 default to no filesystem -#define CLI11_HAS_FILESYSTEM 0 -#else -#define CLI11_HAS_FILESYSTEM 1 -#endif -#else -#define CLI11_HAS_FILESYSTEM 0 -#endif -#endif -#endif -#endif - -/** availability */ -#if !defined(CLI11_CPP26) && !defined(CLI11_HAS_CODECVT) -#if defined(__GNUC__) && !defined(__llvm__) && !defined(__INTEL_COMPILER) && __GNUC__ < 5 -#define CLI11_HAS_CODECVT 0 -#else -#define CLI11_HAS_CODECVT 1 -#include -#endif -#else -#if defined(CLI11_HAS_CODECVT) -#if CLI11_HAS_CODECVT > 0 -#include -#endif -#else -#define CLI11_HAS_CODECVT 0 -#endif -#endif - -/** disable deprecations */ -#if defined(__GNUC__) // GCC or clang -#define CLI11_DIAGNOSTIC_PUSH _Pragma("GCC diagnostic push") -#define CLI11_DIAGNOSTIC_POP _Pragma("GCC diagnostic pop") - -#define CLI11_DIAGNOSTIC_IGNORE_DEPRECATED _Pragma("GCC diagnostic ignored \"-Wdeprecated-declarations\"") - -#elif defined(_MSC_VER) -#define CLI11_DIAGNOSTIC_PUSH __pragma(warning(push)) -#define CLI11_DIAGNOSTIC_POP __pragma(warning(pop)) - -#define CLI11_DIAGNOSTIC_IGNORE_DEPRECATED __pragma(warning(disable : 4996)) - -#else -#define CLI11_DIAGNOSTIC_PUSH -#define CLI11_DIAGNOSTIC_POP - -#define CLI11_DIAGNOSTIC_IGNORE_DEPRECATED - -#endif - -/** Inline macro **/ -#ifdef CLI11_COMPILE -#define CLI11_INLINE -#else -#define CLI11_INLINE inline -#endif - - - -#if defined CLI11_HAS_FILESYSTEM && CLI11_HAS_FILESYSTEM > 0 -#include // NOLINT(build/include) -#else -#include -#include -#endif - - - - -#ifdef CLI11_CPP17 -#include -#endif // CLI11_CPP17 - -#if defined CLI11_HAS_FILESYSTEM && CLI11_HAS_FILESYSTEM > 0 -#include -#include // NOLINT(build/include) -#endif // CLI11_HAS_FILESYSTEM - - - -#if defined(_WIN32) -#if !(defined(_AMD64_) || defined(_X86_) || defined(_ARM_)) -#if defined(__amd64__) || defined(__amd64) || defined(__x86_64__) || defined(__x86_64) || defined(_M_X64) || \ - defined(_M_AMD64) -#define _AMD64_ -#elif defined(i386) || defined(__i386) || defined(__i386__) || defined(__i386__) || defined(_M_IX86) -#define _X86_ -#elif defined(__arm__) || defined(_M_ARM) || defined(_M_ARMT) -#define _ARM_ -#elif defined(__aarch64__) || defined(_M_ARM64) -#define _ARM64_ -#elif defined(_M_ARM64EC) -#define _ARM64EC_ -#endif -#endif - -// first -#ifndef NOMINMAX -// if NOMINMAX is already defined we don't want to mess with that either way -#define NOMINMAX -#include -#undef NOMINMAX -#else -#include -#endif - -// second -#include -// third -#include -#include -#endif - - -namespace CLI { - - -/// Convert a wide string to a narrow string. -CLI11_INLINE std::string narrow(const std::wstring &str); -CLI11_INLINE std::string narrow(const wchar_t *str); -CLI11_INLINE std::string narrow(const wchar_t *str, std::size_t size); - -/// Convert a narrow string to a wide string. -CLI11_INLINE std::wstring widen(const std::string &str); -CLI11_INLINE std::wstring widen(const char *str); -CLI11_INLINE std::wstring widen(const char *str, std::size_t size); - -#ifdef CLI11_CPP17 -CLI11_INLINE std::string narrow(std::wstring_view str); -CLI11_INLINE std::wstring widen(std::string_view str); -#endif // CLI11_CPP17 - -#if defined CLI11_HAS_FILESYSTEM && CLI11_HAS_FILESYSTEM > 0 -/// Convert a char-string to a native path correctly. -CLI11_INLINE std::filesystem::path to_path(std::string_view str); -#endif // CLI11_HAS_FILESYSTEM - - - - -namespace detail { - -#if !CLI11_HAS_CODECVT -/// Attempt to set one of the acceptable unicode locales for conversion -CLI11_INLINE void set_unicode_locale() { - static const std::array unicode_locales{{"C.UTF-8", "en_US.UTF-8", ".UTF-8"}}; - - for(const auto &locale_name : unicode_locales) { - if(std::setlocale(LC_ALL, locale_name) != nullptr) { - return; - } - } - throw std::runtime_error("CLI::narrow: could not set locale to C.UTF-8"); -} - -template struct scope_guard_t { - F closure; - - explicit scope_guard_t(F closure_) : closure(closure_) {} - ~scope_guard_t() { closure(); } -}; - -template CLI11_NODISCARD CLI11_INLINE scope_guard_t scope_guard(F &&closure) { - return scope_guard_t{std::forward(closure)}; -} - -#endif // !CLI11_HAS_CODECVT - -CLI11_DIAGNOSTIC_PUSH -CLI11_DIAGNOSTIC_IGNORE_DEPRECATED - -CLI11_INLINE std::string narrow_impl(const wchar_t *str, std::size_t str_size) { -#if CLI11_HAS_CODECVT -#ifdef _WIN32 - return std::wstring_convert>().to_bytes(str, str + str_size); - -#else - return std::wstring_convert>().to_bytes(str, str + str_size); - -#endif // _WIN32 -#else // CLI11_HAS_CODECVT - (void)str_size; - std::mbstate_t state = std::mbstate_t(); - const wchar_t *it = str; - - std::string old_locale = std::setlocale(LC_ALL, nullptr); - auto sg = scope_guard([&] { std::setlocale(LC_ALL, old_locale.c_str()); }); - set_unicode_locale(); - - std::size_t new_size = std::wcsrtombs(nullptr, &it, 0, &state); - if(new_size == static_cast(-1)) { - throw std::runtime_error("CLI::narrow: conversion error in std::wcsrtombs at offset " + - std::to_string(it - str)); - } - std::string result(new_size, '\0'); - std::wcsrtombs(const_cast(result.data()), &str, new_size, &state); - - return result; - -#endif // CLI11_HAS_CODECVT -} - -CLI11_INLINE std::wstring widen_impl(const char *str, std::size_t str_size) { -#if CLI11_HAS_CODECVT -#ifdef _WIN32 - return std::wstring_convert>().from_bytes(str, str + str_size); - -#else - return std::wstring_convert>().from_bytes(str, str + str_size); - -#endif // _WIN32 -#else // CLI11_HAS_CODECVT - (void)str_size; - std::mbstate_t state = std::mbstate_t(); - const char *it = str; - - std::string old_locale = std::setlocale(LC_ALL, nullptr); - auto sg = scope_guard([&] { std::setlocale(LC_ALL, old_locale.c_str()); }); - set_unicode_locale(); - - std::size_t new_size = std::mbsrtowcs(nullptr, &it, 0, &state); - if(new_size == static_cast(-1)) { - throw std::runtime_error("CLI::widen: conversion error in std::mbsrtowcs at offset " + - std::to_string(it - str)); - } - std::wstring result(new_size, L'\0'); - std::mbsrtowcs(const_cast(result.data()), &str, new_size, &state); - - return result; - -#endif // CLI11_HAS_CODECVT -} - -CLI11_DIAGNOSTIC_POP - -} // namespace detail - -CLI11_INLINE std::string narrow(const wchar_t *str, std::size_t str_size) { return detail::narrow_impl(str, str_size); } -CLI11_INLINE std::string narrow(const std::wstring &str) { return detail::narrow_impl(str.data(), str.size()); } -// Flawfinder: ignore -CLI11_INLINE std::string narrow(const wchar_t *str) { return detail::narrow_impl(str, std::wcslen(str)); } - -CLI11_INLINE std::wstring widen(const char *str, std::size_t str_size) { return detail::widen_impl(str, str_size); } -CLI11_INLINE std::wstring widen(const std::string &str) { return detail::widen_impl(str.data(), str.size()); } -// Flawfinder: ignore -CLI11_INLINE std::wstring widen(const char *str) { return detail::widen_impl(str, std::strlen(str)); } - -#ifdef CLI11_CPP17 -CLI11_INLINE std::string narrow(std::wstring_view str) { return detail::narrow_impl(str.data(), str.size()); } -CLI11_INLINE std::wstring widen(std::string_view str) { return detail::widen_impl(str.data(), str.size()); } -#endif // CLI11_CPP17 - -#if defined CLI11_HAS_FILESYSTEM && CLI11_HAS_FILESYSTEM > 0 -CLI11_INLINE std::filesystem::path to_path(std::string_view str) { - return std::filesystem::path{ -#ifdef _WIN32 - widen(str) -#else - str -#endif // _WIN32 - }; -} -#endif // CLI11_HAS_FILESYSTEM - - - - -namespace detail { -#ifdef _WIN32 -/// Decode and return UTF-8 argv from GetCommandLineW. -CLI11_INLINE std::vector compute_win32_argv(); -#endif -} // namespace detail - - - -namespace detail { - -#ifdef _WIN32 -CLI11_INLINE std::vector compute_win32_argv() { - std::vector result; - int argc = 0; - - auto deleter = [](wchar_t **ptr) { LocalFree(ptr); }; - // NOLINTBEGIN(*-avoid-c-arrays) - auto wargv = std::unique_ptr(CommandLineToArgvW(GetCommandLineW(), &argc), deleter); - // NOLINTEND(*-avoid-c-arrays) - - if(wargv == nullptr) { - throw std::runtime_error("CommandLineToArgvW failed with code " + std::to_string(GetLastError())); - } - - result.reserve(static_cast(argc)); - for(size_t i = 0; i < static_cast(argc); ++i) { - result.push_back(narrow(wargv[i])); - } - - return result; -} -#endif - -} // namespace detail - - - - -/// Include the items in this namespace to get free conversion of enums to/from streams. -/// (This is available inside CLI as well, so CLI11 will use this without a using statement). -namespace enums { - -/// output streaming for enumerations -template ::value>::type> -std::ostream &operator<<(std::ostream &in, const T &item) { - // make sure this is out of the detail namespace otherwise it won't be found when needed - return in << static_cast::type>(item); -} - -} // namespace enums - -/// Export to CLI namespace -using enums::operator<<; - -namespace detail { -/// a constant defining an expected max vector size defined to be a big number that could be multiplied by 4 and not -/// produce overflow for some expected uses -constexpr int expected_max_vector_size{1 << 29}; -// Based on http://stackoverflow.com/questions/236129/split-a-string-in-c -/// Split a string by a delim -CLI11_INLINE std::vector split(const std::string &s, char delim); - -/// Simple function to join a string -template std::string join(const T &v, std::string delim = ",") { - std::ostringstream s; - auto beg = std::begin(v); - auto end = std::end(v); - if(beg != end) - s << *beg++; - while(beg != end) { - s << delim << *beg++; - } - auto rval = s.str(); - if(!rval.empty() && delim.size() == 1 && rval.back() == delim[0]) { - // remove trailing delimiter if the last entry was empty - rval.pop_back(); - } - return rval; -} - -/// Simple function to join a string from processed elements -template ::value>::type> -std::string join(const T &v, Callable func, std::string delim = ",") { - std::ostringstream s; - auto beg = std::begin(v); - auto end = std::end(v); - auto loc = s.tellp(); - while(beg != end) { - auto nloc = s.tellp(); - if(nloc > loc) { - s << delim; - loc = nloc; - } - s << func(*beg++); - } - return s.str(); -} - -/// Join a string in reverse order -template std::string rjoin(const T &v, std::string delim = ",") { - std::ostringstream s; - for(std::size_t start = 0; start < v.size(); start++) { - if(start > 0) - s << delim; - s << v[v.size() - start - 1]; - } - return s.str(); -} - -// Based roughly on http://stackoverflow.com/questions/25829143/c-trim-whitespace-from-a-string - -/// Trim whitespace from left of string -CLI11_INLINE std::string <rim(std::string &str); - -/// Trim anything from left of string -CLI11_INLINE std::string <rim(std::string &str, const std::string &filter); - -/// Trim whitespace from right of string -CLI11_INLINE std::string &rtrim(std::string &str); - -/// Trim anything from right of string -CLI11_INLINE std::string &rtrim(std::string &str, const std::string &filter); - -/// Trim whitespace from string -inline std::string &trim(std::string &str) { return ltrim(rtrim(str)); } - -/// Trim anything from string -inline std::string &trim(std::string &str, const std::string filter) { return ltrim(rtrim(str, filter), filter); } - -/// Make a copy of the string and then trim it -inline std::string trim_copy(const std::string &str) { - std::string s = str; - return trim(s); -} - -/// remove quotes at the front and back of a string either '"' or '\'' -CLI11_INLINE std::string &remove_quotes(std::string &str); - -/// remove quotes from all elements of a string vector and process escaped components -CLI11_INLINE void remove_quotes(std::vector &args); - -/// Add a leader to the beginning of all new lines (nothing is added -/// at the start of the first line). `"; "` would be for ini files -/// -/// Can't use Regex, or this would be a subs. -CLI11_INLINE std::string fix_newlines(const std::string &leader, std::string input); - -/// Make a copy of the string and then trim it, any filter string can be used (any char in string is filtered) -inline std::string trim_copy(const std::string &str, const std::string &filter) { - std::string s = str; - return trim(s, filter); -} - -/// Print subcommand aliases -CLI11_INLINE std::ostream &format_aliases(std::ostream &out, const std::vector &aliases, std::size_t wid); - -/// Verify the first character of an option -/// - is a trigger character, ! has special meaning and new lines would just be annoying to deal with -template bool valid_first_char(T c) { - return ((c != '-') && (static_cast(c) > 33)); // space and '!' not allowed -} - -/// Verify following characters of an option -template bool valid_later_char(T c) { - // = and : are value separators, { has special meaning for option defaults, - // and control codes other than tab would just be annoying to deal with in many places allowing space here has too - // much potential for inadvertent entry errors and bugs - return ((c != '=') && (c != ':') && (c != '{') && ((static_cast(c) > 32) || c == '\t')); -} - -/// Verify an option/subcommand name -CLI11_INLINE bool valid_name_string(const std::string &str); - -/// Verify an app name -inline bool valid_alias_name_string(const std::string &str) { - static const std::string badChars(std::string("\n") + '\0'); - return (str.find_first_of(badChars) == std::string::npos); -} - -/// check if a string is a container segment separator (empty or "%%") -inline bool is_separator(const std::string &str) { - static const std::string sep("%%"); - return (str.empty() || str == sep); -} - -/// Verify that str consists of letters only -inline bool isalpha(const std::string &str) { - return std::all_of(str.begin(), str.end(), [](char c) { return std::isalpha(c, std::locale()); }); -} - -/// Return a lower case version of a string -inline std::string to_lower(std::string str) { - std::transform(std::begin(str), std::end(str), std::begin(str), [](const std::string::value_type &x) { - return std::tolower(x, std::locale()); - }); - return str; -} - -/// remove underscores from a string -inline std::string remove_underscore(std::string str) { - str.erase(std::remove(std::begin(str), std::end(str), '_'), std::end(str)); - return str; -} - -/// Find and replace a substring with another substring -CLI11_INLINE std::string find_and_replace(std::string str, std::string from, std::string to); - -/// check if the flag definitions has possible false flags -inline bool has_default_flag_values(const std::string &flags) { - return (flags.find_first_of("{!") != std::string::npos); -} - -CLI11_INLINE void remove_default_flag_values(std::string &flags); - -/// Check if a string is a member of a list of strings and optionally ignore case or ignore underscores -CLI11_INLINE std::ptrdiff_t find_member(std::string name, - const std::vector names, - bool ignore_case = false, - bool ignore_underscore = false); - -/// Find a trigger string and call a modify callable function that takes the current string and starting position of the -/// trigger and returns the position in the string to search for the next trigger string -template inline std::string find_and_modify(std::string str, std::string trigger, Callable modify) { - std::size_t start_pos = 0; - while((start_pos = str.find(trigger, start_pos)) != std::string::npos) { - start_pos = modify(str, start_pos); - } - return str; -} - -/// close a sequence of characters indicated by a closure character. Brackets allows sub sequences -/// recognized bracket sequences include "'`[(<{ other closure characters are assumed to be literal strings -CLI11_INLINE std::size_t close_sequence(const std::string &str, std::size_t start, char closure_char); - -/// Split a string '"one two" "three"' into 'one two', 'three' -/// Quote characters can be ` ' or " or bracket characters [{(< with matching to the matching bracket -CLI11_INLINE std::vector split_up(std::string str, char delimiter = '\0'); - -/// get the value of an environmental variable or empty string if empty -CLI11_INLINE std::string get_environment_value(const std::string &env_name); - -/// This function detects an equal or colon followed by an escaped quote after an argument -/// then modifies the string to replace the equality with a space. This is needed -/// to allow the split up function to work properly and is intended to be used with the find_and_modify function -/// the return value is the offset+1 which is required by the find_and_modify function. -CLI11_INLINE std::size_t escape_detect(std::string &str, std::size_t offset); - -/// @brief detect if a string has escapable characters -/// @param str the string to do the detection on -/// @return true if the string has escapable characters -CLI11_INLINE bool has_escapable_character(const std::string &str); - -/// @brief escape all escapable characters -/// @param str the string to escape -/// @return a string with the escapable characters escaped with '\' -CLI11_INLINE std::string add_escaped_characters(const std::string &str); - -/// @brief replace the escaped characters with their equivalent -CLI11_INLINE std::string remove_escaped_characters(const std::string &str); - -/// generate a string with all non printable characters escaped to hex codes -CLI11_INLINE std::string binary_escape_string(const std::string &string_to_escape); - -CLI11_INLINE bool is_binary_escaped_string(const std::string &escaped_string); - -/// extract an escaped binary_string -CLI11_INLINE std::string extract_binary_string(const std::string &escaped_string); - -/// process a quoted string, remove the quotes and if appropriate handle escaped characters -CLI11_INLINE bool process_quoted_string(std::string &str, char string_char = '\"', char literal_char = '\''); - -/// This function formats the given text as a paragraph with fixed width and applies correct line wrapping -/// with a custom line prefix. The paragraph will get streamed to the given ostream. -CLI11_INLINE std::ostream &streamOutAsParagraph(std::ostream &out, - const std::string &text, - std::size_t paragraphWidth, - const std::string &linePrefix = "", - bool skipPrefixOnFirstLine = false); - -} // namespace detail - - - - -namespace detail { -CLI11_INLINE std::vector split(const std::string &s, char delim) { - std::vector elems; - // Check to see if empty string, give consistent result - if(s.empty()) { - elems.emplace_back(); - } else { - std::stringstream ss; - ss.str(s); - std::string item; - while(std::getline(ss, item, delim)) { - elems.push_back(item); - } - } - return elems; -} - -CLI11_INLINE std::string <rim(std::string &str) { - auto it = std::find_if(str.begin(), str.end(), [](char ch) { return !std::isspace(ch, std::locale()); }); - str.erase(str.begin(), it); - return str; -} - -CLI11_INLINE std::string <rim(std::string &str, const std::string &filter) { - auto it = std::find_if(str.begin(), str.end(), [&filter](char ch) { return filter.find(ch) == std::string::npos; }); - str.erase(str.begin(), it); - return str; -} - -CLI11_INLINE std::string &rtrim(std::string &str) { - auto it = std::find_if(str.rbegin(), str.rend(), [](char ch) { return !std::isspace(ch, std::locale()); }); - str.erase(it.base(), str.end()); - return str; -} - -CLI11_INLINE std::string &rtrim(std::string &str, const std::string &filter) { - auto it = - std::find_if(str.rbegin(), str.rend(), [&filter](char ch) { return filter.find(ch) == std::string::npos; }); - str.erase(it.base(), str.end()); - return str; -} - -CLI11_INLINE std::string &remove_quotes(std::string &str) { - if(str.length() > 1 && (str.front() == '"' || str.front() == '\'' || str.front() == '`')) { - if(str.front() == str.back()) { - str.pop_back(); - str.erase(str.begin(), str.begin() + 1); - } - } - return str; -} - -CLI11_INLINE std::string &remove_outer(std::string &str, char key) { - if(str.length() > 1 && (str.front() == key)) { - if(str.front() == str.back()) { - str.pop_back(); - str.erase(str.begin(), str.begin() + 1); - } - } - return str; -} - -CLI11_INLINE std::string fix_newlines(const std::string &leader, std::string input) { - std::string::size_type n = 0; - while(n != std::string::npos && n < input.size()) { - n = input.find('\n', n); - if(n != std::string::npos) { - input = input.substr(0, n + 1) + leader + input.substr(n + 1); - n += leader.size(); - } - } - return input; -} - -CLI11_INLINE std::ostream &format_aliases(std::ostream &out, const std::vector &aliases, std::size_t wid) { - if(!aliases.empty()) { - out << std::setw(static_cast(wid)) << " aliases: "; - bool front = true; - for(const auto &alias : aliases) { - if(!front) { - out << ", "; - } else { - front = false; - } - out << detail::fix_newlines(" ", alias); - } - out << "\n"; - } - return out; -} - -CLI11_INLINE bool valid_name_string(const std::string &str) { - if(str.empty() || !valid_first_char(str[0])) { - return false; - } - auto e = str.end(); - for(auto c = str.begin() + 1; c != e; ++c) - if(!valid_later_char(*c)) - return false; - return true; -} - -CLI11_INLINE std::string find_and_replace(std::string str, std::string from, std::string to) { - - std::size_t start_pos = 0; - - while((start_pos = str.find(from, start_pos)) != std::string::npos) { - str.replace(start_pos, from.length(), to); - start_pos += to.length(); - } - - return str; -} - -CLI11_INLINE void remove_default_flag_values(std::string &flags) { - auto loc = flags.find_first_of('{', 2); - while(loc != std::string::npos) { - auto finish = flags.find_first_of("},", loc + 1); - if((finish != std::string::npos) && (flags[finish] == '}')) { - flags.erase(flags.begin() + static_cast(loc), - flags.begin() + static_cast(finish) + 1); - } - loc = flags.find_first_of('{', loc + 1); - } - flags.erase(std::remove(flags.begin(), flags.end(), '!'), flags.end()); -} - -CLI11_INLINE std::ptrdiff_t -find_member(std::string name, const std::vector names, bool ignore_case, bool ignore_underscore) { - auto it = std::end(names); - if(ignore_case) { - if(ignore_underscore) { - name = detail::to_lower(detail::remove_underscore(name)); - it = std::find_if(std::begin(names), std::end(names), [&name](std::string local_name) { - return detail::to_lower(detail::remove_underscore(local_name)) == name; - }); - } else { - name = detail::to_lower(name); - it = std::find_if(std::begin(names), std::end(names), [&name](std::string local_name) { - return detail::to_lower(local_name) == name; - }); - } - - } else if(ignore_underscore) { - name = detail::remove_underscore(name); - it = std::find_if(std::begin(names), std::end(names), [&name](std::string local_name) { - return detail::remove_underscore(local_name) == name; - }); - } else { - it = std::find(std::begin(names), std::end(names), name); - } - - return (it != std::end(names)) ? (it - std::begin(names)) : (-1); -} - -static const std::string escapedChars("\b\t\n\f\r\"\\"); -static const std::string escapedCharsCode("btnfr\"\\"); -static const std::string bracketChars{"\"'`[(<{"}; -static const std::string matchBracketChars("\"'`])>}"); - -CLI11_INLINE bool has_escapable_character(const std::string &str) { - return (str.find_first_of(escapedChars) != std::string::npos); -} - -CLI11_INLINE std::string add_escaped_characters(const std::string &str) { - std::string out; - out.reserve(str.size() + 4); - for(char s : str) { - auto sloc = escapedChars.find_first_of(s); - if(sloc != std::string::npos) { - out.push_back('\\'); - out.push_back(escapedCharsCode[sloc]); - } else { - out.push_back(s); - } - } - return out; -} - -CLI11_INLINE std::uint32_t hexConvert(char hc) { - int hcode{0}; - if(hc >= '0' && hc <= '9') { - hcode = (hc - '0'); - } else if(hc >= 'A' && hc <= 'F') { - hcode = (hc - 'A' + 10); - } else if(hc >= 'a' && hc <= 'f') { - hcode = (hc - 'a' + 10); - } else { - hcode = -1; - } - return static_cast(hcode); -} - -CLI11_INLINE char make_char(std::uint32_t code) { return static_cast(static_cast(code)); } - -CLI11_INLINE void append_codepoint(std::string &str, std::uint32_t code) { - if(code < 0x80) { // ascii code equivalent - str.push_back(static_cast(code)); - } else if(code < 0x800) { // \u0080 to \u07FF - // 110yyyyx 10xxxxxx; 0x3f == 0b0011'1111 - str.push_back(make_char(0xC0 | code >> 6)); - str.push_back(make_char(0x80 | (code & 0x3F))); - } else if(code < 0x10000) { // U+0800...U+FFFF - if(0xD800 <= code && code <= 0xDFFF) { - throw std::invalid_argument("[0xD800, 0xDFFF] are not valid UTF-8."); - } - // 1110yyyy 10yxxxxx 10xxxxxx - str.push_back(make_char(0xE0 | code >> 12)); - str.push_back(make_char(0x80 | (code >> 6 & 0x3F))); - str.push_back(make_char(0x80 | (code & 0x3F))); - } else if(code < 0x110000) { // U+010000 ... U+10FFFF - // 11110yyy 10yyxxxx 10xxxxxx 10xxxxxx - str.push_back(make_char(0xF0 | code >> 18)); - str.push_back(make_char(0x80 | (code >> 12 & 0x3F))); - str.push_back(make_char(0x80 | (code >> 6 & 0x3F))); - str.push_back(make_char(0x80 | (code & 0x3F))); - } -} - -CLI11_INLINE std::string remove_escaped_characters(const std::string &str) { - - std::string out; - out.reserve(str.size()); - for(auto loc = str.begin(); loc < str.end(); ++loc) { - if(*loc == '\\') { - if(str.end() - loc < 2) { - throw std::invalid_argument("invalid escape sequence " + str); - } - auto ecloc = escapedCharsCode.find_first_of(*(loc + 1)); - if(ecloc != std::string::npos) { - out.push_back(escapedChars[ecloc]); - ++loc; - } else if(*(loc + 1) == 'u') { - // must have 4 hex characters - if(str.end() - loc < 6) { - throw std::invalid_argument("unicode sequence must have 4 hex codes " + str); - } - std::uint32_t code{0}; - std::uint32_t mplier{16 * 16 * 16}; - for(int ii = 2; ii < 6; ++ii) { - std::uint32_t res = hexConvert(*(loc + ii)); - if(res > 0x0F) { - throw std::invalid_argument("unicode sequence must have 4 hex codes " + str); - } - code += res * mplier; - mplier = mplier / 16; - } - append_codepoint(out, code); - loc += 5; - } else if(*(loc + 1) == 'U') { - // must have 8 hex characters - if(str.end() - loc < 10) { - throw std::invalid_argument("unicode sequence must have 8 hex codes " + str); - } - std::uint32_t code{0}; - std::uint32_t mplier{16 * 16 * 16 * 16 * 16 * 16 * 16}; - for(int ii = 2; ii < 10; ++ii) { - std::uint32_t res = hexConvert(*(loc + ii)); - if(res > 0x0F) { - throw std::invalid_argument("unicode sequence must have 8 hex codes " + str); - } - code += res * mplier; - mplier = mplier / 16; - } - append_codepoint(out, code); - loc += 9; - } else if(*(loc + 1) == '0') { - out.push_back('\0'); - ++loc; - } else { - throw std::invalid_argument(std::string("unrecognized escape sequence \\") + *(loc + 1) + " in " + str); - } - } else { - out.push_back(*loc); - } - } - return out; -} - -CLI11_INLINE std::size_t close_string_quote(const std::string &str, std::size_t start, char closure_char) { - std::size_t loc{0}; - for(loc = start + 1; loc < str.size(); ++loc) { - if(str[loc] == closure_char) { - break; - } - if(str[loc] == '\\') { - // skip the next character for escaped sequences - ++loc; - } - } - return loc; -} - -CLI11_INLINE std::size_t close_literal_quote(const std::string &str, std::size_t start, char closure_char) { - auto loc = str.find_first_of(closure_char, start + 1); - return (loc != std::string::npos ? loc : str.size()); -} - -CLI11_INLINE std::size_t close_sequence(const std::string &str, std::size_t start, char closure_char) { - - auto bracket_loc = matchBracketChars.find(closure_char); - switch(bracket_loc) { - case 0: - return close_string_quote(str, start, closure_char); - case 1: - case 2: - case std::string::npos: - return close_literal_quote(str, start, closure_char); - default: - break; - } - - std::string closures(1, closure_char); - auto loc = start + 1; - - while(loc < str.size()) { - if(str[loc] == closures.back()) { - closures.pop_back(); - if(closures.empty()) { - return loc; - } - } - bracket_loc = bracketChars.find(str[loc]); - if(bracket_loc != std::string::npos) { - switch(bracket_loc) { - case 0: - loc = close_string_quote(str, loc, str[loc]); - break; - case 1: - case 2: - loc = close_literal_quote(str, loc, str[loc]); - break; - default: - closures.push_back(matchBracketChars[bracket_loc]); - break; - } - } - ++loc; - } - if(loc > str.size()) { - loc = str.size(); - } - return loc; -} - -CLI11_INLINE std::vector split_up(std::string str, char delimiter) { - - auto find_ws = [delimiter](char ch) { - return (delimiter == '\0') ? std::isspace(ch, std::locale()) : (ch == delimiter); - }; - trim(str); - - std::vector output; - while(!str.empty()) { - if(bracketChars.find_first_of(str[0]) != std::string::npos) { - auto bracketLoc = bracketChars.find_first_of(str[0]); - auto end = close_sequence(str, 0, matchBracketChars[bracketLoc]); - if(end >= str.size()) { - output.push_back(std::move(str)); - str.clear(); - } else { - output.push_back(str.substr(0, end + 1)); - if(end + 2 < str.size()) { - str = str.substr(end + 2); - } else { - str.clear(); - } - } - - } else { - auto it = std::find_if(std::begin(str), std::end(str), find_ws); - if(it != std::end(str)) { - std::string value = std::string(str.begin(), it); - output.push_back(value); - str = std::string(it + 1, str.end()); - } else { - output.push_back(str); - str.clear(); - } - } - trim(str); - } - return output; -} - -CLI11_INLINE std::size_t escape_detect(std::string &str, std::size_t offset) { - auto next = str[offset + 1]; - if((next == '\"') || (next == '\'') || (next == '`')) { - auto astart = str.find_last_of("-/ \"\'`", offset - 1); - if(astart != std::string::npos) { - if(str[astart] == ((str[offset] == '=') ? '-' : '/')) - str[offset] = ' '; // interpret this as a space so the split_up works properly - } - } - return offset + 1; -} - -CLI11_INLINE std::string binary_escape_string(const std::string &string_to_escape) { - // s is our escaped output string - std::string escaped_string{}; - // loop through all characters - for(char c : string_to_escape) { - // check if a given character is printable - // the cast is necessary to avoid undefined behaviour - if(isprint(static_cast(c)) == 0) { - std::stringstream stream; - // if the character is not printable - // we'll convert it to a hex string using a stringstream - // note that since char is signed we have to cast it to unsigned first - stream << std::hex << static_cast(static_cast(c)); - std::string code = stream.str(); - escaped_string += std::string("\\x") + (code.size() < 2 ? "0" : "") + code; - } else if(c == 'x' || c == 'X') { - // need to check for inadvertent binary sequences - if(!escaped_string.empty() && escaped_string.back() == '\\') { - escaped_string += std::string("\\x") + (c == 'x' ? "78" : "58"); - } else { - escaped_string.push_back(c); - } - - } else { - escaped_string.push_back(c); - } - } - if(escaped_string != string_to_escape) { - auto sqLoc = escaped_string.find('\''); - while(sqLoc != std::string::npos) { - escaped_string[sqLoc] = '\\'; - escaped_string.insert(sqLoc + 1, "x27"); - sqLoc = escaped_string.find('\''); - } - escaped_string.insert(0, "'B\"("); - escaped_string.push_back(')'); - escaped_string.push_back('"'); - escaped_string.push_back('\''); - } - return escaped_string; -} - -CLI11_INLINE bool is_binary_escaped_string(const std::string &escaped_string) { - size_t ssize = escaped_string.size(); - if(escaped_string.compare(0, 3, "B\"(") == 0 && escaped_string.compare(ssize - 2, 2, ")\"") == 0) { - return true; - } - return (escaped_string.compare(0, 4, "'B\"(") == 0 && escaped_string.compare(ssize - 3, 3, ")\"'") == 0); -} - -CLI11_INLINE std::string extract_binary_string(const std::string &escaped_string) { - std::size_t start{0}; - std::size_t tail{0}; - size_t ssize = escaped_string.size(); - if(escaped_string.compare(0, 3, "B\"(") == 0 && escaped_string.compare(ssize - 2, 2, ")\"") == 0) { - start = 3; - tail = 2; - } else if(escaped_string.compare(0, 4, "'B\"(") == 0 && escaped_string.compare(ssize - 3, 3, ")\"'") == 0) { - start = 4; - tail = 3; - } - - if(start == 0) { - return escaped_string; - } - std::string outstring; - - outstring.reserve(ssize - start - tail); - std::size_t loc = start; - while(loc < ssize - tail) { - // ssize-2 to skip )" at the end - if(escaped_string[loc] == '\\' && (escaped_string[loc + 1] == 'x' || escaped_string[loc + 1] == 'X')) { - auto c1 = escaped_string[loc + 2]; - auto c2 = escaped_string[loc + 3]; - - std::uint32_t res1 = hexConvert(c1); - std::uint32_t res2 = hexConvert(c2); - if(res1 <= 0x0F && res2 <= 0x0F) { - loc += 4; - outstring.push_back(static_cast(res1 * 16 + res2)); - continue; - } - } - outstring.push_back(escaped_string[loc]); - ++loc; - } - return outstring; -} - -CLI11_INLINE void remove_quotes(std::vector &args) { - for(auto &arg : args) { - if(arg.front() == '\"' && arg.back() == '\"') { - remove_quotes(arg); - // only remove escaped for string arguments not literal strings - arg = remove_escaped_characters(arg); - } else { - remove_quotes(arg); - } - } -} - -CLI11_INLINE void handle_secondary_array(std::string &str) { - if(str.size() >= 2 && str.front() == '[' && str.back() == ']') { - // handle some special array processing for arguments if it might be interpreted as a secondary array - std::string tstr{"[["}; - for(std::size_t ii = 1; ii < str.size(); ++ii) { - tstr.push_back(str[ii]); - tstr.push_back(str[ii]); - } - str = std::move(tstr); - } -} - -CLI11_INLINE bool process_quoted_string(std::string &str, char string_char, char literal_char) { - if(str.size() <= 1) { - return false; - } - if(detail::is_binary_escaped_string(str)) { - str = detail::extract_binary_string(str); - handle_secondary_array(str); - return true; - } - if(str.front() == string_char && str.back() == string_char) { - detail::remove_outer(str, string_char); - if(str.find_first_of('\\') != std::string::npos) { - str = detail::remove_escaped_characters(str); - } - handle_secondary_array(str); - return true; - } - if((str.front() == literal_char || str.front() == '`') && str.back() == str.front()) { - detail::remove_outer(str, str.front()); - handle_secondary_array(str); - return true; - } - return false; -} - -std::string get_environment_value(const std::string &env_name) { - char *buffer = nullptr; - std::string ename_string; - -#ifdef _MSC_VER - // Windows version - std::size_t sz = 0; - if(_dupenv_s(&buffer, &sz, env_name.c_str()) == 0 && buffer != nullptr) { - ename_string = std::string(buffer); - free(buffer); - } -#else - // This also works on Windows, but gives a warning - buffer = std::getenv(env_name.c_str()); - if(buffer != nullptr) { - ename_string = std::string(buffer); - } -#endif - return ename_string; -} - -CLI11_INLINE std::ostream &streamOutAsParagraph(std::ostream &out, - const std::string &text, - std::size_t paragraphWidth, - const std::string &linePrefix, - bool skipPrefixOnFirstLine) { - if(!skipPrefixOnFirstLine) - out << linePrefix; // First line prefix - - std::istringstream lss(text); - std::string line = ""; - while(std::getline(lss, line)) { - std::istringstream iss(line); - std::string word = ""; - std::size_t charsWritten = 0; - - while(iss >> word) { - if(word.length() + charsWritten > paragraphWidth) { - out << '\n' << linePrefix; - charsWritten = 0; - } - - out << word << " "; - charsWritten += word.length() + 1; - } - - if(!lss.eof()) - out << '\n' << linePrefix; - } - return out; -} - -} // namespace detail - - - -// Use one of these on all error classes. -// These are temporary and are undef'd at the end of this file. -#define CLI11_ERROR_DEF(parent, name) \ - protected: \ - name(std::string ename, std::string msg, int exit_code) : parent(std::move(ename), std::move(msg), exit_code) {} \ - name(std::string ename, std::string msg, ExitCodes exit_code) \ - : parent(std::move(ename), std::move(msg), exit_code) {} \ - \ - public: \ - name(std::string msg, ExitCodes exit_code) : parent(#name, std::move(msg), exit_code) {} \ - name(std::string msg, int exit_code) : parent(#name, std::move(msg), exit_code) {} - -// This is added after the one above if a class is used directly and builds its own message -#define CLI11_ERROR_SIMPLE(name) \ - explicit name(std::string msg) : name(#name, msg, ExitCodes::name) {} - -/// These codes are part of every error in CLI. They can be obtained from e using e.exit_code or as a quick shortcut, -/// int values from e.get_error_code(). -enum class ExitCodes { - Success = 0, - IncorrectConstruction = 100, - BadNameString, - OptionAlreadyAdded, - FileError, - ConversionError, - ValidationError, - RequiredError, - RequiresError, - ExcludesError, - ExtrasError, - ConfigError, - InvalidError, - HorribleError, - OptionNotFound, - ArgumentMismatch, - BaseClass = 127 -}; - -// Error definitions - -/// @defgroup error_group Errors -/// @brief Errors thrown by CLI11 -/// -/// These are the errors that can be thrown. Some of them, like CLI::Success, are not really errors. -/// @{ - -/// All errors derive from this one -class Error : public std::runtime_error { - int actual_exit_code; - std::string error_name{"Error"}; - - public: - CLI11_NODISCARD int get_exit_code() const { return actual_exit_code; } - - CLI11_NODISCARD std::string get_name() const { return error_name; } - - Error(std::string name, std::string msg, int exit_code = static_cast(ExitCodes::BaseClass)) - : runtime_error(msg), actual_exit_code(exit_code), error_name(std::move(name)) {} - - Error(std::string name, std::string msg, ExitCodes exit_code) : Error(name, msg, static_cast(exit_code)) {} -}; - -// Note: Using Error::Error constructors does not work on GCC 4.7 - -/// Construction errors (not in parsing) -class ConstructionError : public Error { - CLI11_ERROR_DEF(Error, ConstructionError) -}; - -/// Thrown when an option is set to conflicting values (non-vector and multi args, for example) -class IncorrectConstruction : public ConstructionError { - CLI11_ERROR_DEF(ConstructionError, IncorrectConstruction) - CLI11_ERROR_SIMPLE(IncorrectConstruction) - static IncorrectConstruction PositionalFlag(std::string name) { - return IncorrectConstruction(name + ": Flags cannot be positional"); - } - static IncorrectConstruction Set0Opt(std::string name) { - return IncorrectConstruction(name + ": Cannot set 0 expected, use a flag instead"); - } - static IncorrectConstruction SetFlag(std::string name) { - return IncorrectConstruction(name + ": Cannot set an expected number for flags"); - } - static IncorrectConstruction ChangeNotVector(std::string name) { - return IncorrectConstruction(name + ": You can only change the expected arguments for vectors"); - } - static IncorrectConstruction AfterMultiOpt(std::string name) { - return IncorrectConstruction( - name + ": You can't change expected arguments after you've changed the multi option policy!"); - } - static IncorrectConstruction MissingOption(std::string name) { - return IncorrectConstruction("Option " + name + " is not defined"); - } - static IncorrectConstruction MultiOptionPolicy(std::string name) { - return IncorrectConstruction(name + ": multi_option_policy only works for flags and exact value options"); - } -}; - -/// Thrown on construction of a bad name -class BadNameString : public ConstructionError { - CLI11_ERROR_DEF(ConstructionError, BadNameString) - CLI11_ERROR_SIMPLE(BadNameString) - static BadNameString OneCharName(std::string name) { return BadNameString("Invalid one char name: " + name); } - static BadNameString MissingDash(std::string name) { - return BadNameString("Long names strings require 2 dashes " + name); - } - static BadNameString BadLongName(std::string name) { return BadNameString("Bad long name: " + name); } - static BadNameString BadPositionalName(std::string name) { - return BadNameString("Invalid positional Name: " + name); - } - static BadNameString ReservedName(std::string name) { - return BadNameString("Names '-','--','++' are reserved and not allowed as option names " + name); - } - static BadNameString MultiPositionalNames(std::string name) { - return BadNameString("Only one positional name allowed, remove: " + name); - } -}; - -/// Thrown when an option already exists -class OptionAlreadyAdded : public ConstructionError { - CLI11_ERROR_DEF(ConstructionError, OptionAlreadyAdded) - explicit OptionAlreadyAdded(std::string name) - : OptionAlreadyAdded(name + " is already added", ExitCodes::OptionAlreadyAdded) {} - static OptionAlreadyAdded Requires(std::string name, std::string other) { - return {name + " requires " + other, ExitCodes::OptionAlreadyAdded}; - } - static OptionAlreadyAdded Excludes(std::string name, std::string other) { - return {name + " excludes " + other, ExitCodes::OptionAlreadyAdded}; - } -}; - -// Parsing errors - -/// Anything that can error in Parse -class ParseError : public Error { - CLI11_ERROR_DEF(Error, ParseError) -}; - -// Not really "errors" - -/// This is a successful completion on parsing, supposed to exit -class Success : public ParseError { - CLI11_ERROR_DEF(ParseError, Success) - Success() : Success("Successfully completed, should be caught and quit", ExitCodes::Success) {} -}; - -/// -h or --help on command line -class CallForHelp : public Success { - CLI11_ERROR_DEF(Success, CallForHelp) - CallForHelp() : CallForHelp("This should be caught in your main function, see examples", ExitCodes::Success) {} -}; - -/// Usually something like --help-all on command line -class CallForAllHelp : public Success { - CLI11_ERROR_DEF(Success, CallForAllHelp) - CallForAllHelp() - : CallForAllHelp("This should be caught in your main function, see examples", ExitCodes::Success) {} -}; - -/// -v or --version on command line -class CallForVersion : public Success { - CLI11_ERROR_DEF(Success, CallForVersion) - CallForVersion() - : CallForVersion("This should be caught in your main function, see examples", ExitCodes::Success) {} -}; - -/// Does not output a diagnostic in CLI11_PARSE, but allows main() to return with a specific error code. -class RuntimeError : public ParseError { - CLI11_ERROR_DEF(ParseError, RuntimeError) - explicit RuntimeError(int exit_code = 1) : RuntimeError("Runtime error", exit_code) {} -}; - -/// Thrown when parsing an INI file and it is missing -class FileError : public ParseError { - CLI11_ERROR_DEF(ParseError, FileError) - CLI11_ERROR_SIMPLE(FileError) - static FileError Missing(std::string name) { return FileError(name + " was not readable (missing?)"); } -}; - -/// Thrown when conversion call back fails, such as when an int fails to coerce to a string -class ConversionError : public ParseError { - CLI11_ERROR_DEF(ParseError, ConversionError) - CLI11_ERROR_SIMPLE(ConversionError) - ConversionError(std::string member, std::string name) - : ConversionError("The value " + member + " is not an allowed value for " + name) {} - ConversionError(std::string name, std::vector results) - : ConversionError("Could not convert: " + name + " = " + detail::join(results)) {} - static ConversionError TooManyInputsFlag(std::string name) { - return ConversionError(name + ": too many inputs for a flag"); - } - static ConversionError TrueFalse(std::string name) { - return ConversionError(name + ": Should be true/false or a number"); - } -}; - -/// Thrown when validation of results fails -class ValidationError : public ParseError { - CLI11_ERROR_DEF(ParseError, ValidationError) - CLI11_ERROR_SIMPLE(ValidationError) - explicit ValidationError(std::string name, std::string msg) : ValidationError(name + ": " + msg) {} -}; - -/// Thrown when a required option is missing -class RequiredError : public ParseError { - CLI11_ERROR_DEF(ParseError, RequiredError) - explicit RequiredError(std::string name) : RequiredError(name + " is required", ExitCodes::RequiredError) {} - static RequiredError Subcommand(std::size_t min_subcom) { - if(min_subcom == 1) { - return RequiredError("A subcommand"); - } - return {"Requires at least " + std::to_string(min_subcom) + " subcommands", ExitCodes::RequiredError}; - } - static RequiredError - Option(std::size_t min_option, std::size_t max_option, std::size_t used, const std::string &option_list) { - if((min_option == 1) && (max_option == 1) && (used == 0)) - return RequiredError("Exactly 1 option from [" + option_list + "]"); - if((min_option == 1) && (max_option == 1) && (used > 1)) { - return {"Exactly 1 option from [" + option_list + "] is required but " + std::to_string(used) + - " were given", - ExitCodes::RequiredError}; - } - if((min_option == 1) && (used == 0)) - return RequiredError("At least 1 option from [" + option_list + "]"); - if(used < min_option) { - return {"Requires at least " + std::to_string(min_option) + " options used but only " + - std::to_string(used) + " were given from [" + option_list + "]", - ExitCodes::RequiredError}; - } - if(max_option == 1) - return {"Requires at most 1 options be given from [" + option_list + "]", ExitCodes::RequiredError}; - - return {"Requires at most " + std::to_string(max_option) + " options be used but " + std::to_string(used) + - " were given from [" + option_list + "]", - ExitCodes::RequiredError}; - } -}; - -/// Thrown when the wrong number of arguments has been received -class ArgumentMismatch : public ParseError { - CLI11_ERROR_DEF(ParseError, ArgumentMismatch) - CLI11_ERROR_SIMPLE(ArgumentMismatch) - ArgumentMismatch(std::string name, int expected, std::size_t received) - : ArgumentMismatch(expected > 0 ? ("Expected exactly " + std::to_string(expected) + " arguments to " + name + - ", got " + std::to_string(received)) - : ("Expected at least " + std::to_string(-expected) + " arguments to " + name + - ", got " + std::to_string(received)), - ExitCodes::ArgumentMismatch) {} - - static ArgumentMismatch AtLeast(std::string name, int num, std::size_t received) { - return ArgumentMismatch(name + ": At least " + std::to_string(num) + " required but received " + - std::to_string(received)); - } - static ArgumentMismatch AtMost(std::string name, int num, std::size_t received) { - return ArgumentMismatch(name + ": At Most " + std::to_string(num) + " required but received " + - std::to_string(received)); - } - static ArgumentMismatch TypedAtLeast(std::string name, int num, std::string type) { - return ArgumentMismatch(name + ": " + std::to_string(num) + " required " + type + " missing"); - } - static ArgumentMismatch FlagOverride(std::string name) { - return ArgumentMismatch(name + " was given a disallowed flag override"); - } - static ArgumentMismatch PartialType(std::string name, int num, std::string type) { - return ArgumentMismatch(name + ": " + type + " only partially specified: " + std::to_string(num) + - " required for each element"); - } -}; - -/// Thrown when a requires option is missing -class RequiresError : public ParseError { - CLI11_ERROR_DEF(ParseError, RequiresError) - RequiresError(std::string curname, std::string subname) - : RequiresError(curname + " requires " + subname, ExitCodes::RequiresError) {} -}; - -/// Thrown when an excludes option is present -class ExcludesError : public ParseError { - CLI11_ERROR_DEF(ParseError, ExcludesError) - ExcludesError(std::string curname, std::string subname) - : ExcludesError(curname + " excludes " + subname, ExitCodes::ExcludesError) {} -}; - -/// Thrown when too many positionals or options are found -class ExtrasError : public ParseError { - CLI11_ERROR_DEF(ParseError, ExtrasError) - explicit ExtrasError(std::vector args) - : ExtrasError((args.size() > 1 ? "The following arguments were not expected: " - : "The following argument was not expected: ") + - detail::rjoin(args, " "), - ExitCodes::ExtrasError) {} - ExtrasError(const std::string &name, std::vector args) - : ExtrasError(name, - (args.size() > 1 ? "The following arguments were not expected: " - : "The following argument was not expected: ") + - detail::rjoin(args, " "), - ExitCodes::ExtrasError) {} -}; - -/// Thrown when extra values are found in an INI file -class ConfigError : public ParseError { - CLI11_ERROR_DEF(ParseError, ConfigError) - CLI11_ERROR_SIMPLE(ConfigError) - static ConfigError Extras(std::string item) { return ConfigError("INI was not able to parse " + item); } - static ConfigError NotConfigurable(std::string item) { - return ConfigError(item + ": This option is not allowed in a configuration file"); - } -}; - -/// Thrown when validation fails before parsing -class InvalidError : public ParseError { - CLI11_ERROR_DEF(ParseError, InvalidError) - explicit InvalidError(std::string name) - : InvalidError(name + ": Too many positional arguments with unlimited expected args", ExitCodes::InvalidError) { - } -}; - -/// This is just a safety check to verify selection and parsing match - you should not ever see it -/// Strings are directly added to this error, but again, it should never be seen. -class HorribleError : public ParseError { - CLI11_ERROR_DEF(ParseError, HorribleError) - CLI11_ERROR_SIMPLE(HorribleError) -}; - -// After parsing - -/// Thrown when counting a nonexistent option -class OptionNotFound : public Error { - CLI11_ERROR_DEF(Error, OptionNotFound) - explicit OptionNotFound(std::string name) : OptionNotFound(name + " not found", ExitCodes::OptionNotFound) {} -}; - -#undef CLI11_ERROR_DEF -#undef CLI11_ERROR_SIMPLE - -/// @} - - - - -// Type tools - -// Utilities for type enabling -namespace detail { -// Based generally on https://rmf.io/cxx11/almost-static-if -/// Simple empty scoped class -enum class enabler {}; - -/// An instance to use in EnableIf -constexpr enabler dummy = {}; -} // namespace detail - -/// A copy of enable_if_t from C++14, compatible with C++11. -/// -/// We could check to see if C++14 is being used, but it does not hurt to redefine this -/// (even Google does this: https://github.com/google/skia/blob/main/include/private/SkTLogic.h) -/// It is not in the std namespace anyway, so no harm done. -template using enable_if_t = typename std::enable_if::type; - -/// A copy of std::void_t from C++17 (helper for C++11 and C++14) -template struct make_void { - using type = void; -}; - -/// A copy of std::void_t from C++17 - same reasoning as enable_if_t, it does not hurt to redefine -template using void_t = typename make_void::type; - -/// A copy of std::conditional_t from C++14 - same reasoning as enable_if_t, it does not hurt to redefine -template using conditional_t = typename std::conditional::type; - -/// Check to see if something is bool (fail check by default) -template struct is_bool : std::false_type {}; - -/// Check to see if something is bool (true if actually a bool) -template <> struct is_bool : std::true_type {}; - -/// Check to see if something is a shared pointer -template struct is_shared_ptr : std::false_type {}; - -/// Check to see if something is a shared pointer (True if really a shared pointer) -template struct is_shared_ptr> : std::true_type {}; - -/// Check to see if something is a shared pointer (True if really a shared pointer) -template struct is_shared_ptr> : std::true_type {}; - -/// Check to see if something is copyable pointer -template struct is_copyable_ptr { - static bool const value = is_shared_ptr::value || std::is_pointer::value; -}; - -/// This can be specialized to override the type deduction for IsMember. -template struct IsMemberType { - using type = T; -}; - -/// The main custom type needed here is const char * should be a string. -template <> struct IsMemberType { - using type = std::string; -}; - -namespace adl_detail { -/// Check for existence of user-supplied lexical_cast. -/// -/// This struct has to be in a separate namespace so that it doesn't see our lexical_cast overloads in CLI::detail. -/// Standard says it shouldn't see them if it's defined before the corresponding lexical_cast declarations, but this -/// requires a working implementation of two-phase lookup, and not all compilers can boast that (msvc, ahem). -template class is_lexical_castable { - template - static auto test(int) -> decltype(lexical_cast(std::declval(), std::declval()), std::true_type()); - - template static auto test(...) -> std::false_type; - - public: - static constexpr bool value = decltype(test(0))::value; -}; -} // namespace adl_detail - -namespace detail { - -// These are utilities for IsMember and other transforming objects - -/// Handy helper to access the element_type generically. This is not part of is_copyable_ptr because it requires that -/// pointer_traits be valid. - -/// not a pointer -template struct element_type { - using type = T; -}; - -template struct element_type::value>::type> { - using type = typename std::pointer_traits::element_type; -}; - -/// Combination of the element type and value type - remove pointer (including smart pointers) and get the value_type of -/// the container -template struct element_value_type { - using type = typename element_type::type::value_type; -}; - -/// Adaptor for set-like structure: This just wraps a normal container in a few utilities that do almost nothing. -template struct pair_adaptor : std::false_type { - using value_type = typename T::value_type; - using first_type = typename std::remove_const::type; - using second_type = typename std::remove_const::type; - - /// Get the first value (really just the underlying value) - template static auto first(Q &&pair_value) -> decltype(std::forward(pair_value)) { - return std::forward(pair_value); - } - /// Get the second value (really just the underlying value) - template static auto second(Q &&pair_value) -> decltype(std::forward(pair_value)) { - return std::forward(pair_value); - } -}; - -/// Adaptor for map-like structure (true version, must have key_type and mapped_type). -/// This wraps a mapped container in a few utilities access it in a general way. -template -struct pair_adaptor< - T, - conditional_t, void>> - : std::true_type { - using value_type = typename T::value_type; - using first_type = typename std::remove_const::type; - using second_type = typename std::remove_const::type; - - /// Get the first value (really just the underlying value) - template static auto first(Q &&pair_value) -> decltype(std::get<0>(std::forward(pair_value))) { - return std::get<0>(std::forward(pair_value)); - } - /// Get the second value (really just the underlying value) - template static auto second(Q &&pair_value) -> decltype(std::get<1>(std::forward(pair_value))) { - return std::get<1>(std::forward(pair_value)); - } -}; - -// Warning is suppressed due to "bug" in gcc<5.0 and gcc 7.0 with c++17 enabled that generates a -Wnarrowing warning -// in the unevaluated context even if the function that was using this wasn't used. The standard says narrowing in -// brace initialization shouldn't be allowed but for backwards compatibility gcc allows it in some contexts. It is a -// little fuzzy what happens in template constructs and I think that was something GCC took a little while to work out. -// But regardless some versions of gcc generate a warning when they shouldn't from the following code so that should be -// suppressed -#ifdef __GNUC__ -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wnarrowing" -#endif -// check for constructibility from a specific type and copy assignable used in the parse detection -template class is_direct_constructible { - template - static auto test(int, std::true_type) -> decltype( -// NVCC warns about narrowing conversions here -#ifdef __CUDACC__ -#ifdef __NVCC_DIAG_PRAGMA_SUPPORT__ -#pragma nv_diag_suppress 2361 -#else -#pragma diag_suppress 2361 -#endif -#endif - TT{std::declval()} -#ifdef __CUDACC__ -#ifdef __NVCC_DIAG_PRAGMA_SUPPORT__ -#pragma nv_diag_default 2361 -#else -#pragma diag_default 2361 -#endif -#endif - , - std::is_move_assignable()); - - template static auto test(int, std::false_type) -> std::false_type; - - template static auto test(...) -> std::false_type; - - public: - static constexpr bool value = decltype(test(0, typename std::is_constructible::type()))::value; -}; -#ifdef __GNUC__ -#pragma GCC diagnostic pop -#endif - -// Check for output streamability -// Based on https://stackoverflow.com/questions/22758291/how-can-i-detect-if-a-type-can-be-streamed-to-an-stdostream - -template class is_ostreamable { - template - static auto test(int) -> decltype(std::declval() << std::declval(), std::true_type()); - - template static auto test(...) -> std::false_type; - - public: - static constexpr bool value = decltype(test(0))::value; -}; - -/// Check for input streamability -template class is_istreamable { - template - static auto test(int) -> decltype(std::declval() >> std::declval(), std::true_type()); - - template static auto test(...) -> std::false_type; - - public: - static constexpr bool value = decltype(test(0))::value; -}; - -/// Check for complex -template class is_complex { - template - static auto test(int) -> decltype(std::declval().real(), std::declval().imag(), std::true_type()); - - template static auto test(...) -> std::false_type; - - public: - static constexpr bool value = decltype(test(0))::value; -}; - -/// Templated operation to get a value from a stream -template ::value, detail::enabler> = detail::dummy> -bool from_stream(const std::string &istring, T &obj) { - std::istringstream is; - is.str(istring); - is >> obj; - return !is.fail() && !is.rdbuf()->in_avail(); -} - -template ::value, detail::enabler> = detail::dummy> -bool from_stream(const std::string & /*istring*/, T & /*obj*/) { - return false; -} - -// check to see if an object is a mutable container (fail by default) -template struct is_mutable_container : std::false_type {}; - -/// type trait to test if a type is a mutable container meaning it has a value_type, it has an iterator, a clear, and -/// end methods and an insert function. And for our purposes we exclude std::string and types that can be constructed -/// from a std::string -template -struct is_mutable_container< - T, - conditional_t().end()), - decltype(std::declval().clear()), - decltype(std::declval().insert(std::declval().end())>(), - std::declval()))>, - void>> : public conditional_t::value || - std::is_constructible::value, - std::false_type, - std::true_type> {}; - -// check to see if an object is a mutable container (fail by default) -template struct is_readable_container : std::false_type {}; - -/// type trait to test if a type is a container meaning it has a value_type, it has an iterator, and an end -/// method. -template -struct is_readable_container< - T, - conditional_t().end()), decltype(std::declval().begin())>, void>> - : public std::true_type {}; - -// check to see if an object is a wrapper (fail by default) -template struct is_wrapper : std::false_type {}; - -// check if an object is a wrapper (it has a value_type defined) -template -struct is_wrapper, void>> : public std::true_type {}; - -// Check for tuple like types, as in classes with a tuple_size type trait -// Even though in C++26 std::complex gains a std::tuple interface, for our purposes we treat is as NOT a tuple -template class is_tuple_like { - template ::value, detail::enabler> = detail::dummy> - // static auto test(int) - // -> decltype(std::conditional<(std::tuple_size::value > 0), std::true_type, std::false_type>::type()); - static auto test(int) -> decltype(std::tuple_size::type>::value, std::true_type{}); - template static auto test(...) -> std::false_type; - - public: - static constexpr bool value = decltype(test(0))::value; -}; - -/// This will only trigger for actual void type -template struct type_count_base { - static const int value{0}; -}; - -/// Type size for regular object types that do not look like a tuple -template -struct type_count_base::value && !is_mutable_container::value && - !std::is_void::value>::type> { - static constexpr int value{1}; -}; - -/// the base tuple size -template -struct type_count_base::value && !is_mutable_container::value>::type> { - static constexpr int value{// cppcheck-suppress unusedStructMember - std::tuple_size::type>::value}; -}; - -/// Type count base for containers is the type_count_base of the individual element -template struct type_count_base::value>::type> { - static constexpr int value{type_count_base::value}; -}; - -/// Convert an object to a string (directly forward if this can become a string) -template ::value, detail::enabler> = detail::dummy> -auto to_string(T &&value) -> decltype(std::forward(value)) { - return std::forward(value); -} - -/// Construct a string from the object -template ::value && !std::is_convertible::value, - detail::enabler> = detail::dummy> -std::string to_string(T &&value) { - return std::string(value); // NOLINT(google-readability-casting) -} - -/// Convert an object to a string (streaming must be supported for that type) -template ::value && !std::is_constructible::value && - is_ostreamable::value, - detail::enabler> = detail::dummy> -std::string to_string(T &&value) { - std::stringstream stream; - stream << value; - return stream.str(); -} - -// additional forward declarations - -/// Print tuple value string for tuples of size ==1 -template ::value && !std::is_constructible::value && - !is_ostreamable::value && is_tuple_like::value && type_count_base::value == 1, - detail::enabler> = detail::dummy> -inline std::string to_string(T &&value); - -/// Print tuple value string for tuples of size > 1 -template ::value && !std::is_constructible::value && - !is_ostreamable::value && is_tuple_like::value && type_count_base::value >= 2, - detail::enabler> = detail::dummy> -inline std::string to_string(T &&value); - -/// If conversion is not supported, return an empty string (streaming is not supported for that type) -template < - typename T, - enable_if_t::value && !std::is_constructible::value && - !is_ostreamable::value && !is_readable_container::type>::value && - !is_tuple_like::value, - detail::enabler> = detail::dummy> -inline std::string to_string(T &&) { - return {}; -} - -/// convert a readable container to a string -template ::value && !std::is_constructible::value && - !is_ostreamable::value && is_readable_container::value, - detail::enabler> = detail::dummy> -inline std::string to_string(T &&variable) { - auto cval = variable.begin(); - auto end = variable.end(); - if(cval == end) { - return {"{}"}; - } - std::vector defaults; - while(cval != end) { - defaults.emplace_back(CLI::detail::to_string(*cval)); - ++cval; - } - return {"[" + detail::join(defaults) + "]"}; -} - -/// Convert a tuple like object to a string - -/// forward declarations for tuple_value_strings -template -inline typename std::enable_if::value, std::string>::type tuple_value_string(T && /*value*/); - -/// Recursively generate the tuple value string -template -inline typename std::enable_if<(I < type_count_base::value), std::string>::type tuple_value_string(T &&value); - -/// Print tuple value string for tuples of size ==1 -template ::value && !std::is_constructible::value && - !is_ostreamable::value && is_tuple_like::value && type_count_base::value == 1, - detail::enabler>> -inline std::string to_string(T &&value) { - return to_string(std::get<0>(value)); -} - -/// Print tuple value string for tuples of size > 1 -template ::value && !std::is_constructible::value && - !is_ostreamable::value && is_tuple_like::value && type_count_base::value >= 2, - detail::enabler>> -inline std::string to_string(T &&value) { - auto tname = std::string(1, '[') + tuple_value_string(value); - tname.push_back(']'); - return tname; -} - -/// Empty string if the index > tuple size -template -inline typename std::enable_if::value, std::string>::type tuple_value_string(T && /*value*/) { - return std::string{}; -} - -/// Recursively generate the tuple value string -template -inline typename std::enable_if<(I < type_count_base::value), std::string>::type tuple_value_string(T &&value) { - auto str = std::string{to_string(std::get(value))} + ',' + tuple_value_string(value); - if(str.back() == ',') - str.pop_back(); - return str; -} - -/// special template overload -template ::value, detail::enabler> = detail::dummy> -auto checked_to_string(T &&value) -> decltype(to_string(std::forward(value))) { - return to_string(std::forward(value)); -} - -/// special template overload -template ::value, detail::enabler> = detail::dummy> -std::string checked_to_string(T &&) { - return std::string{}; -} -/// get a string as a convertible value for arithmetic types -template ::value, detail::enabler> = detail::dummy> -std::string value_string(const T &value) { - return std::to_string(value); -} -/// get a string as a convertible value for enumerations -template ::value, detail::enabler> = detail::dummy> -std::string value_string(const T &value) { - return std::to_string(static_cast::type>(value)); -} -/// for other types just use the regular to_string function -template ::value && !std::is_arithmetic::value, detail::enabler> = detail::dummy> -auto value_string(const T &value) -> decltype(to_string(value)) { - return to_string(value); -} - -/// template to get the underlying value type if it exists or use a default -template struct wrapped_type { - using type = def; -}; - -/// Type size for regular object types that do not look like a tuple -template struct wrapped_type::value>::type> { - using type = typename T::value_type; -}; - -/// Set of overloads to get the type size of an object - -/// forward declare the subtype_count structure -template struct subtype_count; - -/// forward declare the subtype_count_min structure -template struct subtype_count_min; - -/// This will only trigger for actual void type -template struct type_count { - static const int value{0}; -}; - -/// Type size for regular object types that do not look like a tuple -template -struct type_count::value && !is_tuple_like::value && !is_complex::value && - !std::is_void::value>::type> { - static constexpr int value{1}; -}; - -/// Type size for complex since it sometimes looks like a wrapper -template struct type_count::value>::type> { - static constexpr int value{2}; -}; - -/// Type size of types that are wrappers,except complex and tuples(which can also be wrappers sometimes) -template struct type_count::value>::type> { - static constexpr int value{subtype_count::value}; -}; - -/// Type size of types that are wrappers,except containers complex and tuples(which can also be wrappers sometimes) -template -struct type_count::value && !is_complex::value && !is_tuple_like::value && - !is_mutable_container::value>::type> { - static constexpr int value{type_count::value}; -}; - -/// 0 if the index > tuple size -template -constexpr typename std::enable_if::value, int>::type tuple_type_size() { - return 0; -} - -/// Recursively generate the tuple type name -template - constexpr typename std::enable_if < I::value, int>::type tuple_type_size() { - return subtype_count::type>::value + tuple_type_size(); -} - -/// Get the type size of the sum of type sizes for all the individual tuple types -template struct type_count::value>::type> { - static constexpr int value{tuple_type_size()}; -}; - -/// definition of subtype count -template struct subtype_count { - static constexpr int value{is_mutable_container::value ? expected_max_vector_size : type_count::value}; -}; - -/// This will only trigger for actual void type -template struct type_count_min { - static const int value{0}; -}; - -/// Type size for regular object types that do not look like a tuple -template -struct type_count_min< - T, - typename std::enable_if::value && !is_tuple_like::value && !is_wrapper::value && - !is_complex::value && !std::is_void::value>::type> { - static constexpr int value{type_count::value}; -}; - -/// Type size for complex since it sometimes looks like a wrapper -template struct type_count_min::value>::type> { - static constexpr int value{1}; -}; - -/// Type size min of types that are wrappers,except complex and tuples(which can also be wrappers sometimes) -template -struct type_count_min< - T, - typename std::enable_if::value && !is_complex::value && !is_tuple_like::value>::type> { - static constexpr int value{subtype_count_min::value}; -}; - -/// 0 if the index > tuple size -template -constexpr typename std::enable_if::value, int>::type tuple_type_size_min() { - return 0; -} - -/// Recursively generate the tuple type name -template - constexpr typename std::enable_if < I::value, int>::type tuple_type_size_min() { - return subtype_count_min::type>::value + tuple_type_size_min(); -} - -/// Get the type size of the sum of type sizes for all the individual tuple types -template struct type_count_min::value>::type> { - static constexpr int value{tuple_type_size_min()}; -}; - -/// definition of subtype count -template struct subtype_count_min { - static constexpr int value{is_mutable_container::value - ? ((type_count::value < expected_max_vector_size) ? type_count::value : 0) - : type_count_min::value}; -}; - -/// This will only trigger for actual void type -template struct expected_count { - static const int value{0}; -}; - -/// For most types the number of expected items is 1 -template -struct expected_count::value && !is_wrapper::value && - !std::is_void::value>::type> { - static constexpr int value{1}; -}; -/// number of expected items in a vector -template struct expected_count::value>::type> { - static constexpr int value{expected_max_vector_size}; -}; - -/// number of expected items in a vector -template -struct expected_count::value && is_wrapper::value>::type> { - static constexpr int value{expected_count::value}; -}; - -// Enumeration of the different supported categorizations of objects -enum class object_category : int { - char_value = 1, - integral_value = 2, - unsigned_integral = 4, - enumeration = 6, - boolean_value = 8, - floating_point = 10, - number_constructible = 12, - double_constructible = 14, - integer_constructible = 16, - // string like types - string_assignable = 23, - string_constructible = 24, - wstring_assignable = 25, - wstring_constructible = 26, - other = 45, - // special wrapper or container types - wrapper_value = 50, - complex_number = 60, - tuple_value = 70, - container_value = 80, - -}; - -/// Set of overloads to classify an object according to type - -/// some type that is not otherwise recognized -template struct classify_object { - static constexpr object_category value{object_category::other}; -}; - -/// Signed integers -template -struct classify_object< - T, - typename std::enable_if::value && !std::is_same::value && std::is_signed::value && - !is_bool::value && !std::is_enum::value>::type> { - static constexpr object_category value{object_category::integral_value}; -}; - -/// Unsigned integers -template -struct classify_object::value && std::is_unsigned::value && - !std::is_same::value && !is_bool::value>::type> { - static constexpr object_category value{object_category::unsigned_integral}; -}; - -/// single character values -template -struct classify_object::value && !std::is_enum::value>::type> { - static constexpr object_category value{object_category::char_value}; -}; - -/// Boolean values -template struct classify_object::value>::type> { - static constexpr object_category value{object_category::boolean_value}; -}; - -/// Floats -template struct classify_object::value>::type> { - static constexpr object_category value{object_category::floating_point}; -}; -#if defined _MSC_VER -// in MSVC wstring should take precedence if available this isn't as useful on other compilers due to the broader use of -// utf-8 encoding -#define WIDE_STRING_CHECK \ - !std::is_assignable::value && !std::is_constructible::value -#define STRING_CHECK true -#else -#define WIDE_STRING_CHECK true -#define STRING_CHECK !std::is_assignable::value && !std::is_constructible::value -#endif - -/// String and similar direct assignment -template -struct classify_object< - T, - typename std::enable_if::value && !std::is_integral::value && WIDE_STRING_CHECK && - std::is_assignable::value>::type> { - static constexpr object_category value{object_category::string_assignable}; -}; - -/// String and similar constructible and copy assignment -template -struct classify_object< - T, - typename std::enable_if::value && !std::is_integral::value && - !std::is_assignable::value && (type_count::value == 1) && - WIDE_STRING_CHECK && std::is_constructible::value>::type> { - static constexpr object_category value{object_category::string_constructible}; -}; - -/// Wide strings -template -struct classify_object::value && !std::is_integral::value && - STRING_CHECK && std::is_assignable::value>::type> { - static constexpr object_category value{object_category::wstring_assignable}; -}; - -template -struct classify_object< - T, - typename std::enable_if::value && !std::is_integral::value && - !std::is_assignable::value && (type_count::value == 1) && - STRING_CHECK && std::is_constructible::value>::type> { - static constexpr object_category value{object_category::wstring_constructible}; -}; - -/// Enumerations -template struct classify_object::value>::type> { - static constexpr object_category value{object_category::enumeration}; -}; - -template struct classify_object::value>::type> { - static constexpr object_category value{object_category::complex_number}; -}; - -/// Handy helper to contain a bunch of checks that rule out many common types (integers, string like, floating point, -/// vectors, and enumerations -template struct uncommon_type { - using type = typename std::conditional< - !std::is_floating_point::value && !std::is_integral::value && - !std::is_assignable::value && !std::is_constructible::value && - !std::is_assignable::value && !std::is_constructible::value && - !is_complex::value && !is_mutable_container::value && !std::is_enum::value, - std::true_type, - std::false_type>::type; - static constexpr bool value = type::value; -}; - -/// wrapper type -template -struct classify_object::value && is_wrapper::value && - !is_tuple_like::value && uncommon_type::value)>::type> { - static constexpr object_category value{object_category::wrapper_value}; -}; - -/// Assignable from double or int -template -struct classify_object::value && type_count::value == 1 && - !is_wrapper::value && is_direct_constructible::value && - is_direct_constructible::value>::type> { - static constexpr object_category value{object_category::number_constructible}; -}; - -/// Assignable from int -template -struct classify_object::value && type_count::value == 1 && - !is_wrapper::value && !is_direct_constructible::value && - is_direct_constructible::value>::type> { - static constexpr object_category value{object_category::integer_constructible}; -}; - -/// Assignable from double -template -struct classify_object::value && type_count::value == 1 && - !is_wrapper::value && is_direct_constructible::value && - !is_direct_constructible::value>::type> { - static constexpr object_category value{object_category::double_constructible}; -}; - -/// Tuple type -template -struct classify_object< - T, - typename std::enable_if::value && - ((type_count::value >= 2 && !is_wrapper::value) || - (uncommon_type::value && !is_direct_constructible::value && - !is_direct_constructible::value) || - (uncommon_type::value && type_count::value >= 2))>::type> { - static constexpr object_category value{object_category::tuple_value}; - // the condition on this class requires it be like a tuple, but on some compilers (like Xcode) tuples can be - // constructed from just the first element so tuples of can be constructed from a string, which - // could lead to issues so there are two variants of the condition, the first isolates things with a type size >=2 - // mainly to get tuples on Xcode with the exception of wrappers, the second is the main one and just separating out - // those cases that are caught by other object classifications -}; - -/// container type -template struct classify_object::value>::type> { - static constexpr object_category value{object_category::container_value}; -}; - -// Type name print - -/// Was going to be based on -/// http://stackoverflow.com/questions/1055452/c-get-name-of-type-in-template -/// But this is cleaner and works better in this case - -template ::value == object_category::char_value, detail::enabler> = detail::dummy> -constexpr const char *type_name() { - return "CHAR"; -} - -template ::value == object_category::integral_value || - classify_object::value == object_category::integer_constructible, - detail::enabler> = detail::dummy> -constexpr const char *type_name() { - return "INT"; -} - -template ::value == object_category::unsigned_integral, detail::enabler> = detail::dummy> -constexpr const char *type_name() { - return "UINT"; -} - -template ::value == object_category::floating_point || - classify_object::value == object_category::number_constructible || - classify_object::value == object_category::double_constructible, - detail::enabler> = detail::dummy> -constexpr const char *type_name() { - return "FLOAT"; -} - -/// Print name for enumeration types -template ::value == object_category::enumeration, detail::enabler> = detail::dummy> -constexpr const char *type_name() { - return "ENUM"; -} - -/// Print name for enumeration types -template ::value == object_category::boolean_value, detail::enabler> = detail::dummy> -constexpr const char *type_name() { - return "BOOLEAN"; -} - -/// Print name for enumeration types -template ::value == object_category::complex_number, detail::enabler> = detail::dummy> -constexpr const char *type_name() { - return "COMPLEX"; -} - -/// Print for all other types -template ::value >= object_category::string_assignable && - classify_object::value <= object_category::other, - detail::enabler> = detail::dummy> -constexpr const char *type_name() { - return "TEXT"; -} -/// typename for tuple value -template ::value == object_category::tuple_value && type_count_base::value >= 2, - detail::enabler> = detail::dummy> -std::string type_name(); // forward declaration - -/// Generate type name for a wrapper or container value -template ::value == object_category::container_value || - classify_object::value == object_category::wrapper_value, - detail::enabler> = detail::dummy> -std::string type_name(); // forward declaration - -/// Print name for single element tuple types -template ::value == object_category::tuple_value && type_count_base::value == 1, - detail::enabler> = detail::dummy> -inline std::string type_name() { - return type_name::type>::type>(); -} - -/// Empty string if the index > tuple size -template -inline typename std::enable_if::value, std::string>::type tuple_name() { - return std::string{}; -} - -/// Recursively generate the tuple type name -template -inline typename std::enable_if<(I < type_count_base::value), std::string>::type tuple_name() { - auto str = std::string{type_name::type>::type>()} + ',' + - tuple_name(); - if(str.back() == ',') - str.pop_back(); - return str; -} - -/// Print type name for tuples with 2 or more elements -template ::value == object_category::tuple_value && type_count_base::value >= 2, - detail::enabler>> -inline std::string type_name() { - auto tname = std::string(1, '[') + tuple_name(); - tname.push_back(']'); - return tname; -} - -/// get the type name for a type that has a value_type member -template ::value == object_category::container_value || - classify_object::value == object_category::wrapper_value, - detail::enabler>> -inline std::string type_name() { - return type_name(); -} - -// Lexical cast - -/// Convert to an unsigned integral -template ::value, detail::enabler> = detail::dummy> -bool integral_conversion(const std::string &input, T &output) noexcept { - if(input.empty() || input.front() == '-') { - return false; - } - char *val{nullptr}; - errno = 0; - std::uint64_t output_ll = std::strtoull(input.c_str(), &val, 0); - if(errno == ERANGE) { - return false; - } - output = static_cast(output_ll); - if(val == (input.c_str() + input.size()) && static_cast(output) == output_ll) { - return true; - } - val = nullptr; - std::int64_t output_sll = std::strtoll(input.c_str(), &val, 0); - if(val == (input.c_str() + input.size())) { - output = (output_sll < 0) ? static_cast(0) : static_cast(output_sll); - return (static_cast(output) == output_sll); - } - // remove separators - if(input.find_first_of("_'") != std::string::npos) { - std::string nstring = input; - nstring.erase(std::remove(nstring.begin(), nstring.end(), '_'), nstring.end()); - nstring.erase(std::remove(nstring.begin(), nstring.end(), '\''), nstring.end()); - return integral_conversion(nstring, output); - } - if(std::isspace(static_cast(input.back()))) { - return integral_conversion(trim_copy(input), output); - } - if(input.compare(0, 2, "0o") == 0 || input.compare(0, 2, "0O") == 0) { - val = nullptr; - errno = 0; - output_ll = std::strtoull(input.c_str() + 2, &val, 8); - if(errno == ERANGE) { - return false; - } - output = static_cast(output_ll); - return (val == (input.c_str() + input.size()) && static_cast(output) == output_ll); - } - if(input.compare(0, 2, "0b") == 0 || input.compare(0, 2, "0B") == 0) { - // LCOV_EXCL_START - // In some new compilers including the coverage testing one binary strings are handled properly in strtoull - // automatically so this coverage is missing but is well tested in other compilers - val = nullptr; - errno = 0; - output_ll = std::strtoull(input.c_str() + 2, &val, 2); - if(errno == ERANGE) { - return false; - } - output = static_cast(output_ll); - return (val == (input.c_str() + input.size()) && static_cast(output) == output_ll); - // LCOV_EXCL_STOP - } - return false; -} - -/// Convert to a signed integral -template ::value, detail::enabler> = detail::dummy> -bool integral_conversion(const std::string &input, T &output) noexcept { - if(input.empty()) { - return false; - } - char *val = nullptr; - errno = 0; - std::int64_t output_ll = std::strtoll(input.c_str(), &val, 0); - if(errno == ERANGE) { - return false; - } - output = static_cast(output_ll); - if(val == (input.c_str() + input.size()) && static_cast(output) == output_ll) { - return true; - } - if(input == "true") { - // this is to deal with a few oddities with flags and wrapper int types - output = static_cast(1); - return true; - } - // remove separators and trailing spaces - if(input.find_first_of("_'") != std::string::npos) { - std::string nstring = input; - nstring.erase(std::remove(nstring.begin(), nstring.end(), '_'), nstring.end()); - nstring.erase(std::remove(nstring.begin(), nstring.end(), '\''), nstring.end()); - return integral_conversion(nstring, output); - } - if(std::isspace(static_cast(input.back()))) { - return integral_conversion(trim_copy(input), output); - } - if(input.compare(0, 2, "0o") == 0 || input.compare(0, 2, "0O") == 0) { - val = nullptr; - errno = 0; - output_ll = std::strtoll(input.c_str() + 2, &val, 8); - if(errno == ERANGE) { - return false; - } - output = static_cast(output_ll); - return (val == (input.c_str() + input.size()) && static_cast(output) == output_ll); - } - if(input.compare(0, 2, "0b") == 0 || input.compare(0, 2, "0B") == 0) { - // LCOV_EXCL_START - // In some new compilers including the coverage testing one binary strings are handled properly in strtoll - // automatically so this coverage is missing but is well tested in other compilers - val = nullptr; - errno = 0; - output_ll = std::strtoll(input.c_str() + 2, &val, 2); - if(errno == ERANGE) { - return false; - } - output = static_cast(output_ll); - return (val == (input.c_str() + input.size()) && static_cast(output) == output_ll); - // LCOV_EXCL_STOP - } - return false; -} - -/// Convert a flag into an integer value typically binary flags sets errno to nonzero if conversion failed -inline std::int64_t to_flag_value(std::string val) noexcept { - static const std::string trueString("true"); - static const std::string falseString("false"); - if(val == trueString) { - return 1; - } - if(val == falseString) { - return -1; - } - val = detail::to_lower(val); - std::int64_t ret = 0; - if(val.size() == 1) { - if(val[0] >= '1' && val[0] <= '9') { - return (static_cast(val[0]) - '0'); - } - switch(val[0]) { - case '0': - case 'f': - case 'n': - case '-': - ret = -1; - break; - case 't': - case 'y': - case '+': - ret = 1; - break; - default: - errno = EINVAL; - return -1; - } - return ret; - } - if(val == trueString || val == "on" || val == "yes" || val == "enable") { - ret = 1; - } else if(val == falseString || val == "off" || val == "no" || val == "disable") { - ret = -1; - } else { - char *loc_ptr{nullptr}; - ret = std::strtoll(val.c_str(), &loc_ptr, 0); - if(loc_ptr != (val.c_str() + val.size()) && errno == 0) { - errno = EINVAL; - } - } - return ret; -} - -/// Integer conversion -template ::value == object_category::integral_value || - classify_object::value == object_category::unsigned_integral, - detail::enabler> = detail::dummy> -bool lexical_cast(const std::string &input, T &output) { - return integral_conversion(input, output); -} - -/// char values -template ::value == object_category::char_value, detail::enabler> = detail::dummy> -bool lexical_cast(const std::string &input, T &output) { - if(input.size() == 1) { - output = static_cast(input[0]); - return true; - } - return integral_conversion(input, output); -} - -/// Boolean values -template ::value == object_category::boolean_value, detail::enabler> = detail::dummy> -bool lexical_cast(const std::string &input, T &output) { - errno = 0; - auto out = to_flag_value(input); - if(errno == 0) { - output = (out > 0); - } else if(errno == ERANGE) { - output = (input[0] != '-'); - } else { - return false; - } - return true; -} - -/// Floats -template ::value == object_category::floating_point, detail::enabler> = detail::dummy> -bool lexical_cast(const std::string &input, T &output) { - if(input.empty()) { - return false; - } - char *val = nullptr; - auto output_ld = std::strtold(input.c_str(), &val); - output = static_cast(output_ld); - if(val == (input.c_str() + input.size())) { - return true; - } - while(std::isspace(static_cast(*val))) { - ++val; - if(val == (input.c_str() + input.size())) { - return true; - } - } - - // remove separators - if(input.find_first_of("_'") != std::string::npos) { - std::string nstring = input; - nstring.erase(std::remove(nstring.begin(), nstring.end(), '_'), nstring.end()); - nstring.erase(std::remove(nstring.begin(), nstring.end(), '\''), nstring.end()); - return lexical_cast(nstring, output); - } - return false; -} - -/// complex -template ::value == object_category::complex_number, detail::enabler> = detail::dummy> -bool lexical_cast(const std::string &input, T &output) { - using XC = typename wrapped_type::type; - XC x{0.0}, y{0.0}; - auto str1 = input; - bool worked = false; - auto nloc = str1.find_last_of("+-"); - if(nloc != std::string::npos && nloc > 0) { - worked = lexical_cast(str1.substr(0, nloc), x); - str1 = str1.substr(nloc); - if(str1.back() == 'i' || str1.back() == 'j') - str1.pop_back(); - worked = worked && lexical_cast(str1, y); - } else { - if(str1.back() == 'i' || str1.back() == 'j') { - str1.pop_back(); - worked = lexical_cast(str1, y); - x = XC{0}; - } else { - worked = lexical_cast(str1, x); - y = XC{0}; - } - } - if(worked) { - output = T{x, y}; - return worked; - } - return from_stream(input, output); -} - -/// String and similar direct assignment -template ::value == object_category::string_assignable, detail::enabler> = detail::dummy> -bool lexical_cast(const std::string &input, T &output) { - output = input; - return true; -} - -/// String and similar constructible and copy assignment -template < - typename T, - enable_if_t::value == object_category::string_constructible, detail::enabler> = detail::dummy> -bool lexical_cast(const std::string &input, T &output) { - output = T(input); - return true; -} - -/// Wide strings -template < - typename T, - enable_if_t::value == object_category::wstring_assignable, detail::enabler> = detail::dummy> -bool lexical_cast(const std::string &input, T &output) { - output = widen(input); - return true; -} - -template < - typename T, - enable_if_t::value == object_category::wstring_constructible, detail::enabler> = detail::dummy> -bool lexical_cast(const std::string &input, T &output) { - output = T{widen(input)}; - return true; -} - -/// Enumerations -template ::value == object_category::enumeration, detail::enabler> = detail::dummy> -bool lexical_cast(const std::string &input, T &output) { - typename std::underlying_type::type val; - if(!integral_conversion(input, val)) { - return false; - } - output = static_cast(val); - return true; -} - -/// wrapper types -template ::value == object_category::wrapper_value && - std::is_assignable::value, - detail::enabler> = detail::dummy> -bool lexical_cast(const std::string &input, T &output) { - typename T::value_type val; - if(lexical_cast(input, val)) { - output = val; - return true; - } - return from_stream(input, output); -} - -template ::value == object_category::wrapper_value && - !std::is_assignable::value && std::is_assignable::value, - detail::enabler> = detail::dummy> -bool lexical_cast(const std::string &input, T &output) { - typename T::value_type val; - if(lexical_cast(input, val)) { - output = T{val}; - return true; - } - return from_stream(input, output); -} - -/// Assignable from double or int -template < - typename T, - enable_if_t::value == object_category::number_constructible, detail::enabler> = detail::dummy> -bool lexical_cast(const std::string &input, T &output) { - int val = 0; - if(integral_conversion(input, val)) { - output = T(val); - return true; - } - - double dval = 0.0; - if(lexical_cast(input, dval)) { - output = T{dval}; - return true; - } - - return from_stream(input, output); -} - -/// Assignable from int -template < - typename T, - enable_if_t::value == object_category::integer_constructible, detail::enabler> = detail::dummy> -bool lexical_cast(const std::string &input, T &output) { - int val = 0; - if(integral_conversion(input, val)) { - output = T(val); - return true; - } - return from_stream(input, output); -} - -/// Assignable from double -template < - typename T, - enable_if_t::value == object_category::double_constructible, detail::enabler> = detail::dummy> -bool lexical_cast(const std::string &input, T &output) { - double val = 0.0; - if(lexical_cast(input, val)) { - output = T{val}; - return true; - } - return from_stream(input, output); -} - -/// Non-string convertible from an int -template ::value == object_category::other && std::is_assignable::value, - detail::enabler> = detail::dummy> -bool lexical_cast(const std::string &input, T &output) { - int val = 0; - if(integral_conversion(input, val)) { -#ifdef _MSC_VER -#pragma warning(push) -#pragma warning(disable : 4800) -#endif - // with Atomic this could produce a warning due to the conversion but if atomic gets here it is an old style - // so will most likely still work - output = val; -#ifdef _MSC_VER -#pragma warning(pop) -#endif - return true; - } - // LCOV_EXCL_START - // This version of cast is only used for odd cases in an older compilers the fail over - // from_stream is tested elsewhere an not relevant for coverage here - return from_stream(input, output); - // LCOV_EXCL_STOP -} - -/// Non-string parsable by a stream -template ::value == object_category::other && !std::is_assignable::value && - is_istreamable::value, - detail::enabler> = detail::dummy> -bool lexical_cast(const std::string &input, T &output) { - return from_stream(input, output); -} - -/// Fallback overload that prints a human-readable error for types that we don't recognize and that don't have a -/// user-supplied lexical_cast overload. -template ::value == object_category::other && !std::is_assignable::value && - !is_istreamable::value && !adl_detail::is_lexical_castable::value, - detail::enabler> = detail::dummy> -bool lexical_cast(const std::string & /*input*/, T & /*output*/) { - static_assert(!std::is_same::value, // Can't just write false here. - "option object type must have a lexical cast overload or streaming input operator(>>) defined, if it " - "is convertible from another type use the add_option(...) with XC being the known type"); - return false; -} - -/// Assign a value through lexical cast operations -/// Strings can be empty so we need to do a little different -template ::value && - (classify_object::value == object_category::string_assignable || - classify_object::value == object_category::string_constructible || - classify_object::value == object_category::wstring_assignable || - classify_object::value == object_category::wstring_constructible), - detail::enabler> = detail::dummy> -bool lexical_assign(const std::string &input, AssignTo &output) { - return lexical_cast(input, output); -} - -/// Assign a value through lexical cast operations -template ::value && std::is_assignable::value && - classify_object::value != object_category::string_assignable && - classify_object::value != object_category::string_constructible && - classify_object::value != object_category::wstring_assignable && - classify_object::value != object_category::wstring_constructible, - detail::enabler> = detail::dummy> -bool lexical_assign(const std::string &input, AssignTo &output) { - if(input.empty()) { - output = AssignTo{}; - return true; - } - - return lexical_cast(input, output); -} // LCOV_EXCL_LINE - -/// Assign a value through lexical cast operations -template ::value && !std::is_assignable::value && - classify_object::value == object_category::wrapper_value, - detail::enabler> = detail::dummy> -bool lexical_assign(const std::string &input, AssignTo &output) { - if(input.empty()) { - typename AssignTo::value_type emptyVal{}; - output = emptyVal; - return true; - } - return lexical_cast(input, output); -} - -/// Assign a value through lexical cast operations for int compatible values -/// mainly for atomic operations on some compilers -template ::value && !std::is_assignable::value && - classify_object::value != object_category::wrapper_value && - std::is_assignable::value, - detail::enabler> = detail::dummy> -bool lexical_assign(const std::string &input, AssignTo &output) { - if(input.empty()) { - output = 0; - return true; - } - int val{0}; - if(lexical_cast(input, val)) { -#if defined(__clang__) -/* on some older clang compilers */ -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wsign-conversion" -#endif - output = val; -#if defined(__clang__) -#pragma clang diagnostic pop -#endif - return true; - } - return false; -} - -/// Assign a value converted from a string in lexical cast to the output value directly -template ::value && std::is_assignable::value, - detail::enabler> = detail::dummy> -bool lexical_assign(const std::string &input, AssignTo &output) { - ConvertTo val{}; - bool parse_result = (!input.empty()) ? lexical_cast(input, val) : true; - if(parse_result) { - output = val; - } - return parse_result; -} - -/// Assign a value from a lexical cast through constructing a value and move assigning it -template < - typename AssignTo, - typename ConvertTo, - enable_if_t::value && !std::is_assignable::value && - std::is_move_assignable::value, - detail::enabler> = detail::dummy> -bool lexical_assign(const std::string &input, AssignTo &output) { - ConvertTo val{}; - bool parse_result = input.empty() ? true : lexical_cast(input, val); - if(parse_result) { - output = AssignTo(val); // use () form of constructor to allow some implicit conversions - } - return parse_result; -} - -/// primary lexical conversion operation, 1 string to 1 type of some kind -template ::value <= object_category::other && - classify_object::value <= object_category::wrapper_value, - detail::enabler> = detail::dummy> -bool lexical_conversion(const std::vector &strings, AssignTo &output) { - return lexical_assign(strings[0], output); -} - -/// Lexical conversion if there is only one element but the conversion type is for two, then call a two element -/// constructor -template ::value <= 2) && expected_count::value == 1 && - is_tuple_like::value && type_count_base::value == 2, - detail::enabler> = detail::dummy> -bool lexical_conversion(const std::vector &strings, AssignTo &output) { - // the remove const is to handle pair types coming from a container - using FirstType = typename std::remove_const::type>::type; - using SecondType = typename std::tuple_element<1, ConvertTo>::type; - FirstType v1; - SecondType v2; - bool retval = lexical_assign(strings[0], v1); - retval = retval && lexical_assign((strings.size() > 1) ? strings[1] : std::string{}, v2); - if(retval) { - output = AssignTo{v1, v2}; - } - return retval; -} - -/// Lexical conversion of a container types of single elements -template ::value && is_mutable_container::value && - type_count::value == 1, - detail::enabler> = detail::dummy> -bool lexical_conversion(const std::vector &strings, AssignTo &output) { - output.erase(output.begin(), output.end()); - if(strings.empty()) { - return true; - } - if(strings.size() == 1 && strings[0] == "{}") { - return true; - } - bool skip_remaining = false; - if(strings.size() == 2 && strings[0] == "{}" && is_separator(strings[1])) { - skip_remaining = true; - } - for(const auto &elem : strings) { - typename AssignTo::value_type out; - bool retval = lexical_assign(elem, out); - if(!retval) { - return false; - } - output.insert(output.end(), std::move(out)); - if(skip_remaining) { - break; - } - } - return (!output.empty()); -} - -/// Lexical conversion for complex types -template ::value, detail::enabler> = detail::dummy> -bool lexical_conversion(const std::vector &strings, AssignTo &output) { - - if(strings.size() >= 2 && !strings[1].empty()) { - using XC2 = typename wrapped_type::type; - XC2 x{0.0}, y{0.0}; - auto str1 = strings[1]; - if(str1.back() == 'i' || str1.back() == 'j') { - str1.pop_back(); - } - auto worked = lexical_cast(strings[0], x) && lexical_cast(str1, y); - if(worked) { - output = ConvertTo{x, y}; - } - return worked; - } - return lexical_assign(strings[0], output); -} - -/// Conversion to a vector type using a particular single type as the conversion type -template ::value && (expected_count::value == 1) && - (type_count::value == 1), - detail::enabler> = detail::dummy> -bool lexical_conversion(const std::vector &strings, AssignTo &output) { - bool retval = true; - output.clear(); - output.reserve(strings.size()); - for(const auto &elem : strings) { - - output.emplace_back(); - retval = retval && lexical_assign(elem, output.back()); - } - return (!output.empty()) && retval; -} - -// forward declaration - -/// Lexical conversion of a container types with conversion type of two elements -template ::value && is_mutable_container::value && - type_count_base::value == 2, - detail::enabler> = detail::dummy> -bool lexical_conversion(std::vector strings, AssignTo &output); - -/// Lexical conversion of a vector types with type_size >2 forward declaration -template ::value && is_mutable_container::value && - type_count_base::value != 2 && - ((type_count::value > 2) || - (type_count::value > type_count_base::value)), - detail::enabler> = detail::dummy> -bool lexical_conversion(const std::vector &strings, AssignTo &output); - -/// Conversion for tuples -template ::value && is_tuple_like::value && - (type_count_base::value != type_count::value || - type_count::value > 2), - detail::enabler> = detail::dummy> -bool lexical_conversion(const std::vector &strings, AssignTo &output); // forward declaration - -/// Conversion for operations where the assigned type is some class but the conversion is a mutable container or large -/// tuple -template ::value && !is_mutable_container::value && - classify_object::value != object_category::wrapper_value && - (is_mutable_container::value || type_count::value > 2), - detail::enabler> = detail::dummy> -bool lexical_conversion(const std::vector &strings, AssignTo &output) { - - if(strings.size() > 1 || (!strings.empty() && !(strings.front().empty()))) { - ConvertTo val; - auto retval = lexical_conversion(strings, val); - output = AssignTo{val}; - return retval; - } - output = AssignTo{}; - return true; -} - -/// function template for converting tuples if the static Index is greater than the tuple size -template -inline typename std::enable_if<(I >= type_count_base::value), bool>::type -tuple_conversion(const std::vector &, AssignTo &) { - return true; -} - -/// Conversion of a tuple element where the type size ==1 and not a mutable container -template -inline typename std::enable_if::value && type_count::value == 1, bool>::type -tuple_type_conversion(std::vector &strings, AssignTo &output) { - auto retval = lexical_assign(strings[0], output); - strings.erase(strings.begin()); - return retval; -} - -/// Conversion of a tuple element where the type size !=1 but the size is fixed and not a mutable container -template -inline typename std::enable_if::value && (type_count::value > 1) && - type_count::value == type_count_min::value, - bool>::type -tuple_type_conversion(std::vector &strings, AssignTo &output) { - auto retval = lexical_conversion(strings, output); - strings.erase(strings.begin(), strings.begin() + type_count::value); - return retval; -} - -/// Conversion of a tuple element where the type is a mutable container or a type with different min and max type sizes -template -inline typename std::enable_if::value || - type_count::value != type_count_min::value, - bool>::type -tuple_type_conversion(std::vector &strings, AssignTo &output) { - - std::size_t index{subtype_count_min::value}; - const std::size_t mx_count{subtype_count::value}; - const std::size_t mx{(std::min)(mx_count, strings.size() - 1)}; - - while(index < mx) { - if(is_separator(strings[index])) { - break; - } - ++index; - } - bool retval = lexical_conversion( - std::vector(strings.begin(), strings.begin() + static_cast(index)), output); - if(strings.size() > index) { - strings.erase(strings.begin(), strings.begin() + static_cast(index) + 1); - } else { - strings.clear(); - } - return retval; -} - -/// Tuple conversion operation -template -inline typename std::enable_if<(I < type_count_base::value), bool>::type -tuple_conversion(std::vector strings, AssignTo &output) { - bool retval = true; - using ConvertToElement = typename std:: - conditional::value, typename std::tuple_element::type, ConvertTo>::type; - if(!strings.empty()) { - retval = retval && tuple_type_conversion::type, ConvertToElement>( - strings, std::get(output)); - } - retval = retval && tuple_conversion(std::move(strings), output); - return retval; -} - -/// Lexical conversion of a container types with tuple elements of size 2 -template ::value && is_mutable_container::value && - type_count_base::value == 2, - detail::enabler>> -bool lexical_conversion(std::vector strings, AssignTo &output) { - output.clear(); - while(!strings.empty()) { - - typename std::remove_const::type>::type v1; - typename std::tuple_element<1, typename ConvertTo::value_type>::type v2; - bool retval = tuple_type_conversion(strings, v1); - if(!strings.empty()) { - retval = retval && tuple_type_conversion(strings, v2); - } - if(retval) { - output.insert(output.end(), typename AssignTo::value_type{v1, v2}); - } else { - return false; - } - } - return (!output.empty()); -} - -/// lexical conversion of tuples with type count>2 or tuples of types of some element with a type size>=2 -template ::value && is_tuple_like::value && - (type_count_base::value != type_count::value || - type_count::value > 2), - detail::enabler>> -bool lexical_conversion(const std::vector &strings, AssignTo &output) { - static_assert( - !is_tuple_like::value || type_count_base::value == type_count_base::value, - "if the conversion type is defined as a tuple it must be the same size as the type you are converting to"); - return tuple_conversion(strings, output); -} - -/// Lexical conversion of a vector types for everything but tuples of two elements and types of size 1 -template ::value && is_mutable_container::value && - type_count_base::value != 2 && - ((type_count::value > 2) || - (type_count::value > type_count_base::value)), - detail::enabler>> -bool lexical_conversion(const std::vector &strings, AssignTo &output) { - bool retval = true; - output.clear(); - std::vector temp; - std::size_t ii{0}; - std::size_t icount{0}; - std::size_t xcm{type_count::value}; - auto ii_max = strings.size(); - while(ii < ii_max) { - temp.push_back(strings[ii]); - ++ii; - ++icount; - if(icount == xcm || is_separator(temp.back()) || ii == ii_max) { - if(static_cast(xcm) > type_count_min::value && is_separator(temp.back())) { - temp.pop_back(); - } - typename AssignTo::value_type temp_out; - retval = retval && - lexical_conversion(temp, temp_out); - temp.clear(); - if(!retval) { - return false; - } - output.insert(output.end(), std::move(temp_out)); - icount = 0; - } - } - return retval; -} - -/// conversion for wrapper types -template ::value == object_category::wrapper_value && - std::is_assignable::value, - detail::enabler> = detail::dummy> -bool lexical_conversion(const std::vector &strings, AssignTo &output) { - if(strings.empty() || strings.front().empty()) { - output = ConvertTo{}; - return true; - } - typename ConvertTo::value_type val; - if(lexical_conversion(strings, val)) { - output = ConvertTo{val}; - return true; - } - return false; -} - -/// conversion for wrapper types -template ::value == object_category::wrapper_value && - !std::is_assignable::value, - detail::enabler> = detail::dummy> -bool lexical_conversion(const std::vector &strings, AssignTo &output) { - using ConvertType = typename ConvertTo::value_type; - if(strings.empty() || strings.front().empty()) { - output = ConvertType{}; - return true; - } - ConvertType val; - if(lexical_conversion(strings, val)) { - output = val; - return true; - } - return false; -} - -/// Sum a vector of strings -inline std::string sum_string_vector(const std::vector &values) { - double val{0.0}; - bool fail{false}; - std::string output; - for(const auto &arg : values) { - double tv{0.0}; - auto comp = lexical_cast(arg, tv); - if(!comp) { - errno = 0; - auto fv = detail::to_flag_value(arg); - fail = (errno != 0); - if(fail) { - break; - } - tv = static_cast(fv); - } - val += tv; - } - if(fail) { - for(const auto &arg : values) { - output.append(arg); - } - } else { - std::ostringstream out; - out.precision(16); - out << val; - output = out.str(); - } - return output; -} - -} // namespace detail - - - -namespace detail { - -// Returns false if not a short option. Otherwise, sets opt name and rest and returns true -CLI11_INLINE bool split_short(const std::string ¤t, std::string &name, std::string &rest); - -// Returns false if not a long option. Otherwise, sets opt name and other side of = and returns true -CLI11_INLINE bool split_long(const std::string ¤t, std::string &name, std::string &value); - -// Returns false if not a windows style option. Otherwise, sets opt name and value and returns true -CLI11_INLINE bool split_windows_style(const std::string ¤t, std::string &name, std::string &value); - -// Splits a string into multiple long and short names -CLI11_INLINE std::vector split_names(std::string current); - -/// extract default flag values either {def} or starting with a ! -CLI11_INLINE std::vector> get_default_flag_values(const std::string &str); - -/// Get a vector of short names, one of long names, and a single name -CLI11_INLINE std::tuple, std::vector, std::string> -get_names(const std::vector &input, bool allow_non_standard = false); - -} // namespace detail - - - -namespace detail { - -CLI11_INLINE bool split_short(const std::string ¤t, std::string &name, std::string &rest) { - if(current.size() > 1 && current[0] == '-' && valid_first_char(current[1])) { - name = current.substr(1, 1); - rest = current.substr(2); - return true; - } - return false; -} - -CLI11_INLINE bool split_long(const std::string ¤t, std::string &name, std::string &value) { - if(current.size() > 2 && current.compare(0, 2, "--") == 0 && valid_first_char(current[2])) { - auto loc = current.find_first_of('='); - if(loc != std::string::npos) { - name = current.substr(2, loc - 2); - value = current.substr(loc + 1); - } else { - name = current.substr(2); - value = ""; - } - return true; - } - return false; -} - -CLI11_INLINE bool split_windows_style(const std::string ¤t, std::string &name, std::string &value) { - if(current.size() > 1 && current[0] == '/' && valid_first_char(current[1])) { - auto loc = current.find_first_of(':'); - if(loc != std::string::npos) { - name = current.substr(1, loc - 1); - value = current.substr(loc + 1); - } else { - name = current.substr(1); - value = ""; - } - return true; - } - return false; -} - -CLI11_INLINE std::vector split_names(std::string current) { - std::vector output; - std::size_t val = 0; - while((val = current.find(',')) != std::string::npos) { - output.push_back(trim_copy(current.substr(0, val))); - current = current.substr(val + 1); - } - output.push_back(trim_copy(current)); - return output; -} - -CLI11_INLINE std::vector> get_default_flag_values(const std::string &str) { - std::vector flags = split_names(str); - flags.erase(std::remove_if(flags.begin(), - flags.end(), - [](const std::string &name) { - return ((name.empty()) || (!(((name.find_first_of('{') != std::string::npos) && - (name.back() == '}')) || - (name[0] == '!')))); - }), - flags.end()); - std::vector> output; - output.reserve(flags.size()); - for(auto &flag : flags) { - auto def_start = flag.find_first_of('{'); - std::string defval = "false"; - if((def_start != std::string::npos) && (flag.back() == '}')) { - defval = flag.substr(def_start + 1); - defval.pop_back(); - flag.erase(def_start, std::string::npos); // NOLINT(readability-suspicious-call-argument) - } - flag.erase(0, flag.find_first_not_of("-!")); - output.emplace_back(flag, defval); - } - return output; -} - -CLI11_INLINE std::tuple, std::vector, std::string> -get_names(const std::vector &input, bool allow_non_standard) { - - std::vector short_names; - std::vector long_names; - std::string pos_name; - for(std::string name : input) { - if(name.length() == 0) { - continue; - } - if(name.length() > 1 && name[0] == '-' && name[1] != '-') { - if(name.length() == 2 && valid_first_char(name[1])) { - short_names.emplace_back(1, name[1]); - } else if(name.length() > 2) { - if(allow_non_standard) { - name = name.substr(1); - if(valid_name_string(name)) { - short_names.push_back(name); - } else { - throw BadNameString::BadLongName(name); - } - } else { - throw BadNameString::MissingDash(name); - } - } else { - throw BadNameString::OneCharName(name); - } - } else if(name.length() > 2 && name.substr(0, 2) == "--") { - name = name.substr(2); - if(valid_name_string(name)) { - long_names.push_back(name); - } else { - throw BadNameString::BadLongName(name); - } - } else if(name == "-" || name == "--" || name == "++") { - throw BadNameString::ReservedName(name); - } else { - if(!pos_name.empty()) { - throw BadNameString::MultiPositionalNames(name); - } - if(valid_name_string(name)) { - pos_name = name; - } else { - throw BadNameString::BadPositionalName(name); - } - } - } - return std::make_tuple(short_names, long_names, pos_name); -} - -} // namespace detail - - - -class App; - -/// Holds values to load into Options -struct ConfigItem { - /// This is the list of parents - std::vector parents{}; - - /// This is the name - std::string name{}; - /// Listing of inputs - std::vector inputs{}; - /// @brief indicator if a multiline vector separator was inserted - bool multiline{false}; - /// The list of parents and name joined by "." - CLI11_NODISCARD std::string fullname() const { - std::vector tmp = parents; - tmp.emplace_back(name); - return detail::join(tmp, "."); - (void)multiline; // suppression for cppcheck false positive - } -}; - -/// This class provides a converter for configuration files. -class Config { - protected: - std::vector items{}; - - public: - /// Convert an app into a configuration - virtual std::string to_config(const App *, bool, bool, std::string) const = 0; - - /// Convert a configuration into an app - virtual std::vector from_config(std::istream &) const = 0; - - /// Get a flag value - CLI11_NODISCARD virtual std::string to_flag(const ConfigItem &item) const { - if(item.inputs.size() == 1) { - return item.inputs.at(0); - } - if(item.inputs.empty()) { - return "{}"; - } - throw ConversionError::TooManyInputsFlag(item.fullname()); // LCOV_EXCL_LINE - } - - /// Parse a config file, throw an error (ParseError:ConfigParseError or FileError) on failure - CLI11_NODISCARD std::vector from_file(const std::string &name) const { - std::ifstream input{name}; - if(!input.good()) - throw FileError::Missing(name); - - return from_config(input); - } - - /// Virtual destructor - virtual ~Config() = default; -}; - -/// This converter works with INI/TOML files; to write INI files use ConfigINI -class ConfigBase : public Config { - protected: - /// the character used for comments - char commentChar = '#'; - /// the character used to start an array '\0' is a default to not use - char arrayStart = '['; - /// the character used to end an array '\0' is a default to not use - char arrayEnd = ']'; - /// the character used to separate elements in an array - char arraySeparator = ','; - /// the character used separate the name from the value - char valueDelimiter = '='; - /// the character to use around strings - char stringQuote = '"'; - /// the character to use around single characters and literal strings - char literalQuote = '\''; - /// the maximum number of layers to allow - uint8_t maximumLayers{255}; - /// the separator used to separator parent layers - char parentSeparatorChar{'.'}; - /// comment default values - bool commentDefaultsBool = false; - /// specify the config reader should collapse repeated field names to a single vector - bool allowMultipleDuplicateFields{false}; - /// Specify the configuration index to use for arrayed sections - int16_t configIndex{-1}; - /// Specify the configuration section that should be used - std::string configSection{}; - - public: - std::string - to_config(const App * /*app*/, bool default_also, bool write_description, std::string prefix) const override; - - std::vector from_config(std::istream &input) const override; - /// Specify the configuration for comment characters - ConfigBase *comment(char cchar) { - commentChar = cchar; - return this; - } - /// Specify the start and end characters for an array - ConfigBase *arrayBounds(char aStart, char aEnd) { - arrayStart = aStart; - arrayEnd = aEnd; - return this; - } - /// Specify the delimiter character for an array - ConfigBase *arrayDelimiter(char aSep) { - arraySeparator = aSep; - return this; - } - /// Specify the delimiter between a name and value - ConfigBase *valueSeparator(char vSep) { - valueDelimiter = vSep; - return this; - } - /// Specify the quote characters used around strings and literal strings - ConfigBase *quoteCharacter(char qString, char literalChar) { - stringQuote = qString; - literalQuote = literalChar; - return this; - } - /// Specify the maximum number of parents - ConfigBase *maxLayers(uint8_t layers) { - maximumLayers = layers; - return this; - } - /// Specify the separator to use for parent layers - ConfigBase *parentSeparator(char sep) { - parentSeparatorChar = sep; - return this; - } - /// comment default value options - ConfigBase *commentDefaults(bool comDef = true) { - commentDefaultsBool = comDef; - return this; - } - /// get a reference to the configuration section - std::string §ionRef() { return configSection; } - /// get the section - CLI11_NODISCARD const std::string §ion() const { return configSection; } - /// specify a particular section of the configuration file to use - ConfigBase *section(const std::string §ionName) { - configSection = sectionName; - return this; - } - - /// get a reference to the configuration index - int16_t &indexRef() { return configIndex; } - /// get the section index - CLI11_NODISCARD int16_t index() const { return configIndex; } - /// specify a particular index in the section to use (-1) for all sections to use - ConfigBase *index(int16_t sectionIndex) { - configIndex = sectionIndex; - return this; - } - /// specify that multiple duplicate arguments should be merged even if not sequential - ConfigBase *allowDuplicateFields(bool value = true) { - allowMultipleDuplicateFields = value; - return this; - } -}; - -/// the default Config is the TOML file format -using ConfigTOML = ConfigBase; - -/// ConfigINI generates a "standard" INI compliant output -class ConfigINI : public ConfigTOML { - - public: - ConfigINI() { - commentChar = ';'; - arrayStart = '\0'; - arrayEnd = '\0'; - arraySeparator = ' '; - valueDelimiter = '='; - } -}; - - - -class Option; - -/// @defgroup validator_group Validators - -/// @brief Some validators that are provided -/// -/// These are simple `std::string(const std::string&)` validators that are useful. They return -/// a string if the validation fails. A custom struct is provided, as well, with the same user -/// semantics, but with the ability to provide a new type name. -/// @{ - -/// -class Validator { - protected: - /// This is the description function, if empty the description_ will be used - std::function desc_function_{[]() { return std::string{}; }}; - - /// This is the base function that is to be called. - /// Returns a string error message if validation fails. - std::function func_{[](std::string &) { return std::string{}; }}; - /// The name for search purposes of the Validator - std::string name_{}; - /// A Validator will only apply to an indexed value (-1 is all elements) - int application_index_ = -1; - /// Enable for Validator to allow it to be disabled if need be - bool active_{true}; - /// specify that a validator should not modify the input - bool non_modifying_{false}; - - Validator(std::string validator_desc, std::function func) - : desc_function_([validator_desc]() { return validator_desc; }), func_(std::move(func)) {} - - public: - Validator() = default; - /// Construct a Validator with just the description string - explicit Validator(std::string validator_desc) : desc_function_([validator_desc]() { return validator_desc; }) {} - /// Construct Validator from basic information - Validator(std::function op, std::string validator_desc, std::string validator_name = "") - : desc_function_([validator_desc]() { return validator_desc; }), func_(std::move(op)), - name_(std::move(validator_name)) {} - /// Set the Validator operation function - Validator &operation(std::function op) { - func_ = std::move(op); - return *this; - } - /// This is the required operator for a Validator - provided to help - /// users (CLI11 uses the member `func` directly) - std::string operator()(std::string &str) const; - - /// This is the required operator for a Validator - provided to help - /// users (CLI11 uses the member `func` directly) - std::string operator()(const std::string &str) const { - std::string value = str; - return (active_) ? func_(value) : std::string{}; - } - - /// Specify the type string - Validator &description(std::string validator_desc) { - desc_function_ = [validator_desc]() { return validator_desc; }; - return *this; - } - /// Specify the type string - CLI11_NODISCARD Validator description(std::string validator_desc) const; - - /// Generate type description information for the Validator - CLI11_NODISCARD std::string get_description() const { - if(active_) { - return desc_function_(); - } - return std::string{}; - } - /// Specify the type string - Validator &name(std::string validator_name) { - name_ = std::move(validator_name); - return *this; - } - /// Specify the type string - CLI11_NODISCARD Validator name(std::string validator_name) const { - Validator newval(*this); - newval.name_ = std::move(validator_name); - return newval; - } - /// Get the name of the Validator - CLI11_NODISCARD const std::string &get_name() const { return name_; } - /// Specify whether the Validator is active or not - Validator &active(bool active_val = true) { - active_ = active_val; - return *this; - } - /// Specify whether the Validator is active or not - CLI11_NODISCARD Validator active(bool active_val = true) const { - Validator newval(*this); - newval.active_ = active_val; - return newval; - } - - /// Specify whether the Validator can be modifying or not - Validator &non_modifying(bool no_modify = true) { - non_modifying_ = no_modify; - return *this; - } - /// Specify the application index of a validator - Validator &application_index(int app_index) { - application_index_ = app_index; - return *this; - } - /// Specify the application index of a validator - CLI11_NODISCARD Validator application_index(int app_index) const { - Validator newval(*this); - newval.application_index_ = app_index; - return newval; - } - /// Get the current value of the application index - CLI11_NODISCARD int get_application_index() const { return application_index_; } - /// Get a boolean if the validator is active - CLI11_NODISCARD bool get_active() const { return active_; } - - /// Get a boolean if the validator is allowed to modify the input returns true if it can modify the input - CLI11_NODISCARD bool get_modifying() const { return !non_modifying_; } - - /// Combining validators is a new validator. Type comes from left validator if function, otherwise only set if the - /// same. - Validator operator&(const Validator &other) const; - - /// Combining validators is a new validator. Type comes from left validator if function, otherwise only set if the - /// same. - Validator operator|(const Validator &other) const; - - /// Create a validator that fails when a given validator succeeds - Validator operator!() const; - - private: - void _merge_description(const Validator &val1, const Validator &val2, const std::string &merger); -}; - -/// Class wrapping some of the accessors of Validator -class CustomValidator : public Validator { - public: -}; -// The implementation of the built in validators is using the Validator class; -// the user is only expected to use the const (static) versions (since there's no setup). -// Therefore, this is in detail. -namespace detail { - -/// CLI enumeration of different file types -enum class path_type { nonexistent, file, directory }; - -/// get the type of the path from a file name -CLI11_INLINE path_type check_path(const char *file) noexcept; - -/// Check for an existing file (returns error message if check fails) -class ExistingFileValidator : public Validator { - public: - ExistingFileValidator(); -}; - -/// Check for an existing directory (returns error message if check fails) -class ExistingDirectoryValidator : public Validator { - public: - ExistingDirectoryValidator(); -}; - -/// Check for an existing path -class ExistingPathValidator : public Validator { - public: - ExistingPathValidator(); -}; - -/// Check for an non-existing path -class NonexistentPathValidator : public Validator { - public: - NonexistentPathValidator(); -}; - -/// Validate the given string is a legal ipv4 address -class IPV4Validator : public Validator { - public: - IPV4Validator(); -}; - -class EscapedStringTransformer : public Validator { - public: - EscapedStringTransformer(); -}; - -} // namespace detail - -// Static is not needed here, because global const implies static. - -/// Check for existing file (returns error message if check fails) -const detail::ExistingFileValidator ExistingFile; - -/// Check for an existing directory (returns error message if check fails) -const detail::ExistingDirectoryValidator ExistingDirectory; - -/// Check for an existing path -const detail::ExistingPathValidator ExistingPath; - -/// Check for an non-existing path -const detail::NonexistentPathValidator NonexistentPath; - -/// Check for an IP4 address -const detail::IPV4Validator ValidIPV4; - -/// convert escaped characters into their associated values -const detail::EscapedStringTransformer EscapedString; - -/// Validate the input as a particular type -template class TypeValidator : public Validator { - public: - explicit TypeValidator(const std::string &validator_name) - : Validator(validator_name, [](std::string &input_string) { - using CLI::detail::lexical_cast; - auto val = DesiredType(); - if(!lexical_cast(input_string, val)) { - return std::string("Failed parsing ") + input_string + " as a " + detail::type_name(); - } - return std::string(); - }) {} - TypeValidator() : TypeValidator(detail::type_name()) {} -}; - -/// Check for a number -const TypeValidator Number("NUMBER"); - -/// Modify a path if the file is a particular default location, can be used as Check or transform -/// with the error return optionally disabled -class FileOnDefaultPath : public Validator { - public: - explicit FileOnDefaultPath(std::string default_path, bool enableErrorReturn = true); -}; - -/// Produce a range (factory). Min and max are inclusive. -class Range : public Validator { - public: - /// This produces a range with min and max inclusive. - /// - /// Note that the constructor is templated, but the struct is not, so C++17 is not - /// needed to provide nice syntax for Range(a,b). - template - Range(T min_val, T max_val, const std::string &validator_name = std::string{}) : Validator(validator_name) { - if(validator_name.empty()) { - std::stringstream out; - out << detail::type_name() << " in [" << min_val << " - " << max_val << "]"; - description(out.str()); - } - - func_ = [min_val, max_val](std::string &input) { - using CLI::detail::lexical_cast; - T val; - bool converted = lexical_cast(input, val); - if((!converted) || (val < min_val || val > max_val)) { - std::stringstream out; - out << "Value " << input << " not in range ["; - out << min_val << " - " << max_val << "]"; - return out.str(); - } - return std::string{}; - }; - } - - /// Range of one value is 0 to value - template - explicit Range(T max_val, const std::string &validator_name = std::string{}) - : Range(static_cast(0), max_val, validator_name) {} -}; - -/// Check for a non negative number -const Range NonNegativeNumber((std::numeric_limits::max)(), "NONNEGATIVE"); - -/// Check for a positive valued number (val>0.0), ::min here is the smallest positive number -const Range PositiveNumber((std::numeric_limits::min)(), (std::numeric_limits::max)(), "POSITIVE"); - -/// Produce a bounded range (factory). Min and max are inclusive. -class Bound : public Validator { - public: - /// This bounds a value with min and max inclusive. - /// - /// Note that the constructor is templated, but the struct is not, so C++17 is not - /// needed to provide nice syntax for Range(a,b). - template Bound(T min_val, T max_val) { - std::stringstream out; - out << detail::type_name() << " bounded to [" << min_val << " - " << max_val << "]"; - description(out.str()); - - func_ = [min_val, max_val](std::string &input) { - using CLI::detail::lexical_cast; - T val; - bool converted = lexical_cast(input, val); - if(!converted) { - return std::string("Value ") + input + " could not be converted"; - } - if(val < min_val) - input = detail::to_string(min_val); - else if(val > max_val) - input = detail::to_string(max_val); - - return std::string{}; - }; - } - - /// Range of one value is 0 to value - template explicit Bound(T max_val) : Bound(static_cast(0), max_val) {} -}; - -namespace detail { -template ::type>::value, detail::enabler> = detail::dummy> -auto smart_deref(T value) -> decltype(*value) { - return *value; -} - -template < - typename T, - enable_if_t::type>::value, detail::enabler> = detail::dummy> -typename std::remove_reference::type &smart_deref(T &value) { - return value; -} -/// Generate a string representation of a set -template std::string generate_set(const T &set) { - using element_t = typename detail::element_type::type; - using iteration_type_t = typename detail::pair_adaptor::value_type; // the type of the object pair - std::string out(1, '{'); - out.append(detail::join( - detail::smart_deref(set), - [](const iteration_type_t &v) { return detail::pair_adaptor::first(v); }, - ",")); - out.push_back('}'); - return out; -} - -/// Generate a string representation of a map -template std::string generate_map(const T &map, bool key_only = false) { - using element_t = typename detail::element_type::type; - using iteration_type_t = typename detail::pair_adaptor::value_type; // the type of the object pair - std::string out(1, '{'); - out.append(detail::join( - detail::smart_deref(map), - [key_only](const iteration_type_t &v) { - std::string res{detail::to_string(detail::pair_adaptor::first(v))}; - - if(!key_only) { - res.append("->"); - res += detail::to_string(detail::pair_adaptor::second(v)); - } - return res; - }, - ",")); - out.push_back('}'); - return out; -} - -template struct has_find { - template - static auto test(int) -> decltype(std::declval().find(std::declval()), std::true_type()); - template static auto test(...) -> decltype(std::false_type()); - - static const auto value = decltype(test(0))::value; - using type = std::integral_constant; -}; - -/// A search function -template ::value, detail::enabler> = detail::dummy> -auto search(const T &set, const V &val) -> std::pair { - using element_t = typename detail::element_type::type; - auto &setref = detail::smart_deref(set); - auto it = std::find_if(std::begin(setref), std::end(setref), [&val](decltype(*std::begin(setref)) v) { - return (detail::pair_adaptor::first(v) == val); - }); - return {(it != std::end(setref)), it}; -} - -/// A search function that uses the built in find function -template ::value, detail::enabler> = detail::dummy> -auto search(const T &set, const V &val) -> std::pair { - auto &setref = detail::smart_deref(set); - auto it = setref.find(val); - return {(it != std::end(setref)), it}; -} - -/// A search function with a filter function -template -auto search(const T &set, const V &val, const std::function &filter_function) - -> std::pair { - using element_t = typename detail::element_type::type; - // do the potentially faster first search - auto res = search(set, val); - if((res.first) || (!(filter_function))) { - return res; - } - // if we haven't found it do the longer linear search with all the element translations - auto &setref = detail::smart_deref(set); - auto it = std::find_if(std::begin(setref), std::end(setref), [&](decltype(*std::begin(setref)) v) { - V a{detail::pair_adaptor::first(v)}; - a = filter_function(a); - return (a == val); - }); - return {(it != std::end(setref)), it}; -} - -// the following suggestion was made by Nikita Ofitserov(@himikof) -// done in templates to prevent compiler warnings on negation of unsigned numbers - -/// Do a check for overflow on signed numbers -template -inline typename std::enable_if::value, T>::type overflowCheck(const T &a, const T &b) { - if((a > 0) == (b > 0)) { - return ((std::numeric_limits::max)() / (std::abs)(a) < (std::abs)(b)); - } - return ((std::numeric_limits::min)() / (std::abs)(a) > -(std::abs)(b)); -} -/// Do a check for overflow on unsigned numbers -template -inline typename std::enable_if::value, T>::type overflowCheck(const T &a, const T &b) { - return ((std::numeric_limits::max)() / a < b); -} - -/// Performs a *= b; if it doesn't cause integer overflow. Returns false otherwise. -template typename std::enable_if::value, bool>::type checked_multiply(T &a, T b) { - if(a == 0 || b == 0 || a == 1 || b == 1) { - a *= b; - return true; - } - if(a == (std::numeric_limits::min)() || b == (std::numeric_limits::min)()) { - return false; - } - if(overflowCheck(a, b)) { - return false; - } - a *= b; - return true; -} - -/// Performs a *= b; if it doesn't equal infinity. Returns false otherwise. -template -typename std::enable_if::value, bool>::type checked_multiply(T &a, T b) { - T c = a * b; - if(std::isinf(c) && !std::isinf(a) && !std::isinf(b)) { - return false; - } - a = c; - return true; -} - -} // namespace detail -/// Verify items are in a set -class IsMember : public Validator { - public: - using filter_fn_t = std::function; - - /// This allows in-place construction using an initializer list - template - IsMember(std::initializer_list values, Args &&...args) - : IsMember(std::vector(values), std::forward(args)...) {} - - /// This checks to see if an item is in a set (empty function) - template explicit IsMember(T &&set) : IsMember(std::forward(set), nullptr) {} - - /// This checks to see if an item is in a set: pointer or copy version. You can pass in a function that will filter - /// both sides of the comparison before computing the comparison. - template explicit IsMember(T set, F filter_function) { - - // Get the type of the contained item - requires a container have ::value_type - // if the type does not have first_type and second_type, these are both value_type - using element_t = typename detail::element_type::type; // Removes (smart) pointers if needed - using item_t = typename detail::pair_adaptor::first_type; // Is value_type if not a map - - using local_item_t = typename IsMemberType::type; // This will convert bad types to good ones - // (const char * to std::string) - - // Make a local copy of the filter function, using a std::function if not one already - std::function filter_fn = filter_function; - - // This is the type name for help, it will take the current version of the set contents - desc_function_ = [set]() { return detail::generate_set(detail::smart_deref(set)); }; - - // This is the function that validates - // It stores a copy of the set pointer-like, so shared_ptr will stay alive - func_ = [set, filter_fn](std::string &input) { - using CLI::detail::lexical_cast; - local_item_t b; - if(!lexical_cast(input, b)) { - throw ValidationError(input); // name is added later - } - if(filter_fn) { - b = filter_fn(b); - } - auto res = detail::search(set, b, filter_fn); - if(res.first) { - // Make sure the version in the input string is identical to the one in the set - if(filter_fn) { - input = detail::value_string(detail::pair_adaptor::first(*(res.second))); - } - - // Return empty error string (success) - return std::string{}; - } - - // If you reach this point, the result was not found - return input + " not in " + detail::generate_set(detail::smart_deref(set)); - }; - } - - /// You can pass in as many filter functions as you like, they nest (string only currently) - template - IsMember(T &&set, filter_fn_t filter_fn_1, filter_fn_t filter_fn_2, Args &&...other) - : IsMember( - std::forward(set), - [filter_fn_1, filter_fn_2](std::string a) { return filter_fn_2(filter_fn_1(a)); }, - other...) {} -}; - -/// definition of the default transformation object -template using TransformPairs = std::vector>; - -/// Translate named items to other or a value set -class Transformer : public Validator { - public: - using filter_fn_t = std::function; - - /// This allows in-place construction - template - Transformer(std::initializer_list> values, Args &&...args) - : Transformer(TransformPairs(values), std::forward(args)...) {} - - /// direct map of std::string to std::string - template explicit Transformer(T &&mapping) : Transformer(std::forward(mapping), nullptr) {} - - /// This checks to see if an item is in a set: pointer or copy version. You can pass in a function that will filter - /// both sides of the comparison before computing the comparison. - template explicit Transformer(T mapping, F filter_function) { - - static_assert(detail::pair_adaptor::type>::value, - "mapping must produce value pairs"); - // Get the type of the contained item - requires a container have ::value_type - // if the type does not have first_type and second_type, these are both value_type - using element_t = typename detail::element_type::type; // Removes (smart) pointers if needed - using item_t = typename detail::pair_adaptor::first_type; // Is value_type if not a map - using local_item_t = typename IsMemberType::type; // Will convert bad types to good ones - // (const char * to std::string) - - // Make a local copy of the filter function, using a std::function if not one already - std::function filter_fn = filter_function; - - // This is the type name for help, it will take the current version of the set contents - desc_function_ = [mapping]() { return detail::generate_map(detail::smart_deref(mapping)); }; - - func_ = [mapping, filter_fn](std::string &input) { - using CLI::detail::lexical_cast; - local_item_t b; - if(!lexical_cast(input, b)) { - return std::string(); - // there is no possible way we can match anything in the mapping if we can't convert so just return - } - if(filter_fn) { - b = filter_fn(b); - } - auto res = detail::search(mapping, b, filter_fn); - if(res.first) { - input = detail::value_string(detail::pair_adaptor::second(*res.second)); - } - return std::string{}; - }; - } - - /// You can pass in as many filter functions as you like, they nest - template - Transformer(T &&mapping, filter_fn_t filter_fn_1, filter_fn_t filter_fn_2, Args &&...other) - : Transformer( - std::forward(mapping), - [filter_fn_1, filter_fn_2](std::string a) { return filter_fn_2(filter_fn_1(a)); }, - other...) {} -}; - -/// translate named items to other or a value set -class CheckedTransformer : public Validator { - public: - using filter_fn_t = std::function; - - /// This allows in-place construction - template - CheckedTransformer(std::initializer_list> values, Args &&...args) - : CheckedTransformer(TransformPairs(values), std::forward(args)...) {} - - /// direct map of std::string to std::string - template explicit CheckedTransformer(T mapping) : CheckedTransformer(std::move(mapping), nullptr) {} - - /// This checks to see if an item is in a set: pointer or copy version. You can pass in a function that will filter - /// both sides of the comparison before computing the comparison. - template explicit CheckedTransformer(T mapping, F filter_function) { - - static_assert(detail::pair_adaptor::type>::value, - "mapping must produce value pairs"); - // Get the type of the contained item - requires a container have ::value_type - // if the type does not have first_type and second_type, these are both value_type - using element_t = typename detail::element_type::type; // Removes (smart) pointers if needed - using item_t = typename detail::pair_adaptor::first_type; // Is value_type if not a map - using local_item_t = typename IsMemberType::type; // Will convert bad types to good ones - // (const char * to std::string) - using iteration_type_t = typename detail::pair_adaptor::value_type; // the type of the object pair - - // Make a local copy of the filter function, using a std::function if not one already - std::function filter_fn = filter_function; - - auto tfunc = [mapping]() { - std::string out("value in "); - out += detail::generate_map(detail::smart_deref(mapping)) + " OR {"; - out += detail::join( - detail::smart_deref(mapping), - [](const iteration_type_t &v) { return detail::to_string(detail::pair_adaptor::second(v)); }, - ","); - out.push_back('}'); - return out; - }; - - desc_function_ = tfunc; - - func_ = [mapping, tfunc, filter_fn](std::string &input) { - using CLI::detail::lexical_cast; - local_item_t b; - bool converted = lexical_cast(input, b); - if(converted) { - if(filter_fn) { - b = filter_fn(b); - } - auto res = detail::search(mapping, b, filter_fn); - if(res.first) { - input = detail::value_string(detail::pair_adaptor::second(*res.second)); - return std::string{}; - } - } - for(const auto &v : detail::smart_deref(mapping)) { - auto output_string = detail::value_string(detail::pair_adaptor::second(v)); - if(output_string == input) { - return std::string(); - } - } - - return "Check " + input + " " + tfunc() + " FAILED"; - }; - } - - /// You can pass in as many filter functions as you like, they nest - template - CheckedTransformer(T &&mapping, filter_fn_t filter_fn_1, filter_fn_t filter_fn_2, Args &&...other) - : CheckedTransformer( - std::forward(mapping), - [filter_fn_1, filter_fn_2](std::string a) { return filter_fn_2(filter_fn_1(a)); }, - other...) {} -}; - -/// Helper function to allow ignore_case to be passed to IsMember or Transform -inline std::string ignore_case(std::string item) { return detail::to_lower(item); } - -/// Helper function to allow ignore_underscore to be passed to IsMember or Transform -inline std::string ignore_underscore(std::string item) { return detail::remove_underscore(item); } - -/// Helper function to allow checks to ignore spaces to be passed to IsMember or Transform -inline std::string ignore_space(std::string item) { - item.erase(std::remove(std::begin(item), std::end(item), ' '), std::end(item)); - item.erase(std::remove(std::begin(item), std::end(item), '\t'), std::end(item)); - return item; -} - -/// Multiply a number by a factor using given mapping. -/// Can be used to write transforms for SIZE or DURATION inputs. -/// -/// Example: -/// With mapping = `{"b"->1, "kb"->1024, "mb"->1024*1024}` -/// one can recognize inputs like "100", "12kb", "100 MB", -/// that will be automatically transformed to 100, 14448, 104857600. -/// -/// Output number type matches the type in the provided mapping. -/// Therefore, if it is required to interpret real inputs like "0.42 s", -/// the mapping should be of a type or . -class AsNumberWithUnit : public Validator { - public: - /// Adjust AsNumberWithUnit behavior. - /// CASE_SENSITIVE/CASE_INSENSITIVE controls how units are matched. - /// UNIT_OPTIONAL/UNIT_REQUIRED throws ValidationError - /// if UNIT_REQUIRED is set and unit literal is not found. - enum Options { - CASE_SENSITIVE = 0, - CASE_INSENSITIVE = 1, - UNIT_OPTIONAL = 0, - UNIT_REQUIRED = 2, - DEFAULT = CASE_INSENSITIVE | UNIT_OPTIONAL - }; - - template - explicit AsNumberWithUnit(std::map mapping, - Options opts = DEFAULT, - const std::string &unit_name = "UNIT") { - description(generate_description(unit_name, opts)); - validate_mapping(mapping, opts); - - // transform function - func_ = [mapping, opts](std::string &input) -> std::string { - Number num{}; - - detail::rtrim(input); - if(input.empty()) { - throw ValidationError("Input is empty"); - } - - // Find split position between number and prefix - auto unit_begin = input.end(); - while(unit_begin > input.begin() && std::isalpha(*(unit_begin - 1), std::locale())) { - --unit_begin; - } - - std::string unit{unit_begin, input.end()}; - input.resize(static_cast(std::distance(input.begin(), unit_begin))); - detail::trim(input); - - if(opts & UNIT_REQUIRED && unit.empty()) { - throw ValidationError("Missing mandatory unit"); - } - if(opts & CASE_INSENSITIVE) { - unit = detail::to_lower(unit); - } - if(unit.empty()) { - using CLI::detail::lexical_cast; - if(!lexical_cast(input, num)) { - throw ValidationError(std::string("Value ") + input + " could not be converted to " + - detail::type_name()); - } - // No need to modify input if no unit passed - return {}; - } - - // find corresponding factor - auto it = mapping.find(unit); - if(it == mapping.end()) { - throw ValidationError(unit + - " unit not recognized. " - "Allowed values: " + - detail::generate_map(mapping, true)); - } - - if(!input.empty()) { - using CLI::detail::lexical_cast; - bool converted = lexical_cast(input, num); - if(!converted) { - throw ValidationError(std::string("Value ") + input + " could not be converted to " + - detail::type_name()); - } - // perform safe multiplication - bool ok = detail::checked_multiply(num, it->second); - if(!ok) { - throw ValidationError(detail::to_string(num) + " multiplied by " + unit + - " factor would cause number overflow. Use smaller value."); - } - } else { - num = static_cast(it->second); - } - - input = detail::to_string(num); - - return {}; - }; - } - - private: - /// Check that mapping contains valid units. - /// Update mapping for CASE_INSENSITIVE mode. - template static void validate_mapping(std::map &mapping, Options opts) { - for(auto &kv : mapping) { - if(kv.first.empty()) { - throw ValidationError("Unit must not be empty."); - } - if(!detail::isalpha(kv.first)) { - throw ValidationError("Unit must contain only letters."); - } - } - - // make all units lowercase if CASE_INSENSITIVE - if(opts & CASE_INSENSITIVE) { - std::map lower_mapping; - for(auto &kv : mapping) { - auto s = detail::to_lower(kv.first); - if(lower_mapping.count(s)) { - throw ValidationError(std::string("Several matching lowercase unit representations are found: ") + - s); - } - lower_mapping[detail::to_lower(kv.first)] = kv.second; - } - mapping = std::move(lower_mapping); - } - } - - /// Generate description like this: NUMBER [UNIT] - template static std::string generate_description(const std::string &name, Options opts) { - std::stringstream out; - out << detail::type_name() << ' '; - if(opts & UNIT_REQUIRED) { - out << name; - } else { - out << '[' << name << ']'; - } - return out.str(); - } -}; - -inline AsNumberWithUnit::Options operator|(const AsNumberWithUnit::Options &a, const AsNumberWithUnit::Options &b) { - return static_cast(static_cast(a) | static_cast(b)); -} - -/// Converts a human-readable size string (with unit literal) to uin64_t size. -/// Example: -/// "100" => 100 -/// "1 b" => 100 -/// "10Kb" => 10240 // you can configure this to be interpreted as kilobyte (*1000) or kibibyte (*1024) -/// "10 KB" => 10240 -/// "10 kb" => 10240 -/// "10 kib" => 10240 // *i, *ib are always interpreted as *bibyte (*1024) -/// "10kb" => 10240 -/// "2 MB" => 2097152 -/// "2 EiB" => 2^61 // Units up to exibyte are supported -class AsSizeValue : public AsNumberWithUnit { - public: - using result_t = std::uint64_t; - - /// If kb_is_1000 is true, - /// interpret 'kb', 'k' as 1000 and 'kib', 'ki' as 1024 - /// (same applies to higher order units as well). - /// Otherwise, interpret all literals as factors of 1024. - /// The first option is formally correct, but - /// the second interpretation is more wide-spread - /// (see https://en.wikipedia.org/wiki/Binary_prefix). - explicit AsSizeValue(bool kb_is_1000); - - private: - /// Get mapping - static std::map init_mapping(bool kb_is_1000); - - /// Cache calculated mapping - static std::map get_mapping(bool kb_is_1000); -}; - -namespace detail { -/// Split a string into a program name and command line arguments -/// the string is assumed to contain a file name followed by other arguments -/// the return value contains is a pair with the first argument containing the program name and the second -/// everything else. -CLI11_INLINE std::pair split_program_name(std::string commandline); - -} // namespace detail -/// @} - - - - -CLI11_INLINE std::string Validator::operator()(std::string &str) const { - std::string retstring; - if(active_) { - if(non_modifying_) { - std::string value = str; - retstring = func_(value); - } else { - retstring = func_(str); - } - } - return retstring; -} - -CLI11_NODISCARD CLI11_INLINE Validator Validator::description(std::string validator_desc) const { - Validator newval(*this); - newval.desc_function_ = [validator_desc]() { return validator_desc; }; - return newval; -} - -CLI11_INLINE Validator Validator::operator&(const Validator &other) const { - Validator newval; - - newval._merge_description(*this, other, " AND "); - - // Give references (will make a copy in lambda function) - const std::function &f1 = func_; - const std::function &f2 = other.func_; - - newval.func_ = [f1, f2](std::string &input) { - std::string s1 = f1(input); - std::string s2 = f2(input); - if(!s1.empty() && !s2.empty()) - return std::string("(") + s1 + ") AND (" + s2 + ")"; - return s1 + s2; - }; - - newval.active_ = active_ && other.active_; - newval.application_index_ = application_index_; - return newval; -} - -CLI11_INLINE Validator Validator::operator|(const Validator &other) const { - Validator newval; - - newval._merge_description(*this, other, " OR "); - - // Give references (will make a copy in lambda function) - const std::function &f1 = func_; - const std::function &f2 = other.func_; - - newval.func_ = [f1, f2](std::string &input) { - std::string s1 = f1(input); - std::string s2 = f2(input); - if(s1.empty() || s2.empty()) - return std::string(); - - return std::string("(") + s1 + ") OR (" + s2 + ")"; - }; - newval.active_ = active_ && other.active_; - newval.application_index_ = application_index_; - return newval; -} - -CLI11_INLINE Validator Validator::operator!() const { - Validator newval; - const std::function &dfunc1 = desc_function_; - newval.desc_function_ = [dfunc1]() { - auto str = dfunc1(); - return (!str.empty()) ? std::string("NOT ") + str : std::string{}; - }; - // Give references (will make a copy in lambda function) - const std::function &f1 = func_; - - newval.func_ = [f1, dfunc1](std::string &test) -> std::string { - std::string s1 = f1(test); - if(s1.empty()) { - return std::string("check ") + dfunc1() + " succeeded improperly"; - } - return std::string{}; - }; - newval.active_ = active_; - newval.application_index_ = application_index_; - return newval; -} - -CLI11_INLINE void -Validator::_merge_description(const Validator &val1, const Validator &val2, const std::string &merger) { - - const std::function &dfunc1 = val1.desc_function_; - const std::function &dfunc2 = val2.desc_function_; - - desc_function_ = [=]() { - std::string f1 = dfunc1(); - std::string f2 = dfunc2(); - if((f1.empty()) || (f2.empty())) { - return f1 + f2; - } - return std::string(1, '(') + f1 + ')' + merger + '(' + f2 + ')'; - }; -} - -namespace detail { - -#if defined CLI11_HAS_FILESYSTEM && CLI11_HAS_FILESYSTEM > 0 -CLI11_INLINE path_type check_path(const char *file) noexcept { - std::error_code ec; - auto stat = std::filesystem::status(to_path(file), ec); - if(ec) { - return path_type::nonexistent; - } - switch(stat.type()) { - case std::filesystem::file_type::none: // LCOV_EXCL_LINE - case std::filesystem::file_type::not_found: - return path_type::nonexistent; // LCOV_EXCL_LINE - case std::filesystem::file_type::directory: - return path_type::directory; - case std::filesystem::file_type::symlink: - case std::filesystem::file_type::block: - case std::filesystem::file_type::character: - case std::filesystem::file_type::fifo: - case std::filesystem::file_type::socket: - case std::filesystem::file_type::regular: - case std::filesystem::file_type::unknown: - default: - return path_type::file; - } -} -#else -CLI11_INLINE path_type check_path(const char *file) noexcept { -#if defined(_MSC_VER) - struct __stat64 buffer; - if(_stat64(file, &buffer) == 0) { - return ((buffer.st_mode & S_IFDIR) != 0) ? path_type::directory : path_type::file; - } -#else - struct stat buffer; - if(stat(file, &buffer) == 0) { - return ((buffer.st_mode & S_IFDIR) != 0) ? path_type::directory : path_type::file; - } -#endif - return path_type::nonexistent; -} -#endif - -CLI11_INLINE ExistingFileValidator::ExistingFileValidator() : Validator("FILE") { - func_ = [](std::string &filename) { - auto path_result = check_path(filename.c_str()); - if(path_result == path_type::nonexistent) { - return "File does not exist: " + filename; - } - if(path_result == path_type::directory) { - return "File is actually a directory: " + filename; - } - return std::string(); - }; -} - -CLI11_INLINE ExistingDirectoryValidator::ExistingDirectoryValidator() : Validator("DIR") { - func_ = [](std::string &filename) { - auto path_result = check_path(filename.c_str()); - if(path_result == path_type::nonexistent) { - return "Directory does not exist: " + filename; - } - if(path_result == path_type::file) { - return "Directory is actually a file: " + filename; - } - return std::string(); - }; -} - -CLI11_INLINE ExistingPathValidator::ExistingPathValidator() : Validator("PATH(existing)") { - func_ = [](std::string &filename) { - auto path_result = check_path(filename.c_str()); - if(path_result == path_type::nonexistent) { - return "Path does not exist: " + filename; - } - return std::string(); - }; -} - -CLI11_INLINE NonexistentPathValidator::NonexistentPathValidator() : Validator("PATH(non-existing)") { - func_ = [](std::string &filename) { - auto path_result = check_path(filename.c_str()); - if(path_result != path_type::nonexistent) { - return "Path already exists: " + filename; - } - return std::string(); - }; -} - -CLI11_INLINE IPV4Validator::IPV4Validator() : Validator("IPV4") { - func_ = [](std::string &ip_addr) { - auto result = CLI::detail::split(ip_addr, '.'); - if(result.size() != 4) { - return std::string("Invalid IPV4 address must have four parts (") + ip_addr + ')'; - } - int num = 0; - for(const auto &var : result) { - using CLI::detail::lexical_cast; - bool retval = lexical_cast(var, num); - if(!retval) { - return std::string("Failed parsing number (") + var + ')'; - } - if(num < 0 || num > 255) { - return std::string("Each IP number must be between 0 and 255 ") + var; - } - } - return std::string{}; - }; -} - -CLI11_INLINE EscapedStringTransformer::EscapedStringTransformer() { - func_ = [](std::string &str) { - try { - if(str.size() > 1 && (str.front() == '\"' || str.front() == '\'' || str.front() == '`') && - str.front() == str.back()) { - process_quoted_string(str); - } else if(str.find_first_of('\\') != std::string::npos) { - if(detail::is_binary_escaped_string(str)) { - str = detail::extract_binary_string(str); - } else { - str = remove_escaped_characters(str); - } - } - return std::string{}; - } catch(const std::invalid_argument &ia) { - return std::string(ia.what()); - } - }; -} -} // namespace detail - -CLI11_INLINE FileOnDefaultPath::FileOnDefaultPath(std::string default_path, bool enableErrorReturn) - : Validator("FILE") { - func_ = [default_path, enableErrorReturn](std::string &filename) { - auto path_result = detail::check_path(filename.c_str()); - if(path_result == detail::path_type::nonexistent) { - std::string test_file_path = default_path; - if(default_path.back() != '/' && default_path.back() != '\\') { - // Add folder separator - test_file_path += '/'; - } - test_file_path.append(filename); - path_result = detail::check_path(test_file_path.c_str()); - if(path_result == detail::path_type::file) { - filename = test_file_path; - } else { - if(enableErrorReturn) { - return "File does not exist: " + filename; - } - } - } - return std::string{}; - }; -} - -CLI11_INLINE AsSizeValue::AsSizeValue(bool kb_is_1000) : AsNumberWithUnit(get_mapping(kb_is_1000)) { - if(kb_is_1000) { - description("SIZE [b, kb(=1000b), kib(=1024b), ...]"); - } else { - description("SIZE [b, kb(=1024b), ...]"); - } -} - -CLI11_INLINE std::map AsSizeValue::init_mapping(bool kb_is_1000) { - std::map m; - result_t k_factor = kb_is_1000 ? 1000 : 1024; - result_t ki_factor = 1024; - result_t k = 1; - result_t ki = 1; - m["b"] = 1; - for(std::string p : {"k", "m", "g", "t", "p", "e"}) { - k *= k_factor; - ki *= ki_factor; - m[p] = k; - m[p + "b"] = k; - m[p + "i"] = ki; - m[p + "ib"] = ki; - } - return m; -} - -CLI11_INLINE std::map AsSizeValue::get_mapping(bool kb_is_1000) { - if(kb_is_1000) { - static auto m = init_mapping(true); - return m; - } - static auto m = init_mapping(false); - return m; -} - -namespace detail { - -CLI11_INLINE std::pair split_program_name(std::string commandline) { - // try to determine the programName - std::pair vals; - trim(commandline); - auto esp = commandline.find_first_of(' ', 1); - while(detail::check_path(commandline.substr(0, esp).c_str()) != path_type::file) { - esp = commandline.find_first_of(' ', esp + 1); - if(esp == std::string::npos) { - // if we have reached the end and haven't found a valid file just assume the first argument is the - // program name - if(commandline[0] == '"' || commandline[0] == '\'' || commandline[0] == '`') { - bool embeddedQuote = false; - auto keyChar = commandline[0]; - auto end = commandline.find_first_of(keyChar, 1); - while((end != std::string::npos) && (commandline[end - 1] == '\\')) { // deal with escaped quotes - end = commandline.find_first_of(keyChar, end + 1); - embeddedQuote = true; - } - if(end != std::string::npos) { - vals.first = commandline.substr(1, end - 1); - esp = end + 1; - if(embeddedQuote) { - vals.first = find_and_replace(vals.first, std::string("\\") + keyChar, std::string(1, keyChar)); - } - } else { - esp = commandline.find_first_of(' ', 1); - } - } else { - esp = commandline.find_first_of(' ', 1); - } - - break; - } - } - if(vals.first.empty()) { - vals.first = commandline.substr(0, esp); - rtrim(vals.first); - } - - // strip the program name - vals.second = (esp < commandline.length() - 1) ? commandline.substr(esp + 1) : std::string{}; - ltrim(vals.second); - return vals; -} - -} // namespace detail -/// @} - - - - -class Option; -class App; - -/// This enum signifies the type of help requested -/// -/// This is passed in by App; all user classes must accept this as -/// the second argument. - -enum class AppFormatMode { - Normal, ///< The normal, detailed help - All, ///< A fully expanded help - Sub, ///< Used when printed as part of expanded subcommand -}; - -/// This is the minimum requirements to run a formatter. -/// -/// A user can subclass this is if they do not care at all -/// about the structure in CLI::Formatter. -class FormatterBase { - protected: - /// @name Options - ///@{ - - /// The width of the left column (options/flags/subcommands) - std::size_t column_width_{30}; - - /// The width of the right column (description of options/flags/subcommands) - std::size_t right_column_width_{65}; - - /// The width of the description paragraph at the top of help - std::size_t description_paragraph_width_{80}; - - /// The width of the footer paragraph - std::size_t footer_paragraph_width_{80}; - - /// @brief The required help printout labels (user changeable) - /// Values are Needs, Excludes, etc. - std::map labels_{}; - - ///@} - /// @name Basic - ///@{ - - public: - FormatterBase() = default; - FormatterBase(const FormatterBase &) = default; - FormatterBase(FormatterBase &&) = default; - FormatterBase &operator=(const FormatterBase &) = default; - FormatterBase &operator=(FormatterBase &&) = default; - - /// Adding a destructor in this form to work around bug in GCC 4.7 - virtual ~FormatterBase() noexcept {} // NOLINT(modernize-use-equals-default) - - /// This is the key method that puts together help - virtual std::string make_help(const App *, std::string, AppFormatMode) const = 0; - - ///@} - /// @name Setters - ///@{ - - /// Set the "REQUIRED" label - void label(std::string key, std::string val) { labels_[key] = val; } - - /// Set the left column width (options/flags/subcommands) - void column_width(std::size_t val) { column_width_ = val; } - - /// Set the right column width (description of options/flags/subcommands) - void right_column_width(std::size_t val) { right_column_width_ = val; } - - /// Set the description paragraph width at the top of help - void description_paragraph_width(std::size_t val) { description_paragraph_width_ = val; } - - /// Set the footer paragraph width - void footer_paragraph_width(std::size_t val) { footer_paragraph_width_ = val; } - - ///@} - /// @name Getters - ///@{ - - /// Get the current value of a name (REQUIRED, etc.) - CLI11_NODISCARD std::string get_label(std::string key) const { - if(labels_.find(key) == labels_.end()) - return key; - return labels_.at(key); - } - - /// Get the current left column width (options/flags/subcommands) - CLI11_NODISCARD std::size_t get_column_width() const { return column_width_; } - - /// Get the current right column width (description of options/flags/subcommands) - CLI11_NODISCARD std::size_t get_right_column_width() const { return right_column_width_; } - - /// Get the current description paragraph width at the top of help - CLI11_NODISCARD std::size_t get_description_paragraph_width() const { return description_paragraph_width_; } - - /// Get the current footer paragraph width - CLI11_NODISCARD std::size_t get_footer_paragraph_width() const { return footer_paragraph_width_; } - - ///@} -}; - -/// This is a specialty override for lambda functions -class FormatterLambda final : public FormatterBase { - using funct_t = std::function; - - /// The lambda to hold and run - funct_t lambda_; - - public: - /// Create a FormatterLambda with a lambda function - explicit FormatterLambda(funct_t funct) : lambda_(std::move(funct)) {} - - /// Adding a destructor (mostly to make GCC 4.7 happy) - ~FormatterLambda() noexcept override {} // NOLINT(modernize-use-equals-default) - - /// This will simply call the lambda function - std::string make_help(const App *app, std::string name, AppFormatMode mode) const override { - return lambda_(app, name, mode); - } -}; - -/// This is the default Formatter for CLI11. It pretty prints help output, and is broken into quite a few -/// overridable methods, to be highly customizable with minimal effort. -class Formatter : public FormatterBase { - public: - Formatter() = default; - Formatter(const Formatter &) = default; - Formatter(Formatter &&) = default; - Formatter &operator=(const Formatter &) = default; - Formatter &operator=(Formatter &&) = default; - - /// @name Overridables - ///@{ - - /// This prints out a group of options with title - /// - CLI11_NODISCARD virtual std::string - make_group(std::string group, bool is_positional, std::vector opts) const; - - /// This prints out just the positionals "group" - virtual std::string make_positionals(const App *app) const; - - /// This prints out all the groups of options - std::string make_groups(const App *app, AppFormatMode mode) const; - - /// This prints out all the subcommands - virtual std::string make_subcommands(const App *app, AppFormatMode mode) const; - - /// This prints out a subcommand - virtual std::string make_subcommand(const App *sub) const; - - /// This prints out a subcommand in help-all - virtual std::string make_expanded(const App *sub, AppFormatMode mode) const; - - /// This prints out all the groups of options - virtual std::string make_footer(const App *app) const; - - /// This displays the description line - virtual std::string make_description(const App *app) const; - - /// This displays the usage line - virtual std::string make_usage(const App *app, std::string name) const; - - /// This puts everything together - std::string make_help(const App *app, std::string, AppFormatMode mode) const override; - - ///@} - /// @name Options - ///@{ - - /// This prints out an option help line, either positional or optional form - virtual std::string make_option(const Option *, bool) const; - - /// @brief This is the name part of an option, Default: left column - virtual std::string make_option_name(const Option *, bool) const; - - /// @brief This is the options part of the name, Default: combined into left column - virtual std::string make_option_opts(const Option *) const; - - /// @brief This is the description. Default: Right column, on new line if left column too large - virtual std::string make_option_desc(const Option *) const; - - /// @brief This is used to print the name on the USAGE line - virtual std::string make_option_usage(const Option *opt) const; - - ///@} -}; - - - - -using results_t = std::vector; -/// callback function definition -using callback_t = std::function; - -class Option; -class App; - -using Option_p = std::unique_ptr