
What Underclass Does
Underclass is a local proxy that combines multiple ChatGPT/Codex and GitHub Copilot subscriptions into a single OpenAI-compatible endpoint. Clients communicate with familiar /v1/* routes while Underclass selects an eligible subscription, tracks quota health, and forwards requests to the appropriate upstream service.
This is useful when each subscription has an individual quota. Instead of manually switching accounts whenever one reaches its limit, you can place the accounts in a shared pool. An exhausted account enters a cooling state and automatically returns to rotation after its quota window resets.
Underclass is intended to behave like one well-provisioned provider while preserving account health, session affinity, and prompt-cache effectiveness.
Key Features
- OpenAI-compatible API: Clients can use
/v1/responses,/v1/chat/completions, and/v1/models. - Multiple backends: The pool can contain ChatGPT/Codex accounts, GitHub Copilot accounts, or both.
- Sticky sessions: Requests with
prompt_cache_keyorpromptCacheKeyremain assigned to the same subscription when possible. - Automatic cooldowns: Quota responses move an account out of rotation until its retry window expires.
- Fast saturation responses: If every account eligible for a model is cooling, Underclass returns
429with the earliest availableRetry-Aftervalue instead of queuing indefinitely. - Least-in-flight selection: Eligible Codex and Copilot accounts compete in a flat pool, filtered by each backend's model catalog.
- Persistent state: Accounts, model catalogs, credentials, and sticky bindings survive restarts.
- Administrative web UI: Add and manage accounts, edit model catalogs, and inspect the latest 200 requests.
- Structured observability: Every response has an
x-request-id, and logs identify the account label that served each request.
Install and Start Underclass
Option 1: Run from the Rust project
From the repository, generate separate secrets for API clients and the administrative UI, then start the server:
UNDERCLASS_PROXY_KEY="$(openssl rand -hex 32)" \
UNDERCLASS_UI_TOKEN="$(openssl rand -hex 32)" \
cargo run -- serve
The default listener is http://127.0.0.1:8080, and the web interface is available at the root URL:
http://127.0.0.1:8080/
Save both generated values in a password manager or runtime secret file. The proxy key authorizes requests to /v1/*, while the UI token authorizes administrative actions. Underclass does not write these values to diagnostics.
Option 2: Run with Nix
The repository is also a Nix flake. Run the proxy without installing it:
nix run github:ghuntley/underclass -- serve
To install the package into your Nix profile, use:
nix profile install github:ghuntley/underclass
For development, enter the provided shell and run the tests:
nix develop --no-pure-eval
cargo test
The --no-pure-eval option is required for the devenv development shell. It is not required by nix run or nix profile install.
Add Accounts to the Pool
- Open
http://127.0.0.1:8080/. - Paste the value configured as
UNDERCLASS_UI_TOKEN. - Select Add account.
- Choose either ChatGPT / Codex or GitHub Copilot.
- Open the displayed device authorization URL and enter the device code.
- Wait for onboarding to complete. Underclass labels the account with its email address or username.
- Repeat the process for every subscription that should participate in the pool.
The UI can also enable, disable, remove, or relogin accounts. A disabled account remains configured but is not selected for requests.
Connect OpenCode
Underclass includes a connect command that adds an Underclass provider to OpenCode:
cargo run -- connect
By default, the command updates the global OpenCode configuration under ~/.config/opencode/. It merges the provider block into opencode.json or opencode.jsonc, writes credentials to auth.json, and creates backups. The operation is idempotent, so it can be run again safely.
Start OpenCode with a model exposed by the pool:
opencode --provider underclass --model underclass/gpt-5.5
To write project-local configuration instead, add --project:
cargo run -- connect --project
Preview the merged configuration without writing any files:
cargo run -- connect --dry-run
Remove the generated integration when it is no longer needed:
cargo run -- connect --remove
The command also accepts --base-url, --api-key, --model, and --no-default-model when you need to customize the generated provider configuration.
Use the OpenAI-Compatible API
Any compatible client can use the proxy by setting its base URL to the Underclass listener and supplying the proxy key as a bearer token.
List available models
curl http://127.0.0.1:8080/v1/models \
-H "Authorization: Bearer $UNDERCLASS_PROXY_KEY"
The models endpoint returns a union catalog with merged limits. The catalog is seeded with documented Codex model families and Copilot's live model list. Because the catalog is stored as data, it can be edited through the UI or administrative API without changing proxy code.
Send a chat completion
curl http://127.0.0.1:8080/v1/chat/completions \
-H "Authorization: Bearer $UNDERCLASS_PROXY_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-5.5",
"messages": [
{"role": "user", "content": "Explain sticky session routing."}
]
}'
The proxy also supports POST /v1/responses. Both request routes stream results through the selected account. Once streaming has started, an upstream failure is passed through rather than silently resending a partially completed request.
Understand Routing and Failover
Sticky session behavior
When a request includes prompt_cache_key or promptCacheKey, Underclass binds that value to one subscription. OpenCode can send its session ID when configured with setCacheKey: true. Bindings last for 24 hours and survive proxy restarts.
If the assigned account starts cooling, Underclass preferentially rebinds the session to another account on the same backend. This design helps retain upstream prompt-cache benefits while still allowing recovery from quota exhaustion.
Quota and authentication handling
A quota response, including relevant 429 or usage-limit responses, moves the account into a cooling state. Underclass uses the upstream Retry-After value when available or a configured backend-specific cooldown otherwise. Cooling accounts automatically become eligible again when the cooldown ends.
For an upstream 401, Underclass performs one token refresh and retries once. If authentication still fails, the account requires a new login.
Pool saturation
If no eligible account is available for the requested model, Underclass does not hold the request in a queue. It immediately returns 429 and reports the earliest known reset time through Retry-After.
Configure the Proxy
Optional persistent configuration can be placed in ~/.config/underclass/config.toml:
bind = "127.0.0.1:8080"
proxy_key = "replace-with-a-client-secret"
ui_token = "replace-with-an-admin-secret"
codex_cooldown_secs = 1800
copilot_cooldown_secs = 1800
The default cooldown for each backend is 1,800 seconds when the upstream service does not provide a retry interval.
Environment variables override the corresponding configuration values:
UNDERCLASS_BINDUNDERCLASS_PROXY_KEYUNDERCLASS_UI_TOKEN
For tests against mock services, use UNDERCLASS_CODEX_UPSTREAM and UNDERCLASS_COPILOT_UPSTREAM. Their defaults point to the real upstream endpoints.
Underclass stores credentials, sticky bindings, minted keys, and the model catalog in ~/.local/share/underclass/pool.db. Deleting that database resets the local state completely.
Advanced Operational Tips
Inspect logs and correlate requests
Every handled response includes x-request-id. Logs are structured and can use JSON or pretty output through --log-format json or --log-format pretty. Use RUST_LOG to control filtering.
When preflight is placed in front of Underclass, a valid UUIDv4 supplied as x-request-id is preserved in the response, routing logs, retry logs, and request history. Invalid, duplicate, nil, missing, or non-v4 identifiers are replaced. The identifier supports correlation only; it does not prove authorization or inspection.
Manage the model catalog carefully
Routing eligibility and the OpenCode model block follow the stored catalog. Use the web UI or the administrative catalog API to inspect or edit backend model mappings. Unknown model identifiers pass through to Codex, allowing newly introduced Codex models to work without a proxy release.
Keep the service private by default
Underclass listens on localhost by default. Keep that default unless remote access is intentional and protected. The proxy does not log access tokens, refresh tokens, authorization headers, or prompt bodies, but account labels are visible in logs and the UI.
- Do not commit
pool.dbor generated*.bakfiles. - Protect the UI token separately from the client proxy key.
- Keep OpenCode's generated
auth.jsonprivate; Underclass writes it with0600permissions. - Use a secure tunnel or similarly deliberate protection before exposing the service beyond localhost.
Run Underclass on NixOS
The flake exports a NixOS module. A service configuration can enable Underclass, bind it locally, load secrets from a runtime environment file, and set backend cooldowns:
{
inputs.underclass.url = "github:ghuntley/underclass";
outputs = { nixpkgs, underclass, ... }: {
nixosConfigurations.my-host = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
underclass.nixosModules.default
{
services.underclass = {
enable = true;
bindAddress = "127.0.0.1:8080";
environmentFile = "/run/secrets/underclass.env";
settings = {
codex_cooldown_secs = 1800;
copilot_cooldown_secs = 1800;
};
};
}
];
};
};
}
The runtime environment file can hold the secrets without placing them in the Nix store:
UNDERCLASS_PROXY_KEY=sk-underclass-replace-me
UNDERCLASS_UI_TOKEN=replace-me
The module uses a dynamic user, stores persistent data in /var/lib/underclass, binds to localhost by default, and leaves the firewall closed. Enable services.underclass.openFirewall only when you intentionally bind beyond localhost.
Develop and Test the Project
Build and run the test suite with Cargo:
cargo build
cargo test
The project combines unit tests, Hegel property tests, and end-to-end tests. The pool and health core are synchronous and use an injected clock, while asynchronous behavior is kept at the edges. New providers implement the provider::Backend trait and register with the application.
For architectural background, review the project's architecture decision records. The routing model is explained in ADR 0010, while cross-proxy request correlation is covered by ADR 0012.
Conclusion
Underclass turns several ChatGPT/Codex and GitHub Copilot subscriptions into one local OpenAI-compatible service. Its sticky routing, automatic cooldown recovery, model-aware account selection, and fail-fast saturation behavior reduce manual account switching while preserving predictable client behavior. Start the server, onboard accounts through the web UI, run underclass connect, and point OpenCode or another compatible client at the resulting endpoint.
