diff --git a/client/src/components/settings/LocalLlmTab.jsx b/client/src/components/settings/LocalLlmTab.jsx index 9197b7a6f..6ccf2aac2 100644 --- a/client/src/components/settings/LocalLlmTab.jsx +++ b/client/src/components/settings/LocalLlmTab.jsx @@ -22,17 +22,36 @@ const BACKENDS = [ ]; const labelFor = (id) => BACKENDS.find((b) => b.id === id)?.label || id; -const DFLASH_PRESETS = [ +// llama.cpp exposes one `--spec-type` per drafter family. `draft-dspark` merged +// upstream on 2026-07-28 (ggml-org/llama.cpp#25173) and works on a stock +// `brew install llama.cpp`; `draft-dflash` covers DFlash v1, while the DFlash 2 +// modules are still an open PR (#27342) and need a from-source build. Hence the +// DSpark presets lead — see docs/research/2026-08-19-dspark-vs-dflash2.md. +const SPEC_DECODE_PRESETS = [ + { + id: 'qwen3.8-27b-dspark', + label: 'Qwen 3.8 27B + DSpark Drafter (Recommended — stock llama.cpp)', + model: 'models/Qwen3.8-27B-Instruct-Q4_K_M.gguf', + draftModel: 'models/Qwen3.8-27B-DSpark-bf16.gguf', + specType: 'draft-dspark', + }, + { + id: 'qwen3-8b-dspark', + label: 'Qwen 3 8B + DSpark Drafter (small target)', + model: 'models/Qwen3-8B-Instruct-Q4_K_M.gguf', + draftModel: 'models/dspark_qwen3_8b_block7-bf16.gguf', + specType: 'draft-dspark', + }, { id: 'qwen3.8-27b', - label: 'Qwen 3.8 27B + DFlash 2 Drafter (Recommended)', + label: 'Qwen 3.8 27B + DFlash 2 Drafter (needs llama.cpp PR #27342 build)', model: 'models/Qwen3.8-27B-Instruct-Q4_K_M.gguf', draftModel: 'models/Qwen3.8-27B-DFlash2-Q4_K_M.gguf', specType: 'draft-dflash', }, { id: 'muse-glimmer-30b', - label: 'Muse-Glimmer 30B + DFlash 2 Drafter', + label: 'Muse-Glimmer 30B + DFlash 2 Drafter (needs llama.cpp PR #27342 build)', model: 'models/Muse-Glimmer-30B-Instruct-Q4_K_M.gguf', draftModel: 'models/Muse-Glimmer-30B-DFlash2-Q4_K_M.gguf', specType: 'draft-dflash', @@ -42,7 +61,7 @@ const DFLASH_PRESETS = [ label: 'Custom GGUF / Manual Paths', model: '', draftModel: '', - specType: 'draft-dflash', + specType: 'draft-dspark', }, ]; @@ -411,7 +430,7 @@ export function LocalLlmTab() { const [llamaForm, setLlamaForm] = useState({ model: '', draftModel: '', - specType: 'draft-dflash', + specType: 'draft-dspark', port: 8080, host: '127.0.0.1', ctxSize: 32768, @@ -771,7 +790,7 @@ export function LocalLlmTab() { }; const handlePresetSelect = (presetId) => { - const preset = DFLASH_PRESETS.find((p) => p.id === presetId); + const preset = SPEC_DECODE_PRESETS.find((p) => p.id === presetId); if (!preset) return; if (preset.id !== 'custom') { setLlamaForm((prev) => ({ @@ -940,7 +959,7 @@ export function LocalLlmTab() {

- DFlash 2 enables multi-token speculative drafting (yielding 2.5–3× speedups on local models like Qwen 2.5 / Qwen 3.8 and Muse-Glimmer). You can launch and manage a local llama-server with DFlash 2 speculative drafting directly from PortOS and connect using the OpenCode llama TUI provider. + Speculative decoding pairs a small drafter with your target model for 2–3× faster generation at identical output. You can launch and manage a local llama-server from PortOS and connect using the OpenCode llama TUI provider. DSpark (draft-dspark) works on a stock brew install llama.cpp; the DFlash 2 presets need a from-source build of an unmerged llama.cpp branch.

{llamaStatus?.running ? ( @@ -952,7 +971,7 @@ export function LocalLlmTab() {

Base Model: {llamaStatus.config.model}

)} {llamaStatus.config?.draftModel && ( -

DFlash Drafter: {llamaStatus.config.draftModel} ({llamaStatus.config.specType || 'draft-dflash'})

+

Drafter: {llamaStatus.config.draftModel} ({llamaStatus.config.specType || 'draft-dflash'})

)} {llamaStatus.managed ? ( @@ -981,10 +1000,10 @@ export function LocalLlmTab() { id="llama-preset-select" aria-label="Preset" onChange={(e) => handlePresetSelect(e.target.value)} - defaultValue="qwen3.8-27b" + defaultValue="qwen3.8-27b-dspark" className="bg-port-card border border-port-border rounded px-2 py-1 text-xs text-port-accent focus:outline-none" > - {DFLASH_PRESETS.map((p) => ( + {SPEC_DECODE_PRESETS.map((p) => ( diff --git a/docs/README.md b/docs/README.md index e346d10a1..743fcd36e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -34,7 +34,7 @@ Start with the [product surface map](./features/product-surfaces.md) for a compl App management: [app-wizard](./features/app-wizard.md) · [autofixer](./features/autofixer.md) · [browser](./features/browser.md) · [error-handling](./features/error-handling.md) · [jira-sprint-manager](./features/jira-sprint-manager.md) -Chief of Staff: [chief-of-staff](./features/chief-of-staff.md) · [cos-agent-runner](./features/cos-agent-runner.md) · [cos-enhancement](./features/cos-enhancement.md) · [agent-context](./features/agent-context.md) · [agent-skills](./features/agent-skills.md) · [memory-system](./features/memory-system.md) · [claude-ollama](./features/claude-ollama.md) · [mtplx](./features/mtplx.md) · [dflash2](./features/dflash2.md) · [prompt-manager](./features/prompt-manager.md) +Chief of Staff: [chief-of-staff](./features/chief-of-staff.md) · [cos-agent-runner](./features/cos-agent-runner.md) · [cos-enhancement](./features/cos-enhancement.md) · [agent-context](./features/agent-context.md) · [agent-skills](./features/agent-skills.md) · [memory-system](./features/memory-system.md) · [claude-ollama](./features/claude-ollama.md) · [mtplx](./features/mtplx.md) · [dflash2](./features/dflash2.md) ([DSpark vs DFlash 2](./research/2026-08-19-dspark-vs-dflash2.md)) · [prompt-manager](./features/prompt-manager.md) Identity & self: [digital-twin](./features/digital-twin.md) · [identity-system](./features/identity-system.md) · [soul-system](./features/soul-system.md) · [privacy-center](./features/privacy-center.md) · [post](./features/post.md) (insights design spike: [plans/2026-06-03](./plans/2026-06-03-cross-domain-insights-engine.md)) diff --git a/docs/features/dflash2.md b/docs/features/dflash2.md index 8a3d032d7..51baab882 100644 --- a/docs/features/dflash2.md +++ b/docs/features/dflash2.md @@ -1,8 +1,12 @@ -# DFlash 2 Speculative Decoding — llama.cpp & OpenCode llama TUI +# Speculative Decoding (DSpark / DFlash 2) — llama.cpp & OpenCode llama TUI [DFlash 2](https://huggingface.co/z-lab) provides deep, ultra-fast block-level speculative drafting for large language models (such as Qwen 2.5, Qwen 3.8, and Muse-Glimmer). By pairing a small speculative drafter model (typically 1.5–3 GB) with a target foundation model (e.g. 27B–30B), DFlash 2 achieves 2.5–3× end-to-end token generation speedups without sacrificing output quality. -PortOS provides direct integration with DFlash 2 through the **OpenCode llama TUI** provider preset and local OpenAI-compatible inference servers like `llama-server`. +[DSpark](https://arxiv.org/html/2607.05147v1) (DeepSeek) is the sibling drafter family — same lossless block-drafting idea, a rank-256 Markov head instead of DFlash 2's candidate selector, and a much wider set of published drafters. + +PortOS integrates both through the **OpenCode llama TUI** provider preset and the managed local `llama-server`. + +> **Which one to run:** `--spec-type draft-dspark` **merged into llama.cpp on 2026-07-28** ([#25173](https://github.com/ggml-org/llama.cpp/pull/25173)) and works on a stock `brew install llama.cpp`. DFlash 2's engine modules are **still an open PR** ([#27342](https://github.com/ggml-org/llama.cpp/pull/27342)) and need a from-source build of that branch. llama.cpp falls back **silently** on a spec-type/drafter mismatch rather than erroring, so a DFlash 2 drafter on a stock build degrades quietly. Start with DSpark unless you have built the DFlash 2 branch. Full comparison: [DSpark vs DFlash 2](../research/2026-08-19-dspark-vs-dflash2.md). --- @@ -10,7 +14,7 @@ PortOS provides direct integration with DFlash 2 through the **OpenCode llama TU 1. **OpenCode llama TUI Provider**: - An attachable `tui` coding-agent provider preset (`opencode-llama-tui`) configured to connect to `http://127.0.0.1:8080/v1`. - - Seeded with default model aliases `["dflash", "qwen3.8-27b-dflash2", "Muse-Glimmer-30B-DFlash2"]` with default `dflash`. + - Seeded with default model aliases `["dflash", "qwen3.8-27b-dflash2", "Muse-Glimmer-30B-DFlash2"]` with default `dflash`. The launcher keeps `--alias dflash` for every drafter family so this alias resolves regardless of which one you run. - Fully enabled by default and equipped with OpenCode's agentic file-writing harness, tool calling, and session persistence. 2. **Model Refresh**: - Support for dynamic model discovery via the **Refresh Models** button on AI Providers, querying the local `llama-server` `/v1/models` endpoint. @@ -22,7 +26,21 @@ PortOS provides direct integration with DFlash 2 through the **OpenCode llama TU ## Setup & Running with llama-server ### 1. Download Base & Draft Models -Download your base GGUF and corresponding DFlash 2 drafter GGUF from Hugging Face: +Download your base GGUF and a matching drafter GGUF from Hugging Face. Drafter +checkpoints are **not standalone models** — they only produce text once an engine +pairs them with their specific target, which is why PortOS's model search filters +them out of the install picker. + +DSpark pairs (stock llama.cpp): + +- **Qwen 3.8 27B**: base `Qwen/Qwen3.8-27B-Instruct-GGUF` + drafter `DimInfer/Qwen3.8-27B-Dspark-v1` +- **Qwen 3 8B**: base `Qwen/Qwen3-8B-Instruct-GGUF` + drafter converted from `deepseek-ai/dspark_qwen3_8b_block7` + +DSpark drafters ship without tokenizers — converting one to GGUF requires passing +`--target-model-dir` so it reuses the target's tokenizer. Keep the drafter at bf16; +the target can be any quant. + +DFlash 2 pairs (require a source build of llama.cpp [#27342](https://github.com/ggml-org/llama.cpp/pull/27342)): - **Qwen 3.8 27B Draft Pair**: - Base: `Qwen/Qwen3.8-27B-Instruct-GGUF` (e.g. `Qwen3.8-27B-Instruct-Q4_K_M.gguf`) @@ -32,13 +50,14 @@ Download your base GGUF and corresponding DFlash 2 drafter GGUF from Hugging Fac - Drafter: `z-lab/Muse-Glimmer-30B-DFlash2-GGUF` ### 2. Launch llama-server -Start `llama-server` on loopback port `8080` with speculative decoding enabled: +Start `llama-server` on loopback port `8080` with speculative decoding enabled +(`--spec-type draft-dflash` for a DFlash drafter, `draft-dspark` for a DSpark one): ```bash llama-server \ -m models/Qwen3.8-27B-Instruct-Q4_K_M.gguf \ - --draft-model models/Qwen3.8-27B-DFlash2-Q4_K_M.gguf \ - --spec-type draft-dflash \ + --draft-model models/Qwen3.8-27B-DSpark-bf16.gguf \ + --spec-type draft-dspark \ --port 8080 \ --host 127.0.0.1 \ --alias dflash \ @@ -51,3 +70,17 @@ llama-server \ 2. Verify **OpenCode llama TUI** is enabled. 3. Click **Refresh Models** to pull the live aliases from `llama-server`, or use the default `dflash` model. 4. Select **OpenCode llama TUI** in the CoS task creator or terminal runner to execute coding and agent tasks with speculative acceleration. + +--- + +## Ollama + +Ollama's vendored llama.cpp engine has carried DSpark since **0.32.6** +([#17545](https://github.com/ollama/ollama/pull/17545)), but Ollama surfaces no +control for it — its server hardcodes `--spec-type draft-mtp` whenever +speculative decoding is on. Selecting DSpark requires overriding +`LLAMA_ARG_SPEC_TYPE` / `LLAMA_ARG_SPEC_DRAFT_MODEL` on the Ollama process and +pointing at a raw blob-store path; the one reported measurement through that +path was ~13%, far below what the drafter delivers natively. Tracked upstream in +[ollama#17016](https://github.com/ollama/ollama/issues/17016). **Use +`llama-server` for speculative decoding in PortOS, not the Ollama backend.** diff --git a/docs/research/2026-08-19-dspark-vs-dflash2.md b/docs/research/2026-08-19-dspark-vs-dflash2.md new file mode 100644 index 000000000..a878bdf1d --- /dev/null +++ b/docs/research/2026-08-19-dspark-vs-dflash2.md @@ -0,0 +1,178 @@ +# DSpark vs DFlash 2 — is DSpark worth adding alongside? + +Date: 2026-08-19 + +Follow-up to [DFlash 2 speculative decoding](./2026-08-19-dflash2-speculative-decoding.md), +prompted by [ARahim3/mlx-dspark](https://github.com/ARahim3/mlx-dspark). Two +questions: is DSpark worth adding alongside DFlash 2, and is there Ollama +support for it? + +## Verdict + +**Yes — but the thing worth adding is a preset, not a backend, and DSpark turns +out to be the *more* available of the two on the engine PortOS already ships.** + +The DFlash 2 note concluded "not adoptable today" partly because "PortOS +orchestrates no llama.cpp server." That is no longer true — PortOS now manages +`llama-server` directly (`server/services/llamaServerManager.js`, Settings → +Local LLMs), and its **Spec Type field is free text** that maps straight onto +llama.cpp's `--spec-type`. So the ranking flipped: + +| Drafter family | llama.cpp status | Reachable from PortOS today | +| --- | --- | --- | +| **DSpark** | [#25173 **merged 2026-07-28**](https://github.com/ggml-org/llama.cpp/pull/25173) | **Yes** — `brew install llama.cpp`, `--spec-type draft-dspark` | +| DFlash (v1) | merged | Yes | +| DFlash 2 | [#27342 **still open**](https://github.com/ggml-org/llama.cpp/pull/27342) (last activity 2026-08-19) | Only from a source build of the PR branch | + +PortOS's shipped presets pointed exclusively at DFlash 2 drafters — the one +family that needs an unmerged branch. DSpark needs **zero** new backend, zero +new dependency, and no server change: a `--spec-type` string and a drafter GGUF. +That is the whole cost, so it clears the surface-area bar the DFlash 2 note set. + +**`mlx-dspark` itself is not worth adopting as a backend** — see below. Its +value here was as a signpost to the merged llama.cpp path. + +## Is there Ollama support for DSpark? + +**Partially, and not in a form PortOS can use.** Precisely: + +- Ollama's **vendored llama.cpp engine has had DSpark since 0.32.6** + ([#17545](https://github.com/ollama/ollama/pull/17545), merged 2026-08-04) — + the engine can decode it. +- **Ollama surfaces no control for it.** `llm/llama_server.go` hardcodes + `--spec-type draft-mtp` whenever speculative decoding is on, so a Modelfile + `DRAFT` + `PARAMETER draft_num_predict` pairing selects MTP, never DSpark. +- The only working recipe is an **environment-variable override** on the Ollama + server process — `LLAMA_ARG_SPEC_TYPE=draft-dspark`, + `LLAMA_ARG_SPEC_DRAFT_N_MAX`, `LLAMA_ARG_SPEC_DRAFT_MODEL` pointed at a raw + **blob-store sha256 path**, with `DRAFT` set but `draft_num_predict` left + unset. The one reported measurement through that path was **~13%**, an order + of magnitude below the 2–3× the drafter delivers natively. +- The feature request, [ollama#17016](https://github.com/ollama/ollama/issues/17016) + (opened 2026-07-03), is **still open**. There is no `x/models/dspark` package + (404) to match the `x/models/dflash` one. + +A recipe that requires editing PortOS's own Ollama service environment and +hand-copying a content-addressed blob path is not something to wire into a UI. +**No Ollama work is warranted** — the DSpark path for PortOS is `llama-server`, +which PortOS already manages and where the flag is a first-class argument. + +## What DSpark is, versus DFlash 2 + +Both are lossless EAGLE-family block drafters: a small head proposes several +tokens, the target verifies them in one pass, and greedy output is unchanged. +They differ in how they fight *suffix decay* (later positions in a block are +guessed without knowing the earlier ones): + +- **DFlash 2** — block diffusion. Keeps the top-16 candidates per position and + adds a bilinear selector plus two-tap depthwise convolutions to trace one + coherent path. Both live in the engine, which is why #27342 is a real code + change and not just a checkpoint. +- **DSpark** (DeepSeek, [arXiv:2607.05147](https://arxiv.org/html/2607.05147v1)) + — a 5-layer parallel backbone over the target's hidden states plus a + **rank-256 Markov head** that conditions each drafted token on the previously + sampled one, and a confidence head for adaptive block length. In llama.cpp it + is layered on the already-merged DFlash drafter machinery, which is why it + landed as a smaller patch and merged first. + +Head-to-head on the same target (`mlx-dspark`'s M4 Pro benchmarks, +Qwen3.8-27B 8-bit) they are close — DFlash 2 measures **3.63×** and DSpark's +hybrid **2.72×**. DFlash 2 is the faster algorithm where it runs. Availability, +not throughput, is what separates them for PortOS today. + +DSpark also has far **wider target coverage**: DeepSeek ships official heads for +Qwen3 4B/8B/14B, and the community has published drafters for DeepSeek-V4-Flash, +Kimi-K3, GLM-5.2, Gemma-4, Nemotron, Muse-Glimmer and Bonsai. DFlash 2's +published set is much narrower. + +## Why `mlx-dspark` is not a backend PortOS should adopt + +[ARahim3/mlx-dspark](https://github.com/ARahim3/mlx-dspark) (MIT, ~450 stars, +active) is a genuinely nice piece of work: a native MLX port of both DSpark and +DFlash, a drafter registry that auto-resolves target→drafter pairs, per-machine +calibration, and an OpenAI- *and* Anthropic-compatible server. Adopting it would +still be wrong: + +- It is a **fourth local inference backend** (after Ollama, LM Studio, and + `llama-server`) — pip-installed, Apple-Silicon-only, Python ≥3.10, MLX + safetensors only, **no GGUF** — bought for a speedup PortOS can already get + through a backend it manages. That is the same trade the DFlash 2 note and the + [Qwen3.8 27B MLX evaluation](./2026-08-16-qwen38-mlx-macos.md) both declined. +- **A user who wants it needs nothing from PortOS.** `mlx-dspark serve` listens + on `127.0.0.1:8080` and speaks `/v1/chat/completions` and `/v1/models` — the + exact endpoint the shipped `opencode-llama-tui` provider preset already points + at. Stop `llama-server`, start `mlx-dspark serve`, hit **Refresh Models**. Any + integration PortOS wrote would duplicate that for no gain. +- Its own caveats narrow the win further: MoE targets (Nemotron 1.10×, + Qwen3.6-35B-A3B 1.32×) and 2-bit Bonsai (1.07×) barely clear break-even, and + hybrid targets can't batch. + +Its **drafter registry is worth borrowing as data, not code** — it is the best +published target→drafter mapping for both families, and the source for the +preset drafter names added here. + +## What shipped with this note + +Two changes, both small and both consequences of the investigation. + +### 1. DSpark presets on the llama-server launcher + +`client/src/components/settings/LocalLlmTab.jsx`: `DFLASH_PRESETS` → +`SPEC_DECODE_PRESETS`, with two `draft-dspark` presets leading (Qwen3.8-27B and +Qwen3-8B) and the DFlash 2 presets kept but relabelled with the build they +require. The default selection and the form's initial `specType` move to +`draft-dspark`, so the out-of-box preset is one a stock Homebrew `llama.cpp` +can actually run. No server change — `specType` was already a pass-through +string, and `alias` stays `dflash` so the `opencode-llama-tui` provider's +default model alias keeps resolving. + +Note for anyone reading the DFlash 2 presets: llama.cpp **silently falls back** +on a spec-type/drafter mismatch rather than erroring, so a DFlash 2 drafter run +against stock `--spec-type draft-dflash` degrades quietly instead of failing +loudly. The relabelled preset names are the warning. + +### 2. DSpark drafters filtered out of the MLX model search + +Same hazard the DFlash 2 note fixed, same class, new family. The MLX branch of +`server/services/huggingFaceCatalog.js` matched `mtp|dflash\d*|drafter` in the +repo name; `dspark` joins it, because a real mlx-community repo slips the tag +predicate: + +`mlx-community/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-DSpark-bf16` declares +`mlx`, `dspark` and `speculative-decoding` — **but no `draft-model` tag**. Under +the shipped filter PortOS offered it as a normal one-click LM Studio MLX +install, handing the user a 30B target's sidecar that cannot chat. Covered by a +new test in `huggingFaceCatalog.test.js` (verified failing before the fix). + +**Residual, accepted — the GGUF branch is left alone.** Sampling the 40 +most-downloaded `dspark` repos on Hugging Face, roughly half of the GGUF ones +carry no `draft-model`/`drafter` tag (`YanissAmz/DeepSeek-V4-Flash-DSpark-draft-GGUF`, +`Lucebox/DeepSeek-V4-Flash-0731-DSpark-GGUF`, `magnitudedev/Qwen3.8-27B-DSpark-GGUF`, +…), so the tag predicate misses them. Widening the GGUF filter to match `dspark` +in the *name* is what the DFlash 2 note explicitly rejected for this branch, and +the reason holds here: that space also contains repos whose names carry `dspark` +while shipping a complete model (abliterated/merged DeepSeek-V4-Flash variants), +so a name match would hide mainstream installs — the worse failure. The MLX +space stays curated enough for name matching; the GGUF long tail does not. + +## When to re-evaluate + +- **DFlash 2 merges into llama.cpp master (#27342).** Then both families run on + a stock build, DFlash 2's higher measured speedup becomes the reason to prefer + it, and the preset ordering here should flip back. This is the same trigger + already tracked in [#4568](https://github.com/atomantic/PortOS/issues/4568). +- **Ollama surfaces a real DSpark control** (closing + [ollama#17016](https://github.com/ollama/ollama/issues/17016) with a Modelfile + directive or a `spec_type` parameter, not the `LLAMA_ARG_*` workaround). Then + the Ollama backend — which serves the Claude-Ollama and OpenCode-Ollama + providers — gets the speedup too, and PortOS's work is again a catalog entry. + +## Sources + +- [ARahim3/mlx-dspark](https://github.com/ARahim3/mlx-dspark) — MLX port, benchmarks, drafter registry +- [DSpark paper — arXiv:2607.05147](https://arxiv.org/html/2607.05147v1) · [DSpark in SGLang](https://www.lmsys.org/blog/2026-07-06-dspark-sglang/) +- [llama.cpp #25173 — spec: add DSpark speculative decoding](https://github.com/ggml-org/llama.cpp/pull/25173) (merged 2026-07-28) +- [llama.cpp #27342 — spec: add DFlash2 support](https://github.com/ggml-org/llama.cpp/pull/27342) (open) +- [ollama #17016 — dspark option](https://github.com/ollama/ollama/issues/17016) · [ollama #17545 — llama.cpp update](https://github.com/ollama/ollama/pull/17545) (merged 2026-08-04) +- [DFlash 2 speculative decoding](./2026-08-19-dflash2-speculative-decoding.md) — the prior evaluation this revises +- [Qwen3.8 27B MLX options on macOS](./2026-08-16-qwen38-mlx-macos.md) — the original sidecar decision diff --git a/server/services/huggingFaceCatalog.js b/server/services/huggingFaceCatalog.js index 935ebb8dd..744c1f310 100644 --- a/server/services/huggingFaceCatalog.js +++ b/server/services/huggingFaceCatalog.js @@ -595,8 +595,12 @@ function hasDrafterTag(model) { // suffix reliably marks a sidecar — unlike the GGUF long tail above. `dflash` // joins the pre-existing `mtp`/`drafter` tokens because DFlash drafters ship as // MLX safetensors too (`jfan/Qwen3.8-27B-heretic-dflash`), and `\d*` covers the -// DFlash2 generation. -const MLX_DRAFTER_NAME_RE = /(?:^|[\/_-])(?:mtp|dflash\d*|drafter)(?:[\/_\-.]|$)/i +// DFlash2 generation. `dspark` is the same sidecar class from DeepSeek's +// drafter family — `mlx-community/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-DSpark-bf16` +// declares `mlx` + `dspark` + `speculative-decoding` but no `draft-model` tag, so +// the tag predicate alone would offer a 30B target's drafter as a one-click +// install (docs/research/2026-08-19-dspark-vs-dflash2.md). +const MLX_DRAFTER_NAME_RE = /(?:^|[\/_-])(?:mtp|dflash\d*|dspark|drafter)(?:[\/_\-.]|$)/i function isMlxDrafter(model) { return hasDrafterTag(model) || MLX_DRAFTER_NAME_RE.test(repoIdOf(model)) diff --git a/server/services/huggingFaceCatalog.test.js b/server/services/huggingFaceCatalog.test.js index 621c0d190..2a0218c85 100644 --- a/server/services/huggingFaceCatalog.test.js +++ b/server/services/huggingFaceCatalog.test.js @@ -724,6 +724,22 @@ describe('huggingFaceCatalog', () => { expect(results.some((r) => r.repository === drafterRepo)).toBe(false) }) + // Real listing: `mlx` + `dspark` + `speculative-decoding` tags, no + // `draft-model` tag — so only the repo-name match keeps it out. + it('does not offer a DSpark drafter published as MLX safetensors', async () => { + const drafterRepo = 'mlx-community/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-DSpark-bf16' + fetch.mockImplementation(urlRouter([ + ['filter=mlx', [mlxListing(drafterRepo, ['model.safetensors'])]], + ['filter=gguf', []], + ])) + + const results = await searchHuggingFaceModels({ + backend: 'lmstudio', query: 'nemotron', appleSilicon: true + }) + + expect(results.some((r) => r.repository === drafterRepo)).toBe(false) + }) + it('does not surface MLX on a non-Apple host even for LM Studio', async () => { const mlxRepo = 'mlx-community/Hidden-On-Intel-4bit' fetch.mockImplementation(urlRouter([