Files
executus/model/bench.go
steve b424261aca
executus CI / test (pull_request) Successful in 58s
Adversarial Review (Gadfly) / review (pull_request) Successful in 26m27s
executus CI / test (push) Successful in 1m2s
P1: model layer (convar->config inversion) + llmmeta
Lifts mort's pkg/logic/llms into executus/model, decoupled from mort:

- tiers.go: the tier resolver now reads a host-supplied config.Source under
  "model.tier.<name>" with host-supplied fallbacks (Configure(cfg, defaults,
  ttl)), instead of convar.Manager. Tier NAMES + specs are host config; the
  resolution mechanism (cache, reasoning-suffix dialect, chain validation) is
  generic. No tier names hard-coded in the harness.
- sink.go: usage/trace recording inverted off mort's llmusage/llmtrace into
  UsageSink / TraceSink seams + a model-owned Span, with nil-safe context
  attribution helpers (WithModel/WithTraceID/WithUsageTool/WithUsageUser).
  Both sinks optional (nil = off) so a light host records nothing.
- lane decoration repointed to executus/lane; utils.Errorf -> fmt.Errorf.
- call.go keeps GenerateWith[T] (instrumented structured output) — this is the
  structured-output primitive; no separate structured/ package.
- llmmeta moved over model/ (the meta-LLM helper: tier allowlist + JSON retry
  + ledger). Its tests configure a minimal tier table via TestMain.

New tests cover the inversion: config overrides fallback, tier registration,
reasoning-suffix survival, nested-tier rejection, nil-sink no-ops.

Full module: go build/vet/test -race green; core go.sum still free of
gorm/redis/discordgo/sqlite.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 19:47:13 -04:00

98 lines
2.7 KiB
Go

// Package llms — bench.go: the mort-flavored facade over majordomo's
// health tracker for the `.failover` Discord commands and the failover
// web UI.
//
// Why a facade (vs exposing health.Tracker directly): the admin surfaces
// want the historical shape — a benched-only list with a manual/auto
// flag. majordomo's tracker treats manual benches (Bench) and automatic
// backoffs identically, so the manual marker is kept mort-side.
package model
import (
"sync"
"time"
)
// BenchedModel is one currently-benched model for admin display.
type BenchedModel struct {
// Model is the "provider/model" target key.
Model string
// Until is the end of the bench window.
Until time.Time
// ConsecutiveFails is the failure count since the last success.
ConsecutiveFails int
// Manual reports the bench was placed by an operator (BenchModel)
// rather than the automatic failure threshold.
Manual bool
}
var (
manualMu sync.Mutex
manualBenches = map[string]time.Time{}
)
// ListBenched returns the currently-benched models, manual and automatic,
// from the live health tracker.
func ListBenched() []BenchedModel {
now := time.Now()
pruneManual(now)
var out []BenchedModel
for _, st := range Health().Snapshot() {
if !st.Until.After(now) {
continue
}
out = append(out, BenchedModel{
Model: st.Key,
Until: st.Until,
ConsecutiveFails: st.ConsecutiveFailures,
Manual: isManual(st.Key, st.Until),
})
}
return out
}
// BenchModel manually benches a model spec until the given time. The
// chain executor skips benched targets until the window expires (or
// UnbenchModel clears it).
func BenchModel(model string, until time.Time) {
Health().Bench(model, until)
manualMu.Lock()
manualBenches[model] = until
manualMu.Unlock()
}
// UnbenchModel clears the bench on a model. Returns true when the model
// was actually benched.
func UnbenchModel(model string) bool {
now := time.Now()
wasBenched := Health().BackedOffUntil(model).After(now)
Health().Unbench(model)
manualMu.Lock()
delete(manualBenches, model)
manualMu.Unlock()
return wasBenched
}
// isManual reports whether the bench window for key matches a manual
// bench placed via BenchModel. An automatic backoff that outlives the
// manual window supersedes the marker.
func isManual(key string, until time.Time) bool {
manualMu.Lock()
defer manualMu.Unlock()
manualUntil, ok := manualBenches[key]
return ok && !until.After(manualUntil)
}
// pruneManual drops expired manual markers so the map can't grow
// unbounded across a long uptime.
func pruneManual(now time.Time) {
manualMu.Lock()
defer manualMu.Unlock()
for k, until := range manualBenches {
if !until.After(now) {
delete(manualBenches, k)
}
}
}