b424261aca
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>
98 lines
2.7 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|