diff --git a/.gitignore b/.gitignore index 38fc96a1f8..3b7fa6ba38 100755 --- a/.gitignore +++ b/.gitignore @@ -21,10 +21,17 @@ unsloth_compiled_cache/ # ML artifacts (large files) outputs/ exports/ +/datasets/ unsloth_training_checkpoints/ *.gguf *.safetensors +# llama.cpp build (built by setup.sh, shared with unsloth-zoo export) +llama.cpp/ + +# Built binaries (llama-server etc.) +bin/ + # IDE / Editors .vscode/ .idea/ @@ -47,6 +54,16 @@ resources/ tmp/ auth.db studio/frontend/package-lock.json + +# Local working docs +**/CLAUDE.md +**/claude.md +**/AGENT.md +**/agent.md +docs/canvas-lab-architecture.md +studio/frontend/test/ +studio/tests/ +studio/backend/tests/ log_rtx.txt log.txt setup_leo.sh diff --git a/README.md b/README.md index 3d2810fae5..9656f1c634 100644 --- a/README.md +++ b/README.md @@ -30,28 +30,119 @@ ## Quick Start -### One-command setup +### Prerequisites + +| Requirement | Linux / WSL | Windows | +|---|---|---| +| **GPU** | NVIDIA GPU with working driver | NVIDIA GPU with working driver | +| **Python** | 3.11 – 3.13 | 3.11 – 3.13 | +| **Git** | Pre-installed on most distros | Auto-installed by setup script (via `winget`) | +| **CMake** | Pre-installed or `sudo apt install cmake` | Auto-installed by setup script (via `winget`) | +| **C++ compiler** | `build-essential` (auto-detected) | Visual Studio Build Tools 2022 (auto-installed by setup script) | +| **CUDA Toolkit** | Optional — setup auto-detects `nvcc` | Auto-installed by setup script (version matched to driver) | + +> [!NOTE] +> On **WSL**, the setup script will also run `sudo apt-get install build-essential cmake curl git libcurl4-openssl-dev` so that GGUF export works in non-interactive subprocesses. You may be prompted for your password during setup. + +--- + +### Linux / Windows WSL ```bash +# 1. Clone the repo +git clone https://github.com/unslothai/unsloth-studio.git +cd unsloth-studio + +# 2. Run setup (installs Node, builds frontend, creates .venv, builds llama.cpp) bash setup.sh + +# 3. Open a new terminal (or source your shell rc), then launch: +unsloth-studio -H 0.0.0.0 -p 8000 ``` -This script will: -1. Install **Node.js ≥ 20** via nvm (if needed) -2. Build the frontend to `studio/frontend/dist` -3. Create a Python virtual environment and install all dependencies (including `unsloth`) -4. Register a convenient `unsloth-ui` shell alias +
+What does setup.sh do? -### Launch the studio +1. Installs **Node.js ≥ 20** via nvm (if needed) +2. Runs `npm install && npm run build` for the React frontend +3. Detects the best **Python 3.11 – 3.13** on your system and creates a `.venv` +4. Installs all Python dependencies (unsloth, PyTorch with CUDA, triton kernels, etc.) +5. On **WSL**: pre-installs build dependencies via `apt-get` +6. Clones and builds **llama.cpp** at `~/.unsloth/llama.cpp` (GPU-accelerated if CUDA is found) +7. Registers `unsloth-studio` and `unsloth-ui` shell aliases in your shell rc (bash, zsh, fish, or ksh) + +
+ +--- + +### Windows (Native) + +> [!IMPORTANT] +> Requires an **NVIDIA GPU** — CPU-only machines are not supported on Windows. + +```powershell +# 1. Clone the repo +git clone https://github.com/unslothai/unsloth-studio.git +cd unsloth-studio + +# 2. Run setup (Right-click → "Run with PowerShell", or from a terminal): +.\setup.bat +# Or directly: +powershell -ExecutionPolicy Bypass -File setup.ps1 +``` + +After setup completes, **open a new terminal** and run: + +```powershell +# PowerShell +unsloth-studio -H 0.0.0.0 -p 8000 + +# Or cmd.exe +unsloth-studio -H 0.0.0.0 -p 8000 +``` + +
+What does setup.ps1 do? + +1. Enables **Windows Long Paths** (required for deep dependency trees — prompts for UAC) +2. Auto-installs missing system tools via `winget`: **Git**, **CMake**, **Visual Studio Build Tools 2022**, **CUDA Toolkit** (version-matched to your driver), **Node.js LTS**, **Python 3.12**, **OpenSSL dev** +3. Builds the React frontend (`npm install && npm run build`) +4. Creates a `.venv` and installs all Python dependencies (including CUDA-enabled PyTorch from the official index) +5. Sets `TORCHINDUCTOR_CACHE_DIR=C:\tc` to avoid Windows MAX_PATH issues with Triton +6. Clones and builds **llama.cpp** at `%USERPROFILE%\.unsloth\llama.cpp` with CUDA + Visual Studio +7. Registers `unsloth-studio` and `unsloth-ui` commands in both PowerShell profile and `cmd.exe` (via batch files on PATH) + +
+ +--- + +### Google Colab + +The setup script auto-detects Colab and installs everything into the existing system Python (no venv): + +```python +!bash setup.sh +``` + +--- + +### Launching the Studio + +After setup on any platform, the command is the same: ```bash -# After setup, open a new terminal (or source ~/.bashrc), then inside your working directory: -unsloth-ui -H 0.0.0.0 -p 8000 +unsloth-studio -H 0.0.0.0 -p 8000 ``` -On **first launch**, a one-time setup token is printed to the console. Use it in the browser to create your admin account. +| Flag | Description | +|---|---| +| `-H` / `--host` | Bind address (`0.0.0.0` for all interfaces, `127.0.0.1` for local only) | +| `-p` / `--port` | Port number (default: `8000`) | -As this repo is in continuous development, please make sure to run the setup.sh file everytime you pull new changes from the repo. +On **first launch**, a one-time setup token is printed to the console. Open the URL shown in your browser and use this token to create your admin account. + +> [!TIP] +> This repo is in active development. After pulling new changes, **always re-run the setup script** (`bash setup.sh` or `.\setup.bat`) to pick up dependency and build updates. ## API Reference @@ -105,7 +196,10 @@ new-ui-prototype/ │ ├── export.py │ ├── ui.py │ └── studio.py -├── setup.sh # One-command bootstrap script +├── setup.sh # Bootstrap script (Linux / WSL / Colab) +├── setup.ps1 # Bootstrap script (Windows native) +├── setup.bat # Wrapper to launch setup.ps1 via double-click +├── install_python_stack.py # Cross-platform Python dependency installer └── studio/ ├── backend/ │ ├── main.py # FastAPI app & middleware diff --git a/install_python_stack.py b/install_python_stack.py new file mode 100644 index 0000000000..c9b73034b4 --- /dev/null +++ b/install_python_stack.py @@ -0,0 +1,245 @@ +#!/usr/bin/env python3 +"""Cross-platform Python dependency installer for Unsloth Studio. + +Called by both setup.sh (Linux / WSL) and setup.ps1 (Windows) after the +virtual environment is already activated. Expects `pip` and `python` on +PATH to point at the venv. +""" + +from __future__ import annotations + +import os +import subprocess +import sys +import tempfile +import urllib.request +from pathlib import Path + +IS_WINDOWS = sys.platform == "win32" + +# ── Paths ────────────────────────────────────────────────────────────── +SCRIPT_DIR = Path(__file__).resolve().parent +REQ_ROOT = SCRIPT_DIR / "studio" / "backend" / "requirements" +SINGLE_ENV = REQ_ROOT / "single-env" +CONSTRAINTS = SINGLE_ENV / "constraints.txt" + +# ── Color support ────────────────────────────────────────────────────── + +def _enable_colors() -> bool: + """Try to enable ANSI color support. Returns True if available.""" + if not hasattr(sys.stdout, "fileno"): + return False + try: + if not os.isatty(sys.stdout.fileno()): + return False + except Exception: + return False + if IS_WINDOWS: + try: + import ctypes + kernel32 = ctypes.windll.kernel32 + # Enable ENABLE_VIRTUAL_TERMINAL_PROCESSING (0x0004) on stdout + handle = kernel32.GetStdHandle(-11) # STD_OUTPUT_HANDLE + mode = ctypes.c_ulong() + kernel32.GetConsoleMode(handle, ctypes.byref(mode)) + kernel32.SetConsoleMode(handle, mode.value | 0x0004) + return True + except Exception: + return False + return True # Unix terminals support ANSI by default + +_HAS_COLOR = _enable_colors() + +def _green(msg: str) -> str: + return f"\033[92m{msg}\033[0m" if _HAS_COLOR else msg + +def _cyan(msg: str) -> str: + return f"\033[96m{msg}\033[0m" if _HAS_COLOR else msg + +def _red(msg: str) -> str: + return f"\033[91m{msg}\033[0m" if _HAS_COLOR else msg + + +def run(label: str, cmd: list[str], *, quiet: bool = True) -> None: + """Run a command; on failure print output and exit.""" + print(_cyan(f" {label}...")) + result = subprocess.run( + cmd, + stdout=subprocess.PIPE if quiet else None, + stderr=subprocess.STDOUT if quiet else None, + ) + if result.returncode != 0: + print(_red(f"❌ {label} failed (exit code {result.returncode}):")) + if result.stdout: + print(result.stdout.decode(errors="replace")) + sys.exit(result.returncode) + + +# Packages to skip on Windows (require special build steps) +WINDOWS_SKIP_PACKAGES = {"open_spiel"} + + +def _filter_requirements(req: Path, skip: set[str]) -> Path: + """Return a temp copy of a requirements file with certain packages removed.""" + lines = req.read_text(encoding="utf-8").splitlines(keepends=True) + filtered = [ + line for line in lines + if not any(line.strip().lower().startswith(pkg) for pkg in skip) + ] + tmp = tempfile.NamedTemporaryFile( + mode="w", suffix=".txt", delete=False, encoding="utf-8", + ) + tmp.writelines(filtered) + tmp.close() + return Path(tmp.name) + + +def pip_install( + label: str, + *args: str, + req: Path | None = None, + constrain: bool = True, +) -> None: + """Build and run a pip install command.""" + cmd = [sys.executable, "-m", "pip", "install"] + cmd.extend(args) + if constrain and CONSTRAINTS.is_file(): + cmd.extend(["-c", str(CONSTRAINTS)]) + actual_req = req + if req is not None and IS_WINDOWS and WINDOWS_SKIP_PACKAGES: + actual_req = _filter_requirements(req, WINDOWS_SKIP_PACKAGES) + if actual_req is not None: + cmd.extend(["-r", str(actual_req)]) + try: + run(label, cmd) + finally: + # Clean up temp file if we created one + if actual_req is not None and actual_req != req: + actual_req.unlink(missing_ok=True) + + + +def download_file(url: str, dest: Path) -> None: + """Download a file using urllib (no curl dependency).""" + urllib.request.urlretrieve(url, dest) + + +def patch_package_file(package_name: str, relative_path: str, url: str) -> None: + """Download a file from url and overwrite a file inside an installed package.""" + result = subprocess.run( + [sys.executable, "-m", "pip", "show", package_name], + capture_output=True, text=True, + ) + if result.returncode != 0: + print(_red(f" ⚠️ Could not find package {package_name}, skipping patch")) + return + + location = None + for line in result.stdout.splitlines(): + if line.lower().startswith("location:"): + location = line.split(":", 1)[1].strip() + break + + if not location: + print(_red(f" ⚠️ Could not determine location of {package_name}")) + return + + dest = Path(location) / relative_path + print(_cyan(f" Patching {dest.name} in {package_name}...")) + download_file(url, dest) + + +# ── Main install sequence ───────────────────────────────────────────── + +def install_python_stack() -> int: + print(_cyan("── Installing Python stack ──")) + + # 1. Upgrade pip + run("Upgrading pip", [sys.executable, "-m", "pip", "install", "--upgrade", "pip"]) + + # 2. Core packages: unsloth-zoo + unsloth + pip_install( + "Installing unsloth-zoo + unsloth", + "--no-cache-dir", + req=REQ_ROOT / "base.txt", + ) + + # 3. Extra dependencies + pip_install( + "Installing additional unsloth dependencies", + "--no-cache-dir", + req=REQ_ROOT / "extras.txt", + ) + + # 4. Overrides (torchao, transformers) — force-reinstall + pip_install( + "Installing torchao + transformers overrides", + "--force-reinstall", "--no-cache-dir", + req=REQ_ROOT / "overrides.txt", + ) + + # 5. Triton kernels (no-deps, from source) + pip_install( + "Installing triton kernels", + "--no-deps", "--no-cache-dir", + req=REQ_ROOT / "triton-kernels.txt", + constrain=False, + ) + + # 6. Patch: override llama_cpp.py with fix from unsloth-zoo feature/llama-cpp-windows-support branch + patch_package_file( + "unsloth-zoo", + os.path.join("unsloth_zoo", "llama_cpp.py"), + "https://raw.githubusercontent.com/unslothai/unsloth-zoo/refs/heads/main/unsloth_zoo/llama_cpp.py", + ) + + # 7a. Patch: override vision.py with fix from unsloth PR #4091 + patch_package_file( + "unsloth", + os.path.join("unsloth", "models", "vision.py"), + "https://raw.githubusercontent.com/unslothai/unsloth/80e0108a684c882965a02a8ed851e3473c1145ab/unsloth/models/vision.py", + ) + + # 7b. Patch : override save.py with fix from feature/llama-cpp-windows-support + patch_package_file( + "unsloth", + os.path.join("unsloth", "save.py"), + "https://raw.githubusercontent.com/unslothai/unsloth/refs/heads/main/unsloth/save.py", + ) + + # 8. Studio dependencies + pip_install( + "Installing studio dependencies", + "--no-cache-dir", + req=REQ_ROOT / "studio.txt", + ) + + # 9. Data-designer dependencies + pip_install( + "Installing data-designer dependencies", + "--no-cache-dir", + req=SINGLE_ENV / "data-designer-deps.txt", + ) + + # 10. Data-designer packages (no-deps to avoid conflicts) + pip_install( + "Installing data-designer", + "--no-cache-dir", "--no-deps", + req=SINGLE_ENV / "data-designer.txt", + ) + + # 11. Patch metadata for single-env compatibility + run( + "Patching single-env metadata", + [sys.executable, str(SINGLE_ENV / "patch_metadata.py")], + ) + + # 12. Final check + run("Running pip check", [sys.executable, "-m", "pip", "check"], quiet=False) + + print(_green("✅ Python dependencies installed")) + return 0 + + +if __name__ == "__main__": + sys.exit(install_python_stack()) diff --git a/setup.bat b/setup.bat new file mode 100644 index 0000000000..ef16abd263 --- /dev/null +++ b/setup.bat @@ -0,0 +1,2 @@ +@echo off +powershell -ExecutionPolicy Bypass -File "%~dp0setup.ps1" %* diff --git a/setup.ps1 b/setup.ps1 new file mode 100644 index 0000000000..8a2ec56033 --- /dev/null +++ b/setup.ps1 @@ -0,0 +1,964 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + Full environment setup for Unsloth Studio on Windows (bundled version). +.DESCRIPTION + Always installs Node.js if needed. When running from pip install: + skips frontend build (already bundled). When running from git repo: + full setup including frontend build. + Requires an NVIDIA GPU -- CPU-only machines are not supported. +.NOTES + Usage: powershell -ExecutionPolicy Bypass -File setup.ps1 +#> + +$ErrorActionPreference = "Stop" +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$PackageDir = Split-Path -Parent $ScriptDir + +# Detect if running from pip install (no studio/frontend/ dir in repo) +$FrontendDir = Join-Path $ScriptDir "studio\frontend" +$IsPipInstall = -not (Test-Path $FrontendDir) + +# ───────────────────────────────────────────── +# Helper functions +# ───────────────────────────────────────────── + +# Reload ALL environment variables from registry. +# Picks up changes made by installers (winget, msi, etc.) including +# Path, CUDA_PATH, CUDA_PATH_V*, and any other vars they set. +function Refresh-Environment { + foreach ($level in @('Machine', 'User')) { + $vars = [System.Environment]::GetEnvironmentVariables($level) + foreach ($key in $vars.Keys) { + if ($key -eq 'Path') { continue } + Set-Item -Path "Env:$key" -Value $vars[$key] -ErrorAction SilentlyContinue + } + } + $machinePath = [System.Environment]::GetEnvironmentVariable('Path', 'Machine') + $userPath = [System.Environment]::GetEnvironmentVariable('Path', 'User') + $env:Path = "$machinePath;$userPath" +} + +# Find nvcc on PATH, CUDA_PATH, or standard toolkit dirs. +# Returns the path to nvcc.exe, or $null if not found. +function Find-Nvcc { + param([string]$MaxVersion = "") + + # If MaxVersion is set, we need to find a toolkit <= that version. + # CUDA toolkits install side-by-side under C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\vX.Y\ + + $toolkitBase = 'C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA' + + if ($MaxVersion -and (Test-Path $toolkitBase)) { + $drMajor = [int]$MaxVersion.Split('.')[0] + $drMinor = [int]$MaxVersion.Split('.')[1] + + # Get all installed CUDA dirs, sorted descending (highest first) + $cudaDirs = Get-ChildItem -Directory $toolkitBase | Where-Object { + $_.Name -match '^v(\d+)\.(\d+)' + } | Sort-Object { [version]($_.Name -replace '^v','') } -Descending + + foreach ($dir in $cudaDirs) { + if ($dir.Name -match '^v(\d+)\.(\d+)') { + $tkMajor = [int]$Matches[1]; $tkMinor = [int]$Matches[2] + $compatible = ($tkMajor -lt $drMajor) -or ($tkMajor -eq $drMajor -and $tkMinor -le $drMinor) + if ($compatible) { + $nvcc = Join-Path $dir.FullName 'bin\nvcc.exe' + if (Test-Path $nvcc) { + return $nvcc + } + } + } + } + + # No compatible side-by-side version found + return $null + } + + # Fallback: no version constraint — pick latest or whatever is available + + # 1. Check nvcc on PATH + $cmd = Get-Command nvcc -ErrorAction SilentlyContinue + if ($cmd) { return $cmd.Source } + + # 2. Check CUDA_PATH env var + $cudaRoot = [Environment]::GetEnvironmentVariable('CUDA_PATH', 'Process') + if (-not $cudaRoot) { $cudaRoot = [Environment]::GetEnvironmentVariable('CUDA_PATH', 'Machine') } + if (-not $cudaRoot) { $cudaRoot = [Environment]::GetEnvironmentVariable('CUDA_PATH', 'User') } + if ($cudaRoot -and (Test-Path (Join-Path $cudaRoot 'bin\nvcc.exe'))) { + return (Join-Path $cudaRoot 'bin\nvcc.exe') + } + + # 3. Scan standard toolkit directory + if (Test-Path $toolkitBase) { + $latest = Get-ChildItem -Directory $toolkitBase | Sort-Object Name | Select-Object -Last 1 + if ($latest -and (Test-Path (Join-Path $latest.FullName 'bin\nvcc.exe'))) { + return (Join-Path $latest.FullName 'bin\nvcc.exe') + } + } + + return $null +} + +# Detect CUDA Compute Capability via nvidia-smi. +# Returns e.g. "80" for A100 (8.0), "89" for RTX 4090 (8.9), etc. +# Returns $null if detection fails. +function Get-CudaComputeCapability { + $nvSmi = Get-Command nvidia-smi -ErrorAction SilentlyContinue + if (-not $nvSmi) { return $null } + + try { + $raw = & nvidia-smi --query-gpu=compute_cap --format=csv,noheader 2>$null + if ($LASTEXITCODE -ne 0 -or -not $raw) { return $null } + + # nvidia-smi may return multiple GPUs; take the first one + $cap = ($raw -split "`n")[0].Trim() + if ($cap -match '^(\d+)\.(\d+)$') { + $major = $Matches[1] + $minor = $Matches[2] + return "$major$minor" + } + } catch { } + + return $null +} + +# Detect driver's max CUDA version from nvidia-smi and return the highest +# compatible PyTorch CUDA index tag (e.g. "cu128"). +# PyTorch on Windows ships CPU-only by default from PyPI; CUDA wheels live at +# https://download.pytorch.org/whl/. The tag must not exceed the driver's +# capability: e.g. driver "CUDA Version: 12.9" → cu128 (not cu130). +function Get-PytorchCudaTag { + $nvSmi = Get-Command nvidia-smi -ErrorAction SilentlyContinue + if (-not $nvSmi) { return "cu124" } + + try { + # 2>&1 | Out-String merges stderr into stdout then converts to a single + # string. Plain 2>$null doesn't fully suppress stderr in PS 5.1 — + # ErrorRecord objects leak into $output and break the -match. + $output = & nvidia-smi 2>&1 | Out-String + if ($output -match 'CUDA Version:\s+(\d+)\.(\d+)') { + $major = [int]$Matches[1] + $minor = [int]$Matches[2] + # PyTorch 2.10 offers: cu124, cu126, cu128, cu130 + if ($major -ge 13) { return "cu130" } + if ($major -eq 12 -and $minor -ge 8) { return "cu128" } + if ($major -eq 12 -and $minor -ge 6) { return "cu126" } + return "cu124" + } + } catch { } + + return "cu124" +} + +# Find Visual Studio Build Tools for cmake -G flag. +# Strategy: (1) vswhere, (2) scan filesystem (handles broken vswhere registration). +# Returns @{ Generator = "Visual Studio 17 2022"; InstallPath = "C:\..."; Source = "..." } or $null. +function Find-VsBuildTools { + $map = @{ '2022' = '17'; '2019' = '16'; '2017' = '15' } + + # --- Try vswhere first (works when VS is properly registered) --- + $vsw = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" + if (Test-Path $vsw) { + $info = & $vsw -latest -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property catalog_productLineVersion 2>$null + $path = & $vsw -latest -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath 2>$null + if ($info -and $path) { + $y = $info.Trim() + $n = $map[$y] + if ($n) { + return @{ Generator = "Visual Studio $n $y"; InstallPath = $path.Trim(); Source = 'vswhere' } + } + } + } + + # --- Scan filesystem (handles broken vswhere registration after winget cycles) --- + $roots = @($env:ProgramFiles, ${env:ProgramFiles(x86)}) + $editions = @('BuildTools', 'Community', 'Professional', 'Enterprise') + $years = @('2022', '2019', '2017') + + foreach ($y in $years) { + foreach ($r in $roots) { + foreach ($ed in $editions) { + $candidate = Join-Path $r "Microsoft Visual Studio\$y\$ed" + if (Test-Path $candidate) { + $vcDir = Join-Path $candidate "VC\Tools\MSVC" + if (Test-Path $vcDir) { + $cl = Get-ChildItem -Path $vcDir -Filter "cl.exe" -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($cl) { + $n = $map[$y] + if ($n) { + return @{ Generator = "Visual Studio $n $y"; InstallPath = $candidate; Source = "filesystem ($ed)"; ClExe = $cl.FullName } + } + } + } + } + } + } + } + + return $null +} + +# ───────────────────────────────────────────── +# Banner +# ───────────────────────────────────────────── +Write-Host "+==============================================+" -ForegroundColor Green +Write-Host "| Unsloth Studio Setup (Windows) |" -ForegroundColor Green +Write-Host "+==============================================+" -ForegroundColor Green + +# ========================================================================== +# PHASE 1: System-level prerequisites (winget installs, env vars) +# All heavy system tool installs happen here BEFORE touching Python. +# ========================================================================== + +# ============================================ +# 1a. GPU requirement check +# ============================================ +$HasNvidiaSmi = $null -ne (Get-Command nvidia-smi -ErrorAction SilentlyContinue) +if (-not $HasNvidiaSmi) { + Write-Host "" + Write-Host "[ERROR] Unsloth Studio requires an NVIDIA GPU." -ForegroundColor Red + Write-Host " CPU-only machines are not supported." -ForegroundColor Red + Write-Host "" + Write-Host " If you have an NVIDIA GPU, ensure the driver is installed:" -ForegroundColor Yellow + Write-Host " https://www.nvidia.com/Download/index.aspx" -ForegroundColor Yellow + exit 1 +} +Write-Host "[OK] NVIDIA GPU detected" -ForegroundColor Green + +# ============================================ +# 1a.5. Windows Long Paths (required for deep node_modules / Python paths) +# ============================================ +$LongPathsEnabled = $false +try { + $regVal = Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem" -Name "LongPathsEnabled" -ErrorAction SilentlyContinue + if ($regVal -and $regVal.LongPathsEnabled -eq 1) { + $LongPathsEnabled = $true + } +} catch {} + +if ($LongPathsEnabled) { + Write-Host "[OK] Windows Long Paths enabled" -ForegroundColor Green +} else { + Write-Host "Windows Long Paths not enabled (required for Triton compilation and deep dependency paths)." -ForegroundColor Yellow + Write-Host " Requesting admin access to fix..." -ForegroundColor Yellow + try { + # Spawn an elevated process to set the registry key (triggers UAC prompt) + $proc = Start-Process -FilePath "reg.exe" ` + -ArgumentList 'add "HKLM\SYSTEM\CurrentControlSet\Control\FileSystem" /v LongPathsEnabled /t REG_DWORD /d 1 /f' ` + -Verb RunAs -Wait -PassThru -ErrorAction Stop + if ($proc.ExitCode -eq 0) { + $LongPathsEnabled = $true + Write-Host "[OK] Windows Long Paths enabled (via UAC)" -ForegroundColor Green + } else { + Write-Host "[WARN] Failed to enable Long Paths (exit code: $($proc.ExitCode))" -ForegroundColor Yellow + } + } catch { + Write-Host "[WARN] Could not enable Long Paths (UAC was declined or not available)" -ForegroundColor Yellow + Write-Host " Run this manually in an Admin terminal:" -ForegroundColor Yellow + Write-Host ' reg add "HKLM\SYSTEM\CurrentControlSet\Control\FileSystem" /v LongPathsEnabled /t REG_DWORD /d 1 /f' -ForegroundColor Cyan + } +} + +# ============================================ +# 1b. Git (required by pip for git+https:// deps and by npm) +# ============================================ +$HasGit = $null -ne (Get-Command git -ErrorAction SilentlyContinue) +if (-not $HasGit) { + Write-Host "Git not found -- installing via winget..." -ForegroundColor Yellow + $HasWinget = $null -ne (Get-Command winget -ErrorAction SilentlyContinue) + if ($HasWinget) { + try { + winget install Git.Git --source winget --accept-package-agreements --accept-source-agreements 2>&1 | Out-Null + Refresh-Environment + $HasGit = $null -ne (Get-Command git -ErrorAction SilentlyContinue) + } catch { } + } + if (-not $HasGit) { + Write-Host "[ERROR] Git is required but could not be installed automatically." -ForegroundColor Red + Write-Host " Install Git from https://git-scm.com/download/win and re-run." -ForegroundColor Red + exit 1 + } + Write-Host "[OK] Git installed: $(git --version)" -ForegroundColor Green +} else { + Write-Host "[OK] Git found: $(git --version)" -ForegroundColor Green +} + +# ============================================ +# 1c. CMake (required for llama.cpp build) +# ============================================ +$HasCmake = $null -ne (Get-Command cmake -ErrorAction SilentlyContinue) +if (-not $HasCmake) { + Write-Host "CMake not found -- installing via winget..." -ForegroundColor Yellow + $HasWinget = $null -ne (Get-Command winget -ErrorAction SilentlyContinue) + if ($HasWinget) { + try { + winget install Kitware.CMake --source winget --accept-package-agreements --accept-source-agreements 2>&1 | Out-Null + Refresh-Environment + $HasCmake = $null -ne (Get-Command cmake -ErrorAction SilentlyContinue) + } catch { } + } + if ($HasCmake) { + Write-Host "[OK] CMake installed" -ForegroundColor Green + } else { + Write-Host "[ERROR] CMake is required but could not be installed." -ForegroundColor Red + Write-Host " Install CMake from https://cmake.org/download/ and re-run." -ForegroundColor Red + exit 1 + } +} else { + Write-Host "[OK] CMake found: $(cmake --version | Select-Object -First 1)" -ForegroundColor Green +} + +# ============================================ +# 1d. Visual Studio Build Tools (C++ compiler for llama.cpp) +# ============================================ +$CmakeGenerator = $null +$VsInstallPath = $null +$vsResult = Find-VsBuildTools + +if (-not $vsResult) { + Write-Host "Visual Studio Build Tools not found -- installing via winget..." -ForegroundColor Yellow + Write-Host " (This is a one-time install, may take several minutes)" -ForegroundColor Gray + $HasWinget = $null -ne (Get-Command winget -ErrorAction SilentlyContinue) + if ($HasWinget) { + $prevEAPTemp = $ErrorActionPreference + $ErrorActionPreference = "Continue" + winget install Microsoft.VisualStudio.2022.BuildTools --source winget --accept-package-agreements --accept-source-agreements --override "--add Microsoft.VisualStudio.Workload.VCTools --includeRecommended --passive --wait" + $ErrorActionPreference = $prevEAPTemp + # Re-scan after install (don't trust vswhere catalog) + $vsResult = Find-VsBuildTools + } +} + +if ($vsResult) { + $CmakeGenerator = $vsResult.Generator + $VsInstallPath = $vsResult.InstallPath + Write-Host "[OK] $CmakeGenerator detected via $($vsResult.Source)" -ForegroundColor Green + if ($vsResult.ClExe) { Write-Host " cl.exe: $($vsResult.ClExe)" -ForegroundColor Gray } +} else { + Write-Host "[ERROR] Visual Studio Build Tools could not be found or installed." -ForegroundColor Red + Write-Host " Manual install:" -ForegroundColor Red + Write-Host ' 1. winget install Microsoft.VisualStudio.2022.BuildTools --source winget' -ForegroundColor Yellow + Write-Host ' 2. Open Visual Studio Installer -> Modify -> check "Desktop development with C++"' -ForegroundColor Yellow + exit 1 +} + +# ============================================ +# 1e. CUDA Toolkit (nvcc for llama.cpp build + env vars) +# ============================================ +# IMPORTANT: The CUDA Toolkit version must be <= the max CUDA version the +# NVIDIA driver supports. nvidia-smi reports this as "CUDA Version: X.Y". +# If we install a toolkit newer than the driver supports, llama-server will +# fail at runtime with "ggml_cuda_init: failed to initialize CUDA: (null)". + +# -- Detect max CUDA version the driver supports -- +$DriverMaxCuda = $null +try { + $smiOut = nvidia-smi 2>&1 | Out-String + if ($smiOut -match "CUDA Version:\s+([\d]+)\.([\d]+)") { + $DriverMaxCuda = "$($Matches[1]).$($Matches[2])" + Write-Host " Driver supports up to CUDA $DriverMaxCuda" -ForegroundColor Gray + } +} catch {} + +# -- Find a toolkit that's compatible with the driver -- +$IncompatibleToolkit = $null +if ($DriverMaxCuda) { + $NvccPath = Find-Nvcc -MaxVersion $DriverMaxCuda + if ($NvccPath) { + Write-Host " [OK] Found compatible CUDA Toolkit (nvcc: $NvccPath)" -ForegroundColor Green + } else { + # Check if there's an incompatible (too new) toolkit installed + $AnyNvcc = Find-Nvcc + if ($AnyNvcc) { + $NvccOut = & $AnyNvcc --version 2>&1 | Out-String + if ($NvccOut -match "release\s+([\d]+\.[\d]+)") { + $IncompatibleToolkit = $Matches[1] + } + } + } +} else { + $NvccPath = Find-Nvcc +} + +# -- If incompatible toolkit is blocking, tell user to uninstall it -- +if (-not $NvccPath -and $IncompatibleToolkit) { + Write-Host "" -ForegroundColor Red + Write-Host "========================================================================" -ForegroundColor Red + Write-Host "[ERROR] CUDA Toolkit $IncompatibleToolkit is installed but INCOMPATIBLE" -ForegroundColor Red + Write-Host " with your NVIDIA driver (which supports up to CUDA $DriverMaxCuda)." -ForegroundColor Red + Write-Host "" -ForegroundColor Red + Write-Host " This will cause 'failed to initialize CUDA' errors at runtime." -ForegroundColor Red + Write-Host "" -ForegroundColor Red + Write-Host " To fix:" -ForegroundColor Yellow + Write-Host " 1. Open Control Panel -> Programs -> Uninstall a program" -ForegroundColor Yellow + Write-Host " 2. Uninstall 'NVIDIA CUDA Toolkit $IncompatibleToolkit'" -ForegroundColor Yellow + Write-Host " 3. Re-run setup.bat (it will install CUDA $DriverMaxCuda automatically)" -ForegroundColor Yellow + Write-Host "" -ForegroundColor Yellow + Write-Host " Alternatively, update your NVIDIA driver to one that supports CUDA $IncompatibleToolkit." -ForegroundColor Gray + Write-Host "========================================================================" -ForegroundColor Red + exit 1 +} + +# -- No toolkit at all: install via winget -- +if (-not $NvccPath) { + Write-Host "CUDA toolkit (nvcc) not found -- installing via winget..." -ForegroundColor Yellow + $HasWinget = $null -ne (Get-Command winget -ErrorAction SilentlyContinue) + if ($HasWinget) { + if ($DriverMaxCuda) { + # Try descending compatible versions + $drMajor = [int]$DriverMaxCuda.Split('.')[0] + $drMinor = [int]$DriverMaxCuda.Split('.')[1] + for ($m = $drMinor; $m -ge 0; $m--) { + $ver = "$drMajor.$m" + Write-Host " Trying CUDA Toolkit $ver via winget..." -ForegroundColor Cyan + $prevEAPCuda = $ErrorActionPreference + $ErrorActionPreference = "Continue" + winget install --id=Nvidia.CUDA --version=$ver -e --source winget --accept-package-agreements --accept-source-agreements 2>&1 | Out-Null + $ErrorActionPreference = $prevEAPCuda + Refresh-Environment + $NvccPath = Find-Nvcc -MaxVersion $DriverMaxCuda + if ($NvccPath) { + Write-Host " [OK] CUDA Toolkit $ver installed (nvcc: $NvccPath)" -ForegroundColor Green + break + } + } + } else { + Write-Host " Installing CUDA Toolkit (latest) via winget..." -ForegroundColor Cyan + winget install --id=Nvidia.CUDA -e --source winget --accept-package-agreements --accept-source-agreements + Refresh-Environment + $NvccPath = Find-Nvcc + if ($NvccPath) { + Write-Host " [OK] CUDA Toolkit installed (nvcc: $NvccPath)" -ForegroundColor Green + } + } + } +} + +if (-not $NvccPath) { + Write-Host "[ERROR] CUDA Toolkit (nvcc) is required but could not be found or installed." -ForegroundColor Red + if ($DriverMaxCuda) { + Write-Host " Install CUDA Toolkit $DriverMaxCuda from https://developer.nvidia.com/cuda-toolkit-archive" -ForegroundColor Yellow + } else { + Write-Host " Install CUDA Toolkit from https://developer.nvidia.com/cuda-downloads" -ForegroundColor Yellow + } + exit 1 +} + +# -- Set CUDA env vars so cmake AND MSBuild can find the toolkit -- +$CudaToolkitRoot = Split-Path (Split-Path $NvccPath -Parent) -Parent +# CUDA_PATH: used by cmake's find_package(CUDAToolkit) +[Environment]::SetEnvironmentVariable('CUDA_PATH', $CudaToolkitRoot, 'Process') +# CudaToolkitDir: the MSBuild property that CUDA .targets checks directly +# Trailing backslash required -- the .targets file appends subpaths to it +[Environment]::SetEnvironmentVariable('CudaToolkitDir', "$CudaToolkitRoot\", 'Process') +# Always persist CUDA_PATH to User registry so the compatible toolkit is used +# in future sessions (overwrites any existing value pointing to a newer, incompatible version) +[Environment]::SetEnvironmentVariable('CUDA_PATH', $CudaToolkitRoot, 'User') +Write-Host " Persisted CUDA_PATH=$CudaToolkitRoot to user environment" -ForegroundColor Gray +# Ensure nvcc's bin dir is on PATH for this process +$nvccBinDir = Split-Path $NvccPath -Parent +if ($env:PATH -notlike "*$nvccBinDir*") { + [Environment]::SetEnvironmentVariable('PATH', "$nvccBinDir;$env:PATH", 'Process') +} +# Persist nvcc bin dir to User PATH so it works in new terminals +$userPath = [Environment]::GetEnvironmentVariable('Path', 'User') +if (-not $userPath -or $userPath -notlike "*$nvccBinDir*") { + if ($userPath) { + [Environment]::SetEnvironmentVariable('Path', "$nvccBinDir;$userPath", 'User') + } else { + [Environment]::SetEnvironmentVariable('Path', "$nvccBinDir", 'User') + } + Write-Host " Persisted CUDA bin dir to user PATH" -ForegroundColor Gray +} + +Write-Host "[OK] CUDA Toolkit: $NvccPath" -ForegroundColor Green +Write-Host " CUDA_PATH = $CudaToolkitRoot" -ForegroundColor Gray +Write-Host " CudaToolkitDir = $CudaToolkitRoot\" -ForegroundColor Gray + +# Detect compute capability (used later for llama.cpp cmake) +$CudaArch = Get-CudaComputeCapability +if ($CudaArch) { + Write-Host " Compute Capability = $($CudaArch.Insert($CudaArch.Length-1, '.')) (sm_$CudaArch)" -ForegroundColor Gray +} else { + Write-Host " [WARN] Could not detect compute capability -- cmake will use defaults" -ForegroundColor Yellow +} + +# ============================================ +# 1f. Node.js / npm (always -- needed regardless of install method) +# ============================================ +# setup.sh installs Node LTS (v22) via nvm. We enforce the same range here: +# Node >= 20, npm >= 11. +$NeedNode = $true +try { + $NodeVersion = (node -v 2>$null) + $NpmVersion = (npm -v 2>$null) + if ($NodeVersion -and $NpmVersion) { + $NodeMajor = [int]($NodeVersion -replace 'v','').Split('.')[0] + $NpmMajor = [int]$NpmVersion.Split('.')[0] + + if ($NodeMajor -ge 20 -and $NpmMajor -ge 11) { + Write-Host "[OK] Node $NodeVersion and npm $NpmVersion already meet requirements." -ForegroundColor Green + $NeedNode = $false + } else { + Write-Host "[WARN] Node $NodeVersion / npm $NpmVersion too old." -ForegroundColor Yellow + } + } +} catch { + Write-Host "[WARN] Node/npm not found." -ForegroundColor Yellow +} + +if ($NeedNode) { + Write-Host "Installing Node.js LTS via winget..." -ForegroundColor Cyan + try { + winget install OpenJS.NodeJS.LTS --source winget --accept-package-agreements --accept-source-agreements + Refresh-Environment + } catch { + Write-Host "[ERROR] Could not install Node.js automatically." -ForegroundColor Red + Write-Host "Please install Node.js >= 20 from https://nodejs.org/" -ForegroundColor Red + exit 1 + } +} + +Write-Host "[OK] Node $(node -v) | npm $(npm -v)" -ForegroundColor Green + +# ============================================ +# 1g. Python (>= 3.11 and < 3.14, matching setup.sh) +# ============================================ +$HasPython = $null -ne (Get-Command python -ErrorAction SilentlyContinue) +$PythonOk = $false + +if ($HasPython) { + $PyVer = python --version 2>&1 + if ($PyVer -match "(\d+)\.(\d+)") { + $PyMajor = [int]$Matches[1]; $PyMinor = [int]$Matches[2] + if ($PyMajor -eq 3 -and $PyMinor -ge 11 -and $PyMinor -lt 14) { + Write-Host "[OK] Python $PyVer" -ForegroundColor Green + $PythonOk = $true + } else { + Write-Host "[ERROR] Python $PyVer is outside supported range (need >= 3.11 and < 3.14)." -ForegroundColor Red + Write-Host " Install Python 3.12 from https://python.org/downloads/" -ForegroundColor Yellow + exit 1 + } + } +} else { + # No Python at all -- install 3.12 + Write-Host "Python not found -- installing Python 3.12 via winget..." -ForegroundColor Yellow + $HasWinget = $null -ne (Get-Command winget -ErrorAction SilentlyContinue) + if ($HasWinget) { + winget install -e --id Python.Python.3.12 --source winget --accept-package-agreements --accept-source-agreements + Refresh-Environment + } + $HasPython = $null -ne (Get-Command python -ErrorAction SilentlyContinue) + if (-not $HasPython) { + Write-Host "[ERROR] Python could not be installed automatically." -ForegroundColor Red + Write-Host " Install Python 3.12 from https://python.org/downloads/" -ForegroundColor Yellow + exit 1 + } + Write-Host "[OK] Python $(python --version)" -ForegroundColor Green + $PythonOk = $true +} + +Write-Host "" +Write-Host "--- System prerequisites ready ---" -ForegroundColor Green +Write-Host "" + +# ========================================================================== +# PHASE 2: Frontend build (skip if pip-installed -- already bundled) +# ========================================================================== +if ($IsPipInstall) { + Write-Host "[OK] Running from pip install - frontend already bundled, skipping build" -ForegroundColor Green +} else { + Write-Host "" + Write-Host "Building frontend..." -ForegroundColor Cyan + # npm writes warnings to stderr; lower ErrorActionPreference so PS doesn't + # treat them as terminating errors (same pattern as the pip section below). + $prevEAP_npm = $ErrorActionPreference + $ErrorActionPreference = "Continue" + Push-Location $FrontendDir + # Remove stale node_modules and package-lock.json to avoid version conflicts + if (Test-Path "node_modules") { Remove-Item -Recurse -Force "node_modules" } + if (Test-Path "package-lock.json") { Remove-Item -Force "package-lock.json" } + npm install 2>&1 | Out-Null + if ($LASTEXITCODE -ne 0) { + Pop-Location + $ErrorActionPreference = $prevEAP_npm + Write-Host "[ERROR] npm install failed (exit code $LASTEXITCODE)" -ForegroundColor Red + Write-Host " Try running 'npm install' manually in studio/frontend/ to see errors" -ForegroundColor Yellow + exit 1 + } + npm run build 2>&1 | Out-Null + if ($LASTEXITCODE -ne 0) { + Pop-Location + $ErrorActionPreference = $prevEAP_npm + Write-Host "[ERROR] npm run build failed (exit code $LASTEXITCODE)" -ForegroundColor Red + exit 1 + } + Pop-Location + $ErrorActionPreference = $prevEAP_npm + Write-Host "[OK] Frontend built to studio/frontend/dist" -ForegroundColor Green +} + +# ========================================================================== +# PHASE 3: Python environment + dependencies +# ========================================================================== +Write-Host "" +Write-Host "Setting up Python environment..." -ForegroundColor Cyan + +# Find Python +$PythonCmd = $null +foreach ($candidate in @("python3.12", "python3.11", "python3.10", "python3.9", "python3", "python")) { + try { + $ver = & $candidate --version 2>&1 + if ($ver -match 'Python 3\.(\d+)') { + $minor = [int]$Matches[1] + if ($minor -le 12) { + $PythonCmd = $candidate + break + } + } + } catch { } +} + +if (-not $PythonCmd) { + Write-Host "[ERROR] No Python <= 3.12 found." -ForegroundColor Red + exit 1 +} + +Write-Host "[OK] Using $PythonCmd ($(& $PythonCmd --version 2>&1))" -ForegroundColor Green + +# Always create a .venv for isolation -- even for pip installs. +# Created in the current working directory (where user ran the command). +$VenvDir = Join-Path (Get-Location) ".venv" +if (-not (Test-Path $VenvDir)) { + Write-Host " Creating virtual environment at $VenvDir..." -ForegroundColor Cyan + & $PythonCmd -m venv $VenvDir +} else { + Write-Host " Reusing existing virtual environment at $VenvDir" -ForegroundColor Green +} + +# pip and python write to stderr even on success (progress bars, warnings). +# With $ErrorActionPreference = "Stop" (set at top of script), PS 5.1 +# converts stderr lines into terminating ErrorRecords, breaking output. +# Lower to "Continue" for the pip/python section. +$prevEAP = $ErrorActionPreference +$ErrorActionPreference = "Continue" + +$ActivateScript = Join-Path $VenvDir "Scripts\Activate.ps1" +. $ActivateScript +pip install --upgrade pip 2>&1 | Out-Null + +# if (-not $IsPipInstall) { +# # Running from repo: copy requirements and do editable install +# $RepoRoot = (Resolve-Path (Join-Path $ScriptDir "..\..")).Path +# $ReqsSrc = Join-Path $RepoRoot "backend\requirements" +# $ReqsDst = Join-Path $PackageDir "requirements" +# if (-not (Test-Path $ReqsDst)) { New-Item -ItemType Directory -Path $ReqsDst | Out-Null } +# Copy-Item (Join-Path $ReqsSrc "*.txt") $ReqsDst -Force + +# Write-Host " Installing CLI entry point..." -ForegroundColor Cyan +# pip install -e $RepoRoot 2>&1 | Out-Null +# } else { +# # Running from pip install: the package is in system Python but not in +# # the fresh .venv. Install it so run_install() can find its modules +# # and bundled requirements files. +# Write-Host " Installing package into venv..." -ForegroundColor Cyan +# pip install unsloth-roland-test 2>&1 | Out-Null +# } + +# Pre-install PyTorch with CUDA support. +# On Windows, the default PyPI torch wheel is CPU-only. +# We need PyTorch's CUDA index to get GPU-enabled wheels. +# PyTorch bundles its own CUDA runtime, so this works regardless +# of whether the CUDA Toolkit is installed yet. +# The CUDA tag is chosen based on the driver's max supported CUDA version. + +# Windows MAX_PATH (260 chars) causes Triton kernel compilation to fail because +# the auto-generated filenames are extremely long. Use a short cache directory. +$TorchCacheDir = "C:\tc" +if (-not (Test-Path $TorchCacheDir)) { New-Item -ItemType Directory -Path $TorchCacheDir -Force | Out-Null } +$env:TORCHINDUCTOR_CACHE_DIR = $TorchCacheDir +[Environment]::SetEnvironmentVariable('TORCHINDUCTOR_CACHE_DIR', $TorchCacheDir, 'User') +Write-Host "[OK] TORCHINDUCTOR_CACHE_DIR set to $TorchCacheDir (avoids MAX_PATH issues)" -ForegroundColor Green + +$CuTag = Get-PytorchCudaTag +Write-Host " Installing PyTorch with CUDA support ($CuTag)..." -ForegroundColor Cyan +pip install torch torchvision torchaudio --index-url "https://download.pytorch.org/whl/$CuTag" 2>&1 | Out-Null + +# Ordered heavy dependency installation — shared cross-platform script +Write-Host " Running ordered dependency installation..." -ForegroundColor Cyan +python "$PSScriptRoot\install_python_stack.py" +# Restore ErrorActionPreference after pip/python work +$ErrorActionPreference = $prevEAP + +# ========================================================================== +# PHASE 3.5: Install OpenSSL dev (for HTTPS support in llama-server) +# ========================================================================== +# llama-server needs OpenSSL to download models from HuggingFace via -hf. +# ShiningLight.OpenSSL.Dev includes headers + libs that cmake can find. +$OpenSslAvailable = $false + +# Check if OpenSSL dev is already installed (look for include dir) +$OpenSslRoots = @( + 'C:\Program Files\OpenSSL-Win64', + 'C:\Program Files\OpenSSL', + 'C:\OpenSSL-Win64' +) +$OpenSslRoot = $null +foreach ($root in $OpenSslRoots) { + if (Test-Path (Join-Path $root 'include\openssl\ssl.h')) { + $OpenSslRoot = $root + break + } +} + +if ($OpenSslRoot) { + $OpenSslAvailable = $true + Write-Host "[OK] OpenSSL dev found at $OpenSslRoot" -ForegroundColor Green +} else { + Write-Host "" + Write-Host "Installing OpenSSL dev (for HTTPS in llama-server)..." -ForegroundColor Cyan + $HasWinget = $null -ne (Get-Command winget -ErrorAction SilentlyContinue) + if ($HasWinget) { + winget install -e --id ShiningLight.OpenSSL.Dev --accept-package-agreements --accept-source-agreements + # Re-check after install + foreach ($root in $OpenSslRoots) { + if (Test-Path (Join-Path $root 'include\openssl\ssl.h')) { + $OpenSslRoot = $root + $OpenSslAvailable = $true + Write-Host "[OK] OpenSSL dev installed at $OpenSslRoot" -ForegroundColor Green + break + } + } + } + if (-not $OpenSslAvailable) { + Write-Host "[WARN] OpenSSL dev not available -- llama-server will be built without HTTPS" -ForegroundColor Yellow + } +} + +# ========================================================================== +# PHASE 4: Build llama.cpp with CUDA for GGUF inference + export +# ========================================================================== +# Builds at ~/.unsloth/llama.cpp — a single shared location under the user's +# home directory. This is used by both the inference server and the GGUF +# export pipeline (unsloth-zoo). +# We build: +# - llama-server: for GGUF model inference (with HTTPS if OpenSSL available) +# - llama-quantize: for GGUF export quantization +# Prerequisites (git, cmake, VS Build Tools, CUDA Toolkit) already installed in Phase 1. +$UnslothHome = Join-Path $env:USERPROFILE ".unsloth" +if (-not (Test-Path $UnslothHome)) { New-Item -ItemType Directory -Force $UnslothHome | Out-Null } +$LlamaCppDir = Join-Path $UnslothHome "llama.cpp" +$BuildDir = Join-Path $LlamaCppDir "build" +$LlamaServerBin = Join-Path $BuildDir "bin\Release\llama-server.exe" + +if (Test-Path $LlamaServerBin) { + Write-Host "" + Write-Host "[OK] llama-server already exists at $LlamaServerBin" -ForegroundColor Green +} else { + Write-Host "" + Write-Host "Building llama.cpp with CUDA support..." -ForegroundColor Cyan + Write-Host " This typically takes 5-10 minutes on first build." -ForegroundColor Gray + Write-Host "" + + # Start total build timer + $totalSw = [System.Diagnostics.Stopwatch]::StartNew() + + # Native commands (git, cmake) write to stderr even on success. + # With $ErrorActionPreference = "Stop" (set at top of script), PS 5.1 + # converts stderr lines into terminating ErrorRecords, breaking output. + # Lower to "Continue" for the build section. + $prevEAP = $ErrorActionPreference + $ErrorActionPreference = "Continue" + + $BuildOk = $true + $FailedStep = "" + + # -- Step A: Clone or pull llama.cpp -- + + if (Test-Path (Join-Path $LlamaCppDir ".git")) { + Write-Host " llama.cpp repo already cloned, pulling latest..." -ForegroundColor Gray + git -C $LlamaCppDir pull + if ($LASTEXITCODE -ne 0) { + Write-Host " [WARN] git pull failed -- using existing source" -ForegroundColor Yellow + } + } else { + Write-Host " Cloning llama.cpp..." -ForegroundColor Gray + if (Test-Path $LlamaCppDir) { Remove-Item -Recurse -Force $LlamaCppDir } + git clone --depth 1 https://github.com/ggml-org/llama.cpp.git $LlamaCppDir + if ($LASTEXITCODE -ne 0) { + $BuildOk = $false + $FailedStep = "git clone" + } + } + + # -- Step B: cmake configure (CUDA + Unsloth flags) -- + if ($BuildOk) { + Write-Host "" + Write-Host "--- cmake configure ---" -ForegroundColor Cyan + + $CmakeArgs = @( + '-S', $LlamaCppDir, + '-B', $BuildDir, + '-G', $CmakeGenerator, + '-Wno-dev' + ) + # Tell cmake exactly where VS is (bypasses registry lookup) + if ($VsInstallPath) { + $CmakeArgs += "-DCMAKE_GENERATOR_INSTANCE=$VsInstallPath" + } + # Common flags + $CmakeArgs += '-DBUILD_SHARED_LIBS=OFF' + # HTTPS support via OpenSSL + if ($OpenSslAvailable -and $OpenSslRoot) { + $CmakeArgs += "-DOPENSSL_ROOT_DIR=$OpenSslRoot" + $CmakeArgs += '-DLLAMA_OPENSSL=ON' + } else { + $CmakeArgs += '-DLLAMA_CURL=OFF' + } + $CmakeArgs += '-DCMAKE_EXE_LINKER_FLAGS=/NODEFAULTLIB:LIBCMT' + # CUDA flags (Unsloth-aligned) + $CmakeArgs += '-DGGML_CUDA=ON' + $CmakeArgs += "-DCUDAToolkit_ROOT=$CudaToolkitRoot" + $CmakeArgs += "-DCMAKE_CUDA_COMPILER=$NvccPath" + $CmakeArgs += '-DGGML_CUDA_FA_ALL_QUANTS=ON' + $CmakeArgs += '-DGGML_CUDA_F16=OFF' + $CmakeArgs += '-DGGML_CUDA_GRAPHS=OFF' + $CmakeArgs += '-DGGML_CUDA_FORCE_CUBLAS=OFF' + $CmakeArgs += '-DGGML_CUDA_PEER_MAX_BATCH_SIZE=8192' + if ($CudaArch) { + $CmakeArgs += "-DCMAKE_CUDA_ARCHITECTURES=$CudaArch" + } + + Write-Host " cmake args:" -ForegroundColor Gray + foreach ($arg in $CmakeArgs) { + Write-Host " $arg" -ForegroundColor Gray + } + Write-Host "" + + cmake @CmakeArgs + if ($LASTEXITCODE -ne 0) { + $BuildOk = $false + $FailedStep = "cmake configure" + } + } + + # -- Step C: Build llama-server -- + $NumCpu = [Environment]::ProcessorCount + if ($NumCpu -lt 1) { $NumCpu = 4 } + + if ($BuildOk) { + Write-Host "" + Write-Host "--- cmake build (llama-server) ---" -ForegroundColor Cyan + Write-Host " Parallel jobs: $NumCpu" -ForegroundColor Gray + Write-Host "" + + cmake --build $BuildDir --config Release --target llama-server -j $NumCpu + if ($LASTEXITCODE -ne 0) { + $BuildOk = $false + $FailedStep = "cmake build (llama-server)" + } + } + + # -- Step D: Build llama-quantize (optional, best-effort) -- + if ($BuildOk) { + Write-Host "" + Write-Host "--- cmake build (llama-quantize) ---" -ForegroundColor Cyan + cmake --build $BuildDir --config Release --target llama-quantize -j $NumCpu + if ($LASTEXITCODE -ne 0) { + Write-Host " [WARN] llama-quantize build failed (GGUF export may be unavailable)" -ForegroundColor Yellow + } + } + + # Restore ErrorActionPreference + $ErrorActionPreference = $prevEAP + + # Stop timer + $totalSw.Stop() + $totalMin = [math]::Floor($totalSw.Elapsed.TotalMinutes) + $totalSec = [math]::Round($totalSw.Elapsed.TotalSeconds % 60, 1) + + # -- Summary -- + Write-Host "" + if ($BuildOk -and (Test-Path $LlamaServerBin)) { + Write-Host "[OK] llama-server built at $LlamaServerBin" -ForegroundColor Green + $QuantizeBin = Join-Path $BuildDir "bin\Release\llama-quantize.exe" + if (Test-Path $QuantizeBin) { + Write-Host "[OK] llama-quantize available for GGUF export" -ForegroundColor Green + } + Write-Host " Build time: ${totalMin}m ${totalSec}s" -ForegroundColor Cyan + } else { + # Check alternate paths (some cmake generators don't use Release subdir) + $altBin = Join-Path $BuildDir "bin\llama-server.exe" + if ($BuildOk -and (Test-Path $altBin)) { + Write-Host "[OK] llama-server built at $altBin" -ForegroundColor Green + Write-Host " Build time: ${totalMin}m ${totalSec}s" -ForegroundColor Cyan + } else { + Write-Host "[FAILED] llama.cpp build failed at step: $FailedStep (${totalMin}m ${totalSec}s)" -ForegroundColor Red + Write-Host " To retry: delete $LlamaCppDir and re-run setup." -ForegroundColor Yellow + exit 1 + } + } +} + +# ============================================ +# Add shell aliases (PowerShell profile + cmd batch files) +# ============================================ +Write-Host "" +$RepoDir = $PSScriptRoot +$VenvPython = Join-Path $RepoDir ".venv\Scripts\python.exe" +$CliScript = Join-Path $RepoDir "cli.py" +$FrontendDist = Join-Path $RepoDir "studio\frontend\dist" +$AliasAdded = $false + +# --- PowerShell profile: add functions --- +$ProfileDir = Split-Path $PROFILE -Parent +if (-not (Test-Path $ProfileDir)) { New-Item -ItemType Directory -Path $ProfileDir -Force | Out-Null } +if (-not (Test-Path $PROFILE)) { New-Item -ItemType File -Path $PROFILE -Force | Out-Null } + +if (-not (Select-String -Path $PROFILE -Pattern "unsloth-studio" -Quiet -ErrorAction SilentlyContinue)) { + $block = @" + +# Unsloth Studio launcher +function unsloth-studio { & "$VenvPython" "$CliScript" studio -f "$FrontendDist" @args } +function unsloth-ui { & "$VenvPython" "$CliScript" studio -f "$FrontendDist" @args } +"@ + Add-Content -Path $PROFILE -Value $block + Write-Host "[OK] Aliases 'unsloth-studio' and 'unsloth-ui' added to $PROFILE" -ForegroundColor Green + $AliasAdded = $true +} else { + Write-Host "[OK] Aliases 'unsloth-studio' and 'unsloth-ui' already exist in $PROFILE" -ForegroundColor Green +} + +# --- cmd.exe: create batch files and ensure they're on PATH --- +$BatDir = Join-Path $RepoDir ".venv\Scripts" +foreach ($name in @("unsloth-studio", "unsloth-ui")) { + $batPath = Join-Path $BatDir "$name.bat" + if (-not (Test-Path $batPath)) { + Set-Content -Path $batPath -Value "@echo off`r`n`"$VenvPython`" `"$CliScript`" studio -f `"$FrontendDist`" %*" + } +} +# Persist .venv\Scripts to User PATH so commands work in new cmd.exe terminals without activation +$userPath = [Environment]::GetEnvironmentVariable('Path', 'User') +if (-not $userPath -or $userPath -notlike "*$BatDir*") { + if ($userPath) { + [Environment]::SetEnvironmentVariable('Path', "$BatDir;$userPath", 'User') + } else { + [Environment]::SetEnvironmentVariable('Path', "$BatDir", 'User') + } + Write-Host " Persisted $BatDir to User PATH" -ForegroundColor Gray +} +Write-Host "[OK] Batch launchers created (works from any new cmd.exe or PowerShell)" -ForegroundColor Green + +# ============================================ +# Done +# ============================================ +Write-Host "" +Write-Host "+===============================================+" -ForegroundColor Green +Write-Host "| Setup Complete! |" -ForegroundColor Green +Write-Host "| |" -ForegroundColor Green +Write-Host "| IMPORTANT: Open a NEW terminal, then run: |" -ForegroundColor Yellow +Write-Host "| |" -ForegroundColor Green +Write-Host "| unsloth-studio -H 0.0.0.0 -p 8000 |" -ForegroundColor Green +Write-Host "| |" -ForegroundColor Green +Write-Host "+===============================================+" -ForegroundColor Green \ No newline at end of file diff --git a/setup.sh b/setup.sh index 1fee994a56..f1622ed7a7 100755 --- a/setup.sh +++ b/setup.sh @@ -69,13 +69,14 @@ if [ "$NEED_NODE" = true ]; then # Load nvm (source ~/.bashrc won't work inside a script) export NVM_DIR="$HOME/.nvm" + set +u [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # ── 3. Install Node LTS ── echo "Installing Node LTS..." run_quiet "nvm install" nvm install --lts nvm use --lts > /dev/null 2>&1 - + set -u # ── 4. Verify versions ── NODE_MAJOR=$(node -v | sed 's/v//' | cut -d. -f1) NPM_MAJOR=$(npm -v | cut -d. -f1) @@ -105,7 +106,9 @@ echo "✅ Frontend built to studio/frontend/dist" echo "" echo "Setting up Python environment..." -# ── 6a. Discover best Python <= 3.12.x ── +# ── 6a. Discover best Python >= 3.11 and < 3.14 (i.e. 3.11.x, 3.12.x, or 3.13.x) ── +MIN_PY_MINOR=11 # minimum minor version (>= 3.11) +MAX_PY_MINOR=13 # maximum minor version (< 3.14) BEST_PY="" BEST_MAJOR=0 BEST_MINOR=0 @@ -115,7 +118,7 @@ for candidate in $(compgen -c python3 2>/dev/null | grep -E '^python3(\.[0-9]+)? if ! command -v "$candidate" &>/dev/null; then continue fi - # Get version string, e.g. "Python 3.11.5" + # Get version string, e.g. "Python 3.12.5" ver_str=$("$candidate" --version 2>&1 | awk '{print $2}') py_major=$(echo "$ver_str" | cut -d. -f1) py_minor=$(echo "$ver_str" | cut -d. -f2) @@ -125,8 +128,13 @@ for candidate in $(compgen -c python3 2>/dev/null | grep -E '^python3(\.[0-9]+)? continue fi - # Skip versions above 3.12 - if [ "$py_minor" -gt 12 ] 2>/dev/null; then + # Skip versions below 3.12 (require > 3.11) + if [ "$py_minor" -lt "$MIN_PY_MINOR" ] 2>/dev/null; then + continue + fi + + # Skip versions above 3.13 (require < 3.14) + if [ "$py_minor" -gt "$MAX_PY_MINOR" ] 2>/dev/null; then continue fi @@ -139,7 +147,7 @@ for candidate in $(compgen -c python3 2>/dev/null | grep -E '^python3(\.[0-9]+)? done if [ -z "$BEST_PY" ]; then - echo "❌ ERROR: No Python version <= 3.12.x found on this system." + echo "❌ ERROR: No Python version between 3.${MIN_PY_MINOR} and 3.${MAX_PY_MINOR} found on this system." echo " Detected Python 3 installations:" for candidate in $(compgen -c python3 2>/dev/null | grep -E '^python3(\.[0-9]+)?$' | sort -u); do if command -v "$candidate" &>/dev/null; then @@ -147,52 +155,34 @@ if [ -z "$BEST_PY" ]; then fi done echo "" - echo " Please install Python <= 3.12.x for maximum compatibility." + echo " Please install Python 3.${MIN_PY_MINOR} or 3.${MAX_PY_MINOR}." echo " For example: sudo apt install python3.12 python3.12-venv" exit 1 fi BEST_VER=$("$BEST_PY" --version 2>&1 | awk '{print $2}') -echo "✅ Using $BEST_PY ($BEST_VER) — compatible (≤ 3.12.x)" +echo "✅ Using $BEST_PY ($BEST_VER) — compatible (3.${MIN_PY_MINOR}.x – 3.${MAX_PY_MINOR}.x)" + +REQ_ROOT="$SCRIPT_DIR/studio/backend/requirements" +SINGLE_ENV_CONSTRAINTS="$REQ_ROOT/single-env/constraints.txt" +SINGLE_ENV_DATA_DESIGNER="$REQ_ROOT/single-env/data-designer.txt" +SINGLE_ENV_DATA_DESIGNER_DEPS="$REQ_ROOT/single-env/data-designer-deps.txt" +SINGLE_ENV_PATCH="$REQ_ROOT/single-env/patch_metadata.py" + +install_python_stack() { + python "$SCRIPT_DIR/install_python_stack.py" +} if [ "$IS_COLAB" = true ]; then # Colab: install packages directly without venv - run_quiet "pip upgrade" pip install --upgrade pip - echo " Installing unsloth-zoo + unsloth..." - run_quiet "pip install unsloth" pip install -r "$SCRIPT_DIR/studio/backend/requirements/base.txt" - echo " Installing additional unsloth dependencies..." - run_quiet "pip install extras" pip install --no-cache-dir -r "$SCRIPT_DIR/studio/backend/requirements/extras.txt" - run_quiet "pip install extras" pip install --no-deps --no-cache-dir -r "$SCRIPT_DIR/studio/backend/requirements/extras-no-deps.txt" - run_quiet "pip install torchao+transformers" pip install --force-reinstall --no-cache-dir -r "$SCRIPT_DIR/studio/backend/requirements/overrides.txt" - run_quiet "pip install triton_kernels" pip install --no-deps -r "$SCRIPT_DIR/studio/backend/requirements/triton-kernels.txt" - # Patch: override llama_cpp.py with fix from unsloth-zoo branch - LLAMA_CPP_DST="$(pip show unsloth-zoo | grep -i '^Location:' | awk '{print $2}')/unsloth_zoo/llama_cpp.py" - curl -sSL "https://raw.githubusercontent.com/unslothai/unsloth-zoo/refs/heads/main/unsloth_zoo/llama_cpp.py" \ - -o "$LLAMA_CPP_DST" - echo " Installing studio dependencies..." - run_quiet "pip install studio" pip install -r "$SCRIPT_DIR/studio/backend/requirements/studio.txt" - echo "✅ Python dependencies installed" + install_python_stack else # Local: create venv (always start fresh to preserve correct install order) rm -rf .venv rm -rf .venv_overlay # Clean up stale transformers version overlay "$BEST_PY" -m venv .venv source .venv/bin/activate - run_quiet "pip upgrade" pip install --upgrade pip - echo " Installing unsloth-zoo + unsloth..." - run_quiet "pip install unsloth" pip install -r "$SCRIPT_DIR/studio/backend/requirements/base.txt" - echo " Installing additional unsloth dependencies..." - run_quiet "pip install extras" pip install --no-cache-dir -r "$SCRIPT_DIR/studio/backend/requirements/extras.txt" - run_quiet "pip install extras" pip install --no-deps --no-cache-dir -r "$SCRIPT_DIR/studio/backend/requirements/extras-no-deps.txt" - run_quiet "pip install torchao+transformers" pip install --force-reinstall --no-cache-dir -r "$SCRIPT_DIR/studio/backend/requirements/overrides.txt" - run_quiet "pip install triton_kernels" pip install --no-deps -r "$SCRIPT_DIR/studio/backend/requirements/triton-kernels.txt" - # Patch: override llama_cpp.py with fix from unsloth-zoo branch - LLAMA_CPP_DST="$(pip show unsloth-zoo | grep -i '^Location:' | awk '{print $2}')/unsloth_zoo/llama_cpp.py" - curl -sSL "https://raw.githubusercontent.com/unslothai/unsloth-zoo/refs/heads/main/unsloth_zoo/llama_cpp.py" \ - -o "$LLAMA_CPP_DST" - echo " Installing studio dependencies..." - run_quiet "pip install studio" pip install -r "$SCRIPT_DIR/studio/backend/requirements/studio.txt" - echo "✅ Python dependencies installed" + install_python_stack # ── 7. WSL: pre-install GGUF build dependencies ── # On WSL, sudo requires a password and can't be entered during GGUF export @@ -207,7 +197,93 @@ else fi fi -# ── 8. Add shell alias (skip in Colab) ── +# ── 8. Build llama.cpp binaries for GGUF inference + export ── +# Builds at ~/.unsloth/llama.cpp — a single shared location under the user's +# home directory. This is used by both the inference server and the GGUF +# export pipeline (unsloth-zoo). +# - llama-server: for GGUF model inference +# - llama-quantize: for GGUF export quantization (symlinked to root for check_llama_cpp()) +UNSLOTH_HOME="$HOME/.unsloth" +mkdir -p "$UNSLOTH_HOME" +LLAMA_CPP_DIR="$UNSLOTH_HOME/llama.cpp" +LLAMA_SERVER_BIN="$LLAMA_CPP_DIR/build/bin/llama-server" +rm -rf "$LLAMA_CPP_DIR" +{ + # Check prerequisites + if ! command -v cmake &>/dev/null; then + echo "" + echo "⚠️ cmake not found — skipping llama-server build (GGUF inference won't be available)" + echo " Install cmake and re-run setup.sh to enable GGUF inference." + elif ! command -v git &>/dev/null; then + echo "" + echo "⚠️ git not found — skipping llama-server build (GGUF inference won't be available)" + else + echo "" + echo "Building llama-server for GGUF inference..." + + BUILD_OK=true + run_quiet "clone llama.cpp" git clone --depth 1 https://github.com/ggml-org/llama.cpp.git "$LLAMA_CPP_DIR" || BUILD_OK=false + + if [ "$BUILD_OK" = true ]; then + CMAKE_ARGS="" + # Detect CUDA: check nvcc on PATH, then common install locations + NVCC_PATH="" + if command -v nvcc &>/dev/null; then + NVCC_PATH="$(command -v nvcc)" + elif [ -x /usr/local/cuda/bin/nvcc ]; then + NVCC_PATH="/usr/local/cuda/bin/nvcc" + export PATH="/usr/local/cuda/bin:$PATH" + elif ls /usr/local/cuda-*/bin/nvcc &>/dev/null 2>&1; then + # Pick the newest cuda-XX.X directory + NVCC_PATH="$(ls -d /usr/local/cuda-*/bin/nvcc 2>/dev/null | sort -V | tail -1)" + export PATH="$(dirname "$NVCC_PATH"):$PATH" + fi + + if [ -n "$NVCC_PATH" ]; then + echo " Building with CUDA support (nvcc: $NVCC_PATH)..." + CMAKE_ARGS="-DGGML_CUDA=ON" + elif [ -d /usr/local/cuda ] || nvidia-smi &>/dev/null; then + echo " CUDA driver detected but nvcc not found — building CPU-only" + echo " To enable GPU: install cuda-toolkit or add nvcc to PATH" + else + echo " Building CPU-only (no CUDA detected)..." + fi + + NCPU=$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4) + + run_quiet "cmake llama.cpp" cmake -S "$LLAMA_CPP_DIR" -B "$LLAMA_CPP_DIR/build" $CMAKE_ARGS || BUILD_OK=false + fi + + if [ "$BUILD_OK" = true ]; then + run_quiet "build llama-server" cmake --build "$LLAMA_CPP_DIR/build" --config Release --target llama-server -j"$NCPU" || BUILD_OK=false + fi + + # Also build llama-quantize (needed by unsloth-zoo's GGUF export pipeline) + if [ "$BUILD_OK" = true ]; then + run_quiet "build llama-quantize" cmake --build "$LLAMA_CPP_DIR/build" --config Release --target llama-quantize -j"$NCPU" || true + # Symlink to llama.cpp root — check_llama_cpp() looks for the binary there + QUANTIZE_BIN="$LLAMA_CPP_DIR/build/bin/llama-quantize" + if [ -f "$QUANTIZE_BIN" ]; then + ln -sf build/bin/llama-quantize "$LLAMA_CPP_DIR/llama-quantize" + fi + fi + + if [ "$BUILD_OK" = true ]; then + if [ -f "$LLAMA_SERVER_BIN" ]; then + echo "✅ llama-server built at $LLAMA_SERVER_BIN" + else + echo "⚠️ llama-server binary not found after build — GGUF inference won't be available" + fi + if [ -f "$LLAMA_CPP_DIR/llama-quantize" ]; then + echo "✅ llama-quantize available for GGUF export" + fi + else + echo "⚠️ llama-server build failed — GGUF inference won't be available, but everything else works" + fi + fi +} + +# ── 9. Add shell alias (skip in Colab) ── # Note: venv activation does NOT persist across terminal sessions. # This alias hardcodes the venv python path so users don't need to activate. if [ "$IS_COLAB" = false ]; then diff --git a/studio/backend/auth/authentication.py b/studio/backend/auth/authentication.py index 6ea668db3e..e725834630 100644 --- a/studio/backend/auth/authentication.py +++ b/studio/backend/auth/authentication.py @@ -1,5 +1,5 @@ import secrets -from datetime import UTC, datetime, timedelta +from datetime import datetime, timedelta, timezone from typing import Optional from fastapi import Depends, HTTPException, status @@ -34,7 +34,7 @@ def create_access_token( Tokens are valid across restarts because SECRET_KEY is stored in SQLite. """ to_encode = {"sub": subject} - expire = datetime.now(UTC) + ( + expire = datetime.now(timezone.utc) + ( expires_delta or timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) ) to_encode.update({"exp": expire}) @@ -48,7 +48,7 @@ def create_refresh_token(subject: str) -> str: Refresh tokens are opaque (not JWTs) and expire after REFRESH_TOKEN_EXPIRE_DAYS. """ token = secrets.token_urlsafe(48) - expires_at = datetime.now(UTC) + timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS) + expires_at = datetime.now(timezone.utc) + timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS) save_refresh_token(token, subject, expires_at.isoformat()) return token diff --git a/studio/backend/auth/storage.py b/studio/backend/auth/storage.py index faea6266e3..e5a486bca2 100644 --- a/studio/backend/auth/storage.py +++ b/studio/backend/auth/storage.py @@ -3,7 +3,7 @@ SQLite storage for authentication data (user credentials + JWT secret). """ import hashlib import sqlite3 -from datetime import UTC, datetime +from datetime import datetime, timezone from pathlib import Path from typing import Optional, Tuple @@ -218,7 +218,7 @@ def verify_refresh_token(token: str) -> Optional[str]: # Clean up any expired tokens while we're here conn.execute( "DELETE FROM refresh_tokens WHERE expires_at < ?", - (datetime.now(UTC).isoformat(),), + (datetime.now(timezone.utc).isoformat(),), ) conn.commit() @@ -235,7 +235,7 @@ def verify_refresh_token(token: str) -> Optional[str]: # Check expiry expires_at = datetime.fromisoformat(row["expires_at"]) - if datetime.now(UTC) > expires_at: + if datetime.now(timezone.utc) > expires_at: conn.execute("DELETE FROM refresh_tokens WHERE id = ?", (row["id"],)) conn.commit() return None diff --git a/studio/backend/core/data_recipe/__init__.py b/studio/backend/core/data_recipe/__init__.py new file mode 100644 index 0000000000..0665a8e4c2 --- /dev/null +++ b/studio/backend/core/data_recipe/__init__.py @@ -0,0 +1,7 @@ +""" +Data Recipe core (DataDesigner wrapper + job runner). +""" + +from .jobs import JobManager, get_job_manager + +__all__ = ["JobManager", "get_job_manager"] diff --git a/studio/backend/core/data_recipe/jobs/__init__.py b/studio/backend/core/data_recipe/jobs/__init__.py new file mode 100644 index 0000000000..bac519ee3a --- /dev/null +++ b/studio/backend/core/data_recipe/jobs/__init__.py @@ -0,0 +1,4 @@ +from .manager import JobManager, get_job_manager + +__all__ = ["JobManager", "get_job_manager"] + diff --git a/studio/backend/core/data_recipe/jobs/constants.py b/studio/backend/core/data_recipe/jobs/constants.py new file mode 100644 index 0000000000..5d4081abbc --- /dev/null +++ b/studio/backend/core/data_recipe/jobs/constants.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +# stages parsed from data-designer logs +STAGE_CREATE = "create" +STAGE_PREVIEW = "preview" +STAGE_DAG = "dag" +STAGE_HEALTHCHECK = "healthcheck" +STAGE_SAMPLING = "sampling" +STAGE_COLUMN_CONFIG = "column_config" +STAGE_GENERATING = "generating" +STAGE_BATCH = "batch" +STAGE_PROFILING = "profiling" + +USAGE_RESET_STAGES = { + STAGE_CREATE, + STAGE_PREVIEW, + STAGE_DAG, + STAGE_HEALTHCHECK, + STAGE_SAMPLING, + STAGE_GENERATING, + STAGE_PROFILING, +} + +# job event types emitted by worker/manager +EVENT_JOB_ENQUEUED = "job.enqueued" +EVENT_JOB_STARTED = "job.started" +EVENT_JOB_CANCELLING = "job.cancelling" +EVENT_JOB_CANCELLED = "job.cancelled" +EVENT_JOB_COMPLETED = "job.completed" +EVENT_JOB_ERROR = "job.error" diff --git a/studio/backend/core/data_recipe/jobs/manager.py b/studio/backend/core/data_recipe/jobs/manager.py new file mode 100644 index 0000000000..dbaa620004 --- /dev/null +++ b/studio/backend/core/data_recipe/jobs/manager.py @@ -0,0 +1,452 @@ +from __future__ import annotations + +import asyncio +import json +import queue +import threading +import time +import uuid +from pathlib import Path +from collections import deque +from dataclasses import dataclass +from typing import Any + +import multiprocessing as mp + +from ..jsonable import to_jsonable +from .constants import ( + EVENT_JOB_CANCELLING, + EVENT_JOB_CANCELLED, + EVENT_JOB_COMPLETED, + EVENT_JOB_ENQUEUED, + EVENT_JOB_ERROR, + EVENT_JOB_STARTED, +) +from .parse import apply_update, coerce_event, parse_log_message +from .types import Job +from .worker import run_job_process + + +_CTX = mp.get_context("spawn") + + +@dataclass +class Subscription: + replay: list[dict] + _q: queue.Queue + _next_id: int = 0 + + async def next_event(self, *, timeout_sec: float) -> dict | None: + """Wait for next event (SSE), w/ timeout so we can check disconnects.""" + try: + return await asyncio.to_thread(self._q.get, True, timeout_sec) + except queue.Empty: + return None + + def format_sse(self, event: dict) -> bytes: + """Turn event dict into SSE bytes (id/event/data).""" + event_id = event.get("seq") + if event_id is None: + self._next_id += 1 + event_id = self._next_id + body = json.dumps(event, separators=(",", ":"), ensure_ascii=False) + event_type = event.get("type") or "message" + return ( + f"id: {event_id}\n" + f"event: {event_type}\n" + f"data: {body}\n\n" + ).encode("utf-8") + + +class JobManager: + def __init__(self) -> None: + """Single-job runner (in-mem). Simple on purpose, not a whole platform.""" + self._lock = threading.Lock() + self._job: Job | None = None + self._proc: mp.Process | None = None + self._mp_q: Any | None = None + self._events: deque[dict] = deque(maxlen=5000) + self._subs: list[queue.Queue] = [] + self._pump_thread: threading.Thread | None = None + self._seq: int = 0 + + def start(self, *, recipe: dict, run: dict) -> str: + """Spawn the job subprocess (one at a time, no cap).""" + llm_columns = recipe.get("columns") or [] + llm_column_count = 0 + if isinstance(llm_columns, list): + for column in llm_columns: + if not isinstance(column, dict): + continue + column_type = str(column.get("column_type") or "").strip().lower() + if column_type.startswith("llm"): + llm_column_count += 1 + if llm_column_count <= 0: + llm_column_count = 1 + + with self._lock: + if self._proc is not None and self._proc.is_alive(): + raise RuntimeError("job already running") + + job_id = uuid.uuid4().hex + self._job = Job(job_id=job_id, status="pending", started_at=time.time()) + self._job.progress_columns_total = llm_column_count + self._events.clear() + self._seq = 0 + + run_payload = dict(run) + run_payload["_job_id"] = job_id + mp_q = _CTX.Queue() + proc = _CTX.Process( + target=run_job_process, + kwargs={"event_queue": mp_q, "recipe": recipe, "run": run_payload}, + daemon=True, + ) + proc.start() + + self._mp_q = mp_q + self._proc = proc + self._pump_thread = threading.Thread(target=self._pump_loop, daemon=True) + self._pump_thread.start() + + self._emit({"type": EVENT_JOB_ENQUEUED, "ts": time.time(), "job_id": job_id}) + return job_id + + def cancel(self, job_id: str) -> bool: + """Hard stop. We terminate the subprocess. Quick + reliable.""" + with self._lock: + if self._job is None or self._job.job_id != job_id: + return False + if self._proc is None or not self._proc.is_alive(): + return True + self._job.status = "cancelling" + self._emit({"type": EVENT_JOB_CANCELLING, "ts": time.time(), "job_id": job_id}) + try: + self._proc.terminate() + except (AttributeError, OSError): + pass + return True + + def get_status(self, job_id: str) -> dict | None: + """UI friendly snapshot that we need. Alternative to sse kinda of and structured""" + with self._lock: + if self._job is None or self._job.job_id != job_id: + return None + job = self._job + return { + "job_id": job.job_id, + "status": job.status, + "stage": job.stage, + "current_column": job.current_column, + "batch": {"idx": job.batch.idx, "total": job.batch.total}, + "progress": { + "done": job.progress.done, + "total": job.progress.total, + "percent": job.progress.percent, + "eta_sec": job.progress.eta_sec, + "rate": job.progress.rate, + "ok": job.progress.ok, + "failed": job.progress.failed, + }, + "column_progress": { + "done": job.column_progress.done, + "total": job.column_progress.total, + "percent": job.column_progress.percent, + "eta_sec": job.column_progress.eta_sec, + "rate": job.column_progress.rate, + "ok": job.column_progress.ok, + "failed": job.column_progress.failed, + }, + "model_usage": { + name: { + "model": usage.model, + "tokens": { + "input": usage.input_tokens, + "output": usage.output_tokens, + "total": usage.total_tokens, + "tps": usage.tps, + }, + "requests": { + "success": usage.requests_success, + "failed": usage.requests_failed, + "total": usage.requests_total, + "rpm": usage.rpm, + }, + } + for name, usage in job.model_usage.items() + }, + "rows": job.rows, + "cols": job.cols, + "error": job.error, + "has_analysis": job.analysis is not None, + "dataset_rows": None if job.dataset is None else len(job.dataset), + "artifact_path": job.artifact_path, + "started_at": job.started_at, + "finished_at": job.finished_at, + } + + def get_current_status(self) -> dict | None: + """Single-job convenience (last/current).""" + job_id = self.get_current_job_id() + if job_id is None: + return None + return self.get_status(job_id) + + def get_current_job_id(self) -> str | None: + """Return current job_id (or None).""" + with self._lock: + return None if self._job is None else self._job.job_id + + def get_analysis(self, job_id: str) -> dict | None: + """Final profiling output (only after job completes).""" + with self._lock: + if self._job is None or self._job.job_id != job_id: + return None + return self._job.analysis + + def get_dataset( + self, + job_id: str, + *, + limit: int, + offset: int = 0, + ) -> dict[str, Any] | None: + """Load dataset page (offset + limit) and include total rows.""" + with self._lock: + if self._job is None or self._job.job_id != job_id: + return None + in_memory_dataset = self._job.dataset + artifact_path = self._job.artifact_path + job_status = self._job.status + + if in_memory_dataset is not None: + total = len(in_memory_dataset) + rows = in_memory_dataset[offset:offset + limit] + return {"dataset": rows, "total": total} + if not artifact_path: + if job_status in {"completed", "error", "cancelled"}: + return {"error": "artifact path missing"} + return None + + try: + base_dataset_path = Path(artifact_path) + parquet_dir = base_dataset_path / "parquet-files" + if not parquet_dir.exists(): + return {"error": f"dataset path missing: {parquet_dir}"} + + return self._load_dataset_page(parquet_dir=parquet_dir, limit=limit, offset=offset) + except Exception as exc: + return {"error": f"dataset load failed: {exc}"} + + @staticmethod + def _load_dataset_page( + *, + parquet_dir: Path, + limit: int, + offset: int, + ) -> dict[str, Any]: + dataset_page = JobManager._load_dataset_page_with_duckdb( + parquet_dir=parquet_dir, + limit=limit, + offset=offset, + ) + if dataset_page is not None: + return dataset_page + return JobManager._load_dataset_page_with_data_designer( + parquet_dir=parquet_dir, + limit=limit, + offset=offset, + ) + + @staticmethod + def _load_dataset_page_with_duckdb( + *, + parquet_dir: Path, + limit: int, + offset: int, + ) -> dict[str, Any] | None: + parquet_glob = str((parquet_dir / "*.parquet").resolve()) + try: + import duckdb # type: ignore + except Exception: + return None + + try: + conn = duckdb.connect(":memory:") + try: + total_row = conn.execute( + "SELECT COUNT(*) FROM read_parquet(?)", + [parquet_glob], + ).fetchone() + total = int(total_row[0] if total_row else 0) + dataframe = conn.execute( + ( + "SELECT *, row_number() OVER (PARTITION BY filename) AS __row_num__ " + "FROM read_parquet(?, filename=true) " + "ORDER BY filename, __row_num__ " + "LIMIT ? OFFSET ?" + ), + [parquet_glob, int(limit), int(offset)], + ).fetchdf() + finally: + conn.close() + except (RuntimeError, ValueError, duckdb.Error): + return None + + for helper_col in ("filename", "__row_num__"): + if helper_col in dataframe.columns: + dataframe = dataframe.drop(columns=[helper_col]) + + rows = dataframe.to_dict(orient="records") + return {"dataset": to_jsonable(rows), "total": total} + + @staticmethod + def _load_dataset_page_with_data_designer( + *, + parquet_dir: Path, + limit: int, + offset: int, + ) -> dict[str, Any]: + from data_designer.config.utils.io_helpers import read_parquet_dataset + + dataframe = read_parquet_dataset(parquet_dir) + total = int(len(dataframe.index)) + rows = dataframe.iloc[offset:offset + limit].to_dict(orient="records") + return {"dataset": to_jsonable(rows), "total": total} + + def subscribe(self, job_id: str, *, after_seq: int | None = None) -> Subscription | None: + """SSE subscribe: get replay buffer + live events stream.""" + with self._lock: + if self._job is None or self._job.job_id != job_id: + return None + q: queue.Queue = queue.Queue(maxsize=2000) + self._subs.append(q) + if after_seq is None: + replay = list(self._events) + else: + replay = [e for e in self._events if int(e.get("seq") or 0) > after_seq] + return Subscription(replay=replay, _q=q) + + def unsubscribe(self, sub: Subscription) -> None: + """Drop SSE subscriber (client disconnected).""" + with self._lock: + self._subs = [q for q in self._subs if q is not sub._q] + + def _emit(self, event: dict) -> None: + """Broadcast event to replay buffer + all subscribers.""" + self._seq += 1 + event["seq"] = self._seq + self._events.append(event) + stale: list[queue.Queue] = [] + for q in self._subs: + try: + q.put_nowait(event) + except queue.Full: + stale.append(q) + if stale: + self._subs = [q for q in self._subs if q not in stale] + + def _snapshot(self) -> tuple[Job, mp.Process, Any] | None: + """Grab pointers for the pump loop (avoid holding lock too long).""" + with self._lock: + if self._job is None or self._proc is None or self._mp_q is None: + return None + return self._job, self._proc, self._mp_q + + @staticmethod + def _read_queue_with_timeout(q: Any, *, timeout_sec: float) -> dict | None: + """Try read 1 event from mp queue. Timeout = pump stays responsive.""" + try: + return coerce_event(q.get(timeout=timeout_sec)) + except queue.Empty: + return None + except (EOFError, OSError, ValueError): + return None + + @staticmethod + def _drain_queue(q: Any) -> list[dict]: + """Drain mp queue fast (used on process exit).""" + events: list[dict] = [] + while True: + try: + events.append(coerce_event(q.get_nowait())) + except queue.Empty: + return events + except (EOFError, OSError, ValueError): + return events + + def _pump_loop(self) -> None: + """Background thread: consumes worker events + updates job snapshot.""" + while True: + snap = self._snapshot() + if snap is None: + return + job, proc, mp_q = snap + + event = self._read_queue_with_timeout(mp_q, timeout_sec=0.25) + if event is not None: + self._handle_event(job, event) + continue + + if proc.is_alive(): + continue + + for e in self._drain_queue(mp_q): + self._handle_event(job, e) + + with self._lock: + if self._job and self._job.status in {"pending", "active", "cancelling"}: + if self._job.status == "cancelling": + self._job.status = "cancelled" + else: + self._job.status = "error" + self._job.error = self._job.error or "process exited" + self._job.finished_at = time.time() + event_type = ( + EVENT_JOB_CANCELLED if self._job.status == "cancelled" else EVENT_JOB_ERROR + ) + self._emit({"type": event_type, "ts": time.time(), "job_id": self._job.job_id}) + return + + def _handle_event(self, job: Job, event: dict) -> None: + """Apply event -> job state + forward to SSE.""" + et = event.get("type") + msg = event.get("message") if et == "log" else None + + with self._lock: + if self._job is None or self._job.job_id != job.job_id: + return + if et == EVENT_JOB_STARTED: + self._job.status = "active" + if et == EVENT_JOB_COMPLETED: + self._job.status = "completed" + self._job.finished_at = time.time() + self._job.analysis = event.get("analysis") + self._job.artifact_path = event.get("artifact_path") + self._job.dataset = event.get("dataset") + self._job.processor_artifacts = event.get("processor_artifacts") + if self._job.progress.total and self._job.progress.total > 0: + self._job.progress.done = self._job.progress.total + self._job.progress.percent = 100.0 + if et == EVENT_JOB_ERROR: + self._job.status = "error" + self._job.finished_at = time.time() + self._job.error = event.get("error") or "error" + + if msg: + upd = parse_log_message(msg) + if upd: + apply_update(self._job, upd) + + self._emit(event) + + +_JOB_MANAGER: JobManager | None = None + + +def get_job_manager() -> JobManager: + """Singleton JobManager (we only run 1 job anyway).""" + global _JOB_MANAGER + if _JOB_MANAGER is None: + _JOB_MANAGER = JobManager() + return _JOB_MANAGER diff --git a/studio/backend/core/data_recipe/jobs/parse.py b/studio/backend/core/data_recipe/jobs/parse.py new file mode 100644 index 0000000000..6e2142adf2 --- /dev/null +++ b/studio/backend/core/data_recipe/jobs/parse.py @@ -0,0 +1,244 @@ +from __future__ import annotations + +import re +from dataclasses import dataclass +from typing import Any + +from .constants import ( + STAGE_BATCH, + STAGE_COLUMN_CONFIG, + STAGE_CREATE, + STAGE_DAG, + STAGE_GENERATING, + STAGE_HEALTHCHECK, + STAGE_PREVIEW, + STAGE_PROFILING, + STAGE_SAMPLING, + USAGE_RESET_STAGES, +) +from .types import Job, ModelUsage, Progress + + +@dataclass(frozen=True) +class ParsedUpdate: + stage: str | None = None + current_column: str | None = None + progress: Progress | None = None + rows: int | None = None + cols: int | None = None + batch_idx: int | None = None + batch_total: int | None = None + usage_model: str | None = None + usage_input_tokens: int | None = None + usage_output_tokens: int | None = None + usage_total_tokens: int | None = None + usage_tps: float | None = None + usage_requests_success: int | None = None + usage_requests_failed: int | None = None + usage_requests_total: int | None = None + usage_rpm: float | None = None + usage_section_start: bool | None = None + +# kinda of a bummber but currently only option, Best effort parser from data-designer logs -> structured status for UI. +_RE_SAMPLERS = re.compile( + r"Preparing samplers to generate (?P\d+) records across (?P\d+) columns" +) +_RE_COLCFG = re.compile(r"model config for column '(?P[^']+)'") +_RE_PROCESSING_COL = re.compile(r"Processing .* column '(?P[^']+)'") +_RE_PROGRESS = re.compile( + r"progress: (?P\d+)/(?P\d+) \((?P\d+)%\) complete, " + r"(?P\d+) ok, (?P\d+) failed, (?P[0-9.]+) rec/s, eta (?P[0-9.]+)s" +) +_RE_BATCH = re.compile(r"Processing batch (?P\d+) of (?P\d+)") +_RE_USAGE_MODEL = re.compile(r"model:\s*(?P.+)$") +_RE_USAGE_TOKENS = re.compile( + r"tokens:\s*input=(?P\d+),\s*output=(?P\d+),\s*total=(?P\d+),\s*tps=(?P[0-9.]+)" +) +_RE_USAGE_REQUESTS = re.compile( + r"requests:\s*success=(?P\d+),\s*failed=(?P\d+),\s*total=(?P\d+),\s*rpm=(?P[0-9.]+)" +) + + +def parse_log_message(msg: str) -> ParsedUpdate | None: + m = _RE_SAMPLERS.search(msg) + if m: + return ParsedUpdate( + stage=STAGE_SAMPLING, + rows=int(m.group("rows")), + cols=int(m.group("cols")), + ) + + if "Sorting column configs into a Directed Acyclic Graph" in msg: + return ParsedUpdate(stage=STAGE_DAG) + if "Running health checks for models" in msg: + return ParsedUpdate(stage=STAGE_HEALTHCHECK) + if "Preview generation in progress" in msg: + return ParsedUpdate(stage=STAGE_PREVIEW) + if "Creating Data Designer dataset" in msg: + return ParsedUpdate(stage=STAGE_CREATE) + if "Measuring dataset column statistics" in msg: + return ParsedUpdate(stage=STAGE_PROFILING) + + m = _RE_COLCFG.search(msg) + if m: + col = m.group("col") + return ParsedUpdate(stage=STAGE_COLUMN_CONFIG, current_column=col) + + m = _RE_PROCESSING_COL.search(msg) + if m: + col = m.group("col") + return ParsedUpdate(stage=STAGE_GENERATING, current_column=col) + + m = _RE_PROGRESS.search(msg) + if m: + p = Progress( + done=int(m.group("done")), + total=int(m.group("total")), + percent=float(m.group("pct")), + ok=int(m.group("ok")), + failed=int(m.group("failed")), + rate=float(m.group("rate")), + eta_sec=float(m.group("eta")), + ) + return ParsedUpdate(stage=STAGE_GENERATING, progress=p) + + m = _RE_BATCH.search(msg) + if m: + return ParsedUpdate( + stage=STAGE_BATCH, + batch_idx=int(m.group("idx")), + batch_total=int(m.group("total")), + ) + + if "Model usage summary" in msg: + return ParsedUpdate(usage_section_start=True) + + m = _RE_USAGE_MODEL.search(msg) + if m and "|-- model:" in msg: + return ParsedUpdate(usage_model=str(m.group("model")).strip()) + + m = _RE_USAGE_TOKENS.search(msg) + if m: + return ParsedUpdate( + usage_input_tokens=int(m.group("input")), + usage_output_tokens=int(m.group("output")), + usage_total_tokens=int(m.group("total")), + usage_tps=float(m.group("tps")), + ) + + m = _RE_USAGE_REQUESTS.search(msg) + if m: + return ParsedUpdate( + usage_requests_success=int(m.group("success")), + usage_requests_failed=int(m.group("failed")), + usage_requests_total=int(m.group("total")), + usage_rpm=float(m.group("rpm")), + ) + + return None + + +def apply_update(job: Job, update: ParsedUpdate) -> None: + if update.stage is not None: + job.stage = update.stage + if update.current_column is not None: + job.current_column = update.current_column + if update.stage == STAGE_GENERATING and update.current_column not in job._seen_generation_columns: + job._seen_generation_columns.append(update.current_column) + if update.rows is not None: + job.rows = update.rows + if update.cols is not None: + job.cols = update.cols + if update.progress is not None: + job.column_progress = update.progress + job.progress = _compute_overall_progress(job, update.progress) + if update.batch_idx is not None: + job.batch.idx = update.batch_idx + if update.batch_total is not None: + job.batch.total = update.batch_total + + if update.stage in USAGE_RESET_STAGES: + # usage summary is a short block so we reset once we move into the next stage. + job._in_usage_summary = False + + if update.usage_section_start is not None: + job._in_usage_summary = update.usage_section_start + if update.usage_section_start: + job._current_usage_model = None + + if not job._in_usage_summary: + return + + if update.usage_model is not None: + name = update.usage_model.strip().strip("'").strip('"') + job._current_usage_model = name + if name not in job.model_usage: + job.model_usage[name] = ModelUsage(model=name) + + if job._current_usage_model is None: + return + + usage = job.model_usage.get(job._current_usage_model) + if usage is None: + return + + if update.usage_input_tokens is not None: + usage.input_tokens = update.usage_input_tokens + if update.usage_output_tokens is not None: + usage.output_tokens = update.usage_output_tokens + if update.usage_total_tokens is not None: + usage.total_tokens = update.usage_total_tokens + if update.usage_tps is not None: + usage.tps = update.usage_tps + if update.usage_requests_success is not None: + usage.requests_success = update.usage_requests_success + if update.usage_requests_failed is not None: + usage.requests_failed = update.usage_requests_failed + if update.usage_requests_total is not None: + usage.requests_total = update.usage_requests_total + if update.usage_rpm is not None: + usage.rpm = update.usage_rpm + + +def _compute_overall_progress(job: Job, column_progress: Progress) -> Progress: + if not job.rows: + return column_progress + + total_rows = max(1, int(job.rows)) + current_done = 0 if column_progress.done is None else int(column_progress.done) + current_done = max(0, min(current_done, total_rows)) + total_columns = max(1, int(job.progress_columns_total or 1)) + + if job.current_column: + job._column_done[job.current_column] = current_done + + if len(job._column_done) == 0: + done = current_done + else: + sum_done = sum(max(0, min(value, total_rows)) for value in job._column_done.values()) + done = int(sum_done / total_columns) + + prev_done = int(job.progress.done or 0) + if done < prev_done: + done = prev_done + if done > total_rows: + done = total_rows + percent = (done / total_rows) * 100 if total_rows > 0 else 100.0 + prev_percent = float(job.progress.percent or 0.0) + if percent < prev_percent: + percent = prev_percent + + return Progress( + done=done, + total=total_rows, + percent=percent, + eta_sec=column_progress.eta_sec, + rate=column_progress.rate, + ok=column_progress.ok, + failed=column_progress.failed, + ) + + +def coerce_event(obj: Any) -> dict: + # worker sends dict already + return obj if isinstance(obj, dict) else {"type": "log", "message": str(obj)} diff --git a/studio/backend/core/data_recipe/jobs/types.py b/studio/backend/core/data_recipe/jobs/types.py new file mode 100644 index 0000000000..24a63d062c --- /dev/null +++ b/studio/backend/core/data_recipe/jobs/types.py @@ -0,0 +1,72 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any, Literal + + +JobStatus = Literal[ + "created", + "pending", + "active", + "cancelling", + "cancelled", + "error", + "completed", +] + + +@dataclass +class Progress: + done: int | None = None + total: int | None = None + percent: float | None = None + eta_sec: float | None = None + rate: float | None = None + ok: int | None = None + failed: int | None = None + + +@dataclass +class BatchProgress: + idx: int | None = None + total: int | None = None + + +@dataclass +class ModelUsage: + model: str + input_tokens: int | None = None + output_tokens: int | None = None + total_tokens: int | None = None + tps: float | None = None + requests_success: int | None = None + requests_failed: int | None = None + requests_total: int | None = None + rpm: float | None = None + + +@dataclass +class Job: + job_id: str + status: JobStatus = "created" + stage: str | None = None + current_column: str | None = None + progress: Progress = field(default_factory=Progress) + column_progress: Progress = field(default_factory=Progress) + batch: BatchProgress = field(default_factory=BatchProgress) + rows: int | None = None + cols: int | None = None + error: str | None = None + started_at: float | None = None + finished_at: float | None = None + + analysis: dict[str, Any] | None = None + artifact_path: str | None = None + dataset: list[dict[str, Any]] | None = None + processor_artifacts: dict[str, Any] | None = None + model_usage: dict[str, ModelUsage] = field(default_factory=dict) + progress_columns_total: int | None = None + _current_usage_model: str | None = None + _in_usage_summary: bool = False + _seen_generation_columns: list[str] = field(default_factory=list) + _column_done: dict[str, int] = field(default_factory=dict) diff --git a/studio/backend/core/data_recipe/jobs/worker.py b/studio/backend/core/data_recipe/jobs/worker.py new file mode 100644 index 0000000000..8c0996b140 --- /dev/null +++ b/studio/backend/core/data_recipe/jobs/worker.py @@ -0,0 +1,145 @@ +from __future__ import annotations + +import logging +import shutil +import time +import traceback +from pathlib import Path +from typing import Any + +from ..jsonable import to_jsonable +from .constants import EVENT_JOB_COMPLETED, EVENT_JOB_ERROR, EVENT_JOB_STARTED +from ..service import build_config_builder, create_data_designer + +_PROJECT_ROOT = Path(__file__).resolve().parents[5] +_ARTIFACT_ROOT = _PROJECT_ROOT / "studio" / "backend" / "assets" / "datasets" + + +class _QueueLogHandler(logging.Handler): + def __init__(self, event_queue): + super().__init__() + self._q = event_queue + + def emit(self, record: logging.LogRecord) -> None: + try: + event = { + "type": "log", + "ts": record.created, + "level": record.levelname, + "logger": record.name, + "message": record.getMessage(), + } + self._q.put(event) + except (OSError, RuntimeError, ValueError): + pass + + +def run_job_process( + *, + event_queue, + recipe: dict[str, Any], + run: dict[str, Any], +) -> None: + """ + Subprocess entrypoint. + Sends events to `event_queue`. + """ + event_queue.put({"type": EVENT_JOB_STARTED, "ts": time.time()}) + + try: + from data_designer.config.run_config import RunConfig + + rows = int(run.get("rows") or 1000) + job_id = str(run.get("_job_id") or "").strip() + if not job_id: + job_id = f"{int(time.time())}" + dataset_name = f"recipe_{job_id}" + merge_batches = bool(run.get("merge_batches")) + _ARTIFACT_ROOT.mkdir(parents=True, exist_ok=True) + run_config_raw = run.get("run_config") or {} + + builder = build_config_builder(recipe) + designer = create_data_designer(recipe, artifact_path=str(_ARTIFACT_ROOT)) + + # DataDesigner configures root logging in DataDesigner.__init__. + # Attach queue logger directly to `data_designer` so parser events survive root resets. + handler = _QueueLogHandler(event_queue) + handler.setLevel(logging.INFO) + data_designer_logger = logging.getLogger("data_designer") + data_designer_logger.addHandler(handler) + data_designer_logger.setLevel(logging.INFO) + data_designer_logger.propagate = True + + if run_config_raw: + designer.set_run_config(RunConfig.model_validate(run_config_raw)) + + execution_type = str(run.get("execution_type") or "full").strip().lower() + if execution_type == "preview": + results = designer.preview(builder, num_records=rows) + analysis = ( + None + if results.analysis is None + else to_jsonable(results.analysis.model_dump(mode="json")) + ) + dataset = ( + [] + if results.dataset is None + else to_jsonable(results.dataset.to_dict(orient="records")) + ) + processor_artifacts = ( + None + if results.processor_artifacts is None + else to_jsonable(results.processor_artifacts) + ) + event_queue.put( + { + "type": EVENT_JOB_COMPLETED, + "ts": time.time(), + "analysis": analysis, + "dataset": dataset, + "processor_artifacts": processor_artifacts, + "artifact_path": None, + "execution_type": execution_type, + } + ) + else: + results = designer.create(builder, num_records=rows, dataset_name=dataset_name) + analysis = to_jsonable(results.load_analysis().model_dump(mode="json")) + if merge_batches: + _merge_batches_to_single_parquet(results.artifact_storage.base_dataset_path) + artifact_path = str(results.artifact_storage.base_dataset_path) + event_queue.put( + { + "type": EVENT_JOB_COMPLETED, + "ts": time.time(), + "analysis": analysis, + "artifact_path": artifact_path, + "execution_type": execution_type, + } + ) + except Exception as exc: + event_queue.put( + { + "type": EVENT_JOB_ERROR, + "ts": time.time(), + "error": str(exc), + "stack": traceback.format_exc(limit=20), + } + ) + + +def _merge_batches_to_single_parquet(base_dataset_path: Path) -> None: + parquet_dir = base_dataset_path / "parquet-files" + parquet_files = sorted(parquet_dir.glob("*.parquet")) + if len(parquet_files) <= 1: + return + + try: + from data_designer.config.utils.io_helpers import read_parquet_dataset + except ImportError: + return + + dataframe = read_parquet_dataset(parquet_dir) + shutil.rmtree(parquet_dir) + parquet_dir.mkdir(parents=True, exist_ok=True) + dataframe.to_parquet(parquet_dir / "batch_00000.parquet", index=False) diff --git a/studio/backend/core/data_recipe/jsonable.py b/studio/backend/core/data_recipe/jsonable.py new file mode 100644 index 0000000000..aa6e1d6b2e --- /dev/null +++ b/studio/backend/core/data_recipe/jsonable.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +from typing import Any + + +def to_jsonable(value: Any) -> Any: + """Convert numpy/pandas-ish values into plain JSON-safe values.""" + try: + import numpy as np # type: ignore + except ImportError: # pragma: no cover + np = None # type: ignore + + if np is not None: + if isinstance(value, np.ndarray): + return value.tolist() + if isinstance(value, np.generic): + return value.item() + + if isinstance(value, dict): + return {str(k): to_jsonable(v) for k, v in value.items()} + if isinstance(value, (list, tuple, set)): + return [to_jsonable(v) for v in value] + + if hasattr(value, "isoformat") and callable(value.isoformat): + try: + return value.isoformat() + except (TypeError, ValueError): + return value + + return value diff --git a/studio/backend/core/data_recipe/service.py b/studio/backend/core/data_recipe/service.py new file mode 100644 index 0000000000..2d11cb2845 --- /dev/null +++ b/studio/backend/core/data_recipe/service.py @@ -0,0 +1,149 @@ +from __future__ import annotations + +import os +from typing import Any + +from .jsonable import to_jsonable + + +def build_model_providers(recipe: dict[str, Any]): + from data_designer.config.default_model_settings import get_default_providers + from data_designer.config.models import ModelProvider + + providers: list[ModelProvider] = [] + for provider in recipe.get("model_providers", []): + api_key = provider.get("api_key") + api_key_env = provider.get("api_key_env") + if not api_key and api_key_env: + api_key = os.getenv(api_key_env) + providers.append( + ModelProvider( + name=provider["name"], + endpoint=provider["endpoint"], + provider_type=provider.get("provider_type", "openai"), + api_key=api_key, + extra_headers=provider.get("extra_headers"), + extra_body=provider.get("extra_body"), + ) + ) + + # DataDesigner currently expects at least one provider even if they only use static samplers, + # but it's fine it gives a warning only. + return providers or get_default_providers() + + +def build_mcp_providers( + recipe: dict[str, Any], +) -> list: + from data_designer.config.mcp import LocalStdioMCPProvider, MCPProvider + + providers: list[MCPProvider | LocalStdioMCPProvider] = [] + for provider in recipe.get("mcp_providers", []): + if not isinstance(provider, dict): + continue + provider_type = provider.get("provider_type") + if provider_type == "stdio": + env = provider.get("env") + if not isinstance(env, dict): + env = {} + args = provider.get("args") + if not isinstance(args, list): + args = [] + providers.append( + LocalStdioMCPProvider( + name=str(provider.get("name", "")), + command=str(provider.get("command", "")), + args=[str(value) for value in args], + env={str(key): str(value) for key, value in env.items()}, + ) + ) + continue + + if provider_type in {"sse", "streamable_http"}: + api_key = provider.get("api_key") + api_key_env = provider.get("api_key_env") + if not api_key and api_key_env: + api_key = os.getenv(str(api_key_env)) + providers.append( + MCPProvider( + name=str(provider.get("name", "")), + endpoint=str(provider.get("endpoint", "")), + api_key=str(api_key) if api_key else None, + ) + ) + return providers + + +def build_config_builder(recipe: dict[str, Any]): + from data_designer.config import DataDesignerConfigBuilder + from data_designer.config.processors import ProcessorType + + recipe_core = { + key: value + for key, value in recipe.items() + if key not in {"model_providers", "mcp_providers"} + } + builder = DataDesignerConfigBuilder.from_config({"data_designer": recipe_core}) + + # DataDesignerConfigBuilder.from_config currently skips processors. + # Re-attach explicitly so drop_columns/schema_transform survive API payload. + for processor in recipe_core.get("processors") or []: + if not isinstance(processor, dict): + continue + processor_type_raw = processor.get("processor_type") + if not isinstance(processor_type_raw, str): + continue + kwargs = {k: v for k, v in processor.items() if k != "processor_type"} + builder.add_processor( + processor_type=ProcessorType(processor_type_raw), + **kwargs, + ) + + return builder + + +def create_data_designer( + recipe: dict[str, Any], + *, + artifact_path: str | None = None, +): + from data_designer.interface.data_designer import DataDesigner + + return DataDesigner( + artifact_path=artifact_path, + model_providers=build_model_providers(recipe), + mcp_providers=build_mcp_providers(recipe), + ) + + +def validate_recipe(recipe: dict[str, Any]) -> None: + builder = build_config_builder(recipe) + designer = create_data_designer(recipe) + designer.validate(builder) + + +def preview_recipe( + recipe: dict[str, Any], + num_records: int, +) -> tuple[list[dict[str, Any]], dict[str, Any] | None, dict[str, Any] | None]: + builder = build_config_builder(recipe) + designer = create_data_designer(recipe) + results = designer.preview(builder, num_records=num_records) + + dataset: list[dict[str, Any]] = [] + if results.dataset is not None: + raw_rows = results.dataset.to_dict(orient="records") + dataset = [to_jsonable(row) for row in raw_rows] + + artifacts = ( + None + if results.processor_artifacts is None + else to_jsonable(results.processor_artifacts) + ) + analysis = ( + None + if results.analysis is None + else to_jsonable(results.analysis.model_dump(mode="json")) + ) + + return dataset, artifacts, analysis diff --git a/studio/backend/core/export/export.py b/studio/backend/core/export/export.py index da5b11c60d..3d3db2d560 100644 --- a/studio/backend/core/export/export.py +++ b/studio/backend/core/export/export.py @@ -2,8 +2,11 @@ """ Export backend - handles model exporting in various formats """ +import glob +import json import logging import os +import shutil from pathlib import Path from typing import Optional, Tuple, List from peft import PeftModel, PeftModelForCausalLM @@ -200,6 +203,18 @@ class ExportBackend: logger.error(traceback.format_exc()) return False, f"Failed to load checkpoint: {str(e)}" + def _write_export_metadata(self, save_directory: str): + """Write export_metadata.json with base model info for Chat page discovery.""" + try: + base_model = get_base_model_from_lora(self.current_checkpoint) if self.current_checkpoint else None + metadata = {"base_model": base_model} + metadata_path = os.path.join(save_directory, "export_metadata.json") + with open(metadata_path, "w") as f: + json.dump(metadata, f, indent=2) + logger.info(f"Wrote export metadata to {metadata_path}") + except Exception as e: + logger.warning(f"Could not write export metadata: {e}") + def export_merged_model(self, save_directory: str, format_type: str = "16-bit (FP16)", @@ -244,6 +259,9 @@ class ExportBackend: self.current_tokenizer, save_method=save_method ) + + # Write export metadata so the Chat page can identify the base model + self._write_export_metadata(save_directory) logger.info(f"Model saved successfully to {save_directory}") # Push to hub if requested @@ -297,6 +315,9 @@ class ExportBackend: self.current_model.save_pretrained(save_directory) self.current_tokenizer.save_pretrained(save_directory) + + # Write export metadata so the Chat page can identify the base model + self._write_export_metadata(save_directory) logger.info(f"Model saved successfully to {save_directory}") # Push to hub if requested @@ -378,53 +399,61 @@ class ExportBackend: # Save locally if requested if save_directory: - logger.info(f"Saving GGUF model locally to: {save_directory}") + # Resolve to absolute path so unsloth's relative-path internals + # (check_llama_cpp, use_local_gguf, _download_convert_hf_to_gguf) + # all resolve against the repo root cwd, NOT the export directory. + abs_save_dir = os.path.abspath(save_directory) + logger.info(f"Saving GGUF model locally to: {abs_save_dir}") # Create the directory if it doesn't exist - os.makedirs(save_directory, exist_ok=True) + os.makedirs(abs_save_dir, exist_ok=True) - # Get the base filename for the GGUF file - import shutil - original_dir = os.getcwd() + # On WSL, patch out sudo check before llama.cpp build + _apply_wsl_sudo_patch() - try: - # Change to target directory - os.chdir(save_directory) - logger.info(f"Changed directory to: {save_directory}") + # Snapshot existing .gguf files in cwd before conversion. + # unsloth's convert_to_gguf writes output files relative to + # cwd (repo root), so we diff afterwards and relocate them. + cwd = os.getcwd() + pre_existing_ggufs = set(glob.glob(os.path.join(cwd, "*.gguf"))) - # On WSL, patch out sudo check before llama.cpp build - _apply_wsl_sudo_patch() + # Pass absolute path — no os.chdir needed. + # unsloth saves intermediate HF model files into model_save_path. + # unsloth-zoo's check_llama_cpp() uses ~/.unsloth/llama.cpp by default. + model_save_path = os.path.join(abs_save_dir, "model") + self.current_model.save_pretrained_gguf( + model_save_path, + self.current_tokenizer, + quantization_method=quant_method + ) - # Now save (will save in current directory) - self.current_model.save_pretrained_gguf( - "model", # Base filename - self.current_tokenizer, - quantization_method=quant_method - ) + # Relocate GGUF artifacts into the export directory. + # convert_to_gguf writes .gguf files to cwd (repo root) + # because --outfile is a relative path like "model.Q4_K_M.gguf". + new_ggufs = set(glob.glob(os.path.join(cwd, "*.gguf"))) - pre_existing_ggufs + for src in sorted(new_ggufs): + dest = os.path.join(abs_save_dir, os.path.basename(src)) + shutil.move(src, dest) + logger.info(f"Relocated GGUF: {os.path.basename(src)} → {abs_save_dir}/") - logger.info(f"GGUF model saved successfully in {save_directory}") + # Flatten any .gguf files from subdirectories into abs_save_dir. + # save_pretrained_gguf may create subdirs (e.g. model_gguf/) + # with a name different from model_save_path. + for sub in list(Path(abs_save_dir).iterdir()): + if not sub.is_dir(): + continue + for src in sub.glob("*.gguf"): + dest = os.path.join(abs_save_dir, src.name) + shutil.move(str(src), dest) + logger.info(f"Relocated GGUF: {src.name} → {abs_save_dir}/") + # Clean up the subdirectory (intermediate HF files, etc.) + shutil.rmtree(str(sub), ignore_errors=True) + logger.info(f"Cleaned up subdirectory: {sub.name}") - # Check if llama.cpp directory was created here - llama_cpp_in_target = os.path.join(save_directory, "llama.cpp") - llama_cpp_in_original = os.path.join(original_dir, "llama.cpp") + # Write export metadata so the Chat page can identify the base model + self._write_export_metadata(abs_save_dir) - if os.path.exists(llama_cpp_in_target): - logger.info(f"Found llama.cpp directory in {save_directory}") - - # Remove llama.cpp from original directory if it exists - if os.path.exists(llama_cpp_in_original): - logger.info(f"Removing existing llama.cpp in {original_dir}") - shutil.rmtree(llama_cpp_in_original) - - # Move llama.cpp back to original directory - logger.info(f"Moving llama.cpp to {original_dir}") - shutil.move(llama_cpp_in_target, llama_cpp_in_original) - logger.info(f"Successfully moved llama.cpp back to original directory") - - finally: - # Always change back to original directory - os.chdir(original_dir) - logger.info(f"Changed back to original directory: {original_dir}") + logger.info(f"GGUF model saved successfully in {abs_save_dir}") # Push to hub if requested if push_to_hub: diff --git a/studio/backend/core/inference/__init__.py b/studio/backend/core/inference/__init__.py index 494229a087..ff8b75d36a 100644 --- a/studio/backend/core/inference/__init__.py +++ b/studio/backend/core/inference/__init__.py @@ -2,8 +2,10 @@ Inference submodule - Inference backend for model loading and generation """ from .inference import InferenceBackend, get_inference_backend +from .llama_cpp import LlamaCppBackend __all__ = [ 'InferenceBackend', 'get_inference_backend', + 'LlamaCppBackend', ] diff --git a/studio/backend/core/inference/inference.py b/studio/backend/core/inference/inference.py index e90e6c0c2a..329f5d944b 100644 --- a/studio/backend/core/inference/inference.py +++ b/studio/backend/core/inference/inference.py @@ -6,8 +6,10 @@ from unsloth.chat_templates import get_chat_template from transformers import TextStreamer from peft import PeftModel, PeftModelForCausalLM +import json import sys import torch +from pathlib import Path from typing import Optional, Union, Generator, Tuple from utils.models import ModelConfig, get_base_model_from_lora from utils.paths import is_model_cached @@ -112,7 +114,18 @@ class InferenceBackend: # In that case, load the real processor from the base model. from transformers import ProcessorMixin if not (isinstance(processor, ProcessorMixin) or hasattr(processor, "image_processor")): + # For LoRA adapters, use the base model. For local merged exports, + # read export_metadata.json to find the original base model. processor_source = config.base_model if config.is_lora else config.identifier + if not config.is_lora and config.is_local: + _meta_path = Path(config.path) / "export_metadata.json" + try: + if _meta_path.exists(): + _meta = json.loads(_meta_path.read_text()) + if _meta.get("base_model"): + processor_source = _meta["base_model"] + except Exception: + pass logger.warning( f"FastVisionModel returned {type(processor).__name__} (no image_processor) " f"for '{model_name}' — loading proper processor from '{processor_source}'" @@ -255,47 +268,6 @@ class InferenceBackend: logger.error(traceback.format_exc()) return False, None - def load_adapter(self, base_model_name: str, adapter_path: str, adapter_name: str = None) -> bool: - """ - Load a LoRA adapter onto the base model if it's not already registered. - This method is idempotent. - """ - if base_model_name not in self.models: - logger.error(f"Base model {base_model_name} not loaded") - return False - - model = self.models[base_model_name].get("model") - if model is None: - logger.error(f"Model object for {base_model_name} is None.") - return False - - if adapter_name is None: - adapter_name = adapter_path.split("/")[-1].replace(".", "_") - - # If we've loaded this adapter before, we don't need to do anything. - if adapter_name in self.models[base_model_name].get("loaded_adapters", {}): - logger.info(f"Adapter '{adapter_name}' is already registered. Skipping.") - return True - - try: - logger.info(f"Loading new adapter '{adapter_name}' from '{adapter_path}' onto {base_model_name}") - - # Unsloth modifies the model in-place and returns None. Do NOT re-assign. - model.load_adapter(adapter_path, adapter_name=adapter_name) - - # Update our internal registry so we don't load it again. - self.models[base_model_name]["loaded_adapters"][adapter_name] = adapter_path - - total_adapters = len(getattr(model, 'peft_config', {})) - logger.info(f"Adapter '{adapter_name}' loaded successfully. (Total adapters on model: {total_adapters})") - return True - except Exception as e: - logger.error(f"Failed to load adapter '{adapter_name}': {e}") - import traceback - logger.error(traceback.format_exc()) - return False - pass - def enable_adapter(self, base_model_name: str, adapter_name: str) -> bool: """Enable specific adapter (for generation)""" if base_model_name not in self.models: @@ -328,55 +300,6 @@ class InferenceBackend: logger.error(f"Failed to disable adapters: {e}") return False - # In backend/inference.py - - def load_for_eval(self, lora_path: str, max_seq_length: int = 2048, - dtype = None, load_in_4bit: bool = True, - hf_token: Optional[str] = None) -> Tuple[bool, Optional[str], Optional[str]]: - """ - Prepare for eval: ensure base model and the specified adapter are loaded. - """ - try: - from utils.models import ModelConfig - lora_config = ModelConfig.from_lora_path(lora_path, hf_token) - if not lora_config: - return False, None, None - - base_model_name = lora_config.base_model - - # 1. Load the base model if it's not already in memory (this logic is correct) - if base_model_name not in self.models or not self.models[base_model_name].get("model"): - logger.info(f"Base model '{base_model_name}' not loaded, loading now.") - base_config = ModelConfig.from_ui_selection(base_model_name, None, is_lora=False) - if not self.load_model(base_config, max_seq_length, dtype, load_in_4bit, hf_token): - return False, None, None - else: - logger.info(f"Base model '{base_model_name}' is already in memory.") - - self.active_model_name = base_model_name - - # 2. Delegate to our now-idempotent load_adapter function. - # It will handle all cases: first adapter, or subsequent adapters. - adapter_name = lora_path.split("/")[-1].replace(".", "_") - adapter_success = self.load_adapter( - base_model_name=base_model_name, - adapter_path=lora_path, - adapter_name=adapter_name - ) - - if not adapter_success: - return False, base_model_name, None - - return True, base_model_name, adapter_name - - except Exception as e: - logger.error(f"Error during load_for_eval: {e}") - import traceback - logger.error(traceback.format_exc()) - return False, None, None - pass - - def load_for_eval(self, lora_path: str, max_seq_length: int = 2048, dtype = None, load_in_4bit: bool = True, hf_token: Optional[str] = None) -> Tuple[bool, Optional[str], Optional[str]]: @@ -1259,47 +1182,6 @@ class InferenceBackend: """Get name of currently loading model""" return next(iter(self.loading_models)) if self.loading_models else None - def load_model_simple(self, - model_path: str, - hf_token: Optional[str] = None, - max_seq_length: int = 2048, - load_in_4bit: bool = True) -> bool: - """ - Simple model loading wrapper for chat interface. - Accepts model path as string and handles ModelConfig creation internally. - - Args: - model_path: Model name or path (e.g., "unsloth/llama-3-8b") - hf_token: HuggingFace token for gated models - max_seq_length: Maximum sequence length - load_in_4bit: Whether to use 4-bit quantization - - Returns: - bool: True if successful, False otherwise - """ - try: - # Create config from string path - config = ModelConfig.from_ui_selection( - model_path, - lora_path=None, # No LoRA for chat - is_lora=False - ) - - # Call existing load_model with config - return self.load_model( - config=config, - max_seq_length=max_seq_length, - dtype=None, # Auto-detect - load_in_4bit=load_in_4bit, - hf_token=hf_token - ) - - except Exception as e: - logger.error(f"Error in load_model_simple: {e}") - return False - - - def load_model_simple(self, model_path: str, hf_token: Optional[str] = None, diff --git a/studio/backend/core/inference/llama_cpp.py b/studio/backend/core/inference/llama_cpp.py new file mode 100644 index 0000000000..f47b9132db --- /dev/null +++ b/studio/backend/core/inference/llama_cpp.py @@ -0,0 +1,573 @@ +""" +llama-server inference backend for GGUF models. + +Manages a llama-server subprocess and proxies chat completions +through its OpenAI-compatible /v1/chat/completions endpoint. +""" +import atexit +import json +import logging +import shutil +import signal +import socket +import subprocess +import threading +import time +from pathlib import Path +from typing import Generator, Optional + +import httpx + +logger = logging.getLogger(__name__) + + +class LlamaCppBackend: + """ + Manages a llama-server subprocess for GGUF model inference. + + Lifecycle: + 1. load_model() — starts llama-server with the GGUF file + 2. generate_chat_completion() — proxies to /v1/chat/completions, streams back + 3. unload_model() — terminates llama-server subprocess + """ + + def __init__(self): + self._process: Optional[subprocess.Popen] = None + self._port: Optional[int] = None + self._model_identifier: Optional[str] = None + self._gguf_path: Optional[str] = None + self._hf_repo: Optional[str] = None + self._hf_variant: Optional[str] = None + self._is_vision: bool = False + self._healthy = False + self._lock = threading.Lock() + self._stdout_lines: list[str] = [] + self._stdout_thread: Optional[threading.Thread] = None + + atexit.register(self._cleanup) + + # ── Properties ──────────────────────────────────────────────── + + @property + def is_loaded(self) -> bool: + return self._process is not None and self._healthy + + @property + def base_url(self) -> str: + return f"http://127.0.0.1:{self._port}" + + @property + def model_identifier(self) -> Optional[str]: + return self._model_identifier + + @property + def is_vision(self) -> bool: + return self._is_vision + + @property + def hf_variant(self) -> Optional[str]: + return self._hf_variant + + # ── Binary discovery ────────────────────────────────────────── + + @staticmethod + def _find_llama_server_binary() -> Optional[str]: + """ + Locate the llama-server binary. + + Search order: + 1. LLAMA_SERVER_PATH environment variable (direct path to binary) + 1b. UNSLOTH_LLAMA_CPP_PATH env var (custom llama.cpp install dir) + 2. ~/.unsloth/llama.cpp/llama-server (make build, root dir) + 3. ~/.unsloth/llama.cpp/build/bin/llama-server (cmake build, Linux) + 4. ~/.unsloth/llama.cpp/build/bin/Release/llama-server.exe (cmake build, Windows) + 5. ./llama.cpp/llama-server (legacy: make build, root dir) + 6. ./llama.cpp/build/bin/llama-server (legacy: cmake in-tree build) + 7. llama-server on PATH (system install) + 8. ./bin/llama-server (legacy: extracted binary) + """ + import os + import sys + + binary_name = "llama-server.exe" if sys.platform == "win32" else "llama-server" + + # 1. Env var — direct path to binary + env_path = os.environ.get("LLAMA_SERVER_PATH") + if env_path and Path(env_path).is_file(): + return env_path + + # 1b. UNSLOTH_LLAMA_CPP_PATH — custom llama.cpp install directory + custom_llama_cpp = os.environ.get("UNSLOTH_LLAMA_CPP_PATH") + if custom_llama_cpp: + custom_dir = Path(custom_llama_cpp) + # Root dir (make builds) + root_bin = custom_dir / binary_name + if root_bin.is_file(): + return str(root_bin) + # build/bin/ (cmake builds on Linux) + cmake_bin = custom_dir / "build" / "bin" / binary_name + if cmake_bin.is_file(): + return str(cmake_bin) + # build/bin/Release/ (cmake builds on Windows) + if sys.platform == "win32": + win_bin = custom_dir / "build" / "bin" / "Release" / binary_name + if win_bin.is_file(): + return str(win_bin) + + # 2–4. ~/.unsloth/llama.cpp (primary — setup.sh / setup.ps1 build here) + unsloth_home = Path.home() / ".unsloth" / "llama.cpp" + # Root dir (make builds copy binaries here) + home_root = unsloth_home / binary_name + if home_root.is_file(): + return str(home_root) + # build/bin/ (cmake builds on Linux) + home_linux = unsloth_home / "build" / "bin" / binary_name + if home_linux.is_file(): + return str(home_linux) + + # 3. Windows MSVC build has Release subdir + if sys.platform == "win32": + home_win = unsloth_home / "build" / "bin" / "Release" / binary_name + if home_win.is_file(): + return str(home_win) + + # 5–6. Legacy: in-tree build (older setup.sh / setup.ps1 versions) + project_root = Path(__file__).resolve().parents[4] + # Root dir (make builds) + root_path = project_root / "llama.cpp" / binary_name + if root_path.is_file(): + return str(root_path) + # build/bin/ (cmake builds) + build_path = project_root / "llama.cpp" / "build" / "bin" / binary_name + if build_path.is_file(): + return str(build_path) + if sys.platform == "win32": + win_path = project_root / "llama.cpp" / "build" / "bin" / "Release" / binary_name + if win_path.is_file(): + return str(win_path) + + # 7. System PATH + system_path = shutil.which("llama-server") + if system_path: + return system_path + + # 8. Legacy: extracted to bin/ + bin_path = project_root / "bin" / binary_name + if bin_path.is_file(): + return str(bin_path) + + return None + + # ── Port allocation ─────────────────────────────────────────── + + @staticmethod + def _find_free_port() -> int: + """Find an available TCP port.""" + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(("", 0)) + return s.getsockname()[1] + + # ── Stdout drain (prevents pipe deadlock on Windows) ───────── + + def _drain_stdout(self): + """ + Read lines from the subprocess stdout in a background thread. + + This prevents a pipe-buffer deadlock on Windows where the default + pipe buffer is only ~4 KB. Without draining, llama-server blocks + on writes and never becomes healthy. + """ + try: + for line in self._process.stdout: + line = line.rstrip() + if line: + self._stdout_lines.append(line) + logger.info(f"[llama-server] {line}") + except (ValueError, OSError): + # Pipe closed — process is terminating + pass + + # ── Lifecycle ───────────────────────────────────────────────── + + def load_model( + self, + *, + # Local mode: pass a path to a .gguf file + gguf_path: Optional[str] = None, + # Vision projection (mmproj) for local vision models + mmproj_path: Optional[str] = None, + # HF mode: let llama-server download via -hf "repo:quant" + hf_repo: Optional[str] = None, + hf_variant: Optional[str] = None, + hf_token: Optional[str] = None, + # Common + model_identifier: str, + is_vision: bool = False, + n_ctx: int = 4096, + n_gpu_layers: int = -1, + n_threads: Optional[int] = None, + ) -> bool: + """ + Start llama-server with a GGUF model. + + Two modes: + - Local: ``gguf_path="/path/to/model.gguf"`` → uses ``-m`` + - HF: ``hf_repo="unsloth/gemma-3-4b-it-GGUF", hf_variant="Q4_K_M"`` → uses ``-hf`` + + In HF mode, llama-server handles downloading, caching, and + auto-loading mmproj files for vision models. + + Returns True if server started and health check passed. + """ + with self._lock: + self._kill_process() + + binary = self._find_llama_server_binary() + if not binary: + raise RuntimeError( + "llama-server binary not found. " + "Run setup.sh to build it, install llama.cpp, " + "or set LLAMA_SERVER_PATH environment variable." + ) + + self._port = self._find_free_port() + + # Build command based on mode + if hf_repo: + # Download the GGUF file ourselves using huggingface_hub + # (llama-server's -hf flag requires HTTPS/curl which may not + # be available, e.g. Windows builds with -DLLAMA_CURL=OFF) + try: + from huggingface_hub import hf_hub_download + except ImportError: + raise RuntimeError( + "huggingface_hub is required for HF model loading. " + "Install it with: pip install huggingface_hub" + ) + + # Determine the filename from the variant (e.g., "Q4_K_M" -> find matching file) + gguf_filename = None + if hf_variant: + # Try common naming patterns + try: + from huggingface_hub import list_repo_files + files = list_repo_files(hf_repo, token=hf_token) + variant_lower = hf_variant.lower() + for f in files: + if f.endswith(".gguf") and variant_lower in f.lower(): + gguf_filename = f + break + except Exception as e: + logger.warning(f"Could not list repo files: {e}") + + if not gguf_filename: + # Fallback: construct common filename pattern + # e.g., "unsloth/gemma-3-4b-it-GGUF" + "Q4_K_M" -> try model name + repo_name = hf_repo.split("/")[-1].replace("-GGUF", "") + gguf_filename = f"{repo_name}-{hf_variant}.gguf" + + logger.info(f"Downloading GGUF: {hf_repo}/{gguf_filename}") + try: + local_path = hf_hub_download( + repo_id=hf_repo, + filename=gguf_filename, + token=hf_token, + ) + except Exception as e: + raise RuntimeError( + f"Failed to download GGUF file '{gguf_filename}' from {hf_repo}: {e}" + ) + + logger.info(f"GGUF downloaded to: {local_path}") + cmd = [ + binary, + "-m", local_path, + "--port", str(self._port), + "-c", str(n_ctx), + "-ngl", str(n_gpu_layers), + ] + elif gguf_path: + if not Path(gguf_path).is_file(): + raise FileNotFoundError(f"GGUF file not found: {gguf_path}") + cmd = [ + binary, + "-m", gguf_path, + "--port", str(self._port), + "-c", str(n_ctx), + "-ngl", str(n_gpu_layers), + ] + else: + raise ValueError("Either gguf_path or hf_repo must be provided") + + if n_threads is not None: + cmd.extend(["--threads", str(n_threads)]) + + # Append mmproj for local vision models + if mmproj_path: + if not Path(mmproj_path).is_file(): + logger.warning(f"mmproj file not found: {mmproj_path}") + else: + cmd.extend(["--mmproj", mmproj_path]) + logger.info(f"Using mmproj for vision: {mmproj_path}") + + logger.info(f"Starting llama-server: {' '.join(cmd)}") + + # Set library paths so llama-server can find its shared libs and CUDA DLLs + import os + import sys + env = os.environ.copy() + binary_dir = str(Path(binary).parent) + + if sys.platform == "win32": + # On Windows, CUDA DLLs (cublas64_12.dll, cudart64_12.dll, etc.) + # must be on PATH. Add CUDA_PATH\bin if available. + path_dirs = [binary_dir] + cuda_path = os.environ.get("CUDA_PATH", "") + if cuda_path: + cuda_bin = os.path.join(cuda_path, "bin") + if os.path.isdir(cuda_bin): + path_dirs.append(cuda_bin) + # Some CUDA installs put DLLs in bin\x64 + cuda_bin_x64 = os.path.join(cuda_path, "bin", "x64") + if os.path.isdir(cuda_bin_x64): + path_dirs.append(cuda_bin_x64) + existing_path = env.get("PATH", "") + env["PATH"] = ";".join(path_dirs) + ";" + existing_path + else: + # Linux: set LD_LIBRARY_PATH for shared libs next to the binary + existing_ld = env.get("LD_LIBRARY_PATH", "") + env["LD_LIBRARY_PATH"] = f"{binary_dir}:{existing_ld}" if existing_ld else binary_dir + + self._stdout_lines = [] + self._process = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + env=env, + ) + + # Start background thread to drain stdout and prevent pipe deadlock + self._stdout_thread = threading.Thread( + target=self._drain_stdout, daemon=True, name="llama-stdout" + ) + self._stdout_thread.start() + + self._gguf_path = gguf_path + self._hf_repo = hf_repo + self._hf_variant = hf_variant + self._is_vision = is_vision + self._model_identifier = model_identifier + + # Wait for llama-server to become healthy + if not self._wait_for_health(timeout=120.0): + self._kill_process() + raise RuntimeError( + "llama-server failed to start. " + "Check that the GGUF file is valid and you have enough memory." + ) + + self._healthy = True + + logger.info( + f"llama-server ready on port {self._port} " + f"for model '{model_identifier}'" + ) + return True + + def unload_model(self) -> bool: + """Terminate the llama-server subprocess and clean up state.""" + with self._lock: + self._kill_process() + logger.info(f"Unloaded GGUF model: {self._model_identifier}") + self._model_identifier = None + self._gguf_path = None + self._hf_repo = None + self._hf_variant = None + self._is_vision = False + self._port = None + self._healthy = False + return True + + def _kill_process(self): + """Terminate the subprocess if running.""" + if self._process is None: + return + try: + self._process.terminate() + self._process.wait(timeout=5) + except subprocess.TimeoutExpired: + logger.warning("llama-server did not exit on SIGTERM, sending SIGKILL") + self._process.kill() + self._process.wait(timeout=5) + except Exception as e: + logger.warning(f"Error killing llama-server process: {e}") + finally: + self._process = None + if self._stdout_thread is not None: + self._stdout_thread.join(timeout=2) + self._stdout_thread = None + + def _cleanup(self): + """atexit handler to ensure llama-server is terminated.""" + self._kill_process() + + def _wait_for_health(self, timeout: float = 120.0, interval: float = 0.5) -> bool: + """ + Poll llama-server's /health endpoint until it responds 200. + + Also monitors subprocess for early exit/crash. + """ + deadline = time.monotonic() + timeout + url = f"http://127.0.0.1:{self._port}/health" + + while time.monotonic() < deadline: + # Check if process crashed + if self._process.poll() is not None: + # Give the drain thread a moment to collect final output + if self._stdout_thread is not None: + self._stdout_thread.join(timeout=2) + output = "\n".join(self._stdout_lines[-50:]) + logger.error( + f"llama-server exited with code {self._process.returncode}. " + f"Output: {output[:2000]}" + ) + return False + + try: + resp = httpx.get(url, timeout=2.0) + if resp.status_code == 200: + return True + except (httpx.ConnectError, httpx.TimeoutException): + pass + + time.sleep(interval) + + logger.error(f"llama-server health check timed out after {timeout}s") + return False + + # ── Message building (OpenAI format) ────────────────────────── + + @staticmethod + def _build_openai_messages( + messages: list[dict], + image_b64: Optional[str] = None, + ) -> list[dict]: + """ + Build OpenAI-format messages, optionally injecting an image_url + content part into the last user message for vision models. + + If no image is provided, returns messages as-is. + """ + if not image_b64: + return messages + + # Find the last user message and convert to multimodal content parts + result = [msg.copy() for msg in messages] + last_user_idx = None + for i, msg in enumerate(result): + if msg["role"] == "user": + last_user_idx = i + + if last_user_idx is not None: + text_content = result[last_user_idx].get("content", "") + result[last_user_idx]["content"] = [ + {"type": "text", "text": text_content}, + { + "type": "image_url", + "image_url": { + "url": f"data:image/png;base64,{image_b64}", + }, + }, + ] + + return result + + # ── Generation (proxy to llama-server) ──────────────────────── + + def generate_chat_completion( + self, + messages: list[dict], + image_b64: Optional[str] = None, + temperature: float = 0.7, + top_p: float = 0.9, + top_k: int = 40, + min_p: float = 0.0, + max_tokens: int = 512, + repetition_penalty: float = 1.1, + stop: Optional[list[str]] = None, + cancel_event: Optional[threading.Event] = None, + ) -> Generator[str, None, None]: + """ + Send a chat completion request to llama-server and stream tokens back. + + Uses /v1/chat/completions — llama-server handles chat template + application and vision (multimodal image_url parts) natively. + + Yields cumulative text (matching InferenceBackend's convention). + """ + if not self.is_loaded: + raise RuntimeError("llama-server is not loaded") + + openai_messages = self._build_openai_messages(messages, image_b64) + + payload = { + "messages": openai_messages, + "stream": True, + "temperature": temperature, + "top_p": top_p, + "top_k": top_k if top_k >= 0 else 0, + "min_p": min_p, + "max_tokens": max_tokens, + "repeat_penalty": repetition_penalty, + } + if stop: + payload["stop"] = stop + + url = f"{self.base_url}/v1/chat/completions" + cumulative = "" + + try: + with httpx.Client(timeout=None) as client: + with client.stream("POST", url, json=payload) as response: + if response.status_code != 200: + error_body = response.read().decode() + raise RuntimeError( + f"llama-server returned {response.status_code}: {error_body}" + ) + + buffer = "" + for raw_chunk in response.iter_text(): + if cancel_event is not None and cancel_event.is_set(): + break + + buffer += raw_chunk + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.strip() + + if not line: + continue + if line == "data: [DONE]": + return + if not line.startswith("data: "): + continue + + try: + data = json.loads(line[6:]) + choices = data.get("choices", []) + if choices: + delta = choices[0].get("delta", {}) + token = delta.get("content", "") + if token: + cumulative += token + yield cumulative + except json.JSONDecodeError: + logger.debug(f"Skipping malformed SSE line: {line[:100]}") + + except httpx.ConnectError: + raise RuntimeError("Lost connection to llama-server") + except Exception as e: + if cancel_event is not None and cancel_event.is_set(): + return + raise diff --git a/studio/backend/core/training/trainer.py b/studio/backend/core/training/trainer.py index 97f460f666..2dd2e6eeeb 100644 --- a/studio/backend/core/training/trainer.py +++ b/studio/backend/core/training/trainer.py @@ -7,7 +7,7 @@ import os os.environ["TOKENIZERS_PARALLELISM"] = "false" import torch -from utils.hardware import clear_gpu_cache +from utils.hardware import clear_gpu_cache, safe_num_proc torch._dynamo.config.recompile_limit = 64 from unsloth import FastLanguageModel, FastVisionModel, is_bfloat16_supported from unsloth.chat_templates import get_chat_template @@ -175,6 +175,15 @@ class UnslothTrainer: token=hf_token, ) logger.info("Loaded vision model") + + # Diagnostic: check if FastVisionModel returned a real Processor or a raw tokenizer + from transformers import ProcessorMixin + tok = self.tokenizer + has_image_proc = isinstance(tok, ProcessorMixin) or hasattr(tok, "image_processor") + print(f"\n[VLM Diagnostic] FastVisionModel returned: {type(tok).__name__}") + print(f"[VLM Diagnostic] Is ProcessorMixin: {isinstance(tok, ProcessorMixin)}") + print(f"[VLM Diagnostic] Has image_processor: {hasattr(tok, 'image_processor')}") + print(f"[VLM Diagnostic] Usable as vision processor: {has_image_proc}\n") else: # Load text model - returns (model, tokenizer) self.model, self.tokenizer = FastLanguageModel.from_pretrained( @@ -345,7 +354,10 @@ class UnslothTrainer: custom_format_mapping: dict = None, subset: str = None, train_split: str = "train", - eval_split: str = None) -> Optional[tuple]: + eval_split: str = None, + eval_steps: float = 0.00, + dataset_slice_start: int = None, + dataset_slice_end: int = None) -> Optional[tuple]: """ Load and prepare dataset for training. @@ -360,6 +372,7 @@ class UnslothTrainer: dataset = None eval_dataset = None has_separate_eval_source = False # True if eval comes from a separate HF split + eval_enabled = eval_steps is not None and eval_steps > 0 if local_datasets: # Load local datasets @@ -412,27 +425,42 @@ class UnslothTrainer: print(f"Loaded dataset from Hugging Face: {dataset_source}\n") # Resolve eval split from a separate HF split (explicit or auto-detected) - if eval_split: - # Explicit eval split provided - load it directly - print(f"Loading explicit eval split: '{eval_split}'\n") - eval_load_kwargs = {"path": dataset_source, "split": eval_split} - if subset: - eval_load_kwargs["name"] = subset - eval_dataset = load_dataset(**eval_load_kwargs) - has_separate_eval_source = True - print(f"Loaded eval split '{eval_split}' with {len(eval_dataset)} rows\n") - else: - # Auto-detect eval split from HF (returns a separate dataset, or None) - eval_dataset = self._auto_detect_eval_split_from_hf( - dataset_source=dataset_source, - subset=subset, - ) - if eval_dataset is not None: + if eval_enabled: + if eval_split: + # Explicit eval split provided - load it directly + print(f"Loading explicit eval split: '{eval_split}'\n") + eval_load_kwargs = {"path": dataset_source, "split": eval_split} + if subset: + eval_load_kwargs["name"] = subset + eval_dataset = load_dataset(**eval_load_kwargs) has_separate_eval_source = True + print(f"Loaded eval split '{eval_split}' with {len(eval_dataset)} rows\n") + else: + # Auto-detect eval split from HF (returns a separate dataset, or None) + eval_dataset = self._auto_detect_eval_split_from_hf( + dataset_source=dataset_source, + subset=subset, + ) + if eval_dataset is not None: + has_separate_eval_source = True + else: + print("Eval disabled (eval_steps <= 0), skipping eval split detection\n") if dataset is None: raise ValueError("No dataset provided") + # Apply index range slicing if requested (inclusive on both ends) + if dataset_slice_start is not None or dataset_slice_end is not None: + total_rows = len(dataset) + start = dataset_slice_start if dataset_slice_start is not None else 0 + end = dataset_slice_end if dataset_slice_end is not None else total_rows - 1 + # Clamp to valid range + start = max(0, min(start, total_rows - 1)) + end = max(start, min(end, total_rows - 1)) + dataset = dataset.select(range(start, end + 1)) + print(f"Sliced dataset to rows [{start}, {end}]: {len(dataset)} of {total_rows} rows\n") + self._update_progress(status_message=f"Sliced dataset to {len(dataset)} rows (indices {start}-{end})") + # Check if stopped before applying template if self.should_stop: print("Stopped before applying chat template\n") @@ -449,6 +477,7 @@ class UnslothTrainer: format_type=format_type, dataset_name=dataset_source, custom_format_mapping=custom_format_mapping, + progress_callback=self._update_progress, ) # Check if stopped during formatting @@ -456,6 +485,14 @@ class UnslothTrainer: print("Stopped during dataset formatting\n") return None + # Abort if dataset formatting/conversion failed + if not dataset_info.get("success", True): + errors = dataset_info.get("errors", []) + error_msg = "; ".join(errors) if errors else "Dataset formatting failed" + logger.error(f"Dataset conversion failed: {error_msg}") + self._update_progress(error=error_msg) + return None + self._update_progress(status_message=f"Dataset formatted and ready for training") print(f"Dataset formatted successfully\n") @@ -474,7 +511,7 @@ class UnslothTrainer: ) eval_dataset = eval_info["dataset"] print(f"Eval dataset formatted successfully\n") - elif not has_separate_eval_source: + elif eval_enabled and not has_separate_eval_source: # No separate eval source — split the already-formatted dataset formatted_dataset = dataset_info["dataset"] split_result = self._resolve_eval_split_from_dataset(formatted_dataset) @@ -545,7 +582,7 @@ class UnslothTrainer: def start_training(self, dataset: Dataset, eval_dataset: Dataset = None, - eval_steps: float = 0.01, + eval_steps: float = 0.00, output_dir: str = "./outputs", num_epochs: int = 3, learning_rate: float = 5e-5, @@ -713,7 +750,7 @@ class UnslothTrainer: "output_dir": output_dir, "report_to": ["wandb"] if training_args.get('enable_wandb', False) else "none", "include_num_input_tokens_seen": True, # Enable token counting - "dataset_num_proc": max(1, os.cpu_count() // 4), + "dataset_num_proc": safe_num_proc(max(1, os.cpu_count() // 4)), } # Add warmup parameter - use warmup_ratio if provided, otherwise warmup_steps @@ -745,12 +782,16 @@ class UnslothTrainer: # ========== EVAL CONFIGURATION ========== eval_dataset = training_args.get('eval_dataset', None) - eval_steps_val = training_args.get('eval_steps', 0.01) + eval_steps_val = training_args.get('eval_steps', 0.00) if eval_dataset is not None: - config_args["eval_strategy"] = "steps" - config_args["eval_steps"] = eval_steps_val - print(f"Evaluation enabled: eval_steps={eval_steps_val} (fraction of total steps)\n") - print(f"Eval dataset: {len(eval_dataset)} rows\n") + if eval_steps_val > 0: + config_args["eval_strategy"] = "steps" + config_args["eval_steps"] = eval_steps_val + print(f"✅ Evaluation enabled: eval_steps={eval_steps_val} (fraction of total steps)\n") + print(f"Eval dataset: {len(eval_dataset)} rows\n") + else: + print(f"⚠️ Eval dataset provided but eval_steps={eval_steps_val} (disabled)\n") + print("To enable evaluation, set eval_steps > 0.0\n") else: print("No eval dataset — evaluation disabled\n") @@ -873,7 +914,7 @@ class UnslothTrainer: self.trainer, instruction_part=instruction_part, response_part=response_part, - num_proc=config_args.get("dataset_num_proc", max(1, os.cpu_count() // 4)), + num_proc=config_args.get("dataset_num_proc", safe_num_proc(max(1, os.cpu_count() // 4))), ) print("Train on responses only configured successfully\n") diff --git a/studio/backend/core/training/training.py b/studio/backend/core/training/training.py index 6fe08c2b9e..153f4335e3 100644 --- a/studio/backend/core/training/training.py +++ b/studio/backend/core/training/training.py @@ -115,8 +115,10 @@ class TrainingBackend: subset: str = None, train_split: str = "train", eval_split: str = None, - eval_steps: float = 0.01, - is_dataset_multimodal: bool = False) -> bool: + eval_steps: float = 0.00, + is_dataset_multimodal: bool = False, + dataset_slice_start: int = None, + dataset_slice_end: int = None) -> bool: """ Start training. @@ -223,6 +225,9 @@ class TrainingBackend: subset=subset, train_split=train_split, eval_split=eval_split, + eval_steps=eval_steps, + dataset_slice_start=dataset_slice_start, + dataset_slice_end=dataset_slice_end, ) # Unpack: load_and_format_dataset returns (dataset, eval_dataset) @@ -232,10 +237,6 @@ class TrainingBackend: dataset = dataset_result eval_dataset = None - # If user set eval_steps to 0, disable evaluation entirely - if eval_steps is not None and float(eval_steps) <= 0: - eval_dataset = None - # Track whether eval is enabled for status reporting self.eval_enabled = eval_dataset is not None diff --git a/studio/backend/main.py b/studio/backend/main.py index 2d00f985e5..72b6424cd0 100644 --- a/studio/backend/main.py +++ b/studio/backend/main.py @@ -9,12 +9,20 @@ from contextlib import asynccontextmanager from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles -from fastapi.responses import FileResponse +from fastapi.responses import FileResponse, HTMLResponse, Response from pathlib import Path from datetime import datetime # Import routers -from routes import training_router, models_router, inference_router, datasets_router, auth_router, export_router +from routes import ( + auth_router, + data_recipe_router, + datasets_router, + export_router, + inference_router, + models_router, + training_router, +) from auth import storage from utils.hardware import detect_hardware, get_device, DeviceType import utils.hardware.hardware as _hw_module @@ -89,6 +97,7 @@ app.include_router(training_router, prefix="/api/train", tags=["training"]) app.include_router(models_router, prefix="/api/models", tags=["models"]) app.include_router(inference_router, prefix="/api/inference", tags=["inference"]) app.include_router(datasets_router, prefix="/api/datasets", tags=["datasets"]) +app.include_router(data_recipe_router, prefix="/api/data-recipe", tags=["data-recipe"]) app.include_router(export_router, prefix="/api/export", tags=["export"]) @@ -154,27 +163,44 @@ async def get_hardware_info(): def setup_frontend(app: FastAPI, build_path: Path): """Mount frontend static files (optional)""" - if build_path.exists(): - # Mount assets - assets_dir = build_path / "assets" - if assets_dir.exists(): - app.mount("/assets", StaticFiles(directory=assets_dir), name="assets") + if not build_path.exists(): + return False - @app.get("/") - async def serve_root(): - return FileResponse(build_path / "index.html", headers={"Cache-Control": "no-cache, no-store, must-revalidate"}) + # Mount assets + assets_dir = build_path / "assets" + if assets_dir.exists(): + app.mount("/assets", StaticFiles(directory=assets_dir), name="assets") - @app.get("/{full_path:path}") - async def serve_frontend(full_path: str): - if full_path.startswith("api"): - return {"error": "API endpoint not found"} + @app.get("/") + async def serve_root(): + content = (build_path / "index.html").read_bytes() + return Response( + content=content, + media_type="text/html", + headers={"Cache-Control": "no-cache, no-store, must-revalidate"}, + ) - file_path = build_path / full_path - if file_path.is_file(): - return FileResponse(file_path) + @app.get("/{full_path:path}") + async def serve_frontend(full_path: str): + if full_path.startswith("api"): + return {"error": "API endpoint not found"} - return FileResponse(build_path / "index.html", headers={"Cache-Control": "no-cache, no-store, must-revalidate"}) + file_path = (build_path / full_path).resolve() - return True - return False + # Block path traversal — ensure resolved path stays inside build_path + if not str(file_path).startswith(str(build_path.resolve())): + return Response(status_code=403) + + if file_path.is_file(): + return FileResponse(file_path) + + # Serve index.html as bytes — avoids Content-Length mismatch + content = (build_path / "index.html").read_bytes() + return Response( + content=content, + media_type="text/html", + headers={"Cache-Control": "no-cache, no-store, must-revalidate"}, + ) + + return True diff --git a/studio/backend/models/__init__.py b/studio/backend/models/__init__.py index fe21d525a4..bafcb45a9f 100644 --- a/studio/backend/models/__init__.py +++ b/studio/backend/models/__init__.py @@ -52,6 +52,13 @@ from .responses import ( LoRABaseModelResponse, VisionCheckResponse, ) +from .data_recipe import ( + RecipePayload, + PreviewResponse, + ValidateError, + ValidateResponse, + JobCreateResponse, +) __all__ = [ # Training schemas @@ -98,4 +105,10 @@ __all__ = [ "TrainingMetricsResponse", "LoRABaseModelResponse", "VisionCheckResponse", + # Data recipe + "RecipePayload", + "PreviewResponse", + "ValidateError", + "ValidateResponse", + "JobCreateResponse", ] diff --git a/studio/backend/models/data_recipe.py b/studio/backend/models/data_recipe.py new file mode 100644 index 0000000000..9e501c15e2 --- /dev/null +++ b/studio/backend/models/data_recipe.py @@ -0,0 +1,60 @@ +""" +Pydantic schemas for Data Recipe (DataDesigner) API. +""" + +from __future__ import annotations + +from typing import Any + +from pydantic import BaseModel, Field + + +class RecipePayload(BaseModel): + recipe: dict[str, Any] = Field(default_factory=dict) + run: dict[str, Any] | None = None + ui: dict[str, Any] | None = None + + +class PreviewResponse(BaseModel): + dataset: list[dict[str, Any]] = Field(default_factory=list) + processor_artifacts: dict[str, Any] | None = None + analysis: dict[str, Any] | None = None + + +class ValidateError(BaseModel): + message: str + path: str | None = None + code: str | None = None + + +class ValidateResponse(BaseModel): + valid: bool + errors: list[ValidateError] = Field(default_factory=list) + raw_detail: str | None = None + + +class JobCreateResponse(BaseModel): + job_id: str + + +class SeedInspectRequest(BaseModel): + dataset_name: str = Field(min_length=1) + hf_token: str | None = None + subset: str | None = None + split: str | None = "train" + preview_size: int = Field(default=10, ge=1, le=50) + + +class SeedInspectUploadRequest(BaseModel): + filename: str = Field(min_length=1) + content_base64: str = Field(min_length=1) + preview_size: int = Field(default=10, ge=1, le=50) + + +class SeedInspectResponse(BaseModel): + dataset_name: str + resolved_path: str + columns: list[str] = Field(default_factory=list) + preview_rows: list[dict[str, Any]] = Field(default_factory=list) + split: str | None = None + subset: str | None = None diff --git a/studio/backend/models/datasets.py b/studio/backend/models/datasets.py index 81adef7577..18f6ec224b 100644 --- a/studio/backend/models/datasets.py +++ b/studio/backend/models/datasets.py @@ -34,3 +34,4 @@ class CheckFormatResponse(BaseModel): detected_text_column: Optional[str] = None preview_samples: Optional[List[Dict]] = None total_rows: Optional[int] = None + warning: Optional[str] = None diff --git a/studio/backend/models/inference.py b/studio/backend/models/inference.py index d2d98d7944..3a908dcfc3 100644 --- a/studio/backend/models/inference.py +++ b/studio/backend/models/inference.py @@ -17,6 +17,7 @@ class LoadRequest(BaseModel): max_seq_length: int = Field(2048, ge=128, le=32768, description="Maximum sequence length") load_in_4bit: bool = Field(True, description="Load model in 4-bit quantization") is_lora: bool = Field(False, description="Whether this is a LoRA adapter") + gguf_variant: Optional[str] = Field(None, description="GGUF quantization variant (e.g. 'Q4_K_M')") class UnloadRequest(BaseModel): @@ -43,6 +44,7 @@ class LoadResponse(BaseModel): display_name: str = Field(..., description="Display name of the model") is_vision: bool = Field(False, description="Whether model is a vision model") is_lora: bool = Field(False, description="Whether model is a LoRA adapter") + is_gguf: bool = Field(False, description="Whether model is a GGUF model (llama.cpp)") inference: dict = Field(..., description="Inference parameters (temperature, top_p, top_k, min_p)") @@ -56,6 +58,8 @@ class InferenceStatusResponse(BaseModel): """Current inference backend status""" active_model: Optional[str] = Field(None, description="Currently active model identifier") is_vision: bool = Field(False, description="Whether the active model is a vision model") + is_gguf: bool = Field(False, description="Whether the active model is a GGUF model (llama.cpp)") + gguf_variant: Optional[str] = Field(None, description="GGUF quantization variant (e.g. Q4_K_M)") loading: List[str] = Field(default_factory=list, description="Models currently being loaded") loaded: List[str] = Field(default_factory=list, description="Models currently loaded") diff --git a/studio/backend/models/models.py b/studio/backend/models/models.py index 5542db76d5..8c7d0c037d 100644 --- a/studio/backend/models/models.py +++ b/studio/backend/models/models.py @@ -53,14 +53,17 @@ class ModelDetails(BaseModel): config: Optional[Dict[str, Any]] = Field(None, description="Model configuration dictionary") is_vision: bool = Field(False, description="Whether model is a vision model") is_lora: bool = Field(False, description="Whether model is a LoRA adapter") + is_gguf: bool = Field(False, description="Whether model is a GGUF model (llama.cpp format)") base_model: Optional[str] = Field(None, description="Base model if this is a LoRA adapter") class LoRAInfo(BaseModel): - """LoRA adapter information""" + """LoRA adapter or exported model information""" display_name: str = Field(..., description="Display name for the LoRA") - adapter_path: str = Field(..., description="Path to the LoRA adapter") + adapter_path: str = Field(..., description="Path to the LoRA adapter or exported model") base_model: Optional[str] = Field(None, description="Base model identifier") + source: Optional[str] = Field(None, description="'training' or 'exported'") + export_type: Optional[str] = Field(None, description="'lora', 'merged', or 'gguf' (for exports)") class LoRAScanResponse(BaseModel): @@ -75,6 +78,21 @@ class ModelListResponse(BaseModel): default_models: List[str] = Field(default_factory=list, description="List of default model IDs") +class GgufVariantDetail(BaseModel): + """A single GGUF quantization variant in a HuggingFace repo.""" + filename: str = Field(..., description="GGUF filename (e.g., 'gemma-3-4b-it-Q4_K_M.gguf')") + quant: str = Field(..., description="Quantization label (e.g., 'Q4_K_M')") + size_bytes: int = Field(0, description="File size in bytes") + + +class GgufVariantsResponse(BaseModel): + """Response for listing GGUF quantization variants in a HuggingFace repo.""" + repo_id: str = Field(..., description="HuggingFace repo ID") + variants: List[GgufVariantDetail] = Field(default_factory=list, description="Available GGUF variants") + has_vision: bool = Field(False, description="Whether the model has vision support (mmproj files)") + default_variant: Optional[str] = Field(None, description="Recommended default quantization variant") + + class LocalModelInfo(BaseModel): """Discovered local model candidate.""" id: str = Field(..., description="Identifier to use for loading/training") diff --git a/studio/backend/models/training.py b/studio/backend/models/training.py index 2b989e6a82..b6b30989bd 100644 --- a/studio/backend/models/training.py +++ b/studio/backend/models/training.py @@ -21,7 +21,9 @@ class TrainingStartRequest(BaseModel): subset: Optional[str] = None train_split: Optional[str] = Field("train", description="Training split name") eval_split: Optional[str] = Field(None, description="Eval split name. None = auto-detect") - eval_steps: float = Field(0.01, description="Fraction of total steps between evals (0-1)") + eval_steps: float = Field(0.00, description="Fraction of total steps between evals (0-1)") + dataset_slice_start: Optional[int] = Field(None, description="Inclusive start row index for dataset slicing") + dataset_slice_end: Optional[int] = Field(None, description="Inclusive end row index for dataset slicing") @model_validator(mode="before") @classmethod diff --git a/studio/backend/requirements/extras.txt b/studio/backend/requirements/extras.txt index 3ed20faa8b..51ef69cf5d 100644 --- a/studio/backend/requirements/extras.txt +++ b/studio/backend/requirements/extras.txt @@ -51,6 +51,6 @@ addict easydict einops tabulate -fastmcp>=2.0.0 +fastmcp>=3.0.2 openai>=2.7.2 -websockets>=13.0,<14 +websockets>=15.0.1 diff --git a/studio/backend/requirements/single-env/constraints.txt b/studio/backend/requirements/single-env/constraints.txt new file mode 100644 index 0000000000..1789bbf713 --- /dev/null +++ b/studio/backend/requirements/single-env/constraints.txt @@ -0,0 +1,16 @@ +# Single-env pins for unsloth + studio + data-designer +# Keep compatible with unsloth transformers bounds. +transformers==4.57.1 +trl==0.23.1 +huggingface-hub==0.36.2 + +# Studio stack +datasets==4.3.0 +pyarrow==23.0.1 + +# FastMCP/OpenEnv compat +fastmcp>=3.0.2 +mcp>=1.24,<2 +websockets>=15.0.1 + +pandas==2.3.3 diff --git a/studio/backend/requirements/single-env/data-designer-deps.txt b/studio/backend/requirements/single-env/data-designer-deps.txt new file mode 100644 index 0000000000..cbf8856073 --- /dev/null +++ b/studio/backend/requirements/single-env/data-designer-deps.txt @@ -0,0 +1,18 @@ +# Data Designer runtime deps installed explicitly (single-env mode). +anyascii<1,>=0.3.3 +duckdb<2,>=1.1.3 +faker<21,>=20.1.0 +httpx<1,>=0.27.2 +httpx-retries<1,>=0.4.2 +json-repair<1,>=0.48.0 +jsonpath-rust-bindings<2,>=1.0 +jsonschema<5,>=4.0.0 +litellm<1.80.12,>=1.73.6 +lxml<7,>=6.0.2 +marko<3,>=2.1.2 +networkx<4,>=3.0 +python-json-logger<4,>=3 +ruff<1,>=0.14.10 +scipy<2,>=1.11.0 +sqlfluff<4,>=3.2.0 +tiktoken<1,>=0.8.0 diff --git a/studio/backend/requirements/single-env/data-designer.txt b/studio/backend/requirements/single-env/data-designer.txt new file mode 100644 index 0000000000..c5ddcddc36 --- /dev/null +++ b/studio/backend/requirements/single-env/data-designer.txt @@ -0,0 +1,5 @@ +# Install Data Designer in same env as Unsloth. +data-designer==0.5.1 +data-designer-config==0.5.1 +data-designer-engine==0.5.1 +prompt-toolkit>=3,<4 diff --git a/studio/backend/requirements/single-env/patch_metadata.py b/studio/backend/requirements/single-env/patch_metadata.py new file mode 100644 index 0000000000..b579637463 --- /dev/null +++ b/studio/backend/requirements/single-env/patch_metadata.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +"""Relax strict metadata pins so pip check matches known working single-env stack. + +Why: +- data-designer pins huggingface-hub>=1.0.1 and pyarrow<20. +- unsloth/transformers pins huggingface-hub<1. +- studio datasets pins pyarrow>=21. + +Runtime works in this app with hub 0.36.x + pyarrow 23.x, but metadata conflicts. +""" + +from __future__ import annotations + +import importlib.metadata as im +import re +from pathlib import Path + +TARGETS = ( + "data-designer", + "data-designer-engine", + "data-designer-config", +) + +PATCHES: tuple[tuple[re.Pattern[str], str], ...] = ( + ( + re.compile(r"^Requires-Dist: huggingface-hub<2,>=1\.0\.1$", re.MULTILINE), + "Requires-Dist: huggingface-hub<2,>=0.34.0", + ), + ( + re.compile(r"^Requires-Dist: pyarrow<20,>=19\.0\.1$", re.MULTILINE), + "Requires-Dist: pyarrow>=21.0.0", + ), +) + + +def metadata_path(dist_name: str) -> Path | None: + try: + dist = im.distribution(dist_name) + except im.PackageNotFoundError: + return None + for f in dist.files or []: + sf = str(f) + if sf.endswith(".dist-info/METADATA"): + return Path(dist.locate_file(f)) + return None + + +def patch_file(path: Path) -> bool: + original = path.read_text(encoding="utf-8") + updated = original + for pattern, repl in PATCHES: + updated = pattern.sub(repl, updated) + if updated == original: + return False + path.write_text(updated, encoding="utf-8") + return True + + +def main() -> int: + changed = 0 + checked = 0 + for name in TARGETS: + p = metadata_path(name) + if p is None: + continue + checked += 1 + if patch_file(p): + changed += 1 + print(f"single-env metadata patch: checked={checked}, changed={changed}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/studio/backend/requirements/studio.txt b/studio/backend/requirements/studio.txt index 6a732664d2..244ed4f48c 100644 --- a/studio/backend/requirements/studio.txt +++ b/studio/backend/requirements/studio.txt @@ -11,4 +11,4 @@ pyjwt easydict addict gradio>=4.0.0 -huggingface-hub==0.36.0 \ No newline at end of file +huggingface-hub==0.36.2 \ No newline at end of file diff --git a/studio/backend/routes/__init__.py b/studio/backend/routes/__init__.py index 04c7ff7f1a..7ee5d318d2 100644 --- a/studio/backend/routes/__init__.py +++ b/studio/backend/routes/__init__.py @@ -7,6 +7,7 @@ from routes.models import router as models_router from routes.inference import router as inference_router from routes.datasets import router as datasets_router from routes.auth import router as auth_router +from routes.data_recipe import router as data_recipe_router from routes.export import router as export_router __all__ = [ @@ -15,5 +16,6 @@ __all__ = [ "inference_router", "datasets_router", "auth_router", + "data_recipe_router", "export_router", -] \ No newline at end of file +] diff --git a/studio/backend/routes/data_recipe/__init__.py b/studio/backend/routes/data_recipe/__init__.py new file mode 100644 index 0000000000..dc0301d1c9 --- /dev/null +++ b/studio/backend/routes/data_recipe/__init__.py @@ -0,0 +1,23 @@ +"""Data Recipe route package.""" + +from __future__ import annotations + +import sys +from pathlib import Path + +from fastapi import APIRouter + +backend_path = Path(__file__).parent.parent.parent +if str(backend_path) not in sys.path: + sys.path.insert(0, str(backend_path)) + +from .jobs import router as jobs_router +from .seed import router as seed_router +from .validate import router as validate_router + +router = APIRouter() +router.include_router(seed_router) +router.include_router(validate_router) +router.include_router(jobs_router) + +__all__ = ["router"] diff --git a/studio/backend/routes/data_recipe/jobs.py b/studio/backend/routes/data_recipe/jobs.py new file mode 100644 index 0000000000..ffbded9474 --- /dev/null +++ b/studio/backend/routes/data_recipe/jobs.py @@ -0,0 +1,143 @@ +"""Job lifecycle endpoints for data recipe.""" + +from __future__ import annotations + +from typing import Any + +from fastapi import APIRouter, HTTPException, Query, Request +from fastapi.responses import JSONResponse, StreamingResponse +from pydantic import ValidationError + +from core.data_recipe.jobs import get_job_manager +from models.data_recipe import JobCreateResponse, RecipePayload + +router = APIRouter() + + +@router.post("/jobs", response_class=JSONResponse, response_model=JobCreateResponse) +def create_job(payload: RecipePayload): + recipe = payload.recipe + if not recipe.get("columns"): + raise HTTPException(status_code=400, detail="Recipe must include columns.") + + run: dict[str, Any] = payload.run or {} + run.pop("artifact_path", None) + run.pop("dataset_name", None) + execution_type = str(run.get("execution_type") or "full").strip().lower() + if execution_type not in {"preview", "full"}: + raise HTTPException(status_code=400, detail="invalid execution_type: must be 'preview' or 'full'") + run["execution_type"] = execution_type + run_config_raw = run.get("run_config") + if run_config_raw is not None: + try: + from data_designer.config.run_config import RunConfig + + RunConfig.model_validate(run_config_raw) + except (ImportError, ValidationError, TypeError, ValueError) as exc: + raise HTTPException(status_code=400, detail=f"invalid run_config: {exc}") from exc + + mgr = get_job_manager() + try: + job_id = mgr.start(recipe=recipe, run=run) + except RuntimeError as exc: + raise HTTPException(status_code=409, detail=str(exc)) from exc + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + + return {"job_id": job_id} + + +@router.get("/jobs/{job_id}/status") +def job_status(job_id: str): + mgr = get_job_manager() + state = mgr.get_status(job_id) + if state is None: + raise HTTPException(status_code=404, detail="job not found") + return state + + +@router.get("/jobs/current") +def current_job(): + mgr = get_job_manager() + state = mgr.get_current_status() + if state is None: + raise HTTPException(status_code=404, detail="no job") + return state + + +@router.post("/jobs/{job_id}/cancel") +def cancel_job(job_id: str): + mgr = get_job_manager() + ok = mgr.cancel(job_id) + if not ok: + raise HTTPException(status_code=404, detail="job not found") + return mgr.get_status(job_id) + + +@router.get("/jobs/{job_id}/analysis") +def job_analysis(job_id: str): + mgr = get_job_manager() + analysis = mgr.get_analysis(job_id) + if analysis is None: + raise HTTPException(status_code=404, detail="analysis not ready") + return analysis + + +@router.get("/jobs/{job_id}/dataset") +def job_dataset( + job_id: str, + limit: int = Query(default=20, ge=1, le=500), + offset: int = Query(default=0, ge=0), +): + mgr = get_job_manager() + result = mgr.get_dataset(job_id, limit=limit, offset=offset) + if result is None: + raise HTTPException(status_code=404, detail="dataset not ready") + if "error" in result: + raise HTTPException(status_code=422, detail=result["error"]) + return { + "dataset": result["dataset"], + "total": result["total"], + "limit": limit, + "offset": offset, + } + + +@router.get("/jobs/{job_id}/events") +async def job_events(request: Request, job_id: str): + mgr = get_job_manager() + last_id = request.headers.get("last-event-id") + after_seq: int | None = None + if last_id: + try: + after_seq = int(str(last_id).strip()) + except (TypeError, ValueError): + after_seq = None + + after_q = request.query_params.get("after") + if after_q: + try: + after_seq = int(str(after_q).strip()) + except (TypeError, ValueError): + pass + + sub = mgr.subscribe(job_id, after_seq=after_seq) + if sub is None: + raise HTTPException(status_code=404, detail="job not found") + + async def gen(): + try: + for event in sub.replay: + yield sub.format_sse(event) + + while True: + if await request.is_disconnected(): + break + event = await sub.next_event(timeout_sec=1.0) + if event is None: + continue + yield sub.format_sse(event) + finally: + mgr.unsubscribe(sub) + + return StreamingResponse(gen(), media_type="text/event-stream") diff --git a/studio/backend/routes/data_recipe/seed.py b/studio/backend/routes/data_recipe/seed.py new file mode 100644 index 0000000000..eb02ab2bbf --- /dev/null +++ b/studio/backend/routes/data_recipe/seed.py @@ -0,0 +1,298 @@ +"""Seed inspect endpoints for data recipe.""" + +from __future__ import annotations + +import base64 +import binascii +from itertools import islice +from pathlib import Path +from typing import Any +from uuid import uuid4 + +from fastapi import APIRouter, HTTPException + +from models.data_recipe import ( + SeedInspectRequest, + SeedInspectResponse, + SeedInspectUploadRequest, +) + +router = APIRouter() + +DATA_EXTS = (".parquet", ".jsonl", ".json", ".csv") +DEFAULT_SPLIT = "train" +LOCAL_UPLOAD_EXTS = {".csv", ".json", ".jsonl"} +SEED_UPLOAD_DIR = Path.home() / ".cache" / "unsloth" / "data-recipe" / "seed-uploads" + + +def _serialize_preview_value(value: Any) -> Any: + if value is None or isinstance(value, (str, int, float, bool)): + return value + if isinstance(value, dict): + return {str(key): _serialize_preview_value(item) for key, item in value.items()} + if isinstance(value, (list, tuple)): + return [_serialize_preview_value(item) for item in value] + return str(value) + + +def _serialize_preview_rows(rows: list[dict[str, Any]]) -> list[dict[str, Any]]: + return [ + {str(key): _serialize_preview_value(value) for key, value in row.items()} + for row in rows + ] + + +def _normalize_optional_text(value: str | None) -> str | None: + if value is None: + return None + trimmed = value.strip() + return trimmed if trimmed else None + + +def _list_hf_data_files(*, dataset_name: str, token: str | None) -> list[str]: + try: + from huggingface_hub import HfApi + from huggingface_hub.utils import HfHubHTTPError + except ImportError: + return [] + try: + api = HfApi() + repo_files = api.list_repo_files(dataset_name, repo_type="dataset", token=token) + return [file for file in repo_files if file.lower().endswith(DATA_EXTS)] + except (HfHubHTTPError, OSError, ValueError): + return [] + + +def _select_best_file(data_files: list[str]) -> str | None: + if not data_files: + return None + split_lower = DEFAULT_SPLIT + + def score(path: str) -> tuple[int, int]: + name = path.lower() + if f"/{split_lower}/" in name: + return (0, len(path)) + if ( + f"_{split_lower}." in name + or f"-{split_lower}." in name + or f"/{split_lower}." in name + or f"/{split_lower}_" in name + or f"/{split_lower}-" in name + ): + return (1, len(path)) + return (2, len(path)) + + return sorted(data_files, key=score)[0] + + +def _resolve_seed_hf_path(dataset_name: str, data_files: list[str]) -> str | None: + selected = _select_best_file(data_files) + if not selected: + return None + + ext = Path(selected).suffix.lower() + if ext not in DATA_EXTS: + return f"datasets/{dataset_name}/{selected}" + + parent = Path(selected).parent.as_posix() + if not parent or parent == ".": + return f"datasets/{dataset_name}/**/*{ext}" + return f"datasets/{dataset_name}/{parent}/**/*{ext}" + + +def _build_stream_load_kwargs( + *, + dataset_name: str, + split: str, + subset: str | None, + token: str | None, + data_file: str | None = None, +) -> dict[str, Any]: + kwargs: dict[str, Any] = { + "path": dataset_name, + "split": split, + "streaming": True, + } + if data_file: + kwargs["data_files"] = [data_file] + if subset: + kwargs["name"] = subset + if token: + kwargs["token"] = token + return kwargs + + +def _load_preview_rows( + *, + load_dataset_fn, + load_kwargs: dict[str, Any], + preview_size: int, +) -> list[dict[str, Any]]: + streamed_ds = load_dataset_fn(**load_kwargs) + return [row for row in islice(streamed_ds, preview_size)] + + +def _extract_columns(rows: list[dict[str, Any]]) -> list[str]: + columns_seen: dict[str, None] = {} + for row in rows: + for key in row.keys(): + columns_seen[str(key)] = None + return list(columns_seen.keys()) + + +def _sanitize_filename(filename: str) -> str: + name = Path(filename).name.strip().replace("\x00", "") + if not name: + return "seed_upload" + return name + + +def _decode_base64_payload(content_base64: str) -> bytes: + raw = content_base64.strip() + if "," in raw and raw.lower().startswith("data:"): + raw = raw.split(",", 1)[1] + try: + return base64.b64decode(raw, validate=True) + except binascii.Error as exc: + raise HTTPException(status_code=400, detail="invalid base64 payload") from exc + + +def _read_preview_rows_from_local_file(path: Path, preview_size: int) -> list[dict[str, Any]]: + try: + import pandas as pd + except ImportError as exc: + raise HTTPException(status_code=500, detail=f"seed inspect dependencies unavailable: {exc}") from exc + + ext = path.suffix.lower() + try: + if ext == ".csv": + df = pd.read_csv(path, nrows=preview_size) + elif ext == ".jsonl": + df = pd.read_json(path, lines=True).head(preview_size) + elif ext == ".json": + try: + df = pd.read_json(path, lines=True).head(preview_size) + except ValueError: + df = pd.read_json(path).head(preview_size) + else: + raise HTTPException(status_code=422, detail=f"unsupported file type: {ext}") + except HTTPException: + raise + except (ValueError, OSError) as exc: + raise HTTPException(status_code=422, detail=f"seed inspect failed: {exc}") from exc + + rows = df.to_dict(orient="records") + return _serialize_preview_rows(rows) + + +@router.post("/seed/inspect", response_model=SeedInspectResponse) +def inspect_seed_dataset(payload: SeedInspectRequest) -> SeedInspectResponse: + dataset_name = payload.dataset_name.strip() + if not dataset_name or dataset_name.count("/") < 1: + raise HTTPException(status_code=400, detail="dataset_name must be a Hugging Face repo id like org/repo") + + try: + from datasets import load_dataset + except ImportError as exc: + raise HTTPException(status_code=500, detail=f"seed inspect dependencies unavailable: {exc}") from exc + + split = DEFAULT_SPLIT + subset = _normalize_optional_text(payload.subset) + token = _normalize_optional_text(payload.hf_token) + preview_size = int(payload.preview_size) + + preview_rows: list[dict[str, Any]] = [] + data_files = _list_hf_data_files(dataset_name=dataset_name, token=token) + + selected_file = _select_best_file(data_files) + if selected_file: + try: + single_file_kwargs = _build_stream_load_kwargs( + dataset_name=dataset_name, + split=DEFAULT_SPLIT, + subset=subset, + token=token, + data_file=selected_file, + ) + preview_rows = _load_preview_rows( + load_dataset_fn=load_dataset, + load_kwargs=single_file_kwargs, + preview_size=preview_size, + ) + except (ValueError, OSError, RuntimeError): + preview_rows = [] + + if not preview_rows: + try: + split_kwargs = _build_stream_load_kwargs( + dataset_name=dataset_name, + split=split, + subset=subset, + token=token, + ) + preview_rows = _load_preview_rows( + load_dataset_fn=load_dataset, + load_kwargs=split_kwargs, + preview_size=preview_size, + ) + except (ValueError, OSError, RuntimeError) as exc: + raise HTTPException(status_code=422, detail=f"seed inspect failed: {exc}") from exc + + if not preview_rows: + raise HTTPException(status_code=422, detail="dataset appears empty or unreadable") + preview_rows = _serialize_preview_rows(preview_rows) + columns = _extract_columns(preview_rows) + + if not data_files: + resolved_path = f"datasets/{dataset_name}/**/*.parquet" + else: + resolved_path = _resolve_seed_hf_path(dataset_name, data_files) + if not resolved_path: + raise HTTPException(status_code=422, detail="unable to resolve seed dataset path") + + return SeedInspectResponse( + dataset_name=dataset_name, + resolved_path=resolved_path, + columns=columns, + preview_rows=preview_rows, + split=None, + subset=subset, + ) + + +@router.post("/seed/inspect-upload", response_model=SeedInspectResponse) +def inspect_seed_upload(payload: SeedInspectUploadRequest) -> SeedInspectResponse: + filename = _sanitize_filename(payload.filename) + ext = Path(filename).suffix.lower() + if ext not in LOCAL_UPLOAD_EXTS: + allowed = ", ".join(sorted(LOCAL_UPLOAD_EXTS)) + raise HTTPException(status_code=400, detail=f"unsupported file type: {ext}. allowed: {allowed}") + + file_bytes = _decode_base64_payload(payload.content_base64) + if not file_bytes: + raise HTTPException(status_code=400, detail="empty upload payload") + max_size_bytes = 50 * 1024 * 1024 + if len(file_bytes) > max_size_bytes: + raise HTTPException(status_code=413, detail="file too large (max 50MB)") + + SEED_UPLOAD_DIR.mkdir(parents=True, exist_ok=True) + stored_name = f"{uuid4().hex}_{filename}" + stored_path = SEED_UPLOAD_DIR / stored_name + stored_path.write_bytes(file_bytes) + + preview_rows = _read_preview_rows_from_local_file( + stored_path, + int(payload.preview_size), + ) + if not preview_rows: + raise HTTPException(status_code=422, detail="dataset appears empty or unreadable") + columns = _extract_columns(preview_rows) + + return SeedInspectResponse( + dataset_name=filename, + resolved_path=str(stored_path), + columns=columns, + preview_rows=preview_rows, + split=None, + subset=None, + ) diff --git a/studio/backend/routes/data_recipe/validate.py b/studio/backend/routes/data_recipe/validate.py new file mode 100644 index 0000000000..a8755f9410 --- /dev/null +++ b/studio/backend/routes/data_recipe/validate.py @@ -0,0 +1,90 @@ +"""Validation endpoints for data recipe.""" + +from __future__ import annotations + +from typing import Any + +from fastapi import APIRouter, HTTPException + +from core.data_recipe.service import ( + build_config_builder, + create_data_designer, + validate_recipe, +) +from models.data_recipe import RecipePayload, ValidateError, ValidateResponse + +router = APIRouter() + + +def _collect_validation_errors(recipe: dict[str, Any]) -> list[ValidateError]: + try: + from data_designer.engine.compiler import ( + _add_internal_row_id_column_if_needed, + _get_allowed_references, + _resolve_and_add_seed_columns, + ) + from data_designer.engine.validation import ( + ViolationLevel, + validate_data_designer_config, + ) + except ImportError: + return [] + + try: + builder = build_config_builder(recipe) + designer = create_data_designer(recipe) + resource_provider = designer._create_resource_provider( # type: ignore[attr-defined] + "validate-configuration", + builder, + ) + config = builder.build() + _resolve_and_add_seed_columns(config, resource_provider.seed_reader) + _add_internal_row_id_column_if_needed(config) + violations = validate_data_designer_config( + columns=config.columns, + processor_configs=config.processors or [], + allowed_references=_get_allowed_references(config), + ) + except (TypeError, ValueError, AttributeError): + return [] + + errors: list[ValidateError] = [] + for violation in violations: + if violation.level != ViolationLevel.ERROR: + continue + code = getattr(violation.type, "value", None) + path = violation.column if violation.column else None + message = str(violation.message).strip() or "Validation failed." + errors.append( + ValidateError( + message=message, + path=path, + code=code, + ) + ) + return errors + + +@router.post("/validate", response_model=ValidateResponse) +def validate(payload: RecipePayload) -> ValidateResponse: + recipe = payload.recipe + if not recipe.get("columns"): + return ValidateResponse( + valid=False, + errors=[ValidateError(message="Recipe must include columns.")], + ) + + try: + validate_recipe(recipe) + except RuntimeError as exc: + raise HTTPException(status_code=503, detail=str(exc)) from exc + except Exception as exc: + detail = str(exc).strip() or "Validation failed." + parsed_errors = _collect_validation_errors(recipe) + return ValidateResponse( + valid=False, + errors=parsed_errors or [ValidateError(message=detail)], + raw_detail=detail, + ) + + return ValidateResponse(valid=True) diff --git a/studio/backend/routes/datasets.py b/studio/backend/routes/datasets.py index 39119f1123..4a475ff8c2 100644 --- a/studio/backend/routes/datasets.py +++ b/studio/backend/routes/datasets.py @@ -5,7 +5,7 @@ import base64 import io import sys from pathlib import Path -from fastapi import APIRouter, HTTPException +from fastapi import APIRouter, Depends, HTTPException import logging # Add backend directory to path @@ -15,6 +15,7 @@ if str(backend_path) not in sys.path: # Import dataset utilities from utils.datasets import check_dataset_format +from auth.authentication import get_current_subject router = APIRouter() logger = logging.getLogger(__name__) @@ -84,7 +85,10 @@ DATA_EXTS = ( @router.post("/check-format", response_model=CheckFormatResponse) -def check_format(request: CheckFormatRequest): +def check_format( + request: CheckFormatRequest, + current_subject: str = Depends(get_current_subject), +): """ Check if a dataset requires manual column mapping. @@ -188,21 +192,40 @@ def check_format(request: CheckFormatRequest): # Generate preview samples preview_samples = None if not result["requires_manual_mapping"]: - try: - format_result = format_dataset( - preview_slice, - format_type="auto", - custom_format_mapping=result.get("suggested_mapping"), - num_proc=1, # Only 10 preview rows — no need for multiprocessing - ) - processed = format_result["dataset"] - preview_samples = _serialize_preview_rows(processed) - except Exception as e: - logger.warning(f"Processed preview generation failed (non-fatal): {e}") + if result.get("suggested_mapping"): + # Heuristic-detected: show raw data so columns match the API response. + # Processing (column stripping) happens at training time, not preview. preview_samples = _serialize_preview_rows(preview_slice) + else: + try: + format_result = format_dataset( + preview_slice, + format_type="auto", + num_proc=1, # Only 10 preview rows — no need for multiprocessing + ) + processed = format_result["dataset"] + preview_samples = _serialize_preview_rows(processed) + except Exception as e: + logger.warning(f"Processed preview generation failed (non-fatal): {e}") + preview_samples = _serialize_preview_rows(preview_slice) else: preview_samples = _serialize_preview_rows(preview_slice) + # Lightweight URL-based image detection for VLM datasets + warning = None + image_col = result.get("detected_image_column") + if image_col and image_col in (result.get("columns") or []): + try: + sample_val = preview_slice[0][image_col] + if isinstance(sample_val, str) and sample_val.startswith(("http://", "https://")): + warning = ( + "This dataset contains image URLs instead of embedded images. " + "Images will be downloaded during training, which may be slow for large datasets." + ) + logger.info(f"URL-based image column detected: {image_col}") + except Exception: + pass + return CheckFormatResponse( requires_manual_mapping=result["requires_manual_mapping"], detected_format=result["detected_format"], @@ -214,6 +237,7 @@ def check_format(request: CheckFormatRequest): detected_text_column=result.get("detected_text_column"), preview_samples=preview_samples, total_rows=total_rows, + warning=warning, ) except HTTPException: diff --git a/studio/backend/routes/inference.py b/studio/backend/routes/inference.py index 63fe246008..7e1cff7a7d 100644 --- a/studio/backend/routes/inference.py +++ b/studio/backend/routes/inference.py @@ -5,7 +5,7 @@ import sys import time import uuid from pathlib import Path -from fastapi import APIRouter, HTTPException, Request +from fastapi import APIRouter, Depends, HTTPException, Request from fastapi.responses import StreamingResponse, JSONResponse from typing import Optional import json @@ -23,6 +23,7 @@ if str(backend_path) not in sys.path: # Import backend functions try: from core.inference import get_inference_backend + from core.inference.llama_cpp import LlamaCppBackend from utils.models import ModelConfig from utils.inference import load_inference_config except ImportError: @@ -30,6 +31,7 @@ except ImportError: if str(parent_backend) not in sys.path: sys.path.insert(0, str(parent_backend)) from core.inference import get_inference_backend + from core.inference.llama_cpp import LlamaCppBackend from utils.models import ModelConfig from utils.inference import load_inference_config @@ -48,6 +50,7 @@ from models.inference import ( CompletionChoice, CompletionMessage, ) +from auth.authentication import get_current_subject router = APIRouter() logger = logging.getLogger(__name__) @@ -61,36 +64,106 @@ if not logger.handlers: logger.addHandler(handler) logger.setLevel(logging.INFO) +# GGUF inference backend (llama-server) +_llama_cpp_backend = LlamaCppBackend() + +def get_llama_cpp_backend() -> LlamaCppBackend: + return _llama_cpp_backend + @router.post("/load", response_model=LoadResponse) -async def load_model(request: LoadRequest): +async def load_model( + request: LoadRequest, + current_subject: str = Depends(get_current_subject), +): """ Load a model for inference. - + The model_path should be a clean identifier from GET /models/list. Returns inference configuration parameters (temperature, top_p, top_k, min_p) from the model's YAML config, falling back to default.yaml for missing values. + + GGUF models are loaded via llama-server (llama.cpp) instead of Unsloth. """ try: # Ensure correct transformers version for this model architecture from utils.transformers_version import ensure_transformers_version ensure_transformers_version(request.model_path) - backend = get_inference_backend() - # Create config using clean factory method # is_lora is auto-detected from adapter_config.json on disk/HF config = ModelConfig.from_identifier( model_id=request.model_path, hf_token=request.hf_token, + gguf_variant=request.gguf_variant, ) - + if not config: raise HTTPException( status_code=400, detail=f"Invalid model identifier: {request.model_path}" ) - + + # ── GGUF path: load via llama-server ────────────────────── + if config.is_gguf: + llama_backend = get_llama_cpp_backend() + unsloth_backend = get_inference_backend() + + # Unload any active Unsloth model first to free VRAM + if unsloth_backend.active_model_name: + logger.info(f"Unloading Unsloth model '{unsloth_backend.active_model_name}' before loading GGUF") + unsloth_backend.unload_model(unsloth_backend.active_model_name) + + # Route to HF mode or local mode based on config + if config.gguf_hf_repo: + # HF mode: llama-server downloads via -hf "repo:quant" + success = llama_backend.load_model( + hf_repo=config.gguf_hf_repo, + hf_variant=config.gguf_variant, + hf_token=request.hf_token, + model_identifier=config.identifier, + is_vision=config.is_vision, + n_ctx=request.max_seq_length, + ) + else: + # Local mode: llama-server loads via -m + success = llama_backend.load_model( + gguf_path=config.gguf_file, + mmproj_path=config.gguf_mmproj_file, + model_identifier=config.identifier, + is_vision=config.is_vision, + n_ctx=request.max_seq_length, + ) + + if not success: + raise HTTPException( + status_code=500, + detail=f"Failed to load GGUF model: {config.display_name}" + ) + + logger.info(f"Loaded GGUF model via llama-server: {config.identifier}") + + inference_config = load_inference_config(config.identifier) + + return LoadResponse( + status="loaded", + model=config.identifier, + display_name=config.display_name, + is_vision=config.is_vision, + is_lora=False, + is_gguf=True, + inference=inference_config, + ) + + # ── Standard path: load via Unsloth/transformers ────────── + backend = get_inference_backend() + + # Unload any active GGUF model first + llama_backend = get_llama_cpp_backend() + if llama_backend.is_loaded: + logger.info("Unloading GGUF model before loading Unsloth model") + llama_backend.unload_model() + # Auto-detect quantization for LoRA adapters from adapter_config.json # The training pipeline patches this file with "unsloth_training_method" # which is 'qlora' or 'lora'. Only LoRA (16-bit) needs load_in_4bit=False. @@ -129,7 +202,7 @@ async def load_model(request: LoadRequest): load_in_4bit = False except Exception as e: logger.warning(f"Could not read adapter_config.json: {e}") - + # Load the model success = backend.load_model( config=config, @@ -137,27 +210,28 @@ async def load_model(request: LoadRequest): load_in_4bit=load_in_4bit, hf_token=request.hf_token, ) - + if not success: raise HTTPException( status_code=500, detail=f"Failed to load model: {config.display_name}" ) - + logger.info(f"Loaded model: {config.identifier}") - + # Load inference configuration parameters inference_config = load_inference_config(config.identifier) - + return LoadResponse( status="loaded", model=config.identifier, display_name=config.display_name, is_vision=config.is_vision, is_lora=config.is_lora, + is_gguf=False, inference=inference_config, ) - + except HTTPException: raise except Exception as e: @@ -169,16 +243,28 @@ async def load_model(request: LoadRequest): @router.post("/unload", response_model=UnloadResponse) -async def unload_model(request: UnloadRequest): +async def unload_model( + request: UnloadRequest, + current_subject: str = Depends(get_current_subject), +): """ Unload a model from memory. + Routes to the correct backend (llama-server for GGUF, Unsloth otherwise). """ try: + # Check if the GGUF backend has this model loaded + llama_backend = get_llama_cpp_backend() + if llama_backend.is_loaded and llama_backend.model_identifier == request.model_path: + llama_backend.unload_model() + logger.info(f"Unloaded GGUF model: {request.model_path}") + return UnloadResponse(status="unloaded", model=request.model_path) + + # Otherwise, unload from Unsloth backend backend = get_inference_backend() backend.unload_model(request.model_path) logger.info(f"Unloaded model: {request.model_path}") return UnloadResponse(status="unloaded", model=request.model_path) - + except Exception as e: logger.error(f"Error unloading model: {e}", exc_info=True) raise HTTPException( @@ -188,7 +274,10 @@ async def unload_model(request: UnloadRequest): @router.post("/generate/stream") -async def generate_stream(request: GenerateRequest): +async def generate_stream( + request: GenerateRequest, + current_subject: str = Depends(get_current_subject), +): """ Generate a chat response with Server-Sent Events (SSE) streaming. @@ -261,25 +350,43 @@ async def generate_stream(request: GenerateRequest): @router.get("/status", response_model=InferenceStatusResponse) -async def get_status(): +async def get_status( + current_subject: str = Depends(get_current_subject), +): """ Get current inference backend status. + Reports whichever backend (Unsloth or llama-server) is currently active. """ try: + llama_backend = get_llama_cpp_backend() + + # If a GGUF model is loaded via llama-server, report that + if llama_backend.is_loaded: + return InferenceStatusResponse( + active_model=llama_backend.model_identifier, + is_vision=llama_backend.is_vision, + is_gguf=True, + gguf_variant=llama_backend.hf_variant, + loading=[], + loaded=[llama_backend.model_identifier], + ) + + # Otherwise, report Unsloth backend status backend = get_inference_backend() - + is_vision = False if backend.active_model_name: model_info = backend.models.get(backend.active_model_name, {}) is_vision = model_info.get("is_vision", False) - + return InferenceStatusResponse( active_model=backend.active_model_name, is_vision=is_vision, + is_gguf=False, loading=list(getattr(backend, 'loading_models', set())), loaded=list(backend.models.keys()), ) - + except Exception as e: logger.error(f"Error getting status: {e}", exc_info=True) raise HTTPException( @@ -349,7 +456,11 @@ def _extract_content_parts( @router.post("/chat/completions") -async def openai_chat_completions(payload: ChatCompletionRequest, request: Request): +async def openai_chat_completions( + payload: ChatCompletionRequest, + request: Request, + current_subject: str = Depends(get_current_subject), +): """ OpenAI-compatible chat completions endpoint. @@ -358,29 +469,163 @@ async def openai_chat_completions(payload: ChatCompletionRequest, request: Reque Streaming (default): returns SSE chunks matching OpenAI's format. Non-streaming: returns a single ChatCompletion JSON object. - """ - backend = get_inference_backend() - if not backend.active_model_name: - raise HTTPException( - status_code=400, - detail="No model loaded. Call POST /inference/load first.", - ) + Automatically routes to the correct backend: + - GGUF models → llama-server via LlamaCppBackend + - Other models → Unsloth/transformers via InferenceBackend + """ + llama_backend = get_llama_cpp_backend() + using_gguf = llama_backend.is_loaded + + # ── Determine which backend is active ───────────────────── + if using_gguf: + model_name = llama_backend.model_identifier or payload.model + else: + backend = get_inference_backend() + if not backend.active_model_name: + raise HTTPException( + status_code=400, + detail="No model loaded. Call POST /inference/load first.", + ) + model_name = backend.active_model_name or payload.model # ── Parse messages (handles multimodal content parts) ───── system_prompt, chat_messages, extracted_image_b64 = _extract_content_parts( payload.messages ) - # If no non-system messages were provided, error out if not chat_messages: raise HTTPException( status_code=400, detail="At least one non-system message is required.", ) - # ── Decode image (from content parts OR legacy field) ───── - # Content-part images take priority; fall back to legacy field + # ── GGUF path: proxy to llama-server /v1/chat/completions ── + if using_gguf: + # Reject images if this GGUF model doesn't support vision + image_b64 = extracted_image_b64 or payload.image_base64 + if image_b64 and not llama_backend.is_vision: + raise HTTPException( + status_code=400, + detail="Image provided but current GGUF model does not support vision.", + ) + + # Build message list with system prompt prepended + gguf_messages = [] + if system_prompt: + gguf_messages.append({"role": "system", "content": system_prompt}) + gguf_messages.extend(chat_messages) + + cancel_event = threading.Event() + + completion_id = f"chatcmpl-{uuid.uuid4().hex[:12]}" + created = int(time.time()) + + def gguf_generate(): + return llama_backend.generate_chat_completion( + messages=gguf_messages, + image_b64=image_b64, + temperature=payload.temperature, + top_p=payload.top_p, + top_k=payload.top_k, + min_p=payload.min_p, + max_tokens=payload.max_tokens or 512, + repetition_penalty=payload.repetition_penalty, + cancel_event=cancel_event, + ) + + if payload.stream: + async def gguf_stream_chunks(): + try: + # First chunk: role + first_chunk = ChatCompletionChunk( + id=completion_id, + created=created, + model=model_name, + choices=[ChunkChoice( + delta=ChoiceDelta(role="assistant"), + finish_reason=None, + )], + ) + yield f"data: {first_chunk.model_dump_json(exclude_none=True)}\n\n" + + # Content chunks — llama backend yields cumulative text + prev_text = "" + for cumulative in gguf_generate(): + if await request.is_disconnected(): + cancel_event.set() + return + new_text = cumulative[len(prev_text):] + prev_text = cumulative + if not new_text: + continue + chunk = ChatCompletionChunk( + id=completion_id, + created=created, + model=model_name, + choices=[ChunkChoice( + delta=ChoiceDelta(content=new_text), + finish_reason=None, + )], + ) + yield f"data: {chunk.model_dump_json(exclude_none=True)}\n\n" + + # Final chunk + final_chunk = ChatCompletionChunk( + id=completion_id, + created=created, + model=model_name, + choices=[ChunkChoice( + delta=ChoiceDelta(), + finish_reason="stop", + )], + ) + yield f"data: {final_chunk.model_dump_json(exclude_none=True)}\n\n" + yield "data: [DONE]\n\n" + + except asyncio.CancelledError: + cancel_event.set() + raise + except Exception as e: + logger.error(f"Error during GGUF streaming: {e}", exc_info=True) + error_chunk = { + "error": {"message": str(e), "type": "server_error"}, + } + yield f"data: {json.dumps(error_chunk)}\n\n" + + return StreamingResponse( + gguf_stream_chunks(), + media_type="text/event-stream", + headers={ + "Cache-Control": "no-cache", + "Connection": "keep-alive", + "X-Accel-Buffering": "no", + }, + ) + else: + try: + full_text = "" + for token in gguf_generate(): + full_text = token + + response = ChatCompletion( + id=completion_id, + created=created, + model=model_name, + choices=[CompletionChoice( + message=CompletionMessage(content=full_text), + finish_reason="stop", + )], + ) + return JSONResponse(content=response.model_dump()) + + except Exception as e: + logger.error(f"Error during GGUF completion: {e}", exc_info=True) + raise HTTPException(status_code=500, detail=str(e)) + + # ── Standard Unsloth path ───────────────────────────────── + + # Decode image (from content parts OR legacy field) image_b64 = extracted_image_b64 or payload.image_base64 image = None @@ -406,7 +651,7 @@ async def openai_chat_completions(payload: ChatCompletionRequest, request: Reque except Exception as e: raise HTTPException(status_code=400, detail=f"Failed to decode image: {e}") - # ── Shared generation kwargs ────────────────────────────── + # Shared generation kwargs gen_kwargs = dict( messages=chat_messages, system_prompt=system_prompt, @@ -419,11 +664,10 @@ async def openai_chat_completions(payload: ChatCompletionRequest, request: Reque repetition_penalty=payload.repetition_penalty, ) - # ── Choose generation path (adapter-controlled or standard) ── + # Choose generation path (adapter-controlled or standard) cancel_event = threading.Event() if payload.use_adapter is not None: - # Compare mode: toggle adapter state atomically with generation def generate(): return backend.generate_with_adapter_control( use_adapter=payload.use_adapter, @@ -431,11 +675,9 @@ async def openai_chat_completions(payload: ChatCompletionRequest, request: Reque **gen_kwargs, ) else: - # Standard path: no adapter toggling def generate(): return backend.generate_chat_response(cancel_event=cancel_event, **gen_kwargs) - model_name = backend.active_model_name or payload.model completion_id = f"chatcmpl-{uuid.uuid4().hex[:12]}" created = int(time.time()) @@ -443,7 +685,6 @@ async def openai_chat_completions(payload: ChatCompletionRequest, request: Reque if payload.stream: async def stream_chunks(): try: - # First chunk: send the role first_chunk = ChatCompletionChunk( id=completion_id, created=created, @@ -455,8 +696,6 @@ async def openai_chat_completions(payload: ChatCompletionRequest, request: Reque ) yield f"data: {first_chunk.model_dump_json(exclude_none=True)}\n\n" - # Content chunks — generate_chat_response yields cumulative - # text, so we diff to get incremental deltas. prev_text = "" for cumulative in generate(): if await request.is_disconnected(): @@ -478,7 +717,6 @@ async def openai_chat_completions(payload: ChatCompletionRequest, request: Reque ) yield f"data: {chunk.model_dump_json(exclude_none=True)}\n\n" - # Final chunk: finish_reason = stop final_chunk = ChatCompletionChunk( id=completion_id, created=created, @@ -518,7 +756,7 @@ async def openai_chat_completions(payload: ChatCompletionRequest, request: Reque try: full_text = "" for token in generate(): - full_text = token # generate_stream yields cumulative text + full_text = token response = ChatCompletion( id=completion_id, diff --git a/studio/backend/routes/models.py b/studio/backend/routes/models.py index cd2aa8b625..e0c149185a 100644 --- a/studio/backend/routes/models.py +++ b/studio/backend/routes/models.py @@ -18,12 +18,15 @@ from auth.authentication import get_current_subject try: from utils.models import ( scan_trained_loras, + scan_exported_models, load_model_defaults, get_base_model_from_lora, is_vision_model, scan_checkpoints, + list_gguf_variants, ModelConfig, ) + from utils.models.model_config import _pick_best_gguf, _extract_quant_label from core.inference import get_inference_backend except ImportError: # Fallback: try to import from parent directory @@ -32,12 +35,15 @@ except ImportError: sys.path.insert(0, str(parent_backend)) from utils.models import ( scan_trained_loras, + scan_exported_models, load_model_defaults, get_base_model_from_lora, is_vision_model, scan_checkpoints, + list_gguf_variants, ModelConfig, ) + from utils.models.model_config import _pick_best_gguf, _extract_quant_label from core.inference import get_inference_backend from models import ( @@ -51,6 +57,7 @@ from models import ( LoRAInfo, ModelListResponse, ) +from models.models import GgufVariantDetail, GgufVariantsResponse from models.responses import LoRABaseModelResponse, VisionCheckResponse router = APIRouter() @@ -88,6 +95,7 @@ def _scan_models_dir(models_dir: Path) -> List[LocalModelInfo]: or (child / "adapter_config.json").exists() or any(child.glob("*.safetensors")) or any(child.glob("*.bin")) + or any(child.glob("*.gguf")) ) if not has_model_files: continue @@ -104,6 +112,23 @@ def _scan_models_dir(models_dir: Path) -> List[LocalModelInfo]: updated_at=updated_at, ), ) + # Also scan for standalone .gguf files directly in the models directory + for gguf_file in models_dir.glob("*.gguf"): + if gguf_file.is_file(): + try: + updated_at = gguf_file.stat().st_mtime + except OSError: + updated_at = None + found.append( + LocalModelInfo( + id=str(gguf_file), + display_name=gguf_file.stem, + path=str(gguf_file), + source="models_dir", + updated_at=updated_at, + ), + ) + return found @@ -294,35 +319,45 @@ async def get_model_config( @router.get("/loras") async def scan_loras( outputs_dir: str = Query(default="./outputs", description="Directory to scan for LoRA adapters"), + exports_dir: str = Query(default="./exports", description="Directory to scan for exported models"), current_subject: str = Depends(get_current_subject), ): """ - Scan for trained LoRA adapters in the outputs directory. - - This endpoint wraps the backend scan_trained_loras function. + Scan for trained LoRA adapters and exported models. + + Returns both training outputs (from outputs_dir) and exported models + (from exports_dir) in a single list, distinguished by source field. """ try: - # Call backend scan function - trained_loras = scan_trained_loras(outputs_dir=outputs_dir) - - # Convert to LoRAInfo objects lora_list = [] + + # Scan training outputs + trained_loras = scan_trained_loras(outputs_dir=outputs_dir) for display_name, adapter_path in trained_loras: - # Get base model if available base_model = get_base_model_from_lora(adapter_path) - - lora_info = LoRAInfo( + lora_list.append(LoRAInfo( display_name=display_name, adapter_path=adapter_path, - base_model=base_model - ) - lora_list.append(lora_info) - + base_model=base_model, + source="training", + )) + + # Scan exported models (merged, LoRA, base — skips GGUF) + exported = scan_exported_models(exports_dir=exports_dir) + for display_name, model_path, export_type, base_model in exported: + lora_list.append(LoRAInfo( + display_name=display_name, + adapter_path=model_path, + base_model=base_model, + source="exported", + export_type=export_type, + )) + return LoRAScanResponse( loras=lora_list, outputs_dir=outputs_dir ) - + except Exception as e: logger.error(f"Error scanning LoRAs: {e}", exc_info=True) raise HTTPException( @@ -398,6 +433,49 @@ async def check_vision_model( detail=f"Failed to check vision model: {str(e)}" ) +@router.get("/gguf-variants", response_model=GgufVariantsResponse) +async def get_gguf_variants( + repo_id: str = Query(..., description="HuggingFace repo ID (e.g. 'unsloth/gemma-3-4b-it-GGUF')"), + hf_token: Optional[str] = Query(None, description="HuggingFace token for private repos"), + current_subject: str = Depends(get_current_subject), +): + """ + List available GGUF quantization variants for a HuggingFace repo. + + Returns all available quantization variants (Q4_K_M, Q8_0, BF16, etc.) + with file sizes, whether the model supports vision, and the recommended + default variant. + """ + try: + variants, has_vision = list_gguf_variants(repo_id, hf_token=hf_token) + + # Determine default variant + filenames = [v.filename for v in variants] + best = _pick_best_gguf(filenames) + default_variant = _extract_quant_label(best) if best else None + + return GgufVariantsResponse( + repo_id=repo_id, + variants=[ + GgufVariantDetail( + filename=v.filename, + quant=v.quant, + size_bytes=v.size_bytes, + ) + for v in variants + ], + has_vision=has_vision, + default_variant=default_variant, + ) + + except Exception as e: + logger.error(f"Error listing GGUF variants for '{repo_id}': {e}", exc_info=True) + raise HTTPException( + status_code=500, + detail=f"Failed to list GGUF variants: {str(e)}", + ) + + @router.get("/checkpoints", response_model=CheckpointListResponse) async def list_checkpoints( outputs_dir: str = Query( diff --git a/studio/backend/routes/training.py b/studio/backend/routes/training.py index 6917bed39e..c73cb661f9 100644 --- a/studio/backend/routes/training.py +++ b/studio/backend/routes/training.py @@ -154,6 +154,8 @@ async def start_training( "train_split": request.train_split, "eval_split": request.eval_split, "eval_steps": request.eval_steps, + "dataset_slice_start": request.dataset_slice_start, + "dataset_slice_end": request.dataset_slice_end, "custom_format_mapping": request.custom_format_mapping, "num_epochs": request.num_epochs, "learning_rate": request.learning_rate, diff --git a/studio/backend/utils/datasets/chat_templates.py b/studio/backend/utils/datasets/chat_templates.py index 6420aa899d..52c876291d 100644 --- a/studio/backend/utils/datasets/chat_templates.py +++ b/studio/backend/utils/datasets/chat_templates.py @@ -283,9 +283,11 @@ def apply_chat_template_to_dataset( } if not isinstance(dataset, IterableDataset): - from multiprocessing import cpu_count + from utils.hardware import safe_num_proc if num_proc is None or type(num_proc) is not int: - num_proc = max(1, cpu_count() // 3) + num_proc = safe_num_proc() + else: + num_proc = safe_num_proc(num_proc) dataset_map_kwargs['num_proc'] = num_proc dataset_map_kwargs['desc'] = "Applying template to Alpaca format" @@ -347,9 +349,11 @@ def apply_chat_template_to_dataset( } if not isinstance(dataset, IterableDataset): - from multiprocessing import cpu_count + from utils.hardware import safe_num_proc if num_proc is None or type(num_proc) is not int: - num_proc = max(1, cpu_count() // 3) + num_proc = safe_num_proc() + else: + num_proc = safe_num_proc(num_proc) dataset_map_kwargs['num_proc'] = num_proc dataset_map_kwargs['desc'] = f"Applying chat template to {final_format}" diff --git a/studio/backend/utils/datasets/dataset_utils.py b/studio/backend/utils/datasets/dataset_utils.py index a75f78d37c..9e1f54a75c 100644 --- a/studio/backend/utils/datasets/dataset_utils.py +++ b/studio/backend/utils/datasets/dataset_utils.py @@ -126,38 +126,76 @@ def check_dataset_format(dataset, is_vlm: bool = False) -> dict: "multimodal_columns": None, } +# Normalise any format-specific role to canonical chatml (user/assistant/system) +_TO_CHATML = { + "user": "user", "human": "user", "instruction": "user", + "assistant": "assistant", "gpt": "assistant", "output": "assistant", + "system": "system", "input": "system", +} +_CHATML_ROLE_ORDER = ("system", "user", "assistant") +_CHATML_TO_ALPACA = {"user": "instruction", "system": "input", "assistant": "output"} + + def _apply_user_mapping(dataset, mapping: dict, batch_size: int = 1000): """ Apply user-provided column mapping to convert dataset to conversations format. - - Args: - dataset: HuggingFace dataset - mapping: Dict like {"question": "user", "answer": "assistant", "context": "system"} - batch_size: Batch size for processing - + + Accepts chatml (user/assistant/system), sharegpt (human/gpt/system), and + alpaca (instruction/input/output) role names — all normalised to chatml output. + Returns: - Dataset with single 'conversations' column (no extra columns preserved) + Dataset with single 'conversations' column """ + # Pre-compute: group columns by canonical chatml role + role_groups: dict[str, list[str]] = {r: [] for r in _CHATML_ROLE_ORDER} + for col_name, role in mapping.items(): + canonical = _TO_CHATML.get(role) + if canonical: + role_groups[canonical].append(col_name) + def _convert(examples): - num_examples = len(examples[list(examples.keys())[0]]) + num = len(next(iter(examples.values()))) conversations = [] - - for i in range(num_examples): + for i in range(num): convo = [] - role_order = ['system', 'user', 'assistant'] - - for target_role in role_order: - for col_name, role in mapping.items(): - if role == target_role and col_name in examples: - content = examples[col_name][i] - # User explicitly mapped - always include even if empty - convo.append({"role": role, "content": str(content) if content else ""}) - + for chatml_role in _CHATML_ROLE_ORDER: + for col in role_groups[chatml_role]: + if col in examples: + content = examples[col][i] + convo.append({"role": chatml_role, "content": str(content) if content else ""}) conversations.append(convo) - - # ONLY return conversations - no extra columns return {"conversations": conversations} - + + return dataset.map(_convert, batched=True, batch_size=batch_size, remove_columns=dataset.column_names) + + +def _apply_user_mapping_alpaca(dataset, mapping: dict, batch_size: int = 1000): + """ + Apply user-provided column mapping to convert dataset to Alpaca format. + + Accepts any format's role names — normalises via _TO_CHATML, then maps + user → instruction, system → input, assistant → output. + + Returns: + Dataset with instruction/input/output columns + """ + col_for: dict[str, str | None] = {"instruction": None, "input": None, "output": None} + for col_name, role in mapping.items(): + canonical = _TO_CHATML.get(role) + alpaca_field = _CHATML_TO_ALPACA.get(canonical) if canonical else None + if alpaca_field: + col_for[alpaca_field] = col_name + + def _convert(examples): + num = len(next(iter(examples.values()))) + instructions, inputs, outputs = [], [], [] + for i in range(num): + for field, dest in (("instruction", instructions), ("input", inputs), ("output", outputs)): + col = col_for[field] + val = str(examples[col][i]) if col and col in examples and examples[col][i] else "" + dest.append(val) + return {"instruction": instructions, "input": inputs, "output": outputs} + return dataset.map(_convert, batched=True, batch_size=batch_size, remove_columns=dataset.column_names) @@ -191,20 +229,30 @@ def format_dataset( # Detect multimodal first (needed for all flows) multimodal_info = detect_multimodal_dataset(dataset) - # NEW: If user provided explicit mapping, skip detection and apply directly + # If user provided explicit mapping, skip detection and apply in the requested format if custom_format_mapping: try: - mapped_dataset = _apply_user_mapping(dataset, custom_format_mapping, batch_size) + if format_type == "alpaca": + mapped_dataset = _apply_user_mapping_alpaca(dataset, custom_format_mapping, batch_size) + final_format = "alpaca" + chat_column = None + else: + # auto / chatml / sharegpt / conversational — all produce chatml conversations + # (sharegpt is always standardized to role/content internally) + mapped_dataset = _apply_user_mapping(dataset, custom_format_mapping, batch_size) + final_format = "chatml_conversations" + chat_column = "conversations" + return { "dataset": mapped_dataset, "detected_format": "user_mapped", - "final_format": "chatml_conversations", - "chat_column": "conversations", + "final_format": final_format, + "chat_column": chat_column, "is_standardized": True, "requires_manual_mapping": False, "is_multimodal": multimodal_info["is_multimodal"], "multimodal_info": multimodal_info, - "warnings": [f"Applied user-provided column mapping: {custom_format_mapping}"] + "warnings": [f"Applied user-provided column mapping ({format_type}): {custom_format_mapping}"] } except Exception as e: return { @@ -224,7 +272,7 @@ def format_dataset( detected = detect_dataset_format(dataset) warnings = [] - # Add multimodal warning if detected + # Add multimodal warning if detected if multimodal_info["is_multimodal"]: warnings.append( f"Multimodal dataset detected. Found columns: {multimodal_info['multimodal_columns']}" @@ -309,48 +357,25 @@ def format_dataset( conversations = [] num_examples = len(examples[list(examples.keys())[0]]) - # NEW: Check if this is user-provided or auto-detected - is_user_provided = custom_format_mapping is not None # Passed explicitly - - # Preserve non-mapped columns ONLY if auto-detected - preserved_columns = {} - if not is_user_provided: # Only preserve for auto-detection - all_columns = set(examples.keys()) - mapped_columns = set(custom_mapping.keys()) - non_mapped_columns = all_columns - mapped_columns - - for col in non_mapped_columns: - preserved_columns[col] = examples[col] + # Preserve non-mapped columns + all_columns = set(examples.keys()) + mapped_columns = set(custom_mapping.keys()) + preserved_columns = { + col: examples[col] + for col in all_columns - mapped_columns + } for i in range(num_examples): convo = [] - - # Enforce standard role order - role_order = ['system', 'user', 'assistant'] - - for target_role in role_order: + for target_role in ['system', 'user', 'assistant']: for col_name, role in custom_mapping.items(): if role == target_role and col_name in examples: content = examples[col_name][i] - - # NEW: Different behavior based on mapping source - if is_user_provided: - # User explicitly mapped this - always include even if empty - convo.append({"role": role, "content": str(content) if content else ""}) - else: - # Auto-detected - skip empty (original behavior) - if content and str(content).strip(): - convo.append({"role": role, "content": str(content)}) - + if content and str(content).strip(): + convo.append({"role": role, "content": str(content)}) conversations.append(convo) - result = {"conversations": conversations} - - # Only add preserved columns if auto-detected - if not is_user_provided: - result.update(preserved_columns) - - return result + return {"conversations": conversations, **preserved_columns} try: @@ -459,7 +484,7 @@ def format_dataset( } # CHATML MODE: Convert to ChatML - elif format_type in ["chatml", "conversational"]: + elif format_type in ["chatml", "conversational", "sharegpt"]: if detected["format"] == "alpaca": converted = convert_alpaca_to_chatml(dataset, batch_size, num_proc) @@ -508,36 +533,38 @@ def format_dataset( else: warnings.append(f"Unknown format, attempting standardization") - try: - standardized = standardize_chat_format( - dataset, tokenizer, aliases_for_system, - aliases_for_user, aliases_for_assistant, - batch_size, num_proc - ) - return { - "dataset": standardized, - "detected_format": "unknown", - "final_format": f"chatml_{detected['chat_column']}", - "chat_column": detected["chat_column"], - "is_standardized": True, - "requires_manual_mapping": False, - "is_multimodal": multimodal_info["is_multimodal"], - "multimodal_info": multimodal_info, - "warnings": warnings - } - except Exception as e: - warnings.append(f"Standardization failed: {e}") - return { - "dataset": dataset, - "detected_format": "unknown", - "final_format": "unknown", - "chat_column": detected["chat_column"], - "is_standardized": False, - "requires_manual_mapping": True, - "is_multimodal": multimodal_info["is_multimodal"], - "multimodal_info": multimodal_info, - "warnings": warnings - } + if detected["chat_column"]: + try: + standardized = standardize_chat_format( + dataset, tokenizer, aliases_for_system, + aliases_for_user, aliases_for_assistant, + batch_size, num_proc + ) + return { + "dataset": standardized, + "detected_format": "unknown", + "final_format": f"chatml_{detected['chat_column']}", + "chat_column": detected["chat_column"], + "is_standardized": True, + "requires_manual_mapping": False, + "is_multimodal": multimodal_info["is_multimodal"], + "multimodal_info": multimodal_info, + "warnings": warnings + } + except Exception as e: + warnings.append(f"Standardization failed: {e}") + + return { + "dataset": dataset, + "detected_format": "unknown", + "final_format": "unknown", + "chat_column": detected["chat_column"], + "is_standardized": False, + "requires_manual_mapping": True, + "is_multimodal": multimodal_info["is_multimodal"], + "multimodal_info": multimodal_info, + "warnings": warnings + } else: raise ValueError(f"Unknown format_type: {format_type}") @@ -566,6 +593,7 @@ def format_and_template_dataset( aliases_for_assistant=["gpt", "assistant", "output",], batch_size=1000, num_proc=None, + progress_callback=None, ): """ Convenience function that combines format_dataset and apply_chat_template_to_dataset. @@ -611,6 +639,7 @@ def format_and_template_dataset( text_column=user_vlm_text_column, image_column=user_vlm_image_column, dataset_name=dataset_name, + progress_callback=progress_callback, ) warnings.append(f"Applied user VLM mapping: image='{user_vlm_image_column}', text='{user_vlm_text_column}'") @@ -707,6 +736,7 @@ def format_and_template_dataset( text_column=vlm_text_column, image_column=vlm_image_column, dataset_name=dataset_name, + progress_callback=progress_callback, ) if vlm_instruction: @@ -768,8 +798,10 @@ def format_and_template_dataset( ) # Step 2: Apply chat template - if "gemma" in model_name.lower() and not dataset_info["is_multimodal"] and (format_type != "alpaca" or (format_type == "auto" and dataset_info["detected_format"] != "alpaca")): - print("remove_bos_prefix is true") + # Gemma emits a leading that must be stripped for text-only chatml/sharegpt. + is_alpaca = format_type == "alpaca" or (format_type == "auto" and dataset_info["detected_format"] == "alpaca") + is_gemma = "gemma" in model_name.lower() + if is_gemma and not dataset_info["is_multimodal"] and not is_alpaca: remove_bos_prefix = True template_result = apply_chat_template_to_dataset( dataset_info=dataset_info, @@ -791,14 +823,24 @@ def format_and_template_dataset( all_warnings = dataset_info.get("warnings", []) + template_result.get("warnings", []) all_errors = template_result.get("errors", []) + # If format_dataset returned "unknown" but apply_chat_template rescued + # it via heuristic detection, update final_format to reflect reality. + final_format = dataset_info["final_format"] + requires_manual = dataset_info.get("requires_manual_mapping", False) + if final_format == "unknown" and template_result["success"]: + out_ds = template_result["dataset"] + if hasattr(out_ds, "column_names") and "text" in out_ds.column_names: + final_format = "chatml_conversations" + requires_manual = False + return { "dataset": template_result["dataset"], "detected_format": dataset_info["detected_format"], - "final_format": dataset_info["final_format"], + "final_format": final_format, "chat_column": dataset_info.get("chat_column"), "is_vlm": False, # This is LLM flow "success": template_result["success"], - "requires_manual_mapping": dataset_info.get("requires_manual_mapping", False), + "requires_manual_mapping": requires_manual, "warnings": all_warnings, "errors": all_errors, "summary": summary, diff --git a/studio/backend/utils/datasets/format_conversion.py b/studio/backend/utils/datasets/format_conversion.py index a97db20ba4..41a9617857 100644 --- a/studio/backend/utils/datasets/format_conversion.py +++ b/studio/backend/utils/datasets/format_conversion.py @@ -107,10 +107,12 @@ def standardize_chat_format( } if not isinstance(dataset, IterableDataset): - from multiprocessing import cpu_count + from utils.hardware import safe_num_proc if num_proc is None or type(num_proc) is not int: - num_proc = max(1, cpu_count() // 3) + num_proc = safe_num_proc() + else: + num_proc = safe_num_proc(num_proc) dataset_map_kwargs['num_proc'] = num_proc dataset_map_kwargs['desc'] = "Standardizing chat format" @@ -173,10 +175,12 @@ def convert_chatml_to_alpaca(dataset, batch_size=1000, num_proc=None): } if not isinstance(dataset, IterableDataset): - from multiprocessing import cpu_count + from utils.hardware import safe_num_proc if num_proc is None or type(num_proc) is not int: - num_proc = max(1, cpu_count() // 3) + num_proc = safe_num_proc() + else: + num_proc = safe_num_proc(num_proc) dataset_map_kwargs['num_proc'] = num_proc dataset_map_kwargs['desc'] = "Converting ChatML to Alpaca format" @@ -221,10 +225,12 @@ def convert_alpaca_to_chatml(dataset, batch_size=1000, num_proc=None): } if not isinstance(dataset, IterableDataset): - from multiprocessing import cpu_count + from utils.hardware import safe_num_proc if num_proc is None or type(num_proc) is not int: - num_proc = max(1, cpu_count() // 3) + num_proc = safe_num_proc() + else: + num_proc = safe_num_proc(num_proc) dataset_map_kwargs['num_proc'] = num_proc dataset_map_kwargs['desc'] = "Converting Alpaca to ChatML format" @@ -232,24 +238,51 @@ def convert_alpaca_to_chatml(dataset, batch_size=1000, num_proc=None): return dataset.map(_convert, **dataset_map_kwargs) +def _format_eta(seconds): + """Format seconds into a human-readable ETA string.""" + if seconds < 60: + return f"{seconds:.0f}s" + elif seconds < 3600: + m, s = divmod(int(seconds), 60) + return f"{m}m {s}s" + else: + h, remainder = divmod(int(seconds), 3600) + m, _ = divmod(remainder, 60) + return f"{h}h {m}m" + + def convert_to_vlm_format( dataset, instruction=None, text_column="text", image_column="image", dataset_name=None, + progress_callback=None, ): """ Converts simple {image, text} format to VLM messages format. Returns a LIST, not a HuggingFace Dataset (to preserve PIL Images). + For URL-based image datasets, runs a 200-sample parallel probe first to + estimate download speed and failure rate, then reports time estimate or + warning through progress_callback before proceeding with the full conversion. + + Args: + progress_callback: Optional callable(status_message=str) to report + progress to the training overlay. + Returns: list: List of dicts with 'messages' field """ from PIL import Image from .vlm_processing import generate_smart_vlm_instruction + def _notify(msg): + """Send status update to the training overlay if callback is available.""" + if progress_callback: + progress_callback(status_message=msg) + # Generate smart instruction if not provided if instruction is None: instruction_info = generate_smart_vlm_instruction( @@ -275,12 +308,17 @@ def convert_to_vlm_format( def _convert_single_sample(sample): """Convert a single sample to VLM format.""" - # Get image (might be PIL Image or path) + # Get image (might be PIL Image, local path, or URL) image_data = sample[image_column] - # Handle image paths if isinstance(image_data, str): - image_data = Image.open(image_data).convert("RGB") + if image_data.startswith(("http://", "https://")): + import fsspec + from io import BytesIO + with fsspec.open(image_data, "rb", expand=True) as f: + image_data = Image.open(BytesIO(f.read())).convert("RGB") + else: + image_data = Image.open(image_data).convert("RGB") # Get text text_data = sample[text_column] @@ -311,11 +349,143 @@ def convert_to_vlm_format( # Return dict with messages return {"messages": messages} - # Use list comprehension and return the LIST directly - print(f"🔄 Converting {len(dataset)} samples to VLM format...") - converted_list = [_convert_single_sample(sample) for sample in dataset] + total = len(dataset) + first_image = next(iter(dataset))[image_column] + has_urls = isinstance(first_image, str) and first_image.startswith(("http://", "https://")) - print(f"✅ Converted {len(converted_list)} samples") + # ── URL probe: 200 samples with parallel workers to estimate speed + failure rate ── + PROBE_SIZE = 200 + MAX_FAIL_RATE = 0.3 + + if has_urls and total > PROBE_SIZE: + import time + from concurrent.futures import ThreadPoolExecutor, as_completed + from utils.hardware import safe_num_proc + + num_workers = safe_num_proc() + _notify(f"Probing {PROBE_SIZE} image URLs with {num_workers} workers...") + print(f"🔍 Probing {PROBE_SIZE}/{total} image URLs with {num_workers} workers...") + + probe_samples = [dataset[i] for i in range(PROBE_SIZE)] + probe_ok = 0 + probe_fail = 0 + probe_start = time.time() + + with ThreadPoolExecutor(max_workers=num_workers) as executor: + futures = {executor.submit(_convert_single_sample, s): s for s in probe_samples} + for future in as_completed(futures): + try: + future.result() + probe_ok += 1 + except Exception: + probe_fail += 1 + + probe_elapsed = time.time() - probe_start + probe_total = probe_ok + probe_fail + fail_rate = probe_fail / probe_total if probe_total > 0 else 0 + throughput = probe_total / probe_elapsed if probe_elapsed > 0 else 0 + + if fail_rate >= MAX_FAIL_RATE: + msg = ( + f"⚠️ {fail_rate:.0%} of the first {PROBE_SIZE} images failed to download " + f"({probe_fail}/{probe_total}). " + "This dataset has too many broken or unreachable image URLs. " + "Consider using a dataset with embedded images instead." + ) + print(msg) + _notify(msg) + raise ValueError(msg) + + # Estimate total time for remaining samples + remaining = total - PROBE_SIZE + estimated_seconds = remaining / throughput if throughput > 0 else 0 + eta_str = _format_eta(estimated_seconds) + + info_msg = ( + f"Downloading {total:,} images ({num_workers} workers, ~{throughput:.1f} img/s). " + f"Estimated time: ~{eta_str}" + ) + if probe_fail > 0: + info_msg += f" | {fail_rate:.0%} broken URLs will be skipped" + + print(f"✅ Probe passed: {probe_ok}/{probe_total} ok, {probe_fail} failed ({fail_rate:.0%}), {throughput:.1f} img/s") + print(f"⏱️ Estimated time for {total:,} samples: ~{eta_str}") + _notify(info_msg) + + # ── Full conversion with progress ── + from tqdm import tqdm + + print(f"🔄 Converting {total} samples to VLM format...") + converted_list = [] + failed_count = 0 + + if has_urls: + # Parallel conversion for URL-based datasets + import time + from concurrent.futures import ThreadPoolExecutor, as_completed + from utils.hardware import safe_num_proc + + num_workers = safe_num_proc() + batch_size = 500 + start_time = time.time() + + for batch_start in range(0, total, batch_size): + batch_end = min(batch_start + batch_size, total) + batch_samples = [dataset[i] for i in range(batch_start, batch_end)] + + with ThreadPoolExecutor(max_workers=num_workers) as executor: + futures = {executor.submit(_convert_single_sample, s): i for i, s in enumerate(batch_samples)} + batch_results = [None] * len(batch_samples) + for future in as_completed(futures): + idx = futures[future] + try: + batch_results[idx] = future.result() + except Exception: + failed_count += 1 + + converted_list.extend(r for r in batch_results if r is not None) + + # Progress update every batch + elapsed = time.time() - start_time + done = batch_end + rate = done / elapsed if elapsed > 0 else 0 + remaining_time = (total - done) / rate if rate > 0 else 0 + eta_str = _format_eta(remaining_time) + progress_msg = f"Downloading images: {done:,}/{total:,} ({done*100//total}%) | ~{eta_str} remaining | {failed_count} skipped" + print(f" [{done}/{total}] {rate:.1f} img/s, {failed_count} failed, ETA {eta_str}") + _notify(progress_msg) + else: + # Sequential conversion for local/embedded images (fast, no I/O bottleneck) + pbar = tqdm(dataset, total=total, desc="Converting VLM samples", unit="sample") + for sample in pbar: + try: + converted_list.append(_convert_single_sample(sample)) + except Exception: + failed_count += 1 + pbar.set_postfix(ok=len(converted_list), failed=failed_count, refresh=False) + pbar.close() + + if failed_count > 0: + fail_rate = failed_count / total + print(f"⚠️ Skipped {failed_count}/{total} ({fail_rate:.0%}) samples with broken/unreachable images") + # For datasets that skipped the probe (small URL datasets), check fail rate now + if has_urls and fail_rate >= MAX_FAIL_RATE: + msg = ( + f"⚠️ {fail_rate:.0%} of images failed to download ({failed_count}/{total}). " + "This dataset has too many broken or unreachable image URLs. " + "Consider using a dataset with embedded images instead." + ) + _notify(msg) + raise ValueError(msg) + + if len(converted_list) == 0: + raise ValueError( + f"All {total} samples failed during VLM conversion — no usable images found. " + "This dataset may contain only image URLs that are no longer accessible." + ) + + print(f"✅ Converted {len(converted_list)}/{total} samples") + _notify(f"Converted {len(converted_list):,}/{total:,} images successfully") # Return list, NOT Dataset return converted_list diff --git a/studio/backend/utils/hardware/__init__.py b/studio/backend/utils/hardware/__init__.py index 667466efea..e8256eb9aa 100644 --- a/studio/backend/utils/hardware/__init__.py +++ b/studio/backend/utils/hardware/__init__.py @@ -13,6 +13,8 @@ from .hardware import ( get_gpu_summary, get_package_versions, get_gpu_utilization, + get_physical_gpu_count, + safe_num_proc, ) __all__ = [ @@ -27,4 +29,6 @@ __all__ = [ 'get_gpu_summary', 'get_package_versions', 'get_gpu_utilization', + 'get_physical_gpu_count', + 'safe_num_proc', ] diff --git a/studio/backend/utils/hardware/hardware.py b/studio/backend/utils/hardware/hardware.py index 0d7cc97cfb..fd43e620bb 100644 --- a/studio/backend/utils/hardware/hardware.py +++ b/studio/backend/utils/hardware/hardware.py @@ -385,3 +385,80 @@ def get_gpu_utilization() -> Dict[str, Any]: "power_limit_w": power_limit, "power_utilization_pct": power_pct, } + + +# ========== Multi-GPU Detection & Safe num_proc ========== + +_physical_gpu_count: Optional[int] = None + +def get_physical_gpu_count() -> int: + """ + Return the number of physical NVIDIA GPUs on the machine. + + Uses ``nvidia-smi -L`` which is NOT affected by CUDA_VISIBLE_DEVICES, + so it always reflects the true hardware count. + Result is cached after the first call. + """ + global _physical_gpu_count + if _physical_gpu_count is not None: + return _physical_gpu_count + + try: + import subprocess + result = subprocess.run( + ["nvidia-smi", "-L"], + capture_output=True, text=True, timeout=5, + ) + if result.returncode == 0 and result.stdout.strip(): + _physical_gpu_count = len(result.stdout.strip().splitlines()) + else: + _physical_gpu_count = 1 + except Exception: + _physical_gpu_count = 1 + + return _physical_gpu_count + + +def safe_num_proc(desired: Optional[int] = None) -> int: + """ + Return a safe ``num_proc`` for ``dataset.map()`` calls. + + On Windows, always returns 1 because Python uses ``spawn`` instead of + ``fork`` for multiprocessing — the overhead of re-importing torch, + transformers, unsloth etc. per worker is typically slower than + single-process for normal dataset sizes. + + On multi-GPU machines the NVIDIA driver spawns extra background threads, + making ``os.fork()`` prone to deadlocks when many workers are created. + This helper caps ``num_proc`` to 4 on such machines. + + On single-GPU (or CPU-only) machines the original value is returned + unchanged. + + Args: + desired: The num_proc you *want*. If None, auto-computes from + ``os.cpu_count()``. + + Returns: + A safe integer ≥ 1. + """ + import os + import sys + + # Windows uses 'spawn' for multiprocessing — the overhead of re-importing + # torch/transformers/unsloth per worker is typically slower than single-process. + if sys.platform == "win32": + return 1 + + if desired is None or not isinstance(desired, int): + desired = max(1, os.cpu_count() // 3) + + if get_physical_gpu_count() > 1: + capped = min(4, desired) + print( + f"⚙️ Multi-GPU detected ({get_physical_gpu_count()} GPUs) — " + f"capping num_proc {desired} → {capped} to avoid fork deadlocks" + ) + return capped + + return desired diff --git a/studio/backend/utils/models/__init__.py b/studio/backend/utils/models/__init__.py index 505fd35edd..92e65cf67c 100644 --- a/studio/backend/utils/models/__init__.py +++ b/studio/backend/utils/models/__init__.py @@ -3,11 +3,14 @@ Model and LoRA configuration handling """ from .model_config import ( ModelConfig, + GgufVariantInfo, is_vision_model, scan_trained_loras, + scan_exported_models, load_model_defaults, get_base_model_from_lora, load_model_config, + list_gguf_variants, MODEL_NAME_MAPPING, UI_STATUS_INDICATORS, ) @@ -15,11 +18,14 @@ from .checkpoints import scan_checkpoints __all__ = [ 'ModelConfig', + 'GgufVariantInfo', 'is_vision_model', 'scan_trained_loras', + 'scan_exported_models', 'load_model_defaults', 'get_base_model_from_lora', 'load_model_config', + 'list_gguf_variants', 'MODEL_NAME_MAPPING', 'UI_STATUS_INDICATORS', 'scan_checkpoints', diff --git a/studio/backend/utils/models/model_config.py b/studio/backend/utils/models/model_config.py index fdf89fce39..5404a198aa 100644 --- a/studio/backend/utils/models/model_config.py +++ b/studio/backend/utils/models/model_config.py @@ -422,6 +422,228 @@ def is_vision_model(model_name: str, hf_token: Optional[str] = None) -> bool: pass +def _is_mmproj(filename: str) -> bool: + """Check if a GGUF filename is a vision projection (mmproj) file.""" + return "mmproj" in filename.lower() + + +def detect_mmproj_file(path: str) -> Optional[str]: + """ + Find the mmproj (vision projection) GGUF file in a directory. + + Args: + path: Directory to search — or a .gguf file (uses its parent dir). + + Returns: + Full path to the mmproj .gguf file, or None if not found. + """ + p = Path(path) + search_dir = p.parent if p.is_file() else p + if not search_dir.is_dir(): + return None + + for f in search_dir.glob("*.gguf"): + if _is_mmproj(f.name): + return str(f.resolve()) + return None + + +def detect_gguf_model(path: str) -> Optional[str]: + """ + Check if the given local path is or contains a GGUF model file. + + Handles two cases: + 1. path is a direct .gguf file path + 2. path is a directory containing .gguf files + + Skips mmproj (vision projection) files — those must be passed via + ``--mmproj``, not ``-m``. Use :func:`detect_mmproj_file` instead. + + Returns the full path to the .gguf file if found, None otherwise. + For HuggingFace repo detection, use detect_gguf_model_remote() instead. + """ + p = Path(path) + + # Case 1: direct .gguf file + if p.suffix == ".gguf" and p.is_file(): + if _is_mmproj(p.name): + return None + return str(p.resolve()) + + # Case 2: directory containing .gguf files (skip mmproj) + if p.is_dir(): + gguf_files = sorted( + (f for f in p.glob("*.gguf") if not _is_mmproj(f.name)), + key=lambda f: f.stat().st_size, reverse=True, + ) + if gguf_files: + return str(gguf_files[0].resolve()) + + return None + + +# Preferred GGUF quantization levels, in descending priority. +# Q4_K_M is a good default: small, fast, acceptable quality. +_GGUF_QUANT_PREFERENCE = [ + "Q4_K_M", "Q4_K_S", "Q5_K_M", "Q5_K_S", + "Q6_K", "Q8_0", "Q3_K_M", "Q3_K_L", "Q2_K", + "F16", "BF16", "F32", +] + + +def _pick_best_gguf(filenames: list[str]) -> Optional[str]: + """ + Pick the best GGUF file from a list of filenames. + + Prefers quantization levels in _GGUF_QUANT_PREFERENCE order. + Falls back to the first .gguf file found. + """ + gguf_files = [f for f in filenames if f.endswith(".gguf")] + if not gguf_files: + return None + + # Try preferred quantization levels + for quant in _GGUF_QUANT_PREFERENCE: + for f in gguf_files: + if quant in f: + return f + + # Fallback: first GGUF file + return gguf_files[0] + + +@dataclass +class GgufVariantInfo: + """A single GGUF quantization variant from a HuggingFace repo.""" + filename: str # e.g., "gemma-3-4b-it-Q4_K_M.gguf" + quant: str # e.g., "Q4_K_M" (extracted from filename) + size_bytes: int # file size + + +def _extract_quant_label(filename: str) -> str: + """ + Extract quantization label like Q4_K_M, IQ4_XS, BF16 from a GGUF filename. + + Examples: + "gemma-3-4b-it-Q4_K_M.gguf" → "Q4_K_M" + "model-IQ4_NL.gguf" → "IQ4_NL" + "model-BF16.gguf" → "BF16" + "model-UD-IQ1_S.gguf" → "UD-IQ1_S" + "model-UD-TQ1_0.gguf" → "UD-TQ1_0" + "MXFP4_MOE/model-MXFP4_MOE-0001.gguf"→ "MXFP4_MOE" + """ + import re + # Use only the basename (rfilename may include directory) + basename = filename.rsplit("/", 1)[-1] + # Strip .gguf and any shard suffix (-00001-of-00010) + stem = re.sub(r'-\d{3,}-of-\d{3,}', '', basename.rsplit(".", 1)[0]) + # Match known quantization patterns + match = re.search( + r'(UD-)?' # Optional UD- prefix (Ultra Discrete) + r'(MXFP[0-9]+(?:_[A-Z0-9]+)*' # MXFP variants: MXFP4, MXFP4_MOE + r'|IQ[0-9]+_[A-Z]+(?:_[A-Z0-9]+)?' # IQ variants: IQ4_XS, IQ4_NL, IQ1_S + r'|TQ[0-9]+_[0-9]+' # Ternary quant: TQ1_0, TQ2_0 + r'|Q[0-9]+_K_[A-Z]+' # K-quant: Q4_K_M, Q3_K_S + r'|Q[0-9]+_[0-9]+' # Standard: Q8_0, Q5_1 + r'|Q[0-9]+_K' # Short K-quant: Q6_K + r'|BF16|F16|F32)', # Full precision + stem, re.IGNORECASE, + ) + if match: + prefix = match.group(1) or "" + return f"{prefix}{match.group(2)}" + # Fallback: last segment after hyphen + return stem.split("-")[-1] + + +def list_gguf_variants( + repo_id: str, + hf_token: Optional[str] = None, +) -> tuple[list[GgufVariantInfo], bool]: + """ + List all GGUF quantization variants in a HuggingFace repo. + + Separates main model files from mmproj (vision projection) files. + The presence of mmproj files indicates a vision-capable model. + + Returns: + (variants, has_vision): list of non-mmproj GGUF variants + vision flag. + """ + from huggingface_hub import model_info as hf_model_info + + info = hf_model_info(repo_id, token=hf_token, files_metadata=True) + variants: list[GgufVariantInfo] = [] + has_vision = False + + quant_totals: dict[str, int] = {} # quant -> total bytes + quant_first_file: dict[str, str] = {} # quant -> first filename (for display) + + for sibling in info.siblings: + fname = sibling.rfilename + if not fname.endswith(".gguf"): + continue + size = sibling.size or 0 + + # mmproj files are vision projection models, not main model files + if "mmproj" in fname.lower(): + has_vision = True + continue + + quant = _extract_quant_label(fname) + quant_totals[quant] = quant_totals.get(quant, 0) + size + if quant not in quant_first_file: + quant_first_file[quant] = fname + + for quant, total_size in quant_totals.items(): + variants.append(GgufVariantInfo( + filename=quant_first_file[quant], + quant=quant, + size_bytes=total_size, + )) + + return variants, has_vision + + +def detect_gguf_model_remote( + repo_id: str, + hf_token: Optional[str] = None, +) -> Optional[str]: + """ + Check if a HuggingFace repo contains GGUF files. + + Returns the filename of the best GGUF file in the repo, or None. + """ + try: + from huggingface_hub import model_info as hf_model_info + + info = hf_model_info(repo_id, token=hf_token) + repo_files = [s.rfilename for s in info.siblings] + return _pick_best_gguf(repo_files) + except Exception as e: + logger.debug(f"Could not check GGUF files for '{repo_id}': {e}") + return None + + +def download_gguf_file( + repo_id: str, + filename: str, + hf_token: Optional[str] = None, +) -> str: + """ + Download a specific GGUF file from a HuggingFace repo. + + Returns the local path to the downloaded file. + """ + from huggingface_hub import hf_hub_download + + local_path = hf_hub_download( + repo_id=repo_id, + filename=filename, + token=hf_token, + ) + return local_path + + def scan_trained_loras(outputs_dir: str = "./outputs") -> List[Tuple[str, str]]: """ Scan outputs folder for trained LoRA adapters. @@ -465,6 +687,129 @@ def scan_trained_loras(outputs_dir: str = "./outputs") -> List[Tuple[str, str]]: logger.error(f"Error scanning outputs folder: {e}") return [] +def scan_exported_models(exports_dir: str = "./exports") -> List[Tuple[str, str, str, Optional[str]]]: + """ + Scan exports folder for exported models (merged, LoRA, GGUF). + + Supports two directory layouts: + - Two-level: {run}/{checkpoint}/ (merged & LoRA exports) + - Flat: {name}-finetune-gguf/ (GGUF exports) + + Returns: + List of tuples: [(display_name, model_path, export_type, base_model), ...] + export_type: "lora" | "merged" | "gguf" + """ + results = [] + exports_path = Path(exports_dir) + + if not exports_path.exists(): + return results + + try: + for run_dir in exports_path.iterdir(): + if not run_dir.is_dir(): + continue + + # Check for flat GGUF export (e.g. exports/gemma-3-4b-it-finetune-gguf/) + # Filter out mmproj (vision projection) files — they aren't loadable as main models + gguf_files = [f for f in run_dir.glob("*.gguf") if not _is_mmproj(f.name)] + if gguf_files: + base_model = None + export_meta = run_dir / "export_metadata.json" + try: + if export_meta.exists(): + meta = json.loads(export_meta.read_text()) + base_model = meta.get("base_model") + except Exception: + pass + + display_name = run_dir.name + model_path = str(gguf_files[0]) # path to the .gguf file + results.append((display_name, model_path, "gguf", base_model)) + logger.debug(f"Found GGUF export: {display_name}") + continue + + # Two-level: {run}/{checkpoint}/ + for checkpoint_dir in run_dir.iterdir(): + if not checkpoint_dir.is_dir(): + continue + + adapter_config = checkpoint_dir / "adapter_config.json" + config_file = checkpoint_dir / "config.json" + has_weights = ( + any(checkpoint_dir.glob("*.safetensors")) + or any(checkpoint_dir.glob("*.bin")) + ) + has_gguf = any(checkpoint_dir.glob("*.gguf")) + + base_model = None + export_type = None + + if adapter_config.exists(): + export_type = "lora" + try: + cfg = json.loads(adapter_config.read_text()) + base_model = cfg.get("base_model_name_or_path") + except Exception: + pass + elif config_file.exists() and has_weights: + export_type = "merged" + export_meta = checkpoint_dir / "export_metadata.json" + try: + if export_meta.exists(): + meta = json.loads(export_meta.read_text()) + base_model = meta.get("base_model") + except Exception: + pass + elif has_gguf: + export_type = "gguf" + gguf_list = list(checkpoint_dir.glob("*.gguf")) + # Check checkpoint_dir first, then fall back to parent run_dir + # (export.py writes metadata to the top-level export directory) + for meta_dir in (checkpoint_dir, run_dir): + export_meta = meta_dir / "export_metadata.json" + try: + if export_meta.exists(): + meta = json.loads(export_meta.read_text()) + base_model = meta.get("base_model") + if base_model: + break + except Exception: + pass + + display_name = f"{run_dir.name} / {checkpoint_dir.name}" + model_path = str(gguf_list[0]) if gguf_list else str(checkpoint_dir) + results.append((display_name, model_path, export_type, base_model)) + logger.debug(f"Found GGUF export: {display_name}") + continue + else: + continue + + # Fallback: read base model from the original training run's + # adapter_config.json in ./outputs/{run_name}/ + if not base_model: + outputs_adapter_cfg = Path("./outputs") / run_dir.name / "adapter_config.json" + try: + if outputs_adapter_cfg.exists(): + cfg = json.loads(outputs_adapter_cfg.read_text()) + base_model = cfg.get("base_model_name_or_path") + except Exception: + pass + + display_name = f"{run_dir.name} / {checkpoint_dir.name}" + model_path = str(checkpoint_dir) + results.append((display_name, model_path, export_type, base_model)) + logger.debug(f"Found exported model: {display_name} ({export_type})") + + results.sort(key=lambda x: Path(x[1]).stat().st_mtime, reverse=True) + logger.info(f"Found {len(results)} exported models in {exports_dir}") + return results + + except Exception as e: + logger.error(f"Error scanning exports folder: {e}") + return [] + + def get_base_model_from_lora(lora_path: str) -> Optional[str]: """ Read the base model name from a LoRA adapter's config. @@ -595,6 +940,11 @@ class ModelConfig: is_cached: bool # Is this already in HF cache? is_vision: bool # Is this a vision model? is_lora: bool # Is this a lora adapter? + is_gguf: bool = False # Is this a GGUF model? + gguf_file: Optional[str] = None # Full path to the .gguf file (local mode) + gguf_mmproj_file: Optional[str] = None # Full path to the mmproj .gguf file (vision projection) + gguf_hf_repo: Optional[str] = None # HF repo ID for -hf mode (e.g. "unsloth/gemma-3-4b-it-GGUF") + gguf_variant: Optional[str] = None # Quantization variant (e.g. "Q4_K_M") base_model: Optional[str] = None # Base model (for LoRAs) @classmethod @@ -650,37 +1000,130 @@ class ModelConfig: cls, model_id: str, hf_token: Optional[str] = None, - is_lora: bool = False + is_lora: bool = False, + gguf_variant: Optional[str] = None, ) -> Optional['ModelConfig']: """ Create ModelConfig from a clean model identifier. - + For FastAPI routes where the frontend sends sanitized model paths. No Gradio dropdown parsing - expects clean identifiers like: - "unsloth/Meta-Llama-3.1-8B-Instruct-bnb-4bit" - "./outputs/my_lora_adapter" - "/absolute/path/to/model" - + Args: model_id: Clean model identifier (HF repo name or local path) hf_token: Optional HF token for vision detection on gated models is_lora: Whether this is a LoRA adapter - + gguf_variant: Optional GGUF quantization variant (e.g. "Q4_K_M"). + For remote GGUF repos, specifies which quant to load via -hf. + If None, auto-selects using _pick_best_gguf(). + Returns: ModelConfig or None if configuration cannot be created """ if not model_id or not model_id.strip(): return None - + identifier = model_id.strip() is_local = is_local_path(identifier) path = normalize_path(identifier) if is_local else identifier - + # Add unsloth/ prefix for shorthand HF models if not is_local and "/" not in identifier: identifier = f"unsloth/{identifier}" path = identifier - + + # Auto-detect GGUF models (check before LoRA/vision detection) + if is_local: + gguf_file = detect_gguf_model(path) + if gguf_file: + display_name = Path(gguf_file).stem + logger.info(f"Detected local GGUF model: {gguf_file}") + + # Detect vision: check if base model is vision, then look for mmproj + mmproj_file = None + gguf_is_vision = False + gguf_dir = Path(gguf_file).parent + + # Determine if this is a vision model from export metadata + base_is_vision = False + meta_path = gguf_dir / "export_metadata.json" + if meta_path.exists(): + try: + meta = json.loads(meta_path.read_text()) + base = meta.get("base_model") + if base and is_vision_model(base, hf_token=hf_token): + base_is_vision = True + logger.info(f"GGUF base model '{base}' is a vision model") + except Exception as e: + logger.debug(f"Could not read export metadata: {e}") + + # If vision (or mmproj happens to exist), find the mmproj file + mmproj_file = detect_mmproj_file(gguf_file) + if mmproj_file: + gguf_is_vision = True + logger.info(f"Detected mmproj for vision: {mmproj_file}") + elif base_is_vision: + logger.warning(f"Base model is vision but no mmproj file found in {gguf_dir}") + + return cls( + identifier=identifier, + display_name=display_name, + path=path, + is_local=True, + is_cached=True, + is_vision=gguf_is_vision, + is_lora=False, + is_gguf=True, + gguf_file=gguf_file, + gguf_mmproj_file=mmproj_file, + ) + else: + # Check if the HF repo contains GGUF files + gguf_filename = detect_gguf_model_remote(identifier, hf_token=hf_token) + if gguf_filename: + # Preflight: verify llama-server binary exists BEFORE user waits + # for a multi-GB download that llama-server handles natively + from core.inference.llama_cpp import LlamaCppBackend + if not LlamaCppBackend._find_llama_server_binary(): + raise RuntimeError( + "llama-server binary not found — cannot load GGUF models. " + "Run setup.sh to build it, or set LLAMA_SERVER_PATH." + ) + + # Use list_gguf_variants() to detect vision & resolve variant + variants, has_vision = list_gguf_variants(identifier, hf_token=hf_token) + variant = gguf_variant + if not variant: + # Auto-select best quantization + variant_filenames = [v.filename for v in variants] + best = _pick_best_gguf(variant_filenames) + if best: + variant = _extract_quant_label(best) + else: + variant = "Q4_K_M" # Fallback — llama-server's own default + + display_name = f"{identifier.split('/')[-1]} ({variant})" + logger.info( + f"Detected remote GGUF repo '{identifier}', " + f"variant={variant}, vision={has_vision}" + ) + return cls( + identifier=identifier, + display_name=display_name, + path=identifier, + is_local=False, + is_cached=False, + is_vision=has_vision, + is_lora=False, + is_gguf=True, + gguf_file=None, + gguf_hf_repo=identifier, + gguf_variant=variant, + ) + # Auto-detect LoRA for local paths (check adapter_config.json on disk) if not is_lora and is_local: detected_base = get_base_model_from_lora(path) diff --git a/studio/frontend/.gitignore b/studio/frontend/.gitignore index d26a9c5159..3483430dcf 100644 --- a/studio/frontend/.gitignore +++ b/studio/frontend/.gitignore @@ -1,28 +1,31 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules dist dist-ssr test/ *.local .env .env.* - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -._* -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? +.omx/ + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +._* +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? +/src/features/recipe-studio/AGENTS.md +/docs diff --git a/studio/frontend/AGENTS.md b/studio/frontend/AGENTS.md new file mode 100644 index 0000000000..b1de874979 --- /dev/null +++ b/studio/frontend/AGENTS.md @@ -0,0 +1,37 @@ +# Repository Guidelines + +## Project Structure & Module Organization +- `src/` is app code; entry is `src/main.tsx`, global styles in `src/index.css`. +- `src/app/` holds app shell and routing; `src/features/` is feature slices w/ public `index.ts` exports. +- Shared UI lives in `src/components/` (shadcn in `src/components/ui/`). +- Shared logic in `src/hooks/`, `src/stores/`, `src/utils/`, `src/lib/`, and types in `src/types/`. +- Static assets: `src/assets/` and `public/`. +- `test/` is a Python harness for payload validation and preview; not a JS test suite. + +## Build, Test, and Development Commands +- `bun run dev`: start Vite dev server. +- `bun run build`: typecheck + build to `dist/`. +- `bun run preview`: serve the production build locally. +- `bun run lint`: ESLint checks for TS/React. +- `bun run typecheck`: `tsc` no-emit verification. +- `bun run biome:check` / `bun run biome:fix`: format + lint w/ Biome. +- Optional harness: `python test/scripts/validate_payload.py test/data/ui_payload.json`. + +## Coding Style & Naming Conventions +- TypeScript + React, 2-space indent (Biome). +- Prefer explicit, compact code; avoid heavy abstraction. +- Use path alias `@/` for app imports. +- Feature boundaries enforced: import from `@/features/` only, not deep paths. +- Components in `PascalCase`, hooks in `useCamelCase`, files in `kebab-case` or `camelCase` per local convention. + +## Testing Guidelines +- No frontend test runner configured yet; add one if needed. +- `test/` is for API payload validation and preview flows; add samples as `test/data/ui_payload_*.json`. + +## Commit & Pull Request Guidelines +- Commit history shows short, imperative messages; optional prefix like `refactor:`; keep it terse. +- PRs should include: clear summary, linked issue (if any), and UI screenshots/gifs for visual changes. +- Call out new deps, config, or required env changes in the PR body. + +## Agent Notes +- Keep changes minimal, focused, and easy to review. diff --git a/studio/frontend/CLAUDE.md b/studio/frontend/CLAUDE.md deleted file mode 100644 index 83460c034a..0000000000 --- a/studio/frontend/CLAUDE.md +++ /dev/null @@ -1,66 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Project Overview - -LLM fine-tuning platform UI (Unsloth-branded). React/TypeScript frontend with a skeletal Python backend. The frontend is the active development focus. - -## Commands - -All commands run from `frontend/`: - -```bash -bun install # install dependencies -bun run dev # start Vite dev server -bun run build # typecheck + production build -bun run typecheck # TypeScript type checking only -bun run lint # ESLint -bun run biome:check # Biome linter + formatter check -bun run biome:fix # Biome auto-fix -``` - -Package manager is **Bun** (not npm/yarn). - -## Architecture - -### Frontend (`frontend/src/`) - - -**Feature-based module architecture** with enforced boundaries: - -- `features/` — self-contained feature modules (chat, onboarding, studio) -- `components/ui/` — shadcn/ui primitives (linting/formatting disabled for these) -- `components/assistant-ui/` — AI chat thread components -- `components/layout/` — layout shells (dashboard) -- `stores/` — Zustand stores (training wizard state) -- `config/` — constants (model lists, hyperparameters, env) -- `types/` — shared TypeScript types -- `app/` — router and root layout (TanStack React Router) - -### Import Rules (ESLint-enforced) - -Cross-feature imports are **prohibited**. Import from feature barrel (`@/features/[name]`), never from internal paths (`@/features/chat/some-component`). - -### Key Technology Choices - -| Concern | Choice | -|---------|--------| -| Routing | TanStack React Router | -| State | Zustand | -| Styling | Tailwind CSS + shadcn/ui (radix-maia style, HugeIcons) | -| Animation | Framer Motion | -| Chat UI | @assistant-ui/react with streaming | -| Local DB | Dexie (IndexedDB) for chat threads/messages | -| Charts | Recharts | - -### Backend (`backend/`) - -Placeholder Python structure. Frontend expects an inference server at the URL in `frontend/.env` (`VITE_INFERENCE_URL`) serving POST `/api/chat/generate` with streaming responses. - -## Code Style - -- Biome handles formatting (2-space indent) and import organization -- `src/components/ui/**` is excluded from Biome linting/formatting (generated shadcn code) -- Path alias: `@` maps to `frontend/src/` -- Prefer KISS and DRY diff --git a/studio/frontend/biome.json b/studio/frontend/biome.json index 926b32279f..66dcd322a0 100644 --- a/studio/frontend/biome.json +++ b/studio/frontend/biome.json @@ -4,6 +4,8 @@ "ignore": [ "dist", "node_modules", + "test", + "test/**", "**/._*", "._*", "**/.DS_Store", diff --git a/studio/frontend/bun.lock b/studio/frontend/bun.lock index 27bc43b946..f200156928 100644 --- a/studio/frontend/bun.lock +++ b/studio/frontend/bun.lock @@ -3,80 +3,84 @@ "configVersion": 1, "workspaces": { "": { - "name": "vite-app", + "name": "unsloth-theme", "dependencies": { - "@assistant-ui/react": "^0.12.3", - "@assistant-ui/react-markdown": "^0.12.1", - "@assistant-ui/react-streamdown": "^0.1.0", - "@base-ui/react": "^1.1.0", + "@assistant-ui/react": "^0.12.10", + "@assistant-ui/react-markdown": "^0.12.3", + "@assistant-ui/react-streamdown": "^0.1.2", + "@base-ui/react": "^1.2.0", "@dagrejs/dagre": "^2.0.4", "@dagrejs/graphlib": "^3.0.4", "@fontsource-variable/figtree": "^5.2.10", "@fontsource-variable/inter": "^5.2.8", "@fontsource-variable/space-grotesk": "^5.2.10", "@hugeicons/core-free-icons": "^3.1.1", - "@hugeicons/react": "^1.1.4", - "@huggingface/hub": "^2.8.0", + "@hugeicons/react": "^1.1.5", + "@huggingface/hub": "^2.9.0", + "@langchain/core": "^1.1.27", + "@langchain/textsplitters": "^1.0.1", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slot": "^1.2.4", - "@streamdown/cjk": "^1.0.1", - "@streamdown/code": "^1.0.1", - "@streamdown/math": "^1.0.1", - "@streamdown/mermaid": "^1.0.1", - "@tailwindcss/vite": "^4.1.17", - "@tanstack/react-router": "^1.156.0", + "@streamdown/cjk": "^1.0.2", + "@streamdown/code": "^1.0.2", + "@streamdown/math": "^1.0.2", + "@streamdown/mermaid": "^1.0.2", + "@tailwindcss/vite": "^4.1.18", + "@tanstack/react-router": "^1.159.10", "@tanstack/react-table": "^8.21.3", "@toolwind/corner-shape": "^0.0.8-3", "@types/canvas-confetti": "^1.9.0", "@xyflow/react": "^12.10.0", - "assistant-stream": "^0.3.0", + "assistant-stream": "^0.3.2", "canvas-confetti": "^1.9.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", "date-fns": "^4.1.0", - "dexie": "^4.2.1", - "framer-motion": "^11.15.0", - "katex": "^0.16.22", - "lucide-react": "^0.563.0", + "dexie": "^4.3.0", + "framer-motion": "^11.18.2", + "js-yaml": "^4.1.1", + "katex": "^0.16.28", + "lucide-react": "^0.575.0", "mammoth": "^1.11.0", - "motion": "^12.29.2", + "motion": "^12.34.0", "next": "^16.1.6", "next-themes": "^0.4.6", "radix-ui": "^1.4.3", - "react": "^19.2.0", - "react-day-picker": "^9.13.0", - "react-dom": "^19.2.0", - "react-resizable-panels": "^4.4.1", - "recharts": "2.15.4", + "react": "^19.2.4", + "react-day-picker": "^9.13.2", + "react-dom": "^19.2.4", + "react-resizable-panels": "^4.6.4", + "recharts": "3.7.0", "remark-gfm": "^4.0.1", - "shadcn": "^3.7.0", + "shadcn": "^3.8.4", "sonner": "^2.0.7", - "streamdown": "^2.1.0", + "streamdown": "^2.2.0", "tailwind-merge": "^3.4.0", - "tailwindcss": "^4.1.17", + "tailwindcss": "^4.1.18", "tw-animate-css": "^1.4.0", - "tw-shimmer": "^0.4.4", + "tw-shimmer": "^0.4.6", "unpdf": "^1.4.0", - "zustand": "^5.0.10", + "zustand": "^5.0.11", }, "devDependencies": { "@biomejs/biome": "^1.9.4", "@eslint/js": "^9.39.1", + "@types/js-yaml": "^4.0.9", "@types/node": "^24.10.1", "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.1", "eslint": "^9.39.1", "eslint-plugin-react-hooks": "^7.0.1", - "eslint-plugin-react-refresh": "^0.4.24", + "eslint-plugin-react-refresh": "^0.4.26", "globals": "^16.5.0", "typescript": "~5.9.3", - "typescript-eslint": "^8.46.4", - "vite": "^7.2.4", + "typescript-eslint": "^8.55.0", + "vite": "^7.3.1", }, }, }, @@ -85,23 +89,25 @@ "@antfu/ni": ["@antfu/ni@25.0.0", "", { "dependencies": { "ansis": "^4.0.0", "fzf": "^0.5.2", "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" }, "bin": { "na": "bin/na.mjs", "ni": "bin/ni.mjs", "nr": "bin/nr.mjs", "nci": "bin/nci.mjs", "nlx": "bin/nlx.mjs", "nun": "bin/nun.mjs", "nup": "bin/nup.mjs" } }, "sha512-9q/yCljni37pkMr4sPrI3G4jqdIk074+iukc5aFJl7kmDCCsiJrbZ6zKxnES1Gwg+i9RcDZwvktl23puGslmvA=="], - "@assistant-ui/react": ["@assistant-ui/react@0.12.3", "", { "dependencies": { "@assistant-ui/store": "^0.1.2", "@assistant-ui/tap": "^0.4.2", "@radix-ui/primitive": "^1.1.3", "@radix-ui/react-compose-refs": "^1.1.2", "@radix-ui/react-context": "^1.1.3", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-primitive": "^2.1.4", "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-use-callback-ref": "^1.1.1", "@radix-ui/react-use-escape-keydown": "^1.1.1", "assistant-cloud": "^0.1.15", "assistant-stream": "^0.3.0", "nanoid": "^5.1.6", "react-textarea-autosize": "^8.5.9", "zod": "^4.3.6", "zustand": "^5.0.10" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^18 || ^19", "react-dom": "^18 || ^19" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-igqniJ+H7viLGjFD1yXBoBkkjbopggUVF7upjdaZvHCX+MdnWSgyuXNxZpvmVceo2pvyJ7iiCdSieHymWy1Rkw=="], + "@assistant-ui/core": ["@assistant-ui/core@0.1.0", "", { "dependencies": { "@assistant-ui/tap": "^0.5.0", "assistant-stream": "^0.3.3", "nanoid": "^5.1.6" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-8fIhNjX5Qvdvl5Zu3u0dypEm6/zFSJMKDAyl5icP6zW/2NGy+/CtFlNSdtvJ+tloKevJR7kXmyyTyTuhZRg25g=="], - "@assistant-ui/react-markdown": ["@assistant-ui/react-markdown@0.12.1", "", { "dependencies": { "@radix-ui/react-primitive": "^2.1.4", "@radix-ui/react-use-callback-ref": "^1.1.1", "classnames": "^2.5.1", "react-markdown": "^10.1.0" }, "peerDependencies": { "@assistant-ui/react": "^0.12.3", "@types/react": "*", "react": "^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-pwEa/Lj0NEaFhkkj5stTitOSQDE599p9P0SNv2I3GbN+7ETFC4esoELJbEXtblSMY9kyduzoB1+yfIdowEgR8w=="], + "@assistant-ui/react": ["@assistant-ui/react@0.12.11", "", { "dependencies": { "@assistant-ui/core": "^0.1.0", "@assistant-ui/store": "^0.2.0", "@assistant-ui/tap": "^0.5.0", "@radix-ui/primitive": "^1.1.3", "@radix-ui/react-compose-refs": "^1.1.2", "@radix-ui/react-context": "^1.1.3", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-primitive": "^2.1.4", "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-use-callback-ref": "^1.1.1", "@radix-ui/react-use-escape-keydown": "^1.1.1", "assistant-cloud": "^0.1.18", "assistant-stream": "^0.3.3", "nanoid": "^5.1.6", "react-textarea-autosize": "^8.5.9", "zod": "^4.3.6", "zustand": "^5.0.11" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^18 || ^19", "react-dom": "^18 || ^19" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-OATx2u8JqYZCUSuR4JuhDFs64IlF+cvyq6DpIv4ZpkZ8HHMkYhS1hame7oxHeJSf9taWO+RcKrVgmG8txNO0Vg=="], - "@assistant-ui/react-streamdown": ["@assistant-ui/react-streamdown@0.1.0", "", { "dependencies": { "rehype-harden": "^1.1.7", "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", "streamdown": "^2.0.0" }, "peerDependencies": { "@assistant-ui/react": "^0.12.3", "@streamdown/cjk": "^1.0.0", "@streamdown/code": "^1.0.0", "@streamdown/math": "^1.0.0", "@streamdown/mermaid": "^1.0.0", "@types/react": "*", "react": "^18 || ^19" }, "optionalPeers": ["@streamdown/cjk", "@streamdown/code", "@streamdown/math", "@streamdown/mermaid", "@types/react"] }, "sha512-gdxLi6sgXa545ohBi4rb3EAj5gw/Ix0bYf+yT1Q0Q5bXHSRuDK5UiSzxzoPtZC8mIEPJEH7FiP8aR4T4GTW5qQ=="], + "@assistant-ui/react-markdown": ["@assistant-ui/react-markdown@0.12.4", "", { "dependencies": { "@radix-ui/react-primitive": "^2.1.4", "@radix-ui/react-use-callback-ref": "^1.1.1", "classnames": "^2.5.1", "react-markdown": "^10.1.0" }, "peerDependencies": { "@assistant-ui/react": "^0.12.11", "@types/react": "*", "react": "^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-6TD9guiuLJxJoOwSjNHUYAVma2ctDCG9uypUqKHE0OUhDwTDD3NsMvTnQ0n0Lh8nnCEwVglOwKKlSEYpV7SnWA=="], - "@assistant-ui/store": ["@assistant-ui/store@0.1.2", "", { "dependencies": { "@assistant-ui/tap": "^0.4.2", "use-effect-event": "^2.0.3" }, "peerDependencies": { "@types/react": "*", "react": "^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-LOGjpK7Q07y14stu18pSk/0E5eZrYlxUNGKPkhZAZKvTPnw5eiy2yghbPGlxEGLMT7ZigJkkXoqcabjyw53VJA=="], + "@assistant-ui/react-streamdown": ["@assistant-ui/react-streamdown@0.1.3", "", { "dependencies": { "rehype-harden": "^1.1.7", "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", "streamdown": "^2.1.0" }, "peerDependencies": { "@assistant-ui/react": "^0.12.11", "@streamdown/cjk": "^1.0.0", "@streamdown/code": "^1.0.0", "@streamdown/math": "^1.0.0", "@streamdown/mermaid": "^1.0.0", "@types/react": "*", "react": "^18 || ^19" }, "optionalPeers": ["@streamdown/cjk", "@streamdown/code", "@streamdown/math", "@streamdown/mermaid", "@types/react"] }, "sha512-n1UCjXQ3svmDtJBMJj/vXqz/BqAQBuy7myrXeymz2tD9l+ENQgqu2JY5ir3J19juJTe5lsi/P3+tOJ2C1jc/nw=="], - "@assistant-ui/tap": ["@assistant-ui/tap@0.4.2", "", { "peerDependencies": { "@types/react": "*", "react": "^18 || ^19" }, "optionalPeers": ["@types/react", "react"] }, "sha512-iryNDkdsDkj7oYYt7L1dlJaeRuSjHzN7PE4Td1BiiVx1kfcBS6iXOaP/4G9NtVkxMGocdNJLjVv83Mwq7IVg/g=="], + "@assistant-ui/store": ["@assistant-ui/store@0.2.0", "", { "dependencies": { "@assistant-ui/core": "^0.1.0", "@assistant-ui/tap": "^0.5.0", "use-effect-event": "^2.0.3" }, "peerDependencies": { "@types/react": "*", "react": "^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-+8Oq7knxhYh1UAGOolvJRlFB3SkLcxnz971oA/iVAxgN/jpp1MH4h6xQwiLoYrwOtcQDSJOSuivoxrDKZdhFrA=="], - "@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="], + "@assistant-ui/tap": ["@assistant-ui/tap@0.5.0", "", { "peerDependencies": { "@types/react": "*", "react": "^18 || ^19" }, "optionalPeers": ["@types/react", "react"] }, "sha512-UUWXTLtD5/iIs1hSDDF0Ieew2kna0G6RzIVqxlfy5Ei0qPGxJr90ICkPwjaMzELxT/JlL0u2eo+78wFUUBCMcA=="], - "@babel/compat-data": ["@babel/compat-data@7.28.6", "", {}, "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg=="], + "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], - "@babel/core": ["@babel/core@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw=="], + "@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="], - "@babel/generator": ["@babel/generator@7.28.6", "", { "dependencies": { "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw=="], + "@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="], + + "@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], "@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], @@ -133,7 +139,7 @@ "@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="], - "@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": "./bin/babel-parser.js" }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="], + "@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="], "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w=="], @@ -153,13 +159,13 @@ "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], - "@babel/traverse": ["@babel/traverse@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/types": "^7.28.6", "debug": "^4.3.1" } }, "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg=="], + "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], - "@babel/types": ["@babel/types@7.28.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg=="], + "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], - "@base-ui/react": ["@base-ui/react@1.1.0", "", { "dependencies": { "@babel/runtime": "^7.28.4", "@base-ui/utils": "0.2.4", "@floating-ui/react-dom": "^2.1.6", "@floating-ui/utils": "^0.2.10", "reselect": "^5.1.1", "tabbable": "^6.4.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-ikcJRNj1mOiF2HZ5jQHrXoVoHcNHdBU5ejJljcBl+VTLoYXR6FidjTN86GjO6hyshi6TZFuNvv0dEOgaOFv6Lw=="], + "@base-ui/react": ["@base-ui/react@1.2.0", "", { "dependencies": { "@babel/runtime": "^7.28.6", "@base-ui/utils": "0.2.5", "@floating-ui/react-dom": "^2.1.6", "@floating-ui/utils": "^0.2.10", "tabbable": "^6.4.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-O6aEQHcm+QyGTFY28xuwRD3SEJGZOBDpyjN2WvpfWYFVhg+3zfXPysAILqtM0C1kWC82MccOE/v1j+GHXE4qIw=="], - "@base-ui/utils": ["@base-ui/utils@0.2.4", "", { "dependencies": { "@babel/runtime": "^7.28.4", "@floating-ui/utils": "^0.2.10", "reselect": "^5.1.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-smZwpMhjO29v+jrZusBSc5T+IJ3vBb9cjIiBjtKcvWmRj9Z4DWGVR3efr1eHR56/bqY5a4qyY9ElkOY5ljo3ng=="], + "@base-ui/utils": ["@base-ui/utils@0.2.5", "", { "dependencies": { "@babel/runtime": "^7.28.6", "@floating-ui/utils": "^0.2.10", "reselect": "^5.1.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-oYC7w0gp76RI5MxprlGLV0wze0SErZaRl3AAkeP3OnNB/UBMb6RqNf6ZSIlxOc9Qp68Ab3C2VOcJQyRs7Xc7Vw=="], "@biomejs/biome": ["@biomejs/biome@1.9.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "1.9.4", "@biomejs/cli-darwin-x64": "1.9.4", "@biomejs/cli-linux-arm64": "1.9.4", "@biomejs/cli-linux-arm64-musl": "1.9.4", "@biomejs/cli-linux-x64": "1.9.4", "@biomejs/cli-linux-x64-musl": "1.9.4", "@biomejs/cli-win32-arm64": "1.9.4", "@biomejs/cli-win32-x64": "1.9.4" }, "bin": { "biome": "bin/biome" } }, "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog=="], @@ -181,15 +187,17 @@ "@braintree/sanitize-url": ["@braintree/sanitize-url@7.1.2", "", {}, "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA=="], - "@chevrotain/cst-dts-gen": ["@chevrotain/cst-dts-gen@11.0.3", "", { "dependencies": { "@chevrotain/gast": "11.0.3", "@chevrotain/types": "11.0.3", "lodash-es": "4.17.21" } }, "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ=="], + "@cfworker/json-schema": ["@cfworker/json-schema@4.1.1", "", {}, "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og=="], - "@chevrotain/gast": ["@chevrotain/gast@11.0.3", "", { "dependencies": { "@chevrotain/types": "11.0.3", "lodash-es": "4.17.21" } }, "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q=="], + "@chevrotain/cst-dts-gen": ["@chevrotain/cst-dts-gen@11.1.1", "", { "dependencies": { "@chevrotain/gast": "11.1.1", "@chevrotain/types": "11.1.1", "lodash-es": "4.17.23" } }, "sha512-fRHyv6/f542qQqiRGalrfJl/evD39mAvbJLCekPazhiextEatq1Jx1K/i9gSd5NNO0ds03ek0Cbo/4uVKmOBcw=="], - "@chevrotain/regexp-to-ast": ["@chevrotain/regexp-to-ast@11.0.3", "", {}, "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA=="], + "@chevrotain/gast": ["@chevrotain/gast@11.1.1", "", { "dependencies": { "@chevrotain/types": "11.1.1", "lodash-es": "4.17.23" } }, "sha512-Ko/5vPEYy1vn5CbCjjvnSO4U7GgxyGm+dfUZZJIWTlQFkXkyym0jFYrWEU10hyCjrA7rQtiHtBr0EaZqvHFZvg=="], - "@chevrotain/types": ["@chevrotain/types@11.0.3", "", {}, "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ=="], + "@chevrotain/regexp-to-ast": ["@chevrotain/regexp-to-ast@11.1.1", "", {}, "sha512-ctRw1OKSXkOrR8VTvOxrQ5USEc4sNrfwXHa1NuTcR7wre4YbjPcKw+82C2uylg/TEwFRgwLmbhlln4qkmDyteg=="], - "@chevrotain/utils": ["@chevrotain/utils@11.0.3", "", {}, "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ=="], + "@chevrotain/types": ["@chevrotain/types@11.1.1", "", {}, "sha512-wb2ToxG8LkgPYnKe9FH8oGn3TMCBdnwiuNC5l5y+CtlaVRbCytU0kbVsk6CGrqTL4ZN4ksJa0TXOYbxpbthtqw=="], + + "@chevrotain/utils": ["@chevrotain/utils@11.1.1", "", {}, "sha512-71eTYMzYXYSFPrbg/ZwftSaSDld7UYlS8OQa3lNnn9jzNtpFbaReRRyghzqS7rI3CDaorqpPJJcXGHK+FE1TVQ=="], "@dagrejs/dagre": ["@dagrejs/dagre@2.0.4", "", { "dependencies": { "@dagrejs/graphlib": "3.0.4" } }, "sha512-J6vCWTNpicHF4zFlZG1cS5DkGzMr9941gddYkakjrg3ZNev4bbqEgLHFTWiFrcJm7UCRu7olO3K6IRDd9gSGhA=="], @@ -203,57 +211,57 @@ "@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], - "@esbuild/android-arm": ["@esbuild/android-arm@0.27.2", "", { "os": "android", "cpu": "arm" }, "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA=="], + "@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="], - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.2", "", { "os": "android", "cpu": "arm64" }, "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA=="], + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="], - "@esbuild/android-x64": ["@esbuild/android-x64@0.27.2", "", { "os": "android", "cpu": "x64" }, "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A=="], + "@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="], - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg=="], + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="], - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA=="], + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="], - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g=="], + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="], - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA=="], + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="], - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.2", "", { "os": "linux", "cpu": "arm" }, "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw=="], + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="], - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw=="], + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="], - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w=="], + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="], - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg=="], + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="], - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw=="], + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="], - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ=="], + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="], - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA=="], + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="], - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w=="], + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="], - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA=="], + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="], - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw=="], + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="], - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.2", "", { "os": "none", "cpu": "x64" }, "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA=="], + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="], - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA=="], + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="], - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg=="], + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="], - "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag=="], + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="], - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg=="], + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="], - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg=="], + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="], - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ=="], + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="], - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="], + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="], "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], @@ -265,19 +273,19 @@ "@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="], - "@eslint/eslintrc": ["@eslint/eslintrc@3.3.3", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ=="], + "@eslint/eslintrc": ["@eslint/eslintrc@3.3.4", "", { "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.3", "strip-json-comments": "^3.1.1" } }, "sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ=="], - "@eslint/js": ["@eslint/js@9.39.2", "", {}, "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA=="], + "@eslint/js": ["@eslint/js@9.39.3", "", {}, "sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw=="], "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="], "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], - "@floating-ui/core": ["@floating-ui/core@1.7.3", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w=="], + "@floating-ui/core": ["@floating-ui/core@1.7.4", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg=="], - "@floating-ui/dom": ["@floating-ui/dom@1.7.4", "", { "dependencies": { "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA=="], + "@floating-ui/dom": ["@floating-ui/dom@1.7.5", "", { "dependencies": { "@floating-ui/core": "^1.7.4", "@floating-ui/utils": "^0.2.10" } }, "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg=="], - "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.6", "", { "dependencies": { "@floating-ui/dom": "^1.7.4" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw=="], + "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.7", "", { "dependencies": { "@floating-ui/dom": "^1.7.5" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg=="], "@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="], @@ -291,11 +299,11 @@ "@hugeicons/core-free-icons": ["@hugeicons/core-free-icons@3.1.1", "", {}, "sha512-UpS2lUQFi5sKyJSWwM6rO+BnPLvVz1gsyCpPHeZyVuZqi89YH8ksliza4cwaODqKOZyeXmG8juo1ty4QtQofkg=="], - "@hugeicons/react": ["@hugeicons/react@1.1.4", "", { "peerDependencies": { "react": ">=16.0.0" } }, "sha512-gsc3eZyd2fGqRUThW9+lfjxxsOkz6KNVmRXRgJjP32GL0OnnLJnl3hytKt47CBbiQj2xE2kCw+rnP3UQCThcKw=="], + "@hugeicons/react": ["@hugeicons/react@1.1.5", "", { "peerDependencies": { "react": ">=16.0.0" } }, "sha512-JX/iDz3oO7hWdVqbjwFwRrAjHk8h2vI+mBkNzp4JcXG3t4idoupfjon73nLOA7cr27m0M8hrRC1Q2h6nEBGKVA=="], - "@huggingface/hub": ["@huggingface/hub@2.8.0", "", { "dependencies": { "@huggingface/tasks": "^0.19.80" }, "optionalDependencies": { "cli-progress": "^3.12.0" }, "bin": { "hfjs": "dist/cli.js" } }, "sha512-eh7lXCrZeNor2YE+2jn2F75/GEzq+TAh81jLTskiqBNcKuBes0I7TIGP1qiDC+rupP765C5vx3XMNxXQVN3N9w=="], + "@huggingface/hub": ["@huggingface/hub@2.10.3", "", { "dependencies": { "@huggingface/tasks": "^0.19.85" }, "optionalDependencies": { "cli-progress": "^3.12.0" }, "bin": { "hfjs": "dist/cli.js" } }, "sha512-qSk4FcVFdTGx0lNpFyy7p2KwgAPCsjM2+tupG/MGToEvUGVLsy+dCmela1BcU/VvJNweCtnH5HwdNr7IQa4Zzw=="], - "@huggingface/tasks": ["@huggingface/tasks@0.19.82", "", {}, "sha512-i8TzJb6Zk7KnYRL8unnYRuh/tW7ku3hJtQw972q/ZXvjkr/YIwWAAVxEfXuaj0vtCUbUljamJji6oWVG4+0PLQ=="], + "@huggingface/tasks": ["@huggingface/tasks@0.19.86", "", {}, "sha512-eab/6J9m+0Z8xw3X2EPPioMLIjFNYjox9nONTmzzgWj0vq6+iMWsMt4tlwrZKLlxxJbFp+acn20VXZi3ejLlng=="], "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], @@ -369,10 +377,6 @@ "@inquirer/type": ["@inquirer/type@3.0.10", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA=="], - "@isaacs/balanced-match": ["@isaacs/balanced-match@4.0.1", "", {}, "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="], - - "@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.0", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA=="], - "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], @@ -383,11 +387,15 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - "@mermaid-js/parser": ["@mermaid-js/parser@0.6.3", "", { "dependencies": { "langium": "3.3.1" } }, "sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA=="], + "@langchain/core": ["@langchain/core@1.1.28", "", { "dependencies": { "@cfworker/json-schema": "^4.0.2", "ansi-styles": "^5.0.0", "camelcase": "6", "decamelize": "1.2.0", "js-tiktoken": "^1.0.12", "langsmith": ">=0.5.0 <1.0.0", "mustache": "^4.2.0", "p-queue": "^6.6.2", "uuid": "^10.0.0", "zod": "^3.25.76 || ^4" } }, "sha512-6FAGdezEp8zHY92LtnsAiv54KaG41nBdsuukk+R+1484edV20cVOyIc36ANuGKPx0pmYFCBWhCUdO0jxB/zn2Q=="], - "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.3", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ=="], + "@langchain/textsplitters": ["@langchain/textsplitters@1.0.1", "", { "dependencies": { "js-tiktoken": "^1.0.12" }, "peerDependencies": { "@langchain/core": "^1.0.0" } }, "sha512-rheJlB01iVtrOUzttscutRgLybPH9qR79EyzBEbf1u97ljWyuxQfCwIWK+SjoQTM9O8M7GGLLRBSYE26Jmcoww=="], - "@mswjs/interceptors": ["@mswjs/interceptors@0.40.0", "", { "dependencies": { "@open-draft/deferred-promise": "^2.2.0", "@open-draft/logger": "^0.3.0", "@open-draft/until": "^2.0.0", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "strict-event-emitter": "^0.5.1" } }, "sha512-EFd6cVbHsgLa6wa4RljGj6Wk75qoHxUSyc5asLyyPSyuhIcdS2Q3Phw6ImS1q+CkALthJRShiYfKANcQMuMqsQ=="], + "@mermaid-js/parser": ["@mermaid-js/parser@1.0.0", "", { "dependencies": { "langium": "^4.0.0" } }, "sha512-vvK0Hi/VWndxoh03Mmz6wa1KDriSPjS2XMZL/1l19HFwygiObEEoEwSDxOqyLzzAI6J2PU3261JjTMTO7x+BPw=="], + + "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.27.1", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA=="], + + "@mswjs/interceptors": ["@mswjs/interceptors@0.41.3", "", { "dependencies": { "@open-draft/deferred-promise": "^2.2.0", "@open-draft/logger": "^0.3.0", "@open-draft/until": "^2.0.0", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "strict-event-emitter": "^0.5.1" } }, "sha512-cXu86tF4VQVfwz8W1SPbhoRyHJkti6mjH/XJIxp40jhO4j2k1m4KYrEykxqWPkFF3vrK4rgQppBh//AwyGSXPA=="], "@next/env": ["@next/env@16.1.6", "", {}, "sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ=="], @@ -545,71 +553,73 @@ "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], - "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.53", "", {}, "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ=="], + "@reduxjs/toolkit": ["@reduxjs/toolkit@2.11.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^11.0.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "optionalPeers": ["react", "react-redux"] }, "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ=="], - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.56.0", "", { "os": "android", "cpu": "arm" }, "sha512-LNKIPA5k8PF1+jAFomGe3qN3bbIgJe/IlpDBwuVjrDKrJhVWywgnJvflMt/zkbVNLFtF1+94SljYQS6e99klnw=="], + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="], - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.56.0", "", { "os": "android", "cpu": "arm64" }, "sha512-lfbVUbelYqXlYiU/HApNMJzT1E87UPGvzveGg2h0ktUNlOCxKlWuJ9jtfvs1sKHdwU4fzY7Pl8sAl49/XaEk6Q=="], + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="], - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.56.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-EgxD1ocWfhoD6xSOeEEwyE7tDvwTgZc8Bss7wCWe+uc7wO8G34HHCUH+Q6cHqJubxIAnQzAsyUsClt0yFLu06w=="], + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="], - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.56.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-1vXe1vcMOssb/hOF8iv52A7feWW2xnu+c8BV4t1F//m9QVLTfNVpEdja5ia762j/UEJe2Z1jAmEqZAK42tVW3g=="], + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.59.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg=="], - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.56.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-bof7fbIlvqsyv/DtaXSck4VYQ9lPtoWNFCB/JY4snlFuJREXfZnm+Ej6yaCHfQvofJDXLDMTVxWscVSuQvVWUQ=="], + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.59.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w=="], - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.56.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-KNa6lYHloW+7lTEkYGa37fpvPq+NKG/EHKM8+G/g9WDU7ls4sMqbVRV78J6LdNuVaeeK5WB9/9VAFbKxcbXKYg=="], + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.59.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA=="], - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.56.0", "", { "os": "linux", "cpu": "arm" }, "sha512-E8jKK87uOvLrrLN28jnAAAChNq5LeCd2mGgZF+fGF5D507WlG/Noct3lP/QzQ6MrqJ5BCKNwI9ipADB6jyiq2A=="], + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.59.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg=="], - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.56.0", "", { "os": "linux", "cpu": "arm" }, "sha512-jQosa5FMYF5Z6prEpTCCmzCXz6eKr/tCBssSmQGEeozA9tkRUty/5Vx06ibaOP9RCrW1Pvb8yp3gvZhHwTDsJw=="], + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw=="], - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.56.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-uQVoKkrC1KGEV6udrdVahASIsaF8h7iLG0U0W+Xn14ucFwi6uS539PsAr24IEF9/FoDtzMeeJXJIBo5RkbNWvQ=="], + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA=="], - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.56.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-vLZ1yJKLxhQLFKTs42RwTwa6zkGln+bnXc8ueFGMYmBTLfNu58sl5/eXyxRa2RarTkJbXl8TKPgfS6V5ijNqEA=="], + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA=="], - "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-FWfHOCub564kSE3xJQLLIC/hbKqHSVxy8vY75/YHHzWvbJL7aYJkdgwD/xGfUlL5UV2SB7otapLrcCj2xnF1dg=="], + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA=="], - "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-z1EkujxIh7nbrKL1lmIpqFTc/sr0u8Uk0zK/qIEFldbt6EDKWFk/pxFq3gYj4Bjn3aa9eEhYRlL3H8ZbPT1xvA=="], + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg=="], - "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.56.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-iNFTluqgdoQC7AIE8Q34R3AuPrJGJirj5wMUErxj22deOcY7XwZRaqYmB6ZKFHoVGqRcRd0mqO+845jAibKCkw=="], + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q=="], - "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.56.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-MtMeFVlD2LIKjp2sE2xM2slq3Zxf9zwVuw0jemsxvh1QOpHSsSzfNOTH9uYW9i1MXFxUSMmLpeVeUzoNOKBaWg=="], + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA=="], - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-in+v6wiHdzzVhYKXIk5U74dEZHdKN9KH0Q4ANHOTvyXPG41bajYRsy7a8TPKbYPl34hU7PP7hMVHRvv/5aCSew=="], + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA=="], - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-yni2raKHB8m9NQpI9fPVwN754mn6dHQSbDTwxdr9SE0ks38DTjLMMBjrwvB5+mXrX+C0npX0CVeCUcvvvD8CNQ=="], + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg=="], - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.56.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-zhLLJx9nQPu7wezbxt2ut+CI4YlXi68ndEve16tPc/iwoylWS9B3FxpLS2PkmfYgDQtosah07Mj9E0khc3Y+vQ=="], + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg=="], - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.56.0", "", { "os": "linux", "cpu": "x64" }, "sha512-MVC6UDp16ZSH7x4rtuJPAEoE1RwS8N4oK9DLHy3FTEdFoUTCFVzMfJl/BVJ330C+hx8FfprA5Wqx4FhZXkj2Kw=="], + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.59.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w=="], - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.56.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ZhGH1eA4Qv0lxaV00azCIS1ChedK0V32952Md3FtnxSqZTBTd6tgil4nZT5cU8B+SIw3PFYkvyR4FKo2oyZIHA=="], + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg=="], - "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.56.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-O16XcmyDeFI9879pEcmtWvD/2nyxR9mF7Gs44lf1vGGx8Vg2DRNx11aVXBEqOQhWb92WN4z7fW/q4+2NYzCbBA=="], + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg=="], - "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.56.0", "", { "os": "none", "cpu": "arm64" }, "sha512-LhN/Reh+7F3RCgQIRbgw8ZMwUwyqJM+8pXNT6IIJAqm2IdKkzpCh/V9EdgOMBKuebIrzswqy4ATlrDgiOwbRcQ=="], + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.59.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ=="], - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.56.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-kbFsOObXp3LBULg1d3JIUQMa9Kv4UitDmpS+k0tinPBz3watcUiV2/LUDMMucA6pZO3WGE27P7DsfaN54l9ing=="], + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.59.0", "", { "os": "none", "cpu": "arm64" }, "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA=="], - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.56.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-vSSgny54D6P4vf2izbtFm/TcWYedw7f8eBrOiGGecyHyQB9q4Kqentjaj8hToe+995nob/Wv48pDqL5a62EWtg=="], + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.59.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A=="], - "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.56.0", "", { "os": "win32", "cpu": "x64" }, "sha512-FeCnkPCTHQJFbiGG49KjV5YGW/8b9rrXAM2Mz2kiIoktq2qsJxRD5giEMEOD2lPdgs72upzefaUvS+nc8E3UzQ=="], + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.59.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA=="], - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.56.0", "", { "os": "win32", "cpu": "x64" }, "sha512-H8AE9Ur/t0+1VXujj90w0HrSOuv0Nq9r1vSZF2t5km20NTfosQsGGUXDaKdQZzwuLts7IyL1fYT4hM95TI9c4g=="], + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="], "@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="], - "@shikijs/core": ["@shikijs/core@3.21.0", "", { "dependencies": { "@shikijs/types": "3.21.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-AXSQu/2n1UIQekY8euBJlvFYZIw0PHY63jUzGbrOma4wPxzznJXTXkri+QcHeBNaFxiiOljKxxJkVSoB3PjbyA=="], + "@shikijs/core": ["@shikijs/core@3.22.0", "", { "dependencies": { "@shikijs/types": "3.22.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-iAlTtSDDbJiRpvgL5ugKEATDtHdUVkqgHDm/gbD2ZS9c88mx7G1zSYjjOxp5Qa0eaW0MAQosFRmJSk354PRoQA=="], - "@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.21.0", "", { "dependencies": { "@shikijs/types": "3.21.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-ATwv86xlbmfD9n9gKRiwuPpWgPENAWCLwYCGz9ugTJlsO2kOzhOkvoyV/UD+tJ0uT7YRyD530x6ugNSffmvIiQ=="], + "@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.22.0", "", { "dependencies": { "@shikijs/types": "3.22.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-jdKhfgW9CRtj3Tor0L7+yPwdG3CgP7W+ZEqSsojrMzCjD1e0IxIbwUMDDpYlVBlC08TACg4puwFGkZfLS+56Tw=="], - "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.21.0", "", { "dependencies": { "@shikijs/types": "3.21.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-OYknTCct6qiwpQDqDdf3iedRdzj6hFlOPv5hMvI+hkWfCKs5mlJ4TXziBG9nyabLwGulrUjHiCq3xCspSzErYQ=="], + "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.22.0", "", { "dependencies": { "@shikijs/types": "3.22.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-DyXsOG0vGtNtl7ygvabHd7Mt5EY8gCNqR9Y7Lpbbd/PbJvgWrqaKzH1JW6H6qFkuUa8aCxoiYVv8/YfFljiQxA=="], - "@shikijs/langs": ["@shikijs/langs@3.21.0", "", { "dependencies": { "@shikijs/types": "3.21.0" } }, "sha512-g6mn5m+Y6GBJ4wxmBYqalK9Sp0CFkUqfNzUy2pJglUginz6ZpWbaWjDB4fbQ/8SHzFjYbtU6Ddlp1pc+PPNDVA=="], + "@shikijs/langs": ["@shikijs/langs@3.22.0", "", { "dependencies": { "@shikijs/types": "3.22.0" } }, "sha512-x/42TfhWmp6H00T6uwVrdTJGKgNdFbrEdhaDwSR5fd5zhQ1Q46bHq9EO61SCEWJR0HY7z2HNDMaBZp8JRmKiIA=="], - "@shikijs/themes": ["@shikijs/themes@3.21.0", "", { "dependencies": { "@shikijs/types": "3.21.0" } }, "sha512-BAE4cr9EDiZyYzwIHEk7JTBJ9CzlPuM4PchfcA5ao1dWXb25nv6hYsoDiBq2aZK9E3dlt3WB78uI96UESD+8Mw=="], + "@shikijs/themes": ["@shikijs/themes@3.22.0", "", { "dependencies": { "@shikijs/types": "3.22.0" } }, "sha512-o+tlOKqsr6FE4+mYJG08tfCFDS+3CG20HbldXeVoyP+cYSUxDhrFf3GPjE60U55iOkkjbpY2uC3It/eeja35/g=="], - "@shikijs/types": ["@shikijs/types@3.21.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-zGrWOxZ0/+0ovPY7PvBU2gIS9tmhSUUt30jAcNV0Bq0gb2S98gwfjIs1vxlmH5zM7/4YxLamT6ChlqqAJmPPjA=="], + "@shikijs/types": ["@shikijs/types@3.22.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-491iAekgKDBFE67z70Ok5a8KBMsQ2IJwOWw3us/7ffQkIBCyOQfm/aNwVMBUriP02QshIfgHCBSIYAl3u2eWjg=="], "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], @@ -617,57 +627,59 @@ "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@streamdown/cjk": ["@streamdown/cjk@1.0.1", "", { "dependencies": { "remark-cjk-friendly": "^1.2.3", "remark-cjk-friendly-gfm-strikethrough": "^1.2.3", "unist-util-visit": "^5.0.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-ElDoEfad2u8iFzmgmEEab15N4mt19r47xeUIPJtHaHVyEF5baojamGo+xw3MywMj2qUsAY3LnTnKbrUtL5tGkg=="], + "@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="], - "@streamdown/code": ["@streamdown/code@1.0.1", "", { "dependencies": { "shiki": "^3.19.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-U9LITfQ28tZYAoY922jdtw1ryg4kgRBdURopqK9hph7G2fBUwPeHthjH7SvaV0fvFv7EqjqCzARJuWUljLe9Ag=="], + "@streamdown/cjk": ["@streamdown/cjk@1.0.2", "", { "dependencies": { "remark-cjk-friendly": "^1.2.3", "remark-cjk-friendly-gfm-strikethrough": "^1.2.3", "unist-util-visit": "^5.0.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-5OOuZjj2Lnae92Zmg2gA5hloSbcKj25gv+QY4iKbYI+iRsiGWbgmYxmgxNUSO9SR6BKOCy783UHN1HM/QEUpdw=="], - "@streamdown/math": ["@streamdown/math@1.0.1", "", { "dependencies": { "katex": "^0.16.27", "rehype-katex": "^7.0.1", "remark-math": "^6.0.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-R9WdHbpERiRU7WeO7oT1aIbnLJ/jraDr89F7X9x2OM//Y8G8UMATRnLD/RUwg4VLr8Nu7QSIJ0Pa8lXd2meM4Q=="], + "@streamdown/code": ["@streamdown/code@1.0.3", "", { "dependencies": { "shiki": "^3.19.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-3Ym5TCLcGhrHY2qBaUVWpqNRtxnZvqh4Y5Qm/pTIKA4AmEWwAAoYjZnxG7mOsvOpWVWiDwETjUtchNL1XzQEAw=="], - "@streamdown/mermaid": ["@streamdown/mermaid@1.0.1", "", { "dependencies": { "mermaid": "^11.12.2" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-LVGbxYd6t1DKMCMqm3cpbfsdD4/EKpQelanOlJaBMKv83kbrl8syZJhVBsd/jka+CawhpeR9xsGQJzSJEpjoVw=="], + "@streamdown/math": ["@streamdown/math@1.0.2", "", { "dependencies": { "katex": "^0.16.27", "rehype-katex": "^7.0.1", "remark-math": "^6.0.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-r8Ur9/lBuFnzZAFdEWrLUF2s/gRwRRRwruqltdZibyjbCBnuW7SJbFm26nXqvpJPW/gzpBUMrBVBzd88z05D5g=="], + + "@streamdown/mermaid": ["@streamdown/mermaid@1.0.2", "", { "dependencies": { "mermaid": "^11.12.2" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-Fr/4sBWnAeSnxM3PcrV/+DiZe5oPMq9gOkUIAH7ZauJeuwrZ/DVzD4g0zlav6AH0axh2m/sOfrfLtY5aLT7niw=="], "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], - "@tailwindcss/node": ["@tailwindcss/node@4.1.18", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.18" } }, "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ=="], + "@tailwindcss/node": ["@tailwindcss/node@4.2.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.31.1", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.1" } }, "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg=="], - "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.18", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.18", "@tailwindcss/oxide-darwin-arm64": "4.1.18", "@tailwindcss/oxide-darwin-x64": "4.1.18", "@tailwindcss/oxide-freebsd-x64": "4.1.18", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", "@tailwindcss/oxide-linux-x64-musl": "4.1.18", "@tailwindcss/oxide-wasm32-wasi": "4.1.18", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" } }, "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A=="], + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.1", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.1", "@tailwindcss/oxide-darwin-arm64": "4.2.1", "@tailwindcss/oxide-darwin-x64": "4.2.1", "@tailwindcss/oxide-freebsd-x64": "4.2.1", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", "@tailwindcss/oxide-linux-x64-musl": "4.2.1", "@tailwindcss/oxide-wasm32-wasi": "4.2.1", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" } }, "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw=="], - "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.18", "", { "os": "android", "cpu": "arm64" }, "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q=="], + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.1", "", { "os": "android", "cpu": "arm64" }, "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg=="], - "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.18", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A=="], + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw=="], - "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.18", "", { "os": "darwin", "cpu": "x64" }, "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw=="], + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw=="], - "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.18", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA=="], + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA=="], - "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18", "", { "os": "linux", "cpu": "arm" }, "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA=="], + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw=="], - "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw=="], + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ=="], - "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg=="], + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ=="], - "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.18", "", { "os": "linux", "cpu": "x64" }, "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g=="], + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g=="], - "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.18", "", { "os": "linux", "cpu": "x64" }, "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ=="], + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g=="], - "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.18", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.0", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.4.0" }, "cpu": "none" }, "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA=="], + "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.1", "", { "dependencies": { "@emnapi/core": "^1.8.1", "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q=="], - "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.18", "", { "os": "win32", "cpu": "arm64" }, "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA=="], + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA=="], - "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.18", "", { "os": "win32", "cpu": "x64" }, "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q=="], + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.1", "", { "os": "win32", "cpu": "x64" }, "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ=="], - "@tailwindcss/vite": ["@tailwindcss/vite@4.1.18", "", { "dependencies": { "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "tailwindcss": "4.1.18" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA=="], + "@tailwindcss/vite": ["@tailwindcss/vite@4.2.1", "", { "dependencies": { "@tailwindcss/node": "4.2.1", "@tailwindcss/oxide": "4.2.1", "tailwindcss": "4.2.1" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w=="], - "@tanstack/history": ["@tanstack/history@1.154.14", "", {}, "sha512-xyIfof8eHBuub1CkBnbKNKQXeRZC4dClhmzePHVOEel4G7lk/dW+TQ16da7CFdeNLv6u6Owf5VoBQxoo6DFTSA=="], + "@tanstack/history": ["@tanstack/history@1.161.4", "", {}, "sha512-Kp/WSt411ZWYvgXy6uiv5RmhHrz9cAml05AQPrtdAp7eUqvIDbMGPnML25OKbzR3RJ1q4wgENxDTvlGPa9+Mww=="], - "@tanstack/react-router": ["@tanstack/react-router@1.156.0", "", { "dependencies": { "@tanstack/history": "1.154.14", "@tanstack/react-store": "^0.8.0", "@tanstack/router-core": "1.156.0", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-ET1MyOhLAWVYNE/6XQmIi1RNcmTVsN+rPAW7sjU7XDbQ62g4kAyK4sip2WkInYOHrB5iWktvqtjDsdErvGugVw=="], + "@tanstack/react-router": ["@tanstack/react-router@1.162.9", "", { "dependencies": { "@tanstack/history": "1.161.4", "@tanstack/react-store": "^0.9.1", "@tanstack/router-core": "1.162.9", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-APbwKAF+YgSNpHAaA+FdgrmfI/7+qa9hApuVO9+P0IVksJayNIWFQ/6AFG90WQiTYWk64RI1R9cFV2K9Z+j2pQ=="], - "@tanstack/react-store": ["@tanstack/react-store@0.8.0", "", { "dependencies": { "@tanstack/store": "0.8.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-1vG9beLIuB7q69skxK9r5xiLN3ztzIPfSQSs0GfeqWGO2tGIyInZx0x1COhpx97RKaONSoAb8C3dxacWksm1ow=="], + "@tanstack/react-store": ["@tanstack/react-store@0.9.1", "", { "dependencies": { "@tanstack/store": "0.9.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-YzJLnRvy5lIEFTLWBAZmcOjK3+2AepnBv/sr6NZmiqJvq7zTQggyK99Gw8fqYdMdHPQWXjz0epFKJXC+9V2xDA=="], "@tanstack/react-table": ["@tanstack/react-table@8.21.3", "", { "dependencies": { "@tanstack/table-core": "8.21.3" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww=="], - "@tanstack/router-core": ["@tanstack/router-core@1.156.0", "", { "dependencies": { "@tanstack/history": "1.154.14", "@tanstack/store": "^0.8.0", "cookie-es": "^2.0.0", "seroval": "^1.4.2", "seroval-plugins": "^1.4.2", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-v4/ecOxEHn9Wd9xvDX5sgHVK9NOQCKYg3VSx3xPwEkJL9mV20/raYj8uY9n3+uSCpE6TpSsak0br9Nw64if85w=="], + "@tanstack/router-core": ["@tanstack/router-core@1.162.9", "", { "dependencies": { "@tanstack/history": "1.161.4", "@tanstack/store": "^0.9.1", "cookie-es": "^2.0.0", "seroval": "^1.4.2", "seroval-plugins": "^1.4.2", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-eG7C0oVtZbFOkfvsaF8UyGuNjEc1BfIfD5EzQNwG4vqLKOAyY5SMFBCNjabAi2sglRhL0ZOwKon1SExusU5fxA=="], - "@tanstack/store": ["@tanstack/store@0.8.0", "", {}, "sha512-Om+BO0YfMZe//X2z0uLF2j+75nQga6TpTJgLJQBiq85aOyZNIhkCgleNcud2KQg4k4v9Y9l+Uhru3qWMPGTOzQ=="], + "@tanstack/store": ["@tanstack/store@0.9.1", "", {}, "sha512-+qcNkOy0N1qSGsP7omVCW0SDrXtaDcycPqBDE726yryiA5eTDFpjBReaYjghVJwNf1pcPMyzIwTGlYjCSQR0Fg=="], "@tanstack/table-core": ["@tanstack/table-core@8.21.3", "", {}, "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg=="], @@ -757,6 +769,8 @@ "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], + "@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="], + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], "@types/katex": ["@types/katex@0.16.8", "", {}, "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg=="], @@ -765,9 +779,9 @@ "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], - "@types/node": ["@types/node@24.10.9", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw=="], + "@types/node": ["@types/node@24.10.13", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg=="], - "@types/react": ["@types/react@19.2.9", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA=="], + "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], @@ -777,63 +791,67 @@ "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], + "@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="], + + "@types/uuid": ["@types/uuid@10.0.0", "", {}, "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ=="], + "@types/validate-npm-package-name": ["@types/validate-npm-package-name@4.0.2", "", {}, "sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.53.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.53.1", "@typescript-eslint/type-utils": "8.53.1", "@typescript-eslint/utils": "8.53.1", "@typescript-eslint/visitor-keys": "8.53.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.53.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-cFYYFZ+oQFi6hUnBTbLRXfTJiaQtYE3t4O692agbBl+2Zy+eqSKWtPjhPXJu1G7j4RLjKgeJPDdq3EqOwmX5Ag=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.56.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/type-utils": "8.56.1", "@typescript-eslint/utils": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.56.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A=="], - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.53.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.53.1", "@typescript-eslint/types": "8.53.1", "@typescript-eslint/typescript-estree": "8.53.1", "@typescript-eslint/visitor-keys": "8.53.1", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-nm3cvFN9SqZGXjmw5bZ6cGmvJSyJPn0wU9gHAZZHDnZl2wF9PhHv78Xf06E0MaNk4zLVHL8hb2/c32XvyJOLQg=="], + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.56.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg=="], - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.53.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.53.1", "@typescript-eslint/types": "^8.53.1", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-WYC4FB5Ra0xidsmlPb+1SsnaSKPmS3gsjIARwbEkHkoWloQmuzcfypljaJcR78uyLA1h8sHdWWPHSLDI+MtNog=="], + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.56.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.56.1", "@typescript-eslint/types": "^8.56.1", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ=="], - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.53.1", "", { "dependencies": { "@typescript-eslint/types": "8.53.1", "@typescript-eslint/visitor-keys": "8.53.1" } }, "sha512-Lu23yw1uJMFY8cUeq7JlrizAgeQvWugNQzJp8C3x8Eo5Jw5Q2ykMdiiTB9vBVOOUBysMzmRRmUfwFrZuI2C4SQ=="], + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1" } }, "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w=="], - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.53.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qfvLXS6F6b1y43pnf0pPbXJ+YoXIC7HKg0UGZ27uMIemKMKA6XH2DTxsEDdpdN29D+vHV07x/pnlPNVLhdhWiA=="], + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.56.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ=="], - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.53.1", "", { "dependencies": { "@typescript-eslint/types": "8.53.1", "@typescript-eslint/typescript-estree": "8.53.1", "@typescript-eslint/utils": "8.53.1", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-MOrdtNvyhy0rHyv0ENzub1d4wQYKb2NmIqG7qEqPWFW7Mpy2jzFC3pQ2yKDvirZB7jypm5uGjF2Qqs6OIqu47w=="], + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/utils": "8.56.1", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg=="], - "@typescript-eslint/types": ["@typescript-eslint/types@8.53.1", "", {}, "sha512-jr/swrr2aRmUAUjW5/zQHbMaui//vQlsZcJKijZf3M26bnmLj8LyZUpj8/Rd6uzaek06OWsqdofN/Thenm5O8A=="], + "@typescript-eslint/types": ["@typescript-eslint/types@8.56.1", "", {}, "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw=="], - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.53.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.53.1", "@typescript-eslint/tsconfig-utils": "8.53.1", "@typescript-eslint/types": "8.53.1", "@typescript-eslint/visitor-keys": "8.53.1", "debug": "^4.4.3", "minimatch": "^9.0.5", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-RGlVipGhQAG4GxV1s34O91cxQ/vWiHJTDHbXRr0li2q/BGg3RR/7NM8QDWgkEgrwQYCvmJV9ichIwyoKCQ+DTg=="], + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.56.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.56.1", "@typescript-eslint/tsconfig-utils": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg=="], - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.53.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.53.1", "@typescript-eslint/types": "8.53.1", "@typescript-eslint/typescript-estree": "8.53.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-c4bMvGVWW4hv6JmDUEG7fSYlWOl3II2I4ylt0NM+seinYQlZMQIaKaXIIVJWt9Ofh6whrpM+EdDQXKXjNovvrg=="], + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.56.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA=="], - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.53.1", "", { "dependencies": { "@typescript-eslint/types": "8.53.1", "eslint-visitor-keys": "^4.2.1" } }, "sha512-oy+wV7xDKFPRyNggmXuZQSBzvoLnpmJs+GhzRhPjrxl2b/jIlyjVokzm47CZCDUdXKr2zd7ZLodPfOBpOPyPlg=="], + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw=="], "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], - "@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.2", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.53", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ=="], + "@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.4", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA=="], "@xmldom/xmldom": ["@xmldom/xmldom@0.8.11", "", {}, "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw=="], - "@xyflow/react": ["@xyflow/react@12.10.0", "", { "dependencies": { "@xyflow/system": "0.0.74", "classcat": "^5.0.3", "zustand": "^4.4.0" }, "peerDependencies": { "react": ">=17", "react-dom": ">=17" } }, "sha512-eOtz3whDMWrB4KWVatIBrKuxECHqip6PfA8fTpaS2RUGVpiEAe+nqDKsLqkViVWxDGreq0lWX71Xth/SPAzXiw=="], + "@xyflow/react": ["@xyflow/react@12.10.1", "", { "dependencies": { "@xyflow/system": "0.0.75", "classcat": "^5.0.3", "zustand": "^4.4.0" }, "peerDependencies": { "react": ">=17", "react-dom": ">=17" } }, "sha512-5eSWtIK/+rkldOuFbOOz44CRgQRjtS9v5nufk77DV+XBnfCGL9HAQ8PG00o2ZYKqkEU/Ak6wrKC95Tu+2zuK3Q=="], - "@xyflow/system": ["@xyflow/system@0.0.74", "", { "dependencies": { "@types/d3-drag": "^3.0.7", "@types/d3-interpolate": "^3.0.4", "@types/d3-selection": "^3.0.10", "@types/d3-transition": "^3.0.8", "@types/d3-zoom": "^3.0.8", "d3-drag": "^3.0.0", "d3-interpolate": "^3.0.1", "d3-selection": "^3.0.0", "d3-zoom": "^3.0.0" } }, "sha512-7v7B/PkiVrkdZzSbL+inGAo6tkR/WQHHG0/jhSvLQToCsfa8YubOGmBYd1s08tpKpihdHDZFwzQZeR69QSBb4Q=="], + "@xyflow/system": ["@xyflow/system@0.0.75", "", { "dependencies": { "@types/d3-drag": "^3.0.7", "@types/d3-interpolate": "^3.0.4", "@types/d3-selection": "^3.0.10", "@types/d3-transition": "^3.0.8", "@types/d3-zoom": "^3.0.8", "d3-drag": "^3.0.0", "d3-interpolate": "^3.0.1", "d3-selection": "^3.0.0", "d3-zoom": "^3.0.0" } }, "sha512-iXs+AGFLi8w/VlAoc/iSxk+CxfT6o64Uw/k0CKASOPqjqz6E0rb5jFZgJtXGZCpfQI6OQpu5EnumP5fGxQheaQ=="], "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], - "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], + "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], - "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + "ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], - "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], "ansis": ["ansis@4.2.0", "", {}, "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig=="], - "argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], - "assistant-cloud": ["assistant-cloud@0.1.15", "", { "dependencies": { "assistant-stream": "^0.3.0" } }, "sha512-LK+HrE6p1/jefzH3IosGUfpZvsM8wLPCsd955TLPEfJ9rMknl9KeE2QxRMHl3Ob17m33NqmJi/t/oWWKdgP6Nw=="], + "assistant-cloud": ["assistant-cloud@0.1.18", "", { "dependencies": { "assistant-stream": "^0.3.3" } }, "sha512-6tq2jPGIBjkjsLQ/Fd4r6PGj4hf05oM2jBl4hBs7YIkaJ3qBVUWiHary2+faNpsPOoY71brsVukl/qz5B1rQkA=="], - "assistant-stream": ["assistant-stream@0.3.0", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "nanoid": "^5.1.6", "secure-json-parse": "^4.1.0" } }, "sha512-gp5wXZiH7fiPdCByusRZ6ZQrmifAjiuDTenR0HSeviiRLXmirePxePvTgHPT1ZahohCLroge7fjtC9vAc+Hqsg=="], + "assistant-stream": ["assistant-stream@0.3.3", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "nanoid": "^5.1.6", "secure-json-parse": "^4.1.0" } }, "sha512-Ne/uTseMIiZx740dTbr/SWxONM8nYj4Z5BRmUfqQN+TNgtOCgWOlC/oTUQ+A7LIUHtmGbcoyZwDf8yd2RASnDA=="], "ast-types": ["ast-types@0.16.1", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg=="], @@ -843,7 +861,7 @@ "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.9.17", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-agD0MgJFUP/4nvjqzIB29zRPUuCF7Ge6mEv9s8dHrtYD7QWXRcx75rOADE/d5ah1NI+0vkDl0yorDd5U852IQQ=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.0", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA=="], "bluebird": ["bluebird@3.4.7", "", {}, "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA=="], @@ -865,7 +883,9 @@ "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], - "caniuse-lite": ["caniuse-lite@1.0.30001766", "", {}, "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA=="], + "camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001774", "", {}, "sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA=="], "canvas-confetti": ["canvas-confetti@1.9.4", "", {}, "sha512-yxQbJkAVrFXWNbTUjPqjF7G+g6pDotOUHGbkZq2NELZUMDpiJ85rIEazVb8GTaAptNW2miJAXbs1BtioA251Pw=="], @@ -881,7 +901,7 @@ "character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="], - "chevrotain": ["chevrotain@11.0.3", "", { "dependencies": { "@chevrotain/cst-dts-gen": "11.0.3", "@chevrotain/gast": "11.0.3", "@chevrotain/regexp-to-ast": "11.0.3", "@chevrotain/types": "11.0.3", "@chevrotain/utils": "11.0.3", "lodash-es": "4.17.21" } }, "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw=="], + "chevrotain": ["chevrotain@11.1.1", "", { "dependencies": { "@chevrotain/cst-dts-gen": "11.1.1", "@chevrotain/gast": "11.1.1", "@chevrotain/regexp-to-ast": "11.1.1", "@chevrotain/types": "11.1.1", "@chevrotain/utils": "11.1.1", "lodash-es": "4.17.23" } }, "sha512-f0yv5CPKaFxfsPTBzX7vGuim4oIC1/gcS7LUGdBSwl2dU6+FON6LVUksdOo1qJjoUvXNn45urgh8C+0a24pACQ=="], "chevrotain-allstar": ["chevrotain-allstar@0.3.1", "", { "dependencies": { "lodash-es": "^4.17.21" }, "peerDependencies": { "chevrotain": "^11.0.0" } }, "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw=="], @@ -921,6 +941,8 @@ "confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], + "console-table-printer": ["console-table-printer@2.15.0", "", { "dependencies": { "simple-wcswidth": "^1.1.2" } }, "sha512-SrhBq4hYVjLCkBVOWaTzceJalvn5K1Zq5aQA6wXC/cYjI3frKWNPEMK3sZsJfNNQApvCQmgBcc13ZKmFj8qExw=="], + "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="], "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], @@ -1029,6 +1051,8 @@ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="], + "decimal.js-light": ["decimal.js-light@2.5.1", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="], "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], @@ -1039,7 +1063,7 @@ "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], - "default-browser": ["default-browser@5.4.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg=="], + "default-browser": ["default-browser@5.5.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw=="], "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="], @@ -1057,33 +1081,31 @@ "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], - "dexie": ["dexie@4.2.1", "", {}, "sha512-Ckej0NS6jxQ4Po3OrSQBFddayRhTCic2DoCAG5zacOfOVB9P2Q5Xc5uL/nVa7ZVs+HdMnvUPzLFCB/JwpB6Csg=="], + "dexie": ["dexie@4.3.0", "", {}, "sha512-5EeoQpJvMKHe6zWt/FSIIuRa3CWlZeIl6zKXt+Lz7BU6RoRRLgX9dZEynRfXrkLcldKYCBiz7xekTEylnie1Ug=="], "diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="], "dingbat-to-unicode": ["dingbat-to-unicode@1.0.1", "", {}, "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w=="], - "dom-helpers": ["dom-helpers@5.2.1", "", { "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" } }, "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA=="], - "dompurify": ["dompurify@3.3.1", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q=="], - "dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="], + "dotenv": ["dotenv@17.3.1", "", {}, "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA=="], "duck": ["duck@0.1.12", "", { "dependencies": { "underscore": "^1.13.1" } }, "sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg=="], "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], - "eciesjs": ["eciesjs@0.4.16", "", { "dependencies": { "@ecies/ciphers": "^0.2.4", "@noble/ciphers": "^1.3.0", "@noble/curves": "^1.9.7", "@noble/hashes": "^1.8.0" } }, "sha512-dS5cbA9rA2VR4Ybuvhg6jvdmp46ubLn3E+px8cG/35aEDNclrqoCjg6mt0HYZ/M+OoESS3jSkCrqk1kWAEhWAw=="], + "eciesjs": ["eciesjs@0.4.17", "", { "dependencies": { "@ecies/ciphers": "^0.2.5", "@noble/ciphers": "^1.3.0", "@noble/curves": "^1.9.7", "@noble/hashes": "^1.8.0" } }, "sha512-TOOURki4G7sD1wDCjj7NfLaXZZ49dFOeEb5y39IXpb8p0hRzVvfvzZHOi5JcT+PpyAbi/Y+lxPb8eTag2WYH8w=="], "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - "electron-to-chromium": ["electron-to-chromium@1.5.278", "", {}, "sha512-dQ0tM1svDRQOwxnXxm+twlGTjr9Upvt8UFWAgmLsxEzFQxhbti4VwxmMjsDxVC51Zo84swW7FVCXEV+VAkhuPw=="], + "electron-to-chromium": ["electron-to-chromium@1.5.302", "", {}, "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg=="], "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], - "enhanced-resolve": ["enhanced-resolve@5.18.4", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q=="], + "enhanced-resolve": ["enhanced-resolve@5.19.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg=="], "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], @@ -1097,7 +1119,9 @@ "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], - "esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="], + "es-toolkit": ["es-toolkit@1.44.0", "", {}, "sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg=="], + + "esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], @@ -1105,7 +1129,7 @@ "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], - "eslint": ["eslint@9.39.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@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.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", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw=="], + "eslint": ["eslint@9.39.3", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@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.39.3", "@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", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg=="], "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.0.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA=="], @@ -1131,7 +1155,7 @@ "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], - "eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="], + "eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], @@ -1141,14 +1165,12 @@ "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], - "express-rate-limit": ["express-rate-limit@7.5.1", "", { "peerDependencies": { "express": ">= 4.11" } }, "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw=="], + "express-rate-limit": ["express-rate-limit@8.2.1", "", { "dependencies": { "ip-address": "10.0.1" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g=="], "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], - "fast-equals": ["fast-equals@5.4.0", "", {}, "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw=="], - "fast-glob": ["fast-glob@3.3.3", "", { "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" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], @@ -1199,7 +1221,7 @@ "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], - "get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="], + "get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="], "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], @@ -1219,7 +1241,7 @@ "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], - "graphql": ["graphql@16.12.0", "", {}, "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ=="], + "graphql": ["graphql@16.13.0", "", {}, "sha512-uSisMYERbaB9bkA9M4/4dnqyktaEkf1kMHNKq/7DHyxVeWqHQ2mBmVqm5u6/FVHwF3iCNalKcg82Zfl+tffWoA=="], "hachure-fill": ["hachure-fill@0.5.2", "", {}, "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg=="], @@ -1263,7 +1285,7 @@ "hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="], - "hono": ["hono@4.11.5", "", {}, "sha512-WemPi9/WfyMwZs+ZUXdiwcCh9Y+m7L+8vki9MzDw3jJ+W9Lc+12HGsd368Qc1vZi1xwW8BWMMsnK5efYKPdt4g=="], + "hono": ["hono@4.12.2", "", {}, "sha512-gJnaDHXKDayjt8ue0n8Gs0A007yKXj4Xzb8+cNjZeYsSzzwKc0Lr+OZgYwVfB0pHfUs17EPoLvrOsEaJ9mj+Tg=="], "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="], @@ -1281,6 +1303,8 @@ "immediate": ["immediate@3.0.6", "", {}, "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="], + "immer": ["immer@10.2.0", "", {}, "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw=="], + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], @@ -1291,6 +1315,8 @@ "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], + "ip-address": ["ip-address@10.0.1", "", {}, "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA=="], + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="], @@ -1333,11 +1359,11 @@ "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], - "is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="], + "is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="], "isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], - "isbot": ["isbot@5.1.33", "", {}, "sha512-P4Hgb5NqswjkI0J1CM6XKXon/sxKY1SuowE7Qx2hrBhIwICFyXy54mfgB5eMHXsbe/eStzzpbIGNOvGmz+dlKg=="], + "isbot": ["isbot@5.1.35", "", {}, "sha512-waFfC72ZNfwLLuJ2iLaoVaqcNo+CAaLR7xCpAn0Y5WfGzkNHv7ZN39Vbi1y+kb+Zs46XHOX3tZNExroFUPX+Kg=="], "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], @@ -1345,6 +1371,8 @@ "jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="], + "js-tiktoken": ["js-tiktoken@1.0.21", "", { "dependencies": { "base64-js": "^1.5.1" } }, "sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g=="], + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], @@ -1367,7 +1395,7 @@ "jszip": ["jszip@3.10.1", "", { "dependencies": { "lie": "~3.3.0", "pako": "~1.0.2", "readable-stream": "~2.3.6", "setimmediate": "^1.0.5" } }, "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g=="], - "katex": ["katex@0.16.28", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-YHzO7721WbmAL6Ov1uzN/l5mY5WWWhJBSW+jq4tkfZfsxmo1hu6frS0EOswvjBUnWE6NtjEs48SFn5CQESRLZg=="], + "katex": ["katex@0.16.33", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-q3N5u+1sY9Bu7T4nlXoiRBXWfwSefNGoKeOwekV+gw0cAXQlz2Ww6BLcmBxVDeXBMUDQv6fK5bcNaJLxob3ZQA=="], "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], @@ -1375,7 +1403,9 @@ "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], - "langium": ["langium@3.3.1", "", { "dependencies": { "chevrotain": "~11.0.3", "chevrotain-allstar": "~0.3.0", "vscode-languageserver": "~9.0.1", "vscode-languageserver-textdocument": "~1.0.11", "vscode-uri": "~3.0.8" } }, "sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w=="], + "langium": ["langium@4.2.1", "", { "dependencies": { "chevrotain": "~11.1.1", "chevrotain-allstar": "~0.3.1", "vscode-languageserver": "~9.0.1", "vscode-languageserver-textdocument": "~1.0.11", "vscode-uri": "~3.1.0" } }, "sha512-zu9QWmjpzJcomzdJQAHgDVhLGq5bLosVak1KVa40NzQHXfqr4eAHupvnPOVXEoLkg6Ocefvf/93d//SB7du4YQ=="], + + "langsmith": ["langsmith@0.5.6", "", { "dependencies": { "@types/uuid": "^10.0.0", "chalk": "^5.6.2", "console-table-printer": "^2.12.1", "p-queue": "^6.6.2", "semver": "^7.6.3", "uuid": "^10.0.0" }, "peerDependencies": { "@opentelemetry/api": "*", "@opentelemetry/exporter-trace-otlp-proto": "*", "@opentelemetry/sdk-trace-base": "*", "openai": "*" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/exporter-trace-otlp-proto", "@opentelemetry/sdk-trace-base", "openai"] }, "sha512-T/RA2l2MsTYX0z1aW8rQ2hBQZEOuXV2v/6tkfG6R5EotJTKMpw1dERCbvP8ezOP8otyWfnNlQA88ZnMRsQ7CHA=="], "layout-base": ["layout-base@1.0.2", "", {}, "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg=="], @@ -1383,36 +1413,34 @@ "lie": ["lie@3.3.0", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="], - "lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="], + "lightningcss": ["lightningcss@1.31.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="], - "lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="], + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.31.1", "", { "os": "android", "cpu": "arm64" }, "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg=="], - "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA=="], + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.31.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg=="], - "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ=="], + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.31.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA=="], - "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA=="], + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.31.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A=="], - "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.2", "", { "os": "linux", "cpu": "arm" }, "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA=="], + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.31.1", "", { "os": "linux", "cpu": "arm" }, "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g=="], - "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A=="], + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg=="], - "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA=="], + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg=="], - "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w=="], + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA=="], - "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA=="], + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA=="], - "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ=="], + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.31.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w=="], - "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="], + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.31.1", "", { "os": "win32", "cpu": "x64" }, "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw=="], "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], - "lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="], - "lodash-es": ["lodash-es@4.17.23", "", {}, "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg=="], "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], @@ -1421,13 +1449,11 @@ "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], - "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], - "lop": ["lop@0.4.2", "", { "dependencies": { "duck": "^0.1.12", "option": "~0.2.1", "underscore": "^1.13.1" } }, "sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw=="], "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], - "lucide-react": ["lucide-react@0.563.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-8dXPB2GI4dI8jV4MgUDGBeLdGk8ekfqVZ0BdLcrRzocGgG75ltNEmWS+gE7uokKF/0oSUuczNDT+g9hFJ23FkA=="], + "lucide-react": ["lucide-react@0.575.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-VuXgKZrk0uiDlWjGGXmKV6MSk9Yy4l10qgVvzGn2AWBx1Ylt0iBexKOAoA6I7JO3m+M9oeovJd3yYENfkUbOeg=="], "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], @@ -1435,13 +1461,13 @@ "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], - "marked": ["marked@17.0.1", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg=="], + "marked": ["marked@17.0.3", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-jt1v2ObpyOKR8p4XaUJVk3YWRJ5n+i4+rjQopxvV32rSndTJXvIzuUdWWIy/1pFQMkQmvTXawzDNqOH/CUmx6A=="], "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="], - "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA=="], + "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="], "mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="], @@ -1479,7 +1505,7 @@ "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], - "mermaid": ["mermaid@11.12.2", "", { "dependencies": { "@braintree/sanitize-url": "^7.1.1", "@iconify/utils": "^3.0.1", "@mermaid-js/parser": "^0.6.3", "@types/d3": "^7.4.3", "cytoscape": "^3.29.3", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.13", "dayjs": "^1.11.18", "dompurify": "^3.2.5", "katex": "^0.16.22", "khroma": "^2.1.0", "lodash-es": "^4.17.21", "marked": "^16.2.1", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0" } }, "sha512-n34QPDPEKmaeCG4WDMGy0OT6PSyxKCfy2pJgShP+Qow2KLrvWjclwbc3yXfSIf4BanqWEhQEpngWwNp/XhZt6w=="], + "mermaid": ["mermaid@11.12.3", "", { "dependencies": { "@braintree/sanitize-url": "^7.1.1", "@iconify/utils": "^3.0.1", "@mermaid-js/parser": "^1.0.0", "@types/d3": "^7.4.3", "cytoscape": "^3.29.3", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.13", "dayjs": "^1.11.18", "dompurify": "^3.2.5", "katex": "^0.16.22", "khroma": "^2.1.0", "lodash-es": "^4.17.23", "marked": "^16.2.1", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0" } }, "sha512-wN5ZSgJQIC+CHJut9xaKWsknLxaFBwCPwPkGTSUYrTiHORWvpT8RxGk849HPnpUAQ+/9BPRqYb80jTpearrHzQ=="], "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="], @@ -1555,13 +1581,13 @@ "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], - "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "minimatch": ["minimatch@3.1.3", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], "mlly": ["mlly@1.8.0", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="], - "motion": ["motion@12.29.2", "", { "dependencies": { "framer-motion": "^12.29.2", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-jMpHdAzEDF1QQ055cB+1lOBLdJ6ialVWl6QQzpJI2OvmHequ7zFVHM2mx0HNAy+Tu4omUlApfC+4vnkX0geEOg=="], + "motion": ["motion@12.34.3", "", { "dependencies": { "framer-motion": "^12.34.3", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-xZIkBGO7v/Uvm+EyaqYd+9IpXu0sZqLywVlGdCFrrMiaO9JI4Kx51mO9KlHSWwll+gZUVY5OJsWgYI5FywJ/tw=="], "motion-dom": ["motion-dom@11.18.1", "", { "dependencies": { "motion-utils": "^11.18.1" } }, "sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw=="], @@ -1569,7 +1595,9 @@ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "msw": ["msw@2.12.7", "", { "dependencies": { "@inquirer/confirm": "^5.0.0", "@mswjs/interceptors": "^0.40.0", "@open-draft/deferred-promise": "^2.2.0", "@types/statuses": "^2.0.6", "cookie": "^1.0.2", "graphql": "^16.12.0", "headers-polyfill": "^4.0.2", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "path-to-regexp": "^6.3.0", "picocolors": "^1.1.1", "rettime": "^0.7.0", "statuses": "^2.0.2", "strict-event-emitter": "^0.5.1", "tough-cookie": "^6.0.0", "type-fest": "^5.2.0", "until-async": "^3.0.2", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": ">= 4.8.x" }, "optionalPeers": ["typescript"], "bin": { "msw": "cli/index.js" } }, "sha512-retd5i3xCZDVWMYjHEVuKTmhqY8lSsxujjVrZiGbbdoxxIBg5S7rCuYy/YQpfrTYIxpd/o0Kyb/3H+1udBMoYg=="], + "msw": ["msw@2.12.10", "", { "dependencies": { "@inquirer/confirm": "^5.0.0", "@mswjs/interceptors": "^0.41.2", "@open-draft/deferred-promise": "^2.2.0", "@types/statuses": "^2.0.6", "cookie": "^1.0.2", "graphql": "^16.12.0", "headers-polyfill": "^4.0.2", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "path-to-regexp": "^6.3.0", "picocolors": "^1.1.1", "rettime": "^0.10.1", "statuses": "^2.0.2", "strict-event-emitter": "^0.5.1", "tough-cookie": "^6.0.0", "type-fest": "^5.2.0", "until-async": "^3.0.2", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": ">= 4.8.x" }, "optionalPeers": ["typescript"], "bin": { "msw": "cli/index.js" } }, "sha512-G3VUymSE0/iegFnuipujpwyTM2GuZAKXNeerUSrG2+Eg391wW63xFs5ixWsK9MWzr1AGoSkYGmyAzNgbR3+urw=="], + + "mustache": ["mustache@4.2.0", "", { "bin": { "mustache": "bin/mustache" } }, "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ=="], "mute-stream": ["mute-stream@2.0.0", "", {}, "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA=="], @@ -1617,10 +1645,16 @@ "outvariant": ["outvariant@1.4.3", "", {}, "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA=="], + "p-finally": ["p-finally@1.0.0", "", {}, "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow=="], + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + "p-queue": ["p-queue@6.6.2", "", { "dependencies": { "eventemitter3": "^4.0.4", "p-timeout": "^3.2.0" } }, "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ=="], + + "p-timeout": ["p-timeout@3.2.0", "", { "dependencies": { "p-finally": "^1.0.0" } }, "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg=="], + "package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="], "pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], @@ -1677,15 +1711,13 @@ "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], - "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], - "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], - "qs": ["qs@6.14.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ=="], + "qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="], "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], @@ -1695,39 +1727,39 @@ "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], - "react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="], + "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="], - "react-day-picker": ["react-day-picker@9.13.0", "", { "dependencies": { "@date-fns/tz": "^1.4.1", "date-fns": "^4.1.0", "date-fns-jalali": "^4.1.0-0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-euzj5Hlq+lOHqI53NiuNhCP8HWgsPf/bBAVijR50hNaY1XwjKjShAnIe8jm8RD2W9IJUvihDIZ+KrmqfFzNhFQ=="], + "react-day-picker": ["react-day-picker@9.13.2", "", { "dependencies": { "@date-fns/tz": "^1.4.1", "date-fns": "^4.1.0", "date-fns-jalali": "^4.1.0-0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-IMPiXfXVIAuR5Yk58DDPBC8QKClrhdXV+Tr/alBrwrHUw0qDDYB1m5zPNuTnnPIr/gmJ4ChMxmtqPdxm8+R4Eg=="], - "react-dom": ["react-dom@19.2.3", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.3" } }, "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg=="], + "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="], - "react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + "react-is": ["react-is@19.2.4", "", {}, "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA=="], "react-markdown": ["react-markdown@10.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ=="], + "react-redux": ["react-redux@9.2.0", "", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "@types/react": "^18.2.25 || ^19", "react": "^18.0 || ^19", "redux": "^5.0.0" }, "optionalPeers": ["@types/react", "redux"] }, "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g=="], + "react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="], "react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="], "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], - "react-resizable-panels": ["react-resizable-panels@4.4.1", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-dpM9oI6rGlAq7VYDeafSRA1JmkJv8aNuKySR+tZLQQLfaeqTnQLSM52EcoI/QdowzsjVUCk6jViKS0xHWITVRQ=="], - - "react-smooth": ["react-smooth@4.0.4", "", { "dependencies": { "fast-equals": "^5.0.1", "prop-types": "^15.8.1", "react-transition-group": "^4.4.5" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q=="], + "react-resizable-panels": ["react-resizable-panels@4.6.5", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-pmQP6qv9KmsesNMvWVNvVfVJAwYSOWWbAOAtrPR8Cre20+j1NWIlyft0btjtDQE+OepXmI6g3VPrCXQY0oD7+Q=="], "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], "react-textarea-autosize": ["react-textarea-autosize@8.5.9", "", { "dependencies": { "@babel/runtime": "^7.20.13", "use-composed-ref": "^1.3.0", "use-latest": "^1.2.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A=="], - "react-transition-group": ["react-transition-group@4.4.5", "", { "dependencies": { "@babel/runtime": "^7.5.5", "dom-helpers": "^5.0.1", "loose-envify": "^1.4.0", "prop-types": "^15.6.2" }, "peerDependencies": { "react": ">=16.6.0", "react-dom": ">=16.6.0" } }, "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g=="], - "readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], "recast": ["recast@0.23.11", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA=="], - "recharts": ["recharts@2.15.4", "", { "dependencies": { "clsx": "^2.0.0", "eventemitter3": "^4.0.1", "lodash": "^4.17.21", "react-is": "^18.3.1", "react-smooth": "^4.0.4", "recharts-scale": "^0.4.4", "tiny-invariant": "^1.3.1", "victory-vendor": "^36.6.8" }, "peerDependencies": { "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw=="], + "recharts": ["recharts@3.7.0", "", { "dependencies": { "@reduxjs/toolkit": "1.x.x || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^10.1.1", "react-redux": "8.x.x || 9.x.x", "reselect": "5.1.1", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-l2VCsy3XXeraxIID9fx23eCb6iCBsxUQDnE8tWm6DFdszVAO7WVY/ChAD9wVit01y6B2PMupYiMmQwhgPHc9Ew=="], - "recharts-scale": ["recharts-scale@0.4.5", "", { "dependencies": { "decimal.js-light": "^2.4.1" } }, "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w=="], + "redux": ["redux@5.0.1", "", {}, "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="], + + "redux-thunk": ["redux-thunk@3.1.0", "", { "peerDependencies": { "redux": "^5.0.0" } }, "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw=="], "regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="], @@ -1735,7 +1767,7 @@ "regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="], - "rehype-harden": ["rehype-harden@1.1.7", "", { "dependencies": { "unist-util-visit": "^5.0.0" } }, "sha512-j5DY0YSK2YavvNGV+qBHma15J9m0WZmRe8posT5AtKDS6TNWtMVTo6RiqF8SidfcASYz8f3k2J/1RWmq5zTXUw=="], + "rehype-harden": ["rehype-harden@1.1.8", "", { "dependencies": { "unist-util-visit": "^5.0.0" } }, "sha512-Qn7vR1xrf6fZCrkm9TDWi/AB4ylrHy+jqsNm1EHOAmbARYA6gsnVJBq/sdBh6kmT4NEZxH5vgIjrscefJAOXcw=="], "rehype-katex": ["rehype-katex@7.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/katex": "^0.16.0", "hast-util-from-html-isomorphic": "^2.0.0", "hast-util-to-text": "^4.0.0", "katex": "^0.16.0", "unist-util-visit-parents": "^6.0.0", "vfile": "^6.0.0" } }, "sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA=="], @@ -1757,7 +1789,7 @@ "remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="], - "remend": ["remend@1.1.0", "", {}, "sha512-JENGyuIhTwzUfCarW43X4r9cehoqTo9QyYxfNDZSud2AmqeuWjZ5pfybasTa4q0dxTJAj5m8NB+wR+YueAFpxQ=="], + "remend": ["remend@1.2.1", "", {}, "sha512-4wC12bgXsfKAjF1ewwkNIQz5sqewz/z1xgIgjEMb3r1pEytQ37F0Cm6i+OhbTWEvguJD7lhOUJhK5fSasw9f0w=="], "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], @@ -1769,13 +1801,13 @@ "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], - "rettime": ["rettime@0.7.0", "", {}, "sha512-LPRKoHnLKd/r3dVxcwO7vhCW+orkOGj9ViueosEBK6ie89CijnfRlhaDhHq/3Hxu4CkWQtxwlBG0mzTQY6uQjw=="], + "rettime": ["rettime@0.10.1", "", {}, "sha512-uyDrIlUEH37cinabq0AX4QbgV4HbFZ/gqoiunWQ1UqBtRvTTytwhNYjE++pO/MjPTZL5KQCf2bEoJ/BJNVQ5Kw=="], "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], "robust-predicates": ["robust-predicates@3.0.2", "", {}, "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="], - "rollup": ["rollup@4.56.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.56.0", "@rollup/rollup-android-arm64": "4.56.0", "@rollup/rollup-darwin-arm64": "4.56.0", "@rollup/rollup-darwin-x64": "4.56.0", "@rollup/rollup-freebsd-arm64": "4.56.0", "@rollup/rollup-freebsd-x64": "4.56.0", "@rollup/rollup-linux-arm-gnueabihf": "4.56.0", "@rollup/rollup-linux-arm-musleabihf": "4.56.0", "@rollup/rollup-linux-arm64-gnu": "4.56.0", "@rollup/rollup-linux-arm64-musl": "4.56.0", "@rollup/rollup-linux-loong64-gnu": "4.56.0", "@rollup/rollup-linux-loong64-musl": "4.56.0", "@rollup/rollup-linux-ppc64-gnu": "4.56.0", "@rollup/rollup-linux-ppc64-musl": "4.56.0", "@rollup/rollup-linux-riscv64-gnu": "4.56.0", "@rollup/rollup-linux-riscv64-musl": "4.56.0", "@rollup/rollup-linux-s390x-gnu": "4.56.0", "@rollup/rollup-linux-x64-gnu": "4.56.0", "@rollup/rollup-linux-x64-musl": "4.56.0", "@rollup/rollup-openbsd-x64": "4.56.0", "@rollup/rollup-openharmony-arm64": "4.56.0", "@rollup/rollup-win32-arm64-msvc": "4.56.0", "@rollup/rollup-win32-ia32-msvc": "4.56.0", "@rollup/rollup-win32-x64-gnu": "4.56.0", "@rollup/rollup-win32-x64-msvc": "4.56.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-9FwVqlgUHzbXtDg9RCMgodF3Ua4Na6Gau+Sdt9vyCN4RhHfVKX2DCHy3BjMLTDd47ITDhYAnTwGulWTblJSDLg=="], + "rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="], "roughjs": ["roughjs@4.6.6", "", { "dependencies": { "hachure-fill": "^0.5.2", "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ=="], @@ -1809,7 +1841,7 @@ "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], - "shadcn": ["shadcn@3.7.0", "", { "dependencies": { "@antfu/ni": "^25.0.0", "@babel/core": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/plugin-transform-typescript": "^7.28.0", "@babel/preset-typescript": "^7.27.1", "@dotenvx/dotenvx": "^1.48.4", "@modelcontextprotocol/sdk": "^1.17.2", "@types/validate-npm-package-name": "^4.0.2", "browserslist": "^4.26.2", "commander": "^14.0.0", "cosmiconfig": "^9.0.0", "dedent": "^1.6.0", "deepmerge": "^4.3.1", "diff": "^8.0.2", "execa": "^9.6.0", "fast-glob": "^3.3.3", "fs-extra": "^11.3.1", "fuzzysort": "^3.1.0", "https-proxy-agent": "^7.0.6", "kleur": "^4.1.5", "msw": "^2.10.4", "node-fetch": "^3.3.2", "open": "^11.0.0", "ora": "^8.2.0", "postcss": "^8.5.6", "postcss-selector-parser": "^7.1.0", "prompts": "^2.4.2", "recast": "^0.23.11", "stringify-object": "^5.0.0", "ts-morph": "^26.0.0", "tsconfig-paths": "^4.2.0", "validate-npm-package-name": "^7.0.1", "zod": "^3.24.1", "zod-to-json-schema": "^3.24.6" }, "bin": { "shadcn": "dist/index.js" } }, "sha512-zOXNAIFclguSYmmoibyXyKiYA6qjEJtXDSvloAMziSREW9Q0R/dLqBUYdb81lOejmZkDYuZApGabbMLH7G8qvQ=="], + "shadcn": ["shadcn@3.8.5", "", { "dependencies": { "@antfu/ni": "^25.0.0", "@babel/core": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/plugin-transform-typescript": "^7.28.0", "@babel/preset-typescript": "^7.27.1", "@dotenvx/dotenvx": "^1.48.4", "@modelcontextprotocol/sdk": "^1.26.0", "@types/validate-npm-package-name": "^4.0.2", "browserslist": "^4.26.2", "commander": "^14.0.0", "cosmiconfig": "^9.0.0", "dedent": "^1.6.0", "deepmerge": "^4.3.1", "diff": "^8.0.2", "execa": "^9.6.0", "fast-glob": "^3.3.3", "fs-extra": "^11.3.1", "fuzzysort": "^3.1.0", "https-proxy-agent": "^7.0.6", "kleur": "^4.1.5", "msw": "^2.10.4", "node-fetch": "^3.3.2", "open": "^11.0.0", "ora": "^8.2.0", "postcss": "^8.5.6", "postcss-selector-parser": "^7.1.0", "prompts": "^2.4.2", "recast": "^0.23.11", "stringify-object": "^5.0.0", "tailwind-merge": "^3.0.1", "ts-morph": "^26.0.0", "tsconfig-paths": "^4.2.0", "validate-npm-package-name": "^7.0.1", "zod": "^3.24.1", "zod-to-json-schema": "^3.24.6" }, "bin": { "shadcn": "dist/index.js" } }, "sha512-jPRx44e+eyeV7xwY3BLJXcfrks00+M0h5BGB9l6DdcBW4BpAj4x3lVmVy0TXPEs2iHEisxejr62sZAAw6B1EVA=="], "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], @@ -1817,7 +1849,7 @@ "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], - "shiki": ["shiki@3.21.0", "", { "dependencies": { "@shikijs/core": "3.21.0", "@shikijs/engine-javascript": "3.21.0", "@shikijs/engine-oniguruma": "3.21.0", "@shikijs/langs": "3.21.0", "@shikijs/themes": "3.21.0", "@shikijs/types": "3.21.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-N65B/3bqL/TI2crrXr+4UivctrAGEjmsib5rPMMPpFp1xAx/w03v8WZ9RDDFYteXoEgY7qZ4HGgl5KBIu1153w=="], + "shiki": ["shiki@3.22.0", "", { "dependencies": { "@shikijs/core": "3.22.0", "@shikijs/engine-javascript": "3.22.0", "@shikijs/engine-oniguruma": "3.22.0", "@shikijs/langs": "3.22.0", "@shikijs/themes": "3.22.0", "@shikijs/types": "3.22.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-LBnhsoYEe0Eou4e1VgJACes+O6S6QC0w71fCSp5Oya79inkwkm15gQ1UF6VtQ8j/taMDh79hAB49WUk8ALQW3g=="], "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], @@ -1829,6 +1861,8 @@ "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + "simple-wcswidth": ["simple-wcswidth@1.1.2", "", {}, "sha512-j7piyCjAeTDSjzTSQ7DokZtMNwNlEAyxqSZeCS+CXH7fJ4jx3FuJ/mTW3mE+6JLs4VJBbcll0Kjn+KXI5t21Iw=="], + "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], "sonner": ["sonner@2.0.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="], @@ -1845,7 +1879,7 @@ "stdin-discarder": ["stdin-discarder@0.2.2", "", {}, "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ=="], - "streamdown": ["streamdown@2.1.0", "", { "dependencies": { "clsx": "^2.1.1", "hast-util-to-jsx-runtime": "^2.3.6", "html-url-attributes": "^3.0.1", "marked": "^17.0.1", "rehype-harden": "^1.1.7", "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remend": "1.1.0", "tailwind-merge": "^3.4.0", "unified": "^11.0.5", "unist-util-visit": "^5.0.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-u9gWd0AmjKg1d+74P44XaPlGrMeC21oDOSIhjGNEYMAttDMzCzlJO6lpTyJ9JkSinQQF65YcK4eOd3q9iTvULw=="], + "streamdown": ["streamdown@2.3.0", "", { "dependencies": { "clsx": "^2.1.1", "hast-util-to-jsx-runtime": "^2.3.6", "html-url-attributes": "^3.0.1", "marked": "^17.0.1", "rehype-harden": "^1.1.8", "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remend": "1.2.1", "tailwind-merge": "^3.4.0", "unified": "^11.0.5", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-OqS3by/lt91lSicE8RQP2nTsYI6Q/dQgGP2vcyn9YesCmRHhNjswAuBAZA1z0F4+oBU3II/eV51LqjCqwTb1lw=="], "strict-event-emitter": ["strict-event-emitter@0.5.1", "", {}, "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ=="], @@ -1879,9 +1913,9 @@ "tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="], - "tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="], + "tailwind-merge": ["tailwind-merge@3.5.0", "", {}, "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A=="], - "tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="], + "tailwindcss": ["tailwindcss@4.2.1", "", {}, "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw=="], "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], @@ -1893,9 +1927,9 @@ "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], - "tldts": ["tldts@7.0.19", "", { "dependencies": { "tldts-core": "^7.0.19" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA=="], + "tldts": ["tldts@7.0.23", "", { "dependencies": { "tldts-core": "^7.0.23" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-ASdhgQIBSay0R/eXggAkQ53G4nTJqTXqC2kbaBbdDwM7SkjyZyO0OaaN1/FH7U/yCeqOHDwFO5j8+Os/IS1dXw=="], - "tldts-core": ["tldts-core@7.0.19", "", {}, "sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A=="], + "tldts-core": ["tldts-core@7.0.23", "", {}, "sha512-0g9vrtDQLrNIiCj22HSe9d4mLVG3g5ph5DZ8zCKBr4OtrspmNB6ss7hVyzArAeE88ceZocIEGkyW1Ime7fxPtQ=="], "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], @@ -1919,21 +1953,21 @@ "tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="], - "tw-shimmer": ["tw-shimmer@0.4.4", "", { "peerDependencies": { "tailwindcss": ">=4.0.0-0" } }, "sha512-uSt6nWbt7k3Xuzv8/vKRiyf57Jcu/rQOcSxcvJvuAY8+DxPFKMEM6z03X3N10EYAmzCIFIMsHmV5xrmTDuQrtA=="], + "tw-shimmer": ["tw-shimmer@0.4.6", "", { "peerDependencies": { "tailwindcss": ">=4.0.0-0" } }, "sha512-Wg3Qy9bcIHw6v2hqFzsvBiuIVHey2HyjDPYY/ozkDCWDYNPirxs1GoIs8FCrNtc0YTb+/wuSySAB7DjbTY6uGw=="], "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], - "type-fest": ["type-fest@5.4.1", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-xygQcmneDyzsEuKZrFbRMne5HDqMs++aFzefrJTgEIKjQ3rekM+RPfFCVq2Gp1VIDqddoYeppCj4Pcb+RZW0GQ=="], + "type-fest": ["type-fest@5.4.4", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw=="], "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - "typescript-eslint": ["typescript-eslint@8.53.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.53.1", "@typescript-eslint/parser": "8.53.1", "@typescript-eslint/typescript-estree": "8.53.1", "@typescript-eslint/utils": "8.53.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-gB+EVQfP5RDElh9ittfXlhZJdjSU4jUSTyE2+ia8CYyNvet4ElfaLlAIqDvQV9JPknKx0jQH1racTYe/4LaLSg=="], + "typescript-eslint": ["typescript-eslint@8.56.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.56.1", "@typescript-eslint/parser": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/utils": "8.56.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ=="], "ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="], - "underscore": ["underscore@1.13.7", "", {}, "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g=="], + "underscore": ["underscore@1.13.8", "", {}, "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ=="], "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], @@ -1983,7 +2017,7 @@ "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], - "uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], + "uuid": ["uuid@10.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="], "validate-npm-package-name": ["validate-npm-package-name@7.0.2", "", {}, "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A=="], @@ -1995,7 +2029,7 @@ "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], - "victory-vendor": ["victory-vendor@36.9.2", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ=="], + "victory-vendor": ["victory-vendor@37.3.6", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ=="], "vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], @@ -2009,7 +2043,7 @@ "vscode-languageserver-types": ["vscode-languageserver-types@3.17.5", "", {}, "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="], - "vscode-uri": ["vscode-uri@3.0.8", "", {}, "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw=="], + "vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="], "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], @@ -2041,19 +2075,17 @@ "yoctocolors-cjs": ["yoctocolors-cjs@2.1.3", "", {}, "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw=="], - "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="], "zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="], - "zustand": ["zustand@5.0.10", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-U1AiltS1O9hSy3rul+Ub82ut2fqIAefiSuwECWt6jlMVUGejvf+5omLcRBSzqbRagSM3hQZbtzdeRc6QVScXTg=="], + "zustand": ["zustand@5.0.11", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg=="], "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], - "@chevrotain/cst-dts-gen/lodash-es": ["lodash-es@4.17.21", "", {}, "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="], - - "@chevrotain/gast/lodash-es": ["lodash-es@4.17.21", "", {}, "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="], + "@assistant-ui/react/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], "@dotenvx/dotenvx/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], @@ -2065,7 +2097,7 @@ "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], - "@modelcontextprotocol/sdk/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + "@modelcontextprotocol/sdk/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], "@radix-ui/react-accordion/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], @@ -2219,6 +2251,8 @@ "@radix-ui/react-visually-hidden/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + "@reduxjs/toolkit/immer": ["immer@11.1.4", "", {}, "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], @@ -2231,21 +2265,23 @@ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "@toolwind/corner-shape/@types/node": ["@types/node@20.19.30", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g=="], + "@toolwind/corner-shape/@types/node": ["@types/node@20.19.33", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw=="], - "@ts-morph/common/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="], + "@ts-morph/common/minimatch": ["minimatch@10.2.2", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw=="], "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], - "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.2", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw=="], - "@typescript-eslint/typescript-estree/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "@typescript-eslint/typescript-estree/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], "@xyflow/react/zustand": ["zustand@4.5.7", "", { "dependencies": { "use-sync-external-store": "^1.2.2" }, "peerDependencies": { "@types/react": ">=16.8", "immer": ">=9.0.6", "react": ">=16.8" }, "optionalPeers": ["@types/react", "immer", "react"] }, "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw=="], - "ajv-formats/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + "ajv-formats/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], - "chevrotain/lodash-es": ["lodash-es@4.17.21", "", {}, "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="], + "chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], @@ -2267,19 +2303,25 @@ "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - "js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "langsmith/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + + "langsmith/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], "log-symbols/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], "log-symbols/is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="], + "mammoth/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], "mermaid/marked": ["marked@16.4.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="], + "mermaid/uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], + "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - "motion/framer-motion": ["framer-motion@12.29.2", "", { "dependencies": { "motion-dom": "^12.29.2", "motion-utils": "^12.29.2", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-lSNRzBJk4wuIy0emYQ/nfZ7eWhqud2umPKw2QAQki6uKhZPKm2hRQHeQoHTG9MIvfobb+A/LbEWPJU794ZUKrg=="], + "motion/framer-motion": ["framer-motion@12.34.3", "", { "dependencies": { "motion-dom": "^12.34.3", "motion-utils": "^12.29.2", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-v81ecyZKYO/DfpTwHivqkxSUBzvceOpoI+wLfgCgoUIKxlFKEXdg0oR9imxwXumT4SFy8vRk9xzJ5l3/Du/55Q=="], "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], @@ -2289,14 +2331,14 @@ "ora/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + "p-queue/eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="], + "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], "postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], "prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], - "prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], - "radix-ui/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], "radix-ui/@radix-ui/react-label": ["@radix-ui/react-label@2.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ=="], @@ -2311,14 +2353,14 @@ "router/path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], - "shadcn/commander": ["commander@14.0.2", "", {}, "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ=="], + "shadcn/commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], - "shadcn/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - - "sharp/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "sharp/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], "string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "@dotenvx/dotenvx/execa/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], @@ -2333,7 +2375,7 @@ "@dotenvx/dotenvx/execa/strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="], - "@dotenvx/dotenvx/which/isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="], + "@dotenvx/dotenvx/which/isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="], "@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], @@ -2399,12 +2441,16 @@ "@toolwind/corner-shape/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + "@ts-morph/common/minimatch/brace-expansion": ["brace-expansion@5.0.3", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA=="], + + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.3", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA=="], "ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], "cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "cmdk/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], "cytoscape-fcose/cose-base/layout-base": ["layout-base@2.0.1", "", {}, "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg=="], @@ -2413,7 +2459,7 @@ "d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="], - "motion/framer-motion/motion-dom": ["motion-dom@12.29.2", "", { "dependencies": { "motion-utils": "^12.29.2" } }, "sha512-/k+NuycVV8pykxyiTCoFzIVLA95Nb1BFIVvfSu9L50/6K6qNeAYtkxXILy/LRutt7AzaYDc2myj0wkCVVYAPPA=="], + "motion/framer-motion/motion-dom": ["motion-dom@12.34.3", "", { "dependencies": { "motion-utils": "^12.29.2" } }, "sha512-sYgFe+pR9aIM7o4fhs2aXtOI+oqlUd33N9Yoxcgo1Fv7M20sRkHtCmzE/VRNIcq7uNJ+qio+Xubt1FXH3pQ+eQ=="], "motion/framer-motion/motion-utils": ["motion-utils@12.29.2", "", {}, "sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A=="], @@ -2424,5 +2470,9 @@ "string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "@ts-morph/common/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + + "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], } } diff --git a/studio/frontend/data-designer.openapi (1).yaml b/studio/frontend/data-designer.openapi (1).yaml new file mode 100644 index 0000000000..5d4bb29b17 --- /dev/null +++ b/studio/frontend/data-designer.openapi (1).yaml @@ -0,0 +1,2644 @@ +openapi: 3.1.0 +info: + title: NeMo Data Designer Microservice + description: Service for generating synthetic data. + version: 1.5.0 +paths: + /v1/data-designer/jobs: + post: + tags: + - Data Designer + summary: Create Job + operationId: create_job_v1_data_designer_jobs_post + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DataDesignerJobRequest' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/DataDesignerJob' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + get: + tags: + - Data Designer + summary: List Jobs + operationId: list_jobs_v1_data_designer_jobs_get + parameters: + - name: page + in: query + required: false + schema: + type: integer + exclusiveMinimum: 0 + description: Page number. + default: 1 + title: Page + description: Page number. + - name: page_size + in: query + required: false + schema: + type: integer + exclusiveMinimum: 0 + description: Page size. + default: 10 + title: Page Size + description: Page size. + - name: sort + in: query + required: false + schema: + allOf: + - $ref: '#/components/schemas/DataDesignerJobsSortField' + description: The field to sort by. To sort in decreasing order, use `-` + in front of the field name. + default: -created_at + description: The field to sort by. To sort in decreasing order, use `-` in + front of the field name. + - in: query + name: filter + style: deepObject + required: false + explode: true + schema: + $ref: '#/components/schemas/DataDesignerJobsListFilter' + description: Filter jobs on various criteria. + - in: query + name: search + style: deepObject + required: false + explode: true + schema: + $ref: '#/components/schemas/DataDesignerJobsSearch' + description: "\nSearch jobs using substring matching.\nYou can combine multiple\ + \ search fields and filters.\n\nFor example:\n- `?search[name]=training`:\ + \ searches all jobs with 'training' in the name.\n- `?search[project]=my-project`:\ + \ searches all jobs with 'my-project'\n in the project field.\n- `?search[name]=training&search[name]=eval`:\ + \ searches all jobs with\n 'training' OR 'eval' in the name.\n- `?search[name]=training&search[project]=my-project`:\ + \ searches all\n jobs with 'training' in the name AND 'my-project' in the\ + \ project.\n" + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/DataDesignerJobsPage' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /v1/data-designer/jobs/{job_id}: + get: + tags: + - Data Designer + summary: Get Job + operationId: get_job_v1_data_designer_jobs__job_id__get + parameters: + - name: job_id + in: path + required: true + schema: + type: string + title: Job Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/DataDesignerJob' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + delete: + tags: + - Data Designer + summary: Delete Job + operationId: delete_job_v1_data_designer_jobs__job_id__delete + parameters: + - name: job_id + in: path + required: true + schema: + type: string + title: Job Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /v1/data-designer/jobs/{job_id}/cancel: + post: + tags: + - Data Designer + summary: Cancel Job + operationId: cancel_job_v1_data_designer_jobs__job_id__cancel_post + parameters: + - name: job_id + in: path + required: true + schema: + type: string + title: Job Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/DataDesignerJob' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /v1/data-designer/jobs/{job_id}/logs: + get: + tags: + - Data Designer + summary: Get Job Logs + operationId: get_job_logs_v1_data_designer_jobs__job_id__logs_get + parameters: + - name: job_id + in: path + required: true + schema: + type: string + title: Job Id + - name: limit + in: query + required: false + schema: + anyOf: + - type: integer + - type: 'null' + title: Limit + - name: page_cursor + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: Page Cursor + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/PlatformJobLogPage' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /v1/data-designer/jobs/{job_id}/results: + get: + tags: + - Data Designer + summary: List Job Results + operationId: list_job_results_v1_data_designer_jobs__job_id__results_get + parameters: + - name: job_id + in: path + required: true + schema: + type: string + title: Job Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/PlatformJobListResultResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /v1/data-designer/jobs/{job_id}/results/analysis/download: + get: + tags: + - Data Designer + summary: Download Job Result Analysis + operationId: download_job_result_analysis_v1_data_designer_jobs__job_id__results_analysis_download_get + parameters: + - name: job_id + in: path + required: true + schema: + type: string + title: Job Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '404': + description: Not Found + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /v1/data-designer/jobs/{job_id}/results/dataset/download: + get: + tags: + - Data Designer + summary: Download Job Result Dataset + operationId: download_job_result_dataset_v1_data_designer_jobs__job_id__results_dataset_download_get + parameters: + - name: job_id + in: path + required: true + schema: + type: string + title: Job Id + responses: + '200': + description: Successful Response + content: + application/octet-stream: + schema: + type: string + format: binary + '404': + description: Not Found + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /v1/data-designer/jobs/{job_id}/results/{result_name}: + get: + tags: + - Data Designer + summary: Get Job Result + operationId: get_job_result_v1_data_designer_jobs__job_id__results__result_name__get + parameters: + - name: job_id + in: path + required: true + schema: + type: string + title: Job Id + - name: result_name + in: path + required: true + schema: + type: string + title: Result Name + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/PlatformJobResultResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /v1/data-designer/jobs/{job_id}/results/{result_name}/download: + get: + tags: + - Data Designer + summary: Download Job Result + operationId: download_job_result_v1_data_designer_jobs__job_id__results__result_name__download_get + parameters: + - name: job_id + in: path + required: true + schema: + type: string + title: Job Id + - name: result_name + in: path + required: true + schema: + type: string + title: Result Name + responses: + '200': + description: Successful Response + content: + application/octet-stream: + schema: + type: string + format: binary + '404': + description: Not Found + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /v1/data-designer/jobs/{job_id}/status: + get: + tags: + - Data Designer + summary: Get Job Status + operationId: get_job_status_v1_data_designer_jobs__job_id__status_get + parameters: + - name: job_id + in: path + required: true + schema: + type: string + title: Job Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/PlatformJobStatusResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /v1/data-designer/preview: + post: + tags: + - Data Designer + summary: Generate preview Data Designer + operationId: preview_v1_data_designer_preview_post + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PreviewRequest' + required: true + responses: + '200': + description: Successful Response + content: + application/jsonl: + schema: + $ref: '#/components/schemas/PreviewMessage' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /v1/data-designer/settings: + get: + tags: + - Data Designer + summary: Get Data Designer settings + description: Returns the settings available for Data Designer. + operationId: get_settings_v1_data_designer_settings_get + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/SettingsResponse' +components: + schemas: + BernoulliMixtureSamplerParams: + properties: + p: + type: number + maximum: 1.0 + minimum: 0.0 + title: P + description: Bernoulli distribution probability of success. + dist_name: + type: string + title: Dist Name + description: Mixture distribution name. Samples will be equal to the distribution + sample with probability `p`, otherwise equal to 0. Must be a valid scipy.stats + distribution name. + dist_params: + additionalProperties: true + type: object + title: Dist Params + description: Parameters of the scipy.stats distribution given in `dist_name`. + sampler_type: + type: string + const: bernoulli_mixture + title: Sampler Type + default: bernoulli_mixture + additionalProperties: false + type: object + required: + - p + - dist_name + - dist_params + title: BernoulliMixtureSamplerParams + description: "Parameters for sampling from a Bernoulli mixture distribution.\n\ + \nCombines a Bernoulli distribution with another continuous distribution,\ + \ creating a mixture\nwhere values are either 0 (with probability 1-p) or\ + \ sampled from the specified distribution\n(with probability p). This is useful\ + \ for modeling scenarios with many zero values mixed with\na continuous distribution\ + \ of non-zero values.\n\nCommon use cases include modeling sparse events,\ + \ zero-inflated data, or situations where\nan outcome either doesn't occur\ + \ (0) or follows a specific distribution when it does occur.\n\nAttributes:\n\ + \ p: Probability of sampling from the mixture distribution (non-zero outcome).\n\ + \ Must be between 0.0 and 1.0 (inclusive). With probability 1-p, the\ + \ sample is 0.\n dist_name: Name of the scipy.stats distribution to sample\ + \ from when outcome is non-zero.\n Must be a valid scipy.stats distribution\ + \ name (e.g., \"norm\", \"gamma\", \"expon\").\n dist_params: Parameters\ + \ for the specified scipy.stats distribution." + BernoulliSamplerParams: + properties: + p: + type: number + maximum: 1.0 + minimum: 0.0 + title: P + description: Probability of success. + sampler_type: + type: string + const: bernoulli + title: Sampler Type + default: bernoulli + additionalProperties: false + type: object + required: + - p + title: BernoulliSamplerParams + description: "Parameters for sampling from a Bernoulli distribution.\n\nSamples\ + \ binary values (0 or 1) representing the outcome of a single trial with a\ + \ fixed\nprobability of success. This is the simplest discrete probability\ + \ distribution, useful for\nmodeling binary outcomes like success/failure,\ + \ yes/no, or true/false.\n\nAttributes:\n p: Probability of success (sampling\ + \ 1). Must be between 0.0 and 1.0 (inclusive).\n The probability of\ + \ failure (sampling 0) is automatically 1 - p." + BinomialSamplerParams: + properties: + n: + type: integer + title: N + description: Number of trials. + p: + type: number + maximum: 1.0 + minimum: 0.0 + title: P + description: Probability of success on each trial. + sampler_type: + type: string + const: binomial + title: Sampler Type + default: binomial + additionalProperties: false + type: object + required: + - n + - p + title: BinomialSamplerParams + description: "Parameters for sampling from a Binomial distribution.\n\nSamples\ + \ integer values representing the number of successes in a fixed number of\ + \ independent\nBernoulli trials, each with the same probability of success.\ + \ Commonly used to model the number\nof successful outcomes in repeated experiments.\n\ + \nAttributes:\n n: Number of independent trials. Must be a positive integer.\n\ + \ p: Probability of success on each trial. Must be between 0.0 and 1.0\ + \ (inclusive)." + BuildStage: + type: string + enum: + - pre_batch + - post_batch + - pre_generation + - post_generation + title: BuildStage + CategorySamplerParams: + properties: + values: + items: + anyOf: + - type: string + - type: integer + - type: number + type: array + minItems: 1 + title: Values + description: List of possible categorical values that can be sampled from. + weights: + type: array + items: + type: number + title: Weights + description: List of unnormalized probability weights to assigned to each + value, in order. Larger values will be sampled with higher probability. + sampler_type: + type: string + const: category + title: Sampler Type + default: category + additionalProperties: false + type: object + required: + - values + title: CategorySamplerParams + description: "Parameters for categorical sampling with optional probability\ + \ weighting.\n\nSamples values from a discrete set of categories. When weights\ + \ are provided, values are\nsampled according to their assigned probabilities.\ + \ Without weights, uniform sampling is used.\n\nAttributes:\n values: List\ + \ of possible categorical values to sample from. Can contain strings, integers,\n\ + \ or floats. Must contain at least one value.\n weights: Optional\ + \ unnormalized probability weights for each value. If provided, must be\n\ + \ the same length as `values`. Weights are automatically normalized\ + \ to sum to 1.0.\n Larger weights result in higher sampling probability\ + \ for the corresponding value." + CodeLang: + type: string + enum: + - go + - javascript + - java + - kotlin + - python + - ruby + - rust + - scala + - swift + - typescript + - sql:sqlite + - sql:tsql + - sql:bigquery + - sql:mysql + - sql:postgres + - sql:ansi + title: CodeLang + CodeValidatorParams: + properties: + code_lang: + allOf: + - $ref: '#/components/schemas/CodeLang' + description: The language of the code to validate + additionalProperties: false + type: object + required: + - code_lang + title: CodeValidatorParams + description: "Configuration for code validation. Supports Python and SQL code\ + \ validation.\n\nAttributes:\n code_lang: The language of the code to validate.\ + \ Supported values include: `python`,\n `sql:sqlite`, `sql:postgres`,\ + \ `sql:mysql`, `sql:tsql`, `sql:bigquery`, `sql:ansi`." + ColumnInequalityConstraint: + properties: + target_column: + type: string + title: Target Column + rhs: + type: string + title: Rhs + operator: + $ref: '#/components/schemas/InequalityOperator' + additionalProperties: false + type: object + required: + - target_column + - rhs + - operator + title: ColumnInequalityConstraint + DataDesignerConfig: + properties: + columns: + items: + oneOf: + - $ref: '#/components/schemas/ExpressionColumnConfig' + - $ref: '#/components/schemas/LLMCodeColumnConfig' + - $ref: '#/components/schemas/LLMJudgeColumnConfig' + - $ref: '#/components/schemas/LLMStructuredColumnConfig' + - $ref: '#/components/schemas/LLMTextColumnConfig' + - $ref: '#/components/schemas/SamplerColumnConfig' + - $ref: '#/components/schemas/SeedDatasetColumnConfig' + - $ref: '#/components/schemas/ValidationColumnConfig' + discriminator: + propertyName: column_type + mapping: + expression: '#/components/schemas/ExpressionColumnConfig' + llm-code: '#/components/schemas/LLMCodeColumnConfig-Input' + llm-judge: '#/components/schemas/LLMJudgeColumnConfig-Input' + llm-structured: '#/components/schemas/LLMStructuredColumnConfig-Input' + llm-text: '#/components/schemas/LLMTextColumnConfig-Input' + sampler: '#/components/schemas/SamplerColumnConfig' + seed-dataset: '#/components/schemas/SeedDatasetColumnConfig' + validation: '#/components/schemas/ValidationColumnConfig-Input' + type: array + minItems: 1 + title: Columns + model_configs: + type: array + items: + $ref: '#/components/schemas/ModelConfigInput' + title: Model Configs + seed_config: + $ref: '#/components/schemas/SeedConfig' + constraints: + type: array + items: + anyOf: + - $ref: '#/components/schemas/ScalarInequalityConstraint' + - $ref: '#/components/schemas/ColumnInequalityConstraint' + title: Constraints + profilers: + type: array + items: + $ref: '#/components/schemas/JudgeScoreProfilerConfig' + title: Profilers + processors: + type: array + items: + $ref: '#/components/schemas/ProcessorConfig' + title: Processors + additionalProperties: false + type: object + required: + - columns + title: DataDesignerConfig + description: "Configuration for NeMo Data Designer.\n\nThis class defines the\ + \ main configuration structure for NeMo Data Designer,\nwhich orchestrates\ + \ the generation of synthetic data.\n\nAttributes:\n columns: Required\ + \ list of column configurations defining how each column\n should be\ + \ generated. Must contain at least one column.\n model_configs: Optional\ + \ list of model configurations for LLM-based generation.\n Each model\ + \ config defines the model, provider, and inference parameters.\n seed_config:\ + \ Optional seed dataset settings to use for generation.\n constraints:\ + \ Optional list of column constraints.\n profilers: Optional list of column\ + \ profilers for analyzing generated data characteristics." + DataDesignerJob: + properties: + id: + type: string + title: Id + name: + type: string + title: Name + description: + type: string + title: Description + project: + type: string + title: Project + namespace: + type: string + title: Namespace + created_at: + type: string + title: Created At + updated_at: + type: string + title: Updated At + spec: + $ref: '#/components/schemas/DataDesignerJobConfig' + status: + $ref: '#/components/schemas/PlatformJobStatus' + status_details: + type: object + additionalProperties: true + title: Status Details + error_details: + type: object + additionalProperties: true + title: Error Details + ownership: + type: object + additionalProperties: true + title: Ownership + custom_fields: + type: object + additionalProperties: true + title: Custom Fields + type: object + required: + - name + - spec + title: DataDesignerJob + DataDesignerJobConfig: + properties: + num_records: + type: integer + title: Num Records + config: + $ref: '#/components/schemas/DataDesignerConfig' + type: object + required: + - num_records + - config + title: DataDesignerJobConfig + DataDesignerJobRequest: + properties: + name: + type: string + title: Name + description: + type: string + title: Description + namespace: + type: string + title: Namespace + project: + type: string + title: Project + spec: + $ref: '#/components/schemas/DataDesignerJobConfig' + ownership: + type: object + additionalProperties: true + title: Ownership + custom_fields: + type: object + additionalProperties: true + title: Custom Fields + type: object + required: + - spec + title: DataDesignerJobRequest + DataDesignerJobsListFilter: + properties: + created_at: + allOf: + - $ref: '#/components/schemas/DatetimeFilter' + description: Jobs created at 'gte' datetime or 'lte' datetime. + name: + type: string + title: Name + description: Name of the job. + namespace: + type: string + title: Namespace + description: Namespace of the job. + project: + type: string + title: Project + description: Project containing the job. + status: + allOf: + - $ref: '#/components/schemas/PlatformJobStatus' + description: The current status. + updated_at: + allOf: + - $ref: '#/components/schemas/DatetimeFilter' + description: Jobs updated at 'gte' datetime or 'lte' datetime. + additionalProperties: false + type: object + title: DataDesignerJobsListFilter + DataDesignerJobsPage: + properties: + object: + type: string + title: Object + description: The type of object being returned. + default: list + data: + items: + $ref: '#/components/schemas/DataDesignerJob' + type: array + title: Data + pagination: + allOf: + - $ref: '#/components/schemas/PaginationData' + description: Pagination information. + sort: + type: string + title: Sort + description: The field on which the results are sorted. + filter: + allOf: + - $ref: '#/components/schemas/DataDesignerJobsListFilter' + description: Filtering information. + search: + allOf: + - $ref: '#/components/schemas/DataDesignerJobsSearch' + description: Search information. + type: object + required: + - data + title: DataDesignerJobsPage + DataDesignerJobsSearch: + properties: + name: + type: array + items: + type: string + title: Name + description: Search jobs where name contains any of these strings. + project: + type: array + items: + type: string + title: Project + description: Search jobs where project contains any of these strings. + type: object + title: DataDesignerJobsSearch + DataDesignerJobsSortField: + type: string + enum: + - created_at + - -created_at + - updated_at + - -updated_at + title: DataDesignerJobsSortField + DatetimeFilter: + properties: + gte: + type: string + title: Gte + description: Filter for results greater than or equal to this datetime. + lte: + type: string + title: Lte + description: Filter for results less than or equal to this datetime. + additionalProperties: false + type: object + title: DatetimeFilter + DatetimeSamplerParams: + properties: + start: + type: string + title: Start + description: Earliest possible datetime for sampling range, inclusive. + end: + type: string + title: End + description: Latest possible datetime for sampling range, inclusive. + unit: + type: string + enum: + - Y + - M + - D + - h + - m + - s + title: Unit + description: Sampling units, e.g. the smallest possible time interval between + samples. + default: D + sampler_type: + type: string + const: datetime + title: Sampler Type + default: datetime + additionalProperties: false + type: object + required: + - start + - end + title: DatetimeSamplerParams + description: "Parameters for uniform datetime sampling within a specified range.\n\ + \nSamples datetime values uniformly between a start and end date with a specified\ + \ granularity.\nThe sampling unit determines the smallest possible time interval\ + \ between consecutive samples.\n\nAttributes:\n start: Earliest possible\ + \ datetime for the sampling range (inclusive). Must be a valid\n datetime\ + \ string parseable by pandas.to_datetime().\n end: Latest possible datetime\ + \ for the sampling range (inclusive). Must be a valid\n datetime string\ + \ parseable by pandas.to_datetime().\n unit: Time unit for sampling granularity.\ + \ Options:\n - \"Y\": Years\n - \"M\": Months\n - \"\ + D\": Days (default)\n - \"h\": Hours\n - \"m\": Minutes\n \ + \ - \"s\": Seconds" + DisplayModelProvider: + properties: + name: + type: string + title: Name + provider_type: + type: string + title: Provider Type + default: openai + extra_body: + type: object + additionalProperties: true + title: Extra Body + allowed_models: + type: array + items: + type: string + title: Allowed Models + additionalProperties: false + type: object + required: + - name + title: DisplayModelProvider + DistributionType: + type: string + enum: + - uniform + - manual + title: DistributionType + ExpressionColumnConfig: + properties: + name: + type: string + title: Name + drop: + type: boolean + title: Drop + default: false + column_type: + type: string + const: expression + title: Column Type + default: expression + expr: + type: string + title: Expr + dtype: + type: string + enum: + - int + - float + - str + - bool + title: Dtype + default: str + additionalProperties: false + type: object + required: + - name + - expr + title: ExpressionColumnConfig + description: "Configuration for derived columns using Jinja2 expressions.\n\n\ + Expression columns compute values by evaluating Jinja2 templates that reference\ + \ other\ncolumns. Useful for transformations, concatenations, conditional\ + \ logic, and derived\nfeatures without requiring LLM generation. The expression\ + \ is evaluated row-by-row.\n\nAttributes:\n expr: Jinja2 expression to\ + \ evaluate. Can reference other column values using\n {{ column_name\ + \ }} syntax. Supports filters, conditionals, and arithmetic.\n Must\ + \ be a valid, non-empty Jinja2 template.\n dtype: Data type to cast the\ + \ result to. Must be one of \"int\", \"float\", \"str\", or \"bool\".\n \ + \ Defaults to \"str\". Type conversion is applied after expression evaluation.\n\ + \ column_type: Discriminator field, always \"expression\" for this configuration\ + \ type." + FileStorageType: + type: string + enum: + - nds + title: FileStorageType + GaussianSamplerParams: + properties: + mean: + type: number + title: Mean + description: Mean of the Gaussian distribution + stddev: + type: number + title: Stddev + description: Standard deviation of the Gaussian distribution + decimal_places: + type: integer + title: Decimal Places + description: Number of decimal places to round the sampled values to. + sampler_type: + type: string + const: gaussian + title: Sampler Type + default: gaussian + additionalProperties: false + type: object + required: + - mean + - stddev + title: GaussianSamplerParams + description: "Parameters for sampling from a Gaussian (Normal) distribution.\n\ + \nSamples continuous values from a normal distribution characterized by its\ + \ mean and standard\ndeviation. The Gaussian distribution is one of the most\ + \ commonly used probability distributions,\nappearing naturally in many real-world\ + \ phenomena due to the Central Limit Theorem.\n\nAttributes:\n mean: Mean\ + \ (center) of the Gaussian distribution. This is the expected value and the\n\ + \ location of the distribution's peak.\n stddev: Standard deviation\ + \ of the Gaussian distribution. Controls the spread or width\n of the\ + \ distribution. Must be positive.\n decimal_places: Optional number of\ + \ decimal places to round sampled values to. If None,\n values are\ + \ not rounded." + HTTPValidationError: + properties: + detail: + items: + $ref: '#/components/schemas/ValidationError' + type: array + title: Detail + type: object + title: HTTPValidationError + ImageContext: + properties: + modality: + allOf: + - $ref: '#/components/schemas/Modality' + default: image + column_name: + type: string + title: Column Name + data_type: + $ref: '#/components/schemas/ModalityDataType' + image_format: + $ref: '#/components/schemas/ImageFormat' + type: object + required: + - column_name + - data_type + title: ImageContext + ImageFormat: + type: string + enum: + - png + - jpg + - jpeg + - gif + - webp + title: ImageFormat + IndexRange: + properties: + start: + type: integer + minimum: 0.0 + title: Start + description: The start index of the index range (inclusive) + end: + type: integer + minimum: 0.0 + title: End + description: The end index of the index range (inclusive) + additionalProperties: false + type: object + required: + - start + - end + title: IndexRange + InequalityOperator: + type: string + enum: + - lt + - le + - gt + - ge + title: InequalityOperator + InferenceParametersInput: + properties: + temperature: + anyOf: + - type: number + - $ref: '#/components/schemas/UniformDistribution' + - $ref: '#/components/schemas/ManualDistribution' + - type: 'null' + title: Temperature + top_p: + anyOf: + - type: number + - $ref: '#/components/schemas/UniformDistribution' + - $ref: '#/components/schemas/ManualDistribution' + - type: 'null' + title: Top P + max_tokens: + type: integer + title: Max Tokens + max_parallel_requests: + type: integer + minimum: 1.0 + title: Max Parallel Requests + default: 4 + timeout: + type: integer + title: Timeout + extra_body: + type: object + additionalProperties: true + title: Extra Body + additionalProperties: false + type: object + title: InferenceParametersInput + InferenceParametersOutput: + properties: + temperature: + anyOf: + - type: number + - $ref: '#/components/schemas/UniformDistribution' + - $ref: '#/components/schemas/ManualDistribution' + - type: 'null' + title: Temperature + top_p: + anyOf: + - type: number + - $ref: '#/components/schemas/UniformDistribution' + - $ref: '#/components/schemas/ManualDistribution' + - type: 'null' + title: Top P + max_tokens: + type: integer + title: Max Tokens + max_parallel_requests: + type: integer + minimum: 1.0 + title: Max Parallel Requests + default: 4 + timeout: + type: integer + title: Timeout + extra_body: + type: object + additionalProperties: true + title: Extra Body + additionalProperties: false + type: object + title: InferenceParametersOutput + JudgeScoreProfilerConfig: + properties: + model_alias: + type: string + title: Model Alias + summary_score_sample_size: + type: integer + title: Summary Score Sample Size + default: 20 + additionalProperties: false + type: object + required: + - model_alias + title: JudgeScoreProfilerConfig + LLMCodeColumnConfig: + properties: + name: + type: string + title: Name + drop: + type: boolean + title: Drop + default: false + column_type: + type: string + const: llm-code + title: Column Type + default: llm-code + prompt: + type: string + title: Prompt + model_alias: + type: string + title: Model Alias + system_prompt: + type: string + title: System Prompt + multi_modal_context: + type: array + items: + $ref: '#/components/schemas/ImageContext' + title: Multi Modal Context + code_lang: + $ref: '#/components/schemas/CodeLang' + additionalProperties: false + type: object + required: + - name + - prompt + - model_alias + - code_lang + title: LLMCodeColumnConfig + description: "Configuration for code generation columns using Large Language\ + \ Models.\n\nExtends LLMTextColumnConfig to generate code snippets in specific\ + \ programming languages\nor SQL dialects. The generated code is automatically\ + \ extracted from markdown code blocks\nfor the specified language. Inherits\ + \ all prompt templating capabilities.\n\nAttributes:\n code_lang: Programming\ + \ language or SQL dialect for code generation. Supported\n values include:\ + \ \"python\", \"javascript\", \"typescript\", \"java\", \"kotlin\", \"go\"\ + ,\n \"rust\", \"ruby\", \"scala\", \"swift\", \"sql:sqlite\", \"sql:postgres\"\ + , \"sql:mysql\",\n \"sql:tsql\", \"sql:bigquery\", \"sql:ansi\". See\ + \ CodeLang enum for complete list.\n column_type: Discriminator field,\ + \ always \"llm-code\" for this configuration type." + LLMJudgeColumnConfig: + properties: + name: + type: string + title: Name + drop: + type: boolean + title: Drop + default: false + column_type: + type: string + const: llm-judge + title: Column Type + default: llm-judge + prompt: + type: string + title: Prompt + model_alias: + type: string + title: Model Alias + system_prompt: + type: string + title: System Prompt + multi_modal_context: + type: array + items: + $ref: '#/components/schemas/ImageContext' + title: Multi Modal Context + scores: + items: + $ref: '#/components/schemas/Score' + type: array + minItems: 1 + title: Scores + additionalProperties: false + type: object + required: + - name + - prompt + - model_alias + - scores + title: LLMJudgeColumnConfig + description: "Configuration for LLM-as-a-judge quality assessment and scoring\ + \ columns.\n\nExtends LLMTextColumnConfig to create judge columns that evaluate\ + \ and score other\ngenerated content based on the defined criteria. Useful\ + \ for quality assessment, preference\nranking, and multi-dimensional evaluation\ + \ of generated data.\n\nAttributes:\n scores: List of Score objects defining\ + \ the evaluation dimensions. Each score\n represents a different aspect\ + \ to evaluate (e.g., accuracy, relevance, fluency).\n Must contain\ + \ at least one score.\n column_type: Discriminator field, always \"llm-judge\"\ + \ for this configuration type." + LLMStructuredColumnConfig: + properties: + name: + type: string + title: Name + drop: + type: boolean + title: Drop + default: false + column_type: + type: string + const: llm-structured + title: Column Type + default: llm-structured + prompt: + type: string + title: Prompt + model_alias: + type: string + title: Model Alias + system_prompt: + type: string + title: System Prompt + multi_modal_context: + type: array + items: + $ref: '#/components/schemas/ImageContext' + title: Multi Modal Context + output_format: + anyOf: + - additionalProperties: true + type: object + - {} + title: Output Format + additionalProperties: false + type: object + required: + - name + - prompt + - model_alias + - output_format + title: LLMStructuredColumnConfig + description: "Configuration for structured JSON generation columns using Large\ + \ Language Models.\n\nExtends LLMTextColumnConfig to generate structured data\ + \ conforming to a specified schema.\nUses JSON schema or Pydantic models to\ + \ define the expected output structure, enabling\ntype-safe and validated\ + \ structured output generation. Inherits prompt templating capabilities.\n\ + \nAttributes:\n output_format: The schema defining the expected output\ + \ structure. Can be either:\n - A Pydantic BaseModel class (recommended)\n\ + \ - A JSON schema dictionary\n column_type: Discriminator field,\ + \ always \"llm-structured\" for this configuration type." + LLMTextColumnConfig: + properties: + name: + type: string + title: Name + drop: + type: boolean + title: Drop + default: false + column_type: + type: string + const: llm-text + title: Column Type + default: llm-text + prompt: + type: string + title: Prompt + model_alias: + type: string + title: Model Alias + system_prompt: + type: string + title: System Prompt + multi_modal_context: + type: array + items: + $ref: '#/components/schemas/ImageContext' + title: Multi Modal Context + additionalProperties: false + type: object + required: + - name + - prompt + - model_alias + title: LLMTextColumnConfig + description: "Configuration for text generation columns using Large Language\ + \ Models.\n\nLLM text columns generate free-form text content using language\ + \ models via LiteLLM.\nPrompts support Jinja2 templating to reference values\ + \ from other columns, enabling\ncontext-aware generation. The generated text\ + \ can optionally include reasoning traces\nwhen models support extended thinking.\n\ + \nAttributes:\n prompt: Prompt template for text generation. Supports Jinja2\ + \ syntax to\n reference other columns (e.g., \"Write a story about\ + \ {{ character_name }}\").\n Must be a valid Jinja2 template.\n \ + \ model_alias: Alias of the model configuration to use for generation.\n \ + \ Must match a model alias defined when initializing the DataDesignerConfigBuilder.\n\ + \ system_prompt: Optional system prompt to set model behavior and constraints.\n\ + \ Also supports Jinja2 templating. If provided, must be a valid Jinja2\ + \ template.\n Do not put any output parsing instructions in the system\ + \ prompt. Instead,\n use the appropriate column type for the output\ + \ you want to generate - e.g.,\n `LLMStructuredColumnConfig` for structured\ + \ output, `LLMCodeColumnConfig` for code.\n multi_modal_context: Optional\ + \ list of image contexts for multi-modal generation.\n Enables vision-capable\ + \ models to generate text based on image inputs.\n column_type: Discriminator\ + \ field, always \"llm-text\" for this configuration type." + LocalCallableValidatorParams: + properties: + validation_function: + title: Validation Function + description: Function (Callable[[pd.DataFrame], pd.DataFrame]) to validate + the data + output_schema: + type: object + additionalProperties: true + title: Output Schema + description: Expected schema for local callable validator's output + additionalProperties: false + type: object + required: + - validation_function + title: LocalCallableValidatorParams + description: "Configuration for local callable validation. Expects a function\ + \ to be passed that validates the data.\n\nAttributes:\n validation_function:\ + \ Function (`Callable[[pd.DataFrame], pd.DataFrame]`) to validate the\n \ + \ data. Output must contain a column `is_valid` of type `bool`.\n \ + \ output_schema: The JSON schema for the local callable validator's output.\ + \ If not provided,\n the output will not be validated." + ManualDistribution: + properties: + distribution_type: + allOf: + - $ref: '#/components/schemas/DistributionType' + default: manual + params: + $ref: '#/components/schemas/ManualDistributionParams' + additionalProperties: false + type: object + required: + - params + title: ManualDistribution + ManualDistributionParams: + properties: + values: + items: + type: number + type: array + minItems: 1 + title: Values + weights: + type: array + items: + type: number + title: Weights + additionalProperties: false + type: object + required: + - values + title: ManualDistributionParams + MessageType: + type: string + enum: + - analysis + - dataset + - heartbeat + - log + title: MessageType + Modality: + type: string + enum: + - image + title: Modality + ModalityDataType: + type: string + enum: + - url + - base64 + title: ModalityDataType + ModelConfigInput: + properties: + alias: + type: string + title: Alias + model: + type: string + title: Model + inference_parameters: + $ref: '#/components/schemas/InferenceParametersInput' + provider: + type: string + title: Provider + additionalProperties: false + type: object + required: + - alias + - model + title: ModelConfigInput + ModelConfigOutput: + properties: + alias: + type: string + title: Alias + model: + type: string + title: Model + inference_parameters: + $ref: '#/components/schemas/InferenceParametersOutput' + provider: + type: string + title: Provider + additionalProperties: false + type: object + required: + - alias + - model + title: ModelConfigOutput + PaginationData: + properties: + page: + type: integer + title: Page + description: The current page number. + page_size: + type: integer + title: Page Size + description: The page size used for the query. + current_page_size: + type: integer + title: Current Page Size + description: The size for the current page. + total_pages: + type: integer + title: Total Pages + description: The total number of pages. + total_results: + type: integer + title: Total Results + description: The total number of results. + type: object + required: + - page + - page_size + - current_page_size + - total_pages + - total_results + title: PaginationData + PartitionBlock: + properties: + index: + type: integer + minimum: 0.0 + title: Index + description: The index of the partition to sample from + default: 0 + num_partitions: + type: integer + minimum: 1.0 + title: Num Partitions + description: The total number of partitions in the dataset + default: 1 + additionalProperties: false + type: object + title: PartitionBlock + PersonFromFakerSamplerParams: + properties: + locale: + type: string + title: Locale + description: Locale string, determines the language and geographic locale + that a synthetic person will be sampled from. E.g, en_US, en_GB, fr_FR, + ... + default: en_US + sex: + type: string + title: Sex + description: If specified, then only synthetic people of the specified sex + will be sampled. + city: + anyOf: + - type: string + - items: + type: string + type: array + title: City + description: If specified, then only synthetic people from these cities + will be sampled. + age_range: + items: + type: integer + type: array + maxItems: 2 + minItems: 2 + title: Age Range + description: If specified, then only synthetic people within this age range + will be sampled. + default: + - 18 + - 114 + sampler_type: + type: string + const: person_from_faker + title: Sampler Type + default: person_from_faker + additionalProperties: false + type: object + title: PersonFromFakerSamplerParams + PersonSamplerParams: + properties: + locale: + type: string + title: Locale + description: 'Locale that determines the language and geographic location + that a synthetic person will be sampled from. Must be a locale supported + by a managed Nemotron Personas dataset. Managed datasets exist for the + following locales: en_US, ja_JP, en_IN, hi_IN.' + default: en_US + sex: + type: string + title: Sex + description: If specified, then only synthetic people of the specified sex + will be sampled. + city: + anyOf: + - type: string + - items: + type: string + type: array + title: City + description: If specified, then only synthetic people from these cities + will be sampled. + age_range: + items: + type: integer + type: array + maxItems: 2 + minItems: 2 + title: Age Range + description: If specified, then only synthetic people within this age range + will be sampled. + default: + - 18 + - 114 + select_field_values: + type: object + additionalProperties: + items: + type: string + type: array + title: Select Field Values + description: Sample synthetic people with the specified field values. This + is meant to be a flexible argument for selecting a subset of the population + from the managed dataset. Note that this sampler does not support rare + combinations of field values and will likely fail if your desired subset + is not well-represented in the managed Nemotron Personas dataset. We generally + recommend using the `sex`, `city`, and `age_range` arguments to filter + the population when possible. + examples: + - education_level: + - high_school + - some_college + - bachelors + state: + - NY + - CA + - OH + - TX + - NV + with_synthetic_personas: + type: boolean + title: With Synthetic Personas + description: If True, then append synthetic persona columns to each generated + person. + default: false + sampler_type: + type: string + const: person + title: Sampler Type + default: person + additionalProperties: false + type: object + title: PersonSamplerParams + description: "Parameters for sampling synthetic person data with demographic\ + \ attributes.\n\nGenerates realistic synthetic person data including names,\ + \ addresses, phone numbers, and other\ndemographic information. Data can be\ + \ sampled from managed datasets (when available) or generated\nusing Faker.\ + \ The sampler supports filtering by locale, sex, age, geographic location,\ + \ and can\noptionally include synthetic persona descriptions.\n\nAttributes:\n\ + \ locale: Locale string determining the language and geographic region\ + \ for synthetic people.\n Format: language_COUNTRY (e.g., \"en_US\"\ + , \"en_GB\", \"fr_FR\", \"de_DE\", \"es_ES\", \"ja_JP\").\n Defaults\ + \ to \"en_US\".\n sex: If specified, filters to only sample people of the\ + \ specified sex. Options: \"Male\" or\n \"Female\". If None, samples\ + \ both sexes.\n city: If specified, filters to only sample people from\ + \ the specified city or cities. Can be\n a single city name (string)\ + \ or a list of city names.\n age_range: Two-element list [min_age, max_age]\ + \ specifying the age range to sample from\n (inclusive). Defaults to\ + \ a standard age range. Both values must be between minimum and\n maximum\ + \ allowed ages.\n with_synthetic_personas: If True, appends additional\ + \ synthetic persona columns including\n personality traits, interests,\ + \ and background descriptions. Only supported for certain\n locales\ + \ with managed datasets.\n sample_dataset_when_available: If True, samples\ + \ from curated managed datasets when available\n for the specified\ + \ locale. If False or unavailable, falls back to Faker-generated data.\n \ + \ Managed datasets typically provide more realistic and diverse synthetic\ + \ people." + PlatformJobListResultResponse: + properties: + object: + type: string + title: Object + description: The type of object being returned. + default: list + data: + items: + $ref: '#/components/schemas/PlatformJobResultResponse' + type: array + title: Data + type: object + required: + - data + title: PlatformJobListResultResponse + PlatformJobLog: + properties: + timestamp: + type: string + format: date-time + title: Timestamp + job_id: + type: string + title: Job Id + job_step: + type: string + title: Job Step + job_task: + type: string + title: Job Task + message: + type: string + title: Message + type: object + required: + - timestamp + - job_id + - job_step + - job_task + - message + title: PlatformJobLog + PlatformJobLogPage: + properties: + object: + type: string + title: Object + description: The type of object being returned. + default: list + data: + items: + $ref: '#/components/schemas/PlatformJobLog' + type: array + title: Data + total: + type: integer + title: Total + next_page: + type: string + title: Next Page + prev_page: + type: string + title: Prev Page + type: object + required: + - data + - total + - next_page + - prev_page + title: PlatformJobLogPage + PlatformJobResultResponse: + properties: + result_name: + type: string + title: Result Name + job_id: + type: string + title: Job Id + namespace: + type: string + title: Namespace + project: + type: string + title: Project + created_at: + type: string + format: date-time + title: Created At + updated_at: + type: string + format: date-time + title: Updated At + artifact_url: + type: string + title: Artifact Url + artifact_storage_type: + $ref: '#/components/schemas/FileStorageType' + type: object + required: + - result_name + - job_id + - namespace + - artifact_url + - artifact_storage_type + title: PlatformJobResultResponse + PlatformJobStatus: + type: string + enum: + - created + - pending + - active + - cancelled + - cancelling + - error + - completed + - paused + - pausing + - resuming + title: PlatformJobStatus + description: 'Enumeration of possible job statuses. + + + This enum represents the various states a job can be in during its lifecycle, + + from creation to a terminal state.' + PlatformJobStatusResponse: + properties: + job_id: + type: string + title: Job Id + status: + $ref: '#/components/schemas/PlatformJobStatus' + status_details: + additionalProperties: true + type: object + title: Status Details + error_details: + type: object + additionalProperties: true + title: Error Details + steps: + items: + $ref: '#/components/schemas/PlatformJobStepStatusResponse' + type: array + title: Steps + type: object + required: + - job_id + - status + - status_details + - error_details + - steps + title: PlatformJobStatusResponse + PlatformJobStepStatusResponse: + properties: + name: + type: string + title: Name + status: + $ref: '#/components/schemas/PlatformJobStatus' + status_details: + additionalProperties: true + type: object + title: Status Details + error_details: + type: object + additionalProperties: true + title: Error Details + tasks: + items: + $ref: '#/components/schemas/PlatformJobTaskStatusResponse' + type: array + title: Tasks + type: object + required: + - name + - status + - status_details + - error_details + - tasks + title: PlatformJobStepStatusResponse + PlatformJobTaskStatusResponse: + properties: + id: + type: string + title: Id + status: + $ref: '#/components/schemas/PlatformJobStatus' + status_details: + additionalProperties: true + type: object + title: Status Details + error_details: + type: object + additionalProperties: true + title: Error Details + error_stack: + type: string + title: Error Stack + type: object + required: + - id + - status + - status_details + - error_details + - error_stack + title: PlatformJobTaskStatusResponse + PoissonSamplerParams: + properties: + mean: + type: number + title: Mean + description: Mean number of events in a fixed interval. + sampler_type: + type: string + const: poisson + title: Sampler Type + default: poisson + additionalProperties: false + type: object + required: + - mean + title: PoissonSamplerParams + description: "Parameters for sampling from a Poisson distribution.\n\nSamples\ + \ non-negative integer values representing the number of events occurring\ + \ in a fixed\ninterval of time or space. The Poisson distribution is commonly\ + \ used to model count data\nlike the number of arrivals, occurrences, or events\ + \ per time period.\n\nThe distribution is characterized by a single parameter\ + \ (mean/rate), and both the mean and\nvariance equal this parameter value.\n\ + \nAttributes:\n mean: Mean number of events in the fixed interval (also\ + \ called rate parameter \u03BB).\n Must be positive. This represents\ + \ both the expected value and the variance of the\n distribution." + PreviewMessage: + properties: + message: + type: string + title: Message + message_type: + $ref: '#/components/schemas/MessageType' + extra: + type: object + additionalProperties: + type: string + title: Extra + additionalProperties: false + type: object + required: + - message + - message_type + title: PreviewMessage + PreviewRequest: + properties: + config: + $ref: '#/components/schemas/DataDesignerConfig' + num_records: + type: integer + title: Num Records + type: object + required: + - config + title: PreviewRequest + ProcessorConfig: + properties: + build_stage: + allOf: + - $ref: '#/components/schemas/BuildStage' + description: 'The stage at which the processor will run. Supported stages: + post_batch' + additionalProperties: false + type: object + required: + - build_stage + title: ProcessorConfig + RemoteValidatorParams: + properties: + endpoint_url: + type: string + title: Endpoint Url + description: URL of the remote endpoint + output_schema: + type: object + additionalProperties: true + title: Output Schema + description: Expected schema for remote validator's output + timeout: + type: number + exclusiveMinimum: 0.0 + title: Timeout + description: The timeout for the HTTP request + default: 30.0 + max_retries: + type: integer + minimum: 0.0 + title: Max Retries + description: The maximum number of retry attempts + default: 3 + retry_backoff: + type: number + exclusiveMinimum: 1.0 + title: Retry Backoff + description: The backoff factor for the retry delay + default: 2.0 + max_parallel_requests: + type: integer + minimum: 1.0 + title: Max Parallel Requests + description: The maximum number of parallel requests to make + default: 4 + additionalProperties: false + type: object + required: + - endpoint_url + title: RemoteValidatorParams + description: "Configuration for remote validation. Sends data to a remote endpoint\ + \ for validation.\n\nAttributes:\n endpoint_url: The URL of the remote\ + \ endpoint.\n output_schema: The JSON schema for the remote validator's\ + \ output. If not provided,\n the output will not be validated.\n \ + \ timeout: The timeout for the HTTP request in seconds. Defaults to 30.0.\n\ + \ max_retries: The maximum number of retry attempts. Defaults to 3.\n \ + \ retry_backoff: The backoff factor for the retry delay in seconds. Defaults\ + \ to 2.0.\n max_parallel_requests: The maximum number of parallel requests\ + \ to make. Defaults to 4." + SamplerColumnConfig: + properties: + name: + type: string + title: Name + drop: + type: boolean + title: Drop + default: false + column_type: + type: string + const: sampler + title: Column Type + default: sampler + sampler_type: + $ref: '#/components/schemas/SamplerType' + params: + oneOf: + - $ref: '#/components/schemas/SubcategorySamplerParams' + - $ref: '#/components/schemas/CategorySamplerParams' + - $ref: '#/components/schemas/DatetimeSamplerParams' + - $ref: '#/components/schemas/PersonSamplerParams' + - $ref: '#/components/schemas/PersonFromFakerSamplerParams' + - $ref: '#/components/schemas/TimeDeltaSamplerParams' + - $ref: '#/components/schemas/UUIDSamplerParams' + - $ref: '#/components/schemas/BernoulliSamplerParams' + - $ref: '#/components/schemas/BernoulliMixtureSamplerParams' + - $ref: '#/components/schemas/BinomialSamplerParams' + - $ref: '#/components/schemas/GaussianSamplerParams' + - $ref: '#/components/schemas/PoissonSamplerParams' + - $ref: '#/components/schemas/UniformSamplerParams' + - $ref: '#/components/schemas/ScipySamplerParams' + title: Params + discriminator: + propertyName: sampler_type + mapping: + bernoulli: '#/components/schemas/BernoulliSamplerParams' + bernoulli_mixture: '#/components/schemas/BernoulliMixtureSamplerParams' + binomial: '#/components/schemas/BinomialSamplerParams' + category: '#/components/schemas/CategorySamplerParams' + datetime: '#/components/schemas/DatetimeSamplerParams' + gaussian: '#/components/schemas/GaussianSamplerParams' + person: '#/components/schemas/PersonSamplerParams' + person_from_faker: '#/components/schemas/PersonFromFakerSamplerParams' + poisson: '#/components/schemas/PoissonSamplerParams' + scipy: '#/components/schemas/ScipySamplerParams' + subcategory: '#/components/schemas/SubcategorySamplerParams' + timedelta: '#/components/schemas/TimeDeltaSamplerParams' + uniform: '#/components/schemas/UniformSamplerParams' + uuid: '#/components/schemas/UUIDSamplerParams' + conditional_params: + additionalProperties: + oneOf: + - $ref: '#/components/schemas/SubcategorySamplerParams' + - $ref: '#/components/schemas/CategorySamplerParams' + - $ref: '#/components/schemas/DatetimeSamplerParams' + - $ref: '#/components/schemas/PersonSamplerParams' + - $ref: '#/components/schemas/PersonFromFakerSamplerParams' + - $ref: '#/components/schemas/TimeDeltaSamplerParams' + - $ref: '#/components/schemas/UUIDSamplerParams' + - $ref: '#/components/schemas/BernoulliSamplerParams' + - $ref: '#/components/schemas/BernoulliMixtureSamplerParams' + - $ref: '#/components/schemas/BinomialSamplerParams' + - $ref: '#/components/schemas/GaussianSamplerParams' + - $ref: '#/components/schemas/PoissonSamplerParams' + - $ref: '#/components/schemas/UniformSamplerParams' + - $ref: '#/components/schemas/ScipySamplerParams' + discriminator: + propertyName: sampler_type + mapping: + bernoulli: '#/components/schemas/BernoulliSamplerParams' + bernoulli_mixture: '#/components/schemas/BernoulliMixtureSamplerParams' + binomial: '#/components/schemas/BinomialSamplerParams' + category: '#/components/schemas/CategorySamplerParams' + datetime: '#/components/schemas/DatetimeSamplerParams' + gaussian: '#/components/schemas/GaussianSamplerParams' + person: '#/components/schemas/PersonSamplerParams' + person_from_faker: '#/components/schemas/PersonFromFakerSamplerParams' + poisson: '#/components/schemas/PoissonSamplerParams' + scipy: '#/components/schemas/ScipySamplerParams' + subcategory: '#/components/schemas/SubcategorySamplerParams' + timedelta: '#/components/schemas/TimeDeltaSamplerParams' + uniform: '#/components/schemas/UniformSamplerParams' + uuid: '#/components/schemas/UUIDSamplerParams' + type: object + title: Conditional Params + default: {} + convert_to: + type: string + title: Convert To + additionalProperties: false + type: object + required: + - name + - sampler_type + - params + title: SamplerColumnConfig + description: "Configuration for columns generated using numerical samplers.\n\ + \nSampler columns provide efficient data generation using numerical samplers\ + \ for\ncommon data types and distributions. Supported samplers include UUID\ + \ generation,\ndatetime/timedelta sampling, person generation, category /\ + \ subcategory sampling,\nand various statistical distributions (uniform, gaussian,\ + \ binomial, poisson, scipy).\n\nAttributes:\n sampler_type: Type of sampler\ + \ to use. Available types include:\n \"uuid\", \"category\", \"subcategory\"\ + , \"uniform\", \"gaussian\", \"bernoulli\",\n \"bernoulli_mixture\"\ + , \"binomial\", \"poisson\", \"scipy\", \"person\", \"datetime\", \"timedelta\"\ + .\n params: Parameters specific to the chosen sampler type. Type varies\ + \ based on the `sampler_type`\n (e.g., `CategorySamplerParams`, `UniformSamplerParams`,\ + \ `PersonSamplerParams`).\n conditional_params: Optional dictionary for\ + \ conditional parameters. The dict keys\n are the conditions that must\ + \ be met (e.g., \"age > 21\") for the conditional parameters\n to be\ + \ used. The values of dict are the parameters to use when the condition is\ + \ met.\n convert_to: Optional type conversion to apply after sampling.\ + \ Must be one of \"float\", \"int\", or \"str\".\n Useful for converting\ + \ numerical samples to strings or other types.\n column_type: Discriminator\ + \ field, always \"sampler\" for this configuration type.\n\n!!! tip \"Displaying\ + \ available samplers and their parameters\"\n The config builder has an\ + \ `info` attribute that can be used to display the\n available samplers\ + \ and their parameters:\n ```python\n config_builder.info.display(\"\ + samplers\")\n ```" + SamplerType: + type: string + enum: + - bernoulli + - bernoulli_mixture + - binomial + - category + - datetime + - gaussian + - person + - person_from_faker + - poisson + - scipy + - subcategory + - timedelta + - uniform + - uuid + title: SamplerType + SamplingStrategy: + type: string + enum: + - ordered + - shuffle + title: SamplingStrategy + ScalarInequalityConstraint: + properties: + target_column: + type: string + title: Target Column + rhs: + type: number + title: Rhs + operator: + $ref: '#/components/schemas/InequalityOperator' + additionalProperties: false + type: object + required: + - target_column + - rhs + - operator + title: ScalarInequalityConstraint + ScipySamplerParams: + properties: + dist_name: + type: string + title: Dist Name + description: Name of a scipy.stats distribution. + dist_params: + additionalProperties: true + type: object + title: Dist Params + description: Parameters of the scipy.stats distribution given in `dist_name`. + decimal_places: + type: integer + title: Decimal Places + description: Number of decimal places to round the sampled values to. + sampler_type: + type: string + const: scipy + title: Sampler Type + default: scipy + additionalProperties: false + type: object + required: + - dist_name + - dist_params + title: ScipySamplerParams + description: "Parameters for sampling from any scipy.stats continuous or discrete\ + \ distribution.\n\nProvides a flexible interface to sample from the wide range\ + \ of probability distributions\navailable in scipy.stats. This enables advanced\ + \ statistical sampling beyond the built-in\ndistribution types (Gaussian,\ + \ Uniform, etc.).\n\nSee: [scipy.stats documentation](https://docs.scipy.org/doc/scipy/reference/stats.html)\n\ + \nAttributes:\n dist_name: Name of the scipy.stats distribution to sample\ + \ from (e.g., \"beta\", \"gamma\",\n \"lognorm\", \"expon\"). Must\ + \ be a valid distribution name from scipy.stats.\n dist_params: Dictionary\ + \ of parameters for the specified distribution. Parameter names\n and\ + \ values must match the scipy.stats distribution specification (e.g., {\"\ + a\": 2, \"b\": 5}\n for beta distribution, {\"scale\": 1.5} for exponential).\n\ + \ decimal_places: Optional number of decimal places to round sampled values\ + \ to. If None,\n values are not rounded." + Score: + properties: + name: + type: string + title: Name + description: A clear name for this score. + description: + type: string + title: Description + description: An informative and detailed assessment guide for using this + score. + options: + additionalProperties: + type: string + type: object + title: Options + description: 'Score options in the format of {score: description}.' + additionalProperties: false + type: object + required: + - name + - description + - options + title: Score + description: "Configuration for a \"score\" in an LLM judge evaluation.\n\n\ + Defines a single scoring criterion with its possible values and descriptions.\ + \ Multiple\nScore objects can be combined in an LLMJudgeColumnConfig to create\ + \ multi-dimensional\nquality assessments.\n\nAttributes:\n name: A clear,\ + \ concise name for this scoring dimension (e.g., \"Relevance\", \"Fluency\"\ + ).\n description: An informative and detailed assessment guide explaining\ + \ how to evaluate\n this dimension. Should provide clear criteria for\ + \ scoring.\n options: Dictionary mapping score values to their descriptions.\ + \ Keys can be integers\n (e.g., 1-5 scale) or strings (e.g., \"Poor\"\ + , \"Good\", \"Excellent\"). Values are\n descriptions explaining what\ + \ each score level means." + SeedConfig: + properties: + dataset: + type: string + title: Dataset + sampling_strategy: + allOf: + - $ref: '#/components/schemas/SamplingStrategy' + default: ordered + selection_strategy: + anyOf: + - $ref: '#/components/schemas/IndexRange' + - $ref: '#/components/schemas/PartitionBlock' + title: Selection Strategy + additionalProperties: false + type: object + required: + - dataset + title: SeedConfig + description: "Configuration for sampling data from a seed dataset.\n\nArgs:\n\ + \ dataset: Path or identifier for the seed dataset.\n sampling_strategy:\ + \ Strategy for how to sample rows from the dataset.\n - ORDERED: Read\ + \ rows sequentially in their original order.\n - SHUFFLE: Randomly\ + \ shuffle rows before sampling. When used with\n selection_strategy,\ + \ shuffling occurs within the selected range/partition.\n selection_strategy:\ + \ Optional strategy to select a subset of the dataset.\n - IndexRange:\ + \ Select a specific range of indices (e.g., rows 100-200).\n - PartitionBlock:\ + \ Select a partition by splitting the dataset into N equal parts.\n \ + \ Partition indices are zero-based (index=0 is the first partition, index=1\ + \ is\n the second, etc.).\n\nExamples:\n Read rows sequentially\ + \ from start to end:\n SeedConfig(dataset=\"my_data.parquet\", sampling_strategy=SamplingStrategy.ORDERED)\n\ + \n Read rows in random order:\n SeedConfig(dataset=\"my_data.parquet\"\ + , sampling_strategy=SamplingStrategy.SHUFFLE)\n\n Read specific index range\ + \ (rows 100-199):\n SeedConfig(\n dataset=\"my_data.parquet\"\ + ,\n sampling_strategy=SamplingStrategy.ORDERED,\n selection_strategy=IndexRange(start=100,\ + \ end=199)\n )\n\n Read random rows from a specific index range\ + \ (shuffles within rows 100-199):\n SeedConfig(\n dataset=\"\ + my_data.parquet\",\n sampling_strategy=SamplingStrategy.SHUFFLE,\n\ + \ selection_strategy=IndexRange(start=100, end=199)\n )\n\ + \n Read from partition 2 (3rd partition, zero-based) of 5 partitions (20%\ + \ of dataset):\n SeedConfig(\n dataset=\"my_data.parquet\"\ + ,\n sampling_strategy=SamplingStrategy.ORDERED,\n selection_strategy=PartitionBlock(index=2,\ + \ num_partitions=5)\n )\n\n Read shuffled rows from partition 0\ + \ of 10 partitions (shuffles within the partition):\n SeedConfig(\n\ + \ dataset=\"my_data.parquet\",\n sampling_strategy=SamplingStrategy.SHUFFLE,\n\ + \ selection_strategy=PartitionBlock(index=0, num_partitions=10)\n\ + \ )" + SeedDatasetColumnConfig: + properties: + name: + type: string + title: Name + drop: + type: boolean + title: Drop + default: false + column_type: + type: string + const: seed-dataset + title: Column Type + default: seed-dataset + additionalProperties: false + type: object + required: + - name + title: SeedDatasetColumnConfig + description: "Configuration for columns sourced from seed datasets.\n\nThis\ + \ config marks columns that come from seed data. It is typically created\n\ + automatically when calling `with_seed_dataset()` on the builder, rather than\n\ + being instantiated directly by users.\n\nAttributes:\n column_type: Discriminator\ + \ field, always \"seed-dataset\" for this configuration type." + SettingsDefaults: + properties: + model_configs: + items: + $ref: '#/components/schemas/ModelConfigOutput' + type: array + title: Model Configs + model_provider: + type: string + title: Model Provider + type: object + required: + - model_configs + - model_provider + title: SettingsDefaults + SettingsResponse: + properties: + defaults: + $ref: '#/components/schemas/SettingsDefaults' + model_providers: + items: + $ref: '#/components/schemas/DisplayModelProvider' + type: array + title: Model Providers + type: object + required: + - defaults + - model_providers + title: SettingsResponse + SubcategorySamplerParams: + properties: + category: + type: string + title: Category + description: Name of parent category to this subcategory. + values: + additionalProperties: + items: + anyOf: + - type: string + - type: integer + - type: number + type: array + type: object + title: Values + description: Mapping from each value of parent category to a list of subcategory + values. + sampler_type: + type: string + const: subcategory + title: Sampler Type + default: subcategory + additionalProperties: false + type: object + required: + - category + - values + title: SubcategorySamplerParams + description: "Parameters for subcategory sampling conditioned on a parent category\ + \ column.\n\nSamples subcategory values based on the value of a parent category\ + \ column. Each parent\ncategory value maps to its own list of possible subcategory\ + \ values, enabling hierarchical\nor conditional sampling patterns.\n\nAttributes:\n\ + \ category: Name of the parent category column that this subcategory depends\ + \ on.\n The parent column must be generated before this subcategory\ + \ column.\n values: Mapping from each parent category value to a list of\ + \ possible subcategory values.\n Each key must correspond to a value\ + \ that appears in the parent category column." + TimeDeltaSamplerParams: + properties: + dt_min: + type: integer + minimum: 0.0 + title: Dt Min + description: Minimum possible time-delta for sampling range, inclusive. + Must be less than `dt_max`. + dt_max: + type: integer + exclusiveMinimum: 0.0 + title: Dt Max + description: Maximum possible time-delta for sampling range, exclusive. + Must be greater than `dt_min`. + reference_column_name: + type: string + title: Reference Column Name + description: Name of an existing datetime column to condition time-delta + sampling on. + unit: + type: string + enum: + - D + - h + - m + - s + title: Unit + description: Sampling units, e.g. the smallest possible time interval between + samples. + default: D + sampler_type: + type: string + const: timedelta + title: Sampler Type + default: timedelta + additionalProperties: false + type: object + required: + - dt_min + - dt_max + - reference_column_name + title: TimeDeltaSamplerParams + description: "Parameters for sampling time deltas relative to a reference datetime\ + \ column.\n\nSamples time offsets within a specified range and adds them to\ + \ values from a reference\ndatetime column. This is useful for generating\ + \ related datetime columns like order dates\nand delivery dates, or event\ + \ start times and end times.\n\nNote:\n Years and months are not supported\ + \ as timedelta units because they have variable lengths.\n See: [pandas\ + \ timedelta documentation](https://pandas.pydata.org/docs/user_guide/timedeltas.html)\n\ + \nAttributes:\n dt_min: Minimum time-delta value (inclusive). Must be non-negative\ + \ and less than `dt_max`.\n Specified in units defined by the `unit`\ + \ parameter.\n dt_max: Maximum time-delta value (exclusive). Must be positive\ + \ and greater than `dt_min`.\n Specified in units defined by the `unit`\ + \ parameter.\n reference_column_name: Name of an existing datetime column\ + \ to add the time-delta to.\n This column must be generated before\ + \ the timedelta column.\n unit: Time unit for the delta values. Options:\n\ + \ - \"D\": Days (default)\n - \"h\": Hours\n - \"m\"\ + : Minutes\n - \"s\": Seconds" + UUIDSamplerParams: + properties: + prefix: + type: string + title: Prefix + description: String prepended to the front of the UUID. + short_form: + type: boolean + title: Short Form + description: If true, all UUIDs sampled will be truncated at 8 characters. + default: false + uppercase: + type: boolean + title: Uppercase + description: If true, all letters in the UUID will be capitalized. + default: false + sampler_type: + type: string + const: uuid + title: Sampler Type + default: uuid + additionalProperties: false + type: object + title: UUIDSamplerParams + description: "Parameters for generating UUID (Universally Unique Identifier)\ + \ values.\n\nGenerates UUID4 (random) identifiers with optional formatting\ + \ options. UUIDs are useful\nfor creating unique identifiers for records,\ + \ entities, or transactions.\n\nAttributes:\n prefix: Optional string to\ + \ prepend to each UUID. Useful for creating namespaced or\n typed identifiers\ + \ (e.g., \"user-\", \"order-\", \"txn-\").\n short_form: If True, truncates\ + \ UUIDs to 8 characters (first segment only). Default is False\n for\ + \ full 32-character UUIDs (excluding hyphens).\n uppercase: If True, converts\ + \ all hexadecimal letters to uppercase. Default is False for\n lowercase\ + \ UUIDs." + UniformDistribution: + properties: + distribution_type: + allOf: + - $ref: '#/components/schemas/DistributionType' + default: uniform + params: + $ref: '#/components/schemas/UniformDistributionParams' + additionalProperties: false + type: object + required: + - params + title: UniformDistribution + UniformDistributionParams: + properties: + low: + type: number + title: Low + high: + type: number + title: High + additionalProperties: false + type: object + required: + - low + - high + title: UniformDistributionParams + UniformSamplerParams: + properties: + low: + type: number + title: Low + description: Lower bound of the uniform distribution, inclusive. + high: + type: number + title: High + description: Upper bound of the uniform distribution, inclusive. + decimal_places: + type: integer + title: Decimal Places + description: Number of decimal places to round the sampled values to. + sampler_type: + type: string + const: uniform + title: Sampler Type + default: uniform + additionalProperties: false + type: object + required: + - low + - high + title: UniformSamplerParams + description: "Parameters for sampling from a continuous Uniform distribution.\n\ + \nSamples continuous values uniformly from a specified range, where every\ + \ value in the range\nhas equal probability of being sampled. This is useful\ + \ when all values within a range are\nequally likely, such as random percentages,\ + \ proportions, or unbiased measurements.\n\nAttributes:\n low: Lower bound\ + \ of the uniform distribution (inclusive). Can be any real number.\n high:\ + \ Upper bound of the uniform distribution (inclusive). Must be greater than\ + \ `low`.\n decimal_places: Optional number of decimal places to round sampled\ + \ values to. If None,\n values are not rounded and may have many decimal\ + \ places." + ValidationColumnConfig: + properties: + name: + type: string + title: Name + drop: + type: boolean + title: Drop + default: false + column_type: + type: string + const: validation + title: Column Type + default: validation + target_columns: + items: + type: string + type: array + title: Target Columns + validator_type: + $ref: '#/components/schemas/ValidatorType' + validator_params: + anyOf: + - $ref: '#/components/schemas/CodeValidatorParams' + - $ref: '#/components/schemas/LocalCallableValidatorParams' + - $ref: '#/components/schemas/RemoteValidatorParams' + title: Validator Params + batch_size: + type: integer + minimum: 1.0 + title: Batch Size + description: Number of records to process in each batch + default: 10 + additionalProperties: false + type: object + required: + - name + - target_columns + - validator_type + - validator_params + title: ValidationColumnConfig + description: "Configuration for validation columns that validate existing columns.\n\ + \nValidation columns execute validation logic against specified target columns\ + \ and return\nstructured results indicating pass/fail status with validation\ + \ details. Supports multiple\nvalidation strategies: code execution (Python/SQL),\ + \ local callable functions (library only),\nand remote HTTP endpoints.\n\n\ + Attributes:\n target_columns: List of column names to validate. These columns\ + \ are passed to the\n validator for validation. All target columns\ + \ must exist in the dataset\n before validation runs.\n validator_type:\ + \ The type of validator to use. Options:\n - \"code\": Execute code\ + \ (Python or SQL) for validation. The code receives a\n DataFrame\ + \ with target columns and must return a DataFrame with validation results.\n\ + \ - \"local_callable\": Call a local Python function with the data.\ + \ Only supported\n when running DataDesigner locally.\n -\ + \ \"remote\": Send data to a remote HTTP endpoint for validation. Useful for\n\ + \ validator_params: Parameters specific to the validator type. Type varies\ + \ by validator:\n - CodeValidatorParams: Specifies code language (python\ + \ or SQL dialect like\n \"sql:postgres\", \"sql:mysql\").\n \ + \ - LocalCallableValidatorParams: Provides validation function (Callable[[pd.DataFrame],\n\ + \ pd.DataFrame]) and optional output schema for validation results.\n\ + \ - RemoteValidatorParams: Configures endpoint URL, HTTP timeout, retry\ + \ behavior\n (max_retries, retry_backoff), and parallel request limits\ + \ (max_parallel_requests).\n batch_size: Number of records to process in\ + \ each validation batch. Defaults to 10.\n Larger batches are more\ + \ efficient but use more memory. Adjust based on validator\n complexity\ + \ and available resources.\n column_type: Discriminator field, always \"\ + validation\" for this configuration type." + ValidationError: + properties: + loc: + items: + anyOf: + - type: string + - type: integer + type: array + title: Location + msg: + type: string + title: Message + type: + type: string + title: Error Type + type: object + required: + - loc + - msg + - type + title: ValidationError + ValidatorType: + type: string + enum: + - code + - local_callable + - remote + title: ValidatorType +tags: +- name: Data Designer + description: Operations related to synthetic data generation. +- name: Health Checks + description: Operations related to NeMo Microservices platform health. diff --git a/studio/frontend/package.json b/studio/frontend/package.json index 0d1352efb3..3d6881cc53 100644 --- a/studio/frontend/package.json +++ b/studio/frontend/package.json @@ -13,77 +13,81 @@ "biome:fix": "biome check . --write" }, "dependencies": { - "@assistant-ui/react": "^0.12.3", - "@assistant-ui/react-markdown": "^0.12.1", - "@assistant-ui/react-streamdown": "^0.1.0", - "@base-ui/react": "^1.1.0", + "@assistant-ui/react": "^0.12.10", + "@assistant-ui/react-markdown": "^0.12.3", + "@assistant-ui/react-streamdown": "^0.1.2", + "@base-ui/react": "^1.2.0", "@dagrejs/dagre": "^2.0.4", "@dagrejs/graphlib": "^3.0.4", "@fontsource-variable/figtree": "^5.2.10", "@fontsource-variable/inter": "^5.2.8", "@fontsource-variable/space-grotesk": "^5.2.10", "@hugeicons/core-free-icons": "^3.1.1", - "@hugeicons/react": "^1.1.4", - "@huggingface/hub": "^2.8.0", + "@hugeicons/react": "^1.1.5", + "@huggingface/hub": "^2.9.0", + "@langchain/core": "^1.1.27", + "@langchain/textsplitters": "^1.0.1", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slot": "^1.2.4", - "@streamdown/cjk": "^1.0.1", - "@streamdown/code": "^1.0.1", - "@streamdown/math": "^1.0.1", - "@streamdown/mermaid": "^1.0.1", - "@tailwindcss/vite": "^4.1.17", - "@tanstack/react-router": "^1.156.0", + "@streamdown/cjk": "^1.0.2", + "@streamdown/code": "^1.0.2", + "@streamdown/math": "^1.0.2", + "@streamdown/mermaid": "^1.0.2", + "@tailwindcss/vite": "^4.1.18", + "@tanstack/react-router": "^1.159.10", "@tanstack/react-table": "^8.21.3", "@toolwind/corner-shape": "^0.0.8-3", "@types/canvas-confetti": "^1.9.0", "@xyflow/react": "^12.10.0", - "assistant-stream": "^0.3.0", + "assistant-stream": "^0.3.2", "canvas-confetti": "^1.9.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", "date-fns": "^4.1.0", - "dexie": "^4.2.1", - "framer-motion": "^11.15.0", - "katex": "^0.16.22", - "lucide-react": "^0.563.0", + "dexie": "^4.3.0", + "framer-motion": "^11.18.2", + "js-yaml": "^4.1.1", + "katex": "^0.16.28", + "lucide-react": "^0.575.0", "mammoth": "^1.11.0", - "motion": "^12.29.2", + "motion": "^12.34.0", "next": "^16.1.6", "next-themes": "^0.4.6", "radix-ui": "^1.4.3", - "react": "^19.2.0", - "react-day-picker": "^9.13.0", - "react-dom": "^19.2.0", - "react-resizable-panels": "^4.4.1", - "recharts": "2.15.4", + "react": "^19.2.4", + "react-day-picker": "^9.13.2", + "react-dom": "^19.2.4", + "react-resizable-panels": "^4.6.4", + "recharts": "3.7.0", "remark-gfm": "^4.0.1", - "shadcn": "^3.7.0", + "shadcn": "^3.8.4", "sonner": "^2.0.7", - "streamdown": "^2.1.0", + "streamdown": "^2.3.0", "tailwind-merge": "^3.4.0", - "tailwindcss": "^4.1.17", + "tailwindcss": "^4.1.18", "tw-animate-css": "^1.4.0", - "tw-shimmer": "^0.4.4", + "tw-shimmer": "^0.4.6", "unpdf": "^1.4.0", - "zustand": "^5.0.10" + "zustand": "^5.0.11" }, "devDependencies": { "@biomejs/biome": "^1.9.4", "@eslint/js": "^9.39.1", + "@types/js-yaml": "^4.0.9", "@types/node": "^24.10.1", "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.1", "eslint": "^9.39.1", "eslint-plugin-react-hooks": "^7.0.1", - "eslint-plugin-react-refresh": "^0.4.24", + "eslint-plugin-react-refresh": "^0.4.26", "globals": "^16.5.0", "typescript": "~5.9.3", - "typescript-eslint": "^8.46.4", - "vite": "^7.2.4" + "typescript-eslint": "^8.55.0", + "vite": "^7.3.1" } } diff --git a/studio/frontend/public/blacklogo.png b/studio/frontend/public/blacklogo.png new file mode 100644 index 0000000000..e74c19040a Binary files /dev/null and b/studio/frontend/public/blacklogo.png differ diff --git a/studio/frontend/public/unsloth-gem.png b/studio/frontend/public/unsloth-gem.png new file mode 100644 index 0000000000..662f5615dd Binary files /dev/null and b/studio/frontend/public/unsloth-gem.png differ diff --git a/studio/frontend/public/whitelogo.png b/studio/frontend/public/whitelogo.png new file mode 100644 index 0000000000..9db7c0e943 Binary files /dev/null and b/studio/frontend/public/whitelogo.png differ diff --git a/studio/frontend/src/app/provider.tsx b/studio/frontend/src/app/provider.tsx index 5c2b93bcc9..da88e0cfa3 100644 --- a/studio/frontend/src/app/provider.tsx +++ b/studio/frontend/src/app/provider.tsx @@ -10,7 +10,7 @@ export function AppProvider({ children }: AppProviderProps) { return ( {children} - + ); } diff --git a/studio/frontend/src/app/router.tsx b/studio/frontend/src/app/router.tsx index 63ae4e201c..dbbd9b1148 100644 --- a/studio/frontend/src/app/router.tsx +++ b/studio/frontend/src/app/router.tsx @@ -1,11 +1,13 @@ import { createRouter } from "@tanstack/react-router"; import { Route as rootRoute } from "./routes/__root"; +import { Route as dataRecipesRoute } from "./routes/data-recipes"; +import { Route as dataRecipeRoute } from "./routes/data-recipes.$recipeId"; import { Route as chatRoute } from "./routes/chat"; +import { Route as exportRoute } from "./routes/export"; import { Route as gridTestRoute } from "./routes/grid-test"; import { Route as indexRoute } from "./routes/index"; import { Route as loginRoute } from "./routes/login"; import { Route as onboardingRoute } from "./routes/onboarding"; -import { Route as exportRoute } from "./routes/export"; import { Route as signupRoute } from "./routes/signup"; import { Route as studioRoute } from "./routes/studio"; @@ -18,6 +20,8 @@ const routeTree = rootRoute.addChildren([ studioRoute, chatRoute, exportRoute, + dataRecipesRoute, + dataRecipeRoute, ]); export const router = createRouter({ routeTree }); diff --git a/studio/frontend/src/app/routes/data-recipes.$recipeId.tsx b/studio/frontend/src/app/routes/data-recipes.$recipeId.tsx new file mode 100644 index 0000000000..0ee88ac0a1 --- /dev/null +++ b/studio/frontend/src/app/routes/data-recipes.$recipeId.tsx @@ -0,0 +1,23 @@ +import { createRoute } from "@tanstack/react-router"; +import type { ReactElement } from "react"; +import { lazy } from "react"; +import { requireAuth } from "../auth-guards"; +import { Route as rootRoute } from "./__root"; + +const EditRecipePage = lazy(() => + import("@/features/data-recipes").then((m) => ({ + default: m.EditRecipePage, + })), +); + +export const Route = createRoute({ + getParentRoute: () => rootRoute, + path: "/data-recipes/$recipeId", + beforeLoad: () => requireAuth(), + component: DataRecipeEditorRoute, +}); + +function DataRecipeEditorRoute(): ReactElement { + const { recipeId } = Route.useParams(); + return ; +} diff --git a/studio/frontend/src/app/routes/data-recipes.tsx b/studio/frontend/src/app/routes/data-recipes.tsx new file mode 100644 index 0000000000..ff34f5530e --- /dev/null +++ b/studio/frontend/src/app/routes/data-recipes.tsx @@ -0,0 +1,17 @@ +import { createRoute } from "@tanstack/react-router"; +import { lazy } from "react"; +import { requireAuth } from "../auth-guards"; +import { Route as rootRoute } from "./__root"; + +const DataRecipesPage = lazy(() => + import("@/features/data-recipes").then((m) => ({ + default: m.DataRecipesPage, + })), +); + +export const Route = createRoute({ + getParentRoute: () => rootRoute, + path: "/data-recipes", + beforeLoad: () => requireAuth(), + component: DataRecipesPage, +}); diff --git a/studio/frontend/src/components/assistant-ui/attachment.tsx b/studio/frontend/src/components/assistant-ui/attachment.tsx index c53c134ea2..94e30fb4ac 100644 --- a/studio/frontend/src/components/assistant-ui/attachment.tsx +++ b/studio/frontend/src/components/assistant-ui/attachment.tsx @@ -149,10 +149,8 @@ const AttachmentUI: FC = () => { return "Document"; case "file": return "File"; - default: { - const _exhaustiveCheck: never = type; - throw new Error(`Unknown attachment type: ${_exhaustiveCheck}`); - } + default: + throw new Error(`Unknown attachment type: ${type as string}`); } }); diff --git a/studio/frontend/src/components/assistant-ui/markdown-text.tsx b/studio/frontend/src/components/assistant-ui/markdown-text.tsx index 1de2c47955..a3f07ab885 100644 --- a/studio/frontend/src/components/assistant-ui/markdown-text.tsx +++ b/studio/frontend/src/components/assistant-ui/markdown-text.tsx @@ -1,20 +1,107 @@ "use client"; -import { INTERNAL } from "@assistant-ui/react"; -import { StreamdownTextPrimitive } from "@assistant-ui/react-streamdown"; +import { INTERNAL, useMessagePartText } from "@assistant-ui/react"; +import { copyToClipboard } from "@/lib/copy-to-clipboard"; +import { Copy02Icon, Tick02Icon } from "@hugeicons/core-free-icons"; +import { HugeiconsIcon } from "@hugeicons/react"; import { code } from "@streamdown/code"; import { math } from "@streamdown/math"; import { mermaid } from "@streamdown/mermaid"; +import { Block, type BlockProps, Streamdown } from "streamdown"; +import { useEffect, useRef, useState } from "react"; import "katex/dist/katex.min.css"; -const { withSmoothContextProvider } = INTERNAL; +const { withSmoothContextProvider, useSmoothStatus } = INTERNAL; + +function getMermaidSource(blockContent: string): string | null { + const source = blockContent.match(/```mermaid\s*([\s\S]*?)```/i)?.[1]?.trim(); + return source && source.length > 0 ? source : null; +} + +const COPY_RESET_MS = 2000; + +function MermaidCopyButton({ source }: { source: string }) { + const [copied, setCopied] = useState(false); + const resetTimeoutRef = useRef | null>(null); + + useEffect(() => { + return () => { + if (resetTimeoutRef.current) { + clearTimeout(resetTimeoutRef.current); + } + }; + }, []); + + return ( + + ); +} + +function StreamdownBlock(props: BlockProps) { + const hasMermaidFence = props.content.includes("```mermaid"); + const mermaidSource = getMermaidSource(props.content); + + if (props.isIncomplete && hasMermaidFence) { + return ( +
+ Loading diagram... +
+ ); + } + + if (mermaidSource) { + return ( +
+ + +
+ ); + } + + return ; +} const MarkdownTextImpl = () => { + const { text } = useMessagePartText(); + const status = useSmoothStatus(); + return ( - +
+ + {text} + +
); }; diff --git a/studio/frontend/src/components/assistant-ui/model-selector.tsx b/studio/frontend/src/components/assistant-ui/model-selector.tsx index bf5f219558..c6685f0214 100644 --- a/studio/frontend/src/components/assistant-ui/model-selector.tsx +++ b/studio/frontend/src/components/assistant-ui/model-selector.tsx @@ -27,6 +27,7 @@ interface ModelSelectorProps { loraModels?: LoraModelOption[]; value?: string; defaultValue?: string; + activeGgufVariant?: string | null; onValueChange?: (value: string, meta: ModelSelectorChangeMeta) => void; onEject?: () => void; variant?: "outline" | "ghost" | "muted"; @@ -62,7 +63,7 @@ function ModelSelectorTrigger({ className={cn( "flex items-center gap-2 transition-colors", variant === "outline" && - "rounded-full border border-border/60 hover:bg-accent", + "rounded-full border border-border/60 hover:bg-accent", variant === "ghost" && "rounded-md hover:bg-accent", variant === "muted" && "rounded-md bg-muted hover:bg-muted/80", size === "sm" && "h-8 px-3 text-xs", @@ -158,6 +159,7 @@ export function ModelSelector({ loraModels = [], value, defaultValue, + activeGgufVariant, onValueChange, onEject, variant = "outline", @@ -183,17 +185,34 @@ export function ModelSelector({ all.set(model.id, model); } for (const lora of loraModels) { + // Strip "/ suffix" from display name (e.g. "foo_123/foo" → "foo_123") + const displayName = lora.name.includes("/") + ? lora.name.split("/")[0].trim() + : lora.name; + // Show type tag instead of base model name + const isExported = lora.source === "exported"; + const isMerged = lora.exportType === "merged"; + const tag = isExported + ? isMerged ? "Merged · Exported" : "LoRA" + : "LoRA"; all.set(lora.id, { ...lora, - description: lora.baseModel || lora.description, + name: displayName, + description: tag, }); } return all; }, [loraModels, models]); - const currentModel = selected - ? optionById.get(selected) ?? { id: selected, name: selected } - : undefined; + const currentModel = useMemo(() => { + if (!selected) return undefined; + const found = optionById.get(selected); + if (activeGgufVariant) { + const desc = `GGUF · ${activeGgufVariant}`; + return found ? { ...found, description: desc } : { id: selected, name: selected, description: desc }; + } + return found ?? { id: selected, name: selected }; + }, [selected, optionById, activeGgufVariant]); function handleSelect(id: string, meta: ModelSelectorChangeMeta) { if (onValueChange) { diff --git a/studio/frontend/src/components/assistant-ui/model-selector/pickers.tsx b/studio/frontend/src/components/assistant-ui/model-selector/pickers.tsx index 80e9dd0b6b..3d91cffb8c 100644 --- a/studio/frontend/src/components/assistant-ui/model-selector/pickers.tsx +++ b/studio/frontend/src/components/assistant-ui/model-selector/pickers.tsx @@ -5,6 +5,8 @@ import { TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; +import { listGgufVariants } from "@/features/chat/api/chat-api"; +import type { GgufVariantDetail } from "@/features/chat/types/api"; import { useDebouncedValue, useGpuInfo, @@ -17,7 +19,7 @@ import type { VramFitStatus } from "@/lib/vram"; import { checkVramFit, estimateLoadingVram } from "@/lib/vram"; import { Search01Icon } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; -import { useMemo, useState, type ReactNode } from "react"; +import { useCallback, useEffect, useMemo, useState, type ReactNode } from "react"; import type { LoraModelOption, ModelOption, @@ -36,6 +38,15 @@ function ListLabel({ children }: { children: ReactNode }) { ); } +/** Format bytes to a human-readable size string. */ +function formatBytes(bytes: number): string { + if (bytes === 0) return "0 B"; + const units = ["B", "KB", "MB", "GB", "TB"]; + const i = Math.floor(Math.log(bytes) / Math.log(1024)); + const value = bytes / 1024 ** i; + return `${value.toFixed(value < 10 ? 1 : 0)} ${units[i]}`; +} + function ModelRow({ label, meta, @@ -44,6 +55,7 @@ function ModelRow({ vramStatus, vramEst, gpuGb, + tooltipText, }: { label: string; meta?: string; @@ -52,6 +64,7 @@ function ModelRow({ vramStatus?: VramFitStatus | null; vramEst?: number; gpuGb?: number; + tooltipText?: ReactNode; }) { const exceeds = vramStatus === "exceeds"; const showVramTooltip = @@ -70,29 +83,26 @@ function ModelRow({ type="button" onClick={onClick} className={cn( - "flex w-full items-center justify-between gap-2 rounded-md px-2.5 py-1.5 text-left text-sm transition-colors hover:bg-accent", + "flex w-full items-center gap-2 rounded-md px-2.5 py-1.5 text-left text-sm transition-colors hover:bg-accent", selected && "bg-accent/60", exceeds && "opacity-50", )} > {label} - + {vramStatus === "exceeds" && ( OOM )} {vramStatus === "tight" && ( TIGHT )} - {vramStatus === "fits" && ( - FIT - )} {meta ? ( {meta} ) : null} @@ -111,9 +121,154 @@ function ModelRow({ ); } + + if (tooltipText) { + return ( + + {content} + + {tooltipText} + + + ); + } return content; } +// ── GGUF Variant Expander ──────────────────────────────────── + +function GgufVariantExpander({ + repoId, + onSelect, + gpuGb, +}: { + repoId: string; + onSelect: (id: string, meta: ModelSelectorChangeMeta) => void; + gpuGb?: number; +}) { + const [variants, setVariants] = useState(null); + const [defaultVariant, setDefaultVariant] = useState(null); + const [hasVision, setHasVision] = useState(false); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + let canceled = false; + setLoading(true); + setError(null); + + listGgufVariants(repoId) + .then((res) => { + if (canceled) return; + setVariants(res.variants); + setDefaultVariant(res.default_variant); + setHasVision(res.has_vision); + }) + .catch((err) => { + if (canceled) return; + setError(err instanceof Error ? err.message : "Failed to load variants"); + }) + .finally(() => { + if (!canceled) setLoading(false); + }); + + return () => { + canceled = true; + }; + }, [repoId]); + + const handleVariantClick = useCallback( + (quant: string) => { + onSelect(repoId, { + source: "hub", + isLora: false, + ggufVariant: quant, + }); + }, + [repoId, onSelect], + ); + + if (loading) { + return ( +
+ + Loading variants… +
+ ); + } + + if (error) { + return ( +
{error}
+ ); + } + + if (!variants || variants.length === 0) { + return ( +
+ No GGUF variants found. +
+ ); + } + + return ( +
+
+ + Quantizations + + {hasVision && ( + Vision + )} +
+ {variants.map((v) => { + const sizeGb = v.size_bytes / (1024 ** 3); + const fitStatus = gpuGb != null && gpuGb > 0 && sizeGb > 0 + ? checkVramFit(sizeGb, gpuGb) + : null; + return ( + + ); + })} +
+ ); +} + +// ── Detect GGUF repos by naming convention ──────────────────── + +function isGgufRepo(id: string): boolean { + return id.toUpperCase().includes("-GGUF"); +} + +// ── Hub Model Picker ────────────────────────────────────────── + export function HubModelPicker({ models, value, @@ -130,6 +285,9 @@ export function HubModelPicker({ debouncedQuery, ); + // Track which GGUF repo is expanded for variant selection + const [expandedGguf, setExpandedGguf] = useState(null); + const recommendedIds = useMemo( () => dedupe([...models.map((model) => model.id), value ?? ""]), [models, value], @@ -199,6 +357,19 @@ export function HubModelPicker({ const { scrollRef, sentinelRef } = useInfiniteScroll(fetchMore, results.length); + /** Handle clicking a model row — GGUF repos expand, others load directly. */ + const handleModelClick = useCallback( + (id: string) => { + if (isGgufRepo(id)) { + // Toggle GGUF variant expander + setExpandedGguf((prev) => (prev === id ? null : id)); + } else { + onSelect(id, { source: "hub", isLora: false }); + } + }, + [onSelect], + ); + return (
@@ -230,18 +401,24 @@ export function HubModelPicker({ recommendedIds.map((id) => { const vram = recommendedVramMap.get(id); return ( - - onSelect(id, { source: "hub", isLora: false }) - } - vramStatus={vram?.status ?? null} - vramEst={vram?.est} - gpuGb={gpu.available ? gpu.memoryTotalGb : undefined} - /> +
+ handleModelClick(id)} + vramStatus={isGgufRepo(id) ? null : vram?.status ?? null} + vramEst={isGgufRepo(id) ? undefined : vram?.est} + gpuGb={gpu.available ? gpu.memoryTotalGb : undefined} + /> + {expandedGguf === id && ( + + )} +
); }) )} @@ -259,18 +436,24 @@ export function HubModelPicker({ hfIds.map((id) => { const vram = vramMap.get(id); return ( - - onSelect(id, { source: "hub", isLora: false }) - } - vramStatus={vram?.status ?? null} - vramEst={vram?.est} - gpuGb={gpu.available ? gpu.memoryTotalGb : undefined} - /> +
+ handleModelClick(id)} + vramStatus={isGgufRepo(id) ? null : vram?.status ?? null} + vramEst={isGgufRepo(id) ? undefined : vram?.est} + gpuGb={gpu.available ? gpu.memoryTotalGb : undefined} + /> + {expandedGguf === id && ( + + )} +
); }) )} @@ -365,15 +548,37 @@ export function LoraModelPicker({
{index > 0 ?
: null} {baseModel} - {adapters.map((adapter) => ( - onSelect(adapter.id, { source: "lora", isLora: true })} - /> - ))} + {adapters.map((adapter) => { + const isExported = adapter.source === "exported"; + const isMerged = adapter.exportType === "merged"; + const isGguf = adapter.exportType === "gguf"; + const tag = isGguf + ? "GGUF" + : isExported + ? isMerged ? "Merged" : "LoRA" + : "LoRA"; + const meta = isExported ? `${tag} · Exported` : tag; + return ( + onSelect(adapter.id, { + source: isExported ? "exported" : "lora", + isLora: !isMerged && !isGguf, + })} + tooltipText={ + <> + {adapter.name} + + {adapter.id} + + + } + /> + ); + })}
)) )} @@ -382,4 +587,3 @@ export function LoraModelPicker({
); } - diff --git a/studio/frontend/src/components/assistant-ui/model-selector/types.ts b/studio/frontend/src/components/assistant-ui/model-selector/types.ts index dcf110bfb7..0e8cf5fb4d 100644 --- a/studio/frontend/src/components/assistant-ui/model-selector/types.ts +++ b/studio/frontend/src/components/assistant-ui/model-selector/types.ts @@ -10,10 +10,13 @@ export interface ModelOption { export interface LoraModelOption extends ModelOption { baseModel?: string; updatedAt?: number; + source?: "training" | "exported"; + exportType?: "lora" | "merged" | "gguf"; } export interface ModelSelectorChangeMeta { - source: "hub" | "lora"; + source: "hub" | "lora" | "exported"; isLora: boolean; + ggufVariant?: string; } diff --git a/studio/frontend/src/components/assistant-ui/thread.tsx b/studio/frontend/src/components/assistant-ui/thread.tsx index aaaca0eeb3..f57effdf9f 100644 --- a/studio/frontend/src/components/assistant-ui/thread.tsx +++ b/studio/frontend/src/components/assistant-ui/thread.tsx @@ -70,7 +70,7 @@ export const Thread: FC<{ hideComposer?: boolean; hideWelcome?: boolean }> = ({ }} /> - + !thread.isEmpty}> {!hideComposer && } diff --git a/studio/frontend/src/components/markdown/markdown-preview.tsx b/studio/frontend/src/components/markdown/markdown-preview.tsx new file mode 100644 index 0000000000..c10ec783be --- /dev/null +++ b/studio/frontend/src/components/markdown/markdown-preview.tsx @@ -0,0 +1,46 @@ +import { cn } from "@/lib/utils"; +import { code } from "@streamdown/code"; +import { math } from "@streamdown/math"; +import { mermaid } from "@streamdown/mermaid"; +import { memo, type ReactElement } from "react"; +import { Streamdown } from "streamdown"; +import "katex/dist/katex.min.css"; + +const MARKDOWN_PLUGINS = { code, math, mermaid } as const; + +type MarkdownPreviewProps = { + markdown: string; + className?: string; + plain?: boolean; +}; + +function MarkdownPreviewImpl({ + markdown, + className, + plain = false, +}: MarkdownPreviewProps): ReactElement { + const markdownClassName = + "w-full max-w-none min-w-0 space-y-2 [overflow-wrap:anywhere] [&_*]:max-w-none [&_p]:w-full [&_ul]:w-full [&_ol]:w-full [&_li]:w-full [&_h1]:w-full [&_h2]:w-full [&_h3]:w-full [&_h4]:w-full [&_h5]:w-full [&_h6]:w-full [&_pre]:w-full [&_table]:w-full [&_p]:break-words [&_li]:break-words [&_code]:break-words [&_pre]:whitespace-pre-wrap [&_pre]:break-words"; + + return ( +
+ + {markdown.trim() ? markdown : "_Empty note_"} + +
+ ); +} + +export const MarkdownPreview = memo(MarkdownPreviewImpl); diff --git a/studio/frontend/src/components/markdown/mermaid-error.tsx b/studio/frontend/src/components/markdown/mermaid-error.tsx new file mode 100644 index 0000000000..4e352d4768 --- /dev/null +++ b/studio/frontend/src/components/markdown/mermaid-error.tsx @@ -0,0 +1,28 @@ +import type { MermaidErrorComponentProps } from "streamdown"; + +function hasSlashComment(chart: string): boolean { + return /(^|[^:])\/\/.*/m.test(chart); +} + +export function MermaidError({ + error, + chart, + retry, +}: MermaidErrorComponentProps) { + return ( +
+

Mermaid render failed

+

{error}

+ {hasSlashComment(chart) ? ( +

Hint: Mermaid comments use `%%`, not `//`.

+ ) : null} + +
+ ); +} diff --git a/studio/frontend/src/components/navbar.tsx b/studio/frontend/src/components/navbar.tsx index d30622f95d..4a75223dc4 100644 --- a/studio/frontend/src/components/navbar.tsx +++ b/studio/frontend/src/components/navbar.tsx @@ -3,6 +3,7 @@ import { HoverCardContent, HoverCardTrigger, } from "@/components/ui/hover-card"; +import { AnimatedThemeToggler } from "@/components/ui/animated-theme-toggler"; import { Sheet, SheetContent, @@ -13,9 +14,9 @@ import { import { cn } from "@/lib/utils"; import { AiChat02Icon, - Analytics01Icon, ArrowRight01Icon, Book03Icon, + ChefHatIcon, CursorInfo02Icon, PackageIcon, ZapIcon, @@ -23,31 +24,30 @@ import { import { HugeiconsIcon } from "@hugeicons/react"; import { useTrainingRuntimeStore } from "@/features/training"; import { Link, useRouterState } from "@tanstack/react-router"; -import { AnimatePresence, motion } from "motion/react"; +import { motion } from "motion/react"; import { useState } from "react"; import { TOUR_OPEN_EVENT } from "@/features/tour"; const NAV_ITEMS = [ { label: "Studio", href: "/studio", icon: ZapIcon, enabled: true }, - { label: "Evaluate", href: "/evaluate", icon: Analytics01Icon, enabled: false }, + { label: "Recipes", href: "/data-recipes", icon: ChefHatIcon, enabled: true }, { label: "Export", href: "/export", icon: PackageIcon, enabled: true }, { label: "Chat", href: "/chat", icon: AiChat02Icon, enabled: true }, ]; +function getTourId(pathname: string): "studio" | "chat" | "export" | null { + if (pathname === "/studio") return "studio"; + if (pathname === "/chat") return "chat"; + if (pathname === "/export") return "export"; + return null; +} + export function Navbar() { const pathname = useRouterState({ select: (s) => s.location.pathname }); const isTrainingRunning = useTrainingRuntimeStore((s) => s.isTrainingRunning); - const [logoHovered, setLogoHovered] = useState(false); const [mobileOpen, setMobileOpen] = useState(false); - const tourId = - pathname === "/studio" - ? "studio" - : pathname === "/chat" - ? "chat" - : pathname === "/export" - ? "export" - : null; + const tourId = getTourId(pathname); const openTour = () => { if (!tourId) return; @@ -58,37 +58,20 @@ export function Navbar() { return (
-
+
{/* Left: logo */} -
setLogoHovered(true)} - onMouseLeave={() => setLogoHovered(false)} - > - + Unsloth - - unsloth - - - {logoHovered && ( - - )} - -
+ Unsloth + {/* Center: pill nav */} {/* Right: docs/tour desktop */} -
+ {/* Right: mobile */} diff --git a/studio/frontend/src/components/section-card.tsx b/studio/frontend/src/components/section-card.tsx index 1548fd57db..aee6fdfa05 100644 --- a/studio/frontend/src/components/section-card.tsx +++ b/studio/frontend/src/components/section-card.tsx @@ -52,7 +52,7 @@ export function SectionCard({ return (
{ + duration?: number +} + +export const AnimatedThemeToggler = ({ + className, + duration = 400, + ...props +}: AnimatedThemeTogglerProps) => { + const [isDark, setIsDark] = useState(false) + const buttonRef = useRef(null) + + useEffect(() => { + const updateTheme = () => { + setIsDark(document.documentElement.classList.contains("dark")) + } + + updateTheme() + + const observer = new MutationObserver(updateTheme) + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ["class"], + }) + + return () => observer.disconnect() + }, []) + + const toggleTheme = useCallback(async () => { + if (!buttonRef.current) return + + await document.startViewTransition(() => { + flushSync(() => { + const newTheme = !isDark + setIsDark(newTheme) + document.documentElement.classList.toggle("dark") + localStorage.setItem("theme", newTheme ? "dark" : "light") + }) + }).ready + + const { top, left, width, height } = + buttonRef.current.getBoundingClientRect() + const x = left + width / 2 + const y = top + height / 2 + const maxRadius = Math.hypot( + Math.max(left, window.innerWidth - left), + Math.max(top, window.innerHeight - top) + ) + + document.documentElement.animate( + { + clipPath: [ + `circle(0px at ${x}px ${y}px)`, + `circle(${maxRadius}px at ${x}px ${y}px)`, + ], + }, + { + duration, + easing: "ease-in-out", + pseudoElement: "::view-transition-new(root)", + } + ) + }, [isDark, duration]) + + return ( + + ) +} diff --git a/studio/frontend/src/components/ui/chart.tsx b/studio/frontend/src/components/ui/chart.tsx index 98dd00f6aa..071148982f 100644 --- a/studio/frontend/src/components/ui/chart.tsx +++ b/studio/frontend/src/components/ui/chart.tsx @@ -100,30 +100,30 @@ ${colorConfig ); }; -const ChartTooltip = RechartsPrimitive.Tooltip; - -function ChartTooltipContent({ - active, - payload, - className, +const ChartTooltip = RechartsPrimitive.Tooltip; + +function ChartTooltipContent({ + active, + payload, + className, indicator = "dot", hideLabel = false, hideIndicator = false, label, labelFormatter, labelClassName, - formatter, - color, - nameKey, - labelKey, -}: React.ComponentProps & - React.ComponentProps<"div"> & { - hideLabel?: boolean; - hideIndicator?: boolean; - indicator?: "line" | "dot" | "dashed"; - nameKey?: string; - labelKey?: string; - }) { + formatter, + color, + nameKey, + labelKey, +}: Partial> & + React.ComponentProps<"div"> & { + hideLabel?: boolean; + hideIndicator?: boolean; + indicator?: "line" | "dot" | "dashed"; + nameKey?: string; + labelKey?: string; + }) { const { config } = useChart(); const tooltipLabel = React.useMemo(() => { @@ -248,20 +248,20 @@ function ChartTooltipContent({ ); } -const ChartLegend = RechartsPrimitive.Legend; - -function ChartLegendContent({ - className, - hideIcon = false, - payload, - verticalAlign = "bottom", - nameKey, -}: React.ComponentProps<"div"> & - Pick & { - hideIcon?: boolean; - nameKey?: string; - }) { - const { config } = useChart(); +const ChartLegend = RechartsPrimitive.Legend; + +function ChartLegendContent({ + className, + hideIcon = false, + payload, + verticalAlign = "bottom", + nameKey, +}: React.ComponentProps<"div"> & + Pick & { + hideIcon?: boolean; + nameKey?: string; + }) { + const { config } = useChart(); if (!payload?.length) { return null; diff --git a/studio/frontend/src/components/ui/combobox.tsx b/studio/frontend/src/components/ui/combobox.tsx index 0893e9b218..34b66ea64e 100644 --- a/studio/frontend/src/components/ui/combobox.tsx +++ b/studio/frontend/src/components/ui/combobox.tsx @@ -7,6 +7,7 @@ import * as React from "react"; import { createContext, useContext, useState } from "react"; import { Button } from "@/components/ui/button"; +import { useDialogPortalContainer } from "@/components/ui/dialog"; import { InputGroup, InputGroupAddon, @@ -139,38 +140,42 @@ function ComboboxInput({ ); } -function ComboboxContent({ - className, - side = "bottom", - sideOffset = 6, +function ComboboxContent({ + className, + side = "bottom", + sideOffset = 6, align = "start", alignOffset = 0, anchor, + container, ...props }: ComboboxPrimitive.Popup.Props & Pick< ComboboxPrimitive.Positioner.Props, "side" | "align" | "sideOffset" | "alignOffset" | "anchor" - >): React.ReactElement { + > & { + container?: HTMLElement | null; + }): React.ReactElement { + const dialogContainer = useDialogPortalContainer(); return ( - - - + + + ); @@ -201,7 +206,7 @@ function ComboboxItem({ { columns: ColumnDef[]; data: TData[]; className?: string; + onRowClick?: (row: TData, rowIndex: number, rowId: string) => void; + getRowClassName?: ( + row: TData, + rowIndex: number, + rowId: string, + ) => string | undefined; } export function DataTable({ columns, data, className, + onRowClick, + getRowClassName, }: DataTableProps) { const [sorting, setSorting] = useState([]); + // eslint-disable-next-line react-hooks/incompatible-library const table = useReactTable({ data, columns, @@ -81,7 +90,9 @@ export function DataTable({ ? "bg-background" : "bg-muted/20", "hover:bg-primary/[0.03]", + getRowClassName?.(row.original, idx, row.id), )} + onClick={() => onRowClick?.(row.original, idx, row.id)} > {row.getVisibleCells().map((cell) => ( ) { - return ; -} - -function DialogTrigger({ - ...props -}: React.ComponentProps) { - return ; -} - -function DialogPortal({ - ...props -}: React.ComponentProps) { - return ; -} - -function DialogClose({ - ...props -}: React.ComponentProps) { - return ; -} - -function DialogOverlay({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function DialogContent({ - className, - children, - showCloseButton = true, - ...props -}: React.ComponentProps & { - showCloseButton?: boolean; -}) { - return ( - - - - {children} - {showCloseButton && ( - - - - )} - - - ); -} - -function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ); -} - -function DialogFooter({ - className, - showCloseButton = false, - children, - ...props -}: React.ComponentProps<"div"> & { - showCloseButton?: boolean; -}) { - return ( -
- {children} - {showCloseButton && ( - - - - )} -
- ); -} - -function DialogTitle({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function DialogDescription({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} - -export { - Dialog, - DialogClose, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogOverlay, - DialogPortal, - DialogTitle, - DialogTrigger, -}; +"use client"; + +import { Dialog as DialogPrimitive } from "radix-ui"; +import type * as React from "react"; +import { createContext, useContext } from "react"; + +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; +import { Cancel01Icon } from "@hugeicons/core-free-icons"; +import { HugeiconsIcon } from "@hugeicons/react"; + +const DialogPortalContainerContext = createContext(null); + +export function useDialogPortalContainer(): HTMLElement | null { + return useContext(DialogPortalContainerContext); +} + +function Dialog({ + ...props +}: React.ComponentProps) { + return ; +} + +function DialogTrigger({ + ...props +}: React.ComponentProps) { + return ; +} + +function DialogPortal({ + ...props +}: React.ComponentProps) { + return ; +} + +function DialogClose({ + ...props +}: React.ComponentProps) { + return ; +} + +function DialogOverlay({ + className, + position = "fixed", + ...props +}: React.ComponentProps & { + position?: "fixed" | "absolute"; +}) { + return ( + + ); +} + +function DialogContent({ + className, + children, + showCloseButton = true, + container, + position = "fixed", + overlayClassName, + overlayPosition, + ...props +}: React.ComponentProps & { + showCloseButton?: boolean; + container?: HTMLElement | null; + position?: "fixed" | "absolute"; + overlayClassName?: string; + overlayPosition?: "fixed" | "absolute"; +}) { + const resolvedContainer = container ?? null; + return ( + + + + + {children} + {showCloseButton && ( + + + + )} + + + + ); +} + +function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function DialogFooter({ + className, + showCloseButton = false, + children, + ...props +}: React.ComponentProps<"div"> & { + showCloseButton?: boolean; +}) { + return ( +
+ {children} + {showCloseButton && ( + + + + )} +
+ ); +} + +function DialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +}; diff --git a/studio/frontend/src/components/ui/empty.tsx b/studio/frontend/src/components/ui/empty.tsx new file mode 100644 index 0000000000..90353e5e18 --- /dev/null +++ b/studio/frontend/src/components/ui/empty.tsx @@ -0,0 +1,104 @@ +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +function Empty({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +const emptyMediaVariants = cva( + "mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0", + { + variants: { + variant: { + default: "bg-transparent", + icon: "bg-muted text-foreground flex size-10 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*='size-'])]:size-6", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function EmptyMedia({ + className, + variant = "default", + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ) +} + +function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) { + return ( +
a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4", + className + )} + {...props} + /> + ) +} + +function EmptyContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { + Empty, + EmptyHeader, + EmptyTitle, + EmptyDescription, + EmptyContent, + EmptyMedia, +} diff --git a/studio/frontend/src/components/ui/select.tsx b/studio/frontend/src/components/ui/select.tsx index 5cd35224c6..4c500af08e 100644 --- a/studio/frontend/src/components/ui/select.tsx +++ b/studio/frontend/src/components/ui/select.tsx @@ -5,6 +5,7 @@ import type * as React from "react"; import { createContext, useContext, useState } from "react"; import { cn } from "@/lib/utils"; +import { useDialogPortalContainer } from "@/components/ui/dialog"; import { ArrowDown01Icon, ArrowUp01Icon, @@ -96,10 +97,14 @@ function SelectContent({ children, position = "item-aligned", align = "center", + container, ...props -}: React.ComponentProps) { +}: React.ComponentProps & { + container?: HTMLElement | null; +}) { + const dialogContainer = useDialogPortalContainer(); return ( - + ) { - return ; -} - -function SheetTrigger({ - ...props -}: React.ComponentProps) { - return ; -} - -function SheetClose({ - ...props -}: React.ComponentProps) { - return ; -} - -function SheetPortal({ - ...props -}: React.ComponentProps) { - return ; -} - -function SheetOverlay({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function SheetContent({ - className, - children, - side = "right", - showCloseButton = true, - overlayClassName, - container, - ...props -}: React.ComponentProps & { - side?: "top" | "right" | "bottom" | "left"; - showCloseButton?: boolean; - overlayClassName?: string; - container?: HTMLElement | null; -}) { - return ( - - - - {children} - {showCloseButton && ( - - - - )} - - - ); -} - -function SheetHeader({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ); -} - -function SheetFooter({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ); -} - -function SheetTitle({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function SheetDescription({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} - -export { - Sheet, - SheetTrigger, - SheetClose, - SheetContent, - SheetHeader, - SheetFooter, - SheetTitle, - SheetDescription, -}; +import { Dialog as SheetPrimitive } from "radix-ui"; +import type * as React from "react"; + +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; +import { Cancel01Icon } from "@hugeicons/core-free-icons"; +import { HugeiconsIcon } from "@hugeicons/react"; + +function Sheet({ ...props }: React.ComponentProps) { + return ; +} + +function SheetTrigger({ + ...props +}: React.ComponentProps) { + return ; +} + +function SheetClose({ + ...props +}: React.ComponentProps) { + return ; +} + +function SheetPortal({ + ...props +}: React.ComponentProps) { + return ; +} + +function SheetOverlay({ + className, + position = "fixed", + ...props +}: React.ComponentProps & { + position?: "fixed" | "absolute"; +}) { + return ( + + ); +} + +function SheetContent({ + className, + children, + side = "right", + showCloseButton = true, + container, + position = "fixed", + overlayClassName, + overlayPosition, + ...props +}: React.ComponentProps & { + side?: "top" | "right" | "bottom" | "left"; + showCloseButton?: boolean; + container?: HTMLElement | null; + position?: "fixed" | "absolute"; + overlayClassName?: string; + overlayPosition?: "fixed" | "absolute"; +}) { + return ( + + + + {children} + {showCloseButton && ( + + + + )} + + + ); +} + +function SheetHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function SheetFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function SheetTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function SheetDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { + Sheet, + SheetTrigger, + SheetClose, + SheetContent, + SheetHeader, + SheetFooter, + SheetTitle, + SheetDescription, +}; diff --git a/studio/frontend/src/components/ui/shine-border.tsx b/studio/frontend/src/components/ui/shine-border.tsx new file mode 100644 index 0000000000..40b1fe8cf1 --- /dev/null +++ b/studio/frontend/src/components/ui/shine-border.tsx @@ -0,0 +1,61 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +interface ShineBorderProps extends React.HTMLAttributes { + /** + * Width of the border in pixels + * @default 1 + */ + borderWidth?: number + /** + * Duration of the animation in seconds + * @default 14 + */ + duration?: number + /** + * Color of the border, can be a single color or an array of colors + * @default "#000000" + */ + shineColor?: string | string[] +} + +/** + * Shine Border + * + * An animated background border effect component with configurable properties. + */ +export function ShineBorder({ + borderWidth = 1, + duration = 14, + shineColor = "#000000", + className, + style, + ...props +}: ShineBorderProps) { + return ( +
+ ) +} diff --git a/studio/frontend/src/components/ui/tooltip.tsx b/studio/frontend/src/components/ui/tooltip.tsx index 004a4b9e1a..6584cfaf95 100644 --- a/studio/frontend/src/components/ui/tooltip.tsx +++ b/studio/frontend/src/components/ui/tooltip.tsx @@ -43,10 +43,10 @@ function TooltipContent({ {children} diff --git a/studio/frontend/src/config/training.ts b/studio/frontend/src/config/training.ts index da60328d40..4df5107022 100644 --- a/studio/frontend/src/config/training.ts +++ b/studio/frontend/src/config/training.ts @@ -39,6 +39,11 @@ export const MODEL_TYPES: ReadonlyArray<{ label: string; description: string; }> = [ + { + value: "text", + label: "Text", + description: "Language models", + }, { value: "vision", label: "Vision", @@ -54,11 +59,6 @@ export const MODEL_TYPES: ReadonlyArray<{ label: "Embeddings", description: "Text embedding models", }, - { - value: "text", - label: "Text", - description: "Language models", - }, ]; export const CONTEXT_LENGTHS = [512, 1024, 2048, 4096, 8192, 16384, 32768]; @@ -103,7 +103,7 @@ export const DEFAULT_HYPERPARAMS = { warmupSteps: 5, maxSteps: 0, saveSteps: 0, - evalSteps: 0.01, + evalSteps: 0.00, packing: false, trainOnCompletions: false, gradientCheckpointing: "unsloth" as const, diff --git a/studio/frontend/src/features/chat/api/chat-api.ts b/studio/frontend/src/features/chat/api/chat-api.ts index 72baf9a6f6..5d5a9551ef 100644 --- a/studio/frontend/src/features/chat/api/chat-api.ts +++ b/studio/frontend/src/features/chat/api/chat-api.ts @@ -1,5 +1,6 @@ import { authFetch } from "@/features/auth"; import type { + GgufVariantsResponse, InferenceStatusResponse, ListLorasResponse, ListModelsResponse, @@ -74,6 +75,16 @@ export async function unloadModel(payload: UnloadModelRequest): Promise { await parseJsonOrThrow(response); } +export async function listGgufVariants( + repoId: string, + hfToken?: string, +): Promise { + const params = new URLSearchParams({ repo_id: repoId }); + if (hfToken) params.set("hf_token", hfToken); + const response = await authFetch(`/api/models/gguf-variants?${params}`); + return parseJsonOrThrow(response); +} + function parseSseEvent(rawEvent: string): string[] { const dataLines: string[] = []; for (const line of rawEvent.split(/\r?\n/)) { diff --git a/studio/frontend/src/features/chat/chat-page.tsx b/studio/frontend/src/features/chat/chat-page.tsx index c363704d61..14a457ad76 100644 --- a/studio/frontend/src/features/chat/chat-page.tsx +++ b/studio/frontend/src/features/chat/chat-page.tsx @@ -33,6 +33,7 @@ import { useRef, useState, } from "react"; +import { toast } from "sonner"; import { GuidedTour, useGuidedTourController } from "@/features/tour"; import { ChatSettingsPanel } from "./chat-settings-sheet"; import { db } from "./db"; @@ -50,7 +51,7 @@ import { SharedComposer, } from "./shared-composer"; import { ThreadSidebar } from "./thread-sidebar"; -import type { ChatView } from "./types"; +import type { ChatView, MessageRecord } from "./types"; import { buildChatTourSteps } from "./tour"; type LoraCandidate = { @@ -90,6 +91,40 @@ function pickBestLoraForBase( return partial ?? sorted[0]; } +function messageHasImage(message: MessageRecord): boolean { + const contentParts = Array.isArray(message.content) ? message.content : []; + if (contentParts.some((part) => part.type === "image")) { + return true; + } + const attachments = Array.isArray(message.attachments) ? message.attachments : []; + for (const attachment of attachments) { + const parts = Array.isArray(attachment.content) ? attachment.content : []; + for (const part of parts as Array<{ type?: string }>) { + if (part?.type === "image") { + return true; + } + } + } + return false; +} + +async function resolveActiveSingleThreadId(view: ChatView): Promise { + if (view.mode !== "single") { + return undefined; + } + if (view.threadId) { + return view.threadId; + } + + // New-thread flow keeps threadId undefined in local view state. + // Fall back to most recent regular base thread. + const candidates = await db.threads.where("modelType").equals("base").toArray(); + const latest = candidates + .filter((thread) => !thread.archived && !thread.pairId) + .sort((a, b) => b.createdAt - a.createdAt)[0]; + return latest?.id; +} + const SingleContent = memo(function SingleContent({ threadId, newThreadNonce, @@ -208,7 +243,7 @@ function InlineSidebar({ return (
@@ -279,6 +314,7 @@ export function ChatPage(): ReactElement { ); const inferenceParams = useChatRuntimeStore((state) => state.params); const setInferenceParams = useChatRuntimeStore((state) => state.setParams); + const activeGgufVariant = useChatRuntimeStore((state) => state.activeGgufVariant); const autoTitle = useChatRuntimeStore((state) => state.autoTitle); const setAutoTitle = useChatRuntimeStore((state) => state.setAutoTitle); const modelsFromStore = useChatRuntimeStore((state) => state.models); @@ -300,19 +336,47 @@ export function ChatPage(): ReactElement { }, [inferenceParams.checkpoint, lorasFromStore]); const handleCheckpointChange = useCallback( - (value: string, meta?: { isLora: boolean }) => { - const currentCheckpoint = - useChatRuntimeStore.getState().params.checkpoint; - if (!value || value === currentCheckpoint) return; - setView({ mode: "single", newThreadNonce: crypto.randomUUID() }); + (value: string, meta?: { isLora: boolean; ggufVariant?: string }) => { + const store = useChatRuntimeStore.getState(); + const currentCheckpoint = store.params.checkpoint; + const currentVariant = store.activeGgufVariant; + if (!value || (value === currentCheckpoint && (meta?.ggufVariant ?? null) === (currentVariant ?? null))) return; void (async () => { - if (currentCheckpoint) { - await ejectModel(); + let switchNote: string | undefined; + const activeThreadId = await resolveActiveSingleThreadId(view); + if (activeThreadId) { + const thread = await db.threads.get(activeThreadId); + if (thread?.modelId && thread.modelId !== value) { + const messages = await db.messages + .where("threadId") + .equals(activeThreadId) + .toArray(); + const hasImage = messages.some(messageHasImage); + const targetModel = modelsFromStore.find((model) => model.id === value); + const nonVisionWithImages = hasImage && targetModel?.isVision === false; + + switchNote = nonVisionWithImages + ? "Full chat history will be sent to the new model. This chat has images; text-only models may fail." + : hasImage + ? "Full chat history will be sent to the new model. This chat includes images." + : "Full chat history will be sent to the new model."; + } } - await selectModel({ id: value, isLora: meta?.isLora }); + + if (switchNote) { + toast.warning("Model changed for this chat", { + description: switchNote, + duration: 6000, + }); + } + await selectModel({ + id: value, + isLora: meta?.isLora, + ggufVariant: meta?.ggufVariant, + }); })(); }, - [selectModel, ejectModel], + [modelsFromStore, selectModel, view], ); const handleEject = useCallback(() => { void ejectModel(); @@ -361,36 +425,8 @@ export function ChatPage(): ReactElement { const handleThreadSelect = useCallback( (nextView: ChatView) => { setView(nextView); - - const threadId = - nextView.mode === "single" ? nextView.threadId : undefined; - const pairId = - nextView.mode === "compare" ? nextView.pairId : undefined; - - void (async () => { - let thread: import("./types").ThreadRecord | undefined; - if (threadId) { - thread = await db.threads.get(threadId); - } else if (pairId) { - thread = await db.threads - .where("pairId") - .equals(pairId) - .first(); - } - const threadModelId = thread?.modelId; - if (!threadModelId) return; - - const currentCheckpoint = - useChatRuntimeStore.getState().params.checkpoint; - if (threadModelId === currentCheckpoint) return; - - if (currentCheckpoint) { - await ejectModel(); - } - await selectModel({ id: threadModelId }); - })(); }, - [ejectModel, selectModel], + [], ); const models = useMemo( @@ -410,6 +446,8 @@ export function ChatPage(): ReactElement { name: lora.name, baseModel: lora.baseModel, updatedAt: lora.updatedAt, + source: lora.source, + exportType: lora.exportType, })), [lorasFromStore], ); @@ -555,6 +593,7 @@ export function ChatPage(): ReactElement { models={models} loraModels={loraModels} value={inferenceParams.checkpoint} + activeGgufVariant={activeGgufVariant} onValueChange={handleCheckpointChange} onEject={handleEject} variant="ghost" diff --git a/studio/frontend/src/features/chat/chat-settings-sheet.tsx b/studio/frontend/src/features/chat/chat-settings-sheet.tsx index 5a72e085ff..e70b24fb54 100644 --- a/studio/frontend/src/features/chat/chat-settings-sheet.tsx +++ b/studio/frontend/src/features/chat/chat-settings-sheet.tsx @@ -157,6 +157,7 @@ export function ChatSettingsPanel({ }: ChatSettingsPanelProps) { const [presets, setPresets] = useState(BUILTIN_PRESETS); const [activePreset, setActivePreset] = useState("Default"); + const isBuiltinPreset = BUILTIN_PRESETS.some((p) => p.name === activePreset); function set(key: K) { return (v: InferenceParams[K]) => onParamsChange({ ...params, [key]: v }); @@ -165,7 +166,11 @@ export function ChatSettingsPanel({ function applyPreset(name: string) { const p = presets.find((pr) => pr.name === name); if (p) { - onParamsChange({ ...p.params, systemPrompt: params.systemPrompt }); + onParamsChange({ + ...p.params, + systemPrompt: params.systemPrompt, + checkpoint: params.checkpoint, + }); setActivePreset(name); } } @@ -195,7 +200,7 @@ export function ChatSettingsPanel({ return (