Quickstart¶
Route OpenAI-compatible traffic to per-model backend pools in one sitting. You
will create three L7 load-balancer rules — one per model — and watch the gateway
pick the backend from the requested model name, carried either in an X-Model
header or in the JSON request body. This walkthrough is built from the runnable
ai-model-routing scenario, so every command is copy-pasteable.
Before you start
Have the gateway running with its REST API reachable on port 11111 — see Installation. This page uses the scenario's lab addresses; substitute your own VIP and backend IPs as needed.
Use an isolated lab
The commands below mirror an unauthenticated test scenario. Do not expose port 11111 to an
untrusted network. The current gateway allows management requests when no user, OAuth, or
manual-token mode is enabled. A production deployment must enable management authentication
and prove an unauthenticated mutation returns 401.
This is a keyless routing lab
These are plain mode: 4 rules. They do not enter the current API-key/quota gate, even when an
independent AI-key store is configured, and adding X-Api-Key does not turn that gate on.
Protected inference requires a fullproxy rule with sse_mode: true or pd_disagg_mode: true.
Complete API Key Management before exposing such a VIP.
What you will build¶
A single VIP (10.10.10.254) fronting three model pools. Each pool is a
separate L7 rule on its own port, distinguished by model_name:
| Port | model_name |
Backend pool | Routes when the request asks for… |
|---|---|---|---|
| 2020 | llama-70b |
198.51.100.11:8080 |
model llama-70b |
| 2021 | mistral-7b |
198.51.100.12:8080 |
model mistral-7b |
| 2022 | "" (wildcard) |
198.51.100.13:8080 |
any model, or no model at all |
For routing, the gateway reads the model from the X-Model header or the
"model" field of an OpenAI-compatible JSON body; when both are present, the
header wins. On an API-key-gated SSE or P/D rule, model authorization has a
different precedence: it uses the JSON body model first, then falls back to
the path prefix or header. Keep these inputs consistent so routing and
authorization cannot select different model names. A
request that matches no rule and has no wildcard to fall back to gets an HTTP
503 with a model_unavailable body.
flowchart LR
C[Client] --> V["loxilb gateway<br/>VIP 10.10.10.254 (mode 4 fullproxy)"]
V -->|"model = llama-70b :2020"| A["llama-70b pool<br/>198.51.100.11:8080"]
V -->|"model = mistral-7b :2021"| B["mistral-7b pool<br/>198.51.100.12:8080"]
V -->|"wildcard / no model :2022"| W["wildcard pool<br/>198.51.100.13:8080"]
Step 1 — Start the gateway and three backends¶
Bring up the gateway (see Installation) and three
OpenAI-compatible HTTP backends. Any HTTP server on port 8080 works; in the
lab these are minimal mock servers that echo a distinct body
(server-llama, server-mistral, server-wild) so you can tell which pool
answered.
- Gateway VIP:
10.10.10.254, REST API on:11111 - Backend 1:
198.51.100.11:8080— answersserver-llama - Backend 2:
198.51.100.12:8080— answersserver-mistral - Backend 3:
198.51.100.13:8080— answersserver-wild
Confirm the REST API is up before configuring:
For a protected management API, keep the bearer token out of repeated command arguments:
install -m 600 /dev/null ./control-plane.headers
printf 'Authorization: Bearer %s\n' "$GATEWAY_TOKEN" > ./control-plane.headers
The curl examples below use this header file. The loxicmd tab assumes the
installed client is already configured with equivalent gateway credentials.
Step 2 — Create the three routing rules¶
Each rule is one POST to /config/loadbalancer. The serviceArguments are
taken verbatim from the scenario: mode: 4 selects the L7 fullproxy required
for model routing, sel: 0 is round-robin within the pool, and
host + path_prefix: "/" + path_match_mode: "prefix" scope the match so the
model key resolves correctly. model_name is the model this rule serves; an
empty model_name ("") makes the rule a wildcard catch-all.
# Rule 1 — port 2020 → llama-70b pool
curl -s -X POST http://10.10.10.254:11111/netlox/v1/config/loadbalancer \
-H @control-plane.headers \
-H "Content-Type: application/json" \
-d '{
"serviceArguments": {
"externalIP": "10.10.10.254",
"port": 2020,
"protocol": "tcp",
"sel": 0,
"mode": 4,
"host": "10.10.10.254",
"path_prefix": "/",
"path_match_mode": "prefix",
"model_name": "llama-70b",
"inactiveTimeOut": 30
},
"endpoints": [
{"endpointIP": "198.51.100.11", "targetPort": 8080, "weight": 1}
]
}'
# Rule 2 — port 2021 → mistral-7b pool
curl -s -X POST http://10.10.10.254:11111/netlox/v1/config/loadbalancer \
-H @control-plane.headers \
-H "Content-Type: application/json" \
-d '{
"serviceArguments": {
"externalIP": "10.10.10.254",
"port": 2021,
"protocol": "tcp",
"sel": 0,
"mode": 4,
"host": "10.10.10.254",
"path_prefix": "/",
"path_match_mode": "prefix",
"model_name": "mistral-7b",
"inactiveTimeOut": 30
},
"endpoints": [
{"endpointIP": "198.51.100.12", "targetPort": 8080, "weight": 1}
]
}'
# Rule 3 — port 2022 → wildcard pool (model_name "")
curl -s -X POST http://10.10.10.254:11111/netlox/v1/config/loadbalancer \
-H @control-plane.headers \
-H "Content-Type: application/json" \
-d '{
"serviceArguments": {
"externalIP": "10.10.10.254",
"port": 2022,
"protocol": "tcp",
"sel": 0,
"mode": 4,
"host": "10.10.10.254",
"path_prefix": "/",
"path_match_mode": "prefix",
"model_name": "",
"inactiveTimeOut": 30
},
"endpoints": [
{"endpointIP": "198.51.100.13", "targetPort": 8080, "weight": 1}
]
}'
# Rule 1 — port 2020 → llama-70b pool
loxicmd create lb 10.10.10.254 --tcp=2020:8080 --endpoints=198.51.100.11:1 --mode=fullproxy --host=10.10.10.254 --path-prefix=/ --path-match-mode=prefix --model-name=llama-70b --inatimeout=30
# Rule 2 — port 2021 → mistral-7b pool
loxicmd create lb 10.10.10.254 --tcp=2021:8080 --endpoints=198.51.100.12:1 --mode=fullproxy --host=10.10.10.254 --path-prefix=/ --path-match-mode=prefix --model-name=mistral-7b --inatimeout=30
# Rule 3 — port 2022 → wildcard pool (model_name "")
loxicmd create lb 10.10.10.254 --tcp=2022:8080 --endpoints=198.51.100.13:1 --mode=fullproxy --host=10.10.10.254 --path-prefix=/ --path-match-mode=prefix --inatimeout=30
Field casing matters
model_name, path_prefix, path_match_mode, and inactiveTimeOut must
be spelled exactly as shown. A mis-cased field is silently ignored, which
looks like a routing miss rather than an error.
Step 3 — Test model-name routing¶
Route by X-Model header¶
Send the model in the X-Model header to the llama-70b rule on port 2020:
Route by JSON body model field¶
Omit the header and let the gateway read the model from an OpenAI-compatible
body. This request hits the mistral-7b rule on port 2021:
curl -s -X POST http://10.10.10.254:2021/ \
-H "Content-Type: application/json" \
-d '{"model":"mistral-7b","messages":[{"role":"user","content":"hi"}]}'
# → server-mistral
Header overrides body for routing
If both X-Model and a JSON "model" are present, the header wins routing. Sending
X-Model: llama-70b with a body of "model":"mistral-7b" to port 2020
routes to the llama pool. On a protected SSE or P/D rule, authorization checks the body
model first; avoid conflicting values.
Wildcard fallback¶
The port 2022 rule has model_name: "", so it serves any request — including
one with no model at all:
An empty X-Model header falls through to the wildcard the same way:
No-match returns 503¶
Port 2020 serves only llama-70b and has no wildcard behind it. A request for a
model it does not serve gets an HTTP 503 with a model_unavailable body:
curl -s -w "\n%{http_code}\n" -H "X-Model: unknown-xyz" http://10.10.10.254:2020/
# → ...model_unavailable...
# → 503
Model matching is case-sensitive
model_name matches exactly. A request for MISTRAL-7B will not match a
rule configured for mistral-7b — it is treated as a different model and
misses.
Step 4 — Verify the configuration¶
List every rule the gateway holds with GET /config/loadbalancer/all. You
should see all three services, each with its model_name and single endpoint:
Pipe it through jq to confirm the model-to-port mapping at a glance:
curl -s -H @control-plane.headers \
http://10.10.10.254:11111/netlox/v1/config/loadbalancer/all \
| jq '.lbAttr[].serviceArguments | {port, model_name, mode}'
Step 5 — Clean up safely¶
Each model name is part of its rule key. Delete a model-specific rule with the same host, path, path
mode, and model used at creation. A delete without model_name matches only the wildcard rule.
# Delete the llama-70b rule.
curl --fail-with-body -sS -X DELETE \
-H @control-plane.headers \
'http://10.10.10.254:11111/netlox/v1/config/loadbalancer/hosturl/10.10.10.254/externalipaddress/10.10.10.254/port/2020/protocol/tcp?path_prefix=%2F&path_match_mode=prefix&model_name=llama-70b'
# Delete the mistral-7b rule.
curl --fail-with-body -sS -X DELETE \
-H @control-plane.headers \
'http://10.10.10.254:11111/netlox/v1/config/loadbalancer/hosturl/10.10.10.254/externalipaddress/10.10.10.254/port/2021/protocol/tcp?path_prefix=%2F&path_match_mode=prefix&model_name=mistral-7b'
# Delete the wildcard rule; it has no model_name query value.
curl --fail-with-body -sS -X DELETE \
-H @control-plane.headers \
'http://10.10.10.254:11111/netlox/v1/config/loadbalancer/hosturl/10.10.10.254/externalipaddress/10.10.10.254/port/2022/protocol/tcp?path_prefix=%2F&path_match_mode=prefix'
loxicmd delete lb 10.10.10.254 --tcp=2020 --host=10.10.10.254 \
--path-prefix=/ --path-match-mode=prefix --model-name=llama-70b
loxicmd delete lb 10.10.10.254 --tcp=2021 --host=10.10.10.254 \
--path-prefix=/ --path-match-mode=prefix --model-name=mistral-7b
loxicmd delete lb 10.10.10.254 --tcp=2022 --host=10.10.10.254 \
--path-prefix=/ --path-match-mode=prefix
Confirm the lab rules are gone:
curl --fail-with-body -sS \
-H @control-plane.headers \
http://10.10.10.254:11111/netlox/v1/config/loadbalancer/all \
| jq '.lbAttr[] | select(.serviceArguments.port == 2020 or .serviceArguments.port == 2021 or .serviceArguments.port == 2022)'
The command should print no matching rules. Avoid DELETE /config/loadbalancer/all on a shared
gateway because it removes unrelated services.
Remove the temporary management header when finished:
Troubleshoot¶
| Symptom | Likely cause | Fix |
|---|---|---|
| Request hangs or connection refused | Gateway not up, or port 11111/VIP unreachable |
Re-check GET /netlox/v1/version; confirm the VIP is bound to the gateway node |
| Every request lands on the wrong pool | mode not 4, or a mis-cased field silently dropped |
Recreate the rule with mode: 4 and exact field names |
Expected 200 but got 503 model_unavailable |
Requested model matches no rule and no wildcard covers that port | Add a rule for that model, or route through the wildcard port |
Rule missing from /config/loadbalancer/all |
POST rejected (bad JSON / duplicate key) | Re-run the POST and check its response body |
| Cleanup returns 404 | A key component was omitted or changed | Repeat the exact host, path, path mode, and model used at creation |
Next steps¶
- Model Load Balancing — model-name routing in depth, including multi-pool and wildcard strategies.
- LLM Routing — CHWBL prefix-cache affinity and GPU-aware selection.
- Configuration Reference — every
serviceArgumentsfield, default, and enum. - Management API Authentication — secure port
11111before moving beyond the lab.