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를 보내면 요청은 세션에 고정(session-sticky)되고, 이 헤더가 없는 요청은 프로바이더별 라운드 로빈을 사용합니다. 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"

# 두 번째 Codex CLI 로그인을 다른 이름으로 가져온 것.
[[providers.codex.accounts]]
name = "primary"
credentials = "~/.shunt/accounts/codex/primary.json"

# 별도 경로로 공급된 원시 ChatGPT 액세스 토큰. 그대로 사용되며 갱신되지 않습니다.
[[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는 만료가 가까워지면 갱신하고, ~/.codex/auth.json 자체와 동일하게 갱신된 토큰을 원자적으로 다시 써 넣습니다.
token_env 사용 가능한 소스 중 하나 원시 ChatGPT 액세스 토큰이 들어 있는 환경 변수. 값은 그대로 사용되며 401 이후 갱신할 수 없습니다.
uuid 아니요 별칭을 통합하는 데 쓰이는 안정적인 업스트림 아이덴티티. 이름만 있는 항목(스토어 스캔으로 해석됨)은 선택이 실행되기 전에 자동으로 채워지므로, 스토어에서 스캔된 별칭은 설정 변경 없이 통합됩니다. credentials 또는 token_env로 구성된 항목의 계정 id는 선택 이후에만 해석되며 절대 uuid를 채우지 않습니다. 따라서 그런 항목의 아이덴티티는 uuid가 설정되어 있으면 그 값, 없으면 name이며, 그 아이덴티티가 다른 별칭의 명시적 uuid 또는 이름 폴백 아이덴티티와 일치할 때마다 통합됩니다 — 명확하고 의도적인 통합을 위해서는 두 항목 모두에 일치하는 비어 있지 않은 uuid를 설정하세요(shunt는 우연한 uuid/name 교차 충돌에 대해서도 경고합니다). 이 값은 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. 갱신이 일어난 뒤에는 오직 새 액세스 토큰의 JWT claim에서만 id를 다시 계산합니다(갱신 응답에는 별도의 account-id 필드가 없습니다).

두 소스 중 어디에서도 id를 얻지 못하면 그 계정은 해석에 실패하며, 아래의 자격 증명 해석 실패로 취급됩니다. 해석된 이 id를 선택에 앞서 인메모리 계정의 uuid 필드에 기록하는 것은 스토어 스캔(accounts 목록이 비어 있는 경우)뿐입니다 — 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와 priority/여유 기준 정렬이 적용됩니다. 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이 전송된) 스트리밍 요청은 뒤의 업스트림이 처리하지 못하면 HTTP 429와 retry-after 없이 스트림 안의 종료 SSE error 이벤트를 받습니다. 하드 캡을 참고하세요.
  • [server.pool] ramp_initial_concurrency가 설정되어 있으면 슬로 스타트 admission 게이트가 페일오버 직후 방금 선택된 계정에 트래픽이 몰리는 것을 막습니다. [server.pool]을 참고하세요.
  • [server.pool]이 설정되어 있으면 오래된 근접 쿼터 계정을 reprobe_seconds(기본 900초, 0이면 비활성)당 한 번씩 선택 순서 맨 앞으로 승격하고 예약합니다. admission이나 자격 증명 확인에 실패하면 예약을 취소하고, 첫 HTTP 전송이 시작될 때 재탐침 시각과 shunt.pool.reprobes를 커밋합니다. [server.pool]을 참고하세요. WebSocket 전송을 켠 프로바이더의 outbound Responses 풀은 예약을 만들지 않고 스트림 내부 rate-limit 이벤트를 안전하게 회전시키지 못하므로 재탐침을 억제합니다. 선택형 inbound Codex HTTP 엔드포인트는 계속 탐침하므로 해당 프로바이더의 카운터는 inbound 탐침만 셉니다. Codex usage 폴러가 없으면 제외된 outbound 마크는 관측시각 기반 창 수명 경계에서 만료될 때까지 남습니다. 신선도는 각 논리 창 5h, 공유 7d, Fable 7d에서 사용률 관측 시각과 status 관측 시각 중 최신 값을 사용하며, 네 번째 값으로 독립된 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 창이 하나 이상 있으면 후보 창이 전혀 없는 버킷의 사용률과 사용률 관찰 시각을 지우지만 응답에서 온 리셋 메타데이터(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 강제 갱신 후 같은 계정을 한 번 재시도합니다. 갱신에 실패하거나 재시도도 401이면 5분 쿨다운 후 로테이션합니다.
token_env에서의 401 갱신 불가: 5분 쿨다운 후 로테이션합니다.
5xx 또는 전송 실패 30초 쿨다운 후 로테이션합니다.
자격 증명 해석 실패 5분 쿨다운 후 로테이션합니다.
detail, error.message, message 중 하나에 model is not supported가 들어 있는 400(대소문자 무시) 백엔드가 이 ChatGPT 계정에 그 모델을 아직 열어 주지 않은 것입니다(계정별 롤아웃 게이트로, 몇 시간 안에 바뀔 수 있음). 다른 계정은 그 모델을 처리할 수 있으므로 로테이션하고, 그 계정의 그 모델만 1시간 쿨다운합니다. 그 계정은 다른 모델을 계속 처리하며, 정상으로 표시되지 않고, 대시보드에 쿨다운으로 나타나지 않습니다. 모든 계정이 거부하면 거부 메시지가 그대로 담긴 400을 릴레이합니다. HTTP 전송에만 적용됩니다. WebSocket 전송(아래)에서는 거부가 스트림 안의 오류로 도착하므로 로테이션 없이 릴레이됩니다.
그 외 status(예: 다른 모든 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 형식의 오류 봉투를 릴레이합니다 — status(예: 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 참고), 풀링된 연결 캐시 키에 계정별 프리픽스가 붙으므로 두 계정이 소켓이나 그 previous_response_id 연속 상태를 공유하는 일은 없습니다. 토큰이 하나라도 스트리밍되기 전에 발생한 WebSocket 실패는 같은 계정에서 HTTP로 폴백합니다(풀 로테이션이 아닙니다). 위의 페일오버는 HTTP 경로의 실패에서만 발동합니다. 새 WebSocket 업그레이드는 그 핸드셰이크의 rate limit 헤더를 기록합니다. 재사용되거나 prewarm된 연결은 다시 핸드셰이크하지 않아 헤더 신호가 없지만, 백엔드가 같은 창을 스트림 안의 codex.rate_limits 이벤트로도 보고하고 shunt가 매 턴 이를 관측하므로 대시보드 사용량은 매 턴 갱신됩니다.

보안 제약

chatgpt_oauth는 다음 조건에서만 허용됩니다:

  • base_url이 HTTPS를 사용하고;
  • 호스트가 chatgpt.com이거나 그 서브도메인일 때.

xai_oauth와 마찬가지로 chatgpt_oauth에는 kind = "responses"가 필요합니다 — openai, xai와 공유하는 Codex 백엔드의 kind입니다. 이는 bearer 유출 방지 장치입니다: Codex bearer를 주입하는 것은 Responses 어댑터뿐이므로, kind = "anthropic"으로 잘못 지정된 프로바이더는 Anthropic 어댑터로 라우팅되어 클라이언트 자신의 자격 증명을 chatgpt.com이라는 다른 오리진으로 전달하게 됩니다. 이 시작 검사는 OAuth bearer가 다른 오리진으로, 또는 평문으로 전송되는 것을 막습니다. HTTPS와 호스트 검사는 루프백 호스트에서는 완화됩니다(localhost, 127.0.0.1, [::1] 등): 루프백 base_url은 평문 HTTP와 임의의 호스트를 쓸 수 있어 로컬 디버깅 프록시나 목(mock)이 트래픽을 받을 수 있습니다. 루프백이 아닌 호스트에는 항상 HTTPS + chatgpt.com이 요구됩니다. 공유 배포에서는 chatgpt_oauth가 게이트웨이 소유 자격 증명을 소비하므로 [server.auth]도 함께 구성하세요.

영향 없음: 단일 계정 경로

accounts가 구성되지 않은 chatgpt_oauth 프로바이더(기본 codex 프로바이더)는 이전과 정확히 동일하게 동작합니다: ~/.codex/auth.json(또는 $CODEX_AUTH_FILE)을 직접 읽고 갱신하며, 쿨다운도 x-shunt-account 헤더도 없습니다.

남은 후속 작업

  • 관리자 웹 프로비저닝: 옵트인 관리자 화면은 브라우저에서 ChatGPT OAuth를 실행해 갱신 가능한 Codex 계정을 저장하고, 공유 풀 테이블에 표시하며, 보고된 5h/7d 사용량을 보여줄 수 있습니다(Codex에는 대응물이 없어 7d_oi는 비어 있습니다).

계정 풀 내부 동작 전체는 M10 동작 명세를, 단일 계정 설정·모델 라우팅·effort/컨텍스트 구성은 ChatGPT / Codex 가이드를 참고하세요.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close