From 4be27cf56e996d423a9491ee1352ee8d823cc7a1 Mon Sep 17 00:00:00 2001 From: LongYinan Date: Thu, 25 Jun 2026 18:50:43 +0800 Subject: [PATCH 1/8] =?UTF-8?q?feat(convert):=20quantize=20gemma4=5Funifie?= =?UTF-8?q?d=20(12B)=20=E2=80=94=20recognize=20+=20keep=20vision=5Fembedde?= =?UTF-8?q?r=20bf16=20(#76)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What Enable `mlx convert` to quantize **`gemma4_unified`** (Gemma 4 12B — encoder-free text+vision+audio) checkpoints. The runtime loader already supports `gemma4_unified` (PR #74); only the **converter** didn't recognize it, and the default quant predicate would have corrupted one vision weight. Motivating model: `google/gemma-4-12B-it-qat-q4_0-unquantized` (dense 12B, bf16, QAT-trained for llama.cpp `q4_0`). The best quality+speed target is **4-bit affine group_size 32** — near-bf16 quality at 4-bit memory/speed. ``` mlx convert -o --quantize --q-mode affine --q-bits 4 --q-group-size 32 ``` ## Changes 1. **Recognize `gemma4_unified`** — `Gemma4Recipe::model_types` / `recipe_for` / `CONVERTIBLE_MODEL_TYPES` route it to the existing shared `Gemma4Recipe` (it is dense — no MoE split, sym8-supported, no MTP — so it behaves identically to `gemma4` on every `recipe_for`-keyed path). 2. **Keep `vision_embedder.*` bf16** — `should_quantize()` excludes any key containing `vision_embedder`. The encoder-free unified vision patch projection is installed **dense-bf16-only** by the loader (`apply_unified_vision_embedder_weights`, no `.scales` branch), so quantizing `patch_dense.weight` would corrupt the vision path. `embed_vision`/`embed_audio` projections **still quantize** (they have affine loader branches). `vision_embedder` is unified-only → zero collateral on other families. 3. **Sanitize keeps `vision_embedder.*` bare** — sibling of `embed_vision.*`, not mis-prefixed under `language_model.model.`. 4. **CLI passes `gemma4_unified` through unchanged** — *not* collapsed to `'gemma4'`. The driver's `model_type` is the CLI value, and the gemma-QAT (wNa8o8) prequantized importer gate is exact `Some("gemma4")`; collapsing would (a) dead-code the native `recipe_for("gemma4_unified")` arm and (b) misroute a gemma-QAT unified checkpoint into the E2B-only importer. ## End-to-end validation Converted the real 23GB checkpoint → **8.4GB** `{bits:4, group_size:32, mode:affine}`: - auto-detected `gemma4_unified` (passthrough confirmed) - `vision_embedder.*` → **bf16, no `.scales`** ✓ · text / `embed_vision` / `embed_audio` → quantized (U32 + scales) ✓ - model **loads and generates coherent text** (e.g. *"The capital of France is Paris."*) ## Tests - Rust: `should_quantize` excludes `vision_embedder` (with positive controls that `embed_vision` + text layers still quantize); `gemma4_unified` resolves to a recipe + is in `CONVERTIBLE_MODEL_TYPES`; sanitize keeps `vision_embedder.*` bare; extended `recipe_registry_reproduces_inline_flags` (sym8 allowlist). - TS: `convert-cmd.test.ts` drives `run()` and asserts the `modelType` handed to native is `gemma4_unified` (and `gemma4`/`gemma4_text` still map to `gemma4`). - Gates: `convert::` 106 pass, fmt + clippy clean, CLI test 4/4, typecheck clean. ## Review Three `/codex:adversarial-review` cycles → final verdict **approve, no material findings**. The CLI-collapse misroute (cycle #1) was fixed; a flagged sanitize-test SIGABRT (cycle #2) was confirmed to be a review-harness/metallib environment artifact (the pre-existing `gemma4_recipe_sanitize_transforms` test aborts identically; CI runs one process per binary). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- > [!NOTE] > **Medium Risk** > Changes conversion routing and quantization rules for a new model type (wrong passthrough could misroute QAT checkpoints); CI runner change is low risk but affects all macOS jobs. > > **Overview** > Adds first-class **`gemma4_unified`** support in `mlx convert` so encoder-free Gemma 4 unified checkpoints can be quantized without breaking vision or misrouting QAT imports. > > The native converter registers **`gemma4_unified`** on the shared **`Gemma4Recipe`**, excludes **`vision_embedder.*`** from quantization (bf16-only loader path), and keeps those weights at **bare keys** during sanitize. The CLI **auto-detects** `gemma4_unified` (including architecture-only configs) and **passes the string through** instead of collapsing to `gemma4`, avoiding the E2B prequantized importer gate. > > CI moves macOS jobs to **`macos-26`** and runs ignored cargo model e2e tests with **`--test-threads=1`** to avoid Metal GPU watchdog timeouts on shared runners. Docs and TS/Rust tests cover detection, quant exclusions, and sanitize routing. > > Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit fb4e8e0b1b6db5ee62fd01ac6656bfe320189513. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). --------- Co-authored-by: Claude Opus 4.8 --- .github/workflows/ci.yml | 16 ++- crates/mlx-core/src/convert.rs | 116 +++++++++++++++++++++- docs/cli.md | 2 +- packages/cli/__test__/convert-cmd.test.ts | 57 ++++++++++- packages/cli/src/commands/convert.ts | 26 +++++ 5 files changed, 206 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 823ab4662..dedf1aa65 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,7 @@ jobs: if: >- github.event.action != 'labeled' || github.event.label.name == 'model-e2e' - runs-on: macos-latest + runs-on: macos-26 steps: - uses: actions/checkout@v6 with: @@ -129,7 +129,7 @@ jobs: # Unit/integration tests don't depend on the label; skip the redundant re-run # a `labeled` event would otherwise spawn (build artifacts are unchanged). if: github.event.action != 'labeled' - runs-on: macos-latest + runs-on: macos-26 needs: build strategy: fail-fast: false @@ -227,7 +227,7 @@ jobs: github.event.label.name == 'model-e2e' || (github.event.action != 'labeled' && contains(github.event.pull_request.labels.*.name, 'model-e2e')) - runs-on: macos-latest + runs-on: macos-26 strategy: fail-fast: false matrix: @@ -344,8 +344,14 @@ jobs: else # cargo leg: point this family's gating env var(s) at the converted # checkpoint so the #[ignore]'d tests un-skip; `-- --ignored` runs only those. + # `--test-threads=1` serializes them: each ignored test loads 2-4 full + # models, and libtest's default per-test thread pool would otherwise run + # several at once, oversubscribing the single shared Metal GPU until a + # command buffer trips the GPU watchdog (kIOGPUCommandBufferCallbackErrorTimeout + # -> SIGABRT) on the shared CI runner. One shared GPU means parallelism + # never sped these up anyway, so serializing costs ~nothing. for v in ${{ matrix.env }}; do export "$v=$ckpt"; done - cargo test -p mlx-core ${{ matrix.tests }} -- --ignored --nocapture + cargo test -p mlx-core ${{ matrix.tests }} -- --ignored --test-threads=1 --nocapture fi lint: @@ -353,7 +359,7 @@ jobs: # Lint doesn't depend on the label; skip the redundant re-run a `labeled` # event would otherwise spawn. if: github.event.action != 'labeled' - runs-on: macos-latest + runs-on: macos-26 steps: - uses: actions/checkout@v6 with: diff --git a/crates/mlx-core/src/convert.rs b/crates/mlx-core/src/convert.rs index 8cf01a590..9f483bcd0 100644 --- a/crates/mlx-core/src/convert.rs +++ b/crates/mlx-core/src/convert.rs @@ -1344,7 +1344,7 @@ pub(crate) mod recipe { impl ConversionRecipe for Gemma4Recipe { fn model_types(&self) -> &'static [&'static str] { - &["gemma4"] + &["gemma4", "gemma4_unified"] } fn sanitize( @@ -1407,6 +1407,7 @@ pub(crate) mod recipe { || stripped.starts_with("audio_encoder.") || stripped.starts_with("embed_audio.") || stripped.starts_with("embed_vision.") + || stripped.starts_with("vision_embedder.") || stripped.starts_with("multi_modal_projector.") { // Apply PyTorch→MLX layout conversions for conv weights @@ -1500,6 +1501,7 @@ pub(crate) mod recipe { "qianfan-ocr", "privacy-filter", "gemma4", + "gemma4_unified", ]; /// Resolve a [`ConversionRecipe`] for an exact HuggingFace `model_type` @@ -1513,7 +1515,7 @@ pub(crate) mod recipe { "paddleocr-vl" => Some(Box::new(PaddleOcrVlRecipe)), "qianfan-ocr" => Some(Box::new(QianfanOcrRecipe)), "privacy-filter" => Some(Box::new(PrivacyFilterRecipe)), - "gemma4" => Some(Box::new(Gemma4Recipe)), + "gemma4" | "gemma4_unified" => Some(Box::new(Gemma4Recipe)), _ => None, } } @@ -2862,6 +2864,13 @@ fn should_quantize(key: &str, embed_quantizable: bool) -> bool { return false; } + // Encoder-free unified vision patch projection (`vision_embedder.patch_dense`). + // The loader installs `vision_embedder.*` as dense bf16 only (no quantized + // branch), so quantizing it would corrupt the unified vision path. Keep bf16. + if key.contains("vision_embedder") { + return false; + } + // lm_head (output head) is ALWAYS excluded — for tied models it is dropped // at sanitize, for untied models it shares the vocab dimension and loads // through an affine-only head loader. @@ -5473,9 +5482,13 @@ mod tests { ); // sym8_supported == old sym8 allowlist (NOTE qwen3_5_moe excluded). + // gemma4_unified routes to Gemma4Recipe and supports sym8 like gemma4. assert_eq!( r.sym8_supported(), - matches!(mt, "qwen3_5" | "lfm2" | "lfm2_moe" | "gemma4"), + matches!( + mt, + "qwen3_5" | "lfm2" | "lfm2_moe" | "gemma4" | "gemma4_unified" + ), "{mt}: sym8_supported mismatch vs inline sym8 allowlist" ); @@ -5618,6 +5631,103 @@ mod tests { ); } + /// The encoder-free `gemma4_unified` loader installs `vision_embedder.*` as + /// dense bf16 only (`apply_unified_vision_embedder_weights`, no `.scales` + /// branch), so `should_quantize` must refuse it under any wrapper depth or + /// the quantized checkpoint corrupts the unified vision path. The sibling + /// `embed_vision.embedding_projection` DOES have an affine loader branch and + /// must keep quantizing — this pins the exclusion to `vision_embedder` only. + #[test] + fn should_quantize_excludes_unified_vision_embedder() { + // vision_embedder patch projection → never quantized (bf16-only loader). + assert!(!should_quantize( + "language_model.model.vision_embedder.patch_dense.weight", + false + )); + assert!(!should_quantize( + "vision_embedder.patch_dense.weight", + false + )); + + // Positive controls: ordinary text MLP weight quantizes, and the + // affine-loadable embed_vision projection must NOT be caught by the + // exclusion (guards against an over-broad `vision` substring match). + assert!(should_quantize( + "language_model.model.layers.0.mlp.gate_proj.weight", + false + )); + assert!(should_quantize( + "embed_vision.embedding_projection.weight", + false + )); + } + + /// `gemma4_unified` must be a first-class convertible model_type: it resolves + /// to a recipe (the shared `Gemma4Recipe`) and appears in the registry's + /// single source of truth. Explicit guard alongside the registry-consistency + /// test in `recipe_registry_reproduces_inline_flags`. + #[test] + fn gemma4_unified_is_convertible() { + assert!(recipe::recipe_for("gemma4_unified").is_some()); + assert!(recipe::CONVERTIBLE_MODEL_TYPES.contains(&"gemma4_unified")); + // Routes to the same recipe family as gemma4, and self-declares coverage. + assert!( + recipe::recipe_for("gemma4_unified") + .unwrap() + .model_types() + .contains(&"gemma4_unified") + ); + } + + /// `vision_embedder.*` must be written at its BARE key (sibling of + /// `embed_vision.*`), not mis-prefixed under `language_model.model.`. The + /// loader installs it from the bare key; the multimodal-keep block in + /// `Gemma4Recipe::sanitize` must route it there instead of falling through to + /// the text re-prefix step. + #[test] + fn gemma4_recipe_sanitize_keeps_vision_embedder_bare() { + let f32 = |numel: usize, shape: &[i64]| { + MxArray::from_float32(&vec![1.0f32; numel], shape).expect("from_float32") + }; + + let mut weights: HashMap = HashMap::new(); + weights.insert( + "model.vision_embedder.patch_dense.weight".into(), + f32(4 * 4, &[4, 4]), + ); + weights.insert( + "model.vision_embedder.pos_embedding".into(), + f32(4 * 4, &[4, 4]), + ); + + let out = recipe::Gemma4Recipe + .sanitize( + weights, + &serde_json::json!({}), + "bfloat16", + /* tie_word_embeddings */ true, + /* verbose */ false, + ) + .expect("gemma4 sanitize"); + + assert!( + out.contains_key("vision_embedder.patch_dense.weight"), + "vision_embedder.patch_dense.weight must be kept bare; got {:?}", + out.keys().collect::>() + ); + assert!( + out.contains_key("vision_embedder.pos_embedding"), + "vision_embedder.pos_embedding must be kept bare; got {:?}", + out.keys().collect::>() + ); + assert!( + !out.keys() + .any(|k| k.contains("language_model.model.vision_embedder")), + "vision_embedder must NOT be re-prefixed under language_model.model.; got {:?}", + out.keys().collect::>() + ); + } + /// The `--quantize` refuse-pre-quantized-MTP guard /// (`is_pre_quantized_mtp_key`) must detect MTP `scales`/`biases` under ALL /// wrapper depths, including the longest triple-wrap diff --git a/docs/cli.md b/docs/cli.md index 83df7bdfc..bd44dbd82 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -99,7 +99,7 @@ Auto-detected by the `.gguf` extension. Supports BF16, F16, F32, Q4_0, Q4_1, Q8_ The converter auto-detects model families and applies family-specific sanitization passes: - `qwen3_5`, `qwen3_5_moe` -- `gemma4` +- `gemma4`, `gemma4_unified` - `paddleocr-vl`, `qianfan-ocr` - `pp-lcnet-ori`, `uvdoc` diff --git a/packages/cli/__test__/convert-cmd.test.ts b/packages/cli/__test__/convert-cmd.test.ts index 9329cd429..986c9c97a 100644 --- a/packages/cli/__test__/convert-cmd.test.ts +++ b/packages/cli/__test__/convert-cmd.test.ts @@ -18,12 +18,12 @@ import { join } from 'node:path'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vite-plus/test'; vi.mock('@mlx-node/core', () => ({ - convertModel: vi.fn(async () => ({})), + convertModel: vi.fn(async () => ({ numTensors: 0, numParameters: 0, outputPath: '', tensorNames: [] })), convertForeignWeights: vi.fn(() => ({})), convertGgufToSafetensors: vi.fn(async () => ({})), })); -import { convertGgufToSafetensors } from '@mlx-node/core'; +import { convertModel, convertGgufToSafetensors } from '@mlx-node/core'; import { run as runConvert } from '../src/commands/convert.js'; @@ -60,3 +60,56 @@ describe('mlx convert GGUF validation', () => { expect(convertGgufToSafetensors).not.toHaveBeenCalled(); }); }); + +describe('mlx convert model-type auto-detection', () => { + // Drive run() against a synthetic config.json (no --model-type) and read back + // the modelType handed to the mocked native convertModel. This guards the + // gemma4_unified pass-through: collapsing it to 'gemma4' would dead-code the + // native recipe_for("gemma4_unified") arm and misroute gemma-QAT unified + // checkpoints into the E2B-only prequantized importer. + const detectModelTypeFromConfig = async (config: Record): Promise => { + vi.spyOn(console, 'log').mockImplementation(() => {}); + vi.spyOn(console, 'error').mockImplementation(() => {}); + vi.spyOn(process, 'exit').mockImplementation(((code?: number) => { + throw new Error(`process.exit(${code})`); + }) as never); + + const inputDir = mkdtempSync(join(tmpdir(), 'mlx-convert-detect-in-')); + writeFileSync(join(inputDir, 'config.json'), JSON.stringify(config)); + const outputDir = join(tmp, 'out'); + + await runConvert(['--input', inputDir, '--output', outputDir]); + + const mock = vi.mocked(convertModel); + expect(mock).toHaveBeenCalledTimes(1); + const opts = mock.mock.calls[0]![0] as { modelType?: unknown }; + rmSync(inputDir, { recursive: true, force: true }); + return opts.modelType; + }; + + const detectModelType = async (configModelType: string): Promise => + detectModelTypeFromConfig({ model_type: configModelType }); + + it("passes 'gemma4_unified' through unchanged (does NOT collapse to 'gemma4')", async () => { + expect(await detectModelType('gemma4_unified')).toBe('gemma4_unified'); + }); + + it("collapses 'gemma4' to 'gemma4'", async () => { + expect(await detectModelType('gemma4')).toBe('gemma4'); + }); + + it("collapses 'gemma4_text' to 'gemma4'", async () => { + expect(await detectModelType('gemma4_text')).toBe('gemma4'); + }); + + it("detects an architecture-only unified config (no model_type) as 'gemma4_unified'", async () => { + // Mirrors the runtime loader: a config with no `model_type` but with + // `architectures: ['Gemma4UnifiedForConditionalGeneration']` must resolve + // to 'gemma4_unified' so Gemma4Recipe::sanitize runs. Without the + // converter arm, modelType would stay undefined and the output would be + // unloadable. + expect(await detectModelTypeFromConfig({ architectures: ['Gemma4UnifiedForConditionalGeneration'] })).toBe( + 'gemma4_unified', + ); + }); +}); diff --git a/packages/cli/src/commands/convert.ts b/packages/cli/src/commands/convert.ts index 24177eb3d..3616e85a0 100644 --- a/packages/cli/src/commands/convert.ts +++ b/packages/cli/src/commands/convert.ts @@ -437,6 +437,32 @@ export async function run(argv: string[]) { } else if (config.model_type === 'gemma4' || config.model_type === 'gemma4_text') { modelType = 'gemma4'; console.log(`Auto-detected model type: ${modelType} (from config.json)`); + } else if ( + config.model_type === 'gemma4_unified' || + (Array.isArray(config.architectures) && config.architectures.includes('Gemma4UnifiedForConditionalGeneration')) + ) { + // Pass the raw 'gemma4_unified' string through unchanged. Native + // recipe_for resolves it to the shared Gemma4Recipe, so every + // recipe-keyed code path (sym8_supported, sanitizer, embed_quantizable, + // mtp_policy, etc.) behaves identically to 'gemma4'. Collapsing it to + // 'gemma4' here would (a) make the native recipe_for("gemma4_unified") + // arm dead code, and (b) misroute a unified checkpoint that carries + // gemma-QAT metadata into the E2B-only prequantized importer, whose + // gate keys on the exact string "gemma4" (and which then hard-errors in + // validate_e2b_qat_schedule). The exact-"gemma4" gate must not match + // unified, so the raw string has to reach the native side. + // + // The architecture-only arm (no `model_type`, only + // `architectures: ['Gemma4UnifiedForConditionalGeneration']`) mirrors + // the runtime loader, which also flags this shape as unified + // (model-loader.ts maps it to gemma4; persistence.rs parse_config sets + // is_unified on EITHER model_type == "gemma4_unified" OR that + // architecture). Without this, an architecture-only config would leave + // modelType undefined and skip Gemma4Recipe::sanitize, producing + // unloadable output. It maps to 'gemma4_unified' (not 'gemma4') so the + // E2B-importer gate above still cannot match it. + modelType = 'gemma4_unified'; + console.log(`Auto-detected model type: ${modelType} (from config.json)`); } else if (config.model_type === 'lfm2_moe' || config.model_type === 'lfm2') { modelType = config.model_type; console.log(`Auto-detected model type: ${modelType} (from config.json)`); From 4a474a9bf36bae71b538992f442aa9850b6ded96 Mon Sep 17 00:00:00 2001 From: LongYinan Date: Fri, 26 Jun 2026 00:26:23 +0800 Subject: [PATCH 2/8] feat(gemma4): load tied lm_head as packed-quantized (8-bit affine) (#77) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What Loads a **quantized tied embedding / lm_head** for gemma4 as a **packed** table instead of dequantizing the whole thing to dense bf16. Before: a quantized `embed_tokens` went through `Embedding::load_quantized`, which dequantizes the entire table into a dense bf16 buffer and transposes it into `embed_weight_t` for the tied lm_head matmul (~2 GB bf16 for the 12B `262144x3840` vocab). After: both tied and untied embeddings go through `load_quantized_packed`. `forward()` dequantizes only the gathered rows, and the tied lm_head projects through `Embedding::as_linear` (`mlx_quantized_matmul`, `transpose=true`) **without ever materializing the dense table**. The five logits sites in `model.rs` detect the packed backend via `is_packed_quantized()` and take the `as_linear` branch, so `embed_weight_t` stays `None` on the packed path. The embedding quant mode is resolved from the per-layer config (`affine` or `mxfp8`) and threaded to the packed backend; biases-presence is driven off the actual `embed_tokens.biases` tensor key (affine has biases; mxfp8 does not). Any other quant mode at this key is rejected loud rather than silently mis-dequantizing. ## Why / measured (gemma4 12B QAT-q4_0, M5 Max) Quantizing the tied head from bf16 → 8-bit affine (gs32): | | bf16 head | 8-bit affine head | |---|---|---| | decode tok/s | ~46 | ~50 (equal within noise) | | peak RSS | 8.9 GB | 8.1 GB (**−0.8 GB**) | | on-disk | 8.3 GB | 7.5 GB (**−0.8 GB**) | | greedy output | — | near-lossless (answers unchanged) | Validated coherent + near-lossless on **both MLX 0.31.2 and 0.32.0**. We also bench'd mxfp8 for this head — it's perf-tied with affine and only ~90 MB smaller, so 8-bit affine is the chosen default; the loader stays mode-generic so an mxfp8 tied head also loads. ## Scope **Load-side support.** It activates only when a checkpoint actually carries a quantized `embed_tokens` (`.scales` present + per-layer config). Dense bf16 tied models are unaffected (`is_packed_quantized()` is false → existing `embed_weight_t` path, unchanged). Auto-emitting a quantized tied head from `mlx convert` for gemma4 is a follow-up. ## Validation - `cargo clippy --all-targets -- -D warnings` + `cargo fmt --check`: clean - Packed-embedding unit tests green, incl. `packed_affine_as_linear_matches_dense_matmul` (guards the exact packed-`as_linear` ≡ dense-matmul equivalence the tied lm_head relies on) and `packed_mxfp8_forward_matches_dequant_full_then_gather` - `/codex:adversarial-review`: **approve, no material findings** (verified transpose orientation, softcap-after-logits at all 5 sites, dense-tied + untied routing intact, no uncovered MTP/draft/warmup reader of `embed_weight_t`) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- > [!NOTE] > **Medium Risk** > Changes core inference logits routing for quantized tied checkpoints; load-time guards reduce mis-dequant risk, but correctness depends on packed matmul matching the prior dense path. > > **Overview** > **Gemma4** no longer fully dequantizes a quantized `embed_tokens` into dense bf16 for a tied lm_head. Load now always uses **`load_quantized_packed`** (tied and untied), with **`resolve_packed_embed_params`** aligning affine vs mxfp8 to on-disk scales/biases and rejecting mis-resolved mxfp4/nvfp4-as-mxfp8 layouts. > > At **five logits sites** in `model.rs`, when there is no separate `lm_head` and embeddings are packed, logits come from **`embed_tokens.as_linear`** (`mlx_quantized_matmul`) instead of **`embed_weight_t`** or a dense transpose of the full table—avoiding a large resident bf16 vocab matrix on the hot path. > > Dense bf16 embeddings and an explicit `lm_head` are unchanged; the new behavior applies only when `.scales` are present and the packed backend is active. > > Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 9b34999d11bf4060c48f7af2e1107f670c702a00. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). --------- Co-authored-by: Claude Opus 4.8 --- crates/mlx-core/src/models/gemma4/model.rs | 20 + .../mlx-core/src/models/gemma4/persistence.rs | 361 ++++++++++++++---- 2 files changed, 312 insertions(+), 69 deletions(-) diff --git a/crates/mlx-core/src/models/gemma4/model.rs b/crates/mlx-core/src/models/gemma4/model.rs index 2064ffcfc..b6ed1eeee 100644 --- a/crates/mlx-core/src/models/gemma4/model.rs +++ b/crates/mlx-core/src/models/gemma4/model.rs @@ -3214,6 +3214,10 @@ impl Gemma4Inner { crate::models::gemma4::diagnostic::dump_norm(0, "post_final_norm", &hidden_states, None); let logits = if let Some(ref head) = self.lm_head { head.forward(&hidden_states)? + } else if self.embed_tokens.is_packed_quantized() { + // Packed tied lm_head: project through the quantized matmul without + // materializing the dense table. + self.embed_tokens.as_linear(&hidden_states)? } else if let Some(ref w_t) = self.embed_weight_t { hidden_states.matmul(w_t)? } else { @@ -3899,6 +3903,10 @@ impl Gemma4Inner { crate::models::gemma4::diagnostic::dump_norm(0, "post_final_norm", &hidden_states, None); let logits = if let Some(ref head) = self.lm_head { head.forward(&hidden_states)? + } else if self.embed_tokens.is_packed_quantized() { + // Packed tied lm_head: project through the quantized matmul without + // materializing the dense table. + self.embed_tokens.as_linear(&hidden_states)? } else if let Some(ref w_t) = self.embed_weight_t { hidden_states.matmul(w_t)? } else { @@ -4065,6 +4073,10 @@ impl Gemma4Inner { crate::models::gemma4::diagnostic::dump_norm(0, "post_final_norm", &hidden_states, None); let logits = if let Some(ref head) = self.lm_head { head.forward(&hidden_states)? + } else if self.embed_tokens.is_packed_quantized() { + // Packed tied lm_head: project through the quantized matmul without + // materializing the dense table. + self.embed_tokens.as_linear(&hidden_states)? } else if let Some(ref w_t) = self.embed_weight_t { hidden_states.matmul(w_t)? } else { @@ -5567,6 +5579,10 @@ pub(crate) fn warmup_forward(inner: &Gemma4Inner) -> Result<()> { h = inner.final_norm.forward(&h)?; let logits = if let Some(ref head) = inner.lm_head { head.forward(&h)? + } else if inner.embed_tokens.is_packed_quantized() { + // Packed tied lm_head: project through the quantized matmul without + // materializing the dense table. + inner.embed_tokens.as_linear(&h)? } else if let Some(ref w_t) = inner.embed_weight_t { h.matmul(w_t)? } else { @@ -5899,6 +5915,10 @@ fn forward_inner( // LM head or tied embeddings let logits = if let Some(head) = lm_head { head.forward(&h)? + } else if embedding.is_packed_quantized() { + // Packed tied lm_head: project through the quantized matmul without + // materializing the dense table. + embedding.as_linear(&h)? } else if let Some(w_t) = embed_weight_t { h.matmul(w_t)? } else { diff --git a/crates/mlx-core/src/models/gemma4/persistence.rs b/crates/mlx-core/src/models/gemma4/persistence.rs index 7551883fa..a775de588 100644 --- a/crates/mlx-core/src/models/gemma4/persistence.rs +++ b/crates/mlx-core/src/models/gemma4/persistence.rs @@ -21,12 +21,12 @@ use crate::tokenizer::Qwen3Tokenizer; use super::config::Gemma4Config; use super::model::{Gemma4Inner, Gemma4Model, warmup_forward}; use super::quantized_linear::{ - DEFAULT_QUANT_BITS, DEFAULT_QUANT_GROUP_SIZE, PerLayerMode, PerLayerQuant, is_mxfp8_checkpoint, - is_quantized_checkpoint, try_build_mxfp4_quantized_linear, - try_build_mxfp4_quantized_switch_linear, try_build_mxfp8_quantized_linear, - try_build_mxfp8_quantized_switch_linear, try_build_nvfp4_quantized_linear, - try_build_nvfp4_quantized_switch_linear, try_build_quantized_linear, - try_build_quantized_switch_linear, try_build_sym8_quantized_linear, + DEFAULT_QUANT_BITS, DEFAULT_QUANT_GROUP_SIZE, MXFP8_BITS, MXFP8_GROUP_SIZE, MXFP8_MODE, + PerLayerMode, PerLayerQuant, is_mxfp8_checkpoint, is_quantized_checkpoint, + try_build_mxfp4_quantized_linear, try_build_mxfp4_quantized_switch_linear, + try_build_mxfp8_quantized_linear, try_build_mxfp8_quantized_switch_linear, + try_build_nvfp4_quantized_linear, try_build_nvfp4_quantized_switch_linear, + try_build_quantized_linear, try_build_quantized_switch_linear, try_build_sym8_quantized_linear, }; // Quantization-block parsing now lives in `crate::models::quant_dispatch`, @@ -837,6 +837,126 @@ fn maybe_shard_ple_embedding( Ok(()) } +/// Packing parameters for a quantized embedding/tied-lm_head load: the +/// `(group_size, bits, mode_str, biases)` tuple handed to +/// `Embedding::load_quantized_packed`. +struct PackedEmbedParams<'a> { + group_size: i32, + bits: i32, + mode_str: &'static str, + biases: Option<&'a MxArray>, +} + +/// Resolve the packed-embedding parameters so `(mode, bits, group_size, biases)` +/// are mutually consistent before they reach `load_quantized_packed` → +/// `mlx_quantized_matmul`/`mlx_dequantize`. +/// +/// The on-disk tensors are the ground truth: affine packing ships a per-group +/// `.biases` companion with floating `.scales`; MXFP8 ships uint8 (E8M0) +/// `.scales` and NO `.biases`. We discriminate off that unambiguous tensor +/// evidence and force the matching pack constants, so a config that disagrees +/// with the tensors can never silently mis-lay-out the table: +/// * mxfp8 → force `(MXFP8_GROUP_SIZE, MXFP8_BITS, "mxfp8")` (the affine 4/64 +/// default that `default_plq` carries is wrong for an E8M0 table; MLX's +/// `quantized_matmul` honors the passed bits/group_size rather than +/// re-deriving them, so 4/64 would mis-unpack) and drop biases. +/// * affine → take `bits`/`group_size` from the resolved PLQ and pass the +/// `.biases` tensor through. +/// +/// Any other mode at this key, or a mode that contradicts the tensor evidence +/// (mxfp8 mode with `.biases`/non-uint8 scales, or affine mode with uint8 +/// scales), is rejected loud rather than producing garbage logits. +/// +/// uint8 scales are NOT exclusive to mxfp8: mxfp4 (4-bit, group_size 32) and +/// nvfp4 (4-bit, group_size 16) also carry uint8 scales and no biases. A +/// mode-less / stale `config.json` makes `is_mxfp8_checkpoint` resolve ANY +/// uint8-scale table to mxfp8, so an mxfp4/nvfp4 embedding can reach this arm. +/// Forcing 8/32 onto such a table would mis-describe it; we additionally verify +/// the packed weight and scales shapes are self-consistent with 8-bit / gs32 +/// before accepting (8-bit packs `32/8 = 4` values per `u32`, so a genuine +/// mxfp8 table satisfies `weight_last * 4 == scales_last * 32`, both equal to +/// the hidden width). A 4-bit mxfp4/nvfp4 table packs 8 values per `u32`, so +/// `weight_last` is half what mxfp8 expects and the equality fails — we reject +/// loud at load instead of forcing 8/32 onto it. +/// +/// Mirrors lfm2's `plq_to_packed_params` and `try_build_mxfp8_quantized_linear`, +/// which likewise force the MX constants and null biases for mxfp8. +fn resolve_packed_embed_params<'a>( + key: &str, + plq: PerLayerQuant, + weight: &MxArray, + scales: &MxArray, + biases: Option<&'a MxArray>, +) -> Result> { + let scales_uint8 = scales.dtype().ok() == Some(DType::Uint8); + match plq.mode { + PerLayerMode::Mxfp8 => { + if biases.is_some() { + return Err(Error::from_reason(format!( + "gemma4 {key} load: quant mode resolves to mxfp8 but a '{key}.biases' tensor \ + is present — mxfp8 has no biases (its E8M0 scales fully describe the \ + dequant); config/tensor disagreement, refusing to load" + ))); + } + if !scales_uint8 { + return Err(Error::from_reason(format!( + "gemma4 {key} load: quant mode resolves to mxfp8 but '{key}.scales' is not \ + uint8 (E8M0) — config/tensor disagreement, refusing to load" + ))); + } + // The mxfp8 resolution can come from a mode-less config that only saw + // uint8 scales (shared by mxfp4/nvfp4). Confirm the packed weight and + // scales last-dims are consistent with 8-bit packing at group_size 32 + // before forcing those constants: 8-bit packs `32/MXFP8_BITS` values + // per u32, so a genuine mxfp8 table has + // `weight_last * (32 / MXFP8_BITS) == scales_last * MXFP8_GROUP_SIZE`. + // mxfp4/nvfp4 (4-bit) halve `weight_last`, so the equality fails and + // we reject rather than mis-unpack. + let weight_last = *weight.shape()?.to_vec().last().ok_or_else(|| { + Error::from_reason(format!("gemma4 {key} load: weight is 0-rank")) + })?; + let scales_last = *scales.shape()?.to_vec().last().ok_or_else(|| { + Error::from_reason(format!("gemma4 {key} load: scales is 0-rank")) + })?; + let weight_cols = weight_last * i64::from(32 / MXFP8_BITS); + let scales_cols = scales_last * i64::from(MXFP8_GROUP_SIZE); + if weight_cols != scales_cols { + return Err(Error::from_reason(format!( + "gemma4 {key} load: quant mode resolved to mxfp8 but the packed weight \ + ({weight_last} u32 cols) and scales ({scales_last} groups) are not consistent \ + with 8-bit / group_size-{MXFP8_GROUP_SIZE} packing ({weight_cols} != \ + {scales_cols}); the table is most likely mxfp4/nvfp4 mis-resolved to mxfp8 \ + from a mode-less config — refusing to load with forced mxfp8 constants" + ))); + } + Ok(PackedEmbedParams { + group_size: MXFP8_GROUP_SIZE, + bits: MXFP8_BITS, + mode_str: MXFP8_MODE, + biases: None, + }) + } + PerLayerMode::Affine => { + if scales_uint8 { + return Err(Error::from_reason(format!( + "gemma4 {key} load: quant mode resolves to affine but '{key}.scales' is uint8 \ + (an E8M0/MX-format dtype) — config/tensor disagreement, refusing to load" + ))); + } + Ok(PackedEmbedParams { + group_size: plq.group_size, + bits: plq.bits, + mode_str: "affine", + biases, + }) + } + other => Err(Error::from_reason(format!( + "gemma4 {key} load: quant mode {other:?} is not supported for the embedding/tied \ + lm_head; only affine and mxfp8 are supported" + ))), + } +} + /// Apply sanitized weights to a Gemma4Inner. fn apply_weights( inner: &mut Gemma4Inner, @@ -925,76 +1045,55 @@ fn apply_weights( // Embedding. Q8 / Q4 affine checkpoints carry `.scales` (+ `.biases`) // companions alongside `.weight` with a packed-last-dim shape, so the // dense `load_weight` path trips its shape guard. Route quantized - // embeddings through `load_quantized`, which pre-dequantizes the full - // table for the forward lookup and (when tied) for the lm_head matmul. + // embeddings through `load_quantized_packed`, which keeps the table PACKED: + // `forward()` dequantizes only the gathered rows, and the tied lm_head + // projects through `as_linear` (`mlx_quantized_matmul`) without ever + // materializing the dense bf16 table. // - // Defense-in-depth: `Embedding::load_quantized` calls - // `mlx_dequantize(..., "affine")` unconditionally, so MXFP4/MXFP8 metadata - // at this key would silently mis-dequantize. The convert path already - // forces `embed_tokens` to affine (see `apply_mxfp_upgrade` and the - // no-recipe path), but if a future regression or hand-edited - // checkpoint claims otherwise we want to fail loud rather than emit - // garbage outputs. + // Defense-in-depth: the packed backend feeds `mlx_dequantize`/ + // `mlx_quantized_matmul` with the mode string passed below, so the mode + // must match the on-disk packing. Affine (Q4/Q8 with `.scales` + `.biases`) + // and MXFP8 (E8M0 `.scales`, no `.biases`) are both supported here; any + // other quant mode at this key is rejected so a regression or hand-edited + // checkpoint fails loud instead of silently mis-dequantizing into garbage. let embed_quantized = params.contains_key("embed_tokens.scales"); - // Only enforce the affine-only guard when the embedding is actually - // quantized (has .scales). Dense bf16 embeddings have no tensor-side - // mode, so metadata claiming MXFP at the top level is irrelevant — - // there is nothing to mis-dequantize. - if embed_quantized { - let embed_plq = per_layer_quant - .get("embed_tokens") - .copied() - .unwrap_or(default_plq); - if embed_plq.mode != PerLayerMode::Affine { - return Err(Error::from_reason(format!( - "gemma4 embed_tokens load: Non-affine FP mode {:?} is not supported; affine only", - embed_plq.mode - ))); - } - } + // Resolve the embedding's quant mode once, defaulting to the checkpoint + // default when no per-layer override is present. Only consult this when the + // embedding is actually quantized (has `.scales`) — a dense bf16 embedding + // has no tensor-side mode, so top-level MXFP metadata is irrelevant. + let embed_plq = per_layer_quant + .get("embed_tokens") + .copied() + .unwrap_or(default_plq); if embed_quantized && let Some(w) = params.get("embed_tokens.weight") { - let embed_plq = per_layer_quant - .get("embed_tokens") - .copied() - .unwrap_or(default_plq); let scales = params.get("embed_tokens.scales").ok_or_else(|| { Error::from_reason("Missing embed_tokens.scales for quantized embedding") })?; let biases = params.get("embed_tokens.biases"); - if config.tie_word_embeddings { - // Tied lm_head reads the whole table as a dense matmul weight - // (`embed_weight_t` below), so it must be dequantized into one dense - // bf16 buffer up front. - inner.embed_tokens.load_quantized( - w, - scales, - biases, - embed_plq.group_size, - embed_plq.bits, - )?; - let dequant = inner.embed_tokens.get_weight(); - let w_t = dequant.transpose(Some(&[1, 0]))?; - inner.embed_weight_t = Some(w_t); - } else { - // Untied: the table is only ever gathered one row at a time in - // `forward()` (never tied, never read via `get_weight()` on the hot - // path), so keep it PACKED and dequantize only the looked-up rows. - // Byte-identical to the dense path — affine dequant is per-element, - // so gather-then-dequant == dequant-then-gather — but the 2-bit - // `[vocab, hidden]` table stays packed instead of materializing a - // dense bf16 copy (measured ~0.63 GiB resident reclaimed for E2B: - // a 262144x1536 2-bit table is 0.75 GiB dense vs 0.12 GiB packed). - // Mirrors the PLE `embed_tokens_per_layer` packed load below; mode - // is guaranteed affine by the guard above. - inner.embed_tokens.load_quantized_packed( - w, - scales, - biases, - embed_plq.group_size, - embed_plq.bits, - "affine", - )?; - } + // Make `(mode, bits, group_size, biases)` mutually consistent before the + // packed backend feeds `mlx_quantized_matmul`/`mlx_dequantize`. The + // helper forces the MX pack constants for mxfp8 (the affine 4/64 carried + // by `default_plq` would mis-unpack an E8M0 table, since MLX honors the + // passed bits/group_size) and drops biases, takes bits/group_size from + // the PLQ for affine, and fails loud on any mode/tensor contradiction. + let packed = resolve_packed_embed_params("embed_tokens", embed_plq, w, scales, biases)?; + // Tied (12B/27B) and untied embeddings both keep the table PACKED and + // dequantize only the gathered rows in `forward()`; the tied lm_head + // projects through `as_linear` (`mlx_quantized_matmul`) without ever + // materializing the dense table (~2 GiB bf16 for the 262144x3840 12B + // vocab). The hot-path logits sites in `model.rs` detect the packed + // backend via `Embedding::is_packed_quantized()` and call `as_linear`, + // so `embed_weight_t` stays None and those sites take the packed branch. + // Mirrors LFM2's tied/quantized embedding load and the PLE packed load + // below; the mode string is threaded through to the dequant/matmul. + inner.embed_tokens.load_quantized_packed( + w, + scales, + packed.biases, + packed.group_size, + packed.bits, + packed.mode_str, + )?; } else if let Some(w) = params.get("embed_tokens.weight") { // Dense embedding fallback (no `.scales`): a stripped quant group // must never reach the dense lookup / tied-lm_head matmul. @@ -3184,4 +3283,128 @@ mod tests { validate_required_weights(&p, &text_only) .expect("text-only config must not require unified vision keys"); } + + /// An mxfp8-mode embedding whose resolved PLQ still carries the affine + /// 4-bit / group_size-64 defaults (e.g. `default_plq` fallback, or an + /// override missing explicit bits/group_size) must NOT thread 4/64 into the + /// packed backend: MLX honors the passed bits/group_size, so 4/64 would + /// mis-unpack the E8M0 table. The resolver forces the MX pack constants + /// (8 / 32) and nulls biases. + #[test] + fn resolve_packed_embed_mxfp8_forces_mx_constants() { + // Genuine mxfp8 table for hidden=64, vocab=4: 8-bit packs 4 vals/u32 so + // the packed weight has 64/4 = 16 u32 cols; group_size 32 gives 64/32 = 2 + // scale groups. weight_last(16)*4 == scales_last(2)*32 == 64, so the + // 8-bit/gs32 self-consistency guard accepts it. + let weight = MxArray::zeros(&[4, 16], Some(DType::Uint32)).expect("packed weight"); + let scales = MxArray::zeros(&[4, 2], Some(DType::Uint8)).expect("uint8 scales"); + let plq = PerLayerQuant { + bits: 4, + group_size: 64, + mode: PerLayerMode::Mxfp8, + }; + let packed = resolve_packed_embed_params("embed_tokens", plq, &weight, &scales, None) + .expect("mxfp8 with affine-default bits must resolve, not error"); + assert_eq!(packed.bits, MXFP8_BITS, "mxfp8 bits must be forced to 8"); + assert_eq!( + packed.group_size, MXFP8_GROUP_SIZE, + "mxfp8 group_size must be forced to 32" + ); + assert_eq!(packed.mode_str, MXFP8_MODE); + assert!(packed.biases.is_none(), "mxfp8 carries no biases"); + } + + /// A uint8-scale table whose packed-weight / scales shapes match mxfp4 + /// (4-bit, group_size 32) rather than mxfp8 — but which a mode-less config + /// mis-resolved to `Mxfp8` via `is_mxfp8_checkpoint` (uint8 scales only) — + /// must be rejected loud, NOT loaded with forced 8/32 mxfp8 constants. + /// + /// hidden=64, vocab=4: 4-bit packs 8 vals/u32 so the packed weight has + /// 64/8 = 8 u32 cols; group_size 32 gives 64/32 = 2 scale groups. Under the + /// forced 8-bit reading, weight_last(8)*4 = 32 != scales_last(2)*32 = 64, so + /// the shapes are inconsistent with 8-bit/gs32 and the guard fires. + #[test] + fn resolve_packed_embed_mxfp4_shapes_resolved_to_mxfp8_fails_loud() { + let weight = MxArray::zeros(&[4, 8], Some(DType::Uint32)).expect("mxfp4-packed weight"); + let scales = MxArray::zeros(&[4, 2], Some(DType::Uint8)).expect("uint8 scales"); + let plq = PerLayerQuant { + bits: 4, + group_size: 32, + mode: PerLayerMode::Mxfp8, + }; + let err = resolve_packed_embed_params("embed_tokens", plq, &weight, &scales, None) + .err() + .expect("mxfp4-shaped table mis-resolved to mxfp8 must fail loud"); + assert!( + err.reason.contains("mxfp8") && err.reason.contains("mxfp4"), + "error names the mxfp8/mxfp4 mismatch: {}", + err.reason + ); + } + + /// Affine mode threads the PLQ's own bits/group_size through and passes the + /// `.biases` tensor (asymmetric affine has per-group biases). + #[test] + fn resolve_packed_embed_affine_passes_plq_params_and_biases() { + let weight = MxArray::zeros(&[4, 16], Some(DType::Uint32)).expect("packed weight"); + let scales = MxArray::zeros(&[4, 2], Some(DType::BFloat16)).expect("bf16 scales"); + let biases = MxArray::zeros(&[4, 2], Some(DType::BFloat16)).expect("bf16 biases"); + let plq = PerLayerQuant { + bits: 8, + group_size: 32, + mode: PerLayerMode::Affine, + }; + let packed = + resolve_packed_embed_params("embed_tokens", plq, &weight, &scales, Some(&biases)) + .expect("affine embedding must resolve"); + assert_eq!(packed.bits, 8); + assert_eq!(packed.group_size, 32); + assert_eq!(packed.mode_str, "affine"); + assert!(packed.biases.is_some(), "affine biases must pass through"); + } + + /// Mode/tensor contradiction is rejected loud: mxfp8 mode never coexists + /// with a `.biases` tensor (mxfp8 has none). + #[test] + fn resolve_packed_embed_mxfp8_with_biases_fails_loud() { + let weight = MxArray::zeros(&[4, 16], Some(DType::Uint32)).expect("packed weight"); + let scales = MxArray::zeros(&[4, 2], Some(DType::Uint8)).expect("uint8 scales"); + let biases = MxArray::zeros(&[4, 2], Some(DType::BFloat16)).expect("bf16 biases"); + let plq = PerLayerQuant { + bits: 8, + group_size: 32, + mode: PerLayerMode::Mxfp8, + }; + // `.err()` (not `expect_err`) so the success type needs no `Debug` bound + // (`PackedEmbedParams` holds `Option<&MxArray>`, and `MxArray: !Debug`). + let err = resolve_packed_embed_params("embed_tokens", plq, &weight, &scales, Some(&biases)) + .err() + .expect("mxfp8 + biases must fail loud"); + assert!( + err.reason.contains("mxfp8"), + "error mentions mxfp8: {}", + err.reason + ); + } + + /// Affine mode with uint8 (E8M0/MX) scales is a contradiction — reject loud + /// rather than feed an MX-format table to the affine dequant. + #[test] + fn resolve_packed_embed_affine_with_uint8_scales_fails_loud() { + let weight = MxArray::zeros(&[4, 16], Some(DType::Uint32)).expect("packed weight"); + let scales = MxArray::zeros(&[4, 2], Some(DType::Uint8)).expect("uint8 scales"); + let plq = PerLayerQuant { + bits: 8, + group_size: 32, + mode: PerLayerMode::Affine, + }; + let err = resolve_packed_embed_params("embed_tokens", plq, &weight, &scales, None) + .err() + .expect("affine + uint8 scales must fail loud"); + assert!( + err.reason.contains("affine"), + "error mentions affine: {}", + err.reason + ); + } } From 409a7b5b3777033bdafb8c81556a69be9ae8682e Mon Sep 17 00:00:00 2001 From: LongYinan Date: Fri, 26 Jun 2026 08:44:48 +0800 Subject: [PATCH 3/8] fix(convert): apply imatrix AWQ on VLM checkpoints + compensate GDN in_proj_a/b (#78) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Converting `qwen-agentworld-35b-a3b` (a `qwen3_5_moe` `*ForConditionalGeneration`, GatedDeltaNet hybrid) to **unsloth + mxfp8** with an imatrix surfaced two real bugs in `apply_awq_prescaling` that silently degrade AWQ for **VLM-wrapped** and **GDN** checkpoints. Both are fixed here with a unit test, and validated end-to-end on the real model. ## Bug 1 — imatrix AWQ was a silent no-op on VLM checkpoints imatrix importance is always keyed canonical `model.layers.N.*` (produced by `gguf_name_to_hf` from `blk.N.*`). But sanitized VLM checkpoints carry `language_model.model.layers.N.*`, and `apply_awq_prescaling` auto-detected that prefix and used it for **both** the weight ops **and** the importance lookups → every `importance.get` missed → `modified 0`, with no warning (the missing-key warning only fires on a *partial* match). **Fix:** `imatrix_lookup_key()` strips the `language_model.` wrapper before the importance lookup; weight ops keep the detected prefix. `apply_awq_prescaling` now returns the modified count, and the caller emits a `warn!` when it is 0 despite an imatrix being supplied (so this class of silent no-op is visible). ## Bug 2 — AWQ Group D distorted GDN `in_proj_a` / `in_proj_b` Group D divides the shared `input_layernorm` by per-channel `s` but scaled only `in_proj_qkv` + `in_proj_z`. All four `in_proj_*` projections read the **same** `input_layernorm` output: - `decoder_layer.rs:203,207` — `normed = input_layernorm.forward(x)` → `gdn.forward(&normed, …)` - `gated_delta_net.rs:264/270-271` — that `normed` feeds **both** `in_proj_qkvz` **and** `in_proj_ba` (= b ++ a) So leaving `in_proj_a`/`in_proj_b` unscaled divided their inputs by `s` (here ~0.18–5.5×/channel) with un-compensated weights → corrupted GDN decay (`a`) and beta (`b`) gates. The reparametrization is output-preserving only if *every* consumer of the divided norm is column-scaled by `s`. **Fix:** column-scale `in_proj_a`/`in_proj_b` by the same `s` (their 8-bit-affine bit-width is orthogonal and unchanged). Also corrects the stale doc comment that claimed a/b "have no preceding norm" — only `o_proj`/`out_proj` lack one. ## Test `awq_prescaling_matches_vlm_prefixed_weights`: VLM-prefixed GDN (Group D) + full-attention (Group C) layers with a canonically-keyed imatrix must report `modified == 9`. ``` modified == 0 before fix 1 (prefix mismatch → no-op) modified == 7 before fix 2 (a/b skipped) modified == 9 after both fixes ``` ## Validation - `cargo test -p mlx-core --lib convert::` → 104 passed; clippy + fmt clean. - Real convert of `qwen-agentworld-35b-a3b` with `--q-recipe unsloth --q-mxfp --q-bits 4 --imatrix-path …` (67GB bf16 → ~20GB, 4 shards) → coherent generation. - On-artifact AWQ fingerprint `conv/(orig+1)`: per-channel **non-uniform** on `input_layernorm` (Group C full-attn + Group D GDN), ≈1.0 on `post_attention_layernorm` (Group A correctly inert — no dense FFN in MoE). > Note: `--q-recipe unsloth --q-mode mxfp8` is rejected by the CLI (recipes allow > only `affine`|`nvfp4`); the recipe+micro-scaling path is `--q-mxfp`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- > [!NOTE] > **Medium Risk** > Changes quantization weight math on convert for VLM and GDN models; incorrect scaling would affect model quality, but scope is limited to the AWQ path with an imatrix and is covered by a targeted regression test. > > **Overview** > Fixes **AWQ pre-scaling** during convert so imatrix-based importance actually applies on **VLM-wrapped** checkpoints and stays **mathematically consistent** on **GatedDeltaNet** layers. > > **VLM / imatrix key mismatch:** Weight keys stay on the detected prefix (e.g. `language_model.model.layers.*`) while imatrix lookups now strip `language_model.` via `imatrix_lookup_key`, matching canonical `model.layers.*` entries from GGUF imatrix files. Previously every lookup missed and AWQ did nothing with no signal. `apply_awq_prescaling` now returns a **modified tensor count**; the convert path **warns** when an imatrix was supplied but zero weights were touched. > > **GDN Group D:** When AWQ divides shared `input_layernorm` and column-scales `in_proj_qkv` / `in_proj_z`, it also column-scales **`in_proj_a` and `in_proj_b`** with the same scale (importance still derived from qkv+z). Those projections share the normed input; skipping them left gates wrongly scaled. Docs for the unsloth recipe are updated to reflect this. > > Adds regression test **`awq_prescaling_matches_vlm_prefixed_weights`** expecting `modified == 9`. > > Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 43468d0cc87e1e6193ab70d66e699e9a74ac7203. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). Co-authored-by: Claude Opus 4.8 --- crates/mlx-core/src/convert.rs | 172 ++++++++++++++++++++++++++++++--- 1 file changed, 158 insertions(+), 14 deletions(-) diff --git a/crates/mlx-core/src/convert.rs b/crates/mlx-core/src/convert.rs index 9f483bcd0..974866799 100644 --- a/crates/mlx-core/src/convert.rs +++ b/crates/mlx-core/src/convert.rs @@ -2301,7 +2301,14 @@ async fn convert_model_inner(options: ConversionOptions) -> Result=2, and the differing reduction order @@ -5130,7 +5141,7 @@ pub(crate) fn apply_awq_prescaling( imatrix: &crate::utils::imatrix::ImatrixData, ratio: f32, num_layers: usize, -) -> Result<()> { +) -> Result { info!( "Applying AWQ pre-scaling: {} layers, ratio={}, {} imatrix entries", num_layers, @@ -5234,16 +5245,29 @@ pub(crate) fn apply_awq_prescaling( } } - // ── Group D: input_layernorm → linear_attn.in_proj_qkv + in_proj_z ── - // (Only present in GatedDeltaNet layers) + // ── Group D: input_layernorm → linear_attn.in_proj_qkv + in_proj_z + // + in_proj_a + in_proj_b ── + // (Only present in GatedDeltaNet layers.) All four in_proj_* projections + // read the SAME input_layernorm output (see gated_delta_net.rs forward: + // both in_proj_qkvz and in_proj_ba matmul the normed input). The AWQ + // scale `s` is derived from qkv+z importance, but because the shared + // input_layernorm is divided by `s` below, EVERY consumer of that norm + // must have its input columns multiplied by `s` for the reparametrization + // to stay output-preserving. Omitting in_proj_a/in_proj_b would leave + // their inputs divided by `s` with un-compensated weights, distorting the + // GDN decay (`a`) and beta (`b`) gates. Their bit-width (8-bit affine) is + // orthogonal and unchanged — this is a correctness compensation, not a + // quantization-quality tweak. let qkv_key = format!("{prefix}.linear_attn.in_proj_qkv.weight"); let z_key = format!("{prefix}.linear_attn.in_proj_z.weight"); + let a_key = format!("{prefix}.linear_attn.in_proj_a.weight"); + let b_key = format!("{prefix}.linear_attn.in_proj_b.weight"); // Only apply if this layer has linear_attn weights (GDN layer) if weights.contains_key(&qkv_key) && let Some(scales) = compute_multi_key_scales(imatrix, &[&qkv_key, &z_key], ratio)? { - for proj_key in [&qkv_key, &z_key] { + for proj_key in [&qkv_key, &z_key, &a_key, &b_key] { if let Some(proj) = weights.remove(proj_key) { let scaled = scale_columns(&proj, &scales)?; weights.insert(proj_key.to_string(), scaled); @@ -5277,7 +5301,19 @@ pub(crate) fn apply_awq_prescaling( "AWQ pre-scaling complete: modified {} weight tensors", modified ); - Ok(()) + Ok(modified) +} + +/// Map a (possibly VLM-wrapped) weight key to its canonical imatrix key. +/// +/// Imatrix importance is always keyed with the canonical `model.layers.N.*` +/// names produced by `gguf_name_to_hf` from a `blk.N.*` GGUF. Sanitized VLM +/// checkpoints (e.g. qwen3_5_moe `*ForConditionalGeneration`) instead carry the +/// `language_model.model.layers.N.*` prefix on their weights. Stripping the +/// `language_model.` wrapper aligns the lookup with the imatrix; plain +/// `model.layers.*` keys pass through unchanged. +fn imatrix_lookup_key(key: &str) -> &str { + key.strip_prefix("language_model.").unwrap_or(key) } /// Compute AWQ scales for Group A (norm → gate_proj + up_proj). @@ -5288,8 +5324,8 @@ fn compute_group_a_scales( up_key: &str, ratio: f32, ) -> Result> { - let gate_imp = imatrix.importance.get(gate_key); - let up_imp = imatrix.importance.get(up_key); + let gate_imp = imatrix.importance.get(imatrix_lookup_key(gate_key)); + let up_imp = imatrix.importance.get(imatrix_lookup_key(up_key)); match (gate_imp, up_imp) { (Some(g), Some(u)) => { @@ -5314,7 +5350,7 @@ fn compute_multi_key_scales( ) -> Result> { let importances: Vec<&Vec> = keys .iter() - .filter_map(|k| imatrix.importance.get(*k)) + .filter_map(|k| imatrix.importance.get(imatrix_lookup_key(k))) .collect(); if importances.is_empty() { @@ -5325,7 +5361,7 @@ fn compute_multi_key_scales( if importances.len() < keys.len() { let missing: Vec<&str> = keys .iter() - .filter(|k| !imatrix.importance.contains_key(**k)) + .filter(|k| !imatrix.importance.contains_key(imatrix_lookup_key(k))) .copied() .collect(); warn!( @@ -5372,7 +5408,7 @@ fn compute_scales_for_key( key: &str, ratio: f32, ) -> Result> { - match imatrix.importance.get(key) { + match imatrix.importance.get(imatrix_lookup_key(key)) { Some(imp) => { let scales = compute_normalized_scales(imp, ratio)?; Ok(Some(scales)) @@ -5444,6 +5480,114 @@ mod tests { use super::*; use crate::convert::recipe::{self, ConversionRecipe}; + /// AWQ pre-scaling must fire on VLM-wrapped checkpoints whose sanitized + /// weights carry the `language_model.model.layers.*` prefix (e.g. the + /// qwen3_5_moe `qwen-agentworld` checkpoint), while the imatrix is always + /// keyed with the canonical `model.layers.*` names produced by + /// `gguf_name_to_hf`. Regression: that prefix mismatch silently turned AWQ + /// into a no-op (`modified == 0`), so the unsloth recipe's low-bit + /// attention/SSM projections shipped without importance correction. + #[test] + fn awq_prescaling_matches_vlm_prefixed_weights() { + use crate::utils::imatrix::ImatrixData; + + const K: i64 = 4; // input features (columns) + const N: i64 = 2; // output features (rows) + + let ones = |shape: &[i64]| { + let numel: usize = shape.iter().product::() as usize; + MxArray::from_float32(&vec![1.0f32; numel], shape).expect("from_float32") + }; + + // Sanitized VLM-wrapped weights: GDN layer 0 + full-attention layer 1. + let mut weights: HashMap = HashMap::new(); + // Layer 0 — GatedDeltaNet (linear_attn) → exercises AWQ Group D. + // in_proj_a/in_proj_b also read the shared input_layernorm output (see + // gated_delta_net.rs forward), so Group D must compensate their columns + // too — otherwise the norm-division distorts the GDN decay/beta gates. + weights.insert( + "language_model.model.layers.0.linear_attn.in_proj_qkv.weight".into(), + ones(&[N, K]), + ); + weights.insert( + "language_model.model.layers.0.linear_attn.in_proj_z.weight".into(), + ones(&[N, K]), + ); + weights.insert( + "language_model.model.layers.0.linear_attn.in_proj_a.weight".into(), + ones(&[N, K]), + ); + weights.insert( + "language_model.model.layers.0.linear_attn.in_proj_b.weight".into(), + ones(&[N, K]), + ); + weights.insert( + "language_model.model.layers.0.input_layernorm.weight".into(), + ones(&[K]), + ); + // Layer 1 — full attention → exercises AWQ Group C. + weights.insert( + "language_model.model.layers.1.self_attn.q_proj.weight".into(), + ones(&[N, K]), + ); + weights.insert( + "language_model.model.layers.1.self_attn.k_proj.weight".into(), + ones(&[N, K]), + ); + weights.insert( + "language_model.model.layers.1.self_attn.v_proj.weight".into(), + ones(&[N, K]), + ); + weights.insert( + "language_model.model.layers.1.input_layernorm.weight".into(), + ones(&[K]), + ); + + // imatrix keyed with canonical `model.layers.*` names (no + // `language_model.`), exactly as gguf_name_to_hf emits them from a + // `blk.N.*` imatrix GGUF. + let importance: HashMap> = [ + ( + "model.layers.0.linear_attn.in_proj_qkv.weight", + vec![1.0, 2.0, 3.0, 4.0], + ), + ( + "model.layers.0.linear_attn.in_proj_z.weight", + vec![4.0, 3.0, 2.0, 1.0], + ), + ( + "model.layers.1.self_attn.q_proj.weight", + vec![1.0, 2.0, 3.0, 4.0], + ), + ( + "model.layers.1.self_attn.k_proj.weight", + vec![2.0, 2.0, 2.0, 2.0], + ), + ( + "model.layers.1.self_attn.v_proj.weight", + vec![4.0, 3.0, 2.0, 1.0], + ), + ] + .into_iter() + .map(|(k, v)| (k.to_string(), v)) + .collect(); + let imatrix = ImatrixData { + importance, + chunk_count: 1, + chunk_size: 1, + }; + + let modified = apply_awq_prescaling(&mut weights, &imatrix, 0.5, 2).expect("awq"); + + // Group D (qkv + z + a + b + norm = 5) on layer 0, Group C (q + k + v + + // norm = 4) on layer 1. Before the prefix-decoupling fix this was 0 + // (silent no-op); before the a/b-compensation fix it was 7 (a/b skipped). + assert_eq!( + modified, 9, + "AWQ must fire on VLM-prefixed weights and compensate in_proj_a/in_proj_b" + ); + } + /// Registry-consistency gate: for the exhaustive set of supported /// `model_type` strings (plus a non-convertible control), the four /// recipe-sourced asymmetry flags must reproduce EXACTLY the From ed672d16ef12243ee0dc2ba20a0c6290b67cc3f1 Mon Sep 17 00:00:00 2001 From: LongYinan Date: Fri, 26 Jun 2026 09:55:57 +0800 Subject: [PATCH 4/8] =?UTF-8?q?test(losses):=20de-flake=20test=5Fcross=5Fe?= =?UTF-8?q?ntropy=5Fqwen3=5Fvocab=20(batch=202=20=E2=86=92=2064)=20(#79)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem Main CI intermittently fails on a single Rust unit test (most recently on the PR #78 merge commit, run `28209793148`): ``` FAILED: nn::losses_test::tests::test_cross_entropy_qwen3_vocab crates/mlx-core/src/nn/losses_test.rs:630 "Loss should be > 10.0, got 9.881153" (1868 passed, 1 failed) ``` It is **unrelated to whatever PR happens to be merging** — the test was last touched in #12 and only `convert.rs` changed in #78. It is a pre-existing flaky test. ## Root cause The test builds random logits/targets with **no seed** and asserts a tight band on the mean-reduced loss: ```rust logits = random_normal([2, 151936], 0,1, None) // None is the DTYPE arg, not a seed targets = randint([2], 0, 151936) loss = mean_over_batch( logsumexp(logits) − logits[target] ) // ≈ log(151936) ≈ 11.93 assert!(10.0 < loss < 15.0) ``` With `batch_size = 2`, the loss is `≈ 12.43 − N(0, 0.71)` — a high random target logit occasionally drags the mean below `10.0` (the observed `9.88` is a ~3.5σ draw). ## Fix Bump `batch_size` 2 → 64. The mean-reduction then concentrates the loss at `log(V)`, shrinking the spread from ±0.71 to ±0.13. Vocab is unchanged, so the large-vocab **chunking path is still exercised**. ### Verification (2,000,000-draw simulation of the exact loss model) | batch | loss | min | P(loss<10) | P(loss>15) | |------:|------|----:|-----------:|-----------:| | 2 | 12.43 ± 0.71 | 9.01 | **0.030%** | 0.015% | | 64 | 12.43 ± 0.13 | 11.82 | **0** | 0 | At batch=64 the loss stays in `[11.8, 13.0]` across 2M draws — ~15σ from the `10.0` bound. (`logsumexp` constant measured = 12.4313, matching `ln(V)+0.5`.) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- > [!NOTE] > **Low Risk** > Test-only change with no production or loss-implementation edits; slightly more work per test run from a larger batch. > > **Overview** > Stabilizes **`test_cross_entropy_qwen3_vocab`** in `losses_test.rs` by raising **`batch_size` from 2 to 64** while keeping the Qwen3-sized vocab (151936) and the same `(10.0, 15.0)` loss band. > > The test still uses unseeded random logits/targets; with a tiny batch, a lucky target logit could drag the mean cross-entropy below 10.0 and fail CI. A larger batch tightens the mean-reduced loss around `log(vocab)` so the assertion is reliable without changing what the test exercises (large-vocab / chunked cross-entropy path). > > Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 0225f735edf67e1535eb4c1bcc74fc0a2f55b12f. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). Co-authored-by: Claude Opus 4.8 --- crates/mlx-core/src/nn/losses_test.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/mlx-core/src/nn/losses_test.rs b/crates/mlx-core/src/nn/losses_test.rs index 0c1518ef5..d89dbcb49 100644 --- a/crates/mlx-core/src/nn/losses_test.rs +++ b/crates/mlx-core/src/nn/losses_test.rs @@ -614,7 +614,10 @@ mod tests { #[test] fn test_cross_entropy_qwen3_vocab() { // Full Qwen3 vocabulary size - let batch_size = 2; + // Large batch so the mean-reduced loss concentrates near log(vocab): + // random_normal/randint are unseeded, and batch=2 was flaky — one high + // random target logit could pull the mean below the 10.0 lower bound. + let batch_size = 64; let vocab_size = 151936; let logits = From 855b91ccdf5d190d454d2b70b0cc1a0e94155556 Mon Sep 17 00:00:00 2001 From: LongYinan Date: Sat, 27 Jun 2026 10:53:27 +0800 Subject: [PATCH 5/8] fix(qwen3.5-VL): correct image inference (interleaved M-RoPE, patch-embed, compressed-position decode + delta lifetime) (#80) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes **Qwen3.5-VL image inference** for both the `qwen3_5` (dense) and `qwen3_5_moe` families. Before this branch, an image turn ran end-to-end but produced garbage descriptions — the model could not read text and hallucinated unrelated subjects (e.g. described a bank-reconciliation table as "dark grey stone / white fabric"). Text inference and `mlx convert` were unaffected; only the visual-feature path was wrong. Found via `ornith-1.0-35b` (a Qwen3.5-VL-MoE-35B-A3B post-train). Root-caused by numeric bisection against the reference `mlx-vlm` implementation. ## Root causes & fixes (all verified vs `mlx-vlm`) 1. **Interleaved multimodal RoPE for image tokens** (`fb95c6d8`) Qwen3.5-VL uses the **interleaved** (stride-3 per-freq) M-RoPE selector, not the PaddleOCR **sectioned/contiguous** one. Text tokens (`t==h==w`) are identical either way — which is exactly why text always worked while images (`t≠h≠w`) got the wrong rotation axis on ~17% of frequencies, every attention layer, destroying the 2D positional structure. 2. **Patch embedding: sum Conv3d temporal slices + load `patch_embed.proj.bias`** (`6a889bdb`) The patch embed previously used only the first temporal slice and hard-coded the conv bias to `None`. 3. **Decode rotates at the compressed M-RoPE position** (`b5c4420e`) Image prefill compresses ~754 placeholder tokens into ~29 M-RoPE positions, so `rope_deltas = max_position + 1 − seq_len` (≈ −726). Decode must rotate the query at `physical_slot + rope_deltas` while K/V still writes at the physical slot. Threaded a `rope_position_offset: i32` through `forward_paged` / `decoder_layer` / `paged_forward` (dense + MoE share one `forward_paged`); the negative delta is carried on `VisionMerge.rope_deltas` and stored in `cached_rope_deltas` at VLM prefill. The physical position is cast `u32 → i32` **before** the negative add to avoid underflow. 4. **rope-delta lifetime gate** (`f7ae00ca`) Found by adversarial review of (3). The cross-turn delta was reset only when `cached_prefix_len == 0`, but the paged-turn planner also yields `cached_prefix_len > 0` with `continued_live_prefix == false` on a **non-live prefix-cache hit**. Because the model instance is shared across all sessions, a stale negative delta from a prior image turn could then leak into an unrelated **text-only** request that merely shares a cached text prefix — rotating that text at `physical + stale_delta` → garbage. Factored the decision into `rope_delta_for_paged_turn(cached_rope_deltas, continued_live_prefix)` (keep the delta iff it is a live image continuation, else clear) and wired it through all six paged reset gates (dense + MoE × sync / stream / engine). This is safe because image requests prefill with `skip_lookup` and never publish a hashable text stream that collides with their expanded-placeholder blocks, so every non-live hit restores only pure-text prefix blocks (delta 0); only a live continuation re-attends the image's compressed-position K/V. Added three model-free lifecycle regression tests. 5. **e2e correctness gate** (`ab044b88`) `qwen3_5_moe_vl_reads_document_text` and `qwen3_5_vl_image_chat.rs` (dense) — `#[ignore]`, env-gated (`MLX_TEST_QWEN35MOE_VL_MODEL_PATH` + `MLX_TEST_VLM_IMAGE_PATH`), asserting the model reads ≥2 `DOC_KEYWORDS` from `examples/ocr.png`. These are the ground-truth gates for VL image inference. ### Note on the addmm commit (`12e89b3a` + `8e6d69d9`) `12e89b3a` replaced the fused `mlx_array_addmm` primitive with an explicit `matmul + add`, originally attributed to a bug in `mlx::core::addmm`. **That premise was wrong** — PyPI MLX's `addmm` applies the `C` term correctly (maxdiff 0) and the FFI wrapper passes its arguments correctly. The real cause was a corrupt local metallib that miscompiled the fused GEMM kernels (the same bad build also miscompiled the NAX gemm). The explicit form is kept as robustness against this project's documented non-deterministic metallib corruption — it is correctness-equivalent and vision/bias-only so the perf cost is negligible — and the `nn::linear` C-application tests double as a build canary. `8e6d69d9` corrects the misleading comment so it no longer claims an mlx source bug. ## Validation - **e2e READS-DOC** on the mxfp8-converted `ornith-1.0-35b` checkpoint: the model reads the document, matching all 7 keywords (`reconciliation, bank, council, trunch, october, 2019, balance`). - `nn::linear` addmm/bias tests, `paged_forward` rope-offset + delta-lifetime tests, and the broader rope/m-rope/delta unit sweep (70 tests) pass. - `cargo clippy -p mlx-core --all-targets -D warnings` clean; `cargo fmt` clean. - Adversarial review of the branch: **approve, no material findings**. > The full `cargo test -p mlx-core --lib` (debug) shows pre-existing Metal-f32 > failures (banded-attn / attention-vjp) plus a few cross-family numerical tests > that fail only against the corrupted local **debug** metallib; all of them pass > in release against a correctly-built metallib (and reproduce on `main`), so the > branch introduces no new failures. ## Test plan ```bash # unit (release, correct metallib) cargo test --release -p mlx-core --lib -- nn::linear paged_forward rope delta # e2e image-correctness (env-gated, sequential — large models) MLX_TEST_QWEN35MOE_VL_MODEL_PATH= \ MLX_TEST_VLM_IMAGE_PATH=$PWD/examples/ocr.png \ cargo test --release -p mlx-core --test qwen3_5_moe_vl_image_chat -- \ --ignored --nocapture qwen3_5_moe_vl_reads_document_text ``` 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- > [!NOTE] > **High Risk** > Changes core attention RoPE, vision weights, and shared paged inference state across dense/MoE VL; incorrect delta lifetime or offset math would corrupt multi-turn or cached-prefix text, though extensive unit and env-gated e2e tests mitigate this. > > **Overview** > Fixes **Qwen3.5-VL image inference** (dense and MoE) by aligning vision, RoPE, and paged decode with `mlx-vlm`. > > **Interleaved M-RoPE** — Adds `apply_multimodal_rotary_pos_emb_interleaved` and switches Qwen3.5 attention from the PaddleOCR sectioned apply so image tokens get stride-3 per-frequency axis selection; text-only paths stay unchanged via invariance tests. > > **Vision tower** — `addmm` is implemented as explicit `matmul` + scaled add (avoids local metallib fused-GEMM corruption that dropped biases). Patch embed loads optional conv bias and collapses Conv3d weights by summing temporal slices instead of taking slice 0. > > **Compressed M-RoPE decode** — `VisionMerge` carries `rope_deltas`; `get_rope_index` uses the global max over t/h/w axes. Paged prefill/decode/MTP thread `cached_rope_deltas` through `paged_rope_offset` and `rope_position_offset` so rotation uses compressed positions while KV stays at physical slots. `rope_delta_for_paged_turn` keeps the delta only on `continued_live_prefix` so stale image deltas do not leak into unrelated text prefix-cache hits. > > **Tests** — Unit tests for interleaved RoPE, rope offset/delta lifecycle, patch embed, linear bias; env-gated e2e `reads_document_text` gates for dense and MoE VL. > > Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 8e6d69d9e3ca6b1b4c2ea9b5dc8e1d4d925e250c. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). --------- Co-authored-by: Claude Opus 4.8 --- crates/mlx-core/src/array/ops.rs | 34 ++- crates/mlx-core/src/engine/vision.rs | 6 + .../src/models/paddleocr_vl/language.rs | 213 ++++++++++++++++++ .../mlx-core/src/models/paddleocr_vl/mod.rs | 4 +- .../src/models/paddleocr_vl/vision.rs | 2 +- .../mlx-core/src/models/qwen3_5/attention.rs | 26 ++- .../src/models/qwen3_5/decoder_layer.rs | 14 +- crates/mlx-core/src/models/qwen3_5/model.rs | 147 +++++++++++- .../src/models/qwen3_5/paged_forward.rs | 187 +++++++++++++++ .../src/models/qwen3_5/persistence.rs | 72 +++++- crates/mlx-core/src/models/qwen3_5/vision.rs | 15 +- .../src/models/qwen3_5_moe/decoder_layer.rs | 3 + .../mlx-core/src/models/qwen3_5_moe/model.rs | 53 ++++- .../src/models/qwen3_5_moe/paged_forward.rs | 35 +++ crates/mlx-core/src/nn/linear.rs | 73 ++++++ crates/mlx-core/src/vision/embeddings.rs | 64 +++++- .../tests/qwen3_5_moe_vl_image_chat.rs | 88 ++++++++ .../mlx-core/tests/qwen3_5_vl_image_chat.rs | 88 ++++++++ 18 files changed, 1070 insertions(+), 54 deletions(-) diff --git a/crates/mlx-core/src/array/ops.rs b/crates/mlx-core/src/array/ops.rs index 1b9716291..ce498fde1 100644 --- a/crates/mlx-core/src/array/ops.rs +++ b/crates/mlx-core/src/array/ops.rs @@ -103,9 +103,20 @@ impl MxArray { MxArray::from_handle(handle, "array_matmul") } - /// Fused matrix multiply-add: D = beta * C + alpha * (self @ B) - /// where self is A. More efficient than separate matmul and add operations. - /// Default: alpha=1.0, beta=1.0, giving D = C + (self @ B) + /// Matrix multiply-add: D = beta * C + alpha * (self @ B), where self is A. + /// Default: alpha=1.0, beta=1.0, giving D = C + (self @ B). + /// + /// Computed explicitly (matmul, optional alpha scale, then add beta*C) + /// rather than via the fused `mlx::core::addmm` primitive. The fused + /// primitive is correct on a well-formed metallib, but this project's local + /// release build is known to non-deterministically miscompile fused GEMM + /// kernels (see the metallib-corruption notes), which manifested as the + /// fused addmm dropping `beta*C` and corrupting every biased linear — most + /// visibly the vision tower (`qkv`, `proj`, `fc1`, `fc2`, merger all carry a + /// bias; bias-free LM/MoE linears pass a zero `C` and were unaffected). The + /// explicit form keeps the `C` term robust to that build hazard; the + /// `nn::linear` unit tests assert it applies a non-zero `C`, so they double + /// as a canary if a future build corrupts the matmul kernel too. #[napi] pub fn addmm( &self, @@ -114,11 +125,18 @@ impl MxArray { alpha: Option, beta: Option, ) -> Result { - let alpha = alpha.unwrap_or(1.0) as f32; - let beta = beta.unwrap_or(1.0) as f32; - let handle = - unsafe { sys::mlx_array_addmm(c.handle.0, self.handle.0, b.handle.0, alpha, beta) }; - MxArray::from_handle(handle, "array_addmm") + let alpha = alpha.unwrap_or(1.0); + let beta = beta.unwrap_or(1.0); + + let mut mm = self.matmul(b)?; + if alpha != 1.0 { + mm = mm.mul_scalar(alpha)?; + } + if beta != 1.0 { + mm.add(&c.mul_scalar(beta)?) + } else { + mm.add(c) + } } /// Indexed matrix multiply for MoE expert selection. diff --git a/crates/mlx-core/src/engine/vision.rs b/crates/mlx-core/src/engine/vision.rs index 9c88ac96c..720076f01 100644 --- a/crates/mlx-core/src/engine/vision.rs +++ b/crates/mlx-core/src/engine/vision.rs @@ -19,4 +19,10 @@ pub(crate) struct VisionMerge { /// M-RoPE position ids for the mixed image+text sequence. Shape /// `[3, 1, seq_len]` (the temporal/height/width axes), i32. pub position_ids: MxArray, + /// `max_position + 1 - seq_len` for this image prefill. Image runs + /// compress their placeholder tokens into fewer M-RoPE positions, so this + /// is NEGATIVE; it is the per-session `cached_rope_deltas` that later + /// decode/warm-continuation steps add to the physical KV slot to recover + /// the compressed rotation position. Text-only prefills compute 0. + pub rope_deltas: i64, } diff --git a/crates/mlx-core/src/models/paddleocr_vl/language.rs b/crates/mlx-core/src/models/paddleocr_vl/language.rs index aebfe997b..21ae430ba 100644 --- a/crates/mlx-core/src/models/paddleocr_vl/language.rs +++ b/crates/mlx-core/src/models/paddleocr_vl/language.rs @@ -288,6 +288,118 @@ pub fn apply_multimodal_rotary_pos_emb( Ok((q_out, k_out)) } +/// Apply INTERLEAVED multimodal rotary position embedding to Q and K (internal). +/// +/// Used by Qwen3.5-VL (`qwen3_5` / `qwen3_5_moe`). Unlike PaddleOCR-VL, whose +/// multimodal RoPE assigns the head_dim to spatial axes in CONTIGUOUS chunks +/// (see [`apply_multimodal_rotary_pos_emb`]), Qwen3.5-VL assigns each inv_freq +/// index to a spatial axis with a STRIDE-3 INTERLEAVE. This mirrors mlx-vlm's +/// `_interleaved_position_selector` (rope_utils.py): +/// selector[idx] = 1 (height) for idx in 1, 4, 7, … up to `mrope_section[1] * 3` +/// selector[idx] = 2 (width) for idx in 2, 5, 8, … up to `mrope_section[2] * 3` +/// selector[idx] = 0 (temporal) otherwise +/// computed over the `half_dim` inv_freq axis, then mirrored across the doubled +/// cos/sin (`emb = concat([freqs, freqs])`). +/// +/// The per-frequency axis selection is performed with a `take_along_axis` gather +/// over axis 0 (the `[temporal, height, width]` axis) of the doubled cos/sin, +/// followed by the same `rotate_half` application as the sectioned path. +/// +/// For TEXT tokens (temporal == height == width) every axis row of cos/sin is +/// bit-identical, so any selector picks identical values — this produces output +/// bit-identical to the sectioned [`apply_multimodal_rotary_pos_emb`]. See +/// `test_interleaved_text_invariance`. +/// +/// `cos`/`sin` shape: `[3, batch, seq_len, head_dim]` (axis 0 = t/h/w). +/// `q`/`k` shape: `[batch, heads, seq_len, q_head_dim]` where `q_head_dim >= +/// head_dim` (the trailing `q_head_dim - head_dim` dims pass through unrotated, +/// matching qwen3_5's partial-rotary factor). +/// +/// Note: Internal implementation detail, not exposed to TypeScript. +pub fn apply_multimodal_rotary_pos_emb_interleaved( + q: &MxArray, + k: &MxArray, + cos: &MxArray, + sin: &MxArray, + mrope_section: Vec, +) -> Result<(MxArray, MxArray)> { + let cos_shape = cos.shape()?; // 1 FFI call — cache and reuse below + let batch_size = cos_shape[1]; + let seq_len = cos_shape[2]; + let head_dim = cos_shape[3]; + let half = head_dim / 2; + let half_usize = half as usize; + + // Build the per-frequency axis selector over the half_dim inv_freq axis, + // matching mlx-vlm's `_interleaved_position_selector`, then mirror it across + // the doubled cos/sin (emb = concat([freqs, freqs]) => sel[j] = sel[j % half]). + let mut sel_half = vec![0i32; half_usize]; + for (dim, offset) in [(1usize, 1usize), (2usize, 2usize)] { + let limit = std::cmp::min(mrope_section[dim] as usize * 3, half_usize); + let mut idx = offset; + while idx < limit { + sel_half[idx] = dim as i32; + idx += 3; + } + } + let mut sel_doubled = vec![0i32; head_dim as usize]; + for (j, slot) in sel_doubled.iter_mut().enumerate() { + *slot = sel_half[j % half_usize]; + } + + // Gather the selected axis per frequency from cos/sin [3, batch, seq, head_dim]. + // indices [1, 1, 1, head_dim] -> broadcast to [1, batch, seq, head_dim]. + let indices = MxArray::from_int32(&sel_doubled, &[1, 1, 1, head_dim])?; + let indices = MxArray::broadcast_to(&indices, &[1, batch_size, seq_len, head_dim])?; + + let cos_sel = cos.take_along_axis(&indices, 0)?; // [1, batch, seq, head_dim] + let sin_sel = sin.take_along_axis(&indices, 0)?; + + // [1, batch, seq, head_dim] -> [batch, 1, seq, head_dim] (flat order preserved). + let cos_final = cos_sel.reshape(&[batch_size, 1, seq_len, head_dim])?; + let sin_final = sin_sel.reshape(&[batch_size, 1, seq_len, head_dim])?; + + // Standard rotate_half application (identical to the sectioned path tail). + let rotary_dim = head_dim; + let q_shape = q.shape()?; // 1 FFI call + let q_dim = q_shape[3]; + let q_ndim = q_shape.len(); + + let q_rot = q.slice_axis(3, 0, rotary_dim)?; + let q_pass = if rotary_dim < q_dim { + Some(q.slice_axis(3, rotary_dim, q_dim)?) + } else { + None + }; + + let k_rot = k.slice_axis(3, 0, rotary_dim)?; + let k_pass = if rotary_dim < q_dim { + Some(k.slice_axis(3, rotary_dim, q_dim)?) + } else { + None + }; + + let q_rotated = rotate_half(&q_rot, q_ndim, rotary_dim)?; + let k_rotated = rotate_half(&k_rot, q_ndim, rotary_dim)?; + + let q_embed = q_rot.mul(&cos_final)?.add(&q_rotated.mul(&sin_final)?)?; + let k_embed = k_rot.mul(&cos_final)?.add(&k_rotated.mul(&sin_final)?)?; + + let q_out = if let Some(q_pass) = q_pass { + MxArray::concatenate_many(vec![&q_embed, &q_pass], Some(-1))? + } else { + q_embed + }; + + let k_out = if let Some(k_pass) = k_pass { + MxArray::concatenate_many(vec![&k_embed, &k_pass], Some(-1))? + } else { + k_embed + }; + + Ok((q_out, k_out)) +} + /// PaddleOCR Attention with mRoPE (internal) /// /// Note: This is an internal implementation detail used by PaddleOCRDecoderLayer. @@ -1072,6 +1184,107 @@ mod tests { assert!(layer.is_ok()); } + #[test] + fn test_interleaved_selection_axis_assignment() { + // Interleaved selector correctness (PRIMARY qwen3_5-VL fix). + // + // rotary head_dim = 12 -> half_dim = 6; mrope_section = [3, 2, 1]. + // mlx-vlm _interleaved_position_selector([3,2,1], freq_dim=6): + // dim=1 (height) offset 1: idx 1, 4 (range(1, min(2*3,6)=6, 3)) -> 1 + // dim=2 (width) offset 2: idx 2 (range(2, min(1*3,6)=3, 3)) -> 2 + // everything else -> 0 + // => sel_half = [0, 1, 2, 0, 1, 0] + // Doubled across emb=concat([f,f]): sel_doubled = sel_half ++ sel_half. + // + // Mark each axis row with its index (cos[axis, .., j] = axis), set sin = 0 + // and q = ones, so q_out[j] = q_rot * cos_final = cos_final = sel_doubled[j]. + let head_dim = 12i64; + let mut cos_data = Vec::with_capacity(3 * head_dim as usize); + for axis in 0..3 { + for _ in 0..head_dim { + cos_data.push(axis as f32); + } + } + let cos = MxArray::from_float32(&cos_data, &[3, 1, 1, head_dim]).unwrap(); + let sin = MxArray::zeros(&[3, 1, 1, head_dim], Some(DType::Float32)).unwrap(); + let q = MxArray::ones(&[1, 1, 1, head_dim], Some(DType::Float32)).unwrap(); + let k = MxArray::ones(&[1, 1, 1, head_dim], Some(DType::Float32)).unwrap(); + + let (q_out, _k_out) = + apply_multimodal_rotary_pos_emb_interleaved(&q, &k, &cos, &sin, vec![3, 2, 1]).unwrap(); + + let expected: [f32; 12] = [ + 0.0, 1.0, 2.0, 0.0, 1.0, 0.0, // sel_half + 0.0, 1.0, 2.0, 0.0, 1.0, 0.0, // mirrored + ]; + let got = q_out.to_float32().unwrap(); + assert_eq!(got.as_ref(), &expected[..]); + } + + #[test] + fn test_interleaved_text_invariance() { + // Hard gate / safety net: for TEXT tokens (temporal == height == width) + // the interleaved apply MUST be bit-identical to the existing sectioned + // apply, proving the text path cannot regress. + // + // Realistic qwen3_5 rotary geometry: rope_dims = 64 -> half_dim = 32, + // mrope_section = [11, 11, 10] (sum 32). cos/sin are built from a real + // MultimodalRoPE forward over position_ids whose three axes are EQUAL. + let rope_dims = 64i32; + let mrope = + MultimodalRoPE::new(rope_dims, 4096, Some(100_000.0), vec![11, 11, 10]).unwrap(); + + let seq_len = 5i64; + // position_ids [3, 1, seq] with all three (t, h, w) rows equal. + let mut pos_data = Vec::with_capacity(3 * seq_len as usize); + for _ in 0..3 { + for p in 0..seq_len { + pos_data.push(p as f32); + } + } + let pos = MxArray::from_float32(&pos_data, &[3, 1, seq_len]).unwrap(); + + // x supplies only the target dtype. + let x = MxArray::zeros(&[1, seq_len, rope_dims as i64], Some(DType::Float32)).unwrap(); + let (cos, sin) = mrope.forward(&x, &pos).unwrap(); + + // Q/K head_dim = 96 > rope_dims = 64 to exercise the partial-rotary + // pass-through path that qwen3_5 uses. + let head_dim = 96i64; + let heads = 2i64; + let q = MxArray::random_uniform( + &[1, heads, seq_len, head_dim], + 0.0, + 1.0, + Some(DType::Float32), + ) + .unwrap(); + let k = MxArray::random_uniform( + &[1, heads, seq_len, head_dim], + 0.0, + 1.0, + Some(DType::Float32), + ) + .unwrap(); + + let (q_sec, k_sec) = + apply_multimodal_rotary_pos_emb(&q, &k, &cos, &sin, vec![11, 11, 10]).unwrap(); + let (q_int, k_int) = + apply_multimodal_rotary_pos_emb_interleaved(&q, &k, &cos, &sin, vec![11, 11, 10]) + .unwrap(); + + assert_eq!( + q_sec.to_float32().unwrap().as_ref(), + q_int.to_float32().unwrap().as_ref(), + "interleaved Q must equal sectioned Q for text tokens (t==h==w)" + ); + assert_eq!( + k_sec.to_float32().unwrap().as_ref(), + k_int.to_float32().unwrap().as_ref(), + "interleaved K must equal sectioned K for text tokens (t==h==w)" + ); + } + #[test] fn test_attention_creation() { // Test creating PaddleOCRAttention diff --git a/crates/mlx-core/src/models/paddleocr_vl/mod.rs b/crates/mlx-core/src/models/paddleocr_vl/mod.rs index 52c87c6dd..386f98ac2 100644 --- a/crates/mlx-core/src/models/paddleocr_vl/mod.rs +++ b/crates/mlx-core/src/models/paddleocr_vl/mod.rs @@ -13,7 +13,9 @@ pub mod processing; pub mod vision; // Re-export public items used cross-module -pub use language::{MultimodalRoPE, apply_multimodal_rotary_pos_emb}; +pub use language::{ + MultimodalRoPE, apply_multimodal_rotary_pos_emb, apply_multimodal_rotary_pos_emb_interleaved, +}; pub use persistence::load_paddleocr_vl_weights; pub use processing::{ ImageProcessorConfig, ProcessedImage, ProcessedImages, aggregate_processed_images, smart_resize, diff --git a/crates/mlx-core/src/models/paddleocr_vl/vision.rs b/crates/mlx-core/src/models/paddleocr_vl/vision.rs index 7fee0fd50..a1aa4ad79 100644 --- a/crates/mlx-core/src/models/paddleocr_vl/vision.rs +++ b/crates/mlx-core/src/models/paddleocr_vl/vision.rs @@ -37,7 +37,7 @@ impl PaddleOCRVisionEmbeddings { patch_weight: &MxArray, position_weight: &MxArray, ) -> Result { - let patch_embedding = PatchEmbedding::new(patch_size, patch_weight)?; + let patch_embedding = PatchEmbedding::new(patch_size, patch_weight, None)?; let num_patches = (image_size / patch_size).pow(2); let position_embedding = diff --git a/crates/mlx-core/src/models/qwen3_5/attention.rs b/crates/mlx-core/src/models/qwen3_5/attention.rs index d14f686bc..426e2db27 100644 --- a/crates/mlx-core/src/models/qwen3_5/attention.rs +++ b/crates/mlx-core/src/models/qwen3_5/attention.rs @@ -7,7 +7,9 @@ use crate::array::mask::create_causal_mask; use crate::inference_trace::{ elapsed_ms, enabled as inference_trace_enabled, write as write_inference_trace, }; -use crate::models::paddleocr_vl::language::{MultimodalRoPE, apply_multimodal_rotary_pos_emb}; +use crate::models::paddleocr_vl::language::{ + MultimodalRoPE, apply_multimodal_rotary_pos_emb_interleaved, +}; use crate::nn::{Activations, Linear, RMSNorm, RoPE}; use crate::transformer::KVCache; use crate::transformer::paged_kv_cache_adapter::PagedKVCacheAdapter; @@ -187,12 +189,14 @@ impl Qwen3_5Attention { // Apply RoPE: either M-RoPE (VLM) or standard scalar offset (text-only) let (queries, keys) = if let (Some(pos_ids), Some(mrope)) = (position_ids, &self.mrope) { - // M-RoPE: compute cos/sin from 3D position IDs [3, B, T] + // M-RoPE: compute cos/sin from 3D position IDs [3, B, T]. + // Qwen3.5-VL uses the INTERLEAVED (stride-3) per-frequency axis + // selector, NOT PaddleOCR-VL's contiguous-chunk (sectioned) one. let (cos, sin) = mrope.forward(&queries, pos_ids)?; - // Transpose to [B, H, T, D] for apply_multimodal_rotary_pos_emb + // Transpose to [B, H, T, D] for the rotary apply. let q_t = queries.transpose(Some(&[0, 2, 1, 3]))?; let k_t = keys.transpose(Some(&[0, 2, 1, 3]))?; - let (q_out, k_out) = apply_multimodal_rotary_pos_emb( + let (q_out, k_out) = apply_multimodal_rotary_pos_emb_interleaved( &q_t, &k_t, &cos, @@ -290,6 +294,7 @@ impl Qwen3_5Attention { cached_prefix_len: u32, is_prefill: bool, position_ids: Option<&MxArray>, + rope_position_offset: i32, ) -> Result { let batch = x.shape_at(0)?; let seq_len = x.shape_at(1)?; @@ -335,10 +340,12 @@ impl Qwen3_5Attention { // the `None` (text-only) arm matches the flat path's scalar-offset // behaviour. let (queries, keys) = if let (Some(pos_ids), Some(mrope)) = (position_ids, &self.mrope) { + // Qwen3.5-VL uses the INTERLEAVED (stride-3) per-frequency axis + // selector, NOT PaddleOCR-VL's contiguous-chunk (sectioned) one. let (cos, sin) = mrope.forward(&queries, pos_ids)?; let q_t = queries.transpose(Some(&[0, 2, 1, 3]))?; let k_t = keys.transpose(Some(&[0, 2, 1, 3]))?; - let (q_out, k_out) = apply_multimodal_rotary_pos_emb( + let (q_out, k_out) = apply_multimodal_rotary_pos_emb_interleaved( &q_t, &k_t, &cos, @@ -349,7 +356,14 @@ impl Qwen3_5Attention { let k_out = k_out.transpose(Some(&[0, 2, 1, 3]))?; (q_out, k_out) } else { - let rope_offset = first_logical_position as i32; + // Scalar-offset RoPE. `rope_position_offset` decouples the + // rotation position from the physical KV slot: a turn that + // warm-continues an image prefill rotates at the compressed + // M-RoPE position (physical slot + a negative cross-turn delta) + // while K/V still writes at the physical slot below. Text turns + // pass `rope_position_offset == first_logical_position as i32`, + // so this stays byte-identical to the prior behaviour. + let rope_offset = rope_position_offset; let queries = self.rope.forward(&queries, Some(rope_offset))?; let keys = self.rope.forward(&keys, Some(rope_offset))?; (queries, keys) diff --git a/crates/mlx-core/src/models/qwen3_5/decoder_layer.rs b/crates/mlx-core/src/models/qwen3_5/decoder_layer.rs index fee166767..bdb4e02e4 100644 --- a/crates/mlx-core/src/models/qwen3_5/decoder_layer.rs +++ b/crates/mlx-core/src/models/qwen3_5/decoder_layer.rs @@ -275,6 +275,7 @@ impl DecoderLayer { flat_cache: Option<&mut Qwen3_5LayerCache>, position_ids: Option<&MxArray>, use_kernel: bool, + rope_position_offset: i32, ) -> Result { match kind { Qwen3_5LayerKind::Linear => { @@ -283,6 +284,7 @@ impl DecoderLayer { let _ = first_logical_position; let _ = cached_prefix_len; let _ = is_prefill; + let _ = rope_position_offset; if !matches!(self.attn, AttentionType::Linear(_)) { return Err(Error::from_reason( "Qwen3_5DecoderLayer::forward_paged_or_flat: kind=Linear applied to a \ @@ -316,6 +318,7 @@ impl DecoderLayer { cached_prefix_len, is_prefill, position_ids, + rope_position_offset, )?; // Residual. let h = x.add(&attn_out)?; @@ -349,6 +352,7 @@ impl DecoderLayer { is_prefill: bool, flat_cache: Option<&mut Qwen3_5LayerCache>, tape_sink: Option<&mut Option>, + rope_position_offset: i32, ) -> Result { match kind { Qwen3_5LayerKind::Linear => { @@ -356,6 +360,7 @@ impl DecoderLayer { let _ = first_logical_position; let _ = cached_prefix_len; let _ = is_prefill; + let _ = rope_position_offset; if !matches!(self.attn, AttentionType::Linear(_)) { return Err(Error::from_reason( "Qwen3_5DecoderLayer::forward_paged_or_flat_with_tape: kind=Linear applied \ @@ -380,8 +385,12 @@ impl DecoderLayer { } }; let normed = self.input_layernorm.forward(x)?; - // MTP tape forwards are text-only (image-bearing MTP+paged turns - // are rejected upstream), so the scalar-offset RoPE path is used. + // MTP tape forwards always carry M-RoPE positions as `None` + // (the K+1 verify ids are re-embedded, not image features), so + // RoPE takes the scalar-offset path. `rope_position_offset` + // still carries any cross-turn delta: a text turn that + // warm-continues an image prefill runs paged MTP and must + // rotate at the compressed position, not the physical slot. let attn_out = attn.forward_paged( &normed, adapter, @@ -390,6 +399,7 @@ impl DecoderLayer { cached_prefix_len, is_prefill, None, + rope_position_offset, )?; let h = x.add(&attn_out)?; let normed = self.post_attention_layernorm.forward(&h)?; diff --git a/crates/mlx-core/src/models/qwen3_5/model.rs b/crates/mlx-core/src/models/qwen3_5/model.rs index ff9031dae..7e2f04013 100644 --- a/crates/mlx-core/src/models/qwen3_5/model.rs +++ b/crates/mlx-core/src/models/qwen3_5/model.rs @@ -2419,7 +2419,15 @@ impl Qwen35Inner { let gdn_prefix_already_primed = gdn_prefix_preparation.already_primed; self.cached_token_history.clear(); self.cached_image_key = None; - self.cached_rope_deltas = None; + // Carry the cross-turn M-RoPE delta only when this turn extends the live + // image sequence (continued_live_prefix). A cold start OR a non-live + // prefix-cache hit (cached_prefix_len > 0 but not a live continuation) + // can only restore pure-text prefix blocks, so a stale image delta is + // dropped and the text suffix rotates at the raw physical slot. + self.cached_rope_deltas = super::paged_forward::rope_delta_for_paged_turn( + self.cached_rope_deltas, + continued_live_prefix, + ); let suffix_len = prompt_token_count .checked_sub(cached_prefix_len) @@ -2589,6 +2597,10 @@ impl Qwen35Inner { let adapter = self.paged_adapter.as_mut().ok_or_else(|| { Error::from_reason("paged_turn_sync_core_inner: paged_adapter dropped") })?; + // Cross-turn M-RoPE delta (0 unless this text turn warm-continues + // an image prefill). Feeds the scalar-offset RoPE so the suffix + // keys stay aligned with the compressed-position image keys. + let rope_deltas = self.cached_rope_deltas.unwrap_or(0); if want_prompt_hidden { let (logits, ph) = super::paged_forward::run_paged_prefill_chunk_with_hidden( tokens, @@ -2604,6 +2616,7 @@ impl Qwen35Inner { &layer_kinds, adapter, Some(mtp_prompt_history.keep_tokens), + rope_deltas, )?; prompt_hidden = Some(ph); logits @@ -2621,6 +2634,7 @@ impl Qwen35Inner { &embedding_weight, &layer_kinds, adapter, + rope_deltas, )? } }; @@ -2778,6 +2792,7 @@ impl Qwen35Inner { &embedding_weight, &layer_kinds, adapter, + self.cached_rope_deltas.unwrap_or(0), )?; logits.squeeze(Some(&[1]))? }; @@ -2809,8 +2824,10 @@ impl Qwen35Inner { /// autoregressive decode loop. /// /// SINGLE-TURN ONLY: the adapter is cold-started (no cache-hit, no warm - /// continue) and decode uses the scalar-offset RoPE path (the physical - /// token count), matching the flat path's decode RoPE. This core runs plain + /// continue) and decode rotates at the physical token count plus the + /// cached M-RoPE delta (`cached_rope_deltas`), i.e. the compressed M-RoPE + /// position for image turns; text turns carry a delta of 0 and stay + /// byte-identical to the flat path's decode RoPE. This core runs plain /// autoregressive decode with no draft/verify; MTP weights are ignored, so /// an MTP-enabled session's image turns route here and decode as AR. #[allow(clippy::too_many_arguments)] @@ -2908,7 +2925,10 @@ impl Qwen35Inner { } self.cached_token_history.clear(); self.cached_image_key = None; - self.cached_rope_deltas = None; + // Store the image prefill's compressed-position delta so a later text + // warm-continuation rotates its queries at the same compressed M-RoPE + // positions the image keys were written with. + self.cached_rope_deltas = Some(merge.rope_deltas as i32); // Fresh per-layer caches (GDN linear slots + empty full-attention slots). let new_caches = (0..self.config.num_layers as usize) @@ -3010,6 +3030,7 @@ impl Qwen35Inner { &embedding_weight, &layer_kinds, adapter, + self.cached_rope_deltas.unwrap_or(0), )?; logits.squeeze(Some(&[1]))? }; @@ -3234,7 +3255,10 @@ impl Qwen35Inner { } self.cached_token_history.clear(); self.cached_image_key = None; - self.cached_rope_deltas = None; + // Store the image prefill's compressed-position delta so a later text + // warm-continuation rotates its queries at the same compressed M-RoPE + // positions the image keys were written with. + self.cached_rope_deltas = Some(merge.rope_deltas as i32); let new_caches = (0..self.config.num_layers as usize) .map(|i| { @@ -3370,6 +3394,7 @@ impl Qwen35Inner { &embedding_weight, &layer_kinds, adapter, + self.cached_rope_deltas.unwrap_or(0), )?; logits.squeeze(Some(&[1]))? }; @@ -3668,7 +3693,14 @@ impl Qwen35Inner { let gdn_prefix_state = gdn_prefix_preparation.state; self.cached_token_history.clear(); self.cached_image_key = None; - self.cached_rope_deltas = None; + // Carry the cross-turn M-RoPE delta only when this turn extends the live + // image sequence (continued_live_prefix); a cold start or a non-live + // prefix-cache hit (text-only prefix) drops a stale image delta so the + // text suffix prefill + decode rotate at the raw physical slot. + self.cached_rope_deltas = super::paged_forward::rope_delta_for_paged_turn( + self.cached_rope_deltas, + continued_live_prefix, + ); let suffix_len = prompt_token_count .checked_sub(cached_prefix_len) @@ -3905,6 +3937,9 @@ impl Qwen35Inner { let adapter = self.paged_adapter.as_mut().ok_or_else(|| { Error::from_reason("paged_turn_stream_core_inner: paged_adapter dropped") })?; + // Cross-turn M-RoPE delta (0 unless this text turn warm-continues + // an image prefill); feeds the scalar-offset RoPE for the suffix. + let rope_deltas = self.cached_rope_deltas.unwrap_or(0); if want_prompt_hidden { let (logits, ph) = super::paged_forward::run_paged_prefill_chunk_with_hidden( tokens, @@ -3920,6 +3955,7 @@ impl Qwen35Inner { &layer_kinds, adapter, Some(mtp_prompt_history.keep_tokens), + rope_deltas, )?; prompt_hidden = Some(ph); logits @@ -3937,6 +3973,7 @@ impl Qwen35Inner { &embedding_weight, &layer_kinds, adapter, + rope_deltas, )? } }; @@ -4115,6 +4152,7 @@ impl Qwen35Inner { &embedding_weight, &layer_kinds, adapter, + self.cached_rope_deltas.unwrap_or(0), )?; logits.squeeze(Some(&[1]))? }; @@ -6524,6 +6562,7 @@ impl DecodeStep for Qwen35PagedDecode<'_> { &embedding_weight, &layer_kinds, adapter, + self.inner.cached_rope_deltas.unwrap_or(0), )? .squeeze(Some(&[1]))? }; @@ -6690,11 +6729,18 @@ impl PagedBackend for Qwen35Inner { )?; let gdn_prefix_already_primed = gdn_prefix_preparation.already_primed; // Clear the per-turn session state here (history is re-set in - // `save_paged_history`; rope deltas + image key are reset because the - // paged path does not carry them across turns). + // `save_paged_history`; image key is reset because the paged path does + // not carry it across turns). The cross-turn M-RoPE delta is carried + // only when this turn extends the live image sequence + // (continued_live_prefix); a cold start or a non-live prefix-cache hit + // (text-only prefix) drops a stale image delta so the text suffix + // rotates at the raw physical slot. self.cached_token_history.clear(); self.cached_image_key = None; - self.cached_rope_deltas = None; + self.cached_rope_deltas = super::paged_forward::rope_delta_for_paged_turn( + self.cached_rope_deltas, + continued_live_prefix, + ); let suffix_len = total_budget.checked_sub(cached_prefix_len).ok_or_else(|| { Error::from_reason("prime_prefix_state: cached_prefix_len > total_prompt_tokens") @@ -6728,6 +6774,10 @@ impl PagedBackend for Qwen35Inner { }); let embed = self.embedding.clone(); let embedding_weight = embed.get_weight(); + // Cross-turn M-RoPE delta (0 unless this engine-driven text turn warm- + // continues an image prefill); aligns the suffix keys with the + // compressed-position image keys. + let rope_deltas = self.cached_rope_deltas.unwrap_or(0); let caches_ref = self .caches .as_mut() @@ -6749,6 +6799,7 @@ impl PagedBackend for Qwen35Inner { &embedding_weight, &layer_kinds, adapter, + rope_deltas, ) } @@ -7217,6 +7268,9 @@ impl MtpStepper for DenseMtpStepper<'_> { ids.eval(); let token_id = ids.item_at_int32(0)? as u32; let inner = &mut *self.inner; + // Cross-turn M-RoPE delta carried by a text turn that warm- + // continues an image prefill; 0 for pure-text sessions. + let rope_deltas = inner.cached_rope_deltas.unwrap_or(0); let caches = inner.caches.as_mut().ok_or_else(|| { Error::from_reason("eager paged MTP forward_with_hidden: caches is None") })?; @@ -7231,6 +7285,7 @@ impl MtpStepper for DenseMtpStepper<'_> { Some(&self.embedding_weight_t), &self.layer_kinds, adapter, + rope_deltas, )?; Ok((logits, hidden, true)) } @@ -7307,6 +7362,9 @@ impl MtpStepper for DenseMtpStepper<'_> { let id_slice: Vec = id_window.iter().take(depth + 1).copied().collect(); let verify_in = MxArray::from_int32(&id_slice, &[1, (depth + 1) as i64])?; let inner = &mut *self.inner; + // Cross-turn M-RoPE delta carried by a text turn that warm- + // continues an image prefill; 0 for pure-text sessions. + let rope_deltas = inner.cached_rope_deltas.unwrap_or(0); let caches = inner.caches.as_mut().ok_or_else(|| { Error::from_reason("eager paged MTP verify_step: caches is None") })?; @@ -7323,6 +7381,7 @@ impl MtpStepper for DenseMtpStepper<'_> { &self.layer_kinds, adapter, tape, + rope_deltas, ) } } @@ -9170,7 +9229,16 @@ pub(crate) fn get_rope_index( let position_ids = MxArray::stack(vec![&t_arr, &h_arr, &w_arr], Some(0))?; - let max_position = *all_position_ids[0].iter().max().unwrap_or(&0); + // Decode offset must reference the GLOBAL max M-RoPE position, i.e. the max + // over all three (t, h, w) axes — matching mlx-vlm's `llm_positions.max()`. + // For an image the spatial (h, w) axes exceed the temporal one, so an + // image-final prompt (no trailing text) would get a too-small delta if only + // axis 0 were considered. + let max_position = all_position_ids + .iter() + .flat_map(|axis| axis.iter().copied()) + .max() + .unwrap_or(0); let rope_deltas = max_position + 1 - seq_len; Ok((position_ids, rope_deltas)) @@ -9338,6 +9406,7 @@ pub(crate) fn vlm_prepare_vision_features( Ok(VisionMerge { inputs_embeds, position_ids, + rope_deltas, }) } @@ -9488,7 +9557,7 @@ mod rope_index_tests { .copied() .collect(); let (ids, grid) = mk_inputs(&tokens, &[(2, 4, 4)]); - let (pos, _) = get_rope_index(&ids, grid.as_ref(), 2, IMG).unwrap(); + let (pos, rope_deltas) = get_rope_index(&ids, grid.as_ref(), 2, IMG).unwrap(); let (t, h, w) = extract_positions(&pos); // Leading text assert_eq!(&t[..2], &[0, 1]); @@ -9499,6 +9568,61 @@ mod rope_index_tests { assert_eq!(&t[10..], &[4, 5]); assert_eq!(&h[10..], &[4, 5]); assert_eq!(&w[10..], &[4, 5]); + + // The image run compresses 8 placeholder tokens into 4 distinct + // temporal positions, so the running M-RoPE counter lags the physical + // sequence length: `rope_deltas = max_position + 1 - seq_len` MUST be + // negative. This is the per-session delta the paged decode/warm- + // continuation path adds to the physical KV slot to recover the + // compressed rotation position; previously it was dropped, leaving + // image-turn decode rotating ~|delta| positions too far ahead. + let max_position = *t.iter().max().unwrap() as i64; // temporal axis (axis 0) + let seq_len = tokens.len() as i64; + assert_eq!(rope_deltas, max_position + 1 - seq_len); + assert!( + rope_deltas < 0, + "image prefill must compress positions (rope_deltas={rope_deltas})" + ); + // 2 text + 8 image (compressed to positions 2..=3) + 2 text → max + // temporal position 5 over 12 tokens → delta = 5 + 1 - 12 = -6. + assert_eq!(rope_deltas, -6); + } + + #[test] + fn image_final_prompt_delta_uses_global_max_axis() { + // An image-FINAL prompt (no trailing text) exposes which axis feeds the + // decode delta: the spatial (h, w) axes outrun the temporal one, so the + // global max M-RoPE position lives on a spatial axis. The delta must use + // that global max (mlx-vlm `llm_positions.max()`), NOT the temporal axis + // alone — otherwise the first generated token rotates at a position + // INSIDE the image's spatial range instead of at global_max + 1. + let _g = mlx_lock().lock().unwrap(); + // 1 text + (grid 1x4x4, spatial_merge=2 → llm grid t=1,h=2,w=2 = 4 image + // tokens) and NOTHING after the image. + let tokens: Vec = std::iter::once(TEXT_A) + .chain(std::iter::repeat_n(IMG, 4)) + .collect(); + let (ids, grid) = mk_inputs(&tokens, &[(1, 4, 4)]); + let (pos, rope_deltas) = get_rope_index(&ids, grid.as_ref(), 2, IMG).unwrap(); + let (t, h, w) = extract_positions(&pos); + + let t_max = *t.iter().max().unwrap() as i64; + let global_max = *t.iter().chain(&h).chain(&w).max().unwrap() as i64; + let seq_len = tokens.len() as i64; + + // The spatial axes must outrun the temporal one here, else the test + // would not distinguish the global-max fix from the axis-0 regression. + assert!( + global_max > t_max, + "test grid is not asymmetric: global_max={global_max} t_max={t_max}" + ); + // The delta references the GLOBAL max, not the temporal-axis max. + assert_eq!(rope_deltas, global_max + 1 - seq_len); + assert_ne!( + rope_deltas, + t_max + 1 - seq_len, + "delta must not use the temporal axis alone (axis-0 regression)" + ); } #[test] @@ -9944,6 +10068,7 @@ mod paged_construction_tests { &layer_kinds, adapter, chunk_size, + /* cached_rope_deltas */ 0, ) } diff --git a/crates/mlx-core/src/models/qwen3_5/paged_forward.rs b/crates/mlx-core/src/models/qwen3_5/paged_forward.rs index 4e6192c68..c8fed2bcb 100644 --- a/crates/mlx-core/src/models/qwen3_5/paged_forward.rs +++ b/crates/mlx-core/src/models/qwen3_5/paged_forward.rs @@ -52,6 +52,54 @@ fn bytes_to_mib(bytes: f64) -> f64 { bytes / (1024.0 * 1024.0) } +/// Compute the scalar RoPE rotation offset for a paged forward step, decoupling +/// the rotation position from the physical KV slot. +/// +/// Image turns compress their ~hundreds of placeholder tokens into far fewer +/// M-RoPE positions, so the running M-RoPE position trails the physical token +/// count by `cached_rope_deltas` (negative). The paged pool still writes K/V at +/// the PHYSICAL slot, but the query/key rotation must use the compressed +/// position so a warm-continuation query lines up with the image-compressed +/// keys it attends over. Text-only turns carry `cached_rope_deltas == 0`, so +/// the result is the physical position unchanged (byte-identical to the prior +/// behaviour). +/// +/// `physical_position` is cast to `i32` BEFORE adding the (possibly negative) +/// delta so the arithmetic never underflows a `u32`. +pub(crate) fn paged_rope_offset(physical_position: u32, cached_rope_deltas: i32) -> i32 { + physical_position as i32 + cached_rope_deltas +} + +/// Decide the cross-turn M-RoPE delta to carry into the next paged turn. +/// +/// `cached_rope_deltas` is shared model state: an image prefill bakes in a +/// compressed-position delta (negative) that only aligns with the image's +/// physically-resident K/V. The delta is meaningful for exactly ONE outcome of +/// the paged turn planner — `continued_live_prefix`, where the live image +/// sequence is being extended and its K/V is re-attended. Every other outcome +/// must drop it: +/// * a cold/fresh turn carries no cross-turn delta; +/// * a NON-live prefix-cache hit (`cached_prefix_len > 0` but +/// `continued_live_prefix == false`) can only restore pure-text prefix blocks +/// — image requests prefill with `skip_lookup` and never publish a hashable +/// text stream that collides with their expanded-placeholder blocks — so the +/// suffix must rotate at the raw physical slot (delta 0), not at the stale +/// negative delta a prior image turn left on the shared model. +/// +/// Keying the reset on `cached_prefix_len == 0` is therefore too weak: it leaks +/// a stale image delta into unrelated text requests that merely share a cached +/// text prefix. +pub(crate) fn rope_delta_for_paged_turn( + cached_rope_deltas: Option, + continued_live_prefix: bool, +) -> Option { + if continued_live_prefix { + cached_rope_deltas + } else { + None + } +} + fn trace_memory_mib() -> (f64, f64, f64) { ( bytes_to_mib(crate::array::get_active_memory()), @@ -122,6 +170,7 @@ pub(crate) fn run_paged_prefill_chunk( embedding_weight: &MxArray, layer_kinds: &[Qwen3_5LayerKind], paged_adapter: &mut PagedKVCacheAdapter, + cached_rope_deltas: i32, ) -> Result { let chunk_size = crate::array::paged_prefill_chunk_size(); run_paged_prefill_chunk_with_size( @@ -138,6 +187,7 @@ pub(crate) fn run_paged_prefill_chunk( layer_kinds, paged_adapter, chunk_size, + cached_rope_deltas, ) } @@ -163,6 +213,7 @@ pub(crate) fn run_paged_prefill_chunk_with_size( layer_kinds: &[Qwen3_5LayerKind], paged_adapter: &mut PagedKVCacheAdapter, chunk_size: i32, + cached_rope_deltas: i32, ) -> Result { if suffix_tokens.is_empty() { return Err(Error::from_reason( @@ -184,6 +235,7 @@ pub(crate) fn run_paged_prefill_chunk_with_size( embedding_weight, layer_kinds, paged_adapter, + cached_rope_deltas, ); } @@ -232,6 +284,7 @@ pub(crate) fn run_paged_prefill_chunk_with_size( paged_adapter, /* inputs_embeds */ None, /* position_ids */ None, + cached_rope_deltas, )?; if is_last_chunk { @@ -312,6 +365,7 @@ fn run_paged_prefill_single_shot( embedding_weight: &MxArray, layer_kinds: &[Qwen3_5LayerKind], paged_adapter: &mut PagedKVCacheAdapter, + cached_rope_deltas: i32, ) -> Result { paged_adapter .record_tokens(suffix_tokens) @@ -332,6 +386,7 @@ fn run_paged_prefill_single_shot( paged_adapter, /* inputs_embeds */ None, /* position_ids */ None, + cached_rope_deltas, )?; project_last_token_logits(&hidden_states, final_norm, lm_head, embedding_weight) @@ -385,6 +440,10 @@ pub(crate) fn run_paged_vlm_prefill( paged_adapter, Some(&merge.inputs_embeds), Some(&merge.position_ids), + // Image prefill drives full-attention layers through the 3-row M-RoPE + // arm (`position_ids` is `Some`), so the scalar offset is unused here. + /* cached_rope_deltas */ + 0, )?; project_last_token_logits(&hidden_states, final_norm, lm_head, embedding_weight) @@ -419,6 +478,7 @@ pub(crate) fn run_paged_prefill_chunk_with_hidden( layer_kinds: &[Qwen3_5LayerKind], paged_adapter: &mut PagedKVCacheAdapter, keep_last_hidden: Option, + cached_rope_deltas: i32, ) -> Result<(MxArray, MxArray)> { let chunk_size = crate::array::paged_prefill_chunk_size(); run_paged_prefill_chunk_with_hidden_with_size( @@ -436,6 +496,7 @@ pub(crate) fn run_paged_prefill_chunk_with_hidden( paged_adapter, chunk_size, keep_last_hidden, + cached_rope_deltas, ) } @@ -455,6 +516,7 @@ fn run_paged_prefill_chunk_with_hidden_with_size( paged_adapter: &mut PagedKVCacheAdapter, chunk_size: i32, keep_last_hidden: Option, + cached_rope_deltas: i32, ) -> Result<(MxArray, MxArray)> { if suffix_tokens.is_empty() { return Err(Error::from_reason( @@ -477,6 +539,7 @@ fn run_paged_prefill_chunk_with_hidden_with_size( layer_kinds, paged_adapter, keep_last_hidden, + cached_rope_deltas, ); } @@ -517,6 +580,7 @@ fn run_paged_prefill_chunk_with_hidden_with_size( paged_adapter, /* inputs_embeds */ None, /* position_ids */ None, + cached_rope_deltas, )?; let chunk_hidden = if overlaps_kept_tail || is_last_chunk { @@ -605,6 +669,7 @@ fn run_paged_prefill_single_shot_with_hidden( layer_kinds: &[Qwen3_5LayerKind], paged_adapter: &mut PagedKVCacheAdapter, keep_last_hidden: Option, + cached_rope_deltas: i32, ) -> Result<(MxArray, MxArray)> { paged_adapter .record_tokens(suffix_tokens) @@ -625,6 +690,7 @@ fn run_paged_prefill_single_shot_with_hidden( paged_adapter, /* inputs_embeds */ None, /* position_ids */ None, + cached_rope_deltas, )?; project_last_token_logits_with_full_hidden( @@ -655,6 +721,7 @@ fn run_paged_prefill_one_chunk( paged_adapter: &mut PagedKVCacheAdapter, inputs_embeds: Option<&MxArray>, position_ids: Option<&MxArray>, + cached_rope_deltas: i32, ) -> Result { debug_assert_eq!(layers.len(), caches.len()); debug_assert_eq!(layers.len(), layer_kinds.len()); @@ -668,6 +735,14 @@ fn run_paged_prefill_one_chunk( } }; + // Scalar-offset RoPE position for this chunk's queries/keys. For a text + // suffix that warm-continues an image prefill, the rotation must trail the + // physical slot by the negative cross-turn delta so the suffix keys stay + // consistent with the immutable compressed-M-RoPE image keys. Text-only + // prefill carries `cached_rope_deltas == 0` (offset == physical position), + // and image prefill uses the M-RoPE arm so this is ignored there. + let rope_position_offset = paged_rope_offset(chunk_first_position, cached_rope_deltas); + for (layer_idx, ((layer, cache_slot), kind)) in layers .iter_mut() .zip(caches.iter_mut()) @@ -691,6 +766,7 @@ fn run_paged_prefill_one_chunk( Some(cache_slot), layer_positions, /* use_kernel */ true, + rope_position_offset, )?; crate::array::maybe_eval_clear_for_paged_prefill_layer(layer_idx, &hidden_states)?; } @@ -776,6 +852,7 @@ pub(crate) fn run_paged_decode_step( embedding_weight: &MxArray, layer_kinds: &[Qwen3_5LayerKind], paged_adapter: &mut PagedKVCacheAdapter, + cached_rope_deltas: i32, ) -> Result { // Capture logical position BEFORE record_tokens advances the // cursor. @@ -787,6 +864,10 @@ pub(crate) fn run_paged_decode_step( let input_ids = MxArray::from_uint32(&[token_id], &[1, 1])?; let mut hidden_states = embed.forward(&input_ids)?; + // Decode rotates the query at the physical slot plus the cross-turn M-RoPE + // delta (0 for text turns) while K/V still writes at the physical slot. + let rope_position_offset = paged_rope_offset(first_logical_position, cached_rope_deltas); + let num_layers = layers.len(); #[allow(clippy::needless_range_loop)] for layer_idx in 0..num_layers { @@ -811,6 +892,7 @@ pub(crate) fn run_paged_decode_step( Some(cache_slot), /* position_ids */ None, /* use_kernel */ true, + rope_position_offset, )?; } @@ -849,6 +931,7 @@ pub(crate) fn run_paged_step_with_hidden( embedding_weight_t: Option<&MxArray>, layer_kinds: &[Qwen3_5LayerKind], paged_adapter: &mut PagedKVCacheAdapter, + cached_rope_deltas: i32, ) -> Result<(MxArray, MxArray)> { let first_logical_position = paged_adapter.current_token_count(); paged_adapter @@ -858,6 +941,11 @@ pub(crate) fn run_paged_step_with_hidden( let input_ids = MxArray::from_uint32(&[token_id], &[1, 1])?; let mut hidden_states = embed.forward(&input_ids)?; + // Same cross-turn delta as `run_paged_decode_step`: a text MTP Step-A + // forward that warm-continues an image prefill must rotate at the + // compressed position. + let rope_position_offset = paged_rope_offset(first_logical_position, cached_rope_deltas); + let num_layers = layers.len(); #[allow(clippy::needless_range_loop)] for layer_idx in 0..num_layers { @@ -882,6 +970,7 @@ pub(crate) fn run_paged_step_with_hidden( Some(cache_slot), /* position_ids */ None, /* use_kernel */ true, + rope_position_offset, )?; } @@ -928,6 +1017,7 @@ pub(crate) fn run_paged_verify_step( layer_kinds: &[Qwen3_5LayerKind], paged_adapter: &mut PagedKVCacheAdapter, tape: &mut Vec>, + cached_rope_deltas: i32, ) -> Result { debug_assert_eq!(layers.len(), caches.len()); debug_assert_eq!(layers.len(), layer_kinds.len()); @@ -956,6 +1046,10 @@ pub(crate) fn run_paged_verify_step( let input_ids = MxArray::from_uint32(&verify_u32, &[1, verify_len as i64])?; let mut hidden_states = embed.forward(&input_ids)?; + // The K+1 verify ids rotate at the physical context start plus the + // cross-turn M-RoPE delta (0 for text turns), matching the Step-A forward. + let rope_position_offset = paged_rope_offset(chunk_first_position, cached_rope_deltas); + let num_layers = layers.len(); tape.clear(); tape.resize(num_layers, None); @@ -980,6 +1074,7 @@ pub(crate) fn run_paged_verify_step( /* is_prefill */ true, Some(cache_slot), Some(&mut slot), + rope_position_offset, )?; tape[layer_idx] = slot; } @@ -997,3 +1092,95 @@ pub(crate) fn run_paged_verify_step( logits, hiddens, )) } + +#[cfg(test)] +mod rope_offset_tests { + //! Model-free coverage for the paged scalar-offset RoPE position helper. + //! + //! The cross-turn image-decode fix decouples the rotation position from the + //! physical KV slot: image prefill compresses ~hundreds of placeholder + //! tokens into far fewer M-RoPE positions, so a warm-continuation turn must + //! rotate its queries at `physical_slot + cached_rope_deltas` (the delta is + //! negative) while K/V still writes at the physical slot. These tests pin + //! that arithmetic — including the cast-before-add type-safety guard — and + //! the text-turn identity (`delta == 0`) that keeps text decode + //! byte-identical. They construct no model, so they run on any host. + + use super::{paged_rope_offset, rope_delta_for_paged_turn}; + + #[test] + fn live_continuation_preserves_image_delta() { + // A live continuation re-attends the image request's physically-resident + // compressed-position K/V, so the negative delta MUST survive to keep the + // text suffix rotating at the compressed position. + assert_eq!(rope_delta_for_paged_turn(Some(-726), true), Some(-726)); + // Suffix at physical slot 754 then rotates at the compressed position 28. + let delta = rope_delta_for_paged_turn(Some(-726), true).unwrap_or(0); + assert_eq!(paged_rope_offset(754, delta), 28); + } + + #[test] + fn cold_start_clears_delta() { + // A fresh/miss turn (no reused prefix) carries no cross-turn delta. + assert_eq!(rope_delta_for_paged_turn(Some(-726), false), None); + assert_eq!(rope_delta_for_paged_turn(None, false), None); + } + + #[test] + fn non_live_prefix_cache_hit_clears_stale_image_delta() { + // Regression: a prior image turn leaves a stale negative delta on the + // shared model. A later TEXT request that merely HITS the cross-request + // prefix cache (cached_prefix_len > 0) is NOT a live image continuation + // (continued_live_prefix == false) — its restored blocks can only be the + // pure-text prefix. The stale delta must be dropped so text rotates at + // the raw physical slot, NOT at physical + stale_negative_delta. + let stale_image_delta = Some(-726); + let after_text_hit = rope_delta_for_paged_turn(stale_image_delta, false); + assert_eq!(after_text_hit, None); + assert_eq!(paged_rope_offset(42, after_text_hit.unwrap_or(0)), 42); + } + + #[test] + fn text_turn_zero_delta_is_identity() { + // Text-only turns store delta 0 -> the rotation offset equals the + // physical KV slot exactly, keeping text decode byte-identical. + assert_eq!(paged_rope_offset(0, 0), 0); + assert_eq!(paged_rope_offset(42, 0), 42); + assert_eq!(paged_rope_offset(1_000_000, 0), 1_000_000); + } + + #[test] + fn image_turn_negative_delta_shifts_offset_down() { + // An image turn compressing ~754 placeholder tokens to ~28 M-RoPE + // positions stores delta = 28 - 754 = -726. Decode at physical slot + // 754 must rotate at the compressed position 28, NOT 754; the next + // physical slot (755) rotates at 29. + let delta = -726; + assert_eq!(paged_rope_offset(754, delta), 28); + assert_eq!(paged_rope_offset(755, delta), 29); + } + + #[test] + fn offset_casts_to_i32_before_adding_negative_delta() { + // Type-safety guard: the cast to i32 happens BEFORE the add, so a small + // physical position with a large negative delta yields a negative i32 + // rather than wrapping a u32 subtraction. In practice the physical + // position always exceeds |delta| on a warm continuation, but the + // helper must not underflow if it ever did not. + assert_eq!(paged_rope_offset(10, -726), -716); + // Physical position equal to |delta| collapses to exactly 0. + assert_eq!(paged_rope_offset(726, -726), 0); + } + + #[test] + fn resetting_delta_to_zero_restores_physical_offset() { + // Round-trip of the stored cross-turn delta: applying a negative delta + // shifts the offset, and clearing it back to 0 (a fresh text turn / + // `Option::unwrap_or(0)`) restores the physical position unchanged. + let physical = 800; + let with_image_delta = paged_rope_offset(physical, -726); + assert_eq!(with_image_delta, 74); + let after_reset = paged_rope_offset(physical, 0); + assert_eq!(after_reset, physical as i32); + } +} diff --git a/crates/mlx-core/src/models/qwen3_5/persistence.rs b/crates/mlx-core/src/models/qwen3_5/persistence.rs index c5e170d37..5534dedb7 100644 --- a/crates/mlx-core/src/models/qwen3_5/persistence.rs +++ b/crates/mlx-core/src/models/qwen3_5/persistence.rs @@ -1996,6 +1996,16 @@ pub(crate) fn parse_vision_config(raw: &Value) -> Qwen3_5VisionConfig { } } +/// Collapse a 5D Conv3d patch-embed weight `[out, kD, kH, kW, in]` into a 2D +/// Conv2d kernel `[out, kH, kW, in]` by summing over the temporal axis. +/// +/// The image processor duplicates the static frame across the temporal axis, so +/// the effective 2D kernel is the sum of the temporal slices (matches mlx-vlm +/// `qwen3_vl/vision.py`). Summing all `kD` slices keeps this robust if `kD != 2`. +fn collapse_patch_embed_conv3d(pe_weight: &MxArray) -> Result { + pe_weight.sum(Some(&[1]), None) +} + /// Load vision encoder weights from params. pub(crate) fn load_vision_weights( encoder: &mut Qwen3_5VisionEncoder, @@ -2011,21 +2021,17 @@ pub(crate) fn load_vision_weights( let get_opt = |key: &str| -> Option<&MxArray> { params.get(key) }; // Patch embedding: handle both 4D Conv2d [out, kH, kW, in] and - // 5D Conv3d [out, kD, kH, kW, in] formats. For Conv3d, extract - // temporal slice 0 for our Conv2d PatchEmbedding. + // 5D Conv3d [out, kD, kH, kW, in] formats. For Conv3d, the static frame is + // duplicated across the temporal axis, so the effective 2D kernel is the + // sum of the temporal slices (not a single slice). if let Some(pe_weight) = get_opt("patch_embed.proj.weight") { + let pe_bias = get_opt("patch_embed.proj.bias"); let ndim = pe_weight.ndim()?; if ndim == 5 { - // Conv3d [out, kD, kH, kW, in] → take slice [:, 0, :, :, :] - let out_c = pe_weight.shape_at(0)?; - let kh = pe_weight.shape_at(2)?; - let kw = pe_weight.shape_at(3)?; - let in_c = pe_weight.shape_at(4)?; - let slice0 = pe_weight.slice(&[0, 0, 0, 0, 0], &[out_c, 1, kh, kw, in_c])?; - let conv2d_weight = slice0.squeeze(Some(&[1]))?; - encoder.set_patch_embed(&conv2d_weight)?; + let conv2d_weight = collapse_patch_embed_conv3d(pe_weight)?; + encoder.set_patch_embed(&conv2d_weight, pe_bias)?; } else { - encoder.set_patch_embed(pe_weight)?; + encoder.set_patch_embed(pe_weight, pe_bias)?; } } @@ -2146,6 +2152,50 @@ mod tests { use super::*; use serde_json::json; + #[test] + fn collapse_patch_embed_conv3d_sums_temporal_slices() { + // Synthetic Conv3d weight [out=2, kD=2, kH=2, kW=2, in=3] with distinct + // values per temporal slice. The collapse must SUM over the temporal + // axis (slice0 + slice1), not drop slice1. + let out_c = 2i64; + let kd = 2i64; + let kh = 2i64; + let kw = 2i64; + let in_c = 3i64; + let n = (out_c * kd * kh * kw * in_c) as usize; + let data: Vec = (0..n).map(|i| i as f32 * 0.5 - 3.0).collect(); + let pe_weight = MxArray::from_float32(&data, &[out_c, kd, kh, kw, in_c]).unwrap(); + + let collapsed = collapse_patch_embed_conv3d(&pe_weight).unwrap(); + collapsed.eval(); + let shape: Vec = collapsed.shape().unwrap().as_ref().to_vec(); + assert_eq!(shape, vec![out_c, kh, kw, in_c]); + + // Expected = slice[:,0,:,:,:] + slice[:,1,:,:,:]. + let slice0 = pe_weight + .slice(&[0, 0, 0, 0, 0], &[out_c, 1, kh, kw, in_c]) + .unwrap() + .squeeze(Some(&[1])) + .unwrap(); + let slice1 = pe_weight + .slice(&[0, 1, 0, 0, 0], &[out_c, 2, kh, kw, in_c]) + .unwrap() + .squeeze(Some(&[1])) + .unwrap(); + let expected = slice0.add(&slice1).unwrap(); + expected.eval(); + + let got: Vec = collapsed.to_float32().unwrap().to_vec(); + let exp: Vec = expected.to_float32().unwrap().to_vec(); + assert_eq!(got.len(), exp.len()); + for (i, (g, e)) in got.iter().zip(exp.iter()).enumerate() { + assert!( + (g - e).abs() < 1e-5, + "element {i}: collapsed {g} != slice0+slice1 {e}" + ); + } + } + #[test] fn mtp_sidecar_candidates_match_mtplx_order() { let raw = json!({ diff --git a/crates/mlx-core/src/models/qwen3_5/vision.rs b/crates/mlx-core/src/models/qwen3_5/vision.rs index 50bd6f374..1a95d44f7 100644 --- a/crates/mlx-core/src/models/qwen3_5/vision.rs +++ b/crates/mlx-core/src/models/qwen3_5/vision.rs @@ -76,7 +76,7 @@ impl Qwen3_5VisionEncoder { ], None, )?; - let patch_embed = PatchEmbedding::new(patch_size, &dummy_weight)?; + let patch_embed = PatchEmbedding::new(patch_size, &dummy_weight, None)?; Ok(Self { config, @@ -88,9 +88,13 @@ impl Qwen3_5VisionEncoder { }) } - /// Set patch embedding weights - pub fn set_patch_embed(&mut self, weight: &MxArray) -> Result<()> { - self.patch_embed = Arc::new(PatchEmbedding::new(self.config.patch_size as u32, weight)?); + /// Set patch embedding weights (and optional Conv bias) + pub fn set_patch_embed(&mut self, weight: &MxArray, bias: Option<&MxArray>) -> Result<()> { + self.patch_embed = Arc::new(PatchEmbedding::new( + self.config.patch_size as u32, + weight, + bias, + )?); Ok(()) } @@ -123,6 +127,9 @@ impl Qwen3_5VisionEncoder { "visual.patch_embed.proj.weight".to_string(), self.patch_embed.weight(), ); + if let Some(b) = self.patch_embed.bias() { + params.insert("visual.patch_embed.proj.bias".to_string(), b); + } // Position embedding if let Some(ref pe) = self.pos_embed { diff --git a/crates/mlx-core/src/models/qwen3_5_moe/decoder_layer.rs b/crates/mlx-core/src/models/qwen3_5_moe/decoder_layer.rs index ec102af27..e7ee3cd9e 100644 --- a/crates/mlx-core/src/models/qwen3_5_moe/decoder_layer.rs +++ b/crates/mlx-core/src/models/qwen3_5_moe/decoder_layer.rs @@ -172,6 +172,7 @@ impl DecoderLayer { flat_cache: Option<&mut Qwen3_5LayerCache>, position_ids: Option<&MxArray>, use_kernel: bool, + rope_position_offset: i32, ) -> Result { match kind { Qwen3_5LayerKind::Linear => { @@ -179,6 +180,7 @@ impl DecoderLayer { let _ = first_logical_position; let _ = cached_prefix_len; let _ = is_prefill; + let _ = rope_position_offset; if !matches!(self.attn, AttentionType::Linear(_)) { return Err(Error::from_reason( "Qwen3_5MoeDecoderLayer::forward_paged_or_flat: kind=Linear applied to a \ @@ -211,6 +213,7 @@ impl DecoderLayer { cached_prefix_len, is_prefill, position_ids, + rope_position_offset, )?; let h = x.add(&attn_out)?; let normed = self.post_attention_layernorm.forward(&h)?; diff --git a/crates/mlx-core/src/models/qwen3_5_moe/model.rs b/crates/mlx-core/src/models/qwen3_5_moe/model.rs index 7c8558b93..52032b319 100644 --- a/crates/mlx-core/src/models/qwen3_5_moe/model.rs +++ b/crates/mlx-core/src/models/qwen3_5_moe/model.rs @@ -1539,7 +1539,10 @@ impl Qwen35MoeInner { } self.cached_token_history.clear(); self.cached_image_key = None; - self.cached_rope_deltas = None; + // Store the image prefill's compressed-position delta so a later text + // warm-continuation rotates its queries at the same compressed M-RoPE + // positions the image keys were written with. + self.cached_rope_deltas = Some(merge.rope_deltas as i32); // Fresh per-layer caches (GDN linear slots + empty full-attention slots). self.caches = Some(fresh_moe_layer_caches(&self.config)); @@ -1632,6 +1635,7 @@ impl Qwen35MoeInner { &embedding_weight, &layer_kinds, adapter, + self.cached_rope_deltas.unwrap_or(0), )?; logits.squeeze(Some(&[1]))? }; @@ -1847,7 +1851,10 @@ impl Qwen35MoeInner { } self.cached_token_history.clear(); self.cached_image_key = None; - self.cached_rope_deltas = None; + // Store the image prefill's compressed-position delta so a later text + // warm-continuation rotates its queries at the same compressed M-RoPE + // positions the image keys were written with. + self.cached_rope_deltas = Some(merge.rope_deltas as i32); self.caches = Some(fresh_moe_layer_caches(&self.config)); @@ -1974,6 +1981,7 @@ impl Qwen35MoeInner { &embedding_weight, &layer_kinds, adapter, + self.cached_rope_deltas.unwrap_or(0), )?; logits.squeeze(Some(&[1]))? }; @@ -2258,7 +2266,14 @@ impl Qwen35MoeInner { let gdn_prefix_already_primed = gdn_prefix_preparation.already_primed; self.cached_token_history.clear(); self.cached_image_key = None; - self.cached_rope_deltas = None; + // Carry the cross-turn M-RoPE delta only when this turn extends the live + // image sequence (continued_live_prefix); a cold start or a non-live + // prefix-cache hit (text-only prefix) drops a stale image delta so the + // text suffix prefill + decode rotate at the raw physical slot. + self.cached_rope_deltas = crate::models::qwen3_5::paged_forward::rope_delta_for_paged_turn( + self.cached_rope_deltas, + continued_live_prefix, + ); let suffix_len = prompt_token_count .checked_sub(cached_prefix_len) @@ -2405,6 +2420,7 @@ impl Qwen35MoeInner { &embedding_weight, &layer_kinds, adapter, + self.cached_rope_deltas.unwrap_or(0), )? }; @@ -2473,6 +2489,7 @@ impl Qwen35MoeInner { &embedding_weight, &layer_kinds, adapter, + self.cached_rope_deltas.unwrap_or(0), )?; logits.squeeze(Some(&[1]))? }; @@ -2620,7 +2637,14 @@ impl Qwen35MoeInner { let gdn_prefix_state = gdn_prefix_preparation.state; self.cached_token_history.clear(); self.cached_image_key = None; - self.cached_rope_deltas = None; + // Carry the cross-turn M-RoPE delta only when this turn extends the live + // image sequence (continued_live_prefix); a cold start or a non-live + // prefix-cache hit (text-only prefix) drops a stale image delta so the + // text suffix prefill + decode rotate at the raw physical slot. + self.cached_rope_deltas = crate::models::qwen3_5::paged_forward::rope_delta_for_paged_turn( + self.cached_rope_deltas, + continued_live_prefix, + ); let suffix_len = prompt_token_count .checked_sub(cached_prefix_len) @@ -2877,6 +2901,7 @@ impl Qwen35MoeInner { &embedding_weight, &layer_kinds, adapter, + self.cached_rope_deltas.unwrap_or(0), )? }; @@ -3006,6 +3031,7 @@ impl Qwen35MoeInner { &embedding_weight, &layer_kinds, adapter, + self.cached_rope_deltas.unwrap_or(0), )?; if let Some(start) = forward_trace_start { decode_forward_ms += elapsed_ms(start); @@ -6116,6 +6142,7 @@ impl DecodeStep for Qwen35MoePagedDecode<'_> { &embedding_weight, &layer_kinds, adapter, + self.inner.cached_rope_deltas.unwrap_or(0), )? .squeeze(Some(&[1]))? }; @@ -6281,11 +6308,18 @@ impl PagedBackend for Qwen35MoeInner { )?; let gdn_prefix_already_primed = gdn_prefix_preparation.already_primed; // Clear the per-turn session state here (history is re-set in - // `save_paged_history`; rope deltas + image key are reset because the - // paged path does not carry them across turns). + // `save_paged_history`; image key is reset because the paged path does + // not carry it across turns). The cross-turn M-RoPE delta is carried + // only when this turn extends the live image sequence + // (continued_live_prefix); a cold start or a non-live prefix-cache hit + // (text-only prefix) drops a stale image delta so the text suffix + // rotates at the raw physical slot. self.cached_token_history.clear(); self.cached_image_key = None; - self.cached_rope_deltas = None; + self.cached_rope_deltas = crate::models::qwen3_5::paged_forward::rope_delta_for_paged_turn( + self.cached_rope_deltas, + continued_live_prefix, + ); let suffix_len = total_budget.checked_sub(cached_prefix_len).ok_or_else(|| { Error::from_reason("prime_prefix_state: cached_prefix_len > total_prompt_tokens") @@ -6318,6 +6352,10 @@ impl PagedBackend for Qwen35MoeInner { ); let embed = self.embedding.clone(); let embedding_weight = embed.get_weight(); + // Cross-turn M-RoPE delta (0 unless this engine-driven text turn warm- + // continues an image prefill); aligns the suffix keys with the + // compressed-position image keys. + let rope_deltas = self.cached_rope_deltas.unwrap_or(0); let caches_ref = self .caches .as_mut() @@ -6339,6 +6377,7 @@ impl PagedBackend for Qwen35MoeInner { &embedding_weight, &layer_kinds, adapter, + rope_deltas, ) } diff --git a/crates/mlx-core/src/models/qwen3_5_moe/paged_forward.rs b/crates/mlx-core/src/models/qwen3_5_moe/paged_forward.rs index b4b3b3b24..c86612f9e 100644 --- a/crates/mlx-core/src/models/qwen3_5_moe/paged_forward.rs +++ b/crates/mlx-core/src/models/qwen3_5_moe/paged_forward.rs @@ -96,6 +96,7 @@ pub(crate) fn run_paged_prefill_chunk( embedding_weight: &MxArray, layer_kinds: &[Qwen3_5LayerKind], paged_adapter: &mut PagedKVCacheAdapter, + cached_rope_deltas: i32, ) -> Result { let chunk_size = crate::array::paged_prefill_chunk_size(); run_paged_prefill_chunk_with_size( @@ -112,6 +113,7 @@ pub(crate) fn run_paged_prefill_chunk( layer_kinds, paged_adapter, chunk_size, + cached_rope_deltas, ) } @@ -166,6 +168,7 @@ pub(crate) fn run_paged_prefill_chunk_with_size( layer_kinds: &[Qwen3_5LayerKind], paged_adapter: &mut PagedKVCacheAdapter, chunk_size: i32, + cached_rope_deltas: i32, ) -> Result { if suffix_tokens.is_empty() { return Err(Error::from_reason( @@ -189,6 +192,7 @@ pub(crate) fn run_paged_prefill_chunk_with_size( embedding_weight, layer_kinds, paged_adapter, + cached_rope_deltas, ); } @@ -250,6 +254,7 @@ pub(crate) fn run_paged_prefill_chunk_with_size( paged_adapter, /* inputs_embeds */ None, /* position_ids */ None, + cached_rope_deltas, )?; if is_last_chunk { @@ -353,6 +358,7 @@ pub(crate) fn run_paged_prefill_single_shot( embedding_weight: &MxArray, layer_kinds: &[Qwen3_5LayerKind], paged_adapter: &mut PagedKVCacheAdapter, + cached_rope_deltas: i32, ) -> Result { paged_adapter .record_tokens(suffix_tokens) @@ -373,6 +379,7 @@ pub(crate) fn run_paged_prefill_single_shot( paged_adapter, /* inputs_embeds */ None, /* position_ids */ None, + cached_rope_deltas, )?; project_last_token_logits_moe(&hidden_states, final_norm, lm_head, embedding_weight) } @@ -428,6 +435,10 @@ pub(crate) fn run_paged_vlm_prefill_moe( paged_adapter, Some(&merge.inputs_embeds), Some(&merge.position_ids), + // Image prefill drives full-attention layers through the 3-row M-RoPE + // arm (`position_ids` is `Some`), so the scalar offset is unused here. + /* cached_rope_deltas */ + 0, )?; project_last_token_logits_moe(&hidden_states, final_norm, lm_head, embedding_weight) @@ -478,6 +489,7 @@ fn run_paged_prefill_one_chunk_moe( paged_adapter: &mut PagedKVCacheAdapter, inputs_embeds: Option<&MxArray>, position_ids: Option<&MxArray>, + cached_rope_deltas: i32, ) -> Result { debug_assert_eq!(layers.len(), caches.len()); debug_assert_eq!(layers.len(), layer_kinds.len()); @@ -491,6 +503,16 @@ fn run_paged_prefill_one_chunk_moe( } }; + // Scalar-offset RoPE position for this chunk: a text suffix that warm- + // continues an image prefill rotates at the physical slot plus the negative + // cross-turn delta so it stays consistent with the compressed-M-RoPE image + // keys. Text-only prefill carries `cached_rope_deltas == 0`; image prefill + // uses the M-RoPE arm, so this is ignored there. + let rope_position_offset = crate::models::qwen3_5::paged_forward::paged_rope_offset( + chunk_first_position, + cached_rope_deltas, + ); + // Layer loop. Safe-by-construction via `iter_mut().zip(...)` — // each iteration takes disjoint `&mut DecoderLayer` and `&mut // Qwen3_5LayerCache` references, with `kind` consumed by-value @@ -518,6 +540,7 @@ fn run_paged_prefill_one_chunk_moe( Some(cache_slot), layer_positions, true, + rope_position_offset, )?; // Smooth the prefill memory peak: every K layers, materialize the // residual stream so MLX can release the upstream graph nodes @@ -571,6 +594,7 @@ pub(crate) fn run_paged_decode_step( embedding_weight: &MxArray, layer_kinds: &[Qwen3_5LayerKind], paged_adapter: &mut PagedKVCacheAdapter, + cached_rope_deltas: i32, ) -> Result { let first_logical_position = paged_adapter.current_token_count(); paged_adapter @@ -580,6 +604,13 @@ pub(crate) fn run_paged_decode_step( let input_ids = MxArray::from_uint32(&[token_id], &[1, 1])?; let mut hidden_states = embed.forward(&input_ids)?; + // Decode rotates the query at the physical slot plus the cross-turn M-RoPE + // delta (0 for text turns) while K/V still writes at the physical slot. + let rope_position_offset = crate::models::qwen3_5::paged_forward::paged_rope_offset( + first_logical_position, + cached_rope_deltas, + ); + let num_layers = layers.len(); #[allow(clippy::needless_range_loop)] for layer_idx in 0..num_layers { @@ -603,6 +634,7 @@ pub(crate) fn run_paged_decode_step( Some(cache_slot), None, true, + rope_position_offset, )?; } @@ -913,6 +945,7 @@ mod tests { &layer_kinds, adapter, chunk_size, + /* cached_rope_deltas */ 0, ) { Ok(l) => l, Err(e) => { @@ -1286,6 +1319,7 @@ mod tests { &embedding_weight, &layer_kinds, adapter, + /* cached_rope_deltas */ 0, ) { Ok(l) => l, Err(e) => { @@ -1347,6 +1381,7 @@ mod tests { &layer_kinds, adapter, 0, + /* cached_rope_deltas */ 0, ) .expect("explicit chunk_size=0 prefill") }; diff --git a/crates/mlx-core/src/nn/linear.rs b/crates/mlx-core/src/nn/linear.rs index e71888ef0..ad17aeb36 100644 --- a/crates/mlx-core/src/nn/linear.rs +++ b/crates/mlx-core/src/nn/linear.rs @@ -238,3 +238,76 @@ impl Linear { }) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn addmm_bias_broadcast_rank() { + // a [2,3] @ b [3,4] -> [2,4]; add c. Test 1D c[4] vs 2D c[1,4] vs c[2,4]. + let a = MxArray::from_float32(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]).unwrap(); + let b = MxArray::from_float32( + &[1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0], + &[3, 4], + ) + .unwrap(); + // a@b row0 = [1, 2, 3, 1+2+3=6]; row1 = [4, 5, 6, 15] + let probe = |c: &MxArray, label: &str| { + let out = a.addmm(c, &b, None, None).unwrap(); + let got = out.to_float32().unwrap(); + eprintln!("[addmm c={label}] out[0]={:?}", &got[..4]); + got + }; + let c1d = MxArray::from_float32(&[10.0, 20.0, 30.0, 40.0], &[4]).unwrap(); + let c2d_row = MxArray::from_float32(&[10.0, 20.0, 30.0, 40.0], &[1, 4]).unwrap(); + let c2d_full = + MxArray::from_float32(&[10.0, 20.0, 30.0, 40.0, 10.0, 20.0, 30.0, 40.0], &[2, 4]) + .unwrap(); + let g1 = probe(&c1d, "[4]"); + let g2 = probe(&c2d_row, "[1,4]"); + let g3 = probe(&c2d_full, "[2,4]"); + // matmul + add (candidate fix) + let add_out = a + .matmul(&b) + .unwrap() + .add(&c1d) + .unwrap() + .to_float32() + .unwrap(); + eprintln!("[matmul+add c=[4]] out[0]={:?}", &add_out[..4]); + // a@b[0] = [1,2,3,6]; +c = [11,22,33,46] + eprintln!( + "applies bias -> 1D-c:{} 2D-row-c:{} 2D-full-c:{} matmul+add:{}", + (g1[0] - 11.0).abs() < 1e-3, + (g2[0] - 11.0).abs() < 1e-3, + (g3[0] - 11.0).abs() < 1e-3, + (add_out[0] - 11.0).abs() < 1e-3 + ); + } + + #[test] + fn linear_forward_applies_bias() { + // weight [out=4, in=3], bias [4], input [2,3]. + // expected = input @ weight.T + bias + let weight = MxArray::from_float32( + &[1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0], + &[4, 3], + ) + .unwrap(); + let bias = MxArray::from_float32(&[10.0, 20.0, 30.0, 40.0], &[4]).unwrap(); + let lin = Linear::from_weights(&weight, Some(&bias)).unwrap(); + + let input = MxArray::from_float32(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]).unwrap(); + let out = lin.forward(&input).unwrap(); + let got = out.to_float32().unwrap(); + let want = [11.0, 22.0, 33.0, 46.0, 14.0, 25.0, 36.0, 55.0]; + for (i, (g, w)) in got.iter().zip(want.iter()).enumerate() { + assert!( + (g - w).abs() < 1e-3, + "bias not applied at {i}: got {g}, want {w} (no-bias would be {})", + w - [10.0, 20.0, 30.0, 40.0][i % 4] + ); + } + } +} diff --git a/crates/mlx-core/src/vision/embeddings.rs b/crates/mlx-core/src/vision/embeddings.rs index dea1d1a4d..45492d882 100644 --- a/crates/mlx-core/src/vision/embeddings.rs +++ b/crates/mlx-core/src/vision/embeddings.rs @@ -29,11 +29,12 @@ impl PatchEmbedding { /// * `in_channels` - Number of input channels (3 for RGB) /// * `embed_dim` - Embedding dimension /// * `weight` - Convolution weights [embed_dim, patch_size, patch_size, in_channels] - pub fn new(patch_size: u32, weight: &MxArray) -> Result { + /// * `bias` - Optional convolution bias [embed_dim] + pub fn new(patch_size: u32, weight: &MxArray, bias: Option<&MxArray>) -> Result { // Create conv layer with stride = kernel_size = patch_size let conv = Conv2d::new( weight, - None, + bias, Some(vec![patch_size, patch_size]), // stride Some(vec![0, 0]), // padding None, // dilation @@ -76,6 +77,11 @@ impl PatchEmbedding { pub fn weight(&self) -> MxArray { self.patch_conv.weight() } + + /// Get the convolution bias if present + pub fn bias(&self) -> Option { + self.patch_conv.bias() + } } /// Vision Position Embedding (internal) @@ -181,7 +187,7 @@ mod tests { ) .unwrap(); - let patch_embed = PatchEmbedding::new(patch_size, &weight).unwrap(); + let patch_embed = PatchEmbedding::new(patch_size, &weight, None).unwrap(); // Input: [1, 8, 8, 3] -> [1, 4, 16] (2x2 patches, 16 dim) let input_data: Vec = (0..(8 * 8 * 3) as usize) @@ -196,6 +202,58 @@ mod tests { assert_eq!(shape, vec![1, 4, 16]); } + #[test] + fn test_patch_embedding_applies_bias() { + // With zero conv weights, the patch-embed output equals the bias, + // broadcast over every patch. This proves the bias is plumbed into + // the underlying Conv2d (previously hardcoded to None). + let patch_size = 4u32; + let in_c = 3i64; + let embed_dim = 5i64; + + let weight = MxArray::zeros( + &[embed_dim, patch_size as i64, patch_size as i64, in_c], + None, + ) + .unwrap(); + + let bias_data: Vec = vec![0.5, -1.0, 2.0, 3.5, -0.25]; + let bias = MxArray::from_float32(&bias_data, &[embed_dim]).unwrap(); + + // Input: [1, 8, 8, 3] -> 2x2 = 4 patches. + let input_data: Vec = (0..(8 * 8 * 3) as usize) + .map(|i| (i % 256) as f32 / 255.0) + .collect(); + let input = MxArray::from_float32(&input_data, &[1, 8, 8, 3]).unwrap(); + + // Without bias: zero weights -> all-zero output. + let no_bias = PatchEmbedding::new(patch_size, &weight, None).unwrap(); + let out_no_bias = no_bias.forward(&input).unwrap(); + out_no_bias.eval(); + let data_no_bias: Vec = out_no_bias.to_float32().unwrap().to_vec(); + assert!( + data_no_bias.iter().all(|&v| v.abs() < 1e-6), + "zero-weight, no-bias output must be all zeros" + ); + + // With bias: every patch equals the bias vector. + let with_bias = PatchEmbedding::new(patch_size, &weight, Some(&bias)).unwrap(); + let out = with_bias.forward(&input).unwrap(); + out.eval(); + let shape: Vec = out.shape().unwrap().as_ref().to_vec(); + assert_eq!(shape, vec![1, 4, 5]); + let data: Vec = out.to_float32().unwrap().to_vec(); + for patch in 0..4usize { + for (c, &expected) in bias_data.iter().enumerate() { + let got = data[patch * embed_dim as usize + c]; + assert!( + (got - expected).abs() < 1e-5, + "patch {patch} channel {c}: expected bias {expected}, got {got}" + ); + } + } + } + #[test] fn test_position_embedding_same_size() { let num_pos = 16u32; // 4x4 grid diff --git a/crates/mlx-core/tests/qwen3_5_moe_vl_image_chat.rs b/crates/mlx-core/tests/qwen3_5_moe_vl_image_chat.rs index b57fef67e..edfec3517 100644 --- a/crates/mlx-core/tests/qwen3_5_moe_vl_image_chat.rs +++ b/crates/mlx-core/tests/qwen3_5_moe_vl_image_chat.rs @@ -216,3 +216,91 @@ async fn qwen3_5_moe_vl_image_chat_t0_capture() { assert_eq!(d1, d1b, "turn 1 digest is not deterministic at T=0"); assert_eq!(d2, d2b, "turn 2 digest is not deterministic at T=0"); } + +/// Keywords from `examples/ocr.png` — a "TRUNCH PARISH COUNCIL — BANK +/// RECONCILIATION AS AT 31ST OCTOBER 2019" financial table. A model that +/// actually READS the image transcribes several of these; a model with broken +/// vision features hallucinates an unrelated scene (e.g. "stone/fabric") and +/// matches none. +const DOC_KEYWORDS: &[&str] = &[ + "reconciliation", + "bank", + "council", + "trunch", + "october", + "2019", + "balance", +]; + +/// Vision-CORRECTNESS gate (distinct from the `_t0_capture` parity digest, which +/// asserts paged==flat byte-identity and so passes even when the vision features +/// are garbage). Sends ONE image+prompt turn at T=0 and requires the model to +/// actually READ the document: the lowercased output must contain at least two +/// of `DOC_KEYWORDS`. This is the TDD ground-truth gate for the qwen3.5 VL image +/// fix — the shared vision path (qwen3_5 / qwen3_5_moe / Qwen3.6-35B-A3B) +/// currently fails it (describes the financial table as "stone/fabric"). +/// +/// CI note: the qwen3_5 MoE checkpoint (35B-A3B, ~70GB) is excluded from the CI +/// model-e2e matrix by size (see the `.github/workflows/ci.yml` model-test +/// comment); like the sibling `#[ignore]` gates this runs only via the env vars +/// locally / on opt-in, never under a plain `cargo test`. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[ignore = "needs MLX_TEST_QWEN35MOE_VL_MODEL_PATH + MLX_TEST_VLM_IMAGE_PATH for a Qwen3.5-VL MoE checkpoint + test image"] +async fn qwen3_5_moe_vl_reads_document_text() { + let Ok(model_path) = std::env::var("MLX_TEST_QWEN35MOE_VL_MODEL_PATH") else { + eprintln!("skipping: MLX_TEST_QWEN35MOE_VL_MODEL_PATH unset"); + return; + }; + assert!( + Path::new(&model_path).exists(), + "MLX_TEST_QWEN35MOE_VL_MODEL_PATH does not exist: {model_path}" + ); + let Some(image_path) = resolve_image_path() else { + eprintln!("skipping: no test image (set MLX_TEST_VLM_IMAGE_PATH or add examples/ocr.png)"); + return; + }; + let image = std::fs::read(&image_path).expect("failed to read test image"); + + let model = Qwen3_5MoeModel::load(model_path.clone()) + .await + .expect("failed to load Qwen3.5-VL MoE model"); + + tokio::task::block_in_place(|| model.reset_caches()).expect("reset_caches failed"); + + // ONE image+text turn at T=0 (greedy/deterministic). max_new_tokens (512) is + // well past the small thinking budget so the transcription answer is emitted. + let turn = model + .chat_session_start( + vec![user_msg( + "Transcribe the text in this document. List the title and any headings you can read.", + Some(&image), + )], + Some(cfg(512)), + ) + .await + .expect("document-transcription chat_session_start failed"); + + // Match against raw_text (thinking + answer) so a transcription counts + // whether the model reasons about the document or answers directly. + let out = turn.raw_text.to_lowercase(); + let hits: Vec<&str> = DOC_KEYWORDS + .iter() + .copied() + .filter(|kw| out.contains(kw)) + .collect(); + + println!( + "READS-DOC ntok={} finish={} hits={:?} :: {}", + turn.num_tokens, + turn.finish_reason, + hits, + oneline(&turn.raw_text) + ); + + assert!( + hits.len() >= 2, + "model did not READ the document: expected >=2 of {DOC_KEYWORDS:?}, \ + matched {hits:?} (broken vision features?). Full output:\n{}", + turn.raw_text + ); +} diff --git a/crates/mlx-core/tests/qwen3_5_vl_image_chat.rs b/crates/mlx-core/tests/qwen3_5_vl_image_chat.rs index c779a8ba9..3e48317c5 100644 --- a/crates/mlx-core/tests/qwen3_5_vl_image_chat.rs +++ b/crates/mlx-core/tests/qwen3_5_vl_image_chat.rs @@ -253,3 +253,91 @@ async fn qwen3_5_vl_image_chat_t0_capture() { assert_eq!(d1, d1b, "turn 1 digest is not deterministic at T=0"); assert_eq!(d2, d2b, "turn 2 digest is not deterministic at T=0"); } + +/// Keywords from `examples/ocr.png` — a "TRUNCH PARISH COUNCIL — BANK +/// RECONCILIATION AS AT 31ST OCTOBER 2019" financial table. A model that +/// actually READS the image transcribes several of these; a model with broken +/// vision features hallucinates an unrelated scene (e.g. "stone/fabric") and +/// matches none. +const DOC_KEYWORDS: &[&str] = &[ + "reconciliation", + "bank", + "council", + "trunch", + "october", + "2019", + "balance", +]; + +/// Vision-CORRECTNESS gate (distinct from the `_t0_capture` parity digest, which +/// asserts paged==flat byte-identity and so passes even when the vision features +/// are garbage). Sends ONE image+prompt turn at T=0 and requires the model to +/// actually READ the document: the lowercased output must contain at least two +/// of `DOC_KEYWORDS`. This is the TDD ground-truth gate for the qwen3.5 VL image +/// fix — the shared vision path (qwen3_5 / qwen3_5_moe / Qwen3.6-35B-A3B) +/// currently fails it (describes the financial table as "stone/fabric"). +/// +/// CI note: the qwen3_5 MoE checkpoint (35B-A3B, ~70GB) is excluded from the CI +/// model-e2e matrix by size (see the `.github/workflows/ci.yml` model-test +/// comment); like the sibling `#[ignore]` gates this runs only via the env vars +/// locally / on opt-in, never under a plain `cargo test`. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[ignore = "needs MLX_TEST_QWEN35_VL_MODEL_PATH + MLX_TEST_VLM_IMAGE_PATH for a Qwen3.5-VL dense checkpoint + test image"] +async fn qwen3_5_vl_reads_document_text() { + let Ok(model_path) = std::env::var("MLX_TEST_QWEN35_VL_MODEL_PATH") else { + eprintln!("skipping: MLX_TEST_QWEN35_VL_MODEL_PATH unset"); + return; + }; + assert!( + Path::new(&model_path).exists(), + "MLX_TEST_QWEN35_VL_MODEL_PATH does not exist: {model_path}" + ); + let Some(image_path) = resolve_image_path() else { + eprintln!("skipping: no test image (set MLX_TEST_VLM_IMAGE_PATH or add examples/ocr.png)"); + return; + }; + let image = std::fs::read(&image_path).expect("failed to read test image"); + + let model = Qwen3_5Model::load(model_path.clone()) + .await + .expect("failed to load Qwen3.5-VL model"); + + tokio::task::block_in_place(|| model.reset_caches()).expect("reset_caches failed"); + + // ONE image+text turn at T=0 (greedy/deterministic). max_new_tokens (512) is + // well past the small thinking budget so the transcription answer is emitted. + let turn = model + .chat_session_start( + vec![user_msg( + "Transcribe the text in this document. List the title and any headings you can read.", + Some(&image), + )], + Some(cfg(512)), + ) + .await + .expect("document-transcription chat_session_start failed"); + + // Match against raw_text (thinking + answer) so a transcription counts + // whether the model reasons about the document or answers directly. + let out = turn.raw_text.to_lowercase(); + let hits: Vec<&str> = DOC_KEYWORDS + .iter() + .copied() + .filter(|kw| out.contains(kw)) + .collect(); + + println!( + "READS-DOC ntok={} finish={} hits={:?} :: {}", + turn.num_tokens, + turn.finish_reason, + hits, + oneline(&turn.raw_text) + ); + + assert!( + hits.len() >= 2, + "model did not READ the document: expected >=2 of {DOC_KEYWORDS:?}, \ + matched {hits:?} (broken vision features?). Full output:\n{}", + turn.raw_text + ); +} From 73f4a89e79e044d9b6452db8fdb6c0cebea4b196 Mon Sep 17 00:00:00 2001 From: LongYinan Date: Tue, 30 Jun 2026 11:54:56 +0800 Subject: [PATCH 6/8] feat(server): honor stop_sequences in /v1/messages + loosen Qwen repetition cutoff (#81) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Why Running `mlx launch claude` against a local MLX model surfaced two server-side defects: 1. **Claude Code's auto-mode safety classifier always sends `stop_sequences`.** The `/v1/messages` mapper rejected any request carrying `stop_sequences` with a `400`, so the classifier failed with *"temporarily unavailable, cannot determine safety of Agent"* and the agent stalled. 2. **The native anti-repetition cutoff truncated legitimate output.** A wide ASCII box border (`────`), a separator (`====`/`----`), a table rule, or repeated indentation tripped the default cutoff (`maxConsecutiveTokens=16`, `maxNgramRepeats=3`), returning `finish_reason:"repetition"` → mapped to `end_turn` — so answers were silently cut off mid-diagram and looked like a clean finish. ## What - **Honor `stop_sequences` end-to-end in `/v1/messages`** (streaming + non-streaming): detect, truncate exactly, and report `stop_reason:"stop_sequence"` + `stop_sequence`. Removes the 400. - **Loosen the Qwen anti-repetition cutoff** in the launch presets (`256 / 8 / 64`) so wide boxes/tables/separators survive while a true runaway loop still stops. TS-only in `packages/server` — **no native/Rust change**, `ChatConfig`/`packages/core` untouched. ## How - `StopSequenceBuffer` (`packages/server/src/stop-sequence-buffer.ts`) — cross-delta detection mirroring `ToolCallTagBuffer`: partial-suffix holdback so a stop split across two streamed deltas is caught; earliest-match-wins, longest-on-tie (including a longer stop that begins at an earlier index); whitespace-only and empty stops filtered to a no-op. - Request/response mappers thread `stopSequences` and can emit `stop_reason:"stop_sequence"` (precedence over `max_tokens`/`tool_use`/`end_turn`). - Streaming done-path uses **suppress-then-natural-done**: it keeps consuming to the native `done` chunk (commit gate + history commit still fire), and finalizes the stop scan over one continuous buffer — streamed deltas, the held partial, tag residue, and the recovered terminal text — **before** emitting the safe prefix, deciding tool-call emission, or computing `stop_reason`. A matched stop suppresses any `tool_use` block so a response never carries both a tool call and `stop_reason:"stop_sequence"`. Empty/absent `stop_sequences` is a byte-identical no-op. - Presets (`packages/server/src/presets.ts`): all four `QWEN_SAMPLING_DEFAULTS` variants get `maxConsecutiveTokens:256, maxNgramRepeats:8, ngramSize:64`. Gemma4/LFM2 unchanged. ## Testing - `__test__/server/` — **24 files, 688 tests pass** (incl. new buffer, mapper, and handler coverage; the original 400-asserting tests replaced with honor-behavior tests). - TDD throughout (failing test first), then 4 adversarial review rounds (Codex); every confirmed finding fixed and re-verified by execution. The one remaining review note (an overlap edge that would only bite if the native terminal chunk were *incremental*) was verified non-occurring — `backend.rs` `finish()` sends the full cumulative `result.text` (`clean_text`), which the overlap logic is built for. - `vp lint --type-aware --type-check` and `yarn typecheck` clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- > [!NOTE] > **Medium Risk** > Large changes to Anthropic message streaming/non-streaming output shaping and stop/tool precedence; behavior is heavily tested but regressions could affect wire format or truncation edge cases. > > **Overview** > **`/v1/messages` now honors Anthropic `stop_sequences` instead of returning 400.** The request mapper normalizes stop strings (filters empty/whitespace-only) and returns them as `stopSequences` beside `ChatConfig`, which has no native stop field. A new **`StopSequenceBuffer`** detects matches across streamed deltas (partial holdback, earliest/longest tie-breaks) and drives truncation plus **`stop_reason: stop_sequence`** and **`stop_sequence`** on responses and SSE **`message_delta`**. Streaming and non-streaming paths wire the buffer through tool-tag buffering, terminal recovery, and flush-at-end; a matched stop **suppresses `tool_use`** blocks so stop wins over tools. Absent stops remain a pass-through no-op. > > **Qwen launch presets** add loosened anti-repetition sampling (**`maxConsecutiveTokens: 256`**, **`maxNgramRepeats: 8`**, **`ngramSize: 64`**) on all four **`QWEN_SAMPLING_DEFAULTS`** variants; Gemma4/LFM2 unchanged. Tests cover buffer, mappers, handler edge cases, and preset merge survival. > > Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit ee1e456937259ad9b763e6a8156dc17bf64548bd. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). --------- Co-authored-by: Claude Opus 4.8 --- .../server/anthropic-request-mapper.test.ts | 76 +- .../server/anthropic-response-mapper.test.ts | 51 + __test__/server/messages-handler.test.ts | 1209 ++++++++++++++++- __test__/server/presets.test.ts | 177 +++ __test__/server/stop-sequence-buffer.test.ts | 144 ++ packages/server/src/endpoints/messages.ts | 512 ++++--- .../server/src/mappers/anthropic-request.ts | 29 +- .../server/src/mappers/anthropic-response.ts | 44 +- packages/server/src/presets.ts | 22 + packages/server/src/stop-sequence-buffer.ts | 161 +++ 10 files changed, 2149 insertions(+), 276 deletions(-) create mode 100644 __test__/server/presets.test.ts create mode 100644 __test__/server/stop-sequence-buffer.test.ts create mode 100644 packages/server/src/stop-sequence-buffer.ts diff --git a/__test__/server/anthropic-request-mapper.test.ts b/__test__/server/anthropic-request-mapper.test.ts index 06006c0c4..e42611a61 100644 --- a/__test__/server/anthropic-request-mapper.test.ts +++ b/__test__/server/anthropic-request-mapper.test.ts @@ -988,31 +988,71 @@ describe('mapAnthropicRequest', () => { expect(config.tools).toHaveLength(2); }); - it('rejects non-empty stop_sequences (no native ChatConfig.stopSequences yet)', () => { - // `stop_sequences` is parsed into the type but `ChatConfig` has no - // matching field. Silently dropping the field would let a client believe - // its stop strings are honoured. Reject explicitly until native support - // lands. - expect(() => - mapAnthropicRequest({ - model: 'claude-3-5-sonnet-20241022', - max_tokens: 1024, - messages: [{ role: 'user', content: 'Hello' }], - stop_sequences: ['STOP'], - }), - ).toThrow(/stop_sequences.*not supported/i); + it('accepts non-empty stop_sequences and threads them out as stopSequences', () => { + // `stop_sequences` is now carried out of the mapper (on the widened + // return) so a downstream consumer can honour the stop strings. It must + // NOT be added to `ChatConfig` (that would need a native rebuild). + const { config, stopSequences } = mapAnthropicRequest({ + model: 'claude-3-5-sonnet-20241022', + max_tokens: 1024, + messages: [{ role: 'user', content: 'Hello' }], + stop_sequences: ['STOP'], + }); + expect(stopSequences).toEqual(['STOP']); + expect(config).not.toHaveProperty('stopSequences'); + expect(config).not.toHaveProperty('stop'); }); - it('accepts empty stop_sequences array (treated as absent)', () => { - // Empty array carries no semantics — accept silently rather than 400 on - // a no-op field. - const { messages } = mapAnthropicRequest({ + it('treats empty stop_sequences array as no stop strings', () => { + const { stopSequences } = mapAnthropicRequest({ model: 'claude-3-5-sonnet-20241022', max_tokens: 1024, messages: [{ role: 'user', content: 'Hello' }], stop_sequences: [], }); - expect(messages).toEqual([{ role: 'user', content: 'Hello' }]); + expect(stopSequences).toEqual([]); + }); + + it('treats absent stop_sequences as no stop strings', () => { + const { stopSequences } = mapAnthropicRequest({ + model: 'claude-3-5-sonnet-20241022', + max_tokens: 1024, + messages: [{ role: 'user', content: 'Hello' }], + }); + expect(stopSequences).toEqual([]); + }); + + it('filters empty strings out of stop_sequences', () => { + const { stopSequences } = mapAnthropicRequest({ + model: 'claude-3-5-sonnet-20241022', + max_tokens: 1024, + messages: [{ role: 'user', content: 'Hello' }], + stop_sequences: ['', 'X'], + }); + expect(stopSequences).toEqual(['X']); + }); + + it('filters whitespace-only strings out of stop_sequences', () => { + // The real Anthropic API rejects whitespace-only stops with a 400, and + // keeping them live would silently truncate normal output at the first + // space/newline. Dropping them makes such entries a no-op. + const { stopSequences } = mapAnthropicRequest({ + model: 'claude-3-5-sonnet-20241022', + max_tokens: 1024, + messages: [{ role: 'user', content: 'Hello' }], + stop_sequences: [' ', '\n', '\t', ' ', 'X'], + }); + expect(stopSequences).toEqual(['X']); + }); + + it('treats an all-whitespace stop_sequences array as no stop strings', () => { + const { stopSequences } = mapAnthropicRequest({ + model: 'claude-3-5-sonnet-20241022', + max_tokens: 1024, + messages: [{ role: 'user', content: 'Hello' }], + stop_sequences: [' ', '\n'], + }); + expect(stopSequences).toEqual([]); }); it('maps max_tokens to maxNewTokens in config', () => { diff --git a/__test__/server/anthropic-response-mapper.test.ts b/__test__/server/anthropic-response-mapper.test.ts index ec9f90e76..4e93bb4bc 100644 --- a/__test__/server/anthropic-response-mapper.test.ts +++ b/__test__/server/anthropic-response-mapper.test.ts @@ -63,6 +63,18 @@ describe('mapStopReason', () => { it('maps unknown reason with tool calls to "tool_use"', () => { expect(mapStopReason('unknown', true)).toBe('tool_use'); }); + + it('returns "stop_sequence" when a stop sequence matched', () => { + expect(mapStopReason('stop', false, 'HALT')).toBe('stop_sequence'); + }); + + it('prioritises a matched stop sequence over "max_tokens"', () => { + expect(mapStopReason('length', false, 'HALT')).toBe('stop_sequence'); + }); + + it('ignores a null matched stop sequence and keeps existing mapping', () => { + expect(mapStopReason('length', false, null)).toBe('max_tokens'); + }); }); describe('buildAnthropicResponse', () => { @@ -199,6 +211,38 @@ describe('buildAnthropicResponse', () => { expect(response.stop_reason).toBe('max_tokens'); }); + it('stop_reason is "stop_sequence" with a non-null stop_sequence when a stop sequence matched', () => { + const result = makeChatResult(); + const response = buildAnthropicResponse(result, baseReq, 'msg_stop_seq', undefined, true, undefined, 'HALT'); + + expect(response.stop_reason).toBe('stop_sequence'); + expect(response.stop_sequence).toBe('HALT'); + }); + + it('suppresses tool_use blocks when a stop sequence matched (stop wins over tool_use)', () => { + // A matched stop halts generation at its position, so a tool call whose + // tag would have followed the stop boundary must not be emitted alongside + // `stop_reason: 'stop_sequence'`. The truncated visible text stays. + const result = makeChatResult({ + text: 'keep this ', + toolCalls: [ + { + id: 'toolu_stop', + name: 'get_weather', + arguments: '{"city":"SF"}', + status: 'ok', + rawContent: '', + }, + ], + }); + const response = buildAnthropicResponse(result, baseReq, 'msg_stop_tool', undefined, true, undefined, 'HALT'); + + expect(response.stop_reason).toBe('stop_sequence'); + expect(response.stop_sequence).toBe('HALT'); + expect(response.content.some((b) => b.type === 'tool_use')).toBe(false); + expect(response.content).toEqual([{ type: 'text', text: 'keep this ' }]); + }); + it('usage maps promptTokens → input_tokens and numTokens → output_tokens', () => { const result = makeChatResult({ promptTokens: 42, numTokens: 7 }); const response = buildAnthropicResponse(result, baseReq, 'msg_usage'); @@ -539,6 +583,13 @@ describe('buildMessageDelta', () => { expect(event.usage.output_tokens).toBe(42); }); + it('emits stop_reason "stop_sequence" with the matched stop_sequence', () => { + const event = buildMessageDelta('stop_sequence', 5, undefined, undefined, undefined, undefined, 'HALT'); + + expect(event.delta.stop_reason).toBe('stop_sequence'); + expect(event.delta.stop_sequence).toBe('HALT'); + }); + it('passes through input_tokens when supplied without cachedTokens', () => { const event = buildMessageDelta('end_turn', 5, 11); diff --git a/__test__/server/messages-handler.test.ts b/__test__/server/messages-handler.test.ts index fbdc03122..33b77c69e 100644 --- a/__test__/server/messages-handler.test.ts +++ b/__test__/server/messages-handler.test.ts @@ -425,15 +425,12 @@ describe('handleCreateMessage', () => { expect(sessionReg.size).toBe(sizeBefore); }); - it('returns 400 when stop_sequences is non-empty (no warm slot touched)', async () => { - // End-to-end: native `ChatConfig` has no `stopSequences` field, so the - // mapper rejects the field rather than silently dropping it. The 400 - // must propagate via the outer try/catch in the handler, and the warm - // slot must remain untouched. + it('accepts non-empty stop_sequences instead of rejecting with 400', async () => { + // End-to-end: the mapper no longer rejects `stop_sequences`; it threads + // the stop strings out so a downstream consumer can honour them. The + // request must proceed normally rather than 400. const registry = new ModelRegistry(); registry.register('test-model', createMockModel()); - const sessionReg = registry.getSessionRegistry('test-model')!; - const sizeBefore = sessionReg.size; const { res, getStatus, getBody } = createMockRes(); await handleCreateMessage( @@ -447,12 +444,9 @@ describe('handleCreateMessage', () => { registry, ); - expect(getStatus()).toBe(400); + expect(getStatus()).toBe(200); const parsed = JSON.parse(getBody()); - expect(parsed.type).toBe('error'); - expect(parsed.error.type).toBe('invalid_request_error'); - expect(parsed.error.message).toContain('stop_sequences'); - expect(sessionReg.size).toBe(sizeBefore); + expect(parsed.type).toBe('message'); }); }); @@ -2030,6 +2024,1197 @@ describe('handleCreateMessage', () => { }); }); + // ----------------------------------------------------------------------- + // Stop sequences (honoring Anthropic `stop_sequences`) + // ----------------------------------------------------------------------- + + describe('stop sequences', () => { + it('non-streaming: truncates text at the stop sequence and reports stop_sequence', async () => { + const registry = new ModelRegistry(); + const mockModel = createMockModel( + makeChatResult({ + text: 'keep this HALT drop this', + rawText: 'keep this HALT drop this', + finishReason: 'stop', + numTokens: 12, + promptTokens: 6, + }), + ); + registry.register('test-model', mockModel); + const { res, getStatus, getBody } = createMockRes(); + + await handleCreateMessage( + res, + { + model: 'test-model', + messages: [{ role: 'user', content: 'go' }], + max_tokens: 100, + stop_sequences: ['HALT'], + }, + registry, + ); + + expect(getStatus()).toBe(200); + const parsed = JSON.parse(getBody()); + expect(parsed.type).toBe('message'); + expect(parsed.content).toHaveLength(1); + expect(parsed.content[0].type).toBe('text'); + expect(parsed.content[0].text).toBe('keep this '); + expect(parsed.stop_reason).toBe('stop_sequence'); + expect(parsed.stop_sequence).toBe('HALT'); + }); + + it('streaming: suppresses the stop sequence + tail and reports stop_sequence', async () => { + const registry = new ModelRegistry(); + const streamEvents = [ + { text: 'keep this HALT drop this', done: false, isReasoning: false }, + { + text: 'keep this HALT drop this', + done: true, + finishReason: 'stop', + toolCalls: [], + thinking: null, + numTokens: 12, + promptTokens: 6, + reasoningTokens: 0, + rawText: 'keep this HALT drop this', + }, + ]; + registry.register('test-model', createMockStreamModel(streamEvents)); + const { res, getBody } = createMockRes(); + + await handleCreateMessage( + res, + { + model: 'test-model', + messages: [{ role: 'user', content: 'go' }], + max_tokens: 100, + stream: true, + stop_sequences: ['HALT'], + }, + registry, + ); + + const events = parseSSE(getBody()); + const textDeltas = events.filter( + (e) => e.event === 'content_block_delta' && (e.data['delta'] as any).type === 'text_delta', + ); + const streamedText = textDeltas.map((d) => (d.data['delta'] as any).text as string).join(''); + expect(streamedText).toBe('keep this '); + expect(streamedText).not.toContain('HALT'); + expect(streamedText).not.toContain('drop this'); + + const msgDelta = events.find((e) => e.event === 'message_delta'); + expect(msgDelta).toBeDefined(); + expect((msgDelta!.data['delta'] as any).stop_reason).toBe('stop_sequence'); + expect((msgDelta!.data['delta'] as any).stop_sequence).toBe('HALT'); + + const msgStop = events.find((e) => e.event === 'message_stop'); + expect(msgStop).toBeDefined(); + }); + + it('streaming: detects a stop sequence split across two deltas', async () => { + const registry = new ModelRegistry(); + const streamEvents = [ + { text: 'keep HA', done: false, isReasoning: false }, + { text: 'LTdrop', done: false, isReasoning: false }, + { + text: 'keep HALTdrop', + done: true, + finishReason: 'stop', + toolCalls: [], + thinking: null, + numTokens: 10, + promptTokens: 5, + reasoningTokens: 0, + rawText: 'keep HALTdrop', + }, + ]; + registry.register('test-model', createMockStreamModel(streamEvents)); + const { res, getBody } = createMockRes(); + + await handleCreateMessage( + res, + { + model: 'test-model', + messages: [{ role: 'user', content: 'go' }], + max_tokens: 100, + stream: true, + stop_sequences: ['HALT'], + }, + registry, + ); + + const events = parseSSE(getBody()); + const textDeltas = events.filter( + (e) => e.event === 'content_block_delta' && (e.data['delta'] as any).type === 'text_delta', + ); + const streamedText = textDeltas.map((d) => (d.data['delta'] as any).text as string).join(''); + expect(streamedText).toBe('keep '); + expect(streamedText).not.toContain('HALT'); + expect(streamedText).not.toContain('drop'); + + const msgDelta = events.find((e) => e.event === 'message_delta'); + expect((msgDelta!.data['delta'] as any).stop_reason).toBe('stop_sequence'); + expect((msgDelta!.data['delta'] as any).stop_sequence).toBe('HALT'); + }); + + it('non-streaming: leaves text and stop_reason untouched when no stop sequence matches', async () => { + const registry = new ModelRegistry(); + const mockModel = createMockModel( + makeChatResult({ + text: 'keep this whole thing', + rawText: 'keep this whole thing', + finishReason: 'stop', + numTokens: 9, + promptTokens: 4, + }), + ); + registry.register('test-model', mockModel); + const { res, getStatus, getBody } = createMockRes(); + + await handleCreateMessage( + res, + { + model: 'test-model', + messages: [{ role: 'user', content: 'go' }], + max_tokens: 100, + stop_sequences: ['HALT'], + }, + registry, + ); + + expect(getStatus()).toBe(200); + const parsed = JSON.parse(getBody()); + expect(parsed.content[0].text).toBe('keep this whole thing'); + expect(parsed.stop_reason).toBe('end_turn'); + expect(parsed.stop_sequence).toBe(null); + }); + + it('streaming: leaves text and stop_reason untouched when no stop sequence matches', async () => { + const registry = new ModelRegistry(); + const streamEvents = [ + { text: 'keep ', done: false, isReasoning: false }, + { text: 'this whole thing', done: false, isReasoning: false }, + { + text: 'keep this whole thing', + done: true, + finishReason: 'stop', + toolCalls: [], + thinking: null, + numTokens: 9, + promptTokens: 4, + reasoningTokens: 0, + rawText: 'keep this whole thing', + }, + ]; + registry.register('test-model', createMockStreamModel(streamEvents)); + const { res, getBody } = createMockRes(); + + await handleCreateMessage( + res, + { + model: 'test-model', + messages: [{ role: 'user', content: 'go' }], + max_tokens: 100, + stream: true, + stop_sequences: ['HALT'], + }, + registry, + ); + + const events = parseSSE(getBody()); + const textDeltas = events.filter( + (e) => e.event === 'content_block_delta' && (e.data['delta'] as any).type === 'text_delta', + ); + const streamedText = textDeltas.map((d) => (d.data['delta'] as any).text as string).join(''); + expect(streamedText).toBe('keep this whole thing'); + + const msgDelta = events.find((e) => e.event === 'message_delta'); + expect((msgDelta!.data['delta'] as any).stop_reason).toBe('end_turn'); + expect((msgDelta!.data['delta'] as any).stop_sequence).toBe(null); + }); + + it('streaming: honors a stop sequence that lands in the visible text before a tool-call tag', async () => { + // Finding 1: the `tagFound` emit path used to write the visible text + // before a structural marker (`cleanPrefix`) straight to the wire + // without consulting the stop-sequence detector, so a stop string in + // that prefix leaked and `stop_reason` stayed `tool_use`. The model + // emits "keep this HALT " immediately followed by a `` in + // the SAME delta, so the tag buffer reports `tagFound` with + // `cleanPrefix === "keep this HALT "`. + const registry = new ModelRegistry(); + const streamEvents = [ + { text: 'keep this HALT {"name":"get_weather"}', done: false, isReasoning: false }, + { + text: '', + done: true, + finishReason: 'stop', + toolCalls: [{ status: 'ok', id: 'toolu_w1', name: 'get_weather', arguments: '{"location":"NYC"}' }], + thinking: null, + numTokens: 12, + promptTokens: 6, + reasoningTokens: 0, + rawText: 'keep this HALT {"name":"get_weather"}', + }, + ]; + registry.register('test-model', createMockStreamModel(streamEvents)); + const { res, getBody } = createMockRes(); + + await handleCreateMessage( + res, + { + model: 'test-model', + messages: [{ role: 'user', content: 'go' }], + max_tokens: 100, + stream: true, + stop_sequences: ['HALT'], + tools: [{ name: 'get_weather', input_schema: { type: 'object', properties: {} } }], + }, + registry, + ); + + const events = parseSSE(getBody()); + const textDeltas = events.filter( + (e) => e.event === 'content_block_delta' && (e.data['delta'] as any).type === 'text_delta', + ); + const streamedText = textDeltas.map((d) => (d.data['delta'] as any).text as string).join(''); + expect(streamedText).toBe('keep this '); + expect(streamedText).not.toContain('HALT'); + expect(streamedText).not.toContain(''); + + // The stop match wins over the tool call: `stop_reason` is + // `stop_sequence`, not `tool_use`. + const msgDelta = events.find((e) => e.event === 'message_delta'); + expect(msgDelta).toBeDefined(); + expect((msgDelta!.data['delta'] as any).stop_reason).toBe('stop_sequence'); + expect((msgDelta!.data['delta'] as any).stop_sequence).toBe('HALT'); + + // The tool call's tag came AFTER the stop boundary, so no tool_use + // content block may be emitted alongside the stop_sequence terminal. + const toolBlock = events.find( + (e) => e.event === 'content_block_start' && (e.data['content_block'] as any).type === 'tool_use', + ); + expect(toolBlock).toBeUndefined(); + + const msgStop = events.find((e) => e.event === 'message_stop'); + expect(msgStop).toBeDefined(); + }); + + it('non-streaming: suppresses the tool_use block when a stop matched in the visible text', async () => { + // Non-streaming sibling of the stop-before-tool case: the visible text + // carries a stop string and the native result also parsed a tool call. + // The truncated text wins and the tool_use block must be dropped. + const registry = new ModelRegistry(); + const mockModel = createMockModel( + makeChatResult({ + text: 'keep this HALT drop this', + rawText: 'keep this HALT {"name":"get_weather"}', + finishReason: 'stop', + toolCalls: [ + { + status: 'ok', + id: 'toolu_stop_ns', + name: 'get_weather', + arguments: '{"location":"NYC"}', + } as ToolCallResult, + ], + numTokens: 12, + promptTokens: 6, + }), + ); + registry.register('test-model', mockModel); + const { res, getStatus, getBody } = createMockRes(); + + await handleCreateMessage( + res, + { + model: 'test-model', + messages: [{ role: 'user', content: 'go' }], + max_tokens: 100, + stop_sequences: ['HALT'], + tools: [{ name: 'get_weather', input_schema: { type: 'object', properties: {} } }], + }, + registry, + ); + + expect(getStatus()).toBe(200); + const parsed = JSON.parse(getBody()); + expect(parsed.stop_reason).toBe('stop_sequence'); + expect(parsed.stop_sequence).toBe('HALT'); + expect(parsed.content.some((b: any) => b.type === 'tool_use')).toBe(false); + expect(parsed.content).toEqual([{ type: 'text', text: 'keep this ' }]); + }); + + it('non-streaming: resolves a held overlapping stop at flush', async () => { + // BUG A: `push()` holds a complete stop ('HALT') because a longer + // overlapping stop ('HALTED') is still viable, so it returns + // `matched:null`. Without a follow-up `flush()` the held stop leaks as + // normal text with `stop_reason: end_turn`. The non-streaming path must + // push+flush like the streaming done-path. + const registry = new ModelRegistry(); + const mockModel = createMockModel( + makeChatResult({ + text: 'keep HALT', + rawText: 'keep HALT', + finishReason: 'stop', + numTokens: 6, + promptTokens: 3, + }), + ); + registry.register('test-model', mockModel); + const { res, getStatus, getBody } = createMockRes(); + + await handleCreateMessage( + res, + { + model: 'test-model', + messages: [{ role: 'user', content: 'go' }], + max_tokens: 100, + stop_sequences: ['HALT', 'HALTED'], + }, + registry, + ); + + expect(getStatus()).toBe(200); + const parsed = JSON.parse(getBody()); + expect(parsed.content).toHaveLength(1); + expect(parsed.content[0].type).toBe('text'); + expect(parsed.content[0].text).toBe('keep '); + expect(parsed.stop_reason).toBe('stop_sequence'); + expect(parsed.stop_sequence).toBe('HALT'); + }); + + it('non-streaming: honors a stop sequence inside recovered suppressed-tool text', async () => { + // BUG B: the request disallows tools, but the native parser still + // produced a tool call and `result.text` is empty, so + // `buildAnthropicContent` emits `recoverSuppressedToolCallText(rawText)` + // as the visible text. The stop scan must run over THAT recovered text, + // not the empty `result.text` — otherwise a stop inside it leaks with + // `stop_reason: end_turn`. + const registry = new ModelRegistry(); + const mockModel = createMockModel( + makeChatResult({ + text: '', + rawText: 'Sure STOP {"name":"get_weather","arguments":{}}', + finishReason: 'stop', + toolCalls: [ + { + status: 'ok', + id: 'call_x', + name: 'get_weather', + arguments: '{}', + } as ToolCallResult, + ], + numTokens: 12, + promptTokens: 6, + }), + ); + registry.register('test-model', mockModel); + const { res, getStatus, getBody } = createMockRes(); + + await handleCreateMessage( + res, + { + model: 'test-model', + messages: [{ role: 'user', content: 'go' }], + max_tokens: 100, + stop_sequences: ['STOP'], + }, + registry, + ); + + expect(getStatus()).toBe(200); + const parsed = JSON.parse(getBody()); + expect(parsed.content.some((b: any) => b.type === 'tool_use')).toBe(false); + const textBlock = parsed.content.find((b: any) => b.type === 'text'); + expect(textBlock).toBeDefined(); + expect(textBlock.text).toBe('Sure '); + expect(parsed.stop_reason).toBe('stop_sequence'); + expect(parsed.stop_sequence).toBe('STOP'); + }); + + it('non-streaming: keeps a trailing incomplete partial when no stop completes', async () => { + // Control for the flush() addition: 'keep HAL' ends in a prefix of + // 'HALT' but never completes it. `flush()` must release the held partial + // as normal text — nothing may be dropped and no false truncation may + // occur. + const registry = new ModelRegistry(); + const mockModel = createMockModel( + makeChatResult({ + text: 'keep HAL', + rawText: 'keep HAL', + finishReason: 'stop', + numTokens: 6, + promptTokens: 3, + }), + ); + registry.register('test-model', mockModel); + const { res, getStatus, getBody } = createMockRes(); + + await handleCreateMessage( + res, + { + model: 'test-model', + messages: [{ role: 'user', content: 'go' }], + max_tokens: 100, + stop_sequences: ['HALT'], + }, + registry, + ); + + expect(getStatus()).toBe(200); + const parsed = JSON.parse(getBody()); + expect(parsed.content[0].text).toBe('keep HAL'); + expect(parsed.stop_reason).toBe('end_turn'); + expect(parsed.stop_sequence).toBe(null); + }); + + it('streaming: honors a stop sequence hidden in suppressed/recovered text on a no-tools turn', async () => { + // Finding 1: the done-path recovery branch re-emits native final text + // that the tag buffer suppressed (here the visible bytes around a + // `` on a request with NO tools). A stop string living in + // that recovered tail was never routed through the detector, so it + // leaked and the terminal kept the native reason. The recovered text + // must run through the SAME detector before emission. + const registry = new ModelRegistry(); + const streamEvents = [ + { text: 'Sure {"name":"x"} STOP now', done: false, isReasoning: false }, + { + text: 'Sure STOP now', + done: true, + finishReason: 'stop', + toolCalls: [{ status: 'ok', id: 'call_x', name: 'x', arguments: '{}' }], + thinking: null, + numTokens: 12, + promptTokens: 6, + reasoningTokens: 0, + rawText: 'Sure {"name":"x"} STOP now', + }, + ]; + registry.register('test-model', createMockStreamModel(streamEvents)); + const { res, getBody } = createMockRes(); + + await handleCreateMessage( + res, + { + model: 'test-model', + messages: [{ role: 'user', content: 'go' }], + max_tokens: 100, + stream: true, + stop_sequences: ['STOP'], + }, + registry, + ); + + const events = parseSSE(getBody()); + const textDeltas = events.filter( + (e) => e.event === 'content_block_delta' && (e.data['delta'] as any).type === 'text_delta', + ); + const streamedText = textDeltas.map((d) => (d.data['delta'] as any).text as string).join(''); + expect(streamedText).not.toContain('STOP'); + expect(streamedText).not.toContain('now'); + + const msgDelta = events.find((e) => e.event === 'message_delta'); + expect(msgDelta).toBeDefined(); + expect((msgDelta!.data['delta'] as any).stop_reason).toBe('stop_sequence'); + expect((msgDelta!.data['delta'] as any).stop_sequence).toBe('STOP'); + + const msgStop = events.find((e) => e.event === 'message_stop'); + expect(msgStop).toBeDefined(); + }); + + it('streaming: flushes parked pre-stop whitespace as the truncated prefix', async () => { + // Finding 5: when the same push that cleared a whitespace-only safe + // prefix also matched the stop, that whitespace was parked in + // `pendingLeadingWhitespace` and then dropped at done, so streaming + // returned no text while non-streaming returned the leading whitespace. + // The parked safe bytes must be flushed as the truncated prefix. + const registry = new ModelRegistry(); + const streamEvents = [ + { text: ' HALTtail', done: false, isReasoning: false }, + { + text: ' HALTtail', + done: true, + finishReason: 'stop', + toolCalls: [], + thinking: null, + numTokens: 8, + promptTokens: 4, + reasoningTokens: 0, + rawText: ' HALTtail', + }, + ]; + registry.register('test-model', createMockStreamModel(streamEvents)); + const { res, getBody } = createMockRes(); + + await handleCreateMessage( + res, + { + model: 'test-model', + messages: [{ role: 'user', content: 'go' }], + max_tokens: 100, + stream: true, + stop_sequences: ['HALT'], + }, + registry, + ); + + const events = parseSSE(getBody()); + const textDeltas = events.filter( + (e) => e.event === 'content_block_delta' && (e.data['delta'] as any).type === 'text_delta', + ); + const streamedText = textDeltas.map((d) => (d.data['delta'] as any).text as string).join(''); + // Exactly the safe pre-stop prefix " ", matching the non-streaming result. + expect(streamedText).toBe(' '); + expect(streamedText).not.toContain('HALT'); + + const msgDelta = events.find((e) => e.event === 'message_delta'); + expect((msgDelta!.data['delta'] as any).stop_reason).toBe('stop_sequence'); + expect((msgDelta!.data['delta'] as any).stop_sequence).toBe('HALT'); + }); + + it('streaming: opens an empty text block when a stop sequence consumes all visible output', async () => { + // Parity with the non-streaming path: when the stop matches at the very + // start so the visible text collapses to '', the reconstructed content + // must be [{type:'text', text:''}] — a text block opened and closed with + // no body, exactly like buildAnthropicContent. The old `body.length > 0` + // guard skipped the block entirely, so a client rebuilding from SSE saw + // `content: []` while the non-streaming sibling returned an empty block. + const registry = new ModelRegistry(); + const streamEvents = [ + { text: 'STOPhello', done: false, isReasoning: false }, + { + text: 'STOPhello', + done: true, + finishReason: 'stop', + toolCalls: [], + thinking: null, + numTokens: 5, + promptTokens: 3, + reasoningTokens: 0, + rawText: 'STOPhello', + }, + ]; + registry.register('test-model', createMockStreamModel(streamEvents)); + const { res, getBody } = createMockRes(); + + await handleCreateMessage( + res, + { + model: 'test-model', + messages: [{ role: 'user', content: 'go' }], + max_tokens: 100, + stream: true, + stop_sequences: ['STOP'], + }, + registry, + ); + + const events = parseSSE(getBody()); + + // Exactly one text block, opened with empty text — the empty-collapse block. + const textStarts = events.filter( + (e) => e.event === 'content_block_start' && (e.data['content_block'] as any).type === 'text', + ); + expect(textStarts).toHaveLength(1); + expect((textStarts[0].data['content_block'] as any).text).toBe(''); + + // The stop ate everything: no visible text leaked and no empty text_delta. + const textDeltas = events.filter( + (e) => e.event === 'content_block_delta' && (e.data['delta'] as any).type === 'text_delta', + ); + expect(textDeltas).toHaveLength(0); + + const msgDelta = events.find((e) => e.event === 'message_delta'); + expect((msgDelta!.data['delta'] as any).stop_reason).toBe('stop_sequence'); + expect((msgDelta!.data['delta'] as any).stop_sequence).toBe('STOP'); + }); + + it('non-streaming: returns the leading whitespace prefix when a stop matches right after it', async () => { + // Non-streaming counterpart of the parked-whitespace streaming case: + // the result text " HALTtail" truncates to " " (the safe prefix). + const registry = new ModelRegistry(); + const mockModel = createMockModel( + makeChatResult({ + text: ' HALTtail', + rawText: ' HALTtail', + finishReason: 'stop', + numTokens: 8, + promptTokens: 4, + }), + ); + registry.register('test-model', mockModel); + const { res, getStatus, getBody } = createMockRes(); + + await handleCreateMessage( + res, + { + model: 'test-model', + messages: [{ role: 'user', content: 'go' }], + max_tokens: 100, + stop_sequences: ['HALT'], + }, + registry, + ); + + expect(getStatus()).toBe(200); + const parsed = JSON.parse(getBody()); + expect(parsed.content).toEqual([{ type: 'text', text: ' ' }]); + expect(parsed.stop_reason).toBe('stop_sequence'); + expect(parsed.stop_sequence).toBe('HALT'); + }); + + it('non-streaming: returns an empty text block when a stop sequence consumes all visible output', async () => { + // Sibling/parity anchor of the streaming empty-collapse case: the result + // text "STOPhello" truncates to '' and the content is a single empty text + // block — the target the streaming path now matches. + const registry = new ModelRegistry(); + const mockModel = createMockModel( + makeChatResult({ + text: 'STOPhello', + rawText: 'STOPhello', + finishReason: 'stop', + numTokens: 5, + promptTokens: 3, + }), + ); + registry.register('test-model', mockModel); + const { res, getStatus, getBody } = createMockRes(); + + await handleCreateMessage( + res, + { + model: 'test-model', + messages: [{ role: 'user', content: 'go' }], + max_tokens: 100, + stop_sequences: ['STOP'], + }, + registry, + ); + + expect(getStatus()).toBe(200); + const parsed = JSON.parse(getBody()); + expect(parsed.content).toEqual([{ type: 'text', text: '' }]); + expect(parsed.stop_reason).toBe('stop_sequence'); + expect(parsed.stop_sequence).toBe('STOP'); + }); + + it('streaming: releases a benign held partial when a tool-call tag interrupts it', async () => { + // Finding 1 (consequence 2): when the detector holds a benign partial + // (a prefix of a stop sequence, e.g. "H" of "HALT") and the next delta + // is a structural marker, the partial used to be stranded in the + // detector and dropped by the suppressed-terminal residue gate. The + // `tagFound` path now flushes the detector, releasing the partial as + // visible text. Here "keep H" holds "H"; the next delta is a bare + // `` (cleanPrefix === ""), and "H" can never become "HALT". + const registry = new ModelRegistry(); + const streamEvents = [ + { text: 'keep H', done: false, isReasoning: false }, + { text: '{"name":"get_weather"}', done: false, isReasoning: false }, + { + text: '', + done: true, + finishReason: 'stop', + toolCalls: [{ status: 'ok', id: 'toolu_w2', name: 'get_weather', arguments: '{"location":"NYC"}' }], + thinking: null, + numTokens: 10, + promptTokens: 5, + reasoningTokens: 0, + rawText: 'keep H{"name":"get_weather"}', + }, + ]; + registry.register('test-model', createMockStreamModel(streamEvents)); + const { res, getBody } = createMockRes(); + + await handleCreateMessage( + res, + { + model: 'test-model', + messages: [{ role: 'user', content: 'go' }], + max_tokens: 100, + stream: true, + stop_sequences: ['HALT'], + tools: [{ name: 'get_weather', input_schema: { type: 'object', properties: {} } }], + }, + registry, + ); + + const events = parseSSE(getBody()); + const textDeltas = events.filter( + (e) => e.event === 'content_block_delta' && (e.data['delta'] as any).type === 'text_delta', + ); + const streamedText = textDeltas.map((d) => (d.data['delta'] as any).text as string).join(''); + // The benign "H" is preserved, not dropped. + expect(streamedText).toBe('keep H'); + + // No stop matched, so the tool call drives the terminal as before. + const msgDelta = events.find((e) => e.event === 'message_delta'); + expect((msgDelta!.data['delta'] as any).stop_reason).toBe('tool_use'); + expect((msgDelta!.data['delta'] as any).stop_sequence).toBe(null); + }); + + it('streaming: preserves order of buffered leading whitespace and a held stop-prefix before a tool-call tag', async () => { + // Re-review of the `tagFound` fix: a turn-leading whitespace-only delta + // (" ") and a stop-prefix ("H" of "HALT") arrive together as " H", then a + // bare `` follows. The detector clears the leading " " (visible) + // and holds "H"; the " " parks in `pendingLeadingWhitespace` because no + // text block is open yet. At the tag, `pendingLeadingWhitespace` is text + // that ALREADY passed the detector, so it must be prepended OUTSIDE the + // buffer. Re-pushing it would queue it after the held "H" and emit "H " + // (order inverted). The visible run is " H" and "H" can never become + // "HALT", so no stop matches and the tool call drives the terminal. + const registry = new ModelRegistry(); + const streamEvents = [ + { text: ' H', done: false, isReasoning: false }, + { text: '{"name":"get_weather"}', done: false, isReasoning: false }, + { + text: '', + done: true, + finishReason: 'stop', + toolCalls: [{ status: 'ok', id: 'toolu_w4', name: 'get_weather', arguments: '{"location":"NYC"}' }], + thinking: null, + numTokens: 9, + promptTokens: 5, + reasoningTokens: 0, + rawText: ' H{"name":"get_weather"}', + }, + ]; + registry.register('test-model', createMockStreamModel(streamEvents)); + const { res, getBody } = createMockRes(); + + await handleCreateMessage( + res, + { + model: 'test-model', + messages: [{ role: 'user', content: 'go' }], + max_tokens: 100, + stream: true, + stop_sequences: ['HALT'], + tools: [{ name: 'get_weather', input_schema: { type: 'object', properties: {} } }], + }, + registry, + ); + + const events = parseSSE(getBody()); + const textDeltas = events.filter( + (e) => e.event === 'content_block_delta' && (e.data['delta'] as any).type === 'text_delta', + ); + const streamedText = textDeltas.map((d) => (d.data['delta'] as any).text as string).join(''); + // Order preserved (" H", not "H "), nothing leaked, nothing dropped. + expect(streamedText).toBe(' H'); + + // No stop matched, so the tool call drives the terminal. + const msgDelta = events.find((e) => e.event === 'message_delta'); + expect((msgDelta!.data['delta'] as any).stop_reason).toBe('tool_use'); + expect((msgDelta!.data['delta'] as any).stop_sequence).toBe(null); + }); + + it('streaming: leaves the pre-tag visible text untouched when no stop_sequences are configured', async () => { + // Finding 2: with `stop_sequences` absent the detector is a pass-through, + // so the `tagFound` path stays byte-identical — the full visible prefix + // (here "keep this HALT ", stop string and all) reaches the wire and + // `stop_sequence` is null. Same stream as the honor test above, minus + // `stop_sequences`. + const registry = new ModelRegistry(); + const streamEvents = [ + { text: 'keep this HALT {"name":"get_weather"}', done: false, isReasoning: false }, + { + text: '', + done: true, + finishReason: 'stop', + toolCalls: [{ status: 'ok', id: 'toolu_w3', name: 'get_weather', arguments: '{"location":"NYC"}' }], + thinking: null, + numTokens: 12, + promptTokens: 6, + reasoningTokens: 0, + rawText: 'keep this HALT {"name":"get_weather"}', + }, + ]; + registry.register('test-model', createMockStreamModel(streamEvents)); + const { res, getBody } = createMockRes(); + + await handleCreateMessage( + res, + { + model: 'test-model', + messages: [{ role: 'user', content: 'go' }], + max_tokens: 100, + stream: true, + tools: [{ name: 'get_weather', input_schema: { type: 'object', properties: {} } }], + }, + registry, + ); + + const events = parseSSE(getBody()); + const textDeltas = events.filter( + (e) => e.event === 'content_block_delta' && (e.data['delta'] as any).type === 'text_delta', + ); + const streamedText = textDeltas.map((d) => (d.data['delta'] as any).text as string).join(''); + expect(streamedText).toBe('keep this HALT '); + + const msgDelta = events.find((e) => e.event === 'message_delta'); + expect((msgDelta!.data['delta'] as any).stop_reason).toBe('tool_use'); + expect((msgDelta!.data['delta'] as any).stop_sequence).toBe(null); + }); + + it('streaming: detects a stop split across a held partial and recovered terminal text', async () => { + // Finding B: the streamed delta "HA" is held by the detector (prefix of + // "HALT"); the native done text "HALTtail" completes the stop. The held + // partial must stay in the buffer while the terminal/recovered text is + // scanned so "HALT" is caught across the boundary instead of leaking. + const registry = new ModelRegistry(); + const streamEvents = [ + { text: 'HA', done: false, isReasoning: false }, + { + text: 'HALTtail', + done: true, + finishReason: 'stop', + toolCalls: [], + thinking: null, + numTokens: 8, + promptTokens: 4, + reasoningTokens: 0, + rawText: 'HALTtail', + }, + ]; + registry.register('test-model', createMockStreamModel(streamEvents)); + const { res, getBody } = createMockRes(); + + await handleCreateMessage( + res, + { + model: 'test-model', + messages: [{ role: 'user', content: 'go' }], + max_tokens: 100, + stream: true, + stop_sequences: ['HALT'], + }, + registry, + ); + + const events = parseSSE(getBody()); + const textDeltas = events.filter( + (e) => e.event === 'content_block_delta' && (e.data['delta'] as any).type === 'text_delta', + ); + const streamedText = textDeltas.map((d) => (d.data['delta'] as any).text as string).join(''); + expect(streamedText).not.toContain('HALT'); + expect(streamedText).not.toContain('tail'); + + const msgDelta = events.find((e) => e.event === 'message_delta'); + expect(msgDelta).toBeDefined(); + expect((msgDelta!.data['delta'] as any).stop_reason).toBe('stop_sequence'); + expect((msgDelta!.data['delta'] as any).stop_sequence).toBe('HALT'); + }); + + it('streaming: detects a stop split across a held partial and tag-suppressed recovered text', async () => { + // Finding B (tag-suppression variant): the model emits "HA", then a + // suppressed on a no-tools turn, then "LTtail". The native + // cleaned done text "HALTtail" reconstitutes the visible text. The held + // "HA" must still be buffered when the recovered tail is scanned, so the + // already-streamed bytes never include any part of the matched stop. + const registry = new ModelRegistry(); + const streamEvents = [ + { text: 'HA{}LTtail', done: false, isReasoning: false }, + { + text: 'HALTtail', + done: true, + finishReason: 'stop', + toolCalls: [], + thinking: null, + numTokens: 8, + promptTokens: 4, + reasoningTokens: 0, + rawText: 'HA{}LTtail', + }, + ]; + registry.register('test-model', createMockStreamModel(streamEvents)); + const { res, getBody } = createMockRes(); + + await handleCreateMessage( + res, + { + model: 'test-model', + messages: [{ role: 'user', content: 'go' }], + max_tokens: 100, + stream: true, + stop_sequences: ['HALT'], + }, + registry, + ); + + const events = parseSSE(getBody()); + const textDeltas = events.filter( + (e) => e.event === 'content_block_delta' && (e.data['delta'] as any).type === 'text_delta', + ); + const streamedText = textDeltas.map((d) => (d.data['delta'] as any).text as string).join(''); + expect(streamedText).not.toContain('HALT'); + expect(streamedText).not.toContain('tail'); + + const msgDelta = events.find((e) => e.event === 'message_delta'); + expect(msgDelta).toBeDefined(); + expect((msgDelta!.data['delta'] as any).stop_reason).toBe('stop_sequence'); + expect((msgDelta!.data['delta'] as any).stop_sequence).toBe('HALT'); + }); + + it('streaming: suppresses the tool_use block when a stop matches in recovered terminal text', async () => { + // Finding C: tools enabled, streamed "prefix ", native done text + // "prefix HALT tail" plus one ok tool call. The stop is found only in + // the recovered terminal text, so the tool call must be suppressed and + // stop_reason must be stop_sequence — a streamed response must never + // carry both a tool_use block and stop_sequence. Matches non-streaming. + const registry = new ModelRegistry(); + const streamEvents = [ + { text: 'prefix ', done: false, isReasoning: false }, + { + text: 'prefix HALT tail', + done: true, + finishReason: 'stop', + toolCalls: [{ status: 'ok', id: 'call_w', name: 'get_weather', arguments: '{"location":"NYC"}' }], + thinking: null, + numTokens: 10, + promptTokens: 5, + reasoningTokens: 0, + rawText: 'prefix HALT tail', + }, + ]; + registry.register('test-model', createMockStreamModel(streamEvents)); + const { res, getBody } = createMockRes(); + + await handleCreateMessage( + res, + { + model: 'test-model', + messages: [{ role: 'user', content: 'go' }], + max_tokens: 100, + stream: true, + stop_sequences: ['HALT'], + tools: [{ name: 'get_weather', input_schema: { type: 'object', properties: {} } }], + }, + registry, + ); + + const events = parseSSE(getBody()); + const textDeltas = events.filter( + (e) => e.event === 'content_block_delta' && (e.data['delta'] as any).type === 'text_delta', + ); + const streamedText = textDeltas.map((d) => (d.data['delta'] as any).text as string).join(''); + expect(streamedText).not.toContain('HALT'); + + const toolBlock = events.find( + (e) => e.event === 'content_block_start' && (e.data['content_block'] as any).type === 'tool_use', + ); + expect(toolBlock).toBeUndefined(); + + const msgDelta = events.find((e) => e.event === 'message_delta'); + expect(msgDelta).toBeDefined(); + expect((msgDelta!.data['delta'] as any).stop_reason).toBe('stop_sequence'); + expect((msgDelta!.data['delta'] as any).stop_sequence).toBe('HALT'); + }); + + it('streaming: suppresses the tool_use block when a stop matches in tag-suppressed recovered text', async () => { + // Finding C (tag-suppression variant): the model emits "prefix ", a + // suppressed , then "HALT tail". The native cleaned done text + // "prefix HALT tail" surfaces the stop. Tools must be suppressed and the + // terminal must be stop_sequence — never both. + const registry = new ModelRegistry(); + const streamEvents = [ + { text: 'prefix {"name":"get_weather"}HALT tail', done: false, isReasoning: false }, + { + text: 'prefix HALT tail', + done: true, + finishReason: 'stop', + toolCalls: [{ status: 'ok', id: 'call_w', name: 'get_weather', arguments: '{"location":"NYC"}' }], + thinking: null, + numTokens: 10, + promptTokens: 5, + reasoningTokens: 0, + rawText: 'prefix {"name":"get_weather"}HALT tail', + }, + ]; + registry.register('test-model', createMockStreamModel(streamEvents)); + const { res, getBody } = createMockRes(); + + await handleCreateMessage( + res, + { + model: 'test-model', + messages: [{ role: 'user', content: 'go' }], + max_tokens: 100, + stream: true, + stop_sequences: ['HALT'], + tools: [{ name: 'get_weather', input_schema: { type: 'object', properties: {} } }], + }, + registry, + ); + + const events = parseSSE(getBody()); + const textDeltas = events.filter( + (e) => e.event === 'content_block_delta' && (e.data['delta'] as any).type === 'text_delta', + ); + const streamedText = textDeltas.map((d) => (d.data['delta'] as any).text as string).join(''); + expect(streamedText).not.toContain('HALT'); + + const toolBlock = events.find( + (e) => e.event === 'content_block_start' && (e.data['content_block'] as any).type === 'tool_use', + ); + expect(toolBlock).toBeUndefined(); + + const msgDelta = events.find((e) => e.event === 'message_delta'); + expect(msgDelta).toBeDefined(); + expect((msgDelta!.data['delta'] as any).stop_reason).toBe('stop_sequence'); + expect((msgDelta!.data['delta'] as any).stop_sequence).toBe('HALT'); + }); + + it('streaming: counts parked leading whitespace in the terminal overlap so a held stop never replays it', async () => { + // Wave #3 regression: the streamed delta " H" parks " " in + // `pendingLeadingWhitespace` (whitespace-only, no block open yet) and holds + // "H" in the stop detector (prefix of "HALT"). The native done text + // " HALTtail" carries the full visible text. The terminal overlap basis must + // include the parked " " so the recovered tail is only "ALTtail" (which + // completes the held "HALT") instead of the whole " HALTtail" — otherwise the + // already-received " H" gets replayed and the "H" of the stop leaks. Correct: + // emit exactly the pre-stop whitespace " ", matching the non-streaming result. + const registry = new ModelRegistry(); + const streamEvents = [ + { text: ' H', done: false, isReasoning: false }, + { + text: ' HALTtail', + done: true, + finishReason: 'stop', + toolCalls: [], + thinking: null, + numTokens: 8, + promptTokens: 4, + reasoningTokens: 0, + rawText: ' HALTtail', + }, + ]; + registry.register('test-model', createMockStreamModel(streamEvents)); + const { res, getBody } = createMockRes(); + + await handleCreateMessage( + res, + { + model: 'test-model', + messages: [{ role: 'user', content: 'go' }], + max_tokens: 100, + stream: true, + stop_sequences: ['HALT'], + }, + registry, + ); + + const events = parseSSE(getBody()); + const textDeltas = events.filter( + (e) => e.event === 'content_block_delta' && (e.data['delta'] as any).type === 'text_delta', + ); + const streamedText = textDeltas.map((d) => (d.data['delta'] as any).text as string).join(''); + expect(streamedText).toBe(' '); + expect(streamedText).not.toContain('H'); + expect(streamedText).not.toContain('HALT'); + expect(streamedText).not.toContain('tail'); + + const msgDelta = events.find((e) => e.event === 'message_delta'); + expect((msgDelta!.data['delta'] as any).stop_reason).toBe('stop_sequence'); + expect((msgDelta!.data['delta'] as any).stop_sequence).toBe('HALT'); + }); + + it('streaming: no-op with no stop_sequences never replays parked whitespace + tag residue', async () => { + // Wave #3 regression (C8 no-op): with NO stop_sequences the streamed delta + // " <" parks " " in `pendingLeadingWhitespace` and holds "<" in the tag + // buffer (possible tag start). The native done text " e.event === 'content_block_delta' && (e.data['delta'] as any).type === 'text_delta', + ); + const streamedText = textDeltas.map((d) => (d.data['delta'] as any).text as string).join(''); + expect(streamedText).toBe(' e.event === 'message_delta'); + expect((msgDelta!.data['delta'] as any).stop_reason).toBe('end_turn'); + expect((msgDelta!.data['delta'] as any).stop_sequence).toBe(null); + }); + + it('streaming: no-op control recovers only the unsent suffix after parked whitespace (no stop_sequences)', async () => { + // Wave #3 control: with NO stop_sequences the streamed delta " H" opens a + // text block immediately (non-whitespace), and the native done text " Htail" + // recovers only the unsent "tail". The combined stream is exactly " Htail" — + // byte-identical to the no-stop behavior, no duplication, no drop. + const registry = new ModelRegistry(); + const streamEvents = [ + { text: ' H', done: false, isReasoning: false }, + { + text: ' Htail', + done: true, + finishReason: 'stop', + toolCalls: [], + thinking: null, + numTokens: 6, + promptTokens: 3, + reasoningTokens: 0, + rawText: ' Htail', + }, + ]; + registry.register('test-model', createMockStreamModel(streamEvents)); + const { res, getBody } = createMockRes(); + + await handleCreateMessage( + res, + { + model: 'test-model', + messages: [{ role: 'user', content: 'go' }], + max_tokens: 100, + stream: true, + }, + registry, + ); + + const events = parseSSE(getBody()); + const textDeltas = events.filter( + (e) => e.event === 'content_block_delta' && (e.data['delta'] as any).type === 'text_delta', + ); + const streamedText = textDeltas.map((d) => (d.data['delta'] as any).text as string).join(''); + expect(streamedText).toBe(' Htail'); + + const msgDelta = events.find((e) => e.event === 'message_delta'); + expect((msgDelta!.data['delta'] as any).stop_reason).toBe('end_turn'); + expect((msgDelta!.data['delta'] as any).stop_sequence).toBe(null); + }); + }); + // ----------------------------------------------------------------------- // Error handling // ----------------------------------------------------------------------- diff --git a/__test__/server/presets.test.ts b/__test__/server/presets.test.ts new file mode 100644 index 000000000..ea41921ab --- /dev/null +++ b/__test__/server/presets.test.ts @@ -0,0 +1,177 @@ +/** + * Coverage for the launch-preset anti-repetition cutoff values. + * + * A live Claude Code session on Ornith (qwen3_5_moe) truncated an + * ASCII box mid-diagram: a run of repeated `─` tripped the native + * sampler's consecutive-token cutoff (`max_consecutive_tokens` + * default 16) → finish_reason `"repetition"` → `stop_reason:"end_turn"`. + * The Qwen launch presets loosen the cutoff so legit repeated content + * (box borders, separators, tables) survives while a true runaway loop + * still stops. + * + * The merge-survival tests drive the REAL `ChatSession.mergeConfig` + * through `ModelRegistry.register({ samplingDefaults }) → getOrCreate → + * session.send`, observing the merged ChatConfig the session forwards + * to `chatSessionStart` (a session-capable mock — no native model). + */ + +import type { ChatConfig, ChatMessage, ChatResult, ToolCallResult } from '@mlx-node/core'; +import type { SessionCapableModel } from '@mlx-node/lm'; +import { + GEMMA4_SAMPLING_DEFAULTS, + LAUNCH_PRESETS, + LFM2_SAMPLING_DEFAULTS, + ModelRegistry, + QWEN_SAMPLING_DEFAULTS, +} from '@mlx-node/server'; +import { describe, expect, it, vi } from 'vite-plus/test'; + +// --------------------------------------------------------------------------- +// Mock model (mirrors sampling-defaults.test.ts): captures the merged +// ChatConfig that ChatSession passes down to chatSessionStart. +// --------------------------------------------------------------------------- + +interface CapturingModel extends SessionCapableModel { + lastStartConfig: ChatConfig | null | undefined; + startCallCount: number; +} + +function createCapturingModel(): CapturingModel { + const emptyResult: ChatResult = { + text: '', + toolCalls: [] as ToolCallResult[], + thinking: null, + numTokens: 0, + promptTokens: 0, + reasoningTokens: 0, + finishReason: 'stop', + rawText: '', + } as unknown as ChatResult; + + // eslint-disable-next-line @typescript-eslint/require-await + async function* emptyStream(): AsyncGenerator> { + yield { done: true, text: '', finishReason: 'stop', toolCalls: [], numTokens: 0, promptTokens: 0 }; + } + + const model = { + lastStartConfig: undefined as ChatConfig | null | undefined, + startCallCount: 0, + chatSessionStart: vi.fn(async (_messages: ChatMessage[], config?: ChatConfig | null) => { + model.lastStartConfig = config; + model.startCallCount += 1; + return emptyResult; + }), + chatSessionContinue: vi.fn().mockResolvedValue(emptyResult), + chatSessionContinueTool: vi.fn().mockResolvedValue(emptyResult), + chatStreamSessionStart: vi.fn(() => emptyStream()), + chatStreamSessionContinue: vi.fn(() => emptyStream()), + chatStreamSessionContinueTool: vi.fn(() => emptyStream()), + resetCaches: vi.fn(), + }; + return model as unknown as CapturingModel; +} + +// --------------------------------------------------------------------------- +// Static preset-value assertions +// --------------------------------------------------------------------------- + +const LOOSENED_CUTOFF = { + maxConsecutiveTokens: 256, + maxNgramRepeats: 8, + ngramSize: 64, +} as const; + +describe('QWEN launch-preset anti-repetition cutoff', () => { + it('loosens the cutoff on every Qwen launch entry (qwen3 / qwen3_5 / qwen3_5_moe)', () => { + for (const key of ['qwen3', 'qwen3_5', 'qwen3_5_moe'] as const) { + const sampling = LAUNCH_PRESETS[key]!.sampling; + expect(sampling.maxConsecutiveTokens).toBe(256); + expect(sampling.maxNgramRepeats).toBe(8); + expect(sampling.ngramSize).toBe(64); + } + }); + + it('sets the loosened cutoff on all four QWEN_SAMPLING_DEFAULTS variants', () => { + for (const key of ['thinkingCoding', 'thinkingGeneral', 'instructGeneral', 'instructReasoning'] as const) { + const sampling = QWEN_SAMPLING_DEFAULTS[key]; + expect(sampling.maxConsecutiveTokens).toBe(256); + expect(sampling.maxNgramRepeats).toBe(8); + expect(sampling.ngramSize).toBe(64); + } + }); + + it('does not disable the cutoff (runaway-loop protection must remain)', () => { + for (const key of ['thinkingCoding', 'thinkingGeneral', 'instructGeneral', 'instructReasoning'] as const) { + const sampling = QWEN_SAMPLING_DEFAULTS[key]; + expect(sampling.maxConsecutiveTokens).not.toBe(0); + expect(sampling.maxNgramRepeats).not.toBe(0); + } + }); + + it('leaves the existing sampling knobs intact (thinkingCoding spec)', () => { + const s = QWEN_SAMPLING_DEFAULTS.thinkingCoding; + expect(s.temperature).toBe(0.6); + expect(s.topP).toBe(0.95); + expect(s.topK).toBe(20); + expect(s.minP).toBe(0); + expect(s.presencePenalty).toBe(0); + expect(s.repetitionPenalty).toBe(1); + }); +}); + +describe('scope lock: non-Qwen presets unchanged', () => { + it('does not set the cutoff on the Gemma4 preset', () => { + expect(GEMMA4_SAMPLING_DEFAULTS.maxConsecutiveTokens).toBeUndefined(); + expect(GEMMA4_SAMPLING_DEFAULTS.maxNgramRepeats).toBeUndefined(); + expect(GEMMA4_SAMPLING_DEFAULTS.ngramSize).toBeUndefined(); + expect(LAUNCH_PRESETS.gemma4!.sampling.maxConsecutiveTokens).toBeUndefined(); + }); + + it('does not set the cutoff on the LFM2 preset', () => { + expect(LFM2_SAMPLING_DEFAULTS.maxConsecutiveTokens).toBeUndefined(); + expect(LFM2_SAMPLING_DEFAULTS.maxNgramRepeats).toBeUndefined(); + expect(LFM2_SAMPLING_DEFAULTS.ngramSize).toBeUndefined(); + expect(LAUNCH_PRESETS.lfm2!.sampling.maxConsecutiveTokens).toBeUndefined(); + }); +}); + +// --------------------------------------------------------------------------- +// Merge survival (real ChatSession.mergeConfig) +// --------------------------------------------------------------------------- + +describe('cutoff survives ChatSession.mergeConfig', () => { + it('carries the loosened cutoff through when the per-request overlay omits it', async () => { + const registry = new ModelRegistry(); + const model = createCapturingModel(); + registry.register('qwen35moe', model, { samplingDefaults: LAUNCH_PRESETS.qwen3_5_moe!.sampling }); + + const sessionReg = registry.getSessionRegistry('qwen35moe')!; + const { session } = sessionReg.getOrCreate(null, null); + + // Claude Code never sends these fields — overlay omits them. + await session.send('hi'); + + expect(model.lastStartConfig).toMatchObject(LOOSENED_CUTOFF); + // Full shape: defaults + reuseCache, nothing else injected. + expect(model.lastStartConfig).toEqual({ + ...LAUNCH_PRESETS.qwen3_5_moe!.sampling, + reuseCache: true, + }); + }); + + it('lets a per-request override win on maxConsecutiveTokens while the other cutoff fields survive', async () => { + const registry = new ModelRegistry(); + const model = createCapturingModel(); + registry.register('qwen35moe', model, { samplingDefaults: LAUNCH_PRESETS.qwen3_5_moe!.sampling }); + + const sessionReg = registry.getSessionRegistry('qwen35moe')!; + const { session } = sessionReg.getOrCreate(null, null); + + await session.send('hi', { config: { maxConsecutiveTokens: 32 } }); + + expect(model.lastStartConfig!.maxConsecutiveTokens).toBe(32); + // Default n-gram fields still carried (overlay didn't touch them). + expect(model.lastStartConfig!.maxNgramRepeats).toBe(8); + expect(model.lastStartConfig!.ngramSize).toBe(64); + }); +}); diff --git a/__test__/server/stop-sequence-buffer.test.ts b/__test__/server/stop-sequence-buffer.test.ts new file mode 100644 index 000000000..06261d5e1 --- /dev/null +++ b/__test__/server/stop-sequence-buffer.test.ts @@ -0,0 +1,144 @@ +import { describe, expect, it } from 'vite-plus/test'; + +import { StopSequenceBuffer } from '../../packages/server/src/stop-sequence-buffer.js'; + +describe('StopSequenceBuffer', () => { + it('passes text through transparently when no stop sequences are configured', () => { + const buffer = new StopSequenceBuffer([]); + + expect(buffer.push('abc')).toEqual({ safeText: 'abc', matched: null }); + expect(buffer.push('def')).toEqual({ safeText: 'def', matched: null }); + expect(buffer.flush()).toEqual({ safeText: '', matched: null }); + expect(buffer.matched).toBeNull(); + }); + + it('treats only-empty stop sequences as no configuration', () => { + const buffer = new StopSequenceBuffer(['', '']); + + expect(buffer.push('anything')).toEqual({ safeText: 'anything', matched: null }); + expect(buffer.flush()).toEqual({ safeText: '', matched: null }); + }); + + it('detects a whole stop sequence within a single push and suppresses the rest', () => { + const buffer = new StopSequenceBuffer(['HALT']); + + expect(buffer.push('abcHALTxyz')).toEqual({ safeText: 'abc', matched: 'HALT' }); + expect(buffer.push('more')).toEqual({ safeText: '', matched: 'HALT' }); + expect(buffer.matched).toBe('HALT'); + }); + + it('detects a stop sequence split across two pushes', () => { + const buffer = new StopSequenceBuffer(['HALT']); + + expect(buffer.push('abcHAL')).toEqual({ safeText: 'abc', matched: null }); + expect(buffer.push('Tyz')).toEqual({ safeText: '', matched: 'HALT' }); + expect(buffer.matched).toBe('HALT'); + }); + + it('releases a false partial that never completes a stop sequence', () => { + const buffer = new StopSequenceBuffer(['HALT']); + + expect(buffer.push('abHAL')).toEqual({ safeText: 'ab', matched: null }); + expect(buffer.push('xz')).toEqual({ safeText: 'HALxz', matched: null }); + expect(buffer.flush()).toEqual({ safeText: '', matched: null }); + expect(buffer.matched).toBeNull(); + }); + + it('detects a stop sequence at the very start of the text', () => { + const buffer = new StopSequenceBuffer(['STOP']); + + expect(buffer.push('STOPnow')).toEqual({ safeText: '', matched: 'STOP' }); + }); + + it('matches the earliest stop sequence when multiple are present', () => { + const buffer = new StopSequenceBuffer(['END', 'HALT']); + + expect(buffer.push('aHALTbENDc')).toEqual({ safeText: 'a', matched: 'HALT' }); + }); + + it('prefers the longest stop sequence on a tie at the same index', () => { + const buffer = new StopSequenceBuffer(['ab', 'abc']); + + expect(buffer.push('xabc')).toEqual({ safeText: 'x', matched: 'abc' }); + }); + + it('never emits a held-back partial suffix prematurely', () => { + const buffer = new StopSequenceBuffer(['STOP']); + + // "ST" could begin "STOP", so it must be withheld, not emitted. + const first = buffer.push('abcST'); + expect(first.safeText).toBe('abc'); + expect(first.safeText).not.toContain('ST'); + expect(first.matched).toBeNull(); + + // The withheld suffix is only released by flush when it cannot complete. + expect(buffer.flush()).toEqual({ safeText: 'ST', matched: null }); + }); + + it('treats whitespace-only stop sequences as no configuration', () => { + const buffer = new StopSequenceBuffer([' ', '\n', '\t']); + + expect(buffer.push('one two\nthree')).toEqual({ safeText: 'one two\nthree', matched: null }); + expect(buffer.flush()).toEqual({ safeText: '', matched: null }); + expect(buffer.matched).toBeNull(); + }); + + it('holds a tail match when a longer same-index stop could still complete on a later push', () => { + const buffer = new StopSequenceBuffer(['ab', 'abc']); + + // "ab" is complete at the tail, but "abc" (same start index) is still + // viable, so the match must be held rather than committed early. + expect(buffer.push('xab')).toEqual({ safeText: 'x', matched: null }); + // The next push completes the longer stop, which wins on the tie. + expect(buffer.push('c tail')).toEqual({ safeText: '', matched: 'abc' }); + expect(buffer.matched).toBe('abc'); + }); + + it('commits the short stop when the longer same-index stop is broken on a later push', () => { + const buffer = new StopSequenceBuffer(['ab', 'abc']); + + expect(buffer.push('xab')).toEqual({ safeText: 'x', matched: null }); + // No "c" arrives, so the longer "abc" can never complete and "ab" wins. + expect(buffer.push(' x')).toEqual({ safeText: '', matched: 'ab' }); + expect(buffer.matched).toBe('ab'); + }); + + it('resolves a held tail match at flush when no longer stop completes', () => { + const buffer = new StopSequenceBuffer(['ab', 'abc']); + + expect(buffer.push('xab')).toEqual({ safeText: 'x', matched: null }); + // Stream ends with only "ab" present; the held match resolves to "ab". + expect(buffer.flush()).toEqual({ safeText: '', matched: 'ab' }); + expect(buffer.matched).toBe('ab'); + }); + + it('holds a short tail match while an EARLIER-index longer stop could still complete', () => { + // Finding A: "xab" contains the short stop "ab" at index 1, but it is also + // a viable prefix of the longer stop "xabc" beginning at the EARLIER index + // 0. Earliest-match-wins means the short "ab" must be HELD (not committed) + // until the earlier "xabc" either completes or is broken. + const buffer = new StopSequenceBuffer(['xabc', 'ab']); + + expect(buffer.push('xab')).toEqual({ safeText: '', matched: null }); + // The next char completes the earlier/longer "xabc", which wins. + expect(buffer.push('c')).toEqual({ safeText: '', matched: 'xabc' }); + expect(buffer.matched).toBe('xabc'); + }); + + it('commits the short stop when the earlier longer stop can no longer complete', () => { + const buffer = new StopSequenceBuffer(['xabc', 'ab']); + + expect(buffer.push('xab')).toEqual({ safeText: '', matched: null }); + // "z" breaks "xabc" (it needed "c"); the short "ab" at index 1 commits + // promptly, emitting the safe "x" before it — no hang, no dropped text. + expect(buffer.push('z')).toEqual({ safeText: 'x', matched: 'ab' }); + expect(buffer.matched).toBe('ab'); + }); + + it('matches the earlier longer stop in a single chunk', () => { + const buffer = new StopSequenceBuffer(['xabc', 'ab']); + + expect(buffer.push('xabc')).toEqual({ safeText: '', matched: 'xabc' }); + expect(buffer.matched).toBe('xabc'); + }); +}); diff --git a/packages/server/src/endpoints/messages.ts b/packages/server/src/endpoints/messages.ts index 5a2af835f..0324d009d 100644 --- a/packages/server/src/endpoints/messages.ts +++ b/packages/server/src/endpoints/messages.ts @@ -88,6 +88,7 @@ import { genId } from '../mappers/response.js'; import type { ModelWorkCoordinator } from '../model-work-coordinator.js'; import type { ModelRegistry } from '../registry.js'; import { QueueFullError, type SessionRegistry } from '../session-registry.js'; +import { StopSequenceBuffer } from '../stop-sequence-buffer.js'; import { beginSSE, endSSE, writeSSEEvent } from '../streaming.js'; import { longestSuffixPrefixOverlap } from '../text-recovery.js'; import { resolveServerTuningForUsage, type ServerTimingForUsage } from '../timing.js'; @@ -206,9 +207,48 @@ async function handleNonStreaming( result: ChatResult, body: AnthropicMessagesRequest, visibility: TransportVisibility, + stopSequences: string[], serverTiming?: ServerTimingForUsage, ): Promise { const messageId = genId('msg_'); + + // Honor client-supplied `stop_sequences`: scan the SAME visible text the + // response builder will emit for the earliest configured stop string. When + // the request disallows tools but the parser still produced a tool call and + // `result.text` is empty, `buildAnthropicContent` emits the recovered + // suppressed-tool text — so the scan must mirror that recovery gate and run + // over the recovered text, not the empty `result.text`. The scan does + // push+flush so a complete stop that `push()` held back (a longer + // overlapping stop was still viable) is resolved at end-of-text, matching + // the streaming done-path. On a match we truncate the text the response is + // built from at the match (dropping the stop string and everything after it) + // and report `stop_reason: 'stop_sequence'` + `stop_sequence: ''`; + // `buildAnthropicResponse` then suppresses tool calls and the recovery + // branch and emits the truncated text verbatim. The native `ChatResult` is + // left untouched. With no match `responseResult` stays `result` (full text + // retained — `flush()` releases any held incomplete partial as normal text), + // so behavior is byte-identical to a request without `stop_sequences`. + const visibleText = + !requestAllowsToolUse(body) && + result.text.length === 0 && + result.toolCalls.filter((t) => t.status === 'ok').length > 0 && + containsToolCallMarkup(result.rawText) + ? recoverSuppressedToolCallText(result.rawText) + : result.text; + + let matchedStopSequence: string | null = null; + let responseResult = result; + if (stopSequences.length > 0) { + const stopBuffer = new StopSequenceBuffer(stopSequences); + const pushed = stopBuffer.push(visibleText); + const flushed = stopBuffer.flush(); + const matched = pushed.matched ?? flushed.matched; + if (matched !== null) { + matchedStopSequence = matched; + responseResult = { ...result, text: pushed.safeText + flushed.safeText }; + } + } + // `result.performance` is only populated when `reportPerformance: true` // rides on the underlying `ChatConfig`; otherwise the field is // `undefined` and the mapper elides the wire-extension fields. The @@ -216,12 +256,13 @@ async function handleNonStreaming( // by default, matching how `cachedTokens` is treated through // `buildAnthropicResponse`. const response = buildAnthropicResponse( - result, + responseResult, body, messageId, result.performance, requestAllowsToolUse(body), serverTiming, + matchedStopSequence, ); // Native `chatSession*` has no AbortSignal surface yet, so a client that @@ -256,6 +297,7 @@ async function handleStreamingNative( httpReq: IncomingMessage | undefined, visibility: TransportVisibility, emitReasoning: boolean, + stopSequences: string[], serverTiming?: ServerTimingForUsage, ): Promise { const messageId = genId('msg_'); @@ -290,6 +332,18 @@ async function handleStreamingNative( // suffix instead of a length-based slice that would chop characters. let emittedText = ''; const tagBuffer = new ToolCallTagBuffer(); + // Client-supplied `stop_sequences` detector. Feeds on the visible text that + // survives `tagBuffer` (structural-marker stripping) so it never sees tool + // markup. An empty `stopSequences` constructs a pass-through buffer + // (`push` returns its input verbatim, `flush` returns ''), so the wire is + // byte-identical to a request without `stop_sequences`. When a stop string + // matches, `matchedStopSequence` is recorded, all later visible text is + // suppressed, and the done-path emits nothing past the stop. The done-path + // also scans the terminal / recovered visible text on this SAME buffer (with + // any held partial still in place), so a stop straddling the stream/terminal + // boundary is caught with buffer continuity. + const stopBuffer = new StopSequenceBuffer(stopSequences); + let matchedStopSequence: string | null = null; // Terminal emission is deferred until after the loop drains so `wasCommitted()` // reads an authoritative `session.turns`. On a committed done chunk we emit @@ -364,47 +418,14 @@ async function handleStreamingNative( break; } - const remainingText = tagBuffer.flush(); - if (!tagBuffer.suppressed && remainingText) { - // Flush any pending leading whitespace alongside the residual - // text — once a real text block opens, its declared content - // must be byte-accurate, so the buffered whitespace is - // promoted to the head of that block. If `remainingText` - // itself is whitespace-only and there is no buffered - // whitespace, `combined.trim()` is empty and we drop both. - const combined = pendingLeadingWhitespace + remainingText; - pendingLeadingWhitespace = ''; - if (hasEmittedText || combined.trim().length > 0) { - if (!hasEmittedText) { - if (hasEmittedThinking) { - writeSSEEvent(res, 'content_block_stop', buildContentBlockStop(contentBlockIndex - 1)); - } - hasEmittedText = true; - writeSSEEvent( - res, - 'content_block_start', - buildContentBlockStart(contentBlockIndex, { - type: 'text', - text: '', - }), - ); - } - emittedText += combined; - emittedTextLength += combined.length; - writeSSEEvent( - res, - 'content_block_delta', - buildContentBlockDelta(contentBlockIndex, { - type: 'text_delta', - text: combined, - }), - ); - } - } - - if (hasEmittedThinking && !hasEmittedText) { - writeSSEEvent(res, 'content_block_stop', buildContentBlockStop(contentBlockIndex - 1)); - } + // Flush the tag buffer's residual but keep the stop buffer intact: it + // may still hold a partial from the streamed deltas, and a stop can + // straddle the boundary between that held partial, the tag residue, and + // the native terminal/recovered text. All of it runs through the SAME + // buffer, in stream order, with a single flush, BEFORE the stop match, + // the emitted text, and the tool decision are finalized. + const tagResidual = tagBuffer.flush(); + const heldPartial = stopBuffer.pending; const parsedToolCalls = event.toolCalls.filter((t) => t.status === 'ok'); if (!allowToolUse && parsedToolCalls.length > 0) { @@ -417,144 +438,139 @@ async function handleStreamingNative( containsToolCallMarkup(event.rawText) ? recoverSuppressedToolCallText(event.rawText) : event.text; - const okToolCalls = allowToolUse ? parsedToolCalls : []; - const hasToolCalls = okToolCalls.length > 0; - // Recovery: suppression triggered but no tool calls parsed — emit final text as a text block. - if (tagBuffer.suppressed && !hasToolCalls && finalText && !hasEmittedText) { - // Thinking block (if any) was already closed above. - hasEmittedText = true; - writeSSEEvent( - res, - 'content_block_start', - buildContentBlockStart(contentBlockIndex, { - type: 'text', - text: '', - }), - ); - emittedText += finalText; - emittedTextLength += finalText.length; - writeSSEEvent( - res, - 'content_block_delta', - buildContentBlockDelta(contentBlockIndex, { - type: 'text_delta', - text: finalText, - }), - ); - } else if ( - tagBuffer.suppressed && - !hasToolCalls && - finalText && - hasEmittedText && - !emittedText.includes(finalText) - ) { - // Recovery: streaming text was cut off by a false-alarm `` tag. - // - // `emittedTextLength` is an index into the streamed chunks, NOT into - // `finalText`. The native side trims leading whitespace after - // `` via `split_at_think_end`, so the prefixes can diverge: - // e.g. streamed=`"\n\n..."`, finalText=`"..."`. - // A naive `finalText.slice(emittedTextLength)` would chop `` and emit `"ool_call>\n...` → finalText=`...`. The malformed - // tag is NOT a substring of the streamed whitespace → emit - // (this is the original ` emittedText.length`) - // misclassify case (b) when the streamed whitespace is long. - const overlap = longestSuffixPrefixOverlap(emittedText, finalText); - const unsent = finalText.slice(overlap); - if (unsent) { - emittedText += unsent; - emittedTextLength += unsent.length; - writeSSEEvent( - res, - 'content_block_delta', - buildContentBlockDelta(contentBlockIndex, { - type: 'text_delta', - text: unsent, - }), - ); + // The visible text the stream already RECEIVED, in stream order: what + // reached the wire (`emittedText`), the parked leading whitespace the + // detector already cleared but no block has shown yet + // (`pendingLeadingWhitespace`), the still-held detector partial + // (`heldPartial`), and the tag residue about to be scanned + // (`tagResidual`). Every parked/held byte is counted exactly once so the + // recovered terminal text is only the suffix of `finalText` the stream + // has not already accounted for — omitting the parked whitespace would + // make a full-text `finalText` look entirely unsent and replay the + // already-received prefix. + const streamedReceived = emittedText + pendingLeadingWhitespace + heldPartial + tagResidual; + let recoveredTail = ''; + if (finalText) { + if (!hasEmittedText && heldPartial.length === 0 && tagResidual.length === 0) { + // Nothing was streamed or held: the whole `finalText` is terminal. + recoveredTail = finalText; + } else if (!streamedReceived.includes(finalText)) { + // `finalText` extends past what the stream produced: recover the + // suffix beyond the longest overlap. The `includes` guard skips the + // duplicate-trim case where `finalText` is a substring of the + // received text (native `.trim()` / post-`` shrinkage). + recoveredTail = finalText.slice(longestSuffixPrefixOverlap(streamedReceived, finalText)); } - } else if (hasEmittedText && finalText && !emittedText.includes(finalText)) { - // Emit any unsent suffix when final text extends past what was - // streamed. Same divergence concern as above (post- trim - // can leave `emittedText` longer than the matching prefix of - // `finalText`), so we use the same overlap-based slice instead of - // a length-based one. When the overlap covers all of `finalText` - // (i.e. nothing more to emit) `unsent` is empty and we skip. - // - // The `!emittedText.includes(finalText)` guard skips the - // duplicate-trim case where finalText is a substring of the - // streamed text (e.g. native `.trim()` shrinkage). See the - // companion comment above for the case-distinction rationale. - const overlap = longestSuffixPrefixOverlap(emittedText, finalText); - const unsent = finalText.slice(overlap); - if (unsent) { - emittedText += unsent; - emittedTextLength += unsent.length; + } + + // One continuous scan — held partial (already buffered) + tag residue + + // recovered tail, in stream order, with a single flush at the end. A + // stop matched anywhere here is caught with buffer continuity, and + // `matchedStopSequence` is finalized before tool emission and + // `stop_reason`. With an empty `stopSequences` the buffer is a + // pass-through, so `terminalVisible` equals the released text verbatim. + let terminalVisible = ''; + for (const segment of [tagResidual, recoveredTail]) { + const pushed = stopBuffer.push(segment); + if (pushed.matched !== null) { + matchedStopSequence = pushed.matched; + } + terminalVisible += pushed.safeText; + } + const flushed = stopBuffer.flush(); + if (flushed.matched !== null) { + matchedStopSequence = flushed.matched; + } + terminalVisible += flushed.safeText; + + // Parked leading whitespace belongs in front of RELEASED held content + // (a stop-buffer partial or tag residue). When the terminal text is + // purely native `finalText` recovery, `finalText` already carries that + // whitespace, so prepending it would double those bytes. + const prependParked = heldPartial.length > 0 || tagResidual.length > 0; + + // Close a dangling reasoning block before any terminal text block opens. + if (hasEmittedThinking && !hasEmittedText) { + writeSSEEvent(res, 'content_block_stop', buildContentBlockStop(contentBlockIndex - 1)); + } + + if (hasEmittedText) { + // A text block is already open from the streamed deltas: append the + // newly released terminal text (if any), then close the block. + if (terminalVisible) { + emittedText += terminalVisible; + emittedTextLength += terminalVisible.length; writeSSEEvent( res, 'content_block_delta', buildContentBlockDelta(contentBlockIndex, { type: 'text_delta', - text: unsent, + text: terminalVisible, }), ); } - } - - if (hasEmittedText) { writeSSEEvent(res, 'content_block_stop', buildContentBlockStop(contentBlockIndex)); contentBlockIndex++; pendingLeadingWhitespace = ''; - } else if (!finalText && hasToolCalls) { - // Pure tool-call turn — no text block. Drop any leading - // whitespace we had buffered before the `` tag. - pendingLeadingWhitespace = ''; - } else if (finalText) { - // All text arrived in the final event; emit it as a single - // block. `finalText` is the FULL accumulated text from the - // native side, NOT a delta — any whitespace we buffered in - // `pendingLeadingWhitespace` from intermediate whitespace-only - // deltas is already part of `finalText`, so prepending the - // buffer here would double-emit those bytes. Drop the buffer - // and emit `finalText` verbatim. - pendingLeadingWhitespace = ''; - writeSSEEvent( - res, - 'content_block_start', - buildContentBlockStart(contentBlockIndex, { - type: 'text', - text: '', - }), - ); - emittedText += finalText; - emittedTextLength += finalText.length; - writeSSEEvent( - res, - 'content_block_delta', - buildContentBlockDelta(contentBlockIndex, { - type: 'text_delta', - text: finalText, - }), - ); - writeSSEEvent(res, 'content_block_stop', buildContentBlockStop(contentBlockIndex)); - contentBlockIndex++; } else { - // No text emission at all this turn — drop the buffer. + // No text block open yet. Build the block body from the terminal text + // (fronted by parked whitespace only when it precedes released held + // content), or from the parked whitespace alone when a stop truncated + // the turn right after it. + const body = terminalVisible + ? prependParked + ? pendingLeadingWhitespace + terminalVisible + : terminalVisible + : matchedStopSequence !== null + ? pendingLeadingWhitespace + : ''; pendingLeadingWhitespace = ''; + // Open a text block for non-whitespace content, for a stop-truncated + // prefix (so the streamed body equals the non-streaming one), or for + // pure native `finalText` recovery (mirrors emitting recovered text + // verbatim). A stop-matched turn always opens a text block — empty if + // the stop consumed all visible output — so the reconstructed content + // matches the non-streaming `[{type:'text', text:''}]`. Whitespace-only + // released held content opens no block. + const openTextBlock = + matchedStopSequence !== null || (body.length > 0 && (body.trim().length > 0 || !prependParked)); + if (openTextBlock) { + hasEmittedText = true; + writeSSEEvent( + res, + 'content_block_start', + buildContentBlockStart(contentBlockIndex, { + type: 'text', + text: '', + }), + ); + if (body.length > 0) { + emittedText += body; + emittedTextLength += body.length; + writeSSEEvent( + res, + 'content_block_delta', + buildContentBlockDelta(contentBlockIndex, { + type: 'text_delta', + text: body, + }), + ); + } + writeSSEEvent(res, 'content_block_stop', buildContentBlockStop(contentBlockIndex)); + contentBlockIndex++; + } } + // Decide tool emission only AFTER the full terminal stop scan: when a + // stop matched ANYWHERE (the pre-tool visible text OR the + // terminal/recovered text) the tool calls are suppressed so a streamed + // turn never carries both a tool_use block and + // `stop_reason: 'stop_sequence'`. This keeps streaming in lockstep with + // the non-streaming path (`buildAnthropicResponse`). + const okToolCalls = allowToolUse && matchedStopSequence === null ? parsedToolCalls : []; + const hasToolCalls = okToolCalls.length > 0; + for (const tc of okToolCalls) { // Translate native `call_` ids (minted by the Rust parser, // which keeps the OpenAI Responses convention) into the @@ -593,7 +609,7 @@ async function handleStreamingNative( // Capture terminal state and break — actual `message_delta` / `message_stop` / // `error` emission is deferred until after the loop so `wasCommitted()` reads // an authoritative `session.turns` (the producer's finally runs on break). - terminalStopReason = mapStopReason(event.finishReason, hasToolCalls); + terminalStopReason = mapStopReason(event.finishReason, hasToolCalls, matchedStopSequence); terminalNumTokens = event.numTokens; terminalPromptTokens = event.promptTokens; terminalCachedTokens = event.cachedTokens; @@ -633,20 +649,41 @@ async function handleStreamingNative( // markers are transport structure, not user-visible text. const { safeText, tagFound, cleanPrefix } = tagBuffer.push(event.text); if (tagFound) { - // A structural tag (`` etc.) follows. When the - // chunk has visible text BEFORE the tag (`cleanPrefix.trim()` - // non-empty), the buffered leading whitespace semantically - // belongs to that visible text — they were one logical text - // run that just happened to land split across deltas. Combine - // them and emit as a single text_delta so the wire preserves - // exactly what the model produced. When the chunk is a pure - // tag transition (no visible prefix), the buffered whitespace - // is dropped so the wire never carries a stray - // whitespace-only `content_block_start`/`_stop` pair before - // the tool_use frame. - if (cleanPrefix.trim()) { - const combined = pendingLeadingWhitespace + cleanPrefix; - pendingLeadingWhitespace = ''; + // A structural tag (`` etc.) follows, so the visible + // text before it terminates here. Only `cleanPrefix` is fresh + // model text — route it through the stop-sequence detector so a + // configured stop string landing in it (e.g. "...HALT " right + // before a ``) is honored, not leaked. Do NOT flush the + // detector here: a held partial (e.g. "HA" of "HALT") must stay + // buffered, because the native cleaned done text can reconstitute + // the bytes that followed the suppressed tag and complete the stop + // across that boundary. The done-path scans the held partial + // together with the terminal/recovered text and resolves it — + // releasing it as visible text if it cannot complete, or suppressing + // it if it does. `pendingLeadingWhitespace` is whitespace the + // detector already cleared on an earlier delta (held back only + // because no text block was open yet), so it is prepended OUTSIDE + // the buffer: re-pushing it would double-scan it AND, because the + // buffer queues it after any held partial, invert stream order + // (e.g. held "H" + buffered " " -> "H ") or forge a false match. On + // a match `matchedStopSequence` is recorded so the terminal reports + // `stop_sequence`. With an empty `stopSequences` the detector is a + // pass-through, so `visibleText === pendingLeadingWhitespace + + // cleanPrefix` and the wire is byte-identical to today. + const stopPushed = stopBuffer.push(cleanPrefix); + if (stopPushed.matched !== null) { + matchedStopSequence = stopPushed.matched; + } + const visibleText = pendingLeadingWhitespace + stopPushed.safeText; + // Mirror the original `cleanPrefix.trim()` gate, now on the + // detector's safe text: emit only when there is non-whitespace to + // show, so a pure-whitespace prefix never ratifies a stray + // whitespace-only text block before the tool_use frame. When the + // safe text is whitespace-only (e.g. the detector is still holding a + // partial), KEEP it parked so the done-path can join it with + // whatever the held partial releases — clearing it here would drop + // it before that text block opens. + if (visibleText.trim().length > 0) { if (!hasEmittedText) { if (hasEmittedThinking) { writeSSEEvent(res, 'content_block_stop', buildContentBlockStop(contentBlockIndex - 1)); @@ -661,69 +698,84 @@ async function handleStreamingNative( }), ); } - emittedText += combined; - emittedTextLength += combined.length; + pendingLeadingWhitespace = ''; + emittedText += visibleText; + emittedTextLength += visibleText.length; writeSSEEvent( res, 'content_block_delta', buildContentBlockDelta(contentBlockIndex, { type: 'text_delta', - text: combined, + text: visibleText, }), ); } else { - pendingLeadingWhitespace = ''; + pendingLeadingWhitespace = hasEmittedText ? '' : visibleText; } } else if (safeText) { - if (!hasEmittedText) { - // Hold back leading whitespace-only text so a `\n\n` emitted - // right before a `` tag never gets ratified into a - // standalone text content block. We can't open the block now - // because we don't yet know whether the next event is a real - // text delta (in which case the buffered prefix is flushed - // together with it) or a structural tag (in which case the - // buffer is dropped silently at tag-found / done time). When - // any non-whitespace arrives we ratify the block exactly - // once with `pendingLeadingWhitespace + safeText`. - const combined = pendingLeadingWhitespace + safeText; - if (combined.trim().length === 0) { - pendingLeadingWhitespace = combined; - } else { - if (hasEmittedThinking) { - writeSSEEvent(res, 'content_block_stop', buildContentBlockStop(contentBlockIndex - 1)); + // Run the tag-buffer's visible text through the stop-sequence + // detector. `visibleText` is what survives suppression: the buffer + // holds back a trailing suffix that could be the start of a stop + // string (released on a later push or at flush), and once a full + // stop string matches it returns empty `safeText` for the rest of + // the stream. We record the match and keep consuming so the native + // `done` chunk still fires the commit gate and history commit. + const stopResult = stopBuffer.push(safeText); + if (stopResult.matched !== null) { + matchedStopSequence = stopResult.matched; + } + const visibleText = stopResult.safeText; + if (visibleText) { + if (!hasEmittedText) { + // Hold back leading whitespace-only text so a `\n\n` emitted + // right before a `` tag never gets ratified into a + // standalone text content block. We can't open the block now + // because we don't yet know whether the next event is a real + // text delta (in which case the buffered prefix is flushed + // together with it) or a structural tag (in which case the + // buffer is dropped silently at tag-found / done time). When + // any non-whitespace arrives we ratify the block exactly + // once with `pendingLeadingWhitespace + visibleText`. + const combined = pendingLeadingWhitespace + visibleText; + if (combined.trim().length === 0) { + pendingLeadingWhitespace = combined; + } else { + if (hasEmittedThinking) { + writeSSEEvent(res, 'content_block_stop', buildContentBlockStop(contentBlockIndex - 1)); + } + hasEmittedText = true; + writeSSEEvent( + res, + 'content_block_start', + buildContentBlockStart(contentBlockIndex, { + type: 'text', + text: '', + }), + ); + pendingLeadingWhitespace = ''; + emittedText += combined; + emittedTextLength += combined.length; + writeSSEEvent( + res, + 'content_block_delta', + buildContentBlockDelta(contentBlockIndex, { + type: 'text_delta', + text: combined, + }), + ); } - hasEmittedText = true; - writeSSEEvent( - res, - 'content_block_start', - buildContentBlockStart(contentBlockIndex, { - type: 'text', - text: '', - }), - ); - pendingLeadingWhitespace = ''; - emittedText += combined; - emittedTextLength += combined.length; + } else { + emittedText += visibleText; + emittedTextLength += visibleText.length; writeSSEEvent( res, 'content_block_delta', buildContentBlockDelta(contentBlockIndex, { type: 'text_delta', - text: combined, + text: visibleText, }), ); } - } else { - emittedText += safeText; - emittedTextLength += safeText.length; - writeSSEEvent( - res, - 'content_block_delta', - buildContentBlockDelta(contentBlockIndex, { - type: 'text_delta', - text: safeText, - }), - ); } } } @@ -765,6 +817,7 @@ async function handleStreamingNative( terminalCachedTokens, terminalPerformance, serverTiming, + matchedStopSequence, ), ); // HTTP/1.1 chunked-encoding trailer: report the engine's cache-hit @@ -984,8 +1037,17 @@ export async function handleCreateMessage( // resolveModel and use as a cheap pre-flight gate. let mappedMessages: ChatMessage[]; let mappedConfig: ChatConfig; + // Client-supplied `stop_sequences`, normalized by the mapper (absent/empty + // dropped). Threaded into the streaming + non-streaming handlers, which own + // the detection/truncation. `ChatConfig` has no native stop field, so this + // rides alongside `config` from the mapper. + let mappedStopSequences: string[]; try { - ({ messages: mappedMessages, config: mappedConfig } = mapAnthropicRequest(body)); + ({ + messages: mappedMessages, + config: mappedConfig, + stopSequences: mappedStopSequences, + } = mapAnthropicRequest(body)); } catch (err) { sendAnthropicBadRequest(res, err instanceof Error ? err.message : 'Invalid request'); return; @@ -1123,6 +1185,7 @@ export async function handleCreateMessage( // trigger a multi-second model load just to 400 a moment later. const messages: ChatMessage[] = mappedMessages; const config: ChatConfig = mappedConfig; + const stopSequences: string[] = mappedStopSequences; // Canonicalize every assistant fan-out's trailing tool block against its // declared sibling order. Several native session backends pair tool results @@ -1354,6 +1417,7 @@ export async function handleCreateMessage( httpReq, visibility, config.includeReasoning !== false, + stopSequences, serverTiming, ); // Warm-slot adopt/drop only applies to the non-paged @@ -1429,7 +1493,7 @@ export async function handleCreateMessage( if (result.cachedTokens > 0) { res.setHeader('X-Cached-Tokens', String(result.cachedTokens)); } - await handleNonStreaming(res, result, body, visibility, serverTiming); + await handleNonStreaming(res, result, body, visibility, stopSequences, serverTiming); // Non-paged success: adopt the warm slot only when the // dispatch actually committed. Mirrors the streaming-side // dual-gate at `streamResult.ok && outcome.wasCommitted()` diff --git a/packages/server/src/mappers/anthropic-request.ts b/packages/server/src/mappers/anthropic-request.ts index 3dafbfdbe..32bdd5e3b 100644 --- a/packages/server/src/mappers/anthropic-request.ts +++ b/packages/server/src/mappers/anthropic-request.ts @@ -17,6 +17,13 @@ import { applyExtraBodyMtpOverrides } from './request.js'; export interface MappedAnthropicRequest { messages: ChatMessage[]; config: ChatConfig; + /** + * Client-supplied stop strings (Anthropic `stop_sequences`), normalized to + * drop absent/empty entries. Carried alongside `config` rather than on it + * because `ChatConfig` has no native stop field; a downstream consumer is + * responsible for honouring these. + */ + stopSequences: string[]; } /** @@ -433,18 +440,14 @@ export function mapAnthropicRequest( config.topK = req.top_k; } - // `stop_sequences` is parsed into the request type but `ChatConfig` has no - // matching field, so wiring it through to the native model would require a - // Rust change (out of scope here). Reject explicitly with a 400 — silently - // dropping the field would let a client believe its custom stop strings are - // honoured when the model continues generating right past them. An empty - // array is treated as "not present" since it carries no semantics. Future - // Rust work can add `stopSequences` to `ChatConfig` and remove this guard. - if (req.stop_sequences != null && req.stop_sequences.length > 0) { - throw new Error( - 'stop_sequences is not supported by this server. Remove the field or wait for a future release that supports it natively.', - ); - } + // `stop_sequences` has no `ChatConfig` field to map onto, so it rides out on + // the widened return instead. Normalize to drop absent/null entries, empty + // strings (which would match at every position and stop generation + // immediately), and whitespace-only entries (which would truncate normal + // output at the first space/newline; the real Anthropic API rejects these + // with a 400, so making them a no-op is the lowest-risk resolution). A + // downstream consumer honours the result. + const stopSequences = (req.stop_sequences ?? []).filter((s) => typeof s === 'string' && s.trim().length > 0); if (req.tools && req.tools.length > 0) { const toolChoice = req.tool_choice; @@ -474,5 +477,5 @@ export function mapAnthropicRequest( applyExtraBodyMtpOverrides(config, req.extra_body); - return { messages, config }; + return { messages, config, stopSequences }; } diff --git a/packages/server/src/mappers/anthropic-response.ts b/packages/server/src/mappers/anthropic-response.ts index 92c318839..c3d4433b2 100644 --- a/packages/server/src/mappers/anthropic-response.ts +++ b/packages/server/src/mappers/anthropic-response.ts @@ -52,7 +52,14 @@ export function anthropicToolUseIdToInternal(id: string): string { return id.startsWith('toolu_') ? `call_${id.slice('toolu_'.length)}` : id; } -export function mapStopReason(finishReason: string, hasToolCalls: boolean): 'end_turn' | 'max_tokens' | 'tool_use' { +export function mapStopReason( + finishReason: string, + hasToolCalls: boolean, + matchedStopSequence?: string | null, +): 'end_turn' | 'max_tokens' | 'stop_sequence' | 'tool_use' { + if (matchedStopSequence) { + return 'stop_sequence'; + } if (finishReason === 'length') { return 'max_tokens'; } @@ -83,7 +90,11 @@ export function recoverSuppressedToolCallText(rawText: string): string { .replace(//g, ''); } -export function buildAnthropicContent(result: ChatResult, allowToolUse = true): AnthropicResponseContent[] { +export function buildAnthropicContent( + result: ChatResult, + allowToolUse = true, + stopMatched = false, +): AnthropicResponseContent[] { const content: AnthropicResponseContent[] = []; if (result.thinking) { @@ -91,9 +102,18 @@ export function buildAnthropicContent(result: ChatResult, allowToolUse = true): } const parsedToolCalls = result.toolCalls.filter((t) => t.status === 'ok'); - const okToolCalls = allowToolUse ? parsedToolCalls : []; + // A matched stop sequence halts generation at its position, so any tool call + // whose tag would have followed the stop boundary is dropped and the + // truncated visible text (`result.text`, already cut at the stop) is emitted + // verbatim — the suppressed-markup recovery is skipped because it would + // re-introduce text that lived after the stop. + const okToolCalls = stopMatched || !allowToolUse ? [] : parsedToolCalls; const text = - !allowToolUse && result.text.length === 0 && parsedToolCalls.length > 0 && containsToolCallMarkup(result.rawText) + !stopMatched && + !allowToolUse && + result.text.length === 0 && + parsedToolCalls.length > 0 && + containsToolCallMarkup(result.rawText) ? recoverSuppressedToolCallText(result.rawText) : result.text; @@ -125,8 +145,13 @@ export function buildAnthropicResponse( performance?: PerformanceMetricsForUsage, allowToolUse = true, serverTiming?: ServerTimingForUsage, + matchedStopSequence?: string | null, ): AnthropicMessagesResponse { - const okToolCalls = allowToolUse ? result.toolCalls.filter((t) => t.status === 'ok') : []; + // A matched stop sequence takes precedence over tool_use: suppress the tool + // calls so `stop_reason: 'stop_sequence'` is never emitted alongside a + // tool_use block whose tag followed the stop boundary. + const stopMatched = Boolean(matchedStopSequence); + const okToolCalls = allowToolUse && !stopMatched ? result.toolCalls.filter((t) => t.status === 'ok') : []; const hasToolCalls = okToolCalls.length > 0; // Cache accounting (Anthropic Messages API spec): @@ -173,9 +198,9 @@ export function buildAnthropicResponse( type: 'message', role: 'assistant', model: req.model, - content: buildAnthropicContent(result, allowToolUse), - stop_reason: mapStopReason(result.finishReason, hasToolCalls), - stop_sequence: null, + content: buildAnthropicContent(result, allowToolUse, stopMatched), + stop_reason: mapStopReason(result.finishReason, hasToolCalls, matchedStopSequence), + stop_sequence: matchedStopSequence ?? null, usage, }; } @@ -239,6 +264,7 @@ export function buildMessageDelta( cachedTokens?: number, performance?: PerformanceMetricsForUsage, serverTiming?: ServerTimingForUsage, + stopSequence?: string | null, ): AnthropicMessageDeltaEvent { // Streaming `message_delta` mirrors the non-streaming response's // cache accounting: when `cachedTokens > 0` we emit @@ -267,7 +293,7 @@ export function buildMessageDelta( type: 'message_delta', delta: { stop_reason: stopReason, - stop_sequence: null, + stop_sequence: stopSequence ?? null, }, usage, }; diff --git a/packages/server/src/presets.ts b/packages/server/src/presets.ts index dfb9ec025..857598fb7 100644 --- a/packages/server/src/presets.ts +++ b/packages/server/src/presets.ts @@ -18,6 +18,16 @@ import type { ChatConfig } from '@mlx-node/core'; * * All modes pin `top_k = 20` and `min_p = 0.0`; they differ in * `temperature`, `top_p`, and `presence_penalty`. + * + * Every mode also loosens the native anti-repetition cutoff + * (`maxConsecutiveTokens` / `maxNgramRepeats`, default 16 / 3). Coding + * answers legitimately contain long single-token runs — ASCII box + * borders (`────`), separators (`====`/`----`), table rules, repeated + * indentation — which the default cutoff treats as a stuck loop and + * truncates mid-diagram (finish_reason `"repetition"` → `end_turn`), + * silently cutting the answer. 256 / 8 lets a wide box or table survive + * while still bounding a true runaway loop; `ngramSize` is the default, + * kept explicit. Do not set 0 — that disables runaway protection. */ export const QWEN_SAMPLING_DEFAULTS = { /** Thinking mode for precise coding tasks: temp=0.6, top_p=0.95, pp=0.0 */ @@ -28,6 +38,9 @@ export const QWEN_SAMPLING_DEFAULTS = { minP: 0.0, presencePenalty: 0.0, repetitionPenalty: 1.0, + maxConsecutiveTokens: 256, + maxNgramRepeats: 8, + ngramSize: 64, } satisfies ChatConfig, /** Thinking mode for general tasks: temp=1.0, top_p=0.95, pp=1.5 */ @@ -38,6 +51,9 @@ export const QWEN_SAMPLING_DEFAULTS = { minP: 0.0, presencePenalty: 1.5, repetitionPenalty: 1.0, + maxConsecutiveTokens: 256, + maxNgramRepeats: 8, + ngramSize: 64, } satisfies ChatConfig, /** Instruct (non-thinking) for general tasks: temp=0.7, top_p=0.8, pp=1.5 */ @@ -48,6 +64,9 @@ export const QWEN_SAMPLING_DEFAULTS = { minP: 0.0, presencePenalty: 1.5, repetitionPenalty: 1.0, + maxConsecutiveTokens: 256, + maxNgramRepeats: 8, + ngramSize: 64, } satisfies ChatConfig, /** Instruct (non-thinking) for reasoning tasks: temp=1.0, top_p=0.95, pp=1.5 */ @@ -58,6 +77,9 @@ export const QWEN_SAMPLING_DEFAULTS = { minP: 0.0, presencePenalty: 1.5, repetitionPenalty: 1.0, + maxConsecutiveTokens: 256, + maxNgramRepeats: 8, + ngramSize: 64, } satisfies ChatConfig, } as const; diff --git a/packages/server/src/stop-sequence-buffer.ts b/packages/server/src/stop-sequence-buffer.ts new file mode 100644 index 000000000..fd88721ed --- /dev/null +++ b/packages/server/src/stop-sequence-buffer.ts @@ -0,0 +1,161 @@ +/** + * Buffers streaming text to detect configured stop sequences. Text that + * cannot be part of a partial stop sequence is released immediately; a + * trailing suffix that could be the start of a stop sequence is held back + * until a later push resolves it or the stream is flushed. Once a full stop + * sequence is seen, everything after it is suppressed. + */ +export class StopSequenceBuffer { + private readonly stopSequences: string[]; + private readonly maxLength: number; + private pending_ = ''; + private _matched: string | null = null; + + constructor(stopSequences: string[]) { + // Drop empty AND whitespace-only entries: a whitespace-only stop would + // truncate normal output at the first space/newline, and the real + // Anthropic API rejects such stops outright. Mirrors the same trim filter + // in the request mapper so a whitespace-only configuration is a no-op. + this.stopSequences = stopSequences.filter((s) => s.trim().length > 0); + this.maxLength = this.stopSequences.reduce((max, s) => Math.max(max, s.length), 0); + } + + /** + * Earliest index wins; on a tie at the same index the longest wins. Returns + * `{ idx, seq }` for the winning stop, or `{ idx: -1, seq: null }` when none + * is present in `pending`. + */ + private findMatch(): { idx: number; seq: string | null } { + let matchIdx = -1; + let matchSeq: string | null = null; + for (const seq of this.stopSequences) { + const idx = this.pending_.indexOf(seq); + if (idx < 0) { + continue; + } + if (matchIdx < 0 || idx < matchIdx || (idx === matchIdx && seq.length > (matchSeq?.length ?? 0))) { + matchIdx = idx; + matchSeq = seq; + } + } + return { idx: matchIdx, seq: matchSeq }; + } + + /** The stop sequence that has matched so far, or `null` if none has. */ + get matched(): string | null { + return this._matched; + } + + /** + * The text currently held back (received but neither emitted nor matched). + * The streaming done-path reads this so it can scan the terminal/recovered + * text on the SAME buffer with the held partial still in place, and so it + * can reconstruct the full received-but-unemitted prefix for overlap math. + */ + get pending(): string { + return this.pending_; + } + + /** + * The earliest start index `j` in `[0, limit]` such that `pending.slice(j)` + * is a non-empty STRICT prefix of some configured stop — i.e. a partial that + * a later push could still grow into that stop. Returns -1 when no pending + * suffix at or before `limit` is viable. Scanning from the front yields the + * earliest start index, which is the one whose completed stop would win the + * earliest-index tiebreak. A suffix longer than `maxLength - 1` can never be + * a strict prefix of any stop, so the search starts no earlier than that. + */ + private earliestViablePrefixIndex(limit: number): number { + if (this.pending_.length === 0) { + return -1; + } + const lowerBound = Math.max(0, this.pending_.length - (this.maxLength - 1)); + const upper = Math.min(limit, this.pending_.length - 1); + for (let j = lowerBound; j <= upper; j++) { + const suffix = this.pending_.slice(j); + if (this.stopSequences.some((seq) => suffix.length < seq.length && seq.startsWith(suffix))) { + return j; + } + } + return -1; + } + + /** + * Feed text in. Returns `safeText` (emit as delta) and `matched` (the stop + * sequence that has been matched, or `null`). After a match every push + * returns empty `safeText` and keeps reporting the matched sequence. + */ + push(text: string): { safeText: string; matched: string | null } { + if (this._matched !== null) { + return { safeText: '', matched: this._matched }; + } + + // Transparent pass-through when there is nothing to detect. + if (this.stopSequences.length === 0) { + return { safeText: text, matched: null }; + } + + this.pending_ += text; + + const { idx: matchIdx, seq: matchSeq } = this.findMatch(); + + // Earliest start index of a still-growable stop prefix. When a full match + // exists we only look at or before it (`limit = matchIdx`): a viable prefix + // beginning AFTER the match would complete at a later index and lose the + // earliest-index tiebreak, and the bytes from the match onward are + // suppressed anyway. A viable prefix at or before the match could still + // complete into a stop that WINS (earlier index, or longer at the same + // index), so the match must be held. With no full match we consider the + // whole pending text. + const limit = matchIdx >= 0 ? matchIdx : this.pending_.length - 1; + const holdIdx = this.earliestViablePrefixIndex(limit); + + if (matchIdx >= 0 && matchSeq !== null) { + if (holdIdx >= 0) { + // A longer/earlier stop could still complete from `holdIdx`; emit only + // the bytes before it and keep the rest pending for a later push or + // `flush()` to resolve. + const safeText = this.pending_.slice(0, holdIdx); + this.pending_ = this.pending_.slice(holdIdx); + return { safeText, matched: null }; + } + const safeText = this.pending_.slice(0, matchIdx); + this._matched = matchSeq; + this.pending_ = ''; + return { safeText, matched: matchSeq }; + } + + // No full match: release everything before the earliest viable prefix and + // hold that suffix back, since a later push could complete it. + const safeLen = holdIdx >= 0 ? holdIdx : this.pending_.length; + const safeText = this.pending_.slice(0, safeLen); + this.pending_ = this.pending_.slice(safeLen); + return { safeText, matched: null }; + } + + /** + * Release any held-back text at stream end. If a stop sequence already + * matched, nothing more is emitted; otherwise the residue could not + * complete any sequence and is released. + */ + flush(): { safeText: string; matched: string | null } { + if (this._matched !== null) { + return { safeText: '', matched: this._matched }; + } + // The stream has ended, so any match `push()` held back for a possible + // longer same-index stop can no longer be extended — resolve it now. + // Re-scan `pending` for the earliest match (longest on tie) and commit it + // if present; otherwise the residue could not complete any sequence and is + // released verbatim. + const { idx: matchIdx, seq: matchSeq } = this.findMatch(); + if (matchIdx >= 0 && matchSeq !== null) { + const safeText = this.pending_.slice(0, matchIdx); + this._matched = matchSeq; + this.pending_ = ''; + return { safeText, matched: matchSeq }; + } + const safeText = this.pending_; + this.pending_ = ''; + return { safeText, matched: null }; + } +} From f65104aa12133e56935216ca58f07255141d8da9 Mon Sep 17 00:00:00 2001 From: LongYinan Date: Tue, 30 Jun 2026 18:54:15 +0800 Subject: [PATCH 7/8] fix: align repetition handling with vLLM (cutoff off by default) (#82) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem Qwen models generating large markdown tables (a `|----------------------------------|` rule, or many byte-identical rows) get **silently truncated**. The native `check_repetition_cutoff` runs every decode step and stops generation on N identical tokens in a row (Tier 1) or a repeating n-gram block (Tier 2). It fires on legitimate repeated structure — table rules, ASCII boxes, separators, repeated rows — and the `"repetition"` finish reason is then mapped to a clean `end_turn`, so the cut looks like a normal answer. PR #81 only loosened the **TS Qwen launch preset** (256/8/64). Every other path — non-launch servers, Gemma4/LFM2, per-request configs that omit the fields, and the napi `GenerationConfig::default()` — still fell back to the tight native defaults (16/3/64), so as few as ~6 repeated tokens still truncated tables. ## How vLLM handles it (the model for this change) vLLM ships **no** repetition-stop heuristic by default. It shapes repetition only with logit-level penalties (`repetition_penalty` 1.0, `frequency_penalty`/`presence_penalty` 0.0 — all no-ops by default) and stops generation only on real conditions (EOS, stop tokens, stop strings, `max_tokens`). Its opt-in loop detector (`repetition_detection`, recent main) defaults to `None`, and when enabled surfaces its own distinct `FINISHED_REPETITION` finish reason rather than disguising the cut. ## Change: fully align with vLLM — cutoff OFF by default, opt-in - **`fix(sampling)`** — Introduce three named constants (all `0`) in `sampling.rs` and route **every** former 16/3/64 default-fallback site through them (`params.rs`, qwen3 / qwen3.5 / qwen3.5-MoE / gemma4 / qianfan_ocr model files, and the napi `GenerationConfig::default()`). With the knobs unset the detectors resolve to disabled (`0` already trips the existing `check_consecutive` / `check_ngram` guards). The detection **algorithm is unchanged**; GRPO training (`grpo/engine.rs`) and paddleocr keep their explicit values. New unit tests lock default-off and prove a positive value still re-enables detection (opt-in). - **`fix(server)`** — Remove the now-unnecessary loosened cutoff (256/8/64) from all four Qwen launch presets; repetition is now shaped by the sampling penalties and bounded by `maxOutputTokens`, with per-request opt-in still winning via `ChatSession.mergeConfig`. Presets test flipped to assert the fields are absent while keeping the opt-in/override coverage. - **`fix(types)`** — Sync the build-generated `packages/core/index.d.cts` cutoff doc comments to match the hand-synced `crates/mlx-core/index.d.cts` (both now say `default: 0 = disabled`), and rename a stale test `describe` block. ### Behavior change / migration A degenerate small model now runs to `max_tokens` (default `max_new_tokens` = 2048) instead of stopping at ~16 tokens. This is intended and vLLM-aligned — `max_tokens` is the backstop. Operators who want the old early repetition-stop must now set `maxConsecutiveTokens` / `maxNgramRepeats` / `ngramSize` explicitly (per-request or via registered `samplingDefaults`). ## Testing - Rust: `cargo test -p mlx-core --lib repetition` → 28 passing (TDD RED→GREEN; algorithm tests unchanged). Full lib suite 1878 pass; the 7 failures are pre-existing flaky f32/Metal numerical tests (banded-attn, attention-vjp, packed-affine embedding, sparse-moe gather, chunked-prefill parity) — verified to fail identically on base `73f4a89e`, no path to this change. - TS: `vp test __test__/server/presets.test.ts` → 7/7. ## Out of scope (noted, not fixed here) An adversarial review flagged that `/v1/responses` never clamps `max_output_tokens` against the registry's per-model `outputTokenLimit` (unlike `/v1/messages`). Independently verified as **pre-existing** (byte-identical at base `73f4a89e`; this branch doesn't touch `responses.ts`/`messages.ts`) and **not** a regression of this change (the cutoff only ever caught exact-repeat loops, never bounded long coherent output). Worth a separate server-hardening PR. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- > [!NOTE] > **Medium Risk** > Changes default generation stopping behavior across all chat paths: legitimate long repeats no longer truncate early, but runaway loops rely on `max_new_tokens` unless operators opt back in via config. > > **Overview** > **Native repetition-stop heuristics are off by default**, matching vLLM: unset `maxConsecutiveTokens`, `maxNgramRepeats`, and `ngramSize` now resolve to **0** (disabled) instead of 16/3/64. Shared `DEFAULT_*` constants in `sampling.rs` replace scattered literal fallbacks across chat param extraction and Qwen/Gemma/Qianfan generation paths; the detection logic is unchanged and **positive per-request values still opt in**. > > **Qwen launch presets** drop the previous 256/8/64 “loosened” cutoff pins—repetition is left to sampling penalties and `maxOutputTokens`, with `ChatSession.mergeConfig` still letting clients override. **API docs** (`ChatConfig` / `GenerationConfig`) and **tests** (presets, `params.rs` unit tests, LFM2 session comment) are updated for default-off behavior. > > Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 5686106532760b9c7d8d20d9d63d4998f8cc893b. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). --------- Co-authored-by: Claude Opus 4.8 --- __test__/server/presets.test.ts | 71 ++++++++----------- crates/mlx-core/index.d.cts | 18 ++--- crates/mlx-core/src/engine/params.rs | 47 +++++++++++- crates/mlx-core/src/engine/types.rs | 6 +- crates/mlx-core/src/models/gemma4/model.rs | 12 +++- .../mlx-core/src/models/qianfan_ocr/model.rs | 48 +++++++++---- .../mlx-core/src/models/qwen3/generation.rs | 18 ++--- crates/mlx-core/src/models/qwen3/model.rs | 24 +++++-- crates/mlx-core/src/models/qwen3_5/model.rs | 12 +++- .../mlx-core/src/models/qwen3_5_moe/model.rs | 12 +++- crates/mlx-core/src/sampling.rs | 10 +++ crates/mlx-core/tests/lfm2_session.rs | 12 ++-- packages/core/index.d.cts | 18 ++--- packages/server/src/presets.ts | 28 ++------ 14 files changed, 209 insertions(+), 127 deletions(-) diff --git a/__test__/server/presets.test.ts b/__test__/server/presets.test.ts index ea41921ab..89046ccab 100644 --- a/__test__/server/presets.test.ts +++ b/__test__/server/presets.test.ts @@ -1,13 +1,13 @@ /** - * Coverage for the launch-preset anti-repetition cutoff values. + * Coverage for the launch-preset repetition handling. * - * A live Claude Code session on Ornith (qwen3_5_moe) truncated an - * ASCII box mid-diagram: a run of repeated `─` tripped the native - * sampler's consecutive-token cutoff (`max_consecutive_tokens` - * default 16) → finish_reason `"repetition"` → `stop_reason:"end_turn"`. - * The Qwen launch presets loosen the cutoff so legit repeated content - * (box borders, separators, tables) survives while a true runaway loop - * still stops. + * The native anti-repetition cutoff is now disabled by default + * (vLLM-aligned — vLLM ships no repetition-stop heuristic), so the Qwen + * launch presets no longer pin `maxConsecutiveTokens` / `maxNgramRepeats` + * / `ngramSize`. Repetition is shaped by the sampling penalties and + * bounded by the per-model `maxOutputTokens`. An operator or client can + * still opt in by setting those fields explicitly; a per-request config + * value wins via `ChatSession.mergeConfig`. * * The merge-survival tests drive the REAL `ChatSession.mergeConfig` * through `ModelRegistry.register({ samplingDefaults }) → getOrCreate → @@ -75,36 +75,24 @@ function createCapturingModel(): CapturingModel { // Static preset-value assertions // --------------------------------------------------------------------------- -const LOOSENED_CUTOFF = { - maxConsecutiveTokens: 256, - maxNgramRepeats: 8, - ngramSize: 64, -} as const; - -describe('QWEN launch-preset anti-repetition cutoff', () => { - it('loosens the cutoff on every Qwen launch entry (qwen3 / qwen3_5 / qwen3_5_moe)', () => { +describe('QWEN launch-preset repetition cutoff is default-off', () => { + it('injects no cutoff fields on every Qwen launch entry (qwen3 / qwen3_5 / qwen3_5_moe)', () => { for (const key of ['qwen3', 'qwen3_5', 'qwen3_5_moe'] as const) { const sampling = LAUNCH_PRESETS[key]!.sampling; - expect(sampling.maxConsecutiveTokens).toBe(256); - expect(sampling.maxNgramRepeats).toBe(8); - expect(sampling.ngramSize).toBe(64); - } - }); - - it('sets the loosened cutoff on all four QWEN_SAMPLING_DEFAULTS variants', () => { - for (const key of ['thinkingCoding', 'thinkingGeneral', 'instructGeneral', 'instructReasoning'] as const) { - const sampling = QWEN_SAMPLING_DEFAULTS[key]; - expect(sampling.maxConsecutiveTokens).toBe(256); - expect(sampling.maxNgramRepeats).toBe(8); - expect(sampling.ngramSize).toBe(64); + expect(sampling.maxConsecutiveTokens).toBeUndefined(); + expect(sampling.maxNgramRepeats).toBeUndefined(); + expect(sampling.ngramSize).toBeUndefined(); } }); - it('does not disable the cutoff (runaway-loop protection must remain)', () => { + it('injects no cutoff fields on any of the four QWEN_SAMPLING_DEFAULTS variants', () => { for (const key of ['thinkingCoding', 'thinkingGeneral', 'instructGeneral', 'instructReasoning'] as const) { - const sampling = QWEN_SAMPLING_DEFAULTS[key]; - expect(sampling.maxConsecutiveTokens).not.toBe(0); - expect(sampling.maxNgramRepeats).not.toBe(0); + // Widen from the `as const` literal to ChatConfig so the now-removed + // cutoff fields are optional-undefined accesses, not TS2339 errors. + const sampling: ChatConfig = QWEN_SAMPLING_DEFAULTS[key]; + expect(sampling.maxConsecutiveTokens).toBeUndefined(); + expect(sampling.maxNgramRepeats).toBeUndefined(); + expect(sampling.ngramSize).toBeUndefined(); } }); @@ -139,8 +127,8 @@ describe('scope lock: non-Qwen presets unchanged', () => { // Merge survival (real ChatSession.mergeConfig) // --------------------------------------------------------------------------- -describe('cutoff survives ChatSession.mergeConfig', () => { - it('carries the loosened cutoff through when the per-request overlay omits it', async () => { +describe('repetition-cutoff handling through ChatSession.mergeConfig', () => { + it('injects no repetition-cutoff fields when the per-request overlay omits them', async () => { const registry = new ModelRegistry(); const model = createCapturingModel(); registry.register('qwen35moe', model, { samplingDefaults: LAUNCH_PRESETS.qwen3_5_moe!.sampling }); @@ -151,15 +139,18 @@ describe('cutoff survives ChatSession.mergeConfig', () => { // Claude Code never sends these fields — overlay omits them. await session.send('hi'); - expect(model.lastStartConfig).toMatchObject(LOOSENED_CUTOFF); - // Full shape: defaults + reuseCache, nothing else injected. + // Full shape: defaults + reuseCache, nothing else injected. The + // preset carries no cutoff fields, so none appear here. expect(model.lastStartConfig).toEqual({ ...LAUNCH_PRESETS.qwen3_5_moe!.sampling, reuseCache: true, }); + expect(model.lastStartConfig!.maxConsecutiveTokens).toBeUndefined(); + expect(model.lastStartConfig!.maxNgramRepeats).toBeUndefined(); + expect(model.lastStartConfig!.ngramSize).toBeUndefined(); }); - it('lets a per-request override win on maxConsecutiveTokens while the other cutoff fields survive', async () => { + it('lets a per-request override reach the model on maxConsecutiveTokens (opt-in)', async () => { const registry = new ModelRegistry(); const model = createCapturingModel(); registry.register('qwen35moe', model, { samplingDefaults: LAUNCH_PRESETS.qwen3_5_moe!.sampling }); @@ -170,8 +161,8 @@ describe('cutoff survives ChatSession.mergeConfig', () => { await session.send('hi', { config: { maxConsecutiveTokens: 32 } }); expect(model.lastStartConfig!.maxConsecutiveTokens).toBe(32); - // Default n-gram fields still carried (overlay didn't touch them). - expect(model.lastStartConfig!.maxNgramRepeats).toBe(8); - expect(model.lastStartConfig!.ngramSize).toBe(64); + // The preset contributes no n-gram fields, so they stay undefined. + expect(model.lastStartConfig!.maxNgramRepeats).toBeUndefined(); + expect(model.lastStartConfig!.ngramSize).toBeUndefined(); }); }); diff --git a/crates/mlx-core/index.d.cts b/crates/mlx-core/index.d.cts index b6054202f..83407a8a3 100644 --- a/crates/mlx-core/index.d.cts +++ b/crates/mlx-core/index.d.cts @@ -2288,11 +2288,11 @@ export interface ChatConfig { frequencyPenalty?: number | undefined; /** Number of recent tokens to consider for frequency penalty (default: 20) */ frequencyContextSize?: number | undefined; - /** Max consecutive identical tokens before stopping (default: 16, 0 = disabled) */ + /** Max consecutive identical tokens before stopping (default: 0 = disabled; opt in with a positive value) */ maxConsecutiveTokens?: number | undefined; - /** Max n-gram repetitions before stopping (default: 3, 0 = disabled) */ + /** Max n-gram repetitions before stopping (default: 0 = disabled; opt in with a positive value) */ maxNgramRepeats?: number | undefined; - /** Max pattern size for n-gram repetition detection (default: 64) */ + /** Max pattern size for n-gram repetition detection (default: 0 = disabled; opt in with a positive value) */ ngramSize?: number | undefined; tools?: Array; /** @@ -2988,19 +2988,19 @@ export interface GenerationConfig { /** Number of recent tokens to consider for frequency penalty (default: 20) */ frequencyContextSize?: number; /** - * Stop if same token repeats this many times consecutively (default: 16) - * Set to 0 to disable. Prevents OOM from degenerate repetitive generation. + * Stop if same token repeats this many times consecutively (default: 0 = disabled). + * Opt in by setting a positive value to guard against degenerate repetitive generation. */ maxConsecutiveTokens?: number; /** - * Stop if a pattern repeats this many times consecutively (default: 3) - * Set to 0 to disable. Detects patterns like "A B A B A B". + * Stop if a pattern repeats this many times consecutively (default: 0 = disabled). + * Opt in with a positive value to detect patterns like "A B A B A B". * Uses range-based detection: checks all pattern sizes from 2 to ngram_size. */ maxNgramRepeats?: number; /** - * Maximum pattern size for repetition detection (default: 64) - * All pattern sizes from 2 up to this value are checked each decode step. + * Maximum pattern size for repetition detection (default: 0 = disabled). + * When enabled, all pattern sizes from 2 up to this value are checked each decode step. * Larger values catch long phrase-level repetition common in small models. */ ngramSize?: number; diff --git a/crates/mlx-core/src/engine/params.rs b/crates/mlx-core/src/engine/params.rs index d2eac1473..4f985b363 100644 --- a/crates/mlx-core/src/engine/params.rs +++ b/crates/mlx-core/src/engine/params.rs @@ -390,9 +390,15 @@ pub(crate) fn extract_chat_params(config: &ChatConfig) -> ChatParams { presence_context_size: config.presence_context_size.unwrap_or(20), frequency_penalty: config.frequency_penalty.unwrap_or(0.0), frequency_context_size: config.frequency_context_size.unwrap_or(20), - max_consecutive_tokens: config.max_consecutive_tokens.unwrap_or(16), - max_ngram_repeats: config.max_ngram_repeats.unwrap_or(3), - ngram_size: config.ngram_size.unwrap_or(64), + max_consecutive_tokens: config + .max_consecutive_tokens + .unwrap_or(crate::sampling::DEFAULT_MAX_CONSECUTIVE_TOKENS), + max_ngram_repeats: config + .max_ngram_repeats + .unwrap_or(crate::sampling::DEFAULT_MAX_NGRAM_REPEATS), + ngram_size: config + .ngram_size + .unwrap_or(crate::sampling::DEFAULT_NGRAM_SIZE), sampling_config: Some(SamplingConfig { temperature: config.temperature, top_k: config.top_k, @@ -602,6 +608,41 @@ mod mtp_params_tests { assert!(!p.mtp_adaptive_depth); assert_eq!(p.mtp_depth, 1); } + + /// Repetition cutoff is OFF by default: with all three knobs unset the + /// resolved params are zero, which the `check_consecutive` / `check_ngram` + /// guards treat as disabled. This matches vLLM, which ships no + /// repetition-stop heuristic out of the box. + #[test] + fn repetition_cutoff_disabled_by_default() { + let cfg = base_config(); + let p = extract_chat_params(&cfg); + assert_eq!( + p.max_consecutive_tokens, 0, + "max_consecutive_tokens must default to 0 (disabled)" + ); + assert_eq!( + p.max_ngram_repeats, 0, + "max_ngram_repeats must default to 0 (disabled)" + ); + assert_eq!(p.ngram_size, 0, "ngram_size must default to 0 (disabled)"); + } + + /// Opt-in still works: a positive value re-enables detection and the + /// per-request override wins over the disabled default. + #[test] + fn repetition_cutoff_opt_in_overrides_default() { + let mut cfg = base_config(); + cfg.max_consecutive_tokens = Some(16); + let p = extract_chat_params(&cfg); + assert_eq!( + p.max_consecutive_tokens, 16, + "explicit max_consecutive_tokens must pass through" + ); + // Knobs left unset stay at the disabled default. + assert_eq!(p.max_ngram_repeats, 0); + assert_eq!(p.ngram_size, 0); + } } #[cfg(test)] diff --git a/crates/mlx-core/src/engine/types.rs b/crates/mlx-core/src/engine/types.rs index 102ab16b9..4e916e389 100644 --- a/crates/mlx-core/src/engine/types.rs +++ b/crates/mlx-core/src/engine/types.rs @@ -47,13 +47,13 @@ pub struct ChatConfig { /// Number of recent tokens to consider for frequency penalty (default: 20) #[napi(ts_type = "number | undefined")] pub frequency_context_size: Option, - /// Max consecutive identical tokens before stopping (default: 16, 0 = disabled) + /// Max consecutive identical tokens before stopping (default: 0 = disabled; opt in with a positive value) #[napi(ts_type = "number | undefined")] pub max_consecutive_tokens: Option, - /// Max n-gram repetitions before stopping (default: 3, 0 = disabled) + /// Max n-gram repetitions before stopping (default: 0 = disabled; opt in with a positive value) #[napi(ts_type = "number | undefined")] pub max_ngram_repeats: Option, - /// Max pattern size for n-gram repetition detection (default: 64) + /// Max pattern size for n-gram repetition detection (default: 0 = disabled; opt in with a positive value) #[napi(ts_type = "number | undefined")] pub ngram_size: Option, #[napi(ts_type = "Array")] diff --git a/crates/mlx-core/src/models/gemma4/model.rs b/crates/mlx-core/src/models/gemma4/model.rs index b6ed1eeee..6c9ec8b81 100644 --- a/crates/mlx-core/src/models/gemma4/model.rs +++ b/crates/mlx-core/src/models/gemma4/model.rs @@ -5650,9 +5650,15 @@ struct Gemma4RepetitionCutoff { fn repetition_cutoff_from_config(config: &ChatConfig) -> Gemma4RepetitionCutoff { Gemma4RepetitionCutoff { - max_consecutive_tokens: config.max_consecutive_tokens.unwrap_or(16), - max_ngram_repeats: config.max_ngram_repeats.unwrap_or(3), - ngram_size: config.ngram_size.unwrap_or(64), + max_consecutive_tokens: config + .max_consecutive_tokens + .unwrap_or(crate::sampling::DEFAULT_MAX_CONSECUTIVE_TOKENS), + max_ngram_repeats: config + .max_ngram_repeats + .unwrap_or(crate::sampling::DEFAULT_MAX_NGRAM_REPEATS), + ngram_size: config + .ngram_size + .unwrap_or(crate::sampling::DEFAULT_NGRAM_SIZE), } } diff --git a/crates/mlx-core/src/models/qianfan_ocr/model.rs b/crates/mlx-core/src/models/qianfan_ocr/model.rs index c91f2a0a9..f364fb7c8 100644 --- a/crates/mlx-core/src/models/qianfan_ocr/model.rs +++ b/crates/mlx-core/src/models/qianfan_ocr/model.rs @@ -386,9 +386,15 @@ impl QianfanOCRInner { let presence_context_size = config.presence_context_size.unwrap_or(20); let frequency_penalty = config.frequency_penalty.unwrap_or(0.0); let frequency_context_size = config.frequency_context_size.unwrap_or(20); - let max_consecutive_tokens = config.max_consecutive_tokens.unwrap_or(16); - let max_ngram_repeats = config.max_ngram_repeats.unwrap_or(3); - let ngram_size = config.ngram_size.unwrap_or(64); + let max_consecutive_tokens = config + .max_consecutive_tokens + .unwrap_or(crate::sampling::DEFAULT_MAX_CONSECUTIVE_TOKENS); + let max_ngram_repeats = config + .max_ngram_repeats + .unwrap_or(crate::sampling::DEFAULT_MAX_NGRAM_REPEATS); + let ngram_size = config + .ngram_size + .unwrap_or(crate::sampling::DEFAULT_NGRAM_SIZE); let enable_thinking = crate::engine::resolve_enable_thinking(&config).unwrap_or(false); let reuse_cache = config.reuse_cache.unwrap_or(true); let report_perf = config.report_performance.unwrap_or(false); @@ -784,9 +790,15 @@ impl QianfanOCRInner { let presence_context_size = config.presence_context_size.unwrap_or(20); let frequency_penalty = config.frequency_penalty.unwrap_or(0.0); let frequency_context_size = config.frequency_context_size.unwrap_or(20); - let max_consecutive_tokens = config.max_consecutive_tokens.unwrap_or(16); - let max_ngram_repeats = config.max_ngram_repeats.unwrap_or(3); - let ngram_size = config.ngram_size.unwrap_or(64); + let max_consecutive_tokens = config + .max_consecutive_tokens + .unwrap_or(crate::sampling::DEFAULT_MAX_CONSECUTIVE_TOKENS); + let max_ngram_repeats = config + .max_ngram_repeats + .unwrap_or(crate::sampling::DEFAULT_MAX_NGRAM_REPEATS); + let ngram_size = config + .ngram_size + .unwrap_or(crate::sampling::DEFAULT_NGRAM_SIZE); let enable_thinking = crate::engine::resolve_enable_thinking(&config).unwrap_or(false); let reuse_cache = config.reuse_cache.unwrap_or(true); let report_perf = config.report_performance.unwrap_or(false); @@ -1367,9 +1379,15 @@ impl QianfanOCRInner { let presence_context_size = config.presence_context_size.unwrap_or(20); let frequency_penalty = config.frequency_penalty.unwrap_or(0.0); let frequency_context_size = config.frequency_context_size.unwrap_or(20); - let max_consecutive_tokens = config.max_consecutive_tokens.unwrap_or(16); - let max_ngram_repeats = config.max_ngram_repeats.unwrap_or(3); - let ngram_size = config.ngram_size.unwrap_or(64); + let max_consecutive_tokens = config + .max_consecutive_tokens + .unwrap_or(crate::sampling::DEFAULT_MAX_CONSECUTIVE_TOKENS); + let max_ngram_repeats = config + .max_ngram_repeats + .unwrap_or(crate::sampling::DEFAULT_MAX_NGRAM_REPEATS); + let ngram_size = config + .ngram_size + .unwrap_or(crate::sampling::DEFAULT_NGRAM_SIZE); let report_perf = config.report_performance.unwrap_or(false); let generation_start = if report_perf { @@ -1844,9 +1862,15 @@ impl QianfanOCRInner { let presence_context_size = config.presence_context_size.unwrap_or(20); let frequency_penalty = config.frequency_penalty.unwrap_or(0.0); let frequency_context_size = config.frequency_context_size.unwrap_or(20); - let max_consecutive_tokens = config.max_consecutive_tokens.unwrap_or(16); - let max_ngram_repeats = config.max_ngram_repeats.unwrap_or(3); - let ngram_size = config.ngram_size.unwrap_or(64); + let max_consecutive_tokens = config + .max_consecutive_tokens + .unwrap_or(crate::sampling::DEFAULT_MAX_CONSECUTIVE_TOKENS); + let max_ngram_repeats = config + .max_ngram_repeats + .unwrap_or(crate::sampling::DEFAULT_MAX_NGRAM_REPEATS); + let ngram_size = config + .ngram_size + .unwrap_or(crate::sampling::DEFAULT_NGRAM_SIZE); let report_perf = config.report_performance.unwrap_or(false); let generation_start = if report_perf { diff --git a/crates/mlx-core/src/models/qwen3/generation.rs b/crates/mlx-core/src/models/qwen3/generation.rs index 5487db14c..d9b845f8b 100644 --- a/crates/mlx-core/src/models/qwen3/generation.rs +++ b/crates/mlx-core/src/models/qwen3/generation.rs @@ -47,17 +47,17 @@ pub struct GenerationConfig { /// Number of recent tokens to consider for frequency penalty (default: 20) pub frequency_context_size: Option, - /// Stop if same token repeats this many times consecutively (default: 16) - /// Set to 0 to disable. Prevents OOM from degenerate repetitive generation. + /// Stop if same token repeats this many times consecutively (default: 0 = disabled). + /// Opt in by setting a positive value to guard against degenerate repetitive generation. pub max_consecutive_tokens: Option, - /// Stop if a pattern repeats this many times consecutively (default: 3) - /// Set to 0 to disable. Detects patterns like "A B A B A B". + /// Stop if a pattern repeats this many times consecutively (default: 0 = disabled). + /// Opt in with a positive value to detect patterns like "A B A B A B". /// Uses range-based detection: checks all pattern sizes from 2 to ngram_size. pub max_ngram_repeats: Option, - /// Maximum pattern size for repetition detection (default: 64) - /// All pattern sizes from 2 up to this value are checked each decode step. + /// Maximum pattern size for repetition detection (default: 0 = disabled). + /// When enabled, all pattern sizes from 2 up to this value are checked each decode step. /// Larger values catch long phrase-level repetition common in small models. pub ngram_size: Option, @@ -94,9 +94,9 @@ impl Default for GenerationConfig { presence_context_size: None, frequency_penalty: None, frequency_context_size: None, - max_consecutive_tokens: Some(16), - max_ngram_repeats: Some(3), - ngram_size: Some(64), + max_consecutive_tokens: Some(crate::sampling::DEFAULT_MAX_CONSECUTIVE_TOKENS), + max_ngram_repeats: Some(crate::sampling::DEFAULT_MAX_NGRAM_REPEATS), + ngram_size: Some(crate::sampling::DEFAULT_NGRAM_SIZE), eos_token_id: None, return_logprobs: Some(true), prefill_step_size: Some(2048), diff --git a/crates/mlx-core/src/models/qwen3/model.rs b/crates/mlx-core/src/models/qwen3/model.rs index 3ce039447..b0387d668 100644 --- a/crates/mlx-core/src/models/qwen3/model.rs +++ b/crates/mlx-core/src/models/qwen3/model.rs @@ -820,9 +820,15 @@ impl Qwen3Inner { let presence_context_size = config.presence_context_size.unwrap_or(20); let frequency_penalty = config.frequency_penalty.unwrap_or(0.0); let frequency_context_size = config.frequency_context_size.unwrap_or(20); - let max_consecutive_tokens = config.max_consecutive_tokens.unwrap_or(16); - let max_ngram_repeats = config.max_ngram_repeats.unwrap_or(3); - let ngram_size = config.ngram_size.unwrap_or(64); + let max_consecutive_tokens = config + .max_consecutive_tokens + .unwrap_or(crate::sampling::DEFAULT_MAX_CONSECUTIVE_TOKENS); + let max_ngram_repeats = config + .max_ngram_repeats + .unwrap_or(crate::sampling::DEFAULT_MAX_NGRAM_REPEATS); + let ngram_size = config + .ngram_size + .unwrap_or(crate::sampling::DEFAULT_NGRAM_SIZE); let eos_token_id = config.eos_token_id.or(Some(self.config.eos_token_id)); let return_logprobs = config.return_logprobs.unwrap_or(true); let prefill_step_size = config.prefill_step_size.unwrap_or(2048) as usize; @@ -1538,9 +1544,15 @@ impl Qwen3Inner { let presence_context_size = config.presence_context_size.unwrap_or(20); let frequency_penalty = config.frequency_penalty.unwrap_or(0.0); let frequency_context_size = config.frequency_context_size.unwrap_or(20); - let max_consecutive_tokens = config.max_consecutive_tokens.unwrap_or(16); - let max_ngram_repeats = config.max_ngram_repeats.unwrap_or(3); - let ngram_size = config.ngram_size.unwrap_or(64); + let max_consecutive_tokens = config + .max_consecutive_tokens + .unwrap_or(crate::sampling::DEFAULT_MAX_CONSECUTIVE_TOKENS); + let max_ngram_repeats = config + .max_ngram_repeats + .unwrap_or(crate::sampling::DEFAULT_MAX_NGRAM_REPEATS); + let ngram_size = config + .ngram_size + .unwrap_or(crate::sampling::DEFAULT_NGRAM_SIZE); let eos_token_id = config.eos_token_id.or(Some(self.config.eos_token_id)); let return_logprobs = config.return_logprobs.unwrap_or(true); let prefill_step_size = config.prefill_step_size.unwrap_or(2048) as usize; diff --git a/crates/mlx-core/src/models/qwen3_5/model.rs b/crates/mlx-core/src/models/qwen3_5/model.rs index 7e2f04013..5aefe8725 100644 --- a/crates/mlx-core/src/models/qwen3_5/model.rs +++ b/crates/mlx-core/src/models/qwen3_5/model.rs @@ -5401,9 +5401,15 @@ impl Qwen35Inner { let presence_context_size = config.presence_context_size.unwrap_or(20); let frequency_penalty = config.frequency_penalty.unwrap_or(0.0); let frequency_context_size = config.frequency_context_size.unwrap_or(20); - let max_consecutive_tokens = config.max_consecutive_tokens.unwrap_or(16); - let max_ngram_repeats = config.max_ngram_repeats.unwrap_or(3); - let ngram_size = config.ngram_size.unwrap_or(64); + let max_consecutive_tokens = config + .max_consecutive_tokens + .unwrap_or(crate::sampling::DEFAULT_MAX_CONSECUTIVE_TOKENS); + let max_ngram_repeats = config + .max_ngram_repeats + .unwrap_or(crate::sampling::DEFAULT_MAX_NGRAM_REPEATS); + let ngram_size = config + .ngram_size + .unwrap_or(crate::sampling::DEFAULT_NGRAM_SIZE); let eos_token_id = config.eos_token_id.or(Some(self.config.eos_token_id)); let return_logprobs = config.return_logprobs.unwrap_or(true); diff --git a/crates/mlx-core/src/models/qwen3_5_moe/model.rs b/crates/mlx-core/src/models/qwen3_5_moe/model.rs index 52032b319..ef404475b 100644 --- a/crates/mlx-core/src/models/qwen3_5_moe/model.rs +++ b/crates/mlx-core/src/models/qwen3_5_moe/model.rs @@ -4816,9 +4816,15 @@ impl Qwen35MoeInner { let presence_context_size = config.presence_context_size.unwrap_or(20); let frequency_penalty = config.frequency_penalty.unwrap_or(0.0); let frequency_context_size = config.frequency_context_size.unwrap_or(20); - let max_consecutive_tokens = config.max_consecutive_tokens.unwrap_or(16); - let max_ngram_repeats = config.max_ngram_repeats.unwrap_or(3); - let ngram_size = config.ngram_size.unwrap_or(64); + let max_consecutive_tokens = config + .max_consecutive_tokens + .unwrap_or(crate::sampling::DEFAULT_MAX_CONSECUTIVE_TOKENS); + let max_ngram_repeats = config + .max_ngram_repeats + .unwrap_or(crate::sampling::DEFAULT_MAX_NGRAM_REPEATS); + let ngram_size = config + .ngram_size + .unwrap_or(crate::sampling::DEFAULT_NGRAM_SIZE); let eos_token_id = config.eos_token_id.or(Some(self.config.eos_token_id)); let return_logprobs = config.return_logprobs.unwrap_or(true); diff --git a/crates/mlx-core/src/sampling.rs b/crates/mlx-core/src/sampling.rs index d0e26a8e5..0c348aa91 100644 --- a/crates/mlx-core/src/sampling.rs +++ b/crates/mlx-core/src/sampling.rs @@ -1406,6 +1406,16 @@ pub(crate) fn apply_frequency_penalty( logits.put_along_axis(&indices, &penalized, ctx.last_axis) } +/// Default repetition-cutoff parameters. All-zero = DISABLED, matching vLLM, +/// which ships no repetition-stop heuristic out of the box and relies on +/// `max_tokens` plus logit penalties (repetition / frequency / presence) to +/// shape repetition. Callers opt in by setting positive `ChatConfig` / +/// `GenerationConfig` values, which re-enable the `check_consecutive` / +/// `check_ngram` guards in [`check_repetition_cutoff`]. +pub(crate) const DEFAULT_MAX_CONSECUTIVE_TOKENS: i32 = 0; +pub(crate) const DEFAULT_MAX_NGRAM_REPEATS: i32 = 0; +pub(crate) const DEFAULT_NGRAM_SIZE: i32 = 0; + /// Check if generation has fallen into a repetitive loop. /// /// Detect degenerate repetitive generation and signal early termination. diff --git a/crates/mlx-core/tests/lfm2_session.rs b/crates/mlx-core/tests/lfm2_session.rs index ec4fb0259..ec1bc5ae4 100644 --- a/crates/mlx-core/tests/lfm2_session.rs +++ b/crates/mlx-core/tests/lfm2_session.rs @@ -593,12 +593,12 @@ async fn lfm2_session_continue_tool_round_trips() { // tool_call_id (its template identifies tool responses positionally), so a // minimal tool-continue on the "thinking" checkpoint enters a think block // immediately and, under the 32-token budget here, legitimately bottoms out - // via any cutoff: EOS ("stop"), the budget cap ("length"), or the - // repetition guard ("repetition", which is active by default — - // params.rs defaults max_consecutive_tokens=16 / max_ngram_repeats=3 / - // ngram_size=64 when the config leaves them unset). All three are valid - // non-cancelled terminal states. Forward correctness is covered separately - // by lfm2_paged_vs_flat_parity (6/6 byte-identical). + // via EOS ("stop") or the budget cap ("length"). The repetition guard is + // OFF by default (params.rs resolves max_consecutive_tokens / max_ngram_repeats + // / ngram_size to 0 when the config leaves them unset), so "repetition" only + // fires if a caller opts in; it stays an accepted terminal state here for that + // case. All are valid non-cancelled terminal states. Forward correctness is + // covered separately by lfm2_paged_vs_flat_parity (6/6 byte-identical). assert!( matches!( result.finish_reason.as_str(), diff --git a/packages/core/index.d.cts b/packages/core/index.d.cts index f9831cd30..2101c8b96 100644 --- a/packages/core/index.d.cts +++ b/packages/core/index.d.cts @@ -2288,11 +2288,11 @@ export interface ChatConfig { frequencyPenalty?: number | undefined; /** Number of recent tokens to consider for frequency penalty (default: 20) */ frequencyContextSize?: number | undefined; - /** Max consecutive identical tokens before stopping (default: 16, 0 = disabled) */ + /** Max consecutive identical tokens before stopping (default: 0 = disabled; opt in with a positive value) */ maxConsecutiveTokens?: number | undefined; - /** Max n-gram repetitions before stopping (default: 3, 0 = disabled) */ + /** Max n-gram repetitions before stopping (default: 0 = disabled; opt in with a positive value) */ maxNgramRepeats?: number | undefined; - /** Max pattern size for n-gram repetition detection (default: 64) */ + /** Max pattern size for n-gram repetition detection (default: 0 = disabled; opt in with a positive value) */ ngramSize?: number | undefined; tools?: Array; /** @@ -2988,19 +2988,19 @@ export interface GenerationConfig { /** Number of recent tokens to consider for frequency penalty (default: 20) */ frequencyContextSize?: number; /** - * Stop if same token repeats this many times consecutively (default: 16) - * Set to 0 to disable. Prevents OOM from degenerate repetitive generation. + * Stop if same token repeats this many times consecutively (default: 0 = disabled). + * Opt in by setting a positive value to guard against degenerate repetitive generation. */ maxConsecutiveTokens?: number; /** - * Stop if a pattern repeats this many times consecutively (default: 3) - * Set to 0 to disable. Detects patterns like "A B A B A B". + * Stop if a pattern repeats this many times consecutively (default: 0 = disabled). + * Opt in with a positive value to detect patterns like "A B A B A B". * Uses range-based detection: checks all pattern sizes from 2 to ngram_size. */ maxNgramRepeats?: number; /** - * Maximum pattern size for repetition detection (default: 64) - * All pattern sizes from 2 up to this value are checked each decode step. + * Maximum pattern size for repetition detection (default: 0 = disabled). + * When enabled, all pattern sizes from 2 up to this value are checked each decode step. * Larger values catch long phrase-level repetition common in small models. */ ngramSize?: number; diff --git a/packages/server/src/presets.ts b/packages/server/src/presets.ts index 857598fb7..b0f1c1c49 100644 --- a/packages/server/src/presets.ts +++ b/packages/server/src/presets.ts @@ -19,15 +19,13 @@ import type { ChatConfig } from '@mlx-node/core'; * All modes pin `top_k = 20` and `min_p = 0.0`; they differ in * `temperature`, `top_p`, and `presence_penalty`. * - * Every mode also loosens the native anti-repetition cutoff - * (`maxConsecutiveTokens` / `maxNgramRepeats`, default 16 / 3). Coding - * answers legitimately contain long single-token runs — ASCII box - * borders (`────`), separators (`====`/`----`), table rules, repeated - * indentation — which the default cutoff treats as a stuck loop and - * truncates mid-diagram (finish_reason `"repetition"` → `end_turn`), - * silently cutting the answer. 256 / 8 lets a wide box or table survive - * while still bounding a true runaway loop; `ngramSize` is the default, - * kept explicit. Do not set 0 — that disables runaway protection. + * The native anti-repetition cutoff is now disabled by default + * (vLLM-aligned — vLLM ships no repetition-stop heuristic), so these + * presets no longer pin `maxConsecutiveTokens` / `maxNgramRepeats` / + * `ngramSize`. Repetition is shaped by the sampling penalties above and + * bounded by the per-model `maxOutputTokens`. An operator or client can + * still opt in by setting those fields explicitly — a per-request config + * value wins via `ChatSession.mergeConfig`. */ export const QWEN_SAMPLING_DEFAULTS = { /** Thinking mode for precise coding tasks: temp=0.6, top_p=0.95, pp=0.0 */ @@ -38,9 +36,6 @@ export const QWEN_SAMPLING_DEFAULTS = { minP: 0.0, presencePenalty: 0.0, repetitionPenalty: 1.0, - maxConsecutiveTokens: 256, - maxNgramRepeats: 8, - ngramSize: 64, } satisfies ChatConfig, /** Thinking mode for general tasks: temp=1.0, top_p=0.95, pp=1.5 */ @@ -51,9 +46,6 @@ export const QWEN_SAMPLING_DEFAULTS = { minP: 0.0, presencePenalty: 1.5, repetitionPenalty: 1.0, - maxConsecutiveTokens: 256, - maxNgramRepeats: 8, - ngramSize: 64, } satisfies ChatConfig, /** Instruct (non-thinking) for general tasks: temp=0.7, top_p=0.8, pp=1.5 */ @@ -64,9 +56,6 @@ export const QWEN_SAMPLING_DEFAULTS = { minP: 0.0, presencePenalty: 1.5, repetitionPenalty: 1.0, - maxConsecutiveTokens: 256, - maxNgramRepeats: 8, - ngramSize: 64, } satisfies ChatConfig, /** Instruct (non-thinking) for reasoning tasks: temp=1.0, top_p=0.95, pp=1.5 */ @@ -77,9 +66,6 @@ export const QWEN_SAMPLING_DEFAULTS = { minP: 0.0, presencePenalty: 1.5, repetitionPenalty: 1.0, - maxConsecutiveTokens: 256, - maxNgramRepeats: 8, - ngramSize: 64, } satisfies ChatConfig, } as const; From 1bdb9f645909788203b2d905a28f979f63e573c1 Mon Sep 17 00:00:00 2001 From: MK Date: Thu, 2 Jul 2026 16:30:41 +0800 Subject: [PATCH 8/8] chore(deps): bump vite-plus to v0.2.2 --- .yarnrc.yml | 10 +- .../server/model-work-coordinator.test.ts | 2 +- package.json | 8 +- packages/lm/package.json | 2 +- packages/privacy/package.json | 2 +- packages/trl/package.json | 4 +- packages/vlm/package.json | 2 +- yarn.lock | 2036 ++++++++++------- 8 files changed, 1283 insertions(+), 783 deletions(-) diff --git a/.yarnrc.yml b/.yarnrc.yml index 0c26bdfc0..ae2c33405 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -3,9 +3,9 @@ approvedGitRepositories: catalog: "@types/node": "npm:@types/node@24.12.2" - vite: "npm:@voidzero-dev/vite-plus-core@0.1.24" - vite-plus: 0.1.24 - vitest: "npm:@voidzero-dev/vite-plus-test@0.1.24" + vite: npm:@voidzero-dev/vite-plus-core@0.0.0-commit.1d7ba811f0e456640b0e761e8e8acf77d1ad3251 + vite-plus: 0.0.0-commit.1d7ba811f0e456640b0e761e8e8acf77d1ad3251 + vitest: 4.1.9 enableScripts: true @@ -18,3 +18,7 @@ npmScopes: npmRegistryServer: "https://npm.jsr.io" yarnPath: .yarn/releases/yarn-4.17.0.cjs +npmPreapprovedPackages: + - vitest + - '@vitest/*' +npmRegistryServer: https://registry-bridge.viteplus.dev/ diff --git a/__test__/server/model-work-coordinator.test.ts b/__test__/server/model-work-coordinator.test.ts index 40f3d0f4b..7de7ecf3b 100644 --- a/__test__/server/model-work-coordinator.test.ts +++ b/__test__/server/model-work-coordinator.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'vitest'; +import { describe, expect, it } from 'vite-plus/test'; import { ModelWorkCoordinator } from '../../packages/server/src/model-work-coordinator.js'; diff --git a/package.json b/package.json index c66bc7540..04603d61b 100644 --- a/package.json +++ b/package.json @@ -38,12 +38,12 @@ "oxc-parser": "^0.137.0", "prettier": "^3.8.4", "typescript": "^6.0.3", - "vite-plus": "0.1.24", - "vitest": "^4.1.9" + "vite-plus": "catalog:", + "vitest": "catalog:" }, "resolutions": { - "vite": "npm:@voidzero-dev/vite-plus-core@0.1.24", - "vitest": "npm:@voidzero-dev/vite-plus-test@0.1.24" + "vite": "npm:@voidzero-dev/vite-plus-core@0.0.0-commit.1d7ba811f0e456640b0e761e8e8acf77d1ad3251", + "vitest": "4.1.9" }, "packageManager": "yarn@4.17.0" } diff --git a/packages/lm/package.json b/packages/lm/package.json index 9550e5cc8..16f452390 100644 --- a/packages/lm/package.json +++ b/packages/lm/package.json @@ -25,7 +25,7 @@ }, "scripts": { "build": "tsc -b", - "test": "vite test run" + "test": "vp test run" }, "dependencies": { "@mlx-node/core": "workspace:*" diff --git a/packages/privacy/package.json b/packages/privacy/package.json index b96e297fb..ac4104105 100644 --- a/packages/privacy/package.json +++ b/packages/privacy/package.json @@ -25,7 +25,7 @@ }, "scripts": { "build": "tsc -b", - "test": "vite test run" + "test": "vp test run" }, "dependencies": { "@mlx-node/core": "workspace:*" diff --git a/packages/trl/package.json b/packages/trl/package.json index 7a8a3678a..7f2d9b0b2 100644 --- a/packages/trl/package.json +++ b/packages/trl/package.json @@ -25,8 +25,8 @@ }, "scripts": { "build": "tsc -b", - "test": "vite test run", - "test:trainer": "TEST_TRAINER=1 vite test run" + "test": "vp test run", + "test:trainer": "TEST_TRAINER=1 vp test run" }, "dependencies": { "@mlx-node/core": "workspace:*", diff --git a/packages/vlm/package.json b/packages/vlm/package.json index 41255497a..2c9a76cbd 100644 --- a/packages/vlm/package.json +++ b/packages/vlm/package.json @@ -25,7 +25,7 @@ }, "scripts": { "build": "tsc -b", - "test": "vite test run" + "test": "vp test run" }, "dependencies": { "@mlx-node/core": "workspace:*", diff --git a/yarn.lock b/yarn.lock index 9e1a2245e..3fd749d23 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5,44 +5,44 @@ __metadata: version: 10 cacheKey: 10c0 -"@ai-sdk/gateway@npm:3.0.134": - version: 3.0.134 - resolution: "@ai-sdk/gateway@npm:3.0.134" +"@ai-sdk/gateway@npm:3.0.142": + version: 3.0.142 + resolution: "@ai-sdk/gateway@npm:3.0.142::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40ai-sdk%2Fgateway%2F-%2Fgateway-3.0.142.tgz" dependencies: - "@ai-sdk/provider": "npm:3.0.10" - "@ai-sdk/provider-utils": "npm:4.0.30" + "@ai-sdk/provider": "npm:3.0.13" + "@ai-sdk/provider-utils": "npm:4.0.35" "@vercel/oidc": "npm:3.2.0" peerDependencies: zod: ^3.25.76 || ^4.1.8 - checksum: 10c0/9f4ef4d43aab397c96a10d02ccc94a527f5f3b476e05a9f9de8b963d57cd052eb54c1acef2329c4812f8534ac73f5700f5eef73b0725ff67dfae42b0d01272dc + checksum: 10c0/870f96ce6ffe758b3101e61851bb70822ec1aedc15d104ccd9cd66597ad34c52d9711415b9eb07c985b2efd564688743bd44e101c70389ce48bd33040f6177ad languageName: node linkType: hard -"@ai-sdk/provider-utils@npm:4.0.30": - version: 4.0.30 - resolution: "@ai-sdk/provider-utils@npm:4.0.30" +"@ai-sdk/provider-utils@npm:4.0.35": + version: 4.0.35 + resolution: "@ai-sdk/provider-utils@npm:4.0.35::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40ai-sdk%2Fprovider-utils%2F-%2Fprovider-utils-4.0.35.tgz" dependencies: - "@ai-sdk/provider": "npm:3.0.10" + "@ai-sdk/provider": "npm:3.0.13" "@standard-schema/spec": "npm:^1.1.0" eventsource-parser: "npm:^3.0.8" peerDependencies: zod: ^3.25.76 || ^4.1.8 - checksum: 10c0/4195612c0da1a634ca11043cbfed8e328c9177fc7289b533653529458163e944398539e53f1a689c6ffdf0f728e25b6cc9c927cfc2fe95e80ed22ae008a8053d + checksum: 10c0/43b7564353fafa0a3baba40714bbed89ca562537aed467f9c170d2c15b0fd8eb3b7ac0a2801030dfa15a4d208bd238d57bf1f67e539bf7de7afbb994006a6678 languageName: node linkType: hard -"@ai-sdk/provider@npm:3.0.10": - version: 3.0.10 - resolution: "@ai-sdk/provider@npm:3.0.10" +"@ai-sdk/provider@npm:3.0.13": + version: 3.0.13 + resolution: "@ai-sdk/provider@npm:3.0.13::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40ai-sdk%2Fprovider%2F-%2Fprovider-3.0.13.tgz" dependencies: json-schema: "npm:^0.4.0" - checksum: 10c0/c9b162165c3fd4684e4f7a1c041db3ad75785c841d904bac577006b7b2299d1b465557b1d32e3f59b6d8536a0ef5741b195aad86fc3e0e1d2a39f077062a83c4 + checksum: 10c0/65f7a0bc5de01561ed7c2007a96e3ecc077dd43af046c2d526d46930c061adb2159c04063ef05075dd059f61745a9597bca2f92871a22c0dcb6ba5c58c2f5d53 languageName: node linkType: hard "@arcanis/slice-ansi@npm:^1.1.1": version: 1.1.1 - resolution: "@arcanis/slice-ansi@npm:1.1.1" + resolution: "@arcanis/slice-ansi@npm:1.1.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40arcanis%2Fslice-ansi%2F-%2Fslice-ansi-1.1.1.tgz" dependencies: grapheme-splitter: "npm:^1.0.4" checksum: 10c0/2f222b121b8aaf67e8495e27d60ebfc34e2472033445c3380e93fb06aba9bfef6ab3096aca190a181b3dd505ed4c07f4dc7243fc9cb5369008b649cd1e39e8d8 @@ -51,70 +51,70 @@ __metadata: "@ast-grep/napi-darwin-arm64@npm:0.44.0": version: 0.44.0 - resolution: "@ast-grep/napi-darwin-arm64@npm:0.44.0" + resolution: "@ast-grep/napi-darwin-arm64@npm:0.44.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40ast-grep%2Fnapi-darwin-arm64%2F-%2Fnapi-darwin-arm64-0.44.0.tgz" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard "@ast-grep/napi-darwin-x64@npm:0.44.0": version: 0.44.0 - resolution: "@ast-grep/napi-darwin-x64@npm:0.44.0" + resolution: "@ast-grep/napi-darwin-x64@npm:0.44.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40ast-grep%2Fnapi-darwin-x64%2F-%2Fnapi-darwin-x64-0.44.0.tgz" conditions: os=darwin & cpu=x64 languageName: node linkType: hard "@ast-grep/napi-linux-arm64-gnu@npm:0.44.0": version: 0.44.0 - resolution: "@ast-grep/napi-linux-arm64-gnu@npm:0.44.0" + resolution: "@ast-grep/napi-linux-arm64-gnu@npm:0.44.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40ast-grep%2Fnapi-linux-arm64-gnu%2F-%2Fnapi-linux-arm64-gnu-0.44.0.tgz" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard "@ast-grep/napi-linux-arm64-musl@npm:0.44.0": version: 0.44.0 - resolution: "@ast-grep/napi-linux-arm64-musl@npm:0.44.0" + resolution: "@ast-grep/napi-linux-arm64-musl@npm:0.44.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40ast-grep%2Fnapi-linux-arm64-musl%2F-%2Fnapi-linux-arm64-musl-0.44.0.tgz" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard "@ast-grep/napi-linux-x64-gnu@npm:0.44.0": version: 0.44.0 - resolution: "@ast-grep/napi-linux-x64-gnu@npm:0.44.0" + resolution: "@ast-grep/napi-linux-x64-gnu@npm:0.44.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40ast-grep%2Fnapi-linux-x64-gnu%2F-%2Fnapi-linux-x64-gnu-0.44.0.tgz" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard "@ast-grep/napi-linux-x64-musl@npm:0.44.0": version: 0.44.0 - resolution: "@ast-grep/napi-linux-x64-musl@npm:0.44.0" + resolution: "@ast-grep/napi-linux-x64-musl@npm:0.44.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40ast-grep%2Fnapi-linux-x64-musl%2F-%2Fnapi-linux-x64-musl-0.44.0.tgz" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard "@ast-grep/napi-win32-arm64-msvc@npm:0.44.0": version: 0.44.0 - resolution: "@ast-grep/napi-win32-arm64-msvc@npm:0.44.0" + resolution: "@ast-grep/napi-win32-arm64-msvc@npm:0.44.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40ast-grep%2Fnapi-win32-arm64-msvc%2F-%2Fnapi-win32-arm64-msvc-0.44.0.tgz" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard "@ast-grep/napi-win32-ia32-msvc@npm:0.44.0": version: 0.44.0 - resolution: "@ast-grep/napi-win32-ia32-msvc@npm:0.44.0" + resolution: "@ast-grep/napi-win32-ia32-msvc@npm:0.44.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40ast-grep%2Fnapi-win32-ia32-msvc%2F-%2Fnapi-win32-ia32-msvc-0.44.0.tgz" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard "@ast-grep/napi-win32-x64-msvc@npm:0.44.0": version: 0.44.0 - resolution: "@ast-grep/napi-win32-x64-msvc@npm:0.44.0" + resolution: "@ast-grep/napi-win32-x64-msvc@npm:0.44.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40ast-grep%2Fnapi-win32-x64-msvc%2F-%2Fnapi-win32-x64-msvc-0.44.0.tgz" conditions: os=win32 & cpu=x64 languageName: node linkType: hard "@ast-grep/napi@npm:^0.44.0": version: 0.44.0 - resolution: "@ast-grep/napi@npm:0.44.0" + resolution: "@ast-grep/napi@npm:0.44.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40ast-grep%2Fnapi%2F-%2Fnapi-0.44.0.tgz" dependencies: "@ast-grep/napi-darwin-arm64": "npm:0.44.0" "@ast-grep/napi-darwin-x64": "npm:0.44.0" @@ -148,9 +148,41 @@ __metadata: languageName: node linkType: hard +"@babel/code-frame@npm:^7.10.4": + version: 7.29.7 + resolution: "@babel/code-frame@npm:7.29.7::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40babel%2Fcode-frame%2F-%2Fcode-frame-7.29.7.tgz" + dependencies: + "@babel/helper-validator-identifier": "npm:^7.29.7" + js-tokens: "npm:^4.0.0" + picocolors: "npm:^1.1.1" + checksum: 10c0/169fc2080169a40c1760155eaaaf739bcb882df0bec76a83adbda5493645bc17270a3434b8848c494b1933e96fe1d147370001e3cda09a39f43ae30f08ef2069 + languageName: node + linkType: hard + +"@babel/helper-validator-identifier@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/helper-validator-identifier@npm:7.29.7::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40babel%2Fhelper-validator-identifier%2F-%2Fhelper-validator-identifier-7.29.7.tgz" + checksum: 10c0/4795354e7ae0dcafa72de1cd04ec51252dc1498517170beaf019e03effc5b7bf13c6b21a3949a77e07b8125be7f106ed1131350d8ebd4566ae874094a726d62b + languageName: node + linkType: hard + +"@babel/runtime@npm:^7.12.5": + version: 7.29.7 + resolution: "@babel/runtime@npm:7.29.7::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40babel%2Fruntime%2F-%2Fruntime-7.29.7.tgz" + checksum: 10c0/ca11572f7146b21e0bde6a9ed4bb6a89eafbee5f0944c7eb54d0d8a2dac962c33638a1d611e14faa71dfbb92b4b5f9236232208568a6b7d5c6f3f39ddb91771e + languageName: node + linkType: hard + +"@blazediff/core@npm:1.9.1": + version: 1.9.1 + resolution: "@blazediff/core@npm:1.9.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40blazediff%2Fcore%2F-%2Fcore-1.9.1.tgz" + checksum: 10c0/fd45cdd0544002341d74831a179ef693a81414abd348c1ff0c01086c0ea03f5e5ee284c4e16c2e6fb3670c265f90a3d85752b9360320efa9a835928e604dae77 + languageName: node + linkType: hard + "@emnapi/core@npm:1.11.1": version: 1.11.1 - resolution: "@emnapi/core@npm:1.11.1" + resolution: "@emnapi/core@npm:1.11.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40emnapi%2Fcore%2F-%2Fcore-1.11.1.tgz" dependencies: "@emnapi/wasi-threads": "npm:1.2.2" tslib: "npm:^2.4.0" @@ -160,7 +192,7 @@ __metadata: "@emnapi/core@npm:1.9.1": version: 1.9.1 - resolution: "@emnapi/core@npm:1.9.1" + resolution: "@emnapi/core@npm:1.9.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40emnapi%2Fcore%2F-%2Fcore-1.9.1.tgz" dependencies: "@emnapi/wasi-threads": "npm:1.2.0" tslib: "npm:^2.4.0" @@ -170,7 +202,7 @@ __metadata: "@emnapi/runtime@npm:1.11.1": version: 1.11.1 - resolution: "@emnapi/runtime@npm:1.11.1" + resolution: "@emnapi/runtime@npm:1.11.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40emnapi%2Fruntime%2F-%2Fruntime-1.11.1.tgz" dependencies: tslib: "npm:^2.4.0" checksum: 10c0/04332fb62076afc440aa23316c04bec42f584ca8b074e5507d08e2b33a47cbe0493b1aadb8f3c1057b64ae1e17f5bde1a7bc37f7facc9d0bc25c18197cbd366f @@ -179,7 +211,7 @@ __metadata: "@emnapi/runtime@npm:1.9.1": version: 1.9.1 - resolution: "@emnapi/runtime@npm:1.9.1" + resolution: "@emnapi/runtime@npm:1.9.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40emnapi%2Fruntime%2F-%2Fruntime-1.9.1.tgz" dependencies: tslib: "npm:^2.4.0" checksum: 10c0/750edca117e0363ab2de10622f8ee60e57d8690c2f29c49704813da5cd627c641798d7f3cb0d953c62fdc71688e02e333ddbf2c1204f38b47e3e40657332a6f5 @@ -188,7 +220,7 @@ __metadata: "@emnapi/wasi-threads@npm:1.2.0": version: 1.2.0 - resolution: "@emnapi/wasi-threads@npm:1.2.0" + resolution: "@emnapi/wasi-threads@npm:1.2.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40emnapi%2Fwasi-threads%2F-%2Fwasi-threads-1.2.0.tgz" dependencies: tslib: "npm:^2.4.0" checksum: 10c0/1e3724b5814b06c14782fda87eee9b9aa68af01576c81ffeaefdf621ddb74386e419d5b3b1027b6a8172397729d95a92f814fc4b8d3c224376428faa07a6a01a @@ -197,7 +229,7 @@ __metadata: "@emnapi/wasi-threads@npm:1.2.2": version: 1.2.2 - resolution: "@emnapi/wasi-threads@npm:1.2.2" + resolution: "@emnapi/wasi-threads@npm:1.2.2::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40emnapi%2Fwasi-threads%2F-%2Fwasi-threads-1.2.2.tgz" dependencies: tslib: "npm:^2.4.0" checksum: 10c0/f0dc8269d6b20ae5a7c7b36e7a6a333452009d461038ef4febb29da2f3f78c1e2b1576d7e8970a5c5789ed3caedc1f80f5b0c2a5373bdaf8d03b20432bb55747 @@ -206,14 +238,14 @@ __metadata: "@huggingface/blake3-jit@npm:0.0.2": version: 0.0.2 - resolution: "@huggingface/blake3-jit@npm:0.0.2" + resolution: "@huggingface/blake3-jit@npm:0.0.2::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40huggingface%2Fblake3-jit%2F-%2Fblake3-jit-0.0.2.tgz" checksum: 10c0/ba8d06a416783653c0e2abcc52529325eb776fe41781de24ec6ee3720fa299e675cd153f90880d77027e5735fcee6a5a9ae7e73410667978898dd1a66a1eeaaf languageName: node linkType: hard "@huggingface/hub@npm:^2.13.2": version: 2.13.2 - resolution: "@huggingface/hub@npm:2.13.2" + resolution: "@huggingface/hub@npm:2.13.2::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40huggingface%2Fhub%2F-%2Fhub-2.13.2.tgz" dependencies: "@huggingface/tasks": "npm:^0.21.13" "@huggingface/xetchunk-wasm": "npm:^0.1.0" @@ -228,15 +260,15 @@ __metadata: linkType: hard "@huggingface/tasks@npm:^0.21.13": - version: 0.21.16 - resolution: "@huggingface/tasks@npm:0.21.16" - checksum: 10c0/9ae807149ad8fb0520f158164f7c232e2c5f0afddc5fdbb265e06ac11f0968480db670e434870eb940b044a2bd4cf089e24dc1b3ed16431b20a6a5d43a07e094 + version: 0.21.18 + resolution: "@huggingface/tasks@npm:0.21.18::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40huggingface%2Ftasks%2F-%2Ftasks-0.21.18.tgz" + checksum: 10c0/4f6b26e7caba8bd77e9ace2d21dfb576cac96c1c859996ca4d18a1e76bf0186d86a6ff654768c68cf0be4489ff4ddc678a7fddc4ec1fb5a62df9b3b17be2c65b languageName: node linkType: hard "@huggingface/xetchunk-wasm@npm:^0.1.0": version: 0.1.0 - resolution: "@huggingface/xetchunk-wasm@npm:0.1.0" + resolution: "@huggingface/xetchunk-wasm@npm:0.1.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40huggingface%2Fxetchunk-wasm%2F-%2Fxetchunk-wasm-0.1.0.tgz" dependencies: "@huggingface/blake3-jit": "npm:0.0.2" gearhash-jit: "npm:1.0.2" @@ -246,14 +278,14 @@ __metadata: "@inquirer/ansi@npm:^2.0.7": version: 2.0.7 - resolution: "@inquirer/ansi@npm:2.0.7" + resolution: "@inquirer/ansi@npm:2.0.7::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40inquirer%2Fansi%2F-%2Fansi-2.0.7.tgz" checksum: 10c0/a574f97a899f0d9346fa26b528b3f4a9ba6dcb9172288efb6b4314d8486470ed53d2f538200f66a25b843c6e0cbf83688c6d5174a8dc6eca853b291b09609c5a languageName: node linkType: hard "@inquirer/checkbox@npm:^5.2.1": version: 5.2.1 - resolution: "@inquirer/checkbox@npm:5.2.1" + resolution: "@inquirer/checkbox@npm:5.2.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40inquirer%2Fcheckbox%2F-%2Fcheckbox-5.2.1.tgz" dependencies: "@inquirer/ansi": "npm:^2.0.7" "@inquirer/core": "npm:^11.2.1" @@ -270,7 +302,7 @@ __metadata: "@inquirer/confirm@npm:^6.1.1": version: 6.1.1 - resolution: "@inquirer/confirm@npm:6.1.1" + resolution: "@inquirer/confirm@npm:6.1.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40inquirer%2Fconfirm%2F-%2Fconfirm-6.1.1.tgz" dependencies: "@inquirer/core": "npm:^11.2.1" "@inquirer/type": "npm:^4.0.7" @@ -285,7 +317,7 @@ __metadata: "@inquirer/core@npm:^11.2.1": version: 11.2.1 - resolution: "@inquirer/core@npm:11.2.1" + resolution: "@inquirer/core@npm:11.2.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40inquirer%2Fcore%2F-%2Fcore-11.2.1.tgz" dependencies: "@inquirer/ansi": "npm:^2.0.7" "@inquirer/figures": "npm:^2.0.7" @@ -305,7 +337,7 @@ __metadata: "@inquirer/editor@npm:^5.2.2": version: 5.2.2 - resolution: "@inquirer/editor@npm:5.2.2" + resolution: "@inquirer/editor@npm:5.2.2::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40inquirer%2Feditor%2F-%2Feditor-5.2.2.tgz" dependencies: "@inquirer/core": "npm:^11.2.1" "@inquirer/external-editor": "npm:^3.0.3" @@ -321,7 +353,7 @@ __metadata: "@inquirer/expand@npm:^5.1.1": version: 5.1.1 - resolution: "@inquirer/expand@npm:5.1.1" + resolution: "@inquirer/expand@npm:5.1.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40inquirer%2Fexpand%2F-%2Fexpand-5.1.1.tgz" dependencies: "@inquirer/core": "npm:^11.2.1" "@inquirer/type": "npm:^4.0.7" @@ -336,7 +368,7 @@ __metadata: "@inquirer/external-editor@npm:^3.0.3": version: 3.0.3 - resolution: "@inquirer/external-editor@npm:3.0.3" + resolution: "@inquirer/external-editor@npm:3.0.3::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40inquirer%2Fexternal-editor%2F-%2Fexternal-editor-3.0.3.tgz" dependencies: chardet: "npm:^2.1.1" iconv-lite: "npm:^0.7.2" @@ -351,14 +383,14 @@ __metadata: "@inquirer/figures@npm:^2.0.7": version: 2.0.7 - resolution: "@inquirer/figures@npm:2.0.7" + resolution: "@inquirer/figures@npm:2.0.7::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40inquirer%2Ffigures%2F-%2Ffigures-2.0.7.tgz" checksum: 10c0/e0573dc9ad25fa3628d5164745e52852d8cd832a9918605b7716df2e37a0005a0aaf40b6d81cef2ca09cb708b200e61b82d1dcd17003f572577e233c19a9ec7b languageName: node linkType: hard "@inquirer/input@npm:^5.1.2": version: 5.1.2 - resolution: "@inquirer/input@npm:5.1.2" + resolution: "@inquirer/input@npm:5.1.2::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40inquirer%2Finput%2F-%2Finput-5.1.2.tgz" dependencies: "@inquirer/core": "npm:^11.2.1" "@inquirer/type": "npm:^4.0.7" @@ -373,7 +405,7 @@ __metadata: "@inquirer/number@npm:^4.1.1": version: 4.1.1 - resolution: "@inquirer/number@npm:4.1.1" + resolution: "@inquirer/number@npm:4.1.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40inquirer%2Fnumber%2F-%2Fnumber-4.1.1.tgz" dependencies: "@inquirer/core": "npm:^11.2.1" "@inquirer/type": "npm:^4.0.7" @@ -388,7 +420,7 @@ __metadata: "@inquirer/password@npm:^5.1.1": version: 5.1.1 - resolution: "@inquirer/password@npm:5.1.1" + resolution: "@inquirer/password@npm:5.1.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40inquirer%2Fpassword%2F-%2Fpassword-5.1.1.tgz" dependencies: "@inquirer/ansi": "npm:^2.0.7" "@inquirer/core": "npm:^11.2.1" @@ -404,7 +436,7 @@ __metadata: "@inquirer/prompts@npm:^8.5.2": version: 8.5.2 - resolution: "@inquirer/prompts@npm:8.5.2" + resolution: "@inquirer/prompts@npm:8.5.2::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40inquirer%2Fprompts%2F-%2Fprompts-8.5.2.tgz" dependencies: "@inquirer/checkbox": "npm:^5.2.1" "@inquirer/confirm": "npm:^6.1.1" @@ -427,7 +459,7 @@ __metadata: "@inquirer/rawlist@npm:^5.3.1": version: 5.3.1 - resolution: "@inquirer/rawlist@npm:5.3.1" + resolution: "@inquirer/rawlist@npm:5.3.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40inquirer%2Frawlist%2F-%2Frawlist-5.3.1.tgz" dependencies: "@inquirer/core": "npm:^11.2.1" "@inquirer/type": "npm:^4.0.7" @@ -442,7 +474,7 @@ __metadata: "@inquirer/search@npm:^4.2.1": version: 4.2.1 - resolution: "@inquirer/search@npm:4.2.1" + resolution: "@inquirer/search@npm:4.2.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40inquirer%2Fsearch%2F-%2Fsearch-4.2.1.tgz" dependencies: "@inquirer/core": "npm:^11.2.1" "@inquirer/figures": "npm:^2.0.7" @@ -458,7 +490,7 @@ __metadata: "@inquirer/select@npm:^5.2.1": version: 5.2.1 - resolution: "@inquirer/select@npm:5.2.1" + resolution: "@inquirer/select@npm:5.2.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40inquirer%2Fselect%2F-%2Fselect-5.2.1.tgz" dependencies: "@inquirer/ansi": "npm:^2.0.7" "@inquirer/core": "npm:^11.2.1" @@ -475,7 +507,7 @@ __metadata: "@inquirer/type@npm:^4.0.7": version: 4.0.7 - resolution: "@inquirer/type@npm:4.0.7" + resolution: "@inquirer/type@npm:4.0.7::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40inquirer%2Ftype%2F-%2Ftype-4.0.7.tgz" peerDependencies: "@types/node": ">=18" peerDependenciesMeta: @@ -487,17 +519,24 @@ __metadata: "@isaacs/fs-minipass@npm:^4.0.0": version: 4.0.1 - resolution: "@isaacs/fs-minipass@npm:4.0.1" + resolution: "@isaacs/fs-minipass@npm:4.0.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40isaacs%2Ffs-minipass%2F-%2Ffs-minipass-4.0.1.tgz" dependencies: minipass: "npm:^7.0.4" checksum: 10c0/c25b6dc1598790d5b55c0947a9b7d111cfa92594db5296c3b907e2f533c033666f692a3939eadac17b1c7c40d362d0b0635dc874cbfe3e70db7c2b07cc97a5d2 languageName: node linkType: hard +"@jridgewell/sourcemap-codec@npm:^1.5.5": + version: 1.5.5 + resolution: "@jridgewell/sourcemap-codec@npm:1.5.5::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40jridgewell%2Fsourcemap-codec%2F-%2Fsourcemap-codec-1.5.5.tgz" + checksum: 10c0/f9e538f302b63c0ebc06eecb1dd9918dd4289ed36147a0ddce35d6ea4d7ebbda243cda7b2213b6a5e1d8087a298d5cf630fb2bd39329cdecb82017023f6081a0 + languageName: node + linkType: hard + "@jsr/std__collections@npm:^1.1.3": - version: 1.2.0 - resolution: "@jsr/std__collections@npm:1.2.0::__archiveUrl=https%3A%2F%2Fnpm.jsr.io%2F~%2F11%2F%40jsr%2Fstd__collections%2F1.2.0.tgz" - checksum: 10c0/55f2db612ec1ad2647d5d850a88eeff5ebf737aab47886b7ade29c95169f244019b69e3b2927aff9e9e52124068450d47748c8f8342bfd6046246d4216fb1ba8 + version: 1.3.0 + resolution: "@jsr/std__collections@npm:1.3.0::__archiveUrl=https%3A%2F%2Fnpm.jsr.io%2F~%2F11%2F%40jsr%2Fstd__collections%2F1.3.0.tgz" + checksum: 10c0/ac08d18cd78aad901b4d1c60ed961f7e0e59d6e8759b98f189d95e84f76b2088690e7d4cc2c801764ab7175d8d55ffb69c67d7cbedc44d38f1592ab8188618b5 languageName: node linkType: hard @@ -605,7 +644,7 @@ __metadata: "@napi-rs/cli@npm:^3.7.2": version: 3.7.2 - resolution: "@napi-rs/cli@npm:3.7.2" + resolution: "@napi-rs/cli@npm:3.7.2::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fcli%2F-%2Fcli-3.7.2.tgz" dependencies: "@inquirer/prompts": "npm:^8.5.2" "@napi-rs/cross-toolchain": "npm:^1.0.3" @@ -633,7 +672,7 @@ __metadata: "@napi-rs/cross-toolchain@npm:^1.0.3": version: 1.0.3 - resolution: "@napi-rs/cross-toolchain@npm:1.0.3" + resolution: "@napi-rs/cross-toolchain@npm:1.0.3::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fcross-toolchain%2F-%2Fcross-toolchain-1.0.3.tgz" dependencies: "@napi-rs/lzma": "npm:^1.4.5" "@napi-rs/tar": "npm:^1.1.0" @@ -676,70 +715,133 @@ __metadata: "@napi-rs/image-android-arm64@npm:1.13.0": version: 1.13.0 - resolution: "@napi-rs/image-android-arm64@npm:1.13.0" + resolution: "@napi-rs/image-android-arm64@npm:1.13.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fimage-android-arm64%2F-%2Fimage-android-arm64-1.13.0.tgz" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"@napi-rs/image-android-arm64@npm:1.14.0": + version: 1.14.0 + resolution: "@napi-rs/image-android-arm64@npm:1.14.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fimage-android-arm64%2F-%2Fimage-android-arm64-1.14.0.tgz" conditions: os=android & cpu=arm64 languageName: node linkType: hard "@napi-rs/image-darwin-arm64@npm:1.13.0": version: 1.13.0 - resolution: "@napi-rs/image-darwin-arm64@npm:1.13.0" + resolution: "@napi-rs/image-darwin-arm64@npm:1.13.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fimage-darwin-arm64%2F-%2Fimage-darwin-arm64-1.13.0.tgz" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@napi-rs/image-darwin-arm64@npm:1.14.0": + version: 1.14.0 + resolution: "@napi-rs/image-darwin-arm64@npm:1.14.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fimage-darwin-arm64%2F-%2Fimage-darwin-arm64-1.14.0.tgz" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard "@napi-rs/image-darwin-x64@npm:1.13.0": version: 1.13.0 - resolution: "@napi-rs/image-darwin-x64@npm:1.13.0" + resolution: "@napi-rs/image-darwin-x64@npm:1.13.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fimage-darwin-x64%2F-%2Fimage-darwin-x64-1.13.0.tgz" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@napi-rs/image-darwin-x64@npm:1.14.0": + version: 1.14.0 + resolution: "@napi-rs/image-darwin-x64@npm:1.14.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fimage-darwin-x64%2F-%2Fimage-darwin-x64-1.14.0.tgz" conditions: os=darwin & cpu=x64 languageName: node linkType: hard "@napi-rs/image-freebsd-x64@npm:1.13.0": version: 1.13.0 - resolution: "@napi-rs/image-freebsd-x64@npm:1.13.0" + resolution: "@napi-rs/image-freebsd-x64@npm:1.13.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fimage-freebsd-x64%2F-%2Fimage-freebsd-x64-1.13.0.tgz" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"@napi-rs/image-freebsd-x64@npm:1.14.0": + version: 1.14.0 + resolution: "@napi-rs/image-freebsd-x64@npm:1.14.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fimage-freebsd-x64%2F-%2Fimage-freebsd-x64-1.14.0.tgz" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard "@napi-rs/image-linux-arm-gnueabihf@npm:1.13.0": version: 1.13.0 - resolution: "@napi-rs/image-linux-arm-gnueabihf@npm:1.13.0" + resolution: "@napi-rs/image-linux-arm-gnueabihf@npm:1.13.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fimage-linux-arm-gnueabihf%2F-%2Fimage-linux-arm-gnueabihf-1.13.0.tgz" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@napi-rs/image-linux-arm-gnueabihf@npm:1.14.0": + version: 1.14.0 + resolution: "@napi-rs/image-linux-arm-gnueabihf@npm:1.14.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fimage-linux-arm-gnueabihf%2F-%2Fimage-linux-arm-gnueabihf-1.14.0.tgz" conditions: os=linux & cpu=arm languageName: node linkType: hard "@napi-rs/image-linux-arm64-gnu@npm:1.13.0": version: 1.13.0 - resolution: "@napi-rs/image-linux-arm64-gnu@npm:1.13.0" + resolution: "@napi-rs/image-linux-arm64-gnu@npm:1.13.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fimage-linux-arm64-gnu%2F-%2Fimage-linux-arm64-gnu-1.13.0.tgz" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@napi-rs/image-linux-arm64-gnu@npm:1.14.0": + version: 1.14.0 + resolution: "@napi-rs/image-linux-arm64-gnu@npm:1.14.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fimage-linux-arm64-gnu%2F-%2Fimage-linux-arm64-gnu-1.14.0.tgz" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard "@napi-rs/image-linux-arm64-musl@npm:1.13.0": version: 1.13.0 - resolution: "@napi-rs/image-linux-arm64-musl@npm:1.13.0" + resolution: "@napi-rs/image-linux-arm64-musl@npm:1.13.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fimage-linux-arm64-musl%2F-%2Fimage-linux-arm64-musl-1.13.0.tgz" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@napi-rs/image-linux-arm64-musl@npm:1.14.0": + version: 1.14.0 + resolution: "@napi-rs/image-linux-arm64-musl@npm:1.14.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fimage-linux-arm64-musl%2F-%2Fimage-linux-arm64-musl-1.14.0.tgz" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard "@napi-rs/image-linux-x64-gnu@npm:1.13.0": version: 1.13.0 - resolution: "@napi-rs/image-linux-x64-gnu@npm:1.13.0" + resolution: "@napi-rs/image-linux-x64-gnu@npm:1.13.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fimage-linux-x64-gnu%2F-%2Fimage-linux-x64-gnu-1.13.0.tgz" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@napi-rs/image-linux-x64-gnu@npm:1.14.0": + version: 1.14.0 + resolution: "@napi-rs/image-linux-x64-gnu@npm:1.14.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fimage-linux-x64-gnu%2F-%2Fimage-linux-x64-gnu-1.14.0.tgz" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard "@napi-rs/image-linux-x64-musl@npm:1.13.0": version: 1.13.0 - resolution: "@napi-rs/image-linux-x64-musl@npm:1.13.0" + resolution: "@napi-rs/image-linux-x64-musl@npm:1.13.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fimage-linux-x64-musl%2F-%2Fimage-linux-x64-musl-1.13.0.tgz" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@napi-rs/image-linux-x64-musl@npm:1.14.0": + version: 1.14.0 + resolution: "@napi-rs/image-linux-x64-musl@npm:1.14.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fimage-linux-x64-musl%2F-%2Fimage-linux-x64-musl-1.14.0.tgz" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard "@napi-rs/image-wasm32-wasi@npm:1.13.0": version: 1.13.0 - resolution: "@napi-rs/image-wasm32-wasi@npm:1.13.0" + resolution: "@napi-rs/image-wasm32-wasi@npm:1.13.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fimage-wasm32-wasi%2F-%2Fimage-wasm32-wasi-1.13.0.tgz" dependencies: "@emnapi/core": "npm:1.11.1" "@emnapi/runtime": "npm:1.11.1" @@ -748,30 +850,62 @@ __metadata: languageName: node linkType: hard +"@napi-rs/image-wasm32-wasi@npm:1.14.0": + version: 1.14.0 + resolution: "@napi-rs/image-wasm32-wasi@npm:1.14.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fimage-wasm32-wasi%2F-%2Fimage-wasm32-wasi-1.14.0.tgz" + dependencies: + "@emnapi/core": "npm:1.11.1" + "@emnapi/runtime": "npm:1.11.1" + "@napi-rs/wasm-runtime": "npm:^1.1.6" + conditions: cpu=wasm32 + languageName: node + linkType: hard + "@napi-rs/image-win32-arm64-msvc@npm:1.13.0": version: 1.13.0 - resolution: "@napi-rs/image-win32-arm64-msvc@npm:1.13.0" + resolution: "@napi-rs/image-win32-arm64-msvc@npm:1.13.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fimage-win32-arm64-msvc%2F-%2Fimage-win32-arm64-msvc-1.13.0.tgz" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@napi-rs/image-win32-arm64-msvc@npm:1.14.0": + version: 1.14.0 + resolution: "@napi-rs/image-win32-arm64-msvc@npm:1.14.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fimage-win32-arm64-msvc%2F-%2Fimage-win32-arm64-msvc-1.14.0.tgz" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard "@napi-rs/image-win32-ia32-msvc@npm:1.13.0": version: 1.13.0 - resolution: "@napi-rs/image-win32-ia32-msvc@npm:1.13.0" + resolution: "@napi-rs/image-win32-ia32-msvc@npm:1.13.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fimage-win32-ia32-msvc%2F-%2Fimage-win32-ia32-msvc-1.13.0.tgz" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@napi-rs/image-win32-ia32-msvc@npm:1.14.0": + version: 1.14.0 + resolution: "@napi-rs/image-win32-ia32-msvc@npm:1.14.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fimage-win32-ia32-msvc%2F-%2Fimage-win32-ia32-msvc-1.14.0.tgz" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard "@napi-rs/image-win32-x64-msvc@npm:1.13.0": version: 1.13.0 - resolution: "@napi-rs/image-win32-x64-msvc@npm:1.13.0" + resolution: "@napi-rs/image-win32-x64-msvc@npm:1.13.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fimage-win32-x64-msvc%2F-%2Fimage-win32-x64-msvc-1.13.0.tgz" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@napi-rs/image-win32-x64-msvc@npm:1.14.0": + version: 1.14.0 + resolution: "@napi-rs/image-win32-x64-msvc@npm:1.14.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fimage-win32-x64-msvc%2F-%2Fimage-win32-x64-msvc-1.14.0.tgz" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"@napi-rs/image@npm:1.13.0, @napi-rs/image@npm:^1.13.0": +"@napi-rs/image@npm:1.13.0": version: 1.13.0 - resolution: "@napi-rs/image@npm:1.13.0" + resolution: "@napi-rs/image@npm:1.13.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fimage%2F-%2Fimage-1.13.0.tgz" dependencies: "@napi-rs/image-android-arm64": "npm:1.13.0" "@napi-rs/image-darwin-arm64": "npm:1.13.0" @@ -817,93 +951,141 @@ __metadata: languageName: node linkType: hard +"@napi-rs/image@npm:^1.13.0": + version: 1.14.0 + resolution: "@napi-rs/image@npm:1.14.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fimage%2F-%2Fimage-1.14.0.tgz" + dependencies: + "@napi-rs/image-android-arm64": "npm:1.14.0" + "@napi-rs/image-darwin-arm64": "npm:1.14.0" + "@napi-rs/image-darwin-x64": "npm:1.14.0" + "@napi-rs/image-freebsd-x64": "npm:1.14.0" + "@napi-rs/image-linux-arm-gnueabihf": "npm:1.14.0" + "@napi-rs/image-linux-arm64-gnu": "npm:1.14.0" + "@napi-rs/image-linux-arm64-musl": "npm:1.14.0" + "@napi-rs/image-linux-x64-gnu": "npm:1.14.0" + "@napi-rs/image-linux-x64-musl": "npm:1.14.0" + "@napi-rs/image-wasm32-wasi": "npm:1.14.0" + "@napi-rs/image-win32-arm64-msvc": "npm:1.14.0" + "@napi-rs/image-win32-ia32-msvc": "npm:1.14.0" + "@napi-rs/image-win32-x64-msvc": "npm:1.14.0" + dependenciesMeta: + "@napi-rs/image-android-arm64": + optional: true + "@napi-rs/image-darwin-arm64": + optional: true + "@napi-rs/image-darwin-x64": + optional: true + "@napi-rs/image-freebsd-x64": + optional: true + "@napi-rs/image-linux-arm-gnueabihf": + optional: true + "@napi-rs/image-linux-arm64-gnu": + optional: true + "@napi-rs/image-linux-arm64-musl": + optional: true + "@napi-rs/image-linux-x64-gnu": + optional: true + "@napi-rs/image-linux-x64-musl": + optional: true + "@napi-rs/image-wasm32-wasi": + optional: true + "@napi-rs/image-win32-arm64-msvc": + optional: true + "@napi-rs/image-win32-ia32-msvc": + optional: true + "@napi-rs/image-win32-x64-msvc": + optional: true + checksum: 10c0/84239d0c79d0298e8aa12fec576eaba550a7568f101403766ce047e4d4b072a1789aecca9aaebe17bbbc5e8c47766f2e217421fdfdf2476413d37548c2ee5cc0 + languageName: node + linkType: hard + "@napi-rs/keyring-darwin-arm64@npm:1.3.0": version: 1.3.0 - resolution: "@napi-rs/keyring-darwin-arm64@npm:1.3.0" + resolution: "@napi-rs/keyring-darwin-arm64@npm:1.3.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fkeyring-darwin-arm64%2F-%2Fkeyring-darwin-arm64-1.3.0.tgz" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard "@napi-rs/keyring-darwin-x64@npm:1.3.0": version: 1.3.0 - resolution: "@napi-rs/keyring-darwin-x64@npm:1.3.0" + resolution: "@napi-rs/keyring-darwin-x64@npm:1.3.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fkeyring-darwin-x64%2F-%2Fkeyring-darwin-x64-1.3.0.tgz" conditions: os=darwin & cpu=x64 languageName: node linkType: hard "@napi-rs/keyring-freebsd-x64@npm:1.3.0": version: 1.3.0 - resolution: "@napi-rs/keyring-freebsd-x64@npm:1.3.0" + resolution: "@napi-rs/keyring-freebsd-x64@npm:1.3.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fkeyring-freebsd-x64%2F-%2Fkeyring-freebsd-x64-1.3.0.tgz" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard "@napi-rs/keyring-linux-arm-gnueabihf@npm:1.3.0": version: 1.3.0 - resolution: "@napi-rs/keyring-linux-arm-gnueabihf@npm:1.3.0" + resolution: "@napi-rs/keyring-linux-arm-gnueabihf@npm:1.3.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fkeyring-linux-arm-gnueabihf%2F-%2Fkeyring-linux-arm-gnueabihf-1.3.0.tgz" conditions: os=linux & cpu=arm languageName: node linkType: hard "@napi-rs/keyring-linux-arm64-gnu@npm:1.3.0": version: 1.3.0 - resolution: "@napi-rs/keyring-linux-arm64-gnu@npm:1.3.0" + resolution: "@napi-rs/keyring-linux-arm64-gnu@npm:1.3.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fkeyring-linux-arm64-gnu%2F-%2Fkeyring-linux-arm64-gnu-1.3.0.tgz" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard "@napi-rs/keyring-linux-arm64-musl@npm:1.3.0": version: 1.3.0 - resolution: "@napi-rs/keyring-linux-arm64-musl@npm:1.3.0" + resolution: "@napi-rs/keyring-linux-arm64-musl@npm:1.3.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fkeyring-linux-arm64-musl%2F-%2Fkeyring-linux-arm64-musl-1.3.0.tgz" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard "@napi-rs/keyring-linux-riscv64-gnu@npm:1.3.0": version: 1.3.0 - resolution: "@napi-rs/keyring-linux-riscv64-gnu@npm:1.3.0" + resolution: "@napi-rs/keyring-linux-riscv64-gnu@npm:1.3.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fkeyring-linux-riscv64-gnu%2F-%2Fkeyring-linux-riscv64-gnu-1.3.0.tgz" conditions: os=linux & cpu=riscv64 & libc=glibc languageName: node linkType: hard "@napi-rs/keyring-linux-x64-gnu@npm:1.3.0": version: 1.3.0 - resolution: "@napi-rs/keyring-linux-x64-gnu@npm:1.3.0" + resolution: "@napi-rs/keyring-linux-x64-gnu@npm:1.3.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fkeyring-linux-x64-gnu%2F-%2Fkeyring-linux-x64-gnu-1.3.0.tgz" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard "@napi-rs/keyring-linux-x64-musl@npm:1.3.0": version: 1.3.0 - resolution: "@napi-rs/keyring-linux-x64-musl@npm:1.3.0" + resolution: "@napi-rs/keyring-linux-x64-musl@npm:1.3.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fkeyring-linux-x64-musl%2F-%2Fkeyring-linux-x64-musl-1.3.0.tgz" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard "@napi-rs/keyring-win32-arm64-msvc@npm:1.3.0": version: 1.3.0 - resolution: "@napi-rs/keyring-win32-arm64-msvc@npm:1.3.0" + resolution: "@napi-rs/keyring-win32-arm64-msvc@npm:1.3.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fkeyring-win32-arm64-msvc%2F-%2Fkeyring-win32-arm64-msvc-1.3.0.tgz" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard "@napi-rs/keyring-win32-ia32-msvc@npm:1.3.0": version: 1.3.0 - resolution: "@napi-rs/keyring-win32-ia32-msvc@npm:1.3.0" + resolution: "@napi-rs/keyring-win32-ia32-msvc@npm:1.3.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fkeyring-win32-ia32-msvc%2F-%2Fkeyring-win32-ia32-msvc-1.3.0.tgz" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard "@napi-rs/keyring-win32-x64-msvc@npm:1.3.0": version: 1.3.0 - resolution: "@napi-rs/keyring-win32-x64-msvc@npm:1.3.0" + resolution: "@napi-rs/keyring-win32-x64-msvc@npm:1.3.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fkeyring-win32-x64-msvc%2F-%2Fkeyring-win32-x64-msvc-1.3.0.tgz" conditions: os=win32 & cpu=x64 languageName: node linkType: hard "@napi-rs/keyring@npm:^1.3.0": version: 1.3.0 - resolution: "@napi-rs/keyring@npm:1.3.0" + resolution: "@napi-rs/keyring@npm:1.3.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fkeyring%2F-%2Fkeyring-1.3.0.tgz" dependencies: "@napi-rs/keyring-darwin-arm64": "npm:1.3.0" "@napi-rs/keyring-darwin-x64": "npm:1.3.0" @@ -948,98 +1130,98 @@ __metadata: "@napi-rs/lzma-android-arm-eabi@npm:1.4.5": version: 1.4.5 - resolution: "@napi-rs/lzma-android-arm-eabi@npm:1.4.5" + resolution: "@napi-rs/lzma-android-arm-eabi@npm:1.4.5::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Flzma-android-arm-eabi%2F-%2Flzma-android-arm-eabi-1.4.5.tgz" conditions: os=android & cpu=arm languageName: node linkType: hard "@napi-rs/lzma-android-arm64@npm:1.4.5": version: 1.4.5 - resolution: "@napi-rs/lzma-android-arm64@npm:1.4.5" + resolution: "@napi-rs/lzma-android-arm64@npm:1.4.5::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Flzma-android-arm64%2F-%2Flzma-android-arm64-1.4.5.tgz" conditions: os=android & cpu=arm64 languageName: node linkType: hard "@napi-rs/lzma-darwin-arm64@npm:1.4.5": version: 1.4.5 - resolution: "@napi-rs/lzma-darwin-arm64@npm:1.4.5" + resolution: "@napi-rs/lzma-darwin-arm64@npm:1.4.5::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Flzma-darwin-arm64%2F-%2Flzma-darwin-arm64-1.4.5.tgz" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard "@napi-rs/lzma-darwin-x64@npm:1.4.5": version: 1.4.5 - resolution: "@napi-rs/lzma-darwin-x64@npm:1.4.5" + resolution: "@napi-rs/lzma-darwin-x64@npm:1.4.5::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Flzma-darwin-x64%2F-%2Flzma-darwin-x64-1.4.5.tgz" conditions: os=darwin & cpu=x64 languageName: node linkType: hard "@napi-rs/lzma-freebsd-x64@npm:1.4.5": version: 1.4.5 - resolution: "@napi-rs/lzma-freebsd-x64@npm:1.4.5" + resolution: "@napi-rs/lzma-freebsd-x64@npm:1.4.5::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Flzma-freebsd-x64%2F-%2Flzma-freebsd-x64-1.4.5.tgz" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard "@napi-rs/lzma-linux-arm-gnueabihf@npm:1.4.5": version: 1.4.5 - resolution: "@napi-rs/lzma-linux-arm-gnueabihf@npm:1.4.5" + resolution: "@napi-rs/lzma-linux-arm-gnueabihf@npm:1.4.5::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Flzma-linux-arm-gnueabihf%2F-%2Flzma-linux-arm-gnueabihf-1.4.5.tgz" conditions: os=linux & cpu=arm languageName: node linkType: hard "@napi-rs/lzma-linux-arm64-gnu@npm:1.4.5": version: 1.4.5 - resolution: "@napi-rs/lzma-linux-arm64-gnu@npm:1.4.5" + resolution: "@napi-rs/lzma-linux-arm64-gnu@npm:1.4.5::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Flzma-linux-arm64-gnu%2F-%2Flzma-linux-arm64-gnu-1.4.5.tgz" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard "@napi-rs/lzma-linux-arm64-musl@npm:1.4.5": version: 1.4.5 - resolution: "@napi-rs/lzma-linux-arm64-musl@npm:1.4.5" + resolution: "@napi-rs/lzma-linux-arm64-musl@npm:1.4.5::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Flzma-linux-arm64-musl%2F-%2Flzma-linux-arm64-musl-1.4.5.tgz" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard "@napi-rs/lzma-linux-ppc64-gnu@npm:1.4.5": version: 1.4.5 - resolution: "@napi-rs/lzma-linux-ppc64-gnu@npm:1.4.5" + resolution: "@napi-rs/lzma-linux-ppc64-gnu@npm:1.4.5::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Flzma-linux-ppc64-gnu%2F-%2Flzma-linux-ppc64-gnu-1.4.5.tgz" conditions: os=linux & cpu=ppc64 & libc=glibc languageName: node linkType: hard "@napi-rs/lzma-linux-riscv64-gnu@npm:1.4.5": version: 1.4.5 - resolution: "@napi-rs/lzma-linux-riscv64-gnu@npm:1.4.5" + resolution: "@napi-rs/lzma-linux-riscv64-gnu@npm:1.4.5::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Flzma-linux-riscv64-gnu%2F-%2Flzma-linux-riscv64-gnu-1.4.5.tgz" conditions: os=linux & cpu=riscv64 & libc=glibc languageName: node linkType: hard "@napi-rs/lzma-linux-s390x-gnu@npm:1.4.5": version: 1.4.5 - resolution: "@napi-rs/lzma-linux-s390x-gnu@npm:1.4.5" + resolution: "@napi-rs/lzma-linux-s390x-gnu@npm:1.4.5::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Flzma-linux-s390x-gnu%2F-%2Flzma-linux-s390x-gnu-1.4.5.tgz" conditions: os=linux & cpu=s390x & libc=glibc languageName: node linkType: hard "@napi-rs/lzma-linux-x64-gnu@npm:1.4.5": version: 1.4.5 - resolution: "@napi-rs/lzma-linux-x64-gnu@npm:1.4.5" + resolution: "@napi-rs/lzma-linux-x64-gnu@npm:1.4.5::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Flzma-linux-x64-gnu%2F-%2Flzma-linux-x64-gnu-1.4.5.tgz" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard "@napi-rs/lzma-linux-x64-musl@npm:1.4.5": version: 1.4.5 - resolution: "@napi-rs/lzma-linux-x64-musl@npm:1.4.5" + resolution: "@napi-rs/lzma-linux-x64-musl@npm:1.4.5::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Flzma-linux-x64-musl%2F-%2Flzma-linux-x64-musl-1.4.5.tgz" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard "@napi-rs/lzma-wasm32-wasi@npm:1.4.5": version: 1.4.5 - resolution: "@napi-rs/lzma-wasm32-wasi@npm:1.4.5" + resolution: "@napi-rs/lzma-wasm32-wasi@npm:1.4.5::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Flzma-wasm32-wasi%2F-%2Flzma-wasm32-wasi-1.4.5.tgz" dependencies: "@napi-rs/wasm-runtime": "npm:^1.0.3" conditions: cpu=wasm32 @@ -1048,28 +1230,28 @@ __metadata: "@napi-rs/lzma-win32-arm64-msvc@npm:1.4.5": version: 1.4.5 - resolution: "@napi-rs/lzma-win32-arm64-msvc@npm:1.4.5" + resolution: "@napi-rs/lzma-win32-arm64-msvc@npm:1.4.5::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Flzma-win32-arm64-msvc%2F-%2Flzma-win32-arm64-msvc-1.4.5.tgz" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard "@napi-rs/lzma-win32-ia32-msvc@npm:1.4.5": version: 1.4.5 - resolution: "@napi-rs/lzma-win32-ia32-msvc@npm:1.4.5" + resolution: "@napi-rs/lzma-win32-ia32-msvc@npm:1.4.5::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Flzma-win32-ia32-msvc%2F-%2Flzma-win32-ia32-msvc-1.4.5.tgz" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard "@napi-rs/lzma-win32-x64-msvc@npm:1.4.5": version: 1.4.5 - resolution: "@napi-rs/lzma-win32-x64-msvc@npm:1.4.5" + resolution: "@napi-rs/lzma-win32-x64-msvc@npm:1.4.5::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Flzma-win32-x64-msvc%2F-%2Flzma-win32-x64-msvc-1.4.5.tgz" conditions: os=win32 & cpu=x64 languageName: node linkType: hard "@napi-rs/lzma@npm:^1.4.5": version: 1.4.5 - resolution: "@napi-rs/lzma@npm:1.4.5" + resolution: "@napi-rs/lzma@npm:1.4.5::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Flzma%2F-%2Flzma-1.4.5.tgz" dependencies: "@napi-rs/lzma-android-arm-eabi": "npm:1.4.5" "@napi-rs/lzma-android-arm64": "npm:1.4.5" @@ -1129,112 +1311,112 @@ __metadata: "@napi-rs/simple-git-android-arm-eabi@npm:0.1.22": version: 0.1.22 - resolution: "@napi-rs/simple-git-android-arm-eabi@npm:0.1.22" + resolution: "@napi-rs/simple-git-android-arm-eabi@npm:0.1.22::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fsimple-git-android-arm-eabi%2F-%2Fsimple-git-android-arm-eabi-0.1.22.tgz" conditions: os=android & cpu=arm languageName: node linkType: hard "@napi-rs/simple-git-android-arm64@npm:0.1.22": version: 0.1.22 - resolution: "@napi-rs/simple-git-android-arm64@npm:0.1.22" + resolution: "@napi-rs/simple-git-android-arm64@npm:0.1.22::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fsimple-git-android-arm64%2F-%2Fsimple-git-android-arm64-0.1.22.tgz" conditions: os=android & cpu=arm64 languageName: node linkType: hard "@napi-rs/simple-git-darwin-arm64@npm:0.1.22": version: 0.1.22 - resolution: "@napi-rs/simple-git-darwin-arm64@npm:0.1.22" + resolution: "@napi-rs/simple-git-darwin-arm64@npm:0.1.22::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fsimple-git-darwin-arm64%2F-%2Fsimple-git-darwin-arm64-0.1.22.tgz" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard "@napi-rs/simple-git-darwin-x64@npm:0.1.22": version: 0.1.22 - resolution: "@napi-rs/simple-git-darwin-x64@npm:0.1.22" + resolution: "@napi-rs/simple-git-darwin-x64@npm:0.1.22::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fsimple-git-darwin-x64%2F-%2Fsimple-git-darwin-x64-0.1.22.tgz" conditions: os=darwin & cpu=x64 languageName: node linkType: hard "@napi-rs/simple-git-freebsd-x64@npm:0.1.22": version: 0.1.22 - resolution: "@napi-rs/simple-git-freebsd-x64@npm:0.1.22" + resolution: "@napi-rs/simple-git-freebsd-x64@npm:0.1.22::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fsimple-git-freebsd-x64%2F-%2Fsimple-git-freebsd-x64-0.1.22.tgz" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard "@napi-rs/simple-git-linux-arm-gnueabihf@npm:0.1.22": version: 0.1.22 - resolution: "@napi-rs/simple-git-linux-arm-gnueabihf@npm:0.1.22" + resolution: "@napi-rs/simple-git-linux-arm-gnueabihf@npm:0.1.22::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fsimple-git-linux-arm-gnueabihf%2F-%2Fsimple-git-linux-arm-gnueabihf-0.1.22.tgz" conditions: os=linux & cpu=arm languageName: node linkType: hard "@napi-rs/simple-git-linux-arm64-gnu@npm:0.1.22": version: 0.1.22 - resolution: "@napi-rs/simple-git-linux-arm64-gnu@npm:0.1.22" + resolution: "@napi-rs/simple-git-linux-arm64-gnu@npm:0.1.22::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fsimple-git-linux-arm64-gnu%2F-%2Fsimple-git-linux-arm64-gnu-0.1.22.tgz" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard "@napi-rs/simple-git-linux-arm64-musl@npm:0.1.22": version: 0.1.22 - resolution: "@napi-rs/simple-git-linux-arm64-musl@npm:0.1.22" + resolution: "@napi-rs/simple-git-linux-arm64-musl@npm:0.1.22::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fsimple-git-linux-arm64-musl%2F-%2Fsimple-git-linux-arm64-musl-0.1.22.tgz" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard "@napi-rs/simple-git-linux-ppc64-gnu@npm:0.1.22": version: 0.1.22 - resolution: "@napi-rs/simple-git-linux-ppc64-gnu@npm:0.1.22" + resolution: "@napi-rs/simple-git-linux-ppc64-gnu@npm:0.1.22::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fsimple-git-linux-ppc64-gnu%2F-%2Fsimple-git-linux-ppc64-gnu-0.1.22.tgz" conditions: os=linux & cpu=ppc64 & libc=glibc languageName: node linkType: hard "@napi-rs/simple-git-linux-s390x-gnu@npm:0.1.22": version: 0.1.22 - resolution: "@napi-rs/simple-git-linux-s390x-gnu@npm:0.1.22" + resolution: "@napi-rs/simple-git-linux-s390x-gnu@npm:0.1.22::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fsimple-git-linux-s390x-gnu%2F-%2Fsimple-git-linux-s390x-gnu-0.1.22.tgz" conditions: os=linux & cpu=s390x & libc=glibc languageName: node linkType: hard "@napi-rs/simple-git-linux-x64-gnu@npm:0.1.22": version: 0.1.22 - resolution: "@napi-rs/simple-git-linux-x64-gnu@npm:0.1.22" + resolution: "@napi-rs/simple-git-linux-x64-gnu@npm:0.1.22::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fsimple-git-linux-x64-gnu%2F-%2Fsimple-git-linux-x64-gnu-0.1.22.tgz" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard "@napi-rs/simple-git-linux-x64-musl@npm:0.1.22": version: 0.1.22 - resolution: "@napi-rs/simple-git-linux-x64-musl@npm:0.1.22" + resolution: "@napi-rs/simple-git-linux-x64-musl@npm:0.1.22::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fsimple-git-linux-x64-musl%2F-%2Fsimple-git-linux-x64-musl-0.1.22.tgz" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard "@napi-rs/simple-git-win32-arm64-msvc@npm:0.1.22": version: 0.1.22 - resolution: "@napi-rs/simple-git-win32-arm64-msvc@npm:0.1.22" + resolution: "@napi-rs/simple-git-win32-arm64-msvc@npm:0.1.22::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fsimple-git-win32-arm64-msvc%2F-%2Fsimple-git-win32-arm64-msvc-0.1.22.tgz" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard "@napi-rs/simple-git-win32-ia32-msvc@npm:0.1.22": version: 0.1.22 - resolution: "@napi-rs/simple-git-win32-ia32-msvc@npm:0.1.22" + resolution: "@napi-rs/simple-git-win32-ia32-msvc@npm:0.1.22::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fsimple-git-win32-ia32-msvc%2F-%2Fsimple-git-win32-ia32-msvc-0.1.22.tgz" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard "@napi-rs/simple-git-win32-x64-msvc@npm:0.1.22": version: 0.1.22 - resolution: "@napi-rs/simple-git-win32-x64-msvc@npm:0.1.22" + resolution: "@napi-rs/simple-git-win32-x64-msvc@npm:0.1.22::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fsimple-git-win32-x64-msvc%2F-%2Fsimple-git-win32-x64-msvc-0.1.22.tgz" conditions: os=win32 & cpu=x64 languageName: node linkType: hard "@napi-rs/simple-git@npm:^0.1.22": version: 0.1.22 - resolution: "@napi-rs/simple-git@npm:0.1.22" + resolution: "@napi-rs/simple-git@npm:0.1.22::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fsimple-git%2F-%2Fsimple-git-0.1.22.tgz" dependencies: "@napi-rs/simple-git-android-arm-eabi": "npm:0.1.22" "@napi-rs/simple-git-android-arm64": "npm:0.1.22" @@ -1288,91 +1470,91 @@ __metadata: "@napi-rs/tar-android-arm-eabi@npm:1.1.0": version: 1.1.0 - resolution: "@napi-rs/tar-android-arm-eabi@npm:1.1.0" + resolution: "@napi-rs/tar-android-arm-eabi@npm:1.1.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Ftar-android-arm-eabi%2F-%2Ftar-android-arm-eabi-1.1.0.tgz" conditions: os=android & cpu=arm languageName: node linkType: hard "@napi-rs/tar-android-arm64@npm:1.1.0": version: 1.1.0 - resolution: "@napi-rs/tar-android-arm64@npm:1.1.0" + resolution: "@napi-rs/tar-android-arm64@npm:1.1.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Ftar-android-arm64%2F-%2Ftar-android-arm64-1.1.0.tgz" conditions: os=android & cpu=arm64 languageName: node linkType: hard "@napi-rs/tar-darwin-arm64@npm:1.1.0": version: 1.1.0 - resolution: "@napi-rs/tar-darwin-arm64@npm:1.1.0" + resolution: "@napi-rs/tar-darwin-arm64@npm:1.1.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Ftar-darwin-arm64%2F-%2Ftar-darwin-arm64-1.1.0.tgz" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard "@napi-rs/tar-darwin-x64@npm:1.1.0": version: 1.1.0 - resolution: "@napi-rs/tar-darwin-x64@npm:1.1.0" + resolution: "@napi-rs/tar-darwin-x64@npm:1.1.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Ftar-darwin-x64%2F-%2Ftar-darwin-x64-1.1.0.tgz" conditions: os=darwin & cpu=x64 languageName: node linkType: hard "@napi-rs/tar-freebsd-x64@npm:1.1.0": version: 1.1.0 - resolution: "@napi-rs/tar-freebsd-x64@npm:1.1.0" + resolution: "@napi-rs/tar-freebsd-x64@npm:1.1.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Ftar-freebsd-x64%2F-%2Ftar-freebsd-x64-1.1.0.tgz" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard "@napi-rs/tar-linux-arm-gnueabihf@npm:1.1.0": version: 1.1.0 - resolution: "@napi-rs/tar-linux-arm-gnueabihf@npm:1.1.0" + resolution: "@napi-rs/tar-linux-arm-gnueabihf@npm:1.1.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Ftar-linux-arm-gnueabihf%2F-%2Ftar-linux-arm-gnueabihf-1.1.0.tgz" conditions: os=linux & cpu=arm languageName: node linkType: hard "@napi-rs/tar-linux-arm64-gnu@npm:1.1.0": version: 1.1.0 - resolution: "@napi-rs/tar-linux-arm64-gnu@npm:1.1.0" + resolution: "@napi-rs/tar-linux-arm64-gnu@npm:1.1.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Ftar-linux-arm64-gnu%2F-%2Ftar-linux-arm64-gnu-1.1.0.tgz" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard "@napi-rs/tar-linux-arm64-musl@npm:1.1.0": version: 1.1.0 - resolution: "@napi-rs/tar-linux-arm64-musl@npm:1.1.0" + resolution: "@napi-rs/tar-linux-arm64-musl@npm:1.1.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Ftar-linux-arm64-musl%2F-%2Ftar-linux-arm64-musl-1.1.0.tgz" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard "@napi-rs/tar-linux-ppc64-gnu@npm:1.1.0": version: 1.1.0 - resolution: "@napi-rs/tar-linux-ppc64-gnu@npm:1.1.0" + resolution: "@napi-rs/tar-linux-ppc64-gnu@npm:1.1.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Ftar-linux-ppc64-gnu%2F-%2Ftar-linux-ppc64-gnu-1.1.0.tgz" conditions: os=linux & cpu=ppc64 & libc=glibc languageName: node linkType: hard "@napi-rs/tar-linux-s390x-gnu@npm:1.1.0": version: 1.1.0 - resolution: "@napi-rs/tar-linux-s390x-gnu@npm:1.1.0" + resolution: "@napi-rs/tar-linux-s390x-gnu@npm:1.1.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Ftar-linux-s390x-gnu%2F-%2Ftar-linux-s390x-gnu-1.1.0.tgz" conditions: os=linux & cpu=s390x & libc=glibc languageName: node linkType: hard "@napi-rs/tar-linux-x64-gnu@npm:1.1.0": version: 1.1.0 - resolution: "@napi-rs/tar-linux-x64-gnu@npm:1.1.0" + resolution: "@napi-rs/tar-linux-x64-gnu@npm:1.1.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Ftar-linux-x64-gnu%2F-%2Ftar-linux-x64-gnu-1.1.0.tgz" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard "@napi-rs/tar-linux-x64-musl@npm:1.1.0": version: 1.1.0 - resolution: "@napi-rs/tar-linux-x64-musl@npm:1.1.0" + resolution: "@napi-rs/tar-linux-x64-musl@npm:1.1.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Ftar-linux-x64-musl%2F-%2Ftar-linux-x64-musl-1.1.0.tgz" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard "@napi-rs/tar-wasm32-wasi@npm:1.1.0": version: 1.1.0 - resolution: "@napi-rs/tar-wasm32-wasi@npm:1.1.0" + resolution: "@napi-rs/tar-wasm32-wasi@npm:1.1.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Ftar-wasm32-wasi%2F-%2Ftar-wasm32-wasi-1.1.0.tgz" dependencies: "@napi-rs/wasm-runtime": "npm:^1.0.3" conditions: cpu=wasm32 @@ -1381,28 +1563,28 @@ __metadata: "@napi-rs/tar-win32-arm64-msvc@npm:1.1.0": version: 1.1.0 - resolution: "@napi-rs/tar-win32-arm64-msvc@npm:1.1.0" + resolution: "@napi-rs/tar-win32-arm64-msvc@npm:1.1.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Ftar-win32-arm64-msvc%2F-%2Ftar-win32-arm64-msvc-1.1.0.tgz" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard "@napi-rs/tar-win32-ia32-msvc@npm:1.1.0": version: 1.1.0 - resolution: "@napi-rs/tar-win32-ia32-msvc@npm:1.1.0" + resolution: "@napi-rs/tar-win32-ia32-msvc@npm:1.1.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Ftar-win32-ia32-msvc%2F-%2Ftar-win32-ia32-msvc-1.1.0.tgz" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard "@napi-rs/tar-win32-x64-msvc@npm:1.1.0": version: 1.1.0 - resolution: "@napi-rs/tar-win32-x64-msvc@npm:1.1.0" + resolution: "@napi-rs/tar-win32-x64-msvc@npm:1.1.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Ftar-win32-x64-msvc%2F-%2Ftar-win32-x64-msvc-1.1.0.tgz" conditions: os=win32 & cpu=x64 languageName: node linkType: hard "@napi-rs/tar@npm:^1.1.0": version: 1.1.0 - resolution: "@napi-rs/tar@npm:1.1.0" + resolution: "@napi-rs/tar@npm:1.1.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Ftar%2F-%2Ftar-1.1.0.tgz" dependencies: "@napi-rs/tar-android-arm-eabi": "npm:1.1.0" "@napi-rs/tar-android-arm64": "npm:1.1.0" @@ -1457,9 +1639,9 @@ __metadata: languageName: node linkType: hard -"@napi-rs/wasm-runtime@npm:^1.0.3, @napi-rs/wasm-runtime@npm:^1.1.2, @napi-rs/wasm-runtime@npm:^1.1.5": +"@napi-rs/wasm-runtime@npm:^1.0.3, @napi-rs/wasm-runtime@npm:^1.1.2, @napi-rs/wasm-runtime@npm:^1.1.5, @napi-rs/wasm-runtime@npm:^1.1.6": version: 1.1.6 - resolution: "@napi-rs/wasm-runtime@npm:1.1.6" + resolution: "@napi-rs/wasm-runtime@npm:1.1.6::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fwasm-runtime%2F-%2Fwasm-runtime-1.1.6.tgz" dependencies: "@tybys/wasm-util": "npm:^0.10.3" peerDependencies: @@ -1471,70 +1653,70 @@ __metadata: "@napi-rs/wasm-tools-android-arm-eabi@npm:1.0.1": version: 1.0.1 - resolution: "@napi-rs/wasm-tools-android-arm-eabi@npm:1.0.1" + resolution: "@napi-rs/wasm-tools-android-arm-eabi@npm:1.0.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fwasm-tools-android-arm-eabi%2F-%2Fwasm-tools-android-arm-eabi-1.0.1.tgz" conditions: os=android & cpu=arm languageName: node linkType: hard "@napi-rs/wasm-tools-android-arm64@npm:1.0.1": version: 1.0.1 - resolution: "@napi-rs/wasm-tools-android-arm64@npm:1.0.1" + resolution: "@napi-rs/wasm-tools-android-arm64@npm:1.0.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fwasm-tools-android-arm64%2F-%2Fwasm-tools-android-arm64-1.0.1.tgz" conditions: os=android & cpu=arm64 languageName: node linkType: hard "@napi-rs/wasm-tools-darwin-arm64@npm:1.0.1": version: 1.0.1 - resolution: "@napi-rs/wasm-tools-darwin-arm64@npm:1.0.1" + resolution: "@napi-rs/wasm-tools-darwin-arm64@npm:1.0.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fwasm-tools-darwin-arm64%2F-%2Fwasm-tools-darwin-arm64-1.0.1.tgz" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard "@napi-rs/wasm-tools-darwin-x64@npm:1.0.1": version: 1.0.1 - resolution: "@napi-rs/wasm-tools-darwin-x64@npm:1.0.1" + resolution: "@napi-rs/wasm-tools-darwin-x64@npm:1.0.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fwasm-tools-darwin-x64%2F-%2Fwasm-tools-darwin-x64-1.0.1.tgz" conditions: os=darwin & cpu=x64 languageName: node linkType: hard "@napi-rs/wasm-tools-freebsd-x64@npm:1.0.1": version: 1.0.1 - resolution: "@napi-rs/wasm-tools-freebsd-x64@npm:1.0.1" + resolution: "@napi-rs/wasm-tools-freebsd-x64@npm:1.0.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fwasm-tools-freebsd-x64%2F-%2Fwasm-tools-freebsd-x64-1.0.1.tgz" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard "@napi-rs/wasm-tools-linux-arm64-gnu@npm:1.0.1": version: 1.0.1 - resolution: "@napi-rs/wasm-tools-linux-arm64-gnu@npm:1.0.1" + resolution: "@napi-rs/wasm-tools-linux-arm64-gnu@npm:1.0.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fwasm-tools-linux-arm64-gnu%2F-%2Fwasm-tools-linux-arm64-gnu-1.0.1.tgz" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard "@napi-rs/wasm-tools-linux-arm64-musl@npm:1.0.1": version: 1.0.1 - resolution: "@napi-rs/wasm-tools-linux-arm64-musl@npm:1.0.1" + resolution: "@napi-rs/wasm-tools-linux-arm64-musl@npm:1.0.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fwasm-tools-linux-arm64-musl%2F-%2Fwasm-tools-linux-arm64-musl-1.0.1.tgz" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard "@napi-rs/wasm-tools-linux-x64-gnu@npm:1.0.1": version: 1.0.1 - resolution: "@napi-rs/wasm-tools-linux-x64-gnu@npm:1.0.1" + resolution: "@napi-rs/wasm-tools-linux-x64-gnu@npm:1.0.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fwasm-tools-linux-x64-gnu%2F-%2Fwasm-tools-linux-x64-gnu-1.0.1.tgz" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard "@napi-rs/wasm-tools-linux-x64-musl@npm:1.0.1": version: 1.0.1 - resolution: "@napi-rs/wasm-tools-linux-x64-musl@npm:1.0.1" + resolution: "@napi-rs/wasm-tools-linux-x64-musl@npm:1.0.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fwasm-tools-linux-x64-musl%2F-%2Fwasm-tools-linux-x64-musl-1.0.1.tgz" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard "@napi-rs/wasm-tools-wasm32-wasi@npm:1.0.1": version: 1.0.1 - resolution: "@napi-rs/wasm-tools-wasm32-wasi@npm:1.0.1" + resolution: "@napi-rs/wasm-tools-wasm32-wasi@npm:1.0.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fwasm-tools-wasm32-wasi%2F-%2Fwasm-tools-wasm32-wasi-1.0.1.tgz" dependencies: "@napi-rs/wasm-runtime": "npm:^1.0.3" conditions: cpu=wasm32 @@ -1543,28 +1725,28 @@ __metadata: "@napi-rs/wasm-tools-win32-arm64-msvc@npm:1.0.1": version: 1.0.1 - resolution: "@napi-rs/wasm-tools-win32-arm64-msvc@npm:1.0.1" + resolution: "@napi-rs/wasm-tools-win32-arm64-msvc@npm:1.0.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fwasm-tools-win32-arm64-msvc%2F-%2Fwasm-tools-win32-arm64-msvc-1.0.1.tgz" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard "@napi-rs/wasm-tools-win32-ia32-msvc@npm:1.0.1": version: 1.0.1 - resolution: "@napi-rs/wasm-tools-win32-ia32-msvc@npm:1.0.1" + resolution: "@napi-rs/wasm-tools-win32-ia32-msvc@npm:1.0.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fwasm-tools-win32-ia32-msvc%2F-%2Fwasm-tools-win32-ia32-msvc-1.0.1.tgz" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard "@napi-rs/wasm-tools-win32-x64-msvc@npm:1.0.1": version: 1.0.1 - resolution: "@napi-rs/wasm-tools-win32-x64-msvc@npm:1.0.1" + resolution: "@napi-rs/wasm-tools-win32-x64-msvc@npm:1.0.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fwasm-tools-win32-x64-msvc%2F-%2Fwasm-tools-win32-x64-msvc-1.0.1.tgz" conditions: os=win32 & cpu=x64 languageName: node linkType: hard "@napi-rs/wasm-tools@npm:^1.0.1": version: 1.0.1 - resolution: "@napi-rs/wasm-tools@npm:1.0.1" + resolution: "@napi-rs/wasm-tools@npm:1.0.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40napi-rs%2Fwasm-tools%2F-%2Fwasm-tools-1.0.1.tgz" dependencies: "@napi-rs/wasm-tools-android-arm-eabi": "npm:1.0.1" "@napi-rs/wasm-tools-android-arm64": "npm:1.0.1" @@ -1612,7 +1794,7 @@ __metadata: "@nodelib/fs.scandir@npm:2.1.5": version: 2.1.5 - resolution: "@nodelib/fs.scandir@npm:2.1.5" + resolution: "@nodelib/fs.scandir@npm:2.1.5::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40nodelib%2Ffs.scandir%2F-%2Ffs.scandir-2.1.5.tgz" dependencies: "@nodelib/fs.stat": "npm:2.0.5" run-parallel: "npm:^1.1.9" @@ -1622,14 +1804,14 @@ __metadata: "@nodelib/fs.stat@npm:2.0.5, @nodelib/fs.stat@npm:^2.0.2": version: 2.0.5 - resolution: "@nodelib/fs.stat@npm:2.0.5" + resolution: "@nodelib/fs.stat@npm:2.0.5::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40nodelib%2Ffs.stat%2F-%2Ffs.stat-2.0.5.tgz" checksum: 10c0/88dafe5e3e29a388b07264680dc996c17f4bda48d163a9d4f5c1112979f0ce8ec72aa7116122c350b4e7976bc5566dc3ddb579be1ceaacc727872eb4ed93926d languageName: node linkType: hard "@nodelib/fs.walk@npm:^1.2.3": version: 1.2.8 - resolution: "@nodelib/fs.walk@npm:1.2.8" + resolution: "@nodelib/fs.walk@npm:1.2.8::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40nodelib%2Ffs.walk%2F-%2Ffs.walk-1.2.8.tgz" dependencies: "@nodelib/fs.scandir": "npm:2.1.5" fastq: "npm:^1.6.0" @@ -1639,7 +1821,7 @@ __metadata: "@octokit/app@npm:^16.1.2": version: 16.1.2 - resolution: "@octokit/app@npm:16.1.2" + resolution: "@octokit/app@npm:16.1.2::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40octokit%2Fapp%2F-%2Fapp-16.1.2.tgz" dependencies: "@octokit/auth-app": "npm:^8.1.2" "@octokit/auth-unauthenticated": "npm:^7.0.3" @@ -1654,7 +1836,7 @@ __metadata: "@octokit/auth-app@npm:^8.1.2": version: 8.2.0 - resolution: "@octokit/auth-app@npm:8.2.0" + resolution: "@octokit/auth-app@npm:8.2.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40octokit%2Fauth-app%2F-%2Fauth-app-8.2.0.tgz" dependencies: "@octokit/auth-oauth-app": "npm:^9.0.3" "@octokit/auth-oauth-user": "npm:^6.0.2" @@ -1670,7 +1852,7 @@ __metadata: "@octokit/auth-oauth-app@npm:^9.0.2, @octokit/auth-oauth-app@npm:^9.0.3": version: 9.0.3 - resolution: "@octokit/auth-oauth-app@npm:9.0.3" + resolution: "@octokit/auth-oauth-app@npm:9.0.3::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40octokit%2Fauth-oauth-app%2F-%2Fauth-oauth-app-9.0.3.tgz" dependencies: "@octokit/auth-oauth-device": "npm:^8.0.3" "@octokit/auth-oauth-user": "npm:^6.0.2" @@ -1683,7 +1865,7 @@ __metadata: "@octokit/auth-oauth-device@npm:^8.0.3": version: 8.0.3 - resolution: "@octokit/auth-oauth-device@npm:8.0.3" + resolution: "@octokit/auth-oauth-device@npm:8.0.3::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40octokit%2Fauth-oauth-device%2F-%2Fauth-oauth-device-8.0.3.tgz" dependencies: "@octokit/oauth-methods": "npm:^6.0.2" "@octokit/request": "npm:^10.0.6" @@ -1695,7 +1877,7 @@ __metadata: "@octokit/auth-oauth-user@npm:^6.0.1, @octokit/auth-oauth-user@npm:^6.0.2": version: 6.0.2 - resolution: "@octokit/auth-oauth-user@npm:6.0.2" + resolution: "@octokit/auth-oauth-user@npm:6.0.2::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40octokit%2Fauth-oauth-user%2F-%2Fauth-oauth-user-6.0.2.tgz" dependencies: "@octokit/auth-oauth-device": "npm:^8.0.3" "@octokit/oauth-methods": "npm:^6.0.2" @@ -1708,14 +1890,14 @@ __metadata: "@octokit/auth-token@npm:^6.0.0": version: 6.0.0 - resolution: "@octokit/auth-token@npm:6.0.0" + resolution: "@octokit/auth-token@npm:6.0.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40octokit%2Fauth-token%2F-%2Fauth-token-6.0.0.tgz" checksum: 10c0/32ecc904c5f6f4e5d090bfcc679d70318690c0a0b5040cd9a25811ad9dcd44c33f2cf96b6dbee1cd56cf58fde28fb1819c01b58718aa5c971f79c822357cb5c0 languageName: node linkType: hard "@octokit/auth-unauthenticated@npm:^7.0.2, @octokit/auth-unauthenticated@npm:^7.0.3": version: 7.0.3 - resolution: "@octokit/auth-unauthenticated@npm:7.0.3" + resolution: "@octokit/auth-unauthenticated@npm:7.0.3::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40octokit%2Fauth-unauthenticated%2F-%2Fauth-unauthenticated-7.0.3.tgz" dependencies: "@octokit/request-error": "npm:^7.0.2" "@octokit/types": "npm:^16.0.0" @@ -1725,7 +1907,7 @@ __metadata: "@octokit/core@npm:^7.0.5, @octokit/core@npm:^7.0.6": version: 7.0.6 - resolution: "@octokit/core@npm:7.0.6" + resolution: "@octokit/core@npm:7.0.6::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40octokit%2Fcore%2F-%2Fcore-7.0.6.tgz" dependencies: "@octokit/auth-token": "npm:^6.0.0" "@octokit/graphql": "npm:^9.0.3" @@ -1740,7 +1922,7 @@ __metadata: "@octokit/endpoint@npm:^11.0.3": version: 11.0.3 - resolution: "@octokit/endpoint@npm:11.0.3" + resolution: "@octokit/endpoint@npm:11.0.3::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40octokit%2Fendpoint%2F-%2Fendpoint-11.0.3.tgz" dependencies: "@octokit/types": "npm:^16.0.0" universal-user-agent: "npm:^7.0.2" @@ -1750,7 +1932,7 @@ __metadata: "@octokit/graphql@npm:^9.0.3": version: 9.0.3 - resolution: "@octokit/graphql@npm:9.0.3" + resolution: "@octokit/graphql@npm:9.0.3::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40octokit%2Fgraphql%2F-%2Fgraphql-9.0.3.tgz" dependencies: "@octokit/request": "npm:^10.0.6" "@octokit/types": "npm:^16.0.0" @@ -1761,7 +1943,7 @@ __metadata: "@octokit/oauth-app@npm:^8.0.3": version: 8.0.3 - resolution: "@octokit/oauth-app@npm:8.0.3" + resolution: "@octokit/oauth-app@npm:8.0.3::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40octokit%2Foauth-app%2F-%2Foauth-app-8.0.3.tgz" dependencies: "@octokit/auth-oauth-app": "npm:^9.0.2" "@octokit/auth-oauth-user": "npm:^6.0.1" @@ -1777,14 +1959,14 @@ __metadata: "@octokit/oauth-authorization-url@npm:^8.0.0": version: 8.0.0 - resolution: "@octokit/oauth-authorization-url@npm:8.0.0" + resolution: "@octokit/oauth-authorization-url@npm:8.0.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40octokit%2Foauth-authorization-url%2F-%2Foauth-authorization-url-8.0.0.tgz" checksum: 10c0/ab4964bebd8d076f945a2f3210a8a0a221a408362569d9fc2f49875ad06e594365f5fd871dac08d820793f687bff50237f7acf40d9d39c5f9de7575b6f4bad93 languageName: node linkType: hard "@octokit/oauth-methods@npm:^6.0.1, @octokit/oauth-methods@npm:^6.0.2": version: 6.0.2 - resolution: "@octokit/oauth-methods@npm:6.0.2" + resolution: "@octokit/oauth-methods@npm:6.0.2::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40octokit%2Foauth-methods%2F-%2Foauth-methods-6.0.2.tgz" dependencies: "@octokit/oauth-authorization-url": "npm:^8.0.0" "@octokit/request": "npm:^10.0.6" @@ -1796,21 +1978,21 @@ __metadata: "@octokit/openapi-types@npm:^27.0.0": version: 27.0.0 - resolution: "@octokit/openapi-types@npm:27.0.0" + resolution: "@octokit/openapi-types@npm:27.0.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40octokit%2Fopenapi-types%2F-%2Fopenapi-types-27.0.0.tgz" checksum: 10c0/602d1de033da180a2e982cdbd3646bd5b2e16ecf36b9955a0f23e37ae9e6cb086abb48ff2ae6f2de000fce03e8ae9051794611ae4a95a8f5f6fb63276e7b8e31 languageName: node linkType: hard "@octokit/openapi-webhooks-types@npm:12.1.0": version: 12.1.0 - resolution: "@octokit/openapi-webhooks-types@npm:12.1.0" + resolution: "@octokit/openapi-webhooks-types@npm:12.1.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40octokit%2Fopenapi-webhooks-types%2F-%2Fopenapi-webhooks-types-12.1.0.tgz" checksum: 10c0/0352ee895f849d16e8a33e8474fc87d7c0c117fcd72d3e03271232d82ed9f648ca252737e6453f3cd5919c9a2b92fb048327d306d761d4d750c2eee8eca3669f languageName: node linkType: hard "@octokit/plugin-paginate-graphql@npm:^6.0.0": version: 6.0.0 - resolution: "@octokit/plugin-paginate-graphql@npm:6.0.0" + resolution: "@octokit/plugin-paginate-graphql@npm:6.0.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40octokit%2Fplugin-paginate-graphql%2F-%2Fplugin-paginate-graphql-6.0.0.tgz" peerDependencies: "@octokit/core": ">=6" checksum: 10c0/354553741317a8a5977dffdf26ddeb767f3c67266a18a5d5e3e5c31fbc118f157ed726f43f998c1aad4bfa704312ce11a4f3e7b681e863245eac8afcac2c5861 @@ -1819,7 +2001,7 @@ __metadata: "@octokit/plugin-paginate-rest@npm:^14.0.0": version: 14.0.0 - resolution: "@octokit/plugin-paginate-rest@npm:14.0.0" + resolution: "@octokit/plugin-paginate-rest@npm:14.0.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40octokit%2Fplugin-paginate-rest%2F-%2Fplugin-paginate-rest-14.0.0.tgz" dependencies: "@octokit/types": "npm:^16.0.0" peerDependencies: @@ -1830,7 +2012,7 @@ __metadata: "@octokit/plugin-request-log@npm:^6.0.0": version: 6.0.0 - resolution: "@octokit/plugin-request-log@npm:6.0.0" + resolution: "@octokit/plugin-request-log@npm:6.0.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40octokit%2Fplugin-request-log%2F-%2Fplugin-request-log-6.0.0.tgz" peerDependencies: "@octokit/core": ">=6" checksum: 10c0/40e46ad0c77235742d0bf698ab4e17df1ae06e0d7824ffc5867ed71e27de860875adb73d89629b823fe8647459a8f262c26ed1aa6ee374873fa94095f37df0bb @@ -1839,7 +2021,7 @@ __metadata: "@octokit/plugin-rest-endpoint-methods@npm:^17.0.0": version: 17.0.0 - resolution: "@octokit/plugin-rest-endpoint-methods@npm:17.0.0" + resolution: "@octokit/plugin-rest-endpoint-methods@npm:17.0.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40octokit%2Fplugin-rest-endpoint-methods%2F-%2Fplugin-rest-endpoint-methods-17.0.0.tgz" dependencies: "@octokit/types": "npm:^16.0.0" peerDependencies: @@ -1850,7 +2032,7 @@ __metadata: "@octokit/plugin-retry@npm:^8.0.3": version: 8.1.0 - resolution: "@octokit/plugin-retry@npm:8.1.0" + resolution: "@octokit/plugin-retry@npm:8.1.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40octokit%2Fplugin-retry%2F-%2Fplugin-retry-8.1.0.tgz" dependencies: "@octokit/request-error": "npm:^7.0.2" "@octokit/types": "npm:^16.0.0" @@ -1863,7 +2045,7 @@ __metadata: "@octokit/plugin-throttling@npm:^11.0.3": version: 11.0.3 - resolution: "@octokit/plugin-throttling@npm:11.0.3" + resolution: "@octokit/plugin-throttling@npm:11.0.3::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40octokit%2Fplugin-throttling%2F-%2Fplugin-throttling-11.0.3.tgz" dependencies: "@octokit/types": "npm:^16.0.0" bottleneck: "npm:^2.15.3" @@ -1875,7 +2057,7 @@ __metadata: "@octokit/request-error@npm:^7.0.0, @octokit/request-error@npm:^7.0.2": version: 7.1.0 - resolution: "@octokit/request-error@npm:7.1.0" + resolution: "@octokit/request-error@npm:7.1.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40octokit%2Frequest-error%2F-%2Frequest-error-7.1.0.tgz" dependencies: "@octokit/types": "npm:^16.0.0" checksum: 10c0/62b90a54545c36a30b5ffdda42e302c751be184d85b68ffc7f1242c51d7ca54dbd185b7d0027b491991776923a910c85c9c51269fe0d86111bac187507a5abc4 @@ -1883,8 +2065,8 @@ __metadata: linkType: hard "@octokit/request@npm:^10.0.6": - version: 10.0.10 - resolution: "@octokit/request@npm:10.0.10" + version: 10.0.11 + resolution: "@octokit/request@npm:10.0.11::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40octokit%2Frequest%2F-%2Frequest-10.0.11.tgz" dependencies: "@octokit/endpoint": "npm:^11.0.3" "@octokit/request-error": "npm:^7.0.2" @@ -1892,13 +2074,13 @@ __metadata: content-type: "npm:^2.0.0" json-with-bigint: "npm:^3.5.3" universal-user-agent: "npm:^7.0.2" - checksum: 10c0/1d93f508dbc903282d92558168736c6d1a8c04c511e6d7ebe67594c4cfe93f711d67b5628e9f4e8f194c7ef124e066196308e638e93ad33c73ceb0f4b4398eae + checksum: 10c0/ee9782d77faa8e1d8ac2505ff7b18b29e7f9eac6b4a844a5806b478ab2298e8add05d9707079ffb83b8fcfe48b39a9b4af3d6e5188ff77e854b55465ead9db97 languageName: node linkType: hard "@octokit/rest@npm:^22.0.1": version: 22.0.1 - resolution: "@octokit/rest@npm:22.0.1" + resolution: "@octokit/rest@npm:22.0.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40octokit%2Frest%2F-%2Frest-22.0.1.tgz" dependencies: "@octokit/core": "npm:^7.0.6" "@octokit/plugin-paginate-rest": "npm:^14.0.0" @@ -1910,7 +2092,7 @@ __metadata: "@octokit/types@npm:^16.0.0": version: 16.0.0 - resolution: "@octokit/types@npm:16.0.0" + resolution: "@octokit/types@npm:16.0.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40octokit%2Ftypes%2F-%2Ftypes-16.0.0.tgz" dependencies: "@octokit/openapi-types": "npm:^27.0.0" checksum: 10c0/b8d41098ba6fc194d13d641f9441347e3a3b96c0efabac0e14f57319340a2d4d1c8676e4cb37ab3062c5c323c617e790b0126916e9bf7b201b0cced0826f8ae2 @@ -1919,14 +2101,14 @@ __metadata: "@octokit/webhooks-methods@npm:^6.0.0": version: 6.0.0 - resolution: "@octokit/webhooks-methods@npm:6.0.0" + resolution: "@octokit/webhooks-methods@npm:6.0.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40octokit%2Fwebhooks-methods%2F-%2Fwebhooks-methods-6.0.0.tgz" checksum: 10c0/7f10740e838d65c78e859bb041499cca69df7831e9f633ee70a46ca8e53d0844f2c84500df204453d171c8c3c0f8eb8b68716ee1d5c95e3cf5d09690f32e13e1 languageName: node linkType: hard "@octokit/webhooks@npm:^14.0.0": version: 14.2.0 - resolution: "@octokit/webhooks@npm:14.2.0" + resolution: "@octokit/webhooks@npm:14.2.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40octokit%2Fwebhooks%2F-%2Fwebhooks-14.2.0.tgz" dependencies: "@octokit/openapi-webhooks-types": "npm:12.1.0" "@octokit/request-error": "npm:^7.0.0" @@ -1936,25 +2118,25 @@ __metadata: linkType: hard "@openrouter/ai-sdk-provider@npm:^2.9.1": - version: 2.9.1 - resolution: "@openrouter/ai-sdk-provider@npm:2.9.1" + version: 2.10.0 + resolution: "@openrouter/ai-sdk-provider@npm:2.10.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40openrouter%2Fai-sdk-provider%2F-%2Fai-sdk-provider-2.10.0.tgz" peerDependencies: ai: ^6.0.0 zod: ^3.25.0 || ^4.0.0 - checksum: 10c0/0d3aa754bce0af1ac6ca4d126acaf9f37ba0f952a2b40f6c0bae280474e7880a8ceba469b04874713ace311f413b8236833ef9b5ce673fa4f2f49de06f6fc483 + checksum: 10c0/22b948f8e82c0affcc4cba0a9b996a09cbc424d17c9f30a9803fb7c3b42c832441e4188d147beed98910cec1bb7200a8c5483fbc89702b0bed8f3d8d975f3108 languageName: node linkType: hard "@opentelemetry/api@npm:^1.9.0": version: 1.9.1 - resolution: "@opentelemetry/api@npm:1.9.1" + resolution: "@opentelemetry/api@npm:1.9.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40opentelemetry%2Fapi%2F-%2Fapi-1.9.1.tgz" checksum: 10c0/c608485fc8b5a91e1f7e05e843b45b509307456b31cd2ad365933d90813e40ebfedf179f1451c762037e82d7c76aa8500e95d2da3609f640a1206cde5322cd14 languageName: node linkType: hard "@oxc-node/cli@npm:^0.1.0": version: 0.1.0 - resolution: "@oxc-node/cli@npm:0.1.0" + resolution: "@oxc-node/cli@npm:0.1.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxc-node%2Fcli%2F-%2Fcli-0.1.0.tgz" dependencies: "@oxc-node/core": "npm:0.1.0" bin: @@ -1965,98 +2147,98 @@ __metadata: "@oxc-node/core-android-arm-eabi@npm:0.1.0": version: 0.1.0 - resolution: "@oxc-node/core-android-arm-eabi@npm:0.1.0" + resolution: "@oxc-node/core-android-arm-eabi@npm:0.1.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxc-node%2Fcore-android-arm-eabi%2F-%2Fcore-android-arm-eabi-0.1.0.tgz" conditions: os=android & cpu=arm languageName: node linkType: hard "@oxc-node/core-android-arm64@npm:0.1.0": version: 0.1.0 - resolution: "@oxc-node/core-android-arm64@npm:0.1.0" + resolution: "@oxc-node/core-android-arm64@npm:0.1.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxc-node%2Fcore-android-arm64%2F-%2Fcore-android-arm64-0.1.0.tgz" conditions: os=android & cpu=arm64 languageName: node linkType: hard "@oxc-node/core-darwin-arm64@npm:0.1.0": version: 0.1.0 - resolution: "@oxc-node/core-darwin-arm64@npm:0.1.0" + resolution: "@oxc-node/core-darwin-arm64@npm:0.1.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxc-node%2Fcore-darwin-arm64%2F-%2Fcore-darwin-arm64-0.1.0.tgz" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard "@oxc-node/core-darwin-x64@npm:0.1.0": version: 0.1.0 - resolution: "@oxc-node/core-darwin-x64@npm:0.1.0" + resolution: "@oxc-node/core-darwin-x64@npm:0.1.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxc-node%2Fcore-darwin-x64%2F-%2Fcore-darwin-x64-0.1.0.tgz" conditions: os=darwin & cpu=x64 languageName: node linkType: hard "@oxc-node/core-freebsd-x64@npm:0.1.0": version: 0.1.0 - resolution: "@oxc-node/core-freebsd-x64@npm:0.1.0" + resolution: "@oxc-node/core-freebsd-x64@npm:0.1.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxc-node%2Fcore-freebsd-x64%2F-%2Fcore-freebsd-x64-0.1.0.tgz" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard "@oxc-node/core-linux-arm-gnueabihf@npm:0.1.0": version: 0.1.0 - resolution: "@oxc-node/core-linux-arm-gnueabihf@npm:0.1.0" + resolution: "@oxc-node/core-linux-arm-gnueabihf@npm:0.1.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxc-node%2Fcore-linux-arm-gnueabihf%2F-%2Fcore-linux-arm-gnueabihf-0.1.0.tgz" conditions: os=linux & cpu=arm languageName: node linkType: hard "@oxc-node/core-linux-arm64-gnu@npm:0.1.0": version: 0.1.0 - resolution: "@oxc-node/core-linux-arm64-gnu@npm:0.1.0" + resolution: "@oxc-node/core-linux-arm64-gnu@npm:0.1.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxc-node%2Fcore-linux-arm64-gnu%2F-%2Fcore-linux-arm64-gnu-0.1.0.tgz" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard "@oxc-node/core-linux-arm64-musl@npm:0.1.0": version: 0.1.0 - resolution: "@oxc-node/core-linux-arm64-musl@npm:0.1.0" + resolution: "@oxc-node/core-linux-arm64-musl@npm:0.1.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxc-node%2Fcore-linux-arm64-musl%2F-%2Fcore-linux-arm64-musl-0.1.0.tgz" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard "@oxc-node/core-linux-ppc64-gnu@npm:0.1.0": version: 0.1.0 - resolution: "@oxc-node/core-linux-ppc64-gnu@npm:0.1.0" + resolution: "@oxc-node/core-linux-ppc64-gnu@npm:0.1.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxc-node%2Fcore-linux-ppc64-gnu%2F-%2Fcore-linux-ppc64-gnu-0.1.0.tgz" conditions: os=linux & cpu=ppc64 & libc=glibc languageName: node linkType: hard "@oxc-node/core-linux-s390x-gnu@npm:0.1.0": version: 0.1.0 - resolution: "@oxc-node/core-linux-s390x-gnu@npm:0.1.0" + resolution: "@oxc-node/core-linux-s390x-gnu@npm:0.1.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxc-node%2Fcore-linux-s390x-gnu%2F-%2Fcore-linux-s390x-gnu-0.1.0.tgz" conditions: os=linux & cpu=s390x & libc=glibc languageName: node linkType: hard "@oxc-node/core-linux-x64-gnu@npm:0.1.0": version: 0.1.0 - resolution: "@oxc-node/core-linux-x64-gnu@npm:0.1.0" + resolution: "@oxc-node/core-linux-x64-gnu@npm:0.1.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxc-node%2Fcore-linux-x64-gnu%2F-%2Fcore-linux-x64-gnu-0.1.0.tgz" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard "@oxc-node/core-linux-x64-musl@npm:0.1.0": version: 0.1.0 - resolution: "@oxc-node/core-linux-x64-musl@npm:0.1.0" + resolution: "@oxc-node/core-linux-x64-musl@npm:0.1.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxc-node%2Fcore-linux-x64-musl%2F-%2Fcore-linux-x64-musl-0.1.0.tgz" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard "@oxc-node/core-openharmony-arm64@npm:0.1.0": version: 0.1.0 - resolution: "@oxc-node/core-openharmony-arm64@npm:0.1.0" + resolution: "@oxc-node/core-openharmony-arm64@npm:0.1.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxc-node%2Fcore-openharmony-arm64%2F-%2Fcore-openharmony-arm64-0.1.0.tgz" conditions: os=openharmony & cpu=arm64 languageName: node linkType: hard "@oxc-node/core-wasm32-wasi@npm:0.1.0": version: 0.1.0 - resolution: "@oxc-node/core-wasm32-wasi@npm:0.1.0" + resolution: "@oxc-node/core-wasm32-wasi@npm:0.1.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxc-node%2Fcore-wasm32-wasi%2F-%2Fcore-wasm32-wasi-0.1.0.tgz" dependencies: "@emnapi/core": "npm:1.9.1" "@emnapi/runtime": "npm:1.9.1" @@ -2067,28 +2249,28 @@ __metadata: "@oxc-node/core-win32-arm64-msvc@npm:0.1.0": version: 0.1.0 - resolution: "@oxc-node/core-win32-arm64-msvc@npm:0.1.0" + resolution: "@oxc-node/core-win32-arm64-msvc@npm:0.1.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxc-node%2Fcore-win32-arm64-msvc%2F-%2Fcore-win32-arm64-msvc-0.1.0.tgz" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard "@oxc-node/core-win32-ia32-msvc@npm:0.1.0": version: 0.1.0 - resolution: "@oxc-node/core-win32-ia32-msvc@npm:0.1.0" + resolution: "@oxc-node/core-win32-ia32-msvc@npm:0.1.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxc-node%2Fcore-win32-ia32-msvc%2F-%2Fcore-win32-ia32-msvc-0.1.0.tgz" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard "@oxc-node/core-win32-x64-msvc@npm:0.1.0": version: 0.1.0 - resolution: "@oxc-node/core-win32-x64-msvc@npm:0.1.0" + resolution: "@oxc-node/core-win32-x64-msvc@npm:0.1.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxc-node%2Fcore-win32-x64-msvc%2F-%2Fcore-win32-x64-msvc-0.1.0.tgz" conditions: os=win32 & cpu=x64 languageName: node linkType: hard "@oxc-node/core@npm:0.1.0, @oxc-node/core@npm:^0.1.0": version: 0.1.0 - resolution: "@oxc-node/core@npm:0.1.0" + resolution: "@oxc-node/core@npm:0.1.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxc-node%2Fcore%2F-%2Fcore-0.1.0.tgz" dependencies: "@oxc-node/core-android-arm-eabi": "npm:0.1.0" "@oxc-node/core-android-arm64": "npm:0.1.0" @@ -2149,119 +2331,119 @@ __metadata: "@oxc-parser/binding-android-arm-eabi@npm:0.137.0": version: 0.137.0 - resolution: "@oxc-parser/binding-android-arm-eabi@npm:0.137.0" + resolution: "@oxc-parser/binding-android-arm-eabi@npm:0.137.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxc-parser%2Fbinding-android-arm-eabi%2F-%2Fbinding-android-arm-eabi-0.137.0.tgz" conditions: os=android & cpu=arm languageName: node linkType: hard "@oxc-parser/binding-android-arm64@npm:0.137.0": version: 0.137.0 - resolution: "@oxc-parser/binding-android-arm64@npm:0.137.0" + resolution: "@oxc-parser/binding-android-arm64@npm:0.137.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxc-parser%2Fbinding-android-arm64%2F-%2Fbinding-android-arm64-0.137.0.tgz" conditions: os=android & cpu=arm64 languageName: node linkType: hard "@oxc-parser/binding-darwin-arm64@npm:0.137.0": version: 0.137.0 - resolution: "@oxc-parser/binding-darwin-arm64@npm:0.137.0" + resolution: "@oxc-parser/binding-darwin-arm64@npm:0.137.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxc-parser%2Fbinding-darwin-arm64%2F-%2Fbinding-darwin-arm64-0.137.0.tgz" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard "@oxc-parser/binding-darwin-x64@npm:0.137.0": version: 0.137.0 - resolution: "@oxc-parser/binding-darwin-x64@npm:0.137.0" + resolution: "@oxc-parser/binding-darwin-x64@npm:0.137.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxc-parser%2Fbinding-darwin-x64%2F-%2Fbinding-darwin-x64-0.137.0.tgz" conditions: os=darwin & cpu=x64 languageName: node linkType: hard "@oxc-parser/binding-freebsd-x64@npm:0.137.0": version: 0.137.0 - resolution: "@oxc-parser/binding-freebsd-x64@npm:0.137.0" + resolution: "@oxc-parser/binding-freebsd-x64@npm:0.137.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxc-parser%2Fbinding-freebsd-x64%2F-%2Fbinding-freebsd-x64-0.137.0.tgz" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard "@oxc-parser/binding-linux-arm-gnueabihf@npm:0.137.0": version: 0.137.0 - resolution: "@oxc-parser/binding-linux-arm-gnueabihf@npm:0.137.0" + resolution: "@oxc-parser/binding-linux-arm-gnueabihf@npm:0.137.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxc-parser%2Fbinding-linux-arm-gnueabihf%2F-%2Fbinding-linux-arm-gnueabihf-0.137.0.tgz" conditions: os=linux & cpu=arm languageName: node linkType: hard "@oxc-parser/binding-linux-arm-musleabihf@npm:0.137.0": version: 0.137.0 - resolution: "@oxc-parser/binding-linux-arm-musleabihf@npm:0.137.0" + resolution: "@oxc-parser/binding-linux-arm-musleabihf@npm:0.137.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxc-parser%2Fbinding-linux-arm-musleabihf%2F-%2Fbinding-linux-arm-musleabihf-0.137.0.tgz" conditions: os=linux & cpu=arm languageName: node linkType: hard "@oxc-parser/binding-linux-arm64-gnu@npm:0.137.0": version: 0.137.0 - resolution: "@oxc-parser/binding-linux-arm64-gnu@npm:0.137.0" + resolution: "@oxc-parser/binding-linux-arm64-gnu@npm:0.137.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxc-parser%2Fbinding-linux-arm64-gnu%2F-%2Fbinding-linux-arm64-gnu-0.137.0.tgz" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard "@oxc-parser/binding-linux-arm64-musl@npm:0.137.0": version: 0.137.0 - resolution: "@oxc-parser/binding-linux-arm64-musl@npm:0.137.0" + resolution: "@oxc-parser/binding-linux-arm64-musl@npm:0.137.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxc-parser%2Fbinding-linux-arm64-musl%2F-%2Fbinding-linux-arm64-musl-0.137.0.tgz" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard "@oxc-parser/binding-linux-ppc64-gnu@npm:0.137.0": version: 0.137.0 - resolution: "@oxc-parser/binding-linux-ppc64-gnu@npm:0.137.0" + resolution: "@oxc-parser/binding-linux-ppc64-gnu@npm:0.137.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxc-parser%2Fbinding-linux-ppc64-gnu%2F-%2Fbinding-linux-ppc64-gnu-0.137.0.tgz" conditions: os=linux & cpu=ppc64 & libc=glibc languageName: node linkType: hard "@oxc-parser/binding-linux-riscv64-gnu@npm:0.137.0": version: 0.137.0 - resolution: "@oxc-parser/binding-linux-riscv64-gnu@npm:0.137.0" + resolution: "@oxc-parser/binding-linux-riscv64-gnu@npm:0.137.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxc-parser%2Fbinding-linux-riscv64-gnu%2F-%2Fbinding-linux-riscv64-gnu-0.137.0.tgz" conditions: os=linux & cpu=riscv64 & libc=glibc languageName: node linkType: hard "@oxc-parser/binding-linux-riscv64-musl@npm:0.137.0": version: 0.137.0 - resolution: "@oxc-parser/binding-linux-riscv64-musl@npm:0.137.0" + resolution: "@oxc-parser/binding-linux-riscv64-musl@npm:0.137.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxc-parser%2Fbinding-linux-riscv64-musl%2F-%2Fbinding-linux-riscv64-musl-0.137.0.tgz" conditions: os=linux & cpu=riscv64 & libc=musl languageName: node linkType: hard "@oxc-parser/binding-linux-s390x-gnu@npm:0.137.0": version: 0.137.0 - resolution: "@oxc-parser/binding-linux-s390x-gnu@npm:0.137.0" + resolution: "@oxc-parser/binding-linux-s390x-gnu@npm:0.137.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxc-parser%2Fbinding-linux-s390x-gnu%2F-%2Fbinding-linux-s390x-gnu-0.137.0.tgz" conditions: os=linux & cpu=s390x & libc=glibc languageName: node linkType: hard "@oxc-parser/binding-linux-x64-gnu@npm:0.137.0": version: 0.137.0 - resolution: "@oxc-parser/binding-linux-x64-gnu@npm:0.137.0" + resolution: "@oxc-parser/binding-linux-x64-gnu@npm:0.137.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxc-parser%2Fbinding-linux-x64-gnu%2F-%2Fbinding-linux-x64-gnu-0.137.0.tgz" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard "@oxc-parser/binding-linux-x64-musl@npm:0.137.0": version: 0.137.0 - resolution: "@oxc-parser/binding-linux-x64-musl@npm:0.137.0" + resolution: "@oxc-parser/binding-linux-x64-musl@npm:0.137.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxc-parser%2Fbinding-linux-x64-musl%2F-%2Fbinding-linux-x64-musl-0.137.0.tgz" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard "@oxc-parser/binding-openharmony-arm64@npm:0.137.0": version: 0.137.0 - resolution: "@oxc-parser/binding-openharmony-arm64@npm:0.137.0" + resolution: "@oxc-parser/binding-openharmony-arm64@npm:0.137.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxc-parser%2Fbinding-openharmony-arm64%2F-%2Fbinding-openharmony-arm64-0.137.0.tgz" conditions: os=openharmony & cpu=arm64 languageName: node linkType: hard "@oxc-parser/binding-wasm32-wasi@npm:0.137.0": version: 0.137.0 - resolution: "@oxc-parser/binding-wasm32-wasi@npm:0.137.0" + resolution: "@oxc-parser/binding-wasm32-wasi@npm:0.137.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxc-parser%2Fbinding-wasm32-wasi%2F-%2Fbinding-wasm32-wasi-0.137.0.tgz" dependencies: "@emnapi/core": "npm:1.11.1" "@emnapi/runtime": "npm:1.11.1" @@ -2272,378 +2454,378 @@ __metadata: "@oxc-parser/binding-win32-arm64-msvc@npm:0.137.0": version: 0.137.0 - resolution: "@oxc-parser/binding-win32-arm64-msvc@npm:0.137.0" + resolution: "@oxc-parser/binding-win32-arm64-msvc@npm:0.137.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxc-parser%2Fbinding-win32-arm64-msvc%2F-%2Fbinding-win32-arm64-msvc-0.137.0.tgz" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard "@oxc-parser/binding-win32-ia32-msvc@npm:0.137.0": version: 0.137.0 - resolution: "@oxc-parser/binding-win32-ia32-msvc@npm:0.137.0" + resolution: "@oxc-parser/binding-win32-ia32-msvc@npm:0.137.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxc-parser%2Fbinding-win32-ia32-msvc%2F-%2Fbinding-win32-ia32-msvc-0.137.0.tgz" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard "@oxc-parser/binding-win32-x64-msvc@npm:0.137.0": version: 0.137.0 - resolution: "@oxc-parser/binding-win32-x64-msvc@npm:0.137.0" + resolution: "@oxc-parser/binding-win32-x64-msvc@npm:0.137.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxc-parser%2Fbinding-win32-x64-msvc%2F-%2Fbinding-win32-x64-msvc-0.137.0.tgz" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"@oxc-project/runtime@npm:=0.133.0": - version: 0.133.0 - resolution: "@oxc-project/runtime@npm:0.133.0" - checksum: 10c0/08e440cdb57b30f3166ae60c93006cb8a0ce88510ffa600b7ca1cf2262a169d372c89a263ac9fe022e4fab1cec498d5fe77ef91237322655112683e88df1afb6 +"@oxc-project/runtime@npm:=0.138.0": + version: 0.138.0 + resolution: "@oxc-project/runtime@npm:0.138.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxc-project%2Fruntime%2F-%2Fruntime-0.138.0.tgz" + checksum: 10c0/757dce891b8a47926a46dc006767c53490f607ced437fe1590f6ccdc52cd31d011b202b469f43dc2bf0c99cca8b82bad517b48e31bdc6c66b9f3f7e48fdc41c8 languageName: node linkType: hard -"@oxc-project/types@npm:=0.133.0": - version: 0.133.0 - resolution: "@oxc-project/types@npm:0.133.0" - checksum: 10c0/70c57ba58644f7ec217b670c301801f4d06995f4ccdba6b2bd106ea3e5ee49d616573e6ef8d55530b87571a960696543687f3850e87ad173d3f88965c30cdd63 +"@oxc-project/types@npm:=0.138.0": + version: 0.138.0 + resolution: "@oxc-project/types@npm:0.138.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxc-project%2Ftypes%2F-%2Ftypes-0.138.0.tgz" + checksum: 10c0/eacd260df0b961cb8863d0a0b3fab00c1f7701edfca28834b54628ca50ce703a4a3e7e6f9932ca11607d0cf876a0e1047b343a4672f1ad86d961017f7501524f languageName: node linkType: hard "@oxc-project/types@npm:^0.137.0": version: 0.137.0 - resolution: "@oxc-project/types@npm:0.137.0" + resolution: "@oxc-project/types@npm:0.137.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxc-project%2Ftypes%2F-%2Ftypes-0.137.0.tgz" checksum: 10c0/5a6a50174e5ac79aebf38a120fe57be7a84c8bb0c77117f30de15183aa5ab0161e78364d2d3725397090e362e5c5f6eda754b53057b0b63983e3ee604f888aca languageName: node linkType: hard -"@oxfmt/binding-android-arm-eabi@npm:0.52.0": - version: 0.52.0 - resolution: "@oxfmt/binding-android-arm-eabi@npm:0.52.0" +"@oxfmt/binding-android-arm-eabi@npm:0.57.0": + version: 0.57.0 + resolution: "@oxfmt/binding-android-arm-eabi@npm:0.57.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxfmt%2Fbinding-android-arm-eabi%2F-%2Fbinding-android-arm-eabi-0.57.0.tgz" conditions: os=android & cpu=arm languageName: node linkType: hard -"@oxfmt/binding-android-arm64@npm:0.52.0": - version: 0.52.0 - resolution: "@oxfmt/binding-android-arm64@npm:0.52.0" +"@oxfmt/binding-android-arm64@npm:0.57.0": + version: 0.57.0 + resolution: "@oxfmt/binding-android-arm64@npm:0.57.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxfmt%2Fbinding-android-arm64%2F-%2Fbinding-android-arm64-0.57.0.tgz" conditions: os=android & cpu=arm64 languageName: node linkType: hard -"@oxfmt/binding-darwin-arm64@npm:0.52.0": - version: 0.52.0 - resolution: "@oxfmt/binding-darwin-arm64@npm:0.52.0" +"@oxfmt/binding-darwin-arm64@npm:0.57.0": + version: 0.57.0 + resolution: "@oxfmt/binding-darwin-arm64@npm:0.57.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxfmt%2Fbinding-darwin-arm64%2F-%2Fbinding-darwin-arm64-0.57.0.tgz" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@oxfmt/binding-darwin-x64@npm:0.52.0": - version: 0.52.0 - resolution: "@oxfmt/binding-darwin-x64@npm:0.52.0" +"@oxfmt/binding-darwin-x64@npm:0.57.0": + version: 0.57.0 + resolution: "@oxfmt/binding-darwin-x64@npm:0.57.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxfmt%2Fbinding-darwin-x64%2F-%2Fbinding-darwin-x64-0.57.0.tgz" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@oxfmt/binding-freebsd-x64@npm:0.52.0": - version: 0.52.0 - resolution: "@oxfmt/binding-freebsd-x64@npm:0.52.0" +"@oxfmt/binding-freebsd-x64@npm:0.57.0": + version: 0.57.0 + resolution: "@oxfmt/binding-freebsd-x64@npm:0.57.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxfmt%2Fbinding-freebsd-x64%2F-%2Fbinding-freebsd-x64-0.57.0.tgz" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"@oxfmt/binding-linux-arm-gnueabihf@npm:0.52.0": - version: 0.52.0 - resolution: "@oxfmt/binding-linux-arm-gnueabihf@npm:0.52.0" +"@oxfmt/binding-linux-arm-gnueabihf@npm:0.57.0": + version: 0.57.0 + resolution: "@oxfmt/binding-linux-arm-gnueabihf@npm:0.57.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxfmt%2Fbinding-linux-arm-gnueabihf%2F-%2Fbinding-linux-arm-gnueabihf-0.57.0.tgz" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@oxfmt/binding-linux-arm-musleabihf@npm:0.52.0": - version: 0.52.0 - resolution: "@oxfmt/binding-linux-arm-musleabihf@npm:0.52.0" +"@oxfmt/binding-linux-arm-musleabihf@npm:0.57.0": + version: 0.57.0 + resolution: "@oxfmt/binding-linux-arm-musleabihf@npm:0.57.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxfmt%2Fbinding-linux-arm-musleabihf%2F-%2Fbinding-linux-arm-musleabihf-0.57.0.tgz" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@oxfmt/binding-linux-arm64-gnu@npm:0.52.0": - version: 0.52.0 - resolution: "@oxfmt/binding-linux-arm64-gnu@npm:0.52.0" +"@oxfmt/binding-linux-arm64-gnu@npm:0.57.0": + version: 0.57.0 + resolution: "@oxfmt/binding-linux-arm64-gnu@npm:0.57.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxfmt%2Fbinding-linux-arm64-gnu%2F-%2Fbinding-linux-arm64-gnu-0.57.0.tgz" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@oxfmt/binding-linux-arm64-musl@npm:0.52.0": - version: 0.52.0 - resolution: "@oxfmt/binding-linux-arm64-musl@npm:0.52.0" +"@oxfmt/binding-linux-arm64-musl@npm:0.57.0": + version: 0.57.0 + resolution: "@oxfmt/binding-linux-arm64-musl@npm:0.57.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxfmt%2Fbinding-linux-arm64-musl%2F-%2Fbinding-linux-arm64-musl-0.57.0.tgz" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@oxfmt/binding-linux-ppc64-gnu@npm:0.52.0": - version: 0.52.0 - resolution: "@oxfmt/binding-linux-ppc64-gnu@npm:0.52.0" +"@oxfmt/binding-linux-ppc64-gnu@npm:0.57.0": + version: 0.57.0 + resolution: "@oxfmt/binding-linux-ppc64-gnu@npm:0.57.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxfmt%2Fbinding-linux-ppc64-gnu%2F-%2Fbinding-linux-ppc64-gnu-0.57.0.tgz" conditions: os=linux & cpu=ppc64 & libc=glibc languageName: node linkType: hard -"@oxfmt/binding-linux-riscv64-gnu@npm:0.52.0": - version: 0.52.0 - resolution: "@oxfmt/binding-linux-riscv64-gnu@npm:0.52.0" +"@oxfmt/binding-linux-riscv64-gnu@npm:0.57.0": + version: 0.57.0 + resolution: "@oxfmt/binding-linux-riscv64-gnu@npm:0.57.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxfmt%2Fbinding-linux-riscv64-gnu%2F-%2Fbinding-linux-riscv64-gnu-0.57.0.tgz" conditions: os=linux & cpu=riscv64 & libc=glibc languageName: node linkType: hard -"@oxfmt/binding-linux-riscv64-musl@npm:0.52.0": - version: 0.52.0 - resolution: "@oxfmt/binding-linux-riscv64-musl@npm:0.52.0" +"@oxfmt/binding-linux-riscv64-musl@npm:0.57.0": + version: 0.57.0 + resolution: "@oxfmt/binding-linux-riscv64-musl@npm:0.57.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxfmt%2Fbinding-linux-riscv64-musl%2F-%2Fbinding-linux-riscv64-musl-0.57.0.tgz" conditions: os=linux & cpu=riscv64 & libc=musl languageName: node linkType: hard -"@oxfmt/binding-linux-s390x-gnu@npm:0.52.0": - version: 0.52.0 - resolution: "@oxfmt/binding-linux-s390x-gnu@npm:0.52.0" +"@oxfmt/binding-linux-s390x-gnu@npm:0.57.0": + version: 0.57.0 + resolution: "@oxfmt/binding-linux-s390x-gnu@npm:0.57.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxfmt%2Fbinding-linux-s390x-gnu%2F-%2Fbinding-linux-s390x-gnu-0.57.0.tgz" conditions: os=linux & cpu=s390x & libc=glibc languageName: node linkType: hard -"@oxfmt/binding-linux-x64-gnu@npm:0.52.0": - version: 0.52.0 - resolution: "@oxfmt/binding-linux-x64-gnu@npm:0.52.0" +"@oxfmt/binding-linux-x64-gnu@npm:0.57.0": + version: 0.57.0 + resolution: "@oxfmt/binding-linux-x64-gnu@npm:0.57.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxfmt%2Fbinding-linux-x64-gnu%2F-%2Fbinding-linux-x64-gnu-0.57.0.tgz" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@oxfmt/binding-linux-x64-musl@npm:0.52.0": - version: 0.52.0 - resolution: "@oxfmt/binding-linux-x64-musl@npm:0.52.0" +"@oxfmt/binding-linux-x64-musl@npm:0.57.0": + version: 0.57.0 + resolution: "@oxfmt/binding-linux-x64-musl@npm:0.57.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxfmt%2Fbinding-linux-x64-musl%2F-%2Fbinding-linux-x64-musl-0.57.0.tgz" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@oxfmt/binding-openharmony-arm64@npm:0.52.0": - version: 0.52.0 - resolution: "@oxfmt/binding-openharmony-arm64@npm:0.52.0" +"@oxfmt/binding-openharmony-arm64@npm:0.57.0": + version: 0.57.0 + resolution: "@oxfmt/binding-openharmony-arm64@npm:0.57.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxfmt%2Fbinding-openharmony-arm64%2F-%2Fbinding-openharmony-arm64-0.57.0.tgz" conditions: os=openharmony & cpu=arm64 languageName: node linkType: hard -"@oxfmt/binding-win32-arm64-msvc@npm:0.52.0": - version: 0.52.0 - resolution: "@oxfmt/binding-win32-arm64-msvc@npm:0.52.0" +"@oxfmt/binding-win32-arm64-msvc@npm:0.57.0": + version: 0.57.0 + resolution: "@oxfmt/binding-win32-arm64-msvc@npm:0.57.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxfmt%2Fbinding-win32-arm64-msvc%2F-%2Fbinding-win32-arm64-msvc-0.57.0.tgz" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@oxfmt/binding-win32-ia32-msvc@npm:0.52.0": - version: 0.52.0 - resolution: "@oxfmt/binding-win32-ia32-msvc@npm:0.52.0" +"@oxfmt/binding-win32-ia32-msvc@npm:0.57.0": + version: 0.57.0 + resolution: "@oxfmt/binding-win32-ia32-msvc@npm:0.57.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxfmt%2Fbinding-win32-ia32-msvc%2F-%2Fbinding-win32-ia32-msvc-0.57.0.tgz" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@oxfmt/binding-win32-x64-msvc@npm:0.52.0": - version: 0.52.0 - resolution: "@oxfmt/binding-win32-x64-msvc@npm:0.52.0" +"@oxfmt/binding-win32-x64-msvc@npm:0.57.0": + version: 0.57.0 + resolution: "@oxfmt/binding-win32-x64-msvc@npm:0.57.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxfmt%2Fbinding-win32-x64-msvc%2F-%2Fbinding-win32-x64-msvc-0.57.0.tgz" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"@oxlint-tsgolint/darwin-arm64@npm:0.23.0": - version: 0.23.0 - resolution: "@oxlint-tsgolint/darwin-arm64@npm:0.23.0" +"@oxlint-tsgolint/darwin-arm64@npm:0.24.0": + version: 0.24.0 + resolution: "@oxlint-tsgolint/darwin-arm64@npm:0.24.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxlint-tsgolint%2Fdarwin-arm64%2F-%2Fdarwin-arm64-0.24.0.tgz" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@oxlint-tsgolint/darwin-x64@npm:0.23.0": - version: 0.23.0 - resolution: "@oxlint-tsgolint/darwin-x64@npm:0.23.0" +"@oxlint-tsgolint/darwin-x64@npm:0.24.0": + version: 0.24.0 + resolution: "@oxlint-tsgolint/darwin-x64@npm:0.24.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxlint-tsgolint%2Fdarwin-x64%2F-%2Fdarwin-x64-0.24.0.tgz" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@oxlint-tsgolint/linux-arm64@npm:0.23.0": - version: 0.23.0 - resolution: "@oxlint-tsgolint/linux-arm64@npm:0.23.0" +"@oxlint-tsgolint/linux-arm64@npm:0.24.0": + version: 0.24.0 + resolution: "@oxlint-tsgolint/linux-arm64@npm:0.24.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxlint-tsgolint%2Flinux-arm64%2F-%2Flinux-arm64-0.24.0.tgz" conditions: os=linux & cpu=arm64 languageName: node linkType: hard -"@oxlint-tsgolint/linux-x64@npm:0.23.0": - version: 0.23.0 - resolution: "@oxlint-tsgolint/linux-x64@npm:0.23.0" +"@oxlint-tsgolint/linux-x64@npm:0.24.0": + version: 0.24.0 + resolution: "@oxlint-tsgolint/linux-x64@npm:0.24.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxlint-tsgolint%2Flinux-x64%2F-%2Flinux-x64-0.24.0.tgz" conditions: os=linux & cpu=x64 languageName: node linkType: hard -"@oxlint-tsgolint/win32-arm64@npm:0.23.0": - version: 0.23.0 - resolution: "@oxlint-tsgolint/win32-arm64@npm:0.23.0" +"@oxlint-tsgolint/win32-arm64@npm:0.24.0": + version: 0.24.0 + resolution: "@oxlint-tsgolint/win32-arm64@npm:0.24.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxlint-tsgolint%2Fwin32-arm64%2F-%2Fwin32-arm64-0.24.0.tgz" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@oxlint-tsgolint/win32-x64@npm:0.23.0": - version: 0.23.0 - resolution: "@oxlint-tsgolint/win32-x64@npm:0.23.0" +"@oxlint-tsgolint/win32-x64@npm:0.24.0": + version: 0.24.0 + resolution: "@oxlint-tsgolint/win32-x64@npm:0.24.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxlint-tsgolint%2Fwin32-x64%2F-%2Fwin32-x64-0.24.0.tgz" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"@oxlint/binding-android-arm-eabi@npm:1.67.0": - version: 1.67.0 - resolution: "@oxlint/binding-android-arm-eabi@npm:1.67.0" +"@oxlint/binding-android-arm-eabi@npm:1.72.0": + version: 1.72.0 + resolution: "@oxlint/binding-android-arm-eabi@npm:1.72.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxlint%2Fbinding-android-arm-eabi%2F-%2Fbinding-android-arm-eabi-1.72.0.tgz" conditions: os=android & cpu=arm languageName: node linkType: hard -"@oxlint/binding-android-arm64@npm:1.67.0": - version: 1.67.0 - resolution: "@oxlint/binding-android-arm64@npm:1.67.0" +"@oxlint/binding-android-arm64@npm:1.72.0": + version: 1.72.0 + resolution: "@oxlint/binding-android-arm64@npm:1.72.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxlint%2Fbinding-android-arm64%2F-%2Fbinding-android-arm64-1.72.0.tgz" conditions: os=android & cpu=arm64 languageName: node linkType: hard -"@oxlint/binding-darwin-arm64@npm:1.67.0": - version: 1.67.0 - resolution: "@oxlint/binding-darwin-arm64@npm:1.67.0" +"@oxlint/binding-darwin-arm64@npm:1.72.0": + version: 1.72.0 + resolution: "@oxlint/binding-darwin-arm64@npm:1.72.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxlint%2Fbinding-darwin-arm64%2F-%2Fbinding-darwin-arm64-1.72.0.tgz" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@oxlint/binding-darwin-x64@npm:1.67.0": - version: 1.67.0 - resolution: "@oxlint/binding-darwin-x64@npm:1.67.0" +"@oxlint/binding-darwin-x64@npm:1.72.0": + version: 1.72.0 + resolution: "@oxlint/binding-darwin-x64@npm:1.72.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxlint%2Fbinding-darwin-x64%2F-%2Fbinding-darwin-x64-1.72.0.tgz" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@oxlint/binding-freebsd-x64@npm:1.67.0": - version: 1.67.0 - resolution: "@oxlint/binding-freebsd-x64@npm:1.67.0" +"@oxlint/binding-freebsd-x64@npm:1.72.0": + version: 1.72.0 + resolution: "@oxlint/binding-freebsd-x64@npm:1.72.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxlint%2Fbinding-freebsd-x64%2F-%2Fbinding-freebsd-x64-1.72.0.tgz" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"@oxlint/binding-linux-arm-gnueabihf@npm:1.67.0": - version: 1.67.0 - resolution: "@oxlint/binding-linux-arm-gnueabihf@npm:1.67.0" +"@oxlint/binding-linux-arm-gnueabihf@npm:1.72.0": + version: 1.72.0 + resolution: "@oxlint/binding-linux-arm-gnueabihf@npm:1.72.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxlint%2Fbinding-linux-arm-gnueabihf%2F-%2Fbinding-linux-arm-gnueabihf-1.72.0.tgz" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@oxlint/binding-linux-arm-musleabihf@npm:1.67.0": - version: 1.67.0 - resolution: "@oxlint/binding-linux-arm-musleabihf@npm:1.67.0" +"@oxlint/binding-linux-arm-musleabihf@npm:1.72.0": + version: 1.72.0 + resolution: "@oxlint/binding-linux-arm-musleabihf@npm:1.72.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxlint%2Fbinding-linux-arm-musleabihf%2F-%2Fbinding-linux-arm-musleabihf-1.72.0.tgz" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@oxlint/binding-linux-arm64-gnu@npm:1.67.0": - version: 1.67.0 - resolution: "@oxlint/binding-linux-arm64-gnu@npm:1.67.0" +"@oxlint/binding-linux-arm64-gnu@npm:1.72.0": + version: 1.72.0 + resolution: "@oxlint/binding-linux-arm64-gnu@npm:1.72.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxlint%2Fbinding-linux-arm64-gnu%2F-%2Fbinding-linux-arm64-gnu-1.72.0.tgz" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@oxlint/binding-linux-arm64-musl@npm:1.67.0": - version: 1.67.0 - resolution: "@oxlint/binding-linux-arm64-musl@npm:1.67.0" +"@oxlint/binding-linux-arm64-musl@npm:1.72.0": + version: 1.72.0 + resolution: "@oxlint/binding-linux-arm64-musl@npm:1.72.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxlint%2Fbinding-linux-arm64-musl%2F-%2Fbinding-linux-arm64-musl-1.72.0.tgz" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@oxlint/binding-linux-ppc64-gnu@npm:1.67.0": - version: 1.67.0 - resolution: "@oxlint/binding-linux-ppc64-gnu@npm:1.67.0" +"@oxlint/binding-linux-ppc64-gnu@npm:1.72.0": + version: 1.72.0 + resolution: "@oxlint/binding-linux-ppc64-gnu@npm:1.72.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxlint%2Fbinding-linux-ppc64-gnu%2F-%2Fbinding-linux-ppc64-gnu-1.72.0.tgz" conditions: os=linux & cpu=ppc64 & libc=glibc languageName: node linkType: hard -"@oxlint/binding-linux-riscv64-gnu@npm:1.67.0": - version: 1.67.0 - resolution: "@oxlint/binding-linux-riscv64-gnu@npm:1.67.0" +"@oxlint/binding-linux-riscv64-gnu@npm:1.72.0": + version: 1.72.0 + resolution: "@oxlint/binding-linux-riscv64-gnu@npm:1.72.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxlint%2Fbinding-linux-riscv64-gnu%2F-%2Fbinding-linux-riscv64-gnu-1.72.0.tgz" conditions: os=linux & cpu=riscv64 & libc=glibc languageName: node linkType: hard -"@oxlint/binding-linux-riscv64-musl@npm:1.67.0": - version: 1.67.0 - resolution: "@oxlint/binding-linux-riscv64-musl@npm:1.67.0" +"@oxlint/binding-linux-riscv64-musl@npm:1.72.0": + version: 1.72.0 + resolution: "@oxlint/binding-linux-riscv64-musl@npm:1.72.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxlint%2Fbinding-linux-riscv64-musl%2F-%2Fbinding-linux-riscv64-musl-1.72.0.tgz" conditions: os=linux & cpu=riscv64 & libc=musl languageName: node linkType: hard -"@oxlint/binding-linux-s390x-gnu@npm:1.67.0": - version: 1.67.0 - resolution: "@oxlint/binding-linux-s390x-gnu@npm:1.67.0" +"@oxlint/binding-linux-s390x-gnu@npm:1.72.0": + version: 1.72.0 + resolution: "@oxlint/binding-linux-s390x-gnu@npm:1.72.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxlint%2Fbinding-linux-s390x-gnu%2F-%2Fbinding-linux-s390x-gnu-1.72.0.tgz" conditions: os=linux & cpu=s390x & libc=glibc languageName: node linkType: hard -"@oxlint/binding-linux-x64-gnu@npm:1.67.0": - version: 1.67.0 - resolution: "@oxlint/binding-linux-x64-gnu@npm:1.67.0" +"@oxlint/binding-linux-x64-gnu@npm:1.72.0": + version: 1.72.0 + resolution: "@oxlint/binding-linux-x64-gnu@npm:1.72.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxlint%2Fbinding-linux-x64-gnu%2F-%2Fbinding-linux-x64-gnu-1.72.0.tgz" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@oxlint/binding-linux-x64-musl@npm:1.67.0": - version: 1.67.0 - resolution: "@oxlint/binding-linux-x64-musl@npm:1.67.0" +"@oxlint/binding-linux-x64-musl@npm:1.72.0": + version: 1.72.0 + resolution: "@oxlint/binding-linux-x64-musl@npm:1.72.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxlint%2Fbinding-linux-x64-musl%2F-%2Fbinding-linux-x64-musl-1.72.0.tgz" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@oxlint/binding-openharmony-arm64@npm:1.67.0": - version: 1.67.0 - resolution: "@oxlint/binding-openharmony-arm64@npm:1.67.0" +"@oxlint/binding-openharmony-arm64@npm:1.72.0": + version: 1.72.0 + resolution: "@oxlint/binding-openharmony-arm64@npm:1.72.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxlint%2Fbinding-openharmony-arm64%2F-%2Fbinding-openharmony-arm64-1.72.0.tgz" conditions: os=openharmony & cpu=arm64 languageName: node linkType: hard -"@oxlint/binding-win32-arm64-msvc@npm:1.67.0": - version: 1.67.0 - resolution: "@oxlint/binding-win32-arm64-msvc@npm:1.67.0" +"@oxlint/binding-win32-arm64-msvc@npm:1.72.0": + version: 1.72.0 + resolution: "@oxlint/binding-win32-arm64-msvc@npm:1.72.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxlint%2Fbinding-win32-arm64-msvc%2F-%2Fbinding-win32-arm64-msvc-1.72.0.tgz" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@oxlint/binding-win32-ia32-msvc@npm:1.67.0": - version: 1.67.0 - resolution: "@oxlint/binding-win32-ia32-msvc@npm:1.67.0" +"@oxlint/binding-win32-ia32-msvc@npm:1.72.0": + version: 1.72.0 + resolution: "@oxlint/binding-win32-ia32-msvc@npm:1.72.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxlint%2Fbinding-win32-ia32-msvc%2F-%2Fbinding-win32-ia32-msvc-1.72.0.tgz" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@oxlint/binding-win32-x64-msvc@npm:1.67.0": - version: 1.67.0 - resolution: "@oxlint/binding-win32-x64-msvc@npm:1.67.0" +"@oxlint/binding-win32-x64-msvc@npm:1.72.0": + version: 1.72.0 + resolution: "@oxlint/binding-win32-x64-msvc@npm:1.72.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxlint%2Fbinding-win32-x64-msvc%2F-%2Fbinding-win32-x64-msvc-1.72.0.tgz" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"@oxlint/plugins@npm:=1.61.0": - version: 1.61.0 - resolution: "@oxlint/plugins@npm:1.61.0" - checksum: 10c0/214ca89a0ec498adda90031afd6c5d0571ac039c6e7bb6974e43642517fbd257c8069eb35925c3c705c2948ba19ed5585e2033a4c1b8bbc1fa0f39a72c1e7fb4 +"@oxlint/plugins@npm:=1.68.0": + version: 1.68.0 + resolution: "@oxlint/plugins@npm:1.68.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40oxlint%2Fplugins%2F-%2Fplugins-1.68.0.tgz" + checksum: 10c0/6ae7ccc55bc0abed0caeb0c5522e4f9a48207fafc14b1b6aa7e32d51085ff0f62dfd775ed735268c677cf739b89a72ff98baa867b28ead3290ff01a54b6b04f9 languageName: node linkType: hard "@polka/url@npm:^1.0.0-next.24": version: 1.0.0-next.29 - resolution: "@polka/url@npm:1.0.0-next.29" + resolution: "@polka/url@npm:1.0.0-next.29::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40polka%2Furl%2F-%2Furl-1.0.0-next.29.tgz" checksum: 10c0/0d58e081844095cb029d3c19a659bfefd09d5d51a2f791bc61eba7ea826f13d6ee204a8a448c2f5a855c17df07b37517373ff916dd05801063c0568ae9937684 languageName: node linkType: hard "@sindresorhus/is@npm:^4.0.0": version: 4.6.0 - resolution: "@sindresorhus/is@npm:4.6.0" + resolution: "@sindresorhus/is@npm:4.6.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40sindresorhus%2Fis%2F-%2Fis-4.6.0.tgz" checksum: 10c0/33b6fb1d0834ec8dd7689ddc0e2781c2bfd8b9c4e4bacbcb14111e0ae00621f2c264b8a7d36541799d74888b5dccdf422a891a5cb5a709ace26325eedc81e22e languageName: node linkType: hard "@standard-schema/spec@npm:^1.1.0": version: 1.1.0 - resolution: "@standard-schema/spec@npm:1.1.0" + resolution: "@standard-schema/spec@npm:1.1.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40standard-schema%2Fspec%2F-%2Fspec-1.1.0.tgz" checksum: 10c0/d90f55acde4b2deb983529c87e8025fa693de1a5e8b49ecc6eb84d1fd96328add0e03d7d551442156c7432fd78165b2c26ff561b970a9a881f046abb78d6a526 languageName: node linkType: hard @@ -2659,32 +2841,64 @@ __metadata: "@szmarczak/http-timer@npm:^4.0.5": version: 4.0.6 - resolution: "@szmarczak/http-timer@npm:4.0.6" + resolution: "@szmarczak/http-timer@npm:4.0.6::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40szmarczak%2Fhttp-timer%2F-%2Fhttp-timer-4.0.6.tgz" dependencies: defer-to-connect: "npm:^2.0.0" checksum: 10c0/73946918c025339db68b09abd91fa3001e87fc749c619d2e9c2003a663039d4c3cb89836c98a96598b3d47dec2481284ba85355392644911f5ecd2336536697f languageName: node linkType: hard +"@testing-library/dom@npm:^10.4.1": + version: 10.4.1 + resolution: "@testing-library/dom@npm:10.4.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40testing-library%2Fdom%2F-%2Fdom-10.4.1.tgz" + dependencies: + "@babel/code-frame": "npm:^7.10.4" + "@babel/runtime": "npm:^7.12.5" + "@types/aria-query": "npm:^5.0.1" + aria-query: "npm:5.3.0" + dom-accessibility-api: "npm:^0.5.9" + lz-string: "npm:^1.5.0" + picocolors: "npm:1.1.1" + pretty-format: "npm:^27.0.2" + checksum: 10c0/19ce048012d395ad0468b0dbcc4d0911f6f9e39464d7a8464a587b29707eed5482000dad728f5acc4ed314d2f4d54f34982999a114d2404f36d048278db815b1 + languageName: node + linkType: hard + +"@testing-library/user-event@npm:^14.6.1": + version: 14.6.1 + resolution: "@testing-library/user-event@npm:14.6.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40testing-library%2Fuser-event%2F-%2Fuser-event-14.6.1.tgz" + peerDependencies: + "@testing-library/dom": ">=7.21.4" + checksum: 10c0/75fea130a52bf320d35d46ed54f3eec77e71a56911b8b69a3fe29497b0b9947b2dc80d30f04054ad4ce7f577856ae3e5397ea7dff0ef14944d3909784c7a93fe + languageName: node + linkType: hard + "@tybys/wasm-util@npm:^0.10.3": version: 0.10.3 - resolution: "@tybys/wasm-util@npm:0.10.3" + resolution: "@tybys/wasm-util@npm:0.10.3::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40tybys%2Fwasm-util%2F-%2Fwasm-util-0.10.3.tgz" dependencies: tslib: "npm:^2.4.0" checksum: 10c0/fd2bd2a79c6cd8c79ed1cf7a0fa375c64589264c88a27acaf9756d556b453ea222b62a4f68dd2fbb8b3a78b6bab3b1f4fb2431b6afc6aeda8344b53a521a1cd3 languageName: node linkType: hard +"@types/aria-query@npm:^5.0.1": + version: 5.0.4 + resolution: "@types/aria-query@npm:5.0.4::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40types%2Faria-query%2F-%2Faria-query-5.0.4.tgz" + checksum: 10c0/dc667bc6a3acc7bba2bccf8c23d56cb1f2f4defaa704cfef595437107efaa972d3b3db9ec1d66bc2711bfc35086821edd32c302bffab36f2e79b97f312069f08 + languageName: node + linkType: hard + "@types/aws-lambda@npm:^8.10.83": version: 8.10.162 - resolution: "@types/aws-lambda@npm:8.10.162" + resolution: "@types/aws-lambda@npm:8.10.162::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40types%2Faws-lambda%2F-%2Faws-lambda-8.10.162.tgz" checksum: 10c0/0b93ebe339bf79d40e0811fb6ba658b425a5f9a45c2b07a2240e4260036ac949e6e5a5776db6269bc71218f127b046d7793f950f61ed7e94262b4fecf643eb7c languageName: node linkType: hard "@types/cacheable-request@npm:^6.0.1": version: 6.0.3 - resolution: "@types/cacheable-request@npm:6.0.3" + resolution: "@types/cacheable-request@npm:6.0.3::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40types%2Fcacheable-request%2F-%2Fcacheable-request-6.0.3.tgz" dependencies: "@types/http-cache-semantics": "npm:*" "@types/keyv": "npm:^3.1.4" @@ -2696,7 +2910,7 @@ __metadata: "@types/chai@npm:^5.2.2": version: 5.2.3 - resolution: "@types/chai@npm:5.2.3" + resolution: "@types/chai@npm:5.2.3::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40types%2Fchai%2F-%2Fchai-5.2.3.tgz" dependencies: "@types/deep-eql": "npm:*" assertion-error: "npm:^2.0.1" @@ -2706,28 +2920,35 @@ __metadata: "@types/deep-eql@npm:*": version: 4.0.2 - resolution: "@types/deep-eql@npm:4.0.2" + resolution: "@types/deep-eql@npm:4.0.2::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40types%2Fdeep-eql%2F-%2Fdeep-eql-4.0.2.tgz" checksum: 10c0/bf3f811843117900d7084b9d0c852da9a044d12eb40e6de73b552598a6843c21291a8a381b0532644574beecd5e3491c5ff3a0365ab86b15d59862c025384844 languageName: node linkType: hard "@types/emscripten@npm:^1.39.6": version: 1.41.5 - resolution: "@types/emscripten@npm:1.41.5" + resolution: "@types/emscripten@npm:1.41.5::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40types%2Femscripten%2F-%2Femscripten-1.41.5.tgz" checksum: 10c0/ae816da716f896434e59df7a71b67c71ae7e85ca067a32aef1616572fc4757459515d42ade6f5b8fd8d69733a9dbd0cf23010fec5b2f41ce52c09501aa350e45 languageName: node linkType: hard +"@types/estree@npm:^1.0.0": + version: 1.0.9 + resolution: "@types/estree@npm:1.0.9::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40types%2Festree%2F-%2Festree-1.0.9.tgz" + checksum: 10c0/3ad3286ca2988cd550dafb8f2ad599c8474868e954fa601a36655bdfefd8039f7c714b8c1c7f2ae219ffbd58bd4660e66fa7479a0120fc02d4777057d4865387 + languageName: node + linkType: hard + "@types/http-cache-semantics@npm:*": version: 4.2.0 - resolution: "@types/http-cache-semantics@npm:4.2.0" + resolution: "@types/http-cache-semantics@npm:4.2.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40types%2Fhttp-cache-semantics%2F-%2Fhttp-cache-semantics-4.2.0.tgz" checksum: 10c0/82dd33cbe7d4843f1e884a251c6a12d385b62274353b9db167462e7fbffdbb3a83606f9952203017c5b8cabbd7b9eef0cf240a3a9dedd20f69875c9701939415 languageName: node linkType: hard "@types/keyv@npm:^3.1.4": version: 3.1.4 - resolution: "@types/keyv@npm:3.1.4" + resolution: "@types/keyv@npm:3.1.4::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40types%2Fkeyv%2F-%2Fkeyv-3.1.4.tgz" dependencies: "@types/node": "npm:*" checksum: 10c0/ff8f54fc49621210291f815fe5b15d809fd7d032941b3180743440bd507ecdf08b9e844625fa346af568c84bf34114eb378dcdc3e921a08ba1e2a08d7e3c809c @@ -2735,17 +2956,17 @@ __metadata: linkType: hard "@types/node@npm:*, @types/node@npm:^26.0.0": - version: 26.0.0 - resolution: "@types/node@npm:26.0.0" + version: 26.1.0 + resolution: "@types/node@npm:26.1.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40types%2Fnode%2F-%2Fnode-26.1.0.tgz" dependencies: undici-types: "npm:~8.3.0" - checksum: 10c0/f36e21634fd8e8ded162ca486508bd8bb229d398a8d5541f6d8c1255968d4464e53327a77f403b216ef98e3b6f6956882eef83e4857d4bc2be9569dd55d37aae + checksum: 10c0/61c22ad1ef215e24138cb8b7368fb68bab9de4c123b3f23d411749b015ba1b7d22f878ad54add26a0b69068d7e07c04af665069d8571b6c6c3b9b2fb73b7bd91 languageName: node linkType: hard "@types/node@npm:@types/node@24.12.2": version: 24.12.2 - resolution: "@types/node@npm:24.12.2" + resolution: "@types/node@npm:24.12.2::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40types%2Fnode%2F-%2Fnode-24.12.2.tgz" dependencies: undici-types: "npm:~7.16.0" checksum: 10c0/710050c42f89075c4479e4e1e4c2532486b0c41b1e2a8a13ad88641c88b88cdaea87414e19224f30028719737bd70e327edcaa184d50e86b9418941edd7eb02b @@ -2754,7 +2975,7 @@ __metadata: "@types/responselike@npm:^1.0.0": version: 1.0.3 - resolution: "@types/responselike@npm:1.0.3" + resolution: "@types/responselike@npm:1.0.3::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40types%2Fresponselike%2F-%2Fresponselike-1.0.3.tgz" dependencies: "@types/node": "npm:*" checksum: 10c0/a58ba341cb9e7d74f71810a88862da7b2a6fa42e2a1fc0ce40498f6ea1d44382f0640117057da779f74c47039f7166bf48fad02dc876f94e005c7afa50f5e129 @@ -2763,40 +2984,151 @@ __metadata: "@types/semver@npm:^7.1.0, @types/semver@npm:^7.7.1": version: 7.7.1 - resolution: "@types/semver@npm:7.7.1" + resolution: "@types/semver@npm:7.7.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40types%2Fsemver%2F-%2Fsemver-7.7.1.tgz" checksum: 10c0/c938aef3bf79a73f0f3f6037c16e2e759ff40c54122ddf0b2583703393d8d3127130823facb880e694caa324eb6845628186aac1997ee8b31dc2d18fafe26268 languageName: node linkType: hard "@types/treeify@npm:^1.0.0": version: 1.0.3 - resolution: "@types/treeify@npm:1.0.3" + resolution: "@types/treeify@npm:1.0.3::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40types%2Ftreeify%2F-%2Ftreeify-1.0.3.tgz" checksum: 10c0/758902638ff83a790c13359729d77aeb80aae50f7039670037e3a0ba2bcc7b09dd49173ab21a96946d83af1682fcd70e448e49151ecd46e190f8925077142d4a languageName: node linkType: hard "@vercel/oidc@npm:3.2.0": version: 3.2.0 - resolution: "@vercel/oidc@npm:3.2.0" + resolution: "@vercel/oidc@npm:3.2.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40vercel%2Foidc%2F-%2Foidc-3.2.0.tgz" checksum: 10c0/98318d3236f58c296616c8c2e1655b268c7bf58525bcd985adac7af6d900e05fc610f6f03ce2ff4bdcd3df7885a40c0ca44fdc761f122dcfe15a78c2756b0243 languageName: node linkType: hard -"@voidzero-dev/vite-plus-core@npm:0.1.24": - version: 0.1.24 - resolution: "@voidzero-dev/vite-plus-core@npm:0.1.24" +"@vitest/browser-preview@npm:4.1.9": + version: 4.1.9 + resolution: "@vitest/browser-preview@npm:4.1.9::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40vitest%2Fbrowser-preview%2F-%2Fbrowser-preview-4.1.9.tgz" + dependencies: + "@testing-library/dom": "npm:^10.4.1" + "@testing-library/user-event": "npm:^14.6.1" + "@vitest/browser": "npm:4.1.9" + peerDependencies: + vitest: 4.1.9 + checksum: 10c0/6c22a806ae33d3a55e8a23de1ce06cd4452d421dd513a35d9f2a68bf5e65f1bd8a0b5325b25ea0b5d0b4c03a769a698792c3cd00e0ca7cd22545cd8e747dc80a + languageName: node + linkType: hard + +"@vitest/browser@npm:4.1.9": + version: 4.1.9 + resolution: "@vitest/browser@npm:4.1.9::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40vitest%2Fbrowser%2F-%2Fbrowser-4.1.9.tgz" dependencies: - "@oxc-project/runtime": "npm:=0.133.0" - "@oxc-project/types": "npm:=0.133.0" + "@blazediff/core": "npm:1.9.1" + "@vitest/mocker": "npm:4.1.9" + "@vitest/utils": "npm:4.1.9" + magic-string: "npm:^0.30.21" + pngjs: "npm:^7.0.0" + sirv: "npm:^3.0.2" + tinyrainbow: "npm:^3.1.0" + ws: "npm:^8.19.0" + peerDependencies: + vitest: 4.1.9 + checksum: 10c0/9ff634e56b0bd5a3727f2eec7666f97a089b92fb596d5b9e4fbe2a2e4fd988aa7a7bd2a7b98d7cf14790717db69db8f72bae6dc007f2fac613b9bb93d8e8edfb + languageName: node + linkType: hard + +"@vitest/expect@npm:4.1.9": + version: 4.1.9 + resolution: "@vitest/expect@npm:4.1.9::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40vitest%2Fexpect%2F-%2Fexpect-4.1.9.tgz" + dependencies: + "@standard-schema/spec": "npm:^1.1.0" + "@types/chai": "npm:^5.2.2" + "@vitest/spy": "npm:4.1.9" + "@vitest/utils": "npm:4.1.9" + chai: "npm:^6.2.2" + tinyrainbow: "npm:^3.1.0" + checksum: 10c0/243bacaed2cba5e0ea4ec7465662fcec465a358a0e06381e337fac49426aa67a73b104fbb9d65d8bccadfba8f70e27f57ffb897aacfa140f579a556367357875 + languageName: node + linkType: hard + +"@vitest/mocker@npm:4.1.9": + version: 4.1.9 + resolution: "@vitest/mocker@npm:4.1.9::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40vitest%2Fmocker%2F-%2Fmocker-4.1.9.tgz" + dependencies: + "@vitest/spy": "npm:4.1.9" + estree-walker: "npm:^3.0.3" + magic-string: "npm:^0.30.21" + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + checksum: 10c0/707353b7435bbfd441cc754e4ee7bc5921b70d07b051c6e414b6bbe4ca369154702b0ddeb603389469fe87ca1983e002eb2d55044582661f54a1945dd27e5c82 + languageName: node + linkType: hard + +"@vitest/pretty-format@npm:4.1.9": + version: 4.1.9 + resolution: "@vitest/pretty-format@npm:4.1.9::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40vitest%2Fpretty-format%2F-%2Fpretty-format-4.1.9.tgz" + dependencies: + tinyrainbow: "npm:^3.1.0" + checksum: 10c0/5b96295f25ab885616230ad1355fc82f490bebb39cc707688d7c8969c08270d7e076ed8a10af4e762ed57145193c6061a1f549f136f0ded344f8db0c2b3fb3de + languageName: node + linkType: hard + +"@vitest/runner@npm:4.1.9": + version: 4.1.9 + resolution: "@vitest/runner@npm:4.1.9::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40vitest%2Frunner%2F-%2Frunner-4.1.9.tgz" + dependencies: + "@vitest/utils": "npm:4.1.9" + pathe: "npm:^2.0.3" + checksum: 10c0/d206b4891a64b1f55c346f832b0a7b489108094d8ae34438d3b53e78be7b45b139fa95ffa027c98c357bd532268ee573168de1943235b7eed32a9236ed5978bb + languageName: node + linkType: hard + +"@vitest/snapshot@npm:4.1.9": + version: 4.1.9 + resolution: "@vitest/snapshot@npm:4.1.9::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40vitest%2Fsnapshot%2F-%2Fsnapshot-4.1.9.tgz" + dependencies: + "@vitest/pretty-format": "npm:4.1.9" + "@vitest/utils": "npm:4.1.9" + magic-string: "npm:^0.30.21" + pathe: "npm:^2.0.3" + checksum: 10c0/c3099df12ad1f9c1e180441856c9eb82f1990f87ff16aafedd6fa19978eaff20bc59220b692a99fcc822daef86eab256ba3dadb49544b7bd625b57c49cd9d995 + languageName: node + linkType: hard + +"@vitest/spy@npm:4.1.9": + version: 4.1.9 + resolution: "@vitest/spy@npm:4.1.9::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40vitest%2Fspy%2F-%2Fspy-4.1.9.tgz" + checksum: 10c0/e51f328f55b76e8ba66e5e18f183484a8dc0a092685b101112d3e9fb8e989ddca162c98ddf00254476502c25bc05c4ec1e277fd6ad8bfc702464c08f6b5dd115 + languageName: node + linkType: hard + +"@vitest/utils@npm:4.1.9": + version: 4.1.9 + resolution: "@vitest/utils@npm:4.1.9::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40vitest%2Futils%2F-%2Futils-4.1.9.tgz" + dependencies: + "@vitest/pretty-format": "npm:4.1.9" + convert-source-map: "npm:^2.0.0" + tinyrainbow: "npm:^3.1.0" + checksum: 10c0/d55506c077fd72c091eb66f02926f0abf72801c87a085f565698289562f47befa114ae2c680ab8736dfe46abab0cfd6b8031f2ac519bafeb37578aa6e5ad03c5 + languageName: node + linkType: hard + +"@voidzero-dev/vite-plus-core@npm:0.0.0-commit.1d7ba811f0e456640b0e761e8e8acf77d1ad3251, vite@npm:@voidzero-dev/vite-plus-core@0.0.0-commit.1d7ba811f0e456640b0e761e8e8acf77d1ad3251": + version: 0.0.0-commit.1d7ba811f0e456640b0e761e8e8acf77d1ad3251 + resolution: "@voidzero-dev/vite-plus-core@npm:0.0.0-commit.1d7ba811f0e456640b0e761e8e8acf77d1ad3251::__archiveUrl=https%3A%2F%2Fregistry-bridge.viteplus.dev%2Ftarballs%2F%40voidzero-dev%2Fvite-plus-core%2F0.0.0-commit.1d7ba811f0e456640b0e761e8e8acf77d1ad3251.tgz" + dependencies: + "@oxc-project/runtime": "npm:=0.138.0" + "@oxc-project/types": "npm:=0.138.0" fsevents: "npm:~2.3.3" - lightningcss: "npm:^1.30.2" + lightningcss: "npm:^1.32.0" postcss: "npm:^8.5.6" peerDependencies: "@arethetypeswrong/core": ^0.18.1 - "@tsdown/css": 0.22.1 - "@tsdown/exe": 0.22.1 "@types/node": ^20.19.0 || >=22.12.0 - "@vitejs/devtools": ^0.1.18 + "@vitejs/devtools": ^0.3.0 esbuild: ^0.27.0 || ^0.28.0 jiti: ">=1.21.0" less: ^4.0.0 @@ -2817,10 +3149,6 @@ __metadata: peerDependenciesMeta: "@arethetypeswrong/core": optional: true - "@tsdown/css": - optional: true - "@tsdown/exe": - optional: true "@types/node": optional: true "@vitejs/devtools": @@ -2853,119 +3181,69 @@ __metadata: optional: true yaml: optional: true - checksum: 10c0/7b26a180c0cb1afe251e3f34b5cb51885d533dc28b1b790f8c81caf5c2099c0d04f48389c32549016abb388ad6d14f45a2fd19de6b847649a0c3c0128f46734b + checksum: 10c0/c1a3818d16b9889cd5b8625ca70b15acf108ba0630a876bcfb643e8edb08c6f452f7ce91db838d2814e37cf6ec9d684d118f106bd76d26303ab2a1d1e3c85dcb languageName: node linkType: hard -"@voidzero-dev/vite-plus-darwin-arm64@npm:0.1.24": - version: 0.1.24 - resolution: "@voidzero-dev/vite-plus-darwin-arm64@npm:0.1.24" +"@voidzero-dev/vite-plus-darwin-arm64@npm:0.0.0-commit.1d7ba811f0e456640b0e761e8e8acf77d1ad3251": + version: 0.0.0-commit.1d7ba811f0e456640b0e761e8e8acf77d1ad3251 + resolution: "@voidzero-dev/vite-plus-darwin-arm64@npm:0.0.0-commit.1d7ba811f0e456640b0e761e8e8acf77d1ad3251::__archiveUrl=https%3A%2F%2Fregistry-bridge.viteplus.dev%2Ftarballs%2F%40voidzero-dev%2Fvite-plus-darwin-arm64%2F0.0.0-commit.1d7ba811f0e456640b0e761e8e8acf77d1ad3251.tgz" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@voidzero-dev/vite-plus-darwin-x64@npm:0.1.24": - version: 0.1.24 - resolution: "@voidzero-dev/vite-plus-darwin-x64@npm:0.1.24" +"@voidzero-dev/vite-plus-darwin-x64@npm:0.0.0-commit.1d7ba811f0e456640b0e761e8e8acf77d1ad3251": + version: 0.0.0-commit.1d7ba811f0e456640b0e761e8e8acf77d1ad3251 + resolution: "@voidzero-dev/vite-plus-darwin-x64@npm:0.0.0-commit.1d7ba811f0e456640b0e761e8e8acf77d1ad3251::__archiveUrl=https%3A%2F%2Fregistry-bridge.viteplus.dev%2Ftarballs%2F%40voidzero-dev%2Fvite-plus-darwin-x64%2F0.0.0-commit.1d7ba811f0e456640b0e761e8e8acf77d1ad3251.tgz" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@voidzero-dev/vite-plus-linux-arm64-gnu@npm:0.1.24": - version: 0.1.24 - resolution: "@voidzero-dev/vite-plus-linux-arm64-gnu@npm:0.1.24" +"@voidzero-dev/vite-plus-linux-arm64-gnu@npm:0.0.0-commit.1d7ba811f0e456640b0e761e8e8acf77d1ad3251": + version: 0.0.0-commit.1d7ba811f0e456640b0e761e8e8acf77d1ad3251 + resolution: "@voidzero-dev/vite-plus-linux-arm64-gnu@npm:0.0.0-commit.1d7ba811f0e456640b0e761e8e8acf77d1ad3251::__archiveUrl=https%3A%2F%2Fregistry-bridge.viteplus.dev%2Ftarballs%2F%40voidzero-dev%2Fvite-plus-linux-arm64-gnu%2F0.0.0-commit.1d7ba811f0e456640b0e761e8e8acf77d1ad3251.tgz" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@voidzero-dev/vite-plus-linux-arm64-musl@npm:0.1.24": - version: 0.1.24 - resolution: "@voidzero-dev/vite-plus-linux-arm64-musl@npm:0.1.24" +"@voidzero-dev/vite-plus-linux-arm64-musl@npm:0.0.0-commit.1d7ba811f0e456640b0e761e8e8acf77d1ad3251": + version: 0.0.0-commit.1d7ba811f0e456640b0e761e8e8acf77d1ad3251 + resolution: "@voidzero-dev/vite-plus-linux-arm64-musl@npm:0.0.0-commit.1d7ba811f0e456640b0e761e8e8acf77d1ad3251::__archiveUrl=https%3A%2F%2Fregistry-bridge.viteplus.dev%2Ftarballs%2F%40voidzero-dev%2Fvite-plus-linux-arm64-musl%2F0.0.0-commit.1d7ba811f0e456640b0e761e8e8acf77d1ad3251.tgz" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@voidzero-dev/vite-plus-linux-x64-gnu@npm:0.1.24": - version: 0.1.24 - resolution: "@voidzero-dev/vite-plus-linux-x64-gnu@npm:0.1.24" +"@voidzero-dev/vite-plus-linux-x64-gnu@npm:0.0.0-commit.1d7ba811f0e456640b0e761e8e8acf77d1ad3251": + version: 0.0.0-commit.1d7ba811f0e456640b0e761e8e8acf77d1ad3251 + resolution: "@voidzero-dev/vite-plus-linux-x64-gnu@npm:0.0.0-commit.1d7ba811f0e456640b0e761e8e8acf77d1ad3251::__archiveUrl=https%3A%2F%2Fregistry-bridge.viteplus.dev%2Ftarballs%2F%40voidzero-dev%2Fvite-plus-linux-x64-gnu%2F0.0.0-commit.1d7ba811f0e456640b0e761e8e8acf77d1ad3251.tgz" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@voidzero-dev/vite-plus-linux-x64-musl@npm:0.1.24": - version: 0.1.24 - resolution: "@voidzero-dev/vite-plus-linux-x64-musl@npm:0.1.24" +"@voidzero-dev/vite-plus-linux-x64-musl@npm:0.0.0-commit.1d7ba811f0e456640b0e761e8e8acf77d1ad3251": + version: 0.0.0-commit.1d7ba811f0e456640b0e761e8e8acf77d1ad3251 + resolution: "@voidzero-dev/vite-plus-linux-x64-musl@npm:0.0.0-commit.1d7ba811f0e456640b0e761e8e8acf77d1ad3251::__archiveUrl=https%3A%2F%2Fregistry-bridge.viteplus.dev%2Ftarballs%2F%40voidzero-dev%2Fvite-plus-linux-x64-musl%2F0.0.0-commit.1d7ba811f0e456640b0e761e8e8acf77d1ad3251.tgz" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@voidzero-dev/vite-plus-test@npm:0.1.24, vitest@npm:@voidzero-dev/vite-plus-test@0.1.24": - version: 0.1.24 - resolution: "@voidzero-dev/vite-plus-test@npm:0.1.24" - dependencies: - "@standard-schema/spec": "npm:^1.1.0" - "@types/chai": "npm:^5.2.2" - "@voidzero-dev/vite-plus-core": "npm:0.1.24" - es-module-lexer: "npm:^1.7.0" - obug: "npm:^2.1.1" - pixelmatch: "npm:^7.1.0" - pngjs: "npm:^7.0.0" - sirv: "npm:^3.0.2" - std-env: "npm:^4.0.0" - tinybench: "npm:^2.9.0" - tinyexec: "npm:^1.0.2" - tinyglobby: "npm:^0.2.15" - ws: "npm:^8.18.3" - peerDependencies: - "@edge-runtime/vm": "*" - "@opentelemetry/api": ^1.9.0 - "@types/node": ^20.0.0 || ^22.0.0 || >=24.0.0 - "@vitest/coverage-istanbul": 4.1.8 - "@vitest/coverage-v8": 4.1.8 - "@vitest/ui": 4.1.8 - happy-dom: "*" - jsdom: "*" - vite: ^6.0.0 || ^7.0.0 || ^8.0.0 - peerDependenciesMeta: - "@edge-runtime/vm": - optional: true - "@opentelemetry/api": - optional: true - "@types/node": - optional: true - "@vitest/coverage-istanbul": - optional: true - "@vitest/coverage-v8": - optional: true - "@vitest/ui": - optional: true - happy-dom: - optional: true - jsdom: - optional: true - vite: - optional: false - checksum: 10c0/abb405e45a4304fb75ee057b3c00f2df7a401e34fa1d4c049bd8a608255db6343764a10a1dc972ff11585a885745b826bee49ad348edac4d859ff1793ee155af - languageName: node - linkType: hard - -"@voidzero-dev/vite-plus-win32-arm64-msvc@npm:0.1.24": - version: 0.1.24 - resolution: "@voidzero-dev/vite-plus-win32-arm64-msvc@npm:0.1.24" +"@voidzero-dev/vite-plus-win32-arm64-msvc@npm:0.0.0-commit.1d7ba811f0e456640b0e761e8e8acf77d1ad3251": + version: 0.0.0-commit.1d7ba811f0e456640b0e761e8e8acf77d1ad3251 + resolution: "@voidzero-dev/vite-plus-win32-arm64-msvc@npm:0.0.0-commit.1d7ba811f0e456640b0e761e8e8acf77d1ad3251::__archiveUrl=https%3A%2F%2Fregistry-bridge.viteplus.dev%2Ftarballs%2F%40voidzero-dev%2Fvite-plus-win32-arm64-msvc%2F0.0.0-commit.1d7ba811f0e456640b0e761e8e8acf77d1ad3251.tgz" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@voidzero-dev/vite-plus-win32-x64-msvc@npm:0.1.24": - version: 0.1.24 - resolution: "@voidzero-dev/vite-plus-win32-x64-msvc@npm:0.1.24" +"@voidzero-dev/vite-plus-win32-x64-msvc@npm:0.0.0-commit.1d7ba811f0e456640b0e761e8e8acf77d1ad3251": + version: 0.0.0-commit.1d7ba811f0e456640b0e761e8e8acf77d1ad3251 + resolution: "@voidzero-dev/vite-plus-win32-x64-msvc@npm:0.0.0-commit.1d7ba811f0e456640b0e761e8e8acf77d1ad3251::__archiveUrl=https%3A%2F%2Fregistry-bridge.viteplus.dev%2Ftarballs%2F%40voidzero-dev%2Fvite-plus-win32-x64-msvc%2F0.0.0-commit.1d7ba811f0e456640b0e761e8e8acf77d1ad3251.tgz" conditions: os=win32 & cpu=x64 languageName: node linkType: hard "@yarnpkg/core@npm:^4.9.0": version: 4.9.0 - resolution: "@yarnpkg/core@npm:4.9.0" + resolution: "@yarnpkg/core@npm:4.9.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40yarnpkg%2Fcore%2F-%2Fcore-4.9.0.tgz" dependencies: "@arcanis/slice-ansi": "npm:^1.1.1" "@types/semver": "npm:^7.1.0" @@ -2999,7 +3277,7 @@ __metadata: "@yarnpkg/fslib@npm:^3.1.2, @yarnpkg/fslib@npm:^3.1.3, @yarnpkg/fslib@npm:^3.1.5": version: 3.1.5 - resolution: "@yarnpkg/fslib@npm:3.1.5" + resolution: "@yarnpkg/fslib@npm:3.1.5::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40yarnpkg%2Ffslib%2F-%2Ffslib-3.1.5.tgz" dependencies: tslib: "npm:^2.4.0" checksum: 10c0/e360209a31d60cc8452417cdd750d9d7c5face37eb3508de2947477509f3a93c6c5d0737bf1a3a270bce5ef16dd4f5a562fb9489c7d9b80aa10cc787c45485d3 @@ -3008,7 +3286,7 @@ __metadata: "@yarnpkg/libzip@npm:^3.2.2": version: 3.2.2 - resolution: "@yarnpkg/libzip@npm:3.2.2" + resolution: "@yarnpkg/libzip@npm:3.2.2::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40yarnpkg%2Flibzip%2F-%2Flibzip-3.2.2.tgz" dependencies: "@types/emscripten": "npm:^1.39.6" "@yarnpkg/fslib": "npm:^3.1.3" @@ -3021,7 +3299,7 @@ __metadata: "@yarnpkg/parsers@npm:^3.0.3": version: 3.0.3 - resolution: "@yarnpkg/parsers@npm:3.0.3" + resolution: "@yarnpkg/parsers@npm:3.0.3::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40yarnpkg%2Fparsers%2F-%2Fparsers-3.0.3.tgz" dependencies: js-yaml: "npm:^3.10.0" tslib: "npm:^2.4.0" @@ -3031,7 +3309,7 @@ __metadata: "@yarnpkg/shell@npm:^4.1.3": version: 4.1.3 - resolution: "@yarnpkg/shell@npm:4.1.3" + resolution: "@yarnpkg/shell@npm:4.1.3::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40yarnpkg%2Fshell%2F-%2Fshell-4.1.3.tgz" dependencies: "@yarnpkg/fslib": "npm:^3.1.2" "@yarnpkg/parsers": "npm:^3.0.3" @@ -3049,44 +3327,51 @@ __metadata: "abbrev@npm:^5.0.0": version: 5.0.0 - resolution: "abbrev@npm:5.0.0" + resolution: "abbrev@npm:5.0.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fabbrev%2F-%2Fabbrev-5.0.0.tgz" checksum: 10c0/8e88f5c798ea4562d28c5a3e9ad69e3879890bc5d695d8f2dffb8609be4c890aacc8f80ef4553fdd2c6a62d70c2ce8bc57b38074e383beb7487bdafa9ed42ea5 languageName: node linkType: hard "ai@npm:^6.0.209": - version: 6.0.209 - resolution: "ai@npm:6.0.209" + version: 6.0.218 + resolution: "ai@npm:6.0.218::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fai%2F-%2Fai-6.0.218.tgz" dependencies: - "@ai-sdk/gateway": "npm:3.0.134" - "@ai-sdk/provider": "npm:3.0.10" - "@ai-sdk/provider-utils": "npm:4.0.30" + "@ai-sdk/gateway": "npm:3.0.142" + "@ai-sdk/provider": "npm:3.0.13" + "@ai-sdk/provider-utils": "npm:4.0.35" "@opentelemetry/api": "npm:^1.9.0" peerDependencies: zod: ^3.25.76 || ^4.1.8 - checksum: 10c0/b3ed9a57a84ff5d6f2a8177382637cd788f74be61bfc6419d56345a5943b98c5e98c4e1348e2be7cb054292b7b09888e18b813ec407af56e4df2000caecb2ce2 + checksum: 10c0/a4ede88ae30190ee1573166abc4bfc213bf9aa824213731541dc61f8e6fef4439921e2bf2af60c42b8a9182f58a953ecc36fb7e380ad0d1e71e27f9492ba8699 languageName: node linkType: hard "ansi-regex@npm:^5.0.1": version: 5.0.1 - resolution: "ansi-regex@npm:5.0.1" + resolution: "ansi-regex@npm:5.0.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fansi-regex%2F-%2Fansi-regex-5.0.1.tgz" checksum: 10c0/9a64bb8627b434ba9327b60c027742e5d17ac69277960d041898596271d992d4d52ba7267a63ca10232e29f6107fc8a835f6ce8d719b88c5f8493f8254813737 languageName: node linkType: hard "ansi-styles@npm:^4.1.0": version: 4.3.0 - resolution: "ansi-styles@npm:4.3.0" + resolution: "ansi-styles@npm:4.3.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fansi-styles%2F-%2Fansi-styles-4.3.0.tgz" dependencies: color-convert: "npm:^2.0.1" checksum: 10c0/895a23929da416f2bd3de7e9cb4eabd340949328ab85ddd6e484a637d8f6820d485f53933446f5291c3b760cbc488beb8e88573dd0f9c7daf83dccc8fe81b041 languageName: node linkType: hard +"ansi-styles@npm:^5.0.0": + version: 5.2.0 + resolution: "ansi-styles@npm:5.2.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fansi-styles%2F-%2Fansi-styles-5.2.0.tgz" + checksum: 10c0/9c4ca80eb3c2fb7b33841c210d2f20807f40865d27008d7c3f707b7f95cab7d67462a565e2388ac3285b71cb3d9bb2173de8da37c57692a362885ec34d6e27df + languageName: node + linkType: hard + "argparse@npm:^1.0.7": version: 1.0.10 - resolution: "argparse@npm:1.0.10" + resolution: "argparse@npm:1.0.10::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fargparse%2F-%2Fargparse-1.0.10.tgz" dependencies: sprintf-js: "npm:~1.0.2" checksum: 10c0/b2972c5c23c63df66bca144dbc65d180efa74f25f8fd9b7d9a0a6c88ae839db32df3d54770dcb6460cf840d232b60695d1a6b1053f599d84e73f7437087712de @@ -3095,35 +3380,44 @@ __metadata: "argparse@npm:^2.0.1": version: 2.0.1 - resolution: "argparse@npm:2.0.1" + resolution: "argparse@npm:2.0.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fargparse%2F-%2Fargparse-2.0.1.tgz" checksum: 10c0/c5640c2d89045371c7cedd6a70212a04e360fd34d6edeae32f6952c63949e3525ea77dbec0289d8213a99bbaeab5abfa860b5c12cf88a2e6cf8106e90dd27a7e languageName: node linkType: hard +"aria-query@npm:5.3.0": + version: 5.3.0 + resolution: "aria-query@npm:5.3.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Faria-query%2F-%2Faria-query-5.3.0.tgz" + dependencies: + dequal: "npm:^2.0.3" + checksum: 10c0/2bff0d4eba5852a9dd578ecf47eaef0e82cc52569b48469b0aac2db5145db0b17b7a58d9e01237706d1e14b7a1b0ac9b78e9c97027ad97679dd8f91b85da1469 + languageName: node + linkType: hard + "assertion-error@npm:^2.0.1": version: 2.0.1 - resolution: "assertion-error@npm:2.0.1" + resolution: "assertion-error@npm:2.0.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fassertion-error%2F-%2Fassertion-error-2.0.1.tgz" checksum: 10c0/bbbcb117ac6480138f8c93cf7f535614282dea9dc828f540cdece85e3c665e8f78958b96afac52f29ff883c72638e6a87d469ecc9fe5bc902df03ed24a55dba8 languageName: node linkType: hard "before-after-hook@npm:^4.0.0": version: 4.0.0 - resolution: "before-after-hook@npm:4.0.0" + resolution: "before-after-hook@npm:4.0.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fbefore-after-hook%2F-%2Fbefore-after-hook-4.0.0.tgz" checksum: 10c0/9f8ae8d1b06142bcfb9ef6625226b5e50348bb11210f266660eddcf9734e0db6f9afc4cb48397ee3f5ac0a3728f3ae401cdeea88413f7bed748a71db84657be2 languageName: node linkType: hard "bottleneck@npm:^2.15.3": version: 2.19.5 - resolution: "bottleneck@npm:2.19.5" + resolution: "bottleneck@npm:2.19.5::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fbottleneck%2F-%2Fbottleneck-2.19.5.tgz" checksum: 10c0/b0f72e45b2e0f56a21ba720183f16bef8e693452fb0495d997fa354e42904353a94bd8fd429868e6751bc85e54b6755190519eed5a0ae0a94a5185209ae7c6d0 languageName: node linkType: hard "braces@npm:^3.0.3": version: 3.0.3 - resolution: "braces@npm:3.0.3" + resolution: "braces@npm:3.0.3::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fbraces%2F-%2Fbraces-3.0.3.tgz" dependencies: fill-range: "npm:^7.1.1" checksum: 10c0/7c6dfd30c338d2997ba77500539227b9d1f85e388a5f43220865201e407e076783d0881f2d297b9f80951b4c957fcf0b51c1d2d24227631643c3f7c284b0aa04 @@ -3132,14 +3426,14 @@ __metadata: "cacheable-lookup@npm:^5.0.3": version: 5.0.4 - resolution: "cacheable-lookup@npm:5.0.4" + resolution: "cacheable-lookup@npm:5.0.4::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fcacheable-lookup%2F-%2Fcacheable-lookup-5.0.4.tgz" checksum: 10c0/a6547fb4954b318aa831cbdd2f7b376824bc784fb1fa67610e4147099e3074726072d9af89f12efb69121415a0e1f2918a8ddd4aafcbcf4e91fbeef4a59cd42c languageName: node linkType: hard "cacheable-request@npm:^7.0.2": version: 7.0.4 - resolution: "cacheable-request@npm:7.0.4" + resolution: "cacheable-request@npm:7.0.4::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fcacheable-request%2F-%2Fcacheable-request-7.0.4.tgz" dependencies: clone-response: "npm:^1.0.2" get-stream: "npm:^5.1.0" @@ -3154,14 +3448,21 @@ __metadata: "camelcase@npm:^5.3.1": version: 5.3.1 - resolution: "camelcase@npm:5.3.1" + resolution: "camelcase@npm:5.3.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fcamelcase%2F-%2Fcamelcase-5.3.1.tgz" checksum: 10c0/92ff9b443bfe8abb15f2b1513ca182d16126359ad4f955ebc83dc4ddcc4ef3fdd2c078bc223f2673dc223488e75c99b16cc4d056624374b799e6a1555cf61b23 languageName: node linkType: hard +"chai@npm:^6.2.2": + version: 6.2.2 + resolution: "chai@npm:6.2.2::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fchai%2F-%2Fchai-6.2.2.tgz" + checksum: 10c0/e6c69e5f0c11dffe6ea13d0290936ebb68fcc1ad688b8e952e131df6a6d5797d5e860bc55cef1aca2e950c3e1f96daf79e9d5a70fb7dbaab4e46355e2635ed53 + languageName: node + linkType: hard + "chalk@npm:^4.1.2": version: 4.1.2 - resolution: "chalk@npm:4.1.2" + resolution: "chalk@npm:4.1.2::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fchalk%2F-%2Fchalk-4.1.2.tgz" dependencies: ansi-styles: "npm:^4.1.0" supports-color: "npm:^7.1.0" @@ -3171,35 +3472,35 @@ __metadata: "change-case@npm:^5.4.4": version: 5.4.4 - resolution: "change-case@npm:5.4.4" + resolution: "change-case@npm:5.4.4::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fchange-case%2F-%2Fchange-case-5.4.4.tgz" checksum: 10c0/2a9c2b9c9ad6ab2491105aaf506db1a9acaf543a18967798dcce20926c6a173aa63266cb6189f3086e3c14bf7ae1f8ea4f96ecc466fcd582310efa00372f3734 languageName: node linkType: hard "chardet@npm:^2.1.1": version: 2.2.0 - resolution: "chardet@npm:2.2.0" + resolution: "chardet@npm:2.2.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fchardet%2F-%2Fchardet-2.2.0.tgz" checksum: 10c0/8d43a1dd3ce535aa070d04139fae62f879b4388394eb1d857364ce416675a1f0f63974fba8208e9cd11b49e1a6da3e65ea6393d0fc654a8c4217c0d74c16b1b4 languageName: node linkType: hard "chownr@npm:^3.0.0": version: 3.0.0 - resolution: "chownr@npm:3.0.0" + resolution: "chownr@npm:3.0.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fchownr%2F-%2Fchownr-3.0.0.tgz" checksum: 10c0/43925b87700f7e3893296c8e9c56cc58f926411cce3a6e5898136daaf08f08b9a8eb76d37d3267e707d0dcc17aed2e2ebdf5848c0c3ce95cf910a919935c1b10 languageName: node linkType: hard "ci-info@npm:^4.0.0": version: 4.4.0 - resolution: "ci-info@npm:4.4.0" + resolution: "ci-info@npm:4.4.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fci-info%2F-%2Fci-info-4.4.0.tgz" checksum: 10c0/44156201545b8dde01aa8a09ee2fe9fc7a73b1bef9adbd4606c9f61c8caeeb73fb7a575c88b0443f7b4edb5ee45debaa59ed54ba5f99698339393ca01349eb3a languageName: node linkType: hard "cli-progress@npm:^3.12.0": version: 3.12.0 - resolution: "cli-progress@npm:3.12.0" + resolution: "cli-progress@npm:3.12.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fcli-progress%2F-%2Fcli-progress-3.12.0.tgz" dependencies: string-width: "npm:^4.2.3" checksum: 10c0/f464cb19ebde2f3880620a2adfaeeefaec6cb15c8e610c8a659ca1047ee90d69f3bf2fdabbb1fe33ac408678e882e3e0eecdb84ab5df0edf930b269b8a72682d @@ -3208,14 +3509,14 @@ __metadata: "cli-width@npm:^4.1.0": version: 4.1.0 - resolution: "cli-width@npm:4.1.0" + resolution: "cli-width@npm:4.1.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fcli-width%2F-%2Fcli-width-4.1.0.tgz" checksum: 10c0/1fbd56413578f6117abcaf858903ba1f4ad78370a4032f916745fa2c7e390183a9d9029cf837df320b0fdce8137668e522f60a30a5f3d6529ff3872d265a955f languageName: node linkType: hard "clipanion@npm:^4.0.0-rc.2, clipanion@npm:^4.0.0-rc.4": version: 4.0.0-rc.4 - resolution: "clipanion@npm:4.0.0-rc.4" + resolution: "clipanion@npm:4.0.0-rc.4::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fclipanion%2F-%2Fclipanion-4.0.0-rc.4.tgz" dependencies: typanion: "npm:^3.8.0" peerDependencies: @@ -3226,7 +3527,7 @@ __metadata: "clone-response@npm:^1.0.2": version: 1.0.3 - resolution: "clone-response@npm:1.0.3" + resolution: "clone-response@npm:1.0.3::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fclone-response%2F-%2Fclone-response-1.0.3.tgz" dependencies: mimic-response: "npm:^1.0.0" checksum: 10c0/06a2b611824efb128810708baee3bd169ec9a1bf5976a5258cd7eb3f7db25f00166c6eee5961f075c7e38e194f373d4fdf86b8166ad5b9c7e82bbd2e333a6087 @@ -3235,7 +3536,7 @@ __metadata: "color-convert@npm:^2.0.1": version: 2.0.1 - resolution: "color-convert@npm:2.0.1" + resolution: "color-convert@npm:2.0.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fcolor-convert%2F-%2Fcolor-convert-2.0.1.tgz" dependencies: color-name: "npm:~1.1.4" checksum: 10c0/37e1150172f2e311fe1b2df62c6293a342ee7380da7b9cfdba67ea539909afbd74da27033208d01d6d5cfc65ee7868a22e18d7e7648e004425441c0f8a15a7d7 @@ -3244,28 +3545,35 @@ __metadata: "color-name@npm:~1.1.4": version: 1.1.4 - resolution: "color-name@npm:1.1.4" + resolution: "color-name@npm:1.1.4::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fcolor-name%2F-%2Fcolor-name-1.1.4.tgz" checksum: 10c0/a1a3f914156960902f46f7f56bc62effc6c94e84b2cae157a526b1c1f74b677a47ec602bf68a61abfa2b42d15b7c5651c6dbe72a43af720bc588dff885b10f95 languageName: node linkType: hard "colorette@npm:^2.0.20": version: 2.0.20 - resolution: "colorette@npm:2.0.20" + resolution: "colorette@npm:2.0.20::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fcolorette%2F-%2Fcolorette-2.0.20.tgz" checksum: 10c0/e94116ff33b0ff56f3b83b9ace895e5bf87c2a7a47b3401b8c3f3226e050d5ef76cf4072fb3325f9dc24d1698f9b730baf4e05eeaf861d74a1883073f4c98a40 languageName: node linkType: hard "content-type@npm:^2.0.0": version: 2.0.0 - resolution: "content-type@npm:2.0.0" + resolution: "content-type@npm:2.0.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fcontent-type%2F-%2Fcontent-type-2.0.0.tgz" checksum: 10c0/491539fff707d7594b0ca4fabcc084bef2a31ffa754ff0a4f80c4377e3963cff0394317f9271c24087596c97fa675bc123d61fa34ffe65b4904e7d3d3098de72 languageName: node linkType: hard +"convert-source-map@npm:^2.0.0": + version: 2.0.0 + resolution: "convert-source-map@npm:2.0.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fconvert-source-map%2F-%2Fconvert-source-map-2.0.0.tgz" + checksum: 10c0/8f2f7a27a1a011cc6cc88cc4da2d7d0cfa5ee0369508baae3d98c260bb3ac520691464e5bbe4ae7cdf09860c1d69ecc6f70c63c6e7c7f7e3f18ec08484dc7d9b + languageName: node + linkType: hard + "cross-spawn@npm:^7.0.3": version: 7.0.6 - resolution: "cross-spawn@npm:7.0.6" + resolution: "cross-spawn@npm:7.0.6::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fcross-spawn%2F-%2Fcross-spawn-7.0.6.tgz" dependencies: path-key: "npm:^3.1.0" shebang-command: "npm:^2.0.0" @@ -3276,7 +3584,7 @@ __metadata: "debug@npm:^4.4.1": version: 4.4.3 - resolution: "debug@npm:4.4.3" + resolution: "debug@npm:4.4.3::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fdebug%2F-%2Fdebug-4.4.3.tgz" dependencies: ms: "npm:^2.1.3" peerDependenciesMeta: @@ -3288,7 +3596,7 @@ __metadata: "decompress-response@npm:^6.0.0": version: 6.0.0 - resolution: "decompress-response@npm:6.0.0" + resolution: "decompress-response@npm:6.0.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fdecompress-response%2F-%2Fdecompress-response-6.0.0.tgz" dependencies: mimic-response: "npm:^3.1.0" checksum: 10c0/bd89d23141b96d80577e70c54fb226b2f40e74a6817652b80a116d7befb8758261ad073a8895648a29cc0a5947021ab66705cb542fa9c143c82022b27c5b175e @@ -3297,35 +3605,49 @@ __metadata: "defer-to-connect@npm:^2.0.0": version: 2.0.1 - resolution: "defer-to-connect@npm:2.0.1" + resolution: "defer-to-connect@npm:2.0.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fdefer-to-connect%2F-%2Fdefer-to-connect-2.0.1.tgz" checksum: 10c0/625ce28e1b5ad10cf77057b9a6a727bf84780c17660f6644dab61dd34c23de3001f03cedc401f7d30a4ed9965c2e8a7336e220a329146f2cf85d4eddea429782 languageName: node linkType: hard +"dequal@npm:^2.0.3": + version: 2.0.3 + resolution: "dequal@npm:2.0.3::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fdequal%2F-%2Fdequal-2.0.3.tgz" + checksum: 10c0/f98860cdf58b64991ae10205137c0e97d384c3a4edc7f807603887b7c4b850af1224a33d88012009f150861cbee4fa2d322c4cc04b9313bee312e47f6ecaa888 + languageName: node + linkType: hard + "detect-libc@npm:^2.0.3": version: 2.1.2 - resolution: "detect-libc@npm:2.1.2" + resolution: "detect-libc@npm:2.1.2::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fdetect-libc%2F-%2Fdetect-libc-2.1.2.tgz" checksum: 10c0/acc675c29a5649fa1fb6e255f993b8ee829e510b6b56b0910666949c80c364738833417d0edb5f90e4e46be17228b0f2b66a010513984e18b15deeeac49369c4 languageName: node linkType: hard "diff@npm:^5.1.0": version: 5.2.2 - resolution: "diff@npm:5.2.2" + resolution: "diff@npm:5.2.2::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fdiff%2F-%2Fdiff-5.2.2.tgz" checksum: 10c0/52da594c54e9033423da26984b1449ae6accd782d5afc4431c9a192a8507ddc83120fe8f925d7220b9da5b5963c7b6f5e46add3660a00cb36df7a13420a09d4b languageName: node linkType: hard +"dom-accessibility-api@npm:^0.5.9": + version: 0.5.16 + resolution: "dom-accessibility-api@npm:0.5.16::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fdom-accessibility-api%2F-%2Fdom-accessibility-api-0.5.16.tgz" + checksum: 10c0/b2c2eda4fae568977cdac27a9f0c001edf4f95a6a6191dfa611e3721db2478d1badc01db5bb4fa8a848aeee13e442a6c2a4386d65ec65a1436f24715a2f8d053 + languageName: node + linkType: hard + "dotenv@npm:^16.3.1": version: 16.6.1 - resolution: "dotenv@npm:16.6.1" + resolution: "dotenv@npm:16.6.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fdotenv%2F-%2Fdotenv-16.6.1.tgz" checksum: 10c0/15ce56608326ea0d1d9414a5c8ee6dcf0fffc79d2c16422b4ac2268e7e2d76ff5a572d37ffe747c377de12005f14b3cc22361e79fc7f1061cce81f77d2c973dc languageName: node linkType: hard "emnapi@npm:^1.11.1": version: 1.11.1 - resolution: "emnapi@npm:1.11.1" + resolution: "emnapi@npm:1.11.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Femnapi%2F-%2Femnapi-1.11.1.tgz" peerDependencies: node-addon-api: ">= 6.1.0" peerDependenciesMeta: @@ -3337,14 +3659,14 @@ __metadata: "emoji-regex@npm:^8.0.0": version: 8.0.0 - resolution: "emoji-regex@npm:8.0.0" + resolution: "emoji-regex@npm:8.0.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Femoji-regex%2F-%2Femoji-regex-8.0.0.tgz" checksum: 10c0/b6053ad39951c4cf338f9092d7bfba448cdfd46fe6a2a034700b149ac9ffbc137e361cbd3c442297f86bed2e5f7576c1b54cc0a6bf8ef5106cc62f496af35010 languageName: node linkType: hard "end-of-stream@npm:^1.1.0": version: 1.4.5 - resolution: "end-of-stream@npm:1.4.5" + resolution: "end-of-stream@npm:1.4.5::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fend-of-stream%2F-%2Fend-of-stream-1.4.5.tgz" dependencies: once: "npm:^1.4.0" checksum: 10c0/b0701c92a10b89afb1cb45bf54a5292c6f008d744eb4382fa559d54775ff31617d1d7bc3ef617575f552e24fad2c7c1a1835948c66b3f3a4be0a6c1f35c883d8 @@ -3353,21 +3675,21 @@ __metadata: "env-paths@npm:^2.2.0": version: 2.2.1 - resolution: "env-paths@npm:2.2.1" + resolution: "env-paths@npm:2.2.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fenv-paths%2F-%2Fenv-paths-2.2.1.tgz" checksum: 10c0/285325677bf00e30845e330eec32894f5105529db97496ee3f598478e50f008c5352a41a30e5e72ec9de8a542b5a570b85699cd63bd2bc646dbcb9f311d83bc4 languageName: node linkType: hard -"es-module-lexer@npm:^1.7.0": - version: 1.7.0 - resolution: "es-module-lexer@npm:1.7.0" - checksum: 10c0/4c935affcbfeba7fb4533e1da10fa8568043df1e3574b869385980de9e2d475ddc36769891936dbb07036edb3c3786a8b78ccf44964cd130dedc1f2c984b6c7b +"es-module-lexer@npm:^2.0.0": + version: 2.3.0 + resolution: "es-module-lexer@npm:2.3.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fes-module-lexer%2F-%2Fes-module-lexer-2.3.0.tgz" + checksum: 10c0/14b28d854ec2aec285c481daeea268451e01ab2813f09f98eb1ac432521d8c3b38ce552a49f2e4af3ba188b74acbafe9e8d7271ce912bd98f1651b02bd06717e languageName: node linkType: hard "es-toolkit@npm:^1.39.7, es-toolkit@npm:^1.47.0": - version: 1.48.1 - resolution: "es-toolkit@npm:1.48.1" + version: 1.49.0 + resolution: "es-toolkit@npm:1.49.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fes-toolkit%2F-%2Fes-toolkit-1.49.0.tgz" dependenciesMeta: "@trivago/prettier-plugin-sort-imports@4.3.0": unplugged: true @@ -3375,13 +3697,13 @@ __metadata: unplugged: true vitepress-plugin-sandpack@1.1.4: unplugged: true - checksum: 10c0/29bfccb8aaf088f27ea91b5bf6448b8a46ef5d809139ad9335bea6919d0bc3aadc3206c957ab6c72d65864906fd75cf9ab41fbd936aaf69e58c6259d2821187d + checksum: 10c0/ab0864bb0c5c494ed09043d53a74b58a0ab9df1b89b9b54bce72a63de2869152d7d65e489dc89ab671f0fea243728922c8a8f8c873f1b2a53350a23360b3e2df languageName: node linkType: hard "esprima@npm:^4.0.0": version: 4.0.1 - resolution: "esprima@npm:4.0.1" + resolution: "esprima@npm:4.0.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fesprima%2F-%2Fesprima-4.0.1.tgz" bin: esparse: ./bin/esparse.js esvalidate: ./bin/esvalidate.js @@ -3389,9 +3711,18 @@ __metadata: languageName: node linkType: hard +"estree-walker@npm:^3.0.3": + version: 3.0.3 + resolution: "estree-walker@npm:3.0.3::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Festree-walker%2F-%2Festree-walker-3.0.3.tgz" + dependencies: + "@types/estree": "npm:^1.0.0" + checksum: 10c0/c12e3c2b2642d2bcae7d5aa495c60fa2f299160946535763969a1c83fc74518ffa9c2cd3a8b69ac56aea547df6a8aac25f729a342992ef0bbac5f1c73e78995d + languageName: node + linkType: hard + "eventsource-parser@npm:^3.0.8": version: 3.1.0 - resolution: "eventsource-parser@npm:3.1.0" + resolution: "eventsource-parser@npm:3.1.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Feventsource-parser%2F-%2Feventsource-parser-3.1.0.tgz" checksum: 10c0/5ab4c6c9a2a042be0b387b6d03810eb580bac4ce90e299ede56458125a97ffe3af8145b2740089fc898a96cfa5aae792ee79f2a06257fba2776b0e7bce037071 languageName: node linkType: hard @@ -3411,16 +3742,23 @@ __metadata: languageName: unknown linkType: soft +"expect-type@npm:^1.3.0": + version: 1.4.0 + resolution: "expect-type@npm:1.4.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fexpect-type%2F-%2Fexpect-type-1.4.0.tgz" + checksum: 10c0/d40d76b8570695d36587beb3cc28494da2ca3ec8f04e67f5622ed2d372d850e401a9adef19c6835e1a8173903f157c79540b34c7b3fbd7cd8ce726cc903c57b7 + languageName: node + linkType: hard + "exponential-backoff@npm:^3.1.1": version: 3.1.3 - resolution: "exponential-backoff@npm:3.1.3" + resolution: "exponential-backoff@npm:3.1.3::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fexponential-backoff%2F-%2Fexponential-backoff-3.1.3.tgz" checksum: 10c0/77e3ae682b7b1f4972f563c6dbcd2b0d54ac679e62d5d32f3e5085feba20483cf28bd505543f520e287a56d4d55a28d7874299941faf637e779a1aa5994d1267 languageName: node linkType: hard "fast-glob@npm:^3.2.2": version: 3.3.3 - resolution: "fast-glob@npm:3.3.3" + resolution: "fast-glob@npm:3.3.3::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Ffast-glob%2F-%2Ffast-glob-3.3.3.tgz" dependencies: "@nodelib/fs.stat": "npm:^2.0.2" "@nodelib/fs.walk": "npm:^1.2.3" @@ -3433,14 +3771,14 @@ __metadata: "fast-string-truncated-width@npm:^3.0.2": version: 3.0.3 - resolution: "fast-string-truncated-width@npm:3.0.3" + resolution: "fast-string-truncated-width@npm:3.0.3::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Ffast-string-truncated-width%2F-%2Ffast-string-truncated-width-3.0.3.tgz" checksum: 10c0/043b8663397d14a3880ce4f3407bcda60b40db9bbeafe62863a35d1f9c69ea17c8da3fcd72de235553e6c9cd053128cde9e24ca0d4a7463208f48db3cd23d981 languageName: node linkType: hard "fast-string-width@npm:^3.0.2": version: 3.0.2 - resolution: "fast-string-width@npm:3.0.2" + resolution: "fast-string-width@npm:3.0.2::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Ffast-string-width%2F-%2Ffast-string-width-3.0.2.tgz" dependencies: fast-string-truncated-width: "npm:^3.0.2" checksum: 10c0/c8822d175315bb353ebe782b65214ac53b13e3bf704e03b132ea7bdfa8de6a636375b3ab7a4097545393d109381c37c4f387c72a462c90b61412dbc4632f39a7 @@ -3449,7 +3787,7 @@ __metadata: "fast-wrap-ansi@npm:^0.2.0": version: 0.2.2 - resolution: "fast-wrap-ansi@npm:0.2.2" + resolution: "fast-wrap-ansi@npm:0.2.2::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Ffast-wrap-ansi%2F-%2Ffast-wrap-ansi-0.2.2.tgz" dependencies: fast-string-width: "npm:^3.0.2" checksum: 10c0/1aa7be4f7cb86f4bdb14691cb6bcc0b8df8b3b89df142ade3ae1602332dcf6f990cd750a923cd581ca0847808cb4ec1aa5afaafa7a72f849e87a2a62c98fa370 @@ -3458,7 +3796,7 @@ __metadata: "fastq@npm:^1.6.0": version: 1.20.1 - resolution: "fastq@npm:1.20.1" + resolution: "fastq@npm:1.20.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Ffastq%2F-%2Ffastq-1.20.1.tgz" dependencies: reusify: "npm:^1.0.4" checksum: 10c0/e5dd725884decb1f11e5c822221d76136f239d0236f176fab80b7b8f9e7619ae57e6b4e5b73defc21e6b9ef99437ee7b545cff8e6c2c337819633712fa9d352e @@ -3467,7 +3805,7 @@ __metadata: "fdir@npm:^6.5.0": version: 6.5.0 - resolution: "fdir@npm:6.5.0" + resolution: "fdir@npm:6.5.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Ffdir%2F-%2Ffdir-6.5.0.tgz" peerDependencies: picomatch: ^3 || ^4 peerDependenciesMeta: @@ -3479,7 +3817,7 @@ __metadata: "fill-range@npm:^7.1.1": version: 7.1.1 - resolution: "fill-range@npm:7.1.1" + resolution: "fill-range@npm:7.1.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Ffill-range%2F-%2Ffill-range-7.1.1.tgz" dependencies: to-regex-range: "npm:^5.0.1" checksum: 10c0/b75b691bbe065472f38824f694c2f7449d7f5004aa950426a2c28f0306c60db9b880c0b0e4ed819997ffb882d1da02cfcfc819bddc94d71627f5269682edf018 @@ -3488,7 +3826,7 @@ __metadata: "fsevents@npm:~2.3.3": version: 2.3.3 - resolution: "fsevents@npm:2.3.3" + resolution: "fsevents@npm:2.3.3::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Ffsevents%2F-%2Ffsevents-2.3.3.tgz" dependencies: node-gyp: "npm:latest" checksum: 10c0/a1f0c44595123ed717febbc478aa952e47adfc28e2092be66b8ab1635147254ca6cfe1df792a8997f22716d4cbafc73309899ff7bfac2ac3ad8cf2e4ecc3ec60 @@ -3498,7 +3836,7 @@ __metadata: "fsevents@patch:fsevents@npm%3A~2.3.3#optional!builtin": version: 2.3.3 - resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin::version=2.3.3&hash=df0bf1" + resolution: "fsevents@patch:fsevents@npm%3A2.3.3%3A%3A__archiveUrl=https%253A%252F%252Fregistry.npmjs.org%252Ffsevents%252F-%252Ffsevents-2.3.3.tgz#optional!builtin::version=2.3.3&hash=df0bf1" dependencies: node-gyp: "npm:latest" conditions: os=darwin @@ -3507,14 +3845,14 @@ __metadata: "gearhash-jit@npm:1.0.2": version: 1.0.2 - resolution: "gearhash-jit@npm:1.0.2" + resolution: "gearhash-jit@npm:1.0.2::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fgearhash-jit%2F-%2Fgearhash-jit-1.0.2.tgz" checksum: 10c0/e0bad0f1fc8b89e881b2c1074685a63d81ee2441b42263d66c549c69cd9d068742d7198ccbe14a9eee0134249c90d2f7ab1c9c6f3068e8677a6417ac8c6b57b6 languageName: node linkType: hard "get-stream@npm:^5.1.0": version: 5.2.0 - resolution: "get-stream@npm:5.2.0" + resolution: "get-stream@npm:5.2.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fget-stream%2F-%2Fget-stream-5.2.0.tgz" dependencies: pump: "npm:^3.0.0" checksum: 10c0/43797ffd815fbb26685bf188c8cfebecb8af87b3925091dd7b9a9c915993293d78e3c9e1bce125928ff92f2d0796f3889b92b5ec6d58d1041b574682132e0a80 @@ -3523,7 +3861,7 @@ __metadata: "glob-parent@npm:^5.1.2": version: 5.1.2 - resolution: "glob-parent@npm:5.1.2" + resolution: "glob-parent@npm:5.1.2::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fglob-parent%2F-%2Fglob-parent-5.1.2.tgz" dependencies: is-glob: "npm:^4.0.1" checksum: 10c0/cab87638e2112bee3f839ef5f6e0765057163d39c66be8ec1602f3823da4692297ad4e972de876ea17c44d652978638d2fd583c6713d0eb6591706825020c9ee @@ -3532,7 +3870,7 @@ __metadata: "got@npm:^11.7.0": version: 11.8.6 - resolution: "got@npm:11.8.6" + resolution: "got@npm:11.8.6::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fgot%2F-%2Fgot-11.8.6.tgz" dependencies: "@sindresorhus/is": "npm:^4.0.0" "@szmarczak/http-timer": "npm:^4.0.5" @@ -3551,42 +3889,42 @@ __metadata: "graceful-fs@npm:^4.2.6": version: 4.2.11 - resolution: "graceful-fs@npm:4.2.11" + resolution: "graceful-fs@npm:4.2.11::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fgraceful-fs%2F-%2Fgraceful-fs-4.2.11.tgz" checksum: 10c0/386d011a553e02bc594ac2ca0bd6d9e4c22d7fa8cfbfc448a6d148c59ea881b092db9dbe3547ae4b88e55f1b01f7c4a2ecc53b310c042793e63aa44cf6c257f2 languageName: node linkType: hard "grapheme-splitter@npm:^1.0.4": version: 1.0.4 - resolution: "grapheme-splitter@npm:1.0.4" + resolution: "grapheme-splitter@npm:1.0.4::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fgrapheme-splitter%2F-%2Fgrapheme-splitter-1.0.4.tgz" checksum: 10c0/108415fb07ac913f17040dc336607772fcea68c7f495ef91887edddb0b0f5ff7bc1d1ab181b125ecb2f0505669ef12c9a178a3bbd2dd8e042d8c5f1d7c90331a languageName: node linkType: hard "has-flag@npm:^4.0.0": version: 4.0.0 - resolution: "has-flag@npm:4.0.0" + resolution: "has-flag@npm:4.0.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fhas-flag%2F-%2Fhas-flag-4.0.0.tgz" checksum: 10c0/2e789c61b7888d66993e14e8331449e525ef42aac53c627cc53d1c3334e768bcb6abdc4f5f0de1478a25beec6f0bd62c7549058b7ac53e924040d4f301f02fd1 languageName: node linkType: hard "hpagent@npm:^1.2.0": version: 1.2.0 - resolution: "hpagent@npm:1.2.0" + resolution: "hpagent@npm:1.2.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fhpagent%2F-%2Fhpagent-1.2.0.tgz" checksum: 10c0/505ef42e5e067dba701ea21e7df9fa73f6f5080e59d53680829827d34cd7040f1ecf7c3c8391abe9df4eb4682ef4a4321608836b5b70a61b88c1b3a03d77510b languageName: node linkType: hard "http-cache-semantics@npm:^4.0.0": version: 4.2.0 - resolution: "http-cache-semantics@npm:4.2.0" + resolution: "http-cache-semantics@npm:4.2.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fhttp-cache-semantics%2F-%2Fhttp-cache-semantics-4.2.0.tgz" checksum: 10c0/45b66a945cf13ec2d1f29432277201313babf4a01d9e52f44b31ca923434083afeca03f18417f599c9ab3d0e7b618ceb21257542338b57c54b710463b4a53e37 languageName: node linkType: hard "http2-wrapper@npm:^1.0.0-beta.5.2": version: 1.0.3 - resolution: "http2-wrapper@npm:1.0.3" + resolution: "http2-wrapper@npm:1.0.3::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fhttp2-wrapper%2F-%2Fhttp2-wrapper-1.0.3.tgz" dependencies: quick-lru: "npm:^5.1.1" resolve-alpn: "npm:^1.0.0" @@ -3596,7 +3934,7 @@ __metadata: "iconv-lite@npm:^0.7.2": version: 0.7.2 - resolution: "iconv-lite@npm:0.7.2" + resolution: "iconv-lite@npm:0.7.2::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Ficonv-lite%2F-%2Ficonv-lite-0.7.2.tgz" dependencies: safer-buffer: "npm:>= 2.1.2 < 3.0.0" checksum: 10c0/3c228920f3bd307f56bf8363706a776f4a060eb042f131cd23855ceca962951b264d0997ab38a1ad340e1c5df8499ed26e1f4f0db6b2a2ad9befaff22f14b722 @@ -3605,21 +3943,21 @@ __metadata: "is-extglob@npm:^2.1.1": version: 2.1.1 - resolution: "is-extglob@npm:2.1.1" + resolution: "is-extglob@npm:2.1.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fis-extglob%2F-%2Fis-extglob-2.1.1.tgz" checksum: 10c0/5487da35691fbc339700bbb2730430b07777a3c21b9ebaecb3072512dfd7b4ba78ac2381a87e8d78d20ea08affb3f1971b4af629173a6bf435ff8a4c47747912 languageName: node linkType: hard "is-fullwidth-code-point@npm:^3.0.0": version: 3.0.0 - resolution: "is-fullwidth-code-point@npm:3.0.0" + resolution: "is-fullwidth-code-point@npm:3.0.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fis-fullwidth-code-point%2F-%2Fis-fullwidth-code-point-3.0.0.tgz" checksum: 10c0/bb11d825e049f38e04c06373a8d72782eee0205bda9d908cc550ccb3c59b99d750ff9537982e01733c1c94a58e35400661f57042158ff5e8f3e90cf936daf0fc languageName: node linkType: hard "is-glob@npm:^4.0.1": version: 4.0.3 - resolution: "is-glob@npm:4.0.3" + resolution: "is-glob@npm:4.0.3::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fis-glob%2F-%2Fis-glob-4.0.3.tgz" dependencies: is-extglob: "npm:^2.1.1" checksum: 10c0/17fb4014e22be3bbecea9b2e3a76e9e34ff645466be702f1693e8f1ee1adac84710d0be0bd9f967d6354036fd51ab7c2741d954d6e91dae6bb69714de92c197a @@ -3628,72 +3966,79 @@ __metadata: "is-number@npm:^7.0.0": version: 7.0.0 - resolution: "is-number@npm:7.0.0" + resolution: "is-number@npm:7.0.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fis-number%2F-%2Fis-number-7.0.0.tgz" checksum: 10c0/b4686d0d3053146095ccd45346461bc8e53b80aeb7671cc52a4de02dbbf7dc0d1d2a986e2fe4ae206984b4d34ef37e8b795ebc4f4295c978373e6575e295d811 languageName: node linkType: hard "isexe@npm:^2.0.0": version: 2.0.0 - resolution: "isexe@npm:2.0.0" + resolution: "isexe@npm:2.0.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fisexe%2F-%2Fisexe-2.0.0.tgz" checksum: 10c0/228cfa503fadc2c31596ab06ed6aa82c9976eec2bfd83397e7eaf06d0ccf42cd1dfd6743bf9aeb01aebd4156d009994c5f76ea898d2832c1fe342da923ca457d languageName: node linkType: hard "isexe@npm:^4.0.0": version: 4.0.0 - resolution: "isexe@npm:4.0.0" + resolution: "isexe@npm:4.0.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fisexe%2F-%2Fisexe-4.0.0.tgz" checksum: 10c0/5884815115bceac452877659a9c7726382531592f43dc29e5d48b7c4100661aed54018cb90bd36cb2eaeba521092570769167acbb95c18d39afdccbcca06c5ce languageName: node linkType: hard +"js-tokens@npm:^4.0.0": + version: 4.0.0 + resolution: "js-tokens@npm:4.0.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fjs-tokens%2F-%2Fjs-tokens-4.0.0.tgz" + checksum: 10c0/e248708d377aa058eacf2037b07ded847790e6de892bbad3dac0abba2e759cb9f121b00099a65195616badcb6eca8d14d975cb3e89eb1cfda644756402c8aeed + languageName: node + linkType: hard + "js-yaml@npm:^3.10.0": - version: 3.14.2 - resolution: "js-yaml@npm:3.14.2" + version: 3.15.0 + resolution: "js-yaml@npm:3.15.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fjs-yaml%2F-%2Fjs-yaml-3.15.0.tgz" dependencies: argparse: "npm:^1.0.7" esprima: "npm:^4.0.0" bin: js-yaml: bin/js-yaml.js - checksum: 10c0/3261f25912f5dd76605e5993d0a126c2b6c346311885d3c483706cd722efe34f697ea0331f654ce27c00a42b426e524518ec89d65ed02ea47df8ad26dcc8ce69 + checksum: 10c0/ca966bd354ac5b1b7a4694ebdba46526796aa3a6a99529fa540af2abf85918bd155a50ccc0166b413130a00622999973754458ec01e7095bc902177bfdbd5b64 languageName: node linkType: hard "js-yaml@npm:^4.2.0": - version: 4.2.0 - resolution: "js-yaml@npm:4.2.0" + version: 4.3.0 + resolution: "js-yaml@npm:4.3.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fjs-yaml%2F-%2Fjs-yaml-4.3.0.tgz" dependencies: argparse: "npm:^2.0.1" bin: js-yaml: bin/js-yaml.js - checksum: 10c0/1916456c118746603b067d74bbcbb0445d9a1d5e474ad4ae775e7b20525bed902e01d9d97dd0c81fcd8d4f596162309d0eb057f4aa38f3e9647f14075e9dea45 + checksum: 10c0/058b30473d6915ca5b4feb11e2f7d4d97242f98d00a798ed48dd90b46b7c640398afe9128c5db22c5300f8c6528fe2a174b9a93f351a70ebc28c6203938d8bff languageName: node linkType: hard "json-buffer@npm:3.0.1": version: 3.0.1 - resolution: "json-buffer@npm:3.0.1" + resolution: "json-buffer@npm:3.0.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fjson-buffer%2F-%2Fjson-buffer-3.0.1.tgz" checksum: 10c0/0d1c91569d9588e7eef2b49b59851f297f3ab93c7b35c7c221e288099322be6b562767d11e4821da500f3219542b9afd2e54c5dc573107c1126ed1080f8e96d7 languageName: node linkType: hard "json-schema@npm:^0.4.0": version: 0.4.0 - resolution: "json-schema@npm:0.4.0" + resolution: "json-schema@npm:0.4.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fjson-schema%2F-%2Fjson-schema-0.4.0.tgz" checksum: 10c0/d4a637ec1d83544857c1c163232f3da46912e971d5bf054ba44fdb88f07d8d359a462b4aec46f2745efbc57053365608d88bc1d7b1729f7b4fc3369765639ed3 languageName: node linkType: hard "json-with-bigint@npm:^3.5.3": version: 3.5.8 - resolution: "json-with-bigint@npm:3.5.8" + resolution: "json-with-bigint@npm:3.5.8::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fjson-with-bigint%2F-%2Fjson-with-bigint-3.5.8.tgz" checksum: 10c0/a0c4e37626d74a9a493539f9f9a94855933fa15ea2f028859a787229a42c5f11803db6f94f1ce7b1d89756c1e80a7c1f11006bac266ec7ce819b75701765ca0a languageName: node linkType: hard "keyv@npm:^4.0.0": version: 4.5.4 - resolution: "keyv@npm:4.5.4" + resolution: "keyv@npm:4.5.4::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fkeyv%2F-%2Fkeyv-4.5.4.tgz" dependencies: json-buffer: "npm:3.0.1" checksum: 10c0/aa52f3c5e18e16bb6324876bb8b59dd02acf782a4b789c7b2ae21107fab95fab3890ed448d4f8dba80ce05391eeac4bfabb4f02a20221342982f806fa2cf271e @@ -3702,84 +4047,84 @@ __metadata: "lightningcss-android-arm64@npm:1.32.0": version: 1.32.0 - resolution: "lightningcss-android-arm64@npm:1.32.0" + resolution: "lightningcss-android-arm64@npm:1.32.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Flightningcss-android-arm64%2F-%2Flightningcss-android-arm64-1.32.0.tgz" conditions: os=android & cpu=arm64 languageName: node linkType: hard "lightningcss-darwin-arm64@npm:1.32.0": version: 1.32.0 - resolution: "lightningcss-darwin-arm64@npm:1.32.0" + resolution: "lightningcss-darwin-arm64@npm:1.32.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Flightningcss-darwin-arm64%2F-%2Flightningcss-darwin-arm64-1.32.0.tgz" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard "lightningcss-darwin-x64@npm:1.32.0": version: 1.32.0 - resolution: "lightningcss-darwin-x64@npm:1.32.0" + resolution: "lightningcss-darwin-x64@npm:1.32.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Flightningcss-darwin-x64%2F-%2Flightningcss-darwin-x64-1.32.0.tgz" conditions: os=darwin & cpu=x64 languageName: node linkType: hard "lightningcss-freebsd-x64@npm:1.32.0": version: 1.32.0 - resolution: "lightningcss-freebsd-x64@npm:1.32.0" + resolution: "lightningcss-freebsd-x64@npm:1.32.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Flightningcss-freebsd-x64%2F-%2Flightningcss-freebsd-x64-1.32.0.tgz" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard "lightningcss-linux-arm-gnueabihf@npm:1.32.0": version: 1.32.0 - resolution: "lightningcss-linux-arm-gnueabihf@npm:1.32.0" + resolution: "lightningcss-linux-arm-gnueabihf@npm:1.32.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Flightningcss-linux-arm-gnueabihf%2F-%2Flightningcss-linux-arm-gnueabihf-1.32.0.tgz" conditions: os=linux & cpu=arm languageName: node linkType: hard "lightningcss-linux-arm64-gnu@npm:1.32.0": version: 1.32.0 - resolution: "lightningcss-linux-arm64-gnu@npm:1.32.0" + resolution: "lightningcss-linux-arm64-gnu@npm:1.32.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Flightningcss-linux-arm64-gnu%2F-%2Flightningcss-linux-arm64-gnu-1.32.0.tgz" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard "lightningcss-linux-arm64-musl@npm:1.32.0": version: 1.32.0 - resolution: "lightningcss-linux-arm64-musl@npm:1.32.0" + resolution: "lightningcss-linux-arm64-musl@npm:1.32.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Flightningcss-linux-arm64-musl%2F-%2Flightningcss-linux-arm64-musl-1.32.0.tgz" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard "lightningcss-linux-x64-gnu@npm:1.32.0": version: 1.32.0 - resolution: "lightningcss-linux-x64-gnu@npm:1.32.0" + resolution: "lightningcss-linux-x64-gnu@npm:1.32.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Flightningcss-linux-x64-gnu%2F-%2Flightningcss-linux-x64-gnu-1.32.0.tgz" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard "lightningcss-linux-x64-musl@npm:1.32.0": version: 1.32.0 - resolution: "lightningcss-linux-x64-musl@npm:1.32.0" + resolution: "lightningcss-linux-x64-musl@npm:1.32.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Flightningcss-linux-x64-musl%2F-%2Flightningcss-linux-x64-musl-1.32.0.tgz" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard "lightningcss-win32-arm64-msvc@npm:1.32.0": version: 1.32.0 - resolution: "lightningcss-win32-arm64-msvc@npm:1.32.0" + resolution: "lightningcss-win32-arm64-msvc@npm:1.32.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Flightningcss-win32-arm64-msvc%2F-%2Flightningcss-win32-arm64-msvc-1.32.0.tgz" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard "lightningcss-win32-x64-msvc@npm:1.32.0": version: 1.32.0 - resolution: "lightningcss-win32-x64-msvc@npm:1.32.0" + resolution: "lightningcss-win32-x64-msvc@npm:1.32.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Flightningcss-win32-x64-msvc%2F-%2Flightningcss-win32-x64-msvc-1.32.0.tgz" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"lightningcss@npm:^1.30.2": +"lightningcss@npm:^1.32.0": version: 1.32.0 - resolution: "lightningcss@npm:1.32.0" + resolution: "lightningcss@npm:1.32.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Flightningcss%2F-%2Flightningcss-1.32.0.tgz" dependencies: detect-libc: "npm:^2.0.3" lightningcss-android-arm64: "npm:1.32.0" @@ -3822,21 +4167,39 @@ __metadata: "lowercase-keys@npm:^2.0.0": version: 2.0.0 - resolution: "lowercase-keys@npm:2.0.0" + resolution: "lowercase-keys@npm:2.0.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Flowercase-keys%2F-%2Flowercase-keys-2.0.0.tgz" checksum: 10c0/f82a2b3568910509da4b7906362efa40f5b54ea14c2584778ddb313226f9cbf21020a5db35f9b9a0e95847a9b781d548601f31793d736b22a2b8ae8eb9ab1082 languageName: node linkType: hard +"lz-string@npm:^1.5.0": + version: 1.5.0 + resolution: "lz-string@npm:1.5.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Flz-string%2F-%2Flz-string-1.5.0.tgz" + bin: + lz-string: bin/bin.js + checksum: 10c0/36128e4de34791838abe979b19927c26e67201ca5acf00880377af7d765b38d1c60847e01c5ec61b1a260c48029084ab3893a3925fd6e48a04011364b089991b + languageName: node + linkType: hard + +"magic-string@npm:^0.30.21": + version: 0.30.21 + resolution: "magic-string@npm:0.30.21::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fmagic-string%2F-%2Fmagic-string-0.30.21.tgz" + dependencies: + "@jridgewell/sourcemap-codec": "npm:^1.5.5" + checksum: 10c0/299378e38f9a270069fc62358522ddfb44e94244baa0d6a8980ab2a9b2490a1d03b236b447eee309e17eb3bddfa482c61259d47960eb018a904f0ded52780c4a + languageName: node + linkType: hard + "merge2@npm:^1.3.0": version: 1.4.1 - resolution: "merge2@npm:1.4.1" + resolution: "merge2@npm:1.4.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fmerge2%2F-%2Fmerge2-1.4.1.tgz" checksum: 10c0/254a8a4605b58f450308fc474c82ac9a094848081bf4c06778200207820e5193726dc563a0d2c16468810516a5c97d9d3ea0ca6585d23c58ccfff2403e8dbbeb languageName: node linkType: hard "micromatch@npm:^4.0.2, micromatch@npm:^4.0.8": version: 4.0.8 - resolution: "micromatch@npm:4.0.8" + resolution: "micromatch@npm:4.0.8::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fmicromatch%2F-%2Fmicromatch-4.0.8.tgz" dependencies: braces: "npm:^3.0.3" picomatch: "npm:^2.3.1" @@ -3846,28 +4209,28 @@ __metadata: "mimic-response@npm:^1.0.0": version: 1.0.1 - resolution: "mimic-response@npm:1.0.1" + resolution: "mimic-response@npm:1.0.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fmimic-response%2F-%2Fmimic-response-1.0.1.tgz" checksum: 10c0/c5381a5eae997f1c3b5e90ca7f209ed58c3615caeee850e85329c598f0c000ae7bec40196580eef1781c60c709f47258131dab237cad8786f8f56750594f27fa languageName: node linkType: hard "mimic-response@npm:^3.1.0": version: 3.1.0 - resolution: "mimic-response@npm:3.1.0" + resolution: "mimic-response@npm:3.1.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fmimic-response%2F-%2Fmimic-response-3.1.0.tgz" checksum: 10c0/0d6f07ce6e03e9e4445bee655202153bdb8a98d67ee8dc965ac140900d7a2688343e6b4c9a72cfc9ef2f7944dfd76eef4ab2482eb7b293a68b84916bac735362 languageName: node linkType: hard "minipass@npm:^7.0.4, minipass@npm:^7.1.2": version: 7.1.3 - resolution: "minipass@npm:7.1.3" + resolution: "minipass@npm:7.1.3::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fminipass%2F-%2Fminipass-7.1.3.tgz" checksum: 10c0/539da88daca16533211ea5a9ee98dc62ff5742f531f54640dd34429e621955e91cc280a91a776026264b7f9f6735947629f920944e9c1558369e8bf22eb33fbb languageName: node linkType: hard "minizlib@npm:^3.1.0": version: 3.1.0 - resolution: "minizlib@npm:3.1.0" + resolution: "minizlib@npm:3.1.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fminizlib%2F-%2Fminizlib-3.1.0.tgz" dependencies: minipass: "npm:^7.1.2" checksum: 10c0/5aad75ab0090b8266069c9aabe582c021ae53eb33c6c691054a13a45db3b4f91a7fb1bd79151e6b4e9e9a86727b522527c0a06ec7d45206b745d54cd3097bcec @@ -3896,35 +4259,35 @@ __metadata: oxc-parser: "npm:^0.137.0" prettier: "npm:^3.8.4" typescript: "npm:^6.0.3" - vite-plus: "npm:0.1.24" - vitest: "npm:^4.1.9" + vite-plus: "catalog:" + vitest: "catalog:" languageName: unknown linkType: soft "mrmime@npm:^2.0.0": version: 2.0.1 - resolution: "mrmime@npm:2.0.1" + resolution: "mrmime@npm:2.0.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fmrmime%2F-%2Fmrmime-2.0.1.tgz" checksum: 10c0/af05afd95af202fdd620422f976ad67dc18e6ee29beb03dd1ce950ea6ef664de378e44197246df4c7cdd73d47f2e7143a6e26e473084b9e4aa2095c0ad1e1761 languageName: node linkType: hard "ms@npm:^2.1.3": version: 2.1.3 - resolution: "ms@npm:2.1.3" + resolution: "ms@npm:2.1.3::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fms%2F-%2Fms-2.1.3.tgz" checksum: 10c0/d924b57e7312b3b63ad21fc5b3dc0af5e78d61a1fc7cfb5457edaf26326bf62be5307cc87ffb6862ef1c2b33b0233cdb5d4f01c4c958cc0d660948b65a287a48 languageName: node linkType: hard "mute-stream@npm:^3.0.0": version: 3.0.0 - resolution: "mute-stream@npm:3.0.0" + resolution: "mute-stream@npm:3.0.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fmute-stream%2F-%2Fmute-stream-3.0.0.tgz" checksum: 10c0/12cdb36a101694c7a6b296632e6d93a30b74401873cf7507c88861441a090c71c77a58f213acadad03bc0c8fa186639dec99d68a14497773a8744320c136e701 languageName: node linkType: hard "nanoid@npm:^3.3.12": version: 3.3.15 - resolution: "nanoid@npm:3.3.15" + resolution: "nanoid@npm:3.3.15::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fnanoid%2F-%2Fnanoid-3.3.15.tgz" bin: nanoid: bin/nanoid.cjs checksum: 10c0/e0b12e3a1d361f74150fa4b25631d0ae29f7162dab01a12f0f1be1f53b7a2a219f9b729504e474d4821207d0fe349bd3c97569ab5cf7ec2fff6aa94711956c93 @@ -3933,7 +4296,7 @@ __metadata: "node-gyp@npm:latest": version: 13.0.0 - resolution: "node-gyp@npm:13.0.0" + resolution: "node-gyp@npm:13.0.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fnode-gyp%2F-%2Fnode-gyp-13.0.0.tgz" dependencies: env-paths: "npm:^2.2.0" exponential-backoff: "npm:^3.1.1" @@ -3953,7 +4316,7 @@ __metadata: "nopt@npm:^10.0.0": version: 10.0.1 - resolution: "nopt@npm:10.0.1" + resolution: "nopt@npm:10.0.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fnopt%2F-%2Fnopt-10.0.1.tgz" dependencies: abbrev: "npm:^5.0.0" bin: @@ -3964,21 +4327,21 @@ __metadata: "normalize-url@npm:^6.0.1": version: 6.1.0 - resolution: "normalize-url@npm:6.1.0" + resolution: "normalize-url@npm:6.1.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fnormalize-url%2F-%2Fnormalize-url-6.1.0.tgz" checksum: 10c0/95d948f9bdd2cfde91aa786d1816ae40f8262946e13700bf6628105994fe0ff361662c20af3961161c38a119dc977adeb41fc0b41b1745eb77edaaf9cb22db23 languageName: node linkType: hard "obug@npm:^2.1.1, obug@npm:^2.1.2": version: 2.1.3 - resolution: "obug@npm:2.1.3" + resolution: "obug@npm:2.1.3::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fobug%2F-%2Fobug-2.1.3.tgz" checksum: 10c0/cb8187fed0a5fc8445507c950e89f3c1bd43895658c398b5803f6b7804dfa0c562975ecce1e67f3d9247d521452a5bfade9e0e951cc0326b7444272f7c24d25f languageName: node linkType: hard "octokit@npm:^5.0.5": version: 5.0.5 - resolution: "octokit@npm:5.0.5" + resolution: "octokit@npm:5.0.5::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Foctokit%2F-%2Foctokit-5.0.5.tgz" dependencies: "@octokit/app": "npm:^16.1.2" "@octokit/core": "npm:^7.0.6" @@ -3997,7 +4360,7 @@ __metadata: "once@npm:^1.3.1, once@npm:^1.4.0": version: 1.4.0 - resolution: "once@npm:1.4.0" + resolution: "once@npm:1.4.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fonce%2F-%2Fonce-1.4.0.tgz" dependencies: wrappy: "npm:1" checksum: 10c0/5d48aca287dfefabd756621c5dfce5c91a549a93e9fdb7b8246bc4c4790aa2ec17b34a260530474635147aeb631a2dcc8b32c613df0675f96041cbb8244517d0 @@ -4006,7 +4369,7 @@ __metadata: "oxc-parser@npm:^0.137.0": version: 0.137.0 - resolution: "oxc-parser@npm:0.137.0" + resolution: "oxc-parser@npm:0.137.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Foxc-parser%2F-%2Foxc-parser-0.137.0.tgz" dependencies: "@oxc-parser/binding-android-arm-eabi": "npm:0.137.0" "@oxc-parser/binding-android-arm64": "npm:0.137.0" @@ -4074,29 +4437,29 @@ __metadata: languageName: node linkType: hard -"oxfmt@npm:=0.52.0": - version: 0.52.0 - resolution: "oxfmt@npm:0.52.0" - dependencies: - "@oxfmt/binding-android-arm-eabi": "npm:0.52.0" - "@oxfmt/binding-android-arm64": "npm:0.52.0" - "@oxfmt/binding-darwin-arm64": "npm:0.52.0" - "@oxfmt/binding-darwin-x64": "npm:0.52.0" - "@oxfmt/binding-freebsd-x64": "npm:0.52.0" - "@oxfmt/binding-linux-arm-gnueabihf": "npm:0.52.0" - "@oxfmt/binding-linux-arm-musleabihf": "npm:0.52.0" - "@oxfmt/binding-linux-arm64-gnu": "npm:0.52.0" - "@oxfmt/binding-linux-arm64-musl": "npm:0.52.0" - "@oxfmt/binding-linux-ppc64-gnu": "npm:0.52.0" - "@oxfmt/binding-linux-riscv64-gnu": "npm:0.52.0" - "@oxfmt/binding-linux-riscv64-musl": "npm:0.52.0" - "@oxfmt/binding-linux-s390x-gnu": "npm:0.52.0" - "@oxfmt/binding-linux-x64-gnu": "npm:0.52.0" - "@oxfmt/binding-linux-x64-musl": "npm:0.52.0" - "@oxfmt/binding-openharmony-arm64": "npm:0.52.0" - "@oxfmt/binding-win32-arm64-msvc": "npm:0.52.0" - "@oxfmt/binding-win32-ia32-msvc": "npm:0.52.0" - "@oxfmt/binding-win32-x64-msvc": "npm:0.52.0" +"oxfmt@npm:=0.57.0": + version: 0.57.0 + resolution: "oxfmt@npm:0.57.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Foxfmt%2F-%2Foxfmt-0.57.0.tgz" + dependencies: + "@oxfmt/binding-android-arm-eabi": "npm:0.57.0" + "@oxfmt/binding-android-arm64": "npm:0.57.0" + "@oxfmt/binding-darwin-arm64": "npm:0.57.0" + "@oxfmt/binding-darwin-x64": "npm:0.57.0" + "@oxfmt/binding-freebsd-x64": "npm:0.57.0" + "@oxfmt/binding-linux-arm-gnueabihf": "npm:0.57.0" + "@oxfmt/binding-linux-arm-musleabihf": "npm:0.57.0" + "@oxfmt/binding-linux-arm64-gnu": "npm:0.57.0" + "@oxfmt/binding-linux-arm64-musl": "npm:0.57.0" + "@oxfmt/binding-linux-ppc64-gnu": "npm:0.57.0" + "@oxfmt/binding-linux-riscv64-gnu": "npm:0.57.0" + "@oxfmt/binding-linux-riscv64-musl": "npm:0.57.0" + "@oxfmt/binding-linux-s390x-gnu": "npm:0.57.0" + "@oxfmt/binding-linux-x64-gnu": "npm:0.57.0" + "@oxfmt/binding-linux-x64-musl": "npm:0.57.0" + "@oxfmt/binding-openharmony-arm64": "npm:0.57.0" + "@oxfmt/binding-win32-arm64-msvc": "npm:0.57.0" + "@oxfmt/binding-win32-ia32-msvc": "npm:0.57.0" + "@oxfmt/binding-win32-x64-msvc": "npm:0.57.0" tinypool: "npm:2.1.0" peerDependencies: svelte: ^5.0.0 @@ -4147,20 +4510,20 @@ __metadata: optional: true bin: oxfmt: bin/oxfmt - checksum: 10c0/a67a597202e29432f29049a6862feb927b6f996e5e909cb32acb2fe282d4b6d01d2e693c2b8a0ca6d016f236301cdd66c9250fd13af0876aee6c5b0eddba259f + checksum: 10c0/39879f27afef9b812094865d70ed51d84ab3b66b85cc6ec8a02dad0aa4ea528d0b8a33553a3a66cefa999c029529959ceb1907e4c71b1b01a882dbccb77ebc1b languageName: node linkType: hard -"oxlint-tsgolint@npm:=0.23.0": - version: 0.23.0 - resolution: "oxlint-tsgolint@npm:0.23.0" +"oxlint-tsgolint@npm:=0.24.0": + version: 0.24.0 + resolution: "oxlint-tsgolint@npm:0.24.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Foxlint-tsgolint%2F-%2Foxlint-tsgolint-0.24.0.tgz" dependencies: - "@oxlint-tsgolint/darwin-arm64": "npm:0.23.0" - "@oxlint-tsgolint/darwin-x64": "npm:0.23.0" - "@oxlint-tsgolint/linux-arm64": "npm:0.23.0" - "@oxlint-tsgolint/linux-x64": "npm:0.23.0" - "@oxlint-tsgolint/win32-arm64": "npm:0.23.0" - "@oxlint-tsgolint/win32-x64": "npm:0.23.0" + "@oxlint-tsgolint/darwin-arm64": "npm:0.24.0" + "@oxlint-tsgolint/darwin-x64": "npm:0.24.0" + "@oxlint-tsgolint/linux-arm64": "npm:0.24.0" + "@oxlint-tsgolint/linux-x64": "npm:0.24.0" + "@oxlint-tsgolint/win32-arm64": "npm:0.24.0" + "@oxlint-tsgolint/win32-x64": "npm:0.24.0" dependenciesMeta: "@oxlint-tsgolint/darwin-arm64": optional: true @@ -4176,33 +4539,33 @@ __metadata: optional: true bin: tsgolint: bin/tsgolint.js - checksum: 10c0/052035ea9fe2fe654aa1187ba30f842e1c44883bec34fffbb959dd86b5cfd0067af1bf0c27897b720f28346b3641422672e35913db8940f3e02728e459a1d8f1 - languageName: node - linkType: hard - -"oxlint@npm:=1.67.0": - version: 1.67.0 - resolution: "oxlint@npm:1.67.0" - dependencies: - "@oxlint/binding-android-arm-eabi": "npm:1.67.0" - "@oxlint/binding-android-arm64": "npm:1.67.0" - "@oxlint/binding-darwin-arm64": "npm:1.67.0" - "@oxlint/binding-darwin-x64": "npm:1.67.0" - "@oxlint/binding-freebsd-x64": "npm:1.67.0" - "@oxlint/binding-linux-arm-gnueabihf": "npm:1.67.0" - "@oxlint/binding-linux-arm-musleabihf": "npm:1.67.0" - "@oxlint/binding-linux-arm64-gnu": "npm:1.67.0" - "@oxlint/binding-linux-arm64-musl": "npm:1.67.0" - "@oxlint/binding-linux-ppc64-gnu": "npm:1.67.0" - "@oxlint/binding-linux-riscv64-gnu": "npm:1.67.0" - "@oxlint/binding-linux-riscv64-musl": "npm:1.67.0" - "@oxlint/binding-linux-s390x-gnu": "npm:1.67.0" - "@oxlint/binding-linux-x64-gnu": "npm:1.67.0" - "@oxlint/binding-linux-x64-musl": "npm:1.67.0" - "@oxlint/binding-openharmony-arm64": "npm:1.67.0" - "@oxlint/binding-win32-arm64-msvc": "npm:1.67.0" - "@oxlint/binding-win32-ia32-msvc": "npm:1.67.0" - "@oxlint/binding-win32-x64-msvc": "npm:1.67.0" + checksum: 10c0/1e62561c748a0b47a78775fd64c415104abc3aae28115926c8495a8d8d28ff1e3ba4a938cdd79ba85a76eb9fd0e0cae43c2b1461ee562f6b557b5fb1a437a4a1 + languageName: node + linkType: hard + +"oxlint@npm:=1.72.0": + version: 1.72.0 + resolution: "oxlint@npm:1.72.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Foxlint%2F-%2Foxlint-1.72.0.tgz" + dependencies: + "@oxlint/binding-android-arm-eabi": "npm:1.72.0" + "@oxlint/binding-android-arm64": "npm:1.72.0" + "@oxlint/binding-darwin-arm64": "npm:1.72.0" + "@oxlint/binding-darwin-x64": "npm:1.72.0" + "@oxlint/binding-freebsd-x64": "npm:1.72.0" + "@oxlint/binding-linux-arm-gnueabihf": "npm:1.72.0" + "@oxlint/binding-linux-arm-musleabihf": "npm:1.72.0" + "@oxlint/binding-linux-arm64-gnu": "npm:1.72.0" + "@oxlint/binding-linux-arm64-musl": "npm:1.72.0" + "@oxlint/binding-linux-ppc64-gnu": "npm:1.72.0" + "@oxlint/binding-linux-riscv64-gnu": "npm:1.72.0" + "@oxlint/binding-linux-riscv64-musl": "npm:1.72.0" + "@oxlint/binding-linux-s390x-gnu": "npm:1.72.0" + "@oxlint/binding-linux-x64-gnu": "npm:1.72.0" + "@oxlint/binding-linux-x64-musl": "npm:1.72.0" + "@oxlint/binding-openharmony-arm64": "npm:1.72.0" + "@oxlint/binding-win32-arm64-msvc": "npm:1.72.0" + "@oxlint/binding-win32-ia32-msvc": "npm:1.72.0" + "@oxlint/binding-win32-x64-msvc": "npm:1.72.0" peerDependencies: oxlint-tsgolint: ">=0.22.1" vite-plus: "*" @@ -4252,20 +4615,20 @@ __metadata: optional: true bin: oxlint: bin/oxlint - checksum: 10c0/2532bebc3ed26f04e14bad2932763d1b71990c9b0bc103ab830eda990a97b9cc195b286ca5f174034684f9b1c842cbdcfd47c99adfb9519193ef23d0d3892507 + checksum: 10c0/c8d8199b9ddf7986e96ac49ed3828c3cd33424cd0dff404f3ade834db66b10e1c7c1bf6b294d50ee1003d0b509e48e946490d7fcde5abdde07620c7c28d43738 languageName: node linkType: hard "p-cancelable@npm:^2.0.0": version: 2.1.1 - resolution: "p-cancelable@npm:2.1.1" + resolution: "p-cancelable@npm:2.1.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fp-cancelable%2F-%2Fp-cancelable-2.1.1.tgz" checksum: 10c0/8c6dc1f8dd4154fd8b96a10e55a3a832684c4365fb9108056d89e79fbf21a2465027c04a59d0d797b5ffe10b54a61a32043af287d5c4860f1e996cbdbc847f01 languageName: node linkType: hard "p-limit@npm:^2.2.0": version: 2.3.0 - resolution: "p-limit@npm:2.3.0" + resolution: "p-limit@npm:2.3.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fp-limit%2F-%2Fp-limit-2.3.0.tgz" dependencies: p-try: "npm:^2.0.0" checksum: 10c0/8da01ac53efe6a627080fafc127c873da40c18d87b3f5d5492d465bb85ec7207e153948df6b9cbaeb130be70152f874229b8242ee2be84c0794082510af97f12 @@ -4274,94 +4637,101 @@ __metadata: "p-try@npm:^2.0.0": version: 2.2.0 - resolution: "p-try@npm:2.2.0" + resolution: "p-try@npm:2.2.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fp-try%2F-%2Fp-try-2.2.0.tgz" checksum: 10c0/c36c19907734c904b16994e6535b02c36c2224d433e01a2f1ab777237f4d86e6289fd5fd464850491e940379d4606ed850c03e0f9ab600b0ebddb511312e177f languageName: node linkType: hard "path-key@npm:^3.1.0": version: 3.1.1 - resolution: "path-key@npm:3.1.1" + resolution: "path-key@npm:3.1.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fpath-key%2F-%2Fpath-key-3.1.1.tgz" checksum: 10c0/748c43efd5a569c039d7a00a03b58eecd1d75f3999f5a28303d75f521288df4823bc057d8784eb72358b2895a05f29a070bc9f1f17d28226cc4e62494cc58c4c languageName: node linkType: hard -"picocolors@npm:^1.1.1": +"pathe@npm:^2.0.3": + version: 2.0.3 + resolution: "pathe@npm:2.0.3::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fpathe%2F-%2Fpathe-2.0.3.tgz" + checksum: 10c0/c118dc5a8b5c4166011b2b70608762e260085180bb9e33e80a50dcdb1e78c010b1624f4280c492c92b05fc276715a4c357d1f9edc570f8f1b3d90b6839ebaca1 + languageName: node + linkType: hard + +"picocolors@npm:1.1.1, picocolors@npm:^1.1.1": version: 1.1.1 - resolution: "picocolors@npm:1.1.1" + resolution: "picocolors@npm:1.1.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fpicocolors%2F-%2Fpicocolors-1.1.1.tgz" checksum: 10c0/e2e3e8170ab9d7c7421969adaa7e1b31434f789afb9b3f115f6b96d91945041ac3ceb02e9ec6fe6510ff036bcc0bf91e69a1772edc0b707e12b19c0f2d6bcf58 languageName: node linkType: hard "picomatch@npm:^2.3.1": version: 2.3.2 - resolution: "picomatch@npm:2.3.2" + resolution: "picomatch@npm:2.3.2::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fpicomatch%2F-%2Fpicomatch-2.3.2.tgz" checksum: 10c0/a554d1709e59be97d1acb9eaedbbc700a5c03dbd4579807baed95100b00420bc729335440ef15004ae2378984e2487a7c1cebd743cfdb72b6fa9ab69223c0d61 languageName: node linkType: hard -"picomatch@npm:^4.0.4": +"picomatch@npm:^4.0.3, picomatch@npm:^4.0.4": version: 4.0.4 - resolution: "picomatch@npm:4.0.4" + resolution: "picomatch@npm:4.0.4::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fpicomatch%2F-%2Fpicomatch-4.0.4.tgz" checksum: 10c0/e2c6023372cc7b5764719a5ffb9da0f8e781212fa7ca4bd0562db929df8e117460f00dff3cb7509dacfc06b86de924b247f504d0ce1806a37fac4633081466b0 languageName: node linkType: hard "pirates@npm:^4.0.7": version: 4.0.7 - resolution: "pirates@npm:4.0.7" + resolution: "pirates@npm:4.0.7::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fpirates%2F-%2Fpirates-4.0.7.tgz" checksum: 10c0/a51f108dd811beb779d58a76864bbd49e239fa40c7984cd11596c75a121a8cc789f1c8971d8bb15f0dbf9d48b76c05bb62fcbce840f89b688c0fa64b37e8478a languageName: node linkType: hard -"pixelmatch@npm:^7.1.0": - version: 7.2.0 - resolution: "pixelmatch@npm:7.2.0" - dependencies: - pngjs: "npm:^7.0.0" - bin: - pixelmatch: bin/pixelmatch - checksum: 10c0/d8a2454ecf3a977738d6050ed087effa0a9f1b53a0da92d37381a74bb04db5c21e14cfcd41965fbdac4a5967aa5ff2525e2afe9826a77eae043b8339dafc53b6 - languageName: node - linkType: hard - "pngjs@npm:^7.0.0": version: 7.0.0 - resolution: "pngjs@npm:7.0.0" + resolution: "pngjs@npm:7.0.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fpngjs%2F-%2Fpngjs-7.0.0.tgz" checksum: 10c0/0d4c7a0fd476a9c33df7d0a2a73e1d56537628a668841f6995c2bca070cf30819f9254a64363266bc14ef2fee47659dd3b4f2b18eec7ab65143015139f497b38 languageName: node linkType: hard "postcss@npm:^8.5.6": - version: 8.5.15 - resolution: "postcss@npm:8.5.15" + version: 8.5.16 + resolution: "postcss@npm:8.5.16::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fpostcss%2F-%2Fpostcss-8.5.16.tgz" dependencies: nanoid: "npm:^3.3.12" picocolors: "npm:^1.1.1" source-map-js: "npm:^1.2.1" - checksum: 10c0/7f2e63ae22fbe43aace1bf652bd99da4e90737c64194d49e51ddc9cd0f9e51ff2861a7d734379b494deffa03a880a5c65eec70bc29ee9ebaa7136dde3eee8f31 + checksum: 10c0/625de7a02f662f3a340964d14b487bd5097adf16f5f171e257d19005ba37aea8768ee446557500e88e91ca46b4d14d6cb4a0bf033c6ec0c8c0b660d85719f1ef languageName: node linkType: hard "prettier@npm:^3.8.4": - version: 3.8.4 - resolution: "prettier@npm:3.8.4" + version: 3.9.4 + resolution: "prettier@npm:3.9.4::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fprettier%2F-%2Fprettier-3.9.4.tgz" bin: prettier: bin/prettier.cjs - checksum: 10c0/b90a0cbe75b88ac0af9c13fe0f359bd19926fabccd88483227b21f71f0c1cc42da056fc1ac3a361e665577c568371d5ccfb2c62c31c8a1186f8d1bd531a063e9 + checksum: 10c0/dc6ec13d692745c6b7e7bf39beda9eb1b3b76c619118319bce393928c392264b1273337c75f80499695e165affefc7b3a9d2ebb18983af0a8f4dfbb2f45f6ff3 + languageName: node + linkType: hard + +"pretty-format@npm:^27.0.2": + version: 27.5.1 + resolution: "pretty-format@npm:27.5.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fpretty-format%2F-%2Fpretty-format-27.5.1.tgz" + dependencies: + ansi-regex: "npm:^5.0.1" + ansi-styles: "npm:^5.0.0" + react-is: "npm:^17.0.1" + checksum: 10c0/0cbda1031aa30c659e10921fa94e0dd3f903ecbbbe7184a729ad66f2b6e7f17891e8c7d7654c458fa4ccb1a411ffb695b4f17bbcd3fe075fabe181027c4040ed languageName: node linkType: hard "proc-log@npm:^7.0.0": version: 7.0.0 - resolution: "proc-log@npm:7.0.0" + resolution: "proc-log@npm:7.0.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fproc-log%2F-%2Fproc-log-7.0.0.tgz" checksum: 10c0/b89c2d862604f35fec795477b0c7e376feab3ba0d4f4d291c4e959567442697cf451ac557d0623c1cc38af45a78128b983410f397a10c5d3a67f76c33de4754b languageName: node linkType: hard "pump@npm:^3.0.0": version: 3.0.4 - resolution: "pump@npm:3.0.4" + resolution: "pump@npm:3.0.4::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fpump%2F-%2Fpump-3.0.4.tgz" dependencies: end-of-stream: "npm:^1.1.0" once: "npm:^1.3.1" @@ -4371,28 +4741,35 @@ __metadata: "queue-microtask@npm:^1.2.2": version: 1.2.3 - resolution: "queue-microtask@npm:1.2.3" + resolution: "queue-microtask@npm:1.2.3::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fqueue-microtask%2F-%2Fqueue-microtask-1.2.3.tgz" checksum: 10c0/900a93d3cdae3acd7d16f642c29a642aea32c2026446151f0778c62ac089d4b8e6c986811076e1ae180a694cedf077d453a11b58ff0a865629a4f82ab558e102 languageName: node linkType: hard "quick-lru@npm:^5.1.1": version: 5.1.1 - resolution: "quick-lru@npm:5.1.1" + resolution: "quick-lru@npm:5.1.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fquick-lru%2F-%2Fquick-lru-5.1.1.tgz" checksum: 10c0/a24cba5da8cec30d70d2484be37622580f64765fb6390a928b17f60cd69e8dbd32a954b3ff9176fa1b86d86ff2ba05252fae55dc4d40d0291c60412b0ad096da languageName: node linkType: hard +"react-is@npm:^17.0.1": + version: 17.0.2 + resolution: "react-is@npm:17.0.2::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Freact-is%2F-%2Freact-is-17.0.2.tgz" + checksum: 10c0/2bdb6b93fbb1820b024b496042cce405c57e2f85e777c9aabd55f9b26d145408f9f74f5934676ffdc46f3dcff656d78413a6e43968e7b3f92eea35b3052e9053 + languageName: node + linkType: hard + "resolve-alpn@npm:^1.0.0": version: 1.2.1 - resolution: "resolve-alpn@npm:1.2.1" + resolution: "resolve-alpn@npm:1.2.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fresolve-alpn%2F-%2Fresolve-alpn-1.2.1.tgz" checksum: 10c0/b70b29c1843bc39781ef946c8cd4482e6d425976599c0f9c138cec8209e4e0736161bf39319b01676a847000085dfdaf63583c6fb4427bf751a10635bd2aa0c4 languageName: node linkType: hard "responselike@npm:^2.0.0": version: 2.0.1 - resolution: "responselike@npm:2.0.1" + resolution: "responselike@npm:2.0.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fresponselike%2F-%2Fresponselike-2.0.1.tgz" dependencies: lowercase-keys: "npm:^2.0.0" checksum: 10c0/360b6deb5f101a9f8a4174f7837c523c3ec78b7ca8a7c1d45a1062b303659308a23757e318b1e91ed8684ad1205721142dd664d94771cd63499353fd4ee732b5 @@ -4401,14 +4778,14 @@ __metadata: "reusify@npm:^1.0.4": version: 1.1.0 - resolution: "reusify@npm:1.1.0" + resolution: "reusify@npm:1.1.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Freusify%2F-%2Freusify-1.1.0.tgz" checksum: 10c0/4eff0d4a5f9383566c7d7ec437b671cc51b25963bd61bf127c3f3d3f68e44a026d99b8d2f1ad344afff8d278a8fe70a8ea092650a716d22287e8bef7126bb2fa languageName: node linkType: hard "run-parallel@npm:^1.1.9": version: 1.2.0 - resolution: "run-parallel@npm:1.2.0" + resolution: "run-parallel@npm:1.2.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Frun-parallel%2F-%2Frun-parallel-1.2.0.tgz" dependencies: queue-microtask: "npm:^1.2.2" checksum: 10c0/200b5ab25b5b8b7113f9901bfe3afc347e19bb7475b267d55ad0eb86a62a46d77510cb0f232507c9e5d497ebda569a08a9867d0d14f57a82ad5564d991588b39 @@ -4417,14 +4794,14 @@ __metadata: "safer-buffer@npm:>= 2.1.2 < 3.0.0": version: 2.1.2 - resolution: "safer-buffer@npm:2.1.2" + resolution: "safer-buffer@npm:2.1.2::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fsafer-buffer%2F-%2Fsafer-buffer-2.1.2.tgz" checksum: 10c0/7e3c8b2e88a1841c9671094bbaeebd94448111dd90a81a1f606f3f67708a6ec57763b3b47f06da09fc6054193e0e6709e77325415dc8422b04497a8070fa02d4 languageName: node linkType: hard "semver@npm:^7.1.2, semver@npm:^7.3.5, semver@npm:^7.8.2, semver@npm:^7.8.5": version: 7.8.5 - resolution: "semver@npm:7.8.5" + resolution: "semver@npm:7.8.5::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fsemver%2F-%2Fsemver-7.8.5.tgz" bin: semver: bin/semver.js checksum: 10c0/b1f3127a5be8125a94f37188b361c212466c292c6910adce3ec106cff5dc211ccaedc4739c11bb70fda59d6fc1f040a9bca289f4e093451521a2372e5231fe0c @@ -4433,7 +4810,7 @@ __metadata: "shebang-command@npm:^2.0.0": version: 2.0.0 - resolution: "shebang-command@npm:2.0.0" + resolution: "shebang-command@npm:2.0.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fshebang-command%2F-%2Fshebang-command-2.0.0.tgz" dependencies: shebang-regex: "npm:^3.0.0" checksum: 10c0/a41692e7d89a553ef21d324a5cceb5f686d1f3c040759c50aab69688634688c5c327f26f3ecf7001ebfd78c01f3c7c0a11a7c8bfd0a8bc9f6240d4f40b224e4e @@ -4442,21 +4819,28 @@ __metadata: "shebang-regex@npm:^3.0.0": version: 3.0.0 - resolution: "shebang-regex@npm:3.0.0" + resolution: "shebang-regex@npm:3.0.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fshebang-regex%2F-%2Fshebang-regex-3.0.0.tgz" checksum: 10c0/1dbed0726dd0e1152a92696c76c7f06084eb32a90f0528d11acd764043aacf76994b2fb30aa1291a21bd019d6699164d048286309a278855ee7bec06cf6fb690 languageName: node linkType: hard +"siginfo@npm:^2.0.0": + version: 2.0.0 + resolution: "siginfo@npm:2.0.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fsiginfo%2F-%2Fsiginfo-2.0.0.tgz" + checksum: 10c0/3def8f8e516fbb34cb6ae415b07ccc5d9c018d85b4b8611e3dc6f8be6d1899f693a4382913c9ed51a06babb5201639d76453ab297d1c54a456544acf5c892e34 + languageName: node + linkType: hard + "signal-exit@npm:^4.1.0": version: 4.1.0 - resolution: "signal-exit@npm:4.1.0" + resolution: "signal-exit@npm:4.1.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fsignal-exit%2F-%2Fsignal-exit-4.1.0.tgz" checksum: 10c0/41602dce540e46d599edba9d9860193398d135f7ff72cab629db5171516cfae628d21e7bfccde1bbfdf11c48726bc2a6d1a8fb8701125852fbfda7cf19c6aa83 languageName: node linkType: hard "sirv@npm:^3.0.2": version: 3.0.2 - resolution: "sirv@npm:3.0.2" + resolution: "sirv@npm:3.0.2::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fsirv%2F-%2Fsirv-3.0.2.tgz" dependencies: "@polka/url": "npm:^1.0.0-next.24" mrmime: "npm:^2.0.0" @@ -4467,28 +4851,35 @@ __metadata: "source-map-js@npm:^1.2.1": version: 1.2.1 - resolution: "source-map-js@npm:1.2.1" + resolution: "source-map-js@npm:1.2.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fsource-map-js%2F-%2Fsource-map-js-1.2.1.tgz" checksum: 10c0/7bda1fc4c197e3c6ff17de1b8b2c20e60af81b63a52cb32ec5a5d67a20a7d42651e2cb34ebe93833c5a2a084377e17455854fee3e21e7925c64a51b6a52b0faf languageName: node linkType: hard "sprintf-js@npm:~1.0.2": version: 1.0.3 - resolution: "sprintf-js@npm:1.0.3" + resolution: "sprintf-js@npm:1.0.3::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fsprintf-js%2F-%2Fsprintf-js-1.0.3.tgz" checksum: 10c0/ecadcfe4c771890140da5023d43e190b7566d9cf8b2d238600f31bec0fc653f328da4450eb04bd59a431771a8e9cc0e118f0aa3974b683a4981b4e07abc2a5bb languageName: node linkType: hard -"std-env@npm:^4.0.0": +"stackback@npm:0.0.2": + version: 0.0.2 + resolution: "stackback@npm:0.0.2::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fstackback%2F-%2Fstackback-0.0.2.tgz" + checksum: 10c0/89a1416668f950236dd5ac9f9a6b2588e1b9b62b1b6ad8dff1bfc5d1a15dbf0aafc9b52d2226d00c28dffff212da464eaeebfc6b7578b9d180cef3e3782c5983 + languageName: node + linkType: hard + +"std-env@npm:^4.0.0-rc.1": version: 4.1.0 - resolution: "std-env@npm:4.1.0" + resolution: "std-env@npm:4.1.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fstd-env%2F-%2Fstd-env-4.1.0.tgz" checksum: 10c0/2e14b6b490db34cb969a48d9cf7c35bca4a47653914aac2814221baae7b867a5b15940d133625c391621971f98cd2266a5dc7036669960e883f1081db2a56558 languageName: node linkType: hard "string-width@npm:^4.2.3": version: 4.2.3 - resolution: "string-width@npm:4.2.3" + resolution: "string-width@npm:4.2.3::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fstring-width%2F-%2Fstring-width-4.2.3.tgz" dependencies: emoji-regex: "npm:^8.0.0" is-fullwidth-code-point: "npm:^3.0.0" @@ -4499,7 +4890,7 @@ __metadata: "strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": version: 6.0.1 - resolution: "strip-ansi@npm:6.0.1" + resolution: "strip-ansi@npm:6.0.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fstrip-ansi%2F-%2Fstrip-ansi-6.0.1.tgz" dependencies: ansi-regex: "npm:^5.0.1" checksum: 10c0/1ae5f212a126fe5b167707f716942490e3933085a5ff6c008ab97ab2f272c8025d3aa218b7bd6ab25729ca20cc81cddb252102f8751e13482a5199e873680952 @@ -4508,7 +4899,7 @@ __metadata: "supports-color@npm:^7.1.0": version: 7.2.0 - resolution: "supports-color@npm:7.2.0" + resolution: "supports-color@npm:7.2.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fsupports-color%2F-%2Fsupports-color-7.2.0.tgz" dependencies: has-flag: "npm:^4.0.0" checksum: 10c0/afb4c88521b8b136b5f5f95160c98dee7243dc79d5432db7efc27efb219385bbc7d9427398e43dd6cc730a0f87d5085ce1652af7efbe391327bc0a7d0f7fc124 @@ -4516,35 +4907,35 @@ __metadata: linkType: hard "tar@npm:^7.5.3, tar@npm:^7.5.4": - version: 7.5.16 - resolution: "tar@npm:7.5.16" + version: 7.5.19 + resolution: "tar@npm:7.5.19::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Ftar%2F-%2Ftar-7.5.19.tgz" dependencies: "@isaacs/fs-minipass": "npm:^4.0.0" chownr: "npm:^3.0.0" minipass: "npm:^7.1.2" minizlib: "npm:^3.1.0" yallist: "npm:^5.0.0" - checksum: 10c0/4f37f3c4bd2ca2755fd736a5df1d573c1a868ec1b1e893346aeafa95ac510f9e2fd1469420bd866cc7904799e5bd4ac62b5d4f03fe27747d6e1e373b44505c5c + checksum: 10c0/7022e8cb04a8ceccc0689f2c731743fa2aab2e3c3f559f7dbc37b65ef7d5913049b427284eded2ec0765c5db5ff72dd7939fe2ae15785ff422cef2116c95d798 languageName: node linkType: hard "tinybench@npm:^2.9.0": version: 2.9.0 - resolution: "tinybench@npm:2.9.0" + resolution: "tinybench@npm:2.9.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Ftinybench%2F-%2Ftinybench-2.9.0.tgz" checksum: 10c0/c3500b0f60d2eb8db65250afe750b66d51623057ee88720b7f064894a6cb7eb93360ca824a60a31ab16dab30c7b1f06efe0795b352e37914a9d4bad86386a20c languageName: node linkType: hard "tinyexec@npm:^1.0.2": version: 1.2.4 - resolution: "tinyexec@npm:1.2.4" + resolution: "tinyexec@npm:1.2.4::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Ftinyexec%2F-%2Ftinyexec-1.2.4.tgz" checksum: 10c0/153b8db6b080194b558ff145b9cffc36b80a6e07babd644dcfbe49c807eee668c876049d28bdee90b96304476f883352f2dad91b3f86bc23832532f4363e66ff languageName: node linkType: hard "tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.15": version: 0.2.17 - resolution: "tinyglobby@npm:0.2.17" + resolution: "tinyglobby@npm:0.2.17::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Ftinyglobby%2F-%2Ftinyglobby-0.2.17.tgz" dependencies: fdir: "npm:^6.5.0" picomatch: "npm:^4.0.4" @@ -4554,21 +4945,28 @@ __metadata: "tinylogic@npm:^2.0.0": version: 2.0.0 - resolution: "tinylogic@npm:2.0.0" + resolution: "tinylogic@npm:2.0.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Ftinylogic%2F-%2Ftinylogic-2.0.0.tgz" checksum: 10c0/c9417c4b65dfc469c71c9eba4d43d44813ab8baceb80ba2c0e6c286de2e93e9c4b8522e4b0a7b91cb4a85353368ee93838a862262ce54bac431b884e694d1c89 languageName: node linkType: hard "tinypool@npm:2.1.0": version: 2.1.0 - resolution: "tinypool@npm:2.1.0" + resolution: "tinypool@npm:2.1.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Ftinypool%2F-%2Ftinypool-2.1.0.tgz" checksum: 10c0/9fb1c760558c6264e0f4cfde96a63b12450b43f1730fbe6274aa24ddbdf488745c08924d0dea7a1303b47d555416a6415f2113898c69b6ecf731e75ac95238a5 languageName: node linkType: hard +"tinyrainbow@npm:^3.1.0": + version: 3.1.0 + resolution: "tinyrainbow@npm:3.1.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Ftinyrainbow%2F-%2Ftinyrainbow-3.1.0.tgz" + checksum: 10c0/f11cf387a26c5c9255bec141a90ac511b26172981b10c3e50053bc6700ea7d2336edcc4a3a21dbb8412fe7c013477d2ba4d7e4877800f3f8107be5105aad6511 + languageName: node + linkType: hard + "to-regex-range@npm:^5.0.1": version: 5.0.1 - resolution: "to-regex-range@npm:5.0.1" + resolution: "to-regex-range@npm:5.0.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fto-regex-range%2F-%2Fto-regex-range-5.0.1.tgz" dependencies: is-number: "npm:^7.0.0" checksum: 10c0/487988b0a19c654ff3e1961b87f471702e708fa8a8dd02a298ef16da7206692e8552a0250e8b3e8759270f62e9d8314616f6da274734d3b558b1fc7b7724e892 @@ -4577,42 +4975,42 @@ __metadata: "toad-cache@npm:^3.7.0": version: 3.7.1 - resolution: "toad-cache@npm:3.7.1" + resolution: "toad-cache@npm:3.7.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Ftoad-cache%2F-%2Ftoad-cache-3.7.1.tgz" checksum: 10c0/22efc940723bbe30b8386af82ed0824263567b84d37f29f57651ad28d9aef823b678f07f6d6c2e142448d5f351f89e15e05f37f59e2c7f1d183294b8d32f93fb languageName: node linkType: hard "totalist@npm:^3.0.0": version: 3.0.1 - resolution: "totalist@npm:3.0.1" + resolution: "totalist@npm:3.0.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Ftotalist%2F-%2Ftotalist-3.0.1.tgz" checksum: 10c0/4bb1fadb69c3edbef91c73ebef9d25b33bbf69afe1e37ce544d5f7d13854cda15e47132f3e0dc4cafe300ddb8578c77c50a65004d8b6e97e77934a69aa924863 languageName: node linkType: hard "treeify@npm:^1.1.0": version: 1.1.0 - resolution: "treeify@npm:1.1.0" + resolution: "treeify@npm:1.1.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Ftreeify%2F-%2Ftreeify-1.1.0.tgz" checksum: 10c0/2f0dea9e89328b8a42296a3963d341ab19897a05b723d6b0bced6b28701a340d2a7b03241aef807844198e46009aaf3755139274eb082cfce6fdc1935cbd69dd languageName: node linkType: hard "tslib@npm:^2.4.0": version: 2.8.1 - resolution: "tslib@npm:2.8.1" + resolution: "tslib@npm:2.8.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Ftslib%2F-%2Ftslib-2.8.1.tgz" checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62 languageName: node linkType: hard "typanion@npm:^3.14.0, typanion@npm:^3.8.0": version: 3.14.0 - resolution: "typanion@npm:3.14.0" + resolution: "typanion@npm:3.14.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Ftypanion%2F-%2Ftypanion-3.14.0.tgz" checksum: 10c0/8b03b19844e6955bfd906c31dc781bae6d7f1fb3ce4fe24b7501557013d4889ae5cefe671dafe98d87ead0adceb8afcb8bc16df7dc0bd2b7331bac96f3a7cae2 languageName: node linkType: hard "typescript@npm:^6.0.3": version: 6.0.3 - resolution: "typescript@npm:6.0.3" + resolution: "typescript@npm:6.0.3::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Ftypescript%2F-%2Ftypescript-6.0.3.tgz" bin: tsc: bin/tsc tsserver: bin/tsserver @@ -4622,7 +5020,7 @@ __metadata: "typescript@patch:typescript@npm%3A^6.0.3#optional!builtin": version: 6.0.3 - resolution: "typescript@patch:typescript@npm%3A6.0.3#optional!builtin::version=6.0.3&hash=5786d5" + resolution: "typescript@patch:typescript@npm%3A6.0.3%3A%3A__archiveUrl=https%253A%252F%252Fregistry.npmjs.org%252Ftypescript%252F-%252Ftypescript-6.0.3.tgz#optional!builtin::version=6.0.3&hash=5786d5" bin: tsc: bin/tsc tsserver: bin/tsserver @@ -4632,58 +5030,70 @@ __metadata: "undici-types@npm:~7.16.0": version: 7.16.0 - resolution: "undici-types@npm:7.16.0" + resolution: "undici-types@npm:7.16.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fundici-types%2F-%2Fundici-types-7.16.0.tgz" checksum: 10c0/3033e2f2b5c9f1504bdc5934646cb54e37ecaca0f9249c983f7b1fc2e87c6d18399ebb05dc7fd5419e02b2e915f734d872a65da2e3eeed1813951c427d33cc9a languageName: node linkType: hard "undici-types@npm:~8.3.0": version: 8.3.0 - resolution: "undici-types@npm:8.3.0" + resolution: "undici-types@npm:8.3.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fundici-types%2F-%2Fundici-types-8.3.0.tgz" checksum: 10c0/c8aa7e2fbebfce519654dafadc0ece59be888d2ccaf180fb4495da875e7b536d2456345c384069c7e6f3e9c9ab7435f074957da306f142343eee86ff8048855a languageName: node linkType: hard "undici@npm:^6.25.0": version: 6.27.0 - resolution: "undici@npm:6.27.0" + resolution: "undici@npm:6.27.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fundici%2F-%2Fundici-6.27.0.tgz" checksum: 10c0/f88c3dae3957dbf9d93cb481440aced317bd3c4941b5914fea5efba516d51138988cdb5c76006f0bb1337e41d56c3443351055d492e73af2428521c37ba2a76f languageName: node linkType: hard "universal-github-app-jwt@npm:^2.2.0": version: 2.2.2 - resolution: "universal-github-app-jwt@npm:2.2.2" + resolution: "universal-github-app-jwt@npm:2.2.2::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Funiversal-github-app-jwt%2F-%2Funiversal-github-app-jwt-2.2.2.tgz" checksum: 10c0/7ae5f031fb89c01a4407459b764c5e6341d725d436e1ceec161f9b754dd4883d9704cc8de53d5b6314b7e1bef8dbc7561799fc23001e706f213d468c17026fb6 languageName: node linkType: hard "universal-user-agent@npm:^7.0.0, universal-user-agent@npm:^7.0.2": version: 7.0.3 - resolution: "universal-user-agent@npm:7.0.3" + resolution: "universal-user-agent@npm:7.0.3::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Funiversal-user-agent%2F-%2Funiversal-user-agent-7.0.3.tgz" checksum: 10c0/6043be466a9bb96c0ce82392842d9fddf4c37e296f7bacc2cb25f47123990eb436c82df824644f9c5070a94dbdb117be17f66d54599ab143648ec57ef93dbcc8 languageName: node linkType: hard -"vite-plus@npm:0.1.24": - version: 0.1.24 - resolution: "vite-plus@npm:0.1.24" - dependencies: - "@oxc-project/types": "npm:=0.133.0" - "@oxlint/plugins": "npm:=1.61.0" - "@voidzero-dev/vite-plus-core": "npm:0.1.24" - "@voidzero-dev/vite-plus-darwin-arm64": "npm:0.1.24" - "@voidzero-dev/vite-plus-darwin-x64": "npm:0.1.24" - "@voidzero-dev/vite-plus-linux-arm64-gnu": "npm:0.1.24" - "@voidzero-dev/vite-plus-linux-arm64-musl": "npm:0.1.24" - "@voidzero-dev/vite-plus-linux-x64-gnu": "npm:0.1.24" - "@voidzero-dev/vite-plus-linux-x64-musl": "npm:0.1.24" - "@voidzero-dev/vite-plus-test": "npm:0.1.24" - "@voidzero-dev/vite-plus-win32-arm64-msvc": "npm:0.1.24" - "@voidzero-dev/vite-plus-win32-x64-msvc": "npm:0.1.24" - oxfmt: "npm:=0.52.0" - oxlint: "npm:=1.67.0" - oxlint-tsgolint: "npm:=0.23.0" +"vite-plus@npm:0.0.0-commit.1d7ba811f0e456640b0e761e8e8acf77d1ad3251": + version: 0.0.0-commit.1d7ba811f0e456640b0e761e8e8acf77d1ad3251 + resolution: "vite-plus@npm:0.0.0-commit.1d7ba811f0e456640b0e761e8e8acf77d1ad3251::__archiveUrl=https%3A%2F%2Fregistry-bridge.viteplus.dev%2Ftarballs%2Fvite-plus%2F0.0.0-commit.1d7ba811f0e456640b0e761e8e8acf77d1ad3251.tgz" + dependencies: + "@oxc-project/types": "npm:=0.138.0" + "@oxlint/plugins": "npm:=1.68.0" + "@vitest/browser": "npm:4.1.9" + "@vitest/browser-preview": "npm:4.1.9" + "@vitest/expect": "npm:4.1.9" + "@vitest/mocker": "npm:4.1.9" + "@vitest/pretty-format": "npm:4.1.9" + "@vitest/runner": "npm:4.1.9" + "@vitest/snapshot": "npm:4.1.9" + "@vitest/spy": "npm:4.1.9" + "@vitest/utils": "npm:4.1.9" + "@voidzero-dev/vite-plus-core": "npm:0.0.0-commit.1d7ba811f0e456640b0e761e8e8acf77d1ad3251" + "@voidzero-dev/vite-plus-darwin-arm64": "npm:0.0.0-commit.1d7ba811f0e456640b0e761e8e8acf77d1ad3251" + "@voidzero-dev/vite-plus-darwin-x64": "npm:0.0.0-commit.1d7ba811f0e456640b0e761e8e8acf77d1ad3251" + "@voidzero-dev/vite-plus-linux-arm64-gnu": "npm:0.0.0-commit.1d7ba811f0e456640b0e761e8e8acf77d1ad3251" + "@voidzero-dev/vite-plus-linux-arm64-musl": "npm:0.0.0-commit.1d7ba811f0e456640b0e761e8e8acf77d1ad3251" + "@voidzero-dev/vite-plus-linux-x64-gnu": "npm:0.0.0-commit.1d7ba811f0e456640b0e761e8e8acf77d1ad3251" + "@voidzero-dev/vite-plus-linux-x64-musl": "npm:0.0.0-commit.1d7ba811f0e456640b0e761e8e8acf77d1ad3251" + "@voidzero-dev/vite-plus-win32-arm64-msvc": "npm:0.0.0-commit.1d7ba811f0e456640b0e761e8e8acf77d1ad3251" + "@voidzero-dev/vite-plus-win32-x64-msvc": "npm:0.0.0-commit.1d7ba811f0e456640b0e761e8e8acf77d1ad3251" + oxfmt: "npm:=0.57.0" + oxlint: "npm:=1.72.0" + oxlint-tsgolint: "npm:=0.24.0" + vitest: "npm:4.1.9" + peerDependencies: + "@vitest/browser-playwright": 4.1.9 + "@vitest/browser-webdriverio": 4.1.9 dependenciesMeta: "@voidzero-dev/vite-plus-darwin-arm64": optional: true @@ -4701,17 +5111,91 @@ __metadata: optional: true "@voidzero-dev/vite-plus-win32-x64-msvc": optional: true + peerDependenciesMeta: + "@vitest/browser-playwright": + optional: true + "@vitest/browser-webdriverio": + optional: true bin: - oxfmt: bin/oxfmt - oxlint: bin/oxlint - vp: bin/vp - checksum: 10c0/11046fa8d253fae1b5eb1ce9e6995fae95610ac0cfc9a98c98dc15f0495bc6ede17e099e5676ed7b2531ad20a2e1a7bb944e09d96aa86173a4497d837aa62708 + oxfmt: ./bin/oxfmt + oxlint: ./bin/oxlint + vp: ./bin/vp + vpr: ./bin/vpr + checksum: 10c0/9a902884a81632bd6ff93f33048cbd72711b4530a8433db330767743527c942c09981ec3b51e6264850ca048068b958d39170b3afb957d7c166f04f879023b86 + languageName: node + linkType: hard + +"vitest@npm:4.1.9": + version: 4.1.9 + resolution: "vitest@npm:4.1.9::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fvitest%2F-%2Fvitest-4.1.9.tgz" + dependencies: + "@vitest/expect": "npm:4.1.9" + "@vitest/mocker": "npm:4.1.9" + "@vitest/pretty-format": "npm:4.1.9" + "@vitest/runner": "npm:4.1.9" + "@vitest/snapshot": "npm:4.1.9" + "@vitest/spy": "npm:4.1.9" + "@vitest/utils": "npm:4.1.9" + es-module-lexer: "npm:^2.0.0" + expect-type: "npm:^1.3.0" + magic-string: "npm:^0.30.21" + obug: "npm:^2.1.1" + pathe: "npm:^2.0.3" + picomatch: "npm:^4.0.3" + std-env: "npm:^4.0.0-rc.1" + tinybench: "npm:^2.9.0" + tinyexec: "npm:^1.0.2" + tinyglobby: "npm:^0.2.15" + tinyrainbow: "npm:^3.1.0" + vite: "npm:^6.0.0 || ^7.0.0 || ^8.0.0" + why-is-node-running: "npm:^2.3.0" + peerDependencies: + "@edge-runtime/vm": "*" + "@opentelemetry/api": ^1.9.0 + "@types/node": ^20.0.0 || ^22.0.0 || >=24.0.0 + "@vitest/browser-playwright": 4.1.9 + "@vitest/browser-preview": 4.1.9 + "@vitest/browser-webdriverio": 4.1.9 + "@vitest/coverage-istanbul": 4.1.9 + "@vitest/coverage-v8": 4.1.9 + "@vitest/ui": 4.1.9 + happy-dom: "*" + jsdom: "*" + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + "@edge-runtime/vm": + optional: true + "@opentelemetry/api": + optional: true + "@types/node": + optional: true + "@vitest/browser-playwright": + optional: true + "@vitest/browser-preview": + optional: true + "@vitest/browser-webdriverio": + optional: true + "@vitest/coverage-istanbul": + optional: true + "@vitest/coverage-v8": + optional: true + "@vitest/ui": + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vite: + optional: false + bin: + vitest: ./vitest.mjs + checksum: 10c0/1ac80ef4991be82822a52aea48415f1bc64ddf8fd88ee24c172ec368f1d480fefacbde622c3c951982f7961a1d07313e18deaafc774d29e42ad6f6ffa63334a7 languageName: node linkType: hard "which@npm:^2.0.1": version: 2.0.2 - resolution: "which@npm:2.0.2" + resolution: "which@npm:2.0.2::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fwhich%2F-%2Fwhich-2.0.2.tgz" dependencies: isexe: "npm:^2.0.0" bin: @@ -4722,7 +5206,7 @@ __metadata: "which@npm:^7.0.0": version: 7.0.0 - resolution: "which@npm:7.0.0" + resolution: "which@npm:7.0.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fwhich%2F-%2Fwhich-7.0.0.tgz" dependencies: isexe: "npm:^4.0.0" bin: @@ -4731,16 +5215,28 @@ __metadata: languageName: node linkType: hard +"why-is-node-running@npm:^2.3.0": + version: 2.3.0 + resolution: "why-is-node-running@npm:2.3.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fwhy-is-node-running%2F-%2Fwhy-is-node-running-2.3.0.tgz" + dependencies: + siginfo: "npm:^2.0.0" + stackback: "npm:0.0.2" + bin: + why-is-node-running: cli.js + checksum: 10c0/1cde0b01b827d2cf4cb11db962f3958b9175d5d9e7ac7361d1a7b0e2dc6069a263e69118bd974c4f6d0a890ef4eedfe34cf3d5167ec14203dbc9a18620537054 + languageName: node + linkType: hard + "wrappy@npm:1": version: 1.0.2 - resolution: "wrappy@npm:1.0.2" + resolution: "wrappy@npm:1.0.2::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fwrappy%2F-%2Fwrappy-1.0.2.tgz" checksum: 10c0/56fece1a4018c6a6c8e28fbc88c87e0fbf4ea8fd64fc6c63b18f4acc4bd13e0ad2515189786dd2c30d3eec9663d70f4ecf699330002f8ccb547e4a18231fc9f0 languageName: node linkType: hard -"ws@npm:^8.18.3": +"ws@npm:^8.19.0": version: 8.21.0 - resolution: "ws@npm:8.21.0" + resolution: "ws@npm:8.21.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fws%2F-%2Fws-8.21.0.tgz" peerDependencies: bufferutil: ^4.0.1 utf-8-validate: ">=5.0.2" @@ -4755,14 +5251,14 @@ __metadata: "yallist@npm:^5.0.0": version: 5.0.0 - resolution: "yallist@npm:5.0.0" + resolution: "yallist@npm:5.0.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fyallist%2F-%2Fyallist-5.0.0.tgz" checksum: 10c0/a499c81ce6d4a1d260d4ea0f6d49ab4da09681e32c3f0472dee16667ed69d01dae63a3b81745a24bd78476ec4fcf856114cb4896ace738e01da34b2c42235416 languageName: node linkType: hard "zod@npm:^4.4.3": version: 4.4.3 - resolution: "zod@npm:4.4.3" + resolution: "zod@npm:4.4.3::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fzod%2F-%2Fzod-4.4.3.tgz" checksum: 10c0/7ea31b558e88f9faf44f31dd185e2e1cbf51fed3081787fb96cc2534749b50c0acfc6da7f0922a7353ed092dd358c7d50c28ea96c94d04af64191bd33152eca3 languageName: node linkType: hard