ai
One OpenAI / Anthropic / Gemini compatible endpoint for every tool and person on the instance, in front of
subscription accounts (through CLIProxyAPI) and official API keys, with teams, keys, quotas, routing and
usage analytics. Host: ai.<d> (API at ai.<d>/v1, /v1beta, Anthropic /v1/messages). Binary:
aid. Stage 8. Built on CLIProxyAPI (ADR 0012).
Concepts
- Provider account: a subscription account signed in through its CLI's OAuth (Claude Code, Codex,
Gemini CLI, Antigravity, Grok, …, whatever CLIProxyAPI supports) or an API key (Anthropic, OpenAI,
Google, OpenRouter, any OpenAI-compatible URL). Owned by the instance or a team; can be granted to other
teams with a quota (ADR 0005). State: healthy, rate-limited until, expired, failing.
- Pool: accounts of one upstream; requests rotate across them by remaining window (5-hour and weekly
limits where the provider has them, tracked like
cca does), with cool-down on rate limits.
- Model: the public name clients use (
claude-sonnet, gpt, gemini-pro, or exact upstream ids),
mapped to one or more upstreams with priorities and fallbacks.
- Key:
sk-xvii-… per user, team or project (and per app in apps, delivered through env), with
allowed models, rate limits and a budget.
- Quota: per team, key or user: requests, input / output / cached tokens and cost per window (hour,
day, month), hard or soft (soft notifies through
notify). Cost from a price table per model kept in
the repo and editable.
- Usage: every request logged with key, team, project, model, upstream account, tokens, cost,
latency, status; bodies not stored unless a team enables it for debugging (with retention).
Endpoints
- Compatible APIs: OpenAI (
/v1/chat/completions, /v1/responses, /v1/embeddings, /v1/models),
Anthropic (/v1/messages, count tokens), Gemini (/v1beta/models/*). Streaming everywhere.
- Quota endpoints:
GET /v1/quota (remaining for the calling key: requests, tokens, budget, reset
times) and per upstream account for admins, in a shape tools like opencode-quota can read.
- Admin API for accounts, pools, models, keys, quotas, usage.
Panel
Overview (requests, tokens, cost by model, team, key), accounts and pools with live limits, models and
routes, keys, quotas, usage explorer, a playground.
Updates
A scheduled CI job follows CLIProxyAPI releases, bumps the module, runs the compatibility tests (recorded
upstream responses), and opens a merge request; home offers the new ai release like any other.
CLI and MCP
xvii ai keys create, xvii ai usage, xvii ai accounts add claude (runs the provider's OAuth through
the browser), xvii ai quota. MCP tools for usage and quota questions.
Risk
Proxying consumer subscriptions can break the providers' terms; the panel says so when such an account
is added (ADR 0012).
As built (stage 8)
- aid embeds CLIProxyAPI v8 through its SDK (ADR 0012), on loopback (
PROXY_PORT, 8813) with its own
config, credentials and management key in DATA_DIR. Outside, ai.<d>/v1 and /v1beta go through
aid's gate; /api/v1 and the panel are aid's.
- Keys
sk-xvii-… (stored as sha256, shown once): a team's, or a person's own in a team; allowed models
(ids or * patterns); requests a minute and a day, tokens and dollars a month. The gate reads the key
where the SDK puts it (Bearer, x-api-key, x-goog-api-key, ?key=), refuses in the caller's API's
error shape, and passes only the key's id (X-Xvii-Key) to CLIProxyAPI, whose "xvii" access provider
makes it the request's principal. GET /v1/quota answers what the calling key has left.
- Usage: CLIProxyAPI's usage plugin reports every upstream call (key, model, provider, account, tokens from
its accounting breakdown, latency, stream, failure); aid prices it from a built-in table of list prices
(
internal/meter/prices.go, estimates for subscriptions too) with the instance's overrides, and writes
it in batches. The team limit ai.tokens (a month) and ai.keys apply.
- Accounts: superusers sign in subscription accounts (claude, codex, antigravity, kimi, xai, devin, meta)
through CLIProxyAPI's OAuth — the panel or
xvii ai accounts add opens the provider's page and takes the
address the browser ends on — and add official API keys (Anthropic, Gemini, any OpenAI-compatible base
URL with its models). The panel never shows keys, only their last four characters, and warns about
subscription terms.
- Provider quotas: the Accounts table shows each subscription's remaining windows and reset times,
separately from aid's usage metering.
GET /api/v1/accounts/quota/{name} (superusers) uses
CLIProxyAPI's quota provider when available; otherwise fixed read-only probes for Codex, Claude and
Antigravity run through its management API with the account's token/proxy. Neither tokens nor raw
provider responses leave aid. Checks are cached five minutes; ?refresh=1 allows a check at most once
a minute. Missing data is unknown, never 100% free; a failed refresh keeps the last snapshot with its
timestamp and an error. Providers without a quota API are explicitly unsupported.
- Pool (v1.1): aid reads every enabled account's quota every five minutes into
quota_snapshots (30
days): history (GET /api/v1/accounts/history/{name}), and GET /api/v1/accounts/pool with each
window's pace and when it runs out if before it resets, the last day's calls per account, and what the
last 30 days cost at list API prices. An account whose account-wide window (Claude 5h/7d, Codex primary/
secondary; not Antigravity's per-model ones) has under 2% left is disabled until that window resets
(parked_accounts), unless no other account of its provider is in rotation; a person's toggle unparks
it. Superusers hear through notify (ai.account.parked, .resumed, .exhausted, ai.quota.low under
10%, ai.account.failing when half of 10+ calls in 15 minutes failed).
- Aliases (v1.2): the instance's own model names (
aliases, superusers) for an ordered list of models;
the gate tries them in order and moves on while one answers 429, 404 or 5xx (the last answer passes
through, X-Xvii-Model names the model). /v1/models lists them. A key may call an alias if it may call
its name or any of its models, and then only those models.
- Analytics (v1.3):
GET /api/v1/usage/stats — latency and TTFT p50/p95, failures by status, cache hits,
output speed; by hour or day, model, provider, key and (superusers) account. A key's team hears when its
month passes 80% or all of its budget or tokens (ai.key.budget, ai.key.tokens).
- Playground (v1.4):
/api/v1/playground/{key}/v1/chat/completions|messages|models calls the gate with a
key the caller may change (a developer's team key or their own), so it counts and is limited on that key;
the panel never holds a key.
- Chat (v1.6): the playground is a chat built from @xvii items (
conversation-list, chat-message,
prompt-input, markdown). Conversations are a person's own (conversations: title, model, pinned,
data opaque to the server, up to 32 MB with pictures), readable by a team they're shared with
(team_id), never writable by others. Answers keep variants (regenerate, answer with another model);
image models go to /v1/images/generations, or /v1/images/edits with the last picture or an attached
one (the gate also reads the model of multipart forms). After the first answer a cheap model (haiku,
mini, flash…) names the conversation. prompts are a person's system prompts and /command templates.
- Service model (v1.7):
settings keeps the instance's service model (names chats; "" picks a cheap one
by name) and whether chats are named. A model's kind (chat or image, /v1/images) comes from its name as
CLIProxyAPI routes them (gpt-image-, dall-e, imagen, grok-imagine-image), or by hand
(model_kinds); /api/v1/models and aliases carry it.
- Compression (v1.8): Headroom (chopratejas/headroom, Apache-2.0) runs in one Docker container aid manages
through the Engine API (
xvii-ai-headroom, pinned image, host network on loopback HEADROOM_PORT 8814,
a volume for its state, 1 GB / 2 GB with ML; the manifest adds docker to ai's groups). Trust note
(audit 2026-10): that socket is root-equivalent on the host — aid is only as isolated as Docker is;
a break-out is aid's, not Headroom's (it has no socket). Where that's not acceptable, run a socket
proxy (tecnativa/docker-socket-proxy) allowing only HEAD/GET/POST on containers and images, and point
DOCKER_HOST at it. Its beacon,
update checks and (without ML) downloads are off. The gate sends the messages of /v1/messages and
/v1/chat/completions to /v1/compress — a session per key, model and first message keeps earlier turns
byte-identical for the prompt cache — and forwards the result; slow (timeout_ms) or failing, the call
goes as it came. Instance: off (container stopped), audit (count only, no session) or on; then a model rule
(first match), the key, the team (team_settings, its admins), and the call's X-Xvii-Compress header,
which can't turn on what the instance has off. compressions keeps each look (tokens before and after,
added latency, transforms); /api/v1/compression/stats prices savings at the model's input price.
- Logs (v1.5): a key's
log_days (0, or up to 30) keeps its calls' bodies (request_logs, 256 KB each,
streams as sent) for whoever sees the key (GET /api/v1/keys/{id}/logs[/{log}]); turning logs off or
deleting the key deletes them; expired ones go every five minutes.
- Updates: a weekly CI schedule runs
scripts/upstream.sh, which opens a merge request when CLIProxyAPI
releases (a project token in LAB_TOKEN).
- Not yet: team-owned accounts and grants, per-team routing to accounts, MCP tools.