Skip to content

Anthropic 멀티 계정

여러 Claude 구독 OAuth 계정을 하나의 풀로 묶어 세션 스티키·모델 인지 선제 로테이션과 반응형 페일오버로 운용합니다.

Updated View as Markdown

shunt는 내장 anthropic 프로바이더 뒤에서 여러 Claude 구독 OAuth 자격 증명을 풀로 묶을 수 있습니다. Claude Code가 x-claude-code-session-id를 보내면 요청은 세션에 고정(session-sticky)되고, 이 헤더가 없는 요청은 프로바이더별 라운드 로빈을 사용합니다. shunt는 각 계정의 업스트림 쿼터 헤더를 추적해 스티키 계정이 모델 관련 쿼터에 근접하면 선제적으로 로테이션하며, 쿼터 거부·인증 실패·업스트림 장애에 대해서는 반응형 페일오버가 안전판으로 유지됩니다.

풀 구성

auth = "claude_oauth"를 설정하고 명시적인 계정 항목을 추가합니다:

[providers.anthropic]
kind = "anthropic"
base_url = "https://api.anthropic.com"
auth = "claude_oauth"

# 기존 Claude Code 자격 증명 파일. shunt가 갱신하고 다시 써 넣습니다.
[[providers.anthropic.accounts]]
name = "primary"
credentials = "~/.claude/.credentials.json"
uuid = "00000000-0000-0000-0000-000000000000" # 선택

# 장기(long-lived) `claude setup-token` 값. 그대로 사용되며 갱신되지 않습니다.
[[providers.anthropic.accounts]]
name = "backup"
token_env = "CLAUDE_BACKUP_OAUTH_TOKEN"
uuid = "11111111-1111-1111-1111-111111111111" # 선택
export CLAUDE_BACKUP_OAUTH_TOKEN='<value from claude setup-token>'
shunt check
shunt run

세 가지 Claude 로그인 모드 중 하나로 계정을 저장할 수 있습니다:

# 새 갱신 가능 로그인 생성(기본은 자동 localhost callback).
shunt login claude --name primary --mode oauth

# 현재의 갱신 가능한 Claude Code 로그인 가져오기.
shunt login claude --name imported --mode import

# 1년짜리 추론 전용 setup token 생성 및 저장.
shunt login claude --name backup --mode setup-token

TTY에서 --mode를 생략하면 OAuth가 기본 선택된 3-way 프롬프트가 열립니다. 비대화형 입력에서는 기존 import 기본값을 유지합니다. --long-lived는 --mode setup-token의 deprecated alias입니다. Full OAuth는 보통 일회성 127.0.0.1 callback으로 완료됩니다. <code>#<state>를 붙여 넣으려면 --manual을 사용하세요. 브라우저 실행, callback bind, 또는 5분 대기가 실패해도 shunt가 수동 붙여넣기로 fallback합니다.

그다음 이름만 있는 항목을 사용합니다:

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

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

스토어 파일은 ~/.shunt/accounts/claude/<name>.json에 저장되며, SHUNT_CLAUDE_ACCOUNTS_DIR로 디렉터리를 재정의할 수 있습니다. 구성된 accounts 목록이 비어 있으면 shunt는 스토어를 스캔해 유효한 JSON 계정 파일 전부를 파일명 순서로 사용합니다. 스토어 파일은 비공개입니다(Unix에서 0600, 디렉터리는 0700).

원격 운영자는 옵트인 관리자 웹 화면에서 브라우저로 갱신 가능한 Full OAuth 계정 또는 1년짜리 setup token 계정을 프로비저닝하고 풀의 현재 상태를 볼 수 있습니다. 기존 credential 파일 가져오기는 CLI 전용입니다.

Full OAuth는 새로운 갱신 가능 credential을 만듭니다. import는 현재의 ~/.claude/.credentials.json credential을 shunt 스토어로 복사합니다. 두 방식 모두 갱신 기능을 보존하며, import는 현재 계정 UUID도 기록합니다. setup-token 모드는 claude setup-token과 동일한 1년짜리 추론 전용 PKCE 플로우를 실행합니다. 승인 후 shunt는 표시된 인가 코드를 교환하고 토큰과 발급 계정 UUID를 함께 저장하며 토큰을 출력하지 않습니다. 이렇게 하면 풀이 다른 계정을 선택할 때도 metadata.user_id.account_uuid가 일치된 상태로 유지됩니다. 이름을 재사용하면 해당 계정의 스토어 파일이 교체됩니다. 기존 외부 setup token에는 여전히 token_env와 명시적 uuid가 필요합니다.

계정 필드

