Releases: lpalbou/AIRelays
Releases · lpalbou/AIRelays
Release list
AIRelays v0.14.1
Added
- One-line installers.
scripts/install-desktop.sh(macOS Apple Silicon, Linux x86_64) andscripts/install-desktop.ps1(Windows x64) install or update the desktop app from the newest GitHub Release, verify its published SHA-256 digest, and need no sudo, admin rights, Python, or Node.scripts/install-headless.shinstalls theairelaysrelay and CLI from PyPI into an isolated environment (uv or a Python 3.11+ virtualenv). All three acceptAIRELAYS_VERSIONto pin a release. See the README's Install section.
Fixed
- The desktop tray icon pulses within about a quarter second of each served request, and bursts stay visible until a second after the last one. Activity polling uses a lightweight
GET /v1/relay/status?activity_only=truethat requires the same authentication as full status but does not count as a request, consume rate-limit quota, or occupy a concurrency slot.
Changed
- Disclaimer and FAQ wording on intended use and provider terms were clarified.
AIRelays v0.14.0
Added
- Automatic traffic-log retention: 7 days and 1 GiB total by default, with hourly/50 MiB rotation. Cleanup runs at startup and while idle. Oversized records are explicitly marked as omitted, and storage failures are reported without failing relay requests.
- Live
GET/PUT /v1/relay/logging, offline-capableairelays logs, and tray Settings controls with week/month presets, custom limits, usage, and cleanup errors. The shared saved policy survives desktop config regeneration and requires no restart.
Upgrade notes
- Retention applies to existing traffic logs on the first upgraded start. Eligible files are permanently deleted; archive any history you need beforehand. Console output, uploads and stored conversations are outside this policy.
AIRelays v0.13.1
Fixed
- OpenAI models are the union of enrolled account catalogs. Requests, conversation affinity, and failover use only the subset listing the requested model, including during cooldowns. A failed primary catalog no longer hides other accounts' models.
- The Models tab reports account coverage and upstream-hidden catalog entries without guessing their underlying model identity.
- Claude usage includes modern scoped limits such as Fable's weekly allowance, independently of all-model limits, plus usage-credit status and correctly scaled monetary details.
- Usage snapshots retain their fetch timestamps; Overview refreshes every five minutes and updates reset countdowns from absolute times. Unknown and expired percentages are not displayed as zero usage.
- OpenAI usage preserves model availability and distinguishes available limit-reset credits from credits usable right now.
AIRelays v0.12.5
Fixed
- An OpenAI account whose stored sign-in was invalidated upstream (
refresh_token_invalidated) no longer shows a "Ready" badge next to a bare "Usage unavailable" note in the desktop accounts card. The usage probe's verdict now outranks the credentials-on-disk readiness flags: the badge reads "Sign-in expired", the note says what happened, and the badge sits next to a one-click "Sign in again" button that launches the standard OpenAI sign-in — the relay refreshes the matching account slot in place, so no sign-out is needed first. The raw upstream error stays in the tooltip.
Changed
GET /v1/subscription/status?all_accounts=truenow always answers the list shape, one entry per account, with a per-accounterrorwhen a probe fails. Previously a lone enrolled account folded to the bare single-account shape, so its probe failure became a whole-request 503 and single-account installs had no way to see why usage was missing (found by adversarial review). Callers that requestall_accounts=truewith one enrolled account now receive{"object": "subscription_status_list", "accounts": [...]}instead of the bare status object.
Added
- The desktop accounts card now surfaces everything the normalized usage payload reports: the code-review quota renders as its own labeled bar (it was previously dropped), bar details carry the absolute reset time next to the countdown, and the per-account "more" panel lists per-window reset timestamps, credits, spend control, limit-reset credits, and the snapshot time above the existing per-model token table. The panel flips downward when there is no room above the row.
AIRelays v0.12.4
Fixed
- Multi-account OpenAI model admission no longer serves a stale
/v1/modelssnapshot after the enrolled account set changes. The local model-catalog cache key now follows the current OpenAI account pool, so adding, removing, or reauthenticating a secondary account invalidates the cached catalog before AIRelays decides whether to forward a model id upstream. - Added a regression test for the account-pool topology change case, covering the failure mode where a model was valid for one account set, then should become locally rejected after a second authenticated account narrows the shared model intersection.
AIRelays v0.12.2
Fixed
- Cursor custom-endpoint compatibility on
/v1/chat/completions. AIRelays now accepts both malformed Cursor request families publicly reported in 2026: full Responses-style bodies sent to the chat route, and flat Responses-stylecustomtools / tool choices / assistant tool calls such asApplyPatch. These requests are normalized locally, sent upstream in the canonical Responses shape, and upstreamcustom_tool_callitems are translated back into chat-completionstool_callson both streaming and non-streaming responses. Tool-only streamed turns now emit the initialrole: "assistant"chunk that strict chat-stream consumers expect. - Chat tool-route hardening.
/no-tools/v1/chat/completionsnow rejects non-nonetool_choicevalues instead of forwarding them upstream, androle:"tool"messages must reference a preceding assistant tool call in the same request. AIRelays no longer guesses a function-tool output type for orphaned tool results; malformed transcripts fail loudly instead of silently shifting semantics. - Cursor-facing docs now state exactly what AIRelays normalizes on the chat route, how to verify it in traffic logs, and which malformed payloads are still rejected by design.
AIRelays v0.12.1
Fixed
- Deterministic upstream client errors no longer trigger account rotation, benching, or backoff retries. An in-stream
invalid_request_error(e.g. "Your input exceeds the context window of this model") was previously classified with everything non-quota as a retriable 502, so one unacceptable request could be retried across every account and several backoff rounds — multiplying paid upstream calls — before answering with a misleading account-limits error. Stream failures are now classified once, infailure_backend_error, into three classes: quota/rate-limit vocabulary →429(bench + failover, unchanged);invalid_request_errortype, request-shape codes, or aparam-bearing error →400surfaced immediately with the upstreamtype/code/message/parampassed through verbatim, after exactly one upstream call; anything unknown →502(short bench + failover + retry, deliberately kept for genuine upstream bad windows). All OpenAI lanes (non-streaming collect, streaming chat/completions,/v1/responsespassthrough) share that single classifier. - "All N OpenAI accounts are at their limits (earliest retry in Xs)" is now only claimed when every account is benched by limit-class evidence (an explicit quota rejection or the upstream usage report). Benches carry their evidence kind (
quota/auth/transient); rounds of transient 5xx failures answer "All N OpenAI accounts failed for this request" with the last upstream error, andresets_in_secondsis only advertised on all-quota rounds, where the horizon is authoritative. Account statuses expose the bench kind aslimited_kind. - The traffic log no longer scrubs upstream error codes. The blanket
coderedaction (meant for OAuth authorization codes) also hiterror.codeinside error objects, logging"code": "[REDACTED]"where the diagnostic vocabulary belongs. Redaction ofcodeis now scoped: kept readable inside error objects (under anerrorkey, or alongside amessagesibling), still scrubbed everywhere else (OAuth callbacks, token exchanges). Client-visible response bodies were never affected — redaction always ran at log-serialization time only.
AIRelays v0.12.0
Added
- Automatic retry with exponential backoff for failed upstream OpenAI calls. A failed call is retried 3 times by default, waiting 5s, 20s, then 60s, and each retry re-runs the full account-pool failover pass, so a transient upstream disruption resolves without the client seeing an error; a request that keeps failing returns the real error. Configure with
retry_attemptsandretry_backoff_seconds([providers.openai]; envAIRELAYS_OPENAI_RETRY_ATTEMPTS/AIRELAYS_OPENAI_RETRY_BACKOFF_SECONDS; desktop Settings → Providers).0disables retrying, and a schedule shorter than the attempt count repeats its last delay. Retries run only while no response byte has reached the client — non-streaming requests and the pre-header phase of streaming ones. Retries that cannot succeed are skipped: a quota error whose reset lies beyond the remaining backoff budget returns immediately, and a disconnected client stops the loop. Retries appear in the traffic log asretry_backoffrecords; deliberately skipped retries asretry_skipped.
Fixed
- Upstream failures surface as OpenAI-shaped errors instead of empty successes. A ChatGPT-backend stream that ends in
response.failed/errorevents, or closes without completing, previously produced an HTTP 200 withcontent: nulland no usage on the non-streaming routes; it now returns{"error": {...}}JSON with the real HTTP status and the upstream's own code and message — 429 for quota errors (includingresets_in_seconds, and the earliest account recovery when every enrolled account is at its limit), 502 otherwise. Clients reading FastAPI's default envelope keep working: the body carries adetailstring alongside theerrorobject. - Streaming failures are signaled, not truncated. On
/v1/chat/completionsand/v1/completions, a failure after streaming started emits an OpenAI-style in-banddata: {"error": ...}event and ends the stream without[DONE]; a stream that ends with no terminal event emits anincomplete_streamerror event;response.incompleteterminals finish withfinish_reason: "length"and[DONE]. On/v1/responses, upstream failure events pass through verbatim and transport failures surface as an in-banderrorevent. All three streaming lanes contact the upstream before committing SSE headers, so a stream that fails before its first event returns a real HTTP status and participates in automatic retry. - The account pool benches and fails over on in-stream failure events for streaming requests, matching the non-streaming path. Events that precede model output are buffered until the stream proves healthy, so a stream that fails before any content moves to the next account transparently; conversation stickiness only pins to accounts that actually delivered content, and requests skip benched or usage-maxed accounts outright. Failover and retry both stop once content has reached the client. The pool also releases the upstream HTTP stream deterministically when a consumer stops mid-stream.
- The traffic log records why upstream calls fail:
response.failed/errorSSE events are written asupstream_stream_errorrecords with the upstream error payload, andupstream_usagerecords (plus the account token tally) coverresponse.incompleteterminals, which bill real usage.
AIRelays v0.11.0
Changed
- Multi-account balancing now compares accounts on their longest usage window (the weekly budget) instead of the payload's primary slot. The upstream usage payload is plan-dependent — some plans report only a weekly window while others keep a 5h window plus a weekly one — so the primary slot no longer identifies a horizon: comparing it across accounts mixed a weekly percentage against a 5h percentage and could route sustained traffic into a small plan's scarce weekly budget while a large plan idled. Windows are now identified by duration (
limit_window_seconds), the balanced strategy equalizes weekly consumption as a percentage of each plan's own capacity, and short-window exhaustion remains covered by proactive benching and failover. Benching still considers every reported window. - The per-account "more" token breakdown (desktop Accounts card,
window_tokenson the account status payload) is now scoped to the account's longest usage window instead of the 5h bucket, so it accumulates over the weekly budget rather than clearing every few hours. The payload carries the window identity (window_label,window_seconds;scopeis nowcurrent_usage_window_via_this_relay), and the panel title names the window it covers (e.g. "This weekly window, via this relay"). On upgrade, breakdowns previously anchored to a 5h bucket start fresh once; existing fields (models,totals) are unchanged. - Account cards render only the usage windows the upstream actually reports. The synthesized idle "5h window · 0% used · starts with the next request" row is gone: plans without a 5h window no longer show a fabricated one.
Fixed
pytestfrom the repository root now collects only the real test suite (testpaths), instead of crawling into gitignored desktop runtime bundles whose vendored packages fail collection and shadowsrc/airelay.
Added
- Structured outputs on Claude chat completions.
response_format.type=json_schemaandjson_objectonclaude:*models now map to the claude CLI's native--json-schemaenforcement (json_objectenforces the permissive{"type": "object"}schema), instead of returning a 422. The responsecontentis the enforced JSON only — the model's surrounding prose never reaches a client that asked for JSON — and on streaming requests the JSON text streams as the content deltas (the CLI's StructuredOutput fragments), with a result-envelope fallback if no fragments arrive. Enforcement runs as an internal tool turn upstream, so schema-enforced calls bill some additional output tokens. Unsupportedresponse_formatshapes are still rejected loudly, including on/v1/completions, where the parameter does not exist in the OpenAI API and silently ignoring it would hand unenforced text to a client that asked for JSON. /v1/modelsnow publishes each model's structured-output support underairelays.structured_output(parameter, supportedtypes):json_schema+json_objectfor Claude models,json_schemafor OpenAI models (translated for the subscription backend, as before). The desktop Models tab andairelays modelsadvertise it alongside the reasoning modes.
Fixed
- Streaming requests no longer swallow errors (found by adversarial review). Request validation used to run inside the response generator, after the SSE headers were committed, so an invalid
reasoning_effortorresponse_formaton astream: truerequest produced an empty200stream instead of an error. Claude streaming requests now validate before headers and return real 4xx status codes; both OpenAI streaming routes now contact the upstream and await its first event before committing headers, so upstream rejections surface as real status codes too. Failures that happen after streaming has started (provider timeouts, CLI failures) now emit an OpenAI-style in-banddata: {"error": ...}event instead of silently truncating the stream — including the CLI's exit-0 error envelopes, which the streaming paths previously narrated as assistant content. reasoning_effortis now honored on/v1/completionsfor OpenAI models (it was silently dropped there while chat completions and the Claude runtime honored it), and an explicit JSONnulleffort is treated as absent on both routes instead of being forwarded asreasoning: {"effort": null}.- Structured-output honesty hardening (found by adversarial review): a schema-enforced run that produces no schema-conforming output now fails loudly instead of serving prose or an upstream error message under the JSON contract; a
response_formatwithout atypeis rejected like OpenAI does instead of silently degrading to unenforced text; oversized schemas (200 KB serialized cap; they travel on the local CLI's argv) and NaN/Infinity values get clean 422s instead of raw OS errors; and if a future CLI ever streamed more than one structured-output block, only the first is forwarded rather than concatenating into unparseable JSON. - The Claude runtime documentation no longer claims structured outputs are unsupported; docs/api.md, README, troubleshooting, and the LLM-readable indexes were aligned with the verified behavior above.
AIRelays v0.10.0
Added
- Structured outputs on Claude chat completions.
response_format.type=json_schemaandjson_objectonclaude:*models now map to the claude CLI's native--json-schemaenforcement (json_objectenforces the permissive{"type": "object"}schema), instead of returning a 422. The responsecontentis the enforced JSON only — the model's surrounding prose never reaches a client that asked for JSON — and on streaming requests the JSON text streams as the content deltas (the CLI's StructuredOutput fragments), with a result-envelope fallback if no fragments arrive. Enforcement runs as an internal tool turn upstream, so schema-enforced calls bill some additional output tokens. Unsupportedresponse_formatshapes are still rejected loudly, including on/v1/completions, where the parameter does not exist in the OpenAI API and silently ignoring it would hand unenforced text to a client that asked for JSON. /v1/modelsnow publishes each model's structured-output support underairelays.structured_output(parameter, supportedtypes):json_schema+json_objectfor Claude models,json_schemafor OpenAI models (translated for the subscription backend, as before). The desktop Models tab andairelays modelsadvertise it alongside the reasoning modes.
Fixed
- Streaming requests no longer swallow errors (found by adversarial review). Request validation used to run inside the response generator, after the SSE headers were committed, so an invalid
reasoning_effortorresponse_formaton astream: truerequest produced an empty200stream instead of an error. Claude streaming requests now validate before headers and return real 4xx status codes; both OpenAI streaming routes now contact the upstream and await its first event before committing headers, so upstream rejections surface as real status codes too. Failures that happen after streaming has started (provider timeouts, CLI failures) now emit an OpenAI-style in-banddata: {"error": ...}event instead of silently truncating the stream — including the CLI's exit-0 error envelopes, which the streaming paths previously narrated as assistant content. reasoning_effortis now honored on/v1/completionsfor OpenAI models (it was silently dropped there while chat completions and the Claude runtime honored it), and an explicit JSONnulleffort is treated as absent on both routes instead of being forwarded asreasoning: {"effort": null}.- Structured-output honesty hardening (found by adversarial review): a schema-enforced run that produces no schema-conforming output now fails loudly instead of serving prose or an upstream error message under the JSON contract; a
response_formatwithout atypeis rejected like OpenAI does instead of silently degrading to unenforced text; oversized schemas (200 KB serialized cap; they travel on the local CLI's argv) and NaN/Infinity values get clean 422s instead of raw OS errors; and if a future CLI ever streamed more than one structured-output block, only the first is forwarded rather than concatenating into unparseable JSON. - The Claude runtime documentation no longer claims structured outputs are unsupported; docs/api.md, README, troubleshooting, and the LLM-readable indexes were aligned with the verified behavior above.