sglang-model-gateway — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited sglang-model-gateway (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Target audience: operators running vLLM and/or SGLang inference on Kubernetes, fronting workers with a router that does cache-aware load-balancing, optional prefill-decode disaggregation, and dynamic worker registration. Especially: hosting multiple replicas of the same model behind one address, in air-gapped clusters with local model mirrors (no live huggingface.co).
A single vLLM or SGLang process serves one Pod. To scale beyond one GPU, operators either fan-out replicas (N Pods, one Service) or run engine-internal data-parallelism (vllm serve --data-parallel-size N). Plain Kubernetes Service round-robins requests, which fragments per-replica prefix caches — every replica builds its own copy of the same prefix and hit rate divides by ~N. The Model Gateway is a Rust router that recovers most of that with a cache-aware policy (steers same-prefix requests to the same replica), adds health checks / circuit breakers / retries, exposes a unified OpenAI-compatible endpoint regardless of backend, and integrates with Kubernetes service discovery via label selectors. It also handles prefill-decode disaggregation when phases are split across worker pools.
This skill stays inside the router's territory. Defer to:
/dev/shm emptyDir, initialDelaySeconds: 600 (matches VLLM_ENGINE_READY_TIMEOUT_S), RHAIIS root-UID gotcha, LWS multi-node, container image selection.HF_HUB_OFFLINE, TRANSFORMERS_OFFLINE, HF_ENDPOINT (no-trailing-slash gotcha), VLLM_USE_MODELSCOPE, HF_TOKEN for gated-offline, telemetry triple-opt-out.vllm:* metric semantics (the gateway only adds smg_*).smg_router_request_queue_length or smg_inference_requests_running.The project was renamed in Dec 2025:
| Field | Old (pre Dec 2025) | Current (April 2026) |
|---|---|---|
| Source directory | sgl-router/ | sgl-model-gateway/ |
| Rust crate | sglang-router | sgl-model-gateway |
| Binary names | sglang-router | sgl-model-gateway, smg, amg (3 aliases) |
| Docker image | lmsysorg/sglang-router:* | lmsysorg/sgl-model-gateway:* (current :v0.3.2) |
| Prometheus metric prefix | sgl_router_* | smg_* |
| Release tag prefix | router-vX.Y.Z | gateway-vX.Y.Z |
Release cadence, measured 2026-07-21. Newest gateway-* git tag is gateway-v0.3.1 (2026-01-09) — over six months old. Newest published image is v0.3.2 / latest, both last built 2026-05-27; Docker Hub carries only four tags total. Meanwhile sgl-model-gateway/ on main is still active, with commits through 2026-07-03 (PD-router cancel-paired-decode, DP-aware PD dispatch #26245, a PD cache-aware routing fix, a cargo-workspace restructure). The gap matters operationally: recent work is not consumable from a published artifact. Anyone who needs those fixes builds from source, and anyone running :v0.3.2 is pinned to a 2026-05-27 snapshot no matter how busy the repo looks. Re-measure before assuming either direction — see references/sources.md. | Python launcher module | sglang_router | sglang_router (not renamed) |
The Python entry point python3 -m sglang_router.launch_router still works. CLI flags are unchanged across the rename. *Dashboards on `sgl_router_` go silently empty after upgrading** — fix the metric prefix in Grafana dashboards and Prometheus alerts.
PR refs: sgl-project/sglang#14283 (crate rename), sgl-project/sglang#14312 (directory rename).
The gateway is a stateless Rust process that accepts OpenAI-compatible HTTP and native gRPC traffic on a front port, maintains a registry of healthy backend workers, and forwards each request to one worker chosen by a policy. Six policies are selectable via --policy: cache_aware (default — radix-tree prefix matching on raw text, no tokenizer needed), random, round_robin, power_of_two, prefix_hash (xxh3 over the first 256 token IDs against a consistent-hash ring — needs a working tokenizer), and manual. Two more — consistent_hashing (deterministic hash-ring) and bucket — exist in the policy factory but are not in the --policy value_parser, so they are constructed at runtime rather than chosen by the CLI flag (src/policies/factory.rs:77-91). Workers are added either statically (--worker-urls), dynamically (POST /workers), or via Kubernetes service discovery (--service-discovery --selector key=value --service-discovery-namespace ns). The internal WorkerType enum has three variants — Regular, Prefill, Decode (PD-disagg) — and the RuntimeType enum has three — Sglang, Vllm, External (OpenAI-compatible non-local). Connection mode is Http or Grpc. The gRPC path hard-requires a HuggingFace tokenizer (so tiktoken-only models like Kimi K2/K2.6 must use HTTP). A separate Prometheus exporter on --prometheus-port (default 29000) emits 40+ smg_* metrics. Source: sgl-model-gateway/src/core/worker.rs, src/core/steps/worker/local/discover_metadata.rs, src/policies/, src/server.rs, src/service_discovery.rs.
"Stateless" holds for chat traffic only. If clients use /v1/responses or /v1/conversations, the gateway keeps that conversation state in a --history-backend, which defaults to in-process memory — so a gateway restart or a second replica loses or splits it. Anyone running more than one replica, or serving stateful endpoints at all, needs references/history.md before choosing a backend.
The single most-asked question is "how do I put N vLLM replicas behind one sgl-model-gateway?" There are three viable paths, and choosing the wrong one wastes a day:
Worker condition → Path
├── On a vLLM gRPC fork (CatherineSue/vllm or upstream once it lands)→ Path A: vLLM-gRPC native (best, currently requires the fork build)
├── Vanilla vLLM (HTTP), basic load-balancing only → Path B: HTTP transparent proxy via --worker-urls
└── Vanilla vLLM, internal DP within one Pod → Path C: vLLM internal DP-LB (no gateway needed for fan-out)Requires a vLLM build with the gRPC server (PR sgl-project/sglang#13120 references upstream CatherineSue/vllm#2). When available, the gateway treats vLLM workers as a typed RuntimeType::Vllm over Connection::Grpc. Auto-detect probes SGLang first, falls back to vLLM. Pin explicitly with --runtime vllm when registering. Limitations from #13120: no n>1, no logprobs, no multimodal, no LoRA, no PD-disaggregation with vLLM workers. If those features are required, stay on SGLang workers for now.
--worker-urls or service discovery (the realistic vanilla-vLLM path)Vanilla vLLM speaks OpenAI-compatible HTTP and exposes /v1/chat/completions, /v1/completions, /v1/models, /health, /metrics. It does not expose /server_info or /model_info — the metadata-enrichment endpoints SGLang workers use. The gateway's HTTP service-discovery probe falls through gracefully: when both /server_info and /model_info return 404, the discovery loop returns Ok((labels=empty, None)) and the worker is still registered (source: discover_metadata.rs:237-298 — unwrap_or_else(|e| { warn!(...); (HashMap::new(), None) })).
So both --worker-urls and --service-discovery --selector model_id=... work for vLLM HTTP workers — service discovery just registers them with empty discovered labels (no model name, no max-context auto-pulled from the worker). For Kubernetes, prefer service discovery so worker autoscaling adds/removes Pods without gateway restarts:
sgl-model-gateway \
--worker-urls http://vllm-pool-0:8000 http://vllm-pool-1:8000 http://vllm-pool-2:8000 \
--policy cache_aware \
--host 0.0.0.0 --port 8080Confirm it came up before sending traffic — three checks, in this order:
curl -s http://localhost:8080/v1/models | jq '.data[].id' # models the gateway will route
curl -s http://localhost:8080/workers | jq # every worker registered, not just reachable
curl -s http://localhost:8080/metrics | grep -c '^smg_' # non-zero: new metric prefix is liveAn empty /workers list with a healthy /v1/models means discovery registered nothing — check the selector and namespace before assuming the workers are down.
No --tokenizer-path is needed for cache_aware-only deployments — the radix tree is text-based, not token-based (full rationale in the "Tokenizer must live" section below). Cache-aware policy steers same-prefix requests to the same replica so vLLM's own prefix cache hits — it does not query vLLM's internal cache state. Set --enable-prompt-tokens-details on vLLM for OpenAI-standard usage.prompt_tokens_details.cached_tokens in responses (off by default).
For dynamic worker membership without K8s service discovery, use the runtime endpoints:
curl -X POST http://gateway:8080/workers -d '{"url":"http://vllm-3:8000","model_id":"llama-3-8b"}'
curl -X DELETE http://gateway:8080/workers/<worker_id>
curl http://gateway:8080/workersA K8s sidecar can kubectl get endpoints -w and reconcile this list. Crude but works today.
--data-parallel-size N in one Pod)vLLM has its own DP load-balancer modes (vllm/entrypoints/cli/serve.py:64-104):
vllm serve --data-parallel-size N process exposes a single --port and ZMQ-fans-out to N engines. From the gateway's perspective this is one upstream — DP is invisible. Use this when fitting N engines on one Pod is operationally simpler than N Pods, and cross-replica routing visibility is not needed.--data-parallel-external-lb + --data-parallel-rank per Pod): each Pod runs one DP rank, exposes its own port, and the external LB (= sgl-model-gateway) makes routing decisions per replica. This is what the gateway expects when true per-replica visibility is required. Per vllm/config/parallel.py:135-146: "useful for a 'one-pod-per-rank' wide-EP setup in Kubernetes."Rule: don't put a gateway in front of vLLM internal-DP-LB Pods — per-replica visibility is lost and requests double-hop. Either use vLLM internal-LB alone, or external-LB + gateway, never both.
The gateway only invokes a tokenizer in three situations:
src/routers/grpc/utils.rs); tiktoken-only models — Kimi K2/K2.6, DeepSeek-V3-style, GPT-OSS-style — cannot use gRPC mode at all today.`cache_aware` does NOT need a tokenizer. The radix tree stores raw text characters (src/policies/cache_aware.rs:22 comment, tree.insert(text, …)); routing is pure text-prefix matching. This is why operators successfully run Kimi K2.6 (tiktoken-only) behind cache_aware in production — no tokenizer ever runs on the gateway side. The tokenizer choice only matters when explicitly using one of the three paths above.
Pass via --tokenizer-path /local/dir or --model-path /local/dir. Both accept either an HF repo ID or a local directory. Critical: the Rust gateway's HF resolver does not honour HF_ENDPOINT (verified — zero hits in the source). In an air-gapped cluster with only an internal mirror, passing an HF repo ID will fail even with HF_ENDPOINT set — always pass a local path.
For the full format-dispatch matrix (what the loader does with tokenizer.json vs tiktoken.model vs SentencePiece vs GGUF vs custom Python tokenizers), the cl100k_base regex caveat for Kimi K2 / DeepSeek, and the K2.6 multimodal-file inventory, see references/tokenizers.md. For air-gapped specifics, see references/air-gapped.md.
The doc line Cache hits may decrease by 10-20% is widely misquoted. It applies to running multiple gateway replicas in front of the same workers, not to multiple worker replicas behind one gateway. Each gateway replica builds its own radix tree, the trees are not shared by default, so when a request lands on Gateway-A and the next same-prefix request lands on Gateway-B, B doesn't know A made the previous routing decision and may pick a different worker → cache miss. The 10-20% is the empirical penalty of HA-pairing the gateway itself.
Three mitigations, in increasing operational complexity:
crdts = "7.3" Rust crate; the actual mesh implementation is the external smg-mesh crate). What gets synced across replicas: worker registry, `cache_aware` radix tree, rate-limit window counters (src/policies/cache_aware.rs:67,324,428,453 for tree ops, src/server.rs:753-759 for rate-limit window). What does not sync: circuit-breaker state, which stays per-replica by design. Eventual consistency. Default mesh port 39527, default peer-discovery annotation sglang.ai/ha-port (main.rs:903 — note: not sglang.ai/mesh-port, that name was a doc error). Two ways to wire peers on K8s: static --mesh-peer-urls (parsed as IP:port SocketAddr — hostnames and grpc:// URLs fail to parse, and only first() is used as the gossip bootstrap peer per main.rs:1099-1102), or the K8s-native --router-selector role=gateway with the sglang.ai/ha-port annotation, which gets real pod IPs from the kube API. The K8s-native path is the practical one — see references/kubernetes.md "HA: multiple gateway replicas with mesh" for both recipes. The upstream doc warning about "10-20% cache hit reduction" predates this feature and is stale once mesh is on.For N worker replicas behind 1 gateway (the common case), cache_aware routing is the recovery mechanism, not a penalty. But it's only beneficial when:
sgl-project/sglang#17623 for an operator's reproduction where cache_aware ≈ k8s-RR with comfortable KV memory.For single-turn unique-prompt workloads, cache_aware ≈ random — that's fine, just don't expect a TTFT win.
Worker pods always need the model snapshot mounted. That part is non-negotiable — the worker loads weights, tokenizer, and chat template from disk. Specifically:
tokenizer.json (or tiktoken.model), tokenizer_config.json, chat_template.jinja (or embedded in tokenizer_config.chat_template), config.json, weights.HF_HOME and HF_HUB_CACHE env vars pointing into that store.HF_HOME location for any side-files vLLM/transformers caches at runtime — never point HF_HOME at a read-only PVC mount or initialization writes will fail.HF_HUB_OFFLINE=1 and TRANSFORMERS_OFFLINE=1 on the worker (the gateway doesn't read these — it just doesn't try the network when given a local path).Gateway pods only need the snapshot if they actually use a tokenizer — i.e. running prefix_hash, exposing /v1/tokenize, or using gRPC mode against an HF-tokenized model. For the most common case (HTTP + cache_aware), the gateway pod needs no model files at all — no PVC mount, no init container, no S3 pull.
If a tokenizer is needed on the gateway, the gateway args reference a local path (no repo IDs — the Rust gateway does not honor HF_ENDPOINT):
--tokenizer-path /models/huggingface/hub/models--meta-llama--Llama-3.1-8B-Instruct/snapshots/<sha>/For tiktoken-only models (Kimi K2/K2.6, DeepSeek-V3 family) the same flag works — point at the dir containing tiktoken.model + tokenizer_config.json + chat_template.jinja. The loader's directory scan picks the right format automatically.
For the full flag list, snapshot directory layout, ModelScope alternative, and gated-model-offline trap, see references/air-gapped.md. For tokenizer format dispatch, see references/tokenizers.md.
The pattern in production (matches the user's k8s-homelab/ reference) is one Deployment+Service+RBAC per model, keyed by a model_id label:
model_id=<name>, port 30000 (SGLang) or 8000 (vLLM), httpGet /health probes, hostPath/PVC for /models.clusterIP: ClusterIP (or headless), selects on model_id.lmsysorg/sgl-model-gateway:v0.3.2, args include --service-discovery --selector model_id=<name> --service-discovery-namespace <ns> --service-discovery-port 30000 for SGLang workers (HTTP discovery works) OR --worker-urls http://worker-svc:30000 for vLLM HTTP workers (discovery does not work).pods, endpoints, services with verbs get, list, watch.model_id, pod, node labels.Full manifests in assets/sglang-gateway-deployment.yaml (SGLang worker) and assets/vllm-behind-gateway.yaml (vLLM worker). For RBAC details, multi-port-per-pod limitation (#20184), gRPC probes-need-numeric-ports, and HA gateway pattern, see references/kubernetes.md.
Verify the rollout before pointing clients at it:
kubectl -n <ns> rollout status deploy/sgl-model-gateway --timeout=120s
kubectl -n <ns> port-forward svc/sgl-model-gateway 8080:8080 &
curl -s localhost:8080/workers | jq 'length' # equals the worker replica count
kubectl -n <ns> logs deploy/sgl-model-gateway | grep -i 'discover\|registered' | tailIf /workers returns fewer entries than there are worker Pods, the RBAC verbs or the --selector/--service-discovery-namespace pair is wrong — the gateway starts healthy either way, so an unvalidated rollout looks identical to a working one.
Neither is fixed at time of writing; both are the kind that look like an infrastructure fault rather than a gateway bug.
(#39400, OPEN, filed 2026-09-14). In PD-disaggregation mode the Prefill-side Bootstrap HTTP service exposes PUT /route with no authentication and no integrity check, so any client that can reach it can overwrite the Decode transfer endpoint — route poisoning, or redirection of metadata to an endpoint the attacker controls. If you run PD disaggregation, treat the bootstrap port as a trusted-network service and keep it off any segment you would not trust with the KV path itself. A NetworkPolicy restricting it to the worker set is the cheap mitigation.
model_id: "unknown" ([#37554](https://github.com/sgl-project/sglang/issues/37554), OPEN, filed 2026-09-02). Reported from production: a large share of a fleet silently stopped being routed to after a rollout, and a router restart was the only recovery** — the bad registration never self-heals. The symptom is capacity that exists and passes health checks but receives no traffic, which reads as a load-balancer or autoscaling problem rather than a discovery one. Check the gateway's registered model_id values after any rollout, not just worker readiness.
/server_info + /model_info, which vLLM 404s. The gateway falls through gracefully (discover_metadata.rs:237-298 returns Ok((empty_labels, None))) and the worker is still registered — routing works, but worker-side label enrichment is missing. For prefix_hash, /v1/tokenize, or gRPC mode, pass --tokenizer-path explicitly since the gateway can't fetch it from the worker. Static --worker-urls is also fine — pick whichever fits the autoscaling story.HF_ENDPOINT appears in 16 files repo-wide in sgl-project/sglang but zero under sgl-model-gateway/. Setting it is not obviously wrong, which is exactly why it gets set. Probe: git grep -il HF_ENDPOINT -- 'sgl-model-gateway/'.** Service-discovery probes use raw reqwest; tokenizer fetches go through llm-tokenizer's hf-hub path which is also HF_ENDPOINT-unaware. Verified by grep: zero hits for HF_ENDPOINT in the gateway source. Always pass a local directory to --model-path / --tokenizer-path in air-gapped clusters./health, named ports work fine.#20184). For nodes hosting two decode workers on different ports, split into two Pods, one port each. Still true 2026-07-21: #20184 shows as closed, but the sglang stale bot closed it for inactivity — no fix landed.--enable-mesh is on. Mitigations covered above in §"Hosting multiple replicas"./workers, pass api_key=... explicitly.sgl_router_* → smg_*. Update Grafana dashboards and Prometheus alerts. The lmsysorg/sgl-model-gateway:v0.3.x images all use the new prefix.prompt_tokens_details: null and the gateway loses per-request cache feedback — vllm:prefix_cache_hits from /metrics is the fallback signal.--data-parallel-external-lb + per-Pod --data-parallel-rank) for gateway-level routing intelligence.--tokenizer-path points at it. Dead weight for HTTP + cache_aware. Drop the init container, the mount, and the flag. Keep only when the deployment actually calls /v1/tokenize, runs prefix_hash, or runs gRPC mode. Reproduction and fix in pitfall #19 in references/pitfalls.md.src/routers/grpc/utils.rs:407 does downcast_ref::<HuggingFaceTokenizer>(); for tiktoken the downcast returns None and the request fails with "gRPC router requires HuggingFace tokenizer with chat template support". Use HTTP mode for these models. Cache_aware works fine over HTTP regardless of tokenizer format.For the extended troubleshooting list, see references/pitfalls.md. For tokenizer format dispatch, see references/tokenizers.md.
tokenizer.json vs tiktoken.model, what the Rust loader accepts, why gRPC is HF-only, the cl100k_base regex caveat for Kimi K2 / DeepSeek, K2.6 multimodal-file inventory) → references/tokenizers.md--enable-prompt-tokens-details, DP modes, what vLLM endpoints the gateway sees) → references/vllm-backend.mdreferences/air-gapped.mdreferences/kubernetes.mdreferences/cli-flags.md Prometheus surface** (40+ metrics, alert recipes, PromQL for cache hit rate / queue depth / circuit breaker state) → references/metrics.md`references/pitfalls.md/v1/responses and /v1/conversations — what they're for, when a non-memory backend is genuinely needed, the privacy/proxy angle) → references/history.mdassets/sglang-gateway-deployment.yaml, assets/vllm-behind-gateway.yaml~/projects/github.com/sgl-project/sglang/sgl-model-gateway/~/projects/github.com/sgl-project/sglang/k8s-homelab/--help: docker run --rm lmsysorg/sgl-model-gateway:v0.3.2 --help. If this skill disagrees with --help, trust --help and freshen the skill.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.