Troubleshooting¶
OpenAI rejects reasoning off¶
A reasoning_effort_not_allowed error means the selected subscription account
refused the requested effort. Check /v1/models for the account pool's advertised
modes. To permit minimum reasoning when none is rejected, enable the desktop
Settings option Allow minimum reasoning when an account refuses reasoning off
or set [providers.openai] reasoning_fallback = true and restart the relay.
The relay attempts none first, then retries once on the same account with its
lowest advertised effort. Inspect the reasoning_fallback traffic event or
airelays.reasoning_fallback response metadata for the effective value. It may
use more reasoning tokens. If the retry fails, the error is returned without
further automatic retries. Leave the option disabled when exact reasoning
settings are required. See configuration.
Desktop app install¶
The desktop builds are not notarized (macOS) or code-signed (Windows), so the operating system may warn when you install them by hand. The one-line installers in the README avoid these prompts on macOS and Linux.
- macOS says "AIRelays is damaged" or "cannot be verified" after you downloaded the DMG in a browser: clear the quarantine flag, then open the app again.
xattr -dr com.apple.quarantine /Applications/AIRelays.app
Alternatively, open System Settings → Privacy & Security and choose
Open Anyway for AIRelays.
- The installer reports that no desktop installer exists for your system:
desktop builds cover Apple Silicon Macs, x86_64 Linux, and x64 Windows.
On other machines, including Intel Macs, use the headless installer
(scripts/install-headless.sh); it runs the same relay without the tray.
- Windows SmartScreen blocks the setup .exe: choose More info →
Run anyway, or use the PowerShell installer.
- Windows, AIRelays 0.18.0 and earlier: the relay shows Failed and the
Console tab (or an OpenAI sign-in) reports
ModuleNotFoundError: No module named 'threading'. Those Windows builds
shipped an incomplete embedded Python. Install 0.19.0 or later by running
the PowerShell installer again; your accounts and settings are kept.
- Linux: the AppImage does not start from a terminal with a FUSE error:
install libfuse2 (libfuse2t64 on Ubuntu 24.04), or run it with
APPIMAGE_EXTRACT_AND_RUN=1. The menu entry created by the installer
already does this when FUSE 2 is missing. GNOME needs the AppIndicator
extension to show the tray icon.
- The headless installer warns that ~/.local/bin is not on your PATH:
add export PATH="$HOME/.local/bin:$PATH" to your shell profile and open
a new terminal.
A desktop update did not install¶
AIRelays reopens on the version you had and shows why the update failed.
The full log is ~/.airelays/update/update.log.
- "Checksum mismatch": the download did not match the digest GitHub publishes. Nothing was installed. Retry later from About.
- "Check for updates again before installing": the dashboard was open across a new release; choose Check for updates, then update.
- The updater does not start (for example where policy blocks PowerShell scripts): run the one-line installer from the README, or download the installer from the release page.
Traffic logs report a cleanup error¶
The tray Settings page or GET /v1/relay/logging can report last_error, or
airelays logs can print a cleanup error. AIRelays pauses traffic-log writes
until it can enforce the configured limit; relay requests continue normally.
Check that the configured log directory is writable and has space available:
airelays logs
airelays status --json
If the limit is smaller than the history you need, archive the relevant log files first, then raise the total limit or shorten retention:
airelays logs --retention-days 30 --max-total-mb 2048 --max-file-mb 50
Use the same --config, --data-dir, or --logs-dir arguments as the
running relay. Apply the policy again, then confirm that last_error is empty
and over_budget is false. See traffic-log retention
for the policy, ownership boundaries, and permanent-deletion behavior.
A model is missing or a Claude alias is unclear¶
Use the Models tab's Refresh button or GET /v1/models?refresh=true to
reload both provider catalogs. OpenAI discovery uses client_version =
"auto" by default; an explicit older version pin can select an older
catalog. Update the installed Codex CLI to follow newer catalogs.
For Claude, update the configured claude CLI and inspect the alias's
airelays.resolved_model in /v1/models. Concrete ids are also listed.
If discovery fails, inspect providers.claude.models_discovery_error in
GET /v1/relay/status; configured ids and the last successful catalog
remain available. See model discovery and
configuration.
"Another AIRelays relay is already running with the data directory …"¶
Only one relay can use a data directory at a time, so accounts, uploads, conversations and logs are never shared between two relays. The message gives the running relay's process id, and the command exits with status 75. The desktop app reports this case with a notification and does not retry.
- if the desktop app is running, it already runs the relay: use that one, or quit the app first (tray menu → Quit AIRelays)
- if a terminal session runs
airelays serve, stop it first - to run two relays on purpose, give each its own
--data-dir
401 Missing or invalid AIRelays bearer token¶
- run
airelays status - run
airelays doctor --skip-response - confirm the relay token is present
- confirm the client is calling
http://HOST:PORT/v1/... - use
airelays token showif needed
503 No ChatGPT login found¶
- run
airelays status - run
airelays doctor --skip-response - if the OpenAI runtime is enabled, run
airelays login - on a server or over SSH, run
airelays login --device - if the browser flow cannot bind
localhost:1455, useairelays login --device
An OpenAI account is missing after upgrading to 0.16.1¶
AIRelays 0.16.1 and later store OpenAI sign-ins only as owner-only files (ADR 0008). Sign-ins that 0.16.0 or earlier kept in the OS keychain, Credential Manager, or Secret Service are no longer read.
- run
airelays doctor --skip-response; it lists those accounts underopenai_keyring_sign_ins - sign in again with
airelays login(or Sign in in the desktop app), once per account - you can remove the old
AIRelays Authentries from your OS keychain afterwards
I opened the login URL on my laptop and got a connection error at localhost:1455¶
The browser flow's sign-in redirect goes to localhost:1455 on the machine
running the browser — pasting the URL into a browser on another computer
sends the redirect to the wrong machine, so the login on the server never
completes and eventually times out.
Two fixes:
- Device-code login (recommended):
airelays login --deviceprints a short code you approve from a browser on any device. This is the default on SSH sessions and displayless Linux. - SSH tunnel (if you specifically need the full browser flow, e.g. for a
browser-profile picker): run
ssh -L 1455:localhost:1455 user@serverfirst, then open the printed URL in your local browser. The tunnel can be opened even afterairelays loginhas started waiting.
Claude CLI not found, or a .cmd shim is refused (Windows)¶
- run
airelays doctor --skip-response; the Claude check shows whichclaudethe relay found - AIRelays looks for
claudeon PATH and in the usual install locations; aclaudeinstalled after the tray started is found after a relay restart - on Windows, install Claude Code with its native installer (
claude.exe). npm'sclaude.cmdshim is refused because cmd.exe would re-parse request text; to use an npm install, set[providers.claude].commandto["node", "<path to the package's cli.js>"]; with the desktop app, set theAIRELAYS_CLAUDE_COMMANDenvironment variable to the same JSON array instead, because the app rewritesconfig.toml - a
413on a Claude request means its system prompt or JSON schema exceeds the OS command-line limit; shorten it
503 No Claude account is connected to AIRelays¶
AIRelays uses only Claude accounts it owns. Your personal Claude Code sign-in
(claude auth login or /login), an inherited CLAUDE_CONFIG_DIR, and an
exported CLAUDE_CODE_OAUTH_TOKEN are ignored, so signing in to Claude Code
does not connect the relay. Until an account is connected, claude:*
requests return 503 provider_auth_error and /v1/models lists no Claude
models.
- run
airelays claude login(desktop: Sign in → "In a browser") once per subscription - on a headless host or under systemd/docker, run
claude setup-tokenon any machine with a browser, thenairelays claude set-token(desktop: "With a token"); the token is stored 0600 in~/.airelays/claude-token airelays doctorreports aCLAUDE_CODE_OAUTH_TOKENset in the relay's environment; AIRelays ignores it
After upgrading from 0.15.0 or earlier, installs that relied on the personal sign-in or an exported token need this once. Claude Code refresh tokens rotate, so AIRelays cannot copy the personal sign-in. Profiles already enrolled keep working, and switching or signing out Claude Code does not affect the relay.
A Claude request fails: which account or model is affected¶
AIRelays decides what a Claude CLI failure affects before taking any account out of rotation, and the error message says which case applies:
- Your request (a bad image, an unreachable URL, a prompt that is too long): 422 or 413 explaining what to change. No account is paused.
- One model on one account: the API does not serve that model to the
account (a "not found" or permission refusal while a live usage check shows
the account's sign-in works). Only that model is paused on that account
(
unavailable_modelsin/v1/relay/status); other models keep working, and another account is tried. If none can serve it, you get 404model_unavailable; use another model from/v1/models. - A passing sign-in refusal: the CLI reported a sign-in error, but the live usage check shows the sign-in works (for example a token that was just renewed). Nothing is paused; another account is tried, and retrying usually works.
- An account's usage limit or a temporary failure: the account pauses
until its reset time (or
account_cooldown_secondswhen the reset is unknown), or about 30 seconds after a temporary failure, and another account is tried. The message lists each account's state, withRetry-After; a wait over a minute also carriesx-should-retry: false. When the check cannot tell whether the sign-in works, the account pauses the same way. - An account's sign-in: the usage check confirms the sign-in was rejected (an explicit authentication error, such as a revoked token). The account is marked as needing a new sign-in, leaves rotation until you sign in again, and the message names the command to run (see below).
502 invalid_json_output on a Claude model¶
You asked for response_format: {"type": "json_object"} and the model's
answer was not a single JSON object. AIRelays checks JSON-mode answers before
returning them, so you never receive broken JSON with a success status. Retry
the request, or send json_schema with a schema: the Claude CLI enforces
named schemas natively.
A Claude request with tools fails with 502 cli_tools_unavailable¶
The Claude CLI could not load the request's tools, so AIRelays stopped the request instead of answering without them.
- check each tool's
parameters: it must be a JSON Schema of typeobject(other shapes are refused with422) - the tool helper is a small file inside the installed AIRelays package, started with the relay's own Python: if the installation was moved or partly removed, reinstall AIRelays (or the desktop app)
- retry the request; the error names the tools that were missing
502 cli_unexpected_tools means the CLI reported or attempted a tool that
AIRelays did not define. The request was stopped before anything ran; update
AIRelays and the Claude CLI, then retry. Neither error pauses an account.
422 for an image or document¶
Both runtimes check images and documents before any upstream call.
- images must be PNG, JPEG, GIF or WebP, as a base64
data:URL, an http(s) URL, or afile_idfromPOST /v1/files; a wrong declared type is corrected automatically - documents (
fileparts) must be PDFs or UTF-8 text files - on Claude models, "Claude could not read N of the M images or documents" means the CLI could not process a file (for example a CMYK JPEG or an animated image): convert it to a standard PNG, JPEG or PDF and retry
An answer stops early with finish_reason: "length"¶
The request set an output-token limit (max_completion_tokens,
max_tokens, or max_output_tokens on /v1/responses), and the model
reached it. Both account types enforce the limit, as OpenAI does.
- raise the limit, or remove it to let the model finish
- on GPT models the minimum is 16; lower values return the upstream's
400 - check your client's defaults: some SDK helpers set a small
max_tokens
A Claude account needs to sign in again¶
Use airelays claude accounts to find the account id, then
airelays claude login --replace ACCOUNT. Choose the same account in the
browser. A different identity is not eligible for requests through the enrolled
profile. To add another subscription, use airelays claude login without
--replace. The desktop offers Sign in again on unavailable profile rows.
For the setup-token account (default), run claude setup-token and
airelays claude set-token again.
Do not move or rename a profile's config/ directory: the path identifies its
macOS Keychain entry. After a machine migration, sign in again on the new host.
On macOS, run the relay in your login session (the desktop app does), not over
SSH or as a LaunchDaemon, so Claude Code can use the keychain. An unavailable
usage meter alone does not require signing out.
A Claude usage bar is stale or missing¶
Usage comes from each profile's Claude Code (get_usage). A stale bar shows
the last good reading with its age; the reason is stale_reason in
GET /v1/subscription/status?provider=claude&all_accounts=true:
usage_unavailable: Claude returned no live data (for example, the upstream rate-limited its usage endpoint). Retries back off from 5 to 60 minutes; requests are unaffectednonessential_traffic_disabled: removeCLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFICfrom the relay's environmentcli_unsupported: update Claude Code (verified with 2.1.292+)cli_failed: check that the configuredclaudebinary runs; seeairelays doctor
The setup-token account never has a bar (Claude reports no usage for setup
tokens); add a browser profile with airelays claude login for a meter.
Stale readings never unblock or rank an account.
Desktop app shows "Sign-in expired" on an OpenAI account¶
OpenAI ended that account's stored session (for example after signing in to Codex elsewhere), so the relay can no longer refresh its token. The account is out of rotation until you sign in again.
- click Sign in again next to the badge and pick the same account in the browser — the stored sign-in is replaced in place, no sign-out needed
- the usage bars return within a few seconds of a successful sign-in
- the raw upstream error is available on hover over the note under the row
- from the CLI,
airelays logindoes the same repair: signing in with an already-enrolled account refreshes that account's stored credentials
Desktop app shows "Running — not responding"¶
The relay process is alive but did not answer the app's health probe — usually heavy system load or a long request burst.
- it recovers on its own once the relay answers again; the label flips back to "Running"
- if it persists, open the Console tab for relay output, or use Restart
- Stop/Restart keep working: the app still manages the process
404 model_not_found or 422 on chat and completions¶
A 404 with code model_not_found means the model id is not available:
confirm it appears in /v1/models (a discovered alias, concrete
claude-* id, configured override, or an OpenAI model your account's
catalog lists).
A 422 names what the relay cannot honor. Checks:
- on Claude models: remove audio, a forced tool call (
tool_choicerequiredor a named tool),parallel_tool_calls: false,strict: truetools, non-function tools, andprevious_response_id(useconversation); an image Claude cannot read (corrupt, CMYK, animated) must be converted to a standard PNG, JPEG or PDF - remove
stop,logprobs,top_logprobs,logit_bias,web_search_optionsandmoderation: neither upstream can honor them, so AIRelays rejects them rather than returning output that ignores them - remove other unsupported generation controls
If a request succeeds but behaves as if a parameter were missing, check the
x-airelays-ignored-parameters response header: it lists every parameter
AIRelays dropped, including ones it does not recognize.
Cursor custom OpenAI endpoint errors¶
If Cursor is configured to call AIRelays as a custom OpenAI-compatible endpoint and Agent/Edit flows fail, first identify which malformed request family you are seeing.
AIRelays 0.12.2 and later accepts the two Cursor request shapes reported publicly by Cursor users and staff on February 26, 2026 and July 9, 2026:
- full Responses-style bodies sent to
/v1/chat/completions - flat Responses-style
customtools / tool choices / tool calls on the chat route (for exampleApplyPatch)
AIRelays 0.12.3 and later also strips unsupported top-level caller
identity fields (user, safety_identifier) before the upstream request
and rejects OpenAI model ids that are absent from a working live
/v1/models catalog, instead of letting those failures happen upstream.
Checks:
- if you see
Only function tools are currently supported on chat routes., upgrade AIRelays - if you see
Unsupported parameter: user, upgrade AIRelays; current versions stripuser(andsafety_identifier) locally because the ChatGPT/Codex backend does not accept caller-supplied end-user ids - if you see
The '...model...' model is not supported when using Codex with a ChatGPT account., compare the chosen model againstGET /v1/models; with a working upstream catalog AIRelays now rejects unsupported OpenAI ids locally and only[providers.openai].extra_modelsshould bypass that check. On multi-account relays, if you just added or removed an account, current versions refresh that admission cache automatically before deciding - inspect the traffic log under
~/.airelays/logs/...for acompatibility_adaptationrecord showing the chat route accepted a Responses-shaped body - if the request mixes both
messagesandinput, AIRelays rejects it loudly; that payload is ambiguous and must be fixed at the client - if a
role:"tool"message does not reference a preceding assistant tool call in the same request, AIRelays rejects it as malformed instead of guessing the wrong tool type - if Cursor still fails after the normalization above, capture the inbound request body from the traffic log and compare it with the documented supported shapes in docs/api.md
The relay refuses to start: network scope or Claude guardrail¶
airelays serve and airelays init stop with a message instead of starting
an unsafe or ambiguous relay:
- "
network_scope = "any"(anyone on the internet) requiresrequire_bearer_auth = true": internet access needs the API key. Drop--no-auth(or setrequire_bearer_auth = true), or choose--network-scope privateorloopback. Invalid [security] network_scope value ...: useloopback,private, oranyinconfig.tomlorAIRELAYS_NETWORK_SCOPE.The Claude runtime does not allow trust_x_forwarded_for: settrust_x_forwarded_for = false, or disable the Claude runtime ([providers.claude] enabled = false).
See Who Can Connect and Configuration.
403 client_not_on_private_network or client_not_on_this_machine¶
The relay answered, but the client's address is outside its network scope; the check runs before auth, so the API key was not even looked at.
client_not_on_private_network: the scope isprivateand the client came from a public address, for example through router port forwarding, a cloud host, or a reverse proxy withtrust_x_forwarded_for = true.client_not_on_this_machine: the scope isloopbackon a non-loopback listener, and the client is another device.
Check what the relay enforces:
airelays status # "Who can connect"
Fix: connect from an allowed network (for example over Tailscale, which
private allows), or widen the scope with airelays serve --host 0.0.0.0
--network-scope any (the API key stays required). The desktop app's Who
can connect selector does the same. A refused request is logged as an
endpoint_rejected traffic record with reason network_scope.
The Internet address refuses connections¶
You chose Anyone on the internet (or network_scope = "any"), but
clients cannot reach the public address.
- Confirm the scope:
airelays statusmust showany (anyone on the internet ...). A listener on127.0.0.1narrows every scope toloopback; listen on0.0.0.0. - Forward the relay's TCP port (CLI default
8080, desktop8317) on your router to this machine's private address, the LAN row in the desktop Connect card. - Allow incoming connections for the relay in the macOS application firewall or your Linux host firewall.
- Test from outside your network, for example from a phone on mobile data. From inside the same network the public address only answers when the router supports NAT loopback (hairpinning); use the LAN address there.
- Some providers place customers behind carrier-grade NAT, where port
forwarding on your router cannot reach you. Use a VPN such as Tailscale
(allowed by the default
privatescope) or a reverse proxy on a host with a public address. - The desktop shows "The public address could not be looked up" when
api.ipify.organdcheckip.amazonaws.comare unreachable; the relay still serves the scope, and clients can use your router's public IP.
Verify recovery from the outside device:
curl http://<PUBLIC_IP>:<PORT>/healthz
429 Too many invalid authentication attempts from this IP¶
- wait for the
Retry-Afterwindow - update the client to the correct relay token
- rotate the token if needed
413 on uploads or requests with files¶
- confirm the file is below the per-file upload ceiling
- confirm the relay has not reached the total stored-upload quota
413 request_too_largeon a Claude request: the images and documents in one request (inline or byfile_id) are limited to 32 MiB in total, the Claude API's request limit; split the files across requests
502 / 429 with server_is_overloaded or usage_limit_reached, or slow answers while the upstream is degraded¶
- the upstream itself is failing or out of quota; the error body carries the upstream's own code and message
- before surfacing the error, the relay retried automatically (default 3
retries waiting 5s/20s/60s, each re-running account failover), which is why
a failing request can take a minute or more before answering — check the
traffic log for
retry_backoff/retry_skipped/upstream_stream_errorrecords to see what happened - tune or disable with
retry_attempts/retry_backoff_seconds([providers.openai], or desktop Settings → Providers) - a
429that names a reset far in the future is not retried (waiting a minute cannot help a window that resets in hours); with several accounts enrolled, the message reports the earliest account recovery - a
400invalid_request_error(for example an input that exceeds the model's context window) is answered immediately with the upstream's own error: it is deterministic, so it is never retried, never rotated to another account, and never benches an account — fix the request instead - "All N OpenAI accounts are at their limits" is only claimed when every account is benched by real limit evidence (a quota rejection or the usage report); rounds of transient upstream failures answer "All N OpenAI accounts failed for this request" instead
Live upstream verification¶
Use airelays doctor when local state looks correct but client requests still
fail. It checks local setup, then verifies the OpenAI upstream /models route
and runs a tiny /responses smoke request when the OpenAI runtime is enabled
and logged in.
airelays doctor
Use airelays doctor --skip-response when you want setup and model-list checks
without sending a generation request.