Skip to content

Budget и лимиты

Ongrid форсит глобальный per-UTC-day token cap по каждому provider. По умолчанию — unlimited; одна env-переменная включает:

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

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)

Graph-kernel runtime устанавливает callback-handler в свою eino callbacks chain. На каждом ChatModel OnStart:

  1. Оценить prompt-токены: len(text)/4 (консервативно).
  2. BudgetChecker.Check(ctx, userID, estPromptTokens).
  3. При отказе — сохранить ErrBudgetExceeded в контекст, чтобы downstream-нода могла short-circuit'нуться; последующий код его поднимает.

На OnEnd фактический Usage.TotalTokens записывается против текущего UTC-day bucket.

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
}

Ошибка распространяется в:

  • 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:

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
}

Следствия:

  • Нет persistence — рестарт manager ресетит дневной счётчик. Если вы реально хотите жёсткий daily cap, который переживает рестарты, свопайте реализацию. Интерфейс BudgetChecker — это шов.
  • Single-process — если вы запускаете несколько manager за load balancer (вы не должны пока, но если), у каждого свой счётчик.
  • Global, не per-useruserID течёт через интерфейс, так что будущая MySQL usage_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

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 dashboard рендерит их как daily-spend график плюс алерт при 80% cap.

Отключить для одной нагрузки

Нет ручки «отключить budget для investigator». Если RCA бьётся в cap, и вы предпочли бы, чтобы он продолжал работать, чем chat, поднимите cap — для этого он и нужен. Альтернатива (per-workload quotas) припаркована вместе с multi-tenancy.

См. также