Skip to content

インバウンド Codex エンドポイント

OpenAI の Codex CLI 自身を shunt へ向け、ChatGPT/Codex OAuth アカウントプールで負荷分散する。

Updated View as Markdown

このサイトの他のガイドはすべて Claude Code を別のバックエンドへルーティングします。shunt は逆方向にも動けます。オプトインの生の OpenAI Responses パススルーによって、Codex CLI が自身の base_url を shunt へ向け、ChatGPT/Codex OAuth アカウントプールで負荷分散されるようにするものです。これはオプトインです。[server.codex_endpoint] がない場合、それらのルートはいずれも登録されず、shunt のデフォルトの HTTP サーフェスは変わりません。

これは Codex マルチアカウントと同じアカウントプールの上に構築されます — 選択、クールダウン、リフレッシュはそのまま共有されます。正確なフェイルオーバーの表とリロードのセマンティクスを含む完全な仕様は、M11 の挙動仕様を参照してください。

エンドツーエンドのセットアップ — エンドポイントの有効化、Codex CLI を shunt へ向ける、クライアント認証、アカウントのプロビジョニング、entitle されたモデルの選択 — については Codex CLI の接続に従ってください。このページはエンドポイントが何をするかに焦点を当てており、あちらのガイドがどう接続するかのチェックリストです。

エンドポイントを有効にする

[server.codex_endpoint]   # all keys optional; default shown
provider = "codex"        # must be a chatgpt_oauth provider
shunt check
shunt run

起動時の検証は、未知の provider や auth = "chatgpt_oauth" を使わないプロバイダーを拒否します — このエンドポイントはオペレーターの Codex ベアラーを注入するため、chatgpt_oauth プロバイダーだけが要件を満たします。すべてのキーとデフォルトは設定リファレンスを、登録されるルートは HTTP エンドポイントを参照してください。

このオプトインにより、Codex CLI のモデル検出も解析可能になります。GET /models と GET /backend-api/codex/models は有効なフォールバック {"models":[]} を返します。共有の GET /v1/models パスでは、client_version クエリフィールドが Anthropic 風のヘッダーより優先され、Codex 形式を選択します。このフィールドがなければ、既存の Anthropic 検出レスポンスは変わりません。これらのリクエストは通常のモデル検出認証ゲートを通り、shunt は不完全な Codex モデル行を生成しません。

Web 検索

Codex CLI の組み込み Web 検索ツール(web.run)は、/responses を付けるのと同じ base URL の {base_url}/alpha/search へ POST します。shunt は base URL の各形式についてこのパスを登録します。

  • POST /backend-api/codex/alpha/search
  • POST /alpha/search
  • POST /v1/alpha/search

検索は Responses のターンと同じ [server.auth] ポリシーと同じ ChatGPT/Codex アカウントプール(セッション固定の選択、フェイルオーバー、リフレッシュ)を使い、リクエストボディとレスポンスをバックエンドの /codex/alpha/search へそのまま中継します。検索はモデルのターンではないため [[server.codex_endpoint.routes]] は適用されず、常に [server.codex_endpoint].provider へ送られます。これらのルートは POST のみを受け付けます。このツールには WebSocket 形式がありません。

クライアント analytics のシンク

Codex CLI は base URL へプロダクト analytics も POST します。shunt は CLI が生成しうる両方のパスを受け付けます。

  • POST /backend-api/codex/analytics-events/events
  • POST /codex/analytics-events/events

これらのルートは Responses ルートと同じ [server.auth] ポリシーを使いますが、テレメトリーを上流へ転送することは決してありません。プールされたアカウントを 1 つ選ぶと、クライアントのイベントがそのアカウントへ誤って帰属されてしまうためです。認証後は、不正な形式・読み取り不能・サイズ超過のボディも含めて、常に 200 {} を返します。

payload とイベントのプロパティは、ログにも記録されずエクスポートもされません。shunt が記録するのは、オプトインの shunt.codex_client_events カウンターの event 属性としてサニタイズ済みの event_type だけです。名前に使えるのは小文字の ASCII 英字、数字、.、_、- で、最大 64 バイトです。不正な名前は other に、認識できないバッチは unparsed になります。Sentry も OpenTelemetry のメトリクスも有効でなければ、これは純粋な破棄シンクです。

Codex CLI を shunt へ向ける

