mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-10-26 17:41:33 +08:00
Stop RegisterModels from publishing a synthetic plugin client for plugins without an executor, which advertised their models without any routable credential and bypassed per-auth model exclusions. Retain the static catalog only for executor arbitration and per-auth registration, so public availability now follows active credentials and their exclusions. Reserve native provider models before plugin arbitration to preserve native priority, and remove the now unused model client commit path.
125 lines
4.9 KiB
Go
125 lines
4.9 KiB
Go
package pluginhost
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/router-for-me/CLIProxyAPI/v8/internal/registry"
|
|
"github.com/router-for-me/CLIProxyAPI/v8/sdk/pluginapi"
|
|
)
|
|
|
|
func TestRegisterExecutorsPreservesNativeCatalogPriority(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
nativeExecutor bool
|
|
extraPluginModel bool
|
|
}{
|
|
{name: "native catalog blocks competing executor", nativeExecutor: true},
|
|
{name: "native catalog only blocks overlapping model", nativeExecutor: true, extraPluginModel: true},
|
|
{name: "catalog without native executor does not reserve model"},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
modelRegistry := newFakeModelRegistry()
|
|
manager := newFakeExecutorManager()
|
|
native := &fakeProviderExecutor{provider: "native-provider"}
|
|
if tc.nativeExecutor {
|
|
manager.RegisterExecutor(native)
|
|
}
|
|
// The existing credential has not yet registered the new catalog model.
|
|
modelRegistry.RegisterClient("native-auth", "native-provider", []*registry.ModelInfo{{ID: "native-base"}})
|
|
pluginModels := []pluginapi.ModelInfo{{ID: "shared-model"}}
|
|
if tc.extraPluginModel {
|
|
pluginModels = append(pluginModels, pluginapi.ModelInfo{ID: "plugin-only"})
|
|
}
|
|
catalog := capabilityRecord{
|
|
id: "native-catalog",
|
|
priority: 10,
|
|
plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{
|
|
ModelRegistrar: staticModelRegistrar("native-provider", "shared-model"),
|
|
}},
|
|
}
|
|
competitor := capabilityRecord{
|
|
id: "competing-executor",
|
|
priority: 20,
|
|
plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{
|
|
ModelRegistrar: modelRegistrarFunc(func(context.Context, pluginapi.ModelRegistrationRequest) (pluginapi.ModelRegistrationResponse, error) {
|
|
return pluginapi.ModelRegistrationResponse{Provider: "plugin-provider", Models: pluginModels}, nil
|
|
}),
|
|
Executor: &fakeExecutor{identifier: "plugin-provider"},
|
|
}},
|
|
}
|
|
host := newHostWithRecords(competitor, catalog)
|
|
for _, stage := range []struct {
|
|
name string
|
|
publishModel bool
|
|
}{
|
|
{name: "first load before credential refresh"},
|
|
{name: "reload before credential refresh"},
|
|
{name: "reload after credential refresh", publishModel: true},
|
|
{name: "reload after credential withdraws model"},
|
|
} {
|
|
nativeModels := []*registry.ModelInfo{{ID: "native-base"}}
|
|
if stage.publishModel && tc.nativeExecutor {
|
|
nativeModels = append(nativeModels, ®istry.ModelInfo{ID: "shared-model"})
|
|
}
|
|
modelRegistry.RegisterClient("native-auth", "native-provider", nativeModels)
|
|
host.RegisterModels(t.Context(), modelRegistry)
|
|
host.RegisterExecutors(manager, modelRegistry)
|
|
|
|
if tc.nativeExecutor {
|
|
if got, _ := manager.Executor("native-provider"); got != native {
|
|
t.Fatalf("%s: native executor was replaced", stage.name)
|
|
}
|
|
}
|
|
wantExecutor := !tc.nativeExecutor || tc.extraPluginModel
|
|
if _, got := manager.Executor("plugin-provider"); got != wantExecutor {
|
|
t.Fatalf("%s: competing executor registered = %t, want %t", stage.name, got, wantExecutor)
|
|
}
|
|
gotModels := host.ModelsForProvider("plugin-provider")
|
|
wantCount := 0
|
|
if !tc.nativeExecutor {
|
|
wantCount++
|
|
}
|
|
if tc.extraPluginModel {
|
|
wantCount++
|
|
}
|
|
if len(gotModels) != wantCount {
|
|
t.Fatalf("%s: competing catalog has %d models, want %d", stage.name, len(gotModels), wantCount)
|
|
}
|
|
for _, model := range gotModels {
|
|
if tc.nativeExecutor && model.ID == "shared-model" {
|
|
t.Fatalf("%s: competing catalog claimed native shared-model", stage.name)
|
|
}
|
|
}
|
|
if models := host.ModelsForProvider("native-provider"); len(models) != 1 || models[0].ID != "shared-model" {
|
|
t.Fatalf("%s: native source catalog was not retained", stage.name)
|
|
}
|
|
if _, published := modelRegistry.clients["plugin:native-catalog:native-provider"]; published {
|
|
t.Fatalf("%s: catalog-only plugin independently published models", stage.name)
|
|
}
|
|
providers := modelRegistry.GetModelProviders("shared-model")
|
|
wantProvider := ""
|
|
if !tc.nativeExecutor {
|
|
wantProvider = "plugin-provider"
|
|
} else if stage.publishModel {
|
|
wantProvider = "native-provider"
|
|
}
|
|
if wantProvider == "" && len(providers) != 0 || wantProvider != "" && (len(providers) != 1 || providers[0] != wantProvider) {
|
|
t.Fatalf("%s: shared-model providers = %v, want %q", stage.name, providers, wantProvider)
|
|
}
|
|
}
|
|
|
|
// Removing the catalog releases ownership on the next arbitration pass.
|
|
setHostSnapshotForTest(host, true, competitor)
|
|
host.RegisterModels(t.Context(), modelRegistry)
|
|
host.RegisterExecutors(manager, modelRegistry)
|
|
if _, ok := manager.Executor("plugin-provider"); !ok {
|
|
t.Fatal("competing executor was not registered after native catalog removal")
|
|
}
|
|
if models := host.ModelsForProvider("plugin-provider"); len(models) != len(pluginModels) {
|
|
t.Fatalf("competing models after catalog removal = %d, want %d", len(models), len(pluginModels))
|
|
}
|
|
})
|
|
}
|
|
}
|