예산 & 제한
Ongrid 는 모든 provider 에 걸쳐 UTC 일별 전역 토큰 상한을 강제합니다. 기본은 무제한; 환경 변수 하나가 켭니다:
ONGRID_LLM_DAILY_TOKEN_LIMIT=2000000 # 2 million tokens per UTC day<=0 는 상한 비활성화. 단일 값, provider 별 아님 — 이것이 MVP 범위. 테넌트가 안착하면 조직별 설정으로 이동; 이 노브는 안전망 전역 상한으로 남음.
어떻게 배선되나
internal/pkg/llm/ 에 세 조각:
// 1. The interface
type BudgetChecker interface {
Check(ctx context.Context, userID uint64, estPromptTokens int) error
Record(ctx context.Context, userID uint64, usage Usage) error
}
// 2. The MVP implementation
budget := llm.NewInMemoryBudget(cfg.LLM.DailyTokenLimit)
// 3. The eino callback that bridges to the graph kernel
handler := llm.NewBudgetCallbackHandler(budget, userID)그래프 커널 런타임이 eino callback 체인에 callback 핸들러 설치. 모든 ChatModel OnStart 마다:
- Prompt 토큰 추정:
len(text)/4(보수적). BudgetChecker.Check(ctx, userID, estPromptTokens).- 거부 시 —
ErrBudgetExceeded를 context 에 저장하여 다운스트림 노드가 short-circuit 가능; 후속 코드가 표면화.
OnEnd 에서 실제 Usage.TotalTokens 가 현재 UTC 일 버킷에 대해 기록.
ErrBudgetExceeded
// internal/pkg/llm/budget.go:37
func (b *InMemoryBudget) Check(ctx context.Context, userID uint64, estPromptTokens int) error {
if b.dailyLimit <= 0 {
return nil
}
b.mu.Lock()
defer b.mu.Unlock()
key := b.dayKey()
if b.used[key]+estPromptTokens > b.dailyLimit {
return ErrBudgetExceeded
}
return nil
}에러는 다음으로 전파:
- 채팅 send 엔드포인트 — 채팅 UI 가 인라인으로 렌더링하는
{ "error": "budget_exceeded", "message": "..." }body 와 함께 HTTP 429 반환. - RCA investigator worker — 보고서 행이
status_reason="budget_exceeded"로status=failed안착. - 번역 경로 — "translation unavailable (budget exceeded)" 로 폴백, 원본 텍스트 표시.
InMemoryBudget 주의사항
MVP 구현은 in-memory:
type InMemoryBudget struct {
mu sync.Mutex
dailyLimit int // tokens per UTC day; <=0 means unlimited
used map[string]int // key = "YYYY-MM-DD" (UTC)
now func() time.Time
}결과:
- 영속성 없음 — 매니저 재시작이 일별 카운터 리셋. 재시작에서도 살아남는 하드 일별 상한을 원한다면 구현 스왑.
BudgetChecker인터페이스가 seam. - 단일 프로세스 — 로드 밸런서 뒤에 다중 매니저 실행 시 (아직 아니지만 만약), 각각 자체 카운터를 가짐.
- 전역, 사용자별 아님 —
userID가 인터페이스를 통해 흐르므로 향후 MySQLusage_daily테이블이 드롭인이지만, 오늘 상한은 모두에게 같은 숫자.
단일 테넌트로의 피벗이 사용자별 백엔드를 보류; 다중 사용자가 돌아올 때 인터페이스는 forward-compatible.
토큰 추정
BudgetCallbackHandler.OnStart 가 문자 카운트 / 4 로 prompt 토큰 추정. 의도적으로 보수적 — 실제 토큰화는 provider / 모델별로 다름, 예산은 초과보다 경계 호출 거부 측으로 기울어야 함.
OnEnd 에서 provider 가 반환한 실제 Usage.TotalTokens 가 기록 — 추정이 빗나가도 예산은 ground truth 추적.
Provider 가 토큰 카운트를 반환하지 않으면 (일부 custom 엔드포인트는 안 함), callback 은 response-meta 휴리스틱으로 폴백; 테스트의 OnEndUsesResponseMetaFallback 참고.
예산 관찰
curl -s localhost:9100/metrics | grep llm_budget
# llm_budget_daily_limit_tokens 2000000
# llm_budget_used_tokens_today 412847
# llm_budget_rejections_total 3메트릭은 BudgetCallbackHandler.Stats() 가 배선. Self-obs Prom 대시보드가 이를 일일 지출 그래프 + 상한의 80% 에서 알림으로 렌더링.
한 워크로드에 대해 비활성화
"investigator 에 대해 예산 비활성화" 노브는 없음. RCA 가 상한을 치고 있고 채팅보다 RCA 가 계속 실행되길 원한다면 상한을 올리세요 — 그것이 존재 이유. 대안 (워크로드별 quota) 은 다중 테넌시와 함께 보류.