3689 lines · 138.3 KB · Go
RawDownload
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.