Configuration reference
The gateway boots from a TOML file (--config, default rolter.toml); see rolter.example.toml. At runtime, the control plane is the source of truth and applies changes without a restart (config-and-hot-reload.md).
Schema
[server]
host(string, default0.0.0.0)port(u16, default4000)metrics_path(string, default/metrics) — path the Prometheus metrics endpoint is served on; change it to avoid colliding with an upstream app or sidecar that already owns/metrics. Must be rooted (/…) and must not collide with a built-in route (/healthz,/v1/*).
[tls]
ca_bundles(string[], default[]) — PEM CA-bundle files added to the normal public-root trust store for outbound upstream TLS.ROLTER_CA_BUNDLEreplaces this global list with a single deployment-local path. Files are checked for missing, unreadable, empty, and malformed content while config is loaded.
[[providers]]
name(string, unique) — referenced by route targetskind(openai|anthropic|openai_compatible|ollama|ollama_cloud|llama_cpp|openrouter|tei|azure_openai|bedrock|vertex|gemini|gemini_native|gemini_interactions|mistral|groq|xai|meta_llama_api|cohere|perplexity|together|fireworks|databricks|aleph_alpha|nebius|ovhcloud|scaleway|deepseek|qwen|zhipu|kimi|ernie|doubao|hunyuan|yi|minimax|baichuan|gigachat|yandex_gpt|cloud_ru|mts_ai|naver|upstage|rinna|rakuten|sarvam|krutrim|falcon)api_base(string) — base URL, no trailing slashapi_key(string, optional) — preferapi_key_envapi_key_env(string, optional) — environment variable to read the key fromrole_profile(openai|system_only|anthropic, optional) — explicit instruction-role semantics. The default isopenaiforkind = "openai",anthropicforkind = "anthropic", and conservativesystem_onlyfor every OpenAI-compatible kind.system_onlyconverts leadingdevelopermessages tosystemin place; it rejects asystemordevelopermessage after a user/assistant/tool turn withrole_capability_unsupportedrather than silently changing it.model_role_profiles(table, optional) — upstream-model-specificrole_profileoverrides. Use this only for a custom template whose developer-role support is explicitly known; rolter never probes a vLLM template at runtime.
Role-capability profiles
openai_compatible describes the HTTP surface only. vLLM, in particular,
renders roles using the selected model’s chat template, so an endpoint’s role
support must not be inferred from its /v1 API. The default system_only
profile is suitable for Qwen-style templates that do not define developer.
Set role_profile = "openai" or a model_role_profiles entry only after
confirming that the deployed template supports distinct developer messages.
Anthropic targets collect leading OpenAI developer and system messages into
ordered top-level system blocks. Instruction messages placed after a
conversation turn are rejected for anthropic and system_only profiles;
rolter returns an OpenAI-style 400 with code
role_capability_unsupported instead of dropping or reclassifying them.
Ollama: local daemon vs Cloud
Use ollama for a local/self-hosted daemon such as http://localhost:11434 (no authentication). Use ollama_cloud for direct programmatic Cloud access. Cloud requires api_key_env (normally OLLAMA_API_KEY); inline keys and key pools are rejected. Configure api_base = "https://ollama.com"; rolter uses the OpenAI-compatible /v1/chat/completions and /v1/models endpoints with bearer authentication. Ollama’s native /api/* endpoints are distinct.
[[providers]]
name = "ollama-cloud"
kind = "ollama_cloud"
api_base = "https://ollama.com"
api_key_env = "OLLAMA_API_KEY"
Azure OpenAI, Amazon Bedrock, and Vertex AI
These providers use their current OpenAI-compatible APIs. Set api_base to the
provider’s OpenAI-compatible prefix and use an environment-sourced credential:
[[providers]]
name = "azure"
kind = "azure_openai"
api_base = "https://RESOURCE.openai.azure.com/openai/v1"
api_key_env = "AZURE_OPENAI_API_KEY"
[[providers]]
name = "bedrock"
kind = "bedrock"
api_base = "https://bedrock-runtime.us-east-1.amazonaws.com/v1"
api_key_env = "AWS_BEARER_TOKEN_BEDROCK"
[[providers]]
name = "vertex"
kind = "vertex"
api_base = "https://aiplatform.googleapis.com/v1/projects/PROJECT/locations/global/endpoints/openapi"
api_key_env = "VERTEX_ACCESS_TOKEN"
Azure credentials are sent in the api-key header. Bedrock and Vertex
credentials are sent as bearer tokens. The default active-health probes use
Azure’s model list, Bedrock ListFoundationModels, and Vertex’s publisher model
list, respectively; none invokes a model.
Google Gemini, Mistral, Groq, and xAI (Grok) expose hosted OpenAI-compatible
APIs. Their
api_base already carries the version segment, so rolter strips the leading
/v1 from the gateway path before appending it. Keys are bearer tokens sourced
from api_key_env (inline keys are rejected); the free health probe lists
{api_base}/models.
gemini_native targets Gemini’s native generateContent surface instead of its
OpenAI-compatible shim. rolter translates OpenAI Chat / Anthropic Messages /
OpenAI Responses requests into Gemini’s contents/parts wire format and
converts the response (and SSE stream) back, so clients keep speaking their
usual protocol. The model and method are embedded in the URL
({api_base}/models/{model}:generateContent, or :streamGenerateContent?alt=sse
for streaming), the key is sent as x-goog-api-key, and api_base points at the
version root with no /openai suffix.
gemini_interactions targets Gemini’s stateful Interactions API. Every
chat-shaped request is translated onto the single {api_base}/interactions
endpoint: turns become input items, system/developer messages become
system_instruction, tool calls and tool results become function_call /
function_result items, and sampling parameters become generation_config. The
response steps[] (and the interaction.created / step.delta /
interaction.completed SSE events) are converted back into the client’s dialect.
The thread is client-driven: the interaction id is returned as the response id,
and a client resumes it by sending previous_response_id (OpenAI Responses) or
previous_interaction_id — rolter keeps no interaction state of its own. Auth is
x-goog-api-key, and api_base points at the version root. Endpoints with no
interactions equivalent (embeddings, audio) are rejected rather than forwarded.
[[providers]]
name = "gemini"
kind = "gemini"
api_base = "https://generativelanguage.googleapis.com/v1beta/openai"
api_key_env = "GEMINI_API_KEY"
# native generateContent wire format (translated from OpenAI/Anthropic)
[[providers]]
name = "gemini-native"
kind = "gemini_native"
api_base = "https://generativelanguage.googleapis.com/v1beta"
api_key_env = "GEMINI_API_KEY"
# stateful Interactions API (translated from OpenAI/Anthropic/Responses)
[[providers]]
name = "gemini-interactions"
kind = "gemini_interactions"
api_base = "https://generativelanguage.googleapis.com/v1beta"
api_key_env = "GEMINI_API_KEY"
[[providers]]
name = "mistral"
kind = "mistral"
api_base = "https://api.mistral.ai/v1"
api_key_env = "MISTRAL_API_KEY"
[[providers]]
name = "groq"
kind = "groq"
api_base = "https://api.groq.com/openai/v1"
api_key_env = "GROQ_API_KEY"
[[providers]]
name = "xai"
kind = "xai"
api_base = "https://api.x.ai/v1"
api_key_env = "XAI_API_KEY"
[[providers.api_keys]](optional) — multiple weighted API keys for one provider; when present it takes precedence over the singleapi_key/api_key_envpair. Providers cap throughput per key, so rotating across keys multiplies effective RPM/TPMkey(string, optional) — inline key value; preferenvenv(string, optional) — environment variable to read the key fromweight(u32, default1) — relative selection weight
api_key_env(string, optional) — env var to read the key fromegress_proxy(string, optional) — legacy single HTTP/HTTPS/SOCKS5 outbound proxy; treated as a one-element poolegress_proxies(string[], optional) — round-robin HTTP, HTTPS, SOCKS5, or SOCKS5H proxy pool. A connect/tunnel failure retries the next member; three consecutive failures quarantine a member for 30 seconds. Authenticated proxy URLs must be supplied as whole-value environment references such as"${PROVIDER_PROXY_EU}", keeping credentials out of config snapshots and database/API outputca_bundles(string[], optional) — provider-specific replacement for global[tls].ca_bundles;[]explicitly selects public roots only[providers.kv_events](optional) — vLLM V1 ZMQ KV-event source forprecise_cache_aware:endpoint(tcp://…),topic(defaultkv-events),max_blocks(default 1,000,000), andstale_secs(default 30)[providers.lmcache](optional) — LMCache controller signal forlmcache_aware:endpoint(HTTP JSON occupancy signal),refresh_secs(default 2), andstale_secs(default 10)also_track_via_llm_call(bool, defaultfalse) — when set, active health checks send a realmax_tokens = 1completion to this provider instead of the free/v1/modelsliveness probe, so a healthy result proves end-to-end inference. This burns a few tokens on every sweep (interval_secs); leave it off unless you need inference-level health. Recorded assource = llm_callinprovider_health_events.llm_probe_model(string, optional) — the upstream model id thealso_track_via_llm_callcompletion targets (e.g.gpt-4o-mini). Required when the flag is on; without it (or an api key) the checker logs a warning and falls back to the free probe.status_page_url(string, optional) — statuspage.io-stylestatus.jsonURL (e.g.https://status.anthropic.com/api/v2/status.json). When set, a slow background poll records the provider’s public status as a secondarystatus_pagehealth signal — it surfaces inprovider_health_events, the dashboard androlter_status_page_degraded_total, but never marks the provider unhealthy or affects routing on its own. Parse/transport failures are logged and skipped.
See Custom CA bundles for rotation behavior and Docker/Kubernetes mount examples.
[[routes]]
model(string) — public model name clients requeststrategy(round_robin|random|power_of_two|consistent_hash|cache_aware|weighted|pipeline|cheapest|fastest|precise_cache_aware|lmcache_aware|adaptive|lora_aware|predicted_latency, defaultround_robin)[[routes.targets]]provider(string) — a providernamemodel(string, optional) — upstream model id; defaults to the requested modelweight(u32, default1)
[routes.params](table, optional) — admin default inference params injected into the request body (e.g.temperature,max_tokens,stop). Provider-agnostic: keys are whatever the upstream accepts. An unset param passes through untouched.[routes.param_policy]— whether callers may override theparamsdefaultsmode(allow|deny, defaultallow) — baseline override policyallow(string[], default[]) — params callers may override whenmode = "deny"deny(string[], default[]) — params callers may not override whenmode = "allow"- when an override is denied and the caller sends the param anyway, the admin default silently wins
[[routes.variants]](optional) — weighted variants for A/B, canary, and key-split traffic. When present, the route ignores the top-leveltargetspool: a request samples one variant by weight (the primary) and, on failure, falls over to the remaining variants in declared order. Within a variant the route’sstrategypicks which target leads; the remaining targets follow in declared order as the deterministic fallback tail.name(string) — variant identifier, attributed in request logs (thevariantcolumn)weight(u32, default1) — relative traffic share for the primary draw[[routes.variants.targets]]— same shape as[[routes.targets]][routes.variants.params](table, optional) — variant-scoped param defaults, layered over[routes.params](the variant wins) under the route’sparam_policy
[routes.advanced.guardrails](optional) — per-route selection layered over the global[guardrails]rule set. Rules are named, so adding one globally still reaches every route that has not opted out of it by name.disable(string[], default[]) — rules that do not apply on this routeenable(string[], default[]) — rules that apply on this route; wins overdisableon a conflict- a name matching no configured rule fails validation rather than being ignored — a typo in
disablewould otherwise read as “this rule is off here” while the rule kept running
[[virtual_keys]]
key(string) — the bearer token clients presentname(string, optional)models(string[], default[]) — allow-list; empty = all
[adaptive_routing]
Deployment-wide policy for routes using the adaptive strategy. See load balancing.
enabled(bool, defaultfalse) — kill switch; while off, everyadaptiveroute serves thepipelinestacklatency_weight(f32, default1.0),cost_weight(f32, default0.5),load_weight(f32, default0.25) — blend weights; negatives are clamped to0, and all-zero disables the blendexploration_ratio(f32, default0.05) — share of picks made at random to keep latency samples fresh; clamped to[0, 0.5]min_samples(u32, default50) — requests a route must serve before the blend engages
[logging]
clickhouse_url(string, optional)
[health]
enabled(bool, defaultfalse) — master switch for active upstream probinginterval_secs(u64, default10) — seconds between probe sweepstimeout_secs(u64, default2) — per-probe timeoutpath(string, default/) — probe path; the default resolves to each provider kind’s free liveness endpoint (normally/v1/models, or the provider-native Azure, Bedrock, or Vertex model-list endpoint)probe_concurrency(usize, default2) — max probes in flight at once during a sweep, so probing never stampedes upstreamsconsecutive_failure_threshold(u32, default3) — consecutive probe failures before a provider is marked unhealthyrecovery_success_threshold(u32, default2) — consecutive successes before an unhealthy provider recoversstatus_page_interval_secs(u64, default60) — seconds between provider status-page polls; only providers with astatus_page_urlare polled, and the poller runs even whenenabled = false- probes are jittered across the first quarter of the interval (per-provider stable offset), and a
429on the probe itself pauses that provider’s probing with exponential backoff (1, 2, 4, 8 sweeps) without marking it unhealthy
[realtime]
Guardrails for persistent /v1/realtime WebSocket sessions. All limits are per gateway process; set a value to 0 to disable that limit.
max_connections(u64, default1000) — concurrent sessions admitted by this gateway instancemax_session_secs(u64, default3600) — hard session-duration limitidle_timeout_secs(u64, default300) — closes a session when neither side sends a frame
[egress]
Where the gateway may send upstream traffic. A provider’s api_base (and any egress_proxy) is operator-supplied, so without a destination policy a crafted or compromised provider row turns the gateway into an SSRF primitive pointed at whatever sits inside its network position — cloud instance metadata being the classic target.
block_link_local(bool, defaulttrue) — deny169.254.0.0/16andfe80::/10. This is the cloud instance-metadata range; leave it on unless you have a concrete reasonblock_loopback(bool, defaultfalse) — deny127.0.0.0/8and::1. Off by default because sidecar and single-host deployments legitimately serve models on localhostblock_private(bool, defaultfalse) — deny10.0.0.0/8,172.16.0.0/12,192.168.0.0/16andfc00::/7. Off by default because on-prem clusters are the common caseallow_hosts(string[], default[]) — hosts exempt from every check above, matched verbatim against the URL host (an IP literal or a hostname). The escape hatch for a deployment that must reach one otherwise-denied address
Enforcement happens twice. IP literals are classified during config validation, so a bad api_base is rejected at startup or when the control plane writes it. Hostnames are classified at connect time, against the address DNS actually returned — validation never resolves DNS itself (that would make config validation depend on a live resolver and break air-gapped deployments), and a connect-time check is what makes the policy DNS-rebinding-safe. A denied address fails the request; it is not silently retried elsewhere.
[egress]
block_link_local = true
block_private = true
allow_hosts = ["vllm.internal.example.com"] # the one private upstream we mean to reach
[guardrails]
Built-in, zero-dependency regex guardrails and PII redaction, evaluated inside the gateway with no external service and no network hop. Disabled by default; a disabled or empty block adds no hot-path cost. Complements — never replaces — the custom guardrail webhook (ROL-257) and external PII engines (ROL-258).
enabled(bool, defaultfalse) — master switchmax_scan_bytes(usize, default262144) — cap on total request text scanned per request; oversized content is forwarded unscanned so work stays boundedstreaming_post_call(string, defaultreject) — what a streamed request does on a route that haspost_callrules; see Output masking and streaming
Each [[guardrails.rules]] entry:
name(string, required) — stable, unique; surfaced in metrics, never carries match textbuiltin(string) — one ofemail,phone,api_token,payment_card; orpattern(string) for a custom regex. Set exactly one.stage(string, defaultpre_call) —pre_callscans request content before proxying;post_callmasks the response body before it reaches the clientaction(string, defaultannotate) —annotate(count only, forward unchanged),block(reject with an OpenAI-compatibleguardrail_blockederror), orredact(replace each match withreplacement)replacement(string) — redaction token; defaults to the built-in entity token (e.g.[REDACTED:EMAIL]) or[REDACTED]include_system(bool, defaultfalse) — also scan operator-authoredsystem/developermessages; excluded by default
default_onwas removed. It documented a per-request client opt-in that was never implemented, so it never selected anything: every configured rule applied regardless of its value. Existing configs that still set it keep loading — the key is ignored — and behaviour is unchanged. To turn a rule off somewhere, name it in that route’s[routes.advanced.guardrails]disablelist (see[[routes]]).
Patterns use the linear-time (RE2-style) regex engine with no catastrophic backtracking, and are compiled under a bounded program size during config validation — an invalid or unbounded pattern fails at startup/snapshot validation, never on the request path. The request path never logs raw matched values; metrics expose rolter_guardrail_blocks_total and rolter_guardrail_redactions_total only.
Scanned surfaces: OpenAI /v1/chat/completions and /v1/responses (messages + input), /v1/completions (prompt), and Anthropic /v1/messages (system + messages). String, string-array, and typed text parts are all covered.
[guardrails]
enabled = true
[[guardrails.rules]]
name = "email"
builtin = "email"
action = "redact"
[[guardrails.rules]]
name = "card"
builtin = "payment_card"
action = "block"
[[guardrails.rules]]
name = "leaked-key"
builtin = "api_token"
stage = "post_call"
action = "redact"
Output masking and streaming
A post_call rule runs on the response body after the upstream replies and before the client sees it. redact rewrites the matched text in place; block withholds the completion and returns a 403 with code guardrail_blocked and the rule name — nothing was malformed and no upstream failed, so it is neither a 400 nor a 5xx, and it stays out of the range clients retry on.
Masked surfaces: choices[].message.content and choices[].text (OpenAI chat and legacy completions), the top-level content parts array (Anthropic /v1/messages), and output[].content[].text plus output_text (/v1/responses).
Tool-call arguments are scanned too. An address the model passes to a send_email tool has left the completion just as surely as one it printed. Arguments that arrive as a JSON document inside a JSON string (choices[].message.tool_calls[].function.arguments, the legacy function_call.arguments, and /v1/responses output[].arguments) are parsed, masked and re-encoded, so a replacement token containing a quote or a backslash cannot turn valid arguments into something the client fails to parse; non-string values are untouched. Anthropic tool_use parts are masked through their input object. Arguments that are not valid JSON are masked as plain text.
Output masking requires the whole completion. A match can straddle any number of token boundaries, so a rule redacting a@b.com cannot act on a frame holding only a@b, and buffering the full stream would remove the only property streaming has. streaming_post_call therefore decides what a streamed request does on a route with post_call rules:
reject(default) — refuse with a400carrying codeguardrail_streaming_unsupported, counted inrolter_guardrail_stream_rejections_total. The default fails closed: a masking rule that silently stops applying because the client passed"stream": trueis the failure mode worth ruling out.passthrough— serve the stream with output rules not applied.pre_callrules still run on the request.
Non-streamed responses are buffered by the gateway when (and only when) a post_call rule applies to the route, so a route without them keeps its existing forwarding behaviour. Cached responses are stored as the upstream returned them and masked on every delivery, not once at store time — so a rule added after an entry was cached still applies to it, and an entry shared by two routes is masked per the route serving it. Output metrics: rolter_guardrail_output_redactions_total and rolter_guardrail_output_blocks_total.
[guardrail_webhook]
A vendor-neutral hook to a self-hosted semantic guardrail service (e.g. Guardrails AI, LLM Guard). Before proxying, the gateway POSTs a stable JSON envelope to the configured endpoint; the service replies with an allow/block/transform/annotate decision. Disabled by default; complements the built-in regex guardrails.
enabled(bool, defaultfalse) — master switchurl(string) — http(s) endpoint the envelope is POSTed to (required when enabled)stage(string, defaultpre_call) —pre_callinspects the request.post_callis validated but not yet enforced (output/SSE stage deferred).timeout_ms(u64, default2000) — per-call timeoutmax_retries(u32, default0) — extra attempts on a transient failure (connect/timeout/non-2xx)failure_mode(string, defaultfail_open) —fail_openforwards unchanged when the service is unreachable;fail_closedrejects with an OpenAI-compatible errormax_body_bytes(usize, default65536) — cap on the content forwarded; oversized content is sent as a truncated preview withtruncated: trueauth— optional credential resolved from the environment at call time, never inlined:{ bearer = { token_env = "GUARD_TOKEN" } }→Authorization: Bearer <env>{ shared_secret = { secret_env = "GUARD_SECRET" } }→X-Rolter-Guardrail-Secret: <env>
Contract. Request envelope: { direction, stage, model, route, trace_id, tenant: { org, team, project, key }, truncated, content }. Only these fields are sent; prompt content is never logged by the gateway. Response: { "action": "allow" | "block" | "transform" | "annotate", "content"?, "reason"?, "annotations"? }. An unrecognized or malformed decision defaults to allow (transport failures are governed by failure_mode). Metrics: rolter_guardrail_webhook_blocks_total, _transforms_total, _errors_total; the trace id is propagated in the X-Rolter-Trace-Id header.
[guardrail_webhook]
enabled = true
url = "https://guard.internal/check"
failure_mode = "fail_closed"
auth = { bearer = { token_env = "GUARD_TOKEN" } }
[prompt_templates]
Centrally-managed, versioned prompt templates and deterministic route decorators (ROL-256). Applications reuse approved system instructions through a named template without being granted arbitrary prompt-authoring privileges. Disabled by default; an empty or disabled block adds no hot-path cost.
enabled(bool, defaultfalse) — master switch
Each [[prompt_templates.templates]] entry is one immutable version:
id(string, required) — stable identifier surfaced in safe metadata, never in content logsversion(u32, required, ≥ 1) — immutable version; the operator lists exactly the versions to activate.(id, version)must be unique.routes(array of string, default all) — public model names this template applies to; empty means every route[[prompt_templates.templates.variables]]— a named variable a decorator may reference as{{ name }}:name(string,[A-Za-z_][A-Za-z0-9_]*)required(bool, defaultfalse) — the caller must supply it; mutually exclusive withdefaultdefault(string) — value used when the caller omits it
[[prompt_templates.templates.decorators]]— a message injected around the caller’s own messages:role(string, defaultsystem) —system,assistant, oruserposition(string, defaultprepend) —prepend(before the caller’s messages) orappend(after), both in declared ordercontent(string) — message text, with optional{{ variable }}placeholders
Variables and escaping. Callers pass values in a rolter_template_vars object on the request body; it is always stripped before forwarding upstream. A caller value overrides the declared default; an unknown variable, a missing required variable, or an oversized value (variable > 4 KiB, rendered message > 16 KiB) is rejected with an invalid_prompt_template error. Substitution is structural: each rendered message is emitted as a JSON string through the serializer, never string-concatenated into raw JSON, so a variable value can never break out of its string or inject additional messages. Every {{ placeholder }} is validated at config-load time to reference a declared variable.
Surfaces and ordering. Applied to /v1/chat/completions, /v1/responses, and Anthropic /v1/messages. Prepend decorators wrap before, append after, preserving the caller’s own message order and semantics. For Anthropic, system decorators fold into the top-level system field (joined by blank lines); assistant/user decorators wrap the messages array. Surfaces without a chat message array (e.g. /v1/completions) are not decorated. The gateway applies only the configured immutable version from its reload-free snapshot. Applied template id/version and decoration count are recorded in safe metadata; metrics expose rolter_prompt_template_decorations_total and rolter_prompt_template_rejections_total.
[prompt_templates]
enabled = true
[[prompt_templates.templates]]
id = "support-preamble"
version = 3
routes = ["gpt-4o"]
[[prompt_templates.templates.variables]]
name = "persona"
default = "a helpful support assistant"
[[prompt_templates.templates.decorators]]
role = "system"
position = "prepend"
content = "You are {{persona}}. Follow the company policy and be concise."
A caller then supplies variables per request:
{
"model": "gpt-4o",
"messages": [{ "role": "user", "content": "hi" }],
"rolter_template_vars": { "persona": "a billing specialist" }
}
When the control plane uses PostgreSQL, prompt templates can also be authored
through the org-scoped /api/v1/orgs/{org_id}/prompt-templates endpoints. Draft
versions are immutable once published, publication and rollback update the
reload-free snapshot, and activation can be limited to an organization, project,
route, or virtual key. Seed import creates missing versions idempotently and
refuses to overwrite an existing immutable version with different content.
Organization skills use the adjacent
/api/v1/orgs/{org_id}/skills endpoints. A skill has mutable metadata and access
policy, immutable inline or reference-backed versions, explicit
publish/rollback/retire lifecycle, and a deterministic
/resolve/{slug} endpoint. Reference-backed versions accept https, git+https,
oci, and s3 references without embedded credentials; secret-bearing metadata
keys are rejected.
Environment variables
ROLTER_CONFIG,ROLTER_HOST,ROLTER_PORT— gatewayROLTER_CONTROL_HOST,ROLTER_CONTROL_PORT,ROLTER_UI_DIR— control planeROLTER_KEK— AES-256-GCM KEK for provider-secret encryptionROLTER_PUBLIC_URL— the control plane’s externally reachable base URL (defaulthttp://localhost:4001). The OIDC redirect URI is derived from it, so single sign-on needs it set correctly behind a proxy; see Single sign-onDATABASE_URL,REDIS_URL,CLICKHOUSE_URL— datastoresRUST_LOG— tracing filter (e.g.info,rolter_gateway=debug)- provider key vars referenced by
api_key_env(e.g.OPENAI_API_KEY)
CLI flags override env, which override file values.