Skip to content

Codex 多账户

将 ChatGPT/Codex 订阅 OAuth 账户组成池,以会话粘性、感知配额的主动选择和被动故障转移运行。

Updated View as Markdown

shunt 可以在一个 chatgpt_oauth 提供方背后把多个 ChatGPT 订阅 OAuth 凭据组成池 —— 既可以是内置的 codex 提供方,也可以是任何使用该认证模式的 responses 提供方。当 Claude Code 提供 x-claude-code-session-id 时请求具有会话粘性;不带该头部的请求使用按提供方的轮询。与 Anthropic 账户池一样,选择是感知配额的:shunt 记录后端的 x-codex-* 5 小时/7 天用量窗口,并在粘性账户接近其配额时主动轮换掉它,而基于冷却的被动故障转移仍是安全兜底。

面向客户端的 GET /usage 聚合仅在没有任何未禁用账户报告某个窗口时才将该窗口报告为 null。Codex 响应中的 x-codex-* 头部和可选的 wham/usage 轮询会填充 5 小时和共享每周窗口;Codex 本身没有 Fable 范围(7d_oi)的信号,但混合提供方池中的其他提供方可以提供聚合 Fable 值。轮询默认关闭。

配置账户池

先用 Codex CLI 登录 —— shunt 从不自己执行 ChatGPT 登录,只导入 codex login 已经写好的凭据:

codex login

然后设置 auth = "chatgpt_oauth" 并添加显式的账户条目:

[providers.codex]
kind = "responses"
base_url = "https://chatgpt.com/backend-api"
auth = "chatgpt_oauth"

# A second Codex CLI login, imported under a distinct name.
[[providers.codex.accounts]]
name = "primary"
credentials = "~/.shunt/accounts/codex/primary.json"

# A raw ChatGPT access token supplied out of band. Used verbatim; not refreshed.
[[providers.codex.accounts]]
name = "backup"
token_env = "CODEX_BACKUP_ACCESS_TOKEN"
export CODEX_BACKUP_ACCESS_TOKEN='<a valid ChatGPT access token>'
shunt check
shunt run

把一次 Codex CLI 登录导入 shunt 自己的存储来保存账户:

shunt login codex --name primary

然后使用只带名字的条目:

[[providers.codex.accounts]]
name = "primary"

[[providers.codex.accounts]]
name = "backup"

存储文件位于 ~/.shunt/accounts/codex/<name>.json;设置 SHUNT_CODEX_ACCOUNTS_DIR 可覆盖该目录。如果配置的 accounts 列表为空,shunt 会扫描存储目录,按文件名顺序使用所有有效的 JSON 账户文件。存储文件是私有的(Unix 上为 0600,目录为 0700),并原样保存 Codex CLI 自己的 auth.json 形态 —— 与 Claude 存储不同,这里没有外层包装对象。

shunt login codex 没有 --long-lived 标志 —— Codex 没有 setup-token 这一概念,因此每个由存储管理的账户,都是从已有的 codex login 导入的可刷新 OAuth 登录。

导入只接受 ChatGPT 登录。当前版本的 codex login 会向 $CODEX_AUTH_FILE(或 ~/.codex/auth.json)写入 "auth_mode": "chatgpt";shunt 比较该字段时忽略大小写,因此旧版 Codex 留下的文件("ChatGPT")也能原样导入。API 密钥登录("auth_mode": "apikey")会被拒绝,并报 is not a ChatGPT login (auth_mode is not "chatgpt") —— 请重新运行 codex login 并选择 ChatGPT 账户。

账户字段

字段 必填 含义
name 是 只含小写字母、数字和连字符的唯一标签。若没有其他来源字段,则解析同名的 shunt 存储文件。
credentials 可用来源之一 Codex CLI auth.json 形态的文件。shunt 在临近过期时刷新,并将刷新后的 token 原子性地写回,与 ~/.codex/auth.json 本身的处理方式相同。
token_env 可用来源之一 包含原始 ChatGPT access token 的环境变量。按原样使用;401 之后无法刷新。
uuid 否 用于合并别名的稳定上游身份。仅有名称的条目(通过存储扫描解析)会在选择发生前自动填入该值,因此存储扫描得到的别名无需任何配置改动即可合并。通过 credentials 或 token_env 配置的条目,其账户 id 只在选择之后才解析,并且绝不写入 uuid,因此这类条目的身份在设置了 uuid 时为该值,否则为其 name;只要该身份与另一别名显式的 uuid 或名称回退身份相等,就会与之合并 —— 为清晰、有意的合并,请在两个条目上设置匹配且非空的 uuid(当 uuid 与 name 意外交叉碰撞时,shunt 也会发出警告)。它不会被写入 Codex 请求体。
priority 否 可用账户之间的选择优先级;数值越低越优先,默认 100。在 Codex 路径上生效。
disabled 否 true 把该账户完全移出选择,同时保留在配置中。在 Codex 路径上生效。

