shunt は、chatgpt_oauth プロバイダー — 組み込みの codex プロバイダー、またはその認証モードを使う任意の responses プロバイダー — の背後で、複数の ChatGPT サブスクリプション OAuth 認証情報をプールできます。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 runCodex 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 |
使用可能なソースのいずれか 1 つ | Codex CLI の auth.json 形式のファイル。shunt は期限が近づくとリフレッシュし、リフレッシュ済みトークンをアトミックに書き戻します(~/.codex/auth.json そのものと同じ挙動です)。 |
token_env |
使用可能なソースのいずれか 1 つ | 生の ChatGPT アクセストークンを含む環境変数。値はそのまま使われ、401 の後にリフレッシュできません。 |
uuid |
いいえ | エイリアスを統合するために使われる、安定した上流のアイデンティティ。名前のみのエントリ(ストアスキャンで解決)は選択が行われる前に自動的に埋められるため、ストアスキャンで検出されたエイリアスは設定変更なしで統合されます。credentials または token_env で設定されたエントリのアカウント id は選択の後にのみ解決され、uuid に書き込まれることはありません。そのため、そうしたエントリのアイデンティティは、設定されていれば uuid、なければ name です。そのアイデンティティが他のエイリアスの明示的な uuid または名前フォールバックのアイデンティティと一致するたびに統合されるため、明確で意図的な統合のためには両方のエントリに一致する空白だけでない uuid を設定してください(shunt は uuid と name の偶発的な衝突についても警告します)。この値が Codex のリクエストボディへ書き込まれることはありません。 |
priority |
いいえ | 利用可能なアカウント間での選択優先度。値が小さいほど優先され、デフォルトは 100 です。Codex 経路でも有効です。 |
disabled |
いいえ | true にすると、設定には残したまま、アカウントを選択対象から完全に除外します。Codex 経路でも有効です。 |
1 つのアカウントに credentials と token_env の両方を設定しないでください。
アカウント id の解決
Codex には設定可能な明示的な uuid フィールドがありません。代わりに shunt は、各アカウントについて次を行います。
- 保存された
tokens.account_idを優先します。 - なければ
access_tokenの JWT をデコードし、chatgpt_account_idクレームを読み取ります。 - リフレッシュ後は、新しいアクセストークンの JWT クレームのみから id を再計算します(リフレッシュのレスポンスに独立したアカウント id フィールドはありません)。
どちらのソースからも id が得られない場合、そのアカウントは解決に失敗し、後述の認証情報解決の失敗として扱われます。解決された id を選択の前にメモリ上のアカウントの uuid フィールドへ書き込むのは、ストアスキャン(accounts リストが空の場合)だけです — credentials / token_env で設定されたアカウントの id はリクエストごとに解決され、uuid へフィードバックされることはありません。ストアのスキャン自体はストアディレクトリの mtime でキャッシュされるため、定常状態のディスカバリーはディレクトリの stat 1 回だけで済み、認証情報ファイルの読み取りは発生しません。選択されたアカウントの実際の認証情報の解決では、期限の確認/リフレッシュに必要な範囲で、引き続きリクエストごとにその認証情報ファイルを読み取ります。
選択とクールダウン
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と priority / 余裕(headroom)による順序付けが適用されます。x-codex-rate-limit-reached-typeの値は表示用にのみ記録されます。 - ハードキャップ(アカウントの
max_utilization*、[server.pool]のdefault_max_utilization*)は、5 時間または週次の使用率がキャップ以上の間、ソフトしきい値のように並べ替えるだけでなくアカウントを選択から除外します。Codex には7d_oiウィンドウがないため、Fable 用のキャップのキーはここでは無効です。キャップにより選択可能なアカウントがなくなると、shunt はゲートウェイが生成する429(インバウンドの Codex エンドポイントでは OpenAI のエラー形式)を返し、これは[[upstreams]]のフェイルオーバーチェーンを進めます。すでにコミット済み(HTTP200送信済み)のストリーミングリクエストは、後続のアップストリームが処理できない場合、HTTP429やretry-afterではなく、ストリーム内の終端 SSEerrorイベントを受け取ります。ハードキャップを参照してください。 [server.pool] ramp_initial_concurrencyを設定すると、スロースタートの受け入れゲートが、フェイルオーバー直後に選択されたばかりのアカウントへ殺到が起きるのを防ぎます。[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 の各ウィンドウで使用量観測と status 観測の新しい方を取り、4 つ目に独立した aggregate status 観測を使って判定します。使用量だけのポーリングは使用量の鮮度だけを更新し、各ウィンドウの status 鮮度は更新しません。- 成功レスポンスは、そのアカウントのクールダウンを解除します。
| トリガー | クールダウン |
|---|---|
| 認証情報の解決失敗(アカウント id / トークンが解決不能) | 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 ウィンドウが少なくとも 1 つあり、あるバケットに候補ウィンドウがない場合、そのバケットの使用量と使用量の観測時刻だけをクリアし、レスポンス由来のリセットメタデータ(x-codex-* ヘッダーと WebSocket の codex.rate_limits イベント)とヘッダー由来のステータスメタデータは保持します。報告されたウィンドウでは、未来の保存済みリセットを保持し、経過した保存済みリセットだけを新しい使用率を書き込む前にクリアします。wham の reset_at は実際のリセットメタデータとして採用しません。null のウィンドウは不在ですが、認識できない期間の候補は不明としてクリアを抑止します。取得またはパースのエラーと、使用可能な認識済みウィンドウがない空のスナップショットは従来の状態を保持します。これは、ウィンドウを省略しても以前の値を保持する Claude の /api/oauth/usage 経路とは異なります。一方のウィンドウが不正な形状であっても、もう一方のウィンドウの適用を妨げません。ここでのパース失敗がアカウントを不健全とマークしたり、リクエストのルーティングに影響したりすることはなく、レスポンス由来の状態がフォールバックとして残るだけです。
対象条件は Claude 側と同じです:imported ログイン(ストア管理アカウント、または tokens.refresh_token を持つインライン credentials パス)のみがポーリング対象になります — 生の token_env ベアラーは静的であるためスキップされます。
フェイルオーバーのルール
| レスポンス | 挙動 |
|---|---|
| 2xx | 中継し、健全としてマークします。 |
| 429 | 常にクールダウンしてローテーションします。オプションの Codex 拒否シグナルは表示用に記録されますが、フェイルオーバーの分類は変えません。したがって、すべての 429 はそのアカウントの枯渇として扱われます。 |
credentials / ストアでの 401 |
強制リフレッシュして同じアカウントを 1 回リトライします。リフレッシュに失敗、またはリトライしてもまだ 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 のときにのみ適用されます。プールが少なくとも 1 つの上流レスポンスを受け取った後に試行を使い切った場合、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 の identity ヘッダーを、そのアカウントの認証情報から埋めて送信します。
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 を参照)、プールされたコネクションキャッシュのキーはアカウントごとにプレフィックスされるため、2 つのアカウントがソケットや previous_response_id の継続状態を共有することはありません。トークンがまだ 1 つもストリーミングされていない時点での WebSocket の失敗は、同じアカウントで HTTP へフォールバックします(プールのローテーションではありません)。フェイルオーバーが起きるのは HTTP 経路での失敗だけです。新規の WebSocket アップグレードは、そのハンドシェイクから得られるレート制限ヘッダーを記録します。再利用/プリウォームされたコネクションは再度ハンドシェイクしないためヘッダーのシグナルを持ちませんが、バックエンドは同じウィンドウをストリーム内の codex.rate_limits イベントとしても報告し、shunt が毎ターンそれを観測するため、ダッシュボードの使用量は毎ターン更新されます。
セキュリティ上の制約
chatgpt_oauth は次の場合にのみ受け入れられます。
base_urlが HTTPS を使っている。- ホストが
chatgpt.com、またはそのサブドメインである。
xai_oauth と同様に、chatgpt_oauth は kind = "responses" を必要とします — これは openai や xai と共有される Codex バックエンドの kind です。これはベアラー漏洩のガードです。Codex のベアラーを注入するのは Responses アダプターだけなので、kind = "anthropic" の不一致なプロバイダーは Anthropic アダプターへルーティングされ、代わりにクライアント自身の認証情報をオリジン外の chatgpt.com へ転送してしまいます。これらの起動時チェックは、OAuth ベアラーがオリジン外へ、あるいは平文で送られるのを防ぎます。HTTPS とホストのチェックはループバックホストでは緩和されます(localhost、127.0.0.1、[::1] など)。ループバックの base_url は平文 HTTP と任意のホストを使えるため、ローカルのデバッグプロキシやモックがトラフィックを受け取れます。非ループバックのホストには常に HTTPS + chatgpt.com が求められます。共有デプロイでは、chatgpt_oauth がゲートウェイ所有の認証情報を消費するため、[server.auth] も設定してください。
影響を受けないもの: 単一アカウント経路
accounts が設定されていない chatgpt_oauth プロバイダー(デフォルトの codex プロバイダー)は、これまでどおりに動作します。~/.codex/auth.json(または $CODEX_AUTH_FILE)を直接読み取ってリフレッシュし、クールダウンも x-shunt-account ヘッダーもありません。
残りのフォローアップ
- admin web でのプロビジョニング: オプトインの管理サーフェスは、ブラウザーで ChatGPT OAuth を実行し、リフレッシュ可能な Codex アカウントを保存し、共有プールのテーブルに表示し、報告された 5h/7d の使用量を表示できます(Codex には相当するものがないため
7d_oiは空のままです)。
アカウントプールの内部仕様の全体は M10 の挙動仕様を、単一アカウントのセットアップ、モデルルーティング、エフォート/コンテキストの設定については ChatGPT / Codex ガイドを参照してください。