Skip to content

예산 & 제한

Ongrid 는 모든 provider 에 걸쳐 UTC 일별 전역 토큰 상한을 강제합니다. 기본은 무제한; 환경 변수 하나가 켭니다:

bash
ONGRID_LLM_DAILY_TOKEN_LIMIT=2000000   # 2 million tokens per UTC day

<=0 는 상한 비활성화. 단일 값, provider 별 아님 — 이것이 MVP 범위. 테넌트가 안착하면 조직별 설정으로 이동; 이 노브는 안전망 전역 상한으로 남음.

어떻게 배선되나

internal/pkg/llm/ 에 세 조각:

go
// 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 마다:

  1. Prompt 토큰 추정: len(text)/4 (보수적).
  2. BudgetChecker.Check(ctx, userID, estPromptTokens).
  3. 거부 시 — ErrBudgetExceeded 를 context 에 저장하여 다운스트림 노드가 short-circuit 가능; 후속 코드가 표면화.

OnEnd 에서 실제 Usage.TotalTokens 가 현재 UTC 일 버킷에 대해 기록.

ErrBudgetExceeded

go
// 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:

go
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 가 인터페이스를 통해 흐르므로 향후 MySQL usage_daily 테이블이 드롭인이지만, 오늘 상한은 모두에게 같은 숫자.

단일 테넌트로의 피벗이 사용자별 백엔드를 보류; 다중 사용자가 돌아올 때 인터페이스는 forward-compatible.

토큰 추정

BudgetCallbackHandler.OnStart 가 문자 카운트 / 4 로 prompt 토큰 추정. 의도적으로 보수적 — 실제 토큰화는 provider / 모델별로 다름, 예산은 초과보다 경계 호출 거부 측으로 기울어야 함.

OnEnd 에서 provider 가 반환한 실제 Usage.TotalTokens 가 기록 — 추정이 빗나가도 예산은 ground truth 추적.

Provider 가 토큰 카운트를 반환하지 않으면 (일부 custom 엔드포인트는 안 함), callback 은 response-meta 휴리스틱으로 폴백; 테스트의 OnEndUsesResponseMetaFallback 참고.

예산 관찰

bash
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) 은 다중 테넌시와 함께 보류.

같이 보기