diff --git a/.github/workflows/dashboard-test.yml b/.github/workflows/dashboard-test.yml new file mode 100644 index 0000000000..67b5a81d73 --- /dev/null +++ b/.github/workflows/dashboard-test.yml @@ -0,0 +1,85 @@ +name: Dashboard Test + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + branches: + - main + push: + branches: + - main + workflow_dispatch: # Allow manual triggering + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + # Use changes filter to detect dashboard changes + changes: + uses: ./.github/workflows/ci-changes.yml + + dashboard-test: + needs: changes + if: >- + ${{ !github.event.pull_request.draft + && needs.changes.outputs.dashboard == 'true' }} + runs-on: ubuntu-latest + name: Dashboard Lint and Type Check + + steps: + - name: Check out the repo + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v5 + with: + node-version: "23" + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: "1.24" + + - name: Cache Node.js dependencies + uses: actions/cache@v4 + with: + path: | + ~/.npm + dashboard/frontend/node_modules + key: ${{ runner.os }}-node-dashboard-${{ hashFiles('dashboard/frontend/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node-dashboard- + + - name: Cache Go dependencies + uses: actions/cache@v4 + with: + path: | + ~/go/pkg/mod + key: ${{ runner.os }}-go-dashboard-${{ hashFiles('dashboard/backend/go.sum') }} + restore-keys: | + ${{ runner.os }}-go-dashboard- + + - name: Set up golangci-lint + uses: golangci/golangci-lint-action@v7 + with: + version: v2.5.0 + install-mode: binary + args: --help + + - name: Run dashboard checks + run: make dashboard-check + + - name: Build dashboard + run: make dashboard-build + + - name: Show results on failure + if: failure() + run: | + echo "::error::Dashboard checks failed. Please fix the issues and commit again." + echo "" + echo "To run checks locally:" + echo " make dashboard-check" + echo "" + echo "To auto-fix lint issues:" + echo " make dashboard-lint-fix" diff --git a/.github/workflows/docker-stack.yml b/.github/workflows/docker-stack.yml new file mode 100644 index 0000000000..2f8ab023d1 --- /dev/null +++ b/.github/workflows/docker-stack.yml @@ -0,0 +1,136 @@ +name: Build Stack Docker Image + +on: + workflow_dispatch: + push: + branches: ["main"] + paths: + - "Dockerfile.stack" + - "deploy/stack/**" + - "!deploy/stack/*.md" + pull_request: + paths: + - "Dockerfile.stack" + - "deploy/stack/**" + - "!deploy/stack/*.md" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }}/stack + +jobs: + build: + if: github.repository == 'vllm-project/semantic-router' && !github.event.pull_request.draft + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + strategy: + matrix: + # arch: [amd64, arm64] + arch: [amd64] + fail-fast: true + + steps: + - name: Free Disk Space + uses: jlumbroso/free-disk-space@main + with: + tool-cache: true + android: true + dotnet: true + haskell: true + large-packages: true + docker-images: true + swap-storage: true + + - name: Setup Docker to use /mnt + run: | + # Stop docker + sudo systemctl stop docker + + # Move docker data to /mnt + sudo mv /var/lib/docker /mnt/docker + sudo ln -s /mnt/docker /var/lib/docker + + # Restart docker + sudo systemctl start docker + + # Verify + df -h /mnt + + - name: Check out the repo + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + buildkitd-config-inline: | + [worker.oci] + gc = false + [worker.containerd] + gc = false + driver-opts: | + image=moby/buildkit:latest + env.BUILDKIT_STEP_LOG_MAX_SIZE=-1 + network=host + + # - name: Set up QEMU + # if: matrix.arch == 'arm64' + # uses: docker/setup-qemu-action@v3 + # with: + # platforms: arm64 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile.stack + platforms: linux/${{ matrix.arch }} + push: ${{ github.event_name != 'pull_request' }} + load: ${{ github.event_name == 'pull_request' && matrix.arch == 'amd64' }} + tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}-${{ matrix.arch }} + cache-from: type=gha,scope=${{ matrix.arch }} + cache-to: type=gha,mode=max,scope=${{ matrix.arch }} + + # Create multi-arch manifest + manifest: + needs: build + if: github.event_name != 'pull_request' + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Create and push manifest + run: | + docker buildx imagetools create \ + --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} \ + --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest \ + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}-amd64 + # TODO: Re-enable arm64 when buildx supports it + # ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}-arm64 + + - name: Build summary + run: | + echo "### Stack Build Summary" >> $GITHUB_STEP_SUMMARY + echo "- **Image**: \`${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest\`" >> $GITHUB_STEP_SUMMARY + echo "- **Architectures**: amd64" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/integration-test-docker.yml b/.github/workflows/integration-test-docker.yml index 3045dd1037..4e37d79b62 100644 --- a/.github/workflows/integration-test-docker.yml +++ b/.github/workflows/integration-test-docker.yml @@ -6,14 +6,14 @@ on: branches: - main paths-ignore: - - 'website/**' - - '**/*.md' + - "website/**" + - "**/*.md" push: branches: - main paths-ignore: - - 'website/**' - - '**/*.md' + - "website/**" + - "**/*.md" workflow_dispatch: # Allow manual triggering concurrency: @@ -24,7 +24,7 @@ jobs: test-ci-compose: if: github.repository == 'vllm-project/semantic-router' && !github.event.pull_request.draft runs-on: ubuntu-latest - timeout-minutes: 20 # Reduced from 30 - CI compose is faster + timeout-minutes: 20 # Reduced from 30 - CI compose is faster steps: - name: Check out the repo @@ -36,11 +36,11 @@ jobs: # This helps prevent "no space left on device" errors echo "Disk space before setup:" df -h / && df -h /mnt - + # Create /mnt/models directory if it doesn't exist sudo mkdir -p /mnt/models sudo chown -R $USER:$USER /mnt/models - + # If models directory already exists in workspace, move it to /mnt if [ -d "models" ] && [ ! -L "models" ]; then echo "Moving existing models directory to /mnt/models..." @@ -53,7 +53,7 @@ jobs: sudo mv models /mnt/models fi fi - + # Create symlink from models/ to /mnt/models/ so existing code continues to work if [ ! -e "models" ]; then ln -s /mnt/models models @@ -63,7 +63,7 @@ jobs: else echo "Warning: models exists but is not a symlink" fi - + echo "Disk space after setup:" df -h / && df -h /mnt echo "Models directory setup complete. Models will be stored in /mnt/models" @@ -71,13 +71,13 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: '3.11' + python-version: "3.11" - name: Install dependencies run: | sudo apt-get update sudo apt-get install -y make curl - pip install huggingface_hub[cli] + pip install -r src/model_manager/requirements.txt - name: Download models run: | @@ -86,6 +86,7 @@ jobs: env: CI: true CI_MINIMAL_MODELS: true + HF_TOKEN: ${{ secrets.HF_TOKEN }} HF_HUB_ENABLE_HF_TRANSFER: 1 HF_HUB_DISABLE_TELEMETRY: 1 @@ -101,7 +102,7 @@ jobs: echo "Waiting for services to be healthy..." max_attempts=60 attempt=1 - + while [ $attempt -le $max_attempts ]; do echo "Attempt $attempt/$max_attempts: Checking service health..." @@ -128,7 +129,7 @@ jobs: sleep 5 ((attempt++)) done - + echo "❌ Timeout waiting for services to be healthy" docker ps -a exit 1 @@ -173,7 +174,7 @@ jobs: }') echo "Response: $response" - + # Verify we got a response if echo "$response" | grep -q "choices"; then echo "✅ Chat completions test passed" @@ -181,6 +182,93 @@ jobs: echo "⚠️ Response may not contain expected fields, but request succeeded" fi + - name: Test Response API - Create Response + run: | + echo "Testing Response API: POST /v1/responses..." + + response=$(curl -s -X POST http://localhost:8801/v1/responses \ + -H "Content-Type: application/json" \ + -d '{ + "model": "qwen3", + "input": "What is 2 + 2?", + "store": true + }') + + echo "Response: $response" + + # Extract response ID for subsequent tests + response_id=$(echo "$response" | jq -r '.id // empty') + if [ -n "$response_id" ] && [[ "$response_id" == resp_* ]]; then + echo "✅ Response API create test passed (id=$response_id)" + echo "RESPONSE_ID=$response_id" >> $GITHUB_ENV + else + echo "❌ Response API create test failed - invalid or missing response ID" + exit 1 + fi + + - name: Test Response API - Get Response + run: | + echo "Testing Response API: GET /v1/responses/$RESPONSE_ID..." + + response=$(curl -s -X GET "http://localhost:8801/v1/responses/$RESPONSE_ID" \ + -H "Content-Type: application/json") + + echo "Response: $response" + + # Verify response ID matches + got_id=$(echo "$response" | jq -r '.id // empty') + if [ "$got_id" = "$RESPONSE_ID" ]; then + echo "✅ Response API get test passed" + else + echo "❌ Response API get test failed - ID mismatch (expected=$RESPONSE_ID, got=$got_id)" + exit 1 + fi + + - name: Test Response API - Get Input Items + run: | + echo "Testing Response API: GET /v1/responses/$RESPONSE_ID/input_items..." + + response=$(curl -s -X GET "http://localhost:8801/v1/responses/$RESPONSE_ID/input_items" \ + -H "Content-Type: application/json") + + echo "Response: $response" + + # Verify it's a list + object_type=$(echo "$response" | jq -r '.object // empty') + if [ "$object_type" = "list" ]; then + echo "✅ Response API input_items test passed" + else + echo "❌ Response API input_items test failed - expected object=list, got=$object_type" + exit 1 + fi + + - name: Test Response API - Delete Response + run: | + echo "Testing Response API: DELETE /v1/responses/$RESPONSE_ID..." + + response=$(curl -s -X DELETE "http://localhost:8801/v1/responses/$RESPONSE_ID" \ + -H "Content-Type: application/json") + + echo "Response: $response" + + # Verify deletion + deleted=$(echo "$response" | jq -r '.deleted // empty') + if [ "$deleted" = "true" ]; then + echo "✅ Response API delete test passed" + else + echo "❌ Response API delete test failed - expected deleted=true" + exit 1 + fi + + # Verify 404 on subsequent get + get_response=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:8801/v1/responses/$RESPONSE_ID") + if [ "$get_response" = "404" ]; then + echo "✅ Response API delete verification passed (404 on get)" + else + echo "❌ Response API delete verification failed - expected 404, got $get_response" + exit 1 + fi + - name: Show service logs on failure if: failure() run: | diff --git a/.github/workflows/integration-test-dynamic-config.yml b/.github/workflows/integration-test-dynamic-config.yml index ed59291ad1..a5590de962 100644 --- a/.github/workflows/integration-test-dynamic-config.yml +++ b/.github/workflows/integration-test-dynamic-config.yml @@ -6,14 +6,14 @@ on: branches: - main paths-ignore: - - 'website/**' - - '**/*.md' + - "website/**" + - "**/*.md" push: branches: - main paths-ignore: - - 'website/**' - - '**/*.md' + - "website/**" + - "**/*.md" workflow_dispatch: # Allow manual triggering concurrency: @@ -33,7 +33,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: '1.24' + go-version: "1.24" - name: Set up Rust uses: actions-rust-lang/setup-rust-toolchain@v1 @@ -57,7 +57,8 @@ jobs: - name: Install kubectl run: | - curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" + KUBECTL_VERSION=$(curl -L -s https://dl.k8s.io/release/stable.txt) + curl -LO "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl" chmod +x kubectl sudo mv kubectl /usr/local/bin/kubectl @@ -174,5 +175,3 @@ jobs: if: always() run: | make e2e-cleanup || true - - diff --git a/.github/workflows/performance-nightly.yml b/.github/workflows/performance-nightly.yml index 26232a296c..145deff849 100644 --- a/.github/workflows/performance-nightly.yml +++ b/.github/workflows/performance-nightly.yml @@ -1,9 +1,10 @@ name: Nightly Performance Baseline on: - schedule: - # Run at 3:00 AM UTC daily - - cron: "0 3 * * *" + # Disabled - baseline comparison not currently used in PR tests + # schedule: + # # Run at 3:00 AM UTC daily + # - cron: "0 3 * * *" workflow_dispatch: # Allow manual triggering jobs: @@ -62,13 +63,14 @@ jobs: - name: Build Rust library (CPU-only) run: make rust-ci - - name: Install HuggingFace CLI + - name: Install Model Manager dependencies run: | - pip install -U "huggingface_hub[cli]" hf_transfer + pip install -r src/model_manager/requirements.txt - name: Download models (minimal set for nightly) env: - CI_MINIMAL_MODELS: true + CI_MINIMAL_MODELS: false + HF_TOKEN: ${{ secrets.HF_TOKEN }} HF_HUB_ENABLE_HF_TRANSFER: 1 HF_HUB_DISABLE_TELEMETRY: 1 run: make download-models diff --git a/.github/workflows/performance-test.yml b/.github/workflows/performance-test.yml index 166a2faa8d..c47b0ab2c7 100644 --- a/.github/workflows/performance-test.yml +++ b/.github/workflows/performance-test.yml @@ -5,16 +5,16 @@ on: branches: - main paths: - - 'src/semantic-router/**' - - 'candle-binding/**' - - 'perf/**' - - '.github/workflows/performance-test.yml' + - "src/semantic-router/**" + - "candle-binding/**" + - "perf/**" + - ".github/workflows/performance-test.yml" workflow_dispatch: permissions: contents: read - pull-requests: write # Required to comment on PRs - issues: write # Required to comment on PRs (PRs are issues) + pull-requests: write # Required to comment on PRs + issues: write # Required to comment on PRs (PRs are issues) jobs: component-benchmarks: @@ -25,7 +25,7 @@ jobs: - name: Check out the repo uses: actions/checkout@v4 with: - fetch-depth: 0 # Need full history for baseline comparison + fetch-depth: 0 # Need full history for baseline comparison - name: Set up Go uses: actions/setup-go@v5 @@ -72,25 +72,18 @@ jobs: - name: Build Rust library (CPU-only) run: make rust-ci - - name: Install HuggingFace CLI + - name: Install Model Manager dependencies run: | - pip install -U "huggingface_hub[cli]" hf_transfer + pip install -r src/model_manager/requirements.txt - name: Download models (minimal) env: CI_MINIMAL_MODELS: true + HF_TOKEN: ${{ secrets.HF_TOKEN }} HF_HUB_ENABLE_HF_TRANSFER: 1 HF_HUB_DISABLE_TELEMETRY: 1 run: make download-models - - name: Download performance baselines - continue-on-error: true - run: | - mkdir -p perf/testdata/baselines - git show main:perf/testdata/baselines/classification.json > perf/testdata/baselines/classification.json 2>/dev/null || echo '{"version":"v1.0.0","benchmarks":{}}' > perf/testdata/baselines/classification.json - git show main:perf/testdata/baselines/decision.json > perf/testdata/baselines/decision.json 2>/dev/null || echo '{"version":"v1.0.0","benchmarks":{}}' > perf/testdata/baselines/decision.json - git show main:perf/testdata/baselines/cache.json > perf/testdata/baselines/cache.json 2>/dev/null || echo '{"version":"v1.0.0","benchmarks":{}}' > perf/testdata/baselines/cache.json - - name: Run component benchmarks run: | mkdir -p reports @@ -127,7 +120,7 @@ jobs: - name: Comment PR with results if: github.event_name == 'pull_request' - continue-on-error: true # May fail for PRs from forks due to GitHub security restrictions + continue-on-error: true # May fail for PRs from forks due to GitHub security restrictions uses: actions/github-script@v7 with: script: | @@ -177,21 +170,3 @@ jobs: path: | reports/ retention-days: 30 - - - name: Check for regressions (placeholder) - id: regression_check - continue-on-error: true - run: | - # In a real implementation, this would: - # 1. Parse benchmark output - # 2. Compare against baselines - # 3. Calculate % changes - # 4. Exit 1 if regressions exceed thresholds - echo "No regressions detected (placeholder check)" - - - name: Fail on regression - if: steps.regression_check.outcome == 'failure' - run: | - echo "❌ Performance regressions detected!" - echo "See benchmark results in artifacts for details" - exit 1 diff --git a/.github/workflows/test-and-build.yml b/.github/workflows/test-and-build.yml index db3a337a52..fd74ab5932 100644 --- a/.github/workflows/test-and-build.yml +++ b/.github/workflows/test-and-build.yml @@ -34,7 +34,6 @@ jobs: || needs.changes.outputs.make == 'true' || needs.changes.outputs.ci == 'true') }} runs-on: ubuntu-latest - steps: - name: Check out the repo uses: actions/checkout@v4 @@ -86,11 +85,11 @@ jobs: # This helps prevent "no space left on device" errors echo "Disk space before setup:" df -h / && df -h /mnt - + # Create /mnt/models directory if it doesn't exist sudo mkdir -p /mnt/models sudo chown -R $USER:$USER /mnt/models - + # If models directory already exists in workspace, move it to /mnt if [ -d "models" ] && [ ! -L "models" ]; then echo "Moving existing models directory to /mnt/models..." @@ -103,7 +102,7 @@ jobs: sudo mv models /mnt/models fi fi - + # Create symlink from models/ to /mnt/models/ so existing code continues to work if [ ! -e "models" ]; then ln -s /mnt/models models @@ -113,7 +112,7 @@ jobs: else echo "Warning: models exists but is not a symlink" fi - + echo "Disk space after setup:" df -h / && df -h /mnt echo "Models directory setup complete. Models will be stored in /mnt/models" @@ -134,13 +133,14 @@ jobs: - name: Build Rust library (CPU-only, no CUDA) run: make rust-ci - - name: Install HuggingFace CLI + - name: Install Model Manager dependencies run: | - pip install -U "huggingface_hub[cli]" hf_transfer + pip install -r src/model_manager/requirements.txt - name: Download models (minimal on PRs) env: CI_MINIMAL_MODELS: ${{ github.event_name == 'pull_request' }} + HF_TOKEN: ${{ secrets.HF_TOKEN }} HF_HUB_ENABLE_HF_TRANSFER: 1 HF_HUB_DISABLE_TELEMETRY: 1 run: make download-models @@ -169,7 +169,7 @@ jobs: echo "Milvus is ready at localhost:19530" docker ps --filter "name=milvus-semantic-cache" - + - name: Start Redis service run: | echo "Starting Redis Stack..." diff --git a/Dockerfile.stack b/Dockerfile.stack new file mode 100644 index 0000000000..d6cc63600f --- /dev/null +++ b/Dockerfile.stack @@ -0,0 +1,388 @@ +# ============================================================================ +# vLLM Semantic Router Stack Docker Image +# ============================================================================ +# Complete single-container deployment with all components: +# - semantic-router (Rust library + Go service) +# - dashboard (Go backend + React frontend) +# - Envoy proxy (API gateway) +# - Prometheus (metrics collection) +# - Grafana (monitoring dashboards) +# - Jaeger (distributed tracing) +# - MongoDB (database) +# - chat-ui (web UI) +# - open-webui (web UI) +# - pipelines (web UI) +# - LLM-katan (LLM) +# +# Supports: linux/amd64, linux/arm64 +# +# Usage: +# docker build -f Dockerfile.stack -t vsr-stack:latest . +# docker buildx build -f Dockerfile.stack --platform linux/amd64,linux/arm64 -t vsr-stack:latest . +# +# Run: +# docker run -d --name vsr \ +# -p 8801:8801 \ +# -p 8002:8002 \ +# -p 8700:8700 \ +# -p 3000:3000 \ +# -p 3001:3001 \ +# -p 5173:5173 \ +# -p 9090:9090 \ +# -p 9099:9099 \ +# -p 16686:16686 \ +# -v ./models:/app/models:ro \ # need run make download-models +# vsr-stack:latest +# ============================================================================ + +# Build arguments for target architecture +ARG TARGETARCH +ARG TARGETOS=linux + +# Version arguments for components +ARG ENVOY_VERSION=1.31.7 +ARG PROMETHEUS_VERSION=2.53.0 +ARG GRAFANA_VERSION=11.5.1 +ARG JAEGER_VERSION=1.62.0 +ARG MONGODB_VERSION=8.0.4 +ARG CHATUI_VERSION=latest + +# Stage 1: Rust library (candle-binding) +FROM rustlang/rust:nightly AS rust-builder + +RUN apt-get update && apt-get install -y \ + make \ + build-essential \ + pkg-config \ + lld \ + clang \ + gcc-aarch64-linux-gnu \ + && rm -rf /var/lib/apt/lists/* + +RUN rustup target add aarch64-unknown-linux-gnu x86_64-unknown-linux-gnu + +ENV CARGO_NET_GIT_FETCH_WITH_CLI=true +ENV CARGO_INCREMENTAL=1 +ENV CARGO_PROFILE_RELEASE_LTO=thin +ENV CARGO_PROFILE_RELEASE_CODEGEN_UNITS=1 +ENV RUSTFLAGS="-C link-arg=-fuse-ld=lld" + +WORKDIR /app + +COPY candle-binding/Cargo.toml ./candle-binding/ +COPY candle-binding/Cargo.loc[k] ./candle-binding/ +COPY tools/make/ tools/make/ +COPY Makefile ./ + +RUN cd candle-binding && \ + mkdir -p src && \ + echo "fn main() {}" > src/lib.rs && \ + cargo build --release --no-default-features && \ + rm -rf src + +COPY candle-binding/src/ ./candle-binding/src/ + +RUN cd candle-binding && \ + cargo clean && \ + cargo build --release --no-default-features + +# Stage 2: semantic-router (Go) +FROM golang:1.24 AS go-router-builder + +WORKDIR /app + +RUN mkdir -p src/semantic-router +COPY src/semantic-router/go.mod src/semantic-router/go.sum src/semantic-router/ +COPY candle-binding/go.mod candle-binding/semantic-router.go candle-binding/ + +RUN cd src/semantic-router && go mod download && \ + cd /app/candle-binding && go mod download + +COPY src/semantic-router/ src/semantic-router/ +COPY --from=rust-builder /app/candle-binding/target/release/libcandle_semantic_router.so /app/candle-binding/target/release/ + +ENV CGO_ENABLED=1 +ENV LD_LIBRARY_PATH=/app/candle-binding/target/release +ENV GOOS=linux + +RUN mkdir -p bin && cd src/semantic-router && \ + go build -ldflags="-w -s" -o ../../bin/router cmd/main.go + +# Stage 3: Dashboard frontend (Node.js) +FROM node:20-alpine AS frontend-builder + +WORKDIR /app/frontend +COPY dashboard/frontend/package.json dashboard/frontend/package-lock.json ./ +COPY dashboard/frontend/tsconfig.json dashboard/frontend/tsconfig.node.json ./ +COPY dashboard/frontend/vite.config.ts ./ +COPY dashboard/frontend/src ./src +COPY dashboard/frontend/public ./public +COPY dashboard/frontend/index.html ./ + +RUN npm install --include=dev + +# Stack: configure Open WebUI port for direct access +ENV VITE_OPENWEBUI_PORT=3001 + +RUN npm run build + +# Stage 4: Dashboard backend (Go) +FROM golang:1.24 AS dashboard-builder + +ARG TARGETARCH +ARG TARGETOS=linux + +WORKDIR /app + +COPY dashboard/backend/go.mod dashboard/backend/go.sum /app/dashboard/backend/ +WORKDIR /app/dashboard/backend +RUN go mod download + +COPY dashboard/backend/ /app/dashboard/backend/ +RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -ldflags="-w -s" -o /app/dashboard-backend main.go + +# Stage 5: chat-ui +FROM ghcr.io/huggingface/chat-ui-db:latest AS chatui-source + +# Stage 5b: Open WebUI +FROM ghcr.io/open-webui/open-webui:main AS openwebui-source + +# Stage 5c: Pipelines +FROM ghcr.io/open-webui/pipelines:main AS pipelines-source + +# Stage 6a: Envoy +FROM envoyproxy/envoy:v1.31.7 AS envoy-source + +# Stage 6b: MongoDB +FROM mongo:7 AS mongodb-source + +# Stage 6c: Download binaries (Prometheus, Grafana, Jaeger) +FROM debian:testing AS downloader + +ARG TARGETARCH +ARG PROMETHEUS_VERSION +ARG GRAFANA_VERSION +ARG JAEGER_VERSION +ARG MONGODB_VERSION + +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl \ + ca-certificates \ + xz-utils \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /downloads + +# Download Prometheus +RUN if [ "${TARGETARCH}" = "arm64" ]; then PROM_ARCH="arm64"; else PROM_ARCH="amd64"; fi && \ + curl -fsSL "https://github.com/prometheus/prometheus/releases/download/v${PROMETHEUS_VERSION}/prometheus-${PROMETHEUS_VERSION}.linux-${PROM_ARCH}.tar.gz" \ + | tar -xzf - --strip-components=1 -C /downloads prometheus-${PROMETHEUS_VERSION}.linux-${PROM_ARCH}/prometheus prometheus-${PROMETHEUS_VERSION}.linux-${PROM_ARCH}/promtool + +# Download Grafana +RUN if [ "${TARGETARCH}" = "arm64" ]; then GRAFANA_ARCH="arm64"; else GRAFANA_ARCH="amd64"; fi && \ + curl -fsSL "https://dl.grafana.com/oss/release/grafana-${GRAFANA_VERSION}.linux-${GRAFANA_ARCH}.tar.gz" \ + | tar -xzf - -C /downloads && \ + mv /downloads/grafana-v${GRAFANA_VERSION} /downloads/grafana || mv /downloads/grafana-${GRAFANA_VERSION} /downloads/grafana + +# Download Jaeger +RUN if [ "${TARGETARCH}" = "arm64" ]; then JAEGER_ARCH="arm64"; else JAEGER_ARCH="amd64"; fi && \ + curl -fsSL "https://github.com/jaegertracing/jaeger/releases/download/v${JAEGER_VERSION}/jaeger-${JAEGER_VERSION}-linux-${JAEGER_ARCH}.tar.gz" \ + | tar -xzf - --strip-components=1 -C /downloads jaeger-${JAEGER_VERSION}-linux-${JAEGER_ARCH}/jaeger-all-in-one + +# Stage 7: Final image +FROM debian:testing + +LABEL maintainer="vLLM Semantic Router Team" +LABEL description="Stack image with semantic-router, dashboard, chat-ui, llm-katan, Envoy, MongoDB, Prometheus, Grafana, and Jaeger" + +ENV DEBIAN_FRONTEND=noninteractive +ENV TZ=UTC + +RUN apt-get update && apt-get install -y --no-install-recommends \ + supervisor \ + curl \ + wget \ + ca-certificates \ + libc6 \ + libstdc++6 \ + libgcc-s1 \ + gettext-base \ + adduser \ + libfontconfig1 \ + nodejs \ + npm \ + python3 \ + python3-pip \ + python3-venv \ + && rm -rf /var/lib/apt/lists/* \ + && mkdir -p /var/log/supervisor /var/run/supervisor + +# Create application directories +RUN mkdir -p \ + /app/config \ + /app/lib \ + /app/models \ + /app/frontend \ + /app/chatui \ + /app/openwebui \ + /app/pipelines \ + /etc/envoy \ + /etc/prometheus \ + /etc/grafana/provisioning/datasources \ + /etc/grafana/provisioning/dashboards \ + /var/lib/grafana \ + /var/lib/prometheus \ + /var/lib/mongodb \ + /var/lib/openwebui \ + /opt/llmkatan-env \ + /etc/supervisor/conf.d + +# Copy binaries +COPY --from=envoy-source /usr/local/bin/envoy /usr/local/bin/envoy + +# Prometheus +COPY --from=downloader /downloads/prometheus /usr/local/bin/prometheus +COPY --from=downloader /downloads/promtool /usr/local/bin/promtool + +# Grafana +COPY --from=downloader /downloads/grafana /opt/grafana + +# Jaeger +COPY --from=downloader /downloads/jaeger-all-in-one /usr/local/bin/jaeger-all-in-one + +# semantic-router +COPY --from=go-router-builder /app/bin/router /app/extproc-server +COPY --from=rust-builder /app/candle-binding/target/release/libcandle_semantic_router.so /app/lib/ + +# Dashboard +COPY --from=dashboard-builder /app/dashboard-backend /app/dashboard-backend +COPY --from=frontend-builder /app/frontend/dist /app/frontend + +# MongoDB (from official Docker image) +COPY --from=mongodb-source /usr/bin/mongod /usr/local/bin/mongod + +# chat-ui (HuggingChat) - from pre-built image +COPY --from=chatui-source /app /app/chatui + +# Open WebUI - independent Python environment at /opt/openwebui-env +COPY --from=openwebui-source /usr/local /opt/openwebui-env +COPY --from=openwebui-source /app /app/openwebui + +# Pipelines - independent Python environment at /opt/pipelines-env +COPY --from=pipelines-source /usr/local /opt/pipelines-env +COPY --from=pipelines-source /app /app/pipelines + +# LLM-Katan - source files (will be installed in venv below) +COPY e2e-tests/llm-katan /app/llm-katan-src + +# Install LLM-Katan in a virtual environment +# Using CPU-only PyTorch to save space (~2GB vs ~8GB with CUDA) +RUN python3 -m venv /opt/llmkatan-env && \ + /opt/llmkatan-env/bin/pip install --no-cache-dir --upgrade pip && \ + /opt/llmkatan-env/bin/pip install --no-cache-dir torch --index-url https://download.pytorch.org/whl/cpu && \ + /opt/llmkatan-env/bin/pip install --no-cache-dir -r /app/llm-katan-src/requirements.txt && \ + /opt/llmkatan-env/bin/pip install --no-cache-dir /app/llm-katan-src && \ + rm -rf /app/llm-katan-src + +# vllm semantic router pipeline config (stack version with localhost:8801) +COPY deploy/stack/vllm_semantic_router_pipe.py /app/pipelines/vllm_semantic_router_pipe.py + +# Configuration files +COPY deploy/stack/config.stack.yaml /app/config/config.yaml + +# Envoy config +COPY deploy/stack/envoy.template.yaml /etc/envoy/envoy.template.yaml + +# Prometheus config +COPY deploy/stack/prometheus.yaml /etc/prometheus/prometheus.yml + +# Grafana provisioning +COPY deploy/stack/grafana-datasource.yaml /etc/grafana/provisioning/datasources/datasource.yaml +COPY deploy/stack/grafana-dashboard.yaml /etc/grafana/provisioning/dashboards/dashboard.yaml +COPY deploy/docker-compose/addons/llm-router-dashboard.json /etc/grafana/provisioning/dashboards/ + +# Supervisor config +COPY deploy/stack/supervisord.stack.conf /etc/supervisor/supervisord.conf + +# Entrypoint +COPY deploy/stack/entrypoint-stack.sh /app/entrypoint.sh +RUN chmod +x /app/entrypoint.sh /app/extproc-server /app/dashboard-backend + +# Environment variables +ENV LD_LIBRARY_PATH=/app/lib + +# Application config +ENV CONFIG_FILE=/app/config/config.yaml + +# Envoy +ENV ENVOY_LISTEN_PORT=8801 +ENV ENVOY_ADMIN_PORT=19000 +ENV EXTPROC_HOST=localhost +ENV EXTPROC_PORT=50051 + +# Dashboard +ENV DASHBOARD_PORT=8700 + +# Grafana +ENV GF_PATHS_HOME=/opt/grafana +ENV GF_PATHS_DATA=/var/lib/grafana +ENV GF_PATHS_PROVISIONING=/etc/grafana/provisioning +ENV GF_SECURITY_ADMIN_USER=admin +ENV GF_SECURITY_ADMIN_PASSWORD=admin + +# Jaeger +ENV COLLECTOR_OTLP_ENABLED=true + +# MongoDB +ENV MONGODB_DATA_PATH=/var/lib/mongodb + +# chat-ui (HuggingChat) +ENV CHATUI_PORT=5173 +ENV MONGODB_URL=mongodb://127.0.0.1:27017 +ENV MONGODB_DB_NAME=chat-ui +ENV OPENAI_BASE_URL=http://localhost:8801/v1 +ENV OPENAI_API_KEY=placeholder +ENV PUBLIC_APP_NAME=HuggingChat +ENV PUBLIC_APP_ASSETS=chatui +ENV COOKIE_SECURE=false +ENV COOKIE_NAME=hf-chat +ENV COOKIE_SAMESITE=lax +ENV APP_BASE_URL=http://localhost:5173 + +# Open WebUI +ENV OPENWEBUI_PORT=3001 +ENV OPENWEBUI_DATA_DIR=/var/lib/openwebui +ENV OPENAI_API_BASE_URL=http://localhost:9099 + +# Pipelines +ENV PIPELINES_PORT=9099 + +# LLM-Katan (lightweight LLM server) +ENV LLMKATAN_PORT=8002 +ENV LLMKATAN_MODEL=/app/models/Qwen/Qwen3-0.6B +ENV LLMKATAN_SERVED_MODEL_NAME=qwen3 + +# Ports: +# 8801 - Envoy HTTP proxy (main API endpoint) +# 19000 - Envoy admin interface +# 50051 - semantic-router gRPC +# 8080 - semantic-router HTTP (health/metrics) +# 8700 - Dashboard +# 9090 - Prometheus +# 3000 - Grafana +# 16686 - Jaeger UI +# 4317 - Jaeger OTLP gRPC +# 5173 - chat-ui (HuggingChat) +# 3001 - Open WebUI +# 9099 - Pipelines +# 8002 - LLM-Katan +EXPOSE 8801 19000 50051 8080 8700 9090 3000 16686 4317 5173 3001 9099 8002 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=120s --retries=3 \ + CMD curl -f http://localhost:8080/health && curl -f http://localhost:8700/healthz || exit 1 + +WORKDIR /app + +ENTRYPOINT ["/app/entrypoint.sh"] +CMD ["supervisord", "-c", "/etc/supervisor/supervisord.conf", "-n"] diff --git a/Makefile b/Makefile index 75b14be4c2..4a7172ebd5 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,7 @@ _run: -f tools/make/rust.mk \ -f tools/make/build-run-test.mk \ -f tools/make/docs.mk \ + -f tools/make/dashboard.mk \ -f tools/make/linter.mk \ -f tools/make/milvus.mk \ -f tools/make/redis.mk \ diff --git a/README.md b/README.md index 6ef6807e17..ff80740ba8 100644 --- a/README.md +++ b/README.md @@ -17,16 +17,13 @@ *Latest News* 🔥 -- [2025/11/19] We released the [Signal-Decision Driven Architecture: Reshaping Semantic Routing at Scale](https://blog.vllm.ai/2025/11/19/signal-decision.html) 🧠 +- [2025/12/15] New Blog: [Token-Level Truth: Real-Time Hallucination Detection for Production LLMs](https://blog.vllm.ai/2025/12/14/halugate.html) 🚪 +- [2025/11/19] New Blog: [Signal-Decision Driven Architecture: Reshaping Semantic Routing at Scale](https://blog.vllm.ai/2025/11/19/signal-decision.html) 🧠 - [2025/11/03] **Our paper** [Category-Aware Semantic Caching for Heterogeneous LLM Workloads](https://arxiv.org/abs/2510.26835) published 📝 -- [2025/10/26] We reached 2000 stars on GitHub! 🔥 - [2025/10/21] We announced the [2025 Q4 Roadmap: Journey to Iris](https://vllm-semantic-router.com/blog/q4-roadmap-iris) 📅. -- [2025/10/16] We established the [vLLM Semantic Router Youtube Channel](https://www.youtube.com/@vLLMSemanticRouter) ✨. -- [2025/10/15] We announced the [vLLM Semantic Router Dashboard](https://www.youtube.com/watch?v=E2IirN8PsFw) 🚀. - [2025/10/12] **Our paper** [When to Reason: Semantic Router for vLLM](https://arxiv.org/abs/2510.08731) accepted by NeurIPS 2025 MLForSys 🧠. - [2025/10/08] We announced the integration with [vLLM Production Stack](https://github.com/vllm-project/production-stack) Team 👋. - [2025/10/01] We supported to deploy on [Kubernetes](https://vllm-semantic-router.com/docs/installation/k8s/ai-gateway) 🌊. -- [2025/09/15] We reached 1000 stars on GitHub! 🔥 - [2025/09/01] We released the project officially: [vLLM Semantic Router: Next Phase in LLM inference](https://blog.vllm.ai/2025/09/11/semantic-router.html) 🚀. --- @@ -81,19 +78,60 @@ Detect PII in the prompt, avoiding sending PII to the LLM so as to protect the p Detect if the prompt is a jailbreak prompt, avoiding sending jailbreak prompts to the LLM so as to prevent the LLM from misbehaving. Can be configured globally or at the category level for fine-grained security control. -### vLLM Semantic Router Dashboard 💬 +## Quick Start 🚀 -Watch the quick demo of the dashboard below: +### Using VSR CLI (Recommended) -
- - vLLM Semantic Router Dashboard - -
+The `vsr` CLI tool provides a unified interface for managing the vLLM Semantic Router across all environments. It reduces setup time from hours to minutes with intelligent auto-detection, comprehensive diagnostics, and beautiful CLI output. -## Quick Start 🚀 +#### Installation + +```bash +# Clone and build +cd semantic-router/src/semantic-router +make build-cli +export PATH=$PATH:$(pwd)/bin + +# Verify installation +vsr --version +``` + +#### Get Started in 4 Commands + +```bash +vsr init # Initialize configuration +make download-models # Download AI models +vsr config validate # Validate setup +vsr deploy docker # Deploy with Docker Compose +``` + +#### Key Features + +- **Multi-Environment Support**: Deploy to Local, Docker, Kubernetes, or Helm +- **Model Management**: Download, validate, list, and inspect models +- **Health Monitoring**: Status checks, diagnostics, and health reports +- **Debug Tools**: Interactive debugging and troubleshooting +- **Dashboard Integration**: Auto-detect and open dashboard in browser +- **Enhanced Logging**: Multi-environment log fetching with filtering + +#### Common Commands -Get up and running in seconds with our interactive setup script: +```bash +vsr status # Check deployment status +vsr logs --follow # View logs in real-time +vsr health # Quick health check +vsr dashboard # Open dashboard +vsr model list # List available models +vsr debug # Run diagnostics +vsr upgrade docker # Upgrade deployment +vsr undeploy docker # Stop deployment +``` + +For complete CLI documentation, see [src/semantic-router/cmd/vsr/README.md](src/semantic-router/cmd/vsr/README.md) or [Quick Start Guide](src/semantic-router/cmd/vsr/QUICKSTART.md). + +### Using Quickstart Script + +Alternatively, get up and running in seconds with our interactive setup script: ```bash bash ./scripts/quickstart.sh @@ -162,3 +200,19 @@ If you find Semantic Router helpful in your research or projects, please conside We opened the project at Aug 31, 2025. We love open source and collaboration ❤️ [![Star History Chart](https://api.star-history.com/svg?repos=vllm-project/semantic-router&type=Date)](https://www.star-history.com/#vllm-project/semantic-router&Date) + +## Sponsors 👋 + +We are grateful to our sponsors who support us: + +--- + +[**AMD**](https://www.amd.com) provides us with GPU resources and [ROCm™](https://www.amd.com/en/products/software/rocm.html) Software for training and researching the frontier router models, enhancing e2e testing, and building online models playground. + +
+ + AMD + +
+ +--- diff --git a/bench/hallucination/README.md b/bench/hallucination/README.md new file mode 100644 index 0000000000..bffd5427e4 --- /dev/null +++ b/bench/hallucination/README.md @@ -0,0 +1,137 @@ +# Hallucination Detection Benchmark + +E2E evaluation of hallucination detection through the semantic router. + +## Quick Start + +```bash +# 1. Start vLLM (if not running) +docker run -d --gpus all -p 8083:8000 vllm/vllm-openai:latest \ + vllm serve --model Qwen/Qwen2.5-14B-Instruct-AWQ + +# 2. Start semantic router with hallucination config +cd /path/to/semantic-router +export LD_LIBRARY_PATH=$PWD/candle-binding/target/release +./bin/router -config=bench/hallucination/config-7b.yaml + +# 3. Start Envoy +make run-envoy + +# 4. Run benchmark +python3 -m bench.hallucination.evaluate \ + --endpoint http://localhost:8801 \ + --dataset halueval \ + --max-samples 50 + +# Or use the Makefile target: +make bench-hallucination MAX_SAMPLES=50 +``` + +## Using the Large Model + +The large model (`lettucedect-large-modernbert-en-v1`, 395M params) provides better detection accuracy than the base model. + +### Step 1: Download the Large Model + +```bash +cd /path/to/semantic-router + +# Download from HuggingFace +hf download KRLabsOrg/lettucedect-large-modernbert-en-v1 \ + --local-dir models/lettucedect-large-modernbert-en-v1 +``` + +### Step 2: Update Config + +Edit `bench/hallucination/config-7b.yaml`: + +```yaml +hallucination_mitigation: + enabled: true + + hallucination_model: + model_id: "models/lettucedect-large-modernbert-en-v1" # Use large model + threshold: 0.5 + use_cpu: true # Set to false for GPU +``` + +### Step 3: Restart Router + +```bash +# Kill existing router +pkill -f "router.*config" + +# Start with updated config +export LD_LIBRARY_PATH=$PWD/candle-binding/target/release +./bin/router -config=bench/hallucination/config-7b.yaml +``` + +## Supported Models + +| Model | Params | HuggingFace ID | +|-------|--------|----------------| +| Base | 149M | `KRLabsOrg/lettucedect-base-modernbert-en-v1` | +| Large | 395M | `KRLabsOrg/lettucedect-large-modernbert-en-v1` | + +Both use `ModernBertForTokenClassification` architecture supported by candle-binding. + +## Config Reference + +Key settings in `config-7b.yaml`: + +```yaml +# vLLM endpoint +vllm_endpoints: + - name: "vllm-general" + address: "127.0.0.1" + port: 8083 + +# Hallucination detection +hallucination_mitigation: + enabled: true + hallucination_model: + model_id: "models/lettucedect-large-modernbert-en-v1" + threshold: 0.5 + use_cpu: true + on_hallucination_detected: "warn" # or "block" +``` + +## Datasets + +| Dataset | Command | +|---------|---------| +| HaluEval | `--dataset halueval` | +| Custom | `--dataset /path/to/data.jsonl` | + +## Output + +Results saved to `bench/hallucination/results/` with: + +- Precision, Recall, F1 (when ground truth available) +- Latency metrics (avg, p50, p99) +- Per-sample detection results + +### Two-Stage Pipeline Efficiency Metrics + +The benchmark tracks the computational savings from the two-stage detection pipeline: + +``` +⚡ Two-Stage Pipeline Efficiency: +---------------------------------------- + Fact-check needed: 65/100 queries + Detection skipped: 35/100 queries + Avg context length: 4500 chars + Estimated detect time: 6500.00 ms (if all ran) + Actual detect time: 4225.00 ms + Time saved: 2275.00 ms + Efficiency gain: 35.0% + + 💡 Pre-filtering skipped 35.0% of requests, + saving 2275ms of detection compute. +``` + +This demonstrates the value of the HaluGate Sentinel pre-classifier: + +- **O(1) filtering** before **O(n) detection** (n = context length) +- Non-factual queries (creative, opinion, brainstorming) skip expensive token classification +- Critical for RAG applications with large contexts (8K+ tokens) diff --git a/bench/hallucination/__init__.py b/bench/hallucination/__init__.py new file mode 100644 index 0000000000..c83b18d6b0 --- /dev/null +++ b/bench/hallucination/__init__.py @@ -0,0 +1,16 @@ +"""Hallucination Detection Benchmark for Semantic Router. + +This package provides end-to-end evaluation of the hallucination detection pipeline +through the router + Envoy stack. +""" + +from .evaluate import HallucinationBenchmark +from .datasets import HaluEvalDataset, CustomDataset, get_dataset + +__all__ = [ + "HallucinationBenchmark", + "HaluEvalDataset", + "FinancialFactEvalDataset", + "CustomDataset", + "get_dataset", +] diff --git a/bench/hallucination/config-7b.yaml b/bench/hallucination/config-7b.yaml new file mode 100644 index 0000000000..dfeaa29e4c --- /dev/null +++ b/bench/hallucination/config-7b.yaml @@ -0,0 +1,181 @@ +# Configuration for hallucination detection benchmark +# Connects to real vLLM server at port 8083 + +bert_model: + model_id: models/all-MiniLM-L12-v2 + threshold: 0.6 + use_cpu: true + +semantic_cache: + enabled: false # Disable cache for benchmarking + +# Classifier configuration +classifier: + category_model: + model_id: "models/category_classifier_modernbert-base_model" + use_modernbert: true + threshold: 0.6 + use_cpu: true + category_mapping_path: "models/category_classifier_modernbert-base_model/category_mapping.json" + pii_model: + model_id: "models/pii_classifier_modernbert-base_presidio_token_model" + use_modernbert: true + threshold: 0.7 + use_cpu: true + pii_mapping_path: "models/pii_classifier_modernbert-base_presidio_token_model/pii_type_mapping.json" + +# Hallucination mitigation configuration +hallucination_mitigation: + enabled: true + + # Fact-check classifier: determines if a prompt needs fact verification + fact_check_model: + model_id: "models/halugate-sentinel" + threshold: 0.6 + use_cpu: true + + # Hallucination detector: verifies if LLM response is grounded in context + # Using large model (395M params) for better accuracy + hallucination_model: + model_id: "models/lettucedect-large-modernbert-en-v1" + threshold: 0.5 + use_cpu: true + + # NLI model: provides explanations for hallucinated spans + nli_model: + model_id: "models/ModernBERT-base-nli" + threshold: 0.7 + use_cpu: true + + # Action when hallucination detected: "warn" adds headers, "block" returns error + on_hallucination_detected: "warn" + +# Fact-check rules for signal classification +# The classifier outputs one of these signals that can be referenced in decision conditions +fact_check_rules: + - name: needs_fact_check + description: "Query contains factual claims that should be verified against context" + - name: no_fact_check_needed + description: "Query is creative, code-related, or opinion-based - no fact verification needed" + +# Prompt guard +prompt_guard: + enabled: true + use_modernbert: true + model_id: "models/jailbreak_classifier_modernbert-base_model" + threshold: 0.7 + use_cpu: true + jailbreak_mapping_path: "models/jailbreak_classifier_modernbert-base_model/jailbreak_type_mapping.json" + +# vLLM endpoint - real vLLM server +vllm_endpoints: + - name: "vllm-general" + address: "127.0.0.1" + port: 8083 + weight: 1 + health_check_path: "/health" + +# Model configuration - use the actual model from vLLM +model_config: + "Qwen/Qwen2.5-14B-Instruct-AWQ": + reasoning_family: "qwen3" + preferred_endpoints: ["vllm-general"] + +# Categories for routing +categories: + - name: general + description: "General questions" + mmlu_categories: ["other"] + - name: math + description: "Mathematics and quantitative reasoning" + mmlu_categories: ["math"] + - name: science + description: "Science questions" + mmlu_categories: ["physics", "chemistry", "biology"] + +strategy: "priority" + +decisions: + - name: "math_decision" + description: "Mathematics and quantitative reasoning" + priority: 100 + rules: + operator: "AND" + conditions: + - type: "domain" + name: "math" + modelRefs: + - model: "Qwen/Qwen2.5-14B-Instruct-AWQ" + use_reasoning: true + plugins: + - type: "pii" + configuration: + enabled: true + pii_types_allowed: [] + - type: "hallucination" + configuration: + enabled: true + use_nli: true + hallucination_action: "header" + unverified_factual_action: "header" + include_hallucination_details: false + + - name: "science_decision" + description: "Science questions" + priority: 100 + rules: + operator: "AND" + conditions: + - type: "domain" + name: "science" + modelRefs: + - model: "Qwen/Qwen2.5-14B-Instruct-AWQ" + use_reasoning: true + plugins: + - type: "pii" + configuration: + enabled: true + pii_types_allowed: [] + - type: "hallucination" + configuration: + enabled: true + use_nli: true + hallucination_action: "header" + unverified_factual_action: "header" + include_hallucination_details: false + + - name: "general_decision" + description: "General questions" + priority: 50 + rules: + operator: "AND" + conditions: + - type: "domain" + name: "general" + modelRefs: + - model: "Qwen/Qwen2.5-14B-Instruct-AWQ" + use_reasoning: false + plugins: + - type: "pii" + configuration: + enabled: true + pii_types_allowed: [] + - type: "hallucination" + configuration: + enabled: true + use_nli: true + hallucination_action: "header" + unverified_factual_action: "header" + include_hallucination_details: false + +default_model: "Qwen/Qwen2.5-14B-Instruct-AWQ" + +# API Configuration +api: + batch_classification: + metrics: + enabled: true + detailed_goroutine_tracking: true + high_resolution_timing: false + sample_rate: 1.0 + diff --git a/bench/hallucination/config.yaml b/bench/hallucination/config.yaml new file mode 100644 index 0000000000..487940974d --- /dev/null +++ b/bench/hallucination/config.yaml @@ -0,0 +1,181 @@ +# Configuration for hallucination detection benchmark +# Connects to real vLLM server at port 8083 + +bert_model: + model_id: models/all-MiniLM-L12-v2 + threshold: 0.6 + use_cpu: true + +semantic_cache: + enabled: false # Disable cache for benchmarking + +# Classifier configuration +classifier: + category_model: + model_id: "models/category_classifier_modernbert-base_model" + use_modernbert: true + threshold: 0.6 + use_cpu: true + category_mapping_path: "models/category_classifier_modernbert-base_model/category_mapping.json" + pii_model: + model_id: "models/pii_classifier_modernbert-base_presidio_token_model" + use_modernbert: true + threshold: 0.7 + use_cpu: true + pii_mapping_path: "models/pii_classifier_modernbert-base_presidio_token_model/pii_type_mapping.json" + +# Hallucination mitigation configuration +hallucination_mitigation: + enabled: true + + # Fact-check classifier: determines if a prompt needs fact verification + fact_check_model: + model_id: "models/halugate-sentinel" + threshold: 0.6 + use_cpu: true + + # Hallucination detector: verifies if LLM response is grounded in context + # Using large model (395M params) for better accuracy + hallucination_model: + model_id: "models/lettucedect-large-modernbert-en-v1" + threshold: 0.5 + use_cpu: true + + # NLI model: provides explanations for hallucinated spans + nli_model: + model_id: "models/ModernBERT-base-nli" + threshold: 0.7 + use_cpu: true + + # Action when hallucination detected: "warn" adds headers, "block" returns error + on_hallucination_detected: "warn" + +# Fact-check rules for signal classification +# The classifier outputs one of these signals that can be referenced in decision conditions +fact_check_rules: + - name: needs_fact_check + description: "Query contains factual claims that should be verified against context" + - name: no_fact_check_needed + description: "Query is creative, code-related, or opinion-based - no fact verification needed" + +# Prompt guard +prompt_guard: + enabled: true + use_modernbert: true + model_id: "models/jailbreak_classifier_modernbert-base_model" + threshold: 0.7 + use_cpu: true + jailbreak_mapping_path: "models/jailbreak_classifier_modernbert-base_model/jailbreak_type_mapping.json" + +# vLLM endpoint - real vLLM server +vllm_endpoints: + - name: "vllm-qwen" + address: "127.0.0.1" + port: 8083 + weight: 1 + health_check_path: "/health" + +# Model configuration - use the actual model from vLLM +model_config: + "Qwen/Qwen2.5-14B-Instruct-AWQ": + reasoning_family: "qwen3" + preferred_endpoints: ["vllm-qwen"] + +# Categories for routing +categories: + - name: general + description: "General questions" + mmlu_categories: ["other"] + - name: math + description: "Mathematics and quantitative reasoning" + mmlu_categories: ["math"] + - name: science + description: "Science questions" + mmlu_categories: ["physics", "chemistry", "biology"] + +strategy: "priority" + +decisions: + - name: "math_decision" + description: "Mathematics and quantitative reasoning" + priority: 100 + rules: + operator: "AND" + conditions: + - type: "domain" + name: "math" + modelRefs: + - model: "Qwen/Qwen2.5-14B-Instruct-AWQ" + use_reasoning: true + plugins: + - type: "pii" + configuration: + enabled: true + pii_types_allowed: [] + - type: "hallucination" + configuration: + enabled: true + use_nli: true + hallucination_action: "header" + unverified_factual_action: "header" + include_hallucination_details: false + + - name: "science_decision" + description: "Science questions" + priority: 100 + rules: + operator: "AND" + conditions: + - type: "domain" + name: "science" + modelRefs: + - model: "Qwen/Qwen2.5-14B-Instruct-AWQ" + use_reasoning: true + plugins: + - type: "pii" + configuration: + enabled: true + pii_types_allowed: [] + - type: "hallucination" + configuration: + enabled: true + use_nli: true + hallucination_action: "header" + unverified_factual_action: "header" + include_hallucination_details: false + + - name: "general_decision" + description: "General questions" + priority: 50 + rules: + operator: "AND" + conditions: + - type: "domain" + name: "general" + modelRefs: + - model: "Qwen/Qwen2.5-14B-Instruct-AWQ" + use_reasoning: false + plugins: + - type: "pii" + configuration: + enabled: true + pii_types_allowed: [] + - type: "hallucination" + configuration: + enabled: true + use_nli: true + hallucination_action: "header" + unverified_factual_action: "header" + include_hallucination_details: false + +default_model: "Qwen/Qwen2.5-14B-Instruct-AWQ" + +# API Configuration +api: + batch_classification: + metrics: + enabled: true + detailed_goroutine_tracking: true + high_resolution_timing: false + sample_rate: 1.0 + diff --git a/bench/hallucination/datasets.py b/bench/hallucination/datasets.py new file mode 100644 index 0000000000..ca3303b935 --- /dev/null +++ b/bench/hallucination/datasets.py @@ -0,0 +1,214 @@ +"""Dataset loaders for hallucination detection benchmarks. + +Supports: +- HaluEval: General hallucination evaluation dataset +- Custom datasets in JSONL format +""" + +import json +from abc import ABC, abstractmethod +from dataclasses import dataclass +from pathlib import Path +from typing import List, Optional, Iterator + +try: + from datasets import load_dataset + + HAS_DATASETS = True +except ImportError: + HAS_DATASETS = False + + +@dataclass +class HallucinationSample: + """A single sample for hallucination detection evaluation.""" + + id: str + context: str # Ground truth context (tool results / RAG context) + question: str # User question + gold_answer: str # Correct answer from context + llm_response: Optional[str] = None # Generated LLM response + hallucination_spans: Optional[List[dict]] = None # Annotated hallucination spans + is_faithful: Optional[bool] = None # Whether response is faithful to context + metadata: Optional[dict] = None # Additional metadata + + +class DatasetInterface(ABC): + """Abstract interface for hallucination datasets.""" + + @abstractmethod + def load(self, max_samples: Optional[int] = None) -> List[HallucinationSample]: + """Load the dataset.""" + pass + + @abstractmethod + def name(self) -> str: + """Dataset name.""" + pass + + +class HaluEvalDataset(DatasetInterface): + """HaluEval dataset loader - general hallucination evaluation.""" + + def name(self) -> str: + return "halueval" + + def load(self, max_samples: Optional[int] = None) -> List[HallucinationSample]: + """Load HaluEval dataset from HuggingFace.""" + if not HAS_DATASETS: + raise ImportError( + "datasets package not installed. Run: pip install datasets" + ) + + print(f"Loading HaluEval dataset...") + + try: + dataset = load_dataset("pminervini/HaluEval", "qa_samples", split="data") + except Exception as e: + print(f"Error loading HaluEval: {e}") + raise + + samples = [] + for i, item in enumerate(dataset): + if max_samples and i >= max_samples: + break + + samples.append( + HallucinationSample( + id=f"halueval_{i}", + context=item.get("knowledge", ""), + question=item.get("question", ""), + gold_answer=item.get("right_answer", ""), + llm_response=item.get( + "hallucinated_answer", item.get("answer", "") + ), + is_faithful=item.get("hallucination", "no") == "no", + metadata={ + "dataset": "halueval", + }, + ) + ) + + print(f"Loaded {len(samples)} samples from HaluEval") + return samples + + +class FinancialFactEvalDataset(DatasetInterface): + """FinanceBench dataset loader. + Loads `PatronusAI/financebench` from the Hugging Face datasets hub. + The dataset's schema varies between versions and splits, so the loader + maps common fields with sensible fallbacks. + """ + + def name(self) -> str: + return "financebench" + + def load(self, max_samples: Optional[int] = None) -> List[HallucinationSample]: + if not HAS_DATASETS: + raise ImportError( + "datasets package not installed. Run: pip install datasets" + ) + + print("Loading FinanceBench dataset...") + try: + ds = load_dataset("PatronusAI/financebench") + except Exception as e: + raise RuntimeError(f"Failed to load PatronusAI/financebench: {e}") + + samples: List[HallucinationSample] = [] + for i, item in enumerate(ds): + if max_samples and i >= max_samples: + break + + # Map common fields with fallbacks for different dataset schemas. + # The financebench dataset contains financial claims/questions and + # references; exact field names may vary across releases. + id_ = item.get("id") or item.get("financebench_id") or f"finance_{i}" + evidence = item.get("evidence") + if isinstance(evidence, dict) and "evidence_text" in evidence: + context = evidence["evidence_text"] + else: + context = "" + print( + f"Warning: 'evidence_text' not found in evidence for sample {id_}" + ) + question = item.get("question") + gold_answer = item.get("answer") + llm_response = None + hallucination_spans = None + is_faithful = None + # Some datasets include a binary correctness/faithful flag. + if "is_faithful" in item: + is_faithful = bool(item.get("is_faithful")) + + samples.append( + HallucinationSample( + id=str(id_), + context=str(context or ""), + question=str(question or ""), + gold_answer=str(gold_answer or ""), + llm_response=( + str(llm_response) if llm_response is not None else None + ), + hallucination_spans=hallucination_spans, + is_faithful=is_faithful, + metadata={"dataset": "financebench", "raw_keys": list(item.keys())}, + ) + ) + + print(f"Loaded {len(samples)} samples from FinanceBench") + return samples + + +class CustomDataset(DatasetInterface): + """Load custom dataset from JSONL file.""" + + def __init__(self, file_path: str): + self.file_path = Path(file_path) + self._name = self.file_path.stem + + def name(self) -> str: + return self._name + + def load(self, max_samples: Optional[int] = None) -> List[HallucinationSample]: + """Load dataset from JSONL file.""" + if not self.file_path.exists(): + raise FileNotFoundError(f"Dataset file not found: {self.file_path}") + + samples = [] + with open(self.file_path) as f: + for i, line in enumerate(f): + if max_samples and i >= max_samples: + break + + item = json.loads(line.strip()) + samples.append( + HallucinationSample( + id=item.get("id", f"custom_{i}"), + context=item.get("context", item.get("knowledge", "")), + question=item.get("question", item.get("query", "")), + gold_answer=item.get("gold_answer", item.get("answer", "")), + llm_response=item.get("llm_response", item.get("response", "")), + hallucination_spans=item.get("hallucination_spans", []), + is_faithful=item.get("is_faithful"), + metadata=item.get("metadata", {}), + ) + ) + + print(f"Loaded {len(samples)} samples from {self.file_path}") + return samples + + +def get_dataset(name: str, **kwargs) -> DatasetInterface: + """Factory function to get a dataset by name.""" + datasets = { + "halueval": HaluEvalDataset, + "financebench": FinancialFactEvalDataset, + } + + if name in datasets: + return datasets[name]() + elif Path(name).exists(): + return CustomDataset(name) + else: + raise ValueError(f"Unknown dataset: {name}. Available: {list(datasets.keys())}") diff --git a/bench/hallucination/evaluate.py b/bench/hallucination/evaluate.py new file mode 100644 index 0000000000..3a0bfbf16c --- /dev/null +++ b/bench/hallucination/evaluate.py @@ -0,0 +1,624 @@ +"""End-to-end hallucination detection benchmark. + +This module evaluates the hallucination detection pipeline by sending requests +to an already-running semantic router + vLLM setup. + +The benchmark: +1. Discovers available models from /v1/models endpoint +2. Sends requests with tool context through the router +3. Collects hallucination detection results from response headers +4. Calculates precision/recall metrics + +Usage: + # Discover models and run quick test + python -m bench.hallucination_bench.evaluate --max-samples 10 + + # Full evaluation with HaluEval dataset + python -m bench.hallucination_bench.evaluate --dataset halueval --max-samples 100 + + # Specify custom endpoint + python -m bench.hallucination_bench.evaluate --endpoint http://localhost:8801 +""" + +import argparse +import json +import sys +import time +from dataclasses import asdict, dataclass +from pathlib import Path +from typing import Dict, List, Optional + +import requests + +from .datasets import HallucinationSample, get_dataset + + +@dataclass +class DetectionResult: + """Result from hallucination detection.""" + + sample_id: str + question: str + context_length: int + response_length: int + model_used: str + + # Hallucination detection results (from response headers) + hallucination_detected: Optional[bool] = None + hallucination_spans: Optional[str] = None + + # Efficiency metrics (from response headers) + fact_check_needed: Optional[bool] = None # Was fact-check classification triggered? + detection_skipped: bool = False # Was detection skipped due to non-factual query? + + # Ground truth (if available) + gt_is_faithful: Optional[bool] = None + + # Metrics + latency_ms: Optional[float] = None + error: Optional[str] = None + + +@dataclass +class BenchmarkResults: + """Aggregated benchmark results.""" + + dataset_name: str + endpoint: str + model: str + total_samples: int + successful_requests: int + failed_requests: int + + # Detection metrics + total_hallucinations_detected: int + + # Accuracy (when ground truth available) + true_positives: int # Correctly detected hallucinations + false_positives: int # Incorrectly flagged as hallucination + true_negatives: int # Correctly identified faithful responses + false_negatives: int # Missed hallucinations + + precision: float + recall: float + f1_score: float + accuracy: float + + # Latency + avg_latency_ms: float + p50_latency_ms: float + p99_latency_ms: float + + # Individual results + results: List[dict] = None # type: ignore # Initialized in __post_init__ + + # Efficiency metrics (two-stage pipeline savings) + fact_check_needed_count: int = 0 # Queries that needed fact-checking + detection_skipped_count: int = 0 # Queries where detection was skipped + avg_context_length: float = 0.0 # Average context size in chars + estimated_detection_time_ms: float = 0.0 # Estimated time if all ran detection + actual_detection_time_ms: float = 0.0 # Actual detection time (skipped = 0) + time_saved_ms: float = 0.0 # Time saved by pre-filtering + efficiency_gain_percent: float = 0.0 # Percentage improvement + + def __post_init__(self): + if self.results is None: + self.results = [] + + +class HallucinationBenchmark: + """End-to-end hallucination detection benchmark.""" + + def __init__( + self, + endpoint: str = "http://localhost:8801", + timeout: int = 120, + ): + self.endpoint = endpoint.rstrip("/") + self.timeout = timeout + self.results: List[DetectionResult] = [] + self.available_models: List[str] = [] + + def discover_models(self) -> List[str]: + """Discover available models from /v1/models endpoint.""" + try: + resp = requests.get(f"{self.endpoint}/v1/models", timeout=10) + if resp.status_code != 200: + print(f"❌ Failed to get models: HTTP {resp.status_code}") + return [] + + data = resp.json() + models = [ + m.get("id", m.get("name", "unknown")) for m in data.get("data", []) + ] + self.available_models = models + + print(f"✓ Discovered {len(models)} models:") + for m in models: + print(f" - {m}") + + return models + except requests.exceptions.RequestException as e: + print(f"❌ Failed to discover models: {e}") + return [] + + def check_health(self) -> bool: + """Check if the endpoint is healthy.""" + try: + # Try /v1/models as health check + resp = requests.get(f"{self.endpoint}/v1/models", timeout=10) + if resp.status_code == 200: + print(f"✓ Endpoint {self.endpoint} is healthy") + return True + else: + print(f"❌ Endpoint returned HTTP {resp.status_code}") + return False + except requests.exceptions.RequestException as e: + print(f"❌ Health check failed: {e}") + return False + + def send_request( + self, + sample: HallucinationSample, + model: str, + include_context: bool = True, + ) -> DetectionResult: + """Send a request and collect hallucination detection results.""" + start_time = time.time() + + result = DetectionResult( + sample_id=sample.id, + question=sample.question[:100], + context_length=len(sample.context), + response_length=0, + model_used=model, + gt_is_faithful=sample.is_faithful, + ) + + try: + # Build messages - include tool context if available + messages = [{"role": "user", "content": sample.question}] + + if include_context and sample.context: + # Add tool result with context (enables hallucination detection) + messages.append( + { + "role": "tool", + "tool_call_id": f"ctx_{sample.id}", + "content": sample.context, + } + ) + + # Send request + response = requests.post( + f"{self.endpoint}/v1/chat/completions", + json={ + "model": model, + "messages": messages, + "max_tokens": 512, + "temperature": 0.7, + }, + timeout=self.timeout, + ) + + latency_ms = (time.time() - start_time) * 1000 + result.latency_ms = latency_ms + + if response.status_code != 200: + result.error = f"HTTP {response.status_code}: {response.text[:200]}" + return result + + # Parse response + data = response.json() + if "choices" in data and data["choices"]: + content = data["choices"][0].get("message", {}).get("content", "") + result.response_length = len(content) + + # Extract hallucination detection headers + headers = response.headers + result.hallucination_detected = ( + headers.get("x-vsr-hallucination-detected", "").lower() == "true" + ) + result.hallucination_spans = headers.get("x-vsr-hallucination-spans", "") + + # Extract efficiency-related headers + result.fact_check_needed = ( + headers.get("x-vsr-fact-check-needed", "").lower() == "true" + ) + # Detection is skipped if fact-check says not needed, or if unverified (no context) + unverified = ( + headers.get("x-vsr-unverified-factual-response", "").lower() == "true" + ) + result.detection_skipped = not result.fact_check_needed or unverified + + except requests.exceptions.Timeout: + result.error = "Request timeout" + except requests.exceptions.RequestException as e: + result.error = str(e) + except Exception as e: + result.error = f"Unexpected error: {e}" + + return result + + def run_benchmark( + self, + samples: List[HallucinationSample], + model: str, + include_context: bool = True, + verbose: bool = True, + ) -> BenchmarkResults: + """Run the benchmark on a list of samples.""" + from tqdm import tqdm + + print(f"\nRunning hallucination detection benchmark...") + print(f" Endpoint: {self.endpoint}") + print(f" Model: {model}") + print(f" Samples: {len(samples)}") + print(f" Context: {'included' if include_context else 'excluded'}") + print() + + self.results = [] + detected_count = 0 + + # Use tqdm with leave=True to keep progress bar visible + pbar = tqdm( + samples, + desc="Evaluating", + unit="sample", + bar_format="{l_bar}{bar}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}, {rate_fmt}] Hallucinations: {postfix}", + ) + pbar.set_postfix_str("0") + + for sample in pbar: + result = self.send_request(sample, model, include_context=include_context) + self.results.append(result) + + # Print hallucination detection immediately when found + if result.hallucination_detected and verbose: + detected_count += 1 + pbar.set_postfix_str(str(detected_count)) + + # Print detection details below progress bar + tqdm.write("") + tqdm.write(f"🚨 HALLUCINATION DETECTED [{detected_count}]") + tqdm.write(f" Question: {result.question[:80]}...") + if result.hallucination_spans: + spans_preview = result.hallucination_spans[:150] + tqdm.write( + f" Spans: {spans_preview}{'...' if len(result.hallucination_spans) > 150 else ''}" + ) + if result.gt_is_faithful is not None: + gt_label = ( + "✅ Correct" + if not result.gt_is_faithful + else "❌ False Positive" + ) + tqdm.write(f" Ground Truth: {gt_label}") + tqdm.write(f" Latency: {result.latency_ms:.0f}ms") + tqdm.write("") + + # Also print errors immediately + elif result.error and verbose: + tqdm.write(f"❌ ERROR: {result.error[:100]}") + + dataset_name = ( + samples[0].metadata.get("dataset", "unknown") if samples else "unknown" + ) + return self.calculate_metrics(dataset_name, model) + + def calculate_metrics(self, dataset_name: str, model: str) -> BenchmarkResults: + """Calculate benchmark metrics from results.""" + successful = [r for r in self.results if r.error is None] + failed = [r for r in self.results if r.error is not None] + + # Detection counts + hallucinations_detected = sum(1 for r in successful if r.hallucination_detected) + + # Accuracy metrics (when ground truth available) + tp = fp = tn = fn = 0 + for r in successful: + if r.gt_is_faithful is not None: + detected = r.hallucination_detected or False + is_hallucination = not r.gt_is_faithful + + if detected and is_hallucination: + tp += 1 + elif detected and not is_hallucination: + fp += 1 + elif not detected and not is_hallucination: + tn += 1 + elif not detected and is_hallucination: + fn += 1 + + total_with_gt = tp + fp + tn + fn + precision = tp / (tp + fp) if (tp + fp) > 0 else 0.0 + recall = tp / (tp + fn) if (tp + fn) > 0 else 0.0 + f1 = ( + 2 * precision * recall / (precision + recall) + if (precision + recall) > 0 + else 0.0 + ) + accuracy = (tp + tn) / total_with_gt if total_with_gt > 0 else 0.0 + + # Latency metrics + latencies = [r.latency_ms for r in successful if r.latency_ms is not None] + avg_latency = sum(latencies) / len(latencies) if latencies else 0.0 + sorted_latencies = sorted(latencies) + p50 = sorted_latencies[len(sorted_latencies) // 2] if sorted_latencies else 0.0 + p99_idx = min(int(len(sorted_latencies) * 0.99), len(sorted_latencies) - 1) + p99 = sorted_latencies[p99_idx] if sorted_latencies else 0.0 + + # Efficiency metrics - calculate savings from two-stage pipeline + fact_check_needed_count = sum(1 for r in successful if r.fact_check_needed) + detection_skipped_count = sum(1 for r in successful if r.detection_skipped) + + # Average context length + context_lengths = [r.context_length for r in successful if r.context_length > 0] + avg_context_length = ( + sum(context_lengths) / len(context_lengths) if context_lengths else 0.0 + ) + + # Estimate detection time based on context length + # ModernBERT-large (395M params) timing estimates: + # - Fact-check classifier (ModernBERT-base): ~12ms (prompt only) + # - LettuceDetect-large: ~45ms base + ~0.02ms per character + # These are empirical measurements on CPU; GPU would be faster + CLASSIFIER_TIME_MS = ( + 12.0 # Fact-check classifier time (ModernBERT-base, prompt only) + ) + DETECTION_BASE_MS = 45.0 # Base detection time (ModernBERT-large) + DETECTION_PER_CHAR_MS = 0.02 # Additional time per character (large model) + + estimated_detection_times = [] + actual_detection_times = [] + for r in successful: + # Estimated time if we ran detection on everything + est_time = DETECTION_BASE_MS + (r.context_length * DETECTION_PER_CHAR_MS) + estimated_detection_times.append(est_time) + + # Actual time (0 if skipped, otherwise estimated) + if r.detection_skipped: + actual_detection_times.append(0.0) + else: + actual_detection_times.append(est_time) + + estimated_total = sum(estimated_detection_times) + actual_total = sum(actual_detection_times) + time_saved = estimated_total - actual_total + efficiency_gain = ( + (time_saved / estimated_total * 100) if estimated_total > 0 else 0.0 + ) + + return BenchmarkResults( + dataset_name=dataset_name, + endpoint=self.endpoint, + model=model, + total_samples=len(self.results), + successful_requests=len(successful), + failed_requests=len(failed), + total_hallucinations_detected=hallucinations_detected, + true_positives=tp, + false_positives=fp, + true_negatives=tn, + false_negatives=fn, + precision=precision, + recall=recall, + f1_score=f1, + accuracy=accuracy, + avg_latency_ms=avg_latency, + p50_latency_ms=p50, + p99_latency_ms=p99, + # Efficiency metrics + fact_check_needed_count=fact_check_needed_count, + detection_skipped_count=detection_skipped_count, + avg_context_length=avg_context_length, + estimated_detection_time_ms=estimated_total, + actual_detection_time_ms=actual_total, + time_saved_ms=time_saved, + efficiency_gain_percent=efficiency_gain, + results=[asdict(r) for r in self.results], + ) + + +def print_results(results: BenchmarkResults): + """Print benchmark results.""" + print("\n" + "=" * 60) + print(f"HALLUCINATION DETECTION BENCHMARK") + print("=" * 60) + + print(f"\n📌 Configuration:") + print("-" * 40) + print(f" Dataset: {results.dataset_name}") + print(f" Endpoint: {results.endpoint}") + print(f" Model: {results.model}") + + print(f"\n📊 Request Statistics:") + print("-" * 40) + print(f" Total samples: {results.total_samples}") + print(f" Successful: {results.successful_requests}") + print(f" Failed: {results.failed_requests}") + + print(f"\n📋 Detection Results:") + print("-" * 40) + print(f" Hallucinations detected: {results.total_hallucinations_detected}") + + total_gt = ( + results.true_positives + + results.false_positives + + results.true_negatives + + results.false_negatives + ) + if total_gt > 0: + print(f"\n🎯 Accuracy Metrics (vs ground truth):") + print("-" * 40) + print(f" True Positives: {results.true_positives}") + print(f" False Positives: {results.false_positives}") + print(f" True Negatives: {results.true_negatives}") + print(f" False Negatives: {results.false_negatives}") + print(f" Precision: {results.precision:.4f}") + print(f" Recall: {results.recall:.4f}") + print(f" F1 Score: {results.f1_score:.4f}") + print(f" Accuracy: {results.accuracy:.4f}") + else: + print(f"\n⚠️ No ground truth labels available for accuracy metrics") + + print(f"\n⏱️ Latency:") + print("-" * 40) + print(f" Average: {results.avg_latency_ms:.2f} ms") + print(f" P50: {results.p50_latency_ms:.2f} ms") + print(f" P99: {results.p99_latency_ms:.2f} ms") + + # Efficiency metrics - two-stage pipeline savings + print(f"\n⚡ Two-Stage Pipeline Efficiency:") + print("-" * 40) + print( + f" Fact-check needed: {results.fact_check_needed_count}/{results.successful_requests} queries" + ) + print( + f" Detection skipped: {results.detection_skipped_count}/{results.successful_requests} queries" + ) + print(f" Avg context length: {results.avg_context_length:.0f} chars") + print( + f" Estimated detect time: {results.estimated_detection_time_ms:.2f} ms (if all ran)" + ) + print(f" Actual detect time: {results.actual_detection_time_ms:.2f} ms") + print(f" Time saved: {results.time_saved_ms:.2f} ms") + print(f" Efficiency gain: {results.efficiency_gain_percent:.1f}%") + + if results.detection_skipped_count > 0: + skip_rate = results.detection_skipped_count / results.successful_requests * 100 + print(f"\n 💡 Pre-filtering skipped {skip_rate:.1f}% of requests,") + print(f" saving {results.time_saved_ms:.0f}ms of detection compute.") + + # Show sample results + print(f"\n🔍 Sample Results (first 5):") + print("-" * 60) + for r in results.results[:5]: + detected = "🚨" if r.get("hallucination_detected") else "✅" + gt = ( + "HAL" + if r.get("gt_is_faithful") is False + else ("OK" if r.get("gt_is_faithful") else "?") + ) + print(f" {detected} GT:{gt} | {r.get('question', '')[:50]}...") + + print("=" * 60) + + +def main(): + parser = argparse.ArgumentParser(description="Hallucination Detection Benchmark") + + # Endpoint options + parser.add_argument( + "--endpoint", + type=str, + default="http://localhost:8801", + help="Router/Envoy endpoint URL", + ) + + # Dataset options + parser.add_argument( + "--dataset", + type=str, + default="halueval", + help="Dataset to use (halueval, or path to JSONL)", + ) + parser.add_argument( + "--max-samples", type=int, default=50, help="Maximum samples to evaluate" + ) + + # Model options + parser.add_argument( + "--model", + type=str, + default=None, + help="Model to use (auto-discovered if not specified)", + ) + + # Test modes + parser.add_argument( + "--no-context", + action="store_true", + help="Don't include tool context (disables hallucination detection)", + ) + parser.add_argument( + "--list-models", action="store_true", help="Just list available models and exit" + ) + parser.add_argument( + "--quiet", + action="store_true", + help="Don't print hallucinations as they're detected", + ) + + # Output options + parser.add_argument( + "--output-dir", + type=str, + default="bench/hallucination/results", + help="Output directory for results", + ) + + args = parser.parse_args() + + # Create benchmark + benchmark = HallucinationBenchmark(endpoint=args.endpoint) + + # Check health and discover models + if not benchmark.check_health(): + print(f"\n❌ Cannot connect to {args.endpoint}") + print(" Make sure the semantic router and vLLM are running.") + sys.exit(1) + + models = benchmark.discover_models() + if not models: + print("\n❌ No models available") + sys.exit(1) + + if args.list_models: + print("\nAvailable models:") + for m in models: + print(f" - {m}") + sys.exit(0) + + # Select model + model = args.model or models[0] + if model not in models and model != "auto": + print(f"\n⚠️ Model '{model}' not in available models, using anyway...") + + print(f"\n📦 Using model: {model}") + + # Load dataset + try: + dataset = get_dataset(args.dataset) + samples = dataset.load(max_samples=args.max_samples) + except Exception as e: + print(f"\n❌ Failed to load dataset: {e}") + sys.exit(1) + + # Run benchmark + results = benchmark.run_benchmark( + samples, + model=model, + include_context=not args.no_context, + verbose=not args.quiet, + ) + + # Print results + print_results(results) + + # Save results + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + + timestamp = time.strftime("%Y%m%d_%H%M%S") + results_file = output_dir / f"results_{args.dataset}_{timestamp}.json" + with open(results_file, "w") as f: + # Don't include individual results in JSON to keep file small + summary = asdict(results) + summary["results"] = summary["results"][:10] # Only first 10 for summary + json.dump(summary, f, indent=2) + print(f"\n📁 Results saved to {results_file}") + + +if __name__ == "__main__": + main() diff --git a/candle-binding/README.md b/candle-binding/README.md index d717f69f98..44a8e3152a 100644 --- a/candle-binding/README.md +++ b/candle-binding/README.md @@ -48,4 +48,4 @@ go test -v ## Notes - The Go tests depend on the native library being present and correctly built. -- Some tests may download data from the internet (e.g., from norvig.com). +- Some tests may download data from the internet (e.g., from norvig.com). diff --git a/config/config.yaml b/config/config.yaml index c62261b0c0..e4fc85bfb7 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -3,6 +3,14 @@ bert_model: threshold: 0.6 use_cpu: true +# Response API Configuration +# Enables OpenAI Response API support with conversation chaining +response_api: + enabled: true + store_backend: "memory" # Options: "memory", "milvus", "redis" + ttl_seconds: 86400 # 24 hours + max_responses: 1000 + semantic_cache: enabled: true backend_type: "memory" # Options: "memory", "milvus", or "hybrid" diff --git a/config/model_manager/models.lora.yaml b/config/model_manager/models.lora.yaml new file mode 100644 index 0000000000..22375f144f --- /dev/null +++ b/config/model_manager/models.lora.yaml @@ -0,0 +1,31 @@ +# LoRA Adapters Only +# This file contains only LoRA adapter models for incremental CI downloads. +# +# Usage: +# python -m model_manager --config config/model_manager/models.lora.yaml +# +# Equivalent to: make download-models-lora + +cache_dir: "models" +verify: "size" + +models: + # ============================================================================= + # LoRA Adapters - BERT-based + # ============================================================================= + + - id: lora_intent_classifier_bert-base-uncased_model + repo_id: LLM-Semantic-Router/lora_intent_classifier_bert-base-uncased_model + + - id: lora_pii_detector_bert-base-uncased_model + repo_id: LLM-Semantic-Router/lora_pii_detector_bert-base-uncased_model + + - id: lora_jailbreak_classifier_bert-base-uncased_model + repo_id: LLM-Semantic-Router/lora_jailbreak_classifier_bert-base-uncased_model + + # ============================================================================= + # Embedding Models (Advanced) + # ============================================================================= + + - id: Qwen3-Embedding-0.6B + repo_id: Qwen/Qwen3-Embedding-0.6B diff --git a/config/model_manager/models.minimal.yaml b/config/model_manager/models.minimal.yaml new file mode 100644 index 0000000000..cff4b5d06f --- /dev/null +++ b/config/model_manager/models.minimal.yaml @@ -0,0 +1,78 @@ +# Minimal Model Set for CI +# This file contains only the essential models needed for CI tests. +# Use this to avoid rate limits and speed up CI runs. +# +# Usage: +# CI_MINIMAL_MODELS=true python -m model_manager +# # or explicitly: +# python -m model_manager --config config/model_manager/models.minimal.yaml +# +# Equivalent to: make download-models-minimal +# or CI_MINIMAL_MODELS=true make download-models +# +# Note: This is the minimal set for fast CI runs. Larger models like +# embeddinggemma-300m are in models.yaml (full set) for local development. + +cache_dir: "models" +verify: "size" # Use size for faster CI runs + +models: + # ============================================================================= + # Core Classifiers (ModernBERT) - Required for classification tests + # ============================================================================= + + - id: category_classifier_modernbert-base_model + repo_id: LLM-Semantic-Router/category_classifier_modernbert-base_model + + - id: pii_classifier_modernbert-base_presidio_token_model + repo_id: LLM-Semantic-Router/pii_classifier_modernbert-base_presidio_token_model + + - id: jailbreak_classifier_modernbert-base_model + repo_id: LLM-Semantic-Router/jailbreak_classifier_modernbert-base_model + + - id: pii_classifier_modernbert-base_model + repo_id: LLM-Semantic-Router/pii_classifier_modernbert-base_model + + # ============================================================================= + # LoRA Adapters (BERT-based) - Required for unified classifier tests + # ============================================================================= + + - id: lora_intent_classifier_bert-base-uncased_model + repo_id: LLM-Semantic-Router/lora_intent_classifier_bert-base-uncased_model + + - id: lora_pii_detector_bert-base-uncased_model + repo_id: LLM-Semantic-Router/lora_pii_detector_bert-base-uncased_model + + - id: lora_jailbreak_classifier_bert-base-uncased_model + repo_id: LLM-Semantic-Router/lora_jailbreak_classifier_bert-base-uncased_model + + # ============================================================================= + # Embedding Models - Required for cache and embedding tests + # ============================================================================= + + - id: all-MiniLM-L12-v2 + repo_id: sentence-transformers/all-MiniLM-L12-v2 + + - id: Qwen3-Embedding-0.6B + repo_id: Qwen/Qwen3-Embedding-0.6B + + # ============================================================================= + # Hallucination Detection - Required for hallucination tests + # ============================================================================= + + - id: halugate-detector + repo_id: KRLabsOrg/lettucedect-base-modernbert-en-v1 + + - id: halugate-sentinel + repo_id: LLM-Semantic-Router/halugate-sentinel + + - id: ModernBERT-base-nli + repo_id: tasksource/ModernBERT-base-nli + + # ============================================================================= + # LLM Models - For llm-katan tests + # ============================================================================= + + - id: Qwen/Qwen3-0.6B + repo_id: Qwen/Qwen3-0.6B + local_dir: Qwen/Qwen3-0.6B diff --git a/config/model_manager/models.yaml b/config/model_manager/models.yaml new file mode 100644 index 0000000000..fd32b268bf --- /dev/null +++ b/config/model_manager/models.yaml @@ -0,0 +1,104 @@ +# Full Model Set for Local Development +# This file contains ALL models for full local development, matching models.mk download-models-full +# +# Usage: +# python -m model_manager +# python -m model_manager --config config/model_manager/models.yaml +# +# Includes additional LoRA variants (roberta, modernbert) and gated models not in minimal set. +# +# Equivalent to: make download-models-full + +cache_dir: "models" +verify: "sha256" # Full verification for production use + +models: + # ============================================================================= + # Core Classifiers (ModernBERT) + # ============================================================================= + + - id: category_classifier_modernbert-base_model + repo_id: LLM-Semantic-Router/category_classifier_modernbert-base_model + + - id: pii_classifier_modernbert-base_presidio_token_model + repo_id: LLM-Semantic-Router/pii_classifier_modernbert-base_presidio_token_model + + - id: jailbreak_classifier_modernbert-base_model + repo_id: LLM-Semantic-Router/jailbreak_classifier_modernbert-base_model + + - id: pii_classifier_modernbert-base_model + repo_id: LLM-Semantic-Router/pii_classifier_modernbert-base_model + + # ============================================================================= + # LoRA Adapters - BERT-based + # ============================================================================= + + - id: lora_intent_classifier_bert-base-uncased_model + repo_id: LLM-Semantic-Router/lora_intent_classifier_bert-base-uncased_model + + - id: lora_pii_detector_bert-base-uncased_model + repo_id: LLM-Semantic-Router/lora_pii_detector_bert-base-uncased_model + + - id: lora_jailbreak_classifier_bert-base-uncased_model + repo_id: LLM-Semantic-Router/lora_jailbreak_classifier_bert-base-uncased_model + + # ============================================================================= + # LoRA Adapters - RoBERTa-based (Full only) + # ============================================================================= + + - id: lora_intent_classifier_roberta-base_model + repo_id: LLM-Semantic-Router/lora_intent_classifier_roberta-base_model + + - id: lora_pii_detector_roberta-base_model + repo_id: LLM-Semantic-Router/lora_pii_detector_roberta-base_model + + - id: lora_jailbreak_classifier_roberta-base_model + repo_id: LLM-Semantic-Router/lora_jailbreak_classifier_roberta-base_model + + # ============================================================================= + # LoRA Adapters - ModernBERT-based (Full only) + # ============================================================================= + + - id: lora_intent_classifier_modernbert-base_model + repo_id: LLM-Semantic-Router/lora_intent_classifier_modernbert-base_model + + - id: lora_pii_detector_modernbert-base_model + repo_id: LLM-Semantic-Router/lora_pii_detector_modernbert-base_model + + - id: lora_jailbreak_classifier_modernbert-base_model + repo_id: LLM-Semantic-Router/lora_jailbreak_classifier_modernbert-base_model + + # ============================================================================= + # Embedding Models + # ============================================================================= + + - id: all-MiniLM-L12-v2 + repo_id: sentence-transformers/all-MiniLM-L12-v2 + + - id: Qwen3-Embedding-0.6B + repo_id: Qwen/Qwen3-Embedding-0.6B + + # Gated model - requires HF_TOKEN + - id: embeddinggemma-300m + repo_id: google/embeddinggemma-300m + + # ============================================================================= + # Hallucination Detection Models + # ============================================================================= + + - id: halugate-detector + repo_id: KRLabsOrg/lettucedect-base-modernbert-en-v1 + + - id: halugate-sentinel + repo_id: LLM-Semantic-Router/halugate-sentinel + + - id: ModernBERT-base-nli + repo_id: tasksource/ModernBERT-base-nli + + # ============================================================================= + # LLM Models (for llm-katan) + # ============================================================================= + + - id: Qwen/Qwen3-0.6B + repo_id: Qwen/Qwen3-0.6B + local_dir: Qwen/Qwen3-0.6B diff --git a/config/config.prompt_guard.vllm.yaml b/config/prompt-guard/prompt_guard.yaml similarity index 100% rename from config/config.prompt_guard.vllm.yaml rename to config/prompt-guard/prompt_guard.yaml diff --git a/config/config.redis.yaml b/config/semantic-cache/config.redis.yaml similarity index 100% rename from config/config.redis.yaml rename to config/semantic-cache/config.redis.yaml diff --git a/config/testing/config.hallucination.yaml b/config/testing/config.hallucination.yaml index 14545ec29c..9207e4112c 100644 --- a/config/testing/config.hallucination.yaml +++ b/config/testing/config.hallucination.yaml @@ -42,7 +42,6 @@ hallucination_mitigation: model_id: "models/halugate-sentinel" threshold: 0.6 use_cpu: true - mapping_path: "models/halugate-sentinel/fact_check_mapping.json" # Hallucination detector: verifies if LLM response is grounded in context hallucination_model: diff --git a/dashboard/backend/handlers/config.go b/dashboard/backend/handlers/config.go index 03abae9339..ee886b4a7e 100644 --- a/dashboard/backend/handlers/config.go +++ b/dashboard/backend/handlers/config.go @@ -71,7 +71,7 @@ func UpdateConfigHandler(configPath string) http.HandlerFunc { } // Write to file - if err := os.WriteFile(configPath, yamlData, 0644); err != nil { + if err := os.WriteFile(configPath, yamlData, 0o644); err != nil { log.Printf("Error writing config file: %v", err) http.Error(w, fmt.Sprintf("Failed to write config file: %v", err), http.StatusInternalServerError) return @@ -80,6 +80,8 @@ func UpdateConfigHandler(configPath string) http.HandlerFunc { log.Printf("Configuration updated successfully") w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(map[string]string{"status": "success", "message": "Configuration updated successfully"}) + if err := json.NewEncoder(w).Encode(map[string]string{"status": "success", "message": "Configuration updated successfully"}); err != nil { + log.Printf("Error encoding response: %v", err) + } } } diff --git a/dashboard/backend/handlers/health.go b/dashboard/backend/handlers/health.go index 2e8d349823..13c633d844 100644 --- a/dashboard/backend/handlers/health.go +++ b/dashboard/backend/handlers/health.go @@ -8,5 +8,5 @@ import ( func HealthCheck(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - w.Write([]byte(`{"status":"healthy","service":"semantic-router-dashboard"}`)) + _, _ = w.Write([]byte(`{"status":"healthy","service":"semantic-router-dashboard"}`)) } diff --git a/dashboard/backend/handlers/static.go b/dashboard/backend/handlers/static.go index 67dc80c59e..0bbe4d034a 100644 --- a/dashboard/backend/handlers/static.go +++ b/dashboard/backend/handlers/static.go @@ -19,7 +19,7 @@ func StaticFileServer(staticDir string) http.Handler { strings.HasPrefix(p, "/metrics/") || strings.HasPrefix(p, "/public/") || strings.HasPrefix(p, "/avatar/") || strings.HasPrefix(p, "/_app/") || strings.HasPrefix(p, "/_next/") || strings.HasPrefix(p, "/chatui/") || - p == "/conversation" || strings.HasPrefix(p, "/conversations") || + strings.HasPrefix(p, "/static/") || p == "/conversation" || strings.HasPrefix(p, "/conversations") || strings.HasPrefix(p, "/settings") || p == "/login" || p == "/logout" || strings.HasPrefix(p, "/r/") { // These paths should have been handled by other handlers diff --git a/dashboard/backend/proxy/proxy.go b/dashboard/backend/proxy/proxy.go index 246fc8eacf..87b1ba53db 100644 --- a/dashboard/backend/proxy/proxy.go +++ b/dashboard/backend/proxy/proxy.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "log" + "net" "net/http" "net/http/httputil" "net/url" @@ -42,7 +43,6 @@ func NewReverseProxy(targetBase, stripPrefix string, forwardAuth bool) (*httputi p = "/" + p } r.URL.Path = p - r.Host = targetURL.Host // Capture incoming Origin for downstream CORS decisions incomingOrigin := r.Header.Get("Origin") @@ -62,9 +62,50 @@ func NewReverseProxy(targetBase, stripPrefix string, forwardAuth bool) (*httputi } // Set Origin header to match the target URL for iframe embedding - // This is required for services like Grafana and Chat UI to accept the iframe embedding + // This is required for services like Grafana, Chat UI, and OpenWebUI to accept the iframe embedding + // and pass CSRF/Origin validation checks. The original Origin is preserved in X-Forwarded-Origin + // for CORS response handling. This override is intentional and necessary for iframe embedding to work. r.Header.Set("Origin", targetURL.Scheme+"://"+targetURL.Host) + // Set X-Forwarded-* headers to preserve client information + // These headers should reflect the original client request, not the target service + r.Header.Set("X-Forwarded-Host", r.Host) + + // Determine the original protocol (http or https) + proto := "http" + if r.TLS != nil { + proto = "https" + } + // Also check X-Forwarded-Proto from upstream (if we're behind another proxy) + if forwardedProto := r.Header.Get("X-Forwarded-Proto"); forwardedProto != "" { + proto = forwardedProto + } + r.Header.Set("X-Forwarded-Proto", proto) + + // Extract client IP from RemoteAddr (strip port if present) + var clientIP string + if r.RemoteAddr != "" { + ip, _, err := net.SplitHostPort(r.RemoteAddr) + if err != nil { + // If SplitHostPort fails, RemoteAddr might not have a port + clientIP = r.RemoteAddr + } else { + clientIP = ip + } + } + + // Append to existing X-Forwarded-For if present (we might be behind another proxy) + if clientIP != "" { + if existing := r.Header.Get("X-Forwarded-For"); existing != "" { + r.Header.Set("X-Forwarded-For", existing+", "+clientIP) + } else { + r.Header.Set("X-Forwarded-For", clientIP) + } + } + + // Set Host header to match target (some services check this) + r.Host = targetURL.Host + // Optionally forward Authorization header if !forwardAuth { r.Header.Del("Authorization") diff --git a/dashboard/backend/router/router.go b/dashboard/backend/router/router.go index a6ec0c8a96..c7cefa3889 100644 --- a/dashboard/backend/router/router.go +++ b/dashboard/backend/router/router.go @@ -55,36 +55,77 @@ func Setup(cfg *config.Config) *http.ServeMux { }) // Proxy for Grafana static assets (no prefix stripping) - grafanaStaticProxy, _ = proxy.NewReverseProxy(cfg.GrafanaURL, "", false) + grafanaStaticProxy, err = proxy.NewReverseProxy(cfg.GrafanaURL, "", false) + if err != nil { + log.Printf("Warning: failed to create Grafana static proxy: %v", err) + grafanaStaticProxy = nil + } mux.HandleFunc("/public/", func(w http.ResponseWriter, r *http.Request) { if middleware.HandleCORSPreflight(w, r) { return } + if grafanaStaticProxy == nil { + w.Header().Set("Content-Type", "application/json") + http.Error(w, `{"error":"Service not available","message":"Grafana static proxy not configured"}`, http.StatusBadGateway) + return + } grafanaStaticProxy.ServeHTTP(w, r) }) mux.HandleFunc("/avatar/", func(w http.ResponseWriter, r *http.Request) { if middleware.HandleCORSPreflight(w, r) { return } + if grafanaStaticProxy == nil { + w.Header().Set("Content-Type", "application/json") + http.Error(w, `{"error":"Service not available","message":"Grafana static proxy not configured"}`, http.StatusBadGateway) + return + } grafanaStaticProxy.ServeHTTP(w, r) }) - log.Printf("Grafana proxy configured: %s", cfg.GrafanaURL) - log.Printf("Grafana static assets proxied: /public/, /avatar/") + if grafanaStaticProxy != nil { + log.Printf("Grafana proxy configured: %s", cfg.GrafanaURL) + log.Printf("Grafana static assets proxied: /public/, /avatar/") + } else { + log.Printf("Grafana proxy configured: %s (static proxy failed to initialize)", cfg.GrafanaURL) + } } else { mux.HandleFunc("/embedded/grafana/", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusServiceUnavailable) - w.Write([]byte(`{"error":"Grafana not configured","message":"TARGET_GRAFANA_URL environment variable is not set"}`)) + _, _ = w.Write([]byte(`{"error":"Grafana not configured","message":"TARGET_GRAFANA_URL environment variable is not set"}`)) }) log.Printf("Warning: Grafana URL not configured") } + // OpenWebUI static proxy (needs to be set up early for the smart /api/ router below) + var openwebuiStaticProxy *httputil.ReverseProxy + if cfg.OpenWebUIURL != "" { + var err error + openwebuiStaticProxy, err = proxy.NewReverseProxy(cfg.OpenWebUIURL, "", false) + if err != nil { + log.Printf("Warning: failed to create OpenWebUI static proxy: %v", err) + openwebuiStaticProxy = nil + } + } + // Jaeger API proxy (needs to be set up early for the smart router below) var jaegerAPIProxy *httputil.ReverseProxy + var jaegerStaticProxy *httputil.ReverseProxy if cfg.JaegerURL != "" { // Create proxy for Jaeger API (no prefix stripping for /api/*) - jaegerAPIProxy, _ = proxy.NewReverseProxy(cfg.JaegerURL, "", false) + var err error + jaegerAPIProxy, err = proxy.NewReverseProxy(cfg.JaegerURL, "", false) + if err != nil { + log.Printf("Warning: failed to create Jaeger API proxy: %v", err) + jaegerAPIProxy = nil + } + // Create proxy for Jaeger static assets (reused in handlers) + jaegerStaticProxy, err = proxy.NewReverseProxy(cfg.JaegerURL, "", false) + if err != nil { + log.Printf("Warning: failed to create Jaeger static proxy: %v", err) + jaegerStaticProxy = nil + } } // Chat UI proxy (exposed early for smart /api routing and root-level assets) @@ -92,7 +133,12 @@ func Setup(cfg *config.Config) *http.ServeMux { var chatUIProxy *httputil.ReverseProxy if cfg.ChatUIURL != "" { // Root-level proxy (no prefix stripping) for assets and API - chatUIProxy, _ = proxy.NewReverseProxy(cfg.ChatUIURL, "", false) + var err error + chatUIProxy, err = proxy.NewReverseProxy(cfg.ChatUIURL, "", false) + if err != nil { + log.Printf("Warning: failed to create ChatUI proxy: %v", err) + chatUIProxy = nil + } // Main UI under /embedded/chatui with prefix stripping cup, err := proxy.NewReverseProxy(cfg.ChatUIURL, "/embedded/chatui", false) if err != nil { @@ -110,19 +156,18 @@ func Setup(cfg *config.Config) *http.ServeMux { } cup.ServeHTTP(w, r) }) - // Static assets commonly used by HF Chat UI (SvelteKit/Next) - mux.HandleFunc("/_app/", func(w http.ResponseWriter, r *http.Request) { - if middleware.HandleCORSPreflight(w, r) { - return - } - log.Printf("Proxying Chat UI asset: %s", r.URL.Path) - chatUIProxy.ServeHTTP(w, r) - }) + // Note: /_app/ is also used by OpenWebUI, so it's handled by OpenWebUI's handler + // (registered later) which checks referer and routes to ChatUI if needed // SvelteKit static assets mux.HandleFunc("/_next/", func(w http.ResponseWriter, r *http.Request) { if middleware.HandleCORSPreflight(w, r) { return } + if chatUIProxy == nil { + w.Header().Set("Content-Type", "application/json") + http.Error(w, `{"error":"Service not available","message":"ChatUI proxy not configured"}`, http.StatusBadGateway) + return + } log.Printf("Proxying Chat UI Next.js asset: %s", r.URL.Path) chatUIProxy.ServeHTTP(w, r) }) @@ -131,24 +176,44 @@ func Setup(cfg *config.Config) *http.ServeMux { if middleware.HandleCORSPreflight(w, r) { return } + if chatUIProxy == nil { + w.Header().Set("Content-Type", "application/json") + http.Error(w, `{"error":"Service not available","message":"ChatUI proxy not configured"}`, http.StatusBadGateway) + return + } chatUIProxy.ServeHTTP(w, r) }) mux.HandleFunc("/manifest.webmanifest", func(w http.ResponseWriter, r *http.Request) { if middleware.HandleCORSPreflight(w, r) { return } + if chatUIProxy == nil { + w.Header().Set("Content-Type", "application/json") + http.Error(w, `{"error":"Service not available","message":"ChatUI proxy not configured"}`, http.StatusBadGateway) + return + } chatUIProxy.ServeHTTP(w, r) }) mux.HandleFunc("/manifest.json", func(w http.ResponseWriter, r *http.Request) { if middleware.HandleCORSPreflight(w, r) { return } + if chatUIProxy == nil { + w.Header().Set("Content-Type", "application/json") + http.Error(w, `{"error":"Service not available","message":"ChatUI proxy not configured"}`, http.StatusBadGateway) + return + } chatUIProxy.ServeHTTP(w, r) }) mux.HandleFunc("/robots.txt", func(w http.ResponseWriter, r *http.Request) { if middleware.HandleCORSPreflight(w, r) { return } + if chatUIProxy == nil { + w.Header().Set("Content-Type", "application/json") + http.Error(w, `{"error":"Service not available","message":"ChatUI proxy not configured"}`, http.StatusBadGateway) + return + } chatUIProxy.ServeHTTP(w, r) }) @@ -158,6 +223,11 @@ func Setup(cfg *config.Config) *http.ServeMux { if middleware.HandleCORSPreflight(w, r) { return } + if chatUIProxy == nil { + w.Header().Set("Content-Type", "application/json") + http.Error(w, `{"error":"Service not available","message":"ChatUI proxy not configured"}`, http.StatusBadGateway) + return + } log.Printf("Proxying Chat UI conversation API: %s %s", r.Method, r.URL.Path) chatUIProxy.ServeHTTP(w, r) }) @@ -166,6 +236,11 @@ func Setup(cfg *config.Config) *http.ServeMux { if middleware.HandleCORSPreflight(w, r) { return } + if chatUIProxy == nil { + w.Header().Set("Content-Type", "application/json") + http.Error(w, `{"error":"Service not available","message":"ChatUI proxy not configured"}`, http.StatusBadGateway) + return + } log.Printf("Proxying Chat UI conversation API: %s %s", r.Method, r.URL.Path) chatUIProxy.ServeHTTP(w, r) }) @@ -173,6 +248,11 @@ func Setup(cfg *config.Config) *http.ServeMux { if middleware.HandleCORSPreflight(w, r) { return } + if chatUIProxy == nil { + w.Header().Set("Content-Type", "application/json") + http.Error(w, `{"error":"Service not available","message":"ChatUI proxy not configured"}`, http.StatusBadGateway) + return + } log.Printf("Proxying Chat UI conversations API: %s %s", r.Method, r.URL.Path) chatUIProxy.ServeHTTP(w, r) }) @@ -180,6 +260,11 @@ func Setup(cfg *config.Config) *http.ServeMux { if middleware.HandleCORSPreflight(w, r) { return } + if chatUIProxy == nil { + w.Header().Set("Content-Type", "application/json") + http.Error(w, `{"error":"Service not available","message":"ChatUI proxy not configured"}`, http.StatusBadGateway) + return + } log.Printf("Proxying Chat UI conversations API: %s %s", r.Method, r.URL.Path) chatUIProxy.ServeHTTP(w, r) }) @@ -187,6 +272,11 @@ func Setup(cfg *config.Config) *http.ServeMux { if middleware.HandleCORSPreflight(w, r) { return } + if chatUIProxy == nil { + w.Header().Set("Content-Type", "application/json") + http.Error(w, `{"error":"Service not available","message":"ChatUI proxy not configured"}`, http.StatusBadGateway) + return + } log.Printf("Proxying Chat UI settings: %s %s", r.Method, r.URL.Path) chatUIProxy.ServeHTTP(w, r) }) @@ -194,6 +284,11 @@ func Setup(cfg *config.Config) *http.ServeMux { if middleware.HandleCORSPreflight(w, r) { return } + if chatUIProxy == nil { + w.Header().Set("Content-Type", "application/json") + http.Error(w, `{"error":"Service not available","message":"ChatUI proxy not configured"}`, http.StatusBadGateway) + return + } log.Printf("Proxying Chat UI settings: %s %s", r.Method, r.URL.Path) chatUIProxy.ServeHTTP(w, r) }) @@ -219,6 +314,11 @@ func Setup(cfg *config.Config) *http.ServeMux { grafanaStaticProxy.ServeHTTP(w, r) return } + if chatUIProxy == nil { + w.Header().Set("Content-Type", "application/json") + http.Error(w, `{"error":"Service not available","message":"ChatUI proxy not configured"}`, http.StatusBadGateway) + return + } log.Printf("Proxying Chat UI login: %s %s (contentType=%s)", r.Method, r.URL.Path, contentType) chatUIProxy.ServeHTTP(w, r) }) @@ -226,6 +326,11 @@ func Setup(cfg *config.Config) *http.ServeMux { if middleware.HandleCORSPreflight(w, r) { return } + if chatUIProxy == nil { + w.Header().Set("Content-Type", "application/json") + http.Error(w, `{"error":"Service not available","message":"ChatUI proxy not configured"}`, http.StatusBadGateway) + return + } log.Printf("Proxying Chat UI logout: %s %s", r.Method, r.URL.Path) chatUIProxy.ServeHTTP(w, r) }) @@ -234,6 +339,11 @@ func Setup(cfg *config.Config) *http.ServeMux { if middleware.HandleCORSPreflight(w, r) { return } + if chatUIProxy == nil { + w.Header().Set("Content-Type", "application/json") + http.Error(w, `{"error":"Service not available","message":"ChatUI proxy not configured"}`, http.StatusBadGateway) + return + } log.Printf("Proxying Chat UI shared conversation: %s %s", r.Method, r.URL.Path) chatUIProxy.ServeHTTP(w, r) }) @@ -242,6 +352,11 @@ func Setup(cfg *config.Config) *http.ServeMux { if middleware.HandleCORSPreflight(w, r) { return } + if chatUIProxy == nil { + w.Header().Set("Content-Type", "application/json") + http.Error(w, `{"error":"Service not available","message":"ChatUI proxy not configured"}`, http.StatusBadGateway) + return + } log.Printf("Proxying Chat UI assets: %s", r.URL.Path) chatUIProxy.ServeHTTP(w, r) }) @@ -253,7 +368,7 @@ func Setup(cfg *config.Config) *http.ServeMux { mux.HandleFunc("/embedded/chatui/", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusServiceUnavailable) - w.Write([]byte(`{"error":"HuggingChat not configured","message":"TARGET_CHATUI_URL environment variable is not set"}`)) + _, _ = w.Write([]byte(`{"error":"HuggingChat not configured","message":"TARGET_CHATUI_URL environment variable is not set"}`)) }) log.Printf("Info: HuggingChat not configured (optional)") } @@ -273,14 +388,29 @@ func Setup(cfg *config.Config) *http.ServeMux { routerAPIProxy.ServeHTTP(w, r) return } - // If path is Jaeger API (services, traces, operations, etc.), use Jaeger proxy + // If path is Jaeger API (services, traces, operations, dependencies, etc.), use Jaeger proxy if jaegerAPIProxy != nil && (strings.HasPrefix(r.URL.Path, "/api/services") || strings.HasPrefix(r.URL.Path, "/api/traces") || - strings.HasPrefix(r.URL.Path, "/api/operations")) { + strings.HasPrefix(r.URL.Path, "/api/operations") || + strings.HasPrefix(r.URL.Path, "/api/dependencies")) { log.Printf("Routing to Jaeger API: %s", r.URL.Path) jaegerAPIProxy.ServeHTTP(w, r) return } + // Check if request is from OpenWebUI (by referer) and route to OpenWebUI API + referer := r.Header.Get("Referer") + if openwebuiStaticProxy != nil && referer != "" && strings.Contains(referer, "/embedded/openwebui") { + log.Printf("Routing to OpenWebUI API: %s (referer: %s)", r.URL.Path, referer) + openwebuiStaticProxy.ServeHTTP(w, r) + return + } + // Check if path is a known OpenWebUI API endpoint (even without referer) + // OpenWebUI uses /api/config for configuration + if openwebuiStaticProxy != nil && strings.HasPrefix(r.URL.Path, "/api/config") { + log.Printf("Routing to OpenWebUI API: %s (by path pattern)", r.URL.Path) + openwebuiStaticProxy.ServeHTTP(w, r) + return + } // Prefer Chat UI API when available (to avoid returning HTML from other backends) if chatUIProxy != nil { log.Printf("Routing to Chat UI API: %s", r.URL.Path) @@ -327,7 +457,7 @@ func Setup(cfg *config.Config) *http.ServeMux { mux.HandleFunc("/embedded/prometheus/", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusServiceUnavailable) - w.Write([]byte(`{"error":"Prometheus not configured","message":"TARGET_PROMETHEUS_URL environment variable is not set"}`)) + _, _ = w.Write([]byte(`{"error":"Prometheus not configured","message":"TARGET_PROMETHEUS_URL environment variable is not set"}`)) }) log.Printf("Warning: Prometheus URL not configured") } @@ -353,21 +483,31 @@ func Setup(cfg *config.Config) *http.ServeMux { }) // Jaeger static assets are typically served under /static/* from the same origin - // Provide a passthrough proxy without prefix stripping - jStatic, _ := proxy.NewReverseProxy(cfg.JaegerURL, "", false) - mux.Handle("/static/", jStatic) + // Note: /static/ is shared with OpenWebUI, so we handle it in OpenWebUI section with referer-based routing + + // Jaeger /dependencies page (accessible directly, not under /embedded/jaeger) + // Use the pre-created jaegerStaticProxy + if jaegerStaticProxy != nil { + mux.HandleFunc("/dependencies", func(w http.ResponseWriter, r *http.Request) { + if middleware.HandleCORSPreflight(w, r) { + return + } + log.Printf("Proxying Jaeger dependencies page: %s", r.URL.Path) + jaegerStaticProxy.ServeHTTP(w, r) + }) + } - log.Printf("Jaeger proxy configured: %s; static assets proxied at /static/", cfg.JaegerURL) + log.Printf("Jaeger proxy configured: %s; static assets proxied at /static/, /dependencies", cfg.JaegerURL) } else { mux.HandleFunc("/embedded/jaeger/", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusServiceUnavailable) - w.Write([]byte(`{"error":"Jaeger not configured","message":"TARGET_JAEGER_URL environment variable is not set"}`)) + _, _ = w.Write([]byte(`{"error":"Jaeger not configured","message":"TARGET_JAEGER_URL environment variable is not set"}`)) }) log.Printf("Info: Jaeger URL not configured (optional)") } - // Open WebUI proxy (optional) + // Open WebUI proxy (optional) - MUST handle /static/ with referer-based routing for Jaeger compatibility if cfg.OpenWebUIURL != "" { op, err := proxy.NewReverseProxy(cfg.OpenWebUIURL, "/embedded/openwebui", true) if err != nil { @@ -385,12 +525,75 @@ func Setup(cfg *config.Config) *http.ServeMux { } op.ServeHTTP(w, r) }) + + // Static assets for OpenWebUI and Jaeger - route based on referer + mux.HandleFunc("/static/", func(w http.ResponseWriter, r *http.Request) { + if middleware.HandleCORSPreflight(w, r) { + return + } + // Check referer to determine if request is from Jaeger or OpenWebUI + referer := r.Header.Get("Referer") + if referer != "" && strings.Contains(referer, "/embedded/jaeger") { + // Route to Jaeger if referer indicates Jaeger + if jaegerStaticProxy != nil { + log.Printf("Proxying Jaeger /static/ asset: %s (referer: %s)", r.URL.Path, referer) + jaegerStaticProxy.ServeHTTP(w, r) + return + } + } + // Default to OpenWebUI when it's configured + if openwebuiStaticProxy != nil { + log.Printf("Proxying OpenWebUI /static/ asset: %s (referer: %s)", r.URL.Path, referer) + openwebuiStaticProxy.ServeHTTP(w, r) + } else { + w.Header().Set("Content-Type", "application/json") + http.Error(w, `{"error":"Service not available","message":"OpenWebUI static proxy not configured"}`, http.StatusBadGateway) + } + }) + + // OpenWebUI also uses /_app/ for its main JS/CSS bundles + mux.HandleFunc("/_app/", func(w http.ResponseWriter, r *http.Request) { + if middleware.HandleCORSPreflight(w, r) { + return + } + // Check Referer to determine if request is from OpenWebUI or ChatUI + referer := r.Header.Get("Referer") + isOpenWebUIRequest := referer != "" && strings.Contains(referer, "/embedded/openwebui") + isChatUIRequest := referer != "" && strings.Contains(referer, "/embedded/chatui") + + // If referer indicates OpenWebUI, route to OpenWebUI + if isOpenWebUIRequest && openwebuiStaticProxy != nil { + log.Printf("Proxying OpenWebUI /_app/ asset: %s (referer: %s)", r.URL.Path, referer) + openwebuiStaticProxy.ServeHTTP(w, r) + return + } + // If referer indicates ChatUI, route to ChatUI (if configured) + if isChatUIRequest && chatUIProxy != nil { + log.Printf("Proxying Chat UI /_app/ asset: %s (referer: %s)", r.URL.Path, referer) + chatUIProxy.ServeHTTP(w, r) + return + } + // If no referer or unclear, try OpenWebUI first (since it's configured) + if openwebuiStaticProxy != nil { + log.Printf("Proxying /_app/ asset to OpenWebUI (no clear referer): %s (referer: %s)", r.URL.Path, referer) + openwebuiStaticProxy.ServeHTTP(w, r) + } else { + w.Header().Set("Content-Type", "application/json") + http.Error(w, `{"error":"Service not available","message":"No handler available for /_app/"}`, http.StatusBadGateway) + } + }) + log.Printf("Open WebUI proxy configured: %s", cfg.OpenWebUIURL) + if openwebuiStaticProxy != nil { + log.Printf("Open WebUI static assets proxied at: /static/, /_app/") + } else { + log.Printf("Open WebUI static assets proxy failed to initialize") + } } else { mux.HandleFunc("/embedded/openwebui/", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusServiceUnavailable) - w.Write([]byte(`{"error":"Open WebUI not configured","message":"TARGET_OPENWEBUI_URL environment variable is not set or empty"}`)) + _, _ = w.Write([]byte(`{"error":"Open WebUI not configured","message":"TARGET_OPENWEBUI_URL environment variable is not set or empty"}`)) }) log.Printf("Info: Open WebUI not configured (optional)") } diff --git a/dashboard/frontend/eslint.config.js b/dashboard/frontend/eslint.config.js new file mode 100644 index 0000000000..231b64129c --- /dev/null +++ b/dashboard/frontend/eslint.config.js @@ -0,0 +1,36 @@ +import js from "@eslint/js"; +import globals from "globals"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; +import tseslint from "typescript-eslint"; + +export default tseslint.config( + { ignores: ["dist", "node_modules"] }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ["**/*.{ts,tsx}"], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + "react-hooks": reactHooks, + "react-refresh": reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + "react-refresh/only-export-components": [ + "warn", + { allowConstantExport: true }, + ], + "@typescript-eslint/no-unused-vars": [ + "error", + { argsIgnorePattern: "^_" }, + ], + // Temporarily relaxed rules for existing code - can be tightened later + "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/ban-ts-comment": "warn", + "no-useless-escape": "warn", + }, + } +); diff --git a/dashboard/frontend/package-lock.json b/dashboard/frontend/package-lock.json index 4518f1eb8c..7e124e60fe 100644 --- a/dashboard/frontend/package-lock.json +++ b/dashboard/frontend/package-lock.json @@ -14,6 +14,7 @@ "reactflow": "^11.11.4" }, "devDependencies": { + "@eslint/js": "^9.18.0", "@types/react": "^18.3.18", "@types/react-dom": "^18.3.5", "@typescript-eslint/eslint-plugin": "^8.45.0", @@ -22,7 +23,9 @@ "eslint": "^9.18.0", "eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-react-refresh": "^0.4.16", + "globals": "^16.1.0", "typescript": "^5.9.3", + "typescript-eslint": "^8.45.0", "vite": "^5.4.11" } }, @@ -42,9 +45,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", - "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", "dev": true, "license": "MIT", "engines": { @@ -52,21 +55,21 @@ } }, "node_modules/@babel/core": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", - "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.3", + "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", - "@babel/parser": "^7.28.4", + "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.4", - "@babel/types": "^7.28.4", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -93,14 +96,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", - "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.3", - "@babel/types": "^7.28.2", + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -199,9 +202,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "dev": true, "license": "MIT", "engines": { @@ -233,13 +236,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", - "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.4" + "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" @@ -296,18 +299,18 @@ } }, "node_modules/@babel/traverse": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", - "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.3", + "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.4", + "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4", + "@babel/types": "^7.28.5", "debug": "^4.3.1" }, "engines": { @@ -315,14 +318,14 @@ } }, "node_modules/@babel/types": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", - "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -739,9 +742,9 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, "license": "MIT", "engines": { @@ -749,13 +752,13 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", - "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.6", + "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" }, @@ -788,22 +791,22 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.0.tgz", - "integrity": "sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.16.0" + "@eslint/core": "^0.17.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/core": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz", - "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==", + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -814,9 +817,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", "dev": true, "license": "MIT", "dependencies": { @@ -826,7 +829,7 @@ "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", + "js-yaml": "^4.1.1", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" }, @@ -848,6 +851,19 @@ "concat-map": "0.0.1" } }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@eslint/eslintrc/node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -872,9 +888,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.37.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.37.0.tgz", - "integrity": "sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==", + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", "dev": true, "license": "MIT", "engines": { @@ -885,9 +901,9 @@ } }, "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -895,13 +911,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz", - "integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.16.0", + "@eslint/core": "^0.17.0", "levn": "^0.4.1" }, "engines": { @@ -1010,47 +1026,9 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/@reactflow/background": { "version": "11.3.14", - "resolved": "https://mirrors.tencent.com/npm/@reactflow/background/-/background-11.3.14.tgz", + "resolved": "https://registry.npmjs.org/@reactflow/background/-/background-11.3.14.tgz", "integrity": "sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA==", "license": "MIT", "dependencies": { @@ -1065,7 +1043,7 @@ }, "node_modules/@reactflow/controls": { "version": "11.2.14", - "resolved": "https://mirrors.tencent.com/npm/@reactflow/controls/-/controls-11.2.14.tgz", + "resolved": "https://registry.npmjs.org/@reactflow/controls/-/controls-11.2.14.tgz", "integrity": "sha512-MiJp5VldFD7FrqaBNIrQ85dxChrG6ivuZ+dcFhPQUwOK3HfYgX2RHdBua+gx+40p5Vw5It3dVNp/my4Z3jF0dw==", "license": "MIT", "dependencies": { @@ -1080,7 +1058,7 @@ }, "node_modules/@reactflow/core": { "version": "11.11.4", - "resolved": "https://mirrors.tencent.com/npm/@reactflow/core/-/core-11.11.4.tgz", + "resolved": "https://registry.npmjs.org/@reactflow/core/-/core-11.11.4.tgz", "integrity": "sha512-H4vODklsjAq3AMq6Np4LE12i1I4Ta9PrDHuBR9GmL8uzTt2l2jh4CiQbEMpvMDcp7xi4be0hgXj+Ysodde/i7Q==", "license": "MIT", "dependencies": { @@ -1101,7 +1079,7 @@ }, "node_modules/@reactflow/minimap": { "version": "11.7.14", - "resolved": "https://mirrors.tencent.com/npm/@reactflow/minimap/-/minimap-11.7.14.tgz", + "resolved": "https://registry.npmjs.org/@reactflow/minimap/-/minimap-11.7.14.tgz", "integrity": "sha512-mpwLKKrEAofgFJdkhwR5UQ1JYWlcAAL/ZU/bctBkuNTT1yqV+y0buoNVImsRehVYhJwffSWeSHaBR5/GJjlCSQ==", "license": "MIT", "dependencies": { @@ -1120,7 +1098,7 @@ }, "node_modules/@reactflow/node-resizer": { "version": "2.2.14", - "resolved": "https://mirrors.tencent.com/npm/@reactflow/node-resizer/-/node-resizer-2.2.14.tgz", + "resolved": "https://registry.npmjs.org/@reactflow/node-resizer/-/node-resizer-2.2.14.tgz", "integrity": "sha512-fwqnks83jUlYr6OHcdFEedumWKChTHRGw/kbCxj0oqBd+ekfs+SIp4ddyNU0pdx96JIm5iNFS0oNrmEiJbbSaA==", "license": "MIT", "dependencies": { @@ -1137,7 +1115,7 @@ }, "node_modules/@reactflow/node-toolbar": { "version": "1.3.14", - "resolved": "https://mirrors.tencent.com/npm/@reactflow/node-toolbar/-/node-toolbar-1.3.14.tgz", + "resolved": "https://registry.npmjs.org/@reactflow/node-toolbar/-/node-toolbar-1.3.14.tgz", "integrity": "sha512-rbynXQnH/xFNu4P9H+hVqlEUafDCkEoCy0Dg9mG22Sg+rY/0ck6KkrAQrYrTgXusd+cEJOMK0uOOFCK2/5rSGQ==", "license": "MIT", "dependencies": { @@ -1151,9 +1129,9 @@ } }, "node_modules/@remix-run/router": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", - "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==", + "version": "1.23.1", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.1.tgz", + "integrity": "sha512-vDbaOzF7yT2Qs4vO6XV1MHcJv+3dgR1sT+l3B8xxOVhUC336prMvqrvsLL/9Dnw2xr6Qhz4J0dmS0llNAbnUmQ==", "license": "MIT", "engines": { "node": ">=14.0.0" @@ -1167,9 +1145,9 @@ "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.4.tgz", - "integrity": "sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.5.tgz", + "integrity": "sha512-iDGS/h7D8t7tvZ1t6+WPK04KD0MwzLZrG0se1hzBjSi5fyxlsiggoJHwh18PCFNn7tG43OWb6pdZ6Y+rMlmyNQ==", "cpu": [ "arm" ], @@ -1181,9 +1159,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.4.tgz", - "integrity": "sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.5.tgz", + "integrity": "sha512-wrSAViWvZHBMMlWk6EJhvg8/rjxzyEhEdgfMMjREHEq11EtJ6IP6yfcCH57YAEca2Oe3FNCE9DSTgU70EIGmVw==", "cpu": [ "arm64" ], @@ -1195,9 +1173,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.4.tgz", - "integrity": "sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.5.tgz", + "integrity": "sha512-S87zZPBmRO6u1YXQLwpveZm4JfPpAa6oHBX7/ghSiGH3rz/KDgAu1rKdGutV+WUI6tKDMbaBJomhnT30Y2t4VQ==", "cpu": [ "arm64" ], @@ -1209,9 +1187,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.4.tgz", - "integrity": "sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.5.tgz", + "integrity": "sha512-YTbnsAaHo6VrAczISxgpTva8EkfQus0VPEVJCEaboHtZRIb6h6j0BNxRBOwnDciFTZLDPW5r+ZBmhL/+YpTZgA==", "cpu": [ "x64" ], @@ -1223,9 +1201,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.4.tgz", - "integrity": "sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.5.tgz", + "integrity": "sha512-1T8eY2J8rKJWzaznV7zedfdhD1BqVs1iqILhmHDq/bqCUZsrMt+j8VCTHhP0vdfbHK3e1IQ7VYx3jlKqwlf+vw==", "cpu": [ "arm64" ], @@ -1237,9 +1215,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.4.tgz", - "integrity": "sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.5.tgz", + "integrity": "sha512-sHTiuXyBJApxRn+VFMaw1U+Qsz4kcNlxQ742snICYPrY+DDL8/ZbaC4DVIB7vgZmp3jiDaKA0WpBdP0aqPJoBQ==", "cpu": [ "x64" ], @@ -1251,9 +1229,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.4.tgz", - "integrity": "sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.5.tgz", + "integrity": "sha512-dV3T9MyAf0w8zPVLVBptVlzaXxka6xg1f16VAQmjg+4KMSTWDvhimI/Y6mp8oHwNrmnmVl9XxJ/w/mO4uIQONA==", "cpu": [ "arm" ], @@ -1265,9 +1243,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.4.tgz", - "integrity": "sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.5.tgz", + "integrity": "sha512-wIGYC1x/hyjP+KAu9+ewDI+fi5XSNiUi9Bvg6KGAh2TsNMA3tSEs+Sh6jJ/r4BV/bx/CyWu2ue9kDnIdRyafcQ==", "cpu": [ "arm" ], @@ -1279,9 +1257,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.4.tgz", - "integrity": "sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.5.tgz", + "integrity": "sha512-Y+qVA0D9d0y2FRNiG9oM3Hut/DgODZbU9I8pLLPwAsU0tUKZ49cyV1tzmB/qRbSzGvY8lpgGkJuMyuhH7Ma+Vg==", "cpu": [ "arm64" ], @@ -1293,9 +1271,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.4.tgz", - "integrity": "sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.5.tgz", + "integrity": "sha512-juaC4bEgJsyFVfqhtGLz8mbopaWD+WeSOYr5E16y+1of6KQjc0BpwZLuxkClqY1i8sco+MdyoXPNiCkQou09+g==", "cpu": [ "arm64" ], @@ -1307,9 +1285,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.4.tgz", - "integrity": "sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.5.tgz", + "integrity": "sha512-rIEC0hZ17A42iXtHX+EPJVL/CakHo+tT7W0pbzdAGuWOt2jxDFh7A/lRhsNHBcqL4T36+UiAgwO8pbmn3dE8wA==", "cpu": [ "loong64" ], @@ -1321,9 +1299,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.4.tgz", - "integrity": "sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.5.tgz", + "integrity": "sha512-T7l409NhUE552RcAOcmJHj3xyZ2h7vMWzcwQI0hvn5tqHh3oSoclf9WgTl+0QqffWFG8MEVZZP1/OBglKZx52Q==", "cpu": [ "ppc64" ], @@ -1335,9 +1313,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.4.tgz", - "integrity": "sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.5.tgz", + "integrity": "sha512-7OK5/GhxbnrMcxIFoYfhV/TkknarkYC1hqUw1wU2xUN3TVRLNT5FmBv4KkheSG2xZ6IEbRAhTooTV2+R5Tk0lQ==", "cpu": [ "riscv64" ], @@ -1349,9 +1327,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.4.tgz", - "integrity": "sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.5.tgz", + "integrity": "sha512-GwuDBE/PsXaTa76lO5eLJTyr2k8QkPipAyOrs4V/KJufHCZBJ495VCGJol35grx9xryk4V+2zd3Ri+3v7NPh+w==", "cpu": [ "riscv64" ], @@ -1363,9 +1341,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.4.tgz", - "integrity": "sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.5.tgz", + "integrity": "sha512-IAE1Ziyr1qNfnmiQLHBURAD+eh/zH1pIeJjeShleII7Vj8kyEm2PF77o+lf3WTHDpNJcu4IXJxNO0Zluro8bOw==", "cpu": [ "s390x" ], @@ -1377,9 +1355,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.4.tgz", - "integrity": "sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.5.tgz", + "integrity": "sha512-Pg6E+oP7GvZ4XwgRJBuSXZjcqpIW3yCBhK4BcsANvb47qMvAbCjR6E+1a/U2WXz1JJxp9/4Dno3/iSJLcm5auw==", "cpu": [ "x64" ], @@ -1391,9 +1369,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.4.tgz", - "integrity": "sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.5.tgz", + "integrity": "sha512-txGtluxDKTxaMDzUduGP0wdfng24y1rygUMnmlUJ88fzCCULCLn7oE5kb2+tRB+MWq1QDZT6ObT5RrR8HFRKqg==", "cpu": [ "x64" ], @@ -1405,9 +1383,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.4.tgz", - "integrity": "sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.5.tgz", + "integrity": "sha512-3DFiLPnTxiOQV993fMc+KO8zXHTcIjgaInrqlG8zDp1TlhYl6WgrOHuJkJQ6M8zHEcntSJsUp1XFZSY8C1DYbg==", "cpu": [ "arm64" ], @@ -1419,9 +1397,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.4.tgz", - "integrity": "sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.5.tgz", + "integrity": "sha512-nggc/wPpNTgjGg75hu+Q/3i32R00Lq1B6N1DO7MCU340MRKL3WZJMjA9U4K4gzy3dkZPXm9E1Nc81FItBVGRlA==", "cpu": [ "arm64" ], @@ -1433,9 +1411,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.4.tgz", - "integrity": "sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.5.tgz", + "integrity": "sha512-U/54pTbdQpPLBdEzCT6NBCFAfSZMvmjr0twhnD9f4EIvlm9wy3jjQ38yQj1AGznrNO65EWQMgm/QUjuIVrYF9w==", "cpu": [ "ia32" ], @@ -1447,9 +1425,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.4.tgz", - "integrity": "sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.5.tgz", + "integrity": "sha512-2NqKgZSuLH9SXBBV2dWNRCZmocgSOx8OJSdpRaEcRlIfX8YrKxUT6z0F1NpvDVhOsl190UFTRh2F2WDWWCYp3A==", "cpu": [ "x64" ], @@ -1461,9 +1439,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.4.tgz", - "integrity": "sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.5.tgz", + "integrity": "sha512-JRpZUhCfhZ4keB5v0fe02gQJy05GqboPOaxvjugW04RLSYYoB/9t2lx2u/tMs/Na/1NXfY8QYjgRljRpN+MjTQ==", "cpu": [ "x64" ], @@ -1521,7 +1499,7 @@ }, "node_modules/@types/d3": { "version": "7.4.3", - "resolved": "https://mirrors.tencent.com/npm/@types/d3/-/d3-7.4.3.tgz", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", "license": "MIT", "dependencies": { @@ -1559,13 +1537,13 @@ }, "node_modules/@types/d3-array": { "version": "3.2.2", - "resolved": "https://mirrors.tencent.com/npm/@types/d3-array/-/d3-array-3.2.2.tgz", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", "license": "MIT" }, "node_modules/@types/d3-axis": { "version": "3.0.6", - "resolved": "https://mirrors.tencent.com/npm/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", "license": "MIT", "dependencies": { @@ -1574,7 +1552,7 @@ }, "node_modules/@types/d3-brush": { "version": "3.0.6", - "resolved": "https://mirrors.tencent.com/npm/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", "license": "MIT", "dependencies": { @@ -1583,19 +1561,19 @@ }, "node_modules/@types/d3-chord": { "version": "3.0.6", - "resolved": "https://mirrors.tencent.com/npm/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", "license": "MIT" }, "node_modules/@types/d3-color": { "version": "3.1.3", - "resolved": "https://mirrors.tencent.com/npm/@types/d3-color/-/d3-color-3.1.3.tgz", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", "license": "MIT" }, "node_modules/@types/d3-contour": { "version": "3.0.6", - "resolved": "https://mirrors.tencent.com/npm/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", "license": "MIT", "dependencies": { @@ -1605,19 +1583,19 @@ }, "node_modules/@types/d3-delaunay": { "version": "6.0.4", - "resolved": "https://mirrors.tencent.com/npm/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", "license": "MIT" }, "node_modules/@types/d3-dispatch": { "version": "3.0.7", - "resolved": "https://mirrors.tencent.com/npm/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", "license": "MIT" }, "node_modules/@types/d3-drag": { "version": "3.0.7", - "resolved": "https://mirrors.tencent.com/npm/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", "license": "MIT", "dependencies": { @@ -1626,19 +1604,19 @@ }, "node_modules/@types/d3-dsv": { "version": "3.0.7", - "resolved": "https://mirrors.tencent.com/npm/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", "license": "MIT" }, "node_modules/@types/d3-ease": { "version": "3.0.2", - "resolved": "https://mirrors.tencent.com/npm/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", "license": "MIT" }, "node_modules/@types/d3-fetch": { "version": "3.0.7", - "resolved": "https://mirrors.tencent.com/npm/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", "license": "MIT", "dependencies": { @@ -1647,19 +1625,19 @@ }, "node_modules/@types/d3-force": { "version": "3.0.10", - "resolved": "https://mirrors.tencent.com/npm/@types/d3-force/-/d3-force-3.0.10.tgz", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", "license": "MIT" }, "node_modules/@types/d3-format": { "version": "3.0.4", - "resolved": "https://mirrors.tencent.com/npm/@types/d3-format/-/d3-format-3.0.4.tgz", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", "license": "MIT" }, "node_modules/@types/d3-geo": { "version": "3.1.0", - "resolved": "https://mirrors.tencent.com/npm/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", "license": "MIT", "dependencies": { @@ -1668,13 +1646,13 @@ }, "node_modules/@types/d3-hierarchy": { "version": "3.1.7", - "resolved": "https://mirrors.tencent.com/npm/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", "license": "MIT" }, "node_modules/@types/d3-interpolate": { "version": "3.0.4", - "resolved": "https://mirrors.tencent.com/npm/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", "license": "MIT", "dependencies": { @@ -1683,31 +1661,31 @@ }, "node_modules/@types/d3-path": { "version": "3.1.1", - "resolved": "https://mirrors.tencent.com/npm/@types/d3-path/-/d3-path-3.1.1.tgz", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", "license": "MIT" }, "node_modules/@types/d3-polygon": { "version": "3.0.2", - "resolved": "https://mirrors.tencent.com/npm/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", "license": "MIT" }, "node_modules/@types/d3-quadtree": { "version": "3.0.6", - "resolved": "https://mirrors.tencent.com/npm/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", "license": "MIT" }, "node_modules/@types/d3-random": { "version": "3.0.3", - "resolved": "https://mirrors.tencent.com/npm/@types/d3-random/-/d3-random-3.0.3.tgz", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", "license": "MIT" }, "node_modules/@types/d3-scale": { "version": "4.0.9", - "resolved": "https://mirrors.tencent.com/npm/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", "license": "MIT", "dependencies": { @@ -1716,19 +1694,19 @@ }, "node_modules/@types/d3-scale-chromatic": { "version": "3.1.0", - "resolved": "https://mirrors.tencent.com/npm/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", "license": "MIT" }, "node_modules/@types/d3-selection": { "version": "3.0.11", - "resolved": "https://mirrors.tencent.com/npm/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", "license": "MIT" }, "node_modules/@types/d3-shape": { "version": "3.1.7", - "resolved": "https://mirrors.tencent.com/npm/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", "license": "MIT", "dependencies": { @@ -1737,25 +1715,25 @@ }, "node_modules/@types/d3-time": { "version": "3.0.4", - "resolved": "https://mirrors.tencent.com/npm/@types/d3-time/-/d3-time-3.0.4.tgz", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", "license": "MIT" }, "node_modules/@types/d3-time-format": { "version": "4.0.3", - "resolved": "https://mirrors.tencent.com/npm/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", "license": "MIT" }, "node_modules/@types/d3-timer": { "version": "3.0.2", - "resolved": "https://mirrors.tencent.com/npm/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", "license": "MIT" }, "node_modules/@types/d3-transition": { "version": "3.0.9", - "resolved": "https://mirrors.tencent.com/npm/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", "license": "MIT", "dependencies": { @@ -1764,7 +1742,7 @@ }, "node_modules/@types/d3-zoom": { "version": "3.0.8", - "resolved": "https://mirrors.tencent.com/npm/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", "license": "MIT", "dependencies": { @@ -1781,7 +1759,7 @@ }, "node_modules/@types/geojson": { "version": "7946.0.16", - "resolved": "https://mirrors.tencent.com/npm/@types/geojson/-/geojson-7946.0.16.tgz", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", "license": "MIT" }, @@ -1800,14 +1778,14 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "18.3.26", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.26.tgz", - "integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==", + "version": "18.3.27", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", + "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", "devOptional": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", - "csstype": "^3.0.2" + "csstype": "^3.2.2" } }, "node_modules/@types/react-dom": { @@ -1821,18 +1799,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.46.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.0.tgz", - "integrity": "sha512-hA8gxBq4ukonVXPy0OKhiaUh/68D0E88GSmtC1iAEnGaieuDi38LhS7jdCHRLi6ErJBNDGCzvh5EnzdPwUc0DA==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.50.0.tgz", + "integrity": "sha512-O7QnmOXYKVtPrfYzMolrCTfkezCJS9+ljLdKW/+DCvRsc3UAz+sbH6Xcsv7p30+0OwUbeWfUDAQE0vpabZ3QLg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.46.0", - "@typescript-eslint/type-utils": "8.46.0", - "@typescript-eslint/utils": "8.46.0", - "@typescript-eslint/visitor-keys": "8.46.0", - "graphemer": "^1.4.0", + "@typescript-eslint/scope-manager": "8.50.0", + "@typescript-eslint/type-utils": "8.50.0", + "@typescript-eslint/utils": "8.50.0", + "@typescript-eslint/visitor-keys": "8.50.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" @@ -1845,22 +1822,22 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.46.0", + "@typescript-eslint/parser": "^8.50.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.46.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.0.tgz", - "integrity": "sha512-n1H6IcDhmmUEG7TNVSspGmiHHutt7iVKtZwRppD7e04wha5MrkV1h3pti9xQLcCMt6YWsncpoT0HMjkH1FNwWQ==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.50.0.tgz", + "integrity": "sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.46.0", - "@typescript-eslint/types": "8.46.0", - "@typescript-eslint/typescript-estree": "8.46.0", - "@typescript-eslint/visitor-keys": "8.46.0", + "@typescript-eslint/scope-manager": "8.50.0", + "@typescript-eslint/types": "8.50.0", + "@typescript-eslint/typescript-estree": "8.50.0", + "@typescript-eslint/visitor-keys": "8.50.0", "debug": "^4.3.4" }, "engines": { @@ -1876,14 +1853,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.46.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.0.tgz", - "integrity": "sha512-OEhec0mH+U5Je2NZOeK1AbVCdm0ChyapAyTeXVIYTPXDJ3F07+cu87PPXcGoYqZ7M9YJVvFnfpGg1UmCIqM+QQ==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.50.0.tgz", + "integrity": "sha512-Cg/nQcL1BcoTijEWyx4mkVC56r8dj44bFDvBdygifuS20f3OZCHmFbjF34DPSi07kwlFvqfv/xOLnJ5DquxSGQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.46.0", - "@typescript-eslint/types": "^8.46.0", + "@typescript-eslint/tsconfig-utils": "^8.50.0", + "@typescript-eslint/types": "^8.50.0", "debug": "^4.3.4" }, "engines": { @@ -1898,14 +1875,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.46.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.0.tgz", - "integrity": "sha512-lWETPa9XGcBes4jqAMYD9fW0j4n6hrPtTJwWDmtqgFO/4HF4jmdH/Q6wggTw5qIT5TXjKzbt7GsZUBnWoO3dqw==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.50.0.tgz", + "integrity": "sha512-xCwfuCZjhIqy7+HKxBLrDVT5q/iq7XBVBXLn57RTIIpelLtEIZHXAF/Upa3+gaCpeV1NNS5Z9A+ID6jn50VD4A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.0", - "@typescript-eslint/visitor-keys": "8.46.0" + "@typescript-eslint/types": "8.50.0", + "@typescript-eslint/visitor-keys": "8.50.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1916,9 +1893,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.46.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.0.tgz", - "integrity": "sha512-WrYXKGAHY836/N7zoK/kzi6p8tXFhasHh8ocFL9VZSAkvH956gfeRfcnhs3xzRy8qQ/dq3q44v1jvQieMFg2cw==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.50.0.tgz", + "integrity": "sha512-vxd3G/ybKTSlm31MOA96gqvrRGv9RJ7LGtZCn2Vrc5htA0zCDvcMqUkifcjrWNNKXHUU3WCkYOzzVSFBd0wa2w==", "dev": true, "license": "MIT", "engines": { @@ -1933,15 +1910,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.46.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.0.tgz", - "integrity": "sha512-hy+lvYV1lZpVs2jRaEYvgCblZxUoJiPyCemwbQZ+NGulWkQRy0HRPYAoef/CNSzaLt+MLvMptZsHXHlkEilaeg==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.50.0.tgz", + "integrity": "sha512-7OciHT2lKCewR0mFoBrvZJ4AXTMe/sYOe87289WAViOocEmDjjv8MvIOT2XESuKj9jp8u3SZYUSh89QA4S1kQw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.0", - "@typescript-eslint/typescript-estree": "8.46.0", - "@typescript-eslint/utils": "8.46.0", + "@typescript-eslint/types": "8.50.0", + "@typescript-eslint/typescript-estree": "8.50.0", + "@typescript-eslint/utils": "8.50.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -1958,9 +1935,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.46.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.0.tgz", - "integrity": "sha512-bHGGJyVjSE4dJJIO5yyEWt/cHyNwga/zXGJbJJ8TiO01aVREK6gCTu3L+5wrkb1FbDkQ+TKjMNe9R/QQQP9+rA==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.50.0.tgz", + "integrity": "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w==", "dev": true, "license": "MIT", "engines": { @@ -1972,21 +1949,20 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.46.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.0.tgz", - "integrity": "sha512-ekDCUfVpAKWJbRfm8T1YRrCot1KFxZn21oV76v5Fj4tr7ELyk84OS+ouvYdcDAwZL89WpEkEj2DKQ+qg//+ucg==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.50.0.tgz", + "integrity": "sha512-W7SVAGBR/IX7zm1t70Yujpbk+zdPq/u4soeFSknWFdXIFuWsBGBOUu/Tn/I6KHSKvSh91OiMuaSnYp3mtPt5IQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.46.0", - "@typescript-eslint/tsconfig-utils": "8.46.0", - "@typescript-eslint/types": "8.46.0", - "@typescript-eslint/visitor-keys": "8.46.0", + "@typescript-eslint/project-service": "8.50.0", + "@typescript-eslint/tsconfig-utils": "8.50.0", + "@typescript-eslint/types": "8.50.0", + "@typescript-eslint/visitor-keys": "8.50.0", "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", + "tinyglobby": "^0.2.15", "ts-api-utils": "^2.1.0" }, "engines": { @@ -2001,16 +1977,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.46.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.0.tgz", - "integrity": "sha512-nD6yGWPj1xiOm4Gk0k6hLSZz2XkNXhuYmyIrOWcHoPuAhjT9i5bAG+xbWPgFeNR8HPHHtpNKdYUXJl/D3x7f5g==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.50.0.tgz", + "integrity": "sha512-87KgUXET09CRjGCi2Ejxy3PULXna63/bMYv72tCAlDJC3Yqwln0HiFJ3VJMst2+mEtNtZu5oFvX4qJGjKsnAgg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.46.0", - "@typescript-eslint/types": "8.46.0", - "@typescript-eslint/typescript-estree": "8.46.0" + "@typescript-eslint/scope-manager": "8.50.0", + "@typescript-eslint/types": "8.50.0", + "@typescript-eslint/typescript-estree": "8.50.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2025,13 +2001,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.46.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.0.tgz", - "integrity": "sha512-FrvMpAK+hTbFy7vH5j1+tMYHMSKLE6RzluFJlkFNKD0p9YsUT75JlBSmr5so3QRzvMwU5/bIEdeNrxm8du8l3Q==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.50.0.tgz", + "integrity": "sha512-Xzmnb58+Db78gT/CCj/PVCvK+zxbnsw6F+O1oheYszJbBSdEjVhQi3C/Xttzxgi/GLmpvOggRs1RFpiJ8+c34Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.0", + "@typescript-eslint/types": "8.50.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -2147,9 +2123,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.8.15", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.15.tgz", - "integrity": "sha512-qsJ8/X+UypqxHXN75M7dF88jNK37dLBRW7LeUzCPz+TNs37G8cfWy9nWzS+LS//g600zrt2le9KuXt0rWfDz5Q==", + "version": "2.9.9", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.9.tgz", + "integrity": "sha512-V8fbOCSeOFvlDj7LLChUcqbZrdKD9RU/VR260piF1790vT0mfLSwGc/Qzxv3IqiTukOpNtItePa0HBpMAj7MDg==", "dev": true, "license": "Apache-2.0", "bin": { @@ -2166,23 +2142,10 @@ "balanced-match": "^1.0.0" } }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/browserslist": { - "version": "4.26.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz", - "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "dev": true, "funding": [ { @@ -2200,11 +2163,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.8.9", - "caniuse-lite": "^1.0.30001746", - "electron-to-chromium": "^1.5.227", - "node-releases": "^2.0.21", - "update-browserslist-db": "^1.1.3" + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" @@ -2224,9 +2187,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001749", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001749.tgz", - "integrity": "sha512-0rw2fJOmLfnzCRbkm8EyHL8SvI2Apu5UbnQuTsJ0ClgrH8hcwFooJ1s5R0EP8o8aVrFu8++ae29Kt9/gZAZp/Q==", + "version": "1.0.30001760", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001760.tgz", + "integrity": "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==", "dev": true, "funding": [ { @@ -2263,7 +2226,7 @@ }, "node_modules/classcat": { "version": "5.0.5", - "resolved": "https://mirrors.tencent.com/npm/classcat/-/classcat-5.0.5.tgz", + "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", "license": "MIT" }, @@ -2317,15 +2280,15 @@ } }, "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "devOptional": true, "license": "MIT" }, "node_modules/d3-color": { "version": "3.1.0", - "resolved": "https://mirrors.tencent.com/npm/d3-color/-/d3-color-3.1.0.tgz", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", "license": "ISC", "engines": { @@ -2334,16 +2297,18 @@ }, "node_modules/d3-dispatch": { "version": "3.0.1", - "resolved": "https://mirrors.tencent.com/npm/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", "engines": { "node": ">=12" } }, "node_modules/d3-drag": { "version": "3.0.0", - "resolved": "https://mirrors.tencent.com/npm/d3-drag/-/d3-drag-3.0.0.tgz", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", "dependencies": { "d3-dispatch": "1 - 3", "d3-selection": "3" @@ -2354,15 +2319,16 @@ }, "node_modules/d3-ease": { "version": "3.0.1", - "resolved": "https://mirrors.tencent.com/npm/d3-ease/-/d3-ease-3.0.1.tgz", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", "engines": { "node": ">=12" } }, "node_modules/d3-interpolate": { "version": "3.0.1", - "resolved": "https://mirrors.tencent.com/npm/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", "license": "ISC", "dependencies": { @@ -2374,15 +2340,16 @@ }, "node_modules/d3-selection": { "version": "3.0.0", - "resolved": "https://mirrors.tencent.com/npm/d3-selection/-/d3-selection-3.0.0.tgz", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", "engines": { "node": ">=12" } }, "node_modules/d3-timer": { "version": "3.0.1", - "resolved": "https://mirrors.tencent.com/npm/d3-timer/-/d3-timer-3.0.1.tgz", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", "license": "ISC", "engines": { @@ -2391,7 +2358,7 @@ }, "node_modules/d3-transition": { "version": "3.0.1", - "resolved": "https://mirrors.tencent.com/npm/d3-transition/-/d3-transition-3.0.1.tgz", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", "license": "ISC", "dependencies": { @@ -2410,8 +2377,9 @@ }, "node_modules/d3-zoom": { "version": "3.0.0", - "resolved": "https://mirrors.tencent.com/npm/d3-zoom/-/d3-zoom-3.0.0.tgz", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", @@ -2449,9 +2417,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.234", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.234.tgz", - "integrity": "sha512-RXfEp2x+VRYn8jbKfQlRImzoJU01kyDvVPBmG39eU2iuRVhuS6vQNocB8J0/8GrIMLnPzgz4eW6WiRnJkTuNWg==", + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", "dev": true, "license": "ISC" }, @@ -2518,25 +2486,24 @@ } }, "node_modules/eslint": { - "version": "9.37.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.37.0.tgz", - "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.4.0", - "@eslint/core": "^0.16.0", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.37.0", - "@eslint/plugin-kit": "^0.4.0", + "@eslint/js": "9.39.2", + "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", @@ -2592,9 +2559,9 @@ } }, "node_modules/eslint-plugin-react-refresh": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.23.tgz", - "integrity": "sha512-G4j+rv0NmbIR45kni5xJOrYvCtyD3/7LjpVH8MPPcudXDcNu8gv+4ATTDXTtbRR8rTCM5HxECvCSsRmxKnWDsA==", + "version": "0.4.26", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -2762,36 +2729,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -2806,14 +2743,22 @@ "dev": true, "license": "MIT" }, - "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } } }, "node_modules/file-entry-cache": { @@ -2829,19 +2774,6 @@ "node": ">=16.0.0" } }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -2919,9 +2851,9 @@ } }, "node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", "dev": true, "license": "MIT", "engines": { @@ -2931,13 +2863,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -3008,16 +2933,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -3032,9 +2947,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -3160,30 +3075,6 @@ "yallist": "^3.0.2" } }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, "node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -3234,9 +3125,9 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.23", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.23.tgz", - "integrity": "sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==", + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", "dev": true, "license": "MIT" }, @@ -3331,13 +3222,13 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { - "node": ">=8.6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" @@ -3392,27 +3283,6 @@ "node": ">=6" } }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -3449,12 +3319,12 @@ } }, "node_modules/react-router": { - "version": "6.30.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz", - "integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==", + "version": "6.30.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.2.tgz", + "integrity": "sha512-H2Bm38Zu1bm8KUE5NVWRMzuIyAV8p/JrOaBJAwVmp37AXG72+CZJlEBw6pdn9i5TBgLMhNDgijS4ZlblpHyWTA==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.23.0" + "@remix-run/router": "1.23.1" }, "engines": { "node": ">=14.0.0" @@ -3464,13 +3334,13 @@ } }, "node_modules/react-router-dom": { - "version": "6.30.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz", - "integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==", + "version": "6.30.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.2.tgz", + "integrity": "sha512-l2OwHn3UUnEVUqc6/1VMmR1cvZryZ3j3NzapC2eUXO1dB0sYp5mvwdjiXhpUbRb21eFow3qSxpP8Yv6oAU824Q==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.23.0", - "react-router": "6.30.1" + "@remix-run/router": "1.23.1", + "react-router": "6.30.2" }, "engines": { "node": ">=14.0.0" @@ -3482,7 +3352,7 @@ }, "node_modules/reactflow": { "version": "11.11.4", - "resolved": "https://mirrors.tencent.com/npm/reactflow/-/reactflow-11.11.4.tgz", + "resolved": "https://registry.npmjs.org/reactflow/-/reactflow-11.11.4.tgz", "integrity": "sha512-70FOtJkUWH3BAOsN+LU9lCrKoKbtOPnz2uq0CV2PLdNSwxTXOhCbsZr50GmZ+Rtw3jx8Uv7/vBFtCGixLfd4Og==", "license": "MIT", "dependencies": { @@ -3508,21 +3378,10 @@ "node": ">=4" } }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, "node_modules/rollup": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz", - "integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.5.tgz", + "integrity": "sha512-iTNAbFSlRpcHeeWu73ywU/8KuU/LZmNCSxp6fjQkJBD3ivUb8tpDrXhIxEzA05HlYMEwmtaUnb3RP+YNv162OQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3536,55 +3395,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.52.4", - "@rollup/rollup-android-arm64": "4.52.4", - "@rollup/rollup-darwin-arm64": "4.52.4", - "@rollup/rollup-darwin-x64": "4.52.4", - "@rollup/rollup-freebsd-arm64": "4.52.4", - "@rollup/rollup-freebsd-x64": "4.52.4", - "@rollup/rollup-linux-arm-gnueabihf": "4.52.4", - "@rollup/rollup-linux-arm-musleabihf": "4.52.4", - "@rollup/rollup-linux-arm64-gnu": "4.52.4", - "@rollup/rollup-linux-arm64-musl": "4.52.4", - "@rollup/rollup-linux-loong64-gnu": "4.52.4", - "@rollup/rollup-linux-ppc64-gnu": "4.52.4", - "@rollup/rollup-linux-riscv64-gnu": "4.52.4", - "@rollup/rollup-linux-riscv64-musl": "4.52.4", - "@rollup/rollup-linux-s390x-gnu": "4.52.4", - "@rollup/rollup-linux-x64-gnu": "4.52.4", - "@rollup/rollup-linux-x64-musl": "4.52.4", - "@rollup/rollup-openharmony-arm64": "4.52.4", - "@rollup/rollup-win32-arm64-msvc": "4.52.4", - "@rollup/rollup-win32-ia32-msvc": "4.52.4", - "@rollup/rollup-win32-x64-gnu": "4.52.4", - "@rollup/rollup-win32-x64-msvc": "4.52.4", + "@rollup/rollup-android-arm-eabi": "4.53.5", + "@rollup/rollup-android-arm64": "4.53.5", + "@rollup/rollup-darwin-arm64": "4.53.5", + "@rollup/rollup-darwin-x64": "4.53.5", + "@rollup/rollup-freebsd-arm64": "4.53.5", + "@rollup/rollup-freebsd-x64": "4.53.5", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.5", + "@rollup/rollup-linux-arm-musleabihf": "4.53.5", + "@rollup/rollup-linux-arm64-gnu": "4.53.5", + "@rollup/rollup-linux-arm64-musl": "4.53.5", + "@rollup/rollup-linux-loong64-gnu": "4.53.5", + "@rollup/rollup-linux-ppc64-gnu": "4.53.5", + "@rollup/rollup-linux-riscv64-gnu": "4.53.5", + "@rollup/rollup-linux-riscv64-musl": "4.53.5", + "@rollup/rollup-linux-s390x-gnu": "4.53.5", + "@rollup/rollup-linux-x64-gnu": "4.53.5", + "@rollup/rollup-linux-x64-musl": "4.53.5", + "@rollup/rollup-openharmony-arm64": "4.53.5", + "@rollup/rollup-win32-arm64-msvc": "4.53.5", + "@rollup/rollup-win32-ia32-msvc": "4.53.5", + "@rollup/rollup-win32-x64-gnu": "4.53.5", + "@rollup/rollup-win32-x64-msvc": "4.53.5", "fsevents": "~2.3.2" } }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -3666,17 +3501,21 @@ "node": ">=8" } }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "dev": true, "license": "MIT", "dependencies": { - "is-number": "^7.0.0" + "fdir": "^6.5.0", + "picomatch": "^4.0.3" }, "engines": { - "node": ">=8.0" + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" } }, "node_modules/ts-api-utils": { @@ -3719,10 +3558,34 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.50.0.tgz", + "integrity": "sha512-Q1/6yNUmCpH94fbgMUMg2/BSAr/6U7GBk61kZTv1/asghQOWOjTlp9K8mixS5NcJmm2creY+UFfGeW/+OcA64A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.50.0", + "@typescript-eslint/parser": "8.50.0", + "@typescript-eslint/typescript-estree": "8.50.0", + "@typescript-eslint/utils": "8.50.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "dev": true, "funding": [ { @@ -3762,7 +3625,7 @@ }, "node_modules/use-sync-external-store": { "version": "1.6.0", - "resolved": "https://mirrors.tencent.com/npm/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", "license": "MIT", "peerDependencies": { @@ -3770,9 +3633,9 @@ } }, "node_modules/vite": { - "version": "5.4.20", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.20.tgz", - "integrity": "sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g==", + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", "dependencies": { @@ -3877,7 +3740,7 @@ }, "node_modules/zustand": { "version": "4.5.7", - "resolved": "https://mirrors.tencent.com/npm/zustand/-/zustand-4.5.7.tgz", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", "license": "MIT", "dependencies": { diff --git a/dashboard/frontend/package.json b/dashboard/frontend/package.json index 030688c2b8..520274e738 100644 --- a/dashboard/frontend/package.json +++ b/dashboard/frontend/package.json @@ -6,7 +6,7 @@ "dev": "vite", "build": "tsc && vite build", "preview": "vite preview", - "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "lint": "eslint .", "type-check": "tsc --noEmit" }, "dependencies": { @@ -16,6 +16,7 @@ "reactflow": "^11.11.4" }, "devDependencies": { + "@eslint/js": "^9.18.0", "@types/react": "^18.3.18", "@types/react-dom": "^18.3.5", "@typescript-eslint/eslint-plugin": "^8.45.0", @@ -24,7 +25,9 @@ "eslint": "^9.18.0", "eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-react-refresh": "^0.4.16", + "globals": "^16.1.0", "typescript": "^5.9.3", + "typescript-eslint": "^8.45.0", "vite": "^5.4.11" } } diff --git a/dashboard/frontend/src/pages/PlaygroundPage.tsx b/dashboard/frontend/src/pages/PlaygroundPage.tsx index e0eadc78ab..23d1b4e50c 100644 --- a/dashboard/frontend/src/pages/PlaygroundPage.tsx +++ b/dashboard/frontend/src/pages/PlaygroundPage.tsx @@ -1,48 +1,15 @@ -import React, { useState, useEffect } from 'react' import styles from './PlaygroundPage.module.css' -const PlaygroundPage: React.FC = () => { - // Detect OpenWebUI URL based on current hostname - // Assumes openwebui and dashboard have matching hostname patterns - const getOpenWebUIUrl = () => { - const hostname = window.location.hostname - const openwebuiHost = hostname.replace('dashboard', 'openwebui') - return `${window.location.protocol}//${openwebuiHost}` - } - - const [openWebUIUrl] = useState(getOpenWebUIUrl()) - const [currentUrl, setCurrentUrl] = useState('') - - // Auto-load on mount - useEffect(() => { - // Default to loading the configured URL on mount - setCurrentUrl(openWebUIUrl) - }, [openWebUIUrl]) // Load when URL changes - +const PlaygroundPage = () => { return (
- {!currentUrl && ( -
- 🎮 -

Open WebUI Playground

-

- Test your LLM models and semantic routing with Open WebUI. -

-

- Note: Open WebUI needs to be deployed separately. Check the dashboard README for instructions. -

-
- )} - - {currentUrl && (