
Underclass 的作用
Underclass 是一个本地代理,可将多个 ChatGPT/Codex 和 GitHub Copilot 订阅整合到单一的 OpenAI 兼容端点。客户端通过熟悉的 /v1/* 路由进行通信,Underclass 会选择符合条件的订阅、跟踪配额状态,并将请求转发到相应的上游服务。
当每个订阅都有独立配额时,这一功能非常实用。无需在某个账户达到限制后手动切换账户,只需将账户放入共享池即可。配额耗尽的账户会进入冷却状态,并在其配额窗口重置后自动恢复轮换。
Underclass 旨在表现得像一个配置充足的服务提供商,同时保留账户状态、会话亲和性和提示缓存的有效性。
主要功能
- OpenAI 兼容 API:客户端可以使用
/v1/responses、/v1/chat/completions和/v1/models。 - 多个后端:池中可以包含 ChatGPT/Codex 账户、GitHub Copilot 账户,或两者兼有。
- 固定会话:带有
prompt_cache_key或promptCacheKey的请求会尽可能持续分配给同一订阅。 - 自动冷却:配额响应会将账户移出轮换,直到其重试窗口结束。
- 快速返回饱和响应:如果某个模型的所有符合条件的账户都处于冷却状态,Underclass 会返回带有最早可用
Retry-After值的429,而不是无限排队。 - 最少进行中请求选择:符合条件的 Codex 和 Copilot 账户会在扁平池中竞争,并根据各后端的模型目录进行筛选。
- 持久化状态:账户、模型目录、凭据和固定绑定会在重启后保留。
- 管理 Web 界面:添加和管理账户、编辑模型目录,并查看最近 200 个请求。
- 结构化可观测性:每个响应都包含
x-request-id,日志会标识处理每个请求的账户标签。
安装并启动 Underclass
选项 1:从 Rust 项目运行
在仓库中,为 API 客户端和管理界面分别生成密钥,然后启动服务器:
UNDERCLASS_PROXY_KEY="$(openssl rand -hex 32)" \
UNDERCLASS_UI_TOKEN="$(openssl rand -hex 32)" \
cargo run -- serve
默认监听地址为 http://127.0.0.1:8080,Web 界面位于根 URL:
http://127.0.0.1:8080/
请将生成的两个值保存到密码管理器或运行时密钥文件中。代理密钥用于授权对 /v1/* 的请求,UI 令牌用于授权管理操作。Underclass 不会将这些值写入诊断信息。
选项 2:使用 Nix 运行
该仓库也是一个 Nix flake。无需安装即可运行代理:
nix run github:ghuntley/underclass -- serve
要将软件包安装到 Nix 配置文件中,请使用:
nix profile install github:ghuntley/underclass
进行开发时,进入提供的 shell 并运行测试:
nix develop --no-pure-eval
cargo test
devenv 开发 shell 要求使用 --no-pure-eval 选项。nix run 或 nix profile install 不需要该选项。
将账户添加到池中
- 打开
http://127.0.0.1:8080/。 - 粘贴配置为
UNDERCLASS_UI_TOKEN的值。 - 选择 添加账户。
- 选择 ChatGPT / Codex 或 GitHub Copilot。
- 打开显示的设备授权 URL,并输入设备代码。
- 等待引导流程完成。Underclass 会使用账户的电子邮件地址或用户名为其设置标签。
- 对所有应参与池的订阅重复此过程。
UI 还可以启用、禁用、移除或重新登录账户。被禁用的账户仍会保留配置,但不会被选用于处理请求。
连接 OpenCode
Underclass 包含一个 connect 命令,可向 OpenCode 添加 Underclass 提供商:
cargo run -- connect
默认情况下,该命令会更新 ~/.config/opencode/ 下的全局 OpenCode 配置。它会将提供商配置块合并到 opencode.json 或 opencode.jsonc,将凭据写入 auth.json,并创建备份。该操作具有幂等性,因此可以安全地重复运行。
使用池中公开的模型启动 OpenCode:
opencode --provider underclass --model underclass/gpt-5.5
若要改为写入项目本地配置,请添加 --project:
cargo run -- connect --project
预览合并后的配置而不写入任何文件:
cargo run -- connect --dry-run
不再需要时,移除生成的集成配置:
cargo run -- connect --remove
如果需要自定义生成的提供商配置,该命令还接受 --base-url、--api-key、--model 和 --no-default-model。
使用 OpenAI 兼容 API
任何兼容的客户端都可以使用该代理:将其基础 URL 设置为 Underclass 监听地址,并将代理密钥作为 bearer 令牌提供。
列出可用模型
curl http://127.0.0.1:8080/v1/models \
-H "Authorization: Bearer $UNDERCLASS_PROXY_KEY"
models 端点返回一个合并限制后的联合目录。该目录预置了有文档记录的 Codex 模型系列和 Copilot 的实时模型列表。由于目录以数据形式存储,因此无需修改代理代码,即可通过 UI 或管理 API 进行编辑。
发送聊天补全请求
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."}
]
}'
代理还支持 POST /v1/responses。这两个请求路由都会通过选定的账户流式传输结果。流式传输开始后,上游故障会直接传递,而不会悄悄重新发送已部分完成的请求。
了解路由和故障转移
粘性会话行为
当请求包含 prompt_cache_key 或 promptCacheKey 时,Underclass 会将该值绑定到一个订阅。配置 setCacheKey: true 后,OpenCode 可以发送其会话 ID。绑定持续 24 小时,并且在代理重启后仍然有效。
如果分配的账户开始进入冷却状态,Underclass 会优先将会话重新绑定到同一后端上的其他账户。此设计有助于保留上游提示缓存的优势,同时仍能从配额耗尽中恢复。
配额和身份验证处理
配额响应,包括相关的 429 或使用限制响应,会使账户进入冷却状态。上游提供 Retry-After 值时,Underclass 会使用该值;否则使用已配置的后端专属冷却时间。冷却结束后,冷却中的账户会自动重新获得资格。
对于上游 401,Underclass 会刷新一次令牌并重试一次。如果身份验证仍然失败,则账户需要重新登录。
池饱和
如果没有符合条件的账户可用于请求的模型,Underclass 不会将请求放入队列等待,而是立即返回 429,并通过 Retry-After 报告已知的最早重置时间。
配置代理
可将可选的持久化配置放置在 ~/.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
当上游服务未提供重试间隔时,各后端的默认冷却时间为 1,800 秒。
环境变量会覆盖对应的配置值:
UNDERCLASS_BINDUNDERCLASS_PROXY_KEYUNDERCLASS_UI_TOKEN
针对模拟服务进行测试时,请使用 UNDERCLASS_CODEX_UPSTREAM 和 UNDERCLASS_COPILOT_UPSTREAM。它们的默认值指向真实的上游端点。
Underclass 会将凭据、粘性绑定、生成的密钥和模型目录存储在 ~/.local/share/underclass/pool.db 中。删除该数据库即可完全重置本地状态。
高级运维提示
检查日志并关联请求
每个已处理的响应都包含 x-request-id。日志采用结构化格式,可通过 --log-format json 或 --log-format pretty 使用 JSON 或易读输出。使用 RUST_LOG 控制筛选。
将 preflight 放置在 Underclass 前面时,作为 x-request-id 提供的有效 UUIDv4 会保留在响应、路由日志、重试日志和请求历史中。无效、重复、nil、缺失或非 v4 标识符会被替换。该标识符仅用于关联,不代表授权或检查已完成。
谨慎管理模型目录
路由资格和 OpenCode 模型块都遵循存储的目录。使用 Web UI 或管理目录 API 检查或编辑后端模型映射。未知的模型标识符会直接传递给 Codex,因此新引入的 Codex 模型无需发布代理新版本即可使用。
默认保持服务私有
Underclass 默认监听 localhost。除非确实需要远程访问并已采取保护措施,否则请保留此默认设置。代理不会记录访问令牌、刷新令牌、授权标头或提示正文,但账户标签会显示在日志和 UI 中。
- 不要提交
pool.db或生成的*.bak文件。 - 将 UI 令牌与客户端代理密钥分开保护。
- 妥善保护 OpenCode 生成的
auth.json;Underclass 会以0600权限写入该文件。 - 在将服务暴露到 localhost 之外之前,使用安全隧道或同等审慎的保护措施。
在 NixOS 上运行 Underclass
该 flake 导出一个 NixOS 模块。服务配置可以启用 Underclass、将其绑定到本地地址、从运行时环境文件加载密钥,并设置后端冷却时间:
{
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;
};
};
}
];
};
};
}
运行时环境文件可以保存这些密钥,而无需将它们放入 Nix store:
UNDERCLASS_PROXY_KEY=sk-underclass-replace-me
UNDERCLASS_UI_TOKEN=replace-me
该模块使用动态用户,将持久化数据存储在 /var/lib/underclass,默认绑定到 localhost,并保持防火墙关闭。只有在你明确需要绑定到 localhost 之外时,才启用 services.underclass.openFirewall。
开发和测试项目
使用 Cargo 构建项目并运行测试套件:
cargo build
cargo test
该项目结合了单元测试、Hegel 属性测试和端到端测试。池与健康检查核心是同步的,并使用注入的时钟,而异步行为则保留在边界层。新提供程序需要实现 provider::Backend trait,并向应用程序注册。
如需了解架构背景,请查阅项目的架构决策记录。路由模型在 ADR 0010 中有说明,跨代理请求关联则由 ADR 0012 介绍。
结论
Underclass 将多个 ChatGPT/Codex 和 GitHub Copilot 订阅转换为一个本地、兼容 OpenAI 的服务。其粘性路由、自动冷却恢复、感知模型的账户选择以及快速失败的饱和行为,减少了手动切换账户的需要,同时保持客户端行为的可预测性。启动服务器,通过 Web UI 接入账户,运行 underclass connect,然后将 OpenCode 或其他兼容客户端指向生成的端点。
