mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-09-20 17:07:59 +00:00
feat: AUTO chain rewrite — silent failover+busy skip+pref round-robin+503 tier summary; chain edits reset slot cooldowns (P0/P1); stats by_status + audit jsonl rotation; UI priority-page health badges & status-code card; ctx-menu capture-phase close (outside-press guard); main.go ops warnings; local bundled-Lua verified tests (3 latent bugs fixed); plan.md
This commit is contained in:
@ -11,6 +11,7 @@ import (
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"llmsproxy/internal/config"
|
||||
@ -26,6 +27,7 @@ type Core struct {
|
||||
store *config.Store
|
||||
scheduler *scheduler.Scheduler
|
||||
registry *provider.Registry
|
||||
autoChain atomic.Pointer[scheduler.Chain]
|
||||
}
|
||||
|
||||
// New builds the core from a config file plus runtime overlay.
|
||||
@ -138,6 +140,11 @@ func (c *Core) Scheduler() *scheduler.Scheduler { return c.scheduler }
|
||||
|
||||
func (c *Core) Registry() *provider.Registry { return c.registry }
|
||||
|
||||
// AutoChain returns the current AUTO scheduling chain (immutable after build;
|
||||
// a rebuilt chain is swapped in atomically). nil before the first build or
|
||||
// when no auto slots could be resolved.
|
||||
func (c *Core) AutoChain() *scheduler.Chain { return c.autoChain.Load() }
|
||||
|
||||
func (c *Core) DefaultModel() string { return c.cfg.DefaultModel }
|
||||
|
||||
func (c *Core) GatewayKeys() []string { return c.cfg.GatewayKeys }
|
||||
@ -228,9 +235,34 @@ func cleanScopes(entries []config.ModelScope) []config.ModelScope {
|
||||
return clean
|
||||
}
|
||||
|
||||
// SaveAutoRules persists the AUTO scheduling slots.
|
||||
// SaveAutoRules persists the AUTO scheduling slots, rebuilds the chain and
|
||||
// clears the cooldown of every slot in it — preference scores are kept, so a
|
||||
// reliably good model keeps its edge while an edited chain applies
|
||||
// immediately. Providers are NOT rebuilt here (their per-model state survives
|
||||
// the edit, plan 2.4 lifecycle); rebuildRegistry covers source edits.
|
||||
func (c *Core) SaveAutoRules(entries []config.ModelScope) error {
|
||||
return c.store.SaveAutoRules(cleanScopes(entries))
|
||||
if err := c.store.SaveAutoRules(cleanScopes(entries)); err != nil {
|
||||
return err
|
||||
}
|
||||
c.buildAutoChain()
|
||||
if ch := c.autoChain.Load(); ch != nil {
|
||||
for _, tn := range ch.Tiers {
|
||||
for _, sl := range tn.Slots {
|
||||
if p := c.registry.ProviderForSlot(sl.Model, sl.Source); p != nil {
|
||||
p.ResetModelCooldown(sl.Model)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResetHealth clears the scheduling backoff state of every provider (admin
|
||||
// UI action). Unlike SaveAutoRules this does not touch the chain itself.
|
||||
func (c *Core) ResetHealth() {
|
||||
for _, p := range c.registry.Providers() {
|
||||
p.ResetHealth()
|
||||
}
|
||||
}
|
||||
|
||||
// Registry resolves model -> owning provider.
|
||||
@ -303,9 +335,88 @@ func (c *Core) rebuildRegistry() error {
|
||||
} else {
|
||||
c.registry.Replace(providers)
|
||||
}
|
||||
c.buildAutoChain()
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildAutoChain rebuilds the AUTO chain snapshot from the persisted rules
|
||||
// against the current providers. Slots whose (model, source) no longer exists
|
||||
// and image-kind models are dropped; a chain with no slots makes AUTO
|
||||
// requests answer "no auto slot configured".
|
||||
func (c *Core) buildAutoChain() {
|
||||
prov := func(model, source string) scheduler.Provider {
|
||||
p := c.registry.ProviderForSlot(model, source)
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
if m := p.ModelByID(model); m != nil && m.Kind == "image" {
|
||||
return nil
|
||||
}
|
||||
return p
|
||||
}
|
||||
rules := c.store.AutoRules()
|
||||
sr := make([]scheduler.Rule, 0, len(rules))
|
||||
for _, e := range rules {
|
||||
r := scheduler.Rule{
|
||||
Model: e.Model,
|
||||
Source: e.Source,
|
||||
Tier: e.Tier,
|
||||
Quota: e.TokenQuota,
|
||||
Period: e.Period,
|
||||
Hours: e.Hours,
|
||||
}
|
||||
if r.Source == "" {
|
||||
// canonicalize to the owning source so summaries/audit/quota
|
||||
// windows always carry a real source name
|
||||
if p := c.registry.ProviderForSlot(e.Model, ""); p != nil {
|
||||
r.Source = p.Name()
|
||||
}
|
||||
}
|
||||
sr = append(sr, r)
|
||||
}
|
||||
c.autoChain.Store(scheduler.BuildChain(sr, prov))
|
||||
}
|
||||
|
||||
// AutoSlotState is the UI-facing health snapshot of one AUTO chain slot.
|
||||
type AutoSlotState struct {
|
||||
Model string `json:"model"`
|
||||
Source string `json:"source"`
|
||||
Pref int64 `json:"pref"`
|
||||
FailCount int64 `json:"fail_count"`
|
||||
CooldownUntil int64 `json:"cooldown_until"`
|
||||
Cooling bool `json:"cooling"`
|
||||
}
|
||||
|
||||
// AutoSlotStates returns per-slot health (preference, failure count,
|
||||
// cooldown) for every slot of the current AUTO chain, mirroring the chain
|
||||
// order so the priority-page UI can annotate its blocks.
|
||||
func (c *Core) AutoSlotStates() []AutoSlotState {
|
||||
ch := c.autoChain.Load()
|
||||
if ch == nil {
|
||||
return nil
|
||||
}
|
||||
now := time.Now().Unix()
|
||||
var out []AutoSlotState
|
||||
for _, tn := range ch.Tiers {
|
||||
for _, sl := range tn.Slots {
|
||||
pp, ok := sl.Prov.(*provider.Provider)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
pref, fail, until := pp.ModelHealthInfo(sl.Model)
|
||||
out = append(out, AutoSlotState{
|
||||
Model: sl.Model,
|
||||
Source: sl.Source,
|
||||
Pref: pref,
|
||||
FailCount: fail,
|
||||
CooldownUntil: until,
|
||||
Cooling: until > now,
|
||||
})
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Reload re-reads the runtime store and rebuilds sources (adapter reload is not
|
||||
// strictly needed since adapters are loaded into the VM at startup; uploaded
|
||||
// adapters are placed in the adapter dir and loaded by the web UI).
|
||||
@ -408,4 +519,4 @@ func (c *Core) Close() {
|
||||
if c.vm != nil {
|
||||
c.vm.Stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user