필드 필수 의미
name 예 소문자, 숫자, 하이픈만 포함하는 고유 레이블. 다른 소스 필드가 없으면 이름이 일치하는 shunt 스토어 파일을 사용합니다.
credentials 사용 가능한 소스 중 하나 Claude Code .credentials.json 형태의 파일. ~/가 확장됩니다. shunt는 만료가 가까워지면 갱신하고 갱신된 토큰을 원자적으로 다시 써 넣습니다.
token_env 사용 가능한 소스 중 하나 setup token이 들어 있는 환경 변수. 값은 그대로 사용되며 401 이후 갱신할 수 없습니다.
uuid 아니요 기존 metadata.user_id.account_uuid를 다시 쓰기 위한, 선택된 계정의 Anthropic UUID이자, 풀에서 별칭을 통합하는 데 쓰이는 안정적인 아이덴티티. 이름만 있는 항목(스토어 스캔으로 해석됨)은 선택이 일어나기 전에 스토어의 shuntAccountUuid로부터 자동으로 채워집니다. credentials 또는 token_env로 설정된 항목의 아이덴티티는 uuid가 설정되어 있으면 그 값, 없으면 name이며, 그 아이덴티티가 다른 별칭의 명시적 uuid 또는 이름 폴백 아이덴티티와 일치할 때마다 통합됩니다. 명확하고 의도적인 통합을 위해서는 두 항목 모두에 일치하는 비어 있지 않은 uuid를 설정하세요(shunt는 명시적 uuid가 다른 계정의 이름 폴백 아이덴티티와 우연히 일치하는 경우도 경고합니다).
threshold 아니요 창(window)별 값이 없는 모든 창에 적용되는 계정별 소프트 쿼터 임계값, [0.0, 1.0] 범위. 낮은 값은 일찍 로테이션되는 백업 계정을 나타냅니다.
threshold_5h / threshold_7d / threshold_fable 아니요 창별 소프트 임계값; 각각 해당 창에서 threshold보다 우선합니다.
max_utilization 아니요 창별 값이 없는 모든 창에 적용되는 계정별 하드 캡, [0.0, 1.0] 범위. 적용되는 창에서 사용률이 이 값 이상인 계정은 선택에서 제외됩니다(하드 캡 참고). [server.pool] 없이도 동작합니다.
max_utilization_5h / max_utilization_7d / max_utilization_fable 아니요 창별 하드 캡; 각각 해당 창에서 max_utilization보다 우선합니다. Fable 캡은 Fable 모델 요청에만 적용됩니다.
priority 아니요 스티키 계정이 비정상일 때의 선택 우선순위; 낮을수록 우선되며 기본값은 100입니다.
disabled 아니요 true이면 구성과 관리자 대시보드에는 남긴 채 계정을 선택 대상에서 완전히 제외합니다.

한 계정에 credentials와 token_env를 동시에 설정하지 마세요.

