API Notes¶
Traffic log retention API¶
GET /v1/relay/logging returns current policy and disk usage without deleting
logs. PUT /v1/relay/logging validates, persists and applies a partial policy
update immediately; omitted fields retain their saved values. These routes use
the same bearer-token gate and rate limits as the other /v1/* routes.
Example PUT body (all values must be integers; sizes are MiB):
{"retention_days": 30, "max_total_mb": 1024, "max_file_mb": 50}
Both responses contain policy, policy_source (saved or config, including
built-in defaults), policy_path, logs_dir, usage_bytes, file_count,
over_budget, cleanup_interval_seconds, last_cleanup_at, deleted_files,
deleted_bytes, oversized_records, dropped_records, and last_error. Cleanup counters and
errors are local to the process answering the request; disk usage and saved
policy are shared. last_cleanup_at is the most recent completed cleanup pass,
which may have encountered deletion errors. It is null before a pass completes.
Invalid values, unknown fields, zero size limits, or a file limit larger than
the total budget return 422 without altering the saved policy. Storage failures
preventing policy reads or persistence return 503. A successfully saved policy
returns 200 even if cleanup fails; inspect last_error and over_budget before
reporting successful reclamation. A policy update permanently deletes eligible
old files. GET requests do not trigger reclamation.
Defaults: 7 days, 1024 MiB total, 50 MiB per file. retention_days: 0 removes the
age limit but leaves size enforcement enabled. See
configuration for bounds, precedence,
rotation behavior, migration, and CLI/tray controls.
Compatibility Adaptations (read this first)¶
The verified upstream is the ChatGPT subscription backend, not the public
platform API. AIRelays adapts requests on the three text-generation routes
(/v1/responses, /v1/chat/completions, /v1/completions) rather than
letting them fail. Parameter stripping is always disclosed; other
compatibility normalizations are documented here and may log dedicated
adaptation records when the request shape itself is repaired:
- Removed unsupported parameters:
temperature,top_p,presence_penalty, andfrequency_penaltyare stripped before the upstream call because the upstream rejects them ("Unsupported parameter: temperature"). Output-token limit fields (max_tokens,max_completion_tokens,max_output_tokens) and caller-supplied end-user identifiers (user,safety_identifier) are also stripped because the verified subscription backend does not honor them. The names of removed parameters are returned in thex-airelays-ignored-parametersresponse header and logged as acompatibility_adaptationtraffic record with the reason. Generation runs with the upstream's own defaults. The same adaptation applies on the Claude routes: the localclaudeCLI exposes no sampling or token limit controls, so these parameters are stripped and disclosed there too instead of failing the request. - Reasoning effort:
reasoning_effort(chat completions and completions) andreasoning.effort(responses) are forwarded verbatim to OpenAI models; onclaude:*modelsreasoning_effortmaps to the local CLI's--effortflag on both text routes. An explicit JSONnullis treated as absent. Each model's supported modes and default are published in/v1/modelsunderairelays.reasoning. Unsupported Claude values are rejected with the supported list (the CLI would silently ignore them); unsupported OpenAI values surface the upstream's own error. Modes and defaults come from the provider catalog when available and vary by model. Omitting effort leaves the choice to the provider; Claude can use an adaptive default. An empty Claude modes list means no effort parameter is advertised for that model. - Structured outputs:
response_format.type=json_schemais forwarded (translated) to OpenAI models; onclaude:*chat completions bothjson_schemaandjson_objectare honored via the claude CLI's native--json-schemaenforcement. Supported types per model are published in/v1/modelsunderairelays.structured_output. - Cursor chat-route compatibility: AIRelays accepts two malformed
request families currently observed from Cursor custom OpenAI endpoints
on
/v1/chat/completions: full Responses-style bodies (input,instructions, flattools, etc.) and flat Responses-stylecustomtool definitions / tool choices / assistant tool calls (for example theApplyPatchtool). AIRelays normalizes those requests locally, sends the canonical Responses shape upstream, and translates upstreamcustom_tool_callitems back into chat-completionstool_callson both streaming and non-streaming responses. Mixed-shape requests that contain bothmessagesandinput, or tool outputs that do not reference a preceding assistant tool call in the same request, are rejected loudly. - Rejected loudly instead of adapted:
store=true,n>1, andbest_of/echo/logprobs/suffixon/v1/completions. These change semantics in ways silent stripping would hide, so they return a clear error. - OpenAI model admission: when AIRelays can fetch the live ChatGPT
Codex model catalog for the authenticated OpenAI account(s), OpenAI
requests are forwarded only for model ids that appear in that catalog
or in
[providers.openai].extra_models. Other ids are rejected locally with a clear 422 and suggestions from/v1/models, instead of failing later upstream with an account-scoped unsupported-model error. In multi-account mode, the same cache is invalidated when the enrolled OpenAI account set changes, so admission follows the union of their catalogs. Routing and failover use only the accounts listing the model, even when all supporting accounts are cooling down. Explicit unlistedextra_modelsoverrides remain eligible across the pool; if an override appears in an account catalog, its discovered subset takes precedence. - Account affinity: with multiple OpenAI accounts, a conversation is
pinned to the account that served its first turn (protects upstream
prompt caching); failover to another account happens only at turn
boundaries, logged as an
account_failovertraffic record. - Upstream failures and automatic retry: failed OpenAI calls are
retried automatically with exponential backoff (
retry_attempts, default 3 retries waiting 5s/20s/60s; each retry re-runs account failover) while no response byte has reached the client — non-streaming requests and the pre-header phase of streaming ones. On final failure the client gets OpenAI-shaped error JSON ({"error": {...}}) with the real HTTP status: the upstream's own code (e.g.server_is_overloaded) and, for quota errors,resets_in_seconds. After a stream has started, failures surface as an in-banddata: {"error": ...}event (chat/completions) or a verbatimresponse.failed/errorevent (responses passthrough); mid-stream failures are not retried. Retries are logged asretry_backofftraffic records (retry_skippedexplains deliberate non-retries), and upstream failure events asupstream_stream_errorrecords.
GET /v1/models¶
Returns an OpenAI-style models list built from the enabled provider runtimes.
- OpenAI models come from the authenticated ChatGPT/Codex upstream catalog. The default client version setting,
auto, follows the installedcodex --version, with a tested version floor when the CLI is missing or older. Model names are not hard-coded into automatic discovery. - Claude models come from the installed CLI's initialization catalog, including its alias resolutions and per-model reasoning modes. Discovery sends no generation prompt. Both
claude:*aliases and discovered concreteclaude-*ids are accepted. - models starting with
claude:orclaude-route to the Claude runtime when it is enabled - other model ids route to the OpenAI runtime when it is enabled
- Each model record includes an
airelaysextension block with provider identity, route capabilities, areasoningblock (parameter, supportedmodes,default), and astructured_outputblock (parameter, supportedtypesforresponse_formaton chat completions). - Provider catalogs are cached in memory for
models_cache_ttl_secondsseconds. The default is 300 seconds;0disables the cache. - Cached OpenAI model lists are scoped to the current local OpenAI auth account and ignored after logout or account changes.
GET /v1/models?refresh=truebypasses the provider catalog caches, including all OpenAI account catalogs. The desktop Refresh button uses this parameter, and the open Models tab also reloads every five minutes while the relay is reachable.airelays.discovery_sourceidentifiesupstream_catalog,claude_cli, or aconfiguredoverride.airelays.upstream_modelis the selector forwarded upstream.airelays.resolved_modelreports the concrete Claude model when the CLI supplies it;airelays.display_namecarries its provider label. Aliases keep following the CLI's selection; use a concrete id to pin a model version.- Configured OpenAI
extra_modelsand Claudemodelsextend discovery. They are retained for compatibility and may not have catalog confirmation. A missing or incompatible Claude CLI retains configured ids and the last successful catalog;providers.claude.models_discovery_errorin relay status reports discovery failures. Failed Claude probes are cached for the same interval to avoid repeatedly launching a failing CLI. - With several OpenAI accounts, discovery returns the union of successful catalogs and preserves each model's metadata.
airelays.account_availabilityreportssupportedandtotalaccount counts without disclosing account identities. A failed account catalog does not hide models returned by other accounts. Routing retains last-known membership during catalog outages rather than broadening a known subset. airelays.catalog_visibilityandairelays.descriptionpreserve upstream metadata. Entries markedhideappear as upstream-hidden in the desktop; AIRelays does not infer their underlying identity or promote them as recommended models.- Catalog discovery reports provider availability and capabilities; it does not generate a test response for every model. Account limits and upstream availability still apply when serving a request.
GET /v1/subscription/status¶
Returns a normalized subscription-usage snapshot with per-window usage percentages, window labels ("5h", "weekly", derived from each window's duration), and reset times. Which windows appear is plan-dependent upstream policy; only reported windows are returned.
- default provider is OpenAI (source:
chatgpt.com/backend-api/wham/usage) ?provider=claudereturns Claude subscription usage in the same normalized shape (see Subscription Status)?account=<email-or-prefix>selects one enrolled OpenAI account?all_accounts=truereturns the list shape with one entry per enrolled OpenAI account (an entry carries anerrorinstead of astatuswhen that account's usage probe fails)?raw=trueincludes the raw upstream payload (OpenAI only)
GET /v1/account/rate_limits is an alias.
POST /v1/relay/accounts/refresh¶
Re-checks every enrolled OpenAI account's capacity immediately and returns
the refreshed account list. Releases are evidence-gated: an account's
usage-limit hold is lifted only when its fresh usage report shows capacity,
so live traffic can never slip onto a still-exhausted account during the
re-check. Use it when you know an account has recovered and don't want to
wait for the scheduled reset. CLI equivalent: airelays accounts refresh.
GET /v1/relay/status¶
Returns relay diagnostics, provider readiness, provider cache status, and
requests_total — the count of real (non-monitoring) requests served by
this process, usable as a lightweight activity signal. OpenAI model-list
cache diagnostics live under providers.openai.models_cache.
GET /v1/relay/status?activity_only=true returns only
{"requests_total": N}. It requires the same authentication as the full
status, but it does not scan storage or probe providers, is not itself
counted, and does not consume rate-limit quota or a concurrency slot, so
clients such as the desktop tray can poll it frequently.
CLI Diagnostics¶
airelays status reports local config, relay-token, and provider readiness
state. airelays doctor runs the same local checks and also probes the OpenAI
upstream /models route plus a tiny /responses smoke request when the OpenAI
runtime is enabled and logged in. Use airelays doctor --skip-response to skip
the response smoke request. airelays models lists every model id the running
relay accepts, grouped by provider (--json supported).
POST /v1/responses¶
OpenAI runtime:
- general OpenAI Responses envelope
stream=true|false- local conversations
- local files and verified
input_fileforms
Current OpenAI limits:
store=truerejected- output-token limit fields rejected explicitly
Claude runtime:
- rejected explicitly on this route
POST /v1/chat/completions¶
OpenAI runtime:
- current AIRelays OpenAI compatibility path
- standard chat-completions
messagesrequests supported - Responses-shaped request bodies also accepted on this route for Cursor compatibility and translated back to chat-completions responses
- tool support includes both OpenAI
functiontools andcustomtools; AIRelays also accepts Cursor's flatter Responses-stylecustomtool shape on this route
Claude runtime:
- discovered Claude aliases and concrete model ids, plus configured overrides
- text-only
system,developer,user, andassistantmessages stream=true|false- no tools
- no files, images, or audio
- no AIRelays local conversation reuse
reasoning_effortsupported (low,medium,high,xhigh,max), mapped to the CLI's--effortflag; omitted means the model's adaptive default- structured outputs supported:
response_format.type=json_schemaandjson_objectmap to the CLI's--json-schemaflag (native schema enforcement;json_objectenforces the permissive{"type": "object"}schema). The responsecontentis the enforced JSON only — a run that produces no schema-conforming output fails loudly instead of returning prose. On streaming requests the JSON text streams as the content deltas. Enforcement runs as an internal tool turn upstream, so schema-enforced calls bill some additional output tokens. Unsupportedresponse_formatshapes (including a missingtype) are rejected with a 422; serialized schemas are capped at 200 KB (they travel on the local CLI's argv). Two documented leniencies versus OpenAI:json_schema.nameis optional (metadata the CLI does not use), andstrictis ignored because the CLI's native enforcement is always strict — clients can only get stricter behavior than asked, never weaker. - sampling parameters stripped and disclosed via
x-airelays-ignored-parameters(theclaudeCLI has no sampling controls); other unsupported generation controls rejected locally
POST /v1/completions¶
OpenAI runtime:
- current AIRelays OpenAI compatibility path
Claude runtime:
- discovered Claude aliases and concrete model ids, plus configured overrides
- text-only prompt-in, text-out
stream=true|false- no files, images, audio, or tools
reasoning_effortsupported (same modes and mapping as chat completions)response_formatrejected loudly (not part of the completions API; ignoring it would silently hand unenforced text to a client that asked for JSON — use/v1/chat/completionsfor structured outputs)- sampling parameters stripped and disclosed via
x-airelays-ignored-parameters; other unsupported generation controls rejected locally
POST /v1/files¶
Local AIRelays file storage for the OpenAI runtime compatibility path.
POST /v1/conversations¶
Local AIRelays conversation storage for the OpenAI runtime compatibility path.
The Claude runtime is stateless and does not use local conversations.
Unsupported Routes¶
These currently return 501 unsupported_error:
- embeddings
- image generation
- audio
- realtime sessions
Claude models are also rejected on any route that is not part of their published subset.