# AIRelays

> AIRelays is a local OpenAI-compatible HTTP server backed by your own subscription logins, with account balancing for OpenAI and Claude. Claude uses AIRelays-owned local CLI profiles, never your personal Claude Code sign-in. It ships as a CLI/server package and as a cross-platform desktop tray app.

## Document Index

- README.md: overview, install (CLI and desktop), quick start, compatibility layer
- docs/getting-started.md: setup, custom CLI configuration, multi-account sign-in, verification
- docs/configuration.md: config file shape and environment overrides
- docs/security.md: relay auth, open local relay mode, Claude guardrails
- docs/capabilities.md: what works on GPT models and on Claude models, side by side
- docs/api.md: routes, compatibility adaptations, provider limits
- docs/architecture.md: request flow and module boundaries
- docs/subscription-status.md: usage reporting for OpenAI and Claude
- docs/faq.md: common questions and limits
- docs/troubleshooting.md: symptoms, causes, fixes
- desktop/README.md: combined connection/access card, hide-on-blur dashboard, tray controls, build, supervision
- docs/adr/README.md: durable design decisions

---

## Capabilities (from docs/capabilities.md)

AIRelays serves GPT models (your ChatGPT subscription) and Claude models (your
Claude subscription) through one OpenAI-compatible endpoint,
`http://127.0.0.1:8317/v1`, with the same API key. The model name picks the
account type: `gpt-…` models use the ChatGPT runtime, `claude:…` and
`claude-…` models use the Claude runtime. `/v1/models` lists both, with each
model's capabilities under `airelays.capabilities`.

This page is the reference for what works on each account type. Details for
each route are in [API Notes](api.md).

Legend: ✅ supported · ⚠️ accepted but not applied, and listed in the
`x-airelays-ignored-parameters` response header · ❌ refused with a clear
error (`422`, or `501` for whole endpoints).

### Routes and responses

| Capability | GPT models | Claude models |
|---|---|---|
| `POST /v1/chat/completions` | ✅ | ✅ |
| `POST /v1/completions` | ✅ | ✅ |
| `POST /v1/responses` | ✅ | ✅ |
| Streaming (`stream: true`) on all three routes | ✅ | ✅ |
| Standard `usage` (`prompt_tokens`, `completion_tokens`, cached and reasoning details) | ✅ | ✅ |
| `stream_options.include_usage` | ✅ | ✅ |
| `GET /v1/models`, `GET /v1/models/{model}` | ✅ | ✅ |

### Generation controls

| Capability | GPT models | Claude models |
|---|---|---|
| Output-token limit (`max_completion_tokens`, `max_tokens`, `max_output_tokens`) | ✅ enforced upstream; at least 16 | ✅ the run stops at the limit |
| `reasoning_effort` | ✅ `none`, `low`, `medium`, `high`, `xhigh` (per model catalog) | ✅ `low`, `medium`, `high`, `xhigh`, `max` (per model) |
| `temperature`, `top_p`, `presence_penalty`, `frequency_penalty` | ⚠️ | ⚠️ |
| `seed`, `user`, `safety_identifier` | ⚠️ | ⚠️ |
| `stop`, `logprobs`, `top_logprobs`, `logit_bias` | ❌ | ❌ |
| `n` greater than 1 | ❌ | ❌ |

A response cut by the output-token limit reports `finish_reason: "length"`
(or `status: "incomplete"` on `/v1/responses`), as on OpenAI.

### Structured output

| Capability | GPT models | Claude models |
|---|---|---|
| JSON mode (`response_format: {"type": "json_object"}`) | ✅ native; the messages must mention JSON | ✅ the answer is checked to be one JSON object |
| `json_schema` | ✅ strict-mode rewrite when `strict: true` | ✅ enforced natively by the CLI |

### Tool calls