Codex CLI は、使用する base URL が何であれ常に /responses を末尾に付けるため、~/.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"

カスタムプロバイダーを使う場合(CLI がローカルログインを必要としないよう requires_openai_auth = false を追加してください)、shunt へ向けた時点で Codex CLI 自身の ~/.codex/auth.json は無関係になります — アカウントはリクエストごとに shunt のプールから来ます。一方 chatgpt_base_url の形状は CLI を ChatGPT ログインモードのままにするため、引き続きローカルのログインが必要で、ゲートされていないエンドポイントに対してのみ動作します。その ChatGPT ベアラーは設定された shunt トークンではないため、[server.auth] はそれを拒否します。

クライアント認証

shunt に [server.auth] が設定されている場合 — ループバックを超えるものには推奨です — クライアントトークンを、OpenAI 形式の Bearer キー(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 ジャーを持たないため、上流がそれに依存することはありません。x-api-key も無条件に取り除かれます — [server.auth] が設定されていない場合も同様です。対象のプロバイダーは起動時に chatgpt_oauth 専用であることが検証されるため、インバウンドの x-api-key の値がこのアップストリームに対して有効な認証情報になることは決してありません。Claude Code の apiKeyHelper のように Authorization と x-api-key の両方に同じキーを設定するクライアントであっても、2 つ目のスロット経由でそのキーが漏れることはありません。インバウンドのクライアントが実際の Codex CLI であるため、パススルーはそのリクエストヘッダーをそのまま転送し(version、originator、OpenAI-Beta、x-codex-*、…)、差し替えるのは選択されたプールアカウントの Authorization ベアラーと chatgpt-account-id だけです。認証の詳しい手順は Codex CLI の接続を参照してください。

WebSocket トランスポート

3 つの Responses パスは HTTP POST と認証済み WebSocket GET アップグレードの両方を受け付けます。認証は 101 Switching Protocols より前に完了します。ソケットでは generate: false のウォームアップをローカルで完了し、通常の response.create は既存の HTTP アカウントプールを再利用して上流ストリーミングを強制し、最初の終端イベントまで各 SSE data: ペイロードを WebSocket テキストフレームとして転送します。ターンの置換またはソケット切断はアクティブな上流ボディをキャンセルします。クライアントフレームと SSE イベントは 4 MiB に制限され、送信には有界バックプレッシャーが適用され、エラーフレームには安全なレスポンスメタデータだけが含まれます。

[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 認証情報 1 つへフォールバックします — プーリングもフェイルオーバーもありません。そのため [server.codex_endpoint] を設定した時点で、Codex ログイン 1 つで動作します。(ハンドラーはまずアカウントストアをスキャンし、見つかったアカウントをプールするため、インポート済みのストアアカウントがあればプーリングは有効になります。)

モデルを別のアップストリームへルーティングする

既定ではすべてのリクエストが [server.codex_endpoint] に指定した 1 つのプロバイダーへ送られます。任意の [[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" のプロバイダーが必要です — Anthropic 種別のプリセットへ Codex モデルをルーティングすると起動時に拒否されます。

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 が解決された場合に新しく生成する 4 つのセッションアフィニティヘッダー(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 は転送されません。
  • 資格情報は 1 つ、フェイルオーバーなし。 ルーティング先のサードパーティの背後にフェイルオーバー用のプールはないため、429 や 5xx はローテーションを起こさず retry-after とともにそのままリレーされます。プールを考慮する唯一の例外は名前付きアカウントを持つ xai_oauth ルートです。このルートもリクエストを 1 回だけ送り(アップストリームの失敗はローテーションせずリレー)、資格情報はプール選択順でログインを読み込める最初のアカウントになります(プロバイダーの 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(英語)を参照してください。

  • チェックの時点。 すべてのターンは、アップストリームを呼ぶ前にチェックされます。3 つの 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> ヘッダーが付きます。promo ヘッダーは blocked_message が純粋な ASCII の場合にのみ送られ、テキストは常にメッセージにも含まれます。名前が挙がる上限は、到達した上限のうち最後にリセットされるものです。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 には、同じエラーと retry-after、x-codex-promo-message を載せた {"type":"error","status":429,"error":{…},"headers":{…}} フレームが 1 つ返ります。ソケットは開いたままで、以降の各ターンは改めてチェックされ、generate: false のウォームアップはチェックされません。
  • Web 検索。 /alpha/search とそのエイリアスは、上限を超えていると拒否され、計測されません。
  • 計測。 完了したターンは Responses の usage から課金され、/v1/messages が書き込むのと同じカウンターに加算されるため、1 つの上限が両方のサーフェスを制限します。キャッシュされたトークンはキャッシュ読み取りレート、残りの入力は入力レートで課金され、推論トークンは出力に含まれ、キャッシュ書き込みはありません。使用量が報告されずに終わったターン(クライアントの切断、置き換えられたターン、途切れたストリーム)は、リクエスト本文から推定した入力に配信済みの出力を加えた下限で課金されます。ストリーミングでない 2xx の JSON 本文も、同様に usage から課金されます。入力が報告されていない本文は、代わりに推定した入力で課金されます。
  • GPT の価格。 組み込みの料金表には GPT/Codex の価格がないため、これらのモデルは行を追加するまで不明モデルのレート(100 万トークンあたり $5 / $25 / $0.50 / $6.25、モデルごとに警告 1 回)で課金されます。
[[server.spend.pricing.overrides]]
upstream = "codex"       # エンドポイントの provider
model = "gpt-6.1-sol"
input = 1.00             # 例のレート、100 万トークンあたり USD
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 を読み取るためだけに、メモリ上でコピーをデコードします。shunt がデコードできないボディでも中継自体は問題なく行われ、劣化するのは model ラベルが unknown になることだけで、理由を示す警告が出ます。
  • モデルに基づくルーティングはオプトイン。 既定ではすべてのリクエストが [server.codex_endpoint] で指定された 1 つのプロバイダーへ行き、ボディの model フィールドはそのまま転送されます。[[server.codex_endpoint.routes]] を設定すると、完全一致する model がそのエントリのプロバイダーを選びます — モデルを別のアップストリームへルーティングするを参照。
  • 枯渇時はそのまま中継。 プールされたすべてのアカウントを試行し、少なくとも 1 つの上流レスポンスが返っていた場合、shunt はその最後のレスポンスを Anthropic 形式のエラーへ作り直すのではなく、変更せずに中継します。Responses のクライアントは、実際の ChatGPT バックエンドから受け取るはずの生の形を期待するためです。
  • ゲートウェイ自身のエラーは OpenAI 形式。 失敗が shunt 自身のものである場合 — 不正または欠落したクライアントトークン(401)、上流レスポンスのないプールの解決不能(502)、サイズ超過のリクエストボディ、未設定のエンドポイント — shunt は同じステータスコードのまま、OpenAI Responses のエラー形({"error":{"message":…,"type":…,"code":null}})で返します。これにより Codex CLI は、Anthropic の {"type":"error",…} エンベロープではなく自身のエラー経路でパースできます。中継される上流のエラー(バックエンドからの 429/4xx/5xx)は、引き続きそのまま通過します。
  • 2 つの受信トランスポート。 HTTP POST はバイト忠実性を維持し、WebSocket GET は有界イベント配信を追加します。これはプロバイダーの outbound websocket = true 設定には依存しません。

セキュリティ

  • ループバックを超えるものでは、このエンドポイントを [server.auth] でゲートしてください — プロバイダーはリクエストごとに実際の Codex ベアラーを注入します。
  • クライアント自身の認証情報が Codex バックエンドへ届くことはありません。パススルーは Codex CLI 自身のリクエストヘッダーをそのまま転送し、差し替えるのは選択されたプールアカウントのベアラーと chatgpt-account-id だけです(shunt のクライアントトークンヘッダー、[server.admin] の認証情報ヘッダー、cookie ヘッダー全体、内部用の x-shunt-inbound-client ラベル、クライアントの Authorization/chatgpt-account-id、そして x-api-key はすべて取り除かれ、転送されることはありません)。
  • 起動時に一度だけ決まるのは、エンドポイントの HTTP ルート登録だけです。[server.codex_endpoint] の実行時のオン/オフ切り替えは、それらのパスを追加・削除するには再起動が必要である旨の警告をログに出力します。テーブルが保持している内容はすべてホットリロードされます — 対象の provider と [[server.codex_endpoint.routes]] のモデルテーブル全体はリクエストごとにライブ設定から読まれるため、ルートの追加・編集・削除はリロードで反映されます。
Navigation

Type to search…

↑↓ navigate↵ selectEsc close