Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/nornir

.nornir/generator-backends.md

Generator backends — the generative-LLM abstraction (EPIC #39)

One Generator trait, three real implementations (candle, mistralrs, onnx) behind a factory, plus a test mock and an off-by-default ollama client. This is how nornir generates text locally without depending on ollama, and how the H5 bake-off (#46) and the funnel intake (#45) get a pluggable model call.

Source: src/warehouse/generator/ (mod.rs + candle.rs + mistralrs.rs + onnx.rs + ollama.rs).


The trait

pub trait Backend {
    fn id(&self) -> &str;        // spec id, e.g. "candle:qwen2-0.5b", "mock"
    fn available(&self) -> bool; // runtime probe of lib + model presence
}

pub trait Generator: Backend + Send + Sync {
    fn complete(&self, req: &GenRequest) -> Result<GenAnswer>;
}

Request / answer

GenRequest { prompt, system, max_tokens, temperature, stop } — the inputs every backend needs; a backend ignores knobs it can't honor rather than erroring. Builder helpers: GenRequest::new(prompt).with_system(..).with_max_tokens(..) .with_temperature(..).with_stop(..).

GenAnswer { text, tokens_in, tokens_out, tokens_per_s, latency_ms } — mirrors the exact fields the bake-off's ModelAnswer needs. GenAnswer::into_model_answer() slots it straight into an agent_model_runs row (local generation is free → cost_usd = 0.0, mcp_tool_calls = 0, score = 0.0 left for a judge).


The factory

pub fn generator(spec: &str) -> Result<Box<dyn Generator>>

spec is <backend>:<model> (or bare mock):

spec form backend feature gate
candle:<model> candle gen-candle
mistralrs:<model> mistral.rs gen-mistralrs
onnx:<model> ONNX/ORT gen-onnx
ollama:<model> ollama HTTP gen-ollama ⚠ NOT one of the 3
mock / mock:<id> mock always on

Each real arm is #[cfg(feature = …)]-gated:

available() runtime-probes so a missing backend degrades gracefully:


CLI / env (CLI-parity LAW)

nornir bakeoff run --task "…" --models a,b --gen candle:qwen2-0.5b
NORNIR_GEN_BACKEND=mistralrs:Qwen/Qwen2.5-0.5B-Instruct nornir bakeoff run …

Priority: --gen <spec> > $NORNIR_GEN_BACKEND (spec_from_env) > --mock > prior-art ollama caller. When a --gen spec is given the CLI prints the resolved backend id + its available() state, then drives the SAME selected model across every matrix cell via BoxGeneratorCaller (a ModelCaller adapter) through the unchanged run_bakeoff_matrix pipeline.


The three backends (real call paths)

candle (gen-candle) — pure Rust, no C

candle-core + candle-transformers. Loads a quantized GGUF Qwen2-family model (quantized_qwen2::ModelWeights::from_gguf) + its tokenizer.json (fetched once from the HF hub via hf-hub). complete runs a real loop: encode → prefill forward → LogitsProcessor::sample → autoregressive decode until the token cap / EOS (<|im_end|>) / a stop sequence. Presets: qwen2-0.5b (default) and qwen2-1.5b.

mistralrs (gen-mistralrs) — candle-based, in-process

The mistralrs crate. TextModelBuilder::new(model_id).with_isq(Q4K).build() builds the in-process engine (fetches weights itself); complete sends a RequestBuilder chat request (system + user turns, temperature + max-len) via model.send_chat_request, then reads choices[0].message.content + usage.{prompt,completion}_tokens. Async engine driven on a current-thread tokio runtime so the trait stays sync.

onnx (gen-onnx) — ORT (onnxruntime-genai-style)

The ort stack + tokenizers. Points at a model dir holding model.onnx + tokenizer.json (a HF Optimum/genai export; $NORNIR_GEN_ONNX_DIR overrides an empty spec). complete builds an ORT Session and runs the genai decode loop: tokenize → session.run(input_ids, attention_mask) → take the last-position logits → greedy argmax → append → repeat until cap / EOS / stop. Links onnxruntime (C++) — the same accepted exception as embed-ort.

ollama (gen-ollama) — NOT one of the three

Off by default. The prior-art {host}/api/generate HTTP client adapted to the trait, kept as a convenience (--gen ollama:<tag>). Pulls no new dependency (reuses the vendored ureq). The original OllamaCaller / ModelCaller / MockCaller in agent_model_runs.rs are untouched so bakeoff run without --gen behaves exactly as before.


How it feeds the rest


Tests (inject-assert LAW)