underclass README

The project's README from github.com/ghuntley/underclass. As one markdown file: README.md.

underclass

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.


Why

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:

Quick start

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 → pick ChatGPT / Codex or GitHub 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.

How routing works

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

Configuration

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, UNDERCLASS_MONITOR_SOCKET (local socket path). 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.

CLI

underclass serve [--bind ADDR]
underclass top [--url HTTP_OR_HTTPS_ORIGIN]
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.

Run underclass top (or utop from the Nix package) in a second terminal while underclass serve is running for a continuously updating, read-only pool monitor. It shows live in-flight requests, a 60-minute attempt chart, current UTC month token accounting, account status and sticky sessions, Codex quota windows and banked resets, and recent outcomes. Account states and quota levels are color coded and also labeled in text. Press q to quit; use arrow keys, j/k, or Page Up/Down to scroll accounts. The dashboard reconnects if the server becomes unavailable. Locally it uses a Unix socket without a token; the NixOS service exposes /run/underclass/monitor.sock to local users. The socket serves only the monitor snapshot, which includes account labels and usage. If no socket is available, top can use the local admin token from UNDERCLASS_UI_TOKEN, config, or the existing database. For another server, pass --url and set UNDERCLASS_UI_TOKEN in the environment. The browser UI still requires its admin token. Copilot quota availability is shown as unknown because underclass has no Copilot quota snapshot. Counts are upstream attempts; token totals omit unknown usage.

Endpoints

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
GET /admin/api/usage admin token token totals grouped by group_by=model,account_id,cache_key (any subset)
GET /admin/api/usage/requests admin token paginated per-attempt accounting rows
GET /admin/api/monitor admin token compact read-only live monitor snapshot
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.

Accounting queries accept from_ms (inclusive), to_ms (exclusive), and exact model, account_id, or cache_key filters. Use missing_key=true to select requests without a key. Grouped summaries and request details accept limit (1–1000, default 100) and offset; summary responses also include unpaginated totals. With no time bounds, queries default to the current UTC month; provide from_ms=0 to query all history. Each summary includes measured and unknown request counts. Token totals sum reported values only; they are lower bounds when unknown requests exist. See ADR 0014.

Correlation with preflight

When preflight fronts underclass, it generates a UUIDv4 and forwards it as x-request-id. Underclass preserves that ID in its response, routing/retry logs, and request history. Search the same request_id in both services to trace inspection through pool routing.

Underclass accepts one hyphenated RFC4122 UUIDv4 and normalizes it to lowercase. Missing, duplicate, invalid, nil, and other-version IDs are replaced before authentication or logging. Every handled response receives the resulting ID, including 401s, 429s, model discovery, and unmatched routes. Provider responses cannot replace it. Correlation IDs convey no authorization or proof that preflight inspected a request. See ADR 0012.

Models

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.

Nix flake

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
utop

The installed package puts both underclass and utop on your PATH. utop runs underclass top and accepts the same --url option. You can also run it without installing with nix run github:ghuntley/underclass#utop. Enabling the NixOS service with its default package also adds both commands to the host PATH. utop connects to the service's local monitor socket without a token.

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.

NixOS module

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:

Security

Development

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/.

License

MIT

Project layout

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

underclass (ghuntley/underclass)

Searching Database…

这是什么?它把多个 ChatGPT/Codex 和 GitHub Copilot 订阅汇集到一个 OpenAI 兼容的接口后面——当某个账号额度用完时会进入冷却,等额度窗口重置后自动恢复使用。
这是云服务,还是需要我自己在电脑上运行的软件?需要自己运行——它是一个 Rust 代理,默认绑定到 127.0.0.1:8080,并把凭据、会话绑定关系和使用记录保存在本地 SQLite 数据库里。
我怎么把它跑起来?它以 Nix flake 的形式发布:可以直接运行无需安装,也可以安装到你的 profile 里,然后启动 'underclass serve'(默认监听 127.0.0.1:8080),再打开网页界面即可。项目还附带了一个 NixOS 服务模块。
我需要 API 密钥,还是它能用我现有的订阅?不需要 API 密钥——你在网页界面里通过设备码登录,把真实的 ChatGPT/Codex 或 GitHub Copilot 订阅接入进来。任何 OpenAI 兼容的客户端都可以指向这个资源池,'underclass connect' 还能自动配置 opencode。
当池子里所有订阅的额度都用完时会发生什么?它会快速失败而不是排队等待:你会收到一个 429 错误,Retry-After 被设置为最早的重置时间。另外,一旦响应流已经开始输出,上游的错误就会直接透传——写到一半的回答不会被悄悄重新发送。
它能在 Windows 上运行吗?没有提到 Windows 支持——这个项目偏 Unix:它以 Nix flake 形式打包并附带 NixOS 模块,监控功能使用 Unix socket,配置和状态数据存放在 ~/.config 和 ~/.local/share 下。
我能把它跑在家用服务器上,还是只能在笔记本上用?它以本地优先的方式设计——默认绑定到 localhost,NixOS 服务也不会打开防火墙端口——但项目提供了 NixOS 服务模块,而且 'underclass top' 可以通过 --url 监控远程的资源池。
它会记录我的提示词或令牌吗?不会——access/refresh 令牌、授权头和提示词内容都不会被记录。按照设计,账号标签(邮箱/用户名)会出现在日志和界面里,connect 写入的 auth.json 权限为 0600。
为什么每个会话都要固定使用同一个订阅?粘性路由可以让上游的提示词缓存保持热度:携带同一个提示词缓存键的请求在 24 小时内会落到同一个账号上,绑定关系在重启后依然保留,并且当某个账号冷却时会在同一个后端内重新绑定。
什么是储备的 Codex 重置(banked Codex resets)?当某个实时请求发现所有启用的 ChatGPT/Codex 账号都不可用时,underclass 会为那个按自然额度恢复要等最久的账号兑换一次储备重置。这个功能默认开启,设置 autocodexresets = false 可以关闭。
我怎么实时查看资源池的运行情况?在另一个终端里运行 'underclass top'(或 utop)——这是一个只读的 TUI,实时显示进行中的请求、60 分钟的尝试图表、令牌用量统计、额度窗口和粘性会话。网页界面的管理页面也显示同样的状态。
它会跟踪每个账号的令牌用量吗?会——每次上游请求都会连同其模型、账号、提示词缓存键以及上报的输入/输出令牌数一起记录到 SQLite 里。管理 API 可以按模型、账号或缓存键汇总统计,账号删除后历史记录仍然可以查询。
Creator
ghuntley
Size
0.2 MB
Stars
211
Updated
CommitOct 16
Age
27d
Commits · 90 Days