Skip to content

Budget & límites

Ongrid refuerza un tope global por UTC-día de tokens a través de cada provider. El default es ilimitado; una env var lo enciende:

bash
ONGRID_LLM_DAILY_TOKEN_LIMIT=2000000   # 2 million tokens per UTC day

<=0 deshabilita el tope. Un único valor, no por-provider — este es el scope MVP. Cuando aterricen los tenants se mueve a settings por-org; esta perilla se queda como un tope global de safety-net.

Cómo está cableado

Tres piezas, en 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)

El runtime del graph-kernel instala el callback handler en su cadena de callbacks de eino. En cada OnStart de ChatModel:

  1. Estima tokens de prompt: len(text)/4 (conservador).
  2. BudgetChecker.Check(ctx, userID, estPromptTokens).
  3. Ante rechazo — almacena ErrBudgetExceeded en el contexto para que el nodo downstream pueda cortocircuitar; el código subsiguiente lo emerge.

En OnEnd, el Usage.TotalTokens real se graba contra el bucket de UTC-día actual.

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
}

El error se propaga a:

  • El endpoint de chat send — devuelve HTTP 429 con un body { "error": "budget_exceeded", "message": "..." } que la UI de chat renderiza in-line.
  • El worker investigator RCA — la fila de informe aterriza como status=failed con status_reason="budget_exceeded".
  • La ruta translate — cae a "translation unavailable (budget exceeded)" y se muestra el texto original.

Caveats de InMemoryBudget

La implementación MVP es 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
}

Consecuencias:

  • Sin persistencia — un reinicio del manager resetea el contador del día. Si realmente quieres un tope diario duro que sobreviva a reinicios, swap la implementación. La interfaz BudgetChecker es el seam.
  • Single-process — si corres múltiples managers detrás de un load balancer (no deberías aún, pero si), cada uno tiene su propio contador.
  • Global, no por-usuariouserID fluye a través de la interfaz para que una futura tabla MySQL usage_daily sea drop-in, pero hoy el tope es el mismo número para todos.

El pivot a single-tenant aplazó el backend por-usuario; la interfaz es forward-compatible para cuando vuelva multi-usuario.

Estimación de tokens

BudgetCallbackHandler.OnStart estima tokens de prompt por conteo de caracteres / 4. Esto es intencionalmente conservador — la tokenización real varía por provider / modelo, y el budget está supuesto a errar del lado de rechazar llamadas borderline en vez de pasarse.

En OnEnd, el Usage.TotalTokens real devuelto por el provider se graba — así que el budget trackea ground truth incluso cuando la estimación fue off.

Si el provider no devuelve conteos de token (algunos endpoints custom no lo hacen), el callback cae a una heurística de response- meta; ver OnEndUsesResponseMetaFallback en los tests.

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

Las métricas las cablea BudgetCallbackHandler.Stats(). El dashboard de self-obs Prom las renderiza como un gráfico de gasto diario más una alerta al 80% del tope.

Deshabilitando para un workload

No hay perilla "deshabilitar budget para el investigator". Si RCA está pegando el tope y prefieres que siga corriendo en lugar del chat, sube el tope — para eso está. La alternativa (cuotas por-workload) está parqueada junto con multi-tenancy.

Ver también