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

FlagDefaultNotes
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 MODELOverride what /v1/models reports
--cache-dir$HF_HUB_CACHE, $HF_HOME/hub, ~/.cache/huggingface/hubHF cache root
--port8888HTTP listen port
--no-fast-loadoff (fast on)Revert to mmap loader — the O_DIRECT + pipelined fast path is default

Memory / budget

FlagDefaultNotes
--gpu-memory-utilization0.90Fraction of GPU memory Metrale Engine will claim
--max-seq-len32768Maximum sequence length in tokens; sizes KV pool
--max-batch-size8Max concurrent sequences per decode step
--max-prefill-tokens8192Chunked-prefill budget per iteration; sizes scratch
--max-num-seqs128Maximum queued sequences
--oom-guard-mb4096Runtime 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

FlagDefaultNotes
--kv-cache-dtypefp8bf16, 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-layers0Keep first/last N attention layers at BF16 (coherence protection). 0 does not mean "none" for every dtype — see below
--fp8-kv-calibration-tokens0Online 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 → nvfp4 KV.
  • Qwen3.6 / Nemotron-H → fp8 with calibration.
  • 122B-class (memory-constrained) → nvfp4 + --kv-high-precision-layers 2.
  • Everything else → fp8 (safe default).

Speculative decoding

FlagDefaultNotes
--speculativeoffEnable MTP — requires MTP weights in checkpoint
--num-drafts1Draft tokens per verify (K = num_drafts + 1); default per-model from MODEL.toml
--draft-confidence-stopoffTAU 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-quantizationbf16Must match main-model checkpoint (nvfp4, fp8, bf16)
--mtp-vocab100000Limit 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-speculativeoffLayer-skipping drafter (no MTP weights required)
--ngram-speculativeoffCPU-side n-gram matching
--prompt-lookup-decodingoffWith --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-ngram4Match length a copy needs
--prompt-lookup-max-drafts8Ceiling 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-match8Tokens 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-backoff16After 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-seqs8Widest 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

FlagDefaultNotes
--enable-prefix-cachingoffRadixAttention + SSM snapshot cache (Marconi)
--ssm-cache-slots16Concurrent SSM snapshot slots
--ssm-checkpoint-interval256Blocks between SSM checkpoints
--schedulerfifofifo or slai (SLO-aware)
--tbt-deadline-ms100SLAI decode deadline
--auto-compactoffActive 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)

FlagDefaultNotes
--rank00 = head (HTTP + scheduler); N > 0 = worker
--world-size1Total ranks; 2 enables EP=2
--master-addr127.0.0.1Rendezvous host (e.g. head's IB IP)
--master-port29500NCCL rendezvous port

See Multi-GPU & EP=2 for the full setup, including the NCCL env vars that matter on GB10.

Reasoning / tools

FlagDefaultNotes
--disable-thinkingoffKill-switch for <think> blocks
--max-thinking-budgetfrom MODEL.tomlPer-request <think> token ceiling
--tool-call-parserMODEL.toml, else tool_defaults.toml by model_typehermes, qwen3_coder, qwen3_xml, gemma4, mistral, minimax_xml, bare_json, poolside_v1
--tool-max-tokens8192Hard 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

FlagDefaultNotes
--profileoffPer-kernel sync + timing (disables CUDA graphs, +10% overhead)
--adaptive-samplingoffEntropy-gated greedy path
--default-top-n-sigma1.0Default σ for top-n-sigma sampler
--default-min-p0.08Default min-p
--swap-space-gb3Disk-backed KV swap at /tmp/metrale-swap/
--request-timeout300Per-request seconds, 0 disables

Endpoints

RouteProtocolNotes
GET /v1/modelsOpenAIReturns one ModelInfo (Metrale Engine serves one model per process)
POST /v1/chat/completionsOpenAIChat; streaming via SSE when stream: true
POST /v1/completionsOpenAI (legacy)Plain completion
POST /v1/responsesOpenAI ResponsesStateful; supports conversation_id
POST /v1/messagesAnthropicFull Messages API with streaming
POST /tokenize, /detokenizehelpersTokenizer round-trip; gated when --require-auth is set
GET /healthinternal200; used by benchmarks

Rate limiting and auth

  • --require-auth (with --auth-token <key> or --auth-tokens-file <path>) — requires an Authorization: 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 setting METRALE_RATE_LIMIT_RPM (requests/min) and/or METRALE_RATE_LIMIT_TPM (tokens/min) > 0 (bursts via METRALE_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 serve with 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-swap is 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_active
  • metrale_time_to_first_token_seconds{model}
  • metrale_prompt_tokens_total, metrale_generation_tokens_total, metrale_decoded_tokens_total
  • metrale_spec_decode_verify_total{k, outcome}
  • metrale_tool_calls_total
  • metrale_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.