Budget и лимиты
Ongrid форсит глобальный per-UTC-day token cap по каждому provider. По умолчанию — unlimited; одна env-переменная включает:
ONGRID_LLM_DAILY_TOKEN_LIMIT=2000000 # 2 million tokens per UTC day<=0 отключает cap. Единое значение, не per-provider — это MVP scope. Когда тенанты приземлятся, это переедет в per-org настройки; эта ручка остаётся как safety-net global cap.
Как это подключено
Три куска, в 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)Graph-kernel runtime устанавливает callback-handler в свою eino callbacks chain. На каждом ChatModel OnStart:
- Оценить prompt-токены:
len(text)/4(консервативно). BudgetChecker.Check(ctx, userID, estPromptTokens).- При отказе — сохранить
ErrBudgetExceededв контекст, чтобы downstream-нода могла short-circuit'нуться; последующий код его поднимает.
На OnEnd фактический Usage.TotalTokens записывается против текущего UTC-day bucket.
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
}Ошибка распространяется в:
- Chat send-эндпоинт — возвращает HTTP 429 с телом
{ "error": "budget_exceeded", "message": "..." }, которое chat-UI рендерит in-line. - RCA investigator worker — строка отчёта приземляется как
status=failedсstatus_reason="budget_exceeded". - Translate-путь — fall back на «translation unavailable (budget exceeded)», и оригинальный текст показывается.
Caveats 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
}Следствия:
- Нет persistence — рестарт manager ресетит дневной счётчик. Если вы реально хотите жёсткий daily cap, который переживает рестарты, свопайте реализацию. Интерфейс
BudgetChecker— это шов. - Single-process — если вы запускаете несколько manager за load balancer (вы не должны пока, но если), у каждого свой счётчик.
- Global, не per-user —
userIDтечёт через интерфейс, так что будущая MySQLusage_dailyтаблица — drop-in, но сегодня cap — это то же число для всех.
Pivot на single-tenant отложил per-user backend; интерфейс forward-compatible для когда multi-user вернётся.
Оценка токенов
BudgetCallbackHandler.OnStart оценивает prompt-токены по character count / 4. Это намеренно консервативно — реальная tokenisation варьируется по provider / model, и budget должен ошибаться в сторону отказа граничных вызовов, а не превышения.
На OnEnd фактический Usage.TotalTokens, возвращённый provider, записывается — так что budget отслеживает ground truth, даже когда оценка была off.
Если provider не возвращает token counts (некоторые custom-эндпоинты не возвращают), callback fall back'ит на response-meta heuristic; см. OnEndUsesResponseMetaFallback в тестах.
Наблюдение budget
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 dashboard рендерит их как daily-spend график плюс алерт при 80% cap.
Отключить для одной нагрузки
Нет ручки «отключить budget для investigator». Если RCA бьётся в cap, и вы предпочли бы, чтобы он продолжал работать, чем chat, поднимите cap — для этого он и нужен. Альтернатива (per-workload quotas) припаркована вместе с multi-tenancy.
См. также
- Обзор моделей.
- Routing — ортогонально budget; cap применяется per-call независимо от того, какой provider был выбран.
- Переменные окружения — ручки
ONGRID_LLM_*.