1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
# rolter bootstrap config
#
# copy to rolter.toml and adjust. secrets should come from environment variables
# referenced by `api_key_env`, not be written inline.
[]
= "0.0.0.0"
= 4000
# path the prometheus metrics endpoint is served on. change it when an upstream
# app or sidecar already owns /metrics behind the same reverse proxy. defaults
# to "/metrics"; must be rooted and must not collide with a built-in route.
# metrics_path = "/internal/metrics"
# secret mixed into virtual-key digests (keeps plaintext keys out of gateway
# memory). prefer the ROLTER_KEY_PEPPER env var over writing it inline.
# key_pepper = "change-me"
# --- providers: where rolter can forward to ---
[[]]
= "openai"
= "openai"
= "https://api.openai.com"
= "OPENAI_API_KEY"
# egress_proxy = "http://corp-proxy:3128" # optional outbound proxy
[[]]
= "anthropic"
= "anthropic"
= "https://api.anthropic.com"
= "ANTHROPIC_API_KEY"
[[]]
= "vllm-1"
= "openai_compatible"
= "http://localhost:8001"
[[]]
= "vllm-2"
= "openai_compatible"
= "http://localhost:8002"
# --- routes: public model name -> upstream targets + strategy ---
[[]]
= "gpt-4o"
= "round_robin"
# admin-set default inference params injected into the request body. an unset
# param passes through untouched. param_policy governs whether callers may
# override a default: mode "allow" (default) lets callers override anything not
# in `deny`; mode "deny" pins every default except those in `allow`. when an
# override is denied the admin default silently wins.
# [routes.params]
# temperature = 0.0 # force deterministic output for this model
# max_tokens = 1024 # cap output length
# [routes.param_policy]
# mode = "deny"
# allow = ["max_tokens"] # callers may still lower max_tokens
[[]]
= "openai"
= "gpt-4o"
[[]]
= "claude"
= "round_robin"
[[]]
= "anthropic"
= "claude-sonnet-4-20250514"
# a self-hosted pool of vllm instances balanced with cache-aware routing
[[]]
= "llama"
= "cache_aware"
[[]]
= "vllm-1"
= "meta-llama/Llama-3.1-8B-Instruct"
[[]]
= "vllm-2"
= "meta-llama/Llama-3.1-8B-Instruct"
# weighted routing: send 3x as much traffic to the first target as the second
# (smooth weighted round-robin — proportional but evenly interleaved)
[[]]
= "llama-weighted"
= "weighted"
[[]]
= "vllm-1"
= "meta-llama/Llama-3.1-8B-Instruct"
= 3
[[]]
= "vllm-2"
= "meta-llama/Llama-3.1-8B-Instruct"
= 1
# composable pipeline: filter → weighted-score → argmax. blends the target
# weight, in-flight load, and prefix-cache affinity scorers into one decision —
# the foundation future cost/latency/kv-cache scorers plug into
[[]]
= "llama-pipeline"
= "pipeline"
[[]]
= "vllm-1"
= "meta-llama/Llama-3.1-8B-Instruct"
= 2
[[]]
= "vllm-2"
= "meta-llama/Llama-3.1-8B-Instruct"
= 1
# weighted variants: A/B, canary, and key-split traffic under one schema. a
# request samples one variant by weight (the primary) and, on failure, falls
# back to the remaining variants in declared order; within a variant the
# targets are tried in order. variant-level params (below) layer over the
# route-level params. the chosen variant name is attributed in request logs
# (the `variant` column) for A/B analysis. a route uses variants instead of the
# top-level targets/strategy pool.
# [[routes]]
# model = "chat"
# [[routes.variants]]
# name = "control"
# weight = 9
# [[routes.variants.targets]]
# provider = "openai"
# model = "gpt-4o"
# [[routes.variants]]
# name = "canary"
# weight = 1
# [[routes.variants.targets]]
# provider = "anthropic"
# model = "claude-sonnet-4-20250514"
# --- virtual keys clients present to the gateway ---
# remove this section to run open (no auth) in local development
[[]]
= "sk-rolter-dev"
= "local-dev"
= [] # empty = all models allowed
= false # revoke without deleting
# expires_at = "2027-01-01T00:00:00Z" # rfc3339; key stops working at/after this
# --- per-model token pricing (usd per million tokens) ---
# drives cost_usd on each request log; models without a price log cost 0
# [[model_prices]]
# model = "gpt-4o"
# input_per_mtok = 2.5
# output_per_mtok = 10.0
# cached_input_per_mtok = 1.25 # optional; defaults to input_per_mtok
# --- spend caps (enforced against redis-tracked cost) ---
# a request is blocked with 402 when any matching budget's spend has reached its
# limit (most-restrictive-wins across the scope chain). requires --redis-url;
# without redis, budgets fail open. `id` matches the request's virtual-key scope
# ids (org/team/project/virtual-key id); config keys carry no scope, so these
# target database-defined keys.
# [[budgets]]
# scope = "org" # org | team | project | key
# id = "org-uuid"
# limit_usd = 500.0
# period = "monthly" # monthly (default) | daily | total
# --- throughput caps (redis sliding window, per-minute) ---
# a request is rejected with 429 (+ retry-after) when a matching limit's
# trailing-minute request or token count has reached its cap (most-restrictive-
# wins across the scope chain). requires --redis-url; without redis, limits fail
# open. `id` matches the request's virtual-key scope ids, like budgets above.
# [[rate_limits]]
# scope = "key" # org | team | project | key
# id = "vk-uuid"
# rpm = 600 # requests per minute; omit to leave requests uncapped
# tpm = 150000 # tokens per minute; omit to leave tokens uncapped
# --- upstream retry policy ---
# on a transient upstream failure (http 408/429/5xx or a connection error) the
# gateway re-picks a target (excluding ones already tried, so retries fail over
# to sibling targets) and forwards again. backoff is exponential with jitter; a
# 429 retry-after header (whole seconds) overrides the computed delay. retries
# only happen before any body bytes reach the client. defaults shown below.
# [retry]
# max_retries = 2 # extra attempts after the first; 0 disables retries
# base_backoff_ms = 100 # base delay for the first retry
# max_backoff_ms = 2000 # ceiling for the backoff delay
# --- per-target cooldowns ---
# after a target returns a transient failure (http 429/5xx or a connection
# error) it is parked for base_secs (or the 429 retry-after, capped at max_secs)
# and the balancer skips it so healthy siblings absorb the load. when every
# sibling is parked the gateway fails open and still forwards. defaults shown.
# [cooldown]
# base_secs = 5 # how long a failing target is parked; 0 disables cooldowns
# max_secs = 300 # ceiling for a cooldown derived from a 429 retry-after
# --- upstream timeouts ---
# connect_secs bounds establishing the connection; request_secs bounds time to
# response headers (not the body, so long SSE streams are never cut short). a
# timeout surfaces as a transient error and feeds the retry/cooldown paths. set
# a field to 0 to disable it. defaults shown below.
# [timeouts]
# connect_secs = 10
# request_secs = 60
# --- active upstream health checks ---
# when enabled, a background task periodically issues a lightweight GET
# {api_base}{path} to each provider; a provider that times out, fails to connect,
# or answers 5xx is marked unhealthy and the balancer skips its targets until a
# later probe recovers it (any non-5xx status, incl. 401/404, counts as healthy).
# when every target of a route is unhealthy the gateway fails open. disabled by
# default. defaults shown below.
# [health]
# enabled = false
# interval_secs = 10 # seconds between probe sweeps
# timeout_secs = 2 # per-probe timeout
# path = "/" # appended to each provider's api_base when probing
# --- per-target circuit breaker ---
# complements the short per-failure cooldown with a longer-lived state machine.
# after failure_threshold consecutive transient failures a target trips OPEN and
# is skipped for open_secs; the first request after that window probes it
# (half-open), a success closes the breaker and a failure re-opens it. where a
# cooldown shrugs off one wobble, the breaker sheds load off a target that is down
# hard. when every target of a route is open the gateway fails open. disabled by
# default. defaults shown below.
# [breaker]
# enabled = false
# failure_threshold = 5 # consecutive failures that trip a target open; 0 disables
# open_secs = 30 # how long a tripped target stays open before a probe
# --- upstream engine metrics scrape ---
# when enabled, a background task periodically pulls each provider's prometheus
# {api_base}{path} and parses the scheduler queue depth (vLLM
# num_requests_waiting, SGLang/TGI fallbacks). the depth is folded into the
# balancer's in-flight load view so load-aware strategies (power_of_two,
# pipeline) steer away from backed-up engines. disabled by default; a scrape
# failure or missing metric contributes zero depth. defaults shown below.
# [metrics_scrape]
# enabled = false
# interval_secs = 5 # seconds between scrape sweeps
# timeout_secs = 2 # per-scrape timeout
# path = "/metrics" # appended to each provider's api_base when scraping
[]
# request logs are written asynchronously in batches, off the request hot path;
# logging is disabled entirely when clickhouse_url is unset
# clickhouse_url = "http://localhost:8123"
# batch_max = 1000 # flush once a batch reaches this many rows
# flush_ms = 1000 # also flush a partial batch at least this often
# queue_capacity = 10000 # rows are dropped (and counted) when this queue is full