# ADR-0037: Pure-C Native ASR Backends Behind a Rust FFI Boundary on Non-Windows Targets

## Status

🔄 Superseded by [ADR-0038](adr-0038.md) (2026-06-13) — originally relaxed [ADR-0033](adr-0033.md) and extended [ADR-0035](adr-0035.md)

Voice functionality was extracted to the [omni-voice](https://github.com/rust-works/omni-voice)
repository and removed from omni-dev. The decision below is retained as a
historical record and continues to apply within omni-voice. See ADR-0038.

## Context

[ADR-0033](adr-0033.md) picked `candle` as the production ASR runtime for
[#802](https://github.com/rust-works/omni-dev/issues/802) under a
**no-C++ / pure-Rust-distribution** framing. That constraint is what ruled out
`whisper-rs` in the first place — it needs `cmake` plus a C++ toolchain — and
ADR-0033's own acceptance grep (`cargo tree | grep -iE 'cmake|cc-build|c\+\+'`)
encodes it. ADR-0033 already conceded one crack in the "no compiled deps at all"
story (the transitive `onig_sys` / `ring` C dependencies pulled through
`candle-core`), but it held the line on **C++**, **cmake**, and any *first-party*
native engine.

[ADR-0035](adr-0035.md) then established how additional ASR backends fit
alongside `candle`: OS-gating happens in the factory (not the Cargo dep graph),
every `BackendDescriptor` is registered on **every** host so `voice doctor`
enumerates the full set with per-host availability, and backend *integration
shape* is chosen per-backend from exactly **two** options — an in-process
cross-platform Rust crate, or a subprocess sidecar
([ADR-0028](adr-0028.md)'s pattern) for runtimes that are platform-gated,
Python-first, or otherwise awkward to link.

The [#930 Voxtral spike](https://github.com/rust-works/omni-dev/issues/930)
returned **GO**: Voxtral Realtime Mini 4B (Apache 2.0) is a strict streaming
upgrade over the shipped Parakeet path
([#898](https://github.com/rust-works/omni-dev/issues/898) /
[#901](https://github.com/rust-works/omni-dev/issues/901)). Measured on the
committed 5-min Sherlock fixture (MLX INT4, Apple Silicon):

| Metric                  | Parakeet (current) | Voxtral Realtime          |
| ----------------------- | ------------------ | ------------------------- |
| Streaming WER           | 9.19 %             | 2.84–3.15 % (= offline)   |
| Streaming-vs-batch gap  | ~7 pp              | ~0 pp                     |
| First-Partial latency   | ~12–18 s           | 0.64–0.91 s               |
| Prefix-loss (first ~13s) | drops ~27 words    | none (causal encoder)     |
| RTF                     | —                  | 0.44–0.53 (real-time)     |
| Peak RSS                | ~2.5 GB            | 4.55 GB                   |

The spike ranked three integration paths: (1) an **MLX-Python subprocess**
backend — ships now but adds a Python+MLX runtime dependency; (2) a **native
`voxtral.c` FFI** backend — Python-free but blocked by ADR-0033's no-C++/no-cmake
framing; (3) a **full candle port** — the pure-Rust endgame, ~4–8 weeks. Path (2)
is the **only** option that yields a native, Python-free Voxtral backend without
a multi-week port.

Follow-up Q&A during the spike sharpened the distinctions that make a *tightly
scoped* relaxation possible:

- [`antirez/voxtral.c`](https://github.com/antirez/voxtral.c) is **pure C** (zero
  `.cpp`), built with a **Makefile** — not cmake. It exposes a clean
  `vox_stream_*` C streaming API, a trivial `bindgen` target. Its BLAS path is
  C + Accelerate/OpenBLAS (macOS + Linux); its fast path adds **Objective-C +
  Metal** (macOS only).
- `mlx-rs` (Rust → MLX) is **not** unblocked by a C-only relaxation: its core
  (`libmlx`, `metal-cpp`) is **C++** + cmake, and MLX is **Apple-only**.
- Native Voxtral is effectively Apple-Silicon-gated for the *fast* path; Linux
  gets the slower C + OpenBLAS path; **Windows gets none** (the Metal fast path is
  Apple-only and the project will not take on a Windows native-toolchain
  requirement).

So the question this ADR answers is narrow: **may a first-party, pure-C native
engine enter the workspace behind an FFI boundary — and on which platforms —
without reopening the C++/cmake door that ADR-0033 deliberately shut?** ADR-0035's
integration taxonomy has no slot for it: an in-process *native-C FFI* backend is
neither a cross-platform Rust crate nor a subprocess sidecar.

## Decision

A single coupled decision in four parts.

### 1. Scoped relaxation of ADR-0033's toolchain constraint

Pure-**C** native dependencies — compiled with **`cc` and/or `make`** — are
permitted **only behind a Rust FFI boundary** and **only on
`cfg(not(target_os = "windows"))`** targets. **C++ remains forbidden on all
platforms**, and **`cmake` remains forbidden**: `voxtral.c` builds with a plain
Makefile, cmake was half the original `whisper-rs` blocker, and admitting it would
broaden the relaxation past anything the engine needs. macOS may **additionally**
use **Objective-C + Metal** for the GPU fast path — Objective-C is distinct from
C++, and the spike's RTF / latency wins depend on the Metal path. Linux uses
C + OpenBLAS.

The pure-Rust `candle` backend (ADR-0033) **remains the mandatory cross-platform
baseline and the only real ASR backend compiled on Windows** (alongside the
`mock` test placeholder) — it is what ADR-0035 §3's auto-upgrade hierarchy falls
through to wherever a faster backend is unavailable. So every supported platform
retains a working ASR with **no native-toolchain requirement**. The relaxation
strictly *adds* an opt-in native option on non-Windows; it removes nothing.

This is deliberately tighter than the `whisper-rs` motivation ADR-0033 rejected:
no C++, no cmake, no libstdc++/libc++. The line that moves is "no *first-party*
native engine at all" → "a pure-C first-party engine, behind FFI, off Windows."

### 2. A third integration shape, extending ADR-0035

ADR-0035's per-backend integration taxonomy gains a **third shape** alongside
"in-process cross-platform Rust crate" and "subprocess sidecar":

> **In-process native-C FFI behind a target-cfg dependency.** The native engine
> is linked into the binary (not a subprocess) but only on matching targets; the
> Cargo dependency lives under
> `[target.'cfg(...)'.dependencies]` so non-matching builds never reference it.

This shape inherits ADR-0035's **cross-platform-descriptor contract verbatim**:
the Voxtral `BackendDescriptor` is registered **on every host, including
Windows**. On Windows (and any target where the native dep is absent) the
descriptor's construction arm `cfg`-routes to a fixed "not available on this
host" error — exactly the "in-process target-gated backend" mechanism ADR-0035
§1 already describes — so `voice doctor` still lists Voxtral with a clear
unavailable-reason rather than silently omitting it.

**Why in-process, not a sidecar.** ADR-0035 §2 sends platform-gated /
Python-first runtimes to a subprocess sidecar; ADR-0028 is the precedent for
isolating *risky or non-portable* backends out-of-process. A native C library
with a small `vox_stream_*` surface is neither Python-first nor in need of OS-level
sandboxing the way the `claude-cli` backend was — it is a linkable artifact with a
C ABI. Linking it in-process avoids a sidecar's spawn tax and IPC framing for a
latency-sensitive streaming path, at the cost of a new in-binary native attack
surface (addressed by a `security-review`, below). The trade-off is recorded here
so the choice is not re-litigated per backend.

### 3. Windows behaviour is graceful fallback, not failure

The Voxtral backend is `cfg`-excluded on Windows; the factory's auto-upgrade
hierarchy (ADR-0035 §3) falls through to `candle`. Requesting Voxtral on Windows
is a clear **config error surfaced at construction time**, never a build break.
The Windows default build provably needs no C toolchain — Phase 1's CI guard
(out of scope here) makes that an enforced invariant rather than a claim.

### 4. Engine: vendor `antirez/voxtral.c` behind a `voxtral-sys` crate

The native engine is `antirez/voxtral.c`, vendored behind a `voxtral-sys` FFI
crate. The crate mechanics — vendoring method, `build.rs` (whether it shells to
`make` or rebuilds via the `cc` crate, both permitted by part 1), `bindgen` over
`voxtral.h`, the safe RAII wrapper — are **implementation, deferred to the
follow-up phases of [#933](https://github.com/rust-works/omni-dev/issues/933)**;
this ADR fixes only the engine choice and the boundary it must respect.

**Rejected alternatives:**

- **`mlx-rs`** — C++ (`libmlx` / `metal-cpp`) + cmake, and Apple-only. A C-only
  relaxation does not reach it, and admitting it would reopen the exact C++/cmake
  door this ADR keeps shut.
- **MLX-Python subprocess backend** — viable and ships sooner, but adds a Python
  runtime dependency. It is the ADR-0035 sidecar shape and is tracked as a
  separate, non-superseded sibling.
- **Full candle port** — the pure-Rust endgame, but ~4–8 weeks. It remains the
  long-term target; this decision is the bridge to it, not a replacement for it.

## Consequences

### Positive

- **Native, Python-free Voxtral without a multi-week port.** Path (2) becomes
  available; the project gets the spike's streaming-quality and latency wins
  without taking on a Python+MLX runtime dependency or waiting on the candle port.
- **Reuses ADR-0035 wholesale.** Descriptor enumeration, factory OS-gating, the
  `voice doctor` probe, and the auto-upgrade fallback all apply unchanged; the new
  shape slots into the existing seam rather than inventing a parallel one.
- **Windows stays toolchain-free and provably so.** `candle` remains the only
  real ASR backend on Windows; the Windows default build needs no C compiler or cmake, an
  invariant Phase 1 CI will enforce (`cargo tree` on the Windows target asserting
  `voxtral-sys` absent).
- **Strictly tighter than the rejected `whisper-rs` blocker.** No C++, no cmake,
  no C++ standard library — the relaxation admits only what `voxtral.c` actually
  requires, and only off Windows.

### Negative

- **New in-binary native attack surface.** A vendored C engine linked in-process
  is FFI-`unsafe` and inside the binary's trust boundary (unlike an ADR-0028
  sidecar). A **`security-review` of the vendored C is mandatory** before the
  backend ships; the upstream author describes the project as "not yet production
  quality," which raises both the review bar and the ongoing-maintenance cost of
  the vendored snapshot.
- **Non-Windows builds with the feature enabled require a C toolchain.** The
  backend is off-by-default and platform-gated, so default builds are unaffected,
  but enabling it reintroduces the C-toolchain build requirement ADR-0033 noted
  for `onig_sys`/`ring` — now first-party and load-bearing rather than transitive.
- **Per-host quality differences widen.** ADR-0035 already accepted that default
  transcripts can differ across hosts; adding an Apple-Silicon-fast / Linux-slow /
  Windows-absent backend deepens that. Mitigated by the same levers: `for_tests()`
  pins `mock`, explicit `--backend` pins reproducibility, and `voice doctor` makes
  the difference inspectable.
- **~8.9 GB bf16 weights (no INT4 in `voxtral.c`).** Much larger disk and RSS than
  the MLX INT4 path (~3 GB). Accepted for v1; if an INT4 C path lands upstream it
  becomes a model-management change, not an ADR change.

### Neutral

- **Objective-C is permitted on macOS, explicitly distinct from the C++ ban.**
  The fast path uses Obj-C + Metal; this does not loosen the C++ prohibition,
  which holds on every platform including macOS.
- **`cmake` stays forbidden.** The relaxation is `cc` + `make` only; a future
  engine needing cmake would require its own ADR.
- **The backend is an off-by-default opt-in.** Like ADR-0033's original
  `whisper-candle` posture, the native path requires explicit enabling; nothing
  about default behaviour on any host changes when the feature is compiled out.
- **ADR-0033 and ADR-0035 remain in force.** ADR-0033's `candle` decision and
  ADR-0035's OS-gating architecture are unchanged; this ADR sits above ADR-0033's
  toolchain framing (scoping an exception) and extends ADR-0035's taxonomy (adding
  a shape). Neither is superseded.

## Out of Scope (Tracked Elsewhere)

- **The MLX-Python subprocess backend and the full candle port** — separate,
  non-superseded siblings from the #930 spike.
- **Permitting C++ or unblocking `mlx-rs`** — explicitly *not* part of this
  relaxation.
- **Voxtral translation / audio-Q&A modes** — STT only.
- **Windows native Voxtral** — excluded by design.
- **All Phase 1–5 mechanics of [#933](https://github.com/rust-works/omni-dev/issues/933)** —
  the off-by-default `voxtral` Cargo feature and `unexpected_cfgs` registration,
  the `[target.'cfg(...)'.dependencies]` stanza, the `voxtral-sys` crate /
  `build.rs` / `bindgen` wrapper, the Windows-exclusion CI guards, the
  `VoxtralBackend` trait impls, the CLI surface and `update-snapshots` run, and the
  ~8.9 GB model-fetch story. This ADR fixes the *decision and its boundary*; the
  build, code, and CI land in the implementing phases.

## Coverage expectations

Mirrors the by-design gaps in [ADR-0031](adr-0031.md), [ADR-0033](adr-0033.md),
and [ADR-0035](adr-0035.md). The native FFI inference path cannot run in CI
without a staged ~8.9 GB model **and** a native build on a matching target, so it
is uncovered by design, exactly as `CandleTranscriber`'s model-bound code is.

The path that **is** coverable and **must** be tested is the
**Windows-fallback-to-`candle`** behaviour: with Voxtral requested on a target
where it is `cfg`-excluded, the factory yields `candle` (or surfaces the
construction-time "not available on this host" error), and transcription still
works. This is the in-process unsupported-host construction arm ADR-0035 already
requires to be covered, applied to the new shape.

## References

- Issue [#933](https://github.com/rust-works/omni-dev/issues/933) — this
  relaxation and the phased Voxtral integration plan.
- Issue [#930](https://github.com/rust-works/omni-dev/issues/930) — the Voxtral
  spike that produced the GO and the three integration paths.
- Issue [#807](https://github.com/rust-works/omni-dev/issues/807) — `voice listen`,
  the product user of this backend.
- Issue [#806](https://github.com/rust-works/omni-dev/issues/806) — streaming-ASR
  umbrella.
- Issues [#898](https://github.com/rust-works/omni-dev/issues/898) /
  [#901](https://github.com/rust-works/omni-dev/issues/901) — the Parakeet
  streaming baseline being upgraded.
- [ADR-0033](adr-0033.md) — `candle` as the production ASR runtime; this ADR
  scopes a C-only exception to its toolchain framing.
- [ADR-0035](adr-0035.md) — OS-gated ASR backends; this ADR adds a third
  integration shape to its taxonomy and reuses its descriptor contract.
- [ADR-0028](adr-0028.md) — sandboxed subprocess pattern; the precedent for
  isolating non-portable backends, and the contrast that justifies linking this
  one in-process.
- [ADR-0031](adr-0031.md) — the original by-design coverage-gap pattern.
- Upstream engine: [`antirez/voxtral.c`](https://github.com/antirez/voxtral.c).
