本站其他所有指南都是把 Claude Code 路由到另一个后端。shunt 也可以反向运行:一个可选启用的原始 OpenAI Responses 透传,让 Codex CLI 把它自己的 base_url 指向 shunt,并在 ChatGPT/Codex OAuth 账户池上做负载均衡。它是可选启用的:[server.codex_endpoint] 不存在时,这些路由一个都不会注册,shunt 默认的 HTTP 暴露面保持不变。
它建立在与 Codex 多账户相同的账户池之上 —— 选择、冷却和刷新都原封不动地共用。完整规范(包括确切的故障转移表和重载语义)见 M11 行为规范。
端到端的设置演练 —— 启用端点、把 Codex CLI 指向 shunt、客户端认证、账户预配以及选择一个有权限的模型 —— 请参阅连接 Codex CLI。本页关注这个端点做什么;那篇指南则是如何连接的清单。
启用该端点
[server.codex_endpoint] # all keys optional; default shown
provider = "codex" # must be a chatgpt_oauth providershunt check
shunt run启动校验会拒绝未知的 provider,或者不使用 auth = "chatgpt_oauth" 的提供方 —— 该端点注入的是运营者的 Codex bearer,因此只有 chatgpt_oauth 提供方符合条件。每个键与默认值见配置参考,已注册的路由见 HTTP 端点。
该可选功能也使 Codex CLI 的模型发现可被正确解析。GET /models 和 GET /backend-api/codex/models 返回有效的回退形状 {"models":[]}。在共用的 GET /v1/models 路径上,client_version 查询字段优先于类 Anthropic 的头部并选择 Codex 形状;没有该字段时,现有 Anthropic 发现响应保持不变。这些请求先通过常规模型发现认证门,且 shunt 不会伪造不完整的 Codex 模型行。
网页搜索
Codex CLI 的内置网页搜索工具(web.run)会向 {base_url}/alpha/search 发送 POST 请求,这与它追加 /responses 的 base URL 相同。shunt 为每种 base URL 形式都注册了该路径:
POST /backend-api/codex/alpha/searchPOST /alpha/searchPOST /v1/alpha/search
搜索与 Responses 轮次使用相同的 [server.auth] 策略和相同的 ChatGPT/Codex 账户池(会话粘性选择、故障转移、刷新),并把请求体和响应逐字中继到后端的 /codex/alpha/search。搜索不是模型轮次,因此 [[server.codex_endpoint.routes]] 不适用,它总是发往 [server.codex_endpoint].provider。这些路由只接受 POST —— 该工具没有 WebSocket 形式。
客户端分析数据接收端
Codex CLI 还会向 base URL 提交产品分析数据。shunt 接受该 CLI 可能产生的两条路径:
POST /backend-api/codex/analytics-events/eventsPOST /codex/analytics-events/events
这些路由使用与 Responses 路由相同的 [server.auth] 策略,但绝不会把遥测转发到上游,因为挑选某一个池化账户会把客户端事件错误地归属到该账户。认证之后它们总是返回 200 {},包括请求体格式错误、不可读或过大的情况。
载荷与事件属性既不会被记录也不会被导出。shunt 只把经过净化的 event_type 记录为可选启用的 shunt.codex_client_events 计数器上的 event 属性:名称可以包含小写 ASCII 字母、数字、.、_ 和 -,最长 64 字节;无效的名称变为 other,无法解析的批次变为 unparsed。在没有启用 Sentry 或 OpenTelemetry 指标时,这就是一个纯粹的丢弃接收端。
把 Codex CLI 指向 shunt
Codex CLI 总会把 /responses 追加到它使用的任何 base URL 之后,因此下面两种 ~/.codex/config.toml 形态都可以:
镜像 ChatGPT 后端的 base URL:
chatgpt_base_url = "http://127.0.0.1:3001/backend-api/codex"或者用一个自定义模型提供方(顶层的 model_provider 必须选中它,否则 CLI 会继续使用它内置的提供方):
model_provider = "shunt"
[model_providers.shunt]
base_url = "http://127.0.0.1:3001/v1"
wire_api = "responses"使用自定义提供方时(加上 requires_openai_auth = false,这样 CLI 不需要本地登录),一旦指向 shunt,Codex CLI 自己的 ~/.codex/auth.json 就变得无关紧要 —— 每个请求的账户都来自 shunt 的池。而 chatgpt_base_url 形态会让 CLI 停留在 ChatGPT 登录模式,因此它仍需本地登录,并且只能对未设门控的端点工作:它的 ChatGPT bearer 不是所配置的 shunt token,因此 [server.auth] 会拒绝它。
客户端认证
如果 shunt 配置了 [server.auth] —— 回环之外的场景都推荐配置 —— 请要么把客户端 token 作为 OpenAI 风格的 Bearer key 提供(OPENAI_API_KEY / 自定义提供方的 env_key,即 LiteLLM/llmgateway 的惯用法),要么作为 x-shunt-token 头部提供:
# A. Bearer — built-in openai provider. Set the base URL in ~/.codex/config.toml,
# NOT via the OPENAI_BASE_URL env var: the env var leaves the CLI's Responses
# WebSocket pointed at wss://api.openai.com, so it bypasses shunt. See
# "Point the Codex CLI at shunt" in the connect guide.
openai_base_url = "http://127.0.0.1:3001/v1"export OPENAI_API_KEY="<shunt-token>" # sent as Authorization: Bearer# B. Header — a custom provider carries it (use env_http_headers to keep it out of the file):
[model_providers.shunt]
base_url = "http://127.0.0.1:3001/v1"
wire_api = "responses"
http_headers = { "x-shunt-token" = "<token>" }没有 [server.auth] 时,该端点对任何能触达它的人开放 —— 对回环或个人使用可以接受,对共享网关则不行。客户端提供的凭据仅用于向 shunt 认证:它(以及 CLI 碰巧发送的任何 Authorization)都会被剥除,绝不转发到上游。[server.admin] 的凭据头部 —— 默认 x-shunt-admin-token,或 [server.admin] header 指定的名字 —— 同样会被剥除,因为管理面正是在该槽位上认证,而管理凭据可以开通上游账户。整个 cookie 头部也会被剥除:管理面同样在该槽位接受会话 cookie,而 shunt 不保留 cookie jar,上游不会依赖它。x-api-key 也会被无条件剥除 —— 即使未配置 [server.auth] 也是如此,因为目标提供方在启动时就被校验为仅 chatgpt_oauth,所以入站的 x-api-key 值永远不可能是该上游的有效凭据;像 Claude Code 的 apiKeyHelper 那样在 Authorization 和 x-api-key 中填入同一个密钥的客户端,也不会因为第二个槽位而泄露该密钥。由于入站客户端是真正的 Codex CLI,该透传会逐字转发它的请求头部(version、originator、OpenAI-Beta、x-codex-* 等),并只换入所选池账户的 Authorization bearer 与 chatgpt-account-id。完整的认证演练见连接 Codex CLI。
WebSocket 传输
这三个 Responses 路径同时接受 HTTP POST 和经过身份验证的 WebSocket GET 升级。身份验证在 101 Switching Protocols 之前完成。在套接字上,generate: false 预热在本地完成;常规的 response.create 帧复用 HTTP 账户池,强制上游流式传输,并在第一个终端事件之前将每个 SSE data: 有效载荷作为 WebSocket 文本帧转发。替换轮次或关闭套接字将取消活动的上游请求体。客户端帧和 SSE 事件限制为 4 MiB,发送应用有界背压,协议/上游故障使用仅包含安全响应元数据的独立 type: "error" 帧。
没有 [server.auth] 时,浏览器发起的升级通过比较 Origin 与 Host 来检查,而该检查无法区分同一主机上的 http 与 https,因此在回环之外的场景请用 [server.auth] 保护该端点。
账户预配
复用与 Codex 多账户相同的账户池:
codex login
shunt login codex --name main[[providers.codex.accounts]]
name = "main"在没有配置 [[providers.codex.accounts]] 且 shunt 账户存储为空时,该端点会回退到单个默认的 ~/.codex/auth.json 凭据 —— 没有池化,也没有故障转移 —— 因此只要设置了 [server.codex_endpoint],一个 Codex 登录就能工作。(处理器会先扫描账户存储,并把发现的任何账户组成池,因此导入到存储中的账户仍然会启用池化。)
把模型路由到其他上游
默认情况下,所有请求都会发往 [server.codex_endpoint] 中指定的那一个提供方。可选的 [[server.codex_endpoint.routes]] 表让 Codex CLI 可以按模型 id 选择另一个兼容 Responses 的上游;没有配置路由的模型仍保持固定提供方的行为。
多家厂商都为 Codex CLI 提供了原生 Responses 端点的文档:Z.ai GLM(https://api.z.ai/api/v1)、DeepSeek(https://api.deepseek.com)、Kimi Code(https://api.kimi.com/coding/v1)、MiniMax(https://api.minimax.io/v1)、Mimo(https://api.xiaomimimo.com/v1)、OpenRouter(https://openrouter.ai/api/v1)、Vercel AI Gateway(https://ai-gateway.vercel.sh/codex/v1),以及原生 OpenAI。shunt 会在提供方的 base_url 后追加 /responses,所以直接填写厂商为 Codex 所记载的同一个 base URL 即可。上游必须原生实现 Responses API —— 这里没有 Responses → Chat Completions 的适配层。
[providers.glm]
kind = "responses"
auth = "api_key"
api_key_env = "GLM_API_KEY"
base_url = "https://api.z.ai/api/v1"
[providers.deepseek]
kind = "responses"
auth = "api_key"
api_key_env = "DEEPSEEK_API_KEY"
base_url = "https://api.deepseek.com"
[server.codex_endpoint]
provider = "codex"
[[server.codex_endpoint.routes]]
model = "glm-5.3"
provider = "glm"
[[server.codex_endpoint.routes]]
model = "deepseek-v4-flash"
provider = "deepseek"upstream_model 是可选的,默认等于 model;当 CLI 中填写的 id 与厂商实际提供的 id 不同时再设置它。被路由到的提供方必须带有真实凭据 —— 客户端自己的 Authorization 总是会被剥离,因此不携带凭据的认证模式(passthrough 或 none)会在启动时被拒绝。shunt 内置的 kimi 预设是 kind = "anthropic",因此指向 Kimi Code 的 Codex 路由需要另建一个 kind = "responses" 的提供方 —— 把 Codex 模型路由到 Anthropic 类型的预设会在启动时被拒绝。
在 CLI 一侧,把 Codex 指向 shunt,并用 model 选择路由:
# ~/.codex/config.toml
model = "glm-5.3"
model_provider = "shunt"
model_catalog_json = "~/.codex/models.json"
[model_providers.shunt]
base_url = "http://127.0.0.1:3001/v1"
wire_api = "responses"
env_key = "SHUNT_TOKEN"shunt 对 Codex CLI 的发现请求返回有效的空回退 {"models":[]},但不会在模型列表中公布 Codex 路由。CLI 按这些厂商记载的方式,从 model_catalog_json 指向的 ~/.codex/models.json 目录获取模型标识的元数据;真正选中 shunt 路由的只有 model 的取值。
路由到非 ChatGPT 上游的请求有以下不同:
- 请求头白名单。 新白名单起初只从客户端取得
content-type(缺省为application/json)和accept,然后加入解析出的凭据,以及被路由到的上游自身所需的请求头:xAI/Grok 会省略的OpenAI-Beta: responses=experimental、xai_oauth路由的整套 Grok CLI identity 请求头,以及当api_key路由的目标主机恰好是api.openai.com且解析出了非空 conversation id 时,重新生成的四个会话亲和请求头(session-id、thread-id、x-client-request-id、x-codex-window-id)。客户端发送的authorization、x-api-key、chatgpt-account-id、originator、version、user-agent、session-id、thread-id、x-client-request-id、x-codex-*、x-shunt-*仍会被剥离;原生 OpenAI 所用的值也是重新生成,而不是转发。xAI 和其他第三方 OpenAI 兼容主机都不会收到这些会话亲和请求头。API 密钥路径也不会生成accept: text/event-stream。 - 改写请求体的
model。 当upstream_model与请求的模型不同时,shunt 只改写顶层的model,其余字段原样保留。不是 JSON 对象的请求体会以400拒绝,而不会被继续转发。 - identity 编码。 zstd 请求体会先解码(原生 Responses API 不接受该编码),且不转发
content-encoding。 - 单一凭据,无故障转移。 被路由到的第三方背后没有故障转移账号池,因此 429 或 5xx 会连同
retry-after原样转发,不会触发轮换。唯一感知账户池的例外是带命名账户的xai_oauth路由:它同样只发送一次请求(上游失败会原样转发,不会轮换),其凭据是账户池选择顺序中第一个能加载登录的账户(遵循提供方的account/accounts范围),无法加载登录的账户会被冷却并跳过;没有命名账户时,它是~/.shunt/xai-auth.json。
匹配是精确且区分大小写的,并且不限制字符集,所以 MiniMax-M3、openai/gpt-5.6-sol、~openai/gpt-latest 这类厂商标识都会按写法路由。指向另一个 chatgpt_oauth 提供方的路由则仍保留完整的账号池透传。路由从实时配置读取,因此在重载时生效。
支出限制
配置 [server.spend]后,该端点会按与 /v1/messages 相同的主体上限进行检查和计量;未配置时下列内容均不适用。完整行为见 Gateway spend limits(英文)。
- 检查时机。 每个回合在调用任何上游之前检查,覆盖三条 Responses 路径的 HTTP
POST和 Responses WebSocket。主体是静态[server.auth]令牌名、已验证的 JWT 邮箱,或[server.auth]关闭时的匿名主体。该端点不接受 gateway 登录会话。 - 拒绝响应。 超限的回合收到
429和{"error":{"message":…,"type":"usage_limit_reached","code":"usage_limit_reached","resets_at":<纪元秒>}},并带有retry-after、x-should-retry: false和x-codex-promo-message: <blocked_message>头部。仅当blocked_message为纯 ASCII 时才发送 promo 头部,文本始终也在 message 中。被点名的上限是已达上限中最晚重置的一个。启用fail_closed_on_error时,支出状态不可用的有上限主体会以相同的 type 被拒绝,且没有resets_at和retry-after。 - Codex CLI 的显示。 在固定版本
rust-v0.159.3上,Codex CLI 不会重试该错误。它根据resets_at显示自己的文案(“You’ve hit your usage limit … Try again at”),头部存在时一并显示 promo 消息,并忽略服务器消息。这是 Codex CLI 的行为,其他版本可能不同。 - WebSocket。 被拒绝的
response.create会收到一个{"type":"error","status":429,"error":{…},"headers":{…}}帧,其中包含相同的错误以及retry-after和x-codex-promo-message。套接字保持打开,之后每个回合都会重新检查,generate: false的预热不检查。 - 网页搜索。
/alpha/search及其别名在超限时被拒绝,且从不计量。 - 计量。 已完成的回合按 Responses 的
usage计费,计入/v1/messages所写的同一组计数器,因此一个上限同时约束两个界面。缓存 token 按缓存读取费率计费,其余输入按输入费率计费;推理计入输出,没有缓存写入。未报告用量就结束的回合(客户端关闭、被替换的回合、中断的流)按“由请求体估算的输入加上已送达的输出”这一下限计费。非流式的 2xx JSON 响应体同样按其usage计费;未报告输入的响应体则改按估算的输入计费。 - GPT 价格。 内置价目表没有 GPT/Codex 的价格,因此这些模型在添加行之前按未知模型费率计费(每百万 token
$5 / $25 / $0.50 / $6.25,每个模型一次警告):
[[server.spend.pricing.overrides]]
upstream = "codex" # 端点的 provider
model = "gpt-6.1-sol"
input = 1.00 # 示例费率,每百万 token 的美元数
output = 8.00
cache_read = 0.10
cache_write = 1.00与 /v1/messages 的差异
- 没有转换。入站的 Responses 请求体会逐字节转发到上游,而上游的响应 —— 无论 SSE 还是 JSON、成功还是错误 —— 都逐字中继回来(状态码与
content-type保留)。完全没有 Anthropic Messages ⇄ Responses 的转换步骤。 - 压缩的请求体直接透传。当前的 Codex 版本在与 ChatGPT 后端通信时会用 zstd 压缩请求体,这也包括指向本端点的
chatgpt_base_url形态。这些字节及其content-encoding: zstd头部会被原样转发;shunt 只是额外在内存中解码一份副本,用来读取请求的model以供指标、日志和 span 使用。shunt 无法解码的请求体照样能正常中继 —— 只有model标签会退化为unknown,并附带一条说明原因的警告。 - 基于模型的路由是可选项。默认情况下每个请求都发往
[server.codex_endpoint]中指定的那一个提供方,请求体的model字段原样转发。配置[[server.codex_endpoint.routes]]后,精确匹配的model会改为选中该条目的提供方 —— 参见把模型路由到其他上游。 - 耗尽时逐字中继。如果所有池化账户都已尝试过,并且至少收到过一个上游响应,shunt 会原样中继最后那个响应,而不是把它重新塑形成 Anthropic 风格的错误 —— 因为 Responses 客户端期待的是它从真实 ChatGPT 后端会得到的原始形态。
- 网关自身的错误使用 OpenAI 形态。当失败源自 shunt 自己时 —— 客户端 token 错误或缺失(
401)、账户池不可用且没有任何上游响应(502)、请求体过大,或端点未配置 —— shunt 会以 OpenAI Responses 的错误形态({"error":{"message":…,"type":…,"code":null}})返回,并保持相同的状态码,这样 Codex CLI 就能走它自己的错误解析路径,而不是 Anthropic 的{"type":"error",…}信封。被中继的上游错误(来自后端的 429/4xx/5xx)仍然逐字透传。 - 两种入站传输。HTTP
POST保持字节级保真;WebSocketGET添加了有界事件交付,且不依赖于提供方的出站websocket = true设置。
安全
- 在回环之外的任何场景都请用
[server.auth]为该端点设置门控 —— 提供方会在每个请求上注入一个真实的 Codex bearer。 - 客户端自己的凭据不会有任何部分到达 Codex 后端;该透传逐字转发 Codex CLI 自己的请求头部,并只换入所选池账户的 bearer 与
chatgpt-account-id(shunt 客户端 token 头部、[server.admin]凭据头部、整个cookie头部、内部的x-shunt-inbound-client标签、客户端的Authorization/chatgpt-account-id,以及x-api-key都会被剥除,绝不转发)。 - 启动时一次性确定的只有该端点的 HTTP 路由注册:在运行时开启或关闭
[server.codex_endpoint]会记录一条警告,说明需要重启才能添加或移除这些路径。该表所包含的内容全部支持热重载 —— 目标provider和整个[[server.codex_endpoint.routes]]模型表都会在每次请求时从实时配置读取,因此添加、修改或删除路由都会在 reload 时生效。