Gateway & Routing
The gateway is the listener your applications talk to, and the router is what decides where each request actually goes. This page covers both, plus health checking and model aliasing.
If you want resilient multi-provider failover specifically, read fallback & failover after this page.
Step 1 — Configure the listener
Section titled “Step 1 — Configure the listener”apiVersion: oculis.ai/v1kind: GatewayConfig
oculis: gateway: listen_address: '0.0.0.0' listen_port: 8080 metrics_port: 9090 max_request_bytes: 8_388_608 # 8 MiB request_timeout_seconds: 120 # hard ceiling for any single request keepalive_seconds: 75 # must exceed your load balancer idle timeout cors: allowed_origins: ['https://app.example.com'] allow_credentials: truegateway: listenAddress: '0.0.0.0' listenPort: 8080 metricsPort: 9090 maxRequestBytes: 8388608 requestTimeoutSeconds: 120 keepaliveSeconds: 75 cors: allowedOrigins: - https://app.example.com allowCredentials: truefrom oculis import GatewayConfig, Cors
config = GatewayConfig( listen_address="0.0.0.0", listen_port=8080, metrics_port=9090, max_request_bytes=8 * 1024 * 1024, request_timeout_seconds=120, keepalive_seconds=75, cors=Cors( allowed_origins=["https://app.example.com"], allow_credentials=True, ),)Step 2 — Register upstreams
Section titled “Step 2 — Register upstreams”An upstream is a destination. Register every place a request could be sent, then let routes choose among them.
oculis: upstreams: # Self-hosted runtime - name: vllm-a provider: vllm endpoint: 'http://gpu-node-1:8000/v1' model: meta-llama/Llama-3.1-70B-Instruct weight: 1 # used by `weighted` strategy timeout_seconds: 30 max_queue_depth: 50 health_check: path: /health interval_seconds: 5 timeout_seconds: 2 healthy_threshold: 2 unhealthy_threshold: 3
- name: vllm-b provider: vllm endpoint: 'http://gpu-node-2:8000/v1' model: meta-llama/Llama-3.1-70B-Instruct timeout_seconds: 30 max_queue_depth: 50
# Commercial API - name: openai provider: openai model: gpt-4o-mini api_key: ${env:OPENAI_API_KEY} timeout_seconds: 60 # Optional: override the base URL for Azure or a proxy # endpoint: "https://my-resource.openai.azure.com/openai/v1"from oculis import Upstream, HealthCheck
config.upstreams = [ Upstream( name="vllm-a", provider="vllm", endpoint="http://gpu-node-1:8000/v1", model="meta-llama/Llama-3.1-70B-Instruct", weight=1, timeout_seconds=30, max_queue_depth=50, health_check=HealthCheck( path="/health", interval_seconds=5, timeout_seconds=2, healthy_threshold=2, unhealthy_threshold=3, ), ), Upstream( name="openai", provider="openai", model="gpt-4o-mini", api_key="${env:OPENAI_API_KEY}", timeout_seconds=60, ),]curl -X PUT http://localhost:8080/admin/v1/upstreams/vllm-a \ -H "Authorization: Bearer $OCULIS_ADMIN_KEY" \ -H "Content-Type: application/json" \ -d '{ "provider": "vllm", "endpoint": "http://gpu-node-1:8000/v1", "model": "meta-llama/Llama-3.1-70B-Instruct", "timeout_seconds": 30, "max_queue_depth": 50 }'Supported providers
Section titled “Supported providers”vllm · tgi · sglang · ollama · llamacpp · tensorrt · openai · azure_openai ·
anthropic · bedrock · vertex · mistral · together
Feature support varies between providers, so confirm behavior before putting two providers in one fallback chain.
Step 3 — Define routes
Section titled “Step 3 — Define routes”oculis: routes: # Even distribution across two identical GPU nodes. - name: default strategy: round_robin upstreams: [vllm-a, vllm-b]
# Explicit alias so applications never name a model directly. - name: fast-summarize strategy: single primary: openai
# Canary: 5% of traffic to a new node. - name: canary strategy: weighted upstreams: - name: vllm-a weight: 95 - name: vllm-b weight: 5from oculis import Route, WeightedUpstream
config.routes = [ Route(name="default", strategy="round_robin", upstreams=["vllm-a", "vllm-b"]), Route(name="fast-summarize", strategy="single", primary="openai"), Route( name="canary", strategy="weighted", upstreams=[ WeightedUpstream(name="vllm-a", weight=95), WeightedUpstream(name="vllm-b", weight=5), ], ),]Choosing a strategy
Section titled “Choosing a strategy”| Strategy | Distributes by | Choose it when |
|---|---|---|
single |
Nothing — one upstream | Development, or an explicit model alias. |
round_robin |
Even rotation over healthy upstreams | Nodes are identical and requests cost roughly the same. |
least_queue |
Current queue depth | Nodes differ in size, or request costs vary widely. |
weighted |
Fixed percentages | Canarying a node, model, or provider. |
primary_with_fallback |
Health and saturation | You own GPUs and want cloud as insurance. |
semantic |
Prompt classification | Cost reduction by sending easy prompts to a small model. |
Step 4 — Apply
Section titled “Step 4 — Apply”-
Validate.
Terminal window oculis config validate --file oculis-config.yaml --explain -
Apply with a rolling restart.
Terminal window oculis apply --file oculis-config.yaml --strategy rolling --drain-timeout 60s -
Watch until every replica reports the new generation.
Terminal window oculis status --watch
Verification
Confirms the listener is bound, every upstream is healthy, each route resolves, and load actually distributes across upstreams.
1. Every upstream is reachable and every route resolves.
oculis gateway test --all-routes[INFO] Route "default" strategy=round_robin upstreams=2[INFO] vllm-a http://gpu-node-1:8000/v1 OK (latency 11ms, queue 0/50)[INFO] vllm-b http://gpu-node-2:8000/v1 OK (latency 13ms, queue 0/50)[INFO] Route "fast-summarize" strategy=single upstreams=1[INFO] openai api.openai.com OK (latency 152ms)[INFO] Route "canary" strategy=weighted upstreams=2[PASS] 3 routes resolved, 3 upstreams healthy, 0 unhealthy.2. Traffic actually distributes. Send a burst and confirm the counters move on both upstreams:
for i in $(seq 1 10); do curl -s -o /dev/null http://localhost:8080/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{"model":"default","messages":[{"role":"user","content":"hi"}],"max_tokens":4}'done
curl -s http://localhost:9090/metrics | grep 'oculis_requests_total{route="default"'oculis_requests_total{route="default",upstream="vllm-a",status="200"} 5oculis_requests_total{route="default",upstream="vllm-b",status="200"} 5If all ten landed on one upstream, the other is failing its health check — it will be reported as unhealthy in step 1 rather than silently skipped.
Model aliasing
Section titled “Model aliasing”Applications send a route name in the model field. This is what lets you change models without
shipping application code:
# The application asks for a route...curl http://localhost:8080/v1/chat/completions \ -d '{"model": "fast-summarize", ...}'# ...and you decide what that means.routes: - name: fast-summarize strategy: single primary: openai # change to vllm-a; no application changeTo let clients continue sending real model names during a migration, add explicit aliases:
oculis: aliases: 'gpt-4o-mini': fast-summarize 'gpt-4o': high-qualityHealth checks
Section titled “Health checks”| Field | Default | Guidance |
|---|---|---|
path |
/health |
Must return 200 without authentication. |
interval_seconds |
10 |
Lower means faster detection and more probe load. |
timeout_seconds |
2 |
Should be well under interval_seconds. |
healthy_threshold |
2 |
Consecutive successes before returning to rotation. |
unhealthy_threshold |
3 |
Below 3, a single GC pause evicts a healthy node. |
Edge cases and known limitations
Section titled “Edge cases and known limitations”max_queue_depthis per replica. Three replicas at depth 50 tolerate 150 queued requests.- Weighted routing is probabilistic, not exact. Over 100 requests a 95/5 split may land 93/7. Judge a canary over thousands of requests, not dozens.
- Changing a route’s strategy does not drain in-flight requests. They complete against the upstream they were already assigned.
request_timeout_secondsis a hard ceiling that overrides any longer per-upstream timeout. A 60s upstream timeout under a 30s gateway timeout is effectively 30s.- Health checks do not validate the model. A node serving the wrong model passes
/healthhappily. Useoculis gateway testto check the model identity.