선택과 선제 로테이션

  • x-claude-code-session-id가 있으면: 안정적인 해시가 스티키 계정을 고릅니다. 그 계정이 사용 가능하고 전환 임계값 아래라면 shunt는 그 계정을 첫 번째로 유지합니다.
  • 헤더가 없거나 빈 값으로 오면: 프로바이더마다 자체 라운드 로빈 카운터를 사용합니다. 빈 값은 하나의 대화가 아니므로, 고정된 계정 하나로 해시되지 않고 이 로테이션을 함께 씁니다.
  • claude_oauth 계정 풀이 처리하는 모든 업스트림 응답에서, shunt는 다음 헤더가 있으면 기록합니다:
    • anthropic-ratelimit-unified-5h-utilization, anthropic-ratelimit-unified-7d-utilization, anthropic-ratelimit-unified-7d_oi-utilization;
    • anthropic-ratelimit-unified-5h-reset, anthropic-ratelimit-unified-7d-reset, anthropic-ratelimit-unified-7d_oi-reset(Unix 초); 그리고
    • anthropic-ratelimit-unified-status, anthropic-ratelimit-unified-5h-status, anthropic-ratelimit-unified-7d-status, anthropic-ratelimit-unified-7d_oi-status.
  • 기본 전환 임계값은 0.98입니다. 5시간 또는 공유 7d status가 rejected이면 모든 모델에서 그 계정은 쿼터에 근접한 것으로 간주합니다. 7d_oi status가 rejected이면 Fable 요청에서 쿼터에 근접한 것으로 간주합니다. 공유 5시간 사용률이 임계값에 도달하거나 적용되는 주간 사용률이 임계값에 도달할 때도 쿼터에 근접한 것으로 간주합니다. status 거부 여부는 대응하는 사용률 헤더의 유무와 무관하게 평가합니다. 사용률 임계값과 여유를 계산할 때 Fable 모델은 7d_oi 사용률이 있으면 이를 사용하고, 없으면 공유 7d를 사용합니다. 집계 status는 창별 status가 하나도 기록되지 않았을 때만 쓰이는 호환용 폴백입니다. 임계값은 계정별로(위의 threshold* 필드) 또는 풀 전체로(자세한 내용은 선택 튜닝 참고) 낮출 수 있습니다.
  • 5시간 버킷은 모든 모델에 적용됩니다. Fable 모델 id는 7d_oi 주간 버킷의 사용률이 있으면 그것을 쓰고, 없으면 공유 7d로 폴백합니다. 그 외 모든 모델 계열은 공유 7d를 사용합니다; 현재 Sonnet 전용 헤더가 없으므로 Sonnet도 7d를 사용합니다.
  • 쿼터에 근접했거나 쿨다운됐거나 비활성화(disabled)된 스티키 계정은 선제적으로 로테이션됩니다. shunt는 임계값 아래의 사용 가능한 계정을 priority(낮은 값 우선) 순으로 먼저 선호하고, 그다음 적용되는 주간 버킷이 가장 빨리 리셋되는 순서로 선호해 쓰지 않으면 사라지는(use-or-lose) 쿼터부터 소진합니다. 주간 리셋을 알 수 없는 계정이 먼저 정렬됩니다. 그다음 사용 가능한 쿼터 근접 계정, 그다음 가장 빨리 회복되는 순서의 쿨다운 계정이 이어집니다. [server.pool]이 구성되어 있으면 번-레이트(burn-rate) 여유가 주간 리셋 타이브레이크를 대신합니다(아래 참고).
  • 로컬 쿼터 및 쿨다운 상태 때문에 시도 순서가 비는 일은 없습니다: disabled가 아니고 일시 정지되지 않은 모든 계정은 쿼터에 근접했거나 쿨다운 중이어도 시도 순서에 남아 있습니다. 유일한 예외는 하드 캡(max_utilization*, 하드 캡 참고)으로, 사용률이 캡 이상인 동안 계정을 제외합니다.
  • 사용률은 자체 관측 시각의 상한과 해당 창의 리셋 중 더 이른 시각에 만료됩니다. 자체 상한만 지나고 해당 창의 리셋이 미래라면 해당 창의 사용률과 그 시각만 지우고 그 미래 리셋은 독립 메타데이터로 남깁니다. 창별 status는 자체 관측 시각의 상한과 status를 관측할 때 캡처한 리셋 경계 중 더 이른 시각에 만료되며, status가 지워질 때 캡처한 경계도 함께 지워집니다. 이후 사용량 갱신이나 reset-only 갱신은 캡처한 status 경계를 옮기지 않습니다. 해당 창의 리셋이 지나면 해당 창의 사용률과 리셋이 지워지고, 캡처한 status 경계도 지났을 때 status가 지워집니다. 시각이 찍힌 집계 status는 자체 수명으로 관리되며 무관한 창이 만료되어도 지워지지 않습니다. legacy unstamped aggregate만 창 만료를 따릅니다. non-Fable 성공 응답은 계정 전체 쿨다운만 해제하고 Fable 전용 쿨다운은 그대로 둡니다; Fable 성공 응답은 두 쿨다운을 모두 해제합니다.

풀의 선택, 쿨다운, 쿼터 상태는 프로세스가 살아 있는 동안 설정 핫 리로드를 거쳐도 유지됩니다. 선제 로테이션으로 업스트림 제한을 피하지 못하는 경우에는 반응형 페일오버가 계속 동작합니다.

선택 튜닝 ([server.pool])

