OpenAI-Compatible Server
Metrale Engine serves via metrale-server — an OpenAI and Anthropic compatible HTTP API over axum. This chapter is the operator's reference for CLI flags, protocols supported, and the knobs that matter in production. The authoritative flag list is always met serve --help; the headings below match the groupings in the CLI so cross-referencing is easy.
CLI structure
met serve <MODEL> [--flags...]
met serve --model-from-path <PATH> [--flags...]
met --version
met --help
Every runtime configuration flag has a long-form name. Most are documented inline with #[arg] doc-strings in crates/server/src/cli/serve_args.rs.
Model selection and I/O
| Flag | Default | Notes |
|---|---|---|
MODEL (positional) | — | HF id (e.g. Sehyo/Qwen3.5-35B-A3B-NVFP4); resolves against ~/.cache/huggingface/hub |
--model-from-path | — | Local path; skips HF resolution entirely |
--model-name (alias --served-model-name) | config _name_or_path or MODEL | Override what /v1/models reports |
--cache-dir | $HF_HUB_CACHE, $HF_HOME/hub, ~/.cache/huggingface/hub | HF cache root |
--port | 8888 | HTTP listen port |
--no-fast-load | off (fast on) | Revert to mmap loader — the O_DIRECT + pipelined fast path is default |
Memory / budget
| Flag | Default | Notes |
|---|---|---|
--gpu-memory-utilization | 0.90 | Fraction of GPU memory Metrale Engine will claim |
--max-seq-len | 32768 | Maximum sequence length in tokens; sizes KV pool |
--max-batch-size | 8 | Max concurrent sequences per decode step |
--max-prefill-tokens | 8192 | Chunked-prefill budget per iteration; sizes scratch |
--max-num-seqs | 128 | Maximum queued sequences |
--oom-guard-mb | 4096 | Runtime safety reserve held back from the KV pool |
Production rule of thumb for tight single-GPU deployments of 100B+ models: drop --max-prefill-tokens to 2048 and --max-batch-size to 1. The default 8192 sizes the scratch arena, not the KV pool; tuning down frees hundreds of MB.
KV cache precision
| Flag | Default | Notes |
|---|---|---|
--kv-cache-dtype | fp8 | bf16, fp8, nvfp4, turbo2, turbo3, turbo4, turbo8, plus nine asymmetric K/V pairings — KvCacheDtype's FromStr (crates/cache/src/kv_cache.rs) is the authority on the accepted set |
--kv-high-precision-layers | 0 | Keep first/last N attention layers at BF16 (coherence protection). 0 does not mean "none" for every dtype — see below |
--fp8-kv-calibration-tokens | 0 | Online max-‖K‖/‖V‖ calibration for first N tokens (FP8 only) |
The flag takes a number or one of three words: auto (a fixed alias for 2,
not a heuristic) and max/all (every attention layer). Anything else that
fails to parse warns and falls back to 0.
--kv-high-precision-layers 0 is not "no promotion" for the turbo* family.
0 means defer to the per-dtype automatic value
(main_modules/kv_dtypes.rs::auto_high_precision_layers, applied at
serve_phases/kv_cache.rs). For bf16 / fp8 / nvfp4 the automatic value is
None, so 0 really is zero. For every turbo* and asymmetric variant it is
not: turbo2 and bf16k_turbo3v promote max(4, ⌈4·L/5⌉) layers, and all the
others promote max(2, ⌈L/3⌉) — roughly a third of attention layers forced to
BF16, which also shrinks the KV pool. Pass an explicit non-zero value if you want
to control it; there is no spelling of this flag that promotes nothing under a
turbo dtype.
See FP8 and NVFP4 for the trade-offs. Metrale Engine's recommendation per model family:
- Qwen3.5 family →
nvfp4KV. - Qwen3.6 / Nemotron-H →
fp8with calibration. - 122B-class (memory-constrained) →
nvfp4+--kv-high-precision-layers 2. - Everything else →
fp8(safe default).
Speculative decoding
| Flag | Default | Notes |
|---|---|---|
--speculative | off | Enable MTP — requires MTP weights in checkpoint |
--num-drafts | 1 | Draft tokens per verify (K = num_drafts + 1); default per-model from MODEL.toml |
--draft-confidence-stop | off | TAU in (0, 1): an MTP chain continues past a draft only while that draft's top-1 probability is at least TAU, so it ends with its first draft below TAU (still verified). Fewer drafter steps and verify rows on unconfident chains; emitted tokens unchanged. With --speculative only |
--mtp-quantization | bf16 | Must match main-model checkpoint (nvfp4, fp8, bf16) |
--mtp-vocab | 100000 | Limit MTP LM head to the first N token ids (0 = full vocab). The default is not 0: out of the box the draft head only scores ids 0..100000, clamped to the model's real vocab size |
--self-speculative | off | Layer-skipping drafter (no MTP weights required) |
--ngram-speculative | off | CPU-side n-gram matching |
--prompt-lookup-decoding | off | With --speculative: a sequence whose last --prompt-lookup-ngram tokens occurred earlier in its prompt or output verifies the tokens that followed them, in place of the MTP chain, for that round; no match leaves the round to MTP. Per sequence inside batched verify, up to --prompt-lookup-max-seqs active sequences |
--prompt-lookup-ngram | 4 | Match length a copy needs |
--prompt-lookup-max-drafts | 8 | Ceiling of the copy window (doubles after a fully accepted copy, halves after a broken one), 1..=16. A lone sequence verifies a copy of up to this length in one pass; inside a batch a copy is cut to 3 drafts (the batched verify's widest 4 rows). The first --prompt-lookup-max-seqs verify-pool slots are sized for it: about 64 MB per slot per draft above the MTP chain on Qwen3.6-35B-A3B, 152 MB on Qwen3.8-27B |
--prompt-lookup-min-match | 8 | Tokens a match must span, counted back from the sequence's end (at least --prompt-lookup-ngram); longer spans copy less often on prose |
--prompt-lookup-miss-backoff | 16 | After copies that matched nothing, skip copying for 1, 2, 4… rounds up to this many; any accepted copied token clears it. 0 never skips |
--prompt-lookup-max-seqs | 8 | Widest batch that proposes copies |
See the MTP deep dive. Use only one of --speculative,
--self-speculative, --ngram-speculative — but note this is guidance, not an
enforced constraint: none of the three carries a clap conflicts_with and
cli/validate.rs has no rule for the combination, so passing several parses and
serves. The scheduler then resolves them by silent precedence (ngram → self-spec
→ MTP) rather than rejecting the config. --dflash is enforced — it declares
conflicts_with = "speculative". --prompt-lookup-decoding is enforced too: it
requires --speculative and conflicts with --dflash, --self-speculative and
--ngram-speculative.
Prompt lookup only changes which drafts a round verifies. Every emitted token is still the target's own pick, so greedy output is the same with the flag on and off wherever verify rows are row-invariant (the canonical tiers).
--num-drafts is also not a plain constant: when it is still 1, the model's
MODEL.toml default_num_drafts replaces it (serve_phases/config.rs). On
qwen3.6-27b that is 3, i.e. K=4.
Boolean flags
Every boolean flag is presence-only and takes no value: --enable-prefix-caching
turns prefix caching on, and --enable-prefix-caching true is refused. A flag
is named for its non-default state, so a feature that is on by default is
turned off with --no-<feature> (--no-ssm-tail-midchunk,
--no-high-speed-swap-graph). A lever whose default is decided elsewhere — by
the compiled target, MODEL.toml or the environment — takes auto, on or
off instead: --ssm-batched-recurrent, --content-loop-watchdog and
--tool-grammar. In a recipe, a boolean key is written true or left out;
key: false restates the default and the recipe is refused.
Environment levers
Some settings are METRALE_* environment variables rather than flags. Every
one the engine reads is declared once in metrale_config::levers with its
type, default, the flag that shadows it (if any) and the file that reads it;
met dump-serve-options prints the list under levers. met refuses to
start when the environment carries a METRALE_* name that is not declared: it
is a typo or a lever that no longer exists, and running as if it applied would
mislabel whatever was measured. A recipe's env: block may name only a
runtime lever.
Scheduling / caching
| Flag | Default | Notes |
|---|---|---|
--enable-prefix-caching | off | RadixAttention + SSM snapshot cache (Marconi) |
--ssm-cache-slots | 16 | Concurrent SSM snapshot slots |
--ssm-checkpoint-interval | 256 | Blocks between SSM checkpoints |
--scheduler | fifo | fifo or slai (SLO-aware) |
--tbt-deadline-ms | 100 | SLAI decode deadline |
--auto-compact | off | Active context compression threshold (e.g. 0.75 = 75% of max-seq-len) |
Agent workloads (Claude Code, OpenCode): always enable --enable-prefix-caching and --scheduler slai. The prefix cache dominates wall-clock for system prompts + tool schemas; SLAI keeps streaming smooth under concurrent load.
Multi-GPU (Expert Parallelism)
| Flag | Default | Notes |
|---|---|---|
--rank | 0 | 0 = head (HTTP + scheduler); N > 0 = worker |
--world-size | 1 | Total ranks; 2 enables EP=2 |
--master-addr | 127.0.0.1 | Rendezvous host (e.g. head's IB IP) |
--master-port | 29500 | NCCL rendezvous port |
See Multi-GPU & EP=2 for the full setup, including the NCCL env vars that matter on GB10.
Reasoning / tools
| Flag | Default | Notes |
|---|---|---|
--disable-thinking | off | Kill-switch for <think> blocks |
--max-thinking-budget | from MODEL.toml | Per-request <think> token ceiling |
--tool-call-parser | MODEL.toml, else tool_defaults.toml by model_type | hermes, qwen3_coder, qwen3_xml, gemma4, mistral, minimax_xml, bare_json, poolside_v1 |
--tool-max-tokens | 8192 | Hard cap on the whole completion whenever tools are present — api/chat/sampling_setup.rs takes req.max_tokens.min(tool_max_tokens), covering prose and reasoning as well as tool arguments. Not a soft cap, and not scoped to arguments |
Observability / experimental
| Flag | Default | Notes |
|---|---|---|
--profile | off | Per-kernel sync + timing (disables CUDA graphs, +10% overhead) |
--adaptive-sampling | off | Entropy-gated greedy path |
--default-top-n-sigma | 1.0 | Default σ for top-n-sigma sampler |
--default-min-p | 0.08 | Default min-p |
--swap-space-gb | 3 | Disk-backed KV swap at /tmp/metrale-swap/ |
--request-timeout | 300 | Per-request seconds, 0 disables |
Endpoints
| Route | Protocol | Notes |
|---|---|---|
GET /v1/models | OpenAI | Returns one ModelInfo (Metrale Engine serves one model per process) |
POST /v1/chat/completions | OpenAI | Chat; streaming via SSE when stream: true |
POST /v1/completions | OpenAI (legacy) | Plain completion |
POST /v1/responses | OpenAI Responses | Stateful; supports conversation_id |
POST /v1/messages | Anthropic | Full Messages API with streaming |
POST /tokenize, /detokenize | helpers | Tokenizer round-trip; gated when --require-auth is set |
GET /health | internal | 200; used by benchmarks |
Rate limiting and auth
--require-auth(with--auth-token <key>or--auth-tokens-file <path>) — requires anAuthorization: Bearer <key>header on write endpoints. The presented token must match one of the loaded tokens (constant-time compare); there is no "accept any key" mode.- Token-bucket rate limiter per key (
crates/server/src/rate_limiter.rs). Off by default; enable by settingMETRALE_RATE_LIMIT_RPM(requests/min) and/orMETRALE_RATE_LIMIT_TPM(tokens/min) > 0 (bursts viaMETRALE_RATE_LIMIT_BURST_RPM/METRALE_RATE_LIMIT_BURST_TPM, default = the cap). A MAX_KEYS DoS guard bounds the key table. - Body-size limit env-configurable via
METRALE_MAX_BODY_BYTES(default 32 MiB —main_modules/serve_router.rs).
Changing the model without restarting
A running server can replace its model in place. Three routes reach it, and they share one code path:
- The dashboard.
met servewith no MODEL starts the listener and opens the Library, where a model and one of its recipes can be picked. Selecting one loads it and returns to the Main view. - The Library, on a server that is already serving. Same flow; the running model is released first.
- A client request naming a different model, Ollama-style — off unless
--auto-swapis passed.
For deployments where the served model is part of the contract, leave
--auto-swap off (the default): no client can then change what the endpoint
is running. There is no opposing --no-auto-swap; a boolean flag names only
its non-default state (see Boolean flags).
Even with --auto-swap, a swap needs a request whose model resolves to a
DIFFERENT model with a known recipe. A name that is absent, unrecognised, or
already live is served by the current model, exactly as before the flag
existed.
What a swap preserves. Stored responses and conversations, rate-limiter
buckets, the API-key policy and --dump all belong to the process, not the
model, and survive unchanged — including while no model is loaded, when
GET /v1/conversations/{id} still answers rather than reporting the model
missing.
What it cannot change. The listening socket is bound once for the process
lifetime, so a recipe's host/port are ignored with a warning and the model
serves on the address already bound. Hot-swap is single-node: a recipe needing
more than one rank is refused rather than half-applied.
While it runs. In-flight requests finish on the model they started on. New
requests that need a model get 503 model_not_loaded — retriable, and the same
shape /health already reports during startup. A load that fails restores the
previous model, and one this build has no kernels for is refused before
anything is released.
Chat templating
Tokenization uses the HF tokenizers crate plus minijinja for chat templates. Metrale Engine ships its own template overrides for a handful of models in jinja-templates/<model_type>.jinja when the upstream template has known issues (e.g. template-forced <think> seeding). The filename is the checkpoint's config.json model_type; --disable-template-overrides ignores it.
Observability
Prometheus-style metrics are exposed on /metrics (registered in crates/server/src/metrics.rs, plus the prefix-cache, token-entropy and kernel-lookup series that api/misc_handlers.rs appends). Among them:
metrale_requests_total,metrale_requests_activemetrale_time_to_first_token_seconds{model}metrale_prompt_tokens_total,metrale_generation_tokens_total,metrale_decoded_tokens_totalmetrale_spec_decode_verify_total{k, outcome}metrale_tool_calls_totalmetrale_prefix_cache_hits_total,metrale_prefix_cache_misses_total,metrale_prefix_cache_hit_rate
Structured logs go to stderr; they're the primary operational signal. Metrale Engine logs a brief line per completed request (model, prompt tokens, generated tokens, TTFT, elapsed, tools used). Per-token DECODE spam is deliberately not logged — it's useless.
A safe production config (Qwen3.5-35B, agents)
serve Sehyo/Qwen3.5-35B-A3B-NVFP4 \
--port 8888 \
--max-seq-len 16384 \
--max-batch-size 4 \
--kv-cache-dtype nvfp4 \
--gpu-memory-utilization 0.88 \
--scheduler slai \
--tbt-deadline-ms 100 \
--enable-prefix-caching \
--speculative --mtp-quantization nvfp4 \
--auto-compact 0.85 \
--adaptive-sampling
Claude Code uses 16k+ context for tool use; running with --max-seq-len 4096 will make agents fail mid-session. Always size up when running an agent workload.