不要在同一个账户上同时设置 credentials 和 token_env。

账户 id 解析

Codex 没有可配置的显式 uuid 字段。作为替代,shunt 对每个账户会:

  1. 优先使用存储的 tokens.account_id;
  2. 否则解码 access_token JWT 并读取 chatgpt_account_id claim;以及
  3. 在任何一次刷新之后,仅根据新 access token 的 JWT claim 重新计算该 id(刷新响应中没有独立的 account-id 字段)。

如果两个来源都得不到 id,该账户解析失败,并按下文当作凭据解析失败处理。只有存储扫描(accounts 列表为空)会在选择之前把解析出的 id 写入内存中账户的 uuid 字段 —— 通过 credentials/token_env 配置的账户,其 id 按请求解析,绝不回写到 uuid。存储扫描本身按存储目录的 mtime 缓存,因此稳态下的发现只执行一次目录 stat,不读取任何凭据文件;而解析所选账户的实际凭据时,仍会按需按请求读取其凭据文件,以检查或刷新过期时间。

选择与冷却

  • 带 x-claude-code-session-id 时:一个稳定的哈希选出粘性账户,机制与 Anthropic 账户池相同。
  • 不带该头部时:每个提供方有自己的轮询计数器。
  • 成功的响应会记录 x-codex-primary-* 与 x-codex-secondary-* 头部中的用量。shunt 按 window-minutes 映射每一组:约 300 分钟归入 5h,约 10080 分钟归入 7d,其他窗口被忽略。即使某组的 x-codex-*-reset-at 头部为空,shunt 仍会记录该位置对应的窗口组及其用量;没有重置时,用量会从自身观测时间起在一个窗口长度后过期。Codex 没有 7d_oi 的对应项。
  • 记录的用量会参与选择,与 Anthropic 账户池完全一致:达到或超过其阈值(内置的 0.98,或 [server.pool]/按账户的软阈值)的粘性账户会主动让位给余量最大的账户,burn_rate_avoidance 以及优先级/余量排序同样适用。x-codex-rate-limit-reached-type 的值仅被记录用于展示。
  • 硬上限(账户上的 max_utilization*,[server.pool] 中的 default_max_utilization*)在 5 小时或周使用率达到或超过上限期间,会把账户移出选择,而不是像软阈值那样只重新排序。Codex 没有 7d_oi 窗口,因此 Fable 上限的键在这里不起作用。当上限使得没有可选账户时,shunt 返回网关自身生成的 429(在入站 Codex 端点上为 OpenAI 错误形态),它会推进 [[upstreams]] 故障转移链。对于已提交(已发送 HTTP 200)的流式请求,若后面的上游都无法处理,客户端收到的是流内的终止 SSE error 事件,而不是 HTTP 429,也没有 retry-after。参见硬上限。
  • 设置了 [server.pool] ramp_initial_concurrency 时,一个 slow-start 准入门控会保护刚刚选中的账户,使其在故障转移之后不被瞬间冲垮;参见 [server.pool]。
  • 设置 [server.pool] 后,一个没有其他观测信号的陈旧近配额账户会每隔 reprobe_seconds(默认 900 秒,0 则禁用)被机会性地提升并预留在选择顺序最前面,让实际请求刷新其配额;参见 [server.pool]。admission 或凭据解析失败会取消预留,首次 HTTP 发送开始时才提交重新探测时间并增加 shunt.pool.reprobes。这条路径在非 WebSocket outbound 选择和可选的 inbound Codex HTTP 端点中,充当下文用量轮询器关闭或两次轮询之间的后备手段。启用 WebSocket 的提供方不会在 outbound Responses 池创建预留,并会抑制这种重新探测,因为流内 rate-limit 事件无法安全轮换;可选的 inbound 端点仍会探测,因此该提供方的计数器只统计 inbound 探测。已配置的轮询器只为 imported 且可刷新的账户提供提前恢复;没有轮询器或账户不符合资格时,被排除的 outbound 标记会一直保留到基于观测时间的窗口寿命上限到期。新鲜度使用 5h、共享 7d、Fable 7d_oi 与 aggregate status 四个观测时间戳。
  • 成功的响应会清除该账户的冷却。
