Repository: kubernetes-sigs/gateway-api-inference-extension

Issue:
Add Config API documentation & guide
Config API documentation & guide should include:
- explanation about the structure, versioning, etc (make it clear that it is not a CRD, not changeable at runtime, etc.).
- how to share same instance across extension points, including default behaviors (new instance if not mentioned otherwise), etc.
- valid config examples (could be just pointers to our usage once it's used in main).
- how to add and register out-of-tree plugins (could be pointers to llm-d?).
- recommendations (e.g., use `my-plugin` naming convention when adding out-of-tree plugin).


cc: @shmuelk @ahg-g @elevran @kfswain 

Hints from issue comments:
@shmuelk: /assign shmuelk@nirrozenbaum: @shmuelk @ahg-g I see #1110 was merged, anything left in this issue?@ahg-g: I think we are good

Solution patch:
diff --git a/Dockerfile b/Dockerfile
index 5512518..fb73765 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -19,7 +19,6 @@ RUN go mod download
 # Sources
 COPY cmd/epp ./cmd/epp
 COPY pkg/epp ./pkg/epp
-COPY conformance/testing-epp ./conformance/testing-epp
 COPY internal ./internal
 COPY api ./api
 WORKDIR /src/cmd/epp
diff --git a/Makefile b/Makefile
index 3e80ac4..dee7e99 100644
--- a/Makefile
+++ b/Makefile
@@ -140,7 +140,7 @@ test-unit: ## Run unit tests.
 	CGO_ENABLED=1 KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test ./pkg/... -race -coverprofile cover.out
 
 .PHONY: test-integration
-test-integration: ## Run integration tests.
+test-integration: envtest ## Run integration tests.
 	CGO_ENABLED=1 KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test ./test/integration/epp/... -race -coverprofile cover.out
 
 .PHONY: test-e2e
diff --git a/RELEASE.md b/RELEASE.md
index 7dad12e..c3eb310 100644
--- a/RELEASE.md
+++ b/RELEASE.md
@@ -2,6 +2,7 @@
 
 The Kubernetes Template Project is released on an as-needed basis. The process is as follows:
 
+1. Update `version/version.go` with the new semver tag
 1. An issue is proposing a new release with a changelog since the last release
 1. All [OWNERS](OWNERS) must LGTM this release
 1. An OWNER runs `git tag -s $VERSION` and inserts the changelog and pushes the tag with `git push $VERSION`
diff --git a/api/v1alpha2/inferencepool_types.go b/api/v1alpha2/inferencepool_types.go
index 214e36d..1edb7ce 100644
--- a/api/v1alpha2/inferencepool_types.go
+++ b/api/v1alpha2/inferencepool_types.go
@@ -17,7 +17,6 @@ limitations under the License.
 package v1alpha2
 
 import (
-	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
@@ -95,9 +94,14 @@ type Extension struct {
 
 // ExtensionReference is a reference to the extension.
 //
+// Connections to this extension MUST use TLS by default. Implementations MAY
+// provide a way to customize this connection to use cleartext, a different
+// protocol, or custom TLS configuration.
+//
 // If a reference is invalid, the implementation MUST update the `ResolvedRefs`
-// Condition on the InferencePool's status to `status: False`. A 5XX status code MUST be returned
-// for the request that would have otherwise been routed to the invalid backend.
+// Condition on the InferencePool's status to `status: False`. A 5XX status code
+// MUST be returned for the request that would have otherwise been routed to the
+// invalid backend.
 type ExtensionReference struct {
 	// Group is the group of the referent.
 	// The default value is "", representing the Core API group.
@@ -177,7 +181,7 @@ type InferencePoolStatus struct {
 // PoolStatus defines the observed state of InferencePool from a Gateway.
 type PoolStatus struct {
 	// GatewayRef indicates the gateway that observed state of InferencePool.
-	GatewayRef corev1.ObjectReference `json:"parentRef"`
+	GatewayRef ParentGatewayReference `json:"parentRef"`
 
 	// Conditions track the state of the InferencePool.
 	//
@@ -265,3 +269,29 @@ const (
 	// or API group, or a reference to a resource that can not be found.
 	InferencePoolReasonInvalidExtensionRef InferencePoolReason = "InvalidExtensionRef"
 )
+
+// ParentGatewayReference identifies an API object including its namespace,
+// defaulting to Gateway.
+type ParentGatewayReference struct {
+	// Group is the group of the referent.
+	//
+	// +optional
+	// +kubebuilder:default="gateway.networking.k8s.io"
+	Group *Group `json:"group"`
+
+	// Kind is kind of the referent. For example "Gateway".
+	//
+	// +optional
+	// +kubebuilder:default=Gateway
+	Kind *Kind `json:"kind"`
+
+	// Name is the name of the referent.
+	Name ObjectName `json:"name"`
+
+	// Namespace is the namespace of the referent.  If not present,
+	// the namespace of the referent is assumed to be the same as
+	// the namespace of the referring object.
+	//
+	// +optional
+	Namespace *Namespace `json:"namespace,omitempty"`
+}
diff --git a/api/v1alpha2/shared_types.go b/api/v1alpha2/shared_types.go
index ea5ef29..d3829ef 100644
--- a/api/v1alpha2/shared_types.go
+++ b/api/v1alpha2/shared_types.go
@@ -106,3 +106,24 @@ type LabelKey string
 // +kubebuilder:validation:MaxLength=63
 // +kubebuilder:validation:Pattern=`^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$`
 type LabelValue string
+
+// Namespace refers to a Kubernetes namespace. It must be a RFC 1123 label.
+//
+// This validation is based off of the corresponding Kubernetes validation:
+// https://github.com/kubernetes/apimachinery/blob/02cfb53916346d085a6c6c7c66f882e3c6b0eca6/pkg/util/validation/validation.go#L187
+//
+// This is used for Namespace name validation here:
+// https://github.com/kubernetes/apimachinery/blob/02cfb53916346d085a6c6c7c66f882e3c6b0eca6/pkg/api/validation/generic.go#L63
+//
+// Valid values include:
+//
+// * "example"
+//
+// Invalid values include:
+//
+// * "example.com" - "." is an invalid character
+//
+// +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`
+// +kubebuilder:validation:MinLength=1
+// +kubebuilder:validation:MaxLength=63
+type Namespace string
diff --git a/api/v1alpha2/zz_generated.deepcopy.go b/api/v1alpha2/zz_generated.deepcopy.go
index 218df16..f064480 100644
--- a/api/v1alpha2/zz_generated.deepcopy.go
+++ b/api/v1alpha2/zz_generated.deepcopy.go
@@ -325,6 +325,36 @@ func (in *InferencePoolStatus) DeepCopy() *InferencePoolStatus {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ParentGatewayReference) DeepCopyInto(out *ParentGatewayReference) {
+	*out = *in
+	if in.Group != nil {
+		in, out := &in.Group, &out.Group
+		*out = new(Group)
+		**out = **in
+	}
+	if in.Kind != nil {
+		in, out := &in.Kind, &out.Kind
+		*out = new(Kind)
+		**out = **in
+	}
+	if in.Namespace != nil {
+		in, out := &in.Namespace, &out.Namespace
+		*out = new(Namespace)
+		**out = **in
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ParentGatewayReference.
+func (in *ParentGatewayReference) DeepCopy() *ParentGatewayReference {
+	if in == nil {
+		return nil
+	}
+	out := new(ParentGatewayReference)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *PoolObjectReference) DeepCopyInto(out *PoolObjectReference) {
 	*out = *in
@@ -343,7 +373,7 @@ func (in *PoolObjectReference) DeepCopy() *PoolObjectReference {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *PoolStatus) DeepCopyInto(out *PoolStatus) {
 	*out = *in
-	out.GatewayRef = in.GatewayRef
+	in.GatewayRef.DeepCopyInto(&out.GatewayRef)
 	if in.Conditions != nil {
 		in, out := &in.Conditions, &out.Conditions
 		*out = make([]v1.Condition, len(*in))
diff --git a/client-go/applyconfiguration/api/v1alpha2/parentgatewayreference.go b/client-go/applyconfiguration/api/v1alpha2/parentgatewayreference.go
new file mode 100644
index 0000000..a003b8a
--- /dev/null
+++ b/client-go/applyconfiguration/api/v1alpha2/parentgatewayreference.go
@@ -0,0 +1,70 @@
+/*
+Copyright 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.
+*/
+
+// Code generated by applyconfiguration-gen. DO NOT EDIT.
+
+package v1alpha2
+
+import (
+	apiv1alpha2 "sigs.k8s.io/gateway-api-inference-extension/api/v1alpha2"
+)
+
+// ParentGatewayReferenceApplyConfiguration represents a declarative configuration of the ParentGatewayReference type for use
+// with apply.
+type ParentGatewayReferenceApplyConfiguration struct {
+	Group     *apiv1alpha2.Group      `json:"group,omitempty"`
+	Kind      *apiv1alpha2.Kind       `json:"kind,omitempty"`
+	Name      *apiv1alpha2.ObjectName `json:"name,omitempty"`
+	Namespace *apiv1alpha2.Namespace  `json:"namespace,omitempty"`
+}
+
+// ParentGatewayReferenceApplyConfiguration constructs a declarative configuration of the ParentGatewayReference type for use with
+// apply.
+func ParentGatewayReference() *ParentGatewayReferenceApplyConfiguration {
+	return &ParentGatewayReferenceApplyConfiguration{}
+}
+
+// WithGroup sets the Group field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Group field is set to the value of the last call.
+func (b *ParentGatewayReferenceApplyConfiguration) WithGroup(value apiv1alpha2.Group) *ParentGatewayReferenceApplyConfiguration {
+	b.Group = &value
+	return b
+}
+
+// WithKind sets the Kind field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Kind field is set to the value of the last call.
+func (b *ParentGatewayReferenceApplyConfiguration) WithKind(value apiv1alpha2.Kind) *ParentGatewayReferenceApplyConfiguration {
+	b.Kind = &value
+	return b
+}
+
+// WithName sets the Name field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Name field is set to the value of the last call.
+func (b *ParentGatewayReferenceApplyConfiguration) WithName(value apiv1alpha2.ObjectName) *ParentGatewayReferenceApplyConfiguration {
+	b.Name = &value
+	return b
+}
+
+// WithNamespace sets the Namespace field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Namespace field is set to the value of the last call.
+func (b *ParentGatewayReferenceApplyConfiguration) WithNamespace(value apiv1alpha2.Namespace) *ParentGatewayReferenceApplyConfiguration {
+	b.Namespace = &value
+	return b
+}
diff --git a/client-go/applyconfiguration/api/v1alpha2/poolstatus.go b/client-go/applyconfiguration/api/v1alpha2/poolstatus.go
index cfa651e..fcc0ddf 100644
--- a/client-go/applyconfiguration/api/v1alpha2/poolstatus.go
+++ b/client-go/applyconfiguration/api/v1alpha2/poolstatus.go
@@ -19,15 +19,14 @@ limitations under the License.
 package v1alpha2
 
 import (
-	v1 "k8s.io/api/core/v1"
-	metav1 "k8s.io/client-go/applyconfigurations/meta/v1"
+	v1 "k8s.io/client-go/applyconfigurations/meta/v1"
 )
 
 // PoolStatusApplyConfiguration represents a declarative configuration of the PoolStatus type for use
 // with apply.
 type PoolStatusApplyConfiguration struct {
-	GatewayRef *v1.ObjectReference                  `json:"parentRef,omitempty"`
-	Conditions []metav1.ConditionApplyConfiguration `json:"conditions,omitempty"`
+	GatewayRef *ParentGatewayReferenceApplyConfiguration `json:"parentRef,omitempty"`
+	Conditions []v1.ConditionApplyConfiguration          `json:"conditions,omitempty"`
 }
 
 // PoolStatusApplyConfiguration constructs a declarative configuration of the PoolStatus type for use with
@@ -39,15 +38,15 @@ func PoolStatus() *PoolStatusApplyConfiguration {
 // WithGatewayRef sets the GatewayRef field in the declarative configuration to the given value
 // and returns the receiver, so that objects can be built by chaining "With" function invocations.
 // If called multiple times, the GatewayRef field is set to the value of the last call.
-func (b *PoolStatusApplyConfiguration) WithGatewayRef(value v1.ObjectReference) *PoolStatusApplyConfiguration {
-	b.GatewayRef = &value
+func (b *PoolStatusApplyConfiguration) WithGatewayRef(value *ParentGatewayReferenceApplyConfiguration) *PoolStatusApplyConfiguration {
+	b.GatewayRef = value
 	return b
 }
 
 // WithConditions adds the given value to the Conditions field in the declarative configuration
 // and returns the receiver, so that objects can be build by chaining "With" function invocations.
 // If called multiple times, values provided by each call will be appended to the Conditions field.
-func (b *PoolStatusApplyConfiguration) WithConditions(values ...*metav1.ConditionApplyConfiguration) *PoolStatusApplyConfiguration {
+func (b *PoolStatusApplyConfiguration) WithConditions(values ...*v1.ConditionApplyConfiguration) *PoolStatusApplyConfiguration {
 	for i := range values {
 		if values[i] == nil {
 			panic("nil value passed to WithConditions")
diff --git a/client-go/applyconfiguration/utils.go b/client-go/applyconfiguration/utils.go
index eafadbe..ea204cd 100644
--- a/client-go/applyconfiguration/utils.go
+++ b/client-go/applyconfiguration/utils.go
@@ -52,6 +52,8 @@ func ForKind(kind schema.GroupVersionKind) interface{} {
 		return &apiv1alpha2.InferencePoolSpecApplyConfiguration{}
 	case v1alpha2.SchemeGroupVersion.WithKind("InferencePoolStatus"):
 		return &apiv1alpha2.InferencePoolStatusApplyConfiguration{}
+	case v1alpha2.SchemeGroupVersion.WithKind("ParentGatewayReference"):
+		return &apiv1alpha2.ParentGatewayReferenceApplyConfiguration{}
 	case v1alpha2.SchemeGroupVersion.WithKind("PoolObjectReference"):
 		return &apiv1alpha2.PoolObjectReferenceApplyConfiguration{}
 	case v1alpha2.SchemeGroupVersion.WithKind("PoolStatus"):
diff --git a/cmd/bbr/main.go b/cmd/bbr/main.go
index 0dffa74..d79d7c6 100644
--- a/cmd/bbr/main.go
+++ b/cmd/bbr/main.go
@@ -40,15 +40,15 @@ import (
 
 var (
 	grpcPort = flag.Int(
-		"grpcPort",
+		"grpc-port",
 		9004,
 		"The gRPC port used for communicating with Envoy proxy")
 	grpcHealthPort = flag.Int(
-		"grpcHealthPort",
+		"grpc-health-port",
 		9005,
 		"The port used for gRPC liveness and readiness probes")
 	metricsPort = flag.Int(
-		"metricsPort", 9090, "The metrics port")
+		"metrics-port", 9090, "The metrics port")
 	streaming = flag.Bool(
 		"streaming", false, "Enables streaming support for Envoy full-duplex streaming mode")
 	logVerbosity = flag.Int("v", logging.DEFAULT, "number for the log level verbosity")
diff --git a/cmd/epp/runner/register.go b/cmd/epp/runner/register.go
index 3a741d5..161a224 100644
--- a/cmd/epp/runner/register.go
+++ b/cmd/epp/runner/register.go
@@ -25,6 +25,7 @@ import (
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/framework/plugins/picker"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/framework/plugins/profile"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/framework/plugins/scorer"
+	testfilter "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/framework/plugins/test/filter"
 )
 
 // RegisterAllPlugins registers the factory functions of all known plugins
@@ -40,12 +41,14 @@ func RegisterAllPlugins() {
 	plugins.Register(profile.SingleProfileHandlerType, profile.SingleProfileHandlerFactory)
 	plugins.Register(scorer.KvCacheScorerType, scorer.KvCacheScorerFactory)
 	plugins.Register(scorer.QueueScorerType, scorer.QueueScorerFactory)
+	// register filter for test purpose only (used in conformance tests)
+	plugins.Register(testfilter.HeaderBasedTestingFilterType, testfilter.HeaderBasedTestingFilterFactory)
 }
 
 // eppHandle is an implementation of the interface plugins.Handle
 type eppHandle struct {
-	ctx     context.Context
-	plugins plugins.HandlePlugins
+	ctx context.Context
+	plugins.HandlePlugins
 }
 
 // Context returns a context the plugins can use, if they need one
@@ -53,30 +56,25 @@ func (h *eppHandle) Context() context.Context {
 	return h.ctx
 }
 
-// Plugins returns the sub-handle for working with instantiated plugins
-func (h *eppHandle) Plugins() plugins.HandlePlugins {
-	return h.plugins
-}
-
 // eppHandlePlugins implements the set of APIs to work with instantiated plugins
 type eppHandlePlugins struct {
-	thePlugins map[string]plugins.Plugin
+	plugins map[string]plugins.Plugin
 }
 
 // Plugin returns the named plugin instance
 func (h *eppHandlePlugins) Plugin(name string) plugins.Plugin {
-	return h.thePlugins[name]
+	return h.plugins[name]
 }
 
 // AddPlugin adds a plugin to the set of known plugin instances
 func (h *eppHandlePlugins) AddPlugin(name string, plugin plugins.Plugin) {
-	h.thePlugins[name] = plugin
+	h.plugins[name] = plugin
 }
 
 // GetAllPlugins returns all of the known plugins
 func (h *eppHandlePlugins) GetAllPlugins() []plugins.Plugin {
 	result := make([]plugins.Plugin, 0)
-	for _, plugin := range h.thePlugins {
+	for _, plugin := range h.plugins {
 		result = append(result, plugin)
 	}
 	return result
@@ -84,14 +82,14 @@ func (h *eppHandlePlugins) GetAllPlugins() []plugins.Plugin {
 
 // GetAllPluginsWithNames returns al of the known plugins with their names
 func (h *eppHandlePlugins) GetAllPluginsWithNames() map[string]plugins.Plugin {
-	return h.thePlugins
+	return h.plugins
 }
 
 func newEppHandle(ctx context.Context) *eppHandle {
 	return &eppHandle{
 		ctx: ctx,
-		plugins: &eppHandlePlugins{
-			thePlugins: map[string]plugins.Plugin{},
+		HandlePlugins: &eppHandlePlugins{
+			plugins: map[string]plugins.Plugin{},
 		},
 	}
 }
diff --git a/cmd/epp/runner/runner.go b/cmd/epp/runner/runner.go
index 81ec373..84c3cc8 100644
--- a/cmd/epp/runner/runner.go
+++ b/cmd/epp/runner/runner.go
@@ -21,6 +21,7 @@ import (
 	"flag"
 	"fmt"
 	"net/http/pprof"
+	"os"
 
 	"github.com/go-logr/logr"
 	"github.com/prometheus/client_golang/prometheus"
@@ -36,7 +37,6 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/metrics/filters"
 	metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
 
-	conformance_epp "sigs.k8s.io/gateway-api-inference-extension/conformance/testing-epp"
 	"sigs.k8s.io/gateway-api-inference-extension/internal/runnable"
 	backendmetrics "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend/metrics"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/common/config/loader"
@@ -46,81 +46,99 @@ import (
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/requestcontrol"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/saturationdetector"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling"
-	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/framework"
-	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/framework/plugins/multi/prefix"
-	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/framework/plugins/picker"
-	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/framework/plugins/profile"
-	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/framework/plugins/scorer"
 	runserver "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/server"
-	envutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/env"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/logging"
 )
 
 var (
 	grpcPort = flag.Int(
-		"grpcPort",
+		"grpc-port",
 		runserver.DefaultGrpcPort,
 		"The gRPC port used for communicating with Envoy proxy")
 	grpcHealthPort = flag.Int(
-		"grpcHealthPort",
-		9003,
+		"grpc-health-port",
+		runserver.DefaultGrpcHealthPort,
 		"The port used for gRPC liveness and readiness probes")
 	metricsPort = flag.Int(
-		"metricsPort", 9090, "The metrics port")
+		"metrics-port",
+		runserver.DefaultMetricsPort,
+		"The metrics port")
+	enablePprof = flag.Bool(
+		"enable-pprof",
+		runserver.DefaultEnablePprof,
+		"Enables pprof handlers. Defaults to true. Set to false to disable pprof handlers.")
 	destinationEndpointHintKey = flag.String(
-		"destinationEndpointHintKey",
+		"destination-endpoint-hint-key",
 		runserver.DefaultDestinationEndpointHintKey,
 		"Header and response metadata key used by Envoy to route to the appropriate pod. This must match Envoy configuration.")
 	destinationEndpointHintMetadataNamespace = flag.String(
-		"DestinationEndpointHintMetadataNamespace",
+		"destination-endpoint-hint-metadata-namespace",
 		runserver.DefaultDestinationEndpointHintMetadataNamespace,
 		"The key for the outer namespace struct in the metadata field of the extproc response that is used to wrap the"+
 			"target endpoint. If not set, then an outer namespace struct should not be created.")
 	poolName = flag.String(
-		"poolName",
+		"pool-name",
 		runserver.DefaultPoolName,
 		"Name of the InferencePool this Endpoint Picker is associated with.")
 	poolNamespace = flag.String(
-		"poolNamespace",
+		"pool-namespace",
 		runserver.DefaultPoolNamespace,
 		"Namespace of the InferencePool this Endpoint Picker is associated with.")
 	refreshMetricsInterval = flag.Duration(
-		"refreshMetricsInterval",
+		"refresh-metrics-interval",
 		runserver.DefaultRefreshMetricsInterval,
 		"interval to refresh metrics")
 	refreshPrometheusMetricsInterval = flag.Duration(
-		"refreshPrometheusMetricsInterval",
+		"refresh-prometheus-metrics-interval",
 		runserver.DefaultRefreshPrometheusMetricsInterval,
 		"interval to flush prometheus metrics")
-	logVerbosity  = flag.Int("v", logging.DEFAULT, "number for the log level verbosity")
+	logVerbosity = flag.Int(
+		"v",
+		logging.DEFAULT,
+		"number for the log level verbosity")
 	secureServing = flag.Bool(
-		"secureServing", runserver.DefaultSecureServing, "Enables secure serving. Defaults to true.")
-	healthChecking = flag.Bool("healthChecking", runserver.DefaultHealthChecking, "Enables health checking")
-	certPath       = flag.String(
-		"certPath", "", "The path to the certificate for secure serving. The certificate and private key files "+
+		"secure-serving",
+		runserver.DefaultSecureServing,
+		"Enables secure serving. Defaults to true.")
+	healthChecking = flag.Bool(
+		"health-checking",
+		runserver.DefaultHealthChecking,
+		"Enables health checking")
+	certPath = flag.String(
+		"cert-path",
+		runserver.DefaultCertPath,
+		"The path to the certificate for secure serving. The certificate and private key files "+
 			"are assumed to be named tls.crt and tls.key, respectively. If not set, and secureServing is enabled, "+
 			"then a self-signed certificate is used.")
 	// metric flags
-	totalQueuedRequestsMetric = flag.String("totalQueuedRequestsMetric",
-		"vllm:num_requests_waiting",
+	totalQueuedRequestsMetric = flag.String(
+		"total-queued-requests-metric",
+		runserver.DefaultTotalQueuedRequestsMetric,
 		"Prometheus metric for the number of queued requests.")
-	kvCacheUsagePercentageMetric = flag.String("kvCacheUsagePercentageMetric",
-		"vllm:gpu_cache_usage_perc",
+	kvCacheUsagePercentageMetric = flag.String(
+		"kv-cache-usage-percentage-metric",
+		runserver.DefaultKvCacheUsagePercentageMetric,
 		"Prometheus metric for the fraction of KV-cache blocks currently in use (from 0 to 1).")
 	// LoRA metrics
-	loraInfoMetric = flag.String("loraInfoMetric",
-		"vllm:lora_requests_info",
+	loraInfoMetric = flag.String(
+		"lora-info-metric",
+		runserver.DefaultLoraInfoMetric,
 		"Prometheus metric for the LoRA info metrics (must be in vLLM label format).")
 	// configuration flags
-	configFile = flag.String("configFile", "", "The path to the configuration file")
-	configText = flag.String("configText", "", "The configuration specified as text, in lieu of a file")
+	configFile = flag.String(
+		"config-file",
+		runserver.DefaultConfigFile,
+		"The path to the configuration file")
+	configText = flag.String(
+		"config-text",
+		runserver.DefaultConfigText,
+		"The configuration specified as text, in lieu of a file")
+
+	modelServerMetricsPort = flag.Int("model-server-metrics-port", 0, "Port to scrape metrics from pods. "+
+		"Default value will be set to InferencePool.Spec.TargetPortNumber if not set.")
+	modelServerMetricsPath = flag.String("model-server-metrics-path", "/metrics", "Path to scrape metrics from pods")
 
 	setupLog = ctrl.Log.WithName("setup")
-
-	// Environment variables
-	schedulerV2                       = envutil.GetEnvBool("EXPERIMENTAL_USE_SCHEDULER_V2", false, setupLog)
-	prefixCacheScheduling             = envutil.GetEnvBool("ENABLE_PREFIX_CACHE_SCHEDULING", false, setupLog)
-	reqHeaderBasedSchedulerForTesting = envutil.GetEnvBool("ENABLE_REQ_HEADER_BASED_SCHEDULER_FOR_TESTING", false, setupLog)
 )
 
 // NewRunner initializes a new EPP Runner and returns its pointer.
@@ -146,7 +164,32 @@ func (r *Runner) WithSchedulerConfig(schedulerConfig *scheduling.SchedulerConfig
 	return r
 }
 
+func bindEnvToFlags() {
+	// map[ENV_VAR]flagName   – add more as needed
+	for env, flg := range map[string]string{
+		"GRPC_PORT":                     "grpc-port",
+		"GRPC_HEALTH_PORT":              "grpc-health-port",
+		"MODEL_SERVER_METRICS_PORT":     "model-server-metrics-port",
+		"MODEL_SERVER_METRICS_PATH":     "model-server-metrics-path",
+		"DESTINATION_ENDPOINT_HINT_KEY": "destination-endpoint-hint-key",
+		"POOL_NAME":                     "pool-name",
+		"POOL_NAMESPACE":                "pool-namespace",
+		// durations & bools work too; flag.Set expects the *string* form
+		"REFRESH_METRICS_INTERVAL": "refresh-metrics-interval",
+		"SECURE_SERVING":           "secure-serving",
+	} {
+		if v := os.Getenv(env); v != "" {
+			// ignore error; Parse() will catch invalid values later
+			_ = flag.Set(flg, v)
+		}
+	}
+}
+
 func (r *Runner) Run(ctx context.Context) error {
+	// Defaults already baked into flag declarations
+	// Load env vars as "soft" overrides
+	bindEnvToFlags()
+
 	opts := zap.Options{
 		Development: true,
 	}
@@ -188,7 +231,11 @@ func (r *Runner) Run(ctx context.Context) error {
 		return err
 	}
 	verifyMetricMapping(*mapping, setupLog)
-	pmf := backendmetrics.NewPodMetricsFactory(&backendmetrics.PodMetricsClientImpl{MetricMapping: mapping}, *refreshMetricsInterval)
+	pmf := backendmetrics.NewPodMetricsFactory(&backendmetrics.PodMetricsClientImpl{
+		MetricMapping:          mapping,
+		ModelServerMetricsPort: int32(*modelServerMetricsPort),
+		ModelServerMetricsPath: *modelServerMetricsPath,
+	}, *refreshMetricsInterval)
 
 	datastore := datastore.NewDatastore(ctx, pmf)
 
@@ -215,26 +262,26 @@ func (r *Runner) Run(ctx context.Context) error {
 		setupLog.Error(err, "Failed to create controller manager")
 		return err
 	}
-	err = setupPprofHandlers(mgr)
-	if err != nil {
-		setupLog.Error(err, "Failed to setup pprof handlers")
-		return err
+
+	if *enablePprof {
+		setupLog.Info("Enabling pprof handlers")
+		err = setupPprofHandlers(mgr)
+		if err != nil {
+			setupLog.Error(err, "Failed to setup pprof handlers")
+			return err
+		}
 	}
 
-	err = r.parseConfiguration(ctx)
+	err = r.parsePluginsConfiguration(ctx)
 	if err != nil {
-		setupLog.Error(err, "Failed to parse the configuration")
+		setupLog.Error(err, "Failed to parse plugins configuration")
 		return err
 	}
 
 	// --- Initialize Core EPP Components ---
-	scheduler, err := r.initializeScheduler()
-	if err != nil {
-		setupLog.Error(err, "Failed to create scheduler")
-		return err
-	}
+	scheduler := scheduling.NewSchedulerWithConfig(r.schedulerConfig)
 
-	saturationDetector := saturationdetector.NewDetector(sdConfig, datastore, ctrl.Log)
+	saturationDetector := saturationdetector.NewDetector(sdConfig, datastore, setupLog)
 
 	director := requestcontrol.NewDirectorWithConfig(datastore, scheduler, saturationDetector, r.requestControlConfig)
 
@@ -279,62 +326,39 @@ func (r *Runner) Run(ctx context.Context) error {
 	return nil
 }
 
-func (r *Runner) initializeScheduler() (*scheduling.Scheduler, error) {
-	if r.schedulerConfig != nil {
-		return scheduling.NewSchedulerWithConfig(r.schedulerConfig), nil
+func (r *Runner) parsePluginsConfiguration(ctx context.Context) error {
+	if *configText == "" && *configFile == "" {
+		return nil // configuring through code, not through file
 	}
 
-	// otherwise, no one configured from outside scheduler config. use existing configuration
-	scheduler := scheduling.NewScheduler()
-	if schedulerV2 {
-		queueScorerWeight := envutil.GetEnvInt("QUEUE_SCORE_WEIGHT", scorer.DefaultQueueScorerWeight, setupLog)
-		kvCacheScorerWeight := envutil.GetEnvInt("KV_CACHE_SCORE_WEIGHT", scorer.DefaultKVCacheScorerWeight, setupLog)
-
-		schedulerProfile := framework.NewSchedulerProfile().
-			WithScorers(framework.NewWeightedScorer(scorer.NewQueueScorer(), queueScorerWeight),
-				framework.NewWeightedScorer(scorer.NewKVCacheScorer(), kvCacheScorerWeight)).
-			WithPicker(picker.NewMaxScorePicker())
-
-		if prefixCacheScheduling {
-			prefixScorerWeight := envutil.GetEnvInt("PREFIX_CACHE_SCORE_WEIGHT", prefix.DefaultScorerWeight, setupLog)
-			if err := schedulerProfile.AddPlugins(framework.NewWeightedScorer(prefix.New(loadPrefixCacheConfig()), prefixScorerWeight)); err != nil {
-				return nil, fmt.Errorf("Failed to register scheduler plugins - %w", err)
-			}
+	var configBytes []byte
+	if *configText != "" {
+		configBytes = []byte(*configText)
+	} else if *configFile != "" { // if config was specified through a file
+		var err error
+		configBytes, err = os.ReadFile(*configFile)
+		if err != nil {
+			return fmt.Errorf("failed to load config from a file '%s' - %w", *configFile, err)
 		}
-
-		schedulerConfig := scheduling.NewSchedulerConfig(profile.NewSingleProfileHandler(), map[string]*framework.SchedulerProfile{"schedulerv2": schedulerProfile})
-		scheduler = scheduling.NewSchedulerWithConfig(schedulerConfig)
 	}
 
-	if reqHeaderBasedSchedulerForTesting {
-		scheduler = conformance_epp.NewReqHeaderBasedScheduler()
+	handle := newEppHandle(ctx)
+	config, err := loader.LoadConfig(configBytes, handle)
+	if err != nil {
+		return fmt.Errorf("failed to load the configuration - %w", err)
 	}
 
-	return scheduler, nil
-}
-
-func (r *Runner) parseConfiguration(ctx context.Context) error {
-	if len(*configText) != 0 || len(*configFile) != 0 {
-		theConfig, err := loader.LoadConfig([]byte(*configText), *configFile)
-		if err != nil {
-			return fmt.Errorf("failed to load the configuration - %w", err)
-		}
+	setupLog.Info("Configuration file loaded", "config", config)
 
-		epp := newEppHandle(ctx)
-
-		err = loader.LoadPluginReferences(theConfig.Plugins, epp)
-		if err != nil {
-			return fmt.Errorf("failed to instantiate the plugins - %w", err)
-		}
+	r.schedulerConfig, err = loader.LoadSchedulerConfig(config.SchedulingProfiles, handle)
+	if err != nil {
+		return fmt.Errorf("failed to create Scheduler configuration - %w", err)
+	}
 
-		r.schedulerConfig, err = loader.LoadSchedulerConfig(theConfig.SchedulingProfiles, epp)
-		if err != nil {
-			return fmt.Errorf("failed to create Scheduler configuration - %w", err)
-		}
+	// Add requestControl plugins
+	r.requestControlConfig.AddPlugins(handle.GetAllPlugins()...)
 
-		// Add requestControl plugins
-		r.requestControlConfig.AddPlugins(epp.Plugins().GetAllPlugins()...)
-	}
+	log.FromContext(ctx).Info("loaded configuration from file/text successfully")
 	return nil
 }
 
@@ -356,16 +380,6 @@ func initLogging(opts *zap.Options) {
 	ctrl.SetLogger(logger)
 }
 
-func loadPrefixCacheConfig() prefix.Config {
-	baseLogger := log.Log.WithName("env-config")
-
-	return prefix.Config{
-		HashBlockSize:          envutil.GetEnvInt("PREFIX_CACHE_HASH_BLOCK_SIZE", prefix.DefaultHashBlockSize, baseLogger),
-		MaxPrefixBlocksToMatch: envutil.GetEnvInt("PREFIX_CACHE_MAX_PREFIX_BLOCKS", prefix.DefaultMaxPrefixBlocks, baseLogger),
-		LRUCapacityPerServer:   envutil.GetEnvInt("PREFIX_CACHE_LRU_CAPACITY_PER_SERVER", prefix.DefaultLRUCapacityPerServer, baseLogger),
-	}
-}
-
 // registerExtProcServer adds the ExtProcServerRunner as a Runnable to the manager.
 func registerExtProcServer(mgr manager.Manager, runner *runserver.ExtProcServerRunner, logger logr.Logger) error {
 	if err := mgr.Add(runner.AsRunnable(logger)); err != nil {
@@ -395,8 +409,8 @@ func validateFlags() error {
 	if *poolName == "" {
 		return fmt.Errorf("required %q flag not set", "poolName")
 	}
-	if len(*configText) != 0 && len(*configFile) != 0 {
-		return fmt.Errorf("both the %s and %s flags can not be set at the same time", "configText", "configFile")
+	if *configText != "" && *configFile != "" {
+		return fmt.Errorf("both the %q and %q flags can not be set at the same time", "configText", "configFile")
 	}
 
 	return nil
diff --git a/config/charts/body-based-routing/templates/bbr.yaml b/config/charts/body-based-routing/templates/bbr.yaml
index ef08ae4..0efa254 100644
--- a/config/charts/body-based-routing/templates/bbr.yaml
+++ b/config/charts/body-based-routing/templates/bbr.yaml
@@ -18,8 +18,8 @@ spec:
         image: {{ .Values.bbr.image.hub }}/{{ .Values.bbr.image.name }}:{{ .Values.bbr.image.tag }}
         imagePullPolicy: {{ .Values.bbr.image.pullPolicy | default "Always" }}
         args:
-        - "-streaming"
-        - "-v"
+        - "--streaming"
+        - "--v"
         - "3"
         ports:
         - containerPort: {{ .Values.bbr.port }}
diff --git a/config/charts/inferencepool/templates/epp-config.yaml b/config/charts/inferencepool/templates/epp-config.yaml
new file mode 100644
index 0000000..12cbd58
--- /dev/null
+++ b/config/charts/inferencepool/templates/epp-config.yaml
@@ -0,0 +1,85 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: {{ include "gateway-api-inference-extension.name" . }}
+  namespace: {{ .Release.Namespace }}
+data:
+  default-plugins.yaml: |
+    apiVersion: inference.networking.x-k8s.io/v1alpha1
+    kind: EndpointPickerConfig
+    plugins:
+    - type: low-queue-filter
+      parameters:
+        threshold: 128
+    - type: lora-affinity-filter
+      parameters:
+        threshold: 0.999
+    - type: least-queue-filter
+    - type: least-kv-cache-filter
+    - type: decision-tree-filter
+      name: low-latency-filter
+      parameters:
+        current:
+          pluginRef: low-queue-filter
+        nextOnSuccess:
+          decisionTree:
+            current:
+              pluginRef: lora-affinity-filter
+            nextOnSuccessOrFailure:
+              decisionTree:
+                current:
+                  pluginRef: least-queue-filter
+                nextOnSuccessOrFailure:
+                  decisionTree:
+                    current:
+                      pluginRef: least-kv-cache-filter
+        nextOnFailure:
+          decisionTree:
+            current:
+              pluginRef: least-queue-filter
+            nextOnSuccessOrFailure:
+              decisionTree:
+                current:
+                  pluginRef: lora-affinity-filter
+                nextOnSuccessOrFailure:
+                  decisionTree:
+                    current:
+                      pluginRef: least-kv-cache-filter
+    - type: random-picker
+      parameters:
+        maxNumOfEndpoints: 1
+    - type: single-profile-handler
+    schedulingProfiles:
+    - name: default
+      plugins:
+      - pluginRef: low-latency-filter
+      - pluginRef: random-picker
+  plugins-v2.yaml: |
+    apiVersion: inference.networking.x-k8s.io/v1alpha1
+    kind: EndpointPickerConfig
+    plugins:
+    - type: queue-scorer
+    - type: kv-cache-scorer
+    - type: prefix-cache-scorer
+      parameters:
+        hashBlockSize: 64
+        maxPrefixBlocksToMatch: 256
+        lruCapacityPerServer: 31250
+    - type: max-score-picker
+      parameters:
+        maxNumOfEndpoints: 1
+    - type: single-profile-handler
+    schedulingProfiles:
+    - name: default
+      plugins:
+      - pluginRef: queue-scorer
+        weight: 1
+      - pluginRef: kv-cache-scorer
+        weight: 1
+      - pluginRef: prefix-cache-scorer
+        weight: 1
+      - pluginRef: max-score-picker
+  {{- if (hasKey .Values.inferenceExtension "pluginsCustomConfig") }}
+  {{- .Values.inferenceExtension.pluginsCustomConfig | toYaml | nindent 2 }}
+  {{- end }}
+  
diff --git a/config/charts/inferencepool/templates/epp-deployment.yaml b/config/charts/inferencepool/templates/epp-deployment.yaml
index 0fcab49..89afbe8 100644
--- a/config/charts/inferencepool/templates/epp-deployment.yaml
+++ b/config/charts/inferencepool/templates/epp-deployment.yaml
@@ -23,24 +23,28 @@ spec:
         image: {{ .Values.inferenceExtension.image.hub }}/{{ .Values.inferenceExtension.image.name }}:{{ .Values.inferenceExtension.image.tag }}
         imagePullPolicy: {{ .Values.inferenceExtension.image.pullPolicy | default "Always" }}
         args:
-        - -poolName
+        - --pool-name
         - {{ .Release.Name }}
-        - -poolNamespace
+        - --pool-namespace
         - {{ .Release.Namespace }}
-        - -v
+        - --v
         - "3"
-        - -grpcPort
+        - --grpc-port
         - "9002"
-        - -grpcHealthPort
+        - --grpc-health-port
         - "9003"
-        - -metricsPort
+        - --metrics-port
         - "9090"
+        - --config-file
+        - "config/{{ .Values.inferenceExtension.pluginsConfigFile }}"
+        # https://pkg.go.dev/flag#hdr-Command_line_flag_syntax; space is only for non-bool flags
+        - "--enable-pprof={{ .Values.inferenceExtension.enablePprof }}"
         {{- if eq (.Values.inferencePool.modelServerType | default "vllm") "triton-tensorrt-llm" }}
-        - -totalQueuedRequestsMetric
+        - --total-queued-requests-metric
         - "nv_trt_llm_request_metrics{request_type=waiting}"
-        - -kvCacheUsagePercentageMetric
+        - --kv-cache-usage-percentage-metric
         - "nv_trt_llm_kv_cache_block_metrics{kv_cache_block_type=fraction}"
-        - -loraInfoMetric
+        - --lora-info-metric
         - "" # Set an empty metric to disable LoRA metric scraping as they are not supported by Triton yet.
         {{- end }}
         ports:
@@ -67,3 +71,10 @@ spec:
         - name: {{ $key }}
           value: {{ $value | quote }}
         {{- end }}
+        volumeMounts:
+        - name: plugins-config-volume
+          mountPath: "/config"
+      volumes:
+      - name: plugins-config-volume
+        configMap:
+          name: {{ include "gateway-api-inference-extension.name" . }}
diff --git a/config/charts/inferencepool/values.yaml b/config/charts/inferencepool/values.yaml
index f793d86..a070c4b 100644
--- a/config/charts/inferencepool/values.yaml
+++ b/config/charts/inferencepool/values.yaml
@@ -7,6 +7,27 @@ inferenceExtension:
     pullPolicy: Always
   extProcPort: 9002
   env: {}
+  enablePprof: true # Enable pprof handlers for profiling and debugging
+  # This is the plugins configuration file. 
+  pluginsConfigFile: "default-plugins.yaml"
+  # pluginsCustomConfig:
+  #   custom-plugins.yaml: |
+  #     apiVersion: inference.networking.x-k8s.io/v1alpha1
+  #     kind: EndpointPickerConfig
+  #     plugins:
+  #     - type: custom-scorer
+  #       parameters:
+  #         custom-threshold: 64
+  #     - type: max-score-picker
+  #     - type: single-profile-handler
+  #     schedulingProfiles:
+  #     - name: default
+  #       plugins:
+  #       - pluginRef: custom-scorer
+  #         weight: 1
+  #       - pluginRef: max-score-picker
+  #         weight: 1
+
   # Example environment variables:
   # env:
   #   KV_CACHE_SCORE_WEIGHT: "1"
diff --git a/config/crd/bases/inference.networking.x-k8s.io_inferencepools.yaml b/config/crd/bases/inference.networking.x-k8s.io_inferencepools.yaml
index db342d0..0c5f1b7 100644
--- a/config/crd/bases/inference.networking.x-k8s.io_inferencepools.yaml
+++ b/config/crd/bases/inference.networking.x-k8s.io_inferencepools.yaml
@@ -242,46 +242,36 @@ spec:
                       description: GatewayRef indicates the gateway that observed
                         state of InferencePool.
                       properties:
-                        apiVersion:
-                          description: API version of the referent.
-                          type: string
-                        fieldPath:
-                          description: |-
-                            If referring to a piece of an object instead of an entire object, this string
-                            should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2].
-                            For example, if the object reference is to a container within a pod, this would take on a value like:
-                            "spec.containers{name}" (where "name" refers to the name of the container that triggered
-                            the event) or if no container name is specified "spec.containers[2]" (container with
-                            index 2 in this pod). This syntax is chosen only to have some well-defined way of
-                            referencing a part of an object.
+                        group:
+                          default: gateway.networking.k8s.io
+                          description: Group is the group of the referent.
+                          maxLength: 253
+                          pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
                           type: string
                         kind:
-                          description: |-
-                            Kind of the referent.
-                            More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
+                          default: Gateway
+                          description: Kind is kind of the referent. For example "Gateway".
+                          maxLength: 63
+                          minLength: 1
+                          pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$
                           type: string
                         name:
-                          description: |-
-                            Name of the referent.
-                            More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
+                          description: Name is the name of the referent.
+                          maxLength: 253
+                          minLength: 1
                           type: string
                         namespace:
                           description: |-
-                            Namespace of the referent.
-                            More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/
-                          type: string
-                        resourceVersion:
-                          description: |-
-                            Specific resourceVersion to which this reference is made, if any.
-                            More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency
-                          type: string
-                        uid:
-                          description: |-
-                            UID of the referent.
-                            More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids
+                            Namespace is the namespace of the referent.  If not present,
+                            the namespace of the referent is assumed to be the same as
+                            the namespace of the referring object.
+                          maxLength: 63
+                          minLength: 1
+                          pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
                           type: string
+                      required:
+                      - name
                       type: object
-                      x-kubernetes-map-type: atomic
                   required:
                   - parentRef
                   type: object
diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml
deleted file mode 100644
index 5c5f0b8..0000000
--- a/config/manager/kustomization.yaml
+++ /dev/null
@@ -1,2 +0,0 @@
-resources:
-- manager.yaml
diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml
deleted file mode 100644
index 384167d..0000000
--- a/config/manager/manager.yaml
+++ /dev/null
@@ -1,95 +0,0 @@
-apiVersion: v1
-kind: Namespace
-metadata:
-  labels:
-    control-plane: controller-manager
-    app.kubernetes.io/name: api
-    app.kubernetes.io/managed-by: kustomize
-  name: system
----
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: controller-manager
-  namespace: system
-  labels:
-    control-plane: controller-manager
-    app.kubernetes.io/name: api
-    app.kubernetes.io/managed-by: kustomize
-spec:
-  selector:
-    matchLabels:
-      control-plane: controller-manager
-  replicas: 1
-  template:
-    metadata:
-      annotations:
-        kubectl.kubernetes.io/default-container: manager
-      labels:
-        control-plane: controller-manager
-    spec:
-      # TODO(user): Uncomment the following code to configure the nodeAffinity expression
-      # according to the platforms which are supported by your solution.
-      # It is considered best practice to support multiple architectures. You can
-      # build your manager image using the makefile target docker-buildx.
-      # affinity:
-      #   nodeAffinity:
-      #     requiredDuringSchedulingIgnoredDuringExecution:
-      #       nodeSelectorTerms:
-      #         - matchExpressions:
-      #           - key: kubernetes.io/arch
-      #             operator: In
-      #             values:
-      #               - amd64
-      #               - arm64
-      #               - ppc64le
-      #               - s390x
-      #           - key: kubernetes.io/os
-      #             operator: In
-      #             values:
-      #               - linux
-      securityContext:
-        runAsNonRoot: true
-        # TODO(user): For common cases that do not require escalating privileges
-        # it is recommended to ensure that all your Pods/Containers are restrictive.
-        # More info: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted
-        # Please uncomment the following code if your project does NOT have to work on old Kubernetes
-        # versions < 1.19 or on vendors versions which do NOT support this field by default (i.e. Openshift < 4.11 ).
-        # seccompProfile:
-        #   type: RuntimeDefault
-      containers:
-      - command:
-        - /manager
-        args:
-          - --leader-elect
-          - --health-probe-bind-address=:8081
-        image: controller:latest
-        name: manager
-        securityContext:
-          allowPrivilegeEscalation: false
-          capabilities:
-            drop:
-            - "ALL"
-        livenessProbe:
-          httpGet:
-            path: /healthz
-            port: 8081
-          initialDelaySeconds: 15
-          periodSeconds: 20
-        readinessProbe:
-          httpGet:
-            path: /readyz
-            port: 8081
-          initialDelaySeconds: 5
-          periodSeconds: 10
-        # TODO(user): Configure the resources accordingly based on the project requirements.
-        # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
-        resources:
-          limits:
-            cpu: 500m
-            memory: 128Mi
-          requests:
-            cpu: 10m
-            memory: 64Mi
-      serviceAccountName: controller-manager
-      terminationGracePeriodSeconds: 10
diff --git a/config/manifests/inferencepool-resources.yaml b/config/manifests/inferencepool-resources.yaml
index 34aacfa..aa8c829 100644
--- a/config/manifests/inferencepool-resources.yaml
+++ b/config/manifests/inferencepool-resources.yaml
@@ -1,6 +1,8 @@
-# Note: If you change this file, please also change the file used for e2e tests!
-# 
-# https://github.com/kubernetes-sigs/gateway-api-inference-extension/blob/main/test/testdata/inferencepool-e2e.yaml
+# Note: If you change this file, please also change:
+#  - ./test/testdata/inferencepool-e2e.yaml
+#  - ./conformance/resources/manifests/manifests.yaml
+#  - ./site-src/guides/inferencepool-rollout.md
+---
 apiVersion: inference.networking.x-k8s.io/v1alpha2
 kind: InferencePool
 metadata:
@@ -51,18 +53,20 @@ spec:
         image: us-central1-docker.pkg.dev/k8s-staging-images/gateway-api-inference-extension/epp:main
         imagePullPolicy: Always
         args:
-        - -poolName
+        - --pool-name
         - "vllm-llama3-8b-instruct"
-        - "-poolNamespace"
+        - "--pool-namespace"
         - "default"
-        - -v
+        - --v
         - "4"
         - --zap-encoder
         - "json"
-        - -grpcPort
+        - --grpc-port
         - "9002"
-        - -grpcHealthPort
+        - --grpc-health-port
         - "9003"
+        - "--config-file"
+        - "/config/default-plugins.yaml"
         ports:
         - containerPort: 9002
         - containerPort: 9003
@@ -80,6 +84,95 @@ spec:
             service: inference-extension
           initialDelaySeconds: 5
           periodSeconds: 10
+        volumeMounts:
+        - name: plugins-config-volume
+          mountPath: "/config"
+      volumes:
+      - name: plugins-config-volume
+        configMap:
+          name: plugins-config
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: plugins-config
+  namespace: default
+data:
+  default-plugins.yaml: |
+    apiVersion: inference.networking.x-k8s.io/v1alpha1
+    kind: EndpointPickerConfig
+    plugins:
+    - type: low-queue-filter
+      parameters:
+        threshold: 128
+    - type: lora-affinity-filter
+      parameters:
+        threshold: 0.999
+    - type: least-queue-filter
+    - type: least-kv-cache-filter
+    - type: decision-tree-filter
+      name: low-latency-filter
+      parameters:
+        current:
+          pluginRef: low-queue-filter
+        nextOnSuccess:
+          decisionTree:
+            current:
+              pluginRef: lora-affinity-filter
+            nextOnSuccessOrFailure:
+              decisionTree:
+                current:
+                  pluginRef: least-queue-filter
+                nextOnSuccessOrFailure:
+                  decisionTree:
+                    current:
+                      pluginRef: least-kv-cache-filter
+        nextOnFailure:
+          decisionTree:
+            current:
+              pluginRef: least-queue-filter
+            nextOnSuccessOrFailure:
+              decisionTree:
+                current:
+                  pluginRef: lora-affinity-filter
+                nextOnSuccessOrFailure:
+                  decisionTree:
+                    current:
+                      pluginRef: least-kv-cache-filter
+    - type: random-picker
+      parameters:
+        maxNumOfEndpoints: 1
+    - type: single-profile-handler
+    schedulingProfiles:
+    - name: default
+      plugins:
+      - pluginRef: low-latency-filter
+      - pluginRef: random-picker
+  plugins-v2.yaml: |
+    apiVersion: inference.networking.x-k8s.io/v1alpha1
+    kind: EndpointPickerConfig
+    plugins:
+    - type: queue-scorer
+    - type: kv-cache-scorer
+    - type: prefix-cache-scorer
+      parameters:
+        hashBlockSize: 64
+        maxPrefixBlocksToMatch: 256
+        lruCapacityPerServer: 31250
+    - type: max-score-picker
+      parameters:
+        maxNumOfEndpoints: 1
+    - type: single-profile-handler
+    schedulingProfiles:
+    - name: default
+      plugins:
+      - pluginRef: queue-scorer
+        weight: 1
+      - pluginRef: kv-cache-scorer
+        weight: 1
+      - pluginRef: prefix-cache-scorer
+        weight: 1
+      - pluginRef: max-score-picker
 ---
 kind: ClusterRole
 apiVersion: rbac.authorization.k8s.io/v1
diff --git a/config/manifests/vllm/sim-deployment.yaml b/config/manifests/vllm/sim-deployment.yaml
index 1862efe..196fe86 100644
--- a/config/manifests/vllm/sim-deployment.yaml
+++ b/config/manifests/vllm/sim-deployment.yaml
@@ -14,7 +14,7 @@ spec:
     spec:
       containers:
       - name: vllm-sim
-        image: ghcr.io/llm-d/llm-d-inference-sim:latest
+        image: ghcr.io/llm-d/llm-d-inference-sim:v0.1.2
         imagePullPolicy: Always
         args:
         - --model
diff --git a/conformance/conformance.go b/conformance/conformance.go
index 2fbcc31..5c1a317 100644
--- a/conformance/conformance.go
+++ b/conformance/conformance.go
@@ -23,7 +23,6 @@ import (
 	"errors"
 	"fmt"
 	"io/fs"
-	"os"
 	"slices"
 	"testing"
 
@@ -41,11 +40,10 @@ import (
 	"k8s.io/apimachinery/pkg/util/sets"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	k8sconfig "sigs.k8s.io/controller-runtime/pkg/client/config"
-	"sigs.k8s.io/yaml"
 
 	// Import necessary types and utilities from the core Gateway API conformance suite.
-	gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"            // Import core Gateway API types
-	confapis "sigs.k8s.io/gateway-api/conformance/apis/v1" // Report struct definition
+	gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" // Import core Gateway API types
+	// Report struct definition
 	confflags "sigs.k8s.io/gateway-api/conformance/utils/flags"
 	apikubernetes "sigs.k8s.io/gateway-api/conformance/utils/kubernetes"
 	confsuite "sigs.k8s.io/gateway-api/conformance/utils/suite"
@@ -54,6 +52,7 @@ import (
 
 	// Import the test definitions package to access the ConformanceTests slice
 	"sigs.k8s.io/gateway-api-inference-extension/conformance/tests"
+	"sigs.k8s.io/gateway-api-inference-extension/version"
 
 	// Import test packages using blank identifier
 	// This triggers the init() functions in these packages, which register the tests
@@ -244,13 +243,11 @@ func RunConformanceWithOptions(t *testing.T, opts confsuite.ConformanceOptions)
 		t.Log("Generating Inference Extension conformance report")
 		report, err := cSuite.Report() // Use the existing report generation logic.
 		require.NoError(t, err, "error generating conformance report")
-
-		// TODO: Modify the report struct here if channel, version need to be modified.
-		// Example (requires adding fields to confapis.ConformanceReport):
-		// report.GatewayAPIInferenceExtensionChannel = opts.GatewayAPIInferenceExtensionChannel
-		// report.GatewayAPIInferenceExtensionVersion = opts.GatewayAPIInferenceExtensionVersion
-
-		err = writeReport(t.Logf, *report, opts.ReportOutputPath)
+		inferenceReport := GatewayAPIInferenceExtensionConformanceReport{
+			GatewayAPIInferenceExtensionVersion: version.BundleVersion,
+			ConformanceReport:                   *report,
+		}
+		err = inferenceReport.WriteReport(t.Logf, opts.ReportOutputPath)
 		require.NoError(t, err, "error writing conformance report")
 	}
 }
@@ -347,23 +344,3 @@ func ensureGatewayAvailableAndReady(t *testing.T, k8sClient client.Client, opts
 	require.NoErrorf(t, err, "shared gateway %s/%s did not get an address", gatewayNN.Namespace, gatewayNN.Name)
 	t.Logf("Shared Gateway %s/%s is ready.", gatewayNN.Namespace, gatewayNN.Name)
 }
-
-// writeReport writes the generated conformance report to the specified output file or logs it.
-// Adapted from the core Gateway API suite.
-func writeReport(logf func(string, ...any), report confapis.ConformanceReport, output string) error {
-	rawReport, err := yaml.Marshal(report)
-	if err != nil {
-		return fmt.Errorf("error marshaling report: %w", err)
-	}
-
-	if output != "" {
-		if err = os.WriteFile(output, rawReport, 0o600); err != nil {
-			return fmt.Errorf("error writing report file %s: %w", output, err)
-		}
-		logf("Conformance report written to %s", output)
-	} else {
-		// Log the report YAML to stdout if no output file is specified.
-		logf("Conformance report:\n%s", string(rawReport))
-	}
-	return nil
-}
diff --git a/conformance/conformancereport.go b/conformance/conformancereport.go
new file mode 100644
index 0000000..46df3ac
--- /dev/null
+++ b/conformance/conformancereport.go
@@ -0,0 +1,56 @@
+/*
+Copyright 2025 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 conformance contains the core setup and execution logic
+// for the Gateway API Inference Extension conformance test suite.
+package conformance
+
+import (
+	"fmt"
+	"os"
+
+	confapis "sigs.k8s.io/gateway-api/conformance/apis/v1"
+	"sigs.k8s.io/yaml"
+)
+
+// GatewayAPIInferenceExtensionConformanceReport is a report of conformance testing results of
+// gateway-api-inference-extension including the specific conformance profiles that were tested
+// and the results of the tests with summaries and statistics.
+type GatewayAPIInferenceExtensionConformanceReport struct {
+	// GatewayAPIInferenceExtensionVersion is the version of the gateway-api-inference-extension the tests run against.
+	GatewayAPIInferenceExtensionVersion string
+	// ConformanceReport is the fields reused from the gateway-api conformance reports.
+	confapis.ConformanceReport
+}
+
+// WriteReport writes the generated conformance report to the specified output file or logs it.
+func (report *GatewayAPIInferenceExtensionConformanceReport) WriteReport(logf func(string, ...any), output string) error {
+	rawReport, err := yaml.Marshal(*report)
+	if err != nil {
+		return fmt.Errorf("error marshaling report: %w", err)
+	}
+
+	if output != "" {
+		if err = os.WriteFile(output, rawReport, 0o600); err != nil {
+			return fmt.Errorf("error writing report file %s: %w", output, err)
+		}
+		logf("Conformance report written to %s", output)
+	} else {
+		// Log the report YAML to stdout if no output file is specified.
+		logf("Conformance report:\n%s", string(rawReport))
+	}
+	return nil
+}
diff --git a/conformance/reports/README.md b/conformance/reports/README.md
index ba0278d..d26ff14 100644
--- a/conformance/reports/README.md
+++ b/conformance/reports/README.md
@@ -27,13 +27,14 @@ This folder stores conformance reports organized first by the version of the Gat
 
 ## Implementation Submissions
 
-Each implementation conformant with a specific profile of a specific version of the Gateway API Inference Extension should have its own folder within the corresponding version and profile directory (e.g., `/conformance/reports/v0.3.0/Gateway/my-implementation/`).
+Each implementation conformant with a specific profile of a specific version of the Gateway API Inference Extension should have its own folder within the corresponding version and profile directory (e.g., `/conformance/reports/v0.3.0/gateway/my-implementation/`).
 
 The implementation is the owner of its folder and is responsible for:
 
 1.  Uploading one or more conformance reports (YAML files).
 2.  Maintaining a mandatory `README.md` file within their folder, structured as follows:
 
+```
     # My Inference Gateway Implementation (Gateway Profile Conformance)
 
     General information about the My/Implementation project.
@@ -48,6 +49,7 @@ The implementation is the owner of its folder and is responsible for:
     ## Reproduce
 
     Instructions on how to reproduce the claimed report(s).
+```
 
 ### Table of Contents (within Implementation README)
 
@@ -89,7 +91,7 @@ To be accepted, submitted conformance reports must comply with the following rul
 
 Conformance reports demonstrating a `success` result for a specific profile (e.g., `Gateway`) should be submitted via Pull Request directly to this repository (`kubernetes-sigs/gateway-api-inference-extension`).
 
-1.  Create a new folder structure under `/conformance/reports/<extension-version>/<profile-name>/` named after your implementation (e.g., `/conformance/reports/v0.3.0/Gateway/my-implementation/`).
+1.  Create a new folder structure under `/conformance/reports/<extension-version>/<profile-name>/` named after your implementation (e.g., `/conformance/reports/v0.3.0/gateway/my-implementation/`).
 2.  Add your implementation's `README.md` to this folder, following the structure described above.
 3.  Add your generated conformance report YAML file(s) to this folder, ensuring they follow the naming convention `<Implementation Version>-<Mode>-<Profile>-report.yaml`.
 4.  Submit the Pull Request.
diff --git a/conformance/resources/manifests/manifests.yaml b/conformance/resources/manifests/manifests.yaml
index 5b1f2d4..7c7d466 100644
--- a/conformance/resources/manifests/manifests.yaml
+++ b/conformance/resources/manifests/manifests.yaml
@@ -199,23 +199,20 @@ spec:
         image: us-central1-docker.pkg.dev/k8s-staging-images/gateway-api-inference-extension/epp:main
         imagePullPolicy: Always
         args:
-        - -poolName
+        - --pool-name
         - "primary-inference-pool"
-        - -poolNamespace
+        - --pool-namespace
         - "gateway-conformance-app-backend"
-        - -v
+        - --v
         - "4"
         - --zap-encoder
         - "json"
-        - -grpcPort
+        - --grpc-port
         - "9002"
-        - -grpcHealthPort
+        - --grpc-health-port
         - "9003"
-        env:
-        - name: USE_STREAMING
-          value: "true"
-        - name: ENABLE_REQ_HEADER_BASED_SCHEDULER_FOR_TESTING # Used for conformance test.
-          value: "true"
+        - "--config-file"
+        - "/config/conformance-plugins.yaml"
         ports:
         - containerPort: 9002
         - containerPort: 9003
@@ -233,6 +230,13 @@ spec:
             service: inference-extension
           initialDelaySeconds: 5
           periodSeconds: 10
+        volumeMounts:
+        - name: plugins-config-volume
+          mountPath: "/config"
+      volumes:
+      - name: plugins-config-volume
+        configMap:
+          name: plugins-config
 ---
 # --- Secondary InferencePool Definition ---
 apiVersion: inference.networking.x-k8s.io/v1alpha2
@@ -289,23 +293,20 @@ spec:
         image: us-central1-docker.pkg.dev/k8s-staging-images/gateway-api-inference-extension/epp:main
         imagePullPolicy: Always
         args:
-        - -poolName
+        - --pool-name
         - "secondary-inference-pool"
-        - -poolNamespace
+        - --pool-namespace
         - "gateway-conformance-app-backend"
-        - -v
+        - --v
         - "4"
         - --zap-encoder
         - "json"
-        - -grpcPort
+        - --grpc-port
         - "9002"
-        - -grpcHealthPort
+        - --grpc-health-port
         - "9003"
-        env:
-        - name: USE_STREAMING
-          value: "true"
-        - name: ENABLE_REQ_HEADER_BASED_SCHEDULER_FOR_TESTING # Used for conformance test.
-          value: "true"
+        - "--config-file"
+        - "/config/conformance-plugins.yaml"
         ports:
         - containerPort: 9002
         - containerPort: 9003
@@ -323,6 +324,34 @@ spec:
             service: inference-extension
           initialDelaySeconds: 5
           periodSeconds: 10
+        volumeMounts:
+        - name: plugins-config-volume
+          mountPath: "/config"
+      volumes:
+      - name: plugins-config-volume
+        configMap:
+          name: plugins-config
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: plugins-config
+  namespace: gateway-conformance-app-backend
+data:
+  conformance-plugins.yaml: |
+    apiVersion: inference.networking.x-k8s.io/v1alpha1
+    kind: EndpointPickerConfig
+    plugins:
+    - type: header-based-testing-filter
+    - type: max-score-picker
+      parameters:
+        maxNumOfEndpoints: 1
+    - type: single-profile-handler
+    schedulingProfiles:
+    - name: conformance-profile
+      plugins:
+      - pluginRef: header-based-testing-filter
+      - pluginRef: max-score-picker
 ---
 # --- Required Role and RoleBinding for Conformance Test for EPP ---
 apiVersion: rbac.authorization.k8s.io/v1
diff --git a/conformance/utils/kubernetes/helpers.go b/conformance/utils/kubernetes/helpers.go
index f2e1e21..6f40999 100644
--- a/conformance/utils/kubernetes/helpers.go
+++ b/conformance/utils/kubernetes/helpers.go
@@ -127,7 +127,8 @@ func InferencePoolMustHaveCondition(t *testing.T, c client.Reader, poolNN types.
 				debugMsg += " (No parent statuses reported)"
 			}
 			for i, parentStatus := range lastObservedPool.Status.Parents {
-				debugMsg += fmt.Sprintf("\n  Parent %d (Gateway: %s/%s):", i, parentStatus.GatewayRef.Namespace, parentStatus.GatewayRef.Name)
+				namespace := parentStatus.GatewayRef.Namespace
+				debugMsg += fmt.Sprintf("\n  Parent %d (Gateway: %s/%s):", i, *namespace, parentStatus.GatewayRef.Name)
 				if len(parentStatus.Conditions) == 0 {
 					debugMsg += " (No conditions reported for this parent)"
 				}
diff --git a/go.mod b/go.mod
index a0cc2e1..1a5f824 100644
--- a/go.mod
+++ b/go.mod
@@ -18,7 +18,7 @@ require (
 	github.com/stretchr/testify v1.10.0
 	go.uber.org/multierr v1.11.0
 	go.uber.org/zap v1.27.0
-	golang.org/x/sync v0.15.0
+	golang.org/x/sync v0.16.0
 	google.golang.org/grpc v1.73.0
 	google.golang.org/protobuf v1.36.6
 	k8s.io/api v0.33.2
diff --git a/go.sum b/go.sum
index 9a74921..e720e60 100644
--- a/go.sum
+++ b/go.sum
@@ -245,8 +245,8 @@ golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKl
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
-golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
+golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
diff --git a/mkdocs.yml b/mkdocs.yml
index d22d937..efa7aef 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -70,6 +70,7 @@ nav:
         - InferencePool Rollout: guides/inferencepool-rollout.md
       - Metrics and Observability: guides/metrics-and-observability.md
       - Configuration Guide:
+          - Configuring the plugins via configuration files or text: guides/epp-configuration/config-text.md    
           - Prefix Cache Aware Plugin: guides/epp-configuration/prefix-aware.md
     - Implementer Guides:
       - Getting started: guides/implementers.md
diff --git a/pkg/epp/backend/metrics/fake.go b/pkg/epp/backend/metrics/fake.go
index 5599d4e..6828318 100644
--- a/pkg/epp/backend/metrics/fake.go
+++ b/pkg/epp/backend/metrics/fake.go
@@ -56,7 +56,7 @@ type FakePodMetricsClient struct {
 	Res   map[types.NamespacedName]*MetricsState
 }
 
-func (f *FakePodMetricsClient) FetchMetrics(ctx context.Context, pod *backend.Pod, existing *MetricsState, port int32) (*MetricsState, error) {
+func (f *FakePodMetricsClient) FetchMetrics(ctx context.Context, pod *backend.Pod, existing *MetricsState, _ int32) (*MetricsState, error) {
 	f.errMu.RLock()
 	err, ok := f.Err[pod.NamespacedName]
 	f.errMu.RUnlock()
diff --git a/pkg/epp/backend/metrics/metrics.go b/pkg/epp/backend/metrics/metrics.go
index 8899e00..590685c 100644
--- a/pkg/epp/backend/metrics/metrics.go
+++ b/pkg/epp/backend/metrics/metrics.go
@@ -37,15 +37,14 @@ const (
 )
 
 type PodMetricsClientImpl struct {
-	MetricMapping *MetricMapping
+	MetricMapping          *MetricMapping
+	ModelServerMetricsPort int32
+	ModelServerMetricsPath string
 }
 
 // FetchMetrics fetches metrics from a given pod, clones the existing metrics object and returns an updated one.
 func (p *PodMetricsClientImpl) FetchMetrics(ctx context.Context, pod *backend.Pod, existing *MetricsState, port int32) (*MetricsState, error) {
-	// Currently the metrics endpoint is hard-coded, which works with vLLM.
-	// TODO(https://github.com/kubernetes-sigs/gateway-api-inference-extension/issues/16): Consume this from InferencePool config.
-	url := "http://" + pod.Address + ":" + strconv.Itoa(int(port)) + "/metrics"
-
+	url := p.getMetricEndpoint(pod, port)
 	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
 	if err != nil {
 		return nil, fmt.Errorf("failed to create request: %v", err)
@@ -70,6 +69,13 @@ func (p *PodMetricsClientImpl) FetchMetrics(ctx context.Context, pod *backend.Po
 	return p.promToPodMetrics(metricFamilies, existing)
 }
 
+func (p *PodMetricsClientImpl) getMetricEndpoint(pod *backend.Pod, targetPortNumber int32) string {
+	if p.ModelServerMetricsPort == 0 {
+		p.ModelServerMetricsPort = targetPortNumber
+	}
+	return fmt.Sprintf("http://%s:%d%s", pod.Address, p.ModelServerMetricsPort, p.ModelServerMetricsPath)
+}
+
 // promToPodMetrics updates internal pod metrics with scraped Prometheus metrics.
 func (p *PodMetricsClientImpl) promToPodMetrics(
 	metricFamilies map[string]*dto.MetricFamily,
diff --git a/pkg/epp/common/config/loader/configloader.go b/pkg/epp/common/config/loader/configloader.go
index 864a3ea..fab130e 100644
--- a/pkg/epp/common/config/loader/configloader.go
+++ b/pkg/epp/common/config/loader/configloader.go
@@ -19,13 +19,12 @@ package loader
 import (
 	"errors"
 	"fmt"
-	"os"
 
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/runtime/serializer"
 	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
+	"k8s.io/apimachinery/pkg/util/sets"
 
-	"sigs.k8s.io/gateway-api-inference-extension/api/config/v1alpha1"
 	configapi "sigs.k8s.io/gateway-api-inference-extension/api/config/v1alpha1"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/plugins"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling"
@@ -39,77 +38,54 @@ func init() {
 	utilruntime.Must(configapi.Install(scheme))
 }
 
-// Load config either from supplied text or from a file
-func LoadConfig(configText []byte, fileName string) (*configapi.EndpointPickerConfig, error) {
-	var err error
-	if len(configText) == 0 {
-		configText, err = os.ReadFile(fileName)
-		if err != nil {
-			return nil, fmt.Errorf("failed to load config file. Error: %s", err)
-		}
-	}
-
-	theConfig := &configapi.EndpointPickerConfig{}
+// Load config from supplied text that was converted to []byte
+func LoadConfig(configBytes []byte, handle plugins.Handle) (*configapi.EndpointPickerConfig, error) {
+	config := &configapi.EndpointPickerConfig{}
 
 	codecs := serializer.NewCodecFactory(scheme, serializer.EnableStrict)
-	err = runtime.DecodeInto(codecs.UniversalDecoder(), configText, theConfig)
+	err := runtime.DecodeInto(codecs.UniversalDecoder(), configBytes, config)
 	if err != nil {
-		return nil, fmt.Errorf("the configuration is invalid. Error: %s", err)
+		return nil, fmt.Errorf("the configuration is invalid - %w", err)
 	}
 
-	// Validate loaded configuration
-	err = validateConfiguration(theConfig)
-	if err != nil {
-		return nil, fmt.Errorf("the configuration is invalid. error: %s", err)
+	// instantiate loaded plugins
+	if err = instantiatePlugins(config.Plugins, handle); err != nil {
+		return nil, fmt.Errorf("failed to instantiate plugins - %w", err)
 	}
-	return theConfig, nil
-}
 
-func LoadPluginReferences(thePlugins []configapi.PluginSpec, handle plugins.Handle) error {
-	for _, pluginConfig := range thePlugins {
-		thePlugin, err := instantiatePlugin(pluginConfig, handle)
-		if err != nil {
-			return err
-		}
-		handle.Plugins().AddPlugin(pluginConfig.Name, thePlugin)
+	if err = validateSchedulingProfiles(config); err != nil {
+		return nil, fmt.Errorf("failed to validate scheduling profiles - %w", err)
 	}
-	return nil
-}
 
-func LoadSchedulerConfig(configProfiles []v1alpha1.SchedulingProfile, handle plugins.Handle) (*scheduling.SchedulerConfig, error) {
-
-	var profiles = map[string]*framework.SchedulerProfile{}
-
-	for _, configProfile := range configProfiles {
-		profile := framework.SchedulerProfile{}
+	return config, nil
+}
 
-		for _, plugin := range configProfile.Plugins {
-			var err error
-			thePlugin := handle.Plugins().Plugin(plugin.PluginRef)
-			if theScorer, ok := thePlugin.(framework.Scorer); ok {
+func LoadSchedulerConfig(configProfiles []configapi.SchedulingProfile, handle plugins.Handle) (*scheduling.SchedulerConfig, error) {
+	profiles := map[string]*framework.SchedulerProfile{}
+	for _, namedProfile := range configProfiles {
+		profile := framework.NewSchedulerProfile()
+		for _, plugin := range namedProfile.Plugins {
+			referencedPlugin := handle.Plugin(plugin.PluginRef)
+			if scorer, ok := referencedPlugin.(framework.Scorer); ok {
 				if plugin.Weight == nil {
 					return nil, fmt.Errorf("scorer '%s' is missing a weight", plugin.PluginRef)
 				}
-				thePlugin = framework.NewWeightedScorer(theScorer, *plugin.Weight)
+				referencedPlugin = framework.NewWeightedScorer(scorer, *plugin.Weight)
 			}
-			err = profile.AddPlugins(thePlugin)
-			if err != nil {
-				return nil, err
+			if err := profile.AddPlugins(referencedPlugin); err != nil {
+				return nil, fmt.Errorf("failed to load scheduler config - %w", err)
 			}
 		}
-		profiles[configProfile.Name] = &profile
+		profiles[namedProfile.Name] = profile
 	}
 
 	var profileHandler framework.ProfileHandler
-	var profileHandlerName string
-
-	for pluginName, thePlugin := range handle.Plugins().GetAllPluginsWithNames() {
-		if theProfileHandler, ok := thePlugin.(framework.ProfileHandler); ok {
+	for pluginName, plugin := range handle.GetAllPluginsWithNames() {
+		if theProfileHandler, ok := plugin.(framework.ProfileHandler); ok {
 			if profileHandler != nil {
-				return nil, fmt.Errorf("only one profile handler is allowed. Both %s and %s are profile handlers", profileHandlerName, pluginName)
+				return nil, fmt.Errorf("only one profile handler is allowed. Both %s and %s are profile handlers", profileHandler.TypedName().Name, pluginName)
 			}
 			profileHandler = theProfileHandler
-			profileHandlerName = pluginName
 		}
 	}
 	if profileHandler == nil {
@@ -119,62 +95,61 @@ func LoadSchedulerConfig(configProfiles []v1alpha1.SchedulingProfile, handle plu
 	return scheduling.NewSchedulerConfig(profileHandler, profiles), nil
 }
 
-func instantiatePlugin(pluginSpec configapi.PluginSpec, handle plugins.Handle) (plugins.Plugin, error) {
-	factory, ok := plugins.Registry[pluginSpec.Type]
-	if !ok {
-		return nil, fmt.Errorf("failed to instantiate the plugin. plugin type %s not found", pluginSpec.Type)
-	}
-	thePlugin, err := factory(pluginSpec.Name, pluginSpec.Parameters, handle)
-	if err != nil {
-		return nil, fmt.Errorf("failed to instantiate the plugin type %s. Error: %s", pluginSpec.Type, err)
-	}
-	return thePlugin, err
-}
-
-func validateConfiguration(theConfig *configapi.EndpointPickerConfig) error {
-	names := make(map[string]struct{})
+func instantiatePlugins(configuredPlugins []configapi.PluginSpec, handle plugins.Handle) error {
+	pluginNames := sets.New[string]() // set of plugin names, a name must be unique
 
-	for _, pluginConfig := range theConfig.Plugins {
+	for _, pluginConfig := range configuredPlugins {
 		if pluginConfig.Type == "" {
-			return fmt.Errorf("plugin definition for %s is missing a type", pluginConfig.Name)
+			return fmt.Errorf("plugin definition for '%s' is missing a type", pluginConfig.Name)
 		}
 
-		if _, ok := names[pluginConfig.Name]; ok {
-			return fmt.Errorf("plugin name %s used more than once", pluginConfig.Name)
+		if pluginNames.Has(pluginConfig.Name) {
+			return fmt.Errorf("plugin name '%s' used more than once", pluginConfig.Name)
 		}
-		names[pluginConfig.Name] = struct{}{}
+		pluginNames.Insert(pluginConfig.Name)
 
-		_, ok := plugins.Registry[pluginConfig.Type]
+		factory, ok := plugins.Registry[pluginConfig.Type]
 		if !ok {
-			return fmt.Errorf("plugin type %s is not found", pluginConfig.Type)
+			return fmt.Errorf("plugin type '%s' is not found in registry", pluginConfig.Type)
 		}
+
+		plugin, err := factory(pluginConfig.Name, pluginConfig.Parameters, handle)
+		if err != nil {
+			return fmt.Errorf("failed to instantiate the plugin type '%s' - %w", pluginConfig.Type, err)
+		}
+
+		handle.AddPlugin(pluginConfig.Name, plugin)
 	}
 
-	if len(theConfig.SchedulingProfiles) == 0 {
+	return nil
+}
+
+func validateSchedulingProfiles(config *configapi.EndpointPickerConfig) error {
+	if len(config.SchedulingProfiles) == 0 {
 		return errors.New("there must be at least one scheduling profile in the configuration")
 	}
 
-	names = map[string]struct{}{}
-	for _, profile := range theConfig.SchedulingProfiles {
+	profileNames := sets.New[string]()
+	for _, profile := range config.SchedulingProfiles {
 		if profile.Name == "" {
-			return errors.New("SchedulingProfiles need a name")
+			return errors.New("SchedulingProfile must have a name")
 		}
 
-		if _, ok := names[profile.Name]; ok {
-			return fmt.Errorf("the name %s has been specified for more than one SchedulingProfile", profile.Name)
+		if profileNames.Has(profile.Name) {
+			return fmt.Errorf("the name '%s' has been specified for more than one SchedulingProfile", profile.Name)
 		}
-		names[profile.Name] = struct{}{}
+		profileNames.Insert(profile.Name)
 
 		if len(profile.Plugins) == 0 {
-			return errors.New("SchedulingProfiles need at least one plugin")
+			return fmt.Errorf("SchedulingProfile '%s' must have at least one plugin", profile.Name)
 		}
 		for _, plugin := range profile.Plugins {
 			if len(plugin.PluginRef) == 0 {
-				return errors.New("SchedulingProfile's plugins need a plugin reference")
+				return fmt.Errorf("SchedulingProfile '%s' plugins must have a plugin reference", profile.Name)
 			}
 
 			notFound := true
-			for _, pluginConfig := range theConfig.Plugins {
+			for _, pluginConfig := range config.Plugins {
 				if plugin.PluginRef == pluginConfig.Name {
 					notFound = false
 					break
diff --git a/pkg/epp/flowcontrol/framework/doc.go b/pkg/epp/flowcontrol/framework/doc.go
new file mode 100644
index 0000000..374c52a
--- /dev/null
+++ b/pkg/epp/flowcontrol/framework/doc.go
@@ -0,0 +1,32 @@
+/*
+Copyright 2025 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 framework defines the core plugin interfaces for extending the `controller.FlowController`.
+//
+// It establishes the contracts that custom logic, such as queueing disciplines and dispatching policies, must adhere
+// to. By building on these interfaces, the Flow Control system can be extended and customized without modifying the
+// core controller logic.
+//
+// The primary contracts are:
+//   - `SafeQueue`: An interface for concurrent-safe queue implementations.
+//   - `IntraFlowDispatchPolicy`: An interface for policies that decide which item to select from within a single flow's
+//     queue.
+//   - `ItemComparator`: An interface vended by policies to make their internal item-ordering logic explicit and
+//     available to other components.
+//
+// These components are linked by `QueueCapability`, which allows policies to declare their queue requirements (e.g.,
+// FIFO or priority-based ordering).
+package framework
diff --git a/pkg/epp/flowcontrol/framework/errors.go b/pkg/epp/flowcontrol/framework/errors.go
new file mode 100644
index 0000000..e994aaf
--- /dev/null
+++ b/pkg/epp/flowcontrol/framework/errors.go
@@ -0,0 +1,44 @@
+/*
+Copyright 2025 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 framework
+
+import (
+	"errors"
+)
+
+// `SafeQueue` Errors
+//
+// These errors relate to operations directly on a `SafeQueue` implementation. They are returned by `SafeQueue` methods
+// and might be handled or wrapped by the `ports.FlowRegistry`'s `ports.ManagedQueue` or the
+// `controller.FlowController`.
+var (
+	// ErrNilQueueItem indicates that a nil `types.QueueItemAccessor` was passed to `SafeQueue.Add()`.
+	ErrNilQueueItem = errors.New("queue item cannot be nil")
+
+	// ErrQueueEmpty indicates an attempt to perform an operation on an empty `SafeQueue` that requires one or more items
+	// (e.g., calling `SafeQueue.PeekHead()`).
+	ErrQueueEmpty = errors.New("queue is empty")
+
+	// ErrInvalidQueueItemHandle indicates that a `types.QueueItemHandle` provided to a `SafeQueue` operation (e.g.,
+	// `SafeQueue.Remove()`) is not valid for that queue, has been invalidated, or does not correspond to an actual item
+	// in the queue.
+	ErrInvalidQueueItemHandle = errors.New("invalid queue item handle")
+
+	// ErrQueueItemNotFound indicates that a `SafeQueue.Remove(handle)` operation did not find an item matching the
+	// provided, valid `types.QueueItemHandle`. This can occur if the item was removed by a concurrent operation.
+	ErrQueueItemNotFound = errors.New("queue item not found for the given handle")
+)
diff --git a/pkg/epp/flowcontrol/framework/mocks/mocks.go b/pkg/epp/flowcontrol/framework/mocks/mocks.go
new file mode 100644
index 0000000..86a8189
--- /dev/null
+++ b/pkg/epp/flowcontrol/framework/mocks/mocks.go
@@ -0,0 +1,65 @@
+/*
+Copyright 2025 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 mocks
+
+import (
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/framework"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/types"
+)
+
+// MockItemComparator provides a mock implementation of the `framework.ItemComparator` interface.
+type MockItemComparator struct {
+	FuncV      framework.ItemComparatorFunc
+	ScoreTypeV string
+}
+
+func (m *MockItemComparator) Func() framework.ItemComparatorFunc { return m.FuncV }
+func (m *MockItemComparator) ScoreType() string                  { return m.ScoreTypeV }
+
+var _ framework.ItemComparator = &MockItemComparator{}
+
+// MockFlowQueueAccessor is a mock implementation of the `framework.FlowQueueAccessor` interface.
+type MockFlowQueueAccessor struct {
+	NameV         string
+	CapabilitiesV []framework.QueueCapability
+	LenV          int
+	ByteSizeV     uint64
+	PeekHeadV     types.QueueItemAccessor
+	PeekHeadErrV  error
+	PeekTailV     types.QueueItemAccessor
+	PeekTailErrV  error
+	FlowSpecV     types.FlowSpecification
+	ComparatorV   framework.ItemComparator
+}
+
+func (m *MockFlowQueueAccessor) Name() string                              { return m.NameV }
+func (m *MockFlowQueueAccessor) Capabilities() []framework.QueueCapability { return m.CapabilitiesV }
+func (m *MockFlowQueueAccessor) Len() int                                  { return m.LenV }
+func (m *MockFlowQueueAccessor) ByteSize() uint64                          { return m.ByteSizeV }
+
+func (m *MockFlowQueueAccessor) PeekHead() (types.QueueItemAccessor, error) {
+	return m.PeekHeadV, m.PeekHeadErrV
+}
+
+func (m *MockFlowQueueAccessor) PeekTail() (types.QueueItemAccessor, error) {
+	return m.PeekTailV, m.PeekTailErrV
+}
+
+func (m *MockFlowQueueAccessor) Comparator() framework.ItemComparator { return m.ComparatorV }
+func (m *MockFlowQueueAccessor) FlowSpec() types.FlowSpecification    { return m.FlowSpecV }
+
+var _ framework.FlowQueueAccessor = &MockFlowQueueAccessor{}
diff --git a/pkg/epp/flowcontrol/framework/plugins/policies/intraflow/dispatch/README.md b/pkg/epp/flowcontrol/framework/plugins/policies/intraflow/dispatch/README.md
new file mode 100644
index 0000000..1432db0
--- /dev/null
+++ b/pkg/epp/flowcontrol/framework/plugins/policies/intraflow/dispatch/README.md
@@ -0,0 +1,63 @@
+# Flow Controller Intra-Flow Dispatch Policy Plugins
+
+This directory contains concrete implementations of the [`framework.IntraFlowDispatchPolicy`](../../../policies.go)
+interface. These policies are responsible for **temporal scheduling**: determining the order in which requests are
+selected for dispatch *from within a single flow's queue*.
+
+## Overview
+
+The `controller.FlowController` uses a two-tier policy system to manage requests. `framework.IntraFlowDispatchPolicy`
+plugins represent the first tier, making tactical decisions about the ordering of requests *within* a single logical
+flow (e.g., for a specific model or tenant).
+
+This contrasts with the `framework.InterFlowDispatchPolicy` (not yet implemented), which is responsible for
+**spatial fairness**: deciding *which flow's queue* gets the next opportunity to dispatch a request. The
+`framework.IntraFlowDispatchPolicy` only operates *after* the inter-flow policy has selected a specific queue.
+
+Key responsibilities and characteristics of a `framework.IntraFlowDispatchPolicy`:
+
+1.  **Request Selection (`SelectItem`)**: The primary method, `SelectItem(queue framework.FlowQueueAccessor)`, inspects
+    the given flow's queue (via a read-only accessor) and decides which item, if any, should be dispatched next from
+    *that specific queue*.
+
+2.  **Priority Definition (`ItemComparator`)**:
+    - This policy type is unique because it defines the nature of priority for items *within its specific managed
+      queue*. It makes this logic explicit by vending a [`framework.ItemComparator`](../../../policies.go).
+    - The vended comparator defines the "less than" relationship between two items and exposes a `ScoreType()` string
+      (e.g., `"enqueue_time_ns_asc"`, `"slo_deadline_urgency"`) that gives a semantic meaning to the comparison.
+
+3.  **Queue Compatibility (`RequiredQueueCapabilities`)**: The policy specifies the capabilities its associated
+    [`framework.SafeQueue`](../../../queue.go) must support for it to function correctly. For example, a simple FCFS
+    policy would require `framework.CapabilityFIFO`, while a more complex, priority-based policy would require
+    `framework.CapabilityPriorityConfigurable`. The `ports.FlowRegistry` uses this information to pair policies with
+    compatible queues.
+
+The `framework.IntraFlowDispatchPolicy` allows for fine-grained control over how individual requests within a single flow are
+serviced, enabling strategies like basic FCFS or more advanced schemes based on SLOs or deadlines.
+
+## Contributing a New `framework.IntraFlowDispatchPolicy` Implementation
+
+To contribute a new dispatch policy implementation, follow these steps:
+
+1.  **Define Your Implementation**
+    - Create a new Go package in a subdirectory (e.g., `mycustompolicy/`).
+    - Implement the `framework.IntraFlowDispatchPolicy` interface.
+    - Ensure all methods are goroutine-safe if your policy maintains any internal state.
+
+2.  **Register Your Policy**
+    - In an `init()` function within your policy's Go file, call [`MustRegisterPolicy()`](./factory.go) with a
+      unique name and a constructor function that matches the `PolicyConstructor` signature.
+
+3.  **Add to the Functional Test**
+    - Add a blank import for your new package to [`functional_test.go`](./functional_test.go). Your policy will then
+      be automatically included in the functional test suite, which validates the basic
+      `framework.IntraFlowDispatchPolicy` contract (e.g., correct initialization, handling of nil/empty queues).
+
+4.  **Add Policy-Specific Tests**
+    - The functional test suite only validates the universal contract. You MUST add a separate `_test.go` file within
+      your package to test the specific logic of your policy.
+    - For example, your tests should validate that your `Comparator()` works as expected and that `SelectItem()`
+      correctly implements your desired selection logic for a non-empty queue.
+
+5.  **Documentation**
+    - Add a package-level GoDoc comment to your new policy's Go file, explaining its behavior and any trade-offs.
diff --git a/pkg/epp/flowcontrol/framework/plugins/policies/intraflow/dispatch/factory.go b/pkg/epp/flowcontrol/framework/plugins/policies/intraflow/dispatch/factory.go
new file mode 100644
index 0000000..b55740c
--- /dev/null
+++ b/pkg/epp/flowcontrol/framework/plugins/policies/intraflow/dispatch/factory.go
@@ -0,0 +1,63 @@
+/*
+Copyright 2025 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 dispatch provides the factory and registration mechanism for all `framework.IntraFlowDispatchPolicy`
+// implementations.
+// It allows new policies to be added to the system and instantiated by name.
+package dispatch
+
+import (
+	"fmt"
+	"sync"
+
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/framework"
+)
+
+// RegisteredPolicyName is the unique name under which a policy is registered.
+type RegisteredPolicyName string
+
+// PolicyConstructor defines the function signature for creating a `framework.IntraFlowDispatchPolicy`.
+type PolicyConstructor func() (framework.IntraFlowDispatchPolicy, error)
+
+var (
+	// mu guards the registration map.
+	mu sync.RWMutex
+	// RegisteredPolicies stores the constructors for all registered policies.
+	RegisteredPolicies = make(map[RegisteredPolicyName]PolicyConstructor)
+)
+
+// MustRegisterPolicy registers a policy constructor, and panics if the name is already registered.
+// This is intended to be called from the `init()` function of a policy implementation.
+func MustRegisterPolicy(name RegisteredPolicyName, constructor PolicyConstructor) {
+	mu.Lock()
+	defer mu.Unlock()
+	if _, ok := RegisteredPolicies[name]; ok {
+		panic(fmt.Sprintf("IntraFlowDispatchPolicy already registered with name %q", name))
+	}
+	RegisteredPolicies[name] = constructor
+}
+
+// NewPolicyFromName creates a new `IntraFlowDispatchPolicy` given its registered name.
+// This is called by the `registry.FlowRegistry` when configuring a flow.
+func NewPolicyFromName(name RegisteredPolicyName) (framework.IntraFlowDispatchPolicy, error) {
+	mu.RLock()
+	defer mu.RUnlock()
+	constructor, ok := RegisteredPolicies[name]
+	if !ok {
+		return nil, fmt.Errorf("no IntraFlowDispatchPolicy registered with name %q", name)
+	}
+	return constructor()
+}
diff --git a/pkg/epp/flowcontrol/framework/plugins/policies/intraflow/dispatch/fcfs/fcfs.go b/pkg/epp/flowcontrol/framework/plugins/policies/intraflow/dispatch/fcfs/fcfs.go
new file mode 100644
index 0000000..edcc02a
--- /dev/null
+++ b/pkg/epp/flowcontrol/framework/plugins/policies/intraflow/dispatch/fcfs/fcfs.go
@@ -0,0 +1,104 @@
+/*
+Copyright 2025 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 fcfs provides a First-Come, First-Served implementation of the `framework.IntraFlowDispatchPolicy`.
+package fcfs
+
+import (
+	"errors"
+
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/framework"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/framework/plugins/policies/intraflow/dispatch"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/types"
+)
+
+// FCFSPolicyName is the name of the FCFS policy implementation.
+const FCFSPolicyName = "FCFS"
+
+func init() {
+	dispatch.MustRegisterPolicy(dispatch.RegisteredPolicyName(FCFSPolicyName),
+		func() (framework.IntraFlowDispatchPolicy, error) {
+			return newFCFS(), nil
+		})
+}
+
+// fcfs (First-Come, First-Served) implements the `framework.IntraFlowDispatchPolicy` interface.
+type fcfs struct {
+	comparator framework.ItemComparator
+}
+
+// newFCFS creates a new `fcfs` policy instance.
+func newFCFS() *fcfs {
+	return &fcfs{
+		comparator: &enqueueTimeComparator{},
+	}
+}
+
+// Name returns the name of the policy.
+func (p *fcfs) Name() string {
+	return FCFSPolicyName
+}
+
+// SelectItem selects the next item from the queue by peeking its head. This implementation relies on the queue being
+// ordered by dispatch preference, as indicated by its `RequiredQueueCapabilities`.
+func (p *fcfs) SelectItem(queue framework.FlowQueueAccessor) (types.QueueItemAccessor, error) {
+	if queue == nil {
+		return nil, nil
+	}
+	item, err := queue.PeekHead()
+	if errors.Is(err, framework.ErrQueueEmpty) {
+		return nil, nil
+	}
+	return item, err
+}
+
+// Comparator returns a `framework.ItemComparator` based on enqueue time.
+func (p *fcfs) Comparator() framework.ItemComparator {
+	return p.comparator
+}
+
+// RequiredQueueCapabilities specifies that this policy needs a queue that supports FIFO operations.
+func (p *fcfs) RequiredQueueCapabilities() []framework.QueueCapability {
+	return []framework.QueueCapability{framework.CapabilityFIFO}
+}
+
+// --- enqueueTimeComparator ---
+
+// enqueueTimeComparator implements `framework.ItemComparator` for FCFS logic.
+// It prioritizes items with earlier enqueue times.
+type enqueueTimeComparator struct{}
+
+// Func returns the comparison logic.
+// It returns true if item 'a' should be dispatched before item 'b'.
+func (c *enqueueTimeComparator) Func() framework.ItemComparatorFunc {
+	return func(a, b types.QueueItemAccessor) bool {
+		if a == nil && b == nil {
+			return false
+		}
+		if a == nil { // Treat nil as lowest priority
+			return false
+		}
+		if b == nil { // Treat non-nil 'a' as higher priority than nil 'b'
+			return true
+		}
+		return a.EnqueueTime().Before(b.EnqueueTime())
+	}
+}
+
+// ScoreType returns a string descriptor for the comparison logic.
+func (c *enqueueTimeComparator) ScoreType() string {
+	return string(framework.EnqueueTimePriorityScoreType)
+}
diff --git a/pkg/epp/flowcontrol/framework/plugins/queue/README.md b/pkg/epp/flowcontrol/framework/plugins/queue/README.md
new file mode 100644
index 0000000..3632487
--- /dev/null
+++ b/pkg/epp/flowcontrol/framework/plugins/queue/README.md
@@ -0,0 +1,113 @@
+# Flow Controller Queue Plugins (`plugins/queue/`)
+
+This directory contains concrete implementations of the [`framework.SafeQueue`](../../queue.go) interface. This contract
+defines core, self-contained queue data structures used by the `controller.FlowController`.
+
+## Overview
+
+The `controller.FlowController` manages requests by organizing them into queues. Each logical "flow" within a given
+priority band has its own `ports.ManagedQueue` instance, which wraps a `framework.SafeQueue`. This design allows the
+`controller.FlowController` to apply policies at both the inter-flow (across different flows) and intra-flow (within a
+single flow's queue) levels.
+
+The `framework.SafeQueue` interface abstracts the underlying data structure and its ordering logic. This pluggable
+design allows for:
+
+- **Different Queuing Disciplines**: A basic FIFO queue ([`listqueue`](./listqueue/)) is provided, but other disciplines
+  like priority queues ([`maxminheap`](./maxminheap/)) can be used for more complex ordering requirements.
+- **Specialized Capabilities**: Policies can declare `RequiredQueueCapabilities()` (e.g., `framework.CapabilityFIFO` or
+  `framework.CapabilityPriorityConfigurable`). The `ports.FlowRegistry` pairs the policy with a queue that provides the
+  necessary capabilities.
+- **Performance Optimization**: Different queue implementations offer varying performance characteristics, which can be
+  compared using the centralized benchmark suite to select the best fit for a given workload.
+
+## Contributing a New `SafeQueue` Implementation
+
+To contribute a new queue implementation, follow these steps:
+
+1.  **Define Your Implementation**
+    - Create a new Go package in a subdirectory (e.g., `mycustomqueue/`).
+    - Implement the `framework.SafeQueue` and `types.QueueItemHandle` interfaces.
+    - Ensure all methods of `framework.SafeQueue` are goroutine-safe, typically by using a `sync.Mutex` or
+      `sync.RWMutex`.
+    - If your queue declares `framework.CapabilityPriorityConfigurable`, it MUST use the
+      [`framework.ItemComparator`](../../policies.go) passed to its constructor for all internal ordering logic.
+
+2. **Register Your Queue**
+    - In an `init()` function within your queue's Go file, call [`queue.MustRegisterQueue()`](./factory.go) with a
+      unique name and a constructor function that matches the `queue.QueueConstructor` signature.
+
+3.  **Add to the Functional Test**
+    - Add a blank import for your new package to [`functional_test.go`](./functional_test.go). Your queue will then be
+      automatically included in the functional test suite, which validates the `framework.SafeQueue` contract.
+
+4.  **Documentation**
+    - Add GoDoc comments to your new queue type, explaining its behavior, capabilities, and any trade-offs.
+
+5.  **Benchmarking**
+    - You do not need to write custom benchmarks. The centralized suite in [`benchmark_test.go`](./benchmark_test.go)
+      automatically includes any new queue implementation after it is registered. This ensures all queues are compared
+      fairly under the same conditions.
+
+## Benchmarking Strategy and Results
+
+A centralized benchmark suite runs against all registered `framework.SafeQueue` implementations to provide a consistent
+performance comparison. To run the benchmarks, use the following command:
+
+```sh
+go test -bench=. -benchmem ./pkg/epp/flowcontrol/framework/plugins/queue/...
+```
+
+### Benchmark Scenarios
+
+The suite includes the following scenarios:
+
+- **`AddRemove`**: Measures throughput of tightly coupled `Add` and `Remove` operations under high parallelism. This
+  tests the raw overhead of the data structure and its locking mechanism for simple, transactional workloads.
+- **`AddPeekRemove`**: Measures performance of a sequential `Add` -> `PeekHead` -> `Remove` loop. This simulates a
+  common consumer pattern where a single worker inspects an item before processing it.
+- **`BulkAddThenBulkRemove`**: Tests performance of adding a large batch of items and then removing them all. This can
+  reveal how the data structure's performance changes as it grows and shrinks under load.
+- **`HighContention`**: Simulates a realistic workload with multiple concurrent producers (adding items) and consumers
+  (peeking and removing items) operating on the same queue.
+
+### Latest Results
+
+*Last Updated: 2025-07-10*
+*(CPU: AMD EPYC 7B12)*
+
+| Benchmark                   | Implementation | Iterations | ns/op   | B/op  | allocs/op |
+| --------------------------- | -------------- | ---------- | ------- | ----- | --------- |
+| **AddRemove**               | `ListQueue`    | 1,889,844  | 609.0   | 224   | 5         |
+|                             | `MaxMinHeap`   | 1,660,987  | 696.7   | 184   | 4         |
+| **AddPeekRemove**           | `ListQueue`    | 3,884,938  | 298.0   | 224   | 5         |
+|                             | `MaxMinHeap`   | 1,857,448  | 615.9   | 184   | 4         |
+| **AddPeekTailRemove**       | `ListQueue`    | 3,576,487  | 308.4   | 224   | 5         |
+|                             | `MaxMinHeap`   | 2,113,134  | 535.3   | 184   | 4         |
+| **BulkAddThenBulkRemove**   | `ListQueue`    | 24,032     | 49,861  | 24801 | 698       |
+|                             | `MaxMinHeap`   | 10,000     | 108,868 | 20787 | 597       |
+| **HighContention**          | `ListQueue`    | 484,574    | 2,328   | 896   | 20        |
+|                             | `MaxMinHeap`   | 84,806     | 18,679  | 783   | 16        |
+
+### Interpretation of Results
+
+The benchmark results highlight the trade-offs between the different queue implementations based on their underlying
+data structures:
+
+- **`ListQueue`**: As a linked list, it excels in scenarios involving frequent additions or removals from either end of
+  the queue (`AddPeekRemove`, `AddPeekTailRemove`), which are O(1) operations. Its performance is less competitive in high-contention and bulk scenarios, which reflects the necessary per-item memory allocation and pointer manipulation
+  overhead.
+- **`MaxMinHeap`**: As a slice-based heap, it has a lower allocation overhead per operation, making it efficient for
+  high-throughput `AddRemove` cycles. Peeking and removing items involves maintaining the heap property, which has an
+  O(log n) cost, making individual peek operations slower than `ListQueue`.
+
+**Choosing a Queue:**
+
+The data suggests the following guidance:
+- For simple **FIFO** workloads where the primary operations are consuming from the head, `ListQueue` is a strong and
+  simple choice.
+- For workloads requiring **priority-based ordering** or those that are sensitive to allocation overhead under high
+  contention, `MaxMinHeap` is likely the more suitable option.
+
+These benchmarks provide a baseline for performance. The best choice for a specific use case will depend on the expected
+workload patterns.
diff --git a/pkg/epp/flowcontrol/framework/plugins/queue/factory.go b/pkg/epp/flowcontrol/framework/plugins/queue/factory.go
new file mode 100644
index 0000000..f0e9bef
--- /dev/null
+++ b/pkg/epp/flowcontrol/framework/plugins/queue/factory.go
@@ -0,0 +1,63 @@
+/*
+Copyright 2025 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 queue provides the factory and registration mechanism for all `framework.SafeQueue` implementations.
+// It allows new queues to be added to the system and instantiated by name.
+package queue
+
+import (
+	"fmt"
+	"sync"
+
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/framework"
+)
+
+// RegisteredQueueName is the unique name under which a queue is registered.
+type RegisteredQueueName string
+
+// QueueConstructor defines the function signature for creating a `framework.SafeQueue`.
+type QueueConstructor func(comparator framework.ItemComparator) (framework.SafeQueue, error)
+
+var (
+	// mu guards the registration map.
+	mu sync.RWMutex
+	// RegisteredQueues stores the constructors for all registered queues.
+	RegisteredQueues = make(map[RegisteredQueueName]QueueConstructor)
+)
+
+// MustRegisterQueue registers a queue constructor, and panics if the name is already registered.
+// This is intended to be called from init() functions.
+func MustRegisterQueue(name RegisteredQueueName, constructor QueueConstructor) {
+	mu.Lock()
+	defer mu.Unlock()
+	if _, ok := RegisteredQueues[name]; ok {
+		panic(fmt.Sprintf("framework.SafeQueue already registered with name %q", name))
+	}
+	RegisteredQueues[name] = constructor
+}
+
+// NewQueueFromName creates a new SafeQueue given its registered name and the `framework.ItemComparator` that will be
+// optionally used to configure the queue (provided it declares `framework.CapabilityPriorityConfigurable`).
+// This is called by the `registry.FlowRegistry` during initialization of a flow's `ports.ManagedQueue`.
+func NewQueueFromName(name RegisteredQueueName, comparator framework.ItemComparator) (framework.SafeQueue, error) {
+	mu.RLock()
+	defer mu.RUnlock()
+	constructor, ok := RegisteredQueues[name]
+	if !ok {
+		return nil, fmt.Errorf("no framework.SafeQueue registered with name %q", name)
+	}
+	return constructor(comparator)
+}
diff --git a/pkg/epp/flowcontrol/framework/plugins/queue/listqueue/listqueue.go b/pkg/epp/flowcontrol/framework/plugins/queue/listqueue/listqueue.go
new file mode 100644
index 0000000..6a1e220
--- /dev/null
+++ b/pkg/epp/flowcontrol/framework/plugins/queue/listqueue/listqueue.go
@@ -0,0 +1,217 @@
+/*
+Copyright 2025 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 listqueue provides a simple, concurrent-safe queue implementation using a standard library
+// `container/list.List` as the underlying data structure for FIFO (First-In, First-Out) behavior.
+package listqueue
+
+import (
+	"container/list"
+	"sync"
+	"sync/atomic"
+
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/framework"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/framework/plugins/queue"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/types"
+)
+
+// ListQueueName is the name of the list queue implementation.
+const ListQueueName = "ListQueue"
+
+func init() {
+	queue.MustRegisterQueue(queue.RegisteredQueueName(ListQueueName),
+		func(_ framework.ItemComparator) (framework.SafeQueue, error) {
+			// The list queue is a simple FIFO queue and does not use a comparator.
+			return newListQueue(), nil
+		})
+}
+
+// listQueue implements the `framework.SafeQueue` interface using a standard `container/list.List` for FIFO behavior.
+// This implementation is concurrent-safe.
+type listQueue struct {
+	requests *list.List
+	byteSize atomic.Uint64
+	mu       sync.RWMutex
+}
+
+// listItemHandle is the concrete type for `types.QueueItemHandle` used by `listQueue`.
+// It wraps the `list.Element` and includes a pointer to the owning `listQueue` for validation.
+type listItemHandle struct {
+	element       *list.Element
+	owner         *listQueue
+	isInvalidated bool
+}
+
+// Handle returns the underlying queue-specific raw handle.
+func (lh *listItemHandle) Handle() any {
+	return lh.element
+}
+
+// Invalidate marks this handle instance as no longer valid for future operations.
+func (lh *listItemHandle) Invalidate() {
+	lh.isInvalidated = true
+}
+
+// IsInvalidated returns true if this handle instance has been marked as invalid.
+func (lh *listItemHandle) IsInvalidated() bool {
+	return lh.isInvalidated
+}
+
+var _ types.QueueItemHandle = &listItemHandle{}
+
+// newListQueue creates a new `listQueue` instance.
+func newListQueue() *listQueue {
+	return &listQueue{
+		requests: list.New(),
+	}
+}
+
+// --- `framework.SafeQueue` Interface Implementation ---
+
+// Add enqueues an item to the back of the list.
+func (lq *listQueue) Add(item types.QueueItemAccessor) (newLen, newByteSize uint64, err error) {
+	lq.mu.Lock()
+	defer lq.mu.Unlock()
+
+	if item == nil {
+		return uint64(lq.requests.Len()), lq.byteSize.Load(), framework.ErrNilQueueItem
+	}
+
+	element := lq.requests.PushBack(item)
+	lq.byteSize.Add(item.OriginalRequest().ByteSize())
+	item.SetHandle(&listItemHandle{element: element, owner: lq})
+	return uint64(lq.requests.Len()), lq.byteSize.Load(), nil
+}
+
+// Remove removes an item identified by the given handle from the queue.
+func (lq *listQueue) Remove(
+	handle types.QueueItemHandle,
+) (removedItem types.QueueItemAccessor, newLen, newByteSize uint64, err error) {
+	lq.mu.Lock()
+	defer lq.mu.Unlock()
+
+	currentLen := uint64(lq.requests.Len())
+	currentByteSize := lq.byteSize.Load()
+
+	if handle == nil || handle.IsInvalidated() {
+		return nil, currentLen, currentByteSize, framework.ErrInvalidQueueItemHandle
+	}
+
+	lh, ok := handle.(*listItemHandle)
+	if !ok {
+		return nil, currentLen, currentByteSize, framework.ErrInvalidQueueItemHandle
+	}
+
+	if lh.owner != lq {
+		return nil, currentLen, currentByteSize, framework.ErrQueueItemNotFound
+	}
+
+	item := lh.element.Value.(types.QueueItemAccessor)
+	lq.requests.Remove(lh.element)
+	lq.byteSize.Add(^item.OriginalRequest().ByteSize() + 1) // Atomic subtraction
+	handle.Invalidate()
+	return item, uint64(lq.requests.Len()), lq.byteSize.Load(), nil
+}
+
+// Cleanup removes items from the queue that satisfy the predicate.
+func (lq *listQueue) Cleanup(predicate framework.PredicateFunc) (cleanedItems []types.QueueItemAccessor, err error) {
+	lq.mu.Lock()
+	defer lq.mu.Unlock()
+
+	var removedItems []types.QueueItemAccessor
+	var next *list.Element
+
+	for e := lq.requests.Front(); e != nil; e = next {
+		next = e.Next() // Get next before potentially removing e
+
+		item := e.Value.(types.QueueItemAccessor)
+		if predicate(item) {
+			lq.requests.Remove(e)
+			lq.byteSize.Add(^item.OriginalRequest().ByteSize() + 1) // Atomic subtraction
+			if itemHandle := item.Handle(); itemHandle != nil {
+				itemHandle.Invalidate()
+			}
+			removedItems = append(removedItems, item)
+		}
+	}
+	return removedItems, nil
+}
+
+// Drain removes all items from the queue and returns them.
+func (lq *listQueue) Drain() (removedItems []types.QueueItemAccessor, err error) {
+	lq.mu.Lock()
+	defer lq.mu.Unlock()
+
+	removedItems = make([]types.QueueItemAccessor, 0, lq.requests.Len())
+
+	for e := lq.requests.Front(); e != nil; e = e.Next() {
+		item := e.Value.(types.QueueItemAccessor)
+		removedItems = append(removedItems, item)
+		if handle := item.Handle(); handle != nil {
+			handle.Invalidate()
+		}
+	}
+
+	lq.requests.Init()
+	lq.byteSize.Store(0)
+	return removedItems, nil
+}
+
+// Name returns the name of the queue.
+func (lq *listQueue) Name() string {
+	return ListQueueName
+}
+
+// Capabilities returns the capabilities of the queue.
+func (lq *listQueue) Capabilities() []framework.QueueCapability {
+	return []framework.QueueCapability{framework.CapabilityFIFO}
+}
+
+// Len returns the number of items in the queue.
+func (lq *listQueue) Len() int {
+	lq.mu.RLock()
+	defer lq.mu.RUnlock()
+	return lq.requests.Len()
+}
+
+// ByteSize returns the total byte size of all items in the queue.
+func (lq *listQueue) ByteSize() uint64 {
+	return lq.byteSize.Load()
+}
+
+// PeekHead returns the item at the front of the queue without removing it.
+func (lq *listQueue) PeekHead() (head types.QueueItemAccessor, err error) {
+	lq.mu.RLock()
+	defer lq.mu.RUnlock()
+
+	if lq.requests.Len() == 0 {
+		return nil, framework.ErrQueueEmpty
+	}
+	element := lq.requests.Front()
+	return element.Value.(types.QueueItemAccessor), nil
+}
+
+// PeekTail returns the item at the back of the queue without removing it.
+func (lq *listQueue) PeekTail() (tail types.QueueItemAccessor, err error) {
+	lq.mu.RLock()
+	defer lq.mu.RUnlock()
+
+	if lq.requests.Len() == 0 {
+		return nil, framework.ErrQueueEmpty
+	}
+	element := lq.requests.Back()
+	return element.Value.(types.QueueItemAccessor), nil
+}
diff --git a/pkg/epp/flowcontrol/framework/plugins/queue/maxminheap/maxminheap.go b/pkg/epp/flowcontrol/framework/plugins/queue/maxminheap/maxminheap.go
new file mode 100644
index 0000000..3d3dae7
--- /dev/null
+++ b/pkg/epp/flowcontrol/framework/plugins/queue/maxminheap/maxminheap.go
@@ -0,0 +1,490 @@
+/*
+Copyright 2025 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 maxminheap provides a concurrent-safe, priority queue implementation using a max-min heap.
+//
+// A max-min heap is a binary tree structure that maintains a specific ordering property: for any node, if it is at an
+// even level (e.g., 0, 2, ...), its value is greater than all values in its subtree (max level). If it is at an odd
+// level (e.g., 1, 3, ...), its value is smaller than all values in its subtree (min level). This structure allows for
+// efficient O(1) retrieval of both the maximum and minimum priority items.
+//
+// The core heap maintenance logic (up, down, and grandchild finding) is adapted from the public domain implementation
+// at https://github.com/esote/minmaxheap, which is licensed under CC0-1.0.
+package maxminheap
+
+import (
+	"math"
+	"sync"
+	"sync/atomic"
+
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/framework"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/framework/plugins/queue"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/types"
+)
+
+// MaxMinHeapName is the name of the max-min heap queue implementation.
+const MaxMinHeapName = "MaxMinHeap"
+
+func init() {
+	queue.MustRegisterQueue(queue.RegisteredQueueName(MaxMinHeapName),
+		func(comparator framework.ItemComparator) (framework.SafeQueue, error) {
+			return newMaxMinHeap(comparator), nil
+		})
+}
+
+// maxMinHeap implements the `framework.SafeQueue` interface using a max-min heap.
+// The heap is ordered by the provided comparator, with higher values considered higher priority.
+// This implementation is concurrent-safe.
+type maxMinHeap struct {
+	items      []types.QueueItemAccessor
+	handles    map[types.QueueItemHandle]*heapItem
+	byteSize   atomic.Uint64
+	mu         sync.RWMutex
+	comparator framework.ItemComparator
+}
+
+// heapItem is an internal struct to hold an item and its index in the heap.
+// This allows for O(log n) removal of items from the queue.
+type heapItem struct {
+	item          types.QueueItemAccessor
+	index         int
+	isInvalidated bool
+}
+
+// Handle returns the heap item itself, which is used as the handle.
+func (h *heapItem) Handle() any {
+	return h
+}
+
+// Invalidate marks the handle as invalid.
+func (h *heapItem) Invalidate() {
+	h.isInvalidated = true
+}
+
+// IsInvalidated returns true if the handle has been invalidated.
+func (h *heapItem) IsInvalidated() bool {
+	return h.isInvalidated
+}
+
+var _ types.QueueItemHandle = &heapItem{}
+
+// newMaxMinHeap creates a new max-min heap with the given comparator.
+func newMaxMinHeap(comparator framework.ItemComparator) *maxMinHeap {
+	return &maxMinHeap{
+		items:      make([]types.QueueItemAccessor, 0),
+		handles:    make(map[types.QueueItemHandle]*heapItem),
+		comparator: comparator,
+	}
+}
+
+// --- `framework.SafeQueue` Interface Implementation ---
+
+// Name returns the name of the queue.
+func (h *maxMinHeap) Name() string {
+	return MaxMinHeapName
+}
+
+// Capabilities returns the capabilities of the queue.
+func (h *maxMinHeap) Capabilities() []framework.QueueCapability {
+	return []framework.QueueCapability{framework.CapabilityPriorityConfigurable}
+}
+
+// Len returns the number of items in the queue.
+func (h *maxMinHeap) Len() int {
+	h.mu.RLock()
+	defer h.mu.RUnlock()
+	return len(h.items)
+}
+
+// ByteSize returns the total byte size of all items in the queue.
+func (h *maxMinHeap) ByteSize() uint64 {
+	return h.byteSize.Load()
+}
+
+// PeekHead returns the item with the highest priority (max value) without removing it.
+// Time complexity: O(1).
+func (h *maxMinHeap) PeekHead() (types.QueueItemAccessor, error) {
+	h.mu.RLock()
+	defer h.mu.RUnlock()
+
+	if len(h.items) == 0 {
+		return nil, framework.ErrQueueEmpty
+	}
+	// The root of the max-min heap is always the maximum element.
+	return h.items[0], nil
+}
+
+// PeekTail returns the item with the lowest priority (min value) without removing it.
+// Time complexity: O(1).
+func (h *maxMinHeap) PeekTail() (types.QueueItemAccessor, error) {
+	h.mu.RLock()
+	defer h.mu.RUnlock()
+
+	n := len(h.items)
+	if n == 0 {
+		return nil, framework.ErrQueueEmpty
+	}
+	if n == 1 {
+		return h.items[0], nil
+	}
+	if n == 2 {
+		// With two items, the root is max, the second is min.
+		return h.items[1], nil
+	}
+
+	// With three or more items, the minimum element is guaranteed to be one of the two children of the root (at indices 1
+	// and 2). We must compare them to find the true minimum.
+	if h.comparator.Func()(h.items[1], h.items[2]) {
+		return h.items[2], nil
+	}
+	return h.items[1], nil
+}
+
+// Add adds an item to the queue.
+// Time complexity: O(log n).
+func (h *maxMinHeap) Add(item types.QueueItemAccessor) (uint64, uint64, error) {
+	h.mu.Lock()
+	defer h.mu.Unlock()
+
+	if item == nil {
+		return uint64(len(h.items)), h.byteSize.Load(), framework.ErrNilQueueItem
+	}
+
+	h.push(item)
+	h.byteSize.Add(item.OriginalRequest().ByteSize())
+	return uint64(len(h.items)), h.byteSize.Load(), nil
+}
+
+// push adds an item to the heap and restores the heap property.
+func (h *maxMinHeap) push(item types.QueueItemAccessor) {
+	heapItem := &heapItem{item: item, index: len(h.items)}
+	h.items = append(h.items, item)
+	item.SetHandle(heapItem)
+	h.handles[item.Handle()] = heapItem
+	h.up(len(h.items) - 1)
+}
+
+// up moves the item at index i up the heap to its correct position.
+func (h *maxMinHeap) up(i int) {
+	if i == 0 {
+		return
+	}
+
+	parentIndex := (i - 1) / 2
+	if isMinLevel(i) {
+		// Current node is on a min level, parent is on a max level.
+		// If the current node is greater than its parent, they are in the wrong order.
+		if h.comparator.Func()(h.items[i], h.items[parentIndex]) {
+			h.swap(i, parentIndex)
+			// After swapping, the new parent (originally at i) might be larger than its ancestors.
+			h.upMax(parentIndex)
+		} else {
+			// The order with the parent is correct, but it might be smaller than a grandparent.
+			h.upMin(i)
+		}
+	} else { // On a max level
+		// Current node is on a max level, parent is on a min level.
+		// If the current node is smaller than its parent, they are in the wrong order.
+		if h.comparator.Func()(h.items[parentIndex], h.items[i]) {
+			h.swap(i, parentIndex)
+			// After swapping, the new parent (originally at i) might be smaller than its ancestors.
+			h.upMin(parentIndex)
+		} else {
+			// The order with the parent is correct, but it might be larger than a grandparent.
+			h.upMax(i)
+		}
+	}
+}
+
+// upMin moves an item up the min levels of the heap.
+func (h *maxMinHeap) upMin(i int) {
+	// Bubble up on min levels by comparing with grandparents.
+	for {
+		parentIndex := (i - 1) / 2
+		if parentIndex == 0 {
+			break
+		}
+		grandparentIndex := (parentIndex - 1) / 2
+		// If the item is smaller than its grandparent, swap them.
+		if h.comparator.Func()(h.items[grandparentIndex], h.items[i]) {
+			h.swap(i, grandparentIndex)
+			i = grandparentIndex
+		} else {
+			break
+		}
+	}
+}
+
+// upMax moves an item up the max levels of the heap.
+func (h *maxMinHeap) upMax(i int) {
+	// Bubble up on max levels by comparing with grandparents.
+	for {
+		parentIndex := (i - 1) / 2
+		if parentIndex == 0 {
+			break
+		}
+		grandparentIndex := (parentIndex - 1) / 2
+		// If the item is larger than its grandparent, swap them.
+		if h.comparator.Func()(h.items[i], h.items[grandparentIndex]) {
+			h.swap(i, grandparentIndex)
+			i = grandparentIndex
+		} else {
+			break
+		}
+	}
+}
+
+// swap swaps two items in the heap and updates their handles.
+func (h *maxMinHeap) swap(i, j int) {
+	h.items[i], h.items[j] = h.items[j], h.items[i]
+	h.handles[h.items[i].Handle()].index = i
+	h.handles[h.items[j].Handle()].index = j
+}
+
+// Remove removes an item from the queue.
+// Time complexity: O(log n).
+func (h *maxMinHeap) Remove(handle types.QueueItemHandle) (types.QueueItemAccessor, uint64, uint64, error) {
+	h.mu.Lock()
+	defer h.mu.Unlock()
+
+	currentLen := uint64(len(h.items))
+	currentByteSize := h.byteSize.Load()
+
+	if handle == nil {
+		return nil, currentLen, currentByteSize, framework.ErrInvalidQueueItemHandle
+	}
+
+	if handle.IsInvalidated() {
+		return nil, currentLen, currentByteSize, framework.ErrInvalidQueueItemHandle
+	}
+
+	heapItem, ok := handle.(*heapItem)
+	if !ok {
+		return nil, currentLen, currentByteSize, framework.ErrInvalidQueueItemHandle
+	}
+
+	// Now we can check if the handle is in the map
+	_, ok = h.handles[handle]
+	if !ok {
+		return nil, currentLen, currentByteSize, framework.ErrQueueItemNotFound
+	}
+
+	i := heapItem.index
+	item := h.items[i]
+	n := len(h.items) - 1
+
+	if i < n {
+		// Swap the item to be removed with the last item.
+		h.swap(i, n)
+		// Remove the last item (which is the one we wanted to remove).
+		h.items = h.items[:n]
+		delete(h.handles, handle)
+		h.byteSize.Add(^item.OriginalRequest().ByteSize() + 1) // Atomic subtraction
+
+		// The swapped item at index i might violate the heap property, so we must restore it
+		// by bubbling the item down the heap.
+		h.down(i)
+	} else {
+		// It's the last item, just remove it.
+		h.items = h.items[:n]
+		delete(h.handles, handle)
+		h.byteSize.Add(^item.OriginalRequest().ByteSize() + 1) // Atomic subtraction
+	}
+
+	handle.Invalidate()
+	return item, uint64(len(h.items)), h.byteSize.Load(), nil
+}
+
+// down moves the item at index i down the heap to its correct position.
+func (h *maxMinHeap) down(i int) {
+	if isMinLevel(i) {
+		h.downMin(i)
+	} else {
+		h.downMax(i)
+	}
+}
+
+// downMin moves an item down the min levels of the heap.
+func (h *maxMinHeap) downMin(i int) {
+	for {
+		m := h.findSmallestChildOrGrandchild(i)
+		if m == -1 {
+			break
+		}
+
+		// If the smallest descendant is smaller than the current item, swap them.
+		if h.comparator.Func()(h.items[i], h.items[m]) {
+			h.swap(i, m)
+			parentOfM := (m - 1) / 2
+			// If m was a grandchild, it might be larger than its new parent.
+			if parentOfM != i {
+				if h.comparator.Func()(h.items[m], h.items[parentOfM]) {
+					h.swap(m, parentOfM)
+				}
+			}
+			i = m
+		} else {
+			break
+		}
+	}
+}
+
+// downMax moves an item down the max levels of the heap.
+func (h *maxMinHeap) downMax(i int) {
+	for {
+		m := h.findLargestChildOrGrandchild(i)
+		if m == -1 {
+			break
+		}
+
+		// If the largest descendant is larger than the current item, swap them.
+		if h.comparator.Func()(h.items[m], h.items[i]) {
+			h.swap(i, m)
+			parentOfM := (m - 1) / 2
+			// If m was a grandchild, it might be smaller than its new parent.
+			if parentOfM != i {
+				if h.comparator.Func()(h.items[parentOfM], h.items[m]) {
+					h.swap(m, parentOfM)
+				}
+			}
+			i = m
+		} else {
+			break
+		}
+	}
+}
+
+// findSmallestChildOrGrandchild finds the index of the smallest child or grandchild of i.
+func (h *maxMinHeap) findSmallestChildOrGrandchild(i int) int {
+	leftChild := 2*i + 1
+	if leftChild >= len(h.items) {
+		return -1 // No descendants
+	}
+
+	m := leftChild // Start with the left child as the smallest.
+
+	// Compare with right child.
+	rightChild := 2*i + 2
+	if rightChild < len(h.items) && h.comparator.Func()(h.items[m], h.items[rightChild]) {
+		m = rightChild
+	}
+
+	// Compare with grandchildren.
+	grandchildStart := 2*leftChild + 1
+	grandchildEnd := grandchildStart + 4
+	for j := grandchildStart; j < grandchildEnd && j < len(h.items); j++ {
+		if h.comparator.Func()(h.items[m], h.items[j]) {
+			m = j
+		}
+	}
+	return m
+}
+
+// findLargestChildOrGrandchild finds the index of the largest child or grandchild of i.
+func (h *maxMinHeap) findLargestChildOrGrandchild(i int) int {
+	leftChild := 2*i + 1
+	if leftChild >= len(h.items) {
+		return -1 // No descendants
+	}
+
+	m := leftChild // Start with the left child as the largest.
+
+	// Compare with right child.
+	rightChild := 2*i + 2
+	if rightChild < len(h.items) && h.comparator.Func()(h.items[rightChild], h.items[m]) {
+		m = rightChild
+	}
+
+	// Compare with grandchildren.
+	grandchildStart := 2*leftChild + 1
+	grandchildEnd := grandchildStart + 4
+	for j := grandchildStart; j < grandchildEnd && j < len(h.items); j++ {
+		if h.comparator.Func()(h.items[j], h.items[m]) {
+			m = j
+		}
+	}
+	return m
+}
+
+// isMinLevel checks if the given index is on a min level of the heap.
+func isMinLevel(i int) bool {
+	// The level is the floor of log2(i+1).
+	// Levels are 0-indexed. 0, 2, 4... are max levels. 1, 3, 5... are min levels.
+	// An integer is on a min level if its level number is odd.
+	level := int(math.Log2(float64(i + 1)))
+	return level%2 != 0
+}
+
+// Cleanup removes items from the queue that satisfy the predicate.
+func (h *maxMinHeap) Cleanup(predicate framework.PredicateFunc) ([]types.QueueItemAccessor, error) {
+	h.mu.Lock()
+	defer h.mu.Unlock()
+
+	var removedItems []types.QueueItemAccessor
+	var itemsToKeep []types.QueueItemAccessor
+
+	for _, item := range h.items {
+		if predicate(item) {
+			removedItems = append(removedItems, item)
+			handle := item.Handle()
+			if handle != nil {
+				handle.Invalidate()
+				delete(h.handles, handle)
+			}
+			h.byteSize.Add(^item.OriginalRequest().ByteSize() + 1) // Atomic subtraction
+		} else {
+			itemsToKeep = append(itemsToKeep, item)
+		}
+	}
+
+	if len(removedItems) > 0 {
+		h.items = itemsToKeep
+		// Re-establish the heap property on the remaining items.
+		// First, update all the indices in the handles map.
+		for i, item := range h.items {
+			h.handles[item.Handle()].index = i
+		}
+		// Then, starting from the last non-leaf node, trickle down to fix the heap.
+		for i := len(h.items)/2 - 1; i >= 0; i-- {
+			h.down(i)
+		}
+	}
+
+	return removedItems, nil
+}
+
+// Drain removes all items from the queue.
+func (h *maxMinHeap) Drain() ([]types.QueueItemAccessor, error) {
+	h.mu.Lock()
+	defer h.mu.Unlock()
+
+	drainedItems := make([]types.QueueItemAccessor, len(h.items))
+	copy(drainedItems, h.items)
+
+	// Invalidate all handles.
+	for _, item := range h.items {
+		if handle := item.Handle(); handle != nil {
+			handle.Invalidate()
+		}
+	}
+
+	// Clear the internal state.
+	h.items = make([]types.QueueItemAccessor, 0)
+	h.handles = make(map[types.QueueItemHandle]*heapItem)
+	h.byteSize.Store(0)
+
+	return drainedItems, nil
+}
diff --git a/pkg/epp/flowcontrol/framework/policies.go b/pkg/epp/flowcontrol/framework/policies.go
new file mode 100644
index 0000000..14cbc5d
--- /dev/null
+++ b/pkg/epp/flowcontrol/framework/policies.go
@@ -0,0 +1,142 @@
+/*
+Copyright 2025 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 framework
+
+import "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/types"
+
+// PriorityScoreType is a descriptor for the domain of a policy's item comparator.
+type PriorityScoreType string
+
+const (
+	// EnqueueTimePriorityScoreType indicates that the priority is based on the item's enqueue time, with earlier times
+	// being higher priority.
+	EnqueueTimePriorityScoreType PriorityScoreType = "enqueue_time_ns_asc"
+)
+
+// ItemComparatorFunc defines the function signature for comparing two `types.QueueItemAccessor` instances to determine
+// their relative dispatch priority.
+//
+// An implementation of this function determines if item 'a' should be dispatched before item 'b'. It returns true if
+// 'a' is of higher priority, and false otherwise. The specific criteria for "higher priority" (e.g., earlier deadline,
+// lower enqueue time) are defined by the `IntraFlowDispatchPolicy` that vends this function via an `ItemComparator`.
+type ItemComparatorFunc func(a, b types.QueueItemAccessor) bool
+
+// ItemComparator encapsulates the logic for comparing two `types.QueueItemAccessor` instances to determine their
+// relative dispatch priority. It is the definitive source of ordering truth for a flow's queue.
+//
+// It is vended by an `IntraFlowDispatchPolicy` to make its internal ordering logic explicit and available to other
+// components. It is used by `SafeQueue` implementations that support the `CapabilityPriorityConfigurable` capability
+// and can also be used by inter-flow policies to compare items from different queues.
+//
+// Design Justification: This design treats item priority as a relational concept defined by a policy, rather than a
+// static attribute on the item itself. This allows for sophisticated, dynamic priority evaluation (e.g., based on
+// real-time SLO attainment), as the comparison logic can be stateful.
+type ItemComparator interface {
+	// Func returns the core comparison logic as an `ItemComparatorFunc`.
+	//
+	// This function is the single source of truth for determining the relative priority between two items. A `SafeQueue`
+	// that declares `CapabilityPriorityConfigurable` MUST use this function for its internal ordering. Inter-flow
+	// policies MAY use this function to compare items from different queues, but only after verifying that their
+	// `ScoreType()` values are identical, ensuring the comparison is meaningful.
+	//
+	// Conformance: MUST NOT return nil.
+	Func() ItemComparatorFunc
+
+	// ScoreType returns a string descriptor that defines the semantic meaning and domain of the comparison logic.
+	//
+	// A non-empty, descriptive string is required for two primary reasons:
+	//  1. Comparability Check: Inter-flow policies that compare items across different queues (e.g., a "BestHead" policy)
+	//     MUST check for identical `ScoreType` strings before using the comparator functions. A comparison is only
+	//     meaningful if the underlying scoring logic is the same.
+	//  2. Introspectability: The string makes the priority scheme human-readable for debugging and observability.
+	//
+	// Examples: "enqueue_time_ns_asc", "slo_urgency_score_desc".
+	//
+	// Future Considerations: While currently a simple string for initial simplicity, a future enhancement could introduce
+	// a more structured `ScoreType`. Such a structure might explicitly encode ordering (ascending/descending) and value
+	// semantics (e.g., time, custom_metric), potentially enabling advanced features like cross-`ScoreType` normalization
+	// plugins.
+	//
+	// Conformance:
+	//   - MUST return a non-empty, meaningful string that describes the domain or unit of comparison.
+	//   - For the present, policies MUST NOT assume any implicit cross-`ScoreType` normalization capabilities.
+	ScoreType() string
+}
+
+// IntraFlowDispatchPolicy selects a specific request to dispatch next from a single flow's queue.
+// Implementations define the dispatch ordering of requests within a single flow.
+//
+// For example, a "First-Come, First-Served" (FCFS) policy would typically inspect a queue and select the item that was
+// enqueued the earliest.
+type IntraFlowDispatchPolicy interface {
+	// Name returns a string identifier for the concrete policy implementation type (e.g., "FCFS").
+	Name() string
+
+	// SelectItem inspects a flow's queue and returns the `types.QueueItemAccessor` of the item chosen for dispatch.
+	//
+	// For queues that inherently order items by dispatch preference, this method will typically just call
+	// `queue.PeekHead()`.
+	//
+	// The `controller.FlowController` uses the handle from the returned item to instruct the `ports.ManagedQueue` to
+	// remove it.
+	//
+	// Returns:
+	//   - `types.QueueItemAccessor`: The selected item, or nil if no item is chosen.
+	//   - error: Non-nil if an unrecoverable error occurs. A nil error is returned if no item is selected (e.g., the
+	//     queue is empty or the policy logic determines a pause is appropriate).
+	//
+	// Conformance: Implementations MUST be goroutine-safe if they maintain internal state.
+	SelectItem(queue FlowQueueAccessor) (selectedItem types.QueueItemAccessor, err error)
+
+	// Comparator returns the `ItemComparator` that defines this policy's item ordering logic. This is the definitive
+	// source for how items within a flow governed by this policy should be prioritized.
+	//
+	// A policy MUST provide a meaningful comparator even if it relies on a queue's inherent ordering (e.g., an FCFS
+	// policy using a `CapabilityFIFO` queue should return a comparator based on enqueue time). This makes the ordering
+	// principle explicit and available to other components, like inter-flow policies.
+	//
+	// Conformance: MUST NOT return nil.
+	Comparator() ItemComparator
+
+	// RequiredQueueCapabilities returns a slice of capabilities that the `SafeQueue` used with this policy MUST support.
+	// This policy is responsible for defining the ordering of items within a flow and so it must require the relevant
+	// *behavioral* capability (e.g., `CapabilityPriorityConfigurable` or `CapabilityFIFO`). The `ItemComparator` vended
+	// by this policy then defines that behavior.
+	RequiredQueueCapabilities() []QueueCapability
+}
+
+// FlowQueueAccessor provides a policy-facing, read-only view of a single flow's queue.
+// It combines general queue inspection methods (embedded via `QueueInspectionMethods`) with flow-specific metadata.
+//
+// Instances of `FlowQueueAccessor` are vended by a `ports.ManagedQueue` and are the primary means by which policies
+// inspect individual queue state.
+//
+// Conformance: Implementations MUST ensure all methods (including those embedded from `QueueInspectionMethods`) are
+// goroutine-safe for concurrent access.
+type FlowQueueAccessor interface {
+	QueueInspectionMethods
+
+	// Comparator returns the `ItemComparator` that defines the ordering logic of the items within this queue.
+	// This is determined by the `IntraFlowDispatchPolicy` associated with this queue's flow.
+	Comparator() ItemComparator
+
+	// FlowSpec returns the `types.FlowSpecification` of the flow this queue accessor is associated with.
+	// This provides essential context (like `FlowID`) to policies.
+	//
+	// Conformance: Implementations MUST return a valid `types.FlowSpecification`.
+	FlowSpec() types.FlowSpecification
+}
diff --git a/pkg/epp/flowcontrol/framework/queue.go b/pkg/epp/flowcontrol/framework/queue.go
new file mode 100644
index 0000000..c4829f0
--- /dev/null
+++ b/pkg/epp/flowcontrol/framework/queue.go
@@ -0,0 +1,108 @@
+/*
+Copyright 2025 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 framework
+
+import (
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/types"
+)
+
+// QueueCapability defines a functional capability that a `SafeQueue` implementation can provide.
+// These capabilities allow policies (like `IntraFlowDispatchPolicy`) to declare their operational requirements,
+// ensuring that a policy is always paired with a compatible queue.
+//
+// For example, a policy that requires a priority-ordered queue would declare `CapabilityPriorityConfigurable`, and the
+// `ports.FlowRegistry` would ensure it is paired with a queue implementation (like a heap) that provides this
+// capability.
+//
+// While a simpler boolean method (e.g., `IsPriorityConfigurable()`) could satisfy current needs, this slice-based
+// approach is intentionally chosen for future extensibility. It allows a queue to advertise multiple features (e.g.,
+// dynamic priority reshuffling, size bounds, etc.) without future breaking changes to the `SafeQueue` interface.
+type QueueCapability string
+
+const (
+	// CapabilityFIFO indicates that the queue operates in a First-In, First-Out manner.
+	// `PeekHead()` will return the oldest item (by logical enqueue time).
+	CapabilityFIFO QueueCapability = "FIFO"
+
+	// CapabilityPriorityConfigurable indicates that the queue's ordering is determined by an `ItemComparator`.
+	// `PeekHead()` will return the highest priority item according to this comparator.
+	CapabilityPriorityConfigurable QueueCapability = "PriorityConfigurable"
+)
+
+// QueueInspectionMethods defines `SafeQueue`'s read-only methods.
+type QueueInspectionMethods interface {
+	// Name returns a string identifier for the concrete queue implementation type (e.g., "ListQueue").
+	Name() string
+
+	// Capabilities returns the set of functional capabilities this queue instance provides.
+	Capabilities() []QueueCapability
+
+	// Len returns the current number of items in the queue.
+	Len() int
+
+	// ByteSize returns the current total byte size of all items in the queue.
+	ByteSize() uint64
+
+	// PeekHead returns the item at the "head" of the queue (the item with the highest priority according to the queue's
+	// ordering) without removing it.
+	//
+	// Returns `ErrQueueEmpty` if the queue is empty.
+	PeekHead() (peekedItem types.QueueItemAccessor, err error)
+
+	// PeekTail returns the item at the "tail" of the queue (the item with the lowest priority according to the queue's
+	// ordering) without removing it.
+	//
+	// Returns `ErrQueueEmpty` if the queue is empty.
+	PeekTail() (peekedItem types.QueueItemAccessor, err error)
+}
+
+// PredicateFunc defines a function that returns true if a given item matches a certain condition.
+// It is used by `SafeQueue.Cleanup` to filter items.
+type PredicateFunc func(item types.QueueItemAccessor) bool
+
+// SafeQueue defines the contract for a single, concurrent-safe queue implementation.
+//
+// All implementations MUST be goroutine-safe.
+type SafeQueue interface {
+	QueueInspectionMethods
+
+	// Add attempts to enqueue an item. On success, it must associate a new, unique `types.QueueItemHandle` with the item
+	// by calling `item.SetHandle()`.
+	//
+	// Returns the new length and byte size of the queue.
+	Add(item types.QueueItemAccessor) (newLen, newByteSize uint64, err error)
+
+	// Remove atomically finds and removes the item identified by the given handle.
+	//
+	// On success, implementations MUST invalidate the provided handle by calling `handle.Invalidate()`.
+	//
+	// Returns the removed item and the new length and byte size of the queue.
+	// Returns `ErrInvalidQueueItemHandle` if the handle is invalid (e.g., nil, wrong type, already invalidated).
+	// Returns `ErrQueueItemNotFound` if the handle is valid but the item is not in the queue.
+	Remove(handle types.QueueItemHandle) (removedItem types.QueueItemAccessor, newLen, newByteSize uint64, err error)
+
+	// Cleanup iterates through the queue and atomically removes all items for which the predicate returns true, returning
+	// them in a slice.
+	//
+	// The handle for each removed item MUST be invalidated.
+	Cleanup(predicate PredicateFunc) (cleanedItems []types.QueueItemAccessor, err error)
+
+	// Drain atomically removes all items from the queue and returns them in a slice.
+	//
+	// The handle for all removed items MUST be invalidated. The queue MUST be empty after this operation.
+	Drain() (drainedItems []types.QueueItemAccessor, err error)
+}
diff --git a/pkg/epp/flowcontrol/types/doc.go b/pkg/epp/flowcontrol/types/doc.go
new file mode 100644
index 0000000..eaa8fc0
--- /dev/null
+++ b/pkg/epp/flowcontrol/types/doc.go
@@ -0,0 +1,22 @@
+/*
+Copyright 2025 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 types defines the core data structures and service contracts for the Flow Control system.
+//
+// It establishes the "vocabulary" of the system, defining the objects that are passed between the main controller,
+// policies, and queue plugins. The central data model revolves around the lifecycle of a request, which is
+// progressively wrapped in interfaces that provide an enriched, read-only view of its state.
+package types
diff --git a/pkg/epp/flowcontrol/types/flow.go b/pkg/epp/flowcontrol/types/flow.go
index 031a837..6c73f90 100644
--- a/pkg/epp/flowcontrol/types/flow.go
+++ b/pkg/epp/flowcontrol/types/flow.go
@@ -14,20 +14,18 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-// Package types defines the core data structures and service contracts for the Flow Controller system. It establishes
-// the "vocabulary" of the system, including the request lifecycle interfaces, final outcomes, and standard error types.
 package types
 
 // FlowSpecification defines the configuration of a logical flow, encapsulating its identity and registered priority.
 //
-// A FlowSpecification acts as the registration key for a flow within the Flow Registry.
-type FlowSpecification interface {
-	// ID returns the unique name or identifier for this flow (e.g., model name, tenant ID), corresponding to the value
-	// from `FlowControlRequest.FlowID()`.
-	ID() string
+// It acts as the registration key for a flow within the `ports.FlowRegistry`.
+type FlowSpecification struct {
+	// ID returns the unique name or identifier for this logical flow, corresponding to the value from
+	// `FlowControlRequest.FlowID()`.
+	ID string
 
-	// Priority returns the numerical priority level currently associated with this flow within the Flow Registry.
+	// Priority returns the numerical priority level currently associated with this flow within the `ports.FlowRegistry`.
 	//
 	// Convention: Lower numerical values indicate higher priority.
-	Priority() uint
+	Priority uint
 }
diff --git a/pkg/epp/flowcontrol/types/mocks/mocks.go b/pkg/epp/flowcontrol/types/mocks/mocks.go
new file mode 100644
index 0000000..04f40b4
--- /dev/null
+++ b/pkg/epp/flowcontrol/types/mocks/mocks.go
@@ -0,0 +1,93 @@
+/*
+Copyright 2025 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 mocks provides simple, configurable mock implementations of the core flow control types, intended for use in
+// unit and integration tests.
+package mocks
+
+import (
+	"context"
+	"time"
+
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/types"
+)
+
+// MockFlowControlRequest provides a mock implementation of the `types.FlowControlRequest` interface.
+type MockFlowControlRequest struct {
+	Ctx                  context.Context
+	FlowIDV              string
+	ByteSizeV            uint64
+	InitialEffectiveTTLV time.Duration
+	IDV                  string
+}
+
+func (m *MockFlowControlRequest) Context() context.Context           { return m.Ctx }
+func (m *MockFlowControlRequest) FlowID() string                     { return m.FlowIDV }
+func (m *MockFlowControlRequest) ByteSize() uint64                   { return m.ByteSizeV }
+func (m *MockFlowControlRequest) InitialEffectiveTTL() time.Duration { return m.InitialEffectiveTTLV }
+func (m *MockFlowControlRequest) ID() string                         { return m.IDV }
+
+var _ types.FlowControlRequest = &MockFlowControlRequest{}
+
+// MockQueueItemHandle provides a mock implementation of the `types.QueueItemHandle` interface.
+type MockQueueItemHandle struct {
+	RawHandle      any
+	IsInvalidatedV bool
+}
+
+func (m *MockQueueItemHandle) Handle() any         { return m.RawHandle }
+func (m *MockQueueItemHandle) Invalidate()         { m.IsInvalidatedV = true }
+func (m *MockQueueItemHandle) IsInvalidated() bool { return m.IsInvalidatedV }
+
+var _ types.QueueItemHandle = &MockQueueItemHandle{}
+
+// MockQueueItemAccessor provides a mock implementation of the `types.QueueItemAccessor` interface.
+type MockQueueItemAccessor struct {
+	EnqueueTimeV     time.Time
+	EffectiveTTLV    time.Duration
+	OriginalRequestV types.FlowControlRequest
+	HandleV          types.QueueItemHandle
+}
+
+func (m *MockQueueItemAccessor) EnqueueTime() time.Time      { return m.EnqueueTimeV }
+func (m *MockQueueItemAccessor) EffectiveTTL() time.Duration { return m.EffectiveTTLV }
+
+func (m *MockQueueItemAccessor) OriginalRequest() types.FlowControlRequest {
+	if m.OriginalRequestV == nil {
+		return &MockFlowControlRequest{}
+	}
+	return m.OriginalRequestV
+}
+
+func (m *MockQueueItemAccessor) Handle() types.QueueItemHandle          { return m.HandleV }
+func (m *MockQueueItemAccessor) SetHandle(handle types.QueueItemHandle) { m.HandleV = handle }
+
+var _ types.QueueItemAccessor = &MockQueueItemAccessor{}
+
+// NewMockQueueItemAccessor is a constructor for `MockQueueItemAccessor` that initializes the mock with a default
+// `MockFlowControlRequest` and `MockQueueItemHandle` to prevent nil pointer dereferences in tests.
+func NewMockQueueItemAccessor(byteSize uint64, reqID, flowID string) *MockQueueItemAccessor {
+	return &MockQueueItemAccessor{
+		EnqueueTimeV: time.Now(),
+		OriginalRequestV: &MockFlowControlRequest{
+			IDV:       reqID,
+			FlowIDV:   flowID,
+			ByteSizeV: byteSize,
+			Ctx:       context.Background(),
+		},
+		HandleV: &MockQueueItemHandle{},
+	}
+}
diff --git a/pkg/epp/flowcontrol/types/request.go b/pkg/epp/flowcontrol/types/request.go
index bf69605..4b6ae01 100644
--- a/pkg/epp/flowcontrol/types/request.go
+++ b/pkg/epp/flowcontrol/types/request.go
@@ -28,37 +28,39 @@ import (
 // wraps this object with its own internal structures (which implement `QueueItemAccessor`) to manage the request's
 // lifecycle without modifying the original.
 type FlowControlRequest interface {
-	// Context returns the request's context. The Flow Controller uses this for monitoring cancellation (e.g., if the
-	// client disconnects or a request-scoped timeout occurs), which can lead to the request being evicted from a queue.
+	// Context returns the request's context. The `controller.FlowController` uses this for monitoring cancellation (e.g.,
+	// if the client disconnects or a request-scoped timeout occurs), which can lead to the request being evicted from a
+	// queue.
 	Context() context.Context
 
 	// FlowID returns the unique identifier for the flow this request belongs to (e.g., model name, tenant ID). The
-	// Flow Controller uses this ID, in conjunction with the flow's registered priority, to look up the active
-	// `ports.ManagedQueue` from the Flow Registry's `ports.RegistryShard`.
+	// `controller.FlowController` uses this ID, in conjunction with the flow's registered priority, to look up the
+	// active `ports.ManagedQueue` from the `ports.FlowRegistry`'s `ports.RegistryShard`.
 	FlowID() string
 
-	// ByteSize returns the request's size in bytes (e.g., prompt size). This is used by the Flow Controller and for
-	// managing byte-based capacity limits and for Flow Registry statistics.
+	// ByteSize returns the request's size in bytes (e.g., prompt size). This is used by the `controller.FlowController`
+	// and for managing byte-based capacity limits and for `ports.FlowRegistry` statistics.
 	ByteSize() uint64
 
 	// InitialEffectiveTTL returns the suggested Time-To-Live for this request.
-	// This value is treated as a hint; the Flow Controller may override it based on its own configuration or policies.
-	// A zero value indicates the request has no specific TTL preference, and a system-wide default should be applied.
+	// This value is treated as a hint; the `controller.FlowController` may override it based on its own configuration or
+	// policies. A zero value indicates the request has no specific TTL preference, and a system-wide default should be
+	// applied.
 	InitialEffectiveTTL() time.Duration
 
 	// ID returns an optional, user-facing unique identifier for this specific request. It is intended for logging,
-	// tracing, and observability. The core flow control logic does not use this ID for dispatching decisions; it uses
+	// tracing, and observability. The `controller.FlowController` does not use this ID for dispatching decisions; it uses
 	// the internal, opaque `QueueItemHandle`.
 	ID() string
 }
 
 // QueueItemHandle is an opaque handle to an item that has been successfully added to a `framework.SafeQueue`. It acts
-// as a key, allowing the Flow Controller to perform targeted operations (like removal) on a specific item without
-// needing to know the queue's internal structure.
+// as a key, allowing the `controller.FlowController` to perform targeted operations (like removal) on a specific item
+// without needing to know the queue's internal structure.
 //
 // A handle is created by and bound to the specific `framework.SafeQueue` instance that stores the item.
 type QueueItemHandle interface {
-	// Handle returns the underlying, queue-specific raw handle (e.g., *list.Element).
+	// Handle returns the underlying, queue-specific raw handle (e.g., `*list.Element`).
 	// This method is intended for internal use by the `framework.SafeQueue` implementation that created it.
 	// Callers outside the queue implementation should treat the returned value as opaque.
 	Handle() any
@@ -82,29 +84,20 @@ type QueueItemHandle interface {
 //
 // The Flow Controller creates an object that implements this interface by wrapping an incoming `FlowControlRequest`.
 type QueueItemAccessor interface {
-	// EnqueueTime is the timestamp when the item was logically accepted by the Flow Controller for queuing (i.e., when
-	// `FlowController.EnqueueAndWait()` was called.
-	EnqueueTime() time.Time
-
-	// ByteSize returns the byte size of the original request, cached from `FlowControlRequest.ByteSize()`.
-	ByteSize() uint64
-
-	// FlowID returns the unique identifier of the flow this item belongs to, cached from `FlowControlRequest.FlowID()`.
-	FlowID() string
-
-	// EffectiveTTL is the actual Time-To-Live assigned to this item by the Flow Controller, taking into account the
-	// request's preference (`FlowControlRequest.InitialEffectiveTTL()`) and any Flow Controller or per-flow
-	// defaults/policies.
-	EffectiveTTL() time.Duration
-
-	// RequestID is the user-facing ID from the original request (`FlowControlRequest.ID()`).
-	RequestID() string
-
 	// OriginalRequest returns the underlying `FlowControlRequest` that this accessor provides a view of.
 	// This method serves as an escape hatch, allowing policies or components that are aware of specific
 	// `FlowControlRequest` implementations to perform type assertions and access richer, application-specific data.
 	OriginalRequest() FlowControlRequest
 
+	// EnqueueTime is the timestamp when the item was logically accepted by the `controller.FlowController` for queuing
+	// (i.e., when `controller.FlowController.EnqueueAndWait()` was called).
+	EnqueueTime() time.Time
+
+	// EffectiveTTL is the actual Time-To-Live assigned to this item by the `controller.FlowController`, taking into
+	// account the request's preference (`FlowControlRequest.InitialEffectiveTTL()`) and any `controller.FlowController`
+	// or per-flow defaults/policies.
+	EffectiveTTL() time.Duration
+
 	// Handle returns the `QueueItemHandle` associated with this item once it has been successfully added to a
 	// `framework.SafeQueue`. It returns nil if the item is not yet in a queue.
 	Handle() QueueItemHandle
@@ -115,7 +108,5 @@ type QueueItemAccessor interface {
 	// immediately after a new `QueueItemHandle` is created for the item. This ensures that the item always carries a
 	// valid handle while it is in a queue. This method is not intended for use outside of `framework.SafeQueue`
 	// implementations.
-	//
-	//go:doc
 	SetHandle(handle QueueItemHandle)
 }
diff --git a/pkg/epp/plugins/handle.go b/pkg/epp/plugins/handle.go
new file mode 100644
index 0000000..f00aa50
--- /dev/null
+++ b/pkg/epp/plugins/handle.go
@@ -0,0 +1,60 @@
+/*
+Copyright 2025 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 plugins
+
+import (
+	"context"
+	"fmt"
+)
+
+// Handle provides plugins a set of standard data and tools to work with
+type Handle interface {
+	// Context returns a context the plugins can use, if they need one
+	Context() context.Context
+
+	HandlePlugins
+}
+
+// HandlePlugins defines a set of APIs to work with instantiated plugins
+type HandlePlugins interface {
+	// Plugin returns the named plugin instance
+	Plugin(name string) Plugin
+
+	// AddPlugin adds a plugin to the set of known plugin instances
+	AddPlugin(name string, plugin Plugin)
+
+	// GetAllPlugins returns all of the known plugins
+	GetAllPlugins() []Plugin
+
+	// GetAllPluginsWithNames returns all of the known plugins with their names
+	GetAllPluginsWithNames() map[string]Plugin
+}
+
+// PluginByType retrieves the specified plugin by name and verifies its type
+func PluginByType[P Plugin](handlePlugins HandlePlugins, name string) (P, error) {
+	var zero P
+
+	rawPlugin := handlePlugins.Plugin(name)
+	if rawPlugin == nil {
+		return zero, fmt.Errorf("there is no plugin with the name '%s' defined", name)
+	}
+	plugin, ok := rawPlugin.(P)
+	if !ok {
+		return zero, fmt.Errorf("the plugin with the name '%s' is not an instance of %T", name, zero)
+	}
+	return plugin, nil
+}
diff --git a/pkg/epp/plugins/plugins.go b/pkg/epp/plugins/plugins.go
index 5d2a5f2..93ed23d 100644
--- a/pkg/epp/plugins/plugins.go
+++ b/pkg/epp/plugins/plugins.go
@@ -16,53 +16,9 @@ limitations under the License.
 
 package plugins
 
-import (
-	"context"
-	"fmt"
-)
-
 // Plugin defines the interface for a plugin.
 // This interface should be embedded in all plugins across the code.
 type Plugin interface {
 	// TypedName returns the type and name tuple of this plugin instance.
 	TypedName() TypedName
 }
-
-// Handle provides plugins a set of standard data and tools to work with
-type Handle interface {
-	// Context returns a context the plugins can use, if they need one
-	Context() context.Context
-
-	// Plugins returns the sub-handle for working with instantiated plugins
-	Plugins() HandlePlugins
-}
-
-// HandlePlugins defines a set of APIs to work with instantiated plugins
-type HandlePlugins interface {
-	// Plugin returns the named plugin instance
-	Plugin(name string) Plugin
-
-	// AddPlugin adds a plugin to the set of known plugin instances
-	AddPlugin(name string, plugin Plugin)
-
-	// GetAllPlugins returns all of the known plugins
-	GetAllPlugins() []Plugin
-
-	// GetAllPluginsWithNames returns all of the known plugins with their names
-	GetAllPluginsWithNames() map[string]Plugin
-}
-
-// PluginByType retrieves the specified plugin by name and verifies its type
-func PluginByType[P Plugin](handlePlugins HandlePlugins, name string) (P, error) {
-	var zero P
-
-	rawPlugin := handlePlugins.Plugin(name)
-	if rawPlugin == nil {
-		return zero, fmt.Errorf("there is no plugin with the name '%s' defined", name)
-	}
-	thePlugin, ok := rawPlugin.(P)
-	if !ok {
-		return zero, fmt.Errorf("the plugin with the name '%s' is not an instance of %T", name, zero)
-	}
-	return thePlugin, nil
-}
diff --git a/pkg/epp/plugins/typedname.go b/pkg/epp/plugins/typedname.go
index eee7813..174d97e 100644
--- a/pkg/epp/plugins/typedname.go
+++ b/pkg/epp/plugins/typedname.go
@@ -16,23 +16,19 @@ limitations under the License.
 
 package plugins
 
-// TypedName is a utility struct providing a type and a name
-// to plugins.
-// It implements the Plugin interface and can be embedded in
-// plugins across the code to reduce boilerplate.
+const (
+	separator = "/"
+)
+
+// TypedName is a utility struct providing a type and a name to plugins.
 type TypedName struct {
-	// Type returns the type of the plugin.
+	// Type returns the type of a plugin.
 	Type string
-	// Name returns the name of this plugin instance.
+	// Name returns the name of a plugin instance.
 	Name string
 }
 
-const (
-	Separator = "/"
-)
-
-// String returns the type and name rendered as
-// "<type>/<name>".
+// String returns the type and name rendered as "<name>/<type>".
 func (tn *TypedName) String() string {
-	return tn.Type + Separator + tn.Name
+	return tn.Name + separator + tn.Type
 }
diff --git a/pkg/epp/requestcontrol/director.go b/pkg/epp/requestcontrol/director.go
index d4a0c0b..670d922 100644
--- a/pkg/epp/requestcontrol/director.go
+++ b/pkg/epp/requestcontrol/director.go
@@ -80,7 +80,7 @@ type Director struct {
 //  1. Parses request details.
 //  2. Calls admitRequest for admission control.
 //  3. Calls Scheduler.Schedule if request is approved.
-//  4. Calls prepareRequest to populate RequestContext with results and call PreRequest plugins.
+//  4. Calls prepareRequest to populate RequestContext with result and call PreRequest plugins.
 //
 // It always returns the requestContext even in the error case, as the request context is used in error handling.
 func (d *Director) HandleRequest(ctx context.Context, reqCtx *handlers.RequestContext) (*handlers.RequestContext, error) {
@@ -147,7 +147,7 @@ func (d *Director) HandleRequest(ctx context.Context, reqCtx *handlers.RequestCo
 	if len(candidatePods) == 0 {
 		return reqCtx, errutil.Error{Code: errutil.ServiceUnavailable, Msg: "failed to find candidate pods for serving the request"}
 	}
-	results, err := d.scheduler.Schedule(ctx, reqCtx.SchedulingRequest, candidatePods)
+	result, err := d.scheduler.Schedule(ctx, reqCtx.SchedulingRequest, candidatePods)
 	if err != nil {
 		return reqCtx, errutil.Error{Code: errutil.InferencePoolResourceExhausted, Msg: fmt.Errorf("failed to find target pod: %w", err).Error()}
 	}
@@ -155,7 +155,7 @@ func (d *Director) HandleRequest(ctx context.Context, reqCtx *handlers.RequestCo
 	// --- 4. Prepare Request (Populates RequestContext and call PreRequest plugins) ---
 	// Insert target endpoint to instruct Envoy to route requests to the specified target pod and attach the port number.
 	// Invoke PreRequest registered plugins.
-	reqCtx, err = d.prepareRequest(ctx, reqCtx, results)
+	reqCtx, err = d.prepareRequest(ctx, reqCtx, result)
 	if err != nil {
 		return reqCtx, err
 	}
@@ -195,13 +195,13 @@ func (d *Director) getCandidatePodsForScheduling(ctx context.Context, requestMet
 
 	subsetMap, found := requestMetadata[subsetHintNamespace].(map[string]any)
 	if !found {
-		return schedulingtypes.ToSchedulerPodMetrics(d.datastore.PodGetAll())
+		return d.toSchedulerPodMetrics(d.datastore.PodGetAll())
 	}
 
 	// Check if endpoint key is present in the subset map and ensure there is at least one value
 	endpointSubsetList, found := subsetMap[subsetHintKey].([]any)
 	if !found {
-		return schedulingtypes.ToSchedulerPodMetrics(d.datastore.PodGetAll())
+		return d.toSchedulerPodMetrics(d.datastore.PodGetAll())
 	} else if len(endpointSubsetList) == 0 {
 		loggerTrace.Info("found empty subset filter in request metadata, filtering all pods")
 		return []schedulingtypes.Pod{}
@@ -227,18 +227,19 @@ func (d *Director) getCandidatePodsForScheduling(ctx context.Context, requestMet
 
 	loggerTrace.Info("filtered candidate pods by subset filtering", "podTotalCount", podTotalCount, "filteredCount", len(podFitleredList))
 
-	return schedulingtypes.ToSchedulerPodMetrics(podFitleredList)
+	return d.toSchedulerPodMetrics(podFitleredList)
 }
 
 // prepareRequest populates the RequestContext and calls the registered PreRequest plugins
-// for allowing plugging customized logic based on the scheduling results.
+// for allowing plugging customized logic based on the scheduling result.
 func (d *Director) prepareRequest(ctx context.Context, reqCtx *handlers.RequestContext, result *schedulingtypes.SchedulingResult) (*handlers.RequestContext, error) {
 	logger := log.FromContext(ctx)
 	if result == nil || len(result.ProfileResults) == 0 {
 		return reqCtx, errutil.Error{Code: errutil.Internal, Msg: "results must be greater than zero"}
 	}
 	// primary profile is used to set destination
-	targetPod := result.ProfileResults[result.PrimaryProfileName].TargetPod.GetPod()
+	// TODO should use multiple destinations according to epp protocol. current code assumes a single target
+	targetPod := result.ProfileResults[result.PrimaryProfileName].TargetPods[0].GetPod()
 
 	pool, err := d.datastore.PoolGet()
 	if err != nil {
@@ -257,6 +258,15 @@ func (d *Director) prepareRequest(ctx context.Context, reqCtx *handlers.RequestC
 	return reqCtx, nil
 }
 
+func (d *Director) toSchedulerPodMetrics(pods []backendmetrics.PodMetrics) []schedulingtypes.Pod {
+	pm := make([]schedulingtypes.Pod, len(pods))
+	for i, pod := range pods {
+		pm[i] = &schedulingtypes.PodMetrics{Pod: pod.GetPod().Clone(), MetricsState: pod.GetMetrics().Clone()}
+	}
+
+	return pm
+}
+
 func (d *Director) HandleResponse(ctx context.Context, reqCtx *handlers.RequestContext) (*handlers.RequestContext, error) {
 	response := &Response{
 		RequestId: reqCtx.Request.Headers[requtil.RequestIdHeaderKey],
@@ -310,7 +320,8 @@ func RandomWeightedDraw(logger logr.Logger, model *v1alpha2.InferenceModel, seed
 }
 
 func (d *Director) runPreRequestPlugins(ctx context.Context, request *schedulingtypes.LLMRequest, schedulingResult *schedulingtypes.SchedulingResult,
-	targetPort int) {
+	targetPort int,
+) {
 	for _, plugin := range d.preRequestPlugins {
 		log.FromContext(ctx).V(logutil.DEBUG).Info("Running pre-request plugin", "plugin", plugin.TypedName().Type)
 		before := time.Now()
diff --git a/pkg/epp/scheduling/framework/plugins/filter/decision_tree_filter.go b/pkg/epp/scheduling/framework/plugins/filter/decision_tree_filter.go
index 662107a..a9a0a7c 100644
--- a/pkg/epp/scheduling/framework/plugins/filter/decision_tree_filter.go
+++ b/pkg/epp/scheduling/framework/plugins/filter/decision_tree_filter.go
@@ -30,7 +30,7 @@ import (
 )
 
 const (
-	DecisionTreeFilterType = "decision-tree"
+	DecisionTreeFilterType = "decision-tree-filter"
 )
 
 // compile-time type assertion
@@ -119,7 +119,7 @@ func loadDecisionTreeEntry(entry *decisionTreeFilterEntry, handle plugins.Handle
 	}
 
 	if entry.PluginRef != nil {
-		instance := handle.Plugins().Plugin(*entry.PluginRef)
+		instance := handle.Plugin(*entry.PluginRef)
 		if instance == nil {
 			return nil, errors.New(*entry.PluginRef + " is a reference to an undefined Plugin")
 		}
diff --git a/pkg/epp/scheduling/framework/plugins/filter/least_kvcache_filter.go b/pkg/epp/scheduling/framework/plugins/filter/least_kvcache_filter.go
index 3cf9bb6..14ef379 100644
--- a/pkg/epp/scheduling/framework/plugins/filter/least_kvcache_filter.go
+++ b/pkg/epp/scheduling/framework/plugins/filter/least_kvcache_filter.go
@@ -27,7 +27,7 @@ import (
 )
 
 const (
-	LeastKVCacheFilterType = "least-KV-cache"
+	LeastKVCacheFilterType = "least-kv-cache-filter"
 )
 
 // compile-time type validation
@@ -41,7 +41,7 @@ func LeastKVCacheFilterFactory(name string, _ json.RawMessage, _ plugins.Handle)
 // NewLeastKVCacheFilter initializes a new LeastKVCacheFilter and returns its pointer.
 func NewLeastKVCacheFilter() *LeastKVCacheFilter {
 	return &LeastKVCacheFilter{
-		tn: plugins.TypedName{Type: LeastKVCacheFilterType, Name: LeastKVCacheFilterType},
+		typedName: plugins.TypedName{Type: LeastKVCacheFilterType, Name: LeastKVCacheFilterType},
 	}
 }
 
@@ -51,17 +51,17 @@ func NewLeastKVCacheFilter() *LeastKVCacheFilter {
 // should consider them all instead of the absolute minimum one. This worked better than picking the
 // least one as it gives more choices for the next filter, which on aggregate gave better results.
 type LeastKVCacheFilter struct {
-	tn plugins.TypedName
+	typedName plugins.TypedName
 }
 
 // TypedName returns the type and name tuple of this plugin instance.
 func (f *LeastKVCacheFilter) TypedName() plugins.TypedName {
-	return f.tn
+	return f.typedName
 }
 
 // WithName sets the name of the filter.
 func (f *LeastKVCacheFilter) WithName(name string) *LeastKVCacheFilter {
-	f.tn.Name = name
+	f.typedName.Name = name
 	return f
 }
 
diff --git a/pkg/epp/scheduling/framework/plugins/filter/least_queue_filter.go b/pkg/epp/scheduling/framework/plugins/filter/least_queue_filter.go
index 387cc59..dea5500 100644
--- a/pkg/epp/scheduling/framework/plugins/filter/least_queue_filter.go
+++ b/pkg/epp/scheduling/framework/plugins/filter/least_queue_filter.go
@@ -27,7 +27,7 @@ import (
 )
 
 const (
-	LeastQueueFilterType = "least-queue"
+	LeastQueueFilterType = "least-queue-filter"
 )
 
 // compile-time type validation
@@ -41,7 +41,7 @@ func LeastQueueFilterFactory(name string, _ json.RawMessage, _ plugins.Handle) (
 // NewLeastQueueFilter initializes a new LeastQueueFilter and returns its pointer.
 func NewLeastQueueFilter() *LeastQueueFilter {
 	return &LeastQueueFilter{
-		tn: plugins.TypedName{Type: LeastQueueFilterType, Name: LeastQueueFilterType},
+		typedName: plugins.TypedName{Type: LeastQueueFilterType, Name: LeastQueueFilterType},
 	}
 }
 
@@ -51,17 +51,17 @@ func NewLeastQueueFilter() *LeastQueueFilter {
 // we should consider them all instead of the absolute minimum one. This worked better than picking
 // the least one as it gives more choices for the next filter, which on aggregate gave better results.
 type LeastQueueFilter struct {
-	tn plugins.TypedName
+	typedName plugins.TypedName
 }
 
 // TypedName returns the type and name tuple of this plugin instance.
 func (f *LeastQueueFilter) TypedName() plugins.TypedName {
-	return f.tn
+	return f.typedName
 }
 
 // WithName sets the name of the filter.
 func (f *LeastQueueFilter) WithName(name string) *LeastQueueFilter {
-	f.tn.Name = name
+	f.typedName.Name = name
 	return f
 }
 
diff --git a/pkg/epp/scheduling/framework/plugins/filter/lora_affinity_filter.go b/pkg/epp/scheduling/framework/plugins/filter/lora_affinity_filter.go
index 145743f..79f2e5f 100644
--- a/pkg/epp/scheduling/framework/plugins/filter/lora_affinity_filter.go
+++ b/pkg/epp/scheduling/framework/plugins/filter/lora_affinity_filter.go
@@ -30,7 +30,7 @@ import (
 )
 
 const (
-	LoraAffinityFilterType = "lora-affinity"
+	LoraAffinityFilterType = "lora-affinity-filter"
 )
 
 type loraAffinityFilterParameters struct {
@@ -43,8 +43,10 @@ var _ framework.Filter = &LoraAffinityFilter{}
 // LoraAffinityFilterFactory defines the factory function for LoraAffinityFilter.
 func LoraAffinityFilterFactory(name string, rawParameters json.RawMessage, _ plugins.Handle) (plugins.Plugin, error) {
 	parameters := loraAffinityFilterParameters{Threshold: config.DefaultLoraAffinityThreshold}
-	if err := json.Unmarshal(rawParameters, &parameters); err != nil {
-		return nil, fmt.Errorf("failed to parse the parameters of the '%s' filter - %w", LoraAffinityFilterType, err)
+	if rawParameters != nil {
+		if err := json.Unmarshal(rawParameters, &parameters); err != nil {
+			return nil, fmt.Errorf("failed to parse the parameters of the '%s' filter - %w", LoraAffinityFilterType, err)
+		}
 	}
 	return NewLoraAffinityFilter(parameters.Threshold).WithName(name), nil
 }
@@ -52,7 +54,7 @@ func LoraAffinityFilterFactory(name string, rawParameters json.RawMessage, _ plu
 // NewLoraAffinityFilter initializes a new LoraAffinityFilter and returns its pointer.
 func NewLoraAffinityFilter(threshold float64) *LoraAffinityFilter {
 	return &LoraAffinityFilter{
-		tn:                    plugins.TypedName{Type: LoraAffinityFilterType, Name: LoraAffinityFilterType},
+		typedName:             plugins.TypedName{Type: LoraAffinityFilterType, Name: LoraAffinityFilterType},
 		loraAffinityThreshold: threshold,
 	}
 }
@@ -65,18 +67,18 @@ func NewLoraAffinityFilter(threshold float64) *LoraAffinityFilter {
 // 2. Using a probability threshold to sometimes select from non-affinity pods to enable load balancing
 // 3. Falling back to whatever group has pods if one group is empty
 type LoraAffinityFilter struct {
-	tn                    plugins.TypedName
+	typedName             plugins.TypedName
 	loraAffinityThreshold float64
 }
 
 // TypedName returns the type and name tuple of this plugin instance.
 func (f *LoraAffinityFilter) TypedName() plugins.TypedName {
-	return f.tn
+	return f.typedName
 }
 
 // WithName sets the type of the filter.
 func (f *LoraAffinityFilter) WithName(name string) *LoraAffinityFilter {
-	f.tn.Name = name
+	f.typedName.Name = name
 	return f
 }
 
diff --git a/pkg/epp/scheduling/framework/plugins/filter/low_queue_filter.go b/pkg/epp/scheduling/framework/plugins/filter/low_queue_filter.go
index 8b121e7..597c2a1 100644
--- a/pkg/epp/scheduling/framework/plugins/filter/low_queue_filter.go
+++ b/pkg/epp/scheduling/framework/plugins/filter/low_queue_filter.go
@@ -29,7 +29,7 @@ import (
 )
 
 const (
-	LowQueueFilterType = "low-queue"
+	LowQueueFilterType = "low-queue-filter"
 )
 
 type lowQueueFilterParameters struct {
@@ -42,8 +42,10 @@ var _ framework.Filter = &LowQueueFilter{}
 // LowQueueFilterFactory defines the factory function for LowQueueFilter.
 func LowQueueFilterFactory(name string, rawParameters json.RawMessage, _ plugins.Handle) (plugins.Plugin, error) {
 	parameters := lowQueueFilterParameters{Threshold: config.DefaultQueueingThresholdLoRA}
-	if err := json.Unmarshal(rawParameters, &parameters); err != nil {
-		return nil, fmt.Errorf("failed to parse the parameters of the '%s' filter - %w", LowQueueFilterType, err)
+	if rawParameters != nil {
+		if err := json.Unmarshal(rawParameters, &parameters); err != nil {
+			return nil, fmt.Errorf("failed to parse the parameters of the '%s' filter - %w", LowQueueFilterType, err)
+		}
 	}
 
 	return NewLowQueueFilter(parameters.Threshold).WithName(name), nil
@@ -52,25 +54,25 @@ func LowQueueFilterFactory(name string, rawParameters json.RawMessage, _ plugins
 // NewLowQueueFilter initializes a new LowQueueFilter and returns its pointer.
 func NewLowQueueFilter(threshold int) *LowQueueFilter {
 	return &LowQueueFilter{
-		tn:                    plugins.TypedName{Type: LowQueueFilterType, Name: LowQueueFilterType},
+		typedName:             plugins.TypedName{Type: LowQueueFilterType, Name: LowQueueFilterType},
 		queueingThresholdLoRA: threshold,
 	}
 }
 
 // LowQueueFilter returns pods that their waiting queue size is less than a configured threshold
 type LowQueueFilter struct {
-	tn                    plugins.TypedName
+	typedName             plugins.TypedName
 	queueingThresholdLoRA int
 }
 
 // TypedName returns the type and name tuple of this plugin instance.
 func (f *LowQueueFilter) TypedName() plugins.TypedName {
-	return f.tn
+	return f.typedName
 }
 
 // WithName sets the name of the filter.
 func (f *LowQueueFilter) WithName(name string) *LowQueueFilter {
-	f.tn.Name = name
+	f.typedName.Name = name
 	return f
 }
 
diff --git a/pkg/epp/scheduling/framework/plugins/multi/prefix/plugin.go b/pkg/epp/scheduling/framework/plugins/multi/prefix/plugin.go
index a291f47..42e6303 100644
--- a/pkg/epp/scheduling/framework/plugins/multi/prefix/plugin.go
+++ b/pkg/epp/scheduling/framework/plugins/multi/prefix/plugin.go
@@ -52,7 +52,7 @@ const (
 	// in vLLM, we will have 250K / 16 = 31.25K blocks.
 	DefaultLRUCapacityPerServer = 31250
 
-	PrefixCachePluginType = "prefix-cache"
+	PrefixCachePluginType = "prefix-cache-scorer"
 )
 
 type Config struct {
@@ -68,8 +68,8 @@ type Config struct {
 
 type Plugin struct {
 	Config
-	tn      plugins.TypedName
-	indexer Indexer
+	typedName plugins.TypedName
+	indexer   Indexer
 }
 
 // podSet holds an pods servers that may have a specific prefix hash.
@@ -125,8 +125,11 @@ func PrefixCachePluginFactory(name string, rawParameters json.RawMessage, _ plug
 		MaxPrefixBlocksToMatch: DefaultMaxPrefixBlocks,
 		LRUCapacityPerServer:   DefaultLRUCapacityPerServer,
 	}
-	if err := json.Unmarshal(rawParameters, &parameters); err != nil {
-		return nil, fmt.Errorf("failed to parse the parameters of the %s plugin. Error: %s", PrefixCachePluginType, err)
+
+	if rawParameters != nil {
+		if err := json.Unmarshal(rawParameters, &parameters); err != nil {
+			return nil, fmt.Errorf("failed to parse the parameters of the %s plugin. Error: %s", PrefixCachePluginType, err)
+		}
 	}
 
 	return New(parameters).WithName(name), nil
@@ -144,20 +147,20 @@ func New(config Config) *Plugin {
 	}
 
 	return &Plugin{
-		tn:      plugins.TypedName{Type: PrefixCachePluginType, Name: PrefixCachePluginType},
-		Config:  config,
-		indexer: newIndexer(capacity),
+		typedName: plugins.TypedName{Type: PrefixCachePluginType, Name: PrefixCachePluginType},
+		Config:    config,
+		indexer:   newIndexer(capacity),
 	}
 }
 
 // TypedName returns the type and name tuple of this plugin instance.
 func (m *Plugin) TypedName() plugins.TypedName {
-	return m.tn
+	return m.typedName
 }
 
 // WithName sets the name of the plugin.
 func (m *Plugin) WithName(name string) *Plugin {
-	m.tn.Name = name
+	m.typedName.Name = name
 	return m
 }
 
@@ -193,7 +196,7 @@ func (m *Plugin) Score(ctx context.Context, cycleState *types.CycleState, reques
 
 // PostCycle records in the plugin cache the result of the scheduling selection.
 func (m *Plugin) PostCycle(ctx context.Context, cycleState *types.CycleState, res *types.ProfileRunResult) {
-	targetPod := res.TargetPod.GetPod()
+	targetPod := res.TargetPods[0].GetPod()
 	state, err := types.ReadCycleStateKey[*SchedulingContextState](cycleState, PrefixCachePluginType)
 	if err != nil {
 		log.FromContext(ctx).Error(err, "failed to read prefix plugin cycle state")
diff --git a/pkg/epp/scheduling/framework/plugins/picker/common.go b/pkg/epp/scheduling/framework/plugins/picker/common.go
new file mode 100644
index 0000000..4bbc300
--- /dev/null
+++ b/pkg/epp/scheduling/framework/plugins/picker/common.go
@@ -0,0 +1,26 @@
+/*
+Copyright 2025 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 picker
+
+const (
+	DefaultMaxNumOfEndpoints = 1 // common default to all pickers
+)
+
+// pickerParameters defines the common parameters for all pickers
+type pickerParameters struct {
+	MaxNumOfEndpoints int `json:"maxNumOfEndpoints"`
+}
diff --git a/pkg/epp/scheduling/framework/plugins/picker/max_score_picker.go b/pkg/epp/scheduling/framework/plugins/picker/max_score_picker.go
index eea8397..bf3ca8d 100644
--- a/pkg/epp/scheduling/framework/plugins/picker/max_score_picker.go
+++ b/pkg/epp/scheduling/framework/plugins/picker/max_score_picker.go
@@ -20,8 +20,10 @@ import (
 	"context"
 	"encoding/json"
 	"fmt"
+	"slices"
 
 	"sigs.k8s.io/controller-runtime/pkg/log"
+
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/plugins"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/framework"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/types"
@@ -29,60 +31,78 @@ import (
 )
 
 const (
-	MaxScorePickerType = "max-score"
+	MaxScorePickerType = "max-score-picker"
 )
 
 // compile-time type validation
 var _ framework.Picker = &MaxScorePicker{}
 
 // MaxScorePickerFactory defines the factory function for MaxScorePicker.
-func MaxScorePickerFactory(name string, _ json.RawMessage, _ plugins.Handle) (plugins.Plugin, error) {
-	return NewMaxScorePicker().WithName(name), nil
+func MaxScorePickerFactory(name string, rawParameters json.RawMessage, _ plugins.Handle) (plugins.Plugin, error) {
+	parameters := pickerParameters{MaxNumOfEndpoints: DefaultMaxNumOfEndpoints}
+	if rawParameters != nil {
+		if err := json.Unmarshal(rawParameters, &parameters); err != nil {
+			return nil, fmt.Errorf("failed to parse the parameters of the '%s' picker - %w", MaxScorePickerType, err)
+		}
+	}
+
+	return NewMaxScorePicker(parameters.MaxNumOfEndpoints).WithName(name), nil
 }
 
 // NewMaxScorePicker initializes a new MaxScorePicker and returns its pointer.
-func NewMaxScorePicker() *MaxScorePicker {
+func NewMaxScorePicker(maxNumOfEndpoints int) *MaxScorePicker {
+	if maxNumOfEndpoints <= 0 {
+		maxNumOfEndpoints = DefaultMaxNumOfEndpoints // on invalid configuration value, fallback to default value
+	}
+
 	return &MaxScorePicker{
-		tn:     plugins.TypedName{Type: MaxScorePickerType, Name: MaxScorePickerType},
-		random: NewRandomPicker(),
+		typedName:         plugins.TypedName{Type: MaxScorePickerType, Name: MaxScorePickerType},
+		maxNumOfEndpoints: maxNumOfEndpoints,
 	}
 }
 
-// MaxScorePicker picks the pod with the maximum score from the list of candidates.
+// MaxScorePicker picks pod(s) with the maximum score from the list of candidates.
 type MaxScorePicker struct {
-	tn     plugins.TypedName
-	random *RandomPicker
-}
-
-// TypedName returns the type and name tuple of this plugin instance.
-func (p *MaxScorePicker) TypedName() plugins.TypedName {
-	return p.tn
+	typedName         plugins.TypedName
+	maxNumOfEndpoints int // maximum number of endpoints to pick
 }
 
 // WithName sets the picker's name
 func (p *MaxScorePicker) WithName(name string) *MaxScorePicker {
-	p.tn.Name = name
+	p.typedName.Name = name
 	return p
 }
 
+// TypedName returns the type and name tuple of this plugin instance.
+func (p *MaxScorePicker) TypedName() plugins.TypedName {
+	return p.typedName
+}
+
 // Pick selects the pod with the maximum score from the list of candidates.
 func (p *MaxScorePicker) Pick(ctx context.Context, cycleState *types.CycleState, scoredPods []*types.ScoredPod) *types.ProfileRunResult {
-	log.FromContext(ctx).V(logutil.DEBUG).Info(fmt.Sprintf("Selecting a pod with the max score from %d candidates: %+v", len(scoredPods), scoredPods))
-
-	highestScorePods := []*types.ScoredPod{}
-	maxScore := -1.0 // pods min score is 0, putting value lower than 0 in order to find at least one pod as highest
-	for _, pod := range scoredPods {
-		if pod.Score > maxScore {
-			maxScore = pod.Score
-			highestScorePods = []*types.ScoredPod{pod}
-		} else if pod.Score == maxScore {
-			highestScorePods = append(highestScorePods, pod)
+	log.FromContext(ctx).V(logutil.DEBUG).Info(fmt.Sprintf("Selecting maximum '%d' pods from %d candidates sorted by max score: %+v", p.maxNumOfEndpoints,
+		len(scoredPods), scoredPods))
+
+	slices.SortStableFunc(scoredPods, func(i, j *types.ScoredPod) int { // highest score first
+		if i.Score > j.Score {
+			return -1
+		}
+		if i.Score < j.Score {
+			return 1
 		}
+		return 0
+	})
+
+	// if we have enough pods to return keep only the "maxNumOfEndpoints" highest scored pods
+	if p.maxNumOfEndpoints < len(scoredPods) {
+		scoredPods = scoredPods[:p.maxNumOfEndpoints]
 	}
 
-	if len(highestScorePods) > 1 {
-		return p.random.Pick(ctx, cycleState, highestScorePods) // pick randomly from the highest score pods
+	targetPods := make([]types.Pod, len(scoredPods))
+	for i, scoredPod := range scoredPods {
+		targetPods[i] = scoredPod
 	}
 
-	return &types.ProfileRunResult{TargetPod: highestScorePods[0]}
+	return &types.ProfileRunResult{TargetPods: targetPods}
+
 }
diff --git a/pkg/epp/scheduling/framework/plugins/picker/random_picker.go b/pkg/epp/scheduling/framework/plugins/picker/random_picker.go
index 7c6955a..bb272f1 100644
--- a/pkg/epp/scheduling/framework/plugins/picker/random_picker.go
+++ b/pkg/epp/scheduling/framework/plugins/picker/random_picker.go
@@ -31,43 +31,71 @@ import (
 )
 
 const (
-	RandomPickerType = "random"
+	RandomPickerType = "random-picker"
 )
 
 // compile-time type validation
 var _ framework.Picker = &RandomPicker{}
 
-// RandomPickerFactory defines the factory function for RandomPicker.
-func RandomPickerFactory(name string, _ json.RawMessage, _ plugins.Handle) (plugins.Plugin, error) {
-	return NewRandomPicker().WithName(name), nil
+func RandomPickerFactory(name string, rawParameters json.RawMessage, _ plugins.Handle) (plugins.Plugin, error) {
+	parameters := pickerParameters{MaxNumOfEndpoints: DefaultMaxNumOfEndpoints}
+	if rawParameters != nil {
+		if err := json.Unmarshal(rawParameters, &parameters); err != nil {
+			return nil, fmt.Errorf("failed to parse the parameters of the '%s' picker - %w", RandomPickerType, err)
+		}
+	}
+
+	return NewRandomPicker(parameters.MaxNumOfEndpoints).WithName(name), nil
 }
 
 // NewRandomPicker initializes a new RandomPicker and returns its pointer.
-func NewRandomPicker() *RandomPicker {
+func NewRandomPicker(maxNumOfEndpoints int) *RandomPicker {
+	if maxNumOfEndpoints <= 0 {
+		maxNumOfEndpoints = DefaultMaxNumOfEndpoints // on invalid configuration value, fallback to default value
+	}
+
 	return &RandomPicker{
-		tn: plugins.TypedName{Type: RandomPickerType, Name: RandomPickerType},
+		typedName:         plugins.TypedName{Type: RandomPickerType, Name: RandomPickerType},
+		maxNumOfEndpoints: maxNumOfEndpoints,
 	}
 }
 
-// RandomPicker picks a random pod from the list of candidates.
+// RandomPicker picks random pod(s) from the list of candidates.
 type RandomPicker struct {
-	tn plugins.TypedName
-}
-
-// TypedName returns the type and name tuple of this plugin instance.
-func (p *RandomPicker) TypedName() plugins.TypedName {
-	return p.tn
+	typedName         plugins.TypedName
+	maxNumOfEndpoints int
 }
 
 // WithName sets the name of the picker.
 func (p *RandomPicker) WithName(name string) *RandomPicker {
-	p.tn.Name = name
+	p.typedName.Name = name
 	return p
 }
 
-// Pick selects a random pod from the list of candidates.
+// TypedName returns the type and name tuple of this plugin instance.
+func (p *RandomPicker) TypedName() plugins.TypedName {
+	return p.typedName
+}
+
+// Pick selects random pod(s) from the list of candidates.
 func (p *RandomPicker) Pick(ctx context.Context, _ *types.CycleState, scoredPods []*types.ScoredPod) *types.ProfileRunResult {
-	log.FromContext(ctx).V(logutil.DEBUG).Info(fmt.Sprintf("Selecting a random pod from %d candidates: %+v", len(scoredPods), scoredPods))
-	i := rand.Intn(len(scoredPods))
-	return &types.ProfileRunResult{TargetPod: scoredPods[i]}
+	log.FromContext(ctx).V(logutil.DEBUG).Info(fmt.Sprintf("Selecting maximum '%d' pods from %d candidates randomly: %+v", p.maxNumOfEndpoints,
+		len(scoredPods), scoredPods))
+
+	// Shuffle in-place
+	rand.Shuffle(len(scoredPods), func(i, j int) {
+		scoredPods[i], scoredPods[j] = scoredPods[j], scoredPods[i]
+	})
+
+	// if we have enough pods to return keep only the relevant subset
+	if p.maxNumOfEndpoints < len(scoredPods) {
+		scoredPods = scoredPods[:p.maxNumOfEndpoints]
+	}
+
+	targetPods := make([]types.Pod, len(scoredPods))
+	for i, scoredPod := range scoredPods {
+		targetPods[i] = scoredPod
+	}
+
+	return &types.ProfileRunResult{TargetPods: targetPods}
 }
diff --git a/pkg/epp/scheduling/framework/plugins/profile/single_profile_handler.go b/pkg/epp/scheduling/framework/plugins/profile/single_profile_handler.go
index e880948..3efaa7b 100644
--- a/pkg/epp/scheduling/framework/plugins/profile/single_profile_handler.go
+++ b/pkg/epp/scheduling/framework/plugins/profile/single_profile_handler.go
@@ -28,7 +28,7 @@ import (
 )
 
 const (
-	SingleProfileHandlerType = "single-profile"
+	SingleProfileHandlerType = "single-profile-handler"
 )
 
 // compile-time type assertion
@@ -42,23 +42,23 @@ func SingleProfileHandlerFactory(name string, _ json.RawMessage, _ plugins.Handl
 // NewSingleProfileHandler initializes a new SingleProfileHandler and returns its pointer.
 func NewSingleProfileHandler() *SingleProfileHandler {
 	return &SingleProfileHandler{
-		tn: plugins.TypedName{Type: SingleProfileHandlerType, Name: SingleProfileHandlerType},
+		typedName: plugins.TypedName{Type: SingleProfileHandlerType, Name: SingleProfileHandlerType},
 	}
 }
 
 // SingleProfileHandler handles a single profile which is always the primary profile.
 type SingleProfileHandler struct {
-	tn plugins.TypedName
+	typedName plugins.TypedName
 }
 
 // TypedName returns the type and name tuple of this plugin instance.
 func (h *SingleProfileHandler) TypedName() plugins.TypedName {
-	return h.tn
+	return h.typedName
 }
 
 // WithName sets the name of the profile handler.
 func (h *SingleProfileHandler) WithName(name string) *SingleProfileHandler {
-	h.tn.Name = name
+	h.typedName.Name = name
 	return h
 }
 
diff --git a/pkg/epp/scheduling/framework/plugins/scorer/kvcache.go b/pkg/epp/scheduling/framework/plugins/scorer/kvcache.go
index 387ae0b..7b208ba 100644
--- a/pkg/epp/scheduling/framework/plugins/scorer/kvcache.go
+++ b/pkg/epp/scheduling/framework/plugins/scorer/kvcache.go
@@ -27,7 +27,7 @@ import (
 
 const (
 	DefaultKVCacheScorerWeight = 1
-	KvCacheScorerType          = "kv-cache"
+	KvCacheScorerType          = "kv-cache-scorer"
 )
 
 // compile-time type assertion
@@ -41,23 +41,23 @@ func KvCacheScorerFactory(name string, _ json.RawMessage, _ plugins.Handle) (plu
 // NewKVCacheScorer initializes a new KVCacheScorer and returns its pointer.
 func NewKVCacheScorer() *KVCacheScorer {
 	return &KVCacheScorer{
-		tn: plugins.TypedName{Type: KvCacheScorerType, Name: KvCacheScorerType},
+		typedName: plugins.TypedName{Type: KvCacheScorerType, Name: KvCacheScorerType},
 	}
 }
 
 // KVCacheScorer scores list of candidate pods based on KV cache utilization.
 type KVCacheScorer struct {
-	tn plugins.TypedName
+	typedName plugins.TypedName
 }
 
 // TypedName returns the type and name tuple of this plugin instance.
 func (s *KVCacheScorer) TypedName() plugins.TypedName {
-	return s.tn
+	return s.typedName
 }
 
 // WithName sets the name of the scorer.
 func (s *KVCacheScorer) WithName(name string) *KVCacheScorer {
-	s.tn.Name = name
+	s.typedName.Name = name
 	return s
 }
 
diff --git a/pkg/epp/scheduling/framework/plugins/scorer/queue.go b/pkg/epp/scheduling/framework/plugins/scorer/queue.go
index c41c1cb..065fcec 100644
--- a/pkg/epp/scheduling/framework/plugins/scorer/queue.go
+++ b/pkg/epp/scheduling/framework/plugins/scorer/queue.go
@@ -28,7 +28,7 @@ import (
 
 const (
 	DefaultQueueScorerWeight = 1
-	QueueScorerType          = "queue"
+	QueueScorerType          = "queue-scorer"
 )
 
 // compile-time type assertion
@@ -42,24 +42,24 @@ func QueueScorerFactory(name string, _ json.RawMessage, _ plugins.Handle) (plugi
 // NewQueueScorer initializes a new QueueScorer and returns its pointer.
 func NewQueueScorer() *QueueScorer {
 	return &QueueScorer{
-		tn: plugins.TypedName{Type: QueueScorerType, Name: QueueScorerType},
+		typedName: plugins.TypedName{Type: QueueScorerType, Name: QueueScorerType},
 	}
 }
 
 // QueueScorer scores list of candidate pods based on the pod's waiting queue size.
 // the less waiting queue size the pod has, the higher score it will get (since it's more available to serve new request).
 type QueueScorer struct {
-	tn plugins.TypedName
+	typedName plugins.TypedName
 }
 
 // TypedName returns the type and name tuple of this plugin instance.
 func (s *QueueScorer) TypedName() plugins.TypedName {
-	return s.tn
+	return s.typedName
 }
 
 // WithName sets the name of the scorer.
 func (s *QueueScorer) WithName(name string) *QueueScorer {
-	s.tn.Name = name
+	s.typedName.Name = name
 	return s
 }
 
diff --git a/pkg/epp/scheduling/framework/scheduler_profile.go b/pkg/epp/scheduling/framework/scheduler_profile.go
index 307f474..c65d201 100644
--- a/pkg/epp/scheduling/framework/scheduler_profile.go
+++ b/pkg/epp/scheduling/framework/scheduler_profile.go
@@ -174,11 +174,11 @@ func (p *SchedulerProfile) runPickerPlugin(ctx context.Context, cycleState *type
 		i++
 	}
 
-	loggerDebug.Info("Before running picker plugin", "pods weighted score", fmt.Sprint(weightedScorePerPod))
+	loggerDebug.Info("Before running picker plugin", "picker", p.picker.TypedName().Type, "pods-weighted-score", fmt.Sprint(weightedScorePerPod))
 	before := time.Now()
 	result := p.picker.Pick(ctx, cycleState, scoredPods)
 	metrics.RecordSchedulerPluginProcessingLatency(PickerPluginType, p.picker.TypedName().Type, time.Since(before))
-	loggerDebug.Info("After running picker plugin", "result", result)
+	loggerDebug.Info("After running picker plugin", "picker", p.picker.TypedName().Type, "result", result)
 
 	return result
 }
diff --git a/pkg/epp/scheduling/scheduler.go b/pkg/epp/scheduling/scheduler.go
index 3edc089..51e188d 100644
--- a/pkg/epp/scheduling/scheduler.go
+++ b/pkg/epp/scheduling/scheduler.go
@@ -25,11 +25,7 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/log"
 	backendmetrics "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend/metrics"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/metrics"
-	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/config"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/framework"
-	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/framework/plugins/filter"
-	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/framework/plugins/picker"
-	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/framework/plugins/profile"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/types"
 	logutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/logging"
 )
@@ -38,46 +34,6 @@ type Datastore interface {
 	PodGetAll() []backendmetrics.PodMetrics
 }
 
-// NewScheduler returns a new scheduler with default scheduler plugins configuration.
-func NewScheduler() *Scheduler {
-	// When the scheduler is initialized with NewScheduler function, thw below config will be used as default.
-	// it's possible to call NewSchedulerWithConfig to pass a different scheduler config.
-	// For build time plugins changes, it's recommended to call in main.go to NewSchedulerWithConfig.
-	loraAffinityFilter := filter.NewLoraAffinityFilter(config.Conf.LoraAffinityThreshold)
-	leastQueueFilter := filter.NewLeastQueueFilter()
-	leastKvCacheFilter := filter.NewLeastKVCacheFilter()
-
-	lowLatencyFilter := &filter.DecisionTreeFilter{
-		Current: filter.NewLowQueueFilter(config.Conf.QueueingThresholdLoRA),
-		NextOnSuccess: &filter.DecisionTreeFilter{
-			Current: loraAffinityFilter,
-			NextOnSuccessOrFailure: &filter.DecisionTreeFilter{
-				Current: leastQueueFilter,
-				NextOnSuccessOrFailure: &filter.DecisionTreeFilter{
-					Current: leastKvCacheFilter,
-				},
-			},
-		},
-		NextOnFailure: &filter.DecisionTreeFilter{
-			Current: leastQueueFilter,
-			NextOnSuccessOrFailure: &filter.DecisionTreeFilter{
-				Current: loraAffinityFilter,
-				NextOnSuccessOrFailure: &filter.DecisionTreeFilter{
-					Current: leastKvCacheFilter,
-				},
-			},
-		},
-	}
-
-	defaultProfile := framework.NewSchedulerProfile().
-		WithFilters(lowLatencyFilter).
-		WithPicker(&picker.RandomPicker{})
-
-	profileHandler := profile.NewSingleProfileHandler()
-
-	return NewSchedulerWithConfig(NewSchedulerConfig(profileHandler, map[string]*framework.SchedulerProfile{"default": defaultProfile}))
-}
-
 // NewSchedulerWithConfig returns a new scheduler with the given scheduler plugins configuration.
 func NewSchedulerWithConfig(config *SchedulerConfig) *Scheduler {
 	return &Scheduler{
@@ -105,18 +61,23 @@ func (s *Scheduler) Schedule(ctx context.Context, request *types.LLMRequest, can
 	cycleState := types.NewCycleState()
 
 	for { // get the next set of profiles to run iteratively based on the request and the previous execution results
+		loggerDebug.Info("Running profile handler, Pick profiles", "plugin", s.profileHandler.TypedName().Type)
 		before := time.Now()
 		profiles := s.profileHandler.Pick(ctx, cycleState, request, s.profiles, profileRunResults)
 		metrics.RecordSchedulerPluginProcessingLatency(framework.ProfilePickerType, s.profileHandler.TypedName().Type, time.Since(before))
+		loggerDebug.Info("After running profile handler Pick profiles", "plugin", s.profileHandler.TypedName().Type, "result", profiles)
 		if len(profiles) == 0 { // profile picker didn't pick any profile to run
 			break
 		}
 
 		for name, profile := range profiles {
+			loggerDebug.Info("Running scheduler profile", "name", name)
 			// run the selected profiles and collect results (current code runs all profiles)
 			profileRunResult, err := profile.Run(ctx, request, cycleState, candidatePods)
 			if err != nil {
 				loggerDebug.Info("failed to run scheduler profile", "profile", name, "error", err.Error())
+			} else {
+				loggerDebug.Info("After running scheduler profile succuessfully", "name", name)
 			}
 
 			profileRunResults[name] = profileRunResult // if profile failed to run, the run result is nil
@@ -124,12 +85,14 @@ func (s *Scheduler) Schedule(ctx context.Context, request *types.LLMRequest, can
 	}
 
 	if len(profileRunResults) == 0 {
-		return nil, fmt.Errorf("failed to run any SchedulingProfile for the request - %s", request)
+		return nil, fmt.Errorf("failed to run any scheduler profile for the request - %s", request)
 	}
 
+	loggerDebug.Info("Running profile handler, ProcessResults", "plugin", s.profileHandler.TypedName().Type)
 	before := time.Now()
 	result, err := s.profileHandler.ProcessResults(ctx, cycleState, request, profileRunResults)
 	metrics.RecordSchedulerPluginProcessingLatency(framework.ProcessProfilesResultsType, s.profileHandler.TypedName().Type, time.Since(before))
+	loggerDebug.Info("After running profile handler ProcessResults", "plugin", s.profileHandler.TypedName().Type)
 
 	return result, err
 }
diff --git a/pkg/epp/scheduling/types/types.go b/pkg/epp/scheduling/types/types.go
index 4513847..2962117 100644
--- a/pkg/epp/scheduling/types/types.go
+++ b/pkg/epp/scheduling/types/types.go
@@ -70,17 +70,9 @@ type PodMetrics struct {
 	*backendmetrics.MetricsState
 }
 
-func ToSchedulerPodMetrics(pods []backendmetrics.PodMetrics) []Pod {
-	pm := make([]Pod, 0, len(pods))
-	for _, pod := range pods {
-		pm = append(pm, &PodMetrics{Pod: pod.GetPod().Clone(), MetricsState: pod.GetMetrics().Clone()})
-	}
-	return pm
-}
-
 // ProfileRunResult captures the profile run result.
 type ProfileRunResult struct {
-	TargetPod Pod
+	TargetPods []Pod
 }
 
 // SchedulingResult captures the result of the scheduling cycle.
diff --git a/pkg/epp/server/runserver.go b/pkg/epp/server/runserver.go
index 7b79ae9..10da323 100644
--- a/pkg/epp/server/runserver.go
+++ b/pkg/epp/server/runserver.go
@@ -20,7 +20,6 @@ import (
 	"context"
 	"crypto/tls"
 	"fmt"
-
 	"time"
 
 	extProcPb "github.com/envoyproxy/go-control-plane/envoy/service/ext_proc/v3"
@@ -63,6 +62,8 @@ type ExtProcServerRunner struct {
 // Default values for CLI flags in main
 const (
 	DefaultGrpcPort                                 = 9002                             // default for --grpcPort
+	DefaultGrpcHealthPort                           = 9003                             // default for --grpcHealthPort
+	DefaultMetricsPort                              = 9090                             // default for --metricsPort
 	DefaultDestinationEndpointHintMetadataNamespace = "envoy.lb"                       // default for --destinationEndpointHintMetadataNamespace
 	DefaultDestinationEndpointHintKey               = "x-gateway-destination-endpoint" // default for --destinationEndpointHintKey
 	DefaultPoolName                                 = ""                               // required but no default
@@ -71,6 +72,13 @@ const (
 	DefaultRefreshPrometheusMetricsInterval         = 5 * time.Second                  // default for --refreshPrometheusMetricsInterval
 	DefaultSecureServing                            = true                             // default for --secureServing
 	DefaultHealthChecking                           = false                            // default for --healthChecking
+	DefaultEnablePprof                              = true                             // default for --enablePprof
+	DefaultTotalQueuedRequestsMetric                = "vllm:num_requests_waiting"      // default for --totalQueuedRequestsMetric
+	DefaultKvCacheUsagePercentageMetric             = "vllm:gpu_cache_usage_perc"      // default for --kvCacheUsagePercentageMetric
+	DefaultLoraInfoMetric                           = "vllm:lora_requests_info"        // default for --loraInfoMetric
+	DefaultCertPath                                 = ""                               // default for --certPath
+	DefaultConfigFile                               = ""                               // default for --configFile
+	DefaultConfigText                               = ""                               // default for --configText
 )
 
 // NewDefaultExtProcServerRunner creates a runner with default values.
diff --git a/site-src/guides/epp-configuration/config-text.md b/site-src/guides/epp-configuration/config-text.md
new file mode 100644
index 0000000..8b334ac
--- /dev/null
+++ b/site-src/guides/epp-configuration/config-text.md
@@ -0,0 +1,255 @@
+# Configuring Plugins via text
+
+The set of lifecycle hooks (plugins) that are used by the Inference Gateway (IGW) is determined by how 
+it is configured. The IGW can be configured in several ways, either by code or via text.
+
+If configured by code either a set of predetermined environment variables must be used or one must
+fork the IGW and change code.
+
+A simpler way to congigure the IGW is to use a text based configuration. This text is in YAML format
+and can either be in a file or specified in-line as a parameter. The configuration defines the set of
+plugins to be instantiated along with their parameters. Each plugin can also be given a name, enabling
+the same plugin type to be instantiated multiple times, if needed. Also defined is a set of
+SchedulingProfiles, which determine the set of plugins to be used when scheduling a request. The set
+of plugins instantiated must also include a Profile Handler, which determines which SchedulingProfiles
+will be used for a particular request.
+
+It should be noted that while the configuration text looks like a Kubernetes Custom Resource, it is
+**NOT** a Kubernetes Custom Resource. Kubernetes infrastructure is used to load the configuration
+text and in the future will also help in versioning the text.
+
+It should also be noted that even when the configuration text is loaded from a file, it is loaded at
+the Endpoint-Picker's (EPP) startup and changes to the file at runtime are ignored.
+
+The configuration text has the following form:
+```yaml
+apiVersion: inference.networking.x-k8s.io/v1alpha1
+kind: EndpointPickerConfig
+plugins:
+- ....
+- ....
+schedulingProfiles:
+- ....
+- ....
+```
+
+The first two lines of the configuration are constant and must appear as is.
+
+The plugins section defines the set of plugins that will be instantiated and their parameters.
+Each entry in this section has the following form:
+
+```yaml
+- name: aName
+  type: a-type
+  parameters:
+    parm1: val1
+    parm2: val2
+```
+
+The fields in a plugin entry are:
+
+- *name* which is optional, provides a name by which the plugin instance can be referenced. If this
+field is omitted, the plugin's type will be used as its name.
+- *type* specifies the type of the plugin to be instantiated.
+- *parameters* which is optional, defines the set of parameters used to configure the plugin in question.
+The actual set of parameters varies from plugin to plugin.
+
+The schedulingProfiles section defines the set of scheduling profiles that can be used in scheduling
+requests to pods. The number of scheduling profiles one defines, depends on the use case. For simple
+serving of requests, one is enough. For disaggregated prefill, two profiles are required. Each entry
+in this section has the following form:
+
+```yaml
+- name: aName
+  plugins:
+  - pluginRef: plugin1
+  - pluginRef: plugin2
+    weight: 50
+```
+
+The fields in a schedulingProfile entry are:
+
+- *name* specifies the scheduling profile's name.
+- *plugins* specifies the set of plugins to be used when this scheduling profile is chosen for a request.
+Each entry in the schedulingProfile's plugins section has the following fields:
+  - *pluginRef* is a reference to the name of the plugin instance to be used
+  - *weight* is the weight to be used if the referenced plugin is a scorer.
+
+A complete configuration might look like this:
+```yaml
+apiVersion: inference.networking.x-k8s.io/v1alpha1
+kind: EndpointPickerConfig
+plugins:
+- type: prefix-cache-scorer
+  parameters:
+    hashBlockSize: 5
+    maxPrefixBlocksToMatch: 256
+    lruCapacityPerServer: 31250
+- type: max-score-picker
+- type: single-profile-handler
+schedulingProfiles:
+- name: default
+  plugins:
+  - pluginRef: max-score-picker
+  - pluginRef: prefix-cache-scorer
+    weight: 50
+```
+
+If the configuration is in a file, the EPP command line argument `--configFile`
+should be used to specify the full path of the file in question. For example:
+
+```yaml
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: ${EPP_NAME}
+  ...
+spec:
+  ...
+  template:
+    ...
+    spec:
+      ...
+      containers:
+      - name: epp
+        image: ghcr.io/llm-d/llm-d-inference-scheduler:latest
+        imagePullPolicy: IfNotPresent
+        args:
+        - -poolName
+        - "${POOL_NAME}"
+        ...
+        - --configFile
+        - "/etc/epp/epp-config.yaml"
+```
+
+If the configuration is passed as in-line text the EPP command line argument `--configText`
+should be used. For example:
+
+```yaml
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: ${EPP_NAME}
+  ...
+spec:
+  ...
+  template:
+    ...
+    spec:
+      ...
+      containers:
+      - name: epp
+        image: ghcr.io/llm-d/llm-d-inference-scheduler:latest
+        imagePullPolicy: IfNotPresent
+        args:
+        - -poolName
+        - "${POOL_NAME}"
+        ...
+        - --configText
+        - |
+          apiVersion: inference.networking.x-k8s.io/v1alpha1
+          kind: EndpointPickerConfig
+          plugins:
+          - type: prefix-cache-scorer
+            parameters:
+              hashBlockSize: 5
+              maxPrefixBlocksToMatch: 256
+              lruCapacityPerServer: 31250
+          - type: max-score-picker
+          - type: single-profile-handler
+          schedulingProfiles:
+          - name: default
+            plugins:
+            - pluginRef: max-score-picker
+            - pluginRef: prefix-cache-scorer
+              weight: 50
+```
+
+## Plugin Configuration
+
+This section describes how to setup the various plugins that are available with the IGW.
+
+#### **SingleProfileHandler**
+
+Selects a single profile which is always the primary profile.
+
+- *Type*: single-profile-handler
+- *Parameters*: none
+
+#### **LeastKVCacheFilter**
+
+Finds the max and min KV cache of all pods, divides the whole range (max-min) by the
+number of pods, and finds the pods that fall into the first range.
+
+- *Type*: least-kv-cache-filter
+- *Parameters*: none
+
+#### **LeastQueueFilter**
+
+Finds the max and min queue size of all pods, divides the whole range (max-min) by the
+number of pods, and finds the pods that fall into the first range.
+
+- *Type*: least-queue-filter
+- *Parameters*: none
+
+#### **LoraAffinityFilter**
+
+Implements a pod selection strategy that when the use of a LoRA adapter is requested, prioritizes pods 
+that are believed to have the specific LoRA adapter loaded. It also allows for load balancing through
+some randomization.
+
+- *Type*: lora-affinity-filter
+- *Parameters*:
+  - `threshold` a probability threshold to sometimes select pods that don't seem to have the LoRA
+    adapter loaded to enable load balancing. If not specified defaults to `0.999`
+
+#### **LowQueueFilter**
+
+Filters out pods who's waiting queue size is greater than the specified theshold.
+
+- *Type*: low-queue-filter
+- *Parameters*:
+  - `threshold` the waiting queue threshold. If not specified defaults to `128`
+
+#### **PrefixCachePlugin**
+
+Scores pods based on the amount of the prompt is believed to be in the pod's KvCache.
+
+- *Type*: prefix-cache-scorer
+- *Parameters*:
+  - `hashBlockSize` specified the size of the blocks to break up the input prompt when
+    calculating the block hashes. If not specified defaults to `64`
+  - `maxPrefixBlocksToMatch` specifies the maximum number of prefix blocks to match. If
+   not specified defaults to `256`
+  - `lruCapacityPerServer` specifies the capacity of the LRU indexer in number of entries
+    per server (pod). If not specified defaults to `31250`
+
+#### **MaxScorePicker**
+
+Picks the pod with the maximum score from the list of candidates.
+
+- *Type*: max-score-picker
+- *Parameters*: none
+
+#### **RandomPicker**
+
+Picks a random pod from the list of candidates.
+
+- *Type*: random-picker
+- *Parameters*: none
+
+#### **KvCacheScorer**
+
+Scores the candidate pods based on their KV cache utilization.
+
+- *Type*: kv-cache-scorer
+- *Parameters*: none
+
+#### **QueueScorer**
+
+Scores list of candidate pods based on the pod's waiting queue size. The lower the
+waiting queue size the pod has, the higher the score it will get (since it's more
+available to serve new request).
+
+- *Type*: queue-scorer
+- *Parameters*: none
\ No newline at end of file
diff --git a/site-src/guides/inferencepool-rollout.md b/site-src/guides/inferencepool-rollout.md
index 89a384a..809fb7f 100644
--- a/site-src/guides/inferencepool-rollout.md
+++ b/site-src/guides/inferencepool-rollout.md
@@ -177,7 +177,6 @@ spec:
       terminationGracePeriodSeconds: 130
       nodeSelector:
         cloud.google.com/gke-accelerator: "nvidia-h100-80gb"
-
       volumes:
         - name: data
           emptyDir: {}
@@ -250,40 +249,133 @@ spec:
     spec:
       terminationGracePeriodSeconds: 130
       containers:
-        - name: epp
-          image: us-central1-docker.pkg.dev/k8s-staging-images/gateway-api-inference-extension/epp:main
-          imagePullPolicy: Always
-          args:
-            - -poolName
-            - "vllm-llama3-8b-instruct-new"
-            - "-poolNamespace"
-            - "default"
-            - -v
-            - "4"
-            - --zap-encoder
-            - "json"
-            - -grpcPort
-            - "9002"
-            - -grpcHealthPort
-            - "9003"
-          ports:
-            - containerPort: 9002
-            - containerPort: 9003
-            - name: metrics
-              containerPort: 9090
-          livenessProbe:
-            grpc:
-              port: 9003
-              service: inference-extension
-            initialDelaySeconds: 5
-            periodSeconds: 10
-          readinessProbe:
-            grpc:
-              port: 9003
-              service: inference-extension
-            initialDelaySeconds: 5
-            periodSeconds: 10
-  EOF
+      - name: epp
+        image: us-central1-docker.pkg.dev/k8s-staging-images/gateway-api-inference-extension/epp:main
+        imagePullPolicy: Always
+        args:
+        - -poolName
+        - "vllm-llama3-8b-instruct-new"
+        - -poolNamespace
+        - "default"
+        - -v
+        - "4"
+        - --zap-encoder
+        - "json"
+        - -grpcPort
+        - "9002"
+        - -grpcHealthPort
+        - "9003"
+        - -configFile
+        - "/config/default-plugins.yaml"
+        ports:
+        - containerPort: 9002
+          name: grpc
+        - containerPort: 9003
+          name: grpc-health
+        - containerPort: 9090
+          name: metrics
+        livenessProbe:
+          grpc:
+            port: 9003
+            service: inference-extension
+          initialDelaySeconds: 5
+          periodSeconds: 10
+        readinessProbe:
+          grpc:
+            port: 9003
+            service: inference-extension
+          initialDelaySeconds: 5
+          periodSeconds: 10
+        volumeMounts:
+        - name: plugins-config-volume
+          mountPath: /config
+      volumes:
+      - name: plugins-config-volume
+        configMap:
+          name: plugins-config
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: plugins-config
+  namespace: default
+data:
+  default-plugins.yaml: |
+    apiVersion: inference.networking.x-k8s.io/v1alpha1
+    kind: EndpointPickerConfig
+    plugins:
+    - type: low-queue-filter
+      parameters:
+        threshold: 128
+    - type: lora-affinity-filter
+      parameters:
+        threshold: 0.999
+    - type: least-queue-filter
+    - type: least-kv-cache-filter
+    - type: decision-tree-filter
+      name: low-latency-filter
+      parameters:
+        current:
+          pluginRef: low-queue-filter
+        nextOnSuccess:
+          decisionTree:
+            current:
+              pluginRef: lora-affinity-filter
+            nextOnSuccessOrFailure:
+              decisionTree:
+                current:
+                  pluginRef: least-queue-filter
+                nextOnSuccessOrFailure:
+                  decisionTree:
+                    current:
+                      pluginRef: least-kv-cache-filter
+        nextOnFailure:
+          decisionTree:
+            current:
+              pluginRef: least-queue-filter
+            nextOnSuccessOrFailure:
+              decisionTree:
+                current:
+                  pluginRef: lora-affinity-filter
+                nextOnSuccessOrFailure:
+                  decisionTree:
+                    current:
+                      pluginRef: least-kv-cache-filter
+    - type: random-picker
+      parameters:
+        maxNumOfEndpoints: 1
+    - type: single-profile-handler
+    schedulingProfiles:
+    - name: default
+      plugins:
+      - pluginRef: low-latency-filter
+      - pluginRef: random-picker
+  plugins-v2.yaml: |
+    apiVersion: inference.networking.x-k8s.io/v1alpha1
+    kind: EndpointPickerConfig
+    plugins:
+    - type: queue-scorer
+    - type: kv-cache-scorer
+    - type: prefix-cache-scorer
+      parameters:
+        hashBlockSize: 64
+        maxPrefixBlocksToMatch: 256
+        lruCapacityPerServer: 31250
+    - type: max-score-picker
+      parameters:
+        maxNumOfEndpoints: 1
+    - type: single-profile-handler
+    schedulingProfiles:
+    - name: default
+      plugins:
+      - pluginRef: queue-scorer
+        weight: 1
+      - pluginRef: kv-cache-scorer
+        weight: 1
+      - pluginRef: prefix-cache-scorer
+        weight: 1
+      - pluginRef: max-score-picker
+EOF
 ```
 
 ### Direct traffic to the new inference pool
diff --git a/version/version.go b/version/version.go
new file mode 100644
index 0000000..1da42f2
--- /dev/null
+++ b/version/version.go
@@ -0,0 +1,22 @@
+/*
+Copyright 2025 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 version
+
+const (
+	// BundleVersion is the value used for labeling the version of the gateway-api-inference-extension.
+	BundleVersion = "v0.4.0-dev"
+)



Test patch:
diff --git a/conformance/testing-epp/scheduler.go b/conformance/testing-epp/scheduler.go
deleted file mode 100644
index 94f9ee5..0000000
--- a/conformance/testing-epp/scheduler.go
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
-Copyright 2025 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 scheduling
-
-import (
-	"sigs.k8s.io/gateway-api-inference-extension/conformance/testing-epp/plugins/filter"
-	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling"
-	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/framework"
-	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/framework/plugins/picker"
-	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/framework/plugins/profile"
-)
-
-// NewReqHeaderBasedScheduler creates a scheduler for conformance tests that selects
-// an endpoint based on the "test-epp-endpoint-selection" request header. If the
-// header is missing or the specified endpoint doesn't exist, no endpoint is returned.
-func NewReqHeaderBasedScheduler() *scheduling.Scheduler {
-	predicatableSchedulerProfile := framework.NewSchedulerProfile().
-		WithFilters(filter.NewHeaderBasedTestingFilter()).
-		WithPicker(picker.NewMaxScorePicker())
-
-	return scheduling.NewSchedulerWithConfig(scheduling.NewSchedulerConfig(
-		profile.NewSingleProfileHandler(), map[string]*framework.SchedulerProfile{"req-header-based-profile": predicatableSchedulerProfile}))
-}
diff --git a/conformance/testing-epp/scheduler_test.go b/conformance/testing-epp/scheduler_test.go
deleted file mode 100644
index 4901e03..0000000
--- a/conformance/testing-epp/scheduler_test.go
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
-Copyright 2025 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 scheduling
-
-import (
-	"context"
-	"testing"
-
-	"github.com/google/go-cmp/cmp"
-	"github.com/google/uuid"
-	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend"
-	backendmetrics "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend/metrics" // Import config for thresholds
-	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/types"
-)
-
-// Tests the scheduler for conformance tests.
-func TestSchedule(t *testing.T) {
-	tests := []struct {
-		name    string
-		input   []backendmetrics.PodMetrics
-		req     *types.LLMRequest
-		wantRes *types.SchedulingResult
-		err     bool
-	}{
-		{
-			name: "no candidate pods and req header is set",
-			req: &types.LLMRequest{
-				Headers:   map[string]string{"test-epp-endpoint-selection": "random-endpoint"},
-				RequestId: uuid.NewString(),
-			},
-			wantRes: nil,
-			err:     true,
-		},
-		{
-			name: "req header not set",
-			input: []backendmetrics.PodMetrics{
-				&backendmetrics.FakePodMetrics{Pod: &backend.Pod{Address: "random-endpoint"}},
-			},
-			req: &types.LLMRequest{
-				Headers:   map[string]string{}, // Deliberately set an empty header.
-				RequestId: uuid.NewString(),
-			},
-			wantRes: nil,
-			err:     true,
-		},
-		{
-			name: "no pods address from the candidate pods matches req header address",
-			input: []backendmetrics.PodMetrics{
-				&backendmetrics.FakePodMetrics{Pod: &backend.Pod{Address: "nonmatched-endpoint"}},
-			},
-			req: &types.LLMRequest{
-				Headers:   map[string]string{"test-epp-endpoint-selection": "matched-endpoint"},
-				RequestId: uuid.NewString(),
-			},
-			wantRes: nil,
-			err:     true,
-		},
-		{
-			name: "one pod address from the candidate pods matches req header address",
-			input: []backendmetrics.PodMetrics{
-				&backendmetrics.FakePodMetrics{Pod: &backend.Pod{Address: "nonmatched-endpoint"}},
-				&backendmetrics.FakePodMetrics{Pod: &backend.Pod{Address: "matched-endpoint"}},
-			},
-			req: &types.LLMRequest{
-				Headers:   map[string]string{"test-epp-endpoint-selection": "matched-endpoint"},
-				RequestId: uuid.NewString(),
-			},
-			wantRes: &types.SchedulingResult{
-				ProfileResults: map[string]*types.ProfileRunResult{
-					"req-header-based-profile": {
-						TargetPod: &types.ScoredPod{
-							Pod: &types.PodMetrics{
-								Pod: &backend.Pod{
-									Address: "matched-endpoint",
-									Labels:  map[string]string{},
-								},
-							},
-						},
-					},
-				},
-				PrimaryProfileName: "req-header-based-profile",
-			},
-		},
-	}
-
-	for _, test := range tests {
-		t.Run(test.name, func(t *testing.T) {
-			scheduler := NewReqHeaderBasedScheduler()
-			got, err := scheduler.Schedule(context.Background(), test.req, types.ToSchedulerPodMetrics(test.input))
-			if test.err != (err != nil) {
-				t.Errorf("Unexpected error, got %v, want %v", err, test.err)
-			}
-
-			if diff := cmp.Diff(test.wantRes, got); diff != "" {
-				t.Errorf("Unexpected output (-want +got): %v", diff)
-			}
-		})
-	}
-}
diff --git a/conformance/tests/basic/inferencepool_invalid_epp_service.go b/conformance/tests/basic/inferencepool_invalid_epp_service.go
index 7e3bfae..a9c3059 100644
--- a/conformance/tests/basic/inferencepool_invalid_epp_service.go
+++ b/conformance/tests/basic/inferencepool_invalid_epp_service.go
@@ -22,6 +22,7 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/types"
 	inferenceapi "sigs.k8s.io/gateway-api-inference-extension/api/v1alpha2"
+	gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
 	"sigs.k8s.io/gateway-api/conformance/utils/kubernetes"
 	"sigs.k8s.io/gateway-api/conformance/utils/suite"
 	"sigs.k8s.io/gateway-api/pkg/features"
@@ -56,7 +57,13 @@ var InferencePoolInvalidEPPService = suite.ConformanceTest{
 		gwNN := types.NamespacedName{Name: "conformance-primary-gateway", Namespace: infraNamespace}
 		poolNN := types.NamespacedName{Name: poolName, Namespace: appNamespace}
 
-		gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, s.Client, s.TimeoutConfig, s.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN)
+		gwAddr := k8sutils.GetGatewayEndpoint(t, s.Client, s.TimeoutConfig, gwNN)
+		acceptedCondition := metav1.Condition{
+			Type:   string(gatewayv1.RouteConditionAccepted),
+			Status: metav1.ConditionTrue,
+			Reason: string(gatewayv1.RouteReasonAccepted),
+		}
+		kubernetes.HTTPRouteMustHaveCondition(t, s.Client, s.TimeoutConfig, routeNN, gwNN, acceptedCondition)
 		t.Run("InferecePool has a ResolvedRefs Condition with status False", func(t *testing.T) {
 			acceptedCondition := metav1.Condition{
 				Type:   string(inferenceapi.InferencePoolConditionResolvedRefs), // Standard condition type
diff --git a/pkg/epp/backend/metrics/metrics_test.go b/pkg/epp/backend/metrics/metrics_test.go
index bfc3e01..9f7c2b8 100644
--- a/pkg/epp/backend/metrics/metrics_test.go
+++ b/pkg/epp/backend/metrics/metrics_test.go
@@ -495,9 +495,9 @@ func TestFetchMetrics(t *testing.T) {
 		},
 	}
 	existing := &MetricsState{}
-	p := &PodMetricsClientImpl{} // No MetricMapping needed for this basic test
+	p := &PodMetricsClientImpl{ModelServerMetricsPort: 9999, ModelServerMetricsPath: "/metrics"} // No MetricMapping needed for this basic test
 
-	_, err := p.FetchMetrics(ctx, pod, existing, 9999) // Use a port that's unlikely to be in use.
+	_, err := p.FetchMetrics(ctx, pod, existing, 9999) // Use a port that's unlikely to be in use
 	if err == nil {
 		t.Errorf("FetchMetrics() expected error, got nil")
 	}
diff --git a/pkg/epp/common/config/loader/configloader_test.go b/pkg/epp/common/config/loader/configloader_test.go
index 1be93ed..233f76a 100644
--- a/pkg/epp/common/config/loader/configloader_test.go
+++ b/pkg/epp/common/config/loader/configloader_test.go
@@ -19,10 +19,12 @@ package loader
 import (
 	"context"
 	"encoding/json"
+	"os"
 	"testing"
 
 	"github.com/google/go-cmp/cmp"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/utils/ptr"
 
 	configapi "sigs.k8s.io/gateway-api-inference-extension/api/config/v1alpha1"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/plugins"
@@ -43,8 +45,6 @@ const (
 )
 
 func TestLoadConfiguration(t *testing.T) {
-	test2Weight := 50
-
 	registerTestPlugins()
 
 	goodConfig := &configapi.EndpointPickerConfig{
@@ -81,7 +81,7 @@ func TestLoadConfiguration(t *testing.T) {
 					},
 					{
 						PluginRef: "test-two",
-						Weight:    &test2Weight,
+						Weight:    ptr.To(50),
 					},
 					{
 						PluginRef: "testPicker",
@@ -178,19 +178,18 @@ func TestLoadConfiguration(t *testing.T) {
 			want:       goodConfig,
 			wantErr:    false,
 		},
-		{
-			name:       "noSuchFile",
-			configText: "",
-			configFile: "../../../../../test/testdata/configloader_error_test.yaml",
-			wantErr:    true,
-		},
 	}
 
 	for _, test := range tests {
-		got, err := LoadConfig([]byte(test.configText), test.configFile)
+		configBytes := []byte(test.configText)
+		if test.configFile != "" {
+			configBytes, _ = os.ReadFile(test.configFile)
+		}
+
+		got, err := LoadConfig(configBytes, utils.NewTestHandle(context.Background()))
 		if err != nil {
 			if !test.wantErr {
-				t.Fatalf("In test %s LoadConfig returned unexpected error: %v, want %v", test.name, err, test.wantErr)
+				t.Fatalf("In test '%s' LoadConfig returned unexpected error: %v, want %v", test.name, err, test.wantErr)
 			}
 			t.Logf("error was %s", err)
 		} else {
@@ -204,41 +203,25 @@ func TestLoadConfiguration(t *testing.T) {
 	}
 }
 
-func TestLoadPluginReferences(t *testing.T) {
-	ctx := context.Background()
-	theConfig, err := LoadConfig([]byte(successConfigText), "")
+func TestInstantiatePlugins(t *testing.T) {
+	handle := utils.NewTestHandle(context.Background())
+	_, err := LoadConfig([]byte(successConfigText), handle)
 	if err != nil {
-		t.Fatalf("LoadConfig returned unexpected error: %v", err)
+		t.Fatalf("LoadConfig returned unexpected error - %v", err)
 	}
-	handle := utils.NewTestHandle(ctx)
-	err = LoadPluginReferences(theConfig.Plugins, handle)
-	if err != nil {
-		t.Fatalf("LoadPluginReferences returned unexpected error: %v", err)
-	}
-	if len(handle.Plugins().GetAllPlugins()) == 0 {
-		t.Fatalf("LoadPluginReferences returned an empty set of references")
+	if len(handle.GetAllPlugins()) == 0 {
+		t.Fatalf("unexpected empty set of loaded plugins")
 	}
-	if t1 := handle.Plugins().Plugin("test1"); t1 == nil {
-		t.Fatalf("LoadPluginReferences returned references did not contain test1")
+	if t1 := handle.Plugin("test1"); t1 == nil {
+		t.Fatalf("loaded plugins did not contain test1")
 	} else if _, ok := t1.(*test1); !ok {
-		t.Fatalf("LoadPluginReferences returned references value for test1 has the wrong type %#v", t1)
+		t.Fatalf("loaded plugins returned test1 has the wrong type %#v", t1)
 	}
 
-	theConfig, err = LoadConfig([]byte(errorBadPluginReferenceParametersText), "")
-	if err != nil {
-		t.Fatalf("LoadConfig returned unexpected error: %v", err)
-	}
-	err = LoadPluginReferences(theConfig.Plugins, utils.NewTestHandle(ctx))
+	handle = utils.NewTestHandle(context.Background())
+	_, err = LoadConfig([]byte(errorBadPluginReferenceParametersText), handle)
 	if err == nil {
-		t.Fatalf("LoadPluginReferences did not return the expected error")
-	}
-}
-
-func TestInstantiatePlugin(t *testing.T) {
-	plugSpec := configapi.PluginSpec{Type: "plover"}
-	_, err := instantiatePlugin(plugSpec, utils.NewTestHandle(context.Background()))
-	if err == nil {
-		t.Fatalf("InstantiatePlugin did not return the expected error")
+		t.Fatalf("LoadConfig did not return error as expected ")
 	}
 }
 
@@ -287,26 +270,17 @@ func TestLoadSchedulerConfig(t *testing.T) {
 
 	registerNeededPlgugins()
 
-	ctx := context.Background()
-
 	for _, test := range tests {
-		theConfig, err := LoadConfig([]byte(test.configText), "")
+		handle := utils.NewTestHandle(context.Background())
+		config, err := LoadConfig([]byte(test.configText), handle)
 		if err != nil {
 			if test.wantErr {
 				continue
 			}
 			t.Fatalf("LoadConfig returned unexpected error: %v", err)
 		}
-		handle := utils.NewTestHandle(ctx)
-		err = LoadPluginReferences(theConfig.Plugins, handle)
-		if err != nil {
-			if test.wantErr {
-				continue
-			}
-			t.Fatalf("LoadPluginReferences returned unexpected error: %v", err)
-		}
 
-		_, err = LoadSchedulerConfig(theConfig.SchedulingProfiles, handle)
+		_, err = LoadSchedulerConfig(config.SchedulingProfiles, handle)
 		if err != nil {
 			if !test.wantErr {
 				t.Errorf("LoadSchedulerConfig returned an unexpected error. error %v", err)
@@ -555,18 +529,18 @@ schedulingProfiles:
 var _ framework.Filter = &test1{}
 
 type test1 struct {
-	tn        plugins.TypedName
+	typedName plugins.TypedName
 	Threshold int `json:"threshold"`
 }
 
 func newTest1() *test1 {
 	return &test1{
-		tn: plugins.TypedName{Type: test1Type, Name: "test-1"},
+		typedName: plugins.TypedName{Type: test1Type, Name: "test-1"},
 	}
 }
 
 func (f *test1) TypedName() plugins.TypedName {
-	return f.tn
+	return f.typedName
 }
 
 // Filter filters out pods that doesn't meet the filter criteria.
@@ -579,17 +553,17 @@ var _ framework.Scorer = &test2{}
 var _ framework.PostCycle = &test2{}
 
 type test2 struct {
-	tn plugins.TypedName
+	typedName plugins.TypedName
 }
 
 func newTest2() *test2 {
 	return &test2{
-		tn: plugins.TypedName{Type: test2Type, Name: "test-2"},
+		typedName: plugins.TypedName{Type: test2Type, Name: "test-2"},
 	}
 }
 
 func (m *test2) TypedName() plugins.TypedName {
-	return m.tn
+	return m.typedName
 }
 
 func (m *test2) Score(_ context.Context, _ *types.CycleState, _ *types.LLMRequest, _ []types.Pod) map[types.Pod]float64 {
@@ -602,17 +576,17 @@ func (m *test2) PostCycle(_ context.Context, _ *types.CycleState, _ *types.Profi
 var _ framework.Picker = &testPicker{}
 
 type testPicker struct {
-	tn plugins.TypedName
+	typedName plugins.TypedName
 }
 
 func newTestPicker() *testPicker {
 	return &testPicker{
-		tn: plugins.TypedName{Type: testPickerType, Name: "test-picker"},
+		typedName: plugins.TypedName{Type: testPickerType, Name: "test-picker"},
 	}
 }
 
 func (p *testPicker) TypedName() plugins.TypedName {
-	return p.tn
+	return p.typedName
 }
 
 func (p *testPicker) Pick(_ context.Context, _ *types.CycleState, _ []*types.ScoredPod) *types.ProfileRunResult {
@@ -623,17 +597,17 @@ func (p *testPicker) Pick(_ context.Context, _ *types.CycleState, _ []*types.Sco
 var _ framework.ProfileHandler = &testProfileHandler{}
 
 type testProfileHandler struct {
-	tn plugins.TypedName
+	typedName plugins.TypedName
 }
 
 func newTestProfileHandler() *testProfileHandler {
 	return &testProfileHandler{
-		tn: plugins.TypedName{Type: testProfileHandlerType, Name: "test-profile-handler"},
+		typedName: plugins.TypedName{Type: testProfileHandlerType, Name: "test-profile-handler"},
 	}
 }
 
 func (p *testProfileHandler) TypedName() plugins.TypedName {
-	return p.tn
+	return p.typedName
 }
 
 func (p *testProfileHandler) Pick(_ context.Context, _ *types.CycleState, _ *types.LLMRequest, _ map[string]*framework.SchedulerProfile, _ map[string]*types.ProfileRunResult) map[string]*framework.SchedulerProfile {
@@ -679,25 +653,25 @@ const successSchedulerConfigText = `
 apiVersion: inference.networking.x-k8s.io/v1alpha1
 kind: EndpointPickerConfig
 plugins:
-- name: lowQueue
-  type: low-queue
+- name: lowQueueFilter
+  type: low-queue-filter
   parameters:
     threshold: 10
-- name: prefixCache
-  type: prefix-cache
+- name: prefixCacheScorer
+  type: prefix-cache-scorer
   parameters:
     hashBlockSize: 32
-- name: maxScore
-  type: max-score
+- name: maxScorePicker
+  type: max-score-picker
 - name: profileHandler
-  type: single-profile
+  type: single-profile-handler
 schedulingProfiles:
 - name: default
   plugins:
-  - pluginRef: lowQueue
-  - pluginRef: prefixCache
+  - pluginRef: lowQueueFilter
+  - pluginRef: prefixCacheScorer
     weight: 50
-  - pluginRef: maxScore
+  - pluginRef: maxScorePicker
 `
 
 // invalid parameter configuration for plugin (string passed, in expected)
@@ -709,14 +683,14 @@ kind: EndpointPickerConfig
 plugins:
 - name:profileHandler
   type: single-profile
-- name: prefixCache
-  type: prefix-cache
+- name: prefixCacheScorer
+  type: prefix-cache-scorer
   parameters:
     hashBlockSize: asdf
 schedulingProfiles:
 - name: default
   plugins:
-  - pluginRef: prefixCache
+  - pluginRef: prefixCacheScorer
     weight: 50
 `
 
@@ -729,14 +703,14 @@ kind: EndpointPickerConfig
 plugins:
 - name: profileHandler
   type: single-profile
-- name: prefixCache
-  type: prefix-cache
+- name: prefixCacheScorer
+  type: prefix-cache-scorer
   parameters:
     hashBlockSize: 32
 schedulingProfiles:
 - name: default
   plugins:
-  - pluginRef: prefixCache
+  - pluginRef: prefixCacheScorer
 `
 
 // multiple pickers in scheduling profile
@@ -766,8 +740,8 @@ const errorConfigText = `
 apiVersion: inference.networking.x-k8s.io/v1alpha1
 kind: EndpointPickerConfig
 plugins:
-- name: lowQueue
-  pluginName: low-queue
+- name: lowQueueFilter
+  pluginName: low-queue-filter
   parameters:
     threshold: 10
 `
diff --git a/pkg/epp/flowcontrol/framework/plugins/policies/intraflow/dispatch/fcfs/fcfs_test.go b/pkg/epp/flowcontrol/framework/plugins/policies/intraflow/dispatch/fcfs/fcfs_test.go
new file mode 100644
index 0000000..8a13c6c
--- /dev/null
+++ b/pkg/epp/flowcontrol/framework/plugins/policies/intraflow/dispatch/fcfs/fcfs_test.go
@@ -0,0 +1,106 @@
+/*
+Copyright 2025 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 fcfs
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/framework"
+	frameworkmocks "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/framework/mocks"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/types"
+	typesmocks "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/types/mocks"
+)
+
+func TestFCFS_Name(t *testing.T) {
+	t.Parallel()
+	policy := newFCFS()
+	assert.Equal(t, FCFSPolicyName, policy.Name())
+}
+
+func TestFCFS_RequiredQueueCapabilities(t *testing.T) {
+	t.Parallel()
+	policy := newFCFS()
+	caps := policy.RequiredQueueCapabilities()
+	require.Len(t, caps, 1, "RequiredQueueCapabilities should return one capability")
+	assert.Equal(t, framework.CapabilityFIFO, caps[0], "Required capability should be FIFO")
+}
+
+func TestFCFS_SelectItem(t *testing.T) {
+	t.Parallel()
+	// Note: The conformance suite validates the policy's contract for nil and empty queues.
+	// This unit test focuses on the policy-specific success path.
+	policy := newFCFS()
+
+	mockItem := typesmocks.NewMockQueueItemAccessor(1, "item1", "flow1")
+	mockQueue := &frameworkmocks.MockFlowQueueAccessor{
+		PeekHeadV: mockItem,
+		LenV:      1,
+	}
+
+	item, err := policy.SelectItem(mockQueue)
+	require.NoError(t, err)
+	assert.Equal(t, mockItem, item, "Should return the item from the head of the queue")
+}
+
+func TestEnqueueTimeComparator_Func(t *testing.T) {
+	t.Parallel()
+	comparator := &enqueueTimeComparator{} // Test the internal comparator directly
+	compareFunc := comparator.Func()
+	require.NotNil(t, compareFunc)
+
+	now := time.Now()
+	itemA := typesmocks.NewMockQueueItemAccessor(10, "itemA", "test-flow")
+	itemA.EnqueueTimeV = now
+
+	itemB := typesmocks.NewMockQueueItemAccessor(20, "itemB", "test-flow")
+	itemB.EnqueueTimeV = now.Add(time.Second) // B is later than A
+
+	itemC := typesmocks.NewMockQueueItemAccessor(30, "itemC", "test-flow")
+	itemC.EnqueueTimeV = now // C is same time as A
+
+	testCases := []struct {
+		name     string
+		item1    types.QueueItemAccessor
+		item2    types.QueueItemAccessor
+		expected bool // true if item1 is higher priority (earlier) than item2
+	}{
+		{"A before B", itemA, itemB, true},
+		{"B after A", itemB, itemA, false},
+		{"A same as C (A not strictly before C)", itemA, itemC, false},
+		{"C same as A (C not strictly before A)", itemC, itemA, false},
+		{"A vs nil B (A is preferred)", itemA, nil, true},
+		{"nil A vs B (B is preferred)", nil, itemB, false},
+		{"nil A vs nil B (no preference)", nil, nil, false},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+			assert.Equal(t, tc.expected, compareFunc(tc.item1, tc.item2))
+		})
+	}
+}
+
+func TestEnqueueTimeComparator_ScoreType(t *testing.T) {
+	t.Parallel()
+	comparator := &enqueueTimeComparator{}
+	assert.Equal(t, string(framework.EnqueueTimePriorityScoreType), comparator.ScoreType())
+}
diff --git a/pkg/epp/flowcontrol/framework/plugins/policies/intraflow/dispatch/functional_test.go b/pkg/epp/flowcontrol/framework/plugins/policies/intraflow/dispatch/functional_test.go
new file mode 100644
index 0000000..5a78d3e
--- /dev/null
+++ b/pkg/epp/flowcontrol/framework/plugins/policies/intraflow/dispatch/functional_test.go
@@ -0,0 +1,79 @@
+/*
+Copyright 2025 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 dispatch_test
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/framework"
+	frameworkmocks "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/framework/mocks"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/framework/plugins/policies/intraflow/dispatch"
+
+	_ "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/framework/plugins/policies/intraflow/dispatch/fcfs"
+)
+
+// TestIntraFlowDispatchPolicyConformance is the main conformance test suite for `framework.IntraFlowDispatchPolicy`
+// implementations.
+// It iterates over all policy implementations registered via `dispatch.MustRegisterPolicy` and runs a series of
+// sub-tests to ensure they adhere to the `framework.IntraFlowDispatchPolicy` contract.
+func TestIntraFlowDispatchPolicyConformance(t *testing.T) {
+	t.Parallel()
+
+	for policyName, constructor := range dispatch.RegisteredPolicies {
+		t.Run(string(policyName), func(t *testing.T) {
+			t.Parallel()
+
+			policy, err := constructor()
+			require.NoError(t, err, "Policy constructor for %s failed", policyName)
+			require.NotNil(t, policy, "Constructor for %s should return a non-nil policy instance", policyName)
+
+			t.Run("Initialization", func(t *testing.T) {
+				t.Parallel()
+				assert.NotEmpty(t, policy.Name(), "Name() for %s should not be empty", policyName)
+
+				comp := policy.Comparator()
+				require.NotNil(t, comp, "Comparator() for %s should not return nil", policyName)
+				assert.NotNil(t, comp.Func(), "Comparator().Func() for %s should not be nil", policyName)
+				assert.NotEmpty(t, comp.ScoreType(), "Comparator().ScoreType() for %s should not be empty", policyName)
+
+				caps := policy.RequiredQueueCapabilities()
+				assert.NotNil(t, caps, "RequiredQueueCapabilities() for %s should not return a nil slice", policyName)
+			})
+
+			t.Run("SelectItemFromNilQueue", func(t *testing.T) {
+				t.Parallel()
+				item, err := policy.SelectItem(nil)
+				require.NoError(t, err, "SelectItem(nil) for %s should not return an error", policyName)
+				assert.Nil(t, item, "SelectItem(nil) for %s should return a nil item", policyName)
+			})
+
+			t.Run("SelectItemFromEmptyQueue", func(t *testing.T) {
+				t.Parallel()
+				mockQueue := &frameworkmocks.MockFlowQueueAccessor{
+					PeekHeadErrV: framework.ErrQueueEmpty,
+					LenV:         0,
+				}
+				item, err := policy.SelectItem(mockQueue)
+				require.NoError(t, err, "SelectItem from an empty queue for %s should not return an error", policyName)
+				assert.Nil(t, item, "SelectItem from an empty queue for %s should return a nil item", policyName)
+			})
+		})
+	}
+}
diff --git a/pkg/epp/flowcontrol/framework/plugins/queue/benchmark_test.go b/pkg/epp/flowcontrol/framework/plugins/queue/benchmark_test.go
new file mode 100644
index 0000000..e1c9354
--- /dev/null
+++ b/pkg/epp/flowcontrol/framework/plugins/queue/benchmark_test.go
@@ -0,0 +1,219 @@
+/*
+Copyright 2025 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 queue_test
+
+import (
+	"fmt"
+	"sync"
+	"testing"
+
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/framework"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/framework/plugins/queue"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/types"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/types/mocks"
+)
+
+// BenchmarkQueues runs a series of benchmarks against all registered queue implementations.
+func BenchmarkQueues(b *testing.B) {
+	for queueName, constructor := range queue.RegisteredQueues {
+		b.Run(string(queueName), func(b *testing.B) {
+			// All queue implementations must support the default enqueue time comparator.
+			q, err := constructor(enqueueTimeComparator)
+			if err != nil {
+				b.Fatalf("Failed to construct queue '%s': %v", queueName, err)
+			}
+
+			b.Run("AddRemove", func(b *testing.B) {
+				benchmarkAddRemove(b, q)
+			})
+
+			b.Run("AddPeekRemove", func(b *testing.B) {
+				benchmarkAddPeekRemove(b, q)
+			})
+
+			b.Run("AddPeekTailRemove", func(b *testing.B) {
+				benchmarkAddPeekTailRemove(b, q)
+			})
+
+			b.Run("BulkAddThenBulkRemove", func(b *testing.B) {
+				benchmarkBulkAddThenBulkRemove(b, q)
+			})
+
+			b.Run("HighContention", func(b *testing.B) {
+				benchmarkHighContention(b, q)
+			})
+		})
+	}
+}
+
+// benchmarkAddRemove measures the throughput of tightly coupled Add and Remove operations in parallel. This is a good
+// measure of the base overhead of the queue's data structure and locking mechanism.
+func benchmarkAddRemove(b *testing.B, q framework.SafeQueue) {
+	b.ReportAllocs()
+	b.ResetTimer()
+
+	b.RunParallel(func(pb *testing.PB) {
+		for pb.Next() {
+			item := mocks.NewMockQueueItemAccessor(1, "item", "benchmark-flow")
+			_, _, err := q.Add(item)
+			if err != nil {
+				b.Fatalf("Add failed: %v", err)
+			}
+			_, _, _, err = q.Remove(item.Handle())
+			if err != nil {
+				b.Fatalf("Remove failed: %v", err)
+			}
+		}
+	})
+}
+
+// benchmarkAddPeekRemove measures the throughput of a serial Add, PeekHead, and Remove sequence. This simulates a
+// common consumer pattern where a single worker peeks at an item before deciding to process and remove it.
+func benchmarkAddPeekRemove(b *testing.B, q framework.SafeQueue) {
+	// Pre-add one item so PeekHead doesn't fail on the first iteration.
+	initialItem := mocks.NewMockQueueItemAccessor(1, "initial", "benchmark-flow")
+	if _, _, err := q.Add(initialItem); err != nil {
+		b.Fatalf("Failed to add initial item: %v", err)
+	}
+
+	b.ReportAllocs()
+
+	for b.Loop() {
+		item := mocks.NewMockQueueItemAccessor(1, "item", "benchmark-flow")
+		_, _, err := q.Add(item)
+		if err != nil {
+			b.Fatalf("Add failed: %v", err)
+		}
+
+		peeked, err := q.PeekHead()
+		if err != nil {
+			// In a concurrent benchmark, this could happen if the queue becomes empty.
+			// In a serial one, it's a fatal error.
+			b.Fatalf("PeekHead failed: %v", err)
+		}
+
+		_, _, _, err = q.Remove(peeked.Handle())
+		if err != nil {
+			b.Fatalf("Remove failed: %v", err)
+		}
+	}
+}
+
+// benchmarkBulkAddThenBulkRemove measures performance of filling the queue up with a batch of items and then draining
+// it. This can reveal performance characteristics related to how the data structure grows and shrinks.
+func benchmarkBulkAddThenBulkRemove(b *testing.B, q framework.SafeQueue) {
+	b.ReportAllocs()
+
+	for i := 0; b.Loop(); i++ {
+		// Add a batch of items
+		items := make([]types.QueueItemAccessor, 100)
+		for j := range items {
+			item := mocks.NewMockQueueItemAccessor(1, fmt.Sprintf("bulk-%d-%d", i, j), "benchmark-flow")
+			items[j] = item
+			if _, _, err := q.Add(item); err != nil {
+				b.Fatalf("Add failed: %v", err)
+			}
+		}
+
+		// Remove the same number of items
+		for range items {
+			peeked, err := q.PeekHead()
+			if err != nil {
+				b.Fatalf("PeekHead failed: %v", err)
+			}
+			if _, _, _, err := q.Remove(peeked.Handle()); err != nil {
+				b.Fatalf("Remove failed: %v", err)
+			}
+		}
+	}
+}
+
+// benchmarkAddPeekTailRemove measures the throughput of a serial Add, PeekTail, and Remove sequence. This is useful for
+// understanding the performance of accessing the lowest-priority item.
+func benchmarkAddPeekTailRemove(b *testing.B, q framework.SafeQueue) {
+	// Pre-add one item so PeekTail doesn't fail on the first iteration.
+	initialItem := mocks.NewMockQueueItemAccessor(1, "initial", "benchmark-flow")
+	if _, _, err := q.Add(initialItem); err != nil {
+		b.Fatalf("Failed to add initial item: %v", err)
+	}
+
+	b.ReportAllocs()
+
+	for b.Loop() {
+		item := mocks.NewMockQueueItemAccessor(1, "item", "benchmark-flow")
+		_, _, err := q.Add(item)
+		if err != nil {
+			b.Fatalf("Add failed: %v", err)
+		}
+
+		peeked, err := q.PeekTail()
+		if err != nil {
+			b.Fatalf("PeekTail failed: %v", err)
+		}
+
+		_, _, _, err = q.Remove(peeked.Handle())
+		if err != nil {
+			b.Fatalf("Remove failed: %v", err)
+		}
+	}
+}
+
+// benchmarkHighContention simulates a more realistic workload with multiple producers and consumers operating on the
+// queue concurrently.
+func benchmarkHighContention(b *testing.B, q framework.SafeQueue) {
+	// Pre-fill the queue to ensure consumers have work to do immediately.
+	for i := range 1000 {
+		item := mocks.NewMockQueueItemAccessor(1, fmt.Sprintf("prefill-%d", i), "benchmark-flow")
+		if _, _, err := q.Add(item); err != nil {
+			b.Fatalf("Failed to pre-fill queue: %v", err)
+		}
+	}
+
+	b.ReportAllocs()
+	b.ResetTimer()
+
+	var wg sync.WaitGroup
+	// Producers
+	for range 4 {
+		wg.Add(1)
+		go func() {
+			defer wg.Done()
+			for b.Loop() {
+				item := mocks.NewMockQueueItemAccessor(1, "item", "benchmark-flow")
+				_, _, _ = q.Add(item)
+			}
+		}()
+	}
+
+	// Consumers
+	for range 4 {
+		wg.Add(1)
+		go func() {
+			defer wg.Done()
+			for j := 0; j < b.N; j++ {
+				peeked, err := q.PeekHead()
+				if err == nil {
+					_, _, _, _ = q.Remove(peeked.Handle())
+				}
+				// Also peek tail to add more read contention
+				_, _ = q.PeekTail()
+			}
+		}()
+	}
+
+	wg.Wait()
+}
diff --git a/pkg/epp/flowcontrol/framework/plugins/queue/functional_test.go b/pkg/epp/flowcontrol/framework/plugins/queue/functional_test.go
new file mode 100644
index 0000000..dfdf286
--- /dev/null
+++ b/pkg/epp/flowcontrol/framework/plugins/queue/functional_test.go
@@ -0,0 +1,607 @@
+/*
+Copyright 2025 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 queue_test
+
+import (
+	"fmt"
+	"slices"
+	"sort"
+	"sync"
+	"sync/atomic"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/framework"
+	frameworkmocks "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/framework/mocks"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/framework/plugins/queue"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/types"
+	typesmocks "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/types/mocks"
+
+	_ "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/framework/plugins/queue/listqueue"
+	_ "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/framework/plugins/queue/maxminheap"
+)
+
+// enqueueTimeComparator orders items by their enqueue time (earlier first).
+// Used as the default comparator for basic FIFO-like ordering tests.
+var enqueueTimeComparator = &frameworkmocks.MockItemComparator{
+	ScoreTypeV: "enqueue_time_ns_asc",
+	FuncV: func(a, b types.QueueItemAccessor) bool {
+		return a.EnqueueTime().Before(b.EnqueueTime())
+	},
+}
+
+// byteSizeComparator orders items by their byte size (smaller first).
+var byteSizeComparator = &frameworkmocks.MockItemComparator{
+	ScoreTypeV: "byte_size_asc",
+	FuncV: func(a, b types.QueueItemAccessor) bool {
+		return a.OriginalRequest().ByteSize() < b.OriginalRequest().ByteSize()
+	},
+}
+
+// reverseEnqueueTimeComparator orders items by their enqueue time (later first - LIFO).
+// Used to test CapabilityPriorityConfigurable queues with a non-FIFO ordering.
+var reverseEnqueueTimeComparator = &frameworkmocks.MockItemComparator{
+	ScoreTypeV: "enqueue_time_ns_desc",
+	FuncV: func(a, b types.QueueItemAccessor) bool {
+		return a.EnqueueTime().After(b.EnqueueTime())
+	},
+}
+
+// testLifecycleAndOrdering is a helper function to execute a standard sequence of Add, PeekHead, and Remove operations
+// on a queue. It verifies the queue's state (length, byte size) and item ordering based on the provided `itemsInOrder`
+// slice, which should be pre-sorted according to the `comparatorName` (which is just a string for
+// logging/identification).
+// This function is crucial for testing different ordering logic (FIFO, custom priority).
+func testLifecycleAndOrdering(
+	t *testing.T,
+	q framework.SafeQueue,
+	itemsInOrder []*typesmocks.MockQueueItemAccessor,
+	comparatorName string,
+) {
+	t.Helper()
+
+	// PeekHead/PeekTail on empty queue
+	peeked, err := q.PeekHead()
+	assert.ErrorIs(t, err, framework.ErrQueueEmpty,
+		"[%s] PeekHead on empty queue should return ErrQueueEmpty", comparatorName)
+	assert.Nil(t, peeked, "[%s] PeekHead on empty queue should return a nil item", comparatorName)
+	peeked, err = q.PeekTail()
+	assert.ErrorIs(t, err, framework.ErrQueueEmpty,
+		"[%s] PeekTail on empty queue should return ErrQueueEmpty", comparatorName)
+	assert.Nil(t, peeked, "[%s] PeekTail on empty queue should return a nil item", comparatorName)
+
+	// Add items
+	currentExpectedLen := 0
+	var currentExpectedByteSize uint64
+	for i, item := range itemsInOrder {
+		newLen, newByteSize, addErr := q.Add(item)
+		require.NoError(t, addErr, "[%s] Add should not fail for a valid item (item %d, ID: %s)",
+			comparatorName, i, item.OriginalRequest().ID())
+		require.NotNil(t, item.Handle(), "[%s] Add must assign a non-nil handle to the item (item %d, ID: %s)",
+			comparatorName, i, item.OriginalRequest().ID())
+		require.False(t, item.Handle().IsInvalidated(),
+			"[%s] A new handle from Add must not be invalidated (item %d, ID: %s)",
+			comparatorName, i, item.OriginalRequest().ID())
+
+		currentExpectedLen++
+		currentExpectedByteSize += item.OriginalRequest().ByteSize()
+		assert.Equal(t, uint64(currentExpectedLen), newLen, "[%s] Add must return the correct new length (item %d, ID: %s)",
+			comparatorName, i, item.OriginalRequest().ID())
+		assert.Equal(t, currentExpectedByteSize, newByteSize,
+			"[%s] Add must return the correct new byte size (item %d, ID: %s)",
+			comparatorName, i, item.OriginalRequest().ID())
+	}
+
+	// Check final state after adds
+	initialLen := len(itemsInOrder)
+	var expectedTotalByteSize uint64
+	for _, item := range itemsInOrder {
+		expectedTotalByteSize += item.OriginalRequest().ByteSize()
+	}
+	assert.Equal(t, initialLen, q.Len(), "[%s] Len() should return the correct count after all items are added",
+		comparatorName)
+	assert.Equal(t, expectedTotalByteSize, q.ByteSize(),
+		"[%s] ByteSize() should return the correct sum after all items are added", comparatorName)
+
+	// Peek and Remove cycle to verify ordering
+	expectedLen := initialLen
+	expectedByteSize := expectedTotalByteSize
+	for i, expectedItem := range itemsInOrder {
+		// Verify PeekHead
+		peeked, err = q.PeekHead()
+		require.NoError(t, err, "[%s] PeekHead should not error on a non-empty queue (iteration %d)", comparatorName, i)
+		require.NotNil(t, peeked, "[%s] PeekHead should return a non-nil item (iteration %d)", comparatorName, i)
+		assert.Equal(t, expectedItem.OriginalRequest().ID(), peeked.OriginalRequest().ID(),
+			"[%s] PeekHead must return the item (ID: %s) at the head of the queue (iteration %d)",
+			comparatorName, expectedItem.OriginalRequest().ID(), i)
+		peekedHandle := peeked.Handle()
+		require.NotNil(t, peekedHandle, "[%s] Handle from a peeked item must not be nil (iteration %d)", comparatorName, i)
+		require.False(t, peekedHandle.IsInvalidated(),
+			"[%s] Handle from a peeked item must not be invalidated (iteration %d)", comparatorName, i)
+		assert.Equal(t, expectedLen, q.Len(), "[%s] Len() must be unchanged after PeekHead (iteration %d)",
+			comparatorName, i)
+		assert.Equal(t, expectedByteSize, q.ByteSize(),
+			"[%s] ByteSize() must be unchanged after PeekHead (iteration %d)", comparatorName, i)
+
+		// Verify PeekTail
+		peekedTail, err := q.PeekTail()
+		require.NoError(t, err, "[%s] PeekTail should not error on a non-empty queue (iteration %d)", comparatorName, i)
+		require.NotNil(t, peekedTail, "[%s] PeekTail should return a non-nil item (iteration %d)", comparatorName, i)
+		// The tail is the last item in the *remaining* ordered slice.
+		expectedTailItem := itemsInOrder[len(itemsInOrder)-1]
+		assert.Equal(t, expectedTailItem.OriginalRequest().ID(), peekedTail.OriginalRequest().ID(),
+			"[%s] PeekTail must return the item with the lowest priority (iteration %d)", comparatorName, i)
+
+		// Remove the head item
+		removed, newLen, newByteSize, removeErr := q.Remove(peekedHandle)
+		require.NoError(t, removeErr, "[%s] Remove with a valid handle should not fail (iteration %d, item ID: %s)",
+			comparatorName, i, expectedItem.OriginalRequest().ID())
+		require.NotNil(t, removed, "[%s] Remove should return the removed item (iteration %d)", comparatorName, i)
+		assert.Equal(t, expectedItem.OriginalRequest().ID(), removed.OriginalRequest().ID(),
+			"[%s] Remove should return the correct item (iteration %d)", comparatorName, i)
+		assert.True(t, peekedHandle.IsInvalidated(),
+			"[%s] Remove must invalidate the handle of the removed item (iteration %d)", comparatorName, i)
+
+		expectedLen--
+		expectedByteSize -= removed.OriginalRequest().ByteSize()
+		assert.Equal(t, uint64(expectedLen), newLen, "[%s] Remove must return the correct new length (iteration %d)",
+			comparatorName, i)
+		assert.Equal(t, expectedByteSize, newByteSize,
+			"[%s] Remove must return the correct new byte size (iteration %d)", comparatorName, i)
+		assert.Equal(t, expectedLen, q.Len(), "[%s] Len() should be correctly updated after Remove (iteration %d)",
+			comparatorName, i)
+		assert.Equal(t, expectedByteSize, q.ByteSize(),
+			"[%s] ByteSize() should be correctly updated after Remove (iteration %d)", comparatorName, i)
+	}
+
+	assert.Zero(t, q.Len(), "[%s] Queue length should be 0 after all items are removed", comparatorName)
+	assert.Zero(t, q.ByteSize(), "[%s] Queue byte size should be 0 after all items are removed", comparatorName)
+
+	peeked, err = q.PeekHead()
+	assert.ErrorIs(t, err, framework.ErrQueueEmpty, "[%s] PeekHead on an empty queue should return ErrQueueEmpty again",
+		comparatorName)
+	assert.Nil(t, peeked, "[%s] PeekHead on an empty queue should return a nil item again", comparatorName)
+}
+
+// TestQueueConformance is the main conformance test suite for `framework.SafeQueue` implementations.
+// It iterates over all queue implementations registered via `queue.MustRegisterQueue` and runs a series of sub-tests to
+// ensure they adhere to the `framework.SafeQueue` contract.
+func TestQueueConformance(t *testing.T) {
+	t.Parallel()
+
+	for queueName, constructor := range queue.RegisteredQueues {
+		t.Run(string(queueName), func(t *testing.T) {
+			t.Parallel()
+			flowSpec := &types.FlowSpecification{ID: "test-flow-1", Priority: 0}
+
+			t.Run("Initialization", func(t *testing.T) {
+				t.Parallel()
+				q, err := constructor(enqueueTimeComparator)
+				require.NoError(t, err, "Setup: creating queue for test should not fail")
+
+				require.NotNil(t, q, "Constructor should return a non-nil queue instance")
+				assert.Zero(t, q.Len(), "A new queue should have a length of 0")
+				assert.Zero(t, q.ByteSize(), "A new queue should have a byte size of 0")
+				assert.Equal(t, string(queueName), q.Name(), "Name() should return the registered name of the queue")
+				assert.NotNil(t, q.Capabilities(), "Capabilities() should not return a nil slice")
+				assert.NotEmpty(t, q.Capabilities(), "Capabilities() should return at least one capability")
+			})
+
+			t.Run("LifecycleAndOrdering_DefaultFIFO", func(t *testing.T) {
+				t.Parallel()
+				q, err := constructor(enqueueTimeComparator)
+				require.NoError(t, err, "Setup: creating queue with enqueueTimeComparator should not fail")
+
+				now := time.Now()
+
+				item1 := typesmocks.NewMockQueueItemAccessor(100, "item1_fifo", flowSpec.ID)
+				item1.EnqueueTimeV = now.Add(-2 * time.Second) // Earliest
+				item2 := typesmocks.NewMockQueueItemAccessor(50, "item2_fifo", flowSpec.ID)
+				item2.EnqueueTimeV = now.Add(-1 * time.Second) // Middle
+				item3 := typesmocks.NewMockQueueItemAccessor(20, "item3_fifo", flowSpec.ID)
+				item3.EnqueueTimeV = now // Latest
+
+				itemsInFIFOOrder := []*typesmocks.MockQueueItemAccessor{item1, item2, item3}
+				testLifecycleAndOrdering(t, q, itemsInFIFOOrder, "DefaultFIFO")
+			})
+
+			qForCapCheck, err := constructor(enqueueTimeComparator)
+			if err == nil && slices.Contains(qForCapCheck.Capabilities(), framework.CapabilityPriorityConfigurable) {
+				t.Run("LifecycleAndOrdering_PriorityConfigurable_ByteSize", func(t *testing.T) {
+					t.Parallel()
+					q, err := constructor(byteSizeComparator)
+					require.NoError(t, err, "Setup: creating queue with byteSizeComparator should not fail")
+
+					itemLarge := typesmocks.NewMockQueueItemAccessor(100, "itemLarge_prio", flowSpec.ID)
+					itemSmall := typesmocks.NewMockQueueItemAccessor(20, "itemSmall_prio", flowSpec.ID)
+					itemMedium := typesmocks.NewMockQueueItemAccessor(50, "itemMedium_prio", flowSpec.ID)
+
+					itemsInByteSizeOrder := []*typesmocks.MockQueueItemAccessor{itemSmall, itemMedium, itemLarge}
+					testLifecycleAndOrdering(t, q, itemsInByteSizeOrder, "PriorityByteSize")
+				})
+
+				t.Run("LifecycleAndOrdering_PriorityConfigurable_LIFO", func(t *testing.T) {
+					t.Parallel()
+					q, err := constructor(reverseEnqueueTimeComparator)
+					require.NoError(t, err, "Setup: creating queue with reverseEnqueueTimeComparator should not fail")
+
+					now := time.Now()
+					item1 := typesmocks.NewMockQueueItemAccessor(100, "item1_lifo", flowSpec.ID)
+					item1.EnqueueTimeV = now.Add(-2 * time.Second) // Earliest
+					item2 := typesmocks.NewMockQueueItemAccessor(50, "item2_lifo", flowSpec.ID)
+					item2.EnqueueTimeV = now.Add(-1 * time.Second) // Middle
+					item3 := typesmocks.NewMockQueueItemAccessor(20, "item3_lifo", flowSpec.ID)
+					item3.EnqueueTimeV = now // Latest
+
+					itemsInLIFOOrder := []*typesmocks.MockQueueItemAccessor{item3, item2, item1}
+					testLifecycleAndOrdering(t, q, itemsInLIFOOrder, "PriorityLIFO")
+				})
+			}
+
+			t.Run("Add_NilItem", func(t *testing.T) {
+				t.Parallel()
+				q, err := constructor(enqueueTimeComparator)
+				require.NoError(t, err, "Setup: creating queue for test should not fail")
+
+				currentLen := q.Len()
+				currentByteSize := q.ByteSize()
+				newLen, newByteSize, err := q.Add(nil)
+				assert.ErrorIs(t, err, framework.ErrNilQueueItem, "Add(nil) must return ErrNilQueueItem")
+				assert.Equal(t, uint64(currentLen), newLen, "Add(nil) must not change the length returned")
+				assert.Equal(t, currentByteSize, newByteSize, "Add(nil) must not change the byte size returned")
+				assert.Equal(t, currentLen, q.Len(), "The queue's length must not change after a failed Add")
+				assert.Equal(t, currentByteSize, q.ByteSize(), "The queue's byte size must not change after a failed Add")
+			})
+
+			t.Run("Remove_InvalidHandle", func(t *testing.T) {
+				t.Parallel()
+				q, err := constructor(enqueueTimeComparator)
+				require.NoError(t, err, "Setup: creating queue for test should not fail")
+
+				item := typesmocks.NewMockQueueItemAccessor(100, "item", flowSpec.ID)
+				_, _, err = q.Add(item)
+				require.NoError(t, err, "Setup: adding an item should succeed")
+
+				otherQ, err := constructor(enqueueTimeComparator) // A different queue instance
+				require.NoError(t, err, "Setup: creating otherQ should succeed")
+				otherItem := typesmocks.NewMockQueueItemAccessor(10, "other_item", "other_flow")
+				_, _, err = otherQ.Add(otherItem)
+				require.NoError(t, err, "Setup: adding item to otherQ should succeed")
+				alienHandle := otherItem.Handle()
+				require.NotNil(t, alienHandle, "Setup: alien handle should not be nil")
+
+				invalidatedHandle := &typesmocks.MockQueueItemHandle{}
+				invalidatedHandle.Invalidate()
+
+				foreignHandle := &typesmocks.MockQueueItemHandle{} // Different type
+
+				testCases := []struct {
+					name      string
+					handle    types.QueueItemHandle
+					expectErr error
+				}{
+					{name: "nil handle", handle: nil, expectErr: framework.ErrInvalidQueueItemHandle},
+					{name: "invalidated handle", handle: invalidatedHandle, expectErr: framework.ErrInvalidQueueItemHandle},
+					{name: "alien handle from other queue", handle: alienHandle, expectErr: framework.ErrQueueItemNotFound},
+					{name: "foreign handle type", handle: foreignHandle, expectErr: framework.ErrInvalidQueueItemHandle},
+				}
+
+				for _, tc := range testCases {
+					t.Run(tc.name, func(t *testing.T) {
+						t.Parallel()
+						currentLen := q.Len()
+						currentByteSize := q.ByteSize()
+
+						_, newLen, newByteSize, removeErr := q.Remove(tc.handle)
+						assert.ErrorIs(t, removeErr, tc.expectErr, "Remove with %s should produce %v", tc.name, tc.expectErr)
+						assert.Equal(t, uint64(currentLen), newLen, "Remove with %s must not change the length returned", tc.name)
+						assert.Equal(t, currentByteSize, newByteSize, "Remove with %s must not change the byte size returned",
+							tc.name)
+						assert.Equal(t, currentLen, q.Len(), "The queue's length must not change after a failed Remove with %s",
+							tc.name)
+						assert.Equal(t, currentByteSize, q.ByteSize(),
+							"The queue's byte size must not change after a failed Remove with %s", tc.name)
+					})
+				}
+			})
+
+			t.Run("Remove_NonHead", func(t *testing.T) {
+				t.Parallel()
+				q, err := constructor(enqueueTimeComparator)
+				require.NoError(t, err, "Setup: creating queue for test should not fail")
+
+				now := time.Now()
+				item1 := typesmocks.NewMockQueueItemAccessor(10, "item1_nonhead", flowSpec.ID)
+				item1.EnqueueTimeV = now.Add(-3 * time.Second)
+				item2 := typesmocks.NewMockQueueItemAccessor(20, "item2_nonhead_TARGET", flowSpec.ID)
+				item2.EnqueueTimeV = now.Add(-2 * time.Second)
+				item3 := typesmocks.NewMockQueueItemAccessor(30, "item3_nonhead", flowSpec.ID)
+				item3.EnqueueTimeV = now.Add(-1 * time.Second)
+
+				_, _, _ = q.Add(item1)
+				_, _, _ = q.Add(item2)
+				_, _, _ = q.Add(item3)
+				require.Equal(t, 3, q.Len(), "Queue should have 3 items before removing non-head")
+				handleNonHead := item2.Handle()
+
+				removed, newLen, newByteSize, err := q.Remove(handleNonHead)
+				require.NoError(t, err, "It should be possible to remove an item that is not the head")
+				require.NotNil(t, removed, "Remove should return the removed item")
+				assert.Equal(t, item2.OriginalRequest().ID(), removed.OriginalRequest().ID(),
+					"Remove should return the correct item (item2)")
+				assert.True(t, handleNonHead.IsInvalidated(), "Remove must invalidate the handle of the removed item")
+				assert.Equal(t, uint64(2), newLen, "Remove must return the correct new length (2)")
+				assert.Equal(t, item1.OriginalRequest().ByteSize()+item3.OriginalRequest().ByteSize(), newByteSize,
+					"Remove must return the correct new byte size")
+				assert.Equal(t, 2, q.Len(), "Queue length should be 2 after removing non-head")
+
+				// Attempt to remove again with the now-stale handle
+				_, _, _, errStaleNonHead := q.Remove(handleNonHead)
+				assert.ErrorIs(t, errStaleNonHead, framework.ErrInvalidQueueItemHandle,
+					"Removing with a stale handle must fail with ErrInvalidQueueItemHandle")
+			})
+
+			predicateRemoveOddSizes := func(item types.QueueItemAccessor) bool {
+				return item.OriginalRequest().ByteSize()%2 != 0
+			}
+
+			t.Run("Cleanup_EmptyQueue", func(t *testing.T) {
+				t.Parallel()
+				emptyQ, _ := constructor(enqueueTimeComparator)
+				cleanedItems, err := emptyQ.Cleanup(predicateRemoveOddSizes)
+				require.NoError(t, err, "Cleanup on an empty queue should not return an error")
+				assert.Empty(t, cleanedItems, "Cleanup on an empty queue should return an empty slice")
+				assert.Zero(t, emptyQ.Len(), "Len() should be 0 after Cleanup on an empty queue")
+				assert.Zero(t, emptyQ.ByteSize(), "ByteSize() should be 0 after Cleanup on an empty queue")
+			})
+
+			t.Run("Cleanup_PredicateMatchesNone", func(t *testing.T) {
+				t.Parallel()
+				q, _ := constructor(enqueueTimeComparator)
+				itemK1 := typesmocks.NewMockQueueItemAccessor(10, "k1_matchNone", flowSpec.ID)
+				itemK2 := typesmocks.NewMockQueueItemAccessor(12, "k2_matchNone", flowSpec.ID)
+				_, _, _ = q.Add(itemK1)
+				_, _, _ = q.Add(itemK2)
+				initialLen := q.Len()
+				initialBs := q.ByteSize()
+
+				cleanedItems, err := q.Cleanup(func(item types.QueueItemAccessor) bool { return false })
+				require.NoError(t, err, "Cleanup should not return an error")
+				assert.Empty(t, cleanedItems, "Cleanup should return an empty slice when no items match the predicate")
+				assert.Equal(t, initialLen, q.Len(), "Len() should not change after Cleanup when no items match thepredicate")
+				assert.Equal(t, initialBs, q.ByteSize(),
+					"ByteSize() should not change after Cleanup when no items match the predicate")
+				assert.False(t, itemK1.Handle().IsInvalidated(), "Handle for kept item 1 must NOT be invalidated")
+				assert.False(t, itemK2.Handle().IsInvalidated(), "Handle for kept item 2 must NOT be invalidated")
+			})
+
+			t.Run("Cleanup_PredicateMatchesAll", func(t *testing.T) {
+				t.Parallel()
+				q, _ := constructor(enqueueTimeComparator)
+				itemR1 := typesmocks.NewMockQueueItemAccessor(11, "r1_matchAll", flowSpec.ID)
+				itemR2 := typesmocks.NewMockQueueItemAccessor(13, "r2_matchAll", flowSpec.ID)
+				_, _, _ = q.Add(itemR1)
+				_, _, _ = q.Add(itemR2)
+
+				cleanedItems, err := q.Cleanup(func(item types.QueueItemAccessor) bool { return true })
+				require.NoError(t, err, "Cleanup should not return an error")
+				assert.Len(t, cleanedItems, 2, "Cleanup should return all items that matched the predicate")
+				assert.Zero(t, q.Len(), "Len() should be 0 after Cleanup")
+				assert.Zero(t, q.ByteSize(), "ByteSize() should be 0 after Cleanup")
+				assert.True(t, itemR1.Handle().IsInvalidated(), "Handle for removed item 1 must be invalidated")
+				assert.True(t, itemR2.Handle().IsInvalidated(), "Handle for removed item 2 must be invalidated")
+			})
+
+			t.Run("Cleanup_PredicateMatchesSubset_VerifyHandles", func(t *testing.T) {
+				t.Parallel()
+				q, _ := constructor(enqueueTimeComparator)
+				iK1 := typesmocks.NewMockQueueItemAccessor(20, "k1_subset", flowSpec.ID)
+				iR1 := typesmocks.NewMockQueueItemAccessor(11, "r1_subset", flowSpec.ID)
+				iK2 := typesmocks.NewMockQueueItemAccessor(22, "k2_subset", flowSpec.ID)
+				iR2 := typesmocks.NewMockQueueItemAccessor(33, "r2_subset", flowSpec.ID)
+				_, _, _ = q.Add(iK1)
+				_, _, _ = q.Add(iR1)
+				_, _, _ = q.Add(iK2)
+				_, _, _ = q.Add(iR2)
+
+				expectedKeptByteSize := iK1.OriginalRequest().ByteSize() + iK2.OriginalRequest().ByteSize()
+
+				cleanedItems, err := q.Cleanup(predicateRemoveOddSizes)
+				require.NoError(t, err, "Cleanup should not return an error")
+				assert.Len(t, cleanedItems, 2, "Cleanup should return 2 items that matched the predicate")
+				assert.Equal(t, 2, q.Len(), "Len() should be 2 after Cleanup")
+				assert.Equal(t, expectedKeptByteSize, q.ByteSize(), "ByteSize() should be sum of kept items after Cleanup")
+
+				foundR1, foundR2 := false, false
+				for _, item := range cleanedItems {
+					if item.OriginalRequest().ID() == iR1.OriginalRequest().ID() {
+						foundR1 = true
+						assert.True(t, iR1.Handle().IsInvalidated(), "Handle for removed item iR1 must be invalidated")
+					}
+					if item.OriginalRequest().ID() == iR2.OriginalRequest().ID() {
+						foundR2 = true
+						assert.True(t, iR2.Handle().IsInvalidated(), "Handle for removed item iR2 must be invalidated")
+					}
+				}
+				assert.True(t, foundR1, "iR1 should have been returned by Cleanup")
+				assert.True(t, foundR2, "iR2 should have been returned by Cleanup")
+
+				assert.False(t, iK1.Handle().IsInvalidated(), "Handle for kept item iK1 must NOT be invalidated")
+				assert.False(t, iK2.Handle().IsInvalidated(), "Handle for kept item iK2 must NOT be invalidated")
+
+				// Verify remaining items are correct
+				var remainingIDs []string
+				for q.Len() > 0 {
+					peeked, _ := q.PeekHead()
+					item, _, _, _ := q.Remove(peeked.Handle())
+					remainingIDs = append(remainingIDs, item.OriginalRequest().ID())
+				}
+				sort.Strings(remainingIDs) // Sort for stable comparison
+				expectedRemainingIDs := []string{iK1.OriginalRequest().ID(), iK2.OriginalRequest().ID()}
+				sort.Strings(expectedRemainingIDs)
+				assert.Equal(t, expectedRemainingIDs, remainingIDs, "Remaining items in queue are not as expected")
+			})
+
+			t.Run("Drain_NonEmptyQueue_VerifyHandles", func(t *testing.T) {
+				t.Parallel()
+				q, err := constructor(enqueueTimeComparator)
+				require.NoError(t, err, "Setup: creating queue for drain test should not fail")
+
+				itemD1 := typesmocks.NewMockQueueItemAccessor(10, "ditem1", flowSpec.ID)
+				itemD2 := typesmocks.NewMockQueueItemAccessor(20, "ditem2", flowSpec.ID)
+				_, _, _ = q.Add(itemD1)
+				_, _, _ = q.Add(itemD2)
+
+				drainedItems, err := q.Drain()
+				require.NoError(t, err, "Drain on a non-empty queue should not fail")
+				assert.Len(t, drainedItems, 2, "Drain should return all items that were in the queue")
+				assert.Zero(t, q.Len(), "Queue length must be 0 after Drain")
+				assert.Zero(t, q.ByteSize(), "Queue byte size must be 0 after Drain")
+
+				assert.True(t, itemD1.Handle().IsInvalidated(), "Handle for drained itemD1 must be invalidated")
+				assert.True(t, itemD2.Handle().IsInvalidated(), "Handle for drained itemD2 must be invalidated")
+
+				var foundD1, foundD2 bool
+				for _, item := range drainedItems {
+					if item.OriginalRequest().ID() == itemD1.OriginalRequest().ID() {
+						foundD1 = true
+					}
+					if item.OriginalRequest().ID() == itemD2.OriginalRequest().ID() {
+						foundD2 = true
+					}
+				}
+				assert.True(t, foundD1, "itemD1 should be in drainedItems")
+				assert.True(t, foundD2, "itemD2 should be in drainedItems")
+			})
+
+			t.Run("Drain_EmptyQueue_DrainTwice", func(t *testing.T) {
+				t.Parallel()
+				q, err := constructor(enqueueTimeComparator)
+				require.NoError(t, err, "Setup: creating queue for empty drain test should not fail")
+
+				drainedItems, err := q.Drain() // First drain on empty
+				require.NoError(t, err, "Drain on an empty queue should not fail")
+				assert.Empty(t, drainedItems, "Drain on an empty queue should return an empty slice")
+
+				drainedAgain, err := q.Drain() // Second drain on already empty
+				require.NoError(t, err, "Second drain on an already empty queue should not fail")
+				assert.Empty(t, drainedAgain, "Second drain on an already empty queue should return an empty slice")
+				assert.Zero(t, q.Len())
+				assert.Zero(t, q.ByteSize())
+			})
+
+			t.Run("Concurrency", func(t *testing.T) {
+				t.Parallel()
+				q, err := constructor(enqueueTimeComparator)
+				require.NoError(t, err, "Setup: creating queue for concurrency test should not fail")
+
+				const (
+					numGoroutines   = 10
+					initialItems    = 200
+					opsPerGoroutine = 50
+				)
+
+				// handleChan acts as a concurrent-safe pool of handles that goroutines can pull from to test Remove.
+				handleChan := make(chan types.QueueItemHandle, initialItems+(numGoroutines*opsPerGoroutine))
+
+				// Pre-populate the queue with an initial set of items.
+				for i := 0; i < initialItems; i++ {
+					item := typesmocks.NewMockQueueItemAccessor(1, fmt.Sprintf("%s_conc_init_%d", flowSpec.ID, i), flowSpec.ID)
+					_, _, err := q.Add(item)
+					require.NoError(t, err, "Setup: pre-populating the queue should not fail")
+					handleChan <- item.Handle()
+				}
+
+				var wg sync.WaitGroup
+				wg.Add(numGoroutines)
+				var successfulAdds, successfulRemoves atomic.Uint64
+
+				// Start goroutines to perform a mix of concurrent operations.
+				for i := range numGoroutines {
+					go func(routineID int) {
+						defer wg.Done()
+						for j := 0; j < opsPerGoroutine; j++ {
+							opType := (j + routineID) % 4 // Vary operations more across goroutines
+							switch opType {
+							case 0: // Add
+								item := typesmocks.NewMockQueueItemAccessor(1,
+									fmt.Sprintf("%s_conc_init_%d_%d", flowSpec.ID, routineID, j), flowSpec.ID)
+								_, _, err := q.Add(item)
+								if assert.NoError(t, err, "Add must be goroutine-safe") {
+									successfulAdds.Add(1)
+									handleChan <- item.Handle()
+								}
+							case 1: // Remove
+								select {
+								case handle := <-handleChan:
+									if handle != nil && !handle.IsInvalidated() { // Check before trying to prevent known-to-fail calls
+										_, _, _, removeErr := q.Remove(handle)
+										if removeErr == nil {
+											successfulRemoves.Add(1)
+										} else {
+											// It's okay if it's ErrInvalidQueueItemHandle or ErrQueueItemNotFound due to races
+											assert.ErrorIs(t, removeErr, framework.ErrInvalidQueueItemHandle,
+												"Expected invalid handle or not found if raced")
+										}
+									}
+								default:
+									// No handles available to remove
+								}
+							case 2: // Inspect
+								_ = q.Len()
+								_ = q.ByteSize()
+								_, err := q.PeekHead()
+								if q.Len() == 0 { // Only expect ErrQueueEmpty if Len is 0
+									assert.ErrorIs(t, err, framework.ErrQueueEmpty, "Peek on empty queue expected ErrQueueEmpty")
+								}
+								_, err = q.PeekTail()
+								if q.Len() == 0 { // Only expect ErrQueueEmpty if Len is 0
+									assert.ErrorIs(t, err, framework.ErrQueueEmpty, "PeekTail on empty queue expected ErrQueueEmpty")
+								}
+							case 3: // Cleanup
+								_, cleanupErr := q.Cleanup(func(item types.QueueItemAccessor) bool { return false })
+								assert.NoError(t, cleanupErr, "Cleanup (no-op) should be goroutine-safe")
+							}
+						}
+					}(i)
+				}
+
+				wg.Wait()
+				close(handleChan)
+
+				// Drain the queue to verify all handles are invalidated and to count remaining items accurately.
+				drainedItems, drainErr := q.Drain()
+				require.NoError(t, drainErr, "Draining queue at the end of concurrency test should not fail")
+
+				for _, item := range drainedItems {
+					require.True(t, item.Handle().IsInvalidated(), "All handles from final drain must be invalidated")
+				}
+
+				// The number of items successfully added minus those successfully removed should equal the number of items
+				// drained.
+				assert.Equal(t, int(initialItems)+int(successfulAdds.Load())-int(successfulRemoves.Load()), len(drainedItems),
+					"Number of items drained (%d) should match initial (%d) + successful adds (%d) - successful removes (%d).",
+					len(drainedItems), initialItems, successfulAdds.Load(), successfulRemoves.Load())
+
+				assert.Zero(t, q.Len(), "Queue length should be 0 after final drain")
+				assert.Zero(t, q.ByteSize(), "Queue byte size should be 0 after final drain")
+			})
+		})
+	}
+}
diff --git a/pkg/epp/flowcontrol/framework/plugins/queue/maxminheap/maxminheap_test.go b/pkg/epp/flowcontrol/framework/plugins/queue/maxminheap/maxminheap_test.go
new file mode 100644
index 0000000..defbf0f
--- /dev/null
+++ b/pkg/epp/flowcontrol/framework/plugins/queue/maxminheap/maxminheap_test.go
@@ -0,0 +1,117 @@
+/*
+Copyright 2025 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 maxminheap
+
+import (
+	"math"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/require"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/framework/mocks"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/types"
+	typesmocks "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/types/mocks"
+)
+
+// enqueueTimeComparator orders items by their enqueue time (earlier first).
+var enqueueTimeComparator = &mocks.MockItemComparator{
+	ScoreTypeV: "enqueue_time_ns_asc",
+	FuncV: func(a, b types.QueueItemAccessor) bool {
+		return a.EnqueueTime().After(b.EnqueueTime())
+	},
+}
+
+// TestMaxMinHeap_InternalProperty validates that the max-min heap property is maintained after a series of `Add` and
+// `Remove` operations. This is a white-box test to ensure the internal data structure is always in a valid state.
+func TestMaxMinHeap_InternalProperty(t *testing.T) {
+	t.Parallel()
+	q := newMaxMinHeap(enqueueTimeComparator)
+
+	items := make([]*typesmocks.MockQueueItemAccessor, 20)
+	now := time.Now()
+	for i := range items {
+		// Add items in a somewhat random order of enqueue times
+		items[i] = typesmocks.NewMockQueueItemAccessor(10, "item", "flow")
+		items[i].EnqueueTimeV = now.Add(time.Duration((i%5-2)*10) * time.Second)
+		_, _, err := q.Add(items[i])
+		require.NoError(t, err, "Add should not fail")
+		assertHeapProperty(t, q, "after adding item %d", i)
+	}
+
+	// Remove a few items from the middle and validate the heap property
+	for _, i := range []int{15, 7, 11} {
+		handle := items[i].Handle()
+		_, _, _, err := q.Remove(handle)
+		require.NoError(t, err, "Remove should not fail for item %d", i)
+		assertHeapProperty(t, q, "after removing item %d", i)
+	}
+
+	// Remove remaining items from the head and validate each time
+	for q.Len() > 0 {
+		head, err := q.PeekHead()
+		require.NoError(t, err)
+		_, _, _, err = q.Remove(head.Handle())
+		require.NoError(t, err)
+		assertHeapProperty(t, q, "after removing head item")
+	}
+}
+
+// assertHeapProperty checks if the slice of items satisfies the max-min heap property.
+func assertHeapProperty(t *testing.T, h *maxMinHeap, msgAndArgs ...any) {
+	t.Helper()
+	if len(h.items) > 0 {
+		verifyNode(t, h, 0, msgAndArgs...)
+	}
+}
+
+// verifyNode recursively checks that the subtree at index `i` satisfies the max-min heap property.
+func verifyNode(t *testing.T, h *maxMinHeap, i int, msgAndArgs ...any) {
+	t.Helper()
+	n := len(h.items)
+	if i >= n {
+		return
+	}
+
+	level := int(math.Floor(math.Log2(float64(i + 1))))
+	isMinLevel := level%2 != 0
+
+	leftChild := 2*i + 1
+	rightChild := 2*i + 2
+
+	// Check children
+	if leftChild < n {
+		if isMinLevel {
+			require.False(t, h.comparator.Func()(h.items[i], h.items[leftChild]),
+				"min-level node %d has child %d with smaller value. %v", i, leftChild, msgAndArgs)
+		} else { // isMaxLevel
+			require.False(t, h.comparator.Func()(h.items[leftChild], h.items[i]),
+				"max-level node %d has child %d with larger value. %v", i, leftChild, msgAndArgs)
+		}
+		verifyNode(t, h, leftChild, msgAndArgs...)
+	}
+
+	if rightChild < n {
+		if isMinLevel {
+			require.False(t, h.comparator.Func()(h.items[i], h.items[rightChild]),
+				"min-level node %d has child %d with smaller value. %v", i, rightChild, msgAndArgs)
+		} else { // isMaxLevel
+			require.False(t, h.comparator.Func()(h.items[rightChild], h.items[i]),
+				"max-level node %d has child %d with larger value. %v", i, rightChild, msgAndArgs)
+		}
+		verifyNode(t, h, rightChild, msgAndArgs...)
+	}
+}
diff --git a/pkg/epp/requestcontrol/director_test.go b/pkg/epp/requestcontrol/director_test.go
index 0c9b080..d0571bf 100644
--- a/pkg/epp/requestcontrol/director_test.go
+++ b/pkg/epp/requestcontrol/director_test.go
@@ -131,11 +131,13 @@ func TestDirector_HandleRequest(t *testing.T) {
 	defaultSuccessfulScheduleResults := &schedulingtypes.SchedulingResult{
 		ProfileResults: map[string]*schedulingtypes.ProfileRunResult{
 			"testProfile": {
-				TargetPod: &schedulingtypes.ScoredPod{
-					Pod: &schedulingtypes.PodMetrics{
-						Pod: &backend.Pod{
-							Address:        "192.168.1.100",
-							NamespacedName: k8stypes.NamespacedName{Name: "pod1", Namespace: "default"},
+				TargetPods: []schedulingtypes.Pod{
+					&schedulingtypes.ScoredPod{
+						Pod: &schedulingtypes.PodMetrics{
+							Pod: &backend.Pod{
+								Address:        "192.168.1.100",
+								NamespacedName: k8stypes.NamespacedName{Name: "pod1", Namespace: "default"},
+							},
 						},
 					},
 				},
diff --git a/pkg/epp/scheduling/framework/plugins/filter/filter_test.go b/pkg/epp/scheduling/framework/plugins/filter/filter_test.go
index 93fd46c..0643fe2 100644
--- a/pkg/epp/scheduling/framework/plugins/filter/filter_test.go
+++ b/pkg/epp/scheduling/framework/plugins/filter/filter_test.go
@@ -275,12 +275,12 @@ func TestDecisionTreeFilterFactory(t *testing.T) {
 
 	testHandle := utils.NewTestHandle(context.Background())
 
-	testHandle.Plugins().AddPlugin("leastKvCache", leastKvCacheFilter)
-	testHandle.Plugins().AddPlugin("leastQueue", leastQueueFilter)
-	testHandle.Plugins().AddPlugin("loraAffinity", loraAffinityFilter)
-	testHandle.Plugins().AddPlugin("lowQueue", lowQueueFilter)
+	testHandle.AddPlugin("leastKvCache", leastKvCacheFilter)
+	testHandle.AddPlugin("leastQueue", leastQueueFilter)
+	testHandle.AddPlugin("loraAffinity", loraAffinityFilter)
+	testHandle.AddPlugin("lowQueue", lowQueueFilter)
 
-	testHandle.Plugins().AddPlugin("kvCacheScorer", kvCacheScorer)
+	testHandle.AddPlugin("kvCacheScorer", kvCacheScorer)
 
 	tests := []struct {
 		name       string
diff --git a/pkg/epp/scheduling/framework/plugins/multi/prefix/plugin_test.go b/pkg/epp/scheduling/framework/plugins/multi/prefix/plugin_test.go
index ea7fab7..27e13d6 100644
--- a/pkg/epp/scheduling/framework/plugins/multi/prefix/plugin_test.go
+++ b/pkg/epp/scheduling/framework/plugins/multi/prefix/plugin_test.go
@@ -61,7 +61,7 @@ func TestPrefixPlugin(t *testing.T) {
 	assert.Equal(t, float64(0), scores[pod2], "score for pod2")
 
 	// Simulate pod1 was picked.
-	plugin.PostCycle(context.Background(), cycleState1, &types.ProfileRunResult{TargetPod: pod1})
+	plugin.PostCycle(context.Background(), cycleState1, &types.ProfileRunResult{TargetPods: []types.Pod{pod1}})
 
 	// Second request doesn't share any prefix with first one. It should be added to the cache but
 	// the pod score should be 0.
@@ -82,7 +82,7 @@ func TestPrefixPlugin(t *testing.T) {
 	assert.Equal(t, float64(0), scores[pod2], "score for pod2")
 
 	// Simulate pod2 was picked.
-	plugin.PostCycle(context.Background(), cycleState2, &types.ProfileRunResult{TargetPod: pod2})
+	plugin.PostCycle(context.Background(), cycleState2, &types.ProfileRunResult{TargetPods: []types.Pod{pod2}})
 
 	// Third request shares partial prefix with first one.
 	req3 := &types.LLMRequest{
@@ -101,7 +101,7 @@ func TestPrefixPlugin(t *testing.T) {
 	assert.Equal(t, float64(2)/float64(3), scores[pod1], "score should be 2/3 - the model and the first prefix block match")
 	assert.Equal(t, float64(0), scores[pod2], "score for pod2")
 
-	plugin.PostCycle(context.Background(), cycleState3, &types.ProfileRunResult{TargetPod: pod1})
+	plugin.PostCycle(context.Background(), cycleState3, &types.ProfileRunResult{TargetPods: []types.Pod{pod1}})
 
 	// 4th request is same as req3 except the model is different, still no match.
 	req4 := &types.LLMRequest{
@@ -120,7 +120,7 @@ func TestPrefixPlugin(t *testing.T) {
 	assert.Equal(t, float64(0), scores[pod1], "score for pod1")
 	assert.Equal(t, float64(0), scores[pod2], "score for pod2")
 
-	plugin.PostCycle(context.Background(), cycleState4, &types.ProfileRunResult{TargetPod: pod1})
+	plugin.PostCycle(context.Background(), cycleState4, &types.ProfileRunResult{TargetPods: []types.Pod{pod1}})
 
 	// 5th request shares partial prefix with 3rd one.
 	req5 := &types.LLMRequest{
@@ -139,7 +139,7 @@ func TestPrefixPlugin(t *testing.T) {
 	assert.Equal(t, 0.75, scores[pod1], "score should be 0.75 - the model and the first 2 prefix blocks match")
 	assert.Equal(t, float64(0), scores[pod2], "score for pod2")
 
-	plugin.PostCycle(context.Background(), cycleState5, &types.ProfileRunResult{TargetPod: pod1})
+	plugin.PostCycle(context.Background(), cycleState5, &types.ProfileRunResult{TargetPods: []types.Pod{pod1}})
 }
 
 // TestPrefixPluginStress is a stress test for the prefix scoring plugin, using prompts of increasing length.
@@ -180,7 +180,7 @@ func BenchmarkPrefixPluginStress(b *testing.B) {
 		// First cycle: simulate scheduling and insert prefix info into the cache
 		cycleState := types.NewCycleState()
 		plugin.Score(context.Background(), cycleState, req, pods)
-		plugin.PostCycle(context.Background(), cycleState, &types.ProfileRunResult{TargetPod: pod})
+		plugin.PostCycle(context.Background(), cycleState, &types.ProfileRunResult{TargetPods: []types.Pod{pod}})
 
 		// Second cycle: validate internal state
 		state, err := types.ReadCycleStateKey[*SchedulingContextState](cycleState, PrefixCachePluginType)
diff --git a/pkg/epp/scheduling/framework/plugins/picker/picker_test.go b/pkg/epp/scheduling/framework/plugins/picker/picker_test.go
index 189687d..2089ed3 100644
--- a/pkg/epp/scheduling/framework/plugins/picker/picker_test.go
+++ b/pkg/epp/scheduling/framework/plugins/picker/picker_test.go
@@ -20,75 +20,100 @@ import (
 	"context"
 	"testing"
 
+	"github.com/google/go-cmp/cmp"
+	k8stypes "k8s.io/apimachinery/pkg/types"
+
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/framework"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/types"
-
-	k8stypes "k8s.io/apimachinery/pkg/types"
 )
 
 func TestPickMaxScorePicker(t *testing.T) {
+	pod1 := &types.PodMetrics{Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod1"}}}
+	pod2 := &types.PodMetrics{Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod2"}}}
+	pod3 := &types.PodMetrics{Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod3"}}}
+
 	tests := []struct {
-		name        string
-		scoredPods  []*types.ScoredPod
-		wantNames   []string
-		shouldPanic bool
+		name   string
+		picker framework.Picker
+		input  []*types.ScoredPod
+		output []types.Pod
 	}{
 		{
-			name: "Single max score",
-			scoredPods: []*types.ScoredPod{
-				{Pod: &types.PodMetrics{Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod1"}}}, Score: 10},
-				{Pod: &types.PodMetrics{Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod2"}}}, Score: 25},
-				{Pod: &types.PodMetrics{Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod3"}}}, Score: 15},
+			name:   "Single max score",
+			picker: NewMaxScorePicker(1),
+			input: []*types.ScoredPod{
+				{Pod: pod1, Score: 10},
+				{Pod: pod2, Score: 25},
+				{Pod: pod3, Score: 15},
+			},
+			output: []types.Pod{
+				&types.ScoredPod{Pod: pod2, Score: 25},
 			},
-			wantNames: []string{"pod2"},
 		},
 		{
-			name: "Multiple max scores",
-			scoredPods: []*types.ScoredPod{
-				{Pod: &types.PodMetrics{Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "podA"}}}, Score: 50},
-				{Pod: &types.PodMetrics{Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "podB"}}}, Score: 50},
-				{Pod: &types.PodMetrics{Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "podC"}}}, Score: 30},
+			name:   "Multiple max scores, all are equally scored",
+			picker: NewMaxScorePicker(2),
+			input: []*types.ScoredPod{
+				{Pod: pod1, Score: 50},
+				{Pod: pod2, Score: 50},
+				{Pod: pod3, Score: 30},
+			},
+			output: []types.Pod{
+				&types.ScoredPod{Pod: pod1, Score: 50},
+				&types.ScoredPod{Pod: pod2, Score: 50},
 			},
-			wantNames: []string{"podA", "podB"},
 		},
 		{
-			name:        "Empty pod list",
-			scoredPods:  []*types.ScoredPod{},
-			wantNames:   nil,
-			shouldPanic: true,
+			name:   "Multiple results sorted by highest score, more pods than needed",
+			picker: NewMaxScorePicker(2),
+			input: []*types.ScoredPod{
+				{Pod: pod1, Score: 20},
+				{Pod: pod2, Score: 25},
+				{Pod: pod3, Score: 30},
+			},
+			output: []types.Pod{
+				&types.ScoredPod{Pod: pod3, Score: 30},
+				&types.ScoredPod{Pod: pod2, Score: 25},
+			},
+		},
+		{
+			name:   "Multiple results sorted by highest score, less pods than needed",
+			picker: NewMaxScorePicker(4), // picker is required to return 4 pods at most, but we have only 3.
+			input: []*types.ScoredPod{
+				{Pod: pod1, Score: 20},
+				{Pod: pod2, Score: 25},
+				{Pod: pod3, Score: 30},
+			},
+			output: []types.Pod{
+				&types.ScoredPod{Pod: pod3, Score: 30},
+				&types.ScoredPod{Pod: pod2, Score: 25},
+				&types.ScoredPod{Pod: pod1, Score: 20},
+			},
+		},
+		{
+			name:   "Multiple results sorted by highest score, num of pods exactly needed",
+			picker: NewMaxScorePicker(3), // picker is required to return 3 pods at most, we have only 3.
+			input: []*types.ScoredPod{
+				{Pod: pod1, Score: 30},
+				{Pod: pod2, Score: 25},
+				{Pod: pod3, Score: 30},
+			},
+			output: []types.Pod{
+				&types.ScoredPod{Pod: pod1, Score: 30},
+				&types.ScoredPod{Pod: pod3, Score: 30},
+				&types.ScoredPod{Pod: pod2, Score: 25},
+			},
 		},
 	}
 
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			if tt.shouldPanic {
-				defer func() {
-					if r := recover(); r == nil {
-						t.Errorf("expected panic but did not get one")
-					}
-				}()
-			}
-
-			p := NewMaxScorePicker()
-			result := p.Pick(context.Background(), nil, tt.scoredPods)
-
-			if len(tt.scoredPods) == 0 && result != nil {
-				t.Errorf("expected nil result for empty input, got %+v", result)
-				return
-			}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			result := test.picker.Pick(context.Background(), types.NewCycleState(), test.input)
+			got := result.TargetPods
 
-			if result != nil {
-				got := result.TargetPod.GetPod().NamespacedName.Name
-				match := false
-				for _, want := range tt.wantNames {
-					if got == want {
-						match = true
-						break
-					}
-				}
-				if !match {
-					t.Errorf("got %q, want one of %v", got, tt.wantNames)
-				}
+			if diff := cmp.Diff(test.output, got); diff != "" {
+				t.Errorf("Unexpected output (-want +got): %v", diff)
 			}
 		})
 	}
diff --git a/pkg/epp/scheduling/framework/plugins/test/README.md b/pkg/epp/scheduling/framework/plugins/test/README.md
new file mode 100644
index 0000000..00c05db
--- /dev/null
+++ b/pkg/epp/scheduling/framework/plugins/test/README.md
@@ -0,0 +1 @@
+This package contains plugins implementation for test purpose only.
diff --git a/conformance/testing-epp/plugins/filter/filter_test.go b/pkg/epp/scheduling/framework/plugins/test/filter/filter_test.go
similarity index 67%
rename from conformance/testing-epp/plugins/filter/filter_test.go
rename to pkg/epp/scheduling/framework/plugins/test/filter/filter_test.go
index 2c00821..1eb1291 100644
--- a/conformance/testing-epp/plugins/filter/filter_test.go
+++ b/pkg/epp/scheduling/framework/plugins/test/filter/filter_test.go
@@ -22,7 +22,6 @@ import (
 
 	"github.com/google/go-cmp/cmp"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend"
-	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/framework"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/types"
 )
 
@@ -30,14 +29,12 @@ func TestFilter(t *testing.T) {
 	tests := []struct {
 		name   string
 		req    *types.LLMRequest
-		filter framework.Filter
 		input  []types.Pod
 		output []types.Pod
 	}{
 		{
-			name:   "TestHeaderBasedFilter, header endpoint unset in request",
-			req:    &types.LLMRequest{}, // Delieverately unset the header.
-			filter: &HeaderBasedTestingFilter{},
+			name: "TestHeaderBasedFilter, header endpoint unset in request",
+			req:  &types.LLMRequest{}, // Delieverately unset the header.
 			input: []types.Pod{
 				&types.PodMetrics{
 					Pod: &backend.Pod{
@@ -48,9 +45,8 @@ func TestFilter(t *testing.T) {
 			output: []types.Pod{},
 		},
 		{
-			name:   "TestHeaderBasedFilter, header endpoint set in request but no match",
-			req:    &types.LLMRequest{Headers: map[string]string{headerTestEppEndPointSelectionKey: "test-endpoint"}},
-			filter: &HeaderBasedTestingFilter{},
+			name: "TestHeaderBasedFilter, header endpoint set in request but no match",
+			req:  &types.LLMRequest{Headers: map[string]string{headerTestEppEndPointSelectionKey: "test-endpoint"}},
 			input: []types.Pod{
 				&types.PodMetrics{
 					Pod: &backend.Pod{
@@ -61,9 +57,8 @@ func TestFilter(t *testing.T) {
 			output: []types.Pod{},
 		},
 		{
-			name:   "TestHeaderBasedFilter, header endpoint set",
-			req:    &types.LLMRequest{Headers: map[string]string{headerTestEppEndPointSelectionKey: "test-endpoint"}},
-			filter: &HeaderBasedTestingFilter{},
+			name: "TestHeaderBasedFilter, header endpoint set",
+			req:  &types.LLMRequest{Headers: map[string]string{headerTestEppEndPointSelectionKey: "test-endpoint"}},
 			input: []types.Pod{
 				&types.PodMetrics{
 					Pod: &backend.Pod{
@@ -80,9 +75,8 @@ func TestFilter(t *testing.T) {
 			},
 		},
 		{
-			name:   "TestHeaderBasedFilter, multiple header endpoints set and multiple matches",
-			req:    &types.LLMRequest{Headers: map[string]string{headerTestEppEndPointSelectionKey: "test-endpoint3,test-endpoint2"}},
-			filter: &HeaderBasedTestingFilter{},
+			name: "TestHeaderBasedFilter, multiple header endpoints set and multiple matches",
+			req:  &types.LLMRequest{Headers: map[string]string{headerTestEppEndPointSelectionKey: "test-endpoint3,test-endpoint2"}},
 			input: []types.Pod{
 				&types.PodMetrics{
 					Pod: &backend.Pod{
@@ -117,7 +111,7 @@ func TestFilter(t *testing.T) {
 
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
-			got := test.filter.Filter(context.Background(), types.NewCycleState(), test.req, test.input)
+			got := NewHeaderBasedTestingFilter().Filter(context.Background(), types.NewCycleState(), test.req, test.input)
 
 			if diff := cmp.Diff(test.output, got); diff != "" {
 				t.Errorf("Unexpected output (-want +got): %v", diff)
diff --git a/conformance/testing-epp/plugins/filter/request_header_based_filter.go b/pkg/epp/scheduling/framework/plugins/test/filter/request_header_based_filter.go
similarity index 78%
rename from conformance/testing-epp/plugins/filter/request_header_based_filter.go
rename to pkg/epp/scheduling/framework/plugins/test/filter/request_header_based_filter.go
index 75039d9..4295b5f 100644
--- a/conformance/testing-epp/plugins/filter/request_header_based_filter.go
+++ b/pkg/epp/scheduling/framework/plugins/test/filter/request_header_based_filter.go
@@ -18,6 +18,7 @@ package filter
 
 import (
 	"context"
+	"encoding/json"
 	"strings"
 
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/plugins"
@@ -31,27 +32,40 @@ const (
 	// E.g., "test-epp-endpoint-selection": "10.0.0.7,10.0.0.8"
 	// The returned order is the same as the order provided in the header.
 	headerTestEppEndPointSelectionKey = "test-epp-endpoint-selection"
+	// HeaderBasedTestingFilterType is the filter type that is used in plugins registry.
+	HeaderBasedTestingFilterType = "header-based-testing-filter"
 )
 
 // compile-time type assertion
 var _ framework.Filter = &HeaderBasedTestingFilter{}
 
+// HeaderBasedTestingFilterFactory defines the factory function for HeaderBasedTestingFilter.
+func HeaderBasedTestingFilterFactory(name string, _ json.RawMessage, _ plugins.Handle) (plugins.Plugin, error) {
+	return NewHeaderBasedTestingFilter().WithName(name), nil
+}
+
 // NewHeaderBasedTestingFilter initializes a new HeaderBasedTestingFilter.
 // This should only be used for testing purposes.
 func NewHeaderBasedTestingFilter() *HeaderBasedTestingFilter {
 	return &HeaderBasedTestingFilter{
-		tn: plugins.TypedName{Type: "header-based-testing", Name: "header-based-testing-filter"},
+		typedName: plugins.TypedName{Type: HeaderBasedTestingFilterType, Name: HeaderBasedTestingFilterType},
 	}
 }
 
 // HeaderBasedTestingFilter filters Pods based on an address specified in the "test-epp-endpoint-selection" request header.
 type HeaderBasedTestingFilter struct {
-	tn plugins.TypedName
+	typedName plugins.TypedName
 }
 
 // TypedName returns the type and name tuple of this plugin instance.
 func (f *HeaderBasedTestingFilter) TypedName() plugins.TypedName {
-	return f.tn
+	return f.typedName
+}
+
+// WithName sets the name of the filter.
+func (f *HeaderBasedTestingFilter) WithName(name string) *HeaderBasedTestingFilter {
+	f.typedName.Name = name
+	return f
 }
 
 // Filter selects pods that match the IP addresses specified in the request header.
diff --git a/pkg/epp/scheduling/framework/scheduler_profile_test.go b/pkg/epp/scheduling/framework/scheduler_profile_test.go
index a2adcca..020223c 100644
--- a/pkg/epp/scheduling/framework/scheduler_profile_test.go
+++ b/pkg/epp/scheduling/framework/scheduler_profile_test.go
@@ -25,27 +25,34 @@ import (
 	k8stypes "k8s.io/apimachinery/pkg/types"
 
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend"
-	backendmetrics "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend/metrics" // Import config for thresholds
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/plugins"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/types"
 )
 
 func TestSchedulePlugins(t *testing.T) {
-	tp1 := newTestPlugin("test1", 0.3,
-		[]k8stypes.NamespacedName{{Name: "pod1"}, {Name: "pod2"}, {Name: "pod3"}},
-		k8stypes.NamespacedName{})
-	tp2 := newTestPlugin("test2", 0.8,
-		[]k8stypes.NamespacedName{{Name: "pod1"}, {Name: "pod2"}},
-		k8stypes.NamespacedName{})
-	tp_filterAll := newTestPlugin("filter all", 0.0,
-		[]k8stypes.NamespacedName{}, k8stypes.NamespacedName{})
-	pickerPlugin := newTestPlugin("picker", 0.0,
-		[]k8stypes.NamespacedName{}, k8stypes.NamespacedName{Name: "pod1"})
+	tp1 := &testPlugin{
+		TypeRes:   "test1",
+		ScoreRes:  0.3,
+		FilterRes: []k8stypes.NamespacedName{{Name: "pod1"}, {Name: "pod2"}, {Name: "pod3"}},
+	}
+	tp2 := &testPlugin{
+		TypeRes:   "test2",
+		ScoreRes:  0.8,
+		FilterRes: []k8stypes.NamespacedName{{Name: "pod1"}, {Name: "pod2"}},
+	}
+	tp_filterAll := &testPlugin{
+		TypeRes:   "filter all",
+		FilterRes: []k8stypes.NamespacedName{},
+	}
+	pickerPlugin := &testPlugin{
+		TypeRes: "picker",
+		PickRes: k8stypes.NamespacedName{Name: "pod1"},
+	}
 
 	tests := []struct {
 		name           string
 		profile        *SchedulerProfile
-		input          []backendmetrics.PodMetrics
+		input          []types.Pod
 		wantTargetPod  k8stypes.NamespacedName
 		targetPodScore float64
 		// Number of expected pods to score (after filter)
@@ -59,10 +66,10 @@ func TestSchedulePlugins(t *testing.T) {
 				WithScorers(NewWeightedScorer(tp1, 1), NewWeightedScorer(tp2, 1)).
 				WithPicker(pickerPlugin).
 				WithPostCyclePlugins(tp1, tp2),
-			input: []backendmetrics.PodMetrics{
-				&backendmetrics.FakePodMetrics{Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod1"}}},
-				&backendmetrics.FakePodMetrics{Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod2"}}},
-				&backendmetrics.FakePodMetrics{Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod3"}}},
+			input: []types.Pod{
+				&types.PodMetrics{Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod1"}}},
+				&types.PodMetrics{Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod2"}}},
+				&types.PodMetrics{Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod3"}}},
 			},
 			wantTargetPod:  k8stypes.NamespacedName{Name: "pod1"},
 			targetPodScore: 1.1,
@@ -76,10 +83,10 @@ func TestSchedulePlugins(t *testing.T) {
 				WithScorers(NewWeightedScorer(tp1, 60), NewWeightedScorer(tp2, 40)).
 				WithPicker(pickerPlugin).
 				WithPostCyclePlugins(tp1, tp2),
-			input: []backendmetrics.PodMetrics{
-				&backendmetrics.FakePodMetrics{Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod1"}}},
-				&backendmetrics.FakePodMetrics{Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod2"}}},
-				&backendmetrics.FakePodMetrics{Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod3"}}},
+			input: []types.Pod{
+				&types.PodMetrics{Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod1"}}},
+				&types.PodMetrics{Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod2"}}},
+				&types.PodMetrics{Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod3"}}},
 			},
 			wantTargetPod:  k8stypes.NamespacedName{Name: "pod1"},
 			targetPodScore: 50,
@@ -93,10 +100,10 @@ func TestSchedulePlugins(t *testing.T) {
 				WithScorers(NewWeightedScorer(tp1, 1), NewWeightedScorer(tp2, 1)).
 				WithPicker(pickerPlugin).
 				WithPostCyclePlugins(tp1, tp2),
-			input: []backendmetrics.PodMetrics{
-				&backendmetrics.FakePodMetrics{Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod1"}}},
-				&backendmetrics.FakePodMetrics{Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod2"}}},
-				&backendmetrics.FakePodMetrics{Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod3"}}},
+			input: []types.Pod{
+				&types.PodMetrics{Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod1"}}},
+				&types.PodMetrics{Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod2"}}},
+				&types.PodMetrics{Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod3"}}},
 			},
 			numPodsToScore: 0,
 			err:            true, // no available pods to server after filter all
@@ -123,7 +130,7 @@ func TestSchedulePlugins(t *testing.T) {
 				RequestId:   uuid.NewString(),
 			}
 			// Run profile cycle
-			got, err := test.profile.Run(context.Background(), request, types.NewCycleState(), types.ToSchedulerPodMetrics(test.input))
+			got, err := test.profile.Run(context.Background(), request, types.NewCycleState(), test.input)
 
 			// Validate error state
 			if test.err != (err != nil) {
@@ -135,11 +142,12 @@ func TestSchedulePlugins(t *testing.T) {
 			}
 
 			// Validate output
-			wantPod := &types.PodMetrics{
-				Pod: &backend.Pod{NamespacedName: test.wantTargetPod, Labels: make(map[string]string)},
-			}
 			wantRes := &types.ProfileRunResult{
-				TargetPod: wantPod,
+				TargetPods: []types.Pod{
+					&types.PodMetrics{
+						Pod: &backend.Pod{NamespacedName: test.wantTargetPod},
+					},
+				},
 			}
 
 			if diff := cmp.Diff(wantRes, got); diff != "" {
@@ -149,32 +157,32 @@ func TestSchedulePlugins(t *testing.T) {
 			for _, plugin := range test.profile.filters {
 				tp, _ := plugin.(*testPlugin)
 				if tp.FilterCallCount != 1 {
-					t.Errorf("Plugin %s Filter() called %d times, expected 1", plugin.TypedName(), tp.FilterCallCount)
+					t.Errorf("Plugin '%s' Filter() called %d times, expected 1", plugin.TypedName(), tp.FilterCallCount)
 				}
 			}
 			for _, plugin := range test.profile.scorers {
 				tp, _ := plugin.Scorer.(*testPlugin)
 				if tp.ScoreCallCount != 1 {
-					t.Errorf("Plugin %s Score() called %d times, expected 1", plugin.TypedName(), tp.ScoreCallCount)
+					t.Errorf("Plugin '%s' Score() called %d times, expected 1", plugin.TypedName(), tp.ScoreCallCount)
 				}
 				if test.numPodsToScore != tp.NumOfScoredPods {
-					t.Errorf("Plugin %s Score() called with %d pods, expected %d", plugin.TypedName(), tp.NumOfScoredPods, test.numPodsToScore)
+					t.Errorf("Plugin '%s' Score() called with %d pods, expected %d", plugin.TypedName(), tp.NumOfScoredPods, test.numPodsToScore)
 				}
 			}
 			tp, _ := test.profile.picker.(*testPlugin)
 			if tp.NumOfPickerCandidates != test.numPodsToScore {
-				t.Errorf("Picker plugin %s Pick() called with %d candidates, expected %d", tp.TypedName(), tp.NumOfPickerCandidates, tp.NumOfScoredPods)
+				t.Errorf("Picker plugin '%s' Pick() called with %d candidates, expected %d", tp.TypedName(), tp.NumOfPickerCandidates, tp.NumOfScoredPods)
 			}
 			if tp.PickCallCount != 1 {
-				t.Errorf("Picker plugin %s Pick() called %d times, expected 1", tp.TypedName(), tp.PickCallCount)
+				t.Errorf("Picker plugin '%s' Pick() called %d times, expected 1", tp.TypedName(), tp.PickCallCount)
 			}
 			if tp.WinnerPodScore != test.targetPodScore {
 				t.Errorf("winner pod score %v, expected %v", tp.WinnerPodScore, test.targetPodScore)
 			}
 			for _, plugin := range test.profile.postCyclePlugins {
 				tp, _ := plugin.(*testPlugin)
-				if tp.PostScheduleCallCount != 1 {
-					t.Errorf("Plugin %s PostSchedule() called %d times, expected 1", plugin.TypedName(), tp.PostScheduleCallCount)
+				if tp.PostCycleCallCount != 1 {
+					t.Errorf("Plugin '%s' PostCycle() called %d times, expected 1", plugin.TypedName(), tp.PostCycleCallCount)
 				}
 			}
 		})
@@ -189,32 +197,22 @@ var _ PostCycle = &testPlugin{}
 
 // testPlugin is an implementation useful in unit tests.
 type testPlugin struct {
-	tn                    plugins.TypedName
+	typedName             plugins.TypedName
 	TypeRes               string
 	ScoreCallCount        int
 	NumOfScoredPods       int
 	ScoreRes              float64
 	FilterCallCount       int
 	FilterRes             []k8stypes.NamespacedName
-	PostScheduleCallCount int
+	PostCycleCallCount    int
 	PickCallCount         int
 	NumOfPickerCandidates int
 	PickRes               k8stypes.NamespacedName
 	WinnerPodScore        float64
 }
 
-func newTestPlugin(typeRes string, score float64, pruned []k8stypes.NamespacedName,
-	target k8stypes.NamespacedName) *testPlugin {
-	return &testPlugin{
-		tn:        plugins.TypedName{Type: typeRes, Name: "test-plugin"},
-		ScoreRes:  score,
-		FilterRes: pruned,
-		PickRes:   target,
-	}
-}
-
 func (tp *testPlugin) TypedName() plugins.TypedName {
-	return tp.tn
+	return tp.typedName
 }
 
 func (tp *testPlugin) Filter(_ context.Context, _ *types.CycleState, _ *types.LLMRequest, pods []types.Pod) []types.Pod {
@@ -237,26 +235,26 @@ func (tp *testPlugin) Pick(_ context.Context, _ *types.CycleState, scoredPods []
 	tp.PickCallCount++
 	tp.NumOfPickerCandidates = len(scoredPods)
 
-	var winnerPod types.Pod
+	winnerPods := []types.Pod{}
 	for _, scoredPod := range scoredPods {
 		if scoredPod.GetPod().NamespacedName.String() == tp.PickRes.String() {
-			winnerPod = scoredPod.Pod
+			winnerPods = append(winnerPods, scoredPod.Pod)
 			tp.WinnerPodScore = scoredPod.Score
 		}
 	}
 
-	return &types.ProfileRunResult{TargetPod: winnerPod}
+	return &types.ProfileRunResult{TargetPods: winnerPods}
 }
 
 func (tp *testPlugin) PostCycle(_ context.Context, _ *types.CycleState, res *types.ProfileRunResult) {
-	tp.PostScheduleCallCount++
+	tp.PostCycleCallCount++
 }
 
 func (tp *testPlugin) reset() {
 	tp.FilterCallCount = 0
 	tp.ScoreCallCount = 0
 	tp.NumOfScoredPods = 0
-	tp.PostScheduleCallCount = 0
+	tp.PostCycleCallCount = 0
 	tp.PickCallCount = 0
 	tp.NumOfPickerCandidates = 0
 }
diff --git a/pkg/epp/scheduling/scheduler_test.go b/pkg/epp/scheduling/scheduler_test.go
index 720a6b4..8923c3d 100644
--- a/pkg/epp/scheduling/scheduler_test.go
+++ b/pkg/epp/scheduling/scheduler_test.go
@@ -25,40 +25,79 @@ import (
 	k8stypes "k8s.io/apimachinery/pkg/types"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend"
 	backendmetrics "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend/metrics" // Import config for thresholds
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/config"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/framework"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/framework/plugins/filter"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/framework/plugins/picker"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/framework/plugins/profile"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/types"
 )
 
 // Tests the default scheduler configuration and expected behavior.
 func TestSchedule(t *testing.T) {
+	loraAffinityFilter := filter.NewLoraAffinityFilter(config.Conf.LoraAffinityThreshold)
+	leastQueueFilter := filter.NewLeastQueueFilter()
+	leastKvCacheFilter := filter.NewLeastKVCacheFilter()
+
+	lowLatencyFilter := &filter.DecisionTreeFilter{
+		Current: filter.NewLowQueueFilter(config.Conf.QueueingThresholdLoRA),
+		NextOnSuccess: &filter.DecisionTreeFilter{
+			Current: loraAffinityFilter,
+			NextOnSuccessOrFailure: &filter.DecisionTreeFilter{
+				Current: leastQueueFilter,
+				NextOnSuccessOrFailure: &filter.DecisionTreeFilter{
+					Current: leastKvCacheFilter,
+				},
+			},
+		},
+		NextOnFailure: &filter.DecisionTreeFilter{
+			Current: leastQueueFilter,
+			NextOnSuccessOrFailure: &filter.DecisionTreeFilter{
+				Current: loraAffinityFilter,
+				NextOnSuccessOrFailure: &filter.DecisionTreeFilter{
+					Current: leastKvCacheFilter,
+				},
+			},
+		},
+	}
+
+	defaultProfile := framework.NewSchedulerProfile().
+		WithFilters(lowLatencyFilter).
+		WithPicker(picker.NewRandomPicker(picker.DefaultMaxNumOfEndpoints))
+
+	profileHandler := profile.NewSingleProfileHandler()
+
+	schedulerConfig := NewSchedulerConfig(profileHandler, map[string]*framework.SchedulerProfile{"default": defaultProfile})
+
 	tests := []struct {
 		name    string
 		req     *types.LLMRequest
-		input   []backendmetrics.PodMetrics
+		input   []types.Pod
 		wantRes *types.SchedulingResult
 		err     bool
 	}{
 		{
 			name: "no candidate pods",
 			req: &types.LLMRequest{
-				TargetModel: "any-model",
 				RequestId:   uuid.NewString(),
+				TargetModel: "any-model",
 			},
-			input:   []backendmetrics.PodMetrics{},
+			input:   []types.Pod{},
 			wantRes: nil,
 			err:     true,
 		},
 		{
 			name: "finds optimal pod",
 			req: &types.LLMRequest{
-				TargetModel: "critical",
 				RequestId:   uuid.NewString(),
+				TargetModel: "critical",
 			},
 			// pod2 will be picked because it has relatively low queue size, with the requested
 			// model being active, and has low KV cache.
-			input: []backendmetrics.PodMetrics{
-				&backendmetrics.FakePodMetrics{
+			input: []types.Pod{
+				&types.PodMetrics{
 					Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod1"}},
-					Metrics: &backendmetrics.MetricsState{
+					MetricsState: &backendmetrics.MetricsState{
 						WaitingQueueSize:    0,
 						KVCacheUsagePercent: 0.2,
 						MaxActiveModels:     2,
@@ -68,9 +107,9 @@ func TestSchedule(t *testing.T) {
 						},
 					},
 				},
-				&backendmetrics.FakePodMetrics{
+				&types.PodMetrics{
 					Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod2"}},
-					Metrics: &backendmetrics.MetricsState{
+					MetricsState: &backendmetrics.MetricsState{
 						WaitingQueueSize:    3,
 						KVCacheUsagePercent: 0.1,
 						MaxActiveModels:     2,
@@ -80,9 +119,9 @@ func TestSchedule(t *testing.T) {
 						},
 					},
 				},
-				&backendmetrics.FakePodMetrics{
+				&types.PodMetrics{
 					Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod3"}},
-					Metrics: &backendmetrics.MetricsState{
+					MetricsState: &backendmetrics.MetricsState{
 						WaitingQueueSize:    10,
 						KVCacheUsagePercent: 0.2,
 						MaxActiveModels:     2,
@@ -95,18 +134,19 @@ func TestSchedule(t *testing.T) {
 			wantRes: &types.SchedulingResult{
 				ProfileResults: map[string]*types.ProfileRunResult{
 					"default": {
-						TargetPod: &types.ScoredPod{
-							Pod: &types.PodMetrics{
-								Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod2"}, Labels: make(map[string]string)},
-								MetricsState: &backendmetrics.MetricsState{
-									WaitingQueueSize:    3,
-									KVCacheUsagePercent: 0.1,
-									MaxActiveModels:     2,
-									ActiveModels: map[string]int{
-										"foo":      1,
-										"critical": 1,
+						TargetPods: []types.Pod{
+							&types.ScoredPod{
+								Pod: &types.PodMetrics{
+									Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod2"}},
+									MetricsState: &backendmetrics.MetricsState{
+										WaitingQueueSize:    3,
+										KVCacheUsagePercent: 0.1,
+										MaxActiveModels:     2,
+										ActiveModels: map[string]int{
+											"foo":      1,
+											"critical": 1,
+										},
 									},
-									WaitingModels: map[string]int{},
 								},
 							},
 						},
@@ -119,8 +159,8 @@ func TestSchedule(t *testing.T) {
 
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
-			scheduler := NewScheduler()
-			got, err := scheduler.Schedule(context.Background(), test.req, types.ToSchedulerPodMetrics(test.input))
+			scheduler := NewSchedulerWithConfig(schedulerConfig)
+			got, err := scheduler.Schedule(context.Background(), test.req, test.input)
 			if test.err != (err != nil) {
 				t.Errorf("Unexpected error, got %v, want %v", err, test.err)
 			}
diff --git a/test/integration/epp/hermetic_test.go b/test/integration/epp/hermetic_test.go
index 6d439d1..3a3bd9e 100644
--- a/test/integration/epp/hermetic_test.go
+++ b/test/integration/epp/hermetic_test.go
@@ -50,11 +50,13 @@ import (
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/cache"
 	k8sclient "sigs.k8s.io/controller-runtime/pkg/client"
-	"sigs.k8s.io/controller-runtime/pkg/config"
+	crconfig "sigs.k8s.io/controller-runtime/pkg/config"
 	"sigs.k8s.io/controller-runtime/pkg/envtest"
 	crmetrics "sigs.k8s.io/controller-runtime/pkg/metrics"
 	"sigs.k8s.io/controller-runtime/pkg/metrics/filters"
 	metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
+	"sigs.k8s.io/yaml"
+
 	"sigs.k8s.io/gateway-api-inference-extension/api/v1alpha2"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend"
 	backendmetrics "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend/metrics"
@@ -63,11 +65,15 @@ import (
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/requestcontrol"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/saturationdetector"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/config"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/framework"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/framework/plugins/filter"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/framework/plugins/picker"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/framework/plugins/profile"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/server"
 	logutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/logging"
 	epptestutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/testing"
 	integrationutils "sigs.k8s.io/gateway-api-inference-extension/test/integration"
-	"sigs.k8s.io/yaml"
 )
 
 const (
@@ -1018,7 +1024,41 @@ func BeforeSuite() func() {
 	// Adjust from defaults
 	serverRunner.PoolNamespacedName = types.NamespacedName{Name: testPoolName, Namespace: testNamespace}
 	serverRunner.Datastore = datastore.NewDatastore(context.Background(), pmf)
-	scheduler := scheduling.NewScheduler()
+
+	loraAffinityFilter := filter.NewLoraAffinityFilter(config.Conf.LoraAffinityThreshold)
+	leastQueueFilter := filter.NewLeastQueueFilter()
+	leastKvCacheFilter := filter.NewLeastKVCacheFilter()
+
+	lowLatencyFilter := &filter.DecisionTreeFilter{
+		Current: filter.NewLowQueueFilter(config.Conf.QueueingThresholdLoRA),
+		NextOnSuccess: &filter.DecisionTreeFilter{
+			Current: loraAffinityFilter,
+			NextOnSuccessOrFailure: &filter.DecisionTreeFilter{
+				Current: leastQueueFilter,
+				NextOnSuccessOrFailure: &filter.DecisionTreeFilter{
+					Current: leastKvCacheFilter,
+				},
+			},
+		},
+		NextOnFailure: &filter.DecisionTreeFilter{
+			Current: leastQueueFilter,
+			NextOnSuccessOrFailure: &filter.DecisionTreeFilter{
+				Current: loraAffinityFilter,
+				NextOnSuccessOrFailure: &filter.DecisionTreeFilter{
+					Current: leastKvCacheFilter,
+				},
+			},
+		},
+	}
+
+	defaultProfile := framework.NewSchedulerProfile().
+		WithFilters(lowLatencyFilter).
+		WithPicker(picker.NewRandomPicker(picker.DefaultMaxNumOfEndpoints))
+
+	profileHandler := profile.NewSingleProfileHandler()
+
+	schedulerConfig := scheduling.NewSchedulerConfig(profileHandler, map[string]*framework.SchedulerProfile{"default": defaultProfile})
+	scheduler := scheduling.NewSchedulerWithConfig(schedulerConfig)
 
 	sdConfig := &saturationdetector.Config{
 		QueueDepthThreshold:       saturationdetector.DefaultQueueDepthThreshold,
@@ -1125,7 +1165,7 @@ func managerTestOptions(namespace, name string, metricsServerOptions metricsserv
 				},
 			},
 		},
-		Controller: config.Controller{
+		Controller: crconfig.Controller{
 			SkipNameValidation: boolPointer(true),
 		},
 		Metrics: metricsServerOptions,
diff --git a/test/testdata/inferencepool-e2e.yaml b/test/testdata/inferencepool-e2e.yaml
index e92a1e0..9e7fd6a 100644
--- a/test/testdata/inferencepool-e2e.yaml
+++ b/test/testdata/inferencepool-e2e.yaml
@@ -50,21 +50,20 @@ spec:
         image: $E2E_IMAGE
         imagePullPolicy: IfNotPresent
         args:
-        - -poolName
+        - --pool-name
         - "vllm-llama3-8b-instruct"
-        - -poolNamespace
+        - --pool-namespace
         - "$E2E_NS"
-        - -v
+        - --v
         - "4"
         - --zap-encoder
         - "json"
-        - -grpcPort
+        - --grpc-port
         - "9002"
-        - -grpcHealthPort
+        - --grpc-health-port
         - "9003"
-        env:
-        - name: USE_STREAMING
-          value: "true"
+        - "--config-file"
+        - "/config/default-plugins.yaml"
         ports:
         - containerPort: 9002
         - containerPort: 9003
@@ -82,6 +81,95 @@ spec:
             service: inference-extension
           initialDelaySeconds: 5
           periodSeconds: 10
+        volumeMounts:
+        - name: plugins-config-volume
+          mountPath: "/config"
+      volumes:
+      - name: plugins-config-volume
+        configMap:
+          name: plugins-config
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: plugins-config
+  namespace: $E2E_NS
+data:
+  default-plugins.yaml: |
+    apiVersion: inference.networking.x-k8s.io/v1alpha1
+    kind: EndpointPickerConfig
+    plugins:
+    - type: low-queue-filter
+      parameters:
+        threshold: 128
+    - type: lora-affinity-filter
+      parameters:
+        threshold: 0.999
+    - type: least-queue-filter
+    - type: least-kv-cache-filter
+    - type: decision-tree-filter
+      name: low-latency-filter
+      parameters:
+        current:
+          pluginRef: low-queue-filter
+        nextOnSuccess:
+          decisionTree:
+            current:
+              pluginRef: lora-affinity-filter
+            nextOnSuccessOrFailure:
+              decisionTree:
+                current:
+                  pluginRef: least-queue-filter
+                nextOnSuccessOrFailure:
+                  decisionTree:
+                    current:
+                      pluginRef: least-kv-cache-filter
+        nextOnFailure:
+          decisionTree:
+            current:
+              pluginRef: least-queue-filter
+            nextOnSuccessOrFailure:
+              decisionTree:
+                current:
+                  pluginRef: lora-affinity-filter
+                nextOnSuccessOrFailure:
+                  decisionTree:
+                    current:
+                      pluginRef: least-kv-cache-filter
+    - type: random-picker
+      parameters:
+        maxNumOfEndpoints: 1
+    - type: single-profile-handler
+    schedulingProfiles:
+    - name: default
+      plugins:
+      - pluginRef: low-latency-filter
+      - pluginRef: random-picker
+  plugins-v2.yaml: |
+    apiVersion: inference.networking.x-k8s.io/v1alpha1
+    kind: EndpointPickerConfig
+    plugins:
+    - type: queue-scorer
+    - type: kv-cache-scorer
+    - type: prefix-cache-scorer
+      parameters:
+        hashBlockSize: 64
+        maxPrefixBlocksToMatch: 256
+        lruCapacityPerServer: 31250
+    - type: max-score-picker
+      parameters:
+        maxNumOfEndpoints: 1
+    - type: single-profile-handler
+    schedulingProfiles:
+    - name: default
+      plugins:
+      - pluginRef: queue-scorer
+        weight: 1
+      - pluginRef: kv-cache-scorer
+        weight: 1
+      - pluginRef: prefix-cache-scorer
+        weight: 1
+      - pluginRef: max-score-picker
 ---
 kind: ClusterRole
 apiVersion: rbac.authorization.k8s.io/v1
diff --git a/test/utils/handle.go b/test/utils/handle.go
index 417346f..4a29dda 100644
--- a/test/utils/handle.go
+++ b/test/utils/handle.go
@@ -24,8 +24,8 @@ import (
 
 // testHandle is an implmentation of plugins.Handle for test purposes
 type testHandle struct {
-	ctx     context.Context
-	plugins plugins.HandlePlugins
+	ctx context.Context
+	plugins.HandlePlugins
 }
 
 // Context returns a context the plugins can use, if they need one
@@ -33,39 +33,35 @@ func (h *testHandle) Context() context.Context {
 	return h.ctx
 }
 
-func (h *testHandle) Plugins() plugins.HandlePlugins {
-	return h.plugins
-}
-
 type testHandlePlugins struct {
-	thePlugins map[string]plugins.Plugin
+	plugins map[string]plugins.Plugin
 }
 
 func (h *testHandlePlugins) Plugin(name string) plugins.Plugin {
-	return h.thePlugins[name]
+	return h.plugins[name]
 }
 
 func (h *testHandlePlugins) AddPlugin(name string, plugin plugins.Plugin) {
-	h.thePlugins[name] = plugin
+	h.plugins[name] = plugin
 }
 
 func (h *testHandlePlugins) GetAllPlugins() []plugins.Plugin {
 	result := make([]plugins.Plugin, 0)
-	for _, plugin := range h.thePlugins {
+	for _, plugin := range h.plugins {
 		result = append(result, plugin)
 	}
 	return result
 }
 
 func (h *testHandlePlugins) GetAllPluginsWithNames() map[string]plugins.Plugin {
-	return h.thePlugins
+	return h.plugins
 }
 
 func NewTestHandle(ctx context.Context) plugins.Handle {
 	return &testHandle{
 		ctx: ctx,
-		plugins: &testHandlePlugins{
-			thePlugins: map[string]plugins.Plugin{},
+		HandlePlugins: &testHandlePlugins{
+			plugins: map[string]plugins.Plugin{},
 		},
 	}
 }