cd /news/ai-tools/underclass-an-openai-compatible-pool… Β· home β€Ί topics β€Ί ai-tools β€Ί article
[ARTICLE Β· art-135068] src=github.com β†— pub= topic=ai-tools verified=true sentiment=Β· neutral

Underclass: An OpenAI-compatible pooling proxy that pins sessions to one account

Developer Geoffrey Huntley released underclass, an OpenAI-compatible local proxy that pools multiple ChatGPT/Codex and GitHub Copilot subscriptions behind a single endpoint at /v1/responses, /v1/chat/completions, and /v1/models. The proxy pins sessions to one subscription for 24 hours to keep upstream prompt caches warm, moves quota-exhausted accounts (429 or usage-limit responses) into a cooling state until retry-after, and returns 429 with the earliest reset time when all accounts for a requested model are cooling. It is written in Rust and run via `cargo run -- serve`, with accounts added through a web UI using ChatGPT/Codex OAuth device flow or GitHub Copilot device flow.

read7 min views1 publishedSep 20, 2026
Underclass: An OpenAI-compatible pooling proxy that pins sessions to one account
Image: Michielbdejong (auto-discovered)

A local proxy that pools multiple ChatGPT/Codex and GitHub Copilot subscriptions behind one OpenAI-compatible endpoint.

                       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                       β”‚            underclass              β”‚
                       β”‚                                    β”‚
 opencode ────────────►│  /v1/responses                     β”‚
 (any OpenAI-compatibleβ”‚  /v1/chat/completions              │────► chatgpt.com
  client)              β”‚  /v1/models                        β”‚      (N Codex subs,
                       β”‚                                    β”‚       OAuth device flow)
 web UI ◄─────────────►│  sticky sessions Β· health pool     β”‚
 (accounts, catalog,   β”‚  fail-fast saturation Β· tracing    │────► api.githubcopilot.com
  live request feed)   β”‚                                    β”‚      (M Copilot subs,
                       β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜       GitHub device flow)

One subscription runs out of quota? It leaves rotation until its window resets β€” and comes back on its own. Sessions stay pinned to one subscription so upstream prompt caches stay warm. When everything is exhausted, the proxy fails fast with the earliest reset time instead of hanging.

