Skip to content

Budget & limites

O Ongrid impõe um cap global por dia-UTC de tokens em todo provider. O padrão é ilimitado; uma única env var o liga:

bash
ONGRID_LLM_DAILY_TOKEN_LIMIT=2000000   # 2 milhões de tokens por dia UTC

<=0 desabilita o cap. Valor único, não por-provider — é o escopo do MVP. Quando tenants pousarem, vai para settings por-org; este knob fica como cap global de rede de segurança.

Como está conectado

Três peças, em internal/pkg/llm/:

go
// 1. A interface
type BudgetChecker interface {
    Check(ctx context.Context, userID uint64, estPromptTokens int) error
    Record(ctx context.Context, userID uint64, usage Usage) error
}

// 2. A implementação MVP
budget := llm.NewInMemoryBudget(cfg.LLM.DailyTokenLimit)

// 3. O callback eino que faz ponte com o graph kernel
handler := llm.NewBudgetCallbackHandler(budget, userID)

O runtime do graph-kernel instala o callback handler em sua cadeia de callbacks eino. Em cada OnStart de ChatModel:

  1. Estima tokens de prompt: len(text)/4 (conservador).
  2. BudgetChecker.Check(ctx, userID, estPromptTokens).
  3. Em rejeição — armazena ErrBudgetExceeded no contexto para que o nó downstream possa fazer short-circuit; código subsequente o expõe.

Em OnEnd, o Usage.TotalTokens real é registrado contra o bucket atual do dia 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
}

O erro propaga para:

  • O endpoint de envio de chat — retorna HTTP 429 com um body { "error": "budget_exceeded", "message": "..." } que a UI de chat renderiza inline.
  • O worker investigator do RCA — a linha do report pousa como status=failed com status_reason="budget_exceeded".
  • O caminho de translate — cai para "translation unavailable (budget exceeded)" e o texto original é mostrado.

Caveats do InMemoryBudget

A implementação MVP é in-memory:

go
type InMemoryBudget struct {
    mu         sync.Mutex
    dailyLimit int            // tokens por dia UTC; <=0 significa ilimitado
    used       map[string]int // key = "YYYY-MM-DD" (UTC)
    now        func() time.Time
}

Consequências:

  • Sem persistência — um restart do manager reseta o counter do dia. Se você de fato quer um cap diário hard que sobreviva a restarts, troque a implementação. A interface BudgetChecker é a costura.
  • Single-process — se você rodar múltiplos managers atrás de um load balancer (não deveria ainda, mas se), cada um tem seu próprio counter.
  • Global, não por-useruserID flui pela interface para que uma futura tabela MySQL usage_daily seja um drop-in, mas hoje o cap é o mesmo número para todo mundo.

O pivô para single-tenant adiou o backend por-user; a interface é forward-compatible para quando multi-user voltar.

Estimativa de tokens

BudgetCallbackHandler.OnStart estima tokens de prompt por contagem de caracteres / 4. Isso é intencionalmente conservador — tokenização real varia por provider / modelo, e o budget deve errar do lado de recusar chamadas borderline em vez de passar.

Em OnEnd, o Usage.TotalTokens real retornado pelo provider é registrado — então o budget rastreia ground truth mesmo quando a estimativa estava errada.

Se o provider não retorna contagem de tokens (alguns endpoints custom não), o callback cai para uma heurística de response-meta; veja OnEndUsesResponseMetaFallback nos testes.

Observando o 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

As métricas estão conectadas por BudgetCallbackHandler.Stats(). O dashboard Prom de self-obs as renderiza como um gráfico de spend diário mais um alerta em 80% do cap.

Desabilitando para um workload

Não há knob "desabilita budget para o investigator". Se o RCA está batendo no cap e você prefere que continue rodando ao invés do chat, suba o cap — é para isso que está ali. A alternativa (quotas por-workload) está parqueada junto com multi-tenancy.

Veja também