Everything is an environment variable. There are no flags and no config file — which makes the gateway trivially deployable in a container and keeps its whole configuration surface visible in one place.
Server#
| Variable | Default | Description |
|---|---|---|
SKYL_ADDR | :8080 | The listen address. |
SKYL_AUTH_TOKENRequired | — | The bearer token callers must present. There is no flag to disable authentication: the server refuses to start without this, because an accidental open relay to billed endpoints must not be one environment variable away. |
SKYL_DEFAULT_PROVIDER | the alphabetically first registered provider | The provider used when a request omits one. Naming a provider that is not registered is a startup error, not a runtime surprise. |
SKYL_REQUEST_TIMEOUT | 120s | Bounds a single upstream request. Applied inside each handler rather than at the router, so a streaming response is not severed mid-generation. |
SKYL_INCLUDE_RAW | false | Includes the provider's untouched response body in the `raw` field. Off by default: raw bodies can echo request content. |
Providers#
| Variable | Default | Description |
|---|---|---|
ANTHROPIC_API_KEY | — | Registers the anthropic provider when present. |
OPENAI_API_KEY | — | Registers the openai provider when present. |
GEMINI_API_KEY | — | Registers the gemini provider when present. |
SKYL_COMPAT_BASE_URL | — | Registers an OpenAI-compatible provider pointed at this host. This is the variable that triggers registration; the other two refine it. |
SKYL_COMPAT_NAME | compat | The name the compatible provider is registered under. |
SKYL_COMPAT_API_KEY | — | The credential for the compatible host. Omit it for Ollama, vLLM or LM Studio. |
A provider is registered for each key present, so an operator controls the
provider set purely through the environment. SKYL_COMPAT_BASE_URL is the
variable that triggers registration of a compatible host; the other two
refine it.
Client behaviour#
These are passed through to skyl.Option on every provider client, so an
operator can see and change them — previously they were fixed at their defaults
with no way to do either.
| Variable | Default | Description |
|---|---|---|
SKYL_MAX_RETRIES | 3 | Passed through to skyl.WithMaxRetries on every provider client. |
SKYL_RETRY_BASE_DELAY | 500ms | The base of the exponential backoff, via skyl.WithRetryDelay. |
SKYL_RETRY_MAX_DELAY | 30s | The ceiling on skyl's computed backoff, via skyl.WithRetryDelay. |
SKYL_RETRY_AFTER_CAP | 5m | Bounds how long a provider's own Retry-After hint may delay a retry. Separate from the backoff ceiling, because a provider asking for 60 seconds is a normal rate-limit window while one asking for an hour should not wedge the caller. |
SKYL_ATTEMPT_TIMEOUT | 10m | Bounds a single attempt, via skyl.WithTimeout. Per attempt, not per call — the retry sequence as a whole is bounded by SKYL_REQUEST_TIMEOUT. |
Hardening#
| Variable | Default | Description |
|---|---|---|
SKYL_AUTH_TOKENS | — | Additional labelled tokens, as "label:token,label:token". The label appears in logs and metrics; the token never does. This is what makes rotation possible without downtime. |
SKYL_MAX_CONCURRENT | unlimited | Caps in-flight requests using chi’s Throttle middleware. Set it: an unbounded gateway converts a traffic spike into a provider rate-limit incident. |
SKYL_ALLOWED_ORIGINS | CORS disabled | A comma-separated allow-list of browser origins. Leave it unset unless a browser calls the gateway directly — which means shipping a token to the browser, so think first. |
SKYL_HEARTBEAT_INTERVAL | 15s | How often an SSE keep-alive frame is written on an idle stream, so proxies do not time the connection out mid-generation. |
SKYL_AUTH_TOKENS takes label:token,label:token. The label appears in
logs and metrics; the token never does. That is what makes rotation possible
without downtime — add a new token, migrate callers, remove the old one.
Observability#
| Variable | Default | Description |
|---|---|---|
SKYL_METRICS | false | Enables /metrics and installs the telemetry hook on every provider client, so the metrics cover all providers rather than whichever one happened to be wired first. |
Enabling metrics installs the telemetry hook on every provider client, so the metrics cover all providers rather than whichever one happened to be wired first.
Validation#
Every parse error names its variable:
SKYL_REQUEST_TIMEOUT: time: invalid duration "12x"Without that, an operator sees invalid duration "12x" with nothing to say
which of a dozen settings is wrong, on a process that has already refused to
start.
Usage#
A production configuration
SKYL_ADDR=:8080
SKYL_AUTH_TOKEN=<32 random bytes, hex>
SKYL_AUTH_TOKENS=rotating:<second token>
SKYL_MAX_CONCURRENT=64
SKYL_REQUEST_TIMEOUT=120s
SKYL_ATTEMPT_TIMEOUT=60s
SKYL_MAX_RETRIES=3
SKYL_METRICS=true
ANTHROPIC_API_KEY=sk-ant-...
OPENAI_API_KEY=sk-...
SKYL_DEFAULT_PROVIDER=anthropicSKYL_ADDR=:8080
SKYL_AUTH_TOKEN=<32 random bytes, hex>
SKYL_AUTH_TOKENS=rotating:<second token>
SKYL_MAX_CONCURRENT=64
SKYL_REQUEST_TIMEOUT=120s
SKYL_ATTEMPT_TIMEOUT=60s
SKYL_MAX_RETRIES=3
SKYL_METRICS=true
ANTHROPIC_API_KEY=sk-ant-...
OPENAI_API_KEY=sk-...
SKYL_DEFAULT_PROVIDER=anthropicLocal development against the sandbox
SKYL_AUTH_TOKEN=local-dev-token
SKYL_COMPAT_BASE_URL=http://127.0.0.1:8099/compat/v1
SKYL_COMPAT_API_KEY=sandbox-key
SKYL_COMPAT_NAME=sandbox
SKYL_INCLUDE_RAW=trueSKYL_AUTH_TOKEN=local-dev-token
SKYL_COMPAT_BASE_URL=http://127.0.0.1:8099/compat/v1
SKYL_COMPAT_API_KEY=sandbox-key
SKYL_COMPAT_NAME=sandbox
SKYL_INCLUDE_RAW=trueTroubleshooting#
The process refuses to start and names a variable
A parse error. The message includes the variable name and the underlying reason — that naming is deliberate, because a bare parse error on startup is otherwise unactionable.
Retries seem to ignore my settings
SKYL_MAX_RETRIES and friends apply to the upstream client. They are
bounded by SKYL_REQUEST_TIMEOUT, so a short request timeout will cut a retry
sequence short regardless of the retry count.
raw is missing from responses
SKYL_INCLUDE_RAW defaults to false. Unlike the library, where Raw is
always populated, the gateway omits it by default because the body crosses a
network boundary and can echo request content back to a caller who should not
see it.