# AIRelays

> AIRelays is a local OpenAI-compatible HTTP server backed by your own subscription logins: an OpenAI runtime (with multi-account balancing) and an Claude runtime via the local `claude` CLI. 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, sign-in flows, verification
- docs/configuration.md: config file shape and environment overrides
- docs/security.md: relay auth, open local relay mode, Claude guardrails
- 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: desktop app layout, build, supervision behavior
- docs/adr/README.md: durable design decisions

---

## 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.
- An optional Claude runtime uses the local `claude` CLI and its existing subscription auth state.
- 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

CLI / server (PyPI):

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

Desktop app (macOS, Windows, Linux): 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 blinks 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`).

## First Run (CLI)

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
claude auth login --claudeai
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 in a 0600 file that survives service managers and reboots). Sign out completely with `airelays claude logout` (also signs the `claude` CLI out machine-wide).

## 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` — 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
- `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`
- `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, and output-token limits (`max_tokens`, `max_completion_tokens`, `max_output_tokens`) are not honored; the relay strips them and discloses it via the `x-airelays-ignored-parameters` response header and a `compatibility_adaptation` traffic record. Generation uses the upstream's own defaults and runs to the model's natural stop. The Claude routes apply the same strip-and-disclose adaptation (the local `claude` CLI has no equivalent controls).
- `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; text chat/completions only, stateless, loopback-only, no tools/files/images. Structured outputs are supported on chat completions: `response_format` `json_schema`/`json_object` map to the claude CLI's `--json-schema` (native enforcement); 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 names the exhausted
scope, 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's five-minute cache and persisted upstream cooldowns protect its
usage endpoint; stale fallbacks explicitly report their age and reason.

`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; its upstream source is not a publicly documented API, so the relay caches it briefly and degrades gracefully. 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`. 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, stored Claude token)

## Troubleshooting Pointers

- 401 then 429: wrong/missing relay token; repeated failures trigger a temporary IP block
- 422 on token-limit fields: the subscription backend does not accept them; remove the fields
- 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
- see docs/troubleshooting.md for full workflows
