Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions internal/gateway/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -849,6 +849,11 @@ func (s *Server) adminModelVisibility(w http.ResponseWriter, r *http.Request) {
writeJSON(w, 500, map[string]any{"ok": false, "error": err.Error()})
return
}
// Keep the lane's probe + failover filters in sync: a newly hidden
// model must stop being probed and must not be auto-picked as a
// failover/rate-limit-switch target (issue #103). An explicit request
// naming it still reaches the model.
s.Lane.SetHidden(out)
if s.logger != nil {
action := "恢复显示"
if in.Hidden {
Expand Down
5 changes: 5 additions & 0 deletions internal/gateway/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,11 @@ func New(l *lane.Lane, st *store.Store) *Server {
// see the raw catalog.
l.OnCatalogReload = s.applyStoredTags
s.applyStoredTags()
// Seed the lane's hidden-model set from the persisted config so models the
// user unchecked on the 模型 page are skipped by probing and failover from
// the first boot (issue #103). adminModelVisibility keeps it in sync on
// every toggle.
s.Lane.SetHidden(st.Config().HiddenModels)
return s
}

Expand Down
30 changes: 30 additions & 0 deletions internal/lane/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ type Lane struct {
egress Egress
defaultMaxTokens int
exposeRegion bool
hidden map[string]bool // model ids the user unchecked on the 模型 page; skipped by probing and failover
failoverEnabled bool
failoverMax int
smartRouting bool
Expand Down Expand Up @@ -319,6 +320,22 @@ func (l *Lane) SetExposeRegion(b bool) {
l.mu.Unlock()
}

// SetHidden installs the set of model ids the user unchecked on the 模型 page
// (store.Config.HiddenModels). A hidden model is still reachable by an explicit
// request naming it, but the lane must not spend a probe slot on it nor pick it
// as an automatic failover/rate-limit-switch target (issue #103). The whole map
// is swapped, never mutated in place, so concurrent reads in ProbeRound and
// nextCandidate stay safe without holding the lock for the entire scan.
func (l *Lane) SetHidden(ids []string) {
set := make(map[string]bool, len(ids))
for _, id := range ids {
set[id] = true
}
l.mu.Lock()
l.hidden = set
l.mu.Unlock()
}

// ProbingModel returns the id currently under probe ("" when idle).
func (l *Lane) ProbingModel() string {
l.mu.RLock()
Expand Down Expand Up @@ -514,6 +531,7 @@ func (l *Lane) ProbeRound(ctx context.Context, manual bool) {
l.probing.Set(true)
cat := make([]ModelInfo, len(l.catalog))
copy(cat, l.catalog)
hidden := l.hidden
l.mu.Unlock()
defer l.probing.Set(false)

Expand All @@ -526,6 +544,11 @@ func (l *Lane) ProbeRound(ctx context.Context, manual bool) {
if ctx.Err() != nil {
break
}
if hidden[m.ID] {
// User unchecked this model on the 模型 page: don't burn a probe
// slot on it (issue #103). Explicit named calls still serve it.
continue
}
if m.Channel == ChannelKilo {
l.mu.Lock()
prev, had := l.availability[m.ID]
Expand Down Expand Up @@ -916,13 +939,20 @@ func (l *Lane) nextCandidate(needs Needs, tried map[string]bool) string {
smart := l.smartRouting
strategy := l.strategy
ttftFn := l.ttft
hidden := l.hidden
l.mu.RUnlock()

best, bestKey := "", candidateKey{avail: 99}
for i, m := range cat {
if m.SystemOne || m.Wire == "systemone" {
continue
}
if hidden[m.ID] {
// User unchecked this model on the 模型 page: never auto-pick it
// as a failover/rate-limit-switch target (issue #103). An explicit
// request naming it still goes straight to the model.
continue
}
if tried[m.ID] || l.throttle.Throttled(m.ID) {
continue
}
Expand Down
32 changes: 32 additions & 0 deletions internal/lane/hidden_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package lane

import "testing"

// TestNextCandidateSkipsHiddenModels covers issue #103: a model the user
// unchecked on the 模型 page must never be auto-picked as a failover /
// rate-limit-switch target. Without the hidden filter, nextCandidate would
// return space-bunny-free (the first catalog row) here.
func TestNextCandidateSkipsHiddenModels(t *testing.T) {
l := strategyLane()
l.SetHidden([]string{"space-bunny-free"})

got := l.nextCandidate(Needs{}, map[string]bool{})
if got == "space-bunny-free" {
t.Fatalf("nextCandidate picked a hidden model: %s", got)
}
if got == "" {
t.Fatalf("nextCandidate returned nothing although unhidden models exist")
}
}

// TestNextCandidateStillServesAfterUnhide confirms clearing the hidden set
// restores the model to the candidate pool.
func TestNextCandidateStillServesAfterUnhide(t *testing.T) {
l := strategyLane()
l.SetHidden([]string{"space-bunny-free"})
l.SetHidden(nil) // unhide everything

if got := l.nextCandidate(Needs{}, map[string]bool{}); got != "space-bunny-free" {
t.Fatalf("after unhide, expected space-bunny-free, got %s", got)
}
}
Loading