Subscription-based model access has a per-account quota. One account is a ceiling; twenty accounts are a pool. underclass turns a pile of personal subscriptions into a single durable endpoint that behaves like one well-provisioned provider:

  • No client changes β€” the surface is plain/v1/* ; point opencode (or anything OpenAI-compatible) at it.
  • No quota whiplash β€” exhausted accounts cool down and recover automatically; clients never see account churn.
  • No cache waste β€” sessions are sticky, so the upstream prompt cache keeps working across turns.
UNDERCLASS_PROXY_KEY="$(openssl rand -hex 32)" \
UNDERCLASS_UI_TOKEN="$(openssl rand -hex 32)" \
cargo run -- serve
underclass listening on http://127.0.0.1:8080
web ui: http://127.0.0.1:8080/

Keep the generated values in a password manager or runtime secret file; underclass never writes them to diagnostics. Open the web UI and paste the value supplied as UNDERCLASS_UI_TOKEN.

  1. Open the web UI and paste the configured admin token.
  2. Click Add account β†’ pickChatGPT / Codex orGitHub Copilot β†’ enter the device code at the shown URL. The account is labeled automatically with the account's email or username.
  3. Repeat for every subscription you want in the pool.
  4. Point opencode at the pool:
cargo run -- connect
opencode --provider underclass --model underclass/gpt-5.5

underclass connect writes the provider block and credentials into your global opencode config (~/.config/opencode/opencode.json{,c} + auth.json), idempotently and with backups. Re-run it any time; --remove undoes it.

  • Sticky sessions. Requests carryingprompt_cache_key /promptCacheKey (opencode sends the session ID when configured withsetCacheKey: true ) always land on the same subscription. Bindings live for 24h, survive restarts, and rebind preferentially within the same backend when an account cools.
  • Health pool. A quota response (429 /usage-limit bodies) moves an account tocooling untilretry-after (or a per-backend default). 401s trigger one token refresh + retry, then the account needs re-login. Cooling accounts stay configured and return to rotation automatically.
  • Fail fast. If every account eligible for the requested model is cooling, the proxy answers429 +Retry-After = earliest reset. No queuing.
  • Flat pool. Codex and Copilot accounts compete by least-in-flight, filtered by per-backend model catalogs. Unknown model IDs pass through to Codex so new models work without proxy changes.
  • Pre-first-byte failover only. Once a stream starts, upstream errors pass through β€” no silent re-send of half-finished turns.

Design decisions and their trade-offs live in docs/adr/ β€” start with ADR 0010 for the routing model.

Optional ~/.config/underclass/config.toml:

key default meaning
bind 127.0.0.1:8080 listen address
proxy_key minted on first run bearer key clients must send to /v1/*
ui_token minted on first run admin token for the web UI + /admin/api/*
codex_cooldown_secs 1800 cooling window when upstream omits retry-after
copilot_cooldown_secs 1800 same, for Copilot

Environment overrides: UNDERCLASS_BIND, UNDERCLASS_PROXY_KEY, UNDERCLASS_UI_TOKEN. For testing against a mock upstream: UNDERCLASS_CODEX_UPSTREAM, UNDERCLASS_COPILOT_UPSTREAM (default to the real endpoints).

State (credentials, sticky bindings, model catalog, minted keys) lives in ~/.local/share/underclass/pool.db. Delete it to start fresh.

underclass serve [--bind ADDR]
underclass connect [--base-url URL] [--api-key KEY] [--model MODEL]
                   [--project] [--no-default-model] [--dry-run] [--remove]

connect targets the global opencode config by default; --project writes ./.opencode/opencode.json instead. --dry-run prints the merged documents without writing.

route auth purpose
POST /v1/responses proxy key Responses API, streamed through to the pool
POST /v1/chat/completions proxy key Chat Completions, same
GET /v1/models proxy key union catalog with merged limits
GET / none web UI
GET /admin/api/state admin token accounts, catalog, last 200 requests
POST /admin/api/flows admin token start a device-flow onboarding
GET /admin/api/flows/{id} admin token poll an onboarding flow
`POST /admin/api/accounts/{id}/enable disable relogin`
DELETE /admin/api/accounts/{id} admin token remove from pool
`GET PUT /admin/api/catalog/{backend}` admin token
GET /admin/api/client-key admin token retrieve the proxy key for connect

Every response carries x-request-id; logs are JSON ( RUST_LOG filters, --log-format json|pretty) and each request logs the account (label) that served it.

The catalog is data, not code: seeded with the Codex families (gpt-5.4, gpt-5.4-mini, gpt-5.3-codex-spark, gpt-5.5, gpt-5.6-sol, gpt-5.6-terra, gpt-5.6-luna, gpt-6-astra) and Copilot's live /models list. Edit it in the UI or via the admin API; routing eligibility and the opencode model block follow it. See ADR 0007.

The repository is a Nix flake: it exposes the CLI as a package/app and the devenv development shell as devShells.default.

Run the proxy without installing:

nix run github:ghuntley/underclass -- serve

Install it into your profile:

nix profile install github:ghuntley/underclass

Use the devenv shell (Rust toolchain, cargo) for development:

nix develop --no-pure-eval
cargo test

--no-pure-eval is required for the devenv shell (devenv inspects the working directory; this matches devenv's own flake template). The package and app outputs are pure β€” nix run and nix profile install need no flags.

Long-term devenv users can keep using devenv shell / devenv test directly β€” nix develop and devenv shell activate the same devenv.nix.

Add underclass to your flake inputs and import its module:

{
  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 provide credentials without placing them in the Nix store:

UNDERCLASS_PROXY_KEY=sk-underclass-...
UNDERCLASS_UI_TOKEN=...

The service uses a dynamic user, persists its database in /var/lib/underclass, binds to localhost by default, and leaves the firewall closed. Set services.underclass.openFirewall = true only when intentionally binding beyond localhost.

The flake also exports overlays.default. Validate the module and its QEMU machine test with:

nix build .#checks.x86_64-linux.underclass-module
nix build .#checks.x86_64-linux.underclass-vm

Notes:

  • The package builds from the committed Cargo.lock ; dependency versions are pinned there.

  • nix build skipscargo test because the property tests compile the Hegel engine as a build step, which needs network access that the Nix sandbox denies. CI runs the full suite via devenv (see.github/workflows/ci.yml ).

  • Access/refresh tokens, authorization headers, and prompt bodies are never logged.

  • Account labels (email/username) appear in logs and the UI by design; raw UUID account IDs are truncated.

  • auth.json written byconnect uses0600 . Never commitpool.db or*.bak .

  • The proxy binds to localhost by default; put it behind a tunnel only if you understand the exposure.

  • OAuth tokens rotate: every Codex refresh persists the new refresh token immediately.

cargo build
cargo test        # 43 unit tests + 8 Hegel property tests + e2e suite

Testing is two-tier: plain unit tests for exact behavior (headers, merges, redaction), and Hegel property tests over the pure pool core β€” stickiness stability, health-state invariants, saturation minimums, TTL/cap bounds. The core (src/pool.rs, src/health.rs) is synchronous with an injected clock; async lives only at the edges. New backends implement the provider::Backend trait and register β€” nothing else changes.

Agent conventions and the ADR policy are in AGENTS.md. Architecture decision records: docs/adr/.

src/
  main.rs      bootstrap, router, background tasks
  config.rs    TOML + env config
  models.rs    domain types (Account, status, catalog, log entries)
  store.rs     SQLite persistence (accounts, bindings, catalog, config)
  pool.rs      pure pool core: stickiness, eligibility, selection
  health.rs    quota classification, retry-after parsing
  provider.rs  Backend trait
  codex.rs     ChatGPT/Codex backend (device flow, refresh, headers)
  copilot.rs   GitHub Copilot backend (device flow, catalog, headers)
  tokens.rs    single-flight token refresh
  proxy.rs     /v1/* handlers, failover, streaming
  logging.rs   structured logs, correlation IDs, redaction
  ui.rs        admin API
  ui.html      embedded single-page web UI
  cli.rs       `connect` verb, JSONC-safe opencode config merge
tests/
  properties.rs  Hegel property tests over the pure core
  e2e.rs         full-pool story against a mock upstream
── more in #ai-tools 4 stories Β· sorted by recency
── more on @underclass 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/underclass-an-openai…] indexed:0 read:7min 2026-09-20 Β· β€”