feat(admin): rates + discovery flag in API

Extend GET /admin/api/overview's existing providers payload rather than
adding a sibling endpoint: the route already returns one providers array
per request on the dashboard's 5s poll, is already admin-gated and
stats-excluded, and the new fields are one more batched limiter read
sharing that same request — a second endpoint would duplicate all three
properties for no benefit.

adminProviderView gains AttemptsDay/FailuresDay/AttemptsMinute/
FailuresMinute (the provider's own current-window counters) and
ModelRates (a per-model breakdown, keyed by upstream model id, via the
new adminModelRateView) — Feature A. buildAdminOverview builds every
provider's and every (provider, model) pair's limitScope in two passes
and reads them all through limiter.providerUsage in ONE storeGetMulti
call, then slices the flat result back by index.

adminProviderView also gains DiscoveryEnabled, mirroring a new
providerSnapshot.discoveryEnabled field (registry.go) read straight off
providerState — it is set once at construction and never mutated
afterward, so it needs neither providerState's mutex nor a place in its
existing snapshot() method's mutable-field return shape. Feature B: this
lets the webui tell "discovery is off" apart from "discovery is on but
has not refreshed yet", which previously both showed the same zero
lastRefresh as an identical "refreshed never".
This commit is contained in:
2026-08-21 23:22:51 +01:00
parent aafd2a0433
commit bf23b41568
4 changed files with 317 additions and 18 deletions
+24 -7
View File
@@ -778,6 +778,22 @@ type providerSnapshot struct {
lastErr string
models []string
modelCount int
// discoveryEnabled mirrors providerState.discoveryEnabled (Feature B,
// v0.22 last-refresh label honesty): read directly rather than through
// providerState.snapshot(), because it is set once at construction
// (newModelRegistry, from ProviderConfig.Discovery) and never
// reassigned afterward — every other providerState field warmFill/
// maybeRefresh/finishRefresh mutate only reads it, never writes it — so
// it needs neither the mutex snapshot() takes for the fields that DO
// mutate
// (models/lastRefresh/lastErr) nor a place in that method's return
// shape. The admin dashboard needs it to tell "discovery is off" apart
// from "discovery is on but has not refreshed yet" — both currently
// read identically as a zero LastRefresh, which GET /admin/api/overview
// (admin.go) and the Providers tab (webui) previously rendered as the
// misleading "refreshed never" for a provider that will never refresh
// by design.
discoveryEnabled bool
}
// aliasSnapshotEntry is one configured alias's read-only view for the
@@ -822,13 +838,14 @@ func (m *modelRegistry) snapshot() []providerSnapshot {
// that class of trap entirely.
models, lastRefresh, lastErr := m.states[name].snapshot()
out = append(out, providerSnapshot{
name: adapter.name(),
typeName: adapter.typeName(),
baseURL: adapter.base(),
models: models,
modelCount: len(models),
lastRefresh: lastRefresh,
lastErr: lastErr,
name: adapter.name(),
typeName: adapter.typeName(),
baseURL: adapter.base(),
models: models,
modelCount: len(models),
lastRefresh: lastRefresh,
lastErr: lastErr,
discoveryEnabled: m.states[name].discoveryEnabled,
})
}
return out