触发条件 冷却
凭据解析失败(account id/tokens 无法解析) 5 分钟
传输/连接失败 30 秒
上游返回 5xx 30 秒
上游返回 429 按数值 retry-after,钳制到 1–3600 秒,默认 60 秒
token_env 账户返回 401 5 分钟
credentials/存储账户返回 401,且刷新失败或重试仍为 401 5 分钟
400 模型拒绝(见下文) 1 小时,仅针对该账户上的该模型

用量轮询器

正的 usage_refresh_seconds 还会对每个 imported(可刷新)的 chatgpt_oauth 账户轮询 GET /wham/usage,以 Anthropic 账户池对 GET /api/oauth/usage 相同的方式,对账校正基于响应推导出的 5h/周配额状态。它填补的正是 reprobe_seconds 用实际请求填补的那个缺口:账户池因某账户接近配额而将其排除后,该账户就再也不会收到代理请求,它的 x-codex-* 头部也就永远不会刷新 —— 即便该账户的窗口早已实际重置也是如此。轮询器改为在带外读取权威用量。

这不是一个有文档记录的 OpenAI/ChatGPT API。 支撑其格式的证据来自 Codex CLI 自身观测到的 60 秒轮询行为,而非已发布的文档,因此它可能在没有任何预告的情况下变化或消失。解析从头到尾都刻意宽松且 fail-soft。成功解析且非空的 wham 响应会权威地反映它列出的窗口。当至少存在一个可用的 5h 或 7d 窗口时,完全没有候选窗口的 bucket 会清除其用量和用量观测时间戳,但会保留来自响应的重置时间元数据(x-codex-* header 与 WebSocket 的 codex.rate_limits 事件)和来自 header 的状态元数据。对于已报告的窗口,未来的存储重置时间会保留;已经过期的存储重置时间会在写入新用量前被清除。wham 的 reset_at 不会被采用为实际重置元数据。null 窗口表示不存在,但无法识别时长的候选属于未知,会抑制清除。获取或解析错误,以及没有可用已识别窗口的空快照,都会保留既有状态。这一点不同于 Claude 的 /api/oauth/usage 路径:该路径省略窗口时会保留原有值。某一个窗口格式损坏,不会阻止其相邻窗口照常生效。这里的任何解析失败都不会把账户标记为不健康,也不会影响请求路由,只会让基于头部的状态继续作为后备。

资格条件与 Claude 一侧一致:只有 imported 登录(存储管理的账户,或带 tokens.refresh_token 的内联 credentials 路径)才会被轮询 —— 原始的 token_env bearer 是静态的,会被跳过。

故障转移规则

响应 行为
2xx 中继并标记为健康。
429 总是冷却并轮换。可选的 Codex 拒绝信号只被记录用于展示,不会改变故障转移的分类,因此每个 429 都被视为该账户配额耗尽。
使用 credentials/存储时的 401 强制刷新,对同一账户重试一次;若刷新失败,或重试仍是 401,则冷却 5 分钟并轮换。
使用 token_env 时的 401 无法刷新:冷却 5 分钟并轮换。
5xx 或传输失败 冷却 30 秒并轮换。
凭据解析失败 冷却 5 分钟并轮换。
detail、error.message 或 message 中包含 model is not supported 的 400(不区分大小写) 后端尚未为该 ChatGPT 账户开放该模型(按账户的灰度门控,可能在几小时内变化),其他账户可能可以处理。轮换,并只对该账户上的该模型冷却 1 小时:该账户继续服务其他模型,不会被标记为健康,仪表盘上也不显示冷却。若所有账户都拒绝,则原样中继带有拒绝消息的 400。仅适用于 HTTP 传输:在 WebSocket 传输(见下文)中,拒绝以流内错误的形式到达,因此不轮换、直接中继。
其他状态(例如其他任何 400) 直接中继,不做故障转移;并把该账户标记为健康。

