package pluginhost import ( "bytes" "context" "errors" "fmt" "io" "net/http" "net/url" "sort" "strings" "testing" "time" "github.com/router-for-me/CLIProxyAPI/v7/internal/config" "github.com/router-for-me/CLIProxyAPI/v7/internal/registry" sdkaccess "github.com/router-for-me/CLIProxyAPI/v7/sdk/access" coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" coreexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" coreusage "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/usage" "github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginabi" "github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginapi" sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator") func TestPluginModelInfoToRegistryModelInfoClonesThinkingAndSlices(t *testing.T) { model := pluginapi.ModelInfo{ ID: "model-1", Object: "model", Created: 123, OwnedBy: "owner", Type: "plugin", DisplayName: "Model One", Name: "provider-model", Version: "v1", Description: "desc", InputTokenLimit: 100, OutputTokenLimit: 200, SupportedGenerationMethods: []string{"generate"}, ContextLength: 300, MaxCompletionTokens: 400, SupportedParameters: []string{"temperature"}, SupportedInputModalities: []string{"text"}, SupportedOutputModalities: []string{"image"}, Thinking: &pluginapi.ThinkingSupport{ Min: 1, Max: 2, ZeroAllowed: true, DynamicAllowed: true, Levels: []string{"low", "high"}, }, UserDefined: true, } got := pluginModelInfoToRegistryModelInfo(model) if got.ID != model.ID || got.Object != model.Object || got.Created != model.Created || got.OwnedBy != model.OwnedBy || got.Type != model.Type || got.DisplayName != model.DisplayName || got.Name != model.Name || got.Version != model.Version || got.Description != model.Description || got.InputTokenLimit != int(model.InputTokenLimit) || got.OutputTokenLimit != int(model.OutputTokenLimit) || got.ContextLength != int(model.ContextLength) || got.MaxCompletionTokens != int(model.MaxCompletionTokens) || !got.UserDefined { t.Fatalf("converted model = %#v, want fields copied from %#v", got, model) } if got.Thinking == nil { t.Fatal("Thinking = nil, want converted thinking support") } if got.Thinking.Min != 1 || got.Thinking.Max != 2 || !got.Thinking.ZeroAllowed || !got.Thinking.DynamicAllowed || fmt.Sprint(got.Thinking.Levels) != "[low high]" { t.Fatalf("Thinking = %#v, want copied thinking support", got.Thinking) } model.SupportedGenerationMethods[0] = "mutated" model.SupportedParameters[0] = "mutated" model.SupportedInputModalities[0] = "mutated" model.SupportedOutputModalities[0] = "mutated" model.Thinking.Levels[0] = "mutated" if got.SupportedGenerationMethods[0] != "generate" || got.SupportedParameters[0] != "temperature" || got.SupportedInputModalities[0] != "text" || got.SupportedOutputModalities[0] != "image" || got.Thinking.Levels[0] != "low" { t.Fatalf("converted model kept aliases to plugin slices: %#v", got) }} func TestExecutorNativeStreamResponseTranslatorExistsRequiresStreamTransform(t *testing.T) { outputFormat := sdktranslator.Format("plugin-output-non-stream-only") requestedFormat := sdktranslator.Format("client-output-non-stream-only") sdktranslator.Register(requestedFormat, outputFormat, nil, sdktranslator.ResponseTransform{ NonStream: func(ctx context.Context, model string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []byte { return rawJSON }, }) if executorNativeStreamResponseTranslatorExists(outputFormat, requestedFormat) { t.Fatal("non-stream-only response transformer was accepted for stream executor output") } streamOutputFormat := sdktranslator.Format("plugin-output-stream") streamRequestedFormat := sdktranslator.Format("client-output-stream") sdktranslator.Register(streamRequestedFormat, streamOutputFormat, nil, sdktranslator.ResponseTransform{ Stream: func(ctx context.Context, model string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte { return [][]byte{rawJSON} }, }) if !executorNativeStreamResponseTranslatorExists(streamOutputFormat, streamRequestedFormat) { t.Fatal("stream response transformer was not accepted for stream executor output") }} func TestRegisterModelsRegistersProviderModelsAndClientID(t *testing.T) { modelRegistry := newFakeModelRegistry() host := newHostWithRecords(capabilityRecord{ id: "alpha", meta: pluginapi.Metadata{Name: "Alpha", Version: "1.0.0"}, plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{ ModelRegistrar: modelRegistrarFunc(func(ctx context.Context, req pluginapi.ModelRegistrationRequest) (pluginapi.ModelRegistrationResponse, error) { if req.Plugin.Name != "Alpha" || req.Plugin.Version != "1.0.0" { t.Fatalf("RegisterModels request plugin = %#v, want Alpha metadata", req.Plugin) } return pluginapi.ModelRegistrationResponse{ Provider: " MixedProvider ", Models: []pluginapi.ModelInfo{{ ID: " model-1 ", Object: "model", Created: 123, OwnedBy: "owner", Type: "chat", DisplayName: "Model One", Name: "native-model-1", Version: "v1", Description: "description", InputTokenLimit: 100, OutputTokenLimit: 200, SupportedGenerationMethods: []string{"generate"}, ContextLength: 300, MaxCompletionTokens: 400, SupportedParameters: []string{"temperature"}, SupportedInputModalities: []string{"text"}, SupportedOutputModalities: []string{"text"}, Thinking: &pluginapi.ThinkingSupport{ Min: 1, Max: 2, ZeroAllowed: true, DynamicAllowed: true, Levels: []string{"low"}, }, UserDefined: true, }}, }, nil }), }}, }) host.RegisterModels(context.Background(), modelRegistry) reg := modelRegistry.clients["plugin:alpha:mixedprovider"] if reg == nil { t.Fatal("plugin:alpha:mixedprovider was not registered") } if reg.provider != "mixedprovider" { t.Fatalf("registered provider = %q, want mixedprovider", reg.provider) } if len(reg.models) != 1 { t.Fatalf("registered model count = %d, want 1", len(reg.models)) } model := reg.models[0] if model.ID != "model-1" || model.Object != "model" || model.Created != 123 || model.OwnedBy != "owner" || model.Type != "chat" || model.DisplayName != "Model One" || model.Name != "native-model-1" || model.Version != "v1" || model.Description != "description" || model.InputTokenLimit != 100 || model.OutputTokenLimit != 200 || model.ContextLength != 300 || model.MaxCompletionTokens != 400 || model.SupportedGenerationMethods[0] != "generate" || model.SupportedParameters[0] != "temperature" || model.SupportedInputModalities[0] != "text" || model.SupportedOutputModalities[0] != "text" || !model.UserDefined { t.Fatalf("registered model = %#v, want converted fields", model) } if model.Thinking == nil || model.Thinking.Min != 1 || model.Thinking.Max != 2 || !model.Thinking.ZeroAllowed || !model.Thinking.DynamicAllowed || model.Thinking.Levels[0] != "low" { t.Fatalf("registered thinking = %#v, want converted thinking", model.Thinking) }} func TestRegisterModelsUsesModelProviderStaticModels(t *testing.T) { modelRegistry := newFakeModelRegistry() called := false host := newHostWithRecords(capabilityRecord{ id: "alpha", meta: pluginapi.Metadata{Name: "Alpha", Version: "1.0.0"}, plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{ ModelProvider: modelProviderFunc{ staticModels: func(ctx context.Context, req pluginapi.StaticModelRequest) (pluginapi.ModelResponse, error) { called = true if req.Plugin.Name != "Alpha" || req.Plugin.Version != "1.0.0" { t.Fatalf("StaticModels request plugin = %#v, want Alpha metadata", req.Plugin) } if req.Host.AuthDir != "/tmp/plugin-auth" || req.Host.ProxyURL != "http://proxy.local" || !req.Host.ForceModelPrefix { t.Fatalf("StaticModels host = %#v, want configured summary", req.Host) } if len(req.Host.OAuthModelAlias["plugin-provider"]) != 1 || req.Host.OAuthModelAlias["plugin-provider"][0].Alias != "alias-model" { t.Fatalf("StaticModels OAuthModelAlias = %#v, want configured alias", req.Host.OAuthModelAlias) } if len(req.Host.ExcludedModels["plugin-provider"]) != 1 || req.Host.ExcludedModels["plugin-provider"][0] != "hidden-model" { t.Fatalf("StaticModels ExcludedModels = %#v, want configured exclusion", req.Host.ExcludedModels) } return pluginapi.ModelResponse{ Provider: " Plugin-Provider ", Models: []pluginapi.ModelInfo{{ ID: " model-static ", Object: "model", DisplayName: "Static Model", }}, }, nil }, }, ModelRegistrar: staticModelRegistrar("legacy-provider", "legacy-model"), }}, }) host.runtimeConfig = &config.Config{ SDKConfig: config.SDKConfig{ ProxyURL: "http://proxy.local", ForceModelPrefix: true, }, AuthDir: "/tmp/plugin-auth", OAuthModelAlias: map[string][]config.OAuthModelAlias{ "plugin-provider": []config.OAuthModelAlias{{Name: "upstream-model", Alias: "alias-model"}}, }, OAuthExcludedModels: map[string][]string{ "plugin-provider": []string{"hidden-model"}, }, } host.RegisterModels(context.Background(), modelRegistry) if !called { t.Fatal("ModelProvider.StaticModels was not called") } reg := modelRegistry.clients["plugin:alpha:plugin-provider"] if reg == nil { t.Fatal("plugin:alpha:plugin-provider was not registered") } if reg.provider != "plugin-provider" { t.Fatalf("registered provider = %q, want plugin-provider", reg.provider) } if len(reg.models) != 1 || reg.models[0].ID != "model-static" || reg.models[0].DisplayName != "Static Model" { t.Fatalf("registered models = %#v, want static model", reg.models) } if _, okLegacy := modelRegistry.clients["plugin:alpha:legacy-provider"]; okLegacy { t.Fatal("legacy ModelRegistrar path was used despite ModelProvider.StaticModels") }} func TestRegisterModelsSkipsErrorEmptyAndInvalidModels(t *testing.T) { modelRegistry := newFakeModelRegistry() host := newHostWithRecords( capabilityRecord{ id: "error", plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{ ModelRegistrar: modelRegistrarFunc(func(ctx context.Context, req pluginapi.ModelRegistrationRequest) (pluginapi.ModelRegistrationResponse, error) { return pluginapi.ModelRegistrationResponse{}, errors.New("register failed") }), }}, }, capabilityRecord{ id: "empty-provider", plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{ ModelRegistrar: modelRegistrarFunc(func(ctx context.Context, req pluginapi.ModelRegistrationRequest) (pluginapi.ModelRegistrationResponse, error) { return pluginapi.ModelRegistrationResponse{Provider: " ", Models: []pluginapi.ModelInfo{{ID: "model"}}}, nil }), }}, }, capabilityRecord{ id: "empty-models", plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{ ModelRegistrar: modelRegistrarFunc(func(ctx context.Context, req pluginapi.ModelRegistrationRequest) (pluginapi.ModelRegistrationResponse, error) { return pluginapi.ModelRegistrationResponse{Provider: "provider"}, nil }), }}, }, capabilityRecord{ id: "invalid-models", plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{ ModelRegistrar: modelRegistrarFunc(func(ctx context.Context, req pluginapi.ModelRegistrationRequest) (pluginapi.ModelRegistrationResponse, error) { return pluginapi.ModelRegistrationResponse{Provider: "provider", Models: []pluginapi.ModelInfo{{ID: " "}}}, nil }), }}, }, ) host.RegisterModels(context.Background(), modelRegistry) if len(modelRegistry.clients) != 0 { t.Fatalf("registered clients = %#v, want none", modelRegistry.clients) }} func TestRegisterModelsPrunesStaleClientAfterSnapshotChange(t *testing.T) { modelRegistry := newFakeModelRegistry() host := newHostWithRecords(capabilityRecord{ id: "alpha", plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{ ModelRegistrar: staticModelRegistrar("provider-a", "model-a"), }}, }) host.RegisterModels(context.Background(), modelRegistry) setHostSnapshotForTest(host, true, capabilityRecord{ id: "bravo", plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{ ModelRegistrar: staticModelRegistrar("provider-b", "model-b"), }}, }) host.RegisterModels(context.Background(), modelRegistry) if _, okClient := modelRegistry.clients["plugin:alpha:provider-a"]; okClient { t.Fatal("stale alpha client is still registered") } if modelRegistry.unregisters[0] != "plugin:alpha:provider-a" { t.Fatalf("unregistered clients = %#v, want alpha client first", modelRegistry.unregisters) } if _, okClient := modelRegistry.clients["plugin:bravo:provider-b"]; !okClient { t.Fatal("bravo client was not registered") }} func TestRegisterModelsDropsResultsWhenSnapshotChangesDuringRegistration(t *testing.T) { modelRegistry := newFakeModelRegistry() host := New() oldRecord := capabilityRecord{ id: "alpha", plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{ ModelRegistrar: modelRegistrarFunc(func(ctx context.Context, req pluginapi.ModelRegistrationRequest) (pluginapi.ModelRegistrationResponse, error) { setHostSnapshotForTest(host, true, capabilityRecord{ id: "bravo", plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{ ModelRegistrar: staticModelRegistrar("provider-b", "model-b"), }}, }) return pluginapi.ModelRegistrationResponse{ Provider: "provider-a", Models: []pluginapi.ModelInfo{{ ID: "model-a", }}, }, nil }), }}, } setHostSnapshotForTest(host, true, oldRecord) host.modelProviders["alpha"] = "existing-provider" host.RegisterModels(context.Background(), modelRegistry) if len(modelRegistry.clients) != 0 { t.Fatalf("registered clients = %#v, want none after stale snapshot", modelRegistry.clients) } if len(modelRegistry.unregisters) != 0 { t.Fatalf("unregistered clients = %#v, want none after stale snapshot", modelRegistry.unregisters) } if host.modelProvider("alpha") != "existing-provider" { t.Fatalf("model provider = %q, want existing-provider", host.modelProvider("alpha")) }} func TestRegisterModelsPanicFusesPluginAndSkipsLaterCalls(t *testing.T) { calls := 0 modelRegistry := newFakeModelRegistry() host := newHostWithRecords(capabilityRecord{ id: "panic-plugin", plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{ ModelRegistrar: modelRegistrarFunc(func(ctx context.Context, req pluginapi.ModelRegistrationRequest) (pluginapi.ModelRegistrationResponse, error) { calls++ panic("register models panic") }), }}, }) host.RegisterModels(context.Background(), modelRegistry) host.RegisterModels(context.Background(), modelRegistry) if calls != 1 { t.Fatalf("RegisterModels calls = %d, want 1", calls) } if !host.isPluginFused("panic-plugin") { t.Fatal("panic-plugin was not fused") } if len(modelRegistry.clients) != 0 { t.Fatalf("registered clients = %#v, want none", modelRegistry.clients) }} func TestRegisterExecutorsDoesNotOverwriteExistingExecutor(t *testing.T) { manager := newFakeExecutorManager() existing := &fakeProviderExecutor{provider: "provider"} manager.RegisterExecutor(existing) host := newHostWithRecords(capabilityRecord{ id: "alpha", plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{ Executor: &fakeExecutor{identifier: "provider"}, }}, }) host.RegisterExecutors(manager, nil) if manager.registerCalls != 1 { t.Fatalf("RegisterExecutor calls = %d, want only existing registration", manager.registerCalls) } got, _ := manager.Executor("provider") if got != existing { t.Fatalf("registered executor = %#v, want existing executor", got) }} func TestRegisterExecutorsSameProviderKeepsFirstSnapshotCandidate(t *testing.T) { manager := newFakeExecutorManager() first := &fakeExecutor{identifier: "provider"} second := &fakeExecutor{identifier: "provider"} host := newHostWithRecords( capabilityRecord{ id: "low", priority: 10, plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{ Executor: second, }}, }, capabilityRecord{ id: "high", priority: 20, plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{ Executor: first, }}, }, ) host.RegisterExecutors(manager, nil) if manager.registerCalls != 1 { t.Fatalf("RegisterExecutor calls = %d, want 1", manager.registerCalls) } adapter, okAdapter := manager.executors["provider"].(*executorAdapter) if !okAdapter { t.Fatalf("registered executor = %#v, want executorAdapter", manager.executors["provider"]) } if adapter.pluginID != "high" || adapter.executor != first { t.Fatalf("registered adapter = %#v, want high priority executor", adapter) }} func TestRegisterExecutorsIdentifierPanicFusesPlugin(t *testing.T) { manager := newFakeExecutorManager() host := newHostWithRecords(capabilityRecord{ id: "panic-identifier", plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{ Executor: &fakeExecutor{panicIdentifier: true}, }}, }) host.RegisterExecutors(manager, nil) if !host.isPluginFused("panic-identifier") { t.Fatal("panic-identifier was not fused") } if manager.registerCalls != 0 { t.Fatalf("RegisterExecutor calls = %d, want 0", manager.registerCalls) }} func TestRegisterExecutorsSelectsHighestPriorityPluginExecutorPerModel(t *testing.T) { modelRegistry := newFakeModelRegistry() manager := newFakeExecutorManager() host := newHostWithRecords( capabilityRecord{ id: "low", priority: 10, plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{ ModelRegistrar: staticModelRegistrar("low-provider", "shared-model"), Executor: &fakeExecutor{identifier: "low-provider"}, }}, }, capabilityRecord{ id: "high", priority: 20, plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{ ModelRegistrar: staticModelRegistrar("high-provider", "shared-model"), Executor: &fakeExecutor{identifier: "high-provider"}, }}, }, ) host.RegisterModels(context.Background(), modelRegistry) host.RegisterExecutors(manager, modelRegistry) if _, okLow := manager.executors["low-provider"]; okLow { t.Fatal("low priority executor was registered for shared-model") } if _, okHigh := manager.executors["high-provider"]; !okHigh { t.Fatal("high priority executor was not registered for shared-model") } if got := host.ModelsForProvider("low-provider"); len(got) != 0 { t.Fatalf("low provider models = %#v, want none", got) } got := host.ModelsForProvider("high-provider") if len(got) != 1 || got[0].ID != "shared-model" { t.Fatalf("high provider models = %#v, want shared-model", got) }} Lines 1–500 of 3689. Load more
package pluginhost import ( "bytes" "context" "errors" "fmt" "io" "net/http" "net/url" "sort" "strings" "testing" "time" "github.com/router-for-me/CLIProxyAPI/v7/internal/config" "github.com/router-for-me/CLIProxyAPI/v7/internal/registry" sdkaccess "github.com/router-for-me/CLIProxyAPI/v7/sdk/access" coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" coreexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" coreusage "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/usage" "github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginabi" "github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginapi" sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator" ) func TestPluginModelInfoToRegistryModelInfoClonesThinkingAndSlices(t *testing.T) { model := pluginapi.ModelInfo{ ID: "model-1", Object: "model", Created: 123, OwnedBy: "owner", Type: "plugin", DisplayName: "Model One", Name: "provider-model", Version: "v1", Description: "desc", InputTokenLimit: 100, OutputTokenLimit: 200, SupportedGenerationMethods: []string{"generate"}, ContextLength: 300, MaxCompletionTokens: 400, SupportedParameters: []string{"temperature"}, SupportedInputModalities: []string{"text"}, SupportedOutputModalities: []string{"image"}, Thinking: &pluginapi.ThinkingSupport{ Min: 1, Max: 2, ZeroAllowed: true, DynamicAllowed: true, Levels: []string{"low", "high"}, }, UserDefined: true, } got := pluginModelInfoToRegistryModelInfo(model) if got.ID != model.ID || got.Object != model.Object || got.Created != model.Created || got.OwnedBy != model.OwnedBy || got.Type != model.Type || got.DisplayName != model.DisplayName || got.Name != model.Name || got.Version != model.Version || got.Description != model.Description || got.InputTokenLimit != int(model.InputTokenLimit) || got.OutputTokenLimit != int(model.OutputTokenLimit) || got.ContextLength != int(model.ContextLength) || got.MaxCompletionTokens != int(model.MaxCompletionTokens) || !got.UserDefined { t.Fatalf("converted model = %#v, want fields copied from %#v", got, model) } if got.Thinking == nil { t.Fatal("Thinking = nil, want converted thinking support") } if got.Thinking.Min != 1 || got.Thinking.Max != 2 || !got.Thinking.ZeroAllowed || !got.Thinking.DynamicAllowed || fmt.Sprint(got.Thinking.Levels) != "[low high]" { t.Fatalf("Thinking = %#v, want copied thinking support", got.Thinking) } model.SupportedGenerationMethods[0] = "mutated" model.SupportedParameters[0] = "mutated" model.SupportedInputModalities[0] = "mutated" model.SupportedOutputModalities[0] = "mutated" model.Thinking.Levels[0] = "mutated" if got.SupportedGenerationMethods[0] != "generate" || got.SupportedParameters[0] != "temperature" || got.SupportedInputModalities[0] != "text" || got.SupportedOutputModalities[0] != "image" || got.Thinking.Levels[0] != "low" { t.Fatalf("converted model kept aliases to plugin slices: %#v", got) } } func TestExecutorNativeStreamResponseTranslatorExistsRequiresStreamTransform(t *testing.T) { outputFormat := sdktranslator.Format("plugin-output-non-stream-only") requestedFormat := sdktranslator.Format("client-output-non-stream-only") sdktranslator.Register(requestedFormat, outputFormat, nil, sdktranslator.ResponseTransform{ NonStream: func(ctx context.Context, model string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []byte { return rawJSON }, }) if executorNativeStreamResponseTranslatorExists(outputFormat, requestedFormat) { t.Fatal("non-stream-only response transformer was accepted for stream executor output") } streamOutputFormat := sdktranslator.Format("plugin-output-stream") streamRequestedFormat := sdktranslator.Format("client-output-stream") sdktranslator.Register(streamRequestedFormat, streamOutputFormat, nil, sdktranslator.ResponseTransform{ Stream: func(ctx context.Context, model string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte { return [][]byte{rawJSON} }, }) if !executorNativeStreamResponseTranslatorExists(streamOutputFormat, streamRequestedFormat) { t.Fatal("stream response transformer was not accepted for stream executor output") } } func TestRegisterModelsRegistersProviderModelsAndClientID(t *testing.T) { modelRegistry := newFakeModelRegistry() host := newHostWithRecords(capabilityRecord{ id: "alpha", meta: pluginapi.Metadata{Name: "Alpha", Version: "1.0.0"}, plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{ ModelRegistrar: modelRegistrarFunc(func(ctx context.Context, req pluginapi.ModelRegistrationRequest) (pluginapi.ModelRegistrationResponse, error) { if req.Plugin.Name != "Alpha" || req.Plugin.Version != "1.0.0" { t.Fatalf("RegisterModels request plugin = %#v, want Alpha metadata", req.Plugin) } return pluginapi.ModelRegistrationResponse{ Provider: " MixedProvider ", Models: []pluginapi.ModelInfo{{ ID: " model-1 ", Object: "model", Created: 123, OwnedBy: "owner", Type: "chat", DisplayName: "Model One", Name: "native-model-1", Version: "v1", Description: "description", InputTokenLimit: 100, OutputTokenLimit: 200, SupportedGenerationMethods: []string{"generate"}, ContextLength: 300, MaxCompletionTokens: 400, SupportedParameters: []string{"temperature"}, SupportedInputModalities: []string{"text"}, SupportedOutputModalities: []string{"text"}, Thinking: &pluginapi.ThinkingSupport{ Min: 1, Max: 2, ZeroAllowed: true, DynamicAllowed: true, Levels: []string{"low"}, }, UserDefined: true, }}, }, nil }), }}, }) host.RegisterModels(context.Background(), modelRegistry) reg := modelRegistry.clients["plugin:alpha:mixedprovider"] if reg == nil { t.Fatal("plugin:alpha:mixedprovider was not registered") } if reg.provider != "mixedprovider" { t.Fatalf("registered provider = %q, want mixedprovider", reg.provider) } if len(reg.models) != 1 { t.Fatalf("registered model count = %d, want 1", len(reg.models)) } model := reg.models[0] if model.ID != "model-1" || model.Object != "model" || model.Created != 123 || model.OwnedBy != "owner" || model.Type != "chat" || model.DisplayName != "Model One" || model.Name != "native-model-1" || model.Version != "v1" || model.Description != "description" || model.InputTokenLimit != 100 || model.OutputTokenLimit != 200 || model.ContextLength != 300 || model.MaxCompletionTokens != 400 || model.SupportedGenerationMethods[0] != "generate" || model.SupportedParameters[0] != "temperature" || model.SupportedInputModalities[0] != "text" || model.SupportedOutputModalities[0] != "text" || !model.UserDefined { t.Fatalf("registered model = %#v, want converted fields", model) } if model.Thinking == nil || model.Thinking.Min != 1 || model.Thinking.Max != 2 || !model.Thinking.ZeroAllowed || !model.Thinking.DynamicAllowed || model.Thinking.Levels[0] != "low" { t.Fatalf("registered thinking = %#v, want converted thinking", model.Thinking) } } func TestRegisterModelsUsesModelProviderStaticModels(t *testing.T) { modelRegistry := newFakeModelRegistry() called := false host := newHostWithRecords(capabilityRecord{ id: "alpha", meta: pluginapi.Metadata{Name: "Alpha", Version: "1.0.0"}, plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{ ModelProvider: modelProviderFunc{ staticModels: func(ctx context.Context, req pluginapi.StaticModelRequest) (pluginapi.ModelResponse, error) { called = true if req.Plugin.Name != "Alpha" || req.Plugin.Version != "1.0.0" { t.Fatalf("StaticModels request plugin = %#v, want Alpha metadata", req.Plugin) } if req.Host.AuthDir != "/tmp/plugin-auth" || req.Host.ProxyURL != "http://proxy.local" || !req.Host.ForceModelPrefix { t.Fatalf("StaticModels host = %#v, want configured summary", req.Host) } if len(req.Host.OAuthModelAlias["plugin-provider"]) != 1 || req.Host.OAuthModelAlias["plugin-provider"][0].Alias != "alias-model" { t.Fatalf("StaticModels OAuthModelAlias = %#v, want configured alias", req.Host.OAuthModelAlias) } if len(req.Host.ExcludedModels["plugin-provider"]) != 1 || req.Host.ExcludedModels["plugin-provider"][0] != "hidden-model" { t.Fatalf("StaticModels ExcludedModels = %#v, want configured exclusion", req.Host.ExcludedModels) } return pluginapi.ModelResponse{ Provider: " Plugin-Provider ", Models: []pluginapi.ModelInfo{{ ID: " model-static ", Object: "model", DisplayName: "Static Model", }}, }, nil }, }, ModelRegistrar: staticModelRegistrar("legacy-provider", "legacy-model"), }}, }) host.runtimeConfig = &config.Config{ SDKConfig: config.SDKConfig{ ProxyURL: "http://proxy.local", ForceModelPrefix: true, }, AuthDir: "/tmp/plugin-auth", OAuthModelAlias: map[string][]config.OAuthModelAlias{ "plugin-provider": []config.OAuthModelAlias{{Name: "upstream-model", Alias: "alias-model"}}, }, OAuthExcludedModels: map[string][]string{ "plugin-provider": []string{"hidden-model"}, }, } host.RegisterModels(context.Background(), modelRegistry) if !called { t.Fatal("ModelProvider.StaticModels was not called") } reg := modelRegistry.clients["plugin:alpha:plugin-provider"] if reg == nil { t.Fatal("plugin:alpha:plugin-provider was not registered") } if reg.provider != "plugin-provider" { t.Fatalf("registered provider = %q, want plugin-provider", reg.provider) } if len(reg.models) != 1 || reg.models[0].ID != "model-static" || reg.models[0].DisplayName != "Static Model" { t.Fatalf("registered models = %#v, want static model", reg.models) } if _, okLegacy := modelRegistry.clients["plugin:alpha:legacy-provider"]; okLegacy { t.Fatal("legacy ModelRegistrar path was used despite ModelProvider.StaticModels") } } func TestRegisterModelsSkipsErrorEmptyAndInvalidModels(t *testing.T) { modelRegistry := newFakeModelRegistry() host := newHostWithRecords( capabilityRecord{ id: "error", plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{ ModelRegistrar: modelRegistrarFunc(func(ctx context.Context, req pluginapi.ModelRegistrationRequest) (pluginapi.ModelRegistrationResponse, error) { return pluginapi.ModelRegistrationResponse{}, errors.New("register failed") }), }}, }, capabilityRecord{ id: "empty-provider", plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{ ModelRegistrar: modelRegistrarFunc(func(ctx context.Context, req pluginapi.ModelRegistrationRequest) (pluginapi.ModelRegistrationResponse, error) { return pluginapi.ModelRegistrationResponse{Provider: " ", Models: []pluginapi.ModelInfo{{ID: "model"}}}, nil }), }}, }, capabilityRecord{ id: "empty-models", plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{ ModelRegistrar: modelRegistrarFunc(func(ctx context.Context, req pluginapi.ModelRegistrationRequest) (pluginapi.ModelRegistrationResponse, error) { return pluginapi.ModelRegistrationResponse{Provider: "provider"}, nil }), }}, }, capabilityRecord{ id: "invalid-models", plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{ ModelRegistrar: modelRegistrarFunc(func(ctx context.Context, req pluginapi.ModelRegistrationRequest) (pluginapi.ModelRegistrationResponse, error) { return pluginapi.ModelRegistrationResponse{Provider: "provider", Models: []pluginapi.ModelInfo{{ID: " "}}}, nil }), }}, }, ) host.RegisterModels(context.Background(), modelRegistry) if len(modelRegistry.clients) != 0 { t.Fatalf("registered clients = %#v, want none", modelRegistry.clients) } } func TestRegisterModelsPrunesStaleClientAfterSnapshotChange(t *testing.T) { modelRegistry := newFakeModelRegistry() host := newHostWithRecords(capabilityRecord{ id: "alpha", plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{ ModelRegistrar: staticModelRegistrar("provider-a", "model-a"), }}, }) host.RegisterModels(context.Background(), modelRegistry) setHostSnapshotForTest(host, true, capabilityRecord{ id: "bravo", plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{ ModelRegistrar: staticModelRegistrar("provider-b", "model-b"), }}, }) host.RegisterModels(context.Background(), modelRegistry) if _, okClient := modelRegistry.clients["plugin:alpha:provider-a"]; okClient { t.Fatal("stale alpha client is still registered") } if modelRegistry.unregisters[0] != "plugin:alpha:provider-a" { t.Fatalf("unregistered clients = %#v, want alpha client first", modelRegistry.unregisters) } if _, okClient := modelRegistry.clients["plugin:bravo:provider-b"]; !okClient { t.Fatal("bravo client was not registered") } } func TestRegisterModelsDropsResultsWhenSnapshotChangesDuringRegistration(t *testing.T) { modelRegistry := newFakeModelRegistry() host := New() oldRecord := capabilityRecord{ id: "alpha", plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{ ModelRegistrar: modelRegistrarFunc(func(ctx context.Context, req pluginapi.ModelRegistrationRequest) (pluginapi.ModelRegistrationResponse, error) { setHostSnapshotForTest(host, true, capabilityRecord{ id: "bravo", plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{ ModelRegistrar: staticModelRegistrar("provider-b", "model-b"), }}, }) return pluginapi.ModelRegistrationResponse{ Provider: "provider-a", Models: []pluginapi.ModelInfo{{ ID: "model-a", }}, }, nil }), }}, } setHostSnapshotForTest(host, true, oldRecord) host.modelProviders["alpha"] = "existing-provider" host.RegisterModels(context.Background(), modelRegistry) if len(modelRegistry.clients) != 0 { t.Fatalf("registered clients = %#v, want none after stale snapshot", modelRegistry.clients) } if len(modelRegistry.unregisters) != 0 { t.Fatalf("unregistered clients = %#v, want none after stale snapshot", modelRegistry.unregisters) } if host.modelProvider("alpha") != "existing-provider" { t.Fatalf("model provider = %q, want existing-provider", host.modelProvider("alpha")) } } func TestRegisterModelsPanicFusesPluginAndSkipsLaterCalls(t *testing.T) { calls := 0 modelRegistry := newFakeModelRegistry() host := newHostWithRecords(capabilityRecord{ id: "panic-plugin", plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{ ModelRegistrar: modelRegistrarFunc(func(ctx context.Context, req pluginapi.ModelRegistrationRequest) (pluginapi.ModelRegistrationResponse, error) { calls++ panic("register models panic") }), }}, }) host.RegisterModels(context.Background(), modelRegistry) host.RegisterModels(context.Background(), modelRegistry) if calls != 1 { t.Fatalf("RegisterModels calls = %d, want 1", calls) } if !host.isPluginFused("panic-plugin") { t.Fatal("panic-plugin was not fused") } if len(modelRegistry.clients) != 0 { t.Fatalf("registered clients = %#v, want none", modelRegistry.clients) } } func TestRegisterExecutorsDoesNotOverwriteExistingExecutor(t *testing.T) { manager := newFakeExecutorManager() existing := &fakeProviderExecutor{provider: "provider"} manager.RegisterExecutor(existing) host := newHostWithRecords(capabilityRecord{ id: "alpha", plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{ Executor: &fakeExecutor{identifier: "provider"}, }}, }) host.RegisterExecutors(manager, nil) if manager.registerCalls != 1 { t.Fatalf("RegisterExecutor calls = %d, want only existing registration", manager.registerCalls) } got, _ := manager.Executor("provider") if got != existing { t.Fatalf("registered executor = %#v, want existing executor", got) } } func TestRegisterExecutorsSameProviderKeepsFirstSnapshotCandidate(t *testing.T) { manager := newFakeExecutorManager() first := &fakeExecutor{identifier: "provider"} second := &fakeExecutor{identifier: "provider"} host := newHostWithRecords( capabilityRecord{ id: "low", priority: 10, plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{ Executor: second, }}, }, capabilityRecord{ id: "high", priority: 20, plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{ Executor: first, }}, }, ) host.RegisterExecutors(manager, nil) if manager.registerCalls != 1 { t.Fatalf("RegisterExecutor calls = %d, want 1", manager.registerCalls) } adapter, okAdapter := manager.executors["provider"].(*executorAdapter) if !okAdapter { t.Fatalf("registered executor = %#v, want executorAdapter", manager.executors["provider"]) } if adapter.pluginID != "high" || adapter.executor != first { t.Fatalf("registered adapter = %#v, want high priority executor", adapter) } } func TestRegisterExecutorsIdentifierPanicFusesPlugin(t *testing.T) { manager := newFakeExecutorManager() host := newHostWithRecords(capabilityRecord{ id: "panic-identifier", plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{ Executor: &fakeExecutor{panicIdentifier: true}, }}, }) host.RegisterExecutors(manager, nil) if !host.isPluginFused("panic-identifier") { t.Fatal("panic-identifier was not fused") } if manager.registerCalls != 0 { t.Fatalf("RegisterExecutor calls = %d, want 0", manager.registerCalls) } } func TestRegisterExecutorsSelectsHighestPriorityPluginExecutorPerModel(t *testing.T) { modelRegistry := newFakeModelRegistry() manager := newFakeExecutorManager() host := newHostWithRecords( capabilityRecord{ id: "low", priority: 10, plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{ ModelRegistrar: staticModelRegistrar("low-provider", "shared-model"), Executor: &fakeExecutor{identifier: "low-provider"}, }}, }, capabilityRecord{ id: "high", priority: 20, plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{ ModelRegistrar: staticModelRegistrar("high-provider", "shared-model"), Executor: &fakeExecutor{identifier: "high-provider"}, }}, }, ) host.RegisterModels(context.Background(), modelRegistry) host.RegisterExecutors(manager, modelRegistry) if _, okLow := manager.executors["low-provider"]; okLow { t.Fatal("low priority executor was registered for shared-model") } if _, okHigh := manager.executors["high-provider"]; !okHigh { t.Fatal("high priority executor was not registered for shared-model") } if got := host.ModelsForProvider("low-provider"); len(got) != 0 { t.Fatalf("low provider models = %#v, want none", got) } got := host.ModelsForProvider("high-provider") if len(got) != 1 || got[0].ID != "shared-model" { t.Fatalf("high provider models = %#v, want shared-model", got) } }