From 5fe4fadbd60b3546ae5e64b0bb4147b6bb8737ed Mon Sep 17 00:00:00 2001 From: Cong Liu Date: Thu, 23 Jul 2026 21:47:51 -0700 Subject: [PATCH 1/2] feat: implement EPP plugin stability lifecycle and experimentalPlugins feature gate (#1912) Signed-off-by: Cong Liu --- cmd/epp/runner/runner.go | 202 ++++++++++-------- cmd/epp/runner/test_runner.go | 7 +- pkg/epp/config/loader/configloader.go | 23 +- pkg/epp/config/loader/configloader_test.go | 108 +++++++--- .../framework/interface/plugin/registry.go | 61 ++++-- .../interface/plugin/registry_test.go | 47 +++- .../framework/interface/plugin/stability.go | 42 ++++ pkg/epp/framework/plugins/README.md | 22 ++ .../eviction/filtering/sheddable.go | 4 - .../eviction/ordering/priority_time.go | 4 - .../disagg/disagg_headers_handler_test.go | 4 +- 11 files changed, 372 insertions(+), 152 deletions(-) create mode 100644 pkg/epp/framework/interface/plugin/stability.go diff --git a/cmd/epp/runner/runner.go b/cmd/epp/runner/runner.go index 0799c17e98..2e9dfd8675 100644 --- a/cmd/epp/runner/runner.go +++ b/cmd/epp/runner/runner.go @@ -75,6 +75,8 @@ import ( sourcemetrics "github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/datalayer/source/metrics" srcmodels "github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/datalayer/source/models" sourcenotifications "github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/datalayer/source/notifications" + evictfiltering "github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/flowcontrol/eviction/filtering" + evictordering "github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/flowcontrol/eviction/ordering" "github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/flowcontrol/fairness/globalstrict" programaware "github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/flowcontrol/fairness/program-aware" "github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/flowcontrol/fairness/roundrobin" @@ -549,115 +551,144 @@ func setupDatastore(ctx context.Context, epFactory datalayer.EndpointFactory, // registerInTreePlugins registers the factory functions of all known plugins func (r *Runner) registerInTreePlugins() { // bylabel role filters - fwkplugin.Register(bylabel.LabelSelectorFilterType, bylabel.SelectorFactory) - fwkplugin.Register(bylabel.ByLabelSelectorType, bylabel.DeprecatedSelectorFactory) //nolint:staticcheck - fwkplugin.Register(bylabel.ByLabelType, bylabel.Factory) //nolint:staticcheck - fwkplugin.Register(bylabel.EncodeRoleType, bylabel.EncodeRoleFactory) - fwkplugin.Register(bylabel.DecodeRoleType, bylabel.DecodeRoleFactory) - fwkplugin.Register(bylabel.PrefillRoleType, bylabel.PrefillRoleFactory) - fwkplugin.Register(endpointattributefilter.EndpointAttributeFilterType, endpointattributefilter.EndpointAttributeFilterFactory) - fwkplugin.Register(sessionaffinityfilter.SessionAffinityType, sessionaffinityfilter.Factory) + // Beta + fwkplugin.Register(bylabel.LabelSelectorFilterType, fwkplugin.StabilityBeta, bylabel.SelectorFactory) + fwkplugin.RegisterDeprecated(bylabel.ByLabelSelectorType, fwkplugin.StabilityBeta, bylabel.DeprecatedSelectorFactory, "v0.9.0", "v0.11.0", bylabel.LabelSelectorFilterType) //nolint:staticcheck // deprecated in v0.9.0 (#842) + fwkplugin.RegisterDeprecated(bylabel.ByLabelType, fwkplugin.StabilityBeta, bylabel.Factory, "v0.9.0", "v0.11.0", bylabel.LabelSelectorFilterType) //nolint:staticcheck // deprecated in v0.9.0 (#842) + fwkplugin.Register(bylabel.EncodeRoleType, fwkplugin.StabilityBeta, bylabel.EncodeRoleFactory) + fwkplugin.Register(bylabel.DecodeRoleType, fwkplugin.StabilityBeta, bylabel.DecodeRoleFactory) + fwkplugin.Register(bylabel.PrefillRoleType, fwkplugin.StabilityBeta, bylabel.PrefillRoleFactory) + fwkplugin.Register(sessionaffinityfilter.SessionAffinityType, fwkplugin.StabilityBeta, sessionaffinityfilter.Factory) + // Alpha + fwkplugin.Register(endpointattributefilter.EndpointAttributeFilterType, fwkplugin.StabilityAlpha, endpointattributefilter.EndpointAttributeFilterFactory) // dataparallel profile handler - fwkplugin.Register(dataparallel.DataParallelProfileHandlerType, dataparallel.ProfileHandlerFactory) + // Beta + fwkplugin.Register(dataparallel.DataParallelProfileHandlerType, fwkplugin.StabilityBeta, dataparallel.ProfileHandlerFactory) // extra scheduling scorers - fwkplugin.Register(loadaware.LoadAwareType, loadaware.Factory) - fwkplugin.Register(sessionaffinity.SessionAffinityType, sessionaffinity.Factory) - fwkplugin.Register(contextlengthaware.ContextLengthAwareType, contextlengthaware.Factory) + // Beta + fwkplugin.Register(loadaware.LoadAwareType, fwkplugin.StabilityBeta, loadaware.Factory) + fwkplugin.Register(sessionaffinity.SessionAffinityType, fwkplugin.StabilityBeta, sessionaffinity.Factory) + fwkplugin.Register(contextlengthaware.ContextLengthAwareType, fwkplugin.StabilityBeta, contextlengthaware.Factory) // data layer models source/extractor - fwkplugin.Register(srcmodels.ModelsDataSourceType, srcmodels.ModelDataSourceFactory) - fwkplugin.Register(attrmodels.ModelsExtractorType, extmodels.ModelServerExtractorFactory) - fwkplugin.Register(attrtopology.TopologyExtractorType, exttopology.Factory) + // Beta + fwkplugin.Register(srcmodels.ModelsDataSourceType, fwkplugin.StabilityBeta, srcmodels.ModelDataSourceFactory) + fwkplugin.Register(attrmodels.ModelsExtractorType, fwkplugin.StabilityBeta, extmodels.ModelServerExtractorFactory) + // Alpha + fwkplugin.Register(attrtopology.TopologyExtractorType, fwkplugin.StabilityAlpha, exttopology.Factory) // data layer DCGM source/extractor - fwkplugin.Register(srcdcgm.DCGMDataSourceType, srcdcgm.DCGMDataSourceFactory) - fwkplugin.Register(attrgpu.DCGMExtractorType, extdcgm.DCGMExtractorFactory) - - fwkplugin.Register(prefix.PrefixCacheScorerPluginType, prefix.PrefixCachePluginFactory) - fwkplugin.Register(maxscore.MaxScorePickerType, maxscore.MaxScorePickerFactory) - fwkplugin.Register(random.RandomPickerType, random.RandomPickerFactory) - fwkplugin.Register(weightedrandom.WeightedRandomPickerType, weightedrandom.WeightedRandomPickerFactory) - fwkplugin.Register(single.SingleProfileHandlerType, single.SingleProfileHandlerFactory) - fwkplugin.Register(disagg.DisaggHeadersHandlerType, disagg.HeadersHandlerFactory) //nolint:staticcheck // intentional: keep backward compatibility - fwkplugin.Register(disagg.PrefillHeaderHandlerType, disagg.HeadersHandlerFactory) //nolint:staticcheck // intentional: keep backward compatibility - fwkplugin.RegisterWithPluginDependencies(disagg.PdProfileHandlerType, disagg.PdProfileHandlerFactory, //nolint:staticcheck // intentional: keep backward compatibility - disagg.PdProfileHandlerConfigParser) - fwkplugin.RegisterWithPluginDependencies(disagg.DisaggProfileHandlerType, disagg.HandlerFactory, disagg.DisaggProfileHandlerConfigParser) - fwkplugin.Register(disagg.AlwaysDisaggPDDeciderPluginType, disagg.AlwaysDisaggPDDeciderPluginFactory) - fwkplugin.Register(disagg.PrefixBasedPDDeciderPluginType, disagg.PrefixBasedPDDeciderPluginFactory) - fwkplugin.Register(disagg.AlwaysDisaggMulimodalPluginType, disagg.AlwaysDisaggMulimodalDeciderPluginFactory) - fwkplugin.Register(kvcacheutilization.KvCacheUtilizationScorerType, kvcacheutilization.KvCacheUtilizationScorerFactory) - fwkplugin.Register(endpointattribute.EndpointAttributeScorerType, endpointattribute.EndpointAttributeScorerFactory) - fwkplugin.Register(queuedepth.QueueScorerType, queuedepth.QueueScorerFactory) - fwkplugin.Register(runningrequests.RunningRequestsSizeScorerType, runningrequests.RunningRequestsSizeScorerFactory) - fwkplugin.Register(loraaffinity.LoraAffinityScorerType, loraaffinity.LoraAffinityScorerFactory) - fwkplugin.Register(tokenload.TokenLoadScorerType, tokenload.TokenLoadScorerFactory) - fwkplugin.Register(nohitlru.NoHitLRUType, nohitlru.Factory) - fwkplugin.Register(activerequest.ActiveRequestType, activerequest.Factory) - fwkplugin.Register(preciseprefixcache.PrecisePrefixCachePluginType, preciseprefixcache.PluginFactory) - fwkplugin.Register(burstprefix.PluginType, burstprefix.Factory) - fwkplugin.Register(mmcacheaffinity.Type, mmcacheaffinity.Factory) - fwkplugin.Register(preciseproducer.PluginType, preciseproducer.PluginFactory) - fwkplugin.Register(p2psource.PluginType, p2psource.PluginFactory) + // Alpha + fwkplugin.Register(srcdcgm.DCGMDataSourceType, fwkplugin.StabilityAlpha, srcdcgm.DCGMDataSourceFactory) + fwkplugin.Register(attrgpu.DCGMExtractorType, fwkplugin.StabilityAlpha, extdcgm.DCGMExtractorFactory) + + // scheduling & profile handler plugins + // Beta + fwkplugin.Register(prefix.PrefixCacheScorerPluginType, fwkplugin.StabilityBeta, prefix.PrefixCachePluginFactory) + fwkplugin.Register(maxscore.MaxScorePickerType, fwkplugin.StabilityBeta, maxscore.MaxScorePickerFactory) + fwkplugin.Register(random.RandomPickerType, fwkplugin.StabilityBeta, random.RandomPickerFactory) + fwkplugin.Register(weightedrandom.WeightedRandomPickerType, fwkplugin.StabilityBeta, weightedrandom.WeightedRandomPickerFactory) + fwkplugin.Register(single.SingleProfileHandlerType, fwkplugin.StabilityBeta, single.SingleProfileHandlerFactory) + fwkplugin.RegisterDeprecated(disagg.DisaggHeadersHandlerType, fwkplugin.StabilityBeta, disagg.HeadersHandlerFactory, "v0.9.0", "v0.11.0", disagg.DisaggProfileHandlerType) //nolint:staticcheck // deprecated in v0.9.0 (#905) + fwkplugin.RegisterDeprecated(disagg.PrefillHeaderHandlerType, fwkplugin.StabilityBeta, disagg.HeadersHandlerFactory, "v0.9.0", "v0.11.0", disagg.DisaggProfileHandlerType) //nolint:staticcheck // deprecated in v0.9.0 (#905) + fwkplugin.RegisterDeprecatedWithPluginDependencies(disagg.PdProfileHandlerType, fwkplugin.StabilityBeta, disagg.PdProfileHandlerFactory, //nolint:staticcheck // deprecated in v0.7.0 (#732/#756) + disagg.PdProfileHandlerConfigParser, "v0.7.0", "v0.9.0", disagg.DisaggProfileHandlerType) + fwkplugin.RegisterWithPluginDependencies(disagg.DisaggProfileHandlerType, fwkplugin.StabilityBeta, disagg.HandlerFactory, disagg.DisaggProfileHandlerConfigParser) + fwkplugin.Register(disagg.AlwaysDisaggPDDeciderPluginType, fwkplugin.StabilityBeta, disagg.AlwaysDisaggPDDeciderPluginFactory) + fwkplugin.Register(disagg.PrefixBasedPDDeciderPluginType, fwkplugin.StabilityBeta, disagg.PrefixBasedPDDeciderPluginFactory) + fwkplugin.Register(disagg.AlwaysDisaggMulimodalPluginType, fwkplugin.StabilityBeta, disagg.AlwaysDisaggMulimodalDeciderPluginFactory) + fwkplugin.Register(kvcacheutilization.KvCacheUtilizationScorerType, fwkplugin.StabilityBeta, kvcacheutilization.KvCacheUtilizationScorerFactory) + fwkplugin.Register(queuedepth.QueueScorerType, fwkplugin.StabilityBeta, queuedepth.QueueScorerFactory) + fwkplugin.Register(runningrequests.RunningRequestsSizeScorerType, fwkplugin.StabilityBeta, runningrequests.RunningRequestsSizeScorerFactory) + fwkplugin.Register(loraaffinity.LoraAffinityScorerType, fwkplugin.StabilityBeta, loraaffinity.LoraAffinityScorerFactory) + fwkplugin.Register(tokenload.TokenLoadScorerType, fwkplugin.StabilityBeta, tokenload.TokenLoadScorerFactory) + fwkplugin.Register(nohitlru.NoHitLRUType, fwkplugin.StabilityBeta, nohitlru.Factory) + fwkplugin.Register(activerequest.ActiveRequestType, fwkplugin.StabilityBeta, activerequest.Factory) + fwkplugin.Register(preciseprefixcache.PrecisePrefixCachePluginType, fwkplugin.StabilityBeta, preciseprefixcache.PluginFactory) + fwkplugin.Register(mmcacheaffinity.Type, fwkplugin.StabilityBeta, mmcacheaffinity.Factory) + fwkplugin.Register(preciseproducer.PluginType, fwkplugin.StabilityBeta, preciseproducer.PluginFactory) + // Alpha + fwkplugin.Register(endpointattribute.EndpointAttributeScorerType, fwkplugin.StabilityAlpha, endpointattribute.EndpointAttributeScorerFactory) + fwkplugin.Register(burstprefix.PluginType, fwkplugin.StabilityAlpha, burstprefix.Factory) + fwkplugin.Register(p2psource.PluginType, fwkplugin.StabilityAlpha, p2psource.PluginFactory) // Flow Control plugins - fwkplugin.Register(globalstrict.GlobalStrictFairnessPolicyType, globalstrict.GlobalStrictFairnessPolicyFactory) - fwkplugin.Register(roundrobin.RoundRobinFairnessPolicyType, roundrobin.RoundRobinFairnessPolicyFactory) - fwkplugin.Register(programaware.ProgramAwarePluginType, programaware.ProgramAwarePluginFactory) - fwkplugin.Register(fcfs.FCFSOrderingPolicyType, fcfs.FCFSOrderingPolicyFactory) - fwkplugin.Register(edf.EDFOrderingPolicyType, edf.EDFOrderingPolicyFactory) - fwkplugin.Register(slodeadline.SLODeadlineOrderingPolicyType, slodeadline.SLODeadlineOrderingPolicyFactory) - fwkplugin.Register(usagelimits.StaticUsageLimitPolicyType, usagelimits.StaticPolicyFactory) - fwkplugin.Register(priorityholdback.PolicyType, priorityholdback.PolicyFactory) - fwkplugin.Register(softreflectiveceiling.PolicyType, softreflectiveceiling.Factory) + // Beta + fwkplugin.Register(globalstrict.GlobalStrictFairnessPolicyType, fwkplugin.StabilityBeta, globalstrict.GlobalStrictFairnessPolicyFactory) + fwkplugin.Register(roundrobin.RoundRobinFairnessPolicyType, fwkplugin.StabilityBeta, roundrobin.RoundRobinFairnessPolicyFactory) + fwkplugin.Register(programaware.ProgramAwarePluginType, fwkplugin.StabilityBeta, programaware.ProgramAwarePluginFactory) + fwkplugin.Register(fcfs.FCFSOrderingPolicyType, fwkplugin.StabilityBeta, fcfs.FCFSOrderingPolicyFactory) + fwkplugin.Register(edf.EDFOrderingPolicyType, fwkplugin.StabilityBeta, edf.EDFOrderingPolicyFactory) + fwkplugin.Register(slodeadline.SLODeadlineOrderingPolicyType, fwkplugin.StabilityBeta, slodeadline.SLODeadlineOrderingPolicyFactory) + fwkplugin.Register(usagelimits.StaticUsageLimitPolicyType, fwkplugin.StabilityBeta, usagelimits.StaticPolicyFactory) + // Alpha + fwkplugin.Register(evictfiltering.SheddableFilterType, fwkplugin.StabilityAlpha, evictfiltering.SheddableFilterFactory) + fwkplugin.Register(evictordering.PriorityThenTimeOrderingType, fwkplugin.StabilityAlpha, evictordering.PriorityThenTimeOrderingFactory) + fwkplugin.Register(priorityholdback.PolicyType, fwkplugin.StabilityAlpha, priorityholdback.PolicyFactory) + fwkplugin.Register(softreflectiveceiling.PolicyType, fwkplugin.StabilityAlpha, softreflectiveceiling.Factory) // Register Request level data producer plugins as defaults for their respective data keys. - fwkplugin.RegisterAsDefaultProducer(reqdataprodprefix.ApproxPrefixCachePluginType, reqdataprodprefix.ApproxPrefixCacheFactory, attrprefix.PrefixCacheMatchInfoDataKey) - fwkplugin.RegisterAsDefaultProducer(inflightload.InFlightLoadProducerType, inflightload.InFlightLoadProducerFactory, attrconcurrency.InFlightLoadDataKey) - fwkplugin.RegisterAsDefaultProducer(mmproducer.ProducerType, mmproducer.Factory, mmproducer.ProducedKey) - fwkplugin.RegisterAsDefaultProducer(latencyproducer.LatencyDataProviderPluginType, latencyproducer.PredictedLatencyFactory, attrlatency.LatencyPredictionInfoDataKey) - fwkplugin.RegisterAsDefaultProducer(tokenizer.PluginType, tokenizer.PluginFactory, tokenizer.TokenizedPromptDataKey) - fwkplugin.Register(tokenizer.LegacyPluginType, tokenizer.LegacyPluginFactory) //nolint:staticcheck // intentional: keep backward compatibility - fwkplugin.RegisterAsDefaultProducer(sessionid.SessionIDProducerType, sessionid.Factory, attrsession.SessionIDDataKey) + // Beta + fwkplugin.RegisterAsDefaultProducer(reqdataprodprefix.ApproxPrefixCachePluginType, fwkplugin.StabilityBeta, reqdataprodprefix.ApproxPrefixCacheFactory, attrprefix.PrefixCacheMatchInfoDataKey) + fwkplugin.RegisterAsDefaultProducer(inflightload.InFlightLoadProducerType, fwkplugin.StabilityBeta, inflightload.InFlightLoadProducerFactory, attrconcurrency.InFlightLoadDataKey) + fwkplugin.RegisterAsDefaultProducer(latencyproducer.LatencyDataProviderPluginType, fwkplugin.StabilityBeta, latencyproducer.PredictedLatencyFactory, attrlatency.LatencyPredictionInfoDataKey) + fwkplugin.RegisterDeprecated(tokenizer.LegacyPluginType, fwkplugin.StabilityBeta, tokenizer.LegacyPluginFactory, "v0.9.0", "v0.11.0", tokenizer.PluginType) //nolint:staticcheck // deprecated in v0.9.0 (#863) + fwkplugin.RegisterAsDefaultProducer(mmproducer.ProducerType, fwkplugin.StabilityBeta, mmproducer.Factory, mmproducer.ProducedKey) + fwkplugin.RegisterAsDefaultProducer(tokenizer.PluginType, fwkplugin.StabilityBeta, tokenizer.PluginFactory, tokenizer.TokenizedPromptDataKey) + fwkplugin.RegisterAsDefaultProducer(sessionid.SessionIDProducerType, fwkplugin.StabilityBeta, sessionid.Factory, attrsession.SessionIDDataKey) // Latency predictor plugins - fwkplugin.Register(latencyslo.LatencyAdmissionPluginType, latencyslo.LatencyAdmissionFactory) - fwkplugin.Register(probabilisticadmitter.Type, probabilisticadmitter.Factory) + // Beta + fwkplugin.Register(latencyslo.LatencyAdmissionPluginType, fwkplugin.StabilityBeta, latencyslo.LatencyAdmissionFactory) + fwkplugin.Register(probabilisticadmitter.Type, fwkplugin.StabilityBeta, probabilisticadmitter.Factory) // Latency scoring and filtering plugins - fwkplugin.Register(prefixcacheaffinity.PluginType, prefixcacheaffinity.Factory) - fwkplugin.Register(sloheadroomtier.PluginType, sloheadroomtier.Factory) - fwkplugin.Register(latencyscorer.LatencyScorerType, latencyscorer.Factory) - fwkplugin.Register(bylabel.PrefillRoleType, bylabel.PrefillRoleFactory) - fwkplugin.Register(bylabel.DecodeRoleType, bylabel.DecodeRoleFactory) + // Beta + fwkplugin.Register(prefixcacheaffinity.PluginType, fwkplugin.StabilityBeta, prefixcacheaffinity.Factory) + fwkplugin.Register(sloheadroomtier.PluginType, fwkplugin.StabilityBeta, sloheadroomtier.Factory) + fwkplugin.Register(latencyscorer.LatencyScorerType, fwkplugin.StabilityBeta, latencyscorer.Factory) + fwkplugin.Register(bylabel.PrefillRoleType, fwkplugin.StabilityBeta, bylabel.PrefillRoleFactory) + fwkplugin.Register(bylabel.DecodeRoleType, fwkplugin.StabilityBeta, bylabel.DecodeRoleFactory) // register filter for test purpose only (used in conformance tests) - fwkplugin.Register(testfilter.HeaderBasedTestingFilterType, testfilter.HeaderBasedTestingFilterFactory) - // register response received plugin for test purpose only (used in conformance tests) - fwkplugin.Register(testresponsereceived.DestinationEndpointServedVerifierType, testresponsereceived.DestinationEndpointServedVerifierFactory) + // Beta + fwkplugin.Register(testfilter.HeaderBasedTestingFilterType, fwkplugin.StabilityBeta, testfilter.HeaderBasedTestingFilterFactory) + fwkplugin.Register(testresponsereceived.DestinationEndpointServedVerifierType, fwkplugin.StabilityBeta, testresponsereceived.DestinationEndpointServedVerifierFactory) + // register datalayer metrics collection plugins - fwkplugin.Register(sourcemetrics.MetricsDataSourceType, sourcemetrics.MetricsDataSourceFactory) - fwkplugin.Register(extractormetrics.MetricsExtractorType, extractormetrics.CoreMetricsExtractorFactory) + // Beta + fwkplugin.Register(sourcemetrics.MetricsDataSourceType, fwkplugin.StabilityBeta, sourcemetrics.MetricsDataSourceFactory) + fwkplugin.Register(extractormetrics.MetricsExtractorType, fwkplugin.StabilityBeta, extractormetrics.CoreMetricsExtractorFactory) + // register datalayer notification source plugins - fwkplugin.Register(sourcenotifications.NotificationSourceType, sourcenotifications.NotificationSourceFactory) - fwkplugin.Register(sourcenotifications.EndpointNotificationSourceType, sourcenotifications.EndpointSourceFactory) + // Beta + fwkplugin.Register(sourcenotifications.NotificationSourceType, fwkplugin.StabilityBeta, sourcenotifications.NotificationSourceFactory) + fwkplugin.Register(sourcenotifications.EndpointNotificationSourceType, fwkplugin.StabilityBeta, sourcenotifications.EndpointSourceFactory) + // register request control plugins - fwkplugin.Register(requestattributereporter.RequestAttributeReporterType, requestattributereporter.RequestAttributeReporterPluginFactory) - fwkplugin.Register(anthropic.AnthropicParserType, anthropic.AnthropicParserPluginFactory) - fwkplugin.Register(openai.OpenAIParserType, openai.OpenAIParserPluginFactory) - fwkplugin.Register(vllmgrpc.VllmGRPCParserType, vllmgrpc.VllmGRPCParserPluginFactory) - fwkplugin.Register(vllmhttp.VllmHTTPParserType, vllmhttp.VllmHTTPParserPluginFactory) - fwkplugin.Register(passthrough.PassthroughParserType, passthrough.PassthroughParserPluginFactory) - fwkplugin.Register(vertexai.VertexAIParserType, vertexai.VertexAIParserPluginFactory) + // Beta + fwkplugin.Register(requestattributereporter.RequestAttributeReporterType, fwkplugin.StabilityBeta, requestattributereporter.RequestAttributeReporterPluginFactory) + fwkplugin.Register(openai.OpenAIParserType, fwkplugin.StabilityBeta, openai.OpenAIParserPluginFactory) + fwkplugin.Register(vllmgrpc.VllmGRPCParserType, fwkplugin.StabilityBeta, vllmgrpc.VllmGRPCParserPluginFactory) + fwkplugin.Register(passthrough.PassthroughParserType, fwkplugin.StabilityBeta, passthrough.PassthroughParserPluginFactory) + fwkplugin.Register(anthropic.AnthropicParserType, fwkplugin.StabilityBeta, anthropic.AnthropicParserPluginFactory) + fwkplugin.Register(vllmhttp.VllmHTTPParserType, fwkplugin.StabilityBeta, vllmhttp.VllmHTTPParserPluginFactory) + fwkplugin.Register(vertexai.VertexAIParserType, fwkplugin.StabilityBeta, vertexai.VertexAIParserPluginFactory) + // register saturation detector plugins - fwkplugin.Register(concurrency.ConcurrencyDetectorType, concurrency.ConcurrencyDetectorFactory) - fwkplugin.Register(utilization.UtilizationDetectorType, utilization.UtilizationDetectorFactory) + // Beta + fwkplugin.Register(concurrency.ConcurrencyDetectorType, fwkplugin.StabilityBeta, concurrency.ConcurrencyDetectorFactory) + fwkplugin.Register(utilization.UtilizationDetectorType, fwkplugin.StabilityBeta, utilization.UtilizationDetectorFactory) + // register discovery plugins - fwkplugin.Register(discoveryfile.PluginType, discoveryfile.Factory) + // Beta + fwkplugin.Register(discoveryfile.PluginType, fwkplugin.StabilityBeta, discoveryfile.Factory) + // register request header processor plugins - fwkplugin.Register(agentidentity.PluginType, agentidentity.PluginFactory) + // Alpha + fwkplugin.Register(agentidentity.PluginType, fwkplugin.StabilityAlpha, agentidentity.PluginFactory) } func (r *Runner) parseConfigurationPhaseOne(ctx context.Context, opts *runserver.Options) (*configapi.EndpointPickerConfig, error) { @@ -685,6 +716,7 @@ func (r *Runner) parseConfigurationPhaseOne(ctx context.Context, opts *runserver loader.RegisterFeatureGate(flowcontrol.FeatureGate, false) loader.RegisterFeatureGate(runserver.HAPopulateNonLeaderDatastoreFeatureGate, true) + loader.RegisterFeatureGate(fwkplugin.ExperimentalPluginsFeatureGate, false) r.registerInTreePlugins() diff --git a/cmd/epp/runner/test_runner.go b/cmd/epp/runner/test_runner.go index 95b9a47dab..781baa8581 100644 --- a/cmd/epp/runner/test_runner.go +++ b/cmd/epp/runner/test_runner.go @@ -41,10 +41,13 @@ func NewTestRunnerSetup(ctx context.Context, cfg *rest.Config, opts *runserver.O if mockDataSource != nil { mockType := mockDataSource.TypedName().Type - fwkplugin.Register(mockType, func(name string, _ *json.Decoder, _ fwkplugin.Handle) (fwkplugin.Plugin, error) { + fwkplugin.Register(mockType, fwkplugin.StabilityStable, func(name string, _ *json.Decoder, _ fwkplugin.Handle) (fwkplugin.Plugin, error) { return mockDataSource, nil }) - defer delete(fwkplugin.Registry, mockType) + defer func() { + delete(fwkplugin.Registry, mockType) + delete(fwkplugin.RegistryMetadata, mockType) + }() } // Skip controller name validation in integration tests to avoid collisions diff --git a/pkg/epp/config/loader/configloader.go b/pkg/epp/config/loader/configloader.go index 606c18fa95..12c8af0199 100644 --- a/pkg/epp/config/loader/configloader.go +++ b/pkg/epp/config/loader/configloader.go @@ -150,11 +150,16 @@ func InstantiateAndConfigure( handle fwkplugin.Handle, logger logr.Logger, ) (*config.Config, error) { + featureGates, err := loadFeatureConfig(rawConfig.FeatureGates) + if err != nil { + return nil, fmt.Errorf("failed to load feature gates: %w", err) + } + if err := validatePlugins(rawConfig.Plugins); err != nil { return nil, fmt.Errorf("configuration validation failed: %w", err) } - if err := instantiatePlugins(rawConfig.Plugins, handle); err != nil { + if err := instantiatePlugins(rawConfig.Plugins, handle, featureGates, logger); err != nil { return nil, fmt.Errorf("plugin instantiation failed: %w", err) } @@ -172,11 +177,6 @@ func InstantiateAndConfigure( return nil, fmt.Errorf("scheduler config build failed: %w", err) } - featureGates, err := loadFeatureConfig(rawConfig.FeatureGates) - if err != nil { - return nil, fmt.Errorf("failed to load feature gates: %w", err) - } - dataConfig, err := buildDataLayerConfig(rawConfig.DataLayer, handle) if err != nil { return nil, fmt.Errorf("data layer config build failed: %w", err) @@ -226,13 +226,22 @@ func decodeRawConfig(configBytes []byte) (*configapi.EndpointPickerConfig, error return cfg, nil } -func instantiatePlugins(configuredPlugins []configapi.PluginSpec, handle fwkplugin.Handle) error { +func instantiatePlugins(configuredPlugins []configapi.PluginSpec, handle fwkplugin.Handle, featureGates map[string]bool, logger logr.Logger) error { orderedPlugins, err := buildPluginDAG(configuredPlugins, handle) if err != nil { return fmt.Errorf("failed to build plugin dependency graph: %w", err) } for _, spec := range orderedPlugins { + stability := fwkplugin.GetPluginStability(spec.Type) + if stability == fwkplugin.StabilityAlpha && !featureGates[fwkplugin.ExperimentalPluginsFeatureGate] { + return fmt.Errorf("plugin '%s' (type: '%s') has %s stability level, but feature gate '%s' is not enabled", spec.Name, spec.Type, stability, fwkplugin.ExperimentalPluginsFeatureGate) + } + + if meta, ok := fwkplugin.RegistryMetadata[spec.Type]; ok && meta.Deprecated { + logger.Info("DEPRECATION warning: plugin is deprecated", "plugin", spec.Name, "type", spec.Type) + } + factory := fwkplugin.Registry[spec.Type] plugin, err := factory(spec.Name, fwkplugin.StrictDecoder(spec.Parameters), handle) if err != nil { diff --git a/pkg/epp/config/loader/configloader_test.go b/pkg/epp/config/loader/configloader_test.go index 4846c9cf91..1a40007d83 100644 --- a/pkg/epp/config/loader/configloader_test.go +++ b/pkg/epp/config/loader/configloader_test.go @@ -81,6 +81,7 @@ func TestLoadRawConfiguration(t *testing.T) { // Register known feature gates for validation. RegisterFeatureGate(testFeatureGate, true) RegisterFeatureGate(flowcontrol.FeatureGate, false) + RegisterFeatureGate(fwkplugin.ExperimentalPluginsFeatureGate, false) queueScorerWeight := 2.0 kvCacheUtilizationScorerWeight := 2.0 @@ -129,8 +130,9 @@ func TestLoadRawConfiguration(t *testing.T) { }, }, wantFeatures: map[string]bool{ - testFeatureGate: true, - flowcontrol.FeatureGate: true, + testFeatureGate: true, + flowcontrol.FeatureGate: true, + fwkplugin.ExperimentalPluginsFeatureGate: false, }, wantErr: false, deprecated: false, @@ -188,8 +190,9 @@ func TestLoadRawConfiguration(t *testing.T) { }, }, wantFeatures: map[string]bool{ - testFeatureGate: false, - flowcontrol.FeatureGate: false, + testFeatureGate: false, + flowcontrol.FeatureGate: false, + fwkplugin.ExperimentalPluginsFeatureGate: false, }, wantErr: false, deprecated: false, @@ -256,8 +259,9 @@ func TestLoadRawConfiguration(t *testing.T) { }, }, wantFeatures: map[string]bool{ - testFeatureGate: true, - flowcontrol.FeatureGate: false, + testFeatureGate: true, + flowcontrol.FeatureGate: false, + fwkplugin.ExperimentalPluginsFeatureGate: false, }, wantErr: false, deprecated: false, @@ -429,7 +433,8 @@ func TestPluginsWithDependencies(t *testing.T) { // 2. Instantiate handle := testutils.NewTestHandle(context.Background()) - err = instantiatePlugins(rawConfig.Plugins, handle) + featureGates, _ := loadFeatureConfig(rawConfig.FeatureGates) + err = instantiatePlugins(rawConfig.Plugins, handle, featureGates, logger) if tc.wantErr { require.Error(t, err, "Expected instantiatePlugins to fail") return @@ -1008,7 +1013,7 @@ func registerTestPlugins(t *testing.T) { // Helper to generate simple factories. register := func(name string, factory fwkplugin.FactoryFunc) { - fwkplugin.Register(name, factory) + fwkplugin.Register(name, fwkplugin.StabilityStable, factory) } mockFactory := func(tType string) fwkplugin.FactoryFunc { @@ -1020,7 +1025,7 @@ func registerTestPlugins(t *testing.T) { // Register standard test mocks. register(testPluginType, mockFactory(testPluginType)) - fwkplugin.Register(testScorerType, func(name string, params *json.Decoder, _ fwkplugin.Handle) (fwkplugin.Plugin, error) { + fwkplugin.Register(testScorerType, fwkplugin.StabilityStable, func(name string, params *json.Decoder, _ fwkplugin.Handle) (fwkplugin.Plugin, error) { // Attempt to unmarshal to trigger errors for invalid JSON in tests. if params != nil { var p struct { @@ -1033,19 +1038,19 @@ func registerTestPlugins(t *testing.T) { return &mockScorer{mockPlugin{t: fwkplugin.TypedName{Name: name, Type: testScorerType}}}, nil }) - fwkplugin.Register("utilization-detector", func(name string, _ *json.Decoder, _ fwkplugin.Handle) (fwkplugin.Plugin, error) { + fwkplugin.Register("utilization-detector", fwkplugin.StabilityStable, func(name string, _ *json.Decoder, _ fwkplugin.Handle) (fwkplugin.Plugin, error) { return &mockSaturationDetector{mockPlugin{t: fwkplugin.TypedName{Name: name, Type: "utilization-detector"}}}, nil }) - fwkplugin.Register(testPickerType, func(name string, _ *json.Decoder, _ fwkplugin.Handle) (fwkplugin.Plugin, error) { + fwkplugin.Register(testPickerType, fwkplugin.StabilityStable, func(name string, _ *json.Decoder, _ fwkplugin.Handle) (fwkplugin.Plugin, error) { return &mockPicker{mockPlugin{t: fwkplugin.TypedName{Name: name, Type: testPickerType}}}, nil }) - fwkplugin.Register(testProfileHandler, func(name string, _ *json.Decoder, _ fwkplugin.Handle) (fwkplugin.Plugin, error) { + fwkplugin.Register(testProfileHandler, fwkplugin.StabilityStable, func(name string, _ *json.Decoder, _ fwkplugin.Handle) (fwkplugin.Plugin, error) { return &mockHandler{mockPlugin{t: fwkplugin.TypedName{Name: name, Type: testProfileHandler}}}, nil }) - fwkplugin.RegisterWithPluginDependencies(testWithDependencies, + fwkplugin.RegisterWithPluginDependencies(testWithDependencies, fwkplugin.StabilityStable, func(name string, decoder *json.Decoder, handle fwkplugin.Handle) (fwkplugin.Plugin, error) { rawCfg, err := mockWithDependenciesConfigParser(decoder, handle) if err != nil { @@ -1064,7 +1069,7 @@ func registerTestPlugins(t *testing.T) { }, mockWithDependenciesConfigParser, ) - fwkplugin.RegisterWithPluginDependencies(testWithNestedDependencies, + fwkplugin.RegisterWithPluginDependencies(testWithNestedDependencies, fwkplugin.StabilityStable, func(name string, decoder *json.Decoder, handle fwkplugin.Handle) (fwkplugin.Plugin, error) { rawCfg, err := mockWithNestedDependenciesConfigParser(decoder, handle) if err != nil { @@ -1093,38 +1098,38 @@ func registerTestPlugins(t *testing.T) { }, mockWithNestedDependenciesConfigParser, ) - fwkplugin.Register(testSourceType, func(name string, _ *json.Decoder, _ fwkplugin.Handle) (fwkplugin.Plugin, error) { + fwkplugin.Register(testSourceType, fwkplugin.StabilityStable, func(name string, _ *json.Decoder, _ fwkplugin.Handle) (fwkplugin.Plugin, error) { return &mockSource{mockPlugin{t: fwkplugin.TypedName{Name: name, Type: testSourceType}}}, nil }) - fwkplugin.Register(testExtractorType, func(name string, _ *json.Decoder, _ fwkplugin.Handle) (fwkplugin.Plugin, error) { + fwkplugin.Register(testExtractorType, fwkplugin.StabilityStable, func(name string, _ *json.Decoder, _ fwkplugin.Handle) (fwkplugin.Plugin, error) { return &mockExtractor{mockPlugin{t: fwkplugin.TypedName{Name: name, Type: testExtractorType}}}, nil }) - fwkplugin.Register(globalstrict.GlobalStrictFairnessPolicyType, func(name string, _ *json.Decoder, _ fwkplugin.Handle) (fwkplugin.Plugin, error) { + fwkplugin.Register(globalstrict.GlobalStrictFairnessPolicyType, fwkplugin.StabilityStable, func(name string, _ *json.Decoder, _ fwkplugin.Handle) (fwkplugin.Plugin, error) { return &fwkfcmocks.MockFairnessPolicy{ TypedNameV: fwkplugin.TypedName{Name: name, Type: globalstrict.GlobalStrictFairnessPolicyType}, }, nil }) - fwkplugin.Register(fcfs.FCFSOrderingPolicyType, func(name string, _ *json.Decoder, _ fwkplugin.Handle) (fwkplugin.Plugin, error) { + fwkplugin.Register(fcfs.FCFSOrderingPolicyType, fwkplugin.StabilityStable, func(name string, _ *json.Decoder, _ fwkplugin.Handle) (fwkplugin.Plugin, error) { return &fwkfcmocks.MockOrderingPolicy{ TypedNameV: fwkplugin.TypedName{Name: name, Type: fcfs.FCFSOrderingPolicyType}, }, nil }) // Ensure system defaults are registered too. - fwkplugin.Register(maxscore.MaxScorePickerType, maxscore.MaxScorePickerFactory) - fwkplugin.Register(single.SingleProfileHandlerType, single.SingleProfileHandlerFactory) - fwkplugin.Register(openai.OpenAIParserType, openai.OpenAIParserPluginFactory) - fwkplugin.Register(vertexai.VertexAIParserType, vertexai.VertexAIParserPluginFactory) - fwkplugin.Register(anthropic.AnthropicParserType, anthropic.AnthropicParserPluginFactory) - fwkplugin.Register(vllmhttp.VllmHTTPParserType, vllmhttp.VllmHTTPParserPluginFactory) - fwkplugin.Register(usagelimits.StaticUsageLimitPolicyType, usagelimits.StaticPolicyFactory) - fwkplugin.Register(prefix.PrefixCacheScorerPluginType, prefix.PrefixCachePluginFactory) - fwkplugin.Register(reqdataprodprefix.ApproxPrefixCachePluginType, reqdataprodprefix.ApproxPrefixCacheFactory) + fwkplugin.Register(maxscore.MaxScorePickerType, fwkplugin.StabilityStable, maxscore.MaxScorePickerFactory) + fwkplugin.Register(single.SingleProfileHandlerType, fwkplugin.StabilityStable, single.SingleProfileHandlerFactory) + fwkplugin.Register(openai.OpenAIParserType, fwkplugin.StabilityStable, openai.OpenAIParserPluginFactory) + fwkplugin.Register(vertexai.VertexAIParserType, fwkplugin.StabilityStable, vertexai.VertexAIParserPluginFactory) + fwkplugin.Register(anthropic.AnthropicParserType, fwkplugin.StabilityStable, anthropic.AnthropicParserPluginFactory) + fwkplugin.Register(vllmhttp.VllmHTTPParserType, fwkplugin.StabilityStable, vllmhttp.VllmHTTPParserPluginFactory) + fwkplugin.Register(usagelimits.StaticUsageLimitPolicyType, fwkplugin.StabilityStable, usagelimits.StaticPolicyFactory) + fwkplugin.Register(prefix.PrefixCacheScorerPluginType, fwkplugin.StabilityStable, prefix.PrefixCachePluginFactory) + fwkplugin.Register(reqdataprodprefix.ApproxPrefixCachePluginType, fwkplugin.StabilityStable, reqdataprodprefix.ApproxPrefixCacheFactory) // Datalayer plugins are now defaults; register their real factories. - fwkplugin.Register(sourcemetrics.MetricsDataSourceType, sourcemetrics.MetricsDataSourceFactory) - fwkplugin.Register(extractormetrics.MetricsExtractorType, extractormetrics.CoreMetricsExtractorFactory) + fwkplugin.Register(sourcemetrics.MetricsDataSourceType, fwkplugin.StabilityStable, sourcemetrics.MetricsDataSourceFactory) + fwkplugin.Register(extractormetrics.MetricsExtractorType, fwkplugin.StabilityStable, extractormetrics.CoreMetricsExtractorFactory) } func TestValidateSaturationDetector(t *testing.T) { @@ -1266,3 +1271,48 @@ func TestFilterExecutionOrderFromYAML(t *testing.T) { require.Equal(t, []string{"filter-A", "filter-B", "filter-C", "scorer-X", "scorer-Y", "maxScorePicker"}, pluginRefs, "Plugins slice must preserve YAML declaration order") } + +func TestExperimentalPluginsFeatureGate(t *testing.T) { + RegisterFeatureGate(fwkplugin.ExperimentalPluginsFeatureGate, false) + + const alphaPluginType = "test-alpha-plugin" + fwkplugin.Register(alphaPluginType, fwkplugin.StabilityAlpha, func(name string, _ *json.Decoder, _ fwkplugin.Handle) (fwkplugin.Plugin, error) { + return &mockPlugin{t: fwkplugin.TypedName{Name: name, Type: alphaPluginType}}, nil + }) + fwkplugin.Register(single.SingleProfileHandlerType, fwkplugin.StabilityStable, single.SingleProfileHandlerFactory) + fwkplugin.Register("utilization-detector", fwkplugin.StabilityStable, func(name string, _ *json.Decoder, _ fwkplugin.Handle) (fwkplugin.Plugin, error) { + return &mockSaturationDetector{mockPlugin{t: fwkplugin.TypedName{Name: name, Type: "utilization-detector"}}}, nil + }) + + handle := testutils.NewTestHandle(context.Background()) + logger := logging.NewTestLogger() + + // 1. Without feature gate enabled -> should fail + rawConfigNoGate := &configapi.EndpointPickerConfig{ + Plugins: []configapi.PluginSpec{ + {Name: "alpha-inst", Type: alphaPluginType}, + }, + } + _, err := InstantiateAndConfigure(rawConfigNoGate, handle, logger) + require.Error(t, err) + require.Contains(t, err.Error(), "has Alpha stability level, but feature gate 'experimentalPlugins' is not enabled") + + // 2. With feature gate enabled -> should succeed (given required profile handler) + rawConfigWithGate := &configapi.EndpointPickerConfig{ + FeatureGates: configapi.FeatureGates{fwkplugin.ExperimentalPluginsFeatureGate}, + Plugins: []configapi.PluginSpec{ + {Name: "alpha-inst", Type: alphaPluginType}, + {Name: "ph", Type: single.SingleProfileHandlerType}, + {Name: "sat", Type: "utilization-detector"}, + }, + SchedulingProfiles: []configapi.SchedulingProfile{ + {Name: "default", Plugins: []configapi.SchedulingPlugin{{PluginRef: "ph"}}}, + }, + FlowControl: &configapi.FlowControlConfig{ + SaturationDetector: &configapi.SaturationDetectorConfig{PluginRef: "sat"}, + }, + } + + _, err = InstantiateAndConfigure(rawConfigWithGate, testutils.NewTestHandle(context.Background()), logger) + require.NoError(t, err) +} diff --git a/pkg/epp/framework/interface/plugin/registry.go b/pkg/epp/framework/interface/plugin/registry.go index d673b77ed0..f730f42c73 100644 --- a/pkg/epp/framework/interface/plugin/registry.go +++ b/pkg/epp/framework/interface/plugin/registry.go @@ -45,30 +45,67 @@ func StrictDecoder(raw json.RawMessage) *json.Decoder { return dec } -// Register is a static function that can be called to register plugin factory functions. -func Register(pluginType string, factory FactoryFunc) { +// Register registers a factory function for the given plugin type along with its stability level. +func Register(pluginType string, stability StabilityLevel, factory FactoryFunc) { Registry[pluginType] = factory + RegistryMetadata[pluginType] = PluginMetadata{ + Type: pluginType, + Stability: stability, + } } -// RegisterAsDefaultProducer registers a factory for the given plugin type and records it as the -// default producer for the given data key. Only one producer may be registered as default per key. -// Out-of-tree projects that extend the EPP can call this to make their producers eligible for -// auto-configuration alongside in-tree producers. -func RegisterAsDefaultProducer(pluginType string, factory FactoryFunc, key DataKey) { - Register(pluginType, factory) +// RegisterAsDefaultProducer registers a factory for the given plugin type with an explicit +// stability level and records it as the default producer for the given data key. +func RegisterAsDefaultProducer(pluginType string, stability StabilityLevel, factory FactoryFunc, key DataKey) { + Register(pluginType, stability, factory) DefaultProducerRegistry[key.String()] = pluginType } -// RegisterWithPluginDependencies registers a factory for the given plugin type and records it as dependent on -// other plugins referenced in the configuration struct returned by the plugin's configuration parser function. -func RegisterWithPluginDependencies(pluginType string, factory FactoryFunc, parser ConfigParserFunc) { - Register(pluginType, factory) +// RegisterWithPluginDependencies registers a factory for the given plugin type with an explicit +// stability level and records it as dependent on other plugins referenced in the configuration struct +// returned by the plugin's configuration parser function. +func RegisterWithPluginDependencies(pluginType string, stability StabilityLevel, factory FactoryFunc, parser ConfigParserFunc) { + Register(pluginType, stability, factory) PluginsWithPluginDependencies[pluginType] = parser } +// RegisterDeprecated registers a plugin factory function with stability and deprecation metadata. +func RegisterDeprecated(pluginType string, stability StabilityLevel, factory FactoryFunc, deprecatedIn, scheduledRemovalIn, replacementType string) { + Register(pluginType, stability, factory) + meta := RegistryMetadata[pluginType] + meta.Deprecated = true + meta.DeprecatedIn = deprecatedIn + meta.ScheduledRemovalIn = scheduledRemovalIn + meta.ReplacementType = replacementType + RegistryMetadata[pluginType] = meta +} + +// RegisterDeprecatedWithPluginDependencies registers a plugin with dependencies and deprecation metadata. +func RegisterDeprecatedWithPluginDependencies(pluginType string, stability StabilityLevel, factory FactoryFunc, parser ConfigParserFunc, deprecatedIn, scheduledRemovalIn, replacementType string) { + RegisterWithPluginDependencies(pluginType, stability, factory, parser) + meta := RegistryMetadata[pluginType] + meta.Deprecated = true + meta.DeprecatedIn = deprecatedIn + meta.ScheduledRemovalIn = scheduledRemovalIn + meta.ReplacementType = replacementType + RegistryMetadata[pluginType] = meta +} + +// GetPluginStability looks up the stability level of a registered plugin type. +// Returns StabilityStable by default for unregistered plugins. +func GetPluginStability(pluginType string) StabilityLevel { + if meta, ok := RegistryMetadata[pluginType]; ok { + return meta.Stability + } + return StabilityStable +} + // Registry is a mapping from plugin type to Factory function. var Registry = map[string]FactoryFunc{} +// RegistryMetadata maps a plugin type to its PluginMetadata. +var RegistryMetadata = map[string]PluginMetadata{} + // DefaultProducerRegistry maps a data key to the default producer plugin name (same as type). // Populated via RegisterAsDefaultProducer. var DefaultProducerRegistry = map[string]string{} diff --git a/pkg/epp/framework/interface/plugin/registry_test.go b/pkg/epp/framework/interface/plugin/registry_test.go index 8ed89122fa..168758732e 100644 --- a/pkg/epp/framework/interface/plugin/registry_test.go +++ b/pkg/epp/framework/interface/plugin/registry_test.go @@ -31,12 +31,17 @@ func snapshotRegistries(t *testing.T) { for k, v := range Registry { origRegistry[k] = v } + origMetadata := make(map[string]PluginMetadata, len(RegistryMetadata)) + for k, v := range RegistryMetadata { + origMetadata[k] = v + } origDefaults := make(map[string]string, len(DefaultProducerRegistry)) for k, v := range DefaultProducerRegistry { origDefaults[k] = v } t.Cleanup(func() { Registry = origRegistry + RegistryMetadata = origMetadata DefaultProducerRegistry = origDefaults }) } @@ -48,24 +53,36 @@ func dummyFactory(name string, parameters *json.Decoder, handle Handle) (Plugin, func TestRegister(t *testing.T) { snapshotRegistries(t) - Register("my-plugin-type", dummyFactory) + Register("my-plugin-type", StabilityStable, dummyFactory) factory, ok := Registry["my-plugin-type"] assert.True(t, ok) assert.NotNil(t, factory) + assert.Equal(t, StabilityStable, GetPluginStability("my-plugin-type")) p, err := factory("instance-a", nil, nil) assert.NoError(t, err) assert.Equal(t, "instance-a/dummy", p.TypedName().String()) } +func TestRegisterWithStability(t *testing.T) { + snapshotRegistries(t) + + Register("alpha-plugin", StabilityAlpha, dummyFactory) + Register("beta-plugin", StabilityBeta, dummyFactory) + + assert.Equal(t, StabilityAlpha, GetPluginStability("alpha-plugin")) + assert.Equal(t, StabilityBeta, GetPluginStability("beta-plugin")) + assert.Equal(t, StabilityStable, GetPluginStability("unregistered-plugin")) +} + func TestRegister_Overwrites(t *testing.T) { snapshotRegistries(t) - Register("dup-type", dummyFactory) + Register("dup-type", StabilityStable, dummyFactory) var sentinel Plugin = &basePlugin{name: TypedName{Type: "sentinel", Name: "sentinel"}} - Register("dup-type", func(name string, parameters *json.Decoder, handle Handle) (Plugin, error) { + Register("dup-type", StabilityStable, func(name string, parameters *json.Decoder, handle Handle) (Plugin, error) { return sentinel, nil }) @@ -79,10 +96,11 @@ func TestRegisterAsDefaultProducer(t *testing.T) { key := NewDataKey("metric.cache", "default-scraper") - RegisterAsDefaultProducer("cache-scraper", dummyFactory, key) + RegisterAsDefaultProducer("cache-scraper", StabilityStable, dummyFactory, key) _, ok := Registry["cache-scraper"] assert.True(t, ok, "factory should be registered") + assert.Equal(t, StabilityStable, GetPluginStability("cache-scraper")) pluginType, ok := DefaultProducerRegistry[key.String()] assert.True(t, ok, "default producer should be recorded") @@ -94,14 +112,15 @@ func TestRegisterAsDefaultProducer_OverwritesDefault(t *testing.T) { key := NewDataKey("metric.queue", "scraper-v1") - RegisterAsDefaultProducer("scraper-v1", dummyFactory, key) - RegisterAsDefaultProducer("scraper-v2", dummyFactory, key) + RegisterAsDefaultProducer("scraper-v1", StabilityStable, dummyFactory, key) + RegisterAsDefaultProducer("scraper-v2", StabilityBeta, dummyFactory, key) assert.Equal(t, "scraper-v2", DefaultProducerRegistry[key.String()]) _, ok := Registry["scraper-v1"] assert.True(t, ok, "v1 factory should still be registered") _, ok = Registry["scraper-v2"] assert.True(t, ok) + assert.Equal(t, StabilityBeta, GetPluginStability("scraper-v2")) } func TestRegistry_IsolatedBetweenTests(t *testing.T) { @@ -110,7 +129,21 @@ func TestRegistry_IsolatedBetweenTests(t *testing.T) { const key = "isolation-marker" _, exists := Registry[key] assert.False(t, exists, "previous test must not have leaked into Registry") - Register(key, dummyFactory) + Register(key, StabilityStable, dummyFactory) _, exists = Registry[key] assert.True(t, exists) } + +func TestRegisterDeprecated(t *testing.T) { + snapshotRegistries(t) + + RegisterDeprecated("old-plugin", StabilityBeta, dummyFactory, "v0.9.0", "v0.11.0", "new-plugin") + + meta, ok := RegistryMetadata["old-plugin"] + assert.True(t, ok) + assert.True(t, meta.Deprecated) + assert.Equal(t, "v0.9.0", meta.DeprecatedIn) + assert.Equal(t, "v0.11.0", meta.ScheduledRemovalIn) + assert.Equal(t, "new-plugin", meta.ReplacementType) + assert.Equal(t, StabilityBeta, meta.Stability) +} diff --git a/pkg/epp/framework/interface/plugin/stability.go b/pkg/epp/framework/interface/plugin/stability.go new file mode 100644 index 0000000000..769163c33d --- /dev/null +++ b/pkg/epp/framework/interface/plugin/stability.go @@ -0,0 +1,42 @@ +/* +Copyright 2026 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package plugin + +// StabilityLevel defines the stability level of a plugin (Alpha, Beta, or Stable). +type StabilityLevel string + +const ( + // StabilityAlpha indicates an experimental plugin with no backwards-compatibility guarantees. + StabilityAlpha StabilityLevel = "Alpha" + // StabilityBeta indicates a feature-complete plugin following the +2 minor release deprecation policy. + StabilityBeta StabilityLevel = "Beta" + // StabilityStable indicates a production-grade plugin guaranteed to be backwards compatible. + StabilityStable StabilityLevel = "Stable" +) + +// ExperimentalPluginsFeatureGate is the feature gate name used to enable Alpha plugins. +const ExperimentalPluginsFeatureGate = "experimentalPlugins" + +// PluginMetadata holds stability and lifecycle metadata for a registered plugin type. +type PluginMetadata struct { + Type string + Stability StabilityLevel + Deprecated bool + DeprecatedIn string + ScheduledRemovalIn string + ReplacementType string +} diff --git a/pkg/epp/framework/plugins/README.md b/pkg/epp/framework/plugins/README.md index e72df40a13..ee1eda0985 100644 --- a/pkg/epp/framework/plugins/README.md +++ b/pkg/epp/framework/plugins/README.md @@ -2,7 +2,29 @@ This directory contains the available plugins. For detailed information on individual plugins, please refer to the README.md located within each plugin's respective directory. To understand how they integrate into the incoming request processing lifecycle, see the [Endpoint Picker (EPP) design](https://github.com/llm-d/llm-d/tree/main/docs/architecture/core/router/epp) document. +## Plugin Stability Levels + +Every plugin in `llm-d-router` is assigned a **Stability Level** upon registration (`cmd/epp/runner/runner.go` is the single source of truth for in-tree plugin stability): + +| Stability Level | Lifecycle & Backwards Compatibility Guarantees | Feature Gate Requirement | +|---|---|---| +| **Alpha** | Experimental features under active development. No backwards-compatibility guarantees (parameters or behavior may change anytime). | Requires `experimentalPlugins` feature gate enabled. | +| **Beta** | Feature-complete and enabled by default. Backwards-compatible within current version; subject to a +2 minor version deprecation policy before removal. | Allowed by default (no feature gate required). | +| **Stable** | Production-grade and fully backwards-compatible across minor releases. Breaking changes only on major version bumps. | Allowed by default (no feature gate required). | + +### Alpha Feature Gate (`experimentalPlugins`) + +To ensure experimental plugins are only enabled intentionally, Alpha plugins require enabling the `experimentalPlugins` feature gate in the EPP configuration: + +```yaml +featureGates: + - experimentalPlugins +``` + +If an Alpha plugin is configured while `experimentalPlugins` feature gate is not enabled (the default), the EPP runner will fail initialization with an explicit error. + ## Related Documentation - [Architecture Overview](../../../../docs/architecture.md) - [Creating a new Filter guide](../../../../docs/create_new_filter.md) + diff --git a/pkg/epp/framework/plugins/flowcontrol/eviction/filtering/sheddable.go b/pkg/epp/framework/plugins/flowcontrol/eviction/filtering/sheddable.go index 591eaa7877..9d6b1a1c9b 100644 --- a/pkg/epp/framework/plugins/flowcontrol/eviction/filtering/sheddable.go +++ b/pkg/epp/framework/plugins/flowcontrol/eviction/filtering/sheddable.go @@ -28,10 +28,6 @@ import ( // This uses the project-wide definition of sheddable from requtil.IsSheddable. const SheddableFilterType = "sheddable-eviction-filter" -func init() { - plugin.Register(SheddableFilterType, SheddableFilterFactory) -} - // SheddableFilterFactory creates a SheddableFilter plugin. func SheddableFilterFactory(name string, _ *json.Decoder, _ plugin.Handle) (plugin.Plugin, error) { f := &SheddableFilter{name: SheddableFilterType} diff --git a/pkg/epp/framework/plugins/flowcontrol/eviction/ordering/priority_time.go b/pkg/epp/framework/plugins/flowcontrol/eviction/ordering/priority_time.go index 88c866a289..265cda2a2b 100644 --- a/pkg/epp/framework/plugins/flowcontrol/eviction/ordering/priority_time.go +++ b/pkg/epp/framework/plugins/flowcontrol/eviction/ordering/priority_time.go @@ -27,10 +27,6 @@ import ( // breaking ties by newest dispatch time (least KV-cache investment). const PriorityThenTimeOrderingType = "priority-then-time-eviction-order-policy" -func init() { - plugin.Register(PriorityThenTimeOrderingType, PriorityThenTimeOrderingFactory) -} - // PriorityThenTimeOrderingFactory creates a PriorityThenTimeOrdering plugin. func PriorityThenTimeOrderingFactory(name string, _ *json.Decoder, _ plugin.Handle) (plugin.Plugin, error) { p := &PriorityThenTimeOrdering{name: PriorityThenTimeOrderingType} diff --git a/pkg/epp/framework/plugins/scheduling/profilehandler/disagg/disagg_headers_handler_test.go b/pkg/epp/framework/plugins/scheduling/profilehandler/disagg/disagg_headers_handler_test.go index 9ce1fb30ad..c642398d7f 100644 --- a/pkg/epp/framework/plugins/scheduling/profilehandler/disagg/disagg_headers_handler_test.go +++ b/pkg/epp/framework/plugins/scheduling/profilehandler/disagg/disagg_headers_handler_test.go @@ -19,8 +19,8 @@ import ( ) func TestMain(m *testing.M) { - fwkplugin.Register(DisaggHeadersHandlerType, HeadersHandlerFactory) - fwkplugin.Register(PrefillHeaderHandlerType, HeadersHandlerFactory) //nolint:staticcheck + fwkplugin.Register(DisaggHeadersHandlerType, fwkplugin.StabilityBeta, HeadersHandlerFactory) + fwkplugin.Register(PrefillHeaderHandlerType, fwkplugin.StabilityBeta, HeadersHandlerFactory) //nolint:staticcheck os.Exit(m.Run()) } From 75a2daed3f339234ea783b9e624a497c02f44ed8 Mon Sep 17 00:00:00 2001 From: Cong Liu Date: Thu, 23 Jul 2026 22:15:31 -0700 Subject: [PATCH 2/2] mark session affinity plugins alpha - they have not been used and are under major refactoring Signed-off-by: Cong Liu --- cmd/epp/runner/runner.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cmd/epp/runner/runner.go b/cmd/epp/runner/runner.go index 2e9dfd8675..78d185bc20 100644 --- a/cmd/epp/runner/runner.go +++ b/cmd/epp/runner/runner.go @@ -558,8 +558,8 @@ func (r *Runner) registerInTreePlugins() { fwkplugin.Register(bylabel.EncodeRoleType, fwkplugin.StabilityBeta, bylabel.EncodeRoleFactory) fwkplugin.Register(bylabel.DecodeRoleType, fwkplugin.StabilityBeta, bylabel.DecodeRoleFactory) fwkplugin.Register(bylabel.PrefillRoleType, fwkplugin.StabilityBeta, bylabel.PrefillRoleFactory) - fwkplugin.Register(sessionaffinityfilter.SessionAffinityType, fwkplugin.StabilityBeta, sessionaffinityfilter.Factory) // Alpha + fwkplugin.Register(sessionaffinityfilter.SessionAffinityType, fwkplugin.StabilityAlpha, sessionaffinityfilter.Factory) fwkplugin.Register(endpointattributefilter.EndpointAttributeFilterType, fwkplugin.StabilityAlpha, endpointattributefilter.EndpointAttributeFilterFactory) // dataparallel profile handler @@ -569,8 +569,9 @@ func (r *Runner) registerInTreePlugins() { // extra scheduling scorers // Beta fwkplugin.Register(loadaware.LoadAwareType, fwkplugin.StabilityBeta, loadaware.Factory) - fwkplugin.Register(sessionaffinity.SessionAffinityType, fwkplugin.StabilityBeta, sessionaffinity.Factory) fwkplugin.Register(contextlengthaware.ContextLengthAwareType, fwkplugin.StabilityBeta, contextlengthaware.Factory) + // Alpha + fwkplugin.Register(sessionaffinity.SessionAffinityType, fwkplugin.StabilityAlpha, sessionaffinity.Factory) // data layer models source/extractor // Beta