From 5a4146d7b047d1bddb7f9bdda7254f1583f02acd Mon Sep 17 00:00:00 2001 From: Datta Nimmaturi Date: Mon, 4 May 2026 09:46:25 +0000 Subject: [PATCH 1/4] Respect GC for GRPO --- .github/workflows/release-desktop.yml | 226 - .github/workflows/studio-backend-ci.yml | 200 - .github/workflows/studio-frontend-ci.yml | 108 - .github/workflows/studio-inference-smoke.yml | 185 - .github/workflows/studio-tauri-smoke.yml | 105 - .github/workflows/wheel-smoke.yml | 124 - .gitignore | 17 +- .pre-commit-config.yaml | 2 +- README.md | 20 +- install.ps1 | 681 +- install.sh | 736 +- pyproject.toml | 1 - studio/Unsloth_Studio_Colab.ipynb | 2 +- studio/backend/auth/authentication.py | 50 +- studio/backend/auth/storage.py | 205 +- .../core/data_recipe/jobs/constants.py | 1 - .../backend/core/data_recipe/jobs/manager.py | 124 +- studio/backend/core/data_recipe/jobs/parse.py | 224 +- studio/backend/core/data_recipe/jobs/types.py | 25 - .../backend/core/data_recipe/jobs/worker.py | 29 +- .../data_recipe/local_callable_validators.py | 9 +- studio/backend/core/export/export.py | 287 +- studio/backend/core/export/orchestrator.py | 33 +- studio/backend/core/inference/audio_codecs.py | 12 +- studio/backend/core/inference/llama_cpp.py | 1197 +- .../core/inference/llama_server_args.py | 120 - .../backend/core/inference/mlx_inference.py | 395 - studio/backend/core/inference/orchestrator.py | 48 +- studio/backend/core/inference/worker.py | 92 - studio/backend/core/training/resume.py | 75 - studio/backend/core/training/trainer.py | 216 +- studio/backend/core/training/training.py | 82 +- studio/backend/core/training/worker.py | 734 +- studio/backend/loggers/config.py | 12 - studio/backend/loggers/handlers.py | 26 +- studio/backend/main.py | 84 +- studio/backend/models/auth.py | 6 - studio/backend/models/inference.py | 62 +- studio/backend/models/training.py | 19 +- .../data-designer-github-repo-seed/README.md | 73 - .../pyproject.toml | 25 - .../__init__.py | 7 - .../data_designer_github_repo_seed/config.py | 64 - .../data_designer_github_repo_seed/impl.py | 83 - .../data_designer_github_repo_seed/plugin.py | 10 - .../data_designer_github_repo_seed/scraper.py | 236 - .../scraper_impl/__init__.py | 2 - .../scraper_impl/gh_client.py | 248 - .../scraper_impl/queries.py | 685 - .../scraper_impl/scraper.py | 756 - .../scraper_impl/state_store.py | 105 - .../single-env/data-designer-deps.txt | 3 +- studio/backend/routes/__init__.py | 2 - studio/backend/routes/auth.py | 25 +- studio/backend/routes/data_recipe/jobs.py | 214 +- studio/backend/routes/data_recipe/seed.py | 12 - studio/backend/routes/data_recipe/validate.py | 85 - studio/backend/routes/inference.py | 924 +- studio/backend/routes/models.py | 335 +- studio/backend/routes/training.py | 40 - studio/backend/routes/training_history.py | 13 +- studio/backend/run.py | 53 +- studio/backend/state/tool_policy.py | 33 - studio/backend/storage/studio_db.py | 81 +- .../tests/test_data_recipe_github_progress.py | 91 - studio/backend/tests/test_desktop_auth.py | 598 - studio/backend/tests/test_gpu_selection.py | 326 +- studio/backend/tests/test_host_defaults.py | 98 - .../backend/tests/test_kv_cache_estimation.py | 1344 +- .../tests/test_llama_cpp_context_fit.py | 6 +- .../test_llama_cpp_max_context_threshold.py | 6 +- .../backend/tests/test_llama_server_args.py | 189 - .../tests/test_mlx_inference_backend.py | 157 - .../tests/test_mlx_training_worker_config.py | 83 - .../tests/test_native_context_length.py | 12 - .../tests/test_openai_tool_passthrough.py | 19 +- .../backend/tests/test_tool_policy_gates.py | 56 - .../backend/tests/test_tool_policy_state.py | 59 - .../tests/test_training_raw_support.py | 183 - .../tests/test_training_worker_flash_attn.py | 16 - studio/backend/tests/test_vision_cache.py | 45 +- studio/backend/tests/test_vram_estimation.py | 1674 -- .../backend/utils/datasets/dataset_utils.py | 30 - .../backend/utils/datasets/model_mappings.py | 4 - studio/backend/utils/datasets/raw_text.py | 142 - .../backend/utils/hardware/VRAM_ESTIMATION.md | 89 +- studio/backend/utils/hardware/amd.py | 2 - studio/backend/utils/hardware/hardware.py | 174 +- studio/backend/utils/hardware/nvidia.py | 13 - .../backend/utils/hardware/vram_estimation.py | 951 +- studio/backend/utils/models/model_config.py | 19 +- studio/backend/utils/native_path_leases.py | 406 - studio/backend/utils/paths/storage_roots.py | 44 +- studio/backend/utils/subprocess_compat.py | 34 - studio/backend/utils/transformers_version.py | 18 +- studio/backend/utils/wheel_utils.py | 5 - studio/frontend/package-lock.json | 16817 ---------------- studio/frontend/package.json | 15 +- studio/frontend/public/studio.png | Bin 27420 -> 0 bytes studio/frontend/src/app/auth-guards.ts | 62 +- studio/frontend/src/app/provider.tsx | 278 +- studio/frontend/src/app/routes/__root.tsx | 9 +- .../frontend/src/components/app-sidebar.tsx | 65 +- .../components/assistant-ui/markdown-text.tsx | 9 +- .../assistant-ui/model-selector.tsx | 72 +- .../model-selector/model-delete-action.tsx | 112 - .../assistant-ui/model-selector/pickers.tsx | 288 +- .../assistant-ui/model-selector/types.ts | 5 - .../src/components/assistant-ui/sources.tsx | 15 +- .../src/components/assistant-ui/thread.tsx | 144 +- .../src/components/tauri/startup-screen.tsx | 461 - .../src/components/tauri/update-banner.tsx | 146 - .../src/components/tauri/update-screen.tsx | 217 - .../src/components/tauri/window-titlebar.tsx | 329 - .../src/components/ui/shimmer-button.tsx | 96 - studio/frontend/src/config/env.ts | 33 +- studio/frontend/src/config/training.ts | 10 - studio/frontend/src/features/auth/api.ts | 89 +- .../features/auth/change-password-page.tsx | 2 +- .../features/auth/components/auth-form.tsx | 16 +- studio/frontend/src/features/auth/index.ts | 5 - .../frontend/src/features/auth/login-page.tsx | 2 +- studio/frontend/src/features/auth/session.ts | 2 - .../src/features/auth/tauri-auto-auth.ts | 106 - .../src/features/chat/api/chat-adapter.ts | 69 +- .../src/features/chat/api/chat-api.ts | 26 +- .../frontend/src/features/chat/chat-page.tsx | 162 +- .../src/features/chat/chat-settings-sheet.tsx | 738 +- .../chat/hooks/use-chat-model-runtime.ts | 288 +- .../chat/hooks/use-chat-search-index.ts | 3 - .../chat/hooks/use-chat-sidebar-items.ts | 2 - .../features/chat/presets/preset-policy.ts | 351 - .../src/features/chat/runtime-provider.tsx | 167 +- .../src/features/chat/shared-composer.tsx | 145 +- .../chat/stores/chat-runtime-store.ts | 83 +- .../frontend/src/features/chat/types/api.ts | 9 - .../chat/utils/chat-thread-tombstones.ts | 12 - .../chat/utils/delete-thread-message.ts | 35 +- .../src/features/chat/utils/qwen-params.ts | 29 - .../learning-recipes/github-support-bot.json | 238 - .../data-recipes/learning-recipes/index.ts | 11 - .../data-recipes/pages/data-recipes-page.tsx | 19 +- .../data-recipes/pages/edit-recipe-page.tsx | 2 +- .../frontend/src/features/export/constants.ts | 1 - .../src/features/export/export-page.tsx | 4 +- .../src/features/native-intents/api.ts | 46 - .../components/native-model-chip.tsx | 107 - .../components/native-model-drop-overlay.tsx | 70 - .../native-intents/native-intent-drain.tsx | 39 - .../src/features/native-intents/store.ts | 23 - .../src/features/native-intents/types.ts | 39 - .../native-intents/use-native-dialogs.ts | 68 - .../native-intents/use-native-drop.ts | 132 - .../native-intents/use-native-readiness.ts | 46 - .../components/steps/dataset-step.tsx | 1 - .../components/steps/model-selection-step.tsx | 1 - .../components/steps/summary-step.tsx | 6 +- .../onboarding/components/wizard-layout.tsx | 2 +- .../profile-personalization-panel.tsx | 2 +- .../profile/utils/resize-image-file.ts | 50 +- .../src/features/recipe-studio/api/index.ts | 70 +- .../recipe-studio/blocks/definitions.ts | 18 +- .../executions/execution-data-tab.tsx | 178 +- .../executions/execution-overview-tab.tsx | 66 - .../components/executions/executions-view.tsx | 87 +- .../components/inline/inline-seed.tsx | 83 +- .../components/recipe-studio-header.tsx | 11 +- .../runtime/execution-progress-island.tsx | 98 +- .../recipe-studio/dialogs/preview-dialog.tsx | 9 +- .../dialogs/seed/seed-dialog.tsx | 796 +- .../easy/github-crawler-easy-view.tsx | 191 - .../features/recipe-studio/execution-types.ts | 26 +- .../recipe-studio/executions/runtime.ts | 11 - .../hooks/use-recipe-executions.ts | 117 +- .../hooks/use-recipe-studio-actions.ts | 4 - .../recipe-studio/recipe-studio-page.tsx | 75 +- .../recipe-studio/stores/recipe-studio.ts | 10 +- .../src/features/recipe-studio/types/index.ts | 19 +- .../import/parsers/seed-config-parser.ts | 32 - .../features/recipe-studio/utils/node-data.ts | 4 +- .../utils/payload/builders-seed.ts | 108 +- .../recipe-studio/utils/payload/types.ts | 2 +- .../recipe-studio/utils/validation.ts | 51 +- .../src/features/settings/api/api-keys.ts | 6 +- .../settings/components/api-key-row.tsx | 2 +- .../settings/components/create-key-form.tsx | 8 +- .../settings/components/key-reveal-card.tsx | 4 +- .../settings/components/usage-examples.tsx | 140 +- .../src/features/settings/settings-dialog.tsx | 36 +- .../src/features/settings/tabs/about-tab.tsx | 20 +- .../features/settings/tabs/api-keys-tab.tsx | 25 +- .../features/settings/tabs/general-tab.tsx | 4 +- .../studio/historical-training-view.tsx | 22 +- .../src/features/studio/history-card-grid.tsx | 74 +- .../features/studio/live-training-view.tsx | 2 - .../sections/dataset-preview-dialog.tsx | 10 +- .../studio/sections/dataset-section.tsx | 113 +- .../studio/sections/model-section.tsx | 13 +- .../studio/sections/params-section.tsx | 76 +- .../studio/sections/progress-section.tsx | 4 +- .../src/features/studio/studio-page.tsx | 7 +- .../studio/training-start-overlay.tsx | 56 +- .../src/features/training/api/mappers.ts | 23 +- .../training/hooks/use-training-actions.ts | 76 +- .../hooks/use-training-runtime-lifecycle.ts | 103 +- .../features/training/lib/model-defaults.ts | 9 +- .../features/training/lib/training-methods.ts | 48 - .../training/stores/training-config-store.ts | 207 +- .../training/stores/training-runtime-store.ts | 8 - .../src/features/training/types/api.ts | 3 - .../src/features/training/types/config.ts | 2 - .../src/features/training/types/history.ts | 2 - .../src/features/training/types/runtime.ts | 11 - studio/frontend/src/hooks/index.ts | 1 - studio/frontend/src/hooks/use-gpu-info.ts | 3 +- .../frontend/src/hooks/use-hf-model-search.ts | 24 +- .../frontend/src/hooks/use-tauri-backend.ts | 581 - studio/frontend/src/hooks/use-tauri-update.ts | 310 - studio/frontend/src/lib/api-base.ts | 30 - studio/frontend/src/lib/latex.ts | 117 - .../frontend/src/lib/native-notifications.ts | 234 - studio/frontend/src/lib/open-link.ts | 31 - studio/frontend/src/lib/tauri-diagnostics.ts | 176 - studio/frontend/src/lib/vram.ts | 16 +- studio/frontend/src/types/training.ts | 8 +- studio/install_llama_prebuilt.py | 29 +- studio/install_python_stack.py | 156 +- studio/setup.ps1 | 296 +- studio/setup.sh | 143 +- studio/src-tauri/Cargo.lock | 6839 ------- studio/src-tauri/Cargo.toml | 48 - studio/src-tauri/Entitlements.plist | 15 - studio/src-tauri/build.rs | 3 - studio/src-tauri/capabilities/default.json | 33 - studio/src-tauri/icons/128x128.png | Bin 9194 -> 0 bytes studio/src-tauri/icons/32x32.png | Bin 1930 -> 0 bytes studio/src-tauri/icons/icon.icns | Bin 263568 -> 0 bytes studio/src-tauri/icons/icon.ico | Bin 34589 -> 0 bytes studio/src-tauri/icons/icon.png | Bin 46007 -> 0 bytes studio/src-tauri/linux/postremove.sh | 9 - studio/src-tauri/src/commands.rs | 644 - studio/src-tauri/src/desktop_auth.rs | 510 - studio/src-tauri/src/diagnostics/mod.rs | 169 - studio/src-tauri/src/diagnostics/phase_log.rs | 823 - studio/src-tauri/src/diagnostics/redaction.rs | 217 - studio/src-tauri/src/diagnostics/report.rs | 607 - studio/src-tauri/src/diagnostics/state.rs | 774 - studio/src-tauri/src/install.rs | 900 - studio/src-tauri/src/main.rs | 229 - studio/src-tauri/src/native_backend_lease.rs | 199 - studio/src-tauri/src/native_intents.rs | 476 - studio/src-tauri/src/native_path_policy.rs | 291 - studio/src-tauri/src/preflight.rs | 841 - studio/src-tauri/src/process.rs | 664 - studio/src-tauri/src/update.rs | 419 - studio/src-tauri/src/windows_job.rs | 72 - studio/src-tauri/tauri.conf.json | 83 - studio/src-tauri/tauri.macos.conf.json | 11 - studio/src-tauri/tauri.windows.conf.json | 10 - .../windows/branding/nsis-header.bmp | Bin 102654 -> 0 bytes .../windows/branding/nsis-sidebar.bmp | Bin 618006 -> 0 bytes studio/src-tauri/windows/hooks.nsh | 9 - studio/src-tauri/windows/installer.nsi | 994 - tests/conftest.py | 141 - .../test_dpo_vision_processor_passthrough.py | 149 - ...sentence_transformer_redirect_lifecycle.py | 252 - tests/python/test_gpu_init_ldconfig_guard.py | 46 - .../test_unsloth_run_tool_policy_resolver.py | 201 - .../saving/non_peft/test_mistral_non_peft.py | 2 +- .../saving/non_peft/test_whisper_non_peft.py | 2 +- .../test_fix_sentencepiece_gguf_robustness.py | 132 - .../test_index_file_sharded_model.py | 4 +- .../vision_models/test_push_to_hub_merged.py | 4 +- ..._merge_qwen2.5vl32B_model_ocr_benchmark.py | 4 +- ...t_save_merge_vision_model_ocr_benchmark.py | 4 +- tests/sh/test_install_host_defaults.sh | 104 - tests/sh/test_tauri_install_exit_order.sh | 95 - .../test_install_llama_prebuilt_logic.py | 3 - tests/studio/install/test_rocm_support.py | 162 +- tests/studio/install/test_selection_logic.py | 17 - tests/studio/test_cancel_atomicity.py | 289 - tests/studio/test_cancel_id_wiring.py | 169 - .../test_chat_preset_builtin_invariants.py | 272 - tests/studio/test_cli_repo_variant.py | 145 - tests/studio/test_cli_run_alias.py | 69 - tests/studio/test_cli_studio_defaults.py | 87 - .../test_export_output_path_contract.py | 122 - tests/studio/test_hardware_dispatch_matrix.py | 395 - tests/studio/test_is_mlx_dispatch_gate.py | 213 - tests/studio/test_llama_cpp_wall_clock_cap.py | 123 - .../test_mlx_training_worker_behaviors.py | 90 - .../test_stream_cancel_registration_timing.py | 718 - .../test_studio_gguf_export_script_pin.py | 231 - .../test_studio_text_descender_clipping.py | 69 - tests/test_peft_weight_converter_compat.py | 259 - tests/test_raw_text.py | 5 +- tests/test_resolve_model_class.py | 137 - tests/test_studio_install_workspace_guard.py | 1021 - tests/test_studio_root_resilience.py | 154 - unsloth/__init__.py | 428 +- unsloth/_gpu_init.py | 346 - unsloth/import_fixes.py | 83 - unsloth/kernels/fp8.py | 3 +- unsloth/kernels/utils.py | 75 +- unsloth/models/_utils.py | 32 +- unsloth/models/rl_replacements.py | 365 +- unsloth/models/sentence_transformer.py | 187 +- unsloth/save.py | 35 +- unsloth/tokenizer_utils.py | 72 +- unsloth/trainer.py | 4 +- unsloth_cli/__init__.py | 14 +- unsloth_cli/_tool_policy.py | 71 - unsloth_cli/commands/studio.py | 586 +- 313 files changed, 3000 insertions(+), 66427 deletions(-) delete mode 100644 .github/workflows/release-desktop.yml delete mode 100644 .github/workflows/studio-backend-ci.yml delete mode 100644 .github/workflows/studio-frontend-ci.yml delete mode 100644 .github/workflows/studio-inference-smoke.yml delete mode 100644 .github/workflows/studio-tauri-smoke.yml delete mode 100644 .github/workflows/wheel-smoke.yml delete mode 100644 studio/backend/core/inference/llama_server_args.py delete mode 100644 studio/backend/core/inference/mlx_inference.py delete mode 100644 studio/backend/core/training/resume.py delete mode 100644 studio/backend/plugins/data-designer-github-repo-seed/README.md delete mode 100644 studio/backend/plugins/data-designer-github-repo-seed/pyproject.toml delete mode 100644 studio/backend/plugins/data-designer-github-repo-seed/src/data_designer_github_repo_seed/__init__.py delete mode 100644 studio/backend/plugins/data-designer-github-repo-seed/src/data_designer_github_repo_seed/config.py delete mode 100644 studio/backend/plugins/data-designer-github-repo-seed/src/data_designer_github_repo_seed/impl.py delete mode 100644 studio/backend/plugins/data-designer-github-repo-seed/src/data_designer_github_repo_seed/plugin.py delete mode 100644 studio/backend/plugins/data-designer-github-repo-seed/src/data_designer_github_repo_seed/scraper.py delete mode 100644 studio/backend/plugins/data-designer-github-repo-seed/src/data_designer_github_repo_seed/scraper_impl/__init__.py delete mode 100644 studio/backend/plugins/data-designer-github-repo-seed/src/data_designer_github_repo_seed/scraper_impl/gh_client.py delete mode 100644 studio/backend/plugins/data-designer-github-repo-seed/src/data_designer_github_repo_seed/scraper_impl/queries.py delete mode 100644 studio/backend/plugins/data-designer-github-repo-seed/src/data_designer_github_repo_seed/scraper_impl/scraper.py delete mode 100644 studio/backend/plugins/data-designer-github-repo-seed/src/data_designer_github_repo_seed/scraper_impl/state_store.py delete mode 100644 studio/backend/state/tool_policy.py delete mode 100644 studio/backend/tests/test_data_recipe_github_progress.py delete mode 100644 studio/backend/tests/test_desktop_auth.py delete mode 100644 studio/backend/tests/test_host_defaults.py delete mode 100644 studio/backend/tests/test_llama_server_args.py delete mode 100644 studio/backend/tests/test_mlx_inference_backend.py delete mode 100644 studio/backend/tests/test_mlx_training_worker_config.py delete mode 100644 studio/backend/tests/test_tool_policy_gates.py delete mode 100644 studio/backend/tests/test_tool_policy_state.py delete mode 100644 studio/backend/tests/test_training_raw_support.py delete mode 100644 studio/backend/utils/datasets/raw_text.py delete mode 100644 studio/backend/utils/native_path_leases.py delete mode 100644 studio/backend/utils/subprocess_compat.py delete mode 100644 studio/frontend/package-lock.json delete mode 100644 studio/frontend/public/studio.png delete mode 100644 studio/frontend/src/components/assistant-ui/model-selector/model-delete-action.tsx delete mode 100644 studio/frontend/src/components/tauri/startup-screen.tsx delete mode 100644 studio/frontend/src/components/tauri/update-banner.tsx delete mode 100644 studio/frontend/src/components/tauri/update-screen.tsx delete mode 100644 studio/frontend/src/components/tauri/window-titlebar.tsx delete mode 100644 studio/frontend/src/components/ui/shimmer-button.tsx delete mode 100644 studio/frontend/src/features/auth/tauri-auto-auth.ts delete mode 100644 studio/frontend/src/features/chat/presets/preset-policy.ts delete mode 100644 studio/frontend/src/features/chat/utils/chat-thread-tombstones.ts delete mode 100644 studio/frontend/src/features/chat/utils/qwen-params.ts delete mode 100644 studio/frontend/src/features/data-recipes/learning-recipes/github-support-bot.json delete mode 100644 studio/frontend/src/features/native-intents/api.ts delete mode 100644 studio/frontend/src/features/native-intents/components/native-model-chip.tsx delete mode 100644 studio/frontend/src/features/native-intents/components/native-model-drop-overlay.tsx delete mode 100644 studio/frontend/src/features/native-intents/native-intent-drain.tsx delete mode 100644 studio/frontend/src/features/native-intents/store.ts delete mode 100644 studio/frontend/src/features/native-intents/types.ts delete mode 100644 studio/frontend/src/features/native-intents/use-native-dialogs.ts delete mode 100644 studio/frontend/src/features/native-intents/use-native-drop.ts delete mode 100644 studio/frontend/src/features/native-intents/use-native-readiness.ts delete mode 100644 studio/frontend/src/features/recipe-studio/easy/github-crawler-easy-view.tsx delete mode 100644 studio/frontend/src/features/training/lib/training-methods.ts delete mode 100644 studio/frontend/src/hooks/use-tauri-backend.ts delete mode 100644 studio/frontend/src/hooks/use-tauri-update.ts delete mode 100644 studio/frontend/src/lib/api-base.ts delete mode 100644 studio/frontend/src/lib/native-notifications.ts delete mode 100644 studio/frontend/src/lib/open-link.ts delete mode 100644 studio/frontend/src/lib/tauri-diagnostics.ts delete mode 100644 studio/src-tauri/Cargo.lock delete mode 100644 studio/src-tauri/Cargo.toml delete mode 100644 studio/src-tauri/Entitlements.plist delete mode 100644 studio/src-tauri/build.rs delete mode 100644 studio/src-tauri/capabilities/default.json delete mode 100644 studio/src-tauri/icons/128x128.png delete mode 100644 studio/src-tauri/icons/32x32.png delete mode 100644 studio/src-tauri/icons/icon.icns delete mode 100644 studio/src-tauri/icons/icon.ico delete mode 100644 studio/src-tauri/icons/icon.png delete mode 100755 studio/src-tauri/linux/postremove.sh delete mode 100644 studio/src-tauri/src/commands.rs delete mode 100644 studio/src-tauri/src/desktop_auth.rs delete mode 100644 studio/src-tauri/src/diagnostics/mod.rs delete mode 100644 studio/src-tauri/src/diagnostics/phase_log.rs delete mode 100644 studio/src-tauri/src/diagnostics/redaction.rs delete mode 100644 studio/src-tauri/src/diagnostics/report.rs delete mode 100644 studio/src-tauri/src/diagnostics/state.rs delete mode 100644 studio/src-tauri/src/install.rs delete mode 100644 studio/src-tauri/src/main.rs delete mode 100644 studio/src-tauri/src/native_backend_lease.rs delete mode 100644 studio/src-tauri/src/native_intents.rs delete mode 100644 studio/src-tauri/src/native_path_policy.rs delete mode 100644 studio/src-tauri/src/preflight.rs delete mode 100644 studio/src-tauri/src/process.rs delete mode 100644 studio/src-tauri/src/update.rs delete mode 100644 studio/src-tauri/src/windows_job.rs delete mode 100644 studio/src-tauri/tauri.conf.json delete mode 100644 studio/src-tauri/tauri.macos.conf.json delete mode 100644 studio/src-tauri/tauri.windows.conf.json delete mode 100644 studio/src-tauri/windows/branding/nsis-header.bmp delete mode 100644 studio/src-tauri/windows/branding/nsis-sidebar.bmp delete mode 100644 studio/src-tauri/windows/hooks.nsh delete mode 100644 studio/src-tauri/windows/installer.nsi delete mode 100644 tests/conftest.py delete mode 100644 tests/python/test_dpo_vision_processor_passthrough.py delete mode 100644 tests/python/test_fast_sentence_transformer_redirect_lifecycle.py delete mode 100644 tests/python/test_gpu_init_ldconfig_guard.py delete mode 100644 tests/python/test_unsloth_run_tool_policy_resolver.py delete mode 100644 tests/saving/test_fix_sentencepiece_gguf_robustness.py delete mode 100755 tests/sh/test_install_host_defaults.sh delete mode 100644 tests/sh/test_tauri_install_exit_order.sh delete mode 100644 tests/studio/test_cancel_atomicity.py delete mode 100644 tests/studio/test_cancel_id_wiring.py delete mode 100644 tests/studio/test_chat_preset_builtin_invariants.py delete mode 100644 tests/studio/test_cli_repo_variant.py delete mode 100644 tests/studio/test_cli_run_alias.py delete mode 100644 tests/studio/test_cli_studio_defaults.py delete mode 100644 tests/studio/test_export_output_path_contract.py delete mode 100644 tests/studio/test_hardware_dispatch_matrix.py delete mode 100644 tests/studio/test_is_mlx_dispatch_gate.py delete mode 100644 tests/studio/test_llama_cpp_wall_clock_cap.py delete mode 100644 tests/studio/test_mlx_training_worker_behaviors.py delete mode 100644 tests/studio/test_stream_cancel_registration_timing.py delete mode 100644 tests/studio/test_studio_gguf_export_script_pin.py delete mode 100644 tests/studio/test_studio_text_descender_clipping.py delete mode 100644 tests/test_peft_weight_converter_compat.py delete mode 100644 tests/test_resolve_model_class.py delete mode 100644 tests/test_studio_install_workspace_guard.py delete mode 100644 tests/test_studio_root_resilience.py delete mode 100644 unsloth/_gpu_init.py delete mode 100644 unsloth_cli/_tool_policy.py diff --git a/.github/workflows/release-desktop.yml b/.github/workflows/release-desktop.yml deleted file mode 100644 index ea82739968..0000000000 --- a/.github/workflows/release-desktop.yml +++ /dev/null @@ -1,226 +0,0 @@ -name: Release Desktop App - -on: - workflow_dispatch: - inputs: - draft: - description: 'Create as draft release' - type: boolean - default: true - -permissions: - contents: write - -jobs: - build: - strategy: - fail-fast: false - max-parallel: 1 - matrix: - include: - - platform: macos-latest - args: '--target aarch64-apple-darwin' - label: macOS (Apple Silicon) - # - platform: macos-latest - # args: '--target x86_64-apple-darwin' - # label: macOS (Intel) - - platform: ubuntu-22.04 - args: '' - label: Linux (x64) - - platform: windows-latest - args: '' - label: Windows (x64) - - name: Build ${{ matrix.label }} - runs-on: ${{ matrix.platform }} - - env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - - - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 - - # ── Linux dependencies ── - - name: Install Linux dependencies - if: matrix.platform == 'ubuntu-22.04' - run: | - sudo apt-get update - sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev libxdo-dev libssl-dev patchelf - - # ── Node.js ── - - name: Setup Node.js - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 - with: - node-version: 24 - - - name: Install pinned Tauri CLI - run: npm install --save-dev --prefix studio @tauri-apps/cli@2.10.1 - - - name: Verify pinned Tauri CLI - shell: bash - run: | - out="$(npx --prefix studio tauri --version)" - echo "$out" - if [ "$out" != "tauri-cli 2.10.1" ]; then - echo "Expected tauri-cli 2.10.1, got $out" >&2 - exit 1 - fi - - - name: Install frontend dependencies - working-directory: studio/frontend - run: npm install - - - name: Verify backend package is published - shell: bash - run: | - node <<'JS' - const { readFileSync } = require('node:fs'); - - (async () => { - const cargo = readFileSync('studio/src-tauri/Cargo.toml', 'utf8'); - const match = cargo.match(/^version\s*=\s*"([^"]+)"/m); - if (!match) throw new Error('Could not read desktop app version'); - - const appVersion = match[1]; - const response = await fetch(`https://pypi.org/pypi/unsloth/${appVersion}/json`); - if (!response.ok) { - const message = 'Publish unsloth=={app_version} to PyPI before the desktop release'; - throw new Error(`${message.replace('{app_version}', appVersion)} (HTTP ${response.status})`); - } - })(); - JS - - # ── Rust ── - - name: Install Rust stable - uses: dtolnay/rust-toolchain@stable - with: - targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }} - - - name: Rust cache - uses: swatinem/rust-cache@42dc69e1aa15d09112580998cf2ef0119e2e91ae - with: - workspaces: 'studio/src-tauri -> target' - - # ── macOS: import signing certificate ── - - name: Import Apple certificate - if: matrix.platform == 'macos-latest' - env: - APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} - APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} - KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} - run: | - echo $APPLE_CERTIFICATE | base64 --decode > certificate.p12 - security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain - security default-keychain -s build.keychain - security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain - security set-keychain-settings -t 3600 -u build.keychain - security import certificate.p12 -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign - security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain - security find-identity -v -p codesigning build.keychain - rm -f certificate.p12 - - # ── Windows: install Azure Trusted Signing CLI ── - - name: Install trusted-signing-cli - if: matrix.platform == 'windows-latest' - run: | - cargo install trusted-signing-cli --version 0.9.0 --locked - echo "$env:USERPROFILE\.cargo\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - - # ── Windows: verify signing CLI is accessible ── - - name: Verify trusted-signing-cli - if: matrix.platform == 'windows-latest' - run: | - Write-Output "PATH: $env:PATH" - Get-Command trusted-signing-cli -ErrorAction SilentlyContinue || Write-Output "trusted-signing-cli NOT in PATH" - trusted-signing-cli --version || Write-Output "trusted-signing-cli failed to run" - - # ── Linux: build + sign + upload ── - - name: Build Linux app - if: matrix.platform == 'ubuntu-22.04' - uses: tauri-apps/tauri-action@84b9d35b5fc46c1e45415bdb6144030364f7ebc5 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} - TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} - with: - projectPath: studio - tauriScript: npx --prefix . tauri - tagName: desktop-v__VERSION__ - releaseName: 'Unsloth Studio (Desktop) v__VERSION__' - releaseBody: | - Desktop app for Unsloth Studio. - - **macOS**: Download the Apple Silicon `.dmg`. - **Windows**: Download the `-setup.exe` installer. - **Linux**: Download `.deb` (Ubuntu/Debian) or `.AppImage` (universal). - - > Linux in-app updates are AppImage-oriented. Package installs should update by downloading a new package. - > Linux AppImage on Ubuntu 24.04+ may require: `sudo apt install libfuse2t64` - > First-run system dependency elevation is supported on Ubuntu/Debian. Other Linux distributions should install system packages manually. - releaseDraft: ${{ inputs.draft }} - prerelease: false - args: -v ${{ matrix.args }} - - # ── macOS: build + sign + notarize + upload ── - - name: Build macOS app - if: matrix.platform == 'macos-latest' - uses: tauri-apps/tauri-action@84b9d35b5fc46c1e45415bdb6144030364f7ebc5 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} - TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} - APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} - APPLE_ID: ${{ secrets.APPLE_ID }} - APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} - APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} - with: - projectPath: studio - tauriScript: npx --prefix . tauri - tagName: desktop-v__VERSION__ - releaseName: 'Unsloth Studio (Desktop) v__VERSION__' - releaseBody: | - Desktop app for Unsloth Studio. - - **macOS**: Download the Apple Silicon `.dmg`. - **Windows**: Download the `-setup.exe` installer. - **Linux**: Download `.deb` (Ubuntu/Debian) or `.AppImage` (universal). - - > Linux in-app updates are AppImage-oriented. Package installs should update by downloading a new package. - > Linux AppImage on Ubuntu 24.04+ may require: `sudo apt install libfuse2t64` - > First-run system dependency elevation is supported on Ubuntu/Debian. Other Linux distributions should install system packages manually. - releaseDraft: ${{ inputs.draft }} - prerelease: false - args: -v ${{ matrix.args }} - - # ── Windows: build + sign + upload ── - - name: Build Windows app - if: matrix.platform == 'windows-latest' - uses: tauri-apps/tauri-action@84b9d35b5fc46c1e45415bdb6144030364f7ebc5 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} - TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} - AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} - AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} - AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} - AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }} - AZURE_CERTIFICATE_PROFILE_NAME: ${{ secrets.AZURE_CERTIFICATE_PROFILE_NAME }} - with: - projectPath: studio - tauriScript: npx --prefix . tauri - tagName: desktop-v__VERSION__ - releaseName: 'Unsloth Studio (Desktop) v__VERSION__' - releaseBody: | - Desktop app for Unsloth Studio. - - **macOS**: Download the Apple Silicon `.dmg`. - **Windows**: Download the `-setup.exe` installer. - **Linux**: Download `.deb` (Ubuntu/Debian) or `.AppImage` (universal). - - > Linux in-app updates are AppImage-oriented. Package installs should update by downloading a new package. - > Linux AppImage on Ubuntu 24.04+ may require: `sudo apt install libfuse2t64` - > First-run system dependency elevation is supported on Ubuntu/Debian. Other Linux distributions should install system packages manually. - releaseDraft: ${{ inputs.draft }} - prerelease: false - args: -v ${{ matrix.args }} diff --git a/.github/workflows/studio-backend-ci.yml b/.github/workflows/studio-backend-ci.yml deleted file mode 100644 index 5a858888e7..0000000000 --- a/.github/workflows/studio-backend-ci.yml +++ /dev/null @@ -1,200 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. - -# Runs the existing studio/backend/tests/ suite (~860 tests, all CPU-friendly) -# on every PR that touches the backend or unsloth library. Until this lands, -# none of those tests run automatically. Verified locally on Python 3.13 with -# the surgical exclusions below: 861 pass, 4 skipped. -# -# Exclusions: -# - tests/test_studio_api.py: end-to-end against a live model + GGUF download, -# too heavy for free runners. Run separately when GPU CI is available. -# - -k 'not llama_cpp_load_progress_live': spawns a real llama.cpp process, -# not appropriate for CPU-only runners. -# -# ruff is non-blocking initially; remove `|| true` once the backend lints clean. - -name: Backend CI - -on: - pull_request: - paths: - - 'studio/**' - - 'unsloth/**' - - 'unsloth_cli/**' - - 'tests/**' - - 'pyproject.toml' - - '.github/workflows/studio-backend-ci.yml' - push: - branches: [main, pip] - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - pytest: - name: (Python ${{ matrix.python }}) - runs-on: ubuntu-latest - timeout-minutes: 15 - strategy: - fail-fast: false - matrix: - python: ['3.10', '3.11', '3.12', '3.13'] - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-python@v5 - with: - python-version: '${{ matrix.python }}' - cache: 'pip' - - - name: Install backend test dependencies (CPU only) - run: | - python -m pip install --upgrade pip - # Studio's declared backend deps: - pip install -r studio/backend/requirements/studio.txt - # Extras that studio.txt does not list but the import chain needs - # (python-multipart for FastAPI form/file uploads, sqlalchemy/cryptography - # for the auth DB, yaml/jinja2 for utils.models.model_config, etc.): - pip install \ - python-multipart aiofiles sqlalchemy cryptography \ - pyyaml jinja2 mammoth unpdf requests \ - 'numpy<3' pytest pytest-asyncio httpx - # Torch CPU + transformers are required by a chunk of the backend test - # suite (gpu_selection, kv_cache_estimation, utils). CPU-only torch - # keeps the install ~250 MB / ~1 min on a clean runner. - pip install --index-url https://download.pytorch.org/whl/cpu 'torch>=2.4,<2.11' - pip install 'transformers>=4.51,<5.5' - - - name: Backend tests - working-directory: studio/backend - # Locally validated against this dep set: 831 passed, 5 skipped, 35 deselected. - # Deselections (all environment-specific, would never pass on a GPU-less - # `ubuntu-latest` runner regardless of code correctness): - # - llama_cpp_load_progress_live: spawns a real llama.cpp process - # - TestGpuAutoSelection / TestPreSpawnGpuResolution / TestPerGpuFitGuardAllCounts: - # require live transformers config introspection on real GPUs - # - TestTransformersIntrospection: same - # - test_returns_cuda_when_cuda_available / test_calls_cuda_cache_when_cuda: - # assume CUDA-capable GPU - run: | - python -m pytest tests/ -q --tb=short \ - --ignore=tests/test_studio_api.py \ - -k 'not llama_cpp_load_progress_live and not TestGpuAutoSelection and not TestPreSpawnGpuResolution and not TestPerGpuFitGuardAllCounts and not TestTransformersIntrospection and not test_returns_cuda_when_cuda_available and not test_calls_cuda_cache_when_cuda' - - repo-cpu-tests: - # Auto-discover everything under tests/ that is not GPU-bound by - # design. New tests added in covered directories are picked up - # without a workflow edit. Locally validated: 779 passed, 11 - # skipped, 23 deselected. tests/conftest.py (mirroring unsloth-zoo - # PR #624) pre-loads unsloth_zoo.device_type and unsloth.device_type - # under a mocked torch.cuda.is_available so the unsloth import - # chain succeeds on CPU. - name: Repo tests (CPU) - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-python@v5 - with: - python-version: '3.12' - cache: 'pip' - - - name: Install deps (shared shape with backend pytest job) - run: | - python -m pip install --upgrade pip - pip install -r studio/backend/requirements/studio.txt - pip install \ - python-multipart aiofiles sqlalchemy cryptography \ - pyyaml jinja2 mammoth unpdf requests typer \ - 'numpy<3' pytest pytest-asyncio httpx - # torchvision is needed because unsloth_zoo.vision_utils imports - # it at module scope and is reached via unsloth.models._utils. - pip install --index-url https://download.pytorch.org/whl/cpu \ - 'torch>=2.4,<2.11' 'torchvision<0.26' - pip install 'transformers>=4.51,<5.5' - # bitsandbytes is a hard import in unsloth/models/_utils.py. - # Recent versions ship a CPU build so it installs on a free - # Linux runner; the kernels still raise on use, but import - # succeeds and the package collects. - pip install 'bitsandbytes>=0.45' - # unsloth.device_type imports unsloth_zoo.utils.Version at module - # scope, so the conftest harness needs unsloth_zoo on the path - # even though it is an optional dep of unsloth. - pip install 'unsloth_zoo>=2026.5.1' - pip install -e . --no-deps - - - name: Repo tests (CPU, auto-discovered) - env: - # tests/python/* import install_python_stack from studio/. - PYTHONPATH: ${{ github.workspace }}/studio - # Skip lazy compilation work the unsloth import chain wants to - # do at import time on a real GPU. - UNSLOTH_COMPILE_DISABLE: '1' - # --ignore: GPU-bound directories (qlora and saving need real - # weights / GPU; tests/sh is a shell suite the next step - # handles; tests/utils is a helpers folder, not tests). - # State-sensitive hardware-spoofing files are pulled out and run - # in isolation in the next step because they mutate - # hardware.py module globals (IS_ROCM / DEVICE) and pollute - # downstream tests. - # -m: honour markers already declared in tests/python/conftest.py - # (`server` = needs studio venv, `e2e` = needs network). - # --deselect: two registry tests that hit huggingface_hub for - # live model existence checks; they belong on a network job. - run: | - python -m pytest tests/ -q --tb=short \ - --ignore=tests/qlora \ - --ignore=tests/saving \ - --ignore=tests/utils \ - --ignore=tests/sh \ - --ignore=tests/studio/test_hardware_dispatch_matrix.py \ - --ignore=tests/studio/test_is_mlx_dispatch_gate.py \ - -m 'not server and not e2e' \ - --deselect tests/test_model_registry.py::test_model_registration \ - --deselect tests/test_model_registry.py::test_all_model_registration - - - name: Hardware-spoof tests (state-sensitive, run in isolation) - env: - PYTHONPATH: ${{ github.workspace }}/studio - UNSLOTH_COMPILE_DISABLE: '1' - # These two files mutate hardware.py module globals at runtime - # via the spoof fixtures, which leaks state into any other test - # that imports hardware. Run them in their own pytest invocation - # so the leak does not cross file boundaries. - run: | - python -m pytest -q --tb=short \ - tests/studio/test_hardware_dispatch_matrix.py \ - tests/studio/test_is_mlx_dispatch_gate.py - - - name: Shell installer tests - # Subset that does not depend on a writable / pristine install.sh - # tree; test_install_host_defaults.sh checks install.ps1 layout - # which has drifted (separate followup). - run: | - set -e - for s in \ - tests/sh/test_get_torch_index_url.sh \ - tests/sh/test_mac_intel_compat.sh \ - tests/sh/test_tauri_install_exit_order.sh \ - tests/sh/test_torch_constraint.sh; do - echo "::group::$s" - bash "$s" - echo "::endgroup::" - done - - ruff: - name: Backend ruff lint (non-blocking) - runs-on: ubuntu-latest - timeout-minutes: 5 - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: '3.12' - cache: 'pip' - - run: pip install ruff - - name: ruff check (non-blocking until accumulated drift is cleared) - run: ruff check studio/backend || true diff --git a/.github/workflows/studio-frontend-ci.yml b/.github/workflows/studio-frontend-ci.yml deleted file mode 100644 index 039bd5dd08..0000000000 --- a/.github/workflows/studio-frontend-ci.yml +++ /dev/null @@ -1,108 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. - -# Frontend PR gate: lockfile freshness, typecheck, build, and a bundle grep -# that catches the 2026.5.1 chat-history regression at the JS level. -# -# biome runs as non-blocking for now: the codebase currently has accumulated -# ~470 errors and ~1650 warnings against the existing biome config. Surfacing -# the count in CI lets us drive it down without forcing a fleet-wide cleanup -# in the same PR. Drop `continue-on-error` once that number is zero. - -name: Frontend CI - -on: - pull_request: - paths: - - 'studio/frontend/**' - - '.github/workflows/studio-frontend-ci.yml' - push: - branches: [main, pip] - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - build: - name: Frontend build + bundle sanity - runs-on: ubuntu-latest - timeout-minutes: 10 - defaults: - run: - working-directory: studio/frontend - steps: - - uses: actions/checkout@v4 - - # FIXME: drop this step once @assistant-ui/* and assistant-stream - # leave 0.x -- on 1.x, caret ranges are conventional. Until then, - # every 0.minor on this surface is a SemVer-major (this is exactly - # how 2026.5.1 shipped a broken chat runtime: ^0.12.19 quietly - # resolved to 0.12.28). - - name: '@assistant-ui must be pinned exactly (no caret/tilde)' - working-directory: ${{ github.workspace }} - run: | - set -e - if grep -nE '"(@assistant-ui/[a-z-]+|assistant-stream)":[[:space:]]*"[\^~]' studio/frontend/package.json; then - echo "::error file=studio/frontend/package.json::These packages must be pinned to exact versions until they leave 0.x. Drop the leading ^ or ~." - exit 1 - fi - echo "All assistant-ui packages are pinned exactly." - - - uses: actions/setup-node@v4 - with: - node-version: '22' - cache: 'npm' - cache-dependency-path: studio/frontend/package-lock.json - - - name: Lockfile must agree with package.json (npm ci is strict) - run: npm ci --no-fund --no-audit - - - name: npm ci must not have modified the working tree - working-directory: ${{ github.workspace }} - run: | - if ! git diff --quiet -- studio/frontend; then - echo "::error::npm ci modified files; commit the updated lockfile" - git status -- studio/frontend - exit 1 - fi - - - name: Typecheck - run: npm run typecheck - - - name: Build - run: npm run build - - - name: Built bundle must not contain Studio's unstable_Provider call site - run: | - set -e - JS=$(ls dist/assets/index-*.js | head -1) - HITS=$(grep -c 'unstable_Provider:' "$JS" || echo 0) - echo "main bundle: $JS" - echo "unstable_Provider: hits=$HITS (assistant-ui internals contribute up to 3)" - if [ "$HITS" -gt 3 ]; then - echo "::error file=studio/frontend/src/features/chat/runtime-provider.tsx::Studio bundle still passes unstable_Provider through useRemoteThreadListRuntime; this is the 2026.5.1 chat-history regression. Pass adapters directly into useLocalRuntime instead." - exit 1 - fi - - - name: Bundle size budget (75 MB) - run: | - SIZE=$(du -sb dist | cut -f1) - BUDGET=$((75 * 1024 * 1024)) - echo "dist size: $SIZE bytes ($((SIZE/1024/1024)) MB), budget: $BUDGET bytes (75 MB)" - if [ "$SIZE" -gt "$BUDGET" ]; then - echo "::error::studio/frontend/dist/ exceeded the 75 MB budget. Drop dead deps (e.g. the unused next dep) or split chunks." - exit 1 - fi - - - name: Biome (non-blocking until accumulated drift is cleared) - continue-on-error: true - run: npm run biome:check - - - name: Upload built dist on failure - if: failure() - uses: actions/upload-artifact@v4 - with: - name: studio-frontend-dist - path: studio/frontend/dist - retention-days: 3 diff --git a/.github/workflows/studio-inference-smoke.yml b/.github/workflows/studio-inference-smoke.yml deleted file mode 100644 index 8efe072d28..0000000000 --- a/.github/workflows/studio-inference-smoke.yml +++ /dev/null @@ -1,185 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. - -# End-to-end smoke: install Studio via install.sh --local --no-torch, download -# a tiny GGUF, boot Studio, log in, change password, load the model, send a -# chat completion, assert a non-empty response. Only workflow that tests "the -# app actually works". -# -# Model: Qwen3.5-2B UD-IQ3_XXS (~890 MiB) -- small enough that the cache miss -# is cheap and inference fits in the 25 min CPU-runner budget. GGUF is cached -# across runs via actions/cache. - -name: Studio GGUF CI - -on: - pull_request: - paths: - - 'studio/**' - - 'unsloth/**' - - 'unsloth_cli/**' - - 'install.sh' - - 'pyproject.toml' - - '.github/workflows/studio-inference-smoke.yml' - push: - branches: [main, pip] - # Manual trigger for pre-warming the GGUF cache on main, or re-running - # against an arbitrary branch without pushing a no-op commit. - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -env: - GGUF_REPO: unsloth/Qwen3.5-2B-GGUF - GGUF_FILE: Qwen3.5-2B-UD-IQ3_XXS.gguf - STUDIO_PORT: '18888' - -jobs: - inference: - name: Studio boots, loads a GGUF, answers a chat completion - runs-on: ubuntu-latest - timeout-minutes: 25 - steps: - - uses: actions/checkout@v4 - - - name: Linux dependencies for llama.cpp prebuilt - run: | - sudo apt-get update - sudo apt-get install -y --no-install-recommends \ - libcurl4-openssl-dev libssl-dev jq - - - uses: actions/setup-node@v4 - with: - node-version: '22' - cache: 'npm' - cache-dependency-path: studio/frontend/package-lock.json - - - uses: actions/setup-python@v5 - with: - python-version: '3.12' - cache: 'pip' - - - name: Cache GGUF model file - id: cache-gguf - uses: actions/cache@v4 - with: - path: gguf-cache - key: ${{ runner.os }}-gguf-${{ env.GGUF_REPO }}-${{ env.GGUF_FILE }}-v1 - - - name: Download GGUF if cache miss - if: steps.cache-gguf.outputs.cache-hit != 'true' - run: | - # huggingface-cli was deprecated in huggingface_hub 1.13; the new CLI is `hf`. - python -m pip install --upgrade huggingface_hub hf_transfer - mkdir -p gguf-cache - HF_HUB_ENABLE_HF_TRANSFER=1 \ - hf download "$GGUF_REPO" "$GGUF_FILE" --local-dir gguf-cache - - - name: Install Studio (--local, --no-torch keeps the install lean) - run: | - mkdir -p logs - set -o pipefail - bash install.sh --local --no-torch 2>&1 | tee logs/install.log - - - name: Assert llama.cpp prebuilt was installed (no source-build fallback) - # ubuntu-latest is CPU-only x86_64, so studio/setup.sh should route - # to ggml-org/llama.cpp and grab bin-ubuntu-x64.tar.gz. A source - # build here means the routing regressed. - run: | - if grep -q "falling back to source build" logs/install.log; then - echo "::error::llama.cpp prebuilt path failed on ubuntu-latest. studio/setup.sh routing regressed; CPU-only Linux x86_64 should hit ggml-org/llama.cpp's bin-ubuntu-x64.tar.gz." - grep -E "llama-prebuilt|llama.cpp" logs/install.log | tail -60 - exit 1 - fi - if ! grep -qE "prebuilt installed and validated|prebuilt up to date and validated" logs/install.log; then - echo "::error::install.log does not contain the success marker for the llama.cpp prebuilt path. Did setup.sh skip the prebuilt install?" - grep -E "llama-prebuilt|llama.cpp" logs/install.log | tail -60 - exit 1 - fi - echo "llama.cpp prebuilt path used successfully" - - - name: Reset auth + start Studio in the background - run: | - unsloth studio reset-password - mkdir -p logs - UNSLOTH_API_ONLY=1 unsloth studio -H 127.0.0.1 -p "$STUDIO_PORT" \ - > logs/studio.log 2>&1 & - echo "STUDIO_PID=$!" >> "$GITHUB_ENV" - - - name: Wait for /api/health - run: | - for i in $(seq 1 60); do - if curl -fs "http://127.0.0.1:${STUDIO_PORT}/api/health" > /tmp/health.json; then - echo "ready after ${i}s" - cat /tmp/health.json - jq -e '.status == "healthy"' /tmp/health.json - exit 0 - fi - sleep 1 - done - echo "Studio did not become healthy in 60s" - tail -200 logs/studio.log - exit 1 - - - name: Login + change bootstrap password - run: | - PW=$(cat ~/.unsloth/studio/auth/.bootstrap_password) - NEW="CIPasswordSmoke12345!" - TOKEN=$(curl -fs -X POST "http://127.0.0.1:${STUDIO_PORT}/api/auth/login" \ - -H 'content-type: application/json' \ - -d "{\"username\":\"unsloth\",\"password\":\"$PW\"}" | jq -r .access_token) - curl -fs -X POST "http://127.0.0.1:${STUDIO_PORT}/api/auth/change-password" \ - -H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' \ - -d "{\"current_password\":\"$PW\",\"new_password\":\"$NEW\"}" > /dev/null - # Re-login to clear must_change_password flag. - NEW_TOKEN=$(curl -fs -X POST "http://127.0.0.1:${STUDIO_PORT}/api/auth/login" \ - -H 'content-type: application/json' \ - -d "{\"username\":\"unsloth\",\"password\":\"$NEW\"}" | jq -r .access_token) - echo "TOKEN=$NEW_TOKEN" >> "$GITHUB_ENV" - - - name: Load the GGUF into Studio - run: | - GGUF_PATH="$GITHUB_WORKSPACE/gguf-cache/${GGUF_FILE}" - ls -lh "$GGUF_PATH" - curl -fs -X POST "http://127.0.0.1:${STUDIO_PORT}/api/inference/load" \ - -H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' \ - --max-time 600 \ - -d "{\"model_path\":\"$GGUF_PATH\",\"is_lora\":false,\"max_seq_length\":2048}" \ - | jq '{status, display_name, is_gguf, context_length}' - - - name: Send a chat completion + assert non-empty response - run: | - RESP=$(curl -fs -X POST "http://127.0.0.1:${STUDIO_PORT}/api/inference/chat/completions" \ - -H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' \ - --max-time 900 \ - -d '{ - "messages":[{"role":"user","content":"Say hello in one short sentence."}], - "max_tokens":40, - "stream":false - }') - echo "raw response: $RESP" - CONTENT=$(echo "$RESP" | jq -r '.choices[0].message.content // empty') - echo "model response: $CONTENT" - if [ -z "$CONTENT" ]; then - echo "::error::Empty assistant response from Studio" - exit 1 - fi - - - name: Stop Studio - if: always() - run: | - kill "${STUDIO_PID}" || true - sleep 2 - ss -tln | grep ":${STUDIO_PORT}" || true - - - name: Upload Studio + install logs on failure - if: failure() - uses: actions/upload-artifact@v4 - with: - name: studio-inference-log - path: | - logs/studio.log - logs/install.log - retention-days: 7 diff --git a/.github/workflows/studio-tauri-smoke.yml b/.github/workflows/studio-tauri-smoke.yml deleted file mode 100644 index fcc9c8d963..0000000000 --- a/.github/workflows/studio-tauri-smoke.yml +++ /dev/null @@ -1,105 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. - -# PR-time smoke for the Tauri desktop wrapper. Builds the frontend and the -# Tauri Linux debug binary, with no codesigning. Catches: -# - tauri.conf.json drift -# - src-tauri Cargo.toml or rust source breakage -# - Tauri CLI version drift (we pin 2.10.1, matching release-desktop.yml) -# - frontend output not picked up by Tauri's distDir -# -# Linux-only on a free `ubuntu-latest` runner. Mac and Windows desktop builds -# stay in release-desktop.yml (manual `workflow_dispatch`) because they need -# code-signing secrets and ~30 min of runner time each. - -name: Studio Tauri CI - -on: - pull_request: - paths: - - 'studio/frontend/**' - - 'studio/src-tauri/**' - - '.github/workflows/studio-tauri-smoke.yml' - push: - branches: [main, pip] - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - linux-debug-build: - name: Tauri Linux debug build (no codesign) - runs-on: ubuntu-22.04 - timeout-minutes: 25 - steps: - - uses: actions/checkout@v4 - - - name: Linux native deps for Tauri / WebKit2GTK - run: | - sudo apt-get update - sudo apt-get install -y \ - libwebkit2gtk-4.1-dev libayatana-appindicator3-dev \ - librsvg2-dev libxdo-dev libssl-dev patchelf - - - uses: actions/setup-node@v4 - with: - node-version: '24' - cache: 'npm' - cache-dependency-path: studio/frontend/package-lock.json - - - uses: dtolnay/rust-toolchain@stable - - - uses: swatinem/rust-cache@v2 - with: - workspaces: studio/src-tauri -> target - - - name: Install pinned Tauri CLI (matches release-desktop.yml) - run: npm install --save-dev --prefix studio @tauri-apps/cli@2.10.1 - - - name: Verify pinned Tauri CLI version - run: | - out="$(npx --prefix studio tauri --version)" - echo "$out" - [ "$out" = "tauri-cli 2.10.1" ] || { echo "::error::expected tauri-cli 2.10.1, got $out"; exit 1; } - - - name: Frontend build (npm ci, vite) - working-directory: studio/frontend - run: | - npm ci --no-fund --no-audit - npm run build - test -f dist/index.html - - - name: Tauri debug build (Linux, no bundle, no codesign) - # `--debug` + `--no-bundle` keeps this lean: compiles the Rust crate, - # confirms the frontend dist is wired into Tauri, but skips the AppImage - # / .deb production. Code signing is irrelevant because we never produce - # a distributable artifact. - env: - TAURI_SIGNING_PRIVATE_KEY: '' - TAURI_SIGNING_PRIVATE_KEY_PASSWORD: '' - run: npx --prefix studio tauri build --debug --no-bundle - - - name: Inspect produced binary - run: | - BIN=$(find studio/src-tauri/target/debug -maxdepth 1 -type f -executable 2>/dev/null \ - | grep -Ev '\.(d|so|dylib|dll)$' \ - | grep -Ev '/(deps|build|examples)$' \ - | head -1) - echo "binary: $BIN" - if [ -z "$BIN" ]; then - echo "::error::Tauri debug binary not produced" - ls -la studio/src-tauri/target/debug/ || true - exit 1 - fi - file "$BIN" - du -h "$BIN" - - - uses: actions/upload-artifact@v4 - if: failure() - with: - name: tauri-debug-build - path: | - studio/src-tauri/target/debug - studio/frontend/dist - retention-days: 3 diff --git a/.github/workflows/wheel-smoke.yml b/.github/workflows/wheel-smoke.yml deleted file mode 100644 index 080a6bb261..0000000000 --- a/.github/workflows/wheel-smoke.yml +++ /dev/null @@ -1,124 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. - -# Builds the PyPI wheel from the PR branch, then verifies the built wheel -# actually contains what we expect to ship and does NOT contain the broken -# Studio bundle that 2026.5.1 published. This is the single workflow that -# would have blocked the 2026.5.1 release before twine upload. -# -# Verified locally end-to-end against this branch: -# - python -m build produces unsloth--py3-none-any.whl in 13s -# - wheel content sanity passes: -# lockfile shipped, frontend dist shipped, -# no node_modules in wheel, no bun.lock in wheel, -# main bundle has unstable_Provider hits=1 (assistant-ui internals only). -# - Studio backend imports cleanly from the installed wheel with the -# lightweight dep set below. - -name: Wheel CI - -on: - pull_request: - paths: - - 'pyproject.toml' - - 'studio/**' - - 'unsloth/**' - - 'unsloth_cli/**' - - '.github/workflows/wheel-smoke.yml' - push: - branches: [main, pip] - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - wheel: - name: Wheel build + content sanity + import smoke - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-node@v4 - with: - node-version: '22' - cache: 'npm' - cache-dependency-path: studio/frontend/package-lock.json - - - uses: actions/setup-python@v5 - with: - python-version: '3.12' - - - name: Build frontend - run: | - cd studio/frontend - npm ci --no-fund --no-audit - npm run build - - - name: Build wheel + sdist - run: | - python -m pip install --upgrade pip build - rm -rf dist build ./*.egg-info - python -m build - - - name: Wheel content sanity - run: | - python - <<'PY' - import zipfile, glob, sys - w = glob.glob("dist/unsloth-*.whl") - if not w: - print("FAIL: no wheel produced"); sys.exit(2) - w = w[0] - print(f"wheel: {w}") - with zipfile.ZipFile(w) as z: - n = z.namelist() - checks = { - "lockfile shipped": any(s.endswith("studio/frontend/package-lock.json") for s in n), - "frontend dist shipped": any(s.endswith("studio/frontend/dist/index.html") for s in n), - "no node_modules": not any("studio/frontend/node_modules/" in s for s in n), - "no bun.lock": not any(s.endswith("studio/frontend/bun.lock") for s in n), - } - js = [s for s in n - if "studio/frontend/dist/assets/" in s - and s.endswith(".js") - and "/index-" in s] - if not js: - print("FAIL: no main bundle index-*.js in wheel"); sys.exit(2) - data = z.read(js[0]).decode("utf-8", "replace") - hits = data.count("unstable_Provider:") - print(f"main bundle: {js[0]}") - print(f"unstable_Provider hits: {hits} (>=4 indicates 2026.5.1 regression)") - checks["bundle has no Studio unstable_Provider call site"] = (hits < 4) - - print() - for k, v in checks.items(): - print(f" [{'PASS' if v else 'FAIL'}] {k}") - sys.exit(0 if all(checks.values()) else 1) - PY - - - name: Studio backend import smoke - # Imports `studio.backend.main:app` from the freshly-installed wheel in - # a clean venv. This catches the class of bug that 2026.5.1 shipped with: - # frontend dist missing, package-lock.json missing, or the wheel's Python - # source tree broken in a way that surfaces only at app construction time. - run: | - python -m venv /tmp/v - /tmp/v/bin/pip install --upgrade pip - /tmp/v/bin/pip install -r studio/backend/requirements/studio.txt - /tmp/v/bin/pip install \ - python-multipart aiofiles sqlalchemy cryptography \ - pyyaml jinja2 mammoth unpdf requests \ - 'numpy<3' - /tmp/v/bin/pip install --no-deps dist/unsloth-*.whl - # Run from /tmp so Python imports the installed package, not the source tree. - cd /tmp - /tmp/v/bin/python -c "from studio.backend.main import app; print('Studio backend OK:', app.title)" - - - name: Upload wheel on failure - if: failure() - uses: actions/upload-artifact@v4 - with: - name: unsloth-wheel - path: dist/ - retention-days: 7 diff --git a/.gitignore b/.gitignore index ae6770bc07..7a24d07c6f 100644 --- a/.gitignore +++ b/.gitignore @@ -24,8 +24,8 @@ dist/ downloads/ eggs/ .eggs/ -/lib/ -/lib64/ +lib/ +lib64/ parts/ sdist/ var/ @@ -204,18 +204,6 @@ tmp/ **/node_modules/ auth.db -# Tauri local build/generated output -studio/src-tauri/target/ -studio/src-tauri/gen/ -studio/src-tauri/artifacts/ -studio/src-tauri/icons/android/ -studio/src-tauri/icons/ios/ -studio/src-tauri/icons/128x128@2x.png -studio/src-tauri/icons/64x64.png -studio/src-tauri/icons/Square*Logo.png -studio/src-tauri/icons/StoreLogo.png -studio/src-tauri/icons/squarehq.png - # Local working docs **/CLAUDE.md **/claude.md @@ -228,4 +216,3 @@ setup_leo.sh server.pid *.log package-lock.json -llama.cpp/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a2a4995d62..1d3137b373 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.12 + rev: v0.15.11 hooks: - id: ruff args: diff --git a/README.md b/README.md index a654518d14..e1b7e448ef 100644 --- a/README.md +++ b/README.md @@ -43,8 +43,7 @@ Unsloth Studio (Beta) lets you run and train text, [audio](https://unsloth.ai/do * **Export models**: [Save or export](https://unsloth.ai/docs/new/studio/export) models to GGUF, 16-bit safetensors and other formats. * **Tool calling**: Support for [self-healing tool calling](https://unsloth.ai/docs/new/studio/chat#auto-healing-tool-calling) and web search * **[Code execution](https://unsloth.ai/docs/new/studio/chat#code-execution)**: lets LLMs test code in Claude artifacts and sandbox environments -* **[API inference endpoint](https://unsloth.ai/docs/basics/api)**: Deploy and run local LLMs in Claude Code, Codex tools with Unsloth -* [Auto set inference settings](https://unsloth.ai/docs/new/studio/chat#auto-parameter-tuning) and customize chat templates. +* [Auto-tune inference parameters](https://unsloth.ai/docs/new/studio/chat#auto-parameter-tuning) and customize chat templates. * We work directly with teams behind [gpt-oss](https://docs.unsloth.ai/new/gpt-oss-how-to-run-and-fine-tune#unsloth-fixes-for-gpt-oss), [Qwen3](https://www.reddit.com/r/LocalLLaMA/comments/1kaodxu/qwen3_unsloth_dynamic_ggufs_128k_context_bug_fixes/), [Llama 4](https://github.com/ggml-org/llama.cpp/pull/12889), [Mistral](models/tutorials/devstral-how-to-run-and-fine-tune.md), [Gemma 1-3](https://news.ycombinator.com/item?id=39671146), and [Phi-4](https://unsloth.ai/blog/phi4), where we’ve fixed bugs that improve model accuracy. * Upload images, audio, PDFs, code, DOCX and more file types to chat with. ### Training @@ -80,9 +79,8 @@ irm https://unsloth.ai/install.ps1 | iex #### Launch ```bash -unsloth studio -p 8888 +unsloth studio -H 0.0.0.0 -p 8888 ``` -> For cloud VMs or LAN access, add `-H 0.0.0.0` to bind on all interfaces. #### Update To update, use the same install commands as above. Or run (does not work on Windows): @@ -150,7 +148,6 @@ Read our [guide](https://unsloth.ai/docs/get-started/fine-tuning-llms-guide). Ad - See detailed documentation for Unsloth [here](https://unsloth.ai/docs) ## 🦥 Unsloth News -- **API inference endpoint**: Deploy and run local LLMs in Claude Code, Codex tools. [Guide](https://unsloth.ai/docs/basics/api) - **Qwen3.6**: Qwen3.6-35B-A3B can now be trained and run in Unsloth Studio. [Blog](https://unsloth.ai/docs/models/qwen3.6) - **Gemma 4**: Run and train Google’s new models directly in Unsloth. [Blog](https://unsloth.ai/docs/models/gemma-4) - **Introducing Unsloth Studio**: our new web UI for running and training LLMs. [Blog](https://unsloth.ai/docs/new/studio) @@ -161,6 +158,7 @@ Read our [guide](https://unsloth.ai/docs/get-started/fine-tuning-llms-guide). Ad - New RoPE & MLP **Triton Kernels** & **Padding Free + Packing**: 3x faster training & 30% less VRAM. [Blog](https://unsloth.ai/docs/new/3x-faster-training-packing) - **500K Context**: Training a 20B model with >500K context is now possible on an 80GB GPU. [Blog](https://unsloth.ai/docs/blog/500k-context-length-fine-tuning) - **FP8 & Vision RL**: You can now do FP8 & VLM GRPO on consumer GPUs. [FP8 Blog](https://unsloth.ai/docs/get-started/reinforcement-learning-rl-guide/fp8-reinforcement-learning) • [Vision RL](https://unsloth.ai/docs/get-started/reinforcement-learning-rl-guide/vision-reinforcement-learning-vlm-rl) +- **gpt-oss** by OpenAI: Read our [RL blog](https://unsloth.ai/docs/models/gpt-oss-how-to-run-and-fine-tune/gpt-oss-reinforcement-learning), [Flex Attention](https://unsloth.ai/docs/models/gpt-oss-how-to-run-and-fine-tune/long-context-gpt-oss-training) blog and [Guide](https://unsloth.ai/docs/models/gpt-oss-how-to-run-and-fine-tune). ## 📥 Advanced Installation The below advanced instructions are for Unsloth Studio. For Unsloth Core advanced installation, [view our docs](https://unsloth.ai/docs/get-started/install/pip-install#advanced-pip-installation). @@ -169,7 +167,7 @@ The below advanced instructions are for Unsloth Studio. For Unsloth Core advance git clone https://github.com/unslothai/unsloth cd unsloth ./install.sh --local -unsloth studio -p 8888 +unsloth studio -H 0.0.0.0 -p 8888 ``` Then to update : ```bash @@ -182,7 +180,7 @@ git clone https://github.com/unslothai/unsloth.git cd unsloth Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass .\install.ps1 --local -unsloth studio -p 8888 +unsloth studio -H 0.0.0.0 -p 8888 ``` Then to update : ```bash @@ -195,11 +193,11 @@ git clone https://github.com/unslothai/unsloth cd unsloth git checkout nightly ./install.sh --local -unsloth studio -p 8888 +unsloth studio -H 0.0.0.0 -p 8888 ``` Then to launch every time: ```bash -unsloth studio -p 8888 +unsloth studio -H 0.0.0.0 -p 8888 ``` #### Nightly: Windows: @@ -210,11 +208,11 @@ cd unsloth git checkout nightly Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass .\install.ps1 --local -unsloth studio -p 8888 +unsloth studio -H 0.0.0.0 -p 8888 ``` Then to launch every time: ```bash -unsloth studio -p 8888 +unsloth studio -H 0.0.0.0 -p 8888 ``` #### Uninstall diff --git a/install.ps1 b/install.ps1 index ef87c5ed08..7f98fc5cb8 100644 --- a/install.ps1 +++ b/install.ps1 @@ -3,100 +3,20 @@ # Local: Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass; .\install.ps1 --local # NoTorch: .\install.ps1 --no-torch (skip PyTorch, GGUF-only mode) # Test: .\install.ps1 --package roland-sloth -# -# Env vars (priority: UNSLOTH_STUDIO_HOME > STUDIO_HOME > USERPROFILE-redirect > default): -# UNSLOTH_STUDIO_HOME / STUDIO_HOME = path -> install under that path -# (DataDir nests inside; user PATH not modified persistently). -# Default ($USERPROFILE\.unsloth\studio) is preserved when no env var is set. function Install-UnslothStudio { $ErrorActionPreference = "Stop" $script:UnslothVerbose = ($env:UNSLOTH_VERBOSE -eq "1") - # ── Tauri structured output ── - function Write-TauriLog { - param([string]$Tag, [string]$Message) - if ($TauriMode) { - Write-Host "[TAURI:$Tag] $Message" - } - } - - function Format-TauriDiagBool { - param([bool]$Value) - if ($Value) { return "true" } - return "false" - } - - function Get-TauriDiagArch { - $arch = [string]$env:PROCESSOR_ARCHITECTURE - if ([string]::IsNullOrWhiteSpace($arch)) { - try { $arch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString() } catch { $arch = "unknown" } - } - $arch = $arch.ToLowerInvariant() - switch ($arch) { - "amd64" { return "x86_64" } - "x64" { return "x86_64" } - "arm64" { return "arm64" } - "x86" { return "x86" } - default { return ($arch -replace '[^a-z0-9_.-]', '_') } - } - } - - function Get-TauriTorchIndexFamily { - param([string]$TorchIndexUrl) - if ($SkipTorch) { return "none" } - if ([string]::IsNullOrWhiteSpace($TorchIndexUrl)) { return "none" } - $leaf = ($TorchIndexUrl.TrimEnd('/') -split '/')[-1].ToLowerInvariant() - if (@("cpu", "cu118", "cu124", "cu126", "cu128", "cu130") -contains $leaf) { return $leaf } - if ($leaf -match '^rocm[0-9]+\.[0-9]+$') { return $leaf } - return "auto" - } - - function Get-TauriGpuBranch { - param([string]$TorchIndexFamily) - if ($SkipTorch) { return "no_torch" } - if ($TorchIndexFamily -like "cu*") { return "cuda" } - if ($TorchIndexFamily -like "rocm*") { return "rocm" } - if ($TorchIndexFamily -eq "cpu") { return "cpu" } - return "unknown" - } - - function Write-TauriDiag { - param( - [string]$GpuBranch = "unknown", - [string]$TorchIndexFamily = "none", - [string]$PythonVersionForDiag = $PythonVersion - ) - if ([string]::IsNullOrWhiteSpace($PythonVersionForDiag)) { $PythonVersionForDiag = "unknown" } - Write-TauriLog "DIAG" "diag_schema=1 platform=windows arch=$(Get-TauriDiagArch) python_version=$($PythonVersionForDiag.ToLowerInvariant()) skip_torch=$(Format-TauriDiagBool $SkipTorch) mac_intel=false gpu_branch=$GpuBranch torch_index_family=$TorchIndexFamily" - } - - function Exit-InstallFailure { - param( - [Parameter(Mandatory = $true)][string]$Message, - [int]$Code = 1 - ) - if ($Code -eq 0) { $Code = 1 } - Write-TauriLog "ERROR" $Message - if (Get-Command Restore-StudioVenvRollback -CommandType Function -ErrorAction SilentlyContinue) { - Restore-StudioVenvRollback - } - if ($TauriMode) { - exit $Code - } - } - # ── Parse flags ── $StudioLocalInstall = $false $PackageName = "unsloth" $RepoRoot = "" - $TauriMode = $false $SkipTorch = $false $argList = $args for ($i = 0; $i -lt $argList.Count; $i++) { switch ($argList[$i]) { "--local" { $StudioLocalInstall = $true } - "--tauri" { $TauriMode = $true } "--no-torch" { $SkipTorch = $true } "--verbose" { $script:UnslothVerbose = $true } "-v" { $script:UnslothVerbose = $true } @@ -104,7 +24,7 @@ function Install-UnslothStudio { $i++ if ($i -ge $argList.Count) { Write-Host "[ERROR] --package requires an argument." -ForegroundColor Red - return (Exit-InstallFailure "--package requires an argument.") + return } $PackageName = $argList[$i] } @@ -120,105 +40,12 @@ function Install-UnslothStudio { $RepoRoot = (Resolve-Path (Split-Path -Parent $PSCommandPath)).Path if (-not (Test-Path (Join-Path $RepoRoot "pyproject.toml"))) { Write-Host "[ERROR] --local must be run from the unsloth repo root (pyproject.toml not found at $RepoRoot)" -ForegroundColor Red - return (Exit-InstallFailure "--local must be run from the unsloth repo root") + return } } - # Validate --package to prevent injection into shell/Python commands - if ($PackageName -notmatch '^[a-zA-Z0-9][a-zA-Z0-9._-]*$') { - Write-Host "[ERROR] --package name contains invalid characters (allowed: a-z A-Z 0-9 . _ -)" -ForegroundColor Red - return (Exit-InstallFailure "--package name contains invalid characters") - } - $PythonVersion = "3.13" - - # Resolve install destinations. Priority: UNSLOTH_STUDIO_HOME, then - # STUDIO_HOME alias, then USERPROFILE-redirect, then default. - # Reject whitespace-only values so " " is treated as unset (matches the - # Python resolvers' .strip()), preventing install/runtime layout drift. - $envOverrideVar = $null - $envOverride = $null - if (-not [string]::IsNullOrWhiteSpace($env:UNSLOTH_STUDIO_HOME)) { - $envOverrideVar = "UNSLOTH_STUDIO_HOME" - $envOverride = $env:UNSLOTH_STUDIO_HOME.Trim() - } elseif (-not [string]::IsNullOrWhiteSpace($env:STUDIO_HOME)) { - $envOverrideVar = "STUDIO_HOME" - $envOverride = $env:STUDIO_HOME.Trim() - } - - # Custom Studio roots are not supported with --tauri (desktop app still - # resolves %USERPROFILE%\.unsloth\studio). Pass through if override == legacy. - if ($TauriMode -and $envOverride) { - $_tauriOverride = $envOverride - if ($_tauriOverride -eq "~" -or $_tauriOverride -like "~/*" -or $_tauriOverride -like "~\*") { - $_tauriOverride = (Join-Path $env:USERPROFILE $_tauriOverride.Substring(1).TrimStart('/','\')) - } - try { - $_tauriOverride = [System.IO.Path]::GetFullPath($_tauriOverride) - } catch {} - $_legacyTauriRoot = Join-Path $env:USERPROFILE ".unsloth\studio" - try { - $_legacyTauriRoot = [System.IO.Path]::GetFullPath($_legacyTauriRoot) - } catch {} - # Strip trailing separators so ".../studio\" matches ".../studio". - $_trimSeps = @( - [System.IO.Path]::DirectorySeparatorChar, - [System.IO.Path]::AltDirectorySeparatorChar - ) - $_tauriOverride = $_tauriOverride.TrimEnd($_trimSeps) - $_legacyTauriRoot = $_legacyTauriRoot.TrimEnd($_trimSeps) - if ($_tauriOverride -ne $_legacyTauriRoot) { - Write-Host "ERROR: $envOverrideVar is not supported with --tauri." -ForegroundColor Red - Write-Host " The desktop app still uses the legacy %USERPROFILE%\.unsloth\studio root." -ForegroundColor Red - Write-Host " Run install.ps1 without --tauri for custom-root shell installs," -ForegroundColor Yellow - Write-Host " or unset the env var for default desktop installs." -ForegroundColor Yellow - throw "$envOverrideVar is not supported with --tauri." - } - } - - $defaultProfile = $null - try { $defaultProfile = [Environment]::GetFolderPath("UserProfile") } catch {} - - # LOCALAPPDATA may be unset in service / CI contexts; Join-Path would abort - # under ErrorActionPreference=Stop without this guard. - $defaultDataDir = if ($env:LOCALAPPDATA -and -not [string]::IsNullOrWhiteSpace($env:LOCALAPPDATA)) { - Join-Path $env:LOCALAPPDATA "Unsloth Studio" - } else { $null } - - if ($envOverride) { - # Tilde expansion: env vars aren't subject to it when quoted on assignment. - if ($envOverride -eq "~" -or $envOverride -like "~/*" -or $envOverride -like "~\*") { - $envOverride = (Join-Path $env:USERPROFILE $envOverride.Substring(1).TrimStart('/','\')) - } - try { - # .NET API: New-Item -Path treats brackets as wildcards and has no - # -LiteralPath in PS 5.1, so a root like C:\studio[abc] would fail. - [System.IO.Directory]::CreateDirectory($envOverride) | Out-Null - $StudioHome = (Resolve-Path -LiteralPath $envOverride).Path - } catch { - Write-Host "ERROR: $envOverrideVar=$envOverride cannot be created or accessed." -ForegroundColor Red - throw "$envOverrideVar=$envOverride cannot be created or accessed." - } - $probe = Join-Path $StudioHome (".unsloth-write-probe-" + [guid]::NewGuid()) - try { - # WriteAllText: literal-path safe + closes handle so Remove-Item works. - [System.IO.File]::WriteAllText($probe, "") - Remove-Item -LiteralPath $probe -Force -ErrorAction SilentlyContinue - } catch { - Write-Host "ERROR: $envOverrideVar=$StudioHome is not writable." -ForegroundColor Red - throw "$envOverrideVar=$StudioHome is not writable." - } - $StudioDataDir = Join-Path $StudioHome "share" - $StudioRedirectMode = 'env' - } elseif ($defaultProfile -and $env:USERPROFILE -and ($env:USERPROFILE -ne $defaultProfile)) { - $StudioHome = Join-Path $env:USERPROFILE ".unsloth\studio" - $StudioDataDir = $defaultDataDir - $StudioRedirectMode = 'profile' - } else { - $StudioHome = Join-Path $env:USERPROFILE ".unsloth\studio" - $StudioDataDir = $defaultDataDir - $StudioRedirectMode = 'default' - } + $StudioHome = Join-Path $env:USERPROFILE ".unsloth\studio" $VenvDir = Join-Path $StudioHome "unsloth_studio" $Rule = [string]::new([char]0x2500, 52) @@ -470,24 +297,24 @@ function Install-UnslothStudio { [Parameter(Mandatory = $true)][string]$UnslothExePath ) - if (-not (Test-Path -LiteralPath $UnslothExePath)) { + if (-not (Test-Path $UnslothExePath)) { substep "cannot create shortcuts, unsloth.exe not found at $UnslothExePath" "Yellow" return } try { # Persist an absolute path in launcher scripts so shortcut working # directory changes do not break process startup. - $UnslothExePath = (Resolve-Path -LiteralPath $UnslothExePath).Path + $UnslothExePath = (Resolve-Path $UnslothExePath).Path # Escape for single-quoted embedding in generated launcher script. # This prevents runtime variable expansion for paths containing '$'. $SingleQuotedExePath = $UnslothExePath -replace "'", "''" - # $StudioDataDir = LOCALAPPDATA\Unsloth Studio, or $StudioHome\share in env-mode. - if (-not $StudioDataDir -or [string]::IsNullOrWhiteSpace($StudioDataDir)) { - substep "DataDir path unavailable; skipped shortcut creation" "Yellow" + $localAppDataDir = $env:LOCALAPPDATA + if (-not $localAppDataDir -or [string]::IsNullOrWhiteSpace($localAppDataDir)) { + substep "LOCALAPPDATA path unavailable; skipped shortcut creation" "Yellow" return } - $appDir = $StudioDataDir + $appDir = Join-Path $localAppDataDir "Unsloth Studio" $launcherPs1 = Join-Path $appDir "launch-studio.ps1" $launcherVbs = Join-Path $appDir "launch-studio.vbs" $desktopDir = [Environment]::GetFolderPath("Desktop") @@ -519,89 +346,23 @@ function Install-UnslothStudio { } $iconUrl = "https://raw.githubusercontent.com/unslothai/unsloth/main/studio/frontend/public/unsloth.ico" - if (-not (Test-Path -LiteralPath $appDir)) { - [System.IO.Directory]::CreateDirectory($appDir) | Out-Null - } - - # Same-install discriminator: per-install opaque id written once at - # install time and read by both this launcher and the backend - # (/api/health). Replaces the older sha256(resolved $StudioHome) - # scheme to (a) avoid leaking the install path on -H 0.0.0.0 - # deployments and (b) sidestep launcher/backend canonicalization - # drift (Resolve-Path vs Path.resolve() junction handling). Lives - # at $StudioHome\share\ (not $appDir) so the backend can find it - # via _STUDIO_ROOT_RESOLVED / "share" / "studio_install_id" - # regardless of mode. 32 bytes of crypto random -> 64 hex chars. - $_studioIdDir = Join-Path $StudioHome "share" - if (-not (Test-Path -LiteralPath $_studioIdDir)) { - [System.IO.Directory]::CreateDirectory($_studioIdDir) | Out-Null - } - $_studioIdFile = Join-Path $_studioIdDir "studio_install_id" - $_studioRootId = "" - if ((Test-Path -LiteralPath $_studioIdFile) -and ` - ((Get-Item -LiteralPath $_studioIdFile).Length -gt 0)) { - $_studioRootId = ([System.IO.File]::ReadAllText($_studioIdFile)).Trim() - } - if (-not $_studioRootId) { - $_idBytes = New-Object byte[] 32 - [Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($_idBytes) - $_studioRootId = -join ($_idBytes | ForEach-Object { $_.ToString('x2') }) - # Atomic write: write to a temp sibling then rename, so a partial - # install cannot leave a half-written id. - $_idTmp = $_studioIdFile + ".$PID.tmp" - [System.IO.File]::WriteAllText($_idTmp, $_studioRootId) - Move-Item -LiteralPath $_idTmp -Destination $_studioIdFile -Force - } - - # Env-mode: persist UNSLOTH_STUDIO_HOME (and llama path) so fresh - # shells don't need to re-export, and bake per-install $portFile / - # $mutexName so concurrent custom-root launchers cannot serialize - # through one global mutex on 8888..8908. Default installs get an - # empty prefix to match pre-PR behavior. - $studioHomeExport = if ($StudioRedirectMode -eq 'env') { - # When override == legacy default, llama.cpp stays at - # ~/.unsloth/llama.cpp (one shared build). Canonicalize the - # legacy side so the comparison survives path normalization. - $_legacyStudio = Join-Path $env:USERPROFILE ".unsloth\studio" - if (Test-Path -LiteralPath $_legacyStudio -PathType Container) { - $_legacyStudio = (Resolve-Path -LiteralPath $_legacyStudio).Path - } - $_llamaPath = if ($StudioHome -eq $_legacyStudio) { - Join-Path $env:USERPROFILE ".unsloth\llama.cpp" - } else { - Join-Path $StudioHome "llama.cpp" - } - $_sq = $StudioHome -replace "'", "''" - $_llama = $_llamaPath -replace "'", "''" - $_appDirSq = $appDir -replace "'", "''" - $_appBytes = [Text.Encoding]::UTF8.GetBytes($appDir) - $_appHash = ([BitConverter]::ToString( - [Security.Cryptography.SHA256]::Create().ComputeHash($_appBytes) - ) -replace '-', '').Substring(0, 16) - # UNSLOTH_LLAMA_CPP_PATH is a pre-existing user override; only default if unset. - "`$env:UNSLOTH_STUDIO_HOME = '$_sq'`nif (-not `$env:UNSLOTH_LLAMA_CPP_PATH) {`n `$env:UNSLOTH_LLAMA_CPP_PATH = '$_llama'`n}`n`$portFile = '$_appDirSq\studio.port'`n`$mutexName = 'Local\UnslothStudioLauncher-$_appHash'`n" - } else { - "`$portFile = `$null`n`$mutexName = 'Local\UnslothStudioLauncher'`n" + if (-not (Test-Path $appDir)) { + New-Item -ItemType Directory -Path $appDir -Force | Out-Null } $launcherContent = @" -$studioHomeExport`$ErrorActionPreference = 'Stop' +`$ErrorActionPreference = 'Stop' `$basePort = 8888 `$maxPortOffset = 20 `$timeoutSec = 60 `$pollIntervalMs = 1000 -`$_ExpectedStudioRootId = '$_studioRootId' function Test-StudioHealth { param([Parameter(Mandatory = `$true)][int]`$Port) try { `$url = "http://127.0.0.1:`$Port/api/health" `$resp = Invoke-RestMethod -Uri `$url -TimeoutSec 1 -Method Get - if (-not (`$resp -and `$resp.status -eq 'healthy' -and `$resp.service -eq 'Unsloth UI Backend')) { return `$false } - # why: verify the backend belongs to THIS install via the install-time - # hex digest; raw path is not leaked over /api/health. - if (`$_ExpectedStudioRootId -and `$resp.studio_root_id -ne `$_ExpectedStudioRootId) { return `$false } - return `$true + return (`$resp -and `$resp.status -eq 'healthy' -and `$resp.service -eq 'Unsloth UI Backend') } catch { return `$false } @@ -627,17 +388,6 @@ function Get-CandidatePorts { } function Find-HealthyStudioPort { - if (`$portFile) { - if (Test-Path -LiteralPath `$portFile) { - `$cached = Get-Content -LiteralPath `$portFile -ErrorAction SilentlyContinue | Select-Object -First 1 - if (`$cached -match '^\d+`$') { - `$cachedPort = [int]`$cached - if (Test-StudioHealth -Port `$cachedPort) { return `$cachedPort } - Remove-Item -LiteralPath `$portFile -Force -ErrorAction SilentlyContinue - } - } - return `$null - } foreach (`$candidate in (Get-CandidatePorts)) { if (Test-StudioHealth -Port `$candidate) { return `$candidate @@ -691,7 +441,7 @@ if (`$existingPort) { exit 0 } -`$launchMutex = [System.Threading.Mutex]::new(`$false, `$mutexName) +`$launchMutex = [System.Threading.Mutex]::new(`$false, 'Local\UnslothStudioLauncher') `$haveMutex = `$false try { try { @@ -721,9 +471,7 @@ try { } catch {} exit 1 } - # Single-quote the path in the child -Command so `$` / backtick in custom - # roots don't get reparsed; double any apostrophes so 'O''Brien' survives. - `$studioCommand = "& '" + (`$studioExe -replace "'", "''") + "' studio -p " + `$launchPort + `$studioCommand = '& "' + `$studioExe + '" studio -H 0.0.0.0 -p ' + `$launchPort `$launchArgs = @( '-NoExit', '-NoProfile', @@ -747,13 +495,9 @@ try { `$browserOpened = `$false `$deadline = (Get-Date).AddSeconds(`$timeoutSec) while ((Get-Date) -lt `$deadline) { - if (Test-StudioHealth -Port `$launchPort) { - if (`$portFile) { - try { - [System.IO.File]::WriteAllText(`$portFile, "`$launchPort`n") - } catch {} - } - Start-Process "http://localhost:`$launchPort" + `$healthyPort = Find-HealthyStudioPort + if (`$healthyPort) { + Start-Process "http://localhost:`$healthyPort" `$browserOpened = `$true break } @@ -788,19 +532,19 @@ cmd = "powershell -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File " shell.Run cmd, 0, False "@ # WSH handles UTF-16LE reliably for .vbs files with non-ASCII paths. - Set-Content -LiteralPath $launcherVbs -Value $vbsContent -Encoding Unicode -Force + Set-Content -Path $launcherVbs -Value $vbsContent -Encoding Unicode -Force # Prefer bundled icon from local clone/dev installs. # If not available, best-effort download from raw GitHub. # We only attach the icon if the resulting file has a valid ICO header. $hasValidIcon = $false - if ($bundledIcon -and (Test-Path -LiteralPath $bundledIcon)) { + if ($bundledIcon -and (Test-Path $bundledIcon)) { try { - Copy-Item -LiteralPath $bundledIcon -Destination $iconPath -Force + Copy-Item -Path $bundledIcon -Destination $iconPath -Force } catch { Write-Host "[DEBUG] Error copying bundled icon: $($_.Exception.Message)" -ForegroundColor DarkGray } - } elseif (-not (Test-Path -LiteralPath $iconPath)) { + } elseif (-not (Test-Path $iconPath)) { try { Invoke-WebRequest -Uri $iconUrl -OutFile $iconPath -UseBasicParsing } catch { @@ -808,7 +552,7 @@ shell.Run cmd, 0, False } } - if (Test-Path -LiteralPath $iconPath) { + if (Test-Path $iconPath) { try { $bytes = [System.IO.File]::ReadAllBytes($iconPath) if ( @@ -820,21 +564,14 @@ shell.Run cmd, 0, False ) { $hasValidIcon = $true } else { - Remove-Item -LiteralPath $iconPath -Force -ErrorAction SilentlyContinue + Remove-Item $iconPath -Force -ErrorAction SilentlyContinue } } catch { Write-Host "[DEBUG] Error validating or removing icon: $($_.Exception.Message)" -ForegroundColor DarkGray - Remove-Item -LiteralPath $iconPath -Force -ErrorAction SilentlyContinue + Remove-Item $iconPath -Force -ErrorAction SilentlyContinue } } - # Env-mode: skip persistent Desktop / Start Menu .lnk shortcuts - # that may point at a deleted workspace; launcher + icon stay. - if ($StudioRedirectMode -eq 'env') { - substep "wrote launcher at $launcherPs1 (persistent shortcuts skipped in env-override mode)" - return - } - $wscriptExe = Join-Path $env:SystemRoot "System32\wscript.exe" $shortcutArgs = "//B //Nologo `"$launcherVbs`"" @@ -872,12 +609,11 @@ shell.Run cmd, 0, False } # ── Check winget ── - Write-TauriLog "STEP" "Checking system dependencies" if (-not (Get-Command winget -ErrorAction SilentlyContinue)) { step "winget" "not available" "Red" substep "Install it from https://aka.ms/getwinget" "Yellow" substep "or install Python $PythonVersion and uv manually, then re-run." "Yellow" - return (Exit-InstallFailure "winget is not available") + return } # ── Helper: detect a working Python 3.11-3.13 on the system ── @@ -952,7 +688,6 @@ shell.Run cmd, 0, False # ── Install Python if no compatible version (3.11-3.13) found ── # Find-CompatiblePython returns @{ Version = "3.13"; Path = "C:\...\python.exe" } or $null. - Write-TauriLog "STEP" "Installing Python" $DetectedPython = Find-CompatiblePython if ($DetectedPython) { step "python" "Python $($DetectedPython.Version) already installed" @@ -996,17 +731,11 @@ shell.Run cmd, 0, False Write-Host " Please install Python $PythonVersion manually from https://www.python.org/downloads/" -ForegroundColor Yellow Write-Host " Make sure to check 'Add Python to PATH' during installation." -ForegroundColor Yellow Write-Host " Then re-run this installer." -ForegroundColor Yellow - return (Exit-InstallFailure "Python installation failed") + return } } - $DiagPythonVersion = $PythonVersion - if ($DetectedPython) { $DiagPythonVersion = $DetectedPython.Version } - $InitialGpuBranch = "unknown" - if ($SkipTorch) { $InitialGpuBranch = "no_torch" } - Write-TauriDiag -GpuBranch $InitialGpuBranch -TorchIndexFamily "none" -PythonVersionForDiag $DiagPythonVersion # ── Install uv if not present ── - Write-TauriLog "STEP" "Installing uv package manager" if (-not (Get-Command uv -ErrorAction SilentlyContinue)) { substep "installing uv package manager..." $prevEAP = $ErrorActionPreference @@ -1017,7 +746,7 @@ shell.Run cmd, 0, False # Fallback: if winget didn't put uv on PATH, try the PowerShell installer if (-not (Get-Command uv -ErrorAction SilentlyContinue)) { substep "trying alternative uv installer..." "Yellow" - Invoke-Expression (Invoke-RestMethod -Uri "https://astral.sh/uv/install.ps1") + powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" Refresh-SessionPath } } @@ -1025,164 +754,66 @@ shell.Run cmd, 0, False if (-not (Get-Command uv -ErrorAction SilentlyContinue)) { step "uv" "could not be installed" "Red" substep "Install it from https://docs.astral.sh/uv/" "Yellow" - return (Exit-InstallFailure "uv could not be installed") + return } # ── Create venv (migrate old layout if possible, otherwise fresh) ── # Pass the resolved executable path to uv so it does not re-resolve # a version string back to a conda interpreter. - Write-TauriLog "STEP" "Creating virtual environment" - if (-not (Test-Path -LiteralPath $StudioHome)) { - # .NET API: New-Item -Path treats brackets as wildcards. - [System.IO.Directory]::CreateDirectory($StudioHome) | Out-Null + if (-not (Test-Path $StudioHome)) { + New-Item -ItemType Directory -Path $StudioHome -Force | Out-Null } $VenvPython = Join-Path $VenvDir "Scripts\python.exe" $_Migrated = $false - $script:StudioVenvRollbackDir = $null - $script:StudioVenvRollbackTarget = $VenvDir - $script:StudioVenvRollbackActive = $false - function Start-StudioVenvRollback { - param([Parameter(Mandatory = $true)][string]$ExistingDir) - $stamp = Get-Date -Format "yyyyMMddHHmmss" - $candidate = Join-Path $StudioHome "unsloth_studio.rollback.$stamp.$PID" - $suffix = 0 - # -LiteralPath: a custom $StudioHome may contain [ ] * ? which - # plain Test-Path / Move-Item would interpret as wildcards. - while (Test-Path -LiteralPath $candidate) { - $suffix++ - $candidate = Join-Path $StudioHome "unsloth_studio.rollback.$stamp.$PID.$suffix" - } - Move-Item -LiteralPath $ExistingDir -Destination $candidate -ErrorAction Stop - $script:StudioVenvRollbackDir = $candidate - $script:StudioVenvRollbackTarget = $ExistingDir - $script:StudioVenvRollbackActive = $true - substep "previous environment preserved for rollback" - } - - function Restore-StudioVenvRollback { - if (-not $script:StudioVenvRollbackActive) { return } - $backup = $script:StudioVenvRollbackDir - $target = $script:StudioVenvRollbackTarget - if (-not $backup -or -not (Test-Path -LiteralPath $backup)) { - $script:StudioVenvRollbackActive = $false - return - } - substep "restoring previous environment after failed install..." "Yellow" - try { - if (Test-Path -LiteralPath $target) { - Remove-Item -LiteralPath $target -Recurse -Force -ErrorAction SilentlyContinue - } - Move-Item -LiteralPath $backup -Destination $target -Force -ErrorAction Stop - substep "restored previous environment" - $script:StudioVenvRollbackActive = $false - $script:StudioVenvRollbackDir = $null - } catch { - Write-Host "[WARN] Could not restore previous environment from $backup to $target" -ForegroundColor Yellow - Write-Host " $($_.Exception.Message)" -ForegroundColor Yellow - } - } - - function Complete-StudioVenvRollback { - if (-not $script:StudioVenvRollbackActive) { return } - $backup = $script:StudioVenvRollbackDir - if ($backup -and (Test-Path -LiteralPath $backup)) { - Remove-Item -LiteralPath $backup -Recurse -Force -ErrorAction SilentlyContinue - } - $script:StudioVenvRollbackActive = $false - $script:StudioVenvRollbackDir = $null - } - - if (Test-Path -LiteralPath $VenvPython) { - # why: matching guard to the .venv branch below -- in env-mode - # $StudioHome is a user-chosen workspace, so refuse to nuke an - # existing $StudioHome\unsloth_studio that lacks Studio sentinels. - # -PathType Leaf rejects a directory at the sentinel path. Accept the - # in-VENV ownership marker so partial-install retries are not blocked. - if ( - $StudioRedirectMode -eq 'env' -and - -not (Test-Path -LiteralPath (Join-Path $VenvDir ".unsloth-studio-owned") -PathType Leaf) -and - -not (Test-Path -LiteralPath (Join-Path $StudioHome "share\studio.conf") -PathType Leaf) -and - -not (Test-Path -LiteralPath (Join-Path $StudioHome "bin\unsloth.exe") -PathType Leaf) - ) { - Write-Host "[ERROR] $VenvDir already exists but does not look like an Unsloth Studio install." -ForegroundColor Red - Write-Host " Move it aside or choose an empty UNSLOTH_STUDIO_HOME." -ForegroundColor Yellow - throw "Refusing to delete non-Studio venv at $VenvDir" - } - # New layout already exists -- replace only after preserving rollback copy. - substep "preserving existing environment for rollback..." - try { - Start-StudioVenvRollback -ExistingDir $VenvDir - } catch { - Write-Host "[ERROR] Could not prepare existing environment for reinstall: $($_.Exception.Message)" -ForegroundColor Red - return (Exit-InstallFailure "Could not prepare existing environment for reinstall") - } - } elseif ( - $StudioRedirectMode -ne 'env' ` - -and (Test-Path -LiteralPath (Join-Path $StudioHome ".venv\Scripts\python.exe")) - ) { - # Old layout (~/.unsloth/studio/.venv) exists -- validate before migrating. - # Skip in env-mode so we don't blow away an unrelated .venv at the - # workspace root (e.g. user's existing project Python venv). + if (Test-Path $VenvPython) { + # New layout already exists -- nuke for fresh install + substep "removing existing environment for fresh install..." + Remove-Item -Recurse -Force $VenvDir + } elseif (Test-Path (Join-Path $StudioHome ".venv\Scripts\python.exe")) { + # Old layout (~/.unsloth/studio/.venv) exists -- validate before migrating $OldVenv = Join-Path $StudioHome ".venv" $OldPy = Join-Path $OldVenv "Scripts\python.exe" substep "found legacy Studio environment, validating..." $prevEAP2 = $ErrorActionPreference $ErrorActionPreference = "Continue" try { - if ($SkipTorch) { - & $OldPy -c "import sys; print(sys.executable)" 2>$null | Out-Null - } else { - & $OldPy -c "import torch; A = torch.ones((2,2)); B = A + A" 2>$null | Out-Null - } - $legacyOk = ($LASTEXITCODE -eq 0) - } catch { $legacyOk = $false } + & $OldPy -c "import torch; A = torch.ones((2,2)); B = A + A" 2>$null | Out-Null + $torchOk = ($LASTEXITCODE -eq 0) + } catch { $torchOk = $false } $ErrorActionPreference = $prevEAP2 - if ($legacyOk) { + if ($torchOk) { substep "legacy environment is healthy -- migrating..." - Move-Item -LiteralPath $OldVenv -Destination $VenvDir -Force + Move-Item -Path $OldVenv -Destination $VenvDir -Force substep "moved .venv -> unsloth_studio" $_Migrated = $true } else { substep "legacy environment failed validation -- creating fresh environment" "Yellow" - $invalidVenv = Join-Path $StudioHome (".venv.invalid.{0}.{1}" -f (Get-Date -Format "yyyyMMddHHmmss"), $PID) - Move-Item -LiteralPath $OldVenv -Destination $invalidVenv -Force -ErrorAction SilentlyContinue + Remove-Item -Recurse -Force $OldVenv -ErrorAction SilentlyContinue } - } elseif ( - $StudioRedirectMode -ne 'env' ` - -and (Test-Path -LiteralPath (Join-Path $env:USERPROFILE "unsloth_studio\Scripts\python.exe")) - ) { - # CWD-relative venv from old install.ps1 -> migrate to absolute path. - # Skip in env-mode so we don't relocate the default-install venv into - # the workspace root. + } elseif (Test-Path (Join-Path $env:USERPROFILE "unsloth_studio\Scripts\python.exe")) { + # CWD-relative venv from old install.ps1 -- migrate to absolute path $CwdVenv = Join-Path $env:USERPROFILE "unsloth_studio" substep "found CWD-relative Studio environment, migrating to $VenvDir..." - Move-Item -LiteralPath $CwdVenv -Destination $VenvDir -Force + Move-Item -Path $CwdVenv -Destination $VenvDir -Force substep "moved ~/unsloth_studio -> ~/.unsloth/studio/unsloth_studio" $_Migrated = $true } - if (-not (Test-Path -LiteralPath $VenvPython)) { + if (-not (Test-Path $VenvPython)) { step "venv" "creating Python $($DetectedPython.Version) virtual environment" substep "$VenvDir" $venvExit = Invoke-InstallCommand { uv venv $VenvDir --python "$($DetectedPython.Path)" } if ($venvExit -ne 0) { Write-Host "[ERROR] Failed to create virtual environment (exit code $venvExit)" -ForegroundColor Red - return (Exit-InstallFailure "Failed to create virtual environment (exit code $venvExit)" $venvExit) + return } } else { step "venv" "using migrated environment" substep "$VenvDir" } - # Mark the freshly-created venv as Studio-owned so a partial install can be - # repaired by re-running install.ps1; the env-mode deletion guard above - # accepts this marker as the primary sentinel. - if (Test-Path -LiteralPath $VenvDir -PathType Container) { - try { [System.IO.File]::WriteAllText((Join-Path $VenvDir ".unsloth-studio-owned"), "") } catch {} - } - # ── Detect GPU (robust: PATH + hardcoded fallback paths, mirrors setup.ps1) ── $HasNvidiaSmi = $false $NvidiaSmiExe = $null @@ -1234,9 +865,6 @@ shell.Run cmd, 0, False return "$baseUrl/cu126" } $TorchIndexUrl = Get-TorchIndexUrl - $TorchIndexFamily = Get-TauriTorchIndexFamily $TorchIndexUrl - $GpuBranch = Get-TauriGpuBranch $TorchIndexFamily - Write-TauriDiag -GpuBranch $GpuBranch -TorchIndexFamily $TorchIndexFamily -PythonVersionForDiag $DetectedPython.Version # ── Print CPU-only hint when no GPU detected ── if (-not $SkipTorch -and $TorchIndexUrl -like "*/cpu") { @@ -1271,7 +899,7 @@ shell.Run cmd, 0, False if ($StudioLocalInstall -and (Test-Path (Join-Path $RepoRoot "studio\backend\requirements\no-torch-runtime.txt"))) { return Join-Path $RepoRoot "studio\backend\requirements\no-torch-runtime.txt" } - $installed = Get-ChildItem -LiteralPath $VenvDir -Recurse -Filter "no-torch-runtime.txt" -ErrorAction SilentlyContinue | + $installed = Get-ChildItem -Path $VenvDir -Recurse -Filter "no-torch-runtime.txt" -ErrorAction SilentlyContinue | Where-Object { $_.FullName -like "*studio*backend*requirements*no-torch-runtime.txt" } | Select-Object -ExpandProperty FullName -First 1 return $installed @@ -1280,12 +908,11 @@ shell.Run cmd, 0, False if ($_Migrated) { # Migrated env: force-reinstall unsloth+unsloth-zoo to ensure clean state # in the new venv location, while preserving existing torch/CUDA - Write-TauriLog "STEP" "Installing unsloth" substep "upgrading unsloth in migrated environment..." if ($SkipTorch) { # No-torch: install unsloth + unsloth-zoo with --no-deps, then # runtime deps (typer, safetensors, transformers, etc.) with --no-deps. - $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --no-deps --reinstall-package unsloth --reinstall-package unsloth-zoo "unsloth>=2026.5.2" unsloth-zoo } + $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --no-deps --reinstall-package unsloth --reinstall-package unsloth-zoo "unsloth>=2026.4.7" unsloth-zoo } if ($baseInstallExit -eq 0) { $NoTorchReq = Find-NoTorchRuntimeFile if ($NoTorchReq) { @@ -1293,45 +920,37 @@ shell.Run cmd, 0, False } } } else { - $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --reinstall-package unsloth --reinstall-package unsloth-zoo "unsloth>=2026.5.2" unsloth-zoo } + $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --reinstall-package unsloth --reinstall-package unsloth-zoo "unsloth>=2026.4.7" unsloth-zoo } } if ($baseInstallExit -ne 0) { Write-Host "[ERROR] Failed to install unsloth (exit code $baseInstallExit)" -ForegroundColor Red - return (Exit-InstallFailure "Failed to install unsloth (exit code $baseInstallExit)" $baseInstallExit) + return } if ($StudioLocalInstall) { substep "overlaying local repo (editable)..." $overlayExit = Invoke-InstallCommand { uv pip install --python $VenvPython -e $RepoRoot --no-deps } if ($overlayExit -ne 0) { Write-Host "[ERROR] Failed to overlay local repo (exit code $overlayExit)" -ForegroundColor Red - return (Exit-InstallFailure "Failed to overlay local repo (exit code $overlayExit)" $overlayExit) - } - substep "overlaying unsloth-zoo from git main..." - $zooOverlayExit = Invoke-InstallCommand { uv pip install --python $VenvPython --no-deps --reinstall-package unsloth-zoo "unsloth-zoo @ git+https://github.com/unslothai/unsloth-zoo" } - if ($zooOverlayExit -ne 0) { - Write-Host "[ERROR] Failed to overlay unsloth-zoo (exit code $zooOverlayExit)" -ForegroundColor Red - return (Exit-InstallFailure "Failed to overlay unsloth-zoo (exit code $zooOverlayExit)" $zooOverlayExit) + return } } } elseif ($TorchIndexUrl) { if ($SkipTorch) { substep "skipping PyTorch (--no-torch flag set)." "Yellow" } else { - Write-TauriLog "STEP" "Installing PyTorch" substep "installing PyTorch ($TorchIndexUrl)..." $torchInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython "torch>=2.4,<2.11.0" torchvision torchaudio --index-url $TorchIndexUrl } if ($torchInstallExit -ne 0) { Write-Host "[ERROR] Failed to install PyTorch (exit code $torchInstallExit)" -ForegroundColor Red - return (Exit-InstallFailure "Failed to install PyTorch (exit code $torchInstallExit)" $torchInstallExit) + return } } - Write-TauriLog "STEP" "Installing unsloth" substep "installing unsloth (this may take a few minutes)..." if ($SkipTorch) { # No-torch: install unsloth + unsloth-zoo with --no-deps, then # runtime deps (typer, safetensors, transformers, etc.) with --no-deps. - $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --no-deps --upgrade-package unsloth --upgrade-package unsloth-zoo "unsloth>=2026.5.2" unsloth-zoo } + $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --no-deps --upgrade-package unsloth --upgrade-package unsloth-zoo "unsloth>=2026.4.7" unsloth-zoo } if ($baseInstallExit -eq 0) { $NoTorchReq = Find-NoTorchRuntimeFile if ($NoTorchReq) { @@ -1339,13 +958,13 @@ shell.Run cmd, 0, False } } } elseif ($StudioLocalInstall) { - $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --upgrade-package unsloth "unsloth>=2026.5.2" unsloth-zoo } + $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --upgrade-package unsloth "unsloth>=2026.4.7" unsloth-zoo } } else { - $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --upgrade-package unsloth -- "$PackageName" } + $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --upgrade-package unsloth "$PackageName" } } if ($baseInstallExit -ne 0) { Write-Host "[ERROR] Failed to install unsloth (exit code $baseInstallExit)" -ForegroundColor Red - return (Exit-InstallFailure "Failed to install unsloth (exit code $baseInstallExit)" $baseInstallExit) + return } if ($StudioLocalInstall) { @@ -1353,87 +972,29 @@ shell.Run cmd, 0, False $overlayExit = Invoke-InstallCommand { uv pip install --python $VenvPython -e $RepoRoot --no-deps } if ($overlayExit -ne 0) { Write-Host "[ERROR] Failed to overlay local repo (exit code $overlayExit)" -ForegroundColor Red - return (Exit-InstallFailure "Failed to overlay local repo (exit code $overlayExit)" $overlayExit) - } - substep "overlaying unsloth-zoo from git main..." - $zooOverlayExit = Invoke-InstallCommand { uv pip install --python $VenvPython --no-deps --reinstall-package unsloth-zoo "unsloth-zoo @ git+https://github.com/unslothai/unsloth-zoo" } - if ($zooOverlayExit -ne 0) { - Write-Host "[ERROR] Failed to overlay unsloth-zoo (exit code $zooOverlayExit)" -ForegroundColor Red - return (Exit-InstallFailure "Failed to overlay unsloth-zoo (exit code $zooOverlayExit)" $zooOverlayExit) + return } } } else { # Fallback: GPU detection failed to produce a URL -- let uv resolve torch - Write-TauriLog "STEP" "Installing unsloth" substep "installing unsloth (this may take a few minutes)..." if ($StudioLocalInstall) { - $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython unsloth-zoo "unsloth>=2026.5.2" --torch-backend=auto } + $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython unsloth-zoo "unsloth>=2026.4.7" --torch-backend=auto } if ($baseInstallExit -ne 0) { Write-Host "[ERROR] Failed to install unsloth (exit code $baseInstallExit)" -ForegroundColor Red - return (Exit-InstallFailure "Failed to install unsloth (exit code $baseInstallExit)" $baseInstallExit) + return } substep "overlaying local repo (editable)..." $overlayExit = Invoke-InstallCommand { uv pip install --python $VenvPython -e $RepoRoot --no-deps } if ($overlayExit -ne 0) { Write-Host "[ERROR] Failed to overlay local repo (exit code $overlayExit)" -ForegroundColor Red - return (Exit-InstallFailure "Failed to overlay local repo (exit code $overlayExit)" $overlayExit) - } - substep "overlaying unsloth-zoo from git main..." - $zooOverlayExit = Invoke-InstallCommand { uv pip install --python $VenvPython --no-deps --reinstall-package unsloth-zoo "unsloth-zoo @ git+https://github.com/unslothai/unsloth-zoo" } - if ($zooOverlayExit -ne 0) { - Write-Host "[ERROR] Failed to overlay unsloth-zoo (exit code $zooOverlayExit)" -ForegroundColor Red - return (Exit-InstallFailure "Failed to overlay unsloth-zoo (exit code $zooOverlayExit)" $zooOverlayExit) + return } } else { - $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --torch-backend=auto -- "$PackageName" } + $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython "$PackageName" --torch-backend=auto } if ($baseInstallExit -ne 0) { Write-Host "[ERROR] Failed to install unsloth (exit code $baseInstallExit)" -ForegroundColor Red - return (Exit-InstallFailure "Failed to install unsloth (exit code $baseInstallExit)" $baseInstallExit) - } - } - } - - # Overlay Tauri-bundled studio fixes that may be ahead of PyPI. Skipped - # for --local: the editable install above already makes _PACKAGE_ROOT in - # unsloth_cli/commands/studio.py resolve to the repo (PEP 660 __file__). - # Source paths match the Tauri bundle layout in studio/src-tauri/tauri.conf.json, - # which bundles install_python_stack.py at the bundle root next to install.ps1. - if ($TauriMode) { - $rawPath = if ($PSCommandPath) { $PSCommandPath } else { $MyInvocation.ScriptName } - if ($rawPath) { - # Strip leading \\?\ extended-length prefix if the launcher passed one. - $scriptDir = Split-Path -Parent ($rawPath -replace '^\\\\\?\\', '') - $overlayMap = [ordered]@{ - "install_python_stack.py" = "Lib\site-packages\studio\install_python_stack.py" - } - foreach ($rel in $overlayMap.Keys) { - $src = Join-Path $scriptDir $rel - $dst = Join-Path $VenvDir $overlayMap[$rel] - # -LiteralPath: $VenvDir derives from $StudioHome which may - # contain [ ] * ? when the user overrode UNSLOTH_STUDIO_HOME. - if (-not (Test-Path -LiteralPath $src)) { continue } - $dstParent = Split-Path -Parent $dst - if (-not (Test-Path -LiteralPath $dstParent)) { - Write-Host "[WARN] Overlay target dir missing: $dstParent; studio setup may use stale bundled file" -ForegroundColor Yellow - continue - } - try { - if (-not (Test-Path -LiteralPath $dst)) { - # Backfill: target file missing but parent dir exists. - Copy-Item -LiteralPath $src -Destination $dst -Force - substep ("backfilled bundled " + (Split-Path -Leaf $rel)) - } else { - # Hash-compare so re-runs are no-ops when files already match. - $srcHash = (Get-FileHash -LiteralPath $src -Algorithm SHA256).Hash - $dstHash = (Get-FileHash -LiteralPath $dst -Algorithm SHA256).Hash - if ($srcHash -ne $dstHash) { - Copy-Item -LiteralPath $src -Destination $dst -Force - substep ("applied bundled " + (Split-Path -Leaf $rel)) - } - } - } catch { - Write-Host "[WARN] Could not overlay $($rel): $($_.Exception.Message); studio setup may use stale bundled file" -ForegroundColor Yellow - } + return } } } @@ -1441,23 +1002,19 @@ shell.Run cmd, 0, False # ── Run studio setup ── # setup.ps1 will handle installing Git, CMake, Visual Studio Build Tools, # CUDA Toolkit, Node.js, and other dependencies automatically via winget. - Write-TauriLog "STEP" "Running studio setup" step "setup" "running unsloth studio setup..." $UnslothExe = Join-Path $VenvDir "Scripts\unsloth.exe" - if (-not (Test-Path -LiteralPath $UnslothExe)) { - Write-TauriLog "ERROR" "unsloth CLI was not installed correctly" + if (-not (Test-Path $UnslothExe)) { Write-Host "[ERROR] unsloth CLI was not installed correctly." -ForegroundColor Red Write-Host " Expected: $UnslothExe" -ForegroundColor Yellow Write-Host " This usually means an older unsloth version was installed that does not include the Studio CLI." -ForegroundColor Yellow Write-Host " Try re-running the installer or see: https://github.com/unslothai/unsloth?tab=readme-ov-file#-quickstart" -ForegroundColor Yellow - return (Exit-InstallFailure "unsloth CLI was not installed correctly") + return } # Tell setup.ps1 to skip base package installation (install.ps1 already did it) $env:SKIP_STUDIO_BASE = "1" $env:STUDIO_PACKAGE_NAME = $PackageName $env:UNSLOTH_NO_TORCH = if ($SkipTorch) { "true" } else { "false" } - # Tauri desktop app bundles its own frontend — skip Node/npm/frontend build - $env:SKIP_STUDIO_FRONTEND = if ($TauriMode) { "1" } else { "0" } # Always set STUDIO_LOCAL_INSTALL explicitly to avoid stale values from # a previous --local run in the same PowerShell session. if ($StudioLocalInstall) { @@ -1470,34 +1027,17 @@ shell.Run cmd, 0, False # Use 'studio setup' (not 'studio update') because 'update' pops # SKIP_STUDIO_BASE, which would cause redundant package reinstallation # and bypass the fast-path version check from PR #4667. - # Propagate UNSLOTH_STUDIO_HOME only for env-override installs; otherwise - # an inherited value would put llama.cpp in the wrong place. - $previousUnslothStudioHome = $env:UNSLOTH_STUDIO_HOME - $hadPreviousUnslothStudioHome = ($null -ne $previousUnslothStudioHome) - if ($StudioRedirectMode -eq 'env') { - $env:UNSLOTH_STUDIO_HOME = $StudioHome - } else { - Remove-Item Env:UNSLOTH_STUDIO_HOME -ErrorAction SilentlyContinue - } $studioArgs = @('studio', 'setup') if ($script:UnslothVerbose) { $studioArgs += '--verbose' } - $env:UNSLOTH_INSTALL_ROLLBACK_MANAGED = "1" - try { - & $UnslothExe @studioArgs - $setupExit = $LASTEXITCODE - } finally { - if ($hadPreviousUnslothStudioHome) { - $env:UNSLOTH_STUDIO_HOME = $previousUnslothStudioHome - } else { - Remove-Item Env:UNSLOTH_STUDIO_HOME -ErrorAction SilentlyContinue - } - Remove-Item Env:UNSLOTH_INSTALL_ROLLBACK_MANAGED -ErrorAction SilentlyContinue - } + & $UnslothExe @studioArgs + $setupExit = $LASTEXITCODE if ($setupExit -ne 0) { Write-Host "[ERROR] unsloth studio setup failed (exit code $setupExit)" -ForegroundColor Red - return (Exit-InstallFailure "unsloth studio setup failed (exit code $setupExit)" $setupExit) + return } + New-StudioShortcuts -UnslothExePath $UnslothExe + # ── Expose `unsloth` via a shim dir containing only unsloth.exe ── # We do NOT add the venv Scripts dir to PATH (it also holds python.exe # and pip.exe, which would hijack the user's system interpreter). @@ -1535,32 +1075,20 @@ shell.Run cmd, 0, False } } catch { } $ShimDir = Join-Path $StudioHome "bin" - [System.IO.Directory]::CreateDirectory($ShimDir) | Out-Null + New-Item -ItemType Directory -Force -Path $ShimDir | Out-Null $ShimExe = Join-Path $ShimDir "unsloth.exe" - # Fatal preflight outside the lock-handling try/catch -- a directory at - # the shim path must not be downgraded to "Continuing with the existing - # launcher", or the install finishes with no usable shim. - if (Test-Path -LiteralPath $ShimExe -PathType Container) { - Write-Host "[ERROR] Cannot create unsloth launcher: $ShimExe is a directory." -ForegroundColor Red - Write-Host " Move or remove it manually, then re-run the installer." -ForegroundColor Yellow - throw "Cannot create unsloth launcher: $ShimExe is a directory." - } # try/catch: if unsloth.exe is locked (Studio running), keep the old shim. $shimUpdated = $false try { - if (Test-Path -LiteralPath $ShimExe) { Remove-Item -LiteralPath $ShimExe -Force -ErrorAction Stop } + if (Test-Path $ShimExe) { Remove-Item $ShimExe -Force -ErrorAction Stop } try { - # New-Item -ItemType HardLink does NOT accept -LiteralPath in any - # PowerShell version, so use -Path. Wildcards in $ShimExe (e.g. - # brackets in custom roots) glob-expand here and fall through to - # the Copy-Item -LiteralPath fallback below. New-Item -ItemType HardLink -Path $ShimExe -Target $UnslothExe -ErrorAction Stop | Out-Null } catch { - Copy-Item -LiteralPath $UnslothExe -Destination $ShimExe -Force -ErrorAction Stop # fallback: copy + Copy-Item -Path $UnslothExe -Destination $ShimExe -Force -ErrorAction Stop # fallback: copy } $shimUpdated = $true } catch { - if (Test-Path -LiteralPath $ShimExe) { + if (Test-Path $ShimExe) { Write-Host "[WARN] Could not refresh unsloth launcher at $ShimExe." -ForegroundColor Yellow Write-Host " This usually means a running 'unsloth studio' process still holds the file open." -ForegroundColor Yellow Write-Host " Close Studio and re-run the installer to pick up the latest launcher." -ForegroundColor Yellow @@ -1571,68 +1099,25 @@ shell.Run cmd, 0, False Write-Host " Launch unsloth studio directly via '$UnslothExe' until the next successful install." -ForegroundColor Yellow } } - # Add to PATH only when launcher exists. Env-mode: session-only export, - # no registry change (workspace path may be deleted later). + # Only add to PATH when the launcher actually exists on disk. $pathAdded = $false - if (Test-Path -LiteralPath $ShimExe) { - if ($StudioRedirectMode -ne 'env') { - $pathAdded = Add-ToUserPath -Directory $ShimDir -Position 'Prepend' - } + if (Test-Path $ShimExe) { + $pathAdded = Add-ToUserPath -Directory $ShimDir -Position 'Prepend' } if ($shimUpdated -and $pathAdded) { step "path" "added unsloth launcher to PATH" } Refresh-SessionPath # sync current session with registry - Complete-StudioVenvRollback - - # Env-mode session export AFTER Refresh-SessionPath; otherwise a legacy - # User PATH entry (Machine > User > current $env:Path) would win. - if ($StudioRedirectMode -eq 'env' -and (Test-Path -LiteralPath $ShimExe)) { - $env:Path = "$ShimDir;$env:Path" - step "path" "exported $ShimDir for this session (no registry PATH change in env-override mode)" - } - - # ── Tauri mode: done, skip shortcuts and auto-launch ── - if ($TauriMode) { - Write-TauriLog "DONE" "" - return - } - # New-StudioShortcuts gates the .lnk shortcuts on env-mode internally. - New-StudioShortcuts -UnslothExePath $UnslothExe - - # In interactive terminals, ask the user before starting Studio. - # In non-interactive environments (CI, Docker) just print instructions. + # Launch studio automatically in interactive terminals; + # in non-interactive environments (CI, Docker) just print instructions. $IsInteractive = [Environment]::UserInteractive -and (-not [Console]::IsInputRedirected) if ($IsInteractive) { - Write-Host "" - $reply = Read-Host " Start Unsloth Studio now? [Y/n]" - if ([string]::IsNullOrWhiteSpace($reply) -or $reply -match '^[Yy]') { - & $UnslothExe studio -p 8888 - } else { - step "launch" "to start later, run:" - substep "unsloth studio -p 8888" - substep "(add -H 0.0.0.0 to allow network / cloud access)" - Write-Host "" - } + & $UnslothExe studio -H 0.0.0.0 -p 8888 } else { step "launch" "manual commands:" - # Single-quote the printed paths so $-vars / backticks in custom roots - # do not reparse when the user pastes the command. - $_actLiteral = "'" + ((Join-Path $VenvDir "Scripts\Activate.ps1") -replace "'", "''") + "'" - if ($StudioRedirectMode -eq 'env') { - # Env-mode skips registry PATH; print the absolute shim path. - $_shim = Join-Path $StudioHome "bin\unsloth.exe" - $_shimLiteral = "'" + ($_shim -replace "'", "''") + "'" - substep "& $_shimLiteral studio -p 8888" - substep "or activate env first:" - substep "& $_actLiteral" - substep "unsloth studio -p 8888" - } else { - substep "& $_actLiteral" - substep "unsloth studio -p 8888" - } - substep "(add -H 0.0.0.0 to allow network / cloud access)" + substep "& `"$VenvDir\Scripts\Activate.ps1`"" + substep "unsloth studio -H 0.0.0.0 -p 8888" Write-Host "" } } diff --git a/install.sh b/install.sh index 9046a9bdf6..0352887de1 100755 --- a/install.sh +++ b/install.sh @@ -6,12 +6,6 @@ # Usage (no-torch): ./install.sh --no-torch (skip PyTorch, GGUF-only mode) # Usage (test): ./install.sh --package roland-sloth (install a different package name) # Usage (py): ./install.sh --python 3.12 (override auto-detected Python version) -# -# Env vars (priority: UNSLOTH_STUDIO_HOME > STUDIO_HOME > HOME-redirect > default): -# UNSLOTH_STUDIO_HOME=/abs/path -> install under that path -# STUDIO_HOME=/abs/path -> alias, same effect (UNSLOTH_STUDIO_HOME wins) -# (DATA_DIR + unsloth CLI shim nest inside; no shell rc-file append.) -# Default ($HOME/.unsloth/studio) is preserved when no env var is set. set -e # ── Output style (aligned with studio/setup.sh) ── @@ -41,7 +35,6 @@ substep() { printf " ${C_DIM}%-15s${2:-$C_DIM}%s${C_RST}\n" "" "$1"; } # ── Parse flags ── STUDIO_LOCAL_INSTALL=false PACKAGE_NAME="unsloth" -TAURI_MODE=false _USER_PYTHON="" _NO_TORCH_FLAG=false _VERBOSE=false @@ -61,7 +54,6 @@ for arg in "$@"; do case "$arg" in --local) STUDIO_LOCAL_INSTALL=true ;; --package) _next_is_package=true ;; - --tauri) TAURI_MODE=true ;; --python) _next_is_python=true ;; --no-torch) _NO_TORCH_FLAG=true ;; --verbose|-v) _VERBOSE=true ;; @@ -72,56 +64,6 @@ if [ "$_VERBOSE" = true ]; then export UNSLOTH_VERBOSE=1 fi -# Custom Studio roots are not supported with --tauri (desktop app still -# resolves ~/.unsloth/studio). Pass through if the override == legacy default. -if [ "$TAURI_MODE" = true ]; then - _tauri_override_var="" - _tauri_override="${UNSLOTH_STUDIO_HOME:-}" - if [ -n "$_tauri_override" ]; then - _tauri_override_var="UNSLOTH_STUDIO_HOME" - else - _tauri_override="${STUDIO_HOME:-}" - [ -n "$_tauri_override" ] && _tauri_override_var="STUDIO_HOME" - fi - # Strip whitespace so " " is treated as unset (matches Python .strip()). - _tauri_override=$(printf '%s' "$_tauri_override" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//') - if [ -n "$_tauri_override" ]; then - case "$_tauri_override" in - "~") _tauri_override="$HOME" ;; - "~/"*) _tauri_override="$HOME/${_tauri_override#'~/'}" ;; - esac - # Canonicalize both sides (CDPATH=, -P) so a CDPATH-set env or - # symlinked $HOME doesn't break the legacy-equality comparison. - if [ -d "$_tauri_override" ]; then - _tauri_override_abs=$(CDPATH= cd -P -- "$_tauri_override" 2>/dev/null && pwd -P) \ - || _tauri_override_abs="$_tauri_override" - else - _tauri_override_abs="$_tauri_override" - fi - # Strip trailing separators so ".../studio/" matches ".../studio". - while [ "$_tauri_override_abs" != "/" ] \ - && [ "${_tauri_override_abs%/}" != "$_tauri_override_abs" ]; do - _tauri_override_abs=${_tauri_override_abs%/} - done - _tauri_legacy_root="$HOME/.unsloth/studio" - if [ -d "$_tauri_legacy_root" ]; then - _tauri_legacy_root=$(CDPATH= cd -P -- "$_tauri_legacy_root" 2>/dev/null && pwd -P) \ - || _tauri_legacy_root="$HOME/.unsloth/studio" - fi - while [ "$_tauri_legacy_root" != "/" ] \ - && [ "${_tauri_legacy_root%/}" != "$_tauri_legacy_root" ]; do - _tauri_legacy_root=${_tauri_legacy_root%/} - done - if [ "$_tauri_override_abs" != "$_tauri_legacy_root" ]; then - echo "ERROR: $_tauri_override_var is not supported with --tauri." >&2 - echo " The desktop app still uses the legacy ~/.unsloth/studio root." >&2 - echo " Run install.sh without --tauri for custom-root shell installs," >&2 - echo " or unset the env var for default desktop installs." >&2 - exit 1 - fi - fi -fi - _is_verbose() { [ "${UNSLOTH_VERBOSE:-0}" = "1" ] } @@ -200,197 +142,9 @@ if [ "$_next_is_python" = true ]; then exit 1 fi -# Validate --package to prevent injection into shell/Python commands. -# Must start with a letter/digit (rejects leading dashes that uv would parse as flags). -case "$PACKAGE_NAME" in - [!a-zA-Z0-9]*) - echo "❌ ERROR: --package name must start with a letter or digit." >&2 - exit 1 ;; - *[!a-zA-Z0-9._-]*) - echo "❌ ERROR: --package name contains invalid characters (allowed: a-z A-Z 0-9 . _ -)" >&2 - exit 1 ;; -esac - -# ── Tauri structured output ── -tauri_log() { - if [ "$TAURI_MODE" = true ]; then - echo "[TAURI:$1] $2" - fi -} - -tauri_diag_marker() { - _diag_gpu_branch="${1:-unknown}" - _diag_torch_index_family="${2:-none}" - tauri_log "DIAG" "diag_schema=1 platform=${OS:-unknown} arch=${_ARCH:-unknown} python_version=${PYTHON_VERSION:-unknown} skip_torch=${SKIP_TORCH:-false} mac_intel=${MAC_INTEL:-false} gpu_branch=${_diag_gpu_branch} torch_index_family=${_diag_torch_index_family}" -} - -_tauri_torch_index_family() { - if [ "${SKIP_TORCH:-false}" = true ]; then - echo "none" - return - fi - _diag_url="${1:-}" - case "$_diag_url" in - */cu118) echo "cu118" ;; - */cu124) echo "cu124" ;; - */cu126) echo "cu126" ;; - */cu128) echo "cu128" ;; - */cu130) echo "cu130" ;; - */cpu) echo "cpu" ;; - */rocm[0-9]*.[0-9]*) - _diag_family=${_diag_url##*/} - case "$_diag_family" in - rocm[0-9]*.[0-9]*) echo "$_diag_family" ;; - *) echo "auto" ;; - esac ;; - "") echo "none" ;; - *) echo "auto" ;; - esac -} - -_tauri_gpu_branch() { - _diag_family="${1:-unknown}" - _diag_radeon="${2:-false}" - if [ "${SKIP_TORCH:-false}" = true ]; then - echo "no_torch" - return - fi - if [ "${OS:-}" = "macos" ]; then - echo "mac" - return - fi - case "$_diag_family" in - cu*) echo "cuda" ;; - rocm*) - if [ "$_diag_radeon" = true ]; then - echo "rocm_radeon" - else - echo "rocm" - fi ;; - radeon) echo "rocm_radeon" ;; - cpu) echo "cpu" ;; - none) echo "no_torch" ;; - *) echo "unknown" ;; - esac -} - PYTHON_VERSION="" # resolved after platform detection - -# Resolve install destinations: env override, HOME-redirect (best-effort -# via getent/dscl), or default. Env-var priority: UNSLOTH_STUDIO_HOME wins -# over STUDIO_HOME (the more specific signal beats the generic alias). -_resolve_studio_destinations() { - _override_var="" - _override="${UNSLOTH_STUDIO_HOME:-}" - if [ -n "$_override" ]; then - _override_var="UNSLOTH_STUDIO_HOME" - else - _override="${STUDIO_HOME:-}" - [ -n "$_override" ] && _override_var="STUDIO_HOME" - fi - # Strip surrounding whitespace so " " is treated as unset (matches the - # Python resolvers' .strip()), preventing install/runtime layout drift. - _override=$(printf '%s' "$_override" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//') - # Tilde expansion: env vars are not subject to it when quoted on assignment. - case "$_override" in - "~") _override="$HOME" ;; - "~/"*) _override="$HOME/${_override#'~/'}" ;; - esac - if [ -n "$_override" ]; then - mkdir -p -- "$_override" 2>/dev/null || { echo "ERROR: $_override_var=$_override cannot be created." >&2; exit 1; } - [ -w "$_override" ] || { echo "ERROR: $_override_var=$_override is not writable." >&2; exit 1; } - STUDIO_HOME="$(CDPATH= cd -P -- "$_override" && pwd -P)" || exit 1 - DATA_DIR="$STUDIO_HOME/share" - _LOCAL_BIN="$STUDIO_HOME/bin" - _STUDIO_HOME_REDIRECT=env - substep "custom $_override_var=$STUDIO_HOME" - return 0 - fi - _default_home="" - if command -v getent >/dev/null 2>&1; then - _default_home=$(getent passwd "${USER:-$(whoami)}" 2>/dev/null | cut -d: -f6) - elif [ "$(uname)" = "Darwin" ] && command -v dscl >/dev/null 2>&1; then - _default_home=$(dscl . -read "/Users/${USER:-$(whoami)}" NFSHomeDirectory 2>/dev/null | awk '{print $2}') - fi - # Canonicalize both sides so a trailing slash on $HOME (or symlink mismatch - # with passwd-DB output) doesn't misfire the redirection branch. - _home_canon="$HOME" - if [ -d "$_home_canon" ]; then - _home_canon=$(CDPATH= cd -P -- "$_home_canon" 2>/dev/null && pwd -P) || _home_canon="$HOME" - fi - _default_home_canon="$_default_home" - if [ -n "$_default_home_canon" ] && [ -d "$_default_home_canon" ]; then - _default_home_canon=$(CDPATH= cd -P -- "$_default_home_canon" 2>/dev/null && pwd -P) || _default_home_canon="$_default_home" - fi - if [ -n "$_default_home_canon" ] && [ "$_home_canon" != "$_default_home_canon" ]; then - STUDIO_HOME="$HOME/.unsloth/studio" - DATA_DIR="$HOME/.local/share/unsloth" - _LOCAL_BIN="$HOME/.local/bin" - _STUDIO_HOME_REDIRECT=home - substep "HOME redirected ($HOME); install follows \$HOME" - return 0 - fi - STUDIO_HOME="$HOME/.unsloth/studio" - DATA_DIR="$HOME/.local/share/unsloth" - _LOCAL_BIN="$HOME/.local/bin" - _STUDIO_HOME_REDIRECT=default -} -_resolve_studio_destinations +STUDIO_HOME="$HOME/.unsloth/studio" VENV_DIR="$STUDIO_HOME/unsloth_studio" -_VENV_ROLLBACK_DIR="" -_VENV_ROLLBACK_TARGET="$VENV_DIR" -_VENV_ROLLBACK_ACTIVE=false - -_start_studio_venv_replacement() { - _existing_dir="$1" - _stamp=$(date +%Y%m%d%H%M%S 2>/dev/null || echo "time") - _candidate="$STUDIO_HOME/unsloth_studio.rollback.$_stamp.$$" - _suffix=0 - while [ -e "$_candidate" ]; do - _suffix=$((_suffix + 1)) - _candidate="$STUDIO_HOME/unsloth_studio.rollback.$_stamp.$$.$_suffix" - done - mv "$_existing_dir" "$_candidate" - _VENV_ROLLBACK_DIR="$_candidate" - _VENV_ROLLBACK_TARGET="$_existing_dir" - _VENV_ROLLBACK_ACTIVE=true - substep "previous environment preserved for rollback" -} - -_restore_studio_venv_replacement() { - [ "$_VENV_ROLLBACK_ACTIVE" = true ] || return 0 - [ -n "$_VENV_ROLLBACK_DIR" ] && [ -d "$_VENV_ROLLBACK_DIR" ] || { - _VENV_ROLLBACK_ACTIVE=false - return 0 - } - substep "restoring previous environment after failed install..." "$C_WARN" - rm -rf "$_VENV_ROLLBACK_TARGET" - if mv "$_VENV_ROLLBACK_DIR" "$_VENV_ROLLBACK_TARGET"; then - substep "restored previous environment" - _VENV_ROLLBACK_ACTIVE=false - _VENV_ROLLBACK_DIR="" - else - echo "⚠️ Could not restore previous environment from $_VENV_ROLLBACK_DIR to $_VENV_ROLLBACK_TARGET" >&2 - fi -} - -_commit_studio_venv_replacement() { - [ "$_VENV_ROLLBACK_ACTIVE" = true ] || return 0 - if [ -n "$_VENV_ROLLBACK_DIR" ] && [ -d "$_VENV_ROLLBACK_DIR" ]; then - rm -rf "$_VENV_ROLLBACK_DIR" || true - fi - _VENV_ROLLBACK_ACTIVE=false - _VENV_ROLLBACK_DIR="" -} - -_on_install_exit() { - _status=$? - if [ "$_status" -ne 0 ]; then - _restore_studio_venv_replacement - fi - exit "$_status" -} -trap _on_install_exit EXIT # ── Helper: download a URL to a file (supports curl and wget) ── download() { @@ -438,12 +192,6 @@ _smart_apt_install() { return 0 fi - # In Tauri mode, report needed packages and exit — Rust handles elevation - if [ "$TAURI_MODE" = true ]; then - tauri_log "NEED_SUDO" "$_STILL_MISSING" - exit 2 - fi - # Step 3: Escalate -- need elevated permissions for remaining packages if command -v sudo >/dev/null 2>&1; then echo "" @@ -499,65 +247,23 @@ create_studio_shortcuts() { _css_exe_dir=$(cd "$(dirname "$_css_exe")" && pwd) _css_exe="$_css_exe_dir/$(basename "$_css_exe")" - _css_data_dir="$DATA_DIR" + _css_data_dir="$HOME/.local/share/unsloth" _css_launcher="$_css_data_dir/launch-studio.sh" _css_icon_png="$_css_data_dir/unsloth-studio.png" _css_gem_png="$_css_data_dir/unsloth-gem.png" mkdir -p "$_css_data_dir" - # Same-install discriminator: per-install opaque id written once at install - # time and read by both this launcher and the backend (/api/health). Replaces - # the older sha256(canonical $STUDIO_HOME) scheme to (a) avoid leaking the - # install path on -H 0.0.0.0 deployments and (b) sidestep launcher/backend - # canonicalization drift (cd -P vs Path.resolve() symlink/junction handling). - # Lives at $STUDIO_HOME/share/ (not $DATA_DIR) so the backend can find it - # via _STUDIO_ROOT_RESOLVED / "share" / "studio_install_id" regardless of - # mode (in env-mode $STUDIO_HOME/share == $DATA_DIR; in default mode they - # diverge but the backend only knows the studio_root). 32 bytes of urandom - # -> 64 hex chars, byte-compatible with the prior digest so launcher - # placeholder, _check_health, and tests stay length-agnostic. - _css_id_dir="$STUDIO_HOME/share" - mkdir -p "$_css_id_dir" - _css_id_file="$_css_id_dir/studio_install_id" - if [ ! -s "$_css_id_file" ]; then - if [ -r /dev/urandom ]; then - _css_new_id=$(od -An -N32 -tx1 /dev/urandom 2>/dev/null | tr -d ' \n') - fi - if [ -z "${_css_new_id:-}" ] && command -v python3 >/dev/null 2>&1; then - _css_new_id=$(python3 -c 'import secrets; print(secrets.token_hex(32))' 2>/dev/null) - fi - if [ -z "${_css_new_id:-}" ]; then - echo "[WARN] Cannot create launcher: no entropy source for studio_install_id" >&2 - return 1 - fi - # Atomic write so a partial install can't leave a half-written id. - _css_id_tmp="$_css_id_file.$$.tmp" - printf '%s' "$_css_new_id" > "$_css_id_tmp" \ - && mv "$_css_id_tmp" "$_css_id_file" - chmod 600 "$_css_id_file" 2>/dev/null || true - unset _css_new_id _css_id_tmp - fi - _css_studio_root_id=$(cat "$_css_id_file" 2>/dev/null) - if [ -z "$_css_studio_root_id" ]; then - echo "[WARN] Cannot create launcher: failed to read $_css_id_file" >&2 - return 1 - fi - _css_is_env_mode=false - [ "$_STUDIO_HOME_REDIRECT" = "env" ] && _css_is_env_mode=true - # ── Write launcher script ── - # Single-quoted heredoc; @@DATA_DIR@@, @@STUDIO_ROOT_ID@@, and - # @@INSTALLED_IS_ENV_MODE@@ are substituted via sed below. + # The launcher is Bash (not POSIX sh). + # We write it with a placeholder and substitute the exe path via sed. cat > "$_css_launcher" << 'LAUNCHER_EOF' #!/usr/bin/env bash # Unsloth Studio Launcher # Auto-generated by install.sh -- do not edit manually. set -euo pipefail -DATA_DIR='@@DATA_DIR@@' -_EXPECTED_STUDIO_ROOT_ID='@@STUDIO_ROOT_ID@@' -_INSTALLED_IS_ENV_MODE='@@INSTALLED_IS_ENV_MODE@@' +DATA_DIR="$HOME/.local/share/unsloth" # Read exe path from config written at install time. # Sourcing is safe: the config file is written by install.sh, not user input. @@ -574,23 +280,7 @@ MAX_PORT_OFFSET=20 TIMEOUT_SEC=60 POLL_INTERVAL_SEC=1 LOG_FILE="$DATA_DIR/studio.log" -# why: in env-override mode multiple installs share an OS user; namespace the -# lock and remember our own healthy port so we never attach to an unrelated -# Studio listening on the global 8888..8908 range. LOCK_DIR="${XDG_RUNTIME_DIR:-/tmp}/unsloth-studio-launcher-$(id -u).lock" -PORT_FILE="" -# why: gate on the install-time mode (baked above) instead of the runtime env -# var; sourcing a custom-root studio.conf in shell must not flip a default-mode -# launcher into env-mode behavior with stale state. -if [ "$_INSTALLED_IS_ENV_MODE" = "true" ]; then - if command -v cksum >/dev/null 2>&1; then - _LOCK_KEY=$(printf '%s' "$DATA_DIR" | cksum | awk '{print $1}') - else - _LOCK_KEY="" - fi - [ -n "$_LOCK_KEY" ] && LOCK_DIR="${XDG_RUNTIME_DIR:-/tmp}/unsloth-studio-launcher-$(id -u)-${_LOCK_KEY}.lock" - PORT_FILE="$DATA_DIR/studio.port" -fi # ── HTTP GET helper (supports curl and wget) ── _http_get() { @@ -609,20 +299,10 @@ _check_health() { _port=$1 _resp=$(_http_get "http://127.0.0.1:$_port/api/health") || return 1 case "$_resp" in - *'"status"'*'"healthy"'*'"service"'*'"Unsloth UI Backend"'*) ;; - *'"service"'*'"Unsloth UI Backend"'*'"status"'*'"healthy"'*) ;; - *) return 1 ;; + *'"status"'*'"healthy"'*'"service"'*'"Unsloth UI Backend"'*) return 0 ;; + *'"service"'*'"Unsloth UI Backend"'*'"status"'*'"healthy"'*) return 0 ;; esac - # why: verify the backend belongs to THIS install. Baked hex digest avoids - # JSON-escape mismatches on paths with `\`/`"` and avoids leaking the raw - # install path to unauthenticated callers. - if [ -n "$_EXPECTED_STUDIO_ROOT_ID" ]; then - case "$_resp" in - *"\"studio_root_id\":\"$_EXPECTED_STUDIO_ROOT_ID\""*|*"\"studio_root_id\": \"$_EXPECTED_STUDIO_ROOT_ID\""*) return 0 ;; - *) return 1 ;; - esac - fi - return 0 + return 1 } # ── Port scanning ── @@ -645,25 +325,6 @@ _candidate_ports() { } _find_healthy_port() { - if [ -n "$PORT_FILE" ] && [ -f "$PORT_FILE" ]; then - # why: env-mode installs only attach to a port we previously launched - # ourselves; never to a sibling Studio that happens to be healthy. - _p=$(cat "$PORT_FILE" 2>/dev/null || true) - case "$_p" in - ''|*[!0-9]*) ;; - *) - if _check_health "$_p"; then - echo "$_p" - return 0 - fi - rm -f "$PORT_FILE" - ;; - esac - return 1 - fi - if [ -n "$PORT_FILE" ]; then - return 1 - fi for _p in $(_candidate_ports | sort -un); do if _check_health "$_p"; then echo "$_p" @@ -814,7 +475,6 @@ if [ -t 1 ]; then _obwr_deadline=$(($(date +%s) + TIMEOUT_SEC)) while [ "$(date +%s)" -lt "$_obwr_deadline" ]; do if _check_health "$_launch_port"; then - [ -n "$PORT_FILE" ] && printf '%s\n' "$_launch_port" > "$PORT_FILE" 2>/dev/null || true _release_lock _open_browser "http://localhost:$_launch_port" exit 0 @@ -826,11 +486,11 @@ if [ -t 1 ]; then ) & # Clear traps so exec does not trigger _release_lock (the subshell owns it) trap - EXIT INT TERM - exec "$UNSLOTH_EXE" studio -p "$_launch_port" + exec "$UNSLOTH_EXE" studio -H 0.0.0.0 -p "$_launch_port" else # ── Background mode (no TTY) ── # Used by macOS .app and headless invocations. - _launch_cmd=$(printf '%q ' "$UNSLOTH_EXE" studio -p "$_launch_port") + _launch_cmd=$(printf '%q ' "$UNSLOTH_EXE" studio -H 0.0.0.0 -p "$_launch_port") _launch_cmd=${_launch_cmd% } _spawn_terminal "$_launch_cmd" @@ -838,7 +498,6 @@ else _deadline=$(($(date +%s) + TIMEOUT_SEC)) while [ "$(date +%s)" -lt "$_deadline" ]; do if _check_health "$_launch_port"; then - [ -n "$PORT_FILE" ] && printf '%s\n' "$_launch_port" > "$PORT_FILE" 2>/dev/null || true _open_browser "http://localhost:$_launch_port" exit 0 fi @@ -851,62 +510,13 @@ else fi LAUNCHER_EOF - # why: bake non-user-controlled placeholders FIRST so a literal - # `@@STUDIO_ROOT_ID@@` inside $DATA_DIR cannot be rewritten below. - sed -e "s|@@STUDIO_ROOT_ID@@|$_css_studio_root_id|g" \ - -e "s|@@INSTALLED_IS_ENV_MODE@@|$_css_is_env_mode|g" \ - "$_css_launcher" > "$_css_launcher.tmp" \ - && mv "$_css_launcher.tmp" "$_css_launcher" - - # Env-mode bakes an absolute DATA_DIR (root fixed at install time); - # default / HOME-redirect keeps the literal $HOME/.local/share/unsloth - # so behavior is byte-identical to pre-override. - if [ "$_STUDIO_HOME_REDIRECT" = "env" ]; then - # Two-stage escape: (1) `'` -> `'\''` for shell single-quote embedding, - # (2) backslash/&/| escape so the value survives the s|...|VALUE| sed - # below. Verified end-to-end with apostrophes, spaces, &, |, $. - _sq_escaped=$(printf '%s' "$DATA_DIR" | sed "s/'/'\\\\''/g") - _sed_safe=$(printf '%s' "$_sq_escaped" | sed 's/[\\&|]/\\&/g') - sed "s|@@DATA_DIR@@|$_sed_safe|g" "$_css_launcher" > "$_css_launcher.tmp" \ - && mv "$_css_launcher.tmp" "$_css_launcher" - else - sed "s|DATA_DIR='@@DATA_DIR@@'|DATA_DIR=\"\$HOME/.local/share/unsloth\"|" \ - "$_css_launcher" > "$_css_launcher.tmp" \ - && mv "$_css_launcher.tmp" "$_css_launcher" - fi - chmod +x "$_css_launcher" - # studio.conf: exe path + (env-mode only) persisted env vars so fresh - # shells launch the right install without re-exporting. + # Write the exe path to a separate conf file sourced by the launcher. + # Using single-quote wrapping with the standard '\'' escape for any + # embedded apostrophes. This avoids all sed metacharacter issues. _css_quoted_exe=$(printf '%s' "$_css_exe" | sed "s/'/'\\\\''/g") - { - printf '%s\n' "UNSLOTH_EXE='$_css_quoted_exe'" - if [ "$_STUDIO_HOME_REDIRECT" = "env" ]; then - # When an override resolves to the legacy default, llama.cpp - # still lives at ~/.unsloth/llama.cpp (one shared build). - # Canonicalize the legacy side so a symlinked $HOME doesn't - # break the comparison. - _css_legacy_studio="$HOME/.unsloth/studio" - if [ -d "$_css_legacy_studio" ]; then - _css_legacy_studio=$(CDPATH= cd -P -- "$_css_legacy_studio" 2>/dev/null && pwd -P) \ - || _css_legacy_studio="$HOME/.unsloth/studio" - fi - if [ "$STUDIO_HOME" = "$_css_legacy_studio" ]; then - _css_llama_path="$HOME/.unsloth/llama.cpp" - else - _css_llama_path="$STUDIO_HOME/llama.cpp" - fi - _css_quoted_home=$(printf '%s' "$STUDIO_HOME" | sed "s/'/'\\\\''/g") - _css_quoted_llama=$(printf '%s' "$_css_llama_path" | sed "s/'/'\\\\''/g") - printf '%s\n' "export UNSLOTH_STUDIO_HOME='$_css_quoted_home'" - # UNSLOTH_LLAMA_CPP_PATH is a pre-existing user-controlled - # llama.cpp dir override; only default it if unset. - printf '%s\n' 'if [ -z "${UNSLOTH_LLAMA_CPP_PATH:-}" ]; then' - printf '%s\n' " export UNSLOTH_LLAMA_CPP_PATH='$_css_quoted_llama'" - printf '%s\n' 'fi' - fi - } > "$_css_data_dir/studio.conf" + printf '%s\n' "UNSLOTH_EXE='$_css_quoted_exe'" > "$_css_data_dir/studio.conf" # ── Icon: try bundled, then download ── # rounded-512.png used for both Linux and macOS icons @@ -952,14 +562,6 @@ LAUNCHER_EOF fi # ── Platform-specific shortcuts ── - # Env-mode installs are workspace-scoped: skip persistent desktop / - # Start-Menu / dock launchers that may point at a deleted workspace. - # Runtime launcher + studio.conf + icon are still written above. - if [ "$_STUDIO_HOME_REDIRECT" = "env" ]; then - substep "wrote launcher at $_css_launcher (persistent shortcuts skipped in env-override mode)" - return 0 - fi - _css_created=0 if [ "$_css_os" = "linux" ]; then @@ -1037,18 +639,11 @@ DESKTOP_EOF PLIST_EOF - # Executable stub: same single-quoted-heredoc + sed-substitute - # pattern as launch-studio.sh so $-vars in $_css_data_dir don't - # expand at .app launch time. - _css_sq_dir=$(printf '%s' "$_css_data_dir" | sed "s/'/'\\\\''/g") - _css_sed_dir=$(printf '%s' "$_css_sq_dir" | sed 's/[\\&|]/\\&/g') - cat > "$_css_macos_dir/launch-studio" << 'STUB_EOF' + # Executable stub + cat > "$_css_macos_dir/launch-studio" << STUB_EOF #!/bin/sh -exec '@@DATA_DIR@@/launch-studio.sh' "$@" +exec "$HOME/.local/share/unsloth/launch-studio.sh" "\$@" STUB_EOF - sed "s|@@DATA_DIR@@|$_css_sed_dir|g" "$_css_macos_dir/launch-studio" \ - > "$_css_macos_dir/launch-studio.tmp" \ - && mv "$_css_macos_dir/launch-studio.tmp" "$_css_macos_dir/launch-studio" chmod +x "$_css_macos_dir/launch-studio" # Build AppIcon.icns from unsloth-gem.png (2240x2240) @@ -1157,7 +752,6 @@ printf " ${C_DIM}%s${C_RST}\n" "$RULE" echo "" # ── Detect platform ── -tauri_log "STEP" "Detecting platform" OS="linux" if [ "$(uname)" = "Darwin" ]; then OS="macos" @@ -1207,18 +801,9 @@ if [ "$_NO_TORCH_FLAG" = true ] || [ "$MAC_INTEL" = true ]; then SKIP_TORCH=true fi -_TAURI_INITIAL_GPU_BRANCH="unknown" -if [ "$SKIP_TORCH" = true ]; then - _TAURI_INITIAL_GPU_BRANCH="no_torch" -elif [ "$OS" = "macos" ]; then - _TAURI_INITIAL_GPU_BRANCH="mac" -fi -tauri_diag_marker "$_TAURI_INITIAL_GPU_BRANCH" "none" - # ── Check system dependencies ── # cmake and git are needed by unsloth studio setup to build the GGUF inference # engine (llama.cpp). build-essential and libcurl-dev are also needed on Linux. -tauri_log "STEP" "Checking system dependencies" MISSING="" command -v cmake >/dev/null 2>&1 || MISSING="$MISSING cmake" @@ -1243,7 +828,9 @@ case "$OS" in fi command -v gcc >/dev/null 2>&1 || MISSING="$MISSING build-essential" # libcurl dev headers for llama.cpp HTTPS support - command -v curl-config >/dev/null 2>&1 || MISSING="$MISSING libcurl4-openssl-dev" + if command -v dpkg >/dev/null 2>&1; then + dpkg -s libcurl4-openssl-dev >/dev/null 2>&1 || MISSING="$MISSING libcurl4-openssl-dev" + fi ;; esac @@ -1268,15 +855,9 @@ if [ -n "$MISSING" ]; then if command -v apt-get >/dev/null 2>&1; then _smart_apt_install $MISSING else - echo " Automatic system package installation is supported on apt-based" - echo " Linux distributions (Ubuntu/Debian) only. Please install the" - echo " missing dependencies with your package manager, then re-run setup:" + echo " apt-get is not available. Please install with your package manager:" echo " $MISSING" - echo "" - echo " Examples:" - echo " Fedora/RHEL: sudo dnf install cmake git gcc gcc-c++ make libcurl-devel" - echo " Arch: sudo pacman -S --needed cmake git base-devel curl" - echo " openSUSE: sudo zypper install cmake git gcc gcc-c++ make libcurl-devel" + echo " Then re-run Unsloth Studio setup." exit 1 fi ;; @@ -1287,7 +868,6 @@ else fi # ── Install uv ── -tauri_log "STEP" "Installing uv package manager" UV_MIN_VERSION="0.7.14" version_ge() { @@ -1342,42 +922,17 @@ if ! command -v uv >/dev/null 2>&1 || ! _uv_version_ok uv; then fi # ── Create venv (migrate old layout if possible, otherwise fresh) ── -tauri_log "STEP" "Creating virtual environment" mkdir -p "$STUDIO_HOME" _MIGRATED=false if [ -x "$VENV_DIR/bin/python" ]; then - # why: matching guard to the .venv branch below -- in env-mode - # $STUDIO_HOME is a user-chosen workspace, so refuse to nuke an - # existing $STUDIO_HOME/unsloth_studio that lacks Studio sentinels. - # Accept the in-VENV ownership marker so partial-install retries are - # not blocked. Sentinels must be regular files: -f follows symlinks - # to files (the legitimate ln -s shim shape) but rejects directories - # and broken/dir-targeted symlinks. - if [ "$_STUDIO_HOME_REDIRECT" = "env" ] \ - && [ ! -f "$VENV_DIR/.unsloth-studio-owned" ] \ - && [ ! -f "$STUDIO_HOME/share/studio.conf" ] \ - && [ ! -f "$STUDIO_HOME/bin/unsloth" ]; then - echo "ERROR: $VENV_DIR already exists but does not look like an Unsloth Studio install." >&2 - echo " Move it aside or choose an empty UNSLOTH_STUDIO_HOME." >&2 - exit 1 - fi - # New layout already exists — replace only after preserving rollback copy. - substep "preserving existing environment for rollback..." - _start_studio_venv_replacement "$VENV_DIR" -elif [ "$_STUDIO_HOME_REDIRECT" != "env" ] && [ -x "$STUDIO_HOME/.venv/bin/python" ]; then - # Old layout exists — validate before migrating. - # Skip in env-mode so we don't rm -rf an unrelated .venv at the - # workspace root (e.g. user's existing project Python venv). - # In no-torch mode, a missing torch package is expected; validate Python only. + # New layout already exists — nuke for fresh install + rm -rf "$VENV_DIR" +elif [ -x "$STUDIO_HOME/.venv/bin/python" ]; then + # Old layout exists — validate before migrating substep "found legacy Studio environment, validating..." - _legacy_ok=false - if [ "$SKIP_TORCH" = true ]; then - if "$STUDIO_HOME/.venv/bin/python" -c "import sys; print(sys.executable)" >/dev/null 2>&1; then - _legacy_ok=true - fi - elif "$STUDIO_HOME/.venv/bin/python" -c " + if "$STUDIO_HOME/.venv/bin/python" -c " import torch device = 'cuda' if torch.cuda.is_available() else 'cpu' A = torch.ones((10, 10), device=device) @@ -1387,17 +942,13 @@ D = A + B E = D @ C torch.testing.assert_close(torch.unique(E), torch.tensor((20,), device=E.device, dtype=E.dtype)) " >/dev/null 2>&1; then - _legacy_ok=true - fi - if [ "$_legacy_ok" = true ]; then echo "✅ Legacy environment is healthy — migrating..." mv "$STUDIO_HOME/.venv" "$VENV_DIR" echo " Moved ~/.unsloth/studio/.venv → $VENV_DIR" _MIGRATED=true else echo "⚠️ Legacy environment failed validation — creating fresh environment" - _invalid_venv="$STUDIO_HOME/.venv.invalid.$(date +%Y%m%d%H%M%S 2>/dev/null || echo time).$$" - mv "$STUDIO_HOME/.venv" "$_invalid_venv" 2>/dev/null || true + rm -rf "$STUDIO_HOME/.venv" fi fi @@ -1418,13 +969,6 @@ if [ ! -x "$VENV_DIR/bin/python" ]; then run_install_cmd "create venv" uv venv "$VENV_DIR" --python "$PYTHON_VERSION" fi -# Mark the freshly-created venv as Studio-owned so a partial install can be -# repaired by re-running install.sh; the env-mode deletion guard above accepts -# this marker as the primary sentinel. -if [ -x "$VENV_DIR/bin/python" ]; then - : > "$VENV_DIR/.unsloth-studio-owned" 2>/dev/null || true -fi - # Guard against Python 3.13.8 torch import bug on Apple Silicon # (skip when the user explicitly chose a version via --python) if [ -z "$_USER_PYTHON" ] && [ "$OS" = "macos" ] && [ "$_ARCH" = "arm64" ]; then @@ -1436,9 +980,6 @@ if [ -z "$_USER_PYTHON" ] && [ "$OS" = "macos" ] && [ "$_ARCH" = "arm64" ]; then rm -rf "$VENV_DIR" PYTHON_VERSION="3.12" run_install_cmd "recreate venv" uv venv "$VENV_DIR" --python "$PYTHON_VERSION" - if [ -x "$VENV_DIR/bin/python" ]; then - : > "$VENV_DIR/.unsloth-studio-owned" 2>/dev/null || true - fi fi fi @@ -1737,12 +1278,6 @@ case "$TORCH_INDEX_URL" in fi ;; esac -_TAURI_TORCH_INDEX_FAMILY=$(_tauri_torch_index_family "$TORCH_INDEX_URL") -if [ "$_amd_gpu_radeon" = true ] && [ "$SKIP_TORCH" = false ]; then - _TAURI_TORCH_INDEX_FAMILY="radeon" -fi -_TAURI_GPU_BRANCH=$(_tauri_gpu_branch "$_TAURI_TORCH_INDEX_FAMILY" "$_amd_gpu_radeon") -tauri_diag_marker "$_TAURI_GPU_BRANCH" "$_TAURI_TORCH_INDEX_FAMILY" # ── Print CPU-only hint when no GPU detected ── case "$TORCH_INDEX_URL" in @@ -1769,7 +1304,6 @@ case "$TORCH_INDEX_URL" in esac # ── Install unsloth directly into the venv (no activation needed) ── -tauri_log "STEP" "Installing PyTorch" _VENV_PY="$VENV_DIR/bin/python" if [ "$_MIGRATED" = true ]; then # Migrated env: force-reinstall unsloth+unsloth-zoo to ensure clean state @@ -1782,7 +1316,7 @@ if [ "$_MIGRATED" = true ]; then # to prevent transitive torch resolution. run_install_cmd "install unsloth (migrated no-torch)" uv pip install --python "$_VENV_PY" --no-deps \ --reinstall-package unsloth --reinstall-package unsloth-zoo \ - "unsloth>=2026.5.2" unsloth-zoo + "unsloth>=2026.4.7" unsloth-zoo _NO_TORCH_RT="$(_find_no_torch_runtime)" if [ -n "$_NO_TORCH_RT" ]; then run_install_cmd "install no-torch runtime deps" uv pip install --python "$_VENV_PY" --no-deps -r "$_NO_TORCH_RT" @@ -1790,15 +1324,11 @@ if [ "$_MIGRATED" = true ]; then else run_install_cmd "install unsloth (migrated)" uv pip install --python "$_VENV_PY" \ --reinstall-package unsloth --reinstall-package unsloth-zoo \ - "unsloth>=2026.5.2" unsloth-zoo + "unsloth>=2026.4.7" unsloth-zoo fi if [ "$STUDIO_LOCAL_INSTALL" = true ]; then substep "overlaying local repo (editable)..." run_install_cmd "overlay local repo" uv pip install --python "$_VENV_PY" -e "$_REPO_ROOT" --no-deps - substep "overlaying unsloth-zoo from git main..." - run_install_cmd "overlay unsloth-zoo (git main)" uv pip install --python "$_VENV_PY" \ - --no-deps --reinstall-package unsloth-zoo \ - "unsloth-zoo @ git+https://github.com/unslothai/unsloth-zoo" fi # AMD ROCm: install bitsandbytes even in migrated environments so # existing ROCm installs gain the AMD bitsandbytes build without a @@ -1951,14 +1481,13 @@ elif [ -n "$TORCH_INDEX_URL" ]; then esac fi # Fresh: Step 2 - install unsloth, preserving pre-installed torch - tauri_log "STEP" "Installing Unsloth" substep "installing unsloth (this may take a few minutes)..." if [ "$SKIP_TORCH" = true ]; then # No-torch: install unsloth + unsloth-zoo with --no-deps, then # runtime deps (typer, safetensors, transformers, etc.) with --no-deps. run_install_cmd "install unsloth (no-torch)" uv pip install --python "$_VENV_PY" --no-deps \ --upgrade-package unsloth --upgrade-package unsloth-zoo \ - "unsloth>=2026.5.2" unsloth-zoo + "unsloth>=2026.4.7" unsloth-zoo _NO_TORCH_RT="$(_find_no_torch_runtime)" if [ -n "$_NO_TORCH_RT" ]; then run_install_cmd "install no-torch runtime deps" uv pip install --python "$_VENV_PY" --no-deps -r "$_NO_TORCH_RT" @@ -1966,23 +1495,15 @@ elif [ -n "$TORCH_INDEX_URL" ]; then if [ "$STUDIO_LOCAL_INSTALL" = true ]; then substep "overlaying local repo (editable)..." run_install_cmd "overlay local repo" uv pip install --python "$_VENV_PY" -e "$_REPO_ROOT" --no-deps - substep "overlaying unsloth-zoo from git main..." - run_install_cmd "overlay unsloth-zoo (git main)" uv pip install --python "$_VENV_PY" \ - --no-deps --reinstall-package unsloth-zoo \ - "unsloth-zoo @ git+https://github.com/unslothai/unsloth-zoo" fi elif [ "$STUDIO_LOCAL_INSTALL" = true ]; then run_install_cmd "install unsloth (local)" uv pip install --python "$_VENV_PY" \ - --upgrade-package unsloth "unsloth>=2026.5.2" unsloth-zoo + --upgrade-package unsloth "unsloth>=2026.4.7" unsloth-zoo substep "overlaying local repo (editable)..." run_install_cmd "overlay local repo" uv pip install --python "$_VENV_PY" -e "$_REPO_ROOT" --no-deps - substep "overlaying unsloth-zoo from git main..." - run_install_cmd "overlay unsloth-zoo (git main)" uv pip install --python "$_VENV_PY" \ - --no-deps --reinstall-package unsloth-zoo \ - "unsloth-zoo @ git+https://github.com/unslothai/unsloth-zoo" else run_install_cmd "install unsloth" uv pip install --python "$_VENV_PY" \ - --upgrade-package unsloth -- "$PACKAGE_NAME" + --upgrade-package unsloth "$PACKAGE_NAME" fi # AMD ROCm: repair torch if the unsloth/unsloth-zoo install pulled in # CUDA torch from PyPI, overwriting the ROCm wheels installed in Step 1. @@ -2002,29 +1523,17 @@ elif [ -n "$TORCH_INDEX_URL" ]; then fi else # Fallback: GPU detection failed to produce a URL -- let uv resolve torch - tauri_log "STEP" "Installing Unsloth" substep "installing unsloth (this may take a few minutes)..." if [ "$STUDIO_LOCAL_INSTALL" = true ]; then - run_install_cmd "install unsloth (auto torch backend)" uv pip install --python "$_VENV_PY" unsloth-zoo "unsloth>=2026.5.2" --torch-backend=auto + run_install_cmd "install unsloth (auto torch backend)" uv pip install --python "$_VENV_PY" unsloth-zoo "unsloth>=2026.4.7" --torch-backend=auto substep "overlaying local repo (editable)..." run_install_cmd "overlay local repo" uv pip install --python "$_VENV_PY" -e "$_REPO_ROOT" --no-deps - substep "overlaying unsloth-zoo from git main..." - run_install_cmd "overlay unsloth-zoo (git main)" uv pip install --python "$_VENV_PY" \ - --no-deps --reinstall-package unsloth-zoo \ - "unsloth-zoo @ git+https://github.com/unslothai/unsloth-zoo" else - run_install_cmd "install unsloth (auto torch backend)" uv pip install --python "$_VENV_PY" --torch-backend=auto -- "$PACKAGE_NAME" + run_install_cmd "install unsloth (auto torch backend)" uv pip install --python "$_VENV_PY" "$PACKAGE_NAME" --torch-backend=auto fi fi -# ── Install mlx-vlm on Apple Silicon (optional, for VLM training) ── -if [ "$OS" = "macos" ] && [ "$_ARCH" = "arm64" ]; then - substep "installing mlx-vlm (VLM training support)..." - run_install_cmd "install mlx-vlm" uv pip install --python "$_VENV_PY" mlx-vlm -fi - # ── Run studio setup ── -tauri_log "STEP" "Running Studio setup" # When --local, use the repo's own setup.sh directly. # Otherwise, find it inside the installed package. SETUP_SH="" @@ -2045,7 +1554,6 @@ if [ -z "$SETUP_SH" ] || [ ! -f "$SETUP_SH" ]; then fi if [ -z "$SETUP_SH" ] || [ ! -f "$SETUP_SH" ]; then - tauri_log "ERROR" "Could not find studio/setup.sh in the installed package" echo "❌ ERROR: Could not find studio/setup.sh in the installed package." exit 1 fi @@ -2063,171 +1571,101 @@ if ! command -v bash >/dev/null 2>&1; then fi step "setup" "running unsloth studio update..." +# install.sh already installs base packages (unsloth + unsloth-zoo) and +# no-torch-runtime.txt above, so tell install_python_stack.py to skip +# the base step to avoid redundant reinstallation. _SKIP_BASE=1 +# Run setup.sh outside set -e so that a llama.cpp build failure (exit 1) +# does not skip PATH setup, shortcuts, and launch below. We capture the +# exit code and propagate it after post-install steps finish. _SETUP_EXIT=0 -# Tauri desktop app bundles its own frontend — skip Node/npm/frontend build -_SKIP_FRONTEND=0 -if [ "$TAURI_MODE" = true ]; then - _SKIP_FRONTEND=1 -fi -# Prepend UNSLOTH_STUDIO_HOME=$STUDIO_HOME to "$@" for env-override installs -# without word-splitting on whitespace paths. -_run_setup_with_studio_home() { - if [ "$_STUDIO_HOME_REDIRECT" = "env" ]; then - UNSLOTH_STUDIO_HOME="$STUDIO_HOME" "$@" - else - "$@" - fi -} if [ "$STUDIO_LOCAL_INSTALL" = true ]; then - _run_setup_with_studio_home env \ SKIP_STUDIO_BASE="$_SKIP_BASE" \ - SKIP_STUDIO_FRONTEND="$_SKIP_FRONTEND" \ STUDIO_PACKAGE_NAME="$PACKAGE_NAME" \ STUDIO_LOCAL_INSTALL=1 \ STUDIO_LOCAL_REPO="$_REPO_ROOT" \ UNSLOTH_NO_TORCH="$SKIP_TORCH" \ bash "$SETUP_SH" &2 - echo " Move or remove it manually, then re-run the installer." >&2 - exit 1 -fi -# why: -sfn is atomic and -n prevents descent into a symlink-to-directory at -# the shim path (the directory guard above already rejects a real directory). -ln -sfn "$VENV_DIR/bin/unsloth" "$_shim_path" +# ── Make 'unsloth' available globally via ~/.local/bin ── +mkdir -p "$HOME/.local/bin" +ln -sf "$VENV_DIR/bin/unsloth" "$HOME/.local/bin/unsloth" +_LOCAL_BIN="$HOME/.local/bin" case ":$PATH:" in *":$_LOCAL_BIN:"*) ;; # already on PATH *) - if [ "$_STUDIO_HOME_REDIRECT" = "env" ]; then - export PATH="$_LOCAL_BIN:$PATH" - step "path" "exported $_LOCAL_BIN for this session (no rc-file append in env-override mode)" - else - _SHELL_PROFILE="" - if [ -n "${ZSH_VERSION:-}" ] || [ "$(basename "${SHELL:-}")" = "zsh" ]; then - _SHELL_PROFILE="$HOME/.zshrc" - elif [ -f "$HOME/.bashrc" ]; then - _SHELL_PROFILE="$HOME/.bashrc" - elif [ -f "$HOME/.profile" ]; then - _SHELL_PROFILE="$HOME/.profile" - fi - if [ -n "$_SHELL_PROFILE" ]; then - if ! grep -q '\.local/bin' "$_SHELL_PROFILE" 2>/dev/null; then - echo '' >> "$_SHELL_PROFILE" - echo '# Added by Unsloth installer' >> "$_SHELL_PROFILE" - echo 'export PATH="$HOME/.local/bin:$PATH"' >> "$_SHELL_PROFILE" - step "path" "added ~/.local/bin to PATH in $_SHELL_PROFILE" - fi + _SHELL_PROFILE="" + if [ -n "${ZSH_VERSION:-}" ] || [ "$(basename "${SHELL:-}")" = "zsh" ]; then + _SHELL_PROFILE="$HOME/.zshrc" + elif [ -f "$HOME/.bashrc" ]; then + _SHELL_PROFILE="$HOME/.bashrc" + elif [ -f "$HOME/.profile" ]; then + _SHELL_PROFILE="$HOME/.profile" + fi + + if [ -n "$_SHELL_PROFILE" ]; then + if ! grep -q '\.local/bin' "$_SHELL_PROFILE" 2>/dev/null; then + echo '' >> "$_SHELL_PROFILE" + echo '# Added by Unsloth installer' >> "$_SHELL_PROFILE" + echo 'export PATH="$HOME/.local/bin:$PATH"' >> "$_SHELL_PROFILE" + step "path" "added ~/.local/bin to PATH in $_SHELL_PROFILE" fi - export PATH="$_LOCAL_BIN:$PATH" fi + export PATH="$_LOCAL_BIN:$PATH" ;; esac -# Non-Tauri installs keep shortcuts even if setup reports failure. -# create_studio_shortcuts gates persistent menu shortcuts on env-mode; -# launcher + studio.conf + icon are always written. -if [ "$TAURI_MODE" != true ]; then - create_studio_shortcuts "$VENV_ABS_BIN/unsloth" "$OS" -fi +create_studio_shortcuts "$VENV_ABS_BIN/unsloth" "$OS" # If setup.sh failed, report and exit now. # PATH and shortcuts are already set up so the user can fix and retry. if [ "$_SETUP_EXIT" -ne 0 ]; then echo "" step "error" "studio setup failed (exit code $_SETUP_EXIT)" "$C_ERR" + substep "Check the output above for details, then re-run:" + if [ "$STUDIO_LOCAL_INSTALL" = true ]; then + substep " unsloth studio update --local" + else + substep " unsloth studio update" + fi echo "" exit "$_SETUP_EXIT" fi -_commit_studio_venv_replacement - -# ── Tauri mode: done, skip shortcuts and auto-launch ── -if [ "$TAURI_MODE" = true ]; then - tauri_log "DONE" "" - exit 0 -fi - echo "" printf " ${C_TITLE}%s${C_RST}\n" "Unsloth Studio installed!" printf " ${C_DIM}%s${C_RST}\n" "$RULE" echo "" -# In interactive terminals, ask the user before starting Studio. -# In non-interactive environments (Docker, CI, cloud-init) just print instructions. +# Launch studio automatically in interactive terminals; +# in non-interactive environments (Docker, CI, cloud-init) just print instructions. if [ -t 1 ]; then - echo "" - printf " Start Unsloth Studio now? [Y/n] " - if [ -r /dev/tty ]; then - read -r _reply Optional[str]: def create_access_token( subject: str, expires_delta: Optional[timedelta] = None, - *, - desktop: bool = False, ) -> str: """ Create a signed JWT for the given subject (e.g. username). @@ -61,8 +59,6 @@ def create_access_token( Tokens are valid across restarts because the signing secret is stored in SQLite. """ to_encode = {"sub": subject} - if desktop: - to_encode["desktop"] = True expire = datetime.now(timezone.utc) + ( expires_delta or timedelta(minutes = ACCESS_TOKEN_EXPIRE_MINUTES) ) @@ -74,29 +70,7 @@ def create_access_token( ) -def is_desktop_access_token(token: str) -> bool: - """Return true only for a valid desktop-issued JWT access token.""" - if token.startswith(API_KEY_PREFIX): - return False - - subject = _decode_subject_without_verification(token) - if subject is None: - return False - - record = get_user_and_secret(subject) - if record is None: - return False - - _salt, _pwd_hash, jwt_secret, _must_change_password = record - try: - payload = jwt.decode(token, jwt_secret, algorithms = [ALGORITHM]) - except jwt.InvalidTokenError: - return False - - return payload.get("sub") == subject and payload.get("desktop") is True - - -def create_refresh_token(subject: str, *, desktop: bool = False) -> str: +def create_refresh_token(subject: str) -> str: """ Create a random refresh token, store its hash in SQLite, and return it. @@ -104,28 +78,21 @@ def create_refresh_token(subject: str, *, desktop: bool = False) -> str: """ token = secrets.token_urlsafe(48) expires_at = datetime.now(timezone.utc) + timedelta(days = REFRESH_TOKEN_EXPIRE_DAYS) - save_refresh_token(token, subject, expires_at.isoformat(), is_desktop = desktop) + save_refresh_token(token, subject, expires_at.isoformat()) return token -def refresh_access_token( - refresh_token: str, -) -> Tuple[Optional[str], Optional[str], bool]: +def refresh_access_token(refresh_token: str) -> Tuple[Optional[str], Optional[str]]: """ Validate a refresh token and issue a new access token. The refresh token itself is NOT consumed — it stays valid until expiry. Returns a new access_token or None if the refresh token is invalid/expired. """ - verified = verify_refresh_token(refresh_token) - if verified is None: - return None, None, False - username, is_desktop = verified - return ( - create_access_token(subject = username, desktop = is_desktop), - username, - is_desktop, - ) + username = verify_refresh_token(refresh_token) + if username is None: + return None, None + return create_access_token(subject = username), username def reload_secret() -> None: @@ -206,8 +173,7 @@ async def secure_endpoint(current_subject: str = Depends(get_current_subject)): status_code = status.HTTP_401_UNAUTHORIZED, detail = "Invalid token payload", ) - is_desktop = payload.get("desktop") is True - if must_change_password and not allow_password_change and not is_desktop: + if must_change_password and not allow_password_change: raise HTTPException( status_code = status.HTTP_403_FORBIDDEN, detail = "Password change required", diff --git a/studio/backend/auth/storage.py b/studio/backend/auth/storage.py index 9a03f5f542..7d55a2dc59 100644 --- a/studio/backend/auth/storage.py +++ b/studio/backend/auth/storage.py @@ -6,7 +6,6 @@ """ import hashlib -import os import secrets import sqlite3 from datetime import datetime, timezone @@ -55,10 +54,6 @@ def generate_bootstrap_password() -> str: # before the user changes the password. ensure_dir(_BOOTSTRAP_PW_PATH.parent) _BOOTSTRAP_PW_PATH.write_text(_bootstrap_password) - try: - os.chmod(_BOOTSTRAP_PW_PATH, 0o600) - except OSError: - pass return _bootstrap_password @@ -68,17 +63,6 @@ def get_bootstrap_password() -> Optional[str]: return _bootstrap_password -def _load_bootstrap_password() -> Optional[str]: - """Load an existing bootstrap password without creating one.""" - global _bootstrap_password - _bootstrap_password = None - if _BOOTSTRAP_PW_PATH.is_file(): - bootstrap_password = _BOOTSTRAP_PW_PATH.read_text().strip() - if bootstrap_password: - _bootstrap_password = bootstrap_password - return _bootstrap_password - - def clear_bootstrap_password() -> None: """Delete the persisted bootstrap password file (called after password change).""" global _bootstrap_password @@ -130,8 +114,7 @@ def get_connection() -> sqlite3.Connection: id INTEGER PRIMARY KEY, token_hash TEXT NOT NULL, username TEXT NOT NULL, - expires_at TEXT NOT NULL, - is_desktop INTEGER NOT NULL DEFAULT 0 + expires_at TEXT NOT NULL ); """ ) @@ -146,18 +129,10 @@ def get_connection() -> sqlite3.Connection: created_at TEXT NOT NULL, last_used_at TEXT, expires_at TEXT, - is_active INTEGER NOT NULL DEFAULT 1, - is_internal INTEGER NOT NULL DEFAULT 0 + is_active INTEGER NOT NULL DEFAULT 1 ); """ ) - api_key_columns = { - row["name"] for row in conn.execute("PRAGMA table_info(api_keys)") - } - if "is_internal" not in api_key_columns: - conn.execute( - "ALTER TABLE api_keys ADD COLUMN is_internal INTEGER NOT NULL DEFAULT 0" - ) conn.execute( """ CREATE TABLE IF NOT EXISTS app_secrets ( @@ -171,13 +146,6 @@ def get_connection() -> sqlite3.Connection: conn.execute( "ALTER TABLE auth_user ADD COLUMN must_change_password INTEGER NOT NULL DEFAULT 0" ) - refresh_columns = { - row["name"] for row in conn.execute("PRAGMA table_info(refresh_tokens)") - } - if "is_desktop" not in refresh_columns: - conn.execute( - "ALTER TABLE refresh_tokens ADD COLUMN is_desktop INTEGER NOT NULL DEFAULT 0" - ) conn.commit() return conn @@ -233,9 +201,6 @@ def _get_or_create_api_key_pbkdf2_salt() -> bytes: _API_KEY_PBKDF2_ITERATIONS = 100_000 -DESKTOP_SECRET_PREFIX = "desktop-" -_DESKTOP_SECRET_HASH_KEY = "desktop_secret_hash" -_DESKTOP_SECRET_CREATED_AT_KEY = "desktop_secret_created_at" def _pbkdf2_api_key(raw_key: str) -> str: @@ -268,10 +233,6 @@ def _pbkdf2_api_key(raw_key: str) -> str: return dk.hex() -def _pbkdf2_desktop_secret(raw_secret: str) -> str: - return _pbkdf2_api_key(raw_secret) - - def is_initialized() -> bool: """Check if auth is ready for login (at least one user exists in DB).""" conn = get_connection() @@ -413,10 +374,6 @@ def ensure_default_admin() -> bool: Uses a randomly generated diceware passphrase as the bootstrap password. Returns True when the default admin was created in this call. """ - if get_user_and_secret(DEFAULT_ADMIN_USERNAME) is not None: - _load_bootstrap_password() - return False - bootstrap_pw = generate_bootstrap_password() try: create_initial_user( @@ -449,19 +406,12 @@ def update_password(username: str, new_password: str) -> bool: conn.commit() if cursor.rowcount > 0: clear_bootstrap_password() - clear_desktop_secret() return cursor.rowcount > 0 finally: conn.close() -def save_refresh_token( - token: str, - username: str, - expires_at: str, - *, - is_desktop: bool = False, -) -> None: +def save_refresh_token(token: str, username: str, expires_at: str) -> None: """ Store a hashed refresh token with its associated username and expiry. """ @@ -470,21 +420,21 @@ def save_refresh_token( try: conn.execute( """ - INSERT INTO refresh_tokens (token_hash, username, expires_at, is_desktop) - VALUES (?, ?, ?, ?) + INSERT INTO refresh_tokens (token_hash, username, expires_at) + VALUES (?, ?, ?) """, - (token_hash, username, expires_at, int(is_desktop)), + (token_hash, username, expires_at), ) conn.commit() finally: conn.close() -def verify_refresh_token(token: str) -> Optional[Tuple[str, bool]]: +def verify_refresh_token(token: str) -> Optional[str]: """ - Verify a refresh token and return the username plus desktop marker. + Verify a refresh token and return the username. - Returns the username and desktop marker if valid and not expired, None otherwise. + Returns the username if valid and not expired, None otherwise. The token is NOT consumed — it stays valid until it expires. """ token_hash = _hash_token(token) @@ -499,7 +449,7 @@ def verify_refresh_token(token: str) -> Optional[Tuple[str, bool]]: cur = conn.execute( """ - SELECT id, username, expires_at, is_desktop FROM refresh_tokens + SELECT id, username, expires_at FROM refresh_tokens WHERE token_hash = ? """, (token_hash,), @@ -515,7 +465,7 @@ def verify_refresh_token(token: str) -> Optional[Tuple[str, bool]]: conn.commit() return None - return row["username"], bool(row["is_desktop"]) + return row["username"] finally: conn.close() @@ -530,65 +480,6 @@ def revoke_user_refresh_tokens(username: str) -> None: conn.close() -def create_desktop_secret() -> str: - """Create/rotate the local desktop credential and return it once.""" - ensure_default_admin() - raw_secret = DESKTOP_SECRET_PREFIX + secrets.token_urlsafe(48) - secret_hash = _pbkdf2_desktop_secret(raw_secret) - now = datetime.now(timezone.utc).isoformat() - conn = get_connection() - try: - conn.execute( - "INSERT OR REPLACE INTO app_secrets (key, value) VALUES (?, ?)", - (_DESKTOP_SECRET_HASH_KEY, secret_hash), - ) - conn.execute( - "INSERT OR REPLACE INTO app_secrets (key, value) VALUES (?, ?)", - (_DESKTOP_SECRET_CREATED_AT_KEY, now), - ) - conn.commit() - return raw_secret - finally: - conn.close() - - -def validate_desktop_secret(raw_secret: str) -> Optional[str]: - """Return the real admin username when the desktop secret matches.""" - if not raw_secret.startswith(DESKTOP_SECRET_PREFIX): - return None - if get_user_and_secret(DEFAULT_ADMIN_USERNAME) is None: - return None - - secret_hash = _pbkdf2_desktop_secret(raw_secret) - conn = get_connection() - try: - cur = conn.execute( - "SELECT value FROM app_secrets WHERE key = ?", - (_DESKTOP_SECRET_HASH_KEY,), - ) - row = cur.fetchone() - if row is None: - return None - if not secrets.compare_digest(row["value"], secret_hash): - return None - return DEFAULT_ADMIN_USERNAME - finally: - conn.close() - - -def clear_desktop_secret() -> None: - """Remove backend-side desktop auth state.""" - conn = get_connection() - try: - conn.execute( - "DELETE FROM app_secrets WHERE key IN (?, ?)", - (_DESKTOP_SECRET_HASH_KEY, _DESKTOP_SECRET_CREATED_AT_KEY), - ) - conn.commit() - finally: - conn.close() - - # --------------------------------------------------------------------------- # API key management # --------------------------------------------------------------------------- @@ -600,15 +491,11 @@ def create_api_key( username: str, name: str, expires_at: Optional[str] = None, - internal: bool = False, ) -> Tuple[str, dict]: """Create a new API key for *username*. Returns ``(raw_key, row_dict)`` where *raw_key* is shown to the user - exactly once. The database only stores the PBKDF2 hash. - - Pass ``internal=True`` for keys minted by workflows (e.g. data-recipe - runs) that should not appear in user-facing key listings. + exactly once. The database only stores the SHA-256 hash. """ raw_key = API_KEY_PREFIX + secrets.token_hex(16) key_hash = _pbkdf2_api_key(raw_key) @@ -619,18 +506,10 @@ def create_api_key( try: conn.execute( """ - INSERT INTO api_keys (username, key_prefix, key_hash, name, created_at, expires_at, is_internal) - VALUES (?, ?, ?, ?, ?, ?, ?) + INSERT INTO api_keys (username, key_prefix, key_hash, name, created_at, expires_at) + VALUES (?, ?, ?, ?, ?, ?) """, - ( - username, - key_prefix, - key_hash, - name, - now, - expires_at, - 1 if internal else 0, - ), + (username, key_prefix, key_hash, name, now, expires_at), ) conn.commit() cur = conn.execute("SELECT * FROM api_keys WHERE key_hash = ?", (key_hash,)) @@ -640,33 +519,19 @@ def create_api_key( conn.close() -def list_api_keys(username: str, include_internal: bool = False) -> list: - """Return API keys for *username*. Internal workflow keys are hidden - by default so they do not clutter user-facing UIs.""" +def list_api_keys(username: str) -> list: + """Return all API keys for *username* (never exposes ``key_hash``).""" conn = get_connection() try: - if include_internal: - cur = conn.execute( - """ - SELECT id, username, key_prefix, name, created_at, last_used_at, - expires_at, is_active, is_internal - FROM api_keys - WHERE username = ? - ORDER BY created_at DESC - """, - (username,), - ) - else: - cur = conn.execute( - """ - SELECT id, username, key_prefix, name, created_at, last_used_at, - expires_at, is_active, is_internal - FROM api_keys - WHERE username = ? AND is_internal = 0 - ORDER BY created_at DESC - """, - (username,), - ) + cur = conn.execute( + """ + SELECT id, username, key_prefix, name, created_at, last_used_at, expires_at, is_active + FROM api_keys + WHERE username = ? + ORDER BY created_at DESC + """, + (username,), + ) return [dict(row) for row in cur.fetchall()] finally: conn.close() @@ -686,24 +551,6 @@ def revoke_api_key(username: str, key_id: int) -> bool: conn.close() -def revoke_internal_api_key(key_id: int) -> bool: - """Revoke an internal workflow-minted key without requiring a username. - - Used by the recipe runner to retire its sk-unsloth-* key once the job - terminates, shrinking the window a leaked key could be abused. - """ - conn = get_connection() - try: - cursor = conn.execute( - "UPDATE api_keys SET is_active = 0 WHERE id = ? AND is_internal = 1", - (key_id,), - ) - conn.commit() - return cursor.rowcount > 0 - finally: - conn.close() - - def validate_api_key(raw_key: str) -> Optional[str]: """Validate *raw_key* and return the owning username, or ``None``. diff --git a/studio/backend/core/data_recipe/jobs/constants.py b/studio/backend/core/data_recipe/jobs/constants.py index 0045276e20..08237326f8 100644 --- a/studio/backend/core/data_recipe/jobs/constants.py +++ b/studio/backend/core/data_recipe/jobs/constants.py @@ -9,7 +9,6 @@ STAGE_DAG = "dag" STAGE_HEALTHCHECK = "healthcheck" STAGE_SAMPLING = "sampling" -STAGE_SOURCE = "source" STAGE_COLUMN_CONFIG = "column_config" STAGE_GENERATING = "generating" STAGE_BATCH = "batch" diff --git a/studio/backend/core/data_recipe/jobs/manager.py b/studio/backend/core/data_recipe/jobs/manager.py index cdc28d9560..3d7cf2dbe6 100644 --- a/studio/backend/core/data_recipe/jobs/manager.py +++ b/studio/backend/core/data_recipe/jobs/manager.py @@ -33,60 +33,6 @@ _CTX = mp.get_context("spawn") -def _github_source_estimated_total(recipe: dict) -> int | None: - seed_config = recipe.get("seed_config") - if not isinstance(seed_config, dict): - return None - source = seed_config.get("source") - if not isinstance(source, dict) or source.get("seed_type") != "github_repo": - return None - - repos_raw = source.get("repos") - repos = ( - [repo for repo in repos_raw if isinstance(repo, str) and repo.strip()] - if isinstance(repos_raw, list) - else [] - ) - item_types_raw = source.get("item_types") - item_types = ( - [ - item - for item in item_types_raw - if isinstance(item, str) and item in {"issues", "pulls", "commits"} - ] - if isinstance(item_types_raw, list) - else [] - ) - try: - limit = int(source.get("limit") or 0) - except (TypeError, ValueError): - return None - if not repos or not item_types or limit <= 0: - return None - return len(repos) * len(item_types) * limit - - -def _source_progress_status(job: Job) -> dict[str, Any] | None: - progress = job.source_progress - if progress is None: - return None - return { - "source": progress.source, - "status": progress.status, - "repo": progress.repo, - "resource": progress.resource, - "page": progress.page, - "page_items": progress.page_items, - "fetched_items": progress.fetched_items, - "estimated_total": progress.estimated_total, - "percent": progress.percent, - "rate_remaining": progress.rate_remaining, - "retry_after_sec": progress.retry_after_sec, - "message": progress.message, - "updated_at": progress.updated_at, - } - - @dataclass class Subscription: replay: list[dict] @@ -125,20 +71,8 @@ def __init__(self) -> None: self._pump_thread: threading.Thread | None = None self._seq: int = 0 - def start( - self, - *, - recipe: dict, - run: dict, - internal_api_key_id: int | None = None, - ) -> str: - """Spawn the job subprocess (one at a time, no cap). - - ``internal_api_key_id`` is the row id of a workflow-scoped - sk-unsloth-* key minted by the route layer for local providers. - JobManager revokes it when the job reaches a terminal state so the - key's live window is no longer than the run. - """ + 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): @@ -158,29 +92,18 @@ def start( 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._job.source_progress_estimated_total = _github_source_estimated_total( - recipe - ) - self._job.internal_api_key_id = internal_api_key_id self._events.clear() self._seq = 0 run_payload = dict(run) run_payload["_job_id"] = job_id - from utils.native_path_leases import ( - native_path_secret_removed_for_child_start, - run_without_native_path_secret, + mp_q = _CTX.Queue() + proc = _CTX.Process( + target = run_job_process, + kwargs = {"event_queue": mp_q, "recipe": recipe, "run": run_payload}, + daemon = True, ) - - with native_path_secret_removed_for_child_start(): - mp_q = _CTX.Queue() - proc = _CTX.Process( - target = run_without_native_path_secret, - args = (run_job_process,), - kwargs = {"event_queue": mp_q, "recipe": recipe, "run": run_payload}, - daemon = True, - ) - proc.start() + proc.start() self._mp_q = mp_q self._proc = proc @@ -240,7 +163,6 @@ def get_status(self, job_id: str) -> dict | None: "ok": job.column_progress.ok, "failed": job.column_progress.failed, }, - "source_progress": _source_progress_status(job), "model_usage": { name: { "model": usage.model, @@ -483,7 +405,6 @@ def _pump_loop(self) -> None: for e in self._drain_queue(mp_q): self._handle_event(job, e) - retired_job: Job | None = None with self._lock: if self._job and self._job.status in { "pending", @@ -508,9 +429,6 @@ def _pump_loop(self) -> None: "job_id": self._job.job_id, } ) - retired_job = self._job - if retired_job is not None: - self._retire_workflow_key(retired_job) return def _handle_event(self, job: Job, event: dict) -> None: @@ -518,7 +436,6 @@ def _handle_event(self, job: Job, event: dict) -> None: et = event.get("type") msg = event.get("message") if et == "log" else None - terminal = False with self._lock: if self._job is None or self._job.job_id != job.job_id: return @@ -535,43 +452,18 @@ def _handle_event(self, job: Job, event: dict) -> None: 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 - terminal = True if et == EVENT_JOB_ERROR: self._job.status = "error" self._job.finished_at = time.time() self._job.error = event.get("error") or "error" - terminal = True - if et == EVENT_JOB_CANCELLED: - terminal = True if msg: upd = parse_log_message(msg) if upd: apply_update(self._job, upd) - if terminal: - self._retire_workflow_key(job) - self._emit(event) - def _retire_workflow_key(self, job: Job) -> None: - """Revoke the workflow-scoped sk-unsloth-* key, if one was minted. - - Best-effort: revocation failures are swallowed. The key would - expire on its own after 24h, so a missed revoke is a latency - concern, not a correctness one. - """ - key_id = getattr(job, "internal_api_key_id", None) - if not key_id: - return - try: - from auth import storage # deferred: avoids circular import - - storage.revoke_internal_api_key(int(key_id)) - except Exception: - pass - job.internal_api_key_id = None - _JOB_MANAGER: JobManager | None = None diff --git a/studio/backend/core/data_recipe/jobs/parse.py b/studio/backend/core/data_recipe/jobs/parse.py index cea6d8ea64..324b62a92e 100644 --- a/studio/backend/core/data_recipe/jobs/parse.py +++ b/studio/backend/core/data_recipe/jobs/parse.py @@ -4,7 +4,6 @@ from __future__ import annotations import re -import time from dataclasses import dataclass from typing import Any @@ -18,10 +17,9 @@ STAGE_PREVIEW, STAGE_PROFILING, STAGE_SAMPLING, - STAGE_SOURCE, USAGE_RESET_STAGES, ) -from .types import Job, ModelUsage, Progress, SourceProgress +from .types import Job, ModelUsage, Progress @dataclass(frozen = True) @@ -43,7 +41,6 @@ class ParsedUpdate: usage_requests_total: int | None = None usage_rpm: float | None = None usage_section_start: bool | None = None - source_progress: SourceProgress | None = None # kinda of a bummber but currently only option, Best effort parser from data-designer logs -> structured status for UI. @@ -64,165 +61,9 @@ class ParsedUpdate: _RE_USAGE_REQUESTS = re.compile( r"requests:\s*success=(?P\d+),\s*failed=(?P\d+),\s*total=(?P\d+),\s*rpm=(?P[0-9.]+)" ) -_RE_GITHUB_PAGE = re.compile( - r"^\[(?P[^\]\s]+/[^\]\s]+)\]\s+" - r"(?Pissues|PRs|commits)\s+page\s+(?P\d+)\s+" - r"\(\+(?P\d+)\).*?\bremaining=(?P\d+)", - re.IGNORECASE, -) -_RE_GITHUB_RATE_LIMIT = re.compile( - r"Rate limit hit\. Sleeping (?P\d+)s until reset\.", - re.IGNORECASE, -) -_RE_GITHUB_SECONDARY_RATE_LIMIT = re.compile( - r"Secondary rate limit(?: on REST)?\. Sleep (?P\d+)s\.", - re.IGNORECASE, -) -_RE_GITHUB_REST_RATE_LIMIT = re.compile( - r"REST 403/429, sleep (?P\d+)", - re.IGNORECASE, -) -_RE_GITHUB_TRANSIENT = re.compile( - r"^(?PGraphQL|REST) (?P\d{3}) transient, retrying", - re.IGNORECASE, -) -_RE_GITHUB_NETWORK_RETRY = re.compile( - r"^(?PGraphQL|REST) network error: .* Retry\.", - re.IGNORECASE, -) -_RE_GITHUB_TRIAL_LIMIT = re.compile( - r"Trial limit reached for (?Pissues|PRs|commits) \((?P\d+)\)", - re.IGNORECASE, -) -_RE_GITHUB_COMPLETE = re.compile( - r"Scraper complete\. GraphQL calls=\d+ REST calls=\d+", - re.IGNORECASE, -) def parse_log_message(msg: str) -> ParsedUpdate | None: - m = _RE_GITHUB_PAGE.search(msg) - if m: - resource_raw = m.group("resource") - resource = "pulls" if resource_raw.lower() == "prs" else resource_raw.lower() - repo = m.group("repo") - page = int(m.group("page")) - page_items = int(m.group("items")) - return ParsedUpdate( - stage = STAGE_SOURCE, - source_progress = SourceProgress( - source = "github", - status = "fetching", - repo = repo, - resource = resource, - page = page, - page_items = page_items, - rate_remaining = int(m.group("remaining")), - message = ( - f"Scraping GitHub source: {repo} " - f"{resource} page {page} (+{page_items})" - ), - ), - ) - - m = _RE_GITHUB_RATE_LIMIT.search(msg) - if m: - seconds = int(m.group("seconds")) - return ParsedUpdate( - stage = STAGE_SOURCE, - source_progress = SourceProgress( - source = "github", - status = "rate_limited", - retry_after_sec = seconds, - message = ( - "Waiting for GitHub rate limit. " - "Studio will resume automatically." - ), - ), - ) - - m = _RE_GITHUB_SECONDARY_RATE_LIMIT.search(msg) - if m: - seconds = int(m.group("seconds")) - return ParsedUpdate( - stage = STAGE_SOURCE, - source_progress = SourceProgress( - source = "github", - status = "rate_limited", - retry_after_sec = seconds, - message = ( - "Waiting for GitHub secondary rate limit. " - "Studio will resume automatically." - ), - ), - ) - - m = _RE_GITHUB_REST_RATE_LIMIT.search(msg) - if m: - seconds = int(m.group("seconds")) - return ParsedUpdate( - stage = STAGE_SOURCE, - source_progress = SourceProgress( - source = "github", - status = "rate_limited", - retry_after_sec = seconds, - message = ( - "Waiting for GitHub rate limit. " - "Studio will resume automatically." - ), - ), - ) - - m = _RE_GITHUB_TRIAL_LIMIT.search(msg) - if m: - resource_raw = m.group("resource") - resource = "pulls" if resource_raw.lower() == "prs" else resource_raw.lower() - items = int(m.group("items")) - return ParsedUpdate( - stage = STAGE_SOURCE, - source_progress = SourceProgress( - source = "github", - status = "fetching", - resource = resource, - message = f"GitHub {resource} trial limit reached ({items}).", - ), - ) - - m = _RE_GITHUB_TRANSIENT.search(msg) - if m: - api = m.group("api") - code = m.group("code") - return ParsedUpdate( - stage = STAGE_SOURCE, - source_progress = SourceProgress( - source = "github", - status = "retrying", - message = f"GitHub {api} returned {code}; retrying automatically.", - ), - ) - - m = _RE_GITHUB_NETWORK_RETRY.search(msg) - if m: - api = m.group("api") - return ParsedUpdate( - stage = STAGE_SOURCE, - source_progress = SourceProgress( - source = "github", - status = "retrying", - message = f"GitHub {api} request failed; retrying automatically.", - ), - ) - - if _RE_GITHUB_COMPLETE.search(msg): - return ParsedUpdate( - stage = STAGE_SOURCE, - source_progress = SourceProgress( - source = "github", - status = "completed", - message = "GitHub source scrape complete.", - ), - ) - m = _RE_SAMPLERS.search(msg) if m: return ParsedUpdate( @@ -331,8 +172,6 @@ def apply_update(job: Job, update: ParsedUpdate) -> None: job.batch.idx = update.batch_idx if update.batch_total is not None: job.batch.total = update.batch_total - if update.source_progress is not None: - _apply_source_progress(job, update.source_progress) if update.stage in USAGE_RESET_STAGES: # usage summary is a short block so we reset once we move into the next stage. @@ -377,67 +216,6 @@ def apply_update(job: Job, update: ParsedUpdate) -> None: usage.rpm = update.usage_rpm -def _apply_source_progress(job: Job, progress: SourceProgress) -> None: - previous = job.source_progress - now = time.time() - - page_items = progress.page_items - if progress.repo and progress.resource and progress.page is not None: - page_key = f"{progress.repo}:{progress.resource}:{progress.page}" - count_key = f"{progress.repo}:{progress.resource}" - if page_key not in job._source_seen_pages: - job._source_seen_pages.add(page_key) - job._source_counts[count_key] = int( - job._source_counts.get(count_key, 0) - ) + int(page_items or 0) - - fetched_items = sum(job._source_counts.values()) - if fetched_items <= 0: - fetched_items = progress.fetched_items or ( - previous.fetched_items if previous else None - ) - - estimated_total = ( - progress.estimated_total - or job.source_progress_estimated_total - or (previous.estimated_total if previous else None) - ) - percent: float | None = progress.percent - if percent is None and estimated_total and fetched_items is not None: - raw_percent = (float(fetched_items) / float(max(1, estimated_total))) * 100.0 - percent = 100.0 if progress.status == "completed" else min(99.0, raw_percent) - if percent is None and previous is not None: - percent = previous.percent - - job.source_progress = SourceProgress( - source = "github", - status = progress.status or (previous.status if previous else None), - repo = progress.repo or (previous.repo if previous else None), - resource = progress.resource or (previous.resource if previous else None), - page = ( - progress.page - if progress.page is not None - else (previous.page if previous else None) - ), - page_items = ( - page_items - if page_items is not None - else (previous.page_items if previous else None) - ), - fetched_items = fetched_items, - estimated_total = estimated_total, - percent = percent, - rate_remaining = ( - progress.rate_remaining - if progress.rate_remaining is not None - else (previous.rate_remaining if previous else None) - ), - retry_after_sec = progress.retry_after_sec, - message = progress.message or (previous.message if previous else None), - updated_at = now, - ) - - def _compute_overall_progress(job: Job, column_progress: Progress) -> Progress: if not job.rows: return column_progress diff --git a/studio/backend/core/data_recipe/jobs/types.py b/studio/backend/core/data_recipe/jobs/types.py index 3d3ddb974e..8d77903238 100644 --- a/studio/backend/core/data_recipe/jobs/types.py +++ b/studio/backend/core/data_recipe/jobs/types.py @@ -35,23 +35,6 @@ class BatchProgress: total: int | None = None -@dataclass -class SourceProgress: - source: str = "github" - status: str | None = None - repo: str | None = None - resource: str | None = None - page: int | None = None - page_items: int | None = None - fetched_items: int | None = None - estimated_total: int | None = None - percent: float | None = None - rate_remaining: int | None = None - retry_after_sec: int | None = None - message: str | None = None - updated_at: float | None = None - - @dataclass class ModelUsage: model: str @@ -74,7 +57,6 @@ class Job: progress: Progress = field(default_factory = Progress) column_progress: Progress = field(default_factory = Progress) batch: BatchProgress = field(default_factory = BatchProgress) - source_progress: SourceProgress | None = None rows: int | None = None cols: int | None = None error: str | None = None @@ -88,15 +70,8 @@ class Job: processor_artifacts: dict[str, Any] | None = None model_usage: dict[str, ModelUsage] = field(default_factory = dict) progress_columns_total: int | None = None - source_progress_estimated_total: int | None = None completed_columns: list[str] = field(default_factory = list) - # Id of the internal sk-unsloth-* API key minted for a local-model - # workflow. Revoked when the job terminates so the key's live window - # matches the run rather than its 24h TTL. - internal_api_key_id: 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) - _source_counts: dict[str, int] = field(default_factory = dict) - _source_seen_pages: set[str] = field(default_factory = set) diff --git a/studio/backend/core/data_recipe/jobs/worker.py b/studio/backend/core/data_recipe/jobs/worker.py index 8c5c7fe657..63e38bd18d 100644 --- a/studio/backend/core/data_recipe/jobs/worker.py +++ b/studio/backend/core/data_recipe/jobs/worker.py @@ -21,15 +21,6 @@ from utils.paths import ensure_dir, recipe_datasets_root _ARTIFACT_ROOT = recipe_datasets_root() -_RE_GITHUB_CURSOR = re.compile(r"\bcursor=[^\s,]+") -_RE_SECRET_TOKEN = re.compile( - r"\b(?:(?:ghp|gho|ghu|ghs|ghr|github_pat)_[A-Za-z0-9_]+|sk-unsloth-[A-Za-z0-9]+)" -) - - -def _sanitize_log_message(message: str) -> str: - message = _RE_GITHUB_CURSOR.sub("cursor=", message) - return _RE_SECRET_TOKEN.sub("", message) class _QueueLogHandler(logging.Handler): @@ -44,7 +35,7 @@ def emit(self, record: logging.LogRecord) -> None: "ts": record.created, "level": record.levelname, "logger": record.name, - "message": _sanitize_log_message(record.getMessage()), + "message": record.getMessage(), } self._q.put(event) except (OSError, RuntimeError, ValueError): @@ -128,16 +119,10 @@ def run_job_process( # Attach queue logger directly to `data_designer` so parser events survive root resets. handler = _QueueLogHandler(event_queue) handler.setLevel(logging.INFO) - for logger_name in ( - "data_designer", - "scraper", - "gh_client", - "data_designer_github_repo_seed", - ): - logger = logging.getLogger(logger_name) - logger.addHandler(handler) - logger.setLevel(logging.INFO) - logger.propagate = True + 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)) @@ -195,8 +180,8 @@ def run_job_process( { "type": EVENT_JOB_ERROR, "ts": time.time(), - "error": _sanitize_log_message(str(exc)), - "stack": _sanitize_log_message(traceback.format_exc(limit = 20)), + "error": str(exc), + "stack": traceback.format_exc(limit = 20), } ) diff --git a/studio/backend/core/data_recipe/local_callable_validators.py b/studio/backend/core/data_recipe/local_callable_validators.py index 44459e88c5..c32b2fccaf 100644 --- a/studio/backend/core/data_recipe/local_callable_validators.py +++ b/studio/backend/core/data_recipe/local_callable_validators.py @@ -33,12 +33,6 @@ _OXC_RUNNER_PATH = _OXC_TOOL_DIR / "validate.mjs" -from utils.native_path_leases import child_env_without_native_path_secret -from utils.subprocess_compat import ( - windows_hidden_subprocess_kwargs as _windows_hidden_subprocess_kwargs, -) - - @dataclass(frozen = True) class OxcLocalCallableValidatorSpec: name: str @@ -249,7 +243,7 @@ def _run_oxc_batch( } try: tmp_dir = ensure_dir(oxc_validator_tmp_root()) - env = child_env_without_native_path_secret() + env = dict(os.environ) tmp_dir_str = str(tmp_dir) env["TMPDIR"] = tmp_dir_str env["TMP"] = tmp_dir_str @@ -262,7 +256,6 @@ def _run_oxc_batch( capture_output = True, check = False, env = env, - **_windows_hidden_subprocess_kwargs(), ) except (OSError, ValueError) as exc: logger.warning("OXC subprocess launch failed: %s", exc) diff --git a/studio/backend/core/export/export.py b/studio/backend/core/export/export.py index 4ab95d896f..d8f2e8fa37 100644 --- a/studio/backend/core/export/export.py +++ b/studio/backend/core/export/export.py @@ -9,14 +9,16 @@ import glob import json import structlog -import tempfile from loggers import get_logger import os import shutil from pathlib import Path from typing import Optional, Tuple, List -from unsloth import FastLanguageModel, FastVisionModel, _IS_MLX +from peft import PeftModel, PeftModelForCausalLM +from unsloth import FastLanguageModel, FastVisionModel from huggingface_hub import HfApi, ModelCard +from transformers.modeling_utils import PushToHubMixin +import torch from utils.hardware import clear_gpu_cache from utils.models import is_vision_model, get_base_model_from_lora @@ -24,16 +26,8 @@ from utils.paths import ensure_dir, outputs_root, resolve_export_dir, resolve_output_dir from core.inference import get_inference_backend -# GPU-only imports — guarded for Apple Silicon where these aren't needed -if not _IS_MLX: - from peft import PeftModel, PeftModelForCausalLM - from transformers.modeling_utils import PushToHubMixin - import torch - logger = get_logger(__name__) -_LLAMA_CPP_SCRIPTS_WARNING_EMITTED = False - def _is_wsl(): """Detect if running under Windows Subsystem for Linux.""" @@ -229,7 +223,7 @@ def load_checkpoint( model, tokenizer = FastModel.from_pretrained( model_name = checkpoint_path, max_seq_length = max_seq_length, - dtype = None if _IS_MLX else torch.float32, + dtype = torch.float32, load_in_4bit = False, trust_remote_code = trust_remote_code, ) @@ -266,12 +260,8 @@ def load_checkpoint( trust_remote_code = trust_remote_code, ) - # Check if PEFT / LoRA model - if _IS_MLX: - # MLX doesn't use PeftModel — detect LoRA via adapter_config.json - self.is_peft = adapter_config.exists() - else: - self.is_peft = isinstance(model, (PeftModel, PeftModelForCausalLM)) + # Check if PEFT model + self.is_peft = isinstance(model, (PeftModel, PeftModelForCausalLM)) # Store loaded model self.current_model = model @@ -333,7 +323,9 @@ def export_merged_model( private: Whether to make the repo private Returns: - Tuple of (success: bool, message: str, output_path: Optional[str]) + Tuple of (success, message, output_path). output_path is the + resolved absolute on-disk directory of the saved model when + ``save_directory`` was set, else None. """ if not self.current_model or not self.current_tokenizer: return False, "No model loaded. Please select a checkpoint first.", None @@ -347,17 +339,14 @@ def export_merged_model( output_path: Optional[str] = None try: - if _IS_MLX: - mlx_save_method = ( - "merged_4bit" if format_type == "4-bit (FP4)" else "merged_16bit" - ) - else: - if format_type == "4-bit (FP4)": - save_method = "merged_4bit_forced" - elif self._audio_type == "whisper": - save_method = None - else: - save_method = "merged_16bit" + # Determine save method + if format_type == "4-bit (FP4)": + save_method = "merged_4bit_forced" + elif self._audio_type == "whisper": + # Whisper uses save_method=None for local 16-bit merged save + save_method = None + else: # 16-bit (FP16) + save_method = "merged_16bit" # Save locally if requested if save_directory: @@ -365,17 +354,11 @@ def export_merged_model( logger.info(f"Saving merged model locally to: {save_directory}") ensure_dir(Path(save_directory)) - if _IS_MLX: - self.current_model.save_pretrained_merged( - save_directory, - self.current_tokenizer, - save_method = mlx_save_method, - ) - else: - self.current_model.save_pretrained_merged( - save_directory, self.current_tokenizer, save_method = save_method - ) + self.current_model.save_pretrained_merged( + save_directory, 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}") output_path = str(Path(save_directory).resolve()) @@ -391,40 +374,17 @@ def export_merged_model( logger.info(f"Pushing merged model to Hub: {repo_id}") - if _IS_MLX: - if save_directory: - self.current_model.push_to_hub_merged( - repo_id, - self.current_tokenizer, - save_directory = save_directory, - token = hf_token, - private = private, - ) - else: - with tempfile.TemporaryDirectory() as tmp_dir: - self.current_model.save_pretrained_merged( - tmp_dir, - self.current_tokenizer, - save_method = mlx_save_method, - ) - self.current_model.push_to_hub_merged( - repo_id, - self.current_tokenizer, - save_directory = tmp_dir, - token = hf_token, - private = private, - ) - else: - hub_save_method = ( - save_method if save_method is not None else "merged_16bit" - ) - self.current_model.push_to_hub_merged( - repo_id, - self.current_tokenizer, - save_method = hub_save_method, - token = hf_token, - private = private, - ) + # Whisper uses save_method=None for local but "merged_16bit" for hub push + hub_save_method = ( + save_method if save_method is not None else "merged_16bit" + ) + self.current_model.push_to_hub_merged( + repo_id, + self.current_tokenizer, + save_method = hub_save_method, + token = hf_token, + private = private, + ) logger.info(f"Model pushed successfully to {repo_id}") return True, "Model exported successfully", output_path @@ -449,7 +409,9 @@ def export_base_model( Export base model (for non-PEFT models). Returns: - Tuple of (success: bool, message: str, output_path: Optional[str]) + Tuple of (success, message, output_path). output_path is the + resolved absolute on-disk directory of the saved model when + ``save_directory`` was set, else None. """ if not self.current_model or not self.current_tokenizer: return False, "No model loaded. Please select a checkpoint first.", None @@ -469,16 +431,8 @@ def export_base_model( logger.info(f"Saving base model locally to: {save_directory}") ensure_dir(Path(save_directory)) - if _IS_MLX: - # MLX: save_pretrained_merged handles non-LoRA models too - # (fuse() is a no-op when there are no LoRA layers) - self.current_model.save_pretrained_merged( - save_directory, - self.current_tokenizer, - ) - else: - self.current_model.save_pretrained(save_directory) - self.current_tokenizer.save_pretrained(save_directory) + 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) @@ -496,73 +450,44 @@ def export_base_model( logger.info(f"Pushing base model to Hub: {repo_id}") - if _IS_MLX: - if save_directory: - self.current_model.push_to_hub_merged( - repo_id, - self.current_tokenizer, - save_directory = save_directory, - token = hf_token, - private = private, - ) - else: - with tempfile.TemporaryDirectory() as tmp_dir: - self.current_model.save_pretrained_merged( - tmp_dir, - self.current_tokenizer, - ) - self.current_model.push_to_hub_merged( - repo_id, - self.current_tokenizer, - save_directory = tmp_dir, - token = hf_token, - private = private, - ) - else: - # Get base model name from request or model config - base_model = ( - base_model_id - or self.current_model.config._name_or_path - or "unknown" - ) + # Get base model name from request or model config + base_model = ( + base_model_id + or self.current_model.config._name_or_path + or "unknown" + ) - # Create repo - hf_api = HfApi(token = hf_token) - repo_id = PushToHubMixin._create_repo( - PushToHubMixin, - repo_id = repo_id, - private = private, - token = hf_token, - ) - username = repo_id.split("/")[0] - - # Create and push model card - content = MODEL_CARD.format( - username = username, - base_model = base_model, - model_type = self.current_model.config.model_type, - method = "", - extra = "unsloth", - ) - card = ModelCard(content) - card.push_to_hub( - repo_id, token = hf_token, commit_message = "Unsloth Model Card" - ) + # Create repo + hf_api = HfApi(token = hf_token) + repo_id = PushToHubMixin._create_repo( + PushToHubMixin, + repo_id = repo_id, + private = private, + token = hf_token, + ) + username = repo_id.split("/")[0] + + # Create and push model card + content = MODEL_CARD.format( + username = username, + base_model = base_model, + model_type = self.current_model.config.model_type, + method = "", + extra = "unsloth", + ) + card = ModelCard(content) + card.push_to_hub( + repo_id, token = hf_token, commit_message = "Unsloth Model Card" + ) - # Upload model files - if save_directory: - hf_api.upload_folder( - folder_path = save_directory, - repo_id = repo_id, - repo_type = "model", - ) - logger.info(f"Model pushed successfully to {repo_id}") - else: - return ( - False, - "Local save directory required for Hub upload", - None, - ) + # Upload model files + if save_directory: + hf_api.upload_folder( + folder_path = save_directory, repo_id = repo_id, repo_type = "model" + ) + logger.info(f"Model pushed successfully to {repo_id}") + else: + return False, "Local save directory required for Hub upload", None return True, "Model exported successfully", output_path @@ -592,7 +517,9 @@ def export_gguf( hf_token: Hugging Face token Returns: - Tuple of (success: bool, message: str, output_path: Optional[str]) + Tuple of (success, message, output_path). output_path is the + resolved absolute on-disk directory containing the .gguf + files when ``save_directory`` was set, else None. """ if not self.current_model or not self.current_tokenizer: return False, "No model loaded. Please select a checkpoint first.", None @@ -602,31 +529,6 @@ def export_gguf( # Convert quantization method to lowercase for unsloth quant_method = quantization_method.lower() - # Pin convert_hf_to_gguf.py to the same llama.cpp ref as the - # llama-quantize binary (Studio installs at a tagged ref via - # setup.sh) so it can't drift past the pinned binary's gguf API. - # Set before both branches; hub-only export has save_directory == "". - global _LLAMA_CPP_SCRIPTS_WARNING_EMITTED - try: - from unsloth_zoo.llama_cpp import ( - LLAMA_CPP_DEFAULT_DIR, - _resolve_local_convert_script, # noqa: F401 - ) - - os.environ.setdefault( - "UNSLOTH_LLAMA_CPP_SCRIPTS_DIR", LLAMA_CPP_DEFAULT_DIR - ) - except ImportError: - if not _LLAMA_CPP_SCRIPTS_WARNING_EMITTED: - logger.warning( - "Unsloth: installed unsloth_zoo does not honor " - "UNSLOTH_LLAMA_CPP_SCRIPTS_DIR; convert_hf_to_gguf.py will " - "still be downloaded from llama.cpp master and may drift " - "past the pinned llama-quantize binary. Upgrade unsloth_zoo " - "to activate the local script pin." - ) - _LLAMA_CPP_SCRIPTS_WARNING_EMITTED = True - # Save locally if requested if save_directory: save_directory = str(resolve_export_dir(save_directory)) @@ -763,7 +665,9 @@ def export_lora_adapter( Export LoRA adapter only (not merged). Returns: - Tuple of (success: bool, message: str, output_path: Optional[str]) + Tuple of (success, message, output_path). output_path is the + resolved absolute on-disk directory of the saved adapter + when ``save_directory`` was set, else None. """ if not self.current_model or not self.current_tokenizer: return False, "No model loaded. Please select a checkpoint first.", None @@ -779,13 +683,8 @@ def export_lora_adapter( logger.info(f"Saving LoRA adapter locally to: {save_directory}") ensure_dir(Path(save_directory)) - if _IS_MLX: - # MLX: save adapters.safetensors + tokenizer files - self.current_model.save_lora_adapters(save_directory) - self.current_tokenizer.save_pretrained(save_directory) - else: - self.current_model.save_pretrained(save_directory) - self.current_tokenizer.save_pretrained(save_directory) + self.current_model.save_pretrained(save_directory) + self.current_tokenizer.save_pretrained(save_directory) logger.info(f"Adapter saved successfully to {save_directory}") output_path = str(Path(save_directory).resolve()) @@ -800,24 +699,10 @@ def export_lora_adapter( logger.info(f"Pushing LoRA adapter to Hub: {repo_id}") - if _IS_MLX: - with tempfile.TemporaryDirectory() as tmp_dir: - self.current_model.save_lora_adapters(tmp_dir) - self.current_tokenizer.save_pretrained(tmp_dir) - hf_api = HfApi(token = hf_token) - hf_api.create_repo(repo_id, private = private, exist_ok = True) - hf_api.upload_folder( - folder_path = tmp_dir, - repo_id = repo_id, - repo_type = "model", - ) - else: - self.current_model.push_to_hub( - repo_id, token = hf_token, private = private - ) - self.current_tokenizer.push_to_hub( - repo_id, token = hf_token, private = private - ) + self.current_model.push_to_hub(repo_id, token = hf_token, private = private) + self.current_tokenizer.push_to_hub( + repo_id, token = hf_token, private = private + ) logger.info(f"Adapter pushed successfully to {repo_id}") return True, "LoRA adapter exported successfully", output_path diff --git a/studio/backend/core/export/orchestrator.py b/studio/backend/core/export/orchestrator.py index 82de925592..206dbd6dbb 100644 --- a/studio/backend/core/export/orchestrator.py +++ b/studio/backend/core/export/orchestrator.py @@ -163,28 +163,21 @@ def is_export_active(self) -> bool: def _spawn_subprocess(self, config: dict) -> None: """Spawn a new export subprocess.""" - from utils.native_path_leases import ( - native_path_secret_removed_for_child_start, - run_without_native_path_secret, - ) - from .worker import run_export_process - with native_path_secret_removed_for_child_start(): - self._cmd_queue = _CTX.Queue() - self._resp_queue = _CTX.Queue() - - self._proc = _CTX.Process( - target = run_without_native_path_secret, - args = (run_export_process,), - kwargs = { - "cmd_queue": self._cmd_queue, - "resp_queue": self._resp_queue, - "config": config, - }, - daemon = True, - ) - self._proc.start() + self._cmd_queue = _CTX.Queue() + self._resp_queue = _CTX.Queue() + + self._proc = _CTX.Process( + target = run_export_process, + kwargs = { + "cmd_queue": self._cmd_queue, + "resp_queue": self._resp_queue, + "config": config, + }, + daemon = True, + ) + self._proc.start() logger.info("Export subprocess started (pid=%s)", self._proc.pid) def _shutdown_subprocess(self, timeout: float = 10.0) -> None: diff --git a/studio/backend/core/inference/audio_codecs.py b/studio/backend/core/inference/audio_codecs.py index df3bf27c16..bcf3ec2937 100644 --- a/studio/backend/core/inference/audio_codecs.py +++ b/studio/backend/core/inference/audio_codecs.py @@ -8,7 +8,6 @@ import io import re -import subprocess import wave import structlog from loggers import get_logger @@ -17,11 +16,6 @@ import numpy as np import torch -from utils.native_path_leases import child_env_without_native_path_secret -from utils.subprocess_compat import ( - windows_hidden_subprocess_kwargs as _windows_hidden_subprocess_kwargs, -) - logger = get_logger(__name__) @@ -87,6 +81,7 @@ def _load_bicodec(self, device: str, model_repo_path: Optional[str] = None) -> N return import os import sys + import subprocess # Clone SparkAudio/Spark-TTS GitHub repo for the sparktts Python package # (same approach as training — the HF model repos don't contain the package) @@ -106,8 +101,6 @@ def _load_bicodec(self, device: str, model_repo_path: Optional[str] = None) -> N spark_code_dir, ], check = True, - env = child_env_without_native_path_secret(), - **_windows_hidden_subprocess_kwargs(), ) if spark_code_dir not in sys.path: @@ -126,6 +119,7 @@ def _load_dac(self, device: str) -> None: return import os import sys + import subprocess # Clone OuteTTS repo (same pattern as Spark-TTS / BiCodec) # The pip package has problematic dependencies; the notebook clones and @@ -145,8 +139,6 @@ def _load_dac(self, device: str) -> None: outetts_code_dir, ], check = True, - env = child_env_without_native_path_secret(), - **_windows_hidden_subprocess_kwargs(), ) # Remove files that pull in heavy / incompatible dependencies # (matches notebook: gguf_model.py is under models/, others under outetts/) diff --git a/studio/backend/core/inference/llama_cpp.py b/studio/backend/core/inference/llama_cpp.py index 8da836de38..2e26995309 100644 --- a/studio/backend/core/inference/llama_cpp.py +++ b/studio/backend/core/inference/llama_cpp.py @@ -11,7 +11,6 @@ import atexit import contextlib import json -import os import re import struct import structlog @@ -19,23 +18,16 @@ import shutil import socket import subprocess -import sys import threading import time from pathlib import Path -from typing import Generator, List, Optional +from typing import Generator, Optional from urllib.parse import urlparse import httpx -from utils.native_path_leases import child_env_without_native_path_secret -from utils.subprocess_compat import ( - windows_hidden_subprocess_kwargs as _windows_hidden_subprocess_kwargs, -) - logger = get_logger(__name__) - # ── Pre-compiled patterns for plan-without-action re-prompt ── # Forward-looking intent signals that indicate the model is # describing what it *will* do rather than giving a final answer. @@ -55,21 +47,6 @@ r")" ) _MAX_REPROMPTS = 3 - -# Without max_tokens, llama-server defaults to n_predict = n_ctx (up to -# 262144 for Qwen3.5), producing many-minute zombie decodes when cancel -# fails. t_max_predict_ms is a wall-clock backstop applied unconditionally, -# but the llama.cpp README notes it ONLY fires after a newline has been -# generated -- a model stuck in a long unbroken non-newline sequence is -# unbounded by it. So we still want a token cap as the front-line limiter. -# -# The cap is the model's effective context length when we know it, -# falling back to a generous floor when metadata is unavailable. 4096 was -# too low: Qwen3 / gpt-oss reasoning traces routinely exceed it, and any -# OpenAI-API caller that omits max_tokens (langchain, llama-index, raw -# curl) sees responses silently truncated mid-sentence. -_DEFAULT_MAX_TOKENS_FLOOR = 32768 -_DEFAULT_T_MAX_PREDICT_MS = 600_000 # 10 min _REPROMPT_MAX_CHARS = 2000 # ── Pre-compiled patterns for GGUF shard detection ─────────── @@ -77,238 +54,6 @@ _SHARD_RE = re.compile(r"^(.*)-\d{5}-of-\d{5}\.gguf$") -# ── Sliding-window-pattern resolver ─────────────────────────── -# Resolves the per-layer SWA mask when a GGUF reports a sliding window -# but no `sliding_window_pattern` field. Tier order in -# `_resolve_swa_pattern`: GGUF metadata, on-disk cache, bootstrap dict -# below, transformers introspection, HF Hub config.json, legacy 1/4 -# fallback. Period N means layer i is SWA iff `(i + 1) % N != 0`, -# matching transformers. Skipped on purpose: phi3 (no key/val length -# in GGUF, window >= ctx anyway), qwen2 family (converter strips -# sliding_window when use_sliding_window=False), mistral v0.1/v0.2 -# (all-SWA can't be expressed as a period). -_BOOTSTRAP_SWA_DEFAULTS: dict[str, int] = { - "gemma2": 2, # Gemma2Config.sliding_window_pattern - "gemma3": 6, # Gemma3TextConfig.sliding_window_pattern - "gemma3n": 5, # text_config.layer_types: SWA*4 + FULL - "gpt_oss": 2, # text_config.layer_types: alternating - "cohere2": 4, # Cohere2Config.sliding_window_pattern -} - -# Process-wide cache backed by JSON on disk. Values are int period or -# list[bool] mask. Lazy-loaded. -_SWA_CACHE: Optional[dict] = None -_SWA_CACHE_LOCK = threading.Lock() - - -def _swa_cache_path() -> Path: - home = os.environ.get("UNSLOTH_STUDIO_HOME") or os.environ.get("STUDIO_HOME") - base = Path(home) if home else Path.home() / ".unsloth" / "studio" - return base / "swa_cache.json" - - -def _load_swa_cache() -> dict: - global _SWA_CACHE - with _SWA_CACHE_LOCK: - if _SWA_CACHE is not None: - return _SWA_CACHE - try: - with open(_swa_cache_path()) as f: - _SWA_CACHE = json.load(f) - if not isinstance(_SWA_CACHE, dict): - _SWA_CACHE = {} - except (FileNotFoundError, json.JSONDecodeError, OSError): - _SWA_CACHE = {} - return _SWA_CACHE - - -def _save_swa_cache(cache: dict) -> None: - try: - path = _swa_cache_path() - path.parent.mkdir(parents = True, exist_ok = True) - tmp = path.with_suffix(".json.tmp") - with open(tmp, "w") as f: - json.dump(cache, f, indent = 2, sort_keys = True) - tmp.replace(path) - except OSError: - pass - - -def _period_from_layer_types(layer_types: list) -> Optional[int]: - """Smallest period N where `(i+1) % N != 0` matches the SWA mask, - or None if no fixed period fits.""" - if not layer_types: - return None - is_swa = ["full" not in str(t).lower() for t in layer_types] - n = len(is_swa) - for N in range(1, n + 1): - if all(((i + 1) % N != 0) == is_swa[i] for i in range(n)): - return N - return None - - -def _fetch_swa_entry_from_hf(repo_id: str) -> Optional[object]: - try: - from huggingface_hub import hf_hub_download - - cfg_path = hf_hub_download(repo_id, "config.json", repo_type = "model") - with open(cfg_path) as f: - cfg = json.load(f) - except Exception: - return None - - src = cfg.get("text_config") if isinstance(cfg.get("text_config"), dict) else cfg - period = src.get("sliding_window_pattern") - if isinstance(period, int) and period > 0: - return period - lt = src.get("layer_types") - if isinstance(lt, list) and lt: - return _period_from_layer_types(lt) or [ - "full" not in str(t).lower() for t in lt - ] - return None - - -def _arch_aliases(arch: str) -> tuple: - # GGUF emits `falcon-h1`; HF model_type is `falcon_h1`. Normalise both ways. - seen = [] - for a in (arch, arch.replace("-", "_"), arch.replace("_", "-")): - if a and a not in seen: - seen.append(a) - return tuple(seen) - - -def _swa_entry_from_config_obj(cfg) -> Optional[object]: - src = getattr(cfg, "text_config", None) or cfg - period = getattr(src, "sliding_window_pattern", None) - if isinstance(period, int) and period > 0: - return period - lt = getattr(src, "layer_types", None) - if isinstance(lt, list) and lt: - return _period_from_layer_types(lt) or [ - "full" not in str(t).lower() for t in lt - ] - return None - - -_SWA_PATTERN_SOURCE_RE = re.compile( - r"sliding_window_pattern\s*(?::\s*[\w\[\], ]*)?\s*=\s*(\d+)" -) - - -def _resolve_swa_entry_from_transformers(arch: str) -> Optional[object]: - """Default-instantiate the matching Config; on failure, regex-parse - its source for `sliding_window_pattern = N`.""" - try: - from transformers.models.auto.configuration_auto import ( - CONFIG_MAPPING, - CONFIG_MAPPING_NAMES, - ) - except Exception: - return None - - cfg_class = None - for alias in _arch_aliases(arch): - if alias in CONFIG_MAPPING_NAMES: - try: - cfg_class = CONFIG_MAPPING[alias] - break - except Exception: - cfg_class = None - if cfg_class is None: - return None - - try: - if (entry := _swa_entry_from_config_obj(cfg_class())) is not None: - return entry - except Exception: - pass - - import inspect - - candidates = [cfg_class] - text_cfg_class = getattr(cfg_class, "sub_configs", {}).get("text_config") - if text_cfg_class is not None: - candidates.append(text_cfg_class) - for cls in candidates: - try: - src = inspect.getsource(cls) - except (OSError, TypeError): - continue - if m := _SWA_PATTERN_SOURCE_RE.search(src): - period = int(m.group(1)) - if period > 0: - return period - return None - - -def _resolve_swa_pattern( - arch: Optional[str], - n_layers: Optional[int], - source_repo_candidates: tuple = (), - *, - allow_network: Optional[bool] = None, -) -> Optional[list]: - if not arch or not n_layers: - return None - if allow_network is None: - allow_network = os.environ.get("UNSLOTH_STUDIO_OFFLINE", "0") not in ( - "1", - "true", - "True", - "yes", - ) - - cache = _load_swa_cache() - - def _entry_to_mask(entry): - if isinstance(entry, int) and entry > 0: - return [(i + 1) % entry != 0 for i in range(n_layers)] - if isinstance(entry, list) and entry: - return [bool(entry[i % len(entry)]) for i in range(n_layers)] - return None - - def _persist(entry): - with _SWA_CACHE_LOCK: - cache[arch] = entry - _save_swa_cache(cache) - - if (entry := cache.get(arch)) is not None: - if (mask := _entry_to_mask(entry)) is not None: - return mask - - if (entry := _BOOTSTRAP_SWA_DEFAULTS.get(arch)) is not None: - return _entry_to_mask(entry) - - entry = _resolve_swa_entry_from_transformers(arch) - if entry is not None: - _persist(entry) - return _entry_to_mask(entry) - - # Tier 3: live HF fetch (with persistent caching of the result) - if allow_network: - for repo_id in source_repo_candidates: - if not repo_id: - continue - entry = _fetch_swa_entry_from_hf(repo_id) - if entry is not None: - _persist(entry) - return _entry_to_mask(entry) - - return None - - -def _hf_repo_from_url(url: Optional[str]) -> Optional[str]: - """Strip `https://huggingface.co/owner/name(/...)` to `owner/name`.""" - if not url or "huggingface.co/" not in url: - return None - tail = url.split("huggingface.co/", 1)[1].rstrip("/") - parts = tail.split("/") - if len(parts) < 2: - return None - return f"{parts[0]}/{parts[1]}" - - # Model size extraction — lazy import to avoid pulling in transformers # at module level. See PR description for the full explanation. def _extract_model_size_b(model_id: str): @@ -336,84 +81,6 @@ def _extract_model_size_b(model_id: str): _TC_PARAM_CLOSE_RE = re.compile(r"\s*\s*$") -_TOOL_TEMPLATE_MARKERS = ( - "{%- if tools %}", - "{%- if tools -%}", - "{% if tools %}", - "{% if tools -%}", - '"role" == "tool"', - "'role' == 'tool'", - 'message.role == "tool"', - "message.role == 'tool'", -) - - -def detect_reasoning_flags( - chat_template: Optional[str], - model_identifier: Optional[str] = None, - *, - log_source: Optional[str] = None, -) -> dict: - """Classify a chat template's reasoning and tool-calling capabilities. - - Returns a dict with the same five keys populated by the GGUF sniffer: - ``supports_reasoning``, ``reasoning_style`` - (``"enable_thinking"`` | ``"reasoning_effort"``), - ``reasoning_always_on``, ``supports_preserve_thinking``, and - ``supports_tools``. Used by both the llama-server backend at load - time and the safetensors/transformers paths in ``routes/inference`` - so the two agree on what the frontend will see. - """ - flags = { - "supports_reasoning": False, - "reasoning_style": "enable_thinking", - "reasoning_always_on": False, - "supports_preserve_thinking": False, - "supports_tools": False, - } - if not chat_template: - return flags - tpl = chat_template - prefix = f"{log_source}: " if log_source else "" - - if "enable_thinking" in tpl: - flags["supports_reasoning"] = True - flags["reasoning_style"] = "enable_thinking" - logger.info(f"{prefix}model supports reasoning (enable_thinking)") - elif "reasoning_effort" in tpl: - # gpt-oss / Harmony templates use reasoning_effort - # ("low" | "medium" | "high") instead of a boolean. - flags["supports_reasoning"] = True - flags["reasoning_style"] = "reasoning_effort" - logger.info(f"{prefix}model supports reasoning (reasoning_effort)") - elif "thinking" in tpl: - # DeepSeek uses 'thinking' instead of 'enable_thinking' - normalized_id = (model_identifier or "").lower() - if "deepseek" in normalized_id: - flags["supports_reasoning"] = True - logger.info(f"{prefix}model supports reasoning (DeepSeek thinking)") - - # Hardcoded tags or reasoning_content in the template mean - # thinking is always on (no toggle to disable it). - if not flags["supports_reasoning"]: - if ("" in tpl and "" in tpl) or "reasoning_content" in tpl: - flags["supports_reasoning"] = True - flags["reasoning_always_on"] = True - logger.info(f"{prefix}model always reasons ( tags in template)") - - # preserve_thinking is an independent kwarg on some Qwen templates - # that keeps historical blocks in prior assistant turns. - if "preserve_thinking" in tpl: - flags["supports_preserve_thinking"] = True - logger.info(f"{prefix}model supports preserve_thinking") - - if any(marker in tpl for marker in _TOOL_TEMPLATE_MARKERS): - flags["supports_tools"] = True - logger.info(f"{prefix}model supports tool calling") - - return flags - - class LlamaCppBackend: """ Manages a llama-server subprocess for GGUF model inference. @@ -439,8 +106,6 @@ def __init__(self): self._chat_template: Optional[str] = None self._supports_reasoning: bool = False self._reasoning_always_on: bool = False - self._reasoning_style: str = "enable_thinking" - self._supports_preserve_thinking: bool = False self._supports_tools: bool = False self._cache_type_kv: Optional[str] = None self._reasoning_default: bool = True @@ -448,24 +113,17 @@ def __init__(self): # KV-cache estimation fields (populated by _read_gguf_metadata) self._n_layers: Optional[int] = None self._n_kv_heads: Optional[int] = None - self._n_kv_heads_by_layer: Optional[list[int]] = None self._n_heads: Optional[int] = None self._embedding_length: Optional[int] = None - # Architecture-aware KV fields for 5-path estimation + # Architecture-aware KV fields (8 new fields for 5-path estimation) self._kv_key_length: Optional[int] = None self._kv_value_length: Optional[int] = None self._sliding_window: Optional[int] = None - self._sliding_window_pattern: Optional[list[bool]] = None self._full_attention_interval: Optional[int] = None self._kv_lora_rank: Optional[int] = None self._key_length_mla: Optional[int] = None - self._kv_key_length_swa: Optional[int] = None - self._kv_value_length_swa: Optional[int] = None self._ssm_inner_size: Optional[int] = None self._ssm_state_size: Optional[int] = None - # Last N layers reuse KV from earlier layers and don't allocate - # their own cache (Gemma 3n / Gemma 4: .attention.shared_kv_layers). - self._shared_kv_layers: Optional[int] = None self._lock = threading.Lock() self._stdout_lines: list[str] = [] self._stdout_thread: Optional[threading.Thread] = None @@ -629,51 +287,10 @@ def supports_reasoning(self) -> bool: def reasoning_always_on(self) -> bool: return self._reasoning_always_on - @property - def reasoning_style(self) -> str: - return self._reasoning_style - - @property - def supports_preserve_thinking(self) -> bool: - return self._supports_preserve_thinking - @property def reasoning_default(self) -> bool: return self._reasoning_default - def _reasoning_kwargs(self, enable_thinking: bool) -> dict: - if self._reasoning_style == "reasoning_effort": - return {"reasoning_effort": "high" if enable_thinking else "low"} - return {"enable_thinking": enable_thinking} - - def _request_reasoning_kwargs( - self, - enable_thinking: Optional[bool], - reasoning_effort: Optional[str] = None, - preserve_thinking: Optional[bool] = None, - ) -> Optional[dict]: - """Build chat_template_kwargs from per-request reasoning fields. - - Produces a merged dict covering the active model's reasoning style - (``enable_thinking`` or ``reasoning_effort``) plus the independent - ``preserve_thinking`` kwarg when the template supports it. - """ - kwargs: dict = {} - # Always-on reasoning models hardcode tags in their template - # and do not consume enable_thinking / reasoning_effort -- skip. - if self._supports_reasoning and not self._reasoning_always_on: - if self._reasoning_style == "reasoning_effort": - if reasoning_effort in ("low", "medium", "high"): - kwargs["reasoning_effort"] = reasoning_effort - elif enable_thinking is not None: - kwargs["reasoning_effort"] = "high" if enable_thinking else "low" - else: - if enable_thinking is not None: - kwargs["enable_thinking"] = enable_thinking - if self._supports_preserve_thinking and preserve_thinking is not None: - kwargs["preserve_thinking"] = preserve_thinking - return kwargs or None - @property def supports_tools(self) -> bool: return self._supports_tools @@ -732,46 +349,22 @@ def _find_llama_server_binary() -> Optional[str]: if win_bin.is_file(): return str(win_bin) - # 2-4. Match installer layout: env-mode -> $STUDIO_HOME/llama.cpp; - # default/HOME-redirect -> ~/.unsloth/llama.cpp (sibling of studio). - legacy_llama = Path.home() / ".unsloth" / "llama.cpp" - try: - from utils.paths.storage_roots import studio_root as _sr # noqa: WPS433 - - _resolved_sr = _sr() - _legacy_studio = Path.home() / ".unsloth" / "studio" - try: - _is_legacy = _resolved_sr.resolve() == _legacy_studio.resolve() - except (OSError, ValueError): - _is_legacy = _resolved_sr == _legacy_studio - if _is_legacy: - search_roots = [legacy_llama] - else: - # why: _kill_orphaned_servers excludes the legacy root in custom - # mode; discovery must match so we never spawn a server we then - # refuse to clean up. UNSLOTH_LLAMA_CPP_PATH (handled earlier) - # is the explicit way to share a build across roots. - search_roots = [_resolved_sr / "llama.cpp"] - except (ImportError, OSError, ValueError): - search_roots = [legacy_llama] - _seen_roots: set[str] = set() - _unique_roots: list[Path] = [] - for r in search_roots: - k = str(r) - if k not in _seen_roots: - _seen_roots.add(k) - _unique_roots.append(r) - for unsloth_home in _unique_roots: - home_root = unsloth_home / binary_name - if home_root.is_file(): - return str(home_root) - home_linux = unsloth_home / "build" / "bin" / binary_name - if home_linux.is_file(): - return str(home_linux) - if sys.platform == "win32": - home_win = unsloth_home / "build" / "bin" / "Release" / binary_name - if home_win.is_file(): - return str(home_win) + # 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] @@ -829,24 +422,14 @@ def _get_gguf_size_bytes(model_path: str) -> int: @staticmethod def _get_gpu_free_memory() -> list[tuple[int, int]]: - """Query free memory per GPU. - - Order: - 1. ``nvidia-smi`` (NVIDIA CUDA hosts) -- respects - ``CUDA_VISIBLE_DEVICES``. - 2. ``torch.cuda.mem_get_info`` -- universal fallback that - works on AMD ROCm too because the HIP runtime - reuses the entire ``torch.cuda.*`` namespace. Covers the - AMD case for issue #5106 (nvidia-smi-only probe silently - returned [] on AMD hosts) and also rescues NVIDIA hosts - where ``nvidia-smi`` is missing from PATH. - - Returns list of (gpu_index, free_mib) sorted by index. Empty - list if no supported GPU is reachable. + """Query free memory per GPU via nvidia-smi. + + Returns list of (gpu_index, free_mib) sorted by index. + Respects CUDA_VISIBLE_DEVICES if set. + Returns empty list if nvidia-smi is not available. """ import os - # ── NVIDIA via nvidia-smi ──────────────────────────────────── try: result = subprocess.run( [ @@ -857,98 +440,31 @@ def _get_gpu_free_memory() -> list[tuple[int, int]]: capture_output = True, text = True, timeout = 10, - env = child_env_without_native_path_secret(), - **_windows_hidden_subprocess_kwargs(), ) - if result.returncode == 0: - allowed: Optional[set[int]] = None - cvd = os.environ.get("CUDA_VISIBLE_DEVICES") - if cvd is not None: - try: - # `if x.strip()` filters trailing-comma masks like - # "0,1," which would otherwise raise ValueError on - # an empty token. An explicitly empty mask (CVD="") - # yields an empty `allowed` set so all GPUs are - # filtered out, matching the codebase convention. - allowed = set( - int(x.strip()) for x in cvd.split(",") if x.strip() - ) - except ValueError: - pass - gpus: list[tuple[int, int]] = [] - for line in result.stdout.strip().splitlines(): - parts = line.split(",") - if len(parts) == 2: - idx = int(parts[0].strip()) - free_mib = int(parts[1].strip()) - if allowed is not None and idx not in allowed: - continue - gpus.append((idx, free_mib)) - # Match the docstring's sort-by-id guarantee. nvidia-smi - # almost always returns sorted output, but driver order - # is not formally guaranteed. - gpus.sort(key = lambda g: g[0]) - if gpus: - return gpus - except Exception as e: - logger.debug(f"nvidia-smi probe failed: {e}") - - # ── Torch fallback (covers AMD ROCm and missing nvidia-smi) ── - try: - import torch - - if not hasattr(torch, "cuda") or not torch.cuda.is_available(): + if result.returncode != 0: return [] - if not hasattr(torch.cuda, "mem_get_info"): - return [] - # torch.cuda enumerates GPUs RELATIVE to the visibility mask. - # On NVIDIA builds the mask is CUDA_VISIBLE_DEVICES; on AMD - # ROCm builds it is HIP_VISIBLE_DEVICES (or ROCR_VISIBLE_DEVICES - # if HIP is unset). Downstream we feed these IDs back into the - # llama-server subprocess as CVD, so we must translate visible - # ordinals back to physical indices first; otherwise launching - # with ``CUDA_VISIBLE_DEVICES=2,3`` would get rewritten to - # ``CUDA_VISIBLE_DEVICES=0,1`` and target the wrong GPUs. - physical_ids: Optional[list[int]] = None - # Match the codebase convention in - # ``utils/hardware/hardware.py::_get_parent_visible_gpu_spec``: - # treat an explicitly empty mask (``HIP_VISIBLE_DEVICES=""``) - # as "set to no GPUs" rather than falling through to the next - # var. ``or`` would coerce empty string to falsy and silently - # promote the wrong source. - if getattr(torch.version, "hip", None) is not None: - hip_v = os.environ.get("HIP_VISIBLE_DEVICES") - rocr_v = os.environ.get("ROCR_VISIBLE_DEVICES") - cvd = ( - hip_v - if hip_v is not None - else rocr_v - if rocr_v is not None - else os.environ.get("CUDA_VISIBLE_DEVICES") - ) - else: - cvd = os.environ.get("CUDA_VISIBLE_DEVICES") - if cvd is not None: + + # Parse which GPUs are allowed by existing CUDA_VISIBLE_DEVICES + allowed = None + cvd = os.environ.get("CUDA_VISIBLE_DEVICES") + if cvd is not None and cvd.strip(): try: - # Empty mask (CVD="") yields an empty list so the - # below loop produces no GPUs, consistent with the - # nvidia-smi path and utils/hardware/hardware.py. - physical_ids = [int(x.strip()) for x in cvd.split(",") if x.strip()] + allowed = set(int(x.strip()) for x in cvd.split(",")) except ValueError: - physical_ids = None + pass # Non-numeric (e.g., "GPU-uuid"), ignore filter + gpus = [] - for ordinal in range(torch.cuda.device_count()): - free_bytes, _total_bytes = torch.cuda.mem_get_info(ordinal) - idx = ( - physical_ids[ordinal] - if physical_ids is not None and ordinal < len(physical_ids) - else ordinal - ) - gpus.append((idx, free_bytes // (1024 * 1024))) - # Match the nvidia-smi path's docstring guarantee of sorted-by-id. - return sorted(gpus, key = lambda g: g[0]) + for line in result.stdout.strip().splitlines(): + parts = line.split(",") + if len(parts) == 2: + idx = int(parts[0].strip()) + free_mib = int(parts[1].strip()) + if allowed is not None and idx not in allowed: + continue + gpus.append((idx, free_mib)) + return gpus except Exception as e: - logger.debug(f"torch GPU probe failed: {e}") + logger.debug(f"Failed to query GPU free memory via nvidia-smi: {e}") return [] @staticmethod @@ -1008,29 +524,13 @@ def _can_estimate_kv(self) -> bool: # New-style: need both explicit key AND value dimensions if self._kv_key_length is not None and self._kv_value_length is not None: return True - # Legacy: need embedding_length + a head count (scalar or per-layer). + # Legacy: need embedding_length + head count return self._embedding_length is not None and ( - self._n_kv_heads is not None - or self._n_heads is not None - or self._n_kv_heads_by_layer is not None + self._n_kv_heads is not None or self._n_heads is not None ) - def _kv_heads_for_layer(self, layer_idx: int, fallback: int) -> int: - if self._n_kv_heads_by_layer is not None and layer_idx < len( - self._n_kv_heads_by_layer - ): - return self._n_kv_heads_by_layer[layer_idx] - return fallback - def _estimate_kv_cache_bytes( - self, - n_ctx: int, - cache_type_kv: Optional[str] = None, - *, - swa_full: bool = False, - n_parallel: int = 1, - kv_unified: bool = True, - ctx_checkpoints: int = 0, + self, n_ctx: int, cache_type_kv: Optional[str] = None ) -> int: """Estimate KV cache VRAM for a given context length. @@ -1041,34 +541,12 @@ def _estimate_kv_cache_bytes( 4. GQA -- standard full KV with explicit key/value dimensions 5. Legacy -- fallback using embed // n_heads - Server-flag knobs (mirror llama-server's CLI): - swa_full -- ``--swa-full``: force SWA layers to cache the - full ``n_ctx`` (collapses path 3 to path 4 - sizing for the SWA layers). - n_parallel -- ``--parallel``: number of server slots. - Verified empirically against llama-server: - non-SWA layers stay constant (cells split - across slots), SWA layers scale linearly - (per-slot window). - kv_unified -- ``--kv-unified`` (default on): retained for - API forward-compat. Currently a no-op for - memory math because the unified buffer total - matches per-slot buffers in measured cases. - ctx_checkpoints -- ``--ctx-checkpoints``: SWA snapshot count per - slot (PR #15293). Each snapshot stores one - sliding-window of state per SWA layer. - Returns 0 if metadata is insufficient for estimation. """ if not self._can_estimate_kv() or n_ctx <= 0: return 0 n_layers = self._n_layers # type: ignore[assignment] - # Gemma 3n / Gemma 4 reuse KV from earlier layers in the last - # ``shared_kv_layers`` blocks -- those don't allocate their own - # cache. Floor at 1 so a misconfigured GGUF can't zero out KV. - shared = self._shared_kv_layers or 0 - n_layers_kv = max(1, n_layers - shared) n_kv = self._n_kv_heads or self._n_heads or 1 # type: ignore[assignment] # Bytes per element depends on KV cache quantization @@ -1084,8 +562,6 @@ def _estimate_kv_cache_bytes( "iq4_nl": 0.5625, }.get(cache_type_kv or "f16", 2.0) - slots = max(1, n_parallel) - # Path 1: MLA (DeepSeek-V2/V3, GLM-4.7, GLM-5, Kimi-K2.5) # MLA stores one compressed KV latent per token/layer (shared across heads). # V is reconstructed from the latent on the fly -- no separate V cache. @@ -1096,7 +572,7 @@ def _estimate_kv_cache_bytes( n_kv_mla = self._n_kv_heads or 1 rope_dim = self._key_length_mla or 64 key_len = self._kv_key_length or (self._kv_lora_rank + rope_dim) - return int(n_layers_kv * n_ctx * n_kv_mla * key_len * bpe) + return int(n_layers * n_ctx * n_kv_mla * key_len * bpe) key_len = self._kv_key_length val_len = self._kv_value_length @@ -1114,19 +590,11 @@ def _estimate_kv_cache_bytes( head_dim = self._embedding_length // self._n_heads if self._n_heads else 128 # type: ignore[operator] return int(n_attn * n_ctx * n_kv * 2 * head_dim * bpe) - # Path 3: Sliding window (Gemma 2/3/3n/4, gpt-oss, Cohere2 ...). - # Pattern is filled in by the resolver at parse time; if absent, - # falls through to the legacy 1/4-global heuristic below. - # Per-layer-type ``--parallel N`` accounting (verified empirically - # against ``llama-server``): - # * non-SWA layers: total cells = n_ctx, partitioned across - # slots -> total memory CONSTANT in slots. - # * SWA layers: per-slot cells = 2 * sliding_window - # (capped at n_ctx and at per_slot_ctx - # when ctx is split among many slots) -> - # total memory grows LINEARLY in slots. - # ``--swa-full`` forces full n_ctx for SWA layers instead. - # ``--ctx-checkpoints N`` adds N snapshots per SWA layer per slot. + # Path 3: Sliding Window (Gemma-3, gpt-oss) + # SWA layers only cache min(ctx, window) tokens; global layers cache full ctx. + # Most SWA architectures use few global layers (e.g., Gemma-3 uses 1 in 6). + # Without an explicit field, we conservatively assume 1/4 of layers are global + # which is still far more accurate than the legacy formula (which ignores SWA). if ( self._sliding_window is not None and self._sliding_window > 0 @@ -1134,72 +602,20 @@ def _estimate_kv_cache_bytes( and val_len is not None ): swa = self._sliding_window - per_slot_ctx = max(1, n_ctx // slots) - # ``--swa-full`` makes SWA layers cache the full context just - # like non-SWA: cells get partitioned across slots, so per-slot - # cells = per_slot_ctx and the slots*per-slot product collapses - # back to the constant ``n_ctx`` total. Otherwise SWA caches - # 2*sliding_window per slot, clamped at the per-slot ctx. - swa_cells_per_slot = ( - per_slot_ctx if swa_full else min(n_ctx, 2 * swa, per_slot_ctx) - ) - key_len_swa = self._kv_key_length_swa or key_len - val_len_swa = self._kv_value_length_swa or val_len - if self._sliding_window_pattern is not None: - global_bytes = 0.0 # constant across slots - swa_bytes_per_slot = 0.0 # multiplied by slots - checkpoint_extra_per_slot = 0.0 - # Iterate only over layers that allocate their own KV; - # the trailing ``shared`` layers reuse earlier caches. - for layer_idx in range(n_layers_kv): - layer_n_kv = self._kv_heads_for_layer(layer_idx, n_kv) - is_swa = ( - layer_idx < len(self._sliding_window_pattern) - and self._sliding_window_pattern[layer_idx] - ) - if is_swa: - swa_bytes_per_slot += ( - swa_cells_per_slot - * layer_n_kv - * (key_len_swa + val_len_swa) - * bpe - ) - if ctx_checkpoints > 0 and not swa_full: - checkpoint_extra_per_slot += ( - ctx_checkpoints - * swa - * layer_n_kv - * (key_len_swa + val_len_swa) - * bpe - ) - else: - global_bytes += n_ctx * layer_n_kv * (key_len + val_len) * bpe - return int( - global_bytes - + slots * (swa_bytes_per_slot + checkpoint_extra_per_slot) - ) - n_global = max(1, n_layers_kv // 4) - n_swa = n_layers_kv - n_global + n_global = max(1, n_layers // 4) + n_swa = n_layers - n_global kv_per_token = n_kv * (key_len + val_len) * bpe - kv_per_token_swa = n_kv * (key_len_swa + val_len_swa) * bpe - global_bytes = n_global * n_ctx * kv_per_token - swa_bytes_per_slot = n_swa * swa_cells_per_slot * kv_per_token_swa - checkpoint_extra_per_slot = ( - ctx_checkpoints * n_swa * swa * kv_per_token_swa - if ctx_checkpoints > 0 and not swa_full - else 0.0 - ) return int( - global_bytes + slots * (swa_bytes_per_slot + checkpoint_extra_per_slot) + n_global * n_ctx * kv_per_token + n_swa * min(n_ctx, swa) * kv_per_token ) # Path 4: Standard GQA with explicit key/value dimensions if key_len is not None and val_len is not None: - return int(n_layers_kv * n_ctx * n_kv * (key_len + val_len) * bpe) + return int(n_layers * n_ctx * n_kv * (key_len + val_len) * bpe) # Path 5: Legacy fallback (old GGUFs without explicit dimensions) head_dim = self._embedding_length // self._n_heads if self._n_heads else 128 # type: ignore[operator] - return int(2 * n_kv * head_dim * n_layers_kv * n_ctx * bpe) + return int(2 * n_kv * head_dim * n_layers * n_ctx * bpe) def _fit_context_to_vram( self, @@ -1208,12 +624,6 @@ def _fit_context_to_vram( model_size_bytes: int, cache_type_kv: Optional[str] = None, min_ctx: int = 4096, - *, - swa_full: bool = False, - n_parallel: int = 1, - kv_unified: bool = True, - ctx_checkpoints: int = 0, - kv_on_gpu: bool = True, ) -> int: """Return the largest context length that fits in GPU VRAM. @@ -1221,11 +631,6 @@ def _fit_context_to_vram( threshold -- 10% reserved for compute buffers, CUDA context, scratch space, flash-attn workspace, etc.). If the model weights alone don't fit, returns min_ctx unchanged. - - ``kv_on_gpu`` mirrors ``--kv-offload`` (default on). When False - the KV cache lives in CPU RAM and doesn't compete with weights - for VRAM; the requested context is honored verbatim. The other - keyword args mirror ``_estimate_kv_cache_bytes``. """ if not self._can_estimate_kv(): logger.debug( @@ -1235,22 +640,11 @@ def _fit_context_to_vram( ) return requested_ctx - # KV lives off-GPU: no VRAM accounting needed for the cache itself. - if not kv_on_gpu: - return requested_ctx - - kv_kwargs = dict( - swa_full = swa_full, - n_parallel = n_parallel, - kv_unified = kv_unified, - ctx_checkpoints = ctx_checkpoints, - ) - budget_bytes = available_mib * 1024 * 1024 * 0.90 model_footprint = model_size_bytes # Check if requested context already fits - kv = self._estimate_kv_cache_bytes(requested_ctx, cache_type_kv, **kv_kwargs) + kv = self._estimate_kv_cache_bytes(requested_ctx, cache_type_kv) if model_footprint + kv <= budget_bytes: return requested_ctx @@ -1272,7 +666,7 @@ def _fit_context_to_vram( best = effective_min while lo <= hi: mid = (lo + hi) // 2 - kv = self._estimate_kv_cache_bytes(mid, cache_type_kv, **kv_kwargs) + kv = self._estimate_kv_cache_bytes(mid, cache_type_kv) if kv <= remaining: best = mid lo = mid + 1 @@ -1405,19 +799,6 @@ def _gguf_skip_value(f, vtype: int) -> None: for _ in range(alen): LlamaCppBackend._gguf_skip_value(f, atype) - @staticmethod - def _gguf_read_array_value(f, atype: int, alen: int) -> Optional[list]: - if atype == 4: # UINT32 - return [struct.unpack(" None: """Read context_length, architecture params, and chat_template from a GGUF header. @@ -1430,50 +811,26 @@ def _read_gguf_metadata(self, gguf_path: str) -> None: self._chat_template = None self._supports_reasoning = False self._reasoning_always_on = False - self._reasoning_style = "enable_thinking" - self._reasoning_default = True - self._supports_preserve_thinking = False self._supports_tools = False self._n_layers = None self._n_kv_heads = None - self._n_kv_heads_by_layer = None self._n_heads = None self._embedding_length = None self._kv_key_length = None self._kv_value_length = None self._sliding_window = None - self._sliding_window_pattern = None self._full_attention_interval = None self._kv_lora_rank = None self._key_length_mla = None - self._kv_key_length_swa = None - self._kv_value_length_swa = None self._ssm_inner_size = None self._ssm_state_size = None - self._shared_kv_layers = None try: - WANTED = { - "general.architecture", - "tokenizer.chat_template", - # Source-repo hints for the SWA resolver's HF fallback. - "general.source.huggingface.repository", - "general.source.url", - "general.source.repo_url", - "general.base_model.0.repo_url", - "general.base_model.0.organization", - "general.base_model.0.name", - "general.basename", - "general.organization", - "general.size_label", - "general.finetune", - } + WANTED = {"general.architecture", "tokenizer.chat_template"} # Additional arch-specific keys are added dynamically once # we know the architecture name. arch_keys: dict[str, str] = {} # gguf_key -> attribute name arch = None - sliding_window_pattern_period: Optional[int] = None - general: dict[str, str] = {} with open(gguf_path, "rb") as f: magic = struct.unpack(" None: _tensor_count, kv_count = struct.unpack(" None: f"GGUF metadata: chat_template={len(self._chat_template)} chars" ) # Detect thinking/reasoning support from chat template - flags = detect_reasoning_flags( - self._chat_template, - self._model_identifier, - log_source = "GGUF metadata", - ) - self._supports_reasoning = flags["supports_reasoning"] - self._reasoning_style = flags["reasoning_style"] - self._reasoning_always_on = flags["reasoning_always_on"] - self._supports_preserve_thinking = flags["supports_preserve_thinking"] - self._supports_tools = flags["supports_tools"] + tpl = self._chat_template + if "enable_thinking" in tpl: + self._supports_reasoning = True + logger.info( + "GGUF metadata: model supports reasoning (enable_thinking)" + ) + elif "thinking" in tpl: + # DeepSeek uses 'thinking' instead of 'enable_thinking' + normalized_id = (self._model_identifier or "").lower() + if "deepseek" in normalized_id: + self._supports_reasoning = True + logger.info( + "GGUF metadata: model supports reasoning (DeepSeek thinking)" + ) + # Models with hardcoded tags or reasoning_content + # in their chat template always produce thinking output + # (no toggle to disable it). + if not self._supports_reasoning: + if ( + "" in tpl + and "" in tpl + or "reasoning_content" in tpl + ): + self._supports_reasoning = True + self._reasoning_always_on = True + logger.info( + "GGUF metadata: model always reasons ( tags in template)" + ) + # Detect tool calling support from chat template + tool_markers = [ + "{%- if tools %}", + "{%- if tools -%}", + "{% if tools %}", + "{% if tools -%}", + '"role" == "tool"', + "'role' == 'tool'", + 'message.role == "tool"', + "message.role == 'tool'", + ] + if any(marker in tpl for marker in tool_markers): + self._supports_tools = True + logger.info("GGUF metadata: model supports tool calling") except Exception as e: logger.warning(f"Failed to read GGUF metadata: {e}") @@ -1853,7 +1150,7 @@ def _download_mmproj( # Prefer F16 variant target = None for f in mmproj_files: - if f.lower().endswith("-f16.gguf"): + if "f16" in f.lower(): target = f break if target is None: @@ -1893,7 +1190,6 @@ def load_model( n_threads: Optional[int] = None, n_gpu_layers: Optional[int] = None, # Accepted for caller compat, unused n_parallel: int = 1, - extra_args: Optional[List[str]] = None, ) -> bool: """ Start llama-server with a GGUF model. @@ -2016,11 +1312,8 @@ def load_model( pool_mib, model_size, cache_type_kv, - n_parallel = n_parallel, - ) - kv = self._estimate_kv_cache_bytes( - capped, cache_type_kv, n_parallel = n_parallel ) + kv = self._estimate_kv_cache_bytes(capped, cache_type_kv) total_mib = (model_size + kv) / (1024 * 1024) if total_mib <= pool_mib * 0.90: best_cap = max(best_cap, capped) @@ -2044,7 +1337,7 @@ def load_model( # have surfaced the "might be slower" warning before # the user submitted a ctx above the fit ceiling. requested_total = model_size + self._estimate_kv_cache_bytes( - effective_ctx, cache_type_kv, n_parallel = n_parallel + effective_ctx, cache_type_kv ) gpu_indices, use_fit = self._select_gpus(requested_total, gpus) # No silent shrink: effective_ctx stays == n_ctx. @@ -2059,11 +1352,8 @@ def load_model( pool_mib, model_size, cache_type_kv, - n_parallel = n_parallel, - ) - kv = self._estimate_kv_cache_bytes( - capped, cache_type_kv, n_parallel = n_parallel ) + kv = self._estimate_kv_cache_bytes(capped, cache_type_kv) total_mib = (model_size + kv) / (1024 * 1024) if total_mib <= pool_mib * 0.90: effective_ctx = capped @@ -2096,9 +1386,7 @@ def load_model( ) if effective_ctx < original_ctx: - kv_est = self._estimate_kv_cache_bytes( - effective_ctx, cache_type_kv, n_parallel = n_parallel - ) + kv_est = self._estimate_kv_cache_bytes(effective_ctx, cache_type_kv) logger.info( f"Context auto-reduced: {original_ctx} -> {effective_ctx} " f"(model: {model_size / (1024**3):.1f} GB, " @@ -2106,7 +1394,7 @@ def load_model( ) kv_cache_bytes = self._estimate_kv_cache_bytes( - effective_ctx, cache_type_kv, n_parallel = n_parallel + effective_ctx, cache_type_kv ) logger.info( f"GGUF size: {model_size / (1024**3):.1f} GB, " @@ -2141,10 +1429,8 @@ def load_model( # Model fits on selected GPU(s) -- offload all layers cmd.extend(["-ngl", "-1"]) - # -1 = llama.cpp auto-detect (physical cores). Pass explicitly so we - # do not inherit llama-server's internal default, which has historically - # varied (hardware concurrency incl. hyperthreads on some builds). - cmd.extend(["--threads", str(n_threads if n_threads is not None else -1)]) + if n_threads is not None: + cmd.extend(["--threads", str(n_threads)]) # Always enable Jinja chat template rendering for proper template support cmd.extend(["--jinja"]) @@ -2176,7 +1462,7 @@ def load_model( # existing text (code refactoring, summarization, reasoning). # For general chat with low repetition, overhead is ~5 ms. # - # Benchmarks from upstream llama.cpp speculative-decoding PRs: + # Benchmarks from llama.cpp PRs #18471, #19164: # Scenario | Without | With | Speedup # gpt-oss-120b code refactor | 181 t/s | 446 t/s | 2.5x # Qwen3-235B offloaded | 12 t/s | 21 t/s | 1.8x @@ -2189,21 +1475,11 @@ def load_model( # ref: https://github.com/ggml-org/llama.cpp/blob/master/docs/speculative.md # ref: https://github.com/ggml-org/llama.cpp/pull/19164 # ref: https://github.com/ggml-org/llama.cpp/pull/18471 - # ``"default"`` -> let llama-server pick a sensible spec - # config via ``--spec-default``. Explicit type names are - # passed through with the manual draft tuning we've shipped - # historically so power users keep their overrides. _valid_spec_types = {"ngram-simple", "ngram-mod"} - normalized_spec = ( - speculative_type.lower().strip() if speculative_type else None - ) - if normalized_spec and normalized_spec != "off" and not is_vision: - if normalized_spec == "default": - cmd.append("--spec-default") - self._speculative_type = "default" - elif normalized_spec in _valid_spec_types: - cmd.extend(["--spec-type", normalized_spec]) - if normalized_spec == "ngram-mod": + if speculative_type and speculative_type in _valid_spec_types: + if not is_vision: # spec decoding disabled for vision models + cmd.extend(["--spec-type", speculative_type]) + if speculative_type == "ngram-mod": cmd.extend( [ "--spec-ngram-size-n", @@ -2214,7 +1490,7 @@ def load_model( "64", ] ) - self._speculative_type = normalized_spec + self._speculative_type = speculative_type else: self._speculative_type = None else: @@ -2224,18 +1500,6 @@ def load_model( if chat_template_override: import tempfile - self._chat_template = chat_template_override - flags = detect_reasoning_flags( - self._chat_template, - self._model_identifier, - log_source = "GGUF chat template override", - ) - self._supports_reasoning = flags["supports_reasoning"] - self._reasoning_style = flags["reasoning_style"] - self._reasoning_always_on = flags["reasoning_always_on"] - self._supports_preserve_thinking = flags["supports_preserve_thinking"] - self._supports_tools = flags["supports_tools"] - self._chat_template_file = tempfile.NamedTemporaryFile( mode = "w", suffix = ".jinja", @@ -2252,8 +1516,7 @@ def load_model( # For reasoning models, set default thinking mode. # Qwen3.5/3.6 models below 9B (0.8B, 2B, 4B) disable thinking by default. # Only 9B and larger enable thinking. - # Always-on templates ignore the kwarg entirely, so skip. - if self._supports_reasoning and not self._reasoning_always_on: + if self._supports_reasoning: thinking_default = True mid = (model_identifier or "").lower() if "qwen3.5" in mid or "qwen3.6" in mid: @@ -2261,14 +1524,15 @@ def load_model( if size_val is not None and size_val < 9: thinking_default = False self._reasoning_default = thinking_default - reasoning_kw = self._reasoning_kwargs(thinking_default) cmd.extend( [ "--chat-template-kwargs", - json.dumps(reasoning_kw), + json.dumps({"enable_thinking": thinking_default}), ] ) - logger.info(f"Reasoning model: {reasoning_kw} by default") + logger.info( + f"Reasoning model: enable_thinking={thinking_default} by default" + ) if mmproj_path: if not Path(mmproj_path).is_file(): @@ -2288,17 +1552,6 @@ def load_model( else: self._api_key = None - # User-supplied pass-through args go last so llama.cpp's - # last-wins flag parsing lets the user override Studio's - # auto-set tier-2 flags (e.g. --cache-type-k, --spec-type). - # The route layer has already validated this list against - # the managed-flag denylist via validate_extra_args(). - if extra_args: - cmd.extend(str(a) for a in extra_args) - logger.info( - f"Appending user extra args to llama-server: {list(extra_args)}" - ) - _log_cmd = list(cmd) if "--api-key" in _log_cmd: _ki = _log_cmd.index("--api-key") + 1 @@ -2310,7 +1563,7 @@ def load_model( import os import sys - env = child_env_without_native_path_secret() + env = os.environ.copy() binary_dir = str(Path(binary).parent) if sys.platform == "win32": @@ -2394,29 +1647,9 @@ def load_model( f"{new_ld}:{existing_ld}" if existing_ld else new_ld ) - # Pin to selected GPU(s). On ROCm, llama-server (and any torch - # in the subprocess) honors HIP_VISIBLE_DEVICES / ROCR_VISIBLE_DEVICES; - # narrowing only CUDA_VISIBLE_DEVICES leaves an AMD child seeing - # the full HIP/ROCR set the parent inherited. + # Pin to selected GPU(s) via CUDA_VISIBLE_DEVICES if gpu_indices is not None: - pinned = ",".join(str(i) for i in gpu_indices) - env["CUDA_VISIBLE_DEVICES"] = pinned - try: - import torch as _torch - - if getattr(_torch.version, "hip", None) is not None: - env["HIP_VISIBLE_DEVICES"] = pinned - env["ROCR_VISIBLE_DEVICES"] = pinned - except Exception as e: - logger.debug( - "Failed to set ROCm visibility env vars for child: %s", e - ) - - # Defensive kill: if a concurrent load slipped past Phase 1 - # (because its `self._process` was None at the time) and - # already stored a Popen handle here, drop that orphan - # before we overwrite the reference. See issue #5161. - self._kill_process() + env["CUDA_VISIBLE_DEVICES"] = ",".join(str(i) for i in gpu_indices) self._stdout_lines = [] self._process = subprocess.Popen( @@ -2425,7 +1658,6 @@ def load_model( stderr = subprocess.STDOUT, text = True, env = env, - **_windows_hidden_subprocess_kwargs(), ) # Start background thread to drain stdout and prevent pipe deadlock @@ -2527,29 +1759,21 @@ def unload_model(self) -> bool: self._chat_template = None self._supports_reasoning = False self._reasoning_always_on = False - self._reasoning_style = "enable_thinking" - self._reasoning_default = True - self._supports_preserve_thinking = False self._supports_tools = False self._cache_type_kv = None self._speculative_type = None self._n_layers = None self._n_kv_heads = None - self._n_kv_heads_by_layer = None self._n_heads = None self._embedding_length = None self._kv_key_length = None self._kv_value_length = None self._sliding_window = None - self._sliding_window_pattern = None self._full_attention_interval = None self._kv_lora_rank = None self._key_length_mla = None - self._kv_key_length_swa = None - self._kv_value_length_swa = None self._ssm_inner_size = None self._ssm_state_size = None - self._shared_kv_layers = None # Clean up temp chat template file if hasattr(self, "_chat_template_file") and self._chat_template_file: try: @@ -2616,27 +1840,8 @@ def _kill_orphaned_servers(): # (binary must be *under* one of these) install_roots: list[Path] = [] - # Env-mode custom root (mirrors _find_llama_server_binary). - _is_custom_root = False - try: - from utils.paths.storage_roots import studio_root as _sr # noqa: WPS433 - - _resolved_sr = _sr() - _legacy_studio = Path.home() / ".unsloth" / "studio" - try: - _is_custom_root = _resolved_sr.resolve() != _legacy_studio.resolve() - except (OSError, ValueError): - _is_custom_root = _resolved_sr != _legacy_studio - if _is_custom_root: - install_roots.append(_resolved_sr / "llama.cpp") - except (ImportError, OSError, ValueError): - pass - - # Primary install dir (default mode only). Env-mode skips this so - # a custom-root Studio cannot kill a concurrent default-install - # Studio's llama-server (same OS user, different install). - if not _is_custom_root: - install_roots.append(Path.home() / ".unsloth" / "llama.cpp") + # Primary install dir (setup.sh / prebuilt installer) + install_roots.append(Path.home() / ".unsloth" / "llama.cpp") # Legacy in-tree build dirs (older setup.sh versions) project_root = Path(__file__).resolve().parents[4] @@ -2724,7 +1929,6 @@ def _kill_orphaned_servers(): capture_output = True, text = True, timeout = 5, - env = child_env_without_native_path_secret(), ) if result.returncode != 0: return @@ -3105,8 +2309,6 @@ def generate_chat_completion( stop: Optional[list[str]] = None, cancel_event: Optional[threading.Event] = None, enable_thinking: Optional[bool] = None, - reasoning_effort: Optional[str] = None, - preserve_thinking: Optional[bool] = None, ) -> Generator[str | dict, None, None]: """ Send a chat completion request to llama-server and stream tokens back. @@ -3131,21 +2333,11 @@ def generate_chat_completion( "repeat_penalty": repetition_penalty, "presence_penalty": presence_penalty, } - # Pass enable_thinking / reasoning_effort / preserve_thinking per-request - _reasoning_kw = self._request_reasoning_kwargs( - enable_thinking, reasoning_effort, preserve_thinking - ) - if _reasoning_kw is not None: - payload["chat_template_kwargs"] = _reasoning_kw - # Default cap to the model's effective context length when known, - # otherwise the conservative floor. The wall-clock backstop below - # keeps a stuck model from running indefinitely either way. - payload["max_tokens"] = ( - max_tokens - if max_tokens is not None - else (self._effective_context_length or _DEFAULT_MAX_TOKENS_FLOOR) - ) - payload["t_max_predict_ms"] = _DEFAULT_T_MAX_PREDICT_MS + # Pass enable_thinking per-request for reasoning models + if self._supports_reasoning and enable_thinking is not None: + payload["chat_template_kwargs"] = {"enable_thinking": enable_thinking} + if max_tokens is not None: + payload["max_tokens"] = max_tokens if stop: payload["stop"] = stop payload["stream_options"] = {"include_usage": True} @@ -3165,9 +2357,7 @@ def generate_chat_completion( _auth_headers = ( {"Authorization": f"Bearer {self._api_key}"} if self._api_key else None ) - with httpx.Client( - timeout = stream_timeout, limits = httpx.Limits(max_keepalive_connections = 0) - ) as client: + with httpx.Client(timeout = stream_timeout) as client: with self._stream_with_retry( client, url, @@ -3281,8 +2471,6 @@ def generate_chat_completion_with_tools( stop: Optional[list[str]] = None, cancel_event: Optional[threading.Event] = None, enable_thinking: Optional[bool] = None, - reasoning_effort: Optional[str] = None, - preserve_thinking: Optional[bool] = None, max_tool_iterations: int = 25, auto_heal_tool_calls: bool = True, tool_call_timeout: int = 300, @@ -3361,17 +2549,10 @@ def _strip_tool_markup(text: str, *, final: bool = False) -> str: "tools": tools, "tool_choice": "auto", } - _reasoning_kw = self._request_reasoning_kwargs( - enable_thinking, reasoning_effort, preserve_thinking - ) - if _reasoning_kw is not None: - payload["chat_template_kwargs"] = _reasoning_kw - payload["max_tokens"] = ( - max_tokens - if max_tokens is not None - else (self._effective_context_length or _DEFAULT_MAX_TOKENS_FLOOR) - ) - payload["t_max_predict_ms"] = _DEFAULT_T_MAX_PREDICT_MS + if self._supports_reasoning and enable_thinking is not None: + payload["chat_template_kwargs"] = {"enable_thinking": enable_thinking} + if max_tokens is not None: + payload["max_tokens"] = max_tokens if stop: payload["stop"] = stop @@ -3410,10 +2591,7 @@ def _strip_tool_markup(text: str, *, final: bool = False) -> str: write = 10, pool = 10, ) - with httpx.Client( - timeout = stream_timeout, - limits = httpx.Limits(max_keepalive_connections = 0), - ) as client: + with httpx.Client(timeout = stream_timeout) as client: with self._stream_with_retry( client, url, @@ -4021,17 +3199,12 @@ def _strip_tool_markup(text: str, *, final: bool = False) -> str: "repeat_penalty": repetition_penalty, "presence_penalty": presence_penalty, } - _reasoning_kw = self._request_reasoning_kwargs( - enable_thinking, reasoning_effort, preserve_thinking - ) - if _reasoning_kw is not None: - stream_payload["chat_template_kwargs"] = _reasoning_kw - stream_payload["max_tokens"] = ( - max_tokens - if max_tokens is not None - else (self._effective_context_length or _DEFAULT_MAX_TOKENS_FLOOR) - ) - stream_payload["t_max_predict_ms"] = _DEFAULT_T_MAX_PREDICT_MS + if self._supports_reasoning and enable_thinking is not None: + stream_payload["chat_template_kwargs"] = { + "enable_thinking": enable_thinking + } + if max_tokens is not None: + stream_payload["max_tokens"] = max_tokens if stop: stream_payload["stop"] = stop stream_payload["stream_options"] = {"include_usage": True} @@ -4050,9 +3223,7 @@ def _strip_tool_markup(text: str, *, final: bool = False) -> str: _auth_headers = ( {"Authorization": f"Bearer {self._api_key}"} if self._api_key else None ) - with httpx.Client( - timeout = stream_timeout, limits = httpx.Limits(max_keepalive_connections = 0) - ) as client: + with httpx.Client(timeout = stream_timeout) as client: with self._stream_with_retry( client, url, diff --git a/studio/backend/core/inference/llama_server_args.py b/studio/backend/core/inference/llama_server_args.py deleted file mode 100644 index 44c7d542c7..0000000000 --- a/studio/backend/core/inference/llama_server_args.py +++ /dev/null @@ -1,120 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 - -"""Validator for user-supplied llama-server pass-through args. - -Studio runs llama-server as a managed subprocess and lets callers pass -extra flags directly (CLI: ``unsloth run ... --top-k 20``; HTTP: -``LoadRequest.llama_extra_args``). This module is the boundary that -rejects only flags Studio fundamentally cannot share with the user -- -model identity, the auth key, and the network endpoint Studio's HTTP -proxy targets. Anything else passes through. - -User-supplied args are appended to ``cmd`` after Studio's auto-set -flags, so llama.cpp's last-wins CLI parsing makes the user's value -override the auto-set one. That covers tunable knobs the user might -reasonably want to override -- ``-c``/``--ctx-size``, -``-np``/``--parallel``, ``-fa``/``--flash-attn``, -``-ngl``/``--gpu-layers``, ``-t``/``--threads``, ``-fit``/``--fit*``, -``--cache-type-k/v``, ``--chat-template-file/-kwargs``, -``--spec-*``, ``--jinja``/``--no-jinja``, -``--no-context-shift``/``--context-shift``, sampling params, etc. - -Reference: https://github.com/ggml-org/llama.cpp/blob/master/tools/server/README.md -""" - -from __future__ import annotations - -from typing import Iterable, Optional - -# Each group is the full set of aliases (short + long) for one -# hard-denied flag, taken from the llama-server README. If llama.cpp -# adds a new alias for an existing denied flag, extend the relevant -# group. -# -# Flags NOT in this list (e.g. -c, --parallel, --flash-attn, -ngl, -# -t/--threads, --jinja, --no-context-shift, --fit*, --cache-type-*, -# --chat-template-*, --spec-*) pass through and override Studio's -# auto-set version via llama.cpp's last-wins CLI parsing. -_DENYLIST_GROUPS: tuple[frozenset[str], ...] = ( - # Model identity -- Studio resolves the model from LoadRequest and - # passes -m / mmproj after downloading from HF if needed. A second - # -m would point at a different model than the one Studio thinks - # is loaded. - frozenset({"-m", "--model"}), - frozenset({"-mu", "--model-url"}), - frozenset({"-dr", "--docker-repo"}), - frozenset({"-hf", "-hfr", "--hf-repo"}), - frozenset({"-hff", "--hf-file"}), - frozenset({"-hfv", "-hfrv", "--hf-repo-v"}), - frozenset({"-hffv", "--hf-file-v"}), - frozenset({"-hft", "--hf-token"}), - frozenset({"-mm", "--mmproj"}), - frozenset({"-mmu", "--mmproj-url"}), - # Networking -- Studio binds llama-server's port and reverse-proxies - # HTTP traffic to it. Retargeting host/port/path/prefix would - # orphan Studio's proxy and the UI would lose the server. - frozenset({"--host"}), - frozenset({"--port"}), - frozenset({"--path"}), - frozenset({"--api-prefix"}), - frozenset({"--reuse-port"}), - # Auth / TLS -- Studio terminates auth at its own layer; an - # upstream --api-key would shadow Studio's UNSLOTH_DIRECT_STREAM - # key, and TLS on llama-server would break the local proxy hop. - frozenset({"--api-key"}), - frozenset({"--api-key-file"}), - frozenset({"--ssl-key-file"}), - frozenset({"--ssl-cert-file"}), - # Single-model server -- Studio runs one model per llama-server - # process and serves its own UI. Enabling multi-model loading or - # llama-server's built-in web UI changes the surface clients see. - frozenset({"--webui", "--no-webui"}), - frozenset({"--models-dir"}), - frozenset({"--models-preset"}), - frozenset({"--models-max"}), - frozenset({"--models-autoload", "--no-models-autoload"}), -) - -_DENYLIST: frozenset[str] = frozenset().union(*_DENYLIST_GROUPS) - - -def _flag_name(token: str) -> Optional[str]: - """Return the flag name for a token, or None if it isn't a flag. - - Peels ``--key=value`` to the bare ``--key``. Plain numeric values - like ``-1`` or ``-0.5`` (e.g. ``--seed -1``) are values, not flags; - llama-server short-form flags always start with a letter. - """ - if not token.startswith("-") or token in {"-", "--"}: - return None - if len(token) >= 2 and (token[1].isdigit() or token[1] == "."): - return None - return token.split("=", 1)[0] - - -def validate_extra_args(args: Optional[Iterable[str]]) -> list[str]: - """Validate user-supplied llama-server args. - - Returns the args as a flat list ready to extend the llama-server - command. Raises ``ValueError`` (with the offending flag in the - message) the moment a token resolves to a Studio-managed flag. - """ - if not args: - return [] - out: list[str] = [] - for raw in args: - token = str(raw) - flag = _flag_name(token) - if flag is not None and flag in _DENYLIST: - raise ValueError( - f"llama-server flag '{flag}' is managed by Unsloth Studio " - f"and cannot be passed as an extra arg" - ) - out.append(token) - return out - - -def is_managed_flag(flag: str) -> bool: - """True if ``flag`` is a Studio-managed llama-server flag.""" - return flag in _DENYLIST diff --git a/studio/backend/core/inference/mlx_inference.py b/studio/backend/core/inference/mlx_inference.py deleted file mode 100644 index 1d2b03ecb9..0000000000 --- a/studio/backend/core/inference/mlx_inference.py +++ /dev/null @@ -1,395 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -"""MLX inference backend for Apple Silicon. - -Drop-in replacement for InferenceBackend — same interface, uses mlx-lm/mlx-vlm -instead of torch/transformers for model loading and generation. -""" - -import threading -from typing import Optional, Generator -from loggers import get_logger - -logger = get_logger(__name__) - - -class MLXInferenceBackend: - def __init__(self): - self.models = {} - self.active_model_name = None - self.loading_models = set() - self.loaded_local_models = [] - self.device = "mlx" - self._generation_lock = threading.Lock() - - # MLX state - self._model = None - self._tokenizer = None - self._processor = None - self._is_vlm = False - self._config = {} - - # Recorded for unload to release pinned memory back to the OS. - self._memory_limits_applied = {} - - def _configure_memory_limits(self): - """Apply Metal memory caps before loading a model. - - Mirrors MLXTrainer._configure_memory_limits's defaults: - memory_limit = 85% of recommended working-set, - wired_limit = min(recommended, memory_limit). Recorded so unload - can lower wired_limit back to release pinned RAM. - """ - import mlx.core as mx - - if not mx.metal.is_available(): - return - info = mx.device_info() - rec_bytes = info.get("max_recommended_working_set_size") - if not rec_bytes or rec_bytes <= 0: - return - rec_gb = rec_bytes / 1e9 - memory_limit_gb = rec_gb * 0.85 - wired_limit_gb = min(rec_gb, memory_limit_gb) - mx.set_memory_limit(int(memory_limit_gb * 1e9)) - mx.set_wired_limit(int(wired_limit_gb * 1e9)) - self._memory_limits_applied = { - "memory_limit_gb": memory_limit_gb, - "wired_limit_gb": wired_limit_gb, - "recommended_gb": rec_gb, - } - logger.info( - "MLX memory caps: memory_limit=%.2f GB, wired_limit=%.2f GB", - memory_limit_gb, - wired_limit_gb, - ) - - def load_model( - self, - config, - max_seq_length = 2048, - load_in_4bit = True, - hf_token = None, - trust_remote_code = False, - gpu_ids = None, - dtype = None, - ) -> bool: - import mlx.core as mx - - model_name = config.identifier if hasattr(config, "identifier") else str(config) - is_vision = getattr(config, "is_vision", False) - - if hf_token: - import os - - os.environ["HF_TOKEN"] = hf_token - self._configure_memory_limits() - - is_lora = getattr(config, "is_lora", False) - - logger.info( - "Loading %s via %s (is_lora=%s)", - model_name, - "mlx-vlm" if is_vision else "mlx-lm", - is_lora, - ) - - try: - from unsloth_zoo.mlx_loader import FastMLXModel - except ImportError as e: - raise ImportError( - "Unsloth: MLX inference requires unsloth-zoo with the MLX modules " - "(unsloth_zoo.mlx_loader). Reinstall via install.sh on Apple Silicon." - ) from e - - model, tokenizer_or_processor = FastMLXModel.from_pretrained( - model_name, - max_seq_length = max_seq_length, - dtype = dtype, - load_in_4bit = load_in_4bit, - token = hf_token, - trust_remote_code = trust_remote_code, - text_only = False if is_vision else True, - ) - - if is_vision: - processor = tokenizer_or_processor - self._model = model - self._processor = processor - self._tokenizer = getattr(processor, "tokenizer", processor) - self._is_vlm = True - else: - tokenizer = tokenizer_or_processor - self._model = model - self._tokenizer = tokenizer - self._processor = None - self._is_vlm = False - - self.active_model_name = model_name - self.models[model_name] = { - "model": self._model, - "tokenizer": self._tokenizer, - "processor": self._processor, - "is_vision": is_vision, - "is_lora": getattr(config, "is_lora", False), - "is_audio": False, - "audio_type": None, - "has_audio_input": False, - } - - logger.info("Model %s loaded successfully", model_name) - return True - - def unload_model(self, model_name: str) -> bool: - import mlx.core as mx - import gc - - if model_name in self.models: - del self.models[model_name] - self._model = None - self._tokenizer = None - self._processor = None - if self.active_model_name == model_name: - self.active_model_name = None - gc.collect() - mx.clear_cache() - - if mx.metal.is_available() and self._memory_limits_applied and not self.models: - try: - mx.set_wired_limit(0) - logger.info("MLX wired_limit released back to OS on unload") - except Exception as e: - logger.warning("Failed to release wired_limit: %s", e) - self._memory_limits_applied = {} - logger.info("Model %s unloaded", model_name) - return True - - def generate_chat_response( - self, - messages, - system_prompt = "", - image = None, - temperature = 0.7, - top_p = 0.9, - top_k = 40, - min_p = 0.0, - max_new_tokens = 256, - repetition_penalty = 1.0, - cancel_event = None, - ) -> Generator[str, None, None]: - if self._model is None: - raise RuntimeError("No model loaded") - - # Build messages with system prompt - full_messages = [] - if system_prompt: - full_messages.append({"role": "system", "content": system_prompt}) - full_messages.extend(messages) - - # Inject image into the last user message for VLM - if self._is_vlm and image is not None: - for msg in reversed(full_messages): - if msg.get("role") == "user": - content = msg.get("content", "") - if isinstance(content, str): - msg["content"] = [ - {"type": "image"}, - {"type": "text", "text": content}, - ] - elif isinstance(content, list): - # Prepend image if not already there - has_image = any( - p.get("type") == "image" - for p in content - if isinstance(p, dict) - ) - if not has_image: - content.insert(0, {"type": "image"}) - break - - if self._is_vlm: - yield from self._generate_vlm( - full_messages, - image, - temperature, - top_p, - top_k, - min_p, - max_new_tokens, - repetition_penalty, - cancel_event, - ) - else: - yield from self._generate_text( - full_messages, - temperature, - top_p, - top_k, - min_p, - max_new_tokens, - repetition_penalty, - cancel_event, - ) - - def _generate_text( - self, - messages, - temperature, - top_p, - top_k, - min_p, - max_new_tokens, - repetition_penalty, - cancel_event, - ): - from mlx_lm import stream_generate - from mlx_lm.sample_utils import make_sampler, make_logits_processors - - prompt = self._tokenizer.apply_chat_template( - messages, - tokenize = False, - add_generation_prompt = True, - ) - if prompt is None: - raise RuntimeError( - "apply_chat_template returned None — tokenizer may be incompatible" - ) - - sampler = make_sampler( - temp = temperature, - top_p = top_p, - top_k = int(top_k or 0), - min_p = float(min_p or 0.0), - min_tokens_to_keep = 1, - ) - # Only build a logits processor when we actually have a non-trivial - # repetition penalty (1.0 is the no-op value). - logits_processors = None - if repetition_penalty is not None and float(repetition_penalty) not in ( - 0.0, - 1.0, - ): - logits_processors = make_logits_processors( - repetition_penalty = float(repetition_penalty), - ) - - token_ids = [] - logger.info( - "Generating: prompt_len=%d, max_tokens=%d, model=%s, tokenizer=%s", - len(prompt), - max_new_tokens, - type(self._model).__name__, - type(self._tokenizer).__name__, - ) - with self._generation_lock: - try: - gen_kwargs = dict( - prompt = prompt, - max_tokens = max_new_tokens, - sampler = sampler, - ) - if logits_processors is not None: - gen_kwargs["logits_processors"] = logits_processors - for response in stream_generate( - self._model, - self._tokenizer, - **gen_kwargs, - ): - token_ids.append(response.token) - # Decode full sequence with skip_special_tokens — same as GPU - cumulative = self._tokenizer.decode( - token_ids, - skip_special_tokens = True, - ) - yield cumulative - - if cancel_event and cancel_event.is_set(): - break - except Exception as e: - import traceback - - logger.error("stream_generate failed:\n%s", traceback.format_exc()) - raise - - def _generate_vlm( - self, - messages, - image, - temperature, - top_p, - top_k, - min_p, - max_new_tokens, - repetition_penalty, - cancel_event, - ): - from mlx_vlm import stream_generate as vlm_stream - - # Apply chat template - chat_fn = getattr(self._processor, "apply_chat_template", None) - if ( - chat_fn is None - or not hasattr(self._processor, "chat_template") - or self._processor.chat_template is None - ): - tok = getattr(self._processor, "tokenizer", self._processor) - chat_fn = tok.apply_chat_template - - prompt = chat_fn(messages, tokenize = False, add_generation_prompt = True) - - # For VLM: always use mlx_vlm's stream_generate which handles - # pixel_values properly (passes None for text-only, image for VLM) - images = [image] if image is not None else None - - cumulative = "" - logger.info( - "VLM generating: prompt_len=%d, has_image=%s", - len(prompt), - image is not None, - ) - # mlx_vlm.stream_generate forwards **kwargs into generate_step, which - # accepts temp/top_p/top_k/repetition_penalty (and builds the sampler - # + logits_processors internally). Pass them through. - # NOTE: mlx_vlm.generate_step expects ``temperature=`` (long form) — - # passing ``temp=`` silently falls into **kwargs and is ignored, - # leaving generation stuck at the default 0.0 (greedy). - vlm_kwargs = dict( - max_tokens = max_new_tokens, - temperature = temperature, - top_p = top_p, - top_k = int(top_k or 0), - min_p = float(min_p or 0.0), - ) - if repetition_penalty is not None and float(repetition_penalty) not in ( - 0.0, - 1.0, - ): - vlm_kwargs["repetition_penalty"] = float(repetition_penalty) - - with self._generation_lock: - for response in vlm_stream( - self._model, - self._processor, - prompt, - images, - **vlm_kwargs, - ): - token_text = ( - response.text if hasattr(response, "text") else str(response) - ) - cumulative += token_text - yield cumulative - if cancel_event and cancel_event.is_set(): - break - - def generate_with_adapter_control( - self, use_adapter = None, cancel_event = None, **gen_kwargs - ) -> Generator[str, None, None]: - # MLX LoRA adapter toggling not yet supported — generate normally - yield from self.generate_chat_response(cancel_event = cancel_event, **gen_kwargs) - - def reset_generation_state(self): - import mlx.core as mx - import gc - - gc.collect() - mx.clear_cache() diff --git a/studio/backend/core/inference/orchestrator.py b/studio/backend/core/inference/orchestrator.py index 5562820f49..cb5d9da34a 100644 --- a/studio/backend/core/inference/orchestrator.py +++ b/studio/backend/core/inference/orchestrator.py @@ -166,30 +166,23 @@ def _fetch_top_models(self) -> None: def _spawn_subprocess(self, config: dict) -> None: """Spawn a new inference subprocess.""" - from utils.native_path_leases import ( - native_path_secret_removed_for_child_start, - run_without_native_path_secret, - ) - from .worker import run_inference_process - with native_path_secret_removed_for_child_start(): - self._cmd_queue = _CTX.Queue() - self._resp_queue = _CTX.Queue() - self._cancel_event = _CTX.Event() - - self._proc = _CTX.Process( - target = run_without_native_path_secret, - args = (run_inference_process,), - kwargs = { - "cmd_queue": self._cmd_queue, - "resp_queue": self._resp_queue, - "cancel_event": self._cancel_event, - "config": config, - }, - daemon = True, - ) - self._proc.start() + self._cmd_queue = _CTX.Queue() + self._resp_queue = _CTX.Queue() + self._cancel_event = _CTX.Event() + + self._proc = _CTX.Process( + target = run_inference_process, + kwargs = { + "cmd_queue": self._cmd_queue, + "resp_queue": self._resp_queue, + "cancel_event": self._cancel_event, + "config": config, + }, + daemon = True, + ) + self._proc.start() logger.info("Inference subprocess started (pid=%s)", self._proc.pid) def _cancel_generation(self) -> None: @@ -715,17 +708,6 @@ def load_model( def unload_model(self, model_name: str) -> bool: """Unload a model from the subprocess.""" - if model_name in self.loading_models: - logger.info( - "Cancelling in-flight load for model '%s' by terminating subprocess", - model_name, - ) - self._shutdown_subprocess(timeout = 0.5) - self.loading_models.discard(model_name) - self.active_model_name = None - self.models.clear() - return True - if not self._ensure_subprocess_alive(): # No subprocess — just clear local state self.models.pop(model_name, None) diff --git a/studio/backend/core/inference/worker.py b/studio/backend/core/inference/worker.py index 085a1ab899..fbcce276ba 100644 --- a/studio/backend/core/inference/worker.py +++ b/studio/backend/core/inference/worker.py @@ -663,98 +663,6 @@ def run_inference_process( model_name = config["model_name"] - # ── 0. MLX fast-path — skip torch/transformers entirely ── - backend_path = str(Path(__file__).resolve().parent.parent.parent) - if backend_path not in sys.path: - sys.path.insert(0, backend_path) - - from utils.hardware import hardware as _hw - - _hw.detect_hardware() - if _hw.DEVICE == _hw.DeviceType.MLX: - try: - _activate_transformers_version(model_name) - except Exception: - pass - try: - from core.inference.mlx_inference import MLXInferenceBackend - - backend = MLXInferenceBackend() - _send_response( - resp_queue, - {"type": "status", "message": "Loading model...", "ts": time.time()}, - ) - _handle_load(backend, config, resp_queue) - except Exception as exc: - _send_response( - resp_queue, - { - "type": "error", - "error": f"MLX inference init failed: {exc}", - "stack": traceback.format_exc(limit = 20), - "ts": time.time(), - }, - ) - return - - # Enter same command loop as GPU path - logger.info("MLX inference subprocess ready, entering command loop") - while True: - try: - cmd = cmd_queue.get(timeout = 1.0) - except _queue.Empty: - continue - except (EOFError, OSError): - return - if cmd is None: - continue - cmd_type = cmd.get("type", "") - try: - if cmd_type == "generate": - cancel_event.clear() - _handle_generate(backend, cmd, resp_queue, cancel_event) - elif cmd_type == "load": - if backend.active_model_name: - backend.unload_model(backend.active_model_name) - _handle_load(backend, cmd, resp_queue) - elif cmd_type == "unload": - _handle_unload(backend, cmd, resp_queue) - elif cmd_type == "cancel": - cancel_event.set() - elif cmd_type == "reset": - cancel_event.set() - backend.reset_generation_state() - _send_response(resp_queue, {"type": "reset_ack", "ts": time.time()}) - elif cmd_type == "status": - _send_response( - resp_queue, - { - "type": "status_response", - "active_model": backend.active_model_name, - "models": { - k: {kk: vv for kk, vv in v.items() if kk != "model"} - for k, v in backend.models.items() - }, - "loading": list(backend.loading_models), - "ts": time.time(), - }, - ) - elif cmd_type == "shutdown": - return - except Exception as exc: - logger.error("MLX command error (%s): %s", cmd_type, exc) - _send_response( - resp_queue, - { - "type": "gen_error" if cmd_type == "generate" else "error", - "request_id": cmd.get("request_id"), - "error": str(exc), - "stack": traceback.format_exc(limit = 20), - "ts": time.time(), - }, - ) - return - # ── 1. Activate correct transformers version BEFORE any ML imports ── try: _activate_transformers_version(model_name) diff --git a/studio/backend/core/training/resume.py b/studio/backend/core/training/resume.py deleted file mode 100644 index 165c1c2cf1..0000000000 --- a/studio/backend/core/training/resume.py +++ /dev/null @@ -1,75 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 - -"""Helpers for validating resumable training outputs.""" - -from pathlib import Path -from typing import Optional - -from utils.paths import outputs_root, resolve_output_dir - - -def _is_under_outputs(path: Path) -> bool: - resolved = path.resolve(strict = False) - root = outputs_root().resolve(strict = False) - try: - resolved.relative_to(root) - return True - except ValueError: - return False - - -def has_resume_state(path_value: Optional[str]) -> bool: - if not path_value: - return False - return get_resume_checkpoint_path(path_value) is not None - - -def _checkpoint_step(path: Path) -> int: - try: - return int(path.name.removeprefix("checkpoint-")) - except ValueError: - return -1 - - -def get_resume_checkpoint_path(path_value: str) -> Optional[str]: - path = resolve_output_dir(path_value) - if not _is_under_outputs(path) or not path.is_dir(): - return None - if (path / "trainer_state.json").is_file(): - return str(path) - - checkpoints = [ - child - for child in path.glob("checkpoint-*") - if child.is_dir() and (child / "trainer_state.json").is_file() - ] - if not checkpoints: - return None - return str(max(checkpoints, key = _checkpoint_step)) - - -def normalize_resume_output_dir(path_value: str) -> str: - path = resolve_output_dir(path_value) - if not _is_under_outputs(path): - raise ValueError("Resume checkpoint must be inside Studio outputs.") - return str(path) - - -def can_resume_run(run: dict) -> bool: - if run.get("resumed_later"): - return False - - final_step = run.get("final_step") - total_steps = run.get("total_steps") - has_remaining_steps = ( - not isinstance(final_step, int) - or not isinstance(total_steps, int) - or total_steps <= 0 - or final_step < total_steps - ) - return ( - run.get("status") == "stopped" - and has_remaining_steps - and has_resume_state(run.get("output_dir")) - ) diff --git a/studio/backend/core/training/trainer.py b/studio/backend/core/training/trainer.py index a3f063694f..77cbda6b45 100644 --- a/studio/backend/core/training/trainer.py +++ b/studio/backend/core/training/trainer.py @@ -49,7 +49,6 @@ import json import threading import math -import subprocess import structlog from loggers import get_logger import time @@ -62,7 +61,6 @@ from utils.models import is_vision_model, detect_audio_type from utils.datasets import format_and_template_dataset from utils.datasets import MODEL_TO_TEMPLATE_MAPPER, TEMPLATE_TO_RESPONSES_MAPPER -from utils.datasets.raw_text import prepare_raw_text_dataset from utils.paths import ( ensure_dir, resolve_dataset_path, @@ -71,11 +69,6 @@ ) from trl import SFTTrainer, SFTConfig -from utils.native_path_leases import child_env_without_native_path_secret -from utils.subprocess_compat import ( - windows_hidden_subprocess_kwargs as _windows_hidden_subprocess_kwargs, -) - logger = get_logger(__name__) @@ -126,7 +119,6 @@ def __init__(self): self.load_in_4bit = True # Track quantization mode for metadata # Model state tracking - self.is_cpt = False # Set to True for Continued Pretraining self.is_vlm = False self.is_audio = False self.is_audio_vlm = ( @@ -379,7 +371,6 @@ def _build_audio_training_args(self, training_args, output_dir, *, extra_args = def _finalize_training(self, output_dir, label = ""): """Save model after training and update progress. Used by all training branches.""" if self.should_stop and self.save_on_stop: - self.trainer._save_checkpoint(self.trainer.model, trial = None) self.trainer.save_model() self.tokenizer.save_pretrained(output_dir) self._patch_adapter_config(output_dir) @@ -927,7 +918,6 @@ def prepare_model_for_training( use_gradient_checkpointing: str = "unsloth", use_rslora: bool = False, use_loftq: bool = False, - modules_to_save: list = None, ) -> bool: """ Prepare model for training (with optional LoRA). @@ -1124,14 +1114,11 @@ def prepare_model_for_training( loftq_config = {"loftq_bits": 4, "loftq_iter": 1} if use_loftq else None, - modules_to_save = modules_to_save, ) else: # Text model LoRA logger.info(f"Text model LoRA configuration:") logger.info(f" - Target modules: {target_modules}\n") - if modules_to_save: - logger.info(f" - Modules to save: {modules_to_save}\n") self.model = FastLanguageModel.get_peft_model( self.model, @@ -1146,7 +1133,6 @@ def prepare_model_for_training( loftq_config = {"loftq_bits": 4, "loftq_iter": 1} if use_loftq else None, - modules_to_save = modules_to_save, ) # Check if stopped during LoRA preparation @@ -1779,8 +1765,6 @@ def _preprocess_bicodec_dataset(self, dataset, custom_format_mapping = None): spark_code_dir, ], check = True, - env = child_env_without_native_path_secret(), - **_windows_hidden_subprocess_kwargs(), ) if spark_code_dir not in sys.path: @@ -1998,6 +1982,8 @@ def _preprocess_dac_dataset(self, dataset, custom_format_mapping = None): device = "cuda" if torch.cuda.is_available() else "cpu" # Clone OuteTTS repo (same as audio_codecs._load_dac) + import subprocess + base_dir = os.path.dirname(os.path.abspath(__file__)) outetts_code_dir = os.path.join(base_dir, "inference", "OuteTTS") outetts_pkg = os.path.join(outetts_code_dir, "outetts") @@ -2014,8 +2000,6 @@ def _preprocess_dac_dataset(self, dataset, custom_format_mapping = None): outetts_code_dir, ], check = True, - env = child_env_without_native_path_secret(), - **_windows_hidden_subprocess_kwargs(), ) for fpath in [ os.path.join(outetts_pkg, "models", "gguf_model.py"), @@ -2349,7 +2333,6 @@ def load_and_format_dataset( eval_steps: float = 0.00, dataset_slice_start: int = None, dataset_slice_end: int = None, - is_cpt: bool = False, ) -> Optional[tuple]: """ Load and prepare dataset for training. @@ -2368,35 +2351,6 @@ def load_and_format_dataset( False # True if eval comes from a separate HF split ) eval_enabled = eval_steps is not None and eval_steps > 0 - raw_text_mode = is_cpt or format_type == "raw" - - def _raw_mode_label() -> str: - return "CPT" if is_cpt else "raw text" - - def _apply_raw_text_prep(ds: Dataset, split_name: str) -> Dataset: - try: - result = prepare_raw_text_dataset( - ds, - mode_label = _raw_mode_label(), - split_name = split_name, - eos_token = getattr(self.tokenizer, "eos_token", None), - append_eos = True, - ) - except ValueError as exc: - error_msg = str(exc) - logger.error(error_msg) - self._update_progress(error = error_msg) - raise - - for notice in result.notices: - if notice.level == "warning": - logger.warning(notice.message) - if notice.update_status: - self._update_progress(status_message = notice.message) - else: - logger.info(f"{notice.message}\n") - - return result.dataset if local_datasets: # Load local datasets using load_dataset() so the result is @@ -2571,48 +2525,6 @@ def _apply_raw_text_prep(ds: Dataset, split_name: str) -> Dataset: processed = self._preprocess_dac_dataset(dataset, custom_format_mapping) return ({"dataset": processed, "final_format": "audio_dac"}, None) - # ========== RAW TEXT BYPASS ========== - if raw_text_mode: - logger.info( - f"{_raw_mode_label().capitalize()} mode: bypassing chat template, " - "using raw text\n" - ) - dataset = _apply_raw_text_prep(dataset, "train") - if has_separate_eval_source and eval_dataset is not None: - eval_dataset = _apply_raw_text_prep(eval_dataset, "eval") - - dataset_info = { - "dataset": dataset, - "detected_format": "raw_text", - "final_format": "raw_text", - "success": True, - } - - if has_separate_eval_source and eval_dataset is not None: - logger.info( - f"{_raw_mode_label().capitalize()}: eval dataset " - f"({len(eval_dataset)} rows) kept as raw text\n" - ) - elif eval_enabled and not has_separate_eval_source: - split_result = self._resolve_eval_split_from_dataset(dataset) - if split_result is not None: - train_portion, eval_dataset = split_result - dataset_info["dataset"] = train_portion - - train_dataset = dataset_info["dataset"] - n = len(train_dataset) if hasattr(train_dataset, "__len__") else None - n_display = f"{n:,}" if isinstance(n, int) else "streaming" - self._update_progress( - status_message = f"Dataset ready ({n_display} samples, raw text)" - ) - logger.info(f"Raw-text dataset ready ({n_display} samples)\n") - - if "text" not in train_dataset.column_names: - raise ValueError( - f"Raw-text dataset missing 'text' column: {train_dataset.column_names}" - ) - return (dataset_info, eval_dataset) - elif self.is_audio_vlm: formatted = self._format_audio_vlm_dataset( dataset, custom_format_mapping @@ -2755,7 +2667,6 @@ def start_training( output_dir: str | None = None, num_epochs: int = 3, learning_rate: float = 2e-4, - embedding_learning_rate: float | None = None, batch_size: int = 2, gradient_accumulation_steps: int = 4, warmup_steps: int = None, @@ -2808,7 +2719,6 @@ def start_training( "output_dir": output_dir, "num_epochs": num_epochs, "learning_rate": learning_rate, - "embedding_learning_rate": embedding_learning_rate, "batch_size": batch_size, "gradient_accumulation_steps": gradient_accumulation_steps, "warmup_steps": warmup_steps, @@ -2913,9 +2823,7 @@ def _train_worker(self, dataset: Dataset, **training_args): total_steps = total, status_message = "Starting CSM training..." ) logger.info(f"CSM training config: {config}\n") - self.trainer.train( - resume_from_checkpoint = training_args.get("resume_from_checkpoint") - ) + self.trainer.train() self._finalize_training(output_dir, "CSM") return @@ -2954,9 +2862,7 @@ def _train_worker(self, dataset: Dataset, **training_args): total_steps = total, status_message = "Starting SNAC training..." ) logger.info(f"SNAC training config: {config}\n") - self.trainer.train( - resume_from_checkpoint = training_args.get("resume_from_checkpoint") - ) + self.trainer.train() self._finalize_training(output_dir, "SNAC") return @@ -3002,9 +2908,7 @@ def _train_worker(self, dataset: Dataset, **training_args): total_steps = total, status_message = "Starting Whisper training..." ) logger.info(f"Whisper training config: {config}\n") - self.trainer.train( - resume_from_checkpoint = training_args.get("resume_from_checkpoint") - ) + self.trainer.train() self._finalize_training(output_dir, "Whisper") return @@ -3026,13 +2930,6 @@ def _train_worker(self, dataset: Dataset, **training_args): logger.info("Configuring data collator...\n") - dataset_final_format = ( - str(dataset.get("final_format", "")).lower() - if isinstance(dataset, dict) - else "" - ) - raw_text_mode = dataset_final_format == "raw_text" - data_collator = None # Default to built-in data collator if is_deepseek_ocr: # Special DeepSeek OCR collator - auto-install if needed @@ -3072,7 +2969,7 @@ def _train_worker(self, dataset: Dataset, **training_args): self._update_progress(error = error_msg, is_training = False) return - elif self.is_audio_vlm and not raw_text_mode: + elif self.is_audio_vlm: # Audio VLM collator (e.g. Gemma 3N with audio data) # Mirrors the collate_fn from Gemma3N_(4B)-Audio notebook logger.info("Configuring audio VLM data collator...\n") @@ -3114,7 +3011,7 @@ def audio_vlm_collate_fn(examples): data_collator = audio_vlm_collate_fn logger.info("Audio VLM data collator configured\n") - elif self.is_vlm and not raw_text_mode: + elif self.is_vlm: # Standard VLM collator (images) logger.info("Using UnslothVisionDataCollator for vision model\n") from unsloth.trainer import UnslothVisionDataCollator @@ -3225,9 +3122,8 @@ def audio_vlm_collate_fn(examples): optim_value = training_args.get("optim", "adamw_8bit") lr_scheduler_type_value = training_args.get("lr_scheduler_type", "linear") - if (self.is_vlm or self.is_audio_vlm) and not raw_text_mode: + if self.is_vlm or self.is_audio_vlm: # Vision / audio VLM config (both need skip_prepare_dataset + remove_unused_columns) - # Raw-text runs on VLM-capable models are routed to the text path below. label = "audio VLM" if self.is_audio_vlm else "vision" logger.info(f"Configuring {label} model training parameters\n") # Use provided values or defaults for vision models @@ -3249,14 +3145,7 @@ def audio_vlm_collate_fn(examples): } ) else: - is_cpt = training_args.get("is_cpt", False) - self.is_cpt = is_cpt - if is_cpt: - logger.info("Configuring Continued Pretraining (CPT) parameters\n") - elif raw_text_mode: - logger.info("Configuring raw-text training parameters\n") - else: - logger.info("Configuring text model training parameters\n") + logger.info("Configuring text model training parameters\n") config_args.update( { "optim": optim_value, @@ -3285,10 +3174,9 @@ def audio_vlm_collate_fn(examples): logger.info("Training configuration prepared\n") # ========== TRAINER INITIALIZATION ========== - if self.is_audio_vlm and not raw_text_mode: + if self.is_audio_vlm: # Audio VLM (e.g. Gemma 3N + audio): raw Dataset from _format_audio_vlm_dataset # Notebook uses processing_class=processor.tokenizer (text tokenizer only) - # Raw-text runs are routed to the text path below. train_dataset = ( dataset if isinstance(dataset, Dataset) else dataset["dataset"] ) @@ -3307,9 +3195,8 @@ def audio_vlm_collate_fn(examples): if eval_dataset is not None: trainer_kwargs["eval_dataset"] = eval_dataset self.trainer = SFTTrainer(**trainer_kwargs) - elif self.is_vlm and not raw_text_mode: + elif self.is_vlm: # Image VLM: dataset is dict wrapper from format_and_template_dataset - # Raw-text runs are routed to the text path below. train_dataset = ( dataset["dataset"] if isinstance(dataset, dict) else dataset ) @@ -3340,48 +3227,16 @@ def audio_vlm_collate_fn(examples): ) sft_tokenizer = self.tokenizer.tokenizer - if is_cpt: - try: - from unsloth import ( - UnslothTrainer as _UnslothCPTTrainer, - UnslothTrainingArguments as _UnslothTrainingArguments, - ) - except ImportError as exc: - raise RuntimeError( - "CPT requires a newer Unsloth install that exports " - "`UnslothTrainer` and `UnslothTrainingArguments` " - "(for embedding_learning_rate support). " - "Upgrade with: `pip install -U unsloth unsloth_zoo`." - ) from exc - - embedding_lr = training_args.get("embedding_learning_rate") - logger.info( - f"CPT: using UnslothTrainer with embedding_learning_rate={embedding_lr}\n" - ) - trainer_kwargs = { - "model": self.model, - "tokenizer": sft_tokenizer, - "train_dataset": dataset["dataset"], - "data_collator": data_collator, - "args": _UnslothTrainingArguments( - embedding_learning_rate = embedding_lr, - **config_args, - ), - } - if eval_dataset is not None: - trainer_kwargs["eval_dataset"] = eval_dataset - self.trainer = _UnslothCPTTrainer(**trainer_kwargs) - else: - trainer_kwargs = { - "model": self.model, - "tokenizer": sft_tokenizer, - "train_dataset": dataset["dataset"], - "data_collator": data_collator, - "args": SFTConfig(**config_args), - } - if eval_dataset is not None: - trainer_kwargs["eval_dataset"] = eval_dataset - self.trainer = SFTTrainer(**trainer_kwargs) + trainer_kwargs = { + "model": self.model, + "tokenizer": sft_tokenizer, + "train_dataset": dataset["dataset"], + "data_collator": data_collator, + "args": SFTConfig(**config_args), + } + if eval_dataset is not None: + trainer_kwargs["eval_dataset"] = eval_dataset + self.trainer = SFTTrainer(**trainer_kwargs) # Restore the full processor as processing_class so checkpoint # saves include preprocessor_config.json (needed for GGUF export). if sft_tokenizer is not self.tokenizer: @@ -3390,32 +3245,19 @@ def audio_vlm_collate_fn(examples): # ========== TRAIN ON RESPONSES ONLY ========== # Determine if we should train on responses only - # Raw-text datasets always train on all tokens. instruction_part = None response_part = None - is_cpt = training_args.get("is_cpt", False) - train_on_responses_enabled = ( - False - if (is_cpt or raw_text_mode) - else training_args.get("train_on_completions", False) + train_on_responses_enabled = training_args.get( + "train_on_completions", False ) - if is_cpt: - logger.info( - "CPT mode: skipping train_on_responses_only — training on all tokens\n" - ) - elif raw_text_mode: - logger.info( - "Raw-text mode: skipping train_on_responses_only — training on all tokens\n" - ) - # DeepSeek OCR handles this internally in its collator, so skip # Audio VLM handles label masking in its collator, so skip if ( train_on_responses_enabled and not self.is_audio_vlm and not self.is_audio - and not (is_deepseek_ocr or dataset_final_format == "alpaca") + and not (is_deepseek_ocr or dataset["final_format"].lower() == "alpaca") ): try: logger.info("Configuring train on responses only...\n") @@ -3461,7 +3303,7 @@ def audio_vlm_collate_fn(examples): and response_part and not self.is_audio_vlm and not self.is_audio - and not (is_deepseek_ocr or dataset_final_format == "alpaca") + and not (is_deepseek_ocr or dataset["final_format"].lower() == "alpaca") ): try: from unsloth.chat_templates import train_on_responses_only @@ -3561,9 +3403,7 @@ def audio_vlm_collate_fn(examples): # ========== START TRAINING ========== self._update_progress(status_message = "Starting training...") logger.info("Starting training...\n") - self.trainer.train( - resume_from_checkpoint = training_args.get("resume_from_checkpoint") - ) + self.trainer.train() # ========== SAVE MODEL ========== self._finalize_training(output_dir) @@ -3594,9 +3434,7 @@ def _patch_adapter_config(self, output_dir: str) -> None: config = json.load(f) # Determine the training method - if self.is_cpt: - method = "CPT" - elif self.load_in_4bit: + if self.load_in_4bit: method = "qlora" else: method = "lora" diff --git a/studio/backend/core/training/training.py b/studio/backend/core/training/training.py index 72b13c3225..f35c7e8ad3 100644 --- a/studio/backend/core/training/training.py +++ b/studio/backend/core/training/training.py @@ -29,10 +29,6 @@ import matplotlib.pyplot as plt from utils.hardware import prepare_gpu_selection -from utils.native_path_leases import ( - native_path_secret_removed_for_child_start, - run_without_native_path_secret, -) logger = get_logger(__name__) @@ -62,7 +58,6 @@ class TrainingProgress: grad_norm: Optional[float] = None num_tokens: Optional[int] = None eval_loss: Optional[float] = None - peak_memory_gb: Optional[float] = None class TrainingBackend: @@ -159,7 +154,6 @@ def start_training(self, job_id: str, **kwargs) -> bool: "is_embedding": kwargs.get("is_embedding", False), "num_epochs": kwargs.get("num_epochs", 3), "learning_rate": kwargs.get("learning_rate", "2e-4"), - "embedding_learning_rate": kwargs.get("embedding_learning_rate"), "batch_size": kwargs.get("batch_size", 2), "gradient_accumulation_steps": kwargs.get("gradient_accumulation_steps", 4), "warmup_steps": kwargs.get("warmup_steps"), @@ -191,57 +185,47 @@ def start_training(self, job_id: str, **kwargs) -> bool: "wandb_project": kwargs.get("wandb_project", "unsloth-training"), "enable_tensorboard": kwargs.get("enable_tensorboard", False), "tensorboard_dir": kwargs.get("tensorboard_dir", "runs"), - "resume_from_checkpoint": kwargs.get("resume_from_checkpoint"), "trust_remote_code": kwargs.get("trust_remote_code", False), "gpu_ids": kwargs.get("gpu_ids"), } - # Full finetuning always runs in 16-bit. LoRA/QLoRA and CPT preserve the - # explicit request so 4-bit adapter/raw-text runs remain possible. - if config["training_type"] == "Full Finetuning": + # Derive load_in_4bit from training_type + if config["training_type"] != "LoRA/QLoRA": config["load_in_4bit"] = False # Spawn subprocess — use locals so state is untouched on failure - from utils.hardware import hardware as _hw - - if _hw.DEVICE == _hw.DeviceType.MLX: - config["resolved_gpu_ids"] = None - config["gpu_selection"] = None - else: - resolved_gpu_ids, gpu_selection = prepare_gpu_selection( - kwargs.get("gpu_ids"), - model_name = config["model_name"], - hf_token = config["hf_token"] or None, - training_type = config["training_type"], - load_in_4bit = config["load_in_4bit"], - batch_size = config.get("batch_size", 4), - max_seq_length = config.get("max_seq_length", 2048), - lora_rank = config.get("lora_r", 16), - target_modules = config.get("target_modules"), - gradient_checkpointing = config.get("gradient_checkpointing", "unsloth"), - optimizer = config.get("optim", "adamw_8bit"), - ) - config["resolved_gpu_ids"] = resolved_gpu_ids - config["gpu_selection"] = gpu_selection + resolved_gpu_ids, gpu_selection = prepare_gpu_selection( + kwargs.get("gpu_ids"), + model_name = config["model_name"], + hf_token = config["hf_token"] or None, + training_type = config["training_type"], + load_in_4bit = config["load_in_4bit"], + batch_size = config.get("batch_size", 4), + max_seq_length = config.get("max_seq_length", 2048), + lora_rank = config.get("lora_r", 16), + target_modules = config.get("target_modules"), + gradient_checkpointing = config.get("gradient_checkpointing", "unsloth"), + optimizer = config.get("optim", "adamw_8bit"), + ) + config["resolved_gpu_ids"] = resolved_gpu_ids + config["gpu_selection"] = gpu_selection from .worker import run_training_process + event_queue = _CTX.Queue() + stop_queue = _CTX.Queue() + + proc = _CTX.Process( + target = run_training_process, + kwargs = { + "event_queue": event_queue, + "stop_queue": stop_queue, + "config": config, + }, + daemon = True, + ) try: - with native_path_secret_removed_for_child_start(): - event_queue = _CTX.Queue() - stop_queue = _CTX.Queue() - - proc = _CTX.Process( - target = run_without_native_path_secret, - args = (run_training_process,), - kwargs = { - "event_queue": event_queue, - "stop_queue": stop_queue, - "config": config, - }, - daemon = True, - ) - proc.start() + proc.start() except Exception: logger.error("Failed to start training subprocess", exc_info = True) return False @@ -521,12 +505,6 @@ def _handle_event(self, event: dict) -> None: self._progress.grad_norm = event.get("grad_norm") self._progress.num_tokens = event.get("num_tokens") self._progress.eval_loss = event.get("eval_loss") - _peak = event.get("peak_memory_gb") - if _peak is not None: - try: - self._progress.peak_memory_gb = float(_peak) - except (TypeError, ValueError): - pass self._progress.is_training = True status = event.get("status_message", "") if status: diff --git a/studio/backend/core/training/worker.py b/studio/backend/core/training/worker.py index ef5cafb175..8ab2b5b2be 100644 --- a/studio/backend/core/training/worker.py +++ b/studio/backend/core/training/worker.py @@ -15,7 +15,6 @@ import structlog from loggers import get_logger -import math import os import shutil import sys @@ -36,15 +35,6 @@ ) -def _output_dir_from_resume_checkpoint( - resume_from_checkpoint: str | None, -) -> str | None: - if not resume_from_checkpoint: - return None - path = Path(resume_from_checkpoint) - return str(path.parent if path.name.startswith("checkpoint-") else path) - - _CAUSAL_CONV1D_RELEASE_TAG = "v1.6.1.post4" _CAUSAL_CONV1D_PACKAGE_VERSION = "1.6.1" _MAMBA_SSM_RELEASE_TAG = "v2.3.1" @@ -60,8 +50,6 @@ def _model_wants_causal_conv1d(model_name: str) -> bool: for key in ( "qwen3.5", "qwen3_5", - "qwen3.6", - "qwen3_6", "qwen3-next", "qwen3_next", "nemotron_h", @@ -339,594 +327,6 @@ def _activate_transformers_version(model_name: str) -> None: activate_transformers_for_subprocess(model_name) -def _adapt_for_mlx_vlm(items): - """Adapt GPU-path VLM dataset output for mlx-vlm consumption. - - The GPU path embeds PIL images inside messages content as - {"type": "image", "image": PIL_Image}. mlx-vlm's prepare_inputs - needs images at top-level to produce pixel_values — regardless of - model type. Extract them and leave bare {"type": "image"} placeholders. - """ - adapted = [] - for item in items: - images = [] - messages = [] - for msg in item.get("messages", []): - content = msg.get("content", "") - if isinstance(content, list): - new_content = [] - for part in content: - if isinstance(part, dict) and part.get("type") == "image": - img = part.get("image") - if img is not None: - images.append(img) - new_content.append({"type": "image"}) - else: - new_content.append(part) - messages.append({"role": msg["role"], "content": new_content}) - else: - messages.append(msg) - out = {"messages": messages} - if images: - out["image"] = images[0] if len(images) == 1 else images - elif "image" in item: - out["image"] = item["image"] - elif "images" in item: - out["images"] = item["images"] - adapted.append(out) - return adapted - - -_MLX_STUDIO_OPTIM_MAP = { - "adamw_8bit": "adamw", - "paged_adamw_8bit": "adamw", - "adamw_bnb_8bit": "adamw", - "paged_adamw_32bit": "adamw", - "adamw_torch": "adamw", - "adamw_torch_fused": "adamw", - "adamw": "adamw", - "adafactor": "adafactor", - "sgd": "sgd", - "adam": "adam", - "muon": "muon", - "lion": "lion", -} -_MLX_STUDIO_LR_SCHEDULERS = {"linear", "cosine", "constant"} - - -def _normalize_mlx_studio_optimizer(value): - raw = str(value or "adamw_8bit").strip().lower() - try: - return _MLX_STUDIO_OPTIM_MAP[raw] - except KeyError: - supported = ", ".join(sorted(_MLX_STUDIO_OPTIM_MAP)) - raise ValueError( - f"Unsupported optimizer for MLX training: {value!r}. " - f"Supported values: {supported}." - ) - - -def _normalize_mlx_studio_scheduler(value): - raw = str(value or "linear").strip().lower() - if raw not in _MLX_STUDIO_LR_SCHEDULERS: - supported = ", ".join(sorted(_MLX_STUDIO_LR_SCHEDULERS)) - raise ValueError( - f"Unsupported LR scheduler for MLX training: {value!r}. " - f"Supported values: {supported}." - ) - return raw - - -def _run_mlx_training(event_queue, stop_queue, config): - """Self-contained MLX training path for Apple Silicon. - - Uses MLXTrainer from unsloth_zoo directly -- no torch/SFTTrainer needed. - Mirrors the event_queue protocol so the parent process pump works unchanged. - """ - import time - import gc - import math - import threading - import queue as _queue - from pathlib import Path - - def _send(event_type, **kwargs): - if event_type == "status" and "message" not in kwargs: - sm = kwargs.get("status_message") - if sm is not None: - kwargs["message"] = sm - event_queue.put({"type": event_type, "ts": time.time(), **kwargs}) - - _send("status", status_message = "Loading MLX libraries...") - - import mlx.core as mx - - try: - from unsloth_zoo.mlx_loader import FastMLXModel - from unsloth_zoo.mlx_trainer import ( - MLXTrainer, - MLXTrainingConfig, - train_on_responses_only, - ) - except ImportError as e: - raise ImportError( - "Unsloth: MLX training requires unsloth-zoo with the MLX modules " - "(unsloth_zoo.mlx_loader / unsloth_zoo.mlx_trainer). Reinstall via " - "install.sh on Apple Silicon." - ) from e - from datasets import load_dataset - - if mx.metal.is_available(): - info = mx.device_info() - rec_bytes = info.get("max_recommended_working_set_size", 0) or 0 - if rec_bytes > 0: - memory_cap = int(rec_bytes * 0.85) - wired_cap = min(int(rec_bytes), memory_cap) - mx.set_memory_limit(memory_cap) - mx.set_wired_limit(wired_cap) - - model_name = config["model_name"] - hf_token = config.get("hf_token") or None - if hf_token: - os.environ["HF_TOKEN"] = hf_token - - if config.get("use_loftq"): - message = "LoftQ is not supported for MLX training yet." - _send("error", error = message) - raise NotImplementedError(message) - - optim_name = _normalize_mlx_studio_optimizer(config.get("optim", "adamw_8bit")) - lr_scheduler_type = _normalize_mlx_studio_scheduler( - config.get("lr_scheduler_type", "linear") - ) - - # ── 1. Load model ── - # Force text-only if the dataset is not an image dataset, even if the model - # has vision capabilities (e.g. Qwen3.5-VL trained on plain alpaca text). - _send("status", status_message = f"Loading {model_name}...") - is_dataset_image = bool(config.get("is_dataset_image", False)) - training_type = config.get("training_type", "LoRA/QLoRA") - use_lora = training_type == "LoRA/QLoRA" - model, tokenizer = FastMLXModel.from_pretrained( - model_name, - load_in_4bit = config.get("load_in_4bit", True), - full_finetuning = not use_lora, - text_only = None if is_dataset_image else True, - token = hf_token, - trust_remote_code = bool(config.get("trust_remote_code", False)), - random_state = config.get("random_seed", 3407), - ) - - is_vlm = bool(is_dataset_image and getattr(model, "_is_vlm_model", False)) - model._is_vlm_model = is_vlm - - # ── 2. Apply LoRA / full FT ── - # Pass gradient_checkpointing as string ("mlx"/"unsloth"/"none"/etc.) - # get_peft_model and MLXTrainer both accept strings and handle them. - gc_setting = config.get("gradient_checkpointing", "mlx") - if isinstance(gc_setting, str): - use_grad_checkpoint = ( - gc_setting if gc_setting.lower() not in ("false", "") else False - ) - else: - use_grad_checkpoint = gc_setting - - if use_lora: - _send("status", status_message = "Configuring LoRA adapters...") - peft_kwargs = dict( - r = config.get("lora_r", 16), - lora_alpha = config.get("lora_alpha", 16), - lora_dropout = config.get("lora_dropout", 0.0), - use_rslora = config.get("use_rslora", False), - init_lora_weights = config.get("init_lora_weights", True), - random_state = config.get("random_seed", 3407), - target_modules = config.get("target_modules") - or [ - "q_proj", - "k_proj", - "v_proj", - "o_proj", - "gate_proj", - "up_proj", - "down_proj", - ], - use_gradient_checkpointing = use_grad_checkpoint, - ) - finetune_language = config.get("finetune_language_layers", True) - finetune_attention = config.get("finetune_attention_modules", True) - finetune_mlp = config.get("finetune_mlp_modules", True) - finetune_vision = ( - config.get("finetune_vision_layers", False) if is_vlm else False - ) - - if ( - (finetune_attention or finetune_mlp) - and not finetune_language - and not finetune_vision - ): - finetune_language = True - - peft_kwargs["finetune_language_layers"] = finetune_language - peft_kwargs["finetune_attention_modules"] = finetune_attention - peft_kwargs["finetune_mlp_modules"] = finetune_mlp - if is_vlm: - peft_kwargs["finetune_vision_layers"] = finetune_vision - model = FastMLXModel.get_peft_model(model, **peft_kwargs) - - # ── 3. Load dataset ── - _send("status", status_message = "Loading dataset...") - hf_dataset = config.get("hf_dataset", "") - subset = config.get("subset") - train_split = config.get("train_split", "train") or "train" - eval_split = config.get("eval_split") - slice_start = config.get("dataset_slice_start") - slice_end = config.get("dataset_slice_end") - - def _slice(ds): - if slice_start is not None or slice_end is not None: - start = slice_start if slice_start is not None else 0 - end = slice_end if slice_end is not None else len(ds) - 1 - if end < start: - return ds.select([]) - ds = ds.select(range(start, min(end + 1, len(ds)))) - return ds - - def _load_local(file_paths): - from core.training.trainer import UnslothTrainer - from datasets import load_from_disk - - if len(file_paths) == 1: - p = Path(file_paths[0]) - if p.is_dir() and ( - (p / "dataset_info.json").exists() or (p / "state.json").exists() - ): - return load_from_disk(str(p)) - all_files = UnslothTrainer._resolve_local_files(file_paths) - if not all_files: - raise ValueError("No local dataset files found") - loader = UnslothTrainer._loader_for_files(all_files) - return load_dataset(loader, data_files = all_files, split = "train") - - if hf_dataset: - load_kwargs = {"split": train_split, "token": hf_token} - if subset: - load_kwargs["name"] = subset - dataset = load_dataset(hf_dataset, **load_kwargs) - dataset = _slice(dataset) - elif config.get("local_datasets"): - dataset = _load_local(config["local_datasets"]) - dataset = _slice(dataset) - else: - raise ValueError("No dataset specified") - - # Eval dataset (separate split or local file) - eval_dataset = None - if eval_split and hf_dataset: - eval_kwargs = {"split": eval_split, "token": hf_token} - if subset: - eval_kwargs["name"] = subset - try: - eval_dataset = load_dataset(hf_dataset, **eval_kwargs) - except Exception as e: - _send("status", status_message = f"Eval split load failed: {e}") - eval_dataset = None - elif config.get("local_eval_datasets"): - eval_dataset = _load_local(config["local_eval_datasets"]) - - # ── 3b. Format dataset (VLM or text) ── - # Reuse the GPU path's format pipeline for both VLM (auto-detects OCR/caption/ - # llava/sharegpt+images) and text (alpaca/sharegpt/chatml → "text" column). - format_type = config.get("format_type", "") - try: - from utils.datasets import format_and_template_dataset - - def _fmt_progress(status_message = "", **_kw): - _send("status", status_message = status_message) - - if is_vlm: - _send("status", status_message = "Formatting VLM dataset...") - vlm_info = format_and_template_dataset( - dataset, - model_name = model_name, - tokenizer = tokenizer, - is_vlm = True, - dataset_name = hf_dataset or "local", - progress_callback = _fmt_progress, - ) - if vlm_info.get("success"): - dataset = _adapt_for_mlx_vlm(vlm_info["dataset"]) - else: - errors = vlm_info.get("errors", []) - raise ValueError( - f"VLM dataset format conversion failed: {'; '.join(errors)}" - ) - if eval_dataset is not None: - ev_info = format_and_template_dataset( - eval_dataset, - model_name = model_name, - tokenizer = tokenizer, - is_vlm = True, - dataset_name = hf_dataset or "local", - ) - if ev_info.get("success"): - eval_dataset = _adapt_for_mlx_vlm(ev_info["dataset"]) - - elif format_type: - _send("status", status_message = f"Formatting dataset ({format_type})...") - info = format_and_template_dataset( - dataset, - model_name = model_name, - tokenizer = tokenizer, - is_vlm = False, - format_type = format_type, - dataset_name = hf_dataset or "local", - ) - if info.get("success", True): - dataset = info.get("dataset", dataset) - if eval_dataset is not None: - ev = format_and_template_dataset( - eval_dataset, - model_name = model_name, - tokenizer = tokenizer, - is_vlm = False, - format_type = format_type, - dataset_name = hf_dataset or "local", - ) - if ev.get("success", True): - eval_dataset = ev.get("dataset", eval_dataset) - except ImportError: - _send("status", status_message = "Format helper unavailable, using raw dataset") - - # ── 4. Resolve training steps ── - max_steps = config.get("max_steps", 0) or 0 - num_epochs = config.get("num_epochs", 3) - max_seq_length = config.get("max_seq_length", 2048) - batch_size = config.get("batch_size", 4) - grad_accum = config.get("gradient_accumulation_steps", 4) - - if max_steps <= 0: - max_steps = max( - 1, - math.ceil(len(dataset) / batch_size / grad_accum) * num_epochs, - ) - - lr_value = float(config.get("learning_rate", "2e-4")) - - # Warmup: prefer warmup_steps; fall back to warmup_ratio - warmup_steps = config.get("warmup_steps") - warmup_ratio = config.get("warmup_ratio") - if warmup_steps is None and warmup_ratio is not None: - warmup_steps = int(round(warmup_ratio * max_steps)) - if warmup_steps is None: - warmup_steps = 5 - - # ── 5. Build output dir ── - output_dir = config.get("output_dir", "") - if not output_dir: - output_dir = f"{model_name.replace('/', '_')}_{int(time.time())}" - # Resolve to ~/.unsloth/studio/outputs/ so the export page can find it - from utils.paths import resolve_output_dir, ensure_dir - - output_dir = str(resolve_output_dir(output_dir)) - ensure_dir(Path(output_dir)) - - # ── 6. Create trainer ── - eval_steps_val = config.get("eval_steps", 0) or 0 - if isinstance(eval_steps_val, float) and 0 < eval_steps_val < 1: - # Studio sometimes sends fraction-of-total-steps - eval_steps_val = max(1, int(eval_steps_val * max_steps)) - else: - eval_steps_val = int(eval_steps_val) - - trainer = MLXTrainer( - model = model, - tokenizer = tokenizer, - train_dataset = dataset, - eval_dataset = eval_dataset, - args = MLXTrainingConfig( - per_device_train_batch_size = batch_size, - gradient_accumulation_steps = grad_accum, - max_steps = max_steps, - learning_rate = lr_value, - warmup_steps = warmup_steps, - lr_scheduler_type = lr_scheduler_type, - optim = optim_name, - weight_decay = float(config.get("weight_decay", 0.001) or 0.001), - logging_steps = 1, - max_seq_length = max_seq_length, - seed = config.get("random_seed", 3407), - use_cce = True, - compile = True, - gradient_checkpointing = use_grad_checkpoint, - streaming = is_vlm, - packing = bool(config.get("packing", False)), - output_dir = output_dir, - save_steps = int(config.get("save_steps", 0) or 0), - eval_steps = eval_steps_val, - ), - ) - - # Tell the parent that eval is configured so the frontend shows the eval chart - if eval_dataset is not None and eval_steps_val > 0: - _send("eval_configured") - - # ── 7. Apply train_on_responses_only if requested ── - if config.get("train_on_completions", False): - _send("status", status_message = "Configuring response-only training...") - try: - from utils.datasets import ( - MODEL_TO_TEMPLATE_MAPPER, - TEMPLATE_TO_RESPONSES_MAPPER, - ) - - template_name = MODEL_TO_TEMPLATE_MAPPER.get(model_name.lower()) - markers = ( - TEMPLATE_TO_RESPONSES_MAPPER.get(template_name) - if template_name - else None - ) - if markers: - trainer = train_on_responses_only( - trainer, - instruction_part = markers["instruction"], - response_part = markers["response"], - ) - else: - _send( - "status", - status_message = f"train_on_completions skipped (no template for {model_name})", - ) - except Exception as e: - _send("status", status_message = f"train_on_completions failed: {e}") - - # ── 8. Setup wandb / tensorboard ── - wandb_run = None - tb_writer = None - if config.get("enable_wandb", False): - try: - import wandb as _wandb - - wandb_token = config.get("wandb_token") - if wandb_token: - os.environ["WANDB_API_KEY"] = wandb_token - _wandb_sensitive = {"hf_token", "wandb_token"} - wandb_run = _wandb.init( - project = config.get("wandb_project") or "unsloth-mlx", - config = {k: v for k, v in config.items() if k not in _wandb_sensitive}, - reinit = True, - ) - except Exception as e: - _send("status", status_message = f"wandb init failed: {e}") - if config.get("enable_tensorboard", False): - try: - from tensorboardX import SummaryWriter - except ImportError: - try: - from torch.utils.tensorboard import SummaryWriter - except ImportError: - SummaryWriter = None - if SummaryWriter is not None: - try: - tb_dir = config.get("tensorboard_dir") or f"{output_dir}/runs" - tb_writer = SummaryWriter(log_dir = tb_dir) - except Exception as e: - _send("status", status_message = f"tensorboard init failed: {e}") - else: - _send( - "status", - status_message = "tensorboard unavailable (install tensorboardX)", - ) - - # ── 9. Real-time progress callback ── - _send("status", status_message = f"Training {model_name}...") - - def _on_step(step, total, loss, lr, tok_s, peak_gb, elapsed, num_tokens): - eta = (elapsed / step * (total - step)) if step > 0 else 0 - _send( - "progress", - step = step, - epoch = round(step / total * num_epochs, 2) if total > 0 else 0, - loss = loss, - learning_rate = lr, - total_steps = total, - elapsed_seconds = elapsed, - eta_seconds = max(0, eta), - grad_norm = None, - num_tokens = num_tokens, - eval_loss = None, - status_message = None, - peak_memory_gb = peak_gb, - ) - if wandb_run is not None: - try: - wandb_run.log( - { - "train/loss": loss, - "train/learning_rate": lr, - "train/tokens_per_sec": tok_s, - "train/peak_gb": peak_gb, - "train/num_tokens": num_tokens, - }, - step = step, - ) - except Exception: - pass - if tb_writer is not None: - try: - tb_writer.add_scalar("train/loss", loss, step) - tb_writer.add_scalar("train/learning_rate", lr, step) - tb_writer.add_scalar("train/tokens_per_sec", tok_s, step) - tb_writer.add_scalar("train/peak_gb", peak_gb, step) - except Exception: - pass - - trainer.add_step_callback(_on_step) - - def _on_eval(step, eval_loss, perplexity): - _send("progress", step = step, eval_loss = eval_loss) - if wandb_run is not None: - try: - wandb_run.log( - {"eval/loss": eval_loss, "eval/perplexity": perplexity}, step = step - ) - except Exception: - pass - if tb_writer is not None: - try: - tb_writer.add_scalar("eval/loss", eval_loss, step) - tb_writer.add_scalar("eval/perplexity", perplexity, step) - except Exception: - pass - - trainer.add_eval_callback(_on_eval) - - # ── 10. Stop signal polling ── - _stop_save = [True] # mutable so thread can update; [save_flag] - - def _poll_stop(): - while True: - try: - msg = stop_queue.get(timeout = 1.0) - if msg and msg.get("type") == "stop": - _stop_save[0] = msg.get("save", True) - trainer.stop_requested = True - return - except _queue.Empty: - continue - except (EOFError, OSError): - # why safe: pipe permanently broken, no further messages can arrive - return - - stop_thread = threading.Thread(target = _poll_stop, daemon = True) - stop_thread.start() - - # ── 11. Run training ── - gc.collect() - mx.synchronize() - trainer.train() - - # ── 12. Save and finalize ── - if trainer.stop_requested and not _stop_save[0]: - # User clicked "Cancel" (save=False) — skip saving - _send("complete", output_dir = None, status_message = "Training cancelled") - else: - _send("status", status_message = "Saving model...") - mx.synchronize() - trainer.save_model(output_dir) - _send("complete", output_dir = output_dir, status_message = "Training completed") - - if tb_writer is not None: - try: - tb_writer.close() - except Exception: - pass - if wandb_run is not None: - try: - wandb_run.finish() - except Exception: - pass - - def run_training_process( *, event_queue: Any, @@ -960,46 +360,6 @@ def run_training_process( model_name = config["model_name"] - # ── 0. MLX FAST-PATH (must run before any torch/transformers imports) ── - # Apple Silicon uses MLXTrainer directly -- skip transformers version - # activation, causal-conv1d install, and torch imports entirely. - backend_path = str(Path(__file__).resolve().parent.parent.parent) - if backend_path not in sys.path: - sys.path.insert(0, backend_path) - - from utils.hardware import hardware as _hw - - _hw.detect_hardware() - if _hw.DEVICE == _hw.DeviceType.MLX: - if config.get("is_dataset_audio"): - event_queue.put( - { - "type": "error", - "error": "Audio dataset training is not yet supported on Apple Silicon.", - "stack": "", - "ts": time.time(), - } - ) - return - # Activate correct transformers version (Gemma-4 needs 5.5.0, etc.) - # Must happen before any transformers/mlx-lm imports in _run_mlx_training. - try: - _activate_transformers_version(model_name) - except Exception: - pass # Non-fatal: fall through with whatever version is installed - try: - _run_mlx_training(event_queue, stop_queue, config) - except Exception as exc: - event_queue.put( - { - "type": "error", - "error": str(exc), - "stack": traceback.format_exc(limit = 20), - "ts": time.time(), - } - ) - return - # ── 1. Activate correct transformers version BEFORE any ML imports ── try: _activate_transformers_version(model_name) @@ -1209,8 +569,6 @@ def _poll_stop(): # ── 4b. Load and format dataset (LLM helper may use VRAM briefly) ── _send_status(event_queue, "Loading and formatting dataset...") hf_dataset = config.get("hf_dataset", "") - training_type = config.get("training_type", "LoRA/QLoRA") - _is_cpt_for_dataset = training_type == "Continued Pretraining" dataset_result = trainer.load_and_format_dataset( dataset_source = hf_dataset if hf_dataset and hf_dataset.strip() else None, format_type = config.get("format_type", ""), @@ -1223,7 +581,6 @@ def _poll_stop(): eval_steps = config.get("eval_steps", 0.00), dataset_slice_start = config.get("dataset_slice_start"), dataset_slice_end = config.get("dataset_slice_end"), - is_cpt = _is_cpt_for_dataset, ) if isinstance(dataset_result, tuple): @@ -1309,9 +666,7 @@ def _monitor_tqdm(): _tqdm_thread.start() training_type = config.get("training_type", "LoRA/QLoRA") - is_cpt = training_type == "Continued Pretraining" - use_lora = training_type in ("LoRA/QLoRA", "Continued Pretraining") - cpt_trains_embeddings = False + use_lora = training_type == "LoRA/QLoRA" # ── 4c. Load training model (uses VRAM — dataset already formatted) ── _send_status(event_queue, "Loading model...") @@ -1343,41 +698,8 @@ def _monitor_tqdm(): ) return - # ── 4d. Prepare model (LoRA, full finetuning, or CPT) ── - if is_cpt: - _send_status(event_queue, "Configuring LoRA for continued pretraining...") - # embed_tokens (if the user included it) goes to modules_to_save — - # trained full-precision at embedding_learning_rate. lm_head stays as - # a LoRA target for merge compatibility (see unsloth PR #4106). - _user_modules = config.get("target_modules") or [] - wants_embed = "embed_tokens" in _user_modules - cpt_trains_embeddings = wants_embed - cpt_target_modules = [m for m in _user_modules if m != "embed_tokens"] - if not cpt_target_modules: - cpt_target_modules = [ - "q_proj", - "k_proj", - "v_proj", - "o_proj", - "gate_proj", - "up_proj", - "down_proj", - "lm_head", - ] - success = trainer.prepare_model_for_training( - use_lora = True, - target_modules = cpt_target_modules, - modules_to_save = ["embed_tokens"] if wants_embed else None, - lora_r = config.get("lora_r", 128), - lora_alpha = config.get("lora_alpha", 32), - lora_dropout = config.get("lora_dropout", 0.0), - use_gradient_checkpointing = config.get( - "gradient_checkpointing", "unsloth" - ), - use_rslora = config.get("use_rslora", False), - use_loftq = config.get("use_loftq", False), - ) - elif use_lora: + # ── 4d. Prepare model (LoRA or full finetuning) ── + if use_lora: _send_status(event_queue, "Configuring LoRA adapters...") success = trainer.prepare_model_for_training( use_lora = True, @@ -1418,9 +740,9 @@ def _monitor_tqdm(): ) return - lr_default = "5e-5" if is_cpt else "2e-4" + # Convert learning rate try: - lr_value = float(config.get("learning_rate", lr_default)) + lr_value = float(config.get("learning_rate", "2e-4")) except ValueError: event_queue.put( { @@ -1432,30 +754,8 @@ def _monitor_tqdm(): ) return - # embedding_learning_rate is validated by the Pydantic model (Optional[float], - # gt=0, lt=1.0); if present it is already a finite float in range. - embedding_lr_value = config.get("embedding_learning_rate") - if is_cpt: - if cpt_trains_embeddings: - if embedding_lr_value is None: - # Default embedding_learning_rate = lr/10 per Unsloth's CPT notebook. - embedding_lr_value = lr_value / 10.0 - logger.info( - f"CPT: using default embedding_learning_rate={embedding_lr_value:.1e} " - f"(lr/10). Set explicitly to override.\n" - ) - elif embedding_lr_value is not None: - logger.warning( - "CPT: embedding_learning_rate was provided but embed_tokens is " - "not being trained; ignoring the override.\n" - ) - embedding_lr_value = None - # Generate output dir - resume_from_checkpoint = config.get("resume_from_checkpoint") - output_dir = config.get("output_dir") or _output_dir_from_resume_checkpoint( - resume_from_checkpoint - ) + output_dir = config.get("output_dir") if not output_dir: output_dir = f"{model_name.replace('/', '_')}_{int(time.time())}" output_dir = str(resolve_output_dir(output_dir)) @@ -1483,7 +783,6 @@ def _monitor_tqdm(): output_dir = output_dir, num_epochs = config.get("num_epochs", 3), learning_rate = lr_value, - embedding_learning_rate = embedding_lr_value, batch_size = config.get("batch_size", 2), gradient_accumulation_steps = config.get("gradient_accumulation_steps", 4), warmup_steps = config.get("warmup_steps"), @@ -1493,9 +792,7 @@ def _monitor_tqdm(): weight_decay = config.get("weight_decay", 0.001), random_seed = config.get("random_seed", 3407), packing = config.get("packing", False), - train_on_completions = False - if is_cpt - else config.get("train_on_completions", False), + train_on_completions = config.get("train_on_completions", False), enable_wandb = config.get("enable_wandb", False), wandb_project = config.get("wandb_project", "unsloth-training"), wandb_token = config.get("wandb_token"), @@ -1506,8 +803,6 @@ def _monitor_tqdm(): max_seq_length = config.get("max_seq_length", 2048), optim = config.get("optim", "adamw_8bit"), lr_scheduler_type = config.get("lr_scheduler_type", "linear"), - is_cpt = is_cpt, - resume_from_checkpoint = resume_from_checkpoint, ) _tqdm_stop.set() @@ -1524,13 +819,10 @@ def _monitor_tqdm(): } ) else: - saved_output_dir = ( - None if trainer.should_stop and not trainer.save_on_stop else output_dir - ) event_queue.put( { "type": "complete", - "output_dir": saved_output_dir, + "output_dir": output_dir, "status_message": progress.status_message or "Training completed", "ts": time.time(), } @@ -1815,15 +1107,11 @@ def _poll_stop(): ) return - resume_from_checkpoint = config.get("resume_from_checkpoint") - output_dir = config.get("output_dir") or _output_dir_from_resume_checkpoint( - resume_from_checkpoint - ) + output_dir = config.get("output_dir") if not output_dir: output_dir = str( resolve_output_dir(f"{model_name.replace('/', '_')}_{int(time.time())}") ) - output_dir = str(resolve_output_dir(output_dir)) num_epochs = config.get("num_epochs", 2) batch_size = config.get("batch_size", 256) @@ -1931,7 +1219,7 @@ def on_step_end(self, args, state, control, **kwargs): callbacks = [_EmbeddingProgressCallback()], ) - trainer.train(resume_from_checkpoint = resume_from_checkpoint) + trainer.train() except Exception as e: event_queue.put( { @@ -1957,8 +1245,6 @@ def on_step_end(self, args, state, control, **kwargs): _send_status(event_queue, "Saving model...") try: - if _should_stop and _save_on_stop: - trainer._save_checkpoint(trainer.model, trial = None) model.save_pretrained(output_dir) model.tokenizer.save_pretrained(output_dir) logger.info("Embedding model saved to %s", output_dir) diff --git a/studio/backend/loggers/config.py b/studio/backend/loggers/config.py index 4a27f13d38..0d32a64657 100644 --- a/studio/backend/loggers/config.py +++ b/studio/backend/loggers/config.py @@ -22,8 +22,6 @@ import structlog -from loggers.handlers import filter_sensitive_data - class LogConfig: """Structured logging configuration for the application. @@ -46,22 +44,12 @@ def setup_logging( # Fallback to INFO if an invalid level is provided log_level = getattr(logging, log_level_name, logging.INFO) - if sys.platform == "win32": - for stream in (sys.stdout, sys.stderr): - if hasattr(stream, "reconfigure"): - try: - stream.reconfigure(encoding = "utf-8", errors = "replace") - except Exception: - pass - structlog.configure( processors = [ # Reorder processors to control field order structlog.processors.TimeStamper(fmt = "iso"), # timestamp first structlog.processors.add_log_level, # level second structlog.contextvars.merge_contextvars, - structlog.processors.format_exc_info, - filter_sensitive_data, # Custom processor to flatten the extra field lambda logger, method_name, event_dict: { "timestamp": event_dict.get("timestamp"), diff --git a/studio/backend/loggers/handlers.py b/studio/backend/loggers/handlers.py index ddd404cdf3..3add92ea1e 100644 --- a/studio/backend/loggers/handlers.py +++ b/studio/backend/loggers/handlers.py @@ -15,7 +15,6 @@ - get_logger: Factory function for structured loggers """ -import re import time from typing import Callable @@ -23,12 +22,7 @@ from fastapi import Request, Response from starlette.middleware.base import BaseHTTPMiddleware -from utils.native_path_leases import redact_native_paths - logger = structlog.get_logger(__name__) -_NATIVE_PATH_LEASE_RE = re.compile( - r"(?i)(\b(?:native_path_lease|nativePathLease)[\"']?\s*[:=]\s*[\"']?)[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+" -) class LoggingMiddleware(BaseHTTPMiddleware): @@ -81,12 +75,6 @@ def filter_sensitive_data(logger, method_name, event_dict): """Structlog processor to filter out base64 data from logs.""" def filter_value(value): - if isinstance(value, str): - try: - value = redact_native_paths(value) - except Exception: - pass - value = _NATIVE_PATH_LEASE_RE.sub(r"\1", value) if ( isinstance(value, str) and len(value) > 100 @@ -95,22 +83,12 @@ def filter_value(value): # Likely base64 data, truncate it return value[:20] + "..." elif isinstance(value, dict): - return { - k: "" - if str(k).replace("_", "").lower() == "nativepathlease" - else filter_value(v) - for k, v in value.items() - } + return {k: filter_value(v) for k, v in value.items()} elif isinstance(value, list): return [filter_value(item) for item in value] return value - return { - k: "" - if str(k).replace("_", "").lower() == "nativepathlease" - else filter_value(v) - for k, v in event_dict.items() - } + return {k: filter_value(v) for k, v in event_dict.items()} def get_logger(name: str) -> structlog.BoundLogger: diff --git a/studio/backend/main.py b/studio/backend/main.py index cd901327db..d146a8ef12 100644 --- a/studio/backend/main.py +++ b/studio/backend/main.py @@ -23,67 +23,12 @@ # See: https://github.com/python/cpython/issues/102396 import _platform_compat # noqa: F401 -# Direct `uvicorn main:app` launches bypass run.py, so re-export here too -# (mirrors run.py). Required BEFORE the unsloth-zoo import below, since -# its LLAMA_CPP_DEFAULT_DIR binding is import-time. -from utils.paths.storage_roots import studio_root as _studio_root - -try: - _LEGACY_STUDIO_ROOT = (_Path.home() / ".unsloth" / "studio").resolve() -except (OSError, ValueError): - _LEGACY_STUDIO_ROOT = _Path.home() / ".unsloth" / "studio" -try: - _STUDIO_ROOT_RESOLVED = _studio_root().resolve() -except (OSError, ValueError): - _STUDIO_ROOT_RESOLVED = _studio_root() -if _STUDIO_ROOT_RESOLVED != _LEGACY_STUDIO_ROOT: - if not os.environ.get("UNSLOTH_STUDIO_HOME"): - os.environ["UNSLOTH_STUDIO_HOME"] = str(_STUDIO_ROOT_RESOLVED) - if not os.environ.get("UNSLOTH_LLAMA_CPP_PATH"): - os.environ["UNSLOTH_LLAMA_CPP_PATH"] = str(_STUDIO_ROOT_RESOLVED / "llama.cpp") - import mimetypes -import re as _re import shutil import warnings from contextlib import asynccontextmanager from importlib.metadata import PackageNotFoundError, version as package_version - -_STUDIO_INSTALL_ID_RE = _re.compile(r"^[0-9a-f]{64}$") - - -def _read_studio_install_id() -> str: - """Per-install opaque id written by install.sh / install.ps1 at - $STUDIO_HOME/share/studio_install_id. Returns "" when the file is - absent (pre-PR install, fresh tree never run through the installer) - or contains anything other than a 64-char lowercase-hex token -- - in which case /api/health emits "" and the launcher's _check_health - falls back to the existing "no baked id, accept any healthy - Unsloth backend" path. This intentionally replaces a previous - sha256(resolved_install_path) so the field carries no install-path - information for callers reaching /api/health (relevant when Studio - is run with -H 0.0.0.0).""" - try: - token = ( - (_STUDIO_ROOT_RESOLVED / "share" / "studio_install_id").read_text().strip() - ) - except (OSError, ValueError): - return "" - return token if _STUDIO_INSTALL_ID_RE.fullmatch(token) else "" - - -_STUDIO_ROOT_ID_CACHE: str = _read_studio_install_id() - - -def _studio_root_id() -> str: - """Same-install discriminator for /api/health: a per-install opaque - token written once by the installer and read once at module import. - Empty when no installer-written token is present; the launcher - contract treats "" as "no baked id, accept any healthy backend".""" - return _STUDIO_ROOT_ID_CACHE - - # Fix broken Windows registry MIME types. Some Windows installs map .js to # "text/plain" in the registry (HKCR\.js\Content Type). Python's mimetypes # module reads from the registry, and FastAPI/Starlette's StaticFiles uses @@ -117,7 +62,6 @@ def _studio_root_id() -> str: datasets_router, export_router, inference_router, - inference_studio_router, models_router, training_history_router, training_router, @@ -133,7 +77,6 @@ def _studio_root_id() -> str: import utils.hardware.hardware as _hw_module from utils.cache_cleanup import clear_unsloth_compiled_cache -from utils.native_path_leases import native_path_leases_supported def get_unsloth_version() -> str: @@ -236,24 +179,9 @@ def _precache(): app.add_middleware(LoggingMiddleware) # CORS middleware -_api_only = os.environ.get("UNSLOTH_API_ONLY") == "1" -_cors_origins = ["*"] -if _api_only: - _cors_origins = [ - "tauri://localhost", # Linux/macOS Tauri webview - "http://tauri.localhost", # Windows Tauri webview - "http://localhost", # dev fallback - "http://localhost:5173", # Tauri dev/Vite - "http://127.0.0.1:5173", # Tauri dev/Vite fallback - ] - _cors_origin_regex = None -else: - _cors_origin_regex = None - app.add_middleware( CORSMiddleware, - allow_origins = _cors_origins, - allow_origin_regex = _cors_origin_regex, + allow_origins = ["*"], # In production, specify allowed origins allow_credentials = True, allow_methods = ["*"], allow_headers = ["*"], @@ -266,9 +194,6 @@ def _precache(): 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"]) -# Studio-only inference endpoints (cancel, etc.) are intentionally NOT -# exposed on the /v1 OpenAI-compat prefix below. -app.include_router(inference_studio_router, prefix = "/api/inference", tags = ["inference"]) # OpenAI-compatible endpoints: mount the same inference router at /v1 # so external tools (Open WebUI, SillyTavern, etc.) can use the @@ -298,13 +223,6 @@ async def health_check(): "version": UNSLOTH_VERSION, "device_type": device_type, "chat_only": _hw_module.CHAT_ONLY, - "desktop_protocol_version": 1, - "supports_desktop_auth": True, - # why: launchers compare against an install-time hash so a sibling - # Studio on the same port is rejected; hex digest avoids leaking the - # raw install path on -H 0.0.0.0. - "studio_root_id": _studio_root_id(), - "native_path_leases_supported": native_path_leases_supported(), } diff --git a/studio/backend/models/auth.py b/studio/backend/models/auth.py index 23eb0ac4c0..c55e646508 100644 --- a/studio/backend/models/auth.py +++ b/studio/backend/models/auth.py @@ -17,12 +17,6 @@ class AuthLoginRequest(BaseModel): password: str = Field(..., description = "Password") -class DesktopLoginRequest(BaseModel): - """Desktop-only local secret exchange payload.""" - - secret: str = Field(..., description = "Desktop local auth secret") - - class RefreshTokenRequest(BaseModel): """Refresh token payload to obtain new access + refresh tokens.""" diff --git a/studio/backend/models/inference.py b/studio/backend/models/inference.py index 43087cc5bf..324002ddbf 100644 --- a/studio/backend/models/inference.py +++ b/studio/backend/models/inference.py @@ -18,9 +18,6 @@ class LoadRequest(BaseModel): """Request to load a model for inference""" model_path: str = Field(..., description = "Model identifier or local path") - native_path_lease: Optional[str] = Field( - None, description = "Frontend-visible signed native path grant" - ) hf_token: Optional[str] = Field( None, description = "HuggingFace token for gated models" ) @@ -55,16 +52,6 @@ class LoadRequest(BaseModel): None, description = "Speculative decoding mode for GGUF models (e.g. 'ngram-simple', 'ngram-mod'). Ignored for non-GGUF and vision models.", ) - llama_extra_args: Optional[List[str]] = Field( - None, - description = ( - "Extra arguments forwarded verbatim to llama-server for GGUF models. " - "One token per list entry, e.g. ['--top-k', '20', '--seed', '42']. " - "Studio-managed flags (model identity, port, context length, GPU placement, " - "auth, --flash-attn, --no-context-shift, --jinja) are rejected. Ignored for " - "non-GGUF models." - ), - ) class UnloadRequest(BaseModel): @@ -82,9 +69,6 @@ class ValidateModelRequest(BaseModel): """ model_path: str = Field(..., description = "Model identifier or local path") - native_path_lease: Optional[str] = Field( - None, description = "Frontend-visible signed native path grant" - ) hf_token: Optional[str] = Field( None, description = "HuggingFace token for gated models" ) @@ -173,20 +157,12 @@ class LoadResponse(BaseModel): ) supports_reasoning: bool = Field( False, - description = "Whether model supports thinking/reasoning mode (enable_thinking or reasoning_effort)", - ) - reasoning_style: Literal["enable_thinking", "reasoning_effort"] = Field( - "enable_thinking", - description = "Reasoning control style: 'enable_thinking' (boolean) or 'reasoning_effort' (low|medium|high)", + description = "Whether model supports thinking/reasoning mode (enable_thinking)", ) reasoning_always_on: bool = Field( False, description = "Whether reasoning is always on (hardcoded tags, not toggleable)", ) - supports_preserve_thinking: bool = Field( - False, - description = "Whether the template understands the optional preserve_thinking kwarg (Qwen3.6-style)", - ) supports_tools: bool = Field( False, description = "Whether model supports tool calling (web search, etc.)", @@ -285,24 +261,12 @@ class InferenceStatusResponse(BaseModel): supports_reasoning: bool = Field( False, description = "Whether the active model supports reasoning/thinking mode" ) - reasoning_style: Literal["enable_thinking", "reasoning_effort"] = Field( - "enable_thinking", - description = "Reasoning control style: 'enable_thinking' (boolean) or 'reasoning_effort' (low|medium|high)", - ) reasoning_always_on: bool = Field( False, description = "Whether reasoning is always on (not toggleable)" ) - supports_preserve_thinking: bool = Field( - False, - description = "Whether the active model's template understands the optional preserve_thinking kwarg", - ) supports_tools: bool = Field( False, description = "Whether the active model supports tool calling" ) - chat_template: Optional[str] = Field( - None, - description = "Jinja2 chat template string for the active model", - ) context_length: Optional[int] = Field( None, description = "Context length of the active model" ) @@ -416,10 +380,7 @@ def _validate_role_shape(self) -> "ChatMessage": if self.name is not None and self.role != "tool": raise ValueError('"name" is only valid on role="tool" messages.') - # Per-role content requirements. OpenAI-compatible clients may send - # ``content=""`` for image-only turns when the image travels in a - # companion field such as Studio's ``image_base64`` extension, so treat - # empty strings as present content for user/system messages. + # Per-role content requirements. if self.role == "tool": if not self.tool_call_id: raise ValueError( @@ -434,8 +395,10 @@ def _validate_role_shape(self) -> "ChatMessage": 'role="assistant" messages require either "content" or "tool_calls".' ) else: # "user" | "system" - if self.content is None or self.content == []: - raise ValueError(f'role="{self.role}" messages require "content".') + if not self.content: + raise ValueError( + f'role="{self.role}" messages require non-empty "content".' + ) return self @@ -518,14 +481,6 @@ class ChatCompletionRequest(BaseModel): None, description = "[x-unsloth] Enable/disable thinking/reasoning mode for supported models", ) - reasoning_effort: Optional[Literal["low", "medium", "high"]] = Field( - None, - description = "[x-unsloth] Reasoning effort level ('low'|'medium'|'high') for Harmony-style reasoning models (e.g. gpt-oss). Overrides enable_thinking when the active model uses reasoning_effort style.", - ) - preserve_thinking: Optional[bool] = Field( - None, - description = "[x-unsloth] When true, keep historical blocks from past assistant turns in the prompt (Qwen3.6 templates). Independent of enable_thinking / reasoning_effort.", - ) enable_tools: Optional[bool] = Field( None, description = "[x-unsloth] Enable tool calling for supported models", @@ -552,10 +507,6 @@ class ChatCompletionRequest(BaseModel): None, description = "[x-unsloth] Session/thread ID for scoping tool execution sandbox.", ) - cancel_id: Optional[str] = Field( - None, - description = "[x-unsloth] Per-request cancellation token. Frontend sends a fresh UUID per run so /inference/cancel matches one specific generation.", - ) # ── Streaming response chunks ──────────────────────────────────── @@ -1017,7 +968,6 @@ class AnthropicMessagesRequest(BaseModel): enable_tools: Optional[bool] = None enabled_tools: Optional[list[str]] = None session_id: Optional[str] = None - cancel_id: Optional[str] = None model_config = {"extra": "allow"} diff --git a/studio/backend/models/training.py b/studio/backend/models/training.py index 8127af1ee6..07a306ca39 100644 --- a/studio/backend/models/training.py +++ b/studio/backend/models/training.py @@ -16,11 +16,8 @@ class TrainingStartRequest(BaseModel): model_name: str = Field( ..., description = "Model identifier (e.g., 'unsloth/llama-3-8b-bnb-4bit')" ) - training_type: Literal["LoRA/QLoRA", "Full Finetuning", "Continued Pretraining"] = ( - Field( - ..., - description = "Training type: 'LoRA/QLoRA', 'Full Finetuning', or 'Continued Pretraining'", - ) + training_type: str = Field( + ..., description = "Training type: 'LoRA/QLoRA' or 'Full Finetuning'" ) hf_token: Optional[str] = Field(None, description = "HuggingFace token") load_in_4bit: bool = Field(True, description = "Load model in 4-bit quantization") @@ -89,13 +86,6 @@ def _compat_split(cls, values: Any) -> Any: packing: bool = Field(False, description = "Enable sequence packing") optim: str = Field("adamw_8bit", description = "Optimizer") lr_scheduler_type: str = Field("linear", description = "Learning rate scheduler type") - embedding_learning_rate: Optional[float] = Field( - None, - gt = 0, - lt = 1.0, - description = "Separate learning rate for embedding matrices (CPT). " - "Must be in (0, 1). Should be 2-10x smaller than the main learning rate.", - ) # LoRA parameters use_lora: bool = Field(True, description = "Use LoRA (derived from training_type)") @@ -137,9 +127,6 @@ def _compat_split(cls, values: Any) -> Any: wandb_project: Optional[str] = Field(None, description = "W&B project name") enable_tensorboard: bool = Field(False, description = "Enable TensorBoard logging") tensorboard_dir: Optional[str] = Field(None, description = "TensorBoard directory") - resume_from_checkpoint: Optional[str] = Field( - None, description = "Saved training output directory to resume from" - ) # GPU selection gpu_ids: Optional[List[int]] = Field( @@ -233,8 +220,6 @@ class TrainingRunSummary(BaseModel): duration_seconds: Optional[float] = None error_message: Optional[str] = None loss_sparkline: Optional[List[float]] = None - can_resume: bool = False - resumed_later: bool = False class TrainingRunListResponse(BaseModel): diff --git a/studio/backend/plugins/data-designer-github-repo-seed/README.md b/studio/backend/plugins/data-designer-github-repo-seed/README.md deleted file mode 100644 index 346d94b305..0000000000 --- a/studio/backend/plugins/data-designer-github-repo-seed/README.md +++ /dev/null @@ -1,73 +0,0 @@ -# data-designer-github-repo-seed - -A Data Designer seed-reader plugin for **Unsloth Studio** that scrapes real -GitHub data (issues, pull requests, commits) from one or more repositories -and hands it to the recipe pipeline as a seed dataset. - -Designed to ship with Studio as a default seed source so any user with a -GitHub token can build training datasets straight from live repos. - -## What it does - -Given a list of `owner/name` repos, a GitHub token, and a per-resource -`limit`, the plugin uses GitHub's GraphQL API to fetch issues, pull -requests, and/or commits, with labels, state, authors, and the first N -comments of each item, and materialises a single JSONL with uniform -columns so the rest of the recipe (LLM text / LLM structured / processors) -can treat it like any other seed table. - -| Column | Description | -|---------------|------------------------------------------------| -| `item_type` | `issue` / `pull` / `commit` | -| `repo` | `owner/name` | -| `number` | Issue/PR number, or commit SHA | -| `title` | Title (or commit message headline) | -| `body` | Issue/PR body (or full commit message) | -| `state` | `OPEN` / `CLOSED` / `MERGED` (empty for commit)| -| `author` | GitHub login of the author | -| `created_at` | ISO8601 | -| `closed_at` | ISO8601 (empty for commits) | -| `url` | Permalink | -| `labels` | List of label names | -| `comments` | First N comments concatenated | - -## Usage in a recipe - -```json -{ - "seed_config": { - "source": { - "seed_type": "github_repo", - "repos": ["unslothai/unsloth", "unslothai/unsloth-zoo"], - "token": "", - "item_types": ["issues", "pulls"], - "limit": 100, - "include_comments": true, - "max_comments_per_item": 30 - }, - "sampling_strategy": "shuffle", - "selection_strategy": null - } -} -``` - -Leave `token` empty to fall back to the server's `GH_TOKEN` / `GITHUB_TOKEN` -environment variable, useful when the recipe is published and shouldn't -carry a secret. - -## Auth - -A GitHub personal access token with `public_repo` scope is enough for public -repositories; `repo` scope is required for private ones. GraphQL requests -are rate-limit aware: the client inspects `x-ratelimit-*` headers and -sleeps until reset when the budget drops below a safety threshold. - -## Install - -Shipped as a default Studio plugin. For development: - -```bash -pip install -e . -``` - -Registered automatically via the `data_designer.plugins` entry point. diff --git a/studio/backend/plugins/data-designer-github-repo-seed/pyproject.toml b/studio/backend/plugins/data-designer-github-repo-seed/pyproject.toml deleted file mode 100644 index e232adc60c..0000000000 --- a/studio/backend/plugins/data-designer-github-repo-seed/pyproject.toml +++ /dev/null @@ -1,25 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 - -[build-system] -requires = ["setuptools>=68", "wheel"] -build-backend = "setuptools.build_meta" - -[project] -name = "data-designer-github-repo-seed" -version = "0.1.0" -description = "Unsloth Studio seed plugin that scrapes GitHub issues, PRs, and commits." -requires-python = ">=3.11" -dependencies = [ - "data-designer-engine>=0.5.4,<0.6", - "requests>=2.31", -] - -[project.entry-points."data_designer.plugins"] -github_repo_seed = "data_designer_github_repo_seed.plugin:github_repo_seed_plugin" - -[tool.setuptools] -package-dir = {"" = "src"} - -[tool.setuptools.packages.find] -where = ["src"] diff --git a/studio/backend/plugins/data-designer-github-repo-seed/src/data_designer_github_repo_seed/__init__.py b/studio/backend/plugins/data-designer-github-repo-seed/src/data_designer_github_repo_seed/__init__.py deleted file mode 100644 index f57af4c6c3..0000000000 --- a/studio/backend/plugins/data-designer-github-repo-seed/src/data_designer_github_repo_seed/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 - -# Intentionally empty. Data-designer loads submodules lazily via qualified names -# (impl_qualified_name / config_qualified_name in plugin.py), so importing this -# package must NOT touch modules that depend on data_designer.engine.* during -# Studio's bootstrap (circular import). diff --git a/studio/backend/plugins/data-designer-github-repo-seed/src/data_designer_github_repo_seed/config.py b/studio/backend/plugins/data-designer-github-repo-seed/src/data_designer_github_repo_seed/config.py deleted file mode 100644 index 6b347c4f83..0000000000 --- a/studio/backend/plugins/data-designer-github-repo-seed/src/data_designer_github_repo_seed/config.py +++ /dev/null @@ -1,64 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 - -from __future__ import annotations - -from typing import Literal - -from pydantic import Field, field_validator, model_validator - -from data_designer.config.seed_source import SeedSource - - -class GitHubRepoSeedSource(SeedSource): - seed_type: Literal["github_repo"] = "github_repo" - - repos: list[str] = Field( - default_factory = list, - description = "List of GitHub repositories to scrape, each in `owner/name` form.", - ) - token: str = Field( - default = "", - description = "Personal access token. Leave blank to read GH_TOKEN / GITHUB_TOKEN from env at run time.", - ) - item_types: list[Literal["issues", "pulls", "commits"]] = Field( - default = ["issues", "pulls"], - description = "Which GitHub item types to fetch per repo.", - ) - limit: int = Field( - default = 100, - ge = 1, - le = 5000, - description = "Maximum items per repo per item type (e.g. limit=100 + ['issues','pulls'] => up to 200 items per repo).", - ) - include_comments: bool = Field( - default = True, - description = "Fetch the first N comments of each issue/PR and include them in the `comments` column.", - ) - max_comments_per_item: int = Field(default = 30, ge = 0, le = 200) - - @field_validator("repos") - @classmethod - def _validate_repos(cls, v: list[str]) -> list[str]: - out: list[str] = [] - for r in v or []: - r = r.strip() - if not r: - continue - if r.count("/") != 1 or not all(r.split("/")): - raise ValueError(f"Each repo must be `owner/name`; got {r!r}") - out.append(r) - return out - - @field_validator("item_types") - @classmethod - def _validate_item_types(cls, v: list[str]) -> list[str]: - if not v: - raise ValueError("item_types must not be empty") - return list(dict.fromkeys(v)) - - @model_validator(mode = "after") - def _ensure_repos(self) -> "GitHubRepoSeedSource": - if not self.repos: - raise ValueError("At least one repo is required") - return self diff --git a/studio/backend/plugins/data-designer-github-repo-seed/src/data_designer_github_repo_seed/impl.py b/studio/backend/plugins/data-designer-github-repo-seed/src/data_designer_github_repo_seed/impl.py deleted file mode 100644 index 5a38e26d6b..0000000000 --- a/studio/backend/plugins/data-designer-github-repo-seed/src/data_designer_github_repo_seed/impl.py +++ /dev/null @@ -1,83 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 - -from __future__ import annotations - -import hashlib -import tempfile -import threading -from pathlib import Path -from typing import Optional - -import data_designer.lazy_heavy_imports as lazy -from data_designer.engine.resources.seed_reader import SeedReader - -from .config import GitHubRepoSeedSource -from .scraper import ScrapeConfig, materialize_to_jsonl - - -# In-process cache mapping a stable config signature to the JSONL materialization -# path. A single recipe job invokes the seed reader multiple times (validation, -# preview, per-column sampling), and the default flow re-scrapes the repo on -# every call: for a 2-repo preview that is ~15s of redundant GitHub GraphQL -# traffic before any generation fires. Memoize the materialization so the second -# and third passes reuse the file the first pass wrote. Cache key excludes the -# raw token and uses a short SHA-256 digest so token values never hit memory -# twice and token rotation invalidates cleanly. -_SCRAPE_CACHE: dict[tuple, str] = {} -_SCRAPE_CACHE_LOCK = threading.Lock() - - -def _scrape_cache_key(cfg: ScrapeConfig) -> tuple: - token_digest = hashlib.sha256( - (cfg.token or "").encode("utf-8"), - ).hexdigest()[:16] - return ( - tuple(cfg.repos), - tuple(cfg.item_types), - cfg.limit, - bool(cfg.include_comments), - cfg.max_comments_per_item, - token_digest, - ) - - -def _lookup_cached_scrape(key: tuple) -> Optional[str]: - with _SCRAPE_CACHE_LOCK: - path = _SCRAPE_CACHE.get(key) - if path and Path(path).exists(): - return path - # Stale entry (tmp cleanup, user restarted, ...); drop it so the caller - # materializes a fresh file rather than returning a dangling path. - if path: - with _SCRAPE_CACHE_LOCK: - _SCRAPE_CACHE.pop(key, None) - return None - - -def _store_cached_scrape(key: tuple, path: str) -> None: - with _SCRAPE_CACHE_LOCK: - _SCRAPE_CACHE[key] = path - - -class GitHubRepoSeedReader(SeedReader[GitHubRepoSeedSource]): - def create_duckdb_connection(self): - return lazy.duckdb.connect() - - def get_dataset_uri(self) -> str: - out_dir = Path(tempfile.gettempdir()) / "studio-github-repo-seed" - cfg = ScrapeConfig( - repos = list(self.source.repos), - token = self.source.token, - item_types = list(self.source.item_types), - limit = self.source.limit, - include_comments = self.source.include_comments, - max_comments_per_item = self.source.max_comments_per_item, - ) - cache_key = _scrape_cache_key(cfg) - cached_path = _lookup_cached_scrape(cache_key) - if cached_path is not None: - return cached_path - path = materialize_to_jsonl(cfg, out_dir) - _store_cached_scrape(cache_key, str(path)) - return str(path) diff --git a/studio/backend/plugins/data-designer-github-repo-seed/src/data_designer_github_repo_seed/plugin.py b/studio/backend/plugins/data-designer-github-repo-seed/src/data_designer_github_repo_seed/plugin.py deleted file mode 100644 index f87dbd0507..0000000000 --- a/studio/backend/plugins/data-designer-github-repo-seed/src/data_designer_github_repo_seed/plugin.py +++ /dev/null @@ -1,10 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 - -from data_designer.plugins.plugin import Plugin, PluginType - -github_repo_seed_plugin = Plugin( - impl_qualified_name = "data_designer_github_repo_seed.impl.GitHubRepoSeedReader", - config_qualified_name = "data_designer_github_repo_seed.config.GitHubRepoSeedSource", - plugin_type = PluginType.SEED_READER, -) diff --git a/studio/backend/plugins/data-designer-github-repo-seed/src/data_designer_github_repo_seed/scraper.py b/studio/backend/plugins/data-designer-github-repo-seed/src/data_designer_github_repo_seed/scraper.py deleted file mode 100644 index d768fe37be..0000000000 --- a/studio/backend/plugins/data-designer-github-repo-seed/src/data_designer_github_repo_seed/scraper.py +++ /dev/null @@ -1,236 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 - -"""Multi-repo GitHub scraper for the Studio seed plugin. - -Drives the GraphQL-based scraper in `scraper_impl/` per repo. Each repo is -scraped with a trial_limits cap so we stop at `limit` items per resource. -After scraping, we read the per-resource JSONL shards and flatten them into -a single unified JSONL with stable columns (`item_type`, `repo`, `number`, -`title`, `body`, ...). -""" - -from __future__ import annotations - -import json -import os -import sys -import time -import uuid -from dataclasses import dataclass -from pathlib import Path - -# Defer scraper_impl imports until `scrape()` runs with a resolved token. -_IMPL_DIR = Path(__file__).parent / "scraper_impl" - - -def _ensure_impl_on_path() -> None: - if str(_IMPL_DIR) not in sys.path: - sys.path.insert(0, str(_IMPL_DIR)) - - -def _load_impl(): - _ensure_impl_on_path() - import importlib - - gh_client = importlib.import_module("gh_client") # type: ignore - scraper_mod = importlib.import_module("scraper") # type: ignore - return gh_client.GitHubClient, scraper_mod.RepoScraper - - -@dataclass -class ScrapeConfig: - repos: list[str] - token: str - item_types: list[str] - limit: int - include_comments: bool - max_comments_per_item: int - - -def _resolve_token(token: str) -> str: - tok = token or os.environ.get("GH_TOKEN", "") or os.environ.get("GITHUB_TOKEN", "") - if not tok: - raise ValueError( - "GitHub token is required. Set it in the recipe config or the GH_TOKEN / GITHUB_TOKEN env var." - ) - return tok - - -def _read_jsonl(path: Path, max_rows: int | None = None): - if not path.exists(): - return - with path.open(encoding = "utf-8") as f: - for i, line in enumerate(f): - if not line.strip(): - continue - if max_rows is not None and i >= max_rows: - return - try: - yield json.loads(line) - except json.JSONDecodeError: - continue - - -def _flatten_issue_row(r: dict, repo: str, include_comments: bool, max_c: int) -> dict: - labels = [ - l.get("name") - for l in (r.get("labels", {}) or {}).get("nodes", []) - if l.get("name") - ] - comments_nodes = (r.get("comments") or {}).get("nodes") or [] - comments_text = "" - if include_comments and comments_nodes: - kept = comments_nodes[:max_c] - comments_text = "\n\n".join( - f"[{(c.get('author') or {}).get('login', '?')}]: {c.get('body') or ''}" - for c in kept - ) - return { - "item_type": "issue", - "repo": repo, - "number": r.get("number"), - "title": r.get("title") or "", - "body": r.get("body") or "", - "state": r.get("state") or "", - "author": (r.get("author") or {}).get("login", ""), - "created_at": r.get("createdAt") or "", - "closed_at": r.get("closedAt") or "", - "url": r.get("url") or r.get("permalink") or "", - "labels": labels, - "comments": comments_text, - } - - -def _flatten_pr_row(r: dict, repo: str, include_comments: bool, max_c: int) -> dict: - labels = [ - l.get("name") - for l in (r.get("labels", {}) or {}).get("nodes", []) - if l.get("name") - ] - comments_nodes = (r.get("comments") or {}).get("nodes") or [] - comments_text = "" - if include_comments and comments_nodes: - kept = comments_nodes[:max_c] - comments_text = "\n\n".join( - f"[{(c.get('author') or {}).get('login', '?')}]: {c.get('body') or ''}" - for c in kept - ) - return { - "item_type": "pull", - "repo": repo, - "number": r.get("number"), - "title": r.get("title") or "", - "body": r.get("body") or "", - "state": r.get("state") or "", - "author": (r.get("author") or {}).get("login", ""), - "created_at": r.get("createdAt") or "", - "closed_at": r.get("closedAt") or "", - "url": r.get("url") or r.get("permalink") or "", - "labels": labels, - "comments": comments_text, - } - - -def _flatten_commit_row(r: dict, repo: str) -> dict: - msg = r.get("messageHeadline") or r.get("message") or "" - body = r.get("messageBody") or r.get("message") or msg - author = r.get("author") or {} - return { - "item_type": "commit", - "repo": repo, - "number": r.get("oid") or r.get("sha") or "", - "title": msg, - "body": body, - "state": "", - "author": (author.get("user") or {}).get("login") or author.get("name", ""), - "created_at": (author.get("date") or r.get("committedDate") or ""), - "closed_at": "", - "url": r.get("url") or "", - "labels": [], - "comments": "", - } - - -def scrape(cfg: ScrapeConfig, base_dir: Path): - token = _resolve_token(cfg.token) - GitHubClient, RepoScraper = _load_impl() - client = GitHubClient(token = token) - base_dir.mkdir(parents = True, exist_ok = True) - - # Per-resource trial limits. limit <= 0 means "all": use a very large cap. - effective_limit = cfg.limit if cfg.limit and cfg.limit > 0 else 1_000_000 - trial_limits: dict[str, int] = {} - if "issues" in cfg.item_types: - trial_limits["issues"] = effective_limit - if "pulls" in cfg.item_types: - trial_limits["pull_requests"] = effective_limit - if "commits" in cfg.item_types: - trial_limits["commits"] = effective_limit - - all_rows: list[dict] = [] - for repo in cfg.repos: - owner, name = repo.split("/", 1) - scraper = RepoScraper( - owner = owner, - name = name, - base_dir = base_dir, - client = client, - trial_limits = trial_limits, - light = True, - ) - try: - repo_meta = scraper.scrape_repo_meta() - if "issues" in cfg.item_types: - scraper.scrape_issues() - if "pulls" in cfg.item_types: - scraper.scrape_prs() - if "commits" in cfg.item_types: - default_ref = repo_meta.get("defaultBranchRef") or {} - default_branch = ( - default_ref.get("name") if isinstance(default_ref, dict) else None - ) - branch = ( - f"refs/heads/{default_branch}" - if default_branch - else "refs/heads/main" - ) - scraper.scrape_commits(branch = branch) - finally: - scraper.close() - - read_cap = cfg.limit if cfg.limit and cfg.limit > 0 else None - repo_dir = base_dir / f"{owner}__{name}" - if "issues" in cfg.item_types: - for row in _read_jsonl(repo_dir / "issues.jsonl", read_cap): - all_rows.append( - _flatten_issue_row( - row, repo, cfg.include_comments, cfg.max_comments_per_item - ) - ) - if "pulls" in cfg.item_types: - for row in _read_jsonl(repo_dir / "pull_requests.jsonl", read_cap): - all_rows.append( - _flatten_pr_row( - row, repo, cfg.include_comments, cfg.max_comments_per_item - ) - ) - if "commits" in cfg.item_types: - for row in _read_jsonl(repo_dir / "commits.jsonl", read_cap): - all_rows.append(_flatten_commit_row(row, repo)) - - return all_rows - - -def materialize_to_jsonl(cfg: ScrapeConfig, out_dir: Path) -> Path: - out_dir.mkdir(parents = True, exist_ok = True) - tag = "-".join(r.replace("/", "__") for r in cfg.repos)[:120] - kinds = "-".join(cfg.item_types) - run_id = f"{int(time.time())}-{uuid.uuid4().hex[:12]}" - fname = f"github_{tag}__{kinds}__{cfg.limit}_{run_id}.jsonl" - out = out_dir / fname - rows = scrape(cfg, out_dir / "raw-runs" / run_id) - with out.open("w", encoding = "utf-8") as f: - for r in rows: - f.write(json.dumps(r, ensure_ascii = False) + "\n") - return out diff --git a/studio/backend/plugins/data-designer-github-repo-seed/src/data_designer_github_repo_seed/scraper_impl/__init__.py b/studio/backend/plugins/data-designer-github-repo-seed/src/data_designer_github_repo_seed/scraper_impl/__init__.py deleted file mode 100644 index 32014236c6..0000000000 --- a/studio/backend/plugins/data-designer-github-repo-seed/src/data_designer_github_repo_seed/scraper_impl/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 diff --git a/studio/backend/plugins/data-designer-github-repo-seed/src/data_designer_github_repo_seed/scraper_impl/gh_client.py b/studio/backend/plugins/data-designer-github-repo-seed/src/data_designer_github_repo_seed/scraper_impl/gh_client.py deleted file mode 100644 index dd2de2f5ce..0000000000 --- a/studio/backend/plugins/data-designer-github-repo-seed/src/data_designer_github_repo_seed/scraper_impl/gh_client.py +++ /dev/null @@ -1,248 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 - -"""GitHub API client with rate-limit awareness, retry, and dual REST/GraphQL support.""" - -from __future__ import annotations - -import json -import os -import time -import logging -from typing import Any, Dict, Iterable, Iterator, List, Optional - -import requests - -log = logging.getLogger("gh_client") - -GRAPHQL_URL = "https://api.github.com/graphql" -REST_BASE = "https://api.github.com" - -BASE_HEADERS = { - "Accept": "application/vnd.github+json", - "X-GitHub-Api-Version": "2022-11-28", - "User-Agent": "github-data-gatherer/1.0", -} - - -class RateLimitError(Exception): - pass - - -class GitHubClient: - def __init__( - self, - min_remaining_graphql: int = 100, - min_remaining_rest: int = 100, - token: str | None = None, - ): - token = token or os.environ.get("GH_TOKEN") or os.environ.get("GITHUB_TOKEN") - if not token: - raise RuntimeError("GH_TOKEN not set in environment") - self.session = requests.Session() - self.session.headers.update( - {**BASE_HEADERS, "Authorization": f"Bearer {token}"} - ) - self.min_remaining_graphql = min_remaining_graphql - self.min_remaining_rest = min_remaining_rest - self.graphql_remaining: Optional[int] = None - self.graphql_reset: Optional[int] = None - self.rest_remaining: Optional[int] = None - self.rest_reset: Optional[int] = None - self.calls_graphql = 0 - self.calls_rest = 0 - self.retry_count = 0 - - def _sleep_until(self, reset_ts: int, buffer_s: int = 10) -> None: - now = int(time.time()) - wait = max(0, reset_ts - now) + buffer_s - log.warning("Rate limit hit. Sleeping %ds until reset.", wait) - time.sleep(wait) - - def _check_rate_and_wait(self, kind: str) -> None: - if kind == "graphql": - remaining = self.graphql_remaining - reset = self.graphql_reset - min_remaining = self.min_remaining_graphql - else: - remaining = self.rest_remaining - reset = self.rest_reset - min_remaining = self.min_remaining_rest - if remaining is not None and remaining < min_remaining: - if reset: - self._sleep_until(reset) - # Reset remaining so we don't spin - if kind == "graphql": - self.graphql_remaining = None - else: - self.rest_remaining = None - - def graphql( - self, - query: str, - variables: Optional[Dict[str, Any]] = None, - max_retries: int = 20, - ) -> Dict[str, Any]: - self._check_rate_and_wait("graphql") - backoff = 2 - last_err = None - for attempt in range(max_retries): - try: - r = self.session.post( - GRAPHQL_URL, - json = {"query": query, "variables": variables or {}}, - timeout = 120, - ) - self.calls_graphql += 1 - # Update rate info from response headers - rem = r.headers.get("X-RateLimit-Remaining") - rst = r.headers.get("X-RateLimit-Reset") - if rem is not None: - try: - self.graphql_remaining = int(rem) - except ValueError: - pass - if rst is not None: - try: - self.graphql_reset = int(rst) - except ValueError: - pass - if r.status_code in (502, 503, 504): - log.warning("GraphQL %s transient, retrying", r.status_code) - time.sleep(backoff) - backoff = min(backoff * 2, 60) - continue - if r.status_code == 403 or r.status_code == 429: - # Check for secondary/abuse - retry_after = r.headers.get("Retry-After") - if retry_after: - t = int(retry_after) - log.warning("Secondary rate limit. Sleep %ds.", t) - time.sleep(t + 2) - continue - if self.graphql_reset: - self._sleep_until(self.graphql_reset) - continue - time.sleep(60) - continue - r.raise_for_status() - data = r.json() - if "errors" in data and data["errors"]: - # Surface errors but allow partial data - errs = data["errors"] - # Retry on RATE_LIMITED - for e in errs: - if e.get("type") == "RATE_LIMITED": - self._sleep_until( - (self.graphql_reset or int(time.time()) + 60) - ) - break - else: - # No rate-limit error, log and return partial - log.warning("GraphQL errors: %s", json.dumps(errs)[:400]) - return data - continue - return data - except requests.RequestException as e: - last_err = e - log.warning("GraphQL network error: %s. Retry.", e) - time.sleep(backoff) - backoff = min(backoff * 2, 60) - raise RuntimeError(f"GraphQL failed after {max_retries} retries: {last_err}") - - def rest( - self, - method: str, - path: str, - params: Optional[Dict[str, Any]] = None, - json_body: Optional[Dict[str, Any]] = None, - max_retries: int = 6, - ) -> requests.Response: - self._check_rate_and_wait("rest") - if path.startswith("http"): - url = path - else: - url = REST_BASE + path - backoff = 2 - last_err = None - for attempt in range(max_retries): - try: - r = self.session.request( - method, url, params = params, json = json_body, timeout = 120 - ) - self.calls_rest += 1 - rem = r.headers.get("X-RateLimit-Remaining") - rst = r.headers.get("X-RateLimit-Reset") - if rem is not None: - try: - self.rest_remaining = int(rem) - except ValueError: - pass - if rst is not None: - try: - self.rest_reset = int(rst) - except ValueError: - pass - if r.status_code in (502, 503, 504): - log.warning("REST %s transient, retrying", r.status_code) - time.sleep(backoff) - backoff = min(backoff * 2, 60) - continue - if r.status_code in (403, 429): - retry_after = r.headers.get("Retry-After") - if retry_after: - t = int(retry_after) - log.warning("Secondary rate limit on REST. Sleep %ds.", t) - time.sleep(t + 2) - continue - # Check if primary rate - if self.rest_remaining == 0 and self.rest_reset: - self._sleep_until(self.rest_reset) - continue - log.warning("REST 403/429, sleep 60") - time.sleep(60) - continue - return r - except requests.RequestException as e: - last_err = e - log.warning("REST network error: %s. Retry.", e) - time.sleep(backoff) - backoff = min(backoff * 2, 60) - raise RuntimeError(f"REST failed after {max_retries} retries: {last_err}") - - def rest_paginate( - self, path: str, params: Optional[Dict[str, Any]] = None, per_page: int = 100 - ) -> Iterator[dict]: - params = dict(params or {}) - params.setdefault("per_page", per_page) - url = path - while True: - r = self.rest("GET", url, params = params if url == path else None) - if r.status_code != 200: - log.error( - "REST paginate got %s at %s: %s", r.status_code, url, r.text[:200] - ) - return - items = r.json() - if isinstance(items, dict): - # Some endpoints return dict with list field - items = items.get("items", []) - for it in items: - yield it - # Follow link header - link = r.headers.get("Link", "") - nxt = None - for part in link.split(","): - if 'rel="next"' in part: - nxt = part.split(";")[0].strip().strip("<>") - break - if not nxt: - return - url = nxt - params = None - - def rate_snapshot(self) -> Dict[str, Any]: - r = self.rest("GET", "/rate_limit") - if r.status_code == 200: - return r.json() - return {} diff --git a/studio/backend/plugins/data-designer-github-repo-seed/src/data_designer_github_repo_seed/scraper_impl/queries.py b/studio/backend/plugins/data-designer-github-repo-seed/src/data_designer_github_repo_seed/scraper_impl/queries.py deleted file mode 100644 index 9dc7613db5..0000000000 --- a/studio/backend/plugins/data-designer-github-repo-seed/src/data_designer_github_repo_seed/scraper_impl/queries.py +++ /dev/null @@ -1,685 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 - -"""GraphQL queries for GitHub data scraping. - -GitHub's GraphQL rejects queries that define unused fragments, so each query -only includes the fragments it actually references. -""" - -# ---- Fragments (kept as raw strings, composed per query) ---- -F_ACTOR = """ -fragment ActorFields on Actor { - __typename - login - url - avatarUrl - ... on User { id databaseId name } - ... on Bot { id databaseId } - ... on Organization { id databaseId name } -} -""" - -F_LABEL = """ -fragment LabelFields on Label { - id - name - color - description - createdAt -} -""" - -F_TIMELINE = """ -fragment TimelineItem on IssueTimelineItems { - __typename - ... on Node { id } - ... on AddedToProjectEvent { createdAt actor { ...ActorFields } } - ... on AssignedEvent { createdAt actor { ...ActorFields } assignee { __typename ... on User { login } ... on Bot { login } } } - ... on ClosedEvent { createdAt actor { ...ActorFields } stateReason closer { __typename ... on Commit { oid url } ... on PullRequest { number url } } } - ... on CommentDeletedEvent { createdAt actor { ...ActorFields } } - ... on ConnectedEvent { createdAt actor { ...ActorFields } source { __typename ... on Issue { number url repository { nameWithOwner } } ... on PullRequest { number url repository { nameWithOwner } } } subject { __typename ... on Issue { number url } ... on PullRequest { number url } } } - ... on ConvertedNoteToIssueEvent { createdAt actor { ...ActorFields } } - ... on CrossReferencedEvent { createdAt actor { ...ActorFields } isCrossRepository willCloseTarget source { __typename ... on Issue { number url repository { nameWithOwner } title } ... on PullRequest { number url repository { nameWithOwner } title } } } - ... on DemilestonedEvent { createdAt actor { ...ActorFields } milestoneTitle } - ... on DisconnectedEvent { createdAt actor { ...ActorFields } subject { __typename ... on Issue { number url } ... on PullRequest { number url } } source { __typename ... on Issue { number url } ... on PullRequest { number url } } } - ... on IssueComment { id databaseId createdAt updatedAt author { ...ActorFields } body url reactionGroups { content reactors { totalCount } } } - ... on LabeledEvent { createdAt actor { ...ActorFields } label { name color } } - ... on LockedEvent { createdAt actor { ...ActorFields } lockReason } - ... on MarkedAsDuplicateEvent { createdAt actor { ...ActorFields } canonical { __typename ... on Issue { number url } ... on PullRequest { number url } } } - ... on MentionedEvent { createdAt actor { ...ActorFields } } - ... on MilestonedEvent { createdAt actor { ...ActorFields } milestoneTitle } - ... on MovedColumnsInProjectEvent { createdAt actor { ...ActorFields } } - ... on PinnedEvent { createdAt actor { ...ActorFields } } - ... on ReferencedEvent { createdAt actor { ...ActorFields } commit { oid url } commitRepository { nameWithOwner } } - ... on RemovedFromProjectEvent { createdAt actor { ...ActorFields } } - ... on RenamedTitleEvent { createdAt actor { ...ActorFields } previousTitle currentTitle } - ... on ReopenedEvent { createdAt actor { ...ActorFields } } - ... on SubscribedEvent { createdAt actor { ...ActorFields } } - ... on TransferredEvent { createdAt actor { ...ActorFields } fromRepository { nameWithOwner } } - ... on UnassignedEvent { createdAt actor { ...ActorFields } assignee { __typename ... on User { login } ... on Bot { login } } } - ... on UnlabeledEvent { createdAt actor { ...ActorFields } label { name color } } - ... on UnlockedEvent { createdAt actor { ...ActorFields } } - ... on UnmarkedAsDuplicateEvent { createdAt actor { ...ActorFields } } - ... on UnpinnedEvent { createdAt actor { ...ActorFields } } - ... on UnsubscribedEvent { createdAt actor { ...ActorFields } } - ... on UserBlockedEvent { createdAt actor { ...ActorFields } blockDuration } -} -""" - -F_PR_TIMELINE = """ -fragment PRTimelineItem on PullRequestTimelineItems { - __typename - ... on Node { id } - ... on AssignedEvent { createdAt actor { ...ActorFields } assignee { __typename ... on User { login } ... on Bot { login } } } - ... on AutoMergeDisabledEvent { createdAt actor { ...ActorFields } reason } - ... on AutoMergeEnabledEvent { createdAt actor { ...ActorFields } } - ... on AutoRebaseEnabledEvent { createdAt actor { ...ActorFields } } - ... on AutoSquashEnabledEvent { createdAt actor { ...ActorFields } } - ... on AutomaticBaseChangeFailedEvent { createdAt actor { ...ActorFields } oldBase newBase } - ... on AutomaticBaseChangeSucceededEvent { createdAt actor { ...ActorFields } oldBase newBase } - ... on BaseRefChangedEvent { createdAt actor { ...ActorFields } previousRefName currentRefName } - ... on BaseRefDeletedEvent { createdAt actor { ...ActorFields } baseRefName } - ... on BaseRefForcePushedEvent { createdAt actor { ...ActorFields } beforeCommit { oid } afterCommit { oid } ref { name } } - ... on ClosedEvent { createdAt actor { ...ActorFields } stateReason } - ... on CommentDeletedEvent { createdAt actor { ...ActorFields } } - ... on ConnectedEvent { createdAt actor { ...ActorFields } source { __typename ... on Issue { number url } ... on PullRequest { number url } } subject { __typename ... on Issue { number url } ... on PullRequest { number url } } } - ... on ConvertToDraftEvent { createdAt actor { ...ActorFields } } - ... on CrossReferencedEvent { createdAt actor { ...ActorFields } isCrossRepository willCloseTarget source { __typename ... on Issue { number url repository { nameWithOwner } title } ... on PullRequest { number url repository { nameWithOwner } title } } } - ... on DemilestonedEvent { createdAt actor { ...ActorFields } milestoneTitle } - ... on DeployedEvent { createdAt actor { ...ActorFields } } - ... on DeploymentEnvironmentChangedEvent { createdAt actor { ...ActorFields } } - ... on DisconnectedEvent { createdAt actor { ...ActorFields } subject { __typename ... on Issue { number url } ... on PullRequest { number url } } source { __typename ... on Issue { number url } ... on PullRequest { number url } } } - ... on HeadRefDeletedEvent { createdAt actor { ...ActorFields } headRefName } - ... on HeadRefForcePushedEvent { createdAt actor { ...ActorFields } beforeCommit { oid } afterCommit { oid } ref { name } } - ... on HeadRefRestoredEvent { createdAt actor { ...ActorFields } } - ... on IssueComment { id databaseId createdAt updatedAt author { ...ActorFields } body url reactionGroups { content reactors { totalCount } } } - ... on LabeledEvent { createdAt actor { ...ActorFields } label { name color } } - ... on LockedEvent { createdAt actor { ...ActorFields } lockReason } - ... on MarkedAsDuplicateEvent { createdAt actor { ...ActorFields } canonical { __typename ... on Issue { number url } ... on PullRequest { number url } } } - ... on MentionedEvent { createdAt actor { ...ActorFields } } - ... on MergedEvent { createdAt actor { ...ActorFields } commit { oid url } mergeRefName } - ... on MilestonedEvent { createdAt actor { ...ActorFields } milestoneTitle } - ... on MovedColumnsInProjectEvent { createdAt actor { ...ActorFields } } - ... on PinnedEvent { createdAt actor { ...ActorFields } } - ... on PullRequestCommit { commit { oid url message author { user { login } date } committedDate } } - ... on PullRequestCommitCommentThread { commit { oid } } - ... on PullRequestReview { id databaseId createdAt submittedAt author { ...ActorFields } body state url reactionGroups { content reactors { totalCount } } } - ... on PullRequestReviewThread { id isResolved isOutdated path line diffSide } - ... on PullRequestRevisionMarker { createdAt lastSeenCommit { oid } } - ... on ReadyForReviewEvent { createdAt actor { ...ActorFields } } - ... on ReferencedEvent { createdAt actor { ...ActorFields } commit { oid url } commitRepository { nameWithOwner } } - ... on RenamedTitleEvent { createdAt actor { ...ActorFields } previousTitle currentTitle } - ... on ReopenedEvent { createdAt actor { ...ActorFields } } - ... on ReviewDismissedEvent { createdAt actor { ...ActorFields } dismissalMessage previousReviewState } - ... on ReviewRequestRemovedEvent { createdAt actor { ...ActorFields } requestedReviewer { __typename ... on User { login } ... on Team { name } } } - ... on ReviewRequestedEvent { createdAt actor { ...ActorFields } requestedReviewer { __typename ... on User { login } ... on Team { name } } } - ... on SubscribedEvent { createdAt actor { ...ActorFields } } - ... on TransferredEvent { createdAt actor { ...ActorFields } fromRepository { nameWithOwner } } - ... on UnassignedEvent { createdAt actor { ...ActorFields } assignee { __typename ... on User { login } ... on Bot { login } } } - ... on UnlabeledEvent { createdAt actor { ...ActorFields } label { name color } } - ... on UnlockedEvent { createdAt actor { ...ActorFields } } - ... on UnmarkedAsDuplicateEvent { createdAt actor { ...ActorFields } } - ... on UnpinnedEvent { createdAt actor { ...ActorFields } } - ... on UnsubscribedEvent { createdAt actor { ...ActorFields } } - ... on UserBlockedEvent { createdAt actor { ...ActorFields } blockDuration } -} -""" - - -def _q(parts: list[str], body: str) -> str: - return "\n".join(parts + [body]) - - -ISSUES_PAGE_QUERY = _q( - [F_ACTOR, F_LABEL, F_TIMELINE], - """ -query IssuesPage($owner: String!, $name: String!, $first: Int!, $after: String) { - repository(owner: $owner, name: $name) { - issues(first: $first, after: $after, orderBy: {field: CREATED_AT, direction: ASC}) { - pageInfo { hasNextPage endCursor } - totalCount - nodes { - id databaseId number title body state stateReason - createdAt updatedAt closedAt - url - author { ...ActorFields } - editor { ...ActorFields } - labels(first: 50) { nodes { ...LabelFields } } - assignees(first: 20) { nodes { login id } } - milestone { title number state dueOn } - reactionGroups { content reactors { totalCount } } - comments(first: 100) { - totalCount - pageInfo { hasNextPage endCursor } - nodes { - id databaseId createdAt updatedAt url body - author { ...ActorFields } - editor { ...ActorFields } - reactionGroups { content reactors { totalCount } } - } - } - timelineItems(first: 100) { - totalCount - pageInfo { hasNextPage endCursor } - nodes { ...TimelineItem } - } - trackedInIssues(first: 20) { totalCount nodes { number url repository { nameWithOwner } } } - trackedIssues(first: 20) { totalCount nodes { number url repository { nameWithOwner } } } - } - } - } - rateLimit { cost remaining resetAt } -} -""", -) - -PRS_PAGE_QUERY = _q( - [F_ACTOR, F_LABEL, F_PR_TIMELINE], - """ -query PRsPage($owner: String!, $name: String!, $first: Int!, $after: String) { - repository(owner: $owner, name: $name) { - pullRequests(first: $first, after: $after, orderBy: {field: CREATED_AT, direction: ASC}) { - pageInfo { hasNextPage endCursor } - totalCount - nodes { - id databaseId number title body state isDraft - createdAt updatedAt closedAt mergedAt - url - headRefName headRefOid - baseRefName baseRefOid - additions deletions changedFiles - mergeable merged mergeStateStatus - author { ...ActorFields } - editor { ...ActorFields } - mergedBy { ...ActorFields } - labels(first: 50) { nodes { ...LabelFields } } - assignees(first: 20) { nodes { login id } } - milestone { title number state dueOn } - reactionGroups { content reactors { totalCount } } - closingIssuesReferences(first: 20) { totalCount nodes { number url repository { nameWithOwner } title } } - comments(first: 100) { - totalCount - pageInfo { hasNextPage endCursor } - nodes { - id databaseId createdAt updatedAt url body - author { ...ActorFields } - editor { ...ActorFields } - reactionGroups { content reactors { totalCount } } - } - } - reviewThreads(first: 50) { - totalCount - pageInfo { hasNextPage endCursor } - nodes { - id isResolved isOutdated path line diffSide - comments(first: 50) { - totalCount - pageInfo { hasNextPage endCursor } - nodes { - id databaseId createdAt updatedAt url body path diffHunk - author { ...ActorFields } - editor { ...ActorFields } - position originalPosition line originalLine - commit { oid } - reactionGroups { content reactors { totalCount } } - } - } - } - } - reviews(first: 50) { - totalCount - pageInfo { hasNextPage endCursor } - nodes { - id databaseId state createdAt submittedAt body url - author { ...ActorFields } - reactionGroups { content reactors { totalCount } } - } - } - commits(first: 100) { - totalCount - pageInfo { hasNextPage endCursor } - nodes { - commit { - oid - message - messageHeadline - committedDate - authoredDate - author { name email user { login } date } - committer { name email user { login } date } - additions deletions changedFilesIfAvailable - parents(first: 3) { nodes { oid } } - } - } - } - files(first: 100) { - totalCount - pageInfo { hasNextPage endCursor } - nodes { - path additions deletions changeType - } - } - timelineItems(first: 100) { - totalCount - pageInfo { hasNextPage endCursor } - nodes { ...PRTimelineItem } - } - } - } - } - rateLimit { cost remaining resetAt } -} -""", -) - -PRS_PAGE_QUERY_LIGHT = _q( - [F_ACTOR, F_LABEL], - """ -query PRsPageLight($owner: String!, $name: String!, $first: Int!, $after: String) { - repository(owner: $owner, name: $name) { - pullRequests(first: $first, after: $after, orderBy: {field: CREATED_AT, direction: ASC}) { - pageInfo { hasNextPage endCursor } - totalCount - nodes { - id databaseId number title body state isDraft - createdAt updatedAt closedAt mergedAt - url - author { ...ActorFields } - labels(first: 50) { nodes { ...LabelFields } } - comments(first: 30) { - totalCount - pageInfo { hasNextPage endCursor } - nodes { - id databaseId createdAt updatedAt url body - author { ...ActorFields } - } - } - } - } - } - rateLimit { cost remaining resetAt } -} -""", -) - -ISSUES_PAGE_QUERY_LIGHT = _q( - [F_ACTOR, F_LABEL], - """ -query IssuesPageLight($owner: String!, $name: String!, $first: Int!, $after: String) { - repository(owner: $owner, name: $name) { - issues(first: $first, after: $after, orderBy: {field: CREATED_AT, direction: ASC}) { - pageInfo { hasNextPage endCursor } - totalCount - nodes { - id databaseId number title body state - createdAt updatedAt closedAt - url - author { ...ActorFields } - labels(first: 50) { nodes { ...LabelFields } } - comments(first: 30) { - totalCount - pageInfo { hasNextPage endCursor } - nodes { - id databaseId createdAt updatedAt url body - author { ...ActorFields } - } - } - } - } - } - rateLimit { cost remaining resetAt } -} -""", -) - -ISSUE_COMMENTS_QUERY = _q( - [F_ACTOR], - """ -query IssueComments($owner: String!, $name: String!, $number: Int!, $after: String) { - repository(owner: $owner, name: $name) { - issueOrPullRequest(number: $number) { - __typename - ... on Issue { - comments(first: 100, after: $after) { - pageInfo { hasNextPage endCursor } - nodes { - id databaseId createdAt updatedAt url body - author { ...ActorFields } - editor { ...ActorFields } - reactionGroups { content reactors { totalCount } } - } - } - } - ... on PullRequest { - comments(first: 100, after: $after) { - pageInfo { hasNextPage endCursor } - nodes { - id databaseId createdAt updatedAt url body - author { ...ActorFields } - editor { ...ActorFields } - reactionGroups { content reactors { totalCount } } - } - } - } - } - } - rateLimit { cost remaining resetAt } -} -""", -) - -ISSUE_TIMELINE_QUERY = _q( - [F_ACTOR, F_TIMELINE], - """ -query IssueTimeline($owner: String!, $name: String!, $number: Int!, $after: String) { - repository(owner: $owner, name: $name) { - issue(number: $number) { - timelineItems(first: 100, after: $after) { - pageInfo { hasNextPage endCursor } - nodes { ...TimelineItem } - } - } - } - rateLimit { cost remaining resetAt } -} -""", -) - -PR_TIMELINE_QUERY = _q( - [F_ACTOR, F_PR_TIMELINE], - """ -query PRTimeline($owner: String!, $name: String!, $number: Int!, $after: String) { - repository(owner: $owner, name: $name) { - pullRequest(number: $number) { - timelineItems(first: 100, after: $after) { - pageInfo { hasNextPage endCursor } - nodes { ...PRTimelineItem } - } - } - } - rateLimit { cost remaining resetAt } -} -""", -) - -PR_COMMITS_QUERY = """ -query PRCommits($owner: String!, $name: String!, $number: Int!, $after: String) { - repository(owner: $owner, name: $name) { - pullRequest(number: $number) { - commits(first: 100, after: $after) { - pageInfo { hasNextPage endCursor } - nodes { - commit { - oid message messageHeadline committedDate authoredDate - author { name email user { login } date } - committer { name email user { login } date } - additions deletions changedFilesIfAvailable - parents(first: 3) { nodes { oid } } - } - } - } - } - } - rateLimit { cost remaining resetAt } -} -""" - -PR_FILES_QUERY = """ -query PRFiles($owner: String!, $name: String!, $number: Int!, $after: String) { - repository(owner: $owner, name: $name) { - pullRequest(number: $number) { - files(first: 100, after: $after) { - pageInfo { hasNextPage endCursor } - nodes { path additions deletions changeType } - } - } - } - rateLimit { cost remaining resetAt } -} -""" - -PR_REVIEW_THREADS_QUERY = _q( - [F_ACTOR], - """ -query PRReviewThreads($owner: String!, $name: String!, $number: Int!, $after: String) { - repository(owner: $owner, name: $name) { - pullRequest(number: $number) { - reviewThreads(first: 50, after: $after) { - pageInfo { hasNextPage endCursor } - nodes { - id isResolved isOutdated path line diffSide - comments(first: 50) { - totalCount - nodes { - id databaseId createdAt updatedAt url body path diffHunk - author { ...ActorFields } - editor { ...ActorFields } - position originalPosition line originalLine - commit { oid } - reactionGroups { content reactors { totalCount } } - } - } - } - } - } - } - rateLimit { cost remaining resetAt } -} -""", -) - -DISCUSSIONS_PAGE_QUERY = _q( - [F_ACTOR, F_LABEL], - """ -query DiscussionsPage($owner: String!, $name: String!, $first: Int!, $after: String) { - repository(owner: $owner, name: $name) { - discussions(first: $first, after: $after, orderBy: {field: CREATED_AT, direction: ASC}) { - pageInfo { hasNextPage endCursor } - totalCount - nodes { - id databaseId number title body - createdAt updatedAt url - author { ...ActorFields } - editor { ...ActorFields } - locked - answerChosenAt - closed closedAt - category { id name emoji description isAnswerable } - labels(first: 30) { nodes { ...LabelFields } } - upvoteCount - answer { id databaseId body author { ...ActorFields } createdAt url } - reactionGroups { content reactors { totalCount } } - comments(first: 50) { - totalCount - pageInfo { hasNextPage endCursor } - nodes { - id databaseId body createdAt updatedAt url - author { ...ActorFields } - editor { ...ActorFields } - upvoteCount - isAnswer - reactionGroups { content reactors { totalCount } } - replies(first: 50) { - totalCount - pageInfo { hasNextPage endCursor } - nodes { - id databaseId body createdAt updatedAt url - author { ...ActorFields } - editor { ...ActorFields } - reactionGroups { content reactors { totalCount } } - } - } - } - } - } - } - } - rateLimit { cost remaining resetAt } -} -""", -) - -DISCUSSION_COMMENTS_QUERY = _q( - [F_ACTOR], - """ -query DiscussionComments($owner: String!, $name: String!, $number: Int!, $after: String) { - repository(owner: $owner, name: $name) { - discussion(number: $number) { - comments(first: 50, after: $after) { - pageInfo { hasNextPage endCursor } - nodes { - id databaseId body createdAt updatedAt url - author { ...ActorFields } - editor { ...ActorFields } - upvoteCount - isAnswer - reactionGroups { content reactors { totalCount } } - replies(first: 50) { - totalCount - nodes { - id databaseId body createdAt updatedAt url - author { ...ActorFields } - editor { ...ActorFields } - reactionGroups { content reactors { totalCount } } - } - } - } - } - } - } - rateLimit { cost remaining resetAt } -} -""", -) - -DISCUSSION_REPLIES_QUERY = _q( - [F_ACTOR], - """ -query DiscussionReplies($commentId: ID!, $after: String) { - node(id: $commentId) { - ... on DiscussionComment { - replies(first: 50, after: $after) { - pageInfo { hasNextPage endCursor } - nodes { - id databaseId body createdAt updatedAt url - author { ...ActorFields } - editor { ...ActorFields } - reactionGroups { content reactors { totalCount } } - } - } - } - } - rateLimit { cost remaining resetAt } -} -""", -) - -COMMITS_PAGE_QUERY = """ -query CommitsPage($owner: String!, $name: String!, $first: Int!, $after: String, $branch: String!) { - repository(owner: $owner, name: $name) { - ref(qualifiedName: $branch) { - target { - ... on Commit { - history(first: $first, after: $after) { - pageInfo { hasNextPage endCursor } - totalCount - nodes { - oid - message - messageHeadline - committedDate - authoredDate - url - additions deletions changedFilesIfAvailable - author { name email date user { login id } } - committer { name email date user { login id } } - parents(first: 3) { nodes { oid } } - associatedPullRequests(first: 5) { nodes { number url state } } - } - } - } - } - } - } - rateLimit { cost remaining resetAt } -} -""" - -RELEASES_QUERY = _q( - [F_ACTOR], - """ -query Releases($owner: String!, $name: String!, $first: Int!, $after: String) { - repository(owner: $owner, name: $name) { - releases(first: $first, after: $after, orderBy: {field: CREATED_AT, direction: ASC}) { - pageInfo { hasNextPage endCursor } - nodes { - id databaseId name tagName description - createdAt publishedAt updatedAt - isDraft isPrerelease isLatest - url - author { ...ActorFields } - tagCommit { oid url } - reactionGroups { content reactors { totalCount } } - releaseAssets(first: 50) { - nodes { name contentType size downloadUrl createdAt updatedAt } - } - } - } - } - rateLimit { cost remaining resetAt } -} -""", -) - -LABELS_QUERY = _q( - [F_LABEL], - """ -query LabelsList($owner: String!, $name: String!, $first: Int!, $after: String) { - repository(owner: $owner, name: $name) { - labels(first: $first, after: $after) { - pageInfo { hasNextPage endCursor } - nodes { ...LabelFields } - } - } - rateLimit { cost remaining resetAt } -} -""", -) - -MILESTONES_QUERY = """ -query Milestones($owner: String!, $name: String!, $first: Int!, $after: String) { - repository(owner: $owner, name: $name) { - milestones(first: $first, after: $after) { - pageInfo { hasNextPage endCursor } - nodes { - id number title description state - createdAt updatedAt closedAt dueOn - creator { login } - } - } - } - rateLimit { cost remaining resetAt } -} -""" - -REPO_META_QUERY = """ -query RepoMeta($owner: String!, $name: String!) { - repository(owner: $owner, name: $name) { - id databaseId name nameWithOwner description url - createdAt updatedAt pushedAt - isArchived isDisabled isFork isPrivate - primaryLanguage { name } - languages(first: 20, orderBy: {field: SIZE, direction: DESC}) { - edges { size node { name } } - totalSize - } - stargazerCount forkCount watchers { totalCount } - diskUsage - licenseInfo { key name } - homepageUrl - defaultBranchRef { name } - } - rateLimit { cost remaining resetAt } -} -""" diff --git a/studio/backend/plugins/data-designer-github-repo-seed/src/data_designer_github_repo_seed/scraper_impl/scraper.py b/studio/backend/plugins/data-designer-github-repo-seed/src/data_designer_github_repo_seed/scraper_impl/scraper.py deleted file mode 100644 index 127129e18b..0000000000 --- a/studio/backend/plugins/data-designer-github-repo-seed/src/data_designer_github_repo_seed/scraper_impl/scraper.py +++ /dev/null @@ -1,756 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 - -"""Main scraper orchestration. Collects issues, PRs, discussions, commits, releases, etc. - -Resumable via state file. Writes JSONL shards under data/{repo}/{resource}.jsonl. -""" - -from __future__ import annotations - -import argparse -import json -import logging -import os -import subprocess -import sys -import time -from pathlib import Path -from typing import Any, Dict, Iterable, List, Optional, Tuple - -# Allow running as a module or script -THIS_DIR = Path(__file__).resolve().parent -if str(THIS_DIR) not in sys.path: - sys.path.insert(0, str(THIS_DIR)) - -from gh_client import GitHubClient -from state_store import JsonlWriter, StateStore -import queries as Q - -log = logging.getLogger("scraper") - - -def ts() -> str: - return time.strftime("%Y-%m-%d %H:%M:%S") - - -class RepoScraper: - def __init__( - self, - owner: str, - name: str, - base_dir: Path, - client: GitHubClient, - trial_limits: Optional[Dict[str, int]] = None, - light: bool = False, - ): - self.owner = owner - self.name = name - self.base_dir = base_dir - self.client = client - self.trial_limits = trial_limits or {} - # When light=True, use trimmed GraphQL queries (no reviewThreads, - # reviews, commits, timelineItems, files) so PR pages can be much - # larger without blowing GitHub's node-count ceiling. - self.light = light - self.repo_dir = base_dir / f"{owner}__{name}" - self.repo_dir.mkdir(parents = True, exist_ok = True) - self.state = StateStore(base_dir / "state" / f"{owner}__{name}.json") - - # Writers - self.writers: Dict[str, JsonlWriter] = {} - for key in ( - "issues", - "pull_requests", - "discussions", - "commits", - "releases", - "labels", - "milestones", - "pr_extra_comments", - "pr_extra_timeline", - "pr_extra_reviews", - "issue_extra_comments", - "issue_extra_timeline", - "discussion_extra_comments", - "discussion_extra_replies", - "repo_meta", - ): - self.writers[key] = JsonlWriter(self.repo_dir / f"{key}.jsonl") - - # ----- helpers ----- - def _trial_stop(self, key: str, counter: int) -> bool: - lim = self.trial_limits.get(key) - if lim is None: - return False - return counter >= lim - - def _log_rate(self, where: str, data: Dict[str, Any]) -> None: - rl = ( - data.get("data", {}).get("rateLimit") - if isinstance(data.get("data"), dict) - else None - ) - if rl: - log.debug( - "[%s] rate cost=%s remaining=%s resetAt=%s", - where, - rl.get("cost"), - rl.get("remaining"), - rl.get("resetAt"), - ) - - # ----- repo meta ----- - def scrape_repo_meta(self) -> Dict[str, Any]: - data = self.client.graphql( - Q.REPO_META_QUERY, {"owner": self.owner, "name": self.name} - ) - self._log_rate("repo_meta", data) - repo = data.get("data", {}).get("repository") or {} - repo["_fetchedAt"] = ts() - self.writers["repo_meta"].write(repo) - return repo - - # ----- issues ----- - def scrape_issues(self) -> int: - key = "issues" - cursor = self.state.get(f"{key}_cursor") - done = self.state.get(f"{key}_done", False) - if done: - log.info("%s/%s issues already complete", self.owner, self.name) - return 0 - total_new = 0 - page = 0 - # Light query skips heavy nested fields; safe at 50 per page. - # Clamp by trial_limit so e.g. limit=1 asks GitHub for first:1 - # instead of fetching a full 50-item page and discarding 49. - page_cap = 50 if self.light else 15 - trial_cap = self.trial_limits.get(key) - per_page = min(page_cap, trial_cap) if trial_cap and trial_cap > 0 else page_cap - while True: - page += 1 - vars_ = { - "owner": self.owner, - "name": self.name, - "first": per_page, - "after": cursor, - } - query = Q.ISSUES_PAGE_QUERY_LIGHT if self.light else Q.ISSUES_PAGE_QUERY - data = self.client.graphql(query, vars_) - self._log_rate("issues", data) - repo = (data.get("data") or {}).get("repository") or {} - issues = repo.get("issues") or {} - nodes = issues.get("nodes") or [] - for it in nodes: - it["_owner"] = self.owner - it["_repo"] = self.name - it["_fetchedAt"] = ts() - if not self.light: - if it.get("comments", {}).get("pageInfo", {}).get("hasNextPage"): - self._paginate_issue_comments( - it["number"], it["comments"]["pageInfo"]["endCursor"] - ) - if ( - it.get("timelineItems", {}) - .get("pageInfo", {}) - .get("hasNextPage") - ): - self._paginate_issue_timeline( - it["number"], - it["timelineItems"]["pageInfo"]["endCursor"], - ) - if self.writers[key].write(it): - total_new += 1 - info = issues.get("pageInfo") or {} - cursor = info.get("endCursor") - self.state.set(f"{key}_cursor", cursor) - log.info( - "[%s/%s] issues page %d (+%d) cursor=%s remaining=%s", - self.owner, - self.name, - page, - len(nodes), - str(cursor)[:20], - self.client.graphql_remaining, - ) - if self._trial_stop(key, total_new): - log.info("Trial limit reached for issues (%d)", total_new) - return total_new - if not info.get("hasNextPage"): - self.state.set(f"{key}_done", True) - break - return total_new - - def _paginate_issue_comments(self, number: int, after: str) -> None: - cur = after - while cur: - vars_ = { - "owner": self.owner, - "name": self.name, - "number": number, - "after": cur, - } - data = self.client.graphql(Q.ISSUE_COMMENTS_QUERY, vars_) - item = ((data.get("data") or {}).get("repository") or {}).get( - "issueOrPullRequest" - ) or {} - comments = item.get("comments") or {} - for c in comments.get("nodes") or []: - c["_owner"] = self.owner - c["_repo"] = self.name - c["_issueNumber"] = number - self.writers["issue_extra_comments"].write(c) - info = comments.get("pageInfo") or {} - cur = info.get("endCursor") if info.get("hasNextPage") else None - - def _paginate_issue_timeline(self, number: int, after: str) -> None: - cur = after - while cur: - vars_ = { - "owner": self.owner, - "name": self.name, - "number": number, - "after": cur, - } - data = self.client.graphql(Q.ISSUE_TIMELINE_QUERY, vars_) - item = ((data.get("data") or {}).get("repository") or {}).get("issue") or {} - tl = item.get("timelineItems") or {} - for ev in tl.get("nodes") or []: - ev["_owner"] = self.owner - ev["_repo"] = self.name - ev["_issueNumber"] = number - self.writers["issue_extra_timeline"].write(ev) - info = tl.get("pageInfo") or {} - cur = info.get("endCursor") if info.get("hasNextPage") else None - - # ----- PRs ----- - def scrape_prs(self) -> int: - key = "pull_requests" - cursor = self.state.get(f"{key}_cursor") - done = self.state.get(f"{key}_done", False) - if done: - log.info("%s/%s PRs already complete", self.owner, self.name) - return 0 - total_new = 0 - page = 0 - # Heavy nested PR query is capped at 3 per page (GitHub node-count - # ceiling); light query skips reviewThreads/reviews/commits/etc and - # can safely go to 25 per page. Clamp by trial_limit for small - # previews so limit=1 does not fetch a whole 25-item page. - page_cap = 25 if self.light else 3 - trial_cap = self.trial_limits.get(key) - per_page = min(page_cap, trial_cap) if trial_cap and trial_cap > 0 else page_cap - while True: - page += 1 - vars_ = { - "owner": self.owner, - "name": self.name, - "first": per_page, - "after": cursor, - } - query = Q.PRS_PAGE_QUERY_LIGHT if self.light else Q.PRS_PAGE_QUERY - data = self.client.graphql(query, vars_) - self._log_rate("prs", data) - repo = (data.get("data") or {}).get("repository") or {} - prs = repo.get("pullRequests") or {} - nodes = prs.get("nodes") or [] - for pr in nodes: - pr["_owner"] = self.owner - pr["_repo"] = self.name - pr["_fetchedAt"] = ts() - num = pr["number"] - if not self.light: - if pr.get("comments", {}).get("pageInfo", {}).get("hasNextPage"): - self._paginate_pr_comments( - num, pr["comments"]["pageInfo"]["endCursor"] - ) - if ( - pr.get("timelineItems", {}) - .get("pageInfo", {}) - .get("hasNextPage") - ): - self._paginate_pr_timeline( - num, pr["timelineItems"]["pageInfo"]["endCursor"] - ) - if pr.get("commits", {}).get("pageInfo", {}).get("hasNextPage"): - self._paginate_pr_commits( - num, pr["commits"]["pageInfo"]["endCursor"] - ) - if pr.get("files", {}).get("pageInfo", {}).get("hasNextPage"): - self._paginate_pr_files( - num, pr["files"]["pageInfo"]["endCursor"] - ) - if ( - pr.get("reviewThreads", {}) - .get("pageInfo", {}) - .get("hasNextPage") - ): - self._paginate_pr_review_threads( - num, pr["reviewThreads"]["pageInfo"]["endCursor"] - ) - if self.writers[key].write(pr): - total_new += 1 - info = prs.get("pageInfo") or {} - cursor = info.get("endCursor") - self.state.set(f"{key}_cursor", cursor) - log.info( - "[%s/%s] PRs page %d (+%d) cursor=%s remaining=%s", - self.owner, - self.name, - page, - len(nodes), - str(cursor)[:20], - self.client.graphql_remaining, - ) - if self._trial_stop(key, total_new): - log.info("Trial limit reached for PRs (%d)", total_new) - return total_new - if not info.get("hasNextPage"): - self.state.set(f"{key}_done", True) - break - return total_new - - def _paginate_pr_comments(self, number: int, after: str) -> None: - cur = after - while cur: - vars_ = { - "owner": self.owner, - "name": self.name, - "number": number, - "after": cur, - } - data = self.client.graphql(Q.ISSUE_COMMENTS_QUERY, vars_) - item = ((data.get("data") or {}).get("repository") or {}).get( - "issueOrPullRequest" - ) or {} - comments = item.get("comments") or {} - for c in comments.get("nodes") or []: - c["_owner"] = self.owner - c["_repo"] = self.name - c["_prNumber"] = number - self.writers["pr_extra_comments"].write(c) - info = comments.get("pageInfo") or {} - cur = info.get("endCursor") if info.get("hasNextPage") else None - - def _paginate_pr_timeline(self, number: int, after: str) -> None: - cur = after - while cur: - vars_ = { - "owner": self.owner, - "name": self.name, - "number": number, - "after": cur, - } - data = self.client.graphql(Q.PR_TIMELINE_QUERY, vars_) - item = ((data.get("data") or {}).get("repository") or {}).get( - "pullRequest" - ) or {} - tl = item.get("timelineItems") or {} - for ev in tl.get("nodes") or []: - ev["_owner"] = self.owner - ev["_repo"] = self.name - ev["_prNumber"] = number - self.writers["pr_extra_timeline"].write(ev) - info = tl.get("pageInfo") or {} - cur = info.get("endCursor") if info.get("hasNextPage") else None - - def _paginate_pr_commits(self, number: int, after: str) -> None: - cur = after - out_key = "pr_extra_commits" - if out_key not in self.writers: - self.writers[out_key] = JsonlWriter(self.repo_dir / f"{out_key}.jsonl") - while cur: - vars_ = { - "owner": self.owner, - "name": self.name, - "number": number, - "after": cur, - } - data = self.client.graphql(Q.PR_COMMITS_QUERY, vars_) - item = ((data.get("data") or {}).get("repository") or {}).get( - "pullRequest" - ) or {} - cc = item.get("commits") or {} - for c in cc.get("nodes") or []: - c["_owner"] = self.owner - c["_repo"] = self.name - c["_prNumber"] = number - self.writers[out_key].write(c) - info = cc.get("pageInfo") or {} - cur = info.get("endCursor") if info.get("hasNextPage") else None - - def _paginate_pr_files(self, number: int, after: str) -> None: - cur = after - out_key = "pr_extra_files" - if out_key not in self.writers: - self.writers[out_key] = JsonlWriter(self.repo_dir / f"{out_key}.jsonl") - while cur: - vars_ = { - "owner": self.owner, - "name": self.name, - "number": number, - "after": cur, - } - data = self.client.graphql(Q.PR_FILES_QUERY, vars_) - item = ((data.get("data") or {}).get("repository") or {}).get( - "pullRequest" - ) or {} - ff = item.get("files") or {} - for f in ff.get("nodes") or []: - f["_owner"] = self.owner - f["_repo"] = self.name - f["_prNumber"] = number - # files don't have id, synthesize one - f["_syntheticId"] = f"{self.owner}/{self.name}#{number}:{f.get('path')}" - self.writers[out_key].write(f) - info = ff.get("pageInfo") or {} - cur = info.get("endCursor") if info.get("hasNextPage") else None - - def _paginate_pr_review_threads(self, number: int, after: str) -> None: - cur = after - out_key = "pr_extra_review_threads" - if out_key not in self.writers: - self.writers[out_key] = JsonlWriter(self.repo_dir / f"{out_key}.jsonl") - while cur: - vars_ = { - "owner": self.owner, - "name": self.name, - "number": number, - "after": cur, - } - data = self.client.graphql(Q.PR_REVIEW_THREADS_QUERY, vars_) - item = ((data.get("data") or {}).get("repository") or {}).get( - "pullRequest" - ) or {} - rt = item.get("reviewThreads") or {} - for th in rt.get("nodes") or []: - th["_owner"] = self.owner - th["_repo"] = self.name - th["_prNumber"] = number - self.writers[out_key].write(th) - info = rt.get("pageInfo") or {} - cur = info.get("endCursor") if info.get("hasNextPage") else None - - # ----- Discussions ----- - def scrape_discussions(self) -> int: - key = "discussions" - cursor = self.state.get(f"{key}_cursor") - done = self.state.get(f"{key}_done", False) - if done: - log.info("%s/%s discussions already complete", self.owner, self.name) - return 0 - total_new = 0 - page = 0 - per_page = 15 - while True: - page += 1 - vars_ = { - "owner": self.owner, - "name": self.name, - "first": per_page, - "after": cursor, - } - data = self.client.graphql(Q.DISCUSSIONS_PAGE_QUERY, vars_) - self._log_rate("discussions", data) - repo = (data.get("data") or {}).get("repository") or {} - dd = repo.get("discussions") or {} - nodes = dd.get("nodes") or [] - for d in nodes: - d["_owner"] = self.owner - d["_repo"] = self.name - d["_fetchedAt"] = ts() - num = d["number"] - if d.get("comments", {}).get("pageInfo", {}).get("hasNextPage"): - self._paginate_discussion_comments( - num, d["comments"]["pageInfo"]["endCursor"] - ) - # paginate replies per comment if needed - for c in d.get("comments", {}).get("nodes", []) or []: - if c.get("replies", {}).get("pageInfo", {}).get("hasNextPage"): - self._paginate_discussion_replies( - c["id"], c["replies"]["pageInfo"]["endCursor"], num - ) - if self.writers[key].write(d): - total_new += 1 - info = dd.get("pageInfo") or {} - cursor = info.get("endCursor") - self.state.set(f"{key}_cursor", cursor) - log.info( - "[%s/%s] discussions page %d (+%d) cursor=%s remaining=%s", - self.owner, - self.name, - page, - len(nodes), - str(cursor)[:20], - self.client.graphql_remaining, - ) - if self._trial_stop(key, total_new): - return total_new - if not info.get("hasNextPage"): - self.state.set(f"{key}_done", True) - break - return total_new - - def _paginate_discussion_comments(self, number: int, after: str) -> None: - cur = after - while cur: - vars_ = { - "owner": self.owner, - "name": self.name, - "number": number, - "after": cur, - } - data = self.client.graphql(Q.DISCUSSION_COMMENTS_QUERY, vars_) - disc = ((data.get("data") or {}).get("repository") or {}).get( - "discussion" - ) or {} - cc = disc.get("comments") or {} - for c in cc.get("nodes") or []: - c["_owner"] = self.owner - c["_repo"] = self.name - c["_discussionNumber"] = number - self.writers["discussion_extra_comments"].write(c) - info = cc.get("pageInfo") or {} - cur = info.get("endCursor") if info.get("hasNextPage") else None - - def _paginate_discussion_replies( - self, comment_id: str, after: str, disc_number: int - ) -> None: - cur = after - while cur: - vars_ = { - "owner": self.owner, - "name": self.name, - "commentId": comment_id, - "after": cur, - } - data = self.client.graphql(Q.DISCUSSION_REPLIES_QUERY, vars_) - node = (data.get("data") or {}).get("node") or {} - replies = node.get("replies") or {} - for r in replies.get("nodes") or []: - r["_owner"] = self.owner - r["_repo"] = self.name - r["_discussionNumber"] = disc_number - r["_commentId"] = comment_id - self.writers["discussion_extra_replies"].write(r) - info = replies.get("pageInfo") or {} - cur = info.get("endCursor") if info.get("hasNextPage") else None - - # ----- Commits ----- - def scrape_commits(self, branch: str = "refs/heads/main") -> int: - key = "commits" - cursor = self.state.get(f"{key}_cursor") - done = self.state.get(f"{key}_done", False) - if done: - return 0 - total_new = 0 - page = 0 - page_cap = 100 - trial_cap = self.trial_limits.get(key) - per_page = min(page_cap, trial_cap) if trial_cap and trial_cap > 0 else page_cap - while True: - page += 1 - vars_ = { - "owner": self.owner, - "name": self.name, - "first": per_page, - "after": cursor, - "branch": branch, - } - data = self.client.graphql(Q.COMMITS_PAGE_QUERY, vars_) - self._log_rate("commits", data) - ref = ((data.get("data") or {}).get("repository") or {}).get("ref") or {} - tgt = ref.get("target") or {} - hist = tgt.get("history") or {} - nodes = hist.get("nodes") or [] - for c in nodes: - c["_owner"] = self.owner - c["_repo"] = self.name - c["_fetchedAt"] = ts() - if self.writers[key].write(c): - total_new += 1 - info = hist.get("pageInfo") or {} - cursor = info.get("endCursor") - self.state.set(f"{key}_cursor", cursor) - log.info( - "[%s/%s] commits page %d (+%d) remaining=%s", - self.owner, - self.name, - page, - len(nodes), - self.client.graphql_remaining, - ) - if self._trial_stop(key, total_new): - return total_new - if not info.get("hasNextPage"): - self.state.set(f"{key}_done", True) - break - return total_new - - # ----- Releases/Labels/Milestones ----- - def scrape_releases(self) -> int: - return self._scrape_simple("releases", Q.RELEASES_QUERY, "releases") - - def scrape_labels(self) -> int: - return self._scrape_simple("labels", Q.LABELS_QUERY, "labels") - - def scrape_milestones(self) -> int: - return self._scrape_simple("milestones", Q.MILESTONES_QUERY, "milestones") - - def _scrape_simple(self, key: str, query: str, field: str) -> int: - cursor = self.state.get(f"{key}_cursor") - done = self.state.get(f"{key}_done", False) - if done: - return 0 - total_new = 0 - while True: - vars_ = { - "owner": self.owner, - "name": self.name, - "first": 50, - "after": cursor, - } - data = self.client.graphql(query, vars_) - repo = (data.get("data") or {}).get("repository") or {} - col = repo.get(field) or {} - for it in col.get("nodes") or []: - it["_owner"] = self.owner - it["_repo"] = self.name - it["_fetchedAt"] = ts() - if self.writers[key].write(it): - total_new += 1 - info = col.get("pageInfo") or {} - cursor = info.get("endCursor") - self.state.set(f"{key}_cursor", cursor) - if self._trial_stop(key, total_new): - return total_new - if not info.get("hasNextPage"): - self.state.set(f"{key}_done", True) - break - log.info("[%s/%s] %s done +%d", self.owner, self.name, key, total_new) - return total_new - - def close(self) -> None: - for w in self.writers.values(): - try: - w.close() - except Exception: - pass - - -def setup_logging(log_file: Path) -> None: - log_file.parent.mkdir(parents = True, exist_ok = True) - fmt = "%(asctime)s %(levelname)s [%(name)s] %(message)s" - handlers = [ - logging.StreamHandler(sys.stdout), - logging.FileHandler(log_file, mode = "a", encoding = "utf-8"), - ] - logging.basicConfig(level = logging.INFO, format = fmt, handlers = handlers, force = True) - - -def main(): - ap = argparse.ArgumentParser() - ap.add_argument( - "--base-dir", default = "/mnt/disks/unslothai/ubuntu/workspace_34/github_scraper" - ) - ap.add_argument( - "--repos", nargs = "+", default = ["unslothai/unsloth", "unslothai/unsloth-zoo"] - ) - ap.add_argument("--trial", action = "store_true", help = "Small trial run") - ap.add_argument( - "--only", - nargs = "+", - default = None, - help = "Only run these resource keys: issues,pulls,discussions,commits,releases,labels,milestones,meta", - ) - ap.add_argument( - "--hf-upload-interval", - type = int, - default = 900, - help = "Seconds between HF uploads (0 to disable)", - ) - args = ap.parse_args() - - base = Path(args.base_dir) - data_dir = base / "data" - data_dir.mkdir(parents = True, exist_ok = True) - setup_logging(base / "logs" / f"scraper_{time.strftime('%Y%m%d_%H%M%S')}.log") - log.info("Scraper starting: repos=%s trial=%s", args.repos, args.trial) - - client = GitHubClient(min_remaining_graphql = 80, min_remaining_rest = 80) - rl = client.rate_snapshot() - log.info( - "Rate limit snapshot: %s", - json.dumps(rl.get("resources", {}), default = str)[:400], - ) - - # Start HF uploader in background if requested - uploader = None - if args.hf_upload_interval > 0: - from hf_uploader import HFUploader - - uploader = HFUploader(data_dir, interval_s = args.hf_upload_interval) - uploader.start() - - trial_limits = None - if args.trial: - trial_limits = { - "issues": 5, - "pull_requests": 5, - "discussions": 3, - "commits": 20, - "releases": 3, - "labels": 20, - "milestones": 20, - } - - only = set(args.only or []) - - try: - for repo_spec in args.repos: - owner, name = repo_spec.split("/") - scraper = RepoScraper(owner, name, data_dir, client, trial_limits) - try: - repo_meta: Dict[str, Any] = {} - if not only or "meta" in only or "commits" in only: - repo_meta = scraper.scrape_repo_meta() - if not only or "labels" in only: - scraper.scrape_labels() - if not only or "milestones" in only: - scraper.scrape_milestones() - if not only or "releases" in only: - scraper.scrape_releases() - if not only or "discussions" in only: - scraper.scrape_discussions() - if not only or "issues" in only: - scraper.scrape_issues() - if not only or "pulls" in only: - scraper.scrape_prs() - if not only or "commits" in only: - default_ref = repo_meta.get("defaultBranchRef") or {} - default_branch = ( - default_ref.get("name") - if isinstance(default_ref, dict) - else None - ) - branch = ( - f"refs/heads/{default_branch}" - if default_branch - else "refs/heads/main" - ) - scraper.scrape_commits(branch = branch) - finally: - scraper.close() - finally: - if uploader: - log.info("Stopping uploader and final sync...") - uploader.stop(final_upload = True) - log.info( - "Scraper complete. GraphQL calls=%d REST calls=%d", - client.calls_graphql, - client.calls_rest, - ) - - -if __name__ == "__main__": - main() diff --git a/studio/backend/plugins/data-designer-github-repo-seed/src/data_designer_github_repo_seed/scraper_impl/state_store.py b/studio/backend/plugins/data-designer-github-repo-seed/src/data_designer_github_repo_seed/scraper_impl/state_store.py deleted file mode 100644 index efa663db2f..0000000000 --- a/studio/backend/plugins/data-designer-github-repo-seed/src/data_designer_github_repo_seed/scraper_impl/state_store.py +++ /dev/null @@ -1,105 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 - -"""Checkpoint state management for resumable scraping.""" - -from __future__ import annotations - -import json -import os -import threading -from pathlib import Path -from typing import Any, Dict - - -class StateStore: - def __init__(self, path: str | Path): - self.path = Path(path) - self.path.parent.mkdir(parents = True, exist_ok = True) - self._lock = threading.Lock() - self._data: Dict[str, Any] = {} - if self.path.exists(): - try: - with self.path.open() as f: - self._data = json.load(f) - except Exception: - self._data = {} - - def get(self, key: str, default: Any = None) -> Any: - with self._lock: - return self._data.get(key, default) - - def set(self, key: str, value: Any) -> None: - with self._lock: - self._data[key] = value - self._flush() - - def update(self, key: str, **kwargs) -> None: - with self._lock: - sub = dict(self._data.get(key, {})) - sub.update(kwargs) - self._data[key] = sub - self._flush() - - def all(self) -> Dict[str, Any]: - with self._lock: - return dict(self._data) - - def _flush(self) -> None: - tmp = self.path.with_suffix(self.path.suffix + ".tmp") - with tmp.open("w") as f: - json.dump(self._data, f, indent = 2, default = str) - os.replace(tmp, self.path) - - -class JsonlWriter: - """Append-only JSONL writer, thread-safe, with line buffering.""" - - def __init__(self, path: str | Path): - self.path = Path(path) - self.path.parent.mkdir(parents = True, exist_ok = True) - self._lock = threading.Lock() - self._fh = self.path.open("a", buffering = 1) - self._count_seen_keys: set[str] = set() - # Preload seen keys if file exists (for dedup across resumes) - if self.path.exists() and self.path.stat().st_size > 0: - try: - with self.path.open() as f: - for line in f: - try: - obj = json.loads(line) - k = self._key(obj) - if k is not None: - self._count_seen_keys.add(k) - except Exception: - pass - except Exception: - pass - - def _key(self, obj: dict) -> str | None: - for k in ("id", "node_id", "number", "sha", "url"): - if k in obj: - return f"{k}:{obj[k]}" - return None - - def has(self, key: str) -> bool: - return key in self._count_seen_keys - - def write(self, obj: dict) -> bool: - """Return True if newly written, False if already present.""" - k = self._key(obj) - with self._lock: - if k is not None and k in self._count_seen_keys: - return False - if k is not None: - self._count_seen_keys.add(k) - self._fh.write(json.dumps(obj, default = str, ensure_ascii = False)) - self._fh.write("\n") - self._fh.flush() - return True - - def close(self) -> None: - try: - self._fh.close() - except Exception: - pass diff --git a/studio/backend/requirements/single-env/data-designer-deps.txt b/studio/backend/requirements/single-env/data-designer-deps.txt index f63c076621..fc63230922 100644 --- a/studio/backend/requirements/single-env/data-designer-deps.txt +++ b/studio/backend/requirements/single-env/data-designer-deps.txt @@ -19,8 +19,7 @@ ruff<1,>=0.14.10 scipy<2,>=1.11.0 sqlfluff<4,>=3.2.0 tiktoken<1,>=0.8.0 -# Local seed plugin deps (plugins installed with --no-deps) -requests>=2.31 +# Unstructured-seed plugin deps (plugin installed with --no-deps) pymupdf>=1.24.0 pymupdf4llm>=0.0.17 mammoth>=1.8.0 diff --git a/studio/backend/routes/__init__.py b/studio/backend/routes/__init__.py index cf4586281b..e79f6553f9 100644 --- a/studio/backend/routes/__init__.py +++ b/studio/backend/routes/__init__.py @@ -8,7 +8,6 @@ from routes.training import router as training_router from routes.models import router as models_router from routes.inference import router as inference_router -from routes.inference import studio_router as inference_studio_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 @@ -19,7 +18,6 @@ "training_router", "models_router", "inference_router", - "inference_studio_router", "datasets_router", "auth_router", "data_recipe_router", diff --git a/studio/backend/routes/auth.py b/studio/backend/routes/auth.py index 3deeb6793b..5cd23bd450 100644 --- a/studio/backend/routes/auth.py +++ b/studio/backend/routes/auth.py @@ -17,7 +17,6 @@ ChangePasswordRequest, CreateApiKeyRequest, CreateApiKeyResponse, - DesktopLoginRequest, RefreshTokenRequest, ) from models.users import Token @@ -81,24 +80,6 @@ async def login(payload: AuthLoginRequest) -> Token: ) -@router.post("/desktop-login", response_model = Token) -async def desktop_login(payload: DesktopLoginRequest) -> Token: - """Exchange a local desktop secret for normal admin-subject tokens.""" - username = storage.validate_desktop_secret(payload.secret) - if username is None: - raise HTTPException( - status_code = status.HTTP_401_UNAUTHORIZED, - detail = "Desktop authentication failed", - ) - - return Token( - access_token = create_access_token(subject = username, desktop = True), - refresh_token = create_refresh_token(subject = username, desktop = True), - token_type = "bearer", - must_change_password = False, - ) - - @router.post("/refresh", response_model = Token) async def refresh(payload: RefreshTokenRequest) -> Token: """ @@ -106,7 +87,7 @@ async def refresh(payload: RefreshTokenRequest) -> Token: The refresh token itself is reusable until it expires (7 days). """ - new_access_token, username, is_desktop = refresh_access_token(payload.refresh_token) + new_access_token, username = refresh_access_token(payload.refresh_token) if new_access_token is None or username is None: raise HTTPException( status_code = status.HTTP_401_UNAUTHORIZED, @@ -117,9 +98,7 @@ async def refresh(payload: RefreshTokenRequest) -> Token: access_token = new_access_token, refresh_token = payload.refresh_token, token_type = "bearer", - must_change_password = False - if is_desktop - else storage.requires_password_change(username), + must_change_password = storage.requires_password_change(username), ) diff --git a/studio/backend/routes/data_recipe/jobs.py b/studio/backend/routes/data_recipe/jobs.py index da6416e324..00546b47a4 100644 --- a/studio/backend/routes/data_recipe/jobs.py +++ b/studio/backend/routes/data_recipe/jobs.py @@ -5,9 +5,8 @@ from __future__ import annotations -import copy -from datetime import datetime, timedelta, timezone -from typing import Any, Optional +from datetime import timedelta +from typing import Any from urllib.parse import urlparse from fastapi import APIRouter, HTTPException, Query, Request @@ -58,20 +57,6 @@ def _resolve_local_v1_endpoint(request: Request) -> str: return f"http://127.0.0.1:{int(port)}/v1" -def _request_has_desktop_access_token(request: Request) -> bool: - auth_header = request.headers.get("authorization") - if not auth_header: - return False - - parts = auth_header.split(None, 1) - if len(parts) != 2 or parts[0].lower() != "bearer": - return False - - from auth.authentication import is_desktop_access_token - - return is_desktop_access_token(parts[1]) - - def _used_llm_model_aliases(recipe: dict[str, Any]) -> set[str]: """Return the set of model_aliases that are actually referenced by an LLM column. Used to narrow the "Chat model loaded" gate so that orphan @@ -95,111 +80,14 @@ def _used_llm_model_aliases(recipe: dict[str, Any]) -> set[str]: return aliases -def _inject_local_structured_response_format( - recipe: dict[str, Any], local_provider_names: set[str] -) -> None: - """For each llm-structured column that targets a local-provider model_config, - clone the model_config and inject an OpenAI ``response_format`` with the - column's ``output_format`` JSON schema. The column is rewritten to point at - the clone so llm-text / llm-judge columns that share the same alias keep - free-form sampling. - - Without this, data_designer only injects a prompt-level "return JSON in a - ```json fence" instruction. Small GGUF models frequently break format, - wasting the full ``max_tokens`` budget per row and then failing to parse. - Forwarding ``response_format`` lets llama-server apply grammar-constrained - sampling from the JSON schema, which guarantees a parseable response and - terminates early. - """ - columns = recipe.get("columns") - model_configs = recipe.get("model_configs") - if not isinstance(columns, list) or not isinstance(model_configs, list): - return - - # alias -> model_config (only configs referencing a local provider qualify). - alias_to_local_mc: dict[str, dict[str, Any]] = {} - for mc in model_configs: - if not isinstance(mc, dict): - continue - if mc.get("provider") in local_provider_names and isinstance( - mc.get("alias"), str - ): - alias_to_local_mc[mc["alias"]] = mc - - if not alias_to_local_mc: - return - - # Clone per (alias, column) so each llm-structured column gets its own - # schema without leaking response_format onto other columns that share the - # same base alias. - seen_clone_aliases: set[str] = { - mc.get("alias") for mc in model_configs if isinstance(mc.get("alias"), str) - } - new_configs: list[dict[str, Any]] = [] - for column in columns: - if not isinstance(column, dict): - continue - if column.get("column_type") != "llm-structured": - continue - alias = column.get("model_alias") - if not isinstance(alias, str) or alias not in alias_to_local_mc: - continue - output_format = column.get("output_format") - if not isinstance(output_format, dict) or not output_format: - continue - base_mc = alias_to_local_mc[alias] - column_name = column.get("name") or "structured" - clone_alias_base = f"{alias}__{column_name}_structured" - clone_alias = clone_alias_base - counter = 1 - while clone_alias in seen_clone_aliases: - counter += 1 - clone_alias = f"{clone_alias_base}_{counter}" - seen_clone_aliases.add(clone_alias) - - clone = copy.deepcopy(base_mc) - clone["alias"] = clone_alias - params = clone.get("inference_parameters") - if not isinstance(params, dict): - params = {} - clone["inference_parameters"] = params - # data_designer's BaseInferenceParams is a pydantic model with - # extra="forbid", so response_format cannot sit at the top level of - # inference_parameters. It does expose an `extra_body: dict` pass- - # through that the OpenAI client spreads into the request body at the - # top level, which is where llama-server reads response_format from. - # llama.cpp server shape (tools/server/README.md): the schema sits - # directly under response_format, not nested in a json_schema object - # the way OpenAI's Chat Completions API expects. llama-server converts - # the schema to a GBNF grammar and applies it during sampling. - extra_body = params.get("extra_body") - if not isinstance(extra_body, dict): - extra_body = {} - extra_body["response_format"] = { - "type": "json_schema", - "schema": output_format, - } - params["extra_body"] = extra_body - new_configs.append(clone) - column["model_alias"] = clone_alias - - if new_configs: - model_configs.extend(new_configs) - - -def _inject_local_providers(recipe: dict[str, Any], request: Request) -> Optional[int]: +def _inject_local_providers(recipe: dict[str, Any], request: Request) -> None: """ Mutate recipe dict in-place: for any provider with is_local=True, - fill in the endpoint pointing at this server and inject a short-lived - internal sk-unsloth-* API key for workflow auth. - - Returns the row id of the minted internal key (so the caller can - revoke it on job completion) or ``None`` when no local provider is - actually reachable from an LLM column. + generate a JWT and fill in the endpoint pointing at this server. """ providers = recipe.get("model_providers") if not providers: - return None + return # Collect local providers and pop is_local from ALL dicts unconditionally. # Strict `is True` guard so malformed payloads (is_local: 1, @@ -213,7 +101,7 @@ def _inject_local_providers(recipe: dict[str, Any], request: Request) -> Optiona local_indices.append(i) if not local_indices: - return None + return endpoint = _resolve_local_v1_endpoint(request) @@ -236,7 +124,6 @@ def _inject_local_providers(recipe: dict[str, Any], request: Request) -> Optiona } token = "" - internal_key_id: Optional[int] = None if local_names & referenced_providers: # Verify a model is loaded. # NOTE: This is a point-in-time check (TOCTOU). The model could be unloaded @@ -257,21 +144,17 @@ def _inject_local_providers(recipe: dict[str, Any], request: Request) -> Optiona "No model loaded in Chat. Load a model first, then run the recipe." ) - from auth import storage # deferred: avoids circular import - - # Mint an internal sk-unsloth-* key scoped to this workflow run. - # Uses the unified API-key issuance path (one mint/revoke/verify - # surface instead of a second JWT code path). The key is marked - # internal so it is hidden from the user's API-key list, and the - # caller revokes it when the job terminates. - expires_at = (datetime.now(timezone.utc) + timedelta(hours = 24)).isoformat() - token, row = storage.create_api_key( - username = "unsloth", - name = "data-recipe workflow", - expires_at = expires_at, - internal = True, + from auth.authentication import ( + create_access_token, + ) # deferred: avoids circular import + + # Uses the "unsloth" admin subject. If the user changes their password, + # the JWT secret rotates and this token becomes invalid mid-run. + # Acceptable for v1 - recipes typically finish well within one session. + token = create_access_token( + subject = "unsloth", + expires_delta = timedelta(hours = 24), ) - internal_key_id = int(row["id"]) # Defensively strip any stale "external"-only fields the frontend may # have left on the dict (extra_headers/extra_body/api_key_env). The UI @@ -298,37 +181,6 @@ def _inject_local_providers(recipe: dict[str, Any], request: Request) -> Optiona continue if mc.get("provider") in local_names: mc["skip_health_check"] = True - # Disable thinking for data-recipe inference on local providers. - # Reasoning models emit a ... preamble before the - # answer, which roughly doubles generated token count per row and - # pushes the visible answer past data_designer's json-fence - # regex. Forward chat_template_kwargs={enable_thinking: False} - # through the OpenAI SDK's extra_body passthrough so llama-server - # renders the template without the reasoning preamble. Free-form - # llm-text columns benefit from the latency cut, and structured - # columns also stop leaking think tags into the grammar- - # constrained JSON (llama-server's GBNF path still enforces the - # schema either way). - params = mc.get("inference_parameters") - if not isinstance(params, dict): - params = {} - mc["inference_parameters"] = params - extra_body = params.get("extra_body") - if not isinstance(extra_body, dict): - extra_body = {} - tpl_kwargs = extra_body.get("chat_template_kwargs") - if not isinstance(tpl_kwargs, dict): - tpl_kwargs = {} - tpl_kwargs.setdefault("enable_thinking", False) - extra_body["chat_template_kwargs"] = tpl_kwargs - params["extra_body"] = extra_body - - # Forward each llm-structured column's output_format as an OpenAI - # response_format so llama-server uses grammar-constrained sampling and - # small GGUFs stop wasting the full max_tokens budget on broken JSON. - _inject_local_structured_response_format(recipe, local_names) - - return internal_key_id def _normalize_run_name(value: Any) -> str | None: @@ -373,49 +225,21 @@ def create_job(payload: RecipePayload, request: Request): ) from exc try: - internal_api_key_id = _inject_local_providers(recipe, request) + _inject_local_providers(recipe, request) except ValueError as exc: raise HTTPException(status_code = 400, detail = str(exc)) from exc - # Single try block covers get_job_manager() AND mgr.start() so a workflow - # key minted above never outlives the request even when an unexpected - # exception type (TypeError from a stale kwarg, OSError from a queue - # write, etc.) bubbles up. Without the bare except, such exceptions let - # the sk-unsloth-* key live until its 24h TTL. + mgr = get_job_manager() try: - mgr = get_job_manager() - job_id = mgr.start( - recipe = recipe, - run = run, - internal_api_key_id = internal_api_key_id, - ) + job_id = mgr.start(recipe = recipe, run = run) except RuntimeError as exc: - if internal_api_key_id is not None: - _revoke_internal_api_key_safe(internal_api_key_id) raise HTTPException(status_code = 409, detail = str(exc)) from exc except ValueError as exc: - if internal_api_key_id is not None: - _revoke_internal_api_key_safe(internal_api_key_id) raise HTTPException(status_code = 400, detail = str(exc)) from exc - except Exception: - if internal_api_key_id is not None: - _revoke_internal_api_key_safe(internal_api_key_id) - raise return {"job_id": job_id} -def _revoke_internal_api_key_safe(key_id: int) -> None: - """Best-effort revoke of a workflow-minted key; swallow any error so - that revocation failures never mask the caller's own error path.""" - try: - from auth import storage # deferred: avoids circular import - - storage.revoke_internal_api_key(key_id) - except Exception: - pass - - @router.get("/jobs/{job_id}/status") def job_status(job_id: str): mgr = get_job_manager() diff --git a/studio/backend/routes/data_recipe/seed.py b/studio/backend/routes/data_recipe/seed.py index 91cf718e6e..e9cf828610 100644 --- a/studio/backend/routes/data_recipe/seed.py +++ b/studio/backend/routes/data_recipe/seed.py @@ -8,7 +8,6 @@ import base64 import binascii import json -import os import re from itertools import islice from pathlib import Path @@ -628,14 +627,3 @@ def inspect_seed_upload(payload: SeedInspectUploadRequest) -> SeedInspectRespons split = None, subset = None, ) - - -@router.get("/seed/github/env-token") -def get_github_env_token_status() -> dict: - """Report whether the server has a GH_TOKEN / GITHUB_TOKEN env var. - - The value is never returned; the UI uses this to tell the user they - can leave the token field blank. - """ - has_token = bool(os.environ.get("GH_TOKEN") or os.environ.get("GITHUB_TOKEN")) - return {"has_token": has_token} diff --git a/studio/backend/routes/data_recipe/validate.py b/studio/backend/routes/data_recipe/validate.py index e794d68e54..555e3eaa06 100644 --- a/studio/backend/routes/data_recipe/validate.py +++ b/studio/backend/routes/data_recipe/validate.py @@ -14,63 +14,10 @@ create_data_designer, validate_recipe, ) -from loggers import get_logger from models.data_recipe import RecipePayload, ValidateError, ValidateResponse -logger = get_logger(__name__) router = APIRouter() -_GITHUB_VALIDATE_NOTE = "Recipe shape is valid. GitHub access and rate limits are checked when the run starts." -_GITHUB_ITEM_TYPES = {"issues", "pulls", "commits"} - - -def _github_seed_source(recipe: dict[str, Any]) -> dict[str, Any] | None: - seed_config = recipe.get("seed_config") - if not isinstance(seed_config, dict): - return None - source = seed_config.get("source") - if not isinstance(source, dict) or source.get("seed_type") != "github_repo": - return None - return source - - -def _validate_github_seed_static(source: dict[str, Any]) -> list[ValidateError]: - errors: list[ValidateError] = [] - - repos = source.get("repos") - if not isinstance(repos, list) or not repos: - errors.append(ValidateError(message = "GitHub seed requires at least one repo.")) - else: - for repo in repos: - if not isinstance(repo, str) or not repo.strip() or "/" not in repo: - errors.append( - ValidateError(message = "GitHub repos must be owner/name strings.") - ) - break - - item_types = source.get("item_types") - if not isinstance(item_types, list) or not item_types: - errors.append( - ValidateError(message = "GitHub seed requires at least one item type.") - ) - else: - invalid_items = [item for item in item_types if item not in _GITHUB_ITEM_TYPES] - if invalid_items: - errors.append( - ValidateError( - message = "GitHub item types must be issues, pulls, or commits." - ) - ) - - try: - limit = int(source.get("limit")) - except (TypeError, ValueError): - limit = 0 - if limit < 1 or limit > 5000: - errors.append(ValidateError(message = "GitHub limit must be from 1 to 5000.")) - - return errors - def _collect_validation_errors(recipe: dict[str, Any]) -> list[ValidateError]: try: @@ -146,38 +93,6 @@ def validate(payload: RecipePayload) -> ValidateResponse: _patch_local_providers(recipe) - github_source = _github_seed_source(recipe) - if github_source is not None: - static_errors = _validate_github_seed_static(github_source) - if static_errors: - return ValidateResponse(valid = False, errors = static_errors) - try: - build_config_builder(recipe) - except ModuleNotFoundError as exc: - # data_designer is an optional runtime dep. Static validation - # already passed; live access + full config validation are - # deferred to run start (per _GITHUB_VALIDATE_NOTE), so a missing - # optional import at validate time should not block the recipe. - # Restrict the bypass to the data_designer module specifically so - # other ImportErrors (e.g. broken internal imports or missing - # transitive deps after a package upgrade) still surface as - # validation failures instead of being silently swallowed. - if not (exc.name or "").startswith("data_designer"): - raise - logger.debug( - "data_designer not installed; deferring full config " - "validation to run start", - missing_module = exc.name, - ) - except Exception as exc: - detail = str(exc).strip() or "Validation failed." - return ValidateResponse( - valid = False, - errors = [ValidateError(message = detail)], - raw_detail = detail, - ) - return ValidateResponse(valid = True, raw_detail = _GITHUB_VALIDATE_NOTE) - try: validate_recipe(recipe) except RuntimeError as exc: diff --git a/studio/backend/routes/inference.py b/studio/backend/routes/inference.py index a6b00360af..e17f8f5882 100644 --- a/studio/backend/routes/inference.py +++ b/studio/backend/routes/inference.py @@ -113,45 +113,19 @@ def _friendly_error(exc: Exception) -> str: # Import backend functions try: from core.inference import get_inference_backend - from core.inference.llama_cpp import ( - LlamaCppBackend, - _DEFAULT_MAX_TOKENS_FLOOR, - _DEFAULT_T_MAX_PREDICT_MS, - detect_reasoning_flags, - ) - from core.inference.llama_server_args import validate_extra_args + from core.inference.llama_cpp import LlamaCppBackend from utils.models import ModelConfig from utils.inference import load_inference_config from utils.models.model_config import load_model_defaults - from utils.native_path_leases import ( - NativePathLeaseError, - display_label_for_native_path, - is_registered_native_path_label, - redact_native_paths, - verify_native_path_lease, - ) except ImportError: parent_backend = backend_path.parent / "backend" 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, - _DEFAULT_MAX_TOKENS_FLOOR, - _DEFAULT_T_MAX_PREDICT_MS, - detect_reasoning_flags, - ) - from core.inference.llama_server_args import validate_extra_args + from core.inference.llama_cpp import LlamaCppBackend from utils.models import ModelConfig from utils.inference import load_inference_config from utils.models.model_config import load_model_defaults - from utils.native_path_leases import ( - NativePathLeaseError, - display_label_for_native_path, - is_registered_native_path_label, - redact_native_paths, - verify_native_path_lease, - ) from models.inference import ( LoadRequest, @@ -211,138 +185,6 @@ def _friendly_error(exc: Exception) -> str: from datetime import date as _date router = APIRouter() -# Studio-only router (not mounted on /v1 OpenAI-compat). -studio_router = APIRouter() - - -def _effective_enable_tools(payload) -> Optional[bool]: - """Resolve `payload.enable_tools` against the process-level tool policy. - - Returns the policy value when set (CLI hard-override from `unsloth run`), - otherwise the per-request value. - """ - from state.tool_policy import get_tool_policy - - policy = get_tool_policy() - return policy if policy is not None else payload.enable_tools - - -# Cancel registry. Proxies (e.g. Colab) can swallow client fetch aborts -# so is_disconnected() never fires. POST /inference/cancel looks up -# in-flight cancel_events here by cancel_id (per-run) or session_id / -# completion_id (fallbacks). -_CANCEL_REGISTRY: dict[str, set[threading.Event]] = {} -_CANCEL_LOCK = threading.Lock() - -# Cancel POSTs that arrive before registration are stashed; the next -# matching __enter__ replays set() within the TTL. -_PENDING_CANCELS: dict[str, float] = {} -_PENDING_CANCEL_TTL_S = 30.0 - - -def _prune_pending(now: float) -> None: - for k in [ - k for k, ts in _PENDING_CANCELS.items() if now - ts > _PENDING_CANCEL_TTL_S - ]: - _PENDING_CANCELS.pop(k, None) - - -class _TrackedCancel: - """Register cancel_event in _CANCEL_REGISTRY for the block's duration.""" - - def __init__(self, event: threading.Event, *keys): - self.event = event - self.keys = tuple(k for k in keys if k) - - def __enter__(self): - # Register + consume-pending must be one critical section to close - # the TOCTOU race against a concurrent cancel POST. - should_cancel = False - with _CANCEL_LOCK: - for k in self.keys: - _CANCEL_REGISTRY.setdefault(k, set()).add(self.event) - now = time.monotonic() - _prune_pending(now) - for k in self.keys: - if k and _PENDING_CANCELS.pop(k, None) is not None: - should_cancel = True - if should_cancel: - self.event.set() - return self.event - - def __exit__(self, *exc): - with _CANCEL_LOCK: - for k in self.keys: - bucket = _CANCEL_REGISTRY.get(k) - if bucket is None: - continue - bucket.discard(self.event) - if not bucket: - _CANCEL_REGISTRY.pop(k, None) - return False - - -def _cancel_by_keys(keys) -> int: - """Set cancel_event for matching registry entries; no stash. - session_id/completion_id are shared across runs on the same thread, - so stashing them would ghost-cancel the user's next request. Only - cancel_id is per-run unique (see _cancel_by_cancel_id_or_stash).""" - if not keys: - return 0 - events: set[threading.Event] = set() - with _CANCEL_LOCK: - _prune_pending(time.monotonic()) - for k in keys: - bucket = _CANCEL_REGISTRY.get(k) - if bucket: - events.update(bucket) - for ev in events: - ev.set() - return len(events) - - -def _cancel_by_cancel_id_or_stash(cancel_id: str) -> int: - """Atomic lookup-or-stash; pairs with _TrackedCancel.__enter__ to - close the TOCTOU race.""" - now = time.monotonic() - events: set[threading.Event] = set() - with _CANCEL_LOCK: - _prune_pending(now) - bucket = _CANCEL_REGISTRY.get(cancel_id) - if bucket: - events.update(bucket) - else: - _PENDING_CANCELS[cancel_id] = now - for ev in events: - ev.set() - return len(events) - - -async def _await_cancel_then_close(cancel_event, resp) -> None: - """Watch a threading.Event from asyncio and close ``resp`` when it fires. - - Used by the passthrough streamers so a /cancel POST can interrupt - while the async iterator is blocked waiting for llama-server prefill. - Without this watcher the in-loop ``cancel_event.is_set()`` check is - unreachable until the first SSE chunk arrives, which is exactly the - proxy/Colab scenario the cancel POST exists to handle. - - Polls a threading.Event because the cancel registry is keyed by - threading.Event so the synchronous /cancel handler can call .set(). - 50ms cadence adds at most that much latency to a prefill cancel; the - common-case streaming cancel path still observes the event in the - iterator's first iteration after the next chunk. - """ - try: - while not cancel_event.is_set(): - await asyncio.sleep(0.05) - try: - await resp.aclose() - except Exception: - pass - except asyncio.CancelledError: - return - # Appended to tool-use nudge to discourage plan-without-action _TOOL_ACTION_NUDGE = ( @@ -360,65 +202,6 @@ async def _await_cancel_then_close(cancel_event, resp) -> None: logger = get_logger(__name__) -def _validate_native_mmproj_companion( - mmproj_path: str | None, gguf_path: str | None -) -> None: - if not mmproj_path or not gguf_path: - return - import stat as _stat_module - - mm = Path(mmproj_path) - gguf = Path(gguf_path) - try: - mm_lstat = os.lstat(mm) - except OSError as exc: - raise HTTPException( - status_code = 400, - detail = "Native vision companion is no longer accessible.", - ) from exc - if _stat_module.S_ISLNK(mm_lstat.st_mode) or not _stat_module.S_ISREG( - mm_lstat.st_mode - ): - raise HTTPException( - status_code = 400, - detail = "Native vision companion must be a regular file.", - ) - try: - if mm.resolve(strict = True).parent != gguf.resolve(strict = True).parent: - raise HTTPException( - status_code = 400, - detail = "Native vision companion must live next to the selected GGUF.", - ) - except OSError as exc: - raise HTTPException( - status_code = 400, - detail = "Native vision companion is no longer accessible.", - ) from exc - - -def _resolve_model_identifier_for_request( - request: LoadRequest | ValidateModelRequest, - *, - operation: str, -) -> tuple[str, str, bool]: - if not request.native_path_lease: - return request.model_path, request.model_path, False - try: - grant = verify_native_path_lease( - request.native_path_lease, - operation = operation, - expected_kind = "model", - expected_path_type = "file", - allowed_suffixes = (".gguf",), - ) - except NativePathLeaseError as exc: - raise HTTPException(status_code = 400, detail = str(exc)) from exc - display_label = ( - grant.display_label or Path(request.model_path).name or "Native model" - ) - return str(grant.canonical_path), display_label, True - - # GGUF inference backend (llama-server) _llama_cpp_backend = LlamaCppBackend() @@ -442,19 +225,7 @@ async def load_model( GGUF models are loaded via llama-server (llama.cpp) instead of Unsloth. """ - native_grant_backed = False - model_log_label = request.model_path try: - # Validate user-supplied llama-server pass-through args up front - # so a managed-flag collision returns 400 before any model work. - try: - extra_llama_args = validate_extra_args(request.llama_extra_args) - except ValueError as exc: - raise HTTPException(status_code = 400, detail = str(exc)) - - model_identifier, model_log_label, native_grant_backed = ( - _resolve_model_identifier_for_request(request, operation = "load-model") - ) # Version switching is handled automatically by the subprocess-based # inference backend — no need for ensure_transformers_version() here. @@ -468,10 +239,10 @@ async def load_model( and llama_backend.hf_variant and llama_backend.hf_variant.lower() == request.gguf_variant.lower() and llama_backend.model_identifier - and llama_backend.model_identifier.lower() == model_identifier.lower() + and llama_backend.model_identifier.lower() == request.model_path.lower() ): logger.info( - f"Model already loaded (GGUF): {model_log_label} variant={request.gguf_variant}, skipping reload" + f"Model already loaded (GGUF): {request.model_path} variant={request.gguf_variant}, skipping reload" ) inference_config = load_inference_config(llama_backend.model_identifier) from utils.models import is_audio_input_type @@ -484,12 +255,8 @@ async def load_model( _gguf_is_audio = getattr(llama_backend, "_is_audio", False) return LoadResponse( status = "already_loaded", - model = model_log_label - if native_grant_backed - else llama_backend.model_identifier, - display_name = model_log_label - if native_grant_backed - else llama_backend.model_identifier, + model = llama_backend.model_identifier, + display_name = llama_backend.model_identifier, is_vision = llama_backend._is_vision, is_lora = False, is_gguf = True, @@ -506,19 +273,17 @@ async def load_model( max_context_length = llama_backend.max_context_length, native_context_length = llama_backend.native_context_length, supports_reasoning = llama_backend.supports_reasoning, - reasoning_style = llama_backend.reasoning_style, reasoning_always_on = llama_backend.reasoning_always_on, - supports_preserve_thinking = llama_backend.supports_preserve_thinking, chat_template = llama_backend.chat_template, speculative_type = llama_backend.speculative_type, ) else: if ( backend.active_model_name - and backend.active_model_name.lower() == model_identifier.lower() + and backend.active_model_name.lower() == request.model_path.lower() ): logger.info( - f"Model already loaded (Unsloth): {model_log_label}, skipping reload" + f"Model already loaded (Unsloth): {request.model_path}, skipping reload" ) inference_config = load_inference_config(backend.active_model_name) _model_info = backend.models.get(backend.active_model_name, {}) @@ -530,29 +295,10 @@ async def load_model( logger.warning( f"Could not retrieve chat template for {backend.active_model_name}: {e}" ) - # Non-GGUF: only advertise reasoning for gpt-oss Harmony, - # which emits reasoning via channels at the tokenizer level. - # Template-level chat_template_kwargs (enable_thinking / - # preserve_thinking / tools) are not yet forwarded through - # the transformers generation path, so avoid advertising - # controls the server cannot honour outside GGUF. - _sf_supports_reasoning = False - _sf_reasoning_style = "enable_thinking" - if hasattr(backend, "_is_gpt_oss_model"): - try: - if backend._is_gpt_oss_model(): - _sf_supports_reasoning = True - _sf_reasoning_style = "reasoning_effort" - except Exception: - pass return LoadResponse( status = "already_loaded", - model = model_log_label - if native_grant_backed - else backend.active_model_name, - display_name = model_log_label - if native_grant_backed - else backend.active_model_name, + model = backend.active_model_name, + display_name = backend.active_model_name, is_vision = _model_info.get("is_vision", False), is_lora = _model_info.get("is_lora", False), is_gguf = False, @@ -563,18 +309,13 @@ async def load_model( requires_trust_remote_code = bool( inference_config.get("trust_remote_code", False) ), - supports_reasoning = _sf_supports_reasoning, - reasoning_style = _sf_reasoning_style, - reasoning_always_on = False, - supports_preserve_thinking = False, - supports_tools = False, chat_template = _chat_template, ) # Create config using clean factory method # is_lora is auto-detected from adapter_config.json on disk/HF config = ModelConfig.from_identifier( - model_id = model_identifier, + model_id = request.model_path, hf_token = request.hf_token, gguf_variant = request.gguf_variant, ) @@ -582,7 +323,7 @@ async def load_model( if not config: raise HTTPException( status_code = 400, - detail = f"Invalid model identifier: {model_log_label}", + detail = f"Invalid model identifier: {request.model_path}", ) # Normalize gpu_ids: empty list means auto-selection, same as None @@ -626,14 +367,9 @@ async def load_model( cache_type_kv = request.cache_type_kv, speculative_type = request.speculative_type, n_parallel = _n_parallel, - extra_args = extra_llama_args, ) else: # Local mode: llama-server loads via -m - if native_grant_backed and config.gguf_mmproj_file: - _validate_native_mmproj_companion( - config.gguf_mmproj_file, config.gguf_file - ) success = await asyncio.to_thread( llama_backend.load_model, gguf_path = config.gguf_file, @@ -645,18 +381,15 @@ async def load_model( cache_type_kv = request.cache_type_kv, speculative_type = request.speculative_type, n_parallel = _n_parallel, - extra_args = extra_llama_args, ) if not success: raise HTTPException( status_code = 500, - detail = f"Failed to load GGUF model: {model_log_label if native_grant_backed else config.display_name}", + detail = f"Failed to load GGUF model: {config.display_name}", ) - logger.info( - f"Loaded GGUF model via llama-server: {model_log_label if native_grant_backed else config.identifier}" - ) + logger.info(f"Loaded GGUF model via llama-server: {config.identifier}") # Detect TTS audio by probing the loaded model's vocabulary from utils.models import is_audio_input_type @@ -665,10 +398,6 @@ async def load_model( _gguf_is_audio = _gguf_audio in ("snac", "bicodec", "dac") llama_backend._is_audio = _gguf_is_audio llama_backend._audio_type = _gguf_audio - llama_backend._native_display_label = ( - model_log_label if native_grant_backed else None - ) - llama_backend._native_grant_backed = bool(native_grant_backed) if _gguf_is_audio: logger.info(f"GGUF model detected as audio: audio_type={_gguf_audio}") await asyncio.to_thread(llama_backend.init_audio_codec, _gguf_audio) @@ -677,10 +406,8 @@ async def load_model( return LoadResponse( status = "loaded", - model = model_log_label if native_grant_backed else config.identifier, - display_name = model_log_label - if native_grant_backed - else config.display_name, + model = config.identifier, + display_name = config.display_name, is_vision = config.is_vision, is_lora = False, is_gguf = True, @@ -695,9 +422,7 @@ async def load_model( max_context_length = llama_backend.max_context_length, native_context_length = llama_backend.native_context_length, supports_reasoning = llama_backend.supports_reasoning, - reasoning_style = llama_backend.reasoning_style, reasoning_always_on = llama_backend.reasoning_always_on, - supports_preserve_thinking = llama_backend.supports_preserve_thinking, supports_tools = llama_backend.supports_tools, cache_type_kv = llama_backend.cache_type_kv, chat_template = llama_backend.chat_template, @@ -803,13 +528,10 @@ async def load_model( ), ) raise HTTPException( - status_code = 500, - detail = f"Failed to load model: {model_log_label if native_grant_backed else config.display_name}", + status_code = 500, detail = f"Failed to load model: {config.display_name}" ) - logger.info( - f"Loaded model: {model_log_label if native_grant_backed else config.identifier}" - ) + logger.info(f"Loaded model: {config.identifier}") # Load inference configuration parameters inference_config = load_inference_config(config.identifier) @@ -823,26 +545,10 @@ async def load_model( except Exception: pass - # Non-GGUF: gpt-oss Harmony surfaces reasoning via tokenizer-level - # channels; other safetensors reasoning/tools/preserve-thinking - # knobs are not forwarded to tokenizer.apply_chat_template yet, so - # we only advertise support for the Harmony case here. - _sf_supports_reasoning = False - _sf_reasoning_style = "enable_thinking" - if hasattr(backend, "_is_gpt_oss_model"): - try: - if backend._is_gpt_oss_model(): - _sf_supports_reasoning = True - _sf_reasoning_style = "reasoning_effort" - except Exception: - pass - return LoadResponse( status = "loaded", - model = model_log_label if native_grant_backed else config.identifier, - display_name = model_log_label - if native_grant_backed - else config.display_name, + model = config.identifier, + display_name = config.display_name, is_vision = config.is_vision, is_lora = config.is_lora, is_gguf = False, @@ -853,28 +559,17 @@ async def load_model( requires_trust_remote_code = bool( inference_config.get("trust_remote_code", False) ), - supports_reasoning = _sf_supports_reasoning, - reasoning_style = _sf_reasoning_style, - reasoning_always_on = False, - supports_preserve_thinking = False, - supports_tools = False, chat_template = _chat_template, ) except HTTPException: raise except ValueError as e: - if native_grant_backed: - redacted_msg = redact_native_paths(str(e)) - logger.warning( - "Rejected inference selection for native model %s: %s", - model_log_label, - redacted_msg, - ) - raise HTTPException(status_code = 400, detail = redacted_msg) logger.warning("Rejected inference GPU selection: %s", e) raise HTTPException(status_code = 400, detail = str(e)) except Exception as e: + logger.error(f"Error loading model: {e}", exc_info = True) + msg = str(e) # Surface a friendlier message for models that Unsloth cannot load not_supported_hints = [ "No config file found", @@ -882,22 +577,6 @@ async def load_model( "is not supported", "does not support", ] - if native_grant_backed: - redacted_msg = redact_native_paths(str(e)) - logger.error( - "Error loading native model %s: %s", - model_log_label, - redacted_msg, - ) - msg = redacted_msg - if any(h.lower() in msg.lower() for h in not_supported_hints): - msg = f"This model is not supported yet. Try a different model. (Original error: {msg})" - raise HTTPException( - status_code = 500, - detail = f"Failed to load native model {model_log_label}: {msg}", - ) - logger.error(f"Error loading model: {e}", exc_info = True) - msg = str(e) if any(h.lower() in msg.lower() for h in not_supported_hints): msg = f"This model is not supported yet. Try a different model. (Original error: {msg})" raise HTTPException(status_code = 500, detail = f"Failed to load model: {msg}") @@ -914,14 +593,9 @@ async def validate_model( This checks that ModelConfig.from_identifier() can resolve the given model_path, but it does NOT actually load model weights into GPU memory. """ - native_grant_backed = False - model_log_label = request.model_path try: - model_identifier, model_log_label, native_grant_backed = ( - _resolve_model_identifier_for_request(request, operation = "validate-model") - ) config = ModelConfig.from_identifier( - model_id = model_identifier, + model_id = request.model_path, hf_token = request.hf_token, gguf_variant = request.gguf_variant, ) @@ -929,16 +603,14 @@ async def validate_model( if not config: raise HTTPException( status_code = 400, - detail = f"Invalid model identifier: {model_log_label}", + detail = f"Invalid model identifier: {request.model_path}", ) return ValidateModelResponse( valid = True, message = "Model identifier is valid.", - identifier = model_log_label if native_grant_backed else config.identifier, - display_name = model_log_label - if native_grant_backed - else getattr(config, "display_name", config.identifier), + identifier = config.identifier, + display_name = getattr(config, "display_name", config.identifier), is_gguf = getattr(config, "is_gguf", False), is_lora = getattr(config, "is_lora", False), is_vision = getattr(config, "is_vision", False), @@ -950,26 +622,6 @@ async def validate_model( except HTTPException: raise except Exception as e: - not_supported_hints = [ - "No config file found", - "not yet supported", - "is not supported", - "does not support", - ] - if native_grant_backed: - redacted_msg = redact_native_paths(str(e)) - logger.error( - "Error validating native model %s: %s", - model_log_label, - redacted_msg, - ) - msg = redacted_msg - if any(h.lower() in msg.lower() for h in not_supported_hints): - msg = f"This model is not supported yet. Try a different model. (Original error: {msg})" - raise HTTPException( - status_code = 400, - detail = f"Invalid native model {model_log_label}: {msg}", - ) logger.error( f"Error validating model identifier '{request.model_path}': {e}", exc_info = True, @@ -994,9 +646,6 @@ async def unload_model( llama_backend = get_llama_cpp_backend() if llama_backend.is_active and ( llama_backend.model_identifier == request.model_path - or is_registered_native_path_label( - llama_backend.model_identifier, request.model_path - ) or not llama_backend.is_loaded ): llama_backend.unload_model() @@ -1014,48 +663,6 @@ async def unload_model( raise HTTPException(status_code = 500, detail = f"Failed to unload model: {str(e)}") -@studio_router.post("/cancel") -async def cancel_inference( - request: Request, - current_subject: str = Depends(get_current_subject), -): - """Cancel in-flight inference requests. - - Body (JSON, at least one key required): - cancel_id - preferred: per-run UUID, matched exclusively. - session_id - fallback when cancel_id is absent. - completion_id - fallback when cancel_id is absent. - - A cancel_id arriving before its stream registers is stashed briefly - and replayed on registration. Returns {"cancelled": N}. - """ - try: - body = await request.json() - if not isinstance(body, dict): - body = {} - except Exception as e: - logger.debug("Failed to parse cancel request body: %s", e) - body = {} - - cancel_id = body.get("cancel_id") - if isinstance(cancel_id, str) and cancel_id: - return {"cancelled": _cancel_by_cancel_id_or_stash(cancel_id)} - - keys = [] - # `message_id` is the Anthropic passthrough's per-run identifier -- - # included so /v1/messages clients can cancel by their native id. - for k in ("completion_id", "session_id", "message_id"): - v = body.get(k) - if isinstance(v, str) and v: - keys.append(v) - - if not keys: - return {"cancelled": 0} - - n = _cancel_by_keys(keys) - return {"cancelled": n} - - @router.post("/generate/stream") async def generate_stream( request: GenerateRequest, @@ -1144,37 +751,23 @@ async def get_status( # If a GGUF model is loaded via llama-server, report that if llama_backend.is_loaded: _model_id = llama_backend.model_identifier - _native_grant_backed = getattr(llama_backend, "_native_grant_backed", False) - _display_model_id = getattr( - llama_backend, "_native_display_label", None - ) or display_label_for_native_path(_model_id) - if ( - _native_grant_backed - and _model_id - and _display_model_id == _model_id - and os.path.isabs(_model_id) - ): - _display_model_id = os.path.basename(_model_id) _inference_cfg = load_inference_config(_model_id) if _model_id else None return InferenceStatusResponse( - active_model = _display_model_id, + active_model = _model_id, is_vision = llama_backend.is_vision, is_gguf = True, gguf_variant = llama_backend.hf_variant, is_audio = getattr(llama_backend, "_is_audio", False), audio_type = getattr(llama_backend, "_audio_type", None), loading = [], - loaded = [_display_model_id] if _display_model_id else [], + loaded = [_model_id], inference = _inference_cfg, requires_trust_remote_code = bool( (_inference_cfg or {}).get("trust_remote_code", False) ), supports_reasoning = llama_backend.supports_reasoning, - reasoning_style = llama_backend.reasoning_style, reasoning_always_on = llama_backend.reasoning_always_on, - supports_preserve_thinking = llama_backend.supports_preserve_thinking, supports_tools = llama_backend.supports_tools, - chat_template = llama_backend.chat_template, context_length = llama_backend.context_length, max_context_length = llama_backend.max_context_length, native_context_length = llama_backend.native_context_length, @@ -1188,32 +781,17 @@ async def get_status( is_audio = False audio_type = None has_audio_input = False - model_info = {} if backend.active_model_name: model_info = backend.models.get(backend.active_model_name, {}) is_vision = model_info.get("is_vision", False) is_audio = model_info.get("is_audio", False) audio_type = model_info.get("audio_type") has_audio_input = model_info.get("has_audio_input", False) - chat_template_info = model_info.get("chat_template_info", {}) - chat_template = ( - chat_template_info.get("template") - if isinstance(chat_template_info, dict) - else None - ) - # Non-GGUF: only gpt-oss Harmony is wired through the transformers - # generation path. Other template-level reasoning / tool kwargs - # are not yet forwarded, so we do not advertise them here. + # gpt-oss safetensors models support reasoning via harmony channels supports_reasoning = False - reasoning_style = "enable_thinking" if backend.active_model_name and hasattr(backend, "_is_gpt_oss_model"): - try: - if backend._is_gpt_oss_model(): - supports_reasoning = True - reasoning_style = "reasoning_effort" - except Exception: - pass + supports_reasoning = backend._is_gpt_oss_model() inference_config = ( load_inference_config(backend.active_model_name) if backend.active_model_name @@ -1234,11 +812,6 @@ async def get_status( (inference_config or {}).get("trust_remote_code", False) ), supports_reasoning = supports_reasoning, - reasoning_style = reasoning_style, - reasoning_always_on = False, - supports_preserve_thinking = False, - supports_tools = False, - chat_template = chat_template, ) except Exception as e: @@ -1484,20 +1057,6 @@ async def openai_chat_completions( llama_backend = get_llama_cpp_backend() using_gguf = llama_backend.is_loaded - # OpenAI-SDK clients send ``chat_template_kwargs`` via ``extra_body``, - # which the SDK spreads into the request body at the top level. Studio's - # ChatCompletionRequest has ``extra="allow"`` so pydantic stashes them in - # ``model_extra``, but the typed ``payload.enable_thinking`` path is what - # downstream generators actually consume. Lift ``enable_thinking`` from - # the extra-body chat_template_kwargs onto the typed field so clients - # that only know the OpenAI shape (data_designer recipe runs, etc.) - # can still control the reasoning preamble. - _extra = getattr(payload, "model_extra", None) - if payload.enable_thinking is None and isinstance(_extra, dict): - _tpl_kw = _extra.get("chat_template_kwargs") - if isinstance(_tpl_kw, dict) and "enable_thinking" in _tpl_kw: - payload.enable_thinking = bool(_tpl_kw["enable_thinking"]) - # ── Determine which backend is active ───────────────────── if using_gguf: model_name = llama_backend.model_identifier or payload.model @@ -1553,9 +1112,6 @@ def audio_input_generate(): ) if payload.stream: - _cancel_keys = (payload.cancel_id, payload.session_id, completion_id) - _tracker = _TrackedCancel(cancel_event, *_cancel_keys) - _tracker.__enter__() async def audio_input_stream(): try: @@ -1572,17 +1128,10 @@ async def audio_input_stream(): ) yield f"data: {first_chunk.model_dump_json(exclude_none = True)}\n\n" - gen = audio_input_generate() - _DONE = object() - while True: - if cancel_event.is_set(): - break + for chunk_text in audio_input_generate(): if await request.is_disconnected(): cancel_event.set() return - chunk_text = await asyncio.to_thread(next, gen, _DONE) - if chunk_text is _DONE: - break if chunk_text: chunk = ChatCompletionChunk( id = completion_id, @@ -1615,8 +1164,6 @@ async def audio_input_stream(): f"Error during audio input streaming: {e}", exc_info = True ) yield f"data: {json.dumps({'error': {'message': _friendly_error(e), 'type': 'server_error'}})}\n\n" - finally: - _tracker.__exit__(None, None, None) return StreamingResponse( audio_input_stream(), @@ -1652,22 +1199,11 @@ async def audio_input_stream(): # carry `tool_calls` (content=None) — both of which are valid in # multi-turn client-side tool loops. _has_tool_messages = any(m.role == "tool" or m.tool_calls for m in payload.messages) - # Route guided-decoding requests through the verbatim passthrough so - # ``response_format`` (JSON schema) actually reaches llama-server and - # the model's GBNF-constrained output comes back unmodified. The - # non-passthrough GGUF path below calls ``generate_chat_completion`` - # which has no response_format kwarg, so the schema gets silently - # dropped and data_designer falls back to free-form sampling. Guided - # decoding does not require ``supports_tools`` - the grammar machinery - # is independent of tool-call parsing. - _has_response_format = _extract_response_format(payload) is not None - _tools_passthrough = llama_backend.supports_tools and ( - (payload.tools and len(payload.tools) > 0) or _has_tool_messages - ) if ( using_gguf - and not _effective_enable_tools(payload) - and (_tools_passthrough or _has_response_format) + and llama_backend.supports_tools + and not payload.enable_tools + and ((payload.tools and len(payload.tools) > 0) or _has_tool_messages) ): # Preserve the vision guard that would otherwise run in the # non-passthrough path below: text-only tool-capable GGUFs @@ -1757,13 +1293,8 @@ async def audio_input_stream(): created = int(time.time()) # ── Tool-calling path (agentic loop) ────────────────── - # `_effective_enable_tools` lets `unsloth run --enable-tools/--disable-tools` - # hard-override the per-request value. Without a CLI override, falls - # back to `payload.enable_tools` (existing behavior). use_tools = ( - _effective_enable_tools(payload) - and llama_backend.supports_tools - and not image_b64 + payload.enable_tools and llama_backend.supports_tools and not image_b64 ) if use_tools: @@ -1862,8 +1393,6 @@ def gguf_generate_with_tools(): presence_penalty = payload.presence_penalty, cancel_event = cancel_event, enable_thinking = payload.enable_thinking, - reasoning_effort = payload.reasoning_effort, - preserve_thinking = payload.preserve_thinking, auto_heal_tool_calls = payload.auto_heal_tool_calls if payload.auto_heal_tool_calls is not None else True, @@ -1878,10 +1407,6 @@ def gguf_generate_with_tools(): _tool_sentinel = object() - _cancel_keys = (payload.cancel_id, payload.session_id, completion_id) - _tracker = _TrackedCancel(cancel_event, *_cancel_keys) - _tracker.__enter__() - async def gguf_tool_stream(): try: first_chunk = ChatCompletionChunk( @@ -1904,8 +1429,6 @@ async def gguf_tool_stream(): _stream_usage = None _stream_timings = None while True: - if cancel_event.is_set(): - break if await request.is_disconnected(): cancel_event.set() return @@ -2013,8 +1536,6 @@ async def gguf_tool_stream(): }, } yield f"data: {json.dumps(error_chunk)}\n\n" - finally: - _tracker.__exit__(None, None, None) return StreamingResponse( gguf_tool_stream(), @@ -2041,16 +1562,11 @@ def gguf_generate(): presence_penalty = payload.presence_penalty, cancel_event = cancel_event, enable_thinking = payload.enable_thinking, - reasoning_effort = payload.reasoning_effort, - preserve_thinking = payload.preserve_thinking, ) _gguf_sentinel = object() if payload.stream: - _cancel_keys = (payload.cancel_id, payload.session_id, completion_id) - _tracker = _TrackedCancel(cancel_event, *_cancel_keys) - _tracker.__enter__() async def gguf_stream_chunks(): try: @@ -2075,8 +1591,6 @@ async def gguf_stream_chunks(): _stream_usage = None _stream_timings = None while True: - if cancel_event.is_set(): - break if await request.is_disconnected(): cancel_event.set() return @@ -2160,8 +1674,6 @@ async def gguf_stream_chunks(): }, } yield f"data: {json.dumps(error_chunk)}\n\n" - finally: - _tracker.__exit__(None, None, None) return StreamingResponse( gguf_stream_chunks(), @@ -2261,9 +1773,6 @@ def generate(): # ── Streaming response ──────────────────────────────────────── if payload.stream: - _cancel_keys = (payload.cancel_id, payload.session_id, completion_id) - _tracker = _TrackedCancel(cancel_event, *_cancel_keys) - _tracker.__enter__() async def stream_chunks(): try: @@ -2291,9 +1800,6 @@ async def stream_chunks(): loop = asyncio.get_event_loop() gen = generate() while True: - if cancel_event.is_set(): - backend.reset_generation_state() - break # next(gen, _DONE) returns _DONE instead of raising # StopIteration — StopIteration cannot propagate # through asyncio futures (Python limitation). @@ -2349,8 +1855,6 @@ async def stream_chunks(): }, } yield f"data: {json.dumps(error_chunk)}\n\n" - finally: - _tracker.__exit__(None, None, None) return StreamingResponse( stream_chunks(), @@ -3031,9 +2535,7 @@ async def _responses_stream( ), ) - body = _build_openai_passthrough_body( - chat_req, backend_ctx = llama_backend.context_length - ) + body = _build_openai_passthrough_body(chat_req) target_url = f"{llama_backend.base_url}/v1/chat/completions" async def event_generator(): @@ -3487,9 +2989,7 @@ async def anthropic_messages( # Server-side agentic loop doesn't support multimodal input — matches # the `not image_b64` gate in /v1/chat/completions. server_tools = ( - _effective_enable_tools(payload) - and llama_backend.supports_tools - and not _has_image + payload.enable_tools and llama_backend.supports_tools and not _has_image ) client_tools = ( not server_tools @@ -3520,8 +3020,6 @@ async def anthropic_messages( repetition_penalty = repetition_penalty, presence_penalty = presence_penalty, tool_choice = openai_tool_choice, - session_id = payload.session_id, - cancel_id = payload.cancel_id, ) return await _anthropic_passthrough_non_streaming( llama_backend, @@ -3882,9 +3380,6 @@ def _build_passthrough_payload( repetition_penalty = None, presence_penalty = None, tool_choice = "auto", - response_format = None, - chat_template_kwargs = None, - backend_ctx = None, ): body = { "messages": openai_messages, @@ -3897,12 +3392,8 @@ def _build_passthrough_payload( } if stream: body["stream_options"] = {"include_usage": True} - body["max_tokens"] = ( - max_tokens - if max_tokens is not None - else (backend_ctx or _DEFAULT_MAX_TOKENS_FLOOR) - ) - body["t_max_predict_ms"] = _DEFAULT_T_MAX_PREDICT_MS + if max_tokens is not None: + body["max_tokens"] = max_tokens if stop: body["stop"] = stop if min_p is not None: @@ -3912,17 +3403,6 @@ def _build_passthrough_payload( body["repeat_penalty"] = repetition_penalty if presence_penalty is not None: body["presence_penalty"] = presence_penalty - if response_format is not None: - # llama-server applies a GBNF grammar derived from the JSON schema - # when response_format is present. Field is documented flat at the - # request root (tools/server/README.md), which is also what the - # OpenAI SDK produces by spreading extra_body into the body top. - body["response_format"] = response_format - if chat_template_kwargs is not None: - # Propagate reasoning / template overrides (e.g. enable_thinking) - # so llama-server renders the Jinja template in the mode the caller - # asked for instead of whatever default the model was loaded with. - body["chat_template_kwargs"] = chat_template_kwargs return body @@ -3943,8 +3423,6 @@ async def _anthropic_passthrough_stream( repetition_penalty = None, presence_penalty = None, tool_choice = "auto", - session_id = None, - cancel_id = None, ): """Streaming client-side pass-through: forward tools to llama-server and translate its streaming response to Anthropic SSE without executing anything.""" @@ -3962,14 +3440,8 @@ async def _anthropic_passthrough_stream( repetition_penalty = repetition_penalty, presence_penalty = presence_penalty, tool_choice = tool_choice, - backend_ctx = llama_backend.context_length, ) - # cancel_id mirrors the OpenAI passthrough so a per-run cancel POST - # works without the caller having to know the local message_id. - _tracker = _TrackedCancel(cancel_event, cancel_id, session_id, message_id) - _tracker.__enter__() - async def _stream(): emitter = AnthropicPassthroughEmitter() for line in emitter.start(message_id, model_name): @@ -4002,28 +3474,15 @@ async def _stream(): # has anything orphaned to finalize. Each aclose is wrapped in # `try: ... except Exception: pass` so anyio cleanup noise from # nested aclose paths can't bubble out. - client = httpx.AsyncClient( - timeout = 600, - limits = httpx.Limits(max_keepalive_connections = 0), - ) + client = httpx.AsyncClient(timeout = 600) resp = None lines_iter = None - cancel_watcher = None try: req = client.build_request("POST", target_url, json = body) resp = await client.send(req, stream = True) - # See _openai_passthrough_stream for rationale: aiter_lines() - # blocks during llama-server prefill, so the in-loop cancel - # check is unreachable until the first SSE chunk arrives. - # The watcher closes `resp` on cancel, raising in aiter_lines. - cancel_watcher = asyncio.create_task( - _await_cancel_then_close(cancel_event, resp) - ) lines_iter = resp.aiter_lines() async for raw_line in lines_iter: - if cancel_event.is_set(): - break if await request.is_disconnected(): cancel_event.set() break @@ -4038,18 +3497,9 @@ async def _stream(): continue for line in emitter.feed_chunk(chunk): yield line - except (httpx.RemoteProtocolError, httpx.ReadError, httpx.CloseError): - if not cancel_event.is_set(): - raise except Exception as e: logger.error("anthropic_messages passthrough stream error: %s", e) finally: - if cancel_watcher is not None: - cancel_watcher.cancel() - try: - await cancel_watcher - except (asyncio.CancelledError, Exception): - pass if lines_iter is not None: try: await lines_iter.aclose() @@ -4064,7 +3514,6 @@ async def _stream(): await client.aclose() except Exception: pass - _tracker.__exit__(None, None, None) for line in emitter.finish(): yield line @@ -4111,7 +3560,6 @@ async def _anthropic_passthrough_non_streaming( repetition_penalty = repetition_penalty, presence_penalty = presence_penalty, tool_choice = tool_choice, - backend_ctx = llama_backend.context_length, ) async with httpx.AsyncClient() as client: @@ -4233,21 +3681,7 @@ def _openai_messages_for_passthrough(payload) -> list[dict]: return messages -def _extract_response_format(payload): - """Return the ``response_format`` field on an incoming ChatCompletionRequest - (or None). The model is declared with ``extra="allow"`` so pydantic stashes - unknown top-level fields in ``model_extra``; OpenAI-SDK clients spread - ``extra_body`` into the request body top level, which is where guided- - decoding recipes park their JSON-schema response_format. - """ - extra = getattr(payload, "model_extra", None) - if not isinstance(extra, dict): - return None - rf = extra.get("response_format") - return rf if isinstance(rf, dict) else None - - -def _build_openai_passthrough_body(payload, backend_ctx = None) -> dict: +def _build_openai_passthrough_body(payload) -> dict: """Assemble the llama-server request body from a ChatCompletionRequest. Only explicitly-known OpenAI / llama-server fields are forwarded so that @@ -4256,12 +3690,6 @@ def _build_openai_passthrough_body(payload, backend_ctx = None) -> dict: """ messages = _openai_messages_for_passthrough(payload) tool_choice = payload.tool_choice if payload.tool_choice is not None else "auto" - # When the caller asked for a specific reasoning mode, forward it to - # llama-server via chat_template_kwargs so the Jinja template renders - # with (or without) the reasoning preamble. - tpl_kwargs = None - if payload.enable_thinking is not None: - tpl_kwargs = {"enable_thinking": bool(payload.enable_thinking)} return _build_passthrough_payload( messages, payload.tools, @@ -4275,9 +3703,6 @@ def _build_openai_passthrough_body(payload, backend_ctx = None) -> dict: repetition_penalty = payload.repetition_penalty, presence_penalty = payload.presence_penalty, tool_choice = tool_choice, - response_format = _extract_response_format(payload), - chat_template_kwargs = tpl_kwargs, - backend_ctx = backend_ctx, ) @@ -4298,56 +3723,103 @@ async def _openai_passthrough_stream( observes a standard OpenAI response. """ target_url = f"{llama_backend.base_url}/v1/chat/completions" - body = _build_openai_passthrough_body( - payload, backend_ctx = llama_backend.context_length - ) - - _cancel_keys = (payload.cancel_id, payload.session_id, completion_id) - _tracker = _TrackedCancel(cancel_event, *_cancel_keys) - _tracker.__enter__() - - # Outer guard: asyncio.CancelledError at `await client.send(...)` is - # a BaseException that bypasses `except httpx.RequestError`; without - # this the tracker leaks. The generator's finally only runs once - # iteration starts. + body = _build_openai_passthrough_body(payload) + + # Dispatch the upstream request BEFORE returning StreamingResponse so + # transport errors and non-200 upstream statuses surface as real HTTP + # errors to the client. OpenAI SDKs rely on status codes to raise + # ``APIError``/``BadRequestError``/...; burying the failure inside a + # 200 SSE ``error`` frame silently breaks their error handling. + client = httpx.AsyncClient(timeout = 600) + resp = None try: - # Dispatch BEFORE returning StreamingResponse so transport errors - # and non-200 upstream statuses surface as real HTTP errors -- - # OpenAI SDKs rely on status codes to raise APIError/BadRequestError. - client = httpx.AsyncClient( - timeout = 600, - limits = httpx.Limits(max_keepalive_connections = 0), + req = client.build_request("POST", target_url, json = body) + resp = await client.send(req, stream = True) + except httpx.RequestError as e: + # llama-server subprocess crashed / still starting / unreachable. + logger.error("openai passthrough stream: upstream unreachable: %s", e) + if resp is not None: + try: + await resp.aclose() + except Exception: + pass + try: + await client.aclose() + except Exception: + pass + raise HTTPException( + status_code = 502, + detail = _friendly_error(e), ) - resp = None + + if resp.status_code != 200: + err_bytes = await resp.aread() + err_text = err_bytes.decode("utf-8", errors = "replace") + logger.error( + "openai passthrough upstream error: status=%s body=%s", + resp.status_code, + err_text[:500], + ) + upstream_status = resp.status_code try: - req = client.build_request("POST", target_url, json = body) - resp = await client.send(req, stream = True) - except httpx.RequestError as e: - # llama-server subprocess crashed / still starting / unreachable. - logger.error("openai passthrough stream: upstream unreachable: %s", e) - if resp is not None: + await resp.aclose() + except Exception: + pass + try: + await client.aclose() + except Exception: + pass + raise HTTPException( + status_code = upstream_status, + detail = f"llama-server error: {err_text[:500]}", + ) + + async def _stream(): + # Same httpx lifecycle pattern as _anthropic_passthrough_stream: + # avoid `async with` on the client/response AND explicitly save + # resp.aiter_lines() so we can close it ourselves in the finally + # block. See the long comment there for the full rationale on + # why the anonymous `async for raw_line in resp.aiter_lines():` + # pattern leaks an unclosed async generator that Python's + # asyncgen GC hook then finalizes in a different asyncio task, + # producing "Exception ignored in:" / "async generator ignored + # GeneratorExit" / anyio cancel-scope traces on Python 3.13 + + # httpcore 1.0.x. + lines_iter = None + try: + lines_iter = resp.aiter_lines() + async for raw_line in lines_iter: + if await request.is_disconnected(): + cancel_event.set() + break + if not raw_line: + continue + if not raw_line.startswith("data: "): + continue + # Relay the llama-server SSE chunk verbatim so the client + # sees its native `id`, `finish_reason`, `delta.tool_calls`, + # and final `usage` unchanged. + yield raw_line + "\n\n" + if raw_line[6:].strip() == "[DONE]": + break + except Exception as e: + # Mid-stream failures still have to be reported inside the SSE + # body because the 200 response headers have already been + # committed by the time the first chunk flushes. + logger.error("openai passthrough stream error: %s", e) + err = { + "error": { + "message": _friendly_error(e), + "type": "server_error", + }, + } + yield f"data: {json.dumps(err)}\n\n" + finally: + if lines_iter is not None: try: - await resp.aclose() + await lines_iter.aclose() except Exception: pass - try: - await client.aclose() - except Exception: - pass - raise HTTPException( - status_code = 502, - detail = _friendly_error(e), - ) - - if resp.status_code != 200: - err_bytes = await resp.aread() - err_text = err_bytes.decode("utf-8", errors = "replace") - logger.error( - "openai passthrough upstream error: status=%s body=%s", - resp.status_code, - err_text[:500], - ) - upstream_status = resp.status_code try: await resp.aclose() except Exception: @@ -4356,91 +3828,16 @@ async def _openai_passthrough_stream( await client.aclose() except Exception: pass - raise HTTPException( - status_code = upstream_status, - detail = f"llama-server error: {err_text[:500]}", - ) - - async def _stream(): - # Same httpx lifecycle pattern as _anthropic_passthrough_stream: - # save resp.aiter_lines() so the finally block can aclose() it - # on our task. See that function for full rationale. - lines_iter = None - # During llama-server prefill, `aiter_lines()` blocks until the - # first SSE chunk arrives. The in-loop `cancel_event` check - # cannot fire until then, which is the exact proxy/Colab - # scenario the cancel POST is meant to recover from. Run a - # tiny watcher that closes `resp` as soon as cancel fires, - # unblocking the iterator with a RemoteProtocolError caught - # in the except clause below. - cancel_watcher = asyncio.create_task( - _await_cancel_then_close(cancel_event, resp) - ) - try: - lines_iter = resp.aiter_lines() - async for raw_line in lines_iter: - if cancel_event.is_set(): - break - if await request.is_disconnected(): - cancel_event.set() - break - if not raw_line: - continue - if not raw_line.startswith("data: "): - continue - # Relay verbatim to preserve llama-server's native id, - # finish_reason, delta.tool_calls, and usage chunks. - yield raw_line + "\n\n" - if raw_line[6:].strip() == "[DONE]": - break - except (httpx.RemoteProtocolError, httpx.ReadError, httpx.CloseError): - # Watcher closed resp on cancel. Emit nothing extra; the - # client either initiated the cancel or already disconnected. - if not cancel_event.is_set(): - raise - except Exception as e: - # 200 headers are already flushed; errors must be in the SSE body. - logger.error("openai passthrough stream error: %s", e) - err = { - "error": { - "message": _friendly_error(e), - "type": "server_error", - }, - } - yield f"data: {json.dumps(err)}\n\n" - finally: - cancel_watcher.cancel() - try: - await cancel_watcher - except (asyncio.CancelledError, Exception): - pass - if lines_iter is not None: - try: - await lines_iter.aclose() - except Exception: - pass - try: - await resp.aclose() - except Exception: - pass - try: - await client.aclose() - except Exception: - pass - _tracker.__exit__(None, None, None) - return StreamingResponse( - _stream(), - media_type = "text/event-stream", - headers = { - "Cache-Control": "no-cache", - "Connection": "keep-alive", - "X-Accel-Buffering": "no", - }, - ) - except BaseException: - _tracker.__exit__(None, None, None) - raise + return StreamingResponse( + _stream(), + media_type = "text/event-stream", + headers = { + "Cache-Control": "no-cache", + "Connection": "keep-alive", + "X-Accel-Buffering": "no", + }, + ) async def _openai_passthrough_non_streaming( @@ -4456,9 +3853,7 @@ async def _openai_passthrough_non_streaming( token counts. """ target_url = f"{llama_backend.base_url}/v1/chat/completions" - body = _build_openai_passthrough_body( - payload, backend_ctx = llama_backend.context_length - ) + body = _build_openai_passthrough_body(payload) try: async with httpx.AsyncClient() as client: @@ -4479,41 +3874,6 @@ async def _openai_passthrough_non_streaming( detail = f"llama-server error: {resp.text[:500]}", ) - # Guided-decoding fence wrap. llama-server returns raw JSON that matches - # the schema (no surrounding markdown) because the GBNF grammar only - # emits the JSON object itself. data_designer's llm-structured parser - # looks for a ```json ... ``` markdown fence and discards unfenced - # output, which collapses a 100%-valid guided-decoding run to 0/N. - # Wrap each choice's content in the expected fence when the caller - # asked for guided decoding, leaving already-fenced content alone. - if _extract_response_format(payload) is not None: - try: - data = resp.json() - changed = False - for choice in data.get("choices", []): - if not isinstance(choice, dict): - continue - msg = choice.get("message") - if not isinstance(msg, dict): - continue - content = msg.get("content") - if not isinstance(content, str): - continue - stripped = content.strip() - if not stripped or stripped.startswith("```"): - continue - msg["content"] = f"```json\n{stripped}\n```" - changed = True - if changed: - return JSONResponse(content = data) - except Exception as exc: - # Wrap is best-effort; fall through to the verbatim body if - # the response is not JSON-shaped or the structure is unusual. - logger.warning( - "response_format fence wrap skipped: %s", - exc, - ) - # Pass the upstream body through as raw bytes — skips a redundant # parse+re-serialize round-trip and keeps the response truly # verbatim (matches the docstring). Status is guaranteed 200 by diff --git a/studio/backend/routes/models.py b/studio/backend/routes/models.py index d01e94b0c9..db27ce1907 100644 --- a/studio/backend/routes/models.py +++ b/studio/backend/routes/models.py @@ -8,7 +8,6 @@ import hashlib import json import os -import shutil import sys import uuid from pathlib import Path @@ -624,7 +623,7 @@ def _make_link(link_dir: Path, link_name: str, target: Path) -> Optional[str]: gguf_link_path: Optional[str] = None quant = f"-{file_type}" if file_type else "" safe_name = repo_name.replace("/", "-") - for layer in manifest.get("layers") or []: + for layer in manifest.get("layers", []): media = layer.get("mediaType", "") digest = layer.get("digest", "") if not digest: @@ -1685,338 +1684,6 @@ async def scan_loras( ) -def _is_path_under(path: Path, root: Path) -> bool: - try: - path.resolve().relative_to(root.resolve()) - return True - except ValueError: - return False - - -def _is_path_under_lexically(path: Path, root: Path) -> bool: - """Check containment without resolving the final path's symlink target.""" - try: - absolute_path = Path(os.path.abspath(str(path))) - absolute_root = Path(os.path.abspath(str(root))) - absolute_path.relative_to(absolute_root) - return True - except ValueError: - return False - - -def _loaded_model_matches_deleted_path(active_model: str, deleted_path: Path) -> bool: - try: - active = Path(active_model).expanduser().resolve() - target = deleted_path.resolve() - return active == target or (target.is_dir() and active.is_relative_to(target)) - except (OSError, RuntimeError, ValueError) as e: - logger.debug( - "Could not resolve loaded/deleted model paths; falling back to string comparison: %s", - e, - ) - active_lower = active_model.lower() - target_lower = str(deleted_path).lower() - return active_lower == target_lower or active_lower.startswith( - f"{target_lower}{os.sep}" - ) - - -def _loading_model_matches_deleted_path( - loading_model: object, - deleted_path: Path, -) -> bool: - if not loading_model: - return False - return _loaded_model_matches_deleted_path(str(loading_model), deleted_path) - - -def _prune_empty_parents(start: Path, stop_at: Path) -> None: - """Remove empty ancestor directories of ``start`` up to (but not including) ``stop_at``. - - Used after deleting a model checkpoint so the enclosing run directory does - not linger as an empty entry in scan results. - """ - try: - stop_resolved = stop_at.resolve() - except OSError: - return - parent = start.parent - while True: - try: - parent_resolved = parent.resolve() - except OSError: - return - if parent_resolved == stop_resolved: - return - try: - parent_resolved.relative_to(stop_resolved) - except ValueError: - return - try: - parent.rmdir() - except OSError: - return - parent = parent.parent - - -def _delete_gguf_variant_files(root: Path, variant: str) -> tuple[int, int]: - deleted_count = 0 - deleted_bytes = 0 - for path in root.rglob("*"): - if not path.is_file() or not _is_main_gguf_filename(path.name): - continue - if _extract_quant_label(path.name).lower() != variant.lower(): - continue - try: - deleted_bytes += path.stat().st_size - except OSError: - pass - path.unlink() - deleted_count += 1 - return deleted_count, deleted_bytes - - -@router.delete("/delete-finetuned") -async def delete_finetuned_model( - model_path: str = Body(...), - source: str = Body(...), - export_type: Optional[str] = Body(None), - gguf_variant: Optional[str] = Body(None), - current_subject: str = Depends(get_current_subject), -): - """Delete a Studio-trained or exported model from disk. - - Only paths under Studio's outputs/exports roots are accepted. Exported - GGUF entries can delete one quantization variant at a time. - """ - if source not in {"training", "exported"}: - raise HTTPException( - status_code = 400, - detail = "Only trained or exported Studio models can be deleted", - ) - - if not model_path or not model_path.strip(): - raise HTTPException(status_code = 400, detail = "model_path is required") - - if export_type == "gguf" and not gguf_variant: - raise HTTPException( - status_code = 400, - detail = "gguf_variant is required when export_type is 'gguf'", - ) - - raw_path = Path(model_path).expanduser() - if source == "training": - target_path = raw_path - allowed_root = outputs_root() - else: - allowed_root = exports_root() - target_path = ( - raw_path.parent - if export_type == "gguf" and raw_path.suffix.lower() == ".gguf" - else raw_path - ) - - allowed_root = allowed_root.resolve() - delete_path = Path(os.path.abspath(str(target_path))) - delete_path_is_symlink = delete_path.is_symlink() - - if delete_path_is_symlink: - if not _is_path_under_lexically(delete_path, allowed_root): - raise HTTPException( - status_code = 400, - detail = "Model path is outside Studio storage", - ) - if export_type == "gguf" and gguf_variant: - target_path = delete_path.resolve() - if not _is_path_under(target_path, allowed_root): - raise HTTPException( - status_code = 400, - detail = "Model path is outside Studio storage", - ) - else: - target_path = delete_path - else: - target_path = target_path.resolve() - - should_check_resolved_path = not delete_path_is_symlink or ( - export_type == "gguf" and gguf_variant - ) - if should_check_resolved_path and not _is_path_under(target_path, allowed_root): - raise HTTPException( - status_code = 400, - detail = "Model path is outside Studio storage", - ) - if target_path == allowed_root: - raise HTTPException( - status_code = 400, - detail = "Refusing to delete storage root", - ) - if not target_path.exists() and not target_path.is_symlink(): - raise HTTPException(status_code = 404, detail = "Model not found on disk") - - if source == "training": - try: - from core.training import get_training_backend - - training_backend = get_training_backend() - if training_backend.is_training_active(): - raise HTTPException( - status_code = 409, - detail = "Cannot delete trained models while training is running", - ) - except HTTPException: - raise - except Exception as e: - logger.warning("Could not check training status before delete: %s", e) - raise HTTPException( - status_code = 500, - detail = "Could not verify training status before deleting", - ) from e - - try: - from routes.inference import get_llama_cpp_backend - - llama_backend = get_llama_cpp_backend() - if ( - llama_backend.is_active - and not llama_backend.is_loaded - and llama_backend.model_identifier - and _loaded_model_matches_deleted_path( - llama_backend.model_identifier, - target_path, - ) - and ( - not gguf_variant - or not llama_backend.hf_variant - or llama_backend.hf_variant.lower() == gguf_variant.lower() - ) - ): - raise HTTPException( - status_code = 409, - detail = "Cannot delete a model while it is loading", - ) - if ( - llama_backend.is_loaded - and llama_backend.model_identifier - and _loaded_model_matches_deleted_path( - llama_backend.model_identifier, - target_path, - ) - and ( - not gguf_variant - or not llama_backend.hf_variant - or llama_backend.hf_variant.lower() == gguf_variant.lower() - ) - ): - raise HTTPException( - status_code = 400, - detail = "Unload the model before deleting", - ) - except HTTPException: - raise - except Exception as e: - logger.warning("Could not check llama.cpp loaded model before delete: %s", e) - raise HTTPException( - status_code = 503, - detail = "Could not verify model load status before deleting", - ) from e - - try: - inference_backend = get_inference_backend() - loading_models = getattr(inference_backend, "loading_models", set()) - if any( - _loading_model_matches_deleted_path(loading_model, target_path) - for loading_model in loading_models - ): - raise HTTPException( - status_code = 409, - detail = "Cannot delete a model while it is loading", - ) - if inference_backend.active_model_name: - if _loaded_model_matches_deleted_path( - inference_backend.active_model_name, - target_path, - ): - raise HTTPException( - status_code = 400, - detail = "Unload the model before deleting", - ) - except HTTPException: - raise - except Exception as e: - logger.warning( - "Could not check inference backend loaded model before delete: %s", e - ) - raise HTTPException( - status_code = 503, - detail = "Could not verify model load status before deleting", - ) from e - - try: - if export_type == "gguf" and gguf_variant: - if not target_path.is_dir(): - raise HTTPException( - status_code = 400, - detail = "GGUF variant deletion requires an export directory", - ) - deleted_count, deleted_bytes = _delete_gguf_variant_files( - target_path, - gguf_variant, - ) - if deleted_count == 0: - raise HTTPException( - status_code = 404, - detail = f"Variant {gguf_variant} not found on disk", - ) - try: - if not any(target_path.iterdir()): - target_path.rmdir() - _prune_empty_parents(target_path, allowed_root) - except OSError: - pass - logger.info( - "Deleted %s GGUF file(s) for exported model at %s variant %s (%0.1f MB freed)", - deleted_count, - target_path, - gguf_variant, - deleted_bytes / (1024 * 1024), - ) - return { - "status": "deleted", - "path": str(target_path), - "gguf_variant": gguf_variant, - } - - if target_path.is_symlink() or target_path.is_file(): - target_path.unlink() - else: - shutil.rmtree(target_path) - - if target_path.exists() or target_path.is_symlink(): - raise HTTPException( - status_code = 500, - detail = "Deletion incomplete; some files could not be removed", - ) - - _prune_empty_parents(target_path, allowed_root) - - logger.info("Deleted fine-tuned model at %s", target_path) - return {"status": "deleted", "path": str(target_path)} - except HTTPException: - raise - except Exception as e: - logger.error( - "Error deleting fine-tuned model %s: %s", - target_path, - e, - exc_info = True, - ) - raise HTTPException( - status_code = 500, - detail = f"Failed to delete fine-tuned model: {str(e)}", - ) - - @router.get("/loras/{lora_path:path}/base-model", response_model = LoRABaseModelResponse) async def get_lora_base_model( lora_path: str, diff --git a/studio/backend/routes/training.py b/studio/backend/routes/training.py index 19202f3883..e625408bad 100644 --- a/studio/backend/routes/training.py +++ b/studio/backend/routes/training.py @@ -25,12 +25,6 @@ # Import backend functions try: from core.training import get_training_backend - from core.training.resume import ( - can_resume_run, - get_resume_checkpoint_path, - normalize_resume_output_dir, - ) - from storage.studio_db import get_resumable_run_by_output_dir from utils.models.model_config import load_model_defaults from utils.paths import resolve_dataset_path except ImportError: @@ -39,12 +33,6 @@ if str(parent_backend) not in sys.path: sys.path.insert(0, str(parent_backend)) from core.training import get_training_backend - from core.training.resume import ( - can_resume_run, - get_resume_checkpoint_path, - normalize_resume_output_dir, - ) - from storage.studio_db import get_resumable_run_by_output_dir from utils.models.model_config import load_model_defaults from utils.paths import resolve_dataset_path @@ -164,28 +152,6 @@ async def start_training( request.local_eval_datasets = _validate_local_dataset_paths( request.local_eval_datasets, "Local eval dataset" ) - resume_output_dir: Optional[str] = None - if request.resume_from_checkpoint: - try: - resume_output_dir = normalize_resume_output_dir( - request.resume_from_checkpoint - ) - except ValueError as e: - raise HTTPException(status_code = 400, detail = str(e)) - - resume_run = get_resumable_run_by_output_dir(resume_output_dir) - if not resume_run or not can_resume_run(resume_run): - raise HTTPException( - status_code = 400, - detail = "Resume checkpoint must belong to a stopped run with saved trainer state.", - ) - resume_checkpoint = get_resume_checkpoint_path(resume_output_dir) - if not resume_checkpoint: - raise HTTPException( - status_code = 400, - detail = "Resume checkpoint must include saved trainer state.", - ) - request.resume_from_checkpoint = resume_checkpoint # Convert request to kwargs for backend training_kwargs = { @@ -207,7 +173,6 @@ async def start_training( "custom_format_mapping": request.custom_format_mapping, "num_epochs": request.num_epochs, "learning_rate": request.learning_rate, - "embedding_learning_rate": request.embedding_learning_rate, "batch_size": request.batch_size, "gradient_accumulation_steps": request.gradient_accumulation_steps, "warmup_steps": request.warmup_steps, @@ -244,8 +209,6 @@ async def start_training( "wandb_project": request.wandb_project or "", "enable_tensorboard": request.enable_tensorboard, "tensorboard_dir": request.tensorboard_dir or "", - "output_dir": resume_output_dir, - "resume_from_checkpoint": request.resume_from_checkpoint, "trust_remote_code": request.trust_remote_code, "gpu_ids": request.gpu_ids, } @@ -474,9 +437,6 @@ async def get_training_status( "loss": getattr(progress, "loss", None), "learning_rate": getattr(progress, "learning_rate", None), } - output_dir = getattr(backend, "_output_dir", None) - if output_dir: - details["output_dir"] = output_dir # Build metric history for chart recovery after SSE reconnection metric_history = None diff --git a/studio/backend/routes/training_history.py b/studio/backend/routes/training_history.py index 6f34321959..597c4424c0 100644 --- a/studio/backend/routes/training_history.py +++ b/studio/backend/routes/training_history.py @@ -11,7 +11,6 @@ from loggers import get_logger from auth.authentication import get_current_subject -from core.training.resume import can_resume_run from models import ( TrainingRunDeleteResponse, TrainingRunDetailResponse, @@ -35,10 +34,7 @@ async def list_training_runs( """List training runs, newest first.""" result = list_runs(limit = limit, offset = offset) return TrainingRunListResponse( - runs = [ - TrainingRunSummary(**{**r, "can_resume": can_resume_run(r)}) - for r in result["runs"] - ], + runs = [TrainingRunSummary(**r) for r in result["runs"]], total = result["total"], ) @@ -62,12 +58,7 @@ async def get_training_run_detail( metrics_data = get_run_metrics(run_id) return TrainingRunDetailResponse( - run = TrainingRunSummary( - **{ - **{k: v for k, v in run.items() if k != "config_json"}, - "can_resume": can_resume_run(run), - } - ), + run = TrainingRunSummary(**{k: v for k, v in run.items() if k != "config_json"}), config = config, metrics = TrainingRunMetrics(**metrics_data), ) diff --git a/studio/backend/run.py b/studio/backend/run.py index 1dd1230a17..9675b9ea4c 100644 --- a/studio/backend/run.py +++ b/studio/backend/run.py @@ -159,27 +159,7 @@ def _find_free_port(host: str, start: int, max_attempts: int = 20) -> int: ) -from utils.paths.storage_roots import studio_root as _studio_root - -_PID_FILE = _studio_root() / "studio.pid" - -# Direct backend launches bypass the CLI's env re-export; do it here for -# real custom roots so unsloth-zoo's import-time LLAMA_CPP_DEFAULT_DIR -# picks up the custom build. Skip for legacy-default to avoid flipping -# default-mode installs into env-override. -try: - _LEGACY_STUDIO_ROOT = (Path.home() / ".unsloth" / "studio").resolve() -except (OSError, ValueError): - _LEGACY_STUDIO_ROOT = Path.home() / ".unsloth" / "studio" -try: - _STUDIO_ROOT_RESOLVED = _studio_root().resolve() -except (OSError, ValueError): - _STUDIO_ROOT_RESOLVED = _studio_root() -if _STUDIO_ROOT_RESOLVED != _LEGACY_STUDIO_ROOT: - if not os.environ.get("UNSLOTH_STUDIO_HOME"): - os.environ["UNSLOTH_STUDIO_HOME"] = str(_STUDIO_ROOT_RESOLVED) - if not os.environ.get("UNSLOTH_LLAMA_CPP_PATH"): - os.environ["UNSLOTH_LLAMA_CPP_PATH"] = str(_STUDIO_ROOT_RESOLVED / "llama.cpp") +_PID_FILE = Path.home() / ".unsloth" / "studio" / "studio.pid" def _write_pid_file(): @@ -264,11 +244,10 @@ def _graceful_shutdown(server = None): def run_server( - host: str = "127.0.0.1", + host: str = "0.0.0.0", port: int = 8888, frontend_path: Path = Path(__file__).resolve().parent.parent / "frontend" / "dist", silent: bool = False, - api_only: bool = False, llama_parallel_slots: int = 1, ): """ @@ -279,7 +258,6 @@ def run_server( port: Port to bind to (auto-increments if in use) frontend_path: Path to frontend build directory (optional) silent: Suppress startup messages - api_only: Run API server only, no frontend serving (for Tauri desktop app) llama_parallel_slots: Number of parallel slots for llama-server Note: @@ -297,10 +275,6 @@ def run_server( except Exception: pass - # Set env var BEFORE importing main so CORS middleware picks it up - if api_only: - os.environ["UNSLOTH_API_ONLY"] = "1" - import nest_asyncio nest_asyncio.apply() @@ -336,12 +310,8 @@ def run_server( print("=" * 50) print("") - # Output port for Tauri to parse when in api-only mode - if api_only: - print(f"TAURI_PORT={port}", flush = True) - - # Setup frontend if path provided (skip in api-only mode) - if frontend_path and not api_only: + # Setup frontend if path provided + if frontend_path: if setup_frontend(app, frontend_path): if not silent: print(f"[OK] Frontend loaded from {frontend_path}") @@ -412,11 +382,7 @@ def _trigger_shutdown(): pass parser = argparse.ArgumentParser(description = "Run Unsloth UI Backend server") - parser.add_argument( - "--host", - default = "127.0.0.1", - help = "Host to bind to (default: 127.0.0.1; use 0.0.0.0 for network/cloud access)", - ) + parser.add_argument("--host", default = "0.0.0.0", help = "Host to bind to") parser.add_argument("--port", type = int, default = 8888, help = "Port to bind to") parser.add_argument( "--frontend", @@ -425,17 +391,10 @@ def _trigger_shutdown(): help = "Path to frontend build", ) parser.add_argument("--silent", action = "store_true", help = "Suppress output") - parser.add_argument( - "--api-only", - action = "store_true", - help = "API server only, no frontend (for Tauri)", - ) args = parser.parse_args() - kwargs = dict( - host = args.host, port = args.port, silent = args.silent, api_only = args.api_only - ) + kwargs = dict(host = args.host, port = args.port, silent = args.silent) if args.frontend is not None: kwargs["frontend_path"] = Path(args.frontend) diff --git a/studio/backend/state/tool_policy.py b/studio/backend/state/tool_policy.py deleted file mode 100644 index 9343a39806..0000000000 --- a/studio/backend/state/tool_policy.py +++ /dev/null @@ -1,33 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. - -"""Process-level server-side tool policy. - -Set by `unsloth run` at startup; consulted by the inference route gates. - - None -> no CLI override (default). Per-request `enable_tools` is honored. - True -> CLI forced tools on for every request. - False -> CLI forced tools off for every request. -""" - -from typing import Optional - -_tool_policy: Optional[bool] = None - - -def get_tool_policy() -> Optional[bool]: - return _tool_policy - - -def set_tool_policy(value: Optional[bool]) -> None: - if value is not None and not isinstance(value, bool): - raise TypeError( - f"tool_policy must be Optional[bool], got {type(value).__name__}" - ) - global _tool_policy - _tool_policy = value - - -def reset_tool_policy() -> None: - global _tool_policy - _tool_policy = None diff --git a/studio/backend/storage/studio_db.py b/studio/backend/storage/studio_db.py index 29e787c196..89f75632ef 100644 --- a/studio/backend/storage/studio_db.py +++ b/studio/backend/storage/studio_db.py @@ -267,23 +267,10 @@ def list_runs(limit: int = 50, offset: int = 0) -> dict: total = conn.execute("SELECT COUNT(*) FROM training_runs").fetchone()[0] rows = conn.execute( """ - SELECT r.id, r.status, r.model_name, r.dataset_name, r.started_at, - r.ended_at, r.total_steps, r.final_step, r.final_loss, - r.output_dir, r.duration_seconds, r.error_message, - r.loss_sparkline, - CASE - WHEN r.status = 'stopped' - AND r.output_dir IS NOT NULL - AND EXISTS ( - SELECT 1 - FROM training_runs newer - WHERE newer.output_dir = r.output_dir - AND newer.status IN ('stopped', 'completed') - AND newer.started_at > r.started_at - ) - THEN 1 ELSE 0 - END AS resumed_later - FROM training_runs r + SELECT id, status, model_name, dataset_name, started_at, ended_at, + total_steps, final_step, final_loss, output_dir, + duration_seconds, error_message, loss_sparkline + FROM training_runs ORDER BY started_at DESC LIMIT ? OFFSET ? """, @@ -310,26 +297,7 @@ def list_runs(limit: int = 50, offset: int = 0) -> dict: def get_run(id: str) -> Optional[dict]: conn = get_connection() try: - row = conn.execute( - """ - SELECT r.*, - CASE - WHEN r.status = 'stopped' - AND r.output_dir IS NOT NULL - AND EXISTS ( - SELECT 1 - FROM training_runs newer - WHERE newer.output_dir = r.output_dir - AND newer.status IN ('stopped', 'completed') - AND newer.started_at > r.started_at - ) - THEN 1 ELSE 0 - END AS resumed_later - FROM training_runs r - WHERE r.id = ? - """, - (id,), - ).fetchone() + row = conn.execute("SELECT * FROM training_runs WHERE id = ?", (id,)).fetchone() if row is None: return None run = dict(row) @@ -345,45 +313,6 @@ def get_run(id: str) -> Optional[dict]: conn.close() -def get_resumable_run_by_output_dir(output_dir: str) -> Optional[dict]: - conn = get_connection() - try: - row = conn.execute( - """ - SELECT r.*, - 0 AS resumed_later - FROM training_runs r - WHERE r.output_dir = ? - AND r.status = 'stopped' - AND NOT EXISTS ( - SELECT 1 - FROM training_runs newer - WHERE newer.output_dir = r.output_dir - AND newer.status IN ('stopped', 'completed') - AND newer.started_at > r.started_at - ) - ORDER BY r.started_at DESC - LIMIT 1 - """, - (output_dir,), - ).fetchone() - if row is None: - return None - run = dict(row) - sparkline = run.get("loss_sparkline") - if sparkline: - try: - run["loss_sparkline"] = json.loads(sparkline) - except (json.JSONDecodeError, TypeError): - logger.debug( - "Failed to parse loss_sparkline for output_dir %s", output_dir - ) - run["loss_sparkline"] = None - return run - finally: - conn.close() - - def get_run_metrics(id: str) -> dict: """Return metric arrays for a run, using paired step arrays per metric.""" conn = get_connection() diff --git a/studio/backend/tests/test_data_recipe_github_progress.py b/studio/backend/tests/test_data_recipe_github_progress.py deleted file mode 100644 index 8e8c3995f4..0000000000 --- a/studio/backend/tests/test_data_recipe_github_progress.py +++ /dev/null @@ -1,91 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 - -from core.data_recipe.jobs.parse import apply_update, parse_log_message -from core.data_recipe.jobs.types import Job -from routes.data_recipe.validate import _GITHUB_VALIDATE_NOTE, validate -from models.data_recipe import RecipePayload - - -def test_github_page_log_updates_source_progress_without_cursor(): - job = Job(job_id = "job-1") - job.source_progress_estimated_total = 200 - - update = parse_log_message( - "[unslothai/unsloth] issues page 2 (+15) cursor=abc123 remaining=2960" - ) - - assert update is not None - apply_update(job, update) - - progress = job.source_progress - assert progress is not None - assert progress.source == "github" - assert progress.status == "fetching" - assert progress.repo == "unslothai/unsloth" - assert progress.resource == "issues" - assert progress.page == 2 - assert progress.page_items == 15 - assert progress.fetched_items == 15 - assert progress.estimated_total == 200 - assert progress.rate_remaining == 2960 - assert progress.message is not None - assert "cursor" not in progress.message - assert "abc123" not in progress.message - - -def test_github_rate_limit_log_updates_source_progress(): - job = Job(job_id = "job-1") - - update = parse_log_message("Rate limit hit. Sleeping 123s until reset.") - - assert update is not None - apply_update(job, update) - - progress = job.source_progress - assert progress is not None - assert progress.status == "rate_limited" - assert progress.retry_after_sec == 123 - assert "resume automatically" in (progress.message or "") - - -def test_github_real_sample_prs_and_trial_limit_are_parsed(): - job = Job(job_id = "job-1") - - for message in ( - "[unslothai/unsloth] PRs page 4 (+25) cursor=abc123 remaining=4983", - "Trial limit reached for PRs (100)", - ): - update = parse_log_message(message) - assert update is not None - apply_update(job, update) - - progress = job.source_progress - assert progress is not None - assert progress.repo == "unslothai/unsloth" - assert progress.resource == "pulls" - assert progress.page == 4 - assert progress.fetched_items == 25 - assert progress.rate_remaining == 4983 - assert progress.message == "GitHub pulls trial limit reached (100)." - - -def test_github_validate_skips_live_access_with_honest_note(): - response = validate( - RecipePayload( - recipe = { - "seed_config": { - "source": { - "seed_type": "github_repo", - "repos": ["unslothai/unsloth"], - "item_types": ["issues"], - "limit": 1, - } - }, - "columns": [{"column_type": "expression", "name": "x", "expr": "1"}], - } - ) - ) - - assert response.valid is True - assert response.raw_detail == _GITHUB_VALIDATE_NOTE diff --git a/studio/backend/tests/test_desktop_auth.py b/studio/backend/tests/test_desktop_auth.py deleted file mode 100644 index a5508c1c8b..0000000000 --- a/studio/backend/tests/test_desktop_auth.py +++ /dev/null @@ -1,598 +0,0 @@ -import importlib.util -import asyncio -import hashlib -import json -import os -import platform -import secrets -import sqlite3 -import subprocess -import sys -from pathlib import Path -from types import SimpleNamespace - -import jwt -import pytest -from fastapi import APIRouter, FastAPI -from fastapi.security import HTTPAuthorizationCredentials -from fastapi.testclient import TestClient - -from auth import storage - - -@pytest.fixture(autouse = True) -def isolated_auth_db(tmp_path, monkeypatch): - monkeypatch.setattr(storage, "DB_PATH", tmp_path / "auth.db") - monkeypatch.setattr(storage, "_BOOTSTRAP_PW_PATH", tmp_path / ".bootstrap_password") - monkeypatch.setattr(storage, "_bootstrap_password", None) - monkeypatch.setattr(storage, "_api_key_pbkdf2_salt_cache", None) - yield - - -def seed_user(*, must_change_password = False): - storage.create_initial_user( - username = storage.DEFAULT_ADMIN_USERNAME, - password = "human-password-123", - jwt_secret = secrets.token_urlsafe(64), - must_change_password = must_change_password, - ) - - -def auth_client(): - route_path = Path(__file__).resolve().parents[1] / "routes" / "auth.py" - spec = importlib.util.spec_from_file_location("_desktop_auth_route", route_path) - auth_route = importlib.util.module_from_spec(spec) - assert spec.loader is not None - spec.loader.exec_module(auth_route) - - app = FastAPI() - app.include_router(auth_route.router, prefix = "/api/auth") - return TestClient(app) - - -def data_recipe_jobs_module(): - route_path = ( - Path(__file__).resolve().parents[1] / "routes" / "data_recipe" / "jobs.py" - ) - spec = importlib.util.spec_from_file_location( - "_desktop_data_recipe_jobs", route_path - ) - jobs_route = importlib.util.module_from_spec(spec) - assert spec.loader is not None - spec.loader.exec_module(jobs_route) - return jobs_route - - -def local_recipe(): - return { - "model_providers": [{"name": "local", "is_local": True}], - "model_configs": [{"alias": "local-model", "provider": "local"}], - "columns": [{"column_type": "llm-text", "model_alias": "local-model"}], - } - - -def local_recipe_request(token): - return SimpleNamespace( - headers = {"authorization": f"Bearer {token}"}, - app = SimpleNamespace(state = SimpleNamespace(server_port = 8888)), - scope = {}, - base_url = "http://testserver/", - ) - - -@pytest.fixture -def loaded_local_model(monkeypatch): - inference_module = SimpleNamespace( - get_llama_cpp_backend = lambda: SimpleNamespace(is_loaded = True), - ) - monkeypatch.setitem(sys.modules, "routes.inference", inference_module) - - -def test_desktop_secret_round_trip_uses_real_admin_subject(): - seed_user() - raw = storage.create_desktop_secret() - - assert raw.startswith("desktop-") - assert storage.validate_desktop_secret(raw) == storage.DEFAULT_ADMIN_USERNAME - assert storage.validate_desktop_secret(raw + "x") is None - - -def test_create_desktop_secret_rotates_old_secret(): - seed_user() - old = storage.create_desktop_secret() - new = storage.create_desktop_secret() - - assert old != new - assert storage.validate_desktop_secret(old) is None - assert storage.validate_desktop_secret(new) == storage.DEFAULT_ADMIN_USERNAME - - -def test_clear_desktop_secret_invalidates_secret(): - seed_user() - raw = storage.create_desktop_secret() - - storage.clear_desktop_secret() - - assert storage.validate_desktop_secret(raw) is None - - -def test_ensure_default_admin_does_not_recreate_bootstrap_for_existing_admin(): - seed_user() - - created = storage.ensure_default_admin() - - assert created is False - assert not storage._BOOTSTRAP_PW_PATH.exists() - - -def test_ensure_default_admin_loads_existing_bootstrap_after_restart(monkeypatch): - created = storage.ensure_default_admin() - bootstrap_pw = storage._BOOTSTRAP_PW_PATH.read_text().strip() - - monkeypatch.setattr(storage, "_bootstrap_password", None) - created_again = storage.ensure_default_admin() - - assert created is True - assert storage._BOOTSTRAP_PW_PATH.exists() - assert created_again is False - assert storage.get_bootstrap_password() == bootstrap_pw - - -def test_ensure_default_admin_does_not_generate_for_empty_existing_bootstrap(): - seed_user() - storage._BOOTSTRAP_PW_PATH.write_text(" \n") - - created = storage.ensure_default_admin() - - assert created is False - assert storage._BOOTSTRAP_PW_PATH.read_text() == " \n" - assert storage.get_bootstrap_password() is None - - -def test_web_login_token_has_no_desktop_marker_and_keeps_password_gate(): - seed_user(must_change_password = True) - client = auth_client() - - response = client.post( - "/api/auth/login", - json = { - "username": storage.DEFAULT_ADMIN_USERNAME, - "password": "human-password-123", - }, - ) - - assert response.status_code == 200 - body = response.json() - assert body["must_change_password"] is True - payload = jwt.decode( - body["access_token"], - storage.get_jwt_secret(storage.DEFAULT_ADMIN_USERNAME), - algorithms = ["HS256"], - ) - assert payload["sub"] == storage.DEFAULT_ADMIN_USERNAME - assert "desktop" not in payload - - gated = client.post( - "/api/auth/api-keys", - headers = {"Authorization": f"Bearer {body['access_token']}"}, - json = {"name": "web"}, - ) - assert gated.status_code == 403 - - -def test_desktop_login_mints_admin_token_without_clearing_web_password_change(): - seed_user(must_change_password = True) - raw = storage.create_desktop_secret() - client = auth_client() - - response = client.post("/api/auth/desktop-login", json = {"secret": raw}) - - assert response.status_code == 200 - body = response.json() - assert body["access_token"] - assert body["refresh_token"] - assert body["token_type"] == "bearer" - assert body["must_change_password"] is False - assert storage.requires_password_change(storage.DEFAULT_ADMIN_USERNAME) is True - - payload = jwt.decode( - body["access_token"], - storage.get_jwt_secret(storage.DEFAULT_ADMIN_USERNAME), - algorithms = ["HS256"], - ) - assert payload["sub"] == storage.DEFAULT_ADMIN_USERNAME - assert payload["desktop"] is True - - -def test_desktop_refresh_preserves_desktop_marker(): - seed_user(must_change_password = True) - raw = storage.create_desktop_secret() - client = auth_client() - login_body = client.post("/api/auth/desktop-login", json = {"secret": raw}).json() - - response = client.post( - "/api/auth/refresh", - json = {"refresh_token": login_body["refresh_token"]}, - ) - - assert response.status_code == 200 - body = response.json() - assert body["must_change_password"] is False - payload = jwt.decode( - body["access_token"], - storage.get_jwt_secret(storage.DEFAULT_ADMIN_USERNAME), - algorithms = ["HS256"], - ) - assert payload["sub"] == storage.DEFAULT_ADMIN_USERNAME - assert payload["desktop"] is True - - -def test_desktop_session_uses_real_admin_identity_for_api_keys(): - seed_user(must_change_password = True) - raw = storage.create_desktop_secret() - client = auth_client() - token = client.post("/api/auth/desktop-login", json = {"secret": raw}).json()[ - "access_token" - ] - - response = client.post( - "/api/auth/api-keys", - headers = {"Authorization": f"Bearer {token}"}, - json = {"name": "desktop"}, - ) - - assert response.status_code == 200 - rows = storage.list_api_keys(storage.DEFAULT_ADMIN_USERNAME) - assert [row["name"] for row in rows] == ["desktop"] - - -def test_local_recipe_token_authenticates_as_admin_for_desktop_user(loaded_local_model): - # _inject_local_providers mints an internal sk-unsloth-* API key (not a - # forwarded JWT). The unified API-key path validates as the real admin - # user regardless of whether the incoming session was desktop or web. - from auth.authentication import create_access_token, get_current_subject - - seed_user(must_change_password = True) - jobs_route = data_recipe_jobs_module() - incoming_token = create_access_token( - subject = storage.DEFAULT_ADMIN_USERNAME, - desktop = True, - ) - recipe = local_recipe() - - jobs_route._inject_local_providers(recipe, local_recipe_request(incoming_token)) - - local_token = recipe["model_providers"][0]["api_key"] - assert local_token.startswith(storage.API_KEY_PREFIX) - credentials = HTTPAuthorizationCredentials( - scheme = "Bearer", - credentials = local_token, - ) - assert ( - asyncio.run(get_current_subject(credentials)) == storage.DEFAULT_ADMIN_USERNAME - ) - - -def test_local_recipe_token_authenticates_as_admin_for_web_user(loaded_local_model): - # Mirror of the desktop variant: API-key issuance is identical for web - # and desktop incoming tokens; auth via get_current_subject works the same. - from auth.authentication import create_access_token, get_current_subject - - seed_user(must_change_password = False) - jobs_route = data_recipe_jobs_module() - incoming_token = create_access_token(subject = storage.DEFAULT_ADMIN_USERNAME) - recipe = local_recipe() - - jobs_route._inject_local_providers(recipe, local_recipe_request(incoming_token)) - - local_token = recipe["model_providers"][0]["api_key"] - assert local_token.startswith(storage.API_KEY_PREFIX) - credentials = HTTPAuthorizationCredentials( - scheme = "Bearer", - credentials = local_token, - ) - assert ( - asyncio.run(get_current_subject(credentials)) == storage.DEFAULT_ADMIN_USERNAME - ) - - -def test_desktop_login_rejects_invalid_secret(): - seed_user(must_change_password = False) - client = auth_client() - - response = client.post( - "/api/auth/desktop-login", - json = {"secret": "desktop-invalid"}, - ) - - assert response.status_code == 401 - - -def test_write_desktop_secret_file_is_0600_on_unix(tmp_path): - from unsloth_cli.commands import studio as studio_cli - - path = tmp_path / ".desktop_secret" - if platform.system() != "Windows": - path.write_text("old-secret") - os.chmod(path, 0o644) - - studio_cli._write_auth_secret(path, "desktop-secret") - - assert path.read_text() == "desktop-secret" - if platform.system() != "Windows": - assert oct(path.stat().st_mode & 0o777) == "0o600" - - -def test_reset_password_removes_desktop_secret_files(tmp_path, monkeypatch): - from typer.testing import CliRunner - from unsloth_cli.commands import studio as studio_cli - - auth_dir = tmp_path / "auth" - auth_dir.mkdir() - (auth_dir / "auth.db").write_text("db") - (auth_dir / ".bootstrap_password").write_text("boot") - (auth_dir / ".desktop_secret").write_text("new") - monkeypatch.setattr(studio_cli, "STUDIO_HOME", tmp_path) - - result = CliRunner().invoke(studio_cli.studio_app, ["reset-password"]) - - assert result.exit_code == 0 - assert not (auth_dir / "auth.db").exists() - assert not (auth_dir / ".bootstrap_password").exists() - assert not (auth_dir / ".desktop_secret").exists() - - -def test_reset_password_removes_desktop_secret_files_without_db(tmp_path, monkeypatch): - from typer.testing import CliRunner - from unsloth_cli.commands import studio as studio_cli - - auth_dir = tmp_path / "auth" - auth_dir.mkdir() - (auth_dir / ".desktop_secret").write_text("new") - monkeypatch.setattr(studio_cli, "STUDIO_HOME", tmp_path) - - result = CliRunner().invoke(studio_cli.studio_app, ["reset-password"]) - - assert result.exit_code == 0 - assert not (auth_dir / ".desktop_secret").exists() - - -def test_desktop_capabilities_json_reports_rollout_safe_flags(): - from typer.testing import CliRunner - import unsloth_cli.commands.studio as studio_cli - - result = CliRunner().invoke( - studio_cli.studio_app, - ["desktop-capabilities", "--json"], - ) - - assert result.exit_code == 0 - body = json.loads(result.output) - assert body["desktop_protocol_version"] == 1 - assert body["supports_provision_desktop_auth"] is True - assert body["supports_api_only"] is True - assert isinstance(body["version"], str) - - -def test_health_response_reports_desktop_capability_fields(monkeypatch): - router_stub = SimpleNamespace( - auth_router = APIRouter(), - data_recipe_router = APIRouter(), - datasets_router = APIRouter(), - export_router = APIRouter(), - inference_router = APIRouter(), - inference_studio_router = APIRouter(), - models_router = APIRouter(), - training_history_router = APIRouter(), - training_router = APIRouter(), - ) - monkeypatch.setitem(sys.modules, "routes", router_stub) - - import studio.backend.main as backend_main - - monkeypatch.setattr(backend_main._hw_module, "CHAT_ONLY", False) - - body = asyncio.run(backend_main.health_check()) - - assert body["desktop_protocol_version"] == 1 - assert body["supports_desktop_auth"] is True - - -def test_provision_desktop_auth_writes_secret_and_creates_db_without_backend_deps( - tmp_path, - monkeypatch, -): - auth_dir = tmp_path / "auth" - auth_dir.mkdir() - - code = """ -import builtins -import sys -from pathlib import Path -from typer.testing import CliRunner - -studio_home = Path(sys.argv[1]) -real_import = builtins.__import__ - -def guarded_import(name, *args, **kwargs): - blocked = ("auth", "fastapi", "structlog", "utils") - if name in blocked or name.startswith(("auth.", "utils.")): - raise ModuleNotFoundError(name) - return real_import(name, *args, **kwargs) - -builtins.__import__ = guarded_import -from unsloth_cli.commands import studio as studio_cli - -studio_cli.STUDIO_HOME = studio_home -result = CliRunner().invoke(studio_cli.studio_app, ["provision-desktop-auth"]) -if result.exit_code != 0: - print(result.output) - if result.exception is not None: - raise result.exception - raise SystemExit(result.exit_code) -""" - result = subprocess.run( - [sys.executable, "-c", code, str(tmp_path)], - cwd = Path(__file__).resolve().parents[3], - env = {**os.environ, "PYTHONPATH": "."}, - text = True, - capture_output = True, - ) - assert result.returncode == 0, result.stderr + result.stdout - secret = (auth_dir / ".desktop_secret").read_text() - assert secret.startswith("desktop-") - - conn = sqlite3.connect(auth_dir / "auth.db") - conn.row_factory = sqlite3.Row - try: - user = conn.execute( - """ - SELECT username, password_salt, password_hash, must_change_password - FROM auth_user - """ - ).fetchone() - app_secrets = { - row["key"]: row["value"] - for row in conn.execute("SELECT key, value FROM app_secrets") - } - refresh_columns = { - row["name"] for row in conn.execute("PRAGMA table_info(refresh_tokens)") - } - finally: - conn.close() - - bootstrap_password = (auth_dir / ".bootstrap_password").read_text().strip() - bootstrap_hash = hashlib.pbkdf2_hmac( - "sha256", - bootstrap_password.encode("utf-8"), - user["password_salt"].encode("utf-8"), - 100_000, - ).hex() - - assert bootstrap_password - assert user["username"] == "unsloth" - assert user["must_change_password"] == 1 - assert bootstrap_hash == user["password_hash"] - assert len(app_secrets["api_key_pbkdf2_salt"]) == 64 - assert len(app_secrets["desktop_secret_hash"]) == 64 - assert app_secrets["desktop_secret_created_at"] - assert "is_desktop" in refresh_columns - - monkeypatch.setattr(storage, "DB_PATH", auth_dir / "auth.db") - monkeypatch.setattr(storage, "_api_key_pbkdf2_salt_cache", None) - assert storage.validate_desktop_secret(secret) == storage.DEFAULT_ADMIN_USERNAME - assert storage.requires_password_change(storage.DEFAULT_ADMIN_USERNAME) is True - - -def test_provision_desktop_auth_keeps_existing_admin_password(tmp_path, monkeypatch): - from typer.testing import CliRunner - from unsloth_cli.commands import studio as studio_cli - - auth_dir = tmp_path / "auth" - auth_dir.mkdir() - monkeypatch.setattr(studio_cli, "STUDIO_HOME", tmp_path) - - conn = sqlite3.connect(auth_dir / "auth.db") - try: - conn.execute( - """ - CREATE TABLE auth_user ( - id INTEGER PRIMARY KEY, - username TEXT UNIQUE NOT NULL, - password_salt TEXT NOT NULL, - password_hash TEXT NOT NULL, - jwt_secret TEXT NOT NULL, - must_change_password INTEGER NOT NULL DEFAULT 0 - ) - """ - ) - conn.execute( - """ - INSERT INTO auth_user ( - username, password_salt, password_hash, jwt_secret, must_change_password - ) - VALUES (?, ?, ?, ?, ?) - """, - ("unsloth", "existing-salt", "existing-hash", "existing-jwt", 0), - ) - conn.commit() - finally: - conn.close() - - result = CliRunner().invoke(studio_cli.studio_app, ["provision-desktop-auth"]) - - assert result.exit_code == 0 - assert not (auth_dir / ".bootstrap_password").exists() - conn = sqlite3.connect(auth_dir / "auth.db") - conn.row_factory = sqlite3.Row - try: - user = conn.execute( - """ - SELECT password_salt, password_hash, jwt_secret, must_change_password - FROM auth_user WHERE username = ? - """, - ("unsloth",), - ).fetchone() - finally: - conn.close() - - assert dict(user) == { - "password_salt": "existing-salt", - "password_hash": "existing-hash", - "jwt_secret": "existing-jwt", - "must_change_password": 0, - } - - -def test_update_password_clears_desktop_secret(): - seed_user() - raw = storage.create_desktop_secret() - assert storage.validate_desktop_secret(raw) == storage.DEFAULT_ADMIN_USERNAME - - changed = storage.update_password( - storage.DEFAULT_ADMIN_USERNAME, "new-admin-password" - ) - assert changed is True - assert storage.validate_desktop_secret(raw) is None - - -def test_update_password_on_unknown_user_leaves_desktop_secret_intact(): - seed_user() - raw = storage.create_desktop_secret() - - changed = storage.update_password("not-a-user", "irrelevant") - assert changed is False - assert storage.validate_desktop_secret(raw) == storage.DEFAULT_ADMIN_USERNAME - - -def test_desktop_auth_provision_has_bounded_timeout(): - rs_path = ( - Path(__file__).resolve().parents[3] - / "studio" - / "src-tauri" - / "src" - / "desktop_auth.rs" - ) - src = rs_path.read_text() - start = src.index("async fn provision_desktop_auth(") - depth = 0 - body_start = src.index("{", start) - body_end = None - for i in range(body_start, len(src)): - c = src[i] - if c == "{": - depth += 1 - elif c == "}": - depth -= 1 - if depth == 0: - body_end = i + 1 - break - assert body_end is not None - body = src[start:body_end] - assert "tokio::time::timeout" in body - import re - - m = re.search(r"Duration::from_secs\(\s*(\d+)\s*\)", body) - assert m is not None - seconds = int(m.group(1)) - assert 5 <= seconds <= 120 diff --git a/studio/backend/tests/test_gpu_selection.py b/studio/backend/tests/test_gpu_selection.py index a1fe5653ef..c6f26037af 100644 --- a/studio/backend/tests/test_gpu_selection.py +++ b/studio/backend/tests/test_gpu_selection.py @@ -746,15 +746,7 @@ def test_inference_route_rejects_gpu_ids_for_gguf(self): ): with self.assertRaises(HTTPException) as exc_info: asyncio.run( - inference_route.load_model( - request, - SimpleNamespace( - app = SimpleNamespace( - state = SimpleNamespace(llama_parallel_slots = 1), - ), - ), - current_subject = "test-user", - ) + inference_route.load_model(request, current_subject = "test-user") ) self.assertEqual(exc_info.exception.status_code, 400) @@ -894,15 +886,7 @@ def load_model(self, **kwargs): ): with self.assertRaises(HTTPException) as exc_info: asyncio.run( - inference_route.load_model( - request, - SimpleNamespace( - app = SimpleNamespace( - state = SimpleNamespace(llama_parallel_slots = 1), - ), - ), - current_subject = "test-user", - ) + inference_route.load_model(request, current_subject = "test-user") ) self.assertEqual(exc_info.exception.status_code, 400) @@ -958,15 +942,7 @@ def load_model(self, **kwargs): ): with self.assertRaises(HTTPException) as exc_info: asyncio.run( - inference_route.load_model( - request, - SimpleNamespace( - app = SimpleNamespace( - state = SimpleNamespace(llama_parallel_slots = 1), - ), - ), - current_subject = "test-user", - ) + inference_route.load_model(request, current_subject = "test-user") ) self.assertEqual(exc_info.exception.status_code, 400) @@ -1049,182 +1025,6 @@ def test_total_equals_min_gpu_vram_1(self): class TestPerGpuFitGuardAllCounts(unittest.TestCase): - def test_training_estimate_resolves_attention_without_raising(self): - with ( - patch("utils.hardware.hardware.get_device", return_value = DeviceType.CUDA), - patch( - "utils.hardware.hardware.estimate_fp16_model_size_bytes", - return_value = (8 * (1024**3), "config"), - ), - patch( - "utils.hardware.hardware._resolve_model_identifier_for_gpu_estimate", - return_value = "unsloth/test", - ), - patch( - "utils.hardware.hardware._load_config_for_gpu_estimate", - return_value = SimpleNamespace( - hidden_size = 4096, - num_hidden_layers = 32, - num_attention_heads = 32, - num_key_value_heads = 8, - intermediate_size = 14336, - vocab_size = 128256, - tie_word_embeddings = False, - ), - ), - patch( - "utils.hardware.hardware._determine_attention_impl_for_gpu_estimate", - return_value = "eager", - ), - patch("utils.hardware.hardware.get_visible_gpu_count", return_value = 1), - ): - _, metadata = estimate_required_model_memory_gb( - "unsloth/test", - training_type = "LoRA/QLoRA", - load_in_4bit = True, - ) - - self.assertEqual(metadata.get("estimation_mode"), "detailed") - self.assertEqual(metadata.get("attention_implementation"), "eager") - - def test_training_estimate_falls_back_when_attention_resolution_fails(self): - with ( - patch("utils.hardware.hardware.get_device", return_value = DeviceType.CUDA), - patch( - "utils.hardware.hardware.estimate_fp16_model_size_bytes", - return_value = (8 * (1024**3), "config"), - ), - patch( - "utils.hardware.hardware._resolve_model_identifier_for_gpu_estimate", - return_value = "unsloth/test", - ), - patch( - "utils.hardware.hardware._load_config_for_gpu_estimate", - return_value = SimpleNamespace( - hidden_size = 4096, - num_hidden_layers = 32, - num_attention_heads = 32, - num_key_value_heads = 8, - intermediate_size = 14336, - vocab_size = 128256, - tie_word_embeddings = False, - ), - ), - patch( - "utils.hardware.hardware._determine_attention_impl_for_gpu_estimate", - side_effect = RuntimeError("attention unavailable"), - ), - patch("utils.hardware.hardware.get_visible_gpu_count", return_value = 1), - ): - _, metadata = estimate_required_model_memory_gb( - "unsloth/test", - training_type = "LoRA/QLoRA", - load_in_4bit = True, - ) - - self.assertEqual(metadata.get("estimation_mode"), "detailed") - self.assertEqual( - metadata.get("attention_implementation"), - "eager", - ) - - def test_attention_resolver_does_not_mutate_loaded_config(self): - from utils.hardware import hardware as hardware_module - - config = SimpleNamespace( - hidden_size = 1024, - num_hidden_layers = 2, - num_attention_heads = 8, - num_key_value_heads = 8, - intermediate_size = 2048, - vocab_size = 1024, - tie_word_embeddings = True, - ) - - def _stub_resolver(model_class, cfg): - cfg._attn_implementation = "eager" - return "eager" - - with patch( - "unsloth.models._utils.resolve_attention_implementation", - side_effect = _stub_resolver, - ): - hardware_module._determine_attention_impl_for_gpu_estimate(config) - - self.assertFalse(hasattr(config, "_attn_implementation")) - - def test_attention_resolver_handles_missing_model_mapping(self): - from utils.hardware import hardware as hardware_module - - config = SimpleNamespace( - hidden_size = 1024, - num_hidden_layers = 2, - num_attention_heads = 8, - num_key_value_heads = 8, - intermediate_size = 2048, - vocab_size = 1024, - tie_word_embeddings = True, - ) - captured = {} - - def _stub_resolver(model_class, cfg): - captured["model_class"] = model_class - return "eager" - - from transformers import AutoModel, AutoModelForCausalLM - - with ( - patch.object(AutoModelForCausalLM, "_model_mapping", new = None), - patch.object(AutoModel, "_model_mapping", new = None), - patch( - "unsloth.models._utils.resolve_attention_implementation", - side_effect = _stub_resolver, - ), - ): - result = hardware_module._determine_attention_impl_for_gpu_estimate(config) - - self.assertEqual(result, "eager") - self.assertIsNone(captured["model_class"]) - - def test_attention_resolver_does_not_mutate_nested_text_config(self): - from utils.hardware import hardware as hardware_module - - text_config = SimpleNamespace( - hidden_size = 1024, - num_hidden_layers = 2, - num_attention_heads = 8, - num_key_value_heads = 8, - intermediate_size = 2048, - vocab_size = 1024, - tie_word_embeddings = True, - ) - config = SimpleNamespace( - hidden_size = 1024, - num_hidden_layers = 2, - num_attention_heads = 8, - num_key_value_heads = 8, - intermediate_size = 2048, - vocab_size = 1024, - tie_word_embeddings = True, - text_config = text_config, - ) - - def _stub_resolver(model_class, cfg): - cfg._attn_implementation = "eager" - inner = getattr(cfg, "text_config", None) - if inner is not None: - inner._attn_implementation = "eager" - return "eager" - - with patch( - "unsloth.models._utils.resolve_attention_implementation", - side_effect = _stub_resolver, - ): - hardware_module._determine_attention_impl_for_gpu_estimate(config) - - self.assertFalse(hasattr(config, "_attn_implementation")) - self.assertFalse(hasattr(text_config, "_attn_implementation")) - def test_min_per_gpu_generated_for_all_visible_counts(self): with ( patch("utils.hardware.hardware.get_device", return_value = DeviceType.CUDA), @@ -1301,123 +1101,3 @@ def test_prepare_gpu_selection_rejects_explicit_ids_on_xpu(self): with patch("utils.hardware.hardware.get_device", return_value = DeviceType.XPU): with self.assertRaisesRegex(ValueError, "only supported on CUDA"): prepare_gpu_selection([0], model_name = "unsloth/test") - - -class TestEstimateFp16ModelSizeBytesPrefersLocalWeights(unittest.TestCase): - def _run( - self, - model_path, - *, - config_bytes, - local_bytes, - safetensors_params = None, - config = object(), - ): - from utils.hardware import hardware as hardware_module - - with ( - patch.object( - hardware_module, - "_resolve_model_identifier_for_gpu_estimate", - return_value = model_path, - ), - patch.object( - hardware_module, - "_get_hf_safetensors_total_params", - return_value = safetensors_params, - ), - patch.object( - hardware_module, - "_load_config_for_gpu_estimate", - return_value = config, - ), - patch.object( - hardware_module, - "_estimate_fp16_model_size_bytes_from_config", - return_value = config_bytes, - ), - patch.object( - hardware_module, - "_get_local_weight_size_bytes", - return_value = local_bytes, - ), - ): - return hardware_module.estimate_fp16_model_size_bytes(model_path) - - def test_local_weight_bytes_preferred_when_larger_than_config(self): - bytes_, src = self._run( - "/local/vlm", - config_bytes = 2 * (1 << 30), - local_bytes = 20 * (1 << 30), - ) - self.assertEqual(bytes_, 20 * (1 << 30)) - self.assertEqual(src, "weight_bytes") - - def test_config_bytes_preferred_when_larger_than_local(self): - bytes_, src = self._run( - "/local/text-only", - config_bytes = 20 * (1 << 30), - local_bytes = 2 * (1 << 30), - ) - self.assertEqual(bytes_, 20 * (1 << 30)) - self.assertEqual(src, "config") - - def test_config_bytes_returned_when_no_local_weights(self): - bytes_, src = self._run( - "/local/no-weights", - config_bytes = 5 * (1 << 30), - local_bytes = None, - ) - self.assertEqual(bytes_, 5 * (1 << 30)) - self.assertEqual(src, "config") - - def test_local_bytes_returned_when_config_resolution_fails(self): - bytes_, src = self._run( - "/local/no-config", - config_bytes = None, - local_bytes = 7 * (1 << 30), - config = None, - ) - self.assertEqual(bytes_, 7 * (1 << 30)) - self.assertEqual(src, "weight_bytes") - - def test_equal_local_and_config_keeps_config_label(self): - # why: tie-breaker is "local must be strictly larger" so an exact - # match keeps the config-derived path. - same = 8 * (1 << 30) - bytes_, src = self._run( - "/local/equal", - config_bytes = same, - local_bytes = same, - ) - self.assertEqual(bytes_, same) - self.assertEqual(src, "config") - - def test_remote_safetensors_path_unaffected_by_local_weights(self): - from utils.hardware import hardware as hardware_module - - with ( - patch.object( - hardware_module, - "_resolve_model_identifier_for_gpu_estimate", - return_value = "owner/repo", - ), - patch.object( - hardware_module, - "_get_hf_safetensors_total_params", - return_value = 1_000_000_000, - ), - patch.object( - hardware_module, - "_load_config_for_gpu_estimate", - ) as mock_load, - patch.object( - hardware_module, - "_get_local_weight_size_bytes", - ) as mock_local, - ): - bytes_, src = hardware_module.estimate_fp16_model_size_bytes("owner/repo") - self.assertEqual(bytes_, 2 * 1_000_000_000) - self.assertEqual(src, "safetensors") - mock_load.assert_not_called() - mock_local.assert_not_called() diff --git a/studio/backend/tests/test_host_defaults.py b/studio/backend/tests/test_host_defaults.py deleted file mode 100644 index 8b81474e92..0000000000 --- a/studio/backend/tests/test_host_defaults.py +++ /dev/null @@ -1,98 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 - -"""Tests that Unsloth Studio defaults to 127.0.0.1 (loopback) not 0.0.0.0. - -Uses AST parsing to inspect source-level defaults without requiring the -full studio venv (run.py has heavy dependencies like structlog/uvicorn). -""" - -import ast -from pathlib import Path - -_RUN_PY = Path(__file__).resolve().parent.parent / "run.py" - - -def _parse_function_param_defaults(source: str, func_name: str) -> dict: - """Return {param_name: default_value} for a named function in *source*. - - Only handles ast.Constant defaults (strings, ints, bools). - """ - tree = ast.parse(source) - for node in ast.walk(tree): - if ( - isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) - and node.name == func_name - ): - result = {} - all_args = node.args.args - defaults = node.args.defaults - # Defaults are right-aligned against the args list - offset = len(all_args) - len(defaults) - for i, default in enumerate(defaults): - arg_name = all_args[offset + i].arg - if isinstance(default, ast.Constant): - result[arg_name] = default.value - return result - return {} - - -def _parse_argparse_add_argument_default(source: str, option_name: str): - """Return the 'default' kwarg value for add_argument(option_name, ...) in *source*. - - Walks the entire module so the call can live in __main__ or in a helper - function — only handles ast.Constant defaults. - """ - tree = ast.parse(source) - for node in ast.walk(tree): - if not isinstance(node, ast.Call): - continue - func = node.func - if not (isinstance(func, ast.Attribute) and func.attr == "add_argument"): - continue - if not node.args: - continue - first_arg = node.args[0] - if not (isinstance(first_arg, ast.Constant) and first_arg.value == option_name): - continue - for kw in node.keywords: - if kw.arg == "default" and isinstance(kw.value, ast.Constant): - return kw.value.value - return None - - -def test_run_server_default_host_is_loopback(): - """run_server() parameter default for 'host' must be 127.0.0.1, not 0.0.0.0. - - Binding to 0.0.0.0 by default exposes the service on all network - interfaces, contradicting the documented "privacy first / 100% local" - guarantee. Loopback (127.0.0.1) is the least-permissive default; - users who need network access can pass -H 0.0.0.0 explicitly. - """ - source = _RUN_PY.read_text() - defaults = _parse_function_param_defaults(source, "run_server") - assert ( - "host" in defaults - ), "run_server() must have a 'host' parameter with a default" - host_default = defaults["host"] - assert host_default == "127.0.0.1", ( - f"run_server() host default must be '127.0.0.1' (loopback) " - f"but got '{host_default}'. Binding to '{host_default}' by default " - f"exposes the service beyond localhost." - ) - - -def test_argparse_default_host_is_loopback(): - """argparse --host add_argument default must be 127.0.0.1. - - When run.py is invoked directly (python run.py), the argparse default - should match the function default so direct execution is equally safe. - """ - source = _RUN_PY.read_text() - host_default = _parse_argparse_add_argument_default(source, "--host") - assert ( - host_default is not None - ), "Could not find add_argument('--host', ...) in run.py" - assert ( - host_default == "127.0.0.1" - ), f"run.py argparse --host default must be '127.0.0.1', got '{host_default}'" diff --git a/studio/backend/tests/test_kv_cache_estimation.py b/studio/backend/tests/test_kv_cache_estimation.py index 29d87804ff..2640ded90d 100644 --- a/studio/backend/tests/test_kv_cache_estimation.py +++ b/studio/backend/tests/test_kv_cache_estimation.py @@ -12,7 +12,6 @@ """ import io -import json import struct import sys import types as _types @@ -38,43 +37,35 @@ _structlog_stub = _types.ModuleType("structlog") sys.modules.setdefault("structlog", _structlog_stub) -# httpx -- only stub when the real library isn't installed. Stubbing -# unconditionally would shadow ``HTTPError`` / ``Response`` etc. that -# ``huggingface_hub.errors`` imports at module load time, which causes -# the transformers introspection tier to silently return None inside -# the test process. -try: - import httpx as _httpx_real # noqa: F401 -except ImportError: - _httpx_stub = _types.ModuleType("httpx") - for _exc_name in ( - "ConnectError", - "TimeoutException", - "ReadTimeout", - "ReadError", - "RemoteProtocolError", - "CloseError", - "HTTPError", - "RequestError", - ): - setattr(_httpx_stub, _exc_name, type(_exc_name, (Exception,), {})) - - class _FakeTimeout: - def __init__(self, *a, **kw): - pass - - _httpx_stub.Timeout = _FakeTimeout - _httpx_stub.Response = type("Response", (), {}) - _httpx_stub.Client = type( - "Client", - (), - { - "__init__": lambda self, **kw: None, - "__enter__": lambda self: self, - "__exit__": lambda self, *a: None, - }, - ) - sys.modules["httpx"] = _httpx_stub +# httpx +_httpx_stub = _types.ModuleType("httpx") +for _exc_name in ( + "ConnectError", + "TimeoutException", + "ReadTimeout", + "ReadError", + "RemoteProtocolError", + "CloseError", +): + setattr(_httpx_stub, _exc_name, type(_exc_name, (Exception,), {})) + + +class _FakeTimeout: + def __init__(self, *a, **kw): + pass + + +_httpx_stub.Timeout = _FakeTimeout +_httpx_stub.Client = type( + "Client", + (), + { + "__init__": lambda self, **kw: None, + "__enter__": lambda self: self, + "__exit__": lambda self, *a: None, + }, +) +sys.modules.setdefault("httpx", _httpx_stub) from core.inference.llama_cpp import LlamaCppBackend @@ -86,7 +77,8 @@ def __init__(self, *a, **kw): def _make_gguf_bytes(arch: str, kv_pairs: dict) -> bytes: """Build a minimal GGUF v3 binary blob with the given KV metadata. - Supports the scalar and simple array metadata used by the parser. + Only supports UINT32 (type 4), UINT64 (type 10), and STRING (type 8) + values, which is all the metadata parser reads. """ buf = io.BytesIO() # Header: magic, version, tensor_count, kv_count @@ -104,17 +96,6 @@ def _make_gguf_bytes(arch: str, kv_pairs: dict) -> bytes: val_bytes = val.encode("utf-8") buf.write(struct.pack(" bytes: return buf.getvalue() -def _backend_from_gguf( - arch: str, fields: dict, general: dict | None = None -) -> LlamaCppBackend: - """Create a LlamaCppBackend with parsed GGUF metadata from given fields. - - `general` lets a test inject extra `general.*` metadata (used to - verify the dynamic SWA resolver picks up source-repo hints from - GGUFs that ship them). - """ +def _backend_from_gguf(arch: str, fields: dict) -> LlamaCppBackend: + """Create a LlamaCppBackend with parsed GGUF metadata from given fields.""" kv = {"general.architecture": arch} - for k, v in (general or {}).items(): - kv[k] = v for k, v in fields.items(): kv[f"{arch}.{k}"] = v import tempfile, os @@ -161,7 +133,7 @@ def _backend_from_gguf( class TestGGUFParserNewFields: - """Verify that architecture-aware fields are correctly parsed.""" + """Verify that the 8 new architecture-aware fields are correctly parsed.""" @pytest.mark.parametrize( "field,gguf_key,value", @@ -186,189 +158,15 @@ def test_missing_fields_are_none(self): "_kv_key_length", "_kv_value_length", "_sliding_window", - "_sliding_window_pattern", "_full_attention_interval", "_kv_lora_rank", "_key_length_mla", - "_kv_key_length_swa", - "_kv_value_length_swa", "_ssm_inner_size", "_ssm_state_size", ]: assert getattr(b, attr) is None - def test_array_fields_parsed(self): - b = _backend_from_gguf( - "gemma4", - { - "block_count": 6, - "attention.head_count_kv": [8, 8, 8, 8, 8, 2], - "attention.sliding_window_pattern": [ - True, - True, - True, - True, - True, - False, - ], - }, - ) - # Per-layer KV head count is preserved exactly... - assert b._n_kv_heads_by_layer == [8, 8, 8, 8, 8, 2] - # ...and mirrored into the scalar field as a conservative max so - # non-SWA estimator paths and any caller using - # `n_kv = self._n_kv_heads or ...` get a safe upper bound. - assert b._n_kv_heads == 8 - assert b._sliding_window_pattern == [True, True, True, True, True, False] - - -class TestArchSwaPatternDefaults: - """Bootstrap arch table fires when GGUF reports `sliding_window` but - no per-layer pattern (true for every Gemma 2/3/3n/gpt-oss GGUF today).""" - - @pytest.mark.parametrize( - "arch,n_layers,expected_period", - [ - ("gemma2", 26, 2), - ("gemma3", 18, 6), - ("gemma3n", 35, 5), - ("gpt_oss", 24, 2), - ("cohere2", 32, 4), - ], - ) - def test_arch_default_pattern_applied(self, arch, n_layers, expected_period): - b = _backend_from_gguf( - arch, - { - "block_count": n_layers, - "attention.head_count": 4, - "attention.head_count_kv": 1, - "attention.key_length": 256, - "attention.value_length": 256, - "attention.sliding_window": 512, - }, - ) - expected_pattern = [(i + 1) % expected_period != 0 for i in range(n_layers)] - assert ( - b._sliding_window_pattern == expected_pattern - ), f"{arch} should expand to period={expected_period}" - - def test_unknown_arch_no_default(self): - b = _backend_from_gguf( - "totallymadeupv7", - { - "block_count": 24, - "attention.head_count": 4, - "attention.head_count_kv": 1, - "attention.key_length": 128, - "attention.value_length": 128, - "attention.sliding_window": 1024, - }, - ) - assert b._sliding_window_pattern is None - - def test_explicit_pattern_overrides_arch_default(self): - # Period=6 is the gemma3 default; the explicit array must win. - b = _backend_from_gguf( - "gemma3", - { - "block_count": 6, - "attention.head_count": 4, - "attention.head_count_kv": 1, - "attention.key_length": 256, - "attention.value_length": 256, - "attention.sliding_window": 512, - "attention.sliding_window_pattern": [ - True, - False, - True, - False, - True, - False, - ], - }, - ) - assert b._sliding_window_pattern == [True, False, True, False, True, False] - - def test_no_sliding_window_no_pattern(self): - b = _backend_from_gguf( - "gemma3", - { - "block_count": 18, - "attention.head_count": 4, - "attention.head_count_kv": 1, - "attention.key_length": 256, - "attention.value_length": 256, - # no sliding_window key - }, - ) - assert b._sliding_window_pattern is None - - @pytest.mark.parametrize( - "arch", ["llama", "qwen2", "qwen3", "mistral", "mistral3", "glm4", "llama4"] - ) - def test_non_swa_arch_uses_full_attention_path(self, arch): - # Pure-GQA arches: GGUF has no sliding_window, no synthetic - # pattern, estimator hits Path 4. - b = _backend_from_gguf( - arch, - { - "block_count": 32, - "attention.head_count": 32, - "attention.head_count_kv": 8, - "attention.key_length": 128, - "attention.value_length": 128, - "embedding_length": 4096, - }, - ) - assert b._sliding_window_pattern is None - assert b._sliding_window is None - kv = b._estimate_kv_cache_bytes(8192, "f16") - gqa_expected = 32 * 8192 * 8 * (128 + 128) * 2 - assert kv == gqa_expected - - def test_arch_default_reduces_kv_estimate_vs_legacy(self): - common = { - "block_count": 62, - "attention.head_count": 32, - "attention.head_count_kv": 16, - "attention.key_length": 128, - "attention.value_length": 128, - "attention.sliding_window": 1024, - "embedding_length": 5376, - } - with_default = _backend_from_gguf("gemma3", common) - # Arch not in the table -> legacy 1/4 path. - without_default = _backend_from_gguf("totallymadeupv7", common) - - kv_default = with_default._estimate_kv_cache_bytes(131072, "f16") - kv_legacy = without_default._estimate_kv_cache_bytes(131072, "f16") - assert kv_default > 0 - assert kv_legacy > 0 - assert kv_default < kv_legacy, ( - f"arch fallback should under-shoot legacy estimate: " - f"{kv_default} >= {kv_legacy}" - ) - - def test_scalar_sliding_window_pattern_expanded(self): - block_count = 8 - b = _backend_from_gguf( - "gemma3", - { - "attention.sliding_window_pattern": 4, - "block_count": block_count, - "attention.head_count_kv": 4, - "attention.key_length": 256, - "attention.value_length": 256, - "attention.sliding_window": 1024, - }, - ) - expected = [(i + 1) % 4 != 0 for i in range(block_count)] - assert isinstance(b._sliding_window_pattern, list) - assert b._sliding_window_pattern == expected - assert b._estimate_kv_cache_bytes(4096, "f16") > 0 - - def test_all_fields_parsed_together(self): + def test_all_13_fields_parsed_together(self): fields = { "context_length": 131072, "block_count": 62, @@ -378,12 +176,9 @@ def test_all_fields_parsed_together(self): "attention.key_length": 128, "attention.value_length": 128, "attention.sliding_window": 1024, - "attention.sliding_window_pattern": [True, False], "full_attention_interval": 6, "attention.kv_lora_rank": 512, "attention.key_length_mla": 256, - "attention.key_length_swa": 64, - "attention.value_length_swa": 64, "ssm.inner_size": 4096, "ssm.state_size": 128, } @@ -396,294 +191,13 @@ def test_all_fields_parsed_together(self): assert b._kv_key_length == 128 assert b._kv_value_length == 128 assert b._sliding_window == 1024 - assert b._sliding_window_pattern == [True, False] assert b._full_attention_interval == 6 assert b._kv_lora_rank == 512 assert b._key_length_mla == 256 - assert b._kv_key_length_swa == 64 - assert b._kv_value_length_swa == 64 assert b._ssm_inner_size == 4096 assert b._ssm_state_size == 128 -_SWA_FIELDS = { - "block_count": 12, - "attention.head_count": 4, - "attention.head_count_kv": 1, - "attention.key_length": 256, - "attention.value_length": 256, - "attention.sliding_window": 512, -} - - -class TestDynamicSwaResolver: - """4-tier resolver: GGUF metadata, on-disk cache, bootstrap, HF fetch.""" - - def _isolate_cache(self, monkeypatch, tmp_path): - from core.inference import llama_cpp as lc - - monkeypatch.setenv("UNSLOTH_STUDIO_HOME", str(tmp_path)) - monkeypatch.setattr(lc, "_SWA_CACHE", None) - return tmp_path - - def test_period_from_layer_types_finds_smallest_period(self): - from core.inference.llama_cpp import _period_from_layer_types - - # gemma3 (1 global per 6), gpt-oss (alternating), gemma3n (1 per 5). - assert ( - _period_from_layer_types( - (["sliding_attention"] * 5 + ["full_attention"]) * 4 - ) - == 6 - ) - assert ( - _period_from_layer_types(["sliding_attention", "full_attention"] * 12) == 2 - ) - assert ( - _period_from_layer_types( - (["sliding_attention"] * 4 + ["full_attention"]) * 7 - ) - == 5 - ) - - def test_period_from_layer_types_returns_none_for_aperiodic(self): - from core.inference.llama_cpp import _period_from_layer_types - - lt = [ - "sliding_attention", - "full_attention", - "sliding_attention", - "sliding_attention", - "full_attention", - "sliding_attention", - "sliding_attention", - "sliding_attention", - ] - assert _period_from_layer_types(lt) is None - - def test_hf_repo_from_url(self): - from core.inference.llama_cpp import _hf_repo_from_url - - assert ( - _hf_repo_from_url("https://huggingface.co/google/gemma-3-1b-it") - == "google/gemma-3-1b-it" - ) - assert ( - _hf_repo_from_url( - "https://huggingface.co/google/gemma-3-1b-it/blob/main/config.json" - ) - == "google/gemma-3-1b-it" - ) - for bad in [ - "https://huggingface.co/google", - "https://example.com/foo/bar", - None, - "", - ]: - assert _hf_repo_from_url(bad) is None - - def test_bootstrap_tier_used_when_no_cache(self, monkeypatch, tmp_path): - self._isolate_cache(monkeypatch, tmp_path) - from core.inference import llama_cpp as lc - - def boom(*a, **kw): - raise AssertionError("HF fetch must not run when bootstrap covers the arch") - - monkeypatch.setattr(lc, "_fetch_swa_entry_from_hf", boom) - b = _backend_from_gguf("gemma3", dict(_SWA_FIELDS, block_count = 18)) - assert b._sliding_window_pattern == [(i + 1) % 6 != 0 for i in range(18)] - - def test_disk_cache_takes_precedence_over_bootstrap(self, monkeypatch, tmp_path): - self._isolate_cache(monkeypatch, tmp_path) - # Override bootstrap=6 with a cached period=3. - with open(tmp_path / "swa_cache.json", "w") as f: - json.dump({"gemma3": 3}, f) - b = _backend_from_gguf("gemma3", dict(_SWA_FIELDS, block_count = 18)) - assert b._sliding_window_pattern == [(i + 1) % 3 != 0 for i in range(18)] - - def test_disk_cache_supports_array_entries(self, monkeypatch, tmp_path): - # Aperiodic mask gets tiled across n_layers. - self._isolate_cache(monkeypatch, tmp_path) - mask = [True, False, True, True, False, True, False, False] - with open(tmp_path / "swa_cache.json", "w") as f: - json.dump({"customarch": mask}, f) - b = _backend_from_gguf("customarch", dict(_SWA_FIELDS, block_count = 16)) - assert b._sliding_window_pattern == [bool(mask[i % 8]) for i in range(16)] - - def test_hf_fetch_populates_cache(self, monkeypatch, tmp_path): - self._isolate_cache(monkeypatch, tmp_path) - from core.inference import llama_cpp as lc - - calls = [] - - def fake_fetch(repo_id): - calls.append(repo_id) - return 4 if repo_id == "vendor/newmodel-1b-instruct" else None - - monkeypatch.setattr(lc, "_fetch_swa_entry_from_hf", fake_fetch) - b = _backend_from_gguf( - "newmodel", - _SWA_FIELDS, - general = { - "general.source.huggingface.repository": "vendor/newmodel-1b-instruct" - }, - ) - assert b._sliding_window_pattern == [(i + 1) % 4 != 0 for i in range(12)] - assert calls == ["vendor/newmodel-1b-instruct"] - with open(tmp_path / "swa_cache.json") as f: - assert json.load(f) == {"newmodel": 4} - - def test_hf_fetch_falls_back_to_other_candidates(self, monkeypatch, tmp_path): - self._isolate_cache(monkeypatch, tmp_path) - from core.inference import llama_cpp as lc - - monkeypatch.setattr( - lc, - "_fetch_swa_entry_from_hf", - lambda r: 6 if r == "vendor/newmodel-base" else None, - ) - b = _backend_from_gguf( - "newmodel", - _SWA_FIELDS, - general = { - "general.base_model.0.repo_url": "https://huggingface.co/vendor/newmodel-base" - }, - ) - assert b._sliding_window_pattern == [(i + 1) % 6 != 0 for i in range(12)] - - def test_offline_env_skips_network(self, monkeypatch, tmp_path): - self._isolate_cache(monkeypatch, tmp_path) - monkeypatch.setenv("UNSLOTH_STUDIO_OFFLINE", "1") - from core.inference import llama_cpp as lc - - def boom(*a, **kw): - raise AssertionError("HF fetch must not run when offline=1") - - monkeypatch.setattr(lc, "_fetch_swa_entry_from_hf", boom) - b = _backend_from_gguf( - "newmodel", - _SWA_FIELDS, - general = {"general.source.huggingface.repository": "vendor/newmodel"}, - ) - assert b._sliding_window_pattern is None - - def test_hf_fetch_failure_falls_through_silently(self, monkeypatch, tmp_path): - self._isolate_cache(monkeypatch, tmp_path) - from core.inference import llama_cpp as lc - - monkeypatch.setattr(lc, "_fetch_swa_entry_from_hf", lambda repo_id: None) - # Force the failure into the Tier 3 path; bypass Tier 2.5. - monkeypatch.setattr( - lc, "_resolve_swa_entry_from_transformers", lambda arch: None - ) - b = _backend_from_gguf( - "newmodel", - _SWA_FIELDS, - general = {"general.source.huggingface.repository": "vendor/does-not-exist"}, - ) - assert b._sliding_window_pattern is None - assert not (tmp_path / "swa_cache.json").exists() - - -class TestTransformersIntrospection: - """Tier 2.5: default-init the matching Config; on failure, parse via inspect.""" - - def _isolate_cache(self, monkeypatch, tmp_path): - from core.inference import llama_cpp as lc - - monkeypatch.setenv("UNSLOTH_STUDIO_HOME", str(tmp_path)) - monkeypatch.setattr(lc, "_SWA_CACHE", None) - return tmp_path - - def test_arch_aliases_normalises_hyphen_underscore(self): - from core.inference.llama_cpp import _arch_aliases - - aliases = _arch_aliases("falcon-h1") - assert aliases[0] == "falcon-h1" and "falcon_h1" in aliases - assert _arch_aliases("gemma3") == ("gemma3",) - assert _arch_aliases("") == () - - def test_resolves_real_transformers_arches(self): - from core.inference.llama_cpp import _resolve_swa_entry_from_transformers - - assert _resolve_swa_entry_from_transformers("gemma3") == 6 - assert _resolve_swa_entry_from_transformers("gemma2") == 2 - assert _resolve_swa_entry_from_transformers("cohere2") == 4 - - def test_falls_back_to_inspect_when_default_init_raises(self, monkeypatch): - from core.inference import llama_cpp as lc - - class _FakeBrokenConfig: - """Class with sliding_window_pattern: int = 7 in its docstring.""" - - def __init__(self, required_arg): - raise TypeError("requires an argument") - - class _FakeLazyMapping(dict): - def __getitem__(self, k): - return ( - _FakeBrokenConfig if k == "brokenarch" else super().__getitem__(k) - ) - - import sys, types as _types - - fake_auto = _types.ModuleType("transformers.models.auto.configuration_auto") - fake_auto.CONFIG_MAPPING_NAMES = {"brokenarch": "FakeBroken"} - fake_auto.CONFIG_MAPPING = _FakeLazyMapping({"brokenarch": "FakeBroken"}) - monkeypatch.setitem( - sys.modules, "transformers.models.auto.configuration_auto", fake_auto - ) - assert lc._resolve_swa_entry_from_transformers("brokenarch") == 7 - - def test_returns_none_when_transformers_unavailable(self, monkeypatch): - from core.inference import llama_cpp as lc - import sys - - orig_import = ( - __builtins__["__import__"] - if isinstance(__builtins__, dict) - else __builtins__.__import__ - ) - - def fake_import(name, *a, **kw): - if name.startswith("transformers"): - raise ImportError("transformers not installed") - return orig_import(name, *a, **kw) - - monkeypatch.setattr("builtins.__import__", fake_import) - for k in list(sys.modules): - if k.startswith("transformers"): - monkeypatch.delitem(sys.modules, k, raising = False) - assert lc._resolve_swa_entry_from_transformers("gemma3") is None - - def test_returns_none_for_arch_unknown_to_transformers(self): - from core.inference.llama_cpp import _resolve_swa_entry_from_transformers - - assert _resolve_swa_entry_from_transformers("totally-fake-arch-xyz") is None - - def test_full_resolver_uses_transformers_before_hf_fetch( - self, monkeypatch, tmp_path - ): - # With bootstrap empty, Tier 2.5 must answer before Tier 3 fires. - self._isolate_cache(monkeypatch, tmp_path) - from core.inference import llama_cpp as lc - - monkeypatch.setattr(lc, "_BOOTSTRAP_SWA_DEFAULTS", {}) - - def boom(repo_id): - raise AssertionError("Tier 3 must not run when Tier 2.5 has the answer") - - monkeypatch.setattr(lc, "_fetch_swa_entry_from_hf", boom) - b = _backend_from_gguf( - "gemma3", - dict(_SWA_FIELDS, block_count = 18), - general = {"general.source.huggingface.repository": "google/gemma-3-1b-it"}, - ) - assert b._sliding_window_pattern == [(i + 1) % 6 != 0 for i in range(18)] - with open(tmp_path / "swa_cache.json") as f: - assert json.load(f) == {"gemma3": 6} - - class TestGGUFParserReset: """Verify that fields are properly reset between parses.""" @@ -695,19 +209,11 @@ def test_reset_between_parses(self): "block_count": 32, "attention.key_length": 128, "attention.kv_lora_rank": 512, - "attention.head_count_kv": [8, 2], - "attention.sliding_window_pattern": [True, False], - "attention.key_length_swa": 64, - "attention.value_length_swa": 64, "ssm.inner_size": 4096, }, ) assert b._kv_key_length == 128 assert b._kv_lora_rank == 512 - assert b._n_kv_heads_by_layer == [8, 2] - assert b._sliding_window_pattern == [True, False] - assert b._kv_key_length_swa == 64 - assert b._kv_value_length_swa == 64 assert b._ssm_inner_size == 4096 # Second parse without those fields -- they should be None @@ -724,10 +230,6 @@ def test_reset_between_parses(self): os.unlink(path) assert b._kv_key_length is None assert b._kv_lora_rank is None - assert b._n_kv_heads_by_layer is None - assert b._sliding_window_pattern is None - assert b._kv_key_length_swa is None - assert b._kv_value_length_swa is None assert b._ssm_inner_size is None assert b._n_layers == 64 @@ -953,9 +455,7 @@ def test_gemma3(self): n_global = max(1, 62 // 4) # 15 n_swa = 62 - n_global # 47 kv_per = 16 * (128 + 128) * 2 - # SWA cache is double-buffered: 2 * sliding_window cells, capped at n_ctx. - swa_cells = min(131072, 2 * 1024) - expected = int(n_global * 131072 * kv_per + n_swa * swa_cells * kv_per) + expected = int(n_global * 131072 * kv_per + n_swa * min(131072, 1024) * kv_per) assert b._estimate_kv_cache_bytes(131072, "f16") == expected def test_gpt_oss(self): @@ -972,52 +472,27 @@ def test_gpt_oss(self): n_global = max(1, 24 // 4) # 6 n_swa = 24 - n_global # 18 kv_per = 8 * (64 + 64) * 2 - swa_cells = min(131072, 2 * 128) - expected = int(n_global * 131072 * kv_per + n_swa * swa_cells * kv_per) + expected = int(n_global * 131072 * kv_per + n_swa * min(131072, 128) * kv_per) assert b._estimate_kv_cache_bytes(131072, "f16") == expected - def test_gemma4_per_layer_swa_metadata(self): - b = self._swa_backend( - _n_layers = 30, - _n_kv_heads = None, - _n_kv_heads_by_layer = [8, 8, 8, 8, 8, 2] * 5, - _n_heads = 16, - _embedding_length = 2816, - _kv_key_length = 512, - _kv_value_length = 512, - _sliding_window = 1024, - _sliding_window_pattern = [True, True, True, True, True, False] * 5, - _kv_key_length_swa = 256, - _kv_value_length_swa = 256, - ) - - full_layers = 5 - sliding_layers = 25 - - def expected(ctx): - full = full_layers * ctx * 2 * (512 + 512) * 2 - sliding = sliding_layers * min(ctx, 2 * 1024) * 8 * (256 + 256) * 2 - return int(full + sliding) - - for ctx in (4096, 46500, 262144): - assert b._estimate_kv_cache_bytes(ctx, "f16") == expected(ctx) - def test_ctx_smaller_than_window(self): - """When context < 2 * sliding_window, SWA cache caps at ctx.""" + """When context < sliding_window, SWA layers use full context anyway.""" b = self._swa_backend(_sliding_window = 8192) n_global = max(1, 62 // 4) # 15 n_swa = 62 - n_global # 47 kv_per = 16 * (128 + 128) * 2 ctx = 4096 - expected = int(n_global * ctx * kv_per + n_swa * min(ctx, 2 * 8192) * kv_per) + expected = int(n_global * ctx * kv_per + n_swa * min(ctx, 8192) * kv_per) + # min(4096, 8192) = 4096, so both pools use full ctx assert b._estimate_kv_cache_bytes(ctx, "f16") == expected def test_odd_layer_count(self): + """Odd layer count: n_global = max(1, n//4), n_swa = n - n_global.""" b = self._swa_backend(_n_layers = 63) n_global = max(1, 63 // 4) # 15 n_swa = 63 - n_global # 48 kv_per = 16 * (128 + 128) * 2 - expected = int(n_global * 1000 * kv_per + n_swa * min(1000, 2 * 1024) * kv_per) + expected = int(n_global * 1000 * kv_per + n_swa * min(1000, 1024) * kv_per) assert b._estimate_kv_cache_bytes(1000, "f16") == expected @@ -1310,686 +785,6 @@ def test_both_heads_none_falls_to_one(self): assert result == expected -# --------------------------------------------------------------------------- -# J2. Server-flag knobs (--swa-full, --kv-unified/--parallel, -# --ctx-checkpoints, --kv-offload) -# --------------------------------------------------------------------------- - - -class TestServerFlags: - """Estimator should mirror llama-server CLI flags that change KV size.""" - - def _swa_backend(self, **overrides): - defaults = { - "_n_layers": 26, - "_n_kv_heads": 4, - "_n_heads": 8, - "_embedding_length": 1152, - "_kv_key_length": 256, - "_kv_value_length": 256, - "_sliding_window": 512, - "_sliding_window_pattern": [True, True, True, True, True, False] * 4 - + [True, True], - } - defaults.update(overrides) - b = LlamaCppBackend() - for k, v in defaults.items(): - setattr(b, k, v) - return b - - def _gqa_backend(self, **overrides): - defaults = { - "_n_layers": 28, - "_n_kv_heads": 8, - "_n_heads": 16, - "_embedding_length": 1024, - "_kv_key_length": 128, - "_kv_value_length": 128, - } - defaults.update(overrides) - b = LlamaCppBackend() - for k, v in defaults.items(): - setattr(b, k, v) - return b - - # ── --swa-full ────────────────────────────────────────────────── - - def test_swa_full_collapses_pattern_path_to_full_ctx(self): - b = self._swa_backend() - ctx = 32_768 - flagged = b._estimate_kv_cache_bytes(ctx, "f16", swa_full = True) - # With swa_full, every layer caches n_ctx -- equals path 4 sizing. - kv_per_token = 4 * (256 + 256) * 2 # n_kv_heads * (k+v) * f16 - expected = 26 * ctx * kv_per_token - assert flagged == expected - assert flagged > b._estimate_kv_cache_bytes(ctx, "f16") - - def test_swa_full_collapses_legacy_path_to_full_ctx(self): - # No per-layer pattern -> 1/4-global heuristic; swa_full overrides. - b = self._swa_backend(_sliding_window_pattern = None) - ctx = 16_384 - flagged = b._estimate_kv_cache_bytes(ctx, "f16", swa_full = True) - n_global = max(1, 26 // 4) - n_swa = 26 - n_global - kv_per = 4 * (256 + 256) * 2 - # swa_cells == n_ctx when swa_full=True - expected = n_global * ctx * kv_per + n_swa * ctx * kv_per - assert flagged == expected - - def test_swa_full_no_op_for_non_swa_model(self): - b = self._gqa_backend() - baseline = b._estimate_kv_cache_bytes(8192, "f16") - flagged = b._estimate_kv_cache_bytes(8192, "f16", swa_full = True) - assert flagged == baseline - - def test_swa_full_suppresses_checkpoint_term(self): - b = self._swa_backend() - with_cp = b._estimate_kv_cache_bytes(8192, "f16", ctx_checkpoints = 8) - with_cp_full = b._estimate_kv_cache_bytes( - 8192, "f16", ctx_checkpoints = 8, swa_full = True - ) - no_cp_full = b._estimate_kv_cache_bytes(8192, "f16", swa_full = True) - # Checkpoints only matter when SWA layers don't already keep n_ctx. - assert with_cp_full == no_cp_full - assert with_cp > b._estimate_kv_cache_bytes(8192, "f16") - - # ── --parallel + --kv-unified ────────────────────────────────── - # Empirically verified against llama-server: non-SWA caches partition - # n_ctx across slots (total memory constant); SWA layers are the only - # portion that scales with --parallel. --kv-unified is currently a - # no-op for memory math (kept for API forward-compat). - - def test_gqa_kv_constant_across_parallel(self): - b = self._gqa_backend() - baseline = b._estimate_kv_cache_bytes(4096, "f16") - for slots in (1, 2, 4, 8): - for unified in (True, False): - assert ( - b._estimate_kv_cache_bytes( - 4096, "f16", n_parallel = slots, kv_unified = unified - ) - == baseline - ) - - def test_zero_parallel_floors_at_one(self): - b = self._gqa_backend() - baseline = b._estimate_kv_cache_bytes(4096, "f16") - for unified in (True, False): - assert ( - b._estimate_kv_cache_bytes( - 4096, "f16", n_parallel = 0, kv_unified = unified - ) - == baseline - ) - - def test_swa_path_scales_only_swa_portion(self): - b = self._swa_backend() - ctx = 8192 - baseline = b._estimate_kv_cache_bytes(ctx, "f16") - # Decompose baseline by walking the same loop the estimator does. - swa = b._sliding_window - per_token_global = 4 * (256 + 256) * 2 # n_kv * (k+v) * f16 - per_token_swa = 4 * (256 + 256) * 2 # k_swa/val_swa fall back - per_slot_swa_cells = min(ctx, 2 * swa) # not clamped at parallel=1 - global_bytes = sum( - ctx * per_token_global - for f in b._sliding_window_pattern[: b._n_layers] - if not f - ) - swa_bytes_per_slot = sum( - per_slot_swa_cells * per_token_swa - for f in b._sliding_window_pattern[: b._n_layers] - if f - ) - # Sanity: parallel=1 reproduces baseline exactly - assert global_bytes + swa_bytes_per_slot == baseline - # Only SWA portion scales by parallel - for slots in (1, 2, 3, 4): - scaled = b._estimate_kv_cache_bytes( - ctx, "f16", n_parallel = slots, kv_unified = False - ) - # SWA cells get clamped to per_slot_ctx when ctx/slots < 2*swa - per_slot_ctx = max(1, ctx // slots) - cells = min(ctx, 2 * swa, per_slot_ctx) - swa_bps = sum( - cells * per_token_swa - for f in b._sliding_window_pattern[: b._n_layers] - if f - ) - assert scaled == global_bytes + slots * swa_bps - - def test_mla_kv_constant_across_parallel(self): - b = LlamaCppBackend() - b._n_layers = 60 - b._n_kv_heads = 1 - b._kv_lora_rank = 512 - b._key_length_mla = 64 - b._kv_key_length = 576 - baseline = b._estimate_kv_cache_bytes(8192, "f16") - for slots in (1, 2, 4, 8): - for unified in (True, False): - assert ( - b._estimate_kv_cache_bytes( - 8192, "f16", n_parallel = slots, kv_unified = unified - ) - == baseline - ) - - # ── --ctx-checkpoints ────────────────────────────────────────── - - def test_ctx_checkpoints_zero_is_no_op(self): - b = self._swa_backend() - baseline = b._estimate_kv_cache_bytes(8192, "f16") - assert b._estimate_kv_cache_bytes(8192, "f16", ctx_checkpoints = 0) == baseline - - def test_ctx_checkpoints_no_op_for_non_swa(self): - b = self._gqa_backend() - baseline = b._estimate_kv_cache_bytes(8192, "f16") - assert b._estimate_kv_cache_bytes(8192, "f16", ctx_checkpoints = 32) == baseline - - def test_ctx_checkpoints_pattern_path_adds_known_bytes(self): - b = self._swa_backend() - ctx = 8192 - baseline = b._estimate_kv_cache_bytes(ctx, "f16") - flagged = b._estimate_kv_cache_bytes(ctx, "f16", ctx_checkpoints = 4) - # 22 SWA layers * 4 checkpoints * 512 cells * 4 heads * (256+256) * 2 bytes - n_swa_layers = sum( - 1 for f in [True, True, True, True, True, False] * 4 + [True, True] if f - ) - per_layer = 4 * 512 * 4 * (256 + 256) * 2 - assert flagged == baseline + n_swa_layers * per_layer - - def test_ctx_checkpoints_legacy_path_adds_known_bytes(self): - b = self._swa_backend(_sliding_window_pattern = None) - ctx = 8192 - baseline = b._estimate_kv_cache_bytes(ctx, "f16") - flagged = b._estimate_kv_cache_bytes(ctx, "f16", ctx_checkpoints = 4) - n_global = max(1, 26 // 4) - n_swa = 26 - n_global - kv_per = 4 * (256 + 256) * 2 - extra = 4 * n_swa * 512 * kv_per # ctx_checkpoints * n_swa * sliding * kv_per - assert flagged == baseline + extra - - def test_ctx_checkpoints_compose_with_n_parallel(self): - # Only the SWA + checkpoint portion scales by n_parallel; the - # global-layer portion stays constant. - b = self._swa_backend() - ctx = 8192 - swa = b._sliding_window - per_token = 4 * (256 + 256) * 2 - global_bytes = sum( - ctx * per_token for f in b._sliding_window_pattern[: b._n_layers] if not f - ) - n_swa_layers = sum(1 for f in b._sliding_window_pattern[: b._n_layers] if f) - slots = 3 - per_slot_ctx = max(1, ctx // slots) - swa_cells = min(ctx, 2 * swa, per_slot_ctx) - swa_bytes_per_slot = n_swa_layers * swa_cells * per_token - cp_extra_per_slot = n_swa_layers * 4 * swa * per_token # 4 checkpoints - flagged = b._estimate_kv_cache_bytes( - ctx, "f16", ctx_checkpoints = 4, n_parallel = slots, kv_unified = False - ) - assert flagged == global_bytes + slots * ( - swa_bytes_per_slot + cp_extra_per_slot - ) - - # ── --kv-offload (kv_on_gpu) ─────────────────────────────────── - - def test_fit_returns_requested_when_kv_off_gpu(self): - b = self._gqa_backend() - # Tiny VRAM budget -- normally would force a reduction. - fitted = b._fit_context_to_vram( - requested_ctx = 32_768, - available_mib = 1, - model_size_bytes = 100, - cache_type_kv = "f16", - kv_on_gpu = False, - ) - assert fitted == 32_768 - - def test_fit_reduces_when_kv_on_gpu(self): - b = self._gqa_backend() - fitted = b._fit_context_to_vram( - requested_ctx = 32_768, - available_mib = 64, - model_size_bytes = 1024 * 1024, # 1 MiB - cache_type_kv = "f16", - kv_on_gpu = True, - ) - assert fitted < 32_768 - - def test_fit_threads_swa_full_through_estimator(self): - # SWA model, generous budget; both should fit but cache size differs. - b = self._swa_backend() - ctx = 8192 - kv_default = b._estimate_kv_cache_bytes(ctx, "f16") - kv_full = b._estimate_kv_cache_bytes(ctx, "f16", swa_full = True) - assert kv_full > kv_default - # Budget = model + kv_default (rounded up) -- swa_full should not fit. - budget_mib = (1024 * 1024 + kv_default) / (1024 * 1024) / 0.90 + 1 - fitted_default = b._fit_context_to_vram( - requested_ctx = ctx, - available_mib = int(budget_mib), - model_size_bytes = 1024 * 1024, - cache_type_kv = "f16", - ) - fitted_full = b._fit_context_to_vram( - requested_ctx = ctx, - available_mib = int(budget_mib), - model_size_bytes = 1024 * 1024, - cache_type_kv = "f16", - swa_full = True, - ) - assert fitted_default == ctx - assert fitted_full < ctx - - -# --------------------------------------------------------------------------- -# J2.5. --parallel N memory accounting (per-layer-type scaling rule) -# --------------------------------------------------------------------------- - - -class TestParallelSWAScaling: - """Verifies the per-layer-type scaling rule against the closed form - measured from llama-server. Empirical formula on Gemma-3 270m at - ctx=8192: total_kv = 24 + parallel * 15 (MiB). - - Rule (verified vs ``llama-server`` log on real GGUFs): - * non-SWA layers: total cells = n_ctx, partitioned across slots, - memory CONSTANT in n_parallel. - * SWA layers: per-slot cells = 2 * sliding_window (clamped at - n_ctx and at per_slot_ctx); memory LINEAR in n_parallel. - * --kv-unified is a no-op for memory math; both modes yield the - same total in measured cases. - """ - - def _gqa_backend(self, **overrides): - defaults = { - "_n_layers": 28, - "_n_kv_heads": 8, - "_n_heads": 16, - "_embedding_length": 1024, - "_kv_key_length": 128, - "_kv_value_length": 128, - } - defaults.update(overrides) - b = LlamaCppBackend() - for k, v in defaults.items(): - setattr(b, k, v) - return b - - def _swa_backend(self, **overrides): - defaults = { - "_n_layers": 18, - "_n_kv_heads": 1, - "_n_heads": 4, - "_embedding_length": 1024, - "_kv_key_length": 256, - "_kv_value_length": 256, - "_sliding_window": 512, - # 15 SWA + 3 global, mirrors gemma-3-270m - "_sliding_window_pattern": [ - t == "swa" for t in (["swa"] * 5 + ["global"]) * 3 - ], - } - defaults.update(overrides) - b = LlamaCppBackend() - for k, v in defaults.items(): - setattr(b, k, v) - return b - - # ── non-SWA paths: constant ──────────────────────────────────── - - def test_pure_gqa_constant_across_parallel(self): - b = self._gqa_backend() - baseline = b._estimate_kv_cache_bytes(8192, "f16") - for slots in (1, 2, 4, 8): - for unified in (True, False): - assert ( - b._estimate_kv_cache_bytes( - 8192, "f16", n_parallel = slots, kv_unified = unified - ) - == baseline - ) - - def test_mla_constant_across_parallel(self): - b = LlamaCppBackend() - b._n_layers = 60 - b._n_kv_heads = 1 - b._kv_lora_rank = 512 - b._key_length_mla = 64 - b._kv_key_length = 576 - baseline = b._estimate_kv_cache_bytes(8192, "f16") - for slots in (1, 2, 4, 8): - assert b._estimate_kv_cache_bytes(8192, "f16", n_parallel = slots) == baseline - - def test_hybrid_constant_across_parallel(self): - b = LlamaCppBackend() - b._n_layers = 64 - b._n_kv_heads = 16 - b._n_heads = 32 - b._embedding_length = 4096 - b._kv_key_length = 128 - b._kv_value_length = 128 - b._ssm_inner_size = 4096 - b._full_attention_interval = 4 - baseline = b._estimate_kv_cache_bytes(8192, "f16") - for slots in (1, 2, 4, 8): - assert b._estimate_kv_cache_bytes(8192, "f16", n_parallel = slots) == baseline - - def test_legacy_constant_across_parallel(self): - b = LlamaCppBackend() - b._n_layers = 32 - b._n_kv_heads = 8 - b._n_heads = 8 - b._embedding_length = 4096 - baseline = b._estimate_kv_cache_bytes(8192, "f16") - for slots in (1, 2, 4, 8): - assert b._estimate_kv_cache_bytes(8192, "f16", n_parallel = slots) == baseline - - # ── SWA paths: scale only the SWA portion ────────────────────── - - def test_swa_pattern_scales_only_swa_portion(self): - b = self._swa_backend() - ctx = 8192 - swa = b._sliding_window - per_token = 1 * (256 + 256) * 2 # n_kv * (k+v) * f16 - n_global = sum(1 for f in b._sliding_window_pattern if not f) - n_swa = sum(1 for f in b._sliding_window_pattern if f) - global_bytes = n_global * ctx * per_token - for slots in (1, 2, 4, 8): - per_slot_ctx = max(1, ctx // slots) - cells = min(ctx, 2 * swa, per_slot_ctx) - swa_bps = n_swa * cells * per_token - for unified in (True, False): - got = b._estimate_kv_cache_bytes( - ctx, "f16", n_parallel = slots, kv_unified = unified - ) - assert got == global_bytes + slots * swa_bps - - def test_swa_fallback_scales_only_swa_portion(self): - # No per-layer pattern -> 1/4-global heuristic. - b = self._swa_backend(_sliding_window_pattern = None) - ctx = 8192 - swa = b._sliding_window - n_layers = 18 - n_global = max(1, n_layers // 4) - n_swa = n_layers - n_global - per_token = 1 * (256 + 256) * 2 - global_bytes = n_global * ctx * per_token - for slots in (1, 2, 4, 8): - per_slot_ctx = max(1, ctx // slots) - cells = min(ctx, 2 * swa, per_slot_ctx) - swa_bps = n_swa * cells * per_token - got = b._estimate_kv_cache_bytes(ctx, "f16", n_parallel = slots) - assert got == global_bytes + slots * swa_bps - - def test_swa_per_slot_clamped_when_ctx_lt_slots_x_2window(self): - # ctx=4096 / slots=8 -> per_slot_ctx=512, but 2*sliding=1024. - # SWA cells should clamp at per_slot_ctx (512), not 2*sliding. - b = self._swa_backend() - ctx = 4096 - per_slot_ctx_at_8 = ctx // 8 - assert per_slot_ctx_at_8 < 2 * b._sliding_window - # Build expected with the clamped formula - n_swa = sum(1 for f in b._sliding_window_pattern if f) - n_global = sum(1 for f in b._sliding_window_pattern if not f) - per_token = 1 * (256 + 256) * 2 - global_bytes = n_global * ctx * per_token - cells = min(ctx, 2 * b._sliding_window, per_slot_ctx_at_8) - assert cells == per_slot_ctx_at_8 - expected = global_bytes + 8 * (n_swa * cells * per_token) - assert b._estimate_kv_cache_bytes(ctx, "f16", n_parallel = 8) == expected - - def test_swa_full_does_not_scale_under_parallel(self): - # swa_full forces every layer to n_ctx; result is the all-global - # GQA-style total, which is constant in parallel. - b = self._swa_backend() - ctx = 8192 - baseline = b._estimate_kv_cache_bytes(ctx, "f16", swa_full = True) - for slots in (1, 2, 4, 8): - assert ( - b._estimate_kv_cache_bytes(ctx, "f16", swa_full = True, n_parallel = slots) - == baseline - ) - - # ── kv_unified: no-op for memory math ────────────────────────── - - def test_kv_unified_is_no_op_for_memory_math(self): - # Both unified=True and unified=False must produce the same - # total bytes for every backend type and every parallel value. - backends = [ - ("gqa", self._gqa_backend()), - ("swa", self._swa_backend()), - ] - for label, b in backends: - for slots in (1, 2, 4, 8): - u = b._estimate_kv_cache_bytes( - 8192, "f16", n_parallel = slots, kv_unified = True - ) - nu = b._estimate_kv_cache_bytes( - 8192, "f16", n_parallel = slots, kv_unified = False - ) - assert u == nu, f"{label} parallel={slots} unified-mismatch" - - # ── Empirical Gemma-3 270m formula ───────────────────────────── - - def test_matches_empirical_gemma3_270m_formula(self): - """Exact match against the formula measured from llama-server: - total_kv = 24 + parallel * 15 (MiB) at ctx=8192. - - Geometry: 18 layers (3 global + 15 SWA), n_kv=1, head_dim=256, - sliding=512, f16. - """ - b = LlamaCppBackend() - b._n_layers = 18 - b._n_kv_heads = 1 - b._n_heads = 4 - b._embedding_length = 1024 - b._kv_key_length = 256 - b._kv_value_length = 256 - b._sliding_window = 512 - # 5-period [swa,swa,swa,swa,full] * 3 + [swa,swa,swa]: mirrors the - # bootstrap-resolved pattern for gemma3 (period 6) on an 18-layer - # model (15 SWA, 3 global). - b._sliding_window_pattern = [(i + 1) % 6 != 0 for i in range(18)] - n_global = 3 - n_swa = 15 - # Confirm pattern shape - assert sum(b._sliding_window_pattern) == n_swa - for slots, expected_mib in [(1, 39), (2, 54), (4, 84)]: - got_bytes = b._estimate_kv_cache_bytes(8192, "f16", n_parallel = slots) - got_mib = got_bytes / (1024 * 1024) - assert ( - got_mib == expected_mib - ), f"slots={slots}: got {got_mib} MiB, expected {expected_mib} MiB" - - -# --------------------------------------------------------------------------- -# J3. shared_kv_layers (Gemma 3n / Gemma 4) -# --------------------------------------------------------------------------- - - -class TestSharedKVLayers: - """``.attention.shared_kv_layers`` reduces the layer count that - actually allocates KV. The trailing ``shared_kv_layers`` blocks reuse - earlier caches (Gemma 3n: 35 layers, 15 shared -> 20 allocate; Gemma 4 - same field). Unset on every other arch -> no behavioural change.""" - - def _gemma3n_backend(self, **overrides): - # Mirrors google/gemma-3n-E4B-it: 35 layers, 15 shared, - # SWA window 1024, period 5 (4 sliding + 1 full repeating). - defaults = { - "_n_layers": 35, - "_n_kv_heads": 4, - "_n_heads": 8, - "_embedding_length": 2048, - "_kv_key_length": 256, - "_kv_value_length": 256, - "_sliding_window": 1024, - "_sliding_window_pattern": [ - t == "sliding_attention" - for t in (["sliding_attention"] * 4 + ["full_attention"]) * 7 - ], - "_shared_kv_layers": 15, - } - defaults.update(overrides) - b = LlamaCppBackend() - for k, v in defaults.items(): - setattr(b, k, v) - return b - - def _gqa_backend(self, **overrides): - defaults = { - "_n_layers": 28, - "_n_kv_heads": 8, - "_n_heads": 16, - "_embedding_length": 1024, - "_kv_key_length": 128, - "_kv_value_length": 128, - } - defaults.update(overrides) - b = LlamaCppBackend() - for k, v in defaults.items(): - setattr(b, k, v) - return b - - def test_field_initialises_to_none(self): - b = LlamaCppBackend() - assert b._shared_kv_layers is None - - def test_unset_field_is_noop(self): - b = self._gqa_backend() - baseline = b._estimate_kv_cache_bytes(8192, "f16") - b._shared_kv_layers = None - assert b._estimate_kv_cache_bytes(8192, "f16") == baseline - b._shared_kv_layers = 0 - assert b._estimate_kv_cache_bytes(8192, "f16") == baseline - - def test_path4_drops_shared_layers(self): - b = self._gqa_backend(_shared_kv_layers = 4) - ctx = 4096 - kv_per = 8 * (128 + 128) * 2 - # 28 - 4 = 24 layers actually allocate - assert b._estimate_kv_cache_bytes(ctx, "f16") == 24 * ctx * kv_per - - def test_path5_drops_shared_layers(self): - b = LlamaCppBackend() - b._n_layers = 32 - b._n_kv_heads = 8 - b._n_heads = 8 - b._embedding_length = 4096 - b._shared_kv_layers = 8 - ctx = 4096 - head_dim = 4096 // 8 # 512 - # 32 - 8 = 24 layers - expected = 2 * 8 * head_dim * 24 * ctx * 2 - assert b._estimate_kv_cache_bytes(ctx, "f16") == expected - - def test_path1_mla_drops_shared_layers(self): - b = LlamaCppBackend() - b._n_layers = 60 - b._n_kv_heads = 1 - b._kv_lora_rank = 512 - b._key_length_mla = 64 - b._kv_key_length = 576 - b._shared_kv_layers = 10 - ctx = 8192 - # 60 - 10 = 50 - assert b._estimate_kv_cache_bytes(ctx, "f16") == 50 * ctx * 1 * 576 * 2 - - def test_path3_pattern_loops_only_unshared_layers(self): - b = self._gemma3n_backend() - ctx = 8192 - # First 20 layers contribute; layers 20..34 are skipped. - # Pattern: [s,s,s,s,F] repeated. In layers 0..19: - # sliding: 16, full: 4 - sliding_in_unshared = sum(b._sliding_window_pattern[:20]) - full_in_unshared = 20 - sliding_in_unshared - assert sliding_in_unshared == 16 - assert full_in_unshared == 4 - kv_per = 4 * (256 + 256) * 2 - swa_cells = min(ctx, 2 * 1024) - expected = ( - full_in_unshared * ctx * kv_per + sliding_in_unshared * swa_cells * kv_per - ) - assert b._estimate_kv_cache_bytes(ctx, "f16") == expected - - def test_shared_layers_reduces_estimate(self): - b = self._gemma3n_backend() - with_shared = b._estimate_kv_cache_bytes(8192, "f16") - b._shared_kv_layers = 0 - without_shared = b._estimate_kv_cache_bytes(8192, "f16") - # 20/35 = 0.571 of the work; expect ~43% reduction. - ratio = with_shared / without_shared - assert 0.5 < ratio < 0.65 - - def test_path3_pattern_with_swa_full_and_shared(self): - b = self._gemma3n_backend() - ctx = 8192 - flagged = b._estimate_kv_cache_bytes(ctx, "f16", swa_full = True) - # Every unshared layer caches n_ctx; equals path-4-style sizing - # over only the 20 unshared layers. - kv_per = 4 * (256 + 256) * 2 - assert flagged == 20 * ctx * kv_per - - def test_path3_fallback_uses_unshared_count(self): - # No per-layer pattern -> 1/4-global heuristic over n_layers_kv, - # not n_layers. - b = self._gemma3n_backend(_sliding_window_pattern = None) - ctx = 8192 - n_layers_kv = 35 - 15 # 20 - n_global = max(1, n_layers_kv // 4) # 5 - n_swa = n_layers_kv - n_global # 15 - kv_per = 4 * (256 + 256) * 2 - swa_cells = min(ctx, 2 * 1024) - expected = n_global * ctx * kv_per + n_swa * swa_cells * kv_per - assert b._estimate_kv_cache_bytes(ctx, "f16") == expected - - def test_shared_floors_at_one_layer(self): - # Pathological: shared >= n_layers should not zero out the cache. - b = self._gqa_backend(_shared_kv_layers = 99) - ctx = 4096 - kv_per = 8 * (128 + 128) * 2 - assert b._estimate_kv_cache_bytes(ctx, "f16") == 1 * ctx * kv_per - - def test_composes_with_n_parallel(self): - # Only the SWA portion of the unshared layers scales by n_parallel; - # the global portion stays constant. - b = self._gemma3n_backend() - ctx = 8192 - swa = b._sliding_window - per_token = 4 * (256 + 256) * 2 - unshared_pattern = b._sliding_window_pattern[:20] # 35 - 15 shared - sliding_in_unshared = sum(unshared_pattern) - global_in_unshared = len(unshared_pattern) - sliding_in_unshared - global_bytes = global_in_unshared * ctx * per_token - slots = 3 - per_slot_ctx = max(1, ctx // slots) - swa_cells = min(ctx, 2 * swa, per_slot_ctx) - swa_bytes_per_slot = sliding_in_unshared * swa_cells * per_token - flagged = b._estimate_kv_cache_bytes( - ctx, "f16", n_parallel = slots, kv_unified = False - ) - assert flagged == global_bytes + slots * swa_bytes_per_slot - - def test_composes_with_ctx_checkpoints(self): - b = self._gemma3n_backend() - ctx = 8192 - baseline = b._estimate_kv_cache_bytes(ctx, "f16") - with_cp = b._estimate_kv_cache_bytes(ctx, "f16", ctx_checkpoints = 4) - # Checkpoints only count over UNSHARED SWA layers (16 of them). - sliding_in_unshared = sum(b._sliding_window_pattern[:20]) - per_cp_layer = 4 * 1024 * 4 * (256 + 256) * 2 # cps * swa * heads * (k+v) * bpe - assert with_cp == baseline + sliding_in_unshared * per_cp_layer - - def test_unload_resets_shared_kv_layers(self): - b = LlamaCppBackend() - b._shared_kv_layers = 12 - b.unload_model() - assert b._shared_kv_layers is None - - # --------------------------------------------------------------------------- # K. Lifecycle Tests # --------------------------------------------------------------------------- @@ -2004,18 +799,13 @@ def test_init_fields_none(self): "_kv_key_length", "_kv_value_length", "_sliding_window", - "_sliding_window_pattern", "_full_attention_interval", "_kv_lora_rank", "_key_length_mla", - "_kv_key_length_swa", - "_kv_value_length_swa", "_ssm_inner_size", "_ssm_state_size", - "_shared_kv_layers", ]: assert getattr(b, attr) is None - assert b._n_kv_heads_by_layer is None def test_unload_resets_fields(self): b = LlamaCppBackend() @@ -2023,30 +813,20 @@ def test_unload_resets_fields(self): b._kv_key_length = 128 b._kv_lora_rank = 512 b._sliding_window = 1024 - b._sliding_window_pattern = [True, False] - b._n_kv_heads_by_layer = [8, 2] - b._kv_key_length_swa = 64 - b._kv_value_length_swa = 64 b._ssm_inner_size = 4096 b._full_attention_interval = 4 - b._shared_kv_layers = 8 b.unload_model() for attr in [ "_kv_key_length", "_kv_value_length", "_sliding_window", - "_sliding_window_pattern", "_full_attention_interval", "_kv_lora_rank", "_key_length_mla", - "_kv_key_length_swa", - "_kv_value_length_swa", "_ssm_inner_size", "_ssm_state_size", - "_shared_kv_layers", ]: assert getattr(b, attr) is None - assert b._n_kv_heads_by_layer is None def test_end_to_end_synthetic_mla(self): """Full round-trip: write GGUF -> parse -> estimate.""" @@ -2107,46 +887,12 @@ def test_end_to_end_synthetic_swa(self): ) assert b._can_estimate_kv() result = b._estimate_kv_cache_bytes(131072, "f16") - # gemma3 -> period 6 from the bootstrap table, SWA cache - # double-buffered to 2 * sliding_window cells. - period = 6 + n_global = max(1, 62 // 4) # 15 + n_swa = 62 - n_global # 47 kv_per = 16 * 256 * 2 - expected = 0 - for i in range(62): - is_swa = (i + 1) % period != 0 - layer_ctx = min(131072, 2 * 1024) if is_swa else 131072 - expected += layer_ctx * kv_per + expected = int(n_global * 131072 * kv_per + n_swa * 1024 * kv_per) assert result == expected - def test_end_to_end_synthetic_shared_kv_round_trip(self): - # Mirrors gemma3n_text: 35 layers, 15 shared, sliding_window=1024. - b = _backend_from_gguf( - "gemma3n_text", - { - "context_length": 32768, - "block_count": 35, - "attention.head_count_kv": 4, - "attention.head_count": 8, - "embedding_length": 2048, - "attention.key_length": 256, - "attention.value_length": 256, - "attention.sliding_window": 1024, - "attention.shared_kv_layers": 15, - }, - ) - assert b._can_estimate_kv() - assert b._shared_kv_layers == 15 - # Bootstrap table for gemma3n_text -> period 5; the resolver - # synthesises a 35-entry bool array. The first 20 entries - # (n_layers - shared) are the only ones that allocate KV. - result = b._estimate_kv_cache_bytes(8192, "f16") - assert result > 0 - # Sanity: setting shared back to 0 must produce a strictly larger - # estimate (more layers allocate). - b._shared_kv_layers = 0 - unshared = b._estimate_kv_cache_bytes(8192, "f16") - assert unshared > result - def test_end_to_end_synthetic_gqa(self): b = _backend_from_gguf( "qwen3", diff --git a/studio/backend/tests/test_llama_cpp_context_fit.py b/studio/backend/tests/test_llama_cpp_context_fit.py index caa6397901..f498655347 100644 --- a/studio/backend/tests/test_llama_cpp_context_fit.py +++ b/studio/backend/tests/test_llama_cpp_context_fit.py @@ -114,13 +114,9 @@ def _make_backend( inst._kv_value_length = kv_value_length inst._kv_lora_rank = None inst._sliding_window = None - inst._sliding_window_pattern = None inst._ssm_inner_size = None inst._full_attention_interval = None inst._key_length_mla = None - inst._n_kv_heads_by_layer = None - inst._kv_key_length_swa = None - inst._kv_value_length_swa = None return inst @@ -141,7 +137,7 @@ def _drive( model_size = int(model_gib * GIB) cache_type_kv = None - def fake_estimate(n_ctx_, _type = None, **_kwargs): + def fake_estimate(n_ctx_, _type = None): return 0 if n_ctx_ <= 0 else n_ctx_ * kv_per_token_bytes inst._estimate_kv_cache_bytes = fake_estimate diff --git a/studio/backend/tests/test_llama_cpp_max_context_threshold.py b/studio/backend/tests/test_llama_cpp_max_context_threshold.py index 22e4cda7d1..5fd0243c9f 100644 --- a/studio/backend/tests/test_llama_cpp_max_context_threshold.py +++ b/studio/backend/tests/test_llama_cpp_max_context_threshold.py @@ -99,13 +99,9 @@ def _make_backend(native_ctx = 131072): inst._kv_value_length = 128 inst._kv_lora_rank = None inst._sliding_window = None - inst._sliding_window_pattern = None inst._ssm_inner_size = None inst._full_attention_interval = None inst._key_length_mla = None - inst._n_kv_heads_by_layer = None - inst._kv_key_length_swa = None - inst._kv_value_length_swa = None return inst @@ -118,7 +114,7 @@ def _compute_max_available_ctx(native_ctx, model_gib, gpus, kv_per_token_bytes = model_size = int(model_gib * GIB) inst._estimate_kv_cache_bytes = ( - lambda n, _t = None, **_kw: 0 if n <= 0 else n * kv_per_token_bytes + lambda n, _t = None: 0 if n <= 0 else n * kv_per_token_bytes ) inst._can_estimate_kv = lambda: True diff --git a/studio/backend/tests/test_llama_server_args.py b/studio/backend/tests/test_llama_server_args.py deleted file mode 100644 index 351fbd014d..0000000000 --- a/studio/backend/tests/test_llama_server_args.py +++ /dev/null @@ -1,189 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 - -"""Unit tests for the llama-server pass-through args validator. - -The validator is the security boundary between user-supplied CLI / HTTP -input and the llama-server subprocess command. These tests pin the -denylist behavior so the boundary doesn't quietly regress when new -managed flags are added. -""" - -from __future__ import annotations - -import pytest - -from core.inference.llama_server_args import ( - is_managed_flag, - validate_extra_args, -) - - -# ── Pass-through (allowed) ─────────────────────────────────────────── - - -@pytest.mark.parametrize( - "args", - [ - # Sampling - ["--top-k", "20"], - ["--top-p", "0.9", "--min-p", "0.05"], - ["--seed", "-1"], # negative value, not a flag - ["--temp", "0.0"], - ["--repeat-penalty", "1.05"], - ["--mirostat", "2", "--mirostat-lr", "0.1"], - ["--xtc-probability", "0.05", "--xtc-threshold", "0.1"], - ["--dry-multiplier", "0.5"], - # Tier-2 knobs that map to LoadRequest fields - ["--cache-type-k", "q8_0"], - ["--cache-type-v", "q8_0"], - ["--chat-template-file", "/tmp/tpl.jinja"], - ["--chat-template-kwargs", '{"reasoning_effort":"high"}'], - ["--spec-type", "ngram-mod"], - ["--spec-default"], - # Reasoning controls - ["--reasoning-format", "deepseek"], - ["-rea", "auto"], - # Soft-managed flags the user may want to override on the CLI; - # llama.cpp's last-wins parsing means these win over Studio's - # auto-set version. - ["-c", "131072"], - ["--ctx-size", "8192"], - ["--parallel", "1"], - ["-np", "8"], - ["--flash-attn", "off"], - ["-fa", "on"], - ["--no-context-shift"], - ["--context-shift"], - ["--jinja"], - ["--no-jinja"], - ["-ngl", "-1"], - ["--gpu-layers", "32"], - ["-t", "16"], - ["--threads", "32"], - ["-fit", "off"], - ["--fit", "on"], - ["--fit-ctx", "8192"], - ], -) -def test_pass_through_allowed(args): - assert validate_extra_args(args) == args - - -def test_none_returns_empty_list(): - assert validate_extra_args(None) == [] - - -def test_empty_list_returns_empty_list(): - assert validate_extra_args([]) == [] - - -def test_value_with_equals_form_passes_through(): - assert validate_extra_args(["--top-k=20"]) == ["--top-k=20"] - - -def test_non_flag_token_passes_through(): - # A bare positional value (not preceded by a flag) is preserved - # verbatim. llama-server may reject it, but that's not our job. - assert validate_extra_args(["foo"]) == ["foo"] - - -# ── Denylist (rejected) ────────────────────────────────────────────── - - -@pytest.mark.parametrize( - "denied", - [ - # Model identity - "-m", - "--model", - "-hf", - "-hfr", - "--hf-repo", - "-hff", - "--hf-file", - "-hft", - "--hf-token", - "-mm", - "--mmproj", - "--mmproj-url", - # Networking (Studio binds + proxies) - "--host", - "--port", - "--path", - "--api-prefix", - "--reuse-port", - # Auth / TLS - "--api-key", - "--api-key-file", - "--ssl-key-file", - "--ssl-cert-file", - # Single-model server - "--webui", - "--no-webui", - "--models-dir", - "--models-max", - ], -) -def test_denylist_rejects_all_aliases(denied): - with pytest.raises(ValueError, match = denied): - validate_extra_args([denied, "value"]) - - -def test_denylist_rejects_equals_form(): - with pytest.raises(ValueError, match = "--port"): - validate_extra_args(["--port=9000"]) - - -def test_denylist_rejects_short_form_when_long_is_denied(): - # -m is the short form of the hard-denied --model; rejecting only - # the long form would leave a trivial bypass. - with pytest.raises(ValueError, match = "-m"): - validate_extra_args(["-m", "/some/other/path.gguf"]) - - -def test_denylist_message_names_offending_flag(): - with pytest.raises(ValueError) as excinfo: - validate_extra_args(["--top-k", "20", "--api-key", "secret"]) - assert "--api-key" in str(excinfo.value) - - -def test_first_denied_flag_short_circuits(): - # Validation stops at the first denied flag; later denied flags - # in the same call don't matter for behaviour, but the message - # should name the first one we hit. - with pytest.raises(ValueError, match = "--port"): - validate_extra_args(["--port", "1", "--host", "x"]) - - -# ── Numeric values that look flag-ish ───────────────────────────────── - - -@pytest.mark.parametrize("value", ["-1", "-0.5", "-42", "-.5"]) -def test_negative_number_value_is_not_flag(value): - # ``--seed -1`` is a value, not a flag. Validator must not try - # to look up "-1" in the denylist. - assert validate_extra_args(["--seed", value]) == ["--seed", value] - - -# ── is_managed_flag helper ─────────────────────────────────────────── - - -def test_is_managed_flag_true_for_denied(): - assert is_managed_flag("--port") is True - assert is_managed_flag("--api-key") is True - assert is_managed_flag("-m") is True - assert is_managed_flag("--model") is True - - -def test_is_managed_flag_false_for_pass_through(): - assert is_managed_flag("--top-k") is False - assert is_managed_flag("--cache-type-k") is False - assert is_managed_flag("--chat-template-file") is False - # Soft-managed flags pass through (last-wins override) - assert is_managed_flag("-c") is False - assert is_managed_flag("--ctx-size") is False - assert is_managed_flag("--parallel") is False - assert is_managed_flag("--flash-attn") is False - assert is_managed_flag("-ngl") is False - assert is_managed_flag("--threads") is False diff --git a/studio/backend/tests/test_mlx_inference_backend.py b/studio/backend/tests/test_mlx_inference_backend.py deleted file mode 100644 index 868e537372..0000000000 --- a/studio/backend/tests/test_mlx_inference_backend.py +++ /dev/null @@ -1,157 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only - -import sys -import types -from types import SimpleNamespace - - -class _DummyMetal: - @staticmethod - def is_available(): - return False - - -class _DummyMX: - metal = _DummyMetal() - - @staticmethod - def set_wired_limit(_limit): - return None - - @staticmethod - def device_info(): - return {"max_recommended_working_set_size": 1024} - - -class _DummyTokenizer: - pass - - -class _DummyProcessor: - tokenizer = _DummyTokenizer() - - -class _DummyModel: - pass - - -def _install_fake_mlx(monkeypatch): - mlx_pkg = types.ModuleType("mlx") - mlx_core = types.ModuleType("mlx.core") - mlx_core.metal = _DummyMetal() - mlx_core.set_wired_limit = _DummyMX.set_wired_limit - mlx_core.device_info = _DummyMX.device_info - mlx_pkg.core = mlx_core - monkeypatch.setitem(sys.modules, "mlx", mlx_pkg) - monkeypatch.setitem(sys.modules, "mlx.core", mlx_core) - - -def _install_fake_fast_mlx(monkeypatch, calls): - class _FastMLXModel: - @staticmethod - def from_pretrained(*args, **kwargs): - calls.append((args, kwargs)) - if kwargs["text_only"] is False: - return _DummyModel(), _DummyProcessor() - return _DummyModel(), _DummyTokenizer() - - unsloth_zoo_pkg = types.ModuleType("unsloth_zoo") - mlx_loader = types.ModuleType("unsloth_zoo.mlx_loader") - mlx_loader.FastMLXModel = _FastMLXModel - unsloth_zoo_pkg.mlx_loader = mlx_loader - monkeypatch.setitem(sys.modules, "unsloth_zoo", unsloth_zoo_pkg) - monkeypatch.setitem(sys.modules, "unsloth_zoo.mlx_loader", mlx_loader) - - -def test_mlx_inference_text_load_forwards_studio_settings(monkeypatch): - _install_fake_mlx(monkeypatch) - calls = [] - _install_fake_fast_mlx(monkeypatch, calls) - - from core.inference.mlx_inference import MLXInferenceBackend - - backend = MLXInferenceBackend() - config = SimpleNamespace(identifier = "fake/text", is_vision = False, is_lora = False) - - assert backend.load_model( - config, - max_seq_length = 4096, - load_in_4bit = False, - hf_token = "hf-token", - trust_remote_code = True, - dtype = "float16", - ) - - assert calls == [ - ( - ("fake/text",), - { - "max_seq_length": 4096, - "dtype": "float16", - "load_in_4bit": False, - "token": "hf-token", - "trust_remote_code": True, - "text_only": True, - }, - ) - ] - assert backend._is_vlm is False - assert isinstance(backend._tokenizer, _DummyTokenizer) - - -def test_mlx_inference_vlm_lora_uses_unsloth_loader_without_native_adapter_rewrite( - monkeypatch, - tmp_path, -): - _install_fake_mlx(monkeypatch) - calls = [] - _install_fake_fast_mlx(monkeypatch, calls) - - def _native_vlm_load(*_args, **_kwargs): - raise AssertionError("Studio MLX VLM inference must use FastMLXModel") - - mlx_vlm = types.ModuleType("mlx_vlm") - mlx_vlm.load = _native_vlm_load - monkeypatch.setitem(sys.modules, "mlx_vlm", mlx_vlm) - - adapter_dir = tmp_path / "adapter" - adapter_dir.mkdir() - cfg_path = adapter_dir / "adapter_config.json" - original_cfg = '{"base_model_name_or_path": "fake/base", "rank": 8}\n' - cfg_path.write_text(original_cfg) - - from core.inference.mlx_inference import MLXInferenceBackend - - backend = MLXInferenceBackend() - config = SimpleNamespace( - identifier = str(adapter_dir), - is_vision = True, - is_lora = True, - base_model = "fake/base", - ) - - assert backend.load_model( - config, - max_seq_length = 8192, - load_in_4bit = True, - hf_token = "hf-token", - trust_remote_code = True, - ) - - assert calls == [ - ( - (str(adapter_dir),), - { - "max_seq_length": 8192, - "dtype": None, - "load_in_4bit": True, - "token": "hf-token", - "trust_remote_code": True, - "text_only": False, - }, - ) - ] - assert cfg_path.read_text() == original_cfg - assert backend._is_vlm is True - assert isinstance(backend._processor, _DummyProcessor) - assert isinstance(backend._tokenizer, _DummyTokenizer) diff --git a/studio/backend/tests/test_mlx_training_worker_config.py b/studio/backend/tests/test_mlx_training_worker_config.py deleted file mode 100644 index 5900af4e3d..0000000000 --- a/studio/backend/tests/test_mlx_training_worker_config.py +++ /dev/null @@ -1,83 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only - -import importlib.util -import sys -import types -from pathlib import Path - -import pytest - - -def _load_worker_module(): - stub_names = ( - "structlog", - "loggers", - "utils", - "utils.hardware", - "utils.wheel_utils", - ) - previous_modules = {name: sys.modules.get(name) for name in stub_names} - - try: - sys.modules["structlog"] = types.ModuleType("structlog") - - loggers = types.ModuleType("loggers") - loggers.get_logger = lambda *_args, **_kwargs: None - sys.modules["loggers"] = loggers - - utils = types.ModuleType("utils") - utils.__path__ = [] - sys.modules["utils"] = utils - - hardware = types.ModuleType("utils.hardware") - hardware.apply_gpu_ids = lambda *_args, **_kwargs: None - sys.modules["utils.hardware"] = hardware - - wheel_utils = types.ModuleType("utils.wheel_utils") - for name in ( - "direct_wheel_url", - "flash_attn_wheel_url", - "install_wheel", - "probe_torch_wheel_env", - "url_exists", - ): - setattr(wheel_utils, name, lambda *_args, **_kwargs: None) - sys.modules["utils.wheel_utils"] = wheel_utils - - worker_path = ( - Path(__file__).resolve().parents[1] / "core" / "training" / "worker.py" - ) - spec = importlib.util.spec_from_file_location( - "mlx_training_worker_under_test", worker_path - ) - module = importlib.util.module_from_spec(spec) - assert spec.loader is not None - spec.loader.exec_module(module) - return module - finally: - for name, module in previous_modules.items(): - if module is None: - sys.modules.pop(name, None) - else: - sys.modules[name] = module - - -_worker = _load_worker_module() -_normalize_mlx_studio_optimizer = _worker._normalize_mlx_studio_optimizer -_normalize_mlx_studio_scheduler = _worker._normalize_mlx_studio_scheduler - - -def test_mlx_studio_optimizer_aliases_are_explicit(): - assert _normalize_mlx_studio_optimizer("adamw_8bit") == "adamw" - assert _normalize_mlx_studio_optimizer("paged_adamw_8bit") == "adamw" - assert _normalize_mlx_studio_optimizer("adafactor") == "adafactor" - - -def test_mlx_studio_rejects_unknown_optimizer(): - with pytest.raises(ValueError, match = "Unsupported optimizer for MLX training"): - _normalize_mlx_studio_optimizer("adamw_typo") - - -def test_mlx_studio_rejects_unknown_scheduler(): - with pytest.raises(ValueError, match = "Unsupported LR scheduler for MLX training"): - _normalize_mlx_studio_scheduler("linear_typo") diff --git a/studio/backend/tests/test_native_context_length.py b/studio/backend/tests/test_native_context_length.py index 60622c776d..7c69e56f89 100644 --- a/studio/backend/tests/test_native_context_length.py +++ b/studio/backend/tests/test_native_context_length.py @@ -320,23 +320,11 @@ def test_status_response_has_field(self): """Field exists in InferenceStatusResponse.model_fields.""" assert "native_context_length" in InferenceStatusResponse.model_fields - def test_status_response_has_chat_template_field(self): - """Status includes chat_template so the UI can rehydrate after refresh.""" - assert "chat_template" in InferenceStatusResponse.model_fields - def test_status_response_defaults_none(self): """Omitting native_context_length defaults to None.""" resp = InferenceStatusResponse() assert resp.native_context_length is None - def test_status_response_chat_template_roundtrip(self): - """chat_template serializes and validates as part of status.""" - resp = InferenceStatusResponse(chat_template = "{{ messages }}") - roundtripped = InferenceStatusResponse.model_validate_json( - resp.model_dump_json() - ) - assert roundtripped.chat_template == "{{ messages }}" - def test_roundtrip_preserves_value(self): """model_validate_json(model_dump_json()) round-trips.""" resp = LoadResponse( diff --git a/studio/backend/tests/test_openai_tool_passthrough.py b/studio/backend/tests/test_openai_tool_passthrough.py index cdb7f5d270..ccb0dba325 100644 --- a/studio/backend/tests/test_openai_tool_passthrough.py +++ b/studio/backend/tests/test_openai_tool_passthrough.py @@ -144,14 +144,13 @@ def test_tool_role_empty_tool_call_id_rejected(self): # ── Role-aware content requirements ──────────────────────────── - @pytest.mark.parametrize("role", ["user", "system"]) - def test_empty_string_content_allowed(self, role): - msg = ChatMessage(role = role, content = "") - assert msg.content == "" + def test_user_empty_content_rejected(self): + with pytest.raises(ValidationError): + ChatMessage(role = "user", content = "") - def test_user_missing_content_rejected(self): + def test_system_empty_content_rejected(self): with pytest.raises(ValidationError): - ChatMessage(role = "user") + ChatMessage(role = "system", content = "") def test_user_empty_list_content_rejected(self): with pytest.raises(ValidationError): @@ -227,14 +226,6 @@ def test_tools_parses(self): assert len(req.tools) == 1 assert req.tools[0]["function"]["name"] == "get_weather" - def test_image_base64_allows_empty_user_text(self): - req = ChatCompletionRequest( - messages = [{"role": "user", "content": ""}], - image_base64 = "aW1hZ2U=", - ) - assert req.messages[0].content == "" - assert req.image_base64 == "aW1hZ2U=" - def test_tool_choice_string_auto(self): assert self._make(tool_choice = "auto").tool_choice == "auto" diff --git a/studio/backend/tests/test_tool_policy_gates.py b/studio/backend/tests/test_tool_policy_gates.py deleted file mode 100644 index 01f6bbbc3f..0000000000 --- a/studio/backend/tests/test_tool_policy_gates.py +++ /dev/null @@ -1,56 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. - -""" -Tests for `_effective_enable_tools` -- the helper that folds the -process-level `tool_policy` over a request's `enable_tools` field. - -Truth table (policy x payload.enable_tools -> effective): - policy=None + payload=None -> None - policy=None + payload=True -> True - policy=None + payload=False -> False - policy=True + payload=* -> True - policy=False + payload=* -> False -""" - -import os -import sys -from types import SimpleNamespace - -_backend = os.path.join(os.path.dirname(__file__), "..") -sys.path.insert(0, _backend) - -import pytest - -from routes.inference import _effective_enable_tools -from state.tool_policy import reset_tool_policy, set_tool_policy - - -@pytest.fixture(autouse = True) -def _reset(): - reset_tool_policy() - yield - reset_tool_policy() - - -def _payload(value): - return SimpleNamespace(enable_tools = value) - - -class TestEffectiveEnableTools: - @pytest.mark.parametrize( - "payload_value,expected", - [(None, None), (True, True), (False, False)], - ) - def test_no_policy_falls_through_to_payload(self, payload_value, expected): - assert _effective_enable_tools(_payload(payload_value)) == expected - - @pytest.mark.parametrize("payload_value", [None, True, False]) - def test_policy_true_overrides_any_payload(self, payload_value): - set_tool_policy(True) - assert _effective_enable_tools(_payload(payload_value)) is True - - @pytest.mark.parametrize("payload_value", [None, True, False]) - def test_policy_false_overrides_any_payload(self, payload_value): - set_tool_policy(False) - assert _effective_enable_tools(_payload(payload_value)) is False diff --git a/studio/backend/tests/test_tool_policy_state.py b/studio/backend/tests/test_tool_policy_state.py deleted file mode 100644 index 5f6b228281..0000000000 --- a/studio/backend/tests/test_tool_policy_state.py +++ /dev/null @@ -1,59 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. - -""" -Tests for the process-level server-side tool policy used by `unsloth run`. - -The policy has three states: - None -> no CLI override (default; honor per-request enable_tools) - True -> CLI forced tools on - False -> CLI forced tools off -""" - -import os -import sys - -_backend = os.path.join(os.path.dirname(__file__), "..") -sys.path.insert(0, _backend) - -import pytest - -from state.tool_policy import ( - get_tool_policy, - reset_tool_policy, - set_tool_policy, -) - - -@pytest.fixture(autouse = True) -def _reset(): - reset_tool_policy() - yield - reset_tool_policy() - - -class TestToolPolicy: - def test_default_is_none(self): - assert get_tool_policy() is None - - def test_set_true_then_get(self): - set_tool_policy(True) - assert get_tool_policy() is True - - def test_set_false_then_get(self): - set_tool_policy(False) - assert get_tool_policy() is False - - def test_set_none_clears(self): - set_tool_policy(True) - set_tool_policy(None) - assert get_tool_policy() is None - - def test_reset_clears(self): - set_tool_policy(False) - reset_tool_policy() - assert get_tool_policy() is None - - def test_rejects_non_optional_bool(self): - with pytest.raises(TypeError): - set_tool_policy("true") # type: ignore[arg-type] diff --git a/studio/backend/tests/test_training_raw_support.py b/studio/backend/tests/test_training_raw_support.py deleted file mode 100644 index 876ee34686..0000000000 --- a/studio/backend/tests/test_training_raw_support.py +++ /dev/null @@ -1,183 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 - -import asyncio -import importlib.util -import unittest -from pathlib import Path -from unittest.mock import patch - -from datasets import Dataset - -from core.training.training import TrainingBackend -from models.training import TrainingStartRequest -from utils.datasets import format_dataset, format_and_template_dataset -from utils.datasets.raw_text import prepare_raw_text_dataset - -_BACKEND_ROOT = Path(__file__).resolve().parent.parent - - -def _load_route_module(name: str, relative_path: str): - spec = importlib.util.spec_from_file_location(name, _BACKEND_ROOT / relative_path) - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - return module - - -class TestTrainingRawSupport(unittest.TestCase): - def test_training_backend_preserves_cpt_4bit_and_embedding_lr(self): - backend = TrainingBackend() - - class DummyProcess: - pid = 12345 - - def start(self): - return None - - class DummyThread: - def start(self): - return None - - dummy_queue = object() - - with ( - patch( - "core.training.training.prepare_gpu_selection", - return_value = ([0], {"selection_mode": "auto"}), - ), - patch( - "core.training.training._CTX.Queue", - side_effect = [dummy_queue, dummy_queue], - ), - patch( - "core.training.training._CTX.Process", return_value = DummyProcess() - ) as mock_process, - patch( - "core.training.training.threading.Thread", - return_value = DummyThread(), - ), - ): - backend.start_training( - job_id = "test-cpt-raw", - model_name = "unsloth/test-bnb-4bit", - training_type = "Continued Pretraining", - format_type = "raw", - load_in_4bit = True, - embedding_learning_rate = 1e-5, - ) - - config = mock_process.call_args.kwargs["kwargs"]["config"] - self.assertTrue(config["load_in_4bit"]) - self.assertEqual(config["embedding_learning_rate"], 1e-5) - - def test_training_route_forwards_embedding_learning_rate(self): - training_route = _load_route_module( - "training_route_module_raw_support", - "routes/training.py", - ) - captured: dict = {} - - class DummyBackend: - current_job_id = None - - def is_training_active(self): - return False - - def start_training(self, **kwargs): - captured.update(kwargs) - return True - - request = TrainingStartRequest( - model_name = "unsloth/test-bnb-4bit", - training_type = "Continued Pretraining", - format_type = "raw", - load_in_4bit = True, - embedding_learning_rate = 1e-5, - ) - - with ( - patch.object( - training_route, - "get_training_backend", - return_value = DummyBackend(), - ), - patch.object(training_route, "load_model_defaults", return_value = {}), - patch( - "core.inference.get_inference_backend", - return_value = type( - "InferenceBackend", - (), - {"active_model_name": None}, - )(), - ), - patch( - "core.export.get_export_backend", - return_value = type( - "ExportBackend", - (), - {"current_checkpoint": None}, - )(), - ), - ): - response = asyncio.run( - training_route.start_training(request, current_subject = "test-user") - ) - - self.assertEqual(response.status, "queued") - self.assertEqual(captured["embedding_learning_rate"], 1e-5) - self.assertTrue(captured["load_in_4bit"]) - - def test_format_dataset_supports_raw_text(self): - dataset = Dataset.from_dict( - { - "body": ["hello", "world"], - "title": ["a", "b"], - "id": [1, 2], - } - ) - - result = format_dataset(dataset, format_type = "raw") - - self.assertEqual(result["final_format"], "raw_text") - self.assertIn("text", result["dataset"].column_names) - self.assertEqual(result["dataset"][0]["text"], "hello") - self.assertFalse(result["requires_manual_mapping"]) - - def test_format_and_template_dataset_supports_raw_text_without_template(self): - dataset = Dataset.from_dict({"body": ["hello raw world"]}) - - result = format_and_template_dataset( - dataset, - model_name = "unsloth/test", - tokenizer = None, - format_type = "raw", - ) - - self.assertTrue(result["success"]) - self.assertEqual(result["final_format"], "raw_text") - self.assertEqual(result["dataset"][0]["text"], "hello raw world") - - def test_prepare_raw_text_dataset_drops_null_rows_before_appending_eos(self): - dataset = Dataset.from_dict({"text": ["hello", None, "world"]}) - - result = prepare_raw_text_dataset( - dataset, - mode_label = "CPT", - split_name = "train", - eos_token = "", - append_eos = True, - ) - - self.assertEqual(len(result.dataset), 2) - self.assertEqual(result.dataset[0]["text"], "hello") - self.assertEqual(result.dataset[1]["text"], "world") - self.assertTrue( - any( - "null or non-string 'text' values" in notice.message - for notice in result.notices - ) - ) - - -if __name__ == "__main__": - unittest.main() diff --git a/studio/backend/tests/test_training_worker_flash_attn.py b/studio/backend/tests/test_training_worker_flash_attn.py index 41a7c87df1..986958408e 100644 --- a/studio/backend/tests/test_training_worker_flash_attn.py +++ b/studio/backend/tests/test_training_worker_flash_attn.py @@ -133,22 +133,6 @@ def test_causal_conv1d_fast_path_preserves_wheel_first_install_args(monkeypatch) ) -def test_causal_conv1d_fast_path_includes_qwen3_6_variants(monkeypatch): - install_mock = mock.Mock(return_value = True) - monkeypatch.setattr(worker, "_install_package_wheel_first", install_mock) - - worker._ensure_causal_conv1d_fast_path( - event_queue = [], - model_name = "unsloth/Qwen3.6-4B", - ) - worker._ensure_causal_conv1d_fast_path( - event_queue = [], - model_name = "unsloth/Qwen3_6-4B", - ) - - assert install_mock.call_count == 2 - - def test_mamba_ssm_path_preserves_wheel_first_install_args(monkeypatch): install_mock = mock.Mock(return_value = True) monkeypatch.setattr(worker, "_install_package_wheel_first", install_mock) diff --git a/studio/backend/tests/test_vision_cache.py b/studio/backend/tests/test_vision_cache.py index 9e7bbdd1fb..fae1e95311 100644 --- a/studio/backend/tests/test_vision_cache.py +++ b/studio/backend/tests/test_vision_cache.py @@ -124,50 +124,23 @@ def test_subprocess_called_once_with_cache(self, mock_needs_t5, mock_subprocess) class TestVisionCacheOnException: - """When detection raises an exception, _is_vision_model_uncached - distinguishes permanent failures (cached as False) from transient - failures (returned as None, not cached so the next call can retry). - Verify both contracts.""" - - @patch( - "utils.models.model_config.load_model_config", - side_effect = ValueError("bad config"), - ) - @patch("utils.transformers_version.needs_transformers_5", return_value = False) - def test_permanent_exception_result_cached(self, mock_needs_t5, mock_load_config): - """A permanent failure (ValueError / RepositoryNotFoundError / - GatedRepoError / JSONDecodeError) should be caught, return False, - and that False should be cached so subsequent calls don't retry. - - ValueError is used here because it's the simplest of the - code-path's cacheable exception types and does not require an - import of huggingface_hub errors (whose module path varies - across versions).""" - # First call: load_model_config raises -> except branch -> False. - assert is_vision_model("broken/model") is False - # Second call: cache hit, load_model_config not called again. - assert is_vision_model("broken/model") is False - mock_load_config.assert_called_once() + """When detection raises an exception, _is_vision_model_uncached catches + it and returns False. That False must be cached so subsequent calls don't + retry and fail again.""" @patch( "utils.models.model_config.load_model_config", side_effect = OSError("network down"), ) @patch("utils.transformers_version.needs_transformers_5", return_value = False) - def test_transient_exception_not_cached(self, mock_needs_t5, mock_load_config): - """A transient failure (OSError, timeouts) should return None from - _is_vision_model_uncached, surface as False to the caller, and - NOT be cached, so the next call retries detection. This matches - the documented behaviour on _vision_detection_cache: - 'transient failures (network errors, timeouts) are NOT cached so - they can be retried.'""" - # First call: load_model_config raises OSError -> uncached None - # -> caller returns False without caching. + def test_exception_result_cached(self, mock_needs_t5, mock_load_config): + """A real exception inside _is_vision_model_uncached should be caught, + return False, and that False should be cached for subsequent calls.""" + # First call: load_model_config raises → except branch → False assert is_vision_model("broken/model") is False - # Second call: cache miss again, load_model_config called a - # second time. + # Second call: cache hit, load_model_config not called again assert is_vision_model("broken/model") is False - assert mock_load_config.call_count == 2 + mock_load_config.assert_called_once() # --------------------------------------------------------------------------- diff --git a/studio/backend/tests/test_vram_estimation.py b/studio/backend/tests/test_vram_estimation.py index e54ae6dcf8..0be067310d 100644 --- a/studio/backend/tests/test_vram_estimation.py +++ b/studio/backend/tests/test_vram_estimation.py @@ -2,9 +2,7 @@ # Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. import unittest -from dataclasses import replace from types import SimpleNamespace -from unittest.mock import patch from utils.hardware.vram_estimation import ( ModelArchConfig, @@ -118,55 +116,6 @@ def _gb(b: int) -> float: num_dense_layers = 0, ) -STRUCTURED_MIXED = ModelArchConfig( - hidden_size = 256, - num_hidden_layers = 6, - num_attention_heads = 4, - num_key_value_heads = 2, - intermediate_size = 512, - vocab_size = 1024, - tie_word_embeddings = True, - head_dim = 80, - global_head_dim = 96, - num_global_key_value_heads = 1, - attention_k_eq_v = True, - layer_types = [ - "sliding_attention", - "full_attention", - "sliding_attention", - "full_attention", - "sliding_attention", - "full_attention", - ], -) - -STRUCTURED_SHARED = ModelArchConfig( - hidden_size = 192, - num_hidden_layers = 4, - num_attention_heads = 6, - num_key_value_heads = 2, - intermediate_size = 384, - vocab_size = 512, - tie_word_embeddings = True, - head_dim = 32, - num_kv_shared_layers = 2, - use_double_wide_mlp = True, - vocab_size_per_layer_input = 128, - hidden_size_per_layer_input = 48, - quant_4bit_factor = 3.6, -) - -QUANT_SKIP_STRUCTURED = replace( - STRUCTURED_SHARED, - quantization_skip_modules = [ - "model.layers.0.self_attn.q_proj", - "language_model.model.layers.1.mlp", - "layers.2", - "vision_tower", - "embed_tokens", - ], -) - class TestExtractArchConfig(unittest.TestCase): def test_basic_config(self): @@ -233,42 +182,6 @@ def test_intermediate_size_list(self): arch = extract_arch_config(hf_config) self.assertEqual(arch.intermediate_size, 8192) - def test_structural_and_quantization_fields_are_config_derived(self): - hf_config = SimpleNamespace( - hidden_size = 256, - num_hidden_layers = 2, - num_attention_heads = 4, - num_key_value_heads = 2, - intermediate_size = 512, - vocab_size = 1024, - tie_word_embeddings = True, - head_dim = 80, - global_head_dim = 96, - num_global_key_value_heads = 1, - attention_k_eq_v = True, - layer_types = ["sliding_attention", "full_attention"], - num_kv_shared_layers = 1, - use_double_wide_mlp = True, - vocab_size_per_layer_input = 128, - hidden_size_per_layer_input = 48, - quantization_config = { - "bnb_4bit_use_double_quant": True, - "llm_int8_skip_modules": ["model.layers.0.self_attn"], - }, - ) - arch = extract_arch_config(hf_config) - self.assertEqual(arch.head_dim, 80) - self.assertEqual(arch.global_head_dim, 96) - self.assertEqual(arch.num_global_key_value_heads, 1) - self.assertTrue(arch.attention_k_eq_v) - self.assertEqual(arch.layer_types, ["sliding_attention", "full_attention"]) - self.assertEqual(arch.num_kv_shared_layers, 1) - self.assertTrue(arch.use_double_wide_mlp) - self.assertEqual(arch.vocab_size_per_layer_input, 128) - self.assertEqual(arch.hidden_size_per_layer_input, 48) - self.assertEqual(arch.quantization_skip_modules, ["model.layers.0.self_attn"]) - self.assertEqual(arch.quant_4bit_factor, 3.6) - class TestModelWeightsBytes(unittest.TestCase): def test_llama_8b_fp16(self): @@ -325,18 +238,6 @@ def test_moe_mlp_modules_scale_with_experts(self): ratio = moe_lora / dense_lora self.assertAlmostEqual(ratio, 8.0, delta = 0.5) - def test_structured_moe_mlp_modules_scale_with_experts(self): - structured_moe = replace(QWEN3_MOE_30B, head_dim = 128) - dense_like = replace( - structured_moe, - num_experts = None, - moe_intermediate_size = None, - ) - target_modules = ["gate_proj", "up_proj", "down_proj"] - dense_lora = compute_lora_params(dense_like, 16, target_modules) - moe_lora = compute_lora_params(structured_moe, 16, target_modules) - self.assertGreater(moe_lora, dense_lora * 20) - def test_attention_modules_same_for_moe(self): dense_attn = compute_lora_params( LLAMA_8B, 16, ["q_proj", "k_proj", "v_proj", "o_proj"] @@ -346,41 +247,6 @@ def test_attention_modules_same_for_moe(self): ) self.assertEqual(dense_attn, moe_attn) - def test_all_linear_uses_default_text_modules(self): - text_only = compute_lora_params(STRUCTURED_MIXED, 16, DEFAULT_TARGET_MODULES) - all_linear = compute_lora_params(STRUCTURED_MIXED, 16, ["all-linear"]) - self.assertEqual(all_linear, text_only) - - def test_structural_layer_shapes_are_config_driven(self): - unstructured_arch = replace( - STRUCTURED_MIXED, - head_dim = None, - global_head_dim = None, - num_global_key_value_heads = None, - attention_k_eq_v = False, - layer_types = None, - ) - self.assertNotEqual( - compute_lora_params(unstructured_arch, 16, ["all-linear"]), - compute_lora_params(STRUCTURED_MIXED, 16, ["all-linear"]), - ) - self.assertNotEqual( - compute_model_weights_bytes(unstructured_arch, "qlora", True), - compute_model_weights_bytes(STRUCTURED_MIXED, "qlora", True), - ) - - def test_shared_kv_and_per_layer_inputs_change_weight_count(self): - unstructured_arch = replace( - STRUCTURED_SHARED, - head_dim = None, - num_kv_shared_layers = 0, - use_double_wide_mlp = False, - ) - self.assertNotEqual( - compute_model_weights_bytes(unstructured_arch, "qlora", True), - compute_model_weights_bytes(STRUCTURED_SHARED, "qlora", True), - ) - class TestOptimizerBytes(unittest.TestCase): def test_adamw_8bit(self): @@ -427,163 +293,6 @@ def test_scales_with_seq_len(self): act_4k = compute_activation_bytes(LLAMA_8B, 2, 4096, "unsloth") self.assertAlmostEqual(act_4k / act_2k, 2.0, delta = 0.1) - def test_flash_attention_uses_linear_path(self): - flash = compute_activation_bytes( - STRUCTURED_MIXED, - 1, - 4096, - "unsloth", - is_lora = True, - attention_implementation = "flash_attention_2", - ) - default = compute_activation_bytes( - STRUCTURED_MIXED, - 1, - 4096, - "unsloth", - is_lora = True, - ) - self.assertEqual(flash, default) - - def test_sdpa_attention_uses_linear_path(self): - flash = compute_activation_bytes( - STRUCTURED_MIXED, - 1, - 4096, - "unsloth", - is_lora = True, - attention_implementation = "flash_attention_2", - ) - sdpa = compute_activation_bytes( - STRUCTURED_MIXED, - 1, - 4096, - "unsloth", - is_lora = True, - attention_implementation = "sdpa", - ) - self.assertEqual(sdpa, flash) - - def test_non_flash_attention_uses_quadratic_path(self): - seq_len = 4096 - expected_quadratic = ( - 1 * STRUCTURED_MIXED.num_attention_heads * seq_len * seq_len * 2 * 12.0 - ) - for attention_implementation in ("eager", "unknown_impl", None): - with self.subTest(attention_implementation = attention_implementation): - non_flash = compute_activation_bytes( - STRUCTURED_MIXED, - 1, - seq_len, - "unsloth", - is_lora = True, - attention_implementation = attention_implementation, - ) - self.assertEqual(non_flash, int(expected_quadratic)) - - def test_non_flash_attention_without_gc_scales_quadratic_path_by_layers(self): - seq_len = 4096 - one_layer = ( - 1 * STRUCTURED_MIXED.num_attention_heads * seq_len * seq_len * 2 * 12.0 - ) - non_flash = compute_activation_bytes( - STRUCTURED_MIXED, - 1, - seq_len, - "none", - is_lora = True, - attention_implementation = "eager", - ) - self.assertEqual(non_flash, int(one_layer * STRUCTURED_MIXED.num_hidden_layers)) - self.assertGreater(non_flash, int(one_layer)) - - -class TestQuantizationSkips(unittest.TestCase): - def test_skipped_language_layers_stay_fp16(self): - no_skips = replace(QUANT_SKIP_STRUCTURED, quantization_skip_modules = []) - skipped = compute_model_weights_bytes(QUANT_SKIP_STRUCTURED, "qlora", True) - quantized = compute_model_weights_bytes(no_skips, "qlora", True) - self.assertGreater(skipped, quantized) - - def test_non_language_skips_do_not_double_count_text_weights(self): - arch = replace( - QUANT_SKIP_STRUCTURED, - quantization_skip_modules = ["vision_tower", "embed_tokens"], - ) - no_skips = replace(QUANT_SKIP_STRUCTURED, quantization_skip_modules = []) - self.assertEqual( - compute_model_weights_bytes(arch, "qlora", True), - compute_model_weights_bytes(no_skips, "qlora", True), - ) - - def test_double_quant_factor_reduces_quantized_weight_storage(self): - default_quant = replace(STRUCTURED_MIXED, quant_4bit_factor = 16 / 5) - double_quant = replace(STRUCTURED_MIXED, quant_4bit_factor = 3.6) - self.assertLess( - compute_model_weights_bytes(double_quant, "qlora", True), - compute_model_weights_bytes(default_quant, "qlora", True), - ) - - def test_prefixed_parent_and_child_skips_do_not_double_count(self): - parent_only = replace( - QUANT_SKIP_STRUCTURED, - quantization_skip_modules = ["language_model.model.layers.1.mlp"], - ) - parent_and_child = replace( - QUANT_SKIP_STRUCTURED, - quantization_skip_modules = [ - "language_model.model.layers.1.mlp", - "language_model.model.layers.1.mlp.gate_proj", - "model.layers.1.mlp.up_proj", - ], - ) - self.assertEqual( - compute_model_weights_bytes(parent_and_child, "qlora", True), - compute_model_weights_bytes(parent_only, "qlora", True), - ) - - def test_vlm_prefix_skip_module_does_not_match_text_alias(self): - # vision_tower-prefixed skips must not shadow text aliases sharing the - # same suffix. - baseline = replace(QUANT_SKIP_STRUCTURED, quantization_skip_modules = []) - vlm_skip = replace( - QUANT_SKIP_STRUCTURED, - quantization_skip_modules = [ - "vision_tower.model.layers.0.self_attn.q_proj", - "vision_tower.model.layers.1.mlp", - ], - ) - self.assertEqual( - compute_model_weights_bytes(vlm_skip, "qlora", True), - compute_model_weights_bytes(baseline, "qlora", True), - ) - - def test_mla_skip_module_uses_authoritative_attn_total(self): - from utils.hardware.vram_estimation import ( - _build_text_module_elements, - _compute_attn_elements, - ) - - mla = ModelArchConfig( - hidden_size = 2048, - num_hidden_layers = 4, - num_attention_heads = 16, - num_key_value_heads = 16, - intermediate_size = 8192, - vocab_size = 32000, - tie_word_embeddings = False, - q_lora_rank = 512, - kv_lora_rank = 128, - qk_nope_head_dim = 64, - qk_rope_head_dim = 32, - v_head_dim = 64, - ) - elements, _ = _build_text_module_elements(mla) - self.assertEqual( - elements["text.layers.0.self_attn"], - _compute_attn_elements(mla), - ) - class TestEstimateTrainingVram(unittest.TestCase): def test_llama_8b_qlora_reasonable_total(self): @@ -721,90 +430,6 @@ def test_adamw_fp32_uses_more_optimizer_memory(self): v32.optimizer_states / v8.optimizer_states, 1.5, delta = 0.1 ) - def test_min_gpu_vram_treats_activations_as_per_gpu_fixed(self): - config = TrainingVramConfig(training_method = "qlora", load_in_4bit = True) - breakdown = estimate_training_vram(LLAMA_8B, config) - shardable = ( - breakdown.model_weights - + breakdown.lora_adapters - + breakdown.optimizer_states - + breakdown.gradients - ) - per_gpu_fixed = breakdown.activations + breakdown.cuda_overhead - for n_gpus in (1, 2, 4): - self.assertEqual( - breakdown.min_gpu_vram(n_gpus), - shardable // n_gpus + per_gpu_fixed, - ) - - def test_qlora_gradient_floor_is_capped_by_trainable_scale(self): - config = TrainingVramConfig( - training_method = "qlora", - batch_size = 1, - max_seq_length = 512, - lora_rank = 16, - target_modules = ["all-linear"], - gradient_checkpointing = "unsloth", - optimizer = "adamw_8bit", - load_in_4bit = True, - ) - breakdown = estimate_training_vram(LLAMA_8B, config) - lora_params = compute_lora_params(LLAMA_8B, 16, DEFAULT_TARGET_MODULES) - optimizer_bytes = compute_optimizer_bytes(lora_params, "adamw_8bit") - weight_floor = int(breakdown.model_weights * 0.15) - - self.assertEqual( - breakdown.gradients, - max(breakdown.activations_computed, optimizer_bytes), - ) - self.assertLess(breakdown.gradients, weight_floor) - self.assertEqual(breakdown.activations, breakdown.activations_computed) - - def test_full_finetuning_gradient_floor_remains_uncapped(self): - config = TrainingVramConfig( - training_method = "full", - batch_size = 1, - max_seq_length = 512, - gradient_checkpointing = "unsloth", - optimizer = "adamw_8bit", - load_in_4bit = False, - ) - expected_floor = int( - compute_model_weights_bytes(LLAMA_8B, "full", False) * 0.15 - ) - with patch( - "utils.hardware.vram_estimation.compute_gradient_bytes", - return_value = 1, - ): - breakdown = estimate_training_vram(LLAMA_8B, config) - self.assertEqual(breakdown.gradients, expected_floor) - - def test_non_flash_attention_flows_into_training_estimate(self): - config = TrainingVramConfig( - training_method = "qlora", - batch_size = 1, - max_seq_length = 4096, - lora_rank = 16, - target_modules = ["all-linear"], - gradient_checkpointing = "unsloth", - optimizer = "adamw_8bit", - load_in_4bit = True, - attention_implementation = "eager", - ) - breakdown = estimate_training_vram(STRUCTURED_MIXED, config) - self.assertEqual(breakdown.activations, breakdown.activations_computed) - self.assertGreater( - breakdown.activations, - compute_activation_bytes( - STRUCTURED_MIXED, - 1, - 4096, - "unsloth", - is_lora = True, - attention_implementation = "flash_attention_2", - ), - ) - class TestExtractArchConfigMoE(unittest.TestCase): def test_deepseek_v3_shared_experts(self): @@ -846,16 +471,11 @@ def test_qwen3_moe_decoder_sparse_step(self): moe_intermediate_size = 768, decoder_sparse_step = 1, mlp_only_layers = [], - head_dim = 128, ) arch = extract_arch_config(hf_config) self.assertEqual(arch.num_experts, 128) self.assertEqual(arch.num_dense_layers, 0) - self.assertEqual(arch.head_dim, 128) self.assertIsNone(arch.q_lora_rank) - total_b = compute_total_params(arch) / 1e9 - self.assertGreater(total_b, 20) - self.assertLess(total_b, 50) def test_qwen3_moe_with_mlp_only_layers(self): hf_config = SimpleNamespace( @@ -922,343 +542,6 @@ def test_backward_compat_no_new_fields(self): self.assertEqual(arch.n_shared_experts, 0) self.assertEqual(arch.num_dense_layers, 0) self.assertIsNone(arch.q_lora_rank) - self.assertFalse(arch.moe_has_dense_mlp) - - def test_enable_moe_block_extracted_as_moe_has_dense_mlp(self): - hf_config = SimpleNamespace( - hidden_size = 2048, - num_hidden_layers = 8, - num_attention_heads = 16, - num_key_value_heads = 4, - intermediate_size = 4096, - vocab_size = 32000, - tie_word_embeddings = True, - num_experts = 8, - moe_intermediate_size = 1024, - head_dim = 128, - layer_types = ["full_attention"] * 8, - enable_moe_block = True, - ) - arch = extract_arch_config(hf_config) - self.assertTrue(arch.moe_has_dense_mlp) - - -class TestParallelDenseMoE(unittest.TestCase): - def _arch(self, **overrides): - base = ModelArchConfig( - hidden_size = 512, - num_hidden_layers = 4, - num_attention_heads = 8, - num_key_value_heads = 2, - intermediate_size = 1024, - vocab_size = 1024, - tie_word_embeddings = True, - num_experts = 8, - moe_intermediate_size = 512, - num_dense_layers = 0, - head_dim = 64, - layer_types = ["full_attention"] * 4, - ) - return replace(base, **overrides) - - def test_total_params_includes_parallel_dense_when_enable_moe_block(self): - without_parallel = self._arch(moe_has_dense_mlp = False) - with_parallel = self._arch(moe_has_dense_mlp = True) - self.assertGreater( - compute_total_params(with_parallel), - compute_total_params(without_parallel), - ) - - def test_lora_params_includes_parallel_dense_when_enable_moe_block(self): - without_parallel = self._arch(moe_has_dense_mlp = False) - with_parallel = self._arch(moe_has_dense_mlp = True) - target = ["gate_proj", "up_proj", "down_proj"] - self.assertGreater( - compute_lora_params(with_parallel, 16, target), - compute_lora_params(without_parallel, 16, target), - ) - - def test_activation_bytes_includes_parallel_dense_when_enable_moe_block(self): - without_parallel = self._arch(moe_has_dense_mlp = False) - with_parallel = self._arch(moe_has_dense_mlp = True) - self.assertGreater( - compute_activation_bytes( - with_parallel, - 1, - 2048, - "unsloth", - is_lora = True, - ), - compute_activation_bytes( - without_parallel, - 1, - 2048, - "unsloth", - is_lora = True, - ), - ) - - def test_layer_aggregates_split_dense_mlp_from_experts(self): - from utils.hardware.vram_estimation import _build_text_module_elements - - with_parallel = self._arch(moe_has_dense_mlp = True) - elements, _ = _build_text_module_elements(with_parallel) - moe_only = ( - with_parallel.hidden_size - * with_parallel.moe_intermediate_size - * 3 - * with_parallel.num_experts - + with_parallel.num_experts * with_parallel.hidden_size - ) - dense_only = with_parallel.hidden_size * with_parallel.intermediate_size * 3 - # why: under gemma4 enable_moe_block, the layer's `self.experts` is a - # sibling of `self.mlp`; the `text.layers..mlp` aggregate must - # cover the dense path only, with experts in their own aggregate. - self.assertEqual(elements["text.layers.0.mlp"], dense_only) - self.assertEqual(elements["text.layers.0.experts"], moe_only) - - -class TestDenseLayerIndices(unittest.TestCase): - def test_non_prefix_mlp_only_layers_preserve_position(self): - hf_config = SimpleNamespace( - hidden_size = 1024, - num_hidden_layers = 8, - num_attention_heads = 16, - num_key_value_heads = 4, - intermediate_size = 2048, - vocab_size = 32000, - tie_word_embeddings = True, - num_local_experts = 4, - moe_intermediate_size = 512, - decoder_sparse_step = 1, - mlp_only_layers = [3, 5], - ) - arch = extract_arch_config(hf_config) - self.assertEqual(arch.num_dense_layers, 2) - self.assertIn(3, arch.dense_layer_indices) - self.assertIn(5, arch.dense_layer_indices) - self.assertNotIn(0, arch.dense_layer_indices) - - def test_first_k_dense_replace_indices_are_prefix(self): - hf_config = SimpleNamespace( - hidden_size = 1024, - num_hidden_layers = 6, - num_attention_heads = 16, - num_key_value_heads = 4, - intermediate_size = 2048, - vocab_size = 32000, - tie_word_embeddings = False, - n_routed_experts = 8, - moe_intermediate_size = 512, - first_k_dense_replace = 2, - ) - arch = extract_arch_config(hf_config) - self.assertEqual(tuple(arch.dense_layer_indices), (0, 1)) - - -class TestKvSharedLayer(unittest.TestCase): - def test_fully_shared_kv_returns_false_matching_upstream(self): - from utils.hardware.vram_estimation import _is_kv_shared_layer - - arch = ModelArchConfig( - hidden_size = 512, - num_hidden_layers = 4, - num_attention_heads = 8, - num_key_value_heads = 2, - intermediate_size = 1024, - vocab_size = 1024, - num_kv_shared_layers = 4, - ) - for i in range(arch.num_hidden_layers): - self.assertFalse(_is_kv_shared_layer(arch, i)) - - def test_partial_share_returns_true_for_tail_layers(self): - from utils.hardware.vram_estimation import _is_kv_shared_layer - - arch = ModelArchConfig( - hidden_size = 512, - num_hidden_layers = 4, - num_attention_heads = 8, - num_key_value_heads = 2, - intermediate_size = 1024, - vocab_size = 1024, - num_kv_shared_layers = 2, - ) - self.assertFalse(_is_kv_shared_layer(arch, 0)) - self.assertFalse(_is_kv_shared_layer(arch, 1)) - self.assertTrue(_is_kv_shared_layer(arch, 2)) - self.assertTrue(_is_kv_shared_layer(arch, 3)) - - -class TestFlexAttentionLinear(unittest.TestCase): - def test_flex_attention_treated_as_linear(self): - flash = compute_activation_bytes( - STRUCTURED_MIXED, - 1, - 4096, - "unsloth", - is_lora = True, - attention_implementation = "flash_attention_2", - ) - flex = compute_activation_bytes( - STRUCTURED_MIXED, - 1, - 4096, - "unsloth", - is_lora = True, - attention_implementation = "flex_attention", - ) - self.assertEqual(flex, flash) - - -class TestNonStructuredParallelDense(unittest.TestCase): - def _arch(self, **overrides): - base = ModelArchConfig( - hidden_size = 1024, - num_hidden_layers = 4, - num_attention_heads = 16, - num_key_value_heads = 4, - intermediate_size = 4096, - vocab_size = 32000, - tie_word_embeddings = False, - num_experts = 8, - moe_intermediate_size = 768, - num_dense_layers = 0, - moe_has_dense_mlp = True, - ) - return replace(base, **overrides) - - def test_skip_module_uses_intermediate_size_for_parallel_dense(self): - from utils.hardware.vram_estimation import _build_text_module_elements - - arch = self._arch() - elements, _ = _build_text_module_elements(arch) - gate_proj = elements["text.layers.0.mlp.gate_proj"] - self.assertEqual(gate_proj, arch.hidden_size * arch.intermediate_size) - - -class TestPerLayerInputAccounting(unittest.TestCase): - def _arch(self, **overrides): - base = ModelArchConfig( - hidden_size = 1024, - num_hidden_layers = 4, - num_attention_heads = 16, - num_key_value_heads = 4, - intermediate_size = 2048, - vocab_size = 32000, - tie_word_embeddings = False, - head_dim = 64, - layer_types = ["full_attention"] * 4, - vocab_size_per_layer_input = 256, - hidden_size_per_layer_input = 96, - ) - return replace(base, **overrides) - - def test_per_layer_input_increases_total_params(self): - with_ple = self._arch() - without_ple = replace(with_ple, hidden_size_per_layer_input = 0) - self.assertGreater( - compute_total_params(with_ple), - compute_total_params(without_ple), - ) - - def test_per_layer_input_modules_count_quantizable_block(self): - with_ple = self._arch() - without_ple = replace(with_ple, hidden_size_per_layer_input = 0) - # The PLE block adds: model_projection (hd*nl*pli), per_layer_input_gate - # (hd*pli per layer) + per_layer_projection (pli*hd per layer) as - # quantizable text linears. - n_layers = with_ple.num_hidden_layers - hd = with_ple.hidden_size - pli = with_ple.hidden_size_per_layer_input - expected_quantizable_extra = ( - hd * (n_layers * pli) + (hd * pli) * n_layers + (pli * hd) * n_layers - ) - delta = compute_total_params(with_ple) - compute_total_params(without_ple) - self.assertGreaterEqual(delta, expected_quantizable_extra) - - def test_all_linear_lora_excludes_per_layer_input_modules(self): - # why: Unsloth's get_peft_regex requires module names to contain a - # component tag (mlp/attn/...); PLE module names (per_layer_input_gate, - # per_layer_projection, per_layer_model_projection) lack any tag, so - # all-linear training does NOT attach LoRA to them. - arch = self._arch() - without_ple = replace(arch, hidden_size_per_layer_input = 0) - self.assertEqual( - compute_lora_params(arch, 16, ["all-linear"]), - compute_lora_params(without_ple, 16, ["all-linear"]), - ) - - def test_explicit_target_modules_does_not_add_per_layer_input(self): - arch = self._arch() - without_ple = replace(arch, hidden_size_per_layer_input = 0) - self.assertEqual( - compute_lora_params(arch, 16, ["q_proj", "v_proj"]), - compute_lora_params(without_ple, 16, ["q_proj", "v_proj"]), - ) - - -class TestDenseMlpLayerFallback(unittest.TestCase): - def test_falls_back_to_count_when_indices_empty(self): - from utils.hardware.vram_estimation import _is_dense_mlp_layer - - arch = ModelArchConfig( - hidden_size = 512, - num_hidden_layers = 4, - num_attention_heads = 8, - num_key_value_heads = 2, - intermediate_size = 1024, - vocab_size = 1024, - num_experts = 4, - moe_intermediate_size = 256, - num_dense_layers = 2, - ) - self.assertTrue(_is_dense_mlp_layer(arch, 0)) - self.assertTrue(_is_dense_mlp_layer(arch, 1)) - self.assertFalse(_is_dense_mlp_layer(arch, 2)) - self.assertFalse(_is_dense_mlp_layer(arch, 3)) - - -class TestExpertsSkipGranularity(unittest.TestCase): - def _arch(self): - return ModelArchConfig( - hidden_size = 512, - num_hidden_layers = 4, - num_attention_heads = 8, - num_key_value_heads = 2, - intermediate_size = 1024, - vocab_size = 1024, - tie_word_embeddings = True, - num_experts = 8, - moe_intermediate_size = 512, - num_dense_layers = 0, - head_dim = 64, - layer_types = ["full_attention"] * 4, - moe_has_dense_mlp = True, - ) - - def test_experts_skip_excludes_parallel_dense_projections(self): - no_skip = self._arch() - skip_experts = replace( - no_skip, - quantization_skip_modules = ["model.layers.0.mlp.experts"], - ) - skip_full_mlp = replace( - no_skip, - quantization_skip_modules = ["model.layers.0.mlp"], - ) - bytes_no_skip = compute_model_weights_bytes(no_skip, "qlora", True) - bytes_skip_experts = compute_model_weights_bytes(skip_experts, "qlora", True) - bytes_skip_mlp = compute_model_weights_bytes(skip_full_mlp, "qlora", True) - # why: under gemma4 enable_moe_block, `self.experts` is a sibling of - # `self.mlp`; skipping `model.layers.0.mlp` should cover only the - # dense MLP, while `model.layers.0.mlp.experts` covers the routed - # experts. Routed experts have far more params than the dense MLP, - # so skipping experts must add more bytes than skipping the dense - # path. - self.assertGreater(bytes_skip_experts, bytes_no_skip) - self.assertGreater(bytes_skip_mlp, bytes_no_skip) - self.assertGreater(bytes_skip_experts, bytes_skip_mlp) class TestSharedExperts(unittest.TestCase): @@ -1325,16 +608,6 @@ def test_mla_lora_produces_values(self): lora_p = compute_lora_params(DEEPSEEK_V3, 16, ["q_proj", "v_proj", "o_proj"]) self.assertGreater(lora_p, 0) - def test_mla_with_head_dim_does_not_route_through_structured(self): - from utils.hardware.vram_estimation import _uses_structured_layer_shapes - - mla_with_head_dim = replace(DEEPSEEK_V3, head_dim = 128) - self.assertFalse(_uses_structured_layer_shapes(mla_with_head_dim)) - self.assertEqual( - compute_lora_params(DEEPSEEK_V3, 16, ["q_proj", "v_proj", "o_proj"]), - compute_lora_params(mla_with_head_dim, 16, ["q_proj", "v_proj", "o_proj"]), - ) - class TestDenseMoEMix(unittest.TestCase): def test_dense_layers_change_total(self): @@ -1418,952 +691,5 @@ def test_lora_dense_vs_moe_layers_differ(self): self.assertNotEqual(lora_all, lora_mix) -class TestMlpLayerTypesDispatch(unittest.TestCase): - def _hf(self, **fields): - text_config = SimpleNamespace( - hidden_size = 64, - num_hidden_layers = 4, - num_attention_heads = 4, - num_key_value_heads = 4, - intermediate_size = 128, - vocab_size = 1000, - tie_word_embeddings = True, - num_local_experts = 4, - moe_intermediate_size = 32, - **fields, - ) - return SimpleNamespace(text_config = text_config, quantization_config = {}) - - def test_mlp_layer_types_drives_dense_indices(self): - hf = self._hf(mlp_layer_types = ["sparse", "dense", "sparse", "dense"]) - arch = extract_arch_config(hf) - self.assertIsNotNone(arch) - self.assertEqual(arch.dense_layer_indices, (1, 3)) - self.assertEqual(arch.num_dense_layers, 2) - - def test_mlp_layer_types_takes_priority_over_first_k_dense_replace(self): - hf = self._hf( - mlp_layer_types = ["dense", "sparse", "dense", "sparse"], - first_k_dense_replace = 3, - ) - arch = extract_arch_config(hf) - self.assertEqual(arch.dense_layer_indices, (0, 2)) - - def test_mlp_layer_types_ignores_unknown_entries(self): - hf = self._hf(mlp_layer_types = ["dense", "moe", "dense", "linear"]) - arch = extract_arch_config(hf) - self.assertEqual(arch.dense_layer_indices, (0, 2)) - - def test_mlp_layer_types_shorter_than_layers_only_marks_present(self): - hf = self._hf(mlp_layer_types = ["dense", "sparse"]) - arch = extract_arch_config(hf) - self.assertEqual(arch.dense_layer_indices, (0,)) - - def test_empty_mlp_layer_types_falls_through_to_first_k(self): - hf = self._hf(mlp_layer_types = [], first_k_dense_replace = 2) - arch = extract_arch_config(hf) - self.assertEqual(arch.dense_layer_indices, (0, 1)) - - -class TestPerLayerInputSkipAlias(unittest.TestCase): - def _hf(self, skip): - text_config = SimpleNamespace( - hidden_size = 64, - num_hidden_layers = 2, - num_attention_heads = 4, - num_key_value_heads = 4, - intermediate_size = 128, - vocab_size = 1000, - tie_word_embeddings = True, - hidden_size_per_layer_input = 8, - vocab_size_per_layer_input = 256, - ) - return SimpleNamespace( - text_config = text_config, - quantization_config = {"llm_int8_skip_modules": list(skip)}, - ) - - def test_per_layer_input_gate_skip_pulls_nonzero_delta(self): - from utils.hardware.vram_estimation import _compute_skipped_quantizable_elements - - arch = extract_arch_config(self._hf(["model.layers.0.per_layer_input_gate"])) - delta = _compute_skipped_quantizable_elements(arch) - self.assertEqual(delta, arch.hidden_size * arch.hidden_size_per_layer_input) - - def test_per_layer_model_projection_skip_pulls_global_delta(self): - from utils.hardware.vram_estimation import _compute_skipped_quantizable_elements - - arch = extract_arch_config(self._hf(["model.per_layer_model_projection"])) - delta = _compute_skipped_quantizable_elements(arch) - self.assertEqual( - delta, - arch.hidden_size - * arch.num_hidden_layers - * arch.hidden_size_per_layer_input, - ) - - def test_layer_aggregate_skip_includes_per_layer_input_modules(self): - from utils.hardware.vram_estimation import ( - _compute_skipped_quantizable_elements, - ) - - arch_with = extract_arch_config(self._hf(["model.layers.0"])) - # The text.layers.0 aggregate must include the PLE per-layer modules, - # so the same skip on a config without PLE produces a smaller value. - arch_without = extract_arch_config( - SimpleNamespace( - text_config = SimpleNamespace( - hidden_size = 64, - num_hidden_layers = 2, - num_attention_heads = 4, - num_key_value_heads = 4, - intermediate_size = 128, - vocab_size = 1000, - tie_word_embeddings = True, - hidden_size_per_layer_input = 0, - vocab_size_per_layer_input = 0, - ), - quantization_config = {"llm_int8_skip_modules": ["model.layers.0"]}, - ) - ) - self.assertGreater( - _compute_skipped_quantizable_elements(arch_with), - _compute_skipped_quantizable_elements(arch_without), - ) - - -class TestAllLinearStringHandling(unittest.TestCase): - def test_compute_lora_params_accepts_bare_all_linear_string(self): - list_form = compute_lora_params(LLAMA_8B, 16, ["all-linear"]) - str_form = compute_lora_params(LLAMA_8B, 16, "all-linear") - self.assertEqual(list_form, str_form) - self.assertGreater(list_form, 0) - - def test_compute_lora_params_string_with_underscores_normalized(self): - list_form = compute_lora_params(LLAMA_8B, 16, ["all_linear"]) - str_form = compute_lora_params(LLAMA_8B, 16, "all_linear") - self.assertEqual(list_form, str_form) - self.assertGreater(str_form, 0) - - -class TestSharedExpertVariants(unittest.TestCase): - def _hf(self, **fields): - text_config = SimpleNamespace( - hidden_size = 256, - num_hidden_layers = 4, - num_attention_heads = 8, - num_key_value_heads = 4, - intermediate_size = 1024, - vocab_size = 1000, - tie_word_embeddings = False, - num_local_experts = 8, - moe_intermediate_size = 128, - **fields, - ) - return SimpleNamespace(text_config = text_config, quantization_config = {}) - - def test_shared_expert_intermediate_size_extracted_and_infers_count(self): - arch = extract_arch_config(self._hf(shared_expert_intermediate_size = 64)) - self.assertEqual(arch.shared_expert_intermediate_size, 64) - self.assertEqual(arch.n_shared_experts, 1) - - def test_num_shared_experts_alias_extracted(self): - arch = extract_arch_config(self._hf(num_shared_experts = 2)) - self.assertEqual(arch.n_shared_experts, 2) - - def test_n_shared_experts_takes_priority_over_alias(self): - arch = extract_arch_config(self._hf(n_shared_experts = 3, num_shared_experts = 99)) - self.assertEqual(arch.n_shared_experts, 3) - - def test_shared_expert_size_separate_from_routed_changes_weight_count(self): - from utils.hardware.vram_estimation import _compute_moe_mlp_elements - - arch_separate = extract_arch_config( - self._hf(shared_expert_intermediate_size = 64) - ) - arch_implicit = extract_arch_config(self._hf(n_shared_experts = 1)) - # Different shared sizes (64 vs default moe_intermediate_size=128) must - # produce different MoE element counts. - self.assertNotEqual( - _compute_moe_mlp_elements(arch_separate), - _compute_moe_mlp_elements(arch_implicit), - ) - - def test_shared_expert_gate_counted_only_for_qwen_style(self): - from utils.hardware.vram_estimation import _compute_moe_mlp_elements - - # Qwen-style: shared_expert_intermediate_size set -> shared_expert_gate counted. - qwen_arch = extract_arch_config(self._hf(shared_expert_intermediate_size = 64)) - hd = qwen_arch.hidden_size - ms = qwen_arch.moe_intermediate_size - ne = qwen_arch.num_experts - ss = qwen_arch.shared_expert_intermediate_size - expected = hd * ms * 3 * ne + ne * hd + hd * ss * 3 * 1 + 1 * hd - self.assertEqual(_compute_moe_mlp_elements(qwen_arch), expected) - - # Non-Qwen shared experts (e.g. Exaone-MoE) -> no shared_expert_gate. - plain_arch = extract_arch_config(self._hf(n_shared_experts = 1)) - hd = plain_arch.hidden_size - ms = plain_arch.moe_intermediate_size - ne = plain_arch.num_experts - expected_plain = hd * ms * 3 * ne + ne * hd + hd * ms * 3 * 1 - self.assertEqual(_compute_moe_mlp_elements(plain_arch), expected_plain) - - -class TestSharedExpertActivation(unittest.TestCase): - def _make(self, **fields): - text_config = SimpleNamespace( - hidden_size = 512, - num_hidden_layers = 4, - num_attention_heads = 8, - num_key_value_heads = 4, - intermediate_size = 1024, - vocab_size = 1000, - tie_word_embeddings = False, - num_local_experts = 4, - moe_intermediate_size = 64, - **fields, - ) - return extract_arch_config( - SimpleNamespace(text_config = text_config, quantization_config = {}) - ) - - def test_shared_expert_increases_activation_bytes(self): - with_shared = self._make(shared_expert_intermediate_size = 64) - without = self._make() - self.assertGreater( - compute_activation_bytes( - with_shared, - 2, - 1024, - "none", - is_lora = True, - attention_implementation = "flash_attention_2", - ), - compute_activation_bytes( - without, - 2, - 1024, - "none", - is_lora = True, - attention_implementation = "flash_attention_2", - ), - ) - - def test_shared_expert_plus_dense_block_compose(self): - # gemma4 enable_moe_block with hypothetical shared expert: dense + routed - # + shared all live per layer; mlp_size should sum all three terms. - from utils.hardware.vram_estimation import _layer_qkv_mlp_sizes - - arch = self._make( - enable_moe_block = True, - shared_expert_intermediate_size = 32, - head_dim = 64, - layer_types = ["full_attention"] * 4, - ) - _, mlp_size = _layer_qkv_mlp_sizes(arch, 0) - # routed (64) + shared (32) + parallel dense intermediate (1024) - self.assertEqual(mlp_size, 64 + 32 + 1024) - - -class TestPerLayerInputActivation(unittest.TestCase): - def _make(self, **fields): - text_config = SimpleNamespace( - hidden_size = 512, - num_hidden_layers = 4, - num_attention_heads = 8, - num_key_value_heads = 4, - intermediate_size = 1024, - vocab_size = 1000, - tie_word_embeddings = False, - **fields, - ) - return extract_arch_config( - SimpleNamespace(text_config = text_config, quantization_config = {}) - ) - - def test_ple_increases_activation_bytes(self): - with_ple = self._make( - hidden_size_per_layer_input = 64, - vocab_size_per_layer_input = 256, - ) - without = self._make() - self.assertGreater( - compute_activation_bytes( - with_ple, - 2, - 1024, - "none", - is_lora = True, - attention_implementation = "flash_attention_2", - ), - compute_activation_bytes( - without, - 2, - 1024, - "none", - is_lora = True, - attention_implementation = "flash_attention_2", - ), - ) - - def test_ple_zero_does_not_inflate_activations(self): - without = self._make(hidden_size_per_layer_input = 0) - baseline = self._make() - self.assertEqual( - compute_activation_bytes( - without, - 2, - 512, - "none", - is_lora = True, - attention_implementation = "flash_attention_2", - ), - compute_activation_bytes( - baseline, - 2, - 512, - "none", - is_lora = True, - attention_implementation = "flash_attention_2", - ), - ) - - -class TestKvSharedActivation(unittest.TestCase): - def _make(self, kv_shared): - text_config = SimpleNamespace( - hidden_size = 512, - num_hidden_layers = 4, - num_attention_heads = 8, - num_key_value_heads = 4, - intermediate_size = 1024, - vocab_size = 1000, - tie_word_embeddings = False, - head_dim = 64, - num_kv_shared_layers = kv_shared, - layer_types = ["full_attention"] * 4, - ) - return extract_arch_config( - SimpleNamespace(text_config = text_config, quantization_config = {}) - ) - - def test_kv_shared_layers_keep_activation_bytes(self): - shared = self._make(kv_shared = 2) - full = self._make(kv_shared = 0) - self.assertEqual( - compute_activation_bytes( - shared, - 2, - 1024, - "none", - is_lora = True, - attention_implementation = "flash_attention_2", - ), - compute_activation_bytes( - full, - 2, - 1024, - "none", - is_lora = True, - attention_implementation = "flash_attention_2", - ), - ) - - -class TestSparseMoeSkipAliases(unittest.TestCase): - def _hf(self, skip, **fields): - text_config = SimpleNamespace( - hidden_size = 128, - num_hidden_layers = 2, - num_attention_heads = 4, - num_key_value_heads = 4, - intermediate_size = 256, - vocab_size = 1000, - tie_word_embeddings = False, - num_local_experts = 4, - moe_intermediate_size = 64, - **fields, - ) - return SimpleNamespace( - text_config = text_config, - quantization_config = {"llm_int8_skip_modules": list(skip)}, - ) - - def test_gemma4_layers_experts_alias_pulls_routed(self): - from utils.hardware.vram_estimation import _compute_skipped_quantizable_elements - - arch = extract_arch_config( - self._hf(["model.layers.0.experts"], enable_moe_block = True) - ) - self.assertGreater(_compute_skipped_quantizable_elements(arch), 0) - - def test_qwen_shared_expert_skip_pulls_only_shared(self): - from utils.hardware.vram_estimation import _compute_skipped_quantizable_elements - - arch = extract_arch_config( - self._hf( - ["model.layers.0.mlp.shared_expert"], - shared_expert_intermediate_size = 32, - ) - ) - # shared_expert delta only -- routed mlp.experts is NOT skipped. - delta = _compute_skipped_quantizable_elements(arch) - self.assertGreater(delta, 0) - full_layer = extract_arch_config( - self._hf( - ["model.layers.0.mlp"], - shared_expert_intermediate_size = 32, - ) - ) - self.assertGreater( - _compute_skipped_quantizable_elements(full_layer), - delta, - ) - - def test_exaone_shared_experts_plural_alias(self): - from utils.hardware.vram_estimation import _compute_skipped_quantizable_elements - - arch = extract_arch_config( - self._hf( - ["model.layers.0.mlp.shared_experts"], - num_shared_experts = 1, - ) - ) - self.assertGreater(_compute_skipped_quantizable_elements(arch), 0) - - -class TestAllLinearMoELoraExclusion(unittest.TestCase): - def _arch(self, **fields): - text_config = SimpleNamespace( - hidden_size = 256, - num_hidden_layers = 2, - num_attention_heads = 4, - num_key_value_heads = 4, - intermediate_size = 512, - vocab_size = 1000, - tie_word_embeddings = False, - num_local_experts = 8, - moe_intermediate_size = 64, - **fields, - ) - return extract_arch_config( - SimpleNamespace(text_config = text_config, quantization_config = {}) - ) - - def test_all_linear_drops_routed_moe_expert_lora(self): - arch = self._arch() - all_linear = compute_lora_params(arch, 8, "all-linear") - explicit = compute_lora_params(arch, 8, ["gate_proj", "up_proj", "down_proj"]) - self.assertLess(all_linear, explicit) - - def test_all_linear_drops_shared_expert_lora(self): - arch = self._arch(shared_expert_intermediate_size = 32) - all_linear = compute_lora_params(arch, 8, "all-linear") - explicit = compute_lora_params(arch, 8, ["gate_proj", "up_proj", "down_proj"]) - # explicit includes routed + shared MoE; all-linear includes neither. - self.assertLess(all_linear, explicit) - - def test_all_linear_includes_attention_lora(self): - arch = self._arch() - all_linear = compute_lora_params(arch, 8, "all-linear") - attn_only = compute_lora_params( - arch, 8, ["q_proj", "k_proj", "v_proj", "o_proj"] - ) - # all-linear still attaches to attention nn.Linear modules. - self.assertGreaterEqual(all_linear, attn_only) - - -class TestExplicitPerLayerInputLora(unittest.TestCase): - def _arch(self): - text_config = SimpleNamespace( - hidden_size = 256, - num_hidden_layers = 3, - num_attention_heads = 4, - num_key_value_heads = 4, - intermediate_size = 512, - vocab_size = 1000, - tie_word_embeddings = False, - hidden_size_per_layer_input = 32, - vocab_size_per_layer_input = 128, - ) - return extract_arch_config( - SimpleNamespace(text_config = text_config, quantization_config = {}) - ) - - def test_explicit_per_layer_input_gate_returns_nonzero(self): - arch = self._arch() - result = compute_lora_params(arch, 16, ["per_layer_input_gate"]) - self.assertGreater(result, 0) - - def test_explicit_per_layer_projection_returns_nonzero(self): - arch = self._arch() - result = compute_lora_params(arch, 16, ["per_layer_projection"]) - self.assertGreater(result, 0) - - def test_explicit_per_layer_model_projection_returns_nonzero(self): - arch = self._arch() - result = compute_lora_params(arch, 16, ["per_layer_model_projection"]) - self.assertGreater(result, 0) - - def test_explicit_ple_string_target_handled(self): - # Bare-string target with a PLE name should not be iterated char-by-char. - arch = self._arch() - list_form = compute_lora_params(arch, 16, ["per_layer_input_gate"]) - str_form = compute_lora_params(arch, 16, "per_layer_input_gate") - self.assertEqual(list_form, str_form) - - -class TestTopKExpertActivation(unittest.TestCase): - def _make(self, **fields): - text_config = SimpleNamespace( - hidden_size = 512, - num_hidden_layers = 4, - num_attention_heads = 8, - num_key_value_heads = 4, - intermediate_size = 1024, - vocab_size = 1000, - tie_word_embeddings = False, - num_local_experts = 8, - moe_intermediate_size = 64, - **fields, - ) - return extract_arch_config( - SimpleNamespace(text_config = text_config, quantization_config = {}) - ) - - def test_num_experts_per_tok_extracted(self): - arch = self._make(num_experts_per_tok = 4) - self.assertEqual(arch.num_experts_per_tok, 4) - - def test_top_k_experts_alias_extracted(self): - arch = self._make(top_k_experts = 8) - self.assertEqual(arch.num_experts_per_tok, 8) - - def test_default_top_k_one_unchanged(self): - arch = self._make() - self.assertEqual(arch.num_experts_per_tok, 1) - - def test_top_k_scales_moe_activation(self): - single = self._make() - multi = self._make(num_experts_per_tok = 8) - single_act = compute_activation_bytes( - single, - 2, - 512, - "none", - is_lora = True, - attention_implementation = "flash_attention_2", - ) - multi_act = compute_activation_bytes( - multi, - 2, - 512, - "none", - is_lora = True, - attention_implementation = "flash_attention_2", - ) - self.assertGreater(multi_act, single_act) - - -class TestErnieMoEListConfig(unittest.TestCase): - def _hf(self, **fields): - text_config = SimpleNamespace( - hidden_size = 256, - num_hidden_layers = 4, - num_attention_heads = 4, - num_key_value_heads = 4, - intermediate_size = 1024, - vocab_size = 1000, - tie_word_embeddings = False, - **fields, - ) - return SimpleNamespace(text_config = text_config, quantization_config = {}) - - def test_list_moe_intermediate_size_scalarized(self): - arch = extract_arch_config( - self._hf( - moe_num_experts = 32, - moe_intermediate_size = [1536, 512], - ) - ) - # why: ERNIE 4.5 VL MoE encodes [text_routed, vision_routed]; the - # second element is the vision-routed expert width, not the shared - # expert width. Shared experts are sized from the text-routed width - # (= moe_intermediate_size[0]) when moe_num_shared_experts is set. - self.assertEqual(arch.moe_intermediate_size, 1536) - self.assertIsNone(arch.shared_expert_intermediate_size) - self.assertEqual(arch.n_shared_experts, 0) - - def test_moe_num_experts_alias_extracted(self): - arch = extract_arch_config( - self._hf( - moe_num_experts = 64, - moe_intermediate_size = 1024, - ) - ) - self.assertEqual(arch.num_experts, 64) - - def test_moe_num_shared_experts_alias_extracted(self): - arch = extract_arch_config( - self._hf( - moe_num_experts = 16, - moe_num_shared_experts = 2, - moe_intermediate_size = 1024, - ) - ) - self.assertEqual(arch.n_shared_experts, 2) - - def test_explicit_shared_size_overrides_list_second_element(self): - arch = extract_arch_config( - self._hf( - moe_num_experts = 8, - moe_intermediate_size = [1536, 512], - shared_expert_intermediate_size = 256, - ) - ) - # Explicit shared size wins over moe_intermediate_size[1]. - self.assertEqual(arch.shared_expert_intermediate_size, 256) - - -class TestSuffixSkipModuleMatch(unittest.TestCase): - def _hf(self, skip): - text_config = SimpleNamespace( - hidden_size = 128, - num_hidden_layers = 2, - num_attention_heads = 4, - num_key_value_heads = 4, - intermediate_size = 256, - vocab_size = 1000, - tie_word_embeddings = False, - ) - return SimpleNamespace( - text_config = text_config, - quantization_config = {"llm_int8_skip_modules": list(skip)}, - ) - - def test_q_proj_suffix_skip_matches_all_layers(self): - from utils.hardware.vram_estimation import _compute_skipped_quantizable_elements - - arch = extract_arch_config(self._hf(["q_proj"])) - delta = _compute_skipped_quantizable_elements(arch) - # 2 layers * hd * hd of q_proj weight elements. - self.assertEqual(delta, 2 * arch.hidden_size * arch.hidden_size) - - def test_self_attn_aggregate_skip_matches_aggregate(self): - from utils.hardware.vram_estimation import _compute_skipped_quantizable_elements - - arch = extract_arch_config(self._hf(["self_attn"])) - # The aggregate text.layers..self_attn matches; total covers both layers. - delta = _compute_skipped_quantizable_elements(arch) - self.assertGreater(delta, 0) - - def test_vision_prefix_skip_does_not_match_text_alias(self): - from utils.hardware.vram_estimation import _module_path_matches - - # vision_tower-prefixed full path must NOT match text-tower aliases. - self.assertFalse( - _module_path_matches( - "vision_tower.model.layers.0.self_attn.q_proj", - "model.layers.0.self_attn.q_proj", - ) - ) - - -class TestMultimodalFullModelBytes(unittest.TestCase): - def test_extra_bytes_added_when_safetensors_exceeds_text_arch(self): - from utils.hardware import hardware as hardware_module - - config = SimpleNamespace( - hidden_size = 1024, - num_hidden_layers = 4, - num_attention_heads = 8, - num_key_value_heads = 4, - intermediate_size = 2048, - vocab_size = 32000, - tie_word_embeddings = False, - ) - # Force safetensors size >>> arch text-only bytes. - big_safetensors = 20 * 1024**3 - with ( - patch.object( - hardware_module, - "_load_config_for_gpu_estimate", - return_value = config, - ), - patch.object( - hardware_module, - "estimate_fp16_model_size_bytes", - return_value = (big_safetensors, "safetensors"), - ), - patch.object( - hardware_module, - "_determine_attention_impl_for_gpu_estimate", - return_value = "flash_attention_2", - ), - patch.object( - hardware_module, - "get_visible_gpu_count", - return_value = 1, - ), - ): - _, metadata = hardware_module.estimate_required_model_memory_gb( - "fake/model", - training_type = "LoRA/QLoRA", - load_in_4bit = True, - ) - self.assertEqual(metadata.get("estimation_mode"), "detailed") - # model_weights_gb must reflect the extra non-text bytes (>5 GB - # since text-only arch_fp16 is small for these dims). - self.assertGreater(metadata["vram_breakdown"]["model_weights_gb"], 5.0) - - def test_no_extra_when_safetensors_smaller_than_text_arch(self): - from utils.hardware import hardware as hardware_module - - config = SimpleNamespace( - hidden_size = 4096, - num_hidden_layers = 32, - num_attention_heads = 32, - num_key_value_heads = 8, - intermediate_size = 11008, - vocab_size = 32000, - tie_word_embeddings = False, - ) - tiny_safetensors = 100 # bytes, deliberately absurdly small - with ( - patch.object( - hardware_module, - "_load_config_for_gpu_estimate", - return_value = config, - ), - patch.object( - hardware_module, - "estimate_fp16_model_size_bytes", - return_value = (tiny_safetensors, "safetensors"), - ), - patch.object( - hardware_module, - "_determine_attention_impl_for_gpu_estimate", - return_value = "flash_attention_2", - ), - patch.object( - hardware_module, - "get_visible_gpu_count", - return_value = 1, - ), - ): - required, metadata = hardware_module.estimate_required_model_memory_gb( - "fake/model", - training_type = "LoRA/QLoRA", - load_in_4bit = True, - ) - # No negative extra; required_gb stays a positive finite number. - self.assertGreater(required, 0) - - -class TestLlama4ArchExtraction(unittest.TestCase): - def _llama4_text_config(self, **fields): - base = dict( - hidden_size = 2048, - num_hidden_layers = 4, - num_attention_heads = 16, - num_key_value_heads = 4, - intermediate_size = 8192, - intermediate_size_mlp = 16384, - vocab_size = 32000, - tie_word_embeddings = True, - num_local_experts = 4, - num_experts_per_tok = 2, - ) - base.update(fields) - return SimpleNamespace(**base) - - def test_llama4_moe_layers_dispatch_uses_explicit_indices(self): - from utils.hardware.vram_estimation import _compute_dense_layer_indices - - cfg = SimpleNamespace(num_hidden_layers = 4, moe_layers = [1, 3]) - self.assertEqual(_compute_dense_layer_indices(cfg, 4), (0, 2)) - - def test_llama4_moe_layers_takes_priority_over_first_k_dense_replace(self): - from utils.hardware.vram_estimation import _compute_dense_layer_indices - - cfg = SimpleNamespace( - num_hidden_layers = 6, - moe_layers = [2, 4], - first_k_dense_replace = 4, - ) - self.assertEqual(_compute_dense_layer_indices(cfg, 6), (0, 1, 3, 5)) - - def test_dense_intermediate_size_picks_up_intermediate_size_mlp(self): - from utils.hardware.vram_estimation import _dense_mlp_size - - arch = extract_arch_config(self._llama4_text_config(moe_layers = [1, 3])) - self.assertIsNotNone(arch) - self.assertEqual(arch.intermediate_size, 8192) - self.assertEqual(arch.dense_intermediate_size, 16384) - self.assertEqual(_dense_mlp_size(arch), 16384) - - def test_auto_attaches_one_shared_expert_at_routed_width(self): - from utils.hardware.vram_estimation import _shared_expert_size - - arch = extract_arch_config(self._llama4_text_config(moe_layers = [1, 3])) - self.assertIsNotNone(arch) - self.assertEqual(arch.n_shared_experts, 1) - self.assertIsNone(arch.shared_expert_intermediate_size) - self.assertEqual(_shared_expert_size(arch), arch.intermediate_size) - - def test_non_llama4_config_leaves_dense_intermediate_size_none(self): - from utils.hardware.vram_estimation import _dense_mlp_size - - cfg = SimpleNamespace( - hidden_size = 1024, - num_hidden_layers = 4, - num_attention_heads = 8, - num_key_value_heads = 2, - intermediate_size = 4096, - vocab_size = 32000, - tie_word_embeddings = True, - ) - arch = extract_arch_config(cfg) - self.assertIsNotNone(arch) - self.assertIsNone(arch.dense_intermediate_size) - self.assertEqual(_dense_mlp_size(arch), 4096) - - def test_intermediate_size_mlp_without_moe_does_not_force_shared_expert(self): - cfg = SimpleNamespace( - hidden_size = 2048, - num_hidden_layers = 4, - num_attention_heads = 16, - num_key_value_heads = 4, - intermediate_size = 8192, - intermediate_size_mlp = 16384, - vocab_size = 32000, - tie_word_embeddings = True, - ) - arch = extract_arch_config(cfg) - self.assertIsNotNone(arch) - self.assertEqual(arch.dense_intermediate_size, 16384) - self.assertEqual(arch.n_shared_experts, 0) - - -class TestDbrxFfnConfigExtraction(unittest.TestCase): - def test_extracts_moe_fields_from_ffn_subconfig(self): - ffn = SimpleNamespace(moe_num_experts = 4, moe_top_k = 2, ffn_hidden_size = 1024) - cfg = SimpleNamespace( - hidden_size = 2048, - num_hidden_layers = 4, - num_attention_heads = 16, - num_key_value_heads = 4, - intermediate_size = 2048, - vocab_size = 32000, - tie_word_embeddings = False, - ffn_config = ffn, - ) - arch = extract_arch_config(cfg) - self.assertIsNotNone(arch) - self.assertEqual(arch.num_experts, 4) - self.assertEqual(arch.num_experts_per_tok, 2) - self.assertEqual(arch.moe_intermediate_size, 1024) - - def test_top_level_attrs_take_precedence_over_ffn_config(self): - ffn = SimpleNamespace(moe_num_experts = 4, moe_top_k = 2, ffn_hidden_size = 1024) - cfg = SimpleNamespace( - hidden_size = 2048, - num_hidden_layers = 4, - num_attention_heads = 16, - num_key_value_heads = 4, - intermediate_size = 2048, - vocab_size = 32000, - tie_word_embeddings = False, - ffn_config = ffn, - num_local_experts = 16, - num_experts_per_tok = 8, - ) - arch = extract_arch_config(cfg) - self.assertIsNotNone(arch) - self.assertEqual(arch.num_experts, 16) - self.assertEqual(arch.num_experts_per_tok, 8) - - -class TestErniePhaseModuloDispatch(unittest.TestCase): - def test_phase_modulo_with_interval_two_matches_decoder(self): - from utils.hardware.vram_estimation import _compute_dense_layer_indices - - cfg = SimpleNamespace( - num_hidden_layers = 10, - moe_layer_start_index = 2, - moe_layer_end_index = 8, - moe_layer_interval = 2, - ) - # Decoder gates by ((i + 1) % 2 == 0) AND 2 <= i <= 8 -> MoE = {3, 5, 7}. - self.assertEqual(_compute_dense_layer_indices(cfg, 10), (0, 1, 2, 4, 6, 8, 9)) - - def test_phase_modulo_with_interval_three(self): - from utils.hardware.vram_estimation import _compute_dense_layer_indices - - cfg = SimpleNamespace( - num_hidden_layers = 9, - moe_layer_start_index = 0, - moe_layer_end_index = -1, - moe_layer_interval = 3, - ) - self.assertEqual(_compute_dense_layer_indices(cfg, 9), (0, 1, 3, 4, 6, 7)) - - -class TestErnieVlSharedExpertWidth(unittest.TestCase): - def test_shared_expert_width_uses_text_routed_not_vision(self): - from utils.hardware.vram_estimation import ( - _compute_shared_moe_elements, - _shared_expert_size, - ) - - cfg = SimpleNamespace( - text_config = SimpleNamespace( - hidden_size = 1024, - num_hidden_layers = 4, - num_attention_heads = 8, - num_key_value_heads = 4, - intermediate_size = 2048, - vocab_size = 32000, - tie_word_embeddings = False, - moe_num_experts = 8, - moe_num_shared_experts = 2, - moe_intermediate_size = [1536, 512], - ), - quantization_config = {}, - ) - arch = extract_arch_config(cfg) - self.assertIsNotNone(arch) - self.assertIsNone(arch.shared_expert_intermediate_size) - self.assertEqual(arch.moe_intermediate_size, 1536) - self.assertEqual(arch.n_shared_experts, 2) - self.assertEqual(_shared_expert_size(arch), 1536) - self.assertEqual(_compute_shared_moe_elements(arch), 1024 * 1536 * 3 * 2) - - def test_qwen_style_explicit_shared_expert_size_still_adds_gate(self): - from utils.hardware.vram_estimation import _compute_shared_moe_elements - - cfg = SimpleNamespace( - hidden_size = 1024, - num_hidden_layers = 4, - num_attention_heads = 8, - num_key_value_heads = 4, - intermediate_size = 2048, - vocab_size = 32000, - tie_word_embeddings = False, - num_local_experts = 8, - moe_intermediate_size = 256, - shared_expert_intermediate_size = 768, - ) - arch = extract_arch_config(cfg) - self.assertIsNotNone(arch) - self.assertEqual(arch.shared_expert_intermediate_size, 768) - self.assertEqual(arch.n_shared_experts, 1) - self.assertEqual( - _compute_shared_moe_elements(arch), - 1024 * 768 * 3 + 1 * 1024, - ) - - if __name__ == "__main__": unittest.main() diff --git a/studio/backend/utils/datasets/dataset_utils.py b/studio/backend/utils/datasets/dataset_utils.py index 26378d64ee..fac8c3d295 100644 --- a/studio/backend/utils/datasets/dataset_utils.py +++ b/studio/backend/utils/datasets/dataset_utils.py @@ -41,7 +41,6 @@ get_tokenizer_chat_template, DEFAULT_ALPACA_TEMPLATE, ) -from .raw_text import prepare_raw_text_dataset from .vlm_processing import generate_smart_vlm_instruction from .data_collators import DeepSeekOCRDataCollator, VLMDataCollator from .model_mappings import TEMPLATE_TO_MODEL_MAPPER @@ -438,20 +437,6 @@ def format_dataset( # Detect multimodal first (needed for all flows) multimodal_info = detect_multimodal_dataset(dataset) - if format_type == "raw": - raw_result = prepare_raw_text_dataset(dataset) - return { - "dataset": raw_result.dataset, - "detected_format": "raw_text", - "final_format": "raw_text", - "chat_column": "text", - "is_standardized": True, - "requires_manual_mapping": False, - "is_image": multimodal_info["is_image"], - "multimodal_info": multimodal_info, - "warnings": [notice.message for notice in raw_result.notices], - } - # If user provided explicit mapping, skip detection and apply in the requested format if custom_format_mapping: try: @@ -1120,21 +1105,6 @@ def format_and_template_dataset( num_proc = num_proc, ) - if dataset_info["final_format"] == "raw_text": - summary = get_dataset_info_summary(dataset_info) - return { - "dataset": dataset_info["dataset"], - "detected_format": dataset_info["detected_format"], - "final_format": dataset_info["final_format"], - "chat_column": dataset_info.get("chat_column"), - "is_vlm": False, - "success": True, - "requires_manual_mapping": False, - "warnings": dataset_info.get("warnings", []), - "errors": [], - "summary": summary, - } - # Step 2: Apply chat template detected = dataset_info.get("detected_format", "unknown") if progress_callback and n_rows: diff --git a/studio/backend/utils/datasets/model_mappings.py b/studio/backend/utils/datasets/model_mappings.py index 21e8566ac5..36f6886ef6 100644 --- a/studio/backend/utils/datasets/model_mappings.py +++ b/studio/backend/utils/datasets/model_mappings.py @@ -364,10 +364,6 @@ "unsloth/Qwen3-4B-Thinking-2507-bnb-4bit", "unsloth/Qwen3-30B-A3B-Thinking-2507", "Qwen/Qwen3-30B-A3B-Thinking-2507", - "Qwen/Qwen3.6-35B-A3B", - "unsloth/Qwen3.6-35B-A3B", - "Qwen/Qwen3.6-27B", - "unsloth/Qwen3.6-27B", ), "qwen3.5": ( "unsloth/Qwen3.5-0.8B", diff --git a/studio/backend/utils/datasets/raw_text.py b/studio/backend/utils/datasets/raw_text.py deleted file mode 100644 index 353145fd5a..0000000000 --- a/studio/backend/utils/datasets/raw_text.py +++ /dev/null @@ -1,142 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 - -""" -Shared helpers for raw-text dataset preparation. -""" - -from dataclasses import dataclass -from typing import Literal - -from datasets import Dataset - - -@dataclass(frozen = True) -class RawTextNotice: - message: str - level: Literal["info", "warning"] - update_status: bool = False - - -@dataclass(frozen = True) -class RawTextPreparationResult: - dataset: Dataset - notices: list[RawTextNotice] - - -def _string_columns(dataset: Dataset) -> list[str]: - feature_map = getattr(dataset, "features", {}) or {} - string_cols: list[str] = [] - for col in dataset.column_names: - feature = feature_map.get(col) - dtype = str(getattr(feature, "dtype", "")) - if dtype in {"string", "large_string"}: - string_cols.append(col) - return string_cols - - -def _split_scope(split_name: str | None) -> str: - return f"the {split_name} split" if split_name else "this dataset" - - -def _drop_invalid_text_rows( - dataset: Dataset, - *, - mode_title: str, - split_scope: str, -) -> tuple[Dataset, list[RawTextNotice]]: - filtered_dataset = dataset.filter(lambda ex: isinstance(ex["text"], str)) - dropped_rows = len(dataset) - len(filtered_dataset) - if not dropped_rows: - return filtered_dataset, [] - - if len(filtered_dataset) == 0: - raise ValueError( - f"{mode_title} training requires at least one string 'text' value " - f"in {split_scope}; all {dropped_rows} rows were null or non-string." - ) - - return filtered_dataset, [ - RawTextNotice( - message = ( - f"{mode_title}: dropped {dropped_rows:,} row(s) with null or " - f"non-string 'text' values from {split_scope}" - ), - level = "warning", - update_status = True, - ) - ] - - -def prepare_raw_text_dataset( - dataset: Dataset, - *, - mode_label: str = "raw text", - split_name: str | None = None, - eos_token: str | None = None, - append_eos: bool = False, -) -> RawTextPreparationResult: - notices: list[RawTextNotice] = [] - mode_title = mode_label.capitalize() - split_scope = _split_scope(split_name) - - if "text" not in dataset.column_names: - string_cols = _string_columns(dataset) - if not string_cols: - raise ValueError( - f"{mode_title} training requires a string 'text' column but none " - f"was found in {split_scope} (columns: {dataset.column_names})." - ) - - renamed_col = string_cols[0] - if len(string_cols) > 1: - notices.append( - RawTextNotice( - message = ( - f"{mode_title}: dataset has {len(string_cols)} string " - f"columns ({string_cols}); auto-selecting '{renamed_col}' " - "as the training text. Rename the intended column to " - "'text' to override." - ), - level = "warning", - update_status = True, - ) - ) - notices.append( - RawTextNotice( - message = ( - f"{mode_title}: renaming column '{renamed_col}' -> 'text' " - f"for {split_scope}" - ), - level = "info", - ) - ) - dataset = dataset.rename_column(renamed_col, "text") - - dataset, invalid_row_notices = _drop_invalid_text_rows( - dataset, - mode_title = mode_title, - split_scope = split_scope, - ) - notices.extend(invalid_row_notices) - - if append_eos: - if not eos_token: - notices.append( - RawTextNotice( - message = ( - f"{mode_title}: tokenizer has no eos_token; skipping EOS " - "append. Model will not learn document boundaries." - ), - level = "warning", - ) - ) - else: - - def _append_eos(ex, _eos = eos_token): - text = ex["text"] - return {"text": text if text.endswith(_eos) else text + _eos} - - dataset = dataset.map(_append_eos) - - return RawTextPreparationResult(dataset = dataset, notices = notices) diff --git a/studio/backend/utils/hardware/VRAM_ESTIMATION.md b/studio/backend/utils/hardware/VRAM_ESTIMATION.md index a6b4de29d2..26072b208f 100644 --- a/studio/backend/utils/hardware/VRAM_ESTIMATION.md +++ b/studio/backend/utils/hardware/VRAM_ESTIMATION.md @@ -33,13 +33,7 @@ Non-quantizable = 2*H*L + V*H + (V*H if not tie_embeddings else 0) | QLoRA 4-bit | `Quantizable * 2 / 3.2 + Non-quantizable * 2` | | LoRA / Full fp16 | `(Quantizable + Non-quantizable) * 2` | -The 3.2 factor (`16/5`) accounts for BNB NF4 blockwise scales. Repos whose -quantization config enables `bnb_4bit_use_double_quant` use a tighter, still -conservative 3.6 factor for the quantized portion of the weights. -When a 4-bit config has `llm_int8_skip_modules` entries that point to language -model layers or submodules, those quantizable weights are charged at fp16 -instead of NF4. Generic embedding and multimodal skip names are already covered -by non-quantizable terms or excluded from text training weights. +The 3.2 factor (`16/5`) accounts for BNB NF4 blockwise scales. ## 2. LoRA Adapters @@ -59,18 +53,6 @@ MLP modules multiply by `E` for MoE. LoRA_bytes = sum(A + B per selected module) * L * 2 ``` -`all-linear` is treated as all known text linear modules in the table above. -The estimator deliberately does not infer multimodal or vision-tower LoRA -modules from config shapes; those modules vary too much across VLM families for -a generic config formula. - -Some decoder configs expose layer-shape fields such as `layer_types`, -`head_dim`, `global_head_dim`, `num_global_key_value_heads`, `attention_k_eq_v`, -`num_kv_shared_layers`, `use_double_wide_mlp`, `vocab_size_per_layer_input`, and -`hidden_size_per_layer_input`. When those fields are present, the estimator -derives text weight and LoRA counts from the per-layer shapes instead of -assuming every layer has the same seven projection modules. - ## 3. Optimizer States (calibrated) | Optimizer | Bytes/param | Notes | @@ -95,21 +77,6 @@ Per-layer (from `unsloth_zoo/vllm_utils.py`): Per_layer = (S*B*(H+K+K) + S*B*2 + S*B*(M+M)) * 2 * 1.25 ``` -When the resolved attention implementation is none of `flash_attention_2`, -`sdpa`, or `flex_attention` (PyTorch SDPA dispatches to flash or -memory-efficient kernels and FlexAttention is also a memory-efficient -kernel, all of which are O(n) in memory), activation memory also includes -a quadratic attention-score/workspace estimate: - -``` -Non_flash_attention = B * num_attention_heads * S^2 * 2 * 12.0 * effective_layers -Activations = max(Per_layer_with_gc, Non_flash_attention) -``` - -Studio resolves the attention implementation with Unsloth's -`resolve_attention_implementation` helper and uses that result directly. The -estimator does not duplicate model-family attention policy. - | GC Mode | Full FT | LoRA/QLoRA | |---------|---------|------------| | none | `L` layers | `L` layers | @@ -118,33 +85,13 @@ estimator does not duplicate model-family attention policy. ## 6. Floors -Activations use the computed formula directly: +Gradients and activations have minimum floors at **15% of model weight memory** to account for autograd overhead, attention score matrices, NCCL buffers, mixed-precision scaling, and PyTorch fragmentation. ``` -activation_bytes = computed_activation_bytes +gradient_bytes = max(computed, weights * 0.15) +activation_bytes = max(computed, weights * 0.15 * B/2) ``` -Full fine-tuning keeps the gradient floor at **15% of model weight memory** to -account for autograd overhead, NCCL buffers, mixed-precision scaling, and -PyTorch fragmentation: - -``` -gradient_bytes = max(computed_gradient_bytes, weights * 0.15) -``` - -For LoRA/QLoRA, the base model is frozen, so the weight-derived gradient floor -is capped by trainable-state and live-activation scale: - -``` -raw_gradient_bytes = trainable_params * 2 -gradient_floor = min(weights * 0.15, max(computed_activation_bytes, optimizer_bytes)) -gradient_bytes = max(raw_gradient_bytes, gradient_floor) -``` - -This prevents frozen quantized model size from dominating gradient/state -overhead when the measured runtime footprint is governed by LoRA optimizer -states and live activations. - ## 7. CUDA Overhead **1.4 GB** fixed — CUDA driver + PyTorch runtime, calibrated on RTX 5070 Ti. @@ -159,6 +106,34 @@ usable_gb = free[gpu_0] + sum(free[gpu_i] * 0.85 for i in 1..N) --- +## Reference Table (bsz=2, seq=2048, rank=16, GC=unsloth, adamw_8bit) + +| Model | Weights | LoRA | Optim | Grad | Act | CUDA | Total | +|-------|---------|------|-------|------|-----|------|-------| +| 0.5B QLoRA | 0.5 | 0.0 | 0.0 | 0.1 | 0.1 | 1.4 | **2.1** | +| 1B QLoRA | 1.1 | 0.0 | 0.0 | 0.2 | 0.2 | 1.4 | **2.9** | +| 3B QLoRA | 2.4 | 0.0 | 0.1 | 0.5 | 0.5 | 1.4 | **4.9** | +| 8B QLoRA | 6.0 | 0.1 | 0.2 | 1.2 | 1.2 | 1.4 | **10.1** | +| 8B LoRA fp16 | 15.0 | 0.1 | 0.2 | 3.0 | 3.0 | 1.4 | **22.6** | +| 8B Full FT | 15.0 | — | 29.9 | 15.0 | 3.0 | 1.4 | **64.2** | +| 32B LoRA fp16 | 61.0 | 0.2 | 0.5 | 12.2 | 12.2 | 1.4 | **87.6** | +| 72B QLoRA | 45.5 | 0.4 | 0.8 | 9.1 | 9.1 | 1.4 | **66.3** | + +## E2E Validation (Llama-3.2-1B, B200 emulating 24GB) + +| Config | Estimated | Actual (nvsmi) | Error | +|--------|----------|----------------|-------| +| QLoRA bsz=2 seq=512 | 2.55 GB | 2.65 GB | -3.7% | +| QLoRA bsz=2 seq=2048 | 2.60 GB | 2.65 GB | -1.8% | +| QLoRA bsz=4 seq=2048 | 2.65 GB | 2.65 GB | +0.0% | +| LoRA fp16 bsz=2 | 3.84 GB | 3.88 GB | -1.0% | +| Full FT adamw_8bit | 10.89 GB | 10.80 GB | +0.8% | +| Full FT adamw_torch | 13.19 GB | 12.93 GB | +2.0% | + +*Note: e2e numbers predate the 15% floors, which add safety margin on top.* + +--- + ## Parameter Flow ``` diff --git a/studio/backend/utils/hardware/amd.py b/studio/backend/utils/hardware/amd.py index fdb1ab4520..755314ca3a 100644 --- a/studio/backend/utils/hardware/amd.py +++ b/studio/backend/utils/hardware/amd.py @@ -16,7 +16,6 @@ from typing import Any, Optional from loggers import get_logger -from utils.native_path_leases import child_env_without_native_path_secret logger = get_logger(__name__) @@ -29,7 +28,6 @@ def _run_amd_smi(*args: str, timeout: int = 5) -> Optional[Any]: capture_output = True, text = True, timeout = timeout, - env = child_env_without_native_path_secret(), ) except (OSError, subprocess.TimeoutExpired) as e: logger.warning("amd-smi query failed: %s", e) diff --git a/studio/backend/utils/hardware/hardware.py b/studio/backend/utils/hardware/hardware.py index 3764e38272..be31c00a78 100644 --- a/studio/backend/utils/hardware/hardware.py +++ b/studio/backend/utils/hardware/hardware.py @@ -143,7 +143,6 @@ def detect_hardware() -> DeviceType: # --- MLX: Apple Silicon --- if is_apple_silicon() and _has_mlx(): DEVICE = DeviceType.MLX - CHAT_ONLY = False chip = platform.processor() or platform.machine() print(f"Hardware detected: MLX — Apple Silicon ({chip})") return DEVICE @@ -271,30 +270,19 @@ def get_gpu_memory_info() -> Dict[str, Any]: import mlx.core as mx import psutil - # MLX uses unified memory. Total = system RAM. GPU memory used - # comes from IORegistry's AGXAccelerator (system-wide, no sudo). + # MLX uses unified memory — report system memory as the pool total = psutil.virtual_memory().total - agx = _read_apple_gpu_stats() - allocated = agx.get("vram_used_bytes", 0) if agx else 0 - - try: - info = mx.device_info() - gpu_name = ( - info.get("device_name") - or platform.processor() - or platform.machine() - ) - except Exception: - gpu_name = platform.processor() or platform.machine() + # MLX doesn't expose per-process GPU allocation; report 0 as allocated + allocated = 0 return { "available": True, "backend": _backend_label(device), "device": 0, - "device_name": f"Apple Silicon ({gpu_name})", + "device_name": f"Apple Silicon ({platform.processor() or platform.machine()})", "total_gb": total / (1024**3), "allocated_gb": allocated / (1024**3), - "reserved_gb": allocated / (1024**3), + "reserved_gb": 0, "free_gb": (total - allocated) / (1024**3), "utilization_pct": (allocated / total) * 100 if total else 0, } @@ -472,39 +460,6 @@ def _smi_query(func_name: str, *args, **kwargs) -> Optional[Dict[str, Any]]: return None -def _read_apple_gpu_stats() -> Dict[str, Any]: - """Query macOS IORegistry for AGX (Apple GPU) live stats. No sudo needed. - - Returns dict with utilization_pct, vram_used_bytes (system-wide GPU memory). - Returns empty dict on failure. - """ - import subprocess - import re - - try: - result = subprocess.run( - ["ioreg", "-r", "-c", "AGXAccelerator"], - capture_output = True, - timeout = 2, - ) - text = result.stdout.decode("utf-8", errors = "replace") - except Exception: - return {} - - # PerformanceStatistics block has GPU utilization and in-use memory - m = re.search(r'"PerformanceStatistics" = \{([^}]+)\}', text) - if not m: - return {} - stats_str = m.group(1) - pairs = re.findall(r'"([^"]+)"=(\d+)', stats_str) - stats = {k: int(v) for k, v in pairs} - - return { - "utilization_pct": stats.get("Device Utilization %", 0), - "vram_used_bytes": stats.get("In use system memory", 0), - } - - def get_gpu_utilization() -> Dict[str, Any]: """Return a live snapshot of device utilization information.""" device = get_device() @@ -515,50 +470,6 @@ def get_gpu_utilization() -> Dict[str, Any]: result["backend"] = _backend_label(device) return result - # MLX path: single _read_apple_gpu_stats() call carries both VRAM-used - # bytes and GPU utilization %. psutil for unified-memory total is cheap. - if device == DeviceType.MLX: - try: - import psutil - - agx = _read_apple_gpu_stats() - total_bytes = psutil.virtual_memory().total - except Exception as e: - logger.error(f"Error getting MLX GPU utilization: {e}") - return {"available": False, "backend": device.value, "error": str(e)} - if not agx: - return {"available": False, "backend": device.value} - allocated_bytes = agx.get("vram_used_bytes", 0) or 0 - vram_used_gb = allocated_bytes / (1024**3) - total_gb = total_bytes / (1024**3) - - try: - from core.training import get_training_backend - - tb = get_training_backend() - tb_progress = getattr(tb, "_progress", None) - if tb_progress is not None and getattr(tb_progress, "is_training", False): - tb_peak = getattr(tb_progress, "peak_memory_gb", None) - if tb_peak is not None and tb_peak > 0: - vram_used_gb = float(tb_peak) - except Exception: - pass - - return { - "available": True, - "backend": device.value, - "gpu_utilization_pct": agx.get("utilization_pct") if agx else None, - "temperature_c": None, - "vram_used_gb": round(vram_used_gb, 2), - "vram_total_gb": round(total_gb, 2), - "vram_utilization_pct": ( - round((vram_used_gb / total_gb) * 100, 1) if total_gb > 0 else None - ), - "power_draw_w": None, - "power_limit_w": None, - "power_utilization_pct": None, - } - mem = get_gpu_memory_info() if device != DeviceType.CPU and mem.get("available"): return { @@ -863,34 +774,6 @@ def _load_config_for_gpu_estimate(model_name: str, hf_token: Optional[str] = Non return None -def _determine_attention_impl_for_gpu_estimate(config) -> str: - import copy as _copy - - from unsloth.models._utils import resolve_attention_implementation - from transformers import AutoModel, AutoModelForCausalLM - - # why: resolve_attention_implementation calls _set_attn_impl which writes - # _attn_implementation onto the config; PreTrainedConfig's setter walks - # `sub_configs` and propagates to nested text_config / sub-configs, so a - # shallow copy still mutates those shared inner objects on the cached - # config returned by _load_config_for_gpu_estimate. Deepcopy isolates them. - config_copy = _copy.deepcopy(config) - - model_class = None - for auto_model in (AutoModelForCausalLM, AutoModel): - mapping = getattr(auto_model, "_model_mapping", None) - if mapping is None: - continue - try: - if config_copy.__class__ in mapping: - model_class = mapping[config_copy.__class__] - break - except Exception: - continue - - return resolve_attention_implementation(model_class, config_copy) - - def _estimate_fp16_model_size_bytes_from_config(config) -> Optional[int]: from .vram_estimation import extract_arch_config, compute_total_params @@ -961,21 +844,12 @@ def estimate_fp16_model_size_bytes( return int(total_params * 2), "safetensors" config = _load_config_for_gpu_estimate(estimate_model, hf_token = hf_token) - config_bytes: Optional[int] = None if config is not None: config_bytes = _estimate_fp16_model_size_bytes_from_config(config) + if config_bytes is not None: + return config_bytes, "config" local_bytes = _get_local_weight_size_bytes(estimate_model) - - # why: config-derived bytes cover only the text tower; local safetensors - # include vision/audio towers. Take the larger so the multimodal - # extra_bytes correction can fire. - if config_bytes is not None and local_bytes is not None: - if local_bytes > config_bytes: - return local_bytes, "weight_bytes" - return config_bytes, "config" - if config_bytes is not None: - return config_bytes, "config" if local_bytes is not None: return local_bytes, "weight_bytes" @@ -1003,9 +877,6 @@ def estimate_required_model_memory_gb( TrainingVramConfig, extract_arch_config, estimate_training_vram, - compute_total_params, - compute_optimizer_bytes, - compute_gradient_bytes, CUDA_OVERHEAD_BYTES, QUANT_4BIT_FACTOR, DEFAULT_TARGET_MODULES, @@ -1055,44 +926,13 @@ def estimate_required_model_memory_gb( model_name, hf_token = hf_token ) config = _load_config_for_gpu_estimate(estimate_model, hf_token = hf_token) - if config is not None: - try: - vram_config.attention_implementation = ( - _determine_attention_impl_for_gpu_estimate(config) - ) - except Exception as e: - logger.warning( - "Could not resolve attention implementation for '%s': %s", - estimate_model, - e, - ) - # why: if we cannot prove flash attention is usable, charge the - # quadratic non-flash activation path so GPU selection stays - # conservative. - vram_config.attention_implementation = "eager" arch = extract_arch_config(config) if config is not None else None if arch is not None: breakdown = estimate_training_vram(arch, vram_config) - # why: extract_arch_config only sees text_config; safetensors include - # vision/audio tower bytes that the text-arch fp16 total misses. - arch_fp16_bytes = compute_total_params(arch) * 2 - extra_bytes = max(0, int(model_size_bytes) - arch_fp16_bytes) - if extra_bytes > 0: - breakdown.model_weights += extra_bytes - if training_method == "full": - # why: full fine-tuning makes the extra (vision/audio) params - # trainable; optimizer + gradient bytes scale with them too. - extra_params = extra_bytes // 2 - breakdown.optimizer_states += compute_optimizer_bytes( - extra_params, - vram_config.optimizer, - ) - breakdown.gradients += compute_gradient_bytes(extra_params) required_gb = breakdown.total / (1024**3) metadata["required_gb"] = round(required_gb, 3) metadata["estimation_mode"] = "detailed" - metadata["attention_implementation"] = vram_config.attention_implementation metadata["vram_breakdown"] = breakdown.to_gb_dict() max_gpus = max(1, get_visible_gpu_count()) for n_gpus in range(1, max_gpus + 1): diff --git a/studio/backend/utils/hardware/nvidia.py b/studio/backend/utils/hardware/nvidia.py index 099c5fa3a5..dc5295c302 100644 --- a/studio/backend/utils/hardware/nvidia.py +++ b/studio/backend/utils/hardware/nvidia.py @@ -6,11 +6,6 @@ from loggers import get_logger -from utils.native_path_leases import child_env_without_native_path_secret -from utils.subprocess_compat import ( - windows_hidden_subprocess_kwargs as _windows_hidden_subprocess_kwargs, -) - logger = get_logger(__name__) @@ -66,8 +61,6 @@ def get_physical_gpu_count() -> Optional[int]: capture_output = True, text = True, timeout = 5, - env = child_env_without_native_path_secret(), - **_windows_hidden_subprocess_kwargs(), ) if result.returncode == 0 and result.stdout.strip(): return len(result.stdout.strip().splitlines()) @@ -92,8 +85,6 @@ def get_primary_gpu_utilization() -> dict[str, Any]: capture_output = True, text = True, timeout = 5, - env = child_env_without_native_path_secret(), - **_windows_hidden_subprocess_kwargs(), ) except (OSError, subprocess.TimeoutExpired) as e: logger.warning("nvidia-smi query failed in get_primary_gpu_utilization: %s", e) @@ -144,8 +135,6 @@ def get_visible_gpu_utilization( capture_output = True, text = True, timeout = 5, - env = child_env_without_native_path_secret(), - **_windows_hidden_subprocess_kwargs(), ) except (OSError, subprocess.TimeoutExpired) as e: logger.warning("nvidia-smi query failed in get_visible_gpu_utilization: %s", e) @@ -231,8 +220,6 @@ def get_backend_visible_gpu_info( capture_output = True, text = True, timeout = 10, - env = child_env_without_native_path_secret(), - **_windows_hidden_subprocess_kwargs(), ) except (OSError, subprocess.TimeoutExpired) as e: logger.warning("nvidia-smi query failed in get_backend_visible_gpu_info: %s", e) diff --git a/studio/backend/utils/hardware/vram_estimation.py b/studio/backend/utils/hardware/vram_estimation.py index ba1b1dfe61..e03665374d 100644 --- a/studio/backend/utils/hardware/vram_estimation.py +++ b/studio/backend/utils/hardware/vram_estimation.py @@ -16,26 +16,7 @@ from typing import Dict, Optional QUANT_4BIT_FACTOR = 16 / 5 -DOUBLE_QUANT_4BIT_FACTOR = ( - 3.6 # bnb_4bit_use_double_quant; see VRAM_ESTIMATION.md section 1 -) CUDA_OVERHEAD_BYTES = int(1.4 * 1024**3) # calibrated on RTX 5070 Ti -NON_FLASH_ATTENTION_FACTOR = ( - 12.0 # eager attention score+workspace overhead; see VRAM_ESTIMATION.md section 5 -) - -LINEAR_ATTENTION_IMPLS = frozenset({"flash_attention_2", "sdpa", "flex_attention"}) - -_SKIP_MODULE_TEXT_PREFIXES = frozenset( - { - "model", - "model.model", - "language_model", - "language_model.model", - "model.language_model", - "model.language_model.model", - } -) DEFAULT_TARGET_MODULES = [ "q_proj", @@ -46,8 +27,6 @@ "up_proj", "down_proj", ] -ATTENTION_TARGET_MODULES = {"q_proj", "k_proj", "v_proj", "o_proj"} -MLP_TARGET_MODULES = {"gate_proj", "up_proj", "down_proj"} # Empirically calibrated bytes/param — see VRAM_ESTIMATION.md for rationale. OPTIMIZER_BYTES_PER_PARAM: Dict[str, int] = { @@ -82,28 +61,12 @@ class ModelArchConfig: num_experts: Optional[int] = None moe_intermediate_size: Optional[int] = None n_shared_experts: int = 0 - shared_expert_intermediate_size: Optional[int] = None - num_experts_per_tok: int = 1 num_dense_layers: int = 0 q_lora_rank: Optional[int] = None kv_lora_rank: Optional[int] = None qk_nope_head_dim: Optional[int] = None qk_rope_head_dim: Optional[int] = None v_head_dim: Optional[int] = None - head_dim: Optional[int] = None - global_head_dim: Optional[int] = None - num_global_key_value_heads: Optional[int] = None - attention_k_eq_v: bool = False - layer_types: Optional[list] = None - num_kv_shared_layers: int = 0 - use_double_wide_mlp: bool = False - vocab_size_per_layer_input: int = 0 - hidden_size_per_layer_input: int = 0 - quantization_skip_modules: list = field(default_factory = list) - quant_4bit_factor: float = QUANT_4BIT_FACTOR - moe_has_dense_mlp: bool = False - dense_layer_indices: tuple = () - dense_intermediate_size: Optional[int] = None @dataclass @@ -116,7 +79,6 @@ class TrainingVramConfig: gradient_checkpointing: str = "unsloth" optimizer: str = "adamw_8bit" load_in_4bit: bool = True - attention_implementation: str = "flash_attention_2" @dataclass @@ -127,8 +89,8 @@ class VramBreakdown: gradients: int activations: int cuda_overhead: int - # Equals `activations`; retained for backward compatibility with - # consumers that read this field. + # The computed (formula-based) activation cost before floors. + # This is the true per-layer cost that doesn't shard across GPUs. activations_computed: int = 0 @property @@ -146,15 +108,17 @@ def min_gpu_vram(self, n_gpus: int) -> int: """Minimum VRAM a single GPU needs: its shard + non-shardable costs. Weights/LoRA/optimizer/gradients shard across GPUs. - Activations do NOT shard (the GPU running a layer holds them). + The computed activation cost does NOT shard (one GPU runs the layer). + The floor portion (activations - computed) is overhead that shards. """ shardable = ( self.model_weights + self.lora_adapters + self.optimizer_states + self.gradients + + (self.activations - self.activations_computed) # floor overhead shards ) - per_gpu_fixed = self.activations + self.cuda_overhead + per_gpu_fixed = self.activations_computed + self.cuda_overhead return shardable // max(n_gpus, 1) + per_gpu_fixed def to_gb_dict(self) -> Dict[str, float]: @@ -169,88 +133,28 @@ def to_gb_dict(self) -> Dict[str, float]: } -def _first_scalar(value): - # why: ERNIE MoE configs ship moe_intermediate_size / moe_num_experts as - # [routed, shared] lists; downstream arithmetic needs the routed scalar. - if isinstance(value, (list, tuple)): - return value[0] if value else None - return value - - -def _max_scalar(value): - # why: Hunyuan-V1-MoE moe_topk can be a per-layer list; activation - # accounting uses the max top-k as a conservative upper bound. - if isinstance(value, (list, tuple)): - items = [v for v in value if v is not None] - return max(items) if items else None - return value - - -def _compute_dense_layer_indices(text_config, total_layers: int) -> tuple: - """Layer indices that use dense MLP instead of MoE. Position matters.""" - # why: transformers Exaone-MoE / Laguna / Hy_v3 / GLM-MoE-DSA / GLM4-MoE-Lite / - # Ernie4_5_VL_MoE prefer per-position `mlp_layer_types` over the prefix-style - # `first_k_dense_replace` and may omit `decoder_sparse_step` entirely. - layer_types = getattr(text_config, "mlp_layer_types", None) - if layer_types: - return tuple( - i - for i, t in enumerate(layer_types[:total_layers]) - if str(t).lower() == "dense" - ) - - # why: Llama4TextConfig.__init__ auto-populates self.moe_layers from - # interleave_moe_layer_step; Llama4TextDecoderLayer dispatches via - # `layer_idx in config.moe_layers` (modeling_llama4.py). - llama4_moe_layers = getattr(text_config, "moe_layers", None) - if llama4_moe_layers is not None: - moe_indices = {int(i) for i in llama4_moe_layers} - return tuple(i for i in range(total_layers) if i not in moe_indices) - - # why: transformers ERNIE 4.5 MoE / ERNIE 4.5 VL MoE declare MoE layers - # via moe_layer_start_index / moe_layer_end_index / moe_layer_interval; - # the model's per-layer guard is `(layer_idx + 1) % interval == 0` with - # start <= layer_idx <= end (modeling_ernie4_5_moe.py). - moe_start = getattr(text_config, "moe_layer_start_index", None) - moe_interval = getattr(text_config, "moe_layer_interval", None) - if moe_start is not None and moe_interval is not None and int(moe_interval) > 0: - moe_end_raw = getattr(text_config, "moe_layer_end_index", None) - end = ( - total_layers - if moe_end_raw is None or int(moe_end_raw) == -1 - else min(int(moe_end_raw) + 1, total_layers) - ) - start = max(0, int(moe_start)) - interval = int(moe_interval) - moe_indices = {i for i in range(start, end) if (i + 1) % interval == 0} - return tuple(i for i in range(total_layers) if i not in moe_indices) - +def _compute_num_dense_layers(text_config, total_layers: int) -> int: + """Count how many layers use dense MLP instead of MoE.""" first_k = getattr(text_config, "first_k_dense_replace", None) if first_k is not None: - return tuple(range(min(int(first_k), total_layers))) + return min(int(first_k), total_layers) sparse_step = getattr(text_config, "decoder_sparse_step", None) mlp_only = getattr(text_config, "mlp_only_layers", None) or [] if sparse_step is not None and sparse_step > 0: - mlp_only_set = {int(i) for i in mlp_only} - return tuple( - i + mlp_only_set = set(mlp_only) + moe_count = sum( + 1 for i in range(total_layers) - if i in mlp_only_set or (i + 1) % sparse_step != 0 + if i not in mlp_only_set and (i + 1) % sparse_step == 0 ) - return () + return total_layers - moe_count + + return 0 def extract_arch_config(hf_config) -> Optional[ModelArchConfig]: text_config = getattr(hf_config, "text_config", None) or hf_config - quantization_config = getattr(hf_config, "quantization_config", None) or {} - if not isinstance(quantization_config, dict): - quantization_config = getattr(quantization_config, "to_dict", lambda: {})() - quant_4bit_factor = ( - DOUBLE_QUANT_4BIT_FACTOR - if quantization_config.get("bnb_4bit_use_double_quant", False) - else QUANT_4BIT_FACTOR - ) hidden_size = getattr(text_config, "hidden_size", None) num_layers = getattr(text_config, "num_hidden_layers", None) @@ -273,75 +177,18 @@ def extract_arch_config(hf_config) -> Optional[ModelArchConfig]: num_kv_heads = getattr(text_config, "num_key_value_heads", num_heads) - # why: DBRX places its MoE attrs on the DbrxFFNConfig sub-config; probe - # ffn_config as a secondary source so DBRX is not misclassified as dense. - ffn_config = getattr(text_config, "ffn_config", None) - - def _moe_attr(name): - value = getattr(text_config, name, None) - if value is None and ffn_config is not None: - value = getattr(ffn_config, name, None) - return value - num_experts = None - for attr in ( - "num_local_experts", - "num_experts", - "n_routed_experts", - "moe_num_experts", - ): - num_experts = _first_scalar(_moe_attr(attr)) + for attr in ("num_local_experts", "num_experts", "n_routed_experts"): + num_experts = getattr(text_config, attr, None) if num_experts is not None: break - moe_intermediate_raw = _moe_attr("moe_intermediate_size") - if moe_intermediate_raw is None: - moe_intermediate_raw = _moe_attr("ffn_hidden_size") - moe_intermediate = _first_scalar(moe_intermediate_raw) - # why: Exaone-MoE / ERNIE families alias num_shared_experts / - # moe_num_shared_experts to the canonical n_shared_experts. - n_shared_experts = ( - _first_scalar(_moe_attr("n_shared_experts")) - or _first_scalar(_moe_attr("num_shared_experts")) - or _first_scalar(_moe_attr("moe_num_shared_experts")) - or 0 - ) - shared_expert_intermediate_size = _moe_attr("shared_expert_intermediate_size") - if shared_expert_intermediate_size and n_shared_experts == 0: - n_shared_experts = 1 - # why: DBRX exposes moe_top_k, Hunyuan-V1-MoE exposes moe_topk (which can - # be a per-layer list); _max_scalar normalizes list values to the worst - # case so int(...) below cannot crash on the canonical attribute_map path. - num_experts_per_tok = ( - _max_scalar(_moe_attr("num_experts_per_tok")) - or _max_scalar(_moe_attr("top_k_experts")) - or _max_scalar(_moe_attr("moe_top_k")) - or _max_scalar(_moe_attr("moe_topk")) - or 1 - ) + moe_intermediate = getattr(text_config, "moe_intermediate_size", None) + n_shared_experts = getattr(text_config, "n_shared_experts", None) or 0 - dense_layer_indices: tuple = () + num_dense_layers = 0 if num_experts is not None and num_experts > 1: - dense_layer_indices = _compute_dense_layer_indices(text_config, num_layers) - num_dense_layers = len(dense_layer_indices) - - # why: Llama4 dense layers use intermediate_size_mlp; routed and shared - # experts use intermediate_size. Llama4TextMoe builds one shared_expert - # per MoE layer (modeling_llama4.py). - intermediate_size_mlp_raw = _first_scalar(_moe_attr("intermediate_size_mlp")) - dense_intermediate_size = ( - int(intermediate_size_mlp_raw) - if intermediate_size_mlp_raw is not None - else None - ) - if ( - intermediate_size_mlp_raw is not None - and num_experts is not None - and num_experts > 1 - and shared_expert_intermediate_size is None - and n_shared_experts == 0 - ): - n_shared_experts = 1 + num_dense_layers = _compute_num_dense_layers(text_config, num_layers) q_lora_rank = getattr(text_config, "q_lora_rank", None) kv_lora_rank = getattr(text_config, "kv_lora_rank", None) @@ -360,416 +207,13 @@ def _moe_attr(name): num_experts = num_experts, moe_intermediate_size = moe_intermediate, n_shared_experts = n_shared_experts, - shared_expert_intermediate_size = shared_expert_intermediate_size, - num_experts_per_tok = int(num_experts_per_tok), num_dense_layers = num_dense_layers, q_lora_rank = q_lora_rank, kv_lora_rank = kv_lora_rank, qk_nope_head_dim = qk_nope_head_dim, qk_rope_head_dim = qk_rope_head_dim, v_head_dim = v_head_dim, - head_dim = getattr(text_config, "head_dim", None), - global_head_dim = getattr(text_config, "global_head_dim", None), - num_global_key_value_heads = getattr( - text_config, - "num_global_key_value_heads", - None, - ), - attention_k_eq_v = bool(getattr(text_config, "attention_k_eq_v", False)), - layer_types = getattr(text_config, "layer_types", None), - num_kv_shared_layers = getattr(text_config, "num_kv_shared_layers", None) or 0, - use_double_wide_mlp = bool(getattr(text_config, "use_double_wide_mlp", False)), - vocab_size_per_layer_input = getattr( - text_config, - "vocab_size_per_layer_input", - None, - ) - or 0, - hidden_size_per_layer_input = getattr( - text_config, - "hidden_size_per_layer_input", - None, - ) - or 0, - quantization_skip_modules = list( - quantization_config.get("llm_int8_skip_modules", []) or [] - ), - quant_4bit_factor = quant_4bit_factor, - moe_has_dense_mlp = bool(getattr(text_config, "enable_moe_block", False)), - dense_layer_indices = dense_layer_indices, - dense_intermediate_size = dense_intermediate_size, - ) - - -def _targets_all_linear(target_modules) -> bool: - # why: peft LoraConfig accepts target_modules="all-linear" as a bare - # string; iterating a string yields chars and never matches the set. - if isinstance(target_modules, str): - target_modules = [target_modules] - normalized = {str(module).lower().replace("_", "-") for module in target_modules} - return normalized == {"all-linear"} - - -def _head_dim(arch: ModelArchConfig) -> int: - return arch.head_dim or arch.hidden_size // arch.num_attention_heads - - -def _layer_types(arch: ModelArchConfig) -> list: - if arch.layer_types and len(arch.layer_types) == arch.num_hidden_layers: - return arch.layer_types - return ["full_attention"] * arch.num_hidden_layers - - -def _uses_structured_layer_shapes(arch: ModelArchConfig) -> bool: - # MLA configs have their own q/kv low-rank projection shape formulas in - # _compute_attn_elements / _lora_attn_elements; do not let head_dim or - # other structured fields override that path. - if arch.q_lora_rank is not None: - return False - return bool( - arch.layer_types - or arch.head_dim is not None - or arch.global_head_dim is not None - or arch.num_global_key_value_heads is not None - or arch.attention_k_eq_v - or arch.num_kv_shared_layers > 0 - or arch.use_double_wide_mlp - ) - - -def _is_kv_shared_layer(arch: ModelArchConfig, layer_idx: int) -> bool: - if arch.num_kv_shared_layers <= 0: - return False - first_shared = arch.num_hidden_layers - arch.num_kv_shared_layers - # why: transformers Gemma4 (modeling_gemma4.py:1031, modular_gemma4.py:863) - # uses the same `> 0` guard so a fully-shared config raises during model - # construction; matching upstream avoids producing a detailed estimate - # for a shape the actual model code rejects. - return layer_idx >= first_shared > 0 - - -def _is_dense_mlp_layer(arch: ModelArchConfig, layer_idx: int) -> bool: - if arch.dense_layer_indices: - return layer_idx in arch.dense_layer_indices - return layer_idx < arch.num_dense_layers - - -def _per_layer_input_quantizable(arch: ModelArchConfig) -> int: - # why: Gemma4 PLE block adds per_layer_model_projection (single Linear), - # per_layer_input_gate (per layer), and per_layer_projection (per layer); - # see transformers gemma4/modular_gemma4.py:1077-1083 and :1247-1253. - pli = arch.hidden_size_per_layer_input - if pli <= 0: - return 0 - n_layers = arch.num_hidden_layers - hd = arch.hidden_size - return hd * (n_layers * pli) + (hd * pli) * n_layers + (pli * hd) * n_layers - - -def _per_layer_input_norm_elements(arch: ModelArchConfig) -> int: - pli = arch.hidden_size_per_layer_input - if pli <= 0: - return 0 - n_layers = arch.num_hidden_layers - hd = arch.hidden_size - return hd * n_layers + pli - - -def _per_layer_input_lora_params( - arch: ModelArchConfig, - r: int, - target_modules, -) -> int: - # why: Unsloth's get_peft_regex (unsloth_zoo/peft_utils.py) requires module - # names to contain a component tag (mlp/attn/...); PLE module names lack - # any tag, so all-linear training does NOT attach LoRA to them. Only count - # PLE LoRA when the user explicitly names PLE modules. - pli = arch.hidden_size_per_layer_input - if pli <= 0: - return 0 - targets = ( - {target_modules} - if isinstance(target_modules, str) - else set(target_modules or []) - ) - n_layers = arch.num_hidden_layers - hd = arch.hidden_size - total = 0 - if "per_layer_model_projection" in targets: - total += hd * r + r * (n_layers * pli) - if "per_layer_input_gate" in targets: - total += (hd * r + r * pli) * n_layers - if "per_layer_projection" in targets: - total += (pli * r + r * hd) * n_layers - return total - - -def _layer_attention_dims(arch: ModelArchConfig, layer_idx: int) -> tuple: - layer_types = _layer_types(arch) - layer_type = layer_types[layer_idx] - is_sliding = layer_type == "sliding_attention" - head_dim = ( - arch.global_head_dim - if not is_sliding and arch.global_head_dim - else _head_dim(arch) - ) - use_alt_attention = arch.attention_k_eq_v and not is_sliding - num_kv_heads = ( - arch.num_global_key_value_heads - if use_alt_attention and arch.num_global_key_value_heads - else arch.num_key_value_heads - ) - q_size = arch.num_attention_heads * head_dim - kv_size = num_kv_heads * head_dim - has_k = not _is_kv_shared_layer(arch, layer_idx) - has_v = has_k and not use_alt_attention - return q_size, kv_size, has_k, has_v - - -def _layer_mlp_size(arch: ModelArchConfig, layer_idx: int) -> int: - if arch.use_double_wide_mlp and _is_kv_shared_layer(arch, layer_idx): - return _dense_mlp_size(arch) * 2 - return _dense_mlp_size(arch) - - -def _text_linear_dims( - arch: ModelArchConfig, - layer_idx: int, -) -> Dict[str, tuple[int, int]]: - hd = arch.hidden_size - if _uses_structured_layer_shapes(arch): - q_size, kv_size, has_k, has_v = _layer_attention_dims(arch, layer_idx) - mlp_size = _layer_mlp_size(arch, layer_idx) - else: - q_size = hd - kv_size = _get_kv_size(arch) - has_k = True - has_v = True - mlp_size = _get_mlp_size(arch) - - dims = { - "q_proj": (hd, q_size), - "o_proj": (q_size, hd), - } - if has_k: - dims["k_proj"] = (hd, kv_size) - if has_v: - dims["v_proj"] = (hd, kv_size) - - dims.update( - { - "gate_proj": (hd, mlp_size), - "up_proj": (hd, mlp_size), - "down_proj": (mlp_size, hd), - } ) - return dims - - -def _module_path_matches(skip_module: str, alias: str) -> bool: - skip_parts = [part for part in skip_module.split(".") if part] - alias_parts = [part for part in alias.split(".") if part] - if not skip_parts or not alias_parts: - return False - if alias_parts[0] == "layers": - return skip_parts == alias_parts - if len(skip_parts) <= len(alias_parts): - # why: transformers BNB quantizer suffix-matches short skip entries - # like ["q_proj"] / ["lm_head"] against full module paths, so a skip - # shorter than the alias is a tail match. - return alias_parts[-len(skip_parts) :] == skip_parts - if skip_parts[-len(alias_parts) :] != alias_parts: - return False - prefix_parts = skip_parts[: len(skip_parts) - len(alias_parts)] - if not prefix_parts: - return True - # why: bound the prefix to known text-tower roots so VLM skip names like - # vision_tower.model.layers..self_attn.q_proj do not shadow the text - # alias model.layers..self_attn.q_proj. - return ".".join(prefix_parts) in _SKIP_MODULE_TEXT_PREFIXES - - -def _add_module_aliases( - aliases: Dict[str, str], - canonical: str, - suffix: str, -) -> None: - for prefix in ( - "", - "model", - "model.model", - "language_model", - "language_model.model", - "model.language_model", - "model.language_model.model", - ): - alias = f"{prefix}.{suffix}" if prefix else suffix - aliases[alias] = canonical - - -def _build_text_module_elements( - arch: ModelArchConfig, -) -> tuple[Dict[str, int], Dict[str, str]]: - elements: Dict[str, int] = {} - aliases: Dict[str, str] = {} - - is_mla = arch.q_lora_rank is not None and not _uses_structured_layer_shapes(arch) - pli = arch.hidden_size_per_layer_input - hd_global = arch.hidden_size - - for layer_idx in range(arch.num_hidden_layers): - layer_modules: Dict[str, int] = {} - dims = _text_linear_dims(arch, layer_idx) - attn_dims = { - name: dim for name, dim in dims.items() if name in ATTENTION_TARGET_MODULES - } - mlp_dims = { - name: dim for name, dim in dims.items() if name in MLP_TARGET_MODULES - } - - if is_mla: - # why: _text_linear_dims uses (hd, hd) for q/o; MLA actually splits - # into q_a/q_b/kv_a/kv_b, so emit a single self_attn aggregate at - # the authoritative MLA per-layer total. - layer_modules["self_attn"] = _compute_attn_elements(arch) - else: - for name, (in_dim, out_dim) in attn_dims.items(): - layer_modules[f"self_attn.{name}"] = in_dim * out_dim - - if arch.num_experts and arch.num_experts > 1: - if _is_dense_mlp_layer(arch, layer_idx): - layer_modules.update( - { - f"mlp.{name}": in_dim * out_dim - for name, (in_dim, out_dim) in mlp_dims.items() - } - ) - else: - layer_modules["mlp.experts"] = _compute_routed_moe_elements(arch) - shared_moe = _compute_shared_moe_elements(arch) - if shared_moe: - # why: Qwen3.5-MoE exposes shared expert as - # mlp.shared_expert; Exaone-MoE/Laguna/GLM-style configs use - # mlp.shared_experts. Register both names so child-path - # llm_int8_skip_modules entries match the right shared block. - layer_modules["mlp.shared_expert"] = shared_moe - if arch.moe_has_dense_mlp: - # why: enable_moe_block runs the dense MLP and the MoE - # experts in parallel; register both for skip matching. - # Non-structured _text_linear_dims returns mlp_size from - # _get_mlp_size which prefers moe_intermediate_size, so - # rebuild dense dims from arch.intermediate_size directly. - if _uses_structured_layer_shapes(arch): - dense_dims = mlp_dims - else: - hd = arch.hidden_size - inter = arch.intermediate_size - dense_dims = { - "gate_proj": (hd, inter), - "up_proj": (hd, inter), - "down_proj": (inter, hd), - } - layer_modules.update( - { - f"mlp.{name}": in_dim * out_dim - for name, (in_dim, out_dim) in dense_dims.items() - } - ) - else: - layer_modules.update( - { - f"mlp.{name}": in_dim * out_dim - for name, (in_dim, out_dim) in mlp_dims.items() - } - ) - - if pli > 0: - # why: register PLE per-layer linears so llm_int8_skip_modules - # entries like model.layers.0.per_layer_input_gate match. - layer_modules["per_layer_input_gate"] = hd_global * pli - layer_modules["per_layer_projection"] = pli * hd_global - - attn_total = sum( - value - for name, value in layer_modules.items() - if name == "self_attn" or name.startswith("self_attn.") - ) - # why: gemma4 enable_moe_block puts routed experts at the sibling - # layers..experts attribute, not under self.mlp; the layer's "mlp" - # aggregate must reflect only the dense MLP path so a skip module - # `model.layers.0.mlp` does not over-skip into the experts block. - is_sibling_experts = bool(arch.moe_has_dense_mlp) - mlp_total = sum( - value - for name, value in layer_modules.items() - if ( - name == "mlp" - or ( - name.startswith("mlp.") - and not (is_sibling_experts and name == "mlp.experts") - ) - ) - ) - experts_total = layer_modules.get("mlp.experts", 0) if is_sibling_experts else 0 - layer_total = sum(layer_modules.values()) - - aggregate_modules = { - f"text.layers.{layer_idx}": layer_total, - f"text.layers.{layer_idx}.self_attn": attn_total, - f"text.layers.{layer_idx}.mlp": mlp_total, - } - if experts_total: - aggregate_modules[f"text.layers.{layer_idx}.experts"] = experts_total - elements.update(aggregate_modules) - for canonical in aggregate_modules: - suffix = canonical.removeprefix("text.") - _add_module_aliases(aliases, canonical, suffix) - - for name, value in layer_modules.items(): - canonical = f"text.layers.{layer_idx}.{name}" - elements[canonical] = value - _add_module_aliases(aliases, canonical, canonical.removeprefix("text.")) - if name == "mlp.experts" and arch.moe_has_dense_mlp: - # why: gemma4 enable_moe_block exposes routed experts at - # layers..experts (sibling of self.mlp), not under mlp. - _add_module_aliases(aliases, canonical, f"layers.{layer_idx}.experts") - elif name == "mlp.shared_expert": - # why: Exaone-MoE / Laguna / GLM-style configs use the plural - # `shared_experts` attribute name; register both spellings. - _add_module_aliases( - aliases, - canonical, - f"layers.{layer_idx}.mlp.shared_experts", - ) - - if pli > 0: - canonical = "text.per_layer_model_projection" - elements[canonical] = hd_global * (arch.num_hidden_layers * pli) - _add_module_aliases(aliases, canonical, canonical.removeprefix("text.")) - - return elements, aliases - - -def _compute_skipped_quantizable_elements(arch: ModelArchConfig) -> int: - if not arch.quantization_skip_modules: - return 0 - - module_elements, aliases = _build_text_module_elements(arch) - matched = set() - for skip_module in arch.quantization_skip_modules: - for alias, canonical in aliases.items(): - if _module_path_matches(skip_module, alias): - matched.add(canonical) - - pruned = { - canonical - for canonical in matched - if not any( - canonical != parent and canonical.startswith(f"{parent}.") - for parent in matched - ) - } - return sum(module_elements[canonical] for canonical in pruned) def _get_kv_size(arch: ModelArchConfig) -> int: @@ -782,12 +226,6 @@ def _get_mlp_size(arch: ModelArchConfig) -> int: return arch.intermediate_size -def _dense_mlp_size(arch: ModelArchConfig) -> int: - # why: Llama4 dense layers use intermediate_size_mlp; routed/shared - # experts use intermediate_size. Other configs leave the field None. - return arch.dense_intermediate_size or arch.intermediate_size - - def _get_num_experts(arch: ModelArchConfig) -> int: return arch.num_experts if arch.num_experts and arch.num_experts > 1 else 1 @@ -810,39 +248,14 @@ def _compute_attn_elements(arch: ModelArchConfig) -> int: def _compute_dense_mlp_elements(arch: ModelArchConfig) -> int: - return arch.hidden_size * _dense_mlp_size(arch) * 3 - + return arch.hidden_size * arch.intermediate_size * 3 -def _shared_expert_size(arch: ModelArchConfig) -> int: - # why: Qwen3.5-MoE shared expert has its own intermediate_size (default 512) - # distinct from moe_intermediate_size; fall back to routed mlp_size for - # families that share it (deepseek-style configs). - return arch.shared_expert_intermediate_size or _get_mlp_size(arch) - -def _compute_routed_moe_elements(arch: ModelArchConfig) -> int: +def _compute_moe_mlp_elements(arch: ModelArchConfig) -> int: hd = arch.hidden_size + mlp_size = _get_mlp_size(arch) n_experts = _get_num_experts(arch) - return hd * _get_mlp_size(arch) * 3 * n_experts + n_experts * hd - - -def _compute_shared_moe_elements(arch: ModelArchConfig) -> int: - if not arch.n_shared_experts: - return 0 - hd = arch.hidden_size - shared_size = _shared_expert_size(arch) - total = hd * shared_size * 3 * arch.n_shared_experts - # why: only Qwen2-MoE / Qwen3.5-MoE define a shared_expert_gate Linear - # (hidden_size→1); other families (Exaone-MoE, HY-V3, GLM4-MoE-Lite, Laguna) - # have shared_experts without a gate. shared_expert_intermediate_size is the - # Qwen-style discriminator. - if arch.shared_expert_intermediate_size: - total += arch.n_shared_experts * hd - return total - - -def _compute_moe_mlp_elements(arch: ModelArchConfig) -> int: - return _compute_routed_moe_elements(arch) + _compute_shared_moe_elements(arch) + return hd * mlp_size * 3 * (n_experts + arch.n_shared_experts) + n_experts * hd def _compute_layer_elements(arch: ModelArchConfig): @@ -854,60 +267,22 @@ def _compute_layer_elements(arch: ModelArchConfig): n_layers = arch.num_hidden_layers n_experts = _get_num_experts(arch) - if _uses_structured_layer_shapes(arch): - attn_total = 0 - per_layer_dense_mlp = [] - for layer_idx in range(n_layers): - layer_dense_mlp = 0 - for name, (in_dim, out_dim) in _text_linear_dims( - arch, - layer_idx, - ).items(): - elements = in_dim * out_dim - if name in ATTENTION_TARGET_MODULES: - attn_total += elements - elif name in MLP_TARGET_MODULES: - layer_dense_mlp += elements - per_layer_dense_mlp.append(layer_dense_mlp) - if n_experts > 1: - n_dense = arch.num_dense_layers - n_moe = n_layers - n_dense - moe_mlp_total = _compute_moe_mlp_elements(arch) * n_moe - if arch.moe_has_dense_mlp: - # why: enable_moe_block runs dense MLP and MoE experts in - # parallel; count dense for every layer alongside MoE. - mlp_total = sum(per_layer_dense_mlp) + moe_mlp_total - else: - dense_only_total = sum( - value - for i, value in enumerate(per_layer_dense_mlp) - if _is_dense_mlp_layer(arch, i) - ) - mlp_total = moe_mlp_total + dense_only_total - else: - mlp_total = sum(per_layer_dense_mlp) - elif n_experts > 1: - attn_total = _compute_attn_elements(arch) * n_layers + attn_total = _compute_attn_elements(arch) * n_layers + + if n_experts > 1: n_dense = arch.num_dense_layers n_moe = n_layers - n_dense - moe_mlp_total = _compute_moe_mlp_elements(arch) * n_moe - if arch.moe_has_dense_mlp: - mlp_total = _compute_dense_mlp_elements(arch) * n_layers + moe_mlp_total - else: - mlp_total = moe_mlp_total + _compute_dense_mlp_elements(arch) * n_dense + mlp_total = ( + _compute_moe_mlp_elements(arch) * n_moe + + _compute_dense_mlp_elements(arch) * n_dense + ) else: - attn_total = _compute_attn_elements(arch) * n_layers mlp_total = _compute_dense_mlp_elements(arch) * n_layers layernorms = 2 * hd - per_layer_embed = ( - arch.vocab_size_per_layer_input * arch.hidden_size_per_layer_input * n_layers - ) - ple_text_linear = _per_layer_input_quantizable(arch) - ple_norms = _per_layer_input_norm_elements(arch) - embed_tokens = arch.vocab_size * hd + per_layer_embed + ple_norms + embed_tokens = arch.vocab_size * hd lm_head = 0 if arch.tie_word_embeddings else arch.vocab_size * hd - return attn_total + mlp_total + ple_text_linear, layernorms, embed_tokens, lm_head + return attn_total + mlp_total, layernorms, embed_tokens, lm_head def compute_model_weights_bytes( @@ -920,16 +295,7 @@ def compute_model_weights_bytes( non_quantizable = layernorms * n_layers + embed_tokens + lm_head if training_method == "qlora" and load_in_4bit: - skipped_quantizable = min( - _compute_skipped_quantizable_elements(arch), - total_quantizable, - ) - quantized = total_quantizable - skipped_quantizable - return int( - quantized * 2 / arch.quant_4bit_factor - + skipped_quantizable * 2 - + non_quantizable * 2 - ) + return int(total_quantizable * 2 / QUANT_4BIT_FACTOR + non_quantizable * 2) return int((total_quantizable + non_quantizable) * 2) @@ -997,130 +363,46 @@ def compute_lora_params( lora_rank: int, target_modules: list, ) -> int: - all_linear = _targets_all_linear(target_modules) - selected_modules = list(DEFAULT_TARGET_MODULES) if all_linear else target_modules hd = arch.hidden_size r = lora_rank n_layers = arch.num_hidden_layers n_experts = _get_num_experts(arch) - use_structured_shapes = _uses_structured_layer_shapes(arch) - if use_structured_shapes: - attn_total = 0 - structured_dense_mlp = 0 - per_layer_dense_mlp = [] - for layer_idx in range(n_layers): - layer_dense = 0 - for name, (in_dim, out_dim) in _text_linear_dims( - arch, - layer_idx, - ).items(): - if name not in selected_modules: - continue - if name in ATTENTION_TARGET_MODULES: - attn_total += in_dim * r + r * out_dim - elif name in MLP_TARGET_MODULES: - layer_dense += in_dim * r + r * out_dim - per_layer_dense_mlp.append(layer_dense) - structured_dense_mlp += layer_dense - if n_experts > 1: - n_dense = arch.num_dense_layers - n_moe = n_layers - n_dense - # why: peft "all-linear" attaches LoRA to nn.Linear only; - # routed experts are nn.Parameter and need explicit - # gate_proj/up_proj/down_proj naming via Unsloth's - # get_moe_target_parameters. Shared experts are nn.Linear and - # are picked up by get_peft_regex. - routed_moe = ( - 0 - if all_linear - else _lora_mlp_elements( - hd, - _get_mlp_size(arch), - r, - selected_modules, - n_experts, - ) - ) - shared_moe = _lora_mlp_elements( - hd, - _shared_expert_size(arch), - r, - selected_modules, - arch.n_shared_experts, - ) - moe_mlp = routed_moe + shared_moe - if arch.moe_has_dense_mlp: - # why: parallel dense MLP coexists with MoE on every layer. - mlp_total = structured_dense_mlp + moe_mlp * n_moe - else: - dense_only = sum( - value - for i, value in enumerate(per_layer_dense_mlp) - if _is_dense_mlp_layer(arch, i) - ) - mlp_total = moe_mlp * n_moe + dense_only - else: - mlp_total = structured_dense_mlp - return ( - attn_total - + mlp_total - + _per_layer_input_lora_params(arch, r, target_modules) - ) - elif n_experts > 1: - attn_total = _lora_attn_elements(arch, r, selected_modules) * n_layers + attn_total = _lora_attn_elements(arch, r, target_modules) * n_layers + + if n_experts > 1: n_dense = arch.num_dense_layers n_moe = n_layers - n_dense - # why: routed and shared experts may use different intermediate sizes - # (Qwen3.5-MoE: routed mlp_size != shared_expert_intermediate_size). - # See structured branch for the all-linear exclusion rationale; only - # routed (nn.Parameter) experts are excluded under all-linear. - routed_moe = ( - 0 - if all_linear - else _lora_mlp_elements( - hd, - _get_mlp_size(arch), - r, - selected_modules, - n_experts, - ) - ) - shared_moe = _lora_mlp_elements( + # Include shared experts alongside routed experts + moe_expert_mult = n_experts + arch.n_shared_experts + moe_mlp = _lora_mlp_elements( hd, - _shared_expert_size(arch), + _get_mlp_size(arch), r, - selected_modules, - arch.n_shared_experts, + target_modules, + moe_expert_mult, ) - moe_mlp = routed_moe + shared_moe dense_mlp = _lora_mlp_elements( hd, - _dense_mlp_size(arch), + arch.intermediate_size, r, - selected_modules, + target_modules, 1, ) - if arch.moe_has_dense_mlp: - mlp_total = moe_mlp * n_moe + dense_mlp * n_layers - else: - mlp_total = moe_mlp * n_moe + dense_mlp * n_dense + mlp_total = moe_mlp * n_moe + dense_mlp * n_dense else: - attn_total = _lora_attn_elements(arch, r, selected_modules) * n_layers mlp_total = ( _lora_mlp_elements( hd, - _dense_mlp_size(arch), + arch.intermediate_size, r, - selected_modules, + target_modules, 1, ) * n_layers ) - return ( - attn_total + mlp_total + _per_layer_input_lora_params(arch, r, target_modules) - ) + return attn_total + mlp_total def compute_lora_adapter_bytes(lora_params: int) -> int: @@ -1137,88 +419,26 @@ def compute_gradient_bytes(trainable_params: int) -> int: return trainable_params * 2 -def _is_linear_attention(attention_implementation: Optional[str]) -> bool: - # why: PyTorch SDPA dispatches to flash/memory-efficient O(n) backends; only - # eager (and other non-flash impls) need the quadratic correction. - return attention_implementation in LINEAR_ATTENTION_IMPLS - - -def _compute_non_flash_attention_bytes( - arch: ModelArchConfig, - batch_size: int, - seq_len: int, - effective_layers: float, -) -> int: - score_elements = batch_size * arch.num_attention_heads * seq_len * seq_len - return int(score_elements * 2 * NON_FLASH_ATTENTION_FACTOR * effective_layers) - - -def _layer_qkv_mlp_sizes(arch: ModelArchConfig, layer_idx: int) -> tuple: - n_experts = _get_num_experts(arch) - is_moe_layer = n_experts > 1 and not _is_dense_mlp_layer(arch, layer_idx) - if _uses_structured_layer_shapes(arch): - q_size, kv_size, _has_k, _has_v = _layer_attention_dims(arch, layer_idx) - # why: KV-shared layers (Gemma4/Gemma3n) drop k_proj/v_proj WEIGHTS but - # the donor layer's K/V tensors stay alive across the shared range, so - # activation memory still pays for kv_size; only the weight path uses - # has_k/has_v. - layer_type = _layer_types(arch)[layer_idx] - use_alt_attention = arch.attention_k_eq_v and layer_type != "sliding_attention" - kv_count = 1 if use_alt_attention else 2 - qkv_size = q_size + kv_size * kv_count - if is_moe_layer: - # why: each token routes through `num_experts_per_tok` experts; their - # gate/up/down intermediates are all live during MLP forward. - mlp_size = _get_mlp_size(arch) * arch.num_experts_per_tok - if arch.n_shared_experts: - mlp_size += _shared_expert_size(arch) * arch.n_shared_experts - if arch.moe_has_dense_mlp: - mlp_size += _layer_mlp_size(arch, layer_idx) - else: - mlp_size = _layer_mlp_size(arch, layer_idx) - return qkv_size, mlp_size - kv_size = _get_kv_size(arch) - if is_moe_layer: - mlp_size = _get_mlp_size(arch) * arch.num_experts_per_tok - if arch.n_shared_experts: - mlp_size += _shared_expert_size(arch) * arch.n_shared_experts - if arch.moe_has_dense_mlp: - mlp_size += arch.intermediate_size - else: - mlp_size = _get_mlp_size(arch) - return arch.hidden_size + kv_size + kv_size, mlp_size - - -def _per_layer_activation_bytes( - arch: ModelArchConfig, - layer_idx: int, - batch_size: int, - seq_len: int, -) -> int: - qkv_size, mlp_size = _layer_qkv_mlp_sizes(arch, layer_idx) - activation_qkv = seq_len * batch_size * qkv_size - residual_memory = (seq_len * batch_size) * 2 - activation_mlp = seq_len * batch_size * (mlp_size + mlp_size) - # why: per_layer_input_gate (hd-sized) and per_layer_projection (pli-sized) - # outputs materialize once per decoder layer when hidden_size_per_layer_input - # is set; see gemma4/modular_gemma4.py:1141-1145. - pli = arch.hidden_size_per_layer_input - activation_ple = seq_len * batch_size * (arch.hidden_size + pli) if pli > 0 else 0 - return int( - (activation_qkv + residual_memory + activation_mlp + activation_ple) * 2 * 1.25 - ) - - def compute_activation_bytes( arch: ModelArchConfig, batch_size: int, seq_len: int, gradient_checkpointing: str, is_lora: bool = False, - attention_implementation: Optional[str] = "flash_attention_2", ) -> int: + hd = arch.hidden_size + kv_size = _get_kv_size(arch) + mlp_size = _get_mlp_size(arch) + bsz = batch_size n_layers = arch.num_hidden_layers + activation_qkv = seq_len * bsz * (hd + kv_size + kv_size) + residual_memory = (seq_len * bsz) * 2 + activation_mlp = seq_len * bsz * (mlp_size + mlp_size) + + per_layer_bytes = (activation_qkv + residual_memory + activation_mlp) * 2 + per_layer_bytes = int(per_layer_bytes * 1.25) + gc_key = gradient_checkpointing.lower() gc_entry = GC_LAYER_MULTIPLIERS.get(gc_key, (None, None)) full_ft_mult, lora_mult = gc_entry @@ -1226,35 +446,10 @@ def compute_activation_bytes( if gc_multiplier is None: effective_layers = n_layers - linear_bytes = sum( - _per_layer_activation_bytes(arch, i, batch_size, seq_len) - for i in range(n_layers) - ) else: effective_layers = gc_multiplier - max_layer_bytes = max( - _per_layer_activation_bytes(arch, i, batch_size, seq_len) - for i in range(n_layers) - ) - linear_bytes = int(max_layer_bytes * effective_layers) - - # why: gemma4 per_layer_model_projection runs once outside the per-decoder - # loop and materializes a [B, S, L, PLI] tensor; see modular_gemma4.py:1247. - pli = arch.hidden_size_per_layer_input - if pli > 0: - linear_bytes += int(seq_len * batch_size * n_layers * pli * 2 * 1.25) - - if _is_linear_attention(attention_implementation): - return linear_bytes - return max( - linear_bytes, - _compute_non_flash_attention_bytes( - arch, - batch_size, - seq_len, - effective_layers, - ), - ) + + return int(per_layer_bytes * effective_layers) def estimate_training_vram( @@ -1279,23 +474,21 @@ def estimate_training_vram( trainable_params = lora_params if is_lora else compute_total_params(arch) optimizer_bytes = compute_optimizer_bytes(trainable_params, config.optimizer) + gradient_bytes = max( + compute_gradient_bytes(trainable_params), + int(model_weights * 0.15), + ) activations_computed = compute_activation_bytes( arch, config.batch_size, config.max_seq_length, config.gradient_checkpointing, is_lora = is_lora, - attention_implementation = config.attention_implementation, ) - raw_gradient_bytes = compute_gradient_bytes(trainable_params) - gradient_floor = int(model_weights * 0.15) - if is_lora: - gradient_floor = min( - gradient_floor, - max(activations_computed, optimizer_bytes), - ) - gradient_bytes = max(raw_gradient_bytes, gradient_floor) - activation_bytes = activations_computed + activation_bytes = max( + activations_computed, + int(model_weights * 0.15 * (config.batch_size / 2)), + ) return VramBreakdown( model_weights = model_weights, diff --git a/studio/backend/utils/models/model_config.py b/studio/backend/utils/models/model_config.py index dc8dd08315..a2d48cf009 100644 --- a/studio/backend/utils/models/model_config.py +++ b/studio/backend/utils/models/model_config.py @@ -32,11 +32,6 @@ import yaml -from utils.native_path_leases import child_env_without_native_path_secret -from utils.subprocess_compat import ( - windows_hidden_subprocess_kwargs as _windows_hidden_subprocess_kwargs, -) - logger = get_logger(__name__) # ── Model size extraction ──────────────────────────────────── @@ -500,9 +495,7 @@ def load_model_config( # Pre-computed .venv_t5 paths and backend dir for subprocess version switching. # Vision check uses 5.5.0 (newest, recognizes all architectures). -from utils.paths.storage_roots import studio_root as _studio_root # noqa: E402 - -_VENV_T5_DIR = str(_studio_root() / ".venv_t5_550") +_VENV_T5_DIR = str(Path.home() / ".unsloth" / "studio" / ".venv_t5_550") _BACKEND_DIR = str(Path(__file__).resolve().parent.parent.parent) # Inline script executed in a subprocess with transformers 5.x activated. @@ -586,8 +579,6 @@ def _is_vision_model_subprocess( capture_output = True, text = True, timeout = 60, - env = child_env_without_native_path_secret(), - **_windows_hidden_subprocess_kwargs(), ) if result.returncode != 0: @@ -1231,11 +1222,9 @@ def _resolve_gguf_dir(p: Path) -> Optional[Path]: return p if p.is_file() and p.suffix.lower() == ".gguf": parent = p.parent - if ( - (parent / "config.json").exists() - or (parent / "adapter_config.json").exists() - or (parent / "export_metadata.json").exists() - ): + if (parent / "config.json").exists() or ( + parent / "adapter_config.json" + ).exists(): return parent return None diff --git a/studio/backend/utils/native_path_leases.py b/studio/backend/utils/native_path_leases.py deleted file mode 100644 index a69dfab532..0000000000 --- a/studio/backend/utils/native_path_leases.py +++ /dev/null @@ -1,406 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 - -"""Verification for Tauri native path signed grants. - -Rust signs compact ``base64url(payload_json).base64url(hmac)`` grants. The -frontend can see and forward the grant, but cannot change it without breaking -the HMAC. The backend verifies the original payload segment bytes, then -re-stats the path before any native read. -""" - -from __future__ import annotations - -import base64 -import binascii -import hashlib -import hmac -import json -import os -import stat as _stat_module -import threading -import time -from contextlib import contextmanager -from dataclasses import dataclass -from pathlib import Path -from typing import Any, Callable, Iterable, Iterator, Mapping - -LEASE_SECRET_ENV = "UNSLOTH_STUDIO_NATIVE_PATH_LEASE_SECRET" -_MAX_NATIVE_PATH_REDACTIONS = 100 -_MAX_NATIVE_PATH_LABELS = 10_000 -_MIN_LEASE_SECRET_BYTES = 32 - -_REPLAY_LOCK = threading.Lock() -_USED_NONCES: dict[str, int] = {} -_REDACTION_LOCK = threading.Lock() -_NATIVE_PATH_REDACTIONS: list[str] = [] -_NATIVE_PATH_LABELS: dict[str, str] = {} -_NATIVE_PATH_ENV_LOCK = threading.Lock() -_SECRET_INIT_LOCK = threading.Lock() -_CACHED_LEASE_SECRET: bytes | None = None -_SCRUB_REFCOUNT = 0 -_SCRUB_SAVED_SECRET: str | None = None - - -class NativePathLeaseError(ValueError): - """Raised when a native path grant is missing, invalid, or unsafe.""" - - -@dataclass(frozen = True) -class NativePathGrant: - operation: str - canonical_path: Path - path_kind: str - path_type: str - source_kind: str - token_id_hash: str - display_label: str - expires_at_ms: int - size_bytes: int | None - modified_ms: int | None - - -def native_path_leases_supported() -> bool: - try: - _decode_secret() - except NativePathLeaseError: - return False - return True - - -def child_env_without_native_path_secret( - env: Mapping[str, str] | None = None, -) -> dict[str, str]: - """Return a child-process env with the native path lease secret removed.""" - - if env is None: - with _NATIVE_PATH_ENV_LOCK: - cleaned = dict(os.environ) - else: - cleaned = dict(env) - cleaned.pop(LEASE_SECRET_ENV, None) - return cleaned - - -def run_without_native_path_secret( - target: Callable[..., Any], - *args: Any, - **kwargs: Any, -) -> Any: - """Run a multiprocessing child target without the native path lease secret.""" - - global _CACHED_LEASE_SECRET, _SCRUB_SAVED_SECRET - os.environ.pop(LEASE_SECRET_ENV, None) - _CACHED_LEASE_SECRET = None - _SCRUB_SAVED_SECRET = None - return target(*args, **kwargs) - - -@contextmanager -def native_path_secret_removed_for_child_start() -> Iterator[None]: - global _SCRUB_REFCOUNT, _SCRUB_SAVED_SECRET, _CACHED_LEASE_SECRET - with _NATIVE_PATH_ENV_LOCK: - if _SCRUB_REFCOUNT == 0: - _SCRUB_SAVED_SECRET = os.environ.pop(LEASE_SECRET_ENV, None) - _CACHED_LEASE_SECRET = None - _SCRUB_REFCOUNT += 1 - try: - yield - finally: - with _NATIVE_PATH_ENV_LOCK: - _SCRUB_REFCOUNT -= 1 - if _SCRUB_REFCOUNT == 0 and _SCRUB_SAVED_SECRET is not None: - os.environ[LEASE_SECRET_ENV] = _SCRUB_SAVED_SECRET - _SCRUB_SAVED_SECRET = None - - -def verify_native_path_lease( - lease: str | None, - *, - operation: str, - expected_kind: str | None = None, - expected_path_type: str | None = None, - allowed_suffixes: Iterable[str] | None = None, -) -> NativePathGrant: - if not lease: - raise NativePathLeaseError("Native path grant is required.") - - secret = _decode_secret() - payload_b64, signature_b64 = _split_lease(lease) - expected_signature = hmac.new( - secret, - payload_b64.encode("ascii"), - hashlib.sha256, - ).digest() - supplied_signature = _b64decode(signature_b64) - if not hmac.compare_digest(expected_signature, supplied_signature): - raise NativePathLeaseError("Native path grant signature is invalid.") - - payload = _decode_payload(payload_b64) - _validate_payload(payload, operation = operation, expected_kind = expected_kind) - - path = Path(str(payload["canonical_path"])) - _reject_network_or_device_path(path) - try: - signed_lstat = os.lstat(path) - except OSError as exc: - raise NativePathLeaseError("Native path is no longer accessible.") from exc - if _stat_module.S_ISLNK(signed_lstat.st_mode): - raise NativePathLeaseError("Native path is no longer a regular file.") - try: - resolved = path.resolve(strict = True) - except OSError as exc: - raise NativePathLeaseError("Native path is no longer accessible.") from exc - _reject_network_or_device_path(resolved) - if not _same_native_path(resolved, path): - raise NativePathLeaseError( - "Native path grant no longer resolves to the selected path." - ) - - grant = NativePathGrant( - operation = str(payload["operation"]), - canonical_path = resolved, - path_kind = str(payload["path_kind"]), - path_type = str(payload["path_type"]), - source_kind = str(payload["source_kind"]), - token_id_hash = str(payload["token_id_hash"]), - display_label = str(payload.get("display_label") or resolved.name), - expires_at_ms = _required_int(payload, "expires_at_ms"), - size_bytes = _optional_int(payload.get("size_bytes")), - modified_ms = _optional_int(payload.get("modified_ms")), - ) - - if expected_path_type and grant.path_type != expected_path_type: - raise NativePathLeaseError("Native path grant has the wrong path type.") - suffixes = tuple(s.lower() for s in (allowed_suffixes or ())) - if suffixes and resolved.suffix.lower() not in suffixes: - raise NativePathLeaseError("Native path grant has an unsupported file type.") - - _validate_current_stat(grant) - _consume_nonce(str(payload["nonce"]), grant.expires_at_ms) - _remember_native_path_for_redaction(str(resolved), grant.display_label) - return grant - - -def display_label_for_native_path(value: str | None) -> str | None: - if not value: - return value - with _REDACTION_LOCK: - return _NATIVE_PATH_LABELS.get(value, value) - - -def is_registered_native_path_label(path_value: str | None, label: str | None) -> bool: - if not path_value or not label: - return False - with _REDACTION_LOCK: - return _NATIVE_PATH_LABELS.get(path_value) == label - - -def redact_native_paths(value: str) -> str: - with _REDACTION_LOCK: - paths = sorted(_NATIVE_PATH_REDACTIONS, key = len, reverse = True) - redacted = value - for path in paths: - for variant in {path, path.replace("/", "\\"), path.replace("\\", "/")}: - if variant: - redacted = redacted.replace(variant, "") - return redacted - - -def _decode_secret() -> bytes: - global _CACHED_LEASE_SECRET - if _CACHED_LEASE_SECRET is not None: - return _CACHED_LEASE_SECRET - with _SECRET_INIT_LOCK: - if _CACHED_LEASE_SECRET is not None: - return _CACHED_LEASE_SECRET - with _NATIVE_PATH_ENV_LOCK: - encoded = os.environ.get(LEASE_SECRET_ENV) - if encoded is None and _SCRUB_SAVED_SECRET is not None: - encoded = _SCRUB_SAVED_SECRET - if not encoded: - raise NativePathLeaseError( - "Native path grants require the managed desktop backend." - ) - try: - secret = _b64decode(encoded) - except Exception as exc: - raise NativePathLeaseError("Native path grant secret is invalid.") from exc - if len(secret) < _MIN_LEASE_SECRET_BYTES: - raise NativePathLeaseError("Native path grant secret is invalid.") - _CACHED_LEASE_SECRET = secret - return secret - - -def _split_lease(lease: str) -> tuple[str, str]: - if not isinstance(lease, str): - raise NativePathLeaseError("Native path grant has an invalid format.") - try: - lease.encode("ascii") - except UnicodeEncodeError as exc: - raise NativePathLeaseError("Native path grant has an invalid format.") from exc - parts = lease.split(".") - if len(parts) != 2 or not parts[0] or not parts[1]: - raise NativePathLeaseError("Native path grant has an invalid format.") - return parts[0], parts[1] - - -def _decode_payload(payload_b64: str) -> dict[str, Any]: - try: - payload = json.loads(_b64decode(payload_b64).decode("utf-8")) - except Exception as exc: - raise NativePathLeaseError("Native path grant payload is invalid.") from exc - if not isinstance(payload, dict): - raise NativePathLeaseError("Native path grant payload is invalid.") - return payload - - -def _validate_payload( - payload: dict[str, Any], *, operation: str, expected_kind: str | None -) -> None: - required = ( - "version", - "operation", - "canonical_path", - "path_kind", - "path_type", - "source_kind", - "token_id_hash", - "issued_at_ms", - "expires_at_ms", - "nonce", - ) - missing = [key for key in required if key not in payload] - if missing: - raise NativePathLeaseError( - "Native path grant payload is missing required fields." - ) - if _required_int(payload, "version") != 1: - raise NativePathLeaseError("Native path grant version is unsupported.") - if payload["operation"] != operation: - raise NativePathLeaseError("Native path grant operation is invalid.") - if expected_kind and payload["path_kind"] != expected_kind: - raise NativePathLeaseError("Native path grant kind is invalid.") - now_ms = int(time.time() * 1000) - issued_at_ms = _required_int(payload, "issued_at_ms") - expires_at_ms = _required_int(payload, "expires_at_ms") - if issued_at_ms >= expires_at_ms: - raise NativePathLeaseError("Native path grant timestamps are inconsistent.") - if expires_at_ms <= now_ms: - raise NativePathLeaseError("Native path grant has expired.") - if issued_at_ms > now_ms + 30_000: - raise NativePathLeaseError("Native path grant issue time is invalid.") - for key in ("canonical_path", "nonce", "token_id_hash", "display_label"): - raw = payload.get(key) - if raw is None: - continue - if "\x00" in str(raw): - raise NativePathLeaseError("Native path grant contains invalid characters.") - - -def _validate_current_stat(grant: NativePathGrant) -> None: - try: - st = os.lstat(grant.canonical_path) - except OSError as exc: - raise NativePathLeaseError("Native path is no longer accessible.") from exc - if _stat_module.S_ISLNK(st.st_mode): - raise NativePathLeaseError("Native path is no longer a regular file.") - if grant.path_type == "file": - if not _stat_module.S_ISREG(st.st_mode): - raise NativePathLeaseError("Native path is no longer a regular file.") - elif grant.path_type == "directory": - if not _stat_module.S_ISDIR(st.st_mode): - raise NativePathLeaseError("Native path is no longer a directory.") - else: - raise NativePathLeaseError("Native path grant has an unsupported path type.") - - if grant.size_bytes is not None and st.st_size != grant.size_bytes: - raise NativePathLeaseError("Native path changed after it was selected.") - current_modified_ms = int(st.st_mtime_ns // 1_000_000) - if grant.modified_ms is not None and current_modified_ms != grant.modified_ms: - raise NativePathLeaseError("Native path changed after it was selected.") - - -def _consume_nonce(nonce: str, expires_at_ms: int) -> None: - now_ms = int(time.time() * 1000) - with _REPLAY_LOCK: - for key, expiry in list(_USED_NONCES.items()): - if expiry <= now_ms: - _USED_NONCES.pop(key, None) - if nonce in _USED_NONCES: - raise NativePathLeaseError("Native path grant was already used.") - _USED_NONCES[nonce] = expires_at_ms - - -def _remember_native_path_for_redaction(path: str, display_label: str) -> None: - with _REDACTION_LOCK: - _NATIVE_PATH_LABELS[path] = display_label - if len(_NATIVE_PATH_LABELS) > _MAX_NATIVE_PATH_LABELS: - excess = len(_NATIVE_PATH_LABELS) - _MAX_NATIVE_PATH_LABELS - for stale_path in list(_NATIVE_PATH_LABELS.keys())[:excess]: - _NATIVE_PATH_LABELS.pop(stale_path, None) - if path in _NATIVE_PATH_REDACTIONS: - return - _NATIVE_PATH_REDACTIONS.append(path) - del _NATIVE_PATH_REDACTIONS[:-_MAX_NATIVE_PATH_REDACTIONS] - - -def _reject_network_or_device_path(path: Path) -> None: - text = str(path) - if os.name == "nt": - normalized = text.replace("/", "\\").lower() - if normalized.startswith("\\\\?\\"): - rest = normalized[4:] - is_local_drive = len(rest) >= 3 and rest[0].isalpha() and rest[1:3] == ":\\" - if not is_local_drive: - raise NativePathLeaseError( - "Network paths are not supported for native grants." - ) - elif normalized.startswith("\\\\"): - raise NativePathLeaseError( - "Network paths are not supported for native grants." - ) - if os.name != "nt": - for root in ("/dev", "/proc", "/sys"): - if path.is_relative_to(root): - raise NativePathLeaseError( - "Device and virtual filesystem paths are not supported." - ) - if "\x00" in text: - raise NativePathLeaseError("Native path contains invalid characters.") - - -def _b64decode(value: str) -> bytes: - try: - padding = "=" * (-len(value) % 4) - return base64.urlsafe_b64decode((value + padding).encode("ascii")) - except (UnicodeEncodeError, binascii.Error, ValueError) as exc: - raise NativePathLeaseError("Native path grant has an invalid format.") from exc - - -def _same_native_path(resolved: Path, signed: Path) -> bool: - try: - return resolved.samefile(signed) - except OSError: - return os.path.normcase(str(resolved)) == os.path.normcase(str(signed)) - - -def _optional_int(value: Any) -> int | None: - if value is None: - return None - try: - return int(value) - except (TypeError, ValueError) as exc: - raise NativePathLeaseError("Native path grant payload is invalid.") from exc - - -def _required_int(payload: dict[str, Any], key: str) -> int: - raw = payload.get(key) - if raw is None: - raise NativePathLeaseError( - "Native path grant payload is missing required fields." - ) - try: - return int(raw) - except (TypeError, ValueError) as exc: - raise NativePathLeaseError("Native path grant payload is invalid.") from exc diff --git a/studio/backend/utils/paths/storage_roots.py b/studio/backend/utils/paths/storage_roots.py index 58a4d7967c..b52609b06b 100644 --- a/studio/backend/utils/paths/storage_roots.py +++ b/studio/backend/utils/paths/storage_roots.py @@ -5,59 +5,17 @@ import json import os -import sys from pathlib import Path import tempfile -def _infer_studio_home_from_venv() -> Path | None: - """Return parent dir of sys.prefix as STUDIO_HOME if running from an - installer-managed unsloth_studio venv. Sentinel-gated (share/studio.conf - or bin shim) so a developer venv named unsloth_studio is not misidentified. - """ - try: - prefix = Path(sys.prefix).resolve() - except (OSError, ValueError): - return None - if prefix.name != "unsloth_studio": - return None - candidate = prefix.parent - shim_name = "unsloth.exe" if os.name == "nt" else "unsloth" - try: - has_sentinel = (candidate / "share" / "studio.conf").is_file() or ( - candidate / "bin" / shim_name - ).is_file() - except OSError: - return None - if has_sentinel: - return candidate - return None - - def studio_root() -> Path: - """Studio install root. - - Priority: UNSLOTH_STUDIO_HOME, then STUDIO_HOME alias, then sys.prefix - inference, then legacy ~/.unsloth/studio. UNSLOTH_STUDIO_HOME wins when - both are set (the more specific signal beats the generic alias). - """ - override = (os.environ.get("UNSLOTH_STUDIO_HOME") or "").strip() - if not override: - override = (os.environ.get("STUDIO_HOME") or "").strip() - if override: - try: - return Path(override).expanduser().resolve() - except (OSError, ValueError): - return Path(override).expanduser() - inferred = _infer_studio_home_from_venv() - if inferred is not None: - return inferred return Path.home() / ".unsloth" / "studio" def cache_root() -> Path: """Central cache directory for all studio downloads (models, datasets, etc.).""" - return studio_root() / "cache" + return Path.home() / ".unsloth" / "studio" / "cache" def assets_root() -> Path: diff --git a/studio/backend/utils/subprocess_compat.py b/studio/backend/utils/subprocess_compat.py deleted file mode 100644 index bedf8cf2e6..0000000000 --- a/studio/backend/utils/subprocess_compat.py +++ /dev/null @@ -1,34 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 - -"""Cross-platform subprocess helpers for the Unsloth Studio backend.""" - -import subprocess -import sys - - -def windows_hidden_subprocess_kwargs() -> dict[str, object]: - """Return Windows-only subprocess kwargs that suppress console windows. - - On non-Windows platforms returns an empty dict so callers can always - unpack the result into ``subprocess.run`` / ``subprocess.Popen`` via - ``**windows_hidden_subprocess_kwargs()``. - """ - if sys.platform != "win32": - return {} - - kwargs: dict[str, object] = {} - create_no_window = getattr(subprocess, "CREATE_NO_WINDOW", 0) - if create_no_window: - kwargs["creationflags"] = create_no_window - - startupinfo_factory = getattr(subprocess, "STARTUPINFO", None) - startf_use_showwindow = getattr(subprocess, "STARTF_USESHOWWINDOW", 0) - sw_hide = getattr(subprocess, "SW_HIDE", 0) - if startupinfo_factory is not None and startf_use_showwindow: - startupinfo = startupinfo_factory() - startupinfo.dwFlags |= startf_use_showwindow - startupinfo.wShowWindow = sw_hide - kwargs["startupinfo"] = startupinfo - - return kwargs diff --git a/studio/backend/utils/transformers_version.py b/studio/backend/utils/transformers_version.py index 9075c590ca..36c3a4c22d 100644 --- a/studio/backend/utils/transformers_version.py +++ b/studio/backend/utils/transformers_version.py @@ -36,11 +36,6 @@ import sys from pathlib import Path -from utils.native_path_leases import child_env_without_native_path_secret -from utils.subprocess_compat import ( - windows_hidden_subprocess_kwargs as _windows_hidden_subprocess_kwargs, -) - logger = get_logger(__name__) @@ -64,7 +59,6 @@ TRANSFORMERS_550_MODEL_SUBSTRINGS: tuple[str, ...] = ( "gemma-4", # Gemma-4 (E2B-it, E4B-it, 31B-it, 26B-A4B-it) "gemma4", # Gemma-4 alternate naming - "qwen3.6", ) # Architecture classes / model_type values that require transformers 5.5.0. @@ -95,11 +89,9 @@ # Consumers should prefer TRANSFORMERS_530_VERSION / TRANSFORMERS_550_VERSION. TRANSFORMERS_5_VERSION = TRANSFORMERS_550_VERSION -# Pre-installed directories — created by setup.sh / setup.ps1. -from utils.paths.storage_roots import studio_root as _studio_root # noqa: E402 - -_VENV_T5_530_DIR = str(_studio_root() / ".venv_t5_530") -_VENV_T5_550_DIR = str(_studio_root() / ".venv_t5_550") +# Pre-installed directories — created by setup.sh / setup.ps1 +_VENV_T5_530_DIR = str(Path.home() / ".unsloth" / "studio" / ".venv_t5_530") +_VENV_T5_550_DIR = str(Path.home() / ".unsloth" / "studio" / ".venv_t5_550") # Backwards-compat alias _VENV_T5_DIR = _VENV_T5_550_DIR @@ -507,8 +499,6 @@ def _install_to_dir(pkg: str, target_dir: str) -> bool: stdout = subprocess.PIPE, stderr = subprocess.STDOUT, text = True, - env = child_env_without_native_path_secret(), - **_windows_hidden_subprocess_kwargs(), ) if result.returncode == 0: return True @@ -530,8 +520,6 @@ def _install_to_dir(pkg: str, target_dir: str) -> bool: stdout = subprocess.PIPE, stderr = subprocess.STDOUT, text = True, - env = child_env_without_native_path_secret(), - **_windows_hidden_subprocess_kwargs(), ) if result.returncode != 0: logger.error("install failed:\n%s", result.stdout) diff --git a/studio/backend/utils/wheel_utils.py b/studio/backend/utils/wheel_utils.py index 3ed9bda827..00240f1e69 100644 --- a/studio/backend/utils/wheel_utils.py +++ b/studio/backend/utils/wheel_utils.py @@ -13,8 +13,6 @@ import urllib.request from typing import Callable -from utils.native_path_leases import child_env_without_native_path_secret - _logger = logging.getLogger(__name__) FLASH_ATTN_RELEASE_BASE_URL = ( @@ -61,7 +59,6 @@ def probe_torch_wheel_env(*, timeout: int | None = None) -> dict[str, str] | Non stderr = subprocess.PIPE, text = True, timeout = timeout, - env = child_env_without_native_path_secret(), ) except subprocess.TimeoutExpired: return None @@ -145,7 +142,6 @@ def install_wheel( stdout = subprocess.PIPE, stderr = subprocess.STDOUT, text = True, - env = child_env_without_native_path_secret(), ) attempts.append(("uv", result)) if result.returncode == 0: @@ -157,7 +153,6 @@ def install_wheel( stdout = subprocess.PIPE, stderr = subprocess.STDOUT, text = True, - env = child_env_without_native_path_secret(), ) attempts.append(("pip", result)) return attempts diff --git a/studio/frontend/package-lock.json b/studio/frontend/package-lock.json deleted file mode 100644 index ed2ceb550e..0000000000 --- a/studio/frontend/package-lock.json +++ /dev/null @@ -1,16817 +0,0 @@ -{ - "name": "unsloth-theme", - "version": "0.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "unsloth-theme", - "version": "0.0.0", - "dependencies": { - "@assistant-ui/core": "0.1.17", - "@assistant-ui/react": "0.12.28", - "@assistant-ui/react-markdown": "0.12.11", - "@assistant-ui/react-streamdown": "0.1.11", - "@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": "^4.1.1", - "@hugeicons/react": "^1.1.5", - "@huggingface/hub": "^2.9.0", - "@langchain/core": "^1.1.27", - "@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.3", - "@streamdown/code": "1.1.1", - "@streamdown/math": "1.0.2", - "@streamdown/mermaid": "1.0.2", - "@tailwindcss/vite": "^4.2.2", - "@tanstack/react-router": "^1.159.10", - "@tanstack/react-table": "^8.21.3", - "@tauri-apps/api": "^2.10.1", - "@tauri-apps/plugin-clipboard-manager": "^2.3.2", - "@tauri-apps/plugin-notification": "^2.3.3", - "@tauri-apps/plugin-opener": "^2.5.3", - "@tauri-apps/plugin-process": "^2.3.1", - "@tauri-apps/plugin-updater": "^2.10.1", - "@toolwind/corner-shape": "^0.0.8-3", - "@types/canvas-confetti": "^1.9.0", - "@xyflow/react": "^12.10.0", - "assistant-stream": "0.3.12", - "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.3.0", - "js-yaml": "^4.1.1", - "katex": "^0.16.28", - "lucide-react": "^1.7.0", - "mammoth": "^1.11.0", - "motion": "^12.34.0", - "next": "^16.1.6", - "next-themes": "^0.4.6", - "radix-ui": "^1.4.3", - "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": "^4.2.0", - "sonner": "^2.0.7", - "streamdown": "2.5.0", - "tailwind-merge": "^3.4.0", - "tailwindcss": "^4.1.18", - "tw-animate-css": "^1.4.0", - "tw-shimmer": "^0.4.6", - "unpdf": "^1.4.0", - "zustand": "^5.0.11" - }, - "devDependencies": { - "@biomejs/biome": "^1.9.4", - "@eslint/js": "^9.39.1", - "@types/js-yaml": "^4.0.9", - "@types/node": "^25.5.2", - "@types/react": "^19.2.5", - "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^6.0.1", - "eslint": "^9.39.1", - "eslint-plugin-react-hooks": "^7.0.1", - "eslint-plugin-react-refresh": "^0.5.2", - "globals": "^17.4.0", - "playwright": "^1.59.1", - "typescript": "~5.9.3", - "typescript-eslint": "^8.55.0", - "vite": "^8.0.1" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@antfu/install-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", - "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==", - "license": "MIT", - "dependencies": { - "package-manager-detector": "^1.3.0", - "tinyexec": "^1.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/@assistant-ui/core": { - "version": "0.1.17", - "resolved": "https://registry.npmjs.org/@assistant-ui/core/-/core-0.1.17.tgz", - "integrity": "sha512-IWIP98UVQ9W+oF0yz8XqFRtaX8HtozWVUWt6D/BSV6cyKwLfJ8niHtLG74bSnllTnGcreU2El3GR/tIodR1XuA==", - "license": "MIT", - "dependencies": { - "assistant-stream": "^0.3.12", - "nanoid": "^5.1.9" - }, - "peerDependencies": { - "@assistant-ui/store": "^0.2.9", - "@assistant-ui/tap": "^0.5.10", - "@types/react": "*", - "assistant-cloud": "^0.1.27", - "react": "^18 || ^19", - "zustand": "^5.0.11" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "assistant-cloud": { - "optional": true - }, - "react": { - "optional": true - }, - "zustand": { - "optional": true - } - } - }, - "node_modules/@assistant-ui/react": { - "version": "0.12.28", - "resolved": "https://registry.npmjs.org/@assistant-ui/react/-/react-0.12.28.tgz", - "integrity": "sha512-czjpexLK1lKnNDNM1YMJi8SufeKUWBICqiVUtiHMV+86PYGCwJykOZKkchI8MVbSQ62xZ8A1LfPO5W2IDjed3A==", - "license": "MIT", - "dependencies": { - "@assistant-ui/core": "^0.1.17", - "@assistant-ui/store": "^0.2.9", - "@assistant-ui/tap": "^0.5.10", - "@radix-ui/primitive": "^1.1.3", - "@radix-ui/react-compose-refs": "^1.1.2", - "@radix-ui/react-context": "^1.1.3", - "@radix-ui/react-primitive": "^2.1.4", - "@radix-ui/react-use-callback-ref": "^1.1.1", - "@radix-ui/react-use-escape-keydown": "^1.1.1", - "assistant-cloud": "^0.1.27", - "assistant-stream": "^0.3.12", - "nanoid": "^5.1.9", - "radix-ui": "^1.4.3", - "react-textarea-autosize": "^8.5.9", - "zod": "^4.3.6", - "zustand": "^5.0.12" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^18 || ^19", - "react-dom": "^18 || ^19" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@assistant-ui/react-markdown": { - "version": "0.12.11", - "resolved": "https://registry.npmjs.org/@assistant-ui/react-markdown/-/react-markdown-0.12.11.tgz", - "integrity": "sha512-gYu4XVI2lX3lp9UG7V5VWP1+eO7SZomiBKsAZOKUOeuwn/hoL+J0vFY52FUgJixdF2R8NPPto2lb98DmJE70lA==", - "license": "MIT", - "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.26", - "@types/react": "*", - "react": "^18 || ^19" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@assistant-ui/react-streamdown": { - "version": "0.1.11", - "resolved": "https://registry.npmjs.org/@assistant-ui/react-streamdown/-/react-streamdown-0.1.11.tgz", - "integrity": "sha512-9y+89ZxotYSt81hChSVjK2kwUYRKq7UW/r5qoqZTpcb7119gc0NOj0dx9xxuyXE2QfR6EY8rW6yBz3g+Y7RrhQ==", - "license": "MIT", - "dependencies": { - "rehype-harden": "^1.1.8", - "rehype-raw": "^7.0.0", - "rehype-sanitize": "^6.0.0", - "streamdown": "^2.5.0" - }, - "peerDependencies": { - "@assistant-ui/react": "^0.12.26", - "@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" - }, - "peerDependenciesMeta": { - "@streamdown/cjk": { - "optional": true - }, - "@streamdown/code": { - "optional": true - }, - "@streamdown/math": { - "optional": true - }, - "@streamdown/mermaid": { - "optional": true - }, - "@types/react": { - "optional": true - } - } - }, - "node_modules/@assistant-ui/store": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/@assistant-ui/store/-/store-0.2.9.tgz", - "integrity": "sha512-EDd6yCfirb2OsAKoTo7HeMtqPG+1cqVlNXOzUsho35ZF3O1XQ2CyEY4iUbdhj3HfmWeZo7rmfhvbaYQVEqAfeA==", - "license": "MIT", - "dependencies": { - "use-effect-event": "^2.0.3" - }, - "peerDependencies": { - "@assistant-ui/tap": "^0.5.10", - "@types/react": "*", - "react": "^18 || ^19" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@assistant-ui/tap": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/@assistant-ui/tap/-/tap-0.5.10.tgz", - "integrity": "sha512-sBHTf+q1geRyu5l4gJJp2hk6ZxwhHZHj39ixjC9ARADuIYedYv1B8bCNS82eTC/COpD1xe86mzvT/+HwIsO9WA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^18 || ^19" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "react": { - "optional": true - } - } - }, - "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.29.3", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz", - "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", - "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", - "license": "MIT", - "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" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/generator": { - "version": "7.29.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", - "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", - "license": "MIT", - "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" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", - "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.27.3" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.29.3", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.3.tgz", - "integrity": "sha512-RpLYy2sb51oNLjuu1iD3bwBqCBWUzjO0ocp+iaCP/lJtb2CPLcnC2Fftw+4sAzaMELGeWTgExSKADbdo0GFVzA==", - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-member-expression-to-functions": "^7.28.5", - "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/helper-replace-supers": "^7.28.6", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/traverse": "^7.29.0", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", - "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.28.5", - "@babel/types": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", - "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", - "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-replace-supers": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", - "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", - "license": "MIT", - "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.28.5", - "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/traverse": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", - "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", - "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", - "license": "MIT", - "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.29.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", - "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.0" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", - "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", - "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz", - "integrity": "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==", - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-typescript": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.6.tgz", - "integrity": "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==", - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-create-class-features-plugin": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/plugin-syntax-typescript": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-typescript": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.28.5.tgz", - "integrity": "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-validator-option": "^7.27.1", - "@babel/plugin-syntax-jsx": "^7.27.1", - "@babel/plugin-transform-modules-commonjs": "^7.27.1", - "@babel/plugin-transform-typescript": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", - "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", - "license": "MIT", - "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" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@base-ui/react": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@base-ui/react/-/react-1.4.1.tgz", - "integrity": "sha512-Ab5/LIhcmL8BQcsBUYiOfkSDRdLpvgUBzMK30cu684JPcLclYlztharvCZyNNgzJtbAiREzI9q0pI5erHCMgCw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.29.2", - "@base-ui/utils": "0.2.8", - "@floating-ui/react-dom": "^2.1.8", - "@floating-ui/utils": "^0.2.11", - "use-sync-external-store": "^1.6.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@date-fns/tz": "^1.2.0", - "@types/react": "^17 || ^18 || ^19", - "date-fns": "^4.0.0", - "react": "^17 || ^18 || ^19", - "react-dom": "^17 || ^18 || ^19" - }, - "peerDependenciesMeta": { - "@date-fns/tz": { - "optional": true - }, - "@types/react": { - "optional": true - }, - "date-fns": { - "optional": true - } - } - }, - "node_modules/@base-ui/utils": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/@base-ui/utils/-/utils-0.2.8.tgz", - "integrity": "sha512-jvOi+c+ftGlGotNcKnzPVg2IhCaDTB6/6R3JeqdjdXktuAJi3wKH9T7+svuaKh1mmfVU11UWzUZVH74JDfi/wQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.29.2", - "@floating-ui/utils": "^0.2.11", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@biomejs/biome": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-1.9.4.tgz", - "integrity": "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==", - "dev": true, - "hasInstallScript": true, - "license": "MIT OR Apache-2.0", - "bin": { - "biome": "bin/biome" - }, - "engines": { - "node": ">=14.21.3" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/biome" - }, - "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" - } - }, - "node_modules/@biomejs/cli-darwin-arm64": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.9.4.tgz", - "integrity": "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-darwin-x64": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.9.4.tgz", - "integrity": "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-linux-arm64": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.9.4.tgz", - "integrity": "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-linux-arm64-musl": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.9.4.tgz", - "integrity": "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-linux-x64": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.9.4.tgz", - "integrity": "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-linux-x64-musl": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.9.4.tgz", - "integrity": "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-win32-arm64": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.9.4.tgz", - "integrity": "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-win32-x64": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.9.4.tgz", - "integrity": "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@braintree/sanitize-url": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz", - "integrity": "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==", - "license": "MIT" - }, - "node_modules/@cfworker/json-schema": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@cfworker/json-schema/-/json-schema-4.1.1.tgz", - "integrity": "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==", - "license": "MIT" - }, - "node_modules/@chevrotain/cst-dts-gen": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-12.0.0.tgz", - "integrity": "sha512-fSL4KXjTl7cDgf0B5Rip9Q05BOrYvkJV/RrBTE/bKDN096E4hN/ySpcBK5B24T76dlQ2i32Zc3PAE27jFnFrKg==", - "license": "Apache-2.0", - "dependencies": { - "@chevrotain/gast": "12.0.0", - "@chevrotain/types": "12.0.0" - } - }, - "node_modules/@chevrotain/gast": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-12.0.0.tgz", - "integrity": "sha512-1ne/m3XsIT8aEdrvT33so0GUC+wkctpUPK6zU9IlOyJLUbR0rg4G7ZiApiJbggpgPir9ERy3FRjT6T7lpgetnQ==", - "license": "Apache-2.0", - "dependencies": { - "@chevrotain/types": "12.0.0" - } - }, - "node_modules/@chevrotain/regexp-to-ast": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-12.0.0.tgz", - "integrity": "sha512-p+EW9MaJwgaHguhoqwOtx/FwuGr+DnNn857sXWOi/mClXIkPGl3rn7hGNWvo31HA3vyeQxjqe+H36yZJwYU8cA==", - "license": "Apache-2.0" - }, - "node_modules/@chevrotain/types": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-12.0.0.tgz", - "integrity": "sha512-S+04vjFQKeuYw0/eW3U52LkAHQsB1ASxsPGsLPUyQgrZ2iNNibQrsidruDzjEX2JYfespXMG0eZmXlhA6z7nWA==", - "license": "Apache-2.0" - }, - "node_modules/@chevrotain/utils": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-12.0.0.tgz", - "integrity": "sha512-lB59uJoaGIfOOL9knQqQRfhl9g7x8/wqFkp13zTdkRu1huG9kg6IJs1O8hqj9rs6h7orGxHJUKb+mX3rPbWGhA==", - "license": "Apache-2.0" - }, - "node_modules/@dagrejs/dagre": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@dagrejs/dagre/-/dagre-2.0.4.tgz", - "integrity": "sha512-J6vCWTNpicHF4zFlZG1cS5DkGzMr9941gddYkakjrg3ZNev4bbqEgLHFTWiFrcJm7UCRu7olO3K6IRDd9gSGhA==", - "license": "MIT", - "dependencies": { - "@dagrejs/graphlib": "3.0.4" - } - }, - "node_modules/@dagrejs/graphlib": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@dagrejs/graphlib/-/graphlib-3.0.4.tgz", - "integrity": "sha512-HxZ7fCvAwTLCWCO0WjDkzAFQze8LdC6iOpKbetDKHIuDfIgMlIzYzqZ4nxwLlclQX+3ZVeZ1K2OuaOE2WWcyOg==", - "license": "MIT" - }, - "node_modules/@date-fns/tz": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.4.1.tgz", - "integrity": "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==", - "license": "MIT" - }, - "node_modules/@dotenvx/dotenvx": { - "version": "1.64.0", - "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.64.0.tgz", - "integrity": "sha512-6+xRpZaWuHXEqnhBjae+VmQI9Uaqw5Uzu/ScpO+W7ww9Zp3lHSNBoNjFcUxhrCyc7pRGQzyDjhKzloqrPHERiQ==", - "license": "BSD-3-Clause", - "dependencies": { - "commander": "^11.1.0", - "dotenv": "^17.2.1", - "eciesjs": "^0.4.10", - "execa": "^5.1.1", - "fdir": "^6.2.0", - "ignore": "^5.3.0", - "object-treeify": "1.1.33", - "picomatch": "^4.0.4", - "which": "^4.0.0", - "yocto-spinner": "^1.1.0" - }, - "bin": { - "dotenvx": "src/cli/dotenvx.js" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/@dotenvx/dotenvx/node_modules/commander": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", - "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", - "license": "MIT", - "engines": { - "node": ">=16" - } - }, - "node_modules/@dotenvx/dotenvx/node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/@dotenvx/dotenvx/node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@dotenvx/dotenvx/node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "license": "Apache-2.0", - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/@dotenvx/dotenvx/node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@dotenvx/dotenvx/node_modules/isexe": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", - "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/@dotenvx/dotenvx/node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "license": "MIT", - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@dotenvx/dotenvx/node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@dotenvx/dotenvx/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "license": "ISC" - }, - "node_modules/@dotenvx/dotenvx/node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/@dotenvx/dotenvx/node_modules/which": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", - "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", - "license": "ISC", - "dependencies": { - "isexe": "^3.1.1" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^16.13.0 || >=18.0.0" - } - }, - "node_modules/@ecies/ciphers": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/@ecies/ciphers/-/ciphers-0.2.6.tgz", - "integrity": "sha512-patgsRPKGkhhoBjETV4XxD0En4ui5fbX0hzayqI3M8tvNMGUoUvmyYAIWwlxBc1KX5cturfqByYdj5bYGRpN9g==", - "license": "MIT", - "engines": { - "bun": ">=1", - "deno": ">=2.7.10", - "node": ">=16" - }, - "peerDependencies": { - "@noble/ciphers": "^1.0.0" - } - }, - "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.21.2", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", - "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.7", - "debug": "^4.3.1", - "minimatch": "^3.1.5" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", - "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/core": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", - "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", - "dev": true, - "license": "MIT", - "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.5", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/js": { - "version": "9.39.4", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", - "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - } - }, - "node_modules/@eslint/object-schema": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", - "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", - "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0", - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@floating-ui/core": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", - "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", - "license": "MIT", - "dependencies": { - "@floating-ui/utils": "^0.2.11" - } - }, - "node_modules/@floating-ui/dom": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", - "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", - "license": "MIT", - "dependencies": { - "@floating-ui/core": "^1.7.5", - "@floating-ui/utils": "^0.2.11" - } - }, - "node_modules/@floating-ui/react-dom": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", - "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", - "license": "MIT", - "dependencies": { - "@floating-ui/dom": "^1.7.6" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@floating-ui/utils": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", - "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", - "license": "MIT" - }, - "node_modules/@fontsource-variable/figtree": { - "version": "5.2.10", - "resolved": "https://registry.npmjs.org/@fontsource-variable/figtree/-/figtree-5.2.10.tgz", - "integrity": "sha512-a5Gumbpy3mdd+Yg31g6Qb7CmjYbrfyutJa3bWfP5q8A4GclIOwX7mI+ZuSHsJnw/mHvW6r9oh1AHJcJTIxK4JA==", - "license": "OFL-1.1", - "funding": { - "url": "https://github.com/sponsors/ayuhito" - } - }, - "node_modules/@fontsource-variable/inter": { - "version": "5.2.8", - "resolved": "https://registry.npmjs.org/@fontsource-variable/inter/-/inter-5.2.8.tgz", - "integrity": "sha512-kOfP2D+ykbcX/P3IFnokOhVRNoTozo5/JxhAIVYLpea/UBmCQ/YWPBfWIDuBImXX/15KH+eKh4xpEUyS2sQQGQ==", - "license": "OFL-1.1", - "funding": { - "url": "https://github.com/sponsors/ayuhito" - } - }, - "node_modules/@fontsource-variable/space-grotesk": { - "version": "5.2.10", - "resolved": "https://registry.npmjs.org/@fontsource-variable/space-grotesk/-/space-grotesk-5.2.10.tgz", - "integrity": "sha512-yJQO/o35/hAP3CFnpdFTwQku2yzJOae2HIpBmqkOVoxhhXJaQP3g+b6Jrz7u+eI7A5ZdCIf88uMWpBJdFiGr5w==", - "license": "OFL-1.1", - "funding": { - "url": "https://github.com/sponsors/ayuhito" - } - }, - "node_modules/@hono/node-server": { - "version": "1.19.14", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", - "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", - "license": "MIT", - "engines": { - "node": ">=18.14.1" - }, - "peerDependencies": { - "hono": "^4" - } - }, - "node_modules/@hugeicons/core-free-icons": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@hugeicons/core-free-icons/-/core-free-icons-4.1.1.tgz", - "integrity": "sha512-teqIBvPHl90ygIwKyJwTxOH8aNp1X1PjDTcMvLkEwdPxPD+8mssrZ5kXKIAJJFYPsz69a8LYQY0UPid4PAdavg==", - "license": "MIT" - }, - "node_modules/@hugeicons/react": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@hugeicons/react/-/react-1.1.6.tgz", - "integrity": "sha512-c2LhXJMAW5wN1pC/smBXG0YPqUON6ceR/ZdXHCjEI9KvB+hjtqYjmzIxok5hAQOeXGz0WtORgCQMzqewFKAZwg==", - "license": "MIT", - "peerDependencies": { - "react": ">=16.0.0" - } - }, - "node_modules/@huggingface/hub": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/@huggingface/hub/-/hub-2.11.0.tgz", - "integrity": "sha512-WS6QGaXYeBVFlaB4SOn6z4LGUpLB5kRZNL08uUni4izX353KxiwwZMK5+/AWX86MJh8SMZNa/JFcvFCcQsbszQ==", - "license": "MIT", - "dependencies": { - "@huggingface/tasks": "^0.19.90" - }, - "bin": { - "hfjs": "dist/cli.js" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "cli-progress": "^3.12.0" - } - }, - "node_modules/@huggingface/tasks": { - "version": "0.19.90", - "resolved": "https://registry.npmjs.org/@huggingface/tasks/-/tasks-0.19.90.tgz", - "integrity": "sha512-nfV9luJbvwGQ/5oKXkKhCV9h4X7mwh1YaGG3ORd6UMLDSwr1OFSSatcBX0O9OtBtmNK19aGSjbLFqqgcIR6+IA==", - "license": "MIT" - }, - "node_modules/@humanfs/core": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", - "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/types": "^0.15.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.8", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", - "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.2", - "@humanfs/types": "^0.15.0", - "@humanwhocodes/retry": "^0.4.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/types": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", - "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@iconify/types": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", - "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", - "license": "MIT" - }, - "node_modules/@iconify/utils": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.1.tgz", - "integrity": "sha512-MwzoDtw9rO1x+qfgLTV/IVXsHDBqeYZoMIQC8SfxfYSlaSUG+oWiAcoiB1yajAda6mqblm4/1/w2E8tRu7a7Tw==", - "license": "MIT", - "dependencies": { - "@antfu/install-pkg": "^1.1.0", - "@iconify/types": "^2.0.0", - "mlly": "^1.8.2" - } - }, - "node_modules/@img/colour": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", - "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", - "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.4" - } - }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", - "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.4" - } - }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", - "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", - "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", - "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", - "cpu": [ - "arm" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", - "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", - "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", - "cpu": [ - "ppc64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", - "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", - "cpu": [ - "riscv64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", - "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", - "cpu": [ - "s390x" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", - "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", - "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", - "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", - "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", - "cpu": [ - "arm" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", - "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", - "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", - "cpu": [ - "ppc64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-riscv64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", - "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", - "cpu": [ - "riscv64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", - "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", - "cpu": [ - "s390x" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", - "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.4" - } - }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", - "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" - } - }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", - "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.4" - } - }, - "node_modules/@img/sharp-wasm32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", - "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", - "cpu": [ - "wasm32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.7.0" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", - "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", - "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", - "cpu": [ - "ia32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", - "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@inquirer/ansi": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.5.tgz", - "integrity": "sha512-doc2sWgJpbFQ64UflSVd17ibMGDuxO1yKgOgLMwavzESnXjFWJqUeG8saYosqKpHp4kWiM5x1nXvEjbpx90gzw==", - "license": "MIT", - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" - } - }, - "node_modules/@inquirer/confirm": { - "version": "6.0.12", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.0.12.tgz", - "integrity": "sha512-h9FgGun3QwVYNj5TWIZZ+slii73bMoBFjPfVIGtnFuL4t8gBiNDV9PcSfIzkuxvgquJKt9nr1QzszpBzTbH8Og==", - "license": "MIT", - "dependencies": { - "@inquirer/core": "^11.1.9", - "@inquirer/type": "^4.0.5" - }, - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/core": { - "version": "11.1.9", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-11.1.9.tgz", - "integrity": "sha512-BDE4fG22uYh1bGSifcj7JSx119TVYNViMhMu85usp4Fswrzh6M0DV3yld64jA98uOAa2GSQ4Bg4bZRm2d2cwSg==", - "license": "MIT", - "dependencies": { - "@inquirer/ansi": "^2.0.5", - "@inquirer/figures": "^2.0.5", - "@inquirer/type": "^4.0.5", - "cli-width": "^4.1.0", - "fast-wrap-ansi": "^0.2.0", - "mute-stream": "^3.0.0", - "signal-exit": "^4.1.0" - }, - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/figures": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.5.tgz", - "integrity": "sha512-NsSs4kzfm12lNetHwAn3GEuH317IzpwrMCbOuMIVytpjnJ90YYHNwdRgYGuKmVxwuIqSgqk3M5qqQt1cDk0tGQ==", - "license": "MIT", - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" - } - }, - "node_modules/@inquirer/type": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.0.5.tgz", - "integrity": "sha512-aetVUNeKNc/VriqXlw1NRSW0zhMBB0W4bNbWRJgzRl/3d0QNDQFfk0GO5SDdtjMZVg6o8ZKEiadd7SCCzoOn5Q==", - "license": "MIT", - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@langchain/core": { - "version": "1.1.44", - "resolved": "https://registry.npmjs.org/@langchain/core/-/core-1.1.44.tgz", - "integrity": "sha512-RePW1IjGCHr9ua2vcby3aE8mOOz3EnwDZxMEGbNDT91kf14eqkJqxDXvaZFviGdcN9DTrxM5RPQNAHmwSm4tbg==", - "license": "MIT", - "dependencies": { - "@cfworker/json-schema": "^4.0.2", - "@standard-schema/spec": "^1.1.0", - "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", - "zod": "^3.25.76 || ^4" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@mermaid-js/parser": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.1.0.tgz", - "integrity": "sha512-gxK9ZX2+Fex5zu8LhRQoMeMPEHbc73UKZ0FQ54YrQtUxE1VVhMwzeNtKRPAu5aXks4FasbMe4xB4bWrmq6Jlxw==", - "license": "MIT", - "dependencies": { - "langium": "^4.0.0" - } - }, - "node_modules/@modelcontextprotocol/sdk": { - "version": "1.29.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", - "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", - "license": "MIT", - "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" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@cfworker/json-schema": "^4.1.1", - "zod": "^3.25 || ^4.0" - }, - "peerDependenciesMeta": { - "@cfworker/json-schema": { - "optional": true - }, - "zod": { - "optional": false - } - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/ajv": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", - "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, - "node_modules/@mswjs/interceptors": { - "version": "0.41.8", - "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.41.8.tgz", - "integrity": "sha512-pRLMNKTSGRoLq+KnEB/7OY5vijw1XmcheAAOiv6pj7W1FG32kAGqj1C/RK/cqxRGr1Fh+zBi8sDur8kj3EQv6A==", - "license": "MIT", - "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" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@mswjs/interceptors/node_modules/@open-draft/deferred-promise": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", - "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", - "license": "MIT" - }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", - "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", - "license": "MIT", - "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" - } - }, - "node_modules/@next/env": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.4.tgz", - "integrity": "sha512-dKkkOzOSwFYe5RX6y26fZgkSpVAlIOJKQHIiydQcrWH6y/97+RceSOAdjZ14Qa3zLduVUy0TXcn+EiM6t4rPgw==", - "license": "MIT" - }, - "node_modules/@next/swc-darwin-arm64": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.4.tgz", - "integrity": "sha512-OXTFFox5EKN1Ym08vfrz+OXxmCcEjT4SFMbNRsWZE99dMqt2Kcusl5MqPXcW232RYkMLQTy0hqgAMEsfEd/l2A==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-darwin-x64": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.4.tgz", - "integrity": "sha512-XhpVnUfmYWvD3YrXu55XdcAkQtOnvaI6wtQa8fuF5fGoKoxIUZ0kWPtcOfqJEWngFF/lOS9l3+O9CcownhiQxQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.4.tgz", - "integrity": "sha512-Mx/tjlNA3G8kg14QvuGAJ4xBwPk1tUHq56JxZ8CXnZwz1Etz714soCEzGQQzVMz4bEnGPowzkV6Xrp6wAkEWOQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.4.tgz", - "integrity": "sha512-iVMMp14514u7Nup2umQS03nT/bN9HurK8ufylC3FZNykrwjtx7V1A7+4kvhbDSCeonTVqV3Txnv0Lu+m2oDXNg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.4.tgz", - "integrity": "sha512-EZOvm1aQWgnI/N/xcWOlnS3RQBk0VtVav5Zo7n4p0A7UKyTDx047k8opDbXgBpHl4CulRqRfbw3QrX2w5UOXMQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-musl": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.4.tgz", - "integrity": "sha512-h9FxsngCm9cTBf71AR4fGznDEDx1hS7+kSEiIRjq5kO1oXWm07DxVGZjCvk0SGx7TSjlUqhI8oOyz7NfwAdPoA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.4.tgz", - "integrity": "sha512-3NdJV5OXMSOeJYijX+bjaLge3mJBlh4ybydbT4GFoB/2hAojWHtMhl3CYlYoMrjPuodp0nzFVi4Tj2+WaMg+Ow==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.4.tgz", - "integrity": "sha512-kMVGgsqhO5YTYODD9IPGGhA6iprWidQckK3LmPeW08PIFENRmgfb4MjXHO+p//d+ts2rpjvK5gXWzXSMrPl9cw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@noble/ciphers": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", - "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@noble/curves": { - "version": "1.9.7", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", - "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.8.0" - }, - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@open-draft/deferred-promise": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-3.0.0.tgz", - "integrity": "sha512-XW375UK8/9SqUVNVa6M0yEy8+iTi4QN5VZ7aZuRFQmy76LRwI9wy5F4YIBU6T+eTe2/DNDo8tqu8RHlwLHM6RA==", - "license": "MIT" - }, - "node_modules/@open-draft/logger": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", - "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", - "license": "MIT", - "dependencies": { - "is-node-process": "^1.2.0", - "outvariant": "^1.4.0" - } - }, - "node_modules/@open-draft/until": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", - "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", - "license": "MIT" - }, - "node_modules/@oxc-project/types": { - "version": "0.127.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz", - "integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" - } - }, - "node_modules/@radix-ui/number": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", - "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", - "license": "MIT" - }, - "node_modules/@radix-ui/primitive": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", - "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", - "license": "MIT" - }, - "node_modules/@radix-ui/react-accessible-icon": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.1.7.tgz", - "integrity": "sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-visually-hidden": "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-accordion": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz", - "integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collapsible": "1.1.12", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-alert-dialog": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz", - "integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dialog": "1.1.15", - "@radix-ui/react-primitive": "2.1.3", - "@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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-arrow": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", - "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-arrow/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-arrow/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-aspect-ratio": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.7.tgz", - "integrity": "sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-aspect-ratio/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-aspect-ratio/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-avatar": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz", - "integrity": "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-is-hydrated": "0.1.0", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-checkbox": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", - "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-use-size": "1.1.1" - }, - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-collapsible": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", - "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-collection": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", - "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", - "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-context": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.3.tgz", - "integrity": "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-context-menu": { - "version": "2.2.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.16.tgz", - "integrity": "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-menu": "2.1.16", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-context-menu/node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-context-menu/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-context-menu/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dialog": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", - "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-direction": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", - "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", - "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-escape-keydown": "1.1.1" - }, - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu": { - "version": "2.1.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", - "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-menu": "2.1.16", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-guards": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", - "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", - "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1" - }, - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-form": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-form/-/react-form-0.1.8.tgz", - "integrity": "sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-label": "2.1.7", - "@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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-form/node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-form/node_modules/@radix-ui/react-label": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", - "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-form/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-form/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-hover-card": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.15.tgz", - "integrity": "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-id": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", - "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-label": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz", - "integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.4" - }, - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-menu": { - "version": "2.1.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", - "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-menubar": { - "version": "1.1.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.16.tgz", - "integrity": "sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-menu": "2.1.16", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-navigation-menu": { - "version": "1.2.14", - "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz", - "integrity": "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-visually-hidden": "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-navigation-menu/node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-navigation-menu/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-navigation-menu/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-one-time-password-field": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.1.8.tgz", - "integrity": "sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg==", - "license": "MIT", - "dependencies": { - "@radix-ui/number": "1.1.1", - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-effect-event": "0.0.2", - "@radix-ui/react-use-is-hydrated": "0.1.0", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-one-time-password-field/node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-one-time-password-field/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-one-time-password-field/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-password-toggle-field": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-password-toggle-field/-/react-password-toggle-field-0.1.3.tgz", - "integrity": "sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-effect-event": "0.0.2", - "@radix-ui/react-use-is-hydrated": "0.1.0" - }, - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-password-toggle-field/node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-password-toggle-field/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-password-toggle-field/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", - "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popper": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", - "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", - "license": "MIT", - "dependencies": { - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-rect": "1.1.1", - "@radix-ui/react-use-size": "1.1.1", - "@radix-ui/rect": "1.1.1" - }, - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-portal": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", - "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-portal/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-portal/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-presence": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", - "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-primitive": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", - "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.4" - }, - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-progress": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz", - "integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-context": "1.1.2", - "@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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-radio-group": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz", - "integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-use-size": "1.1.1" - }, - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", - "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-scroll-area": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", - "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==", - "license": "MIT", - "dependencies": { - "@radix-ui/number": "1.1.1", - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", - "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/number": "1.1.1", - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-visually-hidden": "1.2.3", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-separator": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.8.tgz", - "integrity": "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.4" - }, - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-slider": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz", - "integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==", - "license": "MIT", - "dependencies": { - "@radix-ui/number": "1.1.1", - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-use-size": "1.1.1" - }, - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-slot": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", - "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-switch": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz", - "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-use-size": "1.1.1" - }, - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tabs": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", - "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toast": { - "version": "1.2.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.15.tgz", - "integrity": "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-visually-hidden": "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toggle": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", - "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toggle-group": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz", - "integrity": "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-toggle": "1.1.10", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toolbar": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.1.11.tgz", - "integrity": "sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-separator": "1.1.7", - "@radix-ui/react-toggle-group": "1.1.11" - }, - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toolbar/node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toolbar/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toolbar/node_modules/@radix-ui/react-separator": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", - "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toolbar/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tooltip": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", - "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-visually-hidden": "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", - "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", - "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-effect-event": "0.0.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-effect-event": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", - "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", - "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-callback-ref": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-is-hydrated": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz", - "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==", - "license": "MIT", - "dependencies": { - "use-sync-external-store": "^1.5.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", - "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-previous": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", - "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-rect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", - "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", - "license": "MIT", - "dependencies": { - "@radix-ui/rect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-size": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", - "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-visually-hidden": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", - "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/rect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", - "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", - "license": "MIT" - }, - "node_modules/@reduxjs/toolkit": { - "version": "2.11.2", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", - "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - }, - "react-redux": { - "optional": true - } - } - }, - "node_modules/@reduxjs/toolkit/node_modules/immer": { - "version": "11.1.6", - "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.6.tgz", - "integrity": "sha512-uwrF08UBQfxk49i9WcUeCx045wjB1zXEHNJmbYHPVVspxmjwSeWCoKbB8DEIvs3XkBJV6lcRAyLaWJ2+u3MMCw==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" - } - }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz", - "integrity": "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz", - "integrity": "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz", - "integrity": "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz", - "integrity": "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz", - "integrity": "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz", - "integrity": "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz", - "integrity": "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.17.tgz", - "integrity": "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.17.tgz", - "integrity": "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz", - "integrity": "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz", - "integrity": "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.17.tgz", - "integrity": "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz", - "integrity": "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==", - "cpu": [ - "wasm32" - ], - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "1.10.0", - "@emnapi/runtime": "1.10.0", - "@napi-rs/wasm-runtime": "^1.1.4" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz", - "integrity": "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz", - "integrity": "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.7", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", - "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sec-ant/readable-stream": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", - "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", - "license": "MIT" - }, - "node_modules/@shikijs/core": { - "version": "3.23.0", - "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.23.0.tgz", - "integrity": "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA==", - "license": "MIT", - "dependencies": { - "@shikijs/types": "3.23.0", - "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.4", - "hast-util-to-html": "^9.0.5" - } - }, - "node_modules/@shikijs/engine-javascript": { - "version": "3.23.0", - "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-3.23.0.tgz", - "integrity": "sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA==", - "license": "MIT", - "dependencies": { - "@shikijs/types": "3.23.0", - "@shikijs/vscode-textmate": "^10.0.2", - "oniguruma-to-es": "^4.3.4" - } - }, - "node_modules/@shikijs/engine-oniguruma": { - "version": "3.23.0", - "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.23.0.tgz", - "integrity": "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g==", - "license": "MIT", - "dependencies": { - "@shikijs/types": "3.23.0", - "@shikijs/vscode-textmate": "^10.0.2" - } - }, - "node_modules/@shikijs/langs": { - "version": "3.23.0", - "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.23.0.tgz", - "integrity": "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg==", - "license": "MIT", - "dependencies": { - "@shikijs/types": "3.23.0" - } - }, - "node_modules/@shikijs/themes": { - "version": "3.23.0", - "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.23.0.tgz", - "integrity": "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA==", - "license": "MIT", - "dependencies": { - "@shikijs/types": "3.23.0" - } - }, - "node_modules/@shikijs/types": { - "version": "3.23.0", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.23.0.tgz", - "integrity": "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ==", - "license": "MIT", - "dependencies": { - "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.4" - } - }, - "node_modules/@shikijs/vscode-textmate": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", - "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", - "license": "MIT" - }, - "node_modules/@sindresorhus/merge-streams": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", - "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "license": "MIT" - }, - "node_modules/@standard-schema/utils": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", - "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", - "license": "MIT" - }, - "node_modules/@streamdown/cjk": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@streamdown/cjk/-/cjk-1.0.3.tgz", - "integrity": "sha512-WRg8HR/gHbBoTgsMd91OKFUClIoDcEFVofJvluvEAyjx3KpU0aGgD9tGDqHkHj14ShoMSkX0IYetWGegTcwIJw==", - "license": "Apache-2.0", - "dependencies": { - "remark-cjk-friendly": "^2.0.1", - "remark-cjk-friendly-gfm-strikethrough": "^2.0.1", - "unist-util-visit": "^5.0.0" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/@streamdown/code": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@streamdown/code/-/code-1.1.1.tgz", - "integrity": "sha512-i7HTNuDgZWb+VdrNVOam9gQhIc5MSSDXKWXgbUrn/4vSRaSMM+Rtl10MQj4wLWPNpF7p80waJsAqFP8HZfb0Jg==", - "license": "Apache-2.0", - "dependencies": { - "shiki": "^3.19.0" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/@streamdown/math": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@streamdown/math/-/math-1.0.2.tgz", - "integrity": "sha512-r8Ur9/lBuFnzZAFdEWrLUF2s/gRwRRRwruqltdZibyjbCBnuW7SJbFm26nXqvpJPW/gzpBUMrBVBzd88z05D5g==", - "license": "Apache-2.0", - "dependencies": { - "katex": "^0.16.27", - "rehype-katex": "^7.0.1", - "remark-math": "^6.0.0" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/@streamdown/mermaid": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@streamdown/mermaid/-/mermaid-1.0.2.tgz", - "integrity": "sha512-Fr/4sBWnAeSnxM3PcrV/+DiZe5oPMq9gOkUIAH7ZauJeuwrZ/DVzD4g0zlav6AH0axh2m/sOfrfLtY5aLT7niw==", - "license": "Apache-2.0", - "dependencies": { - "mermaid": "^11.12.2" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/@swc/helpers": { - "version": "0.5.15", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", - "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.8.0" - } - }, - "node_modules/@tabby_ai/hijri-converter": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@tabby_ai/hijri-converter/-/hijri-converter-1.0.5.tgz", - "integrity": "sha512-r5bClKrcIusDoo049dSL8CawnHR6mRdDwhlQuIgZRNty68q0x8k3Lf1BtPAMxRf/GgnHBnIO4ujd3+GQdLWzxQ==", - "license": "MIT", - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@tailwindcss/node": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.4.tgz", - "integrity": "sha512-Ai7+yQPxz3ddrDQzFfBKdHEVBg0w3Zl83jnjuwxnZOsnH9pGn93QHQtpU0p/8rYWxvbFZHneni6p1BSLK4DkGA==", - "license": "MIT", - "dependencies": { - "@jridgewell/remapping": "^2.3.5", - "enhanced-resolve": "^5.19.0", - "jiti": "^2.6.1", - "lightningcss": "1.32.0", - "magic-string": "^0.30.21", - "source-map-js": "^1.2.1", - "tailwindcss": "4.2.4" - } - }, - "node_modules/@tailwindcss/oxide": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.4.tgz", - "integrity": "sha512-9El/iI069DKDSXwTvB9J4BwdO5JhRrOweGaK25taBAvBXyXqJAX+Jqdvs8r8gKpsI/1m0LeJLyQYTf/WLrBT1Q==", - "license": "MIT", - "engines": { - "node": ">= 20" - }, - "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.2.4", - "@tailwindcss/oxide-darwin-arm64": "4.2.4", - "@tailwindcss/oxide-darwin-x64": "4.2.4", - "@tailwindcss/oxide-freebsd-x64": "4.2.4", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.4", - "@tailwindcss/oxide-linux-arm64-gnu": "4.2.4", - "@tailwindcss/oxide-linux-arm64-musl": "4.2.4", - "@tailwindcss/oxide-linux-x64-gnu": "4.2.4", - "@tailwindcss/oxide-linux-x64-musl": "4.2.4", - "@tailwindcss/oxide-wasm32-wasi": "4.2.4", - "@tailwindcss/oxide-win32-arm64-msvc": "4.2.4", - "@tailwindcss/oxide-win32-x64-msvc": "4.2.4" - } - }, - "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.4.tgz", - "integrity": "sha512-e7MOr1SAn9U8KlZzPi1ZXGZHeC5anY36qjNwmZv9pOJ8E4Q6jmD1vyEHkQFmNOIN7twGPEMXRHmitN4zCMN03g==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.4.tgz", - "integrity": "sha512-tSC/Kbqpz/5/o/C2sG7QvOxAKqyd10bq+ypZNf+9Fi2TvbVbv1zNpcEptcsU7DPROaSbVgUXmrzKhurFvo5eDg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.4.tgz", - "integrity": "sha512-yPyUXn3yO/ufR6+Kzv0t4fCg2qNr90jxXc5QqBpjlPNd0NqyDXcmQb/6weunH/MEDXW5dhyEi+agTDiqa3WsGg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.4.tgz", - "integrity": "sha512-BoMIB4vMQtZsXdGLVc2z+P9DbETkiopogfWZKbWwM8b/1Vinbs4YcUwo+kM/KeLkX3Ygrf4/PsRndKaYhS8Eiw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.4.tgz", - "integrity": "sha512-7pIHBLTHYRAlS7V22JNuTh33yLH4VElwKtB3bwchK/UaKUPpQ0lPQiOWcbm4V3WP2I6fNIJ23vABIvoy2izdwA==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.4.tgz", - "integrity": "sha512-+E4wxJ0ZGOzSH325reXTWB48l42i93kQqMvDyz5gqfRzRZ7faNhnmvlV4EPGJU3QJM/3Ab5jhJ5pCRUsKn6OQw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.4.tgz", - "integrity": "sha512-bBADEGAbo4ASnppIziaQJelekCxdMaxisrk+fB7Thit72IBnALp9K6ffA2G4ruj90G9XRS2VQ6q2bCKbfFV82g==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.4.tgz", - "integrity": "sha512-7Mx25E4WTfnht0TVRTyC00j3i0M+EeFe7wguMDTlX4mRxafznw0CA8WJkFjWYH5BlgELd1kSjuU2JiPnNZbJDA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.4.tgz", - "integrity": "sha512-2wwJRF7nyhOR0hhHoChc04xngV3iS+akccHTGtz965FwF0up4b2lOdo6kI1EbDaEXKgvcrFBYcYQQ/rrnWFVfA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.4.tgz", - "integrity": "sha512-FQsqApeor8Fo6gUEklzmaa9994orJZZDBAlQpK2Mq+DslRKFJeD6AjHpBQ0kZFQohVr8o85PPh8eOy86VlSCmw==", - "bundleDependencies": [ - "@napi-rs/wasm-runtime", - "@emnapi/core", - "@emnapi/runtime", - "@tybys/wasm-util", - "@emnapi/wasi-threads", - "tslib" - ], - "cpu": [ - "wasm32" - ], - "license": "MIT", - "optional": true, - "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" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.8.1", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.1.0", - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.8.1", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { - "version": "1.1.0", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.1", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1", - "@tybys/wasm-util": "^0.10.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { - "version": "2.8.1", - "inBundle": true, - "license": "0BSD", - "optional": true - }, - "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.4.tgz", - "integrity": "sha512-L9BXqxC4ToVgwMFqj3pmZRqyHEztulpUJzCxUtLjobMCzTPsGt1Fa9enKbOpY2iIyVtaHNeNvAK8ERP/64sqGQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.4.tgz", - "integrity": "sha512-ESlKG0EpVJQwRjXDDa9rLvhEAh0mhP1sF7sap9dNZT0yyl9SAG6T7gdP09EH0vIv0UNTlo6jPWyujD6559fZvw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/vite": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.4.tgz", - "integrity": "sha512-pCvohwOCspk3ZFn6eJzrrX3g4n2JY73H6MmYC87XfGPyTty4YsCjYTMArRZm/zOI8dIt3+EcrLHAFPe5A4bgtw==", - "license": "MIT", - "dependencies": { - "@tailwindcss/node": "4.2.4", - "@tailwindcss/oxide": "4.2.4", - "tailwindcss": "4.2.4" - }, - "peerDependencies": { - "vite": "^5.2.0 || ^6 || ^7 || ^8" - } - }, - "node_modules/@tanstack/history": { - "version": "1.161.6", - "resolved": "https://registry.npmjs.org/@tanstack/history/-/history-1.161.6.tgz", - "integrity": "sha512-NaOGLRrddszbQj9upGat6HG/4TKvXLvu+osAIgfxPYA+eIvYKv8GKDJOrY2D3/U9MRnKfMWD7bU4jeD4xmqyIg==", - "license": "MIT", - "engines": { - "node": ">=20.19" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@tanstack/react-router": { - "version": "1.169.2", - "resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.169.2.tgz", - "integrity": "sha512-OJM7Kguc7ERnweaNRWsyWgIKcl3z23rD1B4jaxjzd9RGdnzpt2HfrWa9rggbT0Hfzhfo4D2ZmsfoTme035tniQ==", - "license": "MIT", - "dependencies": { - "@tanstack/history": "1.161.6", - "@tanstack/react-store": "^0.9.3", - "@tanstack/router-core": "1.169.2", - "isbot": "^5.1.22" - }, - "engines": { - "node": ">=20.19" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": ">=18.0.0 || >=19.0.0", - "react-dom": ">=18.0.0 || >=19.0.0" - } - }, - "node_modules/@tanstack/react-store": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/@tanstack/react-store/-/react-store-0.9.3.tgz", - "integrity": "sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg==", - "license": "MIT", - "dependencies": { - "@tanstack/store": "0.9.3", - "use-sync-external-store": "^1.6.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "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" - } - }, - "node_modules/@tanstack/react-table": { - "version": "8.21.3", - "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz", - "integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==", - "license": "MIT", - "dependencies": { - "@tanstack/table-core": "8.21.3" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": ">=16.8", - "react-dom": ">=16.8" - } - }, - "node_modules/@tanstack/router-core": { - "version": "1.169.2", - "resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.169.2.tgz", - "integrity": "sha512-5sm0DJF1A7Mz+9gy4Gz/lLovNailK3yot4vYvz9MkBUPw26uLnhQiR8hSCYxucjE0wD6Mdlc5l+Z0/XTlZ7xHw==", - "license": "MIT", - "dependencies": { - "@tanstack/history": "1.161.6", - "cookie-es": "^3.0.0", - "seroval": "^1.5.4", - "seroval-plugins": "^1.5.4" - }, - "engines": { - "node": ">=20.19" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@tanstack/store": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.9.3.tgz", - "integrity": "sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@tanstack/table-core": { - "version": "8.21.3", - "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz", - "integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@tauri-apps/api": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.11.0.tgz", - "integrity": "sha512-7CinYODhky9lmO23xHnUFv0Xt43fbtWMyxZcLcRBlFkcgXKuEirBvHpmtJ89YMhyeGcq20Wuc47Fa4XjyniywA==", - "license": "Apache-2.0 OR MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/tauri" - } - }, - "node_modules/@tauri-apps/plugin-clipboard-manager": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-clipboard-manager/-/plugin-clipboard-manager-2.3.2.tgz", - "integrity": "sha512-CUlb5Hqi2oZbcZf4VUyUH53XWPPdtpw43EUpCza5HWZJwxEoDowFzNUDt1tRUXA8Uq+XPn17Ysfptip33sG4eQ==", - "license": "MIT OR Apache-2.0", - "dependencies": { - "@tauri-apps/api": "^2.8.0" - } - }, - "node_modules/@tauri-apps/plugin-notification": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-notification/-/plugin-notification-2.3.3.tgz", - "integrity": "sha512-Zw+ZH18RJb41G4NrfHgIuofJiymusqN+q8fGUIIV7vyCH+5sSn5coqRv/MWB9qETsUs97vmU045q7OyseCV3Qg==", - "license": "MIT OR Apache-2.0", - "dependencies": { - "@tauri-apps/api": "^2.8.0" - } - }, - "node_modules/@tauri-apps/plugin-opener": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.5.4.tgz", - "integrity": "sha512-1HnPkb+AmgO29HBazm4uPLKB+r7zzcTBW1d0fyYp1uP+jwtpoiNDGKMMzz58SFp49nOIrxdE3aUJtT57lfO9CQ==", - "license": "MIT OR Apache-2.0", - "dependencies": { - "@tauri-apps/api": "^2.11.0" - } - }, - "node_modules/@tauri-apps/plugin-process": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-process/-/plugin-process-2.3.1.tgz", - "integrity": "sha512-nCa4fGVaDL/B9ai03VyPOjfAHRHSBz5v6F/ObsB73r/dA3MHHhZtldaDMIc0V/pnUw9ehzr2iEG+XkSEyC0JJA==", - "license": "MIT OR Apache-2.0", - "dependencies": { - "@tauri-apps/api": "^2.8.0" - } - }, - "node_modules/@tauri-apps/plugin-updater": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-updater/-/plugin-updater-2.10.1.tgz", - "integrity": "sha512-NFYMg+tWOZPJdzE/PpFj2qfqwAWwNS3kXrb1tm1gnBJ9mYzZ4WDRrwy8udzWoAnfGCHLuePNLY1WVCNHnh3eRA==", - "license": "MIT OR Apache-2.0", - "dependencies": { - "@tauri-apps/api": "^2.10.1" - } - }, - "node_modules/@toolwind/corner-shape": { - "version": "0.0.8-3", - "resolved": "https://registry.npmjs.org/@toolwind/corner-shape/-/corner-shape-0.0.8-3.tgz", - "integrity": "sha512-MPIF81F2bhtXbzEeXF0vnL+PKpnopCHOzBspOkK8osMzWQvPUujZn2XZOMdsu4DF6wsVbbRYQtdsJr486HmIPQ==", - "license": "MIT", - "dependencies": { - "@types/node": "^20.4.1" - } - }, - "node_modules/@toolwind/corner-shape/node_modules/@types/node": { - "version": "20.19.39", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz", - "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==", - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@toolwind/corner-shape/node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "license": "MIT" - }, - "node_modules/@ts-morph/common": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.27.0.tgz", - "integrity": "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ==", - "license": "MIT", - "dependencies": { - "fast-glob": "^3.3.3", - "minimatch": "^10.0.1", - "path-browserify": "^1.0.1" - } - }, - "node_modules/@ts-morph/common/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/@ts-morph/common/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/@ts-morph/common/node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.5" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", - "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@types/canvas-confetti": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@types/canvas-confetti/-/canvas-confetti-1.9.0.tgz", - "integrity": "sha512-aBGj/dULrimR1XDZLtG9JwxX1b4HPRF6CX9Yfwh3NvstZEm1ZL7RBnel4keCPSqs1ANRu1u2Aoz9R+VmtjYuTg==", - "license": "MIT" - }, - "node_modules/@types/d3": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", - "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", - "license": "MIT", - "dependencies": { - "@types/d3-array": "*", - "@types/d3-axis": "*", - "@types/d3-brush": "*", - "@types/d3-chord": "*", - "@types/d3-color": "*", - "@types/d3-contour": "*", - "@types/d3-delaunay": "*", - "@types/d3-dispatch": "*", - "@types/d3-drag": "*", - "@types/d3-dsv": "*", - "@types/d3-ease": "*", - "@types/d3-fetch": "*", - "@types/d3-force": "*", - "@types/d3-format": "*", - "@types/d3-geo": "*", - "@types/d3-hierarchy": "*", - "@types/d3-interpolate": "*", - "@types/d3-path": "*", - "@types/d3-polygon": "*", - "@types/d3-quadtree": "*", - "@types/d3-random": "*", - "@types/d3-scale": "*", - "@types/d3-scale-chromatic": "*", - "@types/d3-selection": "*", - "@types/d3-shape": "*", - "@types/d3-time": "*", - "@types/d3-time-format": "*", - "@types/d3-timer": "*", - "@types/d3-transition": "*", - "@types/d3-zoom": "*" - } - }, - "node_modules/@types/d3-array": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", - "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", - "license": "MIT" - }, - "node_modules/@types/d3-axis": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", - "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", - "license": "MIT", - "dependencies": { - "@types/d3-selection": "*" - } - }, - "node_modules/@types/d3-brush": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", - "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", - "license": "MIT", - "dependencies": { - "@types/d3-selection": "*" - } - }, - "node_modules/@types/d3-chord": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", - "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", - "license": "MIT" - }, - "node_modules/@types/d3-color": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", - "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", - "license": "MIT" - }, - "node_modules/@types/d3-contour": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", - "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", - "license": "MIT", - "dependencies": { - "@types/d3-array": "*", - "@types/geojson": "*" - } - }, - "node_modules/@types/d3-delaunay": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", - "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", - "license": "MIT" - }, - "node_modules/@types/d3-dispatch": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", - "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", - "license": "MIT" - }, - "node_modules/@types/d3-drag": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", - "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", - "license": "MIT", - "dependencies": { - "@types/d3-selection": "*" - } - }, - "node_modules/@types/d3-dsv": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", - "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", - "license": "MIT" - }, - "node_modules/@types/d3-ease": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", - "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", - "license": "MIT" - }, - "node_modules/@types/d3-fetch": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", - "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", - "license": "MIT", - "dependencies": { - "@types/d3-dsv": "*" - } - }, - "node_modules/@types/d3-force": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", - "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", - "license": "MIT" - }, - "node_modules/@types/d3-format": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", - "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", - "license": "MIT" - }, - "node_modules/@types/d3-geo": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", - "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", - "license": "MIT", - "dependencies": { - "@types/geojson": "*" - } - }, - "node_modules/@types/d3-hierarchy": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", - "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", - "license": "MIT" - }, - "node_modules/@types/d3-interpolate": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", - "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", - "license": "MIT", - "dependencies": { - "@types/d3-color": "*" - } - }, - "node_modules/@types/d3-path": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", - "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", - "license": "MIT" - }, - "node_modules/@types/d3-polygon": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", - "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", - "license": "MIT" - }, - "node_modules/@types/d3-quadtree": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", - "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", - "license": "MIT" - }, - "node_modules/@types/d3-random": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", - "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", - "license": "MIT" - }, - "node_modules/@types/d3-scale": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", - "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", - "license": "MIT", - "dependencies": { - "@types/d3-time": "*" - } - }, - "node_modules/@types/d3-scale-chromatic": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", - "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", - "license": "MIT" - }, - "node_modules/@types/d3-selection": { - "version": "3.0.11", - "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", - "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", - "license": "MIT" - }, - "node_modules/@types/d3-shape": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", - "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", - "license": "MIT", - "dependencies": { - "@types/d3-path": "*" - } - }, - "node_modules/@types/d3-time": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", - "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", - "license": "MIT" - }, - "node_modules/@types/d3-time-format": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", - "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", - "license": "MIT" - }, - "node_modules/@types/d3-timer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", - "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", - "license": "MIT" - }, - "node_modules/@types/d3-transition": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", - "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", - "license": "MIT", - "dependencies": { - "@types/d3-selection": "*" - } - }, - "node_modules/@types/d3-zoom": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", - "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", - "license": "MIT", - "dependencies": { - "@types/d3-interpolate": "*", - "@types/d3-selection": "*" - } - }, - "node_modules/@types/debug": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", - "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", - "license": "MIT", - "dependencies": { - "@types/ms": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "license": "MIT" - }, - "node_modules/@types/estree-jsx": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", - "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", - "license": "MIT", - "dependencies": { - "@types/estree": "*" - } - }, - "node_modules/@types/geojson": { - "version": "7946.0.16", - "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", - "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", - "license": "MIT" - }, - "node_modules/@types/hast": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", - "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "*" - } - }, - "node_modules/@types/js-yaml": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", - "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/katex": { - "version": "0.16.8", - "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.8.tgz", - "integrity": "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==", - "license": "MIT" - }, - "node_modules/@types/mdast": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", - "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", - "license": "MIT", - "dependencies": { - "@types/unist": "*" - } - }, - "node_modules/@types/ms": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", - "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "25.6.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", - "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", - "license": "MIT", - "dependencies": { - "undici-types": "~7.19.0" - } - }, - "node_modules/@types/react": { - "version": "19.2.14", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", - "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", - "license": "MIT", - "dependencies": { - "csstype": "^3.2.2" - } - }, - "node_modules/@types/react-dom": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "devOptional": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^19.2.0" - } - }, - "node_modules/@types/set-cookie-parser": { - "version": "2.4.10", - "resolved": "https://registry.npmjs.org/@types/set-cookie-parser/-/set-cookie-parser-2.4.10.tgz", - "integrity": "sha512-GGmQVGpQWUe5qglJozEjZV/5dyxbOOZ0LHe/lqyWssB88Y4svNfst0uqBVscdDeIKl5Jy5+aPSvy7mI9tYRguw==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/statuses": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.6.tgz", - "integrity": "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==", - "license": "MIT" - }, - "node_modules/@types/trusted-types": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", - "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "license": "MIT", - "optional": true - }, - "node_modules/@types/unist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", - "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", - "license": "MIT" - }, - "node_modules/@types/use-sync-external-store": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", - "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", - "license": "MIT" - }, - "node_modules/@types/validate-npm-package-name": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/validate-npm-package-name/-/validate-npm-package-name-4.0.2.tgz", - "integrity": "sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==", - "license": "MIT" - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.2.tgz", - "integrity": "sha512-j/bwmkBvHUtPNxzuWe5z6BEk3q54YRyGlBXkSsmfoih7zNrBvl5A9A98anlp/7JbyZcWIJ8KXo/3Tq/DjFLtuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.59.2", - "@typescript-eslint/type-utils": "8.59.2", - "@typescript-eslint/utils": "8.59.2", - "@typescript-eslint/visitor-keys": "8.59.2", - "ignore": "^7.0.5", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.59.2", - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.2.tgz", - "integrity": "sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.59.2", - "@typescript-eslint/types": "8.59.2", - "@typescript-eslint/typescript-estree": "8.59.2", - "@typescript-eslint/visitor-keys": "8.59.2", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.2.tgz", - "integrity": "sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.59.2", - "@typescript-eslint/types": "^8.59.2", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.2.tgz", - "integrity": "sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.59.2", - "@typescript-eslint/visitor-keys": "8.59.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.2.tgz", - "integrity": "sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.2.tgz", - "integrity": "sha512-nhqaj1nmTdVVl/BP5omXNRGO38jn5iosis2vbdmupF2txCf8ylWT8lx+JlvMYYVqzGVKtjojUFoQ3JRWK+mfzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.59.2", - "@typescript-eslint/typescript-estree": "8.59.2", - "@typescript-eslint/utils": "8.59.2", - "debug": "^4.4.3", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.2.tgz", - "integrity": "sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.2.tgz", - "integrity": "sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.59.2", - "@typescript-eslint/tsconfig-utils": "8.59.2", - "@typescript-eslint/types": "8.59.2", - "@typescript-eslint/visitor-keys": "8.59.2", - "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.5" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.2.tgz", - "integrity": "sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.59.2", - "@typescript-eslint/types": "8.59.2", - "@typescript-eslint/typescript-estree": "8.59.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.2.tgz", - "integrity": "sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.59.2", - "eslint-visitor-keys": "^5.0.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@ungap/structured-clone": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", - "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", - "license": "ISC" - }, - "node_modules/@upsetjs/venn.js": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@upsetjs/venn.js/-/venn.js-2.0.0.tgz", - "integrity": "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==", - "license": "MIT", - "optionalDependencies": { - "d3-selection": "^3.0.0", - "d3-transition": "^3.0.1" - } - }, - "node_modules/@vitejs/plugin-react": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", - "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rolldown/pluginutils": "1.0.0-rc.7" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "peerDependencies": { - "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", - "babel-plugin-react-compiler": "^1.0.0", - "vite": "^8.0.0" - }, - "peerDependenciesMeta": { - "@rolldown/plugin-babel": { - "optional": true - }, - "babel-plugin-react-compiler": { - "optional": true - } - } - }, - "node_modules/@xmldom/xmldom": { - "version": "0.8.13", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz", - "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@xyflow/react": { - "version": "12.10.2", - "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.10.2.tgz", - "integrity": "sha512-CgIi6HwlcHXwlkTpr0fxLv/0sRVNZ8IdwKLzzeCscaYBwpvfcH1QFOCeaTCuEn1FQEs/B8CjnTSjhs8udgmBgQ==", - "license": "MIT", - "dependencies": { - "@xyflow/system": "0.0.76", - "classcat": "^5.0.3", - "zustand": "^4.4.0" - }, - "peerDependencies": { - "react": ">=17", - "react-dom": ">=17" - } - }, - "node_modules/@xyflow/react/node_modules/zustand": { - "version": "4.5.7", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", - "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", - "license": "MIT", - "dependencies": { - "use-sync-external-store": "^1.2.2" - }, - "engines": { - "node": ">=12.7.0" - }, - "peerDependencies": { - "@types/react": ">=16.8", - "immer": ">=9.0.6", - "react": ">=16.8" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "immer": { - "optional": true - }, - "react": { - "optional": true - } - } - }, - "node_modules/@xyflow/system": { - "version": "0.0.76", - "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.76.tgz", - "integrity": "sha512-hvwvnRS1B3REwVDlWexsq7YQaPZeG3/mKo1jv38UmnpWmxihp14bW6VtEOuHEwJX2FvzFw8k77LyKSk/wiZVNA==", - "license": "MIT", - "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" - } - }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/ajv": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", - "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", - "dev": true, - "license": "MIT", - "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" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", - "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, - "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "license": "Python-2.0" - }, - "node_modules/aria-hidden": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", - "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/assistant-cloud": { - "version": "0.1.27", - "resolved": "https://registry.npmjs.org/assistant-cloud/-/assistant-cloud-0.1.27.tgz", - "integrity": "sha512-BGfVnx7YFN5xtB/kbrgGxRI0TfSWq4yxB3MwYn6RDPlv4JvdtPupvDC1Y6An0EhAe42Z0AYtSmDSsR6p6eeBng==", - "license": "MIT", - "dependencies": { - "assistant-stream": "^0.3.12" - } - }, - "node_modules/assistant-stream": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/assistant-stream/-/assistant-stream-0.3.12.tgz", - "integrity": "sha512-ZdfdyeZjeffkUfZLGTre9rW+9nBSPi6U5tYvchYjAxVuyiYVf5H9vw7SxegTq5bAMT9IitpDOaYMZGWFoMtaow==", - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.1.0", - "nanoid": "^5.1.9", - "secure-json-parse": "^4.1.0" - } - }, - "node_modules/ast-types": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", - "integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/bail": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", - "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/baseline-browser-mapping": { - "version": "2.10.27", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.27.tgz", - "integrity": "sha512-zEs/ufmZoUd7WftKpKyXaT6RFxpQ5Qm9xytKRHvJfxFV9DFJkZph9RvJ1LcOUi0Z1ZVijMte65JbILeV+8QQEA==", - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/bluebird": { - "version": "3.4.7", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", - "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==", - "license": "MIT" - }, - "node_modules/body-parser": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", - "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.3", - "http-errors": "^2.0.0", - "iconv-lite": "^0.7.0", - "on-finished": "^2.4.1", - "qs": "^6.14.1", - "raw-body": "^3.0.1", - "type-is": "^2.0.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/body-parser/node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.28.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", - "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.10.12", - "caniuse-lite": "^1.0.30001782", - "electron-to-chromium": "^1.5.328", - "node-releases": "^2.0.36", - "update-browserslist-db": "^1.2.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/bundle-name": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", - "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", - "license": "MIT", - "dependencies": { - "run-applescript": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001791", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz", - "integrity": "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/canvas-confetti": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/canvas-confetti/-/canvas-confetti-1.9.4.tgz", - "integrity": "sha512-yxQbJkAVrFXWNbTUjPqjF7G+g6pDotOUHGbkZq2NELZUMDpiJ85rIEazVb8GTaAptNW2miJAXbs1BtioA251Pw==", - "license": "ISC", - "funding": { - "type": "donate", - "url": "https://www.paypal.me/kirilvatev" - } - }, - "node_modules/ccount": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", - "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chalk/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/character-entities": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", - "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-entities-html4": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", - "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-entities-legacy": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", - "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-reference-invalid": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", - "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/chevrotain": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-12.0.0.tgz", - "integrity": "sha512-csJvb+6kEiQaqo1woTdSAuOWdN0WTLIydkKrBnS+V5gZz0oqBrp4kQ35519QgK6TpBThiG3V1vNSHlIkv4AglQ==", - "license": "Apache-2.0", - "dependencies": { - "@chevrotain/cst-dts-gen": "12.0.0", - "@chevrotain/gast": "12.0.0", - "@chevrotain/regexp-to-ast": "12.0.0", - "@chevrotain/types": "12.0.0", - "@chevrotain/utils": "12.0.0" - }, - "engines": { - "node": ">=22.0.0" - } - }, - "node_modules/chevrotain-allstar": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.4.3.tgz", - "integrity": "sha512-2X4mkroolSMKqW+H22pyPMUVDqYZzPhephTmg/NODKb1IGYPHfxfhcW0EjS7wcPJNbze2i4vBWT7zT5FKF2lrQ==", - "license": "MIT", - "dependencies": { - "lodash-es": "^4.18.1" - }, - "peerDependencies": { - "chevrotain": "^12.0.0" - } - }, - "node_modules/class-variance-authority": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", - "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", - "license": "Apache-2.0", - "dependencies": { - "clsx": "^2.1.1" - }, - "funding": { - "url": "https://polar.sh/cva" - } - }, - "node_modules/classcat": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", - "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", - "license": "MIT" - }, - "node_modules/classnames": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", - "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", - "license": "MIT" - }, - "node_modules/cli-cursor": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", - "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", - "license": "MIT", - "dependencies": { - "restore-cursor": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-progress": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz", - "integrity": "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==", - "license": "MIT", - "optional": true, - "dependencies": { - "string-width": "^4.2.3" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/cli-spinners": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", - "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-width": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", - "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", - "license": "ISC", - "engines": { - "node": ">= 12" - } - }, - "node_modules/client-only": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", - "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", - "license": "MIT" - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/cliui/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/cmdk": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz", - "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "^1.1.1", - "@radix-ui/react-dialog": "^1.1.6", - "@radix-ui/react-id": "^1.1.0", - "@radix-ui/react-primitive": "^2.0.2" - }, - "peerDependencies": { - "react": "^18 || ^19 || ^19.0.0-rc", - "react-dom": "^18 || ^19 || ^19.0.0-rc" - } - }, - "node_modules/code-block-writer": { - "version": "13.0.3", - "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz", - "integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==", - "license": "MIT" - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT" - }, - "node_modules/comma-separated-tokens": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", - "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/commander": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/confbox": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", - "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", - "license": "MIT" - }, - "node_modules/content-disposition": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", - "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "license": "MIT" - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-es": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-3.1.1.tgz", - "integrity": "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==", - "license": "MIT" - }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "license": "MIT" - }, - "node_modules/cors": { - "version": "2.8.6", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", - "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/cose-base": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", - "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", - "license": "MIT", - "dependencies": { - "layout-base": "^1.0.0" - } - }, - "node_modules/cosmiconfig": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz", - "integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==", - "license": "MIT", - "dependencies": { - "env-paths": "^2.2.1", - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "license": "MIT" - }, - "node_modules/cytoscape": { - "version": "3.33.3", - "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.3.tgz", - "integrity": "sha512-Gej7U+OKR+LZ8kvX7rb2HhCYJ0IhvEFsnkud4SB1PR+BUY/TsSO0dmOW59WEVLu51b1Rm+gQRKoz4bLYxGSZ2g==", - "license": "MIT", - "engines": { - "node": ">=0.10" - } - }, - "node_modules/cytoscape-cose-bilkent": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", - "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", - "license": "MIT", - "dependencies": { - "cose-base": "^1.0.0" - }, - "peerDependencies": { - "cytoscape": "^3.2.0" - } - }, - "node_modules/cytoscape-fcose": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", - "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", - "license": "MIT", - "dependencies": { - "cose-base": "^2.2.0" - }, - "peerDependencies": { - "cytoscape": "^3.2.0" - } - }, - "node_modules/cytoscape-fcose/node_modules/cose-base": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz", - "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", - "license": "MIT", - "dependencies": { - "layout-base": "^2.0.0" - } - }, - "node_modules/cytoscape-fcose/node_modules/layout-base": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz", - "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==", - "license": "MIT" - }, - "node_modules/d3": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", - "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", - "license": "ISC", - "dependencies": { - "d3-array": "3", - "d3-axis": "3", - "d3-brush": "3", - "d3-chord": "3", - "d3-color": "3", - "d3-contour": "4", - "d3-delaunay": "6", - "d3-dispatch": "3", - "d3-drag": "3", - "d3-dsv": "3", - "d3-ease": "3", - "d3-fetch": "3", - "d3-force": "3", - "d3-format": "3", - "d3-geo": "3", - "d3-hierarchy": "3", - "d3-interpolate": "3", - "d3-path": "3", - "d3-polygon": "3", - "d3-quadtree": "3", - "d3-random": "3", - "d3-scale": "4", - "d3-scale-chromatic": "3", - "d3-selection": "3", - "d3-shape": "3", - "d3-time": "3", - "d3-time-format": "4", - "d3-timer": "3", - "d3-transition": "3", - "d3-zoom": "3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-array": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", - "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", - "license": "ISC", - "dependencies": { - "internmap": "1 - 2" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-axis": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", - "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-brush": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", - "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", - "license": "ISC", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-drag": "2 - 3", - "d3-interpolate": "1 - 3", - "d3-selection": "3", - "d3-transition": "3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-chord": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", - "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", - "license": "ISC", - "dependencies": { - "d3-path": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-color": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", - "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-contour": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", - "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", - "license": "ISC", - "dependencies": { - "d3-array": "^3.2.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-delaunay": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", - "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", - "license": "ISC", - "dependencies": { - "delaunator": "5" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-dispatch": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", - "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-drag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", - "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", - "license": "ISC", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-selection": "3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-dsv": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", - "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", - "license": "ISC", - "dependencies": { - "commander": "7", - "iconv-lite": "0.6", - "rw": "1" - }, - "bin": { - "csv2json": "bin/dsv2json.js", - "csv2tsv": "bin/dsv2dsv.js", - "dsv2dsv": "bin/dsv2dsv.js", - "dsv2json": "bin/dsv2json.js", - "json2csv": "bin/json2dsv.js", - "json2dsv": "bin/json2dsv.js", - "json2tsv": "bin/json2dsv.js", - "tsv2csv": "bin/dsv2dsv.js", - "tsv2json": "bin/dsv2json.js" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-dsv/node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, - "node_modules/d3-ease": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", - "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-fetch": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", - "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", - "license": "ISC", - "dependencies": { - "d3-dsv": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-force": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", - "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", - "license": "ISC", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-quadtree": "1 - 3", - "d3-timer": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-format": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", - "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-geo": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", - "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", - "license": "ISC", - "dependencies": { - "d3-array": "2.5.0 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-hierarchy": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", - "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-interpolate": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", - "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", - "license": "ISC", - "dependencies": { - "d3-color": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-path": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", - "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-polygon": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", - "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-quadtree": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", - "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-random": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", - "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-sankey": { - "version": "0.12.3", - "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", - "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-array": "1 - 2", - "d3-shape": "^1.2.0" - } - }, - "node_modules/d3-sankey/node_modules/d3-array": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", - "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", - "license": "BSD-3-Clause", - "dependencies": { - "internmap": "^1.0.0" - } - }, - "node_modules/d3-sankey/node_modules/d3-path": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", - "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", - "license": "BSD-3-Clause" - }, - "node_modules/d3-sankey/node_modules/d3-shape": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", - "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-path": "1" - } - }, - "node_modules/d3-sankey/node_modules/internmap": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", - "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", - "license": "ISC" - }, - "node_modules/d3-scale": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", - "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", - "license": "ISC", - "dependencies": { - "d3-array": "2.10.0 - 3", - "d3-format": "1 - 3", - "d3-interpolate": "1.2.0 - 3", - "d3-time": "2.1.1 - 3", - "d3-time-format": "2 - 4" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-scale-chromatic": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", - "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", - "license": "ISC", - "dependencies": { - "d3-color": "1 - 3", - "d3-interpolate": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-selection": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", - "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-shape": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", - "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", - "license": "ISC", - "dependencies": { - "d3-path": "^3.1.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-time": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", - "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", - "license": "ISC", - "dependencies": { - "d3-array": "2 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-time-format": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", - "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", - "license": "ISC", - "dependencies": { - "d3-time": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-timer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", - "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-transition": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", - "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", - "license": "ISC", - "dependencies": { - "d3-color": "1 - 3", - "d3-dispatch": "1 - 3", - "d3-ease": "1 - 3", - "d3-interpolate": "1 - 3", - "d3-timer": "1 - 3" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "d3-selection": "2 - 3" - } - }, - "node_modules/d3-zoom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", - "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", - "license": "ISC", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-drag": "2 - 3", - "d3-interpolate": "1 - 3", - "d3-selection": "2 - 3", - "d3-transition": "2 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/dagre-d3-es": { - "version": "7.0.14", - "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.14.tgz", - "integrity": "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==", - "license": "MIT", - "dependencies": { - "d3": "^7.9.0", - "lodash-es": "^4.17.21" - } - }, - "node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/date-fns": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", - "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/kossnocorp" - } - }, - "node_modules/date-fns-jalali": { - "version": "4.1.0-0", - "resolved": "https://registry.npmjs.org/date-fns-jalali/-/date-fns-jalali-4.1.0-0.tgz", - "integrity": "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==", - "license": "MIT" - }, - "node_modules/dayjs": { - "version": "1.11.20", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", - "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/decimal.js-light": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", - "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", - "license": "MIT" - }, - "node_modules/decode-named-character-reference": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", - "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", - "license": "MIT", - "dependencies": { - "character-entities": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/dedent": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", - "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", - "license": "MIT", - "peerDependencies": { - "babel-plugin-macros": "^3.1.0" - }, - "peerDependenciesMeta": { - "babel-plugin-macros": { - "optional": true - } - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/default-browser": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", - "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", - "license": "MIT", - "dependencies": { - "bundle-name": "^4.1.0", - "default-browser-id": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/default-browser-id": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", - "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/define-lazy-prop": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", - "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/delaunator": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz", - "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", - "license": "ISC", - "dependencies": { - "robust-predicates": "^3.0.2" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/detect-node-es": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", - "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", - "license": "MIT" - }, - "node_modules/devlop": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", - "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", - "license": "MIT", - "dependencies": { - "dequal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/dexie": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/dexie/-/dexie-4.4.2.tgz", - "integrity": "sha512-zMtV8q79EFE5U8FKZvt0Y/77PCU/Hr/RDxv1EDeo228L+m/HTbeN2AjoQm674rhQCX8n3ljK87lajt7UQuZfvw==", - "license": "Apache-2.0" - }, - "node_modules/diff": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", - "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/dingbat-to-unicode": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dingbat-to-unicode/-/dingbat-to-unicode-1.0.1.tgz", - "integrity": "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==", - "license": "BSD-2-Clause" - }, - "node_modules/dompurify": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.2.tgz", - "integrity": "sha512-lHeS9SA/IKeIFFyYciHBr2n0v1VMPlSj843HdLOwjb2OxNwdq9Xykxqhk+FE42MzAdHvInbAolSE4mhahPpjXA==", - "license": "(MPL-2.0 OR Apache-2.0)", - "optionalDependencies": { - "@types/trusted-types": "^2.0.7" - } - }, - "node_modules/dotenv": { - "version": "17.4.2", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", - "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/duck": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/duck/-/duck-0.1.12.tgz", - "integrity": "sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==", - "license": "BSD", - "dependencies": { - "underscore": "^1.13.1" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/eciesjs": { - "version": "0.4.18", - "resolved": "https://registry.npmjs.org/eciesjs/-/eciesjs-0.4.18.tgz", - "integrity": "sha512-wG99Zcfcys9fZux7Cft8BAX/YrOJLJSZ3jyYPfhZHqN2E+Ffx+QXBDsv3gubEgPtV6dTzJMSQUwk1H98/t/0wQ==", - "license": "MIT", - "dependencies": { - "@ecies/ciphers": "^0.2.5", - "@noble/ciphers": "^1.3.0", - "@noble/curves": "^1.9.7", - "@noble/hashes": "^1.8.0" - }, - "engines": { - "bun": ">=1", - "deno": ">=2", - "node": ">=16" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/electron-to-chromium": { - "version": "1.5.350", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.350.tgz", - "integrity": "sha512-/KWD4qK8nMqIoJh35Rpc37fiVyOe80mcUQKpfje0Dp9uot2ROuipsh+EriCdfInxjleD5v1S4OlIn41I0LXP0g==", - "license": "ISC" - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/enhanced-resolve": { - "version": "5.21.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.0.tgz", - "integrity": "sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.3.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-toolkit": { - "version": "1.46.1", - "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.46.1.tgz", - "integrity": "sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ==", - "license": "MIT", - "workspaces": [ - "docs", - "benchmarks" - ] - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "9.39.4", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", - "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.2", - "@eslint/config-helpers": "^0.4.2", - "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.5", - "@eslint/js": "9.39.4", - "@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.14.0", - "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.5", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-plugin-react-hooks": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", - "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", - "dev": true, - "license": "MIT", - "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" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" - } - }, - "node_modules/eslint-plugin-react-refresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz", - "integrity": "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "eslint": "^9 || ^10" - } - }, - "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.15.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/esquery": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", - "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estree-util-is-identifier-name": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", - "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", - "license": "MIT" - }, - "node_modules/eventsource": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", - "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", - "license": "MIT", - "dependencies": { - "eventsource-parser": "^3.0.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/eventsource-parser": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.8.tgz", - "integrity": "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==", - "license": "MIT", - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/execa": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz", - "integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==", - "license": "MIT", - "dependencies": { - "@sindresorhus/merge-streams": "^4.0.0", - "cross-spawn": "^7.0.6", - "figures": "^6.1.0", - "get-stream": "^9.0.0", - "human-signals": "^8.0.1", - "is-plain-obj": "^4.1.0", - "is-stream": "^4.0.1", - "npm-run-path": "^6.0.0", - "pretty-ms": "^9.2.0", - "signal-exit": "^4.1.0", - "strip-final-newline": "^4.0.0", - "yoctocolors": "^2.1.1" - }, - "engines": { - "node": "^18.19.0 || >=20.5.0" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/express": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", - "license": "MIT", - "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" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express-rate-limit": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.0.tgz", - "integrity": "sha512-XKhFohWaSBdVJNTi5TaHziqnPkv04I9UQV6q1Wy7Ui6GGQZVW12ojDFwqer14EvCXxjvPG0CyWXx7cAXpALB4Q==", - "license": "MIT", - "dependencies": { - "ip-address": "10.1.0" - }, - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/express-rate-limit" - }, - "peerDependencies": { - "express": ">= 4.11" - } - }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "license": "MIT" - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "license": "MIT" - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-string-truncated-width": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", - "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", - "license": "MIT" - }, - "node_modules/fast-string-width": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", - "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", - "license": "MIT", - "dependencies": { - "fast-string-truncated-width": "^3.0.2" - } - }, - "node_modules/fast-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", - "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/fast-wrap-ansi": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.0.tgz", - "integrity": "sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w==", - "license": "MIT", - "dependencies": { - "fast-string-width": "^3.0.2" - } - }, - "node_modules/fastq": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", - "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - }, - "engines": { - "node": "^12.20 || >= 14.13" - } - }, - "node_modules/figures": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", - "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", - "license": "MIT", - "dependencies": { - "is-unicode-supported": "^2.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/finalhandler": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", - "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", - "dev": true, - "license": "ISC" - }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "license": "MIT", - "dependencies": { - "fetch-blob": "^3.1.2" - }, - "engines": { - "node": ">=12.20.0" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/framer-motion": { - "version": "12.38.0", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.38.0.tgz", - "integrity": "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==", - "license": "MIT", - "dependencies": { - "motion-dom": "^12.38.0", - "motion-utils": "^12.36.0", - "tslib": "^2.4.0" - }, - "peerDependencies": { - "@emotion/is-prop-valid": "*", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/is-prop-valid": { - "optional": true - }, - "react": { - "optional": true - }, - "react-dom": { - "optional": true - } - } - }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/fs-extra": { - "version": "11.3.4", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", - "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/fuzzysort": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fuzzysort/-/fuzzysort-3.1.0.tgz", - "integrity": "sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ==", - "license": "MIT" - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-east-asian-width": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", - "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "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" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-nonce": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", - "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/get-own-enumerable-keys": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/get-own-enumerable-keys/-/get-own-enumerable-keys-1.0.0.tgz", - "integrity": "sha512-PKsK2FSrQCyxcGHsGrLDcK0lx+0Ke+6e8KFFozA9/fIQLhQzPaRvJFdcz7+Axg3jUH/Mq+NI4xa5u/UT2tQskA==", - "license": "MIT", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-stream": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", - "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", - "license": "MIT", - "dependencies": { - "@sec-ant/readable-stream": "^0.4.1", - "is-stream": "^4.0.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "17.6.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz", - "integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "license": "ISC" - }, - "node_modules/graphql": { - "version": "16.13.2", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.13.2.tgz", - "integrity": "sha512-5bJ+nf/UCpAjHM8i06fl7eLyVC9iuNAjm9qzkiu2ZGhM0VscSvS6WDPfAwkdkBuoXGM9FJSbKl6wylMwP9Ktig==", - "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" - } - }, - "node_modules/hachure-fill": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz", - "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==", - "license": "MIT" - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", - "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hast-util-from-dom": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/hast-util-from-dom/-/hast-util-from-dom-5.0.1.tgz", - "integrity": "sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q==", - "license": "ISC", - "dependencies": { - "@types/hast": "^3.0.0", - "hastscript": "^9.0.0", - "web-namespaces": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-from-html": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz", - "integrity": "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "devlop": "^1.1.0", - "hast-util-from-parse5": "^8.0.0", - "parse5": "^7.0.0", - "vfile": "^6.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-from-html-isomorphic": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hast-util-from-html-isomorphic/-/hast-util-from-html-isomorphic-2.0.0.tgz", - "integrity": "sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "hast-util-from-dom": "^5.0.0", - "hast-util-from-html": "^2.0.0", - "unist-util-remove-position": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-from-parse5": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", - "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "devlop": "^1.0.0", - "hastscript": "^9.0.0", - "property-information": "^7.0.0", - "vfile": "^6.0.0", - "vfile-location": "^5.0.0", - "web-namespaces": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-is-element": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", - "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-parse-selector": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", - "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-raw": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz", - "integrity": "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "@ungap/structured-clone": "^1.0.0", - "hast-util-from-parse5": "^8.0.0", - "hast-util-to-parse5": "^8.0.0", - "html-void-elements": "^3.0.0", - "mdast-util-to-hast": "^13.0.0", - "parse5": "^7.0.0", - "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.0", - "web-namespaces": "^2.0.0", - "zwitch": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-sanitize": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/hast-util-sanitize/-/hast-util-sanitize-5.0.2.tgz", - "integrity": "sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@ungap/structured-clone": "^1.0.0", - "unist-util-position": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-to-html": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", - "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "ccount": "^2.0.0", - "comma-separated-tokens": "^2.0.0", - "hast-util-whitespace": "^3.0.0", - "html-void-elements": "^3.0.0", - "mdast-util-to-hast": "^13.0.0", - "property-information": "^7.0.0", - "space-separated-tokens": "^2.0.0", - "stringify-entities": "^4.0.0", - "zwitch": "^2.0.4" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-to-jsx-runtime": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", - "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "comma-separated-tokens": "^2.0.0", - "devlop": "^1.0.0", - "estree-util-is-identifier-name": "^3.0.0", - "hast-util-whitespace": "^3.0.0", - "mdast-util-mdx-expression": "^2.0.0", - "mdast-util-mdx-jsx": "^3.0.0", - "mdast-util-mdxjs-esm": "^2.0.0", - "property-information": "^7.0.0", - "space-separated-tokens": "^2.0.0", - "style-to-js": "^1.0.0", - "unist-util-position": "^5.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-to-parse5": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.1.tgz", - "integrity": "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "comma-separated-tokens": "^2.0.0", - "devlop": "^1.0.0", - "property-information": "^7.0.0", - "space-separated-tokens": "^2.0.0", - "web-namespaces": "^2.0.0", - "zwitch": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-to-text": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz", - "integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "hast-util-is-element": "^3.0.0", - "unist-util-find-after": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-whitespace": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", - "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hastscript": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", - "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "comma-separated-tokens": "^2.0.0", - "hast-util-parse-selector": "^4.0.0", - "property-information": "^7.0.0", - "space-separated-tokens": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/headers-polyfill": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-5.0.1.tgz", - "integrity": "sha512-1TJ6Fih/b8h5TIcv+1+Hw0PDQWJTKDKzFZzcKOiW1wJza3XoAQlkCuXLbymPYB8+ZQyw8mHvdw560e8zVFIWyA==", - "license": "MIT", - "dependencies": { - "@types/set-cookie-parser": "^2.4.10", - "set-cookie-parser": "^3.0.1" - } - }, - "node_modules/hermes-estree": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", - "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", - "dev": true, - "license": "MIT" - }, - "node_modules/hermes-parser": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", - "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "hermes-estree": "0.25.1" - } - }, - "node_modules/hono": { - "version": "4.12.17", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.17.tgz", - "integrity": "sha512-FbJJNb/XgX7YW0hX/V8w5oYLztKEsRLykCMZWt1WdLtsfjzMvmoqWBA4H4t5norinq8/rh20oiZYr+WSl4UzAQ==", - "license": "MIT", - "engines": { - "node": ">=16.9.0" - } - }, - "node_modules/html-url-attributes": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", - "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/html-void-elements": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", - "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/human-signals": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", - "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", - "license": "MIT" - }, - "node_modules/immer": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", - "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/inline-style-parser": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", - "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", - "license": "MIT" - }, - "node_modules/internmap": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", - "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/ip-address": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", - "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-alphabetical": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", - "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-alphanumerical": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", - "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", - "license": "MIT", - "dependencies": { - "is-alphabetical": "^2.0.0", - "is-decimal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "license": "MIT" - }, - "node_modules/is-decimal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", - "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-docker": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", - "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", - "license": "MIT", - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-hexadecimal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", - "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-in-ssh": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-in-ssh/-/is-in-ssh-1.0.0.tgz", - "integrity": "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==", - "license": "MIT", - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-inside-container": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", - "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", - "license": "MIT", - "dependencies": { - "is-docker": "^3.0.0" - }, - "bin": { - "is-inside-container": "cli.js" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-interactive": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", - "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-node-process": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", - "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", - "license": "MIT" - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-obj": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-3.0.0.tgz", - "integrity": "sha512-IlsXEHOjtKhpN8r/tRFj2nDyTmHvcfNeu/nrRIcXE17ROeatXchkojffa1SpdqW4cr/Fj6QkEf/Gn4zf6KKvEQ==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "license": "MIT" - }, - "node_modules/is-regexp": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-3.1.0.tgz", - "integrity": "sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-stream": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", - "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-unicode-supported": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", - "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-wsl": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", - "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", - "license": "MIT", - "dependencies": { - "is-inside-container": "^1.0.0" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "license": "MIT" - }, - "node_modules/isbot": { - "version": "5.1.40", - "resolved": "https://registry.npmjs.org/isbot/-/isbot-5.1.40.tgz", - "integrity": "sha512-yNeeynhhtIVRBk12tBV4eHNxwB42HzR4Q3Ea7vCOiJhImGaAIdIMrbJtacQlBizGLjUPw+akkFI5Dn9T70XoVQ==", - "license": "Unlicense", - "engines": { - "node": ">=18" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" - }, - "node_modules/jiti": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", - "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", - "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "node_modules/jose": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", - "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, - "node_modules/js-tiktoken": { - "version": "1.0.21", - "resolved": "https://registry.npmjs.org/js-tiktoken/-/js-tiktoken-1.0.21.tgz", - "integrity": "sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==", - "license": "MIT", - "dependencies": { - "base64-js": "^1.5.1" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-typed": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", - "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", - "license": "BSD-2-Clause" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsonfile": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/jszip": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", - "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", - "license": "(MIT OR GPL-3.0-or-later)", - "dependencies": { - "lie": "~3.3.0", - "pako": "~1.0.2", - "readable-stream": "~2.3.6", - "setimmediate": "^1.0.5" - } - }, - "node_modules/katex": { - "version": "0.16.45", - "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.45.tgz", - "integrity": "sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA==", - "funding": [ - "https://opencollective.com/katex", - "https://github.com/sponsors/katex" - ], - "license": "MIT", - "dependencies": { - "commander": "^8.3.0" - }, - "bin": { - "katex": "cli.js" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/khroma": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", - "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" - }, - "node_modules/kleur": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", - "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/langium": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/langium/-/langium-4.2.3.tgz", - "integrity": "sha512-sOPIi4hISFnY7twwV97ca1TsxpBtXq0URu/LL1AvxwccPG/RIBBlKS7a/f/EL6w8lTNaS0EFs/F+IdSOaqYpng==", - "license": "MIT", - "dependencies": { - "@chevrotain/regexp-to-ast": "~12.0.0", - "chevrotain": "~12.0.0", - "chevrotain-allstar": "~0.4.3", - "vscode-languageserver": "~9.0.1", - "vscode-languageserver-textdocument": "~1.0.11", - "vscode-uri": "~3.1.0" - }, - "engines": { - "node": ">=20.10.0", - "npm": ">=10.2.3" - } - }, - "node_modules/langsmith": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.6.1.tgz", - "integrity": "sha512-qNBNPRFqScIlGaPGfMrxhw/GTOL4GJKMp1P4jeA3xuI+Gkj5Ei3wvOtxpYaZMTeBbXTW3yi4n4Wf3nCgogvttg==", - "license": "MIT", - "dependencies": { - "p-queue": "6.6.2" - }, - "peerDependencies": { - "@opentelemetry/api": "*", - "@opentelemetry/exporter-trace-otlp-proto": "*", - "@opentelemetry/sdk-trace-base": "*", - "openai": "*", - "ws": ">=7" - }, - "peerDependenciesMeta": { - "@opentelemetry/api": { - "optional": true - }, - "@opentelemetry/exporter-trace-otlp-proto": { - "optional": true - }, - "@opentelemetry/sdk-trace-base": { - "optional": true - }, - "openai": { - "optional": true - }, - "ws": { - "optional": true - } - } - }, - "node_modules/layout-base": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", - "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", - "license": "MIT" - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lie": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", - "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", - "license": "MIT", - "dependencies": { - "immediate": "~3.0.5" - } - }, - "node_modules/lightningcss": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", - "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", - "license": "MPL-2.0", - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-android-arm64": "1.32.0", - "lightningcss-darwin-arm64": "1.32.0", - "lightningcss-darwin-x64": "1.32.0", - "lightningcss-freebsd-x64": "1.32.0", - "lightningcss-linux-arm-gnueabihf": "1.32.0", - "lightningcss-linux-arm64-gnu": "1.32.0", - "lightningcss-linux-arm64-musl": "1.32.0", - "lightningcss-linux-x64-gnu": "1.32.0", - "lightningcss-linux-x64-musl": "1.32.0", - "lightningcss-win32-arm64-msvc": "1.32.0", - "lightningcss-win32-x64-msvc": "1.32.0" - } - }, - "node_modules/lightningcss-android-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", - "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", - "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", - "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", - "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", - "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", - "cpu": [ - "arm" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", - "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", - "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", - "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", - "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", - "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", - "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "license": "MIT" - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash-es": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", - "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", - "license": "MIT" - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/log-symbols": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", - "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", - "license": "MIT", - "dependencies": { - "chalk": "^5.3.0", - "is-unicode-supported": "^1.3.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-symbols/node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/log-symbols/node_modules/is-unicode-supported": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", - "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/longest-streak": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", - "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/lop": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/lop/-/lop-0.4.2.tgz", - "integrity": "sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw==", - "license": "BSD-2-Clause", - "dependencies": { - "duck": "^0.1.12", - "option": "~0.2.1", - "underscore": "^1.13.1" - } - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/lucide-react": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.14.0.tgz", - "integrity": "sha512-+1mdWcfSJVUsaTIjN9zoezmUhfXo5l0vP7ekBMPo3jcS/aIkxHnXqAPsByszMZx/Y8oQBRJxJx5xg+RH3urzxA==", - "license": "ISC", - "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/mammoth": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/mammoth/-/mammoth-1.12.0.tgz", - "integrity": "sha512-cwnK1RIcRdDMi2HRx2EXGYlxqIEh0Oo3bLhorgnsVJi2UkbX1+jKxuBNR9PC5+JaX7EkmJxFPmo6mjLpqShI2w==", - "license": "BSD-2-Clause", - "dependencies": { - "@xmldom/xmldom": "^0.8.6", - "argparse": "~1.0.3", - "base64-js": "^1.5.1", - "bluebird": "~3.4.0", - "dingbat-to-unicode": "^1.0.1", - "jszip": "^3.7.1", - "lop": "^0.4.2", - "path-is-absolute": "^1.0.0", - "underscore": "^1.13.1", - "xmlbuilder": "^10.0.0" - }, - "bin": { - "mammoth": "bin/mammoth" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/mammoth/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/markdown-table": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", - "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/marked": { - "version": "16.4.2", - "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz", - "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==", - "license": "MIT", - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/mdast-util-find-and-replace": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", - "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", - "license": "MIT", - "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" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", - "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mdast-util-from-markdown": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", - "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", - "license": "MIT", - "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" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", - "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", - "license": "MIT", - "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" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-autolink-literal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", - "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "ccount": "^2.0.0", - "devlop": "^1.0.0", - "mdast-util-find-and-replace": "^3.0.0", - "micromark-util-character": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-footnote": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", - "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "devlop": "^1.1.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-strikethrough": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", - "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-table": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", - "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "markdown-table": "^3.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-task-list-item": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", - "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-math": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-math/-/mdast-util-math-3.0.0.tgz", - "integrity": "sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "longest-streak": "^3.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.1.0", - "unist-util-remove-position": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdx-expression": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", - "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdx-jsx": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", - "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "ccount": "^2.0.0", - "devlop": "^1.1.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0", - "parse-entities": "^4.0.0", - "stringify-entities": "^4.0.0", - "unist-util-stringify-position": "^4.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdxjs-esm": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", - "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-phrasing": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", - "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-hast": { - "version": "13.2.1", - "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", - "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "@ungap/structured-clone": "^1.0.0", - "devlop": "^1.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "trim-lines": "^3.0.0", - "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-markdown": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", - "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "longest-streak": "^3.0.0", - "mdast-util-phrasing": "^4.0.0", - "mdast-util-to-string": "^4.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-decode-string": "^2.0.0", - "unist-util-visit": "^5.0.0", - "zwitch": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", - "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "license": "MIT" - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/mermaid": { - "version": "11.14.0", - "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.14.0.tgz", - "integrity": "sha512-GSGloRsBs+JINmmhl0JDwjpuezCsHB4WGI4NASHxL3fHo3o/BRXTxhDLKnln8/Q0lRFRyDdEjmk1/d5Sn1Xz8g==", - "license": "MIT", - "dependencies": { - "@braintree/sanitize-url": "^7.1.1", - "@iconify/utils": "^3.0.2", - "@mermaid-js/parser": "^1.1.0", - "@types/d3": "^7.4.3", - "@upsetjs/venn.js": "^2.0.0", - "cytoscape": "^3.33.1", - "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.14", - "dayjs": "^1.11.19", - "dompurify": "^3.3.1", - "katex": "^0.16.25", - "khroma": "^2.1.0", - "lodash-es": "^4.17.23", - "marked": "^16.3.0", - "roughjs": "^4.6.6", - "stylis": "^4.3.6", - "ts-dedent": "^2.2.0", - "uuid": "^11.1.0" - } - }, - "node_modules/micromark": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", - "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "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" - } - }, - "node_modules/micromark-core-commonmark": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", - "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "micromark-factory-destination": "^2.0.0", - "micromark-factory-label": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-factory-title": "^2.0.0", - "micromark-factory-whitespace": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-html-tag-name": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-subtokenize": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-extension-cjk-friendly": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-extension-cjk-friendly/-/micromark-extension-cjk-friendly-2.0.1.tgz", - "integrity": "sha512-OkzoYVTL1ChbvQ8Cc1ayTIz7paFQz8iS9oIYmewncweUSwmWR+hkJF9spJ1lxB90XldJl26A1F4IkPOKS3bDXw==", - "license": "MIT", - "dependencies": { - "devlop": "^1.1.0", - "micromark-extension-cjk-friendly-util": "3.0.1", - "micromark-util-chunked": "^2.0.1", - "micromark-util-resolve-all": "^2.0.1", - "micromark-util-symbol": "^2.0.1" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "micromark": "^4.0.0", - "micromark-util-types": "^2.0.0" - }, - "peerDependenciesMeta": { - "micromark-util-types": { - "optional": true - } - } - }, - "node_modules/micromark-extension-cjk-friendly-gfm-strikethrough": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-extension-cjk-friendly-gfm-strikethrough/-/micromark-extension-cjk-friendly-gfm-strikethrough-2.0.1.tgz", - "integrity": "sha512-wVC0zwjJNqQeX+bb07YTPu/CvSAyCTafyYb7sMhX1r62/Lw5M/df3JyYaANyp8g15c1ypJRFSsookTqA1IDsUg==", - "license": "MIT", - "dependencies": { - "devlop": "^1.1.0", - "get-east-asian-width": "^1.4.0", - "micromark-extension-cjk-friendly-util": "3.0.1", - "micromark-util-character": "^2.1.1", - "micromark-util-chunked": "^2.0.1", - "micromark-util-resolve-all": "^2.0.1", - "micromark-util-symbol": "^2.0.1" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "micromark": "^4.0.0", - "micromark-util-types": "^2.0.0" - }, - "peerDependenciesMeta": { - "micromark-util-types": { - "optional": true - } - } - }, - "node_modules/micromark-extension-cjk-friendly-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/micromark-extension-cjk-friendly-util/-/micromark-extension-cjk-friendly-util-3.0.1.tgz", - "integrity": "sha512-GcbXqTTHOsiZHyF753oIddP/J2eH8j9zpyQPhkof6B2JNxfEJabnQqxbCgzJNuNes0Y2jTNJ3LiYPSXr6eJA8w==", - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.4.0", - "micromark-util-character": "^2.1.1", - "micromark-util-symbol": "^2.0.1" - }, - "engines": { - "node": ">=18" - }, - "peerDependenciesMeta": { - "micromark-util-types": { - "optional": true - } - } - }, - "node_modules/micromark-extension-gfm": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", - "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", - "license": "MIT", - "dependencies": { - "micromark-extension-gfm-autolink-literal": "^2.0.0", - "micromark-extension-gfm-footnote": "^2.0.0", - "micromark-extension-gfm-strikethrough": "^2.0.0", - "micromark-extension-gfm-table": "^2.0.0", - "micromark-extension-gfm-tagfilter": "^2.0.0", - "micromark-extension-gfm-task-list-item": "^2.0.0", - "micromark-util-combine-extensions": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-autolink-literal": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", - "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-footnote": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", - "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-core-commonmark": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-strikethrough": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", - "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-table": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", - "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-tagfilter": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", - "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", - "license": "MIT", - "dependencies": { - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-task-list-item": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", - "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-math": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-math/-/micromark-extension-math-3.1.0.tgz", - "integrity": "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==", - "license": "MIT", - "dependencies": { - "@types/katex": "^0.16.0", - "devlop": "^1.0.0", - "katex": "^0.16.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-factory-destination": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", - "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-label": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", - "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-space": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", - "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-title": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", - "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-whitespace": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", - "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-chunked": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", - "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-classify-character": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", - "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-combine-extensions": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", - "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-chunked": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-decode-numeric-character-reference": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", - "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-decode-string": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", - "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-encode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", - "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-html-tag-name": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", - "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-normalize-identifier": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", - "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-resolve-all": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", - "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-sanitize-uri": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", - "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-encode": "^2.0.0", - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-subtokenize": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", - "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-types": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", - "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/mimic-function": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", - "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/mlly": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", - "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", - "license": "MIT", - "dependencies": { - "acorn": "^8.16.0", - "pathe": "^2.0.3", - "pkg-types": "^1.3.1", - "ufo": "^1.6.3" - } - }, - "node_modules/motion": { - "version": "12.38.0", - "resolved": "https://registry.npmjs.org/motion/-/motion-12.38.0.tgz", - "integrity": "sha512-uYfXzeHlgThchzwz5Te47dlv5JOUC7OB4rjJ/7XTUgtBZD8CchMN8qEJ4ZVsUmTyYA44zjV0fBwsiktRuFnn+w==", - "license": "MIT", - "dependencies": { - "framer-motion": "^12.38.0", - "tslib": "^2.4.0" - }, - "peerDependencies": { - "@emotion/is-prop-valid": "*", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/is-prop-valid": { - "optional": true - }, - "react": { - "optional": true - }, - "react-dom": { - "optional": true - } - } - }, - "node_modules/motion-dom": { - "version": "12.38.0", - "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.38.0.tgz", - "integrity": "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==", - "license": "MIT", - "dependencies": { - "motion-utils": "^12.36.0" - } - }, - "node_modules/motion-utils": { - "version": "12.36.0", - "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.36.0.tgz", - "integrity": "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==", - "license": "MIT" - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/msw": { - "version": "2.14.3", - "resolved": "https://registry.npmjs.org/msw/-/msw-2.14.3.tgz", - "integrity": "sha512-kk8G5cocVlJ4wsKMGZegn2H6XLOEKjbA+nSJE2354e/SRp4mDicCHUYnMXpymzVcVDCs+GUAsmNqSn+yHv4T2A==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "@inquirer/confirm": "^6.0.11", - "@mswjs/interceptors": "^0.41.3", - "@open-draft/deferred-promise": "^3.0.0", - "@types/statuses": "^2.0.6", - "cookie": "^1.1.1", - "graphql": "^16.13.2", - "headers-polyfill": "^5.0.1", - "is-node-process": "^1.2.0", - "outvariant": "^1.4.3", - "path-to-regexp": "^6.3.0", - "picocolors": "^1.1.1", - "rettime": "^0.11.11", - "statuses": "^2.0.2", - "strict-event-emitter": "^0.5.1", - "tough-cookie": "^6.0.1", - "type-fest": "^5.5.0", - "until-async": "^3.0.2", - "yargs": "^17.7.2" - }, - "bin": { - "msw": "cli/index.js" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/mswjs" - }, - "peerDependencies": { - "typescript": ">= 4.8.x" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/msw/node_modules/cookie": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", - "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/mustache": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", - "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", - "license": "MIT", - "bin": { - "mustache": "bin/mustache" - } - }, - "node_modules/mute-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz", - "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==", - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/nanoid": { - "version": "5.1.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.11.tgz", - "integrity": "sha512-v+KEsUv2ps74PaSKv0gHTxTCgMXOIfBEbaqa6w6ISIGC7ZsvHN4N9oJ8d4cmf0n5oTzQz2SLmThbQWhjd/8eKg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.js" - }, - "engines": { - "node": "^18 || >=20" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/next": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/next/-/next-16.2.4.tgz", - "integrity": "sha512-kPvz56wF5frc+FxlHI5qnklCzbq53HTwORaWBGdT0vNoKh1Aya9XC8aPauH4NJxqtzbWsS5mAbctm4cr+EkQ2Q==", - "license": "MIT", - "dependencies": { - "@next/env": "16.2.4", - "@swc/helpers": "0.5.15", - "baseline-browser-mapping": "^2.9.19", - "caniuse-lite": "^1.0.30001579", - "postcss": "8.4.31", - "styled-jsx": "5.1.6" - }, - "bin": { - "next": "dist/bin/next" - }, - "engines": { - "node": ">=20.9.0" - }, - "optionalDependencies": { - "@next/swc-darwin-arm64": "16.2.4", - "@next/swc-darwin-x64": "16.2.4", - "@next/swc-linux-arm64-gnu": "16.2.4", - "@next/swc-linux-arm64-musl": "16.2.4", - "@next/swc-linux-x64-gnu": "16.2.4", - "@next/swc-linux-x64-musl": "16.2.4", - "@next/swc-win32-arm64-msvc": "16.2.4", - "@next/swc-win32-x64-msvc": "16.2.4", - "sharp": "^0.34.5" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.1.0", - "@playwright/test": "^1.51.1", - "babel-plugin-react-compiler": "*", - "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", - "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", - "sass": "^1.3.0" - }, - "peerDependenciesMeta": { - "@opentelemetry/api": { - "optional": true - }, - "@playwright/test": { - "optional": true - }, - "babel-plugin-react-compiler": { - "optional": true - }, - "sass": { - "optional": true - } - } - }, - "node_modules/next-themes": { - "version": "0.4.6", - "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", - "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" - } - }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "deprecated": "Use your platform's native DOMException instead", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "engines": { - "node": ">=10.5.0" - } - }, - "node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "license": "MIT", - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, - "node_modules/node-releases": { - "version": "2.0.38", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", - "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", - "license": "MIT" - }, - "node_modules/npm-run-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", - "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", - "license": "MIT", - "dependencies": { - "path-key": "^4.0.0", - "unicorn-magic": "^0.3.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm-run-path/node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-treeify": { - "version": "1.1.33", - "resolved": "https://registry.npmjs.org/object-treeify/-/object-treeify-1.1.33.tgz", - "integrity": "sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A==", - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", - "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", - "license": "MIT", - "dependencies": { - "mimic-function": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/oniguruma-parser": { - "version": "0.12.2", - "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.2.tgz", - "integrity": "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==", - "license": "MIT" - }, - "node_modules/oniguruma-to-es": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.6.tgz", - "integrity": "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==", - "license": "MIT", - "dependencies": { - "oniguruma-parser": "^0.12.2", - "regex": "^6.1.0", - "regex-recursion": "^6.0.2" - } - }, - "node_modules/open": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/open/-/open-11.0.0.tgz", - "integrity": "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==", - "license": "MIT", - "dependencies": { - "default-browser": "^5.4.0", - "define-lazy-prop": "^3.0.0", - "is-in-ssh": "^1.0.0", - "is-inside-container": "^1.0.0", - "powershell-utils": "^0.1.0", - "wsl-utils": "^0.3.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/option": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/option/-/option-0.2.4.tgz", - "integrity": "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==", - "license": "BSD-2-Clause" - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/ora": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", - "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", - "license": "MIT", - "dependencies": { - "chalk": "^5.3.0", - "cli-cursor": "^5.0.0", - "cli-spinners": "^2.9.2", - "is-interactive": "^2.0.0", - "is-unicode-supported": "^2.0.0", - "log-symbols": "^6.0.0", - "stdin-discarder": "^0.2.2", - "string-width": "^7.2.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora/node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/ora/node_modules/emoji-regex": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", - "license": "MIT" - }, - "node_modules/ora/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/outvariant": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", - "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", - "license": "MIT" - }, - "node_modules/p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-queue": { - "version": "6.6.2", - "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", - "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", - "license": "MIT", - "dependencies": { - "eventemitter3": "^4.0.4", - "p-timeout": "^3.2.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-timeout": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", - "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", - "license": "MIT", - "dependencies": { - "p-finally": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/package-manager-detector": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz", - "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==", - "license": "MIT" - }, - "node_modules/pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "license": "(MIT AND Zlib)" - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-entities": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", - "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "character-entities-legacy": "^3.0.0", - "character-reference-invalid": "^2.0.0", - "decode-named-character-reference": "^1.0.0", - "is-alphanumerical": "^2.0.0", - "is-decimal": "^2.0.0", - "is-hexadecimal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/parse-entities/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parse-ms": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", - "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parse5": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", - "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", - "license": "MIT", - "dependencies": { - "entities": "^6.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-browserify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", - "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", - "license": "MIT" - }, - "node_modules/path-data-parser": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz", - "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==", - "license": "MIT" - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-to-regexp": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", - "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", - "license": "MIT" - }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pkce-challenge": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", - "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", - "license": "MIT", - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/pkg-types": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", - "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", - "license": "MIT", - "dependencies": { - "confbox": "^0.1.8", - "mlly": "^1.7.4", - "pathe": "^2.0.1" - } - }, - "node_modules/playwright": { - "version": "1.59.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", - "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright-core": "1.59.1" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "node_modules/playwright-core": { - "version": "1.59.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", - "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/points-on-curve": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", - "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==", - "license": "MIT" - }, - "node_modules/points-on-path": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz", - "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==", - "license": "MIT", - "dependencies": { - "path-data-parser": "0.1.0", - "points-on-curve": "0.2.0" - } - }, - "node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-selector-parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", - "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss/node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/powershell-utils": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.0.tgz", - "integrity": "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==", - "license": "MIT", - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/pretty-ms": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", - "integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==", - "license": "MIT", - "dependencies": { - "parse-ms": "^4.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "license": "MIT" - }, - "node_modules/prompts": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "license": "MIT", - "dependencies": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/prompts/node_modules/kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/property-information": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", - "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/qs": { - "version": "6.15.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", - "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/radix-ui": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.4.3.tgz", - "integrity": "sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-accessible-icon": "1.1.7", - "@radix-ui/react-accordion": "1.2.12", - "@radix-ui/react-alert-dialog": "1.1.15", - "@radix-ui/react-arrow": "1.1.7", - "@radix-ui/react-aspect-ratio": "1.1.7", - "@radix-ui/react-avatar": "1.1.10", - "@radix-ui/react-checkbox": "1.3.3", - "@radix-ui/react-collapsible": "1.1.12", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-context-menu": "2.2.16", - "@radix-ui/react-dialog": "1.1.15", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-dropdown-menu": "2.1.16", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-form": "0.1.8", - "@radix-ui/react-hover-card": "1.1.15", - "@radix-ui/react-label": "2.1.7", - "@radix-ui/react-menu": "2.1.16", - "@radix-ui/react-menubar": "1.1.16", - "@radix-ui/react-navigation-menu": "1.2.14", - "@radix-ui/react-one-time-password-field": "0.1.8", - "@radix-ui/react-password-toggle-field": "0.1.3", - "@radix-ui/react-popover": "1.1.15", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-progress": "1.1.7", - "@radix-ui/react-radio-group": "1.3.8", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-scroll-area": "1.2.10", - "@radix-ui/react-select": "2.2.6", - "@radix-ui/react-separator": "1.1.7", - "@radix-ui/react-slider": "1.3.6", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-switch": "1.2.6", - "@radix-ui/react-tabs": "1.1.13", - "@radix-ui/react-toast": "1.2.15", - "@radix-ui/react-toggle": "1.1.10", - "@radix-ui/react-toggle-group": "1.1.11", - "@radix-ui/react-toolbar": "1.1.11", - "@radix-ui/react-tooltip": "1.2.8", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-effect-event": "0.0.2", - "@radix-ui/react-use-escape-keydown": "1.1.1", - "@radix-ui/react-use-is-hydrated": "0.1.0", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-size": "1.1.1", - "@radix-ui/react-visually-hidden": "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/radix-ui/node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/radix-ui/node_modules/@radix-ui/react-label": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", - "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/radix-ui/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/radix-ui/node_modules/@radix-ui/react-separator": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", - "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/radix-ui/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", - "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/raw-body/node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/react": { - "version": "19.2.5", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", - "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-day-picker": { - "version": "9.14.0", - "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.14.0.tgz", - "integrity": "sha512-tBaoDWjPwe0M5pGrum4H0SR6Lyk+BO9oHnp9JbKpGKW2mlraNPgP9BMfsg5pWpwrssARmeqk7YBl2oXutZTaHA==", - "license": "MIT", - "dependencies": { - "@date-fns/tz": "^1.4.1", - "@tabby_ai/hijri-converter": "1.0.5", - "date-fns": "^4.1.0", - "date-fns-jalali": "4.1.0-0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "individual", - "url": "https://github.com/sponsors/gpbl" - }, - "peerDependencies": { - "react": ">=16.8.0" - } - }, - "node_modules/react-dom": { - "version": "19.2.5", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", - "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", - "license": "MIT", - "dependencies": { - "scheduler": "^0.27.0" - }, - "peerDependencies": { - "react": "^19.2.5" - } - }, - "node_modules/react-is": { - "version": "19.2.5", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.5.tgz", - "integrity": "sha512-Dn0t8IQhCmeIT3wu+Apm1/YVsJXsGWi6k4sPdnBIdqMVtHtv0IGi6dcpNpNkNac0zB2uUAqNX3MHzN8c+z2rwQ==", - "license": "MIT", - "peer": true - }, - "node_modules/react-markdown": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", - "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==", - "license": "MIT", - "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" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - }, - "peerDependencies": { - "@types/react": ">=18", - "react": ">=18" - } - }, - "node_modules/react-redux": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", - "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", - "license": "MIT", - "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" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "redux": { - "optional": true - } - } - }, - "node_modules/react-remove-scroll": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", - "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", - "license": "MIT", - "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" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-remove-scroll-bar": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", - "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", - "license": "MIT", - "dependencies": { - "react-style-singleton": "^2.2.2", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-resizable-panels": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-4.11.0.tgz", - "integrity": "sha512-LPk/AkFDGkg7SsbOyL93ojrE6E7lhrxxDwnYNjfmnSeI6BE7Sje6dB24PXgZk8DeugdeXNk1LO+ohRqIjhxiLw==", - "license": "MIT", - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/react-style-singleton": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", - "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", - "license": "MIT", - "dependencies": { - "get-nonce": "^1.0.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-textarea-autosize": { - "version": "8.5.9", - "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.5.9.tgz", - "integrity": "sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.20.13", - "use-composed-ref": "^1.3.0", - "use-latest": "^1.2.1" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "license": "MIT", - "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" - } - }, - "node_modules/recast": { - "version": "0.23.11", - "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.11.tgz", - "integrity": "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==", - "license": "MIT", - "dependencies": { - "ast-types": "^0.16.1", - "esprima": "~4.0.0", - "source-map": "~0.6.1", - "tiny-invariant": "^1.3.3", - "tslib": "^2.0.1" - }, - "engines": { - "node": ">= 4" - } - }, - "node_modules/recharts": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.7.0.tgz", - "integrity": "sha512-l2VCsy3XXeraxIID9fx23eCb6iCBsxUQDnE8tWm6DFdszVAO7WVY/ChAD9wVit01y6B2PMupYiMmQwhgPHc9Ew==", - "license": "MIT", - "workspaces": [ - "www" - ], - "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" - }, - "engines": { - "node": ">=18" - }, - "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" - } - }, - "node_modules/recharts/node_modules/eventemitter3": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", - "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", - "license": "MIT" - }, - "node_modules/redux": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", - "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" - }, - "node_modules/redux-thunk": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", - "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", - "license": "MIT", - "peerDependencies": { - "redux": "^5.0.0" - } - }, - "node_modules/regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", - "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", - "license": "MIT", - "dependencies": { - "regex-utilities": "^2.3.0" - } - }, - "node_modules/regex-recursion": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", - "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", - "license": "MIT", - "dependencies": { - "regex-utilities": "^2.3.0" - } - }, - "node_modules/regex-utilities": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", - "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", - "license": "MIT" - }, - "node_modules/rehype-harden": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/rehype-harden/-/rehype-harden-1.1.8.tgz", - "integrity": "sha512-Qn7vR1xrf6fZCrkm9TDWi/AB4ylrHy+jqsNm1EHOAmbARYA6gsnVJBq/sdBh6kmT4NEZxH5vgIjrscefJAOXcw==", - "license": "MIT", - "dependencies": { - "unist-util-visit": "^5.0.0" - } - }, - "node_modules/rehype-katex": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/rehype-katex/-/rehype-katex-7.0.1.tgz", - "integrity": "sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA==", - "license": "MIT", - "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" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/rehype-raw": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz", - "integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "hast-util-raw": "^9.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/rehype-sanitize": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/rehype-sanitize/-/rehype-sanitize-6.0.0.tgz", - "integrity": "sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "hast-util-sanitize": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-cjk-friendly": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/remark-cjk-friendly/-/remark-cjk-friendly-2.0.1.tgz", - "integrity": "sha512-6WwkoQyZf/4j5k53zdFYrR8Ca+UVn992jXdLUSBDZR4eBpFhKyVxmA4gUHra/5fesjGIxrDhHesNr/sVoiiysA==", - "license": "MIT", - "dependencies": { - "micromark-extension-cjk-friendly": "2.0.1" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/mdast": "^4.0.0", - "unified": "^11.0.0" - }, - "peerDependenciesMeta": { - "@types/mdast": { - "optional": true - } - } - }, - "node_modules/remark-cjk-friendly-gfm-strikethrough": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/remark-cjk-friendly-gfm-strikethrough/-/remark-cjk-friendly-gfm-strikethrough-2.0.1.tgz", - "integrity": "sha512-pWKj25O2eLXIL1aBupayl1fKhco+Brw8qWUWJPVB9EBzbQNd7nGLj0nLmJpggWsGLR5j5y40PIdjxby9IEYTuA==", - "license": "MIT", - "dependencies": { - "micromark-extension-cjk-friendly-gfm-strikethrough": "2.0.1" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/mdast": "^4.0.0", - "unified": "^11.0.0" - }, - "peerDependenciesMeta": { - "@types/mdast": { - "optional": true - } - } - }, - "node_modules/remark-gfm": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", - "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-gfm": "^3.0.0", - "micromark-extension-gfm": "^3.0.0", - "remark-parse": "^11.0.0", - "remark-stringify": "^11.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-math": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/remark-math/-/remark-math-6.0.0.tgz", - "integrity": "sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-math": "^3.0.0", - "micromark-extension-math": "^3.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-parse": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", - "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-from-markdown": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-rehype": { - "version": "11.1.2", - "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", - "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "mdast-util-to-hast": "^13.0.0", - "unified": "^11.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-stringify": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", - "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-to-markdown": "^2.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remend": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/remend/-/remend-1.3.0.tgz", - "integrity": "sha512-iIhggPkhW3hFImKtB10w0dz4EZbs28mV/dmbcYVonWEJ6UGHHpP+bFZnTh6GNWJONg5m+U56JrL+8IxZRdgWjw==", - "license": "Apache-2.0" - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/reselect": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", - "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", - "license": "MIT" - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/restore-cursor": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", - "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", - "license": "MIT", - "dependencies": { - "onetime": "^7.0.0", - "signal-exit": "^4.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/rettime": { - "version": "0.11.11", - "resolved": "https://registry.npmjs.org/rettime/-/rettime-0.11.11.tgz", - "integrity": "sha512-ILJRqVWBCTlg9r42fFgwVZx1gnFAcQF8mRoMkbgQfIrjEDf9nbBFDFx00oloOa+Q869FUtaYDXZvEfnecQSCoQ==", - "license": "MIT" - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/robust-predicates": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", - "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", - "license": "Unlicense" - }, - "node_modules/rolldown": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz", - "integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==", - "license": "MIT", - "dependencies": { - "@oxc-project/types": "=0.127.0", - "@rolldown/pluginutils": "1.0.0-rc.17" - }, - "bin": { - "rolldown": "bin/cli.mjs" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-rc.17", - "@rolldown/binding-darwin-arm64": "1.0.0-rc.17", - "@rolldown/binding-darwin-x64": "1.0.0-rc.17", - "@rolldown/binding-freebsd-x64": "1.0.0-rc.17", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17", - "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17", - "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17", - "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17", - "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17", - "@rolldown/binding-linux-x64-musl": "1.0.0-rc.17", - "@rolldown/binding-openharmony-arm64": "1.0.0-rc.17", - "@rolldown/binding-wasm32-wasi": "1.0.0-rc.17", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17", - "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17" - } - }, - "node_modules/rolldown/node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz", - "integrity": "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==", - "license": "MIT" - }, - "node_modules/roughjs": { - "version": "4.6.6", - "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz", - "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==", - "license": "MIT", - "dependencies": { - "hachure-fill": "^0.5.2", - "path-data-parser": "^0.1.0", - "points-on-curve": "^0.2.0", - "points-on-path": "^0.2.1" - } - }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/router/node_modules/path-to-regexp": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", - "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/run-applescript": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", - "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/rw": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", - "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", - "license": "BSD-3-Clause" - }, - "node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/scheduler": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "license": "MIT" - }, - "node_modules/secure-json-parse": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", - "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", - "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.3", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.1", - "mime-types": "^3.0.2", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/seroval": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.5.4.tgz", - "integrity": "sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/seroval-plugins": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.5.4.tgz", - "integrity": "sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "seroval": "^1.0" - } - }, - "node_modules/serve-static": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", - "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/set-cookie-parser": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.0.tgz", - "integrity": "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==", - "license": "MIT" - }, - "node_modules/setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", - "license": "MIT" - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, - "node_modules/shadcn": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/shadcn/-/shadcn-4.7.0.tgz", - "integrity": "sha512-70fwnesNrY1GgeD7Kdzn+3SsYeyfibm8immsA5L68+OusoPTvYF01oWExl8/latKpMpvVXcbgdbbE6VFBJQ38w==", - "license": "MIT", - "dependencies": { - "@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" - } - }, - "node_modules/shadcn/node_modules/commander": { - "version": "14.0.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", - "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", - "license": "MIT", - "engines": { - "node": ">=20" - } - }, - "node_modules/shadcn/node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/shadcn/node_modules/postcss": { - "version": "8.5.14", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", - "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/shadcn/node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/sharp": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", - "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", - "hasInstallScript": true, - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@img/colour": "^1.0.0", - "detect-libc": "^2.1.2", - "semver": "^7.7.3" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "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" - } - }, - "node_modules/sharp/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "license": "ISC", - "optional": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/shiki": { - "version": "3.23.0", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-3.23.0.tgz", - "integrity": "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA==", - "license": "MIT", - "dependencies": { - "@shikijs/core": "3.23.0", - "@shikijs/engine-javascript": "3.23.0", - "@shikijs/engine-oniguruma": "3.23.0", - "@shikijs/langs": "3.23.0", - "@shikijs/themes": "3.23.0", - "@shikijs/types": "3.23.0", - "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.4" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "license": "MIT", - "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" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", - "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "license": "MIT" - }, - "node_modules/sonner": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", - "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==", - "license": "MIT", - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", - "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/space-separated-tokens": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", - "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "license": "BSD-3-Clause" - }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/stdin-discarder": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", - "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/streamdown": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/streamdown/-/streamdown-2.5.0.tgz", - "integrity": "sha512-/tTnURfIOxZK/pqJAxsfCvETG/XCJHoWnk3jq9xLcuz6CSpnjjuxSRBTTL4PKGhxiZQf0lqPxGhImdpwcZ2XwA==", - "license": "Apache-2.0", - "dependencies": { - "clsx": "^2.1.1", - "hast-util-to-jsx-runtime": "^2.3.6", - "html-url-attributes": "^3.0.1", - "marked": "^17.0.1", - "mermaid": "^11.12.2", - "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.3.0", - "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" - } - }, - "node_modules/streamdown/node_modules/marked": { - "version": "17.0.6", - "resolved": "https://registry.npmjs.org/marked/-/marked-17.0.6.tgz", - "integrity": "sha512-gB0gkNafnonOw0obSTEGZTT86IuhILt2Wfx0mWH/1Au83kybTayroZ/V6nS25mN7u8ASy+5fMhgB3XPNrOZdmA==", - "license": "MIT", - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/strict-event-emitter": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", - "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", - "license": "MIT" - }, - "node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/stringify-entities": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", - "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", - "license": "MIT", - "dependencies": { - "character-entities-html4": "^2.0.0", - "character-entities-legacy": "^3.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/stringify-object": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-5.0.0.tgz", - "integrity": "sha512-zaJYxz2FtcMb4f+g60KsRNFOpVMUyuJgA51Zi5Z1DOTC3S59+OQiVOzE9GZt0x72uBGWKsQIuBKeF9iusmKFsg==", - "license": "BSD-2-Clause", - "dependencies": { - "get-own-enumerable-keys": "^1.0.0", - "is-obj": "^3.0.0", - "is-regexp": "^3.1.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/yeoman/stringify-object?sponsor=1" - } - }, - "node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/strip-final-newline": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", - "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/style-to-js": { - "version": "1.1.21", - "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", - "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", - "license": "MIT", - "dependencies": { - "style-to-object": "1.0.14" - } - }, - "node_modules/style-to-object": { - "version": "1.0.14", - "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", - "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", - "license": "MIT", - "dependencies": { - "inline-style-parser": "0.2.7" - } - }, - "node_modules/styled-jsx": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", - "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", - "license": "MIT", - "dependencies": { - "client-only": "0.0.1" - }, - "engines": { - "node": ">= 12.0.0" - }, - "peerDependencies": { - "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "babel-plugin-macros": { - "optional": true - } - } - }, - "node_modules/stylis": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz", - "integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==", - "license": "MIT" - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/tagged-tag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", - "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", - "license": "MIT", - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/tailwind-merge": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", - "integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/dcastil" - } - }, - "node_modules/tailwindcss": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.4.tgz", - "integrity": "sha512-HhKppgO81FQof5m6TEnuBWCZGgfRAWbaeOaGT00KOy/Pf/j6oUihdvBpA7ltCeAvZpFhW3j0PTclkxsd4IXYDA==", - "license": "MIT" - }, - "node_modules/tapable": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", - "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/tiny-invariant": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", - "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", - "license": "MIT" - }, - "node_modules/tinyexec": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz", - "integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tldts": { - "version": "7.0.30", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.30.tgz", - "integrity": "sha512-ELrFxuqsDdHUwoh0XxDbxuLD3Wnz49Z57IFvTtvWy1hJdcMZjXLIuonjilCiWHlT2GbE4Wlv1wKVTzDFnXH1aw==", - "license": "MIT", - "dependencies": { - "tldts-core": "^7.0.30" - }, - "bin": { - "tldts": "bin/cli.js" - } - }, - "node_modules/tldts-core": { - "version": "7.0.30", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.30.tgz", - "integrity": "sha512-uiHN8PIB1VmWyS98eZYja4xzlYqeFZVjb4OuYlJQnZAuJhMw4PbKQOKgHKhBdJR3FE/t5mUQ1Kd80++B+qhD1Q==", - "license": "MIT" - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/tough-cookie": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", - "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", - "license": "BSD-3-Clause", - "dependencies": { - "tldts": "^7.0.5" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/trim-lines": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", - "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/trough": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", - "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/ts-api-utils": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", - "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/ts-dedent": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", - "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", - "license": "MIT", - "engines": { - "node": ">=6.10" - } - }, - "node_modules/ts-morph": { - "version": "26.0.0", - "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-26.0.0.tgz", - "integrity": "sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug==", - "license": "MIT", - "dependencies": { - "@ts-morph/common": "~0.27.0", - "code-block-writer": "^13.0.3" - } - }, - "node_modules/tsconfig-paths": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", - "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", - "license": "MIT", - "dependencies": { - "json5": "^2.2.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/tw-animate-css": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz", - "integrity": "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Wombosvideo" - } - }, - "node_modules/tw-shimmer": { - "version": "0.4.11", - "resolved": "https://registry.npmjs.org/tw-shimmer/-/tw-shimmer-0.4.11.tgz", - "integrity": "sha512-pTpGJzp3xaCPO87WeHETngmZHJYvygiSTt4jqzh2oR3DWBoeudi/ANB304zks9+Cm2vQ1ai3w9fetviYdqY8HQ==", - "license": "MIT", - "peerDependencies": { - "tailwindcss": ">=4.0.0-0" - } - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-fest": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.6.0.tgz", - "integrity": "sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA==", - "license": "(MIT OR CC0-1.0)", - "dependencies": { - "tagged-tag": "^1.0.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "devOptional": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/typescript-eslint": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.2.tgz", - "integrity": "sha512-pJw051uomb3ZeCzGTpRb8RbEqB5Y4WWet8gl/GcTlU35BSx0PVdZ86/bqkQCyKKuraVQEK7r6kBHQXF+fBhkoQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/eslint-plugin": "8.59.2", - "@typescript-eslint/parser": "8.59.2", - "@typescript-eslint/typescript-estree": "8.59.2", - "@typescript-eslint/utils": "8.59.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/ufo": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", - "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", - "license": "MIT" - }, - "node_modules/underscore": { - "version": "1.13.8", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", - "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", - "license": "MIT" - }, - "node_modules/undici-types": { - "version": "7.19.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", - "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", - "license": "MIT" - }, - "node_modules/unicorn-magic": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", - "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/unified": { - "version": "11.0.5", - "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", - "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "bail": "^2.0.0", - "devlop": "^1.0.0", - "extend": "^3.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-find-after": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", - "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-is": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", - "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-position": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", - "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-remove-position": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz", - "integrity": "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-visit": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-stringify-position": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", - "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", - "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0", - "unist-util-visit-parents": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit-parents": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", - "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/unpdf": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/unpdf/-/unpdf-1.6.2.tgz", - "integrity": "sha512-zQ80ySoPuPHOsvIoRp/nJyQt8TOUoTh1+WBCGcBvlddQNgKDLRwm0AY3x8Q35I7+kIiRSgqMx+Ma2pl9McIp7A==", - "license": "MIT", - "peerDependencies": { - "@napi-rs/canvas": "^0.1.69" - }, - "peerDependenciesMeta": { - "@napi-rs/canvas": { - "optional": true - } - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/until-async": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/until-async/-/until-async-3.0.2.tgz", - "integrity": "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/kettanaito" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/use-callback-ref": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", - "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-composed-ref": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/use-composed-ref/-/use-composed-ref-1.4.0.tgz", - "integrity": "sha512-djviaxuOOh7wkj0paeO1Q/4wMZ8Zrnag5H6yBvzN7AKKe8beOaED9SF5/ByLqsku8NP4zQqsvM2u3ew/tJK8/w==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-effect-event": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/use-effect-event/-/use-effect-event-2.0.3.tgz", - "integrity": "sha512-fz1en+z3fYXCXx3nMB8hXDMuygBltifNKZq29zDx+xNJ+1vEs6oJlYd9sK31vxJ0YI534VUsHEBY0k2BATsmBQ==", - "license": "MIT", - "peerDependencies": { - "react": "^18.3 || ^19.0.0-0" - } - }, - "node_modules/use-isomorphic-layout-effect": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.1.tgz", - "integrity": "sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-latest": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/use-latest/-/use-latest-1.3.0.tgz", - "integrity": "sha512-mhg3xdm9NaM8q+gLT8KryJPnRFOz1/5XPBhmDEVZK1webPzDjrPk7f/mbpeLqTgB9msytYWANxgALOCJKnLvcQ==", - "license": "MIT", - "dependencies": { - "use-isomorphic-layout-effect": "^1.1.1" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-sidecar": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", - "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", - "license": "MIT", - "dependencies": { - "detect-node-es": "^1.1.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-sync-external-store": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", - "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, - "node_modules/uuid": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", - "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/esm/bin/uuid" - } - }, - "node_modules/validate-npm-package-name": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-7.0.2.tgz", - "integrity": "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==", - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/vfile": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", - "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-location": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", - "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-message": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", - "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-stringify-position": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/victory-vendor": { - "version": "37.3.6", - "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", - "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", - "license": "MIT AND ISC", - "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" - } - }, - "node_modules/vite": { - "version": "8.0.10", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz", - "integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==", - "license": "MIT", - "dependencies": { - "lightningcss": "^1.32.0", - "picomatch": "^4.0.4", - "postcss": "^8.5.10", - "rolldown": "1.0.0-rc.17", - "tinyglobby": "^0.2.16" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.1.0", - "esbuild": "^0.27.0 || ^0.28.0", - "jiti": ">=1.21.0", - "less": "^4.0.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" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "@vitejs/devtools": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vite/node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/vite/node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/vite/node_modules/postcss": { - "version": "8.5.14", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", - "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/vscode-jsonrpc": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", - "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/vscode-languageserver": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", - "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", - "license": "MIT", - "dependencies": { - "vscode-languageserver-protocol": "3.17.5" - }, - "bin": { - "installServerIntoExtension": "bin/installServerIntoExtension" - } - }, - "node_modules/vscode-languageserver-protocol": { - "version": "3.17.5", - "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", - "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", - "license": "MIT", - "dependencies": { - "vscode-jsonrpc": "8.2.0", - "vscode-languageserver-types": "3.17.5" - } - }, - "node_modules/vscode-languageserver-textdocument": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", - "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", - "license": "MIT" - }, - "node_modules/vscode-languageserver-types": { - "version": "3.17.5", - "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", - "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", - "license": "MIT" - }, - "node_modules/vscode-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", - "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", - "license": "MIT" - }, - "node_modules/web-namespaces": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", - "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/web-streams-polyfill": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", - "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, - "node_modules/wsl-utils": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.3.1.tgz", - "integrity": "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==", - "license": "MIT", - "dependencies": { - "is-wsl": "^3.1.0", - "powershell-utils": "^0.1.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/xmlbuilder": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz", - "integrity": "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==", - "license": "MIT", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "license": "ISC" - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/yocto-spinner": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/yocto-spinner/-/yocto-spinner-1.2.0.tgz", - "integrity": "sha512-Yw0hUB6UA3o4YUgKy3oSe9a4cxoaZ9sBfYDw+JSxo6Id0KoJGoxzPA24qqUXYKBWABs/zDSGTz9kww7t3F0XGw==", - "license": "MIT", - "dependencies": { - "yoctocolors": "^2.1.1" - }, - "engines": { - "node": ">=18.19" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/yoctocolors": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", - "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zod": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", - "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zod-to-json-schema": { - "version": "3.25.2", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", - "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", - "license": "ISC", - "peerDependencies": { - "zod": "^3.25.28 || ^4" - } - }, - "node_modules/zod-validation-error": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", - "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "zod": "^3.25.0 || ^4.0.0" - } - }, - "node_modules/zustand": { - "version": "5.0.13", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.13.tgz", - "integrity": "sha512-efI2tVaVQPqtOh114loML/Z80Y4NP3yc+Ff0fYiZJPauNeWZeIp/bRFD7I9bfmCOYBh/PHxlglQ9+wvlwnPikQ==", - "license": "MIT", - "engines": { - "node": ">=12.20.0" - }, - "peerDependencies": { - "@types/react": ">=18.0.0", - "immer": ">=9.0.6", - "react": ">=18.0.0", - "use-sync-external-store": ">=1.2.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "immer": { - "optional": true - }, - "react": { - "optional": true - }, - "use-sync-external-store": { - "optional": true - } - } - }, - "node_modules/zwitch": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", - "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - } - } -} diff --git a/studio/frontend/package.json b/studio/frontend/package.json index 22088c68a3..a2eebd5cb5 100644 --- a/studio/frontend/package.json +++ b/studio/frontend/package.json @@ -16,10 +16,9 @@ "biome:fix": "biome check . --write" }, "dependencies": { - "@assistant-ui/core": "0.1.17", - "@assistant-ui/react": "0.12.28", - "@assistant-ui/react-markdown": "0.12.11", - "@assistant-ui/react-streamdown": "0.1.11", + "@assistant-ui/react": "^0.12.19", + "@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", @@ -42,16 +41,10 @@ "@tailwindcss/vite": "^4.2.2", "@tanstack/react-router": "^1.159.10", "@tanstack/react-table": "^8.21.3", - "@tauri-apps/api": "^2.10.1", - "@tauri-apps/plugin-clipboard-manager": "^2.3.2", - "@tauri-apps/plugin-notification": "^2.3.3", - "@tauri-apps/plugin-opener": "^2.5.3", - "@tauri-apps/plugin-process": "^2.3.1", - "@tauri-apps/plugin-updater": "^2.10.1", "@toolwind/corner-shape": "^0.0.8-3", "@types/canvas-confetti": "^1.9.0", "@xyflow/react": "^12.10.0", - "assistant-stream": "0.3.12", + "assistant-stream": "^0.3.2", "canvas-confetti": "^1.9.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/studio/frontend/public/studio.png b/studio/frontend/public/studio.png deleted file mode 100644 index 4e531499b7cf115cfa016e1b3976f6741fa6f0a2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 27420 zcmeFYRaBfy*DeY~u;A{YvEc5O;7)L9+}#@w65OG2Pk`VM+}&M6<8Fb*-PygqZ~gn6 zG0x4o+!y;D0~l0wS50|pR=v-h74cO`8V!XQ1r81lO;$!i6%Gz50S*pc6X`9i#VcG> z1NP^msf?;39Go{T99%#!9NYt}Dc~<09Ec4L?$8JhP9O~qj=(9aMMVhKfnXvpEdlo$ z7B|)j`w!VkM#mKn4i)F0A9%QoEJ9c#qMNLuB;w&)cw}n+R8;sbSQEUXi;_D0WS&O` z9GvxDz=ora3+Q1LEn4 zUUGZkW+=!p3}W;VQ$Lc_q4!kc=BKM`x9yPY)=D4$b%RML|6IrXL#6HMH;hhOv;qd#eB&v!>e%`W65P)e~)zwr+~ZwG-w^sGB-XH_NO={|Ep?OpB5He`Qby z{7>7L|3}3C+xU-B{$B?95eb6B!b*tj!((SuodrGFL8l{NPBu%oNcbOT7fbvH2C!dw z*#GqX^B^(&@9rP}r!;=;7YO)AHkm#3pOlJ=0oTRS!iOrk-s+ZU4Aq4oF06#&y{aTb z?IGo-zPP78W#2(w-+nH9p!8_29QA)xG_S@7&Va`kf(N+%JX&Fu{D8bY#J#+YN*^#2 zu_Vo9k4%TKyXF_U|6{`h@Z1k(vW3_NuBuo70BoXRrC=)Xd|u>upJx2Q^et+6P_iTD ze@3mr)=E@5>T$0#=7ur>0O_*<@1IcbV&c4VDL?FLL1oN;{?~#-E_$3dW`^#0Eg)>H zgfUs-H4kncC&2|{-9W?01*5iSE%YKdZ6HVg`z`bB_!SkpbPa%DE%x1Vg5HN6!bXN zue?*(Q_9exgIN@hYg>ud6~3NH{9nl7eJ|Ub*pm0IO@S$kMGw)v+LQNQVh&XQZ+Jn; zDckZsd)HVM`=9PPft4|Ws~?e)`~NBs=&Da7NkOc=kBODQVf~q6V2b-bR^vZmApv1e zw`HF2vpkd(rW*@0Vb?}MZu|fA?b@_JiH1)g{IIY`*UAni0SoO*{Wplv%Q$!pIEiRf zxWa%oQR+f&3T6bA{da}D8=M0@dnOnD=X;KdUdeuc6!RGWRkf?>nMm0q3+53l#au?|SPuag-XAC;)o0hohW=r_{J%4Stxv>q%s^0F7|e_3yuhg9yrus| ztXWFg*2(6QC&_I@xQq>dp=n={RPA zUz>hRpley)w9?XsZ`mq>g*6;K(Kbl#OHjz3EJMAk$J|iB4_oXOTzP8@oiDr{=ESr2 z84(IbdAat&-^-omtd(4eYvD;|;z7mAa~vWrJe+s2r|A>89b1eYSDiX>Qd=(9)=Hci zGimR+%${pBPF4{*wt*eyD25ynvVNu4d=ioS5Hs71TK4gUw9vAbdv{UJY-@#Gd0LK~ z-|Boyk<0Pjc6Qlf5)xzeTg>j3a^Z#QfP^HM%;|)Nx*|#b)SwC}wG5W|2{#c7J}SZ! z{|0h3;^y~tKhc2I{4uTugZdI!%$impVjRL=!hwtQTt{IkSH5_iu zhvTOZ_UK);P);cd`u?1J34Ybr#VF~^Y29{K z|45{2Y`SLR(J|CXxnB_ms6lAt(O-zs*I{+)&6#eS$ow!T?G87erF1kDYlqpK|4U)x zP3R@1>Fm8q_jSHcQfTJgFvJwKS0kTHa4O=sn5-gkov^$0!>;ga)V1fftK?YZAGhqf ziA?#&({x}i%h7VCznPyOSDx1={ z?_*gsbvRr;k;scS#z03sgvKfN-eGHJe`^Y&cc^Qfu!n*lodD)r#4V^Rp9Wk)j04{i zJHK~i8(-a7(l*4Rm#B<@j&tQ0rM^X_{+c>_Hx%P+*&HB^qbX5|Tu(okTl$}=Yk(~TW27y(%wn(L# zSXc3sim(Y?Y+L2;LM{J-{|aB%SohoeLQhueA`Q-Yw5N~tcTZY|=hLTN z)RP>cJD2H5mw4)-dWfnugFzLENqGW;>@)4VHw5TmqTb2ng(Fm9N6?28X@3=>Ts+9{z z1KSF&TCH6E>a1)IjJI65&UTfol*r2wse}jUAn#qUi`+4zvbmC3UhCLg6*`!xu{sL$ z78VMF^_yrK5f5VusfYMI5ODq)i}W{}wQ?@49@+p0hq7HyJZYW121cm-%O?AMw;x(7 z%7bu4=4XU^>D=TuXEJ8&x=o`&h}Jw2Ng9WSUN%nr+xib9y!1QX&XJ(^|Abn#zsaM$ zuz1$jl+rdXjH|P+9Ws(NtUbRdAmyOPxVF8f&YJcqs+6>!&LMsoA|SKrg0>PAt+)wu z$YK)Rec!PKnRv}ds^3B+7tuZ=I`$tku_{UCbuD2rL7_RtVb?!ijv9=#ztZeGf)3#y zDaQKU;>Ti7*H!)mibaET8h$}DW4q6>`;Di!_Er@(} z`QwpK*r1pPySh&P1G)9E-QGGDaiOjaLltKF4Tw1=uZ3A-kSJ(Bk%>$Ss(Xpmt}2)y zq~&wgLGqWUI#xwSkC)a7Ii&jX3xQN~W=$KLlF7&hqq06Z=vhxQRaR=(h7<0&GAdlg zt3u^vK8^vB{_CuH)hn1uiNaxo&4awr$C1oTBWs(qa{Lcs;Kf2*`wE%ffu#=Ou*iI{ z$dPe2pm-!}+__2=zL(b{0g zyCs2?aG3~(>7(kJ(jb}G%aAd{G(n*A-r zOmq@^HK{u&lkDoMBky30BT!8rqa9aYJa|a6(yBcow0HhyoiqFQeK4=K6`7s{Lpj=F ztfIA?_~F;Er9Uj2yR(<5uIc@Dh3DMcL=*dv>CsA(eHYWkPCXjzh#`$_|17I)fO6PO z-|?xX_TZ<8<>-BDn`{6)s3|jlMb_^-DBjLx+%sHkZQk5apt<(>8-$%24P_&_i1y7& zzg|(^8Am0W7SQsOKG}^aVuKOYVafrNQ6+P!{B=H;;F1}Kup2W}LQeZmRE}WA2hWgQ zbV2xgkf*>B-}@TFA1OJE^Q*N^T9$M8)oJ}qCRna%{iGJ>!ij{#h)jx1;`e6TmPKt4 zOTz|ZS00w_OME^Xwan1b=W#t+_7tqP9%|@j`O@5n&Fvb$YB_4`7+@m&kx4cn;O2ve z&m%j8J#zbICjzBTz(-oD*|rJ7+JFw`J~4aATr~YLYJ+wHdUVcvT@wgjU`W%-v9&r; zx{4|{U2x_0;%i5h6`YAVw!WSh%UDr27Uwa;VA-=2G5PeI?jX#TU{(5jjoSW#;{Phh znbxB%ViNE722eV(=~gJL_~yu8KeJ*nBR{ajx6g>&q~rQbPBSLV8VCu{$qr>G*@u|O zd}&jBXVnL+YzKw=yU<;#zGH}oQ{P5e1Ci(q4qXaF>M6* z{Y+wg1r=VJu3pzyze0+zT9u7AJ;cs%-|iAHnuOZd8}V1vA*`_2m*-^59$ZnO_EtTN zS8rs^y*27DkkUVBnR5ozh@ODwASl9omTjY1hyS}}P3e+~vG}{s?qX(Yt^1R&)rn!2 zS+=oxGJl-%eL$i$c1ajR-9AI%g z2CC~-n$^6b&??lZPY~9ept@o4J9s;c%F+g9Lz389sXpbL<;9d9mt?$C0%v3kTYrp{ zbIeKGXYN(RsaqKAhD&_AtQY? zYdv_0=h-~2Xq{2L8vIB&8G)QjvkHc$^_Vjx%7pA%^?NB8A%*Em4`*p!S#s>p4*fW- z=LP3eT7)#ow+E>TGFq~F4wvBrjmPq_Tp%;mc6C&Pi=V^K%gCRx&u}J!jwkWX zYxiJRsVX*}y3ttxlAJZlCZH3ht%MhALb_UR8ct~2wW}ZjwSKim40Y9Cbf;EaILsR5 zyQz+?R#4Wgru|HVjXa>Se}LlRYU-1jPFjgSnZ`n|kgD~}21Pp9j@{sQgG^quoJmQF zU=^Hc{Lsn~sRS_RwhsH%9_R}7#?Y7ZISy@`FFSrM_+j5C-X?|$C8SqP7ipW|fw0R> ze-9LrB7+is1!*Yf!L}N{dG#OrC8pW^nrYt_#=JM^U4DWBuAHT(3~QrbjDZHOk}%9< zL|%@lg%s&UPIyENgBm*Ln(%U+0zl!|>YH|JQkI$WlKTsZaZM8fQgUu)T*9i=k22N_ zuAiu8&sJJ^v*jNxVXt#ZWc=a8hpr+; zau)oYD6}G$41QH{ETTzTo~gO?71P|7P2EtEanOIz#o(6|A^=Luka+N691^ulWL`$5 zEaMhL-i*|`hIpO`PCsP(VdV5V8QyR#h0h<+Er%C;CJEInwz(g%ITzA0w@C)!d0$js z^DJb#m<+!b^Zvn>O8^D*ac>X2^=YL__F>iW8uPaxqtsvN{9Cktsan5nZoi@%5Ly0) z`UHC(hC97V+SCPFz7Qbig>k+}TumnkDFL^5Tv5onGcX}S^TXs$$?fuG$F;=*LF$wG z@5VoXX6R-M^=!hjFxu#is_Aek2DDUrp5dQX4OO|e{L;?VgyT-*+vZ zUpo3Csf$&w8cXSFE3ZGQnNiN`NXsPqEKCT9?X8?PRR1&X&9CFWT;o9?IrSwHiB6oT zC9_wbz;_ur^?Y6eov`o|p~`%xiGb5#MNR$rA|Ge}3hW4(jwL6zwgG*-7kl9@_2zMZ zOEOB~6(UmeY|5Zg&aMh`GBIjpt!D0Ws^Gm+?X{=lf_C{=aGI%$ z=QxF2uZc0_)bp(ih&drHFq7EHvO=f#kHKR(wv^bq=9Sw_P|Ru4K1JLm_;0aMe7%KG zi~j6%J*#=GJ~8o$=!W^J^oTeM^nAi`0tI7~bPybrm#;GRXMreoct;Wb1x=~+qdVev+)+izwrx%dzUDVY=4xs=LjC| z$1EABUgUIQ6s^TcmZmc10JK^C%n|xANv^Sv)<)TYSr#|(ClUyy#KqUd(Z9xI0Mhvs z`-%sV6eUP6h6&xomL?PvK(7)bc%SkU!j1q;eZhq}tw7*gdSCI5;K}FZPSH80{16@h zW%)3@%91c`!c`6de|-JZmyXj?RO)^q-vo-?)7zqZdwLT~mQ6JLl+I+1rtV3l9dEPl zgMLNSBo`;40y0_Z?UvT-a;mpx4e<6fEOq2QZ2R~-tL6zSfA%Odw8M~&O1_g!UmRYh zjk_+KGroqvDEwgO%GNnnq_xA%X;;=w5r?b99#Gz>U|Em23ISL0pxK%!-*(zY zr@r|~OP!h8Qk8J#W_i$?sX4M;iW+oEa2R=|N8r(v8=D2)6=|+N4!sML7TY{bNSnHS z2c;@>tfB6N!tx?{-U-@Ngp*1?FqucHEda79fd&)87R zdlzPF$GKZ)72W4_55f^tU!9{)$mU1)pHxt(gGU(_mLDPS0QaS1F&~UGK+$d2zv6jv z85HdwXkWoq%*^*kr=l?G`hYF!OIx4OtN0?tmcJR2co)BJEr$N$h&N^yd5y%D%?yj~ zOzuLrvcg6r(_oxgL2J19--OaKp&RI4;c6X$N+ zt=uznWE+S{=eED|4dX>@&z%aUN$&^S!qNsLPAU1<&#_t0Efy!KE_3J=HYJ_rtR_48 z0tyFA7XrQ!S!GfBhiQXWn0io;-=6I9$dv&mw;WCiu?JlQDuQm;<4=Sz+cXl6bTc0p zdIcnMV)qRruItkdd?!iwE*YbWdk+5;@Yv1<*dWtb4dUsgEO@>;@x9*OQo!~|+s-pw zj7#?ukM6d8HIZG0`R}HD6K?WD6O}r}G0K7H>aYNW^(xo$xhC3hC`jnw-7kd7JFaDPE1!(Dha7s@n?Ga!0Ia zU5{Sb*3n~@b5lF zqkyIR((W;Sbz)V@GTT(Y2kE4Sa>@OD0#9U#Q9$Jr!w&Tlx@a9x?~*WxMr>(F6?D>9 z|4R5Y7+?R@@t)}n*72Ms9iVx-&VijctGS{Di7;*8LvlZ}dhmz2`c1j2kMvFpZvetQ zUB@b~dcjSkH0M-&3*6g!v5clzR=!`D0BBtm!ysC+PAOIjkWTSy#_Z~@Uf03}XoE!=?aLO7oZU1r#hppIuK`rYLd}nX}A&j#jE3P>) zP)Kg@2fup(dadSNW<1V3I@-;qG3MqCF*%u2FgOmwtA*51KmFT6I(l8KyVjX9!KF+n zFGmOeMTXg=w9sgFS(BK2N_PKe4W+G*?%@K@jXbaBuf?V>VTEOEB2bly&~W_tt#KBlEcRldgU-UZ%~Sk8tLSaK&-; z5MS-k;P-Hr#}9pmTys(sxZ+Nk(;^vlV600_(t=~skdw!bG@Y8 zmRB_XOV!sz$m{fz$$Fe(bM*R$0SMnl;BEz#dD|MR(vEI9_;r9nT3)jrqfaKMMkxcm z@?8<_%C7H3VZA?dGx{^{i`5dHpI{Zkt8A0B?3$RvuM$xj2td5EYh8@ffp?Ma(ke(q zTwthlyBDFdJa_nu*Gj$!*BR<6%NHO{ggtR;6@3_+kO_tKOt*I$e+&E8+vT{$M3xWZ z2MLI^-<0B*GUfDeDvq(48ozbINWr?)g&qoWr@>g0`*$yLx~A1xJK=2OO*ULnU`LiX z8c%M`VgHlXYWWh*MAjG|Hs?^i+_RLzpv9GS?7NRpjx&nsP*izsB$ClJhy8*eJ8B@w18C=ixKJ?k)8( ze~u%d-eBCS!lmN|x$Wbb<7HD)LtBnCD%C>z>^;3|$2cFO>L`uX&8;RH>_rLOVM5Uv z$9gKZN_vlc)Q1H>@n=NCu+oaqUal~k8{eNC4Ah1-srrj z?l4jt@{5$hsVEptTTmMakZsA`6Cykqe~djc6>cNd!#;S^P2jdy3U|En1yLlH>t+^E z56tUC7lARO;F>hX#j(yLvsAx`&a73frCO#2&>Q>OB|g=TTkKZs-3@}r<%O*7{-@b~ z$f9b#$Mr$PeTcAR}m@8PYC{iRh^{2e7DMVja4lR zpmvq2lr^NY1cwhEh@;y~jLHBnogsG8$s;7@)?H=Lcz6(iQ~{k0W=)FXib-p&N=5Hc z2UC{t8Hb3Qhf9ChId+&(TF^gT+5J!m&JXGM4lvDfi#jtBnlKY1dm@$;mpevI+Bu^K zZiJocnrFI?(>Jh}2o{#&>d4veLSEd^X{REz%$+2E5E+5VC##$N`%Fr5PA4beF`(M@ z%zE29(nR0Ouj1(G}Ji4Z`7cbNsh?T;T zJ#vgy!=vP0t%&xrkgfzHVC~5fA8MKR;-1DQ&E|_U&3+gEvL*bHtW5(>qChznwG?!C z-p2Ut9fjthHgt?{bq^_J+ErMeWvwPZy?8=up#1q9)ujEHfEzt&eM(UkvoSbJ-whac zXviN=-<)RLbO!`ZBGk{aYUhCvVwL)OsBfaHK(>!onYja)+aG(or%w>GhkLPf74L=` zL=6&Q8H6Ub2GK42m0Xn`!>ONoxOqS9J{SeaobzkLJ6H2F_pp-v65g^YOm=;FX#!x!P?H>V(0GMkrBp$C=Y<^?SGRvjZ*OfXW9AqA~6TzRKI` zU`SFs8qUPzzG*z*oAuTcT))jGW*jd?Kq7Kw?NTRx^D|U^iR^H>0h;E zlT^iR369-A6a7J1&}?eW!g*7lj~#&ea<_g|x1~E2@v= zVza&ZVtOEBKmXji5t*^MAKb8++{vFZr-u3?p&j5xdrRAOXp zb>WQ>1Y>1OZ6){f$^2iErheV5q|Yavu*HrgJTiT-bmdI+O3F(+lW?%yp$ck1|G4{y zD)8CV;dUN~;X3!Ea3f=67$&~D1ffdK%HG?~5=i?U0R! z|0Ct3Kr{Z8r>?00fW@?X)kV{Ze`Fl$_pVZUx~LFO;d zZ%6pgqg1hb^wrBBq%qZ@h!x*cQ5#vUA zawl)?v7gNue$?Ob{VaPgg*cf&ly(I(Q7C+c*upGc=%sJZz4V#O)J=ojg;)JAn5s+X zFQ`zYvb}w0ohzk_Y)6vX@LD_!f%c%$?+)rwOD~U4^WNdR`}@uc86zFLOSC-tu0hQ&r;0kVikU*eilbl}a14cPjlFHH6Q>X(7N|_?>x_q*aZ98N8E!K=p%`0oWKB7$K zLv`uy7}c5x0B}xJL?B?#rgxKKQVTukpMiZy>jAmvG~`G z+tP1MK1Php#kHy5j4+MeGj#t_h+TRKguF@OAdz|>TW&DmPwcsOE^2QhJm6w&NA~f` zftt}~h;n6ALCTe4P;Jt?3P8jgf$^q6Q+ST`%6L-Tf7%Myc>mchS?_EBrY@g6KO~Il zbN~`Zk=L-!H#w*r=l4#4ZG;SZMH+ALDD^Eg2iJ>MO5G1$IN!bMcS@dlpNZ0ibEzoNi&1`4V6bX^kM}EZCA_z=7xUDoRrd*M& zX(UzkF^S7D1(X_B=Q3i=!~SMuKZTwE4^qb;*Lh15ariQt!XJrF1SC(TU1qs$pq2%X zJ0%+Dljk;ld(f)HYouQa4p-YSYlrzqHFiQyzL zULM8@X8#sMew%jccIuf?<<|uf+2qW39^20jsL&{qSM4sV`X|*ek_~|$InF5titxC2 zwclT&-U}s^O@;EVwpK>DMqJUF@0Cig9-nek+zff6T1qifo!ANs(c_e5nl`sG+CX&} z5(+*-TWQf2+S{WOU`KXB<=U^w?<|6YYVh7KU``BODeT$3SOu+CQd%mg&(ixS+u;V~|y*T5| zM`MLkS05Nk{n@{|;nglgAN#*2GV!0E5K}_<-_fMf^?fW()pc|)zlK3jJ#UYpa(~G*~Q-bfr zBLquMBio)YP9=I9-MtE(3XEX*-i|Sw5eKNsX=DqDLSFx{q-%o6JrAq+W@tzwY1q>N zL}XPm**GI9mwhz>tTn7AdM0*p1P9>FfpcN8GREWMj+9fkiCHetaNh=1=yx z%K}xasopH7w z(}~`*SAq=lsv3u^6#pLbX6v^htg5hPsv}1?M6_e9*6-ksl3?KJx`O5Z@@EE@+x0VrczixE4=GjVOe= za~YHQpLPt>fwqLZg-wF12D-jDimo=&HeTVm4#G_vTV!i6W?y2nb3fB6#&;e z;(#Oo5+#bc6YbcP8>dY`u@rHA(~vU2X)l&y?^j3u;jM5`@u!k0+rwrJhL(hBUG#IQ zYxtSwPbP1aHfj7^CSi3{#D<17ENkcQtQlM0Z4BZ-gUQEJ!P;B)3mGsswK12BTGO|y zKBxj)>YIXD`Thum%Jf(u+Lc9I8%1KdDEbh~QF9_RB(IcHB+C@!kq}hu!jY&xS_@@v z)PMR-d^BpW;HI`BeDX1MU@=ms-mAbp_2T>WR30H=$?EmB8gqg#e#LHEswXY2WPjGa z=Zt_@Y{muBWPwX{fDDu9KR#(^9x)ydu*X{x%j`4Y;ez?6P4WH;N|4)!52NP!X=6Kc!UyfL~o{Cp!ARm4f=+f z0B{gfcI@aM#2N4=&B0U%)uCR8P1t;@Rci1<0=%Cpp@%~bSPyHn$^IG5Fr6dsnVGwS zbKLMTD8R7%&5i^t=KyniZ&RPZXa2c8Bl1l|-oEVJnWiDm>nziJ8AXg5 zav-Unb)A?AC$##%*t&Rc7IML2S-l`2F;VKu`C1L*xWf(KG$aosM^IYI75o%p7c~M7 zufBm@u%O-nH1EFiUc06^|7tQ1$YMAeWA@#ZYAsvkVc}lBFc<5Up>V;MT88{ttCBf} z(W}nBmSmq_N+`6pD#Ulj&K)1L!!TvLu%U27V3=KMVYBFMiot}AkJ?Ega8QoD3=-+2 zF2f_>%=)e8Sfo!jQfgh`E_z;lvs!)orNG_v8*9P$%cy*2^h6=O{y8ER@MnyZs@RX% zt33hszxIyKcZ0L&*(bf(Ft7Nzn?qD!Wpf3_tof?CHemB`4v8v?a#2(jmkl64Pp*7 z#BxKj)wbFgp;ha@`EWv`s?}kZRajxUs9g|EzJzyGOaDgL#WfGKT*EazmO1uw5UwgV zNtQAB#tVZPf2u&HSk4O3LaW^{jy1Y=X?^oNEFCC8DcV-(tfN0(kQGlaT=9by zhEP*Esy}PDsA@0USB}5)Juq<7ciS3AIh3>pWhiftgPmaGZu#;`a9oVwsFdzckL`br z-GC+S`49VaAXpN4I-D@O`T|~61^) zt@rX-7`urTVR(!42}`gNoh_t$7QReXf48yix{Xq+u(f`W@f0(&SA$;_xdOYG z5|MIm&LtWQTnl}#UN8)ph5LV3gOyfP^FQCuWN-A4C1sgY8a}`B^m=P2^goc|?6*<^ zh_nS-lH{$&>J{qIRUM#C6`;2Xuyef~lo8sEOSKbm2HT%QtAH&u?mYw8uqt|v{#*Jf z05aDW`9M>puABJyr;1SDamD=fgcI4pdx!8LF({mQj2AnJ6AA^j=ZYDUv@LlAZo%On z^aWjFUh7P}stPWqI%J)~^lbIN>;L4h(-2^IMt#rLXKzE+s|{0oT?d1arIT6}O9tjh zKI~R9LNUXJd^C^q7*IS%nY1z#34rB>v;g`3V>kS(SVua2T?mgTE#3p&KjIDEZk{15 zH-Wse33n@<@$Q4Hv&K8s+PO_Iol}ZK+w^Gw{-Y~n4Jubg;x~o*+S)e-Lls+XI&iO=9uF)Wq#{(f*Fn;$}-S27~$dnnd{A_ra$0Mb7J;$VmnH9jPxwq-v}NYaO6MPWS;xR z1(sy_PvVQg#wXZXup-S1y>iQ4NeERG%R~`gyIf>n!Zh|U2kUIM;zHJ_Csp&@`R)#1 z4-A%0B=78?K$v4}?WTNm?W3h@ZKoh23Jf=s-j{S>6+a<+B3KeNk3nx3voRJF-PfSj(l6qxhK^$_q{%8;k+1m znHq*tp~?3>4&;sX=h_Wu3lP#mYCK@Aj6}`oYfoU-xFduguoR)J=L1{P0Bk*zsF7`w zE~2B)WbS*!2Ijgp_#5h1?UPz{eA@E$fI>ZHZTS#>kei^`8vkPt?4XL&yqo}vpx!CH ze{1>d`x|DHP_I)*~m!(`OUg)LBCp9b?jRp6iGhyogDfrv^W%b3` z&7~Vw9z875xnL-b3Ja51)5#~5kxd}muOa@?HSTCw!(eL*P|#bnU6dP3oI_{o#!H8- z$7HVB4mf&j2gg{uBpDQE6f@?fcXrI}mgFB$ygddk6f&!7{nq3eZcGYcdy6XO6YtAM zfr}1Mtd&xpe+WIeFFmup@VY7wC@fbNa>t@Z{qD)6P{&ecLXUh?0hWbnb)^!=UM_PN zhXxi6l^Ze7#iiAJ(w3)ed{T(Q3F?@Nt0h;8==kukYK!9*wR}-b>^Ay0qz$ZoX8lwN z^G)}e<0HD8?1@BqFqwgIB(*a>Cc)j}=Smb?N$V!IRM)72ZZjwxzQMk)yE(If6L(Pl zB!kYRcN0B_ptk&Zr43oKj9k6p6lCVf-OHq_p>(R(3b*K8_Sz#}Cmo(I2aHxy{rdE= zlUeW#SH3(^!in!$U($0Tvi7$^7J`^}AF<))$pw`ObNl5A&Y4K+RQdzyuo@OBO^E2X zs-qsVpSZszYRFi$fs~?VMdFD+5QkVy0fdrSQthZW_kF{ii0-B)08C?rTe zMz&kGv_Ce1!vU-OS9${WQVF@IQV$~J4a3}7mj}NxH(_scupXIc9340+rv+QKWhUG# z%ka?fHmnw1nxi;C3B758%otVD=A!6Ew?7b*fX(xYXV$`FBEe3mg<5sUA-;1NFhfE-#0z>ZIaj$d zWzh*{G9R4_-$($k$1w^YJc!Weacw?hv<-ws866RS*!-v6s=SHQU1yP zUXf2-J~q(3ltE1MGkYlux32x*Lp6-CYQuMfL~pAaC~&=k0!5uZz|7tF6yJa?YrMXV z%f92k2T$%)R?Dlu7^VyHx=_ugTh$bqm~OH@voCN5;=4td&B`2y0y|g$jE{YV5;G!U ziR}2bNZNfSM)2~1_icE_4-Z~;#BQ$r#N9X+^M8zLxf&wCr||(4!5!h?sfp{rxLu!d z@6Tm!6!VDLf>mgAu39Dsw|VkYH-7tpeKo8;zq2ZTF*AUoBzE`j>7IW6**09EI1ci} z&TuIL1&0?(!Vu=uqV6=ez#-QwdM61n#i2KnDsq^Z++^1%mQ{>|-PR(mrX-o+n9=*5 z!}MW6cc`J}1Zu?0{lhfamr_(cHX?pf2^fpKvL*fxT}jhM)YLDJy?j-RJ&(8=_EuzT zG`Qw+&{^z&A)(xs>~jI&tn;4W>AZsyI1}|#mgNNXFA{-!f@ww{Lqc66xwb?F6*Fbj zbvq^&+cG=*pFKUayn@WWs(=dX!Gq3_EFEXFU|&H*+u=)tU|;u#_iJkVmw#Am;XN6g ze9-=8e!U=B5ORuQrAZ={F^`~z{EPvpeh^(UJoP9f*KZaKD(YR`w9%Cg-zk$hcGyMn zSnsf>swaURHvw?HIwsTh#j_&BQ`B-D@g&@(5*Ll}^PtlWil4E_+AfbgL5_!VHxx^6 z9nSHfl*z-UV@NAolqzyQjLXTh2#rkvBFH;0ydN!?lXOZ&h-v-z#e#jbE<(>GjvL)uwK(0=N_F=_<#v#`IX+8JQN%_pR?mgYT z5D&OH-WI0$`A?9(YE*%Q8I51w={AO$y{_~7Hyr1chwCgyC;If8!ifNGGWXl^KTaBd zNlOmImd}-_?bjOqa;@ax`bZ9^jvCPSk51^vQIBEVWrxiMSuDq7BQ)>`SnXL_aR;<;TfJZ;;GM_D{|DAdHqYDhHXCr7%$i2u3s-# zizhX+#$5~USJHH^YGN^mgJIAYqNP-9upF0R9(+&=AL5_|BKUUAd}anAb?lLgk)IV~ z4@XM9w<|NBx{PK)7pQ){4zSl*3NI?AXi}U!hV(Mj68aR{4PRp6h<34bFF$cDUWUCa z(D3Tl%KXp(&wrMX+e&>%c){-!1z8V)^NoQr7`jz#oe9<($QRQMAsauvN&(wX4sjaGRxQD|m`k6{$SidD)>aM2%e=+SNEZRnBoFbfjxYhS>x-2QwQJQLR2EQbluSa(&L-m`fPkBl&-rBxXytbHv*pYUB%#gD3 z;EU77sL1k`w!Z#okgwTw!*Fhugn+z)XQyM9{=^N>*l2lenD@?BE1tqb0wpwe-|$M< zmrD22zb*IZQnNZ^YFa?d^Cik^c^9@I9V-pmY9_-V;y5L62K9fhLIUjVdtbj>2vRMa zpmuV8vZ@io^q!e9&&r(NF5^hE|4yq5ZU%IXl)7MGya)Ex2~DCwf*2c%2+4n~t`d>k zS@cM?4H~PcDk(h(-pn`9!8XG3pTBqjIvdjWw!VmvpMuKoR(wSXoO6;ZV!|O8)ev#i z(YcKctC+z@mOj-*fdCn=`v`nG>GlZ>c-Rp)qaQw$2a?(k*hM1)D3A59Fr3W8- z69*b_+GJBJtrv|1HN}hc`Z~p!F6j`CMF{wf9;F{sG_U`rCxMH@ul9WUNQiqDke59~ z5J{YxUs=cP!LO@q5PIo3DF8hha#=$?O=ZqXrIzmV)3w(ZIDboj}gF)3Vt!Tr=xO`JV2CzYPV9zsD6;d2&TlgtHKlliJ!Yn*6li zgLy6D{3W?s6IfH_F}<4~B0jztrgEEF(&N`Q>XRi2yAqs-J;<4+%*Sz<8LI_0z`w?$ zVD{H=!E(iy1>I=-WkiLWMdWtZSMvHep|{dNtm&XCVf*{d3DZh~_vxYrS79{G!G5L+XcI z=H(QNKbyBOn8VqtM(#r|f9}f_+s>I*RL%?I^3n3$!TXx4V%%cs>bOC~ll#A`=|^VY z**f)Iyih%RGL0U}yW+$2Nk@Kp`uukdTa3M?4x+CjqMjm(?_5RV3U~~!9j8*>J;QQ4;qBO zvBp8-r{+WsC%U4xS-hP>@?8N_w&)Lgb&Y&v!1JK|9DK?$3ACz}>U`mA!N>;R1?<2E z4w;B}#cvG}OY2n(v|*F@s{Z^!(+L-%^JUAj z1)E^Q`%yjXP!D$Qj*{t_R>qAdrQq1lvQ|D(Gd`cE?>F;J#R%@Q7S_uYm?~#PZpf%i z{)E72DU6BLzOu^zOW4vyHH9C~v;jP0a@w;t_qH5YX|#grQ6kKxM(wqG54Ad+e50JR zLj8JSA58+A9Ozi9|HV8SwKS1(#}Mq3YYxuQc2D;k?w4(sX9C>9jjV{+3pxF`Ntd-R zUzJyz`YqS?SZ0MLjeS)g8@oX|M3Z^_hvIO!8F9Svv>k|fGMu|)p#h9&P<3pg{39?R zBf0F+2LGscY#M*g?IZH%HSCVjbJ;lEgagk)mawK&HTSuT`ckbi{fM3)!}_m~>*vcd z%Q0)$onqkz=;qgd(dSY4#rvzD>_;lbHgNO4(b%`}#)of+g<#GBE(O%;I2jYLn_Slj zirx51ByLLn4TXdQo16A_LR)_nQJZbQS(ZaRBXG7MEI)rIh{|5(|Q9%PeK`WO_|EMU-t!$a^= zx~fwqe@cmf(%u3Av#iM|HS55f=btbA1MAs*Z)-FzKooy3+Jhc6I+y}mjbsIk^v>Nb zxo-KJ-Bebu(-+nBhE?-*V=xx%J|30W$ZXLF+H%N#%DISBGG(X>4n=cBui6C0&HCEx zyQjA?{i-Jor0tE&Iny|CEud{;^?pI0Y|kRmpeDzCa252ZGj8xuv;vTPv<0@hf*w|y zh=&z0PRS%S3T>hwF%)GSu)i{@CZ2u=gF#5g2y4^zPc4n)M-?h}%LU`~!)3I=*rucj zDw8Si#&v76lh2_TbVfq^oI4@U5?x%c#PU~2BGM?{@I%4by zX#>xXx+IdmyV;nlg64%yjtOBtgpXA0eKU#^j#LFV1cf*{%lBk046D4D$Hz)>#65Tw zQJ}HD4nA6Nn%d)?&~Kk>%n>T%ufkYoi^@MU8))$Pu>J`M#<`m4Kl-7qHzq9R@S9RAE14sdkeN$NdbHU~{W0B{q6h z{+I{0zj`z<C>2!iHek(DRasLAB!`H4EYSO+7u^;e?RkX;7%4s#-Re zX%8X#VV-vs+Z_qQ-`Y8^CW;T~9d+DT{%i&g_M~mSclD~vKSMGgC}#}qC{gigQcjVj z#0lc;(T~T_(|7 zF(W4s_}!TF4`66{mkX{Gs>Zn~XaG%(X!`-{&WxNd=RN}_!jw1jTjxIt$16XNKEa8Q znm&S-^DoPY8PsvN`W^}@h2fjQL@E%$GgtOY4Hl*>~>xTqe`x@9*U(Ycw@?YQvk8HAwCESvs9f?Wd07dJP6VgpmG8PqwBO za!CFp(9A@pezC|_0)8e;O8G;AO#+)8r%Em3(s@4A#FLF|w#0 zV3u&1hJ|S+cbw9b_ePcVF+p{Z*S5TuJcrgSCI?$I6r|ePOEt0a3}c04HRMQ9jji57 zps8C@hVI#OqXe>Sew+pw{<~ma>_bn|dF8SQyWr)SiF`QSp!JY$lq73l-1Ay1Da4%l zh`;nlsVc*BoQY{N<4S!U8|Ir#e?X5A21gx`X!_1IZ|P4|lSH+eDdMp0dB2$SgH`UQ z>fZ4hr5x7dI^?5&VmT@34&4(L_*bSs$PHfRmaleyFnok*n%k!>KfszF;p06bdd9R0 z7TVdg8~I57=n9t81?xRP7Bu}fyfc|<7M`}M3~K*GDz59ByBy<~$==R14x-cDDP3iTU4eb) z78;h4DY+s)GwA!J*JJcl-Tr9c)o1V$M)0}>0^H@SXEq`C2wJ%L}sG)wAuk;kXKtXxNp>$)+;h}-2J#hhpZ zUQ-(cNLyzF5~inCT(io?$sp1rQBVHT1)f)D+Ic{OEBNTOQZhz4GwNH5U;j1{$3_BA zS)j$w9;^XTES!?`$jW_-R2z*z984wQX<~r z7#tIHZkYGCMRpF6kgsW-1(E#yOW`7+86$PD5IUS#aRK%*+79)TpV>p0e!d z0GK_A?CG0K2u*TtcSp%{0 zA0r*u@P*>HEfhB}|o*dQ|2GMW0}>$((!_YO?7(DR#jXl-Eg? zPk3M)_UH$=ywDNMl4SFp67shEG;A-)JA0&Y`69w2Ipvt&WOi)ovMotl|4BPn0tL08 zWlkrH;E`g8Z1|Y;!|hkfDIRPksmB#{jqkP1T91{aiIp9(EIXfjG0=^O+bx$29E;@j z9A1u%s^0O&Bwp!%MrJve!k}B%$%kKH9}aUpqTr#woKQj79}tzQz%|LAMXetbpHIyq z@yU|-rD;#{UzR(RUytth*Err;_*7@z;UQx-UsKXGy0(vnN2gc{OVQ6 z;B98`lCEZX#^=>`)pEHj3Y#jz+VcqfWLi}1!WWpqcqFl|1mQk{sPex2)RFwj zAH&mMsz4adlJFeok-Y+*9)@$lN^HrKqUC<#&iG4@O<^PlH5<>LQySnMZ6QHsuUz!35pQeV0Y+|| zcl~J?tsHK1>aOFRrSoSy8~@-~W~FzRR3<{J6#yQ9P8x$Ta<4%gbnO^OhP%tRK?4Wz z7%Tg+cY3>;)k$5R1uVHn#8}-RqKb>CV}9g0m)slC#^wc-J>ttyLys<5Lq1C*-F0kZ3k7boPa+AK zTrw>Fp{S85F5&E?UfM9saDPEGn(-{G$}^u(TX;felBV4aZkO8gV?*jo>pH_x855q4AHap((XjPcDdD1 zO(#1!22sOAuly^Cp-+@~3v9bgDk)qI%|FteKAQKfYh4z^BBO;_F)9mvWHZZfFP{xG$xF(D%Lgzn5tPv(tVqZ%ed2O7K50uE!Mgy3Y=@bm1d;*u!D+Og}P9? z7ow%?yVM>U!61_E=U^dT|7*c+sw%fXio~md2yy&*9%Wk!ZaN}tnumNpPI>#-c`k;B z<|mz{$bkoQjI{0sl@cK!2l}J^_F1Bxc}!5A}I~07|)XZdEthn#IQeH|u)mSZ?}9J|So} zxJ*;^fe!yng3^goo**8xyw9<}=?0y*CR3JszDpQkUc;DvA79=wv|IiLuC)%6O1Sjt zCiW@bdQ(Z!Us=4}+SZ%3vPT~3okd8qS0Uiq^ck0$dcU7vM`3eZGyn7opTuiAf2CJri0D;kwkfiVCn@giXD`C0 z<`T=orF#9TtQa+yYXg@RHnXY&9m1(mYq+r6T4_chVDdHLx63CY`Dp_Tnl_>MenE+H zX2%+Z5&JG%r%`TnmGUz;0YNp{`O5nZjj7&|3;}r>)MmtN>aVwZO4MUf3)tPEi_ohE zjHHVj`ys2)Mb_Dp`%abAYaOchkC6ta#Paf25ko7FaHu&JeAnmq!R{TpC$7TUz7jOK}#WXn+7> z&A(3Cx@%OpZL#wD-6f&7aqf{%`$w=8c(>d`x$ozlc%2J|7Go!rzSEFPDXH9;b#RZ) zdu-ey-Ypm&xhk+B!bXU;397deW8U;1drLBf&C>Qsd}^OykRkBvA2tHXb#o^rjf{V# zD$wm48Y)MEW5ug~kkuA$ zt^Iooz!wyI+qR%QaWSB)2{I;-&m9JuOO?#tq#pnER_G8;WEy@g%HdPD+aML=Oy%?! z_KasORR(yb;wo%C3z%5O-!)I?O9C$yU$}7HW5XD~B9Ra`GA-1Z`~m|0vR!iSLJ}H+ z6@y01U!R)`IH;0ljK@Ptject%-ogln8EI}lZeLmBU(9FijM=~*KLB>V*?z*${bHl6 zbLZd7=SjQ@(ehk{y4<_L+WC*0tt>RNem8>2U0HKcaG`a5vqs8Q>^hPP6hLJh>nrzM z15In$w?n5!#TB_E^W`!j4CzJ5_fUQa*35M0ZBxNS{hzOEAelO0%}?NOMBt(J4<#oi+}9xH75*0f^spZ&f_DQwTSJ&}<+#xfNibp}S?UDW-2KWtK^QgN4!! zk-&y1;|BfdMIN?<#cHq29)pzWZRtt=U_RdFo{rLERoBD=(pFF6GdgzBEDEM=hV`i| zlT1NY#Z6c)Y5TJ&iyif&j>hjks$)$)zF|A190YI0;*3-urGj?|bf~aR`veNZRtk;j zA!!DdToxMm7veySK@(X+EG1fqwR7QEp7Y;`9&N8H8MH4B9MZj)vn7u$65kMhU3Hl? z#@NruEo$@fk_MMtN1ZtyTJI??!}Kjz$lzt;zvpL*_`-Fe$pUkV^8w_Hqf(g+D( zc1lGg33Lh6pBPa_1OUPmS7c(3HUj?rV#N8DH>}VTN{!rY8Z4OMt1Z~b4U2C!(zM`% z9@0QhXgZEag>wh>h|vY%0KSUI2}bihk%)e>ztq)Hs!}YwOv&2ttdexteUqUj^V*m) z>khzVDabq4Zi&)krl&%>H-;AjjKJ4i)XF5T)TYbVOxI8bh0s9Y2km_twHZ?3nwh!d1 zTV%zqTp&K`)&viYK3s4Db$Omcr(W``Z>(>4Ud3YXm^x&CX007p;m;nW1j2j32_#C= z0u97O!0XD~pXzpU@ST9Iw%C{-K-n#u{QEi(#HB6_|aJ`JJ`Fb|a+76MSPbm`_j z|3p`50esP~QlW%~N%Kldqu}?K=babE;ntZY-8N_@QOk#TJ5X0n5Wm^UL(vtUzkh4W zluCz%F#bDaHu^VrAPEhVhnoV#*Q7VpW@(8#!NDnNI2~W>zi9}hr*r~9hZ0@YhxS4g zfgqrK;K#aD^M&5%%psxo4~Prlc~9wB&P;|kIO3_vv@9f78aJR{DHkUrhZ0= zbI4Reux>P-O3Y(E@av)XSQS~l7LrOsrn|!mln1k0hk(Q_fPR7DXuWJhZ-}Gng-pAe zucUMcQ`O!LF*$q5z^eD90rTQO_|#;BVRxiCOiBSb1W7U04hZBQ4niWIn?wN)gLk$my2xv%1*I zdoy-<(}f|D3HJiYTI{(2OmgXKqL%fJuZVLa&hrUVCpUS`Ho*OTuNzegSPW?bF|<2( z9oFosqa=m-e{3k6YSiE?V^^hiXiM~f2DN5_revS@K|;dA3;Luy$=s%dflc+f5Mg2) zb05Z|7JeJ0(9o=9&zn!vw$FMUoQoRucgY6Vm7f|Isy-rx=H#H6^uJb{EJ8jEh+dO= z<$Ykk76gNX(t5NkmPl23k=|n>1;@kh)J9LuO;qD=jeh_*A=cd^f$!iSJ&&hmpAI=k z!at7oJ4}p4F;3Fq$|MW8H5t~<3X~a@u8>D6{5J3H zz}qdyi9oCP#gPL1l+;|5V05Jx60%tk6fts7%eEE%@#Q06)E!IVL9&M#|AE7vGRq#D zedz-yt98aOH>)YmZs4_4VrNwp9^DcQWw2YK33YcGo)U$dO)w3YW+6@W`aJA%$y^9m-;yMA4RVbLAMz4S@gkRy0}eC ztp{5I)8SmyhYcPk3y&@LVbGtD547~qG4?@q{@PKbV#m}-kR=FAiUT)0?wG&62l7|3 zn?AQ-&0h6MyScz?l=;xZ;hLLy7Ln2a*_5Su;tLn`C%dQG0j;yeK_>F32SF6h*kOqB zAb(*CyEO>>xb7z3=mio^fLuK(mivSy}HFu^uN-_Et-->S%UVll|#}Umg4#Tl|7)m4JG*YbmIHp;5IK;LqCZf?)EF?z>~m2s#&JR!w7c&?M~ zuzo$1z0ox)S;w(IH+fa9WwBxqdsKX>j21cGyd3992FA{f@2zLdHc0Sp3%3~nlifrJ zzJ}WNJIMJ)+{Rgw3p8#>2FSeHoKCL^y;*br063_4V)j)O`eF5uAKtv)E&(3fvyi+8QoBY$2d}@*f6yUy=G?&bJB=&? zc5xVIq~B~toRwF+TEy+vGdyNjPm59T?BB+LvuQGF^;rD$LCe?=d z%K$>rb9pGG z)p5cjR-ve@V+!(dAQy*dXNZxGLZQ{c-eCe)bqWFfS@dKb)5}U?TNO|saEt%matW=tKTRbLzdyB`o|B-)S?13^V8z*39)861TzDzB%;vDk}q7d*N zkXAvS!YO&7cz!@DvwcJKF3>FU;1lqX#Kr>ry_M7pS^H5d{j1c8C&m{h{$5ZOwWlW< zINd-4NsNlEu^nn&PSAn|A5T856`8vlXQ7)ju;M@=CY@+|&1P z%a^{+aH$}^<^&?*2Pe2g-;$4P1*3-DYp6-y;nD1)mUpSLx!%IF`D#@N30Vt|9Grn~_?;~7udNtA&J2pzZ!k6f8uOYJ<^UTI^0@|fJJ z;OV9^I-da*mb+PDr=(?*aD#$3>f+S0Q7X5~vAKIfP2zn(Tum2t^c9`}jmEq+J}(yk zWnSHf-@3fWo{jflN{LWJ8Dwc&1tCWxt1-pcSK{k1g(@wb59U7a%C2Isc?JATk>H~jO{tnH-kryBVv6z;XoiR{s^+b5&-I!ppTv zs_e>&4iVF0otyg`ztdm`id}1(W~*pR+T8B$yH2IJmSxTueOa-dw3{zMHm?LyNaSg7!4V@r+_hKzP!Rc$c^NA zYLtVY&K+#t9ZUR6=g<9?cLe8k?E;;FJiEYF;FQI!rS{_jIS|=&;2Ar(W1$J%1 zQMJ7#Z!kDy*J||BKmD05N*xJ&=zsJvJRVIgA7WX%0!x$CQlq?M`hEI@YS~)U>;wN1 zI-+ib&&DinsnHAx`*(ZD{qCr3=hK07gl~F}{`e#YL`V8Y5E~A@%~>-7;Z0u!lDHm* z?sSpwet%Vacd1K<=;Q%mElV^I=oy7Sf;IsB7{-{gDInv(-9(>gRb8*g3xQVt-n%L{@u%| z|8MPQ-w%t|Jr7=4#cOYfL|w!@iYe(q@1k6shTf2Pp3%9AgwF(AG{lY&%*2DX2maQQ z37dN=RGfeTyG82p)|r#dMEQW?;vp)|qjr;^d14v@(D1u1BJCJE!yFc6MS8gsI>6;a zf;TB3?$-0$ldy}W%`g=Oa}-*-zjQ#+JKcPKA|XebuNl94=aLw=r@+f0&T|o7PTw!O zSj-316f7AJE*#Kn{IGIvlkp6Fr*3ZX?&P)49b)fA$m|$%--HQ%wKpYguH=#B@>O~Y zE2tD$cP2WzM^hS{BuEIHKCgTfU2EfEJpKV9BVHLUbpDabe^?E=>?;m-_=PT#8L{u< z7SC7D;d$v@B$neAZN?6ky)L>b|R8#Dm`{*EjMU3Q5&kC%mP{pW9RV5c;Eksap^WW0FZ z^$KAV|Ct(fuWF};>c0IGC3Mj!yXz_6i%jS;pw31FuBo=$aYIFuh0=PaOSSsx)MHDY z5~A$_&~^gof~`2b3CJ*p8HZ*0%R>6j2RF(cabUqt!_JH3nLX7pkzp&0Wf^lZddDDyJ_5z7Va5ou95dPn-i*yLY z2LCJR1QqWKpT9CdJ2{cURibZlX8=Vl6p%Olcln>(E8&1(;J*tHrvBgM!~g&DrX~I# zX^>kuXf&_Baj~Wu9!UGd<*97wX>H+Y1F>?q0lo-Cg+(O=gvAAfMf8MaAYzgbapA|p z!VqEMa;@r{OjQ3Y!r-QSNho^<}+yAqUgw%hX { return refreshSession(); } -interface AuthStatus { - initialized: boolean; - requires_password_change: boolean; -} - -async function fetchAuthStatus(): Promise { +async function checkAuthInitialized(): Promise { try { - const res = await fetch(apiUrl("/api/auth/status")); - if (!res.ok) return { initialized: true, requires_password_change: mustChangePassword() }; - return (await res.json()) as AuthStatus; + const res = await fetch("/api/auth/status"); + if (!res.ok) return true; // fallback to login on error + const data = (await res.json()) as { initialized: boolean }; + return data.initialized; } catch { - return { initialized: true, requires_password_change: mustChangePassword() }; + return true; // fallback to login on error } } -function authRedirect(to: "/login" | "/change-password"): never { - throw redirect({ to }); +async function checkPasswordChangeRequired(): Promise { + try { + const res = await fetch("/api/auth/status"); + if (!res.ok) return mustChangePassword(); + const data = (await res.json()) as { requires_password_change: boolean }; + return data.requires_password_change || mustChangePassword(); + } catch { + return mustChangePassword(); + } } export async function requireAuth(): Promise { - if (isTauri) { - // AppProvider owns backend startup + desktop auth; route guards run before it mounts. - return; - } - if (await hasActiveSession()) { - const { requires_password_change } = await fetchAuthStatus(); - if (requires_password_change || mustChangePassword()) { - authRedirect("/change-password"); + if (await checkPasswordChangeRequired()) { + throw redirect({ to: "/change-password" }); } return; } - const status = await fetchAuthStatus(); - if (status.requires_password_change || mustChangePassword()) { - authRedirect("/change-password"); - } - authRedirect(status.initialized ? "/login" : "/change-password"); + const requiresPasswordChange = await checkPasswordChangeRequired(); + if (requiresPasswordChange) throw redirect({ to: "/change-password" }); + const initialized = await checkAuthInitialized(); + throw redirect({ to: initialized ? "/login" : "/change-password" }); } export async function requireGuest(): Promise { - if (isTauri) { - throw redirect({ to: "/chat" }); - } if (!(await hasActiveSession())) return; throw redirect({ to: getPostAuthRoute() }); } export async function requirePasswordChangeFlow(): Promise { - if (isTauri) { - throw redirect({ to: "/chat" }); - } + const requiresPasswordChange = await checkPasswordChangeRequired(); + + if (requiresPasswordChange) return; - const status = await fetchAuthStatus(); - if (status.requires_password_change || mustChangePassword()) return; if (await hasActiveSession()) { throw redirect({ to: getPostAuthRoute() }); } - authRedirect(status.initialized ? "/login" : "/change-password"); + + const initialized = await checkAuthInitialized(); + throw redirect({ to: initialized ? "/login" : "/change-password" }); } diff --git a/studio/frontend/src/app/provider.tsx b/studio/frontend/src/app/provider.tsx index 62e78b809a..68ce3061bd 100644 --- a/studio/frontend/src/app/provider.tsx +++ b/studio/frontend/src/app/provider.tsx @@ -1,292 +1,18 @@ // SPDX-License-Identifier: AGPL-3.0-only // Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 -import { StartupScreen } from "@/components/tauri/startup-screen"; -import { UpdateBanner } from "@/components/tauri/update-banner"; -import { UpdateScreen } from "@/components/tauri/update-screen"; -import { - WindowTitlebar, - shouldUseCustomWindowTitlebar, -} from "@/components/tauri/window-titlebar"; import { Toaster } from "@/components/ui/sonner"; -import { getTauriAuthFailure, tauriAutoAuth } from "@/features/auth"; -import { NativeIntentDrain } from "@/features/native-intents/native-intent-drain"; -import { useTauriBackend, type BackendStatus } from "@/hooks/use-tauri-backend"; -import { useTauriUpdate } from "@/hooks/use-tauri-update"; -import { isTauri } from "@/lib/api-base"; -import { useRouterState } from "@tanstack/react-router"; import { ThemeProvider } from "next-themes"; -import { useEffect, useRef, useState, type ReactNode } from "react"; +import type { ReactNode } from "react"; interface AppProviderProps { children: ReactNode; } -// --------------------------------------------------------------------------- -// Tauri window helpers (only imported in Tauri mode) -// --------------------------------------------------------------------------- - -type TauriWindowMode = "setup" | "app"; -type WindowLayoutGuard = () => boolean; - -async function showSetupWindow(isCurrent: WindowLayoutGuard): Promise { - const { getCurrentWindow } = await import("@tauri-apps/api/window"); - if (!isCurrent()) return; - - const win = getCurrentWindow(); - if (!isCurrent()) return; - await win.center(); - if (!isCurrent()) return; - await win.show(); -} - -async function applyAppWindowLayout(isCurrent: WindowLayoutGuard): Promise { - const { getCurrentWindow, currentMonitor, LogicalSize } = await import("@tauri-apps/api/window"); - if (!isCurrent()) return; - - const win = getCurrentWindow(); - const monitor = await currentMonitor(); - if (!isCurrent()) return; - - let finalW = 900; - let finalH = 600; - - if (monitor) { - // Convert physical pixels to logical using scale factor - const scale = monitor.scaleFactor; - const screenW = monitor.size.width / scale; - const screenH = monitor.size.height / scale; - - // Target: 75% of screen width, golden ratio height, capped at min 900x600 - finalW = Math.max(900, Math.round(screenW * 0.75)); - const targetH = Math.max(600, Math.round(finalW / 1.618)); - // Don't exceed screen height - finalH = Math.min(targetH, Math.round(screenH * 0.85)); - } - - // Apply constraints and finalize without animating through intermediate sizes - if (!isCurrent()) return; - await win.setSize(new LogicalSize(finalW, finalH)); - if (!isCurrent()) return; - await win.setSizeConstraints({ minWidth: 900, minHeight: 600 }); - if (!isCurrent()) return; - await win.setResizable(true); - if (!isCurrent()) return; - await win.center(); - if (!isCurrent()) return; - await win.show(); -} - -async function showWindowFallback(): Promise { - const { getCurrentWindow } = await import("@tauri-apps/api/window"); - const win = getCurrentWindow(); - await win.setResizable(true); - await win.show(); -} - -function getTauriWindowMode( - status: BackendStatus, - hasEnteredAppMode: boolean, -): TauriWindowMode | null { - switch (status) { - case "checking": - return null; - case "not-installed": - case "installing": - case "install-error": - case "needs-elevation": - case "repairing": - case "repair-error": - return "setup"; - case "starting": - case "running": - case "stopped": - return "app"; - case "error": - return hasEnteredAppMode ? "app" : "setup"; - } -} - -// --------------------------------------------------------------------------- -// TauriWrapper -// --------------------------------------------------------------------------- - -function TauriUpdateLayer({ isExternalServer }: { isExternalServer: boolean }) { - const update = useTauriUpdate(isExternalServer); - const isUpdating = - update.status === "updating-backend" || - update.status === "downloading" || - update.status === "installing" || - (update.status === "error" && !update.dismissed); - - if (isUpdating) { - return ( - - ); - } - - return ( - - ); -} - -const HIDDEN_TITLEBAR_SIDEBAR_ROUTES = new Set([ - "/onboarding", - "/login", - "/change-password", - "/signup", -]); - -function TauriWrapper({ children }: { children: ReactNode }) { - const pathname = useRouterState({ select: (s) => s.location.pathname }); - const { - status, logs, error, isExternalServer, - currentStepIndex, progressDetail, elevationPackages, - startInstall, retry, retryInstall, approveElevation, copyDiagnostics, - } = useTauriBackend(); - - const appliedWindowModeRef = useRef(null); - const hasEnteredAppModeRef = useRef(false); - const windowLayoutGenerationRef = useRef(0); - const [desktopAuthReady, setDesktopAuthReady] = useState(!isTauri); - const [desktopAuthRetry, setDesktopAuthRetry] = useState(0); - - useEffect(() => { - if (!isTauri) return; - return () => { - windowLayoutGenerationRef.current += 1; - appliedWindowModeRef.current = null; - }; - }, []); - - // Keep the Tauri window hidden during preflight, then show it centered in setup - // mode or apply the final app layout in one instant step. - useEffect(() => { - if (!isTauri) return; - - const nextMode = getTauriWindowMode(status, hasEnteredAppModeRef.current); - if (!nextMode) { - appliedWindowModeRef.current = null; - windowLayoutGenerationRef.current += 1; - return; - } - if (appliedWindowModeRef.current === nextMode) return; - - appliedWindowModeRef.current = nextMode; - if (nextMode === "app") hasEnteredAppModeRef.current = true; - - const layoutGeneration = windowLayoutGenerationRef.current + 1; - windowLayoutGenerationRef.current = layoutGeneration; - const isCurrent = () => windowLayoutGenerationRef.current === layoutGeneration; - const applyWindowMode = nextMode === "setup" ? showSetupWindow : applyAppWindowLayout; - applyWindowMode(isCurrent).catch(async () => { - if (!isCurrent()) return; - // On failure, at minimum make the window visible and resizable so user can fix manually. - try { - await showWindowFallback(); - } catch { /* swallow — window may still be functional */ } - }); - }, [status]); - - useEffect(() => { - if (!isTauri) { - setDesktopAuthReady(true); - return; - } - if (status !== "running") { - setDesktopAuthReady(false); - setDesktopAuthRetry(0); - return; - } - - let disposed = false; - setDesktopAuthReady(false); - tauriAutoAuth({ force: true }).then((authenticated) => { - if (disposed) return; - if (authenticated) { - setDesktopAuthReady(true); - return; - } - if (!getTauriAuthFailure()) { - window.setTimeout(() => { - if (!disposed) setDesktopAuthRetry((value) => value + 1); - }, 500); - } - }); - - return () => { disposed = true; }; - }, [status, desktopAuthRetry]); - - if (!isTauri) return <>{children}; - - const showApp = status === "running" && desktopAuthReady; - const startupStatus = status === "running" ? "starting" : status; - const startupProgressDetail = - status === "running" && !desktopAuthReady - ? "Signing in to desktop session..." - : progressDetail; - - const content = showApp ? ( - <> - - - {children} - - ) : ( - - ); - - if (!shouldUseCustomWindowTitlebar()) return content; - - const showSidebarSurface = - showApp && !HIDDEN_TITLEBAR_SIDEBAR_ROUTES.has(pathname); - - return ( -
- -
- {content} -
-
- ); -} - export function AppProvider({ children }: AppProviderProps) { return ( - - {children} - + {children} ); diff --git a/studio/frontend/src/app/routes/__root.tsx b/studio/frontend/src/app/routes/__root.tsx index 22d149473c..69aeb11748 100644 --- a/studio/frontend/src/app/routes/__root.tsx +++ b/studio/frontend/src/app/routes/__root.tsx @@ -3,8 +3,8 @@ import { AppSidebar } from "@/components/app-sidebar"; import { Navbar } from "@/components/navbar"; -import { fetchDeviceType, usePlatformStore } from "@/config/env"; import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"; +import { usePlatformStore } from "@/config/env"; import { SettingsDialog, useSettingsDialogStore } from "@/features/settings"; import { useTrainingUnloadGuard } from "@/features/training/hooks/use-training-unload-guard"; import { useSidebarPin } from "@/hooks/use-sidebar-pin"; @@ -33,10 +33,7 @@ function isChatOnlyAllowed(pathname: string): boolean { } export const Route = createRootRoute({ - beforeLoad: async ({ location }) => { - // Ensure platform info is fetched before checking chat-only guard. - // fetchDeviceType caches after first call, so subsequent navigations are instant. - await fetchDeviceType(); + beforeLoad: ({ location }) => { const chatOnly = usePlatformStore.getState().isChatOnly(); if (chatOnly && !isChatOnlyAllowed(location.pathname)) { throw redirect({ to: "/chat" }); @@ -81,7 +78,7 @@ function RootLayout() { pinned={pinned} setPinned={setPinned} togglePinned={togglePinned} - className="!min-h-0 h-[calc(100dvh-var(--studio-titlebar-height,0px))] overflow-hidden" + className="!min-h-0 h-dvh overflow-hidden" > diff --git a/studio/frontend/src/components/app-sidebar.tsx b/studio/frontend/src/components/app-sidebar.tsx index 13b8adfa48..6d029a59bd 100644 --- a/studio/frontend/src/components/app-sidebar.tsx +++ b/studio/frontend/src/components/app-sidebar.tsx @@ -31,15 +31,16 @@ import { import { useAnimatedThemeToggle } from "@/components/ui/animated-theme-toggler"; import { cn } from "@/lib/utils"; import { + Book03Icon, ChefHatIcon, ColumnInsertIcon, CursorInfo02Icon, Delete02Icon, Download03Icon, GemIcon, - Globe02Icon, - HelpCircleIcon, + MessageSearch01Icon, Search01Icon, + NewReleasesIcon, PowerIcon, PencilEdit02Icon, LayoutAlignLeftIcon, @@ -526,9 +527,9 @@ export function AppSidebar() { className="!size-8" /> -
+
{displayTitle} - Unsloth + Studio
@@ -546,15 +547,6 @@ export function AppSidebar() { Settings ⌘, - useSettingsDialogStore.getState().openDialog("api-keys")} - > - - API - - New - - } onSelect={(e) => { e.preventDefault(); toggleTheme(); }} @@ -579,12 +571,47 @@ export function AppSidebar() { - useSettingsDialogStore.getState().openDialog("about")} - > - - Help - + + + + + Learn More + + + + + + What's New + + + + + + Feedback + + + + setShutdownOpen(true)}> Shutdown diff --git a/studio/frontend/src/components/assistant-ui/markdown-text.tsx b/studio/frontend/src/components/assistant-ui/markdown-text.tsx index 7eb4b21ba7..2a0517a44a 100644 --- a/studio/frontend/src/components/assistant-ui/markdown-text.tsx +++ b/studio/frontend/src/components/assistant-ui/markdown-text.tsx @@ -5,7 +5,6 @@ import { copyToClipboard } from "@/lib/copy-to-clipboard"; import { preprocessLaTeX } from "@/lib/latex"; -import { openLink } from "@/lib/open-link"; import { INTERNAL, useMessagePartText } from "@assistant-ui/react"; import { Copy02Icon, Tick02Icon } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; @@ -33,13 +32,9 @@ const STREAMDOWN_COMPONENTS = { }: React.ComponentProps<"a">) => ( { - if (href && openLink(href)) { - e.preventDefault(); - } - }} + className="text-primary underline underline-offset-2 decoration-primary/40 hover:decoration-primary transition-colors" {...props} > {children} diff --git a/studio/frontend/src/components/assistant-ui/model-selector.tsx b/studio/frontend/src/components/assistant-ui/model-selector.tsx index 795bcb6d08..3628252177 100644 --- a/studio/frontend/src/components/assistant-ui/model-selector.tsx +++ b/studio/frontend/src/components/assistant-ui/model-selector.tsx @@ -13,25 +13,18 @@ import { usePlatformStore } from "@/config/env"; import { cn } from "@/lib/utils"; import { ArrowDown01Icon, - FolderSearchIcon, Logout01Icon, } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; import { useMemo, useState } from "react"; import type { - DeletedModelRef, LoraModelOption, ModelOption, ModelSelectorChangeMeta, } from "./model-selector/types"; import { HubModelPicker, LoraModelPicker } from "./model-selector/pickers"; -export type { - DeletedModelRef, - LoraModelOption, - ModelOption, - ModelSelectorChangeMeta, -} from "./model-selector/types"; +export type { LoraModelOption, ModelOption, ModelSelectorChangeMeta } from "./model-selector/types"; interface ModelSelectorProps { models: ModelOption[]; @@ -42,9 +35,6 @@ interface ModelSelectorProps { onValueChange?: (value: string, meta: ModelSelectorChangeMeta) => void; onEject?: () => void; onFoldersChange?: () => void; - onPickLocalModel?: () => void | Promise; - onModelsChange?: (deletedModel?: DeletedModelRef) => void; - deleteDisabled?: boolean; variant?: "outline" | "ghost" | "muted"; size?: "sm" | "default" | "lg"; className?: string; @@ -76,7 +66,7 @@ function ModelSelectorTrigger({ type="button" data-tour={dataTour} className={cn( - "flex min-w-0 items-center gap-2 transition-colors", + "flex items-center gap-2 transition-colors", variant === "outline" && "rounded-[8px] border border-border/60 hover:bg-[#ececec] dark:hover:bg-[#2e3035]", variant === "ghost" && "rounded-[8px] hover:bg-[#ececec] dark:hover:bg-[#2e3035]", @@ -90,23 +80,17 @@ function ModelSelectorTrigger({ {isLoaded && ( )} - - - {currentModel?.name ?? "Select model"} - - {currentModel?.description && ( - - {currentModel.description} - - )} - - - + + {currentModel?.name ?? "Select model"} + {currentModel?.description && ( + {currentModel.description} + )} + ); @@ -119,9 +103,6 @@ function ModelSelectorContent({ onSelect, onEject, onFoldersChange, - onPickLocalModel, - onModelsChange, - deleteDisabled, className, dataTour, }: { @@ -131,9 +112,6 @@ function ModelSelectorContent({ onSelect: (id: string, meta: ModelSelectorChangeMeta) => void; onEject?: () => void; onFoldersChange?: () => void; - onPickLocalModel?: () => void; - onModelsChange?: (deletedModel?: DeletedModelRef) => void; - deleteDisabled?: boolean; className?: string; dataTour?: string; }) { @@ -167,26 +145,11 @@ function ModelSelectorContent({ loraModels={loraModels} value={value} onSelect={onSelect} - onModelsChange={onModelsChange} - deleteDisabled={deleteDisabled} /> )} - {onPickLocalModel ? ( -
- -
- ) : null} {hasSelection && onEject ? (
- - { - if (!nextOpen && deleting) return; - setOpen(nextOpen); - }} - > - - - {title} - {description} - - - No - { - e.preventDefault(); - handleConfirm(); - }} - > - {deleting ? loadingLabel : "Yes"} - - - - - - ); -} 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 fae3c22caf..30b8fdecb8 100644 --- a/studio/frontend/src/components/assistant-ui/model-selector/pickers.tsx +++ b/studio/frontend/src/components/assistant-ui/model-selector/pickers.tsx @@ -1,6 +1,16 @@ // SPDX-License-Identifier: AGPL-3.0-only // Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; import { Input } from "@/components/ui/input"; import { Spinner } from "@/components/ui/spinner"; import { @@ -13,7 +23,6 @@ import { type ScanFolderInfo, addScanFolder, deleteCachedModel, - deleteFineTunedModel, listCachedGguf, listCachedModels, listGgufVariants, @@ -41,8 +50,7 @@ import { checkVramFit, estimateLoadingVram } from "@/lib/vram"; import { Add01Icon, Cancel01Icon, Folder02Icon, Search01Icon } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; import { FolderBrowser } from "./folder-browser"; -import { ModelDeleteAction } from "./model-delete-action"; -import { ChevronDownIcon, ChevronRightIcon, DownloadIcon, StarIcon } from "lucide-react"; +import { ChevronDownIcon, ChevronRightIcon, DownloadIcon, StarIcon, Trash2Icon } from "lucide-react"; import { type ReactNode, useCallback, @@ -52,7 +60,6 @@ import { } from "react"; import { toast } from "sonner"; import type { - DeletedModelRef, LoraModelOption, ModelOption, ModelSelectorChangeMeta, @@ -204,22 +211,12 @@ function GgufVariantExpander({ gpuGb, systemRamGb, onDeleteVariant, - sourceOverride, - deleteVariantTitle = "Delete cached model?", - renderDeleteVariantDescription, - getDeleteVariantSuccessMessage, - deleteDisabled = false, }: { repoId: string; onSelect: (id: string, meta: ModelSelectorChangeMeta) => void; gpuGb?: number; systemRamGb?: number; - onDeleteVariant?: (quant: string) => Promise | void; - sourceOverride?: ModelSelectorChangeMeta["source"]; - deleteVariantTitle?: string; - renderDeleteVariantDescription?: (quant: string) => ReactNode; - getDeleteVariantSuccessMessage?: (quant: string) => string; - deleteDisabled?: boolean; + onDeleteVariant?: (quant: string) => void; }) { const [variants, setVariants] = useState(null); const [defaultVariant, setDefaultVariant] = useState(null); @@ -262,14 +259,14 @@ function GgufVariantExpander({ const handleVariantClick = useCallback( (quant: string, downloaded?: boolean, sizeBytes?: number) => { onSelect(repoId, { - source: sourceOverride ?? (isLocalPath ? "local" : "hub"), + source: isLocalPath ? "local" : "hub", isLora: false, ggufVariant: quant, isDownloaded: isLocalPath ? true : downloaded, expectedBytes: sizeBytes, }); }, - [repoId, isLocalPath, onSelect, sourceOverride], + [repoId, isLocalPath, onSelect], ); // GGUF fit classification matching llama-server's _select_gpus logic: @@ -411,29 +408,16 @@ function GgufVariantExpander({ {v.downloaded && onDeleteVariant && ( - - This will remove{" "} - - {repoId} ({v.quant}) - {" "} - from disk. You can re-download it later. - - ) - } - successMessage={ - getDeleteVariantSuccessMessage?.(v.quant) ?? - `Deleted ${repoId} ${v.quant}` - } - buttonClassName="p-1" - iconClassName="size-3" - disabled={deleteDisabled} - onConfirm={() => onDeleteVariant(v.quant)} - /> + )}
); @@ -528,6 +512,9 @@ export function HubModelPicker({ // Track which GGUF repo is expanded for variant selection const [expandedGguf, setExpandedGguf] = useState(null); + // Delete confirmation dialog state + const [deleteTarget, setDeleteTarget] = useState(null); + const [deleting, setDeleting] = useState(false); const [downloadedCollapsed, setDownloadedCollapsed] = useState(false); const [customFoldersCollapsed, setCustomFoldersCollapsed] = useState(false); const [recommendedCollapsed, setRecommendedCollapsed] = useState(false); @@ -688,6 +675,27 @@ export function HubModelPicker({ .finally(check); }, [refreshLocalModelsList, refreshScanFolders]); + const handleDeleteConfirm = useCallback(async () => { + if (!deleteTarget) return; + setDeleting(true); + try { + // deleteTarget is "repo_id" or "repo_id::variant" + const sepIdx = deleteTarget.indexOf("::"); + const repoId = sepIdx >= 0 ? deleteTarget.slice(0, sepIdx) : deleteTarget; + const variant = sepIdx >= 0 ? deleteTarget.slice(sepIdx + 2) : undefined; + await deleteCachedModel(repoId, variant); + toast.success(`Deleted ${variant ? `${repoId} ${variant}` : repoId}`); + refreshCachedLists(); + } catch (err) { + toast.error( + err instanceof Error ? err.message : "Failed to delete model", + ); + } finally { + setDeleting(false); + setDeleteTarget(null); + } + }, [deleteTarget, refreshCachedLists]); + // Deduplicate: don't show downloaded models in the recommended list. // Compare case-insensitively since HF cache lowercases repo IDs. const downloadedSet = useMemo(() => { @@ -944,10 +952,9 @@ export function HubModelPicker({ systemRamGb={ gpu.available ? gpu.systemRamAvailableGb : undefined } - onDeleteVariant={async (quant) => { - await deleteCachedModel(c.repo_id, quant); - refreshCachedLists(); - }} + onDeleteVariant={(quant) => + setDeleteTarget(`${c.repo_id}::${quant}`) + } /> )}
@@ -970,22 +977,16 @@ export function HubModelPicker({ vramStatus={null} /> - - This will remove{" "} - - {c.repo_id} - {" "} - from disk. You can re-download it later. - - } - successMessage={`Deleted ${c.repo_id}`} - onConfirm={() => deleteCachedModel(c.repo_id)} - onDeleted={refreshCachedLists} - /> + ))} @@ -1408,6 +1409,40 @@ export function HubModelPicker({ + { + if (!open && !deleting) setDeleteTarget(null); + }} + > + + + Delete cached model? + + This will remove{" "} + + {deleteTarget?.includes("::") + ? `${deleteTarget.split("::")[0]} (${deleteTarget.split("::")[1]})` + : deleteTarget} + {" "} + from disk. You can re-download it later. + + + + No + { + e.preventDefault(); + handleDeleteConfirm(); + }} + > + {deleting ? "Deleting..." : "Yes"} + + + + ); } @@ -1416,14 +1451,10 @@ export function LoraModelPicker({ loraModels, value, onSelect, - onModelsChange, - deleteDisabled = false, }: { loraModels: LoraModelOption[]; value?: string; onSelect: (id: string, meta: ModelSelectorChangeMeta) => void; - onModelsChange?: (deletedModel?: DeletedModelRef) => void; - deleteDisabled?: boolean; }) { const [query, setQuery] = useState(""); const [expandedGguf, setExpandedGguf] = useState(null); @@ -1510,8 +1541,6 @@ export function LoraModelPicker({ const isExported = adapter.source === "exported"; const isMerged = adapter.exportType === "merged"; const isGguf = adapter.exportType === "gguf"; - const isExportedGguf = isExported && isGguf; - const canDelete = (isTraining || isExported) && !isExportedGguf; const isTrainingFull = isTraining && isMerged; const isLocalGgufDir = isLocal && @@ -1540,69 +1569,38 @@ export function LoraModelPicker({ : tag; return (
-
-
- { - if (isLocalGgufDir || isExportedGguf) { - setExpandedGguf((prev) => - prev === adapter.id ? null : adapter.id, - ); - } else { - onSelect(adapter.id, { - source: isLocal - ? "local" - : isExported - ? "exported" - : "lora", - isLora: !isLocal && !isMerged && !isGguf, - isDownloaded: true, - }); - } - }} - tooltipText={ - <> - - {adapter.name} - - - {adapter.id} - - - } - /> -
- {canDelete && ( - - This will remove{" "} - - {adapter.name} - {" "} - from disk. This cannot be undone. - - } - successMessage={`Deleted ${adapter.name}`} - disabled={deleteDisabled} - onConfirm={() => - deleteFineTunedModel({ - modelPath: adapter.id, - source: isExported ? "exported" : "training", - exportType: adapter.exportType, - }) - } - onDeleted={() => - onModelsChange?.({ id: adapter.id }) - } - /> - )} -
+ { + if (isLocalGgufDir) { + setExpandedGguf((prev) => + prev === adapter.id ? null : adapter.id, + ); + } else { + onSelect(adapter.id, { + source: isLocal + ? "local" + : isExported + ? "exported" + : "lora", + isLora: !isLocal && !isMerged && !isGguf, + isDownloaded: true, + }); + } + }} + tooltipText={ + <> + + {adapter.name} + + + {adapter.id} + + + } + /> {expandedGguf === adapter.id && ( ( - <> - This will remove{" "} - - {adapter.name} ({quant}) - {" "} - from disk. This cannot be undone. - - )} - getDeleteVariantSuccessMessage={(quant) => - `Deleted ${adapter.name} ${quant}` - } - deleteDisabled={deleteDisabled} - onDeleteVariant={ - isExportedGguf - ? async (quant) => { - await deleteFineTunedModel({ - modelPath: adapter.id, - source: "exported", - exportType: "gguf", - ggufVariant: quant, - }); - onModelsChange?.({ - id: adapter.id, - ggufVariant: quant, - }); - } - : undefined - } /> )}
@@ -1652,7 +1619,6 @@ 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 3da75b4d4e..215dd2b38e 100644 --- a/studio/frontend/src/components/assistant-ui/model-selector/types.ts +++ b/studio/frontend/src/components/assistant-ui/model-selector/types.ts @@ -25,8 +25,3 @@ export interface ModelSelectorChangeMeta { isDownloaded?: boolean; expectedBytes?: number; } - -export interface DeletedModelRef { - id: string; - ggufVariant?: string; -} diff --git a/studio/frontend/src/components/assistant-ui/sources.tsx b/studio/frontend/src/components/assistant-ui/sources.tsx index 81c8b0c213..da97ff66b5 100644 --- a/studio/frontend/src/components/assistant-ui/sources.tsx +++ b/studio/frontend/src/components/assistant-ui/sources.tsx @@ -1,6 +1,5 @@ "use client"; -import { openLink } from "@/lib/open-link"; import { memo, useState, @@ -94,8 +93,8 @@ function Source({ variant, size, asChild = false, - href, - onClick, + target = "_blank", + rel = "noopener noreferrer", ...props }: SourceProps) { return ( @@ -110,14 +109,8 @@ function Source({ >
{ - if (href && openLink(href)) { - e.preventDefault(); - } - onClick?.(e); - }} + target={target} + rel={rel} {...(props as ComponentProps<"a">)} /> diff --git a/studio/frontend/src/components/assistant-ui/thread.tsx b/studio/frontend/src/components/assistant-ui/thread.tsx index 0d6cd3bbf9..0b97d98dfd 100644 --- a/studio/frontend/src/components/assistant-ui/thread.tsx +++ b/studio/frontend/src/components/assistant-ui/thread.tsx @@ -24,16 +24,8 @@ import { useScrollThreadToBottom, } from "@/components/assistant-ui/use-intent-aware-autoscroll"; import { Button } from "@/components/ui/button"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; import { sentAudioNames } from "@/features/chat/api/chat-adapter"; import { useChatRuntimeStore } from "@/features/chat/stores/chat-runtime-store"; -import { applyQwenThinkingParams } from "@/features/chat/utils/qwen-params"; -import { isTauri } from "@/lib/api-base"; import { deleteThreadMessage } from "@/features/chat/utils/delete-thread-message"; import { AUDIO_ACCEPT, MAX_AUDIO_SIZE, fileToBase64 } from "@/lib/audio-utils"; import { copyToClipboard } from "@/lib/copy-to-clipboard"; @@ -299,41 +291,27 @@ const Composer: FC<{ disabled?: boolean }> = ({ disabled }) => { [disabled], ); - const composerContent = ( - <> - - - - - - - ); - return ( - {isTauri ? ( - // Phase 1 native model drops own Tauri local-path drops. Restore browser - // attachment drops in Tauri when Phase 1d adds attachment-token bridging. -
- {composerContent} -
- ) : ( - - {composerContent} - - )} + + + + + + +
); }; @@ -395,6 +373,18 @@ const ComposerAudioUpload: FC = () => { ); }; +/** Qwen3/3.5 recommended params differ between thinking on/off. */ +function applyQwenThinkingParams(thinkingOn: boolean): void { + const store = useChatRuntimeStore.getState(); + const checkpoint = store.params.checkpoint?.toLowerCase() ?? ""; + if (!checkpoint.includes("qwen3")) { + return; + } + const params = thinkingOn + ? { temperature: 0.6, topP: 0.95, topK: 20, minP: 0.0 } + : { temperature: 0.7, topP: 0.8, topK: 20, minP: 0.0 }; + store.setParams({ ...store.params, ...params }); +} const ReasoningToggle: FC = () => { const modelLoaded = useChatRuntimeStore( @@ -403,49 +393,8 @@ const ReasoningToggle: FC = () => { const supportsReasoning = useChatRuntimeStore((s) => s.supportsReasoning); const reasoningEnabled = useChatRuntimeStore((s) => s.reasoningEnabled); const setReasoningEnabled = useChatRuntimeStore((s) => s.setReasoningEnabled); - const reasoningStyle = useChatRuntimeStore((s) => s.reasoningStyle); - const reasoningEffort = useChatRuntimeStore((s) => s.reasoningEffort); - const setReasoningEffort = useChatRuntimeStore((s) => s.setReasoningEffort); const disabled = !(modelLoaded && supportsReasoning); - if (reasoningStyle === "reasoning_effort") { - return ( - - - - - - {(["low", "medium", "high"] as const).map((level) => ( - setReasoningEffort(level)} - > - {level.charAt(0).toUpperCase() + level.slice(1)} - {reasoningEffort === level ? " \u2713" : ""} - - ))} - - - ); - } - return ( - ); -}; - const WebSearchToggle: FC = () => { const modelLoaded = useChatRuntimeStore( (s) => !!s.params.checkpoint && !s.modelLoading, @@ -640,7 +551,6 @@ const ComposerAction: FC<{ disabled?: boolean }> = ({ disabled }) => { - diff --git a/studio/frontend/src/components/tauri/startup-screen.tsx b/studio/frontend/src/components/tauri/startup-screen.tsx deleted file mode 100644 index 1eb3f81d10..0000000000 --- a/studio/frontend/src/components/tauri/startup-screen.tsx +++ /dev/null @@ -1,461 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 - -import { ShimmerButton } from "@/components/ui/shimmer-button"; -import type { BackendStatus } from "@/hooks/use-tauri-backend"; -import type { CopySupportDiagnosticsResult } from "@/lib/tauri-diagnostics"; -import { AnimatePresence, motion } from "motion/react"; -import { useState } from "react"; - -interface StartupScreenProps { - status: BackendStatus; - logs: string[]; - error: string | null; - currentStepIndex: number; - progressDetail: string | null; - elevationPackages: string[]; - onInstall: () => void; - onRetry: () => void; - onRetryInstall: () => void; - onApproveElevation: () => void; - onStartServer: () => void; - onCopyDiagnostics: () => Promise; -} - -function DiagnosticsCopyActions({ - onCopyDiagnostics, - children, -}: { - onCopyDiagnostics: () => Promise; - children: React.ReactNode; -}) { - const [copying, setCopying] = useState(false); - const [manualReport, setManualReport] = useState(null); - const [manualMessage, setManualMessage] = useState(null); - - async function handleCopyDiagnostics() { - setCopying(true); - try { - const result = await onCopyDiagnostics(); - if (result.ok) { - setManualReport(null); - setManualMessage(null); - } else { - setManualReport(result.report); - setManualMessage(result.error ?? "Clipboard copy failed. Select and copy the diagnostics below."); - } - } catch (error) { - setManualReport(null); - setManualMessage(`Diagnostics copy failed: ${String(error)}`); - } finally { - setCopying(false); - } - } - - return ( -
-
- void handleCopyDiagnostics()} - > - {copying ? "Copying..." : "Copy Diagnostics"} - - {children} -
- {manualMessage && ( -

{manualMessage}

- )} - {manualReport && ( -