分类发生在响应体开始流式传输之前,因此流中途的失败绝不会被重放。 这也包括在 200 OK 流上到达的、code 为 rate_limit_exceeded、slow_down 或 flex_unavailable 的后端 error / response.failed 事件:非流式客户端会收到带 rate_limit_error 信封的 HTTP 429,而流式客户端保持 200,并以 SSE error 事件的形式内联收到同一个信封。两种情况下该回合都不会被重放,账户也不会被冷却或轮换 —— 上表中的 429 行只在上游 HTTP 响应状态本身为 429 时适用。如果池在收到至少一个上游响应之后耗尽了尝试,shunt 会基于最后那个响应构造并中继一个经过转换的 Anthropic 风格错误信封 —— 状态码(例如 429)保留,但响应体被重新塑形,而不是逐字中继。这与 Anthropic 账户池正好相反,后者会逐字节中继最后一个上游响应。如果所有账户都在收到任何上游响应之前失败,普通的 Anthropic Messages 路由(/v1/messages)会返回一个网关自身的 502 api_error,内容为 all upstreams failed (N attempted)。单独的 [server.codex_endpoint] 入站路径不受影响,并保留 all Codex OAuth accounts failed before receiving an upstream response。

请求与响应的改动

对所选账户,shunt 发送与单账户路径相同的 Codex CLI 身份头部,其值取自该账户的凭据:

authorization: Bearer <selected account's access token>
chatgpt-account-id: <selected account's account id>
originator: codex_cli_rs
OpenAI-Beta: responses=experimental

经过池的响应会标识账户 —— 非流式响应和 WebSocket 流式响应带 x-shunt-account 响应头部,早期提交的 HTTP 流式响应则在中继内容之前带 event: account SSE 帧(已提交的 200 头部无法等待获胜账户):

x-shunt-account: backup

在共享网关上请使用中性的账户名 —— 该头部和该帧会把配置的标签暴露给收到响应的每个已授权客户端。池耗尽后那次经过转换的错误中继会省略该标注。

WebSocket 传输

如果提供方还设置了 websocket = true(参见 ChatGPT / Codex),连接缓存的 key 会按账户加前缀,这样两个账户绝不会共用一个 socket 或它的 previous_response_id 续接状态。在任何 token 流出之前发生的 WebSocket 失败会在同一个账户上回退到 HTTP(而不是池轮换);只有 HTTP 路径的失败才会触发上面的故障转移。一次全新的 WebSocket 升级会记录其握手中的限流头部;被复用/预热的连接不会再次握手,因此没有头部信号 —— 但后端也会以流内的 codex.rate_limits 事件报告同样的窗口,shunt 每一轮都会观测它,因此它们的仪表板用量每一轮都会刷新。

安全约束

chatgpt_oauth 仅在以下条件下被接受:

  • base_url 使用 HTTPS;且
  • 其主机是 chatgpt.com 或其子域。

与 xai_oauth 一样,chatgpt_oauth 要求 kind = "responses" —— 这是 Codex 后端的 kind,与 openai 和 xai 共用。这是一道 bearer 泄漏防护:只有 Responses 适配器会注入 Codex bearer,因此一个不匹配的 kind = "anthropic" 提供方会被路由到 Anthropic 适配器,转而把客户端自己的凭据发往源外的 chatgpt.com。这些启动检查防止 OAuth bearer 被发送到源之外或以明文传输。HTTPS 与主机检查在回环主机上放宽(localhost、127.0.0.1、[::1] 等):回环的 base_url 可以使用纯 HTTP 和任意主机,这样本地调试代理或 mock 能接收流量。非回环主机始终要求 HTTPS + chatgpt.com。在共享部署上,还应配置 [server.auth],因为 chatgpt_oauth 花费的是网关自有的凭据。

不受影响:单账户路径

没有配置 accounts 的 chatgpt_oauth 提供方(默认的 codex 提供方)行为与以前完全一致:它直接读取并刷新 ~/.codex/auth.json(或 $CODEX_AUTH_FILE),没有冷却,也没有 x-shunt-account 头部。

后续待办

  • 管理 Web 预配: 可选启用的管理界面可以在浏览器中运行 ChatGPT OAuth、存储一个可刷新的 Codex 账户、在共享的池表格中展示它,并显示上报的 5h/7d 用量(7d_oi 始终为空,因为 Codex 没有对应项)。

账户池的完整内部机制见 M10 行为规范,单账户设置、模型路由以及力度/上下文配置见 ChatGPT / Codex 指南。

Navigation

Type to search…

↑↓ navigate↵ selectEsc close