# 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

---

## 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.
- Every transit is logged to hourly JSONL files.
- 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

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
- `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`
- `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 per model are published in `/v1/models` under `airelays.reasoning`. Omitting the parameter means OpenAI's low default (`none`) or Claude's adaptive default; set it explicitly for output quality comparable to the official apps.
- `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: explicit `claude:*` models only, 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

`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` (opt-in per-line stream logging; summary records are always kept), `[providers.openai] enabled/balance/account_cooldown_seconds/retry_attempts/retry_backoff_seconds`, `[providers.claude] enabled/bin/models`. See docs/configuration.md for the full file shape and variable list.

## Diagnostics

- `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