선택적 [server.pool] 테이블(이슈 #135)은 위 동작 위에 창(window)별 소프트 임계값과 번-레이트(burn-rate) 인지 정렬을 추가합니다. 이 테이블이 없으면 선택은 이전과 동일하게 단일 내장 0.98 임계값을 사용합니다.

[server.pool]
# hard_threshold = 0.98      # (기본값) 백스톱; 이 값 이상이면 항상 마지막으로 정렬됨
default_threshold = 0.9      # 모든 창에 대한 소프트 기본값
default_threshold_5h = 0.95  # 창별 오버라이드
default_threshold_fable = 0.85
burn_rate_avoidance = true   # 리셋 전에 임계값에 도달할 것으로 예측되는 계정을 회피
usage_refresh_seconds = 300  # 갱신 가능 계정의 out-of-band 사용량 재보정
state_path = "shunt-state.json"  # 재시작 간 쿼터 유지(워밍업)
ramp_initial_concurrency = 2 # 폭주 제어: 방금 전환된 계정을 슬로 스타트

[[providers.anthropic.accounts]]
name = "primary"
priority = 1                 # 스티키 계정이 비정상일 때 우선 선택됨

[[providers.anthropic.accounts]]
name = "backup"
threshold = 0.5              # 백업: 쿼터의 절반이 소진되면 로테이션

[[providers.anthropic.accounts]]
name = "spare"
disabled = true              # 구성은 유지되지만 절대 선택되지 않음
  • 임계값 해석(Threshold resolution). 각 창 X(5h, 7d, fable)에 대해 유효 소프트 임계값은 계정 threshold_X → 계정 threshold → default_threshold_X → default_threshold → hard_threshold 순으로 결정되며, hard_threshold로 상한이 걸립니다. 모든 값은 [0.0, 1.0] 범위의 사용률 비율이며, 범위를 벗어나면 shunt check가 실패합니다.
  • 번-레이트 여유(Burn-rate headroom). 각 창의 사용률과 리셋 시점(창 길이는 5시간과 7일로 고정)으로부터, shunt는 관측된 평균 속도로 소프트 임계값에 도달할 때까지의 시간에서 창이 리셋될 때까지의 시간을 뺀 값을 예측합니다. 여유(headroom)가 양수이면 현재 속도로도 리셋 시점까지 버틸 수 있다는 뜻입니다. priority가 같은 사용 가능한 계정은 여유가 큰 순서로 정렬되며, 관측되지 않은 창은 무제한 여유로 간주됩니다.
  • 예측적 회피(Predictive avoidance). burn_rate_avoidance = true이면, 예측된 여유가 음수인 계정은 임계값에 도달하기 전에 쿼터 근접 상태로 간주되어 로테이션됩니다. 기본값은 꺼짐이며 — 여유 기준 정렬 자체는 이 설정과 무관하게 항상 이루어집니다.
  • 전체 근접 가드(All-near guard). 모든 계정이 소프트 임계값을 넘겼거나(또는 소진이 예측되면), 풀이 비지 않습니다: 근접 계정은 여유가 가장 큰 순서로 서빙되고, hard_threshold 이상인 계정은 여전히 마지막으로 정렬되며, 그다음 쿨다운 중인 계정만 이어집니다.
  • 적용 범위(Scope). 쿼터 관련 노브는 두 풀 계열 모두에 작동합니다: 이 풀은 anthropic-ratelimit-unified-* 헤더로부터, Codex 풀은 보고된 x-codex-* 5h/7d 윈도우로부터 동작합니다(이슈 #195). Codex에는 Fable 범위의 7d_oi 창이 없어 default_threshold_fable은 그곳에서 무력화되며, priority와 disabled는 어디서나 적용됩니다.
  • 관리자 풀 엔드포인트(GET /admin/api/pool)는 각 계정의 priority, disabled 플래그를 보고하며, [server.pool]이 구성되어 있으면 현재 여유(headroom) 예측치를 초 단위로 함께 보고합니다; 대시보드의 상태 열은 비활성화된 계정을 표시합니다.

하드 캡 (max_utilization)

소프트 임계값은 계정을 재정렬만 합니다. 하드 캡은 사용률이 캡 이상인 동안 계정을 선택에서 제거합니다.

[server.pool]
default_max_utilization = 0.95     # 재정의하지 않는 한 모든 창
default_max_utilization_5h = 0.9   # 창별 기본값

[[providers.anthropic.accounts]]
name = "primary"
max_utilization_fable = 0.5        # Fable은 이 계정의 Fable 주간 버킷 약 50%에서 멈추고, Opus는 계속 사용
  • 키. 계정별: max_utilization, max_utilization_5h, max_utilization_7d, max_utilization_fable. [server.pool]에서: default_max_utilization, default_max_utilization_5h, default_max_utilization_7d, default_max_utilization_fable. 값은 [0.0, 1.0] 범위여야 하며, 그 밖의 값은 시작과 shunt check를 실패시킵니다.
  • 결정 순서. 각 창에 대해: 계정 max_utilization_X → 계정 max_utilization → default_max_utilization_X → default_max_utilization → 캡 없음. 캡은 hard_threshold로 제한되지 않습니다. 계정 캡은 [server.pool] 없이도 동작하며, 스토어 스캔으로 찾은 계정([[providers.anthropic.accounts]] 항목이 없는 계정)은 풀 기본값만 받습니다.
  • 제외이지 재정렬이 아님. 적용되는 창에서 관측된 사용률이 캡 이상인 계정은 선택에서 제외됩니다. 5h와 7d 캡은 모든 요청에 적용되고, Fable 캡은 Fable 모델 요청에만, 그리고 해당 계정의 7d_oi 사용률이 관측된 뒤에만 적용됩니다. 사용률이 관측되지 않은 창은 제외하지 않습니다.
  • 복구. 사용률이 캡 아래로 내려가면 계정은 자동으로 다시 선택 대상이 됩니다. 창 리셋 또는 만료, 더 낮은 관측값, 구성 리로드 시 캡 상향이 그 경로입니다. 캡은 새 라우팅만 막으며 정밀한 예산이 아니고 디스패치된 요청을 위해 사용량을 미리 확보하지도 않으므로, 진행 중이거나 동시에 들어온 요청은 소비하는 만큼 캡을 초과할 수 있습니다.
  • 소진. 캡 때문에 선택 가능한 계정이 풀에 남지 않으면 shunt는 게이트웨이가 만든 HTTP 429 rate_limit_error(“all selectable accounts for provider ‘X’ are at their max_utilization cap for this request”)를 반환하며, 가장 이른 재선택 가능 시각을 알면 retry-after를 붙입니다. 업스트림 429와 마찬가지로 [[upstreams]] 페일오버 체인을 진행시키므로, 뒤의 업스트림(종량제일 수 있음)이 요청을 처리하고, 어느 것도 처리하지 못할 때만 클라이언트가 429를 받습니다. 단, 이미 Codex/ChatGPT 풀로 커밋된(HTTP 200이 전송된) 스트리밍 요청은 뒤의 업스트림이 처리하지 못하면 HTTP 429와 retry-after 헤더 없이 스트림 안의 종료 SSE error 이벤트를 받습니다. 인바운드 Codex 엔드포인트에서는 같은 오류가 OpenAI 오류 형태로 반환됩니다.
  • 프로바이더. Claude OAuth 풀(5h, 7d, 7d_oi)과 Codex/ChatGPT 풀(5시간 및 주간 창; Fable 키는 여기서 무효)에서 유효합니다. Kimi OAuth 풀에서도 Kimi 업스트림이 사용률 헤더를 보고하면 캡이 적용됩니다. 지금까지 보고가 관측된 적이 없어 실제로는 비활성 상태이며, 관측되지 않은 창은 제외하지 않습니다. Antigravity 쿼터는 캡에 반영되지 않습니다.
  • 상태 표시. 관리자 대시보드는 캡이 걸린 계정을 “Capped”로, Fable 전용 캡이면 “Capped (Fable)“로 표시하고, 설정된 각 캡을 계정의 사용량 막대와 풀 표에 나타냅니다. GET /admin/api/pool은 계정별로 확정된 max_utilization_5h, max_utilization_7d, max_utilization_fable을 보고합니다(해당 창에 적용되는 캡이 없으면 null). GET /usage는 사용 가능한 계정이 없고 활성화된(disabled가 아닌) 계정 중 일시 정지되지 않은 계정 하나 이상에 캡이 걸려 있으면 풀 상태 capped를 보고합니다.

Usage-API 재보정

쿼터 헤더는 shunt를 통과한 트래픽만 반영합니다. usage_refresh_seconds는 GET /api/oauth/usage를 폴링해 권위 있는 사용률, 보고된 각 창의 고유 리셋 시각, 사용률 관측 시각을 동일한 5시간, 공유 주간(7d), Fable 전용 주간(7d_oi) 창에 적용함으로써 그 간극을 메웁니다.

필드가 없거나 0이면 폴링은 꺼집니다; 60 미만의 양수 값은 60초로 올림됩니다. imported된 갱신 가능 계정만 대상이며, 장기 claude setup-token과 token_env 계정은 토큰이 엔드포인트를 호출할 수 없어 건너뜁니다. 간격은 부팅 시 고정되므로 설정 리로드는 폴러를 시작·중지·재조정하지 않습니다. 이 주기적 보정은 각 창의 사용률, 리셋, 사용률 관측 시각을 갱신합니다. 창별 및 집계 status의 freshness와 status 관측 때 캡처한 리셋 경계만 헤더에서 유지됩니다.

쿼터 상태 영속화

풀 쿼터는 메모리에 있으므로 재시작은 cold로 시작합니다: 각 계정은 재시작 후 첫 응답 전까지 미관측 상태로 보이고, 이로 인해 burn-rate 회피가 비활성화되며 트래픽으로 풀이 다시 채워질 때까지 GET /usage가 빈 값을 반환합니다. state_path를 설정하면 각 계정의 창별 사용률과 리셋을 그 파일에 저장해, 풀이 마지막으로 관측된 상태에서 워밍업합니다.

이 파일은 권위 있는 소스가 아니라 best-effort 캐시입니다 — 쿼터는 어차피 업스트림 응답에서 재도출되므로, 파일이 없거나·오래됐거나·손상돼도 cold start만 발생할 뿐 부팅 실패로 이어지지 않습니다. 쓰기는 비공개 temp 파일(Unix에서 0600)을 대상 위로 원자적으로 rename하는 방식이며, 쿼터가 변경됐을 때만 15초 백그라운드 타이머로 이뤄집니다. 쓰기에 실패하면 dirty 상태를 유지하고 다음 tick에서 재시도합니다. 쿨다운은 저장되지 않고(재시작 시 소멸), 복원된 창 중 이미 리셋이 지난 것은 복원 시 import 단계에서 첫 선택이나 snapshot보다 먼저 폐기됩니다. 사용률과 창별 status는 리셋 없는 경우에도 독립된 수명을 가지며, 사용률이 만료된 뒤에도 해당 창의 미래 리셋은 남을 수 있고 캡처한 status 리셋은 status와 함께 지워집니다. 버전 2 파일은 migration 경로를 거쳐 버전 3으로 다시 쓰며, observed_at_status가 없는 집계 status는 저장된 reset_5h, reset_7d, reset_7d_oi 중 가장 이른 리셋을 변경할 수 없는 기한으로 포착합니다. 그 리셋이 이미 지났으면 만료된 리셋, stamp가 없는 집계 status, 합성한 stamp를 같은 import에서 함께 제거합니다. 7일이라는 타당한 범위를 넘는 미래 리셋은 부팅 시각부터 7일 후를 상한으로 삼고, 리셋이 없으면 부팅 시각부터 7일 cap을 시작합니다. 이미 stamp된 v2 값은 리셋으로 다시 해석하지 않지만, 일반 import는 고아 메타데이터를 정규화하고 경과한 신호를 만료시키며 미래 시각을 부팅 시각으로 보정하고, 남은 stamp 없는 집계에는 필요하면 부팅 시각을 넣습니다. 이후 reset-only나 usage 갱신은 포착한 기한을 연장하지 않으며 v3으로 다시 쓴 뒤 두 번째 복원에서도 같은 상태를 유지합니다. 버전 3의 리셋 없는 status는 reset-only 갱신 뒤에도 리셋 없는 상태로 유지됩니다. 경로는 부팅 시 고정되며, 필드가 없으면 영속화는 꺼집니다.

페일오버 규칙

응답 동작
2xx 릴레이하고 정상으로 표시합니다.
Fable 요청에서 anthropic-ratelimit-unified-7d_oi-status: rejected만 있는 429 Fable 쿼터 소진: 숫자 retry-after로 Fable 요청에만 적용되는 쿨다운(기본 60초, 1–3600초로 클램프)을 건 뒤 로테이션합니다. non-Fable 요청은 계속 그 계정을 쓸 수 있습니다. non-Fable 요청에서 같은 7d_oi 단독 거부가 발생하면 계정 전체 쿨다운을 적용합니다.
429 + anthropic-ratelimit-unified-5h-status: rejected 또는 -7d-status: rejected 공유 쿼터 소진: 숫자 retry-after로 계정 전체 쿨다운(기본 60초, 1–3600초로 클램프)한 뒤 모든 모델에 대해 로테이션합니다. 7d_oi도 함께 rejected인 경우에도 계정 전체 쿨다운입니다.
일반 429 일시적 스로틀: 숫자 retry-after만큼 대기(기본 1초, 상한 300초)하고 같은 계정을 한 번 재시도한 뒤, 그 재시도 응답을 릴레이합니다.
credentials에서의 401 강제 갱신 후 같은 계정을 한 번 재시도; 여전히 401이면 5분 쿨다운 후 로테이션합니다.
token_env 또는 스토어 관리 setup token에서의 401 갱신 불가: 5분 쿨다운 후 로테이션합니다.
5xx 30초 쿨다운 후 로테이션합니다.
전송 실패(연결 끊김이나 TLS 오류처럼 응답이 없는 경우) 스로틀 재시도나 갱신 재시도 중이라도 같은 계정으로 한 번 다시 보냅니다. 다시 보낸 요청도 전송 실패일 때만 30초 쿨다운 후 로테이션합니다. 타임아웃은 다시 보내지 않습니다. 재전송은 전체 POST를 다시 보내는 것이므로 드물게 같은 턴이 두 번 실행될 수 있습니다.
그 외 status 페일오버 없이 릴레이합니다.

분류는 응답 본문이 스트리밍되기 전에 일어나므로, 스트림 중간의 실패는 절대 재전송되지 않습니다. 풀이 응답을 받은 뒤 시도를 소진하면 클라이언트는 마지막 실제 업스트림 status와 본문을 받습니다. 어떤 업스트림 응답도 받기 전에 모든 계정이 실패하면 shunt는 게이트웨이 소유의 오류를 반환합니다.

Anthropic으로 라우팅되는 POST /v1/messages/count_tokens 요청도 같은 풀을 사용합니다.

Claude Sonnet 5.5 대화를 한 계정에 유지하기

Anthropic은 Claude Sonnet 5.5가 만든 thinking 블록을 그 블록을 만든 계정에 묶습니다(Preserved thinking). 다른 계정이 그 블록을 다시 보내면 API는 모델이 보기 전에 그 블록을 버립니다. 요청은 여전히 성공하지만, 모델은 앞선 추론 없이 답합니다. 이전 모델의 thinking 블록은 영향을 받지 않습니다.

풀은 세션에 고정(session-sticky)되므로 대화는 보통 한 계정에 머뭅니다. 다음 경우에는 다른 계정으로 옮겨집니다:

  • 스티키 계정이 쿼터에 근접했거나, 쿨다운됐거나, 비활성화되어 선제 로테이션이 세션을 그 계정에서 옮기는 경우.
  • 응답이 페일오버 규칙에 따라 로테이션을 일으키는 경우: 쿼터 429, 갱신으로 해결되지 않는 401(셋업 토큰은 갱신할 수 없으므로 그 401은 모두 로테이션함), 5xx, 또는 즉시 재전송에서도 반복되는 전송 실패.
  • 풀의 서로 다른 계정 수가 바뀌는 경우. 예를 들어 shunt login claude로 추가한 스토어 항목은 재시작 없이 풀에 들어옵니다. 스티키 계정은 세션 ID를 풀의 서로 다른 계정 수로 해시해 고르므로, 기존 세션 대부분이 다른 계정에 배정됩니다.
  • 요청에 x-claude-code-session-id가 없어 라운드 로빈을 사용하는 경우.

옮겨진 뒤의 Sonnet 5.5 턴은 이전 계정에서 만들어진 추론 없이 실행됩니다. 실패하는 것은 없습니다. shunt는 thinking-binding-controls-2026-08-01 베타 헤더를 추가하지 않으므로, 클라이언트가 이 헤더를 직접 보내지 않는 한 응답은 이 누락을 알리지 않습니다(shunt는 클라이언트의 anthropic-beta 값을 유지하고, oauth-2025-04-20이 없을 때만 덧붙입니다). shunt도 이 누락을 로그에 남기지 않습니다.

모든 Sonnet 5.5 요청을 한 계정에 두려면 그 모델에 해당 계정으로 범위를 좁힌 전용 업스트림을 두세요. 계정 범위 지정은 순서가 있는 [[upstreams]] 형식을 사용하며(구성의 [[upstreams]] 섹션 참고), 이 형식은 한 설정 파일 안에서 [providers.*]와 함께 쓸 수 없습니다. shunt login claude로 계정을 저장하고 이름으로 참조하세요:

[server]
default_provider = "anthropic"

# 먼저 선언했으므로, 이 업스트림에 매핑되는 모든 모델에서 가장 먼저 시도됩니다.
[[upstreams]]
name = "anthropic-sonnet55"
provider = "anthropic"
auth = { mode = "claude_oauth", account = "primary" }

# 그 밖의 모든 모델: 평소의 로테이션을 따르는 Claude 스토어 전체.
[[upstreams]]
name = "anthropic"
provider = "anthropic"
auth = "claude_oauth"

[[models]]
id = "claude-sonnet-5-5"
[models.upstream_model]
anthropic-sonnet55 = "claude-sonnet-5-5"
anthropic = "claude-sonnet-5-5" # 엄격하게 고정하려면 이 줄을 제거

Sonnet 5.5 요청은 먼저 primary로 갑니다. 모델 맵에 anthropic 줄이 있으면, 체인을 진행시키는 primary의 실패(쿼터 429나 5xx 등)는 풀 전체로 넘어갑니다. 각 턴은 그 턴을 처리한 계정이 만든 Sonnet 5.5 추론만 쓸 수 있으므로, 다른 계정이 처리한 턴은 primary에서 만든 추론 없이 실행됩니다. 스토어 전체를 쓰는 풀에는 primary도 들어 있으므로 primary를 다시 시도할 수도 있고, 그러면 primary에서 만든 추론은 유지됩니다. 요청은 풀이 시도한 모든 계정이 실패할 때만 실패합니다. 그 줄이 없으면 고정은 엄격해집니다: 클라이언트는 대신 primary의 오류를 받고, 대화를 옮기는 것은 아무것도 없습니다.

다음 트레이드오프를 염두에 두세요:

  • 모든 Sonnet 5.5 세션이 같은 계정의 5시간 쿼터와 주간 쿼터를 소비합니다.
  • primary는 스토어 전체를 훑는 anthropic 풀에도 남아 있습니다. Sonnet 5.5 전용으로 남겨 두려면 대신 풀의 범위를 좁히세요. 예: auth = { mode = "claude_oauth", accounts = ["backup", "team-2"] }.
  • 쿼터, 쿨다운, 갱신 상태는 업스트림이 아니라 계정에 속합니다. 두 업스트림 모두에 나열된 스토어 계정은 하나의 상태와 하나의 리프레시 락을 공유합니다. 인라인 계정 테이블은 shunt가 계정을 식별할 수 없을 때, 즉 uuid가 없고 자격 증명에도 shuntAccountUuid가 없을 때 업스트림별로 키가 매겨집니다. 그런 자격 증명을 두 업스트림에 인라인으로 반복하면 상태와 리프레시 락이 갈라지므로, 대신 스토어 계정을 이름으로 참조하세요.
  • shunt check는 범위로 지정한 이름이 스토어에 있는지 검증하지 않습니다. 없는 이름은 요청이 풀을 해석할 때 실패합니다.

요청과 응답 변경

선택된 계정에 대해 shunt는 클라이언트 인증을 다음으로 교체합니다:

Authorization: Bearer <selected OAuth token>
anthropic-beta: ...,oauth-2025-04-20

들어온 authorization과 x-api-key를 모두 제거하고, oauth-2025-04-20은 없을 때만 덧붙이며, 다른 종단 간(end-to-end) 헤더는 보존합니다.

풀링된 응답은 계정을 식별합니다:

x-shunt-account: backup

공유 게이트웨이에서는 중립적인 계정 이름을 사용하세요. 이 헤더는 응답을 받는 모든 인가된 클라이언트에게 구성된 레이블을 노출합니다. 풀 소진 후 마지막 업스트림 응답을 릴레이할 때는 x-shunt-account가 생략됩니다.

account_uuid

Claude Code는 문자열 값인 metadata.user_id 안에 계정 메타데이터를 JSON으로 인코딩할 수 있습니다. 선택된 계정에 uuid가 있으면 shunt는 기존 내부 account_uuid를 그 값으로 교체합니다. 메타데이터가 없거나, 형식이 잘못됐거나, account_uuid가 없거나, 선택된 계정에 UUID가 없으면 본문을 그대로 둡니다. 없는 메타데이터를 주입하지는 않습니다.

보안 제약

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

  • 프로바이더가 kind = "anthropic"이고;
  • base_url이 HTTPS를 사용하며;
  • 호스트가 anthropic.com이거나 api.anthropic.com 같은 그 서브도메인일 때.

이 시작 검사는 OAuth bearer가 다른 오리진으로, 또는 평문으로 전송되는 것을 막습니다. HTTPS와 호스트 검사는 루프백 호스트에서는 완화됩니다(localhost, 127.0.0.1, [::1] 등): 루프백 base_url은 평문 HTTP와 임의의 호스트를 쓸 수 있어 로컬 디버깅 프록시나 목(mock)이 트래픽을 받을 수 있습니다 — bearer가 운영자의 머신을 벗어날 수는 없습니다. 루프백이 아닌 호스트에는 항상 HTTPS + anthropic.com이 요구됩니다. 공유 배포에서는 claude_oauth가 게이트웨이 소유 자격 증명을 소비하므로 [server.auth]도 함께 구성하세요. 클라이언트는 이미 보내고 있는 ANTHROPIC_AUTH_TOKEN으로 인증됩니다(x-shunt-token, x-api-key와 나란히 Authorization: Bearer로도 클라이언트 토큰을 받습니다) — 풀 전용 게이트웨이라면 ANTHROPIC_CUSTOM_HEADERS 줄이 필요 없습니다.

폭주 제어(storm control)

[server.pool] ramp_initial_concurrency(기본값 꺼짐)를 설정하면 계정 아이덴티티별 동시 허용(admission)을 슬로 스타트 램프로 제어하므로, 페일오버 전환이 방금 선택된 계정에 진행 중인 모든 요청을 한꺼번에 몰아넣지 못합니다. 방금 트래픽을 받기 시작한 아이덴티티는 최대 구성된 개수만큼의 동시 요청만 허용하며, 성공 응답마다 허용치가 두 배로 늘고(슬로 스타트), 페일오버는 램프를 다시 시작하며, 거부된 요청은 선택 순서상 다음 계정으로 넘어갑니다(마지막 후보는 항상 시도됩니다). [server.pool]를 참고하세요.

구현 동작은 KarpelesLab/teamclaude와 배포된 Claude Code 바이너리를 참고했습니다. shunt는 teamclaude에 대한 런타임 의존성이 없습니다.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close