Enterprise AI subscription quota allocation and governance, self-hosted.
Subpool is a self-hosted control plane for allocating, governing, and auditing AI subscription capacity across teams. Administrators combine authorized subscription and API accounts into pools, distribute employee-specific keys, monitor remaining quota, and expose one consistent API without storing conversation content.
Release pages publish the current version, immutable container image coordinates, source commit, and complete notes from GitHub Releases.
Features
- Pool authorized subscription and API capacity behind one managed endpoint.
- Allocate employee-specific keys across healthy provider accounts.
- Monitor account health, remaining subscription capacity, and reset availability.
- Keep key-to-account assignments visible and auditable.
- Prefer subscription capacity and fall back to paid API accounts with the same employee key.
- Rate-limit, expire, and revoke employee access independently.
- Expose OpenAI-compatible Responses and Chat Completions APIs.
- Track aggregate input and output token telemetry per API key and subscription quota in provider-native units.
- Encrypt upstream credentials and never persist prompts, responses, or source code.
Architecture
For the internal protocol boundaries and request lifecycle, see Provider adapters.
Subpool is a single Go service with an embedded React console and PostgreSQL persistence.
Quick start
Requires Docker Engine and Docker Compose.
cp .env.example .env
Generate independent secrets and place them in .env.
openssl rand -base64 32
openssl rand -base64 32
openssl rand -hex 32
docker compose up -d
Replace every replace-with-* value. Docker Compose pulls the latest published Subpool image from ECR Public; no local image build is required. Open http://localhost:8081 and sign in with the administrator credentials from .env. The employee API remains on http://localhost:8080.
Connect and use
Bailian Token Plan quota
Beijing Token Plan personal endpoints support automatic five-hour, weekly, or monthly quota display, depending on the windows reported by the subscription. Connect the endpoint as an OpenAI-compatible account, then select Connect quota in its account row. Choose either authorization method:
- Browser token (temporary): sign in through the official Bailian CLI browser
- RAM AccessKey (automatic renewal): supply keys from the same subscription,
Delegated console contexts can also pass console_switch_agent in the quota API.
Quota credentials are encrypted within the account's credential record and are never returned by the API. Quota query errors retain the last known values and do not disable inference. Disconnect quota removes only the quota credentials. Other regions, team Credits, Coding Plan, and pay-as-you-go balances are not covered by this integration.
Client setup
- Open Accounts and connect a Codex, GitHub Copilot, or OpenAI-compatible account.
- Create a pool and add the account.
- Create an employee API key for the pool.
After creating an employee API key, configure Codex on each user's machine to route requests through Subpool. Edit the user-level Codex configuration file:
- Linux:
/home/(or/.codex/config.toml ~/.codex/config.toml) - macOS:
/Users/(or/.codex/config.toml ~/.codex/config.toml) - Windows:
C:\Users\(or\.codex\config.toml %USERPROFILE%\.codex\config.toml)
.codex directory and config.toml file if necessary. Replace https://subpool.example.com with your Subpool URL and sk-example-not-a-real-key with the employee API key created in Subpool.
model = "gpt-5.6-sol"
model_provider = "subpool"
model_reasoning_effort = "xhigh"
[model_providers.subpool]
name = "Subpool"
base_url = "https://subpool.example.com/v1"
wire_api = "responses"
experimental_bearer_token = "sk-example-not-a-real-key"
requires_openai_auth = false
supports_websockets = true
[features]
responses_websockets_v2 = true
Save the file, then restart Codex so the new provider and WebSocket settings are loaded. Codex subscription accounts use a dedicated upstream WebSocket, while OpenAI-compatible accounts use the existing HTTP/SSE bridge.
API and networking
Available API endpoints include GET/POST /v1/responses, POST /v1/responses/compact, POST /v1/chat/completions, POST /v1/images/generations, POST /v1/images/edits, GET /v1/models, GET /healthz, GET /readyz, and GET /metrics.
subpool_codex_routing_requests_total observes Codex routing boundaries with fixed transport (http, websocket) and binding labels:
unbound: no previous response and no existing WebSocket account binding.continuation: the request supplies a previous response ID, including the first request after reconnecting.connection: no previous response ID, but the WebSocket already has an account binding.
unbound identifies an opportunity under the current routing contract, not a guarantee of cross-account replay safety, available alternative capacity, or a share of consumed quota. These metrics do not change routing.
Subpool always serves two listeners. SUBPOOL_API_LISTEN_ADDRESS exposes the employee API and operational endpoints, while SUBPOOL_CONSOLE_LISTEN_ADDRESS exposes the console and its /api/v1/* control API. For Compose deployments, SUBPOOL_PORT and SUBPOOL_CONSOLE_PORT select their host ports. The pre deployment uses SUBPOOL_HOST_PORT (API, default 8082) and SUBPOOL_CONSOLE_HOST_PORT (console, default 8083).
Deployment notes
- Terminate TLS at a reverse proxy and set
SUBPOOL_PUBLIC_URLto the console's public origin. - Back up PostgreSQL together with
SUBPOOL_CREDENTIAL_KEYandSUBPOOL_API_KEY_HMAC_KEY. - Use PostgreSQL for shared authentication, rate-limit, assignment, and health state across replicas.
- Responses WebSockets allow up to 32 connections per employee API key and 256 per process. Idle connections count toward these limits and close after five minutes without pending work. Active turns remain limited to 16 per connection and 16 per upstream account. A connection over either limit receives HTTP 503 with
subpool_capacity_exceeded; existing connections stay open.subpool_responses_ws_connection_rejections_total{reason="per_key_limit|global_limit|closing"}distinguishes capacity and shutdown rejections, alongside the existing total counter. These counters reset on restart. - HTTP and Responses WebSocket request bodies default to 256 MiB per request, a 1 GiB estimated buffer budget per process, and a five-minute read timeout. Administrators can change both size limits in Settings → Request limits (MiB). Saved limits override the environment defaults and apply to each new HTTP request or WebSocket message across instances sharing the database, without restarting. In-flight requests keep their original limits and continue to count against the shared process budget; reducing the budget may temporarily reject new requests until old reservations finish. Before limits are saved,
SUBPOOL_MAX_REQUEST_BODY_BYTESandSUBPOOL_MAX_INFLIGHT_REQUEST_BODY_BYTESsupply the defaults. The timeout remains controlled bySUBPOOL_REQUEST_BODY_READ_TIMEOUT. Choose values that fit the available memory; the buffer budget must be at least four times the per-request limit. These settings do not change upstream provider limits. - GitHub Copilot uses the bundled public OAuth client ID by default.
SUBPOOL_COPILOT_CLIENT_ID,SUBPOOL_COPILOT_API_BASE,SUBPOOL_COPILOT_TOKEN_EXCHANGE_URL, andSUBPOOL_COPILOT_ENTITLEMENTS_URLare optional upstream overrides; most deployments should leave them unchanged.
Development
make compose-dev-up
make web-install
make dev
compose.dev.yaml builds subpool:local from the current source tree instead of pulling the published image.
Before opening a pull request:
make web-test
make web-build
go test ./...
Terms
Subpool is licensed under the Apache License 2.0. It is independent and self-hosted. Use only accounts you are authorized to manage, and review each upstream provider's terms before sharing access.
Opt-in Codex quota routing
Set SUBPOOL_CODEX_QUOTA_ROUTING=true to prefer more remaining subscription
capacity for independent, non-image Codex requests and the first unbound
WebSocket turn. The default is false, including self-hosted deployments.
Existing previous_response_id chains and established WebSockets keep their
account; the policy does not migrate or replay them. API keys and client
configuration stay unchanged. New keys still use the existing allocation
policy; their first eligible request can update their default account.
The score is the smallest remaining percentage of the available five-hour and weekly windows. At least one window must be present. Quota must be checked within 15 minutes and error-free; every present window must have a valid usage percentage and a future reset timestamp. An omitted window is not assumed to have zero or unlimited capacity. A candidate must improve the score by more than five percentage points and have the same plan, provider, priority, credential type, and fast-mode setting. Health, cooldown and key-capacity checks still apply. Equal scores prefer lower weighted key count. Missing or stale data keeps the current binding; normal availability failover remains unchanged.
subpool_codex_quota_routing_switches_total counts committed default-binding
changes and resets on process restart, like the routing observation counters.
Compare quota snapshots and failures over time; request counts alone do not
measure quota consumption. Direct upstream usage can also change the balance.
Set the flag back to false and restart to stop further quota-based changes;
existing defaults and response bindings remain valid.