Configuration¶
AIRelays resolves settings in this order:
- CLI flags
AIRELAYS_*environment variables- legacy
OPENAI_ENDPOINT_*migration variables where supported ~/.config/airelays/config.toml- built-in defaults
Traffic-log retention is live configuration: a saved policy in the selected
log directory takes precedence over its [logging] defaults. See
Traffic log retention below.
Traffic log retention¶
Traffic logs rotate hourly and at a size limit. Cleanup runs on startup, every 60 seconds while the relay is running (including when idle), on rotation, and when applying a policy. Both age and size limits apply; whichever requires removing a file first wins. Defaults are deliberately bounded on upgrades too.
| Parameter | Default | Meaning |
|---|---|---|
retention_days |
7 |
Maximum age since a file's last write; 30 means 30 days, 0 disables only the age limit. Range: 0–36500. |
max_total_mb |
1024 |
Total managed traffic-log budget in MiB (1 GiB by default). Range: 1–1048576. |
max_file_mb |
50 |
Rotate before the next record would exceed this size in MiB. Range: 1–10240; must not exceed the total budget. |
Retention is an upper limit, not a promise of seven days of history: under load,
the disk budget removes older files sooner. Cleanup reserves room for the active
file, so usage can be below the configured budget by up to a chunk plus the last
whole file removed. Files are deleted whole, oldest modification time first.
The budget counts file contents; filesystem allocation and metadata overhead
are not included in usage_bytes.
From the CLI (also works with the relay stopped):
airelays logs # policy and current usage; no cleanup
airelays logs --retention-days 30 # keep up to a month, save and apply
airelays logs --retention-days 7 --max-total-mb 1024 --max-file-mb 50
airelays logs --json # machine-readable policy, usage, errors
airelays logs --config /path/to/config.toml --retention-days 14
The selected --config, --data-dir, and --logs-dir determine which log
directory is managed. Use the same directory as the running relay.
In the cross-platform tray app, open Settings → Traffic log retention. Choose 1 week, 1 month (30 days), or a custom duration and disk limit, then Apply log retention. The relay must be running for the tray controls; the CLI can configure it offline. This section applies independently of the other settings' Save & Restart workflow. It shows disk usage and cleanup errors.
The HTTP API exposes the same policy. API/tray updates apply immediately. Other relay processes sharing the directory pick up the saved policy on their next write or within 60 seconds while idle.
Policy changes are saved atomically to <logs_dir>/.retention.json. That file
overrides the three TOML retention defaults and survives restarts, CLI sessions,
and the tray's regeneration of config.toml. Change it through the API or CLI;
editing the TOML defaults after saving a policy does not override that policy.
The stream_lines option remains a separate startup setting.
Cleanup includes existing YYYY/MM/DD-HH.log files and new size-rotated
YYYY/MM/DD-HH.<uuid>.log files. It does not manage console/stdout logs, uploads,
conversations, unrelated files, symlinks, or hard links. Use an app-owned local
log directory; every writer sharing it must run a retention-aware AIRelays
version. External writers or an older relay can defeat the disk limit.
A record larger than max_file_mb becomes an explicit log_record_omitted
JSON record with its request ID, phase, byte count and SHA-256; the payload is
omitted from the log without altering the actual request or response. Cleanup
and I/O failures are reported on stderr and through last_error; traffic
logging pauses on cleanup failure and retries, rather than growing past the
budget or failing client requests. Deletion is permanent: archive any logs you
need before first starting the upgraded relay or reducing a policy.
Default Paths¶
- config:
~/.config/airelays/config.toml - data dir:
~/.airelays - logs dir:
~/.airelays/logs - OpenAI sign-ins:
~/.airelays/auth.json(first account) and~/.airelays/accounts/<slug>/auth.json, owner-only files (see Security) - relay token file:
~/.airelays/relay-token - Claude account profiles:
~/.airelays/claude/accounts/<id>/(metadata inaccount.json; the adjacentconfig/is the profile'sCLAUDE_CONFIG_DIR) - Claude setup token:
~/.airelays/claude-token, run from~/.airelays/claude/token/config
Earlier singular AIRelay paths remain compatible for local upgrades.
Sample Config¶
[server]
host = "127.0.0.1"
port = 8080
[paths]
data_dir = "~/.airelays"
logs_dir = "~/.airelays/logs"
[auth]
storage = "file"
browser_open = false
login_timeout_seconds = 900
[upstream]
base_url = "https://chatgpt.com/backend-api/codex"
issuer_base_url = "https://auth.openai.com"
client_id = "app_EMoamEEZ73f0CkXaXp7hrann"
client_version = "auto"
request_timeout_seconds = 120.0
[security]
require_bearer_auth = true
bearer_token_file = "~/.airelays/relay-token"
auto_generate_bearer_token = false
rate_limit_per_minute = 120
rate_limit_burst = 40
concurrent_requests_per_ip = 50
failed_auth_window_seconds = 300
failed_auth_max_attempts = 8
failed_auth_block_seconds = 900
trust_x_forwarded_for = false
# Who may connect: "loopback" (this machine), "private" (default: loopback,
# RFC 1918, 100.64.0.0/10 CGNAT/Tailscale, link-local, IPv6 ULA and
# link-local) or "any" (the internet; requires require_bearer_auth = true).
# A loopback host always means "loopback". See docs/security.md#who-can-connect.
network_scope = "private"
[uploads]
max_upload_bytes = 33554432
max_total_upload_bytes = 268435456
[logging]
# Opt-in: log every raw upstream SSE line. Hundreds of records per
# streamed response (~50x log growth under load); summary records
# (request, usage, response, errors) are always logged regardless.
stream_lines = false
retention_days = 7
max_total_mb = 1024
max_file_mb = 50
[providers.openai]
enabled = true
models_cache_ttl_seconds = 300.0
# Multi-account routing: "weighted" splits requests in proportion to each
# account's remaining capacity (50% left vs 100% left -> 1:2); "ping_pong"
# takes turns in order; "balanced" (default) routes to the account with
# the most remaining weekly quota so consumption equalizes as a
# percentage of each plan's capacity; "round_robin" sends equal request
# counts; "ordered" drains the first account first.
balance = "balanced"
# Opt-in: if upstream explicitly rejects reasoning=none, retry once on the
# same account at its lowest advertised reasoning level. This may use more
# tokens. False preserves the exact requested reasoning setting.
reasoning_fallback = false
# Extra model ids to advertise in /v1/models beyond the upstream catalog,
# for operator-confirmed models omitted from automatic discovery.
extra_models = []
# Fallback bench duration (seconds) when a limited account's reset time is
# unknown; upstream-reported reset times are used when available.
account_cooldown_seconds = 300
# Automatic retry for failed upstream LLM calls. Each retry re-runs the
# full account-pool failover pass, then waits the next backoff delay.
# 0 disables. A schedule shorter than the attempt count repeats its last
# delay. Retries only happen before any response byte reached the client
# (non-streaming requests, and the pre-header phase of streaming ones);
# retries that cannot succeed (a quota window that resets far beyond the
# backoff budget) are skipped so the honest 429 is not delayed.
retry_attempts = 3
retry_backoff_seconds = [5, 20, 60]
# Image generation (/v1/images/*): the per-image upstream timeout, and
# whether to read the image quota with a prompt-less probe (a free 400 that
# carries the quota headers) alongside each usage probe. Off, the image
# meter updates only when images are generated.
image_timeout_seconds = 300.0
image_usage_probe = true
[providers.claude]
enabled = true
bin = "claude"
command = [] # full argv, wins over `bin` (see below)
timeout_seconds = 600.0
max_concurrent_requests = 2
balance = "balanced" # balanced, weighted, round_robin, ping_pong, or ordered
account_cooldown_seconds = 300 # retry delay when no reset time is reported
strip_api_key_env = true
models = ["claude:sonnet", "claude:opus", "claude:haiku", "claude:fable"]
oauth_token_file = "~/.airelays/claude-token" # written by `airelays claude set-token`
Listener And Network Scope¶
[server] host and port choose where the relay listens; [security]
network_scope chooses which client addresses it serves:
host = "127.0.0.1"(CLI default): this machine only, whatever the scope.host = "0.0.0.0"withnetwork_scope = "private"(the default scope): devices on your home, office or VPN network. The desktop app's default (0.0.0.0:8317).host = "0.0.0.0"withnetwork_scope = "any": anyone who can reach the port, for example through router port forwarding. Requiresrequire_bearer_auth = true.
airelays init --host 0.0.0.0 --port 8080 --network-scope private writes
these keys into a new config file (--force rewrites an existing one).
airelays serve --host, --port and --network-scope override them for one
server process. See Who Can Connect for the
address ranges, the 403 codes, the internet setup steps and the
trust_x_forwarded_for interplay.
Important Environment Variables¶
AIRELAYS_CONFIGAIRELAYS_HOSTAIRELAYS_PORTAIRELAYS_DATA_DIRAIRELAYS_LOGS_DIRAIRELAYS_CLAUDE_COMMAND(a JSON array, for example["node", "C:/npm/cli.js"])AIRELAYS_BROWSER_OPENAIRELAYS_LOGIN_TIMEOUT_SECONDSAIRELAYS_UPSTREAM_BASE_URLAIRELAYS_ISSUER_BASE_URLAIRELAYS_CLIENT_IDAIRELAYS_CLIENT_VERSIONAIRELAYS_REQUEST_TIMEOUT_SECONDSAIRELAYS_REQUIRE_BEARER_AUTHAIRELAYS_NETWORK_SCOPE(privatedefault,loopback, orany;anyrequires bearer auth).airelays serve --network-scope <scope>overrides it for one server process; an unknown value stops the relay at startup. See Who Can Connect.AIRELAYS_TRUST_X_FORWARDED_FOR(falsedefault; refused with the Claude runtime)AIRELAYS_BEARER_TOKENAIRELAYS_BEARER_TOKEN_FILEAIRELAYS_LOG_STREAM_LINESAIRELAYS_ENABLE_OPENAIAIRELAYS_OPENAI_MODELS_CACHE_TTL_SECONDSAIRELAYS_OPENAI_BALANCE(balanceddefault,weighted,round_robin,ping_pong, orordered)airelays serve --balance <strategy>overrides both the file and the variable for that server process;airelays balance <strategy>switches the running relay live (PUT /v1/relay/balance), without a restart; the desktop Settings selects do the same.AIRELAYS_OPENAI_EXTRA_MODELS(comma-separated ids advertised beyond the upstream catalog)AIRELAYS_OPENAI_REASONING_FALLBACK(falsedefault). When enabled, a typedreasoning_effort_not_allowedrejection ofnonetriggers one retry on the same account with its lowest advertised reasoning level. No fallback occurs merely becausenoneis absent from a catalog, for unrelated errors, or after output begins. If the fallback attempt fails, its error is returned without further automatic retries. Successful responses exposeairelays.reasoning_fallbackwithrequested,effective, andreason; streaming chat/completion responses include it on the final chunk, and Responses streams include it on response objects. Traffic logs record thereasoning_fallbackphase. The desktop Settings toggle preserves this policy across restarts. Strict behavior is restored by disabling the option.AIRELAYS_OPENAI_ACCOUNT_COOLDOWN_SECONDSAIRELAYS_OPENAI_RETRY_ATTEMPTS(automatic retries for failed upstream calls;3default,0disables)AIRELAYS_OPENAI_RETRY_BACKOFF_SECONDS(comma-separated wait before each retry;5,20,60default)AIRELAYS_IMAGE_TIMEOUT_SECONDS(per-image upstream timeout for/v1/images/*;300default)AIRELAYS_IMAGE_USAGE_PROBE(read the image quota with a free prompt-less probe on each usage probe;truedefault)AIRELAYS_ENABLE_CLAUDE(legacyAIRELAYS_ENABLE_CLAUDE_EXPERIMENTALis still honored)AIRELAYS_CLAUDE_BINAIRELAYS_CLAUDE_TIMEOUT_SECONDSAIRELAYS_CLAUDE_MAX_CONCURRENT_REQUESTSAIRELAYS_CLAUDE_STRIP_API_KEY_ENVAIRELAYS_CLAUDE_MODELSAIRELAYS_CLAUDE_OAUTH_TOKEN_FILE(setup-token file; default~/.airelays/claude-token)
Relay Token Inputs¶
AIRelays resolves the relay token in this order:
AIRELAYS_BEARER_TOKEN- the configured
bearer_token_file
Override examples:
AIRELAYS_BEARER_TOKEN='YOUR_AIRELAYS_TOKEN' airelays serve --port 8080
airelays serve --bearer-token-file /path/to/relay-token --port 8080
Provider Notes¶
[upstream] client_version = "auto" discovers the installed Codex CLI's
version for model-catalog requests, with a tested minimum of 0.153.4 for
standalone installations. The local version probe is bounded to three
seconds and cached for five minutes. Updating Codex allows discovery to
follow newer version-gated catalogs without adding model names to AIRelays.
The former shipped value 0.124.0 is treated as auto, including existing
desktop configuration files. Other explicit version pins are honored and
can select an older catalog.
OpenAI runtime:
- enabled by default
- stores each sign-in as an owner-only file on every OS;
storageonly acceptsfilein practice (autoandkeyringare read asfile, andairelays doctorflags sign-ins an older version kept in the OS keyring) airelays loginmanages its subscription session- caches successful upstream model-list responses for
models_cache_ttl_secondsseconds by default - the model-list cache is process-local, in-memory, and disabled when
models_cache_ttl_seconds = 0 - cache state is visible under
providers.openai.models_cacheinGET /v1/relay/status extra_modelsdefaults to an empty list. Existing configured entries continue to extend a successful catalog and are deduplicated against it.
AIRELAYS_MODELS_CACHE_TTL_SECONDS remains accepted as a shorter alias for
AIRELAYS_OPENAI_MODELS_CACHE_TTL_SECONDS.
Claude runtime:
- discovers models and alias resolutions from the installed CLI, without
generating text; its model catalog uses
models_cache_ttl_secondstoo - configured
modelsextend discovery and remain available as fallback when CLI discovery is unavailable -
use
GET /v1/models?refresh=trueor the desktop Models Refresh button to reload provider catalogs immediately -
enabled by default; set
[providers.claude].enabled = falseorAIRELAYS_ENABLE_CLAUDE=falseto opt out (requests still require the localclaudeCLI and at least one AIRelays Claude account) - uses the local
claudeCLI airelays claude loginadds an isolated account by running Claude Code's browser sign-in inside a new relay-owned profile; credentials remain CLI-owned- each account has its own
CLAUDE_CONFIG_DIR, usage cache, model catalog, andmax_concurrent_requestslimit balancedefaults tobalanced: available execution slots first, then lowest fresh weekly usage;weightedranks the same way,round_robinandping_pongrotate, andorderedselects the first eligible profile in stable account-id orderAIRELAYS_CLAUDE_BALANCEandAIRELAYS_CLAUDE_ACCOUNT_COOLDOWN_SECONDSoverride the corresponding Claude configuration values- headless login:
claude setup-tokenon a machine with a browser, thenairelays claude set-token; the token is stored 0600 atoauth_token_fileand runs as the inference-only accountdefaultin its own relay-owned configuration directory - only AIRelays-owned accounts are used: the personal Claude Code sign-in
(
~/.claude), an inheritedCLAUDE_CONFIG_DIR, andCLAUDE_CODE_OAUTH_TOKENin the relay's environment are ignored - every
claudeprocess gets an explicit environment, its profile'sCLAUDE_CONFIG_DIR, and a relay-owned working directory - finding
claude:command(a full argv such as["node", "/path/cli.js"]) wins; otherwisebinis looked up on PATH and in the usual install locations (~/.local/bin,~/.claude/local, Homebrew, Volta, bun, nvm; on Windows%USERPROFILE%\.local\bin, Volta and WinGet). The path is used as found, so Claude Code's auto-updates keep working - Windows needs a real
claude.exe; npm'sclaude.cmdshim is refused because cmd.exe would re-parse request text. Install Claude Code with its native installer, or setcommandto runnodewith the npm package'scli.js(desktop app:AIRELAYS_CLAUDE_COMMAND, since the app rewritesconfig.toml) - system prompts are passed through a private file when the CLI supports
it; a request whose arguments still exceed the OS command-line limit gets
413instead of a launch failure - usage comes from Claude Code's
get_usage(verified with 2.1.292+);CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFICin the relay's environment stops it and marks readingsnonessential_traffic_disabled - follows the relay's protected or open local auth mode
- served on the relay's listener (local or network); refused together with
trust_x_forwarded_for