diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 7f5566fb97..5d5454e97b 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,4 +1,4 @@ -FROM node:18-bullseye +FROM node:24.16.0-bullseye RUN useradd -m -s /bin/bash vscode RUN mkdir -p /workspaces && chown -R vscode:vscode /workspaces diff --git a/.do/gitnexus/Dockerfile b/.do/gitnexus/Dockerfile index a1266d5a13..58a79e087e 100644 --- a/.do/gitnexus/Dockerfile +++ b/.do/gitnexus/Dockerfile @@ -5,7 +5,7 @@ # startup. A fresh index only requires rsync + container restart — no # image rebuild on every push. -FROM node:24-slim +FROM node:24.16.0-slim ARG GITNEXUS_VERSION=1.5.3 diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index ae9e6d8e4b..6524947ba2 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -26,7 +26,7 @@ Project maintainers have the right and responsibility to remove, edit, or reject ## 1. Development Setup -1. Use Node.js v20.19.0+ or ^22.12.0 or >= 23.0.0. +1. Use Node.js v24.16.0. 2. Run `npm run smart-reinstall` to install dependencies (uses Turborepo). Use `npm run reinstall` for a clean install, or `npm ci` for a fresh lockfile-based install. 3. Build all compiled code: `npm run build`. 4. Setup and run unit tests: diff --git a/.github/scripts/sync-helm-chart-tags.sh b/.github/scripts/sync-helm-chart-tags.sh new file mode 100755 index 0000000000..f54048a1ea --- /dev/null +++ b/.github/scripts/sync-helm-chart-tags.sh @@ -0,0 +1,237 @@ +#!/usr/bin/env bash +set -euo pipefail + +CHART_PATH="${CHART_PATH:-helm/librechat/Chart.yaml}" +DEFAULT_BRANCH="${DEFAULT_BRANCH:-main}" +BASE_REF="${BASE_REF:-refs/remotes/origin/${DEFAULT_BRANCH}}" +BACKFILL_FROM_VERSION="${BACKFILL_FROM_VERSION:-1.9.0}" +PUSH_TAGS="${PUSH_TAGS:-false}" +TAG_PREFIX="${TAG_PREFIX:-chart-}" +GITHUB_SERVER_URL="${GITHUB_SERVER_URL:-https://github.com}" +DISPATCH_WORKFLOW="${DISPATCH_WORKFLOW:-}" +RELEASE_EXISTING_TAG="${RELEASE_EXISTING_TAG:-}" +SEMVER_REGEX='^(0|[1-9][0-9]*)[.](0|[1-9][0-9]*)[.](0|[1-9][0-9]*)(-[0-9A-Za-z-]+([.][0-9A-Za-z-]+)*)?([+][0-9A-Za-z-]+([.][0-9A-Za-z-]+)*)?$' + +fail() { + printf '::error::%s\n' "$1" >&2 + exit 1 +} + +git_auth_header() { + token="$(printf 'x-access-token:%s' "$GITHUB_TOKEN" | base64 | tr -d '\n')" + printf 'AUTHORIZATION: basic %s' "$token" +} + +git_with_auth() { + if [ -n "${GITHUB_TOKEN:-}" ]; then + git -c "http.extraheader=$(git_auth_header)" "$@" + return + fi + + git "$@" +} + +dispatch_release() { + tag="$1" + + if [ -z "$DISPATCH_WORKFLOW" ]; then + return + fi + + if [ -z "${GITHUB_REPOSITORY:-}" ]; then + fail "GITHUB_REPOSITORY is required to dispatch ${DISPATCH_WORKFLOW}" + fi + + if [[ ! "$GITHUB_REPOSITORY" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]; then + fail "Unexpected repository name: ${GITHUB_REPOSITORY}" + fi + + if [[ ! "$DISPATCH_WORKFLOW" =~ ^[A-Za-z0-9_.-]+[.]ya?ml$ ]]; then + fail "Unexpected workflow file: ${DISPATCH_WORKFLOW}" + fi + + token="${GH_TOKEN:-${GITHUB_TOKEN:-}}" + if [ -z "$token" ]; then + fail "GH_TOKEN or GITHUB_TOKEN is required to dispatch ${DISPATCH_WORKFLOW}" + fi + + command -v gh >/dev/null || + fail "GitHub CLI is required to dispatch ${DISPATCH_WORKFLOW}" + + GH_TOKEN="$token" gh workflow run "$DISPATCH_WORKFLOW" \ + --repo "$GITHUB_REPOSITORY" \ + --ref "$DEFAULT_BRANCH" \ + -f "chart_tag=${tag}" +} + +version_less_than() { + left="${1%%[-+]*}" + right="${2%%[-+]*}" + + IFS=. read -r left_major left_minor left_patch <<<"$left" + IFS=. read -r right_major right_minor right_patch <<<"$right" + + if (( left_major != right_major )); then + (( left_major < right_major )) + return + fi + + if (( left_minor != right_minor )); then + (( left_minor < right_minor )) + return + fi + + (( left_patch < right_patch )) +} + +validate_chart_tag() { + tag="$1" + version="${tag#${TAG_PREFIX}}" + + git check-ref-format "refs/tags/${tag}" >/dev/null || + fail "Refusing to use invalid tag ${tag}" + + if [[ "$tag" != "${TAG_PREFIX}"* || ! "$version" =~ $SEMVER_REGEX ]]; then + fail "Chart tags must use the form ${TAG_PREFIX}, for example ${TAG_PREFIX}2.0.5" + fi +} + +dispatch_existing_tag() { + tag="$1" + + if [ -z "$tag" ]; then + return + fi + + validate_chart_tag "$tag" + + if [ "$PUSH_TAGS" != "true" ]; then + printf 'Would dispatch release workflow for existing %s.\n' "$tag" + return + fi + + if ! git_with_auth ls-remote --exit-code --tags origin "refs/tags/${tag}" >/dev/null 2>&1; then + fail "Remote tag ${tag} does not exist" + fi + + printf 'Dispatching release workflow for existing %s.\n' "$tag" + dispatch_release "$tag" +} + +chart_version_at() { + git show "${1}:${CHART_PATH}" 2>/dev/null | awk ' + /^version:[[:space:]]*/ { + value = $0 + sub(/^version:[[:space:]]*/, "", value) + sub(/[[:space:]]*#.*/, "", value) + gsub(/^[[:space:]"'\''"]+|[[:space:]"'\''"]+$/, "", value) + print value + exit + } + ' +} + +case "$PUSH_TAGS" in + true | false) ;; + *) fail "PUSH_TAGS must be true or false" ;; +esac + +if [[ ! "$BACKFILL_FROM_VERSION" =~ $SEMVER_REGEX ]]; then + fail "BACKFILL_FROM_VERSION must be a valid SemVer value" +fi + +git rev-parse --verify "${BASE_REF}^{commit}" >/dev/null || + fail "Unable to resolve ${BASE_REF}; fetch ${DEFAULT_BRANCH} before running this script" + +history_file="$(mktemp)" +versions_file="$(mktemp)" +seen_file="$(mktemp)" +missing_file="$(mktemp)" +cleanup() { + rm -f "$history_file" "$versions_file" "$seen_file" "$missing_file" +} +trap cleanup EXIT + +git log --first-parent --reverse --format=%H "$BASE_REF" -- "$CHART_PATH" >"$history_file" + +if [ ! -s "$history_file" ]; then + fail "No history found for ${CHART_PATH} on ${BASE_REF}" +fi + +while IFS= read -r commit; do + version="$(chart_version_at "$commit")" + + if [ -z "$version" ]; then + continue + fi + + if [[ ! "$version" =~ $SEMVER_REGEX ]]; then + fail "${CHART_PATH} has invalid SemVer '${version}' at ${commit}" + fi + + if version_less_than "$version" "$BACKFILL_FROM_VERSION"; then + continue + fi + + if grep -Fqx "$version" "$seen_file"; then + continue + fi + + printf '%s\n' "$version" >>"$seen_file" + printf '%s\t%s\n' "$version" "$commit" >>"$versions_file" +done <"$history_file" + +if [ ! -s "$versions_file" ]; then + fail "No chart versions found in ${CHART_PATH}" +fi + +while IFS="$(printf '\t')" read -r version commit; do + tag="${TAG_PREFIX}${version}" + + validate_chart_tag "$tag" + + if git rev-parse --quiet --verify "refs/tags/${tag}" >/dev/null; then + continue + fi + + printf '%s\t%s\n' "$tag" "$commit" >>"$missing_file" +done <"$versions_file" + +if [ ! -s "$missing_file" ]; then + printf 'All chart versions on %s already have %s tags.\n' "$BASE_REF" "$TAG_PREFIX" + dispatch_existing_tag "$RELEASE_EXISTING_TAG" + exit 0 +fi + +while IFS="$(printf '\t')" read -r tag commit; do + short_commit="$(git rev-parse --short "$commit")" + + if [ "$PUSH_TAGS" != "true" ]; then + printf 'Would create %s at %s.\n' "$tag" "$short_commit" + continue + fi + + if git_with_auth ls-remote --exit-code --tags origin "refs/tags/${tag}" >/dev/null 2>&1; then + printf 'Remote tag %s already exists; dispatching release workflow.\n' "$tag" + dispatch_release "$tag" + continue + fi + + git tag "$tag" "$commit" + + if git_with_auth push origin "refs/tags/${tag}"; then + printf 'Created %s at %s.\n' "$tag" "$short_commit" + dispatch_release "$tag" + continue + fi + + if git_with_auth ls-remote --exit-code --tags origin "refs/tags/${tag}" >/dev/null 2>&1; then + printf 'Remote tag %s was created concurrently; dispatching release workflow.\n' "$tag" + dispatch_release "$tag" + continue + fi + + fail "Failed to push ${tag}" +done <"$missing_file" + +dispatch_existing_tag "$RELEASE_EXISTING_TAG" diff --git a/.github/workflows/backend-review.yml b/.github/workflows/backend-review.yml index 46366b2df0..e25e884fee 100644 --- a/.github/workflows/backend-review.yml +++ b/.github/workflows/backend-review.yml @@ -20,10 +20,10 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Use Node.js 20.19 + - name: Use Node.js 24.16.0 uses: actions/setup-node@v4 with: - node-version: '20.19' + node-version: '24.16.0' - name: Restore node_modules cache id: cache-node-modules @@ -35,7 +35,7 @@ jobs: packages/api/node_modules packages/data-provider/node_modules packages/data-schemas/node_modules - key: node-modules-backend-${{ runner.os }}-20.19-${{ hashFiles('package-lock.json') }} + key: node-modules-backend-${{ runner.os }}-24.16.0-${{ hashFiles('package-lock.json') }} - name: Install dependencies if: steps.cache-node-modules.outputs.cache-hit != 'true' @@ -103,10 +103,10 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Use Node.js 20.19 + - name: Use Node.js 24.16.0 uses: actions/setup-node@v4 with: - node-version: '20.19' + node-version: '24.16.0' - name: Restore node_modules cache id: cache-node-modules @@ -118,7 +118,7 @@ jobs: packages/api/node_modules packages/data-provider/node_modules packages/data-schemas/node_modules - key: node-modules-backend-${{ runner.os }}-20.19-${{ hashFiles('package-lock.json') }} + key: node-modules-backend-${{ runner.os }}-24.16.0-${{ hashFiles('package-lock.json') }} - name: Install dependencies if: steps.cache-node-modules.outputs.cache-hit != 'true' @@ -162,10 +162,10 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Use Node.js 20.19 + - name: Use Node.js 24.16.0 uses: actions/setup-node@v4 with: - node-version: '20.19' + node-version: '24.16.0' - name: Restore node_modules cache id: cache-node-modules @@ -177,7 +177,7 @@ jobs: packages/api/node_modules packages/data-provider/node_modules packages/data-schemas/node_modules - key: node-modules-backend-${{ runner.os }}-20.19-${{ hashFiles('package-lock.json') }} + key: node-modules-backend-${{ runner.os }}-24.16.0-${{ hashFiles('package-lock.json') }} - name: Install dependencies if: steps.cache-node-modules.outputs.cache-hit != 'true' @@ -231,10 +231,10 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Use Node.js 20.19 + - name: Use Node.js 24.16.0 uses: actions/setup-node@v4 with: - node-version: '20.19' + node-version: '24.16.0' - name: Restore node_modules cache id: cache-node-modules @@ -246,7 +246,7 @@ jobs: packages/api/node_modules packages/data-provider/node_modules packages/data-schemas/node_modules - key: node-modules-backend-${{ runner.os }}-20.19-${{ hashFiles('package-lock.json') }} + key: node-modules-backend-${{ runner.os }}-24.16.0-${{ hashFiles('package-lock.json') }} - name: Install dependencies if: steps.cache-node-modules.outputs.cache-hit != 'true' @@ -289,10 +289,10 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Use Node.js 20.19 + - name: Use Node.js 24.16.0 uses: actions/setup-node@v4 with: - node-version: '20.19' + node-version: '24.16.0' - name: Restore node_modules cache id: cache-node-modules @@ -304,7 +304,7 @@ jobs: packages/api/node_modules packages/data-provider/node_modules packages/data-schemas/node_modules - key: node-modules-backend-${{ runner.os }}-20.19-${{ hashFiles('package-lock.json') }} + key: node-modules-backend-${{ runner.os }}-24.16.0-${{ hashFiles('package-lock.json') }} - name: Install dependencies if: steps.cache-node-modules.outputs.cache-hit != 'true' @@ -327,10 +327,10 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Use Node.js 20.19 + - name: Use Node.js 24.16.0 uses: actions/setup-node@v4 with: - node-version: '20.19' + node-version: '24.16.0' - name: Restore node_modules cache id: cache-node-modules @@ -342,7 +342,7 @@ jobs: packages/api/node_modules packages/data-provider/node_modules packages/data-schemas/node_modules - key: node-modules-backend-${{ runner.os }}-20.19-${{ hashFiles('package-lock.json') }} + key: node-modules-backend-${{ runner.os }}-24.16.0-${{ hashFiles('package-lock.json') }} - name: Install dependencies if: steps.cache-node-modules.outputs.cache-hit != 'true' @@ -375,10 +375,10 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Use Node.js 20.19 + - name: Use Node.js 24.16.0 uses: actions/setup-node@v4 with: - node-version: '20.19' + node-version: '24.16.0' - name: Restore node_modules cache id: cache-node-modules @@ -390,7 +390,7 @@ jobs: packages/api/node_modules packages/data-provider/node_modules packages/data-schemas/node_modules - key: node-modules-backend-${{ runner.os }}-20.19-${{ hashFiles('package-lock.json') }} + key: node-modules-backend-${{ runner.os }}-24.16.0-${{ hashFiles('package-lock.json') }} - name: Install dependencies if: steps.cache-node-modules.outputs.cache-hit != 'true' diff --git a/.github/workflows/cache-integration-tests.yml b/.github/workflows/cache-integration-tests.yml index c8f10f388b..3e4c5418ae 100644 --- a/.github/workflows/cache-integration-tests.yml +++ b/.github/workflows/cache-integration-tests.yml @@ -28,10 +28,10 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - - name: Use Node.js 20.x + - name: Use Node.js 24.16.0 uses: actions/setup-node@v4 with: - node-version: 20 + node-version: '24.16.0' cache: 'npm' - name: Install Redis tools diff --git a/.github/workflows/client.yml b/.github/workflows/client.yml index 881efa0ebe..e4dc8c5626 100644 --- a/.github/workflows/client.yml +++ b/.github/workflows/client.yml @@ -27,7 +27,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v4 with: - node-version: '20.x' + node-version: '24.16.0' - name: Install client dependencies run: cd packages/client && npm ci @@ -77,7 +77,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v4 with: - node-version: '20.x' + node-version: '24.16.0' registry-url: 'https://registry.npmjs.org' - name: Install npm with OIDC support diff --git a/.github/workflows/data-provider.yml b/.github/workflows/data-provider.yml index 3a9db4d8e5..eae746ece9 100644 --- a/.github/workflows/data-provider.yml +++ b/.github/workflows/data-provider.yml @@ -23,7 +23,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 20 + node-version: '24.16.0' - run: cd packages/data-provider && npm ci - run: cd packages/data-provider && npm run build - name: Pack package @@ -50,7 +50,7 @@ jobs: steps: - uses: actions/setup-node@v4 with: - node-version: 20 + node-version: '24.16.0' registry-url: 'https://registry.npmjs.org' - name: Install npm with OIDC support diff --git a/.github/workflows/data-schemas.yml b/.github/workflows/data-schemas.yml index 977a6eb4c3..bb8f90ea84 100644 --- a/.github/workflows/data-schemas.yml +++ b/.github/workflows/data-schemas.yml @@ -27,7 +27,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v4 with: - node-version: '20.x' + node-version: '24.16.0' - name: Install dependencies run: cd packages/data-schemas && npm ci @@ -77,7 +77,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v4 with: - node-version: '20.x' + node-version: '24.16.0' registry-url: 'https://registry.npmjs.org' - name: Install npm with OIDC support diff --git a/.github/workflows/eslint-ci.yml b/.github/workflows/eslint-ci.yml index 85de6fa18e..3710f8a02e 100644 --- a/.github/workflows/eslint-ci.yml +++ b/.github/workflows/eslint-ci.yml @@ -27,10 +27,10 @@ jobs: with: fetch-depth: 0 - - name: Set up Node.js 20.x + - name: Set up Node.js 24.16.0 uses: actions/setup-node@v4 with: - node-version: 20 + node-version: '24.16.0' cache: npm - name: Install dependencies diff --git a/.github/workflows/frontend-review.yml b/.github/workflows/frontend-review.yml index 0021124192..b84d145bee 100644 --- a/.github/workflows/frontend-review.yml +++ b/.github/workflows/frontend-review.yml @@ -20,10 +20,10 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Use Node.js 20.19 + - name: Use Node.js 24.16.0 uses: actions/setup-node@v4 with: - node-version: '20.19' + node-version: '24.16.0' - name: Restore node_modules cache id: cache-node-modules @@ -34,7 +34,7 @@ jobs: client/node_modules packages/client/node_modules packages/data-provider/node_modules - key: node-modules-frontend-${{ runner.os }}-20.19-${{ hashFiles('package-lock.json') }} + key: node-modules-frontend-${{ runner.os }}-24.16.0-${{ hashFiles('package-lock.json') }} - name: Install dependencies if: steps.cache-node-modules.outputs.cache-hit != 'true' @@ -84,10 +84,10 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Use Node.js 20.19 + - name: Use Node.js 24.16.0 uses: actions/setup-node@v4 with: - node-version: '20.19' + node-version: '24.16.0' - name: Restore node_modules cache id: cache-node-modules @@ -98,7 +98,7 @@ jobs: client/node_modules packages/client/node_modules packages/data-provider/node_modules - key: node-modules-frontend-${{ runner.os }}-20.19-${{ hashFiles('package-lock.json') }} + key: node-modules-frontend-${{ runner.os }}-24.16.0-${{ hashFiles('package-lock.json') }} - name: Install dependencies if: steps.cache-node-modules.outputs.cache-hit != 'true' @@ -128,10 +128,10 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Use Node.js 20.19 + - name: Use Node.js 24.16.0 uses: actions/setup-node@v4 with: - node-version: '20.19' + node-version: '24.16.0' - name: Restore node_modules cache id: cache-node-modules @@ -142,7 +142,7 @@ jobs: client/node_modules packages/client/node_modules packages/data-provider/node_modules - key: node-modules-frontend-${{ runner.os }}-20.19-${{ hashFiles('package-lock.json') }} + key: node-modules-frontend-${{ runner.os }}-24.16.0-${{ hashFiles('package-lock.json') }} - name: Install dependencies if: steps.cache-node-modules.outputs.cache-hit != 'true' @@ -172,10 +172,10 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Use Node.js 20.19 + - name: Use Node.js 24.16.0 uses: actions/setup-node@v4 with: - node-version: '20.19' + node-version: '24.16.0' - name: Restore node_modules cache id: cache-node-modules @@ -186,7 +186,7 @@ jobs: client/node_modules packages/client/node_modules packages/data-provider/node_modules - key: node-modules-frontend-${{ runner.os }}-20.19-${{ hashFiles('package-lock.json') }} + key: node-modules-frontend-${{ runner.os }}-24.16.0-${{ hashFiles('package-lock.json') }} - name: Install dependencies if: steps.cache-node-modules.outputs.cache-hit != 'true' diff --git a/.github/workflows/gitnexus-index.yml b/.github/workflows/gitnexus-index.yml index d3b8ca95e2..21c3f69321 100644 --- a/.github/workflows/gitnexus-index.yml +++ b/.github/workflows/gitnexus-index.yml @@ -136,7 +136,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: 24 + node-version: '24.16.0' - name: Install GitNexus CLI working-directory: ${{ runner.temp }} diff --git a/.github/workflows/helmcharts.yml b/.github/workflows/helmcharts.yml index 9e08c189a7..9e0308ec72 100644 --- a/.github/workflows/helmcharts.yml +++ b/.github/workflows/helmcharts.yml @@ -5,18 +5,54 @@ on: push: tags: - "chart-*" + workflow_dispatch: + inputs: + chart_tag: + description: "Existing chart tag to release, for example chart-2.0.5" + required: true + type: string jobs: release: permissions: - contents: write + contents: read packages: write runs-on: ubuntu-latest + env: + CHART_REPOSITORY: ${{ github.repository_owner }}/librechat-chart steps: + - name: Resolve chart tag + id: chart-version + env: + EVENT_NAME: ${{ github.event_name }} + INPUT_CHART_TAG: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.chart_tag || '' }} + REF_NAME: ${{ github.ref_name }} + run: | + set -euo pipefail + CHART_TAG="$REF_NAME" + if [ "$EVENT_NAME" = "workflow_dispatch" ]; then + CHART_TAG="$INPUT_CHART_TAG" + fi + + CHART_VERSION="${CHART_TAG#chart-}" + SEMVER_REGEX='^[0-9]+[.][0-9]+[.][0-9]+(-[0-9A-Za-z.-]+)?([+][0-9A-Za-z.-]+)?$' + if [[ "$CHART_TAG" != chart-* || ! "$CHART_VERSION" =~ $SEMVER_REGEX ]]; then + echo "::error::Chart tags must use the form chart-, for example chart-2.0.3" + exit 1 + fi + + { + printf 'CHART_REF=refs/tags/%s\n' "$CHART_TAG" + printf 'CHART_TAG=%s\n' "$CHART_TAG" + printf 'CHART_VERSION=%s\n' "$CHART_VERSION" + } >> "$GITHUB_OUTPUT" + - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 + persist-credentials: false + ref: ${{ steps.chart-version.outputs.CHART_REF }} - name: Configure Git run: | @@ -35,20 +71,6 @@ jobs: cd ../librechat-rag-api helm dependency build - - name: Get Chart Version - id: chart-version - env: - REF_NAME: ${{ github.ref_name }} - run: | - set -euo pipefail - CHART_VERSION="${REF_NAME#chart-}" - SEMVER_REGEX='^[0-9]+[.][0-9]+[.][0-9]+(-[0-9A-Za-z.-]+)?([+][0-9A-Za-z.-]+)?$' - if [[ "$REF_NAME" != chart-* || ! "$CHART_VERSION" =~ $SEMVER_REGEX ]]; then - echo "::error::Chart tags must use the form chart-, for example chart-2.0.3" - exit 1 - fi - printf 'CHART_VERSION=%s\n' "$CHART_VERSION" >> "$GITHUB_OUTPUT" - # Log in to GitHub Container Registry - name: Log in to GitHub Container Registry uses: docker/login-action@v3 @@ -63,7 +85,7 @@ jobs: uses: appany/helm-oci-chart-releaser@v0.4.2 with: name: librechat - repository: ${{ github.actor }}/librechat-chart + repository: ${{ env.CHART_REPOSITORY }} tag: ${{ steps.chart-version.outputs.CHART_VERSION }} path: helm/librechat registry: ghcr.io @@ -75,7 +97,7 @@ jobs: uses: appany/helm-oci-chart-releaser@v0.4.2 with: name: librechat-rag-api - repository: ${{ github.actor }}/librechat-chart + repository: ${{ env.CHART_REPOSITORY }} tag: ${{ steps.chart-version.outputs.CHART_VERSION }} path: helm/librechat-rag-api registry: ghcr.io diff --git a/.github/workflows/locize-i18n-sync.yml b/.github/workflows/locize-i18n-sync.yml index 18266a18e3..f5b80b865a 100644 --- a/.github/workflows/locize-i18n-sync.yml +++ b/.github/workflows/locize-i18n-sync.yml @@ -22,7 +22,7 @@ jobs: - name: Set Up Node.js uses: actions/setup-node@v4 with: - node-version: 20 + node-version: '24.16.0' - name: Install locize CLI run: npm install -g locize-cli diff --git a/.github/workflows/sync-helm-chart-tags.yml b/.github/workflows/sync-helm-chart-tags.yml new file mode 100644 index 0000000000..bde4c2f49a --- /dev/null +++ b/.github/workflows/sync-helm-chart-tags.yml @@ -0,0 +1,85 @@ +name: Sync Helm Chart Tags + +on: + push: + branches: + - main + workflow_dispatch: + inputs: + release_existing_tag: + description: "Existing chart-* tag to dispatch if tag creation succeeded but release dispatch failed" + required: false + type: string + +permissions: + contents: read + +concurrency: + group: sync-helm-chart-tags + cancel-in-progress: false + +jobs: + noop: + name: Ignore non-main push + if: github.event_name == 'push' && github.ref != 'refs/heads/main' + runs-on: ubuntu-latest + timeout-minutes: 1 + steps: + - name: Skip tag sync + run: echo "Helm chart tag sync only runs on main pushes or manual dispatch." + + sync: + name: Sync chart tags + if: github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + actions: write + contents: write + env: + BASE_REF: refs/remotes/origin/main + BACKFILL_FROM_VERSION: 1.9.0 + CHART_PATH: helm/librechat/Chart.yaml + DEFAULT_BRANCH: main + DISPATCH_WORKFLOW: helmcharts.yml + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPOSITORY: ${{ github.repository }} + RELEASE_EXISTING_TAG: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.release_existing_tag || '' }} + REPO_DIR: /tmp/librechat-sync + TAG_PREFIX: chart- + steps: + - name: Fetch main and tags + shell: bash + run: | + set -euo pipefail + + if [[ ! "$GITHUB_REPOSITORY" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]; then + echo "::error::Unexpected repository name: $GITHUB_REPOSITORY" + exit 1 + fi + + if [[ "$GITHUB_SERVER_URL" != "https://github.com" ]]; then + echo "::error::Unexpected GitHub server URL: $GITHUB_SERVER_URL" + exit 1 + fi + + rm -rf "$REPO_DIR" + git init "$REPO_DIR" + cd "$REPO_DIR" + git remote add origin "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git" + AUTH_HEADER="$(printf 'x-access-token:%s' "$GITHUB_TOKEN" | base64 | tr -d '\n')" + git -c "http.extraheader=AUTHORIZATION: basic ${AUTH_HEADER}" \ + fetch --prune --force --tags origin \ + "+refs/heads/${DEFAULT_BRANCH}:refs/remotes/origin/${DEFAULT_BRANCH}" + git checkout --detach "$BASE_REF" + + - name: Create missing chart tags + shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PUSH_TAGS: "true" + run: | + set -euo pipefail + cd "$REPO_DIR" + .github/scripts/sync-helm-chart-tags.sh diff --git a/.github/workflows/unused-packages.yml b/.github/workflows/unused-packages.yml index a957bed2e8..5401d37d5b 100644 --- a/.github/workflows/unused-packages.yml +++ b/.github/workflows/unused-packages.yml @@ -20,10 +20,10 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Use Node.js 20.x + - name: Use Node.js 24.16.0 uses: actions/setup-node@v4 with: - node-version: 20 + node-version: '24.16.0' cache: 'npm' - name: Install depcheck diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000000..b832e4001d --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +24.16.0 diff --git a/CLAUDE.md b/CLAUDE.md index 81362cfc57..8172a61405 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -143,7 +143,7 @@ Multi-line imports count total character length across all lines. Consolidate va | `npm run frontend:dev` | Start frontend dev server with HMR (port 3090, requires backend running) | | `npm run build:data-provider` | Rebuild `packages/data-provider` after changes | -- Node.js: v20.19.0+ or ^22.12.0 or >= 23.0.0 +- Node.js: v24.16.0 - Database: MongoDB - Backend runs on `http://localhost:3080/`; frontend dev server on `http://localhost:3090/` diff --git a/Dockerfile b/Dockerfile index be5c5b9935..6253913262 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ # v0.8.6 # Base node image -FROM node:20-alpine AS node +FROM node:24.16.0-alpine AS node RUN apk upgrade --no-cache RUN apk add --no-cache jemalloc diff --git a/Dockerfile.multi b/Dockerfile.multi index 17c88c739d..745e54b8a4 100644 --- a/Dockerfile.multi +++ b/Dockerfile.multi @@ -10,7 +10,7 @@ ARG BUILD_BRANCH= ARG BUILD_DATE= # Base for all builds -FROM node:20-alpine AS base-min +FROM node:24.16.0-alpine AS base-min ARG NPM_CI_TIMEOUT_SECONDS=1500 ARG NPM_CI_ATTEMPTS=2 RUN apk upgrade --no-cache diff --git a/api/server/controllers/mcp.js b/api/server/controllers/mcp.js index a50680f3d1..d58e1feacf 100644 --- a/api/server/controllers/mcp.js +++ b/api/server/controllers/mcp.js @@ -146,6 +146,7 @@ const getMCPTools = async (req, res) => { authField: key, label: value.title || key, description: value.description || '', + sensitive: value.sensitive, })); server.authenticated = false; } diff --git a/api/server/services/Config/loadCustomConfig.js b/api/server/services/Config/loadCustomConfig.js index c914754974..c719a84665 100644 --- a/api/server/services/Config/loadCustomConfig.js +++ b/api/server/services/Config/loadCustomConfig.js @@ -177,7 +177,8 @@ https://www.librechat.ai/docs/configuration/stt_tts`); // Validate and fill out missing values for custom parameters function parseCustomParams(endpointName, customParams) { - const paramEndpoint = customParams.defaultParamsEndpoint; + const paramEndpoint = customParams.defaultParamsEndpoint ?? 'custom'; + customParams.defaultParamsEndpoint = paramEndpoint; customParams.paramDefinitions = customParams.paramDefinitions || []; // Checks if `defaultParamsEndpoint` is a key in `paramSettings`. diff --git a/api/server/services/Config/loadCustomConfig.spec.js b/api/server/services/Config/loadCustomConfig.spec.js index 3fce8777e3..ff7ee90629 100644 --- a/api/server/services/Config/loadCustomConfig.spec.js +++ b/api/server/services/Config/loadCustomConfig.spec.js @@ -11,7 +11,7 @@ jest.mock('librechat-data-provider', () => { paramSettings: { foo: {}, bar: {}, - custom: {}, + custom: [], openrouter: [ { key: 'promptCache', @@ -59,6 +59,7 @@ jest.mock('@librechat/data-schemas', () => { const axios = require('axios'); const { loadYaml } = require('@librechat/api'); const { logger } = require('@librechat/data-schemas'); +const { ReasoningParameterFormat, ReasoningResponseKey } = require('librechat-data-provider'); const loadCustomConfig = require('./loadCustomConfig'); describe('loadCustomConfig', () => { @@ -307,11 +308,28 @@ describe('loadCustomConfig', () => { ); }); - it('throws an error when defaultParamsEndpoint is not provided', async () => { - const malformedCustomParams = { defaultParamsEndpoint: undefined }; - await expect(loadCustomParams(malformedCustomParams)).rejects.toThrow( - 'defaultParamsEndpoint of "Google" endpoint is invalid. Valid options are foo, bar, custom, openrouter, google', - ); + it('defaults defaultParamsEndpoint when only reasoningFormat is provided', async () => { + const parsedConfig = await loadCustomParams({ + reasoningFormat: ReasoningParameterFormat.reasoningObject, + }); + + expect(parsedConfig.endpoints.custom[0].customParams).toEqual({ + defaultParamsEndpoint: 'custom', + reasoningFormat: ReasoningParameterFormat.reasoningObject, + paramDefinitions: [], + }); + }); + + it('defaults defaultParamsEndpoint when only reasoningKey is provided', async () => { + const parsedConfig = await loadCustomParams({ + reasoningKey: ReasoningResponseKey.reasoning, + }); + + expect(parsedConfig.endpoints.custom[0].customParams).toEqual({ + defaultParamsEndpoint: 'custom', + reasoningKey: ReasoningResponseKey.reasoning, + paramDefinitions: [], + }); }); it('fills the paramDefinitions with missing values', async () => { diff --git a/client/package.json b/client/package.json index 99e5c6af3f..d7906026dd 100644 --- a/client/package.json +++ b/client/package.json @@ -111,7 +111,7 @@ "remark-gfm": "^4.0.0", "remark-math": "^6.0.0", "remark-supersub": "^1.0.0", - "sse.js": "^2.5.0", + "sse.js": "^2.8.0", "swr": "^2.3.8", "tailwind-merge": "^1.9.1", "tailwindcss-animate": "^1.0.5", @@ -133,10 +133,10 @@ "@types/jest": "^29.5.14", "@types/js-cookie": "^3.0.6", "@types/lodash": "^4.17.15", - "@types/node": "^20.19.35", + "@types/node": "^24.12.4", "@types/react": "^18.2.11", "@types/react-dom": "^18.2.4", - "@vitejs/plugin-react": "^5.1.4", + "@vitejs/plugin-react": "^6.0.2", "autoprefixer": "^10.4.20", "babel-plugin-replace-ts-export-assignment": "^0.0.2", "babel-plugin-root-import": "^6.6.0", @@ -155,9 +155,9 @@ "postcss-preset-env": "^11.2.0", "tailwindcss": "^3.4.1", "typescript": "^5.3.3", - "vite": "^7.3.1", - "vite-plugin-compression2": "^2.2.1", - "vite-plugin-node-polyfills": "^0.25.0", - "vite-plugin-pwa": "^1.2.0" + "vite": "^8.0.16", + "vite-plugin-compression2": "^2.5.3", + "vite-plugin-node-polyfills": "^0.28.0", + "vite-plugin-pwa": "^1.3.0" } } diff --git a/client/src/common/types.ts b/client/src/common/types.ts index d8f86f23f4..7be7bef749 100644 --- a/client/src/common/types.ts +++ b/client/src/common/types.ts @@ -16,6 +16,8 @@ export function isEphemeralAgent(agentId: string | null | undefined): boolean { export interface ConfigFieldDetail { title: string; description: string; + /** Whether the field holds a secret and should be masked (defaults to masked when omitted). */ + sensitive?: boolean; } export type CodeBarProps = { diff --git a/client/src/components/Auth/LoginForm.tsx b/client/src/components/Auth/LoginForm.tsx index c51c2002e3..3d0a2528d5 100644 --- a/client/src/components/Auth/LoginForm.tsx +++ b/client/src/components/Auth/LoginForm.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect, useContext } from 'react'; import { useForm } from 'react-hook-form'; import { Turnstile } from '@marsidev/react-turnstile'; -import { ThemeContext, Spinner, Button, isDark } from '@librechat/client'; +import { ThemeContext, SecretInput, Spinner, Button, isDark } from '@librechat/client'; import type { TLoginUser, TStartupConfig } from 'librechat-data-provider'; import type { TAuthContext } from '~/common'; import { useResendVerificationEmail, useGetStartupConfig } from '~/data-provider'; @@ -31,6 +31,13 @@ const LoginForm: React.FC = ({ onSubmit, startupConfig, error, const useUsernameLogin = config?.ldap?.username; const validTheme = isDark(theme) ? 'dark' : 'light'; const requireCaptcha = Boolean(startupConfig.turnstile?.siteKey); + const authInputClassName = + 'webkit-dark-styles transition-color peer w-full rounded-2xl border border-border-light bg-surface-primary px-3.5 pb-2.5 pt-3 text-text-primary duration-200 hover:border-border-light focus:border-green-500 focus:outline-none focus-visible:border-green-500'; + const authSecretInputClassName = `${authInputClassName} h-auto pr-12`; + const authLabelClassName = + 'absolute start-3 top-1.5 z-10 origin-[0] -translate-y-4 scale-75 transform bg-surface-primary px-2 text-sm text-text-secondary-alt duration-200 peer-placeholder-shown:top-1/2 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:scale-100 peer-focus:top-1.5 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:px-2 peer-focus:text-green-600 dark:peer-focus:text-green-500 rtl:peer-focus:left-auto rtl:peer-focus:translate-x-1/4'; + const authSecretButtonClassName = + 'size-9 rounded-xl text-text-secondary-alt hover:bg-transparent hover:text-text-primary'; useEffect(() => { if (error && error.includes('422') && !showResendLink) { @@ -102,13 +109,10 @@ const LoginForm: React.FC = ({ onSubmit, startupConfig, error, : (value) => validateEmail(value, localize('com_auth_email_pattern')), })} aria-invalid={!!errors.email} - className="webkit-dark-styles transition-color peer w-full rounded-2xl border border-border-light bg-surface-primary px-3.5 pb-2.5 pt-3 text-text-primary duration-200 focus:border-green-500 focus:outline-none" + className={authInputClassName} placeholder=" " /> -