| Capability | GPT models | Claude models |
|---|---|---|
| Function tools; calls returned as `tool_calls` for your application to run | ✅ | ✅ AIRelays never runs a tool ([how](api.md#tool-calls-on-claude-models)) |
| `tool_choice` `auto` and `none` | ✅ | ✅ |
| Forced call (`required` or a named tool) | ✅ | ❌ |
| `allowed_tools` | ✅ | ✅ in `auto` mode |
| `parallel_tool_calls: false` | ✅ | ❌ |
| `strict: true` function tools | ✅ | ⚠️ run non-strict |
| `custom` (grammar) tools | ✅ | ❌ |
| Deprecated `functions` / `function_call` | ✅ | ❌ |
| Server-side web search (`web_search` tool on `/v1/responses`) | ✅ | ❌ |
| `web_search_options` on chat completions | ❌ | ❌ |

### Inputs, files and conversations

| Capability | GPT models | Claude models |
|---|---|---|
| Images: PNG, JPEG, GIF, WebP as data URL, http(s) URL or local `file_id` | ✅ | ✅ |
| PDFs and UTF-8 text files (`file` parts: `file_data` or `file_id`) | ✅ | ✅ |
| `/v1/files` (upload, list with pagination, retrieve, delete) | ✅ | ✅ |
| `/v1/conversations`, `conversation` on every route | ✅ | ✅ |
| One conversation continued across GPT and Claude | ✅ | ✅ (GPT `custom` tool calls in the history are not supported on Claude) |
| `previous_response_id` | ❌ use `conversation` | ❌ use `conversation` |
| Audio input, `prediction`, `moderation` | ❌ | ❌ |

An image or document a model cannot read returns `422` on both account types,
before any upstream call.

### Endpoints outside text generation

| Capability | GPT models | Claude models |
|---|---|---|
| Embeddings (`/v1/embeddings`) | ❌ `501` | ❌ `501` |
| Image generation, audio, realtime | ❌ `501` | ❌ `501` |

Neither subscription offers embeddings: the ChatGPT subscription refuses
embedding models, and Anthropic has no embedding model.

### Accounts

| Capability | GPT models | Claude models |
|---|---|---|
| Several accounts, balancing and failover | ✅ | ✅ |
| Usage and quota reporting (`/v1/subscription/status`) | ✅ | ✅ for browser sign-ins; not for the setup-token account |

### Streaming notes

Both account types stream answers as they are generated. Three parts arrive
whole instead:

- each tool call's arguments arrive in one chunk;
- on Claude models, a JSON-mode answer arrives in one chunk, because it is
  checked before it is sent;
- on Claude models, the first token takes a few seconds while the local CLI
  starts.

---

## Overview (from README.md)

AIRelays is a local OpenAI-compatible HTTP server with provider-scoped runtimes.

- The default runtime uses an AIRelays-owned ChatGPT subscription login.
- The Claude runtime uses AIRelays-owned local `claude` CLI profiles; the personal Claude Code sign-in is never used or changed.
- AIRelays protects the relay with its own bearer token by default; open local relay mode (`--no-auth`) disables only the client-token gate.
- Traffic logs rotate hourly and at a size limit. By default, AIRelays keeps
  up to 7 days and 1024 MiB of managed traffic logs.
- AIRelays is an independent third-party project designed for a single user running a local relay for personal convenience. It is not a shared, pooled, multi-user, or resale service (see DISCLAIMER.md).

## Install

Headless relay and CLI (macOS, Linux), one line, no sudo; installs the newest PyPI release with uv or into a Python 3.11+ virtualenv linked as `~/.local/bin/airelays` (installs uv first when neither uv nor Python 3.11+ is present):

```bash
curl -fsSL https://raw.githubusercontent.com/lpalbou/AIRelays/main/scripts/install-headless.sh | bash
```

Or directly from PyPI:

```bash
python -m pip install airelays
```

Desktop app, one line, no sudo or admin; downloads the newest GitHub Release installer, verifies its SHA-256 digest, installs, and starts the app. macOS (Apple Silicon) installs `AIRelays.app` into `/Applications`; Linux (x86_64) installs the AppImage as `~/.local/bin/airelays-desktop` with a menu entry; Windows (x64) runs the NSIS setup silently for the current user:

```bash
curl -fsSL https://raw.githubusercontent.com/lpalbou/AIRelays/main/scripts/install-desktop.sh | bash
```

```powershell
irm https://raw.githubusercontent.com/lpalbou/AIRelays/main/scripts/install-desktop.ps1 | iex
```

Set `AIRELAYS_VERSION=X.Y.Z` to pin a release and `AIRELAYS_NO_LAUNCH=1` to skip starting the app. Run the same command again to update. Desktop builds are not notarized or code-signed; see docs/troubleshooting.md "Desktop app install" for Gatekeeper, SmartScreen, and AppImage FUSE notes. Intel Macs and non-x86_64 Linux use the headless installer.

Desktop app details: a Tauri tray app under `desktop/` with a dashboard for relay start/stop, auth and network modes, OpenAI and Claude sign-in/sign-out, per-account usage bars, a model list with copy-ready ids, live traffic, and diagnostics. The tray icon shows connection state and pulses on request activity; the app can start at login, starts the relay when it opens, and restarts a crashed relay automatically. Both installs share the same config (`~/.config/airelays`) and data (`~/.airelays`).

In Overview, **Connect Your App** combines the endpoint and relay key with authentication and network access controls. Access changes apply immediately and restart a running relay. Clicking outside the dashboard hides it while preserving the relay, page, and unsaved edits. On macOS and Windows, left-click the tray icon to reopen it; right-click opens the menu. **Open Dashboard** remains available in the tray menu on all platforms. Automatic hiding is disabled if tray initialization fails; opening the already-running macOS app also reopens the dashboard.

## First Run (CLI)

The CLI and desktop share saved accounts and configuration by default. Use
`--config ./relay.toml` consistently with `init`, sign-in, account commands,
and `serve` to select another configuration. Shared options (`--config`,
`--data-dir`, `--logs-dir`, `--auth-storage`, `--bearer-token-file`) work
before or after subcommands; a later explicit value wins (`--auth-storage` is
accepted for compatibility only). For example:
`airelays --config ./relay.toml claude accounts --json`.

Sign-ins (OpenAI, relay token, Claude setup token) are owner-only files in the
data directory on macOS, Linux and Windows; AIRelays does not use the OS
keychain (ADR 0008). OpenAI sign-ins that 0.16.0 or earlier kept in the
keychain need one new sign-in; `airelays doctor` lists them.

Errors use OpenAI's error object (`{"error": {message, type, param, code}}`,
with a `detail` copy). Unknown models return 404 `model_not_found`; quota
rejections carry `Retry-After`; Claude streams start only once the CLI
produces output, so earlier failures are real HTTP errors. Non-empty `stop`,
`logprobs`, `top_logprobs` and `logit_bias` are rejected with 422 because
neither upstream honors them; `seed` is stripped and disclosed.
`finish_reason` includes `content_filter`, and refusals are returned in
`message.refusal`. `response_format` `json_object` works on GPT models (native JSON mode). Claude CLI failures are classified before any account is paused: request errors pause nothing, a model an account cannot serve pauses only that model there, and a rejected sign-in marks the account as needing a new sign-in; error messages name each account's state and the action to take. Parameters AIRelays does not recognize on chat and
completions are dropped and listed in `x-airelays-ignored-parameters`;
`web_search_options` and `moderation` are rejected. Every response carries
`x-request-id`; upstream quota errors carry `Retry-After`; 501 responses tell
SDKs not to retry.

Chat accepts assistant `content: null` with `tool_calls`, OpenAI `file`
content parts, `allowed_tools` tool choices, `verbosity` and
`prompt_cache_key`, and the deprecated `functions` interface end to end
(`message.function_call`, `finish_reason: "function_call"`). `json_schema`
formats are rewritten to strict-mode form only with `strict: true`; otherwise
they run with `strict: false`, OpenAI's default.
Conversations keep their whole history: each turn sees every earlier
successful turn (messages, client-answered calls, and reasoning with
`encrypted_content`; local files stored as `file_id` references). `GET /v1/files` paginates like OpenAI (`after`, `limit`,
`order`, `purpose`, `has_more`), uploads read `purpose` from the form field,
and `GET /v1/models/{model}` returns one model.

Chat and completions responses report `usage` in OpenAI's Chat Completions
shape on both runtimes: `prompt_tokens` (cached tokens included, broken out in
`prompt_tokens_details`), `completion_tokens`, `total_tokens`; usage is
omitted when a provider reports none. With `stream_options.include_usage`,
chunks carry `usage: null` and a final chunk with `choices: []` holds the
usage. `/v1/responses` keeps the Responses API usage. See docs/api.md.

AIRelays starts every `claude` process through one launcher: it finds
`claude` on PATH and in the usual install locations (the path is used as
found), or runs `[providers.claude].command` (`AIRELAYS_CLAUDE_COMMAND` as a
JSON array for the desktop app) as a full argv. Windows requires a real
`claude.exe`; npm `.cmd` shims are refused. System prompts go through a
private file when the CLI supports it, and requests whose arguments exceed
the OS limit return 413. `airelays doctor` shows the CLI path in use.

OpenAI runtime:

```bash
airelays init
airelays login          # repeat with another account to enroll it too
airelays doctor
airelays serve --port 8080
```

Headless / server (SSH, no browser): `airelays login --device` — approve from a browser on any device. Auto-selected on SSH sessions and displayless Linux.

Claude runtime:

```bash
airelays init
airelays claude login       # repeat to add another account
airelays serve --port 8080
```

Claude headless: run `claude setup-token` on any browser-equipped machine, then `airelays claude set-token` on the relay machine (stores the token 0600 in `~/.airelays/claude-token`, overridable with `AIRELAYS_CLAUDE_OAUTH_TOKEN_FILE` or `[providers.claude].oauth_token_file`; it survives service managers and reboots). The token is the account `default`, runs in its own relay-owned `~/.airelays/claude/token/config`, and is inference-only: Claude reports no usage or email for setup tokens, so it has no meter and is not deduplicated. List accounts with `airelays claude accounts`, renew one with `airelays claude login --replace ACCOUNT`, and sign out one with `airelays claude logout ACCOUNT`. Renewal suspends a managed profile until its original identity is verified; renew the setup token with `airelays claude set-token`. Signing out runs `claude auth logout` only inside that AIRelays profile; `airelays claude logout default` only deletes the stored token. Multiple-account logout requires a target or `--all`.

AIRelays never uses the personal Claude Code sign-in (`~/.claude`), an inherited `CLAUDE_CONFIG_DIR`, or an inherited `CLAUDE_CODE_OAUTH_TOKEN` (`airelays doctor` reports the latter as ignored), and never signs Claude Code out. With no account connected, `claude:*` requests return 503 `provider_auth_error` ("No Claude account is connected to AIRelays. Run `airelays claude login` or `airelays claude set-token`.") and `/v1/models` lists no Claude models. Upgrades from 0.15.0 or earlier that relied on those must connect an account once; refresh tokens rotate, so the personal sign-in cannot be copied. Already-enrolled profiles keep working.

## Multiple Claude Accounts

`airelays claude login` delegates browser authentication to Claude Code in a
unique `CLAUDE_CONFIG_DIR`. Credentials and refresh remain CLI-owned. Metadata
lives under `data_dir/claude/accounts/<id>/account.json`, with a stable adjacent
`config/` directory. Do not move profile directories: macOS Keychain entries
are scoped to their paths. See docs/adr/0006-isolated-claude-subscription-accounts.md
and docs/adr/0007-relay-owned-claude-accounts.md. Every Claude Code process runs
with an explicit environment, its profile's `CLAUDE_CONFIG_DIR`, and a relay-owned
working directory, and is ended gracefully (stdin closed or SIGTERM before
SIGKILL). On macOS, run the relay in the login session (not over SSH or as a
LaunchDaemon) so Claude Code can use the keychain.

The desktop provides Sign in / Add account ("In a browser" or "With a token"),
per-account usage, renewal, and sign-out. Accounts join and leave routing
without restart. A second ready profile of the same subscription carries
`duplicate_of` and counts once; a profile signed in to a different identity is
not eligible for routing.
`[providers.claude].balance` defaults to `balanced`: prefer available execution
slots, then lowest fresh weekly usage. Without fresh usage, accounts take turns.
`round_robin` and `ordered` are alternatives. `max_concurrent_requests` applies
per account. Known exhausted short and model-specific windows are skipped;
account failures fail over only before response bytes reach the client.
`account_cooldown_seconds` defaults to 300 when no reset time is reported;
transient failures use a short cooldown. Each Claude request is one fresh CLI
run; conversation history, when a client uses `conversation`, is kept by
AIRelays and sent with each turn, so there is no account affinity across turns.

Claude usage accepts `?provider=claude&all_accounts=true`, returning
`{"object": "subscription_status_list", "provider": "claude", "accounts": [...]}`
with `{slug, email}` plus one of `status`, `error`, or `usage_unsupported` (the
setup-token account), and `?provider=claude&account=<email-or-id>` for one
account (`account=default` returns 422 `unsupported_for_provider`). Usage state
and persisted backoff are profile-specific.
`POST /v1/relay/accounts/refresh?provider=claude` respects that backoff.
`providers.claude.accounts` in relay status exposes readiness, request
cooldowns, `managed_profile` (`false` for the setup-token account), and
`oauth_token_source` (`profile` or `file`).
Environment overrides are `AIRELAYS_CLAUDE_BALANCE`,
`AIRELAYS_CLAUDE_ACCOUNT_COOLDOWN_SECONDS`, and `AIRELAYS_CLAUDE_OAUTH_TOKEN_FILE`.

## Client Configuration

- Base URL: `http://127.0.0.1:8080/v1` (desktop app default port: 8317)
- API key: the relay token (`airelays token show`); any placeholder in open mode
- List accepted model ids: `airelays models` or `GET /v1/models`

## Multiple OpenAI Accounts

`/v1/models` returns the union of enrolled accounts' catalogs. Balancing,
conversation affinity, and failover stay inside each model's supporting
subset, even if every supporting account is cooling down. Per-model
`airelays.account_availability` reports supporting and total account counts;
`catalog_visibility` and `description` preserve upstream metadata, including
entries hidden in the upstream picker. Unlisted configured overrides remain
eligible across the pool, unless a discovered subset establishes support.

One person can enroll several of their own OpenAI subscriptions; `airelays login` is additive. By default the relay routes each request to the account with the most remaining quota in its longest usage window — the weekly budget; windows are identified by duration because which windows a plan reports is upstream policy — among those that serve the requested model (`balance = "balanced"`), so consumption equalizes as a percentage of each plan's capacity; `balance = "round_robin"` sends strictly equal request counts and `balance = "ordered"` drains the first account before the next. An account at its usage limit is benched until its window resets and rejoins rotation automatically; at launch the relay probes each account's capacity and model catalog so balancing is correct from the first request. Manage with `airelays accounts` (list, order, refresh), sign out one with `airelays logout <email>`. Conversations stick to the account that served their first turn.

## Routes

- `GET /v1/models` (and `GET /v1/models/{model}`) — models from all enabled providers, with an `airelays` extension block per record. OpenAI queries its authenticated upstream catalog using the installed Codex version (`client_version = "auto"`, tested floor `0.153.4`; the former `0.124.0` default also uses automatic mode). Claude queries its CLI initialization catalog without submitting generation prompts and exposes both aliases and concrete model ids. Configured overrides extend discovery; `extra_models` defaults to empty. `airelays.discovery_source` distinguishes provider catalogs from configured overrides, and `airelays.resolved_model` shows the concrete Claude model behind an alias. `GET /v1/models?refresh=true` bypasses provider caches, including all OpenAI account catalogs; the desktop Refresh button uses it, and the Models tab reloads every five minutes while reachable. Failed Claude discovery retains configured ids and the last successful catalog, with its error in relay status. Catalog listing does not guarantee a successful generation under current account limits.
- `POST /v1/responses`, `POST /v1/chat/completions`, `POST /v1/completions` — text generation on both runtimes (see the capability matrix above)
- `GET /v1/subscription/status` (alias `GET /v1/account/rate_limits`) — normalized usage; `?provider=claude`, `?account=`, `?all_accounts=true`, `?raw=true`
- `POST /v1/relay/accounts/refresh` — clear usage-limit holds and re-check capacity
- `GET /v1/relay/status` — diagnostics, provider readiness, `requests_total`; `?activity_only=true` returns only the counter for frequent polling
- `GET`/`PUT /v1/relay/logging` — traffic-log retention policy and usage
- `POST/GET/DELETE /v1/files...`, `POST/GET/DELETE /v1/conversations...` — local files and conversations
- `/no-tools/v1/*` — tool-disabled variants
- `GET /healthz` — minimal public health check

## Compatibility Layer

The verified upstream is the ChatGPT subscription backend, not the public platform API:

- `temperature`, `top_p`, `presence_penalty`, `frequency_penalty` are rejected by the upstream; the relay strips them and discloses it via the `x-airelays-ignored-parameters` response header and a `compatibility_adaptation` traffic record. The Claude routes apply the same strip-and-disclose adaptation. Output-token limits (`max_completion_tokens`, `max_tokens`, `max_output_tokens`) are honoured on both runtimes: GPT models enforce them upstream (at least 16), and a Claude run stops at the limit; a cut answer reports `finish_reason: "length"`. The full per-account-type capability matrix is docs/capabilities.md.
- `reasoning_effort` (chat) and `reasoning.effort` (responses) pass through verbatim to OpenAI models; on Claude models `reasoning_effort` maps to the CLI's `--effort` flag. Supported modes and defaults are read from provider catalogs when available and published in `/v1/models` under `airelays.reasoning`; they vary by model and can include `max` or `ultra`. Omitting effort uses the provider default; Claude can choose adaptively. An empty Claude modes list advertises no effort parameter.
- `store=true`, `n>1`, and `best_of`/`echo`/`logprobs`/`suffix` are rejected loudly instead of silently adapted.
- Failed upstream OpenAI calls are retried automatically with exponential backoff (default 3 retries, 5s/20s/60s; `retry_attempts = 0` disables) while no response byte has reached the client; each retry re-runs account failover. Final failures return OpenAI-shaped `{"error": {...}}` JSON with the real HTTP status and the upstream's own error code. After a stream has started, failures surface as an in-band `data: {"error": ...}` event (chat/completions) or verbatim `response.failed` events (responses passthrough).
- Claude runtime: discovered `claude:*` aliases and concrete `claude-*` ids, plus configured overrides; chat completions, completions and responses, loopback-only; images, PDFs and text files as input; local files and conversations; function tool calls are returned to the application, which runs them (AIRelays never runs a tool: built-in tools are off, client tools are descriptions behind a helper that executes nothing, calls are denied before execution, and the run stops at the call). Structured outputs are supported on chat completions: `response_format` `json_schema` maps to the claude CLI's `--json-schema` (native enforcement) and `json_object` answers are checked to be one JSON object; supported types per model are published in `/v1/models` under `airelays.structured_output`.

## Subscription Usage

Claude reports explicit scoped limits, including Fable's weekly cap when
present, separately from all-model windows. The desktop groups accounts under
OpenAI and Anthropic headings. The **?** beside each account name opens details
on hover, click, or keyboard activation; Escape closes them. It names exhausted
scopes, exposes credit/spend details with their declared currency scale, and
does not infer missing balances. OpenAI preserves `model_usage`, credits,
spend controls, and the distinction between available and currently usable
limit-reset credits. Quota percentages are not token counts; the token
breakdown only covers responses observed through this relay.

Snapshots retain their fetch timestamp on cache reads. The Overview reloads
usage every five minutes and derives countdowns from absolute reset times.
Unknown or expired percentages display as awaiting fresh data, not zero.
Claude usage comes from Claude Code itself: each profile's CLI answers the SDK
`get_usage` control request (no prompt, inference, or quota; experimental in
Claude Code, verified with 2.1.292+) at startup and every five minutes, which
also keeps idle profiles signed in. The relay never reads Claude credentials or
calls the usage endpoint directly. A reading is fresh only when Claude returns
live data; otherwise the last good reading is shown stale with its age and a
`stale_reason`: `usage_unavailable` (no live data, e.g. upstream rate limit),
`nonessential_traffic_disabled` (`CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC` set
in the relay's environment), `cli_unsupported` (update Claude Code; requests
are unaffected), or `cli_failed`. Failures back off 5 → 10 → 20 → 40 → 60
minutes, persisted across restarts. Without any good reading, the API returns
an error message instead of numbers. Stale readings never unblock or rank an
account, observations older than fifteen minutes do not rank accounts, and
known exhausted windows remain blocked until reset or newer capacity evidence.

`GET /v1/subscription/status` returns per-window `used_percent`, `window_label` ("5h", "weekly", derived from each window's duration), and reset times in one shape for both providers, plus credits, spend control, and any named extra quotas (for example code review) the plan reports. Which windows appear is plan-dependent upstream policy; only reported windows are returned. OpenAI reads the subscription usage surface; query one account (`?account=`) or all (`?all_accounts=true`, always the list shape — an account whose probe fails carries a per-account `error` instead of a `status`). Claude (`?provider=claude`) returns the 5-hour and weekly windows plus per-model weekly caps when reported, as read by Claude Code; the setup-token account reports no usage. In the desktop app, an OpenAI account whose stored sign-in was invalidated upstream shows a "Sign-in expired" badge with a one-click "Sign in again" repair that refreshes the account slot in place.

## Security Defaults

- default listener `127.0.0.1:8080` (CLI) / `0.0.0.0:8317` (desktop, with a one-click loopback switch)
- protected routes `/v1/*` and `/no-tools/v1/*`; public: `/` and `GET /healthz`
- rate limit 120 requests/minute (burst 40), 8 concurrent requests per IP, temporary IP block after repeated bad tokens
- the Claude runtime is loopback-only and follows the relay's auth mode

## Configuration

Order: CLI flags → `AIRELAYS_*` environment variables → `~/.config/airelays/config.toml` → defaults. Notable keys: `[server] host/port`, `[security] require_bearer_auth`, `[logging] stream_lines`, `retention_days`, `max_total_mb`, and `max_file_mb`, `[providers.openai] enabled/balance/account_cooldown_seconds/retry_attempts/retry_backoff_seconds`, `[providers.claude] enabled/bin/models/balance/oauth_token_file`. Saved log-directory retention policy overrides the three logging defaults. See docs/configuration.md for the full file shape and limits.

## Diagnostics

- `airelays logs` — inspect or apply traffic-log retention, including `--retention-days 30` for a month and `--max-total-mb` / `--max-file-mb` limits in MiB. The tray's Settings → Traffic log retention and GET/PUT `/v1/relay/logging` use the same saved policy. Defaults are 7 days, 1024 MiB total, and 50 MiB per file; `0` days disables the age limit only. Cleanup permanently removes oldest managed files, including eligible existing traffic logs. It excludes console output, uploads, and conversations. See docs/configuration.md, docs/api.md, and docs/troubleshooting.md for limits, API response fields, and cleanup-error recovery.

- `airelays status` — local config, token, and provider readiness
- `airelays doctor` — setup checks plus live upstream `/models` and a tiny `/responses` smoke request (`--skip-response` to skip)
- `airelays models` — model ids the running relay accepts, grouped by provider
- Traffic logs: hourly JSONL under `~/.airelays/logs`, with per-request records (tokens, status, serving account, adaptations)

## Paths

- config: `~/.config/airelays/config.toml`
- data: `~/.airelays` (logs, relay token, per-account auth slots, Claude profiles under `claude/accounts/<id>/`, Claude setup token in `claude-token`)

## Troubleshooting Pointers

- 401 then 429: wrong/missing relay token; repeated failures trigger a temporary IP block
- answers cut with `finish_reason: "length"`: the request's output-token limit was reached (enforced on both account types; GPT minimum 16)
- Claude 502 `cli_tools_unavailable`: the CLI could not load the request's tools (check that each tool's `parameters` is an object schema; reinstall AIRelays if its tool helper is missing); `cli_unexpected_tools`: the CLI offered or attempted an undefined tool and the request was stopped; neither pauses an account
- 422 for an image or document: images must be PNG, JPEG, GIF or WebP; documents PDF or UTF-8 text; Claude CMYK or animated images must be converted
- 413 `request_too_large` on a Claude request: images and documents are limited to 32 MB per request
- browser login URL only works on the relay's own machine; use `airelays login --device` on servers
- Claude not ready in network mode: the runtime is loopback-only; switch to loopback binding
- Claude 503 `provider_auth_error`: no AIRelays Claude account is connected; run `airelays claude login` or `airelays claude set-token` (the personal Claude Code sign-in and exported tokens are ignored)
- Claude usage stale: check `stale_reason`; update Claude Code for `cli_unsupported`, unset `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC` for `nonessential_traffic_disabled`
- macOS "AIRelays is damaged" after a browser download: `xattr -dr com.apple.quarantine /Applications/AIRelays.app` (the one-line installer avoids this); Linux AppImage FUSE error: install libfuse2 or set `APPIMAGE_EXTRACT_AND_RUN=1`
- see docs/troubleshooting.md for full workflows
