예산 (Budgets)¶
budgets: 섹션은 Continuum Hub 없이 데이터 플레인에서 토큰과 USD 비용의 장기 윈도 쿼터를 적용합니다. 속도 제한(rate limiting)과는 다른 질문에 답합니다. 속도 제한은 호출자가 얼마나 빨리 사용할 수 있는지를 제한하고, 예산은 롤링 윈도가 초기화되기 전까지 얼마나 많이 사용할 수 있는지를 제한합니다.
budgets: 섹션이 없거나 enabled: false인 라우터는 동작이 바뀌지 않습니다. 카운터를 만들지 않고, 요청을 과금하지 않으며, 예산 헤더도 내보내지 않습니다.
세 가지 토큰 제한의 공존¶
키의 토큰을 제한할 수 있는 설정은 세 가지입니다. 서로 독립적인 검사이며, 요청은 해당되는 모든 검사를 통과해야 합니다.
| 설정 | 형태 | 적용 지점 | 호출자에게 보고 |
|---|---|---|---|
api_keys[].max_tokens_budget과 budget_window | 키별 명시적 롤링 윈도 쿼터 | 라우팅 전 인증 미들웨어 | 거부만 |
budgets.scopes.per_api_key | max_tokens_budget을 설정하지 않은 키의 플릿 기본값 | 모델 확인 후 어드미션 | x-budget-api-key-* 헤더 |
rate_limiting.limits.*.tokens_per_minute / tokens_per_day | 리키 버킷 속도 | 어드미션, 완료 시 예약 정산 | x-ratelimit-tokens-* 헤더 |
max_tokens_budget은 원래 의미를 유지합니다. 즉 이슈 #2323의 키별 명시적 롤링 윈도 토큰 쿼터이며, 인증 지점에서 읽고 과금합니다. 동시에 이 섹션의 키별 재정의입니다. 키가 이 값을 설정하면 해당 키에는 budgets.scopes.per_api_key 기본값을 어드미션하지 않고 과금하지도 않으므로, 같은 키가 같은 스코프에 두 번 과금되지 않습니다. 값을 설정하지 않은 키는 플릿 기본값을 따릅니다. budgets.scopes.per_model과 budgets.scopes.global은 해당 키에도 계속 적용됩니다. 이 스코프들은 공유 버킷이며 키 자체의 재정의가 아니기 때문입니다.
tokens_per_minute과 tokens_per_day는 쿼터가 아니라 속도입니다. 속도는 계속 보충되며, 테넌트는 1분 또는 1일치를 한 번에 사용할 수 있습니다. 반면 예산은 롤링 윈도에 걸쳐 서서히 빠져나갑니다. 두 검사는 모두 어드미션에서 실행되며, 어느 쪽도 다른 쪽을 대체하지 않습니다.
설정¶
budgets:
enabled: true
storage: memory # memory | redis
fail_open: true # redis 장애 시: 허용하고 최선형으로 집계
windows:
- id: daily
period: 24h
- id: monthly
period: 30d
scopes:
per_api_key: # max_tokens_budget이 없는 키의 기본값
tokens: 2000000
cost_usd: 50.0
per_model: # 클라이언트가 요청한 모델 이름으로 키를 구성
gpt-5.6-sol:
tokens: 500000
global: # 플릿 전체 버킷 하나
tokens: 20000000
cost_usd: 500.0
cost_source: model_metadata
| 필드 | 타입 | 기본값 | 설명 |
|---|---|---|---|
enabled | bool | false | false이면 완전한 무동작입니다. |
storage | memory 또는 redis | memory | redis는 복제본 간 카운터를 공유합니다. |
fail_open | bool | true | Redis 장애 처리 방식입니다. 저장소를 참고하십시오. |
windows | 배열 | 비어 있음 | 이름이 붙은 롤링 윈도입니다. 섹션이 활성이고 스코프에 제한이 있으면 필요합니다. |
windows[].id | string | 없음 | 고유 식별자이며, 거부 응답, 메트릭, 관리 API에 표시됩니다. |
windows[].period | string | 없음 | <n>m, <n>h, <n>d, <n>w, calendar:day, calendar:month 중 하나입니다. |
scopes.per_api_key | 제한 | 미설정 | 키별 기본값입니다. |
scopes.per_model | 제한 맵 | 비어 있음 | 모델 이름마다 버킷 하나입니다. |
scopes.global | 제한 | 미설정 | 플릿 전체 버킷 하나입니다. |
cost_source | model_metadata | model_metadata | 유일하게 지원되는 출처입니다. |
제한 객체는 tokens, cost_usd, 또는 둘 다를 설정합니다. 스코프가 동작하려면 최소 하나가 설정되어야 하며, 설정된 값은 0보다 커야 합니다.
config validate는 중복 윈도 id, 파싱할 수 없는 기간, 제한은 있지만 윈도가 없는 활성 섹션, 0인 토큰 제한, 0 이하이거나 유한하지 않은 비용, 빈 모델 이름을 거부합니다.
윈도¶
윈도는 달력 경계가 아니라 고정 길이의 롤링 기간입니다. 사용량은 시간 조각 단위로 하나씩 빠져나가므로, 24h 윈도는 항상 최근 24시간입니다. calendar:day와 calendar:month는 자정이나 매월 1일 초기화가 아니라 24시간과 30일의 별칭입니다.
스코프의 tokens와 cost_usd는 설정된 모든 윈도에 적용됩니다. 요청은 해당되는 모든 스코프의 모든 윈도를 만족해야 합니다. 위 예에서 일일 한도 아래에 있는 키라도 월간 한도를 넘게 되면 거부되며, 같은 요청에서 모델 버킷과 전역 버킷을 함께 검사합니다.
스코프와 우선순위¶
- 키별(per-key): 호출자 자신의 신원입니다. 수준을 보고하는 유일한 스코프입니다.
max_tokens_budget이 있는 키는 이 스코프를 사용하지 않습니다. - 모델별(per-model): 클라이언트가 요청한 모델 이름으로 키를 구성합니다. 항목이 없는 모델의 요청은 이 스코프를 건너뜁니다. 해당 모델의 모든 호출자가 버킷을 공유합니다.
- 전역(global): 모든 호출자와 모델이 공유하는 버킷 하나입니다.
검사는 윈도마다 이 순서로 실행되며, 요청을 감당할 수 없는 첫 스코프가 거부합니다. 거부 응답은 해당 스코프, 윈도, 설정된 제한을 표시합니다. 공유 스코프의 현재 사용량은 절대 표시하지 않습니다.
어드미션 추정치는 요청의 입력 추정치와 출력 한도를 모델의 공개 가격으로 계산한 값입니다. 이 추정치는 응답 본문이 끝날 때까지 유지됩니다. 완료된 사용량은 버퍼 응답과 스트리밍 응답 모두에서 사용량 지점(usage seam)을 통해 정확히 한 번 기록됩니다.
비용¶
비용은 model-metadata.yaml 가격에서 가져오며, 응답 비용 헤더와 같은 PricingInfo 경로로 해석합니다. 정수 마이크로 USD로 저장하므로 긴 윈도에서도 부동소수점 오차가 누적되지 않습니다.
가격이 없는 모델은 토큰만 집계하고 비용은 추가하지 않습니다. 따라서 cost_usd만 설정한 제한은 가격이 없는 모델을 제한하지 않습니다.
저장소¶
storage: memory가 기본값입니다. 카운터는 프로세스 안에 있으므로, 로드 밸런서 뒤에서는 각 복제본이 자신이 처리한 트래픽에 대해서만 쿼터를 적용합니다. 플릿이 하나의 쿼터를 공유해야 하면 storage: redis를 사용하십시오.
Redis 저장소는 rate_limiting.redis 엔드포인트와 key_prefix를 재사용합니다. budgets: 아래에 별도 엔드포인트는 없습니다. 빌드 시 redis-cache Cargo 기능이 필요합니다. 카운터 키는 rate_limiting.redis.ttl이 더 짧더라도 가장 긴 윈도만큼 유지되므로, 월간 예산이 캐시 TTL 직후 사라지지 않습니다. rate_limiting.redis가 구성되지 않았거나 시작 시 스토어를 만들 수 없으면 경고를 기록하고 프로세스별 메모리 카운터로 섹션을 적용합니다.
Redis 장애 시 동작은 다음과 같습니다.
fail_open: true(기본값)는 요청을 허용하고 복제본의 로컬 트래커에 집계합니다. 공유 쿼터는 일시적으로 약하게 적용되며, 장애는router_budget_storage_unavailable_total{action="fallback"}에 기록됩니다.fail_open: false는 Redis가 응답할 때까지503과Retry-After: 5로 거부하며,action="refused"로 기록합니다. 공유 쿼터는 정확하게 유지되지만 가용성을 희생합니다.
fail_open과 엔드포인트는 시작 시와 리로드 시 읽습니다. Redis 저장소는 시작 설정으로 만들어지므로 엔드포인트 변경에는 재시작이 필요합니다.
거부 응답¶
소진된 예산은 429 Too Many Requests로 거부되며, Retry-After는 거부한 윈도를 초 단위로 표시합니다.
- OpenAI 인그레스 (
/v1/chat/completions,/v1/completions,/v1/responses): 표준 오류 봉투를 사용하며,type: budget_exceeded,code: budget_exceeded, 그리고scope,window,unit, 설정된limit(또는limit_micro_usd)을 담은error.details를 포함합니다. 모델 거부에는model이 추가됩니다. - Anthropic 인그레스 (
/anthropic/v1/messages,/v1/messages): Anthropic 봉투인{"type":"error","error":{"type":"rate_limit_error",...}}를 사용하며, 같은error.details형제 필드를 포함합니다.
프로세스 로컬 트래커가 가득 찬 경우에도 키를 추적하지 않은 채 허용하지 않고 같은 방식으로 거부합니다.
응답 헤더¶
키 스코프를 예약한 어드미션 요청은 키 자신의 수준을 보고합니다.
| 헤더 | 의미 |
|---|---|
x-budget-api-key-limit | 키 윈도의 토큰 제한 |
x-budget-api-key-remaining | 이 요청의 예약 후 남은 토큰 |
x-budget-api-key-reset | 윈도가 다시 비는 Unix 시각 |
모델별 및 전역 수준은 의도적으로 보고하지 않습니다. 해당 버킷은 다른 호출자와 공유되므로, 채워진 정도를 공개하면 다른 테넌트가 공유 쿼터를 얼마나 사용했는지 노출됩니다. 공유 속도 제한 차원에도 같은 규칙이 적용됩니다.
키별 스코프에 토큰 제한이 없거나, 요청이 어떤 스코프에도 해당하지 않거나, 예산이 꺼져 있으면 헤더는 없습니다. 비용만 설정한 키별 제한은 토큰 숫자를 만들어 내지 않고 수준을 보고하지 않습니다.
관리 API¶
GET /admin/budgets는 유효 설정과 각 스코프·윈도의 현재 카운터를 반환합니다. POST /admin/budgets/reset는 스코프의 기록된 사용량을 지웁니다.
curl -H "Authorization: Bearer $ADMIN_TOKEN" http://localhost:8080/admin/budgets
curl -X POST -H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"scope": "per_api_key", "window": "daily"}' \
http://localhost:8080/admin/budgets/reset
리셋 본문은 scope(per_api_key, per_model, global)를 받고, 선택적으로 window, api_key_id(키별 전용), model(모델별 전용)을 받습니다. Redis 기반 스코프를 지우면 공유 카운터가 삭제되므로 모든 복제본이 리셋을 확인합니다. 두 엔드포인트는 Admin 인증 계층 뒤에 있습니다.
메트릭¶
| 메트릭 | 레이블 | 의미 |
|---|---|---|
router_budget_rejections_total | scope, window | 예산 스코프가 거부한 요청 수 |
router_budget_used | scope, window, unit | 기록된 사용량. unit은 tokens 또는 cost_micro_usd |
router_budget_storage_unavailable_total | action | 장애 수. fallback(fail-open) 또는 refused(fail-closed) |
레이블 집합은 구조적으로 닫혀 있습니다. 모델 이름, 키 id, 그 밖의 자유 텍스트는 포함되지 않습니다.
핫 리로드¶
budgets: 섹션은 요청마다 실시간 설정에서 읽습니다. 제한, 윈도, 스코프, fail_open, storage 변경은 다음 요청부터 적용되며, 넓힌 제한은 즉시 효력을 냅니다.
어드미션 계층은 시작 설정으로 마운트됩니다. budgets: 섹션이 있거나 rate_limiting이 활성인 상태로 시작한 라우터는 이후 리로드에서 섹션 변경(섹션 활성화 포함)을 반영합니다. 둘 다 없이 시작한 라우터에는 활성화할 어드미션 계층이 없으므로, 그러한 배포에 budgets:를 추가하려면 재시작이 필요합니다.
제한 사항¶
- 메모리 카운터는 프로세스 단위입니다. 복제본이 N개인 플릿은 메모리 쿼터를 최대 N배까지 적용합니다.
model_metadata가격만 지원합니다. 가격이 없는 모델은 토큰만 기여하고 비용은 기여하지 않습니다.- 예산 거부는 디스패치 전에 요청을 중단합니다. 백엔드 슬롯을 예약하지 않으며, 취소된 클라이언트는 추정치를 반환합니다.
- 관리 API로 만든 런타임 키에는
max_tokens_budget이 없으므로budgets.scopes.per_api_key를 따릅니다.
함께 보기¶
- 속도 형태와 설계 기록은 속도 제한과 토큰 예산을 참고하십시오.
max_tokens_budget은 보안 및 관리를 참고하십시오.