-
-
Notifications
You must be signed in to change notification settings - Fork 88
fix(providers): hide offline providers' models from model listings #717
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
efcbebf
84db8f7
56f5d5c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1191,6 +1191,93 @@ func TestInitialize_StaleProviderLosesBareModelIDToHealthyDuplicate(t *testing.T | |
| } | ||
| } | ||
|
|
||
| // A provider that goes offline must disappear from every model listing | ||
| // (GET /v1/models, dashboard model list, category counts) while its | ||
| // carried-forward inventory stays resolvable for direct requests, and it must | ||
| // reappear once the provider recovers (issue #705). | ||
| func TestStaleProviderModelsAreNotAdvertised(t *testing.T) { | ||
| registry, _, beta := registerTwoProviderRegistry(t) | ||
|
|
||
| listedIDs := func(t *testing.T) map[string]bool { | ||
| t.Helper() | ||
| ids := make(map[string]bool) | ||
| for _, model := range registry.ListPublicModels() { | ||
| ids["public:"+model.ID] = true | ||
| } | ||
| for _, entry := range registry.ListModelsWithProvider() { | ||
| ids["provider:"+entry.Selector] = true | ||
| } | ||
| for _, model := range registry.ListModels() { | ||
| ids["bare:"+model.ID] = true | ||
| } | ||
| return ids | ||
| } | ||
|
|
||
| categorySelectors := func(t *testing.T) map[string]bool { | ||
| t.Helper() | ||
| selectors := make(map[string]bool) | ||
| for _, entry := range registry.ListModelsWithProviderByCategory(core.CategoryEmbedding) { | ||
| selectors[entry.Selector] = true | ||
| } | ||
| return selectors | ||
| } | ||
|
|
||
| before := listedIDs(t) | ||
| for _, key := range []string{"public:beta/beta-model", "provider:beta/beta-model", "bare:beta-model"} { | ||
| if !before[key] { | ||
| t.Fatalf("%s missing from listings while beta is healthy", key) | ||
| } | ||
| } | ||
| if !categorySelectors(t)["beta/beta-model"] { | ||
| t.Fatal("beta/beta-model missing from embedding category while beta is healthy") | ||
| } | ||
|
|
||
| beta.err = errors.New("connection refused") | ||
| if err := registry.Initialize(context.Background()); err != nil { | ||
| t.Fatalf("refresh Initialize() error = %v", err) | ||
| } | ||
|
|
||
| after := listedIDs(t) | ||
| for _, key := range []string{"public:beta/beta-model", "provider:beta/beta-model", "bare:beta-model"} { | ||
| if after[key] { | ||
| t.Errorf("%s still advertised after beta went offline, want hidden", key) | ||
| } | ||
| } | ||
| for _, key := range []string{"public:alpha/alpha-model", "provider:alpha/alpha-model", "bare:alpha-model"} { | ||
| if !after[key] { | ||
| t.Errorf("%s missing from listings, want healthy provider unaffected", key) | ||
| } | ||
| } | ||
| for _, counts := range registry.GetCategoryCounts() { | ||
| if counts.Category == core.CategoryAll && counts.Count != 1 { | ||
| t.Errorf("GetCategoryCounts()[all] = %d with beta offline, want 1", counts.Count) | ||
| } | ||
| } | ||
| afterCategory := categorySelectors(t) | ||
| if afterCategory["beta/beta-model"] { | ||
| t.Error("beta/beta-model still in embedding category after beta went offline, want hidden") | ||
| } | ||
| if !afterCategory["alpha/alpha-model"] { | ||
| t.Error("alpha/alpha-model missing from embedding category, want healthy provider unaffected") | ||
|
Comment on lines
+1256
to
+1261
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Assert the embedding category count after the outage. The test checks As per coding guidelines: 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
| } | ||
| // Direct requests must still resolve the carried inventory (honest 502 at | ||
| // the provider instead of "model not found"). | ||
| if !registry.Supports("beta/beta-model") { | ||
| t.Error("Supports(beta/beta-model) = false, want carried inventory still resolvable") | ||
| } | ||
|
|
||
| beta.err = nil | ||
| if err := registry.Initialize(context.Background()); err != nil { | ||
| t.Fatalf("recovery Initialize() error = %v", err) | ||
| } | ||
| recovered := listedIDs(t) | ||
| for _, key := range []string{"public:beta/beta-model", "provider:beta/beta-model", "bare:beta-model"} { | ||
| if !recovered[key] { | ||
| t.Errorf("%s missing from listings after recovery, want advertised again", key) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // The fast recheck loop re-probes only providers whose latest refresh failed, | ||
| // so a recovered provider is picked up within the recheck interval instead of | ||
| // waiting for the next full refresh. | ||
|
|
@@ -1245,7 +1332,13 @@ func registerTwoProviderRegistry(t *testing.T) (*ModelRegistry, *registryMockPro | |
| singleModel := func(owner string) *core.ModelsResponse { | ||
| return &core.ModelsResponse{ | ||
| Object: "list", | ||
| Data: []core.Model{{ID: owner + "-model", Object: "model", OwnedBy: owner}}, | ||
| Data: []core.Model{{ | ||
| ID: owner + "-model", Object: "model", OwnedBy: owner, | ||
| Metadata: &core.ModelMetadata{ | ||
| Modes: []string{"embedding"}, | ||
| Categories: []core.ModelCategory{core.CategoryEmbedding}, | ||
| }, | ||
| }}, | ||
| } | ||
| } | ||
| alpha := ®istryMockProvider{name: "alpha", modelsResponse: singleModel("alpha")} | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When one provider refresh fails, its prior inventory remains available for direct provider-qualified routing in the running process. However, this
continueexcludes that inventory from the replacement cache. If the gateway then restarts while the provider is still unavailable and no configured model list supplies the model, the restarted registry cannot resolve requests such asbeta/beta-model; they return model-not-found rather than reaching the provider and producing the intended upstream availability response. Persist stale inventories with their stale state and restore them as non-advertised and non-failover candidates, rather than removing them entirely.Artifacts
Focused Go stale-inventory restart reproduction source
Provider-qualified routing before saving the partial-outage cache
Provider-qualified routing after saving the partial-outage cache
Existing stale-inventory persistence test output
Line-numbered focused reproduction source capture
Review worktree validation output
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Verified end-to-end that this scenario does not produce model-not-found: the reproduction drives
ModelRegistrydirectly and bypasses the router's request-time provider refresh.For a provider-qualified request whose model is missing from the registry,
Router.resolveProvidercallsrefreshProviderModelsForRequest(router.go), which probes the configured provider. While the provider is down this returns the provider-attributed 503 — the same honest degradation the in-process stale inventory provides — and once the provider recovers, the very first request repopulates the inventory and routes normally.Live check (gateway restarted while the provider is down, cache contains only the healthy provider):
Persisting stale inventories with a stale flag would add cache-format surface for no behavioral gain — and keeping offline providers' models out of the cache is exactly what #705 asks for (phantom models were resurrecting from the cache on every restart).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right. My concern rested on the assumption that a missing-registry model returns model-not-found at request time, but
refreshProviderModelsForRequestinrouter.goshort-circuits that path for provider-qualified requests: the router probes the provider directly, so the post-restart behavior is identical — 503 while down, automatic repopulation on recovery. The stale flag in the cache would be pure overhead with no behavioral difference.The original comment stands withdrawn; the implementation is correct.